Skip to content

Commit 14931db

Browse files
2400-2182 GUI updated
1 parent 7094b68 commit 14931db

4 files changed

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