Skip to content

Commit 4c14fa7

Browse files
committed
refactor: Fix thread leak in SimPositioner, improve callback in SimMonitor
1 parent 76f3bf5 commit 4c14fa7

6 files changed

Lines changed: 61 additions & 19 deletions

File tree

ophyd_devices/sim/sim_data.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def __init__(self, *args, parent=None, **kwargs) -> None:
108108
self.parent = parent
109109
self.sim_state = defaultdict(dict)
110110
self.registered_proxies = getattr(self.parent, "registered_proxies", {})
111-
self._model = {}
111+
self._model = None
112112
self._model_params = None
113113
self._params = {}
114114

@@ -329,6 +329,7 @@ def __init__(self, *args, parent=None, **kwargs) -> None:
329329
self._model_lookup = self.init_lmfit_models()
330330
super().__init__(*args, parent=parent, **kwargs)
331331
self.bit_depth = self.parent.BIT_DEPTH
332+
self._cb_registered = False
332333
self._init_default()
333334

334335
@SimulatedDataBase.params.setter
@@ -341,9 +342,22 @@ def _add_callback_to_motor(self) -> None:
341342
mot_name = self.params.get("ref_motor", "")
342343
if not hasattr(self.parent, "device_manager"):
343344
return
345+
if not hasattr(self.parent.device_manager, "devices"):
346+
return
344347
if mot_name in self.parent.device_manager.devices:
345348
if hasattr(self.parent, "setup_readback_monitor"):
346349
self.parent.setup_readback_monitor(mot_name)
350+
# pylint: disable=protected-access
351+
if (
352+
hasattr(self.parent, "_registered_callback")
353+
and self.parent._registered_callback is not None
354+
and self.parent._registered_callback.motor == mot_name
355+
):
356+
# If the callback was registered, keep track of it
357+
self._cb_registered = True
358+
else:
359+
# If no callback was registered, this should also be reflected in self._cb_registered
360+
self._cb_registered = False
347361

348362
def select_model(self, model: str) -> None:
349363
"""
@@ -396,6 +410,8 @@ def get_params_for_model_cls(self) -> dict:
396410
dict: {name: value} for the active simulation model.
397411
"""
398412
rtr = {}
413+
if not isinstance(self._model, Model):
414+
return rtr
399415
params = self._model.make_params()
400416
for name, parameter in params.items():
401417
if name in DEFAULT_PARAMS_LMFIT:
@@ -465,6 +481,10 @@ def _compute(self, *args, **kwargs) -> int:
465481
mot_name = self.params.get("ref_motor", "")
466482
if self.parent.device_manager and mot_name in self.parent.device_manager.devices:
467483
motor_pos = self.parent.device_manager.devices[mot_name].obj.read()[mot_name]["value"]
484+
# It can happen that the SimMonitor was created before the motor was available in the device manager,
485+
# therefore we have to check if a callback is registered and update it if not.
486+
if not self._cb_registered:
487+
self._add_callback_to_motor()
468488
else:
469489
motor_pos = 0
470490
method = self._model

