Fixes/lamni3 #353

Merged
holler merged 4 commits from fixes/lamni3 into main 2026-11-06 12:40:56 +01:00
7 changed files with 173 additions and 15 deletions

No files matched your search

+19 -1
View File
@@ -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
+4 -4
View File
@@ -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
+22 -6
View File
@@ -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
View File
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
+53 -3
View File
@@ -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)
+74 -1
View File
@@ -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