diff --git a/CHANGELOG.md b/CHANGELOG.md index 28586a1b7..e98c3a927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Attention: The newest changes should be on top --> ### Added +- ENH: Enable only radial burning [#815](https://github.com/RocketPy-Team/RocketPy/pull/815) ### Changed diff --git a/rocketpy/motors/hybrid_motor.py b/rocketpy/motors/hybrid_motor.py index e227e4550..64f27bba8 100644 --- a/rocketpy/motors/hybrid_motor.py +++ b/rocketpy/motors/hybrid_motor.py @@ -216,6 +216,7 @@ def __init__( # pylint: disable=too-many-arguments interpolation_method="linear", coordinate_system_orientation="nozzle_to_combustion_chamber", reference_pressure=None, + only_radial_burn=False, ): """Initialize Motor class, process thrust curve and geometrical parameters and store results. @@ -313,6 +314,11 @@ class Function. Thrust units are Newtons. "nozzle_to_combustion_chamber". reference_pressure : int, float, optional Atmospheric pressure in Pa at which the thrust data was recorded. + only_radial_burn : boolean, optional + If True, inhibits the grain from burning axially, only computing + radial burn. If False, allows the grain to also burn + axially. May be useful for axially inhibited grains or hybrid motors. + Default is False. Returns ------- @@ -364,6 +370,7 @@ class Function. Thrust units are Newtons. interpolation_method, coordinate_system_orientation, reference_pressure, + only_radial_burn, ) self.positioned_tanks = self.liquid.positioned_tanks diff --git a/rocketpy/motors/solid_motor.py b/rocketpy/motors/solid_motor.py index a8d823966..c2a78f52f 100644 --- a/rocketpy/motors/solid_motor.py +++ b/rocketpy/motors/solid_motor.py @@ -217,6 +217,7 @@ def __init__( interpolation_method="linear", coordinate_system_orientation="nozzle_to_combustion_chamber", reference_pressure=None, + only_radial_burn=False, ): """Initialize Motor class, process thrust curve and geometrical parameters and store results. @@ -314,6 +315,11 @@ class Function. Thrust units are Newtons. "nozzle_to_combustion_chamber". reference_pressure : int, float, optional Atmospheric pressure in Pa at which the thrust data was recorded. + only_radial_burn : boolean, optional + If True, inhibits the grain from burning axially, only computing + radial burn. If False, allows the grain to also burn + axially. May be useful for axially inhibited grains or hybrid motors. + Default is False. Returns ------- @@ -353,6 +359,9 @@ class Function. Thrust units are Newtons. ) self.grain_initial_mass = self.grain_density * self.grain_initial_volume + # Burn surface definition + self.only_radial_burn = only_radial_burn + self.evaluate_geometry() # Initialize plots and prints object @@ -500,17 +509,25 @@ def geometry_dot(t, y): # Compute state vector derivative grain_inner_radius, grain_height = y - burn_area = ( - 2 - * np.pi - * ( - grain_outer_radius**2 - - grain_inner_radius**2 - + grain_inner_radius * grain_height + if self.only_radial_burn: + burn_area = 2 * np.pi * (grain_inner_radius * grain_height) + + grain_inner_radius_derivative = -volume_diff / burn_area + grain_height_derivative = 0 # Set to zero to disable axial burning + + else: + burn_area = ( + 2 + * np.pi + * ( + grain_outer_radius**2 + - grain_inner_radius**2 + + grain_inner_radius * grain_height + ) ) - ) - grain_inner_radius_derivative = -volume_diff / burn_area - grain_height_derivative = -2 * grain_inner_radius_derivative + + grain_inner_radius_derivative = -volume_diff / burn_area + grain_height_derivative = -2 * grain_inner_radius_derivative return [grain_inner_radius_derivative, grain_height_derivative] @@ -521,32 +538,56 @@ def geometry_jacobian(t, y): # Compute jacobian grain_inner_radius, grain_height = y - factor = volume_diff / ( - 2 - * np.pi - * ( - grain_outer_radius**2 - - grain_inner_radius**2 - + grain_inner_radius * grain_height + if self.only_radial_burn: + factor = volume_diff / ( + 2 * np.pi * (grain_inner_radius * grain_height) ** 2 ) - ** 2 - ) - inner_radius_derivative_wrt_inner_radius = factor * ( - grain_height - 2 * grain_inner_radius - ) - inner_radius_derivative_wrt_height = factor * grain_inner_radius - height_derivative_wrt_inner_radius = ( - -2 * inner_radius_derivative_wrt_inner_radius - ) - height_derivative_wrt_height = -2 * inner_radius_derivative_wrt_height - return [ - [ - inner_radius_derivative_wrt_inner_radius, - inner_radius_derivative_wrt_height, - ], - [height_derivative_wrt_inner_radius, height_derivative_wrt_height], - ] + inner_radius_derivative_wrt_inner_radius = factor * ( + grain_height - 2 * grain_inner_radius + ) + inner_radius_derivative_wrt_height = 0 + height_derivative_wrt_inner_radius = 0 + height_derivative_wrt_height = 0 + # Height is constant when only radial burning is enabled, + # so all derivatives with respect to height are zero + + return [ + [ + inner_radius_derivative_wrt_inner_radius, + inner_radius_derivative_wrt_height, + ], + [height_derivative_wrt_inner_radius, height_derivative_wrt_height], + ] + + else: + factor = volume_diff / ( + 2 + * np.pi + * ( + grain_outer_radius**2 + - grain_inner_radius**2 + + grain_inner_radius * grain_height + ) + ** 2 + ) + + inner_radius_derivative_wrt_inner_radius = factor * ( + grain_height - 2 * grain_inner_radius + ) + inner_radius_derivative_wrt_height = factor * grain_inner_radius + height_derivative_wrt_inner_radius = ( + -2 * inner_radius_derivative_wrt_inner_radius + ) + height_derivative_wrt_height = -2 * inner_radius_derivative_wrt_height + + return [ + [ + inner_radius_derivative_wrt_inner_radius, + inner_radius_derivative_wrt_height, + ], + [height_derivative_wrt_inner_radius, height_derivative_wrt_height], + ] def terminate_burn(t, y): # pylint: disable=unused-argument end_function = (self.grain_outer_radius - y[0]) * y[1] @@ -597,16 +638,24 @@ def burn_area(self): burn_area : Function Function representing the burn area progression with the time. """ - burn_area = ( - 2 - * np.pi - * ( - self.grain_outer_radius**2 - - self.grain_inner_radius**2 - + self.grain_inner_radius * self.grain_height + if self.only_radial_burn: + burn_area = ( + 2 + * np.pi + * (self.grain_inner_radius * self.grain_height) + * self.grain_number + ) + else: + burn_area = ( + 2 + * np.pi + * ( + self.grain_outer_radius**2 + - self.grain_inner_radius**2 + + self.grain_inner_radius * self.grain_height + ) + * self.grain_number ) - * self.grain_number - ) return burn_area @funcify_method("Time (s)", "burn rate (m/s)") @@ -778,6 +827,7 @@ def to_dict(self, include_outputs=False): "grain_initial_height": self.grain_initial_height, "grain_separation": self.grain_separation, "grains_center_of_mass_position": self.grains_center_of_mass_position, + "only_radial_burn": self.only_radial_burn, } ) @@ -822,4 +872,5 @@ def from_dict(cls, data): interpolation_method=data["interpolate"], coordinate_system_orientation=data["coordinate_system_orientation"], reference_pressure=data.get("reference_pressure"), + only_radial_burn=data.get("only_radial_burn", False), ) diff --git a/tests/integration/test_environment.py b/tests/integration/test_environment.py index e4c6b07f5..17d81a76e 100644 --- a/tests/integration/test_environment.py +++ b/tests/integration/test_environment.py @@ -28,7 +28,9 @@ def test_set_elevation_open_elevation( @patch("matplotlib.pyplot.show") -def test_era5_atmosphere(mock_show, example_spaceport_env): # pylint: disable=unused-argument +def test_era5_atmosphere( + mock_show, example_spaceport_env +): # pylint: disable=unused-argument """Tests the Reanalysis model with the ERA5 file. It uses an example file available in the data/weather folder of the RocketPy repository. @@ -49,7 +51,9 @@ def test_era5_atmosphere(mock_show, example_spaceport_env): # pylint: disable=u @patch("matplotlib.pyplot.show") -def test_custom_atmosphere(mock_show, example_plain_env): # pylint: disable=unused-argument +def test_custom_atmosphere( + mock_show, example_plain_env +): # pylint: disable=unused-argument """Tests the custom atmosphere model in the environment object. Parameters @@ -74,7 +78,9 @@ def test_custom_atmosphere(mock_show, example_plain_env): # pylint: disable=unu @patch("matplotlib.pyplot.show") -def test_standard_atmosphere(mock_show, example_plain_env): # pylint: disable=unused-argument +def test_standard_atmosphere( + mock_show, example_plain_env +): # pylint: disable=unused-argument """Tests the standard atmosphere model in the environment object. Parameters @@ -126,7 +132,9 @@ def test_windy_atmosphere(example_euroc_env, model_name): @pytest.mark.slow @patch("matplotlib.pyplot.show") -def test_gfs_atmosphere(mock_show, example_spaceport_env): # pylint: disable=unused-argument +def test_gfs_atmosphere( + mock_show, example_spaceport_env +): # pylint: disable=unused-argument """Tests the Forecast model with the GFS file. It does not test the values, instead the test checks if the method runs without errors. @@ -143,7 +151,9 @@ def test_gfs_atmosphere(mock_show, example_spaceport_env): # pylint: disable=un @pytest.mark.slow @patch("matplotlib.pyplot.show") -def test_nam_atmosphere(mock_show, example_spaceport_env): # pylint: disable=unused-argument +def test_nam_atmosphere( + mock_show, example_spaceport_env +): # pylint: disable=unused-argument """Tests the Forecast model with the NAM file. Parameters @@ -159,7 +169,9 @@ def test_nam_atmosphere(mock_show, example_spaceport_env): # pylint: disable=un @pytest.mark.slow @patch("matplotlib.pyplot.show") -def test_rap_atmosphere(mock_show, example_spaceport_env): # pylint: disable=unused-argument +def test_rap_atmosphere( + mock_show, example_spaceport_env +): # pylint: disable=unused-argument today = date.today() now = datetime.now(timezone.utc) example_spaceport_env.set_date((today.year, today.month, today.day, now.hour)) @@ -169,7 +181,9 @@ def test_rap_atmosphere(mock_show, example_spaceport_env): # pylint: disable=un @pytest.mark.slow @patch("matplotlib.pyplot.show") -def test_gefs_atmosphere(mock_show, example_spaceport_env): # pylint: disable=unused-argument +def test_gefs_atmosphere( + mock_show, example_spaceport_env +): # pylint: disable=unused-argument """Tests the Ensemble model with the GEFS file. Parameters @@ -185,7 +199,9 @@ def test_gefs_atmosphere(mock_show, example_spaceport_env): # pylint: disable=u @pytest.mark.slow @patch("matplotlib.pyplot.show") -def test_wyoming_sounding_atmosphere(mock_show, example_plain_env): # pylint: disable=unused-argument +def test_wyoming_sounding_atmosphere( + mock_show, example_plain_env +): # pylint: disable=unused-argument """Asserts whether the Wyoming sounding model in the environment object behaves as expected with respect to some attributes such as pressure, barometric_height, wind_velocity and temperature. @@ -220,7 +236,9 @@ def test_wyoming_sounding_atmosphere(mock_show, example_plain_env): # pylint: d @pytest.mark.slow @patch("matplotlib.pyplot.show") -def test_hiresw_ensemble_atmosphere(mock_show, example_spaceport_env): # pylint: disable=unused-argument +def test_hiresw_ensemble_atmosphere( + mock_show, example_spaceport_env +): # pylint: disable=unused-argument """Tests the Forecast model with the HIRESW file. Parameters @@ -246,7 +264,9 @@ def test_hiresw_ensemble_atmosphere(mock_show, example_spaceport_env): # pylint @pytest.mark.skip(reason="CMC model is currently not working") @patch("matplotlib.pyplot.show") -def test_cmc_atmosphere(mock_show, example_spaceport_env): # pylint: disable=unused-argument +def test_cmc_atmosphere( + mock_show, example_spaceport_env +): # pylint: disable=unused-argument """Tests the Ensemble model with the CMC file. Parameters diff --git a/tests/integration/test_environment_analysis.py b/tests/integration/test_environment_analysis.py index e6043c85a..8f8987d8f 100644 --- a/tests/integration/test_environment_analysis.py +++ b/tests/integration/test_environment_analysis.py @@ -60,5 +60,7 @@ def test_exports(mock_show, env_analysis): # pylint: disable=unused-argument @pytest.mark.slow @patch("matplotlib.pyplot.show") -def test_create_environment_object(mock_show, env_analysis): # pylint: disable=unused-argument +def test_create_environment_object( + mock_show, env_analysis +): # pylint: disable=unused-argument assert isinstance(env_analysis.create_environment_object(), Environment) diff --git a/tests/integration/test_flight.py b/tests/integration/test_flight.py index 8fddb6486..394ca38a8 100644 --- a/tests/integration/test_flight.py +++ b/tests/integration/test_flight.py @@ -195,7 +195,9 @@ def test_export_pressures(flight_calisto_robust): @patch("matplotlib.pyplot.show") -def test_hybrid_motor_flight(mock_show, flight_calisto_hybrid_modded): # pylint: disable=unused-argument +def test_hybrid_motor_flight( + mock_show, flight_calisto_hybrid_modded +): # pylint: disable=unused-argument """Test the flight of a rocket with a hybrid motor. This test only validates that a flight simulation can be performed with a hybrid motor; it does not validate the results. @@ -211,7 +213,9 @@ def test_hybrid_motor_flight(mock_show, flight_calisto_hybrid_modded): # pylint @patch("matplotlib.pyplot.show") -def test_liquid_motor_flight(mock_show, flight_calisto_liquid_modded): # pylint: disable=unused-argument +def test_liquid_motor_flight( + mock_show, flight_calisto_liquid_modded +): # pylint: disable=unused-argument """Test the flight of a rocket with a liquid motor. This test only validates that a flight simulation can be performed with a liquid motor; it does not validate the results. @@ -228,7 +232,9 @@ def test_liquid_motor_flight(mock_show, flight_calisto_liquid_modded): # pylint @pytest.mark.slow @patch("matplotlib.pyplot.show") -def test_time_overshoot(mock_show, calisto_robust, example_spaceport_env): # pylint: disable=unused-argument +def test_time_overshoot( + mock_show, calisto_robust, example_spaceport_env +): # pylint: disable=unused-argument """Test the time_overshoot parameter of the Flight class. This basically calls the all_info() method for a simulation without time_overshoot and checks if it returns None. It is not testing if the values are correct, @@ -257,7 +263,9 @@ def test_time_overshoot(mock_show, calisto_robust, example_spaceport_env): # py @patch("matplotlib.pyplot.show") -def test_simpler_parachute_triggers(mock_show, example_plain_env, calisto_robust): # pylint: disable=unused-argument +def test_simpler_parachute_triggers( + mock_show, example_plain_env, calisto_robust +): # pylint: disable=unused-argument """Tests different types of parachute triggers. This is important to ensure the code is working as intended, since the parachute triggers can have very different format definitions. It will add 3 parachutes using different @@ -399,7 +407,9 @@ def test_eccentricity_on_flight( # pylint: disable=unused-argument @patch("matplotlib.pyplot.show") -def test_air_brakes_flight(mock_show, flight_calisto_air_brakes): # pylint: disable=unused-argument +def test_air_brakes_flight( + mock_show, flight_calisto_air_brakes +): # pylint: disable=unused-argument """Test the flight of a rocket with air brakes. This test only validates that a flight simulation can be performed with air brakes; it does not validate the results. @@ -419,7 +429,9 @@ def test_air_brakes_flight(mock_show, flight_calisto_air_brakes): # pylint: dis @patch("matplotlib.pyplot.show") -def test_initial_solution(mock_show, example_plain_env, calisto_robust): # pylint: disable=unused-argument +def test_initial_solution( + mock_show, example_plain_env, calisto_robust +): # pylint: disable=unused-argument """Tests the initial_solution option of the Flight class. This test simply simulates the flight using the initial_solution option and checks if the all_info method returns None. @@ -464,7 +476,9 @@ def test_initial_solution(mock_show, example_plain_env, calisto_robust): # pyli @patch("matplotlib.pyplot.show") -def test_empty_motor_flight(mock_show, example_plain_env, calisto_motorless): # pylint: disable=unused-argument +def test_empty_motor_flight( + mock_show, example_plain_env, calisto_motorless +): # pylint: disable=unused-argument flight = Flight( rocket=calisto_motorless, environment=example_plain_env, diff --git a/tests/integration/test_flight_data_importer.py b/tests/integration/test_flight_data_importer.py index 9cff30d57..0187e0397 100644 --- a/tests/integration/test_flight_data_importer.py +++ b/tests/integration/test_flight_data_importer.py @@ -26,9 +26,9 @@ def test_flight_importer_bella_lui(): ) assert fd.name == "Bella Lui, EPFL Rocket Team, 2020" assert "time" in fd._columns[path], "Can't find 'time' column in fd._columns" - assert "altitude" in fd._columns[path], ( - "Can't find 'altitude' column in fd._columns" - ) + assert ( + "altitude" in fd._columns[path] + ), "Can't find 'altitude' column in fd._columns" assert "vz" in fd._columns[path], "Can't find 'vz' column in fd._columns" assert np.isclose(fd.altitude(0), 0.201, atol=1e-4) assert np.isclose(fd.vz(0), 5.028, atol=1e-4) @@ -51,7 +51,7 @@ def test_flight_importer_ndrt(): ) assert fd.name == "NDRT Rocket team, 2020" assert "time" in fd._columns[path], "Can't find 'time' column in fd._columns" - assert "altitude" in fd._columns[path], ( - "Can't find 'altitude' column in fd._columns" - ) + assert ( + "altitude" in fd._columns[path] + ), "Can't find 'altitude' column in fd._columns" assert np.isclose(fd.altitude(0), 0) diff --git a/tests/integration/test_rocket.py b/tests/integration/test_rocket.py index c47096617..9eeda0cdc 100644 --- a/tests/integration/test_rocket.py +++ b/tests/integration/test_rocket.py @@ -45,7 +45,9 @@ def test_airfoil( @patch("matplotlib.pyplot.show") -def test_air_brakes_clamp_on(mock_show, calisto_air_brakes_clamp_on): # pylint: disable=unused-argument +def test_air_brakes_clamp_on( + mock_show, calisto_air_brakes_clamp_on +): # pylint: disable=unused-argument """Test the air brakes class with clamp on configuration. This test checks the basic attributes and the deployment_level setter. It also checks the all_info method. diff --git a/tests/integration/test_sensor.py b/tests/integration/test_sensor.py index 07b57b61b..1e398ee00 100644 --- a/tests/integration/test_sensor.py +++ b/tests/integration/test_sensor.py @@ -93,7 +93,9 @@ def test_gnss_receiver(self): @pytest.mark.parametrize("plane", ["xz", "yz"]) @patch("matplotlib.pyplot.show") -def test_draw(mock_show, calisto_with_sensors, plane): # pylint: disable=unused-argument +def test_draw( + mock_show, calisto_with_sensors, plane +): # pylint: disable=unused-argument """Test the drawing of the sensors.""" calisto_with_sensors.draw(plane=plane) diff --git a/tests/integration/test_solidmotor.py b/tests/integration/test_solidmotor.py new file mode 100644 index 000000000..f10e2dc54 --- /dev/null +++ b/tests/integration/test_solidmotor.py @@ -0,0 +1,16 @@ +from unittest.mock import patch + + +@patch("matplotlib.pyplot.show") +def test_solid_motor_info(mock_show, cesaroni_m1670): + """Tests the SolidMotor.all_info() method. + + Parameters + ---------- + mock_show : mock + Mock of the matplotlib.pyplot.show function. + cesaroni_m1670 : rocketpy.SolidMotor + The SolidMotor object to be used in the tests. + """ + assert cesaroni_m1670.info() is None + assert cesaroni_m1670.all_info() is None diff --git a/tests/unit/test_aero_surfaces.py b/tests/unit/test_aero_surfaces.py index f264ce40b..a59820b5f 100644 --- a/tests/unit/test_aero_surfaces.py +++ b/tests/unit/test_aero_surfaces.py @@ -76,7 +76,9 @@ def test_powerseries_nosecones_setters(power, invalid_power, new_power): @patch("matplotlib.pyplot.show") -def test_elliptical_fins_draw(mock_show, elliptical_fin_set): # pylint: disable=unused-argument +def test_elliptical_fins_draw( + mock_show, elliptical_fin_set +): # pylint: disable=unused-argument assert elliptical_fin_set.plots.draw(filename=None) is None @@ -85,7 +87,9 @@ def test_nose_cone_info(calisto_nose_cone): @patch("matplotlib.pyplot.show") -def test_nose_cone_draw(mock_show, calisto_nose_cone): # pylint: disable=unused-argument +def test_nose_cone_draw( + mock_show, calisto_nose_cone +): # pylint: disable=unused-argument assert calisto_nose_cone.draw(filename=None) is None diff --git a/tests/unit/test_flight.py b/tests/unit/test_flight.py index 260a2b138..fdb60b69b 100644 --- a/tests/unit/test_flight.py +++ b/tests/unit/test_flight.py @@ -512,7 +512,9 @@ def test_rail_length(calisto_robust, example_plain_env, rail_length, out_of_rail @patch("matplotlib.pyplot.show") -def test_lat_lon_conversion_robust(mock_show, example_spaceport_env, calisto_robust): # pylint: disable=unused-argument +def test_lat_lon_conversion_robust( + mock_show, example_spaceport_env, calisto_robust +): # pylint: disable=unused-argument test_flight = Flight( rocket=calisto_robust, environment=example_spaceport_env, @@ -529,7 +531,9 @@ def test_lat_lon_conversion_robust(mock_show, example_spaceport_env, calisto_rob @patch("matplotlib.pyplot.show") -def test_lat_lon_conversion_from_origin(mock_show, example_plain_env, calisto_robust): # pylint: disable=unused-argument +def test_lat_lon_conversion_from_origin( + mock_show, example_plain_env, calisto_robust +): # pylint: disable=unused-argument "additional tests to capture incorrect behaviors during lat/lon conversions" test_flight = Flight( diff --git a/tests/unit/test_function.py b/tests/unit/test_function.py index 77f5916f4..353837458 100644 --- a/tests/unit/test_function.py +++ b/tests/unit/test_function.py @@ -1079,18 +1079,18 @@ def test_low_pass_filter(alpha): # Check that the method works as intended and returns the right object with no issue assert isinstance(filtered_func, Function), "The returned type is not a Function" - assert np.array_equal(filtered_func.source[0], source[0]), ( - "The initial value is not the expected value" - ) - assert len(filtered_func.source) == len(source), ( - "The filtered Function and the Function have different lengths" - ) - assert filtered_func.__interpolation__ == func.__interpolation__, ( - "The interpolation method was unexpectedly changed" - ) - assert filtered_func.__extrapolation__ == func.__extrapolation__, ( - "The extrapolation method was unexpectedly changed" - ) + assert np.array_equal( + filtered_func.source[0], source[0] + ), "The initial value is not the expected value" + assert len(filtered_func.source) == len( + source + ), "The filtered Function and the Function have different lengths" + assert ( + filtered_func.__interpolation__ == func.__interpolation__ + ), "The interpolation method was unexpectedly changed" + assert ( + filtered_func.__extrapolation__ == func.__extrapolation__ + ), "The extrapolation method was unexpectedly changed" for i in range(1, len(source)): expected = alpha * source[i][1] + (1 - alpha) * filtered_func.source[i - 1][1] assert np.isclose(filtered_func.source[i][1], expected, atol=1e-6), ( diff --git a/tests/unit/test_hybridmotor.py b/tests/unit/test_hybridmotor.py index ef03a1998..7db381e69 100644 --- a/tests/unit/test_hybridmotor.py +++ b/tests/unit/test_hybridmotor.py @@ -170,9 +170,49 @@ def test_hybrid_motor_inertia(hybrid_motor, spherical_oxidizer_tank): + DRY_MASS * (-hybrid_motor.center_of_mass + CENTER_OF_DRY_MASS) ** 2 ) - for t in np.linspace(0, 100, 100): + for t in np.linspace(0, BURN_TIME, 100): assert pytest.approx(hybrid_motor.propellant_I_11(t)) == propellant_inertia(t) assert pytest.approx(hybrid_motor.I_11(t)) == inertia(t) # Assert cylindrical symmetry assert pytest.approx(hybrid_motor.propellant_I_22(t)) == propellant_inertia(t) + + +def test_hybrid_motor_only_radial_burn_behavior(hybrid_motor): + """ + Test if only_radial_burn flag in HybridMotor propagates to its SolidMotor + and affects burn_area calculation. + """ + motor = hybrid_motor + + # Activates the radial burning + motor.solid.only_radial_burn = True + assert motor.solid.only_radial_burn is True + + # Calculate the expected initial area using the motor's burn_area method + burn_area_radial = motor.solid.burn_area(0) + + # Manually calculate expected radial burn area for verification + expected_radial_area = ( + 2 + * np.pi + * (motor.solid.grain_inner_radius(0) * motor.solid.grain_height(0)) + * motor.solid.grain_number + ) + + assert np.isclose(burn_area_radial, expected_radial_area, atol=1e-12) + + # Deactivate the radial burning and recalculate the geometry + motor.solid.only_radial_burn = False + motor.solid.evaluate_geometry() + assert motor.solid.only_radial_burn is False + + # In this case the burning area also considers the bases of the grain + inner_radius = motor.solid.grain_inner_radius(0) + outer_radius = motor.solid.grain_outer_radius + burn_area_total = ( + expected_radial_area + + 2 * np.pi * (outer_radius**2 - inner_radius**2) * motor.solid.grain_number + ) + assert np.isclose(motor.solid.burn_area(0), burn_area_total, atol=1e-12) + assert motor.solid.burn_area(0) > burn_area_radial diff --git a/tests/unit/test_rocket.py b/tests/unit/test_rocket.py index 3c1e7168d..82e25d701 100644 --- a/tests/unit/test_rocket.py +++ b/tests/unit/test_rocket.py @@ -10,7 +10,9 @@ @patch("matplotlib.pyplot.show") -def test_elliptical_fins(mock_show, calisto_robust, calisto_trapezoidal_fins): # pylint: disable=unused-argument +def test_elliptical_fins( + mock_show, calisto_robust, calisto_trapezoidal_fins +): # pylint: disable=unused-argument test_rocket = calisto_robust calisto_robust.aerodynamic_surfaces.remove(calisto_trapezoidal_fins) test_rocket.add_elliptical_fins(4, span=0.100, root_chord=0.120, position=-1.168) diff --git a/tests/unit/test_solidmotor.py b/tests/unit/test_solidmotor.py index 3f829d222..d79b79674 100644 --- a/tests/unit/test_solidmotor.py +++ b/tests/unit/test_solidmotor.py @@ -279,3 +279,47 @@ def test_reshape_thrust_curve_asserts_resultant_thrust_curve_correct( assert thrust_reshaped[1][1] == 100 * (tuple_parametric[1] / 7539.1875) assert thrust_reshaped[7][1] == 2034 * (tuple_parametric[1] / 7539.1875) + + +def test_only_radial_burn_parameter_effect(cesaroni_m1670): + """Test if the only_radial_burn flag is properly set and affects burn area calculation.""" + motor = cesaroni_m1670 + motor.only_radial_burn = True + assert motor.only_radial_burn is True + + # When only_radial_burn is active, burn_area should consider only radial area + # Use the motor's burn_area method directly to avoid code duplication + burn_area_radial = motor.burn_area(0) + + # Manually calculate expected radial burn area for verification + expected_radial_area = ( + 2 + * np.pi + * (motor.grain_inner_radius(0) * motor.grain_height(0)) + * motor.grain_number + ) + assert np.isclose(burn_area_radial, expected_radial_area, atol=1e-12) + + +def test_evaluate_geometry_updates_properties(cesaroni_m1670): + """Check if after instantiation, grain_inner_radius and grain_height are valid functions.""" + motor = cesaroni_m1670 + + assert hasattr(motor, "grain_inner_radius") + assert hasattr(motor, "grain_height") + + # Check if the domain of grain_inner_radius function is consistent + times = motor.grain_inner_radius.x_array + values = motor.grain_inner_radius.y_array + + assert times[0] == 0 # expected initial time + assert ( + values[0] == motor.grain_initial_inner_radius + ) # expected initial inner radius + assert ( + values[-1] <= motor.grain_outer_radius + ) # final inner radius should be less or equal than outer radius + + # Evaluate without error + val = motor.grain_inner_radius(0.5) # evaluate at intermediate time + assert isinstance(val, float) diff --git a/tests/unit/test_utilities.py b/tests/unit/test_utilities.py index 55d45de95..51aba41a4 100644 --- a/tests/unit/test_utilities.py +++ b/tests/unit/test_utilities.py @@ -144,7 +144,9 @@ def test_flutter_prints(flight_calisto_custom_wind): @patch("matplotlib.pyplot.show") -def test_flutter_plots(mock_show, flight_calisto_custom_wind): # pylint: disable=unused-argument +def test_flutter_plots( + mock_show, flight_calisto_custom_wind +): # pylint: disable=unused-argument """Tests the _flutter_plots function. Parameters