Skip to content

Commit 972b29d

Browse files
authored
Merge pull request #1: Add balloon flight simulation
2 parents ed2fe8b + e6ecbcc commit 972b29d

7 files changed

Lines changed: 244 additions & 11 deletions

File tree

rocketpy/rocket/aero_surface/linear_generic_surface.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ def __init__(
1313
self,
1414
reference_area,
1515
reference_length,
16-
coefficients,
16+
coefficient_constants=None,
17+
coefficients=None,
1718
center_of_pressure=(0, 0, 0),
1819
name="Generic Linear Surface",
1920
):
@@ -41,6 +42,13 @@ def __init__(
4142
reference_length : int, float
4243
Reference length of the aerodynamic surface. Has the unit of meters.
4344
Commonly defined as the rocket's diameter.
45+
coefficient_constants: list, optional
46+
List of constants to be populated to the coefficients. This
47+
list will overwrite the coefficients dict if not None. The order is
48+
as follows: [cL_0, cL_alpha, cL_beta, cL_p, cL_q, cL_r, cQ_0, cQ_alpha,
49+
cQ_beta, cQ_p, cQ_q, cQ_r, cD_0, cD_alpha, cD_beta, cD_p, cD_q, cD_r,
50+
cm_0, cm_alpha, cm_beta, cm_p, cm_q, cm_r, cn_0, cn_alpha, cn_beta,
51+
cn_p, cn_q, cn_r, cl_0, cl_alpha, cl_beta, cl_p, cl_q, cl_r].
4452
coefficients: dict, optional
4553
List of coefficients. If a coefficient is omitted, it is set to 0.
4654
The valid coefficients are:\n
@@ -157,6 +165,36 @@ def __init__(
157165
name : str
158166
Name of the aerodynamic surface. Default is 'GenericSurface'.
159167
"""
168+
self.coefficient_constants = coefficient_constants
169+
# Populate the coefficients from the list of constants if they are not defined
170+
if coefficients is None:
171+
coefficients = self._get_default_coefficients()
172+
if coefficient_constants is not None:
173+
# helper to build a 7‑input callable returning a fixed value
174+
def _constant_factory(value):
175+
def _constant(
176+
alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate
177+
):
178+
"""Return the captured constant regardless of inputs."""
179+
return value
180+
181+
return _constant
182+
183+
for i, key in enumerate(self._get_default_coefficients().keys()):
184+
const = coefficient_constants[i]
185+
coefficients[key] = Function(
186+
_constant_factory(const),
187+
inputs=[
188+
"alpha",
189+
"beta",
190+
"mach",
191+
"reynolds",
192+
"pitch_rate",
193+
"yaw_rate",
194+
"roll_rate",
195+
],
196+
outputs=[key],
197+
)
160198

161199
super().__init__(
162200
reference_area=reference_area,

rocketpy/rocket/rocket.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,13 +207,16 @@ class Rocket:
207207
Rocket's inertia tensor 13 component with unloaded motor,in kg*m^2.
208208
Rocket.dry_I_23 : float
209209
Rocket's inertia tensor 23 component with unloaded motor,in kg*m^2.
210+
Rocket.volume : float
211+
Rocket's total volume in m³.
210212
"""
211213

212214
def __init__( # pylint: disable=too-many-statements
213215
self,
214216
radius,
215217
mass,
216218
inertia,
219+
volume,
217220
power_off_drag,
218221
power_on_drag,
219222
center_of_mass_without_motor,
@@ -240,6 +243,10 @@ def __init__( # pylint: disable=too-many-statements
240243
in the direction of e_i x e_j. Alternatively, the inertia tensor can
241244
be given as (I_11, I_22, I_33), where I_12 = I_13 = I_23 = 0. This
242245
can also be called as "rocket dry inertia tensor".
246+
volume : int, float
247+
Rocket's total volume in m³. This can be used for buoyancy calculations.
248+
It can also be set to 0 if buoyancy is not being considered.
249+
Use None to let the program calculate the volume based on the added components.
243250
power_off_drag : int, float, callable, string, array
244251
Rocket's drag coefficient when the motor is off. Can be given as an
245252
entry to the Function class. See help(Function) for more
@@ -375,6 +382,13 @@ def __init__( # pylint: disable=too-many-statements
375382
self.evaluate_reduced_mass()
376383
self.evaluate_thrust_to_weight()
377384

385+
if volume is None:
386+
# calculate volume properties for buoyancy
387+
self.evaluate_volume()
388+
else:
389+
# Volume and buoyancy properties
390+
self.volume = volume
391+
378392
# Evaluate stability (even though no aerodynamic surfaces are present yet)
379393
self.evaluate_center_of_pressure()
380394
self.evaluate_stability_margin()
@@ -625,12 +639,14 @@ def evaluate_stability_margin(self):
625639
self.stability_margin.set_source(
626640
lambda mach, time: (
627641
(
628-
self.center_of_mass.get_value_opt(time)
629-
- self.cp_position.get_value_opt(mach)
642+
(
643+
self.center_of_mass.get_value_opt(time)
644+
- self.cp_position.get_value_opt(mach)
645+
)
646+
/ (2 * self.radius)
630647
)
631-
/ (2 * self.radius)
648+
* self._csys
632649
)
633-
* self._csys
634650
)
635651
return self.stability_margin
636652

@@ -647,10 +663,12 @@ def evaluate_static_margin(self):
647663
# Calculate static margin
648664
self.static_margin.set_source(
649665
lambda time: (
650-
self.center_of_mass.get_value_opt(time)
651-
- self.cp_position.get_value_opt(0)
666+
(
667+
self.center_of_mass.get_value_opt(time)
668+
- self.cp_position.get_value_opt(0)
669+
)
670+
/ (2 * self.radius)
652671
)
653-
/ (2 * self.radius)
654672
)
655673
# Change sign if coordinate system is upside down
656674
self.static_margin *= self._csys
@@ -662,6 +680,20 @@ def evaluate_static_margin(self):
662680
)
663681
return self.static_margin
664682

683+
def evaluate_volume(self):
684+
"""Calculates the total volume of the rocket including the motor.
685+
The volume must be set manually using set_volume() or calculated from motor.
686+
687+
Returns
688+
-------
689+
self.volume : float
690+
Total volume of the rocket in m³.
691+
"""
692+
# TODO: calculate volume from radius and surface locations
693+
self.volume = 0
694+
695+
return self.volume
696+
665697
def evaluate_dry_inertias(self):
666698
"""Calculates and returns the rocket's dry inertias relative to
667699
the rocket's center of dry mass. The inertias are saved and returned

rocketpy/simulation/flight.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,6 +1714,10 @@ def u_dot(self, t, u, post_processing=False): # pylint: disable=too-many-locals
17141714
ax, ay, az = K @ Vector(L)
17151715
az -= self.env.gravity.get_value_opt(z) # Include gravity
17161716

1717+
# Include buoyancy force: F_buoyancy = rho * V * g (upward)
1718+
buoyancy_force = rho * self.rocket.volume * self.env.gravity.get_value_opt(z)
1719+
az += buoyancy_force / total_mass_at_t # Add buoyancy acceleration
1720+
17171721
# Coriolis acceleration
17181722
_, w_earth_y, w_earth_z = self.env.earth_rotation_vector
17191723
ax -= 2 * (vz * w_earth_y - vy * w_earth_z)
@@ -1913,9 +1917,12 @@ def u_dot_generalized(self, t, u, post_processing=False): # pylint: disable=too
19131917
)
19141918
M3 += self.rocket.cp_eccentricity_x * R2 - self.rocket.cp_eccentricity_y * R1
19151919

1916-
weight_in_body_frame = Kt @ Vector(
1917-
[0, 0, -total_mass * self.env.gravity.get_value_opt(z)]
1920+
# Calculate weight with buoyancy: F_net = -total_mass * g + rho * V * g
1921+
gravity_accel = self.env.gravity.get_value_opt(z)
1922+
net_gravitational_force = (
1923+
-total_mass * gravity_accel + rho * self.rocket.volume * gravity_accel
19181924
)
1925+
weight_in_body_frame = Kt @ Vector([0, 0, net_gravitational_force])
19191926

19201927
T00 = total_mass * r_CM
19211928
T03 = 2 * total_mass_dot * (r_NOZ - r_CM) - 2 * total_mass * r_CM_dot
@@ -2035,9 +2042,13 @@ def u_dot_parachute(self, t, u, post_processing=False):
20352042
Dx = pseudo_drag * freestream_x # add eta efficiency for wake
20362043
Dy = pseudo_drag * freestream_y
20372044
Dz = pseudo_drag * freestream_z
2045+
2046+
# Calculate buoyancy force during parachute phase
2047+
buoyancy_force = rho * self.rocket.volume * self.env.gravity.get_value_opt(z)
2048+
20382049
ax = Dx / (mp + ma)
20392050
ay = Dy / (mp + ma)
2040-
az = (Dz - mp * self.env.gravity.get_value_opt(z)) / (mp + ma)
2051+
az = (Dz - mp * self.env.gravity.get_value_opt(z) + buoyancy_force) / (mp + ma)
20412052

20422053
# Add coriolis acceleration
20432054
_, w_earth_y, w_earth_z = self.env.earth_rotation_vector

rocketpy/simulation/monte_carlo.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,9 @@ def __run_single_simulation(self):
461461
initial_solution=self.flight.initial_solution,
462462
terminate_on_apogee=self.flight.terminate_on_apogee,
463463
time_overshoot=self.flight.time_overshoot,
464+
max_time=self.flight.max_time,
465+
max_time_step=self.flight.max_time_step,
466+
min_time_step=self.flight.min_time_step,
464467
)
465468

466469
def __evaluate_flight_inputs(self, sim_idx):

rocketpy/stochastic/stochastic_aero_surfaces.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from rocketpy.rocket.aero_surface import (
77
AirBrakes,
88
EllipticalFins,
9+
LinearGenericSurface,
910
NoseCone,
1011
RailButtons,
1112
Tail,
@@ -546,3 +547,91 @@ def create_object(self):
546547
)
547548
air_brakes.drag_coefficient *= generated_dict["drag_coefficient_curve_factor"]
548549
return air_brakes
550+
551+
552+
class StochasticLinearGenericSurface(StochasticModel):
553+
"""A Stochastic Linear Generic Surface class that inherits from StochasticModel.
554+
555+
See Also
556+
--------
557+
:ref:`stochastic_model` and
558+
:class:`LinearGenericSurface <rocketpy.LinearGenericSurface>`
559+
560+
Attributes
561+
----------
562+
object : LinearGenericSurface
563+
LinearGenericSurface object to be used for validation.
564+
reference_area : tuple, list, int, float
565+
Reference area of the aerodynamic surface. Has the unit of meters
566+
squared. Commonly defined as the rocket's cross-sectional area.
567+
reference_length : tuple, list, int, float
568+
Reference length of the aerodynamic surface. Has the unit of meters.
569+
Commonly defined as the rocket's diameter.
570+
center_of_pressure : tuple, optional
571+
Application point of the aerodynamic forces and moments. The
572+
center of pressure is defined in the local coordinate system of the
573+
aerodynamic surface.
574+
coefficient_curve_factor : tuple, list, int, float, optional
575+
The drag curve factor of the air brakes. This value scales the
576+
drag coefficient curve to introduce stochastic variability.
577+
name : list[str]
578+
List with the name of the object. This attribute can not be randomized.
579+
"""
580+
581+
def __init__(
582+
self,
583+
linear_generic_surface,
584+
reference_area=None,
585+
reference_length=None,
586+
coefficient_constants=None,
587+
center_of_pressure=None,
588+
):
589+
"""Initializes the Stochastic Linear Generic Surface class.
590+
591+
See Also
592+
--------
593+
:ref:`stochastic_model`
594+
595+
Parameters
596+
----------
597+
linear_generic_surface : LinearGenericSurface
598+
LinearGenericSurface object to be used for validation.
599+
reference_area : int, float
600+
Reference area of the aerodynamic surface. Has the unit of meters
601+
squared. Commonly defined as the rocket's cross-sectional area.
602+
reference_length : int, float
603+
Reference length of the aerodynamic surface. Has the unit of meters.
604+
Commonly defined as the rocket's diameter.
605+
coefficients : dict
606+
Dictionary containing the aerodynamic coefficients of the surface.
607+
center_of_pressure : tuple, optional
608+
Application point of the aerodynamic forces and moments. The center of pressure is defined in the local coordinate system of the
609+
coefficient_curve_factor : tuple, list, int, float, optional
610+
The drag curve factor of the air brakes. This value scales the
611+
drag coefficient curve to introduce stochastic variability.
612+
"""
613+
super().__init__(
614+
linear_generic_surface,
615+
reference_area=reference_area,
616+
reference_length=reference_length,
617+
coefficient_constants=coefficient_constants,
618+
center_of_pressure=center_of_pressure,
619+
name=None,
620+
)
621+
622+
def create_object(self):
623+
"""Creates and returns a LinearGenericSurface object from the randomly generated input arguments.
624+
625+
Returns
626+
-------
627+
linear_generic_surface : LinearGenericSurface
628+
LinearGenericSurface object with the randomly generated input arguments.
629+
"""
630+
generated_dict = next(self.dict_generator())
631+
linear_generic_surface = LinearGenericSurface(
632+
reference_area=generated_dict["reference_area"],
633+
reference_length=generated_dict["reference_length"],
634+
coefficient_constants=generated_dict["coefficient_constants"],
635+
center_of_pressure=generated_dict["center_of_pressure"],
636+
)
637+
return linear_generic_surface

rocketpy/stochastic/stochastic_flight.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ class StochasticFlight(StochasticModel):
3232
time_overshoot : bool
3333
If False, the simulation will run at the time step defined by the controller
3434
sampling rate. Be aware that this will make the simulation run much slower.
35+
max_time : int, float
36+
The maximum time of the flight simulation. If the flight simulation
37+
reaches this time, it will terminate. This attribute can not be randomized.
38+
min_time_step : int, float
39+
The minimum time step of the flight simulation. This attribute can not be randomized.
40+
max_time_step : int, float
41+
The maximum time step of the flight simulation. This attribute can not be randomized.
3542
"""
3643

3744
def __init__(
@@ -43,6 +50,7 @@ def __init__(
4350
initial_solution=None,
4451
terminate_on_apogee=None,
4552
time_overshoot=None,
53+
max_time=None,
4654
):
4755
"""Initializes the Stochastic Flight class.
4856
@@ -70,6 +78,9 @@ def __init__(
7078
time_overshoot : bool
7179
If False, the simulation will run at the time step defined by the controller
7280
sampling rate. Be aware that this will make the simulation run much slower.
81+
max_time : int, float
82+
The maximum time of the flight simulation. If the flight simulation
83+
reaches this time, it will terminate. This attribute can not be randomized.
7384
"""
7485
if terminate_on_apogee is not None:
7586
assert isinstance(terminate_on_apogee, bool), (
@@ -78,6 +89,9 @@ def __init__(
7889
if time_overshoot is not None:
7990
if not isinstance(time_overshoot, bool):
8091
raise TypeError("`time_overshoot` must be a boolean")
92+
if max_time is not None:
93+
if not isinstance(max_time, (int, float)):
94+
raise TypeError("`max_time` must be a number")
8195
super().__init__(
8296
flight,
8397
rail_length=rail_length,
@@ -87,10 +101,16 @@ def __init__(
87101

88102
self.initial_solution = initial_solution
89103
self.terminate_on_apogee = terminate_on_apogee
104+
if max_time is None:
105+
self.max_time = flight.max_time
106+
else:
107+
self.max_time = max_time
90108
if time_overshoot is None:
91109
self.time_overshoot = flight.time_overshoot
92110
else:
93111
self.time_overshoot = time_overshoot
112+
self.max_time_step = flight.max_time_step
113+
self.min_time_step = flight.min_time_step
94114

95115
def _validate_initial_solution(self, initial_solution):
96116
if initial_solution is not None:
@@ -143,4 +163,5 @@ def create_object(self):
143163
initial_solution=self.initial_solution,
144164
terminate_on_apogee=self.terminate_on_apogee,
145165
time_overshoot=self.time_overshoot,
166+
max_time=self.max_time,
146167
)

0 commit comments

Comments
 (0)