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()
_images/pyAPP7_Examples_21_0.png

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)
_images/pyAPP7_Examples_23_0.png

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]')
_images/pyAPP7_Examples_82_1.png

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()
_images/pyAPP7_Examples_85_0.png

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()
_images/pyAPP7_Examples_87_0.png
[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()
_images/pyAPP7_Examples_90_0.png

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]')
_images/pyAPP7_Examples_101_1.png

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]')
_images/pyAPP7_Examples_114_1.png

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]')
_images/pyAPP7_Examples_116_1.png