Skip to content

Commit 7094b68

Browse files
2400-2182 frontend genrated
1 parent 5af9a7f commit 7094b68

4 files changed

Lines changed: 934 additions & 2 deletions

File tree

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
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

Comments
 (0)