mirror of
https://gitee.com/mirrors_PX4/PX4-Autopilot.git
synced 2026-07-16 11:30:36 +08:00
7ac50a20b0
This is a tool that can be used to generate advanced lift drag plugin parameters automatically using AVL. Rather than having to create .avl files yourself, pass them to AVL, read out the correct parameters and place them in the Advanced Lift Drag plugin, this tool will do all that for you and generate a complete advanced_lift_drag plugin sdf containing all necessary parameters for any vehicle. All that is required is to specify what the physical geometries of the vehicle are. The scripts are adaptable enough to support a self-selected number of control surfaces. --------- Co-authored-by: frederik <frederik@auterion.com>
315 lines
12 KiB
Python
Executable File
315 lines
12 KiB
Python
Executable File
#!/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()
|