Analysis With heliPypter¶
Basic Flight Performance¶
Using heliPypter, performance for a traditional helicopter with single main and tail rotors can be evaluated. The first step is defining all the inputs (there are many). The details of all inputs are fully documented on the API page.
Warning
Units are important, so make sure they are all Imperial! Metric and automated units with Pint may be supported in a future release. If you want it, post on the issues page.
The Helicopter class takes numeric weight values for fuel, and a single lumped value for all other masses. It then adds the remaining fuel weight and empty mass whenver you call the heli.GW property. Let’s use an empty weight fraction to generate this helicopter.
import helipypter.vehicles as vh
# Empty weight fraction
EW_frac = 0.528
# Total Gross Weight
GW_total = 5000
# Crew Weight
w_crew = 200
# Trapped Fluids
w_fluids = 13
w_empty = EW_frac*GW_total + w_crew + w_fluids
# Our payload is 6 people @ 213 lbs each
w_payload = 6*213
w_fuel = GW_total - w_empty - w_payload
doc_chopper = vh.Helicopter(name='Documentation Helicopter Spec',
MR_dia = 35,
MR_b = 4,
MR_ce = 10.4,
MR_Omega = 43.2,
MR_cd0 = 0.0080,
TR_dia = 5.42,
TR_b = 4,
TR_ce = 7,
TR_Omega = 239.85,
TR_cd0 = 0.015,
GW_empty = w_empty,
GW_fuel = w_fuel,
GW_payload = w_payload,
download = 0.03,
fe = 12.9,
l_tail = 21.21,
S_vt = 20.92,
cl_vt = 0.22,
AR_vt = 3
)
Note
The Main Rotor Blade incompressible minimum drag, MR_cd0, is a vehicle characteristic. If we could clean up this blade drag term, it would logically affect all flight performance, so it’s included here in the base definition of the vehicle.
The same goes for the airframe equivalent flat-plate drag, fe. If we were to perform an airframe drag cleanup design cycle on our vehicle, we can reduce this term here, or scale it however you want.
The Helicopter class has many default values. Some aren’t shown here, so it’s always good idea to check the vehicle definition using a simple print function.
print(doc_chopper)
-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
Documentation Helicopter Spec
Rotors: ('MR', 'TR')
-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
Main Rotor Inputs:
MR_dia: 35.000 [ft]
MR_b: 4.000 []
MR_ce: 10.400 [in]
MR_Omega: 43.200 [rad/s]
MR_cd0: 0.008 []
MR_R: 17.500 []
MR_A: 962.113 []
MR_vtip: 756.000 []
MR_sol: 0.063 []
-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
Tail Rotor Inputs:
TR_dia: 5.420 [ft]
TR_b: 4.000 []
TR_ce: 7.000 [in]
TR_Omega: 239.850 [rad/s]
TR_cd0: 0.015 []
TR_R: 2.710 []
TR_A: 23.072 []
TR_vtip: 649.993 []
TR_sol: 0.274 []
-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
Airframe Data:
GW_empty: 2853.000 [lbs]
GW_fuel: 869.000 [lbs]
GW_payload: 1278.000 [lbs]
download: 0.030 [.%]
HIGE_factor: 1.200 []
fe: 12.900 [ft2]
l_tail: 21.210 [ft]
S_vt: 20.920 [ft2]
cl_vt: 0.220 []
AR_vt: 3.000 []
-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
Engine Data:
eta_MRxsmn: 0.985 [.%]
eta_TRxsmn: 0.971 [.%]
eta_xsmn_co: 0.986 [.%]
eta_inst: 0.950 [.%]
xsmn_lim: 674.000 [hp]
pwr_lim: 813.000 [hp]
-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
Note
Not shown here are the engine Brake-Specific Fuel Consumption factors. Four factors can be provided,
defining a polynomial function to return the bsfc, in [lbs/(hp*hr)]. See helipypter.vehicles.Helicopter
method Helicopter.bsfc.
The heli object can now be called to hover, burn fuel, idle, lookup engine power, or fly. However, before we can perform any flight maneuvers, atmospheric properties must be supplied. Here, we create an Environment class. For example, to create a Sea-level standard atmosphere and hover at it:
atm = vh.Environment(alt=0)
output = heli.HOGE(atm)
print('-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-')
print('{:^45}'.format('Results - HOGE'))
print('-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-')
for k,v in doc_chopper.HOGE(atm).items():
print('{:>17}: {:>7.4}'.format(k, v))
Hover Out of Ground Effect (HOGE) returns dictionary of the flight point predictions. Sometimes, dictionary output isn’t the easiest to read, even though it’s easy to lookup. So we created a simple loop to print the data.
-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
Results - HOGE
-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
a: 5.717
delta_0: 0.009518
Ct: 0.003937
TR_thrust: 291.1
Cq_i: 0.0001787
Cq_v: 0.0
Cq_0: 7.502e-05
Cq_1: -1.037e-05
Cq_2: 1.317e-05
Cq: 0.0002565
Q: 6.174e+03
P_MR: 2.425e+05
HP_MR: 485.0
HP_TR: 45.3
SHP_ins: 566.0
SHP_unins: 595.8
sfc: 0.4982
Note
One of the optional inputs to the HOGE method is k_i. This number is the correction factor for non-uniform inflow, linear twist, and taper. It’s defaulted to 1.1, and will typically be between 1 and 1.15.
Forward flight performance can be evaluated just as easily. Let’s perform a speed sweep from 20 knots to 150 knots. The forward_flight method just takes an Environment for atmospheric properties, and either a single or list of airspeeds. This method returns a pandas dataframe that has several columns. It’s sometimes hard to view this data, so heliPypter has convenient plotting functions.
import numpy as np
import helipypter.funcs as func
# Create an array of 14 equally spaced airspeed values
# This is just a little shorthand and not necessary.
speeds = np.linspace(20, 150, num=14)
data = doc_chopper.forward_flight(atm, speeds)
fig, ax = func.speed_power_polar(data)
There’s lots of other data in this dataframe, and built-in functions exist to plot range and rate-of-climb. For now we’ll stop here and move on to mission analysis.
Mission Analysis¶
The first step here is obviously to create a mission. Currently, there’s no built-in classes representing a mission, because the contents of a mission are a simple collection of mission points, where each point has maneuver inputs. This data structure is very easily represented as a namedtuple. You can decide how you want to approach the specifics of mission analysis, this just one example. All helipypter classes should be flexible enough to fit your needs.
Note
In the future, this may change with some built-in missions, or a slightly different structure to make aircraft sizing straight-forward. At the time of creation, this was enough for me and I didn’t need to bother with the overhead of a custom class.
from collections import namedtuple
Point = namedtuple('MissionPoint', ['maneuver', 'altitude', 'duration', 'speed'])
startup = Point(maneuver='idle', altitude=0, duration=1, speed=0)
takeoff = Point(maneuver='IRP', altitude=0, duration=1, speed=0)
climb_0 = Point('MCP', 0, 5, 1000)
cruise_0 = Point('flight', 5000, 160, 110)
hover_1 = Point('hover', 0, 1, 0)
loiter = Point('loiter', 5000, 10, 60)
unload = Point('unload', 0, 5, 1278)
ground = Point('idle', 0, 1, 0)
mission = (startup, takeoff,
climb_0, cruise_0, loiter,
hover_1, unload, hover_1,
climb_0, cruise_0,
hover_1, ground
)
We’ve got a mission now, let’s create a function to run the helicopter through the mission, burning fuel and changing weight as we go. We’ll just use logging to print everything out to the console. If you have multiple missions and vehicles and you want to compare performance across them, you’ll probably want to write all this data to anoter dataframe or dictionary.
This is a lot of clunky code. I’m sure it can be written to be more pythonic. Most of it is just our logging statements, though. Essentially, we step through the mission and evaluate each point, determining the fuel required, removing that fuel weight from the total fuel weight, and logging the results.
import logging
def mission_loop(heli, mission):
'''This temp function performs all the logic to simulate the fuel burn of a mission.'''
# Mission Loop
logging.info('')
logging.info('')
logging.info('-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-')
logging.info('{:^45}'.format('Project Spec Mission'))
logging.info('-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-')
# Initialize the range tracker
mission_range = 0
for point in mission:
if point.maneuver == 'idle':
fuel = heli.idle()/60 * point.duration
heli.burn(fuel)
logging.info(f'Idled for {point.duration}[mins].')
logging.info(f' Burned {fuel:.2f}[lbs] of fuel.')
logging.info(f' New GW = {heli.GW:.2f}[lbs], fuel: {heli.GW_fuel:.2f}')
logging.info('')
elif point.maneuver == 'hover':
# Actually calculate the fuel cost for
# hovering at an exact weight and altitude
data = heli.HOGE(vh.Environment(point.altitude))
fuel = data['sfc']*data['SHP_unins']*point.duration/60
heli.burn(fuel)
logging.info(f'Hovered for {point.duration}[mins], burning {fuel:.2f}[lbs] of fuel.')
logging.info(f' New GW = {heli.GW:.2f}[lbs], fuel: {heli.GW_fuel:.2f}')
logging.info('')
elif point.maneuver == 'loiter':
data = heli.forward_flight(vh.Environment(point.altitude), point.speed)
fuel = data.SHP_uninst[0]*data.bsfc[0]/60 * point.duration
heli.burn(fuel)
logging.info(f'Loitered at {point.speed}[kts] for {point.duration}[mins].')
logging.info(f' Burned {fuel:.2f}[lbs] of fuel.')
logging.info(f' New GW {heli.GW:.2f}[lbs], fuel: {heli.GW_fuel:.2f}')
logging.info('')
elif point.maneuver == 'IRP':
# IRP is the engine rated limit
sfc = heli.bsfc(100)
fuel = sfc*1*heli.pwr_lim/60 * point.duration
heli.burn(fuel)
logging.info(f'Ran at IRP for {point.duration}[mins].')
logging.info(f' Burned {fuel:.2f}[lbs] of fuel.')
logging.info(f' New GW = {heli.GW:.2f}[lbs], fuel: {heli.GW_fuel:.2f}')
logging.info('')
elif point.maneuver == 'MCP':
# MCP is defined as 95% of IRP
sfc = heli.bsfc(95)
fuel = sfc*0.95*heli.pwr_lim/60 * point.duration
heli.burn(fuel)
logging.info(f'MCP Climb for {point.duration}[mins] @ {point.speed}[ft/min].')
logging.info(f' Burned {fuel:.2f}[lbs] of fuel.')
logging.info(f' New GW = {heli.GW:.2f}[lbs], fuel: {heli.GW_fuel:.2f}')
logging.info('')
mission_range += 120*point.duration/60 # 120 kts has more ROC than 1000 TODO: Calculate this.
elif point.maneuver == 'flight':
data = heli.forward_flight(vh.Environment(point.altitude), point.speed)
fuel = point.duration/data.SR[0]
heli.burn(fuel)
logging.info(f'Forward flight for {point.duration}[nm] @ {point.speed}[kts].')
logging.info(f' Burned {fuel:.2f}[lbs] of fuel.')
logging.info(f' New GW = {heli.GW:.2f}[lbs], fuel: {heli.GW_fuel:.2f}')
logging.info('')
mission_range += point.duration
elif point.maneuver == 'climb':
# Represents a hover climb/descent NOT @ MCP
# There's no range credit for a "climb" maneuver instead of an "MCP" maneuver.
data = heli.HOGE(vh.Environment(point.altitude), Vroc=point.speed)
fuel = data['sfc']*data['SHP_unins']*point.duration/60
heli.burn(fuel)
logging.info(f'Climb for {point.duration}[min] @ {point.speed}[ft/min]')
logging.info(f' Burned {fuel:.2f}[lbs] of fuel.')
logging.info(f' New GW = {heli.GW:.2f}[lbs], fuel: {heli.GW_fuel:.2f}')
logging.info('')
elif point.maneuver == 'unload':
logging.info(f'Landed! Unloading {point.speed}[lbs] of cargo.')
heli.unload(point.speed)
fuel = heli.idle()/60 * point.duration
heli.burn(fuel)
logging.info(f'Idled for {point.duration}[mins], burning {fuel:.2f}[lbs] of fuel.')
logging.info(f' New GW = {heli.GW:.2f}[lbs], fuel: {heli.GW_fuel:.2f}')
logging.info('')
logging.info('')
logging.info(f'Mission Complete! {heli.GW_fuel:.2f} [lbs] of fuel remaining.')
logging.info(f'Total Range = {mission_range:.2f}[nm]')
logging.info('-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-')
mission_loop(doc_chopper, mission)
Results:
2020-05-02 21:16:07,914 - INFO - -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
2020-05-02 21:16:07,914 - INFO - Project Spec Mission
2020-05-02 21:16:07,914 - INFO - -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
2020-05-02 21:16:07,914 - INFO - Idled for 1[mins].
2020-05-02 21:16:07,914 - INFO - Burned 2.27[lbs] of fuel.
2020-05-02 21:16:07,914 - INFO - New GW = 4997.73[lbs], fuel: 866.73
2020-05-02 21:16:07,914 - INFO -
2020-05-02 21:16:07,914 - INFO - Ran at IRP for 1[mins].
2020-05-02 21:16:07,914 - INFO - Burned 6.42[lbs] of fuel.
2020-05-02 21:16:07,914 - INFO - New GW = 4991.31[lbs], fuel: 860.31
2020-05-02 21:16:07,914 - INFO -
2020-05-02 21:16:07,914 - INFO - MCP Climb for 5[mins] @ 1000[ft/min].
2020-05-02 21:16:07,914 - INFO - Burned 31.00[lbs] of fuel.
2020-05-02 21:16:07,914 - INFO - New GW = 4960.31[lbs], fuel: 829.31
2020-05-02 21:16:07,914 - INFO -
2020-05-02 21:16:07,944 - INFO - Forward flight for 160[nm] @ 110[kts].
2020-05-02 21:16:07,944 - INFO - Burned 374.09[lbs] of fuel.
2020-05-02 21:16:07,944 - INFO - New GW = 4586.22[lbs], fuel: 455.22
2020-05-02 21:16:07,945 - INFO -
2020-05-02 21:16:07,974 - INFO - Loitered at 60[kts] for 10[mins].
2020-05-02 21:16:07,974 - INFO - Burned 32.14[lbs] of fuel.
2020-05-02 21:16:07,974 - INFO - New GW 4554.09[lbs], fuel: 423.09
2020-05-02 21:16:07,974 - INFO -
2020-05-02 21:16:07,975 - INFO - Hovered for 1[mins], burning 4.60[lbs] of fuel.
2020-05-02 21:16:07,975 - INFO - New GW = 4549.49[lbs], fuel: 418.49
2020-05-02 21:16:07,975 - INFO -
2020-05-02 21:16:07,975 - INFO - Landed! Unloading 1278[lbs] of cargo.
2020-05-02 21:16:07,975 - INFO - Idled for 5[mins], burning 11.34[lbs] of fuel.
2020-05-02 21:16:07,975 - INFO - New GW = 3260.14[lbs], fuel: 407.14
2020-05-02 21:16:07,975 - INFO -
2020-05-02 21:16:07,975 - INFO - Hovered for 1[mins], burning 3.71[lbs] of fuel.
2020-05-02 21:16:07,975 - INFO - New GW = 3256.44[lbs], fuel: 403.44
2020-05-02 21:16:07,975 - INFO -
2020-05-02 21:16:07,975 - INFO - MCP Climb for 5[mins] @ 1000[ft/min].
2020-05-02 21:16:07,975 - INFO - Burned 31.00[lbs] of fuel.
2020-05-02 21:16:07,975 - INFO - New GW = 3225.44[lbs], fuel: 372.44
2020-05-02 21:16:07,975 - INFO -
2020-05-02 21:16:08,005 - INFO - Forward flight for 160[nm] @ 110[kts].
2020-05-02 21:16:08,005 - INFO - Burned 333.85[lbs] of fuel.
2020-05-02 21:16:08,005 - INFO - New GW = 2891.59[lbs], fuel: 38.59
2020-05-02 21:16:08,005 - INFO -
2020-05-02 21:16:08,006 - INFO - Hovered for 1[mins], burning 3.48[lbs] of fuel.
2020-05-02 21:16:08,006 - INFO - New GW = 2888.11[lbs], fuel: 35.11
2020-05-02 21:16:08,006 - INFO -
2020-05-02 21:16:08,006 - INFO - Idled for 1[mins].
2020-05-02 21:16:08,006 - INFO - Burned 2.27[lbs] of fuel.
2020-05-02 21:16:08,006 - INFO - New GW = 2885.84[lbs], fuel: 32.84
2020-05-02 21:16:08,006 - INFO -
2020-05-02 21:16:08,006 - INFO -
2020-05-02 21:16:08,006 - INFO - Mission Complete! 32.84 [lbs] of fuel remaining.
2020-05-02 21:16:08,006 - INFO - Total Range = 340.00[nm]
2020-05-02 21:16:08,006 - INFO - -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
Technology Factors¶
During design of an air vehicle it can be advantageous to explore the effects of different technology factors, represented as percent reductions, on the performance. Because of heliPypter’s object-oriented approach, changing these inputs is relatively straight-forward.
Using our previously-created helicopter as a base, we can update these values one by one, or all at once, it’s really up to you.
import copy
## Reduce the empty weight fraction
EW_factor = 0.95
# Empty weight fraction
EW_frac = 0.528
# Total Gross Weight
GW_total = 5000
# Crew Weight
w_crew = 200
# Trapped Fluids
w_fluids = 13
w_empty = EW_factor*EW_frac*GW_total + w_crew + w_fluids
# Our payload is still 6 people @ 213 lbs each
w_payload = 6*213
w_fuel = GW_total - w_empty - w_payload
lite_args = copy.copy(args)
lite_args[11] = w_empty
lite_args[12] = w_fuel
lite_args[13] = w_payload
# Generate the new vehicle, with all other characteristics the same
lightweight = chopper_gen(lite_args)
out = pd.DataFrame(data=func.missionSim(lightweight, mission), columns=['dist', 'fuel_rem', 'fuel_used'])
print(f'Lite chopper range: {out.dist.sum()}')
print(f'Lite chopper remaining fuel: {out.fuel_rem.iat[-1]:.2f}')
## Reduce the MR_cd0
## Reduce the fe
cd0_factor = 0.95
fe_factor = 0.95
clean_args = copy.copy(args)
clean_args[5] = cd0_factor*clean_args[5]
clean_args[15] = fe_factor*clean_args[15]
clean_chopper = chopper_gen(clean_args)
out = pd.DataFrame(data=func.missionSim(clean_chopper, mission), columns=['dist', 'fuel_rem', 'fuel_used'])
print(f'Clean chopper range: {out.dist.sum()}')
print(f'Clean chopper remaining fuel: {out.fuel_rem.iat[-1]:.2f}')
## Reduce the Induced Power Factor
## Increase the fuel efficiency of the engine
eng_fac = 0.97
# Use this k_i when calling Helicopter.hover()
k_i = 1.05
efficient_chopper = copy.copy(doc_chopper)
efficient_chopper.bsfc_0 = eng_fac*efficient_chopper.bsfc_0
efficient_chopper.bsfc_1 = eng_fac*efficient_chopper.bsfc_1
efficient_chopper.bsfc_2 = eng_fac*efficient_chopper.bsfc_2
efficient_chopper.bsfc_3 = eng_fac*efficient_chopper.bsfc_3
efficient_chopper.bsfc_4 = eng_fac*efficient_chopper.bsfc_4
efficient_chopper.bsfc_5 = eng_fac*efficient_chopper.bsfc_5
# Since this one is a copy of the old one
# We've already burned all the fuel and unloaded
# it, so we need to reset the weight values.
efficient_chopper.refuel()
efficient_chopper.reload()
out = pd.DataFrame(data=func.missionSim(efficient_chopper, mission), columns=['dist', 'fuel_rem', 'fuel_used'])
print(f'Efficient chopper range: {out.dist.sum()}')
print(f'Efficient chopper remaining fuel: {out.fuel_rem.iat[-1]:.2f}')
From here, we can evaluate each verion on the same set of missions, and observe the change in fuel consumption. Changes to the base class aren’t limited to the above. A formulaic optimization procedure could be performed on any number of variables for design optimization. Programming this operation is beyond the scope of this analysis, however, and may be included at a later date.
Let’s just look at the code output. The mission range was constant (although you could change it, but I would argue the the mission should design the vehicle and not vice versa), but the remaining fuel changes with each iteration.
Default chopper range: 340.0
Default chopper remaining fuel: 32.84
Lite chopper range: 340.0
Lite chopper remaining fuel: 164.84
Clean chopper range: 340.0
Clean chopper remaining fuel: 42.34
Efficient chopper range: 340.0
Efficient chopper remaining fuel: 57.55
We can see from the above that our design is very sensitive to weight and fuel efficiency. Improvements in these areas would be more effective uses of future design resources than in cleaner aerodynamics.