diff --git a/bluesky/__init__.py b/bluesky/__init__.py
index f5b2f43610..0db99ac739 100644
--- a/bluesky/__init__.py
+++ b/bluesky/__init__.py
@@ -2,6 +2,7 @@
from bluesky import settings
from bluesky.core import Signal
from bluesky import stack
+from bluesky import tools
# Constants
@@ -35,9 +36,6 @@ def init(mode='sim', pygame=False, discovery=False, cfgfile='', scnfile=''):
- pygame: indicate if BlueSky is started with BlueSky_pygame.py
- discovery: Enable network discovery
'''
- # Initialize global settings first, possibly loading a custom config file
- settings.init(cfgfile)
-
# Is this a server running headless?
headless = (mode[-8:] == 'headless')
@@ -46,6 +44,12 @@ def init(mode='sim', pygame=False, discovery=False, cfgfile='', scnfile=''):
gui_type = 'pygame' if pygame else \
'none' if headless or mode[:3] == 'sim' else 'qtgl'
+ # Initialize global settings first, possibly loading a custom config file
+ settings.init(cfgfile)
+
+ # Initialise tools
+ tools.init()
+
# Load navdatabase in all versions of BlueSky
# Only the headless server doesn't need this
if not headless:
diff --git a/bluesky/core/plugin.py b/bluesky/core/plugin.py
index 033beb7bfd..2f25c4b6b9 100644
--- a/bluesky/core/plugin.py
+++ b/bluesky/core/plugin.py
@@ -1,8 +1,9 @@
""" Implementation of BlueSky's plugin system. """
import ast
+import glob
+
from bluesky.stack.stackbase import sender
from os import path
-from pathlib import Path
import sys
import imp
import bluesky as bs
@@ -90,7 +91,7 @@ def load(cls, name):
@classmethod
def find_plugins(cls, reqtype):
''' Create plugin wrapper objects based on source code of potential plug-in files. '''
- for fname in Path(settings.plugin_path).rglob('*.py'):
+ for fname in glob.glob('{}/**/*.py'.format(settings.plugin_path), recursive=True):
with open(fname, 'rb') as f:
source = f.read()
try:
diff --git a/bluesky/network/client.py b/bluesky/network/client.py
index 3736f95649..09b810e105 100644
--- a/bluesky/network/client.py
+++ b/bluesky/network/client.py
@@ -10,6 +10,7 @@
class Client:
+ ''' Base class for (GUI) clients of a BlueSky server. '''
def __init__(self, actnode_topics=b''):
ctx = zmq.Context.instance()
self.event_io = ctx.socket(zmq.DEALER)
@@ -38,20 +39,24 @@ def __init__(self, actnode_topics=b''):
bluesky.scr = self
def start_discovery(self):
+ ''' Start UDP-based discovery of available BlueSky servers. '''
if not self.discovery:
self.discovery = Discovery(self.client_id)
self.poller.register(self.discovery.handle, zmq.POLLIN)
self.discovery.send_request()
def stop_discovery(self):
+ ''' Stop UDP-based discovery. '''
if self.discovery:
self.poller.unregister(self.discovery.handle)
self.discovery = None
def get_hostid(self):
+ ''' Return the id of the host that this client is connected to. '''
return self.host_id
def sender(self):
+ ''' Return the id of the sender of the most recent event. '''
return self.sender_id
def event(self, name, data, sender_id):
@@ -69,15 +74,42 @@ def actnode_changed(self, newact):
to implement actual actnode change handling. '''
print('Client active node changed.')
- def subscribe(self, streamname, node_id=b''):
- ''' Subscribe to a stream. '''
+ def subscribe(self, streamname, node_id=b'', actonly=False):
+ ''' Subscribe to a stream.
+
+ Arguments:
+ - streamname: The name of the stream to subscribe to
+ - node_id: The id of the node from which to receive the stream (optional)
+ - actonly: Set to true if you only want to receive this stream from
+ the active node.
+ '''
+ if actonly and not node_id and streamname not in self.acttopics:
+ self.acttopics.append(streamname)
+ node_id = self.act
self.stream_in.setsockopt(zmq.SUBSCRIBE, streamname + node_id)
def unsubscribe(self, streamname, node_id=b''):
- ''' Unsubscribe from a stream. '''
+ ''' Unsubscribe from a stream.
+
+ Arguments:
+ - streamname: The name of the stream to unsubscribe from.
+ - node_id: ID of the specific node to unsubscribe from.
+ This is also used when switching active nodes.
+ '''
+ if not node_id and streamname in self.acttopics:
+ self.acttopics.remove(streamname)
+ node_id = self.act
self.stream_in.setsockopt(zmq.UNSUBSCRIBE, streamname + node_id)
def connect(self, hostname='localhost', event_port=0, stream_port=0, protocol='tcp'):
+ ''' Connect client to a server.
+
+ Arguments:
+ - hostname: Network name or ip of the server to connect to
+ - event_port: Network port to use for event communication
+ - stream_port: Network port to use for stream communication
+ - protocol: Network protocol to use
+ '''
conbase = '{}://{}'.format(protocol, hostname)
econ = conbase + (':{}'.format(event_port) if event_port else '')
scon = conbase + (':{}'.format(stream_port) if stream_port else '')
@@ -160,6 +192,7 @@ def _getroute(self, target):
return None
def actnode(self, newact=None):
+ ''' Set the new active node, or return the current active node. '''
if newact:
route = self._getroute(newact)
if route is None:
@@ -178,9 +211,18 @@ def actnode(self, newact=None):
return self.act
def addnodes(self, count=1):
+ ''' Tell the server to add 'count' nodes. '''
self.send_event(b'ADDNODES', count)
def send_event(self, name, data=None, target=None):
+ ''' Send an event to one or all simulation node(s).
+
+ Arguments:
+ - name: Name of the event
+ - data: Data to send as payload
+ - target: Destination of this event. Event is sent to all nodes
+ if * is specified as target.
+ '''
pydata = msgpack.packb(data, default=encode_ndarray, use_bin_type=True)
if not target:
self.event_io.send_multipart(self.actroute + [self.act, name, pydata])
diff --git a/bluesky/settings.py b/bluesky/settings.py
index 01d8f651e5..1da25e5822 100644
--- a/bluesky/settings.py
+++ b/bluesky/settings.py
@@ -5,6 +5,7 @@
import shutil
import site
import inspect
+from pathlib import Path
def init(cfgfile=''):
@@ -30,11 +31,19 @@ def init(cfgfile=''):
root_dirs += site.getsitepackages()
# search for bluesky shared data directory
+ found_dir = False
for root_dir in root_dirs:
dirpath = os.path.join(root_dir, 'share', 'bluesky')
if os.path.exists(dirpath):
srcdir = dirpath
+ found_dir = True
break
+
+ # if the path does not exist, it's worth trying the project root. This
+ # would work if the package was cloned from the git and is installed
+ # with "pip install -e ."
+ if not found_dir:
+ srcdir = get_project_root()
datadir = os.path.join(rundir, 'data')
cachedir = os.path.join(rundir, 'data/cache')
@@ -99,6 +108,18 @@ def init(cfgfile=''):
exec(compile(open(cfgfile).read(), cfgfile, 'exec'), globals())
+ # Use the path specified in cfgfile if available
+ if 'cache_path' in globals():
+ cachedir = globals()['cache_path']
+ if 'log_path' in globals():
+ outdir = globals()['log_path']
+ if 'perf_path_bada' in globals():
+ badadir = globals()['perf_path_bada']
+ if 'scenario_path' in globals():
+ scndir = globals()['scenario_path']
+ if 'plugin_path' in globals():
+ plgdir = globals()['plugin_path']
+
# Update cachedir with python version-specific subfolder
cachedir = os.path.join(cachedir, 'py%d' % sys.version_info[0])
globals()['cache_path'] = cachedir
@@ -196,3 +217,10 @@ def save(fname=None, changes=None):
file_out.write(f'{key} = {value}\n')
return True, f'Saved settings to {fname}'
+
+def get_project_root() -> str:
+ ''' Return the absolute path of the project root. '''
+
+ # return root dir relative to this file, make sure you update it if this
+ # file is moved in the project directory
+ return str(Path(__file__).absolute().parent.parent)
diff --git a/bluesky/simulation/screenio.py b/bluesky/simulation/screenio.py
index 7752bd4e81..05ea4734f5 100644
--- a/bluesky/simulation/screenio.py
+++ b/bluesky/simulation/screenio.py
@@ -27,11 +27,13 @@ def __init__(self):
# Screen state defaults
self.def_pan = (0.0, 0.0)
self.def_zoom = 1.0
+ self.route_all = ""
+
# Screen state overrides per client
self.client_pan = dict()
self.client_zoom = dict()
self.client_ar = dict()
- self.route_acid = dict()
+ self.client_route = dict()
# Dicts of custom aircraft and group colors
self.custacclr = dict()
@@ -58,12 +60,20 @@ def step(self):
self.samplecount += 1
def reset(self):
+ self.client_pan = dict()
+ self.client_zoom = dict()
+ self.client_ar = dict()
+ self.client_route = dict()
+ self.route_all = ''
self.custacclr = dict()
self.custgrclr = dict()
self.samplecount = 0
self.prevcount = 0
self.prevtime = 0.0
+ self.def_pan = (0.0, 0.0)
+ self.def_zoom = 1.0
+
# Communicate reset to gui
bs.net.send_event(b'RESET', b'ALL')
@@ -164,7 +174,11 @@ def trails(self,sw):
def showroute(self, acid):
''' Toggle show route for this aircraft '''
- self.route_acid[stack.sender()] = acid
+ if not stack.sender():
+ self.route_all = acid
+ self.client_route.clear()
+ else:
+ self.client_route[stack.sender()] = acid
return True
def addnavwpt(self, name, lat, lon):
@@ -260,24 +274,42 @@ def send_aircraft_data(self):
bs.net.send_stream(b'ACDATA', data)
def send_route_data(self):
- for sender, acid in self.route_acid.items():
- data = dict()
- data['acid'] = acid
- idx = bs.traf.id2idx(acid)
- if idx >= 0:
- route = bs.traf.ap.route[idx]
- data['iactwp'] = route.iactwp
-
- # We also need the corresponding aircraft position
- data['aclat'] = bs.traf.lat[idx]
- data['aclon'] = bs.traf.lon[idx]
-
- data['wplat'] = route.wplat
- data['wplon'] = route.wplon
-
- data['wpalt'] = route.wpalt
- data['wpspd'] = route.wpspd
-
- data['wpname'] = route.wpname
-
- bs.net.send_stream(b'ROUTEDATA' + (sender or b'*'), data) # Send route data to GUI
+ ''' Send route data to client(s) '''
+ # print(self.client_route, self.route_all)
+ # Case 1: A route is selected by one or more specific clients
+ if self.client_route:
+ for sender, acid in self.client_route.items():
+ _sendrte(sender, acid)
+ # Check if there are other senders and also a scenario-selected route
+ if self.route_all:
+ remclients = bs.sim.clients.difference(self.client_route.keys())
+ #print(bs.sim.clients, remclients)
+ for sender in remclients:
+ _sendrte(sender, self.route_all)
+ # Case 2: only a route selected from scenario file:
+ # Broadcast the same route to everyone
+ elif self.route_all:
+ _sendrte(b'*', self.route_all)
+
+def _sendrte(sender, acid):
+ ''' Local shorthand function to send route. '''
+ data = dict()
+ data['acid'] = acid
+ idx = bs.traf.id2idx(acid)
+ if idx >= 0:
+ route = bs.traf.ap.route[idx]
+ data['iactwp'] = route.iactwp
+
+ # We also need the corresponding aircraft position
+ data['aclat'] = bs.traf.lat[idx]
+ data['aclon'] = bs.traf.lon[idx]
+
+ data['wplat'] = route.wplat
+ data['wplon'] = route.wplon
+
+ data['wpalt'] = route.wpalt
+ data['wpspd'] = route.wpspd
+
+ data['wpname'] = route.wpname
+
+ bs.net.send_stream(b'ROUTEDATA' + (sender or b'*'), data) # Send route data to GUI
diff --git a/bluesky/simulation/simulation.py b/bluesky/simulation/simulation.py
index 8c0797a44b..289f9f6b2a 100644
--- a/bluesky/simulation/simulation.py
+++ b/bluesky/simulation/simulation.py
@@ -50,6 +50,9 @@ def __init__(self):
# Flag indicating whether timestep can be varied to ensure realtime op
self.rtmode = False
+ # Keep track of known clients
+ self.clients = set()
+
def step(self):
''' Perform a simulation timestep. '''
# Simulation starts as soon as there is traffic, or pending commands
@@ -218,6 +221,8 @@ def event(self, eventname, eventdata, sender_rte):
event_processed = True
elif eventname == b'GETSIMSTATE':
+ # Add this client to the list of known clients
+ self.clients.add(sender_rte[-1])
# Send list of stack functions available in this sim to gui at start
stackdict = {cmd : val.brief[len(cmd) + 1:] for cmd, val in bs.stack.get_commands().items()}
shapes = [shape.raw for shape in areafilter.basic_shapes.values()]
diff --git a/bluesky/stack/argparser.py b/bluesky/stack/argparser.py
index c854983388..6e0709eb02 100644
--- a/bluesky/stack/argparser.py
+++ b/bluesky/stack/argparser.py
@@ -20,7 +20,7 @@
# re_getarg = re.compile(r'[\'"]?((?<=[\'"])[^\'"]+|(?= 0 and wpname not in bs.traf.ap.route[refdata.acidx].wpname:
- raise ArgumentError(f'{wpname} not found in the route of {bs.traf.id[refdata.acidx]}')
- return wpname, argstring
-
+ if refdata.acidx >= 0 and wpname in bs.traf.ap.route[refdata.acidx].wpname or wpname == '*':
+ return wpname, argstring
+ raise ArgumentError(f'{wpname} not found in the route of {bs.traf.id[refdata.acidx]}')
class WptArg(Parser):
''' Argument parser for waypoints.
diff --git a/bluesky/stack/basecmds.py b/bluesky/stack/basecmds.py
index a27a726108..13908a0fb1 100644
--- a/bluesky/stack/basecmds.py
+++ b/bluesky/stack/basecmds.py
@@ -5,7 +5,7 @@
import bluesky as bs
from bluesky import settings
from bluesky.core import select_implementation, simtime, varexplorer as ve
-from bluesky.tools import geo, areafilter, plotter
+from bluesky.tools import geo, aero, areafilter, plotter
from bluesky.tools.calculator import calculator
from bluesky.stack.cmdparser import append_commands
@@ -56,44 +56,12 @@ def initbasecmds():
bs.net.addnodes,
"Add a simulation instance/node",
],
- "ADDWPT": [
- "ADDWPT acid, (wpname/lat,lon/FLYBY/FLYOVER/ TAKEOFF,APT/RWY),[alt,spd,afterwp]",
- "acid,wpt,[alt,spd,wpinroute,wpinroute]",
- #
- # lambda *arg: short-hand for using function output as argument, equivalent with:
- #
- # def fun(idx, args):
- # return bs.traf.ap.route[idx].addwptStack(idx, *args)
- # fun(idx,*args)
- #
- lambda idx, *args: bs.traf.ap.route[idx].addwptStack(idx, *args),
- "Add a waypoint to route of aircraft (FMS)",
- ],
- "AFTER": [
- "acid AFTER afterwp ADDWPT (wpname/lat,lon),[alt,spd]",
- "acid,wpinroute,txt,wpt,[alt,spd]",
- lambda idx, * \
- args: bs.traf.ap.route[idx].afteraddwptStack(idx, *args),
- "After waypoint, add a waypoint to route of aircraft (FMS)",
- ],
"AIRWAY": [
"AIRWAY wp/airway",
"txt",
bs.traf.airwaycmd,
"Get info on airway or connections of a waypoint",
],
- "ALT": [
- "ALT acid, alt, [vspd]",
- "acid,alt,[vspd]",
- bs.traf.ap.selaltcmd,
- "Altitude command (autopilot)",
- ],
- "AT": [
- "acid AT wpname [DEL] SPD/ALT/DO [spd/alt/command line]",
- "acid,wpinroute,[txt,txt,...]",
- lambda idx, *args: bs.traf.ap.route[idx].atwptStack(idx, *args),
- "Edit, delete or show spd/alt constraints at a waypoint in the route",
- ],
"ATALT": [
"acid ATALT alt cmd ",
"acid,alt,string",
@@ -113,7 +81,7 @@ def initbasecmds():
"When a/c reaches given speed, execute a command cmd",
],
"BANK": [
- "BANK bankangle[deg]",
+ "BANK acid bankangle[deg]",
"acid,[float]",
bs.traf.setbanklim,
"Set or show bank limit for this vehicle",
@@ -124,14 +92,6 @@ def initbasecmds():
bs.sim.batch,
"Start a scenario file as batch simulation",
],
-
- "BEFORE": [
- "acid BEFORE beforewp ADDWPT (wpname/lat,lon),[alt,spd]",
- "acid,wpinroute,txt,wpt,[alt,spd]",
- lambda idx, * \
- args: bs.traf.ap.route[idx].beforeaddwptStack(idx, *args),
- "Before waypoint, add a waypoint to route of aircraft (FMS)",
- ],
"BLUESKY": ["BLUESKY", "", singbluesky, "Sing"],
"BENCHMARK": [
"BENCHMARK [scenfile,time]",
@@ -153,6 +113,14 @@ def initbasecmds():
calculator,
"Simple in-line math calculator, evaluates expression",
],
+ "CASMACHTHR": [
+ "CASMACHTHR threshold",
+ "float",
+ aero.casmachthr,
+ """Set a threshold below which speeds should be considered as Mach numbers
+ in CRE(ATE), ADDWPT, and SPD commands. Set to zero if speeds should
+ never be considered as Mach number(e.g., when simulating drones)."""
+ ],
"CD": [
"CD [path]",
"[word]",
@@ -167,18 +135,31 @@ def initbasecmds():
),
"Define a circle-shaped area",
],
+ "CLRCRECMD": [
+ "CLRCRECMD",
+ "",
+ bs.traf.clrcrecmd,
+ "CLRCRECMD will clear CRECMD list of commands a/c creation",
+ ],
"COLOR": [
"COLOR name,color (named color or r,g,b)",
"txt,color",
bs.scr.color,
"Set a custom color for an aircraft or shape",
+
],
"CRE": [
"CRE acid,type,lat,lon,hdg,alt,spd",
- "txt,txt,latlon,hdg,alt,spd",
+ "txt,txt,latlon,[hdg,alt,spd]",
bs.traf.cre,
"Create an aircraft",
],
+ "CRECMD": [
+ "CRECMD cmdline (to be added after a/c id )",
+ "string",
+ bs.traf.crecmd,
+ "Add a command for each aircraft to be issued after creation of aircraft",
+ ],
"CRECONFS": [
"CRECONFS id, type, targetid, dpsi, cpa, tlos_hor, dH, tlos_ver, spd",
"txt,txt,acid,hdg,float,time,[alt,time,spd]",
@@ -197,7 +178,7 @@ def initbasecmds():
bs.navdb.defwpt,
"Define a waypoint only for this scenario/run",
],
- "DEL": [
+ "DEL": [
"DEL acid/ALL/WIND/shape",
"acid/txt,...",
lambda *a: bs.traf.wind.clear()
@@ -209,30 +190,7 @@ def initbasecmds():
else bs.traf.delete(a),
"Delete command (aircraft, wind, area)",
],
- "DELRTE": [
- "DELRTE acid",
- "acid",
- lambda idx: bs.traf.ap.route[idx].delrte(idx),
- "Delete for this a/c the complete route/dest/orig (FMS)",
- ],
- "DELWPT": [
- "DELWPT acid,wpname",
- "acid,wpinroute",
- lambda idx, wpname: bs.traf.ap.route[idx].delwpt(wpname, idx),
- "Delete a waypoint from a route (FMS)",
- ],
- "DEST": [
- "DEST acid, latlon/airport",
- "acid,wpt",
- lambda idx, *args: bs.traf.ap.setdestorig("DEST", idx, *args),
- "Set destination of aircraft, aircraft wil fly to this airport",
- ],
- "DIRECT": [
- "DIRECT acid wpname",
- "acid,txt",
- lambda idx, wpname: bs.traf.ap.route[idx].direct(idx, wpname),
- "Go direct to specified waypoint in route (FMS)",
- ],
+
"DIST": [
"DIST lat0, lon0, lat1, lon1",
"latlon,latlon",
@@ -257,12 +215,6 @@ def initbasecmds():
bs.sim.set_dtmult,
"Sel multiplication factor for fast-time simulation",
],
- "DUMPRTE": [
- "DUMPRTE acid",
- "acid",
- lambda idx: bs.traf.ap.route[idx].dumpRoute(idx),
- "Write route to output/routelog.txt",
- ],
"ECHO": [
"ECHO txt",
"string",
@@ -287,12 +239,7 @@ def initbasecmds():
lambda flag, *args: bs.sim.ff(*args) if flag else bs.op(),
"Legacy function for TMX compatibility",
],
- "GETWIND": [
- "GETWIND lat,lon,[alt]",
- "latlon,[alt]",
- bs.traf.wind.get,
- "Get wind at a specified position (and optionally at altitude)",
- ],
+
"GROUP": [
"GROUP [grname, (areaname OR acid,...) ]",
"[txt,acid/txt,...]",
@@ -302,12 +249,6 @@ def initbasecmds():
+ "Returns list of aircraft in group when only a groupname is passed.\n"
+ "A group is created when a group with the given name doesn't exist yet.",
],
- "HDG": [
- "HDG acid,hdg (deg,True or Magnetic)",
- "acid,hdg",
- bs.traf.ap.selhdgcmd,
- "Heading command (autopilot)",
- ],
"HOLD": ["HOLD", "", bs.sim.hold, "Pause(hold) simulation"],
"IMPLEMENTATION": [
"IMPLEMENTATION [base, implementation]",
@@ -333,18 +274,6 @@ def initbasecmds():
lambda name, *coords: areafilter.defineArea(name, "LINE", coords),
"Draw a line on the radar screen",
],
- "LISTRTE": [
- "LISTRTE acid, [pagenr]",
- "acid,[int]",
- lambda idx, *args: bs.traf.ap.route[idx].listrte(idx, *args),
- "Show list of route in window per page of 5 waypoints",
- ],
- "LNAV": [
- "LNAV acid,[ON/OFF]",
- "acid,[onoff]",
- bs.traf.ap.setLNAV,
- "LNAV (lateral FMS mode) switch for autopilot",
- ],
"LSVAR": [
"LSVAR path.to.variable",
"[word]",
@@ -381,24 +310,12 @@ def initbasecmds():
bs.traf.setnoise,
"Turbulence/noise switch",
],
- "NOM": [
- "NOM acid",
- "acid",
- bs.traf.nom,
- "Set nominal acceleration for this aircraft (perf model)",
- ],
"OP": [
"OP",
"",
bs.sim.op,
"Start/Run simulation or continue after hold"
],
- "ORIG": [
- "ORIG acid, latlon/airport",
- "acid,wpt",
- lambda *args: bs.traf.ap.setdestorig("ORIG", *args),
- "Set origin airport of aircraft",
- ],
"PAN": [
"PAN latlon/acid/airport/waypoint/LEFT/RIGHT/ABOVE/DOWN",
"pandir/latlon",
@@ -444,24 +361,12 @@ def initbasecmds():
bs.sim.realtime,
"En-/disable realtime running allowing a variable timestep."],
"RESET": ["RESET", "", bs.sim.reset, "Reset simulation"],
- "RTA": [
- "RTA acid,wpinroute,RTAtime",
- "acid,wpinroute,txt",
- lambda idx, *args: bs.traf.ap.route[idx].SetRTA(idx, *args),
- "Set required time of arrival (RTA) at waypoint in route",
- ],
"SEED": [
"SEED value",
"int",
bs.sim.setseed,
"Set seed for all functions using a randomizer (e.g.mcre,noise)",
],
- "SPD": [
- "SPD acid,spd (CAS-kts/Mach)",
- "acid,spd",
- bs.traf.ap.selspdcmd,
- "Speed command (autopilot)",
- ],
"SSD": [
"SSD ALL/CONFLICTS/OFF or SSD acid0, acid1, ...",
"txt,[...]",
@@ -499,25 +404,7 @@ def initbasecmds():
bs.traf.groups.ungroup,
"Remove aircraft from a group",
],
- "VNAV": [
- "VNAV acid,[ON/OFF]",
- "acid,[onoff]",
- bs.traf.ap.setVNAV,
- "Switch on/off VNAV mode, the vertical FMS mode (autopilot)",
- ],
- "VS": [
- "VS acid,vspd (ft/min)",
- "acid,vspd",
- bs.traf.ap.selvspdcmd,
- "Vertical speed command (autopilot)",
- ],
- "WIND": [
- "WIND lat,lon,alt/*,dir,spd,[alt,dir,spd,alt,...]",
- # last 3 args are repeated
- "latlon,[alt],float,alt/float,...,",
- bs.traf.wind.add,
- "Define a wind vector as part of the 2D or 3D wind field",
- ],
+
"ZOOM": [
"ZOOM IN/OUT or factor",
"float/txt",
@@ -549,15 +436,10 @@ def initbasecmds():
"CLOSE": "QUIT",
"DEBUG": "CALC",
"DELETE": "DEL",
- "DELWP": "DELWPT",
- "DELROUTE": "DELRTE",
- "DIRECTTO": "DIRECT",
- "DIRTO": "DIRECT",
"DISP": "SWRAD",
"END": "QUIT",
"EXIT": "QUIT",
"FWD": "FF",
- "HEADING": "HDG",
"IMPL": "IMPLEMENTATION",
"IMPLEMENT": "IMPLEMENTATION",
"LINES": "POLYLINE",
@@ -574,12 +456,9 @@ def initbasecmds():
"RUN": "OP",
"RUNWAYS": "POS",
"SAVE": "SAVEIC",
- "SPEED": "SPD",
"START": "OP",
"TRAILS": "TRAIL",
- "TURN": "HDG",
- "VAR": "MAGVAR",
- "WPTYPE":"ADDWPT"
+ "VAR": "MAGVAR"
}
append_commands(cmddict, synonyms)
diff --git a/bluesky/stack/cmdparser.py b/bluesky/stack/cmdparser.py
index 70eca03ae9..063281b516 100644
--- a/bluesky/stack/cmdparser.py
+++ b/bluesky/stack/cmdparser.py
@@ -1,6 +1,6 @@
''' Stack Command implementation. '''
import inspect
-
+import sys, os
from bluesky.stack.argparser import Parameter, getnextarg, ArgumentError
@@ -159,9 +159,21 @@ def callback(self, function):
def helptext(self, subcmd=''):
''' Return complete help text. '''
- msg = f'{self.help}\nUsage:\n{self.brief}'
+ msg = f'
{self.help}
\nUsage:\n{self.brief}'
if self.aliases:
msg += ('\nCommand aliases: ' + ','.join(self.aliases))
+ if self._callback.__name__ == '':
+ msg += '\nAnonymous (lambda) function, implemented in '
+ else:
+ msg += f'\nFunction {self._callback.__name__}(), implemented in '
+ if hasattr(self._callback, '__code__'):
+ fname = self._callback.__code__.co_filename
+ fname_stripped = fname.replace(os.getcwd(), '').lstrip('/')
+ firstline = self._callback.__code__.co_firstlineno
+ msg += f'{fname_stripped} on line {firstline}'
+ else:
+ msg += f'module {self._callback.__module__}'
+
return msg
def brieftext(self):
diff --git a/bluesky/tools/__init__.py b/bluesky/tools/__init__.py
index a142390833..91f97b75cb 100644
--- a/bluesky/tools/__init__.py
+++ b/bluesky/tools/__init__.py
@@ -8,12 +8,13 @@
except ImportError:
from . import geo
print('Using Python-based geo functions')
- print("Reading magnetic variation data")
- geo.initdecl_data()
else:
from . import geo
print('Using Python-based geo functions')
- print("Reading magnetic variation data")
- geo.initdecl_data()
from . import cachefile
+
+
+def init():
+ print("Reading magnetic variation data")
+ geo.initdecl_data()
diff --git a/bluesky/tools/aero.py b/bluesky/tools/aero.py
index 84cb1ecb1c..6368e049ca 100644
--- a/bluesky/tools/aero.py
+++ b/bluesky/tools/aero.py
@@ -3,6 +3,10 @@
from math import *
import numpy as np
+from bluesky import settings
+
+
+settings.set_variable_defaults(casmach_threshold=2.0)
# International standard atmpshere only up to 72000 ft / 22 km
#
@@ -27,6 +31,25 @@
beta = -0.0065 # [K/m] ISA temp gradient below tropopause
Rearth = 6371000. # m Average earth radius
a0 = np.sqrt(gamma*R*T0) # sea level speed of sound ISA
+casmach_thr = settings.casmach_threshold # Threshold below which speeds should
+ # be considered as Mach numbers in casormach* functions
+
+
+def casmachthr(threshold:float=None):
+ """ CASMACHTHR threshold
+
+ Set a threshold below which speeds should be considered as Mach numbers
+ in CRE(ATE), ADDWPT, and SPD commands. Set to zero if speeds should
+ never be considered as Mach number (e.g., when simulating drones).
+
+ Argument:
+ - threshold: CAS speed threshold [m/s]
+ """
+ if threshold is None:
+ return True, f'CASMACHTHR: The current CAS/Mach threshold is {casmach_thr} m/s ({casmach_thr / kts} kts'
+
+ globals()['casmach_thr'] = threshold
+ return True, f'CASMACHTHR: Set CAS/Mach threshold to {threshold}'
#
@@ -59,7 +82,17 @@
# ------------------------------------------------------------------------------
# Vectorized aero functions
# ------------------------------------------------------------------------------
-def vatmos(h): # h in m
+def vatmos(h):
+ """ Calculate atmospheric pressure, density, and temperature for a given altitude.
+
+ Arguments:
+ - h: Altitude [m]
+
+ Returns:
+ - p: Pressure [Pa]
+ - rho: Density [kg / m3]
+ - T: Temperature [K]
+ """
# Temp
T = vtemp(h)
@@ -74,23 +107,55 @@ def vatmos(h): # h in m
return p, rho, T
-def vtemp(h): # h [m]
+def vtemp(h):
+ """ Calculate atmospheric temperature for a given altitude.
+
+ Arguments:
+ - h: Altitude [m]
+
+ Returns:
+ - T: Temperature [K]
+ """
T = np.maximum(288.15 - 0.0065 * h, Tstrat)
return T
# Atmos wrappings:
-def vpressure(h): # h [m]
- p, r, T = vatmos(h)
+def vpressure(h):
+ """ Calculate atmospheric pressure for a given altitude.
+
+ Arguments:
+ - h: Altitude [m]
+
+ Returns:
+ - p: Pressure [Pa]
+ """
+ p, _, _ = vatmos(h)
return p
-def vdensity(h): # air density at given altitude h [m]
- p, r, T = vatmos(h)
+def vdensity(h):
+ """ Calculate atmospheric density for a given altitude.
+
+ Arguments:
+ - h: Altitude [m]
+
+ Returns:
+ - rho: Density [kg / m3]
+ """
+ _, r, _ = vatmos(h)
return r
-def vvsound(h): # Speed of sound for given altitude h [m]
+def vvsound(h):
+ """ Calculate the speed of sound for a given altitude.
+
+ Arguments:
+ - h: Altitude [m]
+
+ Returns:
+ - a: Speed of sound [m/s]
+ """
T = vtemp(h)
a = np.sqrt(gamma * R * T)
return a
@@ -98,47 +163,95 @@ def vvsound(h): # Speed of sound for given altitude h [m]
# ---------Speed conversions---h in [m]------------------
def vtas2mach(tas, h):
- """ True airspeed (tas) to mach number conversion """
+ """ True airspeed (tas) to mach number conversion for numpy arrays.
+
+ Arguments:
+ - tas: True airspeed [m/s]
+ - h: Altitude [m]
+
+ Returns:
+ - M: Mach number [-]
+ """
a = vvsound(h)
- M = tas / a
- return M
+ mach = tas / a
+ return mach
-def vmach2tas(M, h):
- """ True airspeed (tas) to mach number conversion """
+def vmach2tas(mach, h):
+ """ True airspeed (tas) to mach number conversion for numpy arrays.
+
+ Arguments:
+ - mach: Mach number [-]
+ - h: Altitude [m]
+
+ Returns:
+ - tas: True airspeed [m/s]
+ """
a = vvsound(h)
- tas = M * a
+ tas = mach * a
return tas
def veas2tas(eas, h):
- """ Equivalent airspeed to true airspeed """
+ """ Equivalent airspeed to true airspeed conversion for numpy arrays.
+
+ Arguments:
+ - eas: Equivalent airspeed [m/s]
+ - h: Altitude [m]
+
+ Returns:
+ - tas: True airspeed [m/s]
+ """
rho = vdensity(h)
tas = eas * np.sqrt(rho0 / rho)
return tas
def vtas2eas(tas, h):
- """ True airspeed to equivent airspeed """
+ """ True airspeed to equivent airspeed conversion for numpy arrays.
+
+ Arguments:
+ - tas: True airspeed [m/s]
+ - h: Altitude [m]
+
+ Returns:
+ - eas: Equivalent airspeed [m/s]
+ """
rho = vdensity(h)
- eas = tas*np.sqrt(rho / rho0)
+ eas = tas * np.sqrt(rho / rho0)
return eas
def vcas2tas(cas, h):
- """ cas2tas conversion both m/s """
- p, rho, T = vatmos(h)
- qdyn = p0*((1.+rho0*cas*cas/(7.*p0))**3.5-1.)
- tas = np.sqrt(7.*p/rho*((1.+qdyn/p)**(2./7.)-1.))
+ """ Calibrated to true airspeed conversion for numpy arrays.
+
+ Arguments:
+ - cas: Calibrated airspeed [m/s]
+ - h: Altitude [m]
+
+ Returns:
+ - tas: True airspeed [m/s]
+ """
+ p, rho, _ = vatmos(h)
+ qdyn = p0 * ((1.0 + rho0 * cas * cas / (7.0 * p0)) ** 3.5 - 1.0)
+ tas = np.sqrt(7.0 * p / rho * ((1.0 + qdyn / p) ** (2.0 / 7.0) - 1.0))
# cope with negative speed
- tas = np.where(cas<0, -1*tas, tas)
+ tas = np.where(cas < 0, -1 * tas, tas)
return tas
def vtas2cas(tas, h):
- """ tas2cas conversion both m/s """
- p, rho, T = vatmos(h)
+ """ True to calibrated airspeed conversion for numpy arrays.
+
+ Arguments:
+ - tas: True airspeed [m/s]
+ - h: Altitude [m]
+
+ Returns:
+ cas: Calibrated airspeed [m/s]
+ """
+ p, rho, _ = vatmos(h)
qdyn = p*((1.+rho*tas*tas/(7.*p))**3.5-1.)
cas = np.sqrt(7.*p0/rho0*((qdyn/p0+1.)**(2./7.)-1.))
@@ -147,43 +260,87 @@ def vtas2cas(tas, h):
return cas
-def vmach2cas(M, h):
- """ Mach to CAS conversion """
- tas = vmach2tas(M, h)
+def vmach2cas(mach, h):
+ """ Mach to calibrated airspeed conversion for numpy arrays.
+
+ Arguments:
+ - mach: Mach number [-]
+ - h: Altitude [m]
+
+ Returns:
+ - cas: Calibrated airspeed [m/s]
+ """
+ tas = vmach2tas(mach, h)
cas = vtas2cas(tas, h)
return cas
def vcas2mach(cas, h):
- """ CAS to Mach conversion """
+ """ Calibrated airspeed to Mach conversion for numpy arrays.
+
+ Arguments:
+ - cas: Calibrated airspeed [m/s]
+ - h: Altitude [m]
+
+ Returns:
+ - mach: Mach number [-]
+ """
tas = vcas2tas(cas, h)
M = vtas2mach(tas, h)
return M
def vcasormach(spd, h):
- ismach = np.logical_and(spd > 0.1, spd < 2.0)
+ """ Interpret input speed as either CAS or a Mach number, and return TAS, CAS, and Mach.
+
+ Arguments:
+ - spd: Airspeed. Interpreted as Mach number [-] when its value is below the
+ CAS/Mach threshold. Otherwise interpreted as CAS [m/s].
+ - h: Altitude [m]
+
+ Returns:
+ - tas: True airspeed [m/s]
+ - cas: Calibrated airspeed [m/s]
+ - mach: Mach number [-]
+ """
+ ismach = np.logical_and(spd > 0.1, spd < casmach_thr)
tas = np.where(ismach, vmach2tas(spd, h), vcas2tas(spd, h))
cas = np.where(ismach, vtas2cas(tas, h), spd)
- m = np.where(ismach, spd, vtas2mach(tas, h))
- return tas, cas, m
+ mach = np.where(ismach, spd, vtas2mach(tas, h))
+ return tas, cas, mach
+
def vcasormach2tas(spd, h):
- tas = np.where(np.abs(spd) < 2.0, vmach2tas(spd, h), vcas2tas(spd, h))
- return tas
+ """ Interpret input speed as either CAS or a Mach number, and return TAS.
+
+ Arguments:
+ - spd: Airspeed. Interpreted as Mach number [-] when its value is below the
+ CAS/Mach threshold. Otherwise interpreted as CAS [m/s].
+ - h: Altitude [m]
+
+ Returns:
+ - tas: True airspeed [m/s]
+ """
+ ismach = np.logical_and(spd > 0.1, spd < casmach_thr)
+ return np.where(ismach, vmach2tas(spd, h), vcas2tas(spd, h))
+
+def crossoveralt(cas, mach):
+ """ Calculate crossover altitude for given CAS and Mach number.
-def crossoveralt(vcas, mach):
- ''' Calculate crossover altitude for given CAS and Mach number.
-
Calculates the altitude where the given CAS and Mach values
correspond to the same true airspeed.
(BADA User Manual 3.12, p. 12)
- Returns: altitude in meters.
- '''
+ Arguments:
+ - cas: Calibrated airspeed [m/s]
+ - mach: Mach number [-]
+
+ Returns:
+ - Altitude [m].
+ """
# Delta: pressure ratio at the transition altitude
- delta = (((1.0 + 0.5 * (gamma - 1.0) * (vcas / a0) ** 2) **
+ delta = (((1.0 + 0.5 * (gamma - 1.0) * (cas / a0) ** 2) **
(gamma / (gamma - 1.0)) - 1.0) /
((1.0 + 0.5 * (gamma - 1.0) * mach ** 2) **
(gamma / (gamma - 1.0)) - 1.0))
@@ -387,7 +544,7 @@ def cas2mach(cas, h):
return M
def casormach(spd,h):
- if 0.1 < spd < 2.0:
+ if 0.1 < spd < casmach_thr:
# Interpret spd as Mach number
tas = mach2tas(spd, h)
cas = mach2cas(spd, h)
@@ -400,7 +557,7 @@ def casormach(spd,h):
return tas, cas, m
def casormach2tas(spd,h):
- if 0.1 < spd < 2.0:
+ if 0.1 < spd < casmach_thr:
# Interpret spd as Mach number
tas = mach2tas(spd, h)
else:
diff --git a/bluesky/tools/areafilter.py b/bluesky/tools/areafilter.py
index db958d8114..49e0508f5c 100644
--- a/bluesky/tools/areafilter.py
+++ b/bluesky/tools/areafilter.py
@@ -1,7 +1,7 @@
"""Area filter module"""
-from matplotlib.path import Path
from weakref import WeakValueDictionary
import numpy as np
+from matplotlib.path import Path
from rtree import index
import bluesky as bs
from bluesky.tools.geo import kwikdist
@@ -40,6 +40,9 @@ def defineArea(areaname, areatype, coordinates, top=1e9, bottom=-1e9):
# Pass the shape on to the screen object
bs.scr.objappend(areatype, areaname, coordinates)
+ return True, f'Created {areatype} {areaname}'
+
+
def checkInside(areaname, lat, lon, alt):
""" Check if points with coordinates lat, lon, alt are inside area with name 'areaname'.
Returns an array of booleans. True == Inside"""
@@ -57,11 +60,12 @@ def deleteArea(areaname):
def reset():
""" Clear all data. """
basic_shapes.clear()
+ Shape.reset()
def get_intersecting(lat0, lon0, lat1, lon1):
''' Return all shapes that intersect with a specified rectangular area.
-
+
Arguments:
- lat0/1, lon0/1: Coordinates of the top-left and bottom-right corner
of the intersection area.
@@ -72,7 +76,7 @@ def get_intersecting(lat0, lon0, lat1, lon1):
def get_knearest(lat0, lon0, lat1, lon1, k=1):
''' Return the k nearest shapes to a specified rectangular area.
-
+
Arguments:
- lat0/1, lon0/1: Coordinates of the top-left and bottom-right corner
of the relevant area.
@@ -83,6 +87,9 @@ def get_knearest(lat0, lon0, lat1, lon1, k=1):
class Shape:
+ '''
+ Base class of BlueSky shapes
+ '''
# Global counter to keep track of used shape ids
max_area_id = 0
@@ -93,6 +100,13 @@ class Shape:
# RTree of all areas for efficient geospatial searching
areatree = index.Index()
+ @classmethod
+ def reset(cls):
+ ''' Reset shape data when simulation is reset. '''
+ # Weak dicts and areatree should be cleared automatically
+ # Reset max area id
+ cls.max_area_id = 0
+
def __init__(self, name, coordinates, top=1e9, bottom=-1e9):
self.raw = dict(name=name, shape=self.kind(), coordinates=coordinates)
self.name = name
@@ -102,7 +116,7 @@ def __init__(self, name, coordinates, top=1e9, bottom=-1e9):
lat = coordinates[::2]
lon = coordinates[1::2]
self.bbox = [min(lat), min(lon), max(lat), max(lon)]
-
+
# Global weak reference and tree storage
self.area_id = Shape.max_area_id
Shape.max_area_id += 1
@@ -110,12 +124,18 @@ def __init__(self, name, coordinates, top=1e9, bottom=-1e9):
Shape.areas_by_name[self.name] = self
Shape.areatree.insert(self.area_id, self.bbox)
- def __delete__(self):
+ def __del__(self):
# Objects are removed automatically from the weak-value dicts,
# but need to be manually removed from the rtree
Shape.areatree.delete(self.area_id, self.bbox)
def checkInside(self, lat, lon, alt):
+ ''' Returns True (or boolean array) if coordinate lat, lon, alt lies
+ within this shape.
+
+ Reimplement this function in the derived shape classes for this to
+ work.
+ '''
return False
def _str_vrange(self):
@@ -139,6 +159,7 @@ def kind(cls):
class Line(Shape):
+ ''' A line shape '''
def __init__(self, name, coordinates):
super().__init__(name, coordinates)
@@ -149,6 +170,7 @@ def __str__(self):
class Box(Shape):
+ ''' A box shape '''
def __init__(self, name, coordinates, top=1e9, bottom=-1e9):
super().__init__(name, coordinates, top, bottom)
# Sort the order of the corner points
@@ -163,8 +185,8 @@ def checkInside(self, lat, lon, alt):
((self.bottom <= alt) & (alt <= self.top))
-
class Circle(Shape):
+ ''' A circle shape '''
def __init__(self, name, coordinates, top=1e9, bottom=-1e9):
super().__init__(name, coordinates, top, bottom)
self.clat = coordinates[0]
@@ -183,6 +205,7 @@ def __str__(self):
class Poly(Shape):
+ ''' A polygon shape '''
def __init__(self, name, coordinates, top=1e9, bottom=-1e9):
super().__init__(name, coordinates, top, bottom)
self.border = Path(np.reshape(coordinates, (len(coordinates) // 2, 2)))
diff --git a/bluesky/tools/geo.py b/bluesky/tools/geo.py
index 5cac4b12a3..d795904f4e 100644
--- a/bluesky/tools/geo.py
+++ b/bluesky/tools/geo.py
@@ -1,7 +1,12 @@
""" This module defines a set of standard geographic functions and constants for
easy use in BlueSky. """
+from os import path
import numpy as np
from math import *
+
+from bluesky import settings
+
+
# Constants
nm = 1852. # m 1 nautical mile
@@ -89,7 +94,6 @@ def qdrdist(latd1, lond1, latd2, lond2):
lat2 = np.radians(latd2)
lon2 = np.radians(lond2)
-
#root = sin1 * sin1 + coslat1 * coslat2 * sin2 * sin2
#d = 2.0 * r * np.arctan2(np.sqrt(root) , np.sqrt(1.0 - root))
# d =2.*r*np.arcsin(np.sqrt(sin1*sin1 + coslat1*coslat2*sin2*sin2))
@@ -100,15 +104,15 @@ def qdrdist(latd1, lond1, latd2, lond2):
# Bearing from Ref. http://www.movable-type.co.uk/scripts/latlong.html
- sin1 = np.sin(0.5 * (lat2 - lat1))
- sin2 = np.sin(0.5 * (lon2 - lon1))
+ # sin1 = np.sin(0.5 * (lat2 - lat1))
+ # sin2 = np.sin(0.5 * (lon2 - lon1))
coslat1 = np.cos(lat1)
coslat2 = np.cos(lat2)
-
qdr = np.degrees(np.arctan2(np.sin(lon2 - lon1) * coslat2,
- coslat1 * np.sin(lat2) - np.sin(lat1) * coslat2 * np.cos(lon2 - lon1)))
+ coslat1 * np.sin(lat2) -
+ np.sin(lat1) * coslat2 * np.cos(lon2 - lon1)))
return qdr, d/nm
@@ -169,7 +173,7 @@ def qdrdist_matrix(lat1, lon1, lat2, lon2):
def latlondist(latd1, lond1, latd2, lond2):
- """ Calculates distance using haversine formulae and avaerage r from wgs'84
+ """ Calculates only distance using haversine notation of the same formulae and average r from wgs'84
Input:
two lat/lon positions in degrees
Out:
@@ -284,10 +288,10 @@ def qdrpos(latd1, lond1, qdr, dist):
# Calculate new position
lat2 = np.arcsin(np.sin(lat1)*np.cos(dist/R) +
- np.cos(lat1)*np.sin(dist/R)*np.cos(np.radians(qdr)))
+ np.cos(lat1)*np.sin(dist/R)*np.cos(np.radians(qdr)))
lon2 = lon1 + np.arctan2(np.sin(np.radians(qdr))*np.sin(dist/R)*np.cos(lat1),
- np.cos(dist/R) - np.sin(lat1)*np.sin(lat2))
+ np.cos(dist/R) - np.sin(lat1)*np.sin(lat2))
return np.degrees(lat2), np.degrees(lon2)
@@ -389,11 +393,10 @@ def kwikpos(latd1, lond1, qdr, dist):
return latd2,lond2
def magdec(latd, lond):
- global decl_read, decl_lat_lon
"""
Gives magnetic declination (also called magnetic variation) at given
position, interpolated from an external data table. The interpolation is
- done using an object of scipy.interpolate.RectSphereBivariateSpline
+ done using an object of scipy.interpolate.RectSphereBivariateSpline
interpo_dec, which is generated by the function init_interpo_dec() defined
in the same module (geo.py). The interpo_dec object rvaluates the magnetic
declination at any latitude and longitude (latd, lond).
@@ -403,9 +406,9 @@ def magdec(latd, lond):
init_interpo_dec() will be called.
The arguments of interpo_dec.ev() are 1-D arrays of latitudes and
longitudes in radians, with latitude ranging from 0 to pi and longitude
- ranging from 0 to 2pi.
- In:
- latd, lond [deg] Position at which the magnetic declination is
+ ranging from 0 to 2pi.
+ In:
+ latd, lond [deg] Position at which the magnetic declination is
evaluated (floats)
Out:
d_hdg [deg] Magnetic declination, the angle of difference
@@ -414,19 +417,22 @@ def magdec(latd, lond):
(10 deg), then a compass at that location pointing
north (magnetic) would actually align 10 deg W of
true North. True North would be 10 deg E relative to
- the magnetic North direction given by the compass.
+ the magnetic North direction given by the compass.
Declination varies with location and slowly changes
in time. Referenced from
https://www.ngdc.noaa.gov/geomag/calculators/help/igrfgridHelp.html
- In short, magnetic heading = true heading - d_hdg, (Reminder MTV : M = T - V)
+ In short, magnetic heading = true heading - d_hdg,
+ (Reminder MTV : M = T - V)
or, true heading = magnetic heading + d_hdg.
Created by : Yaofu Zhou
Modified by J.M. Hoekstra
- Reason: Segmentation fault caused by Scipy's BiVariateSpline interpolation for some data on some machines, so it was
- changed to linear interpolation. Difference in methods has been inspected: it is way less than the inaccuracy
- of the actual data. Axes were regularly spaced at one degree. The direct manual linear interpolation also 6 x times faster.
- TODO: Relocate datafile (make part of navdata?) and move reading of datafile to init phase
+ Reason: Segmentation fault caused by Scipy's BiVariateSpline interpolation
+ for some data on some machines, so it was changed to linear interpolation.
+ Difference in methods has been inspected: it is way less than the inaccuracy
+ of the actual data. Axes were regularly spaced at one degree. The direct
+ manual linear interpolation also 6 x times faster.
"""
+ global decl_read, decl_lat_lon
if not decl_read:
initdecl_data()
decl_read = True
@@ -487,9 +493,9 @@ def initdecl_data():
# lat : 89 ... -90
# Lon: -180 ... 179
global decl_read, decl_lat_lon
-
- dec_table = np.genfromtxt("bluesky/tools/geo_declination_data.csv",\
+ dec_table = np.genfromtxt(path.join(settings.navdata_path, 'geo_declination_data.csv'),
comments='#',delimiter=",")
+
decl = dec_table[:,4]
# <----lon ---->
@@ -513,5 +519,6 @@ def initdecl_data():
# Command MAGVAR to get magnetic variation at position lat,lon
def magdeccmd(latdeg,londeg):
+ ''' MAGVAR Get magnetic variation at position lat/lon. '''
return True,"Magnetic variation at "+str(latdeg)+","+str(londeg) + \
" = "+str(magdec(latdeg,londeg))+" deg"
diff --git a/bluesky/traffic/activewpdata.py b/bluesky/traffic/activewpdata.py
index dce024ff4f..2209b7dde6 100644
--- a/bluesky/traffic/activewpdata.py
+++ b/bluesky/traffic/activewpdata.py
@@ -10,6 +10,11 @@ def __init__(self):
with self.settrafarrays():
self.lat = np.array([]) # [deg] Active WP latitude
self.lon = np.array([]) # [deg] Active WP longitude
+ self.nextturnlat= np.array([]) # [deg] Next turn WP latitude
+ self.nextturnlon= np.array([]) # [deg] Next turn WP longitude
+ self.nextturnspd= np.array([]) # [m/s] Next turn WP speed
+ self.nextturnrad= np.array([]) # [m] Next turn WP turn radius
+ self.nextturnidx= np.array([]) # [-] Next turn WP index
self.nextaltco = np.array([]) # [m] Altitude to arrive at after distance xtoalt
self.xtoalt = np.array([]) # [m] Distance to next altitude constraint
self.nextspd = np.array([]) # [CAS[m/s]/Mach] save speed from next wp for next leg
@@ -18,21 +23,29 @@ def __init__(self):
self.vs = np.array([]) # [m/s] Active vertical speed to use
self.turndist = np.array([]) # [m] Distance when to turn to next waypoint
self.flyby = np.array([]) # Flyby switch, when False, flyover (turndist=0.0)
- self.flyturn = np.array([]) # Flyturn switch, when False, when Fkase, use flyby/flyover
+ self.flyturn = np.array([]) # Flyturn switch, customised turn parameters; when False, use flyby/flyover
self.turnrad = np.array([]) # Flyturn turn radius (<0 => not specified)
self.turnspd = np.array([]) # [m/s, CAS] Flyturn turn speed for next turn (<=0 => not specified)
self.oldturnspd = np.array([]) # [TAS, m/s] Flyturn turn speed for previous turn (<=0 => not specified)
self.turnfromlastwp = np.array([]) # Currently in flyturn-mode from last waypoint (old turn, beginning of leg)
self.turntonextwp = np.array([]) # Currently in flyturn-mode to next waypoint (new flyturn mode, end of leg)
- self.torta = np.array([]) # [s] NExt req Time of Arrival (RTA) (-999. = None)
- self.xtorta = np.array([]) # [m] distance ot next RTA
+ self.torta = np.array([]) # [s] Next req Time of Arrival (RTA) (-999. = None)
+ self.xtorta = np.array([]) # [m] distance to next RTA
self.next_qdr = np.array([]) # [deg] track angle of next leg
- self.swlastwp = np.array([],dtype=np.bool) # switch indicsting this is the last waypoint
+ self.swlastwp = np.array([],dtype=np.bool) # switch indicating this is the last waypoint
+ self.curlegdir = np.array([]) # [deg] direction to active waypoint upon activation
+ self.curleglen = np.array([]) # [deg] direction to active waypoint upon activation
def create(self, n=1):
super().create(n)
# LNAV route navigation
- self.lat[-n:] = 89.99 # [deg]Active WP latitude
+ self.lat[-n:] = 0. # [deg]Active WP latitude
+ self.lon[-n:] = 0. # [deg]Active WP longitude
+ self.nextturnlat[-n:]= 0 # [deg] Next turn WP latitude
+ self.nextturnlon[-n:]= 0 # [deg] Next turn WP longitude
+ self.nextturnspd[-n:]= -999. # [m/s] Next turn WP speed
+ self.nextturnrad[-n:]= -999. # [m] Next turn WP radius
+ self.nextturnidx[-n:]= -999. # [-] Next turn WP index
self.nextspd[-n:] = -999. # [CAS[m/s]/Mach]Next leg speed from current WP
self.spd[-n:] = -999. # [CAS[m/s]/Mach]Active WP speed
self.spdcon[-n:] = -999. # [CAS[m/s]/Mach]Active WP speed constraint
@@ -47,9 +60,10 @@ def create(self, n=1):
self.torta[-n:] = -999.0 # [s] Req Time of Arrival (RTA) for next wp (-999. = None)
self.xtorta[-n:] = 0.0 # Distance to next RTA
self.next_qdr[-n:] = -999.0 # [deg] bearing next leg
- #self.curlegdir[-n:] =
self.swlastwp[-n:] = False # Switch indicating active waypoint is last waypoint
-
+ self.curlegdir[-n:] = -999.0 # [deg] direction to active waypoint upon activation
+ self.curleglen[-n:] = -999.0 # [nm] distance to active waypoint upon activation
+
def Reached(self, qdr, dist, flyby, flyturn, turnradnm,swlastwp):
# Calculate distance before waypoint where to start the turn
# Note: this is a vectorized function, called with numpy traffic arrays
@@ -62,7 +76,7 @@ def Reached(self, qdr, dist, flyby, flyturn, turnradnm,swlastwp):
# First calculate turn distance
next_qdr = np.where(self.next_qdr < -900., qdr, self.next_qdr)
turntas = np.where(bs.traf.actwp.turnspd<0.0,bs.traf.tas,bs.traf.actwp.turnspd)
- flybyturndist,turnrad = self.calcturn(turntas,bs.traf.bank,qdr,next_qdr,turnradnm)
+ flybyturndist,turnrad = self.calcturn(turntas,bs.traf.ap.bankdef,qdr,next_qdr,turnradnm)
# Turb dist iz ero for flyover, calculated distance for others
self.turndist = np.logical_or(flyby,flyturn)*flybyturndist
diff --git a/bluesky/traffic/asas/mvp.py b/bluesky/traffic/asas/mvp.py
index 5ab0cc849e..b113b42ef6 100644
--- a/bluesky/traffic/asas/mvp.py
+++ b/bluesky/traffic/asas/mvp.py
@@ -5,6 +5,7 @@
class MVP(ConflictResolution):
+ ''' Conflict resolution using the Modified Voltage Potential Method. '''
def __init__(self):
super().__init__()
# [-] switch to limit resolution to the horizontal direction
@@ -16,21 +17,6 @@ def __init__(self):
# [-] switch to limit resolution to the vertical direction
self.swresovert = False
- mvp_stackfuns = {
- "RMETHH": [
- "RMETHH [method]",
- "[txt]",
- self.setresometh,
- "Set resolution method to be used horizontally",
- ],
- "RMETHV": [
- "RMETHV [method]",
- "[txt]",
- self.setresometv,
- "Set resolution method to be used vertically",
- ]}
- stack.append_commands(mvp_stackfuns)
-
def setprio(self, flag=None, priocode=''):
'''Set the prio switch and the type of prio '''
if flag is None:
@@ -49,6 +35,7 @@ def setprio(self, flag=None, priocode=''):
return False, "Priority code Not Understood. Available Options: " + str(options)
return super().setprio(flag, priocode)
+ @stack.command(name="RMETHH")
def setresometh(self, value=''):
""" Processes the RMETHH command. Sets swresovert = False"""
# Acceptable arguments for this command
@@ -83,7 +70,7 @@ def setresometh(self, value=''):
self.swresohdg = True
self.swresovert = False
-
+ @stack.command(name='RMETHV')
def setresometv(self, value=''):
""" Processes the RMETHV command. Sets swresohoriz = False."""
# Acceptable arguments for this command
@@ -94,15 +81,15 @@ def setresometv(self, value=''):
("ON" if self.swresovert else "OFF")
if value not in options:
return False, "RMETV Not Understood" + "\nRMETHV [ON / V/S / OFF / NONE]"
- else:
- if value == "ON" or value == "V/S":
- self.swresovert = True
- self.swresohoriz = False
- self.swresospd = False
- self.swresohdg = False
- elif value == "OFF" or value == "OF" or value == "NONE":
- # Do NOT swtich off self.swresohoriz if value == OFF
- self.swresovert = False
+
+ if value == "ON" or value == "V/S":
+ self.swresovert = True
+ self.swresohoriz = False
+ self.swresospd = False
+ self.swresohdg = False
+ elif value == "OFF" or value == "OF" or value == "NONE":
+ # Do NOT swtich off self.swresohoriz if value == OFF
+ self.swresovert = False
def applyprio(self, dv_mvp, dv1, dv2, vs1, vs2):
''' Apply the desired priority setting to the resolution '''
@@ -280,29 +267,32 @@ def resolve(self, conf, ownship, intruder):
def MVP(self, ownship, intruder, conf, qdr, dist, tcpa, tLOS, idx1, idx2):
"""Modified Voltage Potential (MVP) resolution method"""
# Preliminary calculations-------------------------------------------------
-
+ # Determine largest RPZ and HPZ of the conflict pair, use lookahead of ownship
+ rpz_m = np.max(conf.rpz[[idx1, idx2]] * self.resofach)
+ hpz_m = np.max(conf.hpz[[idx1, idx2]] * self.resofacv)
+ dtlook = conf.dtlookahead[idx1]
# Convert qdr from degrees to radians
qdr = np.radians(qdr)
# Relative position vector between id1 and id2
- drel = np.array([np.sin(qdr)*dist, \
- np.cos(qdr)*dist, \
- intruder.alt[idx2]-ownship.alt[idx1]])
+ drel = np.array([np.sin(qdr) * dist, \
+ np.cos(qdr) * dist, \
+ intruder.alt[idx2] - ownship.alt[idx1]])
# Write velocities as vectors and find relative velocity vector
v1 = np.array([ownship.gseast[idx1], ownship.gsnorth[idx1], ownship.vs[idx1]])
v2 = np.array([intruder.gseast[idx2], intruder.gsnorth[idx2], intruder.vs[idx2]])
- vrel = np.array(v2-v1)
+ vrel = v2 - v1
# Horizontal resolution----------------------------------------------------
# Find horizontal distance at the tcpa (min horizontal distance)
dcpa = drel + vrel*tcpa
- dabsH = np.sqrt(dcpa[0]*dcpa[0]+dcpa[1]*dcpa[1])
+ dabsH = np.sqrt(dcpa[0] * dcpa[0] + dcpa[1] * dcpa[1])
# Compute horizontal intrusion
- iH = (conf.rpz * self.resofach) - dabsH
+ iH = rpz_m - dabsH
# Exception handlers for head-on conflicts
# This is done to prevent division by zero in the next step
@@ -313,12 +303,12 @@ def MVP(self, ownship, intruder, conf, qdr, dist, tcpa, tLOS, idx1, idx2):
# If intruder is outside the ownship PZ, then apply extra factor
# to make sure that resolution does not graze IPZ
- if (conf.rpz * self.resofach) < dist and dabsH < dist:
+ if rpz_m < dist and dabsH < dist:
# Compute the resolution velocity vector in horizontal direction.
# abs(tcpa) because it bcomes negative during intrusion.
- erratum=np.cos(np.arcsin((conf.rpz * self.resofach)/dist)-np.arcsin(dabsH/dist))
- dv1 = (((conf.rpz * self.resofach)/erratum - dabsH)*dcpa[0])/(abs(tcpa)*dabsH)
- dv2 = (((conf.rpz * self.resofach)/erratum - dabsH)*dcpa[1])/(abs(tcpa)*dabsH)
+ erratum = np.cos(np.arcsin(rpz_m / dist)-np.arcsin(dabsH / dist))
+ dv1 = ((rpz_m / erratum - dabsH) * dcpa[0]) / (abs(tcpa) * dabsH)
+ dv2 = ((rpz_m / erratum - dabsH) * dcpa[1]) / (abs(tcpa) * dabsH)
else:
dv1 = (iH * dcpa[0]) / (abs(tcpa) * dabsH)
dv2 = (iH * dcpa[1]) / (abs(tcpa) * dabsH)
@@ -327,35 +317,35 @@ def MVP(self, ownship, intruder, conf, qdr, dist, tcpa, tLOS, idx1, idx2):
# Compute the vertical intrusion
# Amount of vertical intrusion dependent on vertical relative velocity
- iV = (conf.hpz * self.resofacv) if abs(vrel[2])>0.0 else (conf.hpz * self.resofacv)-abs(drel[2])
+ iV = hpz_m if abs(vrel[2]) > 0.0 else hpz_m - abs(drel[2])
# Get the time to solve the conflict vertically - tsolveV
- tsolV = abs(drel[2]/vrel[2]) if abs(vrel[2])>0.0 else tLOS
+ tsolV = abs(drel[2] / vrel[2]) if abs(vrel[2]) > 0.0 else tLOS
# If the time to solve the conflict vertically is longer than the look-ahead time,
# because the the relative vertical speed is very small, then solve the intrusion
# within tinconf
- if tsolV>conf.dtlookahead:
+ if tsolV > dtlook:
tsolV = tLOS
- iV = (conf.hpz * self.resofacv)
+ iV = hpz_m
# Compute the resolution velocity vector in the vertical direction
# The direction of the vertical resolution is such that the aircraft with
# higher climb/decent rate reduces their climb/decent rate
- dv3 = np.where(abs(vrel[2])>0.0, (iV/tsolV)*(-vrel[2]/abs(vrel[2])), (iV/tsolV))
+ dv3 = np.where(abs(vrel[2]) > 0.0, (iV / tsolV) * (-vrel[2] / abs(vrel[2])), (iV / tsolV))
# It is necessary to cap dv3 to prevent that a vertical conflict
# is solved in 1 timestep, leading to a vertical separation that is too
# high (high vs assumed in traf). If vertical dynamics are included to
# aircraft model in traffic.py, the below three lines should be deleted.
- # mindv3 = -400*fpm# ~ 2.016 [m/s]
- # maxdv3 = 400*fpm
- # dv3 = np.maximum(mindv3,np.minimum(maxdv3,dv3))
+ # mindv3 = -400*fpm# ~ 2.016 [m/s]
+ # maxdv3 = 400*fpm
+ # dv3 = np.maximum(mindv3,np.minimum(maxdv3,dv3))
# Combine resolutions------------------------------------------------------
# combine the dv components
- dv = np.array([dv1,dv2,dv3])
+ dv = np.array([dv1, dv2, dv3])
return dv, tsolV
diff --git a/bluesky/traffic/asas/resolution.py b/bluesky/traffic/asas/resolution.py
index 6763128265..104ab17eec 100644
--- a/bluesky/traffic/asas/resolution.py
+++ b/bluesky/traffic/asas/resolution.py
@@ -12,10 +12,6 @@
class ConflictResolution(Entity, replaceable=True):
''' Base class for Conflict Resolution implementations. '''
- # ConflictResolution on/off switch. Set to True whenever another
- # implementation than the base implementation (ConflictResolution) is selected.
- do_cr = False
-
def __init__(self):
super().__init__()
# [-] switch to activate priority rules for conflict resolution
@@ -43,6 +39,16 @@ def __init__(self):
self.alt = np.array([]) # alt provided by the ASAS [m]
self.vs = np.array([]) # vspeed provided by the ASAS [m/s]
+ def reset(self):
+ super().reset()
+ self.swprio = False
+ self.priocode = ''
+ self.resopairs.clear()
+ self.resofach = bs.settings.asas_marh
+ self.resofacv = bs.settings.asas_marv
+ self.resodhrelative = True
+ self.resorrelative = True
+
# By default all channels are controlled by self.active,
# but they can be overloaded with separate variables or functions in a
# derived ASAS Conflict Resolution class (@property decorator takes away
@@ -93,7 +99,8 @@ def resolve(self, conf, ownship, intruder):
def update(self, conf, ownship, intruder):
''' Perform an update step of the Conflict Resolution implementation. '''
- if ConflictResolution.do_cr:
+ if ConflictResolution.selected() is not ConflictResolution:
+ # Only perform CR when an actual method is selected
if conf.confpairs:
self.trk, self.tas, self.vs, self.alt = self.resolve(conf, ownship, intruder)
self.resumenav(conf, ownship, intruder)
@@ -111,6 +118,17 @@ def resumenav(self, conf, ownship, intruder):
delpairs = set()
changeactive = dict()
+ # smallest relative angle between vectors of heading a and b
+ def anglediff(a, b):
+ d = a - b
+ if d > 180:
+ return anglediff(a, b + 360)
+ elif d < -180:
+ return anglediff(a + 360, b)
+ else:
+ return d
+
+
# Look at all conflicts, also the ones that are solved but CPA is yet to come
for conflict in self.resopairs:
idx1, idx2 = bs.traf.id2idx(conflict)
@@ -134,20 +152,21 @@ def resumenav(self, conf, ownship, intruder):
# Check if conflict is past CPA
past_cpa = np.dot(dist, vrel) > 0.0
+ rpz = np.max(conf.rpz[[idx1, idx2]])
# hor_los:
# Aircraft should continue to resolve until there is no horizontal
# LOS. This is particularly relevant when vertical resolutions
# are used.
hdist = np.linalg.norm(dist)
- hor_los = hdist < conf.rpz
+ hor_los = hdist < rpz
# Bouncing conflicts:
# If two aircraft are getting in and out of conflict continously,
# then they it is a bouncing conflict. ASAS should stay active until
# the bouncing stops.
is_bouncing = \
- abs(ownship.trk[idx1] - intruder.trk[idx2]) < 30.0 and \
- hdist < conf.rpz * self.resofach
+ abs(anglediff(ownship.trk[idx1], intruder.trk[idx2])) < 30.0 and \
+ hdist < rpz * self.resofach
# Start recovery for ownship if intruder is deleted, or if past CPA
# and not in horizontal LOS or a bouncing conflict
@@ -232,10 +251,10 @@ def setresofacv(self, factor: float = None):
''' Set resolution factor vertical (to maneuver only a fraction of a resolution vector). '''
if factor is None:
return True, f'RFACV [FACTOR]\nCurrent vertical resolution factor is: {self.resofacv}'
- else:
- self.resofacv = factor
- self.resodhrelative = True # Size of resolution zone dh, vertically, set relative to CD zone
- return True, f'Vertical resolution factor set to {self.resofacv}'
+ self.resofacv = factor
+ # Size of resolution zone dh, vertically, set relative to CD zone
+ self.resodhrelative = True
+ return True, f'Vertical resolution factor set to {self.resofacv}'
@command(name='RSZONER', aliases=('RESOZONER',))
def setresozoner(self, zoner : float = None):
@@ -244,13 +263,14 @@ def setresozoner(self, zoner : float = None):
'''
if not bs.traf.cd.global_rpz:
self.resorrelative = True
- return False, f'RSZONER [radiusnm]\nCan only set resolution factor when simulation contains aircraft with different RPZ,\nUse RFACH instead.'
+ return False, 'RSZONER [radiusnm]\nCan only set resolution factor when simulation contains aircraft with different RPZ,\nUse RFACH instead.'
if zoner is None:
return True, f'RSZONER [radiusnm]\nCurrent horizontal resolution factor is: {self.resofach}, resulting in radius: {self.resofach*bs.traf.cd.rpz_def/nm} nm'
- else:
- self.resofach = zoner / bs.traf.cd.rpz_def * nm
- self.resorrelative = False # Size of resolution zone r, vertically, no longer relative to CD zone
- return True, f'Horizontal resolution factor updated to {self.resofach}, resulting in radius: {zoner} nm'
+
+ self.resofach = zoner / bs.traf.cd.rpz_def * nm
+ # Size of resolution zone r, vertically, no longer relative to CD zone
+ self.resorrelative = False
+ return True, f'Horizontal resolution factor updated to {self.resofach}, resulting in radius: {zoner} nm'
@command(name='RSZONEDH', aliases=('RESOZONEDH',))
def setresozonedh(self, zonedh : float = None):
@@ -260,13 +280,14 @@ def setresozonedh(self, zonedh : float = None):
'''
if not bs.traf.cd.global_hpz:
self.resodhrelative = True
- return False, f'RSZONEH [zonedhft]\nCan only set resolution factor when simulation contains aircraft with different HPZ,\nUse RFACV instead.'
+ return False, 'RSZONEH [zonedhft]\nCan only set resolution factor when simulation contains aircraft with different HPZ,\nUse RFACV instead.'
if zonedh is None:
return True, f'RSZONEDH [zonedhft]\nCurrent vertical resolution factor is: {self.resofacv}, resulting in height: {self.resofacv*bs.traf.cd.hpz_def/ft} ft'
- else:
- self.resofacv = zonedh / bs.traf.cd.hpz_def * ft
- self.resodhrelative = False # Size of resolution zone dh, vertically, no longer relative to CD zone
- return True, f'Vertical resolution factor updated to {self.resofacv}, resulting in height: {zonedh} ft'
+
+ self.resofacv = zonedh / bs.traf.cd.hpz_def * ft
+ # Size of resolution zone dh, vertically, no longer relative to CD zone
+ self.resodhrelative = False
+ return True, f'Vertical resolution factor updated to {self.resofacv}, resulting in height: {zonedh} ft'
@staticmethod
@command(name='RESO')
@@ -283,7 +304,6 @@ def setmethod(name : 'txt' = ''):
f'\nAvailable CR methods: {", ".join(names)}'
# Check if the requested method exists
if name == 'OFF':
- ConflictResolution.do_cr = False
ConflictResolution.select()
return True, 'Conflict Resolution turned off.'
method = methods.get(name, None)
@@ -293,5 +313,4 @@ def setmethod(name : 'txt' = ''):
# Select the requested method
method.select()
- ConflictResolution.do_cr = True
return True, f'Selected {method.__name__} as CR method.'
diff --git a/bluesky/traffic/autopilot.py b/bluesky/traffic/autopilot.py
index 8e8ecdc184..109a270614 100644
--- a/bluesky/traffic/autopilot.py
+++ b/bluesky/traffic/autopilot.py
@@ -18,11 +18,24 @@
# In python <3.3 collections.abc doesn't exist
from collections import Collection
-bs.settings.set_variable_defaults( fms_dt=10.5 )
+import bluesky as bs
+from bluesky import stack
+from bluesky.tools import geo
+from bluesky.tools.misc import degto180
+from bluesky.tools.position import txt2pos
+from bluesky.tools.aero import ft, nm, fpm, vcasormach2tas, vcas2tas, tas2cas, cas2tas, g0
+from bluesky.core import Entity, timed_function
+from .route import Route
+#debug
+from inspect import stack as callstack
-class Autopilot( Entity, replaceable=True ):
- def __init__( self ):
+bs.settings.set_variable_defaults(fms_dt=10.5)
+
+
+class Autopilot(Entity, replaceable=True):
+ ''' BlueSky Autopilot implementation. '''
+ def __init__(self):
super().__init__()
# Standard self.steepness for descent
@@ -39,18 +52,35 @@ def __init__( self ):
self.vs = np.array( [] )
# VNAV variables
- self.dist2vs = np.array( [] ) # distance from coming waypoint to TOD
- self.swvnavvs = np.array( [] ) # whether to use given VS or not
- self.vnavvs = np.array( [] ) # vertical speed in VNAV
+ self.swtoc = np.array([]) # ToC switch to switch on VNAV Top of Climb logic (default value True)
+ self.swtod = np.array([]) # ToD switch to switch on VNAV Top of Descent logic (default value True)
+
+ self.dist2vs = np.array([]) # distance from coming waypoint to TOD
+ self.dist2accel = np.array([]) # Distance to go to acceleration(decelaration) for turn next waypoint [nm]
+
+ self.swvnavvs = np.array([]) # whether to use given VS or not
+ self.vnavvs = np.array([]) # vertical speed in VNAV
+
# LNAV variables
- self.qdr2wp = np.array( [] ) # Direction to waypoint from the last time passing was checked
- # to avoid 180 turns due to updated qdr shortly before passing wp
+ self.qdr2wp = np.array([]) # Direction to waypoint from the last time passing was checked
+ # to avoid 180 turns due to updated qdr shortly before passing wp
+ self.dist2wp = np.array([]) # [m] Distance to active waypoint
+ self.qdrturn = np.array([]) # qdr to next turn]
+ self.dist2turn = np.array([]) # Distance to next turn [m]
+ self.inturn = np.array([]) # If we're in a turn maneuver or not
# Traffic navigation information
self.orig = [] # Four letter code of origin airport
self.dest = [] # Four letter code of destination airport
+ # Default values
+ self.bankdef = np.array([]) # nominal bank angle, [radians]
+ self.vsdef = np.array([]) # [m/s]default vertical speed of autopilot
+
+ # Currently used roll/bank angle [rad]
+ self.turnphi = np.array([]) # [rad] bank angle setting of autopilot
+
# Route objects
self.route = []
@@ -63,23 +93,39 @@ def create( self, n=1 ):
self.alt[-n:] = bs.traf.alt[-n:]
# LNAV variables
- self.qdr2wp[-n:] = -999 # Direction to waypoint from the last time passing was checked
+ self.qdr2wp[-n:] = -999. # Direction to waypoint from the last time passing was checked
+ self.dist2wp[-n:] = -999. # Distance to go to next waypoint [nm]
+
# to avoid 180 turns due to updated qdr shortly before passing wp
# VNAV Variables
self.dist2vs[-n:] = -999.
+ self.dist2accel[-n:] = -999. # Distance to go to acceleration(decelaration) for turn next waypoint [nm]
+
+ # Traffic performance data
+ #(temporarily default values)
+ self.vsdef[-n:] = 1500. * fpm # default vertical speed of autopilot
+ self.bankdef[-n:] = np.radians(25.)
# Route objects
for ridx, acid in enumerate( bs.traf.id[-n:] ):
self.route[ridx - n] = Route( acid )
- # no longer timed @timed_function(name='fms', dt=bs.settings.fms_dt, manual=True)
- def update_fms( self, qdr, dist ):
- # Check which aircraft i have reached their active waypoint
- # Shift waypoints for aircraft i where necessary
- # Reached function return list of indices where reached logic is True
- for i in bs.traf.actwp.Reached( qdr, dist, bs.traf.actwp.flyby,
- bs.traf.actwp.flyturn, bs.traf.actwp.turnrad, bs.traf.actwp.swlastwp ):
+ # Default ToC/ToD logic on
+ self.swtoc[-n:] = True
+ self.swtod[-n:] = True
+
+ #no longer timed @timed_function(name='fms', dt=bs.settings.fms_dt, manual=True)
+ def update_fms(self, qdr, dist):
+ """
+ Waypoint switching function:
+ - Check which aircraft i have reached their active waypoint
+ - Reached function return list of indices where reached logic is True
+ - Shift waypoint (last,next etc.) data for aircraft i where necessary
+ - Compute VNAV profile for this new leg
+ """
+ for i in bs.traf.actwp.Reached(qdr, dist, bs.traf.actwp.flyby,
+ bs.traf.actwp.flyturn,bs.traf.actwp.turnrad,bs.traf.actwp.swlastwp):
# Save current wp speed for use on next leg when we pass this waypoint
# VNAV speeds are always FROM-speeds, so we accelerate/decellerate at the waypoint
@@ -90,8 +136,6 @@ def update_fms( self, qdr, dist ):
bs.traf.actwp.spd[i] = bs.traf.actwp.nextspd[i]
bs.traf.actwp.spdcon[i] = bs.traf.actwp.nextspd[i]
-
-
# Execute stack commands for the still active waypoint, which we pass
self.route[i].runactwpstack()
@@ -103,20 +147,25 @@ def update_fms( self, qdr, dist ):
turnspd = bs.traf.tas[i]
if bs.traf.actwp.turnrad[i] > 0.:
- bs.traf.aphi[i] = atan( turnspd * turnspd / ( bs.traf.actwp.turnrad[i] * nm * g0 ) ) # [rad]
+ self.turnphi[i] = atan(turnspd*turnspd/(bs.traf.actwp.turnrad[i]*nm*g0)) # [rad]
else:
- bs.traf.aphi[i] = 0.0 # [rad] or leave untouched???
+ self.turnphi[i] = 0.0 # [rad] or leave untouched???
else:
- bs.traf.aphi[i] = 0.0 # [rad] or leave untouched???
+ self.turnphi[i] = 0.0 #[rad] or leave untouched???
# Get next wp, if there still is one
if not bs.traf.actwp.swlastwp[i]:
- lat, lon, alt, bs.traf.actwp.nextspd[i], bs.traf.actwp.xtoalt[i], toalt, \
+ lat, lon, alt, bs.traf.actwp.nextspd[i], \
+ bs.traf.actwp.xtoalt[i], toalt, \
bs.traf.actwp.xtorta[i], bs.traf.actwp.torta[i], \
- lnavon, flyby, flyturn, turnrad, turnspd, \
- bs.traf.actwp.next_qdr[i], bs.traf.actwp.swlastwp[i] = \
- self.route[i].getnextwp() # note: xtoalt,toalt in [m]
+ lnavon, flyby, flyturn, turnrad, turnspd,\
+ bs.traf.actwp.next_qdr[i], bs.traf.actwp.swlastwp[i] = \
+ self.route[i].getnextwp() # [m] note: xtoalt,nextaltco are in meters
+
+ bs.traf.actwp.nextturnlat[i], bs.traf.actwp.nextturnlon[i], \
+ bs.traf.actwp.nextturnspd[i], bs.traf.actwp.nextturnrad[i], \
+ bs.traf.actwp.nextturnidx[i] = self.route[i].getnextturnwp()
# Prevent trying to activate the next waypoint when it was already the last waypoint
else:
@@ -141,12 +190,25 @@ def update_fms( self, qdr, dist ):
# 1.0 in case of fly by, else fly over
bs.traf.actwp.flyby[i] = int( flyby )
+ # Update qdr and turndist for this new waypoint for ComputeVNAV
+ qdr[i], distnmi = geo.qdrdist(bs.traf.lat[i], bs.traf.lon[i],
+ bs.traf.actwp.lat[i], bs.traf.actwp.lon[i])
+
+ #dist[i] = distnmi * nm
+ self.dist2wp[i] = distnmi*nm
+
+ bs.traf.actwp.curlegdir[i] = qdr[i]
+ bs.traf.actwp.curleglen[i] = self.dist2wp[i]
+
# User has entered an altitude for this waypoint
- if alt >= -0.01:
+ if alt >= -0.01: # positive alt on this waypoint means altitude constraint
bs.traf.actwp.nextaltco[i] = alt # [m]
+ bs.traf.actwp.xtoalt[i] = 0.0
+ else:
+ bs.traf.actwp.nextaltco[i] = toalt # [m]
- if not bs.traf.swlnav[i]:
- bs.traf.actwp.spd[i] = -999.
+ #if not bs.traf.swlnav[i]:
+ # bs.traf.actwp.spd[i] = -997.
# VNAV spd mode: use speed of this waypoint as commanded speed
# while passing waypoint and save next speed for passing next wp
@@ -154,12 +216,6 @@ def update_fms( self, qdr, dist ):
if bs.traf.swvnavspd[i] and bs.traf.actwp.spd[i] >= 0.0:
bs.traf.selspd[i] = bs.traf.actwp.spd[i]
- # Update qdr and turndist for this new waypoint for ComputeVNAV
- qdr[i], distnmi = geo.qdrdist( bs.traf.lat[i], bs.traf.lon[i],
- bs.traf.actwp.lat[i], bs.traf.actwp.lon[i] )
-
- dist[i] = distnmi * nm
-
# Update turndist so ComputeVNAV works, is there a next leg direction or not?
if bs.traf.actwp.next_qdr[i] < -900.:
local_next_qdr = qdr[i]
@@ -185,8 +241,8 @@ def update_fms( self, qdr, dist ):
# Calculate turn dist (and radius which we do not use) now for scalar variable [i]
bs.traf.actwp.turndist[i], dummy = \
- bs.traf.actwp.calcturn( bs.traf.tas[i], bs.traf.bank[i],
- qdr[i], local_next_qdr, turnrad ) # update turn distance for VNAV
+ bs.traf.actwp.calcturn(bs.traf.tas[i], self.bankdef[i],
+ qdr[i], local_next_qdr,turnrad) # update turn distance for VNAV
# Reduce turn dist for reduced turnspd
if bs.traf.actwp.flyturn[i] and bs.traf.actwp.turnrad[i] < 0.0 and bs.traf.actwp.turnspd[i] >= 0.:
@@ -197,6 +253,8 @@ def update_fms( self, qdr, dist ):
self.ComputeVNAV( i, toalt, bs.traf.actwp.xtoalt[i], bs.traf.actwp.torta[i],
bs.traf.actwp.xtorta[i] )
+
+
# End of per waypoint i switching loop
# Update qdr2wp with up-to-date qdr, now that we have checked passing wp
self.qdr2wp = qdr % 360.
@@ -209,9 +267,9 @@ def update_fms( self, qdr, dist ):
if bs.traf.ap.route[iac].wprta[iwp] > -99.:
# For all a/c flying to an RTA waypoint, recalculate speed more often
- dist2go4rta = geo.kwikdist( bs.traf.lat[iac], bs.traf.lon[iac], \
- bs.traf.actwp.lat[iac], bs.traf.actwp.lon[iac] ) * nm \
- +bs.traf.ap.route[iac].wpxtorta[iwp] # last term zero for active wp rta
+ dist2go4rta = geo.kwikdist(bs.traf.lat[iac],bs.traf.lon[iac],
+ bs.traf.actwp.lat[iac],bs.traf.actwp.lon[iac])*nm \
+ + bs.traf.ap.route[iac].wpxtorta[iwp] # last term zero for active wp rta
# Set bs.traf.actwp.spd to rta speed, if necessary
self.setspeedforRTA( iac, bs.traf.actwp.torta[iac], dist2go4rta )
@@ -223,53 +281,57 @@ def update_fms( self, qdr, dist ):
def update( self ):
# FMS LNAV mode:
# qdr[deg],distinnm[nm]
- qdr, distinnm = geo.qdrdist( bs.traf.lat, bs.traf.lon,
- bs.traf.actwp.lat, bs.traf.actwp.lon ) # [deg][nm])
- self.qdr2wp = qdr
- dist2wp = distinnm * nm # Conversion to meters
+ qdr, distinnm = geo.qdrdist(bs.traf.lat, bs.traf.lon,
+ bs.traf.actwp.lat, bs.traf.actwp.lon) # [deg][nm])
+
+ self.qdr2wp = qdr
+ self.dist2wp = distinnm*nm # Conversion to meters
# FMS route update and possibly waypoint shift. Note: qdr, dist2wp will be updated accordingly in case of wp switch
- self.update_fms( qdr, dist2wp ) # Updates self.qdr2wp when necessary
+ self.update_fms(qdr, self.dist2wp) # Updates self.qdr2wp when necessary
#================= Continuous FMS guidance ========================
- # Waypoint switching in the loop above was scalar (per a/c with index i)
- # Code below is vectorized, with arrays for all aircraft
+ # Note that the code below is vectorized, with traffic arrays, so for all aircraft
+ # ComputeVNAV and inside waypoint loop of update_fms, it was scalar (per a/c with index i)
- # Do VNAV start of descent check
- # dy = (bs.traf.actwp.lat - bs.traf.lat) #[deg lat = 60 nm]
- # dx = (bs.traf.actwp.lon - bs.traf.lon) * bs.traf.coslat #[corrected deg lon = 60 nm]
- # dist2wp = 60. * nm * np.sqrt(dx * dx + dy * dy) # [m]
+ # VNAV guidance logic (using the variables prepared by ComputeVNAV when activating waypoint)
+ #
+ # Central question is:
+ # - Can we please we start to descend or to climb?
+ #
+ # Well, when Top of Descent (ToD) switch is on, descend as late as possible,
+ # But when Top of Climb switch is on or off, climb as soon as possible, only difference is steepness used in ComputeVNAV
+ # to calculate bs.traf.actwp.vs
+ # Only use this logic if there is a valid next altitude constraint (nextaltco)
- # VNAV logic: descend as late as possible, climb as soon as possible
- startdescent = ( dist2wp < self.dist2vs ) + ( bs.traf.actwp.nextaltco > bs.traf.alt )
+ startdescorclimb = (bs.traf.actwp.nextaltco>=-0.1) * \
+ np.logical_or(self.swtod * (bs.traf.alt>bs.traf.actwp.nextaltco) * (self.dist2wp < self.dist2vs),
+ bs.traf.alt 0.1, bs.traf.selvs, bs.traf.apvsdef ) # m/s
- self.vs = np.where( self.swvnavvs, self.vnavvs, selvs )
- self.alt = np.where( self.swvnavvs, bs.traf.actwp.nextaltco, bs.traf.selalt )
+ # self.vs = np.where(self.swvnavvs, self.vnavvs, self.vsdef * bs.traf.limvs_flag)
+ # for VNAV use fixed V/S and change start of descent
+ selvs = np.where(abs(bs.traf.selvs) > 0.1, bs.traf.selvs, self.vsdef) # m/s
+ self.vs = np.where(self.swvnavvs, self.vnavvs, selvs)
+ self.alt = np.where(self.swvnavvs, bs.traf.actwp.nextaltco, bs.traf.selalt)
# When descending or climbing in VNAV also update altitude command of select/hold mode
bs.traf.selalt = np.where( self.swvnavvs, bs.traf.actwp.nextaltco, bs.traf.selalt )
@@ -285,33 +347,48 @@ def update( self ):
# use the turn speed
# Is turn speed specified and are we not already slow enough? We only decelerate for turns, not accel.
- turntas = np.where( bs.traf.actwp.turnspd > 0.0, vcas2tas( bs.traf.actwp.turnspd, bs.traf.alt ),
- -1.0 + 0.*bs.traf.tas )
- swturnspd = bs.traf.actwp.flyturn * ( turntas > 0.0 ) * ( bs.traf.actwp.turnspd > 0.0 )
- turntasdiff = np.maximum( 0., ( bs.traf.tas - turntas ) * ( turntas > 0.0 ) )
+ turntas = np.where(bs.traf.actwp.nextturnspd>0.0, vcas2tas(bs.traf.actwp.nextturnspd, bs.traf.alt),
+ -1.0+0.*bs.traf.tas)
+ # Switch is now whether the aircraft has any turn waypoints
+ swturnspd = bs.traf.actwp.nextturnidx > 0
+ turntasdiff = np.maximum(0.,(bs.traf.tas - turntas)*(turntas>0.0))
# t = (v1-v0)/a ; x = v0*t+1/2*a*t*t => dx = (v1*v1-v0*v0)/ (2a)
- ax = bs.traf.perf.acceleration()
- dxturnspdchg = distaccel( turntas, bs.traf.tas, ax )
+ dxturnspdchg = distaccel(turntas,bs.traf.perf.vmax, bs.traf.perf.axmax)
# dxturnspdchg = 0.5*np.abs(turntas*turntas-bs.traf.tas*bs.traf.tas)/(np.sign(turntas-bs.traf.tas)*np.maximum(0.01,np.abs(ax)))
# dxturnspdchg = np.where(swturnspd, np.abs(turntasdiff)/np.maximum(0.01,ax)*(bs.traf.tas+0.5*np.abs(turntasdiff)),
# 0.0*bs.traf.tas)
# Decelerate or accelerate for next required speed because of speed constraint or RTA speed
- nexttas = vcasormach2tas( bs.traf.actwp.nextspd, bs.traf.alt )
+ # Note that because nextspd comes from the stack, and can be either a mach number or
+ # a calibrated airspeed, it can only be converted from Mach / CAS [kts] to TAS [m/s]
+ # once the altitude is known.
+ nexttas = vcasormach2tas(bs.traf.actwp.nextspd, bs.traf.alt)
+
# tasdiff = (nexttas - bs.traf.tas)*(bs.traf.actwp.spd>=0.) # [m/s]
# t = (v1-v0)/a ; x = v0*t+1/2*a*t*t => dx = (v1*v1-v0*v0)/ (2a)
- dxspdconchg = distaccel( bs.traf.tas, nexttas, ax )
+ dxspdconchg = distaccel(bs.traf.tas, nexttas, bs.traf.perf.axmax)
+
+ qdrturn, dist2turn = geo.qdrdist(bs.traf.lat, bs.traf.lon,
+ bs.traf.actwp.nextturnlat, bs.traf.actwp.nextturnlon)
+
+ self.qdrturn = qdrturn
+ dist2turn = dist2turn * nm
+
+ # Where we don't have a turn waypoint, as in turn idx is negative, then put distance
+ # as Earth circumference.
+ self.dist2turn = np.where(bs.traf.actwp.nextturnidx > 0, dist2turn, 40075000)
# Check also whether VNAVSPD is on, if not, SPD SEL has override for next leg
# and same for turn logic
- usenextspdcon = ( dist2wp < dxspdconchg ) * ( bs.traf.actwp.nextspd > -990. ) * \
- bs.traf.swvnavspd * bs.traf.swvnav * bs.traf.swlnav
- useturnspd = np.logical_or( bs.traf.actwp.turntonextwp, ( dist2wp < dxturnspdchg + bs.traf.actwp.turndist ) * \
- swturnspd * bs.traf.swvnavspd * bs.traf.swvnav * bs.traf.swlnav )
+ usenextspdcon = (self.dist2wp < dxspdconchg)*(bs.traf.actwp.nextspd>-990.) * \
+ bs.traf.swvnavspd*bs.traf.swvnav*bs.traf.swlnav
+ useturnspd = np.logical_or(bs.traf.actwp.turntonextwp,
+ (self.dist2turn < (dxturnspdchg+bs.traf.actwp.turndist)) * \
+ swturnspd*bs.traf.swvnavspd*bs.traf.swvnav*bs.traf.swlnav)
# Hold turn mode can only be switched on here, cannot be switched off here (happeps upon passing wp)
bs.traf.actwp.turntonextwp = np.logical_or( bs.traf.actwp.turntonextwp, useturnspd )
@@ -322,63 +399,86 @@ def update( self ):
# Avoid using old turning speeds when turning of this leg to the next leg
# by disabling (old) turningspd when on leg
- bs.traf.actwp.oldturnspd = np.where( oncurrentleg * ( bs.traf.actwp.oldturnspd > 0. ), -999.,
- bs.traf.actwp.oldturnspd )
+ bs.traf.actwp.oldturnspd = np.where(oncurrentleg*(bs.traf.actwp.oldturnspd>0.), -998.,
+ bs.traf.actwp.oldturnspd)
# turnfromlastwp can only be switched off here, not on (latter happens upon passing wp)
bs.traf.actwp.turnfromlastwp = np.logical_and( bs.traf.actwp.turnfromlastwp, inoldturn )
# Select speed: turn sped, next speed constraint, or current speed constraint
- bs.traf.selspd = np.where( useturnspd, bs.traf.actwp.turnspd,
- np.where( usenextspdcon, bs.traf.actwp.nextspd,
- np.where( ( bs.traf.actwp.spdcon >= 0 ) * bs.traf.swvnavspd, bs.traf.actwp.spd,
- bs.traf.selspd ) ) )
-
+ bs.traf.selspd = np.where(useturnspd,bs.traf.actwp.nextturnspd,
+ np.where(usenextspdcon, bs.traf.actwp.nextspd,
+ np.where((bs.traf.actwp.spdcon>=0)*bs.traf.swvnavspd,bs.traf.actwp.spd,
+ bs.traf.selspd)))
# Temporary override when still in old turn
- bs.traf.selspd = np.where( inoldturn * ( bs.traf.actwp.oldturnspd > 0. ) * bs.traf.swvnavspd * bs.traf.swvnav * bs.traf.swlnav,
- bs.traf.actwp.oldturnspd, bs.traf.selspd )
-
- # debug if inoldturn[0]:
- # debug print("inoldturn bs.traf.trk =",bs.traf.trk[0],"qdr =",qdr)
- # debug elif usenextspdcon[0]:
- # debug print("usenextspdcon")
- # debug elif useturnspd[0]:
- # debug print("useturnspd")
- # debug elif bs.traf.actwp.spdcon>0:
- # debug print("using current speed constraint")
- # debug else:
- # debug print("no speed given")
+ bs.traf.selspd = np.where(inoldturn*(bs.traf.actwp.oldturnspd>0.)*bs.traf.swvnavspd*bs.traf.swvnav*bs.traf.swlnav,
+ bs.traf.actwp.oldturnspd,bs.traf.selspd)
+
+ self.inturn = np.logical_or(useturnspd,inoldturn)
+
+ #debug if inoldturn[0]:
+ #debug print("inoldturn bs.traf.trk =",bs.traf.trk[0],"qdr =",qdr)
+ #debug elif usenextspdcon[0]:
+ #debug print("usenextspdcon")
+ #debug elif useturnspd[0]:
+ #debug print("useturnspd")
+ #debug elif bs.traf.actwp.spdcon>0:
+ #debug print("using current speed constraint")
+ #debug else:
+ #debug print("no speed given")
# Below crossover altitude: CAS=const, above crossover altitude: Mach = const
self.tas = vcasormach2tas( bs.traf.selspd, bs.traf.alt )
- return
+ def ComputeVNAV(self, idx, toalt, xtoalt, torta, xtorta):
+ """
+ This function to do VNAV (and RTA) calculations is only called only once per leg.
+ If:
+ - switching to next waypoint
+ - when VNAV is activated
+ - when a DIRECT is given
+
+ It prepares the profile of this leg using the the current altitude and the next altitude constraint (nextaltco).
+ The distance to the next altitude constraint is given by xtoalt [m] after active waypoint.
+
+ Options are (classic VNAV logic, swtoc and swtod True):
+ - no altitude constraint in the future, do nothing
+ - Top of CLimb logic (swtoc=True): if next altitude constrain is baove us, climb as soon as possible with default steepness
+ - Top of Descent Logic (swtod =True) Use ToD logic: descend as late aspossible, based on
+ steepness. Prepare a ToD somewhere on the leg if necessary based on distance to next altitude constraint.
+ This is done by calculating distance to next waypoint where descent should start
+
+ Alternative logic (e.g. for UAVs or GA):
+ - swtoc=False and next alt co is above us, climb with the angle/steepness needed to arrive at the altitude at
+ the waypoint with the altitude constraint (xtoalt m after active waypoint)
+ - swtod=False and next altco is below us, descend with the angle/steepness needed to arrive at at the altitude at
+ the waypoint with the altitude constraint (xtoalt m after active waypoint)
+
+ Output if this function:
+ self.dist2vs = distance 2 next waypoint where climb/descent needs to activated
+ bs.traf.actwp.vs = V/S to be used during climb/descent part, so when dist2wp toalt + 10. * ft:
-
+ epsalt = 2.*ft # deadzone
+ if bs.traf.alt[idx] > toalt + epsalt:
+ # Stop potential current climb (e.g. due to not making it to previous altco)
+ # then stop immediately, as in: do not make it worse.
+ if bs.traf.vs[idx]>0.0001:
+ self.vnavvs[idx] = 0.0
+ self.alt = bs.traf.alt[idx]
+ if bs.traf.swvnav[idx]:
+ bs.traf.selalt[idx] = bs.traf.alt[idx]
+
+ # Descent modes: VNAV (= swtod/Top of Descent logic) or aiming at next alt constraint
# Calculate max allowed altitude at next wp (above toalt)
- bs.traf.actwp.nextaltco[idx] = min( bs.traf.alt[idx], toalt + xtoalt * self.steepness ) # [m] next alt constraint
- bs.traf.actwp.xtoalt[idx] = xtoalt # [m] distance to next alt constraint measured from next waypoint
-
-
- # Dist to waypoint where descent should start [m]
- self.dist2vs[idx] = bs.traf.actwp.turndist[idx] + \
- np.abs( bs.traf.alt[idx] - bs.traf.actwp.nextaltco[idx] ) / self.steepness
- # print("self.dist2vs=",self.dist2vs)
+ bs.traf.actwp.nextaltco[idx] = toalt # [m] next alt constraint
+ bs.traf.actwp.xtoalt[idx] = xtoalt # [m] distance to next alt constraint measured from next waypoint
+
+
+ # VNAV ToD logic
+ if self.swtod[idx]:
+ # Get distance to waypoint
+ self.dist2wp[idx] = nm*geo.kwikdist(bs.traf.lat[idx], bs.traf.lon[idx],
+ bs.traf.actwp.lat[idx],
+ bs.traf.actwp.lon[idx]) # was not always up to date, so update first
+
+ # Distance to next waypoint where we need to start descent (top of descent) [m]
+ descdist = abs(bs.traf.alt[idx] - toalt) / self.steepness # [m] required length for descent
+ self.dist2vs[idx] = descdist - xtoalt # [m] part of that length on this leg
+ print(bs.traf.id[idx],"traf.alt =",bs.traf.alt[idx]/ft,"ft toalt = ",toalt/ft,"ft descdist =",descdist/nm,"nm")
+ print ("d2wp = ",self.dist2wp[idx]/nm,"nm d2vs = ",self.dist2vs[idx]/nm,"nm")
+ print("xtoalt =",xtoalt/nm,"nm descdist =",descdist/nm,"nm")
+
+ # Exceptions: Descend now? Or never on this leg?
+ if self.dist2wp[idx] < self.dist2vs[idx]: # Urgent descent, we're late![m]
+ # Descend now using whole remaining distance on leg to reach altitude
+ self.alt[idx] = bs.traf.actwp.nextaltco[idx] # dial in altitude of next waypoint as calculated
+ t2go = self.dist2wp[idx]/max(0.01,bs.traf.gs[idx])
+ bs.traf.actwp.vs[idx] = (bs.traf.alt[idx]-toalt)/max(0.01,t2go)
+
+ elif xtoalt11.99:
-# print("bs.traf.tas.shape =",bs.traf.tas.shape)
-# print("bs.traf.gs.shape =", bs.traf.gs.shape)
-# print("bs.traf.actwp.vs.shape =", bs.traf.actwp.vs.shape)
-
- bs.traf.actwp.vs[idx] = -self.steepness * ( bs.traf.gs[idx] +
- ( bs.traf.gs[idx] < 0.2 * bs.traf.tas[idx] ) * bs.traf.tas[idx] )
-
+ # We are higher but swtod = False, so there is no ToD descent logic, simply aim at next altco
+ steepness = (bs.traf.alt[idx]-bs.traf.actwp.nextaltco[idx])/(max(0.01,self.dist2wp[idx]+xtoalt))
+ bs.traf.actwp.vs[idx] = -abs(steepness) * (bs.traf.gs[idx] +
+ (bs.traf.gs[idx] < 0.2 * bs.traf.tas[idx]) * bs.traf.tas[
+ idx])
# VNAV climb mode: climb as soon as possible (T/C logic)
elif bs.traf.alt[idx] < toalt - 10. * ft:
+ # Stop potential current descent (e.g. due to not making it to previous altco)
+ # then stop immediately, as in: do not make it worse.
+ if bs.traf.vs[idx] < -0.0001:
+ self.vnavvs[idx] = 0.0
+ self.alt = bs.traf.alt[idx]
+ if bs.traf.swvnav[idx]:
+ bs.traf.selalt[idx] = bs.traf.alt[idx]
# Altitude we want to climb to: next alt constraint in our route (could be further down the route)
- bs.traf.actwp.nextaltco[idx] = toalt # [m]
- bs.traf.actwp.xtoalt[idx] = xtoalt # [m] distance to next alt constraint measured from next waypoint
- self.alt[idx] = bs.traf.actwp.nextaltco[idx] # dial in altitude of next waypoint as calculated
- self.dist2vs[idx] = 99999.*nm # [m] Forces immediate climb as current distance to next wp will be less
-
- # Flat earth distance to next wp
- dy = ( bs.traf.actwp.lat[idx] - bs.traf.lat[idx] )
- dx = ( bs.traf.actwp.lon[idx] - bs.traf.lon[idx] ) * bs.traf.coslat[idx]
- legdist = 60. * nm * np.sqrt( dx * dx + dy * dy ) # [m]
- t2go = max( 0.1, legdist + xtoalt ) / max( 0.01, bs.traf.gs[idx] )
- bs.traf.actwp.vs[idx] = np.maximum( self.steepness * bs.traf.gs[idx], \
- ( bs.traf.actwp.nextaltco[idx] - bs.traf.alt[idx] ) / t2go ) # [m/s]
+ bs.traf.actwp.nextaltco[idx] = toalt # [m]
+ bs.traf.actwp.xtoalt[idx] = xtoalt # [m] distance to next alt constraint measured from next waypoint
+ self.alt[idx] = bs.traf.actwp.nextaltco[idx] # dial in altitude of next waypoint as calculated
+ self.dist2vs[idx] = 99999. #[m] Forces immediate climb as current distance to next wp will be less
+
+
+ t2go = max(0.1, self.dist2wp[idx]+xtoalt) / max(0.01, bs.traf.gs[idx])
+ if self.swtoc[idx]:
+ steepness = self.steepness # default steepness
+ else:
+ steepness = (bs.traf.alt[idx] - bs.traf.actwp.nextaltco[idx]) / (max(0.01, self.dist2wp[idx] + xtoalt))
+
+ bs.traf.actwp.vs[idx] = np.maximum(steepness*bs.traf.gs[idx],
+ (bs.traf.actwp.nextaltco[idx] - bs.traf.alt[idx]) / t2go) # [m/s]
# Level leg: never start V/S
else:
self.dist2vs[idx] = -999. # [m]
-
return
def setspeedforRTA( self, idx, torta, xtorta ):
@@ -479,14 +601,14 @@ def setspeedforRTA( self, idx, torta, xtorta ):
if torta < -90. : # -999 signals there is no RTA defined in remainder of route
return False
- deltime = torta - bs.sim.simt # Remaining time to next RTA [s] in simtime
- if deltime > 0: # Still possible?
- trafax = abs( bs.traf.perf.acceleration()[idx] )
- gsrta = calcvrta( bs.traf.gs[idx], xtorta, deltime, trafax )
+ deltime = torta-bs.sim.simt # Remaining time to next RTA [s] in simtime
+ if deltime>0: # Still possible?
+ gsrta = calcvrta(bs.traf.gs[idx], xtorta,
+ deltime, bs.traf.perf.axmax[idx])
# Subtract tail wind speed vector
- tailwind = ( bs.traf.windnorth[idx] * bs.traf.gsnorth[idx] + bs.traf.windeast[idx] * bs.traf.gseast[idx] ) / \
- bs.traf.gs[idx] * bs.traf.gs[idx]
+ tailwind = (bs.traf.windnorth[idx]*bs.traf.gsnorth[idx] + bs.traf.windeast[idx]*bs.traf.gseast[idx]) / \
+ bs.traf.gs[idx]
# Convert to CAS
rtacas = tas2cas( gsrta - tailwind, bs.traf.alt[idx] )
@@ -500,11 +622,13 @@ def setspeedforRTA( self, idx, torta, xtorta ):
else:
return False
-
- def selaltcmd( self, idx, alt, vspd=None ):
- """ Select altitude command: ALT acid, alt, [vspd] """
- bs.traf.selalt[idx] = alt
- bs.traf.swvnav[idx] = False
+ @stack.command(name='ALT')
+ def selaltcmd(self, idx: 'acid', alt: 'alt', vspd: 'vspd'=None):
+ """ ALT acid, alt, [vspd]
+
+ Select autopilot altitude command."""
+ bs.traf.selalt[idx] = alt
+ bs.traf.swvnav[idx] = False
# Check for optional VS argument
if vspd:
@@ -519,18 +643,24 @@ def selaltcmd( self, idx, alt, vspd=None ):
bs.traf.selvs[idx[oppositevs]] = 0.
- def selvspdcmd( self, idx, vspd ):
- """ Vertical speed autopilot command: VS acid vspd """
- bs.traf.selvs[idx] = vspd # [fpm]
+ @stack.command(name='VS')
+ def selvspdcmd(self, idx: 'acid', vspd:'vspd'):
+ """ VS acid,vspd (ft/min)
+
+ Vertical speed command (autopilot) """
+ bs.traf.selvs[idx] = vspd #[fpm]
# bs.traf.vs[idx] = vspd
bs.traf.swvnav[idx] = False
- def selhdgcmd( self, idx, hdg ): # HDG command
- """ Select heading command: HDG acid, hdg """
- if not isinstance( idx, Collection ):
- idx = np.array( [idx] )
- if not isinstance( hdg, Collection ):
- hdg = np.array( [hdg] )
+ @stack.command(name='HDG', aliases=("HEADING", "TURN"))
+ def selhdgcmd(self, idx: 'acid', hdg: 'hdg'): # HDG command
+ """ HDG acid,hdg (deg,True or Magnetic)
+
+ Autopilot select heading command. """
+ if not isinstance(idx, Collection):
+ idx = np.array([idx])
+ if not isinstance(hdg, Collection):
+ hdg = np.array([hdg])
# If there is wind, compute the corresponding track angle
if bs.traf.wind.winddim > 0:
ab50 = bs.traf.alt[idx] > 50.0 * ft
@@ -552,8 +682,11 @@ def selhdgcmd( self, idx, hdg ): # HDG command
# Everything went ok!
return True
- def selspdcmd( self, idx, casmach ): # SPD command
- """ Select speed command: SPD acid, casmach (= CASkts/Mach) """
+ @stack.command(name='SPD', aliases=("SPEED",))
+ def selspdcmd(self, idx: 'acid', casmach: 'spd'): # SPD command
+ """ SPD acid, casmach (= CASkts/Mach)
+
+ Select autopilot speed. """
# Depending on or position relative to crossover altitude,
# we will maintain CAS or Mach when altitude changes
# We will convert values when needed
@@ -563,86 +696,94 @@ def selspdcmd( self, idx, casmach ): # SPD command
bs.traf.swvnavspd[idx] = False
return True
- def setdestorig( self, cmd, idx, *args ):
- if len( args ) == 0:
- if cmd == 'DEST':
- return True, 'DEST ' + bs.traf.id[idx] + ': ' + self.dest[idx]
- return True, 'ORIG ' + bs.traf.id[idx] + ': ' + self.orig[idx]
-
- route = self.route[idx]
- name = args[0]
- apidx = bs.navdb.getaptidx( name )
+ @stack.command(name='DEST')
+ def setdest(self, acidx: 'acid', wpname:'wpt' = None):
+ ''' DEST acid, latlon/airport
+ Set destination of aircraft, aircraft wil fly to this airport. '''
+ if wpname is None:
+ return True, 'DEST ' + bs.traf.id[acidx] + ': ' + self.dest[acidx]
+ route = self.route[acidx]
+ apidx = bs.navdb.getaptidx(wpname)
if apidx < 0:
- if cmd == "DEST" and bs.traf.ap.route[idx].nwp > 0:
- reflat = bs.traf.ap.route[idx].wplat[-1]
- reflon = bs.traf.ap.route[idx].wplon[-1]
+ if bs.traf.ap.route[acidx].nwp > 0:
+ reflat = bs.traf.ap.route[acidx].wplat[-1]
+ reflon = bs.traf.ap.route[acidx].wplon[-1]
else:
- reflat = bs.traf.lat[idx]
- reflon = bs.traf.lon[idx]
+ reflat = bs.traf.lat[acidx]
+ reflon = bs.traf.lon[acidx]
- success, posobj = txt2pos( name, reflat, reflon )
+ success, posobj = txt2pos(wpname, reflat, reflon)
if success:
lat = posobj.lat
lon = posobj.lon
else:
- return False, ( cmd + ": Position " + name + " not found." )
+ return False, "DEST: Position " + wpname + " not found."
else:
lat = bs.navdb.aptlat[apidx]
lon = bs.navdb.aptlon[apidx]
+ self.dest[acidx] = wpname
+ iwp = route.addwpt(acidx, self.dest[acidx], route.dest,
+ lat, lon, 0.0, bs.traf.cas[acidx])
+ # If only waypoint: activate
+ if (iwp == 0) or (self.orig[acidx] != "" and route.nwp == 2):
+ bs.traf.actwp.lat[acidx] = route.wplat[iwp]
+ bs.traf.actwp.lon[acidx] = route.wplon[iwp]
+ bs.traf.actwp.nextaltco[acidx] = route.wpalt[iwp]
+ bs.traf.actwp.spd[acidx] = route.wpspd[iwp]
+
+ bs.traf.swlnav[acidx] = True
+ bs.traf.swvnav[acidx] = True
+ route.iactwp = iwp
+ route.direct(acidx, route.wpname[iwp])
+
+ # If not found, say so
+ elif iwp < 0:
+ return False, ('DEST position'+self.dest[acidx] + " not found.")
+
+ @stack.command(name='ORIG')
+ def setorig(self, acidx: 'acid', wpname: 'wpt' = None):
+ ''' ORIG acid, latlon/airport
+
+ Set origin of aircraft. '''
+ if wpname is None:
+ return True, 'ORIG ' + bs.traf.id[acidx] + ': ' + self.orig[acidx]
+ route = self.route[acidx]
+ apidx = bs.navdb.getaptidx(wpname)
+ if apidx < 0:
+ if bs.traf.ap.route[acidx].nwp > 0:
+ reflat = bs.traf.ap.route[acidx].wplat[-1]
+ reflon = bs.traf.ap.route[acidx].wplon[-1]
+ else:
+ reflat = bs.traf.lat[acidx]
+ reflon = bs.traf.lon[acidx]
- if cmd == "DEST":
- self.dest[idx] = name
- iwp = route.addwpt( idx, self.dest[idx], route.dest,
- lat, lon, 0.0, bs.traf.cas[idx] )
- # If only waypoint: activate
- if ( iwp == 0 ) or ( self.orig[idx] != "" and route.nwp == 2 ):
- bs.traf.actwp.lat[idx] = route.wplat[iwp]
- bs.traf.actwp.lon[idx] = route.wplon[iwp]
- bs.traf.actwp.nextaltco[idx] = route.wpalt[iwp]
- bs.traf.actwp.spd[idx] = route.wpspd[iwp]
-
- bs.traf.swlnav[idx] = True
- bs.traf.swvnav[idx] = True
- route.iactwp = iwp
- route.direct( idx, route.wpname[iwp] )
-
- # If not found, say so
- elif iwp < 0:
- return False, ( 'DEST' + self.dest[idx] + " not found." )
+ success, posobj = txt2pos(wpname, reflat, reflon)
+ if success:
+ lat = posobj.lat
+ lon = posobj.lon
+ else:
+ return False, ("ORIG: Position " + wpname + " not found.")
- # Origin: bookkeeping only for now, store in route as origin
else:
- self.orig[idx] = name
- apidx = bs.navdb.getaptidx( name )
-
- if apidx < 0:
-
- if cmd == "ORIG" and bs.traf.ap.route[idx].nwp > 0:
- reflat = bs.traf.ap.route[idx].wplat[0]
- reflon = bs.traf.ap.route[idx].wplon[0]
- else:
- reflat = bs.traf.lat[idx]
- reflon = bs.traf.lon[idx]
-
- success, posobj = txt2pos( name, reflat, reflon )
- if success:
- lat = posobj.lat
- lon = posobj.lon
- else:
- return False, ( cmd + ": Orig " + name + " not found." )
-
-
- iwp = route.addwpt( idx, self.orig[idx], route.orig,
- lat, lon, 0.0, bs.traf.cas[idx] )
- if iwp < 0:
- return False, ( self.orig[idx] + " not found." )
+ lat = bs.navdb.aptlat[apidx]
+ lon = bs.navdb.aptlon[apidx]
- def setLNAV( self, idx, flag=None ):
- """ Set LNAV on or off for specific or for all aircraft """
- if not isinstance( idx, Collection ):
+ # Origin: bookkeeping only for now, store in route as origin
+ self.orig[acidx] = wpname
+ iwp = route.addwpt(acidx, self.orig[acidx], route.orig,
+ lat, lon, 0.0, bs.traf.cas[acidx])
+ if iwp < 0:
+ return False, (self.orig[acidx] + " not found.")
+
+ @stack.command(name='LNAV')
+ def setLNAV(self, idx: 'acid', flag: 'bool'=None):
+ """ LNAV acid,[ON/OFF]
+
+ LNAV (lateral FMS mode) switch for autopilot """
+ if not isinstance(idx, Collection):
if idx is None:
# All aircraft are targeted
bs.traf.swlnav = np.array( bs.traf.ntraf * [flag] )
@@ -665,19 +806,22 @@ def setLNAV( self, idx, flag=None ):
route.direct( i, route.wpname[route.findact( i )] )
else:
bs.traf.swlnav[i] = False
- if flag == None:
- return True, '\n'.join( output )
-
- def setVNAV( self, idx, flag=None ):
- """ Set VNAV on or off for specific or for all aircraft """
- if not isinstance( idx, Collection ):
+ if flag is None:
+ return True, '\n'.join(output)
+
+ @stack.command(name='VNAV')
+ def setVNAV(self, idx: 'acid', flag: 'bool'=None):
+ """ VNAV acid,[ON/OFF]
+
+ Switch on/off VNAV mode, the vertical FMS mode (autopilot) """
+ if not isinstance(idx, Collection):
if idx is None:
# All aircraft are targeted
bs.traf.swvnav = np.array( bs.traf.ntraf * [flag] )
bs.traf.swvnavspd = np.array( bs.traf.ntraf * [flag] )
else:
# Prepare for the loop
- idx = np.array( [idx] )
+ idx = np.array([idx])
# Set VNAV for all aircraft in idx array
output = []
@@ -772,6 +916,7 @@ def calcvrta( v0, dx, deltime, trafax ):
# Normal case is one solution
else:
vtarg = vlst[0]
+
return vtarg
def distaccel( v0, v1, axabs ):
@@ -780,4 +925,4 @@ def distaccel( v0, v1, axabs ):
accel/decel is detemremind by sign of v1-v0
axabs is acceleration/deceleration of which absolute value will be used
solve for x: x = vo*t + 1/2*a*t*t v = v0 + a*t """
- return 0.5 * np.abs( v1 * v1 - v0 * v0 ) / np.maximum( .001, np.abs( axabs ) )
+ return 0.5*np.abs(v1*v1-v0*v0)/np.maximum(.001,np.abs(axabs))
diff --git a/bluesky/traffic/performance/bada/coeff_bada.py b/bluesky/traffic/performance/bada/coeff_bada.py
index 73652c01c1..a1bc2c6037 100644
--- a/bluesky/traffic/performance/bada/coeff_bada.py
+++ b/bluesky/traffic/performance/bada/coeff_bada.py
@@ -105,8 +105,8 @@ def init(bada_path=''):
releasefile = path.join(path.normpath(bada_path), 'ReleaseSummary')
if path.isfile(releasefile):
global release_date, bada_version
- re_reldate = re.compile('Summary Date:\s+(.+(?0.) or (bs.traf.selvs.any()<0.):
# thrust = f(selvs)
- T = ((bs.traf.selvs!=0)*(((bs.traf.aporasas.vs*self.mass*g0)/ \
- (self.ESF*np.maximum(bs.traf.eps,bs.traf.tas)*cpred)) \
- + self.D)) + ((bs.traf.selvs==0)*T)
+ T_vs = ((bs.traf.selvs!=0) * \
+ (((bs.traf.aporasas.vs * np.sign(delalt)*self.mass*g0)/ \
+ (self.ESF*np.maximum(bs.traf.eps,bs.traf.tas)*cpred)) \
+ + self.D)) + ((bs.traf.selvs==0)*T)
+ # limit minimum thrust in descent to idle thrust
+ T = np.where(descent, np.maximum(T_vs, T), T)
self.thrust = T
@@ -550,7 +553,12 @@ def update(self, dt):
self.pf_flag = np.where ((bs.traf.alt <0.5)*(self.post_flight), False, self.pf_flag)
- return
+ # define acceleration: aircraft taxiing and taking off use ground acceleration,
+ # landing aircraft use ground deceleration, others use standard acceleration
+ # --> BADA uses the same value for ground acceleration as for deceleration
+ self.axmax = ((self.phase == PHASE['IC']) + (self.phase == PHASE['CR']) + (self.phase == PHASE['AP']) + (self.phase == PHASE['LD'])) * 0.5 \
+ + ((self.phase == PHASE['TO']) + (self.phase == PHASE['GD'])*(1-self.post_flight)) * self.gr_acc \
+ + (self.phase == PHASE['GD']) * self.post_flight * self.gr_acc
def limits(self, intent_v, intent_vs, intent_h, ax):
"""FLIGHT ENVELPOE"""
@@ -595,31 +603,6 @@ def limits(self, intent_v, intent_vs, intent_h, ax):
return allowed_tas, allowed_vs, allowed_alt
- def acceleration(self):
- # define acceleration: aircraft taxiing and taking off use ground acceleration,
- # landing aircraft use ground deceleration, others use standard acceleration
- # --> BADA uses the same value for ground acceleration as for deceleration
-
-
- ax = ((self.phase==PHASE['IC']) + (self.phase==PHASE['CR']) + (self.phase==PHASE['AP']) + (self.phase==PHASE['LD'])) * 0.5 \
- + ((self.phase==PHASE['TO']) + (self.phase==PHASE['GD'])*(1-self.post_flight)) * self.gr_acc \
- + (self.phase==PHASE['GD']) * self.post_flight * self.gr_acc
-
- return ax
-
-
- #------------------------------------------------------------------------------
- #DEBUGGING
-
- #record data
- # self.log.write(self.dt, str(bs.traf.alt[0]), str(bs.traf.tas[0]), str(self.D[0]), str(self.T[0]), str(self.fuelflow[0]), str(bs.traf.vs[0]), str(cd[0]))
- # self.log.save()
-
- # print self.id, self.phase, self.alt/ft, self.tas/kts, self.cas/kts, self.M, \
- # self.thrust, self.D, self.fuelflow, cl, cd, self.vs/fpm, self.ESF,self.atrans, maxthr, \
- # self.vmto/kts, self.vmic/kts ,self.vmcr/kts, self.vmap/kts, self.vmld/kts, \
- # CD0f, kf, self.hmaxact
-
def show_performance(self, acid):
# PERF acid command
bs.scr.echo("Flight phase: %s" % self.phase[acid])
diff --git a/bluesky/traffic/performance/legacy/perfbs.py b/bluesky/traffic/performance/legacy/perfbs.py
index 1bfeda26c8..5ca2d1ba82 100644
--- a/bluesky/traffic/performance/legacy/perfbs.py
+++ b/bluesky/traffic/performance/legacy/perfbs.py
@@ -198,7 +198,7 @@ def update(self, dt):
# allocate aircraft to their flight phase
self.phase, self.bank = phases(bs.traf.alt, bs.traf.gs, delalt,
bs.traf.cas, self.vmto, self.vmic, self.vmap, self.vmcr, self.vmld,
- bs.traf.bank, bs.traf.bphase, bs.traf.swhdgsel,swbada)
+ bs.traf.ap.bankdef, bs.traf.bphase, bs.traf.swhdgsel,swbada)
# AERODYNAMICS
# compute CL: CL = 2*m*g/(VTAS^2*rho*S)
@@ -317,7 +317,11 @@ def update(self, dt):
# otherwise taxiing will be impossible afterwards
self.pf_flag = np.where ((bs.traf.alt <0.5)*(self.post_flight), False, self.pf_flag)
- return
+ # define acceleration: aircraft taxiing and taking off use ground acceleration,
+ # landing aircraft use ground deceleration, others use standard acceleration
+ self.axmax = ((self.phase == PHASE['IC']) + (self.phase == PHASE['CR']) + (self.phase == PHASE['AP']) + (self.phase == PHASE['LD'])) * 0.5 \
+ + ((self.phase == PHASE['TO']) + (self.phase == PHASE['GD'])*(1-self.post_flight)) * self.gr_acc \
+ + (self.phase == PHASE['GD']) * self.post_flight * self.gr_dec
def limits(self, intent_v, intent_vs, intent_h, ax):
"""Flight envelope""" # Connect this with function limits in performance.py
@@ -359,17 +363,6 @@ def limits(self, intent_v, intent_vs, intent_h, ax):
return allowed_tas, allowed_vs, allowed_alt
- def acceleration(self):
- # define acceleration: aircraft taxiing and taking off use ground acceleration,
- # landing aircraft use ground deceleration, others use standard acceleration
-
- ax = ((self.phase==PHASE['IC']) + (self.phase==PHASE['CR']) + (self.phase==PHASE['AP']) + (self.phase==PHASE['LD'])) * 0.5 \
- + ((self.phase==PHASE['TO']) + (self.phase==PHASE['GD'])*(1-self.post_flight)) * self.gr_acc \
- + (self.phase==PHASE['GD']) * self.post_flight * self.gr_dec
-
- return ax
-
-
def engchange(self, idx, engid=None):
"""change of engines - for jet aircraft only!"""
if not engid:
diff --git a/bluesky/traffic/performance/openap/coeff.py b/bluesky/traffic/performance/openap/coeff.py
index 14262c184e..e1fd7e7a7f 100644
--- a/bluesky/traffic/performance/openap/coeff.py
+++ b/bluesky/traffic/performance/openap/coeff.py
@@ -3,9 +3,14 @@
import json
import numpy as np
import pandas as pd
+import bluesky as bs
from bluesky import settings
+from bluesky.settings import get_project_root
-settings.set_variable_defaults(perf_path_openap="data/performance/OpenAP")
+
+settings.set_variable_defaults(
+ perf_path_openap=os.path.join(get_project_root(), "data", "performance", "OpenAP")
+)
LIFT_FIXWING = 1 # fixwing aircraft
LIFT_ROTOR = 2 # rotor aircraft
@@ -36,7 +41,12 @@ def __init__(self):
else:
dataline = line.strip("\n")
acmod, synomod = dataline.split("=")
- self.synodict[acmod.strip().upper()] = synomod.strip().upper()
+ acmod = acmod.strip().upper()
+ synomod = synomod.strip().upper()
+
+ if acmod == synomod:
+ continue
+ self.synodict[acmod] = synomod
self.acs_fixwing = self._load_all_fixwing_flavor()
self.engines_fixwing = pd.read_csv(fixwing_engine_db, encoding="utf-8")
@@ -94,10 +104,10 @@ def _load_all_fixwing_envelop(self):
All unit in SI"""
limits_fixwing = {}
for mdl, ac in self.acs_fixwing.items():
- fenv = fixwing_envelops_dir + mdl.lower() + ".csv"
+ fenv = fixwing_envelops_dir + mdl.lower() + ".txt"
if os.path.exists(fenv):
- df = pd.read_csv(fenv, index_col="param")
+ df = pd.read_fwf(fenv).set_index("variable")
limits_fixwing[mdl] = {}
limits_fixwing[mdl]["vminto"] = df.loc["to_v_lof"]["min"]
limits_fixwing[mdl]["vmaxto"] = df.loc["to_v_lof"]["max"]
@@ -132,17 +142,17 @@ def _load_all_fixwing_envelop(self):
limits_fixwing[mdl]["axmax"] = df.loc["to_acc_tof"]["max"]
limits_fixwing[mdl]["vsmax"] = max(
- df.loc["ic_vz_avg"]["max"],
- df.loc["cl_vz_avg_pre_cas"]["max"],
- df.loc["cl_vz_avg_cas_const"]["max"],
- df.loc["cl_vz_avg_mach_const"]["max"],
+ df.loc["ic_vs_avg"]["max"],
+ df.loc["cl_vs_avg_pre_cas"]["max"],
+ df.loc["cl_vs_avg_cas_const"]["max"],
+ df.loc["cl_vs_avg_mach_const"]["max"],
)
limits_fixwing[mdl]["vsmin"] = min(
- df.loc["ic_vz_avg"]["min"],
- df.loc["de_vz_avg_after_cas"]["min"],
- df.loc["de_vz_avg_cas_const"]["min"],
- df.loc["de_vz_avg_mach_const"]["min"],
+ df.loc["ic_vs_avg"]["min"],
+ df.loc["de_vs_avg_after_cas"]["min"],
+ df.loc["de_vs_avg_cas_const"]["min"],
+ df.loc["de_vs_avg_mach_const"]["min"],
)
# create envolop based on synonym
@@ -153,13 +163,23 @@ def _load_all_fixwing_envelop(self):
return limits_fixwing
def _load_all_rotor_envelop(self):
- """ load rotor aircraft envelop, all unit in SI"""
+ """load rotor aircraft envelop, all unit in SI"""
limits_rotor = {}
for mdl, ac in self.acs_rotor.items():
limits_rotor[mdl] = {}
- limits_rotor[mdl]["vmin"] = ac["envelop"]["v_min"]
- limits_rotor[mdl]["vmax"] = ac["envelop"]["v_max"]
- limits_rotor[mdl]["vsmin"] = ac["envelop"]["vs_min"]
- limits_rotor[mdl]["vsmax"] = ac["envelop"]["vs_max"]
- limits_rotor[mdl]["hmax"] = ac["envelop"]["h_max"]
+
+ limits_rotor[mdl]["vmin"] = ac["envelop"].get("v_min", -20)
+ limits_rotor[mdl]["vmax"] = ac["envelop"].get("v_max", 20)
+ limits_rotor[mdl]["vsmin"] = ac["envelop"].get("vs_min", -5)
+ limits_rotor[mdl]["vsmax"] = ac["envelop"].get("vs_max", 5)
+ limits_rotor[mdl]["hmax"] = ac["envelop"].get("h_max", 2500)
+
+ params = ["v_min", "v_max", "vs_min", "vs_max", "h_max"]
+ if set(params) <= set(ac["envelop"].keys()):
+ pass
+ else:
+ warn = f"Warning: Some performance parameters for {mdl} are not found, default values used."
+ print(warn)
+ bs.scr.echo(warn)
+
return limits_rotor
diff --git a/bluesky/traffic/performance/openap/perfoap.py b/bluesky/traffic/performance/openap/perfoap.py
index 4b3800bdc5..c1d3b5c41c 100644
--- a/bluesky/traffic/performance/openap/perfoap.py
+++ b/bluesky/traffic/performance/openap/perfoap.py
@@ -1,3 +1,5 @@
+import warnings
+from bluesky.stack.stackbase import stack
import numpy as np
import bluesky as bs
from bluesky.tools import aero
@@ -25,20 +27,15 @@ def __init__(self):
self.coeff = coeff.Coefficient()
with self.settrafarrays():
- self.actypes = np.array([], dtype=str)
- self.phase = np.array([])
self.lifttype = np.array([]) # lift type, fixwing [1] or rotor [2]
- self.mass = np.array([]) # mass of aircraft
self.engnum = np.array([], dtype=int) # number of engines
self.engthrmax = np.array([]) # static engine thrust
self.engbpr = np.array([]) # engine bypass ratio
- self.thrust = np.array([]) # thrust ratio at current alt spd
self.max_thrust = np.array([]) # thrust ratio at current alt spd
self.ff_coeff_a = np.array([]) # icao fuel flows coefficient a
self.ff_coeff_b = np.array([]) # icao fuel flows coefficient b
self.ff_coeff_c = np.array([]) # icao fuel flows coefficient c
self.engpower = np.array([]) # engine power, rotor ac
- self.cd0 = np.array([]) # zero drag coefficient
self.cd0_clean = np.array([]) # Cd0, clean configuration
self.k_clean = np.array([]) # k, clean configuration
self.cd0_to = np.array([]) # Cd0, takeoff configuration
@@ -57,7 +54,6 @@ def __init__(self):
self.vsmin = np.array([])
self.vsmax = np.array([])
self.hmax = np.array([])
- self.axmax = np.array([])
self.vminto = np.array([])
self.hcross = np.array([])
self.mmo = np.array([])
@@ -72,8 +68,10 @@ def create(self, n=1):
if (actype not in self.coeff.actypes_rotor) and (
actype not in self.coeff.dragpolar_fixwing
):
- if actype in self.coeff.synodict:
- # print(actype,"replaced by",self.coeff.synodict[actype])
+ if actype in self.coeff.synodict.keys():
+ # warn = f"Warning: {actype} replaced by {self.coeff.synodict[actype]}"
+ # print(warn)
+ # bs.scr.echo(warn)
actype = self.coeff.synodict[actype]
# initialize aircraft / engine performance parameters
@@ -90,6 +88,9 @@ def create(self, n=1):
else:
# convert to known aircraft type
if actype not in self.coeff.actypes_fixwing:
+ # warn = f"Warning: {actype} replaced by B744"
+ # print(warn)
+ # bs.scr.echo(warn)
actype = "B744"
# populate fuel flow model
@@ -170,11 +171,16 @@ def create(self, n=1):
"delta_cd_gear"
]
- # append update actypes, after removing unkown types
- self.actypes[-n:] = [actype] * n
+ # append update actypes, after removing unknown types
+ self.actype[-n:] = [actype] * n
+
+ # Update envelope speed limits
+ mask = np.zeros_like(self.actype, dtype=bool)
+ mask[-n:] = True
+ self.vmin[-n:], self.vmax[-n:] = self._construct_v_limits(mask)
def update(self, dt):
- """ Periodic update function for performance calculations. """
+ """Periodic update function for performance calculations."""
# update phase, infer from spd, roc, alt
lenph1 = len(self.phase)
self.phase = ph.get(
@@ -182,7 +188,7 @@ def update(self, dt):
)
# update speed limits, based on phase change
- self.vmin, self.vmax = self._construct_v_limits(self.actypes, self.phase)
+ self.vmin, self.vmax = self._construct_v_limits()
idx_fixwing = np.where(self.lifttype == coeff.LIFT_FIXWING)[0]
@@ -244,6 +250,9 @@ def update(self, dt):
+ self.ff_coeff_c[idx_fixwing]
)
+ # ----- update max acceleration ----
+ self.axmax = self.calc_axmax()
+
# TODO: implement thrust computation for rotor aircraft
# idx_rotor = np.where(self.lifttype==coeff.LIFT_ROTOR)[0]
# self.thrust[idx_rotor] = 0
@@ -279,7 +288,6 @@ def limits(self, intent_v_tas, intent_vs, intent_h, ax):
allow_h = np.where(intent_h > self.hmax, self.hmax, intent_h)
intent_v_cas = aero.vtas2cas(intent_v_tas, allow_h)
-
allow_v_cas = np.where((intent_v_cas < self.vmin), self.vmin, intent_v_cas)
allow_v_cas = np.where(intent_v_cas > self.vmax, self.vmax, allow_v_cas)
allow_v_tas = aero.vcas2tas(allow_v_cas, allow_h)
@@ -338,21 +346,21 @@ def currentlimits(self, id=None):
else:
return vtasmin, vtasmax, self.vsmin, self.vsmax
- def _construct_v_limits(self, actypes, phases):
+ def _construct_v_limits(self, mask=True):
"""Compute speed limist base on aircraft model and flight phases
Args:
- actypes (String or 1D-array): aircraft type / model
- phases (int or 1D-array): aircraft flight phases
+ mask: Indices (boolean) for aircraft to construct speed limits for.
+ When no indices are passed, all aircraft are updated.
Returns:
2D-array: vmin, vmax
"""
- n = len(actypes)
+ n = len(self.actype)
vmin = np.zeros(n)
vmax = np.zeros(n)
- ifw = np.where(self.lifttype == coeff.LIFT_FIXWING)[0]
+ ifw = np.where(np.logical_and(self.lifttype == coeff.LIFT_FIXWING, mask))[0]
vminfw = np.zeros(len(ifw))
vmaxfw = np.zeros(len(ifw))
@@ -360,25 +368,25 @@ def _construct_v_limits(self, actypes, phases):
# obtain flight envelope for speed, roc, and alt, based on flight phase
# --- minimum speed ---
- vminfw = np.where(phases[ifw] == ph.NA, 0, vminfw)
- vminfw = np.where(phases[ifw] == ph.IC, self.vminic[ifw], vminfw)
+ vminfw = np.where(self.phase[ifw] == ph.NA, 0, vminfw)
+ vminfw = np.where(self.phase[ifw] == ph.IC, self.vminic[ifw], vminfw)
vminfw = np.where(
- (phases[ifw] >= ph.CL) | (phases[ifw] <= ph.DE), self.vminer[ifw], vminfw
+ (self.phase[ifw] >= ph.CL) | (self.phase[ifw] <= ph.DE), self.vminer[ifw], vminfw
)
- vminfw = np.where(phases[ifw] == ph.AP, self.vminap[ifw], vminfw)
- vminfw = np.where(phases[ifw] == ph.GD, 0, vminfw)
+ vminfw = np.where(self.phase[ifw] == ph.AP, self.vminap[ifw], vminfw)
+ vminfw = np.where(self.phase[ifw] == ph.GD, 0, vminfw)
# --- maximum speed ---
- vmaxfw = np.where(phases[ifw] == ph.NA, self.vmaxer[ifw], vmaxfw)
- vmaxfw = np.where(phases[ifw] == ph.IC, self.vmaxic[ifw], vmaxfw)
+ vmaxfw = np.where(self.phase[ifw] == ph.NA, self.vmaxer[ifw], vmaxfw)
+ vmaxfw = np.where(self.phase[ifw] == ph.IC, self.vmaxic[ifw], vmaxfw)
vmaxfw = np.where(
- (phases[ifw] >= ph.CL) | (phases[ifw] <= ph.DE), self.vmaxer[ifw], vmaxfw
+ (self.phase[ifw] >= ph.CL) | (self.phase[ifw] <= ph.DE), self.vmaxer[ifw], vmaxfw
)
- vmaxfw = np.where(phases[ifw] == ph.AP, self.vmaxap[ifw], vmaxfw)
- vmaxfw = np.where(phases[ifw] == ph.GD, self.vmaxic[ifw], vmaxfw)
+ vmaxfw = np.where(self.phase[ifw] == ph.AP, self.vmaxap[ifw], vmaxfw)
+ vmaxfw = np.where(self.phase[ifw] == ph.GD, self.vmaxic[ifw], vmaxfw)
# rotor
- ir = np.where(self.lifttype == coeff.LIFT_ROTOR)[0]
+ ir = np.where(np.logical_and(self.lifttype == coeff.LIFT_ROTOR, mask))[0]
vminr = self.vmin[ir]
vmaxr = self.vmax[ir]
@@ -387,23 +395,30 @@ def _construct_v_limits(self, actypes, phases):
vmin[ir] = vminr
vmax[ir] = vmaxr
- return vmin, vmax
+ if isinstance(mask, bool):
+ return vmin, vmax
+ return vmin[mask], vmax[mask]
- def acceleration(self):
+ def calc_axmax(self):
# accelerations depending on phase and wing type
- acc_fixwing_ground = 2
- acc_rotor = 3.5
+ axmax_fixwing_ground = 2
+ axmax_rotor = 3.5
+
+ axmax = np.zeros(bs.traf.ntraf)
- accs = np.zeros(bs.traf.ntraf)
- accs = (self.max_thrust - self.drag) / self.mass
+ # fix-wing, in flight
+ axmax = (self.max_thrust - self.drag) / self.mass
- accs[self.phase == ph.GD] = acc_fixwing_ground
+ # fix-wing, on ground
+ axmax[self.phase == ph.GD] = axmax_fixwing_ground
- accs[self.lifttype == coeff.LIFT_ROTOR] = acc_rotor
+ # drones
+ axmax[self.lifttype == coeff.LIFT_ROTOR] = axmax_rotor
- accs[accs < 0.5] = 0.5 # minumum acceleration
+ # global minumum acceleration
+ axmax[axmax < 0.5] = 0.5
- return accs
+ return axmax
def show_performance(self, acid):
return (
diff --git a/bluesky/traffic/performance/openap/thrust.py b/bluesky/traffic/performance/openap/thrust.py
index b580a89997..e0cd8a0aa1 100644
--- a/bluesky/traffic/performance/openap/thrust.py
+++ b/bluesky/traffic/performance/openap/thrust.py
@@ -28,7 +28,7 @@ def compute_max_thr_ratio(phase, bpr, v, h, vs, thr0):
# ---- thrust ratio for descent ----
# considering 15% of inflight model thrust
- ratio_idle = 0.15 * ratio_inflight
+ ratio_idle = 0.07 * ratio_inflight
# thrust ratio array
# LD and GN assume ZERO thrust
diff --git a/bluesky/traffic/performance/perfbase.py b/bluesky/traffic/performance/perfbase.py
index dbe138995e..79147dfaea 100644
--- a/bluesky/traffic/performance/perfbase.py
+++ b/bluesky/traffic/performance/perfbase.py
@@ -1,6 +1,6 @@
-''' This module provides PerfBase, the base class for aircraft
+""" This module provides PerfBase, the base class for aircraft
performance implementations.
-'''
+"""
import numpy as np
from bluesky import settings
from bluesky.core import Entity, timed_function
@@ -11,7 +11,8 @@
class PerfBase(Entity, replaceable=True):
- """ Base class for BlueSky aircraft performance implementations. """
+ """Base class for BlueSky aircraft performance implementations."""
+
def __init__(self):
super().__init__()
with self.settrafarrays():
@@ -33,58 +34,73 @@ def __init__(self):
# Performance limits per aircraft
self.vmin = np.array([])
self.vmax = np.array([])
+ self.axmax = np.array([]) # Max/min acceleration [m/s2]
+ def create(self, n):
+ super().create(n=n)
+ self.axmax[-n:] = 2.0 # Default acceleration limit is 2 m/s2
@timed_function(name="performance", dt=settings.performance_dt, manual=True)
def update(self, dt=settings.performance_dt):
- """implement this method """
+ """implement this method"""
pass
def limits(self, intent_v, intent_vs, intent_h, ax):
- """implement this method """
+ """implement this method"""
return intent_v, intent_vs, intent_h
def currentlimits(self):
- """implement this method """
+ """implement this method"""
# Get current kinematic performance envelop of all aircraft
pass
- def acceleration(self):
- ''' Default aircraft acceleration is 2 m/s2. '''
- return 2.0
-
- @command(name='ENG')
- def engchange(self, acid : 'acid', engine_id : 'txt' = ''):
- """ Specify a different engine type for aircraft 'acid' """
- return False, 'The currently selected performance model doesn\'t support engine changes.'
-
- @command(name='PERFSTATS', aliases=('PERFINFO', 'PERFDATA'))
- def show_performance(self, acid : 'acid'):
- """ Show aircraft perfromance parameters for aircraft 'acid' """
- return False, 'The currently selected performance model doesn\'t provide this function.'
-
+ @command(name="ENG")
+ def engchange(self, acid: "acid", engine_id: "txt" = ""):
+ """Specify a different engine type for aircraft 'acid'"""
+ return (
+ False,
+ "The currently selected performance model doesn't support engine changes.",
+ )
+
+ @command(name="PERFSTATS", aliases=("PERFINFO", "PERFDATA"))
+ def show_performance(self, acid: "acid"):
+ """Show aircraft perfromance parameters for aircraft 'acid'"""
+ return (
+ False,
+ "The currently selected performance model doesn't provide this function.",
+ )
@staticmethod
- @command(name='PERF')
- def setmethod(name: 'txt' = ''):
- ''' Select a Performance implementation. '''
+ @command(name="PERF")
+ def setmethod(name: "txt" = ""):
+ """Select a Performance implementation."""
# Get a dict of all registered Performance models
methods = PerfBase.derived()
- names = ['OFF' if n == 'PERFBASE' else n for n in methods]
+ names = ["OFF" if n == "PERFBASE" else n for n in methods]
if not name:
- curname = 'OFF' if PerfBase.selected() is PerfBase else PerfBase.selected().__name__
- return True, f'Current Performance model: {curname}' + \
- f'\nAvailable performance models: {", ".join(names)}'
+ curname = (
+ "OFF"
+ if PerfBase.selected() is PerfBase
+ else PerfBase.selected().__name__
+ )
+ return (
+ True,
+ f"Current Performance model: {curname}"
+ + f'\nAvailable performance models: {", ".join(names)}',
+ )
# Check if the requested method exists
- if name == 'OFF':
+ if name == "OFF":
PerfBase.select()
- return True, 'Performance model turned off.'
+ return True, "Performance model turned off."
method = methods.get(name, None)
if method is None:
- return False, f'{name} doesn\'t exist.\n' + \
- f'Available performance models: {", ".join(names)}'
+ return (
+ False,
+ f"{name} doesn't exist.\n"
+ + f'Available performance models: {", ".join(names)}',
+ )
# Select the requested method
method.select()
- return True, f'Selected {method.__name__} as performance model.'
+ return True, f"Selected {method.__name__} as performance model."
diff --git a/bluesky/traffic/route.py b/bluesky/traffic/route.py
index 36f0916cde..0ef96f1f86 100644
--- a/bluesky/traffic/route.py
+++ b/bluesky/traffic/route.py
@@ -1,5 +1,6 @@
""" Route implementation for the BlueSky FMS."""
from os import path
+from weakref import WeakValueDictionary
from numpy import *
import bluesky as bs
from bluesky.tools import geo
@@ -33,16 +34,21 @@ class Route(Replaceable):
calcwp = 4 # Calculated waypoint (T/C, T/D, A/C)
runway = 5 # Runway: Copy name and positions
+ # Aircraft route objects
+ _routes = WeakValueDictionary()
+
def __init__(self, acid):
+ # Add self to dictionary of all aircraft routes
+ Route._routes[acid] = self
# Aircraft id (callsign) of the aircraft to which this route belongs
self.acid = acid
self.nwp = 0
- # Waypoint data
- self.wpname = []
- self.wptype = []
- self.wplat = []
- self.wplon = []
+ # Waypoint data
+ self.wpname = [] # List of waypoint names for this flight plan
+ self.wptype = [] # List of waypoint types
+ self.wplat = [] # List of waypoint latitudes
+ self.wplon = [] # List of waypoint longitudes
self.wpalt = [] # [m] negative value means not specified
self.wpspd = [] # [m/s] negative value means not specified
self.wprta = [] # [m/s] negative value means not specified
@@ -97,10 +103,50 @@ def get_available_name(data, name_, len_=2):
appi += 1
name_ = name_[:-len_]+fmt_.format(appi)
return name_
+
+ @stack.command(name = 'ADDWPTMODE', annotations = 'acid, [wpt,alt]')
+ @staticmethod
+ def addwptMode(acidx, mode = None, value = None):
+ '''Changes the mode of the ADDWPT command to add waypoints of type 'mode'.
+ Available modes: FLYBY, FLYOVER, FLYTURN. Also used to specify
+ TURNSPEED or TURNRADIUS.'''
+ # Get aircraft route
+ acid = bs.traf.id[acidx]
+ acrte = Route._routes.get(acid)
+ # First, we want to check what 'mode' is, and then call addwptStack
+ # accordingly.
+ if mode in ['FLYBY', 'FLYOVER', 'FLYTURN']:
+ # We're just changing addwpt mode, call the appropriate function.
+ Route.addwptStack(acidx, mode)
+ return True
+
+ elif mode in ['TURNSPEED', 'TURNSPD', 'TURNRADIUS', 'TURNRAD']:
+ # We're changing the turn speed or radius
+ Route.addwptStack(acidx, mode, value)
+ return True
+
+ elif mode == None:
+ # Just echo the current wptmode
+ if acrte.swflyby == True and acrte.swflyturn == False:
+ bs.scr.echo('Current ADDWPT mode is FLYBY.')
+ return True
- def addwptStack(self, idx, *args): # args: all arguments of addwpt
- """ADDWPT acid, (wpname/lat,lon),[alt],[spd],[afterwp],[beforewp]"""
+ elif acrte.swflyby == False and acrte.swflyturn == False:
+ bs.scr.echo('Current ADDWPT mode is FLYOVER.')
+ return True
+ else:
+ bs.scr.echo('Current ADDWPT mode is FLYTURN.')
+ return True
+
+ @stack.command(name='ADDWPT', annotations='acid,wpt,[alt,spd,wpinroute,wpinroute]', aliases=("WPTYPE",))
+ @staticmethod
+ def addwptStack(acidx, *args): # args: all arguments of addwpt
+ """ADDWPT acid, (wpname/lat,lon),[alt],[spd],[afterwp],[beforewp]"""
+ # First get the appropriate ac route
+ acid = bs.traf.id[acidx]
+ acrte = Route._routes.get(acid)
+
#debug print ("addwptStack:",args)
#print("active = ",self.wpname[self.iactwp])
#print(args)
@@ -110,20 +156,18 @@ def addwptStack(self, idx, *args): # args: all arguments of addwpt
swwpmode = args[0].replace('-', '')
if swwpmode == "FLYBY":
- self.swflyby = True
- self.swflyturn = False
+ acrte.swflyby = True
+ acrte.swflyturn = False
return True
elif swwpmode == "FLYOVER":
-
- self.swflyby = False
- self.swflyturn = False
+ acrte.swflyby = False
+ acrte.swflyturn = False
return True
elif swwpmode == "FLYTURN":
-
- self.swflyby = False
- self.swflyturn = True
+ acrte.swflyby = False
+ acrte.swflyturn = True
return True
elif len(args) == 2:
@@ -133,7 +177,7 @@ def addwptStack(self, idx, *args): # args: all arguments of addwpt
if swwpmode == "TURNRAD" or swwpmode == "TURNRADIUS":
try:
- self.turnrad = float(args[1])/ft # arg was originally parsed as wpalt
+ acrte.turnrad = float(args[1])/ft # arg was originally parsed as wpalt
except:
return False,"Error in processing value of turn radius"
return True
@@ -141,7 +185,7 @@ def addwptStack(self, idx, *args): # args: all arguments of addwpt
elif swwpmode == "TURNSPD" or swwpmode == "TURNSPEED":
try:
- self.turnspd = args[1]*kts/ft # [m/s] Arg was wpalt Keep it as IAS/CAS orig in kts, now in m/s
+ acrte.turnspd = args[1]*kts/ft # [m/s] Arg was wpalt Keep it as IAS/CAS orig in kts, now in m/s
except:
return False, "Error in processing value of turn speed"
@@ -152,18 +196,18 @@ def addwptStack(self, idx, *args): # args: all arguments of addwpt
# Choose reference position ot look up VOR and waypoints
# First waypoint: own position
- if self.nwp == 0:
- reflat = bs.traf.lat[idx]
- reflon = bs.traf.lon[idx]
+ if acrte.nwp == 0:
+ reflat = bs.traf.lat[acidx]
+ reflon = bs.traf.lon[acidx]
# Or last waypoint before destination
else:
- if self.wptype[-1] != Route.dest or self.nwp == 1:
- reflat = self.wplat[-1]
- reflon = self.wplon[-1]
+ if acrte.wptype[-1] != Route.dest or acrte.nwp == 1:
+ reflat = acrte.wplat[-1]
+ reflon = acrte.wplon[-1]
else:
- reflat = self.wplat[-2]
- reflon = self.wplon[-2]
+ reflat = acrte.wplat[-2]
+ reflon = acrte.wplon[-2]
# Default altitude, speed and afterwp
alt = -999.
@@ -190,7 +234,7 @@ def addwptStack(self, idx, *args): # args: all arguments of addwpt
wptype = Route.runway
else: # treat as lat/lon
- name = bs.traf.id[idx]
+ name = acid
wptype = Route.wplatlon
if len(args) > 1 and args[1]:
@@ -214,8 +258,8 @@ def addwptStack(self, idx, *args): # args: all arguments of addwpt
# Look up runway in route
rwyrteidx = -1
i = 0
- while i0:
+ while i0:
# print (self.wpname[i])
rwyrteidx = i
i += 1
@@ -227,19 +271,19 @@ def addwptStack(self, idx, *args): # args: all arguments of addwpt
# print ("rwyrteidx =",rwyrteidx)
# We find a runway in the route, so use it
if rwyrteidx>0:
- rwylat = self.wplat[rwyrteidx]
- rwylon = self.wplon[rwyrteidx]
+ rwylat = acrte.wplat[rwyrteidx]
+ rwylon = acrte.wplon[rwyrteidx]
aptidx = bs.navdb.getapinear(rwylat,rwylon)
aptname = bs.navdb.aptname[aptidx]
- rwyname = self.wpname[rwyrteidx].split("/")[1]
+ rwyname = acrte.wpname[rwyrteidx].split("/")[1]
rwyid = rwyname.replace("RWY","").replace("RW","")
rwyhdg = bs.navdb.rwythresholds[aptname][rwyid][2]
else:
- rwylat = bs.traf.lat[idx]
- rwylon = bs.traf.lon[idx]
- rwyhdg = bs.traf.trk[idx]
+ rwylat = bs.traf.lat[acidx]
+ rwylon = bs.traf.lon[acidx]
+ rwyhdg = bs.traf.trk[acidx]
elif args[1].count("/") > 0 or len(args) > 2 and args[2]: # we need apt,rwy
# Take care of both EHAM/RW06 as well as EHAM,RWY18L (so /&, and RW/RWY)
@@ -267,8 +311,8 @@ def addwptStack(self, idx, *args): # args: all arguments of addwpt
if success:
rwylat,rwylon = posobj.lat,posobj.lon
else:
- rwylat = bs.traf.lat[idx]
- rwylon = bs.traf.lon[idx]
+ rwylat = bs.traf.lat[acidx]
+ rwylon = bs.traf.lon[acidx]
else:
return False,"Use ADDWPT TAKEOFF,AIRPORTID,RWYNAME"
@@ -280,258 +324,333 @@ def addwptStack(self, idx, *args): # args: all arguments of addwpt
# Add after the runwy in the route
if rwyrteidx > 0:
- afterwp = self.wpname[rwyrteidx]
+ afterwp = acrte.wpname[rwyrteidx]
- elif self.wptype and self.wptype[0] == Route.orig:
- afterwp = self.wpname[0]
+ elif acrte.wptype and acrte.wptype[0] == Route.orig:
+ afterwp = acrte.wpname[0]
else:
# Assume we're called before other waypoints are added
afterwp = ""
- name = "T/O-" + bs.traf.id[idx] # Use lat/lon naming convention
+ name = "T/O-" + acid # Use lat/lon naming convention
# Add waypoint
- wpidx = self.addwpt(idx, name, wptype, lat, lon, alt, spd, afterwp, beforewp)
+ wpidx = acrte.addwpt(acidx, name, wptype, lat, lon, alt, spd, afterwp, beforewp)
# Recalculate flight plan
- self.calcfp()
+ acrte.calcfp()
# Check for success by checking inserted location in flight plan >= 0
if wpidx < 0:
return False, "Waypoint " + name + " not added."
# check for presence of orig/dest
- norig = int(bs.traf.ap.orig[idx] != "") # 1 if orig is present in route
- ndest = int(bs.traf.ap.dest[idx] != "") # 1 if dest is present in route
+ norig = int(bs.traf.ap.orig[acidx] != "") # 1 if orig is present in route
+ ndest = int(bs.traf.ap.dest[acidx] != "") # 1 if dest is present in route
# Check whether this is first 'real' waypoint (not orig & dest),
# And if so, make active
- if self.nwp - norig - ndest == 1: # first waypoint: make active
- self.direct(idx, self.wpname[norig]) # 0 if no orig
+ if acrte.nwp - norig - ndest == 1: # first waypoint: make active
+ acrte.direct(acidx, acrte.wpname[norig]) # 0 if no orig
#print("direct ",self.wpname[norig])
- bs.traf.swlnav[idx] = True
+ bs.traf.swlnav[acidx] = True
- if afterwp and self.wpname.count(afterwp) == 0:
- return True, "Waypoint " + afterwp + " not found" + \
+ if afterwp and acrte.wpname.count(afterwp) == 0:
+ print(afterwp, acrte.wpname)
+ return True, "Waypoint " + afterwp + " not found\n" + \
"waypoint added at end of route"
else:
return True
+ @stack.command
+ def addwaypoints(acidx: 'acid', *args):
+ # Args come in this order: lat, lon, alt, spd, TURNSPD/TURNRAD/FLYBY, turnspeed or turnrad value
+ # If turn is '0', then ignore turnspeed
+ if len(args)%6 !=0:
+ bs.scr.echo('You missed a waypoint value, arguement number must be a multiple of 6.')
+ return
- def afteraddwptStack(self, idx, *args): # args: all arguments of addwpt
+ acid = bs.traf.id[acidx]
+ acrte = Route._routes.get(acid)
- # AFTER acid, wpinroute ADDWPT (wpname/lat,lon),[alt],[spd]"
- if len(args) < 3:
- return False, "AFTER needs more arguments"
+ args = reshape(args, (int(len(args)/6), 6))
- # Change order of arguments
- arglst = [args[2], None, None, args[0]] # postxt,,,afterwp
+ for wpdata in args:
+ # Get needed values
+ lat = float(wpdata[0]) # deg
+ lon = float(wpdata[1]) # deg
+ if wpdata[2]:
+ alt = txt2alt(wpdata[2]) # comes in feet, convert
+ else:
+ alt = -999
+ if wpdata[3]:
+ spd = txt2spd(wpdata[3])
+ else:
+ spd = -999
+
+ # Do flyby or flyturn processing
+ if wpdata[4] in ['TURNSPD', 'TURNSPEED']:
+ acrte.turnspd = txt2spd(wpdata[5])
+ acrte.swflyby = False
+ acrte.swflyturn = True
+ elif wpdata[4] in ['TURNRAD', 'TURNRADIUS']:
+ acrte.turnrad = float(wpdata[5])
+ acrte.swflyby = False
+ acrte.swflyturn = True
+ else:
+ # Either it's a flyby, or a typo.
+ acrte.swflyby = True
+ acrte.swflyturn = False
- # Add alt when given
- if len(args) > 3:
- arglst[1] = args[3] # alt
+ name = acid
+ wptype = Route.wplatlon
- # Add speed when given
- if len(args) > 4:
- arglst[2] = args[4] # spd
+ wpidx = acrte.addwpt_simple(acidx, name, wptype, lat, lon, alt, spd)
- result = self.addwptStack(idx, *arglst) # args: all arguments of addwpt
+ # Calculate flight plan
+ acrte.calcfp()
- return result
+ # Check for success by checking inserted location in flight plan >= 0
+ if wpidx < 0:
+ return False, "Waypoint " + name + " not added."
- def atwptStack(self, idx, *args): # args: all arguments of addwpt
- #print("args=",args)
+ def addwpt_simple(self, iac, name, wptype, lat, lon, alt=-999., spd=-999.):
+ """Adds waypoint in the most simple way possible"""
+ # For safety
+ self.nwp = len(self.wplat)
- # AT acid, wpinroute [DEL] ALT/SPD spd/alt"
- # args = wpname,SPD/ALT, spd/alt(string)
+ name = name.upper().strip()
- if len(args) < 1:
- return False, "AT needs at least an aicraft id and a waypoint name"
+ wplat = lat
+ wplon = lon
- else:
- name = args[0]
- if name in self.wpname:
- wpidx = self.wpname.index(name)
+ # Check if name already exists, if so add integer 01, 02, 03 etc.
+ newname = Route.get_available_name(
+ self.wpname, name, 3)
+
+ self.addwpt_data(
+ False, self.nwp, newname, wplat, wplon, wptype, alt, spd)
+
+ idx = self.nwp
+ self.nwp += 1
+
+ #update qdr and "last waypoint switch" in traffic
+ if idx>=0:
+ bs.traf.actwp.next_qdr[iac] = self.getnextqdr()
+ bs.traf.actwp.swlastwp[iac] = (self.iactwp==self.nwp-1)
+
+ # Update autopilot settings
+ if 0 <= self.iactwp < self.nwp:
+ self.direct(iac, self.wpname[self.iactwp])
+
+ return idx
+
+ @stack.command
+ @staticmethod
+ def before(acidx : 'acid', beforewp: 'wpinroute', addwpt, waypoint, alt: 'alt' = None, spd: 'spd' = None):
+ ''' BEFORE acid, wpinroute ADDWPT acid, (wpname/lat,lon),[alt],[spd]
+
+ Before waypoint, add a waypoint to route of aircraft (FMS).
+ '''
+ return Route.addwptStack(acidx, waypoint, alt, spd, None, beforewp)
+
+ @stack.command
+ @staticmethod
+ def after(acidx: 'acid', afterwp: 'wpinroute', addwpt, waypoint, alt:'alt' = None, spd: 'spd' = None):
+ ''' AFTER acid, wpinroute ADDWPT (wpname/lat,lon),[alt],[spd]
- if len(args) == 1 or \
- (len(args) == 2 and not args[1].count("/") == 1):
- # Only show Altitude and/or speed set in route at this waypoint:
- # KL204 AT LOPIK => acid AT wpinroute: show alt & spd constraints at this waypoint
- # KL204 AT LOPIK SPD => acid AT wpinroute SPD: show spd constraint at this waypoint
- # KL204 AT LOPIK ALT => acid AT wpinroute ALT: show alt constraint at this waypoint
+ After waypoint, add a waypoint to route of aircraft (FMS).
+ '''
+ return Route.addwptStack(acidx, waypoint, alt, spd, afterwp)
- txt = name + " : "
+ @stack.command
+ @staticmethod
+ def at(acidx: 'acid', atwp : 'wpinroute', *args):
+ ''' AT acid, wpinroute [DEL] ALT/SPD/DO alt/spd/stack command'''
+ # args = wpname,SPD/ALT, spd/alt(string)
+ acid = bs.traf.id[acidx]
+ acrte = Route._routes.get(acid)
+ if atwp in acrte.wpname:
+ wpidx = acrte.wpname.index(atwp)
+
+ if not args or \
+ (len(args) == 1 and not args[0].count("/") == 1):
+ # Only show Altitude and/or speed set in route at this waypoint:
+ # KL204 AT LOPIK => acid AT wpinroute: show alt & spd constraints at this waypoint
+ # KL204 AT LOPIK SPD => acid AT wpinroute SPD: show spd constraint at this waypoint
+ # KL204 AT LOPIK ALT => acid AT wpinroute ALT: show alt constraint at this waypoint
+ txt = atwp + " : "
+
+ # Select what to show
+ if len(args) == 0:
+ swalt = True
+ swspd = True
+ swat = True
+ else:
+ swalt = args[0].upper() == "ALT"
+ swspd = args[0].upper() in ("SPD","SPEED")
+ swat = args[0].upper() in ("DO", "STACK")
- # Select what to show
- if len(args)==1:
+ # To be safe show both when we do not know what
+ if not (swalt or swspd or swat):
swalt = True
swspd = True
swat = True
- else:
- swalt = args[1].upper()=="ALT"
- swspd = args[1].upper() in ("SPD","SPEED")
- swat = args[1].upper() in ("DO", "STACK")
- # To be safe show both when we do not know what
- if not (swalt or swspd or swat):
- swalt = True
- swspd = True
- swat = True
+ # Show altitude
+ if swalt:
+ if acrte.wpalt[wpidx] < 0:
+ txt += "-----"
- # Show altitude
- if swalt:
- if self.wpalt[wpidx] < 0:
- txt += "-----"
+ elif acrte.wpalt[wpidx] > 4500 * ft:
+ fl = int(round((acrte.wpalt[wpidx] / (100. * ft))))
+ txt += "FL" + str(fl)
- elif self.wpalt[wpidx] > 4500 * ft:
- fl = int(round((self.wpalt[wpidx] / (100. * ft))))
- txt += "FL" + str(fl)
+ else:
+ txt += str(int(round(acrte.wpalt[wpidx] / ft)))
- else:
- txt += str(int(round(self.wpalt[wpidx] / ft)))
+ if swspd:
+ txt += "/"
- if swspd:
- txt += "/"
+ # Show speed
+ if swspd:
+ if acrte.wpspd[wpidx] < 0:
+ txt += "---"
+ else:
+ txt += str(int(round(acrte.wpspd[wpidx] / kts)))
- # Show speed
- if swspd:
- if self.wpspd[wpidx] < 0:
- txt += "---"
- else:
- txt += str(int(round(self.wpspd[wpidx] / kts)))
+ # Type
+ if swalt and swspd:
+ if acrte.wptype[wpidx] == Route.orig:
+ txt += "[orig]"
+ elif acrte.wptype[wpidx] == Route.dest:
+ txt += "[dest]"
- # Type
- if swalt and swspd:
- if self.wptype[wpidx] == Route.orig:
- txt += "[orig]"
- elif self.wptype[wpidx] == Route.dest:
- txt += "[dest]"
+ # Show also stacked commands for when passing this waypoint
+ if swat:
+ if len(acrte.wpstack[wpidx])>0:
+ txt = txt+"\nStack:\n"
+ for stackedtxt in acrte.wpstack[wpidx]:
+ txt = txt + stackedtxt + "\n"
- # Show also stacked commands for when passing this waypoint
- if swat:
- if len(self.wpstack[wpidx])>0:
- txt = txt+"\nStack:\n"
- for stackedtxt in self.wpstack[wpidx]:
- txt = txt + stackedtxt + "\n"
+ return True, txt
- return True, txt
+ elif args[0].count("/")==1:
+ # Set both alt & speed at this waypoint
+ # KL204 AT LOPIK FL090/250 => acid AT wpinroute alt/spd
+ success = True
- elif args[1].count("/")==1:
- # Set both alt & speed at this waypoint
- # KL204 AT LOPIK FL090/250 => acid AT wpinroute alt/spd
- success = True
+ # Use parse from stack.py to interpret alt & speed
+ alttxt, spdtxt = args[0].split('/')
- # Use parse from stack.py to interpret alt & speed
- alttxt, spdtxt = args[1].split('/')
+ # Edit waypoint altitude constraint
+ if alttxt.count('-') > 1: # "----" = delete
+ acrte.wpalt[wpidx] = -999.
+ else:
+ try:
+ acrte.wpalt[wpidx] = txt2alt(alttxt)
+ except ValueError as e:
+ success = False
- # Edit waypoint altitude constraint
- if alttxt.count('-') > 1: # "----" = delete
- self.wpalt[wpidx] = -999.
- else:
- try:
- self.wpalt[wpidx] = txt2alt(alttxt)
- except ValueError as e:
- success = False
+ # Edit waypoint speed constraint
+ if spdtxt.count('-') > 1: # "----" = delete
+ acrte.wpspd[wpidx] = -999.
+ else:
+ try:
+ acrte.wpalt[wpidx] = txt2spd(spdtxt)
+ except ValueError as e:
+ success = False
- # Edit waypoint speed constraint
- if spdtxt.count('-') > 1: # "----" = delete
- self.wpspd[wpidx] = -999.
- else:
- try:
- self.wpalt[wpidx] = txt2spd(spdtxt)
- except ValueError as e:
- success = False
+ if not success:
+ return False,"Could not parse "+args[0]+" as alt / spd"
- if not success:
- return False,"Could not parse "+args[1]+" as alt / spd"
+ # If success: update flight plan and guidance
+ acrte.calcfp()
+ acrte.direct(acidx, acrte.wpname[acrte.iactwp])
- # If success: update flight plan and guidance
- self.calcfp()
- self.direct(idx, self.wpname[self.iactwp])
+ #acid AT wpinroute ALT/SPD alt/spd
+ elif len(args)>=2:
+ # KL204 AT LOPIK ALT FL090 => set altitude to be reached at this waypoint in route
+ # KL204 AT LOPIK SPD 250 => Set speed at twhich is set at this waypoint
+ # KL204 AT LOPIK DO PAN LOPIK => When passing stack command after DO
+ # KL204 AT LOPIK STACK PAN LOPIK => AT...STACK synonym for AT...DO
+ # KL204 AT LOPIK DO ALT FL240 => => stack "KL204 ALT FL240" => use acid from beginning if omitted as first argument
- #acid AT wpinroute ALT/SPD alt/spd
- elif len(args)>=3:
- # KL204 AT LOPIK ALT FL090 => set altitude to be reached at this waypoint in route
- # KL204 AT LOPIK SPD 250 => Set speed at twhich is set at this waypoint
- # KL204 AT LOPIK DO PAN LOPIK => When passing stack command after DO
- # KL204 AT LOPIK STACK PAN LOPIK => AT...STACK synonym for AT...DO
- # KL204 AT LOPIK DO ALT FL240 => => stack "KL204 ALT FL240" => use acid from beginning if omitted as first argument
+ swalt = args[0].upper()=="ALT"
+ swspd = args[0].upper() in ("SPD","SPEED")
+ swat = args[0].upper() in ("DO","STACK")
- swalt = args[1].upper()=="ALT"
- swspd = args[1].upper() in ("SPD","SPEED")
- swat = args[1].upper() in ("DO","STACK")
+ # Use parse from stack.py to interpret alt & speed
- # Use parse from stack.py to interpret alt & speed
+ # Edit waypoint altitude constraint
+ if swalt:
+ try:
+ acrte.wpalt[wpidx] = txt2alt(args[1])
+ except ValueError as e:
+ return False, e.args[0]
- # Edit waypoint altitude constraint
- if swalt:
+ # Edit waypoint speed constraint
+ elif swspd:
+ try:
+ acrte.wpspd[wpidx] = txt2spd(args[1])
+ except ValueError as e:
+ return False, e.args[0]
+
+ # add stack command: args[1] is DO or STACK, args[2:] contains a command
+ elif swat:
+ # Check if first argument is missing aircraft id, if so, use this acid
+
+ # IF command starts with aircraft id, it is not missing
+ cmd = args[1].upper()
+ if not(cmd in bs.traf.id):
+ # Look up arg types
try:
- self.wpalt[wpidx] = txt2alt(args[2])
- except ValueError as e:
- return False, e.args[0]
+ cmdobj = Command.cmddict.get(cmd)
- # Edit waypoint speed constraint
- elif swspd:
- try:
- self.wpspd[wpidx] = txt2spd(args[2])
- except ValueError as e:
- return False, e.args[0]
-
- # add stack command: args[1] is DO or STACK, args[2:] contains a command
- elif swat:
- # Check if first argument is missing aircraft id, if so, use this acid
-
- # IF command starts with aircraft id, it is not missing
- cmd = args[2].upper()
- if not(cmd in bs.traf.id):
- # Look up arg types
- try:
- cmdobj = Command.cmddict.get(cmd)
-
- # Command found, check arguments
- argtypes = cmdobj.annotations
-
- if argtypes[0]=="acid" and not (args[3].upper() in bs.traf.id):
- # missing acid, so add ownship acid
- self.wpstack[wpidx].append(self.acid+" "+" ".join(args[2:]))
- else:
- # This command does not need an acid or it is already first argument
- self.wpstack[wpidx].append(" ".join(args[2:]))
- except:
- return False, "Stacked command "+cmd+"unknown"
- else:
- # Command line starts with an aircraft id at the beginning of the command line, stack it
- self.wpstack[wpidx].append(" ".join(args[2:]))
+ # Command found, check arguments
+ argtypes = cmdobj.annotations
- # Delete a constraint (or both) at this waypoint
- elif args[1]=="DEL" or args[1]=="DELETE" or args[1]=="CLR" or args[1]=="CLEAR" :
- swalt = args[2].upper()=="ALT"
- swspd = args[2].upper() in ("SPD","SPEED")
- swboth = args[2].upper()=="BOTH"
- swall = args[2].upper()=="ALL"
+ if argtypes[0]=="acid" and not (args[2].upper() in bs.traf.id):
+ # missing acid, so add ownship acid
+ acrte.wpstack[wpidx].append(acid+" "+" ".join(args[1:]))
+ else:
+ # This command does not need an acid or it is already first argument
+ acrte.wpstack[wpidx].append(" ".join(args[1:]))
+ except:
+ return False, "Stacked command "+cmd+"unknown"
+ else:
+ # Command line starts with an aircraft id at the beginning of the command line, stack it
+ acrte.wpstack[wpidx].append(" ".join(args[1:]))
- if swspd or swboth or swall:
- self.wpspd[wpidx] = -999.
+ # Delete a constraint (or both) at this waypoint
+ elif args[0]=="DEL" or args[0]=="DELETE" or args[0]=="CLR" or args[0]=="CLEAR" :
+ swalt = args[1].upper()=="ALT"
+ swspd = args[1].upper() in ("SPD","SPEED")
+ swboth = args[1].upper()=="BOTH"
+ swall = args[1].upper()=="ALL"
- if swalt or swboth or swall:
- self.wpalt[wpidx] = -999.
+ if swspd or swboth or swall:
+ acrte.wpspd[wpidx] = -999.
- if swall:
- self.wpstack[wpidx]=[]
+ if swalt or swboth or swall:
+ acrte.wpalt[wpidx] = -999.
- else:
- return False,"No "+args[1]+" at ",name
+ if swall:
+ acrte.wpstack[wpidx]=[]
+ else:
+ return False,"No "+args[0]+" at ",atwp
- # If success: update flight plan and guidance
- self.calcfp()
- self.direct(idx, self.wpname[self.iactwp])
- # Waypoint not found in route
- else:
- return False, name + " not found in route " + bs.traf.id[idx]
+ # If success: update flight plan and guidance
+ acrte.calcfp()
+ acrte.direct(acidx, acrte.wpname[acrte.iactwp])
+
+ # Waypoint not found in route
+ else:
+ return False, atwp + " not found in route " + acid
return True
@@ -729,155 +848,151 @@ def addwpt(self, iac, name, wptype, lat, lon, alt=-999., spd=-999., afterwp="",
return idx
- def beforeaddwptStack(self, idx, *args): # args: all arguments of addwpt
- # BEFORE acid, wpinroute ADDWPT acid, (wpname/lat,lon),[alt],[spd]"
- if len(args) < 3:
- return False, "BEFORE needs more arguments"
-
- # Change order of arguments
- arglst = [args[2], None, None, None, args[0]] # postxt,,,,beforewp
-
- # Add alt when given
- if len(args) > 3:
- arglst[1] = args[3] # alt
-
- # Add speed when given
- if len(args) > 4:
- arglst[2] = args[4] # spd
-
- result = self.addwptStack(idx, *arglst) # args: all arguments of addwpt
-
- return result
+ @stack.command(aliases=("DIRECTTO", "DIRTO"))
+ @staticmethod
+ def direct(acidx: 'acid', wpname: 'wpinroute'):
+ """DIRECT acid wpname
+
+ Go direct to specified waypoint in route (FMS)"""
+ acid = bs.traf.id[acidx]
+ acrte = Route._routes.get(acid)
+ wpidx = acrte.wpname.index(wpname)
- def direct(self, idx, wpnam):
- #print("Hello from direct")
- """Set active point to a waypoint by name"""
- name = wpnam.upper().strip()
- if name != "" and self.wpname.count(name) > 0:
- wpidx = self.wpname.index(name)
+ acrte.iactwp = wpidx
+ bs.traf.actwp.lat[acidx] = acrte.wplat[wpidx]
+ bs.traf.actwp.lon[acidx] = acrte.wplon[wpidx]
+ bs.traf.actwp.flyby[acidx] = acrte.wpflyby[wpidx]
+ bs.traf.actwp.flyturn[acidx] = acrte.wpflyturn[wpidx]
+ bs.traf.actwp.turnrad[acidx] = acrte.wpturnrad[wpidx]
+ bs.traf.actwp.turnspd[acidx] = acrte.wpturnspd[wpidx]
- self.iactwp = wpidx
+ bs.traf.actwp.nextturnlat[acidx], bs.traf.actwp.nextturnlon[acidx], \
+ bs.traf.actwp.nextturnspd[acidx], bs.traf.actwp.nextturnrad[acidx], \
+ bs.traf.actwp.nextturnidx[acidx] = acrte.getnextturnwp()
- bs.traf.actwp.lat[idx] = self.wplat[wpidx]
- bs.traf.actwp.lon[idx] = self.wplon[wpidx]
- bs.traf.actwp.flyby[idx] = self.wpflyby[wpidx]
- bs.traf.actwp.flyturn[idx] = self.wpflyturn[wpidx]
- bs.traf.actwp.turnrad[idx] = self.wpturnrad[wpidx]
- bs.traf.actwp.turnspd[idx] = self.wpturnspd[wpidx]
+ # Determine next turn waypoint data
- # Do calculation for VNAV
- self.calcfp()
- bs.traf.actwp.xtoalt[idx] = self.wpxtoalt[wpidx]
- bs.traf.actwp.nextaltco[idx] = self.wptoalt[wpidx]
+ # Do calculation for VNAV
+ acrte.calcfp()
- bs.traf.actwp.torta[idx] = self.wptorta[wpidx] # available for active RTA-guidance
- bs.traf.actwp.xtorta[idx] = self.wpxtorta[wpidx] # available for active RTA-guidance
+ bs.traf.actwp.xtoalt[acidx] = acrte.wpxtoalt[wpidx]
+ bs.traf.actwp.nextaltco[acidx] = acrte.wptoalt[wpidx]
- #VNAV calculations like V/S and speed for RTA
- bs.traf.ap.ComputeVNAV(idx, self.wptoalt[wpidx], self.wpxtoalt[wpidx],\
- self.wptorta[wpidx],self.wpxtorta[wpidx])
+ bs.traf.actwp.torta[acidx] = acrte.wptorta[wpidx] # available for active RTA-guidance
+ bs.traf.actwp.xtorta[acidx] = acrte.wpxtorta[wpidx] # available for active RTA-guidance
- # If there is a speed specified, process it
- if self.wpspd[wpidx]>0.:
- # Set target speed for autopilot
+ #VNAV calculations like V/S and speed for RTA
+ bs.traf.ap.ComputeVNAV(acidx, acrte.wptoalt[wpidx], acrte.wpxtoalt[wpidx],\
+ acrte.wptorta[wpidx],acrte.wpxtorta[wpidx])
- if self.wpalt[wpidx] < 0.0:
- alt = bs.traf.alt[idx]
- else:
- alt = self.wpalt[wpidx]
+ # If there is a speed specified, process it
+ if acrte.wpspd[wpidx]>0.:
+ # Set target speed for autopilot
- # Check for valid Mach or CAS
- if self.wpspd[wpidx] <2.0:
- cas = mach2cas(self.wpspd[wpidx], alt)
- else:
- cas = self.wpspd[wpidx]
-
- # Save it for next leg
- bs.traf.actwp.nextspd[idx] = cas
-
- # No speed specified for next leg
+ if acrte.wpalt[wpidx] < 0.0:
+ alt = bs.traf.alt[acidx]
else:
- bs.traf.actwp.nextspd[idx] = -999.
+ alt = acrte.wpalt[wpidx]
+ # Check for valid Mach or CAS
+ if acrte.wpspd[wpidx] <2.0:
+ cas = mach2cas(acrte.wpspd[wpidx], alt)
+ else:
+ cas = acrte.wpspd[wpidx]
- qdr, dist = geo.qdrdist(bs.traf.lat[idx], bs.traf.lon[idx],
- bs.traf.actwp.lat[idx], bs.traf.actwp.lon[idx])
+ # Save it for next leg
+ bs.traf.actwp.nextspd[acidx] = cas
- if self.wpflyturn[wpidx] or self.wpturnrad[wpidx]<0.:
- turnrad = self.wpturnrad[wpidx]
- else:
- turnrad = bs.traf.tas[idx]*bs.traf.tas[idx]/tan(radians(25.)) / g0 / nm # [nm]default bank angle 25 deg
+ # No speed specified for next leg
+ else:
+ bs.traf.actwp.nextspd[acidx] = -999.
- bs.traf.actwp.turndist[idx] = (bs.traf.actwp.flyby[idx] > 0.5) * \
- turnrad*abs(tan(0.5*radians(max(5., abs(degto180(qdr -
- self.wpdirfrom[self.iactwp])))))) # [nm]
+ qdr,dist = geo.qdrdist(bs.traf.lat[acidx], bs.traf.lon[acidx],
+ bs.traf.actwp.lat[acidx], bs.traf.actwp.lon[acidx])
+ # Save leg length & direction in actwp data
+ bs.traf.actwp.curlegdir[acidx] = qdr #[deg]
+ bs.traf.actwp.curleglen[acidx] = dist*nm #[m]
- bs.traf.swlnav[idx] = True
- return True
+ if acrte.wpflyturn[wpidx] or acrte.wpturnrad[wpidx]<0.:
+ turnrad = acrte.wpturnrad[wpidx]
else:
- return False, "Waypoint " + wpnam + " not found"
+ turnrad = bs.traf.tas[acidx]*bs.traf.tas[acidx]/tan(radians(25.)) / g0 / nm # [nm]default bank angle 25 deg
- def SetRTA(self, idx, name, txt): # all arguments of setRTA
- """SetRTA acid, wpname, time: add RTA to waypoint record"""
- timeinsec = txt2tim(txt)
- #print(timeinsec)
- if name in self.wpname:
- wpidx = self.wpname.index(name)
- self.wprta[wpidx] = timeinsec
- #print("Ik heb",self.wprta[wpidx],"op",self.wpname[wpidx],"gezet!")
+ bs.traf.actwp.turndist[acidx] = (bs.traf.actwp.flyby[acidx] > 0.5) * \
+ turnrad*abs(tan(0.5*radians(max(5., abs(degto180(qdr -
+ acrte.wpdirfrom[acrte.iactwp])))))) # [nm]
- # Recompute route and update actwp because of RTA addition
- self.direct(idx, self.wpname[self.iactwp])
+ bs.traf.swlnav[acidx] = True
return True
- def listrte(self, idx, ipage=0):
- """LISTRTE command: output route to screen"""
- if self.nwp <= 0:
- return False, "Aircraft has no route."
+ @stack.command(name='RTA')
+ @staticmethod
+ def SetRTA(acidx: 'acid', wpname: 'wpinroute', time: 'time'): # all arguments of setRTA
+ """ RTA acid, wpname, time
+
+ Add RTA to waypoint record"""
+ acid = bs.traf.id[acidx]
+ acrte = Route._routes.get(acid)
+ wpidx = acrte.wpname.index(wpname)
+ acrte.wprta[wpidx] = time
+
+ # Recompute route and update actwp because of RTA addition
+ acrte.direct(acidx, acrte.wpname[acrte.iactwp])
+
+ return True
- if idx<0:
- return False, "Aircraft id not found."
+ @stack.command
+ @staticmethod
+ def listrte(acidx: 'acid', ipage=0):
+ """ LISTRTE acid, [pagenr]
+
+ Show list of route in window per page of 5 waypoints/"""
+ # First get the appropriate ac route
+ acid = bs.traf.id[acidx]
+ acrte = Route._routes.get(acid)
+ if acrte.nwp <= 0:
+ return False, "Aircraft has no route."
for i in range(ipage * 7, ipage * 7 + 7):
- if 0 <= i < self.nwp:
+ if 0 <= i < acrte.nwp:
# Name
- if i == self.iactwp:
- txt = "*" + self.wpname[i] + " : "
+ if i == acrte.iactwp:
+ txt = "*" + acrte.wpname[i] + " : "
else:
- txt = " " + self.wpname[i] + " : "
+ txt = " " + acrte.wpname[i] + " : "
# Altitude
- if self.wpalt[i] < 0:
+ if acrte.wpalt[i] < 0:
txt += "-----/"
- elif self.wpalt[i] > 4500 * ft:
- fl = int(round((self.wpalt[i] / (100. * ft))))
+ elif acrte.wpalt[i] > 4500 * ft:
+ fl = int(round((acrte.wpalt[i] / (100. * ft))))
txt += "FL" + str(fl) + "/"
else:
- txt += str(int(round(self.wpalt[i] / ft))) + "/"
+ txt += str(int(round(acrte.wpalt[i] / ft))) + "/"
# Speed
- if self.wpspd[i] < 0.:
+ if acrte.wpspd[i] < 0.:
txt += "---"
- elif self.wpspd[i] > 2.0:
- txt += str(int(round(self.wpspd[i] / kts)))
+ elif acrte.wpspd[i] > 2.0:
+ txt += str(int(round(acrte.wpspd[i] / kts)))
else:
- txt += "M" + str(self.wpspd[i])
+ txt += "M" + str(acrte.wpspd[i])
# Type: orig, dest, C = flyby, | = flyover, U = flyturn
- if self.wptype[i] == Route.orig:
+ if acrte.wptype[i] == Route.orig:
txt += "[orig]"
- elif self.wptype[i] == Route.dest:
+ elif acrte.wptype[i] == Route.dest:
txt += "[dest]"
- elif self.wpflyturn[i]:
+ elif acrte.wpflyturn[i]:
txt += "[U]"
- elif self.wpflyby[i]:
+ elif acrte.wpflyby[i]:
txt += "[C]"
else: # FLYOVER
txt += "[|]"
@@ -887,9 +1002,27 @@ def listrte(self, idx, ipage=0):
bs.scr.echo(txt)
# Add command for next page to screen command line
- npages = int((self.nwp + 6) / 7)
+ npages = int((acrte.nwp + 6) / 7)
if ipage + 1 < npages:
- bs.scr.cmdline("LISTRTE " + bs.traf.id[idx] + "," + str(ipage + 1))
+ bs.scr.cmdline("LISTRTE " + acid + "," + str(ipage + 1))
+
+ def getnextturnwp(self):
+ """Give the next turn waypoint data."""
+ # Starting point
+ wpidx = self.iactwp
+ # Find next turn waypoint index
+ turnidx_all = where(self.wpflyturn)[0]
+ argwhere_arr = argwhere(turnidx_all>=wpidx)
+ if argwhere_arr.size == 0:
+ # No turn waypoints, return default values
+ return [0., 0., -999., -999., -999.]
+
+ trnidx = turnidx_all[argwhere(turnidx_all>=wpidx)[0]][0]
+
+
+
+ # Return the next turn waypoint info
+ return [self.wplat[trnidx], self.wplon[trnidx], self.wpturnspd[trnidx], self.wpturnrad[trnidx], trnidx]
def getnextwp(self):
"""Go to next waypoint and return data"""
@@ -910,13 +1043,9 @@ def getnextwp(self):
# Change RW06,RWY18C,RWY24001 to resp. 06,18C,24
if "RWY" in name:
rwykey = name[8:10]
- if not name[10].isdigit():
- rwykey = rwykey+name[10]
# also if it is only RW
else:
rwykey = name[7:9]
- if not name[9].isdigit():
- rwykey = rwykey+name[9]
wphdg = bs.navdb.rwythresholds[name[:4]][rwykey][2]
@@ -982,61 +1111,69 @@ def runactwpstack(self):
# stack.stack("ECHO "+self.acid+" AT "+self.wpname[self.iactwp]+" command issued:"+cmdline)
return
- def delrte(self,iac=None):
- """Delete complete route"""
+ @stack.command(aliases=("DELROUTE",))
+ @staticmethod
+ def delrte(acidx: 'acid' = None):
+ """ DELRTE acid
+ Delete for this a/c the complete route/dest/orig (FMS)."""
+ if acidx is None:
+ if bs.traf.ntraf == 0:
+ return False, 'No aircraft in simulation'
+ if bs.traf.ntraf > 1:
+ return False, 'Specify callsign of aircraft to delete route of'
+ acidx = 0
# Simple re-initialize this route as empty
- self.__init__(bs.traf.id[iac])
+ acid = bs.traf.id[acidx]
+ acrte = Route._routes.get(acid)
+ acrte.__init__(acid)
# Also disable LNAV,VNAV if route is deleted
- if self.nwp == 0 and (iac or iac == 0):
- bs.traf.swlnav[iac] = False
- bs.traf.swvnav[iac] = False
- bs.traf.swvnavspd[iac] = False
+ bs.traf.swlnav[acidx] = False
+ bs.traf.swvnav[acidx] = False
+ bs.traf.swvnavspd[acidx] = False
return True
- def delwpt(self,delwpname,iac=None):
- """Delete waypoint"""
-
+ @stack.command(aliases=("DELWP",))
+ @staticmethod
+ def delwpt(acidx: 'acid', wpname: 'wpinroute'):
+ """DELWPT acid,wpname
+
+ Delete a waypoint from a route (FMS). """
# Delete complete route?
- if delwpname =="*":
- return self.delrte(iac)
+ if wpname == "*":
+ return Route.delrte(acidx)
# Look up waypoint
- idx = -1
- i = len(self.wpname)
- while idx == -1 and i > 0:
- i -= 1
- if self.wpname[i].upper() == delwpname.upper():
- idx = i
-
+ acid = bs.traf.id[acidx]
+ acrte = Route._routes.get(acid)
+ try:
+ wpidx = acrte.wpname.index(wpname.upper())
+ except ValueError:
+ return False, "Waypoint " + wpname + " not found"
# check if active way point is the one being deleted and that it is not the last wpt.
# If active wpt is deleted then change path of aircraft
- if self.iactwp == idx and not idx == self.nwp - 1:
- self.direct(iac, self.wpname[idx + 1])
-
- # Delete waypoint
- if idx == -1:
- return False, "Waypoint " + delwpname + " not found"
-
- self.nwp =self.nwp - 1
- del self.wpname[idx]
- del self.wplat[idx]
- del self.wplon[idx]
- del self.wpalt[idx]
- del self.wpspd[idx]
- del self.wprta[idx]
- del self.wptype[idx]
- if self.iactwp > idx:
- self.iactwp = max(0, self.iactwp - 1)
-
- self.iactwp = min(self.iactwp, self.nwp - 1)
+ if acrte.iactwp == wpidx and not wpidx == acrte.nwp - 1:
+ acrte.direct(acidx, acrte.wpname[wpidx + 1])
+
+ acrte.nwp =acrte.nwp - 1
+ del acrte.wpname[wpidx]
+ del acrte.wplat[wpidx]
+ del acrte.wplon[wpidx]
+ del acrte.wpalt[wpidx]
+ del acrte.wpspd[wpidx]
+ del acrte.wprta[wpidx]
+ del acrte.wptype[wpidx]
+ if acrte.iactwp > wpidx:
+ acrte.iactwp = max(0, acrte.iactwp - 1)
+
+ acrte.iactwp = min(acrte.iactwp, acrte.nwp - 1)
# If no waypoints left, make sure to disable LNAV/VNAV
- if self.nwp==0 and (iac or iac==0):
- bs.traf.swlnav[iac] = False
- bs.traf.swvnav[iac] = False
- bs.traf.swvnavspd[iac] = False
+ if acrte.nwp==0 and (acidx or acidx==0):
+ bs.traf.swlnav[acidx] = False
+ bs.traf.swvnav[acidx] = False
+ bs.traf.swvnavspd[acidx] = False
return True
@@ -1325,7 +1462,7 @@ def findact(self,i):
# we only turn to the first waypoint if we can reach the required
# heading before reaching the waypoint
- time_turn = max(0.01,bs.traf.tas[i])*radians(delhdg)/(g0*tan(bs.traf.bank[i]))
+ time_turn = max(0.01,bs.traf.tas[i])*radians(delhdg)/(g0*tan(bs.traf.ap.bankdef[i]))
time_straight= sqrt(dist2[iwpnear])*60.*nm/max(0.01,bs.traf.tas[i])
if time_turn > time_straight:
@@ -1333,8 +1470,15 @@ def findact(self,i):
return iwpnear
- def dumpRoute(self, idx):
- acid = bs.traf.id[idx]
+ @stack.command
+ @staticmethod
+ def dumprte(acidx: 'acid'):
+ """ DUMPRTE acid
+
+ Write route to output/routelog.txt.
+ """
+ acid = bs.traf.id[acidx]
+ acrte = Route._routes.get(acid)
# Open file in append mode, write header
with open(path.join(bs.settings.log_path, 'routelog.txt'), "a") as f:
f.write("\nRoute "+acid+":\n")
@@ -1342,11 +1486,11 @@ def dumpRoute(self, idx):
f.write("type: 0=latlon 1=navdb 2=orig 3=dest 4=calwp\n")
# write flight plan VNAV data (Lateral is visible on screen)
- for j in range(self.nwp):
- f.write( str(( j, self.wpname[j], self.wptype[j],
- round(self.wplat[j], 4), round(self.wplon[j], 4),
- int(0.5+self.wpalt[j]/ft), int(0.5+self.wpspd[j]/kts),
- int(0.5+self.wptoalt[j]/ft), round(self.wpxtoalt[j]/nm, 3)
+ for j in range(acrte.nwp):
+ f.write( str(( j, acrte.wpname[j], acrte.wptype[j],
+ round(acrte.wplat[j], 4), round(acrte.wplon[j], 4),
+ int(0.5+acrte.wpalt[j]/ft), int(0.5+acrte.wpspd[j]/kts),
+ int(0.5+acrte.wptoalt[j]/ft), round(acrte.wpxtoalt[j]/nm, 3)
)) + "\n")
# End of data
diff --git a/bluesky/traffic/traffic.py b/bluesky/traffic/traffic.py
index 275889d7c2..8837b04a4d 100644
--- a/bluesky/traffic/traffic.py
+++ b/bluesky/traffic/traffic.py
@@ -81,6 +81,9 @@ def __init__(self):
self.turbulence = Turbulence()
self.translvl = 5000.*ft # [m] Default transition level
+ # Default commands issued for an aircraft after creation
+ self.crecmdlist = []
+
with self.settrafarrays():
# Aircraft Info
self.id = [] # identifier (string)
@@ -103,6 +106,9 @@ def __init__(self):
self.M = np.array([]) # mach number
self.vs = np.array([]) # vertical speed [m/s]
+ # Acceleration
+ self.ax = np.array([]) # [m/s2] current longitudinal acceleration
+
# Atmosphere
self.p = np.array([]) # air pressure [N/m2]
self.rho = np.array([]) # air density [kg/m3]
@@ -138,10 +144,6 @@ def __init__(self):
self.groups = TrafficGroups()
# Traffic autopilot data
- self.apvsdef = np.array([]) # [m/s]default vertical speed of autopilot
- self.aphi = np.array([]) # [rad] bank angle setting of autopilot
- self.ax = np.array([]) # [m/s2] absolute value of longitudinal accelleration
- self.bank = np.array([]) # nominal bank angle, [radians]
self.swhdgsel = np.array([], dtype=np.bool) # determines whether aircraft is turning
# Traffic autothrottle settings
@@ -228,7 +230,7 @@ def cre(self, acid, actype="B744", aclat=52., aclon=4., achdg=None, acalt=0, acs
aclon[aclon > 180.0] -= 360.0
aclon[aclon < -180.0] += 360.0
- achdg = refdata.hdg if achdg is None else achdg
+ achdg = (refdata.hdg or 0.0) if achdg is None else achdg
# Aircraft Info
self.id[-n:] = acid
@@ -265,13 +267,6 @@ def cre(self, acid, actype="B744", aclat=52., aclon=4., achdg=None, acalt=0, acs
self.windnorth[-n:] = 0.0
self.windeast[-n:] = 0.0
- # Traffic performance data
- #(temporarily default values)
- self.apvsdef[-n:] = 1500. * fpm # default vertical speed of autopilot
- self.aphi[-n:] = 0. # bank angle output of autopilot (optional)
- self.ax[-n:] = kts # absolute value of longitudinal accelleration
- self.bank[-n:] = np.radians(25.)
-
# Traffic autopilot settings
self.selspd[-n:] = self.cas[-n:]
self.aptas[-n:] = self.tas[-n:]
@@ -300,6 +295,13 @@ def cre(self, acid, actype="B744", aclat=52., aclon=4., achdg=None, acalt=0, acs
# So insert a dummy command to record the line
savecmd("---",line)
+ # Check for crecmdlist: contains commands to be issued for this a/c
+ # If any are there, then stack them for all aircraft
+ for j in range(self.ntraf - n, self.ntraf):
+ for cmdtxt in self.crecmdlist:
+ bs.stack.stack(self.id[j]+" "+cmdtxt)
+
+
def creconfs(self, acid, actype, targetidx, dpsi, dcpa, tlosh, dH=None, tlosv=None, spd=None):
''' Create an aircraft in conflict with target aircraft.
@@ -435,17 +437,15 @@ def update_asas(self):
def update_airspeed(self):
# Compute horizontal acceleration
delta_spd = self.aporasas.tas - self.tas
- ax = self.perf.acceleration()
- need_ax = np.abs(delta_spd) > np.abs(bs.sim.simdt * ax)
- self.ax = need_ax * np.sign(delta_spd) * ax
+ need_ax = np.abs(delta_spd) > np.abs(bs.sim.simdt * self.perf.axmax)
+ self.ax = need_ax * np.sign(delta_spd) * self.perf.axmax
# Update velocities
self.tas = np.where(need_ax, self.tas + self.ax * bs.sim.simdt, self.aporasas.tas)
self.cas = vtas2cas(self.tas, self.alt)
self.M = vtas2mach(self.tas, self.alt)
# Turning
-
- turnrate = np.degrees(g0 * np.tan(np.where(self.aphi>self.eps,self.aphi,self.bank) \
+ turnrate = np.degrees(g0 * np.tan(np.where(self.ap.turnphi>self.eps,self.ap.turnphi,self.ap.bankdef) \
/ np.maximum(self.tas, self.eps)))
delhdg = (self.aporasas.hdg - self.hdg + 180) % 360 - 180 # [deg]
self.swhdgsel = np.abs(delhdg) > np.abs(bs.sim.simdt * turnrate)
@@ -556,11 +556,6 @@ def move(self, idx, lat, lon, alt=None, hdg=None, casmach=None, vspd=None):
self.vs[idx] = vspd
self.swvnav[idx] = False
-
- def nom(self, idx):
- """ Reset acceleration back to nominal (1 kt/s^2): NOM acid """
- self.ax[idx] = kts #[m/s2]
-
def poscommand(self, idxorwp):# Show info on aircraft(int) or waypoint or airport (str)
"""POS command: Show info or an aircraft, airport, waypoint or navaid"""
# Aircraft index
@@ -774,9 +769,9 @@ def settrans(self, alt=-999.):
def setbanklim(self, idx, bankangle=None):
''' Set bank limit for given aircraft. '''
if bankangle:
- self.bank[idx] = np.radians(bankangle) # [rad]
+ self.ap.bankdef[idx] = np.radians(bankangle) # [rad]
return True
- return True, f"Banklimit of {self.id[idx]} is {int(np.degrees(self.bank[idx]))} deg"
+ return True, f"Banklimit of {self.id[idx]} is {int(np.degrees(self.ap.bankdef[idx]))} deg"
def setthrottle(self,idx,throttle=""):
"""Set throttle to given value or AUTO, meaning autothrottle on (default)"""
@@ -817,3 +812,35 @@ def setthrottle(self,idx,throttle=""):
if self.swats[idx]:
return True,"ATS of "+self.id[idx]+" is ON"
return True, "ATS of " + self.id[idx] + " is OFF. THR is "+str(self.thr[idx])
+
+ def crecmd(self,cmdline):
+ """CRECMD command: list of commands to be issued for each aircraft after creation
+ This commands adds a command to the list of default commands.
+ """
+ # Help text need or info on current list?
+ if cmdline=="" or cmdline=="?":
+ if len(self.crecmdlist)>0:
+ allcmds = ""
+ for i,txt in enumerate(self.crecmdlist):
+ if i==0:
+ allcmds = "[acid] "+txt
+ else:
+ allcmds +="; [acid] "+txt
+ return True,"CRECMD list: "+allcmds
+ else:
+ return True,"CRECMD will add a/c specific commands to an aircraft after creation"
+ # Command to be added to list
+ else:
+ self.crecmdlist.append(cmdline)
+ return True, ""
+
+ def clrcrecmd(self):
+ """CRECMD command: list of commands to be issued for each aircraft after creation
+ This commands adds a command to the list of default commands.
+ """
+ ncrecmd = len(self.crecmdlist)
+ if ncrecmd==0:
+ return True,"CLRCRECMD deletes all commands on clears command"
+ else:
+ self.crecmdlist = []
+ return True,str("All",ncrecmd,"crecmd commands deleted.")
diff --git a/bluesky/traffic/windsim.py b/bluesky/traffic/windsim.py
index d6f8f5c311..79c3c65bf1 100644
--- a/bluesky/traffic/windsim.py
+++ b/bluesky/traffic/windsim.py
@@ -3,27 +3,40 @@
from bluesky.tools.aero import kts
from bluesky.core import Entity
+from bluesky.stack import command
from .windfield import Windfield
class WindSim(Entity, Windfield, replaceable=True):
- def add(self, *arg):
-
- lat = arg[0]
- lon = arg[1]
- winddata = arg[2:]
-
+ @command(name='WIND')
+ def add(self, lat: 'lat', lon: 'lon', *winddata: 'alt/float'):
+ """ Define a wind vector as part of the 2D or 3D wind field.
+
+ Arguments:
+ - lat/lon: Horizonal position to define wind vector(s)
+ - winddata:
+ - If the wind at this location is independent of altitude
+ winddata has two elements:
+ - direction [degrees]
+ - speed (magnitude) [knots]
+ - If the wind varies with altitude winddata has three elements:
+ - altitude [ft]
+ - direction [degrees]
+ - speed (magnitude) [knots]
+ In this case, repeating combinations of alt/dir/spd can be provided
+ to specify wind at multiple altitudes.
+ """
ndata = len(winddata)
# No altitude or just one: same wind for all altitudes at this position
- if ndata == 3 or (ndata == 4 and winddata[0] is None): # only one point, ignore altitude
- if winddata[1] is None or winddata[2] is None:
+ if ndata == 2 or (ndata == 3 and winddata[0] is None): # only one point, ignore altitude
+ if winddata[-2] is None or winddata[-1] is None:
return False, "Wind direction and speed needed."
- self.addpoint(lat,lon,winddata[1],winddata[2]*kts)
+ self.addpoint(lat,lon,winddata[-2],winddata[-1]*kts)
# More than one altitude is given
- elif ndata > 3:
+ elif ndata >= 3:
windarr = array(winddata)
dirarr = windarr[1::3]
spdarr = windarr[2::3] * kts
@@ -39,8 +52,14 @@ def add(self, *arg):
return True
- def get(self, lat, lon, alt=None):
- """ Get wind vector at gioven position (and optioanlly altitude)"""
+ @command(name='GETWIND')
+ def get(self, lat: 'lat', lon: 'lon', alt: 'alt'=None):
+ """ Get wind at a specified position (and optionally at altitude)
+
+ Arguments:
+ - lat, lon: Horizontal position where wind should be determined [deg]
+ - alt: Altitude at which wind should be determined [ft]
+ """
vn,ve = self.getdata(lat,lon,alt)
wdir = (degrees(arctan2(ve,vn)) + 180.) % 360.
diff --git a/bluesky/ui/qtgl/console.py b/bluesky/ui/qtgl/console.py
index 4061294abc..2eb72634d2 100644
--- a/bluesky/ui/qtgl/console.py
+++ b/bluesky/ui/qtgl/console.py
@@ -1,12 +1,15 @@
""" Console interface for the QTGL implementation."""
from PyQt5.QtCore import Qt
+from PyQt5.Qt import QDesktopServices, QUrl, QApplication
from PyQt5.QtWidgets import QWidget, QTextEdit
import bluesky as bs
+from bluesky.tools import cachefile
from bluesky.tools.misc import cmdsplit
from bluesky.core.signal import Signal
from . import autocomplete
+
cmdline_stacked = Signal('cmdline_stacked')
@@ -31,10 +34,14 @@ def get_args():
return Console._instance.args
-def append_cmdline(text):
+def process_cmdline(cmdlines):
assert Console._instance is not None, 'No console created yet: can only change' + \
' command line after main window is created.'
- Console._instance.append_cmdline(text)
+ lines = cmdlines.split('\n')
+ if lines:
+ Console._instance.append_cmdline(lines[-1])
+ for cmd in lines[:-1]:
+ Console._instance.stack(cmd)
class Console(QWidget):
@@ -44,7 +51,11 @@ class Console(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
- self.command_history = []
+ with cachefile.openfile('console_history.p') as cache:
+ try:
+ self.command_history = cache.load()
+ except:
+ self.command_history = []
self.cmd = ''
self.args = []
self.history_pos = 0
@@ -59,6 +70,14 @@ def __init__(self, parent=None):
"already exists! Cannot have more than one console."
Console._instance = self
+ # Connect function to save command history on quit
+ QApplication.instance().aboutToQuit.connect(self.close)
+
+ def close(self):
+ ''' Save command history when BlueSky closes. '''
+ with cachefile.openfile('console_history.p') as cache:
+ cache.dump(self.command_history)
+
def on_simevent_received(self, eventname, eventdata, sender_id):
''' Processing of events from simulation nodes. '''
if eventname == b'CMDLINE':
@@ -66,7 +85,8 @@ def on_simevent_received(self, eventname, eventdata, sender_id):
def actnodedataChanged(self, nodeid, nodedata, changed_elems):
if 'ECHOTEXT' in changed_elems:
- self.stackText.setPlainText(nodedata.echo_text)
+ # self.stackText.setPlainText(nodedata.echo_text)
+ self.stackText.setHtml(nodedata.echo_text.replace('\n', '
') + '
')
self.stackText.verticalScrollBar().setValue(
self.stackText.verticalScrollBar().maximum())
@@ -84,7 +104,8 @@ def stack(self, text):
def echo(self, text):
actdata = bs.net.get_nodedata()
actdata.echo(text)
- self.stackText.append(text)
+ # self.stackText.append(text)
+ self.stackText.insertHtml(text.replace('\n', '
') + '
')
self.stackText.verticalScrollBar().setValue(
self.stackText.verticalScrollBar().maximum())
@@ -235,3 +256,14 @@ def __init__(self, parent=None):
super().__init__(parent)
Console.stackText = self
self.setFocusPolicy(Qt.NoFocus)
+
+ def mousePressEvent(self, e):
+ self.anchor = self.anchorAt(e.pos())
+ if self.anchor:
+ QApplication.setOverrideCursor(Qt.PointingHandCursor)
+
+ def mouseReleaseEvent(self, e):
+ if self.anchor:
+ QDesktopServices.openUrl(QUrl(self.anchor))
+ QApplication.setOverrideCursor(Qt.ArrowCursor)
+ self.anchor = None
diff --git a/bluesky/ui/qtgl/guiclient.py b/bluesky/ui/qtgl/guiclient.py
index 943b2c8713..2029ff9a88 100644
--- a/bluesky/ui/qtgl/guiclient.py
+++ b/bluesky/ui/qtgl/guiclient.py
@@ -10,7 +10,7 @@
from bluesky.tools.aero import ft
# Globals
-UPDATE_ALL = ['SHAPE', 'TRAILS', 'CUSTWPT', 'PANZOOM', 'ECHOTEXT']
+UPDATE_ALL = ['SHAPE', 'TRAILS', 'CUSTWPT', 'PANZOOM', 'ECHOTEXT', 'ROUTEDATA']
ACTNODE_TOPICS = [b'ACDATA', b'PLOT*', b'ROUTEDATA*']
@@ -170,6 +170,10 @@ def clear_scen_data(self):
self.custwplat = np.array([], dtype=np.float32)
self.custwplon = np.array([], dtype=np.float32)
+ self.naircraft = 0
+ self.acdata = ACDataEvent()
+ self.routedata = RouteDataEvent()
+
# Filteralt settings
self.filteralt = False
diff --git a/bluesky/ui/qtgl/radarwidget.py b/bluesky/ui/qtgl/radarwidget.py
index f6db701672..096fdf8895 100644
--- a/bluesky/ui/qtgl/radarwidget.py
+++ b/bluesky/ui/qtgl/radarwidget.py
@@ -326,10 +326,8 @@ def event(self, event):
actdata = bs.net.get_nodedata()
tostack, tocmdline = radarclick(console.get_cmdline(), lat, lon,
actdata.acdata, actdata.routedata)
- if '\n' not in tocmdline:
- console.append_cmdline(tocmdline)
- if tostack:
- bs.stack.stack(tostack)
+
+ console.process_cmdline((tostack + '\n' + tocmdline) if tostack else tocmdline)
elif event.type() == QEvent.MouseMove:
self.mousedragged = True
diff --git a/bluesky/ui/qtgl/tiledtexture.py b/bluesky/ui/qtgl/tiledtexture.py
index 15e5fb106a..6a779f3491 100644
--- a/bluesky/ui/qtgl/tiledtexture.py
+++ b/bluesky/ui/qtgl/tiledtexture.py
@@ -1,4 +1,8 @@
''' Tile texture manager for BlueSky Qt/OpenGL gui. '''
+import traceback
+import math
+from os import makedirs, path
+import weakref
from collections import OrderedDict
from os import makedirs, path
from urllib.error import URLError
@@ -6,7 +10,8 @@
import math
import weakref
-from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal
+from PyQt5.Qt import Qt
+from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QImage
from bluesky.core import Signal
@@ -73,7 +78,8 @@ def __init__( self, source, zoom, tilex, tiley, idxx, idxy ):
fout.write( data )
break
except URLError as e:
- print( e, f'({url.format(zoom=zoom, x=tilex, y=tiley)})' )
+ print(f'Error loading {url.format(zoom=zoom, x=tilex, y=tiley)}:')
+ print(traceback.format_exc())
class TileLoader( QRunnable ):
@@ -111,17 +117,32 @@ def __call__( cls, *args, **kwargs ):
class TiledTexture( glh.Texture, metaclass=TiledTextureMeta ):
''' Tiled texture implementation for the BlueSky GL gui. '''
- def __init__( self, glsurface, tilesource='opentopomap' ):
- super().__init__( target=glh.Texture.Target2DArray )
+ class SlotHolder(QObject):
+ ''' Wrapper class for Qt slot, which can only be owned by a
+ QObject-derived parent. We need slots to allow signal receiver
+ to be executed in the receiving (main) thread. '''
+ def __init__(self, callback):
+ super().__init__()
+ self.cb = callback
+
+ @pyqtSlot(Tile)
+ def slot(self, *args, **kwargs):
+ self.cb(*args, **kwargs)
+
+ def __init__(self, glsurface, tilesource='opentopomap'):
+ super().__init__(target=glh.Texture.Target2DArray)
self.threadpool = QThreadPool()
tileinfo = bs.settings.tile_sources.get( tilesource )
if not tileinfo:
- raise KeyError( f'Tile source {tilesource} not found!' )
- max_dl = tileinfo.get( 'max_download_workers', bs.settings.max_download_workers )
- self.maxzoom = tileinfo.get( 'max_tile_zoom', bs.settings.max_tile_zoom )
- self.threadpool.setMaxThreadCount( min( bs.settings.max_download_workers, max_dl ) )
+ raise KeyError(f'Tile source {tilesource} not found!')
+ max_dl = tileinfo.get('max_download_workers', bs.settings.max_download_workers)
+ self.maxzoom = tileinfo.get('max_tile_zoom', bs.settings.max_tile_zoom)
+ self.threadpool.setMaxThreadCount(min(bs.settings.max_download_workers, max_dl))
+ self.tileslot = TiledTexture.SlotHolder(self.load_tile)
self.tilesource = tilesource
- self.tilesize = ( 256, 256 )
+ self.tilesize = (256, 256)
+ self.curtileext = (0, 0, 0, 0)
+ self.curtilezoom = 1
self.curtiles = OrderedDict()
self.fullscreen = False
self.offsetscale = np.array( [0, 0, 1], dtype=np.float32 )
@@ -133,8 +154,7 @@ def __init__( self, glsurface, tilesource='opentopomap' ):
bs.net.actnodedata_changed.connect( self.actdata_changed )
Signal( 'panzoom' ).connect( self.on_panzoom_changed )
-
- def add_bounding_box( self, lat0, lon0, lat1, lon1 ):
+ def add_bounding_box(self, lat0, lon0, lat1, lon1):
''' Add the bounding box of a textured shape.
These bounding boxes are used to determine if tiles need to be
@@ -219,25 +239,26 @@ def on_panzoom_changed( self, finished=False ):
# Estimated width in longitude of one tile
est_tilewidth = abs( viewport[3] - viewport[1] ) / ntiles_hor
- zoom = tilezoom( est_tilewidth, self.maxzoom )
+ self.curtilezoom = tilezoom(est_tilewidth, self.maxzoom)
# With the tile zoom level get the required number of tiles
# Top-left and bottom-right tiles:
- x0, y0 = latlon2tilenum( *viewport[:2], zoom )
- x1, y1 = latlon2tilenum( *viewport[2:], zoom )
- nx = abs( x1 - x0 ) + 1
- ny = abs( y1 - y0 ) + 1
+ x0, y0 = latlon2tilenum(*viewport[:2], self.curtilezoom)
+ x1, y1 = latlon2tilenum(*viewport[2:], self.curtilezoom)
+ nx = abs(x1 - x0) + 1
+ ny = abs(y1 - y0) + 1
+ self.curtileext = (x0, y0, x1, y1)
# Calculate the offset of the top-left tile w.r.t. the screen top-left corner
- tile0_topleft = np.array( tilenum2latlon( x0, y0, zoom ) )
- tile0_bottomright = np.array( tilenum2latlon( x0 + 1, y0 + 1, zoom ) )
- tilesize_latlon0 = np.abs( tile0_bottomright - tile0_topleft )
+ tile0_topleft = np.array(tilenum2latlon(x0, y0, self.curtilezoom))
+ tile0_bottomright = np.array(tilenum2latlon(x0 + 1, y0 + 1, self.curtilezoom))
+ tilesize_latlon0 = np.abs(tile0_bottomright - tile0_topleft)
offset_latlon0 = viewport[:2] - tile0_topleft
tex_y0, tex_x0 = np.abs( offset_latlon0 / tilesize_latlon0 )
# Calculate the offset of the bottom-right tile w.r.t. the screen bottom right corner
- tile1_topleft = np.array( tilenum2latlon( x1, y1, zoom ) )
- tile1_bottomright = np.array( tilenum2latlon( x1 + 1, y1 + 1, zoom ) )
- tilesize_latlon1 = np.abs( tile1_bottomright - tile1_topleft )
+ tile1_topleft = np.array(tilenum2latlon(x1, y1, self.curtilezoom))
+ tile1_bottomright = np.array(tilenum2latlon(x1 + 1, y1 + 1, self.curtilezoom))
+ tilesize_latlon1 = np.abs(tile1_bottomright - tile1_topleft)
offset_latlon1 = viewport[2:] - tile1_topleft
tex_y1, tex_x1 = np.abs( offset_latlon1 / tilesize_latlon1 ) + [ny - 1, nx - 1]
# Store global offset and scale for shader uniform
@@ -245,32 +266,33 @@ def on_panzoom_changed( self, finished=False ):
[tex_x0, tex_y0, tex_x1 - tex_x0, tex_y1 - tex_y0], dtype=np.float32 )
# Determine required tiles
index_tex = []
+
curtiles = OrderedDict()
curtiles_difzoom = OrderedDict()
for j, y in enumerate( range( y0, y1 + 1 ) ):
for i, x in enumerate( range( x0, x1 + 1 ) ):
# Find tile index if already loaded
- idx = self.curtiles.pop( ( x, y, zoom ), None )
+ idx = self.curtiles.pop((x, y, self.curtilezoom), None)
if idx is not None:
# correct zoom, so dx,dy=0, zoomfac=1
- index_tex.extend( ( 0, 0, 1, idx ) )
- curtiles[( x, y, zoom )] = idx
+ index_tex.extend((0, 0, 1, idx))
+ curtiles[(x, y, self.curtilezoom)] = idx
else:
if finished:
# Tile not loaded yet, fetch in the background
- task = TileLoader( self.tilesource, zoom, x, y, i, j )
- task.signals.finished.connect( self.load_tile )
- self.threadpool.start( task )
+ task = TileLoader(self.tilesource, self.curtilezoom, x, y, i, j)
+ task.signals.finished.connect(self.tileslot.slot, Qt.QueuedConnection)
+ self.threadpool.start(task)
# In the mean time, check if more zoomed-out tiles are loaded that can be used
- for z in range( zoom - 1, max( 2, zoom - 5 ), -1 ):
- zx, zy, dx, dy = zoomout_tilenum( x, y, z - zoom )
- idx = self.curtiles.pop( ( zx, zy, z ), None )
+ for z in range(self.curtilezoom - 1, max(2, self.curtilezoom - 5), -1):
+ zx, zy, dx, dy = zoomout_tilenum(x, y, z - self.curtilezoom)
+ idx = self.curtiles.pop((zx, zy, z), None)
if idx is not None:
- curtiles_difzoom[( zx, zy, z )] = idx
- zoomfac = int( 2 ** ( zoom - z ) )
- dxi = int( round( dx * zoomfac ) )
- dyi = int( round( dy * zoomfac ) )
+ curtiles_difzoom[(zx, zy, z)] = idx
+ zoomfac = int(2 ** (self.curtilezoom - z))
+ dxi = int(round(dx * zoomfac))
+ dyi = int(round(dy * zoomfac))
# offset zoom, so possible offset dxi, dyi
index_tex.extend( ( dxi, dyi, zoomfac, idx ) )
break
@@ -281,7 +303,8 @@ def on_panzoom_changed( self, finished=False ):
curtiles.update( curtiles_difzoom )
curtiles.update( self.curtiles )
self.curtiles = curtiles
- data = np.array( index_tex, dtype=np.int32 )
+
+ data = np.array(index_tex, dtype=np.int32)
self.glsurface.makeCurrent()
self.indextexture.bind( 2 )
glh.gl.glTexSubImage2D_alt( glh.Texture.Target2D, 0, 0, 0, nx, ny,
@@ -294,24 +317,46 @@ def load_tile( self, tile ):
This function is called on callback from the
asynchronous image load function.
'''
- layer = len( self.curtiles )
- if layer >= bs.settings.tile_array_size:
- # we're exceeding the size of the GL texture array. Replace the least-recent tile
- _, layer = self.curtiles.popitem()
+ # First check whether tile is still relevant
+ # If there's a lot of panning/zooming, sometimes tiles are loaded that
+ # become obsolete before they are even downloaded.
+ if not ((self.curtilezoom == tile.zoom) and
+ (self.curtileext[0] <= tile.tilex <= self.curtileext[2]) and
+ (self.curtileext[1] <= tile.tiley <= self.curtileext[3])):
+ return
- self.curtiles[( tile.tilex, tile.tiley, tile.zoom )] = layer
- # Update the ordering of the tile dict: the new tile should be on top
- self.curtiles.move_to_end( ( tile.tilex, tile.tiley, tile.zoom ), last=False )
- idxdata = np.array( [0, 0, 1, layer], dtype=np.int32 )
+ # Make sure our GL context is current
self.glsurface.makeCurrent()
- self.indextexture.bind( 2 )
- glh.gl.glTexSubImage2D_alt( glh.Texture.Target2D, 0, tile.idxx, tile.idxy,
+
+ # Check if tile is already loaded. This can happen here with multiple
+ # pans/zooms shortly after each other
+ layer = self.curtiles.get((tile.tilex, tile.tiley, tile.zoom), None)
+ if layer is None:
+ # This tile is not loaded yet. Select layer to upload it to
+ layer = len(self.curtiles)
+ if layer >= bs.settings.tile_array_size:
+ # we're exceeding the size of the GL texture array.
+ # Replace the least-recent tile
+ _, layer = self.curtiles.popitem()
+
+ self.curtiles[(tile.tilex, tile.tiley, tile.zoom)] = layer
+ # Upload tile to texture array
+ super().bind(4)
+ self.setLayerData(layer, tile.image)
+ self.release()
+
+ # Update the ordering of the tile dict: the new tile should be on top
+ self.curtiles.move_to_end(
+ (tile.tilex, tile.tiley, tile.zoom), last=False)
+
+ # Update index texture
+ idxdata = np.array([0, 0, 1, layer], dtype=np.int32)
+ self.indextexture.bind(2)
+ glh.gl.glTexSubImage2D_alt(glh.Texture.Target2D, 0, tile.idxx, tile.idxy,
1, 1, glh.Texture.RGBA_Integer,
- glh.Texture.Int32, idxdata.tobytes() )
- super().bind( 4 )
- self.setLayerData( layer, tile.image )
+ glh.Texture.Int32, idxdata.tobytes())
+
self.indextexture.release()
- self.release()
def latlon2tilenum( lat_deg, lon_deg, zoom ):
diff --git a/bluesky/ui/radarclick.py b/bluesky/ui/radarclick.py
index 462260cde5..94577ed113 100644
--- a/bluesky/ui/radarclick.py
+++ b/bluesky/ui/radarclick.py
@@ -186,6 +186,7 @@ def radarclick(cmdline, lat, lon, acdata=None, route=None):
# Is it the last argument? (then we will insert ENTER as well)
if curarg + 1 >= totargs:
tostack = cmdline + todisplay
- todisplay = todisplay + '\n'
+ # todisplay = todisplay + '\n'
+ todisplay = ''
return tostack, todisplay
diff --git a/check.py b/check.py
index f818e725ac..46975e15db 100644
--- a/check.py
+++ b/check.py
@@ -130,7 +130,7 @@ def initializeGL( self ):
print( "Checking bluesky modules" )
try:
import bluesky
- bluesky.init()
+ bluesky.init(mode='client')
from bluesky.ui import *
from bluesky.stack import *
from bluesky.simulation import *
diff --git a/data/graphics/mainwindow.ui b/data/graphics/mainwindow.ui
index f645eb10df..6c9b1bf5ee 100644
--- a/data/graphics/mainwindow.ui
+++ b/data/graphics/mainwindow.ui
@@ -901,6 +901,15 @@
+
+
+
+ 0
+ 240
+ 0
+
+
+
@@ -930,6 +939,15 @@
+
+
+
+ 0
+ 240
+ 0
+
+
+
@@ -959,6 +977,15 @@
+
+
+
+ 0
+ 240
+ 0
+
+
+
@@ -981,7 +1008,7 @@
true
- Qt::NoTextInteraction
+ Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse
@@ -1026,6 +1053,15 @@
+
+
+
+ 0
+ 255
+ 0
+
+
+
@@ -1046,6 +1082,15 @@
+
+
+
+ 0
+ 255
+ 0
+
+
+
@@ -1066,6 +1111,15 @@
+
+
+
+ 0
+ 255
+ 0
+
+
+
diff --git a/bluesky/tools/geo_declination_data.csv b/data/navdata/geo_declination_data.csv
similarity index 100%
rename from bluesky/tools/geo_declination_data.csv
rename to data/navdata/geo_declination_data.csv
diff --git a/data/performance/OpenAP/fixwing/dragpolar.csv b/data/performance/OpenAP/fixwing/dragpolar.csv
index bc3672cdc6..8f1f2cdcc2 100644
--- a/data/performance/OpenAP/fixwing/dragpolar.csv
+++ b/data/performance/OpenAP/fixwing/dragpolar.csv
@@ -1,22 +1,26 @@
mdl,cd0_clean,k_clean,e_clean,cd0_to,k_to,e_to,cd0_ld,k_ld,e_ld,delta_cd_gear
-A319,0.019,0.039,0.793,0.021,0.037,0.845,0.025,0.035,0.897,0.017
-A320,0.018,0.039,0.798,0.020,0.036,0.850,0.024,0.034,0.902,0.017
-A321,0.026,0.043,0.746,0.028,0.040,0.798,0.034,0.036,0.876,0.019
-A332,0.029,0.044,0.728,0.030,0.041,0.780,0.035,0.037,0.858,0.014
-A333,0.030,0.044,0.719,0.032,0.041,0.771,0.036,0.037,0.849,0.014
-A359,0.031,0.046,0.725,0.032,0.043,0.777,0.037,0.039,0.855,0.013
-A388,0.028,0.054,0.781,0.030,0.051,0.833,0.033,0.048,0.885,0.012
-B734,0.034,0.049,0.705,0.036,0.046,0.757,0.038,0.043,0.809,0.021
-B737,0.029,0.046,0.736,0.030,0.043,0.788,0.035,0.039,0.866,0.016
-B738,0.023,0.044,0.775,0.024,0.041,0.827,0.029,0.037,0.905,0.017
-B739,0.024,0.044,0.769,0.025,0.041,0.821,0.030,0.038,0.899,0.018
-B744,0.028,0.052,0.774,0.030,0.049,0.826,0.034,0.046,0.878,0.015
-B748,0.027,0.049,0.771,0.029,0.046,0.823,0.032,0.043,0.875,0.015
-B772,0.034,0.051,0.723,0.036,0.047,0.775,0.041,0.043,0.853,0.014
-B77W,0.037,0.048,0.687,0.039,0.045,0.739,0.044,0.041,0.817,0.016
-B788,0.027,0.045,0.748,0.029,0.042,0.800,0.031,0.039,0.852,0.013
-B789,0.029,0.045,0.737,0.030,0.042,0.789,0.033,0.040,0.841,0.014
-E75L,0.019,0.043,0.803,0.020,0.040,0.855,0.025,0.037,0.933,0.017
-E190,0.019,0.044,0.813,0.020,0.041,0.865,0.025,0.038,0.943,0.016
-E195,0.028,0.048,0.752,0.029,0.045,0.804,0.034,0.041,0.882,0.017
+A319,0.020,0.039,0.783,0.021,0.037,0.835,0.026,0.035,0.887,0.017
+A320,0.018,0.039,0.799,0.019,0.036,0.851,0.023,0.034,0.903,0.017
+A20N,0.017,0.038,0.807,0.018,0.036,0.859,0.022,0.034,0.911,0.017
+A321,0.020,0.041,0.785,0.022,0.038,0.837,0.028,0.035,0.915,0.019
+A332,0.022,0.041,0.774,0.023,0.038,0.826,0.028,0.035,0.904,0.014
+A333,0.022,0.041,0.773,0.023,0.038,0.825,0.028,0.035,0.903,0.014
+A343,0.019,0.040,0.799,0.020,0.037,0.851,0.025,0.034,0.929,0.016
+A359,0.022,0.043,0.783,0.023,0.040,0.835,0.028,0.037,0.913,0.013
+A388,0.016,0.050,0.855,0.017,0.047,0.907,0.020,0.044,0.959,0.012
+B734,0.020,0.044,0.793,0.021,0.041,0.845,0.024,0.039,0.897,0.021
+B737,0.022,0.043,0.780,0.023,0.041,0.832,0.028,0.037,0.910,0.016
+B738,0.019,0.042,0.799,0.020,0.040,0.851,0.025,0.036,0.929,0.017
+B38M,0.020,0.042,0.797,0.021,0.040,0.849,0.025,0.036,0.927,0.018
+B739,0.020,0.042,0.797,0.021,0.040,0.849,0.025,0.036,0.927,0.018
+B744,0.021,0.049,0.816,0.022,0.046,0.868,0.027,0.044,0.920,0.015
+B748,0.021,0.047,0.806,0.023,0.044,0.858,0.026,0.041,0.910,0.015
+B752,0.021,0.049,0.813,0.022,0.046,0.865,0.029,0.043,0.943,0.016
+B772,0.024,0.047,0.783,0.025,0.044,0.835,0.031,0.040,0.913,0.014
+B77W,0.024,0.043,0.765,0.026,0.041,0.817,0.031,0.037,0.895,0.016
+B788,0.017,0.041,0.819,0.018,0.038,0.871,0.021,0.036,0.923,0.013
+B789,0.022,0.042,0.783,0.023,0.040,0.835,0.026,0.037,0.887,0.014
+E75L,0.018,0.042,0.809,0.019,0.040,0.861,0.024,0.036,0.939,0.017
+E190,0.018,0.044,0.817,0.019,0.041,0.869,0.024,0.038,0.947,0.016
+E195,0.021,0.045,0.795,0.022,0.042,0.847,0.027,0.039,0.925,0.017
F16,0.0175,0.109,0.908,0.018,0.109,0.908,0.018,0.109,0.908,0.000
diff --git a/data/performance/OpenAP/fixwing/wrap/a319.csv b/data/performance/OpenAP/fixwing/wrap/a319.csv
deleted file mode 100644
index 6a31d7296e..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/a319.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,82.7,72.4,93.0,norm,82.7|7.1
-TO,to_d_tof,1.53,1.05,2.40,gamma,11.88|0.23|0.12
-TO,to_acc_tof,1.84,1.38,2.29,norm,1.84|0.28
-IC,ic_va_avg,80,72,87,norm,80|5
-IC,ic_vz_avg,12.25,9.36,15.15,norm,12.26|1.76
-CL,cl_d_range,207,157,350,gamma,5|138|16
-CL,cl_v_cas_const,148,138,166,gamma,11|121|2
-CL,cl_v_mach_const,0.77,0.73,0.81,norm,0.77|0.02
-CL,cl_h_cas_const,5.0,3.0,7.1,norm,5.1|1.1
-CL,cl_h_mach_const,10.0,7.6,11.1,beta,8.9|3.1|4.0|7.6
-CL,cl_vz_avg_pre_cas,11.54,8.92,14.17,norm,11.55|1.60
-CL,cl_vz_avg_cas_const,8.67,6.02,11.33,norm,8.67|1.61
-CL,cl_vz_avg_mach_const,5.67,3.24,8.12,norm,5.68|1.48
-CR,cr_d_range,288,172,3904,gamma,1|164|651
-CR,cr_v_cas_mean,126,118,140,gamma,18|99|1
-CR,cr_v_cas_max,128,121,148,gamma,3|116|4
-CR,cr_v_mach_mean,0.77,0.74,0.80,norm,0.77|0.02
-CR,cr_v_mach_max,0.79,0.75,0.82,norm,0.79|0.02
-CR,cr_h_mean,11.5,10.1,12.0,beta,6.2|2.0|8.1|4.1
-CR,cr_h_max,11.6,10.3,12.0,beta,6.1|2.0|8.4|3.8
-DE,de_d_range,235,175,492,gamma,3|162|34
-DE,de_v_mach_const,0.76,0.70,0.80,beta,7.49|4.28|0.62|0.22
-DE,de_v_cas_const,143,134,163,gamma,6|123|3
-DE,de_h_mach_const,9.2,6.9,10.7,beta,5.9|3.2|4.1|7.6
-DE,de_h_cas_const,5.2,2.7,8.2,beta,3.7|4.3|0.8|10.0
-DE,de_vz_avg_mach_const,-5.82,-12.06,-2.41,beta,4.07|2.36|-17.36|16.65
-DE,de_vz_avg_cas_const,-9.60,-14.30,-4.88,norm,-9.59|2.87
-DE,de_vz_avg_after_cas,-5.94,-7.76,-4.10,norm,-5.93|1.11
-FA,fa_va_avg,67,63,76,gamma,7|56|1
-FA,fa_vz_avg,-3.42,-4.20,-2.64,norm,-3.42|0.47
-LD,ld_v_app,66.2,60.6,71.9,norm,66.2|3.9
-LD,ld_d_brk,1.40,0.70,4.16,gamma,2.76|0.24|0.66
-LD,ld_acc_brk,-0.79,-1.92,-0.35,beta,22.41|3.49|-7.36|7.34
diff --git a/data/performance/OpenAP/fixwing/wrap/a319.txt b/data/performance/OpenAP/fixwing/wrap/a319.txt
new file mode 100644
index 0000000000..7f5962b02c
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/a319.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 82.7 72.5 92.9 norm 82.71|7.10
+to_d_tof takeoff Takeoff distance 1.51 1.06 2.41 gamma 8.56|0.43|0.14
+to_acc_tof takeoff Mean takeoff accelaration 1.84 1.38 2.29 norm 1.84|0.28
+ic_va_avg initial_climb Mean airspeed 80 73 87 norm 80.26|4.86
+ic_vs_avg initial_climb Mean vertical rate 12.27 9.52 15.04 norm 12.28|1.68
+cl_d_range climb Climb range 206 157 348 gamma 5.16|138.89|16.28
+cl_v_cas_const climb Constant CAS 149 140 159 norm 150.02|6.06
+cl_v_mach_const climb Constant Mach 0.77 0.74 0.8 beta 16.21|6.30|0.62|0.20
+cl_h_cas_const climb Constant CAS crossover altitude 3.2 2.1 6 gamma 5.22|0.99|0.53
+cl_h_mach_const climb Constant Mach crossover altitude 8.9 8 9.8 norm 8.93|0.55
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 11.1 8.41 13.8 norm 11.11|1.64
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 10.15 7.63 12.67 norm 10.15|1.53
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 6.07 3.99 8.16 norm 6.08|1.27
+cr_d_range cruise Cruise range 299 182 4019 gamma 1.19|173.76|669.46
+cr_v_cas_mean cruise Mean cruise CAS 125 119 139 gamma 4.75|114.21|2.92
+cr_v_cas_max cruise Maximum cruise CAS 128 121 148 gamma 3.54|116.92|4.46
+cr_v_mach_mean cruise Mean cruise Mach 0.77 0.74 0.8 norm 0.77|0.02
+cr_v_mach_max cruise Maximum cruise Mach 0.79 0.75 0.82 norm 0.79|0.02
+cr_h_init cruise Initial cruise altitude 11.54 10.27 11.97 beta 7.48|2.22|8.11|4.08
+cr_h_mean cruise Mean cruise altitude 11.54 10.21 11.97 beta 7.00|2.13|8.10|4.09
+cr_h_max cruise Maximum cruise altitude 11.64 10.3 12.03 beta 6.12|1.89|8.42|3.79
+de_d_range descent Descent range 233 176 473 gamma 3.15|163.83|32.50
+de_v_mach_const descent Constant Mach 0.76 0.72 0.8 norm 0.76|0.03
+de_v_cas_const descent Constant CAS 145 136 164 gamma 6.49|126.84|3.35
+de_h_mach_const descent Constant Mach crossover altitude 9.2 7.7 10.7 norm 9.18|0.92
+de_h_cas_const descent Constant CAS crossover altitude 5.3 2.6 8.1 norm 5.34|1.66
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -5.87 -12.13 -2.41 beta 3.27|2.09|-16.27|15.37
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.84 -14.22 -5.45 norm -9.83|2.66
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -5.88 -7.69 -4.07 norm -5.88|1.10
+fa_va_avg final_approach Mean airspeed 67 63 76 gamma 6.43|57.71|1.84
+fa_vs_avg final_approach Mean vertical rate -3.42 -4.19 -2.65 norm -3.42|0.47
+fa_agl final_approach Approach angle 3.15 2.25 4.05 norm 3.15|0.55
+ld_v_app landing Touchdown speed 66.2 61 71.5 norm 66.23|3.65
+ld_d_brk landing Braking distance 1.38 0.71 4.18 gamma 2.62|0.28|0.68
+ld_acc_brk landing Mean braking acceleration -0.87 -1.79 -0.35 beta 5.62|2.87|-2.90|2.86
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/a320.csv b/data/performance/OpenAP/fixwing/wrap/a320.csv
deleted file mode 100644
index eb3f3fb014..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/a320.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,85.3,74.5,96.1,norm,85.3|7.5
-TO,to_d_tof,1.65,1.06,2.25,norm,1.65|0.36
-TO,to_acc_tof,1.93,1.49,2.37,norm,1.93|0.27
-IC,ic_va_avg,83,76,90,norm,83|4
-IC,ic_vz_avg,12.58,9.13,16.06,norm,12.59|2.11
-CL,cl_d_range,236,171,440,gamma,4|149|24
-CL,cl_v_cas_const,155,141,169,norm,155|8
-CL,cl_v_mach_const,0.78,0.70,0.81,beta,38.80|3.94|0.09|0.74
-CL,cl_h_cas_const,5.0,2.8,7.3,norm,5.0|1.1
-CL,cl_h_mach_const,9.6,7.2,11.0,beta,9.4|3.7|3.4|8.2
-CL,cl_vz_avg_pre_cas,10.32,7.72,12.93,norm,10.33|1.58
-CL,cl_vz_avg_cas_const,7.18,4.83,9.54,norm,7.18|1.43
-CL,cl_vz_avg_mach_const,5.07,3.07,7.07,norm,5.07|1.22
-CR,cr_d_range,909,443,4269,gamma,1|391|524
-CR,cr_v_cas_mean,134,124,145,norm,134|6
-CR,cr_v_cas_max,138,127,156,gamma,17|103|2
-CR,cr_v_mach_mean,0.79,0.75,0.81,beta,28.61|5.59|0.53|0.29
-CR,cr_v_mach_max,0.80,0.77,0.83,norm,0.80|0.02
-CR,cr_h_mean,10.9,9.9,11.9,norm,10.9|0.6
-CR,cr_h_max,11.0,10.1,11.9,norm,11.0|0.6
-DE,de_d_range,235,180,468,gamma,3|168|31
-DE,de_v_mach_const,0.77,0.71,0.81,beta,8.96|4.09|0.61|0.23
-DE,de_v_cas_const,146,136,167,gamma,8|122|3
-DE,de_h_mach_const,9.5,7.3,10.8,beta,7.0|3.1|3.9|7.7
-DE,de_h_cas_const,5.7,2.9,8.4,beta,3.1|3.0|1.1|8.9
-DE,de_vz_avg_mach_const,-5.86,-13.45,-2.12,beta,5.70|2.62|-22.78|22.78
-DE,de_vz_avg_cas_const,-9.78,-14.70,-4.84,norm,-9.77|3.00
-DE,de_vz_avg_after_cas,-6.09,-7.92,-4.26,norm,-6.09|1.11
-FA,fa_va_avg,72,67,77,norm,72|3
-FA,fa_vz_avg,-3.55,-4.19,-2.90,norm,-3.55|0.39
-LD,ld_v_app,69.3,62.7,76.0,norm,69.4|4.6
-LD,ld_d_brk,1.10,0.62,3.20,gamma,2.49|0.33|0.52
-LD,ld_acc_brk,-1.22,-1.98,-0.47,norm,-1.22|0.46
diff --git a/data/performance/OpenAP/fixwing/wrap/a320.txt b/data/performance/OpenAP/fixwing/wrap/a320.txt
new file mode 100644
index 0000000000..3d818154ae
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/a320.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 85.3 74.5 96 norm 85.29|7.47
+to_d_tof takeoff Takeoff distance 1.65 1.06 2.24 norm 1.65|0.36
+to_acc_tof takeoff Mean takeoff accelaration 1.93 1.5 2.37 norm 1.93|0.27
+ic_va_avg initial_climb Mean airspeed 83 76 89 norm 83.31|4.64
+ic_vs_avg initial_climb Mean vertical rate 12.59 9.15 16.04 norm 12.59|2.09
+cl_d_range climb Climb range 257 141 374 norm 258.08|45.16
+cl_v_cas_const climb Constant CAS 151 140 161 norm 151.04|6.27
+cl_v_mach_const climb Constant Mach 0.78 0.73 0.8 beta 12.39|3.68|0.60|0.22
+cl_h_cas_const climb Constant CAS crossover altitude 3.7 1.9 5.4 norm 3.66|1.07
+cl_h_mach_const climb Constant Mach crossover altitude 8.8 7.9 9.8 norm 8.80|0.58
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 10.25 7.63 12.87 norm 10.25|1.59
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 8.43 6.28 10.6 norm 8.44|1.31
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 5.28 3.6 6.97 norm 5.29|1.03
+cr_d_range cruise Cruise range 856 487 4352 gamma 1.71|453.95|569.12
+cr_v_cas_mean cruise Mean cruise CAS 133 124 143 norm 133.62|5.82
+cr_v_cas_max cruise Maximum cruise CAS 135 128 154 gamma 4.89|121.28|3.70
+cr_v_mach_mean cruise Mean cruise Mach 0.78 0.75 0.8 beta 17.82|5.05|0.62|0.20
+cr_v_mach_max cruise Maximum cruise Mach 0.8 0.77 0.83 norm 0.80|0.02
+cr_h_init cruise Initial cruise altitude 10.82 9.79 11.85 norm 10.82|0.63
+cr_h_mean cruise Mean cruise altitude 10.92 10 11.84 norm 10.92|0.56
+cr_h_max cruise Maximum cruise altitude 11.06 10.2 11.92 norm 11.06|0.52
+de_d_range descent Descent range 234 180 457 gamma 3.15|169.28|30.17
+de_v_mach_const descent Constant Mach 0.77 0.73 0.81 norm 0.77|0.02
+de_v_cas_const descent Constant CAS 144 135 163 gamma 7.43|124.57|3.15
+de_h_mach_const descent Constant Mach crossover altitude 9.6 7.9 10.7 beta 4.91|2.91|6.03|5.37
+de_h_cas_const descent Constant CAS crossover altitude 5.7 3 8.5 norm 5.75|1.66
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -5.76 -13.45 -2.26 beta 3.52|1.95|-19.00|18.22
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -10.03 -14.68 -5.35 norm -10.02|2.84
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -6.08 -7.88 -4.27 norm -6.08|1.10
+fa_va_avg final_approach Mean airspeed 72 67 77 norm 72.43|3.49
+fa_vs_avg final_approach Mean vertical rate -3.55 -4.18 -2.91 norm -3.55|0.39
+fa_agl final_approach Approach angle 3.06 2.4 3.73 norm 3.07|0.41
+ld_v_app landing Touchdown speed 69.4 62.7 76 norm 69.37|4.61
+ld_d_brk landing Braking distance 1.08 0.63 3.21 gamma 2.36|0.35|0.54
+ld_acc_brk landing Mean braking acceleration -1.22 -1.97 -0.47 norm -1.22|0.46
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/a321.csv b/data/performance/OpenAP/fixwing/wrap/a321.csv
deleted file mode 100644
index 6c96375d5a..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/a321.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,90.8,80.0,101.6,norm,90.8|7.5
-TO,to_d_tof,1.85,1.16,2.54,norm,1.85|0.42
-TO,to_acc_tof,1.95,1.48,2.42,norm,1.95|0.29
-IC,ic_va_avg,86,78,94,norm,86|5
-IC,ic_vz_avg,13.21,9.31,17.13,norm,13.22|2.38
-CL,cl_d_range,236,164,439,gamma,5|136|23
-CL,cl_v_cas_const,158,145,171,norm,158|7
-CL,cl_v_mach_const,0.78,0.72,0.81,beta,15.34|6.21|0.57|0.29
-CL,cl_h_cas_const,5.3,3.5,7.1,norm,5.3|0.9
-CL,cl_h_mach_const,9.2,7.1,10.5,beta,10.3|4.5|3.6|7.7
-CL,cl_vz_avg_pre_cas,9.49,7.16,11.82,norm,9.49|1.42
-CL,cl_vz_avg_cas_const,6.73,4.29,9.18,norm,6.74|1.49
-CL,cl_vz_avg_mach_const,4.89,2.81,6.99,norm,4.90|1.27
-CR,cr_d_range,601,194,5334,gamma,1|161|790
-CR,cr_v_cas_mean,136,127,153,gamma,7|117|2
-CR,cr_v_cas_max,140,130,164,gamma,5|121|4
-CR,cr_v_mach_mean,0.78,0.75,0.80,norm,0.78|0.02
-CR,cr_v_mach_max,0.79,0.77,0.84,gamma,14.56|0.71|0.01
-CR,cr_h_mean,10.4,9.2,11.5,norm,10.4|0.7
-CR,cr_h_max,10.5,9.5,11.5,norm,10.5|0.6
-DE,de_d_range,242,168,464,gamma,4|143|26
-DE,de_v_mach_const,0.77,0.71,0.80,beta,8.91|3.49|0.60|0.22
-DE,de_v_cas_const,149,136,163,norm,149|8
-DE,de_h_mach_const,9.1,7.1,10.4,beta,5.2|3.2|5.1|6.2
-DE,de_h_cas_const,6.2,3.3,8.4,beta,3.3|2.7|1.3|8.4
-DE,de_vz_avg_mach_const,-5.57,-11.75,-2.02,beta,4.71|2.68|-17.89|17.89
-DE,de_vz_avg_cas_const,-9.15,-13.70,-4.57,norm,-9.14|2.77
-DE,de_vz_avg_after_cas,-6.03,-7.81,-4.24,norm,-6.02|1.09
-FA,fa_va_avg,75,70,80,norm,75|3
-FA,fa_vz_avg,-3.69,-4.37,-2.99,norm,-3.68|0.42
-LD,ld_v_app,72.9,66.9,79.0,norm,72.9|4.2
-LD,ld_d_brk,1.44,0.73,4.12,gamma,2.87|0.26|0.63
-LD,ld_acc_brk,-1.06,-2.11,-0.49,beta,9.63|3.80|-4.21|4.17
diff --git a/data/performance/OpenAP/fixwing/wrap/a321.txt b/data/performance/OpenAP/fixwing/wrap/a321.txt
new file mode 100644
index 0000000000..9ec6e22b01
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/a321.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 90.8 80.1 101.5 norm 90.80|7.45
+to_d_tof takeoff Takeoff distance 1.85 1.18 2.52 norm 1.85|0.41
+to_acc_tof takeoff Mean takeoff accelaration 1.95 1.49 2.42 norm 1.95|0.28
+ic_va_avg initial_climb Mean airspeed 86 79 94 norm 86.60|5.22
+ic_vs_avg initial_climb Mean vertical rate 13.21 9.34 17.11 norm 13.22|2.36
+cl_d_range climb Climb range 230 164 426 gamma 4.93|140.03|22.96
+cl_v_cas_const climb Constant CAS 155 144 166 norm 155.67|6.67
+cl_v_mach_const climb Constant Mach 0.77 0.74 0.81 norm 0.77|0.02
+cl_h_cas_const climb Constant CAS crossover altitude 3.7 2.1 5.4 norm 3.75|0.98
+cl_h_mach_const climb Constant Mach crossover altitude 8.4 7.5 9.3 norm 8.41|0.56
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 9.38 7.03 11.73 norm 9.38|1.43
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 8.17 5.91 10.44 norm 8.17|1.38
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 5.19 3.33 7.05 norm 5.19|1.13
+cr_d_range cruise Cruise range 610 196 5338 gamma 1.56|162.63|789.69
+cr_v_cas_mean cruise Mean cruise CAS 138 127 149 norm 138.28|6.66
+cr_v_cas_max cruise Maximum cruise CAS 140 130 162 gamma 5.67|121.09|4.11
+cr_v_mach_mean cruise Mean cruise Mach 0.78 0.75 0.8 norm 0.78|0.02
+cr_v_mach_max cruise Maximum cruise Mach 0.79 0.77 0.84 gamma 13.42|0.72|0.01
+cr_h_init cruise Initial cruise altitude 10.34 9.23 11.46 norm 10.35|0.68
+cr_h_mean cruise Mean cruise altitude 10.41 9.36 11.46 norm 10.41|0.64
+cr_h_max cruise Maximum cruise altitude 10.56 9.6 11.52 norm 10.56|0.58
+de_d_range descent Descent range 236 174 463 gamma 3.63|159.06|29.37
+de_v_mach_const descent Constant Mach 0.77 0.74 0.8 norm 0.77|0.02
+de_v_cas_const descent Constant CAS 150 138 163 norm 150.75|7.62
+de_h_mach_const descent Constant Mach crossover altitude 9 7.8 10.3 norm 9.05|0.76
+de_h_cas_const descent Constant CAS crossover altitude 6.2 3.3 8.5 beta 2.88|2.40|1.57|8.08
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -5.52 -11.86 -2.17 beta 4.06|2.31|-17.23|16.72
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.41 -13.64 -5.15 norm -9.40|2.58
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -6.01 -7.78 -4.24 norm -6.01|1.08
+fa_va_avg final_approach Mean airspeed 75 70 80 norm 75.41|3.61
+fa_vs_avg final_approach Mean vertical rate -3.69 -4.36 -3.01 norm -3.68|0.41
+fa_agl final_approach Approach angle 3.01 2.36 3.66 norm 3.01|0.39
+ld_v_app landing Touchdown speed 72.9 66.9 78.9 norm 72.94|4.17
+ld_d_brk landing Braking distance 1.55 0.64 3.94 beta 1.50|2.41|0.34|4.65
+ld_acc_brk landing Mean braking acceleration -1.21 -1.98 -0.45 norm -1.21|0.47
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/a332.csv b/data/performance/OpenAP/fixwing/wrap/a332.csv
deleted file mode 100644
index 8be4da388a..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/a332.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,89.9,77.8,102.0,norm,89.9|8.4
-TO,to_d_tof,1.98,1.16,2.80,norm,1.98|0.50
-TO,to_acc_tof,1.75,1.32,2.19,norm,1.75|0.26
-IC,ic_va_avg,86,78,94,norm,86|5
-IC,ic_vz_avg,12.48,8.38,16.60,norm,12.49|2.50
-CL,cl_d_range,306,159,453,norm,306|57
-CL,cl_v_cas_const,155,145,166,norm,155|6
-CL,cl_v_mach_const,0.80,0.75,0.86,norm,0.80|0.03
-CL,cl_h_cas_const,5.2,3.2,7.2,norm,5.2|1.0
-CL,cl_h_mach_const,10.1,8.2,12.0,norm,10.1|1.0
-CL,cl_vz_avg_pre_cas,9.72,7.19,12.26,norm,9.73|1.54
-CL,cl_vz_avg_cas_const,6.80,4.19,9.42,norm,6.81|1.59
-CL,cl_vz_avg_mach_const,4.15,2.47,7.39,gamma,9.22|0.05|0.50
-CR,cr_d_range,224,241,10006,beta,0|2|216|10638
-CR,cr_v_cas_mean,130,122,144,gamma,11|109|2
-CR,cr_v_cas_max,136,126,163,gamma,4|118|5
-CR,cr_v_mach_mean,0.82,0.78,0.84,beta,23.04|6.20|0.64|0.22
-CR,cr_v_mach_max,0.84,0.80,0.87,norm,0.84|0.02
-CR,cr_h_mean,12.3,10.3,12.4,beta,4.8|1.2|8.1|4.4
-CR,cr_h_max,12.5,10.7,12.5,beta,5.4|1.0|8.1|4.4
-DE,de_d_range,285,206,517,gamma,5|176|26
-DE,de_v_mach_const,0.80,0.76,0.85,norm,0.80|0.03
-DE,de_v_cas_const,152,138,166,norm,152|8
-DE,de_h_mach_const,10.3,7.7,11.4,beta,9.5|3.0|2.3|9.8
-DE,de_h_cas_const,6.2,3.6,8.9,norm,6.2|1.6
-DE,de_vz_avg_mach_const,-6.18,-12.67,-2.48,beta,6.29|3.17|-21.29|21.29
-DE,de_vz_avg_cas_const,-9.15,-13.51,-4.78,norm,-9.14|2.65
-DE,de_vz_avg_after_cas,-5.81,-7.58,-4.04,norm,-5.81|1.08
-FA,fa_va_avg,72,67,78,norm,72|3
-FA,fa_vz_avg,-3.62,-4.25,-2.98,norm,-3.62|0.39
-LD,ld_v_app,70.7,65.4,75.9,norm,70.7|3.6
-LD,ld_d_brk,1.55,0.86,3.66,gamma,3.68|0.32|0.46
-LD,ld_acc_brk,-1.18,-1.85,-0.50,norm,-1.18|0.41
diff --git a/data/performance/OpenAP/fixwing/wrap/a332.txt b/data/performance/OpenAP/fixwing/wrap/a332.txt
new file mode 100644
index 0000000000..90cd4bba69
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/a332.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 89.9 77.9 101.9 norm 89.89|8.34
+to_d_tof takeoff Takeoff distance 1.97 1.16 2.79 norm 1.98|0.49
+to_acc_tof takeoff Mean takeoff accelaration 1.75 1.32 2.18 norm 1.75|0.26
+ic_va_avg initial_climb Mean airspeed 86 78 94 norm 86.44|5.43
+ic_vs_avg initial_climb Mean vertical rate 12.48 8.41 16.57 norm 12.49|2.48
+cl_d_range climb Climb range 297 152 442 norm 297.55|56.26
+cl_v_cas_const climb Constant CAS 152 143 162 norm 152.98|6.05
+cl_v_mach_const climb Constant Mach 0.81 0.76 0.84 beta 11.67|3.95|0.63|0.22
+cl_h_cas_const climb Constant CAS crossover altitude 3.6 2.3 6 gamma 8.62|0.62|0.39
+cl_h_mach_const climb Constant Mach crossover altitude 9.2 8.3 10.2 norm 9.25|0.60
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 9.53 7.04 12.02 norm 9.53|1.51
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 7.98 5.51 10.47 norm 7.99|1.51
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 5.23 3.03 7.45 norm 5.24|1.34
+cr_d_range cruise Cruise range 1023 229 11012 beta 1.11|2.41|187.36|12081.93
+cr_v_cas_mean cruise Mean cruise CAS 129 123 143 gamma 6.40|116.17|2.49
+cr_v_cas_max cruise Maximum cruise CAS 135 126 160 gamma 3.86|120.04|5.41
+cr_v_mach_mean cruise Mean cruise Mach 0.81 0.79 0.84 norm 0.81|0.02
+cr_v_mach_max cruise Maximum cruise Mach 0.84 0.81 0.87 norm 0.84|0.02
+cr_h_init cruise Initial cruise altitude 11.33 9.99 12.68 norm 11.34|0.82
+cr_h_mean cruise Mean cruise altitude 11.67 10.71 12.63 norm 11.67|0.58
+cr_h_max cruise Maximum cruise altitude 11.95 11.12 12.79 norm 11.96|0.51
+de_d_range descent Descent range 283 207 505 gamma 4.98|180.10|25.87
+de_v_mach_const descent Constant Mach 0.81 0.76 0.84 beta 10.12|3.59|0.65|0.20
+de_v_cas_const descent Constant CAS 152 136 162 beta 4.92|2.86|119.57|49.23
+de_h_mach_const descent Constant Mach crossover altitude 10.2 8.4 11.4 beta 4.18|2.78|6.90|5.17
+de_h_cas_const descent Constant CAS crossover altitude 6.3 3.6 9 norm 6.29|1.63
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -6.28 -12.53 -2.85 beta 4.74|2.60|-18.78|17.84
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.39 -13.44 -5.31 norm -9.38|2.47
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -5.82 -7.56 -4.07 norm -5.82|1.06
+fa_va_avg final_approach Mean airspeed 72 67 78 norm 72.83|3.65
+fa_vs_avg final_approach Mean vertical rate -3.62 -4.24 -2.99 norm -3.62|0.38
+fa_agl final_approach Approach angle 2.98 2.44 3.53 norm 2.99|0.33
+ld_v_app landing Touchdown speed 70.7 65.5 75.9 norm 70.67|3.61
+ld_d_brk landing Braking distance 1.54 0.87 3.67 gamma 3.53|0.35|0.47
+ld_acc_brk landing Mean braking acceleration -1.18 -1.85 -0.5 norm -1.18|0.41
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/a333.csv b/data/performance/OpenAP/fixwing/wrap/a333.csv
deleted file mode 100644
index e4da49dcc4..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/a333.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,89.9,77.7,102.2,norm,90.0|8.5
-TO,to_d_tof,1.89,1.26,2.98,gamma,12.87|0.14|0.15
-TO,to_acc_tof,1.72,1.31,2.14,norm,1.73|0.25
-IC,ic_va_avg,87,79,94,norm,87|5
-IC,ic_vz_avg,12.29,8.41,16.18,norm,12.30|2.36
-CL,cl_d_range,311,158,463,norm,311|59
-CL,cl_v_cas_const,157,146,168,norm,157|6
-CL,cl_v_mach_const,0.79,0.73,0.86,norm,0.79|0.04
-CL,cl_h_cas_const,5.0,2.8,7.3,norm,5.0|1.1
-CL,cl_h_mach_const,9.8,7.8,11.7,norm,9.8|1.0
-CL,cl_vz_avg_pre_cas,9.17,6.79,11.57,norm,9.18|1.45
-CL,cl_vz_avg_cas_const,6.19,4.12,9.55,gamma,15.98|-0.05|0.42
-CL,cl_vz_avg_mach_const,4.28,2.71,7.66,gamma,7.00|0.81|0.58
-CR,cr_d_range,560,571,8414,beta,0|1|553|8450
-CR,cr_v_cas_mean,134,123,144,norm,134|6
-CR,cr_v_cas_max,140,128,162,beta,2|6|121|75
-CR,cr_v_mach_mean,0.81,0.79,0.84,norm,0.81|0.01
-CR,cr_v_mach_max,0.84,0.81,0.87,gamma,25.03|0.74|0.00
-CR,cr_h_mean,11.5,10.5,12.5,norm,11.5|0.6
-CR,cr_h_max,12.4,10.6,12.5,beta,5.1|1.2|8.5|4.0
-DE,de_d_range,286,214,522,gamma,4|191|28
-DE,de_v_mach_const,0.80,0.74,0.86,norm,0.80|0.03
-DE,de_v_cas_const,151,137,166,norm,152|8
-DE,de_h_mach_const,9.7,7.9,11.6,norm,9.7|1.1
-DE,de_h_cas_const,6.3,3.2,9.1,beta,4.1|3.6|0.6|10.5
-DE,de_vz_avg_mach_const,-6.32,-12.18,-2.52,beta,5.21|3.17|-18.57|18.57
-DE,de_vz_avg_cas_const,-8.72,-13.03,-4.39,norm,-8.71|2.63
-DE,de_vz_avg_after_cas,-5.74,-7.45,-4.03,norm,-5.74|1.04
-FA,fa_va_avg,73,69,78,norm,73|3
-FA,fa_vz_avg,-3.65,-4.25,-3.04,norm,-3.65|0.37
-LD,ld_v_app,71.5,65.5,77.6,norm,71.5|4.2
-LD,ld_d_brk,1.51,0.81,3.76,gamma,3.53|0.26|0.49
-LD,ld_acc_brk,-1.20,-1.87,-0.53,norm,-1.20|0.41
diff --git a/data/performance/OpenAP/fixwing/wrap/a333.txt b/data/performance/OpenAP/fixwing/wrap/a333.txt
new file mode 100644
index 0000000000..41f77f6256
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/a333.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 89.9 77.8 102.1 norm 89.97|8.44
+to_d_tof takeoff Takeoff distance 1.86 1.27 3.01 gamma 8.89|0.45|0.18
+to_acc_tof takeoff Mean takeoff accelaration 1.72 1.32 2.13 norm 1.73|0.25
+ic_va_avg initial_climb Mean airspeed 87 79 94 norm 87.04|4.98
+ic_vs_avg initial_climb Mean vertical rate 12.28 8.47 16.11 norm 12.29|2.32
+cl_d_range climb Climb range 287 174 456 beta 2.84|4.07|153.70|358.08
+cl_v_cas_const climb Constant CAS 153 142 165 norm 153.59|7.03
+cl_v_mach_const climb Constant Mach 0.8 0.75 0.84 norm 0.80|0.03
+cl_h_cas_const climb Constant CAS crossover altitude 3.1 1.9 6.2 gamma 4.75|0.84|0.61
+cl_h_mach_const climb Constant Mach crossover altitude 9.1 8 10.2 norm 9.08|0.66
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 8.82 6.46 11.19 norm 8.82|1.44
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 7.65 5.24 10.08 norm 7.66|1.47
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 4.56 3.14 7.9 gamma 5.77|1.63|0.61
+cr_d_range cruise Cruise range 789 604 8911 beta 1.02|1.72|576.76|8731.39
+cr_v_cas_mean cruise Mean cruise CAS 131 124 144 gamma 6.44|117.90|2.41
+cr_v_cas_max cruise Maximum cruise CAS 137 128 161 beta 2.59|7.64|123.03|77.33
+cr_v_mach_mean cruise Mean cruise Mach 0.81 0.79 0.83 norm 0.81|0.01
+cr_v_mach_max cruise Maximum cruise Mach 0.83 0.81 0.87 gamma 17.42|0.76|0.00
+cr_h_init cruise Initial cruise altitude 11.1 9.74 12.46 norm 11.10|0.83
+cr_h_mean cruise Mean cruise altitude 11.49 10.54 12.45 norm 11.50|0.58
+cr_h_max cruise Maximum cruise altitude 12.53 10.63 12.48 beta 4.06|0.99|8.87|3.66
+de_d_range descent Descent range 284 214 508 gamma 4.31|193.23|27.46
+de_v_mach_const descent Constant Mach 0.81 0.77 0.84 norm 0.81|0.02
+de_v_cas_const descent Constant CAS 150 137 162 norm 150.13|7.55
+de_h_mach_const descent Constant Mach crossover altitude 10.2 8.5 11.3 beta 4.10|2.55|6.93|4.96
+de_h_cas_const descent Constant CAS crossover altitude 6.3 3.4 9.1 norm 6.28|1.73
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -6.17 -12.5 -2.68 beta 4.37|2.47|-18.33|17.48
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.03 -13.07 -4.96 norm -9.02|2.46
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -5.74 -7.41 -4.06 norm -5.74|1.02
+fa_va_avg final_approach Mean airspeed 73 69 78 norm 73.84|3.33
+fa_vs_avg final_approach Mean vertical rate -3.74 -4.22 -2.97 gamma 16.90|-5.22|0.09
+fa_agl final_approach Approach angle 2.97 2.44 3.49 norm 2.97|0.32
+ld_v_app landing Touchdown speed 71.5 65.5 77.6 norm 71.55|4.18
+ld_d_brk landing Braking distance 1.48 0.81 3.79 gamma 3.19|0.33|0.53
+ld_acc_brk landing Mean braking acceleration -1.2 -1.86 -0.53 norm -1.20|0.40
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/a343.csv b/data/performance/OpenAP/fixwing/wrap/a343.csv
deleted file mode 100644
index 24816f4177..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/a343.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,87.0,76.0,98.2,norm,87.1|7.7
-TO,to_d_tof,2.21,1.43,3.78,gamma,8.13|0.40|0.25
-TO,to_acc_tof,1.40,1.01,1.80,norm,1.40|0.24
-IC,ic_va_avg,84,76,93,beta,3|5|69|36
-IC,ic_vz_avg,6.58,4.79,10.89,gamma,5.53|2.93|0.81
-CL,cl_d_range,307,173,443,norm,308|52
-CL,cl_v_cas_const,159,146,168,beta,5|2|131|41
-CL,cl_v_mach_const,0.78,0.75,0.82,norm,0.78|0.02
-CL,cl_h_cas_const,5.0,3.0,6.9,norm,5.0|1.0
-CL,cl_h_mach_const,9.5,7.9,11.1,norm,9.5|0.8
-CL,cl_vz_avg_pre_cas,7.13,5.54,10.68,gamma,6.44|3.72|0.63
-CL,cl_vz_avg_cas_const,5.85,3.99,8.94,gamma,14.54|0.46|0.40
-CL,cl_vz_avg_mach_const,4.05,2.03,6.09,norm,4.06|1.23
-CR,cr_d_range,5438,-3430,14332,norm,5450|3447
-CR,cr_v_cas_mean,138,128,149,norm,138|6
-CR,cr_v_cas_max,149,133,166,norm,149|9
-CR,cr_v_mach_mean,0.81,0.79,0.83,norm,0.81|0.01
-CR,cr_v_mach_max,0.83,0.81,0.88,gamma,11.68|0.77|0.01
-CR,cr_h_mean,11.0,10.0,12.0,norm,11.0|0.6
-CR,cr_h_max,11.5,10.6,12.4,norm,11.5|0.6
-DE,de_d_range,284,202,501,gamma,6|166|23
-DE,de_v_mach_const,0.80,0.75,0.84,norm,0.80|0.03
-DE,de_v_cas_const,152,139,166,norm,152|8
-DE,de_h_mach_const,9.6,8.0,11.2,norm,9.6|1.0
-DE,de_h_cas_const,5.9,3.1,8.6,norm,5.9|1.7
-DE,de_vz_avg_mach_const,-5.78,-11.97,-2.68,beta,6.21|2.77|-20.26|19.42
-DE,de_vz_avg_cas_const,-8.98,-13.47,-4.46,norm,-8.97|2.74
-DE,de_vz_avg_after_cas,-5.51,-7.22,-3.80,norm,-5.51|1.04
-FA,fa_va_avg,74,69,79,norm,74|3
-FA,fa_vz_avg,-3.64,-4.32,-2.96,norm,-3.64|0.41
-LD,ld_v_app,72.2,67.5,77.0,norm,72.3|3.3
-LD,ld_d_brk,1.68,0.91,3.97,gamma,3.87|0.28|0.49
-LD,ld_acc_brk,-1.06,-1.82,-0.47,beta,5.15|3.83|-2.62|2.62
diff --git a/data/performance/OpenAP/fixwing/wrap/a343.txt b/data/performance/OpenAP/fixwing/wrap/a343.txt
new file mode 100644
index 0000000000..42ebe3c3d0
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/a343.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 87 76 98.2 norm 87.08|7.70
+to_d_tof takeoff Takeoff distance 2.16 1.46 3.82 gamma 5.71|0.71|0.31
+to_acc_tof takeoff Mean takeoff accelaration 1.4 1.01 1.8 norm 1.40|0.24
+ic_va_avg initial_climb Mean airspeed 84 76 93 beta 3.76|4.87|69.49|35.32
+ic_vs_avg initial_climb Mean vertical rate 6.44 4.82 11.01 gamma 4.18|3.43|0.95
+cl_d_range climb Climb range 293 154 431 norm 293.32|53.81
+cl_v_cas_const climb Constant CAS 155 144 169 beta 6.81|9.57|129.33|64.98
+cl_v_mach_const climb Constant Mach 0.78 0.75 0.81 norm 0.78|0.02
+cl_h_cas_const climb Constant CAS crossover altitude 3.6 1.7 5.5 norm 3.61|1.16
+cl_h_mach_const climb Constant Mach crossover altitude 8.5 7.3 9.8 beta 5.08|5.89|6.16|5.14
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 6.54 5.09 10.41 gamma 4.51|3.79|0.78
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 6.59 4.87 10.05 gamma 8.32|2.54|0.55
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 4.08 2.65 7.17 gamma 6.86|0.95|0.53
+cr_d_range cruise Cruise range 6021 -1865 13929 norm 6032.29|3066.01
+cr_v_cas_mean cruise Mean cruise CAS 138 128 148 norm 138.63|6.17
+cr_v_cas_max cruise Maximum cruise CAS 149 132 165 norm 149.24|9.97
+cr_v_mach_mean cruise Mean cruise Mach 0.81 0.79 0.83 norm 0.81|0.01
+cr_v_mach_max cruise Maximum cruise Mach 0.83 0.81 0.88 gamma 11.93|0.77|0.01
+cr_h_init cruise Initial cruise altitude 10.01 8.92 12.09 gamma 9.56|7.32|0.32
+cr_h_mean cruise Mean cruise altitude 10.86 10.12 12.1 gamma 15.04|8.66|0.16
+cr_h_max cruise Maximum cruise altitude 11.7 10.61 12.35 beta 6.39|3.32|9.14|3.66
+de_d_range descent Descent range 281 203 489 gamma 5.92|169.03|22.84
+de_v_mach_const descent Constant Mach 0.81 0.76 0.84 beta 7.14|4.09|0.69|0.17
+de_v_cas_const descent Constant CAS 154 138 165 beta 3.94|2.85|125.92|46.48
+de_h_mach_const descent Constant Mach crossover altitude 9.9 8.3 11.1 beta 3.65|2.90|7.05|4.83
+de_h_cas_const descent Constant CAS crossover altitude 5.8 3 8.8 beta 2.75|2.94|1.39|9.27
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -5.84 -12.15 -2.68 beta 4.78|2.43|-18.58|17.56
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.27 -13.56 -4.97 norm -9.26|2.61
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -5.5 -7.22 -3.78 norm -5.50|1.04
+fa_va_avg final_approach Mean airspeed 74 69 79 norm 74.30|3.48
+fa_vs_avg final_approach Mean vertical rate -3.64 -4.31 -2.97 norm -3.64|0.41
+fa_agl final_approach Approach angle 3.04 2.47 3.61 norm 3.04|0.35
+ld_v_app landing Touchdown speed 72.2 67.5 77 norm 72.26|3.30
+ld_d_brk landing Braking distance 1.65 0.92 3.99 gamma 3.45|0.37|0.52
+ld_acc_brk landing Mean braking acceleration -1.01 -1.83 -0.49 beta 4.33|2.73|-2.58|2.38
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/a388.csv b/data/performance/OpenAP/fixwing/wrap/a388.csv
deleted file mode 100644
index 7d34682021..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/a388.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,89.9,75.4,104.5,norm,89.9|10.1
-TO,to_d_tof,2.56,1.34,3.79,norm,2.56|0.74
-TO,to_acc_tof,1.35,1.03,1.67,norm,1.35|0.19
-IC,ic_va_avg,88,79,96,norm,88|5
-IC,ic_vz_avg,5.72,4.37,8.91,gamma,5.67|2.95|0.59
-CL,cl_d_range,298,213,506,gamma,6|171|21
-CL,cl_v_cas_const,166,157,174,norm,166|5
-CL,cl_v_mach_const,0.83,0.79,0.87,norm,0.83|0.02
-CL,cl_h_cas_const,5.0,2.8,7.2,norm,5.0|1.1
-CL,cl_h_mach_const,9.9,8.4,11.5,norm,9.9|0.8
-CL,cl_vz_avg_pre_cas,8.45,6.58,10.32,norm,8.45|1.14
-CL,cl_vz_avg_cas_const,6.52,3.92,9.19,beta,6.41|6.73|0.68|12.03
-CL,cl_vz_avg_mach_const,4.88,2.58,7.19,norm,4.88|1.40
-CR,cr_d_range,4111,906,17803,gamma,3|227|1863
-CR,cr_v_cas_mean,136,129,145,beta,4|7|124|35
-CR,cr_v_cas_max,143,135,167,gamma,3|128|5
-CR,cr_v_mach_mean,0.84,0.82,0.86,norm,0.84|0.01
-CR,cr_v_mach_max,0.87,0.85,0.91,gamma,28.56|0.78|0.00
-CR,cr_h_mean,11.8,10.9,12.3,beta,13.1|5.6|8.7|4.2
-CR,cr_h_max,12.2,11.4,12.7,beta,8.0|4.3|10.1|3.0
-DE,de_d_range,313,235,538,gamma,5|204|25
-DE,de_v_mach_const,0.83,0.79,0.87,norm,0.83|0.03
-DE,de_v_cas_const,156,143,170,norm,156|8
-DE,de_h_mach_const,10.3,7.7,11.5,beta,7.8|2.9|3.3|9.0
-DE,de_h_cas_const,6.5,3.8,9.3,norm,6.5|1.7
-DE,de_vz_avg_mach_const,-6.45,-11.67,-2.79,beta,4.14|2.91|-16.15|15.58
-DE,de_vz_avg_cas_const,-8.17,-11.73,-4.60,norm,-8.16|2.17
-DE,de_vz_avg_after_cas,-5.49,-6.97,-4.00,norm,-5.48|0.90
-FA,fa_va_avg,73,68,77,norm,73|3
-FA,fa_vz_avg,-3.70,-4.14,-2.92,gamma,11.94|-4.88|0.11
-LD,ld_v_app,70.0,61.8,78.3,norm,70.0|5.7
-LD,ld_d_brk,2.26,0.73,3.80,norm,2.26|0.93
-LD,ld_acc_brk,-1.01,-1.51,-0.52,norm,-1.01|0.30
diff --git a/data/performance/OpenAP/fixwing/wrap/a388.txt b/data/performance/OpenAP/fixwing/wrap/a388.txt
new file mode 100644
index 0000000000..dc7b8222ee
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/a388.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 89.9 75.4 104.4 norm 89.93|10.07
+to_d_tof takeoff Takeoff distance 2.56 1.35 3.78 norm 2.56|0.74
+to_acc_tof takeoff Mean takeoff accelaration 1.35 1.04 1.66 norm 1.35|0.19
+ic_va_avg initial_climb Mean airspeed 88 80 96 norm 88.15|5.64
+ic_vs_avg initial_climb Mean vertical rate 5.65 4.4 8.94 gamma 4.76|3.22|0.65
+cl_d_range climb Climb range 296 200 446 beta 3.23|5.18|179.46|335.24
+cl_v_cas_const climb Constant CAS 163 155 170 norm 163.39|4.51
+cl_v_mach_const climb Constant Mach 0.84 0.8 0.86 beta 12.23|5.32|0.72|0.17
+cl_h_cas_const climb Constant CAS crossover altitude 3.3 1.3 5.3 norm 3.29|1.24
+cl_h_mach_const climb Constant Mach crossover altitude 8.9 8.2 9.7 norm 8.94|0.47
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 7.85 5.95 9.75 norm 7.85|1.16
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 7.51 5.2 9.82 norm 7.51|1.40
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 5.56 3.23 7.91 norm 5.57|1.42
+cr_d_range cruise Cruise range 4348 892 20565 gamma 2.81|246.73|2274.81
+cr_v_cas_mean cruise Mean cruise CAS 136 130 145 beta 3.32|5.27|126.00|29.75
+cr_v_cas_max cruise Maximum cruise CAS 145 134 164 beta 2.02|3.21|130.38|46.65
+cr_v_mach_mean cruise Mean cruise Mach 0.84 0.82 0.86 norm 0.84|0.01
+cr_v_mach_max cruise Maximum cruise Mach 0.87 0.85 0.9 gamma 16.14|0.80|0.00
+cr_h_init cruise Initial cruise altitude 11.55 9.3 12.23 beta 3.82|1.66|7.49|5.01
+cr_h_mean cruise Mean cruise altitude 11.73 10.87 12.28 beta 7.22|3.92|9.59|3.14
+cr_h_max cruise Maximum cruise altitude 12.06 11.52 12.6 norm 12.06|0.33
+de_d_range descent Descent range 310 238 528 gamma 4.73|213.47|25.87
+de_v_mach_const descent Constant Mach 0.83 0.8 0.87 norm 0.83|0.02
+de_v_cas_const descent Constant CAS 154 142 167 norm 154.84|7.74
+de_h_mach_const descent Constant Mach crossover altitude 10.1 8.6 11.5 norm 10.06|0.88
+de_h_cas_const descent Constant CAS crossover altitude 6.6 3.9 9.4 norm 6.64|1.69
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -6.06 -11.9 -2.97 beta 3.43|2.08|-15.98|14.36
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -8.36 -11.74 -4.97 norm -8.36|2.06
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -5.48 -6.93 -4.02 norm -5.48|0.88
+fa_va_avg final_approach Mean airspeed 73 68 77 norm 73.28|3.02
+fa_vs_avg final_approach Mean vertical rate -3.71 -4.13 -2.92 gamma 9.49|-4.74|0.12
+fa_agl final_approach Approach angle 2.9 2.42 3.38 norm 2.90|0.29
+ld_v_app landing Touchdown speed 70 62.1 78 norm 70.00|5.52
+ld_d_brk landing Braking distance 2.26 0.73 3.8 norm 2.26|0.93
+ld_acc_brk landing Mean braking acceleration -1.01 -1.51 -0.52 norm -1.01|0.30
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/b737.csv b/data/performance/OpenAP/fixwing/wrap/b737.csv
deleted file mode 100644
index 54c08443ee..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/b737.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,83.6,72.3,94.9,norm,83.6|7.9
-TO,to_d_tof,1.55,1.00,2.56,gamma,10.44|0.14|0.15
-TO,to_acc_tof,1.77,1.30,2.23,norm,1.77|0.28
-IC,ic_va_avg,81,70,91,norm,81|7
-IC,ic_vz_avg,11.86,7.69,16.06,norm,11.87|2.54
-CL,cl_d_range,202,154,346,gamma,5|136|16
-CL,cl_v_cas_const,151,137,165,norm,151|8
-CL,cl_v_mach_const,0.78,0.73,0.81,beta,39.32|5.26|0.28|0.56
-CL,cl_h_cas_const,5.3,3.2,7.4,norm,5.3|1.1
-CL,cl_h_mach_const,10.3,7.8,11.6,beta,7.4|3.3|4.7|7.5
-CL,cl_vz_avg_pre_cas,12.32,9.29,15.36,norm,12.33|1.84
-CL,cl_vz_avg_cas_const,9.89,6.32,13.47,norm,9.89|2.18
-CL,cl_vz_avg_mach_const,5.82,3.28,8.38,norm,5.83|1.55
-CR,cr_d_range,491,175,4489,gamma,1|150|675
-CR,cr_v_cas_mean,125,114,135,norm,125|6
-CR,cr_v_cas_max,129,115,150,gamma,24|78|2
-CR,cr_v_mach_mean,0.77,0.75,0.80,norm,0.77|0.02
-CR,cr_v_mach_max,0.80,0.76,0.84,norm,0.80|0.02
-CR,cr_h_mean,11.7,10.7,12.8,norm,11.7|0.6
-CR,cr_h_max,12.5,10.7,12.5,beta,5.3|1.0|8.1|4.4
-DE,de_d_range,249,173,586,gamma,2|157|46
-DE,de_v_mach_const,0.78,0.69,0.81,beta,28.18|3.95|0.18|0.66
-DE,de_v_cas_const,145,131,160,norm,145|8
-DE,de_h_mach_const,9.4,7.6,11.2,norm,9.4|1.1
-DE,de_h_cas_const,5.7,3.0,8.5,norm,5.7|1.7
-DE,de_vz_avg_mach_const,-5.45,-12.87,-2.28,beta,3.94|1.99|-18.99|18.09
-DE,de_vz_avg_cas_const,-8.95,-14.05,-3.84,norm,-8.94|3.10
-DE,de_vz_avg_after_cas,-5.86,-7.94,-3.77,norm,-5.86|1.27
-FA,fa_va_avg,70,65,80,gamma,7|58|1
-FA,fa_vz_avg,-3.58,-4.46,-2.69,norm,-3.58|0.54
-LD,ld_v_app,68.8,62.9,74.7,norm,68.8|4.1
-LD,ld_d_brk,2.07,0.64,4.38,beta,1.43|1.69|0.27|4.71
-LD,ld_acc_brk,-0.82,-1.91,-0.31,beta,6.14|2.65|-3.36|3.34
diff --git a/data/performance/OpenAP/fixwing/wrap/b737.txt b/data/performance/OpenAP/fixwing/wrap/b737.txt
new file mode 100644
index 0000000000..bff99052d6
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/b737.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 83.6 72.3 94.8 norm 83.58|7.82
+to_d_tof takeoff Takeoff distance 1.5 1.03 2.58 gamma 5.97|0.52|0.20
+to_acc_tof takeoff Mean takeoff accelaration 1.77 1.31 2.23 norm 1.77|0.28
+ic_va_avg initial_climb Mean airspeed 81 71 91 norm 81.31|6.79
+ic_vs_avg initial_climb Mean vertical rate 11.93 8.05 15.83 norm 11.94|2.37
+cl_d_range climb Climb range 204 155 348 gamma 5.17|136.24|16.48
+cl_v_cas_const climb Constant CAS 151 139 164 norm 151.95|7.82
+cl_v_mach_const climb Constant Mach 0.78 0.72 0.81 beta 33.60|3.10|0.26|0.56
+cl_h_cas_const climb Constant CAS crossover altitude 3.4 2.2 6.1 gamma 6.82|0.72|0.46
+cl_h_mach_const climb Constant Mach crossover altitude 8.9 7.9 9.8 norm 8.85|0.59
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 11.4 8.01 14.82 norm 11.41|2.07
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 11.71 8.49 14.93 norm 11.71|1.96
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 6.74 4.4 9.08 norm 6.74|1.42
+cr_d_range cruise Cruise range 453 175 4929 gamma 1.38|156.17|773.74
+cr_v_cas_mean cruise Mean cruise CAS 122 115 136 gamma 11.06|103.01|1.94
+cr_v_cas_max cruise Maximum cruise CAS 126 118 150 gamma 3.76|111.86|5.16
+cr_v_mach_mean cruise Mean cruise Mach 0.78 0.74 0.8 beta 14.90|4.49|0.63|0.19
+cr_v_mach_max cruise Maximum cruise Mach 0.8 0.76 0.83 norm 0.80|0.02
+cr_h_init cruise Initial cruise altitude 12.52 10.36 12.47 beta 5.29|0.98|7.50|5.02
+cr_h_mean cruise Mean cruise altitude 11.74 10.73 12.75 norm 11.74|0.61
+cr_h_max cruise Maximum cruise altitude 11.9 11.03 12.78 norm 11.90|0.53
+de_d_range descent Descent range 244 173 566 gamma 2.89|159.35|44.87
+de_v_mach_const descent Constant Mach 0.78 0.7 0.81 beta 22.01|2.63|0.28|0.54
+de_v_cas_const descent Constant CAS 146 133 159 norm 146.60|7.70
+de_h_mach_const descent Constant Mach crossover altitude 9.6 8.1 11.2 norm 9.63|0.95
+de_h_cas_const descent Constant CAS crossover altitude 5.7 3 8.5 norm 5.73|1.69
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -5.63 -13.13 -2.29 beta 2.76|1.69|-17.10|16.02
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.39 -14.21 -4.55 norm -9.38|2.94
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -5.85 -7.84 -3.86 norm -5.85|1.21
+fa_va_avg final_approach Mean airspeed 70 65 80 gamma 7.33|58.48|1.90
+fa_vs_avg final_approach Mean vertical rate -3.58 -4.45 -2.7 norm -3.58|0.53
+fa_agl final_approach Approach angle 3.08 2.28 3.87 norm 3.08|0.48
+ld_v_app landing Touchdown speed 68.8 62.9 74.6 norm 68.78|4.05
+ld_d_brk landing Braking distance 1.91 0.66 4.39 beta 1.32|1.63|0.35|4.63
+ld_acc_brk landing Mean braking acceleration -0.83 -1.9 -0.31 beta 5.10|2.45|-3.08|3.04
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/b738.csv b/data/performance/OpenAP/fixwing/wrap/b738.csv
deleted file mode 100644
index 4577a74c28..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/b738.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,86.5,75.0,98.1,norm,86.5|8.0
-TO,to_d_tof,1.66,1.12,2.63,gamma,11.91|0.19|0.13
-TO,to_acc_tof,1.82,1.36,2.28,norm,1.82|0.28
-IC,ic_va_avg,87,80,93,norm,87|4
-IC,ic_vz_avg,12.27,8.25,16.30,norm,12.28|2.44
-CL,cl_d_range,218,170,359,gamma,5|152|16
-CL,cl_v_cas_const,150,141,167,gamma,9|129|2
-CL,cl_v_mach_const,0.78,0.74,0.81,norm,0.78|0.02
-CL,cl_h_cas_const,5.2,3.1,7.3,norm,5.2|1.1
-CL,cl_h_mach_const,10.0,8.5,11.5,norm,10.0|0.8
-CL,cl_vz_avg_pre_cas,11.36,8.85,13.88,norm,11.37|1.53
-CL,cl_vz_avg_cas_const,8.68,6.25,11.13,norm,8.69|1.48
-CL,cl_vz_avg_mach_const,5.33,3.26,7.40,norm,5.33|1.26
-CR,cr_d_range,937,489,4767,gamma,1|444|613
-CR,cr_v_cas_mean,128,121,142,gamma,7|113|2
-CR,cr_v_cas_max,133,125,153,gamma,5|116|3
-CR,cr_v_mach_mean,0.78,0.75,0.80,beta,13.35|5.86|0.66|0.16
-CR,cr_v_mach_max,0.80,0.77,0.83,norm,0.80|0.02
-CR,cr_h_mean,11.2,10.2,12.2,norm,11.2|0.6
-CR,cr_h_max,11.4,10.5,12.3,norm,11.4|0.5
-DE,de_d_range,244,177,487,gamma,3|158|31
-DE,de_v_mach_const,0.77,0.70,0.81,beta,8.42|3.02|0.58|0.25
-DE,de_v_cas_const,147,132,162,norm,147|9
-DE,de_h_mach_const,9.8,7.6,11.1,beta,5.1|2.7|5.1|6.6
-DE,de_h_cas_const,5.9,3.2,8.5,norm,5.9|1.6
-DE,de_vz_avg_mach_const,-5.69,-12.60,-1.99,beta,4.67|2.52|-19.44|19.44
-DE,de_vz_avg_cas_const,-9.66,-14.36,-4.93,norm,-9.65|2.87
-DE,de_vz_avg_after_cas,-6.20,-7.93,-4.46,norm,-6.19|1.05
-FA,fa_va_avg,77,71,82,norm,77|3
-FA,fa_vz_avg,-3.84,-4.57,-3.11,norm,-3.84|0.44
-LD,ld_v_app,75.4,69.2,81.6,norm,75.4|4.3
-LD,ld_d_brk,1.33,0.66,3.55,gamma,3.31|0.17|0.50
-LD,ld_acc_brk,-1.36,-2.14,-0.57,norm,-1.36|0.48
diff --git a/data/performance/OpenAP/fixwing/wrap/b738.txt b/data/performance/OpenAP/fixwing/wrap/b738.txt
new file mode 100644
index 0000000000..5194fe2aaf
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/b738.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 86.5 75 98 norm 86.54|7.99
+to_d_tof takeoff Takeoff distance 1.64 1.13 2.63 gamma 9.42|0.37|0.15
+to_acc_tof takeoff Mean takeoff accelaration 1.82 1.37 2.28 norm 1.82|0.28
+ic_va_avg initial_climb Mean airspeed 87 80 93 norm 87.24|4.53
+ic_vs_avg initial_climb Mean vertical rate 12.27 8.29 16.26 norm 12.28|2.42
+cl_d_range climb Climb range 216 168 350 gamma 5.40|149.68|15.22
+cl_v_cas_const climb Constant CAS 151 140 161 norm 151.22|6.33
+cl_v_mach_const climb Constant Mach 0.77 0.75 0.8 norm 0.77|0.02
+cl_h_cas_const climb Constant CAS crossover altitude 3.6 1.8 5.4 norm 3.64|1.09
+cl_h_mach_const climb Constant Mach crossover altitude 8.9 7.9 9.8 norm 8.88|0.57
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 10.61 7.83 13.41 norm 10.62|1.70
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 10.24 7.7 12.8 norm 10.25|1.55
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 6.2 4.31 8.11 norm 6.21|1.15
+cr_d_range cruise Cruise range 929 502 4953 gamma 1.71|463.22|654.96
+cr_v_cas_mean cruise Mean cruise CAS 130 121 139 norm 130.27|5.50
+cr_v_cas_max cruise Maximum cruise CAS 132 125 151 gamma 5.83|116.60|3.39
+cr_v_mach_mean cruise Mean cruise Mach 0.78 0.75 0.8 norm 0.78|0.02
+cr_v_mach_max cruise Maximum cruise Mach 0.8 0.77 0.83 norm 0.80|0.02
+cr_h_init cruise Initial cruise altitude 11.13 10.16 12.1 norm 11.13|0.59
+cr_h_mean cruise Mean cruise altitude 11.23 10.35 12.12 norm 11.24|0.54
+cr_h_max cruise Maximum cruise altitude 11.38 10.57 12.19 norm 11.38|0.49
+de_d_range descent Descent range 241 179 472 gamma 3.59|163.64|30.00
+de_v_mach_const descent Constant Mach 0.77 0.73 0.81 norm 0.77|0.03
+de_v_cas_const descent Constant CAS 145 132 159 norm 145.77|8.27
+de_h_mach_const descent Constant Mach crossover altitude 9.7 8.3 11.1 norm 9.68|0.86
+de_h_cas_const descent Constant CAS crossover altitude 5.9 3.2 8.7 norm 5.95|1.65
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -5.8 -12.73 -2.11 beta 3.43|2.10|-17.58|17.11
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.95 -14.39 -5.48 norm -9.94|2.71
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -6.23 -7.91 -4.53 norm -6.22|1.03
+fa_va_avg final_approach Mean airspeed 77 72 82 norm 77.35|3.71
+fa_vs_avg final_approach Mean vertical rate -3.84 -4.56 -3.12 norm -3.84|0.44
+fa_agl final_approach Approach angle 3.02 2.33 3.72 norm 3.03|0.42
+ld_v_app landing Touchdown speed 75.4 69.5 81.3 norm 75.37|4.11
+ld_d_brk landing Braking distance 1.25 0.68 3.6 gamma 2.60|0.33|0.57
+ld_acc_brk landing Mean braking acceleration -1.36 -2.14 -0.57 norm -1.36|0.48
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/b739.csv b/data/performance/OpenAP/fixwing/wrap/b739.csv
deleted file mode 100644
index 0df74ec749..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/b739.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,91.3,77.5,105.1,norm,91.3|9.6
-TO,to_d_tof,1.83,1.18,2.96,gamma,12.03|0.09|0.16
-TO,to_acc_tof,1.86,1.41,2.33,norm,1.87|0.28
-IC,ic_va_avg,91,84,99,norm,91|5
-IC,ic_vz_avg,11.64,7.87,17.70,gamma,16.92|-0.00|0.73
-CL,cl_d_range,243,137,350,norm,243|41
-CL,cl_v_cas_const,159,145,171,beta,4|3|132|46
-CL,cl_v_mach_const,0.79,0.75,0.81,beta,16.78|4.27|0.58|0.25
-CL,cl_h_cas_const,5.5,3.8,7.3,norm,5.5|0.9
-CL,cl_h_mach_const,9.4,8.1,10.8,norm,9.4|0.7
-CL,cl_vz_avg_pre_cas,10.50,8.30,12.71,norm,10.51|1.34
-CL,cl_vz_avg_cas_const,7.38,5.13,9.65,norm,7.39|1.38
-CL,cl_vz_avg_mach_const,4.95,2.82,7.08,norm,4.95|1.29
-CR,cr_d_range,866,189,4416,beta,1|2|159|4736
-CR,cr_v_cas_mean,138,128,149,norm,138|6
-CR,cr_v_cas_max,145,131,159,norm,145|8
-CR,cr_v_mach_mean,0.79,0.75,0.81,beta,13.95|4.86|0.66|0.17
-CR,cr_v_mach_max,0.80,0.77,0.84,gamma,19.02|0.71|0.00
-CR,cr_h_mean,10.5,9.6,11.4,norm,10.5|0.6
-CR,cr_h_max,10.8,9.7,11.6,beta,7.2|5.1|8.1|4.4
-DE,de_d_range,252,174,508,gamma,4|150|31
-DE,de_v_mach_const,0.78,0.71,0.81,beta,9.50|2.96|0.57|0.25
-DE,de_v_cas_const,148,136,160,norm,148|7
-DE,de_h_mach_const,9.2,7.8,10.6,norm,9.2|0.9
-DE,de_h_cas_const,6.4,4.2,8.7,norm,6.4|1.4
-DE,de_vz_avg_mach_const,-5.08,-11.06,-1.64,beta,3.90|2.38|-15.90|15.99
-DE,de_vz_avg_cas_const,-8.69,-13.41,-3.95,norm,-8.68|2.88
-DE,de_vz_avg_after_cas,-6.01,-7.69,-4.32,norm,-6.00|1.03
-FA,fa_va_avg,78,72,83,norm,78|3
-FA,fa_vz_avg,-3.92,-4.73,-3.11,norm,-3.92|0.49
-LD,ld_v_app,76.8,70.4,83.3,norm,76.9|4.5
-LD,ld_d_brk,1.45,0.70,4.23,gamma,2.97|0.18|0.65
-LD,ld_acc_brk,-1.25,-2.19,-0.53,beta,4.23|3.14|-3.02|2.95
diff --git a/data/performance/OpenAP/fixwing/wrap/b739.txt b/data/performance/OpenAP/fixwing/wrap/b739.txt
new file mode 100644
index 0000000000..3595a91a21
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/b739.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 91.3 77.6 105 norm 91.32|9.53
+to_d_tof takeoff Takeoff distance 1.91 1.14 2.9 beta 4.56|6.31|0.42|3.74
+to_acc_tof takeoff Mean takeoff accelaration 1.87 1.41 2.32 norm 1.87|0.28
+ic_va_avg initial_climb Mean airspeed 91 84 99 norm 91.92|5.26
+ic_vs_avg initial_climb Mean vertical rate 12.36 7.85 16.9 norm 12.37|2.75
+cl_d_range climb Climb range 239 142 337 norm 239.93|37.90
+cl_v_cas_const climb Constant CAS 154 143 166 norm 154.87|6.96
+cl_v_mach_const climb Constant Mach 0.78 0.75 0.81 beta 10.40|4.40|0.66|0.16
+cl_h_cas_const climb Constant CAS crossover altitude 3.4 2.3 5.9 gamma 6.04|1.09|0.46
+cl_h_mach_const climb Constant Mach crossover altitude 8.6 7.7 9.5 norm 8.60|0.56
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 10.18 7.64 12.72 norm 10.18|1.55
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 8.88 6.66 11.12 norm 8.89|1.36
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 5.55 3.67 7.43 norm 5.55|1.14
+cr_d_range cruise Cruise range 338 193 4458 beta 1.05|2.54|180.63|4861.59
+cr_v_cas_mean cruise Mean cruise CAS 138 127 149 norm 138.40|6.60
+cr_v_cas_max cruise Maximum cruise CAS 145 131 159 norm 145.21|8.55
+cr_v_mach_mean cruise Mean cruise Mach 0.78 0.76 0.81 norm 0.78|0.02
+cr_v_mach_max cruise Maximum cruise Mach 0.81 0.78 0.84 norm 0.81|0.02
+cr_h_init cruise Initial cruise altitude 10.43 9.43 11.43 norm 10.43|0.61
+cr_h_mean cruise Mean cruise altitude 10.52 9.63 11.41 norm 10.52|0.54
+cr_h_max cruise Maximum cruise altitude 10.74 9.73 11.61 beta 6.51|5.15|8.41|4.08
+de_d_range descent Descent range 245 180 505 gamma 3.29|165.75|34.68
+de_v_mach_const descent Constant Mach 0.78 0.73 0.8 beta 5.13|2.83|0.68|0.14
+de_v_cas_const descent Constant CAS 148 138 159 norm 148.73|6.45
+de_h_mach_const descent Constant Mach crossover altitude 9.4 8.2 10.6 norm 9.36|0.73
+de_h_cas_const descent Constant CAS crossover altitude 6.5 4.2 8.8 norm 6.46|1.39
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -4.78 -11.29 -1.79 beta 3.42|1.92|-15.82|15.27
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -8.96 -13.87 -4.17 beta 4.39|4.26|-18.26|18.26
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -6.02 -7.69 -4.35 norm -6.02|1.02
+fa_va_avg final_approach Mean airspeed 78 72 83 norm 78.30|3.70
+fa_vs_avg final_approach Mean vertical rate -3.92 -4.72 -3.13 norm -3.92|0.48
+fa_agl final_approach Approach angle 3.03 2.43 3.64 norm 3.03|0.37
+ld_v_app landing Touchdown speed 76.8 70.5 83.2 norm 76.86|4.40
+ld_d_brk landing Braking distance 1.45 0.7 4.22 gamma 2.95|0.19|0.65
+ld_acc_brk landing Mean braking acceleration -1.33 -2.12 -0.54 norm -1.33|0.48
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/b744.csv b/data/performance/OpenAP/fixwing/wrap/b744.csv
deleted file mode 100644
index 459caead57..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/b744.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,92.4,79.3,105.5,norm,92.4|9.1
-TO,to_d_tof,2.29,1.27,3.31,norm,2.29|0.62
-TO,to_acc_tof,1.67,1.21,2.13,norm,1.67|0.28
-IC,ic_va_avg,91,82,101,norm,91|6
-IC,ic_vz_avg,9.32,6.32,14.11,gamma,17.23|-0.00|0.57
-CL,cl_d_range,231,179,392,gamma,4|160|18
-CL,cl_v_cas_const,171,159,182,norm,171|6
-CL,cl_v_mach_const,0.84,0.81,0.88,norm,0.84|0.02
-CL,cl_h_cas_const,5.6,3.9,7.4,norm,5.6|0.9
-CL,cl_h_mach_const,9.7,8.0,11.4,norm,9.7|0.9
-CL,cl_vz_avg_pre_cas,9.07,7.31,13.09,gamma,6.25|5.31|0.72
-CL,cl_vz_avg_cas_const,8.18,5.07,11.30,norm,8.18|1.90
-CL,cl_vz_avg_mach_const,5.39,2.71,8.10,norm,5.40|1.64
-CR,cr_d_range,5230,-1657,12137,norm,5239|2677
-CR,cr_v_cas_mean,147,135,158,norm,147|7
-CR,cr_v_cas_max,157,140,175,norm,157|10
-CR,cr_v_mach_mean,0.84,0.82,0.87,norm,0.84|0.01
-CR,cr_v_mach_max,0.87,0.84,0.93,gamma,22.25|0.75|0.01
-CR,cr_h_mean,10.8,9.8,11.9,norm,10.8|0.6
-CR,cr_h_max,11.3,10.1,12.3,beta,8.6|7.1|8.2|5.5
-DE,de_d_range,267,193,523,gamma,3|172|32
-DE,de_v_mach_const,0.81,0.75,0.88,norm,0.81|0.04
-DE,de_v_cas_const,153,139,166,norm,153|8
-DE,de_h_mach_const,9.7,7.9,11.4,norm,9.7|1.1
-DE,de_h_cas_const,6.5,4.0,9.1,norm,6.5|1.5
-DE,de_vz_avg_mach_const,-5.45,-11.96,-2.19,beta,4.64|2.38|-18.39|17.87
-DE,de_vz_avg_cas_const,-8.72,-13.61,-3.81,norm,-8.71|2.98
-DE,de_vz_avg_after_cas,-6.25,-8.03,-4.47,norm,-6.25|1.08
-FA,fa_va_avg,79,72,85,norm,79|4
-FA,fa_vz_avg,-3.89,-4.70,-3.09,norm,-3.89|0.49
-LD,ld_v_app,77.9,71.0,85.0,norm,78.0|4.9
-LD,ld_d_brk,1.67,0.92,4.04,gamma,3.56|0.34|0.52
-LD,ld_acc_brk,-1.20,-1.97,-0.59,beta,6.26|4.55|-2.95|2.92
diff --git a/data/performance/OpenAP/fixwing/wrap/b744.txt b/data/performance/OpenAP/fixwing/wrap/b744.txt
new file mode 100644
index 0000000000..434eea931e
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/b744.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 92.4 79.3 105.4 norm 92.39|9.06
+to_d_tof takeoff Takeoff distance 2.29 1.27 3.31 norm 2.29|0.62
+to_acc_tof takeoff Mean takeoff accelaration 1.67 1.21 2.13 norm 1.67|0.28
+ic_va_avg initial_climb Mean airspeed 91 82 101 beta 4.95|5.64|70.82|44.93
+ic_vs_avg initial_climb Mean vertical rate 9.24 6.76 13.74 gamma 11.16|2.72|0.64
+cl_d_range climb Climb range 223 175 374 gamma 4.44|160.55|18.26
+cl_v_cas_const climb Constant CAS 168 157 180 norm 168.81|7.06
+cl_v_mach_const climb Constant Mach 0.84 0.81 0.87 norm 0.84|0.02
+cl_h_cas_const climb Constant CAS crossover altitude 3.9 2.2 5.6 norm 3.89|1.05
+cl_h_mach_const climb Constant Mach crossover altitude 8.3 7.6 9.7 gamma 10.78|6.39|0.20
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 8.66 6 11.33 norm 8.67|1.62
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 8.77 6.65 13.11 gamma 7.91|3.87|0.71
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 6.72 4.07 9.39 norm 6.73|1.61
+cr_d_range cruise Cruise range 5580 -1544 12725 norm 5590.66|2769.86
+cr_v_cas_mean cruise Mean cruise CAS 146 134 158 norm 146.64|7.16
+cr_v_cas_max cruise Maximum cruise CAS 157 139 174 norm 157.08|10.62
+cr_v_mach_mean cruise Mean cruise Mach 0.84 0.82 0.87 norm 0.84|0.01
+cr_v_mach_max cruise Maximum cruise Mach 0.87 0.84 0.92 gamma 16.48|0.77|0.01
+cr_h_init cruise Initial cruise altitude 10.28 8.87 11.7 norm 10.28|0.86
+cr_h_mean cruise Mean cruise altitude 10.81 9.77 11.84 norm 10.81|0.63
+cr_h_max cruise Maximum cruise altitude 11.29 10.17 12.4 beta 6.02|5.99|8.84|4.88
+de_d_range descent Descent range 262 195 510 gamma 3.60|178.39|32.24
+de_v_mach_const descent Constant Mach 0.83 0.77 0.87 beta 5.70|2.75|0.68|0.21
+de_v_cas_const descent Constant CAS 152 139 164 norm 152.44|7.62
+de_h_mach_const descent Constant Mach crossover altitude 10 8.7 11.3 norm 10.00|0.79
+de_h_cas_const descent Constant CAS crossover altitude 6.6 4 9.2 norm 6.61|1.57
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -5.43 -12.37 -2.2 beta 3.86|2.07|-17.94|17.19
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.17 -13.85 -4.48 norm -9.16|2.85
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -6.29 -8.02 -4.55 norm -6.29|1.05
+fa_va_avg final_approach Mean airspeed 79 72 85 norm 79.03|4.54
+fa_vs_avg final_approach Mean vertical rate -4.03 -4.6 -3.03 gamma 13.46|-5.66|0.13
+fa_agl final_approach Approach angle 2.98 2.33 3.64 norm 2.98|0.40
+ld_v_app landing Touchdown speed 77.9 71 84.9 norm 77.96|4.85
+ld_d_brk landing Braking distance 1.64 0.93 4.06 gamma 3.26|0.41|0.55
+ld_acc_brk landing Mean braking acceleration -1.18 -1.98 -0.6 beta 5.71|3.74|-2.92|2.76
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/b752.csv b/data/performance/OpenAP/fixwing/wrap/b752.csv
deleted file mode 100644
index cda52b6b0b..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/b752.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,90.5,76.7,104.4,norm,90.5|9.6
-TO,to_d_tof,1.66,0.99,2.70,beta,4.52|8.88|0.37|4.19
-TO,to_acc_tof,1.91,1.45,2.37,norm,1.91|0.28
-IC,ic_va_avg,88,78,98,norm,88|7
-IC,ic_vz_avg,12.93,8.25,17.63,norm,12.94|2.85
-CL,cl_d_range,228,169,389,gamma,5|146|18
-CL,cl_v_cas_const,158,145,171,norm,158|7
-CL,cl_v_mach_const,0.79,0.75,0.84,norm,0.79|0.03
-CL,cl_h_cas_const,5.3,3.6,7.1,norm,5.3|0.9
-CL,cl_h_mach_const,9.9,7.8,11.1,beta,16.6|5.2|2.5|9.4
-CL,cl_vz_avg_pre_cas,10.80,8.34,13.27,norm,10.80|1.50
-CL,cl_vz_avg_cas_const,8.14,5.50,10.79,norm,8.15|1.61
-CL,cl_vz_avg_mach_const,5.63,2.96,8.31,norm,5.63|1.63
-CR,cr_d_range,1023,497,6775,beta,1|2|470|7451
-CR,cr_v_cas_mean,133,123,144,norm,133|6
-CR,cr_v_cas_max,139,127,159,gamma,19|98|2
-CR,cr_v_mach_mean,0.79,0.77,0.82,norm,0.79|0.02
-CR,cr_v_mach_max,0.82,0.78,0.86,norm,0.82|0.02
-CR,cr_h_mean,11.2,10.1,12.1,beta,9.5|6.1|8.1|5.0
-CR,cr_h_max,11.5,10.4,12.3,beta,10.8|5.6|8.1|5.0
-DE,de_d_range,255,173,508,gamma,4|144|30
-DE,de_v_mach_const,0.78,0.73,0.83,norm,0.78|0.03
-DE,de_v_cas_const,151,137,165,norm,151|8
-DE,de_h_mach_const,9.2,7.6,10.9,norm,9.2|1.0
-DE,de_h_cas_const,6.3,3.8,8.7,norm,6.3|1.5
-DE,de_vz_avg_mach_const,-6.23,-12.48,-2.54,beta,4.51|2.66|-18.41|17.94
-DE,de_vz_avg_cas_const,-9.23,-14.31,-4.12,norm,-9.21|3.10
-DE,de_vz_avg_after_cas,-6.13,-7.96,-4.29,norm,-6.12|1.11
-FA,fa_va_avg,69,62,76,norm,69|4
-FA,fa_vz_avg,-3.41,-4.21,-2.61,norm,-3.41|0.49
-LD,ld_v_app,66.7,60.2,73.3,norm,66.7|4.5
-LD,ld_d_brk,1.22,0.68,3.42,gamma,2.67|0.33|0.53
-LD,ld_acc_brk,-0.99,-1.88,-0.47,beta,7.89|3.62|-3.37|3.28
diff --git a/data/performance/OpenAP/fixwing/wrap/b752.txt b/data/performance/OpenAP/fixwing/wrap/b752.txt
new file mode 100644
index 0000000000..897be851c6
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/b752.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 90.5 76.8 104.3 norm 90.55|9.57
+to_d_tof takeoff Takeoff distance 1.7 0.98 2.66 beta 3.89|5.55|0.41|3.34
+to_acc_tof takeoff Mean takeoff accelaration 1.91 1.46 2.37 norm 1.91|0.28
+ic_va_avg initial_climb Mean airspeed 88 78 98 norm 88.51|7.17
+ic_vs_avg initial_climb Mean vertical rate 12.99 8.6 17.4 norm 13.00|2.68
+cl_d_range climb Climb range 220 167 384 gamma 4.69|149.01|19.46
+cl_v_cas_const climb Constant CAS 155 144 166 norm 155.59|6.70
+cl_v_mach_const climb Constant Mach 0.79 0.75 0.82 norm 0.79|0.02
+cl_h_cas_const climb Constant CAS crossover altitude 4 2.3 5.7 norm 4.00|1.02
+cl_h_mach_const climb Constant Mach crossover altitude 8.7 8 9.5 norm 8.75|0.48
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 10.29 7.8 12.8 norm 10.30|1.52
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 8.95 6.41 11.51 norm 8.96|1.55
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 6.23 3.8 8.67 norm 6.24|1.48
+cr_d_range cruise Cruise range 497 512 6087 beta 0.99|1.86|497.24|5936.62
+cr_v_cas_mean cruise Mean cruise CAS 131 123 145 gamma 11.69|110.95|1.94
+cr_v_cas_max cruise Maximum cruise CAS 138 128 159 gamma 6.92|116.87|3.65
+cr_v_mach_mean cruise Mean cruise Mach 0.79 0.77 0.82 norm 0.79|0.02
+cr_v_mach_max cruise Maximum cruise Mach 0.81 0.79 0.86 gamma 9.66|0.75|0.01
+cr_h_init cruise Initial cruise altitude 11 9.96 12.05 norm 11.00|0.63
+cr_h_mean cruise Mean cruise altitude 11.17 10.28 12.07 norm 11.17|0.54
+cr_h_max cruise Maximum cruise altitude 11.64 10.39 12.22 beta 5.67|2.52|8.87|3.67
+de_d_range descent Descent range 247 178 504 gamma 3.53|162.02|33.57
+de_v_mach_const descent Constant Mach 0.79 0.75 0.82 norm 0.79|0.02
+de_v_cas_const descent Constant CAS 151 137 165 norm 151.53|8.29
+de_h_mach_const descent Constant Mach crossover altitude 9.4 8 10.8 norm 9.40|0.84
+de_h_cas_const descent Constant CAS crossover altitude 6.3 3.9 8.8 norm 6.35|1.50
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -6.8 -12.65 -2.52 beta 2.78|2.22|-15.86|15.25
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.56 -14.46 -4.64 norm -9.55|2.99
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -6.16 -7.9 -4.41 norm -6.15|1.06
+fa_va_avg final_approach Mean airspeed 69 62 76 norm 69.77|4.86
+fa_vs_avg final_approach Mean vertical rate -3.41 -4.2 -2.62 norm -3.41|0.48
+fa_agl final_approach Approach angle 3.09 2.2 3.98 norm 3.09|0.54
+ld_v_app landing Touchdown speed 66.7 60.2 73.2 norm 66.71|4.50
+ld_d_brk landing Braking distance 1.21 0.68 3.43 gamma 2.56|0.35|0.55
+ld_acc_brk landing Mean braking acceleration -1.03 -1.85 -0.46 beta 4.58|3.08|-2.63|2.53
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/b763.csv b/data/performance/OpenAP/fixwing/wrap/b763.csv
deleted file mode 100644
index f7bc84d14f..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/b763.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,92.4,79.5,105.3,norm,92.4|9.0
-TO,to_d_tof,1.82,1.03,2.62,norm,1.82|0.48
-TO,to_acc_tof,1.97,1.46,2.49,norm,1.97|0.31
-IC,ic_va_avg,88,80,96,norm,88|5
-IC,ic_vz_avg,14.28,9.41,19.17,norm,14.29|2.97
-CL,cl_d_range,211,165,354,gamma,4|149|17
-CL,cl_v_cas_const,163,149,174,beta,7|4|126|57
-CL,cl_v_mach_const,0.80,0.75,0.84,norm,0.80|0.03
-CL,cl_h_cas_const,5.7,4.1,7.3,norm,5.7|0.8
-CL,cl_h_mach_const,9.6,7.8,11.4,norm,9.6|0.9
-CL,cl_vz_avg_pre_cas,11.02,8.27,13.78,norm,11.02|1.68
-CL,cl_vz_avg_cas_const,8.78,5.15,12.42,norm,8.79|2.21
-CL,cl_vz_avg_mach_const,5.48,2.25,8.72,norm,5.49|1.97
-CR,cr_d_range,1325,540,9818,beta,1|4|496|13389
-CR,cr_v_cas_mean,138,127,149,norm,138|6
-CR,cr_v_cas_max,143,132,166,gamma,6|120|4
-CR,cr_v_mach_mean,0.80,0.77,0.83,norm,0.80|0.02
-CR,cr_v_mach_max,0.82,0.79,0.88,gamma,10.79|0.75|0.01
-CR,cr_h_mean,10.9,9.9,11.8,norm,10.9|0.6
-CR,cr_h_max,11.2,10.1,12.1,beta,7.9|6.1|8.4|4.8
-DE,de_d_range,248,177,497,gamma,3|158|31
-DE,de_v_mach_const,0.79,0.73,0.84,norm,0.79|0.03
-DE,de_v_cas_const,152,137,167,norm,152|9
-DE,de_h_mach_const,9.4,7.7,11.0,norm,9.4|1.0
-DE,de_h_cas_const,6.6,4.2,8.9,norm,6.6|1.4
-DE,de_vz_avg_mach_const,-6.52,-12.96,-2.15,beta,3.30|2.40|-17.28|17.28
-DE,de_vz_avg_cas_const,-9.66,-14.85,-4.44,norm,-9.65|3.17
-DE,de_vz_avg_after_cas,-6.22,-7.96,-4.46,norm,-6.21|1.06
-FA,fa_va_avg,74,68,80,norm,74|4
-FA,fa_vz_avg,-3.73,-4.44,-2.70,gamma,27.03|-6.39|0.10
-LD,ld_v_app,72.1,65.3,79.0,norm,72.1|4.8
-LD,ld_d_brk,1.58,0.64,3.71,beta,1.78|3.06|0.28|4.72
-LD,ld_acc_brk,-1.09,-1.95,-0.52,beta,5.75|3.52|-2.97|2.87
diff --git a/data/performance/OpenAP/fixwing/wrap/b763.txt b/data/performance/OpenAP/fixwing/wrap/b763.txt
new file mode 100644
index 0000000000..4623667f40
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/b763.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 92.4 79.6 105.3 norm 92.43|8.92
+to_d_tof takeoff Takeoff distance 1.82 1.03 2.61 norm 1.82|0.48
+to_acc_tof takeoff Mean takeoff accelaration 1.97 1.46 2.49 norm 1.97|0.31
+ic_va_avg initial_climb Mean airspeed 88 80 96 beta 4.65|5.42|71.86|36.69
+ic_vs_avg initial_climb Mean vertical rate 14.28 9.45 19.12 norm 14.29|2.94
+cl_d_range climb Climb range 202 160 338 gamma 4.37|146.83|16.52
+cl_v_cas_const climb Constant CAS 159 146 171 beta 5.42|4.92|131.86|52.14
+cl_v_mach_const climb Constant Mach 0.79 0.76 0.83 norm 0.79|0.02
+cl_h_cas_const climb Constant CAS crossover altitude 4 2.6 5.5 norm 4.05|0.90
+cl_h_mach_const climb Constant Mach crossover altitude 8.3 7.6 9.8 gamma 8.58|6.61|0.23
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 10.16 7.27 13.07 norm 10.17|1.76
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 10.26 7.06 13.48 norm 10.27|1.95
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 6.95 3.76 10.16 norm 6.96|1.95
+cr_d_range cruise Cruise range 1201 562 9544 beta 1.06|1.84|526.21|9525.03
+cr_v_cas_mean cruise Mean cruise CAS 137 127 148 norm 137.87|6.52
+cr_v_cas_max cruise Maximum cruise CAS 142 132 165 gamma 5.96|121.86|4.14
+cr_v_mach_mean cruise Mean cruise Mach 0.8 0.77 0.83 norm 0.80|0.02
+cr_v_mach_max cruise Maximum cruise Mach 0.82 0.79 0.87 gamma 9.85|0.75|0.01
+cr_h_init cruise Initial cruise altitude 10.65 9.11 11.75 beta 4.71|3.26|7.61|4.88
+cr_h_mean cruise Mean cruise altitude 10.81 9.9 11.73 norm 10.82|0.55
+cr_h_max cruise Maximum cruise altitude 11.17 10.3 12.04 norm 11.17|0.53
+de_d_range descent Descent range 244 178 489 gamma 3.61|161.96|31.72
+de_v_mach_const descent Constant Mach 0.79 0.76 0.83 norm 0.79|0.02
+de_v_cas_const descent Constant CAS 151 139 164 norm 151.80|7.70
+de_h_mach_const descent Constant Mach crossover altitude 9.5 8.3 10.8 norm 9.55|0.77
+de_h_cas_const descent Constant CAS crossover altitude 6.7 4.4 9 norm 6.70|1.42
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -6.29 -13.2 -2.33 beta 2.75|1.91|-16.88|16.10
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.98 -14.99 -4.94 norm -9.97|3.06
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -6.29 -7.99 -4.58 norm -6.29|1.04
+fa_va_avg final_approach Mean airspeed 74 68 80 norm 74.38|4.27
+fa_vs_avg final_approach Mean vertical rate -3.76 -4.37 -2.75 gamma 15.83|-5.60|0.12
+fa_agl final_approach Approach angle 2.98 2.22 3.74 norm 2.98|0.46
+ld_v_app landing Touchdown speed 72.1 65.3 78.9 norm 72.10|4.70
+ld_d_brk landing Braking distance 1.51 0.66 3.71 beta 1.65|2.94|0.35|4.63
+ld_acc_brk landing Mean braking acceleration -1.1 -1.94 -0.52 beta 4.70|3.14|-2.76|2.62
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/b77w.csv b/data/performance/OpenAP/fixwing/wrap/b77w.csv
deleted file mode 100644
index eeae8262d1..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/b77w.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,96.4,83.9,109.0,norm,96.5|8.7
-TO,to_d_tof,2.21,1.38,3.04,norm,2.21|0.50
-TO,to_acc_tof,1.89,1.45,2.33,norm,1.89|0.27
-IC,ic_va_avg,98,88,106,beta,3|3|80|33
-IC,ic_vz_avg,13.02,9.69,16.36,norm,13.03|2.03
-CL,cl_d_range,261,154,369,norm,262|41
-CL,cl_v_cas_const,166,158,173,norm,166|4
-CL,cl_v_mach_const,0.83,0.78,0.88,norm,0.83|0.03
-CL,cl_h_cas_const,5.7,3.4,8.0,norm,5.7|1.2
-CL,cl_h_mach_const,9.5,7.5,11.5,norm,9.5|1.0
-CL,cl_vz_avg_pre_cas,9.86,8.18,14.19,gamma,4.91|6.56|0.84
-CL,cl_vz_avg_cas_const,7.22,3.11,11.35,norm,7.23|2.51
-CL,cl_vz_avg_mach_const,4.48,1.69,10.26,beta,1.86|3.03|0.59|13.14
-CR,cr_d_range,2831,675,14048,beta,1|1|594|14118
-CR,cr_v_cas_mean,149,140,158,norm,149|5
-CR,cr_v_cas_max,160,144,176,norm,160|9
-CR,cr_v_mach_mean,0.84,0.82,0.86,norm,0.84|0.01
-CR,cr_v_mach_max,0.86,0.84,0.91,gamma,4.92|0.82|0.01
-CR,cr_h_mean,10.4,9.8,11.5,gamma,13.1|8.7|0.1
-CR,cr_h_max,11.1,10.2,11.7,beta,5.5|3.7|9.2|3.0
-DE,de_d_range,260,197,488,gamma,3|181|29
-DE,de_v_mach_const,0.82,0.76,0.87,norm,0.82|0.03
-DE,de_v_cas_const,156,142,171,norm,156|8
-DE,de_h_mach_const,9.5,8.0,11.0,norm,9.5|0.9
-DE,de_h_cas_const,6.6,4.1,9.1,norm,6.6|1.5
-DE,de_vz_avg_mach_const,-6.94,-11.78,-2.08,norm,-6.93|2.95
-DE,de_vz_avg_cas_const,-8.68,-13.22,-4.12,norm,-8.67|2.77
-DE,de_vz_avg_after_cas,-6.27,-7.92,-4.62,norm,-6.27|1.00
-FA,fa_va_avg,78,73,83,norm,78|3
-FA,fa_vz_avg,-4.03,-4.49,-3.27,gamma,15.93|-5.43|0.09
-LD,ld_v_app,76.9,70.8,83.1,norm,76.9|4.3
-LD,ld_d_brk,1.49,0.84,3.52,gamma,3.57|0.34|0.45
-LD,ld_acc_brk,-1.33,-2.00,-0.66,norm,-1.33|0.41
diff --git a/data/performance/OpenAP/fixwing/wrap/b77w.txt b/data/performance/OpenAP/fixwing/wrap/b77w.txt
new file mode 100644
index 0000000000..86855e285b
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/b77w.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 96.4 83.9 109 norm 96.46|8.74
+to_d_tof takeoff Takeoff distance 2.21 1.38 3.04 norm 2.21|0.50
+to_acc_tof takeoff Mean takeoff accelaration 1.89 1.45 2.33 norm 1.89|0.27
+ic_va_avg initial_climb Mean airspeed 98 88 106 beta 3.65|3.27|80.15|33.27
+ic_vs_avg initial_climb Mean vertical rate 13.02 9.69 16.36 norm 13.03|2.03
+cl_d_range climb Climb range 214 173 354 gamma 4.08|161.44|17.33
+cl_v_cas_const climb Constant CAS 164 158 170 norm 164.33|3.45
+cl_v_mach_const climb Constant Mach 0.83 0.8 0.86 norm 0.83|0.02
+cl_h_cas_const climb Constant CAS crossover altitude 3.8 1.8 5.9 norm 3.84|1.25
+cl_h_mach_const climb Constant Mach crossover altitude 8.8 8.1 9.5 norm 8.80|0.42
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 8.99 6.8 11.19 norm 8.99|1.33
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 8.55 6.41 12.95 gamma 7.87|3.61|0.72
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 5.76 3.31 10.81 gamma 7.72|0.15|0.83
+cr_d_range cruise Cruise range 4843 758 14177 beta 1.32|1.74|606.13|14106.67
+cr_v_cas_mean cruise Mean cruise CAS 148 139 158 norm 148.69|5.75
+cr_v_cas_max cruise Maximum cruise CAS 159 142 175 norm 159.16|10.07
+cr_v_mach_mean cruise Mean cruise Mach 0.84 0.82 0.86 norm 0.84|0.01
+cr_v_mach_max cruise Maximum cruise Mach 0.86 0.84 0.91 gamma 5.16|0.82|0.01
+cr_h_init cruise Initial cruise altitude 9.63 8.74 11.52 gamma 7.37|7.62|0.32
+cr_h_mean cruise Mean cruise altitude 10.35 9.75 11.4 gamma 12.87|8.67|0.14
+cr_h_max cruise Maximum cruise altitude 11 10.31 11.69 norm 11.00|0.42
+de_d_range descent Descent range 257 197 476 gamma 3.62|182.39|28.42
+de_v_mach_const descent Constant Mach 0.82 0.79 0.86 norm 0.82|0.02
+de_v_cas_const descent Constant CAS 156 140 167 beta 4.64|3.00|124.43|50.43
+de_h_mach_const descent Constant Mach crossover altitude 9.7 8.6 10.8 norm 9.72|0.67
+de_h_cas_const descent Constant CAS crossover altitude 6.7 4.2 9.2 norm 6.68|1.51
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -7.04 -12.03 -2.02 norm -7.02|3.04
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.05 -13.36 -4.72 norm -9.04|2.63
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -6.3 -7.92 -4.67 norm -6.30|0.99
+fa_va_avg final_approach Mean airspeed 78 73 83 norm 78.67|3.38
+fa_vs_avg final_approach Mean vertical rate -4.03 -4.49 -3.27 gamma 15.93|-5.43|0.09
+fa_agl final_approach Approach angle 2.95 2.44 3.46 norm 2.95|0.31
+ld_v_app landing Touchdown speed 76.9 70.8 83.1 norm 76.94|4.25
+ld_d_brk landing Braking distance 1.49 0.84 3.52 gamma 3.57|0.34|0.45
+ld_acc_brk landing Mean braking acceleration -1.33 -2 -0.66 norm -1.33|0.41
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/b788.csv b/data/performance/OpenAP/fixwing/wrap/b788.csv
deleted file mode 100644
index e26ccb73ee..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/b788.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,90.3,74.3,106.4,norm,90.3|11.1
-TO,to_d_tof,2.16,1.07,3.26,norm,2.16|0.67
-TO,to_acc_tof,1.61,1.15,2.07,norm,1.61|0.28
-IC,ic_va_avg,87,78,97,beta,3|4|69|39
-IC,ic_vz_avg,10.63,6.22,14.50,beta,7.70|6.24|-0.33|19.51
-CL,cl_d_range,297,150,446,norm,298|57
-CL,cl_v_cas_const,162,149,176,norm,162|8
-CL,cl_v_mach_const,0.85,0.80,0.89,norm,0.85|0.03
-CL,cl_h_cas_const,5.6,3.7,7.6,norm,5.6|1.0
-CL,cl_h_mach_const,11.0,8.5,12.2,beta,11.3|3.7|3.9|9.0
-CL,cl_vz_avg_pre_cas,9.95,7.48,12.42,norm,9.95|1.50
-CL,cl_vz_avg_cas_const,7.70,4.82,10.59,norm,7.70|1.75
-CL,cl_vz_avg_mach_const,4.86,2.46,7.27,norm,4.86|1.46
-CR,cr_d_range,918,231,11432,beta,1|1|189|12012
-CR,cr_v_cas_mean,132,125,146,gamma,7|117|2
-CR,cr_v_cas_max,140,129,166,gamma,5|118|4
-CR,cr_v_mach_mean,0.85,0.83,0.87,norm,0.85|0.01
-CR,cr_v_mach_max,0.87,0.85,0.91,gamma,20.73|0.79|0.00
-CR,cr_h_mean,12.0,11.1,12.9,norm,12.0|0.5
-CR,cr_h_max,12.3,11.5,13.1,norm,12.3|0.5
-DE,de_d_range,296,218,564,gamma,3|196|33
-DE,de_v_mach_const,0.83,0.79,0.88,norm,0.83|0.03
-DE,de_v_cas_const,155,140,170,norm,155|8
-DE,de_h_mach_const,10.2,8.5,11.8,norm,10.2|1.0
-DE,de_h_cas_const,7.2,4.1,9.6,beta,3.5|2.8|1.8|9.1
-DE,de_vz_avg_mach_const,-7.42,-13.94,-3.20,beta,3.86|2.59|-19.14|18.22
-DE,de_vz_avg_cas_const,-9.31,-13.84,-4.75,norm,-9.30|2.76
-DE,de_vz_avg_after_cas,-6.15,-7.94,-4.36,norm,-6.15|1.09
-FA,fa_va_avg,75,70,80,norm,75|3
-FA,fa_vz_avg,-3.91,-4.62,-3.19,norm,-3.90|0.44
-LD,ld_v_app,71.3,61.5,81.1,norm,71.3|6.8
-LD,ld_d_brk,2.08,0.45,3.71,norm,2.08|0.99
-LD,ld_acc_brk,-1.12,-1.79,-0.45,norm,-1.12|0.41
diff --git a/data/performance/OpenAP/fixwing/wrap/b788.txt b/data/performance/OpenAP/fixwing/wrap/b788.txt
new file mode 100644
index 0000000000..e4bfe60f98
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/b788.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 90.3 74.4 106.3 norm 90.35|11.10
+to_d_tof takeoff Takeoff distance 2.16 1.08 3.25 norm 2.16|0.66
+to_acc_tof takeoff Mean takeoff accelaration 1.61 1.16 2.07 norm 1.61|0.28
+ic_va_avg initial_climb Mean airspeed 87 78 97 beta 3.16|3.65|71.08|36.50
+ic_vs_avg initial_climb Mean vertical rate 10.65 6.27 14.45 beta 6.24|5.05|0.78|17.49
+cl_d_range climb Climb range 286 146 427 norm 287.05|54.42
+cl_v_cas_const climb Constant CAS 158 146 170 norm 158.68|7.34
+cl_v_mach_const climb Constant Mach 0.85 0.81 0.87 beta 14.41|4.76|0.69|0.21
+cl_h_cas_const climb Constant CAS crossover altitude 3.8 2.5 6.3 gamma 7.78|0.85|0.43
+cl_h_mach_const climb Constant Mach crossover altitude 9.5 8.4 10.6 norm 9.52|0.68
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 9.58 7.32 11.85 norm 9.58|1.38
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 9.02 6.12 11.93 norm 9.02|1.77
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 6.02 3.59 8.47 norm 6.03|1.48
+cr_d_range cruise Cruise range 1874 269 11619 beta 1.14|1.87|206.46|12053.57
+cr_v_cas_mean cruise Mean cruise CAS 132 126 146 gamma 6.84|119.57|2.28
+cr_v_cas_max cruise Maximum cruise CAS 139 130 164 gamma 4.29|122.70|5.16
+cr_v_mach_mean cruise Mean cruise Mach 0.85 0.83 0.87 norm 0.85|0.01
+cr_v_mach_max cruise Maximum cruise Mach 0.87 0.85 0.91 gamma 16.44|0.80|0.00
+cr_h_init cruise Initial cruise altitude 11.65 10.37 12.92 norm 11.65|0.78
+cr_h_mean cruise Mean cruise altitude 12.01 11.16 12.85 norm 12.01|0.51
+cr_h_max cruise Maximum cruise altitude 12.28 11.52 13.06 norm 12.29|0.47
+de_d_range descent Descent range 292 219 551 gamma 3.85|198.90|32.88
+de_v_mach_const descent Constant Mach 0.84 0.8 0.87 norm 0.84|0.02
+de_v_cas_const descent Constant CAS 153 140 167 norm 153.71|8.29
+de_h_mach_const descent Constant Mach crossover altitude 10.5 8.8 11.7 beta 3.43|2.59|7.57|4.76
+de_h_cas_const descent Constant CAS crossover altitude 7 4.4 9.7 norm 7.06|1.58
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -7.59 -14.16 -3.35 beta 3.41|2.37|-18.73|17.46
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.52 -13.91 -5.11 norm -9.51|2.67
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -6.2 -7.94 -4.45 norm -6.19|1.06
+fa_va_avg final_approach Mean airspeed 75 70 80 norm 75.58|3.36
+fa_vs_avg final_approach Mean vertical rate -3.99 -4.59 -3.11 gamma 24.44|-6.14|0.09
+fa_agl final_approach Approach angle 2.88 2.37 3.4 norm 2.88|0.31
+ld_v_app landing Touchdown speed 71.3 62 80.6 norm 71.29|6.48
+ld_d_brk landing Braking distance 2.08 0.45 3.71 norm 2.08|0.99
+ld_acc_brk landing Mean braking acceleration -1.12 -1.79 -0.46 norm -1.12|0.40
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/b789.csv b/data/performance/OpenAP/fixwing/wrap/b789.csv
deleted file mode 100644
index 25813109dd..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/b789.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,96.1,81.0,111.3,norm,96.1|10.5
-TO,to_d_tof,2.49,1.30,3.70,norm,2.50|0.73
-TO,to_acc_tof,1.63,1.19,2.06,norm,1.63|0.26
-IC,ic_va_avg,93,81,103,beta,5|4|64|50
-IC,ic_vz_avg,10.60,6.45,14.77,norm,10.61|2.53
-CL,cl_d_range,282,181,453,beta,3|6|156|399
-CL,cl_v_cas_const,167,152,181,beta,3|3|140|51
-CL,cl_v_mach_const,0.85,0.81,0.88,norm,0.85|0.02
-CL,cl_h_cas_const,5.7,3.9,7.5,norm,5.7|0.9
-CL,cl_h_mach_const,10.1,8.3,11.9,norm,10.1|0.9
-CL,cl_vz_avg_pre_cas,9.19,7.72,12.61,gamma,6.07|6.08|0.62
-CL,cl_vz_avg_cas_const,7.33,4.54,10.14,norm,7.34|1.70
-CL,cl_vz_avg_mach_const,4.88,2.35,7.42,norm,4.88|1.54
-CR,cr_d_range,5206,-3385,13821,norm,5218|3340
-CR,cr_v_cas_mean,139,129,149,norm,139|6
-CR,cr_v_cas_max,146,133,173,beta,4|15|121|138
-CR,cr_v_mach_mean,0.85,0.83,0.87,norm,0.85|0.01
-CR,cr_v_mach_max,0.88,0.85,0.92,gamma,13.22|0.80|0.01
-CR,cr_h_mean,11.6,10.7,12.5,norm,11.6|0.5
-CR,cr_h_max,12.0,11.1,12.7,beta,16.2|8.0|8.4|5.3
-DE,de_d_range,303,211,548,gamma,5|171|26
-DE,de_v_mach_const,0.83,0.79,0.88,norm,0.83|0.03
-DE,de_v_cas_const,155,139,171,norm,155|9
-DE,de_h_mach_const,10.6,8.0,11.6,beta,7.4|2.6|3.7|8.5
-DE,de_h_cas_const,6.8,4.2,9.5,norm,6.9|1.6
-DE,de_vz_avg_mach_const,-6.87,-12.78,-2.78,beta,3.52|2.56|-17.01|16.40
-DE,de_vz_avg_cas_const,-8.86,-13.44,-4.26,norm,-8.85|2.79
-DE,de_vz_avg_after_cas,-6.15,-7.95,-4.34,norm,-6.15|1.10
-FA,fa_va_avg,77,72,83,norm,77|3
-FA,fa_vz_avg,-4.11,-4.78,-3.11,gamma,24.35|-6.53|0.10
-LD,ld_v_app,74.0,64.8,83.3,norm,74.1|6.4
-LD,ld_d_brk,2.49,0.67,4.31,norm,2.49|1.11
-LD,ld_acc_brk,-1.11,-1.74,-0.47,norm,-1.11|0.39
diff --git a/data/performance/OpenAP/fixwing/wrap/b789.txt b/data/performance/OpenAP/fixwing/wrap/b789.txt
new file mode 100644
index 0000000000..248af707fc
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/b789.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 96.1 81.1 111.2 norm 96.15|10.46
+to_d_tof takeoff Takeoff distance 2.49 1.31 3.69 norm 2.50|0.72
+to_acc_tof takeoff Mean takeoff accelaration 1.63 1.2 2.06 norm 1.63|0.26
+ic_va_avg initial_climb Mean airspeed 92 81 103 beta 3.43|3.27|72.17|39.84
+ic_vs_avg initial_climb Mean vertical rate 10.6 6.48 14.74 norm 10.61|2.51
+cl_d_range climb Climb range 263 173 420 beta 2.91|5.02|156.74|333.07
+cl_v_cas_const climb Constant CAS 163 151 174 norm 163.14|7.01
+cl_v_mach_const climb Constant Mach 0.84 0.82 0.87 norm 0.84|0.02
+cl_h_cas_const climb Constant CAS crossover altitude 4.1 2.3 5.9 norm 4.10|1.07
+cl_h_mach_const climb Constant Mach crossover altitude 9.1 8.1 10.2 norm 9.14|0.66
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 9.46 7.18 11.75 norm 9.47|1.39
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 8.68 6.04 11.81 beta 3.89|4.77|3.94|10.92
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 6.05 3.63 8.47 norm 6.05|1.47
+cr_d_range cruise Cruise range 5695 -3140 14555 norm 5707.50|3434.94
+cr_v_cas_mean cruise Mean cruise CAS 139 129 149 norm 139.87|6.06
+cr_v_cas_max cruise Maximum cruise CAS 148 133 170 beta 2.67|4.04|125.61|63.39
+cr_v_mach_mean cruise Mean cruise Mach 0.85 0.83 0.87 norm 0.85|0.01
+cr_v_mach_max cruise Maximum cruise Mach 0.87 0.85 0.92 gamma 6.01|0.83|0.01
+cr_h_init cruise Initial cruise altitude 11.3 9.42 12.44 beta 4.59|2.75|7.61|5.49
+cr_h_mean cruise Mean cruise altitude 11.58 10.72 12.44 norm 11.58|0.52
+cr_h_max cruise Maximum cruise altitude 11.99 11.28 12.7 norm 11.99|0.43
+de_d_range descent Descent range 293 224 550 gamma 3.57|207.20|33.45
+de_v_mach_const descent Constant Mach 0.84 0.79 0.87 beta 7.21|3.08|0.71|0.17
+de_v_cas_const descent Constant CAS 154 139 169 norm 154.59|8.89
+de_h_mach_const descent Constant Mach crossover altitude 10.5 8.6 11.6 beta 3.21|2.22|7.41|4.75
+de_h_cas_const descent Constant CAS crossover altitude 7 4.3 9.6 norm 6.99|1.61
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -7.16 -12.88 -2.81 beta 2.83|2.31|-16.09|15.29
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.14 -13.41 -4.84 norm -9.13|2.61
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -6.19 -7.91 -4.46 norm -6.18|1.05
+fa_va_avg final_approach Mean airspeed 77 72 83 norm 77.88|3.71
+fa_vs_avg final_approach Mean vertical rate -4.15 -4.71 -3.15 gamma 11.92|-5.65|0.14
+fa_agl final_approach Approach angle 2.89 2.36 3.43 norm 2.89|0.32
+ld_v_app landing Touchdown speed 74 64.8 83.3 norm 74.07|6.42
+ld_d_brk landing Braking distance 2.49 0.68 4.31 norm 2.49|1.11
+ld_acc_brk landing Mean braking acceleration -1.11 -1.74 -0.47 norm -1.11|0.38
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/e190.csv b/data/performance/OpenAP/fixwing/wrap/e190.csv
deleted file mode 100644
index f3619f69eb..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/e190.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,88.1,78.6,97.6,norm,88.1|6.6
-TO,to_d_tof,1.80,1.18,2.43,norm,1.81|0.38
-TO,to_acc_tof,1.83,1.37,2.28,norm,1.83|0.28
-IC,ic_va_avg,78,65,91,norm,78|8
-IC,ic_vz_avg,11.19,5.76,16.64,norm,11.20|3.31
-CL,cl_d_range,225,165,388,gamma,5|140|18
-CL,cl_v_cas_const,142,131,158,gamma,20|107|1
-CL,cl_v_mach_const,0.75,0.69,0.82,norm,0.75|0.04
-CL,cl_h_cas_const,4.1,2.6,7.4,gamma,7.8|1.1|0.4
-CL,cl_h_mach_const,10.1,7.6,11.5,beta,12.6|4.3|2.5|9.8
-CL,cl_vz_avg_pre_cas,11.22,8.15,14.30,norm,11.22|1.87
-CL,cl_vz_avg_cas_const,7.99,5.25,10.73,norm,7.99|1.67
-CL,cl_vz_avg_mach_const,4.35,2.26,6.45,norm,4.35|1.27
-CR,cr_d_range,690,460,2710,gamma,1|437|324
-CR,cr_v_cas_mean,131,120,141,norm,131|6
-CR,cr_v_cas_max,133,125,153,gamma,5|117|3
-CR,cr_v_mach_mean,0.77,0.74,0.81,norm,0.77|0.02
-CR,cr_v_mach_max,0.80,0.76,0.85,norm,0.80|0.03
-CR,cr_h_mean,11.1,10.0,12.1,norm,11.1|0.6
-CR,cr_h_max,11.2,10.2,12.2,norm,11.2|0.6
-DE,de_d_range,255,169,443,beta,2|5|156|379
-DE,de_v_mach_const,0.77,0.70,0.83,norm,0.77|0.04
-DE,de_v_cas_const,146,134,159,norm,146|7
-DE,de_h_mach_const,9.1,7.4,10.8,norm,9.1|1.0
-DE,de_h_cas_const,5.4,2.7,8.2,norm,5.4|1.7
-DE,de_vz_avg_mach_const,-5.90,-11.66,-1.69,beta,9.15|5.35|-22.01|24.73
-DE,de_vz_avg_cas_const,-8.77,-13.14,-4.39,norm,-8.76|2.66
-DE,de_vz_avg_after_cas,-5.91,-7.97,-3.84,norm,-5.90|1.25
-FA,fa_va_avg,70,64,76,norm,70|4
-FA,fa_vz_avg,-3.56,-4.43,-2.68,norm,-3.55|0.53
-LD,ld_v_app,66.9,56.3,77.5,norm,66.9|7.4
-LD,ld_d_brk,1.84,0.73,3.73,beta,2.16|3.24|0.23|4.74
-LD,ld_acc_brk,-0.94,-1.92,-0.39,beta,5.77|2.95|-3.12|3.07
diff --git a/data/performance/OpenAP/fixwing/wrap/e190.txt b/data/performance/OpenAP/fixwing/wrap/e190.txt
new file mode 100644
index 0000000000..fc2637f3e7
--- /dev/null
+++ b/data/performance/OpenAP/fixwing/wrap/e190.txt
@@ -0,0 +1,36 @@
+variable flight phase name opt min max model parameters
+to_v_lof takeoff Liftoff speed 88.1 78.7 97.5 norm 88.13|6.52
+to_d_tof takeoff Takeoff distance 1.8 1.19 2.42 norm 1.81|0.38
+to_acc_tof takeoff Mean takeoff accelaration 1.83 1.38 2.28 norm 1.83|0.27
+ic_va_avg initial_climb Mean airspeed 78 66 91 norm 78.72|8.77
+ic_vs_avg initial_climb Mean vertical rate 11.19 5.78 16.62 norm 11.20|3.30
+cl_d_range climb Climb range 215 162 389 gamma 4.36|144.99|21.11
+cl_v_cas_const climb Constant CAS 140 133 156 gamma 8.78|122.65|2.36
+cl_v_mach_const climb Constant Mach 0.75 0.71 0.8 norm 0.75|0.03
+cl_h_cas_const climb Constant CAS crossover altitude 3 1.8 6.5 gamma 4.03|0.82|0.73
+cl_h_mach_const climb Constant Mach crossover altitude 9.2 8.1 10.2 norm 9.19|0.63
+cl_vs_avg_pre_cas climb Mean climb rate, pre-constant-CAS 10.44 7.18 13.71 norm 10.44|1.98
+cl_vs_avg_cas_const climb Mean climb rate, constant-CAS 8.93 6.27 11.6 norm 8.93|1.62
+cl_vs_avg_mach_const climb Mean climb rate, constant-Mach 4.82 2.95 6.7 norm 4.82|1.14
+cr_d_range cruise Cruise range 734 464 2710 gamma 1.98|433.55|308.41
+cr_v_cas_mean cruise Mean cruise CAS 131 120 141 norm 131.14|6.25
+cr_v_cas_max cruise Maximum cruise CAS 137 124 150 norm 137.30|7.93
+cr_v_mach_mean cruise Mean cruise Mach 0.77 0.74 0.81 norm 0.77|0.02
+cr_v_mach_max cruise Maximum cruise Mach 0.79 0.76 0.85 gamma 17.56|0.68|0.01
+cr_h_init cruise Initial cruise altitude 10.99 9.99 12 norm 11.00|0.61
+cr_h_mean cruise Mean cruise altitude 11.05 10.05 12.05 norm 11.05|0.61
+cr_h_max cruise Maximum cruise altitude 11.16 10.21 12.13 norm 11.17|0.58
+de_d_range descent Descent range 253 171 425 beta 2.17|3.85|161.90|315.91
+de_v_mach_const descent Constant Mach 0.77 0.72 0.81 norm 0.77|0.03
+de_v_cas_const descent Constant CAS 148 134 159 beta 3.94|3.27|124.00|42.94
+de_h_mach_const descent Constant Mach crossover altitude 9.3 8 10.6 norm 9.29|0.78
+de_h_cas_const descent Constant CAS crossover altitude 5.4 2.6 8.2 norm 5.42|1.72
+de_vs_avg_mach_const descent Mean descent rate, constant-Mach -5.25 -11.79 -2.1 beta 4.50|2.29|-18.06|17.51
+de_vs_avg_cas_const descent Mean descent rate, constant-CAS -9.08 -13.29 -4.85 norm -9.07|2.56
+de_vs_avg_after_cas descent Mean descent rate, after-constant-CAS -5.9 -7.92 -3.88 norm -5.90|1.23
+fa_va_avg final_approach Mean airspeed 70 64 76 norm 70.46|4.17
+fa_vs_avg final_approach Mean vertical rate -3.57 -4.34 -2.81 norm -3.57|0.47
+fa_agl final_approach Approach angle 2.92 2.38 3.46 norm 2.92|0.33
+ld_v_app landing Touchdown speed 66.9 56.6 77.2 norm 66.90|7.16
+ld_d_brk landing Braking distance 2.1 0.7 3.52 norm 2.11|0.86
+ld_acc_brk landing Mean braking acceleration -1.08 -1.8 -0.36 norm -1.08|0.44
\ No newline at end of file
diff --git a/data/performance/OpenAP/fixwing/wrap/f16.csv b/data/performance/OpenAP/fixwing/wrap/f16.csv
deleted file mode 100644
index 5deffeff35..0000000000
--- a/data/performance/OpenAP/fixwing/wrap/f16.csv
+++ /dev/null
@@ -1,34 +0,0 @@
-phase,param,opt,min,max,model,pm
-TO,to_v_lof,88.1,78.6,97.6,norm,
-TO,to_d_tof,1.80,1.18,2.43,norm,
-TO,to_acc_tof,1.83,1.37,2.28,norm,
-IC,ic_va_avg,78,65,91,norm,
-IC,ic_vz_avg,11.19,5.76,16.64,norm,
-CL,cl_d_range,225,165,388,norm,
-CL,cl_v_cas_const,142,131,158,norm,
-CL,cl_v_mach_const,0.75,0.69,0.82,norm,
-CL,cl_h_cas_const,4.1,2.6,7.4,norm,
-CL,cl_h_mach_const,10.1,7.6,11.5,norm,
-CL,cl_vz_avg_pre_cas,11.22,8.15,14.30,norm,
-CL,cl_vz_avg_cas_const,7.99,5.25,10.73,norm,
-CL,cl_vz_avg_mach_const,4.35,2.26,6.45,norm,
-CR,cr_d_range,900,500,2000,norm,
-CR,cr_v_cas_mean,600,600,600,norm,
-CR,cr_v_cas_max,800,800,800,norm,
-CR,cr_v_mach_mean,1,1,1,norm,
-CR,cr_v_mach_max,2,2,2,norm,
-CR,cr_h_mean,10,10,10,norm,
-CR,cr_h_max,15,15,15,norm,
-DE,de_d_range,255,169,443,norm,
-DE,de_v_mach_const,0.77,0.70,0.83,norm,
-DE,de_v_cas_const,146,134,159,norm,
-DE,de_h_mach_const,9.1,7.4,10.8,norm,
-DE,de_h_cas_const,5.4,2.7,8.2,norm,
-DE,de_vz_avg_mach_const,-5.90,-11.66,-1.69,norm,
-DE,de_vz_avg_cas_const,-8.77,-13.14,-4.39,norm,
-DE,de_vz_avg_after_cas,-5.91,-7.97,-3.84,norm,
-FA,fa_va_avg,70,64,76,norm,
-FA,fa_vz_avg,-3.56,-4.43,-2.68,norm,
-LD,ld_v_app,66.9,56.3,77.5,norm,
-LD,ld_d_brk,1.84,0.73,3.73,norm,
-LD,ld_acc_brk,-0.94,-1.92,-0.39,norm,
diff --git a/docs/python_demo.ipynb b/docs/python_demo.ipynb
new file mode 100644
index 0000000000..52e27ffd59
--- /dev/null
+++ b/docs/python_demo.ipynb
@@ -0,0 +1,328 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Bluesky interfacing demo\n",
+ "\n",
+ "This Jupyter Notebook demonstrates the use of Bluesky as a Python package and how it can be used to generate aircraft trajectories.\n",
+ "\n",
+ "**Note:** Make sure that you have Bluesky installed as a package in your Python environment.\n",
+ "\n",
+ "Let's start by importing the relevant modules."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Using Python-based geo functions\n"
+ ]
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "\n",
+ "import bluesky as bs\n",
+ "from bluesky.simulation import ScreenIO\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We create a dummy class that acts as the screen of BlueSky. Since we don't want to actually load the screen from BlueSky here, a simple and small class is used instead to avoid errors when something within BlueSky is calling the *echo* function."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class ScreenDummy(ScreenIO):\n",
+ " \"\"\"\n",
+ " Dummy class for the screen. Inherits from ScreenIO to make sure all the\n",
+ " necessary methods are there. This class is there to reimplement the echo\n",
+ " method so that console messages are printed.\n",
+ " \"\"\"\n",
+ " def echo(self, text='', flags=0):\n",
+ " \"\"\"Just print echo messages\"\"\"\n",
+ " print(\"BlueSky console:\", text)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The first step to take is to initialise bluesky (here imported as *bs*) as a disconnected, single simulation.\n",
+ "Next we replace the screen object with our derived variant so that bluesky console messages are printed."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Reading config from settings.cfg\n",
+ "Creating directory \"scenario\", and copying default files\n",
+ "Creating directory \"plugins\", and copying default files\n",
+ "Reading magnetic variation data\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Unable to copy \"scenario\" files to \"scenario\"\n",
+ "Unable to copy \"plugins\" files to \"plugins\"\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Loading global navigation database...\n",
+ "Reading cache: data/cache/py3/navdata.p\n",
+ "Error loading plugin: plugin AREA not found.\n",
+ "Error loading plugin: plugin DATAFEED not found.\n"
+ ]
+ }
+ ],
+ "source": [
+ "# initialize bluesky as non-networked simulation node\n",
+ "bs.init('sim-detached')\n",
+ "\n",
+ "# initialize dummy screen\n",
+ "bs.scr = ScreenDummy()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now that everything is initialized, we can get started and and generate some traffic. Here, we generate 3 aircraft of the type *A320*."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# generate some trajectories\n",
+ "n = 3\n",
+ "\n",
+ "# create n aircraft with random positions, altitudes, speeds\n",
+ "bs.traf.mcre(n, actype=\"A320\")\n",
+ "\n",
+ "# alternative: individually initialize each aircraft by passing the initial\n",
+ "# position, heading, altitude, and speed.\n",
+ "# bs.traf.cre(acid=acids, actype=actypes, aclat=aclats, aclon=aclons,\n",
+ "# achdg=achdgs, acalt=acalts, acspd=acspds)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The traffic in this example are not given any initial conditions such as initial position and velocity. This is not needed when the traffic departs at an airport. You can also pass the initial conditions as additional parameters to the funciton if you want to initialize the traffic in flight.\n",
+ "\n",
+ "Next we want to assign some waypoints to the traffic. In this example we assign the same route to all the flights. This is just for the sake of simplicity and it obviously wouldn't make a lot of sense in a practical application, but it should highlight how waypoints are added."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# iterate over traffic and add the same waypoints\n",
+ "# Note that preferably, all simulation commands are initiated through the stack\n",
+ "# however, if you wish, you can also call the functions directly, such as the\n",
+ "# mcre command in the above cell.\n",
+ "for acid in bs.traf.id:\n",
+ " # set the origin (not needed if initialized in flight),\n",
+ " # and add some waypoints, here only the altitude (in m) is passed to the\n",
+ " # function, but you can additionally pass a speed as well\n",
+ " # finally turn on VNAV for each flight\n",
+ " bs.stack.stack(f'ORIG {acid} EGLL;'\n",
+ " f'ADDWPT {acid} BPK FL60;'\n",
+ " f'ADDWPT {acid} TOTRI FL107;'\n",
+ " f'ADDWPT {acid} MATCH FL115;'\n",
+ " f'ADDWPT {acid} BRAIN FL164;'\n",
+ " f'VNAV {acid} ON')\n",
+ "\n",
+ " # you can also set the way the waypoint should be flown\n",
+ " # bs.stack.stack(f'ADDWPT {acid} FLYOVER')\n",
+ "\n",
+ " # you can also set a destination\n",
+ " # bs.stack.stack(f'DEST {acid} EHAM')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now that all our traffic has a route to fly, it's time to start the simulation."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "BlueSky console: Base dt set to 1.0\n",
+ "performance dt is unchanged.\n",
+ "asas dt is unchanged.\n"
+ ]
+ }
+ ],
+ "source": [
+ "# set simulation time step, and enable fast-time running\n",
+ "bs.stack.stack('DT 1;FF')\n",
+ "\n",
+ "# we'll run the simulation for up to 4000 seconds\n",
+ "t_max = 4000\n",
+ "\n",
+ "ntraf = bs.traf.ntraf\n",
+ "n_steps = int(t_max + 1)\n",
+ "t = np.linspace(0, t_max, n_steps)\n",
+ "\n",
+ "# allocate some empty arrays for the results\n",
+ "res = np.zeros((n_steps, 4, ntraf))\n",
+ "\n",
+ "# iteratively simulate the traffic\n",
+ "for i in range(n_steps):\n",
+ " # Perform one step of the simulation\n",
+ " bs.sim.step()\n",
+ "\n",
+ " # save the results from the simulator in the results array,\n",
+ " # here we keep the latitude, longitude, altitude and TAS\n",
+ " res[i] = [bs.traf.lat,\n",
+ " bs.traf.lon,\n",
+ " bs.traf.alt,\n",
+ " bs.traf.tas]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Finally, we do a bit of plotting to visualize the results. Again, the three trajectories are the same since we passed the same route to them, but this, of course, can be easily changed."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# plot\n",
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "for idx, acid in enumerate(bs.traf.id):\n",
+ " fig = plt.figure(figsize=(10, 15))\n",
+ " ax1 = plt.subplot2grid((4, 1), (0, 0), rowspan=2)\n",
+ " ax2 = plt.subplot2grid((4, 1), (2, 0))\n",
+ " ax3 = plt.subplot2grid((4, 1), (3, 0))\n",
+ "\n",
+ " ax1.plot(res[:, 1, idx], res[:, 0, idx])\n",
+ "\n",
+ " ax2.plot(t, res[:, 2, idx])\n",
+ " ax2.set_xlabel('t [s]')\n",
+ " ax2.set_ylabel('alt [m]')\n",
+ "\n",
+ " ax3.plot(t, res[:, 3, idx])\n",
+ " ax3.set_xlabel('t [s]')\n",
+ " ax3.set_ylabel('TAS [m/s]')\n",
+ " \n",
+ " fig.suptitle(f'Trajectory {acid}')\n",
+ "\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "interpreter": {
+ "hash": "ea503857f36d21b450d152aa103b3c6b332184b2225df50c3964ea06a7d8f5ea"
+ },
+ "kernelspec": {
+ "display_name": "Python 3.6.15 64-bit",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.15"
+ },
+ "orig_nbformat": 4
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/extra/CRmethods/Difgame.py b/extra/CRmethods/Difgame.py
index 18f66a4204..e84bb4dd6c 100644
--- a/extra/CRmethods/Difgame.py
+++ b/extra/CRmethods/Difgame.py
@@ -108,7 +108,7 @@ def resolve(dbconf, traf):
np.where(bankcontrol>0,traf.trk+90,traf.trk-90))%360
# Set bank angle for aircraft in conflict
- traf.aphi=np.radians(np.where(dbconf.asasactive,np.abs(bankcontrol),25.))
+ traf.ap.aphi=np.radians(np.where(dbconf.asasactive,np.abs(bankcontrol),25.))
# Change autopilot desired altitude
traf.aalt=np.where(climbcontrol==0,traf.alt,\
diff --git a/plugins/trafgenclasses.py b/plugins/trafgenclasses.py
index c4a4eeae2f..5287dd88a7 100644
--- a/plugins/trafgenclasses.py
+++ b/plugins/trafgenclasses.py
@@ -5,7 +5,7 @@
from bluesky import stack,traf,sim,tools,navdb
from bluesky.tools.position import txt2pos
from bluesky.tools.geo import kwikqdrdist,kwikpos,latlondist,qdrdist
-from bluesky.tools.misc import degto180,txt2alt,txt2tas
+from bluesky.tools.misc import degto180,txt2alt,txt2spd
from bluesky.tools.aero import nm,ft
# Default values
@@ -163,11 +163,11 @@ def setalt(self,cmdargs):
def setspd(self,cmdargs):
if len(cmdargs)==1:
- spd = txt2tas(cmdargs[0])
+ spd = txt2spd(cmdargs[0])
self.startspdmin = spd
self.startapdmax = spd
elif len(cmdargs)>1:
- spd0,spd1 = txt2tas(cmdargs[0]),txt2tas(cmdargs[1])
+ spd0,spd1 = txt2spd(cmdargs[0]),txt2spd(cmdargs[1])
self.startspdmin = min(spd0,spd1)
self.startspdmax = max(spd0,spd1)
else:
@@ -597,11 +597,11 @@ def setalt(self,cmdargs):
def setspd(self,cmdargs):
if len(cmdargs)==1:
- spd = txt2tas(cmdargs[0])
+ spd = txt2spd(cmdargs[0])
self.startspdmin = spd
self.startapdmax = spd
elif len(cmdargs)>1:
- spd0,spd1 = txt2tas(cmdargs[0]),txt2tas(cmdargs[1])
+ spd0,spd1 = txt2spd(cmdargs[0]),txt2spd(cmdargs[1])
self.startspdmin = min(spd0,spd1)
self.startspdmax = max(spd0,spd1)
else:
diff --git a/scenario/LNAV_VNAV/LNAV-VNAV-TwoRoutesFlights.scn b/scenario/LNAV_VNAV/LNAV-VNAV-TwoRoutesFlights.scn
index 2febbdad13..3c3dd8bf2b 100644
--- a/scenario/LNAV_VNAV/LNAV-VNAV-TwoRoutesFlights.scn
+++ b/scenario/LNAV_VNAV/LNAV-VNAV-TwoRoutesFlights.scn
@@ -3,12 +3,13 @@
# Test VNAV mode
# Land in EHAM, runway 06
-00:00:00.00>CRE KL204,A320,EHAM,RWY18L,*,0,0
+00:00:00.00>CRE KL204,A320,EHAM,RWY18L,,0,0
00:00:00.00>ORIG KL204 EHAM RWY18L
00:00:00.00>DEST KL204 EHBK RWY21
00:00:00.00>SPD KL204 250
00:00:00.00>ALT KL204 3000
00:00:00.00>ADDWPT KL204 NV
+00:00:00.00>KL204 AT NV STACK KL204 VNAV ON
00:00:00.00>ADDWPT KL204 RR, FL50
00:00:00.00>ADDWPT KL204 EHN, 3000
#0:00:00.00>ADDWPT KL204 THN, 2000,200
@@ -21,7 +22,7 @@
# A flight from EHAM 18L to Madrid (LEMD)
-00:00:00.00>CRE KLM1705,A320,EHAM,RWY18L,*,0,0
+00:00:00.00>CRE KLM1705,A320,EHAM,RWY18L,,0,0
00:00:00.00>SPD KLM1705 250
00:00:00.00>ALT KLM1705 300
@@ -34,15 +35,16 @@
00:00:00.00>KLM1705 ADDWPT 52'12'51,004'48'28
00:00:00.00>KLM1705 ADDWPT LEKKO FL100
-00:00:00.00>KLM1705 ADDWPT LARAS
+00:00:00.00>KLM1705 AT LEKKO STACK KLM1705 VNAV ON
+00:00:00.00>KLM1705 ADDWPT LARAS,,275
00:00:00.00>KLM1705 ADDWPT WOODY
00:00:00.00>KLM1705 ADDWPT NIK
00:00:00.00>KLM1705 ADDWPT DENOX
00:00:00.00>KLM1705 ADDWPT CIV,,0.80
00:00:00.00>KLM1705 ADDWPT MEDIL
-00:00:00.00>KLM1705 ADDWPT KOVIN,FL350
+00:00:00.00>KLM1705 ADDWPT KOVIN,FL330
00:00:00.00>KLM1705 ADDWPT PON
-00:00:00.00>KLM1705 ADDWPT DITAL,FL370
+00:00:00.00>KLM1705 ADDWPT DITAL,FL350
00:00:00.00>KLM1705 ADDWPT GODIX
00:00:00.00>KLM1705 ADDWPT DIDAK
00:00:00.00>KLM1705 ADDWPT KURIS
@@ -52,7 +54,7 @@
00:00:00.00>KLM1705 ADDWPT NEA
# ToD
-00:00:00.00>KLM1705 ADDWPT ORBIS,FL190,250
+00:00:00.00>KLM1705 ADDWPT ORBIS,FL100,250
00:00:00.00>KLM1705 ADDWPT LEMD RW18R
@@ -65,6 +67,6 @@
# Switch VNAV on
#(LNAV is already on due to ADDWPT)
-#00:00:00.00>KLM1705 VNAV on
-#00:00:00.00>pos klm1705
+00:00:00.00>KLM1705 VNAV on
+00:00:00.00>pos klm1705