264 lines
8.8 KiB
Python
264 lines
8.8 KiB
Python
import pyvisa
|
|
# import pkg_resources
|
|
|
|
from ..base import Lock_In_Amp
|
|
from prsctrl.util.visa import enumerate_devices
|
|
|
|
import logging
|
|
log = logging.getLogger(__name__)
|
|
|
|
import numpy as np
|
|
|
|
class Model7260(Lock_In_Amp):
|
|
"""
|
|
Wrapper class for the Model 7260 DSP Lock-In controlled via pyvisa
|
|
"""
|
|
def __init__(self, instr, check_front_switch=True):
|
|
self.instr = instr
|
|
init_script = """
|
|
' INPUT
|
|
' voltage
|
|
IMODE 0
|
|
' set input A
|
|
VMODE 1
|
|
' set float
|
|
FLOAT 1
|
|
' set AC coupling
|
|
CP 0
|
|
' Enable 1x and 2x Line notch filters
|
|
LF 4
|
|
' Enable synchronous filter < 200Hz
|
|
' SYNC 1
|
|
' REFERENCE
|
|
' Set external TTL rear panel
|
|
IE 1
|
|
' Set detection harmonic to 1'
|
|
REFN 1
|
|
' TODO time constant, filter slope,
|
|
"""
|
|
self.run(init_script)
|
|
|
|
self._buffer_length = None
|
|
|
|
def __del__(self):
|
|
"""Properly close the instrument connection"""
|
|
self.instr.close()
|
|
|
|
# RUN COMMANDS ON THE DEVICE
|
|
def run(self, code, split=True):
|
|
"""
|
|
Run SCPI code on the device by writing it.
|
|
Empty lines, leading whitespaces and lines starting with ' or # are ignored.
|
|
Parameters
|
|
----------
|
|
code : str
|
|
SCPI commands
|
|
"""
|
|
script = [] if split else ''
|
|
for line in code.strip(" ").split("\n"):
|
|
l = line.strip(" ")
|
|
if len(l) == 0 or l[0] in "#'": continue
|
|
if split:
|
|
script.append(l)
|
|
else:
|
|
script += l + "\n"
|
|
if split:
|
|
for cmd in script:
|
|
self.run_and_check(cmd)
|
|
else:
|
|
self.run_and_check(script)
|
|
|
|
def run_and_check(self, cmd):
|
|
"""Run a command and check for errors afterwards by reading the
|
|
standard event status byte"""
|
|
try:
|
|
log.debug(f"Running command(s): --BEGIN--\n{cmd}\n--END--")
|
|
ret = self.instr.write(cmd)
|
|
status = int(self.query("ST"))
|
|
if status & (1 << 1):
|
|
raise RuntimeError(f"Invalid command(s):\n{cmd}\n")
|
|
elif status & (1 << 5):
|
|
raise RuntimeError(f"Command parameter error after command(s):\n{cmd}\n")
|
|
# if ret != 8:
|
|
# raise RuntimeError(f"Error while writing command(s):\n'{script}'\n\nDevice returned code {ret}")
|
|
except pyvisa.VisaIOError as e:
|
|
raise RuntimeError(f"VisaIOError raised while writing command(s):\n'{cmd}'\n\nVisaIOError:\n{e}")
|
|
|
|
def get_frequency(self) -> float:
|
|
# TODo
|
|
pass
|
|
|
|
def query(self, query):
|
|
return self.instr.query(query, delay=0.05).strip("\n")
|
|
|
|
def snap(self, what="3,4,5,7"):
|
|
"""
|
|
Parameters
|
|
----------
|
|
what : TYPE, optional
|
|
1 X
|
|
2 Y
|
|
3 R
|
|
4 θ
|
|
5 Aux In 1
|
|
6 Aux In 2
|
|
7 Aux In 3
|
|
8 Aux In 4
|
|
9 Reference Frequency
|
|
10 CH1 display
|
|
11 CH2 display
|
|
The default is "3,4,5,7".
|
|
|
|
Returns
|
|
-------
|
|
vals : list[float]
|
|
Values converted to float
|
|
"""
|
|
vals = self.query(f"SNAP? {what}").split(",")
|
|
vals = map(float, vals)
|
|
return vals
|
|
|
|
def measureTODO(): pass
|
|
|
|
def read_value(self):
|
|
"""Read the value of R"""
|
|
return float(self.query("MAG."))
|
|
|
|
def reset(self):
|
|
# TODO
|
|
pass
|
|
|
|
def test_connection(self):
|
|
pass
|
|
|
|
def buffer_setup(self, X=0, Y=0, MAG=1, PHASE=1, SENS=0, ADC1=1, ADC2=2, length=None, interval_ms=5):
|
|
"""
|
|
Prepare the device for a buffer (curve) measurement
|
|
|
|
:param X: 1 if the output should be stored, 0 else
|
|
:param Y: 1 if the output should be stored, 0 else
|
|
:param MAG: 1 if the output should be stored, 0 else
|
|
:param PHASE: 1 if the output should be stored, 0 else
|
|
:param SENS: 1 if the output should be stored, 0 else
|
|
:param ADC1: 1 if the output should be stored, 0 else
|
|
:param ADC2: 1 if the output should be stored, 0 else
|
|
:param length: Number of values to store in the buffer, or None to use the maximum number of values
|
|
:param interval_ms: Interval in ms with resolution of 5ms
|
|
"""
|
|
self.buffer_cbd = (X << 0) | (Y << 1) | (MAG << 2) | (PHASE << 3) | (SENS << 4) | (ADC1 << 5) | (ADC2 << 6)
|
|
self.run(f"CBD {self.buffer_cbd}")
|
|
n_outputs = self.buffer_cbd.bit_count()
|
|
|
|
# length
|
|
max_length = 32768 // n_outputs
|
|
log.info(f"Setting up buffer, n_outputs={n_outputs}, max_length={max_length}, CBD={self.buffer_cbd:016b}")
|
|
if length is None:
|
|
self._buffer_length = max_length
|
|
else:
|
|
self._buffer_length = length
|
|
if self._buffer_length > max_length:
|
|
raise ValueError(f"For {n_outputs} outputs, the maximum length is {max_length} but {length} was given.")
|
|
self.run(f"LEN {self._buffer_length}")
|
|
|
|
# set the interval
|
|
self.run(f"STR {interval_ms}")
|
|
# init the buffer (new curve)
|
|
self.run("NC")
|
|
|
|
def buffer_start_fill(self):
|
|
if self._buffer_length is None: raise RuntimeError(f"Buffer not set up, call buffer_setup() first.")
|
|
self.run("TD") # take data
|
|
|
|
def buffer_is_done(self) -> bool:
|
|
vals = self.query("M").split(",")
|
|
vals = list(map(int, vals))
|
|
log.info(f"buffer status: {vals[0]}, {vals[1]}, {vals[2]:08b}, {vals[3]}")
|
|
return vals[0] == 0
|
|
|
|
def buffer_get_data(self, raw=False):
|
|
"""
|
|
Get the data from the buffer.
|
|
|
|
:return:
|
|
"""
|
|
if self._buffer_length is None: raise RuntimeError(f"Buffer not set up, call buffer_setup() first.")
|
|
n_points = int(self.query("M").split(",")[3])
|
|
data = np.empty((n_points, self.buffer_cbd.bit_count()), dtype=int)
|
|
self.instr.write(f"DCT {self.buffer_cbd}") # this is a query, we need to read the next <n_points> lines now
|
|
for i in range(n_points):
|
|
vals = self.instr.read().strip("\n").split(",")
|
|
data[i,:] = [int(x) for x in vals]
|
|
# check if all values have been transmitted by issuing another command
|
|
length = self.query("LEN")
|
|
try:
|
|
length = int(length)
|
|
except ValueError:
|
|
raise RuntimeError(f"Error buffer data transmission: Buffer length {length} is not an integer.")
|
|
if length != self._buffer_length:
|
|
raise RuntimeError(f"Error buffer data transmission: Buffer length reported as {length}, but should be {self._buffer_length}.")
|
|
if raw:
|
|
return data
|
|
return self._buffer_convert_from_full_scale(data)
|
|
|
|
def _buffer_convert_from_full_scale(self, data):
|
|
"""
|
|
Convert values from fullscale to Volts and Degrees
|
|
|
|
See section 6.4.09 on page 141 in the manual
|
|
|
|
:param data: full scale data
|
|
:return: data converted to Volt and Degrees
|
|
"""
|
|
# get full voltage scale
|
|
# if the sensitivity would have been stored (bit 4), its value could be used too
|
|
fs = float(self.query("SEN."))
|
|
scales = [
|
|
1E-4*fs, # X
|
|
1E-4*fs, # Y
|
|
1E-4*fs, # MAG
|
|
1E-2, # Phase FS=18000 = 180°
|
|
1, # Sens
|
|
1E-3, # ADC1 FS=10000 = 10 V
|
|
1E-3, # ADC2 FS=10000 = 10 V
|
|
1E-3, # ADC3 FS=10000 = 10 V
|
|
]
|
|
data_conv = np.empty(data.shape, dtype=float)
|
|
i_bit = 0
|
|
for i_val in range(self.buffer_cbd.bit_count()):
|
|
while i_bit < len(scales) and self.buffer_cbd & (1 << i_bit) == 0:
|
|
i_bit += 1
|
|
if i_bit == len(scales):
|
|
raise RuntimeError(f"Could not find the output of value number {i_val+1}")
|
|
# i_valth value is the quantity belonging to bit i_bit
|
|
data_conv[:,i_val] = data[:,i_val] * scales[i_bit]
|
|
return data_conv
|
|
|
|
|
|
|
|
|
|
def auto_gain(self):
|
|
self.instr.write("AGAN")
|
|
# wait until the device responds again, meaning it is no longer busy
|
|
self.instr.write("SENS?")
|
|
i = 0
|
|
while i < 50:
|
|
try:
|
|
gain = self.instr.read().strip("\n")
|
|
return gain
|
|
except:
|
|
i += 1
|
|
raise RuntimeError("Could not get response from device after sending auto gain command")
|
|
|
|
@staticmethod
|
|
def enumerate_devices(query="(GPIB)?*:INSTR"):
|
|
return enumerate_devices("7260", query, cmd="ID")
|
|
|
|
@staticmethod
|
|
def connect_device(name):
|
|
rm = pyvisa.ResourceManager()
|
|
instr = rm.open_resource(name)
|
|
return Model7260(instr)
|
|
|
|
def __str__(self):
|
|
return "SR830"
|