from __future__ import annotations import unittest from app.simulation.components.amesim.mechanical.translational import ( AmesimF000, AmesimForc, AmesimMecmas21, ) from app.simulation.components.amesim.signals.sources import AmesimStep0 from app.simulation.core.medium import IdealGasMedium from app.simulation.solvers.solver import SolveIVPConfig from app.simulation.systems.generic import ( SimulationSampleTimeError, simulation_preparation_issues, simulation_sample_times, ) from app.simulation.systems.network import SimulationNetwork from app.system_xml import validate_system_xml_document def _system_xml( *, t_start: str, t_stop: str, sample_step: str, ) -> str: return f""" """ def _fanout_mechanical_network( *, second_island_has_storage: bool, ) -> SimulationNetwork: medium = IdealGasMedium() network = SimulationNetwork("signal-fanout-physical-islands") command = AmesimStep0( "command", medium, initial=0.0, final=1.0, time=0.0, ) force_a = AmesimForc("force_a") mass_a = AmesimMecmas21("mass_a", medium) zero_a = AmesimF000("zero_a") force_b = AmesimForc("force_b") zero_b = AmesimF000("zero_b") components = [command, force_a, mass_a, zero_a, force_b, zero_b] mass_b = None if second_island_has_storage: mass_b = AmesimMecmas21("mass_b", medium) components.append(mass_b) for component in components: network.add_component(component) network.connect("command", "out", "force_a", "res") network.connect("command", "out", "force_b", "res") network.connect("force_a", "port_2", "mass_a", "port_1") network.connect("mass_a", "port_2", "zero_a", "port_1") if mass_b is None: network.connect("force_b", "port_2", "zero_b", "port_1") else: network.connect("force_b", "port_2", "mass_b", "port_1") network.connect("mass_b", "port_2", "zero_b", "port_1") return network class SimulationSampleTimeSafetyTests(unittest.TestCase): def test_grid_contains_both_endpoints_and_is_strictly_increasing(self) -> None: times = simulation_sample_times( SolveIVPConfig(t_start=1.0, t_stop=2.0), 0.3, ) self.assertEqual(times[0], 1.0) self.assertEqual(times[-1], 2.0) self.assertGreaterEqual(len(times), 2) self.assertTrue( all(first < second for first, second in zip(times, times[1:])) ) def test_step_larger_than_duration_still_returns_two_endpoints(self) -> None: self.assertEqual( simulation_sample_times( SolveIVPConfig(t_start=1.0, t_stop=1.25), 10.0, ), [1.0, 1.25], ) def test_grid_can_exceed_the_legacy_point_limit(self) -> None: times = simulation_sample_times( SolveIVPConfig(t_start=0.0, t_stop=5.0), 0.0001, ) self.assertEqual(len(times), 50001) self.assertEqual(times[0], 0.0) self.assertEqual(times[-1], 5.0) def test_system_xml_accepts_more_than_the_legacy_point_limit(self) -> None: report = validate_system_xml_document( _system_xml(t_start="0", t_stop="5", sample_step="0.0001") ) self.assertTrue(report.valid, report.issues) def test_tiny_step_is_rejected_before_an_oversized_grid_is_allocated(self) -> None: with self.assertRaises(SimulationSampleTimeError) as caught: simulation_sample_times( SolveIVPConfig(t_start=0.0, t_stop=1.0), 1.0e-300, ) self.assertEqual( caught.exception.code, "SIMULATION_SAMPLE_COUNT_UNREPRESENTABLE", ) def test_non_finite_derived_duration_has_a_stable_error(self) -> None: with self.assertRaises(SimulationSampleTimeError) as caught: simulation_sample_times( SolveIVPConfig(t_start=-1.0e308, t_stop=1.0e308), 1.0e308, ) self.assertEqual(caught.exception.code, "SIMULATION_TIME_SPAN_NOT_FINITE") def test_step_that_cannot_advance_absolute_time_is_rejected(self) -> None: with self.assertRaises(SimulationSampleTimeError) as caught: simulation_sample_times( SolveIVPConfig(t_start=1.0e16, t_stop=1.0e16 + 4.0), 1.0, ) self.assertEqual( caught.exception.code, "SIMULATION_SAMPLE_TIME_UNREPRESENTABLE", ) def test_system_xml_reports_sample_grid_failures_during_semantic_validation( self, ) -> None: cases = ( ( _system_xml(t_start="0", t_stop="1", sample_step="1e-300"), "SIMULATION_SAMPLE_COUNT_UNREPRESENTABLE", ), ( _system_xml( t_start="-1e308", t_stop="1e308", sample_step="1e308", ), "SIMULATION_TIME_SPAN_NOT_FINITE", ), ( _system_xml( t_start="10000000000000000", t_stop="10000000000000004", sample_step="1", ), "SIMULATION_SAMPLE_TIME_UNREPRESENTABLE", ), ) for xml, expected_code in cases: with self.subTest(expected_code=expected_code): report = validate_system_xml_document(xml) self.assertFalse(report.valid) self.assertIn( expected_code, {issue.code for issue in report.issues}, ) class PhysicalIslandSafetyTests(unittest.TestCase): def test_signal_fanout_does_not_hide_an_unanchored_physical_island(self) -> None: issues = simulation_preparation_issues( _fanout_mechanical_network(second_island_has_storage=False) ) island_issues = [ issue for issue in issues if issue.code == "ALGEBRAIC_ISLAND_HAS_NO_STORAGE" ] self.assertEqual(len(island_issues), 1) self.assertIn("force_b", island_issues[0].message) self.assertIn("zero_b", island_issues[0].message) self.assertNotIn("command", island_issues[0].message) self.assertNotIn("mass_a", island_issues[0].message) def test_signal_fanout_between_two_anchored_physical_islands_is_allowed( self, ) -> None: issues = simulation_preparation_issues( _fanout_mechanical_network(second_island_has_storage=True) ) self.assertNotIn( "ALGEBRAIC_ISLAND_HAS_NO_STORAGE", {issue.code for issue in issues}, ) if __name__ == "__main__": unittest.main()