diff --git a/rocketpy/rocket/parachute.py b/rocketpy/rocket/parachute.py index f0bf86f67..b068a1d75 100644 --- a/rocketpy/rocket/parachute.py +++ b/rocketpy/rocket/parachute.py @@ -47,14 +47,21 @@ class Parachute: - The string "apogee" which triggers the parachute at apogee, i.e., when the rocket reaches its highest point and starts descending. + - The string "launch + X" where X is a number in seconds. The parachute + will be ejected X seconds after launch (t=0). This is useful for + simulating delay charges that activate at a fixed time from launch. + + - The string "burnout + X" where X is a number in seconds. The parachute + will be ejected X seconds after motor burnout. This is useful for + simulating delay charges in motors with delay elements. Parachute.triggerfunc : function Trigger function created from the trigger used to evaluate the trigger condition for the parachute ejection system. It is a callable function - that takes three arguments: Freestream pressure in Pa, Height above - ground level in meters, and the state vector of the simulation. The - returns ``True`` if the parachute ejection system should be triggered - and ``False`` otherwise. + that takes six arguments: Freestream pressure in Pa, Height above + ground level in meters, the state vector of the simulation, sensors + list, current time t, and rocket object. It returns ``True`` if the + parachute ejection system should be triggered and ``False`` otherwise. .. note: @@ -153,7 +160,14 @@ def __init__( height above ground level. - The string "apogee" which triggers the parachute at apogee, i.e., \ when the rocket reaches its highest point and starts descending. - + - The string "launch + X" where X is the delay in seconds from launch. \ + For example, "launch + 5" triggers 5 seconds after launch. This is \ + useful for simulating delay charges that activate at a fixed time \ + from launch. + - The string "burnout + X" where X is the delay in seconds from motor \ + burnout. For example, "burnout + 3.5" triggers 3.5 seconds after \ + motor burnout. This is useful for simulating delay charges in motors \ + with delay elements. .. note:: The function will be called according to the sampling rate specified. @@ -232,14 +246,18 @@ def __evaluate_trigger_function(self, trigger): sig = signature(triggerfunc) if len(sig.parameters) == 3: - def triggerfunc(p, h, y, sensors): + def triggerfunc(p, h, y, sensors, t=None, rocket=None): return trigger(p, h, y) + elif len(sig.parameters) == 4: + + def triggerfunc(p, h, y, sensors, t=None, rocket=None): + return trigger(p, h, y, sensors) self.triggerfunc = triggerfunc elif isinstance(trigger, (int, float)): # The parachute is deployed at a given height - def triggerfunc(p, h, y, sensors): # pylint: disable=unused-argument + def triggerfunc(p, h, y, sensors, t=None, rocket=None): # pylint: disable=unused-argument # p = pressure considering parachute noise signal # h = height above ground level considering parachute noise signal # y = [x, y, z, vx, vy, vz, e0, e1, e2, e3, w1, w2, w3] @@ -247,20 +265,74 @@ def triggerfunc(p, h, y, sensors): # pylint: disable=unused-argument self.triggerfunc = triggerfunc - elif trigger.lower() == "apogee": - # The parachute is deployed at apogee - def triggerfunc(p, h, y, sensors): # pylint: disable=unused-argument - # p = pressure considering parachute noise signal - # h = height above ground level considering parachute noise signal - # y = [x, y, z, vx, vy, vz, e0, e1, e2, e3, w1, w2, w3] - return y[5] < 0 + elif isinstance(trigger, str): + trigger_lower = trigger.lower().strip() + + if trigger_lower == "apogee": + # The parachute is deployed at apogee + def triggerfunc(p, h, y, sensors, t=None, rocket=None): # pylint: disable=unused-argument + # p = pressure considering parachute noise signal + # h = height above ground level considering parachute noise signal + # y = [x, y, z, vx, vy, vz, e0, e1, e2, e3, w1, w2, w3] + return y[5] < 0 + + self.triggerfunc = triggerfunc + + elif "+" in trigger_lower: + # Time-based trigger: "launch + X" or "burnout + X" + parts = trigger_lower.split("+") + if len(parts) != 2: + raise ValueError( + f"Invalid time-based trigger format for parachute '{self.name}'. " + + "Expected format: 'launch + delay' or 'burnout + delay' " + + "where delay is a number in seconds." + ) + + event = parts[0].strip() + try: + delay = float(parts[1].strip()) + except ValueError: + raise ValueError( + f"Invalid delay value in trigger '{trigger}' for parachute '{self.name}'. " + + "Delay must be a number in seconds." + ) + + if event == "launch": + # Deploy at launch time + delay + def triggerfunc(p, h, y, sensors, t=None, rocket=None): # pylint: disable=unused-argument + if t is None: + return False + return t >= delay + + self.triggerfunc = triggerfunc + + elif event == "burnout": + # Deploy at motor burnout time + delay + def triggerfunc(p, h, y, sensors, t=None, rocket=None): # pylint: disable=unused-argument + if t is None or rocket is None: + return False + burnout_time = rocket.motor.burn_out_time + return t >= burnout_time + delay + + self.triggerfunc = triggerfunc + + else: + raise ValueError( + f"Invalid time-based trigger event '{event}' for parachute '{self.name}'. " + + "Supported events are 'launch' and 'burnout'." + ) - self.triggerfunc = triggerfunc + else: + raise ValueError( + f"Unable to set the trigger function for parachute '{self.name}'. " + + "Trigger string must be 'apogee', 'launch + ', or 'burnout + '. " + + "See the Parachute class documentation for more information." + ) else: raise ValueError( f"Unable to set the trigger function for parachute '{self.name}'. " - + "Trigger must be a callable, a float value or the string 'apogee'. " + + "Trigger must be a callable, a float value or a string. " + "See the Parachute class documentation for more information." ) diff --git a/rocketpy/simulation/flight.py b/rocketpy/simulation/flight.py index a139a84ee..a3c04238f 100644 --- a/rocketpy/simulation/flight.py +++ b/rocketpy/simulation/flight.py @@ -731,6 +731,8 @@ def __simulate(self, verbose): height_above_ground_level, self.y_sol, self.sensors, + node.t, + self.rocket, ): # Remove parachute from flight parachutes self.parachutes.remove(parachute) @@ -800,6 +802,125 @@ def __simulate(self, verbose): if self.__check_simulation_events(phase, phase_index, node_index): break # Stop if simulation termination event occurred + # List and feed overshootable time nodes + if self.time_overshoot: + # Initialize phase overshootable time nodes + overshootable_nodes = self.TimeNodes() + # Add overshootable parachute time nodes + overshootable_nodes.add_parachutes( + self.parachutes, self.solution[-2][0], self.t + ) + # Add last time node (always skipped) + overshootable_nodes.add_node(self.t, [], [], []) + if len(overshootable_nodes) > 1: + # Sort and merge equal overshootable time nodes + overshootable_nodes.sort() + overshootable_nodes.merge() + # Clear if necessary + if overshootable_nodes[0].t == phase.t and phase.clear: + overshootable_nodes[0].parachutes = [] + overshootable_nodes[0].callbacks = [] + # Feed overshootable time nodes trigger + interpolator = phase.solver.dense_output() + for ( + overshootable_index, + overshootable_node, + ) in self.time_iterator(overshootable_nodes): + # Calculate state at node time + overshootable_node.y_sol = interpolator( + overshootable_node.t + ) + for parachute in overshootable_node.parachutes: + # Calculate and save pressure signal + ( + noisy_pressure, + height_above_ground_level, + ) = self.__calculate_and_save_pressure_signals( + parachute, + overshootable_node.t, + overshootable_node.y_sol[2], + ) + + # Check for parachute trigger + if parachute.triggerfunc( + noisy_pressure, + height_above_ground_level, + overshootable_node.y_sol, + self.sensors, + overshootable_node.t, + self.rocket, + ): + # Remove parachute from flight parachutes + self.parachutes.remove(parachute) + # Create phase for time after detection and + # before inflation + # Must only be created if parachute has any lag + i = 1 + if parachute.lag != 0: + self.flight_phases.add_phase( + overshootable_node.t, + phase.derivative, + clear=True, + index=phase_index + i, + ) + i += 1 + # Create flight phase for time after inflation + callbacks = [ + lambda self, + parachute_cd_s=parachute.cd_s: setattr( + self, "parachute_cd_s", parachute_cd_s + ), + lambda self, + parachute_radius=parachute.radius: setattr( + self, + "parachute_radius", + parachute_radius, + ), + lambda self, + parachute_height=parachute.height: setattr( + self, + "parachute_height", + parachute_height, + ), + lambda self, + parachute_porosity=parachute.porosity: setattr( + self, + "parachute_porosity", + parachute_porosity, + ), + lambda self, + added_mass_coefficient=parachute.added_mass_coefficient: setattr( + self, + "parachute_added_mass_coefficient", + added_mass_coefficient, + ), + ] + self.flight_phases.add_phase( + overshootable_node.t + parachute.lag, + self.u_dot_parachute, + callbacks, + clear=False, + index=phase_index + i, + ) + # Rollback history + self.t = overshootable_node.t + self.y_sol = overshootable_node.y_sol + self.solution[-1] = [ + overshootable_node.t, + *overshootable_node.y_sol, + ] + # Prepare to leave loops and start new flight phase + overshootable_nodes.flush_after( + overshootable_index + ) + phase.time_nodes.flush_after(node_index) + phase.time_nodes.add_node(self.t, [], [], []) + phase.solver.status = "finished" + # Save parachute event + self.parachute_events.append( + [self.t, parachute] + ) + # Process overshootable time nodes if enabled if self.time_overshoot and self.__process_overshootable_nodes( phase, phase_index, node_index @@ -946,6 +1067,8 @@ def __check_and_handle_parachute_triggers( height_above_ground_level, self.y_sol, self.sensors, + node.t, + self.rocket, ): continue # Check next parachute @@ -1355,6 +1478,8 @@ def __check_overshootable_parachute_triggers( height_above_ground_level, overshootable_node.y_sol, self.sensors, + overshootable_node.t, + self.rocket, ): continue # Check next parachute diff --git a/tests/integration/test_parachute_time_trig.py b/tests/integration/test_parachute_time_trig.py new file mode 100644 index 000000000..0a59d80fc --- /dev/null +++ b/tests/integration/test_parachute_time_trig.py @@ -0,0 +1,202 @@ +"""Integration tests for time-based parachute triggers in flight simulations.""" + +import pytest + +from rocketpy import Flight + + +@pytest.mark.slow +def test_flight_with_launch_plus_delay_trigger( + example_spaceport_env, calisto_motorless, cesaroni_m1670 +): + """Test a complete flight simulation with a 'launch + X' parachute trigger. + + This simulates a rocket with a delay charge that deploys the parachute at a + fixed time after launch, similar to model rockets without avionics. + """ + # Use existing rocket and add motor + rocket = calisto_motorless + rocket.add_motor(cesaroni_m1670, position=-1.373) + + # Add a parachute with "launch + 5" trigger (5 seconds after launch) + rocket.add_parachute( + name="Main", + cd_s=10.0, + trigger="launch + 5", + sampling_rate=100, + lag=1.5, + noise=(0, 8.3, 0.5), + ) + + # Run flight simulation + flight = Flight( + environment=example_spaceport_env, + rocket=rocket, + rail_length=5.2, + inclination=85, + heading=0, + terminate_on_apogee=False, + ) + + # Verify that the parachute was deployed at approximately the right time + # The parachute should deploy at t=5s + lag=1.5s = 6.5s (fully deployed) + assert flight.t is not None + + # Check that parachute deployment happened (should have parachute_cd_s set) + # This attribute is set when parachute is deployed + assert hasattr(flight, "parachute_cd_s") + + # Verify simulation completed successfully + assert flight.apogee_time > 0 + + +@pytest.mark.slow +def test_flight_with_burnout_plus_delay_trigger( + example_spaceport_env, calisto_motorless, cesaroni_m1670 +): + """Test a complete flight simulation with a 'burnout + X' parachute trigger. + + This simulates a rocket with a motor delay charge that deploys the parachute + at a fixed time after motor burnout, typical of model rocket motors. + """ + # Use existing rocket and add motor + rocket = calisto_motorless + rocket.add_motor(cesaroni_m1670, position=-1.373) + + # Get motor burnout time + motor_burnout = rocket.motor.burn_out_time + + # Add a parachute with "burnout + 3" trigger (3 seconds after burnout) + rocket.add_parachute( + name="Main", + cd_s=10.0, + trigger="burnout + 3", + sampling_rate=100, + lag=1.5, + noise=(0, 8.3, 0.5), + ) + + # Run flight simulation + flight = Flight( + environment=example_spaceport_env, + rocket=rocket, + rail_length=5.2, + inclination=85, + heading=0, + terminate_on_apogee=False, + ) + + # Verify that the parachute was deployed + assert flight.t is not None + + # Check that parachute deployment happened + assert hasattr(flight, "parachute_cd_s") + + # The parachute should deploy after motor burnout + delay + # Expected deployment at burnout_time + 3s + lag + expected_deploy_time = motor_burnout + 3.0 + 1.5 + + # Verify simulation completed successfully and parachute deployed after burnout + assert flight.apogee_time > 0 + # The simulation should run past the expected deployment time + assert flight.t_final >= expected_deploy_time + + +@pytest.mark.slow +def test_flight_with_multiple_time_based_parachutes( + example_spaceport_env, calisto_motorless, cesaroni_m1670 +): + """Test a flight with multiple time-based parachutes (drogue and main). + + This simulates a dual-deployment system using time-based triggers. + """ + # Use existing rocket and add motor + rocket = calisto_motorless + rocket.add_motor(cesaroni_m1670, position=-1.373) + + # Add drogue parachute - deploys at burnout + 2 seconds + rocket.add_parachute( + name="Drogue", + cd_s=1.0, + trigger="burnout + 2", + sampling_rate=100, + lag=0.5, + noise=(0, 8.3, 0.5), + ) + + # Add main parachute - deploys at burnout + 10 seconds + rocket.add_parachute( + name="Main", + cd_s=10.0, + trigger="burnout + 10", + sampling_rate=100, + lag=1.0, + noise=(0, 8.3, 0.5), + ) + + # Run flight simulation + flight = Flight( + environment=example_spaceport_env, + rocket=rocket, + rail_length=5.2, + inclination=85, + heading=0, + terminate_on_apogee=False, + ) + + # Verify simulation completed successfully + assert flight.t is not None + assert flight.apogee_time > 0 + + # Check that parachute deployment happened + assert hasattr(flight, "parachute_cd_s") + + +@pytest.mark.slow +def test_flight_with_mixed_trigger_types( + example_spaceport_env, calisto_motorless, cesaroni_m1670 +): + """Test a flight with both time-based and traditional parachute triggers. + + This ensures backward compatibility when mixing trigger types. + """ + # Use existing rocket and add motor + rocket = calisto_motorless + rocket.add_motor(cesaroni_m1670, position=-1.373) + + # Add drogue parachute with traditional apogee trigger + rocket.add_parachute( + name="Drogue", + cd_s=1.0, + trigger="apogee", + sampling_rate=100, + lag=0.5, + noise=(0, 8.3, 0.5), + ) + + # Add main parachute with altitude trigger + rocket.add_parachute( + name="Main", + cd_s=10.0, + trigger=800.0, # 800 meters AGL + sampling_rate=100, + lag=1.0, + noise=(0, 8.3, 0.5), + ) + + # Run flight simulation + flight = Flight( + environment=example_spaceport_env, + rocket=rocket, + rail_length=5.2, + inclination=85, + heading=0, + terminate_on_apogee=False, + ) + + # Verify simulation completed successfully + assert flight.t is not None + assert flight.apogee_time > 0 + + # Check that parachute deployment happened + assert hasattr(flight, "parachute_cd_s") diff --git a/tests/unit/rocket/test_parachute.py b/tests/unit/rocket/test_parachute.py new file mode 100644 index 000000000..38118e0fa --- /dev/null +++ b/tests/unit/rocket/test_parachute.py @@ -0,0 +1,288 @@ +"""Unit tests for the Parachute class, specifically focusing on trigger mechanisms.""" + +import pytest + +from rocketpy import Parachute + + +class TestParachuteTriggers: + """Test class for parachute trigger functionality.""" + + def test_apogee_trigger(self): + """Test that the 'apogee' trigger is correctly parsed and works.""" + parachute = Parachute( + name="test_apogee", + cd_s=1.0, + trigger="apogee", + sampling_rate=100, + lag=0, + ) + + # Test trigger function with descending velocity (should trigger) + state_descending = [0, 0, 1000, 0, 0, -10, 1, 0, 0, 0, 0, 0, 0] + assert ( + parachute.triggerfunc(101325, 1000, state_descending, [], 5.0, None) is True + ) + + # Test trigger function with ascending velocity (should not trigger) + state_ascending = [0, 0, 1000, 0, 0, 10, 1, 0, 0, 0, 0, 0, 0] + assert ( + parachute.triggerfunc(101325, 1000, state_ascending, [], 3.0, None) is False + ) + + def test_altitude_trigger(self): + """Test that altitude-based trigger works correctly.""" + parachute = Parachute( + name="test_altitude", + cd_s=1.0, + trigger=500.0, # 500 meters + sampling_rate=100, + lag=0, + ) + + # Test at altitude above trigger point with descending velocity (should not trigger) + state_above = [0, 0, 1000, 0, 0, -10, 1, 0, 0, 0, 0, 0, 0] + assert parachute.triggerfunc(101325, 600, state_above, [], 10.0, None) is False + + # Test at altitude below trigger point with descending velocity (should trigger) + state_below = [0, 0, 1000, 0, 0, -10, 1, 0, 0, 0, 0, 0, 0] + assert parachute.triggerfunc(101325, 400, state_below, [], 15.0, None) is True + + # Test at altitude below trigger point with ascending velocity (should not trigger) + state_ascending = [0, 0, 1000, 0, 0, 10, 1, 0, 0, 0, 0, 0, 0] + assert ( + parachute.triggerfunc(101325, 400, state_ascending, [], 2.0, None) is False + ) + + def test_launch_plus_delay_trigger_parsing(self): + """Test that 'launch + X' trigger string is correctly parsed.""" + parachute = Parachute( + name="test_launch_delay", + cd_s=1.0, + trigger="launch + 5", + sampling_rate=100, + lag=0, + ) + + # Check that the parachute was created successfully + assert parachute.name == "test_launch_delay" + assert parachute.trigger == "launch + 5" + + def test_launch_plus_delay_trigger_functionality(self): + """Test that 'launch + X' trigger works correctly.""" + delay = 5.0 + parachute = Parachute( + name="test_launch_delay", + cd_s=1.0, + trigger=f"launch + {delay}", + sampling_rate=100, + lag=0, + ) + + state = [0, 0, 1000, 0, 0, -10, 1, 0, 0, 0, 0, 0, 0] + + # Before delay time - should not trigger + assert parachute.triggerfunc(101325, 1000, state, [], 3.0, None) is False + + # Exactly at delay time - should trigger + assert parachute.triggerfunc(101325, 1000, state, [], 5.0, None) is True + + # After delay time - should trigger + assert parachute.triggerfunc(101325, 1000, state, [], 7.0, None) is True + + def test_burnout_plus_delay_trigger_parsing(self): + """Test that 'burnout + X' trigger string is correctly parsed.""" + parachute = Parachute( + name="test_burnout_delay", + cd_s=1.0, + trigger="burnout + 3.5", + sampling_rate=100, + lag=0, + ) + + # Check that the parachute was created successfully + assert parachute.name == "test_burnout_delay" + assert parachute.trigger == "burnout + 3.5" + + def test_burnout_plus_delay_trigger_functionality(self): + """Test that 'burnout + X' trigger works correctly.""" + + # Create a mock rocket with motor + class MockMotor: + def __init__(self, burn_out_time): + self.burn_out_time = burn_out_time + + class MockRocket: + def __init__(self, burn_out_time): + self.motor = MockMotor(burn_out_time) + + delay = 3.5 + burnout_time = 2.0 + parachute = Parachute( + name="test_burnout_delay", + cd_s=1.0, + trigger=f"burnout + {delay}", + sampling_rate=100, + lag=0, + ) + + rocket = MockRocket(burnout_time) + state = [0, 0, 1000, 0, 0, -10, 1, 0, 0, 0, 0, 0, 0] + + # Before burnout + delay - should not trigger + assert parachute.triggerfunc(101325, 1000, state, [], 4.0, rocket) is False + + # Exactly at burnout + delay - should trigger + assert parachute.triggerfunc(101325, 1000, state, [], 5.5, rocket) is True + + # After burnout + delay - should trigger + assert parachute.triggerfunc(101325, 1000, state, [], 10.0, rocket) is True + + def test_launch_trigger_with_whitespace(self): + """Test that whitespace in trigger string is handled correctly.""" + parachute1 = Parachute( + name="test1", + cd_s=1.0, + trigger="launch + 5", + sampling_rate=100, + lag=0, + ) + + parachute2 = Parachute( + name="test2", + cd_s=1.0, + trigger=" launch + 5 ", + sampling_rate=100, + lag=0, + ) + + parachute3 = Parachute( + name="test3", + cd_s=1.0, + trigger="LAUNCH + 5", + sampling_rate=100, + lag=0, + ) + + state = [0, 0, 1000, 0, 0, -10, 1, 0, 0, 0, 0, 0, 0] + + # All should behave the same way + assert parachute1.triggerfunc(101325, 1000, state, [], 6.0, None) is True + assert parachute2.triggerfunc(101325, 1000, state, [], 6.0, None) is True + assert parachute3.triggerfunc(101325, 1000, state, [], 6.0, None) is True + + def test_invalid_trigger_format(self): + """Test that invalid trigger formats raise appropriate errors.""" + # Invalid string without '+' + with pytest.raises(ValueError, match="Unable to set the trigger function"): + Parachute( + name="test", + cd_s=1.0, + trigger="invalid_trigger", + sampling_rate=100, + lag=0, + ) + + # Invalid event name + with pytest.raises(ValueError, match="Invalid time-based trigger event"): + Parachute( + name="test", + cd_s=1.0, + trigger="invalid_event + 5", + sampling_rate=100, + lag=0, + ) + + # Invalid delay value (not a number) + with pytest.raises(ValueError, match="Invalid delay value"): + Parachute( + name="test", + cd_s=1.0, + trigger="launch + not_a_number", + sampling_rate=100, + lag=0, + ) + + # Invalid format (multiple '+') + with pytest.raises(ValueError, match="Invalid time-based trigger format"): + Parachute( + name="test", + cd_s=1.0, + trigger="launch + 5 + 3", + sampling_rate=100, + lag=0, + ) + + def test_decimal_delay_values(self): + """Test that decimal delay values work correctly.""" + parachute = Parachute( + name="test_decimal", + cd_s=1.0, + trigger="launch + 2.75", + sampling_rate=100, + lag=0, + ) + + state = [0, 0, 1000, 0, 0, -10, 1, 0, 0, 0, 0, 0, 0] + + # Just before delay - should not trigger + assert parachute.triggerfunc(101325, 1000, state, [], 2.74, None) is False + + # At and after delay - should trigger + assert parachute.triggerfunc(101325, 1000, state, [], 2.75, None) is True + assert parachute.triggerfunc(101325, 1000, state, [], 3.0, None) is True + + def test_custom_callable_trigger_backward_compatibility(self): + """Test that custom callable triggers still work with backward compatibility.""" + + # 3-parameter trigger (old style) + def old_trigger(p, h, y): + return y[5] < 0 and h < 800 + + parachute_old = Parachute( + name="test_old", + cd_s=1.0, + trigger=old_trigger, + sampling_rate=100, + lag=0, + ) + + # 4-parameter trigger (with sensors) + def new_trigger(p, h, y, sensors): + return y[5] < 0 and h < 800 + + parachute_new = Parachute( + name="test_new", + cd_s=1.0, + trigger=new_trigger, + sampling_rate=100, + lag=0, + ) + + state = [0, 0, 1000, 0, 0, -10, 1, 0, 0, 0, 0, 0, 0] + + # Both should work with the new 6-parameter signature + assert parachute_old.triggerfunc(101325, 700, state, [], 10.0, None) is True + assert parachute_new.triggerfunc(101325, 700, state, [], 10.0, None) is True + + # Should not trigger above altitude + assert parachute_old.triggerfunc(101325, 900, state, [], 10.0, None) is False + assert parachute_new.triggerfunc(101325, 900, state, [], 10.0, None) is False + + def test_zero_delay_trigger(self): + """Test that zero delay triggers work correctly.""" + parachute = Parachute( + name="test_zero", + cd_s=1.0, + trigger="launch + 0", + sampling_rate=100, + lag=0, + ) + + state = [0, 0, 1000, 0, 0, -10, 1, 0, 0, 0, 0, 0, 0] + + # Should trigger immediately at t=0 + assert parachute.triggerfunc(101325, 1000, state, [], 0.0, None) is True + + # Should also trigger at any positive time + assert parachute.triggerfunc(101325, 1000, state, [], 0.1, None) is True