Skip to content

ENH: Enable only radial burning #815

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
Attention: The newest changes should be on top -->

### Added

- ENH: Enable only radial burning [#815](https://github.com/RocketPy-Team/RocketPy/pull/815)
- ENH: Added Crop and Clip Methods to Function Class [#817](https://github.com/RocketPy-Team/RocketPy/pull/817)
- DOC: Add Flight class usage documentation and update index [#841](https://github.com/RocketPy-Team/RocketPy/pull/841)
- ENH: Discretized and No-Pickle Encoding Options [#827] (https://github.com/RocketPy-Team/RocketPy/pull/827)
Expand Down
2 changes: 2 additions & 0 deletions rocketpy/motors/hybrid_motor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=True,
):
"""Initialize Motor class, process thrust curve and geometrical
parameters and store results.
Expand Down Expand Up @@ -364,6 +365,7 @@ class Function. Thrust units are Newtons.
interpolation_method,
coordinate_system_orientation,
reference_pressure,
only_radial_burn,
)

self.positioned_tanks = self.liquid.positioned_tanks
Expand Down
136 changes: 93 additions & 43 deletions rocketpy/motors/solid_motor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
-------
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]

Expand All @@ -521,32 +538,55 @@ 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 a constant, so all the derivatives with respect to it are set to 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]
Expand Down Expand Up @@ -597,16 +637,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)")
Expand Down Expand Up @@ -778,6 +826,7 @@ def to_dict(self, **kwargs):
"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,
}
)

Expand Down Expand Up @@ -827,4 +876,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),
)
15 changes: 15 additions & 0 deletions tests/integration/test_solidmotor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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
40 changes: 38 additions & 2 deletions tests/unit/test_hybridmotor.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def test_hybrid_motor_center_of_mass(hybrid_motor, spherical_oxidizer_tank):
propellant_center_of_mass = propellant_balance / (grain_mass + oxidizer_mass)
center_of_mass = balance / (grain_mass + oxidizer_mass + DRY_MASS)

for t in np.linspace(0, 100, 100):
for t in np.linspace(0, BURN_TIME, 100):
assert pytest.approx(
hybrid_motor.center_of_propellant_mass(t)
) == propellant_center_of_mass(t)
Expand Down Expand Up @@ -170,9 +170,45 @@ 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

# Calculates the expected initial area
burn_area_radial = (
2
* np.pi
* (motor.solid.grain_inner_radius(0) * motor.solid.grain_height(0))
* motor.solid.grain_number
)

assert np.isclose(motor.solid.burn_area(0), burn_area_radial, atol=1e-12)

# Deactivates 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 = (
burn_area_radial
+ 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
29 changes: 29 additions & 0 deletions tests/unit/test_solidmotor.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,32 @@ 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):
# Tests if the only_radial_burn flag is properly set
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
burn_area_radial = 2 * np.pi * (motor.grain_inner_radius(0) * motor.grain_height(0)) * motor.grain_number
assert np.isclose(motor.burn_area(0), burn_area_radial, atol=1e-12)

def test_evaluate_geometry_updates_properties(cesaroni_m1670):
# Checks 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")

# Checks 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

# Evaluates without error
val = motor.grain_inner_radius(0.5) # evaluate at intermediate time
assert isinstance(val, float)
Loading