Skip to content

Commit a927248

Browse files
committed
ENH: improve structure
1 parent 080e316 commit a927248

2 files changed

Lines changed: 185 additions & 64 deletions

File tree

rocketpy/simulation/events.py

Lines changed: 182 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,157 @@
11
class Event:
2-
def __init__(self, trigger, action, name):
2+
# TODO: should "sensors" arg of the trigger function be a dictionary instead
3+
# of a list? It would be more intuitive to access the sensors by name
4+
def __init__(self, trigger, action, name, event_context=None):
35
"""Initializes an Event object.
46
57
Parameters
68
----------
79
trigger : function
8-
A function that takes the current state of the simulation as input and returns a boolean value. The event will be triggered when this function returns True.
10+
A function that must return a boolean value. The event will be
11+
triggered when this function returns True. The function should be
12+
defined with the following signature: trigger(**kwargs), where
13+
kwargs is a dictionary containing the keys:
14+
15+
- `"time"` (float): The current simulation time in seconds.
16+
- `"state"` (list): The state vector of the simulation, structured
17+
as `[x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz]`.
18+
- `"state_dot"` (list): The time derivative of the state vector,
19+
structured as `[vx, vy, vz, ax, ay, az, e0_dot, e1_dot, e2_dot, e3_dot, wx_dot, wy_dot, wz_dot]`.
20+
- `"sampling_rate"` (float or None): The sampling rate of the
21+
event, in seconds. If None, the event will be checked for
22+
triggering at every time step of the simulation. If a float
23+
value is provided, the event will only be checked for
24+
triggering at that specific time interval.
25+
- `"sensors"` (list): A list of sensors that are attached to the
26+
rocket. The most recent measurements of the sensors are provided
27+
with the ``sensor.measurement`` attribute. The sensors are
28+
listed in the same order as they are added to the rocket.
29+
- `"environment"` (Environment): The current environment object
30+
assigned to the simulation.
31+
- `"rocket"` (Rocket): The current rocket object assigned to the
32+
simulation.
33+
- `"phase"` (FlightPhase): The current flight phase object.
34+
- `"phase_index"` (int): The index of the current flight phase.
35+
- `"node_index"` (int): The index of the current node in the
36+
current flight phase.
37+
- Any additional custom key-value pairs provided via the
38+
`event_context` parameter (see below).
39+
940
action : function
10-
A function that takes the current state of the simulation as input and performs some action when the event is triggered.
41+
A function that will be executed when the event is triggered. The
42+
function should be defined with the following signature:
43+
action(**kwargs), where kwargs is a dictionary containing the same
44+
keys as the trigger function. The action function can also modify
45+
the state of the simulation by returning a dictionary with the keys:
46+
- `"state"` (list): A new state vector to replace the current state
47+
vector. The structure of the state vector is the same as the
48+
one provided in the trigger function.
49+
- `"disable_event"` (bool): If True, the event will not be
50+
checked for triggering again after being triggered, making
51+
it a one-time event. Defaults to True.
52+
- `"new_events"` (list): A list of new Event objects to be added
53+
to the simulation when the event is triggered. This can be
54+
used to create events that spawn new events when they are
55+
triggered, such as a parachute deployment event that spawns
56+
a new event to check for the parachute deployment after a
57+
certain time delay.
58+
- `"remove_events"` (list): A list of Event objects to be
59+
removed from the simulation when the event is triggered. This
60+
can be used to create events that remove other events when
61+
they are triggered, such as a parachute deployment event that
62+
removes the apogee event when it is triggered.
63+
- Any other key-value pairs defined in `event_context` will
64+
also be included. These allow you to maintain custom state or
65+
counters across multiple trigger and action calls. Use cases
66+
include: tracking the number of times an event has been triggered
67+
(e.g., `{"trigger_count": 0}`), recording the time of the last
68+
trigger (e.g., `{"last_trigger_time": None}`), or any other
69+
custom data your trigger/action functions need to share state.
70+
71+
Example: If you initialize the event with
72+
`event_context={"trigger_count": 0}`, your trigger and action
73+
functions will receive `trigger_count=0` in their kwargs dict.
74+
You can then update this value in the action function by
75+
including it in the returned dictionary (e.g.,
76+
`{"trigger_count": 1}`), and it will be passed to subsequent
77+
trigger/action calls.
78+
1179
name : str
1280
A name for the event, used for identification purposes.
13-
81+
event_context : dict, optional
82+
A dictionary of custom key-value pairs that will be passed to the
83+
trigger and action functions. This allows you to initialize and
84+
maintain custom state that persists across multiple trigger/action
85+
calls. For example, `event_context={"trigger_count": 0,
86+
"last_trigger_time": None}` can be used to track event state.
87+
When the action function returns a dictionary with updated values
88+
(e.g., `{"trigger_count": 1}`), those values persist and are
89+
passed to subsequent calls. Defaults to an empty dictionary if not
90+
provided.
1491
"""
15-
self.trigger = trigger
16-
self.action = action
92+
self.trigger = self.__verify_trigger(trigger)
93+
self.action = self.__verify_action(action)
1794
self.name = name
95+
self.event_context = event_context if event_context is not None else {}
96+
97+
# TODO: implement tracking for whether this event is currently enabled
98+
# or disabled. The disable_event flag from the action return value should
99+
# control whether this event continues to be checked for triggering.
18100

