44 Measurement (Keysight E4980A + Lakeshore 350).
55Original Authors: Prathamesh Deshmukh (template programs)
66Integrated by: AI-assisted merge per design specification
7- Version: V: 1.0
7+ Version: V: 1.1
88"""
99
1010# ===============================================================================
@@ -305,6 +305,7 @@ def perform_measurement(self, freq, delay):
305305 self .instrument .write (f":FREQ { freq } " )
306306 time .sleep (delay )
307307 self .instrument .write (":TRIG:IMM" )
308+ self .instrument .query ("*OPC?" ) # added for robustness on LAN
308309 vals = self .instrument .query_ascii_values (":FETC?" )
309310 R , X = vals [0 ], vals [1 ]
310311 status = int (vals [2 ]) if len (vals ) > 2 else 0
@@ -345,6 +346,7 @@ def __init__(self):
345346 self .lakeshore = None
346347 self .lcr = LCR_Backend ()
347348 self .params = {}
349+ self .SAFETY_KILL_TEMP_K = 350.0 # Hard safety ceiling
348350
349351 def initialize_instruments (self , parameters ):
350352 self .params = parameters
@@ -357,7 +359,20 @@ def initialize_instruments(self, parameters):
357359 # corr_enabled, cable_len
358360 self .lcr .initialize_instrument (parameters )
359361
360- def measure_frequency_sweep (self , frequencies , delay ):
362+ def check_safety_kill (self , temperature_k ):
363+ if temperature_k >= self .SAFETY_KILL_TEMP_K :
364+ for attempt in range (3 ):
365+ try :
366+ self .lakeshore .set_heater_range (1 , 'off' )
367+ break
368+ except Exception as e :
369+ print (f"Kill attempt { attempt + 1 } failed: { e } " )
370+ time .sleep (0.5 )
371+ print (f"!!! SAFETY KILL: T={ temperature_k :.3f} K. Heater OFF." )
372+ return True
373+ return False
374+
375+ def measure_frequency_sweep (self , frequencies , delay , stop_event = None ):
361376 """
362377 Appendix A.1 — CRITICAL:
363378 Reads Lakeshore temperature INSIDE the frequency loop so every
@@ -366,11 +381,21 @@ def measure_frequency_sweep(self, frequencies, delay):
366381 """
367382 htr = self .lakeshore .get_heater_output (1 )
368383 points = []
384+ killed = False
369385 for f in frequencies :
386+ if stop_event is not None and stop_event .is_set ():
387+ break # abort mid-sweep, cleanly
370388 temp = self .lakeshore .get_temperature ('A' ) # T for THIS point
389+
390+ # Issue #2: per-point safety kill check
391+ if temp >= self .SAFETY_KILL_TEMP_K :
392+ killed = self .check_safety_kill (temp )
393+ points .append ((temp , f , float ('nan' ), float ('nan' ), - 1 ))
394+ break
395+
371396 R , X , status = self .lcr .perform_measurement (f , delay )
372397 points .append ((temp , f , R , X , status ))
373- return {'heater' : htr , 'points' : points }
398+ return {'heater' : htr , 'points' : points , 'killed' : killed }
374399
375400 def close_instruments (self ):
376401 print ("\n --- [Backend] Closing all instrument connections. ---" )
@@ -392,7 +417,7 @@ class Integrated_CT_GUI:
392417 LCR measurement.
393418 """
394419
395- PROGRAM_VERSION = "1.0 "
420+ PROGRAM_VERSION = "1.1 "
396421 LOGO_SIZE = 110
397422
398423 # --- Default frequency list (Section 4 of original instructions) ---
@@ -454,6 +479,9 @@ def __init__(self, root):
454479 self ._last_draw_time = 0.0
455480 self ._redraw_interval = 0.25 # seconds; redraw at most ~4×/sec
456481
482+ # --- Stop Event (Issue #1 Fix) ---
483+ self .stop_event = threading .Event ()
484+
457485 # --- Backend ---
458486 self .backend = Combined_Backend ()
459487 atexit .register (self .backend .close_instruments )
@@ -977,7 +1005,7 @@ def calculate_impedance_parameters(self, f, R, X):
9771005
9781006 # Complex capacitance C* = C' - jC''
9791007 Cp_double_prime = G / omega_safe
980- Cs_double_prime = Cp_double_prime
1008+ Cs_double_prime = D * Cs # Fix Issue #7: Physically correct series loss
9811009
9821010 return [
9831011 Q , # 0
@@ -1096,6 +1124,7 @@ def start_measurement(self):
10961124
10971125 # --- Reset state ---
10981126 self .is_stabilizing , self .is_running = True , False
1127+ self .stop_event .clear () # Issue #1: Clear stop event
10991128 self .start_button .config (state = 'disabled' )
11001129 self .stop_button .config (state = 'normal' )
11011130 self .scan_button .config (state = 'disabled' )
@@ -1197,18 +1226,29 @@ def _on_plot_freq_change(self, event=None):
11971226
11981227 # ------------------------------------------------------------------
11991228 def stop_measurement (self , from_user = True ):
1200- if self .is_running or self .is_stabilizing :
1201- self .is_running , self .is_stabilizing = False , False
1202- if from_user :
1203- self .log ("Measurement stopped by user." )
1204- self .start_button .config (state = 'normal' )
1205- self .stop_button .config (state = 'disabled' )
1206- self .scan_button .config (state = 'normal' )
1207- self .backend .close_instruments ()
1208- if from_user :
1209- messagebox .showinfo (
1210- "Info" ,
1211- "Measurement stopped and instruments disconnected." )
1229+ # Issue #1 Fix: Wait for worker to finish before touching VISA
1230+ if not (self .is_running or self .is_stabilizing ):
1231+ return
1232+ self .is_running , self .is_stabilizing = False , False
1233+ self .stop_event .set ()
1234+ if from_user :
1235+ self .log ("Stop requested; waiting for worker to finish..." )
1236+
1237+ # Wait for the worker to exit BEFORE touching the VISA sessions.
1238+ if self .measurement_thread and self .measurement_thread .is_alive ():
1239+ self .measurement_thread .join (timeout = 15.0 )
1240+ if self .measurement_thread .is_alive ():
1241+ self .log ("WARNING: worker did not exit in 15 s; "
1242+ "closing sessions anyway." )
1243+
1244+ self .start_button .config (state = 'normal' )
1245+ self .stop_button .config (state = 'disabled' )
1246+ self .scan_button .config (state = 'normal' )
1247+ self .backend .close_instruments ()
1248+ if from_user :
1249+ self .log ("Measurement stopped and instruments disconnected." )
1250+ messagebox .showinfo (
1251+ "Info" , "Measurement stopped and instruments disconnected." )
12121252
12131253 # ==================================================================
12141254 # WORKER THREAD (Section 7, amended by Appendix A.1)
@@ -1217,7 +1257,7 @@ def _measurement_worker(self):
12171257 params = self .backend .params
12181258 try :
12191259 # --- Stabilization Phase (verbatim from R-T template) ---
1220- while self .is_stabilizing :
1260+ while self .is_stabilizing and not self . stop_event . is_set () :
12211261 current_temp = self .backend .lakeshore .get_temperature ('A' )
12221262 self .data_queue .put (
12231263 f"LOG:Stabilizing... Current: { current_temp :.4f} K "
@@ -1234,14 +1274,14 @@ def _measurement_worker(self):
12341274 self .data_queue .put (
12351275 f"LOG:Stabilized at { current_temp :.4f} K. "
12361276 f"Waiting 5s before ramp..." )
1237- time . sleep (5 )
1277+ self . stop_event . wait (5 )
12381278 self .is_stabilizing = False
12391279 self .is_running = True
12401280 break
1241- time . sleep (2 )
1281+ self . stop_event . wait (2 )
12421282
12431283 # --- Ramp Phase ---
1244- if self .is_running :
1284+ if self .is_running and not self . stop_event . is_set () :
12451285 self .backend .lakeshore .set_setpoint (1 , params ['end_temp' ])
12461286 self .backend .lakeshore .setup_ramp (1 , params ['rate' ])
12471287 self .backend .lakeshore .set_heater_range (1 , 'high' )
@@ -1253,12 +1293,21 @@ def _measurement_worker(self):
12531293 # --- Measurement Loop ---
12541294 # Appendix A.1: measure_frequency_sweep reads T inside the
12551295 # frequency loop so every data point is bound to its own T.
1256- while self .is_running :
1296+ while self .is_running and not self . stop_event . is_set () :
12571297 cycle = self .backend .measure_frequency_sweep (
1258- self .frequencies , params ['delay' ])
1298+ self .frequencies , params ['delay' ], self .stop_event )
1299+
1300+ if not cycle ['points' ]:
1301+ break
1302+
12591303 elapsed = time .time () - self .start_time
12601304 self .data_queue .put (('CYCLE' , cycle , elapsed ))
12611305
1306+ # Check if the safety kill was triggered
1307+ if cycle .get ('killed' , False ):
1308+ self .data_queue .put ("CUTOFF" )
1309+ break
1310+
12621311 # Termination check uses the temperature of the LAST
12631312 # point measured in this cycle.
12641313 last_temp = cycle ['points' ][- 1 ][0 ]
@@ -1276,27 +1325,35 @@ def _measurement_worker(self):
12761325 # QUEUE PROCESSING (main thread)
12771326 # ==================================================================
12781327 def _process_data_queue (self ):
1328+ # Issue #4 Fix: Fully drain the queue before acting on terminal events
1329+ terminal = None
12791330 try :
12801331 while not self .data_queue .empty ():
12811332 data = self .data_queue .get_nowait ()
12821333
12831334 if isinstance (data , str ) and data .startswith ("LOG:" ):
12841335 self ._handle_log_message (data [4 :])
12851336 elif isinstance (data , str ) and data == "CUTOFF" :
1286- self ._handle_cutoff_event ()
1287- return
1337+ terminal = "CUTOFF" # defer; keep draining
12881338 elif isinstance (data , str ) and data == "COMPLETE" :
1289- self ._handle_complete_event ()
1290- return
1339+ terminal = "COMPLETE" # defer; keep draining
12911340 elif isinstance (data , Exception ):
1292- self ._handle_runtime_error (data )
1293- return
1341+ terminal = data
12941342 elif isinstance (data , tuple ) and data [0 ] == 'CYCLE' :
12951343 _ , cycle , elapsed = data
12961344 self ._process_cycle (cycle , elapsed )
12971345 except queue .Empty :
12981346 pass
12991347
1348+ if terminal == "CUTOFF" :
1349+ self ._handle_cutoff_event ()
1350+ return
1351+ if terminal == "COMPLETE" :
1352+ self ._handle_complete_event ()
1353+ return
1354+ if isinstance (terminal , Exception ):
1355+ self ._handle_runtime_error (terminal )
1356+ return
13001357 if self .is_running or self .is_stabilizing :
13011358 self .root .after (200 , self ._process_data_queue )
13021359
0 commit comments