# 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.Plots.Common import set_axes, plot_style
# Pacakge imports
import numpy as np
from matplotlib import pyplot as plt
# ----------------------------------------------------------------------
# Calculate vehicle Payload Range Diagram
# ----------------------------------------------------------------------
[docs]
def compute_payload_range_diagram(mission = None, cruise_segment_tag = "cruise", fuel_reserve_percentage=0., plot_diagram = True, fuel_name=None):
"""
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
"""
if mission == None:
raise AssertionError('Mission not specifed!')
initial_segment = list(mission.segments.keys())[0]
# perform inital weights analysis
weights_analysis = mission.segments[initial_segment].analyses.weights
weights_analysis.evaluate() # evaluate weights to make sure mass variables are defined
vehicle = weights_analysis.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,plot_diagram,fuel_name)
else:
payload_range = electric_payload_range_diagram(vehicle,mission,cruise_segment_tag,plot_diagram)
return payload_range
[docs]
def conventional_payload_range_diagram(vehicle,mission,cruise_segment_tag,fuel_reserve_percentage,plot_diagram, fuel_name):
"""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.operating_empty:
raise AttributeError("Error calculating Payload Range Diagram: Vehicle Operating Empty not defined")
else:
OEW = mass.operating_empty
if not mass.max_zero_fuel:
raise AttributeError("Error calculating Payload Range Diagram: Vehicle MZFW not defined")
else:
MZFW = vehicle.mass_properties.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[1] - 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]
# loop for each point of Payload Range Diagram
for i in range(len(TOW)):
## for i in [2]:
# Define takeoff weight
mission.segments[0].analyses.weights.vehicle.mass_properties.takeoff = TOW[i]
mission.segments[0].analyses.weights.vehicle.mass_properties.payload = PLD[i]
mission.segments[0].analyses.weights.vehicle.mass_properties.fuel = FUEL[i]
# Evaluate mission with current TOW
results = mission.evaluate()
segment = results.segments[cruise_segment_tag]
# 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.total_mass[0,0] - segment.conditions.weights.total_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.total_mass[-1,0] ) - FUEL[i] + reserve_fuel
if iter == maxIter:
print(f"Did not converge.")
break
# 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
if plot_diagram:
# get plotting style
ps = plot_style()
parameters = {'axes.labelsize': ps.axis_font_size,
'xtick.labelsize': ps.axis_font_size,
'ytick.labelsize': ps.axis_font_size,
'axes.titlesize': ps.title_font_size}
plt.rcParams.update(parameters)
if fuel_name == None:
fig = plt.figure( vehicle.tag + ' Fuel_Payload_Range_Diagram')
else:
fig = plt.figure(vehicle.tag + ' Fuel_Payload_Range_Diagram for ' + fuel_name)
axis_1 = fig.add_subplot(1,2,1)
axis_1.plot(payload_range.range /Units.nmi,payload_range.payload/Units.lbm ,color = 'k', linewidth = ps.line_width )
axis_1.set_xlabel('Range (nautical miles)')
axis_1.set_ylabel('Payload (lbs)')
set_axes(axis_1)
axis_2 = fig.add_subplot(1,2,2)
axis_2.plot(payload_range.range /Units.nmi,payload_range.oew_plus_payload/Units.lbm ,color = 'k', linewidth = ps.line_width )
axis_2.set_xlabel('Range (nautical miles)')
axis_2.set_ylabel('OEW + Payload (lbs)')
set_axes(axis_2)
fig.tight_layout()
return payload_range
[docs]
def electric_payload_range_diagram(vehicle,mission,cruise_segment_tag,plot_diagram):
"""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.operating_empty:
raise AttributeError("Error calculating Payload Range Diagram: vehicle Operating Empty Weight is undefined.")
else:
OEW = mass.operating_empty
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.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.weights.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)
if plot_diagram:
# get plotting style
ps = plot_style()
parameters = {'axes.labelsize': ps.axis_font_size,
'xtick.labelsize': ps.axis_font_size,
'ytick.labelsize': ps.axis_font_size,
'axes.titlesize': ps.title_font_size}
plt.rcParams.update(parameters)
fig = plt.figure('Electric_Payload_Range_Diagram')
axis = fig.add_subplot(1,1,1)
axis.plot(payload_range.range /Units.nmi, payload_range.payload,color = 'k', linewidth = ps.line_width )
axis.set_xlabel('Range (nautical miles)')
axis.set_ylabel('Payload (kg)')
axis.set_title('Payload Range Diagram')
set_axes(axis)
fig.tight_layout()
return payload_range