19101
# TODO: check_trigger does note receive enough arguments to substitute parachute events
20-
def check_trigger(self, state):
21-
"""Checks if the event should be triggered based on the current state of the simulation.
102+
def __verify_trigger(self, trigger):
103+
"""Verifies that the trigger function is valid.
22104
23105
Parameters
24106
----------
25-
state : dict
26-
The current state of the simulation, containing information such as altitude, velocity, and time.
107+
trigger : function
108+
The trigger function to be verified.
27109
28110
Returns
29111
-------
30-
bool
31-
True if the event should be triggered, False otherwise.
112+
function
113+
The verified trigger function.
32114
115+
Raises
116+
------
117+
ValueError
118+
If the trigger function does not have the correct signature or does not return a boolean value.
33119
"""
34-
return self.trigger(state)
35-
36-
def execute_action(self, state):
37-
"""Executes the action associated with the event.
120+
# TODO: implement inspection of trigger function to verify:
121+
# 1. It accepts **kwargs (accepts arbitrary keyword arguments)
122+
# 2. Return type annotation is bool or can be tested to return bool
123+
# 3. Consider allowing signature to be flexible (accepts **kwargs)
124+
# to accommodate user-defined custom event_context keys
125+
return trigger
126+
127+
def __verify_action(self, action):
128+
"""Verifies that the action function is valid.
38129
39130
Parameters
40131
----------
41-
state : dict
42-
The current state of the simulation, containing information such as altitude, velocity, and time.
132+
action : function
133+
The action function to be verified.
134+
135+
Returns
136+
-------
137+
function
138+
The verified action function.
43139
140+
Raises
141+
------
142+
ValueError
143+
If the action function does not have the correct signature.
44144
"""
45-
return self.action(state)
145+
# TODO: implement inspection of action function to verify:
146+
# 1. It accepts **kwargs (accepts arbitrary keyword arguments)
147+
# 2. It can optionally return None or a dict with any of these keys:
148+
# - \"state\": list of floats
149+
# - \"disable_event\": bool
150+
# - \"new_events\": list of Event objects
151+
# - \"remove_events\": list of Event objects
152+
# - Any custom keys to update event_context
153+
# 3. Raise ValueError if signature doesn't match expectations
154+
return action
46155

47156
def __repr__(self):
48157
# TODO: Implement a more informative string representation of the Event object.
@@ -53,48 +162,61 @@ def __str__(self):
53162
pass
54163

55164
def __call__(self, *args, **kwds):
56-
# TODO: This should call the action (or the trigger?)
165+
# TODO: Implement the main event logic:
166+
# 1. Construct kwargs dict with:
167+
# - "time": current simulation time
168+
# - "state": current state vector [x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz]
169+
# - "state_dot": time derivative of state
170+
# - "sampling_rate": event sampling rate (None for every step)
171+
# - "sensors": list of sensor objects with latest measurements
172+
# - "environment": current Environment object
173+
# - "rocket": current Rocket object
174+
# - "phase": current FlightPhase object
175+
# - "phase_index": index of current flight phase
176+
# - "node_index": index of current node in phase
177+
# - All key-value pairs from self.event_context
178+
# 2. Call self.trigger(**kwargs) and check return value is boolean
179+
# 3. If trigger returns True:
180+
# a. Call self.action(**kwargs)
181+
# b. If action returns a dict, process return values:
182+
# - "state": update simulation state if provided
183+
# - "disable_event": set internal flag to disable this event from
184+
# being triggered again (default True)
185+
# - "new_events": add new Event objects to the simulation
186+
# - "remove_events": remove Event objects from the simulation
187+
# - Any other keys: update self.event_context for next trigger call
188+
# 4. Log trigger result and current simulation time for debugging
189+
# TODO: handle sampling_rate: if not None, only check trigger at
190+
# specified time intervals, not at every time step
57191
pass
58192

