Source code for RCAIDE.Library.Mission.Common.Pre_Process.mass_properties

# RCAIDE/Library/Missions/Common/Pre_Process/mass_properties.py
# 
# 
# Created: Mar 2025, M. Clarke
# Modified: Aug 2025 S. Shekar, Jan 2026 A. Molloy

# ----------------------------------------------------------------------------------------------------------------------
#  RCAIDE
# ---------------------------------------------------------------------------------------------------------------------- 
from copy import deepcopy
import RCAIDE 
from RCAIDE.Library.Methods.Mass_Properties.Moment_of_Inertia  import compute_vehicle_moment_of_inertia
from RCAIDE.Library.Methods.Mass_Properties.Center_of_Gravity  import compute_vehicle_center_of_gravity 
from RCAIDE.Library.Methods.Powertrain.Sources.Fuel_Tanks.compute_fuel_mass import compute_fuel_mass
from RCAIDE.Library.Mission.Common.Pre_Process.mass_properties_correction_factors   import apply_correction_factors, apply_component_weights
from RCAIDE.Library.Mission.Common.Pre_Process.mass_properties_report import print_mass_report, write_mass_report   
from scipy.optimize import brentq, minimize_scalar
import numpy as np
import pandas as pd 

# ----------------------------------------------------------------------------------------------------------------------
#  mass_properties
# ----------------------------------------------------------------------------------------------------------------------  
[docs] def mass_properties(mission): """Calculate and update mass properties for all mission segments. Performs weight analysis, center of gravity computation, and moment of inertia calculations for each mission segment. Handles multiple analysis scenarios including user-defined weights, MTOW-based calculations, and iterative weight convergence. Parameters ---------- mission : RCAIDE.Framework.Mission Mission object containing segments with weight analysis requirements. Raises ------ AttributeError If max_takeoff weight is not defined. AssertionError If payload exceeds max_payload or fuel exceeds max_fuel. Notes ----- The function operates in three main modes: 1. **Pre-defined weights**: Uses existing takeoff weight if provided 2. **Simple MTOW**: Falls back to MTOW if aircraft_type undefined 3. **Full analysis**: Performs complete weight buildup with iterations, if the setting update_takeoff_weight is True then it updates the aircraft takeoff weight with the new one based on the buildup. otherwise the takeoff weight is not adjusted To complete a weight breakdown the methods require an aircraft to have the following defined: MTOW, payload and/or fuel weight, aircraft method type (included in the weights analysis type most times), max fuel and max zero fuel weights or neither. If the user desires to have the takeoff weight calculated by the weight breakdown used for further analyses then specify under the weight analysis settings "update_takeoff_weight == True". Algorithm Flow ~~~~~~~~~~~~~~ .. code-block:: text For each segment: ├── Check if weights analysis exists ├── Validate MTOW defined (required) ├── If aircraft_type undefined → Use MTOW for takeoff weight ├── Else if no payload/fuel → Use MTOW ├── Else → Perform weight analysis: │ ├── Check payload/fuel limits │ ├── If max_fuel/max_zero_fuel undefined: │ │ └── Iterate to convergence (max 100 iterations) │ │ ├── Initial guess from regression │ │ ├── Evaluate weights │ │ ├── Apply corrections │ │ └── Check convergence (<10 kg residual) │ ├── Single evaluation | └── Apply correction factors if specified ├── Update takeoff weight if requested ├── Update CG if requested ├── Update MOI if requested └── Copy vehicle to aerodynamics Weight Equations ~~~~~~~~~~~~~~~~ .. math:: W_{takeoff} = W_{OEW} + W_{payload} + W_{fuel} W_{OEW} = W_{empty} + W_{operational} W_{max\\_zero\\_fuel} = W_{OEW} + W_{max\\_payload} W_{max\\_fuel} = W_{MTOW} - W_{OEW} - W_{min\\_payload} Initial Regression Estimates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When max_fuel or max_zero_fuel undefined: .. math:: W_{max\\_fuel}^{(0)} = 0.477 \\cdot W_{MTOW} - 13455 W_{max\\_zero\\_fuel}^{(0)} = 0.6269 \\cdot W_{MTOW} + 20505 """ for i , segment in enumerate(mission.segments): if segment.analyses.weights == None: raise AssertionError('Define weights analysis method') else: if i ==0 or segment.analyses.geometry.settings.unique_geometry: mass_properties_preprocess_routine(segment, i) else: analyses = segment.analyses weights_analysis = analyses.weights vehicle_1 = deepcopy(mission.segments[i-1].analyses.vehicle) segment.analyses.vehicle.mass_properties = vehicle_1.mass_properties weights_analysis.settings.iterate_mtow = False mass_properties_preprocess_routine(segment, i) return
[docs] def mass_properties_preprocess_routine(segment, i = 0): analyses = segment.analyses weights_analysis = analyses.weights # --------------------------------------------------------------------------------------------------------------------------- # STEP 1: Compute fuel mass and max fuel mass for the vehicle based on the fuel tanks defined in the vehicle # --------------------------------------------------------------------------------------------------------------------------- update_max_fuel_mass = weights_analysis.settings.update_max_fuel_mass update_fuel_mass = weights_analysis.settings.update_fuel_mass compute_fuel_mass(analyses.vehicle, update_fuel_mass, update_max_fuel_mass) # --------------------------------------------------------------------------------------------------------------------------- # STEP 2: Pre-checks for weights analysis # --------------------------------------------------------------------------------------------------------------------------- if analyses.vehicle.mass_properties.max_takeoff == None: # For all weights analysis a maximum take off weight needs to be defined by the user raise AttributeError("Max Takeoff Weight for aircraft not defined") # orig_takeoff_weight = analyses.vehicle.mass_properties.takeoff if weights_analysis.aircraft_type == None: # If an aircraft type is not defined analysis cannot be performed, and the maximum take off weight will be assumed to be the takeoff weight print('\n Warning: Weight Analysis type not defined') if analyses.vehicle.mass_properties.takeoff == None: print('\n Using Maximum Takeoff Weight') analyses.vehicle.mass_properties.takeoff = analyses.vehicle.mass_properties.max_takeoff elif analyses.vehicle.mass_properties.takeoff == None and analyses.vehicle.mass_properties.payload == None and analyses.vehicle.mass_properties.fuel == None: # Without either fuel on board or payload on board, takeoff weight cannot be computed thus the takeoff weight is assumed to be max takeoff print('Warning: Payload or fuel weight not defined; assuming takeoff weight is MTOW') analyses.vehicle.mass_properties.takeoff = analyses.vehicle.mass_properties.max_takeoff elif weights_analysis.settings.run_weights_analysis: # --------------------------------------------------------------------------------------------------------------------------- # STEP 3: Run weights analysis # --------------------------------------------------------------------------------------------------------------------------- if i == 0 and analyses.vehicle.mass_properties.payload > analyses.vehicle.mass_properties.max_payload: print('Warning: Prescribed payload weight is greater than maximum payload weight') if weights_analysis.settings.iterate_mtow: solve_for_mtow(analyses, weights_analysis, i) else: if analyses.vehicle.mass_properties.max_zero_fuel == None: # Before proceeding to the weight buildups, the buildups need either the max fuel capacity or the max zero fuel to compute OEW if i == 0: print('\n Warning: Max Fuel or Max Zero Fuel not defined. Iterating to find these values.') iterate_max_fuel_and_max_zero_fuel(analyses) _ = weights_analysis.evaluate(analyses.vehicle) if i == 0 and analyses.vehicle.mass_properties.payload > analyses.vehicle.mass_properties.max_payload: print('Warning: Computed payload weight is greater than maximum payload weight') # Compute OEW if weights_analysis.settings.overwrite_operating_empty_weight: analyses.vehicle.mass_properties.operating_empty = analyses.vehicle.mass_properties.weight_breakdown.empty.total # Apply correction factors if any apply_correction_factors(analyses) if i == 0: apply_component_weights(analyses) if (analyses.vehicle.mass_properties.fuel == 0 or analyses.vehicle.mass_properties.fuel is None) and weights_analysis.propulsion_architecture != 'Electric': ('Fuel Weight for the mission is not defned. Filling up the airplace till max takeoff weight') analyses.vehicle.mass_properties.fuel = analyses.vehicle.mass_properties.max_takeoff- (analyses.vehicle.mass_properties.operating_empty + analyses.vehicle.mass_properties.payload) # Compute takeoff weight and max zero fuel weight if analyses.vehicle.mass_properties.takeoff == None: analyses.vehicle.mass_properties.takeoff = analyses.vehicle.mass_properties.operating_empty + analyses.vehicle.mass_properties.payload+ analyses.vehicle.mass_properties.fuel analyses.vehicle.mass_properties.max_zero_fuel = analyses.vehicle.mass_properties.operating_empty\ + analyses.vehicle.mass_properties.max_payload # --------------------------------------------------------------------------------------------------------------------------- # STEP 4: Print weight statements and apply weight factors # --------------------------------------------------------------------------------------------------------------------------- if i == 0: if weights_analysis.print_weight_analysis_report and type(weights_analysis) != RCAIDE.Framework.Analyses.Weights.Weights: print_mass_report(analyses) if weights_analysis.settings.write_mass_properties: excel_filename = write_mass_report(analyses) # --------------------------------------------------------------------------------------------------------------------------- # STEP 5: Compute Center of Gravity # --------------------------------------------------------------------------------------------------------------------------- if weights_analysis.settings.run_center_of_gravity_analysis: centre_of_gravity_df = pd.DataFrame(columns=["Component", "Mass (kg)", "CG x (m)", "CG y (m)", "CG z (m)" ]) if i != 0: verbose_flag = False else: verbose_flag = weights_analysis.print_weight_analysis_report _ ,_, _, centre_of_gravity_df = compute_vehicle_center_of_gravity(analyses.vehicle,centre_of_gravity_df, overwrite_center_of_gravity = weights_analysis.settings.run_center_of_gravity_analysis , segment=segment, verbose=verbose_flag) if i==0 and weights_analysis.settings.write_mass_properties: # Centre of Gravity sheet with pd.ExcelWriter(excel_filename, engine="openpyxl",mode="a",if_sheet_exists="replace") as writer: centre_of_gravity_df.to_excel(writer,sheet_name="Centre of Gravity",index=False) print(f"CG breakdown written to Excel:\n {excel_filename}") analyses.vehicle.mass_properties.center_of_gravity_breakdown = centre_of_gravity_df # --------------------------------------------------------------------------------------------------------------------------- # STEP 6: Compute Moment of Inertia # --------------------------------------------------------------------------------------------------------------------------- if weights_analysis.settings.run_moments_of_inertia_analysis: moment_of_inertia_df = pd.DataFrame(columns=["Component", "Mass (kg)","Ixx (kg·m²)","Iyy (kg·m²)","Izz (kg·m²)","Ixy (kg·m²)","Ixz (kg·m²)","Iyz (kg·m²)", ]) overwrite_MOI = False tensor = analyses.vehicle.mass_properties.moments_of_inertia.tensor if np.all(tensor == 0): overwrite_MOI = True if i != 0: verbose_flag = False else: verbose_flag = weights_analysis.print_weight_analysis_report _ ,moment_of_inertia_df = compute_vehicle_moment_of_inertia(analyses.vehicle,moment_of_inertia_df, overwrite_moment_of_intertia = overwrite_MOI, segment=segment, verbose=verbose_flag) if i==0 and weights_analysis.settings.write_mass_properties: # Moment of Inertia sheet with pd.ExcelWriter(excel_filename, engine="openpyxl",mode="a",if_sheet_exists="replace") as writer: moment_of_inertia_df.to_excel(writer,sheet_name="Moment of Inertia",index=False) print(f"MOI breakdown written to Excel:\n {excel_filename}")
[docs] def iterate_max_fuel_and_max_zero_fuel(analyses, max_iterations=100): # Inital guess for max fuel and max zero fuel based on regressional analysis which use max takeoff weight of the aircraft compute_max_fuel = False if analyses.vehicle.mass_properties.max_fuel == None: analyses.vehicle.mass_properties.max_fuel = 0.477*analyses.vehicle.mass_properties.max_takeoff -13455 compute_max_fuel = True analyses.vehicle.mass_properties.max_zero_fuel = 0.6269*analyses.vehicle.mass_properties.max_takeoff + 20505 iteration = 0 # Convergence loop while iteration < max_iterations: # Run weights analysis ! _ = analyses.weights.evaluate(analyses.vehicle) # Compute OEW if analyses.weights.settings.overwrite_operating_empty_weight: analyses.vehicle.mass_properties.operating_empty = analyses.vehicle.mass_properties.weight_breakdown.empty.total # Apply Correction Factors if any apply_correction_factors(analyses) apply_component_weights(analyses) analyses.vehicle.mass_properties.takeoff = analyses.vehicle.mass_properties.operating_empty + analyses.vehicle.mass_properties.payload + analyses.vehicle.mass_properties.fuel mew_max_zero_fuel = analyses.vehicle.mass_properties.operating_empty + analyses.vehicle.mass_properties.max_payload residual_max_zero_fuel = abs(mew_max_zero_fuel - analyses.vehicle.mass_properties.max_zero_fuel) analyses.vehicle.mass_properties.max_zero_fuel = mew_max_zero_fuel residual_max_fuel = 0 if compute_max_fuel: new_max_fuel = analyses.vehicle.mass_properties.max_takeoff - analyses.vehicle.mass_properties.operating_empty - analyses.vehicle.mass_properties.min_payload fuel_density = next((ft.fuel.density for network in analyses.vehicle.networks for fl in network.fuel_lines for ft in fl.fuel_tanks), None) if fuel_density is not None: max_fuel_by_volume = analyses.vehicle.volume_properties.max_fuel * fuel_density new_max_fuel = min(new_max_fuel, max_fuel_by_volume) residual_max_fuel = abs(new_max_fuel - analyses.vehicle.mass_properties.max_fuel) analyses.vehicle.mass_properties.max_fuel = new_max_fuel iteration += 1 if residual_max_fuel < 1 and residual_max_zero_fuel <1: break else: analyses.vehicle.mass_properties.max_zero_fuel += residual_max_zero_fuel * 0.1 if compute_max_fuel: analyses.vehicle.mass_properties.max_fuel += residual_max_fuel * 0.1 return
[docs] def solve_for_mtow(analyses, weights_analysis, i): """Solves for MTOW that satisfies the target capacity fraction. The MTOW capacity fraction is MTOW / (OEW + Max Fuel + Max Payload). Uses brentq for superlinear convergence instead of fixed-point iteration. """ target_fraction = weights_analysis.settings.mtow_capacity_fraction max_zero_fuel_flag = analyses.vehicle.mass_properties.max_zero_fuel is None if max_zero_fuel_flag and i == 0: print('\n Warning: Max Fuel or Max Zero Fuel not defined. Iterating to find these values.') def _mtow_residual(mtow): analyses.vehicle.mass_properties.max_takeoff = mtow if max_zero_fuel_flag: analyses.vehicle.mass_properties.max_zero_fuel = None iterate_max_fuel_and_max_zero_fuel(analyses) _ = weights_analysis.evaluate(analyses.vehicle) analyses.vehicle.mass_properties.operating_empty = analyses.vehicle.mass_properties.weight_breakdown.empty.total apply_correction_factors(analyses) if i == 0: apply_component_weights(analyses) oew = analyses.vehicle.mass_properties.operating_empty max_fuel = analyses.vehicle.mass_properties.max_fuel max_payload = analyses.vehicle.mass_properties.max_payload return target_fraction - mtow / (oew + max_fuel + max_payload) mtow_0 = analyses.vehicle.mass_properties.max_takeoff try: mtow_converged = brentq(_mtow_residual, 0.5 * mtow_0, 1.5 * mtow_0, xtol=1.0) except ValueError: res = minimize_scalar(lambda m: _mtow_residual(m)**2, bounds=(0.5 * mtow_0, 1.5 * mtow_0), method='bounded') mtow_converged = float(res.x) # Final evaluation to leave vehicle in correct state _mtow_residual(mtow_converged) analyses.vehicle.mass_properties.max_takeoff = mtow_converged return