diff --git a/ceasiompy/PyAVL/README.md b/ceasiompy/PyAVL/README.md new file mode 100644 index 000000000..f20754ecf --- /dev/null +++ b/ceasiompy/PyAVL/README.md @@ -0,0 +1,40 @@ + + + +# ModuleTemplate + +**Categories:** Template module, Example, Illustration + +**State**: :heavy_check_mark: + +This is a template module. Its purpose is to illustrate how other modules of CEASIOMpy should be structured, set up and documented. + +

+ +

+ +Example picture. Image in the public domain, from [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Spirit_of_St._Louis.jpg) + +## Inputs + +ModuleTemplate needs no inputs. + +## Analyses + +ModuleTemplate computes nothing. + +## Outputs + +ModuleTemplate outputs nothing. + +## Installation or requirements + +ModuleTemplate is a native CEASIOMpy module, hence it is available and installed by default. + +## Limitations + +ModuleTemplate is limited in every aspect. + +## More information + +* diff --git a/ceasiompy/PyAVL/__init__.py b/ceasiompy/PyAVL/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ceasiompy/PyAVL/__specs__.py b/ceasiompy/PyAVL/__specs__.py new file mode 100644 index 000000000..36bd60781 --- /dev/null +++ b/ceasiompy/PyAVL/__specs__.py @@ -0,0 +1,90 @@ +from ceasiompy.utils.moduleinterfaces import CPACSInOut +from ceasiompy.utils.commonxpath import CEASIOMPY_XPATH, AVL_PLOT_XPATH, AVL_VORTEX_DISTR_XPATH +from pathlib import Path + +# ===== Module Status ===== +# True if the module is active +# False if the module is disabled (not working or not ready) +module_status = True # Because it is just an example not a real module + +# ===== CPACS inputs and outputs ===== + +cpacs_inout = CPACSInOut() + +include_gui = True + +RESULTS_DIR = Path("Results", "PyAVL") + +# ----- Input ----- + +# * In the following example we add three (!) new entries to 'cpacs_inout' +# * Try to use (readable) loops instead of copy-pasting three almost same entries :) +cpacs_inout.add_input( + var_name="aeromap_uid", + var_type=list, + default_value=None, + unit=None, + descr="Name of the aero map to calculate", + xpath="/cpacs/toolspecific/CEASIOMpy/aerodynamics/avl/aeroMapUID", + gui=True, + gui_name="__AEROMAP_SELECTION", + gui_group="Aeromap settings", +) + +cpacs_inout.add_input( + var_name="other_var", + var_type=float, + default_value=1.0, + unit=None, + descr="Must be in the range [-3.0 ; +3.0]", + xpath=AVL_VORTEX_DISTR_XPATH + "/Distribution", + gui=True, + gui_name="Choice of distribution", + gui_group="Vortex Lattice Spacing Distributions", +) + +cpacs_inout.add_input( + var_name="other_var", + var_type=int, + default_value=20, + unit=None, + descr="Select the number of chordwise vortices", + xpath=AVL_VORTEX_DISTR_XPATH + "/Nchordwise", + gui=True, + gui_name="Number of chordwise vortices", + gui_group="Vortex Lattice Spacing Distributions", +) + +cpacs_inout.add_input( + var_name="other_var", + var_type=int, + default_value=50, + unit=None, + descr="Select the number of spanwise vortices", + xpath=AVL_VORTEX_DISTR_XPATH + "/Nspanwise", + gui=True, + gui_name="Number of spanwise vortices", + gui_group="Vortex Lattice Spacing Distributions", +) + +cpacs_inout.add_input( + var_name="other_var", + var_type=bool, + default_value=False, + unit=None, + descr="Select to save geometry and results plots", + xpath=AVL_PLOT_XPATH, + gui=True, + gui_name="Save plots", + gui_group="Plots", +) + +# ----- Output ----- + +cpacs_inout.add_output( + var_name="output", + default_value=None, + unit="1", + descr="Description of the output", + xpath=CEASIOMPY_XPATH + "/test/myOutput", +) diff --git a/ceasiompy/PyAVL/avlrun.py b/ceasiompy/PyAVL/avlrun.py new file mode 100644 index 000000000..855630c9d --- /dev/null +++ b/ceasiompy/PyAVL/avlrun.py @@ -0,0 +1,126 @@ +""" +CEASIOMpy: Conceptual Aircraft Design Software + +Developed by CFS ENGINEERING, 1015 Lausanne, Switzerland + +Script to run AVL calculations in CEASIOMpy. +AVL allows to perform aerodynamic analyses using +the vortex-lattice method (VLM) + +Python version: >=3.8 + +| Author: Romain Gauthier +| Creation: 2024-03-14 + +TODO: + + * Things to improve ... + +""" + +# ============================================================================== +# IMPORTS +# ============================================================================== +from ceasiompy.utils.ceasiomlogger import get_logger +from ceasiompy.utils.moduleinterfaces import get_toolinput_file_path, get_tooloutput_file_path +from ceasiompy.PyAVL.func.cpacs2avl import convert_cpacs_to_avl +from ceasiompy.PyAVL.func.avlconfig import write_command_file, get_aeromap_conditions, get_option_settings +from ceasiompy.PyAVL.func.avlresults import get_avl_results +from ceasiompy.utils.ceasiompyutils import get_results_directory + +import subprocess +from pathlib import Path +from ambiance import Atmosphere + + +log = get_logger() + +MODULE_DIR = Path(__file__).parent +MODULE_NAME = MODULE_DIR.name + +# ================================================================================================= +# CLASSES +# ================================================================================================= + + +# ================================================================================================= +# FUNCTIONS +# ================================================================================================= +def run_avl(cpacs_path, wkdir): + """Function to run AVL. + + Function 'run_avl' runs AVL calculations using a CPACS file + as input. + + Args: + cpacs_path (Path) : path to the CPACS input file + wkdir (Path) : path to the working directory + """ + + alt_list, mach_list, aoa_list, aos_list = get_aeromap_conditions(cpacs_path) + save_fig, _, _, _ = get_option_settings(cpacs_path) + + for i_case in range(len(alt_list)): + alt = alt_list[i_case] + mach = mach_list[i_case] + aoa = aoa_list[i_case] + aos = aos_list[i_case] + Atm = Atmosphere(alt) + + density = Atm.density[0] + velocity = Atm.speed_of_sound[0] * mach + g = Atm.grav_accel[0] + + case_dir_name = ( + f"Case{str(i_case).zfill(2)}_alt{alt}_mach{round(mach, 2)}" + f"_aoa{round(aoa, 1)}_aos{round(aos, 1)}" + ) + + Path(wkdir, case_dir_name).mkdir(exist_ok=True) + case_dir_path = Path(wkdir, case_dir_name) + + avl_path = convert_cpacs_to_avl(cpacs_path, wkdir=case_dir_path) + + command_path = write_command_file(avl_path, + case_dir_path, + save_plots=save_fig, + alpha=aoa, + beta=aos, + mach_number=mach, + ref_velocity=velocity, + ref_density=density, + g_acceleration=g, + ) + subprocess.run(["avl"], + stdin=open(str(command_path), "r")) + + source_force_path = str(Path.cwd()) + for force_file in ["ft", "fn", "fs", "fe"]: + Path(source_force_path + "/" + force_file + + ".txt").rename(str(case_dir_path) + "/" + force_file + ".txt") + + if save_fig: + source_plot_path = str(Path.cwd()) + "/plot.ps" + Path(source_plot_path).rename(str(case_dir_path) + "/plot.ps") + +# ================================================================================================= +# MAIN +# ================================================================================================= + + +def main(cpacs_path, cpacs_out_path): + log.info("----- Start of " + MODULE_NAME + " -----") + + results_dir = get_results_directory(module_name='PyAVL') + run_avl(cpacs_path, wkdir=results_dir) + + get_avl_results(cpacs_path, cpacs_out_path, wkdir=results_dir) + + log.info("----- End of " + MODULE_NAME + " -----") + + +if __name__ == "__main__": + cpacs_path = get_toolinput_file_path(MODULE_NAME) + cpacs_out_path = get_tooloutput_file_path(MODULE_NAME) + + main(cpacs_path, cpacs_out_path) diff --git a/ceasiompy/PyAVL/files/template.mass b/ceasiompy/PyAVL/files/template.mass new file mode 100644 index 000000000..9faeeb38e --- /dev/null +++ b/ceasiompy/PyAVL/files/template.mass @@ -0,0 +1,19 @@ +#------------------------------------------------- +# N+3 Config D8-1 +# +# Dimensional unit and parameter data. +# Mass & Inertia breakdown. +#------------------------------------------------- + +# Names and scalings for units to be used for trim and eigenmode calculations. +# The Lunit and Munit values scale the mass, xyz, and inertia table data below. +# Lunit value will also scale all lengths and areas in the AVL input file. +Lunit = 1.0 m +Munit = 1.0 kg +Tunit = 1.0 s + +#------------------------- +# Gravity and density to be used as default values in trim setup. +# Must be in the units given above. +g = 9.81 +rho = 1.2 diff --git a/ceasiompy/PyAVL/func/avlconfig.py b/ceasiompy/PyAVL/func/avlconfig.py new file mode 100644 index 000000000..4cddda23e --- /dev/null +++ b/ceasiompy/PyAVL/func/avlconfig.py @@ -0,0 +1,213 @@ +""" +CEASIOMpy: Conceptual Aircraft Design Software + +Developed by CFS ENGINEERING, 1015 Lausanne, Switzerland + +Script to get the flight conditions (alt, aoa, mach...) from +the input CPACS file, and write the command file for AVL. + +Python version: >=3.8 + +| Author: Romain Gauthier +| Creation: 2024-03-14 + +TODO: + + * Things to improve... + +""" + +# ============================================================================== +# IMPORTS +# ============================================================================== + +from pathlib import Path + +from ceasiompy.utils.ceasiomlogger import get_logger +from ceasiompy.utils.commonxpath import RANGE_XPATH, AVL_AEROMAP_UID_XPATH, AVL_PLOT_XPATH, AVL_VORTEX_DISTR_XPATH +from cpacspy.cpacsfunctions import get_value_or_default +from ceasiompy.utils.moduleinterfaces import get_module_path +from cpacspy.cpacspy import CPACS + +from ambiance import Atmosphere + +log = get_logger() + + +# ================================================================================================= +# FUNCTIONS +# ================================================================================================= +def write_command_file( + avl_path, + case_dir_path, + alpha, + beta, + mach_number, + ref_velocity, + ref_density, + g_acceleration, + save_plots): + """Function to write the command file for AVL. + + Function 'write_command_file' writes the command file to + execute for AVL calculations. + + Args: + avl_path (Path) : path to the AVL input file + case_dir_path (Path) : path to the run case directory + alpha (float) : angle of attack [deg] + beta (float) : angle of attack [deg] + mach_number (float) : Mach number + ref_velocity (float) : reference upstream velocity [m/s] + ref_density (float) : reference upstream density [kg/m^3] + g_acceleration (float) : gravitational acceleration [m/s^2] + + Returns: + avl_commands.txt : write the command AVL file. + command_path (Path) : path to the command file. + """ + + command_path = str(case_dir_path) + "/avl_commands.txt" + pyavl_dir = get_module_path("PyAVL") + mass_path = Path(pyavl_dir, "files", "template.mass") + + if save_plots: + with open(command_path, 'w') as command_file: + command_file.writelines(["load " + str(avl_path) + "\n", + "mass " + str(mass_path) + "\n", + "oper\n", + "g\n", + "h\n\n", + "a a " + str(alpha) + "\n", + "b b " + str(beta) + "\n", + "m\n", + "mn " + str(mach_number) + "\n", + "g " + str(g_acceleration) + "\n", + "d " + str(ref_density) + "\n", + "v " + str(ref_velocity) + "\n\n"]) + command_file.write("x\n") + command_file.writelines(["t\n", + "h\n\n"]) + command_file.writelines(["g\n", + "lo\n", + "h\n\n"]) + command_file.write("x\n") + for force_file in ["ft", "fn", "fs", "fe"]: + command_file.write(force_file + "\n") + command_file.write(str(Path.cwd()) + "/" + force_file + ".txt\n") + command_file.write("\n\n\n") + command_file.write("quit") + + # command_file.write("ft\n") + # command_file.write(str(Path.cwd()) + "/forces.txt\n") + # command_file.write("o\n\n\n") + # command_file.write("quit") + + else: # same without lines saving figures + with open(command_path, 'w') as command_file: + command_file.writelines(["load " + str(avl_path) + "\n", + "mass " + str(mass_path) + "\n", + "oper\n", + "a a " + str(alpha) + "\n", + "b b " + str(beta) + "\n", + "m\n", + "mn " + str(mach_number) + "\n", + "g " + str(g_acceleration) + "\n", + "d " + str(ref_density) + "\n", + "v " + str(ref_velocity) + "\n\n"]) + command_file.write("x\n") + for force_file in ["ft", "fn", "fs", "fe"]: + command_file.write(force_file + "\n") + command_file.write(str(Path.cwd()) + "/" + force_file + ".txt\n") + command_file.write("\n\n\n") + command_file.write("quit") + + return Path(command_path) + + +def get_aeromap_conditions(cpacs_path): + """Function read the flight conditions from the aeromap. + + Function 'get_aeromap_conditions' reads the flight conditions + (angle of attack, mach number...) from the aeromap of CEASIOMpy. + + Args: + cpacs_path (Path) : path to the cpacs input file + + Returns: + alt_list (list) : altitude of the cases. + mach_list (list) : mach number of the cases. + aoa_list (list) : angle of attack of the cases. + aos_list (list) : angle of sweep of the cases. + """ + cpacs = CPACS(cpacs_path) + + # Get the first aeroMap as default one or create automatically one + aeromap_list = cpacs.get_aeromap_uid_list() + + if aeromap_list: + aeromap_default = aeromap_list[0] + log.info(f'The aeromap is {aeromap_default}') + + aeromap_uid = get_value_or_default(cpacs.tixi, AVL_AEROMAP_UID_XPATH, aeromap_default) + + activate_aeromap = cpacs.get_aeromap_by_uid(aeromap_uid) + alt_list = activate_aeromap.get("altitude").tolist() + mach_list = activate_aeromap.get("machNumber").tolist() + aoa_list = activate_aeromap.get("angleOfAttack").tolist() + aos_list = activate_aeromap.get("angleOfSideslip").tolist() + + else: + default_aeromap = cpacs.create_aeromap("DefaultAeromap") + default_aeromap.description = "AeroMap created automatically" + + mach = get_value_or_default(cpacs.tixi, RANGE_XPATH + "/cruiseMach", 0.3) + alt = get_value_or_default(cpacs.tixi, RANGE_XPATH + "/cruiseAltitude", 10000) + + default_aeromap.add_row(alt=alt, mach=mach, aos=0.0, aoa=0.0) + default_aeromap.save() + + alt_list = [alt] + mach_list = [mach] + aoa_list = [0.0] + aos_list = [0.0] + + aeromap_uid = get_value_or_default(cpacs.tixi, AVL_AEROMAP_UID_XPATH, "DefaultAeromap") + log.info(f"{aeromap_uid} has been created") + + return alt_list, mach_list, aoa_list, aos_list + + +def get_option_settings(cpacs_path): + """Function read the setting of the graphical user interface. + + Function 'get_option_settings' reads the setting to use in AVL + from the graphical user interface of CEASIOMpy. + + Args: + cpacs_path (Path) : path to the cpacs input file + + Returns: + save_plots (bool) : to save the geometry and results figures. + vortex_distribution (float) : distribution of the vortices. + Nchordwise (int) : number of chordwise vortices. + Nspanwise (int) : number of spanwise vortices. + """ + cpacs = CPACS(cpacs_path) + + save_plots = get_value_or_default(cpacs.tixi, AVL_PLOT_XPATH, False) + vortex_distribution = get_value_or_default( + cpacs.tixi, AVL_VORTEX_DISTR_XPATH + "/Distribution", 1) + Nchordwise = get_value_or_default(cpacs.tixi, AVL_VORTEX_DISTR_XPATH + "/Nchordwise", 20) + Nspanwise = get_value_or_default(cpacs.tixi, AVL_VORTEX_DISTR_XPATH + "/Nspanwise", 20) + + return save_plots, vortex_distribution, Nchordwise, Nspanwise + + +# ================================================================================================= +# MAIN +# ================================================================================================= + +if __name__ == "__main__": + + log.info("Nothing to execute!") diff --git a/ceasiompy/PyAVL/func/avlresults.py b/ceasiompy/PyAVL/func/avlresults.py new file mode 100644 index 000000000..8bde787a8 --- /dev/null +++ b/ceasiompy/PyAVL/func/avlresults.py @@ -0,0 +1,141 @@ +""" +CEASIOMpy: Conceptual Aircraft Design Software + +Developed by CFS ENGINEERING, 1015 Lausanne, Switzerland + +Extract results from AVL calculations and save them in a CPACS file. + +Python version: >=3.8 + +| Author: Romain Gauthier +| Creation: 2024-03-18 + +TODO: + + * + +""" + +# ================================================================================================= +# IMPORTS +# ================================================================================================= + +from pathlib import Path + +from ceasiompy.utils.ceasiomlogger import get_logger +from ceasiompy.utils.commonxpath import CEASIOMPY_XPATH + +from cpacspy.cpacsfunctions import get_value +from cpacspy.cpacspy import CPACS + +log = get_logger() + + +# ================================================================================================= +# CLASSES +# ================================================================================================= + + +# ================================================================================================= +# FUNCTIONS +# ================================================================================================= + +def get_avl_aerocoefs(force_file): + """Get aerodynamic coefficients and velocity from AVL forces file (forces.txt) + + Args: + force_file (Path): Path to the SU2 forces file + + Returns: + cl, cd, cs, cmd, cms, cml: Aerodynamic coefficients + """ + + if not force_file.is_file(): + raise FileNotFoundError(f"The AVL forces file '{force_file}' has not been found!") + + cl, cd = None, None + + with open(force_file) as f: + for line in f.readlines(): + if "CLtot" in line: + cl = float(line.split("=")[1].strip()) + if "CDtot" in line: + cd = float(line.split("=")[1].strip()) + if "Cmtot" in line: + cm = float(line.split("=")[2].strip()) + + return cl, cd, cm + + +def get_avl_results(cpacs_path, cpacs_out_path, wkdir): + """Function to write AVL results in a CPACS file. + + Function 'get_avl_results' gets available results from the latest AVL calculation and put them + at the correct place in the CPACS file. + + '/cpacs/vehicles/aircraft/model/analyses/aeroPerformance/aeroMap[n]/aeroPerformanceMap' + + Args: + cpacs_path (Path): Path to input CPACS file + cpacs_out_path (Path): Path to output CPACS file + wkdir (Path): Path to the working directory + + """ + + cpacs = CPACS(cpacs_path) + AVL_XPATH = CEASIOMPY_XPATH + "/aerodynamics/avl" + AVL_AEROMAP_UID_XPATH = AVL_XPATH + "/aeroMapUID" + + if not wkdir.exists(): + raise OSError(f"The working directory : {wkdir} does not exit!") + + aeromap_uid = get_value(cpacs.tixi, AVL_AEROMAP_UID_XPATH) + + log.info(f"The aeromap uid is: {aeromap_uid}") + aeromap = cpacs.get_aeromap_by_uid(aeromap_uid) + + alt_list = aeromap.get("altitude").tolist() + mach_list = aeromap.get("machNumber").tolist() + aoa_list = aeromap.get("angleOfAttack").tolist() + aos_list = aeromap.get("angleOfSideslip").tolist() + + case_dir_list = [case_dir for case_dir in wkdir.iterdir() if "Case" in case_dir.name] + + for config_dir in sorted(case_dir_list): + + if not config_dir.is_dir(): + continue + + force_file_path = Path(config_dir, "ft.txt") + if not force_file_path.exists(): + raise OSError("No result force file have been found!") + + case_nb = int(config_dir.name.split("_")[0].split("Case")[1]) + + aoa = aoa_list[case_nb] + aos = aos_list[case_nb] + mach = mach_list[case_nb] + alt = alt_list[case_nb] + + cl, cd, cm = get_avl_aerocoefs(force_file_path) + + aeromap.add_coefficients( + alt=alt, + mach=mach, + aos=aos, + aoa=aoa, + cd=cd, + cl=cl, + cms=cm + ) + aeromap.save() + cpacs.save_cpacs(cpacs_out_path, overwrite=True) + + +# ================================================================================================= +# MAIN +# ================================================================================================= + +if __name__ == "__main__": + + log.info("Nothing to execute!") diff --git a/ceasiompy/PyAVL/func/cpacs2avl.py b/ceasiompy/PyAVL/func/cpacs2avl.py new file mode 100644 index 000000000..d82727629 --- /dev/null +++ b/ceasiompy/PyAVL/func/cpacs2avl.py @@ -0,0 +1,659 @@ +""" +CEASIOMpy: Conceptual Aircraft Design Software + +Developed by CFS ENGINEERING, 1015 Lausanne, Switzerland + +Script to convert CPACS file geometry into AVL geometry + +Python version: >=3.8 + +| Author: Romain Gauthier +| Adapted from : Aidan Jungo script cpacs2sumo.py +| Creation: 2024-03-14 + +TODO: + + * Improve link between wings and fuselages parts + +""" + +# ============================================================================== +# IMPORTS +# ============================================================================== + +from ceasiompy.utils.ceasiompyutils import get_results_directory +import math +import numpy as np +from pathlib import Path +from scipy import interpolate + +from ceasiompy.CPACS2SUMO.func.getprofile import get_profile_coord + +from ceasiompy.utils.ceasiomlogger import get_logger +from ceasiompy.utils.commonxpath import WINGS_XPATH, FUSELAGES_XPATH, REF_XPATH + +from ceasiompy.utils.generalclasses import SimpleNamespace, Transformation +from ceasiompy.utils.mathfunctions import euler2fix +from cpacspy.cpacsfunctions import open_tixi + +from ceasiompy.PyAVL.func.rotation3D import rotate_3D_points +from ceasiompy.PyAVL.func.avlconfig import get_option_settings + +log = get_logger() + +# ================================================================================================= +# FUNCTIONS +# ================================================================================================= + + +def convert_cpacs_to_avl(cpacs_path, wkdir): + """Function to convert a CPACS file geometry into an AVL file geometry. + + Function 'convert_cpacs_to_avl' opens an input CPACS file with TIXI handle + and converts every element (as much as possible) in the AVL (.avl) format. + The output sumo file is saved in ... + + Source: + * https://github.com/cfsengineering/CEASIOMpy/blob/main/ceasiompy/CPACS2SUMO/cpacs2sumo.py + + Args: + cpacs_path (Path) : path to the CPACS input file + + Returns: + aircraft.avl : write the input AVL file + avl_path (Path) : path to the AVL input file + """ + tixi = open_tixi(cpacs_path) + + # Get the aircraft name + name_aircraft = tixi.getTextElement("/cpacs/header/name") + + results_dir = get_results_directory("PyAVL") + results_path = str(results_dir) + avl_path = str(results_dir) + "/" + name_aircraft + ".avl" + + with open(avl_path, 'w') as avl_file: + avl_file.write(name_aircraft + "\n\n") + + # Get the flight conditions + FLIGHT_XPATH = "/cpacs/vehicles/aircraft/model/analyses/aeroPerformance/aeroMap[1]/aeroPerformanceMap" + mach = tixi.getDoubleElement(FLIGHT_XPATH + '/machNumber') + AoA = tixi.getDoubleElement(FLIGHT_XPATH + '/angleOfAttack') + with open(avl_path, 'a') as avl_file: + # Mach number + avl_file.write('#Mach\n') + avl_file.write(str(mach) + "\n\n") + # Symmetry + avl_file.write("#IYsym IZsym Zsym\n") + avl_file.write("0\t0\t0\n\n") + + # Get the reference dimensions + area_ref = tixi.getDoubleElement(REF_XPATH + '/area') + chord_ref = tixi.getDoubleElement(REF_XPATH + '/length') + span_ref = area_ref / chord_ref + points_ref = np.array([tixi.getDoubleElement(REF_XPATH + '/point/x'), + tixi.getDoubleElement(REF_XPATH + '/point/y'), + tixi.getDoubleElement(REF_XPATH + '/point/z')]) + with open(avl_path, 'a') as avl_file: + # Reference dimensions + avl_file.write("#Sref Cref Bref\n") + avl_file.write(f"{area_ref:.3f}\t{chord_ref:.3f}\t{span_ref:.3f}\n\n") + # Reference location for moments/rotations + avl_file.write("#Xref Yref Zref\n") + for i_points in range(3): + avl_file.write(f"{points_ref[i_points]:.3f}\t") + avl_file.write("\n\n") + + # Fuselage(s) --------------------------------------------------------------- + + if tixi.checkElement(FUSELAGES_XPATH): + fus_cnt = tixi.getNamedChildrenCount(FUSELAGES_XPATH, "fuselage") + log.info(str(fus_cnt) + " fuselage has been found.") + else: + fus_cnt = 0 + log.warning("No fuselage has been found in this CPACS file!") + + for i_fus in reversed(range(fus_cnt)): + fus_xpath = FUSELAGES_XPATH + "/fuselage[" + str(i_fus + 1) + "]" + fus_uid = tixi.getTextAttribute(fus_xpath, "uID") + fus_transf = Transformation() + fus_transf.get_cpacs_transf(tixi, fus_xpath) + + body_transf = Transformation() + body_transf.translation = fus_transf.translation + + # Convert angles + body_transf.rotation = euler2fix(fus_transf.rotation) + + # Add body origin + body_ori_str = ( + str(body_transf.translation.x) + + "\t" + + str(body_transf.translation.y) + + "\t" + + str(body_transf.translation.z) + ) + + # Write fuselage settings + with open(avl_path, 'a') as avl_file: + avl_file.write("#--------------------------------------------------\n") + avl_file.write("BODY\n") + avl_file.write("Fuselage\n\n") + avl_file.write("!Nbody Bspace\n") + avl_file.write("100\t1.0\n\n") + + # Scaling + avl_file.write("SCALE\n") + avl_file.write(str(fus_transf.scaling.x) + + "\t" + + str(fus_transf.scaling.y) + + "\t" + + str(fus_transf.scaling.z) + + "\n\n") + + # Translation + avl_file.write("TRANSLATE\n") + avl_file.write(body_ori_str + "\n\n") + + # avl_file.write("NOWAKE\n\n") + + # Positionings + if tixi.checkElement(fus_xpath + "/positionings"): + pos_cnt = tixi.getNamedChildrenCount(fus_xpath + "/positionings", "positioning") + log.info(str(fus_cnt) + ' "Positioning" has been found : ') + + pos_x_list = [] + pos_y_list = [] + pos_z_list = [] + from_sec_list = [] + to_sec_list = [] + + for i_pos in range(pos_cnt): + pos_xpath = fus_xpath + "/positionings/positioning[" + str(i_pos + 1) + "]" + + length = tixi.getDoubleElement(pos_xpath + "/length") + sweep_deg = tixi.getDoubleElement(pos_xpath + "/sweepAngle") + sweep = math.radians(sweep_deg) + dihedral_deg = tixi.getDoubleElement(pos_xpath + "/dihedralAngle") + dihedral = math.radians(dihedral_deg) + + # Get the corresponding translation of each positioning + pos_x_list.append(length * math.sin(sweep)) + pos_y_list.append(length * math.cos(dihedral) * math.cos(sweep)) + pos_z_list.append(length * math.sin(dihedral) * math.cos(sweep)) + + # Get which section are connected by the positioning + if tixi.checkElement(pos_xpath + "/fromSectionUID"): + from_sec = tixi.getTextElement(pos_xpath + "/fromSectionUID") + else: + from_sec = "" + from_sec_list.append(from_sec) + + if tixi.checkElement(pos_xpath + "/toSectionUID"): + to_sec = tixi.getTextElement(pos_xpath + "/toSectionUID") + else: + to_sec = "" + to_sec_list.append(to_sec) + + # Re-loop though the positioning to re-order them + for j_pos in range(pos_cnt): + if from_sec_list[j_pos] == "": + prev_pos_x = 0 + prev_pos_y = 0 + prev_pos_z = 0 + + elif from_sec_list[j_pos] == to_sec_list[j_pos - 1]: + prev_pos_x = pos_x_list[j_pos - 1] + prev_pos_y = pos_y_list[j_pos - 1] + prev_pos_z = pos_z_list[j_pos - 1] + + else: + index_prev = to_sec_list.index(from_sec_list[j_pos]) + prev_pos_x = pos_x_list[index_prev] + prev_pos_y = pos_y_list[index_prev] + prev_pos_z = pos_z_list[index_prev] + + pos_x_list[j_pos] += prev_pos_x + pos_y_list[j_pos] += prev_pos_y + pos_z_list[j_pos] += prev_pos_z + + else: + log.warning('No "positionings" have been found!') + pos_cnt = 0 + + # Sections + sec_cnt = tixi.getNamedChildrenCount(fus_xpath + "/sections", "section") + log.info(" -" + str(sec_cnt) + " fuselage sections have been found") + x_fuselage = np.zeros(sec_cnt) + y_fuselage_top = np.zeros(sec_cnt) + y_fuselage_bottom = np.zeros(sec_cnt) + fus_radius_vec = np.zeros(sec_cnt) + body_width_vec = np.zeros(sec_cnt) + body_height_vec = np.zeros(sec_cnt) + + if pos_cnt == 0: + pos_x_list = [0.0] * sec_cnt + pos_y_list = [0.0] * sec_cnt + pos_z_list = [0.0] * sec_cnt + + for i_sec in range(sec_cnt): + sec_xpath = fus_xpath + "/sections/section[" + str(i_sec + 1) + "]" + sec_uid = tixi.getTextAttribute(sec_xpath, "uID") + + sec_transf = Transformation() + sec_transf.get_cpacs_transf(tixi, sec_xpath) + + if sec_transf.rotation.x or sec_transf.rotation.y or sec_transf.rotation.z: + log.warning( + f"Sections '{sec_uid}' is rotated, it is" + "not possible to take that into account in SUMO !" + ) + + # Elements + elem_cnt = tixi.getNamedChildrenCount(sec_xpath + "/elements", "element") + + if elem_cnt > 1: + log.warning( + "Sections " + + sec_uid + + " contains multiple \ + element, it could be an issue for the conversion \ + to SUMO!" + ) + + for i_elem in range(elem_cnt): + elem_xpath = sec_xpath + "/elements/element[" + str(i_elem + 1) + "]" + elem_uid = tixi.getTextAttribute(elem_xpath, "uID") + + elem_transf = Transformation() + elem_transf.get_cpacs_transf(tixi, elem_xpath) + + if elem_transf.rotation.x or elem_transf.rotation.y or elem_transf.rotation.z: + log.warning( + f"Element '{elem_uid}' is rotated, it is" + "not possible to take that into account in SUMO !" + ) + + # Fuselage profiles + prof_uid = tixi.getTextElement(elem_xpath + "/profileUID") + prof_vect_x, prof_vect_y, prof_vect_z = get_profile_coord(tixi, prof_uid) + + prof_size_y = (max(prof_vect_y) - min(prof_vect_y)) / 2 + prof_size_z = (max(prof_vect_z) - min(prof_vect_z)) / 2 + + prof_vect_y[:] = [y / prof_size_y for y in prof_vect_y] + prof_vect_z[:] = [z / prof_size_z for z in prof_vect_z] + + prof_min_y = min(prof_vect_y) + prof_min_z = min(prof_vect_z) + + prof_vect_y[:] = [y - 1 - prof_min_y for y in prof_vect_y] + prof_vect_z[:] = [z - 1 - prof_min_z for z in prof_vect_z] + + # Could be a problem if they are less positionings than sections + # TODO: solve that! + pos_y_list[i_sec] += ((1 + prof_min_y) * prof_size_y) * elem_transf.scaling.y + pos_z_list[i_sec] += ((1 + prof_min_z) * prof_size_z) * elem_transf.scaling.z + + # Compute coordinates of the center of section + body_frm_center_x = ( + elem_transf.translation.x + sec_transf.translation.x + pos_x_list[i_sec] + ) * fus_transf.scaling.x + + body_frm_center_y = ( + elem_transf.translation.y * sec_transf.scaling.y + + sec_transf.translation.y + + pos_y_list[i_sec] + ) * fus_transf.scaling.y + + body_frm_center_z = ( + elem_transf.translation.z * sec_transf.scaling.z + + sec_transf.translation.z + + pos_z_list[i_sec] + ) * fus_transf.scaling.z + + # Compute height and width of the section + body_frm_height = ( + prof_size_z + * 2 + * elem_transf.scaling.z + * sec_transf.scaling.z + * fus_transf.scaling.z + ) + body_frm_width = ( + prof_size_y + * 2 + * elem_transf.scaling.y + * sec_transf.scaling.y + * fus_transf.scaling.y + ) + + # Compute diameter of the section as the mean between height and width + # AVL assumes only circular cross section for fuselage + fus_radius = np.mean([body_frm_height, body_frm_width]) / 2 + fus_radius_vec[i_sec] = (fus_radius) + + # Save the coordinates of the fuselage + x_fuselage[i_sec] = body_frm_center_x + y_fuselage_top[i_sec] = body_frm_center_z + fus_radius + y_fuselage_bottom[i_sec] = body_frm_center_z - fus_radius + + body_width_vec[i_sec] = body_frm_width + body_height_vec[i_sec] = body_frm_height + + fus_z_profile = interpolate.interp1d( + x_fuselage + body_transf.translation.x, y_fuselage_top - fus_radius_vec) + fus_radius_profile = interpolate.interp1d( + x_fuselage + body_transf.translation.x, fus_radius_vec) + + fus_dat_path = results_path + "/" + fus_uid + ".dat" + + with open(fus_dat_path, 'w') as fus_file: + fus_file.write("fuselage" + str(i_fus + 1) + "\n") + + # Write coordinates of the top surface + for x_fus, y_fus in reversed(list(zip(x_fuselage[1:], y_fuselage_top[1:]))): + # fus_file.write(str(x_fus) + "\t" + str(y_fus) + "\n") + fus_file.write(f"{x_fus:.3f}\t{y_fus:.3f}\n") + + # Write coordinates of the nose of the fuselage + y_nose = np.mean([y_fuselage_top[0], y_fuselage_bottom[0]]) + # fus_file.write(str(x_fuselage[0]) + "\t" + str(y_nose) + "\n") + fus_file.write(f"{x_fuselage[0]:.3f}\t{y_nose:.3f}\n") + + # Write coordinates of the bottom surface + for x_fus, y_fus in zip(x_fuselage[1:], y_fuselage_bottom[1:]): + # fus_file.write(str(x_fus) + "\t" + str(y_fus) + "\n") + fus_file.write(f"{x_fus:.3f}\t{y_fus:.3f}\n") + + with open(avl_path, 'a') as avl_file: + avl_file.write("BFILE\n") + avl_file.write(fus_dat_path + "\n\n") + + # Wing(s) ------------------------------------------------------------------ + if tixi.checkElement(WINGS_XPATH): + wing_cnt = tixi.getNamedChildrenCount(WINGS_XPATH, "wing") + log.info(str(wing_cnt) + " wings has been found.") + else: + wing_cnt = 0 + log.warning("No wings has been found in this CPACS file!") + + _, vortex_distribution, Nchordwise, Nspanwise = get_option_settings(cpacs_path) + if vortex_distribution > 3 or vortex_distribution < -3: + log.warning( + "The vortex distribution is not in the range [-3 ; 3]. Default value of 1 will be used.") + vortex_distribution = 1 + + for i_wing in range(wing_cnt): + root_defined = False + wing_xpath = WINGS_XPATH + "/wing[" + str(i_wing + 1) + "]" + wing_transf = Transformation() + wing_transf.get_cpacs_transf(tixi, wing_xpath) + + # Create a class for the transformation of the WingSkeleton + wg_sk_transf = Transformation() + + # Convert WingSkeleton rotation + wg_sk_transf.rotation = euler2fix(wing_transf.rotation) + + # Add WingSkeleton origin + wg_sk_transf.translation = wing_transf.translation + wg_sk_ori_str = ( + str(round(wg_sk_transf.translation.x, 3)) + + "\t" + + str(round(wg_sk_transf.translation.y, 3)) + + "\t" + + str(round(wg_sk_transf.translation.z, 3)) + ) + + # Write wing settings + with open(avl_path, 'a') as avl_file: + avl_file.write("#--------------------------------------------------\n") + avl_file.write("SURFACE\n") + avl_file.write("Wing\n\n") + avl_file.write("!Nchordwise Cspace Nspanwise Sspace\n") + avl_file.write( + f"{Nchordwise} {vortex_distribution} {Nspanwise} {vortex_distribution}\n\n") + avl_file.write('COMPONENT\n') + avl_file.write("1\n\n") + + # Symmetry + if tixi.checkAttribute(wing_xpath, "symmetry"): + if tixi.getTextAttribute(wing_xpath, "symmetry") == "x-z-plane": + avl_file.write('YDUPLICATE\n') + avl_file.write("0\n\n") + + # Angle + avl_file.write('ANGLE\n') + avl_file.write(str(AoA) + "\n\n") + + # Scaling + avl_file.write("SCALE\n") + avl_file.write(str(wing_transf.scaling.x) + + "\t" + + str(wing_transf.scaling.y) + + "\t" + + str(wing_transf.scaling.z) + + "\n\n") + + # Translation + avl_file.write("TRANSLATE\n") + avl_file.write(wg_sk_ori_str + "\n\n") + + # Positionings + if tixi.checkElement(wing_xpath + "/positionings"): + pos_cnt = tixi.getNamedChildrenCount(wing_xpath + "/positionings", "positioning") + log.info(str(pos_cnt) + ' "positioning" has been found : ') + + pos_x_list = [] + pos_y_list = [] + pos_z_list = [] + from_sec_list = [] + to_sec_list = [] + + for i_pos in range(pos_cnt): + pos_xpath = wing_xpath + "/positionings/positioning[" + str(i_pos + 1) + "]" + + length = tixi.getDoubleElement(pos_xpath + "/length") + sweep_deg = tixi.getDoubleElement(pos_xpath + "/sweepAngle") + sweep = math.radians(sweep_deg) + dihedral_deg = tixi.getDoubleElement(pos_xpath + "/dihedralAngle") + dihedral = math.radians(dihedral_deg) + + # Get the corresponding translation of each positioning + pos_x_list.append(length * math.sin(sweep)) + pos_y_list.append(length * math.cos(dihedral) * math.cos(sweep)) + pos_z_list.append(length * math.sin(dihedral) * math.cos(sweep)) + + # Get which section are connected by the positioning + if tixi.checkElement(pos_xpath + "/fromSectionUID"): + from_sec = tixi.getTextElement(pos_xpath + "/fromSectionUID") + else: + from_sec = "" + from_sec_list.append(from_sec) + + if tixi.checkElement(pos_xpath + "/toSectionUID"): + to_sec = tixi.getTextElement(pos_xpath + "/toSectionUID") + else: + to_sec = "" + to_sec_list.append(to_sec) + + # Re-loop though the positioning to re-order them + for j_pos in range(pos_cnt): + if from_sec_list[j_pos] == "": + prev_pos_x = 0 + prev_pos_y = 0 + prev_pos_z = 0 + elif from_sec_list[j_pos] == to_sec_list[j_pos - 1]: + prev_pos_x = pos_x_list[j_pos - 1] + prev_pos_y = pos_y_list[j_pos - 1] + prev_pos_z = pos_z_list[j_pos - 1] + else: + index_prev = to_sec_list.index(from_sec_list[j_pos]) + prev_pos_x = pos_x_list[index_prev] + prev_pos_y = pos_y_list[index_prev] + prev_pos_z = pos_z_list[index_prev] + + pos_x_list[j_pos] += prev_pos_x + pos_y_list[j_pos] += prev_pos_y + pos_z_list[j_pos] += prev_pos_z + + else: + log.warning('No "positionings" have been found!') + pos_cnt = 0 + + # Sections + sec_cnt = tixi.getNamedChildrenCount(wing_xpath + "/sections", "section") + log.info(" -" + str(sec_cnt) + " wing sections have been found") + + if pos_cnt == 0: + pos_x_list = [0.0] * sec_cnt + pos_y_list = [0.0] * sec_cnt + pos_z_list = [0.0] * sec_cnt + + for i_sec in range(sec_cnt): + sec_xpath = wing_xpath + "/sections/section[" + str(i_sec + 1) + "]" + sec_uid = tixi.getTextAttribute(sec_xpath, "uID") + sec_transf = Transformation() + sec_transf.get_cpacs_transf(tixi, sec_xpath) + + # Elements + elem_cnt = tixi.getNamedChildrenCount(sec_xpath + "/elements", "element") + + if elem_cnt > 1: + log.warning( + f"Sections {sec_uid} contains multiple element," + " it could be an issue for the conversion to SUMO!" + ) + + for i_elem in range(elem_cnt): + elem_xpath = sec_xpath + "/elements/element[" + str(i_elem + 1) + "]" + elem_transf = Transformation() + elem_transf.get_cpacs_transf(tixi, elem_xpath) + + # Get wing profile (airfoil) + prof_uid = tixi.getTextElement(elem_xpath + "/airfoilUID") + prof_vect_x, prof_vect_y, prof_vect_z = get_profile_coord(tixi, prof_uid) + foil_dat_path = results_path + "/" + prof_uid + ".dat" + + with open(foil_dat_path, 'w') as dat_file: + dat_file.write(prof_uid + "\n") + # Limit the number of points to 100 (otherwise AVL error) + if len(prof_vect_x) < 100: + for coord_x, coord_z in zip(prof_vect_x, prof_vect_z): + dat_file.write(str(coord_x) + '\t' + str(coord_z) + "\n") + else: + step = round(len(prof_vect_x) / 100) + for coord_x, coord_z in zip(prof_vect_x[0:len(prof_vect_x):step], prof_vect_z[0:len(prof_vect_x):step]): + dat_file.write(str(coord_x) + '\t' + str(coord_z) + "\n") + + # Apply scaling + for i, item in enumerate(prof_vect_x): + prof_vect_x[i] = ( + item * elem_transf.scaling.x * sec_transf.scaling.x * wing_transf.scaling.x + ) + for i, item in enumerate(prof_vect_y): + prof_vect_y[i] = ( + item * elem_transf.scaling.y * sec_transf.scaling.y * wing_transf.scaling.y + ) + for i, item in enumerate(prof_vect_z): + prof_vect_z[i] = ( + item * elem_transf.scaling.z * sec_transf.scaling.z * wing_transf.scaling.z + ) + + prof_size_x = max(prof_vect_x) - min(prof_vect_x) + prof_size_y = max(prof_vect_y) - min(prof_vect_y) + + if prof_size_y == 0: + prof_vect_x[:] = [x / prof_size_x for x in prof_vect_x] + prof_vect_z[:] = [z / prof_size_x for z in prof_vect_z] + # Is it correct to divide by prof_size_x ???? + + wg_sec_chord = prof_size_x + else: + log.error("An airfoil profile is not define correctly") + + # Add rotation from element and sections + # Adding the two angles: Maybe not work in every case!!! + add_rotation = SimpleNamespace() + add_rotation.x = elem_transf.rotation.x + sec_transf.rotation.x + wg_sk_transf.rotation.x + add_rotation.y = elem_transf.rotation.y + sec_transf.rotation.y + wg_sk_transf.rotation.y + add_rotation.z = elem_transf.rotation.z + sec_transf.rotation.z + wg_sk_transf.rotation.z + + # Get Section rotation + wg_sec_rot = euler2fix(add_rotation) + wg_sec_dihed = math.radians(wg_sec_rot.x) + wg_sec_twist = math.radians(wg_sec_rot.y) + wg_sec_yaw = math.radians(wg_sec_rot.z) + + # Define the leading edge position from translations + x_LE = sec_transf.translation.x + elem_transf.translation.x + y_LE = sec_transf.translation.y + elem_transf.translation.y + z_LE = sec_transf.translation.z + elem_transf.translation.z + + if all(abs(value) < 1e-6 for value in pos_y_list): + x_LE_rot, y_LE_rot, z_LE_rot = rotate_3D_points( + x_LE, y_LE, z_LE, wg_sec_dihed, wg_sec_twist, wg_sec_yaw) + else: + x_LE_rot, y_LE_rot, z_LE_rot = rotate_3D_points( + pos_x_list[i_sec], pos_y_list[i_sec], pos_z_list[i_sec], wg_sec_dihed, wg_sec_twist, wg_sec_yaw) + + ''' + + if ((pos_x_list == np.zeros(shape=(len(pos_x_list)))).all()): + if ((pos_y_list == np.zeros(shape=(len(pos_y_list)))).all()): + if ((pos_z_list == np.zeros(shape=(len(pos_z_list)))).all()): + x_LE_rot, y_LE_rot, z_LE_rot = rotate_3D_points( + x_LE, y_LE, z_LE, wg_sec_dihed, wg_sec_twist, wg_sec_yaw) + else: + x_LE_rot, y_LE_rot, z_LE_rot = rotate_3D_points( + pos_x_list[i_sec], pos_y_list[i_sec], pos_z_list[i_sec], wg_sec_dihed, wg_sec_twist, wg_sec_yaw) + ''' + + # Compute the absolute location of the leading edge + x_LE_abs = x_LE_rot + wg_sk_transf.translation.x + y_LE_abs = y_LE_rot + wg_sk_transf.translation.y + z_LE_abs = z_LE_rot + wg_sk_transf.translation.z + + # Compute the radius of the fuselage and the height difference ... + # between fuselage center and leading edge + radius_fus = fus_radius_profile(x_LE_abs + wg_sec_chord / 2) + fus_z_center = fus_z_profile(x_LE_abs + wg_sec_chord / 2) + delta_z = np.abs(fus_z_center + body_transf.translation.z - z_LE_abs) + + # If the root wing section is inside the fuselage, translate it to the fuselage border + # To make sure there is no wing part inside the fuselage + if np.sqrt((y_LE_abs)**2 + (delta_z)**2) < radius_fus and wg_sec_dihed < math.pi / 2 and root_defined == False: + y_LE_abs += np.sqrt(radius_fus**2 - delta_z**2) - y_LE_abs + y_LE_rot = y_LE_abs - wg_sk_transf.translation.y + root_defined = True + with open(avl_path, 'a') as avl_file: + avl_file.write("#---------------\n") + avl_file.write("SECTION\n") + avl_file.write("#Xle Yle Zle Chord Ainc\n") + avl_file.write( + f"{x_LE_rot:.3f} {y_LE_rot:.3f} {z_LE_rot:.3f} {(wg_sec_chord):.3f} {wg_sec_rot.y}\n\n") + avl_file.write("AFILE\n") + avl_file.write(foil_dat_path + "\n\n") + + elif np.sqrt((y_LE_abs)**2 + (delta_z)**2) > radius_fus or wg_sec_dihed > 0.95 * math.pi / 2: + # Write the leading edge coordinates and the airfoil file + with open(avl_path, 'a') as avl_file: + avl_file.write("#---------------\n") + avl_file.write("SECTION\n") + avl_file.write("#Xle Yle Zle Chord Ainc\n") + avl_file.write( + f"{x_LE_rot:.3f} {y_LE_rot:.3f} {z_LE_rot:.3f} {(wg_sec_chord):.3f} {wg_sec_rot.y}\n\n") + avl_file.write("AFILE\n") + avl_file.write(foil_dat_path + "\n\n") + + return Path(avl_path) + + +# ================================================================================================= +# MAIN +# ================================================================================================= + +if __name__ == "__main__": + + log.info("Nothing to execute!") diff --git a/ceasiompy/PyAVL/func/rotation3D.py b/ceasiompy/PyAVL/func/rotation3D.py new file mode 100644 index 000000000..0591b882a --- /dev/null +++ b/ceasiompy/PyAVL/func/rotation3D.py @@ -0,0 +1,74 @@ +""" +CEASIOMpy: Conceptual Aircraft Design Software + +Developed by CFS ENGINEERING, 1015 Lausanne, Switzerland + +Function to apply a 3D rotation to the coordinates of a point. + +Python version: >=3.8 + +| Author: Romain Gauthier +| Creation: 2024-03-13 + +""" + + +# ============================================================================== +# IMPORTS +# ============================================================================== + +from ceasiompy.utils.ceasiomlogger import get_logger +import numpy as np +import math + +log = get_logger() + + +# ============================================================================== +# FUNCTIONS +# ============================================================================== + + +def rotate_3D_points(x, y, z, angle_x, angle_y, angle_z): + """Function to apply a 3D rotation to the coordinates of a point + + Function 'rotate_3D_points' returns the rotated points after applying + a 3D rotation matrix. + + Source: + * https://en.wikipedia.org/wiki/Rotation_matrix + + Args: + x (float): x coordinate of the initial point + y (float): y coordinate of the initial point + z (float): z coordinate of the initial point + angle_x (float): rotation angle around x-axis [rad] + angle_y (float): rotation angle around y-axis [rad] + angle_z (float): rotation angle around z-axis [rad] + + Returns: + x_rot (float): x coordinate of the rotated point + y_rot (float): y coordinate of the rotated point + z_rot (float): z coordinate of the rotated point + """ + rotation_matrix = np.array([[math.cos(angle_z) * math.cos(angle_y), math.cos(angle_z) * math.sin(angle_y) * math.sin(angle_x) - math.sin(angle_z) * math.cos(angle_x), math.cos(angle_z) * math.sin(angle_y) * math.cos(angle_x) + math.sin(angle_z) * math.sin(angle_x)], + [math.sin(angle_z) * math.cos(angle_y), math.sin(angle_z) * math.sin(angle_y) * math.sin(angle_x) + math.cos(angle_z) * math.cos( + angle_x), math.sin(angle_z) * math.sin(angle_y) * math.cos(angle_x) - math.cos(angle_z) * math.sin(angle_x)], + [-math.sin(angle_y), math.cos(angle_y) * math.sin(angle_x), math.cos(angle_y) * math.cos(angle_x)]]) + x_rot = x * rotation_matrix[0, 0] + y * \ + rotation_matrix[0, 1] + z * rotation_matrix[0, 2] + y_rot = x * rotation_matrix[1, 0] + y * \ + rotation_matrix[1, 1] + z * rotation_matrix[1, 2] + z_rot = x * rotation_matrix[2, 0] + y * \ + rotation_matrix[2, 1] + z * rotation_matrix[2, 2] + + return x_rot, y_rot, z_rot + + +# ============================================================================== +# MAIN +# ============================================================================== + +if __name__ == "__main__": + + print("Nothing to execute!") diff --git a/ceasiompy/PyAVL/tests/test_moduletemplate.py b/ceasiompy/PyAVL/tests/test_moduletemplate.py new file mode 100644 index 000000000..e833cdbd6 --- /dev/null +++ b/ceasiompy/PyAVL/tests/test_moduletemplate.py @@ -0,0 +1,95 @@ +""" +CEASIOMpy: Conceptual Aircraft Design Software + +Developed by CFS ENGINEERING, 1015 Lausanne, Switzerland + +Test functions for 'lib/ModuleTemplate/moduletemplate.py' + +Python version: >=3.8 + +| Author : Aidan Jungo +| Creation: 2019-08-14 + +""" + +# ================================================================================================= +# IMPORTS +# ================================================================================================= + + +from pathlib import Path + +import pytest +from ceasiompy.ModuleTemplate.func.subfunc import my_subfunc +from ceasiompy.ModuleTemplate.moduletemplate import MyClass, get_fuselage_scaling, sum_funcion +from pytest import approx + +MODULE_DIR = Path(__file__).parent +CPACS_IN_PATH = Path(MODULE_DIR, "ToolInput", "simpletest_cpacs.xml") +CPACS_OUT_PATH = Path(MODULE_DIR, "ToolOutput", "ToolOutput.xml") + + +# ================================================================================================= +# CLASSES +# ================================================================================================= + + +# ================================================================================================= +# FUNCTIONS +# ================================================================================================= + + +def test_MyClass(): + """Test Class 'MyClass'""" + + TestClass = MyClass() + + assert TestClass.var_a == 1.1 + assert TestClass.var_b == 2.2 + assert TestClass.var_c == 0.0 + + TestClass.add_my_var() + assert TestClass.var_c == approx(3.3) + + +def test_sum_funcion(): + """Test function 'sum_funcion'""" + + # Test Raise ValueError + with pytest.raises(ValueError): + sum_funcion(5.5, 4.4) + + # Test 'sum_funcion' normal use + assert sum_funcion(5, 4.4) == approx(9.4) + + +def test_get_fuselage_scaling(): + """Test function 'get_fuselage_scaling'""" + + x, y, z = get_fuselage_scaling(CPACS_IN_PATH, CPACS_OUT_PATH) + + assert x == approx(1) + assert y == approx(0.5) + assert z == approx(0.5) + + +def test_subfunc(): + """Test subfunction 'my_subfunc'""" + + a = "a" + b = "b" + + res = my_subfunc(a, b) + + assert res == "a and b" + + +# ================================================================================================= +# MAIN +# ================================================================================================= + +if __name__ == "__main__": + + print("Test ModuleTemplate") + print("To run test use the following command:") + print(">> pytest -v")