59193

60-
# TODO: Implement functions which are standard types of events, such as motor burnout events, landing events, etc.
61-
# def motor_burnout_trigger(state):
62-
# """A trigger function that returns
63-
64-
# # TODO: think about this
65-
# class ParachuteEvent(Event):
66-
67-
68-
# def __init__(self, trigger, action, name, parachute):
69-
# """Initializes a ParachuteEvent object.
70-
71-
# Parameters
72-
# ----------
73-
# trigger : function
74-
# A function that takes the current state of the simulation as input and returns a boolean value. The event will be triggered when this function returns True.
75-
# action : function
76-
# A function that takes the current state of the simulation as input and performs some action when the event is triggered.
77-
# name : str
78-
# A name for the event, used for identification purposes.
79-
# parachute : Parachute
80-
# The parachute associated with this event.
81-
82-
# """
83-
# super().__init__(trigger, action, name)
84-
# self.parachute = parachute
85-
86-
# def apogee_trigger(state):
87-
# """A trigger function that returns True when the rocket reaches apogee.
88-
89-
# Parameters
90-
# ----------
91-
# state : dict
92-
# The current state of the simulation, containing information such as altitude, velocity, and time.
93-
94-
# Returns
95-
# -------
96-
# bool
97-
# True if the rocket has reached apogee, False otherwise.
98194

99-
# """
100-
# return state['velocity'] <= 0
195+
# TODO: add a parameter to the Event class that specify whether the event should
196+
# be triggered only once, or if it can be triggered multiple times. Also, add a
197+
# way to stop the event from continuously triggering on command inside the action
198+
# function, such as a "disable" method that can be called inside the action
199+
# function to prevent the event from being triggered again.
200+
201+
# TODO: add a parameter to the Event class that specify whether the event should
202+
# be a discrete event, meaning that it should only be checked for triggering at
203+
# specific time intervals (e.g. every 0.1 seconds) instead of at every time step
204+
# of the simulation. This would be useful for parachute events. This should be
205+
# done by adding a "sampling_rate" parameter to the Event class, that is none by
206+
# default (meaning that the event is checked at every time step), but if it is
207+
# set to a float value, the event will only be checked for triggering at that
208+
# specific time interval. The flight class should be able to differentiate
209+
# between the discrete and continuous events (we will handle this later)
210+
211+
212+
# FOR STANO:
213+
# TODO: Implement Event orchestration at the Flight/Simulation level:
214+
# - Flight or an event manager class should maintain a list of active events
215+
# - At each simulation step, iterate through enabled events and call them with
216+
# the current simulation state (time, state, state_dot, sensors, etc.)
217+
# - Collect return values from events and apply state changes, add/remove events,
218+
# and update event_context values for subsequent calls
219+
# - Respect the disable_event flag and sampling_rate to control when events
220+
# are checked for triggering
221+
# - Handle the sampling_rate logic: only check events at the specified intervals,
222+
# not at every simulation time step

rocketpy/simulation/flight.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,10 +1030,9 @@ def __check_simulation_events(self, phase, phase_index, node_index):
10301030
"""
10311031
# TODO: make all these 3 events be handled with the Events class
10321032
# Check for first out of rail event
1033-
if len(self.out_of_rail_state) == 1 and self.out_of_rail_event.trigger(
1034-
self.y_sol
1035-
):
1036-
return self.out_of_rail_event.action(phase, phase_index, node_index)
1033+
if len(self.out_of_rail_state) == 1:
1034+
if self.out_of_rail_event.trigger(self.y_sol):
1035+
return self.out_of_rail_event.action(phase, phase_index, node_index)
10371036

10381037
# Check for apogee event
10391038
# TODO: negative vz doesn't really mean apogee. Improve this.

0 commit comments

Comments
 (0)