Skip to content
Snippets Groups Projects
Commit fdbe49bd authored by Christopher Ruwisch's avatar Christopher Ruwisch Committed by Florian Schültke
Browse files

[FEATURE] Python packages for working with avl and unicado

parent fc9b664b
No related branches found
No related tags found
1 merge request!42[FEATURE] Python packages for working with avl and unicado
Showing
with 3162 additions and 0 deletions
# Add the package to the package list for exporting the target
# and propagate the resulting list back to the parent scope
list( APPEND PYTHON_TARGETS ${CMAKE_CURRENT_LIST_DIR} )
set( PYTHON_TARGETS ${PYTHON_TARGETS} PARENT_SCOPE )
install(DIRECTORY ${CMAKE_CURRENT_LIST_DIR} DESTINATION lib)
# PYAVL Package - Python package for working with AVL (Athena vortex lattice)
## Usage
Used for Interfacing AVL
## License
This project is licensed under the GNU General Public License, Version 3
- see the License.md file for details.
## Contact
For questions or feedback, please contact contacts@unicado.io
# UNICADO - UNIversity Conceptual Aircraft Design and Optimization
#
# Copyright (C) 2025 UNICADO consortium
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Description:
# This file is part of UNICADO.
from .pyavlcore import *
from .pyavlfileread import *
from .pyavlfilewrite import *
from .pyavlrunner import *
\ No newline at end of file
# UNICADO - UNIversity Conceptual Aircraft Design and Optimization
#
# Copyright (C) 2025 UNICADO consortium
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Description:
# This file is part of UNICADO.
"""
PyAvl Core classes
"""
from .pyavlfilewrite import *
from .pyavlfileread import *
from ambiance import Atmosphere
"""
Geometry classes (.avl)
"""
class Header:
def __init__(self, runcase: str="base", mach: float=0.0, sym: list=[0, 0, 0.0], base: list[float]=[1.0, 1.0, 1.0], ref: list[float]=[0.0, 0.0, 0.0]):
""" Initialize avl header - please see avl primer
Args:
runcase (str, optional): Runcase name. Defaults to "base".
mach (float, optional): mach number. Defaults to 0.0.
sym (list, optional): symmetry elements according to AVL. Defaults to [0, 0, 0.0].
base (list[float], optional): Basic aerodynamic parameters (Sref, cref, bref). Defaults to [1.0, 1.0, 1.0].
ref (list[float], optional): Reference cg point. Defaults to [0.0, 0.0, 0.0].
"""
self.runcase = runcase
self.mach = mach
self.sym = sym
self.base = base
self.ref = ref
self.author = ""
def write(self, fh) -> None:
""" Write data to file via fh -> filehandler
Args:
fh (): filehandler/filedescriptor
"""
write_separation_big(fh, "BASE FILE")
write_headline(fh, "Geometric Name")
write_elem_tab(fh, self.runcase, 2)
write_headline(fh, "Mach (normal to wing)")
write_elem_tab(fh, self.mach, 2)
write_headline(fh, "Symmetric properties (iYsym, iZsym, Zsym)")
write_elem_tab(fh, self.sym, 2)
write_headline(fh, "Reference properties Sref, Cref, Bref")
write_elem_tab(fh, self.base, 2)
write_headline(fh, "Reference CG position (X,Y,Z)")
write_elem_tab(fh, self.ref, 2)
write_separation_big(fh, "Begin of Geometry")
class Control:
def __init__(self, tag="ctrl", gain=1.0, xhinge=0.7, xyzhvec=[0.0, 1.0, 0.0], sgndup=1.0):
self.key = "CONTROL"
self.tag = tag
self.gain = gain
self.xhinge = xhinge
self.xyzhvec = xyzhvec
self.sgndup = sgndup
self.description = ["!name", "gain", "xhinge", "XYZhvec", "SgnDup"]
def write(self, fh) -> None:
""" Write data to file via fh -> filehandler
Args:
fh (): filehandler/filedescriptor
"""
write_key(fh, self.key)
write_elem_tab(fh, self.description, 1)
write_elem_tab(fh, self.tag)
write_elem_tab(fh, self.gain)
write_elem_tab(fh, self.xhinge)
write_elem_tab(fh, self.xyzhvec)
write_elem_tab(fh, self.sgndup, 2)
class Section:
def __init__(self, refLE: list[float]=[0.0, 0.0, 0.0], chord: float=1.0, dihedral: float=0.0, nspan: int=1, sspace: float=1.0, afileKey: str="NACA",
afile: str="0012"):
""" Section class
Args:
refLE (list[float], optional): Position of section reference point (leading edge). Defaults to [0.0, 0.0, 0.0].
chord (float, optional): chord length. Defaults to 1.0.
dihedral (float, optional): dihedral. Defaults to 0.0.
nspan (int, optional): number of spanwise horseshoe vortices. Defaults to 1.
sspace (float, optional): spanwise vortex spacing. Defaults to 1.0.
afileKey (str, optional): airfoil file key (NACA or AFILE). Defaults to "NACA".
afile (str, optional): for key NACA -> enter 4 digit naca profile, otherwise if AFILE -> enter path to airfoil. Defaults to "0012".
"""
self.key = "SECTION"
self.ref = refLE
self.chord = chord
self.dihedral = dihedral
self.nspan = nspan
self.sspace = sspace
self.afileKey = afileKey
self.afile = afile
self.controls = []
self.description = ["!Xle", "Yle", "Zle", "Chord", "dihedral", "nspan", "sspace"]
def add_control_device(self, control: Control) -> None:
"""
Args:
control (Control): Add control object
"""
self.controls.append(control)
def write(self, fh) -> None:
""" Write data to file via fh -> filehandler
Args:
fh (): filehandler/filedescriptor
"""
write_separation_small(fh, "Section")
write_key(fh, self.key)
write_elem_tab(fh, self.description, 1)
write_elem_tab(fh, self.ref)
write_elem_tab(fh, self.chord)
write_elem_tab(fh, self.dihedral)
write_elem_tab(fh, self.nspan)
write_elem_tab(fh, self.sspace, 2)
write_afile(fh, self.afileKey, self.afile)
write_new_line(fh)
for control in self.controls:
control.write(fh)
class Yduplicate:
def __init__(self, ydupl: float=0.0):
""" Yduplicate
Args:
ydupl (float, optional): Y postion of X-Z plane -> only use if iYsym is 0. Defaults to 0.0.
"""
self.key = "YDUPLICATE"
self.ydupl = ydupl
self.description = ["!YDuplicate - Geometric Symmetry, non aerodynamic symmetry"]
def write(self, fh) -> None:
""" Write yduplicate
Args:
fh (): filehandler/filedescriptor
"""
write_key(fh, self.key)
write_elem_tab(fh, self.description, 1)
write_elem_tab(fh, self.ydupl, 2)
class Surface:
def __init__(self, tag: str="surf", nchord: int=10, cspace: float=1.0, nspan: int=20, sspace: float=1.0):
""" General surface
Args:
tag (str, optional): Surface tag. Defaults to "surf".
nchord (int, optional): Number of chordwise horseshoe vortices. Defaults to 10.
cspace (float, optional): Chordwise vortex spacing parameter. Defaults to 1.0.
nspan (int, optional): Number of spanwise horseshoe vortices. Defaults to 20.
sspace (float, optional): Spanwise vortex spacing. Defaults to 1.0.
"""
self.key = "SURFACE"
self.tag = tag
self.nchord = nchord
self.cspace = cspace
self.nspan = nspan
self.sspace = sspace
self.yduplicates = []
self.angles = []
self.scales = []
self.translates = []
self.sections = []
def add_yduplicate(self, ydupl) -> None:
""" Adds yduplicate object
Args:
ydupl (object): yduplicate object
"""
self.yduplicates.append(ydupl)
def add_angle(self, angle) -> None:
""" Adds angle object
Args:
angle (object): angle object
"""
self.angles.append(angle)
def add_scale(self, scale) -> None:
""" Adds scale object
Args:
scale (object): scale object
"""
self.scales.append(scale)
def add_translate(self, translate) -> None:
""" Add translate object
Args:
translate (object): translate object
"""
self.translates.append(translate)
def add_section(self, section) -> None:
""" Add section object
Args:
section (object): section object
"""
self.sections.append(section)
def write(self, fh) -> None:
""" Write data to file via fh -> filehandler
Args:
fh (): filehandler/filedescriptor
"""
write_separation_big(fh, f'Begin of Surface [{self.tag}]')
write_key(fh, self.key)
write_tag(fh, self.tag)
write_elem_tab(fh, self.nchord)
write_elem_tab(fh, self.cspace)
write_elem_tab(fh, self.nspan)
write_elem_tab(fh, self.sspace, 2)
for ydl in self.yduplicates:
ydl.write(fh)
for angle in self.angles:
angle.write(fh)
for scale in self.scales:
scale.write(fh)
for translate in self.translates:
translate.write(fh)
for section in self.sections:
section.write(fh)
write_separation_big(fh, f'End of Surface [{self.tag}]')
class Scale:
def __init__(self, xscale: float=1.0, yscale: float=1.0, zscale: float=1.0):
""" Scale object
Args:
xscale (float, optional): x-scale factor. Defaults to 1.0.
yscale (float, optional): y-scale factor. Defaults to 1.0.
zscale (float, optional): z-scale factor. Defaults to 1.0.
"""
self.key = "SCALE"
self.xscale = xscale
self.yscale = yscale
self.zscale = zscale
self.description = ["!Xscale", "Yscale", "Zscale"]
def write(self, fh) -> None:
""" Write to file
Args:
fh (): filehandler/filedescriptor
"""
write_key(fh, self.key)
write_elem_tab(fh, self.description, 1)
write_elem_tab(fh, self.xscale)
write_elem_tab(fh, self.yscale)
write_elem_tab(fh, self.zscale, 2)
class Translate:
def __init__(self, dX: float=0.0, dY: float=0.0, dZ: float=0.0):
""" Translate object (right hand coordinate system starting in nose through back (x),
left (y), up (z))
Args:
dX (float, optional): x translation. Defaults to 0.0.
dY (float, optional): y translation. Defaults to 0.0.
dZ (float, optional): z translation. Defaults to 0.0.
"""
self.key = "TRANSLATE"
self.dX = dX
self.dY = dY
self.dZ = dZ
self.description = ["!dX", "dY", "dZ"]
def write(self, fh) -> None:
""" Write to file
Args:
fh (): filehandler/filedescriptor
"""
write_key(fh, self.key)
write_elem_tab(fh, self.description, 1)
write_elem_tab(fh, self.dX)
write_elem_tab(fh, self.dY)
write_elem_tab(fh, self.dZ, 2)
class Angle:
def __init__(self, dAinc: float=0.0) -> None:
""" Angle object
Args:
dAinc (float, optional): Offset added on to the Ainc values (deg). Defaults to 0.0.
"""
self.key = "ANGLE"
self.dAinc = dAinc
self.description = ["!Dihedral"]
def write(self, fh) -> None:
""" Write to file
Args:
fh (): filehandler/filedescriptor
"""
write_key(fh, self.key)
write_elem_tab(fh, self.description, 1)
write_elem_tab(fh, self.dAinc, 1)
"""
Runcase class (.run)
"""
class Runcase:
def __init__(self, alpha: list[float]=[0.0], beta: float=0.0, mach: float=0.0, cgID: int=1, cgpos: list[float]=[0.0, 0.0, 0.0], height: float=11000.0,
aileron: float=0.0,
elevator: float=0.0, rudder: float=0.0):
""" Initialize a single runcase for avl -> inputs for .run file
Args:
alpha (list[float], optional): angle of attack. Defaults to [0.0].
mach (float, optional): mach number. Defaults to 0.0.
beta (float, optional): angle. Defaults to 0.0.
cgID (int, optional): center of gravity id (depends on user). Defaults to 1.
cgpos (list[float], optional): cgpos w.r.t. cgID. Defaults to [0.0, 0.0, 0.0].
height (list[float], optional): height/altitude. Defaults to [11000.0].
aileron (float, optional): aileron (roll) control surface deflection. Defaults to 0.0.
elevator (float, optional): elevator (pitch) control surface deflection. Defaults to 0.0.
rudder (float, optional): rudder (yaw) control surface deflection. Defaults to 0.0.
"""
atmo = Atmosphere(height)
if alpha is None:
alpha = [0.0]
self.alpha = alpha
self.beta = beta
self.mach = mach
self.aileron = aileron
self.elevator = elevator
self.rudder = rudder
self.cgID = cgID
self.cgpos = cgpos
self.density = float(atmo.density[0])
self.a = float(atmo.speed_of_sound[0])
self.numOfRuncases = len(alpha)
self.cases = []
self.caseName = None
def create_runcase_fileName(self) -> str:
""" Create runcase filename
Returns:
str -- runcase filename
"""
strmach = f'mach{self.mach}_'
strcg = f'cgID{self.cgID}'
s = "Runcase_" + strmach + strcg
s = s.split('.')
s = '_'.join(s)
return s
def create_runcase_file(self,id: int) -> None:
""" Creates a .run file based on a Runcase
Args:
id (int): Runcase file id
"""
self.caseName = self.create_runcase_fileName()
idx = 1
with open(self.caseName + ".run", "w") as file:
for a in self.alpha:
self.cases.append(f'file_id{id+idx}')
write_runcase(file, idx, self.cases[-1], alpha=a, beta=self.beta, mach=self.mach, density=self.density,
a=self.a, aileron=self.aileron, elevator=self.elevator,
rudder=self.rudder, cgref=self.cgpos)
idx += 1
def get_runcases(self) -> list:
""" Returns list of runcases
Returns:
list: list of runcases
"""
return self.cases
def get_runcase_filename(self) -> str:
""" Get runcase filename
Returns:
str: runcase filename
"""
return self.caseName
# UNICADO - UNIversity Conceptual Aircraft Design and Optimization
#
# Copyright (C) 2025 UNICADO consortium
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Description:
# This file is part of UNICADO.
"""
Python AVL Class for Reading Data
This module provides classes to process AVL stability files and extract relevant aerodynamics data.
"""
import numpy as np
import re
import csv
import math
class PyAvlStabilityFilesConvert:
"""
Converts AVL stability files to a structured format and writes them to a CSV file.
Attributes:
filenames (list): List of AVL stability filenames.
datatable (numpy.ndarray): Table of extracted data.
keys (list): List of keys corresponding to the data columns.
stabfiles (list): List of PyAvlStabilityFile objects.
keyListControls (list, optional): List of control keys.
"""
def __init__(self, filenames=[], keyListControls=None, fileout="default.csv"):
"""
Initializes the PyAvlStabilityFilesConvert class and processes the given stability files.
Args:
filenames (list): List of filenames to process.
keyListControls (list, optional): List of control keys.
fileout (str): Output CSV filename.
"""
filenames.sort()
self.filenames = filenames
self.datatable = None
self.keys = None
self.stabfiles = []
self.keyListControls = keyListControls
for filename in self.filenames:
self.stabfiles.append(PyAvlStabilityFile(filename, keylistControls=keyListControls))
with open(fileout, "w", newline='') as file:
if self.keys is None:
self.keys = self.stabfiles[0].get_keys()
writer = csv.DictWriter(file, fieldnames=self.keys)
writer.writeheader()
datatable = []
for stabfile in self.stabfiles:
writer.writerow(stabfile.get_data())
tmp = stabfile.get_data()
datatable.append(list(tmp.values()))
self.datatable = np.array(datatable)
def get_keys(self):
"""Returns the list of keys from the stability files."""
return self.keys
def get_data(self):
"""Returns the numerical data extracted from the stability files."""
return self.datatable
def get_data_by_key(self,key):
"""
Retrieves data for a specific key.
Args:
key (str): Key for which data is requested.
Returns:
numpy.ndarray or None: Data for the given key, or None if not found.
"""
try:
key_index = self.keys.index(key)
return self.datatable[:,key_index]
except ValueError:
return None
def get_data_by_control_key(self,key: str ,controlkey: str, exact: bool=False):
"""
Retrieves data based on control key modifications.
Args:
key (str): The aerodynamic coefficient key.
controlkey (str): The control surface key.
exact (bool, optional): Whether to look for an exact match of the control key.
Returns:
numpy.ndarray or None: The extracted data.
"""
available_keys = ["CL", "CY", "Cl", "Cm", "Cn","CDff"]
try:
# Get key in available key list -> set to 1 since zero would be direct control key
available_key_index = available_keys.index(key) + 1
if not exact:
control_key_indices = [idx + available_key_index for idx, item in enumerate(self.keys) if item.startswith(controlkey)]
# if more than one key index is available
if len(control_key_indices) > 1:
# Combine data
return np.sum([self.datatable[:, idx]*math.degrees(1) for idx in control_key_indices], axis=1)
else:
# else
return self.datatable[:, control_key_indices[0]]*math.degrees(1)
else:
control_key_index = self.keys.index(controlkey) + available_key_index
return self.datatable[:, control_key_index]*math.degrees(1)
except ValueError:
return None
class PyAvlStabilityFiles:
"""
Manages multiple AVL stability files.
Attributes:
filenames (list): List of AVL stability filenames.
stabfiles (list): List of processed stability file data.
keys (list): List of data keys.
ctrlKeys (list, optional): List of control keys.
"""
def __init__(self,filenames=[], keyListControls=None):
"""
Initializes the class and processes the given stability files.
Args:
filenames (list): List of filenames.
keyListControls (list, optional): List of control keys.
"""
self.filenames = filenames
self.stabfiles = []
self.keys = None
self.ctrlKeys = keyListControls
for filename in self.filenames:
stabilityfile = PyAvlStabilityFile(filename, keylistControls=keyListControls)
if self.keys is None:
self.keys = stabilityfile.get_keys()
self.stabfiles.append(stabilityfile.get_data())
def get_data(self):
"""Returns the data extracted from the stability files."""
return self.stabfiles
def get_keys(self):
"""Returns the list of keys from the stability files."""
return self.keys
class PyAvlStabilityFile:
"""
Represents an AVL stability file.
Attributes:
filename (str): Name of the AVL stability file.
keylist (list): List of extracted data keys.
keylistControls (list): List of control keys.
data (dict): Extracted data from the file.
"""
def __init__(self, filename, keylistControls=None):
"""
Initializes the stability file by parsing its contents.
Args:
filename (str): Filename of the AVL stability file.
keylistControls (list, optional): List of control keys.
"""
self.filename = filename
self.keylist = self.init_dictionary()
self.keylistControls = self.set_control_keys(keylistControls)
self.keyControlNums = []
self.data = self.fill_dictionary()
def get_keys(self):
"""Returns the list of keys in the stability file."""
return self.keylist
def get_data(self):
"""Returns the extracted data from the stability file."""
return self.data
def init_dictionary(self):
"""Initializes the dictionary with predefined aerodynamic data keys."""
keylist = []
keylist += ["Altitude","Sref", "Cref", "Bref"]
keylist += ["Xref", "Yref", "Zref", "Xnp"]
keylist += ["Alpha", "Beta", "Mach", "pb/2V", "qc/2V", "rb/2V"]
keylist += ["CLtot","CYtot", "CDtot", "CDvis", "CDind", "Cltot", "Cmtot", "Cntot"]
namederiv = ["CL", "CY", "Cl", "Cm", "Cn"]
addderiv = ["a", "b", "p", "q", "r"]
for id in addderiv:
keylist += [val + id for val in namederiv]
return keylist
def fill_dictionary(self):
"""
Reads the AVL stability file and fills the data dictionary.
Returns:
dict: Extracted aerodynamic data mapped to their respective keys.
"""
with open(self.filename, "r") as file:
lines = file.readlines()
addderiv = []
namederiv = ["CL", "CY", "Cl", "Cm", "Cn","CDff"]
for ctrlkey in self.keylistControls:
id = self.get_control_device_num(lines, ctrlkey)
self.keylist.append(ctrlkey)
self.keylist += [val + id for val in namederiv]
data = dict.fromkeys(self.keylist, None)
for key in self.keylist:
if key == "Altitude":
data["Altitude"] = self.get_ft_from_runcase(lines)
else:
data[key] = self.get_x_value(lines, key)
return data
def set_control_keys(self, controlnames=None):
"""Sets the control keys."""
controlnames = list(controlnames)
keylistControls = []
if controlnames is None or type(controlnames) is not type([]):
return keylistControls
else:
keylistControls = controlnames
return keylistControls
def get_x_value(self, lines, key):
"""
Extracts the numerical value associated with a given key from the file.
Args:
lines (list): Lines from the AVL stability file.
key (str): The key whose value needs to be extracted.
Returns:
float or None: Extracted numerical value or None if not found.
"""
searchObj = re.compile(rf'{key} * = *.\d*.\d*')
for line in lines:
res = searchObj.search(line)
if res is not None:
ret = res.group().split("=")
return float(ret[1])
def get_control_device_num(self, lines, ctrlkey):
"""
Retrieves the control device number for a given control key.
Args:
lines (list): Lines from the AVL stability file.
ctrlkey (str): The control key whose number is needed.
Returns:
str: Control device number as a string.
"""
searchObj = re.compile(rf'{ctrlkey[:]} * d\d*.\d*')
for line in lines:
res = searchObj.search(line)
if res is not None:
ret = res.group().split()
return ret[1]
def get_ft_from_runcase(self,lines):
"""
Extracts altitude information from the run case.
Args:
lines (list): Lines from the AVL stability file.
Returns:
float: Altitude value extracted from the run case.
"""
searchObj = re.compile(rf'Run case: RunCase \d* - \d*.\d*')
for line in lines:
res = searchObj.search(line)
if res is not None:
ret = res.group().split("-")
return float(ret[-1])
def write(self, fileout):
"""
Writes the extracted data to a CSV file.
Args:
fileout (str): The output filename without extension.
"""
with open(fileout + ".csv", "w", newline='') as file:
writer = csv.DictWriter(file, fieldnames=self.keylist)
writer.writeheader()
writer.writerow(self.data)
# UNICADO - UNIversity Conceptual Aircraft Design and Optimization
#
# Copyright (C) 2025 UNICADO consortium
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Description:
# This file is part of UNICADO.
"""
Python AVL Class for Reading Data
This module provides classes to process AVL stability files and extract relevant aerodynamics data.
"""
def write_elem_new_line(fh, val):
"""
Writes a value to the file and adds a new line.
Args:
fh (file object): File handle.
val (str): Value to write.
"""
if len(val):
fh.write(f'{val}\n')
def write_elem_tab(fh, elems, newline=0):
"""
Writes elements separated by spaces and optionally adds a new line.
Args:
fh (file object): File handle.
elems (list or str): Elements to write.
newline (int, optional): Number of new lines to add.
"""
if type(elems) is list:
for elem in elems:
fh.write(f'{elem} ')
else:
fh.write(f'{elems} ')
write_new_line(fh, newline)
def write_key(fh, key):
"""Writes a key as a new line."""
write_elem_new_line(fh, key)
def write_tag(fh, tag):
"""Writes a tag as a new line."""
write_elem_new_line(fh, tag)
def write_afile(fh, key, afile):
"""
Writes a key followed by a filename as new lines.
Args:
fh (file object): File handle.
key (str): Key to write.
afile (str): Associated filename.
"""
write_elem_new_line(fh, key)
write_elem_new_line(fh, f'{afile}')
def write_commentline(list, commentstring):
"""
Adds a commented line to a list.
Args:
lst (list): List to append the comment.
commentstring (str): Comment text.
"""
commentline = "# " + commentstring + "\n"
list.append(commentline)
def write_separation_big(file, token=""):
"""
Writes a large separation line with an optional token.
Args:
file (file object): File handle.
token (str, optional): Text to include in the separation.
"""
sepString = "# =="
if len(token):
sepString += f' {token} '
lsepString = len(sepString)
for i in range(0, 80 - lsepString):
sepString += "="
sepString += "\n"
file.write(sepString)
def write_separation_small(file, token=""):
"""
Writes a small separation line with an optional token.
Args:
file (file object): File handle.
token (str, optional): Text to include in the separation.
"""
sepString = "# --"
if len(token):
sepString += f' {token} '
lsepString = len(sepString)
for i in range(0, 80 - lsepString):
sepString += "-"
sepString += "\n"
file.write(sepString)
def write_headline(file, short=""):
"""Writes a headline in a formatted manner."""
file.write(f'# -- {short} --\n')
def write_new_line(fh, num=1):
"""
Writes the specified number of new lines.
Args:
fh (file object): File handle.
num (int): Number of new lines to write.
"""
crnl = ""
for i in range(0, num):
crnl += "\n"
fh.write(crnl)
def write_avl_file(author="", datalist=[], filename="filename", postfix=".avl"):
"""
Writes AVL data to a specified file.
Args:
author (str, optional): Author name.
datalist (list): Data elements to write.
filename (str): Base filename.
postfix (str, optional): File extension.
"""
with open(filename + postfix, "w") as avlFile:
for item in datalist:
item.write(avlFile)
def write_runcase(file, runcasenum=1, runcasename="default", alpha=0.0, beta=0.0, pb2v=0.0, qc2v=0.0, rb2v=0.0,
controls=None, mach=0.0, CD0=0.02, density=1.225,a=340.294, cgref=[0.0, 0.0, 0.0]):
"""
Writes a run case block to the file.
Args:
file (file object): File handle.
runcasenum (int, optional): Run case number.
runcasename (str, optional): Run case name.
alpha (float, optional): Angle of attack.
beta (float, optional): Sideslip angle.
pb2v, qc2v, rb2v (float, optional): Non-dimensional rotational rates.
controls (list, optional): List of control surfaces.
mach (float, optional): Mach number.
CD0 (float, optional): Zero-lift drag coefficient.
density (float, optional): Air density.
a (float, optional): Speed of sound.
cgref (list, optional): Center of gravity reference coordinates.
"""
file.write(f' ---------------------------------------------\n')
file.write(f' Run case {runcasenum}: {runcasename}\n')
file.write(f' alpha -> alpha = {alpha}\n')
file.write(f' beta -> beta = {beta}\n')
file.write(f' pb/2V -> pb/2V = {pb2v}\n')
file.write(f' qc/2V -> qc/2V = {qc2v}\n')
file.write(f' rb/2V -> rb/2V = {rb2v}\n')
if controls is not None and isinstance(controls,list):
for control in controls:
file.write(f' {control}\t\t->{control}\t\t=\t{0.0}\n')
# if aileron is not None:
# file.write(f' aileron -> aileron = {aileron}\n')
# if elevator is not None:
# file.write(f' elevator -> elevator = {elevator}\n')
# if rudder is not None:
# file.write(f' rudder -> rudder = {rudder}\n')
file.write("\n")
default = 0.0
velocity = mach*a[0]
file.write(f' alpha = {alpha} deg\n')
file.write(f' beta = {beta} deg\n')
file.write(f' pb/2V = {default}\n')
file.write(f' qc/2V = {default}\n')
file.write(f' rb/2V = {default}\n')
file.write(f' CL = {default}\n')
file.write(f' CDo = {CD0}\n')
file.write(f' bank = {default} deg\n')
file.write(f' elevation = {default} deg\n')
file.write(f' heading = {default} deg\n')
file.write(f' Mach = {mach}\n')
file.write(f' velocity = {velocity:.4f} Lunit/Tunit\n')
file.write(f' density = {density[0]:.4f} Munit/Lunit^3\n')
file.write(f' grav.acc. = {9.80665} Lunit/Tunit^2\n')
file.write(f' turn_rad. = {default} Lunit\n')
file.write(f' load_fac. = {1.00000}\n')
file.write(f' X_cg = {cgref[0]} Lunit\n')
file.write(f' Y_cg = {cgref[1]} Lunit\n')
file.write(f' Z_cg = {cgref[2]} Lunit\n')
file.write(f' mass = {1.00000} Munit\n')
file.write(f' Ixx = {1.00000} Munit-Lunit^2\n')
file.write(f' Iyy = {1.00000} Munit-Lunit^2\n')
file.write(f' Izz = {1.00000} Munit-Lunit^2\n')
file.write(f' Ixy = {0.00000} Munit-Lunit^2\n')
file.write(f' Iyz = {0.00000} Munit-Lunit^2\n')
file.write(f' Izx = {0.00000} Munit-Lunit^2\n')
file.write(f' visc CL_a = {0.00000}\n')
file.write(f' visc CL_u = {0.00000}\n')
file.write(f' visc CM_a = {0.00000}\n')
file.write(f' visc CM_u = {0.00000}\n\n')
# UNICADO - UNIversity Conceptual Aircraft Design and Optimization
#
# Copyright (C) 2025 UNICADO consortium
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Description:
# This file is part of UNICADO.
"""
PyAvl - A Python to AVL conversion script for generating lifting surfaces based on given geometry.
This module provides utilities to run Athena Vortex Lattice (AVL) simulations and install AVL/XFOIL tools.
"""
import os
import subprocess
import platform
import urllib.request
import shutil
import stat
class PyAvlRunner:
"""
Handles execution of AVL simulations, manages run cases, and processes stability results.
"""
def __init__(self,avlFile=None,runcasefile=None,runcases=["default"]):
"""
Initializes the PyAvlRunner class.
Args:
avlFile (str, optional): Path to AVL geometry file.
runcasefile (str, optional): Path to AVL run case file.
runcases (list, optional): List of run case names.
"""
self.avlproc = None
self.cmdline = ""
self.avlFile = avlFile
self.runcaseFile = runcasefile
self.runcases = runcases
self.count = 1
self.path_to_session_folder = None
self.path_to_stability_files = None
self.path_to_runcase_files = None
def session_setup(self):
"""
Sets up the AVL session with geometry and run cases.
"""
# Load geometry and case
self.cmdline += f"load {self.avlFile}\n"
self.cmdline += f"case {self.runcaseFile}.run\n"
# Switch to operation mode
self.cmdline += f"oper\n"
# Select runcase
for id, runcase in enumerate(self.runcases):
self.cmdline += f"{id+1}\n"
self.cmdline += "x\n"
self.cmdline += f"st\n{runcase}_{id}\n"
self.cmdline += "\nquit\n"
def session_setup_single_rc(self, path_to_session_folder, runcase_files_folder, stability_files_folder, results_folder, runcase_ids):
# Setup paths to session and resulting files
self.path_to_session_folder = path_to_session_folder
self.path_to_runcase_files = runcase_files_folder
self.path_to_stability_files = stability_files_folder
self.path_to_session_result_file = results_folder
# Load geometry and case
self.cmdline += f"load {path_to_session_folder}/{self.avlFile}\n"
for _, id in enumerate(runcase_ids):
self.cmdline += f"case {runcase_files_folder}/rc_{id:03}.run\n"
# Switch to operation mode
self.cmdline += f"oper\n"
self.cmdline += f"I\n"
self.cmdline += "x\n"
self.cmdline += f"st {stability_files_folder}/res_{id:03}.stab\n"
self.cmdline += "\n"
self.cmdline += "quit\n"
return self.cmdline
def run_avl_session(self, cmdline=None):
"""
Executes the AVL session.
Args:
cmdline (str, optional): Custom command sequence for AVL.
"""
if cmdline is None:
cmdline = self.cmdline
print("Computing aerodynamics with Athena Vortex Lattice (AVL) ... ",end="")
process = self.avl_process()
process.communicate(input=cmdline.encode())
process.wait()
print("finished")
def avl_process(self, path_to_avl_executable: str="."):
"""
Initializes the AVL process.
Args:
path_to_avl_executable (str, optional): Path to the AVL executable.
Returns:
subprocess.Popen: AVL process.
"""
avl_executable = "avl"
if platform.system() == "Windows":
avl_executable = "avl.exe"
return subprocess.Popen(args=[f'{path_to_avl_executable}/{avl_executable}'],
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
bufsize=0)
def session_End(self):
"""End avl session"""
self.avlproc.terminate()
self.avlproc.kill()
def session_folder(self):
"""Return session folder path"""
return self.path_to_session_folder
def stability_files_folder(self):
"""Returns stability files folder path"""
return self.path_to_stability_files
def runcase_files_folder(self):
"""Returns runcases files folder path"""
return self.path_to_runcase_files
def result_files_folder(self):
"""Returns result file folder path"""
return self.path_to_session_result_file
class AVLinstaller:
"""
Downloads and installs AVL based on the user's operating system.
"""
def __init__(self):
"""Initializes AVLinstaller with appropriate download URLs."""
self.avl_url = {"root": "http://web.mit.edu/drela/Public/web/avl/",
"Windows": "avl3.40_execs/WIN64/avl.exe",
"Darwin-x86_64": "avl3.40_execs/DARWIN64/avl",
"Darwin-arm64": "avl3.40_execs/DARWINM1/avl",
"Linux": "avl3.40_execs/LINUX64/avl"}
self.operating_system: str=self.check_os()
self.machine_architecture: str=self.check_architecture()
self.avl_download_link: str=None
def check_os(self) -> str:
"""Returns the operating system name."""
return platform.system()
def check_architecture(self) -> str:
"""Returns the system architecture."""
return platform.machine()
def set_avl_download_link(self) -> None:
"""Determines the correct AVL download link based on the OS and architecture."""
if self.operating_system == "Darwin":
self.operating_system += "-" + self.machine_architecture
self.avl_download_link = self.avl_url["root"] + self.avl_url[self.operating_system]
def is_command_installed(self, command):
"""Determines if command is installed correctly"""
return shutil.which(command) is not None
def download_avl(self) -> bool:
"""Downloads and installs AVL if not already installed."""
print("Checking AVL ...")
executable = "avl"
if self.operating_system == "Windows":
executable += ".exe"
path_to_executable = "./" + executable
if self.is_command_installed(path_to_executable) or os.path.exists(path_to_executable):
print("AVL is already installed.")
return True
self.set_avl_download_link()
print("Downloading AVL ...")
print(self.avl_download_link)
print(os.getcwd())
urllib.request.urlretrieve(self.avl_download_link, os.getcwd()+"/" +executable)
if self.is_command_installed(path_to_executable) or os.path.exists(path_to_executable):
print("AVL is installed.")
print("Changing rights")
os.chmod(path_to_executable, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
return True
else:
return False
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "pyavl"
version = "1.0.0"
description = "Simple python avl interface to build .avl files and runcase files."
authors = [{ name = "Christopher Ruwisch", email = "christopher.ruwisch@tu-berlin.de" }]
readme = "README.md"
license = { file = "LICENSE" }
keywords = ["avl"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: OS Independent",
]
dependencies = [
"numpy>=1.26.4",
]
[tool.setuptools]
packages = ["pyavl"]
[project.urls]
homepage = "https://unicado.pages.rwth-aachen.de/unicado.gitlab.io/"
repository = "https://git.rwth-aachen.de/unicado/libraries"
\ No newline at end of file
# Add the package to the package list for exporting the target
# and propagate the resulting list back to the parent scope
list( APPEND PYTHON_TARGETS ${CMAKE_CURRENT_LIST_DIR} )
set( PYTHON_TARGETS ${PYTHON_TARGETS} PARENT_SCOPE )
install(DIRECTORY ${CMAKE_CURRENT_LIST_DIR} DESTINATION lib)
# PYAVLUNICADO Package - Python package for working with AVL (Athena vortex lattice) in combination with Unicado
## Usage
Used for Interfacing AVL and UNICADO to compute aerodynamic derivatives
## License
This project is licensed under the GNU General Public License, Version 3
- see the License.md file for details.
## Contact
For questions or feedback, please contact contacts@unicado.io
# UNICADO - UNIversity Conceptual Aircraft Design and Optimization
#
# Copyright (C) 2025 UNICADO consortium
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Description:
# This file is part of UNICADO.
File added
This diff is collapsed.
# UNICADO - UNIversity Conceptual Aircraft Design and Optimization
#
# Copyright (C) 2025 UNICADO consortium
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Description:
# This file is part of UNICADO.
"""
PyAvlUnicado - A Python Interface for Athena Vortex Lattice (AVL) for use with UNICADO
This module provides a Python interface for generating and executing AVL simulations. It includes:
- Geometry initialization and manipulation
- Run case setup and execution
- Interaction with AVL for aerodynamic analysis
Features:
- Reads and processes aircraft geometry
- Manages aerodynamic surfaces and control devices
- Creates AVL input files for simulations
- Automates AVL run case execution and result extraction
Dependencies:
- numpy
- ambiance
- pyaircraftgeometry2
- pyavl
"""
import pyaircraftgeometry2 as geom2
import pyavl as avl
import os
import shutil
from .avlunicadogeometry import AerodynamicSurface
from .avlunicadoutility import load_aerodynamic_surfaces
from ambiance import Atmosphere
import numpy as np
class Avlinterface:
"""
Manages the interface for AVL simulations, including geometry initialization and run case management.
"""
aerodynamic_surfaces = None
aerodynamic_components: list=[]
aerodynamic_reference_data: list=[]
aerodynamic_surface_control_keys = set()
paths: str=None
aircraft_name: str=None
runcases: list=[]
derivative_data = None
none_high_lift_devices: list = ["aileron", "elevator", "rudder"]
def __init__(self, paths: str = None, aircraft_name: str = "default_ac") -> None:
"""
Initializes the Avlinterface class.
Args:
paths (str, optional): Path to aircraft geometry files.
aircraft_name (str, optional): Name of the aircraft model.
"""
self.paths = paths
self.aircraft_name = aircraft_name
def initialize_geometry(self, component_list: list[str] = [], use_control_devices=False, use_high_lift_devices=False):
"""
Initializes aerodynamic geometry based on input components.
Args:
component_list (list, optional): List of components to include.
use_control_devices (bool, optional): Whether to include control devices.
use_high_lift_devices (bool, optional): Whether to include high lift devices.
"""
# clean up aerodynamic components
if self.aerodynamic_components:
self.aerodynamic_components.clear()
# Load aerodynamic surfaces from acxml
self.aerodynamic_surfaces = load_aerodynamic_surfaces(self.paths, component_list)
# Read reference data (S_ref, mac, Span)
if "wing" in self.aerodynamic_surfaces:
reference_area: float = 0
reference_mac: float = 0
reference_span: float = 0
for aerodynamic_surface in self.aerodynamic_surfaces["wing"]["aerodynamic_surfaces"]:
current_wing = aerodynamic_surface["geometry"]
current_area = geom2.measure.reference_area(current_wing)
# select reference values from biggest area
if current_area > reference_area:
reference_area = current_area
reference_mac = geom2.measure.mean_aerodynamic_chord(current_wing)
reference_span = geom2.measure.span(current_wing)
reference_phi_25 = -geom2.measure.sweep(current_wing, 0.25*reference_span, 0.25)
self.aerodynamic_reference_data = [reference_area, reference_mac, reference_span, reference_phi_25]
for component in self.aerodynamic_surfaces:
# Read reference position
component_reference_position = self.aerodynamic_surfaces[component]["reference_position"]
for aerodynamic_surface in self.aerodynamic_surfaces[component]["aerodynamic_surfaces"]:
# If use_high_lift_devices -> False ... only none_high_lift_devices are used for geometry, else all elements are used
# WARNING -> if all devices are used, make sure that they are not too close to eachother, otherwise avl has issues with computation
if not use_high_lift_devices:
aerodynamic_surface["control_devices"] = [control for control in aerodynamic_surface["control_devices"] if control.name in self.none_high_lift_devices]
self.aerodynamic_components.append(AerodynamicSurface(self.paths, self.aircraft_name).build(
component_reference_position, aerodynamic_surface, use_control_devices))
for control in aerodynamic_surface["control_devices"]:
self.aerodynamic_surface_control_keys.add(control.name)
def create_avl_geometry_file(self, author: str = "", filename: str = "", runcasename: str = "default", mach_number: float = 0.78, reference_data: list[float] = None, reference_cog: list[float] = None):
"""
Creates an AVL geometry input file.
"""
if filename == "":
filename = f"{self.aircraft_name}/{self.aircraft_name}"
else:
filename = f"{self.aircraft_name}/{filename}"
if reference_data is None:
reference_data = self.aerodynamic_reference_data
if reference_cog is None:
reference_cog = [0., 0., 0.]
print(f"Reference center of gravity is None ... set to {reference_cog}")
header_avl_file = [avl.Header(runcase=runcasename, mach=mach_number,
sym=[0, 0, 0.0], base=reference_data[:3], ref=reference_cog)]
avl.write_avl_file(author, header_avl_file + self.aerodynamic_components, filename)
def add_runcase(self, runcase):
""" Add runcase
Args:
runcase (object): Runcase
"""
if not isinstance(runcase, AvlRuncase):
print("Runcase will not be appended - no AvlRuncase...")
else:
self.runcases.append(runcase)
def number_of_runcases(self):
"""Return numer of runcases"""
return len(self.runcases)
def runcase_ids(self):
"""Return list of runcase ids"""
return [rc.runcase_id for rc in self.runcases]
def define_runcase_batch(self, runcase_folder: str=".", alphas: list[float]=[0.0], betas: list[float]=[0.0], pb2vs: list[float]=[0.0], qc2vs: list[float]=[0.0], rb2vs: list[float]=[0.0], mach_numbers: list[float]=[0.0], CD0: float=0.02, heights_in_m: list[float]=[0.0], cog: list[float]=[0.0,0.0,0.0], controls: list[str]=[], clear_runcase_folder: bool=True):
"""
Defines a batch of AVL run cases by varying aerodynamic parameters.
Args:
runcase_folder (str, optional): Path to store run case files. Defaults to ".".
alphas (list[float], optional): List of angle of attack values. Defaults to [0.0].
betas (list[float], optional): List of sideslip angle values. Defaults to [0.0].
pb2vs (list[float], optional): List of roll rate values. Defaults to [0.0].
qc2vs (list[float], optional): List of pitch rate values. Defaults to [0.0].
rb2vs (list[float], optional): List of yaw rate values. Defaults to [0.0].
mach_numbers (list[float], optional): List of Mach numbers. Defaults to [0.0].
CD0 (float, optional): Zero-lift drag coefficient. Defaults to 0.02.
heights_in_m (list[float], optional): List of altitude values in meters. Defaults to [0.0].
cog (list[float], optional): Center of gravity coordinates [x, y, z]. Defaults to [0.0, 0.0, 0.0].
controls (list[str], optional): List of control surface names. Defaults to an empty list.
clear_runcase_folder (bool, optional): Whether to clear the run case folder before creation. Defaults to True.
"""
runcase_id = self.number_of_runcases() + 1
print("Clear existing runcases ...")
self.runcases.clear()
if os.path.isdir(runcase_folder) and clear_runcase_folder:
if not self.is_directory_empty(runcase_folder):
self.empty_directory(runcase_folder)
# Create list from set
if len(controls) == 0:
controls=list(self.aerodynamic_surface_control_keys)
# Create different runcase batch
for mach_number, height_in_m in zip(mach_numbers,heights_in_m):
for rb2v in rb2vs:
for qc2v in qc2vs:
for pb2v in pb2vs:
for beta in betas:
for alpha in alphas:
self.runcases.append(AvlRuncase(runcase_folder, runcase_id, f"RunCase {runcase_id} - {height_in_m:.2f}", alpha, beta, pb2v, qc2v, rb2v, mach_number*np.cos(self.aerodynamic_reference_data[3]), CD0, height_in_m, cog, controls).create())
runcase_id += 1
print(f"Current number of Runcases: ... {self.number_of_runcases()}")
def run_avl(self, runcase_folder, stability_files_folder, results_folder, derivative_collection_file=None):
"""
Executes an AVL simulation using the specified run cases and stability file storage.
Args:
runcase_folder (str): Path to the folder containing AVL run case files.
stability_files_folder (str): Path to the folder where AVL stability results will be stored.
results_folder (str): Path to the folder where final results will be saved.
derivative_collection_file (str, optional): Name of the output CSV file to store aerodynamic derivatives.
Returns:
avl.PyAvlStabilityFilesConvert or None: Returns processed aerodynamic derivatives if `derivative_collection_file` is provided, otherwise None.
"""
geometry_file_name = f"{self.aircraft_name}.avl"
runner = avl.pyavlrunner.PyAvlRunner(geometry_file_name)
runner.session_setup_single_rc(path_to_session_folder=self.aircraft_name, runcase_files_folder=runcase_folder, stability_files_folder=stability_files_folder, results_folder=results_folder, runcase_ids=self.runcase_ids())
runner.run_avl_session()
if isinstance(derivative_collection_file, str):
os.makedirs(results_folder, exist_ok=True)
stability_files = [f"{runner.stability_files_folder()}/{file}" for file in os.listdir(runner.stability_files_folder()) if file.endswith(".stab")]
self.derivative_data = avl.PyAvlStabilityFilesConvert(filenames=stability_files,
keyListControls=list(self.aerodynamic_surface_control_keys), fileout=f"{results_folder}/{derivative_collection_file}.csv")
return self.derivative_data
def is_directory_empty(self,directory):
"""Check whether a directory is empty or ot"""
return not any(os.scandir(directory))
def empty_directory(self,directory):
""" Empty directory
Args:
directory (str): removes the content of this directory
"""
for item in os.listdir(directory):
item_path = os.path.join(directory, item)
if os.path.isfile(item_path):
os.remove(item_path)
elif os.path.isdir(item_path):
shutil.rmtree(item_path)
class AvlRuncase:
"""
Defines an AVL run case with aerodynamic parameters.
"""
def __init__(self, runcase_folder: str=".", runcase_id: int=1, runcase_name: str="default_runcase", alpha: float=0.0, beta: float=0.0, pb2v: float=0.0, qc2v: float=0.0, rb2v: float=0.0, mach_number: float=0.0, CD0: float=0.02, height_in_m: float=0.0, cog: list[float]=[0.0,0.0,0.0], controls=None):
"""
Initializes an AVL run case.
"""
self.runcase_directory = runcase_folder
self.runcase_id = runcase_id
self.runcase_name = runcase_name
self.runcase_file: str= f"rc_{runcase_id:03}.run"
self.alpha = alpha
self.beta = beta
self.pb2v = pb2v
self.qc2v = qc2v
self.rb2v = rb2v
self.controls = controls
self.CD0 = CD0
self.mach_number = mach_number
self.altitude = height_in_m
self.density = Atmosphere(self.altitude).density
self.speed_of_sound = Atmosphere(self.altitude).speed_of_sound
self.velocity = mach_number * self.speed_of_sound
self.cog = cog
def create(self):
"""
Creates an AVL run case file in the specified directory.
"""
# Create runcase directory
os.makedirs(self.runcase_directory, exist_ok=True)
full_path_to_runcase = f"{self.runcase_directory}/{self.runcase_file}"
if os.path.exists(full_path_to_runcase):
os.remove(full_path_to_runcase)
with open(full_path_to_runcase, "+w") as rcfile:
avl.write_runcase(file=rcfile, runcasenum=1, runcasename=self.runcase_name, alpha=self.alpha, beta=self.beta, pb2v=self.pb2v, qc2v=self.qc2v, rb2v=self.rb2v, mach=self.mach_number, CD0=self.CD0, density=self.density, a=self.speed_of_sound, cgref=self.cog, controls=self.controls)
return self
# UNICADO - UNIversity Conceptual Aircraft Design and Optimization
#
# Copyright (C) 2025 UNICADO consortium
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Description:
# This file is part of UNICADO.
"""
PyAvlUnicado - A Python Interface for Athena Vortex Lattice (AVL) for use with UNICADO
This module provides utility functions for handling aircraft geometry data,
specifically for interacting with aircraft XML files and extracting aerodynamic
surface information.
Functions:
- get_element_path(element, tree): Retrieves the hierarchical path of an XML element.
- load_aerodynamic_surfaces(paths, component_names): Loads aerodynamic surfaces from aircraft XML data.
- load_aerodynamic_surfaces_from_information(paths, aerodynamic_surfaces_information): Extracts aerodynamic surface details.
- aerodynamic_surfaces_from_component_available(component): Identifies aerodynamic surfaces in a component.
- load_reference_position(component): Extracts the reference position of a component.
"""
import pyaircraftgeometry2 as geom2
import pyaixml as aixml
def get_element_path(element, tree):
"""
Retrieves the hierarchical path of an XML element relative to the root.
Args:
element (ET.Element): The XML element whose path is to be determined.
tree (ET.Element): The root of the XML tree.
Returns:
str: The hierarchical path of the element.
"""
element_path = []
while element != tree:
parent = tree.find(f".//{element.tag}/..")
element_path.append(element.tag)
element = parent
element_path.reverse()
return '/'.join(element_path)
def load_aerodynamic_surfaces(paths, component_names: list[str]=[]):
"""
Loads aerodynamic surface data from aircraft XML files.
Args:
paths (dict): Dictionary containing paths to aircraft exchange files.
component_names (list[str], optional): List of component names to extract data from.
Returns:
dict: A dictionary containing aerodynamic surface details for each component.
Raises:
RuntimeError: If component_names is not a valid list of strings.
"""
component_design_items = None
if component_names or isinstance(component_names, list) and all(isinstance(name, str) for name in component_names):
if component_names:
component_design_items = []
for component_name in component_names:
component_design_items.append(paths["root_of_aircraft_exchange_tree"].find(
f".//component_design/{component_name}"))
else:
component_design_items = paths["root_of_aircraft_exchange_tree"].find('.//component_design')
aerodynamic_surfaces = {}
if component_design_items is not None:
for component in component_design_items:
info = aerodynamic_surfaces_from_component_available(component)
if info is not None:
aerodynamic_surfaces[component.tag] = {
"aerodynamic_surfaces": load_aerodynamic_surfaces_from_information(paths, info),
"reference_position": load_reference_position(component),
"info": info
}
return aerodynamic_surfaces
else:
raise RuntimeError(
f"component_names does not match required type list - current type: {type(component_names)}")
def load_aerodynamic_surfaces_from_information(paths, aerodynamic_surfaces_information):
"""
Extracts aerodynamic surfaces from the given aircraft XML data.
Args:
paths (dict): Dictionary containing paths to aircraft exchange files.
aerodynamic_surfaces_information (list): List of aerodynamic surface information.
Returns:
list: A list of aerodynamic surfaces including geometry and control devices.
"""
acxml = aixml.openDocument(paths["path_to_aircraft_exchange_file"])
aerodynamic_surfaces = []
for aerodynamic_surface_info in aerodynamic_surfaces_information:
aerodynamic_surface = {}
aerodynamic_surface["geometry"] = geom2.factory.WingFactory(
acxml, paths["airfoil_data_directory"]).create(aerodynamic_surface_info["geometry_path"])
aerodynamic_surface["control_devices"] = []
for control_device in aerodynamic_surface_info["control_devices"]:
device = geom2.factory.ControlDeviceFactory(acxml, paths["airfoil_data_directory"]).create(control_device["path"])
device.name = control_device["name"]
aerodynamic_surface["control_devices"].append(device)
aerodynamic_surfaces.append(aerodynamic_surface)
return aerodynamic_surfaces
def aerodynamic_surfaces_from_component_available(component):
"""
Identifies aerodynamic surfaces in an aircraft component.
Args:
component (ET.Element): XML element representing the aircraft component.
Returns:
list or None: A list of aerodynamic surfaces found in the component, or None if no surfaces are available.
"""
components_aerodynamic_surfaces = component.findall(".//aerodynamic_surface[@ID]")
if components_aerodynamic_surfaces:
aerodynamic_surfaces_available = []
for components_aerodynamic_surface in components_aerodynamic_surfaces:
aerodynamic_surface_available = {}
aerodynamic_surface_available["geometry_path"] = f"{component.tag}/{get_element_path(components_aerodynamic_surface, component)}@{components_aerodynamic_surface.attrib['ID']}"
aerodynamic_surface_available["control_devices"] = []
# look for control devices
control_devices = components_aerodynamic_surface.findall(".//control_device[@ID]")
if control_devices:
for control_device in control_devices:
control_device_info = {
"path": f"{aerodynamic_surface_available['geometry_path']}/{get_element_path(control_device, components_aerodynamic_surface)}@{control_device.attrib['ID']}",
"name": control_device.attrib["description"]
}
aerodynamic_surface_available["control_devices"].append(control_device_info)
aerodynamic_surfaces_available.append(aerodynamic_surface_available)
return aerodynamic_surfaces_available
else:
return None
def load_reference_position(component):
"""
Extracts the reference position of an aircraft component.
Args:
component (ET.Element): XML element representing the aircraft component.
Returns:
dict: Dictionary containing x, y, and z coordinates of the reference position.
"""
position = {
"x": float(component.find("./position/x/value").text),
"y": float(component.find("./position/y/value").text),
"z": float(component.find("./position/z/value").text)
}
return position
This diff is collapsed.
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "pyavlunicado"
version = "1.0.0"
description = "Unicado avl interface to build generate aerodynamic derivatives."
authors = [{ name = "Christopher Ruwisch", email = "christopher.ruwisch@tu-berlin.de" }]
readme = "README.md"
license = { file = "LICENSE" }
keywords = ["avl"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: OS Independent",
]
dependencies = [
"numpy>=1.26.4",
]
[tool.setuptools]
packages = ["pyavlunicado"]
[project.urls]
homepage = "https://unicado.pages.rwth-aachen.de/unicado.gitlab.io/"
repository = "https://git.rwth-aachen.de/unicado/libraries"
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment