Skip to content

Commit 202e934

Browse files
ramp to zero added for safety
1 parent 1b74f50 commit 202e934

1 file changed

Lines changed: 91 additions & 66 deletions

File tree

pica/keysight/CV_KE4980A_GUI.py

Lines changed: 91 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,22 @@ def safe_ramp_dc_bias(self, target_v, step=0.5, dwell=0.1):
162162
self.instrument.write(f":BIAS:VOLT {v:.3f}")
163163
time.sleep(dwell)
164164

165+
def force_zero_bias(self):
166+
"""Emergency safety: aggressively drive bias to 0V.
167+
Swallows exceptions so it can be used in error handlers safely."""
168+
if not self.instrument:
169+
return
170+
try:
171+
try:
172+
# Try a fast ramp first
173+
self.safe_ramp_dc_bias(0.0, step=1.0, dwell=0.05)
174+
except Exception:
175+
# If ramp fails (e.g. VISA timeout), force a hard write
176+
self.instrument.write(":BIAS:VOLT 0")
177+
time.sleep(0.2)
178+
except Exception as e:
179+
print(f" CRITICAL SAFETY WARNING: Failed to force bias to 0V! {e}")
180+
165181
def set_bias_voltage(self, v):
166182
"""Ramp DC bias to v. Final hardware-protection clamp lives here."""
167183
if abs(v) > V_ABS_MAX: # defense in depth — should never trigger
@@ -195,90 +211,90 @@ def initialize_instrument(self, p):
195211
inst.write_termination = "\n"
196212
self.instrument = inst
197213

198-
idn = inst.query("*IDN?").strip()
199-
if "E4980" not in idn:
200-
inst.close()
201-
raise ConnectionError(f"Not an E4980A: {idn}")
202-
203-
self.has_opt001 = "001" in inst.query("*OPT?")
204-
205-
# --- Hardcoded safety ceilings (do not modify) ---
206-
if not self.has_opt001:
207-
raise RuntimeError(
208-
"CV sweeps require Option 001 (continuous DC bias). "
209-
"Standard unit only supports discrete 0/1.5/2 V bias."
210-
)
211-
for key in ("v_start", "v_stop"):
212-
if abs(p[key]) > V_ABS_MAX:
214+
try:
215+
idn = inst.query("*IDN?").strip()
216+
if "E4980" not in idn:
217+
inst.close()
218+
raise ConnectionError(f"Not an E4980A: {idn}")
219+
220+
self.has_opt001 = "001" in inst.query("*OPT?")
221+
222+
# --- Hardcoded safety ceilings (do not modify) ---
223+
if not self.has_opt001:
224+
raise RuntimeError(
225+
"CV sweeps require Option 001 (continuous DC bias). "
226+
"Standard unit only supports discrete 0/1.5/2 V bias."
227+
)
228+
for key in ("v_start", "v_stop"):
229+
if abs(p[key]) > V_ABS_MAX:
230+
raise ValueError(
231+
f"|{key}| = {abs(p[key])} V exceeds hardcoded "
232+
f"{V_ABS_MAX} V safety cutoff."
233+
)
234+
if not (0 < p["ac_bias"] <= V_AC_MAX):
213235
raise ValueError(
214-
f"|{key}| = {abs(p[key])} V exceeds hardcoded "
215-
f"{V_ABS_MAX} V safety cutoff."
236+
f"AC level must be in (0, {V_AC_MAX}] Vrms."
216237
)
217-
if not (0 < p["ac_bias"] <= V_AC_MAX):
218-
raise ValueError(
219-
f"AC level must be in (0, {V_AC_MAX}] Vrms."
220-
)
221238

222-
# Instrument configuration
223-
inst.write("*RST; *CLS")
224-
time.sleep(1.0) # Graceful reset
225-
inst.write(":DISP:ENAB ON")
226-
time.sleep(0.2)
239+
# Instrument configuration
240+
inst.write("*RST; *CLS")
241+
time.sleep(1.0) # Graceful reset
242+
inst.write(":DISP:ENAB ON")
243+
time.sleep(0.2)
227244

228-
inst.write(":FUNC:IMP RX")
229-
inst.write(f":APER {p['aper']}")
230-
inst.write(":FUNC:IMP:RANG:AUTO ON")
231-
time.sleep(0.2)
245+
inst.write(":FUNC:IMP RX")
246+
inst.write(f":APER {p['aper']}")
247+
inst.write(":FUNC:IMP:RANG:AUTO ON")
248+
time.sleep(0.2)
232249

233-
inst.write(":FORM ASC")
250+
inst.write(":FORM ASC")
234251

235-
inst.write(":FUNC:SMON:VAC ON")
236-
inst.write(":FUNC:SMON:IAC ON")
237-
inst.write(":FUNC:SMON:VDC OFF")
238-
inst.write(":FUNC:SMON:IDC OFF")
239-
time.sleep(0.2)
252+
inst.write(":FUNC:SMON:VAC ON")
253+
inst.write(":FUNC:SMON:IAC ON")
254+
inst.write(":FUNC:SMON:VDC OFF")
255+
inst.write(":FUNC:SMON:IDC OFF")
256+
time.sleep(0.2)
240257

