pyAPP7 Examples¶
Content: These examples are grouped into three main sections:
Version: pyAPP7 version 1.0
Note: This example was written as a jupyter notebook (version 4.4.0), and has been tested with Python 2.7.16 |Anaconda (64-bit). The notebook file is available in the Examples directory of the pyAPP7 distribution.
Imports & Constants¶
Imports for plotting (matplotlib) and arrays (numpy):
[1]:
import matplotlib.pyplot as plt
import numpy as np
Jupyter Notebook specific imports:
[2]:
%matplotlib inline
Constants:
[3]:
APP7DIR = r'C:\Program Files (x86)\ALR Aerospace\APP 7 Professional Edition'
Files¶
The Files module is used to open, change and save APP Files. It can be used for: * acft (Aircraft) * mis (Mission Computation) * perf (Performance Charts)
file types.
It is recommended to create a new file using the APP GUI and subsequenty modify this file using Python/pyAPP7, instead of creating a file from scratch with pyAPP7.
Import the pyAPP7 modules
[4]:
from pyAPP7 import Files
from pyAPP7 import Database
from pyAPP7 import Units
The Units and Database modules are imported as well for this example. They are useful to convert units and translate APP indices to human-readable text
Aircraft File (*.acft)¶
To load an APP aircraft model, the class AircraftModel is used. A new instance can be created directly with the fromFile class method:
[5]:
aircraftpath = r'data\\LWF.acft'
acft = Files.AircraftModel.fromFile(aircraftpath)
Now we have the aircraft file available in the acft variable. All data within the aircraft can be accessed through class member variables directly, or by using get functions. This examples shows how to access fields in the General Data tab of APP’s aircraft model GUI:
[6]:
data = acft.getGeneralData()
print ('Aircraft Name:', data.m_sAircraftName)
print ('Author:', data.m_sAuthor)
('Aircraft Name:', 'LWF')
('Author:', 'ALR')
Getter functions exist for all the main datasets. To print lists of the available data sets, use:
[7]:
print(acft.getMassLimitsNames())
print(acft.getAeroNames())
print(acft.getPropulsionNames())
print(acft.getStoreNames())
['Standard']
['Cruise', 'TO Flaps 27\xb0']
['LWF']
['AIM-9 Wingtip']
This example demonstrates how to loop through an X2Table (in this case the CL/CDi table) and correctly lable the drag polars:
[8]:
i = 0
aero = acft.getAero(i) #get the first aerodanymic dataset, in this case 'Cruise'
fig = plt.figure(figsize=(8.3, 5.8)) #A5 landscape figure, size is in inches
ax = plt.subplot(1,1,1)
for val, table in zip(aero.cdITable.value,aero.cdITable.table):
ax.plot(table[:,1], table[:,0], 'd-', label='Mach = '+str(val))
# adjust Axis properties
ax.set_title(acft.getAeroName(i))
ax.legend(loc='best')
ax.set_xlabel('$CD_i$')
ax.set_ylabel('$CL$')
ax.grid()
For an detailed explaination of the XTables classes, consult the pyAPP user guide.
A more involved example would be to compare lift curves of all available aero datasets:
[9]:
fig = plt.figure(figsize=(8.3, 5.8)) #A5 landscape figure, size is in inches
ax = plt.subplot(1, 1, 1)
for aero, aeroName in zip(acft.getAeroList(), acft.getAeroNames()):
ax.plot(aero.clTable.table[0][:,0]*Units.DEG, aero.clTable.table[0][:,1], label=aeroName.decode('cp1252'))
ax.set_xlabel(u'$AoA$ [°]')
ax.set_ylabel(u'$CL$')
leg = ax.legend(loc=2)
#fig.savefig('CL_comparison.png',dpi=200)
Additionally, this example demonstrates the use of the Units module to convert from radians to degrees.
Note: In oder for the legend label for the TO Flaps 27° setting to be printed correctly, the aeroName string has to be converted to unicode with the enconding of the original text file, in this case cp1252. In addition, to print the ° sign in the x-axis label, the string has to be unicode and is typed with the prefix ‘u’
Mission File (*.mis)¶
The mission file is loaded using the classmethod fromFile in the MissionComputationFile class:
[10]:
missionpath = r'data\\LWF Air Combat Mission RoA.mis'
missionFile = Files.MissionComputationFile.fromFile(missionpath)
We have now the mission file as a python variable missionFile in the memory ready to be be examined and changed.
For example, getInitialCondition() can be used to access the initial conditions. The return value is of type Files.FlightData
[11]:
initFd = missionFile.getInitialCondition()
print(initFd.alt.xx) #altitude in meters
print(initFd.fuel.xx) #initial fuel as a factor [0...1]
0.0
1.0
To loop through the segments, use getSegmentList() to access the list of segments. The following code prints the segment index (identifier) of each segment:
[12]:
for segment in missionFile.getSegmentList():
print(segment.segmentIndex)
SEG_GROUNDOP
SEG_TAKEOFF
SEG_CLIMB
SEG_BESTCLIMBRATE
SEG_ACCELERATION
SEG_TARGETMACHCRUISE
SEG_MANEUVRE
SEG_STOREDROP
SEG_MANEUVRE
SEG_STOREDROP
SEG_LOITER
SEG_SPECIFICRANGE
SEG_DECELERATION
SEG_CASDESCENT
SEG_LANDINGROLL
In order to display the label of each segment instead of the index string, we can use the Database class:
[13]:
db = Database.Database()
[14]:
for segment in missionFile.getSegmentList():
print(db.GetTextFromID(segment.segmentIndex))
Ground Operation
Takeoff
Climb
Climb at Best Rate
Acceleration
Cruise at Mach
Maneuver at Max. LF
Store Drop
Maneuver at Max. LF
Store Drop
Loiter
Cruise at Best SR
Deceleration
Descent at CAS
Landing Roll
Similarly, the type and value of the segment end condition can shown:
[15]:
for segment in missionFile.getSegmentList():
print(db.GetTextFromID(segment.endValue1.realIdx),':', segment.endValue1.xx, )
('Seg. Time', ':', 600.0)
('Velocity', ':', 75.4455900943)
('Altitude', ':', 500.0)
('Altitude', ':', 9500.0)
('Mach', ':', 0.9)
('Seg. Dist.', ':', 320053.202172)
('Turns', ':', 12.5663706144)
('Seg. Time', ':', 100.0)
('Turns', ':', 6.28318530718)
('Seg. Dist.', ':', 100.0)
('Seg. Time', ':', 600.0)
('Seg. Dist.', ':', 402135.694779)
('CAS', ':', 102.888888976)
('Altitude', ':', 500.0)
('Velocity', ':', 0.01)
In the following code examples we show how to make changes to the mission and save it to a new file.
The frist example shows how to change the initial fuel mass to 80% and the initial altitude to 1000 m:
[16]:
initFd = missionFile.getInitialCondition()
initFd.fuel.xx = 0.8
initFd.alt.xx = 1000.0
Next, we change parameters of a segment, in this example the altitude (stop condition) of the segment “Climb at Best Rate” (segment index 3) from 9500m to 7000m:
[17]:
print(missionFile.getSegment(3).endValue1.xx)
missionFile.getSegment(3).endValue1.xx = 7000.0
print(missionFile.getSegment(3).endValue1.xx)
9500.0
7000.0
In addition, we change the altitude of the initial climb after takeoff (Segment index 2) to 500m above the starting altitude.
[18]:
print(missionFile.getSegment(2).endValue1.xx)
missionFile.getSegment(2).endValue1.xx = initFd.alt.xx + 500.0
print(missionFile.getSegment(2).endValue1.xx)
500.0
1500.0
Finally, we save the changed mission to a new file.
[19]:
missionpath_mod = r'data\\LWF Air Combat Mission RoA_mod.mis'
missionFile.saveToFile(missionpath_mod,overwrite=True)
Performance Chart File (*.perf)¶
A PerformanceChartFile is instantiated via the fromFile classmethod:
[20]:
chartpath = r'data\\LWF Climb Rate Chart 50% Fuel.perf'
chart = Files.PerformanceChartFile.fromFile(chartpath)
This example shows how to change the flight state (initial condition). The function getInitialCondition returns an instance of type FlightData:
[21]:
fd = chart.getInitialCondition()
[22]:
print fd.alt.xx
print fd.speed.xx, db.GetTextFromID(fd.speed.realIdx) #Mach Number
print fd.fuel.xx, db.GetTextFromID(fd.fuel.realIdx)
0.0
0.0 Mach
0.5 Fuel Percent
Note: the speed variable can be either Mach or TAS. Check the corresponding realIdx string. Similarly, the variables payload, climb, thrust and pull can be of different type
Change the fuel from the current state (50%) to 100%
[23]:
print(fd.fuel.xx)
0.5
[24]:
fd.fuel.xx = 1.0
To change the aircraft Configuration, for example from Dry (configuration index 0) to Reheat (configuration index 1), access the ProjectAircraftSetting class. To see what configurations are available, open the aircraft model.
[25]:
configNames = acft.getConfigurationNames()
print 'Configurations in the aircraft model:\n', configNames, '\n'
cfg = chart.getAircraftConfiguration()
print cfg.activeSetting, configNames[cfg.activeSetting]
cfg.activeSetting = 1
print cfg.activeSetting, configNames[cfg.activeSetting]
Configurations in the aircraft model:
['Cruise, Dry', 'Cruise, Reheat', 'TOL, Reheat', 'TOL, Dry']
0 Cruise, Dry
1 Cruise, Reheat
Similarly, External Store Configurations can be changed:
[26]:
storeConfigNames = acft.getStoreConfigurationNames()
print 'Store configurations in the aircraft model:\n',storeConfigNames,'\n'
cfg = chart.getAircraftConfiguration()
print cfg.activeStoreSetting, storeConfigNames[cfg.activeStoreSetting]
cfg.activeStoreSetting = -1 #use -1 for no external stores (clean)
Store configurations in the aircraft model:
['Air-to-Air']
0 Air-to-Air
To access the computation, use the getComputation method. The type of performance chart can be checked with the CompType variable. In the case of a Point Performance Computation, the type of equation solved is stored in resData.CmpType.
[27]:
comp = chart.getComputation()
print db.GetTextFromID(comp.CompType)
print db.GetTextFromID(comp.resData.CmpType)
Point Performance Computation
Climb
The resData attribute also holds the data ranges for the chart in two X0Tables, one for the X-Range the other for the Parameter:
[28]:
print comp.resData.X1Range.X0Typ
print comp.resData.X1Range.table
print comp.resData.X2Range.X0Typ
print comp.resData.X2Range.table
REAL_MACH
[ 0.2 0.25 0.3 0.35 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75
0.8 0.85 0.9 0.95]
REAL_ALT
[ 0. 2500. 5000. 7500. 10000.]
For example, to change the computed altitudes, replace the table with a new numpy array:
[29]:
comp.resData.X2Range.table = np.linspace(0.0, 10000.0, 3)
print comp.resData.X2Range.table
[ 0. 5000. 10000.]
or, add values manually (as floats):
[30]:
comp.resData.X2Range.table = np.array([0.0, 10000.0])
print comp.resData.X2Range.table
[ 0. 10000.]
Save your modified file:
[31]:
chartpath_mod = r'data\\LWF Climb Rate Chart 100% Fuel.perf'
chart.saveToFile(chartpath_mod, overwrite=True)
Mission Computation¶
Import the Mission module from pyAPP7:
[32]:
from pyAPP7 import Mission
In order to run APP mission computations, create an instance of the MissionComputation class. The path to the directory where the APP executable can be found has to be provided
[33]:
misCmp = Mission.MissionComputation(APP7Directory = APP7DIR)
[34]:
misCmp.run(missionpath)
[34]:
True
[35]:
res = misCmp.result
Access data by looping through the segments. To get a specific variable, find the index of the variable by using the function getVariableIndex. To access the data of the segment, use getData. getData returns a 2D numpy array, with the first dimension being the datapoint and the second dimension the variable. For example, the variable Fuel Mass at the end of each segment can be obtained by using:
[36]:
idx_fuel = res.getVariableIndex('Fuel Mass')
for seg in res.getSegmentList():
print res.getVariableName(idx_fuel),':',seg.getData()[-1,idx_fuel]
Fuel Mass [kg] : 1896.218
Fuel Mass [kg] : 1859.49218997
Fuel Mass [kg] : 1779.27456082
Fuel Mass [kg] : 1532.93143492
Fuel Mass [kg] : 1520.88752268
Fuel Mass [kg] : 1123.86312629
Fuel Mass [kg] : 914.583951541
Fuel Mass [kg] : 914.583951541
Fuel Mass [kg] : 815.505620848
Fuel Mass [kg] : 815.505620848
Fuel Mass [kg] : 619.613596679
Fuel Mass [kg] : 225.152121272
Fuel Mass [kg] : 222.695756009
Fuel Mass [kg] : 104.648393277
Fuel Mass [kg] : 100.120415794
Instead of using getData to access the raw output, we can call getVariableData and get a list of numpy arrays for the output of a specific variable:
[37]:
var_name, mission_data = res.getVariableData('Fuel Mass')
print var_name
print len(mission_data)
print mission_data[0] #time-dependent data of the first segment
Fuel Mass [kg]
15
[ 2000. 1998.2703 1996.5406 1994.8109 1993.0812 1991.3515
1989.6218 1987.8921 1986.1624 1984.4327 1982.703 1980.9733
1979.2436 1977.5139 1975.7842 1974.0545 1972.3248 1970.5951
1968.8654 1967.1357 1965.406 1963.6763 1961.9466 1960.2169
1958.4872 1956.7575 1955.0278 1953.2981 1951.5684 1949.8387
1948.109 1946.3793 1944.6496 1942.9199 1941.1902 1939.4605
1937.7308 1936.0011 1934.2714 1932.5417 1930.812 1929.0823
1927.3526 1925.6229 1923.8932 1922.1635 1920.4338 1918.7041
1916.9744 1915.2447 1913.515 1911.7853 1910.0556 1908.3259
1906.5962 1904.8665 1903.1368 1901.4071 1899.6774 1897.9477
1896.218 1896.218 ]
A list of the fuel consumed per segment can be easily ontained using a list comprehension:
[38]:
idx_segFuel = res.getVariableIndex('Seg. Fuel')
segFuelList = [seg.getData()[-1,idx_segFuel] for seg in res.getSegmentList()]
print segFuelList
[103.782, 36.7258100321, 80.2176291448, 246.34312590799999, 12.043912239799999, 397.02439638800001, 209.279174746, 0.0, 99.0783306923, 0.0, 195.892024169, 394.46147540700002, 2.4563652631499999, 118.047362732, 4.5279774831899999]
[39]:
fig = plt.figure(figsize=(8.3, 5.8)) #A5 landscape figure, size is in inches
ax = plt.subplot(1,1,1)
ax.bar(range(len(segFuelList)),
segFuelList,
align='center',
color='#336699')
ax.set_xticks(range(len(segFuelList)))
ax.set_xticklabels(res.getSegmentNameList(), rotation=45, ha='right')
ax.set_ylabel(res.getVariableName(idx_segFuel))
[39]:
Text(0,0.5,'Seg. Fuel [kg]')
Looping through the segments can also be useful to plot the mission profile:
[40]:
idx1 = res.getVariableIndex('Time')
idx2 = res.getVariableIndex('Distance')
idx3 = res.getVariableIndex('Altitude')
[41]:
fig = plt.figure(figsize=(8.3, 5.8)) #A5 landscape figure, size is in inches
ax = plt.subplot(2,1,1)
for seg in res.getSegmentList():
ax.plot(seg.getData()[:,idx1],seg.getData()[:,idx3])
ax.set_xlabel(res.getVariableName(idx1))
ax.set_ylabel(res.getVariableName(idx3))
ax = plt.subplot(2,1,2)
for seg in res.getSegmentList():
ax.plot(seg.getData()[:,idx2],seg.getData()[:,idx3])
ax.set_xlabel(res.getVariableName(idx2))
ax.set_ylabel(res.getVariableName(idx3))
plt.tight_layout()
Matplotlib offers a lot of formatting options for legends: http://matplotlib.org/api/legend_api.html#matplotlib.legend.Legend
[42]:
idx1 = res.getVariableIndex('Distance')
idx2 = res.getVariableIndex('Altitude')
idx_segDst = res.getVariableIndex('Seg. Dist')
fig = plt.figure(figsize=(8.3, 5.8)) #A5 landscape figure, size is in inches
ax = plt.subplot(1,1,1)
colormap = plt.cm.rainbow
ax.set_prop_cycle('color',[colormap(i) for i in np.linspace(0, 0.9, 7)])
for i,seg in enumerate(res.getSegmentList()):
if seg.getData()[-1,idx_segDst]>2.0:
ax.plot(seg.getData()[:,idx1], seg.getData()[:,idx2], label=seg.getName(),lw=2.0)
ax.set_xlabel(res.getVariableName(idx1))
ax.set_ylabel(res.getVariableName(idx2))
plt.subplots_adjust(bottom=0.2)
ax.legend(bbox_to_anchor=(1.05,-0.1), ncol=3, fontsize = 9, handlelength = 2.0)
ax.grid()
[43]:
misCmp_mod = Mission.MissionComputation(APP7Directory = APP7DIR)
misCmp_mod.run(missionpath_mod)
[43]:
True
[44]:
res_mod = misCmp_mod.result
idx_segDst = res.getVariableIndex('Seg. Dist')
[45]:
fig = plt.figure(figsize=(8.3, 5.8)) #A5 landscape figure, size is in inches
ax = plt.subplot(1,1,1)
colormap = plt.cm.rainbow
ax.set_prop_cycle('color',[colormap(i) for i in np.linspace(0, 0.9, 7)])
for i,seg in enumerate(res.getSegmentList()):
if seg.getData()[-1,idx_segDst]>2.0:
ax.plot(seg.getData()[:,idx1], seg.getData()[:,idx2], label=seg.getName(),lw=2.0)
for i,seg in enumerate(res_mod.getSegmentList()):
if seg.getData()[-1,idx_segDst]>2.0:
ax.plot(seg.getData()[:,idx1], seg.getData()[:,idx2],lw=2.0)
ax.set_xlabel(res.getVariableName(idx1))
ax.set_ylabel(res.getVariableName(idx2))
plt.subplots_adjust(bottom=0.2)
ax.legend(bbox_to_anchor=(1.05,-0.1), ncol=3, fontsize = 9, handlelength = 2.0)
ax.grid()
The result of a mission computation can also be loaded from the result text-file after the computation:
[46]:
resfile = r'data\\LWF Air Combat Mission RoA.mis_output.txt'
res = Mission.MissionResult.fromFile(resfile)
Complex Mission Loop¶
[47]:
cap_path = r'data\\LWF CAP Loop.mis'
cap_path_mod = r'data\\LWF CAP Loop_mod.mis'
[48]:
mis = Files.MissionComputationFile.fromFile(cap_path)
[(i, seg.getName()) for i, seg in enumerate(mis.getSegmentList())]
[48]:
[(0, 'SEG_GROUNDOP'),
(1, 'SEG_TAKEOFF'),
(2, 'SEG_CLIMB'),
(3, 'SEG_BESTCLIMBRATE'),
(4, 'SEG_ACCELERATION'),
(5, 'SEG_TARGETMACHCRUISE'),
(6, 'SEG_LOITER'),
(7, 'SEG_STOREDROP'),
(8, 'SEG_STOREDROP'),
(9, 'SEG_MANEUVRE'),
(10, 'SEG_SPECIFICRANGE'),
(11, 'SEG_NOCREDIT')]
[49]:
idx_loiter = 6
idx_combat = 9
[50]:
range_combat = np.linspace(0, 10, 6) # minutes
Read a CAP mission from an existing file, adjust the end-value of the combat segment and save the mission to another file. Afterwards, run the mission, extract the result and store it to a list (i.e. loiter_time).
[51]:
loiter_time = []
for i in range_combat:
misFile = Files.MissionComputationFile.fromFile(cap_path)
combat = misFile.getSegment(idx_combat)
combat.endValue1.xx = i*60.0 # convert minutes to seconds
misFile.saveToFile(cap_path_mod, overwrite=True)
mis = Mission.MissionComputation(APP7DIR)
mis.run(cap_path_mod)
res = mis.getResult()
idx_segTime = res.getVariableIndex('Seg. Time')
loiter_time.append(res.getSegment(idx_loiter).getData()[-1,idx_segTime])
Plot the results as a bar-chart.
[52]:
fig = plt.figure(figsize=(8.3, 5.8)) #A5 landscape figure, size is in inches
ax = plt.subplot(1,1,1)
width = 1.4
ax.bar(x=range_combat-2.0*width,
height=loiter_time, width=width,
tick_label=[str(c) for c in range_combat],
align='center',
color='#336699')
ax.set_title('Combat Air Patrol (CAP)')
ax.set_xlabel('Combat Time [min]')
ax.set_ylabel('Time on Station [min]')
[52]:
Text(0,0.5,'Time on Station [min]')
Note: input data is always in SI units (e.g. the combat time segment endValue is in seconds), but the output values are formatted (e.g. loiter time is in minutes)
Performance Charts¶
Import the Performance module from pyAPP7
[53]:
from pyAPP7 import Performance
[54]:
perf = Performance.PerformanceChart(APP7Directory=APP7DIR)
perf.run(chartpath)
[54]:
True
The result is loaded into a PerformanceChartResult instance:
[55]:
res = perf.result
A PerformanceChartResult contains a list of ResultLine objects. The ResultLine contains the data as a 2d numpy array, with the first dimension being the datapoints and the second dimension the variable index:
[56]:
line = res.getLine(0)
data = line.getData()
print data.shape
print data
(16L, 88L)
[[ 0. 0. 0. ..., 64.27329108
64.93182676 20.39259343]
[ 0. 0. 0. ..., 64.27329108 79.2340213
30.97531047]
[ 0. 0. 0. ..., 64.27329108
94.60491582 38.3654778 ]
...,
[ 0. 0. 0. ..., 68.52726956
288.03991245 26.42931541]
[ 0. 0. 0. ..., 68.23628197
306.24737031 3.24777545]
[ 0. 0. -0. ..., 69.04558153
315.66211611 -69.76337364]]
To find the index of the desired variable, use the getVariableIndex function:
[57]:
idx1 = res.getVariableIndex('CAS')
idx2 = res.getVariableIndex('Climb Speed')
The lines can then be plotted using Matploltib:
[58]:
fig = plt.figure(figsize=(8.3, 5.8)) #A5 landscape figure, size is in inches
ax = plt.subplot(1,1,1)
#Plot the lines
for line in res.getLineList():
ax.plot(line.getData()[:,idx1],line.getData()[:,idx2], label=line.getLabel())
ax.legend(loc=3)
ax.set_xlabel(res.getVariableName(idx1))
ax.set_ylabel(res.getVariableName(idx2))
[58]:
Text(0,0.5,'Climb Speed [m/sec]')
Since each data line is a numpy array, data can easily be processed using the powerful functions of numpy. This example extracts the maxima of each line and plots them. Note: the line contains NaNs, therefore the function np.nanargmax is used to extract the maxima.
[59]:
fig = plt.figure(figsize=(8.3, 5.8)) #A5 landscape figure, size is in inches
ax = plt.subplot(1,1,1)
#Plot the lines
for line in res.getLineList():
ax.plot(line.getData()[:,idx1], line.getData()[:,idx2], label=line.getLabel())
ax.set_prop_cycle(None) #Resets the color cycle
#Plot the maxima
for line in res.getLineList():
xdata = line.getData()[:,idx1]
ydata = line.getData()[:,idx2]
idx_max = np.nanargmax(ydata) #find the location of the maximum
ax.plot(xdata[idx_max], ydata[idx_max], 'd', label=str(ydata[idx_max]))
ax.legend(loc=3, numpoints=1, ncol=2)
ax.set_xlabel(res.getVariableName(idx1))
ax.set_ylabel(res.getVariableName(idx2))
[59]:
Text(0,0.5,'Climb Speed [m/sec]')