diff --git a/rocketpy/simulation/flight.py b/rocketpy/simulation/flight.py index 55ca3486f..cc804d0e1 100644 --- a/rocketpy/simulation/flight.py +++ b/rocketpy/simulation/flight.py @@ -1225,6 +1225,8 @@ def __handle_impact_event(self, phase, phase_index, node_index): ] if len(valid_t_root) > 1: # pragma: no cover raise ValueError("Multiple roots found when solving for impact time.") + if len(valid_t_root) == 0: + raise ValueError("No valid roots found when solving for impact time.") # Determine impact state at t_root self.t = self.t_final = valid_t_root[0] + self.solution[-2][0] interpolator = phase.solver.dense_output() diff --git a/tests/unit/simulation/test_flight.py b/tests/unit/simulation/test_flight.py index 9a3c54477..391d89411 100644 --- a/tests/unit/simulation/test_flight.py +++ b/tests/unit/simulation/test_flight.py @@ -1,6 +1,7 @@ import json import os -from unittest.mock import patch +from types import SimpleNamespace +from unittest.mock import MagicMock, patch import matplotlib as plt import numpy as np @@ -84,6 +85,65 @@ def compute_static_margin_error_given_distance(position, static_margin, rocket): # Tests +def _make_impact_event_state(): + flight = object.__new__(Flight) + flight.env = SimpleNamespace(elevation=0) + flight.solution = [ + [10.0, 0, 0, 1, 0, 0, -1], + [11.0, 0, 0, -1, 0, 0, -1], + ] + flight.flight_phases = SimpleNamespace( + flush_after=MagicMock(), add_phase=MagicMock() + ) + + solver = SimpleNamespace( + step_size=1.0, + dense_output=lambda: lambda _: np.array([2, 3, 0, 4, 5, -6]), + status="running", + ) + time_nodes = SimpleNamespace(flush_after=MagicMock(), add_node=MagicMock()) + phase = SimpleNamespace(solver=solver, time_nodes=time_nodes) + return flight, phase + + +@pytest.mark.parametrize( + "roots, match", + [ + ([-1 + 0j, 2 + 0j], "No valid roots found"), + ([0.25 + 0j, 0.75 + 0j], "Multiple roots found"), + ], +) +def test_handle_impact_event_reports_invalid_root_counts(roots, match): + flight, phase = _make_impact_event_state() + + with patch( + "rocketpy.simulation.flight.find_roots_cubic_function", return_value=roots + ): + with pytest.raises(ValueError, match=match): + flight._Flight__handle_impact_event(phase, phase_index=1, node_index=2) + + +def test_handle_impact_event_uses_single_valid_root(): + flight, phase = _make_impact_event_state() + + with patch( + "rocketpy.simulation.flight.find_roots_cubic_function", + return_value=[0.5 + 0j], + ): + handled = flight._Flight__handle_impact_event( + phase, phase_index=1, node_index=2 + ) + + assert handled is True + assert flight.t == flight.t_final == pytest.approx(10.5) + assert flight.impact_velocity == -6 + assert phase.solver.status == "finished" + flight.flight_phases.flush_after.assert_called_once_with(1) + flight.flight_phases.add_phase.assert_called_once_with(10.5) + phase.time_nodes.flush_after.assert_called_once_with(2) + phase.time_nodes.add_node.assert_called_once_with(10.5, [], [], []) + + def test_get_solution_at_time(flight_calisto): """Test the get_solution_at_time method of the Flight class. This test simply calls the method at the initial and final time and checks if the