import time
import numpy as np
import pathlib
from . import base
[docs]
class SourceMeter_KI2400(base.VisaHardwareBase):
"""Source meter (Langmuir experiment)
Model name: KEITHLEY KI2400
Communication:
- RS-232 (USB adapter), using VISA protocol and SCPI commands
"""
SAVE_DIR = pathlib.Path.home() / "saved_data"
def __init__(self, visa_string, **kwargs):
super().__init__(visa_string, baud_rate=57600, **kwargs)
self._last = {
"channel" : 0,
}
self._progress = 0
# ==== Inherited abstract methods ====
def _safestate(self):
"""Safety kill-switch. Turns off anything dangerous after interrupt."""
self.raw_scpi(":OUTPUT:STATE 0")
# def _readerror(self):
# """Read new error from device."""
# # @TODO
def _defaultstate(self):
"""Init device to a well defined state. (Auto called after connect.)"""
self.raw_scpi(":SOURCE:CLEAR:AUTO 1")
self.clear_channels()
# @TODO
# This method should run a series of commands to bring the device to some well defined state
# Probably: *RST, set source voltage = 0, disable output, etc.
# def _clear_defaultstate(self):
# """Clean-up device settings (i.e. keys lock). (Auto called before disconnect.)"""
# # @TODO
# ==== Commands ====
[docs]
@base.base_command
def read_data(self, dc):
"""Read data from the buffer."""
self.log.info("Fetching sweep data (~ 10s)")
data = self.raw_scpi(":DATA:DATA?", "ascii", container=np.array, delay=10)
self.SAVE_DIR.mkdir(exist_ok=True)
filepath = "{}_sweep_{}.txt".format(type(self).__name__, time.asctime())
filepath = self.SAVE_DIR / filepath.replace(" ", "_").replace(":", ",")
h = "Unspecified sweep data"
np.savetxt(filepath, data, fmt="%.6e", header=h)
with filepath.open("r") as file:
dc.emit_datafile(file, filepath.name)
self.log.info("Fetched sweep data from the device!")
return str(filepath)
[docs]
@base.base_command
def set_voltage(self, voltage):
"""Set the source voltage [V]"""
voltage = float(voltage) # In device classes, the method arguments always need to be converted from string
if not (-210 <= voltage <= 210) :
raise base.CommandError("Invalid voltage value, use -210 to 210")
self.raw_scpi(f":SOURCE:VOLTAGE {voltage:.2f}")
[docs]
@base.base_command
def voltage(self):
"""Read the source voltage [V]"""
return float(self.raw_scpi(":SOURCE:VOLTAGE?"))
[docs]
@base.base_command
def measure_current(self, voltage):
"""Execute a one-off measurment of current and return the result [A]"""
voltage = float(voltage)
self.set_voltage(voltage)
current = self.raw_scpi(":MEASURE?", "ascii")[1]
return current
@base.base_command
def _configure_sweep (self, v_min, v_max, v_step, n_steps):
self.raw_scpi(":TRACE:CLEAR")
self.raw_scpi(":TRIGGER:CLEAR")
self.raw_scpi(":TRACE:FEED:CONTROL NEVER")
self.raw_scpi(":SOURCE:VOLTAGE:MODE SWEEP")
self.raw_scpi(f":SOURCE:VOLTAGE:START {v_min:.2f}")
self.raw_scpi(f":SOURCE:VOLTAGE:STOP {v_max:.2f}")
self.raw_scpi(f":SOURCE:VOLTAGE:STEP {v_step}")
self.raw_scpi(":SOURCE:FUNCTION:MODE VOLTAGE")
self.raw_scpi(f":TRIGGER:COUNT {n_steps}")
self.raw_scpi(":TRIGGER:DELAY MIN")
self.raw_scpi(f":TRACE:POINTS {n_steps}")
self.raw_scpi(":TRACE:FEED:CONTROL NEXT")
@base.base_command
def _save_data(self,dc):
data = self.raw_scpi(":TRACE:DATA?","ascii", container=np.array, delay = 5)
data = data.reshape((-1,5)).T[[3,0,1]]
self.SAVE_DIR.mkdir(exist_ok=True)
filepath = "{}_sweep_{}.txt".format(type(self).__name__, time.asctime())
filepath = self.SAVE_DIR / filepath.replace(" ", "_").replace(":", "-")
h = "Time (s), Voltage (V), Current (A)"
np.savetxt(filepath, data.T, fmt="%.6e", header=h)
if dc is not None:
with filepath.open("r") as file:
dc.emit_datafile(file, filepath.name)
dc.emit("graph", {
"title": "IV Characteristics",
"x": data[1].tolist(),
"y": data[2].tolist(),
"xlabel": "Voltage [V]",
"ylabel": "Current [A]",
"id": "iv_characteristics"
})
return data
[docs]
@base.base_command
def sweep(self,dc,v_min, v_max, v_step):
"""Execute a sweep measurement and save the data to a file"""
v_min = float(v_min)
v_max = float(v_max)
v_step = float (v_step)
if not (-210 <= v_min <= 210) :
raise base.CommandError("Invalid v_min value, use -210 to 210")
if not (-210 <= v_max <= 210) :
raise base.CommandError("Invalid v_max value, use -210 to 210")
if not (-420 <= v_step <= 420) :
raise base.CommandError("Invalid v_step value, use -420 to 420")
n_steps = abs(int((v_max-v_min)/v_step))+1
self.log.info(f"Sweep started {v_min:.1f} {v_max:.1f} {v_step:.1f}")
"""Configuring the sweep"""
self._configure_sweep(v_min, v_max, v_step, n_steps)
time.sleep(1)
self.raw_scpi(":INITIATE")
time.sleep((n_steps/16)*1.2)
"""Saving the data"""
data = self._save_data(dc)
return data
[docs]
@base.base_command
def set_channel(self, channel):
channel = int(channel)
if (channel == 1):
self._last["channel"] = 1
self.raw_scpi(":SOURCE2:TTL 6")
elif (channel == 2):
self._last["channel"] = 2
self.raw_scpi(":SOURCE2:TTL 5")
elif(channel == 3):
self._last["channel"] = 3
self.raw_scpi(":SOURCE2:TTL 3")
else:
raise base.CommandError ("The number of the channel should be 1, 2 or 3")
[docs]
@base.base_command
def clear_channels(self):
self.raw_scpi(":SOURCE2:TTL 7")
# ==== Methods/commands related to DeviceClient ====
@base.idle_command
def _idle_update_all(self):
self._last["channel"] = {
7: 0,
6: 1,
5: 2,
3 :3,
}[int(self.raw_scpi(":SOURCE2:TTL:ACTUAL?"))]
[docs]
def update_frontend(self, device_client):
# Not an idle command, it does not communicate with hardware
# => frontend can be updated even when a command is running
self._idle_update_all()
value = self._last["channel"]
device_client.emit("value", {
"value": value,
"formatted": str(value),
"label": "Channel",
"min": 0,
"max": 3,
"id": "Channel",
})