Skip to content

Commit 04c9a47

Browse files
ENH: Implement only_radial_burn feature for SolidMotor and HybridMotor classes
Co-authored-by: Gui-FernandesBR <[email protected]>
1 parent 9fc643f commit 04c9a47

File tree

6 files changed

+196
-44
lines changed

6 files changed

+196
-44
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Attention: The newest changes should be on top -->
3232

3333
### Added
3434

35+
- ENH: Enable only radial burning [#815](https://github.com/RocketPy-Team/RocketPy/pull/815)
3536

3637
### Changed
3738

rocketpy/motors/hybrid_motor.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ def __init__( # pylint: disable=too-many-arguments
216216
interpolation_method="linear",
217217
coordinate_system_orientation="nozzle_to_combustion_chamber",
218218
reference_pressure=None,
219+
only_radial_burn=False,
219220
):
220221
"""Initialize Motor class, process thrust curve and geometrical
221222
parameters and store results.
@@ -313,6 +314,11 @@ class Function. Thrust units are Newtons.
313314
"nozzle_to_combustion_chamber".
314315
reference_pressure : int, float, optional
315316
Atmospheric pressure in Pa at which the thrust data was recorded.
317+
only_radial_burn : boolean, optional
318+
If True, inhibits the grain from burning axially, only computing
319+
radial burn. If False, allows the grain to also burn
320+
axially. May be useful for axially inhibited grains or hybrid motors.
321+
Default is False.
316322
317323
Returns
318324
-------
@@ -364,6 +370,7 @@ class Function. Thrust units are Newtons.
364370
interpolation_method,
365371
coordinate_system_orientation,
366372
reference_pressure,
373+
only_radial_burn,
367374
)
368375

369376
self.positioned_tanks = self.liquid.positioned_tanks

rocketpy/motors/solid_motor.py

Lines changed: 94 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ def __init__(
217217
interpolation_method="linear",
218218
coordinate_system_orientation="nozzle_to_combustion_chamber",
219219
reference_pressure=None,
220+
only_radial_burn=False,
220221
):
221222
"""Initialize Motor class, process thrust curve and geometrical
222223
parameters and store results.
@@ -314,6 +315,11 @@ class Function. Thrust units are Newtons.
314315
"nozzle_to_combustion_chamber".
315316
reference_pressure : int, float, optional
316317
Atmospheric pressure in Pa at which the thrust data was recorded.
318+
only_radial_burn : boolean, optional
319+
If True, inhibits the grain from burning axially, only computing
320+
radial burn. If False, allows the grain to also burn
321+
axially. May be useful for axially inhibited grains or hybrid motors.
322+
Default is False.
317323
318324
Returns
319325
-------
@@ -353,6 +359,9 @@ class Function. Thrust units are Newtons.
353359
)
354360
self.grain_initial_mass = self.grain_density * self.grain_initial_volume
355361

362+
# Burn surface definition
363+
self.only_radial_burn = only_radial_burn
364+
356365
self.evaluate_geometry()
357366

358367
# Initialize plots and prints object
@@ -500,17 +509,25 @@ def geometry_dot(t, y):
500509

