import aerosandbox as asb
import aerosandbox.numpy as np
import aerosandbox.tools.units as u
from .raymer_fudge_factors import advanced_composites
from typing import Union
# From Raymer, Aircraft Design: A Conceptual Approach, 5th Ed.
# Section 15.3.2: Cargo/Transport Weights
[docs]def mass_wing(
wing: asb.Wing,
design_mass_TOGW: float,
ultimate_load_factor: float,
use_advanced_composites: bool = False,
) -> float:
"""
Computes the mass of the wing for a cargo/transport aircraft, according to Raymer's Aircraft Design: A Conceptual
Approach.
Note: Torenbeek's wing mass model is likely more accurate; see `mass_wing()` in `torenbeek_weights.py` (same
directory).
Args:
wing: The wing object.
design_mass_TOGW: The design take-off gross weight of the entire airplane [kg].
ultimate_load_factor: Ultimate load factor of the airplane.
use_advanced_composites: Whether to use advanced composites for the wing. If True, the wing mass is modified
accordingly.
Returns:
Wing mass [kg].
"""
airfoil_thicknesses = [
xsec.airfoil.max_thickness()
for xsec in wing.xsecs
]
airfoil_t_over_c = np.min(airfoil_thicknesses)
return (
0.0051 *
(design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.557 *
(wing.area('planform') / u.foot ** 2) ** 0.649 *
wing.aspect_ratio() ** 0.5 *
airfoil_t_over_c ** -0.4 *
(1 + wing.taper_ratio()) ** 0.1 *
np.cosd(wing.mean_sweep_angle()) ** -1 *
(wing.control_surface_area() / u.foot ** 2) ** 0.1 *
(advanced_composites["wing"] if use_advanced_composites else 1)
) * u.lbm
[docs]def mass_hstab(
hstab: asb.Wing,
design_mass_TOGW: float,
ultimate_load_factor: float,
wing_to_hstab_distance: float,
fuselage_width_at_hstab_intersection: float,
aircraft_y_radius_of_gyration: float = None,
use_advanced_composites: bool = False,
) -> float:
"""
Computes the mass of the horizontal stabilizer for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach.
Args:
hstab: The horizontal stabilizer object.
design_mass_TOGW: The design take-off gross weight of the entire airplane [kg].
ultimate_load_factor: Ultimate load factor of the airplane.
wing_to_hstab_distance: Distance from the wing's root-quarter-chord-point to the hstab's
root-quarter-chord-point [m].
fuselage_width_at_hstab_intersection: Width of the fuselage at the intersection of the wing and hstab [m].
aircraft_y_radius_of_gyration: Radius of gyration of the aircraft about the y-axis [m]. If None, estimates
this as `0.3 * wing_to_hstab_distance`.
use_advanced_composites: Whether to use advanced composites for the hstab. If True, the hstab mass is modified
accordingly.
Returns:
The mass of the horizontal stabilizer [kg].
"""
if aircraft_y_radius_of_gyration is None:
aircraft_y_radius_of_gyration = 0.3 * wing_to_hstab_distance
area = hstab.area()
### Determine if the hstab is all-moving or not
all_moving = True
for xsec in hstab.xsecs:
for control_surface in xsec.control_surfaces:
if (
(control_surface.trailing_edge and control_surface.hinge_point > 0) or
(not control_surface.trailing_edge and control_surface.hinge_point < 1)
):
all_moving = False
break
return (
0.0379 *
(1.143 if all_moving else 1) *
(1 + fuselage_width_at_hstab_intersection / hstab.span()) ** -0.25 *
(design_mass_TOGW / u.lbm) ** 0.639 *
ultimate_load_factor ** 0.10 *
(area / u.foot ** 2) ** 0.75 *
(wing_to_hstab_distance / u.foot) ** -1 *
(aircraft_y_radius_of_gyration / u.foot) ** 0.704 *
np.cosd(hstab.mean_sweep_angle()) ** -1 *
hstab.aspect_ratio() ** 0.166 *
(1 + hstab.control_surface_area() / area) ** 0.1 *
(advanced_composites["tails"] if use_advanced_composites else 1)
) * u.lbm
[docs]def mass_vstab(
vstab: asb.Wing,
design_mass_TOGW: float,
ultimate_load_factor: float,
wing_to_vstab_distance: float,
is_t_tail: bool = False,
aircraft_z_radius_of_gyration: float = None,
use_advanced_composites: bool = False,
) -> float:
"""
Computes the mass of the vertical stabilizer for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach.
Args:
vstab: The vertical stabilizer object.
design_mass_TOGW: The design take-off gross weight of the entire airplane [kg].
ultimate_load_factor: Ultimate load factor of the airplane.
wing_to_vstab_distance: Distance from the wing's root-quarter-chord-point to the vstab's
root-quarter-chord-point [m].
is_t_tail: Whether the airplane is a T-tail or not.
aircraft_z_radius_of_gyration: The z-radius of gyration of the entire airplane [m]. If None, estimates this
as `1 * wing_to_vstab_distance`.
use_advanced_composites: Whether to use advanced composites for the vstab. If True, the vstab mass is modified
accordingly.
Returns:
The mass of the vertical stabilizer [kg].
"""
airfoil_thicknesses = [
xsec.airfoil.max_thickness()
for xsec in vstab.xsecs
]
airfoil_t_over_c = np.min(airfoil_thicknesses)
if aircraft_z_radius_of_gyration is None:
aircraft_z_radius_of_gyration = 1 * wing_to_vstab_distance
return (
0.0026 *
(1 + (1 if is_t_tail else 0)) ** 0.225 *
(design_mass_TOGW / u.lbm) ** 0.556 *
ultimate_load_factor ** 0.536 *
(wing_to_vstab_distance / u.foot) ** -0.5 *
(vstab.area('planform') / u.foot ** 2) ** 0.5 *
(aircraft_z_radius_of_gyration / u.foot) ** 0.875 *
np.cosd(vstab.mean_sweep_angle()) ** -1 *
vstab.aspect_ratio() ** 0.35 *
airfoil_t_over_c ** -0.5 *
(advanced_composites["tails"] if use_advanced_composites else 1)
) * u.lbm
[docs]def mass_fuselage(
fuselage: asb.Fuselage,
design_mass_TOGW: float,
ultimate_load_factor: float,
L_over_D: float,
main_wing: asb.Wing,
n_cargo_doors: int = 1,
has_aft_clamshell_door: bool = False,
landing_gear_mounted_on_fuselage: bool = False,
use_advanced_composites: bool = False,
) -> float:
"""
Computes the mass of the fuselage for a cargo/transport aircraft, according to Raymer's Aircraft Design: A
Conceptual Approach.
Args:
fuselage: The fuselage object.
design_mass_TOGW: The design take-off gross weight of the entire airplane [kg].
ultimate_load_factor: Ultimate load factor of the airplane.
L_over_D: The lift-to-drag ratio of the airplane in cruise.
main_wing: The main wing object. Can be:
* An instance of an AeroSandbox wing object (`asb.Wing`)
* None, if the airplane has no main wing.
n_cargo_doors: The number of cargo doors on the fuselage.
has_aft_clamshell_door: Whether or not the fuselage has an aft clamshell door.
landing_gear_mounted_on_fuselage: Whether or not the landing gear is mounted on the fuselage.
use_advanced_composites: Whether to use advanced composites for the fuselage. If True, the fuselage mass is
modified accordingly.
Returns:
The mass of the fuselage [kg].
"""
K_door = (1 + (0.06 * n_cargo_doors)) * (1.12 if has_aft_clamshell_door else 1)
K_lg = 1.12 if landing_gear_mounted_on_fuselage else 1
fuselage_structural_length = fuselage.length()
if main_wing is not None:
K_ws = (
0.75 *
(
(1 + 2 * main_wing.taper_ratio()) /
(1 + main_wing.taper_ratio())
) *
(
main_wing.span() / fuselage_structural_length *
np.tand(main_wing.mean_sweep_angle())
)
)
else:
K_ws = 0
return (
0.3280 *
K_door *
K_lg *
(design_mass_TOGW / u.lbm * ultimate_load_factor) ** 0.5 *
(fuselage_structural_length / u.foot) ** 0.25 *
(fuselage.area_wetted() / u.foot ** 2) ** 0.302 *
(1 + K_ws) ** 0.04 *
L_over_D ** 0.10 * # L/D
(advanced_composites["fuselage/nacelle"] if use_advanced_composites else 1)
) * u.lbm
[docs]def mass_main_landing_gear(
main_gear_length: float,
landing_speed: float,
design_mass_TOGW: float,
is_kneeling: bool = False,
n_gear: int = 2,
n_wheels: int = 12,
n_shock_struts: int = 4,
use_advanced_composites: bool = False,
) -> float:
"""
Computes the mass of the main landing gear for a cargo/transport aircraft, according to Raymer's Aircraft Design:
A Conceptual Approach.
Args:
main_gear_length: length of the main landing gear [m].
landing_speed: landing speed [m/s].
design_mass_TOGW: The design take-off gross weight of the entire airplane [kg].
is_kneeling: whether the main landing gear is capable of kneeling.
n_gear: number of landing gear.
n_wheels: number of wheels in total on the main landing gear.
n_shock_struts: number of shock struts.
use_advanced_composites: Whether to use advanced composites for the landing gear. If True, the landing gear mass
is modified accordingly.
Returns:
mass of the main landing gear [kg].
"""
K_mp = 1.126 if is_kneeling else 1
ultimate_landing_load_factor = n_gear * 1.5
return (
0.0106 *
K_mp * # non-kneeling LG
(design_mass_TOGW / u.lbm) ** 0.888 *
ultimate_landing_load_factor ** 0.25 *
(main_gear_length / u.inch) ** 0.4 *
n_wheels ** 0.321 *
n_shock_struts ** -0.5 *
(landing_speed / u.knot) ** 0.1 *
(advanced_composites["landing_gear"] if use_advanced_composites else 1)
) * u.lbm
[docs]def mass_nose_landing_gear(
nose_gear_length: float,
design_mass_TOGW: float,
is_kneeling: bool = False,
n_gear: int = 1,
n_wheels: int = 2,
use_advanced_composites: bool = False,
) -> float:
"""
Computes the mass of the nose landing gear for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach.
Args:
nose_gear_length: Length of nose landing gear when fully-extended [m].
design_mass_TOGW: The design take-off gross weight of the entire airplane [kg].
is_kneeling: Whether the nose landing gear is capable of kneeling.
n_gear: Number of nose landing gear.
n_wheels: Number of wheels in total on the nose landing gear.
use_advanced_composites: Whether to use advanced composites for the landing gear. If True, the landing gear mass
is modified accordingly.
Returns:
Mass of nose landing gear [kg].
"""
K_np = 1.15 if is_kneeling else 1
ultimate_landing_load_factor = n_gear * 1.5
return (
0.032 *
K_np *
(design_mass_TOGW / u.lbm) ** 0.646 *
ultimate_landing_load_factor ** 0.2 *
(nose_gear_length / u.inch) ** 0.5 *
n_wheels ** 0.45 *
(advanced_composites["landing_gear"] if use_advanced_composites else 1)
) * u.lbm
[docs]def mass_nacelles(
nacelle_length: float,
nacelle_width: float,
nacelle_height: float,
ultimate_load_factor: float,
mass_per_engine: float,
n_engines: int,
is_pylon_mounted: bool = False,
engines_have_propellers: bool = False,
engines_have_thrust_reversers: bool = False,
use_advanced_composites: bool = False,
) -> float:
"""
Computes the mass of the nacelles for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach. Excludes the engine itself and immediate engine peripherals.
Args:
nacelle_length: length of the nacelle, front to back [m]
nacelle_width: width of the nacelle [m]
nacelle_height: height of the nacelle, top to bottom [m]
ultimate_load_factor: ultimate load factor of the aircraft
mass_per_engine: mass of the engine itself [kg]
n_engines: number of engines
is_pylon_mounted: whether the engine is pylon-mounted or not
engines_have_propellers: whether the engines have propellers or not (e.g., a jet)
engines_have_thrust_reversers: whether the engines have thrust reversers or not
use_advanced_composites: Whether to use advanced composites for the nacelles. If True, the nacelles mass
is modified accordingly.
Returns:
mass of the nacelles [kg]
"""
K_ng = 1.017 if is_pylon_mounted else 1
K_p = 1.4 if engines_have_propellers else 1
K_tr = 1.18 if engines_have_thrust_reversers else 1
mass_per_engine_with_contents = np.softmax(
(2.331 * (mass_per_engine / u.lbm) ** 0.901) * K_p * K_tr * u.lbm,
mass_per_engine,
hardness=10 / mass_per_engine
)
nacelle_wetted_area = (
nacelle_length * nacelle_height * 2 +
nacelle_width * nacelle_height * 2
)
return (
0.6724 *
K_ng *
(nacelle_length / u.foot) ** 0.10 *
(nacelle_width / u.foot) ** 0.294 *
(ultimate_load_factor) ** 0.119 *
(mass_per_engine_with_contents / u.lbm) ** 0.611 *
(n_engines) ** 0.984 *
(nacelle_wetted_area / u.foot ** 2) ** 0.224 *
(advanced_composites["fuselage/nacelle"] if use_advanced_composites else 1)
)
[docs]def mass_engine_controls(
n_engines: int,
cockpit_to_engine_length: float,
) -> float:
"""
Computes the mass of the engine controls for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach.
Args:
n_engines: The number of engines in the aircraft.
cockpit_to_engine_length: The distance from the cockpit to the engine [m].
Returns:
The mass of the engine controls [kg].
"""
return (
5 * n_engines +
0.80 * (cockpit_to_engine_length / u.foot) * n_engines
) * u.lbm
[docs]def mass_starter(
n_engines: int,
mass_per_engine: float,
) -> float:
"""
Computes the mass of the engine starter for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach.
Args:
n_engines: The number of engines in the aircraft.
mass_per_engine: The mass of the engine [kg].
Returns:
The mass of the engine starter [kg].
"""
return (
49.19 * (
mass_per_engine / u.lbm * n_engines
/ 1000
) ** 0.541
) * u.lbm
[docs]def mass_fuel_system(
fuel_volume: float,
n_tanks: int,
fraction_in_integral_tanks: float = 0.5,
) -> float:
"""
Computes the mass of the fuel system (e.g., tanks, pumps, but not the fuel itself) for a cargo/transport
aircraft, according to Raymer's Aircraft Design: A Conceptual Approach.
Args:
fuel_volume: The volume of fuel in the aircraft [m^3].
n_tanks: The number of fuel tanks in the aircraft.
fraction_in_integral_tanks: The fraction of the fuel volume that is in integral tanks, as opposed to
protected tanks.
Returns:
The mass of the fuel system [kg].
"""
fraction_in_protected_tanks = 1 - fraction_in_integral_tanks
return (
2.405 *
(fuel_volume / u.gallon) ** 0.606 *
(1 + fraction_in_integral_tanks) ** -1 *
(1 + fraction_in_protected_tanks) *
n_tanks ** 0.5
) * u.lbm
[docs]def mass_flight_controls(
airplane: asb.Airplane,
aircraft_Iyy: float,
fraction_of_mechanical_controls: int = 0,
) -> float:
"""
Computes the added mass of the flight control surfaces (and any applicable linkages, in the case of mechanical
controls) for a cargo/transport aircraft, according to Raymer's Aircraft Design: A Conceptual Approach.
Args:
airplane: The airplane to calculate the mass of the flight controls for.
aircraft_Iyy: The moment of inertia of the aircraft about the y-axis.
fraction_of_mechanical_controls: The fraction of the flight controls that are mechanical, as opposed to
hydraulic.
Returns:
The mass of the flight controls [kg].
"""
### Compute how many functions the control surfaces are performing (e.g., aileron, elevator, flap, rudder, etc.)
N_functions_performed_by_controls = 0
for wing in airplane.wings:
N_functions_performed_by_controls += len(wing.get_control_surface_names())
### Compute the control surface area
control_surface_area = 0
for wing in airplane.wings:
control_surface_area += wing.control_surface_area()
return (
145.9 *
N_functions_performed_by_controls ** 0.554 * # number of functions performed by controls
(1 + fraction_of_mechanical_controls) ** -1 *
(control_surface_area / u.foot ** 2) ** 0.20 *
(aircraft_Iyy / (u.lbm * u.foot ** 2) * 1e-6) ** 0.07
) * u.lbm
[docs]def mass_APU(
mass_APU_uninstalled: float,
):
"""
Computes the mass of the auxiliary power unit (APU) for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach.
Args:
mass_APU_uninstalled: The mass of the APU uninstalled [kg].
Returns:
The mass of the APU, as installed [kg].
"""
return 2.2 * mass_APU_uninstalled
[docs]def mass_instruments(
fuselage: asb.Fuselage,
main_wing: asb.Wing,
n_engines: int,
n_crew: Union[int, float],
engine_is_reciprocating: bool = False,
engine_is_turboprop: bool = False,
):
"""
Computes the mass of the flight instruments for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach.
Args:
fuselage: The fuselage of the airplane.
main_wing: The main wing of the airplane.
n_engines: The number of engines on the airplane.
n_crew: The number of crew members on the airplane. Use 0.5 for a UAV.
engine_is_reciprocating: Whether the engine is reciprocating.
engine_is_turboprop: Whether the engine is a turboprop.
Returns:
The mass of the instruments [kg]
"""
K_r = 1.133 if engine_is_reciprocating else 1
K_tp = 0.793 if engine_is_turboprop else 1
return (
4.509 *
K_r *
K_tp *
n_crew ** 0.541 *
n_engines *
(fuselage.length() / u.foot * main_wing.span() / u.foot) ** 0.5
) * u.lbm
[docs]def mass_hydraulics(
airplane: asb.Airplane,
fuselage: asb.Fuselage,
main_wing: asb.Wing,
):
"""
Computes the mass of the hydraulic system for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach.
Args:
airplane: The airplane to calculate the mass of the hydraulic system for.
fuselage: The fuselage of the airplane.
main_wing: The main wing of the airplane.
Returns:
The mass of the hydraulic system [kg].
"""
N_functions_performed_by_controls = 0
for wing in airplane.wings:
N_functions_performed_by_controls += len(wing.get_control_surface_names())
return (
0.2673 *
N_functions_performed_by_controls *
(fuselage.length() / u.foot * main_wing.span() / u.foot) ** 0.937
) * u.lbm
[docs]def mass_electrical(
system_electrical_power_rating: float,
electrical_routing_distance: float,
n_engines: int,
):
"""
Computes the mass of the electrical system for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach.
Args:
system_electrical_power_rating: The total electrical power rating of the aircraft's electrical system [Watts].
Typical values:
* Transport airplane: 40,000 - 60,000 W
* Fighter/bomber airplane: 110,000 - 160,000 W
electrical_routing_distance: The electrical routing distance, generators to avionics to cockpit. [meters]
Returns:
The mass of the electrical system [kg].
"""
return (
7.291 *
(system_electrical_power_rating / 1e3) ** 0.782 *
(electrical_routing_distance / u.foot) ** 0.346 *
(n_engines) ** 0.10
) * u.lbm
[docs]def mass_avionics(
mass_uninstalled_avionics: float,
):
"""
Computes the mass of the avionics for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach.
Args:
mass_uninstalled_avionics: The mass of the avionics, before installation [kg].
Returns:
The mass of the avionics, as installed [kg].
"""
return (
1.73 *
(mass_uninstalled_avionics / u.lbm) ** 0.983
) * u.lbm
[docs]def mass_furnishings(
n_crew: Union[int, float],
mass_cargo: float,
fuselage: asb.Fuselage,
):
"""
Computes the mass of the furnishings for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach. Does not include cargo handling gear or seats.
Args:
n_crew: The number of crew members on the airplane. Use 0.5 for a UAV.
mass_cargo: The mass of the cargo [kg].
fuselage: The fuselage of the airplane.
Returns:
The mass of the furnishings [kg].
"""
return (
0.0577 *
n_crew ** 0.1 *
(mass_cargo / u.lbm) ** 0.393 *
(fuselage.area_wetted() / u.foot ** 2) ** 0.75
) * u.lbm
[docs]def mass_air_conditioning(
n_crew: int,
n_pax: int,
volume_pressurized: float,
mass_uninstalled_avionics: float,
):
"""
Computes the mass of the air conditioning system for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach.
Args:
n_crew: The number of crew members on the airplane.
n_pax: The number of passengers on the airplane.
volume_pressurized: The volume of the pressurized cabin [meters^3].
mass_uninstalled_avionics: The mass of the avionics, before installation [kg].
Returns:
The mass of the air conditioning system [kg].
"""
return (
62.36 *
(n_crew + n_pax) ** 0.25 *
(volume_pressurized / u.foot ** 3 / 1e3) ** 0.604 *
(mass_uninstalled_avionics / u.lbm) ** 0.10
) * u.lbm
[docs]def mass_anti_ice(
design_mass_TOGW: float,
):
"""
Computes the mass of the anti-ice system for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach.
Args:
design_mass_TOGW: The design takeoff gross weight of the entire airplane [kg].
Returns:
The mass of the anti-ice system [kg].
"""
return 0.002 * design_mass_TOGW
[docs]def mass_handling_gear(
design_mass_TOGW: float,
):
"""
Computes the mass of the handling gear for a cargo/transport aircraft, according to Raymer's Aircraft
Design: A Conceptual Approach.
Args:
design_mass_TOGW: The design takeoff gross weight of the entire airplane [kg].
Returns:
The mass of the handling gear [kg].
"""
return 3e-4 * design_mass_TOGW
[docs]def mass_military_cargo_handling_system(
cargo_floor_area: float,
):
"""
Computes the mass of the military cargo handling system for a cargo/transport aircraft, according to Raymer's
Aircraft Design: A Conceptual Approach.
Args:
cargo_floor_area: The floor area of the cargo compartment [meters^2].
Returns:
The mass of the military cargo handling system [kg].
"""
return (
2.4 *
(cargo_floor_area / u.foot ** 2)
) * u.lbm