updates
This commit is contained in:
175
flicker_watch.py
175
flicker_watch.py
@@ -49,8 +49,16 @@ LP_V_SCALE = 0.2
|
||||
LP_V_OFFSET = 0.6
|
||||
LP_TRIG_LEVEL = 0.6
|
||||
|
||||
# Segmented memory: capture N back-to-back LP triggers per :DIGitize, then
|
||||
# dump the whole acquisition as a single H5 file. Massively higher coverage
|
||||
# than single-shot CSV captures.
|
||||
SEGMENT_COUNT = 100
|
||||
SAVE_FORMAT = "H5" # Keysight native multi-segment format
|
||||
|
||||
CYCLE_S = 10.0 # seconds video is on per cycle
|
||||
TRIG_TIMEOUT_S = 2.0 # per-capture trigger wait
|
||||
# Filling N segments takes ~N × LP-trigger period. LP triggers fire roughly
|
||||
# at line rate (≈48 kHz) so 100 segments fill in ms, but allow margin.
|
||||
TRIG_TIMEOUT_S = max(SEGMENT_COUNT * 0.020 + 5.0, 10.0)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scope setup
|
||||
@@ -84,7 +92,7 @@ def setup_scope() -> None:
|
||||
|
||||
|
||||
def configure_for_lp() -> None:
|
||||
"""LP-mode: widen vertical range, falling-edge trigger on Ch3."""
|
||||
"""LP-mode + segmented memory: N back-to-back LP triggers per acquisition."""
|
||||
for ch in (1, 2, 3, 4):
|
||||
scope.write(f":CHANnel{ch}:SCALe {LP_V_SCALE:.3f}")
|
||||
scope.write(f":CHANnel{ch}:OFFSet {LP_V_OFFSET:.3f}")
|
||||
@@ -95,7 +103,10 @@ def configure_for_lp() -> None:
|
||||
scope.write(f":TIMebase:SCALe {LP_SCALE:.3E}")
|
||||
scope.write(f":ACQuire:POINts {LP_POINTS}")
|
||||
scope.write(f":TIMebase:POSition {LP_TRIG_OFFSET:.2E}")
|
||||
time.sleep(0.3)
|
||||
# Segmented memory: fill N segments per :DIGitize.
|
||||
scope.write(":ACQuire:MODE SEGMented")
|
||||
scope.write(f":ACQuire:SEGMented:COUNt {SEGMENT_COUNT}")
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
def arm_and_wait(timeout_s: float) -> bool:
|
||||
@@ -128,12 +139,13 @@ def arm_and_wait(timeout_s: float) -> bool:
|
||||
|
||||
|
||||
def save_lp(base_name: str) -> None:
|
||||
"""Save Ch1 (CLK+) and Ch3 (DAT0+) as CSV to scope's C:\\TEMP\\."""
|
||||
"""Save all N segments of Ch1 (CLK+) and Ch3 (DAT0+) as a single H5 each."""
|
||||
base = f"C:\\TEMP\\{base_name}"
|
||||
scope.write(f':DISK:SAVE:WAVeform CHANnel1,"{base}_clk.csv",CSV')
|
||||
time.sleep(2.5)
|
||||
scope.write(f':DISK:SAVE:WAVeform CHANnel3,"{base}_dat.csv",CSV')
|
||||
time.sleep(2.5)
|
||||
ext = SAVE_FORMAT.lower()
|
||||
scope.write(f':DISK:SAVE:WAVeform CHANnel1,"{base}_clk.{ext}",{SAVE_FORMAT}')
|
||||
time.sleep(3.0)
|
||||
scope.write(f':DISK:SAVE:WAVeform CHANnel3,"{base}_dat.{ext}",{SAVE_FORMAT}')
|
||||
time.sleep(3.0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -174,6 +186,48 @@ def video_stop() -> None:
|
||||
print(f" VIDEO STOP failed: {e}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# H5 transfer (ai_mgmt only handles CSV — segmented mode produces .h5)
|
||||
# ---------------------------------------------------------------------------
|
||||
def _transfer_h5_files() -> int:
|
||||
"""SMB-pull every .h5 from the scope share into DATA_DIR; delete on scope."""
|
||||
from smb.SMBConnection import SMBConnection
|
||||
import socket
|
||||
conn = SMBConnection(
|
||||
ai_mgmt.USERNAME, ai_mgmt.PASSWORD,
|
||||
socket.gethostname(), ai_mgmt.SERVER_NAME,
|
||||
use_ntlm_v2=True, is_direct_tcp=True,
|
||||
)
|
||||
if not conn.connect(ai_mgmt.SERVER, 445):
|
||||
print(" H5 transfer: could not connect to scope share")
|
||||
return 0
|
||||
count = 0
|
||||
try:
|
||||
h5_paths: list[str] = []
|
||||
def walk(path: str) -> None:
|
||||
for entry in conn.listPath(ai_mgmt.SHARE, path):
|
||||
if entry.filename in (".", ".."):
|
||||
continue
|
||||
full = f"{path}/{entry.filename}"
|
||||
if entry.isDirectory:
|
||||
walk(full)
|
||||
elif entry.filename.lower().endswith(".h5"):
|
||||
h5_paths.append(full)
|
||||
walk("/")
|
||||
for remote in h5_paths:
|
||||
local = DATA_DIR / Path(remote).name
|
||||
try:
|
||||
with open(local, "wb") as fh:
|
||||
conn.retrieveFile(ai_mgmt.SHARE, remote, fh)
|
||||
conn.deleteFiles(ai_mgmt.SHARE, remote)
|
||||
count += 1
|
||||
except Exception as e:
|
||||
print(f" H5 transfer failed for {Path(remote).name}: {e}")
|
||||
finally:
|
||||
conn.close()
|
||||
return count
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Register snapshot from device (DSIM PHY + SN65DSI83)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -251,37 +305,90 @@ def archive_and_analyse(event: str, since_iso: str) -> None:
|
||||
return
|
||||
print(f" {copied} file(s) transferred ({failed} failed)")
|
||||
|
||||
# Move just-arrived CSVs out of data/ (flat) into the event folder.
|
||||
# ai_mgmt only fetches CSVs. H5 (segmented) files need a separate pass.
|
||||
h5_count = _transfer_h5_files()
|
||||
if h5_count:
|
||||
print(f" {h5_count} H5 file(s) transferred")
|
||||
|
||||
# Move just-arrived files (csv + h5) out of data/ (flat) into the event folder.
|
||||
moved = 0
|
||||
for csv in DATA_DIR.glob("*.csv"):
|
||||
if csv.is_file():
|
||||
shutil.move(str(csv), target / csv.name)
|
||||
for f in list(DATA_DIR.glob("*.csv")) + list(DATA_DIR.glob("*.h5")):
|
||||
if f.is_file():
|
||||
shutil.move(str(f), target / f.name)
|
||||
moved += 1
|
||||
print(f" {moved} file(s) archived to {target.relative_to(DATA_DIR.parent)}")
|
||||
|
||||
# Explode each H5 into per-segment CSVs so csv_preprocessor can analyse them.
|
||||
from explode_h5 import explode
|
||||
h5_files = sorted(target.glob("*_lp_*.h5"))
|
||||
seg_csv_count = 0
|
||||
for h5 in h5_files:
|
||||
try:
|
||||
csvs = explode(h5)
|
||||
seg_csv_count += len(csvs)
|
||||
except Exception as e:
|
||||
print(f" EXPLODE error on {h5.name}: {e}")
|
||||
if h5_files:
|
||||
print(f" exploded {len(h5_files)} H5 file(s) → {seg_csv_count} segment CSV(s)")
|
||||
|
||||
if event != "flicker":
|
||||
return
|
||||
|
||||
# Analyse the LP captures we just archived.
|
||||
print("\n LP analysis (csv_preprocessor):")
|
||||
print(" " + "-" * 78)
|
||||
print(f" {'file':<46} {'lp_low_ns':>10} {'hs_amp_mV':>10} {'flicker?':>9}")
|
||||
print(" " + "-" * 78)
|
||||
|
||||
lp_files = sorted(target.glob("*_lp_*_dat.csv"))
|
||||
for f in lp_files:
|
||||
# Analyse every segment CSV. Flag outliers.
|
||||
print("\n Per-segment LP analysis:")
|
||||
rows = []
|
||||
for f in sorted(target.glob("*_lp_*_dat.csv")):
|
||||
try:
|
||||
m = analyze_lp_file(f)
|
||||
lp_low = getattr(m, "lp_low_duration_ns", None)
|
||||
hs_amp = getattr(m, "hs_amp_mV", None)
|
||||
sus = getattr(m, "flicker_suspect", False)
|
||||
print(f" {f.name:<46} "
|
||||
f"{(f'{lp_low:.1f}' if lp_low is not None else '?'):>10} "
|
||||
f"{(f'{hs_amp:.1f}' if hs_amp is not None else '?'):>10} "
|
||||
f"{('YES' if sus else 'no'):>9}")
|
||||
rows.append({
|
||||
"file": f.name,
|
||||
"lp_low": float(m.lp_low_duration_ns) if m.lp_low_duration_ns is not None else None,
|
||||
"hs_amp": float(m.hs_amplitude_mv) if m.hs_amplitude_mv is not None else None,
|
||||
"hs_dur": float(m.hs_burst_dur_ns) if m.hs_burst_dur_ns is not None else None,
|
||||
"n_burst": int(m.n_hs_bursts) if m.n_hs_bursts is not None else None,
|
||||
"sus": bool(m.flicker_suspect),
|
||||
})
|
||||
except Exception as e:
|
||||
print(f" {f.name:<46} ERROR: {e}")
|
||||
print(" " + "-" * 78)
|
||||
rows.append({"file": f.name, "error": str(e)})
|
||||
|
||||
n_total = len(rows)
|
||||
n_sus = sum(1 for r in rows if r.get("sus"))
|
||||
print(f" {n_total} segments analysed ({n_sus} flagged as flicker_suspect)")
|
||||
|
||||
# Outlier search across the segments themselves.
|
||||
def _outliers(field: str, lo_thresh: float | None = None,
|
||||
hi_thresh: float | None = None) -> list[dict]:
|
||||
vals = sorted(r[field] for r in rows if r.get(field) is not None)
|
||||
if not vals:
|
||||
return []
|
||||
med = vals[len(vals) // 2]
|
||||
out = []
|
||||
for r in rows:
|
||||
v = r.get(field)
|
||||
if v is None: continue
|
||||
far = (lo_thresh is not None and v < lo_thresh) or \
|
||||
(hi_thresh is not None and v > hi_thresh)
|
||||
if far:
|
||||
out.append({"file": r["file"], field: v, "median": med})
|
||||
return out
|
||||
|
||||
print("\n Anomalies vs segment-set median:")
|
||||
for label, field, lo, hi in [
|
||||
("very-short LP-low (<50 ns)", "lp_low", 50, None),
|
||||
("very-low HS amplitude (<50 mV)", "hs_amp", 50, None),
|
||||
("very-high HS amplitude (>140 mV)","hs_amp", None, 140),
|
||||
("short HS burst (<8000 ns)", "hs_dur", 8000, None),
|
||||
]:
|
||||
ax = _outliers(field, lo, hi)
|
||||
if ax:
|
||||
print(f" {label}: {len(ax)} segment(s)")
|
||||
for x in ax[:8]:
|
||||
print(f" {x['file']} {field}={x[field]:.1f} "
|
||||
f"(set median={x['median']:.1f})")
|
||||
if len(ax) > 8:
|
||||
print(f" ... +{len(ax) - 8} more")
|
||||
else:
|
||||
print(f" {label}: none")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -310,7 +417,8 @@ def main() -> None:
|
||||
|
||||
video_start()
|
||||
print(f"\n[cycle {cycle:03d} {cycle_ts}] video ON "
|
||||
f"({CYCLE_S:.0f}s window)", flush=True)
|
||||
f"({CYCLE_S:.0f}s window, {SEGMENT_COUNT} segs/acquire)",
|
||||
flush=True)
|
||||
|
||||
event = None
|
||||
last_tick = 0.0
|
||||
@@ -323,8 +431,8 @@ def main() -> None:
|
||||
try:
|
||||
save_lp(base)
|
||||
cycle_caps.append(base)
|
||||
print(f" + cap {seq:02d} [{remaining():4.1f}s left]",
|
||||
flush=True)
|
||||
print(f" + acq {seq:02d} ({SEGMENT_COUNT} segs) "
|
||||
f"[{remaining():4.1f}s left]", flush=True)
|
||||
except Exception as e:
|
||||
print(f" save error: {e}", flush=True)
|
||||
else:
|
||||
@@ -342,7 +450,8 @@ def main() -> None:
|
||||
video_stop()
|
||||
if event is None:
|
||||
print(f"[cycle {cycle:03d}] ended "
|
||||
f"({len(cycle_caps)} cap(s), no event)",
|
||||
f"({len(cycle_caps)} acq(s) ≈ "
|
||||
f"{len(cycle_caps) * SEGMENT_COUNT} segments, no event)",
|
||||
flush=True)
|
||||
|
||||
if event == "f":
|
||||
|
||||
Reference in New Issue
Block a user