diff --git a/weight_and_balance_analysis/main.py b/weight_and_balance_analysis/main.py index 6e0a4dad7b66a2ec6ad0178ab58c89441ca81543..824beb3581a69cab04ad89eeeec1ad5a1da88515 100644 --- a/weight_and_balance_analysis/main.py +++ b/weight_and_balance_analysis/main.py @@ -1,4 +1,24 @@ -"""Calculation module main file.""" +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + + # Import standard modules. import sys import logging @@ -13,9 +33,9 @@ from src.datapostprocessing import data_postprocessing def main(): - """Execute the main program for cost estimation. + """Execute the main program for weight and balance analysis. - This function serves as the main entry point for performing the cost estimation. + This function serves as the main entry point for performing the weight and balance analysis. It goes through the following key steps: (1) Preprocessing - Acquire necessary data and paths: Call the 'data_preprocessing' function from 'datapreprocessing.py' to set up data and routing information. diff --git a/weight_and_balance_analysis/src/datapostprocessing.py b/weight_and_balance_analysis/src/datapostprocessing.py index 9767ca87ba202f8eb23756af72aae80d854bdbf2..4f838785ae633af44a14e440e3305211bdfd93c1 100644 --- a/weight_and_balance_analysis/src/datapostprocessing.py +++ b/weight_and_balance_analysis/src/datapostprocessing.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """Module providing functions for data postprocessing.""" # Import standard modules. diff --git a/weight_and_balance_analysis/src/datapreprocessing.py b/weight_and_balance_analysis/src/datapreprocessing.py index 87234f3196c36a54972e8a433578cb3cfece0e02..d001a0d62ee21b66d72a8500ff6655630c2b6bb5 100644 --- a/weight_and_balance_analysis/src/datapreprocessing.py +++ b/weight_and_balance_analysis/src/datapreprocessing.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """Module providing functions for data preprocessing.""" # Import standard modules. import importlib diff --git a/weight_and_balance_analysis/src/readlayertext.py b/weight_and_balance_analysis/src/readlayertext.py index 22b307c206e26f12d711afa2d5a87f64520d41db..64f43ddc6553b3fd2458320fcce6770329fb1270 100644 --- a/weight_and_balance_analysis/src/readlayertext.py +++ b/weight_and_balance_analysis/src/readlayertext.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """File providing functions to read layer text from aircraft XML file.""" # Import standard libraries. import sys diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/acommodationIO.py b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/acommodationIO.py index 2b3bcb3daebe088b9e098d9a484ba8077d81b6bb..ee85c520225776f6e02e40f9842dfd380ab71942 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/acommodationIO.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/acommodationIO.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """Acommodation IO""" import pandas as pd diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/componentMassIO.py b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/componentMassIO.py index 2dfc447372212dcad4a75c5c8cdab4cedf5a5402..b0c24713c48d7f4bb2acbcc3bc1f76ae9f0666d4 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/componentMassIO.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/componentMassIO.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """ComponentMass IO""" import pandas as pd diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/massPropertiesIO.py b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/massPropertiesIO.py index d703e1792d6ae7e06031c78e8cc117ed181d1048..f0b878b036d2f2d040818909c52792c3a56ed84f 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/massPropertiesIO.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/massPropertiesIO.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """ MassProperties IO """ diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/methodbasic.py b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/methodbasic.py index 4c8fc33da4b8bce446faeaa83dc57d0c72f9c262..c992f1935dc2aaf1894f06543445f572cfb4e5f3 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/methodbasic.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/methodbasic.py @@ -1,2211 +1,2240 @@ -"""Module providing calculation functions provided by the user.""" -# Import own modules. - -from .acommodationIO import AcommodationIO -from .massPropertiesIO import MassPropertiesIO -from .tankIO import TankIO -from .propulsionIO import NacelleIO, PylonIO, EngineIO -from .transportTaskIO import TransportTaskIO -from .componentMassIO import ComponentMassIO - -import sys -import pandas as pd -import math -import csv -import numpy as np -import copy - -import pyaircraftgeometry2 as geom2 -import pyaixml as aixml - -TO_M = 1852 # conversion nautical miles to m from constants - - -def method_basic(paths_and_names, routing_dict, dict_ac_exchange, dict_mod_config, runtime_output): - """Weight and balance function. - - This function performs the calculation of the aircraft weights and the weight and balance loading diagramm. - [Add more information here...] - The output dictionary 'basic_output_dict' contains the results of the methodbasic.py and - is structured according to the following scheme: - basic_output_dict = {'parameter_name_1': value, ...} - - :param dict paths_and_names: Dictionary containing system paths and ElementTrees - :param dict routing_dict: Dictionary containing routing parameter - :param dict dict_ac_exchange: Dict containing parameter and according values from aircraft exchange file - :param dict dict_mod_config: Dict containing parameter and according values from module configuration file - :return dict basic_output_dict: Dictionary containing the aircraft weights, components masses and CG-positions - for kerosene-powered aircraft - """ - ################################################################################################################## - - # Data preparation and reading: - ac_exchange_file = paths_and_names["root_of_aircraft_exchange_tree"] - aircraft_class = dict_mod_config["aircraft_class"] - excel_file_path = paths_and_names['project_directory'] + \ - '/' + "Seats_positions.xlsx" - airplane_seatings = pd.read_excel(excel_file_path) - DEBUG(msg="ToDo: Update this when acommodation positions are available.") - - # Read fuselage and wing - wing = read_wing(paths_and_names) - fuselage = read_fuselage(paths_and_names) - fuselage_length = geom2.measure.length(fuselage) - wing_span = geom2.measure.span(wing) - - # Method selection area: - # Select inertia method - inertia_methods_available = { - "mode_0": {"method": calculate_inertia_by_lth_method, "additional_parameters": [wing_span, fuselage_length]}, - "mode_1": {"method": calculate_inertia_by_components, "additional_parameters": None}, - "mode_2": {"method": calculate_inertia_by_Raymer, - "additional_parameters": [wing_span, fuselage_length, aircraft_class]}, - } - # Select maximum landing mass method here - maximum_landing_mass_methods_available = { - "mode_0": {"method": calculate_maximum_landing_mass_by_default_method, - "additional_parameters": [ac_exchange_file]}, # noPep8 e501 - "mode_1": {"method": calculate_maximum_landing_mass_by_regression_rwth, - "additional_parameters": None} - } - inertia_method = inertia_methods_available[dict_mod_config["mode_inertia_calculation"]] - maximum_landing_mass_method = maximum_landing_mass_methods_available[ - dict_mod_config["mode_maximum_landing_mass_calculation"]] - # Select loading and active modes - # ferry range or design mission - refueling_mode = dict_mod_config["refueling_mode"] - defueling_mode = dict_mod_config["defueling_mode"] # active or not active - # row-wise or window-aisle - passengers_boarding_mode = dict_mod_config["passengers_boarding_mode"] - - # Read further components and mission data - mission_information = { - "trip_fuel_mass": determine_trip_fuel(dict_ac_exchange), - "mission_fuel_mass": determine_mission_fuel(dict_ac_exchange), - "taxi_out_fuel_mass": determine_taxi_out_fuel(dict_ac_exchange), - "taxi_in_fuel_mass": determine_taxi_in_fuel(dict_ac_exchange), - "takeoff_fuel_mass": determine_takeoff_fuel(dict_ac_exchange, runtime_output), - "landing_fuel_mass": determine_landing_fuel(dict_ac_exchange, runtime_output) - } - transport_task_data = read_transport_task(ac_exchange_file) - main_components_mass_properties = read_main_components( - ac_exchange_file, dict_ac_exchange, runtime_output) - tanks, sorted_tanks_defueling = read_tank_properties( - ac_exchange_file, defueling_mode, runtime_output) - nacelles, pylons, engines = read_propulsion_properties(ac_exchange_file) - maximum_takeoff_mass = read_maximum_takeoff_mass(ac_exchange_file) - operator_items, fuselage_operator_items_mass, systems_operator_items_mass = read_operator_items( - ac_exchange_file, fuselage_length) - furnishings_mass, furnishings = read_furnishings(ac_exchange_file) - systems = read_systems(ac_exchange_file) - acommodation_data = read_acommodation(ac_exchange_file, excel_file_path) - # x-coordinates of seats and cargo, nr of rows and nr of passengers per row are manually fixed in AcommodationIO! - DEBUG(msg="ToDo: Modify the AcommodationIO when the data is available") - - # Determine wing geometry data and NP position - mac = geom2.measure.mean_aerodynamic_chord(wing) - x_wing_global_position = dict_ac_exchange["x_wing_globlal_position"] - x_mac = geom2.measure.centroid(wing).x() - 1 / 2 * mac - x_leading_edge_mac = x_wing_global_position + x_mac - neutral_point_mac = ( - dict_ac_exchange["x_aircraft_neutral_point"] - x_leading_edge_mac) / mac * 100. - - ################################################################################################################## - ### Calculate masses ### - - # Operating mass empty - OME - # Maximum fuel mass - # Design fuel mass - # Design payload mass - # Maximum takeoff mass = design mass - # Maximum landing mass - - # Manufacturer mass empty - manufacturer_mass_empty = calculate_manufacturer_mass_empty( - operator_items, main_components_mass_properties, inertia_method).print("Manufacturer mass empty") - - # Operating mass empty = MME + operator items - operating_mass_empty = calculate_operating_mass_empty( - main_components_mass_properties, inertia_method, x_leading_edge_mac, mac).print("Operating mass empty") - - # Maximum fuel mass + maximum fuel mass per tank - maximum_fuel_mass, maximum_fuel_mass_per_tank, ferry_range_mass = calculate_maximum_fuel_mass( - tanks, operating_mass_empty, x_leading_edge_mac, mac, maximum_takeoff_mass, runtime_output) - maximum_fuel_mass.print("Maximum fuel mass") - ferry_range_mass.print("Ferry range mass") - - # Maximum PL mass - maximum_payload_mass = calculate_maximum_payload_mass( - dict_ac_exchange["maximum_structural_payload_mass"], - main_components_mass_properties["fuselage"]).print("Maximimum payload mass") - - # Design payload mass - design_payload_mass = calculate_design_payload_mass( - transport_task_data, main_components_mass_properties["fuselage"], maximum_payload_mass, runtime_output).print( - "Design payload mass") - - # Design fuel mass - design_fuel_mass_takeoff, design_fuel_mass_midflight, design_fuel_mass_minimum_landing, \ - design_fuel_mass_takeoff_per_tank, consumed_fuel, design_fuel_mass, design_fuel_mass_per_tank = \ - calculate_design_fuel_mass( - mission_information, maximum_takeoff_mass, operating_mass_empty, design_payload_mass, tanks, - routing_dict, dict_ac_exchange) - design_fuel_mass.path_to_element = "./analysis/masses_cg_inertia/design_fuel_mass" - design_fuel_mass.print("Design fuel mass") - - # Maximum zero fuel mass - maximum_zero_fuel_mass = calculate_maximum_zero_fuel_mass( - [operating_mass_empty, maximum_payload_mass], inertia_method).print("Maximum zero fuel mass") - maximum_zero_fuel_mass.cg_mac = ( - maximum_zero_fuel_mass.center_of_gravity['x'] - x_leading_edge_mac) / mac * 100 - - # Design mass - design_mass = calculate_design_mass( - [operating_mass_empty, design_payload_mass, - design_fuel_mass], inertia_method - ).print("Design mass") - design_mass.path_to_element = "./analysis/masses_cg_inertia/design_mass" - design_mass.cg_mac = ( - design_mass.center_of_gravity['x'] - x_leading_edge_mac) / mac * 100 - - # Design mass takeoff - design_mass_takeoff = calculate_design_mass( - [operating_mass_empty, design_payload_mass, - design_fuel_mass_takeoff], inertia_method - ).print("Design mass takeoff") - - # Maximum takeoff mass - maximum_takeoff_mass = copy.copy(design_mass_takeoff) - maximum_takeoff_mass.path_to_element = "./analysis/masses_cg_inertia/maximum_takeoff_mass" - maximum_takeoff_mass.cg_mac = ( - design_mass_takeoff.center_of_gravity['x'] - x_leading_edge_mac) / mac * 100 - maximum_takeoff_mass.print("Maximum Takeoff Mass") - - # Maximum payload + fuel up to MTOW - mtow_max_payload_mass = calculate_mtow_max_payload( - operating_mass_empty, maximum_payload_mass, main_components_mass_properties["wing"], - maximum_takeoff_mass, x_leading_edge_mac, mac) - - # Maximum fuel + payload up to MTOW - mtow_max_fuel_mass = calculate_mtow_max_fuel( - operating_mass_empty, maximum_fuel_mass_per_tank, - main_components_mass_properties["wing"], main_components_mass_properties["fuselage"], - maximum_takeoff_mass, x_leading_edge_mac, mac) - - # Midflight - design_mass_midflight = calculate_design_mass( - [operating_mass_empty, design_payload_mass, design_fuel_mass_midflight], - inertia_method).print("Design mass midflight") - - # Landing mass - if dict_mod_config["mode_maximum_landing_mass_calculation"] == "mode_0": - maximum_landing_mass = calculate_maximum_landing_mass_by_default_method( - mission_information, inertia_method, [ - operating_mass_empty, design_payload_mass, design_fuel_mass_minimum_landing], - maximum_landing_mass_method["additional_parameters"]).print("Maximum landing mass") - else: - runtime_output.print("Calculate MLM by RWTH regression ...") - maximum_landing_mass = calculate_maximum_landing_mass_by_regression_rwth( - maximum_takeoff_mass, inertia_method, main_components_mass_properties).print("Maximum landing mass") - maximum_landing_mass.path_to_element = "./analysis/masses_cg_inertia/maximum_landing_mass" - - ################################################################################################################## - ### Determine W&B diagram and CG curtailment ### - - runtime_output.print("== Calculate loading diagramm cg change ==") - - if refueling_mode == "mode_1": - runtime_output.print("REFUELING (ferry mission)...") - # Calculate the change in CG after refueling - ferry range mission case: - cg_positions_over_mac_refueling, total_mass_refueling = \ - calculate_cg_position_over_mac_refueling(operating_mass_empty.center_of_gravity, - operating_mass_empty.mass, maximum_fuel_mass_per_tank, - mac, x_leading_edge_mac) - for fuel in maximum_fuel_mass_per_tank: - runtime_output.print( - f"Tank '{fuel.tank_location}' was filled up to the maximum capacity with {fuel.mass:.2f} kg {fuel.energy_carrier}.") - runtime_output.print( - f" ...The flag, whether the tank should have been filled (true) or not (false), is {fuel.used_tank}." - ) - - elif refueling_mode == "mode_0": - runtime_output.print("REFUELING (design mission)...") - # Calculate the change in CG after refueling - design mission case: - cg_positions_over_mac_refueling, total_mass_refueling = \ - calculate_cg_position_over_mac_refueling(operating_mass_empty.center_of_gravity, - operating_mass_empty.mass, design_fuel_mass_takeoff_per_tank, - mac, x_leading_edge_mac) - for fuel in design_fuel_mass_takeoff_per_tank: - runtime_output.print( - f"Tank '{fuel.tank_location}' was filled up with {fuel.mass:.2f} kg {fuel.energy_carrier}.") - runtime_output.print( - f" ...Maximum fuel mass capacity of this tank is of {fuel.maximum_capacity:.2f} kg." - ) - runtime_output.print( - f" ...The flag, whether the tank should have been filled (true) or not (false), is {fuel.used_tank}." - ) - else: - runtime_output.critical("Invalid refueling mode specified") - raise ValueError("Invalid refueling mode specified") - - # Prepare passengers boarding - starting_mass = total_mass_refueling[-1] - starting_cg = cg_positions_over_mac_refueling[-1] * \ - mac / 100 + x_leading_edge_mac - x_coord_front_back = acommodation_data.seating_spacing_x_coordinate - passenger_front_back = acommodation_data.number_of_passengers_per_row - x_coord_back_front = x_coord_front_back[::-1] - passenger_back_front = passenger_front_back[::-1] - - runtime_output.print("PASSENGERS BOARDING...") - - if passengers_boarding_mode == "mode_0": - # Calculate the change in CG after the boarding of the passengers from the front to the back - row-wise case: - runtime_output.print("ROW-WISE FRONT TO BACK") - cg_positions_over_mac_passengers_front_back, total_mass_passengers_front_back = \ - cg_change_passengers_boarding_front_back( - starting_cg, starting_mass, x_coord_front_back, passenger_front_back, - acommodation_data, mac, x_leading_edge_mac) - - # Calculate the change in CG after the boarding of the passengers from the back to the front - row-wise case: - runtime_output.print("ROW-WISE BACK TO FRONT") - cg_positions_over_mac_passengers_back_front, total_mass_passengers_back_front = \ - cg_change_passengers_boarding_front_back( - starting_cg, starting_mass, x_coord_back_front, passenger_back_front, - acommodation_data, mac, x_leading_edge_mac) - - DEBUG(msg="If the data is to be read from excel and not from acommodationIO modify/adapt this") - # cg_positions_over_mac_passengers_front_back, total_mass_passengers_front_back = \ - # cg_change_passengers_boarding_front_back_excel(starting_cg, starting_mass, airplane_seatings, - # acommodation_data, mac, x_leading_edge_mac) - - elif passengers_boarding_mode == "mode_1": - runtime_output.print("WINDOW AISLE FRONT TO BACK") - cg_positions_over_mac_passengers_front_back, total_mass_passengers_front_back = \ - cg_change_passengers_boarding_window_aisle( - starting_cg, starting_mass, x_coord_front_back, passenger_front_back, - acommodation_data, mac, x_leading_edge_mac) - - runtime_output.print("WINDOW AISLE BACK TO FRONT") - cg_positions_over_mac_passengers_back_front, total_mass_passengers_back_front = \ - cg_change_passengers_boarding_window_aisle( - starting_cg, starting_mass, x_coord_back_front, passenger_back_front, - acommodation_data, mac, x_leading_edge_mac) - else: - runtime_output.critical("Invalid passengers boarding mode specified") - raise ValueError("Invalid passengers boarding mode specified") - - # Prepare cargo - starting_mass = total_mass_passengers_front_back[-1] - starting_cg = cg_positions_over_mac_passengers_front_back[-1] * \ - mac / 100 + x_leading_edge_mac - x_coord_front_back_cargo = acommodation_data.cargo_spacing_x_coordinate - x_coord_back_front_cargo = x_coord_front_back_cargo[::-1] - - runtime_output.print("CARGO...") - DEBUG(msg="The palets positions unknown. The cargo is loaded at the same locations as the seats positions.") - - cg_positions_over_mac_cargo_front_back, total_mass_cargo_front_back = \ - cg_change_cargo_loading_front_back( - starting_cg, starting_mass, x_coord_front_back_cargo, transport_task_data, mac, x_leading_edge_mac) - - cg_positions_over_mac_cargo_back_front, total_mass_cargo_back_front = \ - cg_change_cargo_loading_front_back( - starting_cg, starting_mass, x_coord_back_front_cargo, transport_task_data, mac, x_leading_edge_mac) - - # Prepare defueling - defueling_mass_per_tank = calculate_fuel_properties_defueling( - sorted_tanks_defueling, consumed_fuel, design_fuel_mass_takeoff_per_tank, - runtime_output, routing_dict, dict_ac_exchange) - - defueling_mass_per_tank_ferry = calculate_fuel_properties_defueling( - sorted_tanks_defueling, consumed_fuel, maximum_fuel_mass_per_tank, - runtime_output, routing_dict, dict_ac_exchange) - - starting_mass = total_mass_cargo_back_front[-1] - starting_cg = cg_positions_over_mac_cargo_front_back[-1] * \ - mac / 100 + x_leading_edge_mac - - runtime_output.print("DEFUELING...") - - if refueling_mode == "mode_1": - # Calculate the change in CG after defueling - ferry range mission case: - cg_positions_over_mac_defueling, total_mass_defueling = calculate_cg_position_over_mac_defueling( - starting_cg, starting_mass, defueling_mass_per_tank_ferry, mac, x_leading_edge_mac) - DEBUG(msg="Consumed fuel for ferry mission = consumed fuel for design mission.") - - for fuel in defueling_mass_per_tank_ferry: - runtime_output.print( - f"From tank '{fuel.name}' was defueld {-fuel.mass:.2f} kg. Fuel left in tank: {fuel.fuel_left_in_tank:.2f} kg.") - - elif refueling_mode == "mode_0": - - # Calculate the change in CG after refueling - design mission case: - cg_positions_over_mac_defueling, total_mass_defueling = calculate_cg_position_over_mac_defueling( - starting_cg, starting_mass, defueling_mass_per_tank, mac, x_leading_edge_mac) - - for fuel in defueling_mass_per_tank: - runtime_output.print( - f"From tank '{fuel.name}' was defueld {-fuel.mass:.2f} kg. Fuel left in tank: {fuel.fuel_left_in_tank:.2f} kg.") - else: - runtime_output.critical("Invalid refueling mode specified") - raise ValueError("Invalid refueling mode specified") - - # Determine the CG curtailment: - - most_fwd_CG_over_mac = min([min(cg_positions_over_mac_refueling), min(cg_positions_over_mac_passengers_front_back), - min(cg_positions_over_mac_cargo_front_back), min(cg_positions_over_mac_defueling)]) - most_aft_CG_over_mac = max([max(cg_positions_over_mac_refueling), max(cg_positions_over_mac_passengers_back_front), - max(cg_positions_over_mac_cargo_back_front), max(cg_positions_over_mac_defueling)]) - - most_fwd_CG = most_fwd_CG_over_mac * mac / 100 + x_leading_edge_mac - most_aft_CG = most_aft_CG_over_mac * mac / 100 + x_leading_edge_mac - - runtime_output.print( - f"Most forward CG global position is {most_fwd_CG:.2f} m.") - runtime_output.print( - f"Most aft CG global position is {most_aft_CG:.2f} m.") - - # Determine the corresponding mass to the most fwd CG - if most_fwd_CG_over_mac in cg_positions_over_mac_refueling: - corresponding_index = cg_positions_over_mac_refueling.index( - most_fwd_CG_over_mac) - corresponding_mass_most_fwd_CG = total_mass_refueling[corresponding_index] - elif most_fwd_CG_over_mac in cg_positions_over_mac_passengers_front_back: - corresponding_index = cg_positions_over_mac_passengers_front_back.index( - most_fwd_CG_over_mac) - corresponding_mass_most_fwd_CG = total_mass_passengers_front_back[corresponding_index] - elif most_fwd_CG_over_mac in cg_positions_over_mac_cargo_front_back: - corresponding_index = cg_positions_over_mac_cargo_front_back.index( - most_fwd_CG_over_mac) - corresponding_mass_most_fwd_CG = total_mass_cargo_front_back[corresponding_index] - else: - corresponding_index = cg_positions_over_mac_defueling.index( - most_fwd_CG_over_mac) - corresponding_mass_most_fwd_CG = total_mass_defueling[corresponding_index] - - runtime_output.print( - f"Corresponding mass most fwd CG is of {corresponding_mass_most_fwd_CG:.2f} kg.") - - # Determine the corresponding mass to the most aft CG - if most_aft_CG_over_mac in cg_positions_over_mac_refueling: - corresponding_index = cg_positions_over_mac_refueling.index( - most_aft_CG_over_mac) - corresponding_mass_most_aft_CG = total_mass_refueling[corresponding_index] - elif most_aft_CG_over_mac in cg_positions_over_mac_passengers_back_front: - corresponding_index = cg_positions_over_mac_passengers_back_front.index( - most_aft_CG_over_mac) - corresponding_mass_most_aft_CG = total_mass_passengers_back_front[corresponding_index] - elif most_aft_CG_over_mac in cg_positions_over_mac_cargo_back_front: - corresponding_index = cg_positions_over_mac_cargo_back_front.index( - most_aft_CG_over_mac) - corresponding_mass_most_aft_CG = total_mass_cargo_back_front[corresponding_index] - else: - corresponding_index = cg_positions_over_mac_defueling.index( - most_aft_CG_over_mac) - corresponding_mass_most_aft_CG = total_mass_defueling[corresponding_index] - - runtime_output.print( - f"Corresponding mass most aft CG is of {corresponding_mass_most_aft_CG:.2f} kg.") - - # Prepare the data to be written as nodes for axml - most_forward_mass = MassPropertiesIO( - "./analysis/masses_cg_inertia/most_forward_mass") - most_forward_mass.center_of_gravity["x"] = most_fwd_CG - most_forward_mass.center_of_gravity["y"] = 0.0 - most_forward_mass.center_of_gravity["z"] = 0.0 - most_forward_mass.mass = corresponding_mass_most_fwd_CG - most_forward_mass.inertia = inertia_method["method"]( - [operating_mass_empty, MassPropertiesIO().initialize_zero(), - MassPropertiesIO().initialize_zero()], - inertia_method["additional_parameters"]) - - most_afterward_mass = MassPropertiesIO( - "./analysis/masses_cg_inertia/most_afterward_mass") - most_afterward_mass.center_of_gravity["x"] = most_aft_CG - most_afterward_mass.center_of_gravity["y"] = 0.0 - most_afterward_mass.center_of_gravity["z"] = 0.0 - most_afterward_mass.mass = corresponding_mass_most_aft_CG - most_afterward_mass.inertia = inertia_method["method"]( - [operating_mass_empty, MassPropertiesIO().initialize_zero(), - MassPropertiesIO().initialize_zero()], - inertia_method["additional_parameters"]) - - ########################################################################################################### - # Prepare output # - - # Define Group masses - group_mass_structure = main_components_mass_properties["wing"].mass + \ - main_components_mass_properties["fuselage"].mass - fuselage_operator_items_mass - furnishings_mass + \ - main_components_mass_properties["empennage"].mass + \ - main_components_mass_properties["landing_gear"].mass +\ - sum([pylon.mass for pylon in pylons if pylon is not None]) - group_mass_power_unit = sum([sum([nacelle.mass for nacelle in nacelles if nacelle is not None]), sum( - [engine.mass for engine in engines if engine is not None])]) - group_mass_systems = main_components_mass_properties["systems"].mass - \ - systems_operator_items_mass - group_mass_furnishings = furnishings_mass - group_mass_operator_items = sum(item.mass for item in operator_items) - group_masses = { - "Structure": group_mass_structure, - "Propulsion": group_mass_power_unit, - "Systems": group_mass_systems, - "Furnishings": group_mass_furnishings, - "Operator Items": group_mass_operator_items - } - structure_masses = { - "Wing": main_components_mass_properties["wing"].mass, - "Fuselage": main_components_mass_properties["fuselage"].mass - fuselage_operator_items_mass - furnishings_mass, - "Empennage": main_components_mass_properties["empennage"].mass, - "Landing Gear": main_components_mass_properties["landing_gear"].mass, - "Pylons": sum([pylon.mass for pylon in pylons if pylon is not None]) - } - - basic_output_dict = { - "structure_components": structure_masses, - "operating_mass_empty": operating_mass_empty, - "manufacturer_mass_empty": manufacturer_mass_empty, - "maximum_takeoff_mass": maximum_takeoff_mass, - "maximum_payload_mass": maximum_payload_mass, - "maximum_fuel_mass": maximum_fuel_mass, - "ferry_range_mass": ferry_range_mass, - "maximum_zero_fuel_mass": maximum_zero_fuel_mass, - "maximum_landing_mass": maximum_landing_mass, - "mtow_with_max_payload": mtow_max_payload_mass, - "mtow_with_max_fuel": mtow_max_fuel_mass, - "design_mass": design_mass, - "design_payload_mass": design_payload_mass, - "design_mass_midflight": design_mass_midflight, - "design_fuel_mass": design_fuel_mass, - "design_fuel_mass_takeoff": design_fuel_mass_takeoff, - "design_fuel_mass_midflight": design_fuel_mass_midflight, - "design_fuel_mass_minimum_landing": design_fuel_mass_minimum_landing, - "group_masses": group_masses, - "Refueling CG change": cg_positions_over_mac_refueling, - "Refueling mass change": total_mass_refueling, - "Defueling CG change": cg_positions_over_mac_defueling, - "Defueling mass change": total_mass_defueling, - "Defueling mode": defueling_mode, - "Passengers boarding front back CG change": cg_positions_over_mac_passengers_front_back, - "Passengers boarding front back mass change": total_mass_passengers_front_back, - "Passengers boarding back front CG change": cg_positions_over_mac_passengers_back_front, - "Passengers boarding back front mass change": total_mass_passengers_back_front, - "Cargo loading front back CG change": cg_positions_over_mac_cargo_front_back, - "Cargo loading front back mass change": total_mass_cargo_front_back, - "Cargo loading back front CG change": cg_positions_over_mac_cargo_back_front, - "Cargo loading back front mass change": total_mass_cargo_back_front, - "Neutral point mac position": neutral_point_mac, - "most_forward_mass": most_forward_mass, - "most_afterward_mass": most_afterward_mass, - "Most forward CG mac position": most_fwd_CG_over_mac, - "Most aft CG mac position": most_aft_CG_over_mac, - "Furnishings": furnishings, - "Operator_items": operator_items, - "Nacelles": nacelles, - "Engines": engines, - "Systems": systems - } - - # Check if the given tool_level from aircrat exchange file is None -> if true: -> initialize with 0. - if dict_ac_exchange['tool_level'] is None: - basic_output_dict['current_tool_level'] = 0 - # Else if condition: Check if given tool_level from aircrat exchange is lower than the given own tool level - # from module configuration file -> if true: Add 1 to current tool level from airaft exchange file. - elif dict_ac_exchange['tool_level'] < int(routing_dict['tool_level']): - basic_output_dict['current_tool_level'] = dict_ac_exchange['tool_level'] + 1 - # Else condition: The given tool_level from aircrat exchange is equal to the given own tool level - else: - basic_output_dict['current_tool_level'] = int( - routing_dict['tool_level']) - - return basic_output_dict - - -def calculate_inertia_by_components(mass_properties=[], additional_parameters=None): - """Calculate inertia by using component inertia and steiner - - Args: - mass_properties (list, optional): List of mass_properties to compute inertia. Defaults to []. - additional_parameters (_type_, optional): No additional parameters needed. Defaults to None. - - Returns: - dict: inertia - """ - reference_center_of_gravity = calculate_center_of_gravity(mass_properties) - inertia = { - "j_xx": 0.0, - "j_yy": 0.0, - "j_zz": 0.0, - "j_xy": 0.0, - "j_xz": 0.0, - "j_yx": 0.0, - "j_yz": 0.0, - "j_zx": 0.0, - "j_zy": 0.0 - } - - for component, value in inertia.items(): - for element in mass_properties: - inertia[component] += element.inertia[component] + element.mass * calculate_geometric_distance_by_axis( - component, reference_center_of_gravity, element.center_of_gravity) - - return inertia - - -def calculate_geometric_distance_by_axis(axis, reference_axis, current_axis): - """Calculation of the geometric distance for inertia components - - Args: - axis (str): Inertia axis string - reference_axis (dict): center_of_gravity to apply inertia - current_axis (dict): center_of_gravity of current component - """ - def main_axis(p, q): - return (p**2 + q**2) - - def deviation_axis(p, q): - return -(p * q) - - to_get = { - "j_xx": {"first": "y", "second": "z", "calculation": main_axis}, - "j_yy": {"first": "x", "second": "z", "calculation": main_axis}, - "j_zz": {"first": "x", "second": "y", "calculation": main_axis}, - "j_xy": {"first": "x", "second": "y", "calculation": deviation_axis}, - "j_xz": {"first": "x", "second": "z", "calculation": deviation_axis}, - "j_yx": {"first": "y", "second": "x", "calculation": deviation_axis}, - "j_yz": {"first": "y", "second": "z", "calculation": deviation_axis}, - "j_zx": {"first": "z", "second": "x", "calculation": deviation_axis}, - "j_zy": {"first": "z", "second": "y", "calculation": deviation_axis} - } - - # Call calculation method and axis - not shortest possibility to calculate but easy to read - selected = to_get[axis] - first = selected["first"] - second = selected["second"] - calculation = selected["calculation"] - - # Calculate delta values between reference and current axis for first and second component - delta_ref_curr_first = reference_axis[first] - current_axis[first] - delta_ref_curr_second = reference_axis[second] - current_axis[second] - - # Calculate inertia distance for axis - return calculation(delta_ref_curr_first, delta_ref_curr_second) - - -def calculate_inertia_by_lth_method(mass_properties=[], additional_parameters=[]): - """Calculate inertia by 'luftfahrttechnisches Handbuch (LTH)' (Empirical table) - Only valid for tube and wing aircrafts - - Args: - mass_operation_empty (MassPropertiesIO Object list): - operation mass empty, payload mass expected, fuel mass expected - additional_parameters (list): wing span, fuselage length - - Returns: - dict: inertia - """ - mass_operation_empty = mass_properties[0] - mass_payload_expected = mass_properties[1] - mass_fuel_expected = mass_properties[2] - - wing_span = additional_parameters[0] - fuselage_length = additional_parameters[1] - - expected_mass = mass_operation_empty.mass + \ - mass_fuel_expected.mass + mass_payload_expected.mass - expected_mass_over_ome = expected_mass / mass_operation_empty.mass - expected_fuel_over_ome = mass_fuel_expected.mass / mass_operation_empty.mass - - # Transferred table chart page 2 of 4 from LTH - MA 402 42-01 Ausgabe A to formula below - factor_kx = (1.0 / 12.0 * ((-2.0 / 3.0 * (expected_mass_over_ome - - 1.0) + expected_fuel_over_ome) + 1.0) + 0.065) - - # Transferred table chart page 3 of 4 from LTH - MA 402 42-01 Ausgabe A to formula below - factor_ky = (-0.065 / 1.72 * (0.2 * (expected_mass_over_ome - - 1.0) + expected_fuel_over_ome) + 0.2025) - - factor_tech_j_xx = 0.8 # self estimated tech factor for Jxx - factor_tech_j_yy = 0.9 # self estimated tech factor for Jyy - - j_xx = factor_tech_j_xx * factor_kx**2 * wing_span**2 * expected_mass - j_yy = factor_tech_j_yy * factor_ky**2 * fuselage_length**2 * expected_mass - j_zz = 0.96 * (j_xx + j_yy) - inertia = { - "j_xx": j_xx, - "j_yy": j_yy, - "j_zz": j_zz, - "j_xy": 0.0, - "j_xz": 0.0, - "j_yx": 0.0, - "j_yz": 0.0, - "j_zx": 0.0, - "j_zy": 0.0 - } - return inertia - - -def calculate_inertia_by_Raymer(mass_properties=[], additional_parameters=[]): - """Calculate inertia by 'Raymer p.623, table 16.1' (Empirical table) - - - Args: - mass_operation_empty (MassPropertiesIO Object list): - operation mass empty, payload mass expected, fuel mass expected - additional_parameters (list): wing span, fuselage length, aircraft_type - - Returns: - dict: inertia - """ - mass_operation_empty = mass_properties[0] - mass_payload_expected = mass_properties[1] - mass_fuel_expected = mass_properties[2] - - wing_span = additional_parameters[0] - fuselage_length = additional_parameters[1] - aircraft_class = additional_parameters[2] - - expected_mass = mass_operation_empty.mass + \ - mass_fuel_expected.mass + mass_payload_expected.mass - - # Dictionary of nondimensional radii of gyration for different aircraft classes - radii_of_gyration = { - # "Single-engine prop": {"R_x": 0.25, "R_y": 0.38, "R_z": 0.39}, - # "Twin-engine prop": {"R_x": 0.30, "R_y": 0.40, "R_z": 0.44}, - # "Business jet twin": {"R_x": 0.30, "R_y": 0.34, "R_z": 0.39}, - # "Twin turboprop transport": {"R_x": 0.22, "R_y": 0.33, "R_z": 0.38}, - "jet_fuselage_eng": {"R_x": 0.24, "R_y": 0.34, "R_z": 0.42}, - "jet_two_wing_eng": {"R_x": 0.23, "R_y": 0.33, "R_z": 0.45}, - "jet_four_wing_eng": {"R_x": 0.24, "R_y": 0.36, "R_z": 0.44}, - "blended_wing": {"R_x": 0.28, "R_y": 0.40, "R_z": 0.46}, - } - - # Retrieve the nondimensional radii of gyration for the specified aircraft class - radii = radii_of_gyration[aircraft_class] - R_x = radii["R_x"] - R_y = radii["R_y"] - R_z = radii["R_z"] - - tech_fact_yy = 1.25 - tech_fact_xx = 1.15 - - # Calculate mass from weight - mass = expected_mass - - # Roll moment of inertia (I_xx) - I_xx = (wing_span**2 * mass * R_x**2) / 4 * tech_fact_xx - - # Pitch moment of inertia (I_yy) - I_yy = (fuselage_length**2 * mass * R_y**2) / 4 * tech_fact_yy - - # Yaw moment of inertia (I_zz) - I_zz = (((wing_span + fuselage_length) / 2)**2 * mass * R_z**2) / 4 - - inertia = { - "j_xx": I_xx, - "j_yy": I_yy, - "j_zz": I_zz, - "j_xy": 0.0, - "j_xz": 0.0, - "j_yx": 0.0, - "j_yz": 0.0, - "j_zx": 0.0, - "j_zy": 0.0 - } - return inertia - - -def calculate_center_of_gravity(mass_properties): - import logging - runtime_output = logging.getLogger('module_logger') - - center_of_gravity = {"x": None, - "y": None, - "z": None} - components = [] - if isinstance(mass_properties, dict): - components = list(mass_properties.values()) - elif isinstance(mass_properties, list): - components = mass_properties - else: - runtime_output.critical( - "Wrong type of mass_properties (no dict or list)... abort program!") - raise ValueError( - "Wrong type of mass_properties (no dict or list)... abort program!") - - for ax in center_of_gravity: - overall_mass = 0.0 - scaled_mass = 0.0 - for component in components: - overall_mass += component.mass - scaled_mass += component.mass * \ - component.center_of_gravity[ax] - center_of_gravity[ax] = scaled_mass / overall_mass - - return center_of_gravity - - -def calculate_manufacturer_mass_empty(operator_items, mass_properties, inertia_method): - - manufacturer_mass_empty = MassPropertiesIO( - "./analysis/masses_cg_inertia/manufacturer_mass_empty") - - # op items are calculated by both fuselage and systems, here they are combined - cg_items = calculate_center_of_gravity(operator_items) - mass_op_items = sum(item.mass for item in operator_items) - - # mass properties contains the total mass of components (ex. fuselage = structure + op items + furnishings) - mass_tot = sum( - [mass_properties[element].mass for element in mass_properties]) # = OME - cg_tot = calculate_center_of_gravity(mass_properties) # = CG OME - - # removing the operator items to determine MME - manufacturer_mass_empty.mass = mass_tot - mass_op_items - - manufacturer_mass_empty.center_of_gravity['x'] = ( - mass_tot * cg_tot['x'] - mass_op_items * cg_items['x']) / (mass_tot - mass_op_items) - manufacturer_mass_empty.center_of_gravity['y'] = ( - mass_tot * cg_tot['y'] - mass_op_items * cg_items['y']) / (mass_tot - mass_op_items) - manufacturer_mass_empty.center_of_gravity['z'] = ( - mass_tot * cg_tot['z'] - mass_op_items * cg_items['z']) / (mass_tot - mass_op_items) - - manufacturer_mass_empty.inertia = inertia_method["method"]([manufacturer_mass_empty, MassPropertiesIO( - ).initialize_zero(), MassPropertiesIO().initialize_zero()], inertia_method["additional_parameters"]) - - return manufacturer_mass_empty - - -def calculate_operating_mass_empty(mass_properties, inertia_method, LE, mac): - """Calculate opertaing mass empty - - Args: - mass_properties (MassPropertiesIO Objects): list of mass_properties objects - cg_method (function): center of gravity method - inertia_method (function): inertia method - """ - operating_mass_empty = MassPropertiesIO( - "./analysis/masses_cg_inertia/operating_mass_empty") - - operating_mass_empty.mass = sum( - [mass_properties[element].mass for element in mass_properties]) - # operator_items_mass already considered in the total mass of components - # not needed extra, already considered under systems and fuselage - - operating_mass_empty.center_of_gravity = calculate_center_of_gravity( - mass_properties) - operating_mass_empty.mass += 0.1 # ToDo avoid division by zero - operating_mass_empty.inertia = inertia_method["method"]([operating_mass_empty, MassPropertiesIO( - ).initialize_zero(), MassPropertiesIO().initialize_zero()], inertia_method["additional_parameters"]) - operating_mass_empty.cg_mac = ( - operating_mass_empty.center_of_gravity['x'] - LE) / mac * 100 - return operating_mass_empty - - -def calculate_maximum_landing_mass_by_default_method( - mission_information=None, inertia_method=None, mass_properties=None, additional_parameters=[]): - """Calculate maximum landing mass by default method - OME - Design Payload - Reserve fuel - - Args: - operating_mass_empty (float, optional): operating mass empty. Defaults to None. - transport_task_data (object, optional): tranport task data. Defaults to None. - mission_information (object, optional): mission information. Defaults to None. - - Returns: - float: maximum landing mass - """ - - maximum_landing_mass = MassPropertiesIO( - "./analysis/masses_cg_inertia/maximum_landing_mass/mass_properties") - - design_fuel_mass_trip = mission_information["trip_fuel_mass"] - design_fuel_mass_mission = mission_information["mission_fuel_mass"] - - # If mission information not existent - work with current landing mass - if design_fuel_mass_trip is None or design_fuel_mass_mission is None: - maximum_landing_mass.read(additional_parameters[0]) - maximum_landing_mass.center_of_gravity = calculate_center_of_gravity( - mass_properties) - maximum_landing_mass.inertia = inertia_method["method"]( - [maximum_landing_mass, MassPropertiesIO().initialize_zero(), - MassPropertiesIO().initialize_zero()], - inertia_method["additional_parameters"]) - return maximum_landing_mass - - if mission_information is not None: - maximum_landing_mass.mass = sum( - [element.mass for element in mass_properties]) - maximum_landing_mass.center_of_gravity = calculate_center_of_gravity( - mass_properties) - maximum_landing_mass.inertia = inertia_method["method"]([maximum_landing_mass, MassPropertiesIO( - ).initialize_zero(), MassPropertiesIO().initialize_zero()], inertia_method["additional_parameters"]) - return maximum_landing_mass - - -def calculate_maximum_landing_mass_by_regression_rwth(maximum_takeoff_mass, inertia_method, mass_properties): - """Calculate maximum landing mass by RWTH regression formular - - Args: - maximum_takeoff_mass (float): maximum takeoff mass value - - Returns: - float: maximum landing mass - """ - maximum_landing_mass = MassPropertiesIO( - "./analysis/masses_cg_inertia/maximum_landing_mass/mass_properties") - if maximum_takeoff_mass.mass > 15000.0: - maximum_landing_mass.mass = 1.9689 * maximum_takeoff_mass.mass**0.9248 - else: - maximum_landing_mass.mass = 0.9009 * maximum_takeoff_mass.mass + 410.85 - maximum_landing_mass.center_of_gravity = calculate_center_of_gravity( - mass_properties) - maximum_landing_mass.inertia = inertia_method["method"]([maximum_landing_mass, MassPropertiesIO( - ).initialize_zero(), MassPropertiesIO().initialize_zero()], inertia_method["additional_parameters"]) - - return maximum_landing_mass - - -def calculate_maximum_fuel_mass(tanks, ome, LE, mac, mtom, runtime_output): - """Calculation of maximum fuel mass - - Args: - tanks (float): list of tanks - fuel_density (float): fuel density per tank - - Returns: - _type_: _description_ - """ - max_fuel_mass_per_tank = [] - mass_prop = [] - max_fuel_mass = MassPropertiesIO( - "./analysis/masses_cg_inertia/maximum_fuel_mass") - - # the order to be filled is already sorted in the tanks - for empty_tank in tanks: - filled_tank_wo_structure = MassPropertiesIO().initialize_zero() - filled_tank_wo_structure.center_of_gravity = empty_tank.mass_properties.center_of_gravity - filled_tank_wo_structure.mass = empty_tank.energy / empty_tank.gravimetric_density - filled_tank_wo_structure.tank_location = empty_tank.tank_location - filled_tank_wo_structure.energy_carrier = empty_tank.energy_carrier - filled_tank_wo_structure.used_tank = empty_tank.used_tank - # No adaption of Center of gravity or Inertia here - - filled_tank_wo_structure.moment_change = ( - filled_tank_wo_structure.mass * filled_tank_wo_structure.center_of_gravity["x"]) - - max_fuel_mass_per_tank.append(filled_tank_wo_structure) - mass_prop.append(filled_tank_wo_structure) - - max_fuel_mass.mass = sum( - [filled_tank.mass for filled_tank in max_fuel_mass_per_tank]) - if max_fuel_mass.mass < 0.01: - max_fuel_mass.mass = 0.1 - max_fuel_mass.center_of_gravity = calculate_center_of_gravity(mass_prop) - max_fuel_mass.initialize_zero_inertia() - - ferry_range_mass = MassPropertiesIO( - "./analysis/masses_cg_inertia/ferry_range_mass") - mass_prop.append(ome) - - ferry_range_mass.mass = max_fuel_mass.mass + ome.mass - if ferry_range_mass.mass < 0.01: - ferry_range_mass.mass = 0.1 - ferry_range_mass.center_of_gravity = calculate_center_of_gravity(mass_prop) - ferry_range_mass.initialize_zero_inertia() - ferry_range_mass.cg_mac = ( - max_fuel_mass.center_of_gravity['x'] - LE) / mac * 100 - - if ferry_range_mass.mass >= mtom.mass: - runtime_output.warning( - "Attention: OEW + max_fuel_mass is higher than MTOW.") - - return max_fuel_mass, max_fuel_mass_per_tank, ferry_range_mass - - -def calculate_cg_position_over_mac_refueling(initial_cg, initial_weight, fuel_mass_per_tank, mac, LE): - - cg_positions = [] - total_weight = [] - - total_weight.append(initial_weight) - total_weight_new = initial_weight - - total_moment_change = initial_cg["x"] * initial_weight - - initial_mac_position = (initial_cg["x"] - LE) / mac * 100.0 - cg_positions.append(initial_mac_position) - - for filled_tank in fuel_mass_per_tank: - - total_weight_new += filled_tank.mass - total_moment_change += filled_tank.moment_change - - new_cg = total_moment_change / total_weight_new - new_mac_position = (new_cg - LE) / mac * 100.0 - - cg_positions.append(new_mac_position) - total_weight.append(total_weight_new) - - return cg_positions, total_weight - - -def calculate_cg_position_over_mac_defueling(initial_cg, initial_weight, fuel_mass_per_tank, mac, LE): - - cg_positions = [] - total_weight = [] - - total_weight.append(initial_weight) - total_weight_new = initial_weight - - total_moment_change = initial_cg * initial_weight - - initial_mac_position = (initial_cg - LE) / mac * 100.0 - cg_positions.append(initial_mac_position) - - for filled_tank in fuel_mass_per_tank: - - total_weight_new += filled_tank.mass - total_moment_change += filled_tank.moment_change - - new_cg = total_moment_change / total_weight_new - new_mac_position = (new_cg - LE) / mac * 100.0 - - cg_positions.append(new_mac_position) - total_weight.append(total_weight_new) - - return cg_positions, total_weight - - -def calculate_fuel_mass_properties_evenly_distributed(tanks, fuel_mass, runtime_output): - - # Divide fuel mass evenly on tanks - might be adaptable to inner and outer tanks - overall_fuel_mass_per_tank = fuel_mass / len(tanks) - fuel_mass_properties = MassPropertiesIO().initialize_zero() - fuel_mass_per_tank = [] - for empty_tank in tanks: - filled_tank_wo_structure = MassPropertiesIO().initialize_zero() - filled_tank_wo_structure.center_of_gravity = empty_tank.mass_properties.center_of_gravity - - if overall_fuel_mass_per_tank <= empty_tank.energy / empty_tank.gravimetric_density: - # if overall_fuel_mass_per_tank <= empty_tank.volume["value"] * empty_tank.fuel_density: - filled_tank_wo_structure.mass = overall_fuel_mass_per_tank - else: - runtime_output.critical( - "Fuel mass calculation failed ... tank to small") - raise ValueError("Fuel mass calculation failed ... tank to small") - # No adaption of Center of gravity or Inertia here - - filled_tank_wo_structure.moment_change = (filled_tank_wo_structure.mass * - filled_tank_wo_structure.center_of_gravity["x"]) - - fuel_mass_per_tank.append(filled_tank_wo_structure) - - fuel_mass_properties.mass = sum( - [filled_tank.mass for filled_tank in fuel_mass_per_tank]) - fuel_mass_properties.center_of_gravity = calculate_center_of_gravity( - fuel_mass_per_tank) - fuel_mass_properties.initialize_zero_inertia() - - return fuel_mass_properties, fuel_mass_per_tank - - -def calculate_fuel_mass_properties(tanks, fuel_mass, routing_dict, dict_ac_exchange): - """ - Refuel tanks in the following priority order: - 1. Left and Right wing tanks (equally). These are also sorted based on the order defined before according - to the wing mounting - 2. Center tank. - 3. Remaining tanks. - - Parameters: - tanks (list): List of tank objects with properties like capacity, location, and mass. - fuel_mass (float): Total fuel mass to be distributed among the tanks. - - Returns: - list: Fuel distribution across the tanks. - - Raises: - ValueError: If the fuel mass exceeds the total tank capacity. - """ - import logging - runtime_output = logging.getLogger('module_logger') - - # Helper function to categorize tanks based on location string - def categorize_tank_location(tank): - location = tank.tank_location.split() - if "left" in location: - return "left" - elif "right" in location: - return "right" - elif "center" in location: - return "center" - else: - return "other" - - # Sort tanks into categories based on location - left_wing_tanks = [ - tank for tank in tanks if categorize_tank_location(tank) == "left"] - right_wing_tanks = [ - tank for tank in tanks if categorize_tank_location(tank) == "right"] - center_tanks = [ - tank for tank in tanks if categorize_tank_location(tank) == "center"] - other_tanks = [ - tank for tank in tanks if categorize_tank_location(tank) == "other"] - - # Calculate the total available capacity - total_capacity = sum( - tank.energy / tank.gravimetric_density for tank in tanks) - - if fuel_mass > total_capacity: - runtime_output.critical( - "Fuel mass to be filled up exceeds the total capacity of all tanks!") - - # Distribute fuel across left and right wing tanks first - remaining_fuel = fuel_mass - fuel_mass_per_tank = [] - - # Step 1: Fill the wing tanks (equally, if present) - if left_wing_tanks and right_wing_tanks: - for left_tank, right_tank in zip(left_wing_tanks, right_wing_tanks): - left_capacity = left_tank.energy / left_tank.gravimetric_density - right_capacity = right_tank.energy / right_tank.gravimetric_density - - filled_tank_wo_structure_left = MassPropertiesIO().initialize_zero() - filled_tank_wo_structure_right = MassPropertiesIO().initialize_zero() - - # Amount to fill equally - fuel_to_fill = min( - left_capacity, right_capacity, remaining_fuel / 2) - - # Fill both left and right tanks - if fuel_to_fill > 0: - remaining_fuel -= 2 * fuel_to_fill - - filled_tank_wo_structure_left.mass = fuel_to_fill - filled_tank_wo_structure_left.center_of_gravity = left_tank.mass_properties.center_of_gravity - filled_tank_wo_structure_left.moment_change = ( - fuel_to_fill * filled_tank_wo_structure_left.center_of_gravity["x"]) - filled_tank_wo_structure_left.tank_location = left_tank.tank_location - filled_tank_wo_structure_left.energy_carrier = right_tank.energy_carrier - filled_tank_wo_structure_left.used_tank = left_tank.used_tank - filled_tank_wo_structure_left.maximum_capacity = left_tank.max_fuel_mass_capacity - - filled_tank_wo_structure_right.mass = fuel_to_fill - filled_tank_wo_structure_right.center_of_gravity = right_tank.mass_properties.center_of_gravity - filled_tank_wo_structure_right.moment_change = ( - fuel_to_fill * filled_tank_wo_structure_right.center_of_gravity["x"]) - filled_tank_wo_structure_right.tank_location = right_tank.tank_location - filled_tank_wo_structure_right.energy_carrier = right_tank.energy_carrier - filled_tank_wo_structure_right.used_tank = right_tank.used_tank - filled_tank_wo_structure_right.maximum_capacity = right_tank.max_fuel_mass_capacity - - fuel_mass_per_tank.append(filled_tank_wo_structure_left) - fuel_mass_per_tank.append(filled_tank_wo_structure_right) - else: - filled_tank_wo_structure_left.mass = 0 - filled_tank_wo_structure_left.center_of_gravity = left_tank.mass_properties.center_of_gravity - filled_tank_wo_structure_left.moment_change = 0 - filled_tank_wo_structure_left.tank_location = left_tank.tank_location - filled_tank_wo_structure_left.energy_carrier = left_tank.energy_carrier - filled_tank_wo_structure_left.used_tank = left_tank.used_tank - filled_tank_wo_structure_left.maximum_capacity = left_tank.max_fuel_mass_capacity - - filled_tank_wo_structure_right.mass = 0 - filled_tank_wo_structure_right.center_of_gravity = right_tank.mass_properties.center_of_gravity - filled_tank_wo_structure_right.moment_change = 0 - filled_tank_wo_structure_right.tank_location = right_tank.tank_location - filled_tank_wo_structure_right.energy_carrier = right_tank.energy_carrier - filled_tank_wo_structure_right.used_tank = right_tank.used_tank - filled_tank_wo_structure_right.maximum_capacity = right_tank.max_fuel_mass_capacity - - fuel_mass_per_tank.append(filled_tank_wo_structure_left) - fuel_mass_per_tank.append(filled_tank_wo_structure_right) - - # Fill the center tank (if available) - if center_tanks: - for center_tank in center_tanks: - if remaining_fuel > 0: - center_capacity = center_tank.energy / center_tank.gravimetric_density - fuel_to_fill = min(center_capacity, remaining_fuel) - if fuel_to_fill > 0: - filled_tank_wo_structure = MassPropertiesIO().initialize_zero() - filled_tank_wo_structure.mass = fuel_to_fill - filled_tank_wo_structure.center_of_gravity = center_tank.mass_properties.center_of_gravity - filled_tank_wo_structure.moment_change = ( - fuel_to_fill * filled_tank_wo_structure.center_of_gravity["x"]) - filled_tank_wo_structure.tank_location = center_tank.tank_location - filled_tank_wo_structure.energy_carrier = center_tank.energy_carrier - filled_tank_wo_structure.used_tank = center_tank.used_tank - filled_tank_wo_structure.maximum_capacity = center_tank.max_fuel_mass_capacity - fuel_mass_per_tank.append(filled_tank_wo_structure) - remaining_fuel -= fuel_to_fill - else: - filled_tank_wo_structure = MassPropertiesIO().initialize_zero() - filled_tank_wo_structure.mass = 0 - filled_tank_wo_structure.center_of_gravity = center_tank.mass_properties.center_of_gravity - filled_tank_wo_structure.moment_change = 0 - filled_tank_wo_structure.tank_location = center_tank.tank_location - filled_tank_wo_structure.energy_carrier = center_tank.energy_carrier - filled_tank_wo_structure.used_tank = center_tank.used_tank - filled_tank_wo_structure.maximum_capacity = center_tank.max_fuel_mass_capacity - fuel_mass_per_tank.append(filled_tank_wo_structure) - - # Fill the remaining tanks with any leftover fuel - for other_tank in other_tanks: - if remaining_fuel <= 0: - filled_tank_wo_structure = MassPropertiesIO().initialize_zero() - filled_tank_wo_structure.mass = 0 - filled_tank_wo_structure.center_of_gravity = other_tank.mass_properties.center_of_gravity - filled_tank_wo_structure.moment_change = 0 - filled_tank_wo_structure.tank_location = other_tank.tank_location - filled_tank_wo_structure.energy_carrier = other_tank.energy_carrier - filled_tank_wo_structure.used_tank = other_tank.used_tank - filled_tank_wo_structure.maximum_capacity = other_tank.max_fuel_mass_capacity - fuel_mass_per_tank.append(filled_tank_wo_structure) - else: - other_capacity = other_tank.energy / other_tank.gravimetric_density - fuel_to_fill = min(other_capacity, remaining_fuel) - if fuel_to_fill > 0: - filled_tank_wo_structure = MassPropertiesIO().initialize_zero() - filled_tank_wo_structure.mass = fuel_to_fill - filled_tank_wo_structure.center_of_gravity = other_tank.mass_properties.center_of_gravity - filled_tank_wo_structure.moment_change = ( - fuel_to_fill * filled_tank_wo_structure.center_of_gravity["x"]) - filled_tank_wo_structure.tank_location = other_tank.tank_location - filled_tank_wo_structure.energy_carrier = other_tank.energy_carrier - filled_tank_wo_structure.used_tank = other_tank.used_tank - filled_tank_wo_structure.maximum_capacity = other_tank.max_fuel_mass_capacity - fuel_mass_per_tank.append(filled_tank_wo_structure) - remaining_fuel -= fuel_to_fill - - # Check if there is still remaining fuel (should not happen unless there is a capacity error) - if remaining_fuel > 0: - if int(dict_ac_exchange['tool_level']) < int(routing_dict['tool_level']): - runtime_output.warning( - "Fuel mass calculation failed ... Not enough capacity in tanks!") - else: - raise ValueError( - "Fuel mass calculation failed ... Not enough capacity in tanks!") - - fuel_mass_properties = MassPropertiesIO().initialize_zero() - fuel_mass_properties.mass = sum( - [filled_tank.mass for filled_tank in fuel_mass_per_tank]) - fuel_mass_properties.center_of_gravity = calculate_center_of_gravity( - fuel_mass_per_tank) - fuel_mass_properties.initialize_zero_inertia() - - # Return the fuel mass properties and distribution per tank - return fuel_mass_properties, fuel_mass_per_tank - - -def calculate_fuel_properties_defueling( - tanks, fuel_to_remove, fuel_mass_per_tank, runtime_output, routing_dict, dict_ac_exchange): - - def map_fuel_mass_to_tanks(tanks, fuel_mass_per_tank, runtime_output): - - # Helper function to categorize tanks and fuel mass objects by their location - def get_tank_location_key(tank): - # Create a normalized key from tank location - return tank.tank_location.strip().lower() - - # Create a mapping of locations to fuel_mass_per_tank objects - fuel_mass_mapping = {get_tank_location_key( - fuel_tank): fuel_tank for fuel_tank in fuel_mass_per_tank} - - # Iterate over tanks and update their filled_fuel_mass based on the corresponding fuel_mass_per_tank - for tank in tanks: - tank_location_key = get_tank_location_key(tank) - - if tank_location_key in fuel_mass_mapping: - corresponding_fuel = fuel_mass_mapping[tank_location_key] - # Update tank with the filled mass value - tank.filled_fuel_mass = corresponding_fuel.mass - else: - runtime_output.critical( - f"No matching fuel mass found for tank at location: {tank.tank_location}. \ - No fuel was loaded in this tank.") - raise ValueError( - f"No matching fuel mass found for tank at location: {tank.tank_location}. \ - No fuel was loaded in this tank.") - - # This function modifies the tanks in place, so no need to return anything - - map_fuel_mass_to_tanks(tanks, fuel_mass_per_tank, - runtime_output) # the tanks are already sorted in the order for defueling, \ - # this is why here they are only mapped here to get the existing filled up fuel mass - - # Helper function to categorize tanks based on location string - def categorize_tank_location(tank): - location = tank.tank_location.split() - if "left" in location: - return "left" - elif "right" in location: - return "right" - elif "center" in location: - return "center" - else: - return "other" - - # Sort tanks into categories based on location - left_wing_tanks = [ - tank for tank in tanks if categorize_tank_location(tank) == "left"] - right_wing_tanks = [ - tank for tank in tanks if categorize_tank_location(tank) == "right"] - center_tanks = [ - tank for tank in tanks if categorize_tank_location(tank) == "center"] - other_tanks = [ - tank for tank in tanks if categorize_tank_location(tank) == "other"] - - # Calculate the total available fuel in the tanks - total_fuel_available = sum(tank.filled_fuel_mass for tank in tanks) - - if fuel_to_remove > total_fuel_available: - if int(dict_ac_exchange['tool_level']) < int(routing_dict['tool_level']): - runtime_output.warning( - "Fuel to remove exceeds the available fuel in all tanks!") - else: - raise ValueError( - "Fuel to remove exceeds the available fuel in all tanks!") - - remaining_fuel_to_remove = fuel_to_remove - defueled_mass_per_tank = [] - - # Step 1: Defuel other tanks first (start with the trim tank if existing) - for other_tank in other_tanks: - - fuel_available = other_tank.filled_fuel_mass - fuel_to_defuel = min(fuel_available, remaining_fuel_to_remove) - - if fuel_to_defuel > 0: - remaining_fuel_to_remove -= fuel_to_defuel - defueled_tank = MassPropertiesIO().initialize_zero() - defueled_tank.mass = -fuel_to_defuel # Negative for defueling - defueled_tank.center_of_gravity = other_tank.mass_properties.center_of_gravity - defueled_tank.moment_change = (-fuel_to_defuel * - defueled_tank.center_of_gravity["x"]) - defueled_tank.name = other_tank.tank_location - defueled_tank.fuel_left_in_tank = fuel_available - fuel_to_defuel - defueled_mass_per_tank.append(defueled_tank) - other_tank.filled_fuel_mass -= fuel_to_defuel - else: - defueled_tank = MassPropertiesIO().initialize_zero() - defueled_tank.mass = 0 # Negative for defueling - defueled_tank.center_of_gravity = other_tank.mass_properties.center_of_gravity - defueled_tank.moment_change = 0 - defueled_tank.name = other_tank.tank_location - defueled_tank.fuel_left_in_tank = fuel_available - 0 - defueled_mass_per_tank.append(defueled_tank) - other_tank.filled_fuel_mass -= 0 - - # Step 2: Defuel center tank - for center_tank in center_tanks: - if remaining_fuel_to_remove > 0: - fuel_available = center_tank.filled_fuel_mass - fuel_to_defuel = min(fuel_available, - remaining_fuel_to_remove) - if fuel_to_defuel > 0: - defueled_tank = MassPropertiesIO().initialize_zero() - defueled_tank.mass = -fuel_to_defuel # Negative for defueling - defueled_tank.center_of_gravity = center_tank.mass_properties.center_of_gravity - defueled_tank.moment_change = (-fuel_to_defuel * - defueled_tank.center_of_gravity["x"]) - defueled_tank.name = center_tank.tank_location - defueled_tank.fuel_left_in_tank = fuel_available - fuel_to_defuel - defueled_mass_per_tank.append(defueled_tank) - center_tank.filled_fuel_mass -= fuel_to_defuel - remaining_fuel_to_remove -= fuel_to_defuel - else: - defueled_tank = MassPropertiesIO().initialize_zero() - defueled_tank.mass = 0 # nothing to remove - defueled_tank.center_of_gravity = center_tank.mass_properties.center_of_gravity - defueled_tank.moment_change = 0 - defueled_tank.name = center_tank.tank_location - defueled_tank.fuel_left_in_tank = fuel_available - 0 - defueled_mass_per_tank.append(defueled_tank) - center_tank.filled_fuel_mass -= 0 - - # Step 3: Defuel wing tanks (opposite to refueling) - if left_wing_tanks and right_wing_tanks and remaining_fuel_to_remove > 0: - for left_tank, right_tank in zip(left_wing_tanks, right_wing_tanks): - - left_fuel_available = left_tank.filled_fuel_mass - right_fuel_available = right_tank.filled_fuel_mass - - fuel_to_defuel = min( - left_fuel_available, right_fuel_available, remaining_fuel_to_remove / 2) - - if fuel_to_defuel > 0: - remaining_fuel_to_remove -= 2 * fuel_to_defuel - - defueled_tank_left = MassPropertiesIO().initialize_zero() - defueled_tank_left.mass = -fuel_to_defuel # Negative for defueling - defueled_tank_left.center_of_gravity = left_tank.mass_properties.center_of_gravity - defueled_tank_left.moment_change = ( - -fuel_to_defuel * defueled_tank_left.center_of_gravity["x"]) - defueled_tank_left.name = left_tank.tank_location - defueled_tank_left.fuel_left_in_tank = left_fuel_available - fuel_to_defuel - - defueled_tank_right = MassPropertiesIO().initialize_zero() - defueled_tank_right.mass = -fuel_to_defuel # Negative for defueling - defueled_tank_right.center_of_gravity = right_tank.mass_properties.center_of_gravity - defueled_tank_right.moment_change = ( - -fuel_to_defuel * defueled_tank_right.center_of_gravity["x"]) - defueled_tank_right.name = right_tank.tank_location - defueled_tank_right.fuel_left_in_tank = right_fuel_available - fuel_to_defuel - - defueled_mass_per_tank.append(defueled_tank_left) - defueled_mass_per_tank.append(defueled_tank_right) - - left_tank.filled_fuel_mass -= fuel_to_defuel - right_tank.filled_fuel_mass -= fuel_to_defuel - else: - defueled_tank_left = MassPropertiesIO().initialize_zero() - defueled_tank_left.mass = 0 # nothing to remove - defueled_tank_left.center_of_gravity = left_tank.mass_properties.center_of_gravity - defueled_tank_left.moment_change = 0 - defueled_tank_left.name = left_tank.tank_location - defueled_tank_left.fuel_left_in_tank = left_fuel_available - 0 - - defueled_tank_right = MassPropertiesIO().initialize_zero() - defueled_tank_right.mass = 0 - defueled_tank_right.center_of_gravity = right_tank.mass_properties.center_of_gravity - defueled_tank_right.moment_change = 0 - defueled_tank_right.name = right_tank.tank_location - defueled_tank_right.fuel_left_in_tank = right_fuel_available - 0 - - defueled_mass_per_tank.append(defueled_tank_left) - defueled_mass_per_tank.append(defueled_tank_right) - - left_tank.filled_fuel_mass -= 0 - right_tank.filled_fuel_mass -= 0 - - # Check if we have defueled the correct amount - if remaining_fuel_to_remove > 0: - if int(dict_ac_exchange['tool_level']) < int(routing_dict['tool_level']): - runtime_output.warning( - "Not enough fuel in the tanks to complete defueling!") - else: - raise ValueError( - "Not enough fuel in the tanks to complete defueling!") - - return defueled_mass_per_tank - - -def calculate_design_fuel_mass(mission_information=None, maximum_takeoff_mass=None, - operating_mass_empty=None, design_payload_mass=None, tanks=None, - routing_dict=None, dict_ac_exchange=None): - """Calculation of design fuel mass - - Args: - mission_information (object, optional): design mission information from ac_exchange_file. - maximum_takeoff_mass (float, optional): maximum takeoff mass. - operating_mass_empty (float, optional): operating mass empty. - transport_task_data (float, optional): transport task data (passengers, mass p.P, luggage p.P). - Defaults to None. - - Returns: - float: design fuel @ takeoff, midflight and landing (minimum) - """ - - if mission_information is not None: - # print("Calculating design fuel mass based on mission information") - - cruise_fuel = mission_information["trip_fuel_mass"] - \ - mission_information["takeoff_fuel_mass"] - \ - mission_information["landing_fuel_mass"] - design_fuel_mass = mission_information["mission_fuel_mass"] - design_fuel_mass_at_takeoff = mission_information["mission_fuel_mass"] - \ - mission_information["taxi_out_fuel_mass"] - design_fuel_mass_midflight = design_fuel_mass_at_takeoff - \ - mission_information["takeoff_fuel_mass"] - cruise_fuel / 2 - design_fuel_mass_landing_minimum = mission_information["mission_fuel_mass"] - \ - mission_information["trip_fuel_mass"] - \ - mission_information["taxi_out_fuel_mass"] - consumed_fuel_during_flight = mission_information["trip_fuel_mass"] - - # distribute the fuel along the tanks and save the informations per tank - # the refueling order is already saved in the sorted tanks - # = (preparation for the calculation of the refueling-cg-shift) - design_fuel_mass, design_fuel_mass_per_tank = calculate_fuel_mass_properties( - tanks, design_fuel_mass, routing_dict, dict_ac_exchange) - design_fuel_mass_takeoff, design_fuel_mass_takeoff_per_tank = calculate_fuel_mass_properties( - tanks, design_fuel_mass_at_takeoff, routing_dict, dict_ac_exchange) - design_fuel_mass_midflight, _ = calculate_fuel_mass_properties( - tanks, design_fuel_mass_midflight, routing_dict, dict_ac_exchange) - design_fuel_mass_landing_minimum, _ = calculate_fuel_mass_properties( - tanks, design_fuel_mass_landing_minimum, routing_dict, dict_ac_exchange) - else: - contingency_fuel_factor = 0.05 - alternate_fuel_factor = 0.1 - final_reserve_fuel_factor = 0.02 - - design_fuel_mass_takeoff = ( - maximum_takeoff_mass.mass - operating_mass_empty.mass - design_payload_mass.mass) - design_fuel_mass_midflight = design_fuel_mass_takeoff / 2.0 - design_fuel_mass_landing_minimum = design_fuel_mass_takeoff * \ - (contingency_fuel_factor + alternate_fuel_factor + final_reserve_fuel_factor) - consumed_fuel_during_flight = design_fuel_mass_takeoff - \ - design_fuel_mass_landing_minimum - - # distribute the fuel along the tanks and save the informations per tank - # the refueling order is already saved in the sorted tanks - # = (preparation for the calculation of the refueling-cg-shift) - design_fuel_mass_takeoff, design_fuel_mass_takeoff_per_tank = calculate_fuel_mass_properties( - tanks, design_fuel_mass_takeoff) - design_fuel_mass_midflight, _ = calculate_fuel_mass_properties( - tanks, design_fuel_mass_midflight, routing_dict, dict_ac_exchange) - design_fuel_mass_landing_minimum, _ = calculate_fuel_mass_properties( - tanks, design_fuel_mass_landing_minimum, routing_dict, dict_ac_exchange) - design_fuel_mass = design_fuel_mass_at_takeoff - design_fuel_mass_per_tank = design_fuel_mass_takeoff_per_tank - - return (design_fuel_mass_takeoff, design_fuel_mass_midflight, design_fuel_mass_landing_minimum, - design_fuel_mass_takeoff_per_tank, consumed_fuel_during_flight, design_fuel_mass, design_fuel_mass_per_tank) - - -def calculate_design_payload_mass(transport_task_data, fuselage_mass_properties, max_payload, runtime_output): - """Design payload mass - - Args: - transport_task_data (TransportTaskIO object): Transport task data - fuselage_mass_properties (MassPropertiesIO object): Fuselage mass properties - - Returns: - MassPropertiesIO Object: design payload mass - """ - design_payload_mass = MassPropertiesIO( - "./analysis/masses_cg_inertia/design_payload_mass") - design_payload_mass.mass = transport_task_data.combined_passenger_and_luggage_mass + \ - transport_task_data.additional_cargo_mass - - if design_payload_mass.mass > max_payload.mass: - runtime_output.critical( - "Design payload mass exceeds the maximum allowed payload mass!") - raise ValueError( - "Design payload mass exceeds the maximum allowed payload mass!") - - # cargodeck + passenger_deck + additional cargo - design_payload_mass.center_of_gravity = fuselage_mass_properties.center_of_gravity - design_payload_mass.initialize_zero_inertia() - DEBUG(msg="The CG of the design payload is set at the fuselage CG.") - - return design_payload_mass - - -def cg_change_passengers_boarding_front_back( - initial_cg, initial_weight, x_coordinate_row, number_of_passengers_per_row, acommodation_data, mac, LE): - - cg_positions = [] - total_weight = [] - - total_weight.append(initial_weight) - total_weight_new = initial_weight - total_moment_change = initial_cg * initial_weight - initial_mac_position = (initial_cg - LE) / mac * 100.0 - cg_positions.append(initial_mac_position) - - for row in range(len(x_coordinate_row)): - added_mass = acommodation_data.mass_per_passenger * \ - number_of_passengers_per_row[row] - - total_weight_new += added_mass - total_moment_change += added_mass * (x_coordinate_row[row]) - new_cg = total_moment_change / total_weight_new - new_mac_position = (new_cg - LE) / mac * 100.0 - - cg_positions.append(new_mac_position) - total_weight.append(total_weight_new) - - return cg_positions, total_weight - - -def cg_change_cargo_loading_front_back(initial_cg, initial_weight, x_coordinate_row, transport_task_data, mac, LE): - - cg_positions = [] - total_weight = [] - - total_weight.append(initial_weight) - total_weight_new = initial_weight - total_moment_change = initial_cg * initial_weight - initial_mac_position = (initial_cg - LE) / mac * 100.0 - cg_positions.append(initial_mac_position) - - for row in range(len(x_coordinate_row)): - added_mass = (transport_task_data.luggage_mass_per_passenger * transport_task_data.number_of_passengers + - transport_task_data.additional_cargo_mass) / len(x_coordinate_row) - - total_weight_new += added_mass - total_moment_change += added_mass * (x_coordinate_row[row]) - - new_cg = total_moment_change / total_weight_new - new_mac_position = (new_cg - LE) / mac * 100.0 - - cg_positions.append(new_mac_position) - total_weight.append(total_weight_new) - - return cg_positions, total_weight - - -def cg_change_passengers_boarding_front_back_excel(initial_cg, initial_weight, seating, acommodation_data, mac, LE): - - cg_positions = [] - total_weight = [] - - total_weight.append(initial_weight) - total_weight_new = initial_weight - total_moment_change = initial_cg * initial_weight - initial_mac_position = (initial_cg - LE) / mac * 100.0 - cg_positions.append(initial_mac_position) - - for index, row in seating.iterrows(): - total_weight_new += row['Number of Seats'] * \ - acommodation_data.mass_per_passenger - total_moment_change += (row['X Coordinate']) * \ - row['Number of Seats'] * acommodation_data.mass_per_passenger - - new_cg = total_moment_change / total_weight_new - new_mac_position = (new_cg - LE) / mac * 100.0 - - cg_positions.append(new_mac_position) - total_weight.append(total_weight_new) - - return cg_positions, total_weight - - -def cg_change_passengers_boarding_window_aisle( - initial_cg, initial_weight, x_coordinate_row, number_of_passengers_per_row, acommodation_data, mac, LE): - - # total nr of seats per row - nr_of_seats = number_of_passengers_per_row.copy() - - max_seats = math.ceil(max(nr_of_seats) / 2) - - cg_positions = [] - total_weight = [] - - total_weight.append(initial_weight) - total_weight_new = initial_weight - total_moment_change = initial_cg * initial_weight - initial_mac_position = (initial_cg - LE) / mac * 100.0 - cg_positions.append(initial_mac_position) - - for y in range(max_seats): - - for row in range(len(x_coordinate_row)): - - left_seats = nr_of_seats[row] - nr_of_seats[row] = nr_of_seats[row] - 2 - - if left_seats >= 2: - added_mass = acommodation_data.mass_per_passenger * 2 - elif left_seats == 1: - added_mass = acommodation_data.mass_per_passenger - else: - continue # No more seats left to occupy in this row - - total_weight_new += added_mass - total_moment_change += added_mass * (x_coordinate_row[row]) - - new_cg = total_moment_change / total_weight_new - new_mac_position = (new_cg - LE) / mac * 100.0 - - cg_positions.append(new_mac_position) - total_weight.append(total_weight_new) - - return cg_positions, total_weight - - -def calculate_maximum_payload_mass(maximum_structural_payload_mass, fuselage_accomodation_mass_properties): - """Design maximum mass - equal to maximum payload mass # ToDo - - Args: - transport_task_data (TransportTaskIO object): Transport task data - fuselage_mass_properties (MassPropertiesIO object): Fuselage mass properties - - Returns: - MassPropertiesIO Object: maximum payload mass - """ - maximum_payload_mass = MassPropertiesIO( - "./analysis/masses_cg_inertia/maximum_payload_mass") - maximum_payload_mass.mass = maximum_structural_payload_mass - - # Use accomodation center_of_gravity - maximum_payload_mass.center_of_gravity = fuselage_accomodation_mass_properties.center_of_gravity - maximum_payload_mass.initialize_zero_inertia() - DEBUG(msg="The CG of the maximum payload is set at the fuselage CG.") - - return maximum_payload_mass - - -def calculate_maximum_zero_fuel_mass(mass_properties, inertia_method): - """Maximum zero fuel mass - OME + MAX_PAYLOAD - - Arguments: - mass_properties {MassProperties IO Object} -- _description_ - inertia_method {_type_} -- _description_ - - Returns: - _type_ -- _description_ - """ - maximum_zero_fuel_mass = MassPropertiesIO( - "./analysis/masses_cg_inertia/maximum_zero_fuel_mass") - - maximum_zero_fuel_mass.mass = sum( - [element.mass for element in mass_properties]) - maximum_zero_fuel_mass.center_of_gravity = calculate_center_of_gravity( - mass_properties) - - maximum_zero_fuel_mass.inertia = inertia_method["method"]( - [mass_properties[0], mass_properties[1], MassPropertiesIO( - ).initialize_zero()], inertia_method["additional_parameters"] - ) - return maximum_zero_fuel_mass - - -def calculate_design_mass(mass_properties, inertia_method): - design_mass = MassPropertiesIO( - "./analysis/masses_cg_inertia/design_mass") - - design_mass.mass = sum([element.mass for element in mass_properties]) - design_mass.center_of_gravity = calculate_center_of_gravity( - mass_properties) - design_mass.inertia = inertia_method["method"]( - mass_properties, inertia_method["additional_parameters"] - ) - - return design_mass - - -def calculate_mtow_max_payload(ome, maximum_payload_mass, wing, mtom, LE, mac): - mtow_max_payload = MassPropertiesIO() - - fuel = mtom.mass - maximum_payload_mass.mass - ome.mass - - mtow_max_payload.mass = ome.mass + fuel + maximum_payload_mass.mass - mtow_max_payload.center_of_gravity = ( - ome.mass * ome.center_of_gravity['x'] + fuel * wing.center_of_gravity['x'] + - maximum_payload_mass.mass * maximum_payload_mass.center_of_gravity['x']) / mtow_max_payload.mass - DEBUG(msg="Estimated CG of MTOM with max payload. (fuel CG set in wing CG, payload CG set in fuselage CG.)") - mtow_max_payload.cg_mac = ( - mtow_max_payload.center_of_gravity - LE) / mac * 100.0 - - return mtow_max_payload - - -def calculate_mtow_max_fuel(ome, fuel_mass, wing, fuselage, mtom, LE, mac): - mtow_max_fuel = MassPropertiesIO() - - max_fuel = sum([element.mass for element in fuel_mass]) - - payload_mass = mtom.mass - max_fuel - ome.mass - - mtow_max_fuel.mass = ome.mass + max_fuel + payload_mass - mtow_max_fuel.center_of_gravity = (ome.mass * ome.center_of_gravity['x'] + max_fuel * wing.center_of_gravity['x'] + - payload_mass * fuselage.center_of_gravity['x']) / mtow_max_fuel.mass - DEBUG(msg="Estimated CG of MTOM with max fuel. (fuel CG set in wing CG, payload CG set in fuselage CG.)") - mtow_max_fuel.cg_mac = (mtow_max_fuel.center_of_gravity - LE) / mac * 100.0 - - return mtow_max_fuel - - -def custom_sort_low_wing(obj): - # Split the 'tank_location' entry into words - name_words = obj.tank_location.split() - - if 'right' in name_words and 'wing' in name_words: - if 'inner' in name_words: - return 0 - elif 'outer' in name_words: - return 2 - elif 'center' in name_words: - return 4 - elif 'left' in name_words and 'wing' in name_words: - if 'inner' in name_words: - return 1 - elif 'outer' in name_words: - return 3 - elif 'center' in name_words: - return 5 - else: - return 6 # If none of the criteria are met, maintain the order - - -def custom_sort_high_wing(obj): - # Split the 'tank_location' entry into words - name_words = obj.tank_location.split() - - if 'right' in name_words and 'wing' in name_words: - if 'outer' in name_words: - return 0 - elif 'inner' in name_words: - return 2 - elif 'center' in name_words: - return 4 - elif 'left' in name_words and 'wing' in name_words: - if 'outer' in name_words: - return 1 - elif 'inner' in name_words: - return 3 - elif 'center' in name_words: - return 5 - else: - return 6 # If none of the criteria are met, maintain the order - - -def custom_sort_defueling(obj): - # Split the 'tank_location' entry into words - name_words = obj.tank_location.split() - - if 'right' in name_words and 'wing' in name_words: - if 'center' in name_words: - return 0 - elif 'inner' in name_words: - return 2 - elif 'outer' in name_words: - return 4 - elif 'left' in name_words and 'wing' in name_words: - if 'center' in name_words: - return 1 - elif 'inner' in name_words: - return 3 - elif 'outer' in name_words: - return 5 - else: - return 6 # If none of the criteria are met, maintain the order - - -def read_tank_properties(ac_exchange_file, defueling, runtime_output): - """Read tank properties - mass specific - - Args: - ac_exchange_file (elementtree): aircraft_xml_elementtree - - Returns: - list of tanks: list of all tanks in the aircraft - """ - tanks_nodes = ac_exchange_file.findall( - "./component_design/tank/specific/tank[@ID]") - tanks = [] - - for tank_node in tanks_nodes: - tanks.append(TankIO().read(tank_node)) - - tanks_ref_point_x = float(ac_exchange_file.find( - "./component_design/tank/position/x/value").text) - - energy_carrier = [] - tank_requirements_nodes = ac_exchange_file.findall( - "./requirements_and_specifications/design_specification/configuration/tank_definition/tank[@ID]") - i = 0 - for tank_definition_node in tank_requirements_nodes: - - tank_energy_carrier_ID = int( - tank_definition_node.find("./energy_carrier_ID/value").text) - - energy_carrier_nodes = ac_exchange_file.findall( - "./requirements_and_specifications/design_specification/energy_carriers/energy_carrier[@ID]") - - energy_carrier = energy_carrier_nodes[tank_energy_carrier_ID].find( - "./type/value").text - energy_carrier_density = float( - energy_carrier_nodes[tank_energy_carrier_ID].find("./density/value").text) - energy_carrier_gravimetric_density = float( - energy_carrier_nodes[tank_energy_carrier_ID].find("./gravimetric_density/value").text) - - if len(tank_requirements_nodes) == len(tanks): - tanks[i].energy_carrier = energy_carrier - # 783 # kg/m^3 Jet A1 Kerosene - tanks[i].fuel_density = energy_carrier_density - # 43 MJ/kg for kerosene, 1.1993E8 for liqiud hydrogen - tanks[i].gravimetric_density = energy_carrier_gravimetric_density - tanks[i].max_fuel_mass_capacity = tanks[i].energy / \ - tanks[i].gravimetric_density - tanks[i].mass_properties.center_of_gravity["x"] = tanks_ref_point_x + \ - tanks[i].position["x"]["value"] + \ - tanks[i].centroid["x"]["value"] - else: - runtime_output.critical( - "No consistent tanks definitions with the number of designed tanks.") - raise ValueError( - "No consistent tanks definitions with the number of designed tanks.") - i += 1 - - # Sort the tanks according to the wing mounting using the custom sorting function - wing_mounting = str(ac_exchange_file.find( - "./requirements_and_specifications/design_specification/configuration/wing_definition/mounting/value").text) - - if wing_mounting == "high": - # For high wing -> outer - inner - center - other tanks - sorted_tanks = sorted(tanks, key=custom_sort_high_wing) - elif wing_mounting == "low" or wing_mounting == "mid": - # For low wing -> inner - outer - center - other tanks - sorted_tanks = sorted(tanks, key=custom_sort_low_wing) - else: - # order as in axml - sorted_tanks = tanks # adapt / change for bwb, h2? - - if defueling == "mode_1": - # inverse order of refueling - sorted_tanks_defueling = sorted_tanks[::-1] - else: - sorted_tanks_defueling = tanks # adapt / change for bwb, h2? - - return sorted_tanks, sorted_tanks_defueling - - -def read_propulsion_properties(ac_exchange_file): - propulsor_nodes = ac_exchange_file.findall( - "./component_design/propulsion/specific/propulsion[@ID]" - ) - - pylons = [] - nacelles = [] - engines = [] - for propulsor_node in propulsor_nodes: - - nacelle_nodes = propulsor_node.findall(".//nacelle[@ID]") - for nacelle_node in nacelle_nodes: - nacelles.append(NacelleIO().read(nacelle_node)) - - # nacelles.append(NacelleIO().read(propulsor_node)) - pylons.append(PylonIO().read(propulsor_node)) - engines.append(EngineIO().read(propulsor_node)) - - return nacelles, pylons, engines - - -def read_main_components(ac_exchange_file, dict_ac_exchange, runtime_output): - - components_available = [ - key.replace('_exist', '') for key in dict_ac_exchange if dict_ac_exchange[key] is True and '_exist' in key] - - mass_properties = {} - # Get overall mass properties - for component in components_available: - runtime_output.print( - f"Mass properties of component {component} read ...") - mass_properties[component] = MassPropertiesIO( - f"./component_design/{component}/mass_properties") - mass_properties[component].read(ac_exchange_file) - runtime_output.print( - f"{component} - mass: {mass_properties[component].mass}") - - return mass_properties - - -def read_transport_task(ac_exchange_file): - return TransportTaskIO().read(ac_exchange_file) - - -def read_acommodation(ac_exchange_file, excel_seatings_file): - return AcommodationIO().read(ac_exchange_file, excel_seatings_file) - - -def read_maximum_takeoff_mass(ac_exchange_file): - return MassPropertiesIO("./analysis/masses_cg_inertia/maximum_takeoff_mass/mass_properties").read(ac_exchange_file) - - -def read_operator_items(ac_exchange_file, fuselage_length): - # the operator items are written by the fuselage node and the systems node - operator_items = [] - i = 0 - total_mass = 0 - - fuselage_nodes = ac_exchange_file.findall( - "./component_design/fuselage/specific/geometry/fuselage[@ID]" - ) - for fuselage_node in fuselage_nodes: - fuselage_operator_nodes = fuselage_node.findall( - "./mass_breakdown/fuselage_operator_items/component_mass[@ID]") - for operator in fuselage_operator_nodes: - operator_items.append(ComponentMassIO(".").read(operator)) - operator_items[i].center_of_gravity['x'] = 0.45 * \ - fuselage_length # + fuselage_reference_point - total_mass += operator_items[i].mass - fuselage_operator_items_mass = total_mass - i = i + 1 - DEBUG(msg="CGs of fuselage operator items estimated here at 45% fuselage length") - - systems_operator_nodes = ac_exchange_file.findall( - "./component_design/systems/operator_items/operator_item[@ID]" - ) - - for operator in systems_operator_nodes: - operator_items.append(MassPropertiesIO( - "./mass_properties").read(operator)) - operator_items[i].name = str(operator.find("./name/value").text) - total_mass += operator_items[i].mass - i = i + 1 - - systems_operator_items_mass = total_mass - fuselage_operator_items_mass - - return operator_items, fuselage_operator_items_mass, systems_operator_items_mass - - -def read_furnishings(ac_exchange_file): - - fuselage_nodes = ac_exchange_file.findall( - "./component_design/fuselage/specific/geometry/fuselage[@ID]" - ) - furnishings = [] - i = 0 - furnishings_total_mass = 0 - - for fuselage_node in fuselage_nodes: - furnishing_nodes = fuselage_node.findall( - "./mass_breakdown/fuselage_furnishing/component_mass[@ID]") - for furnishing in furnishing_nodes: - furnishings.append(ComponentMassIO(".").read(furnishing)) - furnishings_total_mass += furnishings[i].mass - i = i + 1 - - return furnishings_total_mass, furnishings - - -def read_systems(ac_exchange_file): - - systems_nodes = ac_exchange_file.findall( - "./component_design/systems/specific/geometry/mass_properties/" - ) - systems = [] - - for node in systems_nodes: - item = {} - item['name'] = node.tag - item['mass'] = float(node.find("./value").text) - - systems.append(item) - - return systems - - -def read_wing(paths): - - acxml = aixml.openDocument(paths["path_to_aircraft_exchange_file"]) - airfoil_data_dir = f'{paths["project_directory"]}/geometry_data/airfoil_data' - - return geom2.factory.WingFactory(acxml, airfoil_data_dir).create("wing/specific/geometry/aerodynamic_surface@0") - - -def read_fuselage(paths): - acxml = aixml.openDocument(paths["path_to_aircraft_exchange_file"]) - geometry_data_dir = f'{paths["project_directory"]}/geometry_data/' - return geom2.factory.FuselageFactory(acxml, geometry_data_dir).create("fuselage/specific/geometry/fuselage@0") - - -def determine_mission_fuel(dict_ac_exchange): - - # Initialize an empty dictionary to store fuel mass for each entry - fuel_mass_results = {} - - # Iterate over the energy entries - for energy_id, consumed_energy in dict_ac_exchange["loaded_mission_energy"].items(): - # Generate the corresponding carrier key by replacing "energy" with "energy_carrier" - carrier_key = energy_id.replace("energy", "energy_carrier") - - # Retrieve the integer carrier ID from "loaded_mission_energy_carrier" - carrier_id_int = dict_ac_exchange["loaded_mission_energy_carrier"].get( - carrier_key) - - # Construct the gravimetric density ID using the integer carrier ID - density_id = f"energy_carrier_gravimetric_density_ID{int(carrier_id_int)}" - - # Retrieve the gravimetric density for this specific carrier - gravimetric_density = dict_ac_exchange["energy_carrier_gravimetric_density"].get( - density_id) - - # Calculate fuel mass if gravimetric density is available - if gravimetric_density is not None: - fuel_mass = consumed_energy / gravimetric_density - fuel_mass_results[energy_id] = fuel_mass - else: - print( - f"{energy_id} - Gravimetric density not found for carrier ID {carrier_id_int}") - - mission_fuel_mass = sum(fuel_mass_results.values()) - - return mission_fuel_mass - - -def determine_taxi_in_fuel(dict_ac_exchange): - - # Initialize an empty dictionary to store fuel mass for each entry - fuel_mass_results = {} - - # Iterate over the energy entries - for energy_id, consumed_energy in dict_ac_exchange["taxi_energy_in"].items(): - # Generate the corresponding carrier key by replacing "energy" with "energy_carrier" - carrier_key = energy_id.replace("in", "in_carrier") - - # Retrieve the integer carrier ID from "loaded_mission_energy_carrier" - carrier_id_int = dict_ac_exchange["taxi_energy_in_carrier"].get( - carrier_key) - - # Construct the gravimetric density ID using the integer carrier ID - density_id = f"energy_carrier_gravimetric_density_ID{int(carrier_id_int)}" - - # Retrieve the gravimetric density for this specific carrier - gravimetric_density = dict_ac_exchange["energy_carrier_gravimetric_density"].get( - density_id) - - # Calculate fuel mass if gravimetric density is available - if gravimetric_density is not None: - fuel_mass = consumed_energy / gravimetric_density - fuel_mass_results[energy_id] = fuel_mass - else: - print( - f"{energy_id} - Gravimetric density not found for carrier ID {carrier_id_int}") - - taxi_in_fuel_mass = sum(fuel_mass_results.values()) - - return taxi_in_fuel_mass - - -def determine_taxi_out_fuel(dict_ac_exchange): - - # Initialize an empty dictionary to store fuel mass for each entry - fuel_mass_results = {} - - # Iterate over the energy entries - for energy_id, consumed_energy in dict_ac_exchange["taxi_energy_out"].items(): - # Generate the corresponding carrier key by replacing "energy" with "energy_carrier" - carrier_key = energy_id.replace("out", "out_carrier") - - # Retrieve the integer carrier ID from "loaded_mission_energy_carrier" - carrier_id_int = dict_ac_exchange["taxi_energy_out_carrier"].get( - carrier_key) - - # Construct the gravimetric density ID using the integer carrier ID - density_id = f"energy_carrier_gravimetric_density_ID{int(carrier_id_int)}" - - # Retrieve the gravimetric density for this specific carrier - gravimetric_density = dict_ac_exchange["energy_carrier_gravimetric_density"].get( - density_id) - - # Calculate fuel mass if gravimetric density is available - if gravimetric_density is not None: - fuel_mass = consumed_energy / gravimetric_density - fuel_mass_results[energy_id] = fuel_mass - else: - print( - f"{energy_id} - Gravimetric density not found for carrier ID {carrier_id_int}") - - taxi_out_fuel_mass = sum(fuel_mass_results.values()) - - return taxi_out_fuel_mass - - -def determine_takeoff_fuel(dict_ac_exchange, runtime_output): - - # Initialize an empty dictionary to store fuel mass for each entry - fuel_mass_results = {} - - # Iterate over the energy entries - for energy_id, consumed_energy in dict_ac_exchange["take_off_energy"].items(): - if consumed_energy is not None: - # Generate the corresponding carrier key by replacing "energy" with "energy_carrier" - carrier_key = energy_id.replace("energy", "energy_carrier") - - # Retrieve the integer carrier ID from "loaded_mission_energy_carrier" - carrier_id_int = dict_ac_exchange["take_off_energy_carrier"].get( - carrier_key) - - # Construct the gravimetric density ID using the integer carrier ID - density_id = f"energy_carrier_gravimetric_density_ID{int(carrier_id_int)}" - - # Retrieve the gravimetric density for this specific carrier - gravimetric_density = dict_ac_exchange["energy_carrier_gravimetric_density"].get( - density_id) - - # Calculate fuel mass if gravimetric density is available - if gravimetric_density is not None: - fuel_mass = consumed_energy / gravimetric_density - fuel_mass_results[energy_id] = fuel_mass - else: - print( - f"{energy_id} - Gravimetric density not found for carrier ID {carrier_id_int}") - else: - runtime_output.critical( - "The take off energy node not found in the mission analysis block. \ - Fuel mass needed for the takeoff segment set to 0.") - - take_off_fuel_mass = sum(fuel_mass_results.values()) - - return take_off_fuel_mass - - -def determine_landing_fuel(dict_ac_exchange, runtime_output): - - # Initialize an empty dictionary to store fuel mass for each entry - fuel_mass_results = {} - - # Iterate over the energy entries - for energy_id, consumed_energy in dict_ac_exchange["landing_energy"].items(): - if consumed_energy is not None: - # Generate the corresponding carrier key by replacing "energy" with "energy_carrier" - carrier_key = energy_id.replace("energy", "energy_carrier") - - # Retrieve the integer carrier ID from "loaded_mission_energy_carrier" - carrier_id_int = dict_ac_exchange["landing_energy_carrier"].get( - carrier_key) - - # Construct the gravimetric density ID using the integer carrier ID - density_id = f"energy_carrier_gravimetric_density_ID{int(carrier_id_int)}" - - # Retrieve the gravimetric density for this specific carrier - gravimetric_density = dict_ac_exchange["energy_carrier_gravimetric_density"].get( - density_id) - - # Calculate fuel mass if gravimetric density is available - if gravimetric_density is not None: - fuel_mass = consumed_energy / gravimetric_density - fuel_mass_results[energy_id] = fuel_mass - else: - print( - f"{energy_id} - Gravimetric density not found for carrier ID {carrier_id_int}") - else: - runtime_output.critical( - "The landing energy node not found in the mission analysis block. \ - Fuel mass needed for the landing segment set to 0.") - - landing_fuel_mass = sum(fuel_mass_results.values()) - - return landing_fuel_mass - - -def determine_trip_fuel(dict_ac_exchange): - - # Initialize an empty dictionary to store fuel mass for each entry - fuel_mass_results = {} - - # Iterate over the energy entries - for energy_id, consumed_energy in dict_ac_exchange["trip_energy"].items(): - # Generate the corresponding carrier key by replacing "energy" with "energy_carrier" - carrier_key = energy_id.replace("energy", "energy_carrier") - - # Retrieve the integer carrier ID from "loaded_mission_energy_carrier" - carrier_id_int = dict_ac_exchange["trip_energy_carrier"].get( - carrier_key) - - # Construct the gravimetric density ID using the integer carrier ID - density_id = f"energy_carrier_gravimetric_density_ID{int(carrier_id_int)}" - - # Retrieve the gravimetric density for this specific carrier - gravimetric_density = dict_ac_exchange["energy_carrier_gravimetric_density"].get( - density_id) - - # Calculate fuel mass if gravimetric density is available - if gravimetric_density is not None: - fuel_mass = consumed_energy / gravimetric_density - fuel_mass_results[energy_id] = fuel_mass - else: - print( - f"{energy_id} - Gravimetric density not found for carrier ID {carrier_id_int}") - - trip_fuel_mass = sum(fuel_mass_results.values()) - - return trip_fuel_mass - - -def DEBUG(msg=""): - import logging - runtime_output = logging.getLogger('module_logger') - runtime_output.warning(f"Debug {sys._getframe().f_back.f_lineno}: {msg}") +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + +"""Module providing calculation functions provided by the user.""" +# Import own modules. + +from .acommodationIO import AcommodationIO +from .massPropertiesIO import MassPropertiesIO +from .tankIO import TankIO +from .propulsionIO import NacelleIO, PylonIO, EngineIO +from .transportTaskIO import TransportTaskIO +from .componentMassIO import ComponentMassIO + +import sys +import pandas as pd +import math +import csv +import numpy as np +import copy + +import pyaircraftgeometry2 as geom2 +import pyaixml as aixml + +TO_M = 1852 # conversion nautical miles to m from constants + + +def method_basic(paths_and_names, routing_dict, dict_ac_exchange, dict_mod_config, runtime_output): + """Weight and balance function. + + This function performs the calculation of the aircraft weights and the weight and balance loading diagramm. + [Add more information here...] + The output dictionary 'basic_output_dict' contains the results of the methodbasic.py and + is structured according to the following scheme: + basic_output_dict = {'parameter_name_1': value, ...} + + :param dict paths_and_names: Dictionary containing system paths and ElementTrees + :param dict routing_dict: Dictionary containing routing parameter + :param dict dict_ac_exchange: Dict containing parameter and according values from aircraft exchange file + :param dict dict_mod_config: Dict containing parameter and according values from module configuration file + :return dict basic_output_dict: Dictionary containing the aircraft weights, components masses and CG-positions + for kerosene-powered aircraft + """ + ################################################################################################################## + + # Data preparation and reading: + ac_exchange_file = paths_and_names["root_of_aircraft_exchange_tree"] + aircraft_class = dict_mod_config["aircraft_class"] + excel_file_path = paths_and_names['project_directory'] + \ + '/' + "Seats_positions.xlsx" + airplane_seatings = pd.read_excel(excel_file_path) + DEBUG(msg="ToDo: Update this when acommodation positions are available.") + + # Read fuselage and wing + wing = read_wing(paths_and_names) + fuselage = read_fuselage(paths_and_names) + fuselage_length = geom2.measure.length(fuselage) + wing_span = geom2.measure.span(wing) + + # Method selection area: + # Select inertia method + inertia_methods_available = { + "mode_0": {"method": calculate_inertia_by_lth_method, "additional_parameters": [wing_span, fuselage_length]}, + "mode_1": {"method": calculate_inertia_by_components, "additional_parameters": None}, + "mode_2": {"method": calculate_inertia_by_Raymer, + "additional_parameters": [wing_span, fuselage_length, aircraft_class]}, + } + # Select maximum landing mass method here + maximum_landing_mass_methods_available = { + "mode_0": {"method": calculate_maximum_landing_mass_by_default_method, + "additional_parameters": [ac_exchange_file]}, # noPep8 e501 + "mode_1": {"method": calculate_maximum_landing_mass_by_regression_rwth, + "additional_parameters": None} + } + inertia_method = inertia_methods_available[dict_mod_config["mode_inertia_calculation"]] + maximum_landing_mass_method = maximum_landing_mass_methods_available[ + dict_mod_config["mode_maximum_landing_mass_calculation"]] + # Select loading and active modes + # ferry range or design mission + refueling_mode = dict_mod_config["refueling_mode"] + defueling_mode = dict_mod_config["defueling_mode"] # active or not active + # row-wise or window-aisle + passengers_boarding_mode = dict_mod_config["passengers_boarding_mode"] + + # Read further components and mission data + mission_information = { + "trip_fuel_mass": determine_trip_fuel(dict_ac_exchange), + "mission_fuel_mass": determine_mission_fuel(dict_ac_exchange), + "taxi_out_fuel_mass": determine_taxi_out_fuel(dict_ac_exchange), + "taxi_in_fuel_mass": determine_taxi_in_fuel(dict_ac_exchange), + "takeoff_fuel_mass": determine_takeoff_fuel(dict_ac_exchange, runtime_output), + "landing_fuel_mass": determine_landing_fuel(dict_ac_exchange, runtime_output) + } + transport_task_data = read_transport_task(ac_exchange_file) + main_components_mass_properties = read_main_components( + ac_exchange_file, dict_ac_exchange, runtime_output) + tanks, sorted_tanks_defueling = read_tank_properties( + ac_exchange_file, defueling_mode, runtime_output) + nacelles, pylons, engines = read_propulsion_properties(ac_exchange_file) + maximum_takeoff_mass = read_maximum_takeoff_mass(ac_exchange_file) + operator_items, fuselage_operator_items_mass, systems_operator_items_mass = read_operator_items( + ac_exchange_file, fuselage_length) + furnishings_mass, furnishings = read_furnishings(ac_exchange_file) + systems = read_systems(ac_exchange_file) + acommodation_data = read_acommodation(ac_exchange_file, excel_file_path) + # x-coordinates of seats and cargo, nr of rows and nr of passengers per row are manually fixed in AcommodationIO! + DEBUG(msg="ToDo: Modify the AcommodationIO when the data is available") + + # Determine wing geometry data and NP position + mac = geom2.measure.mean_aerodynamic_chord(wing) + x_wing_global_position = dict_ac_exchange["x_wing_globlal_position"] + x_mac = geom2.measure.centroid(wing).x() - 1 / 2 * mac + x_leading_edge_mac = x_wing_global_position + x_mac + neutral_point_mac = ( + dict_ac_exchange["x_aircraft_neutral_point"] - x_leading_edge_mac) / mac * 100. + + ################################################################################################################## + ### Calculate masses ### + + # Operating mass empty - OME + # Maximum fuel mass + # Design fuel mass + # Design payload mass + # Maximum takeoff mass = design mass + # Maximum landing mass + + # Manufacturer mass empty + manufacturer_mass_empty = calculate_manufacturer_mass_empty( + operator_items, main_components_mass_properties, inertia_method).print("Manufacturer mass empty") + + # Operating mass empty = MME + operator items + operating_mass_empty = calculate_operating_mass_empty( + main_components_mass_properties, inertia_method, x_leading_edge_mac, mac).print("Operating mass empty") + + # Maximum PL mass + maximum_payload_mass = calculate_maximum_payload_mass( + dict_ac_exchange["maximum_structural_payload_mass"], + main_components_mass_properties["fuselage"]).print("Maximimum payload mass") + + # Design payload mass + design_payload_mass = calculate_design_payload_mass( + transport_task_data, main_components_mass_properties["fuselage"], maximum_payload_mass, runtime_output).print( + "Design payload mass") + + # Design fuel mass + design_fuel_mass_takeoff, design_fuel_mass_midflight, design_fuel_mass_minimum_landing, \ + design_fuel_mass_takeoff_per_tank, consumed_fuel, mission_fuel_mass, _ = \ + calculate_design_fuel_mass( + mission_information, maximum_takeoff_mass, operating_mass_empty, design_payload_mass, tanks, + routing_dict, dict_ac_exchange) + mission_fuel_mass.path_to_element = "./analysis/masses_cg_inertia/mission_fuel_mass" + mission_fuel_mass.print("Mission fuel mass") + + design_fuel_mass_takeoff.path_to_element = "./analysis/masses_cg_inertia/design_fuel_mass" + design_fuel_mass_takeoff.print("Design fuel mass (at takeoff)") + + # Maximum zero fuel mass + maximum_zero_fuel_mass = calculate_maximum_zero_fuel_mass( + [operating_mass_empty, maximum_payload_mass], inertia_method).print("Maximum zero fuel mass") + maximum_zero_fuel_mass.cg_mac = ( + maximum_zero_fuel_mass.center_of_gravity['x'] - x_leading_edge_mac) / mac * 100 + + # Design mass + design_mass = calculate_design_mass( + [operating_mass_empty, design_payload_mass, + mission_fuel_mass], inertia_method + ).print("Design mass (mission)") + + # Design mass takeoff + design_mass_takeoff = calculate_design_mass( + [operating_mass_empty, design_payload_mass, + design_fuel_mass_takeoff], inertia_method + ).print("Design mass takeoff") + design_mass_takeoff.path_to_element = "./analysis/masses_cg_inertia/design_mass" + design_mass_takeoff.cg_mac = ( + design_mass.center_of_gravity['x'] - x_leading_edge_mac) / mac * 100 + + # Maximum takeoff mass + maximum_takeoff_mass = copy.copy(design_mass_takeoff) + maximum_takeoff_mass.path_to_element = "./analysis/masses_cg_inertia/maximum_takeoff_mass" + maximum_takeoff_mass.cg_mac = ( + design_mass_takeoff.center_of_gravity['x'] - x_leading_edge_mac) / mac * 100 + maximum_takeoff_mass.print("Maximum Takeoff Mass") + + # Maximum fuel mass + maximum fuel mass per tank + maximum_fuel_mass, maximum_fuel_mass_per_tank, ferry_range_mass = calculate_maximum_fuel_mass( + tanks, operating_mass_empty, x_leading_edge_mac, mac, maximum_takeoff_mass, runtime_output) + maximum_fuel_mass.print("Maximum fuel mass") + ferry_range_mass.print("Ferry range mass") + + # Maximum payload + fuel up to MTOW + mtow_max_payload_mass = calculate_mtow_max_payload( + operating_mass_empty, maximum_payload_mass, main_components_mass_properties["wing"], + maximum_takeoff_mass, x_leading_edge_mac, mac) + + # Maximum fuel + payload up to MTOW + mtow_max_fuel_mass = calculate_mtow_max_fuel( + operating_mass_empty, maximum_fuel_mass_per_tank, + main_components_mass_properties["wing"], main_components_mass_properties["fuselage"], + maximum_takeoff_mass, x_leading_edge_mac, mac) + + # Midflight + design_mass_midflight = calculate_design_mass( + [operating_mass_empty, design_payload_mass, design_fuel_mass_midflight], + inertia_method).print("Design mass midflight") + + # Landing mass + if dict_mod_config["mode_maximum_landing_mass_calculation"] == "mode_0": + maximum_landing_mass = calculate_maximum_landing_mass_by_default_method( + mission_information, inertia_method, [ + operating_mass_empty, design_payload_mass, design_fuel_mass_minimum_landing], + maximum_landing_mass_method["additional_parameters"]).print("Maximum landing mass") + else: + runtime_output.print("Calculate MLM by RWTH regression ...") + maximum_landing_mass = calculate_maximum_landing_mass_by_regression_rwth( + maximum_takeoff_mass, inertia_method, main_components_mass_properties).print("Maximum landing mass") + maximum_landing_mass.path_to_element = "./analysis/masses_cg_inertia/maximum_landing_mass" + + ################################################################################################################## + ### Determine W&B diagram and CG curtailment ### + + runtime_output.print("== Calculate loading diagramm cg change ==") + + if refueling_mode == "mode_1": + runtime_output.print("REFUELING (ferry mission)...") + # Calculate the change in CG after refueling - ferry range mission case: + cg_positions_over_mac_refueling, total_mass_refueling = \ + calculate_cg_position_over_mac_refueling(operating_mass_empty.center_of_gravity, + operating_mass_empty.mass, maximum_fuel_mass_per_tank, + mac, x_leading_edge_mac) + for fuel in maximum_fuel_mass_per_tank: + runtime_output.print( + f"Tank '{fuel.tank_location}' was filled up to the maximum capacity with {fuel.mass:.2f} kg {fuel.energy_carrier}.") + runtime_output.print( + f" ...The flag, whether the tank should have been filled (true) or not (false), is {fuel.used_tank}." + ) + + elif refueling_mode == "mode_0": + runtime_output.print("REFUELING (design mission)...") + # Calculate the change in CG after refueling - design mission case: + cg_positions_over_mac_refueling, total_mass_refueling = \ + calculate_cg_position_over_mac_refueling(operating_mass_empty.center_of_gravity, + operating_mass_empty.mass, design_fuel_mass_takeoff_per_tank, + mac, x_leading_edge_mac) + for fuel in design_fuel_mass_takeoff_per_tank: + runtime_output.print( + f"Tank '{fuel.tank_location}' was filled up with {fuel.mass:.2f} kg {fuel.energy_carrier}.") + runtime_output.print( + f" ...Maximum fuel mass capacity of this tank is of {fuel.maximum_capacity:.2f} kg." + ) + runtime_output.print( + f" ...The flag, whether the tank should have been filled (true) or not (false), is {fuel.used_tank}." + ) + else: + runtime_output.critical("Invalid refueling mode specified") + raise ValueError("Invalid refueling mode specified") + + # Prepare passengers boarding + starting_mass = total_mass_refueling[-1] + starting_cg = cg_positions_over_mac_refueling[-1] * \ + mac / 100 + x_leading_edge_mac + x_coord_front_back = acommodation_data.seating_spacing_x_coordinate + passenger_front_back = acommodation_data.number_of_passengers_per_row + x_coord_back_front = x_coord_front_back[::-1] + passenger_back_front = passenger_front_back[::-1] + + runtime_output.print("PASSENGERS BOARDING...") + + if passengers_boarding_mode == "mode_0": + # Calculate the change in CG after the boarding of the passengers from the front to the back - row-wise case: + runtime_output.print("ROW-WISE FRONT TO BACK") + cg_positions_over_mac_passengers_front_back, total_mass_passengers_front_back = \ + cg_change_passengers_boarding_front_back( + starting_cg, starting_mass, x_coord_front_back, passenger_front_back, + acommodation_data, mac, x_leading_edge_mac) + + # Calculate the change in CG after the boarding of the passengers from the back to the front - row-wise case: + runtime_output.print("ROW-WISE BACK TO FRONT") + cg_positions_over_mac_passengers_back_front, total_mass_passengers_back_front = \ + cg_change_passengers_boarding_front_back( + starting_cg, starting_mass, x_coord_back_front, passenger_back_front, + acommodation_data, mac, x_leading_edge_mac) + + DEBUG(msg="If the data is to be read from excel and not from acommodationIO modify/adapt this") + # cg_positions_over_mac_passengers_front_back, total_mass_passengers_front_back = \ + # cg_change_passengers_boarding_front_back_excel(starting_cg, starting_mass, airplane_seatings, + # acommodation_data, mac, x_leading_edge_mac) + + elif passengers_boarding_mode == "mode_1": + runtime_output.print("WINDOW AISLE FRONT TO BACK") + cg_positions_over_mac_passengers_front_back, total_mass_passengers_front_back = \ + cg_change_passengers_boarding_window_aisle( + starting_cg, starting_mass, x_coord_front_back, passenger_front_back, + acommodation_data, mac, x_leading_edge_mac) + + runtime_output.print("WINDOW AISLE BACK TO FRONT") + cg_positions_over_mac_passengers_back_front, total_mass_passengers_back_front = \ + cg_change_passengers_boarding_window_aisle( + starting_cg, starting_mass, x_coord_back_front, passenger_back_front, + acommodation_data, mac, x_leading_edge_mac) + else: + runtime_output.critical("Invalid passengers boarding mode specified") + raise ValueError("Invalid passengers boarding mode specified") + + # Prepare cargo + starting_mass = total_mass_passengers_front_back[-1] + starting_cg = cg_positions_over_mac_passengers_front_back[-1] * \ + mac / 100 + x_leading_edge_mac + x_coord_front_back_cargo = acommodation_data.cargo_spacing_x_coordinate + x_coord_back_front_cargo = x_coord_front_back_cargo[::-1] + + runtime_output.print("CARGO...") + DEBUG(msg="The palets positions unknown. The cargo is loaded at the same locations as the seats positions.") + + cg_positions_over_mac_cargo_front_back, total_mass_cargo_front_back = \ + cg_change_cargo_loading_front_back( + starting_cg, starting_mass, x_coord_front_back_cargo, transport_task_data, mac, x_leading_edge_mac) + + cg_positions_over_mac_cargo_back_front, total_mass_cargo_back_front = \ + cg_change_cargo_loading_front_back( + starting_cg, starting_mass, x_coord_back_front_cargo, transport_task_data, mac, x_leading_edge_mac) + + # Prepare defueling + defueling_mass_per_tank = calculate_fuel_properties_defueling( + sorted_tanks_defueling, consumed_fuel, design_fuel_mass_takeoff_per_tank, + runtime_output, routing_dict, dict_ac_exchange) + + defueling_mass_per_tank_ferry = calculate_fuel_properties_defueling( + sorted_tanks_defueling, consumed_fuel, maximum_fuel_mass_per_tank, + runtime_output, routing_dict, dict_ac_exchange) + + starting_mass = total_mass_cargo_back_front[-1] + starting_cg = cg_positions_over_mac_cargo_front_back[-1] * \ + mac / 100 + x_leading_edge_mac + + runtime_output.print("DEFUELING...") + + if refueling_mode == "mode_1": + # Calculate the change in CG after defueling - ferry range mission case: + cg_positions_over_mac_defueling, total_mass_defueling = calculate_cg_position_over_mac_defueling( + starting_cg, starting_mass, defueling_mass_per_tank_ferry, mac, x_leading_edge_mac) + DEBUG(msg="Consumed fuel for ferry mission = consumed fuel for design mission.") + + for fuel in defueling_mass_per_tank_ferry: + runtime_output.print( + f"From tank '{fuel.name}' was defueld {-fuel.mass:.2f} kg. Fuel left in tank: {fuel.fuel_left_in_tank:.2f} kg.") + + elif refueling_mode == "mode_0": + + # Calculate the change in CG after refueling - design mission case: + cg_positions_over_mac_defueling, total_mass_defueling = calculate_cg_position_over_mac_defueling( + starting_cg, starting_mass, defueling_mass_per_tank, mac, x_leading_edge_mac) + + for fuel in defueling_mass_per_tank: + runtime_output.print( + f"From tank '{fuel.name}' was defueld {-fuel.mass:.2f} kg. Fuel left in tank: {fuel.fuel_left_in_tank:.2f} kg.") + else: + runtime_output.critical("Invalid refueling mode specified") + raise ValueError("Invalid refueling mode specified") + + # Determine the CG curtailment: + + most_fwd_CG_over_mac = min([min(cg_positions_over_mac_refueling), min(cg_positions_over_mac_passengers_front_back), + min(cg_positions_over_mac_cargo_front_back), min(cg_positions_over_mac_defueling)]) + most_aft_CG_over_mac = max([max(cg_positions_over_mac_refueling), max(cg_positions_over_mac_passengers_back_front), + max(cg_positions_over_mac_cargo_back_front), max(cg_positions_over_mac_defueling)]) + + most_fwd_CG = most_fwd_CG_over_mac * mac / 100 + x_leading_edge_mac + most_aft_CG = most_aft_CG_over_mac * mac / 100 + x_leading_edge_mac + + runtime_output.print( + f"Most forward CG global position is {most_fwd_CG:.2f} m.") + runtime_output.print( + f"Most aft CG global position is {most_aft_CG:.2f} m.") + + # Determine the corresponding mass to the most fwd CG + if most_fwd_CG_over_mac in cg_positions_over_mac_refueling: + corresponding_index = cg_positions_over_mac_refueling.index( + most_fwd_CG_over_mac) + corresponding_mass_most_fwd_CG = total_mass_refueling[corresponding_index] + elif most_fwd_CG_over_mac in cg_positions_over_mac_passengers_front_back: + corresponding_index = cg_positions_over_mac_passengers_front_back.index( + most_fwd_CG_over_mac) + corresponding_mass_most_fwd_CG = total_mass_passengers_front_back[corresponding_index] + elif most_fwd_CG_over_mac in cg_positions_over_mac_cargo_front_back: + corresponding_index = cg_positions_over_mac_cargo_front_back.index( + most_fwd_CG_over_mac) + corresponding_mass_most_fwd_CG = total_mass_cargo_front_back[corresponding_index] + else: + corresponding_index = cg_positions_over_mac_defueling.index( + most_fwd_CG_over_mac) + corresponding_mass_most_fwd_CG = total_mass_defueling[corresponding_index] + + runtime_output.print( + f"Corresponding mass most fwd CG is of {corresponding_mass_most_fwd_CG:.2f} kg.") + + # Determine the corresponding mass to the most aft CG + if most_aft_CG_over_mac in cg_positions_over_mac_refueling: + corresponding_index = cg_positions_over_mac_refueling.index( + most_aft_CG_over_mac) + corresponding_mass_most_aft_CG = total_mass_refueling[corresponding_index] + elif most_aft_CG_over_mac in cg_positions_over_mac_passengers_back_front: + corresponding_index = cg_positions_over_mac_passengers_back_front.index( + most_aft_CG_over_mac) + corresponding_mass_most_aft_CG = total_mass_passengers_back_front[corresponding_index] + elif most_aft_CG_over_mac in cg_positions_over_mac_cargo_back_front: + corresponding_index = cg_positions_over_mac_cargo_back_front.index( + most_aft_CG_over_mac) + corresponding_mass_most_aft_CG = total_mass_cargo_back_front[corresponding_index] + else: + corresponding_index = cg_positions_over_mac_defueling.index( + most_aft_CG_over_mac) + corresponding_mass_most_aft_CG = total_mass_defueling[corresponding_index] + + runtime_output.print( + f"Corresponding mass most aft CG is of {corresponding_mass_most_aft_CG:.2f} kg.") + + # Prepare the data to be written as nodes for axml + most_forward_mass = MassPropertiesIO( + "./analysis/masses_cg_inertia/most_forward_mass") + most_forward_mass.center_of_gravity["x"] = most_fwd_CG + most_forward_mass.center_of_gravity["y"] = 0.0 + most_forward_mass.center_of_gravity["z"] = 0.0 + most_forward_mass.mass = corresponding_mass_most_fwd_CG + most_forward_mass.inertia = inertia_method["method"]( + [operating_mass_empty, MassPropertiesIO().initialize_zero(), + MassPropertiesIO().initialize_zero()], + inertia_method["additional_parameters"]) + + most_afterward_mass = MassPropertiesIO( + "./analysis/masses_cg_inertia/most_afterward_mass") + most_afterward_mass.center_of_gravity["x"] = most_aft_CG + most_afterward_mass.center_of_gravity["y"] = 0.0 + most_afterward_mass.center_of_gravity["z"] = 0.0 + most_afterward_mass.mass = corresponding_mass_most_aft_CG + most_afterward_mass.inertia = inertia_method["method"]( + [operating_mass_empty, MassPropertiesIO().initialize_zero(), + MassPropertiesIO().initialize_zero()], + inertia_method["additional_parameters"]) + + ########################################################################################################### + # Prepare output # + + # Define Group masses + group_mass_structure = main_components_mass_properties["wing"].mass + \ + main_components_mass_properties["fuselage"].mass - fuselage_operator_items_mass - furnishings_mass + \ + main_components_mass_properties["empennage"].mass + \ + main_components_mass_properties["landing_gear"].mass +\ + sum([pylon.mass for pylon in pylons if pylon is not None]) + group_mass_power_unit = sum([sum([nacelle.mass for nacelle in nacelles if nacelle is not None]), sum( + [engine.mass for engine in engines if engine is not None])]) + group_mass_systems = main_components_mass_properties["systems"].mass - \ + systems_operator_items_mass + group_mass_furnishings = furnishings_mass + group_mass_operator_items = sum(item.mass for item in operator_items) + group_masses = { + "Structure": group_mass_structure, + "Propulsion": group_mass_power_unit, + "Systems": group_mass_systems, + "Furnishings": group_mass_furnishings, + "Operator Items": group_mass_operator_items + } + structure_masses = { + "Wing": main_components_mass_properties["wing"].mass, + "Fuselage": main_components_mass_properties["fuselage"].mass - fuselage_operator_items_mass - furnishings_mass, + "Empennage": main_components_mass_properties["empennage"].mass, + "Landing Gear": main_components_mass_properties["landing_gear"].mass, + "Pylons": sum([pylon.mass for pylon in pylons if pylon is not None]) + } + + basic_output_dict = { + "structure_components": structure_masses, + "operating_mass_empty": operating_mass_empty, + "manufacturer_mass_empty": manufacturer_mass_empty, + "maximum_takeoff_mass": maximum_takeoff_mass, + "maximum_payload_mass": maximum_payload_mass, + "maximum_fuel_mass": maximum_fuel_mass, + "ferry_range_mass": ferry_range_mass, + "maximum_zero_fuel_mass": maximum_zero_fuel_mass, + "maximum_landing_mass": maximum_landing_mass, + "mtow_with_max_payload": mtow_max_payload_mass, + "mtow_with_max_fuel": mtow_max_fuel_mass, + "design_mass": design_mass, + "design_mass_takeoff": design_mass_takeoff, + "design_payload_mass": design_payload_mass, + "design_mass_midflight": design_mass_midflight, + "mission_fuel_mass": mission_fuel_mass, + "design_fuel_mass": design_fuel_mass_takeoff, + "design_fuel_takeoff": design_fuel_mass_takeoff, + "design_fuel_mass_midflight": design_fuel_mass_midflight, + "design_fuel_mass_minimum_landing": design_fuel_mass_minimum_landing, + "group_masses": group_masses, + "Refueling CG change": cg_positions_over_mac_refueling, + "Refueling mass change": total_mass_refueling, + "Defueling CG change": cg_positions_over_mac_defueling, + "Defueling mass change": total_mass_defueling, + "Defueling mode": defueling_mode, + "Passengers boarding front back CG change": cg_positions_over_mac_passengers_front_back, + "Passengers boarding front back mass change": total_mass_passengers_front_back, + "Passengers boarding back front CG change": cg_positions_over_mac_passengers_back_front, + "Passengers boarding back front mass change": total_mass_passengers_back_front, + "Cargo loading front back CG change": cg_positions_over_mac_cargo_front_back, + "Cargo loading front back mass change": total_mass_cargo_front_back, + "Cargo loading back front CG change": cg_positions_over_mac_cargo_back_front, + "Cargo loading back front mass change": total_mass_cargo_back_front, + "Neutral point mac position": neutral_point_mac, + "most_forward_mass": most_forward_mass, + "most_afterward_mass": most_afterward_mass, + "Most forward CG mac position": most_fwd_CG_over_mac, + "Most aft CG mac position": most_aft_CG_over_mac, + "Furnishings": furnishings, + "Operator_items": operator_items, + "Nacelles": nacelles, + "Engines": engines, + "Systems": systems + } + + # Check if the given tool_level from aircrat exchange file is None -> if true: -> initialize with 0. + if dict_ac_exchange['tool_level'] is None: + basic_output_dict['current_tool_level'] = 0 + # Else if condition: Check if given tool_level from aircrat exchange is lower than the given own tool level + # from module configuration file -> if true: Add 1 to current tool level from airaft exchange file. + elif dict_ac_exchange['tool_level'] < int(routing_dict['tool_level']): + basic_output_dict['current_tool_level'] = dict_ac_exchange['tool_level'] + 1 + # Else condition: The given tool_level from aircrat exchange is equal to the given own tool level + else: + basic_output_dict['current_tool_level'] = int( + routing_dict['tool_level']) + + return basic_output_dict + + +def calculate_inertia_by_components(mass_properties=[], additional_parameters=None): + """Calculate inertia by using component inertia and steiner + + Args: + mass_properties (list, optional): List of mass_properties to compute inertia. Defaults to []. + additional_parameters (_type_, optional): No additional parameters needed. Defaults to None. + + Returns: + dict: inertia + """ + reference_center_of_gravity = calculate_center_of_gravity(mass_properties) + inertia = { + "j_xx": 0.0, + "j_yy": 0.0, + "j_zz": 0.0, + "j_xy": 0.0, + "j_xz": 0.0, + "j_yx": 0.0, + "j_yz": 0.0, + "j_zx": 0.0, + "j_zy": 0.0 + } + + for component, value in inertia.items(): + for element in mass_properties: + inertia[component] += element.inertia[component] + element.mass * calculate_geometric_distance_by_axis( + component, reference_center_of_gravity, element.center_of_gravity) + + return inertia + + +def calculate_geometric_distance_by_axis(axis, reference_axis, current_axis): + """Calculation of the geometric distance for inertia components + + Args: + axis (str): Inertia axis string + reference_axis (dict): center_of_gravity to apply inertia + current_axis (dict): center_of_gravity of current component + """ + def main_axis(p, q): + return (p**2 + q**2) + + def deviation_axis(p, q): + return -(p * q) + + to_get = { + "j_xx": {"first": "y", "second": "z", "calculation": main_axis}, + "j_yy": {"first": "x", "second": "z", "calculation": main_axis}, + "j_zz": {"first": "x", "second": "y", "calculation": main_axis}, + "j_xy": {"first": "x", "second": "y", "calculation": deviation_axis}, + "j_xz": {"first": "x", "second": "z", "calculation": deviation_axis}, + "j_yx": {"first": "y", "second": "x", "calculation": deviation_axis}, + "j_yz": {"first": "y", "second": "z", "calculation": deviation_axis}, + "j_zx": {"first": "z", "second": "x", "calculation": deviation_axis}, + "j_zy": {"first": "z", "second": "y", "calculation": deviation_axis} + } + + # Call calculation method and axis - not shortest possibility to calculate but easy to read + selected = to_get[axis] + first = selected["first"] + second = selected["second"] + calculation = selected["calculation"] + + # Calculate delta values between reference and current axis for first and second component + delta_ref_curr_first = reference_axis[first] - current_axis[first] + delta_ref_curr_second = reference_axis[second] - current_axis[second] + + # Calculate inertia distance for axis + return calculation(delta_ref_curr_first, delta_ref_curr_second) + + +def calculate_inertia_by_lth_method(mass_properties=[], additional_parameters=[]): + """Calculate inertia by 'luftfahrttechnisches Handbuch (LTH)' (Empirical table) + Only valid for tube and wing aircrafts + + Args: + mass_operation_empty (MassPropertiesIO Object list): + operation mass empty, payload mass expected, fuel mass expected + additional_parameters (list): wing span, fuselage length + + Returns: + dict: inertia + """ + mass_operation_empty = mass_properties[0] + mass_payload_expected = mass_properties[1] + mass_fuel_expected = mass_properties[2] + + wing_span = additional_parameters[0] + fuselage_length = additional_parameters[1] + + expected_mass = mass_operation_empty.mass + \ + mass_fuel_expected.mass + mass_payload_expected.mass + expected_mass_over_ome = expected_mass / mass_operation_empty.mass + expected_fuel_over_ome = mass_fuel_expected.mass / mass_operation_empty.mass + + # Transferred table chart page 2 of 4 from LTH - MA 402 42-01 Ausgabe A to formula below + factor_kx = (1.0 / 12.0 * ((-2.0 / 3.0 * (expected_mass_over_ome - + 1.0) + expected_fuel_over_ome) + 1.0) + 0.065) + + # Transferred table chart page 3 of 4 from LTH - MA 402 42-01 Ausgabe A to formula below + factor_ky = (-0.065 / 1.72 * (0.2 * (expected_mass_over_ome - + 1.0) + expected_fuel_over_ome) + 0.2025) + + factor_tech_j_xx = 0.8 # self estimated tech factor for Jxx + factor_tech_j_yy = 0.9 # self estimated tech factor for Jyy + + j_xx = factor_tech_j_xx * factor_kx**2 * wing_span**2 * expected_mass + j_yy = factor_tech_j_yy * factor_ky**2 * fuselage_length**2 * expected_mass + j_zz = 0.96 * (j_xx + j_yy) + inertia = { + "j_xx": j_xx, + "j_yy": j_yy, + "j_zz": j_zz, + "j_xy": 0.0, + "j_xz": 0.0, + "j_yx": 0.0, + "j_yz": 0.0, + "j_zx": 0.0, + "j_zy": 0.0 + } + return inertia + + +def calculate_inertia_by_Raymer(mass_properties=[], additional_parameters=[]): + """Calculate inertia by 'Raymer p.623, table 16.1' (Empirical table) + + + Args: + mass_operation_empty (MassPropertiesIO Object list): + operation mass empty, payload mass expected, fuel mass expected + additional_parameters (list): wing span, fuselage length, aircraft_type + + Returns: + dict: inertia + """ + mass_operation_empty = mass_properties[0] + mass_payload_expected = mass_properties[1] + mass_fuel_expected = mass_properties[2] + + wing_span = additional_parameters[0] + fuselage_length = additional_parameters[1] + aircraft_class = additional_parameters[2] + + expected_mass = mass_operation_empty.mass + \ + mass_fuel_expected.mass + mass_payload_expected.mass + + # Dictionary of nondimensional radii of gyration for different aircraft classes + radii_of_gyration = { + # "Single-engine prop": {"R_x": 0.25, "R_y": 0.38, "R_z": 0.39}, + # "Twin-engine prop": {"R_x": 0.30, "R_y": 0.40, "R_z": 0.44}, + # "Business jet twin": {"R_x": 0.30, "R_y": 0.34, "R_z": 0.39}, + # "Twin turboprop transport": {"R_x": 0.22, "R_y": 0.33, "R_z": 0.38}, + "jet_fuselage_eng": {"R_x": 0.24, "R_y": 0.34, "R_z": 0.42}, + "jet_two_wing_eng": {"R_x": 0.23, "R_y": 0.33, "R_z": 0.45}, + "jet_four_wing_eng": {"R_x": 0.24, "R_y": 0.36, "R_z": 0.44}, + "blended_wing": {"R_x": 0.28, "R_y": 0.40, "R_z": 0.46}, + } + + # Retrieve the nondimensional radii of gyration for the specified aircraft class + radii = radii_of_gyration[aircraft_class] + R_x = radii["R_x"] + R_y = radii["R_y"] + R_z = radii["R_z"] + + tech_fact_yy = 1.25 + tech_fact_xx = 1.15 + + # Calculate mass from weight + mass = expected_mass + + # Roll moment of inertia (I_xx) + I_xx = (wing_span**2 * mass * R_x**2) / 4 * tech_fact_xx + + # Pitch moment of inertia (I_yy) + I_yy = (fuselage_length**2 * mass * R_y**2) / 4 * tech_fact_yy + + # Yaw moment of inertia (I_zz) + I_zz = (((wing_span + fuselage_length) / 2)**2 * mass * R_z**2) / 4 + + inertia = { + "j_xx": I_xx, + "j_yy": I_yy, + "j_zz": I_zz, + "j_xy": 0.0, + "j_xz": 0.0, + "j_yx": 0.0, + "j_yz": 0.0, + "j_zx": 0.0, + "j_zy": 0.0 + } + return inertia + + +def calculate_center_of_gravity(mass_properties): + import logging + runtime_output = logging.getLogger('module_logger') + + center_of_gravity = {"x": None, + "y": None, + "z": None} + components = [] + if isinstance(mass_properties, dict): + components = list(mass_properties.values()) + elif isinstance(mass_properties, list): + components = mass_properties + else: + runtime_output.critical( + "Wrong type of mass_properties (no dict or list)... abort program!") + raise ValueError( + "Wrong type of mass_properties (no dict or list)... abort program!") + + for ax in center_of_gravity: + overall_mass = 0.0 + scaled_mass = 0.0 + for component in components: + overall_mass += component.mass + scaled_mass += component.mass * \ + component.center_of_gravity[ax] + center_of_gravity[ax] = scaled_mass / overall_mass + + return center_of_gravity + + +def calculate_manufacturer_mass_empty(operator_items, mass_properties, inertia_method): + + manufacturer_mass_empty = MassPropertiesIO( + "./analysis/masses_cg_inertia/manufacturer_mass_empty") + + # op items are calculated by both fuselage and systems, here they are combined + cg_items = calculate_center_of_gravity(operator_items) + mass_op_items = sum(item.mass for item in operator_items) + + # mass properties contains the total mass of components (ex. fuselage = structure + op items + furnishings) + mass_tot = sum( + [mass_properties[element].mass for element in mass_properties]) # = OME + cg_tot = calculate_center_of_gravity(mass_properties) # = CG OME + + # removing the operator items to determine MME + manufacturer_mass_empty.mass = mass_tot - mass_op_items + + manufacturer_mass_empty.center_of_gravity['x'] = ( + mass_tot * cg_tot['x'] - mass_op_items * cg_items['x']) / (mass_tot - mass_op_items) + manufacturer_mass_empty.center_of_gravity['y'] = ( + mass_tot * cg_tot['y'] - mass_op_items * cg_items['y']) / (mass_tot - mass_op_items) + manufacturer_mass_empty.center_of_gravity['z'] = ( + mass_tot * cg_tot['z'] - mass_op_items * cg_items['z']) / (mass_tot - mass_op_items) + + manufacturer_mass_empty.inertia = inertia_method["method"]([manufacturer_mass_empty, MassPropertiesIO( + ).initialize_zero(), MassPropertiesIO().initialize_zero()], inertia_method["additional_parameters"]) + + return manufacturer_mass_empty + + +def calculate_operating_mass_empty(mass_properties, inertia_method, LE, mac): + """Calculate opertaing mass empty + + Args: + mass_properties (MassPropertiesIO Objects): list of mass_properties objects + cg_method (function): center of gravity method + inertia_method (function): inertia method + """ + operating_mass_empty = MassPropertiesIO( + "./analysis/masses_cg_inertia/operating_mass_empty") + + operating_mass_empty.mass = sum( + [mass_properties[element].mass for element in mass_properties]) + # operator_items_mass already considered in the total mass of components + # not needed extra, already considered under systems and fuselage + + operating_mass_empty.center_of_gravity = calculate_center_of_gravity( + mass_properties) + operating_mass_empty.mass += 0.1 # ToDo avoid division by zero + operating_mass_empty.inertia = inertia_method["method"]([operating_mass_empty, MassPropertiesIO( + ).initialize_zero(), MassPropertiesIO().initialize_zero()], inertia_method["additional_parameters"]) + operating_mass_empty.cg_mac = ( + operating_mass_empty.center_of_gravity['x'] - LE) / mac * 100 + return operating_mass_empty + + +def calculate_maximum_landing_mass_by_default_method( + mission_information=None, inertia_method=None, mass_properties=None, additional_parameters=[]): + """Calculate maximum landing mass by default method - OME - Design Payload - Reserve fuel + + Args: + operating_mass_empty (float, optional): operating mass empty. Defaults to None. + transport_task_data (object, optional): tranport task data. Defaults to None. + mission_information (object, optional): mission information. Defaults to None. + + Returns: + float: maximum landing mass + """ + + maximum_landing_mass = MassPropertiesIO( + "./analysis/masses_cg_inertia/maximum_landing_mass/mass_properties") + + design_fuel_mass_trip = mission_information["trip_fuel_mass"] + design_fuel_mass_mission = mission_information["mission_fuel_mass"] + + # If mission information not existent - work with current landing mass + if design_fuel_mass_trip is None or design_fuel_mass_mission is None: + maximum_landing_mass.read(additional_parameters[0]) + maximum_landing_mass.center_of_gravity = calculate_center_of_gravity( + mass_properties) + maximum_landing_mass.inertia = inertia_method["method"]( + [maximum_landing_mass, MassPropertiesIO().initialize_zero(), + MassPropertiesIO().initialize_zero()], + inertia_method["additional_parameters"]) + return maximum_landing_mass + + if mission_information is not None: + maximum_landing_mass.mass = sum( + [element.mass for element in mass_properties]) + maximum_landing_mass.center_of_gravity = calculate_center_of_gravity( + mass_properties) + maximum_landing_mass.inertia = inertia_method["method"]([maximum_landing_mass, MassPropertiesIO( + ).initialize_zero(), MassPropertiesIO().initialize_zero()], inertia_method["additional_parameters"]) + return maximum_landing_mass + + +def calculate_maximum_landing_mass_by_regression_rwth(maximum_takeoff_mass, inertia_method, mass_properties): + """Calculate maximum landing mass by RWTH regression formular + + Args: + maximum_takeoff_mass (float): maximum takeoff mass value + + Returns: + float: maximum landing mass + """ + maximum_landing_mass = MassPropertiesIO( + "./analysis/masses_cg_inertia/maximum_landing_mass/mass_properties") + if maximum_takeoff_mass.mass > 15000.0: + maximum_landing_mass.mass = 1.9689 * maximum_takeoff_mass.mass**0.9248 + else: + maximum_landing_mass.mass = 0.9009 * maximum_takeoff_mass.mass + 410.85 + maximum_landing_mass.center_of_gravity = calculate_center_of_gravity( + mass_properties) + maximum_landing_mass.inertia = inertia_method["method"]([maximum_landing_mass, MassPropertiesIO( + ).initialize_zero(), MassPropertiesIO().initialize_zero()], inertia_method["additional_parameters"]) + + return maximum_landing_mass + + +def calculate_maximum_fuel_mass(tanks, ome, LE, mac, mtom, runtime_output): + """Calculation of maximum fuel mass + + Args: + tanks (float): list of tanks + fuel_density (float): fuel density per tank + + Returns: + _type_: _description_ + """ + max_fuel_mass_per_tank = [] + mass_prop = [] + max_fuel_mass = MassPropertiesIO( + "./analysis/masses_cg_inertia/maximum_fuel_mass") + + # the order to be filled is already sorted in the tanks + for empty_tank in tanks: + filled_tank_wo_structure = MassPropertiesIO().initialize_zero() + filled_tank_wo_structure.center_of_gravity = empty_tank.mass_properties.center_of_gravity + filled_tank_wo_structure.mass = empty_tank.energy / empty_tank.gravimetric_density + filled_tank_wo_structure.tank_location = empty_tank.tank_location + filled_tank_wo_structure.energy_carrier = empty_tank.energy_carrier + filled_tank_wo_structure.used_tank = empty_tank.used_tank + # No adaption of Center of gravity or Inertia here + + filled_tank_wo_structure.moment_change = ( + filled_tank_wo_structure.mass * filled_tank_wo_structure.center_of_gravity["x"]) + + max_fuel_mass_per_tank.append(filled_tank_wo_structure) + mass_prop.append(filled_tank_wo_structure) + + max_fuel_mass.mass = sum( + [filled_tank.mass for filled_tank in max_fuel_mass_per_tank]) + if max_fuel_mass.mass < 0.01: + max_fuel_mass.mass = 0.1 + max_fuel_mass.center_of_gravity = calculate_center_of_gravity(mass_prop) + max_fuel_mass.initialize_zero_inertia() + + ferry_range_mass = MassPropertiesIO( + "./analysis/masses_cg_inertia/ferry_range_mass") + mass_prop.append(ome) + + ferry_range_mass.mass = max_fuel_mass.mass + ome.mass + if ferry_range_mass.mass < 0.01: + ferry_range_mass.mass = 0.1 + ferry_range_mass.center_of_gravity = calculate_center_of_gravity(mass_prop) + ferry_range_mass.initialize_zero_inertia() + ferry_range_mass.cg_mac = ( + max_fuel_mass.center_of_gravity['x'] - LE) / mac * 100 + + if ferry_range_mass.mass >= mtom.mass: + runtime_output.warning( + "Attention: OEW + max_fuel_mass is higher than MTOW.") + + return max_fuel_mass, max_fuel_mass_per_tank, ferry_range_mass + + +def calculate_cg_position_over_mac_refueling(initial_cg, initial_weight, fuel_mass_per_tank, mac, LE): + + cg_positions = [] + total_weight = [] + + total_weight.append(initial_weight) + total_weight_new = initial_weight + + total_moment_change = initial_cg["x"] * initial_weight + + initial_mac_position = (initial_cg["x"] - LE) / mac * 100.0 + cg_positions.append(initial_mac_position) + + for filled_tank in fuel_mass_per_tank: + + total_weight_new += filled_tank.mass + total_moment_change += filled_tank.moment_change + + new_cg = total_moment_change / total_weight_new + new_mac_position = (new_cg - LE) / mac * 100.0 + + cg_positions.append(new_mac_position) + total_weight.append(total_weight_new) + + return cg_positions, total_weight + + +def calculate_cg_position_over_mac_defueling(initial_cg, initial_weight, fuel_mass_per_tank, mac, LE): + + cg_positions = [] + total_weight = [] + + total_weight.append(initial_weight) + total_weight_new = initial_weight + + total_moment_change = initial_cg * initial_weight + + initial_mac_position = (initial_cg - LE) / mac * 100.0 + cg_positions.append(initial_mac_position) + + for filled_tank in fuel_mass_per_tank: + + total_weight_new += filled_tank.mass + total_moment_change += filled_tank.moment_change + + new_cg = total_moment_change / total_weight_new + new_mac_position = (new_cg - LE) / mac * 100.0 + + cg_positions.append(new_mac_position) + total_weight.append(total_weight_new) + + return cg_positions, total_weight + + +def calculate_fuel_mass_properties_evenly_distributed(tanks, fuel_mass, runtime_output): + + # Divide fuel mass evenly on tanks - might be adaptable to inner and outer tanks + overall_fuel_mass_per_tank = fuel_mass / len(tanks) + fuel_mass_properties = MassPropertiesIO().initialize_zero() + fuel_mass_per_tank = [] + for empty_tank in tanks: + filled_tank_wo_structure = MassPropertiesIO().initialize_zero() + filled_tank_wo_structure.center_of_gravity = empty_tank.mass_properties.center_of_gravity + + if overall_fuel_mass_per_tank <= empty_tank.energy / empty_tank.gravimetric_density: + # if overall_fuel_mass_per_tank <= empty_tank.volume["value"] * empty_tank.fuel_density: + filled_tank_wo_structure.mass = overall_fuel_mass_per_tank + else: + runtime_output.critical( + "Fuel mass calculation failed ... tank to small") + raise ValueError("Fuel mass calculation failed ... tank to small") + # No adaption of Center of gravity or Inertia here + + filled_tank_wo_structure.moment_change = (filled_tank_wo_structure.mass * + filled_tank_wo_structure.center_of_gravity["x"]) + + fuel_mass_per_tank.append(filled_tank_wo_structure) + + fuel_mass_properties.mass = sum( + [filled_tank.mass for filled_tank in fuel_mass_per_tank]) + fuel_mass_properties.center_of_gravity = calculate_center_of_gravity( + fuel_mass_per_tank) + fuel_mass_properties.initialize_zero_inertia() + + return fuel_mass_properties, fuel_mass_per_tank + + +def calculate_fuel_mass_properties(tanks, fuel_mass, routing_dict, dict_ac_exchange): + """ + Refuel tanks in the following priority order: + 1. Left and Right wing tanks (equally). These are also sorted based on the order defined before according + to the wing mounting + 2. Center tank. + 3. Remaining tanks. + + Parameters: + tanks (list): List of tank objects with properties like capacity, location, and mass. + fuel_mass (float): Total fuel mass to be distributed among the tanks. + + Returns: + list: Fuel distribution across the tanks. + + Raises: + ValueError: If the fuel mass exceeds the total tank capacity. + """ + import logging + runtime_output = logging.getLogger('module_logger') + + # Helper function to categorize tanks based on location string + def categorize_tank_location(tank): + location = tank.tank_location.split() + if "left" in location: + return "left" + elif "right" in location: + return "right" + elif "center" in location: + return "center" + else: + return "other" + + # Sort tanks into categories based on location + left_wing_tanks = [ + tank for tank in tanks if categorize_tank_location(tank) == "left"] + right_wing_tanks = [ + tank for tank in tanks if categorize_tank_location(tank) == "right"] + center_tanks = [ + tank for tank in tanks if categorize_tank_location(tank) == "center"] + other_tanks = [ + tank for tank in tanks if categorize_tank_location(tank) == "other"] + + # Calculate the total available capacity + total_capacity = sum( + tank.energy / tank.gravimetric_density for tank in tanks) + + if fuel_mass > total_capacity: + runtime_output.critical( + "Fuel mass to be filled up exceeds the total capacity of all tanks!") + + # Distribute fuel across left and right wing tanks first + remaining_fuel = fuel_mass + fuel_mass_per_tank = [] + + # Step 1: Fill the wing tanks (equally, if present) + if left_wing_tanks and right_wing_tanks: + for left_tank, right_tank in zip(left_wing_tanks, right_wing_tanks): + left_capacity = left_tank.energy / left_tank.gravimetric_density + right_capacity = right_tank.energy / right_tank.gravimetric_density + + filled_tank_wo_structure_left = MassPropertiesIO().initialize_zero() + filled_tank_wo_structure_right = MassPropertiesIO().initialize_zero() + + # Amount to fill equally + fuel_to_fill = min( + left_capacity, right_capacity, remaining_fuel / 2) + + # Fill both left and right tanks + if fuel_to_fill > 0: + remaining_fuel -= 2 * fuel_to_fill + + filled_tank_wo_structure_left.mass = fuel_to_fill + filled_tank_wo_structure_left.center_of_gravity = left_tank.mass_properties.center_of_gravity + filled_tank_wo_structure_left.moment_change = ( + fuel_to_fill * filled_tank_wo_structure_left.center_of_gravity["x"]) + filled_tank_wo_structure_left.tank_location = left_tank.tank_location + filled_tank_wo_structure_left.energy_carrier = right_tank.energy_carrier + filled_tank_wo_structure_left.used_tank = left_tank.used_tank + filled_tank_wo_structure_left.maximum_capacity = left_tank.max_fuel_mass_capacity + + filled_tank_wo_structure_right.mass = fuel_to_fill + filled_tank_wo_structure_right.center_of_gravity = right_tank.mass_properties.center_of_gravity + filled_tank_wo_structure_right.moment_change = ( + fuel_to_fill * filled_tank_wo_structure_right.center_of_gravity["x"]) + filled_tank_wo_structure_right.tank_location = right_tank.tank_location + filled_tank_wo_structure_right.energy_carrier = right_tank.energy_carrier + filled_tank_wo_structure_right.used_tank = right_tank.used_tank + filled_tank_wo_structure_right.maximum_capacity = right_tank.max_fuel_mass_capacity + + fuel_mass_per_tank.append(filled_tank_wo_structure_left) + fuel_mass_per_tank.append(filled_tank_wo_structure_right) + else: + filled_tank_wo_structure_left.mass = 0 + filled_tank_wo_structure_left.center_of_gravity = left_tank.mass_properties.center_of_gravity + filled_tank_wo_structure_left.moment_change = 0 + filled_tank_wo_structure_left.tank_location = left_tank.tank_location + filled_tank_wo_structure_left.energy_carrier = left_tank.energy_carrier + filled_tank_wo_structure_left.used_tank = left_tank.used_tank + filled_tank_wo_structure_left.maximum_capacity = left_tank.max_fuel_mass_capacity + + filled_tank_wo_structure_right.mass = 0 + filled_tank_wo_structure_right.center_of_gravity = right_tank.mass_properties.center_of_gravity + filled_tank_wo_structure_right.moment_change = 0 + filled_tank_wo_structure_right.tank_location = right_tank.tank_location + filled_tank_wo_structure_right.energy_carrier = right_tank.energy_carrier + filled_tank_wo_structure_right.used_tank = right_tank.used_tank + filled_tank_wo_structure_right.maximum_capacity = right_tank.max_fuel_mass_capacity + + fuel_mass_per_tank.append(filled_tank_wo_structure_left) + fuel_mass_per_tank.append(filled_tank_wo_structure_right) + + # Fill the center tank (if available) + if center_tanks: + for center_tank in center_tanks: + if remaining_fuel > 0: + center_capacity = center_tank.energy / center_tank.gravimetric_density + fuel_to_fill = min(center_capacity, remaining_fuel) + if fuel_to_fill > 0: + filled_tank_wo_structure = MassPropertiesIO().initialize_zero() + filled_tank_wo_structure.mass = fuel_to_fill + filled_tank_wo_structure.center_of_gravity = center_tank.mass_properties.center_of_gravity + filled_tank_wo_structure.moment_change = ( + fuel_to_fill * filled_tank_wo_structure.center_of_gravity["x"]) + filled_tank_wo_structure.tank_location = center_tank.tank_location + filled_tank_wo_structure.energy_carrier = center_tank.energy_carrier + filled_tank_wo_structure.used_tank = center_tank.used_tank + filled_tank_wo_structure.maximum_capacity = center_tank.max_fuel_mass_capacity + fuel_mass_per_tank.append(filled_tank_wo_structure) + remaining_fuel -= fuel_to_fill + else: + filled_tank_wo_structure = MassPropertiesIO().initialize_zero() + filled_tank_wo_structure.mass = 0 + filled_tank_wo_structure.center_of_gravity = center_tank.mass_properties.center_of_gravity + filled_tank_wo_structure.moment_change = 0 + filled_tank_wo_structure.tank_location = center_tank.tank_location + filled_tank_wo_structure.energy_carrier = center_tank.energy_carrier + filled_tank_wo_structure.used_tank = center_tank.used_tank + filled_tank_wo_structure.maximum_capacity = center_tank.max_fuel_mass_capacity + fuel_mass_per_tank.append(filled_tank_wo_structure) + + # Fill the remaining tanks with any leftover fuel + for other_tank in other_tanks: + if remaining_fuel <= 0: + filled_tank_wo_structure = MassPropertiesIO().initialize_zero() + filled_tank_wo_structure.mass = 0 + filled_tank_wo_structure.center_of_gravity = other_tank.mass_properties.center_of_gravity + filled_tank_wo_structure.moment_change = 0 + filled_tank_wo_structure.tank_location = other_tank.tank_location + filled_tank_wo_structure.energy_carrier = other_tank.energy_carrier + filled_tank_wo_structure.used_tank = other_tank.used_tank + filled_tank_wo_structure.maximum_capacity = other_tank.max_fuel_mass_capacity + fuel_mass_per_tank.append(filled_tank_wo_structure) + else: + other_capacity = other_tank.energy / other_tank.gravimetric_density + fuel_to_fill = min(other_capacity, remaining_fuel) + if fuel_to_fill > 0: + filled_tank_wo_structure = MassPropertiesIO().initialize_zero() + filled_tank_wo_structure.mass = fuel_to_fill + filled_tank_wo_structure.center_of_gravity = other_tank.mass_properties.center_of_gravity + filled_tank_wo_structure.moment_change = ( + fuel_to_fill * filled_tank_wo_structure.center_of_gravity["x"]) + filled_tank_wo_structure.tank_location = other_tank.tank_location + filled_tank_wo_structure.energy_carrier = other_tank.energy_carrier + filled_tank_wo_structure.used_tank = other_tank.used_tank + filled_tank_wo_structure.maximum_capacity = other_tank.max_fuel_mass_capacity + fuel_mass_per_tank.append(filled_tank_wo_structure) + remaining_fuel -= fuel_to_fill + + # Check if there is still remaining fuel (should not happen unless there is a capacity error) + if remaining_fuel > 0: + if int(dict_ac_exchange['tool_level']) < int(routing_dict['tool_level']): + runtime_output.warning( + "Fuel mass calculation failed ... Not enough capacity in tanks!") + else: + raise ValueError( + "Fuel mass calculation failed ... Not enough capacity in tanks!") + + fuel_mass_properties = MassPropertiesIO().initialize_zero() + fuel_mass_properties.mass = sum( + [filled_tank.mass for filled_tank in fuel_mass_per_tank]) + fuel_mass_properties.center_of_gravity = calculate_center_of_gravity( + fuel_mass_per_tank) + fuel_mass_properties.initialize_zero_inertia() + + # Return the fuel mass properties and distribution per tank + return fuel_mass_properties, fuel_mass_per_tank + + +def calculate_fuel_properties_defueling( + tanks, fuel_to_remove, fuel_mass_per_tank, runtime_output, routing_dict, dict_ac_exchange): + + def map_fuel_mass_to_tanks(tanks, fuel_mass_per_tank, runtime_output): + + # Helper function to categorize tanks and fuel mass objects by their location + def get_tank_location_key(tank): + # Create a normalized key from tank location + return tank.tank_location.strip().lower() + + # Create a mapping of locations to fuel_mass_per_tank objects + fuel_mass_mapping = {get_tank_location_key( + fuel_tank): fuel_tank for fuel_tank in fuel_mass_per_tank} + + # Iterate over tanks and update their filled_fuel_mass based on the corresponding fuel_mass_per_tank + for tank in tanks: + tank_location_key = get_tank_location_key(tank) + + if tank_location_key in fuel_mass_mapping: + corresponding_fuel = fuel_mass_mapping[tank_location_key] + # Update tank with the filled mass value + tank.filled_fuel_mass = corresponding_fuel.mass + else: + runtime_output.critical( + f"No matching fuel mass found for tank at location: {tank.tank_location}. \ + No fuel was loaded in this tank.") + raise ValueError( + f"No matching fuel mass found for tank at location: {tank.tank_location}. \ + No fuel was loaded in this tank.") + + # This function modifies the tanks in place, so no need to return anything + + map_fuel_mass_to_tanks(tanks, fuel_mass_per_tank, + runtime_output) # the tanks are already sorted in the order for defueling, \ + # this is why here they are only mapped here to get the existing filled up fuel mass + + # Helper function to categorize tanks based on location string + def categorize_tank_location(tank): + location = tank.tank_location.split() + if "left" in location: + return "left" + elif "right" in location: + return "right" + elif "center" in location: + return "center" + else: + return "other" + + # Sort tanks into categories based on location + left_wing_tanks = [ + tank for tank in tanks if categorize_tank_location(tank) == "left"] + right_wing_tanks = [ + tank for tank in tanks if categorize_tank_location(tank) == "right"] + center_tanks = [ + tank for tank in tanks if categorize_tank_location(tank) == "center"] + other_tanks = [ + tank for tank in tanks if categorize_tank_location(tank) == "other"] + + # Calculate the total available fuel in the tanks + total_fuel_available = sum(tank.filled_fuel_mass for tank in tanks) + + if fuel_to_remove > total_fuel_available: + if int(dict_ac_exchange['tool_level']) < int(routing_dict['tool_level']): + runtime_output.warning( + "Fuel to remove exceeds the available fuel in all tanks!") + else: + raise ValueError( + "Fuel to remove exceeds the available fuel in all tanks!") + + remaining_fuel_to_remove = fuel_to_remove + defueled_mass_per_tank = [] + + # Step 1: Defuel other tanks first (start with the trim tank if existing) + for other_tank in other_tanks: + + fuel_available = other_tank.filled_fuel_mass + fuel_to_defuel = min(fuel_available, remaining_fuel_to_remove) + + if fuel_to_defuel > 0: + remaining_fuel_to_remove -= fuel_to_defuel + defueled_tank = MassPropertiesIO().initialize_zero() + defueled_tank.mass = -fuel_to_defuel # Negative for defueling + defueled_tank.center_of_gravity = other_tank.mass_properties.center_of_gravity + defueled_tank.moment_change = (-fuel_to_defuel * + defueled_tank.center_of_gravity["x"]) + defueled_tank.name = other_tank.tank_location + defueled_tank.fuel_left_in_tank = fuel_available - fuel_to_defuel + defueled_mass_per_tank.append(defueled_tank) + other_tank.filled_fuel_mass -= fuel_to_defuel + else: + defueled_tank = MassPropertiesIO().initialize_zero() + defueled_tank.mass = 0 # Negative for defueling + defueled_tank.center_of_gravity = other_tank.mass_properties.center_of_gravity + defueled_tank.moment_change = 0 + defueled_tank.name = other_tank.tank_location + defueled_tank.fuel_left_in_tank = fuel_available - 0 + defueled_mass_per_tank.append(defueled_tank) + other_tank.filled_fuel_mass -= 0 + + # Step 2: Defuel center tank + for center_tank in center_tanks: + if remaining_fuel_to_remove > 0: + fuel_available = center_tank.filled_fuel_mass + fuel_to_defuel = min(fuel_available, + remaining_fuel_to_remove) + if fuel_to_defuel > 0: + defueled_tank = MassPropertiesIO().initialize_zero() + defueled_tank.mass = -fuel_to_defuel # Negative for defueling + defueled_tank.center_of_gravity = center_tank.mass_properties.center_of_gravity + defueled_tank.moment_change = (-fuel_to_defuel * + defueled_tank.center_of_gravity["x"]) + defueled_tank.name = center_tank.tank_location + defueled_tank.fuel_left_in_tank = fuel_available - fuel_to_defuel + defueled_mass_per_tank.append(defueled_tank) + center_tank.filled_fuel_mass -= fuel_to_defuel + remaining_fuel_to_remove -= fuel_to_defuel + else: + defueled_tank = MassPropertiesIO().initialize_zero() + defueled_tank.mass = 0 # nothing to remove + defueled_tank.center_of_gravity = center_tank.mass_properties.center_of_gravity + defueled_tank.moment_change = 0 + defueled_tank.name = center_tank.tank_location + defueled_tank.fuel_left_in_tank = fuel_available - 0 + defueled_mass_per_tank.append(defueled_tank) + center_tank.filled_fuel_mass -= 0 + + # Step 3: Defuel wing tanks (opposite to refueling) + if left_wing_tanks and right_wing_tanks and remaining_fuel_to_remove > 0: + for left_tank, right_tank in zip(left_wing_tanks, right_wing_tanks): + + left_fuel_available = left_tank.filled_fuel_mass + right_fuel_available = right_tank.filled_fuel_mass + + fuel_to_defuel = min( + left_fuel_available, right_fuel_available, remaining_fuel_to_remove / 2) + + if fuel_to_defuel > 0: + remaining_fuel_to_remove -= 2 * fuel_to_defuel + + defueled_tank_left = MassPropertiesIO().initialize_zero() + defueled_tank_left.mass = -fuel_to_defuel # Negative for defueling + defueled_tank_left.center_of_gravity = left_tank.mass_properties.center_of_gravity + defueled_tank_left.moment_change = ( + -fuel_to_defuel * defueled_tank_left.center_of_gravity["x"]) + defueled_tank_left.name = left_tank.tank_location + defueled_tank_left.fuel_left_in_tank = left_fuel_available - fuel_to_defuel + + defueled_tank_right = MassPropertiesIO().initialize_zero() + defueled_tank_right.mass = -fuel_to_defuel # Negative for defueling + defueled_tank_right.center_of_gravity = right_tank.mass_properties.center_of_gravity + defueled_tank_right.moment_change = ( + -fuel_to_defuel * defueled_tank_right.center_of_gravity["x"]) + defueled_tank_right.name = right_tank.tank_location + defueled_tank_right.fuel_left_in_tank = right_fuel_available - fuel_to_defuel + + defueled_mass_per_tank.append(defueled_tank_left) + defueled_mass_per_tank.append(defueled_tank_right) + + left_tank.filled_fuel_mass -= fuel_to_defuel + right_tank.filled_fuel_mass -= fuel_to_defuel + else: + defueled_tank_left = MassPropertiesIO().initialize_zero() + defueled_tank_left.mass = 0 # nothing to remove + defueled_tank_left.center_of_gravity = left_tank.mass_properties.center_of_gravity + defueled_tank_left.moment_change = 0 + defueled_tank_left.name = left_tank.tank_location + defueled_tank_left.fuel_left_in_tank = left_fuel_available - 0 + + defueled_tank_right = MassPropertiesIO().initialize_zero() + defueled_tank_right.mass = 0 + defueled_tank_right.center_of_gravity = right_tank.mass_properties.center_of_gravity + defueled_tank_right.moment_change = 0 + defueled_tank_right.name = right_tank.tank_location + defueled_tank_right.fuel_left_in_tank = right_fuel_available - 0 + + defueled_mass_per_tank.append(defueled_tank_left) + defueled_mass_per_tank.append(defueled_tank_right) + + left_tank.filled_fuel_mass -= 0 + right_tank.filled_fuel_mass -= 0 + + # Check if we have defueled the correct amount + if remaining_fuel_to_remove > 0: + if int(dict_ac_exchange['tool_level']) < int(routing_dict['tool_level']): + runtime_output.warning( + "Not enough fuel in the tanks to complete defueling!") + else: + raise ValueError( + "Not enough fuel in the tanks to complete defueling!") + + return defueled_mass_per_tank + + +def calculate_design_fuel_mass(mission_information=None, maximum_takeoff_mass=None, + operating_mass_empty=None, design_payload_mass=None, tanks=None, + routing_dict=None, dict_ac_exchange=None): + """Calculation of design fuel mass + + Args: + mission_information (object, optional): design mission information from ac_exchange_file. + maximum_takeoff_mass (float, optional): maximum takeoff mass. + operating_mass_empty (float, optional): operating mass empty. + transport_task_data (float, optional): transport task data (passengers, mass p.P, luggage p.P). + Defaults to None. + + Returns: + float: design fuel @ takeoff, midflight and landing (minimum) + """ + + if mission_information is not None: + # print("Calculating design fuel mass based on mission information") + + cruise_fuel = mission_information["trip_fuel_mass"] - \ + mission_information["takeoff_fuel_mass"] - \ + mission_information["landing_fuel_mass"] + design_fuel_mass_ini = mission_information["mission_fuel_mass"] + design_fuel_mass_at_takeoff_ini = mission_information["mission_fuel_mass"] - \ + mission_information["taxi_out_fuel_mass"] + design_fuel_mass_midflight_ini = design_fuel_mass_at_takeoff_ini - \ + mission_information["takeoff_fuel_mass"] - cruise_fuel / 2 + design_fuel_mass_landing_minimum_ini = mission_information["mission_fuel_mass"] - \ + mission_information["trip_fuel_mass"] - \ + mission_information["taxi_out_fuel_mass"] + consumed_fuel_during_flight = mission_information["trip_fuel_mass"] + + # distribute the fuel along the tanks and save the informations per tank + # the refueling order is already saved in the sorted tanks + # = (preparation for the calculation of the refueling-cg-shift) + design_fuel_mass, design_fuel_mass_per_tank = calculate_fuel_mass_properties( + tanks, design_fuel_mass_ini, routing_dict, dict_ac_exchange) + design_fuel_mass.mass = design_fuel_mass_ini + design_fuel_mass_takeoff, design_fuel_mass_takeoff_per_tank = calculate_fuel_mass_properties( + tanks, design_fuel_mass_at_takeoff_ini, routing_dict, dict_ac_exchange) + design_fuel_mass_takeoff.mass = design_fuel_mass_at_takeoff_ini + design_fuel_mass_midflight, _ = calculate_fuel_mass_properties( + tanks, design_fuel_mass_midflight_ini, routing_dict, dict_ac_exchange) + design_fuel_mass_midflight.mass = design_fuel_mass_midflight_ini + design_fuel_mass_landing_minimum, _ = calculate_fuel_mass_properties( + tanks, design_fuel_mass_landing_minimum_ini, routing_dict, dict_ac_exchange) + design_fuel_mass_landing_minimum.mass = design_fuel_mass_landing_minimum_ini + else: + contingency_fuel_factor = 0.05 + alternate_fuel_factor = 0.1 + final_reserve_fuel_factor = 0.02 + + design_fuel_mass_takeoff = ( + maximum_takeoff_mass.mass - operating_mass_empty.mass - design_payload_mass.mass) + design_fuel_mass_midflight = design_fuel_mass_takeoff / 2.0 + design_fuel_mass_landing_minimum = design_fuel_mass_takeoff * \ + (contingency_fuel_factor + alternate_fuel_factor + final_reserve_fuel_factor) + consumed_fuel_during_flight = design_fuel_mass_takeoff - \ + design_fuel_mass_landing_minimum + + # distribute the fuel along the tanks and save the informations per tank + # the refueling order is already saved in the sorted tanks + # = (preparation for the calculation of the refueling-cg-shift) + design_fuel_mass_takeoff, design_fuel_mass_takeoff_per_tank = calculate_fuel_mass_properties( + tanks, design_fuel_mass_takeoff) + design_fuel_mass_midflight, _ = calculate_fuel_mass_properties( + tanks, design_fuel_mass_midflight, routing_dict, dict_ac_exchange) + design_fuel_mass_landing_minimum, _ = calculate_fuel_mass_properties( + tanks, design_fuel_mass_landing_minimum, routing_dict, dict_ac_exchange) + design_fuel_mass = design_fuel_mass_takeoff + design_fuel_mass_per_tank = design_fuel_mass_takeoff_per_tank + + return (design_fuel_mass_takeoff, design_fuel_mass_midflight, design_fuel_mass_landing_minimum, + design_fuel_mass_takeoff_per_tank, consumed_fuel_during_flight, design_fuel_mass, design_fuel_mass_per_tank) + + +def calculate_design_payload_mass(transport_task_data, fuselage_mass_properties, max_payload, runtime_output): + """Design payload mass + + Args: + transport_task_data (TransportTaskIO object): Transport task data + fuselage_mass_properties (MassPropertiesIO object): Fuselage mass properties + + Returns: + MassPropertiesIO Object: design payload mass + """ + design_payload_mass = MassPropertiesIO( + "./analysis/masses_cg_inertia/design_payload_mass") + design_payload_mass.mass = transport_task_data.combined_passenger_and_luggage_mass + \ + transport_task_data.additional_cargo_mass + + if design_payload_mass.mass > max_payload.mass: + runtime_output.critical( + "Design payload mass exceeds the maximum allowed payload mass!") + raise ValueError( + "Design payload mass exceeds the maximum allowed payload mass!") + + # cargodeck + passenger_deck + additional cargo + design_payload_mass.center_of_gravity = fuselage_mass_properties.center_of_gravity + design_payload_mass.initialize_zero_inertia() + DEBUG(msg="The CG of the design payload is set at the fuselage CG.") + + return design_payload_mass + + +def cg_change_passengers_boarding_front_back( + initial_cg, initial_weight, x_coordinate_row, number_of_passengers_per_row, acommodation_data, mac, LE): + + cg_positions = [] + total_weight = [] + + total_weight.append(initial_weight) + total_weight_new = initial_weight + total_moment_change = initial_cg * initial_weight + initial_mac_position = (initial_cg - LE) / mac * 100.0 + cg_positions.append(initial_mac_position) + + for row in range(len(x_coordinate_row)): + added_mass = acommodation_data.mass_per_passenger * \ + number_of_passengers_per_row[row] + + total_weight_new += added_mass + total_moment_change += added_mass * (x_coordinate_row[row]) + new_cg = total_moment_change / total_weight_new + new_mac_position = (new_cg - LE) / mac * 100.0 + + cg_positions.append(new_mac_position) + total_weight.append(total_weight_new) + + return cg_positions, total_weight + + +def cg_change_cargo_loading_front_back(initial_cg, initial_weight, x_coordinate_row, transport_task_data, mac, LE): + + cg_positions = [] + total_weight = [] + + total_weight.append(initial_weight) + total_weight_new = initial_weight + total_moment_change = initial_cg * initial_weight + initial_mac_position = (initial_cg - LE) / mac * 100.0 + cg_positions.append(initial_mac_position) + + for row in range(len(x_coordinate_row)): + added_mass = (transport_task_data.luggage_mass_per_passenger * transport_task_data.number_of_passengers + + transport_task_data.additional_cargo_mass) / len(x_coordinate_row) + + total_weight_new += added_mass + total_moment_change += added_mass * (x_coordinate_row[row]) + + new_cg = total_moment_change / total_weight_new + new_mac_position = (new_cg - LE) / mac * 100.0 + + cg_positions.append(new_mac_position) + total_weight.append(total_weight_new) + + return cg_positions, total_weight + + +def cg_change_passengers_boarding_front_back_excel(initial_cg, initial_weight, seating, acommodation_data, mac, LE): + + cg_positions = [] + total_weight = [] + + total_weight.append(initial_weight) + total_weight_new = initial_weight + total_moment_change = initial_cg * initial_weight + initial_mac_position = (initial_cg - LE) / mac * 100.0 + cg_positions.append(initial_mac_position) + + for index, row in seating.iterrows(): + total_weight_new += row['Number of Seats'] * \ + acommodation_data.mass_per_passenger + total_moment_change += (row['X Coordinate']) * \ + row['Number of Seats'] * acommodation_data.mass_per_passenger + + new_cg = total_moment_change / total_weight_new + new_mac_position = (new_cg - LE) / mac * 100.0 + + cg_positions.append(new_mac_position) + total_weight.append(total_weight_new) + + return cg_positions, total_weight + + +def cg_change_passengers_boarding_window_aisle( + initial_cg, initial_weight, x_coordinate_row, number_of_passengers_per_row, acommodation_data, mac, LE): + + # total nr of seats per row + nr_of_seats = number_of_passengers_per_row.copy() + + max_seats = math.ceil(max(nr_of_seats) / 2) + + cg_positions = [] + total_weight = [] + + total_weight.append(initial_weight) + total_weight_new = initial_weight + total_moment_change = initial_cg * initial_weight + initial_mac_position = (initial_cg - LE) / mac * 100.0 + cg_positions.append(initial_mac_position) + + for y in range(max_seats): + + for row in range(len(x_coordinate_row)): + + left_seats = nr_of_seats[row] + nr_of_seats[row] = nr_of_seats[row] - 2 + + if left_seats >= 2: + added_mass = acommodation_data.mass_per_passenger * 2 + elif left_seats == 1: + added_mass = acommodation_data.mass_per_passenger + else: + continue # No more seats left to occupy in this row + + total_weight_new += added_mass + total_moment_change += added_mass * (x_coordinate_row[row]) + + new_cg = total_moment_change / total_weight_new + new_mac_position = (new_cg - LE) / mac * 100.0 + + cg_positions.append(new_mac_position) + total_weight.append(total_weight_new) + + return cg_positions, total_weight + + +def calculate_maximum_payload_mass(maximum_structural_payload_mass, fuselage_accomodation_mass_properties): + """Design maximum mass - equal to maximum payload mass # ToDo + + Args: + transport_task_data (TransportTaskIO object): Transport task data + fuselage_mass_properties (MassPropertiesIO object): Fuselage mass properties + + Returns: + MassPropertiesIO Object: maximum payload mass + """ + maximum_payload_mass = MassPropertiesIO( + "./analysis/masses_cg_inertia/maximum_payload_mass") + maximum_payload_mass.mass = maximum_structural_payload_mass + + # Use accomodation center_of_gravity + maximum_payload_mass.center_of_gravity = fuselage_accomodation_mass_properties.center_of_gravity + maximum_payload_mass.initialize_zero_inertia() + DEBUG(msg="The CG of the maximum payload is set at the fuselage CG.") + + return maximum_payload_mass + + +def calculate_maximum_zero_fuel_mass(mass_properties, inertia_method): + """Maximum zero fuel mass - OME + MAX_PAYLOAD + + Arguments: + mass_properties {MassProperties IO Object} -- _description_ + inertia_method {_type_} -- _description_ + + Returns: + _type_ -- _description_ + """ + maximum_zero_fuel_mass = MassPropertiesIO( + "./analysis/masses_cg_inertia/maximum_zero_fuel_mass") + + maximum_zero_fuel_mass.mass = sum( + [element.mass for element in mass_properties]) + maximum_zero_fuel_mass.center_of_gravity = calculate_center_of_gravity( + mass_properties) + + maximum_zero_fuel_mass.inertia = inertia_method["method"]( + [mass_properties[0], mass_properties[1], MassPropertiesIO( + ).initialize_zero()], inertia_method["additional_parameters"] + ) + return maximum_zero_fuel_mass + + +def calculate_design_mass(mass_properties, inertia_method): + design_mass = MassPropertiesIO( + "./analysis/masses_cg_inertia/design_mass") + + design_mass.mass = sum([element.mass for element in mass_properties]) + design_mass.center_of_gravity = calculate_center_of_gravity( + mass_properties) + design_mass.inertia = inertia_method["method"]( + mass_properties, inertia_method["additional_parameters"] + ) + + return design_mass + + +def calculate_mtow_max_payload(ome, maximum_payload_mass, wing, mtom, LE, mac): + mtow_max_payload = MassPropertiesIO() + + fuel = mtom.mass - maximum_payload_mass.mass - ome.mass + + mtow_max_payload.mass = ome.mass + fuel + maximum_payload_mass.mass + mtow_max_payload.center_of_gravity = ( + ome.mass * ome.center_of_gravity['x'] + fuel * wing.center_of_gravity['x'] + + maximum_payload_mass.mass * maximum_payload_mass.center_of_gravity['x']) / mtow_max_payload.mass + DEBUG(msg="Estimated CG of MTOM with max payload. (fuel CG set in wing CG, payload CG set in fuselage CG.)") + mtow_max_payload.cg_mac = ( + mtow_max_payload.center_of_gravity - LE) / mac * 100.0 + + return mtow_max_payload + + +def calculate_mtow_max_fuel(ome, fuel_mass, wing, fuselage, mtom, LE, mac): + mtow_max_fuel = MassPropertiesIO() + + max_fuel = sum([element.mass for element in fuel_mass]) + + payload_mass = mtom.mass - max_fuel - ome.mass + + mtow_max_fuel.mass = ome.mass + max_fuel + payload_mass + mtow_max_fuel.center_of_gravity = (ome.mass * ome.center_of_gravity['x'] + max_fuel * wing.center_of_gravity['x'] + + payload_mass * fuselage.center_of_gravity['x']) / mtow_max_fuel.mass + DEBUG(msg="Estimated CG of MTOM with max fuel. (fuel CG set in wing CG, payload CG set in fuselage CG.)") + mtow_max_fuel.cg_mac = (mtow_max_fuel.center_of_gravity - LE) / mac * 100.0 + + return mtow_max_fuel + + +def custom_sort_low_wing(obj): + # Split the 'tank_location' entry into words + name_words = obj.tank_location.split() + + if 'right' in name_words and 'wing' in name_words: + if 'inner' in name_words: + return 0 + elif 'outer' in name_words: + return 2 + elif 'center' in name_words: + return 4 + elif 'left' in name_words and 'wing' in name_words: + if 'inner' in name_words: + return 1 + elif 'outer' in name_words: + return 3 + elif 'center' in name_words: + return 5 + else: + return 6 # If none of the criteria are met, maintain the order + + +def custom_sort_high_wing(obj): + # Split the 'tank_location' entry into words + name_words = obj.tank_location.split() + + if 'right' in name_words and 'wing' in name_words: + if 'outer' in name_words: + return 0 + elif 'inner' in name_words: + return 2 + elif 'center' in name_words: + return 4 + elif 'left' in name_words and 'wing' in name_words: + if 'outer' in name_words: + return 1 + elif 'inner' in name_words: + return 3 + elif 'center' in name_words: + return 5 + else: + return 6 # If none of the criteria are met, maintain the order + + +def custom_sort_defueling(obj): + # Split the 'tank_location' entry into words + name_words = obj.tank_location.split() + + if 'right' in name_words and 'wing' in name_words: + if 'center' in name_words: + return 0 + elif 'inner' in name_words: + return 2 + elif 'outer' in name_words: + return 4 + elif 'left' in name_words and 'wing' in name_words: + if 'center' in name_words: + return 1 + elif 'inner' in name_words: + return 3 + elif 'outer' in name_words: + return 5 + else: + return 6 # If none of the criteria are met, maintain the order + + +def read_tank_properties(ac_exchange_file, defueling, runtime_output): + """Read tank properties - mass specific + + Args: + ac_exchange_file (elementtree): aircraft_xml_elementtree + + Returns: + list of tanks: list of all tanks in the aircraft + """ + tanks_nodes = ac_exchange_file.findall( + "./component_design/tank/specific/tank[@ID]") + tanks = [] + + for tank_node in tanks_nodes: + tanks.append(TankIO().read(tank_node)) + + tanks_ref_point_x = float(ac_exchange_file.find( + "./component_design/tank/position/x/value").text) + + energy_carrier = [] + tank_requirements_nodes = ac_exchange_file.findall( + "./requirements_and_specifications/design_specification/configuration/tank_definition/tank[@ID]") + i = 0 + for tank_definition_node in tank_requirements_nodes: + + tank_energy_carrier_ID = int( + tank_definition_node.find("./energy_carrier_ID/value").text) + + energy_carrier_nodes = ac_exchange_file.findall( + "./requirements_and_specifications/design_specification/energy_carriers/energy_carrier[@ID]") + + energy_carrier = energy_carrier_nodes[tank_energy_carrier_ID].find( + "./type/value").text + energy_carrier_density = float( + energy_carrier_nodes[tank_energy_carrier_ID].find("./density/value").text) + energy_carrier_gravimetric_density = float( + energy_carrier_nodes[tank_energy_carrier_ID].find("./gravimetric_density/value").text) + + if len(tank_requirements_nodes) == len(tanks): + tanks[i].energy_carrier = energy_carrier + # 783 # kg/m^3 Jet A1 Kerosene + tanks[i].fuel_density = energy_carrier_density + # 43 MJ/kg for kerosene, 1.1993E8 for liqiud hydrogen + tanks[i].gravimetric_density = energy_carrier_gravimetric_density + tanks[i].max_fuel_mass_capacity = tanks[i].energy / \ + tanks[i].gravimetric_density + tanks[i].mass_properties.center_of_gravity["x"] = tanks_ref_point_x + \ + tanks[i].position["x"]["value"] + \ + tanks[i].centroid["x"]["value"] + else: + runtime_output.critical( + "No consistent tanks definitions with the number of designed tanks.") + raise ValueError( + "No consistent tanks definitions with the number of designed tanks.") + i += 1 + + # Sort the tanks according to the wing mounting using the custom sorting function + wing_mounting = str(ac_exchange_file.find( + "./requirements_and_specifications/design_specification/configuration/wing_definition/mounting/value").text) + + if wing_mounting == "high": + # For high wing -> outer - inner - center - other tanks + sorted_tanks = sorted(tanks, key=custom_sort_high_wing) + elif wing_mounting == "low" or wing_mounting == "mid": + # For low wing -> inner - outer - center - other tanks + sorted_tanks = sorted(tanks, key=custom_sort_low_wing) + else: + # order as in axml + sorted_tanks = tanks # adapt / change for bwb, h2? + + if defueling == "mode_1": + # inverse order of refueling + sorted_tanks_defueling = sorted_tanks[::-1] + else: + sorted_tanks_defueling = tanks # adapt / change for bwb, h2? + + return sorted_tanks, sorted_tanks_defueling + + +def read_propulsion_properties(ac_exchange_file): + propulsor_nodes = ac_exchange_file.findall( + "./component_design/propulsion/specific/propulsion[@ID]" + ) + + pylons = [] + nacelles = [] + engines = [] + for propulsor_node in propulsor_nodes: + + nacelle_nodes = propulsor_node.findall(".//nacelle[@ID]") + for nacelle_node in nacelle_nodes: + nacelles.append(NacelleIO().read(nacelle_node)) + + # nacelles.append(NacelleIO().read(propulsor_node)) + pylons.append(PylonIO().read(propulsor_node)) + engines.append(EngineIO().read(propulsor_node)) + + return nacelles, pylons, engines + + +def read_main_components(ac_exchange_file, dict_ac_exchange, runtime_output): + + components_available = [ + key.replace('_exist', '') for key in dict_ac_exchange if dict_ac_exchange[key] is True and '_exist' in key] + + mass_properties = {} + # Get overall mass properties + for component in components_available: + runtime_output.print( + f"Mass properties of component {component} read ...") + mass_properties[component] = MassPropertiesIO( + f"./component_design/{component}/mass_properties") + mass_properties[component].read(ac_exchange_file) + runtime_output.print( + f"{component} - mass: {mass_properties[component].mass}") + + return mass_properties + + +def read_transport_task(ac_exchange_file): + return TransportTaskIO().read(ac_exchange_file) + + +def read_acommodation(ac_exchange_file, excel_seatings_file): + return AcommodationIO().read(ac_exchange_file, excel_seatings_file) + + +def read_maximum_takeoff_mass(ac_exchange_file): + return MassPropertiesIO("./analysis/masses_cg_inertia/maximum_takeoff_mass/mass_properties").read(ac_exchange_file) + + +def read_operator_items(ac_exchange_file, fuselage_length): + # the operator items are written by the fuselage node and the systems node + operator_items = [] + i = 0 + total_mass = 0 + + fuselage_nodes = ac_exchange_file.findall( + "./component_design/fuselage/specific/geometry/fuselage[@ID]" + ) + for fuselage_node in fuselage_nodes: + fuselage_operator_nodes = fuselage_node.findall( + "./mass_breakdown/fuselage_operator_items/component_mass[@ID]") + for operator in fuselage_operator_nodes: + operator_items.append(ComponentMassIO(".").read(operator)) + operator_items[i].center_of_gravity['x'] = 0.45 * \ + fuselage_length # + fuselage_reference_point + total_mass += operator_items[i].mass + fuselage_operator_items_mass = total_mass + i = i + 1 + DEBUG(msg="CGs of fuselage operator items estimated here at 45% fuselage length") + + systems_operator_nodes = ac_exchange_file.findall( + "./component_design/systems/operator_items/operator_item[@ID]" + ) + + for operator in systems_operator_nodes: + operator_items.append(MassPropertiesIO( + "./mass_properties").read(operator)) + operator_items[i].name = str(operator.find("./name/value").text) + total_mass += operator_items[i].mass + i = i + 1 + + systems_operator_items_mass = total_mass - fuselage_operator_items_mass + + return operator_items, fuselage_operator_items_mass, systems_operator_items_mass + + +def read_furnishings(ac_exchange_file): + + fuselage_nodes = ac_exchange_file.findall( + "./component_design/fuselage/specific/geometry/fuselage[@ID]" + ) + furnishings = [] + i = 0 + furnishings_total_mass = 0 + + for fuselage_node in fuselage_nodes: + furnishing_nodes = fuselage_node.findall( + "./mass_breakdown/fuselage_furnishing/component_mass[@ID]") + for furnishing in furnishing_nodes: + furnishings.append(ComponentMassIO(".").read(furnishing)) + furnishings_total_mass += furnishings[i].mass + i = i + 1 + + return furnishings_total_mass, furnishings + + +def read_systems(ac_exchange_file): + + systems_nodes = ac_exchange_file.findall( + "./component_design/systems/specific/geometry/mass_properties/" + ) + systems = [] + + for node in systems_nodes: + item = {} + item['name'] = node.tag + item['mass'] = float(node.find("./value").text) + + systems.append(item) + + return systems + + +def read_wing(paths): + + acxml = aixml.openDocument(paths["path_to_aircraft_exchange_file"]) + airfoil_data_dir = f'{paths["project_directory"]}/geometry_data/airfoil_data' + + return geom2.factory.WingFactory(acxml, airfoil_data_dir).create("wing/specific/geometry/aerodynamic_surface@0") + + +def read_fuselage(paths): + acxml = aixml.openDocument(paths["path_to_aircraft_exchange_file"]) + geometry_data_dir = f'{paths["project_directory"]}/geometry_data/' + return geom2.factory.FuselageFactory(acxml, geometry_data_dir).create("fuselage/specific/geometry/fuselage@0") + + +def determine_mission_fuel(dict_ac_exchange): + + # Initialize an empty dictionary to store fuel mass for each entry + fuel_mass_results = {} + + # Iterate over the energy entries + for energy_id, consumed_energy in dict_ac_exchange["loaded_mission_energy"].items(): + # Generate the corresponding carrier key by replacing "energy" with "energy_carrier" + carrier_key = energy_id.replace("energy", "energy_carrier") + + # Retrieve the integer carrier ID from "loaded_mission_energy_carrier" + carrier_id_int = dict_ac_exchange["loaded_mission_energy_carrier"].get( + carrier_key) + + # Construct the gravimetric density ID using the integer carrier ID + density_id = f"energy_carrier_gravimetric_density_ID{int(carrier_id_int)}" + + # Retrieve the gravimetric density for this specific carrier + gravimetric_density = dict_ac_exchange["energy_carrier_gravimetric_density"].get( + density_id) + + # Calculate fuel mass if gravimetric density is available + if gravimetric_density is not None: + fuel_mass = consumed_energy / gravimetric_density + fuel_mass_results[energy_id] = fuel_mass + else: + print( + f"{energy_id} - Gravimetric density not found for carrier ID {carrier_id_int}") + + mission_fuel_mass = sum(fuel_mass_results.values()) + + return mission_fuel_mass + + +def determine_taxi_in_fuel(dict_ac_exchange): + + # Initialize an empty dictionary to store fuel mass for each entry + fuel_mass_results = {} + + # Iterate over the energy entries + for energy_id, consumed_energy in dict_ac_exchange["taxi_energy_in"].items(): + # Generate the corresponding carrier key by replacing "energy" with "energy_carrier" + carrier_key = energy_id.replace("in", "in_carrier") + + # Retrieve the integer carrier ID from "loaded_mission_energy_carrier" + carrier_id_int = dict_ac_exchange["taxi_energy_in_carrier"].get( + carrier_key) + + # Construct the gravimetric density ID using the integer carrier ID + density_id = f"energy_carrier_gravimetric_density_ID{int(carrier_id_int)}" + + # Retrieve the gravimetric density for this specific carrier + gravimetric_density = dict_ac_exchange["energy_carrier_gravimetric_density"].get( + density_id) + + # Calculate fuel mass if gravimetric density is available + if gravimetric_density is not None: + fuel_mass = consumed_energy / gravimetric_density + fuel_mass_results[energy_id] = fuel_mass + else: + print( + f"{energy_id} - Gravimetric density not found for carrier ID {carrier_id_int}") + + taxi_in_fuel_mass = sum(fuel_mass_results.values()) + + return taxi_in_fuel_mass + + +def determine_taxi_out_fuel(dict_ac_exchange): + + # Initialize an empty dictionary to store fuel mass for each entry + fuel_mass_results = {} + + # Iterate over the energy entries + for energy_id, consumed_energy in dict_ac_exchange["taxi_energy_out"].items(): + # Generate the corresponding carrier key by replacing "energy" with "energy_carrier" + carrier_key = energy_id.replace("out", "out_carrier") + + # Retrieve the integer carrier ID from "loaded_mission_energy_carrier" + carrier_id_int = dict_ac_exchange["taxi_energy_out_carrier"].get( + carrier_key) + + # Construct the gravimetric density ID using the integer carrier ID + density_id = f"energy_carrier_gravimetric_density_ID{int(carrier_id_int)}" + + # Retrieve the gravimetric density for this specific carrier + gravimetric_density = dict_ac_exchange["energy_carrier_gravimetric_density"].get( + density_id) + + # Calculate fuel mass if gravimetric density is available + if gravimetric_density is not None: + fuel_mass = consumed_energy / gravimetric_density + fuel_mass_results[energy_id] = fuel_mass + else: + print( + f"{energy_id} - Gravimetric density not found for carrier ID {carrier_id_int}") + + taxi_out_fuel_mass = sum(fuel_mass_results.values()) + + return taxi_out_fuel_mass + + +def determine_takeoff_fuel(dict_ac_exchange, runtime_output): + + # Initialize an empty dictionary to store fuel mass for each entry + fuel_mass_results = {} + + # Iterate over the energy entries + for energy_id, consumed_energy in dict_ac_exchange["take_off_energy"].items(): + if consumed_energy is not None: + # Generate the corresponding carrier key by replacing "energy" with "energy_carrier" + carrier_key = energy_id.replace("energy", "energy_carrier") + + # Retrieve the integer carrier ID from "loaded_mission_energy_carrier" + carrier_id_int = dict_ac_exchange["take_off_energy_carrier"].get( + carrier_key) + + # Construct the gravimetric density ID using the integer carrier ID + density_id = f"energy_carrier_gravimetric_density_ID{int(carrier_id_int)}" + + # Retrieve the gravimetric density for this specific carrier + gravimetric_density = dict_ac_exchange["energy_carrier_gravimetric_density"].get( + density_id) + + # Calculate fuel mass if gravimetric density is available + if gravimetric_density is not None: + fuel_mass = consumed_energy / gravimetric_density + fuel_mass_results[energy_id] = fuel_mass + else: + print( + f"{energy_id} - Gravimetric density not found for carrier ID {carrier_id_int}") + else: + runtime_output.critical( + "The take off energy node not found in the mission analysis block. \ + Fuel mass needed for the takeoff segment set to 0.") + + take_off_fuel_mass = sum(fuel_mass_results.values()) + + return take_off_fuel_mass + + +def determine_landing_fuel(dict_ac_exchange, runtime_output): + + # Initialize an empty dictionary to store fuel mass for each entry + fuel_mass_results = {} + + # Iterate over the energy entries + for energy_id, consumed_energy in dict_ac_exchange["landing_energy"].items(): + if consumed_energy is not None: + # Generate the corresponding carrier key by replacing "energy" with "energy_carrier" + carrier_key = energy_id.replace("energy", "energy_carrier") + + # Retrieve the integer carrier ID from "loaded_mission_energy_carrier" + carrier_id_int = dict_ac_exchange["landing_energy_carrier"].get( + carrier_key) + + # Construct the gravimetric density ID using the integer carrier ID + density_id = f"energy_carrier_gravimetric_density_ID{int(carrier_id_int)}" + + # Retrieve the gravimetric density for this specific carrier + gravimetric_density = dict_ac_exchange["energy_carrier_gravimetric_density"].get( + density_id) + + # Calculate fuel mass if gravimetric density is available + if gravimetric_density is not None: + fuel_mass = consumed_energy / gravimetric_density + fuel_mass_results[energy_id] = fuel_mass + else: + print( + f"{energy_id} - Gravimetric density not found for carrier ID {carrier_id_int}") + else: + runtime_output.critical( + "The landing energy node not found in the mission analysis block. \ + Fuel mass needed for the landing segment set to 0.") + + landing_fuel_mass = sum(fuel_mass_results.values()) + + return landing_fuel_mass + + +def determine_trip_fuel(dict_ac_exchange): + + # Initialize an empty dictionary to store fuel mass for each entry + fuel_mass_results = {} + + # Iterate over the energy entries + for energy_id, consumed_energy in dict_ac_exchange["trip_energy"].items(): + # Generate the corresponding carrier key by replacing "energy" with "energy_carrier" + carrier_key = energy_id.replace("energy", "energy_carrier") + + # Retrieve the integer carrier ID from "loaded_mission_energy_carrier" + carrier_id_int = dict_ac_exchange["trip_energy_carrier"].get( + carrier_key) + + # Construct the gravimetric density ID using the integer carrier ID + density_id = f"energy_carrier_gravimetric_density_ID{int(carrier_id_int)}" + + # Retrieve the gravimetric density for this specific carrier + gravimetric_density = dict_ac_exchange["energy_carrier_gravimetric_density"].get( + density_id) + + # Calculate fuel mass if gravimetric density is available + if gravimetric_density is not None: + fuel_mass = consumed_energy / gravimetric_density + fuel_mass_results[energy_id] = fuel_mass + else: + print( + f"{energy_id} - Gravimetric density not found for carrier ID {carrier_id_int}") + + trip_fuel_mass = sum(fuel_mass_results.values()) + + return trip_fuel_mass + + +def DEBUG(msg=""): + import logging + runtime_output = logging.getLogger('module_logger') + runtime_output.warning(f"Debug {sys._getframe().f_back.f_lineno}: {msg}") diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/propulsionIO.py b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/propulsionIO.py index 10605f4d88ccbbb872b5a4a9b18ab5fa7930f68b..af2ed424d0300701ea366aeef58b9e4aa4663db4 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/propulsionIO.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/propulsionIO.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """PylonIO """ diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/tankIO.py b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/tankIO.py index 6e99afca01f72e6e3ee233e52bac29dcbc5120f5..6f421847599b5ebc649ecd4e8a45f16e64575802 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/tankIO.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/tankIO.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """Tank IO """ from .massPropertiesIO import MassPropertiesIO diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/transportTaskIO.py b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/transportTaskIO.py index e740580c1bb8e5034e0ceb5e84a76110c5741b5d..4b5495795b91b5a03e4d568e402d1c6fcc29dc9d 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/basic/transportTaskIO.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/basic/transportTaskIO.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """Transport task IO""" diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodhtmlreport.py b/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodhtmlreport.py index 0db9803744c282ed5f14c2e0d3e832a6adb2ffd0..a6765a90b9ead9e03a2120b2886d19ea39974bca 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodhtmlreport.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodhtmlreport.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """Module providing report functionalities for current calculation method.""" from yattag import Doc from bs4 import BeautifulSoup @@ -34,6 +54,8 @@ def method_html_report(paths_and_names, routing_dict, data_dict, method_specific data_dict["operating_mass_empty"].mass), ("Maximum Takeoff Mass", "MTOM", data_dict["maximum_takeoff_mass"].mass), + ("Maximum Landing Mass", "MLM", + data_dict["maximum_landing_mass"].mass), ("Maximum Zero Fuel Mass", "MZFM", data_dict["maximum_zero_fuel_mass"].mass), ("Maximum Payload Mass", "-", data_dict["maximum_payload_mass"].mass), @@ -44,14 +66,15 @@ def method_html_report(paths_and_names, routing_dict, data_dict, method_specific ] aircraft_design_masses = [ - ("Maximum Takeoff Mass", "MTOM", - data_dict["maximum_takeoff_mass"].mass), - ("Maximum Landing Mass", "MLM", - data_dict["maximum_landing_mass"].mass), + ("Design Mass (Mission)", "-", + data_dict["design_mass"].mass), + ("Design Mass (Takeoff)", "-", + data_dict["design_mass_takeoff"].mass), ("Design Payload Mass", "-", data_dict["design_payload_mass"].mass), - ("Design Fuel Mass", "-", data_dict["design_fuel_mass"].mass), - ("Design Fuel Mass Takeoff", "-", - data_dict["design_fuel_mass_takeoff"].mass), + ("Design Fuel Mass (Mission)", "-", + data_dict["mission_fuel_mass"].mass), + ("Design Fuel Mass (Takeoff)", "-", + data_dict["design_fuel_takeoff"].mass), ("Design Fuel Mass Midflight", "-", data_dict["design_fuel_mass_midflight"].mass), ("Design Fuel Mass Minimum Landing", "-", diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodplot.py b/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodplot.py index 9247c045c78947da6381c2325ba843c4e8f87411..6616338cde7772e4456290be0bea4841b84a0564 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodplot.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodplot.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """Module providing plotting functionalities for current calculation method.""" # Import standard libraries. import os @@ -180,8 +200,8 @@ def method_plot(paths_and_names, routing_dict, data_dict, method_specific_output plt.plot(data_dict["ferry_range_mass"].cg_mac, data_dict["ferry_range_mass"].mass, 'o') - plt.plot(data_dict["design_mass"].cg_mac, - data_dict["design_mass"].mass, 's') + plt.plot(data_dict["design_mass_takeoff"].cg_mac, + data_dict["design_mass_takeoff"].mass, 's') # Define legend labels and colors legend_labels2 = ['OEM', 'MZFM (max Payload)', 'max Payload + Fuel', 'max Fuel + Payload', 'max Fuel', diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodtexoutput.py b/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodtexoutput.py index 87a9aa043988da76e4dcc285947e5c66c16b4fb8..a1646acb0a61238a9ad4a4ae9e51b18007537cb5 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodtexoutput.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodtexoutput.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """Module providing report functionalities for current calculation method.""" diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodxmlexport.py b/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodxmlexport.py index c618c2efff79e11e7e8ac9883a32d2233362bbe6..43b00c5458488c1196a4ba2d07fb61a03d808cf0 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodxmlexport.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/general/methodxmlexport.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """Module providing export functionalities for current calculation method.""" # Import standard libraries. import xml.etree.ElementTree as ET diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/test_basic/test_IO.py b/weight_and_balance_analysis/src/tube_and_wing/standard/test_basic/test_IO.py index 58c676b078f72aa385c461a5ad741153b074092b..847a837727d5b29e0565f889ea26cb59e954cf1c 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/test_basic/test_IO.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/test_basic/test_IO.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + import pytest import xml.etree.ElementTree as ET diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/test_basic/test_methodbasic.py b/weight_and_balance_analysis/src/tube_and_wing/standard/test_basic/test_methodbasic.py index 5db7e91a887e68050c9a8643a8c5cdb64c414128..6053d2f31838d76f71dbac328db15b61beb2e557 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/test_basic/test_methodbasic.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/test_basic/test_methodbasic.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + import pytest from basic.methodbasic import calculate_center_of_gravity, calculate_geometric_distance_by_axis, calculate_inertia_by_components diff --git a/weight_and_balance_analysis/src/tube_and_wing/standard/usermethoddatapreparation.py b/weight_and_balance_analysis/src/tube_and_wing/standard/usermethoddatapreparation.py index 61e300cf2e52b7bdea75cbbf2f025b1c25a0e513..bd444494c38e0cd1ee12549f7d4178f4e1505ab7 100644 --- a/weight_and_balance_analysis/src/tube_and_wing/standard/usermethoddatapreparation.py +++ b/weight_and_balance_analysis/src/tube_and_wing/standard/usermethoddatapreparation.py @@ -1,3 +1,23 @@ +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2024 UNICADO consortium +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. + """Module providing functions for the preparation of user data.""" @@ -149,7 +169,7 @@ def user_method_data_output_preparation(data_dict): key_output_dict.update( data_dict["most_afterward_mass"].user_method_data_generation()) key_output_dict.update( - data_dict["design_mass"].user_method_data_generation()) + data_dict["design_mass_takeoff"].user_method_data_generation()) key_output_dict.update( data_dict["design_fuel_mass"].user_method_data_generation())