241-
if p["alc_enabled"]:
242-
inst.write(":AMPL:ALC ON")
243-
else:
244-
inst.write(":AMPL:ALC OFF")
245-
time.sleep(0.2)
258+
if p["alc_enabled"]:
259+
inst.write(":AMPL:ALC ON")
260+
else:
261+
inst.write(":AMPL:ALC OFF")
262+
time.sleep(0.2)
246263

247-
inst.write(f":CORR:LENG {p['cable_len']}")
248-
if p["corr_enabled"]:
249-
inst.write(":CORR:OPEN:STAT ON")
250-
inst.write(":CORR:SHOR:STAT ON")
251-
else:
252-
inst.write(":CORR:OPEN:STAT OFF")
253-
inst.write(":CORR:SHOR:STAT OFF")
254-
time.sleep(0.2)
264+
inst.write(f":CORR:LENG {p['cable_len']}")
265+
if p["corr_enabled"]:
266+
inst.write(":CORR:OPEN:STAT ON")
267+
inst.write(":CORR:SHOR:STAT ON")
268+
else:
269+
inst.write(":CORR:OPEN:STAT OFF")
270+
inst.write(":CORR:SHOR:STAT OFF")
271+
time.sleep(0.2)
255272

256-
inst.write(f":VOLT {p['ac_bias']}")
257-
time.sleep(0.5) # Let AC level settle
273+
inst.write(f":VOLT {p['ac_bias']}")
274+
time.sleep(0.5) # Let AC level settle
258275

259-
inst.write(":TRIG:SOUR BUS")
260-
inst.write(":INIT:CONT ON")
261-
time.sleep(0.2)
276+
inst.write(":TRIG:SOUR BUS")
277+
inst.write(":INIT:CONT ON")
278+
time.sleep(0.2)
262279

263-
# CV mode: start at 0 V with bias output enabled; sweep sets values.
264-
inst.write(":BIAS:VOLT 0")
265-
inst.write(":BIAS:STAT ON")
266-
time.sleep(0.5)
280+
# CV mode: start at 0 V with bias output enabled; sweep sets values.
281+
inst.write(":BIAS:VOLT 0")
282+
inst.write(":BIAS:STAT ON")
283+
time.sleep(0.5)
267284

268-
self._check_errors("configuration")
269-
print(f" Connected & configured (RX/CV mode): {idn}")
285+
self._check_errors("configuration")
286+
print(f" Connected & configured (RX/CV mode): {idn}")
287+
except Exception:
288+
# If init fails after bias is turned on, force 0V before raising
289+
self.force_zero_bias()
290+
raise
270291

271292
def close_instrument(self):
272293
print("--- [Backend] Closing instrument connection. ---")
273294
if not self.instrument:
274295
return
275296
try:
276-
if self.has_opt001:
277-
print(" Ramping bias to zero and turning off...")
278-
self.safe_ramp_dc_bias(0.0)
279-
else:
280-
self.instrument.write(":BIAS:VOLT 0")
281-
time.sleep(0.5)
297+
self.force_zero_bias() # Safety: always ramp to 0V
282298
self.instrument.write(":BIAS:STAT OFF")
283299
self.instrument.write(":DISP:PAGE MEAS")
284300
time.sleep(0.2)
@@ -1030,6 +1046,9 @@ def start_sweep(self):
10301046

10311047
except Exception as e:
10321048
self.log(f"ERROR during startup: {traceback.format_exc()}")
1049+
# Safety: if init failed after bias was turned on, force 0V
1050+
if self.backend.instrument:
1051+
self.backend.force_zero_bias()
10331052
messagebox.showerror(
10341053
"Initialization Error",
10351054
f"Could not start sweep.\n\n{e}",
@@ -1092,6 +1111,8 @@ def _sweep_loop(self):
10921111
if not self.stop_event.is_set():
10931112
self.data_queue.put(("DONE", None, None, None, None, None))
10941113
except Exception as e:
1114+
# CRITICAL SAFETY: drive voltage to zero on any worker crash
1115+
self.backend.force_zero_bias()
10951116
self.data_queue.put(("ERROR", e, None, None, None, None))
10961117

10971118
def _open_new_file(self, freq):
@@ -1263,6 +1284,10 @@ def _handle_sweep_completion(self):
12631284

12641285
def _handle_sweep_error(self, exception):
12651286
self.log(f"RUNTIME ERROR: {traceback.format_exc()}")
1287+
# Explicitly call force_zero_bias just to be absolutely certain
1288+
# the worker thread's safety routine executed.
1289+
if self.backend.instrument:
1290+
self.backend.force_zero_bias()
12661291
self.stop_sweep(
12671292
"A critical hardware or measurement error occurred."
12681293
)

0 commit comments

Comments
 (0)