Skip to content

Commit 26580c4

Browse files
caioessouzaGui-FernandesBR
authored andcommitted
ENH: Enable only radial burning
1 parent 170e89c commit 26580c4

File tree

8 files changed

+237
-60
lines changed

8 files changed

+237
-60
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ 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)
36+
3537
### Changed
3638

3739
### Fixed
@@ -71,6 +73,7 @@ Attention: The newest changes should be on top -->
7173
## [v1.10.0] - 2025-05-16
7274

7375
### Added
76+
7477
- ENH: Support for ND arithmetic in Function class. [#810] (https://github.com/RocketPy-Team/RocketPy/pull/810)
7578
- ENH: allow users to provide custom samplers [#803](https://github.com/RocketPy-Team/RocketPy/pull/803)
7679
- ENH: Implement Multivariate Rejection Sampling (MRS) [#738] (https://github.com/RocketPy-Team/RocketPy/pull/738)

rocketpy/motors/hybrid_motor.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,12 @@ class HybridMotor(Motor):
193193
HybridMotor.reference_pressure : int, float
194194
Atmospheric pressure in Pa at which the thrust data was recorded.
195195
It will allow to obtain the net thrust in the Flight class.
196+
SolidMotor.only_radial_burn : bool
197+
If True, grain regression is restricted to radial burn only (inner radius growth).
198+
Grain length remains constant throughout the burn. Default is False.
196199
"""
197200

201+
# pylint: disable=too-many-arguments
198202
def __init__( # pylint: disable=too-many-arguments
199203
self,
200204
thrust_source,
@@ -216,6 +220,7 @@ def __init__( # pylint: disable=too-many-arguments
216220
interpolation_method="linear",
217221
coordinate_system_orientation="nozzle_to_combustion_chamber",
218222
reference_pressure=None,
223+
only_radial_burn=True,
219224
):
220225
"""Initialize Motor class, process thrust curve and geometrical
221226
parameters and store results.
@@ -313,6 +318,11 @@ class Function. Thrust units are Newtons.
313318
"nozzle_to_combustion_chamber".
314319
reference_pressure : int, float, optional
315320
Atmospheric pressure in Pa at which the thrust data was recorded.
321+
only_radial_burn : boolean, optional
322+
If True, inhibits the grain from burning axially, only computing
323+
radial burn. If False, allows the grain to also burn
324+
axially. May be useful for axially inhibited grains or hybrid motors.
325+
Default is False.
316326
317327
Returns
318328
-------
@@ -364,6 +374,7 @@ class Function. Thrust units are Newtons.
364374
interpolation_method,
365375
coordinate_system_orientation,
366376
reference_pressure,
377+
only_radial_burn,
367378
)
368379

369380
self.positioned_tanks = self.liquid.positioned_tanks

rocketpy/motors/solid_motor.py

Lines changed: 96 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ class SolidMotor(Motor):
193193
SolidMotor.reference_pressure : int, float
194194
Atmospheric pressure in Pa at which the thrust data was recorded.
195195
It will allow to obtain the net thrust in the Flight class.
196+
SolidMotor.only_radial_burn : bool
197+
If True, grain regression is restricted to radial burn only (inner radius growth).
198+
Grain length remains constant throughout the burn. Default is False.
196199
"""
197200

198201
# pylint: disable=too-many-arguments
@@ -217,6 +220,7 @@ def __init__(
217220
interpolation_method="linear",
218221
coordinate_system_orientation="nozzle_to_combustion_chamber",
219222
reference_pressure=None,
223+
only_radial_burn=False,
220224
):
221225
"""Initialize Motor class, process thrust curve and geometrical
222226
parameters and store results.
@@ -314,11 +318,19 @@ class Function. Thrust units are Newtons.
314318
"nozzle_to_combustion_chamber".
315319
reference_pressure : int, float, optional
316320
Atmospheric pressure in Pa at which the thrust data was recorded.
321+
only_radial_burn : boolean, optional
322+
If True, inhibits the grain from burning axially, only computing
323+
radial burn. If False, allows the grain to also burn
324+
axially. May be useful for axially inhibited grains or hybrid motors.
325+
Default is False.
317326
318327
Returns
319328
-------
320329
None
321330
"""
331+
# Store before calling super().__init__() since it calls evaluate_geometry()
332+
self.only_radial_burn = only_radial_burn
333+
322334
super().__init__(
323335
thrust_source=thrust_source,
324336
dry_inertia=dry_inertia,
@@ -500,17 +512,25 @@ def geometry_dot(t, y):
500512

501513
# Compute state vector derivative
502514
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
515+
if self.only_radial_burn:
516+
burn_area = 2 * np.pi * (grain_inner_radius * grain_height)
517+
518+
grain_inner_radius_derivative = -volume_diff / burn_area
519+
grain_height_derivative = 0 # Set to zero to disable axial burning
520+
521+
else:
522+
burn_area = (
523+
2
524+
* np.pi
525+
* (
526+
grain_outer_radius**2
527+
- grain_inner_radius**2
528+
+ grain_inner_radius * grain_height
529+
)
510530
)
511-
)
512-
grain_inner_radius_derivative = -volume_diff / burn_area
513-
grain_height_derivative = -2 * grain_inner_radius_derivative
531+
532+
grain_inner_radius_derivative = -volume_diff / burn_area
533+
grain_height_derivative = -2 * grain_inner_radius_derivative
514534

515535
return [grain_inner_radius_derivative, grain_height_derivative]
516536

@@ -521,32 +541,55 @@ def geometry_jacobian(t, y):
521541

522542
# Compute jacobian
523543
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
544+
if self.only_radial_burn:
545+
factor = volume_diff / (
546+
2 * np.pi * (grain_inner_radius * grain_height) ** 2
531547
)
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
542548

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-
]
549+
inner_radius_derivative_wrt_inner_radius = factor * (
550+
grain_height - 2 * grain_inner_radius
551+
)
552+
inner_radius_derivative_wrt_height = 0
553+
height_derivative_wrt_inner_radius = 0
554+
height_derivative_wrt_height = 0
555+
# Height is a constant, so all the derivatives with respect to it are set to zero
556+
557+
return [
558+
[
559+
inner_radius_derivative_wrt_inner_radius,
560+
inner_radius_derivative_wrt_height,
561+
],
562+
[height_derivative_wrt_inner_radius, height_derivative_wrt_height],
563+
]
564+
565+
else:
566+
factor = volume_diff / (
567+
2
568+
* np.pi
569+
* (
570+
grain_outer_radius**2
571+
- grain_inner_radius**2
572+
+ grain_inner_radius * grain_height
573+
)
574+
** 2
575+
)
576+
577+
inner_radius_derivative_wrt_inner_radius = factor * (
578+
grain_height - 2 * grain_inner_radius
579+
)
580+
inner_radius_derivative_wrt_height = factor * grain_inner_radius
581+
height_derivative_wrt_inner_radius = (
582+
-2 * inner_radius_derivative_wrt_inner_radius
583+
)
584+
height_derivative_wrt_height = -2 * inner_radius_derivative_wrt_height
585+
586+
return [
587+
[
588+
inner_radius_derivative_wrt_inner_radius,
589+
inner_radius_derivative_wrt_height,
590+
],
591+
[height_derivative_wrt_inner_radius, height_derivative_wrt_height],
592+
]
550593

551594
def terminate_burn(t, y): # pylint: disable=unused-argument
552595
end_function = (self.grain_outer_radius - y[0]) * y[1]
@@ -597,16 +640,24 @@ def burn_area(self):
597640
burn_area : Function
598641
Function representing the burn area progression with the time.
599642
"""
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
643+
if self.only_radial_burn:
644+
burn_area = (
645+
2
646+
* np.pi
647+
* (self.grain_inner_radius * self.grain_height)
648+
* self.grain_number
649+
)
650+
else:
651+
burn_area = (
652+
2
653+
* np.pi
654+
* (
655+
self.grain_outer_radius**2
656+
- self.grain_inner_radius**2
657+
+ self.grain_inner_radius * self.grain_height
658+
)
659+
* self.grain_number
607660
)
608-
* self.grain_number
609-
)
610661
return burn_area
611662

612663
@funcify_method("Time (s)", "burn rate (m/s)")
@@ -778,6 +829,7 @@ def to_dict(self, **kwargs):
778829
"grain_initial_height": self.grain_initial_height,
779830
"grain_separation": self.grain_separation,
780831
"grains_center_of_mass_position": self.grains_center_of_mass_position,
832+
"only_radial_burn": self.only_radial_burn,
781833
}
782834
)
783835

