Initial commit
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python
|
||||
"""Live-streaming spectrum viewer for an Ocean Optics spectrometer.
|
||||
|
||||
Opens a matplotlib window and continuously redraws the current spectrum.
|
||||
|
||||
Usage: python live_spectrum.py [integration_time_us]
|
||||
|
||||
Keys (while the plot window is focused):
|
||||
q / Esc quit
|
||||
a toggle autoscale of the y-axis on/off
|
||||
d capture the current spectrum as a dark reference
|
||||
x clear the dark reference
|
||||
space freeze / unfreeze acquisition
|
||||
t toggle a live Planck temperature fit (use --response for a real T)
|
||||
"""
|
||||
import argparse
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.animation import FuncAnimation
|
||||
from seabreeze.spectrometers import Spectrometer
|
||||
|
||||
from response import add_calibration_args, apply_response, dark_at, load_csv
|
||||
from fit_temperature import fit_planck
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("integration_us", nargs="?", type=int, default=50000,
|
||||
help="integration time in microseconds (default: 50000)")
|
||||
parser.add_argument("--interval", type=int, default=30,
|
||||
help="redraw interval in ms (default: 30)")
|
||||
parser.add_argument("--fit-band", nargs=2, type=float, default=[420.0, 800.0],
|
||||
metavar=("LO", "HI"),
|
||||
help="band (nm) for the live temperature fit (default: 420 800)")
|
||||
add_calibration_args(parser)
|
||||
args = parser.parse_args()
|
||||
|
||||
cal = load_csv(args.response) if args.response else None
|
||||
dark = dark_at(args.dark, args.integration_us)
|
||||
|
||||
EDGE_PIXELS = 5 # ignore the fixed edge artifacts (e.g. the ~340 nm pixel-1 spike)
|
||||
|
||||
spec = Spectrometer.from_first_available()
|
||||
spec.integration_time_micros(args.integration_us)
|
||||
wl = spec.wavelengths()
|
||||
print(f"{spec.model} serial={spec.serial_number} pixels={spec.pixels}")
|
||||
print(f"range {wl[0]:.1f}-{wl[-1]:.1f} nm integration {args.integration_us} us")
|
||||
if cal is not None:
|
||||
print(f"applied response calibration: {args.response}")
|
||||
print("keys: q quit | a autoscale | d dark | x clear dark | space freeze | t fit T")
|
||||
|
||||
state = {"autoscale": True, "frozen": False, "dark": None, "fit": False}
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 5))
|
||||
(line,) = ax.plot(wl, np.zeros_like(wl), lw=0.8)
|
||||
# Two lines for the fit: the full-range curve (extrapolated) and, on top, the
|
||||
# segment over the band actually used to constrain it.
|
||||
(fit_full,) = ax.plot([], [], lw=1.2, color="C1")
|
||||
(fit_band,) = ax.plot([], [], lw=2.0, color="C3")
|
||||
fit_full.set_visible(False)
|
||||
fit_band.set_visible(False)
|
||||
ax.set_xlim(wl[0], wl[-1])
|
||||
ax.set_ylim(0, 4096)
|
||||
ax.set_xlabel("wavelength (nm)")
|
||||
ax.set_ylabel("relative irradiance" if cal is not None else "intensity (counts)")
|
||||
title = ax.set_title("")
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
|
||||
def on_key(event):
|
||||
if event.key in ("q", "escape"):
|
||||
plt.close(fig)
|
||||
elif event.key == "a":
|
||||
state["autoscale"] = not state["autoscale"]
|
||||
elif event.key == " ":
|
||||
state["frozen"] = not state["frozen"]
|
||||
elif event.key == "d":
|
||||
state["dark"] = spec.intensities(correct_nonlinearity=True)
|
||||
print("dark reference captured")
|
||||
elif event.key == "x":
|
||||
state["dark"] = None
|
||||
print("dark reference cleared")
|
||||
elif event.key == "t":
|
||||
state["fit"] = not state["fit"]
|
||||
if state["fit"] and cal is None:
|
||||
print("fitting uncalibrated counts; use --response for a meaningful T")
|
||||
|
||||
|
||||
fig.canvas.mpl_connect("key_press_event", on_key)
|
||||
|
||||
|
||||
def _hide_fit():
|
||||
fit_full.set_visible(False)
|
||||
fit_band.set_visible(False)
|
||||
if ax.get_legend():
|
||||
ax.get_legend().remove()
|
||||
|
||||
|
||||
def update(_frame):
|
||||
if state["frozen"]:
|
||||
return line, fit_full, fit_band, title
|
||||
y = spec.intensities(correct_nonlinearity=True)
|
||||
if state["dark"] is not None:
|
||||
y = y - state["dark"]
|
||||
if cal is not None:
|
||||
y = apply_response(wl, y, cal, dark)
|
||||
line.set_ydata(y)
|
||||
if state["autoscale"]:
|
||||
ymin, ymax = float(np.nanmin(y)), float(np.nanmax(y))
|
||||
pad = max(1e-9, 0.05 * (ymax - ymin))
|
||||
ax.set_ylim(ymin - pad, ymax + pad)
|
||||
# Peak over the interior only, so the fixed edge spikes never win.
|
||||
pk = EDGE_PIXELS + int(np.nanargmax(y[EDGE_PIXELS:-EDGE_PIXELS]))
|
||||
if state["fit"]:
|
||||
result = fit_planck(wl, y, tuple(args.fit_band))
|
||||
if result is None:
|
||||
_hide_fit()
|
||||
else:
|
||||
temperature, dT, predict = result
|
||||
lo, hi = args.fit_band
|
||||
model = predict(wl)
|
||||
in_band = (wl >= lo) & (wl <= hi)
|
||||
fit_full.set_data(wl, model) # extrapolated over the full range
|
||||
fit_band.set_data(wl[in_band], model[in_band]) # region used for the fit
|
||||
note = "" if cal is not None else " (uncalibrated)"
|
||||
fit_full.set_label("Planck fit (extrapolated)")
|
||||
fit_band.set_label(f"used {lo:.0f}-{hi:.0f} nm, "
|
||||
f"T = {temperature:.2f} +/- {dT:.2f} K{note}")
|
||||
fit_full.set_visible(True)
|
||||
fit_band.set_visible(True)
|
||||
ax.legend(loc="upper right", fontsize=9)
|
||||
else:
|
||||
_hide_fit()
|
||||
flags = []
|
||||
if state["dark"] is not None:
|
||||
flags.append("dark-sub")
|
||||
if state["frozen"]:
|
||||
flags.append("FROZEN")
|
||||
if not state["autoscale"]:
|
||||
flags.append("fixed-y")
|
||||
suffix = (" [" + " ".join(flags) + "]") if flags else ""
|
||||
title.set_text(f"peak {wl[pk]:.2f} nm = {y[pk]:.2f}{suffix}")
|
||||
return line, fit_full, fit_band, title
|
||||
|
||||
|
||||
# blit=False because autoscale mutates the axes limits each frame.
|
||||
ani = FuncAnimation(fig, update, interval=args.interval, blit=False,
|
||||
cache_frame_data=False)
|
||||
|
||||
try:
|
||||
plt.show()
|
||||
finally:
|
||||
spec.close()
|
||||
Reference in New Issue
Block a user