@@ -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