11import inspect
2- from typing import get_type_hints
32import warnings
3+ from typing import get_type_hints
4+
5+
46class Event :
57 # TODO: should "sensors" arg of the trigger function be a dictionary instead
6- # of a list? It would be more intuitive to access the sensors by name
8+ # of a list? It would be more intuitive to access the sensors by name
79 def __init__ (self , trigger , action , name , event_context = None ):
810 """Initializes an Event object.
911
1012 Parameters
1113 ----------
1214 trigger : function
13- A function that must return a boolean value. The event will be
14- triggered when this function returns True. The function should be
15+ A function that must return a boolean value. The event will be
16+ triggered when this function returns True. The function should be
1517 defined with the following signature: trigger(**kwargs), where
1618 kwargs is a dictionary containing the keys:
1719
1820 - `"time"` (float): The current simulation time in seconds.
1921 - `"state"` (list): The state vector of the simulation, structured
2022 as `[x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz]`.
21- - `"state_dot"` (list): The time derivative of the state vector,
23+ - `"state_dot"` (list): The time derivative of the state vector,
2224 structured as `[vx, vy, vz, ax, ay, az, e0_dot, e1_dot, e2_dot, e3_dot, wx_dot, wy_dot, wz_dot]`.
2325 - `"sampling_rate"` (float or None): The sampling rate of the
2426 event, in seconds. If None, the event will be checked for
2527 triggering at every time step of the simulation. If a float
26- value is provided, the event will only be checked for
28+ value is provided, the event will only be checked for
2729 triggering at that specific time interval.
28- - `"sensors"` (list): A list of sensors that are attached to the
30+ - `"sensors"` (list): A list of sensors that are attached to the
2931 rocket. The most recent measurements of the sensors are provided
3032 with the ``sensor.measurement`` attribute. The sensors are
3133 listed in the same order as they are added to the rocket.
@@ -37,59 +39,59 @@ def __init__(self, trigger, action, name, event_context=None):
3739 - `"phase_index"` (int): The index of the current flight phase.
3840 - `"node_index"` (int): The index of the current node in the
3941 current flight phase.
40- - Any additional custom key-value pairs provided via the
42+ - Any additional custom key-value pairs provided via the
4143 `event_context` parameter (see below).
42-
44+
4345 action : function
4446 A function that will be executed when the event is triggered. The
45- function should be defined with the following signature:
47+ function should be defined with the following signature:
4648 action(**kwargs), where kwargs is a dictionary containing the same
4749 keys as the trigger function. The action function can also modify
4850 the state of the simulation by returning a dictionary with the keys:
4951 - `"state"` (list): A new state vector to replace the current state
5052 vector. The structure of the state vector is the same as the
5153 one provided in the trigger function.
52- - `"disable_event"` (bool): If True, the event will not be
53- checked for triggering again after being triggered, making
54+ - `"disable_event"` (bool): If True, the event will not be
55+ checked for triggering again after being triggered, making
5456 it a one-time event. Defaults to True.
5557 - `"new_events"` (list): A list of new Event objects to be added
5658 to the simulation when the event is triggered. This can be
5759 used to create events that spawn new events when they are
5860 triggered, such as a parachute deployment event that spawns
5961 a new event to check for the parachute deployment after a
6062 certain time delay.
61- - `"remove_events"` (list): A list of Event objects to be
62- removed from the simulation when the event is triggered. This
63- can be used to create events that remove other events when
64- they are triggered, such as a parachute deployment event that
63+ - `"remove_events"` (list): A list of Event objects to be
64+ removed from the simulation when the event is triggered. This
65+ can be used to create events that remove other events when
66+ they are triggered, such as a parachute deployment event that
6567 removes the apogee event when it is triggered.
66- - Any other key-value pairs defined in `event_context` will
67- also be included. These allow you to maintain custom state or
68- counters across multiple trigger and action calls. Use cases
68+ - Any other key-value pairs defined in `event_context` will
69+ also be included. These allow you to maintain custom state or
70+ counters across multiple trigger and action calls. Use cases
6971 include: tracking the number of times an event has been triggered
70- (e.g., `{"trigger_count": 0}`), recording the time of the last
71- trigger (e.g., `{"last_trigger_time": None}`), or any other
72+ (e.g., `{"trigger_count": 0}`), recording the time of the last
73+ trigger (e.g., `{"last_trigger_time": None}`), or any other
7274 custom data your trigger/action functions need to share state.
73-
74- Example: If you initialize the event with
75- `event_context={"trigger_count": 0}`, your trigger and action
76- functions will receive `trigger_count=0` in their kwargs dict.
77- You can then update this value in the action function by
78- including it in the returned dictionary (e.g.,
79- `{"trigger_count": 1}`), and it will be passed to subsequent
75+
76+ Example: If you initialize the event with
77+ `event_context={"trigger_count": 0}`, your trigger and action
78+ functions will receive `trigger_count=0` in their kwargs dict.
79+ You can then update this value in the action function by
80+ including it in the returned dictionary (e.g.,
81+ `{"trigger_count": 1}`), and it will be passed to subsequent
8082 trigger/action calls.
8183
8284 name : str
8385 A name for the event, used for identification purposes.
8486 event_context : dict, optional
85- A dictionary of custom key-value pairs that will be passed to the
86- trigger and action functions. This allows you to initialize and
87- maintain custom state that persists across multiple trigger/action
88- calls. For example, `event_context={"trigger_count": 0,
89- "last_trigger_time": None}` can be used to track event state.
90- When the action function returns a dictionary with updated values
91- (e.g., `{"trigger_count": 1}`), those values persist and are
92- passed to subsequent calls. Defaults to an empty dictionary if not
87+ A dictionary of custom key-value pairs that will be passed to the
88+ trigger and action functions. This allows you to initialize and
89+ maintain custom state that persists across multiple trigger/action
90+ calls. For example, `event_context={"trigger_count": 0,
91+ "last_trigger_time": None}` can be used to track event state.
92+ When the action function returns a dictionary with updated values
93+ (e.g., `{"trigger_count": 1}`), those values persist and are
94+ passed to subsequent calls. Defaults to an empty dictionary if not
9395 provided.
9496 """
9597 self .name = name
@@ -125,31 +127,40 @@ def __verify_trigger(self, trigger):
125127 # 3. Consider allowing signature to be flexible (accepts **kwargs)
126128 # to accommodate user-defined custom event_context keys
127129 # verify if the return type is bool when annotated
128- return_annotation = get_type_hints (trigger ).get (' return' , None )
130+ return_annotation = get_type_hints (trigger ).get (" return" , None )
129131 if return_annotation is not None and return_annotation is not bool :
130- raise ValueError (f"Trigger function { self .name } must return a boolean value." )
132+ raise ValueError (
133+ f"Trigger function { self .name } must return a boolean value."
134+ )
131135 # verify if the trigger function accepts **kwargs and therefore can
132136 # receive standard event arguments plus custom event_context keys
133137 s = inspect .signature (trigger )
134- if not any (p .kind == inspect .Parameter .VAR_KEYWORD for p in s .parameters .values ()):
138+ if not any (
139+ p .kind == inspect .Parameter .VAR_KEYWORD for p in s .parameters .values ()
140+ ):
135141 raise ValueError (
136142 f"Trigger function { self .name } must accept **kwargs to receive event context "
137143 f"and simulation state."
138144 )
139- if any (p .kind == inspect .Parameter .POSITIONAL_ONLY for p in s .parameters .values ()):
145+ if any (
146+ p .kind == inspect .Parameter .POSITIONAL_ONLY for p in s .parameters .values ()
147+ ):
140148 raise ValueError (
141149 f"Trigger function { self .name } must accept keyword arguments; "
142150 "positional-only parameters are not supported."
143151 )
152+
144153 # Helper function to generate dummy values based on type annotations
145154 # of parameters, allowing to test the function without real values
146155 def _placeholder_for_parameter (parameter ):
147156 annotation = parameter .annotation
148157 if annotation is inspect .Parameter .empty :
149- warnings .warn (f"Trigger function { self .name } : Test with parameters skipped due "
150- f"to missing type annotation for parameter '{ parameter .name } '. \n "
158+ warnings .warn (
159+ f"Trigger function { self .name } : Test with parameters skipped due "
160+ f"to missing type annotation for parameter '{ parameter .name } '. \n "
151161 f"Is highly recommended that parameters have type annotations "
152- f"(var: type). Parameter '{ parameter .name } ' has no annotation." )
162+ f"(var: type). Parameter '{ parameter .name } ' has no annotation."
163+ )
153164 skip_test = True
154165 return None , skip_test
155166 if annotation in (int , float ):
@@ -164,6 +175,7 @@ def _placeholder_for_parameter(parameter):
164175 if origin in (list , tuple , set , dict ):
165176 return origin (), False
166177 return None , False
178+
167179 # Build a dictionary with dummy values to test if function accepts **kwargs
168180 # Include an unexpected argument to validate the function doesn't complain
169181 test_kwargs = {"unexpected_kwarg" : 123 }
@@ -178,10 +190,14 @@ def _placeholder_for_parameter(parameter):
178190 annotation = parameter .annotation
179191 if annotation in (list , tuple , set , dict ):
180192 skip_test = True
181- elif hasattr (annotation , "__origin__" ) and getattr (annotation , "__origin__" , None ) in (list , tuple , set , dict ):
193+ elif hasattr (annotation , "__origin__" ) and getattr (
194+ annotation , "__origin__" , None
195+ ) in (list , tuple , set , dict ):
182196 skip_test = True
183197 else :
184- test_kwargs [name ], skip_test = _placeholder_for_parameter (parameter )
198+ test_kwargs [name ], skip_test = _placeholder_for_parameter (
199+ parameter
200+ )
185201 # Execute the trigger function with test values to validate compatibility
186202 # If TypeError occurs, the function doesn't properly accept **kwargs
187203 if not skip_test :
@@ -274,21 +290,20 @@ def __call__(self, *args, **kwds):
274290 pass
275291
276292
277-
278293# TODO: add a parameter to the Event class that specify whether the event should
279- # be triggered only once, or if it can be triggered multiple times. Also, add a
294+ # be triggered only once, or if it can be triggered multiple times. Also, add a
280295# way to stop the event from continuously triggering on command inside the action
281- # function, such as a "disable" method that can be called inside the action
296+ # function, such as a "disable" method that can be called inside the action
282297# function to prevent the event from being triggered again.
283298
284299# TODO: add a parameter to the Event class that specify whether the event should
285300# be a discrete event, meaning that it should only be checked for triggering at
286301# specific time intervals (e.g. every 0.1 seconds) instead of at every time step
287- # of the simulation. This would be useful for parachute events. This should be
302+ # of the simulation. This would be useful for parachute events. This should be
288303# done by adding a "sampling_rate" parameter to the Event class, that is none by
289304# default (meaning that the event is checked at every time step), but if it is
290- # set to a float value, the event will only be checked for triggering at that
291- # specific time interval. The flight class should be able to differentiate
305+ # set to a float value, the event will only be checked for triggering at that
306+ # specific time interval. The flight class should be able to differentiate
292307# between the discrete and continuous events (we will handle this later)
293308
294309
@@ -302,4 +317,4 @@ def __call__(self, *args, **kwds):
302317# - Respect the disable_event flag and sampling_rate to control when events
303318# are checked for triggering
304319# - Handle the sampling_rate logic: only check events at the specified intervals,
305- # not at every simulation time step
320+ # not at every simulation time step
0 commit comments