1- # -------------------------------------------------------------------------------
2- # Name: IV Sweep GUI for Keithley 2400/2182
3- # Purpose: Provide a professional GUI for performing IV sweeps using a
4- # Keithley 2400 as a current source and a Keithley 2182
5- # as a nanovoltmeter.
6- # Author: Prathamesh Deshmukh (GUI by Gemini)
7- # Created: 04/10/2025
8- # Version: 1.0
9- # -------------------------------------------------------------------------------
10-
11- # --- GUI and Plotting Packages ---
12- import tkinter as tk
13- from tkinter import ttk , filedialog , messagebox , scrolledtext
14- import numpy as np
15- import os
16- import time
17- import traceback
18- import csv
19- from datetime import datetime
20- from matplotlib .figure import Figure
21- from matplotlib .backends .backend_tkagg import FigureCanvasTkAgg
22- import matplotlib as mpl
23-
24- # --- Instrument Control Packages ---
25- try :
26- import pyvisa
27- from pymeasure .instruments .keithley import Keithley2400
28- PYMEASURE_AVAILABLE = True
29- except ImportError :
30- pyvisa , Keithley2400 = None , None
31- PYMEASURE_AVAILABLE = False
32-
33- # -------------------------------------------------------------------------------
34- # --- BACKEND INSTRUMENT CONTROL ---
35- # -------------------------------------------------------------------------------
36- class IV_Backend :
37- """ Manages communication with the Keithley 2400 and 2182. """
38- def __init__ (self ):
39- self .k2400 , self .k2182 = None , None
40- if pyvisa :
41- try : self .rm = pyvisa .ResourceManager ()
42- except Exception as e : print (f"Could not initialize VISA: { e } " ); self .rm = None
43-
44- def connect (self , k2400_visa , k2182_visa ):
45- if not self .rm : raise ConnectionError ("PyVISA is not available." )
46- if not PYMEASURE_AVAILABLE : raise ImportError ("Pymeasure is not available." )
47- self .k2400 = Keithley2400 (k2400_visa )
48- print (f" K2400 Connected: { self .k2400 .id } " )
49- self .k2182 = self .rm .open_resource (k2182_visa )
50- print (f" K2182 Connected: { self .k2182 .query ('*IDN?' ).strip ()} " )
51-
52- def configure_instruments (self , compliance_v , current_range_a ):
53- # Keithley 2400 setup
54- self .k2400 .reset ()
55- self .k2400 .apply_current ()
56- self .k2400 .source_current_range = current_range_a
57- self .k2400 .compliance_voltage = compliance_v
58- self .k2400 .source_current = 0
59- self .k2400 .enable_source ()
60-
61- # Keithley 2182 setup
62- self .k2182 .write ("*rst; status:preset; *cls" )
63- time .sleep (1 )
64-
65- def measure_voltage_at_current (self , current_a , delay_s ):
66- self .k2400 .ramp_to_current (current_a , steps = 10 , pause = 0.05 )
67- time .sleep (delay_s )
68-
69- # K2182 measurement sequence
70- self .k2182 .write ("status:measurement:enable 512; *sre 1" )
71- self .k2182 .write ("sample:count 2" )
72- self .k2182 .write ("trigger:source bus" )
73- self .k2182 .write ("trigger:delay 0.1" )
74- self .k2182 .write ("trace:points 2" )
75- self .k2182 .write ("trace:feed sense1; feed:control next" )
76- self .k2182 .write ("initiate" )
77- self .k2182 .assert_trigger ()
78- self .k2182 .wait_for_srq (timeout = 10 )
79- voltages = self .k2182 .query_ascii_values ("trace:data?" )
80- self .k2182 .query ("status:measurement?" )
81- self .k2182 .write ("trace:clear; feed:control next" )
82-
83- return sum (voltages ) / len (voltages ) if voltages else float ('nan' )
84-
85- def shutdown (self ):
86- if self .k2400 :
87- try : self .k2400 .shutdown ()
88- except : pass
89- if self .k2182 :
90- try : self .k2182 .write ("*rst" ); self .k2182 .close ()
91- except : pass
92- print (" Instruments shut down and disconnected." )
93-
94- # -------------------------------------------------------------------------------
95- # --- FRONT END (GUI) ---
96- # -------------------------------------------------------------------------------
97- class IV_GUI :
98- PROGRAM_VERSION = "1.0"
99- CLR_BG = '#2B3D4F' ; CLR_HEADER = '#3A506B' ; CLR_FG = '#EDF2F4'
100- CLR_FRAME_BG = '#3A506B' ; CLR_INPUT_BG = '#4C566A'
101- CLR_ACCENT_GREEN , CLR_ACCENT_RED , CLR_ACCENT_BLUE = '#A7C957' , '#E74C3C' , '#8D99AE'
102- CLR_ACCENT_GOLD = '#FFC107' ; CLR_CONSOLE_BG = '#1E2B38'
103- FONT_BASE = ('Segoe UI' , 11 ); FONT_TITLE = ('Segoe UI' , 13 , 'bold' )
104-
105- def __init__ (self , root ):
106- self .root = root
107- self .root .title ("I-V Sweep (K2400 + K2182)" )
108- self .root .geometry ("1600x950" )
109- self .root .minsize (1400 , 800 )
110- self .root .configure (bg = self .CLR_BG )
111- self .is_running = False
112- self .backend = IV_Backend ()
113- self .data_storage = {'current' : [], 'voltage' : []}
114- self .setup_styles ()
115- self .create_widgets ()
116- self .root .protocol ("WM_DELETE_WINDOW" , self ._on_closing )
117-
118- def setup_styles (self ):
119- style = ttk .Style (self .root ); style .theme_use ('clam' )
120- style .configure ('.' , background = self .CLR_BG , foreground = self .CLR_FG , font = self .FONT_BASE )
121- style .configure ('TFrame' , background = self .CLR_BG ); style .configure ('TPanedWindow' , background = self .CLR_BG )
122- style .configure ('TLabel' , background = self .CLR_FRAME_BG , foreground = self .CLR_FG )
123- style .configure ('Header.TLabel' , background = self .CLR_HEADER )
124- style .configure ('TEntry' , fieldbackground = self .CLR_INPUT_BG , foreground = self .CLR_FG , insertcolor = self .CLR_FG )
125- style .configure ('TButton' , font = self .FONT_BASE , padding = (10 , 9 ), foreground = self .CLR_ACCENT_GOLD , background = self .CLR_HEADER )
126- style .map ('TButton' , background = [('active' , self .CLR_ACCENT_GOLD ), ('hover' , self .CLR_ACCENT_GOLD )], foreground = [('active' , self .CLR_BG ), ('hover' , self .CLR_BG )])
127- style .configure ('Start.TButton' , background = self .CLR_ACCENT_GREEN , foreground = self .CLR_BG )
128- style .map ('Start.TButton' , background = [('active' , '#8AB845' ), ('hover' , '#8AB845' )])
129- style .configure ('Stop.TButton' , background = self .CLR_ACCENT_RED , foreground = self .CLR_FG )
130- style .map ('Stop.TButton' , background = [('active' , '#D63C2A' ), ('hover' , '#D63C2A' )])
131- style .configure ('TLabelframe' , background = self .CLR_FRAME_BG , bordercolor = self .CLR_ACCENT_BLUE )
132- style .configure ('TLabelframe.Label' , background = self .CLR_FRAME_BG , foreground = self .CLR_FG , font = self .FONT_TITLE )
133- mpl .rcParams .update ({'font.family' : 'Segoe UI' , 'font.size' : 11 , 'axes.titlesize' : 15 , 'axes.labelsize' : 13 })
134-
135- def create_widgets (self ):
136- header = tk .Frame (self .root , bg = self .CLR_HEADER ); header .pack (side = 'top' , fill = 'x' )
137- ttk .Label (header , text = f"I-V Sweep (K2400 + K2182) v{ self .PROGRAM_VERSION } " , style = 'Header.TLabel' , font = self .FONT_TITLE ).pack (side = 'left' , padx = 20 , pady = 10 )
138- main_pane = ttk .PanedWindow (self .root , orient = 'horizontal' ); main_pane .pack (fill = 'both' , expand = True , padx = 10 , pady = 10 )
139- left_panel = self ._create_left_panel (main_pane ); main_pane .add (left_panel , weight = 2 )
140- right_panel = self ._create_right_panel (main_pane ); main_pane .add (right_panel , weight = 3 )
141-
142- def _create_left_panel (self , parent ):
143- panel = ttk .Frame (parent , padding = 5 ); panel .grid_columnconfigure (0 , weight = 1 ); panel .grid_rowconfigure (2 , weight = 1 )
144- self ._create_params_panel (panel , 0 ); self ._create_control_panel (panel , 1 ); self ._create_console_panel (panel , 2 )
145- return panel
146-
147- def _create_right_panel (self , parent ):
148- panel = ttk .Frame (parent , padding = 5 )
149- container = ttk .LabelFrame (panel , text = 'Live I-V Curve' ); container .pack (fill = 'both' , expand = True )
150- self .figure = Figure (dpi = 100 , facecolor = 'white' )
151- self .ax_main = self .figure .add_subplot (111 )
152- self .line_main , = self .ax_main .plot ([], [], color = self .CLR_ACCENT_RED , marker = 'o' , markersize = 4 , linestyle = '-' )
153- self .ax_main .set_title ("Waiting for experiment..." , fontweight = 'bold' ); self .ax_main .set_xlabel ("Voltage (V)" ); self .ax_main .set_ylabel ("Current (A)" )
154- self .ax_main .grid (True , linestyle = '--' , alpha = 0.6 ); self .figure .tight_layout ()
155- self .canvas = FigureCanvasTkAgg (self .figure , container ); self .canvas .get_tk_widget ().pack (fill = 'both' , expand = True , padx = 5 , pady = 5 )
156- return panel
157-
158- def _create_params_panel (self , parent , grid_row ):
159- container = ttk .LabelFrame (parent , text = 'Sweep Parameters' ); container .grid (row = grid_row , column = 0 , sticky = 'new' , pady = 5 )
160- container .grid_columnconfigure (1 , weight = 1 ); self .entries = {}
161- self ._create_entry (container , "Start Current (mA)" , "-1" , 0 )
162- self ._create_entry (container , "Stop Current (mA)" , "1" , 1 )
163- self ._create_entry (container , "Step Current (mA)" , "0.1" , 2 )
164- self ._create_entry (container , "Compliance (V)" , "10" , 3 )
165- self ._create_entry (container , "Dwell Time (s)" , "0.5" , 4 )
166- self .k2400_cb = self ._create_combobox (container , "Keithley 2400 VISA" , 5 )
167- self .k2182_cb = self ._create_combobox (container , "Keithley 2182 VISA" , 6 )
168-
169- def _create_control_panel (self , parent , grid_row ):
170- frame = ttk .LabelFrame (parent , text = 'Experiment Control' ); frame .grid (row = grid_row , column = 0 , sticky = 'new' , pady = 5 )
171- frame .grid_columnconfigure (0 , weight = 1 )
172- self ._create_entry (frame , "Sample Name" , "Sample_IV" , 0 )
173- self ._create_entry (frame , "Save Location" , "" , 1 , browse = True )
174- button_frame = ttk .Frame (frame ); button_frame .grid (row = 2 , column = 0 , columnspan = 4 , sticky = 'ew' , pady = 5 )
175- button_frame .grid_columnconfigure ((0 ,1 ,2 ), weight = 1 )
176- self .start_button = ttk .Button (button_frame , text = "Start" , style = 'Start.TButton' , command = self .start_experiment )
177- self .start_button .grid (row = 0 , column = 0 , sticky = 'ew' , padx = 5 )
178- self .stop_button = ttk .Button (button_frame , text = "Stop" , style = 'Stop.TButton' , state = 'disabled' , command = self .stop_experiment )
179- self .stop_button .grid (row = 0 , column = 1 , sticky = 'ew' , padx = 5 )
180- ttk .Button (button_frame , text = "Scan" , command = self ._scan_for_visa ).grid (row = 0 , column = 2 , sticky = 'ew' , padx = 5 )
181-
182- def _create_console_panel (self , parent , grid_row ):
183- frame = ttk .LabelFrame (parent , text = 'Console' ); frame .grid (row = grid_row , column = 0 , sticky = 'nsew' , pady = 5 )
184- self .console = scrolledtext .ScrolledText (frame , state = 'disabled' , bg = self .CLR_CONSOLE_BG , fg = self .CLR_FG , font = ('Consolas' , 9 ), wrap = 'word' , borderwidth = 0 )
185- self .console .pack (fill = 'both' , expand = True , padx = 5 , pady = 5 )
186-
187- def log (self , message ):
188- ts = datetime .now ().strftime ("%H:%M:%S" ); log_msg = f"[{ ts } ] { message } \n "
189- self .console .config (state = 'normal' ); self .console .insert ('end' , log_msg ); self .console .see ('end' ); self .console .config (state = 'disabled' )
190-
191- def start_experiment (self ):
192- try :
193- self .params = self ._validate_and_get_params ()
194- self .log ("Connecting to instruments..." ); self .backend .connect (self .params ['k2400_visa' ], self .params ['k2182_visa' ])
195- self .backend .configure_instruments (self .params ['compliance_v' ], self .params ['stop_i' ])
196- self .log ("All instruments connected and configured." )
197-
198- start_i , stop_i , step_i = self .params ['start_i' ], self .params ['stop_i' ], self .params ['step_i' ]
199- self .current_points = np .arange (start_i , stop_i + step_i / 2 , step_i )
200- if not len (self .current_points ): raise ValueError ("Current sweep results in zero points." )
201-
202- ts = datetime .now ().strftime ("%Y%m%d_%H%M%S" ); filename = f"{ self .params ['name' ]} _{ ts } _IV.csv"
203- self .data_filepath = os .path .join (self .params ['save_path' ], filename )
204- with open (self .data_filepath , 'w' , newline = '' ) as f :
205- writer = csv .writer (f ); writer .writerow (["Current (A)" , "Voltage (V)" ])
206-
207- self .current_step_index = 0 ; self .set_ui_state (running = True )
208- for key in self .data_storage : self .data_storage [key ].clear ()
209- self .line_main .set_data ([], []); self .ax_main .set_title (f"I-V Curve: { self .params ['name' ]} " ); self .canvas .draw ()
210- self .log (f"Starting sweep: { len (self .current_points )} current points." )
211- self .root .after (100 , self ._experiment_loop )
212- except Exception as e :
213- self .log (f"ERROR: { traceback .format_exc ()} " ); messagebox .showerror ("Start Failed" , f"{ e } " ); self .backend .shutdown ()
214-
215- def stop_experiment (self , reason = "" ):
216- if not self .is_running : return
217- self .log (f"Stopping... { reason } " if reason else "Stopping by user request." )
218- self .is_running = False ; self .backend .shutdown (); self .set_ui_state (running = False )
219- self .ax_main .set_title ("Experiment stopped." ); self .canvas .draw ()
220- if reason : messagebox .showinfo ("Experiment Finished" , f"Reason: { reason } " )
221-
222- def _experiment_loop (self ):
223- if not self .is_running : return
224- try :
225- current_setpoint = self .current_points [self .current_step_index ]
226- self .log (f"--- Setting current to { current_setpoint :.3e} A ({ self .current_step_index + 1 } /{ len (self .current_points )} ) ---" )
227- self .ax_main .set_title (f"Measuring at { current_setpoint :.3e} A..." ); self .canvas .draw ()
228-
229- voltage = self .backend .measure_voltage_at_current (current_setpoint , self .params ['delay_s' ])
230- self .log (f" Read: V = { voltage :.6e} V" )
231-
232- self .data_storage ['current' ].append (current_setpoint ); self .data_storage ['voltage' ].append (voltage )
233- with open (self .data_filepath , 'a' , newline = '' ) as f : csv .writer (f ).writerow ([f"{ current_setpoint :.6e} " , f"{ voltage :.6e} " ])
234- self .line_main .set_data (self .data_storage ['voltage' ], self .data_storage ['current' ])
235- self .ax_main .relim (); self .ax_main .autoscale_view (); self .figure .tight_layout (); self .canvas .draw ()
236-
237- self .current_step_index += 1
238- if self .current_step_index >= len (self .current_points ):
239- self .stop_experiment ("All points measured." )
240- else :
241- self .root .after (100 , self ._experiment_loop )
242- except Exception as e :
243- self .log (f"CRITICAL ERROR: { traceback .format_exc ()} " ); messagebox .showerror ("Runtime Error" , f"{ e } " ); self .stop_experiment ("Runtime Error" )
244-
245- def _validate_and_get_params (self ):
246- try :
247- params = {
248- 'name' : self .entries ["Sample Name" ].get (), 'save_path' : self .entries ["Save Location" ].get (),
249- 'start_i' : float (self .entries ["Start Current (mA)" ].get ()) * 1e-3 ,
250- 'stop_i' : float (self .entries ["Stop Current (mA)" ].get ()) * 1e-3 ,
251- 'step_i' : float (self .entries ["Step Current (mA)" ].get ()) * 1e-3 ,
252- 'compliance_v' : float (self .entries ["Compliance (V)" ].get ()),
253- 'delay_s' : float (self .entries ["Dwell Time (s)" ].get ()),
254- 'k2400_visa' : self .k2400_cb .get (), 'k2182_visa' : self .k2182_cb .get ()
255- }
256- if not all (params .values ()): raise ValueError ("All fields must be filled." )
257- if params ['step_i' ] == 0 : raise ValueError ("Step Current cannot be zero." )
258- return params
259- except Exception as e : raise ValueError (f"Invalid parameter input: { e } " )
260-
261- def set_ui_state (self , running : bool ):
262- self .is_running = running
263- state = 'disabled' if running else 'normal'
264- self .start_button .config (state = state )
265- for w in self .entries .values (): w .config (state = state )
266- for cb in [self .k2400_cb , self .k2182_cb ]: cb .config (state = state if state == 'normal' else 'readonly' )
267- self .stop_button .config (state = 'normal' if running else 'disabled' )
268-
269- def _scan_for_visa (self ):
270- if self .backend .rm is None : self .log ("ERROR: PyVISA library missing." ); return
271- self .log ("Scanning for VISA instruments..." ); resources = self .backend .rm .list_resources ()
272- if resources :
273- self .log (f"Found: { resources } " ); self .k2400_cb ['values' ] = resources ; self .k2182_cb ['values' ] = resources
274- for r in resources :
275- if '2400' in r or 'GPIB::4' in r : self .k2400_cb .set (r )
276- if '2182' in r or 'GPIB::7' in r : self .k2182_cb .set (r )
277- else : self .log ("No VISA instruments found." )
278-
279- def _browse_file_location (self ):
280- path = filedialog .askdirectory ()
281- if path :
282- self .entries ["Save Location" ].config (state = 'normal' ); self .entries ["Save Location" ].delete (0 , 'end' )
283- self .entries ["Save Location" ].insert (0 , path ); self .entries ["Save Location" ].config (state = 'disabled' )
284-
285- def _create_entry (self , parent , label_text , default_value , row , browse = False ):
286- ttk .Label (parent , text = f"{ label_text } :" ).grid (row = row , column = 0 , sticky = 'w' , padx = 10 , pady = 3 )
287- entry = ttk .Entry (parent , font = self .FONT_BASE )
288- entry .grid (row = row , column = 1 , sticky = 'ew' , padx = 10 , pady = 3 , columnspan = 2 if browse else 1 )
289- entry .insert (0 , default_value ); self .entries [label_text ] = entry
290- if browse :
291- btn = ttk .Button (parent , text = "..." , width = 3 , command = self ._browse_file_location )
292- btn .grid (row = row , column = 3 , sticky = 'e' , padx = (0 ,10 ))
293- entry .config (state = 'disabled' )
294-
295- def _create_combobox (self , parent , label_text , row ):
296- ttk .Label (parent , text = f"{ label_text } :" ).grid (row = row , column = 0 , sticky = 'w' , padx = 10 , pady = 3 )
297- cb = ttk .Combobox (parent , font = self .FONT_BASE , state = 'readonly' )
298- cb .grid (row = row , column = 1 , sticky = 'ew' , padx = 10 , pady = 3 , columnspan = 3 )
299- return cb
300-
301- def _on_closing (self ):
302- if self .is_running and messagebox .askyesno ("Exit" , "Experiment is running. Stop and exit?" ):
303- self .stop_experiment ("Application closed by user." ); self .root .destroy ()
304- elif not self .is_running : self .root .destroy ()
305-
306- if __name__ == '__main__' :
307- if not PYMEASURE_AVAILABLE :
308- messagebox .showerror ("Dependency Error" , "Pymeasure or PyVISA is not installed. Please run 'pip install pymeasure'." )
309- else :
310- root = tk .Tk (); app = IV_GUI (root ); root .mainloop ()
0 commit comments