From 7ac50a20b0b3c4ad539afd5f2c539233b7383240 Mon Sep 17 00:00:00 2001 From: Frederik Markus <80588263+frede791@users.noreply.github.com> Date: Wed, 18 Oct 2023 21:33:50 +0200 Subject: [PATCH] Tools/simulation/gz: initial AVL automation tool (#22204) 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 --- .../gz/tools/avl_automation/README.md | 149 ++++++++ .../gz/tools/avl_automation/avl_out_parse.py | 342 ++++++++++++++++++ .../gz/tools/avl_automation/avl_steps.txt | 10 + .../gz/tools/avl_automation/input.yml | 142 ++++++++ .../gz/tools/avl_automation/input_avl.py | 314 ++++++++++++++++ .../gz/tools/avl_automation/process.sh | 27 ++ .../templates/advanced_lift_drag_template.sdf | 43 +++ .../templates/control_surface.sdf | 11 + 8 files changed, 1038 insertions(+) create mode 100755 Tools/simulation/gz/tools/avl_automation/README.md create mode 100755 Tools/simulation/gz/tools/avl_automation/avl_out_parse.py create mode 100755 Tools/simulation/gz/tools/avl_automation/avl_steps.txt create mode 100755 Tools/simulation/gz/tools/avl_automation/input.yml create mode 100755 Tools/simulation/gz/tools/avl_automation/input_avl.py create mode 100755 Tools/simulation/gz/tools/avl_automation/process.sh create mode 100644 Tools/simulation/gz/tools/avl_automation/templates/advanced_lift_drag_template.sdf create mode 100644 Tools/simulation/gz/tools/avl_automation/templates/control_surface.sdf diff --git a/Tools/simulation/gz/tools/avl_automation/README.md b/Tools/simulation/gz/tools/avl_automation/README.md new file mode 100755 index 0000000000..472cee03b6 --- /dev/null +++ b/Tools/simulation/gz/tools/avl_automation/README.md @@ -0,0 +1,149 @@ +## Purpose + +The idea of this tool is to automate the writing of the Advanced Lift Drag plugin by automatizing the coefficient generation and requiring minimal user calculations. + +## Setup + +In order to run this tool, it is necessary to follow these steps: + +1. Download AVL 3.36 from . The file for AVL version 3.36 can be found about halfway down the page. +2. After downloading, extract AVL and move it to the home directory using: + +```shell +sudo tar -xf avl3.36.tgz +mv ./Avl /home/ +``` + +Follow the README.md found in Avl to finish the setup process for AVL (requires to set up plotlib and eispack libraries). I recommend using the gfortran compile option. This might require you to install gfortran. This can be done by running: + +```shell +sudo apt update +sudo apt install gfortran +``` + +When running the Makefile for AVL, you might encounter an Error 1 message stating that there is a directory missing. This does not prevent AVL from working for our purposes. Once the process described in the AVL README is completed, AVL is ready to be used. No further set up is required on the side of the AVL or the tool. +If you want to move the location of the AVL directory, this can simply be done by passing the `--avl_path` flag to the `input_avl.py` file, using the desired directory location for the flag (don't forget to place a "/" behind the last part of the path). Running this will automatically also adjust the paths where necessary. + +## Run + +To run the tool all that is needed is to modify the `input.yml` to the plane that you desire and then run `python input_avl.py .yml` Note that you require to have the yaml and argparse packages in your python environment to run this. An example template has been provided in the form of the `input.yml` that implements a standard plane with two ailerons, an elevator and a rudder. This example template can be run using: `python input_avl.py --yaml_file input.yml`. +Once the script has been executed, the generated .avl, .sdf and a plot of the proposed control surfaces can be found in directory. The sdf file is the generated Advanced Lift Drag Plugin that can be copied and pasted straight into a model.sdf file, which can then be run in Gazebo. + +## Functionality + +The tool first asks the user for a range of vehicle specific parameters that are needed in order to specify the geometry and physical properties of the plane. The user has the choice to define a completely custom model, or alternatively select a predefined model template (such as a Cessna or a VTOL), which has a known number of control surfaces, and then provide only some physical properties, without having to define the entire model themselves. The input_avl.py file takes the provided parameter and creates an .avl file from this that can be read by AVL (the program). This happens in the process.sh file. The necessary output generated by AVL will be saved in two files: custom_vehicle_body_axis_derivatives.txt and custom_vehicle_stability_derivatives.txt. These two files contain the parameters that are required in order to populate the Advanced Lift Drag Plugin. Finally, avl_out_parse.py reads the generated .txt files and accordingly assigns parameters to the correct element in sdf. Once this is done, it is only a question of copy and pasting the generated Advanced Lift Drag plugin (found as .sdf into the desired model.sdf file. ) + + +## Usability + +The current implementation provides a minimal working example. More accurate measurements can be made by adjusting the chosen number of vortices along span and chord according to desired preferences. A good starting point for this can be found here: . Furthermore, one can also more accurately model a vehicle by using a larger number of sections. In the current .yml file, only a left and right edge are defined for each surface yielding exactly one section, but the code supports expanding this to any number of desired sections. + +## IMPORTANT POINTS TO NOTE + +- A control surface in AVL is always defined from left to right. This means you need to first provide the left edge of a surface and then the right edge. If you do this the opposite way around, a surface will essentially be defined upside down. +- The tool is designed to only support at most two control surfaces of any type on any one vehicle. Having more surfaces than that can lead to faulty behavior. +- Another important point is that these scripts make use of the match, case syntax, which was only introduced in Python in version 3.10. +- The primary reference resource for AVL can be found at . This document was written by the creators of AVL and contains all the variables that could be required in defining the control surfaces. +- AVL cannot predict stall values. As such these need to be calculated/estimated another way. In the current implementation, default stall values have been taken from PX4's Advanced Plane. These should naturally be changed for new/different models. + +## Parameter Assignment + +Below is a comprehensive list on how the parameters are assigned at output and what files in AVL they are taken from. I am by no means an AVL expert, so please verify that these are actually the correct parameters required by the Advanced Lift Drag Plugin. For an explanation of what the parameters do, please see take a look at the Advanced Lift Drag Plugin. + +(name-in-AVL) -> (name-in-plugin) + +From the stability derivatives log file, the following advanced lift drag plugin parameters are taken: + +Alpha -> alpha The angle of attack + +Cmtot -> Cem0 Pitching moment coefficient at zero angle of attack + +CLtot -> CL0 Lift Coefficient at zero angle of attack + +CDtot -> CD0 Drag coefficient at zero angle of attack + +CLa -> CLa dCL/da (slope of CL-alpha curve) + +CYa -> CYa dCy/da (sideforce slope wrt alpha) + +Cla -> Cella dCl/da (roll moment slope wrt alpha) + +Cma -> Cema dCm/da (pitching moment slope wrt alpha - before stall) + +Cna -> Cena dCn/da (yaw moment slope wrt alpha) + +CLb -> CLb dCL/dbeta (lift coefficient slope wrt beta) + +CYb -> CYb dCY/dbeta (side force slope wrt beta) + +Clb -> Cellb dCl/dbeta (roll moment slope wrt beta) + +Cmb -> Cemb dCm/dbeta (pitching moment slope wrt beta) + +Cnb -> Cenb dCn/dbeta (yaw moment slope wrt beta) + + +From the body axis derivatives log file, the following advanced lift drag plugin parameters are taken: + +e -> eff Wing efficiency (Oswald efficiency factor for a 3D wing) + +CXp -> CDp dCD/dp (drag coefficient slope wrt roll rate) + +CYp -> CYp dCY/dp (sideforce slope wrt roll rate) + +CZp -> CLp dCL/dp (lift coefficient slope wrt roll rate) + +Clp -> Cellp dCl/dp (roll moment slope wrt roll rate) + +Cmp -> Cemp dCm/dp (pitching moment slope wrt roll rate) + +Cmp -> Cenp dCn/dp (yaw moment slope wrt roll rate) + +CXq -> CDq dCD/dq (drag coefficient slope wrt pitching rate) + +CYq -> CYq dCY/dq (side force slope wrt pitching rate) + +CZq -> CLq dCL/dq (lift coefficient slope wrt pitching rate) + +Clq -> Cellq dCl/dq (roll moment slope wrt pitching rate) + +Cmq -> Cemq dCm/dq (pitching moment slope wrt pitching rate) + +Cnq -> Cenq dCn/dq (yaw moment slope wrt pitching rate) + +CXr -> CDr dCD/dr (drag coefficient slope wrt yaw rate) + +CYr -> CYr dCY/dr (side force slope wrt yaw rate) + +CZr -> CLr dCL/dr (lift coefficient slope wrt yaw rate) + +Clr -> Cellr dCl/dr (roll moment slope wrt yaw rate) + +Cmr -> Cemr dCm/dr (pitching moment slope wrt yaw rate) + +Cnr -> Cenr dCn/dr (yaw moment slope wrt yaw rate) + + +Furthermore, every control surface also has six own parameters, which are also derived from this log file. {i} below ranges from 1 to the number of unique control surface types in the model. + +CXd{i} -> CD_ctrl Effect of the control surface's deflection on drag + +CYd{i} -> CY_ctrl Effect of the control surface's deflection on side force + +CZd{i} -> CL_ctrl Effect of the control surface's deflection on lift + +Cld{i} -> Cell_ctrl Effect of the control surface's deflection on roll moment + +Cmd{i} -> Cem_ctrl Effect of the control surface's deflection on pitching moment + +Cnd{i} -> Cen_ctrl Effect of the control surface's deflection on yaw moment + + +## Future Work + +The tool, while self-contained, could be expanded into multiple directions. + +1. Currently hinge positions and gains are set at default levels, and these could, if desired be further customized for more control. +2. More vehicles could be added to provide default templates that require less input. At the moment, only "custom" works completely. +3. Fuselage modelling could be included to further improve the accuracy of calculated coefficients. +4. At the moment only NACA airfoils are provided as a way to generate cambered surfaces. An alternative to this would be to use custom airfoil files. diff --git a/Tools/simulation/gz/tools/avl_automation/avl_out_parse.py b/Tools/simulation/gz/tools/avl_automation/avl_out_parse.py new file mode 100755 index 0000000000..ecc38e0bd0 --- /dev/null +++ b/Tools/simulation/gz/tools/avl_automation/avl_out_parse.py @@ -0,0 +1,342 @@ +#!/usr/bin/env + +import argparse +import shutil +import fileinput +import subprocess +import os +from typing import TextIO + + +""" +Get the desired coefficient from the AVL output files by looking through the file line by line and picking it out when encountered. + +Args: + file (TextIO): The file from which the desired coefficient should be read. + token (str): The coefficient which to look for. + +Return: + value (str): The value associated with the desired coefficient. + +""" +def get_coef(file: TextIO,token: str) -> str: + + linesplit = [] + for line in file: + if f' {token} ' in line: + linesplit = line.split() + break + + index = 0 + for i,v in enumerate(linesplit): + if v == token: + index = i + value = linesplit[index+2] + return value + + + +""" +Write all gathered, model-wide coefficients to the sdf file. + +Args: + file (TextIO): The file to which the desired coefficient should be written. + token_str (str): The coefficients for which the associated value should be written. + token (str): The value which should be placed in the avl. + +Return: + None. + +""" +def write_coef(file: TextIO, token_str: str, token: str): + old_line = f'<{token_str}>' + new_line = f'<{token_str}>{token}' + with fileinput.FileInput(file, inplace=True) as output_file: + for line in output_file: + print(line.replace(old_line, new_line), end='') + + + +""" +Write all gathered, control surface specific parameters to the sdf file. + +Args: + file (TextIO): The file to which the desired coefficients should be written. + ctrl_surface_vec (list): A vector that contains all 6 necessary coefficient values for the control surface in question. + index (str): The model-wide index number of the control surface in question. + direction (str): The direction in which the control surface can be actuated. + +Return: + None. +""" +def ctrl_surface_coef(file: TextIO,ctrl_surface_vec: list,index: str, direction: str): + + extracted_text = '' + with open("./templates/control_surface.sdf",'r') as open_file: + for line in open_file: + extracted_text += line + open_file.close() + + # Insert necessary coefficient values, index and direction in correct sdf location. + extracted_text = extracted_text.replace("",f'servo_{index}') + extracted_text = extracted_text.replace("",f'{index}') + extracted_text = extracted_text.replace("",f'{direction}') + extracted_text = extracted_text.replace("",f'{ctrl_surface_vec[0]}') + extracted_text = extracted_text.replace("",f'{ctrl_surface_vec[1]}') + extracted_text = extracted_text.replace("",f'{ctrl_surface_vec[2]}') + extracted_text = extracted_text.replace("",f'{ctrl_surface_vec[3]}') + extracted_text = extracted_text.replace("",f'{ctrl_surface_vec[4]}') + extracted_text = extracted_text.replace("",f'{ctrl_surface_vec[5]}') + + + # Create model specific template + with open(file,'a') as plugin_file: + plugin_file.write(extracted_text + "\n") + plugin_file.close() + +""" +Read out the necessary log files to gather the desired parameters and write them to the sdf plugin file. +Arguments provided here are passed in the input_avl.py file. + +Args: + file_name (TextIO): The file to which the desired coefficients should be written. + vehicle_type (str): The type of vehicle in use. + AR (str): The calculated aspect ratio. + mac (str): The calculated mean aerodynamic chord. + ref_pt_x (str): The x coordinate of the reference point, at which forces and moments are applied. + ref_pt_y (str): The y coordinate of the reference point, at which forces and moments are applied. + ref_pt_z (str): The z coordinate of the reference point, at which forces and moments are applied. + num_ctrl_surfaces (str): The number of control surfaces that the model uses. + area (str): The wing surface area. + ctrl_surface_order (list): A list containing the types of control surfaces, in theorder in which + they have been defined in the .avl file. + avl_path (str): A string containing the directory where the AVL directory should be moved to. + +Return: + None. +""" + +def main(file_name: TextIO, vehicle_type: str, AR: str, mac: str, ref_pt_x: str, ref_pt_y: str, ref_pt_z: str, num_ctrl_surfaces: str, area: str, ctrl_surface_order: list, avl_path:str): + + # Set current path for user + curr_path = subprocess.run(['pwd'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + if curr_path.returncode == 0: + # Save the output in a variable + savedir = curr_path.stdout.strip() + else: + raise LookupError("Invalid path to directory. Check both the avl_automation directory and the Avl directory are positioned correctly.") + + # Set the file directory path from where the AVL output logs can be read. + filedir = f'{avl_path}Avl/runs/' + + # Read out all necessary parameters from the stability and body axis derivatives files. + with open(f'{filedir}custom_vehicle_stability_derivatives.txt','r+') as stability_file: + original_position = stability_file.tell() + + # As plane is modelled at 0 degree AoA, the total coefficients should(?) correspond to the + # 0 degree coefficients required by the plugin. + alpha = get_coef(stability_file,"Alpha") + Cem0 = get_coef(stability_file,"Cmtot") + CL0 = get_coef(stability_file,"CLtot") + CD0 = get_coef(stability_file,"CDtot") + + CLa = get_coef(stability_file,"CLa") + CYa = get_coef(stability_file,"CYa") + Cella = get_coef(stability_file,"Cla") + Cema = get_coef(stability_file,"Cma") + Cena = get_coef(stability_file,"Cna") + + stability_file.seek(original_position) + + CLb = get_coef(stability_file,"CLb") + CYb = get_coef(stability_file,"CYb") + Cellb = get_coef(stability_file,"Clb") + Cemb = get_coef(stability_file,"Cmb") + Cenb = get_coef(stability_file,"Cnb") + stability_file.close() + + with open(f'{filedir}custom_vehicle_body_axis_derivatives.txt') as bodyax_file: + original_position = bodyax_file.tell() + + eff = get_coef(bodyax_file,"e") + + bodyax_file.seek(original_position) + + CDp = get_coef(bodyax_file,"CXp") + CYp = get_coef(bodyax_file,"CYp") + CLp = get_coef(bodyax_file,"CZp") + Cellp = get_coef(bodyax_file,"Clp") + Cemp = get_coef(bodyax_file,"Cmp") + Cenp = get_coef(bodyax_file,"Cnp") + + bodyax_file.seek(original_position) + + CDq = get_coef(bodyax_file,"CXq") + CYq = get_coef(bodyax_file,"CYq") + CLq = get_coef(bodyax_file,"CZq") + Cellq = get_coef(bodyax_file,"Clq") + Cemq = get_coef(bodyax_file,"Cmq") + Cenq = get_coef(bodyax_file,"Cnq") + + bodyax_file.seek(original_position) + + CDr = get_coef(bodyax_file,"CXr") + CYr = get_coef(bodyax_file,"CYr") + CLr = get_coef(bodyax_file,"CZr") + Cellr = get_coef(bodyax_file,"Clr") + Cemr = get_coef(bodyax_file,"Cmr") + Cenr = get_coef(bodyax_file,"Cnr") + bodyax_file.close() + + plane_type = vehicle_type + ctrl_surface_mat = [] + + # Maybe in the future you want more types of set aircraft. Thus us a case differentiator. + match plane_type: + + case "custom": + ctrl_surface_vec = [] + with open(f'{filedir}custom_vehicle_body_axis_derivatives.txt') as bodyax_file: + original_position = bodyax_file.tell() + for i in range(1,(len(set(ctrl_surface_order)))+1): + ctrl_surface_vec = [] + ctrl_surface_vec.append(get_coef(bodyax_file,f'CXd{i}')) + ctrl_surface_vec.append(get_coef(bodyax_file,f'CYd{i}')) + ctrl_surface_vec.append(get_coef(bodyax_file,f'CZd{i}')) + ctrl_surface_vec.append(get_coef(bodyax_file,f'Cld{i}')) + ctrl_surface_vec.append(get_coef(bodyax_file,f'Cmd{i}')) + ctrl_surface_vec.append(get_coef(bodyax_file,f'Cnd{i}')) + bodyax_file.seek(original_position) + ctrl_surface_mat.append(ctrl_surface_vec) + + + # SPECIFY STALL PARAMETERS BASED ON AIRCRAFT TYPE (IF PROVIDED) + if not os.path.exists(f'{savedir}/{file_name}'): + os.makedirs(f'{savedir}/{file_name}') + file_name = f'{savedir}/{file_name}/{file_name}.sdf' + shutil.copy(f'{savedir}/templates/advanced_lift_drag_template.sdf',file_name) + + # Get argument coefficients taken directly from the input file. + write_coef(file_name,"a0",alpha) + write_coef(file_name,"CL0",CL0) + write_coef(file_name,"CD0",CD0) + write_coef(file_name,"Cem0",Cem0) + write_coef(file_name,"AR",AR) + write_coef(file_name,"area",area) + write_coef(file_name,"mac",mac) + write_coef(file_name,"air_density",1.2041) # TODO: Provide custom air density option + write_coef(file_name,"forward","1 0 0") + write_coef(file_name,"upward","0 0 1") + write_coef(file_name,"link_name","base_link") + write_coef(file_name,"cp",f'{ref_pt_x} {ref_pt_y} {ref_pt_z}') + write_coef(file_name,"num_ctrl_surfaces",num_ctrl_surfaces) + + write_coef(file_name,"CLa",CLa) + write_coef(file_name,"CYa",CYa) + write_coef(file_name,"Cella",Cella) + write_coef(file_name,"Cema",Cema) + write_coef(file_name,"Cena",Cena) + write_coef(file_name,"CLb",CLb) + write_coef(file_name,"CYb",CYb) + write_coef(file_name,"Cellb",Cellb) + write_coef(file_name,"Cemb",Cemb) + write_coef(file_name,"Cenb",Cenb) + + write_coef(file_name,"CDp",CDp) + write_coef(file_name,"CYp",CYp) + write_coef(file_name,"CLp",CLp) + write_coef(file_name,"Cellp",Cellp) + write_coef(file_name,"Cemp",Cemp) + write_coef(file_name,"Cenp",Cenp) + write_coef(file_name,"CDq",CDq) + write_coef(file_name,"CYq",CYq) + write_coef(file_name,"CLq",CLq) + write_coef(file_name,"Cellq",Cellq) + write_coef(file_name,"Cemq",Cemq) + write_coef(file_name,"Cenq",Cenq) + write_coef(file_name,"CDr",CDr) + write_coef(file_name,"CYr",CYr) + write_coef(file_name,"CLr",CLr) + write_coef(file_name,"Cellr",Cellr) + write_coef(file_name,"Cemr",Cemr) + write_coef(file_name,"Cenr",Cenr) + + write_coef(file_name,"eff",eff) + + # TODO: Improve this for custom stall values + # Note: Currently these stall values are simply taken from advanced_plane presets. + + write_coef(file_name,"alpha_stall","0.3391428111") + write_coef(file_name,"CLa_stall","-3.85") + write_coef(file_name,"CDa_stall","-0.9233984055") + write_coef(file_name,"Cema_stall","0") + + # Check whether a particular type of control surface has been seen before. If it has, + # then the current control surface is the (right) counterpart. + + # ASSUMPTION: There is the assumption that an vehicle will only ever have two of any + # particular type of control surface. (left and right). If this is not the case, the negation + # below will likely not work correctly. + type_seen = list() + + # Dictionary containing the directions that each type of control surface can move. + ctrl_direction = {"aileron": 1,"elevator": -1,"rudder": 1} + + # More set types in the future? + match plane_type: + + case "custom": + for i, ctrl_surface in enumerate(ctrl_surface_order): + + # Check whether a particular type of control surface has been seen before. If it has, + # then the current control surface is the (right) counterpart. Depending on the exact + # nature of the encountered type you then need to negate the correct parameters. + if ctrl_surface in type_seen: + # Work out what the corresponding index for the first encounter of the ctrl surface is. + seen_index = type_seen.index(ctrl_surface) + + if ctrl_surface == 'aileron': + #Change for right wing aileron by flipping sign + ctrl_surface_mat[seen_index][3] = -float(ctrl_surface_mat[0][3]) + ctrl_surface_mat[seen_index][5] = -float(ctrl_surface_mat[0][5]) + + # Split Elevators are assumed to never run differentially. Feel free to add a + # condition if your plane does require differential elevator action. + + else: + # If a ctrl surface has not been encountered add it to the type_seen list and + # set the index to the length of the list - 1 as this corresponds to the newest + # unseen element in ctrl_surface_mat . + type_seen.append(ctrl_surface) + seen_index = len(type_seen) - 1 + + ctrl_surface_coef(file_name,ctrl_surface_mat[seen_index],i,ctrl_direction[ctrl_surface]) + + + # close the sdf file with plugin + with open(file_name,'a') as plugin_file: + plugin_file.write("") + plugin_file.close() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + + parser.add_argument("file_name", help="The file to which the desired coefficients should be written.") + parser.add_argument("vehicle_type", help="The type of vehicle in use.") + parser.add_argument("AR", help="The calculated aspect ratio.") + parser.add_argument("mac", help="The calculated mean aerodynamic chord.") + parser.add_argument("ref_pt_x", help="The x coordinate of the reference point, at which forces and moments are applied.") + parser.add_argument("ref_pt_y", help="The y coordinate of the reference point, at which forces and moments are applied.") + parser.add_argument("ref_pt_z", help="The z coordinate of the reference point, at which forces and moments are applied.") + parser.add_argument("num_ctrl_surfaces", help="The number of control surfaces that the model uses.") + parser.add_argument("area", help= "The wing surface area.") + parser.add_argument("ctrl_surface_order", help=" A list containing the types of control surfaces, in theorder in which \ + they have been defined in the .avl file.") + parser.add_argument("avl_path",help="A string containing the directory where the AVL directory should be moved to.") + + args = parser.parse_args() + + main(args.file_name,args.vehicle_type,args.AR,args.mac,args.ref_pt_x,args.ref_pt_y, + args.ref_pt_z,args.num_ctrl_surfaces,args.area,args.ctrl_surface_order,args.avl_path) diff --git a/Tools/simulation/gz/tools/avl_automation/avl_steps.txt b/Tools/simulation/gz/tools/avl_automation/avl_steps.txt new file mode 100755 index 0000000000..48c20a52c1 --- /dev/null +++ b/Tools/simulation/gz/tools/avl_automation/avl_steps.txt @@ -0,0 +1,10 @@ +oper +x +n custom_plane +st custom_vehicle_stability_derivatives.txt +sb custom_vehicle_body_axis_derivatives.txt +g +h + + +quit diff --git a/Tools/simulation/gz/tools/avl_automation/input.yml b/Tools/simulation/gz/tools/avl_automation/input.yml new file mode 100755 index 0000000000..3231757b09 --- /dev/null +++ b/Tools/simulation/gz/tools/avl_automation/input.yml @@ -0,0 +1,142 @@ +# Enter a name for your vehicle +vehicle_name: plane_example_2 + +# Enter the type of airframe you would like to use: +frame_type: custom + +# First define some model-wide parameters for custom models: +reference_area: 12 +wing_span: 15 +# Provide a reference point at which the forces and moments generated will act. +reference_point: + X: 0 + Y: 0 + Z: 0 + +#Provide information on each of the Control Surfaces +num_ctrl_surfaces: 4 +control_surfaces: + - name: right_wing + type: aileron + nchord: 1 + cspace: 1 + nspan: 16 + sspace: -2 + angle: 4 + translation: + X: 0 + Y: 0 + Z: 0 + naca: 2412 + sections: + - name: section_1 + position: + X: -0.25 + Y: 0 + Z: 0 + chord: 1 + ainc: 0 + nspan: 8 + sspace: 1 + - name: section_2 + position: + X: -0.175 + Y: 5 + Z: 0.5 + chord: 0.7 + ainc: 0 + nspan: 0 + sspace: 0 + + + - name: left_wing + type: aileron + nchord: 1 + cspace: 1 + nspan: 16 + sspace: -2 + angle: 4 + translation: + X: 0 + Y: 0 + Z: 0 + naca: 2412 + sections: + - name: section_1 + position: + X: -0.175 + Y: -5 + Z: 0.5 + chord: 0.7 + ainc: 0 + nspan: 0 + sspace: 0 + - name: section_2 + position: + X: -0.25 + Y: 0 + Z: 0 + chord: 1 + ainc: 0 + nspan: 8 + sspace: 1 + + - name: elevator + type: elevator + nchord: 1 + cspace: 1 + nspan: 7 + sspace: -2 + translation: + X: 6 + Y: 0 + Z: 0.5 + sections: + - name: section_1 + position: + X: -0.1 + Y: 0 + Z: 0 + chord: 0.4 + ainc: 0 + nspan: 7 + sspace: -1.25 + - name: section_2 + position: + X: -0.075 + Y: 2 + Z: 0 + chord: 0.3 + ainc: 0 + nspan: 0 + sspace: 0 + + - name: fin + type: rudder + nchord: 1 + cspace: 1 + nspan: 10 + sspace: 1 + translation: + X: 6 + Y: 0 + Z: 0.5 + sections: + - name: section_1 + position: + X: -0.1 + Y: 0 + Z: 0 + chord: 0.4 + ainc: 0 + nspan: 7 + sspace: -1.25 + - name: section_2 + position: + X: -0.075 + Y: 0 + Z: 1 + chord: 0.3 + ainc: 0 + nspan: 0 + sspace: 0 diff --git a/Tools/simulation/gz/tools/avl_automation/input_avl.py b/Tools/simulation/gz/tools/avl_automation/input_avl.py new file mode 100755 index 0000000000..1a260efe88 --- /dev/null +++ b/Tools/simulation/gz/tools/avl_automation/input_avl.py @@ -0,0 +1,314 @@ +#!/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() diff --git a/Tools/simulation/gz/tools/avl_automation/process.sh b/Tools/simulation/gz/tools/avl_automation/process.sh new file mode 100755 index 0000000000..fb8e8a19fb --- /dev/null +++ b/Tools/simulation/gz/tools/avl_automation/process.sh @@ -0,0 +1,27 @@ +#!/bin/bash +CUSTOM_MODEL=$1 +DIR_PATH=$(pwd) + +cp $DIR_PATH/$CUSTOM_MODEL.avl /home/$USER/Avl/runs/ +cd +cd /home/$USER/Avl/runs + +old_stability_derivatives="custom_vehicle_stability_derivatives.txt" +old_body_ax_derivatives="custom_vehicle_body_axis_derivatives.txt" + +if [ -e "$old_stability_derivatives" ]; then + # Delete old stability derivative file + rm "$old_stability_derivatives" +fi +if [ -e "$old_body_ax_derivatives" ]; then + # Delete old body_axis derivative file + rm "$old_body_ax_derivatives" +fi + +#avl_steps.txt can be used to run commands on the AVL commandline. +../bin/avl $CUSTOM_MODEL.avl < $DIR_PATH/avl_steps.txt +echo "\n" + +#After completion move the plot to avl_automation directory +mv /home/$USER/Avl/runs/plot.ps $DIR_PATH/ +mv $DIR_PATH/plot.ps $DIR_PATH/$CUSTOM_MODEL.ps diff --git a/Tools/simulation/gz/tools/avl_automation/templates/advanced_lift_drag_template.sdf b/Tools/simulation/gz/tools/avl_automation/templates/advanced_lift_drag_template.sdf new file mode 100644 index 0000000000..d024658bdb --- /dev/null +++ b/Tools/simulation/gz/tools/avl_automation/templates/advanced_lift_drag_template.sdf @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tools/simulation/gz/tools/avl_automation/templates/control_surface.sdf b/Tools/simulation/gz/tools/avl_automation/templates/control_surface.sdf new file mode 100644 index 0000000000..79d5b38e97 --- /dev/null +++ b/Tools/simulation/gz/tools/avl_automation/templates/control_surface.sdf @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file