Skip to content

Commit d1b18a1

Browse files
Add multi-dimensional drag coefficient support to Flight class and integration tests
Co-authored-by: Gui-FernandesBR <[email protected]>
1 parent 7292277 commit d1b18a1

File tree

4 files changed

+308
-38
lines changed

4 files changed

+308
-38
lines changed

rocketpy/mathutils/function.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4146,19 +4146,7 @@ def from_grid(cls, grid_data, axes, inputs=None, outputs=None,
41464146
# Create a new Function instance
41474147
func = cls.__new__(cls)
41484148

4149-
# Initialize basic attributes
4150-
func.source = None # Will be set to indicate grid source
4151-
func.__inputs__ = inputs
4152-
func.__outputs__ = outputs if outputs is not None else "f"
4153-
func.__interpolation__ = interpolation
4154-
func.__extrapolation__ = extrapolation
4155-
func.title = kwargs.get('title', None)
4156-
func.__img_dim__ = 1
4157-
func.__cropped_domain__ = (None, None)
4158-
func._source_type = SourceType.ARRAY
4159-
func.__dom_dim__ = len(axes)
4160-
4161-
# Store grid-specific data
4149+
# Store grid-specific data first
41624150
func._grid_axes = axes
41634151
func._grid_data = grid_data
41644152

@@ -4181,6 +4169,20 @@ def from_grid(cls, grid_data, axes, inputs=None, outputs=None,
41814169
func._domain = domain_points
41824170
func._image = grid_data.ravel()
41834171

4172+
# Set source as flattened data array (for compatibility with serialization, etc.)
4173+
func.source = np.column_stack([domain_points, func._image])
4174+
4175+
# Initialize basic attributes
4176+
func.__inputs__ = inputs
4177+
func.__outputs__ = outputs if outputs is not None else "f"
4178+
func.__interpolation__ = interpolation
4179+
func.__extrapolation__ = extrapolation
4180+
func.title = kwargs.get('title', None)
4181+
func.__img_dim__ = 1
4182+
func.__cropped_domain__ = (None, None)
4183+
func._source_type = SourceType.ARRAY
4184+
func.__dom_dim__ = len(axes)
4185+
41844186
# Set basic array attributes for compatibility
41854187
func.x_array = axes[0]
41864188
func.x_initial, func.x_final = axes[0][0], axes[0][-1]

rocketpy/rocket/rocket.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -341,20 +341,28 @@ def __init__( # pylint: disable=too-many-statements
341341
)
342342

343343
# Define aerodynamic drag coefficients
344-
self.power_off_drag = Function(
345-
power_off_drag,
346-
"Mach Number",
347-
"Drag Coefficient with Power Off",
348-
"linear",
349-
"constant",
350-
)
351-
self.power_on_drag = Function(
352-
power_on_drag,
353-
"Mach Number",
354-
"Drag Coefficient with Power On",
355-
"linear",
356-
"constant",
357-
)
344+
# If already a Function, use it directly (preserves multi-dimensional drag)
345+
if isinstance(power_off_drag, Function):
346+
self.power_off_drag = power_off_drag
347+
else:
348+
self.power_off_drag = Function(
349+
power_off_drag,
350+
"Mach Number",
351+
"Drag Coefficient with Power Off",
352+
"linear",
353+
"constant",
354+
)
355+
356+
if isinstance(power_on_drag, Function):
357+
self.power_on_drag = power_on_drag
358+
else:
359+
self.power_on_drag = Function(
360+
power_on_drag,
361+
"Mach Number",
362+
"Drag Coefficient with Power On",
363+
"linear",
364+
"constant",
365+
)
358366

359367
# Create a, possibly, temporary empty motor
360368
# self.motors = Components() # currently unused, only 1 motor is supported

rocketpy/simulation/flight.py

Lines changed: 149 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,6 +1348,80 @@ def lateral_surface_wind(self):
13481348

13491349
return -wind_u * np.cos(heading_rad) + wind_v * np.sin(heading_rad)
13501350

1351+
def __get_drag_coefficient(
1352+
self, drag_function, mach, z, velocity_body, freestream_velocity_body
1353+
):
1354+
"""Calculate drag coefficient, handling both 1D and multi-dimensional functions.
1355+
1356+
Parameters
1357+
----------
1358+
drag_function : Function
1359+
The drag coefficient function (power_on_drag or power_off_drag)
1360+
mach : float
1361+
Mach number
1362+
z : float
1363+
Altitude in meters
1364+
velocity_body : Vector or array-like
1365+
Rocket velocity in body frame [vx_b, vy_b, vz_b]
1366+
freestream_velocity_body : Vector or array-like
1367+
Freestream velocity in body frame [stream_vx_b, stream_vy_b, stream_vz_b]
1368+
1369+
Returns
1370+
-------
1371+
float
1372+
Drag coefficient value
1373+
"""
1374+
# Check if drag function is multi-dimensional by examining its inputs
1375+
if hasattr(drag_function, "__inputs__") and len(drag_function.__inputs__) > 1:
1376+
# Multi-dimensional drag function - calculate additional parameters
1377+
1378+
# Calculate Reynolds number
1379+
# Re = rho * V * L / mu
1380+
# where L is characteristic length (rocket diameter)
1381+
rho = self.env.density.get_value_opt(z)
1382+
mu = self.env.dynamic_viscosity.get_value_opt(z)
1383+
freestream_speed = np.linalg.norm(freestream_velocity_body)
1384+
characteristic_length = 2 * self.rocket.radius # Diameter
1385+
reynolds = rho * freestream_speed * characteristic_length / mu
1386+
1387+
# Calculate angle of attack
1388+
# Angle between freestream velocity and rocket axis (z-axis in body frame)
1389+
# The z component of freestream velocity in body frame
1390+
if hasattr(freestream_velocity_body, "z"):
1391+
stream_vz_b = -freestream_velocity_body.z
1392+
else:
1393+
stream_vz_b = -freestream_velocity_body[2]
1394+
1395+
# Normalize and calculate angle
1396+
if freestream_speed > 1e-6:
1397+
cos_alpha = stream_vz_b / freestream_speed
1398+
# Clamp to [-1, 1] to avoid numerical issues
1399+
cos_alpha = np.clip(cos_alpha, -1.0, 1.0)
1400+
alpha_rad = np.arccos(cos_alpha)
1401+
alpha_deg = np.rad2deg(alpha_rad)
1402+
else:
1403+
alpha_deg = 0.0
1404+
1405+
# Determine which parameters to pass based on input names
1406+
input_names = [name.lower() for name in drag_function.__inputs__]
1407+
args = []
1408+
1409+
for name in input_names:
1410+
if "mach" in name or name == "m":
1411+
args.append(mach)
1412+
elif "reynolds" in name or name == "re":
1413+
args.append(reynolds)
1414+
elif "alpha" in name or name == "a" or "attack" in name:
1415+
args.append(alpha_deg)
1416+
else:
1417+
# Unknown parameter, default to mach
1418+
args.append(mach)
1419+
1420+
return drag_function.get_value_opt(*args)
1421+
else:
1422+
# 1D drag function - only mach number
1423+
return drag_function.get_value_opt(mach)
1424+
13511425
def udot_rail1(self, t, u, post_processing=False):
13521426
"""Calculates derivative of u state vector with respect to time
13531427
when rocket is flying in 1 DOF motion in the rail.
@@ -1384,7 +1458,37 @@ def udot_rail1(self, t, u, post_processing=False):
13841458
+ (vz) ** 2
13851459
) ** 0.5
13861460
free_stream_mach = free_stream_speed / self.env.speed_of_sound.get_value_opt(z)
1387-
drag_coeff = self.rocket.power_on_drag.get_value_opt(free_stream_mach)
1461+
1462+
# For rail motion, rocket is constrained - velocity mostly along z-axis in body frame
1463+
# Calculate velocity in body frame (simplified for rail)
1464+
a11 = 1 - 2 * (e2**2 + e3**2)
1465+
a12 = 2 * (e1 * e2 - e0 * e3)
1466+
a13 = 2 * (e1 * e3 + e0 * e2)
1467+
a21 = 2 * (e1 * e2 + e0 * e3)
1468+
a22 = 1 - 2 * (e1**2 + e3**2)
1469+
a23 = 2 * (e2 * e3 - e0 * e1)
1470+
a31 = 2 * (e1 * e3 - e0 * e2)
1471+
a32 = 2 * (e2 * e3 + e0 * e1)
1472+
a33 = 1 - 2 * (e1**2 + e2**2)
1473+
1474+
vx_b = a11 * vx + a21 * vy + a31 * vz
1475+
vy_b = a12 * vx + a22 * vy + a32 * vz
1476+
vz_b = a13 * vx + a23 * vy + a33 * vz
1477+
1478+
# Freestream velocity in body frame
1479+
wind_vx = self.env.wind_velocity_x.get_value_opt(z)
1480+
wind_vy = self.env.wind_velocity_y.get_value_opt(z)
1481+
stream_vx_b = a11 * (wind_vx - vx) + a21 * (wind_vy - vy) + a31 * (-vz)
1482+
stream_vy_b = a12 * (wind_vx - vx) + a22 * (wind_vy - vy) + a32 * (-vz)
1483+
stream_vz_b = a13 * (wind_vx - vx) + a23 * (wind_vy - vy) + a33 * (-vz)
1484+
1485+
drag_coeff = self.__get_drag_coefficient(
1486+
self.rocket.power_on_drag,
1487+
free_stream_mach,
1488+
z,
1489+
[vx_b, vy_b, vz_b],
1490+
[stream_vx_b, stream_vy_b, stream_vz_b],
1491+
)
13881492

13891493
# Calculate Forces
13901494
pressure = self.env.pressure.get_value_opt(z)
@@ -1552,12 +1656,34 @@ def u_dot(self, t, u, post_processing=False): # pylint: disable=too-many-locals
15521656
) ** 0.5
15531657
free_stream_mach = free_stream_speed / speed_of_sound
15541658

1659+
# Get rocket velocity in body frame (needed for drag calculation)
1660+
vx_b = a11 * vx + a21 * vy + a31 * vz
1661+
vy_b = a12 * vx + a22 * vy + a32 * vz
1662+
vz_b = a13 * vx + a23 * vy + a33 * vz
1663+
1664+
# Calculate freestream velocity in body frame
1665+
stream_vx_b = a11 * (wind_velocity_x - vx) + a21 * (wind_velocity_y - vy) + a31 * (-vz)
1666+
stream_vy_b = a12 * (wind_velocity_x - vx) + a22 * (wind_velocity_y - vy) + a32 * (-vz)
1667+
stream_vz_b = a13 * (wind_velocity_x - vx) + a23 * (wind_velocity_y - vy) + a33 * (-vz)
1668+
15551669
# Determine aerodynamics forces
15561670
# Determine Drag Force
15571671
if t < self.rocket.motor.burn_out_time:
1558-
drag_coeff = self.rocket.power_on_drag.get_value_opt(free_stream_mach)
1672+
drag_coeff = self.__get_drag_coefficient(
1673+
self.rocket.power_on_drag,
1674+
free_stream_mach,
1675+
z,
1676+
[vx_b, vy_b, vz_b],
1677+
[stream_vx_b, stream_vy_b, stream_vz_b],
1678+
)
15591679
else:
1560-
drag_coeff = self.rocket.power_off_drag.get_value_opt(free_stream_mach)
1680+
drag_coeff = self.__get_drag_coefficient(
1681+
self.rocket.power_off_drag,
1682+
free_stream_mach,
1683+
z,
1684+
[vx_b, vy_b, vz_b],
1685+
[stream_vx_b, stream_vy_b, stream_vz_b],
1686+
)
15611687
rho = self.env.density.get_value_opt(z)
15621688
R3 = -0.5 * rho * (free_stream_speed**2) * self.rocket.area * drag_coeff
15631689
for air_brakes in self.rocket.air_brakes:
@@ -1579,10 +1705,6 @@ def u_dot(self, t, u, post_processing=False): # pylint: disable=too-many-locals
15791705
# Off center moment
15801706
M1 += self.rocket.cp_eccentricity_y * R3
15811707
M2 -= self.rocket.cp_eccentricity_x * R3
1582-
# Get rocket velocity in body frame
1583-
vx_b = a11 * vx + a21 * vy + a31 * vz
1584-
vy_b = a12 * vx + a22 * vy + a32 * vz
1585-
vz_b = a13 * vx + a23 * vy + a33 * vz
15861708
# Calculate lift and moment for each component of the rocket
15871709
velocity_in_body_frame = Vector([vx_b, vy_b, vz_b])
15881710
w = Vector([omega1, omega2, omega3])
@@ -1831,17 +1953,35 @@ def u_dot_generalized(self, t, u, post_processing=False): # pylint: disable=too
18311953
speed_of_sound = self.env.speed_of_sound.get_value_opt(z)
18321954
free_stream_mach = free_stream_speed / speed_of_sound
18331955

1956+
# Get rocket velocity in body frame (needed for drag calculation)
1957+
velocity_in_body_frame = Kt @ v
1958+
# Calculate freestream velocity in body frame
1959+
freestream_velocity = wind_velocity - v
1960+
freestream_velocity_body = Kt @ freestream_velocity
1961+
18341962
if self.rocket.motor.burn_start_time < t < self.rocket.motor.burn_out_time:
18351963
pressure = self.env.pressure.get_value_opt(z)
18361964
net_thrust = max(
18371965
self.rocket.motor.thrust.get_value_opt(t)
18381966
+ self.rocket.motor.pressure_thrust(pressure),
18391967
0,
18401968
)
1841-
drag_coeff = self.rocket.power_on_drag.get_value_opt(free_stream_mach)
1969+
drag_coeff = self.__get_drag_coefficient(
1970+
self.rocket.power_on_drag,
1971+
free_stream_mach,
1972+
z,
1973+
velocity_in_body_frame,
1974+
freestream_velocity_body,
1975+
)
18421976
else:
18431977
net_thrust = 0
1844-
drag_coeff = self.rocket.power_off_drag.get_value_opt(free_stream_mach)
1978+
drag_coeff = self.__get_drag_coefficient(
1979+
self.rocket.power_off_drag,
1980+
free_stream_mach,
1981+
z,
1982+
velocity_in_body_frame,
1983+
freestream_velocity_body,
1984+
)
18451985
R3 += -0.5 * rho * (free_stream_speed**2) * self.rocket.area * drag_coeff
18461986
for air_brakes in self.rocket.air_brakes:
18471987
if air_brakes.deployment_level > 0:
@@ -1859,8 +1999,6 @@ def u_dot_generalized(self, t, u, post_processing=False): # pylint: disable=too
18591999
R3 = air_brakes_force # Substitutes rocket drag coefficient
18602000
else:
18612001
R3 += air_brakes_force
1862-
# Get rocket velocity in body frame
1863-
velocity_in_body_frame = Kt @ v
18642002
# Calculate lift and moment for each component of the rocket
18652003
for aero_surface, _ in self.rocket.aerodynamic_surfaces:
18662004
# Component cp relative to CDM in body frame

0 commit comments

Comments
 (0)