Source code for semi_cr.core.lab.drivers.keithley_2636V3

# ----------------------------------------------------
# Note
# ----------------------------------------------------
"""
Code is good but still areas for improving and making
more pythonic - A.I. is good at brain storming ideas
to do this.

Need to trouble shoot and ensure parameters are actually being
updated - e.g. nplc.
"""

# ----------------------------------------------------
# Imports
# ----------------------------------------------------
import time
from dataclasses import dataclass, field

import pyvisa


# ----------------------------------------------------
# Dataclasses Classes
# ----------------------------------------------------
# Settings for source and measurement
[docs] @dataclass class Source: channel: str = "a" delay: float = 0.1 iv: str = "v" current_level: float = 0.01 voltage_level: float = 0.01 # V
[docs] @dataclass class Measurement: filter: str = "FILTER_REPEAT_AVG" filter_count: int = 3 filter_status: bool = True nplc: int = 10
[docs] @dataclass class Settings: source: Source = field(default_factory=Source) measurement: Measurement = field(default_factory=Measurement)
# ---------------------------------------------------- # Custom Errors # ---------------------------------------------------- # TODO: PUT THEM INTO A FILE AND USE FOR CQCC AND THIS FILE ???
[docs] class ConnectionError(Exception): pass
[docs] class DisconnectionError(Exception): pass
[docs] class MeasurementError(Exception): pass
# ---------------------------------------------------- # Keithley Driver Class # ----------------------------------------------------
[docs] class Keithley2636bDriver: # ---------------------------------------------------- # Initialise, Connect, Disconnect, Checking Connection # ---------------------------------------------------- def __init__(self): self._settings = Settings() self.rm = pyvisa.ResourceManager() self._status = None self.connected = False
[docs] def connect(self, IP): try: # Connect self._status = self.rm.open_resource(IP) self._status.read_termination = "\n" self._status.timeout = 10000 # Set timeout to 5 seconds self.connected = True except pyvisa.VisaIOError as e: raise ConnectionError(f"Error connecting to Keithley: {e}")
[docs] def disconnect(self): try: self._status.close() self._status = None self.connected = False except Exception as e: raise DisconnectionError(f"Error disconnecting from Keithley: {e}")
[docs] def check_connection(self): if self._status is None: raise ConnectionError("Keithley not connected.")
# ---------------------------------------------------- # Updating Measurement Parameters # ---------------------------------------------------- # TODO: Ensure users cannot make invalid entries - e.g. if the misspell nplc # TODO: Update the requirements to make them consistent with Keithley documentation # Used to update dataclasses
[docs] def update_settings(self, **kwargs): # Channel if "channel" in kwargs: if kwargs["channel"] not in {"a", "b"}: raise ValueError("Channel must be 'a' or 'b'.") else: self._settings.source.channel = kwargs["channel"] # Delay if "delay" in kwargs: if not isinstance(kwargs["delay"], (int, float)): raise ValueError("Delay must be a number") elif kwargs["delay"] <= 0: raise ValueError("Delay must be positive") else: self._settings.source.delay = kwargs["delay"] # Voltage if "voltage_level" in kwargs: if not isinstance(kwargs["voltage"], (int, float)): raise ValueError("Voltage must be a number") elif kwargs["voltage"] <= 0: raise ValueError("Voltage must be positive") else: self._settings.source.voltage_level = kwargs["voltage"] # Current if "current_level" in kwargs: if not isinstance(kwargs["current_level"], (int, float)): raise ValueError("Current must be a number") elif kwargs["current_level"] <= 0: raise ValueError("Current level must be positive") else: self._settings.source.current_level = kwargs["current_level"] # iv if "iv" in kwargs: if kwargs["iv"] not in {"i", "v"}: raise ValueError("iv must be 'i' or 'v'") else: self._settings.source.iv = kwargs["iv"] # Filter if "filter" in kwargs: if kwargs["filter"] not in {"FILTER_REPEAT_AVG"}: # TODO: Update this with more parameters ... raise ValueError("Delay must be a number") else: self._settings.measurement.filter = kwargs["filter"] # Filter count if "filter_count" in kwargs: if type(kwargs["filter_count"]) is not int: raise ValueError("Filter count must be an interger") elif kwargs["filter_count"] <= 0: raise ValueError("Filter count must be positive interger") else: self._settings.measurement.filter_count = kwargs["filter_count"] # Filter status if "filter_status" in kwargs: if kwargs["filter_status"] not in {False, True}: raise ValueError("Filter _status must be a boolean") else: self._settings.measurement.filter = kwargs["filter_status"] # Nplc if "nplc" in kwargs: if type(kwargs["nplc"]) is not int: raise ValueError("Nplc must be an interger") elif kwargs["nplc"] <= 0: raise ValueError("Nplc must be positive interger") else: self._settings.measurement.nplc = kwargs["nplc"] # Apply new settings self._apply_settings()
# ---------------------------------------------------- # Applying Settings Parameters # ---------------------------------------------------- def _apply_settings(self): try: # Update source settings if self._settings.source.iv == "v": self._status.write(f"smu{self._settings.source.channel}.source.func = smu{self._settings.source.channel}.OUTPUT_DCVOLTS") self._status.write(f"smu{self._settings.source.channel}.source.level{self._settings.source.iv} = {self._settings.source.voltage_level}") else: self._status.write(f"smu{self._settings.source.channel}.source.func = smu{self._settings.source.channel}.OUTPUT_DCCURRENT") self._status.write(f"smu{self._settings.source.channel}.source.level{self._settings.source.iv} = {self._settings.source.current_level}") self._status.write(f"smu{self._settings.source.channel}.source.delay = {self._settings.source.delay}") # Update measurement _settings self._status.write(f"smu{self._settings.source.channel}.measure.nplc = {self._settings.measurement.nplc}") if self._settings.measurement.filter_status: self._status.write( f"smu{self._settings.source.channel}.measure.filter.type = smu{self._settings.source.channel}.{self._settings.measurement.filter}" ) self._status.write(f"smu{self._settings.source.channel}.measure.filter.count = {self._settings.measurement.filter_count}") self._status.write(f"smu{self._settings.source.channel}.measure.filter.enable = smu{self._settings.source.channel}.FILTER_ON") else: self._status.write(f"smu{self._settings.source.channel}.measure.filter.enable = smu{self._settings.source.channel}.FILTER_OFF") except pyvisa.VisaIOError as e: raise ConnectionError(f"Connection error: {e}") except ValueError as e: raise ValueError(f"Value error: {e}") # ---------------------------------------------------- # Measure I and V # ----------------------------------------------------
[docs] def measure_resistance(self): try: # Measure resistance self._status.write(f"smu{self._settings.source.channel}.source.output = smu{self._settings.source.channel}.OUTPUT_ON") resistance = self._status.query(f"print(smu{self._settings.source.channel}.measure.r())") self._status.write(f"smu{self._settings.source.channel}.source.output = smu{self._settings.source.channel}.OUTPUT_OFF") return float(resistance) except pyvisa.VisaIOError as e: raise ConnectionError(f"Measurement error: {e}")
[docs] def measure_current(self): try: # Measure current self._status.write(f"smu{self._settings.source.channel}.source.output = smu{self._settings.source.channel}.OUTPUT_ON") current = self._status.query(f"print(smu{self._settings.source.channel}.measure.i())") self._status.write(f"smu{self._settings.source.channel}.source.output = smu{self._settings.source.channel}.OUTPUT_OFF") return float(current) # Checking for measurement error except pyvisa.VisaIOError as e: raise ConnectionError(f"Measurement error: {e}")
# ---------------------------------------------------- # Full Matix Fnc.s # ----------------------------------------------------
[docs] def upload_lua(self, path_to_script, limit=None): print("Uploading Lua script...") time.sleep(1) with open(path_to_script, encoding="utf-8") as f: for line in f: line_split = line.rstrip() print(line_split) if line_split: if limit is not None: new_line = line_split.replace("$num_of_trigger", str(limit)) self._status.write(new_line) time.sleep(0.01) print("Upload complete")
[docs] def read_keithley_buffer(self): n = int(float(self._status.query("print(smua.nvbuffer1.n)"))) print(f"Number of readings in buffer: {n}") data = [] for i in range(int(n)): reading = self._status.query(f"print(smua.nvbuffer1.readings[{i + 1}])") data.append(float(reading)) time.sleep(0.01) # add a small delay to avoid overwhelming the instrument return data
# ---------------------------------------------------- # Debugging / Displaying # ----------------------------------------------------
[docs] def print_settings(self): self.check_connection() print("-----Current Settings-----") print("NPLC :", self._status.query("print(smua.measure.nplc)").strip()) print("Delay :", self._status.query("print(smua.source.delay)").strip()) print("Voltage :", self._status.query("print(smua.source.levelv)").strip()) print("Filter :", self._status.query("print(smua.measure.filter.enable)").strip()) print("Filter Count:", self._status.query("print(smua.measure.filter.count)").strip()) print("######################################")