nost-tools 2.0.0__py3-none-any.whl → 2.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nost-tools might be problematic. Click here for more details.
- nost_tools/__init__.py +29 -29
- nost_tools/application.py +901 -793
- nost_tools/application_utils.py +262 -262
- nost_tools/configuration.py +304 -304
- nost_tools/entity.py +73 -73
- nost_tools/errors.py +14 -14
- nost_tools/logger_application.py +192 -192
- nost_tools/managed_application.py +261 -261
- nost_tools/manager.py +472 -472
- nost_tools/observer.py +181 -181
- nost_tools/publisher.py +141 -141
- nost_tools/schemas.py +458 -426
- nost_tools/simulator.py +531 -531
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.2.dist-info}/METADATA +118 -119
- nost_tools-2.0.2.dist-info/RECORD +18 -0
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.2.dist-info}/licenses/LICENSE +29 -29
- nost_tools-2.0.0.dist-info/RECORD +0 -18
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.2.dist-info}/WHEEL +0 -0
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.2.dist-info}/top_level.txt +0 -0
nost_tools/simulator.py
CHANGED
|
@@ -1,531 +1,531 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Provides classes to execute a simulation.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
import time
|
|
7
|
-
from datetime import datetime, timedelta, timezone
|
|
8
|
-
from enum import Enum
|
|
9
|
-
from typing import List, Type
|
|
10
|
-
|
|
11
|
-
from .entity import Entity
|
|
12
|
-
from .observer import Observable
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class Mode(str, Enum):
|
|
18
|
-
"""
|
|
19
|
-
Enumeration of simulation modes.
|
|
20
|
-
|
|
21
|
-
The six simulation modes include
|
|
22
|
-
* `UNDEFINED`: Simulation is in an undefined state that is not one of the other modes.
|
|
23
|
-
For example, the simulation reverts to UNDEFINED after adding a new entity
|
|
24
|
-
and must be re-initialized before execution.
|
|
25
|
-
* `INITIALIZING`: Simulation is in the process of initialization.
|
|
26
|
-
* `INITIALIZED`: Simulation has finished initialization and is ready to execute.
|
|
27
|
-
* `EXECUTING`: Simulation is in the process of execution.
|
|
28
|
-
* `TERMINATING`: Simulation is in the process of termination.
|
|
29
|
-
* `TERMINATED`: Simulation has finished termination and is ready for initialization.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
UNDEFINED = "UNDEFINED"
|
|
33
|
-
INITIALIZING = "INITIALIZING"
|
|
34
|
-
INITIALIZED = "INITIALIZED"
|
|
35
|
-
EXECUTING = "EXECUTING"
|
|
36
|
-
TERMINATING = "TERMINATING"
|
|
37
|
-
TERMINATED = "TERMINATED"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class Simulator(Observable):
|
|
41
|
-
"""
|
|
42
|
-
Object that manages simulation of entities in a scenario.
|
|
43
|
-
|
|
44
|
-
Notifies observers of changes to observable properties
|
|
45
|
-
* `time`: current scenario time
|
|
46
|
-
* `mode`: current execution mode
|
|
47
|
-
* `duration`: scenario execution duration
|
|
48
|
-
* `time_step`: scenario time step duration
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
PROPERTY_MODE = "mode"
|
|
52
|
-
PROPERTY_TIME = "time"
|
|
53
|
-
|
|
54
|
-
def __init__(self, wallclock_offset: timedelta = timedelta()):
|
|
55
|
-
"""
|
|
56
|
-
Initializes a new simulator.
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
wallclock_offset (:obj:`timedelta`): difference between the system
|
|
60
|
-
clock and trusted wallclock source (default: zero)
|
|
61
|
-
"""
|
|
62
|
-
# call super class constructor
|
|
63
|
-
super().__init__()
|
|
64
|
-
# offset from the system clock to "true" time
|
|
65
|
-
self._wallclock_offset = wallclock_offset
|
|
66
|
-
# list of entities that participate in this simulation
|
|
67
|
-
self._entities = []
|
|
68
|
-
# current mode of the simulator
|
|
69
|
-
self._mode = Mode.UNDEFINED
|
|
70
|
-
# current simulation time; next simulation time, initial simulation time
|
|
71
|
-
self._time = self._next_time = self._init_time = 0
|
|
72
|
-
# current simulation time step; next simulation time step
|
|
73
|
-
self._time_step = self._next_time_step = None
|
|
74
|
-
# current simulation duration; next simulation duration
|
|
75
|
-
self._duration = self._next_duration = None
|
|
76
|
-
# wallclock time when the simulation starts or changes time scaling
|
|
77
|
-
self._wallclock_epoch = None
|
|
78
|
-
# simulation time when the simulation starts or changes time scaling
|
|
79
|
-
self._simulation_epoch = None
|
|
80
|
-
# simulation time at which to perform a time scale change
|
|
81
|
-
self._time_scale_change_time = None
|
|
82
|
-
# relationship between the wallclock time and simulation time
|
|
83
|
-
self._time_scale_factor = self._next_time_scale_factor = 1
|
|
84
|
-
|
|
85
|
-
def add_entity(self, entity: Entity) -> None:
|
|
86
|
-
"""
|
|
87
|
-
Adds an entity the the simulation.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
entity (:obj:`Entity`): entity to be added
|
|
91
|
-
"""
|
|
92
|
-
if self._mode == Mode.INITIALIZING:
|
|
93
|
-
raise RuntimeError("Cannot add entity: simulator is initializing")
|
|
94
|
-
elif self._mode == Mode.EXECUTING:
|
|
95
|
-
raise RuntimeError("Cannot add entity: simulator is executing")
|
|
96
|
-
elif self._mode == Mode.TERMINATING:
|
|
97
|
-
raise RuntimeError("Cannot add entity: simulator is terminating")
|
|
98
|
-
self._set_mode(Mode.UNDEFINED)
|
|
99
|
-
self._entities.append(entity)
|
|
100
|
-
|
|
101
|
-
def get_entities(self) -> List[Entity]:
|
|
102
|
-
"""
|
|
103
|
-
Retrieves a list of all entities in the simulation.
|
|
104
|
-
|
|
105
|
-
Returns:
|
|
106
|
-
List(Entity): list of entities in the simulation
|
|
107
|
-
"""
|
|
108
|
-
# perform shallow copy to prevent external modification
|
|
109
|
-
return self._entities.copy()
|
|
110
|
-
|
|
111
|
-
def get_entities_by_name(self, name: str) -> List[Entity]:
|
|
112
|
-
"""
|
|
113
|
-
Retrieves a list of entities by name.
|
|
114
|
-
|
|
115
|
-
Args:
|
|
116
|
-
name (str): name of the entity
|
|
117
|
-
|
|
118
|
-
Returns:
|
|
119
|
-
List(Entity): list of entities with a matching name
|
|
120
|
-
"""
|
|
121
|
-
return [entity for entity in self._entities if entity.name == name]
|
|
122
|
-
|
|
123
|
-
def get_entities_by_type(self, type: Type) -> List[Entity]:
|
|
124
|
-
"""
|
|
125
|
-
Retrieves a list of entities by type (class).
|
|
126
|
-
|
|
127
|
-
Args:
|
|
128
|
-
type (Type): type (class) of entity
|
|
129
|
-
|
|
130
|
-
Returns:
|
|
131
|
-
List(Entity): list of entities with a matching type
|
|
132
|
-
"""
|
|
133
|
-
return [entity for entity in self._entities if isinstance(entity, type)]
|
|
134
|
-
|
|
135
|
-
def remove_entity(self, entity: Entity) -> Entity:
|
|
136
|
-
"""
|
|
137
|
-
Removes an entity from the simulation.
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
entity (:obj:`Entity`): entity to be removed
|
|
141
|
-
Returns:
|
|
142
|
-
:obj:`Entity`: removed entity
|
|
143
|
-
"""
|
|
144
|
-
if self._mode == Mode.INITIALIZING:
|
|
145
|
-
raise RuntimeError("Cannot add entity: simulator is initializing")
|
|
146
|
-
elif self._mode == Mode.EXECUTING:
|
|
147
|
-
raise RuntimeError("Cannot add entity: simulator is executing")
|
|
148
|
-
elif self._mode == Mode.TERMINATING:
|
|
149
|
-
raise RuntimeError("Cannot add entity: simulator is terminating")
|
|
150
|
-
if entity in self._entities:
|
|
151
|
-
self._set_mode(Mode.UNDEFINED)
|
|
152
|
-
return self._entities.remove(entity)
|
|
153
|
-
else:
|
|
154
|
-
return None
|
|
155
|
-
|
|
156
|
-
def initialize(
|
|
157
|
-
self,
|
|
158
|
-
init_time: datetime,
|
|
159
|
-
wallclock_epoch: datetime = None,
|
|
160
|
-
time_scale_factor: float = 1,
|
|
161
|
-
) -> None:
|
|
162
|
-
"""
|
|
163
|
-
Initializes the simulation to an initial scenario time. Requires that the
|
|
164
|
-
simulator is in UNDEFINED, INITIALIZED, or TERMINATED mode.
|
|
165
|
-
|
|
166
|
-
Transitions to the INITIALIZING mode, initializes all entities to the
|
|
167
|
-
initial scenario time, sets the wallclock epoch (wallclock time corresponding
|
|
168
|
-
with the initial scenario time), and finally transitions to the INITIALIZED mode.
|
|
169
|
-
|
|
170
|
-
Args:
|
|
171
|
-
init_time (:obj:`datetime`): initial scenario time
|
|
172
|
-
wallclock_epoch (:obj:`datetime`): wallclock time corresponding to the
|
|
173
|
-
initial scenario time, None uses the current wallclock time (default: None)
|
|
174
|
-
time_scale_factor (float): number of scenario seconds per wallclock second (default: 1)
|
|
175
|
-
"""
|
|
176
|
-
if self._mode == Mode.INITIALIZING:
|
|
177
|
-
raise RuntimeError("Cannot initialize: simulator is initializing.")
|
|
178
|
-
elif self._mode == Mode.EXECUTING:
|
|
179
|
-
raise RuntimeError("Cannot initialize: simulator is executing.")
|
|
180
|
-
elif self._mode == Mode.TERMINATING:
|
|
181
|
-
raise RuntimeError("Cannot initialize: simulator is terminating.")
|
|
182
|
-
self._set_mode(Mode.INITIALIZING)
|
|
183
|
-
logger.info(
|
|
184
|
-
f"Initializing simulator to time {init_time} (wallclock time {wallclock_epoch})"
|
|
185
|
-
)
|
|
186
|
-
for entity in self._entities:
|
|
187
|
-
entity.initialize(init_time)
|
|
188
|
-
self._time = self._next_time = self._init_time = init_time
|
|
189
|
-
self._simulation_epoch = init_time
|
|
190
|
-
if wallclock_epoch is None:
|
|
191
|
-
self._wallclock_epoch = self.get_wallclock_time()
|
|
192
|
-
else:
|
|
193
|
-
self._wallclock_epoch = wallclock_epoch
|
|
194
|
-
self._time_scale_factor = self._next_time_scale_factor = time_scale_factor
|
|
195
|
-
self._set_mode(Mode.INITIALIZED)
|
|
196
|
-
|
|
197
|
-
def execute(
|
|
198
|
-
self,
|
|
199
|
-
init_time: datetime,
|
|
200
|
-
duration: timedelta,
|
|
201
|
-
time_step: timedelta,
|
|
202
|
-
wallclock_epoch: datetime = None,
|
|
203
|
-
time_scale_factor: float = 1,
|
|
204
|
-
) -> None:
|
|
205
|
-
"""
|
|
206
|
-
Executes a simulation for a specified duration with uniform time steps. Requires that the
|
|
207
|
-
simulator is in UNDEFINED, INITIALIZED, or TERMINATED mode.
|
|
208
|
-
|
|
209
|
-
Initializes the simulation (if not already in the INITIALIZED mode), waits for the
|
|
210
|
-
specified wallclock epoch, and transitions to the EXECUTING mode. During execution,
|
|
211
|
-
incrementally performs state transitions for each entity. At the end of the simulation,
|
|
212
|
-
transitions to the TERMINATING and, finally, TERMINATED mode.
|
|
213
|
-
|
|
214
|
-
Args:
|
|
215
|
-
init_time (:obj:`datetime`): initial scenario time
|
|
216
|
-
duration (:obj:`timedelta`): scenario execution duration
|
|
217
|
-
time_step (:obj:`timedelta`): scenario time step duration
|
|
218
|
-
wallclock_epoch (:obj:`datetime`): wallclock time corresponding to the
|
|
219
|
-
initial scenario time, None uses the current wallclock time (default: None)
|
|
220
|
-
time_scale_factor (float): number of scenario seconds per wallclock second (default value: 1)
|
|
221
|
-
"""
|
|
222
|
-
if self._mode != Mode.INITIALIZED:
|
|
223
|
-
self.initialize(init_time, wallclock_epoch, time_scale_factor)
|
|
224
|
-
|
|
225
|
-
self._duration = self._next_duration = duration
|
|
226
|
-
self._time_step = self._next_time_step = time_step
|
|
227
|
-
|
|
228
|
-
logger.info(
|
|
229
|
-
f"Executing simulator for {duration} ({time_step} steps), starting at {self._wallclock_epoch}."
|
|
230
|
-
)
|
|
231
|
-
self._wait_for_wallclock_epoch()
|
|
232
|
-
self._set_mode(Mode.EXECUTING)
|
|
233
|
-
|
|
234
|
-
logger.info("Starting main simulation loop.")
|
|
235
|
-
while (
|
|
236
|
-
self._mode == Mode.EXECUTING
|
|
237
|
-
and self.get_time() < self.get_init_time() + self.get_duration()
|
|
238
|
-
):
|
|
239
|
-
# compute time step (last step may be shorter)
|
|
240
|
-
time_step = min(
|
|
241
|
-
self._time_step, self._init_time + self._duration - self._time
|
|
242
|
-
)
|
|
243
|
-
# tick each entity
|
|
244
|
-
for entity in self._entities:
|
|
245
|
-
entity.tick(
|
|
246
|
-
min(time_step, self._init_time + self._duration - self._time)
|
|
247
|
-
)
|
|
248
|
-
# store the next time
|
|
249
|
-
self._next_time = self._time + time_step
|
|
250
|
-
if (
|
|
251
|
-
self._time_scale_change_time is not None
|
|
252
|
-
and self._time_scale_change_time < self._next_time
|
|
253
|
-
):
|
|
254
|
-
# update the wallclock epoch of this change
|
|
255
|
-
self._wallclock_epoch = self.get_wallclock_time_at_simulation_time(
|
|
256
|
-
self._time
|
|
257
|
-
)
|
|
258
|
-
# update the simulation epoch of this change
|
|
259
|
-
self._simulation_epoch = self._time
|
|
260
|
-
# reset the flag to change the time scale factor
|
|
261
|
-
self._time_scale_change_time = None
|
|
262
|
-
# commit the change to the time scale factor and notify observers
|
|
263
|
-
prev_time_scale_factor = self._time_scale_factor
|
|
264
|
-
self._time_scale_factor = self._next_time_scale_factor
|
|
265
|
-
self.notify_observers(
|
|
266
|
-
"time_scale_factor", prev_time_scale_factor, self._time_scale_factor
|
|
267
|
-
)
|
|
268
|
-
# wait for the correct time
|
|
269
|
-
self._wait_for_tock()
|
|
270
|
-
# break out of loop if terminating execution
|
|
271
|
-
if self._mode == Mode.TERMINATING:
|
|
272
|
-
logger.debug("Terminating: exiting execution loop.")
|
|
273
|
-
break
|
|
274
|
-
# tock each entity
|
|
275
|
-
for entity in self._entities:
|
|
276
|
-
entity.tock()
|
|
277
|
-
# update the execution duration, if needed
|
|
278
|
-
if self._duration != self._next_duration:
|
|
279
|
-
prev_duration = self._duration
|
|
280
|
-
self._duration = self._next_duration
|
|
281
|
-
logger.info(f"Updated duration to {self._duration}.")
|
|
282
|
-
self.notify_observers("duration", prev_duration, self._duration)
|
|
283
|
-
# update the execution time step, if needed
|
|
284
|
-
if self._time_step != self._next_time_step:
|
|
285
|
-
prev_time_step = self._time_step
|
|
286
|
-
self._time_step = self._next_time_step
|
|
287
|
-
logger.info(f"Updated time step to {self._time_step}.")
|
|
288
|
-
self.notify_observers("time_step", prev_time_step, self._time_step)
|
|
289
|
-
# update the execution time
|
|
290
|
-
if self._time != self._next_time:
|
|
291
|
-
prev_time = self._time
|
|
292
|
-
self._time = self._next_time
|
|
293
|
-
logger.debug(f"Updated time {self._time}.")
|
|
294
|
-
self.notify_observers(self.PROPERTY_TIME, prev_time, self._time)
|
|
295
|
-
logger.debug(f"Simulation advanced to time {self.get_time()}.")
|
|
296
|
-
|
|
297
|
-
logger.info("Simulation complete; terminating.")
|
|
298
|
-
self._set_mode(Mode.TERMINATING)
|
|
299
|
-
self._set_mode(Mode.TERMINATED)
|
|
300
|
-
|
|
301
|
-
def _wait_for_tock(self) -> None:
|
|
302
|
-
"""
|
|
303
|
-
Waits until the wallclock time matches the next time step interval.
|
|
304
|
-
"""
|
|
305
|
-
while (
|
|
306
|
-
self._mode == Mode.EXECUTING
|
|
307
|
-
and self.get_wallclock_time_at_simulation_time(self._next_time)
|
|
308
|
-
> self.get_wallclock_time()
|
|
309
|
-
):
|
|
310
|
-
time_diff = (
|
|
311
|
-
self.get_wallclock_time_at_simulation_time(self._next_time)
|
|
312
|
-
- self.get_wallclock_time()
|
|
313
|
-
)
|
|
314
|
-
if time_diff > timedelta(seconds=0):
|
|
315
|
-
logger.debug(f"Waiting for {time_diff} to advance time.")
|
|
316
|
-
# sleep for up to a second
|
|
317
|
-
time.sleep(min(1, time_diff / timedelta(seconds=1)))
|
|
318
|
-
|
|
319
|
-
def _wait_for_wallclock_epoch(self) -> None:
|
|
320
|
-
"""
|
|
321
|
-
Waits until the wallclock time matches the designated wallclock epoch.
|
|
322
|
-
"""
|
|
323
|
-
epoch_diff = self._wallclock_epoch - self.get_wallclock_time()
|
|
324
|
-
if epoch_diff > timedelta(seconds=0):
|
|
325
|
-
logger.info(f"Waiting for {epoch_diff} to synchronize execution start.")
|
|
326
|
-
time.sleep(epoch_diff / timedelta(seconds=1))
|
|
327
|
-
|
|
328
|
-
def get_mode(self) -> Mode:
|
|
329
|
-
"""
|
|
330
|
-
Gets the current simulation mode.
|
|
331
|
-
|
|
332
|
-
Returns:
|
|
333
|
-
:obj:`Mode`: current simulation mode
|
|
334
|
-
"""
|
|
335
|
-
return self._mode
|
|
336
|
-
|
|
337
|
-
def _set_mode(self, mode: Mode) -> None:
|
|
338
|
-
"""
|
|
339
|
-
Sets the simulation mode and notifies observers.
|
|
340
|
-
|
|
341
|
-
Args:
|
|
342
|
-
mode (:obj:`Mode`): new simulation mode
|
|
343
|
-
"""
|
|
344
|
-
prev_mode = self._mode
|
|
345
|
-
self._mode = mode
|
|
346
|
-
self.notify_observers(self.PROPERTY_MODE, prev_mode, self._mode)
|
|
347
|
-
|
|
348
|
-
def get_time_scale_factor(self) -> float:
|
|
349
|
-
"""
|
|
350
|
-
Gets the time scale factor in scenario seconds per wall clock second (>1 is faster-than-real-time).
|
|
351
|
-
|
|
352
|
-
Returns:
|
|
353
|
-
float: current time scale factor
|
|
354
|
-
"""
|
|
355
|
-
return self._time_scale_factor
|
|
356
|
-
|
|
357
|
-
def get_wallclock_epoch(self) -> datetime:
|
|
358
|
-
"""
|
|
359
|
-
Gets the wallclock epoch.
|
|
360
|
-
|
|
361
|
-
Returns:
|
|
362
|
-
:obj:`datetime`: current wallclock epoch
|
|
363
|
-
"""
|
|
364
|
-
return self._wallclock_epoch
|
|
365
|
-
|
|
366
|
-
def get_simulation_epoch(self) -> datetime:
|
|
367
|
-
"""
|
|
368
|
-
Gets the scenario epoch.
|
|
369
|
-
|
|
370
|
-
Returns:
|
|
371
|
-
:obj:`datetime`: current scenario epoch
|
|
372
|
-
"""
|
|
373
|
-
return self._simulation_epoch
|
|
374
|
-
|
|
375
|
-
def get_duration(self) -> timedelta:
|
|
376
|
-
"""
|
|
377
|
-
Gets the scenario duration.
|
|
378
|
-
|
|
379
|
-
Returns:
|
|
380
|
-
:obj:`timedelta`: current scenario duration
|
|
381
|
-
"""
|
|
382
|
-
return self._duration
|
|
383
|
-
|
|
384
|
-
def get_end_time(self) -> datetime:
|
|
385
|
-
"""
|
|
386
|
-
Gets the scenario end time.
|
|
387
|
-
|
|
388
|
-
Returns:
|
|
389
|
-
:obj:`datetime`: final scenario time
|
|
390
|
-
"""
|
|
391
|
-
return self._init_time + self._duration
|
|
392
|
-
|
|
393
|
-
def get_init_time(self) -> datetime:
|
|
394
|
-
"""
|
|
395
|
-
Gets the initial scenario time.
|
|
396
|
-
|
|
397
|
-
Returns:
|
|
398
|
-
:obj:`datetime`: initial scenario time
|
|
399
|
-
"""
|
|
400
|
-
return self._init_time
|
|
401
|
-
|
|
402
|
-
def get_time(self) -> datetime:
|
|
403
|
-
"""
|
|
404
|
-
Gets the current scenario time.
|
|
405
|
-
|
|
406
|
-
Returns:
|
|
407
|
-
:obj:`datetime`: current scenario time
|
|
408
|
-
"""
|
|
409
|
-
return self._time
|
|
410
|
-
|
|
411
|
-
def get_time_step(self) -> timedelta:
|
|
412
|
-
"""
|
|
413
|
-
Gets the scenario time step duration.
|
|
414
|
-
|
|
415
|
-
Returns:
|
|
416
|
-
:obj:`timedelta`: time step duration
|
|
417
|
-
"""
|
|
418
|
-
return self._time_step
|
|
419
|
-
|
|
420
|
-
def get_wallclock_time_step(self) -> timedelta:
|
|
421
|
-
"""
|
|
422
|
-
Gets the wallclock time step duration.
|
|
423
|
-
|
|
424
|
-
Returns:
|
|
425
|
-
:obj:`timedelta`: time step duration
|
|
426
|
-
"""
|
|
427
|
-
if self._time_scale_factor is None or self._time_scale_factor <= 0:
|
|
428
|
-
return self._time_step
|
|
429
|
-
else:
|
|
430
|
-
return self._time_step * self._time_scale_factor
|
|
431
|
-
|
|
432
|
-
def get_wallclock_time(self) -> datetime:
|
|
433
|
-
"""
|
|
434
|
-
Gets the current wallclock time.
|
|
435
|
-
|
|
436
|
-
Returns:
|
|
437
|
-
:obj:`datetime`: current wallclock time
|
|
438
|
-
"""
|
|
439
|
-
return datetime.now(tz=timezone.utc) + self._wallclock_offset
|
|
440
|
-
|
|
441
|
-
def get_wallclock_time_at_simulation_time(self, time: datetime) -> datetime:
|
|
442
|
-
"""
|
|
443
|
-
Gets the wallclock time corresponding to the designated scenario time.
|
|
444
|
-
|
|
445
|
-
Args:
|
|
446
|
-
time (:obj:`datetime`): scenario time
|
|
447
|
-
|
|
448
|
-
Returns:
|
|
449
|
-
:obj:`datetime`: wallclock time
|
|
450
|
-
"""
|
|
451
|
-
if self._time_scale_factor is None or self._time_scale_factor <= 0:
|
|
452
|
-
return self.get_wallclock_time()
|
|
453
|
-
else:
|
|
454
|
-
return (
|
|
455
|
-
self._wallclock_epoch
|
|
456
|
-
+ (time - self.get_simulation_epoch()) / self._time_scale_factor
|
|
457
|
-
)
|
|
458
|
-
|
|
459
|
-
def set_time_scale_factor(
|
|
460
|
-
self, time_scale_factor: float, simulation_epoch: datetime = None
|
|
461
|
-
) -> None:
|
|
462
|
-
"""
|
|
463
|
-
Sets the time scale factor in scenario seconds per wallclock second
|
|
464
|
-
(>1 is faster-than-real-time). Requires that the simulator is in EXECUTING mode.
|
|
465
|
-
|
|
466
|
-
Args:
|
|
467
|
-
time_scale_factor (float): number of scenario seconds per wallclock second
|
|
468
|
-
simulation_epoch (:obj:`datetime`): scenario time at which the time scale factor changes
|
|
469
|
-
"""
|
|
470
|
-
if self._mode != Mode.EXECUTING:
|
|
471
|
-
raise RuntimeError("Can only change time scale factor while executing.")
|
|
472
|
-
self._next_time_scale_factor = time_scale_factor
|
|
473
|
-
if simulation_epoch is None:
|
|
474
|
-
self._time_scale_change_time = self._time
|
|
475
|
-
else:
|
|
476
|
-
self._time_scale_change_time = simulation_epoch
|
|
477
|
-
|
|
478
|
-
def set_end_time(self, end_time: datetime) -> None:
|
|
479
|
-
"""
|
|
480
|
-
Sets the scenario end time. Requires that the simulator is in EXECUTING mode.
|
|
481
|
-
|
|
482
|
-
Args:
|
|
483
|
-
end_time (:obj:`datetime`): scenario end time
|
|
484
|
-
"""
|
|
485
|
-
if self._mode != Mode.EXECUTING:
|
|
486
|
-
raise RuntimeError("Can only change scenario end time while executing.")
|
|
487
|
-
self.set_duration(end_time - self._init_time)
|
|
488
|
-
|
|
489
|
-
def set_duration(self, duration: timedelta) -> None:
|
|
490
|
-
"""
|
|
491
|
-
Sets the scenario duration. Requires that the simulator is in EXECUTING mode.
|
|
492
|
-
|
|
493
|
-
Args:
|
|
494
|
-
duration (:obj:`timedelta`): scenario duration
|
|
495
|
-
"""
|
|
496
|
-
if self._mode != Mode.EXECUTING:
|
|
497
|
-
raise RuntimeError("Can only change scenario duration while executing.")
|
|
498
|
-
self._next_duration = duration
|
|
499
|
-
|
|
500
|
-
def set_time_step(self, time_step: timedelta) -> None:
|
|
501
|
-
"""
|
|
502
|
-
Set the scenario time step duration. Requires that the simulator is in EXECUTING mode.
|
|
503
|
-
|
|
504
|
-
Args:
|
|
505
|
-
time_step (:obj:`timedelta`): scenario time step duration
|
|
506
|
-
"""
|
|
507
|
-
if self._mode != Mode.EXECUTING:
|
|
508
|
-
raise RuntimeError("Can only change scenario time step while executing.")
|
|
509
|
-
self._next_time_step = time_step
|
|
510
|
-
|
|
511
|
-
def set_wallclock_offset(self, wallclock_offset: timedelta) -> None:
|
|
512
|
-
"""
|
|
513
|
-
Set the wallclock offset (difference between system clock and trusted wallclock source).
|
|
514
|
-
Requires that the simulator is in UNDEFINED, INITIALIZING, INITIALIZED, or TERMINATED mode.
|
|
515
|
-
|
|
516
|
-
Args:
|
|
517
|
-
wallclock_offset(:obj:`timedelta`): difference between system clock and trusted wallclock source
|
|
518
|
-
"""
|
|
519
|
-
if self._mode == Mode.EXECUTING:
|
|
520
|
-
raise RuntimeError("Cannot set wallclock offset: simulator is executing")
|
|
521
|
-
elif self._mode == Mode.TERMINATING:
|
|
522
|
-
raise RuntimeError("Cannot set wallclock offset: simulator is terminating")
|
|
523
|
-
self._wallclock_offset = wallclock_offset
|
|
524
|
-
|
|
525
|
-
def terminate(self) -> None:
|
|
526
|
-
"""
|
|
527
|
-
Terminates the scenario execution. Requires that the simulator is in EXECUTING mode.
|
|
528
|
-
"""
|
|
529
|
-
if self._mode != Mode.EXECUTING:
|
|
530
|
-
raise RuntimeError("Cannot terminate: simulator is not executing.")
|
|
531
|
-
self._set_mode(Mode.TERMINATING)
|
|
1
|
+
"""
|
|
2
|
+
Provides classes to execute a simulation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import List, Type
|
|
10
|
+
|
|
11
|
+
from .entity import Entity
|
|
12
|
+
from .observer import Observable
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Mode(str, Enum):
|
|
18
|
+
"""
|
|
19
|
+
Enumeration of simulation modes.
|
|
20
|
+
|
|
21
|
+
The six simulation modes include
|
|
22
|
+
* `UNDEFINED`: Simulation is in an undefined state that is not one of the other modes.
|
|
23
|
+
For example, the simulation reverts to UNDEFINED after adding a new entity
|
|
24
|
+
and must be re-initialized before execution.
|
|
25
|
+
* `INITIALIZING`: Simulation is in the process of initialization.
|
|
26
|
+
* `INITIALIZED`: Simulation has finished initialization and is ready to execute.
|
|
27
|
+
* `EXECUTING`: Simulation is in the process of execution.
|
|
28
|
+
* `TERMINATING`: Simulation is in the process of termination.
|
|
29
|
+
* `TERMINATED`: Simulation has finished termination and is ready for initialization.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
UNDEFINED = "UNDEFINED"
|
|
33
|
+
INITIALIZING = "INITIALIZING"
|
|
34
|
+
INITIALIZED = "INITIALIZED"
|
|
35
|
+
EXECUTING = "EXECUTING"
|
|
36
|
+
TERMINATING = "TERMINATING"
|
|
37
|
+
TERMINATED = "TERMINATED"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Simulator(Observable):
|
|
41
|
+
"""
|
|
42
|
+
Object that manages simulation of entities in a scenario.
|
|
43
|
+
|
|
44
|
+
Notifies observers of changes to observable properties
|
|
45
|
+
* `time`: current scenario time
|
|
46
|
+
* `mode`: current execution mode
|
|
47
|
+
* `duration`: scenario execution duration
|
|
48
|
+
* `time_step`: scenario time step duration
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
PROPERTY_MODE = "mode"
|
|
52
|
+
PROPERTY_TIME = "time"
|
|
53
|
+
|
|
54
|
+
def __init__(self, wallclock_offset: timedelta = timedelta()):
|
|
55
|
+
"""
|
|
56
|
+
Initializes a new simulator.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
wallclock_offset (:obj:`timedelta`): difference between the system
|
|
60
|
+
clock and trusted wallclock source (default: zero)
|
|
61
|
+
"""
|
|
62
|
+
# call super class constructor
|
|
63
|
+
super().__init__()
|
|
64
|
+
# offset from the system clock to "true" time
|
|
65
|
+
self._wallclock_offset = wallclock_offset
|
|
66
|
+
# list of entities that participate in this simulation
|
|
67
|
+
self._entities = []
|
|
68
|
+
# current mode of the simulator
|
|
69
|
+
self._mode = Mode.UNDEFINED
|
|
70
|
+
# current simulation time; next simulation time, initial simulation time
|
|
71
|
+
self._time = self._next_time = self._init_time = 0
|
|
72
|
+
# current simulation time step; next simulation time step
|
|
73
|
+
self._time_step = self._next_time_step = None
|
|
74
|
+
# current simulation duration; next simulation duration
|
|
75
|
+
self._duration = self._next_duration = None
|
|
76
|
+
# wallclock time when the simulation starts or changes time scaling
|
|
77
|
+
self._wallclock_epoch = None
|
|
78
|
+
# simulation time when the simulation starts or changes time scaling
|
|
79
|
+
self._simulation_epoch = None
|
|
80
|
+
# simulation time at which to perform a time scale change
|
|
81
|
+
self._time_scale_change_time = None
|
|
82
|
+
# relationship between the wallclock time and simulation time
|
|
83
|
+
self._time_scale_factor = self._next_time_scale_factor = 1
|
|
84
|
+
|
|
85
|
+
def add_entity(self, entity: Entity) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Adds an entity the the simulation.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
entity (:obj:`Entity`): entity to be added
|
|
91
|
+
"""
|
|
92
|
+
if self._mode == Mode.INITIALIZING:
|
|
93
|
+
raise RuntimeError("Cannot add entity: simulator is initializing")
|
|
94
|
+
elif self._mode == Mode.EXECUTING:
|
|
95
|
+
raise RuntimeError("Cannot add entity: simulator is executing")
|
|
96
|
+
elif self._mode == Mode.TERMINATING:
|
|
97
|
+
raise RuntimeError("Cannot add entity: simulator is terminating")
|
|
98
|
+
self._set_mode(Mode.UNDEFINED)
|
|
99
|
+
self._entities.append(entity)
|
|
100
|
+
|
|
101
|
+
def get_entities(self) -> List[Entity]:
|
|
102
|
+
"""
|
|
103
|
+
Retrieves a list of all entities in the simulation.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
List(Entity): list of entities in the simulation
|
|
107
|
+
"""
|
|
108
|
+
# perform shallow copy to prevent external modification
|
|
109
|
+
return self._entities.copy()
|
|
110
|
+
|
|
111
|
+
def get_entities_by_name(self, name: str) -> List[Entity]:
|
|
112
|
+
"""
|
|
113
|
+
Retrieves a list of entities by name.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
name (str): name of the entity
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
List(Entity): list of entities with a matching name
|
|
120
|
+
"""
|
|
121
|
+
return [entity for entity in self._entities if entity.name == name]
|
|
122
|
+
|
|
123
|
+
def get_entities_by_type(self, type: Type) -> List[Entity]:
|
|
124
|
+
"""
|
|
125
|
+
Retrieves a list of entities by type (class).
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
type (Type): type (class) of entity
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List(Entity): list of entities with a matching type
|
|
132
|
+
"""
|
|
133
|
+
return [entity for entity in self._entities if isinstance(entity, type)]
|
|
134
|
+
|
|
135
|
+
def remove_entity(self, entity: Entity) -> Entity:
|
|
136
|
+
"""
|
|
137
|
+
Removes an entity from the simulation.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
entity (:obj:`Entity`): entity to be removed
|
|
141
|
+
Returns:
|
|
142
|
+
:obj:`Entity`: removed entity
|
|
143
|
+
"""
|
|
144
|
+
if self._mode == Mode.INITIALIZING:
|
|
145
|
+
raise RuntimeError("Cannot add entity: simulator is initializing")
|
|
146
|
+
elif self._mode == Mode.EXECUTING:
|
|
147
|
+
raise RuntimeError("Cannot add entity: simulator is executing")
|
|
148
|
+
elif self._mode == Mode.TERMINATING:
|
|
149
|
+
raise RuntimeError("Cannot add entity: simulator is terminating")
|
|
150
|
+
if entity in self._entities:
|
|
151
|
+
self._set_mode(Mode.UNDEFINED)
|
|
152
|
+
return self._entities.remove(entity)
|
|
153
|
+
else:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def initialize(
|
|
157
|
+
self,
|
|
158
|
+
init_time: datetime,
|
|
159
|
+
wallclock_epoch: datetime = None,
|
|
160
|
+
time_scale_factor: float = 1,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Initializes the simulation to an initial scenario time. Requires that the
|
|
164
|
+
simulator is in UNDEFINED, INITIALIZED, or TERMINATED mode.
|
|
165
|
+
|
|
166
|
+
Transitions to the INITIALIZING mode, initializes all entities to the
|
|
167
|
+
initial scenario time, sets the wallclock epoch (wallclock time corresponding
|
|
168
|
+
with the initial scenario time), and finally transitions to the INITIALIZED mode.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
init_time (:obj:`datetime`): initial scenario time
|
|
172
|
+
wallclock_epoch (:obj:`datetime`): wallclock time corresponding to the
|
|
173
|
+
initial scenario time, None uses the current wallclock time (default: None)
|
|
174
|
+
time_scale_factor (float): number of scenario seconds per wallclock second (default: 1)
|
|
175
|
+
"""
|
|
176
|
+
if self._mode == Mode.INITIALIZING:
|
|
177
|
+
raise RuntimeError("Cannot initialize: simulator is initializing.")
|
|
178
|
+
elif self._mode == Mode.EXECUTING:
|
|
179
|
+
raise RuntimeError("Cannot initialize: simulator is executing.")
|
|
180
|
+
elif self._mode == Mode.TERMINATING:
|
|
181
|
+
raise RuntimeError("Cannot initialize: simulator is terminating.")
|
|
182
|
+
self._set_mode(Mode.INITIALIZING)
|
|
183
|
+
logger.info(
|
|
184
|
+
f"Initializing simulator to time {init_time} (wallclock time {wallclock_epoch})"
|
|
185
|
+
)
|
|
186
|
+
for entity in self._entities:
|
|
187
|
+
entity.initialize(init_time)
|
|
188
|
+
self._time = self._next_time = self._init_time = init_time
|
|
189
|
+
self._simulation_epoch = init_time
|
|
190
|
+
if wallclock_epoch is None:
|
|
191
|
+
self._wallclock_epoch = self.get_wallclock_time()
|
|
192
|
+
else:
|
|
193
|
+
self._wallclock_epoch = wallclock_epoch
|
|
194
|
+
self._time_scale_factor = self._next_time_scale_factor = time_scale_factor
|
|
195
|
+
self._set_mode(Mode.INITIALIZED)
|
|
196
|
+
|
|
197
|
+
def execute(
|
|
198
|
+
self,
|
|
199
|
+
init_time: datetime,
|
|
200
|
+
duration: timedelta,
|
|
201
|
+
time_step: timedelta,
|
|
202
|
+
wallclock_epoch: datetime = None,
|
|
203
|
+
time_scale_factor: float = 1,
|
|
204
|
+
) -> None:
|
|
205
|
+
"""
|
|
206
|
+
Executes a simulation for a specified duration with uniform time steps. Requires that the
|
|
207
|
+
simulator is in UNDEFINED, INITIALIZED, or TERMINATED mode.
|
|
208
|
+
|
|
209
|
+
Initializes the simulation (if not already in the INITIALIZED mode), waits for the
|
|
210
|
+
specified wallclock epoch, and transitions to the EXECUTING mode. During execution,
|
|
211
|
+
incrementally performs state transitions for each entity. At the end of the simulation,
|
|
212
|
+
transitions to the TERMINATING and, finally, TERMINATED mode.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
init_time (:obj:`datetime`): initial scenario time
|
|
216
|
+
duration (:obj:`timedelta`): scenario execution duration
|
|
217
|
+
time_step (:obj:`timedelta`): scenario time step duration
|
|
218
|
+
wallclock_epoch (:obj:`datetime`): wallclock time corresponding to the
|
|
219
|
+
initial scenario time, None uses the current wallclock time (default: None)
|
|
220
|
+
time_scale_factor (float): number of scenario seconds per wallclock second (default value: 1)
|
|
221
|
+
"""
|
|
222
|
+
if self._mode != Mode.INITIALIZED:
|
|
223
|
+
self.initialize(init_time, wallclock_epoch, time_scale_factor)
|
|
224
|
+
|
|
225
|
+
self._duration = self._next_duration = duration
|
|
226
|
+
self._time_step = self._next_time_step = time_step
|
|
227
|
+
|
|
228
|
+
logger.info(
|
|
229
|
+
f"Executing simulator for {duration} ({time_step} steps), starting at {self._wallclock_epoch}."
|
|
230
|
+
)
|
|
231
|
+
self._wait_for_wallclock_epoch()
|
|
232
|
+
self._set_mode(Mode.EXECUTING)
|
|
233
|
+
|
|
234
|
+
logger.info("Starting main simulation loop.")
|
|
235
|
+
while (
|
|
236
|
+
self._mode == Mode.EXECUTING
|
|
237
|
+
and self.get_time() < self.get_init_time() + self.get_duration()
|
|
238
|
+
):
|
|
239
|
+
# compute time step (last step may be shorter)
|
|
240
|
+
time_step = min(
|
|
241
|
+
self._time_step, self._init_time + self._duration - self._time
|
|
242
|
+
)
|
|
243
|
+
# tick each entity
|
|
244
|
+
for entity in self._entities:
|
|
245
|
+
entity.tick(
|
|
246
|
+
min(time_step, self._init_time + self._duration - self._time)
|
|
247
|
+
)
|
|
248
|
+
# store the next time
|
|
249
|
+
self._next_time = self._time + time_step
|
|
250
|
+
if (
|
|
251
|
+
self._time_scale_change_time is not None
|
|
252
|
+
and self._time_scale_change_time < self._next_time
|
|
253
|
+
):
|
|
254
|
+
# update the wallclock epoch of this change
|
|
255
|
+
self._wallclock_epoch = self.get_wallclock_time_at_simulation_time(
|
|
256
|
+
self._time
|
|
257
|
+
)
|
|
258
|
+
# update the simulation epoch of this change
|
|
259
|
+
self._simulation_epoch = self._time
|
|
260
|
+
# reset the flag to change the time scale factor
|
|
261
|
+
self._time_scale_change_time = None
|
|
262
|
+
# commit the change to the time scale factor and notify observers
|
|
263
|
+
prev_time_scale_factor = self._time_scale_factor
|
|
264
|
+
self._time_scale_factor = self._next_time_scale_factor
|
|
265
|
+
self.notify_observers(
|
|
266
|
+
"time_scale_factor", prev_time_scale_factor, self._time_scale_factor
|
|
267
|
+
)
|
|
268
|
+
# wait for the correct time
|
|
269
|
+
self._wait_for_tock()
|
|
270
|
+
# break out of loop if terminating execution
|
|
271
|
+
if self._mode == Mode.TERMINATING:
|
|
272
|
+
logger.debug("Terminating: exiting execution loop.")
|
|
273
|
+
break
|
|
274
|
+
# tock each entity
|
|
275
|
+
for entity in self._entities:
|
|
276
|
+
entity.tock()
|
|
277
|
+
# update the execution duration, if needed
|
|
278
|
+
if self._duration != self._next_duration:
|
|
279
|
+
prev_duration = self._duration
|
|
280
|
+
self._duration = self._next_duration
|
|
281
|
+
logger.info(f"Updated duration to {self._duration}.")
|
|
282
|
+
self.notify_observers("duration", prev_duration, self._duration)
|
|
283
|
+
# update the execution time step, if needed
|
|
284
|
+
if self._time_step != self._next_time_step:
|
|
285
|
+
prev_time_step = self._time_step
|
|
286
|
+
self._time_step = self._next_time_step
|
|
287
|
+
logger.info(f"Updated time step to {self._time_step}.")
|
|
288
|
+
self.notify_observers("time_step", prev_time_step, self._time_step)
|
|
289
|
+
# update the execution time
|
|
290
|
+
if self._time != self._next_time:
|
|
291
|
+
prev_time = self._time
|
|
292
|
+
self._time = self._next_time
|
|
293
|
+
logger.debug(f"Updated time {self._time}.")
|
|
294
|
+
self.notify_observers(self.PROPERTY_TIME, prev_time, self._time)
|
|
295
|
+
logger.debug(f"Simulation advanced to time {self.get_time()}.")
|
|
296
|
+
|
|
297
|
+
logger.info("Simulation complete; terminating.")
|
|
298
|
+
self._set_mode(Mode.TERMINATING)
|
|
299
|
+
self._set_mode(Mode.TERMINATED)
|
|
300
|
+
|
|
301
|
+
def _wait_for_tock(self) -> None:
|
|
302
|
+
"""
|
|
303
|
+
Waits until the wallclock time matches the next time step interval.
|
|
304
|
+
"""
|
|
305
|
+
while (
|
|
306
|
+
self._mode == Mode.EXECUTING
|
|
307
|
+
and self.get_wallclock_time_at_simulation_time(self._next_time)
|
|
308
|
+
> self.get_wallclock_time()
|
|
309
|
+
):
|
|
310
|
+
time_diff = (
|
|
311
|
+
self.get_wallclock_time_at_simulation_time(self._next_time)
|
|
312
|
+
- self.get_wallclock_time()
|
|
313
|
+
)
|
|
314
|
+
if time_diff > timedelta(seconds=0):
|
|
315
|
+
logger.debug(f"Waiting for {time_diff} to advance time.")
|
|
316
|
+
# sleep for up to a second
|
|
317
|
+
time.sleep(min(1, time_diff / timedelta(seconds=1)))
|
|
318
|
+
|
|
319
|
+
def _wait_for_wallclock_epoch(self) -> None:
|
|
320
|
+
"""
|
|
321
|
+
Waits until the wallclock time matches the designated wallclock epoch.
|
|
322
|
+
"""
|
|
323
|
+
epoch_diff = self._wallclock_epoch - self.get_wallclock_time()
|
|
324
|
+
if epoch_diff > timedelta(seconds=0):
|
|
325
|
+
logger.info(f"Waiting for {epoch_diff} to synchronize execution start.")
|
|
326
|
+
time.sleep(epoch_diff / timedelta(seconds=1))
|
|
327
|
+
|
|
328
|
+
def get_mode(self) -> Mode:
|
|
329
|
+
"""
|
|
330
|
+
Gets the current simulation mode.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
:obj:`Mode`: current simulation mode
|
|
334
|
+
"""
|
|
335
|
+
return self._mode
|
|
336
|
+
|
|
337
|
+
def _set_mode(self, mode: Mode) -> None:
|
|
338
|
+
"""
|
|
339
|
+
Sets the simulation mode and notifies observers.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
mode (:obj:`Mode`): new simulation mode
|
|
343
|
+
"""
|
|
344
|
+
prev_mode = self._mode
|
|
345
|
+
self._mode = mode
|
|
346
|
+
self.notify_observers(self.PROPERTY_MODE, prev_mode, self._mode)
|
|
347
|
+
|
|
348
|
+
def get_time_scale_factor(self) -> float:
|
|
349
|
+
"""
|
|
350
|
+
Gets the time scale factor in scenario seconds per wall clock second (>1 is faster-than-real-time).
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
float: current time scale factor
|
|
354
|
+
"""
|
|
355
|
+
return self._time_scale_factor
|
|
356
|
+
|
|
357
|
+
def get_wallclock_epoch(self) -> datetime:
|
|
358
|
+
"""
|
|
359
|
+
Gets the wallclock epoch.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
:obj:`datetime`: current wallclock epoch
|
|
363
|
+
"""
|
|
364
|
+
return self._wallclock_epoch
|
|
365
|
+
|
|
366
|
+
def get_simulation_epoch(self) -> datetime:
|
|
367
|
+
"""
|
|
368
|
+
Gets the scenario epoch.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
:obj:`datetime`: current scenario epoch
|
|
372
|
+
"""
|
|
373
|
+
return self._simulation_epoch
|
|
374
|
+
|
|
375
|
+
def get_duration(self) -> timedelta:
|
|
376
|
+
"""
|
|
377
|
+
Gets the scenario duration.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
:obj:`timedelta`: current scenario duration
|
|
381
|
+
"""
|
|
382
|
+
return self._duration
|
|
383
|
+
|
|
384
|
+
def get_end_time(self) -> datetime:
|
|
385
|
+
"""
|
|
386
|
+
Gets the scenario end time.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
:obj:`datetime`: final scenario time
|
|
390
|
+
"""
|
|
391
|
+
return self._init_time + self._duration
|
|
392
|
+
|
|
393
|
+
def get_init_time(self) -> datetime:
|
|
394
|
+
"""
|
|
395
|
+
Gets the initial scenario time.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
:obj:`datetime`: initial scenario time
|
|
399
|
+
"""
|
|
400
|
+
return self._init_time
|
|
401
|
+
|
|
402
|
+
def get_time(self) -> datetime:
|
|
403
|
+
"""
|
|
404
|
+
Gets the current scenario time.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
:obj:`datetime`: current scenario time
|
|
408
|
+
"""
|
|
409
|
+
return self._time
|
|
410
|
+
|
|
411
|
+
def get_time_step(self) -> timedelta:
|
|
412
|
+
"""
|
|
413
|
+
Gets the scenario time step duration.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
:obj:`timedelta`: time step duration
|
|
417
|
+
"""
|
|
418
|
+
return self._time_step
|
|
419
|
+
|
|
420
|
+
def get_wallclock_time_step(self) -> timedelta:
|
|
421
|
+
"""
|
|
422
|
+
Gets the wallclock time step duration.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
:obj:`timedelta`: time step duration
|
|
426
|
+
"""
|
|
427
|
+
if self._time_scale_factor is None or self._time_scale_factor <= 0:
|
|
428
|
+
return self._time_step
|
|
429
|
+
else:
|
|
430
|
+
return self._time_step * self._time_scale_factor
|
|
431
|
+
|
|
432
|
+
def get_wallclock_time(self) -> datetime:
|
|
433
|
+
"""
|
|
434
|
+
Gets the current wallclock time.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
:obj:`datetime`: current wallclock time
|
|
438
|
+
"""
|
|
439
|
+
return datetime.now(tz=timezone.utc) + self._wallclock_offset
|
|
440
|
+
|
|
441
|
+
def get_wallclock_time_at_simulation_time(self, time: datetime) -> datetime:
|
|
442
|
+
"""
|
|
443
|
+
Gets the wallclock time corresponding to the designated scenario time.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
time (:obj:`datetime`): scenario time
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
:obj:`datetime`: wallclock time
|
|
450
|
+
"""
|
|
451
|
+
if self._time_scale_factor is None or self._time_scale_factor <= 0:
|
|
452
|
+
return self.get_wallclock_time()
|
|
453
|
+
else:
|
|
454
|
+
return (
|
|
455
|
+
self._wallclock_epoch
|
|
456
|
+
+ (time - self.get_simulation_epoch()) / self._time_scale_factor
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
def set_time_scale_factor(
|
|
460
|
+
self, time_scale_factor: float, simulation_epoch: datetime = None
|
|
461
|
+
) -> None:
|
|
462
|
+
"""
|
|
463
|
+
Sets the time scale factor in scenario seconds per wallclock second
|
|
464
|
+
(>1 is faster-than-real-time). Requires that the simulator is in EXECUTING mode.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
time_scale_factor (float): number of scenario seconds per wallclock second
|
|
468
|
+
simulation_epoch (:obj:`datetime`): scenario time at which the time scale factor changes
|
|
469
|
+
"""
|
|
470
|
+
if self._mode != Mode.EXECUTING:
|
|
471
|
+
raise RuntimeError("Can only change time scale factor while executing.")
|
|
472
|
+
self._next_time_scale_factor = time_scale_factor
|
|
473
|
+
if simulation_epoch is None:
|
|
474
|
+
self._time_scale_change_time = self._time
|
|
475
|
+
else:
|
|
476
|
+
self._time_scale_change_time = simulation_epoch
|
|
477
|
+
|
|
478
|
+
def set_end_time(self, end_time: datetime) -> None:
|
|
479
|
+
"""
|
|
480
|
+
Sets the scenario end time. Requires that the simulator is in EXECUTING mode.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
end_time (:obj:`datetime`): scenario end time
|
|
484
|
+
"""
|
|
485
|
+
if self._mode != Mode.EXECUTING:
|
|
486
|
+
raise RuntimeError("Can only change scenario end time while executing.")
|
|
487
|
+
self.set_duration(end_time - self._init_time)
|
|
488
|
+
|
|
489
|
+
def set_duration(self, duration: timedelta) -> None:
|
|
490
|
+
"""
|
|
491
|
+
Sets the scenario duration. Requires that the simulator is in EXECUTING mode.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
duration (:obj:`timedelta`): scenario duration
|
|
495
|
+
"""
|
|
496
|
+
if self._mode != Mode.EXECUTING:
|
|
497
|
+
raise RuntimeError("Can only change scenario duration while executing.")
|
|
498
|
+
self._next_duration = duration
|
|
499
|
+
|
|
500
|
+
def set_time_step(self, time_step: timedelta) -> None:
|
|
501
|
+
"""
|
|
502
|
+
Set the scenario time step duration. Requires that the simulator is in EXECUTING mode.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
time_step (:obj:`timedelta`): scenario time step duration
|
|
506
|
+
"""
|
|
507
|
+
if self._mode != Mode.EXECUTING:
|
|
508
|
+
raise RuntimeError("Can only change scenario time step while executing.")
|
|
509
|
+
self._next_time_step = time_step
|
|
510
|
+
|
|
511
|
+
def set_wallclock_offset(self, wallclock_offset: timedelta) -> None:
|
|
512
|
+
"""
|
|
513
|
+
Set the wallclock offset (difference between system clock and trusted wallclock source).
|
|
514
|
+
Requires that the simulator is in UNDEFINED, INITIALIZING, INITIALIZED, or TERMINATED mode.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
wallclock_offset(:obj:`timedelta`): difference between system clock and trusted wallclock source
|
|
518
|
+
"""
|
|
519
|
+
if self._mode == Mode.EXECUTING:
|
|
520
|
+
raise RuntimeError("Cannot set wallclock offset: simulator is executing")
|
|
521
|
+
elif self._mode == Mode.TERMINATING:
|
|
522
|
+
raise RuntimeError("Cannot set wallclock offset: simulator is terminating")
|
|
523
|
+
self._wallclock_offset = wallclock_offset
|
|
524
|
+
|
|
525
|
+
def terminate(self) -> None:
|
|
526
|
+
"""
|
|
527
|
+
Terminates the scenario execution. Requires that the simulator is in EXECUTING mode.
|
|
528
|
+
"""
|
|
529
|
+
if self._mode != Mode.EXECUTING:
|
|
530
|
+
raise RuntimeError("Cannot terminate: simulator is not executing.")
|
|
531
|
+
self._set_mode(Mode.TERMINATING)
|