Skip to content

Commit 10445c7

Browse files
PICA updated
1 parent 51a424e commit 10445c7

2 files changed

Lines changed: 300 additions & 4 deletions

File tree

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
# -------------------------------------------------------------------------------
2+
# Name: Passive R-T Logger for Keithley 2400
3+
# Purpose: Provide a GUI for passively logging R-T data using a K2400
4+
# and LS350. This version does not control temperature.
5+
# Author: Prathamesh Deshmukh (Adapted from 6517B & 2400 scripts)
6+
# Created: 05/10/2025
7+
# Version: 1.0
8+
# -------------------------------------------------------------------------------
9+
10+
# --- GUI and Plotting Packages ---
11+
import tkinter as tk
12+
from tkinter import ttk, filedialog, messagebox, scrolledtext, Canvas
13+
import os
14+
import time
15+
import traceback
16+
from datetime import datetime; import csv
17+
from matplotlib.figure import Figure
18+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
19+
import matplotlib as mpl
20+
21+
try:
22+
from PIL import Image, ImageTk
23+
PIL_AVAILABLE = True
24+
except ImportError:
25+
PIL_AVAILABLE = False
26+
27+
try:
28+
import pyvisa
29+
from pymeasure.instruments.keithley import Keithley2400
30+
PYMEASURE_AVAILABLE = True
31+
except ImportError:
32+
pyvisa, Keithley2400 = None, None
33+
PYMEASURE_AVAILABLE = False
34+
35+
# -------------------------------------------------------------------------------
36+
# --- BACKEND INSTRUMENT CONTROL ---
37+
# -------------------------------------------------------------------------------
38+
class RT_Backend_Passive:
39+
""" Manages communication for passive monitoring. """
40+
def __init__(self):
41+
self.k2400, self.lakeshore = None, None
42+
if pyvisa:
43+
try: self.rm = pyvisa.ResourceManager()
44+
except Exception as e: print(f"Could not initialize VISA: {e}"); self.rm = None
45+
46+
def connect(self, k2400_visa, ls_visa):
47+
if not self.rm: raise ConnectionError("PyVISA is not available.")
48+
if not PYMEASURE_AVAILABLE: raise ImportError("Pymeasure is not available.")
49+
self.k2400 = Keithley2400(k2400_visa); print(f" K2400 Connected: {self.k2400.id}")
50+
self.lakeshore = self.rm.open_resource(ls_visa); print(f" Lakeshore Connected: {self.lakeshore.query('*IDN?').strip()}")
51+
52+
def configure_instruments(self, current_ma, compliance_v):
53+
# Lakeshore setup for passive monitoring
54+
self.lakeshore.write('*RST'); time.sleep(0.5); self.lakeshore.write('*CLS')
55+
self.lakeshore.write('RANGE 1,0') # Ensure heater is OFF
56+
57+
# Keithley 2400 setup
58+
self.k2400.reset(); self.k2400.use_front_terminals()
59+
self.k2400.apply_current()
60+
self.k2400.source_current_range = abs(current_ma * 1e-3) * 1.05
61+
self.k2400.compliance_voltage = compliance_v
62+
self.k2400.source_current = current_ma * 1e-3
63+
self.k2400.measure_voltage()
64+
self.k2400.enable_source()
65+
66+
def get_measurement(self):
67+
voltage = self.k2400.voltage
68+
temperature = float(self.lakeshore.query('KRDG? A').strip())
69+
return temperature, voltage
70+
71+
def shutdown(self):
72+
if self.k2400:
73+
try: self.k2400.shutdown()
74+
except: pass
75+
if self.lakeshore:
76+
try: self.lakeshore.write("RANGE 1,0"); self.lakeshore.close()
77+
except: pass
78+
print(" Instruments shut down and disconnected.")
79+
80+
# -------------------------------------------------------------------------------
81+
# --- FRONT END (GUI) ---
82+
# -------------------------------------------------------------------------------
83+
class RT_GUI_Passive:
84+
PROGRAM_VERSION = "1.0"
85+
CLR_BG = '#2B3D4F'; CLR_HEADER = '#3A506B'; CLR_FG = '#EDF2F4'
86+
CLR_FRAME_BG = '#3A506B'; CLR_INPUT_BG = '#4C566A'
87+
CLR_ACCENT_GREEN, CLR_ACCENT_RED, CLR_ACCENT_BLUE = '#A7C957', '#E74C3C', '#8D99AE'
88+
CLR_ACCENT_GOLD = '#FFC107'; CLR_CONSOLE_BG = '#1E2B38'
89+
FONT_BASE = ('Segoe UI', 11); FONT_TITLE = ('Segoe UI', 13, 'bold')
90+
91+
def __init__(self, root):
92+
self.root = root; self.root.title("Passive R-T Logger (K2400 + LS350)")
93+
self.root.geometry("1600x950"); self.root.minsize(1400, 800); self.root.configure(bg=self.CLR_BG)
94+
self.is_running = False; self.logo_image = None
95+
self.backend = RT_Backend_Passive(); self.data_storage = {'temperature': [], 'voltage': [], 'resistance': []}
96+
self.setup_styles(); self.create_widgets(); self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
97+
98+
def setup_styles(self):
99+
style = ttk.Style(self.root); style.theme_use('clam')
100+
style.configure('.', background=self.CLR_BG, foreground=self.CLR_FG, font=self.FONT_BASE)
101+
style.configure('TFrame', background=self.CLR_BG); style.configure('TPanedWindow', background=self.CLR_BG)
102+
style.configure('TLabel', background=self.CLR_FRAME_BG, foreground=self.CLR_FG)
103+
style.configure('Header.TLabel', background=self.CLR_HEADER)
104+
style.configure('TEntry', fieldbackground=self.CLR_INPUT_BG, foreground=self.CLR_FG, insertcolor=self.CLR_FG)
105+
style.configure('TButton', font=self.FONT_BASE, padding=(10, 9), foreground=self.CLR_ACCENT_GOLD, background=self.CLR_HEADER)
106+
style.map('TButton', background=[('active', self.CLR_ACCENT_GOLD), ('hover', self.CLR_ACCENT_GOLD)], foreground=[('active', self.CLR_BG), ('hover', self.CLR_BG)])
107+
style.configure('Start.TButton', background=self.CLR_ACCENT_GREEN, foreground=self.CLR_BG)
108+
style.map('Start.TButton', background=[('active', '#8AB845'), ('hover', '#8AB845')])
109+
style.configure('Stop.TButton', background=self.CLR_ACCENT_RED, foreground=self.CLR_FG)
110+
style.map('Stop.TButton', background=[('active', '#D63C2A'), ('hover', '#D63C2A')])
111+
style.configure('TLabelframe', background=self.CLR_FRAME_BG, bordercolor=self.CLR_ACCENT_BLUE)
112+
style.configure('TLabelframe.Label', background=self.CLR_FRAME_BG, foreground=self.CLR_FG, font=self.FONT_TITLE)
113+
mpl.rcParams.update({'font.family': 'Segoe UI', 'font.size': 11, 'axes.titlesize': 15, 'axes.labelsize': 13})
114+
115+
def create_widgets(self):
116+
header = tk.Frame(self.root, bg=self.CLR_HEADER); header.pack(side='top', fill='x')
117+
ttk.Label(header, text=f"Passive R-T Logger (K2400) v{self.PROGRAM_VERSION}", style='Header.TLabel', font=self.FONT_TITLE).pack(side='left', padx=20, pady=10)
118+
main_pane = ttk.PanedWindow(self.root, orient='horizontal'); main_pane.pack(fill='both', expand=True, padx=10, pady=10)
119+
left_panel = self._create_left_panel(main_pane); main_pane.add(left_panel, weight=2)
120+
right_panel = self._create_right_panel(main_pane); main_pane.add(right_panel, weight=3)
121+
122+
def _create_left_panel(self, parent):
123+
panel = ttk.Frame(parent, padding=5); panel.grid_columnconfigure(0, weight=1); panel.grid_rowconfigure(3, weight=1)
124+
self._create_info_panel(panel, 0)
125+
self._create_params_panel(panel, 1); self._create_control_panel(panel, 2); self._create_console_panel(panel, 3)
126+
return panel
127+
128+
def _create_info_panel(self, parent, grid_row):
129+
frame = ttk.LabelFrame(parent, text='Information'); frame.grid(row=grid_row, column=0, sticky='new', pady=5)
130+
frame.grid_columnconfigure(1, weight=1)
131+
logo_canvas = Canvas(frame, width=80, height=80, bg=self.CLR_FRAME_BG, highlightthickness=0)
132+
logo_canvas.grid(row=0, column=0, rowspan=2, padx=10, pady=10)
133+
try:
134+
script_dir = os.path.dirname(os.path.abspath(__file__))
135+
logo_path = os.path.join(script_dir, "..", "_assets", "LOGO", "UGC_DAE_CSR.jpeg")
136+
if PIL_AVAILABLE and os.path.exists(logo_path):
137+
img = Image.open(logo_path).resize((80, 80), Image.Resampling.LANCZOS)
138+
self.logo_image = ImageTk.PhotoImage(img)
139+
logo_canvas.create_image(40, 40, image=self.logo_image)
140+
except Exception as e: self.log(f"Warning: Could not load logo. {e}")
141+
info_text = ("Institute: UGC DAE CSR, Mumbai\nMeasurement: R vs. T (Passive)\nInstruments: K2400, LS350")
142+
ttk.Label(frame, text=info_text, justify='left').grid(row=0, column=1, rowspan=2, sticky='w', padx=5)
143+
144+
def _create_right_panel(self, parent):
145+
panel = ttk.Frame(parent, padding=5)
146+
container = ttk.LabelFrame(panel, text='Live R-T Curve'); container.pack(fill='both', expand=True)
147+
self.figure = Figure(dpi=100, facecolor='white')
148+
self.ax_main = self.figure.add_subplot(111)
149+
self.line_main, = self.ax_main.plot([], [], color=self.CLR_ACCENT_RED, marker='o', markersize=4, linestyle='-')
150+
self.ax_main.set_title("Waiting for logging...", fontweight='bold'); self.ax_main.set_xlabel("Temperature (K)"); self.ax_main.set_ylabel("Resistance (Ω)")
151+
self.ax_main.grid(True, linestyle='--', alpha=0.6); self.figure.tight_layout()
152+
self.canvas = FigureCanvasTkAgg(self.figure, container); self.canvas.get_tk_widget().pack(fill='both', expand=True, padx=5, pady=5)
153+
return panel
154+
155+
def _create_params_panel(self, parent, grid_row):
156+
container = ttk.Frame(parent); container.grid(row=grid_row, column=0, sticky='new', pady=5)
157+
container.grid_columnconfigure((0, 1), weight=1); self.entries = {}
158+
iv_frame = ttk.LabelFrame(container, text='Measurement Settings'); iv_frame.grid(row=0, column=0, columnspan=2, sticky='nsew')
159+
iv_frame.grid_columnconfigure(1, weight=1)
160+
self._create_entry(iv_frame, "Source Current (mA)", "1", 0); self._create_entry(iv_frame, "Compliance (V)", "10", 1)
161+
self._create_entry(iv_frame, "Logging Delay (s)", "1", 2)
162+
self.ls_cb = self._create_combobox(iv_frame, "Lakeshore VISA", 3)
163+
self.k2400_cb = self._create_combobox(iv_frame, "Keithley 2400 VISA", 4)
164+
165+
def _create_control_panel(self, parent, grid_row):
166+
frame = ttk.LabelFrame(parent, text='File Control'); frame.grid(row=grid_row, column=0, sticky='new', pady=5)
167+
frame.grid_columnconfigure(0, weight=1)
168+
self._create_entry(frame, "Sample Name", "Sample_RT_Passive", 0)
169+
self._create_entry(frame, "Save Location", "", 1, browse=True)
170+
button_frame = ttk.Frame(frame); button_frame.grid(row=2, column=0, columnspan=4, sticky='ew', pady=5)
171+
button_frame.grid_columnconfigure((0,1,2), weight=1)
172+
self.start_button = ttk.Button(button_frame, text="Start", style='Start.TButton', command=self.start_experiment)
173+
self.start_button.grid(row=0, column=0, sticky='ew', padx=5)
174+
self.stop_button = ttk.Button(button_frame, text="Stop", style='Stop.TButton', state='disabled', command=self.stop_experiment)
175+
self.stop_button.grid(row=0, column=1, sticky='ew', padx=5)
176+
ttk.Button(button_frame, text="Scan", command=self._scan_for_visa).grid(row=0, column=2, sticky='ew', padx=5)
177+
178+
def _create_console_panel(self, parent, grid_row):
179+
frame = ttk.LabelFrame(parent, text='Console'); frame.grid(row=grid_row, column=0, sticky='nsew', pady=5)
180+
self.console = scrolledtext.ScrolledText(frame, state='disabled', bg=self.CLR_CONSOLE_BG, fg=self.CLR_FG, font=('Consolas', 9), wrap='word', borderwidth=0)
181+
self.console.pack(fill='both', expand=True, padx=5, pady=5)
182+
183+
def log(self, message):
184+
ts = datetime.now().strftime("%H:%M:%S"); log_msg = f"[{ts}] {message}\n"
185+
self.console.config(state='normal'); self.console.insert('end', log_msg); self.console.see('end'); self.console.config(state='disabled')
186+
187+
def start_experiment(self):
188+
try:
189+
self.params = self._validate_and_get_params()
190+
self.log("Connecting to instruments..."); self.backend.connect(self.params['k2400_visa'], self.params['ls_visa'])
191+
self.backend.configure_instruments(self.params['current_ma'], self.params['compliance_v']); self.log("All instruments connected and configured for passive logging.")
192+
193+
ts = datetime.now().strftime("%Y%m%d_%H%M%S"); filename = f"{self.params['name']}_{ts}_RT_Passive.csv"
194+
self.data_filepath = os.path.join(self.params['save_path'], filename)
195+
with open(self.data_filepath, 'w', newline='') as f:
196+
writer = csv.writer(f); writer.writerow(["Temperature (K)", "Voltage (V)", "Resistance (Ohm)", "Elapsed Time (s)"])
197+
198+
self.set_ui_state(running=True)
199+
for key in self.data_storage: self.data_storage[key].clear()
200+
self.line_main.set_data([], []); self.ax_main.set_title(f"R-T Curve: {self.params['name']}"); self.canvas.draw()
201+
self.log("Starting passive logging..."); self.start_time = time.time()
202+
self.root.after(100, self._experiment_loop)
203+
except Exception as e:
204+
self.log(f"ERROR: {traceback.format_exc()}"); messagebox.showerror("Start Failed", f"{e}"); self.backend.shutdown()
205+
206+
def stop_experiment(self, reason=""):
207+
if not self.is_running: return
208+
self.log(f"Stopping... {reason}" if reason else "Stopping by user request.")
209+
self.is_running = False; self.backend.shutdown(); self.set_ui_state(running=False)
210+
self.ax_main.set_title("Logging stopped."); self.canvas.draw()
211+
if reason: messagebox.showinfo("Experiment Finished", f"Reason: {reason}")
212+
213+
def _experiment_loop(self):
214+
if not self.is_running: return
215+
try:
216+
temp, voltage = self.backend.get_measurement()
217+
resistance = voltage / (self.params['current_ma'] * 1e-3) if self.params['current_ma'] != 0 else float('inf')
218+
elapsed = time.time() - self.start_time
219+
self.log(f"T: {temp:.3f} K | R: {resistance:.4e} Ω")
220+
221+
self.data_storage['temperature'].append(temp); self.data_storage['voltage'].append(voltage); self.data_storage['resistance'].append(resistance)
222+
with open(self.data_filepath, 'a', newline='') as f: csv.writer(f).writerow([f"{temp:.4f}", f"{voltage:.6e}", f"{resistance:.6e}", f"{elapsed:.2f}"])
223+
self.line_main.set_data(self.data_storage['temperature'], self.data_storage['resistance'])
224+
self.ax_main.relim(); self.ax_main.autoscale_view(); self.figure.tight_layout(); self.canvas.draw()
225+
226+
self.root.after(int(self.params['delay_s'] * 1000), self._experiment_loop)
227+
228+
except Exception as e:
229+
self.log(f"CRITICAL ERROR: {traceback.format_exc()}"); messagebox.showerror("Runtime Error", f"{e}"); self.stop_experiment("Runtime Error")
230+
231+
def _validate_and_get_params(self):
232+
try:
233+
params = {'name': self.entries["Sample Name"].get(), 'save_path': self.entries["Save Location"].get(),
234+
'ls_visa': self.ls_cb.get(), 'current_ma': float(self.entries["Source Current (mA)"].get()),
235+
'compliance_v': float(self.entries["Compliance (V)"].get()), 'delay_s': float(self.entries["Logging Delay (s)"].get()),
236+
'k2400_visa': self.k2400_cb.get()}
237+
if not all(params.values()): raise ValueError("All fields must be filled.")
238+
return params
239+
except Exception as e: raise ValueError(f"Invalid parameter input: {e}")
240+
241+
def set_ui_state(self, running: bool):
242+
self.is_running = running
243+
state = 'disabled' if running else 'normal'
244+
self.start_button.config(state=state)
245+
for w in self.entries.values(): w.config(state=state)
246+
for cb in [self.ls_cb, self.k2400_cb]: cb.config(state=state if state == 'normal' else 'readonly')
247+
self.stop_button.config(state='normal' if running else 'disabled')
248+
249+
def _scan_for_visa(self):
250+
if self.backend.rm is None: self.log("ERROR: PyVISA library missing."); return
251+
self.log("Scanning for VISA instruments..."); resources = self.backend.rm.list_resources()
252+
if resources:
253+
self.log(f"Found: {resources}"); self.ls_cb['values'] = resources; self.k2400_cb['values'] = resources
254+
for r in resources:
255+
if '12' in r or '15' in r: self.ls_cb.set(r)
256+
if '2400' in r or 'GPIB::4' in r: self.k2400_cb.set(r)
257+
else: self.log("No VISA instruments found.")
258+
259+
def _browse_file_location(self):
260+
path = filedialog.askdirectory()
261+
if path:
262+
self.entries["Save Location"].config(state='normal'); self.entries["Save Location"].delete(0, 'end')
263+
self.entries["Save Location"].insert(0, path); self.entries["Save Location"].config(state='disabled')
264+
265+
def _create_entry(self, parent, label_text, default_value, row, browse=False):
266+
ttk.Label(parent, text=f"{label_text}:").grid(row=row, column=0, sticky='w', padx=10, pady=3)
267+
entry = ttk.Entry(parent, font=self.FONT_BASE)
268+
entry.grid(row=row, column=1, sticky='ew', padx=10, pady=3, columnspan=2 if browse else 1)
269+
entry.insert(0, default_value); self.entries[label_text] = entry
270+
if browse:
271+
btn = ttk.Button(parent, text="...", width=3, command=self._browse_file_location)
272+
btn.grid(row=row, column=3, sticky='e', padx=(0,10))
273+
entry.config(state='disabled')
274+
275+
def _create_combobox(self, parent, label_text, row):
276+
ttk.Label(parent, text=f"{label_text}:").grid(row=row, column=0, sticky='w', padx=10, pady=3)
277+
cb = ttk.Combobox(parent, font=self.FONT_BASE, state='readonly')
278+
cb.grid(row=row, column=1, sticky='ew', padx=10, pady=3, columnspan=3)
279+
return cb
280+
281+
def _on_closing(self):
282+
if self.is_running and messagebox.askyesno("Exit", "Experiment is running. Stop and exit?"):
283+
self.stop_experiment("Application closed by user."); self.root.destroy()
284+
elif not self.is_running: self.root.destroy()
285+
286+
if __name__ == '__main__':
287+
if not PYMEASURE_AVAILABLE:
288+
messagebox.showerror("Dependency Error", "Pymeasure or PyVISA is not installed. Please run 'pip install pymeasure'.")
289+
else:
290+
root = tk.Tk(); app = RT_GUI_Passive(root); root.mainloop()

0 commit comments

Comments
 (0)