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("######################################")