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")