#!/usr/bin/python
import argparse
import re
import numpy as np
from enum import Enum
from pathlib import Path
from aviary.api import NamedValues
from aviary.utils.csv_data_file import write_data_file
from aviary.utils.functions import get_path
[docs]
class CodeOrigin(Enum):
FLOPS = 'FLOPS'
GASP = 'GASP'
allowed_headers = {
'altitude': 'Altitude',
'alt': 'Altitude',
'alpha': 'Angle of Attack',
'mach': 'Mach',
'delflp': 'Flap Deflection',
'cltot': 'CL',
'cl': 'CL',
'cd': 'CD',
'hob': 'Hob',
'del_cl': 'Delta CL',
'del_cd': 'Delta CD'
}
[docs]
def AeroDataConverter(input_file=None, output_file=None, data_format=None):
"""This is a utility class to convert a legacy aero data file to Aviary format.
There are two options for the legacy aero data file format: FLOPS and GASP.
As an Aviary command, the usage is:
aviary convert_aero_table -F {FLOPS|GASP} input_file output_file
"""
data_format = CodeOrigin(data_format)
data_file = get_path(input_file)
if isinstance(output_file, str):
output_file = Path(output_file)
elif isinstance(output_file, list):
for ii, file in enumerate(output_file):
output_file[ii] = Path(file)
if not output_file:
if data_format is CodeOrigin.GASP:
# Default output file name is same location and name as input file, with
# '_aviary' appended to filename
path = data_file.parents[0]
name = data_file.stem
suffix = data_file.suffix
output_file = path / (name + '_aviary' + suffix)
elif data_format is CodeOrigin.FLOPS:
# Default output file name is same location and name as input file, with
# '_aviary' appended to filename
path = data_file.parents[0]
name = data_file.stem
suffix = data_file.suffix
file1 = path / name + '_aviary_CDI' + suffix
file2 = path / name + '_aviary_CD0' + suffix
output_file = [file1, file2]
stamp = f'# {data_format.value}-derived aerodynamics data converted from {data_file.name}'
if data_format is CodeOrigin.GASP:
data, comments = _load_gasp_aero_table(data_file)
comments = [stamp] + comments
write_data_file(output_file, data, comments, include_timestamp=True)
elif data_format is CodeOrigin.FLOPS:
if type(output_file) is not list:
# if only one filename is given, split into two
path = output_file.parents[0]
name = output_file.stem
suffix = output_file.suffix
file1 = path / (name + '_CDi' + suffix)
file2 = path / (name + '_CD0' + suffix)
output_file = [file1, file2]
lift_drag_data, lift_drag_comments, \
zero_lift_drag_data, zero_lift_drag_comments = _load_flops_aero_table(
data_file)
# write lift-dependent drag file
lift_drag_comments = [stamp] + lift_drag_comments
write_data_file(output_file[0], lift_drag_data,
lift_drag_comments, include_timestamp=True)
# write zero-lift drag file
zero_lift_drag_comments = [stamp] + zero_lift_drag_comments
write_data_file(output_file[1], zero_lift_drag_data,
zero_lift_drag_comments, include_timestamp=True)
def _load_flops_aero_table(filepath: Path):
"""Load an aero table in FLOPS format"""
def _read_line(line_count, comments):
line = file_contents[line_count].strip()
items = re.split(r'[\s]*\s', line)
if items[0] == '#':
comments.append(line)
nonlocal offset
offset += 1
try:
items = _read_line(line_count+1, comments)
except IndexError:
return
else:
# try to convert line to float
try:
items = [float(var) for var in items]
# data contains things other than floats
except (ValueError):
raise ValueError(
f'Non-numerical value found in data file <{filepath.name}> on '
f'line {str(line_count)}')
return items
lift_drag = []
lift_drag_data = NamedValues()
lift_drag_comments = []
zero_lift_drag = []
zero_lift_drag_data = NamedValues()
zero_lift_drag_comments = []
file_contents = []
with open(filepath, 'r') as reader:
for line in reader:
file_contents.append(line)
offset = 0
# these are not needed, we can determine the length of data vectors directly
lift_drag_mach_count, cl_count = _read_line(0 + offset, lift_drag_comments)
lift_drag_machs = _read_line(1 + offset, lift_drag_comments)
cls = _read_line(2 + offset, lift_drag_comments)
lift_drag = []
for i in range(len(lift_drag_machs)):
drag = _read_line(3 + i + offset, lift_drag_comments)
if len(drag) == len(cls):
lift_drag.append(drag)
else:
raise ValueError('Number of data points provided for '
f'lift-dependent drag at Mach {lift_drag_machs[i]} '
'does not match number of CLs provided '
f'(FLOPS aero data file {filepath.name})')
if len(lift_drag) != len(lift_drag_machs):
raise ValueError('Number of data rows provided for lift-dependent drag does '
'not match number of Mach numbers provided (FLOPS aero data '
f'file {filepath.name})')
offset = offset + i
# these are not needed, we can determine the length of data vectors directly
altitude_count, zero_lift_mach_count = _read_line(
4 + offset, zero_lift_drag_comments)
zero_lift_machs = _read_line(5 + offset, zero_lift_drag_comments)
altitudes = _read_line(6 + offset, zero_lift_drag_comments)
for i in range(len(zero_lift_machs)):
drag = _read_line(7 + i + offset, zero_lift_drag_comments)
if len(drag) == len(altitudes):
zero_lift_drag.append(drag)
else:
raise ValueError('Number of data points provided for '
f'zero-lift drag at Mach {zero_lift_machs[i]} '
'does not match number of Altitudes provided '
f'(FLOPS aero data file {filepath.name})')
if len(zero_lift_drag) != len(zero_lift_machs):
raise ValueError('Number of data rows provided for zero-lift drag does '
'not match number of Mach numbers provided (FLOPS aero data '
f'file {filepath.name})')
cl, mach = np.meshgrid(cls, lift_drag_machs)
lift_drag_data.set_val('Mach', mach.flatten(), 'unitless')
lift_drag_data.set_val('Lift Coefficient', cl.flatten(), 'unitless')
lift_drag_data.set_val('Lift-Dependent Drag Coefficient',
np.array(lift_drag).flatten(), 'unitless')
altitude, mach = np.meshgrid(altitudes, zero_lift_machs)
zero_lift_drag_data.set_val('Altitude', altitude.flatten(), 'ft')
zero_lift_drag_data.set_val('Mach', mach.flatten(), 'unitless')
zero_lift_drag_data.set_val('Zero-Lift Drag Coefficient',
np.array(zero_lift_drag).flatten(), 'unitless')
return lift_drag_data, lift_drag_comments, zero_lift_drag_data, zero_lift_drag_comments
def _load_gasp_aero_table(filepath: Path):
"""Load an aero table in GASP format"""
data = NamedValues()
raw_data = []
comments = []
variables = []
units = []
read_header = True
read_units = False
with open(filepath, 'r') as reader:
for line_count, line in enumerate(reader):
# ignore empty lines
if not line or line.strip() == ['']:
continue
if line[0] == '#':
line = line.strip('# ').strip()
if read_header:
items = re.split(r'[\s]*\s', line)
if all(name.lower() in allowed_headers for name in items):
variables = [name for name in items]
read_header = False
read_units = True
else:
if line:
comments.append(line)
else:
if read_units:
items = re.split(r'[\s]*\s', line)
for item in items:
item = item.strip('()')
if item == '-':
item = 'unitless'
units.append(item)
continue
else:
# this is data
items = re.split(r'[\s]*\s', line.strip())
# try to convert line to float
try:
line_data = [float(var) for var in items]
# data contains things other than floats
except (ValueError):
raise ValueError(
f'Non-numerical value found in data file <{filepath.name}> on '
f'line {str(line_count)}')
else:
raw_data.append(line_data)
raw_data = np.array(raw_data)
# translate raw data into NamedValues object
for idx, var in enumerate(variables):
if var.lower() in allowed_headers:
data.set_val(allowed_headers[var.lower()], raw_data[:, idx], units[idx])
return data, comments
def _setup_ATC_parser(parser):
parser.add_argument('input_file', type=str,
help='path to aero data file to be converted')
parser.add_argument('output_file', type=str, nargs='?',
help='path to file where new converted data will be written')
parser.add_argument('-f', '--data_format', type=str, choices=[origin.value for origin in CodeOrigin],
help='data format used by input_file')
def _exec_ATC(args, user_args):
AeroDataConverter(
input_file=args.input_file,
output_file=args.output_file,
data_format=args.data_format
)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Converts FLOPS- or GASP-formatted '
'aero data files into Aviary csv format.\n')
_setup_ATC_parser(parser)
args = parser.parse_args()
_exec_ATC(args, None)