Skip to content

Commit be3827c

Browse files
committed
Use timeout instead of dedicated PV for dispatching simulation
1 parent 7854de5 commit be3827c

File tree

2 files changed

+121
-7
lines changed

2 files changed

+121
-7
lines changed

simulation_server/beamdriver.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Dict, Callable, Any
99
from simulation_server.virtual_accelerator import VirtualAccelerator
1010
import threading
11+
from .utils.timer import Timer
1112

1213
class SimServer(SimpleServer):
1314
"""
@@ -83,6 +84,10 @@ def __init__(self, pvdb: dict, prefix: str = "", threading: bool = True):
8384
self._db[self.sim_pv_name] = {
8485
"value": 0
8586
}
87+
self.sim_timeout_name = "VIRT:BEAM:SIMULATE_TIMEOUT"
88+
self._db[self.sim_timeout_name] = {
89+
"value": 0.5
90+
}
8691

8792
# Create CA PVs
8893
self.createPV(prefix, self._db)
@@ -287,6 +292,8 @@ def __init__(
287292
self.thread = threading.Thread(target=self._model_update_thread)
288293
self.new_data = {}
289294

295+
self.timer = Timer(0.5, self._trigger_sim, periodic=True, manual=True)
296+
290297
# get list of pvs that should be updated every time we write to a PV
291298
self.measurement_pvs = self.get_measurement_pvs()
292299

@@ -299,6 +306,12 @@ def __init__(
299306
self.update_cache(self.measurement_pvs, True)
300307

301308
self.thread.start()
309+
if self.server.threaded:
310+
self.timer.start()
311+
312+
def _trigger_sim(self):
313+
with self.write_guard:
314+
self.thread_cond.notify_all()
302315

303316
def _set_and_simulate(self, new_data: dict):
304317
"""Updates PVs on the model, then updates the PV cache with results"""
@@ -316,9 +329,11 @@ def _model_update_thread(self):
316329
# Unlocked by thread_cond.wait()
317330
self.write_guard.acquire()
318331

319-
# Wait for a trigger
332+
# Wait for a trigger (unlocks write_guard)
320333
self.thread_cond.wait()
321334

335+
print('Simulation triggered')
336+
322337
start = time.time()
323338

324339
# run simulation
@@ -435,24 +450,32 @@ def write(self, reason, value):
435450
"""write to a PV, run the simulation, and then update all other PVs"""
436451
print(f"Writing {value} to {reason}")
437452

453+
# Update internal values quickly so readbacks dont fail
454+
self.set_cached_value(reason, value, True)
455+
456+
# Halt countdown
457+
self.timer.cancel()
458+
438459
# Re-run the entire simulation if requested
439460
if reason == self.server.sim_pv_name:
440461
with self.write_guard:
441-
self.set_cached_value(reason, value, True)
442462
self.thread_cond.notify_all()
443463
return
444464

465+
# Adjust simulation timeout
466+
if reason == self.server.sim_timeout_name:
467+
self.timer.interval = int(value)
468+
return
469+
445470
# Single threaded mode; do all updates immediately
446471
if not self.server.threaded:
447-
# Set new value for this PV and update monitors to make caput readback behave nicely
448-
self.set_cached_value(reason, value, True)
449472
# Set changed PVs, and update the new values
450473
self._set_and_simulate({reason: value})
451474
return
452475

453476
with self.write_guard:
454-
# update the control PV fast
455-
self.set_cached_value(reason, value, True)
456-
457477
# this is sent to the updater thread
458478
self.new_data[reason] = value
479+
480+
# Begin simulation timeout period
481+
self.timer.reset()

simulation_server/utils/timer.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
2+
from threading import Thread, Event
3+
4+
class Timer(Thread):
5+
"""
6+
Simple timer class that's better than the one provided by threading.Timer()
7+
"""
8+
def __init__(self, interval: float, method, arg = None, periodic = False, manual = False):
9+
"""
10+
Parameters
11+
----------
12+
interval: float
13+
Timer interval in seconds
14+
method: Any
15+
Callback on timer expiration
16+
arg: Any
17+
Argument for method()
18+
periodic: bool
19+
If set, the timer will continue executing after expiration. May be paired with manual
20+
manual: bool
21+
If set, the timer must be manually reset after expiration
22+
"""
23+
Thread.__init__(self)
24+
self._interval = interval
25+
self._cancel_event = Event()
26+
self._relaunch_event = Event()
27+
self._periodic = periodic
28+
self._arg = arg
29+
self._method = method
30+
self._manual = manual
31+
32+
@property
33+
def interval(self) -> float:
34+
return self._interval
35+
36+
@interval.setter
37+
def interval(self, i: float):
38+
"""
39+
Sets the interval to a specific value.
40+
"""
41+
self._interval = i
42+
43+
@property
44+
def periodic(self) -> bool:
45+
return self._periodic
46+
47+
@property
48+
def manual(self) -> bool:
49+
return self._manual
50+
51+
def reset(self):
52+
"""
53+
Reset the timer
54+
Manual timers need to call this explicitly to get a relaunch and initial launch.
55+
For periodic timers, this method is equivalent to cancel()
56+
"""
57+
self._cancel_event.set()
58+
self._relaunch_event.set()
59+
60+
def cancel(self):
61+
"""
62+
Cancels the timer.
63+
Periodic timers will relaunch immediately.
64+
For manual timers, this will halt the timer until a subsequent call to reset() is performed.
65+
"""
66+
self._cancel_event.set()
67+
68+
def run(self):
69+
"""Do not call me directly! Use start() instead!"""
70+
while True:
71+
# manual threads must be relaunched explicitly
72+
if self._manual:
73+
self._relaunch_event.wait()
74+
self._relaunch_event.clear()
75+
76+
# Wait for a timeout, or cancellation
77+
self._cancel_event.clear()
78+
if self._cancel_event.wait(self._interval):
79+
self._cancel_event.clear() # Timer cancelled, restart
80+
if not self._periodic:
81+
return
82+
continue
83+
self._cancel_event.clear()
84+
85+
if self._arg:
86+
self._method(self._arg)
87+
else:
88+
self._method()
89+
90+
if not self._periodic:
91+
return

0 commit comments

Comments
 (0)