-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsimulator.py
More file actions
334 lines (273 loc) · 12.9 KB
/
Copy pathsimulator.py
File metadata and controls
334 lines (273 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
"""simulator.py - many flows share one pump and one logical clock.
Python port of cpp/examples/simulator/. A console sim where five runner flows
plus a renderer flow all live on ONE Runtime (one pump thread) and one
VirtualClock. Type commands to drive time itself:
pause freeze logical time (runners stop, dashboard stays live)
resume resume logical time
speed <n> scale logical time (n > 0; 0.5 = half, 4 = 4x)
quit stop and exit
FEATURE FOCUS: VirtualClock (scale / freeze) + single-thread cooperation.
The pump, the five runners, and the renderer all live on one thread; the main
thread does nothing but read stdin and poke the clock (thread-safe).
NOTE ON UNITS: the C++ port measures phases in milliseconds; the Python
VirtualClock.Now() returns SECONDS, so the same durations are expressed in
seconds here (move 3.8 s vs 3800 ms, gate 0.7 s, rest 0.5 s).
"""
import os
import sys
import threading
# --- import shim: make the python/ package dir importable, then sibling console
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import uniflow # noqa: E402
import console # noqa: E402 (same dir as this file)
# ============================================================================
# snapshot - the data the renderer draws each frame (mirrors snapshot.h/.cpp)
# ============================================================================
#
# REFERENCE NOTE: there is NO lock here, on purpose. Every Flow_Runner and the
# Flow_View renderer are modules on the SAME Runtime, so they all advance on the
# one pump thread, round-robin. A runner writing its row and the view reading it
# never overlap - that is the core uniflow guarantee: single thread, no locks.
# The only other thread (stdin in main) touches just the clock and g_stop.
K_RUNNER_COUNT = 5
# Fixed dashboard layout (1-based rows). The renderer only ever draws rows
# 1..(K_PROMPT_ROW-1); the stdin prompt lives on K_PROMPT_ROW and is never
# touched by the renderer, so typed input is not clobbered by frame redraws.
K_PROMPT_ROW = 6 + K_RUNNER_COUNT
class RunnerRow:
"""One dashboard line. The runner owns its slot (indexed by ctor id)."""
def __init__(self):
self.name = ""
self.step = "-" # current step name - the "what is it doing" column
self.percent = 0.0
self.lap = 0
# Plain module-level state, pump-thread-only (see note above).
g_rows = [RunnerRow() for _ in range(K_RUNNER_COUNT)]
# Cross-thread shutdown latch: set by the stdin loop, read by every flow's
# steps so they can return Done() and let WaitUntilIdle() return.
g_stop = threading.Event()
# ============================================================================
# Flow_Runner - one simulated worker that laps a track forever (uf_runner.*)
# ============================================================================
#
# FEATURE FOCUS: logical (virtual) time. Progress is measured against
# rt.clock (the VirtualClock) - so every runner speeds up, slows down, and
# freezes together when the user scales or pauses the sim, with no per-flow
# plumbing. The three steps (Gate -> Move -> Rest) loop; each loop is one lap.
class Flow_Runner(uniflow.Uniflow):
K_GATE_SEC = 0.7 # virtual-time hold at the gate
K_REST_SEC = 0.5 # virtual-time rest after the line
def __init__(self, rt, name, idx, move_sec):
# idx selects this runner's dashboard row; move_sec is the virtual-time
# length of one Move phase (different per runner for a staggered field).
super().__init__(rt, name=name)
self.clock = rt.clock # bind to the Runtime's logical clock
self.id = idx
self.move_sec = move_sec
self.lap = 0
self.ctx_run = self.Task_Run()
self.AddTask(self.ctx_run)
g_rows[self.id].name = name
class Task_Run(uniflow.Task):
def OnEnter(self):
# Re-anchor virtual time whenever the task is entered.
self.phase_start = self.flow().clock.Now()
def Entry(self):
return self.Step1_Gate()
# Virtual seconds elapsed in the current phase. Because Now() comes from
# the VirtualClock, this stalls when the sim is paused and stretches /
# compresses when it is scaled - the whole point of this example.
def v_elapsed_sec(self):
return self.flow().clock.Now() - self.phase_start
def publish(self, percent, step):
# Plain writes to our own row - same pump thread as the renderer,
# no lock.
row = g_rows[self.flow().id]
row.percent = percent
row.step = step
row.lap = self.flow().lap
def Step1_Gate(self):
if g_stop.is_set(): # cooperative shutdown so WaitUntilIdle returns
return self.Done()
self.publish(0.0, "Step1_Gate")
if self.v_elapsed_sec() < Flow_Runner.K_GATE_SEC:
return self.Stay() # re-poll this step next round
self.phase_start = self.flow().clock.Now() # reset for Move phase
self.Describe("leaving the gate")
return self.Next(self.Step2_Move)
def Step2_Move(self):
if g_stop.is_set():
return self.Done()
frac = self.v_elapsed_sec() / self.flow().move_sec
if frac >= 1.0:
self.publish(100.0, "Step2_Move")
self.phase_start = self.flow().clock.Now()
self.Describe("reached the line")
return self.Next(self.Step3_Rest)
self.publish(frac * 100.0, "Step2_Move")
return self.Stay()
def Step3_Rest(self):
if g_stop.is_set():
return self.Done()
self.publish(100.0, "Step3_Rest")
if self.v_elapsed_sec() < Flow_Runner.K_REST_SEC:
return self.Stay()
self.flow().lap += 1 # one full lap completed
self.phase_start = self.flow().clock.Now()
return self.Next(self.Step1_Gate) # loop forever (until g_stop)
# ============================================================================
# Flow_View - the dashboard renderer, itself a uniflow module (uf_view.*)
# ============================================================================
#
# FEATURE FOCUS: a renderer is just another flow on the same pump. It reads
# every runner's row with plain access (no lock) because they share the thread.
# Its frame cadence uses a REAL-time UFTimer (clock=None) - NOT the virtual
# clock - so when the user pauses the sim (clock frozen), the runners stop but
# the dashboard keeps redrawing and shows [PAUSED].
_SEP = " " + "-" * 60
class Flow_View(uniflow.Uniflow):
def __init__(self, rt):
super().__init__(rt, name="Flow_View")
self.clock = rt.clock # read scale/frozen state for the header
self.ctx_draw = self.Task_Draw()
self.AddTask(self.ctx_draw)
class Task_Draw(uniflow.Task):
def OnEnter(self):
# Real-clock throttle (keeps drawing while the sim clock is paused).
self.fps = uniflow.UFTimer()
self.fps.Restart()
def Entry(self):
return self.Step1_Draw()
def Step1_Draw(self):
if g_stop.is_set():
return self.Done()
# Throttle to ~30 fps on REAL time. fps is a default UFTimer (wall
# clock), so the dashboard keeps refreshing even while the sim clock
# is frozen.
if self.fps.Passed(0.033):
self.fps.Restart()
self.render()
return self.Stay()
def render(self):
out = []
# Save the user's cursor (sitting on the prompt line), redraw the
# dashboard above it at fixed positions, then restore it. The prompt
# row is never touched.
out.append(console.SAVE_CURSOR)
def put(row, text):
out.append(console.at(row, 1) + console.CLEAR_LINE + text)
put(1, " " + console.BOLD + "uniflow simulator " + console.RESET
+ console.DIM + "v" + uniflow.__version__ + console.RESET)
put(2, _SEP)
# Header reads live scale/freeze straight off the VirtualClock.
clk = self.flow().clock
if clk.Frozen():
status = " " + console.YELLOW + "[PAUSED] " + console.RESET
else:
status = " " + console.GREEN + "[RUNNING]" + console.RESET
status += (" speed " + console.CYAN + "x"
+ f"{clk.Scale():.2f}" + console.RESET
+ " " + console.GRAY
+ "pause | resume | speed <n> | quit" + console.RESET)
put(3, status)
put(4, _SEP)
for i in range(K_RUNNER_COUNT):
r = g_rows[i]
line = (" " + f"{r.name:<8}"
+ " lap " + f"{r.lap:>2}"
+ " [" + console.GREEN + console.bar(r.percent / 100.0, 20)
+ console.RESET + "] " + f"{int(r.percent + 0.5):>3}" + "% "
+ console.DIM + r.step + console.RESET)
put(5 + i, line)
put(5 + K_RUNNER_COUNT, _SEP)
out.append(console.RESTORE_CURSOR)
sys.stdout.write("".join(out))
sys.stdout.flush()
# ============================================================================
# App - the Runtime plus every module (app.h). Two-phase init.
# ============================================================================
class App:
def __init__(self):
# Silent runtime: this app OWNS the console (the dashboard), so the
# default ConsoleObserver's trace output must be suppressed. An empty
# Observer prints nothing.
#
# All-Stay rounds (everyone polling) should be short so motion and the
# ~30 fps renderer stay smooth (sleeps are in SECONDS).
cfg = uniflow.Config(idle_sleep_sec=0.001,
stay_sleep_sec=0.005,
step_interval_sleep_sec=0.0)
self.rt = uniflow.Runtime(observer=uniflow.Observer(), config=cfg)
# Phase 1: construct. renderer then runners.
self.view = Flow_View(self.rt)
self.runners = [
Flow_Runner(self.rt, "Atlas", 0, 3.8),
Flow_Runner(self.rt, "Bolt", 1, 2.6),
Flow_Runner(self.rt, "Comet", 2, 4.6),
Flow_Runner(self.rt, "Dash", 3, 3.2),
Flow_Runner(self.rt, "Echo", 4, 5.2),
]
def Start(self):
# Phase 2: launch every task. Each StartFlow() puts one task on the pump.
self.view.ctx_draw.StartFlow()
for r in self.runners:
r.ctx_run.StartFlow()
def Shutdown(self):
g_stop.set() # every step checks this and returns Done()
self.rt.Wake() # nudge the pump out of any sleep
for r in self.runners:
r.WaitUntilIdle()
self.view.WaitUntilIdle()
self.rt.stop()
# ============================================================================
# main - stdin command loop (mirrors main.cpp)
# ============================================================================
def draw_prompt():
# Draw the input prompt on its reserved row. The renderer never touches this
# row, so what the user types is not overwritten by frame redraws.
sys.stdout.write(console.at(K_PROMPT_ROW, 1) + console.CLEAR_LINE + " "
+ console.BOLD + "Type a command and press Enter"
+ console.RESET + console.DIM
+ " (pause | resume | speed <n> | quit): " + console.RESET)
sys.stdout.flush()
def run_input_loop(rt):
while True:
draw_prompt()
line = sys.stdin.readline()
if line == "":
break # EOF (e.g. piped input ended) - treat as quit
parts = line.split()
if not parts:
continue
cmd = parts[0]
if cmd in ("quit", "exit", "q"):
break
if cmd == "pause":
rt.clock.Freeze() # stop logical time for ALL runners at once
elif cmd == "resume":
rt.clock.Resume()
elif cmd == "speed":
if len(parts) >= 2:
try:
n = float(parts[1])
except ValueError:
n = 0.0
if n > 0.0:
rt.clock.SetScale(n) # one call rescales every flow's pace
# Unknown commands are ignored; the dashboard keeps running.
def main():
console.enable_ansi()
console.hide_cursor()
console.clear()
app = App()
app.Start()
try:
run_input_loop(app.rt)
finally:
app.Shutdown()
console.show_cursor()
sys.stdout.write(console.at(K_PROMPT_ROW + 1, 1) + console.CLEAR_LINE
+ " simulator stopped.\n")
sys.stdout.flush()
if __name__ == "__main__":
main()