ophyd_devices/sim/sim_positioner.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def __init__(
8585
self.precision = precision
8686
self.sim_init = sim_init
8787
self._registered_proxies = {}
88+
self._lock = threading.RLock()
8889

8990
self.update_frequency = update_frequency
9091
self._stopped = False
@@ -186,21 +187,25 @@ def _move_to_setpoint(self) -> None:
186187
raise DeviceStopError(f"{self.name} was stopped")
187188
ttime.sleep(1 / self.update_frequency)
188189
self._update_state(self.readback.get())
189-
for status in self._status_list:
190-
status.set_finished()
191190
# pylint: disable=broad-except
192191
except Exception as exc:
193192
content = traceback.format_exc()
194193
logger.warning(
195-
f"Error in on_complete call in device {self.name}. Error traceback: {content}"
194+
f"Error in _move_to_setpoint call in device {self.name}. Error traceback: {content}"
196195
)
197-
for status in self._status_list:
198-
status.set_exception(exc=exc)
196+
with self._lock:
197+
for status in self._status_list:
198+
status.set_exception(exc=exc)
199+
self._status_list = []
199200
finally:
200-
self.motor_is_moving.put(0)
201-
if not self._stopped:
202-
self._update_state(self.readback.get())
203-
self._status_list = []
201+
with self._lock:
202+
self.motor_is_moving.put(0)
203+
if not self._stopped:
204+
self._update_state(self.readback.get())
205+
for status in self._status_list:
206+
if not status.done:
207+
status.set_finished()
208+
self._status_list = []
204209

205210
def move(self, value: float, **kwargs) -> DeviceStatus:
206211
"""Change the setpoint of the simulated device, and simultaneously initiate a motion."""
@@ -210,7 +215,8 @@ def move(self, value: float, **kwargs) -> DeviceStatus:
210215
self.setpoint.put(value)
211216

212217
st = DeviceStatus(device=self)
213-
self._status_list.append(st)
218+
with self._lock:
219+
self._status_list.append(st)
214220
if self.delay:
215221
if self.move_thread is None or not self.move_thread.is_alive():
216222
self.move_thread = threading.Thread(target=self._move_to_setpoint)

ophyd_devices/sim/sim_signals.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ def get(self, **kwargs) -> Any:
8080
"""
8181
old_value = self._readback
8282
self._readback = self._value = self._get_value()
83-
self._run_subs(sub_type=self.SUB_VALUE, old_value=old_value, value=self._readback)
83+
if old_value != self._readback: # only run subs if the value has changed
84+
self._run_subs(sub_type=self.SUB_VALUE, old_value=old_value, value=self._readback)
8485
return self._readback
8586

8687
# pylint: disable=arguments-differ
@@ -89,9 +90,11 @@ def put(self, value) -> None:
8990
9091
Core function for signal.
9192
"""
93+
old_value = self._value
9294
self.check_value(value)
9395
self._update_sim_state(value)
9496
self._value = value
97+
self._run_subs(sub_type=self.SUB_VALUE, old_value=old_value, value=self._value)
9598
super().put(value)
9699

97100
def set(self, value):

ophyd_devices/sim/sim_waveform.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ def __init__(
129129
self._trigger_received = 0
130130
self.scan_info = scan_info
131131
self._delay_slice_update = False
132+
self._slice_index = 0
132133
if self.sim_init:
133134
self.sim.set_init(self.sim_init)
134-
self._slice_index = 0
135135

136136
@property
137137
def delay_slice_update(self) -> bool:

ophyd_devices/utils/static_device_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ def run(
286286
if device_manager is not None: # Only possible if bec-server is installed
287287
obj = self.construct_device_obj(name, conf)
288288
if obj is None: # construction failed, skip connection test
289-
return_value += 1
289+
return_val += 1
290290
elif obj is not None and connect:
291291
return_val += self.connect_device(
292292
name,

tests/test_simulation.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def waveform(name="waveform"):
4848
"""Fixture for SimWaveform."""
4949
dm = DMMock()
5050
wave = SimWaveform(name=name, device_manager=dm)
51+
wave.wait_for_connection()
5152
yield wave
5253

5354

@@ -59,10 +60,21 @@ def signal(name="signal"):
5960

6061

6162
@pytest.fixture(scope="function")
62-
def monitor(name="monitor"):
63-
"""Fixture for SimMonitor."""
63+
def samx(name="samx"):
64+
"""Fixture for SimPositioner."""
6465
dm = DMMock()
66+
pos = SimPositioner(name=name, device_manager=dm)
67+
yield pos
68+
69+
70+
@pytest.fixture(scope="function")
71+
def monitor(samx, name="monitor"):
72+
"""Fixture for SimMonitor."""
73+
dm = samx.device_manager
74+
samx.obj = samx # Set obj attribute to itself for proxy lookup
75+
dm.devices["samx"] = samx
6576
mon = SimMonitor(name=name, device_manager=dm)
77+
mon.wait_for_connection()
6678
yield mon
6779

6880

@@ -97,6 +109,7 @@ def async_monitor(name="async_monitor"):
97109
"""Fixture for SimMonitorAsync."""
98110
dm = DMMock()
99111
mon = SimMonitorAsync(name=name, device_manager=dm)
112+
mon.wait_for_connection()
100113
yield mon
101114

102115

@@ -168,6 +181,7 @@ def test_monitor_with_sim_init():
168181
"""Test to see if the sim init parameters are passed to the device"""
169182
dm = DMMock()
170183
sim = SimMonitor(name="sim", device_manager=dm)
184+
sim.wait_for_connection()
171185
assert sim.sim._model._name == "constant"
172186
model = "GaussianModel"
173187
params = {
@@ -179,6 +193,7 @@ def test_monitor_with_sim_init():
179193
"ref_motor": "samy",
180194
}
181195
sim = SimMonitor(name="sim", device_manager=dm, sim_init={"model": model, "params": params})
196+
sim.wait_for_connection()
182197
assert sim.sim._model._name == model.strip("Model").lower()
183198
diff_keys = set(sim.sim.params.keys()) - set(params.keys())
184199
for k in params:
@@ -224,9 +239,7 @@ def test_init_async_monitor(async_monitor):
224239
def test_monitor_readback(monitor, center, positioner):
225240
"""Test the readback method of SimMonitor."""
226241
motor_pos = 0
227-
samx = SimPositioner(name="samx", device_manager=monitor.device_manager)
228-
setattr(samx, "obj", samx) # Set obj attribute to itself for proxy lookup
229-
monitor.device_manager.devices["samx"] = samx
242+
samx = monitor.device_manager.devices.get("samx", None)
230243
for model_name in monitor.sim.get_models():
231244
monitor.sim.select_model(model_name)
232245
monitor.sim.params["noise_multipler"] = 10

0 commit comments

Comments
 (0)