You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

314 lines
12 KiB

#!/usr/bin/env
import argparse
import avl_out_parse
import os
import yaml
import subprocess
import shutil
"""
Write individual airfoil section definitions to the .avl file.
Sections are defined through a 3D point in space and assigned properties such as chord, angle of incidence etc.
AVL then links them up to the other sections of a particular surface. You can define any number of sections for
a particular surface, but there always have to be at least two (a left and right edge).
Args:
plane_name (str): The name of the vehicle.
x (str): The x coordinate of the section.
y (str): The y coordinate of the section.
z (str): The z coordinate of the section.
chord (str): Chord in this section of the surface. Trailing edge is at x + chord, y, z.
ainc (str): Angle of incidence for this section. Taken as a rotation (RH rule) about the surface's
spanwise axis projected onto the Y-Z plane.
nspan (str): Number of spanwise vortices in until the next section.
sspan (str): Controls the spanwise spacing of the vortices.
naca_number (str): The chosen NACA number that will define the cambered properties of this section
of the surface. For help picking an airfoil go to: http://airfoiltools.com/airfoil/naca4digit.
ctrl_surface_type: The selected type of control surface. This should be consistent along the entirety of
the surface. (Question: Flap and Aileron along the same airfoil?)
Return:
None.
"""
def write_section(plane_name: str,x: str,y: str,z: str,chord: str,ainc: str,nspan: str,sspace: str,naca_number: str,ctrl_surf_type: str):
with open(f'{plane_name}.avl','a') as avl_file:
avl_file.write("SECTION \n")
avl_file.write("!Xle Yle Zle Chord Ainc Nspanwise Sspace \n")
avl_file.write(f'{x} {y} {z} {chord} {ainc} {nspan} {sspace} \n')
if naca_number != "0000":
avl_file.write("NACA \n")
avl_file.write(f'{naca_number} \n')
avl_file.close()
match ctrl_surf_type:
case 'aileron':
#TODO provide custom options for gain and hinge positions
with open(f'{plane_name}.avl','a') as avl_file:
avl_file.write("CONTROL \n")
avl_file.write("aileron 1.0 0.0 0.0 0.0 0.0 -1 \n")
avl_file.close()
case 'elevator':
with open(f'{plane_name}.avl','a') as avl_file:
avl_file.write("CONTROL \n")
avl_file.write("elevator 1.0 0.0 0.0 0.0 0.0 1 \n")
avl_file.close()
case 'rudder':
with open(f'{plane_name}.avl','a') as avl_file:
avl_file.write("CONTROL \n")
avl_file.write("rudder 1.0 0.0 0.0 0.0 0.0 1 \n")
avl_file.close()
"""
Read the provided yaml file and generate the corresponding .avl file that can be read into AVL.
Also calls AVL and the avl_out_parse.py file that generates the sdf plugin.
Args:
yaml_file: Path to the input yaml file
avl_path: Set the avl_path to provide a desired directory for where Avl should be located.
Return:
None
"""
def main():
user = os.environ.get('USER')
# This will find Avl on a users machine.
for root, dirs, _ in os.walk(f'/home/{user}/'):
if "Avl" in dirs:
target_directory_path = os.path.join(root, "Avl")
break
parent_directory_path = os.path.dirname(target_directory_path)
filedir = f'{parent_directory_path}/'
print(filedir)
parser = argparse.ArgumentParser()
parser.add_argument("--yaml_file", help="Path to input yaml file.")
parser.add_argument("--avl_path", default=filedir, help="Provide an absolute AVL path. If this argument is passed, AVL will be moved there and the files will adjust their paths accordingly.")
inputs = parser.parse_args()
# If the user passes the avl_path argument then move Avl to that location:
if inputs.avl_path != filedir:
#Check if the directory is already there
if os.path.exists(f'{inputs.avl_path}/Avl') and os.path.isdir(f'{inputs.avl_path}/Avl'):
print("Avl is already at desired location")
else:
shutil.move(f'{filedir}Avl',inputs.avl_path)
# Adjust paths to AVL in process.sh
print("Adjusting paths")
with open("./process.sh", "r") as file:
all_lines = file.readlines()
file.close()
it = 0
for line in all_lines:
if "cp $DIR_PATH/$CUSTOM_MODEL.avl" in line:
new_line = f'cp $DIR_PATH/$CUSTOM_MODEL.avl {inputs.avl_path}Avl/runs\n'
all_lines[it] = new_line
if "/Avl/runs/plot.ps $DIR_PATH/" in line:
new_line =f'mv {inputs.avl_path}Avl/runs/plot.ps $DIR_PATH/\n'
all_lines[it] = new_line
if "cd" in line and "/Avl/runs" in line:
new_line = f'cd {inputs.avl_path}Avl/runs\n'
all_lines[it] = new_line
it += 1
with open("./process.sh", "w") as file:
file.writelines(all_lines)
file.close()
with open(inputs.yaml_file,'r') as yaml_file:
yaml_data = yaml.safe_load(yaml_file)
airframes = ['cessna','standard_vtol','custom']
plane_name = yaml_data['vehicle_name']
frame_type = yaml_data['frame_type']
if not frame_type in airframes:
raise ValueError("\nThis is not a valid airframe, please choose a valid airframe. \n")
# Parameters that need to be provided:
# General
# - Reference Area (Sref)
# - Wing span (Bref) (wing span squared / area = aspect ratio which is a required parameter for the sdf file)
# - Reference point (X,Y,Zref) point at which moments and forces are calculated
#Control Surface specific
# - type (select from options; aileron,elevator,rudder)
# - nchord
# - cspace
# - nspanwise
# - sspace
# - x,y,z 1. (section)
# - chord 1. (section)
# - ainc 1. (section)
# - Nspan 1. (optional for section)
# - sspace 1. (optional for section)
# - x,y,z 2. (section)
# - chord 2. (section)
# - ainc 2. (section)
# - Nspan 2. (optional for section)
# - sspace 2. (optional for section)
# TODO: Find out if elevons are defined
ctrl_surface_types = ['aileron','elevator','rudder']
# - Reference Chord (Cref) (= area/wing span)
delineation = '!***************************************'
sec_demark = '#--------------------------------------------------'
num_ctrl_surfaces = 0
ctrl_surface_order = []
area = 0
span = 0
ref_pt_x = None
ref_pt_y = None
ref_pt_z = None
# Future work: Provide some pre-worked frames for a Cessna and standard VTOL if there is a need for it
match frame_type:
case "custom":
# These parameters are consistent across all models.
# At the moment we do not use any symmetry axis for mirroring.
with open(f'{plane_name}.avl','w') as avl_file:
avl_file.write(f'{delineation} \n')
avl_file.write(f'!{plane_name} input dataset \n')
avl_file.write(f'{delineation} \n')
avl_file.write(f'{plane_name} \n')
avl_file.write('!Mach \n0.0 \n')
avl_file.write('!IYsym IZsym Zsym \n')
avl_file.write('0 0 0 \n')
avl_file.close()
# First define some model-specific parameters for custom models
area = yaml_data["reference_area"]
span = yaml_data["wing_span"]
ref_pt_x = yaml_data["reference_point"]["X"]
ref_pt_y = yaml_data["reference_point"]["Y"]
ref_pt_z = yaml_data["reference_point"]["Z"]
if(span != 0 and area != 0):
ref_chord = float(area)/float(span)
else:
raise ValueError("Invalid reference chord value. Check area and wing span values.")
# Write the gathered model-wide parameters into the .avl file
with open(f'{plane_name}.avl','a') as avl_file:
avl_file.write('!Sref Cref Bref \n')
avl_file.write(f'{area} {str(ref_chord)} {span} \n')
avl_file.write('!Xref Yref Zref \n')
avl_file.write(f'{ref_pt_x} {ref_pt_y} {ref_pt_z} \n')
avl_file.close()
num_ctrl_surfaces = yaml_data["num_ctrl_surfaces"]
for i, control_surface in enumerate(yaml_data["control_surfaces"]):
# Wings always need to be defined from left to right
ctrl_surf_name = control_surface['name']
ctrl_surf_type = control_surface['type']
if ctrl_surf_type not in ctrl_surface_types:
raise ValueError(f'The selected type is invalid. Available types are: {ctrl_surface_types}')
# The order of control surfaces becomes important in the output parsing
# to correctly assign derivatives to particular surfaces.
ctrl_surface_order.append(ctrl_surf_type)
nchord = control_surface["nchord"]
cspace = control_surface["cspace"]
nspanwise = control_surface["nspan"]
sspace = control_surface["sspace"]
# TODO: Add more control surface types that also require Angles.
if ctrl_surf_type.lower() == 'aileron':
angle = control_surface["angle"]
#Translation of control surface, will move the whole surface to specified position
tx = control_surface["translation"]["X"]
ty = control_surface["translation"]["Y"]
tz = control_surface["translation"]["Z"]
# Write common part of this surface to .avl file
with open(f'{plane_name}.avl','a') as avl_file:
avl_file.write(sec_demark)
avl_file.write("\nSURFACE \n")
avl_file.write(f'{ctrl_surf_name} \n')
avl_file.write("!Nchordwise Cspace Nspanwise Sspace \n")
avl_file.write(f'{nchord} {cspace} {nspanwise} {sspace} \n')
# If we have a elevator, we can duplicate the defined control surface along the y-axis of the model
# as both sides are generally modelled and controlled as one in simulation. Adjust for split elevators if desired.
if ctrl_surf_type.lower() == 'elevator':
avl_file.write("\nYDUPLICATE\n")
avl_file.write("0.0\n\n")
# Elevators and Rudders do not require an angle of incidence.
if ctrl_surf_type.lower() == 'aileron':
avl_file.write("ANGLE \n")
avl_file.write(f'{angle} \n')
# Translate the surface to a particular position in space.
avl_file.write("TRANSLATE \n")
avl_file.write(f'{tx} {ty} {tz} \n')
avl_file.close()
# Define NACA airfoil shape.
# For help picking an airfoil go to: http://airfoiltools.com/airfoil/naca4digit
# NOTE: AVL can only use 4-digit NACA codes.
if ctrl_surf_type.lower() == "aileron":
naca_number = control_surface["naca"]
else:
# Provide a default NACA number for unused airfoils
naca_number = '0000'
# Iterating over each defined section for the control surface. There need to be at least
# two in order to define a left and right edge, but there is no upper limit.
# CRITICAL: ALWAYS DEFINE YOUR SECTION FROM LEFT TO RIGHT
for j, section in enumerate(control_surface["sections"]):
print(f'Defining {j}. section of {i+1}. control surface \n')
y = section["position"]["Y"]
z = section["position"]["Z"]
x = section["position"]["X"]
chord = section["chord"]
ainc = section["ainc"]
nspan = section["nspan"]
write_section(plane_name,x,y,z,chord,ainc,nspan,sspace,naca_number,ctrl_surf_type)
print(f'\nPARAMETER DEFINITION FOR {i+1}. CONTROL SURFACE COMPLETED \n')
# Calculation of Aspect Ratio (AR) and Mean Aerodynamic Chord (mac)
AR = str((float(span)*float(span))/float(area))
mac = str((2/3)*(float(area)/float(span)))
# Call shell script that will pass the generated .avl file to AVL
os.system(f'./process.sh {plane_name}')
# Call main function of avl parse script to parse the generated AVL files.
avl_out_parse.main(plane_name,frame_type,AR,mac,ref_pt_x,ref_pt_y,ref_pt_z,num_ctrl_surfaces,area,ctrl_surface_order,inputs.avl_path)
# Finally move all generated files to a new directory and show the generated geometry image:
result = subprocess.run(['pwd'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode == 0:
# Save the output in a variable
current_path = result.stdout.strip()
# Run image plot from avl_automation directory.
os.system(f'mv ./{plane_name}.* ./{plane_name}' )
os.system(f'evince {current_path}/{plane_name}/{plane_name}.ps')
if __name__ == '__main__':
main()