@@ -827,4 +879,5 @@ def from_dict(cls, data):
827879
interpolation_method=data["interpolate"],
828880
coordinate_system_orientation=data["coordinate_system_orientation"],
829881
reference_pressure=data.get("reference_pressure"),
882+
only_radial_burn=data.get("only_radial_burn", False),
830883
)

tests/fixtures/motor/hybrid_fixtures.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55

66
@pytest.fixture
7-
def hybrid_motor(spherical_oxidizer_tank):
7+
def hybrid_motor(oxidizer_tank):
88
"""An example of a hybrid motor with spherical oxidizer
99
tank and fuel grains.
1010
@@ -35,6 +35,6 @@ def hybrid_motor(spherical_oxidizer_tank):
3535
grains_center_of_mass_position=-0.1,
3636
)
3737

38-
motor.add_tank(spherical_oxidizer_tank, position=0.3)
38+
motor.add_tank(oxidizer_tank, position=0.3)
3939

4040
return motor

tests/integration/motors/test_hybridmotor.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
# pylint: disable=unused-argument
21
from unittest.mock import patch
32

3+
import numpy as np
44

5-
@patch("matplotlib.pyplot.show")
5+
6+
@patch("matplotlib.pyplot.show") # pylint: disable=unused-argument
67
def test_hybrid_motor_info(mock_show, hybrid_motor):
78
"""Tests the HybridMotor.all_info() method.
89
@@ -15,3 +16,40 @@ def test_hybrid_motor_info(mock_show, hybrid_motor):
1516
"""
1617
assert hybrid_motor.info() is None
1718
assert hybrid_motor.all_info() is None
19+
20+
21+
def test_hybrid_motor_only_radial_burn_behavior(hybrid_motor):
22+
"""
23+
Test if only_radial_burn flag in HybridMotor propagates to its SolidMotor
24+
and affects burn_area calculation.
25+
"""
26+
motor = hybrid_motor
27+
28+
# Activates the radial burning
29+
motor.solid.only_radial_burn = True
30+
assert motor.solid.only_radial_burn is True
31+
32+
# Calculates the expected initial area
33+
burn_area_radial = (
34+
2
35+
* np.pi
36+
* (motor.solid.grain_inner_radius(0) * motor.solid.grain_height(0))
37+
* motor.solid.grain_number
38+
)
39+
40+
assert np.isclose(motor.solid.burn_area(0), burn_area_radial, atol=1e-12)
41+
42+
# Deactivates the radial burning and recalculate the geometry
43+
motor.solid.only_radial_burn = False
44+
motor.solid.evaluate_geometry()
45+
assert motor.solid.only_radial_burn is False
46+
47+
# In this case the burning area also considers the bases of the grain
48+
inner_radius = motor.solid.grain_inner_radius(0)
49+
outer_radius = motor.solid.grain_outer_radius
50+
burn_area_total = (
51+
burn_area_radial
52+
+ 2 * np.pi * (outer_radius**2 - inner_radius**2) * motor.solid.grain_number
53+
)
54+
assert np.isclose(motor.solid.burn_area(0), burn_area_total, atol=1e-12)
55+
assert motor.solid.burn_area(0) > burn_area_radial
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import numpy as np
2+
3+
4+
def test_only_radial_burn_parameter_effect(cesaroni_m1670):
5+
"""Tests the effect of the only_radial_burn parameter on burn area
6+
calculation. When enabled, the burn area should only account for
7+
the radial surface of the grains (no axial regression).
8+
9+
Parameters
10+
----------
11+
cesaroni_m1670 : rocketpy.SolidMotor
12+
The SolidMotor object used in the test.
13+
"""
14+
motor = cesaroni_m1670
15+
motor.only_radial_burn = True
16+
assert motor.only_radial_burn
17+
18+
# When only_radial_burn is active, burn_area should consider only radial area
19+
burn_area_radial = (
20+
2
21+
* np.pi
22+
* motor.grain_inner_radius(0)
23+
* motor.grain_height(0)
24+
* motor.grain_number
25+
)
26+
assert np.isclose(motor.burn_area(0), burn_area_radial, atol=1e-12)
27+
28+
29+
def test_evaluate_geometry_updates_properties(cesaroni_m1670):
30+
"""Tests if the grain geometry evaluation correctly updates SolidMotor
31+
properties after instantiation. It ensures that grain geometry
32+
functions are created and behave as expected.
33+
34+
Parameters
35+
----------
36+
cesaroni_m1670 : rocketpy.SolidMotor
37+
The SolidMotor object used in the test.
38+
"""
39+
motor = cesaroni_m1670
40+
41+
assert hasattr(motor, "grain_inner_radius")
42+
assert hasattr(motor, "grain_height")
43+
44+
# Checks if the domain of grain_inner_radius function is consistent
45+
times = motor.grain_inner_radius.x_array
46+
values = motor.grain_inner_radius.y_array
47+
48+
# expected initial time
49+
assert times[0] == 0
50+
51+
# expected initial inner radius
52+
assert values[0] == motor.grain_initial_inner_radius
53+
54+
# final inner radius should be less or equal than outer radius
55+
assert values[-1] <= motor.grain_outer_radius
56+
57+
# evaluate at intermediate time
58+
assert isinstance(motor.grain_inner_radius(0.5), float)

0 commit comments

Comments
 (0)