501510
# Compute state vector derivative
502511
grain_inner_radius, grain_height = y
503-
burn_area = (
504-
2
505-
* np.pi
506-
* (
507-
grain_outer_radius**2
508-
- grain_inner_radius**2
509-
+ grain_inner_radius * grain_height
512+
if self.only_radial_burn:
513+
burn_area = 2 * np.pi * (grain_inner_radius * grain_height)
514+
515+
grain_inner_radius_derivative = -volume_diff / burn_area
516+
grain_height_derivative = 0 # Set to zero to disable axial burning
517+
518+
else:
519+
burn_area = (
520+
2
521+
* np.pi
522+
* (
523+
grain_outer_radius**2
524+
- grain_inner_radius**2
525+
+ grain_inner_radius * grain_height
526+
)
510527
)
511-
)
512-
grain_inner_radius_derivative = -volume_diff / burn_area
513-
grain_height_derivative = -2 * grain_inner_radius_derivative
528+
529+
grain_inner_radius_derivative = -volume_diff / burn_area
530+
grain_height_derivative = -2 * grain_inner_radius_derivative
514531

515532
return [grain_inner_radius_derivative, grain_height_derivative]
516533

@@ -521,32 +538,56 @@ def geometry_jacobian(t, y):
521538

522539
# Compute jacobian
523540
grain_inner_radius, grain_height = y
524-
factor = volume_diff / (
525-
2
526-
* np.pi
527-
* (
528-
grain_outer_radius**2
529-
- grain_inner_radius**2
530-
+ grain_inner_radius * grain_height
541+
if self.only_radial_burn:
542+
factor = volume_diff / (
543+
2 * np.pi * (grain_inner_radius * grain_height) ** 2
531544
)
532-
** 2
533-
)
534-
inner_radius_derivative_wrt_inner_radius = factor * (
535-
grain_height - 2 * grain_inner_radius
536-
)
537-
inner_radius_derivative_wrt_height = factor * grain_inner_radius
538-
height_derivative_wrt_inner_radius = (
539-
-2 * inner_radius_derivative_wrt_inner_radius
540-
)
541-
height_derivative_wrt_height = -2 * inner_radius_derivative_wrt_height
542545

543-
return [
544-
[
545-
inner_radius_derivative_wrt_inner_radius,
546-
inner_radius_derivative_wrt_height,
547-
],
548-
[height_derivative_wrt_inner_radius, height_derivative_wrt_height],
549-
]
546+
inner_radius_derivative_wrt_inner_radius = factor * (
547+
grain_height - 2 * grain_inner_radius
548+
)
549+
inner_radius_derivative_wrt_height = 0
550+
height_derivative_wrt_inner_radius = 0
551+
height_derivative_wrt_height = 0
552+
# Height is constant when only radial burning is enabled,
553+
# so all derivatives with respect to height are zero
554+
555+
return [
556+
[
557+
inner_radius_derivative_wrt_inner_radius,
558+
inner_radius_derivative_wrt_height,
559+
],
560+
[height_derivative_wrt_inner_radius, height_derivative_wrt_height],
561+
]
562+
563+
else:
564+
factor = volume_diff / (
565+
2
566+
* np.pi
567+
* (
568+
grain_outer_radius**2
569+
- grain_inner_radius**2
570+
+ grain_inner_radius * grain_height
571+
)
572+
** 2
573+
)
574+
575+
inner_radius_derivative_wrt_inner_radius = factor * (
576+
grain_height - 2 * grain_inner_radius
577+
)
578+
inner_radius_derivative_wrt_height = factor * grain_inner_radius
579+
height_derivative_wrt_inner_radius = (
580+
-2 * inner_radius_derivative_wrt_inner_radius
581+
)
582+
height_derivative_wrt_height = -2 * inner_radius_derivative_wrt_height
583+
584+
return [
585+
[
586+
inner_radius_derivative_wrt_inner_radius,
587+
inner_radius_derivative_wrt_height,
588+
],
589+
[height_derivative_wrt_inner_radius, height_derivative_wrt_height],
590+
]
550591

551592
def terminate_burn(t, y): # pylint: disable=unused-argument
552593
end_function = (self.grain_outer_radius - y[0]) * y[1]
@@ -597,16 +638,24 @@ def burn_area(self):
597638
burn_area : Function
598639
Function representing the burn area progression with the time.
599640
"""
600-
burn_area = (
601-
2
602-
* np.pi
603-
* (
604-
self.grain_outer_radius**2
605-
- self.grain_inner_radius**2
606-
+ self.grain_inner_radius * self.grain_height
641+
if self.only_radial_burn:
642+
burn_area = (
643+
2
644+
* np.pi
645+
* (self.grain_inner_radius * self.grain_height)
646+
* self.grain_number
647+
)
648+
else:
649+
burn_area = (
650+
2
651+
* np.pi
652+
* (
653+
self.grain_outer_radius**2
654+
- self.grain_inner_radius**2
655+
+ self.grain_inner_radius * self.grain_height
656+
)
657+
* self.grain_number
607658
)
608-
* self.grain_number
609-
)
610659
return burn_area
611660

612661
@funcify_method("Time (s)", "burn rate (m/s)")
@@ -778,6 +827,7 @@ def to_dict(self, include_outputs=False):
778827
"grain_initial_height": self.grain_initial_height,
779828
"grain_separation": self.grain_separation,
780829
"grains_center_of_mass_position": self.grains_center_of_mass_position,
830+
"only_radial_burn": self.only_radial_burn,
781831
}
782832
)
783833

@@ -822,4 +872,5 @@ def from_dict(cls, data):
822872
interpolation_method=data["interpolate"],
823873
coordinate_system_orientation=data["coordinate_system_orientation"],
824874
reference_pressure=data.get("reference_pressure"),
875+
only_radial_burn=data.get("only_radial_burn", False),
825876
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from unittest.mock import patch
2+
3+
4+
@patch("matplotlib.pyplot.show")
5+
def test_solid_motor_info(mock_show, cesaroni_m1670):
6+
"""Tests the SolidMotor.all_info() method.
7+
8+
Parameters
9+
----------
10+
mock_show : mock
11+
Mock of the matplotlib.pyplot.show function.
12+
cesaroni_m1670 : rocketpy.SolidMotor
13+
The SolidMotor object to be used in the tests.
14+
"""
15+
assert cesaroni_m1670.info() is None
16+
assert cesaroni_m1670.all_info() is None

tests/unit/test_hybridmotor.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,49 @@ def test_hybrid_motor_inertia(hybrid_motor, spherical_oxidizer_tank):
170170
+ DRY_MASS * (-hybrid_motor.center_of_mass + CENTER_OF_DRY_MASS) ** 2
171171
)
172172

173-
for t in np.linspace(0, 100, 100):
173+
for t in np.linspace(0, BURN_TIME, 100):
174174
assert pytest.approx(hybrid_motor.propellant_I_11(t)) == propellant_inertia(t)
175175
assert pytest.approx(hybrid_motor.I_11(t)) == inertia(t)
176176

177177
# Assert cylindrical symmetry
178178
assert pytest.approx(hybrid_motor.propellant_I_22(t)) == propellant_inertia(t)
179+
180+
181+
def test_hybrid_motor_only_radial_burn_behavior(hybrid_motor):
182+
"""
183+
Test if only_radial_burn flag in HybridMotor propagates to its SolidMotor
184+
and affects burn_area calculation.
185+
"""
186+
motor = hybrid_motor
187+
188+
# Activates the radial burning
189+
motor.solid.only_radial_burn = True
190+
assert motor.solid.only_radial_burn is True
191+
192+
# Calculate the expected initial area using the motor's burn_area method
193+
burn_area_radial = motor.solid.burn_area(0)
194+
195+
# Manually calculate expected radial burn area for verification
196+
expected_radial_area = (
197+
2
198+
* np.pi
199+
* (motor.solid.grain_inner_radius(0) * motor.solid.grain_height(0))
200+
* motor.solid.grain_number
201+
)
202+
203+
assert np.isclose(burn_area_radial, expected_radial_area, atol=1e-12)
204+
205+
# Deactivate the radial burning and recalculate the geometry
206+
motor.solid.only_radial_burn = False
207+
motor.solid.evaluate_geometry()
208+
assert motor.solid.only_radial_burn is False
209+
210+
# In this case the burning area also considers the bases of the grain
211+
inner_radius = motor.solid.grain_inner_radius(0)
212+
outer_radius = motor.solid.grain_outer_radius
213+
burn_area_total = (
214+
expected_radial_area
215+
+ 2 * np.pi * (outer_radius**2 - inner_radius**2) * motor.solid.grain_number
216+
)
217+
assert np.isclose(motor.solid.burn_area(0), burn_area_total, atol=1e-12)
218+
assert motor.solid.burn_area(0) > burn_area_radial

tests/unit/test_solidmotor.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,40 @@ def test_reshape_thrust_curve_asserts_resultant_thrust_curve_correct(
279279

280280
assert thrust_reshaped[1][1] == 100 * (tuple_parametric[1] / 7539.1875)
281281
assert thrust_reshaped[7][1] == 2034 * (tuple_parametric[1] / 7539.1875)
282+
283+
284+
def test_only_radial_burn_parameter_effect(cesaroni_m1670):
285+
"""Test if the only_radial_burn flag is properly set and affects burn area calculation."""
286+
motor = cesaroni_m1670
287+
motor.only_radial_burn = True
288+
assert motor.only_radial_burn is True
289+
290+
# When only_radial_burn is active, burn_area should consider only radial area
291+
# Use the motor's burn_area method directly to avoid code duplication
292+
burn_area_radial = motor.burn_area(0)
293+
294+
# Manually calculate expected radial burn area for verification
295+
expected_radial_area = (
296+
2 * np.pi * (motor.grain_inner_radius(0) * motor.grain_height(0)) * motor.grain_number
297+
)
298+
assert np.isclose(burn_area_radial, expected_radial_area, atol=1e-12)
299+
300+
301+
def test_evaluate_geometry_updates_properties(cesaroni_m1670):
302+
"""Check if after instantiation, grain_inner_radius and grain_height are valid functions."""
303+
motor = cesaroni_m1670
304+
305+
assert hasattr(motor, "grain_inner_radius")
306+
assert hasattr(motor, "grain_height")
307+
308+
# Check if the domain of grain_inner_radius function is consistent
309+
times = motor.grain_inner_radius.x_array
310+
values = motor.grain_inner_radius.y_array
311+
312+
assert times[0] == 0 # expected initial time
313+
assert values[0] == motor.grain_initial_inner_radius # expected initial inner radius
314+
assert values[-1] <= motor.grain_outer_radius # final inner radius should be less or equal than outer radius
315+
316+
# Evaluate without error
317+
val = motor.grain_inner_radius(0.5) # evaluate at intermediate time
318+
assert isinstance(val, float)

0 commit comments

Comments
 (0)