# RCAIDE/Library/Methods/Performance/compute_payload_range_diagram.py
#
#
# Created: Jul 2023, M. Clarke
# ----------------------------------------------------------------------------------------------------------------------
# IMPORT
# ----------------------------------------------------------------------------------------------------------------------
# RCAIDE imports
import RCAIDE
from RCAIDE.Framework.Core import Units , Data
from RCAIDE.Library.Mission.Common.Pre_Process import mass_properties,geometry
# Pacakge imports
import numpy as np
import os,sys
# ----------------------------------------------------------------------
# Calculate vehicle Payload Range Diagram
# ----------------------------------------------------------------------
[docs]
def compute_payload_range_diagram(mission = None, cruise_segment_tag = "cruise", fuel_reserve_percentage=0.05):
"""
Calculate and plot the payload range diagram for an aircraft by modifying the cruise segment and weights.
Parameters
----------
mission : Data
Data structure containing the mission to be analyzed
cruise_segment_tag : str, optional
String identifier for the cruise segment in the mission
Default: "cruise"
fuel_reserve_percentage : float, optional
Fraction of maximum fuel to be reserved (not used for range)
Default: 0.0
plot_diagram : bool, optional
Flag to generate payload-range plots
Default: True
fuel_name : str, optional
Name of fuel for plot title
Default: None
Returns
-------
payload_range : Data
Data structure containing payload range properties
- range : ndarray
Range values for each point [m]
- payload : ndarray
Payload values for each point [kg]
- oew_plus_payload : ndarray
Operating empty weight plus payload for each point [kg]
- fuel : ndarray
Fuel weight for each point [kg]
- takeoff_weight : ndarray
Takeoff weight for each point [kg]
- fuel_reserve_percentage : float
Fraction of fuel reserved
Notes
-----
Computes three key points for conventional aircraft:
1. Maximum payload at maximum takeoff weight
2. Maximum fuel with maximum takeoff weight
3. Maximum fuel with zero payload (ferry range)
For electric aircraft computes:
1. Maximum payload range
2. Ferry range (zero payload)
**Major Assumptions**
* Constant cruise speed and altitude
* Fixed reserve fuel fraction
* Linear interpolation between payload-range points
* Battery energy content remains constant (electric aircraft)
**Theory**
The payload-range diagram shows the trade-off between how much payload an aircraft
can carry versus how far it can fly. For conventional aircraft, the diagram typically
has three segments:
1. Maximum payload segment: Range increases by burning fuel initially loaded
2. Maximum fuel segment: Range increases by trading payload for fuel
3. Ferry range segment: Maximum range with zero payload
For electric aircraft, the diagram is simpler with just two points connected by
a straight line, as there is no fuel weight to trade for payload.
See Also
--------
RCAIDE.Library.Methods.Performance.conventional_payload_range_diagram
RCAIDE.Library.Methods.Performance.electric_payload_range_diagram
"""
mission.tag = "payload_range_mission"
initial_segment = list(mission.segments.keys())[0]
# remove takeoff weight from aircraft if defined
geometry(mission)
for segment in mission.segments:
segment.analyses.vehicle.mass_properties.takeoff = None
segment.analyses.weights.print_weight_analysis_report = True
segment.analyses.aerodynamics.settings.store_training_data = True
segment.analyses.aerodynamics.settings.reuse_training_data = False
segment.analyses.aerodynamics.settings.use_surrogate = True
# if stability analysis is defined and neutral point already computed
if segment.analyses.stability != None:
segment.analyses.stability.settings.compute_neutral_point = False
# run preliminary mass properties analyss
mass_properties(mission)
# run payload range analysis
vehicle = mission.segments[initial_segment].analyses.vehicle
for network in vehicle.networks:
if type(network) == RCAIDE.Framework.Networks.Fuel:
payload_range = conventional_payload_range_diagram(vehicle,mission,cruise_segment_tag,fuel_reserve_percentage)
else:
payload_range = electric_payload_range_diagram(vehicle,mission,cruise_segment_tag)
# delete aerodynamic surrogates
file_name = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), segment.analyses.vehicle.tag + "_" + segment.analyses.aerodynamics.tag + "_aero_training_data.pkl")
os.remove(file_name)
print("\n============== Payload Range Report ==============\n")
try:
reserve_pct = payload_range.pop('fuel_reserve_percentage')
print("\nFuel Reserve Percentage:", f"{reserve_pct * 100:.0f}%")
except: pass
keys = list(payload_range.keys())
values = [payload_range[key] for key in keys]
# Header
print(f"{'Parameter':<20} {'Point 1':>12} {'Point 2':>12} {'Point 3':>12} {'Point 4':>12}")
print("-" * 70)
# Rows
for key, val in zip(keys, values):
if key == 'range':
row = f"{'Range [nmi]':<20}"
val = [v / Units.nmi for v in val]
elif key == 'payload':
row = f"{'Payload [kg]':<20}"
elif key == 'oew_plus_payload':
row = f"{'OEW + Payload [kg]':<20}"
elif key == 'fuel':
row = f"{'Fuel [kg]':<20}"
elif key == 'takeoff_weight':
row = f"{'TO Weight [kg]':<20}"
else:
row = f"{key:<20}"
row += "".join([f"{v:12.2f}" for v in val])
print(row)
print("\n===============================\n")
return payload_range
[docs]
def conventional_payload_range_diagram(vehicle,mission,cruise_segment_tag,fuel_reserve_percentage):
"""Calculates and plots the payload range diagram for a fuel-bases aircraft by modifying the
cruise segment range and weights of the aicraft .
Sources:
N/A
Assumptions:
None
Inputs:
vehicle data structure for aircraft [-]
mission data structure for mission [-]
cruise_segment_tag string of cruise segment [string]
fuel_reserve_percentage reserve fuel [unitless]
Outputs:
payload_range data structure of payload range properties [m/s]
"""
# unpack
mass = vehicle.mass_properties
if not mass.max_payload:
raise AttributeError("Error calculating Payload Range Diagram: vehicle Maximum Payload Weight is undefined.")
else:
MaxPLD = mass.max_payload
if not mass.operating_empty:
if not mass.max_zero_fuel:
raise AttributeError("Error calculating Payload Range Diagram: vehicle Operating Empty Weight and Max Zero Fuel Weight is undefined.")
else:
OEW = mass.max_zero_fuel - MaxPLD
MZFW = mass.max_zero_fuel
else:
OEW = mass.operating_empty
if not mass.max_zero_fuel:
MZFW = OEW + MaxPLD
else:
MZFW = mass.max_zero_fuel
if not mass.max_takeoff:
raise AttributeError("Error calculating Payload Range Diagram: Vehicle MTOW not defined")
else:
MTOW = vehicle.mass_properties.max_takeoff
if mass.max_payload == 0:
MaxPLD = MZFW - OEW
else:
MaxPLD = vehicle.mass_properties.max_payload
MaxPLD = min(MaxPLD , MZFW - OEW) #limit in structural capability
if mass.max_fuel == 0:
MaxFuel = MTOW - OEW # If not defined, calculate based in design weights
else:
MaxFuel = vehicle.mass_properties.max_fuel # If max fuel capacity not defined
MaxFuel = min(MaxFuel, MTOW - OEW)
# Define payload range points
#Point = [ RANGE WITH MAX. PLD , RANGE WITH MAX. FUEL , FERRY RANGE ]
TOW = [ MTOW , MTOW , OEW + MaxFuel ]
FUEL = [ min(TOW[0] - OEW - MaxPLD,MaxFuel) , MaxFuel , MaxFuel ]
PLD = [ MaxPLD , MTOW - MaxFuel - OEW , 0. ]
OEW_PLD = [ OEW + MaxPLD , MTOW - MaxFuel , OEW ]
# allocating Range array
R = [0,0,0]
for segment in mission.segments:
segment.analyses.weights.settings.run_weights_analysis = False
segment.analyses.weights.settings.run_center_of_gravity_analysis = False
segment.analyses.weights.settings.run_moments_of_inertia_analysis = False
segment.analyses.weights.settings.update_fuel_mass = False
segment.analyses.weights.settings.update_max_fuel_mass = False
segment.analyses.geometry.settings.compute_fuel_volume = False
# loop for each point of Payload Range Diagram
for i in range(len(TOW)):
# Define takeoff weight
mission.segments[0].analyses.vehicle.mass_properties.takeoff = TOW[i]
mission.segments[0].analyses.vehicle.mass_properties.payload = PLD[i]
mission.segments[0].analyses.vehicle.mass_properties.fuel = FUEL[i]
# Evaluate mission with current TOW
results = mission.evaluate()
segment = results.segments[cruise_segment_tag]
for segment in mission.segments:
segment.analyses.aerodynamics.settings.reuse_training_data = True
# Neutral point is now computed — disable recomputation for speed
if segment.analyses.stability != None:
segment.analyses.stability.settings.compute_neutral_point = False
# Distance convergency in order to have total fuel equal to target fuel
# User don't have the option of run a mission for a given fuel. So, we
# have to iterate distance in order to have total fuel equal to target fuel
maxIter = 10 # maximum iteration limit
tol = 1. # fuel convergency tolerance
err = 9999. # error to be minimized
iter = 0 # iteration count
while abs(err) > tol and iter < maxIter:
iter = iter + 1
# Current total fuel burned in mission
TotalFuel = results.segments[-1].conditions.energy.cumulative_fuel_consumption[-1, 0]
# Difference between burned fuel and target fuel
reserve_fuel = fuel_reserve_percentage * MaxFuel
missingFuel = FUEL[i] - TotalFuel - reserve_fuel
# Current distance and fuel consuption in the cruise segment
CruiseDist = np.diff( segment.conditions.frames.inertial.position_vector[[0,-1],0] )[0] # Distance [m]
CruiseFuel = segment.conditions.weights.vehicle.mass[0,0] - segment.conditions.weights.vehicle.mass[-1,0] # [kg]
# Current specific range (m/kg)
CruiseSR = CruiseDist / CruiseFuel # [m/kg]
# Estimated distance that will result in total fuel burn = target fuel
DeltaDist = CruiseSR * missingFuel
mission.segments[cruise_segment_tag].distance = (CruiseDist + DeltaDist)
# running mission with new distance
results = mission.evaluate()
segment = results.segments[cruise_segment_tag]
# Difference between burned fuel and target fuel
err = ( TOW[i] - results.segments[-1].conditions.weights.vehicle.mass[-1,0] ) - FUEL[i] + reserve_fuel
if iter == maxIter:
print(f"Did not converge.")
break
if (CruiseDist + DeltaDist) <=0: # This raise exception can be reworked but it is good to have this here
raise Exception('Negative Cruise distance not enough fuel for fixed portions of flight (To -> Climb)')
# Allocating resulting range in ouput array.
R[i] = results.segments[-1].conditions.frames.inertial.position_vector[-1,0]
# Inserting point (0,0) in output arrays
R.insert(0,0)
PLD.insert(0,MaxPLD)
OEW_PLD.insert(0,OEW + MaxPLD )
FUEL.insert(0,0)
TOW.insert(0,0)
# packing results
payload_range = Data()
payload_range.range = np.array(R)
payload_range.payload = np.array(PLD)
payload_range.oew_plus_payload = np.array(OEW_PLD)
payload_range.fuel = np.array(FUEL)
payload_range.takeoff_weight = np.array(TOW)
payload_range.fuel_reserve_percentage = fuel_reserve_percentage
return payload_range
[docs]
def electric_payload_range_diagram(vehicle,mission,cruise_segment_tag):
"""Calculates and plots the payload range diagram for an electric aircraft by modifying the
cruise segment distance and payload weight of the aicraft .
Sources:
N/A
Assumptions:
None
Inputs:
vehicle data structure for aircraft [-]
mission data structure for mission [-]
cruise_segment_tag string of cruise segment [string]
fuel_reserve_percentage reserve fuel [unitless]
Outputs:
payload_range data structure of payload range properties [m/s]
"""
mass = vehicle.mass_properties
if not mass.max_payload:
raise AttributeError("Error calculating Payload Range Diagram: vehicle Maximum Payload Weight is undefined.")
else:
MaxPLD = mass.max_payload
if not mass.operating_empty:
if not mass.max_zero_fuel:
raise AttributeError("Error calculating Payload Range Diagram: vehicle Operating Empty Weight is undefined.")
else:
OEW = mass.max_zero_fuel - MaxPLD
else:
OEW = mass.operating_empty
if not mass.max_takeoff:
raise AttributeError("Error calculating Payload Range Diagram: vehicle Maximum Payload Weight is undefined.")
else:
MTOW = mass.max_takeoff
# Define Diagram Points
# Point = [Value at Maximum Payload Range, Value at Ferry Range]
TOW = [MTOW, OEW] # Takeoff Weights
PLD = [MaxPLD, 0.] # Payload Weights
# Initialize Range Array
R = np.zeros(2)
# Calculate Vehicle Range for Max Payload and Ferry Conditions
for i in range(2):
mission.segments[0].analyses.vehicle.mass_properties.takeoff = TOW[i]
results = mission.evaluate()
segment = results.segments[cruise_segment_tag]
R[i] = segment.conditions.frames.inertial.position_vector[-1,0]
# Insert Starting Point for Diagram Construction
R = np.insert(R, 0, 0)
PLD = np.insert(PLD, 0, MaxPLD)
TOW = np.insert(TOW, 0, 0)
# Pack Results
payload_range = Data()
payload_range.range = np.array(R)
payload_range.payload = np.array(PLD)
payload_range.takeoff_weight = np.array(TOW)
return payload_range