Fixes/lamni3 #353
No files matched your search
@@ -288,10 +288,27 @@ poly:
|
||||
userParameter:
|
||||
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
|
||||
init_position: -23
|
||||
diamond_500mu: 0.8
|
||||
diamond_300mu: -9.75
|
||||
bl_smar_stage: 3
|
||||
deviceTags:
|
||||
- bl_optics_hutch
|
||||
|
||||
polrotz:
|
||||
description: "polarizer 2 crystal rotation"
|
||||
deviceClass: ophyd_devices.EpicsMotorEC
|
||||
deviceConfig:
|
||||
prefix: "X12SA-OP-POL2:ROTZ"
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
userParameter:
|
||||
zero_degrees: 7.7
|
||||
deviceTags:
|
||||
- bl_optics_hutch
|
||||
|
||||
scinx:
|
||||
description: scintillator in OPbox
|
||||
deviceClass: csaxs_bec.devices.mcs2.mcs2_ophyd.Mcs2Motor
|
||||
@@ -312,7 +329,8 @@ scinx:
|
||||
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
|
||||
init_position: -23
|
||||
bl_smar_stage: 4
|
||||
in_position: -12.5
|
||||
scin_in_position: -12.5
|
||||
diode_in_position: 10
|
||||
deviceTags:
|
||||
- bl_optics_hutch
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ detectors:
|
||||
#sastt:
|
||||
# - !include ./sastt.yaml
|
||||
|
||||
sastt:
|
||||
- !include ./sastt_aerotech.yaml
|
||||
# sastt:
|
||||
# - !include ./sastt_aerotech.yaml
|
||||
|
||||
# flomni:
|
||||
# - !include ./ptycho_flomni.yaml
|
||||
@@ -34,8 +34,8 @@ sastt:
|
||||
#omny:
|
||||
# - !include ./ptycho_omny.yaml
|
||||
|
||||
# lamni:
|
||||
# - !include ./ptycho_lamni.yaml
|
||||
lamni:
|
||||
- !include ./ptycho_lamni.yaml
|
||||
|
||||
# user setup:
|
||||
# - !include ./user_setup.yaml
|
||||
|
||||
@@ -207,7 +207,10 @@ losax:
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: -1.6437
|
||||
#250 micron, 30 nm, 7.9 kev, loptz at 80.8429
|
||||
#in: -1.6437
|
||||
#250 micron, 30 nm, 7.24 kev, loptz at
|
||||
in: -1.603676
|
||||
deviceTags:
|
||||
- ptycho_lamni
|
||||
losay:
|
||||
@@ -227,7 +230,10 @@ losay:
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: -0.2109
|
||||
#250 micron, 30 nm, 7.9 kev
|
||||
#in: -0.2109
|
||||
#250 micron, 30 nm, 7.24 kev
|
||||
in: -0.150825
|
||||
out: 3.8
|
||||
deviceTags:
|
||||
- ptycho_lamni
|
||||
@@ -248,7 +254,10 @@ losaz:
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
userParameter:
|
||||
in: 6.8288
|
||||
#250 micron, 30 nm, 7.9 kev
|
||||
#in: 6.8288
|
||||
#250 micron, 30 nm, 7.24 kev
|
||||
in: 1.828631
|
||||
out: -3
|
||||
deviceTags:
|
||||
- ptycho_lamni
|
||||
@@ -354,8 +363,14 @@ rt_positions:
|
||||
############################################################
|
||||
# RS-232 through the TCP-serial bridge on mpc-lamni (service lamni-serial-keithley, TCP 3336).
|
||||
# The Keithley is not always connected; onFailure: buffer keeps the session running without it.
|
||||
# compliance_voltage: the output never drives more than this, also with the load disconnected.
|
||||
# remote_sense: true needs the sense leads connected at the load.
|
||||
# compliance_voltage: voltage limit in V. With remote_sense it is measured at the sense leads, so a
|
||||
# broken sense lead lets the output rise above it, up to ovp_voltage.
|
||||
# remote_sense: true for 4-wire sensing, needs the sense leads connected at the load. The measured
|
||||
# voltage (dev.keithley.voltage, front panel) is then the voltage at the load.
|
||||
# ovp_voltage: output voltage protection, hard limit at the output terminals in any sense mode.
|
||||
# Possible values: 20, 40, 60, 80, 100, 120, 160 (V), or null for no limit. 20 V is the lowest
|
||||
# possible. Must not be below compliance_voltage.
|
||||
# All settings are only applied when the output is off at connect.
|
||||
keithley:
|
||||
description: Keithley 2400 current source
|
||||
deviceClass: csaxs_bec.devices.keithley.keithley_2400.Keithley2400
|
||||
@@ -368,7 +383,8 @@ keithley:
|
||||
tolerance: 0.01
|
||||
compliance_voltage: 5
|
||||
source_range_ma: 10
|
||||
remote_sense: false
|
||||
remote_sense: true
|
||||
ovp_voltage: 20
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
|
||||
Executable → Regular
File mode changed.
@@ -17,6 +17,7 @@ keithley:
|
||||
compliance_voltage: 5
|
||||
source_range_ma: 10
|
||||
remote_sense: false
|
||||
ovp_voltage: 20
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
|
||||
@@ -43,6 +43,8 @@ class Keithley2400Controller(Controller):
|
||||
"""SCPI communication with the Keithley 2400."""
|
||||
|
||||
_axes_per_controller = 1
|
||||
# :SOUR:VOLT:PROT takes only these levels in V, None stands for NONE (no limit).
|
||||
OVP_LEVELS = (20, 40, 60, 80, 100, 120, 160)
|
||||
READBACK_MAX_AGE = 0.1 # s, the voltage and current signals share one :READ?
|
||||
|
||||
def _initialize(self):
|
||||
@@ -80,7 +82,11 @@ class Keithley2400Controller(Controller):
|
||||
|
||||
@threadlocked
|
||||
def configure(
|
||||
self, compliance_voltage: float, source_range_ma: float, remote_sense: bool
|
||||
self,
|
||||
compliance_voltage: float,
|
||||
source_range_ma: float,
|
||||
remote_sense: bool,
|
||||
ovp_voltage: float | None = None,
|
||||
) -> None:
|
||||
"""Set up the current source. With the output on only harmless settings are sent."""
|
||||
idn = self.socket_put_and_receive("*IDN?")
|
||||
@@ -102,6 +108,11 @@ class Keithley2400Controller(Controller):
|
||||
f"Keithley output is on, compliance stays at {actual} V "
|
||||
f"(configured: {compliance_voltage} V)."
|
||||
)
|
||||
if ovp_voltage is not None and not np.isclose(self.get_ovp_voltage(), ovp_voltage):
|
||||
logger.warning(
|
||||
f"Keithley output is on, output voltage protection stays at "
|
||||
f"{self.get_ovp_voltage()} V (configured: {ovp_voltage} V)."
|
||||
)
|
||||
else:
|
||||
for command in (
|
||||
":SOUR:FUNC CURR",
|
||||
@@ -113,8 +124,15 @@ class Keithley2400Controller(Controller):
|
||||
f":SYST:RSEN {'ON' if remote_sense else 'OFF'}",
|
||||
":ARM:COUN 1",
|
||||
":TRIG:COUN 1",
|
||||
f":SOUR:VOLT:PROT {'NONE' if ovp_voltage is None else int(ovp_voltage)}",
|
||||
):
|
||||
self.socket_put(command)
|
||||
if ovp_voltage is not None:
|
||||
actual = self.get_ovp_voltage()
|
||||
if not np.isclose(actual, ovp_voltage):
|
||||
raise Keithley2400Error(
|
||||
f"Output voltage protection is {actual} V, expected {ovp_voltage} V."
|
||||
)
|
||||
self.set_compliance_voltage(compliance_voltage)
|
||||
actual = self.get_compliance_voltage()
|
||||
if not np.isclose(actual, compliance_voltage):
|
||||
@@ -147,6 +165,10 @@ class Keithley2400Controller(Controller):
|
||||
def set_compliance_voltage(self, volts: float) -> None:
|
||||
self.socket_put(f":SENS:VOLT:PROT {volts:.6E}")
|
||||
|
||||
def get_ovp_voltage(self) -> float:
|
||||
"""Output voltage protection in V, effective at the output terminals also with remote sense."""
|
||||
return float(self.socket_put_and_receive(":SOUR:VOLT:PROT?"))
|
||||
|
||||
def in_compliance(self) -> bool:
|
||||
return self.socket_put_and_receive(":SENS:VOLT:PROT:TRIP?") == "1"
|
||||
|
||||
@@ -237,9 +259,13 @@ class Keithley2400(Device, PositionerBase):
|
||||
Args:
|
||||
limits: Allowed source current in mA.
|
||||
tolerance: Allowed difference between measured and requested current in mA.
|
||||
compliance_voltage: Voltage limit in V.
|
||||
compliance_voltage: Voltage limit in V. With remote sense it is measured at the sense
|
||||
leads, so an open sense lead lets the output rise above it up to ovp_voltage.
|
||||
source_range_ma: Fixed source range in mA, fixed so level changes cause no range glitch.
|
||||
remote_sense: True for 4-wire sensing, needs the sense leads connected at the load.
|
||||
ovp_voltage: Output voltage protection in V, a hard limit at the output terminals
|
||||
independent of the sense mode. Only 20, 40, 60, 80, 100, 120 or 160 V, or None for
|
||||
no limit. Must not be below compliance_voltage.
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["output_on", "output_off", "status"]
|
||||
@@ -280,6 +306,7 @@ class Keithley2400(Device, PositionerBase):
|
||||
compliance_voltage: float = 5.0,
|
||||
source_range_ma: float = 10.0,
|
||||
remote_sense: bool = False,
|
||||
ovp_voltage: float | None = None,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
@@ -292,6 +319,17 @@ class Keithley2400(Device, PositionerBase):
|
||||
self._compliance_voltage = compliance_voltage
|
||||
self._source_range_ma = source_range_ma
|
||||
self._remote_sense = remote_sense
|
||||
if ovp_voltage is not None:
|
||||
if ovp_voltage not in Keithley2400Controller.OVP_LEVELS:
|
||||
raise ValueError(
|
||||
f"ovp_voltage {ovp_voltage} V not one of "
|
||||
f"{Keithley2400Controller.OVP_LEVELS} V or None."
|
||||
)
|
||||
if compliance_voltage > ovp_voltage:
|
||||
raise ValueError(
|
||||
f"compliance_voltage {compliance_voltage} V above ovp_voltage {ovp_voltage} V."
|
||||
)
|
||||
self._ovp_voltage = ovp_voltage
|
||||
|
||||
super().__init__(
|
||||
prefix,
|
||||
@@ -313,7 +351,7 @@ class Keithley2400(Device, PositionerBase):
|
||||
def wait_for_connection(self, timeout: float = 30.0) -> bool:
|
||||
self.controller.on(timeout=timeout)
|
||||
self.controller.configure(
|
||||
self._compliance_voltage, self._source_range_ma, self._remote_sense
|
||||
self._compliance_voltage, self._source_range_ma, self._remote_sense, self._ovp_voltage
|
||||
)
|
||||
|
||||
def destroy(self):
|
||||
@@ -393,13 +431,24 @@ class Keithley2400(Device, PositionerBase):
|
||||
)
|
||||
raise Keithley2400Error(f"Requested {position} mA, measured {amps * 1e3} mA.{hint}")
|
||||
|
||||
def _publish_reading(self) -> None:
|
||||
"""Push a fresh reading to BEC, an RPC call alone leaves the cached values untouched."""
|
||||
self._run_subs(
|
||||
sub_type=self.SUB_READBACK, value=self.readback.read(), timestamp=time.time()
|
||||
)
|
||||
|
||||
def output_on(self) -> None:
|
||||
"""Switch the output on at the programmed level. Use ``umv`` to set a new level."""
|
||||
self.controller.set_output(True)
|
||||
try:
|
||||
self._wait_for_current(self.controller.get_level() * 1e3)
|
||||
finally:
|
||||
self._publish_reading()
|
||||
|
||||
def output_off(self) -> None:
|
||||
"""Switch the output off. The programmed level is kept."""
|
||||
self.controller.set_output(False)
|
||||
self._publish_reading()
|
||||
|
||||
def status(self) -> None:
|
||||
"""Print the state of the instrument."""
|
||||
@@ -414,6 +463,7 @@ class Keithley2400(Device, PositionerBase):
|
||||
["Measured voltage", f"{volts:.4f} V"],
|
||||
["Compliance voltage", f"{self.controller.get_compliance_voltage()} V"],
|
||||
["In compliance", "YES" if self.controller.in_compliance() else "no"],
|
||||
["Output voltage protection", f"{self.controller.get_ovp_voltage()} V"],
|
||||
]
|
||||
)
|
||||
print(table)
|
||||
|
||||
@@ -19,6 +19,7 @@ class FakeKeithleySocket:
|
||||
level = 0.0
|
||||
source_function = "VOLT"
|
||||
compliance = 21.0
|
||||
ovp = 210.0 # NONE
|
||||
max_volts = 100.0 # load limit, above it the instrument sits in compliance
|
||||
resistance = 1000.0
|
||||
|
||||
@@ -50,6 +51,7 @@ class FakeKeithleySocket:
|
||||
":SOUR:FUNC?": self.source_function,
|
||||
":SOUR:CURR?": f"{self.level:E}",
|
||||
":SENS:VOLT:PROT?": f"{self.compliance:E}",
|
||||
":SOUR:VOLT:PROT?": f"{self.ovp:E}",
|
||||
":SYST:ERR?": '0,"No error"',
|
||||
}.get(command)
|
||||
if command == ":READ?":
|
||||
@@ -65,6 +67,9 @@ class FakeKeithleySocket:
|
||||
type(self).level = float(command.split()[1])
|
||||
elif command.startswith(":SENS:VOLT:PROT "):
|
||||
type(self).compliance = float(command.split()[1])
|
||||
elif command.startswith(":SOUR:VOLT:PROT "):
|
||||
value = command.split()[1]
|
||||
type(self).ovp = 210.0 if value == "NONE" else float(value)
|
||||
if reply is not None:
|
||||
data = f"{reply}\r\n".encode()
|
||||
self.replies += [part for part in (data[:3], data[3:]) if part]
|
||||
@@ -95,7 +100,60 @@ def test_connect_with_output_off_configures_and_keeps_it_off(keithley, socket_cl
|
||||
assert socket_cls.source_function == "CURR"
|
||||
assert socket_cls.compliance == 5.0
|
||||
assert socket_cls.output is False
|
||||
assert not any("OUTP ON" in cmd or "MEAS" in cmd or cmd == "*RST" for cmd in commands(socket_cls))
|
||||
assert not any(
|
||||
"OUTP ON" in cmd or "MEAS" in cmd or cmd == "*RST" for cmd in commands(socket_cls)
|
||||
)
|
||||
|
||||
|
||||
def test_connect_sets_ovp_and_remote_sense(socket_cls):
|
||||
dev = Keithley2400(
|
||||
name="keithley",
|
||||
socket_cls=socket_cls,
|
||||
host="fake",
|
||||
port=1,
|
||||
device_manager=mock.MagicMock(),
|
||||
remote_sense=True,
|
||||
ovp_voltage=20,
|
||||
)
|
||||
try:
|
||||
dev.wait_for_connection()
|
||||
assert socket_cls.ovp == 20.0
|
||||
assert ":SYST:RSEN ON" in commands(socket_cls)
|
||||
finally:
|
||||
dev.controller.off(update_config=False)
|
||||
|
||||
|
||||
def test_connect_with_output_on_keeps_ovp(socket_cls):
|
||||
socket_cls.output, socket_cls.level, socket_cls.source_function = True, 2e-3, "CURR"
|
||||
dev = Keithley2400(
|
||||
name="keithley",
|
||||
socket_cls=socket_cls,
|
||||
host="fake",
|
||||
port=1,
|
||||
device_manager=mock.MagicMock(),
|
||||
ovp_voltage=20,
|
||||
)
|
||||
try:
|
||||
dev.wait_for_connection()
|
||||
assert socket_cls.ovp == 210.0
|
||||
assert not any(cmd.startswith(":SOUR:VOLT:PROT ") for cmd in commands(socket_cls))
|
||||
finally:
|
||||
dev.controller.off(update_config=False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"kwargs", [{"ovp_voltage": 25}, {"ovp_voltage": 20, "compliance_voltage": 30}]
|
||||
)
|
||||
def test_invalid_ovp_rejected(socket_cls, kwargs):
|
||||
with pytest.raises(ValueError):
|
||||
Keithley2400(
|
||||
name="keithley",
|
||||
socket_cls=socket_cls,
|
||||
host="fake",
|
||||
port=1,
|
||||
device_manager=mock.MagicMock(),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def test_connect_with_output_on_leaves_the_source_alone(keithley, socket_cls):
|
||||
@@ -189,3 +247,18 @@ def test_closed_connection_raises(keithley, socket_cls):
|
||||
socket_cls.instances[-1].put = lambda msg: None
|
||||
with pytest.raises(ControllerCommunicationError):
|
||||
keithley.controller.socket_put_and_receive(":OUTP?")
|
||||
|
||||
|
||||
def test_output_on_and_off_publish_a_fresh_reading(keithley, socket_cls):
|
||||
keithley.wait_for_connection()
|
||||
keithley.move(1.5).wait(timeout=5)
|
||||
published = []
|
||||
keithley.subscribe(
|
||||
lambda *_, obj, **__: published.append(obj.root.read()), event_type="readback", run=False
|
||||
)
|
||||
keithley.output_off()
|
||||
assert published[-1]["keithley"]["value"] == 0.0
|
||||
assert published[-1]["keithley_output_enabled"]["value"] == 0
|
||||
keithley.output_on()
|
||||
assert published[-1]["keithley"]["value"] == pytest.approx(1.5)
|
||||
assert published[-1]["keithley_output_enabled"]["value"] == 1
|
||||
Reference in new issue
Block a user