├── src └── fastuav │ ├── __init__.py │ ├── models │ ├── __init__.py │ ├── add_ons │ │ ├── __init__.py │ │ └── sample_discipline.py │ ├── mtow │ │ ├── __init__.py │ │ ├── mtow_fixedwing.py │ │ ├── mtow_multirotor.py │ │ └── mtow_hybrid.py │ ├── wires │ │ ├── __init__.py │ │ ├── wires_fixedwing.py │ │ ├── wires_multirotor.py │ │ └── wires_hybrid.py │ ├── aerodynamics │ │ ├── __init__.py │ │ ├── aerodynamics_multirotor.py │ │ └── aerodynamics_hybrid.py │ ├── geometry │ │ ├── __init__.py │ │ └── geometry_multirotor.py │ ├── performance │ │ ├── __init__.py │ │ ├── mission │ │ │ ├── __init__.py │ │ │ └── mission_definition │ │ │ │ ├── __init__.py │ │ │ │ ├── tests │ │ │ │ ├── __init__.py │ │ │ │ ├── data │ │ │ │ │ └── mission.yaml │ │ │ │ └── test_schema.py │ │ │ │ ├── resources │ │ │ │ └── __init__.py │ │ │ │ └── schema.py │ │ └── range_and_endurance.py │ ├── propulsion │ │ ├── __init__.py │ │ ├── energy │ │ │ ├── __init__.py │ │ │ └── battery │ │ │ │ ├── __init__.py │ │ │ │ ├── battery.py │ │ │ │ └── performance_analysis.py │ │ ├── esc │ │ │ ├── __init__.py │ │ │ ├── esc.py │ │ │ ├── estimation_models.py │ │ │ ├── performance_analysis.py │ │ │ ├── catalogue.py │ │ │ ├── definition_parameters.py │ │ │ └── constraints.py │ │ ├── motor │ │ │ ├── __init__.py │ │ │ ├── motor.py │ │ │ └── definition_parameters.py │ │ ├── gearbox │ │ │ ├── __init__.py │ │ │ └── gearbox.py │ │ ├── propeller │ │ │ ├── __init__.py │ │ │ ├── aerodynamics │ │ │ │ └── __init__.py │ │ │ ├── propeller.py │ │ │ └── estimation_models.py │ │ ├── propulsion_multirotor.py │ │ ├── propulsion_fixedwing.py │ │ ├── propulsion_hybrid.py │ │ └── propulsion.py │ ├── scenarios │ │ ├── __init__.py │ │ ├── thrust │ │ │ ├── __init__.py │ │ │ ├── hover.py │ │ │ ├── flight_models.py │ │ │ └── takeoff.py │ │ ├── wing_loading │ │ │ └── __init__.py │ │ ├── scenarios_multirotor.py │ │ ├── scenarios_fixedwing.py │ │ └── scenarios_hybrid.py │ ├── stability │ │ ├── __init__.py │ │ ├── static_longitudinal │ │ │ ├── __init__.py │ │ │ ├── center_of_gravity │ │ │ │ ├── __init__.py │ │ │ │ ├── components │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── cog_propulsion.py │ │ │ │ └── cog.py │ │ │ ├── neutral_point.py │ │ │ └── static_margin.py │ │ ├── stability_fixedwing.py │ │ └── stability_hybrid.py │ └── structures │ │ ├── __init__.py │ │ ├── wing │ │ ├── __init__.py │ │ ├── wing.py │ │ ├── constraints.py │ │ └── structural_analysis.py │ │ ├── structures_fixedwing.py │ │ ├── structures_hybrid.py │ │ ├── fuselage.py │ │ ├── tails.py │ │ └── structures_multirotor.py │ ├── notebooks │ ├── __init__.py │ ├── workdir │ │ └── sensitivity_analysis │ │ │ ├── y_morris.txt │ │ │ ├── y_sobol.txt │ │ │ ├── x_morris.txt │ │ │ ├── problem_morris.txt │ │ │ └── problem_sobol.txt │ ├── img │ │ └── uncertainty.gif │ ├── data │ │ ├── configurations │ │ │ ├── multirotor_performance.yaml │ │ │ ├── doe_simple_model.yaml │ │ │ ├── __init__.py │ │ │ ├── multirotor_model.yaml │ │ │ ├── hybrid_model.yaml │ │ │ ├── fixedwing_model.yaml │ │ │ ├── multirotor_model_cots.yaml │ │ │ ├── multirotor_mda.yaml │ │ │ ├── doe_sobol_analysis.yaml │ │ │ └── multirotor_mda_cots.yaml │ │ ├── missions │ │ │ ├── missions_fixedwing.yaml │ │ │ ├── missions_hybrid.yaml │ │ │ ├── missions_multirotor_doe.yaml │ │ │ ├── missions_multirotor.yaml │ │ │ ├── __init__.py │ │ │ └── missions_multirotor_off_design.yaml │ │ ├── __init__.py │ │ └── source_files │ │ │ ├── __init__.py │ │ │ └── problem_inputs_doe.xml │ └── README.md │ ├── utils │ ├── __init__.py │ ├── drivers │ │ └── __init__.py │ ├── catalogues │ │ └── __init__.py │ ├── postprocessing │ │ ├── __init__.py │ │ ├── old │ │ │ └── __init__.py │ │ └── sensitivity_analysis │ │ │ ├── __init__.py │ │ │ ├── sobol_plot.py │ │ │ ├── distribution_plot.py │ │ │ └── morris_plot.py │ └── configurations_versatility.py │ ├── configurations │ ├── __init__.py │ ├── multirotor_performance.yaml │ ├── doe_simple_model.yaml │ ├── multirotor_model.yaml │ ├── hybrid_model.yaml │ ├── fixedwing_model.yaml │ ├── multirotor_model_cots.yaml │ ├── multirotor_mda.yaml │ ├── doe_sobol_analysis.yaml │ └── multirotor_mda_cots.yaml │ ├── missions │ ├── missions_fixedwing.yaml │ ├── missions_hybrid.yaml │ └── missions_multirotor.yaml │ ├── data │ ├── __init__.py │ └── catalogues │ │ └── ESC │ │ ├── Non-Dominated-ESC.csv │ │ └── ESC_data.csv │ └── constants.py ├── pytest.ini ├── docs └── assets │ └── banner.png ├── .vscode └── settings.json ├── .gitignore ├── .github └── workflows │ ├── tests.yml │ └── watchdog_tests.yml └── pyproject.toml /src/fastuav/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/notebooks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/configurations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/add_ons/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/mtow/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/wires/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/utils/drivers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/aerodynamics/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/geometry/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/performance/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/scenarios/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/stability/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/structures/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/utils/catalogues/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/energy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/esc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/motor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/scenarios/thrust/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/structures/wing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/utils/postprocessing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/utils/postprocessing/old/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/performance/mission/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/gearbox/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/propeller/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/scenarios/wing_loading/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/energy/battery/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --nbval-lax -p no:python 3 | -------------------------------------------------------------------------------- /src/fastuav/models/stability/static_longitudinal/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/propeller/aerodynamics/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/utils/postprocessing/sensitivity_analysis/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/performance/mission/mission_definition/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/performance/mission/mission_definition/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/stability/static_longitudinal/center_of_gravity/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/performance/mission/mission_definition/resources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastuav/models/stability/static_longitudinal/center_of_gravity/components/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SizingLab/FAST-UAV/HEAD/docs/assets/banner.png -------------------------------------------------------------------------------- /src/fastuav/notebooks/workdir/sensitivity_analysis/y_morris.txt: -------------------------------------------------------------------------------- 1 | data:performance:endurance:hover -------------------------------------------------------------------------------- /src/fastuav/notebooks/workdir/sensitivity_analysis/y_sobol.txt: -------------------------------------------------------------------------------- 1 | data:performance:endurance:hover -------------------------------------------------------------------------------- /src/fastuav/notebooks/img/uncertainty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SizingLab/FAST-UAV/HEAD/src/fastuav/notebooks/img/uncertainty.gif -------------------------------------------------------------------------------- /src/fastuav/notebooks/workdir/sensitivity_analysis/x_morris.txt: -------------------------------------------------------------------------------- 1 | ['uncertainty:propulsion:multirotor:battery:power:max:rel', 'uncertainty:propulsion:multirotor:motor:torque:friction:rel'] -------------------------------------------------------------------------------- /src/fastuav/models/aerodynamics/aerodynamics_multirotor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Multirotor Airframe Aerodynamics 3 | """ 4 | import fastoad.api as oad 5 | import openmdao.api as om 6 | 7 | 8 | # THIS MODULE STILL HAS TO BE IMPLEMENTED -------------------------------------------------------------------------------- /src/fastuav/notebooks/workdir/sensitivity_analysis/problem_morris.txt: -------------------------------------------------------------------------------- 1 | {'num_vars': 2, 'names': ['uncertainty:propulsion:multirotor:battery:power:max:rel', 'uncertainty:propulsion:multirotor:motor:torque:friction:rel'], 'bounds': [[-0.1, 0.1, 'unif'], [-0.1, 0.1, 'unif']]} -------------------------------------------------------------------------------- /src/fastuav/notebooks/workdir/sensitivity_analysis/problem_sobol.txt: -------------------------------------------------------------------------------- 1 | {'num_vars': 2, 'names': ['uncertainty:propulsion:multirotor:battery:power:max:rel', 'uncertainty:propulsion:multirotor:motor:torque:friction:rel'], 'bounds': [[-0.1, 0.1, 'unif'], [-0.1, 0.1, 'unif']]} -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "src" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true, 7 | "python-envs.defaultEnvManager": "ms-python.python:poetry", 8 | "python-envs.pythonProjects": [], 9 | "python-envs.defaultPackageManager": "ms-python.python:poetry" 10 | } -------------------------------------------------------------------------------- /src/fastuav/configurations/multirotor_performance.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone Performance # Performance evaluation with existing UAV design. 2 | 3 | # Input and output files 4 | input_file: ../../workdir/problem_inputs.xml 5 | output_file: ../../workdir/problem_outputs.xml 6 | 7 | # Definition of OpenMDAO model 8 | model: 9 | missions: 10 | id: fastuav.performance.mission 11 | file_path: ../missions/missions_multirotor_off_design.yaml # path to the mission definition file 12 | -------------------------------------------------------------------------------- /src/fastuav/models/performance/mission/mission_definition/tests/data/mission.yaml: -------------------------------------------------------------------------------- 1 | routes: 2 | main_route: 3 | takeoff_part: 4 | phase_id: vertical_takeoff 5 | climb_part: 6 | phase_id: multirotor_climb 7 | cruise_part: 8 | phase_id: fixedwing_cruise 9 | diversion: 10 | climb_part: 11 | phase_id: fixedwing_climb 12 | cruise_part: 13 | phase_id: fixedwing_cruise 14 | 15 | missions: 16 | sizing: 17 | parts: 18 | - route: main_route 19 | - route: diversion -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/configurations/multirotor_performance.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone Performance # Performance evaluation with existing UAV design. 2 | 3 | # Input and output files 4 | input_file: ../../workdir/problem_inputs.xml 5 | output_file: ../../workdir/problem_outputs.xml 6 | 7 | # Definition of OpenMDAO model 8 | model: 9 | missions: 10 | id: fastuav.performance.mission 11 | file_path: ../missions/missions_multirotor_off_design.yaml # path to the mission definition file 12 | -------------------------------------------------------------------------------- /src/fastuav/models/mtow/mtow_fixedwing.py: -------------------------------------------------------------------------------- 1 | """ 2 | MTOW for fixed wing UAVs 3 | """ 4 | 5 | import fastoad.api as oad 6 | from fastuav.constants import FW_PROPULSION 7 | from fastuav.models.mtow.mtow import MTOW 8 | 9 | 10 | @oad.RegisterOpenMDAOSystem("fastuav.mtow.fixedwing") 11 | class MTOWFixedWing(MTOW): 12 | """ 13 | Group containing the MTOW module for fixed wing UAVs 14 | """ 15 | 16 | def initialize(self): 17 | MTOW.initialize(self) 18 | self.options["propulsion_id_list"] = [FW_PROPULSION] -------------------------------------------------------------------------------- /src/fastuav/models/wires/wires_fixedwing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fixed wing wires 3 | """ 4 | import fastoad.api as oad 5 | from fastuav.constants import FW_PROPULSION 6 | from fastuav.models.wires.wires import Wires 7 | 8 | 9 | @oad.RegisterOpenMDAOSystem("fastuav.propulsion.wires.fixedwing") 10 | class WiresFixedWing(Wires): 11 | """ 12 | Group containing the fixed wing wires calculations 13 | """ 14 | 15 | def initialize(self): 16 | Wires.initialize(self) 17 | self.options["propulsion_id"] = [FW_PROPULSION] -------------------------------------------------------------------------------- /src/fastuav/models/wires/wires_multirotor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Multirotor wires 3 | """ 4 | import fastoad.api as oad 5 | from fastuav.constants import MR_PROPULSION 6 | from fastuav.models.wires.wires import Wires 7 | 8 | 9 | @oad.RegisterOpenMDAOSystem("fastuav.propulsion.wires.multirotor") 10 | class WiresMultirotor(Wires): 11 | """ 12 | Group containing the multirotor wires calculations 13 | """ 14 | 15 | def initialize(self): 16 | Wires.initialize(self) 17 | self.options["propulsion_id"] = [MR_PROPULSION] -------------------------------------------------------------------------------- /src/fastuav/models/mtow/mtow_multirotor.py: -------------------------------------------------------------------------------- 1 | """ 2 | MTOW for multirotor UAVs 3 | """ 4 | 5 | import fastoad.api as oad 6 | from fastuav.constants import MR_PROPULSION 7 | from fastuav.models.mtow.mtow import MTOW 8 | 9 | 10 | @oad.RegisterOpenMDAOSystem("fastuav.mtow.multirotor") 11 | class MTOWMultirotor(MTOW): 12 | """ 13 | Group containing the MTOW module for multirotor UAVs 14 | """ 15 | 16 | def initialize(self): 17 | MTOW.initialize(self) 18 | self.options["propulsion_id_list"] = [MR_PROPULSION] -------------------------------------------------------------------------------- /src/fastuav/missions/missions_fixedwing.yaml: -------------------------------------------------------------------------------- 1 | # Missions definition for fixed wing UAV 2 | 3 | routes: 4 | main_route: 5 | climb_part: 6 | phase_id: fixedwing_climb 7 | cruise_part: 8 | phase_id: fixedwing_cruise 9 | diversion: 10 | climb_part: 11 | phase_id: fixedwing_climb 12 | cruise_part: 13 | phase_id: fixedwing_cruise 14 | 15 | missions: 16 | sizing: 17 | parts: 18 | - route: main_route 19 | #operational: 20 | # parts: 21 | # - route: main_route 22 | # - route: diversion 23 | -------------------------------------------------------------------------------- /src/fastuav/configurations/doe_simple_model.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone MDO with LCA 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of OpenMDAO model 11 | model: 12 | missions: 13 | id: fastuav.performance.mission 14 | file_path: ../missions/missions_multirotor_doe.yaml # path to the mission definition file 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/fastuav/models/mtow/mtow_hybrid.py: -------------------------------------------------------------------------------- 1 | """ 2 | MTOW for hybrid (fixed wing VTOL) UAVs 3 | """ 4 | 5 | import fastoad.api as oad 6 | from fastuav.constants import FW_PROPULSION, MR_PROPULSION 7 | from fastuav.models.mtow.mtow import MTOW 8 | 9 | 10 | @oad.RegisterOpenMDAOSystem("fastuav.mtow.hybrid") 11 | class MTOWHybrid(MTOW): 12 | """ 13 | Group containing the MTOW module for hybrid UAVs 14 | """ 15 | 16 | def initialize(self): 17 | MTOW.initialize(self) 18 | self.options["propulsion_id_list"] = [MR_PROPULSION, FW_PROPULSION] 19 | -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/missions/missions_fixedwing.yaml: -------------------------------------------------------------------------------- 1 | # Missions definition for fixed wing UAV 2 | 3 | routes: 4 | main_route: 5 | climb_part: 6 | phase_id: fixedwing_climb 7 | cruise_part: 8 | phase_id: fixedwing_cruise 9 | diversion: 10 | climb_part: 11 | phase_id: fixedwing_climb 12 | cruise_part: 13 | phase_id: fixedwing_cruise 14 | 15 | missions: 16 | sizing: 17 | parts: 18 | - route: main_route 19 | #operational: 20 | # parts: 21 | # - route: main_route 22 | # - route: diversion 23 | -------------------------------------------------------------------------------- /src/fastuav/models/wires/wires_hybrid.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hybrid (fixed wing + VTOL) wires 3 | """ 4 | import fastoad.api as oad 5 | from fastuav.constants import FW_PROPULSION, MR_PROPULSION 6 | from fastuav.models.wires.wires import Wires 7 | 8 | 9 | @oad.RegisterOpenMDAOSystem("fastuav.propulsion.wires.hybrid") 10 | class WiresHybrid(Wires): 11 | """ 12 | Group containing the hybrid (fixed wing + VTOL) wires calculations 13 | """ 14 | 15 | def initialize(self): 16 | Wires.initialize(self) 17 | self.options["propulsion_id"] = [MR_PROPULSION, FW_PROPULSION] -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/configurations/doe_simple_model.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone MDO with LCA 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of OpenMDAO model 11 | model: 12 | missions: 13 | id: fastuav.performance.mission 14 | file_path: ../missions/missions_multirotor_doe.yaml # path to the mission definition file 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/propulsion_multirotor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Multirotor propulsion system 3 | """ 4 | 5 | import fastoad.api as oad 6 | from fastuav.constants import MR_PROPULSION 7 | from fastuav.models.propulsion.propulsion import Propulsion 8 | 9 | 10 | @oad.RegisterOpenMDAOSystem("fastuav.propulsion.multirotor") 11 | class PropulsionMultirotor(Propulsion): 12 | """ 13 | Group containing the multirotor propulsion system calculations 14 | """ 15 | 16 | def initialize(self): 17 | Propulsion.initialize(self) 18 | self.options["propulsion_id"] = [MR_PROPULSION] -------------------------------------------------------------------------------- /src/fastuav/missions/missions_hybrid.yaml: -------------------------------------------------------------------------------- 1 | # Missions definition for hybrid VTOL UAV 2 | 3 | routes: 4 | main_route: 5 | climb_part: 6 | phase_id: multirotor_climb 7 | hover_part: 8 | phase_id: hover 9 | cruise_part: 10 | phase_id: fixedwing_cruise 11 | diversion: 12 | climb_part: 13 | phase_id: fixedwing_climb 14 | cruise_part: 15 | phase_id: fixedwing_cruise 16 | 17 | missions: 18 | sizing: 19 | parts: 20 | - route: main_route 21 | #operational: 22 | # parts: 23 | # - route: main_route 24 | # - route: diversion 25 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/propulsion_fixedwing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fixed wing propulsion system 3 | """ 4 | 5 | import fastoad.api as oad 6 | from fastuav.constants import FW_PROPULSION 7 | from fastuav.models.propulsion.propulsion import Propulsion 8 | 9 | 10 | @oad.RegisterOpenMDAOSystem("fastuav.propulsion.fixedwing") 11 | class PropulsionFixedWing(Propulsion): 12 | """ 13 | Group containing the fixed wing propulsion system calculations 14 | """ 15 | 16 | def initialize(self): 17 | Propulsion.initialize(self) 18 | self.options["propulsion_id"] = [FW_PROPULSION] 19 | -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/missions/missions_hybrid.yaml: -------------------------------------------------------------------------------- 1 | # Missions definition for hybrid VTOL UAV 2 | 3 | routes: 4 | main_route: 5 | climb_part: 6 | phase_id: multirotor_climb 7 | hover_part: 8 | phase_id: hover 9 | cruise_part: 10 | phase_id: fixedwing_cruise 11 | diversion: 12 | climb_part: 13 | phase_id: fixedwing_climb 14 | cruise_part: 15 | phase_id: fixedwing_cruise 16 | 17 | missions: 18 | sizing: 19 | parts: 20 | - route: main_route 21 | #operational: 22 | # parts: 23 | # - route: main_route 24 | # - route: diversion 25 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/propulsion_hybrid.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hybrid (fixed wing VTOL) propulsion system 3 | """ 4 | 5 | import fastoad.api as oad 6 | from fastuav.constants import FW_PROPULSION, MR_PROPULSION 7 | from fastuav.models.propulsion.propulsion import Propulsion 8 | 9 | 10 | @oad.RegisterOpenMDAOSystem("fastuav.propulsion.hybrid") 11 | class PropulsionHybrid(Propulsion): 12 | """ 13 | Group containing the hybrid propulsion system calculations 14 | """ 15 | 16 | def initialize(self): 17 | Propulsion.initialize(self) 18 | self.options["propulsion_id"] = [MR_PROPULSION, FW_PROPULSION] 19 | -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/missions/missions_multirotor_doe.yaml: -------------------------------------------------------------------------------- 1 | # Missions definition for multirotor UAV 2 | 3 | routes: 4 | main_route: 5 | climb_part: 6 | phase_id: multirotor_climb 7 | hover_part: 8 | phase_id: hover 9 | cruise_part: 10 | phase_id: multirotor_cruise 11 | diversion: 12 | climb_part: 13 | phase_id: multirotor_climb 14 | hover_part: 15 | phase_id: hover 16 | cruise_part: 17 | phase_id: multirotor_cruise 18 | 19 | missions: 20 | sizing: 21 | parts: 22 | - route: main_route 23 | operational: 24 | parts: 25 | - route: main_route 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/fastuav/missions/missions_multirotor.yaml: -------------------------------------------------------------------------------- 1 | # Missions definition for multirotor UAV 2 | 3 | routes: 4 | main_route: 5 | climb_part: 6 | phase_id: multirotor_climb 7 | hover_part: 8 | phase_id: hover 9 | cruise_part: 10 | phase_id: multirotor_cruise 11 | diversion: 12 | climb_part: 13 | phase_id: multirotor_climb 14 | hover_part: 15 | phase_id: hover 16 | cruise_part: 17 | phase_id: multirotor_cruise 18 | 19 | missions: 20 | sizing: 21 | parts: 22 | - route: main_route 23 | #operational: 24 | # parts: 25 | # - route: main_route 26 | # - route: diversion 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/missions/missions_multirotor.yaml: -------------------------------------------------------------------------------- 1 | # Missions definition for multirotor UAV 2 | 3 | routes: 4 | main_route: 5 | climb_part: 6 | phase_id: multirotor_climb 7 | hover_part: 8 | phase_id: hover 9 | cruise_part: 10 | phase_id: multirotor_cruise 11 | diversion: 12 | climb_part: 13 | phase_id: multirotor_climb 14 | hover_part: 15 | phase_id: hover 16 | cruise_part: 17 | phase_id: multirotor_cruise 18 | 19 | missions: 20 | sizing: 21 | parts: 22 | - route: main_route 23 | #operational: 24 | # parts: 25 | # - route: main_route 26 | # - route: diversion 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/fastuav/data/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of FAST : A framework for rapid Overall Aircraft Design 2 | # Copyright (C) 2020 ONERA & ISAE-SUPAERO 3 | # FAST is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of FAST : A framework for rapid Overall Aircraft Design 2 | # Copyright (C) 2020 ONERA & ISAE-SUPAERO 3 | # FAST is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/missions/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of FAST : A framework for rapid Overall Aircraft Design 2 | # Copyright (C) 2020 ONERA & ISAE-SUPAERO 3 | # FAST is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/configurations/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of FAST : A framework for rapid Overall Aircraft Design 2 | # Copyright (C) 2020 ONERA & ISAE-SUPAERO 3 | # FAST is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/source_files/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of FAST : A framework for rapid Overall Aircraft Design 2 | # Copyright (C) 2020 ONERA & ISAE-SUPAERO 3 | # FAST is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /src/fastuav/configurations/.ipynb_checkpoints/ 2 | /src/fastuav/notebooks/workdir/* 3 | !/src/fastuav/notebooks/workdir/sensitivity_analysis 4 | /src/fastuav/notebooks/workdir/sensitivity_analysis/* 5 | !/src/fastuav/notebooks/workdir/sensitivity_analysis/figures 6 | /src/fastuav/notebooks/workdir/sensitivity_analysis/figures/* 7 | /src/fastuav/notebooks/.ipynb_checkpoints/ 8 | /.ipynb_checkpoints/ 9 | /src/fastuav/models/performance/.pytest_cache/ 10 | /src/fastuav/notebooks/outcmaes/ 11 | /src/fastuav/missions/.ipynb_checkpoints/ 12 | /src/fastuav/data/.ipynb_checkpoints/ 13 | /.pytest_cache/ 14 | /src/fastuav/models/propulsion/propeller/aerodynamics/surrogate_models_old.py 15 | /src/fastuav/notebooks/cases.sql 16 | /src/fastuav/configurations/cases.sql 17 | /src/fastuav/utils/postprocessing/old/* 18 | /dist/ 19 | /src/fastuav.egg-info/ 20 | /src/fastuav/notebooks/contributions_components_v2.ipynb 21 | /src/fastuav/gui/ 22 | /src/fastuav/notebooks/reports/ 23 | /src/fastuav/notebooks/04_LCA.ipynb 24 | .python-version 25 | **/__pycache__/ -------------------------------------------------------------------------------- /src/fastuav/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | Constants for various purposes. 3 | """ 4 | 5 | # Concepts 6 | FW_PROPULSION = "fixedwing" 7 | MR_PROPULSION = "multirotor" 8 | PROPULSION_ID_LIST = [MR_PROPULSION, FW_PROPULSION] 9 | 10 | # Missions 11 | PHASE_ID_TAG = "phase_id" 12 | ROUTE_TAG = "route" 13 | PARTS_TAG = "parts" 14 | TAKEOFF_PART_TAG = "takeoff_part" 15 | CLIMB_PART_TAG = "climb_part" 16 | CRUISE_PART_TAG = "cruise_part" 17 | DESCENT_PART_TAG = "descent_part" 18 | HOVER_PART_TAG = "hover_part" 19 | MISSION_DEFINITION_TAG = "missions" 20 | ROUTE_DEFINITION_TAG = "routes" 21 | SIZING_MISSION_TAG = "sizing" 22 | TAKEOFF_TAG = "takeoff" 23 | VERTICAL_TAKEOFF_TAG = "vertical_takeoff" 24 | LAUNCHER_TAKEOFF_TAG = "launcher_takeoff" 25 | STANDARD_TAKEOFF_TAG = "standard_takeoff" 26 | CLIMB_TAG = "climb" 27 | MR_CLIMB_TAG = "multirotor_climb" 28 | FW_CLIMB_TAG = "fixedwing_climb" 29 | CRUISE_TAG = "cruise" 30 | MR_CRUISE_TAG = "multirotor_cruise" 31 | FW_CRUISE_TAG = "fixedwing_cruise" 32 | HOVER_TAG = "hover" 33 | PHASE_TAGS_LIST = [TAKEOFF_TAG, CLIMB_TAG, CRUISE_TAG, HOVER_TAG] -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/missions/missions_multirotor_off_design.yaml: -------------------------------------------------------------------------------- 1 | # Missions definition for multirotor UAV 2 | 3 | routes: 4 | main_route: 5 | climb_part: 6 | phase_id: multirotor_climb 7 | hover_part: 8 | phase_id: hover 9 | cruise_part: 10 | phase_id: multirotor_cruise 11 | diversion: 12 | hover_part: 13 | phase_id: hover 14 | cruise_part: 15 | phase_id: multirotor_cruise 16 | route_1: 17 | climb_part: 18 | phase_id: multirotor_climb 19 | cruise_part: 20 | phase_id: multirotor_cruise 21 | route_2: 22 | hover_part: 23 | phase_id: hover 24 | 25 | missions: 26 | sizing: # it is mandatory to define this mission 27 | parts: 28 | - route: main_route 29 | 30 | operational_1: # first off-desing mission to evaluate 31 | parts: 32 | - route: main_route 33 | - route: diversion 34 | 35 | operational_2: # second off-design mission to evaluate 36 | parts: 37 | - route: route_1 38 | - route: route_2 39 | # ... 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/fastuav/models/stability/stability_fixedwing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fixed wing UAV stability 3 | """ 4 | import fastoad.api as oad 5 | import openmdao.api as om 6 | 7 | from fastuav.models.stability.static_longitudinal.static_margin import StaticMargin, StaticMarginConstraints 8 | from fastuav.models.stability.static_longitudinal.neutral_point import NeutralPoint 9 | from fastuav.models.stability.static_longitudinal.center_of_gravity.cog import CenterOfGravity 10 | from fastuav.constants import FW_PROPULSION 11 | 12 | 13 | @oad.RegisterOpenMDAOSystem("fastuav.stability.fixedwing") 14 | class StabilityFixedWing(om.Group): 15 | """ 16 | Group containing the fixed wing stability calculations 17 | """ 18 | 19 | def setup(self): 20 | self.add_subsystem("center_of_gravity", CenterOfGravity(propulsion_id_list=[FW_PROPULSION]), promotes=["*"]) 21 | self.add_subsystem("neutral_point", NeutralPoint(), promotes=["*"]) 22 | self.add_subsystem("static_margin", StaticMargin(), promotes=["*"]) 23 | self.add_subsystem("constraints", StaticMarginConstraints(), promotes=["*"]) 24 | -------------------------------------------------------------------------------- /src/fastuav/models/stability/stability_hybrid.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hybrid VTOL UAV stability 3 | """ 4 | import fastoad.api as oad 5 | import openmdao.api as om 6 | 7 | from fastuav.models.stability.static_longitudinal.static_margin import StaticMargin, StaticMarginConstraints 8 | from fastuav.models.stability.static_longitudinal.neutral_point import NeutralPoint 9 | from fastuav.models.stability.static_longitudinal.center_of_gravity.cog import CenterOfGravity 10 | from fastuav.constants import PROPULSION_ID_LIST 11 | 12 | 13 | @oad.RegisterOpenMDAOSystem("fastuav.stability.hybrid") 14 | class StabilityFixedWing(om.Group): 15 | """ 16 | Group containing the fixed wing stability calculations 17 | """ 18 | 19 | def setup(self): 20 | self.add_subsystem("center_of_gravity", CenterOfGravity(propulsion_id_list=PROPULSION_ID_LIST), promotes=["*"]) 21 | self.add_subsystem("neutral_point", NeutralPoint(), promotes=["*"]) 22 | self.add_subsystem("static_margin", StaticMargin(), promotes=["*"]) 23 | self.add_subsystem("constraints", StaticMarginConstraints(), promotes=["*"]) 24 | -------------------------------------------------------------------------------- /src/fastuav/models/structures/structures_fixedwing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fixed Wing Structures 3 | """ 4 | import fastoad.api as oad 5 | import openmdao.api as om 6 | from fastuav.models.structures.wing.wing import WingStructuresFW 7 | from fastuav.models.structures.tails import HorizontalTailStructures, VerticalTailStructures 8 | from fastuav.models.structures.fuselage import FuselageStructures 9 | 10 | 11 | @oad.RegisterOpenMDAOSystem("fastuav.structures.fixedwing") 12 | class Structures(om.Group): 13 | """ 14 | Group containing the airframe structural analysis and weights calculation 15 | """ 16 | 17 | def initialize(self): 18 | self.options.declare("spar_model", default="pipe", values=["pipe", "I_beam"]) 19 | 20 | def setup(self): 21 | self.add_subsystem( 22 | "wing", WingStructuresFW(spar_model=self.options["spar_model"]), promotes=["*"] 23 | ) 24 | self.add_subsystem("horizontal_tail", HorizontalTailStructures(), promotes=["*"]) 25 | self.add_subsystem("vertical_tail", VerticalTailStructures(), promotes=["*"]) 26 | self.add_subsystem("fuselage", FuselageStructures(), promotes=["*"]) 27 | -------------------------------------------------------------------------------- /src/fastuav/utils/postprocessing/sensitivity_analysis/sobol_plot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Adapted from SALib plotting module 3 | """ 4 | 5 | import pandas as pd 6 | 7 | # magic string indicating DF columns holding conf bound values 8 | CONF_COLUMN = "_conf" 9 | 10 | 11 | def sobol_plot(Si, ax=None): 12 | """Create bar chart of results. 13 | 14 | Parameters 15 | ---------- 16 | * Si_df: pd.DataFrame, of sensitivity results 17 | 18 | Returns 19 | ---------- 20 | * ax : matplotlib axes object 21 | """ 22 | if len(Si.to_df()) == 2: 23 | total, first = Si.to_df() 24 | else: 25 | total, first, _ = Si.to_df() 26 | Si_df = pd.concat([total, first], axis=1) 27 | 28 | conf_cols = Si_df.columns.str.contains(CONF_COLUMN) 29 | 30 | confs = Si_df.loc[:, conf_cols] 31 | confs.columns = [c.replace(CONF_COLUMN, "") for c in confs.columns] 32 | confs = confs.rename(columns={"S1": "first-order", "ST": "total-order"}) 33 | 34 | Sis = Si_df.loc[:, ~conf_cols] 35 | Sis = Sis.rename(columns={"S1": "first-order", "ST": "total-order"}) 36 | 37 | ax = Sis.plot(kind="bar", yerr=confs, ax=ax, ylabel="Sobol' Index") 38 | return ax 39 | -------------------------------------------------------------------------------- /src/fastuav/configurations/multirotor_model.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone Model # Single evaluation (no MDA or MDO) 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-9, optimizer='SLSQP') 12 | 13 | # Definition of OpenMDAO model 14 | model: 15 | scenarios: 16 | id: fastuav.scenarios.multirotor 17 | propulsion: 18 | id: fastuav.propulsion.multirotor 19 | gearbox: False 20 | geometry: 21 | id: fastuav.geometry.multirotor 22 | structures: 23 | id: fastuav.structures.multirotor 24 | wires: 25 | id: fastuav.propulsion.wires.multirotor 26 | mtow: 27 | id: fastuav.mtow.multirotor 28 | performance: 29 | endurance: 30 | id: fastuav.performance.endurance.multirotor 31 | missions: 32 | id: fastuav.performance.mission 33 | file_path: ../missions/missions_multirotor.yaml -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/configurations/multirotor_model.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone Model # Single evaluation (no MDA or MDO) 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-9, optimizer='SLSQP') 12 | 13 | # Definition of OpenMDAO model 14 | model: 15 | scenarios: 16 | id: fastuav.scenarios.multirotor 17 | propulsion: 18 | id: fastuav.propulsion.multirotor 19 | gearbox: False 20 | geometry: 21 | id: fastuav.geometry.multirotor 22 | structures: 23 | id: fastuav.structures.multirotor 24 | wires: 25 | id: fastuav.propulsion.wires.multirotor 26 | mtow: 27 | id: fastuav.mtow.multirotor 28 | performance: 29 | endurance: 30 | id: fastuav.performance.endurance.multirotor 31 | missions: 32 | id: fastuav.performance.mission 33 | file_path: ../missions/missions_multirotor.yaml -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_call: 6 | push: 7 | branches: 8 | - '**' 9 | tags-ignore: 10 | - '**' 11 | pull_request: 12 | branches: 13 | - '**' 14 | jobs: 15 | tests: 16 | strategy: 17 | matrix: 18 | python-version: [ '3.9', '3.10', '3.11', '3.12' ] 19 | os: ${{ fromJson(github.event_name == 'push' && '["ubuntu-latest"]' || '["ubuntu-latest","windows-latest","macos-latest"]') }} 20 | runs-on: ${{ matrix.os }} 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Install poetry 26 | run: pipx install poetry==${{ vars.POETRY_VERSION }} 27 | 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | cache: 'poetry' 33 | - name: Activate environment and install dependencies 34 | run: | 35 | poetry env use ${{ matrix.python-version }} 36 | poetry install 37 | 38 | - name: Notebook tests 39 | run: poetry run pytest --no-cov --nbval-lax -p no:python src/fastuav/notebooks 40 | shell: bash -------------------------------------------------------------------------------- /.github/workflows/watchdog_tests.yml: -------------------------------------------------------------------------------- 1 | name: watchdog-tests 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_call: 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ 'main' ] 9 | schedule: 10 | - cron: '0 4 * * 1' # Monday 06:00 (UTC+2) 11 | 12 | jobs: 13 | tests: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | python-version: [ '3.9', '3.10', '3.11', '3.12' ] 18 | os: [ ubuntu-latest, windows-latest, macos-latest ] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Install poetry 24 | run: pipx install poetry==${{ vars.POETRY_VERSION }} 25 | 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | cache: 'poetry' 31 | 32 | - name: Install latest version of dependencies 33 | run: | 34 | poetry env use ${{ matrix.python-version }} 35 | poetry update 36 | poetry install 37 | 38 | - name: Notebook tests 39 | run: poetry run pytest --no-cov --nbval-lax -p no:python src/fastuav/notebooks 40 | shell: bash -------------------------------------------------------------------------------- /src/fastuav/data/catalogues/ESC/Non-Dominated-ESC.csv: -------------------------------------------------------------------------------- 1 | ;TYPE;Model;Imax_A;Weight_g;Vmax_V;Pmax_W;Dominated 2 | 7;KOSMIK;Kosmik 200+ HV ;200;200;51.8;10360;0 3 | 8;YGE;YGE 7 S ;7;0.7;7.4;52;0 4 | 9;YGE;YGE 8 S ;8;4.9;11.1;89;0 5 | 10;YGE;YGE 12 S ;12;6;11.1;133;0 6 | 11;YGE;YGE 18 ;18;14;14.8;266;0 7 | 12;YGE;YGE 30 ;30;21;14.8;444;0 8 | 13;YGE;YGE 40 ;40;35;22.2;888;0 9 | 14;YGE;YGE 60 ;60;35;22.2;1332;0 10 | 18;YGE;YGE 160 FAI ;160;79;22.2;3552;0 11 | 19;YGE;YGE 60 HV ;60;49;44.4;2664;0 12 | 20;YGE;YGE 90 HV ;90;79;44.4;3996;0 13 | 21;YGE;YGE 150 HV FAI ;150;89;44.4;6660;0 14 | 22;YGE;YGE 200 HV FAI ;200;119;44.4;8880;0 15 | 25;SPIN OPTO;MasterSPIN 11 ;11;12;14.8;163;0 16 | 26;SPIN OPTO;MasterSPIN 22 ;22;18;14.8;326;0 17 | 27;SPIN OPTO;MasterSPIN 33 ;33;30;18.5;611;0 18 | 32;SPIN OPTO;MasterSPIN 48 Opto ;48;45;37;1776;0 19 | 33;SPIN OPTO;MasterSPIN 75 0pto ;75;55;37;2775;0 20 | 38;SPIN OPTO;MasterSPIN 220 Opto ;220;460;51.8;11396;0 21 | 39;SPIN OPTO;SPIN 11 ;11;12;14.8;163;0 22 | 41;SPIN OPTO;SPIN 33 ;33;32;18.5;611;0 23 | 45;SPIN OPTO;SPIN 44 OPTO ;44;35;22.2;977;0 24 | 46;SPIN OPTO;SPIN 66 OPTO ;70;45;22.2;1554;0 25 | 47;SPIN OPTO;SPIN 48 OPTO ;48;45;37;1776;0 26 | 48;SPIN OPTO;SPIN 75 OPTO ;75;55;37;2775;0 27 | 53;SPIN OPTO;SPIN 300 OPTO ;220;360;51.8;11396;0 28 | -------------------------------------------------------------------------------- /src/fastuav/models/structures/structures_hybrid.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hybrid VTOL Structures 3 | """ 4 | import fastoad.api as oad 5 | import openmdao.api as om 6 | from fastuav.models.structures.wing.wing import WingStructuresHybrid 7 | from fastuav.models.structures.tails import HorizontalTailStructures, VerticalTailStructures 8 | from fastuav.models.structures.fuselage import FuselageStructures 9 | from fastuav.models.structures.structures_multirotor import ArmsWeight 10 | 11 | 12 | @oad.RegisterOpenMDAOSystem("fastuav.structures.hybrid") 13 | class Structures(om.Group): 14 | """ 15 | Group containing the airframe structural analysis and weights calculation 16 | """ 17 | 18 | def initialize(self): 19 | self.options.declare("spar_model", default="pipe", values=["pipe", "I_beam"]) 20 | 21 | def setup(self): 22 | self.add_subsystem( 23 | "wing", WingStructuresHybrid(spar_model=self.options["spar_model"]), promotes=["*"] 24 | ) 25 | self.add_subsystem("horizontal_tail", HorizontalTailStructures(), promotes=["*"]) 26 | self.add_subsystem("vertical_tail", VerticalTailStructures(), promotes=["*"]) 27 | self.add_subsystem("fuselage", FuselageStructures(), promotes=["*"]) 28 | self.add_subsystem("vtol_arms", ArmsWeight(), promotes=["*"]) 29 | -------------------------------------------------------------------------------- /src/fastuav/configurations/hybrid_model.yaml: -------------------------------------------------------------------------------- 1 | title: Hybrid VTOL Drone MDO 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-9, optimizer='SLSQP') 12 | 13 | # Definition of OpenMDAO model 14 | model: 15 | scenarios: 16 | id: fastuav.scenarios.hybrid 17 | propulsion: 18 | id: fastuav.propulsion.hybrid 19 | gearbox: False 20 | geometry: 21 | id: fastuav.geometry.hybrid 22 | structures: 23 | id: fastuav.structures.hybrid 24 | spar_model: "I_beam" # "I_beam" or "pipe" 25 | aerodynamics: 26 | id: fastuav.aerodynamics.hybrid 27 | wires: 28 | id: fastuav.propulsion.wires.hybrid 29 | mtow: 30 | id: fastuav.mtow.hybrid 31 | stability: 32 | id: fastuav.stability.hybrid 33 | performance: 34 | cruise_range: 35 | id: fastuav.performance.endurance.hybrid 36 | missions: 37 | id: fastuav.performance.mission 38 | file_path: ../missions/missions_hybrid.yaml 39 | -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/configurations/hybrid_model.yaml: -------------------------------------------------------------------------------- 1 | title: Hybrid VTOL Drone MDO 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-9, optimizer='SLSQP') 12 | 13 | # Definition of OpenMDAO model 14 | model: 15 | scenarios: 16 | id: fastuav.scenarios.hybrid 17 | propulsion: 18 | id: fastuav.propulsion.hybrid 19 | gearbox: False 20 | geometry: 21 | id: fastuav.geometry.hybrid 22 | structures: 23 | id: fastuav.structures.hybrid 24 | spar_model: "I_beam" # "I_beam" or "pipe" 25 | aerodynamics: 26 | id: fastuav.aerodynamics.hybrid 27 | wires: 28 | id: fastuav.propulsion.wires.hybrid 29 | mtow: 30 | id: fastuav.mtow.hybrid 31 | stability: 32 | id: fastuav.stability.hybrid 33 | performance: 34 | cruise_range: 35 | id: fastuav.performance.endurance.hybrid 36 | missions: 37 | id: fastuav.performance.mission 38 | file_path: ../missions/missions_hybrid.yaml 39 | -------------------------------------------------------------------------------- /src/fastuav/configurations/fixedwing_model.yaml: -------------------------------------------------------------------------------- 1 | title: Fixed-Wing Drone MDO 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-9, optimizer='SLSQP') 12 | 13 | # Definition of OpenMDAO model 14 | model: 15 | scenarios: 16 | id: fastuav.scenarios.fixedwing 17 | propulsion: 18 | id: fastuav.propulsion.fixedwing 19 | gearbox: False 20 | geometry: 21 | id: fastuav.geometry.fixedwing 22 | structures: 23 | id: fastuav.structures.fixedwing 24 | spar_model: "I_beam" # "I_beam" or "pipe" 25 | aerodynamics: 26 | id: fastuav.aerodynamics.fixedwing 27 | wires: 28 | id: fastuav.propulsion.wires.fixedwing 29 | mtow: 30 | id: fastuav.mtow.fixedwing 31 | stability: 32 | id: fastuav.stability.fixedwing 33 | performance: 34 | cruise_range: 35 | id: fastuav.performance.endurance.fixedwing 36 | missions: 37 | id: fastuav.performance.mission 38 | file_path: ../missions/missions_fixedwing.yaml 39 | 40 | -------------------------------------------------------------------------------- /src/fastuav/notebooks/README.md: -------------------------------------------------------------------------------- 1 | 📚 Notebooks 2 | ================== 3 | 4 | This folder contains the set of notebook tutorials (in Python) to help the user get more familiar with FAST-UAV. 5 | * [0) Tutorial](0_Tutorial.ipynb): provides the basics for using FAST-UAV and its main features. 6 | * [1.a) Multirotor Design](1_Multirotor_Design.ipynb): example notebook for the design of a multirotor UAV. 7 | * [1.b) Mission Performance](1b_Mission_performance.ipynb): example notebook to estimate the performance of a pre-defined multirotor UAV for an off-design mission. 8 | * [1.c) Multirotor Design with catalogues](1c_Multirotor_Design_Catalogues.ipynb): example notebook for the design of a multirotor UAV using off-the-shelf components instead of continuous models. 9 | * [2) Fixed-Wing Design](2_FixedWing_Design.ipynb): example notebook for the design of a fixed-wing UAV. 10 | * [3) Hybrid VTOL Design](3_HybridVTOL_Design.ipynb): example notebook for the design of a hybrid VTOL (quadplane) UAV. 11 | * [4) Uncertainty Analysis](4_Uncertainty_analysis.ipynb): provides the means to perform an uncertainty analysis of the design process. 12 | * [5) Design of Experiments](5_Design_of_experiments_Tutorial.ipynb): provides an overview of how to run a design of experiments to quickly evaluate a range of specifications and designs. 13 | 14 | 💡 Feel free to create your own notebooks for your specific studies! -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/configurations/fixedwing_model.yaml: -------------------------------------------------------------------------------- 1 | title: Fixed-Wing Drone MDO 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-9, optimizer='SLSQP') 12 | 13 | # Definition of OpenMDAO model 14 | model: 15 | scenarios: 16 | id: fastuav.scenarios.fixedwing 17 | propulsion: 18 | id: fastuav.propulsion.fixedwing 19 | gearbox: False 20 | geometry: 21 | id: fastuav.geometry.fixedwing 22 | structures: 23 | id: fastuav.structures.fixedwing 24 | spar_model: "I_beam" # "I_beam" or "pipe" 25 | aerodynamics: 26 | id: fastuav.aerodynamics.fixedwing 27 | wires: 28 | id: fastuav.propulsion.wires.fixedwing 29 | mtow: 30 | id: fastuav.mtow.fixedwing 31 | stability: 32 | id: fastuav.stability.fixedwing 33 | performance: 34 | cruise_range: 35 | id: fastuav.performance.endurance.fixedwing 36 | missions: 37 | id: fastuav.performance.mission 38 | file_path: ../missions/missions_fixedwing.yaml 39 | 40 | -------------------------------------------------------------------------------- /src/fastuav/configurations/multirotor_model_cots.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone Model with COTS components # Single evaluation (no MDA or MDO) 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-6, optimizer='SLSQP') 12 | # driver: om.pyOptSparseDriver(optimizer='SLSQP') # not supported yet for windows users 13 | # driver: CMAESDriver() 14 | 15 | # Definition of OpenMDAO model 16 | model: 17 | scenarios: 18 | id: fastuav.scenarios.multirotor 19 | propulsion: 20 | id: fastuav.propulsion.multirotor 21 | gearbox: False 22 | off_the_shelf_propeller: True 23 | geometry: 24 | id: fastuav.geometry.multirotor 25 | structures: 26 | id: fastuav.structures.multirotor 27 | wires: 28 | id: fastuav.propulsion.wires.multirotor 29 | mtow: 30 | id: fastuav.mtow.multirotor 31 | performance: 32 | endurance: 33 | id: fastuav.performance.endurance.multirotor 34 | missions: 35 | id: fastuav.performance.mission 36 | file_path: ../missions/missions_multirotor.yaml -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/configurations/multirotor_model_cots.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone Model with COTS components # Single evaluation (no MDA or MDO) 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-6, optimizer='SLSQP') 12 | # driver: om.pyOptSparseDriver(optimizer='SLSQP') # not supported yet for windows users 13 | # driver: CMAESDriver() 14 | 15 | # Definition of OpenMDAO model 16 | model: 17 | scenarios: 18 | id: fastuav.scenarios.multirotor 19 | propulsion: 20 | id: fastuav.propulsion.multirotor 21 | gearbox: False 22 | off_the_shelf_propeller: True 23 | geometry: 24 | id: fastuav.geometry.multirotor 25 | structures: 26 | id: fastuav.structures.multirotor 27 | wires: 28 | id: fastuav.propulsion.wires.multirotor 29 | mtow: 30 | id: fastuav.mtow.multirotor 31 | performance: 32 | endurance: 33 | id: fastuav.performance.endurance.multirotor 34 | missions: 35 | id: fastuav.performance.mission 36 | file_path: ../missions/missions_multirotor.yaml -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/esc/esc.py: -------------------------------------------------------------------------------- 1 | """ 2 | ESC component 3 | """ 4 | import fastoad.api as oad 5 | import openmdao.api as om 6 | from fastuav.models.propulsion.esc.definition_parameters import ESCDefinitionParameters 7 | from fastuav.models.propulsion.esc.estimation_models import ESCEstimationModels 8 | from fastuav.models.propulsion.esc.catalogue import ESCCatalogueSelection 9 | from fastuav.models.propulsion.esc.performance_analysis import ESCPerformanceGroup 10 | from fastuav.models.propulsion.esc.constraints import ESCConstraints 11 | 12 | 13 | @oad.RegisterOpenMDAOSystem("fastuav.propulsion.esc") 14 | class ESC(om.Group): 15 | """ 16 | Group containing the Electronic Speed Controller analysis. 17 | """ 18 | 19 | def initialize(self): 20 | self.options.declare("off_the_shelf", default=False, types=bool) 21 | 22 | def setup(self): 23 | self.add_subsystem("definition_parameters", ESCDefinitionParameters(), promotes=["*"]) 24 | self.add_subsystem("estimation_models", ESCEstimationModels(), promotes=["*"]) 25 | self.add_subsystem("catalogue_selection" if self.options["off_the_shelf"] else "skip_catalogue_selection", 26 | ESCCatalogueSelection(off_the_shelf=self.options["off_the_shelf"]), 27 | promotes=["*"], 28 | ) 29 | self.add_subsystem("performance_analysis", ESCPerformanceGroup(), promotes=["*"]) 30 | self.add_subsystem("constraints", ESCConstraints(), promotes=["*"]) 31 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/motor/motor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Motor component 3 | """ 4 | import fastoad.api as oad 5 | import openmdao.api as om 6 | from fastuav.models.propulsion.motor.definition_parameters import MotorDefinitionParameters 7 | from fastuav.models.propulsion.motor.estimation_models import MotorEstimationModels 8 | from fastuav.models.propulsion.motor.catalogue import MotorCatalogueSelection 9 | from fastuav.models.propulsion.motor.performance_analysis import MotorPerformanceGroup 10 | from fastuav.models.propulsion.motor.constraints import MotorConstraints 11 | 12 | 13 | @oad.RegisterOpenMDAOSystem("fastuav.propulsion.motor") 14 | class Motor(om.Group): 15 | """ 16 | Group containing the Motor MDA. 17 | """ 18 | 19 | def initialize(self): 20 | self.options.declare("off_the_shelf", default=False, types=bool) 21 | 22 | def setup(self): 23 | self.add_subsystem("definition_parameters", MotorDefinitionParameters(), promotes=["*"]) 24 | self.add_subsystem("estimation_models", MotorEstimationModels(), promotes=["*"]) 25 | self.add_subsystem("catalogue_selection" if self.options["off_the_shelf"] else "skip_catalogue_selection", 26 | MotorCatalogueSelection(off_the_shelf=self.options["off_the_shelf"]), 27 | promotes=["*"], 28 | ) 29 | self.add_subsystem("performance_analysis", MotorPerformanceGroup(), promotes=["*"]) 30 | self.add_subsystem("constraints", MotorConstraints(), promotes=["*"]) 31 | -------------------------------------------------------------------------------- /src/fastuav/models/add_ons/sample_discipline.py: -------------------------------------------------------------------------------- 1 | """ 2 | FAST - Copyright (c) 2016 ONERA ISAE. 3 | """ 4 | 5 | # This file is part of FAST : A framework for rapid Overall Aircraft Design 6 | # Copyright (C) 2020 ONERA & ISAE-SUPAERO 7 | # FAST is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import openmdao.api as om 19 | import numpy as np 20 | import fastoad.api as oad 21 | 22 | 23 | @oad.RegisterOpenMDAOSystem("fastuav.plugin.sample_discipline") 24 | class SampleDiscipline(om.ExplicitComponent): 25 | """ 26 | Sample discipline to give an example of how to register a custom module. 27 | """ 28 | 29 | def setup(self): 30 | self.add_input("sample_input", val=np.nan, units="kg") 31 | 32 | self.add_output("sample_output", units="kg") 33 | 34 | def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): 35 | outputs["sample_output"] = 2.0 * inputs["sample_input"] -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/energy/battery/battery.py: -------------------------------------------------------------------------------- 1 | """ 2 | Battery component 3 | """ 4 | import fastoad.api as oad 5 | import openmdao.api as om 6 | from fastuav.models.propulsion.energy.battery.definition_parameters import ( 7 | BatteryDefinitionParameters, 8 | ) 9 | from fastuav.models.propulsion.energy.battery.estimation_models import BatteryEstimationModels 10 | from fastuav.models.propulsion.energy.battery.catalogue import BatteryCatalogueSelection 11 | from fastuav.models.propulsion.energy.battery.performance_analysis import BatteryPerformanceGroup 12 | from fastuav.models.propulsion.energy.battery.constraints import BatteryConstraints 13 | 14 | 15 | @oad.RegisterOpenMDAOSystem("fastuav.propulsion.battery") 16 | class Battery(om.Group): 17 | """ 18 | Group containing the Battery MDA. 19 | """ 20 | 21 | def initialize(self): 22 | self.options.declare("off_the_shelf", default=False, types=bool) 23 | 24 | def setup(self): 25 | self.add_subsystem("definition_parameters", BatteryDefinitionParameters(), promotes=["*"]) 26 | self.add_subsystem("estimation_models", BatteryEstimationModels(), promotes=["*"]) 27 | self.add_subsystem("catalogue_selection" if self.options["off_the_shelf"] else "skip_catalogue_selection", 28 | BatteryCatalogueSelection(off_the_shelf=self.options["off_the_shelf"]), 29 | promotes=["*"], 30 | ) 31 | self.add_subsystem("performance_analysis", BatteryPerformanceGroup(), promotes=["*"]) 32 | self.add_subsystem("constraints", BatteryConstraints(), promotes=["*"]) 33 | -------------------------------------------------------------------------------- /src/fastuav/models/scenarios/scenarios_multirotor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sizing scenarios definition for multirotor drones. 3 | The sizing scenarios return the thrusts and loads requirements to size the UAV. 4 | The sizing scenarios are extracted from a sizing mission defined by the user. 5 | """ 6 | import fastoad.api as oad 7 | import openmdao.api as om 8 | from fastuav.models.mtow.mtow import MtowGuess 9 | from fastuav.models.geometry.geometry_multirotor import ProjectedAreasGuess 10 | from fastuav.models.scenarios.thrust.takeoff import VerticalTakeoffThrust 11 | from fastuav.models.scenarios.thrust.cruise import MultirotorCruiseThrust 12 | from fastuav.models.scenarios.thrust.climb import MultirotorClimbThrust 13 | from fastuav.models.scenarios.thrust.hover import HoverThrust 14 | 15 | 16 | @oad.RegisterOpenMDAOSystem("fastuav.scenarios.multirotor") 17 | class SizingScenariosMultirotor(om.Group): 18 | """ 19 | Sizing scenarios definition for multirotor configurations 20 | """ 21 | 22 | def setup(self): 23 | preliminary = self.add_subsystem("preliminary", om.Group(), promotes=["*"]) 24 | preliminary.add_subsystem("mtow_guess", MtowGuess(), promotes=["*"]) 25 | preliminary.add_subsystem("areas_guess", ProjectedAreasGuess(), promotes=["*"]) 26 | 27 | thrust = self.add_subsystem("thrust", om.Group(), promotes=["*"]) 28 | thrust.add_subsystem("hover", HoverThrust(), promotes=["*"]) 29 | thrust.add_subsystem("takeoff", VerticalTakeoffThrust(), promotes=["*"]) 30 | thrust.add_subsystem("climb", MultirotorClimbThrust(), promotes=["*"]) 31 | thrust.add_subsystem("cruise", MultirotorCruiseThrust(), promotes=["*"]) 32 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/propeller/propeller.py: -------------------------------------------------------------------------------- 1 | """ 2 | Propeller component 3 | """ 4 | import openmdao.api as om 5 | from fastuav.models.propulsion.propeller.definition_parameters import PropellerDefinitionParameters 6 | from fastuav.models.propulsion.propeller.estimation_models import PropellerEstimationModels 7 | from fastuav.models.propulsion.propeller.catalogue import PropellerCatalogueSelection 8 | from fastuav.models.propulsion.propeller.performance_analysis import PropellerPerformanceGroup 9 | from fastuav.models.propulsion.propeller.constraints import PropellerConstraints 10 | 11 | 12 | class Propeller(om.Group): 13 | """ 14 | Group containing the Propeller MDA. 15 | """ 16 | 17 | def initialize(self): 18 | self.options.declare("off_the_shelf", default=False, types=bool) 19 | 20 | def setup(self): 21 | self.add_subsystem("definition_parameters", PropellerDefinitionParameters(), promotes=["*"]) 22 | self.add_subsystem("estimation_models", 23 | PropellerEstimationModels(), 24 | promotes=["*"]) 25 | self.add_subsystem("catalogue_selection" if self.options["off_the_shelf"] else "skip_catalogue_selection", 26 | PropellerCatalogueSelection(off_the_shelf=self.options["off_the_shelf"]), 27 | promotes=["*"]) 28 | self.add_subsystem("performance_analysis", 29 | PropellerPerformanceGroup(), 30 | promotes=["*"]) 31 | self.add_subsystem("constraints", 32 | PropellerConstraints(), 33 | promotes=["*"]) 34 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/esc/estimation_models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Estimation models for the Electronic Speed Controller (ESC) 3 | """ 4 | import openmdao.api as om 5 | import numpy as np 6 | from fastuav.utils.uncertainty import add_subsystem_with_deviation 7 | 8 | 9 | class ESCEstimationModels(om.Group): 10 | """ 11 | Group containing the estimation models for the Electronic Speed Controller. 12 | Estimation models take a reduced set of definition parameters and estimate the main component characteristics from it. 13 | """ 14 | 15 | def setup(self): 16 | add_subsystem_with_deviation( 17 | self, 18 | "weight", 19 | Weight(), 20 | uncertain_outputs={"data:weight:propulsion:esc:mass:estimated": "kg"}, 21 | ) 22 | 23 | 24 | class Weight(om.ExplicitComponent): 25 | """ 26 | Computes ESC weight 27 | """ 28 | 29 | def setup(self): 30 | self.add_input("models:weight:propulsion:esc:mass:reference", val=np.nan, units="kg") 31 | self.add_input("models:propulsion:esc:power:reference", val=np.nan, units="W") 32 | self.add_input("data:propulsion:esc:power:max:estimated", val=np.nan, units="W") 33 | self.add_output("data:weight:propulsion:esc:mass:estimated", units="kg") 34 | 35 | def setup_partials(self): 36 | # Finite difference all partials. 37 | self.declare_partials("*", "*", method="fd") 38 | 39 | def compute(self, inputs, outputs): 40 | m_esc_ref = inputs["models:weight:propulsion:esc:mass:reference"] 41 | P_esc_ref = inputs["models:propulsion:esc:power:reference"] 42 | P_esc = inputs["data:propulsion:esc:power:max:estimated"] 43 | 44 | m_esc = m_esc_ref * (P_esc / P_esc_ref) # [kg] Mass ESC 45 | 46 | outputs["data:weight:propulsion:esc:mass:estimated"] = m_esc 47 | -------------------------------------------------------------------------------- /src/fastuav/configurations/multirotor_mda.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone MDA # only consistency constraints are solved. No design optimization. 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-3, optimizer='SLSQP', maxiter=50) 12 | 13 | # Definition of OpenMDAO model 14 | model: 15 | scenarios: 16 | id: fastuav.scenarios.multirotor 17 | propulsion: 18 | id: fastuav.propulsion.multirotor 19 | off_the_shelf_propeller: False 20 | geometry: 21 | id: fastuav.geometry.multirotor 22 | structures: 23 | id: fastuav.structures.multirotor 24 | wires: 25 | id: fastuav.propulsion.wires.multirotor 26 | mtow: 27 | id: fastuav.mtow.multirotor 28 | performance: 29 | endurance: 30 | id: fastuav.performance.endurance.multirotor 31 | missions: 32 | id: fastuav.performance.mission 33 | file_path: ../missions/missions_multirotor.yaml 34 | 35 | optimization: 36 | design_variables: 37 | - name: optimization:variables:weight:mtow:k # over estimation coefficient on the mass (to solve the mass consistency constraint) 38 | upper: 40.0 39 | lower: 1.0 40 | 41 | constraints: 42 | - name: optimization:constraints:weight:mtow:consistency # consistency constraint on the drone mass 43 | lower: 0.0 44 | 45 | objective: 46 | # MASS MINIMIZATION (this objective enables to transform the inequality constraint into an equality) 47 | - name: data:weight:mtow 48 | scaler: 1e-1 -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/configurations/multirotor_mda.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone MDA # only consistency constraints are solved. No design optimization. 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-3, optimizer='SLSQP', maxiter=50) 12 | 13 | # Definition of OpenMDAO model 14 | model: 15 | scenarios: 16 | id: fastuav.scenarios.multirotor 17 | propulsion: 18 | id: fastuav.propulsion.multirotor 19 | off_the_shelf_propeller: False 20 | geometry: 21 | id: fastuav.geometry.multirotor 22 | structures: 23 | id: fastuav.structures.multirotor 24 | wires: 25 | id: fastuav.propulsion.wires.multirotor 26 | mtow: 27 | id: fastuav.mtow.multirotor 28 | performance: 29 | endurance: 30 | id: fastuav.performance.endurance.multirotor 31 | missions: 32 | id: fastuav.performance.mission 33 | file_path: ../missions/missions_multirotor.yaml 34 | 35 | optimization: 36 | design_variables: 37 | - name: optimization:variables:weight:mtow:k # over estimation coefficient on the mass (to solve the mass consistency constraint) 38 | upper: 40.0 39 | lower: 1.0 40 | 41 | constraints: 42 | - name: optimization:constraints:weight:mtow:consistency # consistency constraint on the drone mass 43 | lower: 0.0 44 | 45 | objective: 46 | # MASS MINIMIZATION (this objective enables to transform the inequality constraint into an equality) 47 | - name: data:weight:mtow 48 | scaler: 1e-1 -------------------------------------------------------------------------------- /src/fastuav/models/scenarios/thrust/hover.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hover scenarios 3 | """ 4 | 5 | import numpy as np 6 | from scipy.constants import g 7 | import openmdao.api as om 8 | from fastuav.constants import MR_PROPULSION, FW_PROPULSION 9 | 10 | 11 | class HoverThrust(om.ExplicitComponent): 12 | """ 13 | Thrust to maintain hover. 14 | """ 15 | 16 | def initialize(self): 17 | self.options.declare("propulsion_id", default=MR_PROPULSION, values=[MR_PROPULSION]) 18 | 19 | def setup(self): 20 | propulsion_id = self.options["propulsion_id"] 21 | self.add_input("optimization:variables:weight:mtow:guess", val=np.nan, units="kg") 22 | self.add_input("data:propulsion:%s:propeller:number" % propulsion_id, val=np.nan, units=None) 23 | self.add_output("data:propulsion:%s:propeller:thrust:hover" % propulsion_id, units="N") 24 | 25 | def setup_partials(self): 26 | # Finite difference all partials. 27 | self.declare_partials("*", "*", method="fd") 28 | 29 | def compute(self, inputs, outputs): 30 | propulsion_id = self.options["propulsion_id"] 31 | m_uav_guess = inputs["optimization:variables:weight:mtow:guess"] 32 | Npro = inputs["data:propulsion:%s:propeller:number" % propulsion_id] 33 | 34 | F_pro_hov = m_uav_guess * g / Npro # [N] Thrust per propeller 35 | 36 | outputs["data:propulsion:%s:propeller:thrust:hover" % propulsion_id] = F_pro_hov 37 | 38 | 39 | class NoHover(om.ExplicitComponent): 40 | """ 41 | Simple component to declare the absence of hover scenario. 42 | """ 43 | 44 | def initialize(self): 45 | self.options.declare("propulsion_id", default=FW_PROPULSION, values=[FW_PROPULSION]) 46 | 47 | def setup(self): 48 | propulsion_id = self.options["propulsion_id"] 49 | self.add_output("data:propulsion:%s:propeller:thrust:hover" % propulsion_id, units="N") 50 | 51 | def compute(self, inputs, outputs): 52 | propulsion_id = self.options["propulsion_id"] 53 | outputs["data:propulsion:%s:propeller:thrust:hover" % propulsion_id] = 0.0 -------------------------------------------------------------------------------- /src/fastuav/models/structures/fuselage.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fuselage Structures and Weights 3 | """ 4 | import openmdao.api as om 5 | import numpy as np 6 | 7 | 8 | class FuselageStructures(om.ExplicitComponent): 9 | """ 10 | Computes Fuselage mass 11 | """ 12 | 13 | def setup(self): 14 | self.add_input("data:geometry:fuselage:surface", val=np.nan, units="m**2") 15 | self.add_input("data:geometry:fuselage:surface:nose", val=np.nan, units="m**2") 16 | self.add_input("data:geometry:fuselage:surface:mid", val=np.nan, units="m**2") 17 | self.add_input("data:geometry:fuselage:surface:rear", val=np.nan, units="m**2") 18 | self.add_input("data:weight:airframe:fuselage:mass:density", val=np.nan, units="kg/m**2") 19 | self.add_output("data:weight:airframe:fuselage:mass", units="kg", lower=0.0) 20 | self.add_output("data:weight:airframe:fuselage:mass:nose", units="kg", lower=0.0) 21 | self.add_output("data:weight:airframe:fuselage:mass:mid", units="kg", lower=0.0) 22 | self.add_output("data:weight:airframe:fuselage:mass:rear", units="kg", lower=0.0) 23 | 24 | def setup_partials(self): 25 | # Finite difference all partials. 26 | self.declare_partials("*", "*", method="fd") 27 | 28 | def compute(self, inputs, outputs): 29 | S_fus = inputs["data:geometry:fuselage:surface"] 30 | S_nose = inputs["data:geometry:fuselage:surface:nose"] 31 | S_mid = inputs["data:geometry:fuselage:surface:mid"] 32 | S_rear = inputs["data:geometry:fuselage:surface:rear"] 33 | rho_fus = inputs["data:weight:airframe:fuselage:mass:density"] 34 | 35 | m_nose = S_nose * rho_fus 36 | m_mid = S_mid * rho_fus 37 | m_rear = S_rear * rho_fus 38 | m_fus = S_fus * rho_fus # mass of fuselage [kg] 39 | 40 | outputs["data:weight:airframe:fuselage:mass"] = m_fus 41 | outputs["data:weight:airframe:fuselage:mass:nose"] = m_nose 42 | outputs["data:weight:airframe:fuselage:mass:mid"] = m_mid 43 | outputs["data:weight:airframe:fuselage:mass:rear"] = m_rear 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/fastuav/configurations/doe_sobol_analysis.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone MDA # only consistency constraints are solved. No design optimization. 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-3, optimizer='SLSQP') 12 | 13 | # Definition of OpenMDAO model 14 | model: 15 | scenarios: 16 | id: fastuav.scenarios.multirotor 17 | propulsion: 18 | id: fastuav.propulsion.multirotor 19 | gearbox: False 20 | geometry: 21 | id: fastuav.geometry.multirotor 22 | structures: 23 | id: fastuav.structures.multirotor 24 | wires: 25 | id: fastuav.propulsion.wires.multirotor 26 | mtow: 27 | id: fastuav.mtow.multirotor 28 | performance: 29 | endurance: 30 | id: fastuav.performance.endurance.multirotor 31 | missions: 32 | id: fastuav.performance.mission 33 | file_path: ../missions/missions_multirotor.yaml 34 | 35 | optimization: 36 | design_variables: 37 | - name: optimization:variables:weight:mtow:k # over estimation coefficient on the mass (to solve the mass consistency constraint) 38 | upper: 40.0 39 | lower: 1.0 40 | 41 | constraints: 42 | - name: optimization:constraints:weight:mtow:consistency # consistency constraint on the drone mass 43 | lower: 0.0 44 | 45 | 46 | objective: 47 | # MASS MINIMIZATION 48 | - name: data:weight:mtow 49 | scaler: 1e-1 50 | 51 | # ENERGY MINIMIZATION 52 | #- name: mission:sizing:energy 53 | # scaler: 1e-3 54 | 55 | # MAX. RANGE MAXIMIZATION 56 | #- name: data:performance:range:cruise 57 | # scaler: -1e-3 58 | 59 | # HOVER AUTONOMY MAXIMIZATION 60 | #- name: data:performance:endurance:hover:max 61 | # scaler: -1e-1 -------------------------------------------------------------------------------- /src/fastuav/models/stability/static_longitudinal/neutral_point.py: -------------------------------------------------------------------------------- 1 | """ 2 | Neutral Point module. 3 | The neutral point (NP) is the position of center of mass where the UAV would be on the edge of stability. 4 | """ 5 | import openmdao.api as om 6 | import numpy as np 7 | 8 | 9 | class NeutralPoint(om.ExplicitComponent): 10 | """ 11 | Computes the neutral of a fixed wing UAV. 12 | The fuselage contribution is neglected. 13 | Only the wing and the horizontal tail are taken into account. 14 | """ 15 | 16 | def setup(self): 17 | self.add_input("optimization:variables:geometry:wing:AR", val=np.nan, units=None) 18 | self.add_input("data:geometry:wing:MAC:length", val=np.nan, units="m") 19 | self.add_input("data:geometry:tail:horizontal:coefficient", val=0.5, units=None) 20 | self.add_input("optimization:variables:geometry:tail:horizontal:AR", val=4.0, units=None) 21 | self.add_input("data:aerodynamics:CDi:e", val=np.nan, units=None) 22 | self.add_input("data:geometry:wing:MAC:C4:x", val=np.nan, units="m") 23 | self.add_output("data:stability:neutral_point", units="m") 24 | 25 | def setup_partials(self): 26 | # Finite difference all partials. 27 | self.declare_partials("*", "*", method="fd") 28 | 29 | def compute(self, inputs, outputs): 30 | AR_w = inputs["optimization:variables:geometry:wing:AR"] 31 | c_MAC = inputs["data:geometry:wing:MAC:length"] 32 | V_ht = inputs["data:geometry:tail:horizontal:coefficient"] 33 | AR_ht = inputs["optimization:variables:geometry:tail:horizontal:AR"] 34 | e = inputs["data:aerodynamics:CDi:e"] 35 | x_ac_w = inputs[ 36 | "data:geometry:wing:MAC:C4:x" 37 | ] # wing aerodynamic center (located at quarter chord of the wing) 38 | 39 | l_np = ( 40 | c_MAC * V_ht * (1 - 4 / (2 + e * AR_w)) * (1 + 2 / e / AR_w) / (1 + 2 / AR_ht) 41 | ) # distance from neutral point to wing aerodynamic center [m] 42 | x_np = x_ac_w + l_np # distance from neutral point to nose tip [m] 43 | 44 | outputs["data:stability:neutral_point"] = x_np 45 | -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/configurations/doe_sobol_analysis.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone MDA # only consistency constraints are solved. No design optimization. 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-3, optimizer='SLSQP') 12 | 13 | # Definition of OpenMDAO model 14 | model: 15 | scenarios: 16 | id: fastuav.scenarios.multirotor 17 | propulsion: 18 | id: fastuav.propulsion.multirotor 19 | gearbox: False 20 | geometry: 21 | id: fastuav.geometry.multirotor 22 | structures: 23 | id: fastuav.structures.multirotor 24 | wires: 25 | id: fastuav.propulsion.wires.multirotor 26 | mtow: 27 | id: fastuav.mtow.multirotor 28 | performance: 29 | endurance: 30 | id: fastuav.performance.endurance.multirotor 31 | missions: 32 | id: fastuav.performance.mission 33 | file_path: ../missions/missions_multirotor.yaml 34 | 35 | optimization: 36 | design_variables: 37 | - name: optimization:variables:weight:mtow:k # over estimation coefficient on the mass (to solve the mass consistency constraint) 38 | upper: 40.0 39 | lower: 1.0 40 | 41 | constraints: 42 | - name: optimization:constraints:weight:mtow:consistency # consistency constraint on the drone mass 43 | lower: 0.0 44 | 45 | 46 | objective: 47 | # MASS MINIMIZATION 48 | - name: data:weight:mtow 49 | scaler: 1e-1 50 | 51 | # ENERGY MINIMIZATION 52 | #- name: mission:sizing:energy 53 | # scaler: 1e-3 54 | 55 | # MAX. RANGE MAXIMIZATION 56 | #- name: data:performance:range:cruise 57 | # scaler: -1e-3 58 | 59 | # HOVER AUTONOMY MAXIMIZATION 60 | #- name: data:performance:endurance:hover:max 61 | # scaler: -1e-1 -------------------------------------------------------------------------------- /src/fastuav/configurations/multirotor_mda_cots.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone MDA with COTS components # only consistency constraints are solved. No design optimization. 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-3, optimizer='SLSQP', maxiter=50) 12 | 13 | # Definition of OpenMDAO model 14 | model: 15 | scenarios: 16 | id: fastuav.scenarios.multirotor 17 | propulsion: 18 | id: fastuav.propulsion.multirotor 19 | off_the_shelf_propeller: True 20 | geometry: 21 | id: fastuav.geometry.multirotor 22 | structures: 23 | id: fastuav.structures.multirotor 24 | wires: 25 | id: fastuav.propulsion.wires.multirotor 26 | mtow: 27 | id: fastuav.mtow.multirotor 28 | performance: 29 | endurance: 30 | id: fastuav.performance.endurance.multirotor 31 | missions: 32 | id: fastuav.performance.mission 33 | file_path: ../missions/missions_multirotor.yaml 34 | 35 | optimization: 36 | design_variables: 37 | - name: optimization:variables:weight:mtow:k # over estimation coefficient on the mass (to solve the mass consistency constraint) 38 | upper: 40.0 39 | lower: 1.0 40 | 41 | constraints: 42 | - name: optimization:constraints:weight:mtow:consistency # consistency constraint on the drone mass 43 | lower: 0.0 44 | 45 | 46 | objective: 47 | # MASS MINIMIZATION 48 | - name: data:weight:mtow 49 | scaler: 1e-1 50 | 51 | # ENERGY MINIMIZATION 52 | #- name: mission:sizing:energy 53 | # scaler: 1e-3 54 | 55 | # MAX. RANGE MAXIMIZATION 56 | #- name: data:performance:range:cruise 57 | # scaler: -1e-3 58 | 59 | # HOVER AUTONOMY MAXIMIZATION 60 | #- name: data:performance:endurance:hover:max 61 | # scaler: -1e-1 -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/configurations/multirotor_mda_cots.yaml: -------------------------------------------------------------------------------- 1 | title: Multirotor Drone MDA with COTS components # only consistency constraints are solved. No design optimization. 2 | 3 | # List of folder paths where user added custom registered OpenMDAO components 4 | module_folders: 5 | 6 | # Input and output files 7 | input_file: ../../workdir/problem_inputs.xml 8 | output_file: ../../workdir/problem_outputs.xml 9 | 10 | # Definition of problem driver assuming the OpenMDAO convention import openmdao.api as om 11 | driver: om.ScipyOptimizeDriver(tol=1e-3, optimizer='SLSQP', maxiter=50) 12 | 13 | # Definition of OpenMDAO model 14 | model: 15 | scenarios: 16 | id: fastuav.scenarios.multirotor 17 | propulsion: 18 | id: fastuav.propulsion.multirotor 19 | off_the_shelf_propeller: True 20 | geometry: 21 | id: fastuav.geometry.multirotor 22 | structures: 23 | id: fastuav.structures.multirotor 24 | wires: 25 | id: fastuav.propulsion.wires.multirotor 26 | mtow: 27 | id: fastuav.mtow.multirotor 28 | performance: 29 | endurance: 30 | id: fastuav.performance.endurance.multirotor 31 | missions: 32 | id: fastuav.performance.mission 33 | file_path: ../missions/missions_multirotor.yaml 34 | 35 | optimization: 36 | design_variables: 37 | - name: optimization:variables:weight:mtow:k # over estimation coefficient on the mass (to solve the mass consistency constraint) 38 | upper: 40.0 39 | lower: 1.0 40 | 41 | constraints: 42 | - name: optimization:constraints:weight:mtow:consistency # consistency constraint on the drone mass 43 | lower: 0.0 44 | 45 | 46 | objective: 47 | # MASS MINIMIZATION 48 | - name: data:weight:mtow 49 | scaler: 1e-1 50 | 51 | # ENERGY MINIMIZATION 52 | #- name: mission:sizing:energy 53 | # scaler: 1e-3 54 | 55 | # MAX. RANGE MAXIMIZATION 56 | #- name: data:performance:range:cruise 57 | # scaler: -1e-3 58 | 59 | # HOVER AUTONOMY MAXIMIZATION 60 | #- name: data:performance:endurance:hover:max 61 | # scaler: -1e-1 -------------------------------------------------------------------------------- /src/fastuav/models/structures/tails.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tails Structures and Weights 3 | """ 4 | import openmdao.api as om 5 | import numpy as np 6 | from fastuav.models.structures.wing.estimation_models import WingStructuresEstimationModels 7 | 8 | 9 | class HorizontalTailStructures(om.ExplicitComponent): 10 | """ 11 | Computes Horizontal Tail mass 12 | """ 13 | 14 | def setup(self): 15 | self.add_input("data:geometry:tail:horizontal:surface", val=np.nan, units="m**2") 16 | self.add_input("data:weight:airframe:tail:density", val=np.nan, units="kg/m**2") 17 | self.add_output("data:weight:airframe:tail:horizontal:mass", units="kg", lower=0.0) 18 | 19 | def setup_partials(self): 20 | # Finite difference all partials. 21 | self.declare_partials("*", "*", method="fd") 22 | 23 | def compute(self, inputs, outputs): 24 | S_ht = inputs["data:geometry:tail:horizontal:surface"] 25 | rho_skin = inputs["data:weight:airframe:tail:density"] 26 | 27 | m_skin = WingStructuresEstimationModels.skin(S_ht / 2, rho_skin) # skin 28 | m_wing = 2 * m_skin # total mass (both sides) [kg] 29 | 30 | outputs["data:weight:airframe:tail:horizontal:mass"] = m_wing 31 | 32 | 33 | class VerticalTailStructures(om.ExplicitComponent): 34 | """ 35 | Computes Vertical Tail mass 36 | """ 37 | 38 | def setup(self): 39 | self.add_input("data:geometry:tail:vertical:surface", val=np.nan, units="m**2") 40 | self.add_input("data:weight:airframe:tail:density", val=np.nan, units="kg/m**2") 41 | self.add_output("data:weight:airframe:tail:vertical:mass", units="kg", lower=0.0) 42 | 43 | def setup_partials(self): 44 | # Finite difference all partials. 45 | self.declare_partials("*", "*", method="fd") 46 | 47 | def compute(self, inputs, outputs): 48 | S_vt = inputs["data:geometry:tail:vertical:surface"] 49 | rho_skin = inputs["data:weight:airframe:tail:density"] 50 | 51 | m_skin = WingStructuresEstimationModels.skin(S_vt, rho_skin) # skin 52 | m_wing = m_skin # total mass (both sides) [kg] 53 | 54 | outputs["data:weight:airframe:tail:vertical:mass"] = m_wing 55 | -------------------------------------------------------------------------------- /src/fastuav/models/scenarios/scenarios_fixedwing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sizing scenarios definition for fixed wing drones. 3 | The sizing scenarios return the thrusts and loads requirements to size the UAV. 4 | The sizing scenarios are extracted from a sizing mission defined by the user. 5 | """ 6 | import fastoad.api as oad 7 | import openmdao.api as om 8 | from fastuav.models.mtow.mtow import MtowGuess 9 | from fastuav.models.aerodynamics.aerodynamics_fixedwing import SpanEfficiency, InducedDragConstant 10 | from fastuav.models.scenarios.thrust.takeoff import LauncherTakeoff 11 | from fastuav.models.scenarios.thrust.cruise import FixedwingCruiseThrust 12 | from fastuav.models.scenarios.thrust.climb import FixedwingClimbThrust 13 | from fastuav.models.scenarios.thrust.hover import NoHover 14 | from fastuav.models.scenarios.wing_loading.wing_loading import ( 15 | WingLoadingCruise, 16 | WingLoadingStall, 17 | WingLoadingSelection, 18 | ) 19 | 20 | 21 | @oad.RegisterOpenMDAOSystem("fastuav.scenarios.fixedwing") 22 | class SizingScenariosFixedWing(om.Group): 23 | """ 24 | Sizing scenarios definition for fixed wing configurations 25 | """ 26 | 27 | def setup(self): 28 | preliminary = self.add_subsystem("preliminary", om.Group(), promotes=["*"]) 29 | preliminary.add_subsystem("mtow_guess", MtowGuess(), promotes=["*"]) 30 | preliminary.add_subsystem("span_efficiency", SpanEfficiency(), promotes=["*"]) 31 | preliminary.add_subsystem("induced_drag_constant", InducedDragConstant(), promotes=["*"]) 32 | 33 | wingloading = self.add_subsystem("wing_loading", om.Group(), promotes=["*"]) 34 | wingloading.add_subsystem("stall", WingLoadingStall(), promotes=["*"]) 35 | wingloading.add_subsystem("cruise", WingLoadingCruise(), promotes=["*"]) 36 | wingloading.add_subsystem("selection", WingLoadingSelection(), promotes=["*"]) 37 | 38 | thrust = self.add_subsystem("thrust", om.Group(), promotes=["*"]) 39 | thrust.add_subsystem("takeoff", LauncherTakeoff(), promotes=["*"]) 40 | thrust.add_subsystem("climb", FixedwingClimbThrust(), promotes=["*"]) 41 | thrust.add_subsystem("cruise", FixedwingCruiseThrust(), promotes=["*"]) 42 | thrust.add_subsystem("no_hover", NoHover(), promotes=["*"]) 43 | 44 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/esc/performance_analysis.py: -------------------------------------------------------------------------------- 1 | """ 2 | ESC performances 3 | """ 4 | import openmdao.api as om 5 | import numpy as np 6 | 7 | 8 | class ESCPerformanceModel: 9 | """ 10 | ESC model for performances calculation 11 | """ 12 | 13 | @staticmethod 14 | def power(P_mot, U_mot, U_bat): 15 | P_esc = P_mot * U_bat / U_mot if U_mot > 0 else 0.0 # [W] electronic power 16 | return P_esc 17 | 18 | 19 | class ESCPerformanceGroup(om.Group): 20 | """ 21 | Group containing the performance functions of the ESC 22 | """ 23 | 24 | def setup(self): 25 | self.add_subsystem("takeoff", ESCPerformance(scenario="takeoff"), promotes=["*"]) 26 | self.add_subsystem("hover", ESCPerformance(scenario="hover"), promotes=["*"]) 27 | self.add_subsystem("climb", ESCPerformance(scenario="climb"), promotes=["*"]) 28 | self.add_subsystem("cruise", ESCPerformance(scenario="cruise"), promotes=["*"]) 29 | 30 | 31 | class ESCPerformance(om.ExplicitComponent): 32 | """ 33 | Performances calculation of ESC for given flight scenario 34 | """ 35 | 36 | def initialize(self): 37 | self.options.declare("scenario", default="cruise", values=["takeoff", "climb", "hover", "cruise"]) 38 | 39 | def setup(self): 40 | scenario = self.options["scenario"] 41 | self.add_input("data:propulsion:motor:power:%s" % scenario, val=np.nan, units="W") 42 | self.add_input("data:propulsion:motor:voltage:%s" % scenario, val=np.nan, units="V") 43 | self.add_input("data:propulsion:battery:voltage", val=np.nan, units="V") 44 | self.add_output("data:propulsion:esc:power:%s" % scenario, units="W") 45 | 46 | def setup_partials(self): 47 | # Finite difference all partials. 48 | self.declare_partials("*", "*", method="fd") 49 | 50 | def compute(self, inputs, outputs): 51 | scenario = self.options["scenario"] 52 | P_mot = inputs["data:propulsion:motor:power:%s" % scenario] 53 | U_mot = inputs["data:propulsion:motor:voltage:%s" % scenario] 54 | U_bat = inputs["data:propulsion:battery:voltage"] 55 | 56 | P_esc = ESCPerformanceModel.power(P_mot, U_mot, U_bat) # [W] electronic power 57 | 58 | outputs["data:propulsion:esc:power:%s" % scenario] = P_esc 59 | 60 | -------------------------------------------------------------------------------- /src/fastuav/models/structures/wing/wing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wing Structures and Weights 3 | """ 4 | import openmdao.api as om 5 | from fastuav.models.structures.wing.estimation_models import WingStructuresEstimationModelsGroup 6 | from fastuav.models.structures.wing.structural_analysis import SparsStressVTOL 7 | from fastuav.models.structures.wing.constraints import SparsGeometricalConstraint, SparsStressVTOLConstraint 8 | 9 | 10 | class WingStructuresFW(om.Group): 11 | """ 12 | Computes Wing Structures and Masses for fixed-wing UAVs. 13 | """ 14 | 15 | def initialize(self): 16 | self.options.declare("spar_model", default="pipe", values=["pipe", "I_beam"]) 17 | 18 | def setup(self): 19 | spar_model = self.options["spar_model"] 20 | self.add_subsystem("estimation_models", 21 | WingStructuresEstimationModelsGroup(spar_model=spar_model), 22 | promotes=["*"]) 23 | self.add_subsystem("constraints", 24 | SparsGeometricalConstraint(spar_model=spar_model), 25 | promotes=["*"]) 26 | 27 | 28 | class WingStructuresHybrid(om.Group): 29 | """ 30 | Computes Wing Structures and Masses for FW-VTOL UAVs. 31 | The difference with fixed-wing UAVs consists of the additional loads resulting 32 | from the vertical takeoff scenario (maximum thrust of VTOL propellers). 33 | """ 34 | 35 | def initialize(self): 36 | self.options.declare("spar_model", default="pipe", values=["pipe", "I_beam"]) 37 | 38 | def setup(self): 39 | spar_model = self.options["spar_model"] 40 | 41 | self.add_subsystem("estimation_models", 42 | WingStructuresEstimationModelsGroup(spar_model=spar_model), 43 | promotes=["*"]) 44 | 45 | self.add_subsystem("structural_analysis", 46 | SparsStressVTOL(spar_model=spar_model), 47 | promotes=["*"]) 48 | 49 | constraints = self.add_subsystem("constraints", om.Group(), promotes=["*"]) 50 | constraints.add_subsystem("spar_height", 51 | SparsGeometricalConstraint(spar_model=spar_model), 52 | promotes=["*"]) 53 | constraints.add_subsystem("vtol_stress", 54 | SparsStressVTOLConstraint(), 55 | promotes=["*"]) 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/gearbox/gearbox.py: -------------------------------------------------------------------------------- 1 | """ 2 | Gearbox model 3 | """ 4 | import openmdao.api as om 5 | import numpy as np 6 | 7 | 8 | class Gearbox(om.ExplicitComponent): 9 | """ 10 | Simple Gearbox Model 11 | """ 12 | 13 | def setup(self): 14 | self.add_input("data:propulsion:gearbox:N_red", val=1.0, units=None) 15 | self.add_input("data:propulsion:motor:torque:nominal", val=np.nan, units="N*m") 16 | self.add_output("data:weight:propulsion:gearbox:mass", units="kg") 17 | self.add_output("data:propulsion:gearbox:gear_diameter", units="m") 18 | self.add_output("data:propulsion:gearbox:pinion_diameter", units="m") 19 | self.add_output("data:propulsion:gearbox:inner_diameter", units="m") 20 | 21 | def setup_partials(self): 22 | # Finite difference all partials. 23 | self.declare_partials("*", "*", method="fd") 24 | 25 | def compute(self, inputs, outputs): 26 | N_red = inputs["data:propulsion:gearbox:N_red"] 27 | T_mot_nom = inputs["data:propulsion:motor:torque:nominal"] 28 | 29 | mg1 = 0.0309 * N_red**2 + 0.1944 * N_red + 0.6389 # Ratio input pinion to mating gear 30 | WF = ( 31 | 1 + 1 / mg1 + mg1 + mg1**2 + N_red**2 / mg1 + N_red**2 32 | ) # Weight Factor (ƩFd2/C) [-] 33 | k_sd = 1000 # Surface durability factor [lb/in] 34 | C = 2 * 8.85 * T_mot_nom / k_sd # Coefficient (C=2T/K) [in3] 35 | Fd2 = WF * C # Solid rotor volume [in3] 36 | Mgear = ( 37 | Fd2 * 0.3 * 0.4535 38 | ) # Mass reducer [kg] (0.3 is a coefficient evaluated for aircraft application and 0.4535 to pass from lb to kg) 39 | Fdp2 = C * (N_red + 1) / N_red # Solid rotor pinion volume [in3] 40 | dp = (Fdp2 / 0.7) ** (1 / 3) * 0.0254 # Pinion diameter [m] (0.0254 to pass from in to m) 41 | dg = N_red * dp # Gear diameter [m] 42 | di = mg1 * dp # Inner diameter [m] 43 | 44 | outputs["data:weight:propulsion:gearbox:mass"] = Mgear 45 | outputs["data:propulsion:gearbox:gear_diameter"] = dg 46 | outputs["data:propulsion:gearbox:pinion_diameter"] = dp 47 | outputs["data:propulsion:gearbox:inner_diameter"] = di 48 | 49 | 50 | class NoGearbox(om.ExplicitComponent): 51 | """ 52 | No gearbox: sets the value 'data:weight:propulsion:gearbox:mass' to 0.0 and reduction ratio to 1.0 53 | """ 54 | 55 | def setup(self): 56 | self.add_output("data:weight:propulsion:gearbox:mass", units="kg") 57 | self.add_output("data:propulsion:gearbox:N_red", units=None) 58 | 59 | def compute(self, inputs, outputs): 60 | outputs["data:weight:propulsion:gearbox:mass"] = 0.0 61 | outputs["data:propulsion:gearbox:N_red"] = 1.0 62 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "FASTUAV" 3 | version = "0.1.1" 4 | description = "FAST-UAV is a framework for performing rapid Overall Aircraft Design for Unmanned Aerial Vehicles" 5 | readme = "README.md" 6 | authors = [ 7 | "Félix POLLET ", 8 | "Scott DELBECQ ", 9 | "Marc BUDINGER " 10 | ] 11 | packages = [ 12 | { include = "fastuav", from = "src" }, 13 | ] 14 | 15 | homepage = "https://github.com/SizingLab/FAST-UAV" 16 | keywords = [ 17 | "uav", 18 | "design", 19 | "multi-disciplinary" 20 | ] 21 | license = "GPL-3.0-only" 22 | classifiers = [ 23 | "Development Status :: 4 - Beta", 24 | "Environment :: Console", 25 | "Intended Audience :: Science/Research", 26 | "Intended Audience :: Education", 27 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 28 | "Natural Language :: English", 29 | "Operating System :: Microsoft :: Windows", 30 | "Operating System :: POSIX :: Linux", 31 | "Operating System :: MacOS", 32 | "Programming Language :: Python :: 3.7", 33 | "Programming Language :: Python :: 3.8", 34 | "Topic :: Scientific/Engineering :: Physics" 35 | ] 36 | 37 | [tool.poetry.dependencies] 38 | # IMPORTANT: when modifying this list, docs/requirements.txt must be updated for 39 | # ReadTheDocs to be able to compile the documentation. 40 | # A pre-commit hook has been added to do this task. As a result, any modification 41 | # of poetry.lock file will modify docs/requirements.txt and make 42 | # the commit fail because "files were modified by this hook". In that case, 43 | # doing again the commit including changes in docs/requirements.txt will succeed. 44 | python = "^3.9, <3.13" 45 | fast-oad-core = "^1.8.3" 46 | openmdao = "<=3.33.0" 47 | cma = "^3.1.0" 48 | scikit-learn = "^1.0.2" 49 | psutil = "*" 50 | kaleido = "0.2.1" 51 | SALib = "1.4.5" 52 | seaborn = "^0.13" 53 | pyzmq = ">=26,<27" # Issue with Ubuntu 24.04 and Python 3.12 54 | plotly = "^6" 55 | ipywidgets = "^8.1.8" 56 | 57 | [tool.poetry.extras] 58 | jupyterlab = ["jupyterlab"] 59 | 60 | [tool.poetry.plugins."fastoad.plugins"] 61 | "uav" = "fastuav" 62 | 63 | [tool.poetry.dev-dependencies] 64 | pytest = "^6.2" 65 | pytest-cov = "^3.0" 66 | coverage = { extras = ["toml"], version = "^5.5" } 67 | pre-commit = "^2.14.1" 68 | black = { version = "22.3.0", extras = ["jupyter"], allow-prereleases = true } 69 | pylint = "^2.10.2" 70 | nbval = "^0.9.6" 71 | sphinx = "^4.1.2" 72 | sphinx-rtd-theme = "^1.0" 73 | sphinxcontrib-bibtex = "^2.3.0" 74 | flake8 = "^4.0.1" 75 | nbstripout = "^0.5.0" 76 | 77 | [tool.black] 78 | line-length = 100 79 | 80 | # For installing with 'poetry install' command. 81 | [build-system] 82 | requires = ["poetry>=0.12"] 83 | build-backend = "poetry.masonry.api" 84 | 85 | # For installing with 'pip install -e .' command. 86 | #[build-system] 87 | #requires = ["setuptools", "setuptools-scm"] 88 | #build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /src/fastuav/models/scenarios/scenarios_hybrid.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sizing scenarios definition for hybrid VTOL drones. 3 | The sizing scenarios return the thrusts and loads requirements to size the UAV. 4 | The sizing scenarios are extracted from a sizing mission defined by the user. 5 | """ 6 | import fastoad.api as oad 7 | import openmdao.api as om 8 | from fastuav.models.mtow.mtow import MtowGuess 9 | from fastuav.models.geometry.geometry_fixedwing import ProjectedAreasGuess 10 | from fastuav.models.aerodynamics.aerodynamics_fixedwing import SpanEfficiency, InducedDragConstant 11 | from fastuav.models.scenarios.thrust.takeoff import VerticalTakeoffThrust, LauncherTakeoff 12 | from fastuav.models.scenarios.thrust.cruise import FixedwingCruiseThrust, NoCruise 13 | from fastuav.models.scenarios.thrust.climb import MultirotorClimbThrust, FixedwingClimbThrust 14 | from fastuav.models.scenarios.thrust.hover import HoverThrust, NoHover 15 | from fastuav.models.scenarios.wing_loading.wing_loading import ( 16 | WingLoadingCruise, 17 | WingLoadingStall, 18 | WingLoadingSelection, 19 | ) 20 | 21 | 22 | @oad.RegisterOpenMDAOSystem("fastuav.scenarios.hybrid") 23 | class SizingScenariosHybrid(om.Group): 24 | """ 25 | Sizing scenarios definition for hybrid VTOL configurations 26 | """ 27 | 28 | def setup(self): 29 | preliminary = self.add_subsystem("preliminary", om.Group(), promotes=["*"]) 30 | preliminary.add_subsystem("mtow_guess", MtowGuess(), promotes=["*"]) 31 | preliminary.add_subsystem("span_efficiency", SpanEfficiency(), promotes=["*"]) 32 | preliminary.add_subsystem("induced_drag_constant", InducedDragConstant(), promotes=["*"]) 33 | 34 | wingloading = self.add_subsystem("wing_loading", om.Group(), promotes=["*"]) 35 | wingloading.add_subsystem("stall", WingLoadingStall(), promotes=["*"]) 36 | wingloading.add_subsystem("cruise", WingLoadingCruise(), promotes=["*"]) 37 | wingloading.add_subsystem("selection", WingLoadingSelection(), promotes=["*"]) 38 | 39 | self.add_subsystem("areas_guess", ProjectedAreasGuess(), promotes=["*"]) 40 | 41 | thrust = self.add_subsystem("thrust", om.Group(), promotes=["*"]) 42 | multirotor = thrust.add_subsystem("multirotor", om.Group(), promotes=["*"]) 43 | multirotor.add_subsystem("takeoff", VerticalTakeoffThrust(), promotes=["*"]) 44 | multirotor.add_subsystem("climb", MultirotorClimbThrust(), promotes=["*"]) 45 | multirotor.add_subsystem("hover", HoverThrust(), promotes=["*"]) 46 | multirotor.add_subsystem("no_cruise", NoCruise(), promotes=["*"]) 47 | fixedwing = thrust.add_subsystem("fixedwing", om.Group(), promotes=["*"]) 48 | fixedwing.add_subsystem("takeoff", LauncherTakeoff(), promotes=["*"]) 49 | fixedwing.add_subsystem("climb", FixedwingClimbThrust(), promotes=["*"]) 50 | fixedwing.add_subsystem("no_hover", NoHover(), promotes=["*"]) 51 | fixedwing.add_subsystem("cruise", FixedwingCruiseThrust(), promotes=["*"]) 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/fastuav/utils/postprocessing/sensitivity_analysis/distribution_plot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plots for displaying the outputs of a Design of Experiments (e.g., Monte Carlo) 3 | """ 4 | 5 | import matplotlib.pyplot as plt 6 | from scipy.stats import norm 7 | import numpy as np 8 | import pandas as pd 9 | import seaborn as sns 10 | 11 | 12 | def hist_dist_plot(df_output, y): 13 | """ 14 | Create histogram plot for visualizing distribution of a variable y. 15 | 16 | Parameters 17 | ---------- 18 | * df_output: pd.DataFrame, of DoE results 19 | 20 | Returns 21 | ---------- 22 | * fig : matplotlib fig object 23 | """ 24 | 25 | # Get data and fit normal law 26 | mu, std = norm.fit(df_output[y]) 27 | 28 | # Initialize plot 29 | fig, axes = plt.subplots(nrows=2, ncols=1, sharex=True, figsize=(6, 5), gridspec_kw={'height_ratios': [1, 2]}) 30 | fig.tight_layout() 31 | 32 | # Plot the histogram. 33 | q25, q50, q75 = np.percentile(df_output[y], [25, 50, 75]) 34 | bin_width = 2 * (q75 - q25) * len(df_output[y]) ** (-1 / 3) # Freedman–Diaconis number of bins 35 | bins = round((df_output[y].max() - df_output[y].min()) / bin_width) 36 | n, bins, patches = axes[1].hist( 37 | df_output[y], 38 | bins=bins, 39 | density=True, 40 | edgecolor='black', 41 | linewidth=0.5, 42 | label="output distribution", 43 | ) 44 | 45 | # Plot the PDF. 46 | xmin, xmax = plt.xlim() 47 | x = np.linspace(xmin, xmax, 100) 48 | p = norm.pdf(x, mu, std) 49 | axes[1].plot(x, p, "r", linewidth=3, label="normal distribution $\mu$=%.2f, $\sigma$=%.2f" % (mu, std)) 50 | 51 | plt.xlabel(y) 52 | plt.ylabel("Probability") 53 | axes[1].legend(loc='upper center', bbox_to_anchor=(0.5, -0.18), 54 | ncol=1, fancybox=True, shadow=True) 55 | 56 | # top boxplot 57 | axes[0].boxplot(df_output[y], 0, "", vert=False) 58 | axes[0].axvline(q50, ymin=0.1, ymax=0.8, color='red', alpha=.6, linewidth=2) 59 | axes[0].axis('off') 60 | plt.subplots_adjust(hspace=0) 61 | 62 | return fig 63 | 64 | 65 | def DoE_plot(df_output_array, x, y): 66 | """ 67 | Create a plot for visualizing the results of a DoE, along with their distributions. 68 | The plot is two-axes, i.e. one can visualize two variables at a time. 69 | 70 | Parameters 71 | ---------- 72 | * df_output_array: array of pd.DataFrame, of DoE results 73 | 74 | Returns 75 | ---------- 76 | * g : seaborn JointGrid object 77 | """ 78 | 79 | df_concat = pd.concat([df_output_array[i].assign(dataset='DoE %d' %i) for i in range(len(df_output_array))]) 80 | 81 | # Plot results and distributions 82 | g = sns.jointplot(x=x, 83 | y=y, 84 | data=df_concat, 85 | hue='dataset', 86 | s=10, 87 | # size=df_concat['dataset'], 88 | # sizes=[5, 10, 35], 89 | style=df_concat['dataset'], 90 | edgecolor=None) 91 | 92 | return g -------------------------------------------------------------------------------- /src/fastuav/models/performance/mission/mission_definition/tests/test_schema.py: -------------------------------------------------------------------------------- 1 | # This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design 2 | # Copyright (C) 2021 ONERA & ISAE-SUPAERO 3 | # FAST is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | 14 | import os.path as pth 15 | from collections import OrderedDict 16 | 17 | from ..schema import MissionDefinition 18 | 19 | MISSIONS_FOLDER_PATH = pth.join(pth.dirname(__file__), "data") 20 | 21 | 22 | def test_schema(): 23 | obtained_dict = MissionDefinition(pth.join(MISSIONS_FOLDER_PATH, "mission.yaml")) 24 | 25 | # As we use Python 3.7+, Python dictionaries are ordered, but they are still 26 | # considered equal when the order of key differs. 27 | # To check order, we need to convert both dictionaries to OrderedDict (recursively!) 28 | obtained_dict = _to_ordered_dict(obtained_dict) 29 | expected_dict = _to_ordered_dict(_get_expected_dict()) 30 | 31 | assert obtained_dict == expected_dict 32 | 33 | 34 | def _to_ordered_dict(item): 35 | """Returns the item with all dictionaries inside transformed to OrderedDict.""" 36 | if isinstance(item, dict): 37 | ordered_dict = OrderedDict(item) 38 | for key, value in ordered_dict.items(): 39 | ordered_dict[key] = _to_ordered_dict(value) 40 | return ordered_dict 41 | elif isinstance(item, list): 42 | for i, value in enumerate(item): 43 | item[i] = _to_ordered_dict(value) 44 | else: 45 | return item 46 | 47 | 48 | def _get_expected_dict(): 49 | return { 50 | "routes": { 51 | "main_route": { 52 | "takeoff_part": { 53 | "phase_id": "vertical_takeoff" 54 | }, 55 | "climb_part": { 56 | "phase_id": "multirotor_climb" 57 | }, 58 | "cruise_part": { 59 | "phase_id": "fixedwing_cruise" 60 | }, 61 | }, 62 | "diversion": { 63 | "climb_part": { 64 | "phase_id": "fixedwing_climb" 65 | }, 66 | "cruise_part": { 67 | "phase_id": "fixedwing_cruise" 68 | }, 69 | } 70 | }, 71 | "missions": { 72 | "sizing": { 73 | "parts": [ 74 | {"route": "main_route"}, 75 | {"route": "diversion"}, 76 | ] 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/energy/battery/performance_analysis.py: -------------------------------------------------------------------------------- 1 | """ 2 | Battery performance analysis 3 | """ 4 | import openmdao.api as om 5 | import numpy as np 6 | 7 | 8 | class BatteryPerformanceModel: 9 | """ 10 | Battery model for performances calculation 11 | """ 12 | 13 | @staticmethod 14 | def power(P_mot, N_pro, eta_esc, P_payload): 15 | P_bat = P_mot * N_pro / eta_esc + P_payload # [W] Power of the battery 16 | return P_bat 17 | 18 | @staticmethod 19 | def current(P_bat, U_bat): 20 | I_bat = P_bat / U_bat if U_bat > 0 else 0.0 # [I] Current of the battery 21 | return I_bat 22 | 23 | 24 | class BatteryPerformanceGroup(om.Group): 25 | """ 26 | Group containing the performance functions of the battery 27 | """ 28 | 29 | def setup(self): 30 | self.add_subsystem("takeoff", BatteryPerformance(scenario="takeoff"), promotes=["*"]) 31 | self.add_subsystem("hover", BatteryPerformance(scenario="hover"), promotes=["*"]) 32 | self.add_subsystem("climb", BatteryPerformance(scenario="climb"), promotes=["*"]) 33 | self.add_subsystem("cruise", BatteryPerformance(scenario="cruise"), promotes=["*"]) 34 | 35 | 36 | class BatteryPerformance(om.ExplicitComponent): 37 | """ 38 | Computes performances of the battery for given flight scenario 39 | """ 40 | 41 | def initialize(self): 42 | self.options.declare("scenario", default="cruise", values=["takeoff", "climb", "hover", "cruise"]) 43 | 44 | def setup(self): 45 | scenario = self.options["scenario"] 46 | self.add_input("data:propulsion:propeller:number", val=np.nan, units=None) 47 | self.add_input("data:propulsion:motor:power:%s" % scenario, val=np.nan, units="W") 48 | self.add_input("data:propulsion:esc:efficiency:estimated", val=np.nan, units=None) 49 | self.add_input("data:propulsion:battery:voltage", val=np.nan, units="V") 50 | self.add_input("mission:sizing:payload:power", val=np.nan, units="W") 51 | self.add_output("data:propulsion:battery:current:%s" % scenario, units="A") 52 | self.add_output("data:propulsion:battery:power:%s" % scenario, units="W") 53 | 54 | def setup_partials(self): 55 | # Finite difference all partials. 56 | self.declare_partials("*", "*", method="fd") 57 | 58 | def compute(self, inputs, outputs): 59 | scenario = self.options["scenario"] 60 | N_pro = inputs["data:propulsion:propeller:number"] 61 | P_mot = inputs["data:propulsion:motor:power:%s" % scenario] 62 | eta_ESC = inputs[ 63 | "data:propulsion:esc:efficiency:estimated" 64 | ] #TODO: replace by 'real' efficiency (ESC catalogue output, but be careful to algebraic loops...) 65 | U_bat = inputs["data:propulsion:battery:voltage"] 66 | P_payload = inputs["mission:sizing:payload:power"] 67 | 68 | P_bat = BatteryPerformanceModel.power(P_mot, N_pro, eta_ESC, P_payload) 69 | I_bat = BatteryPerformanceModel.current(P_bat, U_bat) 70 | 71 | outputs["data:propulsion:battery:power:%s" % scenario] = P_bat 72 | outputs["data:propulsion:battery:current:%s" % scenario] = I_bat -------------------------------------------------------------------------------- /src/fastuav/data/catalogues/ESC/ESC_data.csv: -------------------------------------------------------------------------------- 1 | TYPE;Model;I_max_A;Mass_g;V_max_V;Power_max_W 2 | JIVE;Jive 80+ LV ;80;84;22.2;1776 3 | JIVE;Jive 100+ LV ;100;92;22.2;2220 4 | JIVE;FAI Jive 150+ LV ;150;140;18.5;2775 5 | JIVE;Jive 60+ HV ;60;84;44.4;2664 6 | JIVE;Jive 80+ HV ;80;84;44.4;3552 7 | JIVE;Powerkive 120+ HV ;120;140;44.4;5328 8 | KOSMIK;Kosmik 160+ HV ;160;200;51.8;8288 9 | KOSMIK;Kosmik 200+ HV ;200;200;51.8;10360 10 | YGE;YGE 7 S ;7;0.7;7.4;52 11 | YGE;YGE 8 S ;8;4.9;11.1;89 12 | YGE;YGE 12 S ;12;6;11.1;133 13 | YGE;YGE 18 ;18;14;14.8;266 14 | YGE;YGE 30 ;30;21;14.8;444 15 | YGE;YGE 40 ;40;35;22.2;888 16 | YGE;YGE 60 ;60;35;22.2;1332 17 | YGE;YGE 80 ;80;55;22.2;1776 18 | YGE;YGE 100 ;100;69;22.2;2220 19 | YGE;YGE 120 ;120;71;22.2;2664 20 | YGE;YGE 160 FAI ;160;79;22.2;3552 21 | YGE;YGE 60 HV ;60;49;44.4;2664 22 | YGE;YGE 90 HV ;90;79;44.4;3996 23 | YGE;YGE 150 HV FAI ;150;89;44.4;6660 24 | YGE;YGE 200 HV FAI ;200;119;44.4;8880 25 | YGE;YGE 120 HV ;120;119;51.8;6216 26 | YGE;YGE 160 HV ;160;156;51.8;8288 27 | SPIN OPTO;MasterSPIN 11 ;11;12;14.8;163 28 | SPIN OPTO;MasterSPIN 22 ;22;18;14.8;326 29 | SPIN OPTO;MasterSPIN 33 ;33;30;18.5;611 30 | SPIN OPTO;MasterSPIN 44 ;44;40;22.2;977 31 | SPIN OPTO;MasterSPIN 55 ;55;56;25.9;1425 32 | SPIN OPTO;MasterSPIN 66 ;66;50;22.2;1465 33 | SPIN OPTO;MasterSPIN 70 Opto ;70;50;22.2;1554 34 | SPIN OPTO;MasterSPIN 48 Opto ;48;45;37;1776 35 | SPIN OPTO;MasterSPIN 75 0pto ;75;55;37;2775 36 | SPIN OPTO;MasterSPIN 77 0pto ;77;105;44.4;3419 37 | SPIN OPTO;MasterSPIN 99 0pto ;99;105;44.4;4396 38 | SPIN OPTO;MasterSPIN 125 Opto ;125;160;44.4;5550 39 | SPIN OPTO;MasterSPIN 170 Opto ;170;270;51.8;8806 40 | SPIN OPTO;MasterSPIN 220 Opto ;220;460;51.8;11396 41 | SPIN OPTO;SPIN 11 ;11;12;14.8;163 42 | SPIN OPTO;SPIN 22 ;22;26;14.8;326 43 | SPIN OPTO;SPIN 33 ;33;32;18.5;611 44 | SPIN OPTO;SPIN 44 ;44;44;22.2;977 45 | SPIN OPTO;SPIN 55 ;55;60;29.6;1628 46 | SPIN OPTO;SPIN 66 ;70;56;22.2;1554 47 | SPIN OPTO;SPIN 44 OPTO ;44;35;22.2;977 48 | SPIN OPTO;SPIN 66 OPTO ;70;45;22.2;1554 49 | SPIN OPTO;SPIN 48 OPTO ;48;45;37;1776 50 | SPIN OPTO;SPIN 75 OPTO ;75;55;37;2775 51 | SPIN OPTO;SPIN 77 OPTO ;77;110;44.4;3419 52 | SPIN OPTO;SPIN 99 OPTO ;90;110;44.4;3996 53 | SPIN OPTO;SPIN 125 OPTO ;125;120;44.4;5550 54 | SPIN OPTO;SPIN 200 OPTO ;170;326;51.8;8806 55 | SPIN OPTO;SPIN 300 OPTO ;220;360;51.8;11396 56 | TURNIGY;Turnigy Monster 2000 ;200;335;44.4;8880 57 | TURNIGY;Turnigy K-Force ;150;169;22.2;3330 58 | TURNIGY;Turnigy K-Force ;120;169;44.4;5328 59 | TURNIGY;Turnigy dlux 80HV ;80;153;44.4;3552 60 | TURNIGY;Turnigy Superbrain ;100;109;44.4;4440 61 | TURNIGY;Turnigy Superbrain ;60;69;22.2;1332 62 | TURNIGY;Turnigy Superbrain ;80;69;22.2;1776 63 | TURNIGY;Turnigy Superbrain ;45;68;22.2;999 64 | TURNIGY;Turnigy K_Force 70HV ;70;115;44.4;3108 65 | TURNIGY;Turnigy K-Force 100 ;100;115;22.2;2220 66 | TURNIGY;Turnigy K-Force 40 ;40;38;22.2;888 67 | TURNIGY;Turnigy dlux 55 ;55;110;22.2;1221 68 | TURNIGY;Turnigy dlux 70 ;70;110;22.2;1554 69 | TURNIGY;Turnigy Plush 80 ;80;120;22.2;1776 70 | TURNIGY;Turnigy Plush 40 ;40;79;22.2;888 71 | TURNIGY;Turnigy Plush 60 ;60;75;22.2;1332 72 | TURNIGY;Turnigy Plush 30 ;30;55;14.8;444 73 | TURNIGY;Turnigy Trust 70 ;70;79;22.2;1554 74 | TURNIGY;Turnigy Trust 55 ;55;79;22.2;1221 75 | TURNIGY;Turnigy Trust 45 ;45;69;22.2;999 76 | -------------------------------------------------------------------------------- /src/fastuav/models/stability/static_longitudinal/center_of_gravity/cog.py: -------------------------------------------------------------------------------- 1 | """ 2 | Center of gravity for fixed wing UAVs. 3 | """ 4 | import openmdao.api as om 5 | import numpy as np 6 | from fastuav.models.stability.static_longitudinal.center_of_gravity.components.cog_airframe import CoG_airframe 7 | from fastuav.models.stability.static_longitudinal.center_of_gravity.components.cog_propulsion import CoG_propulsion_FW, CoG_propulsion_MR 8 | from fastuav.constants import FW_PROPULSION, MR_PROPULSION, PROPULSION_ID_LIST 9 | 10 | 11 | class CenterOfGravity(om.Group): 12 | """ 13 | Group containing the center of gravity calculations for fixed wing or hybrid VTOL UAVs 14 | """ 15 | def initialize(self): 16 | self.options.declare("propulsion_id_list", 17 | default=None, 18 | values=[[FW_PROPULSION], PROPULSION_ID_LIST]) 19 | 20 | def setup(self): 21 | propulsion_id_list = self.options["propulsion_id_list"] 22 | 23 | self.add_subsystem("airframe", CoG_airframe(propulsion_id_list=propulsion_id_list), promotes=["*"]) 24 | 25 | if FW_PROPULSION in propulsion_id_list: 26 | self.add_subsystem("propulsion_fw", CoG_propulsion_FW(), promotes=["*"]) 27 | if MR_PROPULSION in propulsion_id_list: 28 | self.add_subsystem("propulsion_mr", CoG_propulsion_MR(), promotes=["*"]) 29 | 30 | self.add_subsystem("UAV", CoG_UAV(propulsion_id_list=propulsion_id_list), promotes=["*"]) 31 | 32 | 33 | class CoG_UAV(om.ExplicitComponent): 34 | """ 35 | Computes the center of gravity of a fixed wing or hybrid VTOL UAV. 36 | """ 37 | def initialize(self): 38 | self.options.declare("propulsion_id_list", 39 | default=None, 40 | values=[[FW_PROPULSION], PROPULSION_ID_LIST]) 41 | 42 | def setup(self): 43 | propulsion_id_list = self.options["propulsion_id_list"] 44 | 45 | # Airframe 46 | self.add_input("data:stability:CoG:airframe", val=np.nan, units="m") 47 | self.add_input("data:weight:airframe", val=np.nan, units="kg") 48 | 49 | # Propulsion systems 50 | for propulsion_id in propulsion_id_list: 51 | self.add_input("data:stability:CoG:propulsion:%s" % propulsion_id, val=np.nan, units="m") 52 | self.add_input("data:weight:propulsion:%s" % propulsion_id, val=np.nan, units="kg") 53 | 54 | self.add_output("data:stability:CoG", units="m") 55 | 56 | def setup_partials(self): 57 | # Finite difference all partials. 58 | self.declare_partials("*", "*", method="fd") 59 | 60 | def compute(self, inputs, outputs): 61 | propulsion_id_list = self.options["propulsion_id_list"] 62 | 63 | # Airframe 64 | x_cg_airframe = inputs["data:stability:CoG:airframe"] 65 | m_airframe = inputs["data:weight:airframe"] 66 | 67 | # Propulsion systems 68 | m_propulsion = sum(inputs["data:weight:propulsion:%s" % propulsion_id] for propulsion_id in propulsion_id_list) 69 | x_cg_propulsion = sum(inputs["data:stability:CoG:propulsion:%s" % propulsion_id] 70 | * inputs["data:weight:propulsion:%s" % propulsion_id] 71 | for propulsion_id in propulsion_id_list) / m_propulsion 72 | 73 | # UAV 74 | x_cg_uav = (x_cg_airframe * m_airframe + x_cg_propulsion * m_propulsion) / (m_propulsion + m_airframe) 75 | 76 | outputs["data:stability:CoG"] = x_cg_uav -------------------------------------------------------------------------------- /src/fastuav/utils/configurations_versatility.py: -------------------------------------------------------------------------------- 1 | """ 2 | Methods to ensure the OpenMDAO components' versatility for both fixed wing and multirotor configurations. 3 | """ 4 | 5 | import openmdao.api as om 6 | import re 7 | from typing import List 8 | import warnings 9 | 10 | 11 | def promote_and_rename( 12 | group: om.Group, 13 | subsys: om.Group, 14 | rename_inputs: bool=True, 15 | rename_outputs: bool=True, 16 | old_patterns_list: List[str] = None, 17 | new_patterns_list: List[str] = None, 18 | ): 19 | """ 20 | Promote the inputs and outputs variables of the OpenMDAO subsystem, 21 | and rename the variables according the new pattern. 22 | 23 | Parameters 24 | ---------- 25 | group : om.Group 26 | Parent group object. 27 | subsys : om.Group 28 | Subsystem whose variables are to be promoted. 29 | rename_inputs : bool 30 | whether to rename inputs or not. 31 | rename_outputs : bool 32 | whether to rename outputs or not. 33 | old_patterns_list : list[str] 34 | Old string pattern to be renamed. 35 | new_patterns_list : list[str] 36 | New string pattern. 37 | ---------- 38 | 39 | Example 40 | >> promote_and_rename(parent_group, subsystem, old_pattern=":propulsion:", new_pattern=":propulsion:multirotor:") 41 | 42 | PLEASE NOTE: 43 | promote_and_rename() must be called in the configure method of the parent group instead of the setup method. 44 | This is because the information from the subsystems (i.e. the variables names) is not available until the full 45 | setup has be achieved. 46 | Visit https://openmdao.org/newdocs/versions/latest/theory_manual/setup_stack.html for more information. 47 | 48 | """ 49 | 50 | # Get input and output variables names from subsystem 51 | # TODO: list only promoted variables from subsubsystems \ 52 | # (here '*uncertainty:*:mean' are non-promoted variables but still visible so have to be excluded by hand) 53 | var_in_names = [var[0].split(".")[-1] for var in 54 | subsys.list_inputs(val=False, out_stream=None, excludes=['*uncertainty:*:mean'])] 55 | var_out_names = [var[0].split(".")[-1] for var in 56 | subsys.list_outputs(val=False, out_stream=None, excludes=['*uncertainty:*:mean'])] 57 | 58 | # Keep only unique values 59 | var_in_names = list(set(var_in_names)) 60 | var_out_names = list(set(var_out_names)) 61 | 62 | # Create lists of new names 63 | var_in_names_new = var_in_names 64 | var_out_names_new = var_out_names 65 | for old_pattern, new_pattern in zip(old_patterns_list, new_patterns_list): 66 | var_in_names_new = [re.sub(old_pattern, new_pattern, name) for name in var_in_names_new] 67 | var_out_names_new = [re.sub(old_pattern, new_pattern, name) for name in var_out_names_new] 68 | 69 | # Promote variables with new name 70 | inputs = [(old_name, new_name) for old_name, new_name in 71 | zip(var_in_names, var_in_names_new)] if rename_inputs else var_in_names 72 | outputs = [(old_name, new_name) for old_name, new_name in 73 | zip(var_out_names, var_out_names_new)] if rename_outputs else var_out_names 74 | group.promotes(subsys.name, 75 | inputs=inputs, 76 | outputs=outputs) 77 | 78 | # Turn off warnings (calling list_inputs and list_outputs before final_setup will issue a warning 79 | # as only the default values of the variables will be displayed. This behaviour is not impacting the results here) 80 | warnings.filterwarnings('ignore', category=om.OpenMDAOWarning) -------------------------------------------------------------------------------- /src/fastuav/models/performance/mission/mission_definition/schema.py: -------------------------------------------------------------------------------- 1 | """ 2 | Schema for mission definition files. 3 | """ 4 | # This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design 5 | # Copyright (C) 2021 ONERA & ISAE-SUPAERO 6 | # FAST is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import json 18 | from importlib.resources import open_text 19 | from os import PathLike 20 | from typing import Union 21 | 22 | from ensure import Ensure 23 | from jsonschema import validate 24 | from ruamel.yaml import YAML 25 | 26 | from . import resources 27 | 28 | JSON_SCHEMA_NAME = "mission_schema.json" 29 | 30 | # Tags 31 | PHASE_ID_TAG = "phase_id" 32 | ROUTE_TAG = "route" 33 | PARTS_TAG = "parts" 34 | TAKEOFF_PART_TAG = "takeoff_part" 35 | CLIMB_PART_TAG = "climb_part" 36 | CRUISE_PART_TAG = "cruise_part" 37 | DESCENT_PART_TAG = "descent_part" 38 | HOVER_PART_TAG = "hover_part" 39 | MISSION_DEFINITION_TAG = "missions" 40 | ROUTE_DEFINITION_TAG = "routes" 41 | SIZING_MISSION_TAG = "sizing" 42 | MAIN_ROUTE_TAG = "main_route" 43 | 44 | 45 | class MissionDefinition(dict): 46 | def __init__(self, file_path: Union[str, PathLike] = None): 47 | """ 48 | Class for reading a mission definition from a YAML file. 49 | 50 | Path of YAML file should be provided at instantiation, or in 51 | :meth:`load`. 52 | 53 | :param file_path: path of YAML file to read. 54 | """ 55 | super().__init__() 56 | if file_path: 57 | self.load(file_path) 58 | 59 | def load(self, file_path: Union[str, PathLike]): 60 | """ 61 | Loads a mission definition from provided file path. 62 | 63 | Any existing definition will be overwritten. 64 | 65 | :param file_path: path of YAML file to read. 66 | """ 67 | self.clear() 68 | yaml = YAML() 69 | 70 | with open(file_path) as yaml_file: 71 | data = yaml.load(yaml_file) 72 | 73 | with open_text(resources, JSON_SCHEMA_NAME) as json_file: 74 | json_schema = json.loads(json_file.read()) 75 | validate(data, json_schema) 76 | 77 | self._validate(data) 78 | self.update(data) 79 | 80 | @classmethod 81 | def _validate(cls, content: dict): 82 | """ 83 | Does a second pass validation of file content. 84 | Errors are raised if file content is incorrect. 85 | 86 | :param content: 87 | """ 88 | 89 | # Ensure "sizing" mission is defined 90 | Ensure(SIZING_MISSION_TAG).is_in(content[MISSION_DEFINITION_TAG].keys()) 91 | 92 | # Ensure "main_route" is defined 93 | Ensure(MAIN_ROUTE_TAG).is_in(content[ROUTE_DEFINITION_TAG].keys()) 94 | 95 | # Ensure each mission is made of routes that have been defined 96 | for mission_definition in content[MISSION_DEFINITION_TAG].values(): 97 | for part in mission_definition[PARTS_TAG]: 98 | part_type, value = tuple(*part.items()) 99 | Ensure(part_type).equals(ROUTE_TAG) 100 | Ensure(value).is_in(content[ROUTE_DEFINITION_TAG].keys()) 101 | 102 | # Ensure "sizing" mission contains "main_route" 103 | if mission_definition == SIZING_MISSION_TAG: 104 | Ensure(MAIN_ROUTE_TAG).is_in(mission_definition.keys()) 105 | 106 | -------------------------------------------------------------------------------- /src/fastuav/models/stability/static_longitudinal/static_margin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Static Margin module. 3 | Static margin is defined as the distance between the center of gravity and the neutral point of the aircraft, 4 | expressed as a percentage of the mean aerodynamic chord of the wing. 5 | The greater this distance and the narrower the wing, the more stable the aircraft. 6 | """ 7 | import openmdao.api as om 8 | import numpy as np 9 | 10 | 11 | class StaticMargin(om.ExplicitComponent): 12 | """ 13 | Computes the static margin of a fixed wing UAV 14 | """ 15 | 16 | def setup(self): 17 | self.add_input("data:stability:neutral_point", val=np.nan, units="m") 18 | self.add_input("data:stability:CoG", val=np.nan, units="m") 19 | self.add_input("data:geometry:wing:MAC:length", val=np.nan, units="m") 20 | self.add_output("data:stability:static_margin", units=None) 21 | 22 | def setup_partials(self): 23 | self.declare_partials("*", "*", method="exact") 24 | 25 | def compute(self, inputs, outputs): 26 | x_np = inputs["data:stability:neutral_point"] 27 | x_cg = inputs["data:stability:CoG"] 28 | c_MAC = inputs["data:geometry:wing:MAC:length"] 29 | 30 | SM = (x_np - x_cg) / c_MAC # static margin [-] 31 | 32 | outputs["data:stability:static_margin"] = SM 33 | 34 | def compute_partials(self, inputs, partials, discrete_inputs=None): 35 | x_np = inputs["data:stability:neutral_point"] 36 | x_cg = inputs["data:stability:CoG"] 37 | c_MAC = inputs["data:geometry:wing:MAC:length"] 38 | 39 | partials["data:stability:static_margin", 40 | "data:stability:neutral_point"] = 1 / c_MAC 41 | partials["data:stability:static_margin", 42 | "data:stability:CoG"] = - 1 / c_MAC 43 | partials["data:stability:static_margin", 44 | "data:geometry:wing:MAC:length"] = - (x_np - x_cg) / c_MAC ** 2 45 | 46 | 47 | class StaticMarginConstraints(om.ExplicitComponent): 48 | """ 49 | Constraint on the required static margin 50 | """ 51 | 52 | def setup(self): 53 | self.add_input("data:stability:static_margin", val=np.nan, units=None) 54 | self.add_input("data:stability:static_margin:requirement:min", val=0.10, units=None) 55 | self.add_input("data:stability:static_margin:requirement:max", val=0.40, units=None) 56 | self.add_output("optimization:constraints:stability:static_margin:min", units=None) 57 | self.add_output("optimization:constraints:stability:static_margin:max", units=None) 58 | 59 | def setup_partials(self): 60 | self.declare_partials("*", "*", method="exact") 61 | 62 | def compute(self, inputs, outputs): 63 | SM = inputs["data:stability:static_margin"] 64 | SM_min = inputs["data:stability:static_margin:requirement:min"] 65 | SM_max = inputs["data:stability:static_margin:requirement:max"] 66 | 67 | SM_con_min = (SM - SM_min) / SM_min if SM_min != 0 else (SM - SM_min) 68 | SM_con_max = (SM_max - SM) / SM_max if SM_max != 0 else (SM_max - SM) 69 | 70 | outputs["optimization:constraints:stability:static_margin:min"] = SM_con_min 71 | outputs["optimization:constraints:stability:static_margin:max"] = SM_con_max 72 | 73 | def compute_partials(self, inputs, partials, discrete_inputs=None): 74 | SM_min = inputs["data:stability:static_margin:requirement:min"] 75 | SM_max = inputs["data:stability:static_margin:requirement:max"] 76 | 77 | partials["optimization:constraints:stability:static_margin:min", 78 | "data:stability:static_margin"] = 1 / SM_min if SM_min != 0 else 1.0 79 | partials["optimization:constraints:stability:static_margin:min", 80 | "data:stability:static_margin:requirement:min"] = - 1 / SM_min**2 if SM_min != 0 else -1.0 81 | partials["optimization:constraints:stability:static_margin:max", 82 | "data:stability:static_margin"] = - 1 / SM_max if SM_max != 0 else -1.0 83 | partials["optimization:constraints:stability:static_margin:max", 84 | "data:stability:static_margin:requirement:max"] = 1 / SM_max ** 2 if SM_max != 0 else 1.0 85 | 86 | -------------------------------------------------------------------------------- /src/fastuav/models/structures/structures_multirotor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Multirotor Structures 3 | """ 4 | import fastoad.api as oad 5 | import openmdao.api as om 6 | import numpy as np 7 | from fastuav.constants import MR_PROPULSION 8 | 9 | 10 | @oad.RegisterOpenMDAOSystem("fastuav.structures.multirotor") 11 | class Structures(om.Group): 12 | """ 13 | Group containing the airframe structural analysis and weights calculation 14 | """ 15 | 16 | def setup(self): 17 | self.add_subsystem("arms", ArmsWeight(), promotes=["*"]) 18 | self.add_subsystem("body", BodyWeight(), promotes=["*"]) 19 | 20 | 21 | class ArmsWeight(om.ExplicitComponent): 22 | """ 23 | Computes arms weight 24 | """ 25 | 26 | def initialize(self): 27 | self.options.declare("propulsion_id", default=MR_PROPULSION, values=[MR_PROPULSION]) 28 | 29 | def setup(self): 30 | propulsion_id = self.options["propulsion_id"] 31 | 32 | self.add_input("optimization:variables:structures:arms:diameter:k", val=np.nan, units=None) 33 | self.add_input("data:geometry:arms:number", val=np.nan, units=None) 34 | self.add_input("data:geometry:arms:prop_per_arm", val=np.nan, units=None) 35 | self.add_input("data:geometry:arms:length", val=np.nan, units="m") 36 | self.add_input("data:weight:arms:density", val=np.nan, units="kg/m**3") 37 | self.add_input("data:structures:arms:stress:max", val=np.nan, units="N/m**2") 38 | self.add_input("data:propulsion:%s:propeller:thrust:takeoff" % propulsion_id, val=np.nan, units="N") 39 | 40 | self.add_output("data:structures:arms:diameter:outer", units="m", lower=0.0) 41 | self.add_output("data:structures:arms:diameter:inner", units="m", lower=0.0) 42 | self.add_output("data:weight:airframe:arms:mass", units="kg") 43 | 44 | def setup_partials(self): 45 | # Finite difference all partials. 46 | self.declare_partials("*", "*", method="fd") 47 | 48 | def compute(self, inputs, outputs): 49 | propulsion_id = self.options["propulsion_id"] 50 | D_ratio = inputs["optimization:variables:structures:arms:diameter:k"] 51 | Narm = inputs["data:geometry:arms:number"] 52 | Npro_arm = inputs["data:geometry:arms:prop_per_arm"] 53 | Larm = inputs["data:geometry:arms:length"] 54 | rho = inputs["data:weight:arms:density"] 55 | Sigma_max = inputs["data:structures:arms:stress:max"] 56 | F_pro_to = inputs["data:propulsion:%s:propeller:thrust:takeoff" % propulsion_id] 57 | 58 | # Inner and outer diameters 59 | Dout = (F_pro_to * Npro_arm * Larm * 32 / (np.pi * Sigma_max * (1 - D_ratio ** 4))) ** ( 60 | 1 / 3 61 | ) # [m] outer diameter of the beam (sized from max thrust) 62 | Din = D_ratio * Dout # [m] inner diameter of the beam 63 | 64 | # Mass calculation 65 | Marms = ( 66 | np.pi / 4 * (Dout**2 - (D_ratio * Dout) ** 2) * Larm * rho * Narm 67 | ) # [kg] mass of the arms 68 | 69 | outputs["data:structures:arms:diameter:outer"] = Dout 70 | outputs["data:structures:arms:diameter:inner"] = Din 71 | outputs["data:weight:airframe:arms:mass"] = Marms 72 | 73 | 74 | class BodyWeight(om.ExplicitComponent): 75 | """ 76 | Computes body weight 77 | """ 78 | 79 | def setup(self): 80 | self.add_input("models:weight:airframe:arms:mass:reference", val=np.nan, units="kg") 81 | self.add_input("models:weight:airframe:body:mass:reference", val=np.nan, units="kg") 82 | self.add_input("data:weight:airframe:arms:mass", val=np.nan, units="kg") 83 | self.add_output("data:weight:airframe:body:mass", units="kg") 84 | 85 | def setup_partials(self): 86 | # Finite difference all partials. 87 | self.declare_partials("*", "*", method="fd") 88 | 89 | def compute(self, inputs, outputs): 90 | Marm_ref = inputs["models:weight:airframe:arms:mass:reference"] 91 | Mbody_ref = inputs["models:weight:airframe:body:mass:reference"] 92 | Marms = inputs["data:weight:airframe:arms:mass"] 93 | 94 | Mbody = Mbody_ref * (Marms / Marm_ref) # [kg] mass of the frame 95 | 96 | outputs["data:weight:airframe:body:mass"] = Mbody 97 | -------------------------------------------------------------------------------- /src/fastuav/models/scenarios/thrust/flight_models.py: -------------------------------------------------------------------------------- 1 | """ 2 | UAV flight models for thrust calculations - static methods definition. 3 | """ 4 | 5 | import numpy as np 6 | from scipy.constants import g 7 | from scipy.optimize import minimize 8 | 9 | 10 | class MultirotorFlightModel: 11 | """ 12 | Flight model for multirotor. 13 | """ 14 | 15 | @staticmethod 16 | def get_drag(V, alpha, S_front, S_top, C_D, rho_air): 17 | """ 18 | Computes body drag from drag coefficient, reference surfaces and velocity 19 | """ 20 | S_ref = S_top * np.sin(alpha) + S_front * np.cos(alpha) # [m2] reference area 21 | drag = 0.5 * rho_air * C_D * S_ref * V**2 # [N] drag 22 | return drag 23 | 24 | @staticmethod 25 | def get_lift(V, alpha, S_top, C_L0, rho_air): 26 | """ 27 | Computes body lift from lift coefficient, reference surface and velocity. 28 | Derived from a flat plate model. 29 | """ 30 | S_ref = S_top * np.sin(alpha) # [m2] reference area 31 | C_L = C_L0 * np.sin( 32 | -2 * alpha 33 | ) # [-] flat plate model (minus in front of alpha because of angle definition) 34 | lift = 0.5 * rho_air * C_L * S_ref * V**2 # [N] lift 35 | return lift 36 | 37 | @staticmethod 38 | def get_angle_of_attack(m_uav, V, RoC, S_front, S_top, C_D, C_L0, rho_air): 39 | """ 40 | Computes angle of attack to maintain flight path 41 | """ 42 | # flight path angle [rad] 43 | if V != .0 and V > RoC: 44 | theta = np.arcsin(RoC / V) 45 | else: 46 | alpha = theta = np.pi / 2 47 | return alpha 48 | 49 | def func(x): 50 | drag = MultirotorFlightModel.get_drag(V, x, S_front, S_top, C_D, rho_air) # [N] drag 51 | lift = MultirotorFlightModel.get_lift(V, x, S_top, C_L0, rho_air) # [N] lift 52 | weight = m_uav * g # [N] weight 53 | res = np.tan(abs(x - theta)) - (drag * np.cos(theta) + lift * np.sin(theta)) / ( 54 | weight + drag * np.sin(theta) - lift * np.cos(theta) 55 | ) # [-] equilibrium residual 56 | return res**2 57 | 58 | bnds = ((0.0, np.pi / 2),) 59 | res = minimize(func, (np.pi/4), bounds=bnds, method='SLSQP') # [rad] angle of attack 60 | alpha = res.x if res.success else np.pi/2 61 | return alpha 62 | 63 | @staticmethod 64 | def get_thrust(m_uav, V, RoC, alpha, S_front, S_top, C_D, C_L0, rho_air): 65 | """ 66 | Computes thrust to maintain flight path 67 | """ 68 | # flight path angle [rad] 69 | if V != .0 and V >= RoC: 70 | theta = np.arcsin(RoC / V) 71 | else: 72 | theta = np.pi / 2 73 | 74 | weight = m_uav * g # [N] weight 75 | lift = MultirotorFlightModel.get_lift(V, alpha, S_top, C_L0, rho_air) # [N] lift 76 | drag = MultirotorFlightModel.get_drag(V, alpha, S_front, S_top, C_D, rho_air) # [N] drag 77 | thrust = ( 78 | (weight + drag * np.sin(theta) - lift * np.cos(theta)) ** 2 79 | + (drag * np.cos(theta) + lift * np.sin(theta)) ** 2 80 | ) ** ( 81 | 1 / 2 82 | ) # [N] total thrust requirement 83 | return thrust 84 | 85 | 86 | class FixedwingFlightModel: 87 | """ 88 | Flight model for fixed wings. 89 | """ 90 | 91 | @staticmethod 92 | def get_angle_of_attack(): 93 | """ 94 | Computes angle of attack to maintain flight path 95 | """ 96 | alpha = np.pi / 2 # [rad] Rotor disk Angle of Attack (assumption: axial flight TODO: estimate trim?) 97 | return alpha 98 | 99 | @staticmethod 100 | def get_thrust(m_uav, V, RoC, WS, K, CD0, rho_air): 101 | """ 102 | Computes thrust to maintain flight path 103 | """ 104 | q = 0.5 * rho_air * V ** 2 # [Pa] dynamic pressure 105 | TW = ( 106 | RoC / V + q * CD0 / WS + K / q * WS 107 | ) # thrust-to-weight ratio in climb conditions [-] 108 | thrust = TW * m_uav * g # [N] total thrust requirement 109 | return thrust 110 | -------------------------------------------------------------------------------- /src/fastuav/utils/postprocessing/sensitivity_analysis/morris_plot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Adapted from SALib plotting module 3 | """ 4 | 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | 8 | 9 | def _sort_Si(Si, key, sortby="mu_star"): 10 | return np.array([Si[key][x] for x in np.argsort(Si[sortby])]) 11 | 12 | 13 | def _sort_Si_by_index(Si, key, index): 14 | return np.array([Si[key][x] for x in index]) 15 | 16 | 17 | def covariance_plot(ax, Si, unit="", legend=None, opts=None): 18 | """ 19 | Plots mu* against sigma or the 95% confidence interval 20 | """ 21 | 22 | if opts is None: 23 | opts = {} 24 | 25 | if Si["sigma"] is not None: 26 | out = [] 27 | x = Si["mu_star"] 28 | y = Si["sigma"] 29 | # c = np.random.rand(len(y)) 30 | # c = plt.cm.get_cmap('hsv', len(y)) 31 | # m = ['o', 'v', '^', '<', '>', '8', 's', 'p', '*', 'h', 'H', 'D', 'd', 'P', 'X'] 32 | m = ["d", "v", "s", "*", "^", "d", "v", "s", "*", "^", "p"] 33 | m = np.resize(m, len(y)) 34 | 35 | for xp, yp, mp in zip(x, y, m): 36 | outp = ax.scatter(xp, yp, marker=mp) 37 | out.append(outp) 38 | # out = ax.scatter(Si['mu_star'], y, c=c, 39 | # **opts) 40 | # out = mscatter(x, y, c=c, m=m, ax=ax, **opts) 41 | ax.set_ylabel(r"$\sigma$ " + unit) 42 | 43 | ax.set_xlim( 44 | 0, 45 | ) 46 | ax.set_ylim( 47 | 0, 48 | ) 49 | 50 | x_axis_bounds = np.array(ax.get_xlim()) 51 | 52 | (line1,) = ax.plot(x_axis_bounds, x_axis_bounds, "k-") 53 | (line2,) = ax.plot(x_axis_bounds, 0.5 * x_axis_bounds, "k--") 54 | (line3,) = ax.plot(x_axis_bounds, 0.1 * x_axis_bounds, "k-.") 55 | 56 | legend_0 = [ 57 | r"$\sigma / \mu^{\star} = 1.0$", 58 | r"$\sigma / \mu^{\star} = 0.5$", 59 | r"$\sigma / \mu^{\star} = 0.1$", 60 | ] 61 | legend = Si["names"] 62 | 63 | if legend is not None: 64 | # ax.legend([line1, line2, line3] + out.legend_elements()[0], legend_0 + legend) 65 | ax.legend( 66 | [line1, line2, line3] + out, 67 | legend_0 + legend, 68 | loc="upper right", 69 | bbox_to_anchor=(1.0, -0.12), 70 | fancybox=True, 71 | shadow=True, 72 | ncol=3, 73 | ) 74 | else: 75 | ax.legend((line1, line2, line3), legend_0, loc="best") 76 | 77 | else: 78 | y = Si["mu_star_conf"] 79 | out = ax.scatter(Si["mu_star"], y, c="k", marker="o", **opts) 80 | ax.set_ylabel(r"$95\% CI$") 81 | 82 | ax.set_xlabel(r"$\mu^\star$ " + unit) 83 | ax.set_ylim( 84 | 0 - (0.01 * np.array(ax.get_ylim()[1])), 85 | ) 86 | 87 | return out 88 | 89 | 90 | def horizontal_bar_plot(ax, Si, opts=None, sortby="mu_star", unit="", legend=None): 91 | """ 92 | Updates a matplotlib axes instance with a horizontal bar plot 93 | of mu_star, with error bars representing mu_star_conf. 94 | """ 95 | assert sortby in ["mu_star", "mu_star_conf", "sigma", "mu"] 96 | 97 | if opts is None: 98 | opts = {} 99 | 100 | # Sort all the plotted elements by mu_star (or optionally another 101 | # metric) 102 | names_sorted = _sort_Si(Si, "names", sortby) 103 | mu_star_sorted = _sort_Si(Si, "mu_star", sortby) 104 | mu_star_conf_sorted = _sort_Si(Si, "mu_star_conf", sortby) 105 | 106 | # Plot horizontal barchart 107 | y_pos = np.arange(len(mu_star_sorted)) 108 | plot_names = names_sorted 109 | 110 | out = ax.barh( 111 | y_pos, mu_star_sorted, xerr=mu_star_conf_sorted, align="center", ecolor="black", **opts 112 | ) 113 | 114 | ax.set_yticks(y_pos) 115 | ax.set_yticklabels(plot_names) 116 | ax.set_xlabel(r"$\mu^\star$" + unit) 117 | 118 | ax.set_ylim(min(y_pos) - 1, max(y_pos) + 1) 119 | 120 | return out 121 | 122 | 123 | def morris_plot(Si, unit="", opts=None): 124 | ax1 = plt.subplot(121) 125 | ax2 = plt.subplot(122) 126 | out_1 = horizontal_bar_plot(ax1, Si, unit=unit) 127 | out_2 = covariance_plot(ax2, Si, unit=unit) 128 | return out_1, out_2 129 | -------------------------------------------------------------------------------- /src/fastuav/models/structures/wing/constraints.py: -------------------------------------------------------------------------------- 1 | """ 2 | Structural constraints for the wing 3 | """ 4 | 5 | import openmdao.api as om 6 | import numpy as np 7 | 8 | 9 | class SparsGeometricalConstraint(om.ExplicitComponent): 10 | """ 11 | Geometrical constraint definition for the spars. 12 | """ 13 | 14 | def initialize(self): 15 | self.options.declare("spar_model", default="pipe", values=["pipe", "I_beam"]) 16 | 17 | def setup(self): 18 | spar_model = self.options["spar_model"] 19 | self.add_input("data:geometry:wing:tip:thickness", val=np.nan, units="m") 20 | if spar_model == "pipe": 21 | self.add_input("data:structures:wing:spar:diameter:outer", val=np.nan, units="m") 22 | self.add_output("optimization:constraints:structures:wing:spar:diameter", units=None) 23 | else: 24 | self.add_input("data:structures:wing:spar:depth", val=np.nan, units="m") 25 | self.add_output("optimization:constraints:structures:wing:spar:depth", units=None) 26 | 27 | def setup_partials(self): 28 | self.declare_partials("*", "*", method="exact") 29 | 30 | def compute(self, inputs, outputs): 31 | spar_model = self.options["spar_model"] 32 | t_wingtip = inputs["data:geometry:wing:tip:thickness"] 33 | if spar_model == "pipe": 34 | d_out = inputs["data:structures:wing:spar:diameter:outer"] 35 | spar_cnstr = (t_wingtip - d_out) / d_out # constraint on spar external dimension [-] 36 | outputs["optimization:constraints:structures:wing:spar:diameter"] = spar_cnstr 37 | else: 38 | h_spar = inputs["data:structures:wing:spar:depth"] 39 | spar_cnstr = (t_wingtip - h_spar) / h_spar # constraint on spar external dimension [-] 40 | outputs["optimization:constraints:structures:wing:spar:depth"] = spar_cnstr 41 | 42 | def compute_partials(self, inputs, partials, discrete_inputs=None): 43 | spar_model = self.options["spar_model"] 44 | t_wingtip = inputs["data:geometry:wing:tip:thickness"] 45 | if spar_model == "pipe": 46 | d_out = inputs["data:structures:wing:spar:diameter:outer"] 47 | partials["optimization:constraints:structures:wing:spar:diameter", 48 | "data:geometry:wing:tip:thickness"] = 1 / d_out 49 | partials["optimization:constraints:structures:wing:spar:diameter", 50 | "data:structures:wing:spar:diameter:outer"] = - t_wingtip / d_out ** 2 51 | else: 52 | h_spar = inputs["data:structures:wing:spar:depth"] 53 | partials["optimization:constraints:structures:wing:spar:depth", 54 | "data:geometry:wing:tip:thickness"] = 1 / h_spar 55 | partials["optimization:constraints:structures:wing:spar:depth", 56 | "data:structures:wing:spar:depth"] = - t_wingtip / h_spar ** 2 57 | 58 | 59 | class SparsStressVTOLConstraint(om.ExplicitComponent): 60 | """ 61 | Constraint on spar's stress during vertical takeoff scenario. 62 | """ 63 | 64 | def setup(self): 65 | self.add_input("data:structures:wing:spar:stress:VTOL", val=np.nan, units="N/m**2") 66 | self.add_input("data:structures:wing:spar:stress:max", val=np.nan, units="N/m**2") 67 | self.add_output("optimization:constraints:structures:wing:spar:stress:VTOL", units=None) 68 | 69 | def setup_partials(self): 70 | self.declare_partials("*", "*", method="exact") 71 | 72 | def compute(self, inputs, outputs): 73 | sig_root = inputs["data:structures:wing:spar:stress:VTOL"] 74 | sig_max = inputs["data:structures:wing:spar:stress:max"] 75 | 76 | stress_cnstr = (sig_max - sig_root) / sig_root 77 | 78 | outputs["optimization:constraints:structures:wing:spar:stress:VTOL"] = stress_cnstr 79 | 80 | def compute_partials(self, inputs, partials, discrete_inputs=None): 81 | sig_root = inputs["data:structures:wing:spar:stress:VTOL"] 82 | sig_max = inputs["data:structures:wing:spar:stress:max"] 83 | 84 | partials["optimization:constraints:structures:wing:spar:stress:VTOL", 85 | "data:structures:wing:spar:stress:VTOL"] = - sig_max / sig_root ** 2 86 | 87 | partials["optimization:constraints:structures:wing:spar:stress:VTOL", 88 | "data:structures:wing:spar:stress:max"] = 1 / sig_root 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/fastuav/models/structures/wing/structural_analysis.py: -------------------------------------------------------------------------------- 1 | """ 2 | Structural analysis for the wing 3 | """ 4 | 5 | import openmdao.api as om 6 | import numpy as np 7 | from fastuav.constants import MR_PROPULSION 8 | 9 | 10 | class WingStructuralAnalysisModels: 11 | """ 12 | Structural analysis models for the wing. 13 | """ 14 | 15 | @staticmethod 16 | def i_beam_stress(M_root, h_web, k_spar, k_flange=0.1): 17 | """ 18 | Structural analysis for a cantilever I-shaped beam with a bending moment at root M_root. 19 | The model assumes that the bending moment is entirely reacted by the spar flanges. 20 | 21 | :param M_root: Bending moment at root [N.m] 22 | :param h_web: Depth of the web [m] 23 | :param k_spar: Depth ratio of the spar (k_a = flange depth / web depth) [-] 24 | :param k_flange: Aspect ratio of the flange (b_flange = a_flange / k_flange) [-] 25 | :return sig_root: Stress at root [Pa] 26 | """ 27 | sig_root = M_root * (1 + k_spar) / (h_web**3 * k_spar**2 * (1 + k_spar**2 / 3) / k_flange) # [Pa] 28 | return sig_root 29 | 30 | @staticmethod 31 | def pipe_stress(M_root, d_out, k_spar): 32 | """ 33 | Structural analysis for a cantilever pipe with a bending moment at root M_root. 34 | 35 | :param M_root: Bending moment at root [N.m] 36 | :param d_out: External diameter of the spar [m] 37 | :param k_spar: Diameter ratio of the spar (k_d = d_in / d_out) [-] 38 | :return sig_root: Stress at root [Pa] 39 | """ 40 | sig_root = (32 * M_root) / (np.pi * (1 - k_spar**4) * d_out**3) 41 | return sig_root 42 | 43 | 44 | class SparsStressVTOL(om.ExplicitComponent): 45 | """ 46 | Stress calculation during vertical takeoff with VTOL propellers. 47 | """ 48 | 49 | def initialize(self): 50 | self.options.declare("spar_model", default="pipe", values=["pipe", "I_beam"]) 51 | self.options.declare("propulsion_id", default=MR_PROPULSION, values=[MR_PROPULSION]) 52 | 53 | def setup(self): 54 | spar_model = self.options["spar_model"] 55 | propulsion_id = self.options["propulsion_id"] 56 | 57 | self.add_input("data:propulsion:%s:propeller:thrust:takeoff" % propulsion_id, val=np.nan, units="N") 58 | self.add_input("data:propulsion:%s:propeller:number" % propulsion_id, val=np.nan, units=None) 59 | self.add_input("data:geometry:%s:propeller:y" % propulsion_id, val=np.nan, units="m") 60 | if spar_model == "pipe": 61 | self.add_input("data:structures:wing:spar:diameter:outer", val=np.nan, units="m") 62 | self.add_input("optimization:variables:structures:wing:spar:diameter:k", val=0.9, units=None) 63 | else: 64 | self.add_input("data:structures:wing:spar:web:depth", val=np.nan, units="m") 65 | self.add_input("optimization:variables:structures:wing:spar:depth:k", val=0.1, units=None) 66 | self.add_output("data:structures:wing:spar:stress:VTOL", units="N/m**2") 67 | 68 | def setup_partials(self): 69 | self.declare_partials("*", "*", method="fd") 70 | 71 | def compute(self, inputs, outputs): 72 | spar_model = self.options["spar_model"] 73 | propulsion_id = self.options["propulsion_id"] 74 | F_pro_to = inputs["data:propulsion:%s:propeller:thrust:takeoff" % propulsion_id] 75 | N_pro = inputs["data:propulsion:%s:propeller:number" % propulsion_id] 76 | y_pro_MR = inputs["data:geometry:%s:propeller:y" % propulsion_id] 77 | 78 | # Half-wing supports half of the total thrust at takeoff 79 | M_root = F_pro_to * N_pro / 2 * y_pro_MR # bending moment at spar's root [N.m] 80 | 81 | if spar_model == "pipe": 82 | d_out = inputs["data:structures:wing:spar:diameter:outer"] # outer diameter [m] 83 | k_spar = inputs["optimization:variables:structures:wing:spar:diameter:k"] # aspect ratio of the spar [-] 84 | sig_root = WingStructuralAnalysisModels.pipe_stress(M_root, d_out, k_spar) 85 | else: 86 | h_web = inputs["data:structures:wing:spar:web:depth"] # distance between the two flanges [m] 87 | k_spar = inputs["optimization:variables:structures:wing:spar:depth:k"] # aspect ratio of the spar [-] 88 | sig_root = WingStructuralAnalysisModels.i_beam_stress(M_root, h_web, k_spar) 89 | 90 | outputs["data:structures:wing:spar:stress:VTOL"] = sig_root -------------------------------------------------------------------------------- /src/fastuav/models/scenarios/thrust/takeoff.py: -------------------------------------------------------------------------------- 1 | """ 2 | Takeoff scenarios 3 | """ 4 | 5 | import numpy as np 6 | from scipy.constants import g 7 | import openmdao.api as om 8 | from stdatm import AtmosphereSI 9 | from fastuav.constants import FW_PROPULSION, MR_PROPULSION 10 | 11 | 12 | class VerticalTakeoffThrust(om.ExplicitComponent): 13 | """ 14 | Thrust for the desired vertical takeoff acceleration. 15 | """ 16 | 17 | def initialize(self): 18 | self.options.declare("propulsion_id", default=MR_PROPULSION, values=[MR_PROPULSION]) 19 | 20 | def setup(self): 21 | propulsion_id = self.options["propulsion_id"] 22 | self.add_input("mission:sizing:thrust_weight_ratio:%s" % propulsion_id, val=np.nan, units=None) 23 | self.add_input("optimization:variables:weight:mtow:guess", val=np.nan, units="kg") 24 | self.add_input("data:propulsion:%s:propeller:number" % propulsion_id, val=np.nan, units=None) 25 | self.add_output("data:propulsion:%s:propeller:thrust:takeoff" % propulsion_id, units="N") 26 | 27 | def setup_partials(self): 28 | # Finite difference all partials. 29 | self.declare_partials("*", "*", method="fd") 30 | 31 | def compute(self, inputs, outputs): 32 | propulsion_id = self.options["propulsion_id"] 33 | k_maxthrust = inputs["mission:sizing:thrust_weight_ratio:%s" % propulsion_id] 34 | m_uav_guess = inputs["optimization:variables:weight:mtow:guess"] 35 | Npro = inputs["data:propulsion:%s:propeller:number" % propulsion_id] 36 | 37 | F_pro_to = m_uav_guess * g / Npro * k_maxthrust # [N] Thrust per propeller 38 | 39 | outputs["data:propulsion:%s:propeller:thrust:takeoff" % propulsion_id] = F_pro_to 40 | 41 | 42 | class LauncherTakeoff(om.ExplicitComponent): 43 | """ 44 | Thrust required for takeoff assuming the use of a rail launcher or bungee, in fixed wing configuration. 45 | The launching system brings the UAV at the required speed for takeoff (10% margin on stall speed). 46 | """ 47 | 48 | def initialize(self): 49 | self.options.declare("propulsion_id", default=FW_PROPULSION, values=[FW_PROPULSION]) 50 | 51 | def setup(self): 52 | propulsion_id = self.options["propulsion_id"] 53 | self.add_input("optimization:variables:weight:mtow:guess", val=np.nan, units="kg") 54 | self.add_input("data:propulsion:%s:propeller:number" % propulsion_id, val=1.0, units=None) 55 | self.add_input("data:geometry:wing:loading", val=np.nan, units="N/m**2") 56 | self.add_input("mission:sizing:main_route:takeoff:altitude", val=0.0, units="m") 57 | self.add_input("mission:sizing:main_route:stall:speed:%s" % propulsion_id, val=np.nan, units="m/s") 58 | self.add_input("mission:sizing:dISA", val=0.0, units="K") 59 | self.add_input("optimization:variables:aerodynamics:CD0:guess", val=0.04, units=None) 60 | self.add_input("data:aerodynamics:CDi:K", val=np.nan, units=None) 61 | self.add_output("data:propulsion:%s:propeller:thrust:takeoff" % propulsion_id, units="N") 62 | 63 | def setup_partials(self): 64 | self.declare_partials("*", "*", method="fd") 65 | 66 | def compute(self, inputs, outputs): 67 | # UAV configuration 68 | propulsion_id = self.options["propulsion_id"] 69 | Npro = inputs["data:propulsion:%s:propeller:number" % propulsion_id] 70 | WS = inputs["data:geometry:wing:loading"] 71 | 72 | # Flight parameters 73 | V_stall = inputs["mission:sizing:main_route:stall:speed:%s" % propulsion_id] 74 | altitude_takeoff = inputs["mission:sizing:main_route:takeoff:altitude"] 75 | dISA = inputs["mission:sizing:dISA"] 76 | atm = AtmosphereSI(altitude_takeoff, dISA) 77 | atm.true_airspeed = 1.1 * V_stall # 10% margin on the stall speed [kg/ms2] 78 | q_takeoff = atm.dynamic_pressure 79 | 80 | # Weight 81 | m_uav_guess = inputs["optimization:variables:weight:mtow:guess"] 82 | Weight = m_uav_guess * g # [N] 83 | 84 | # Induced drag parameters 85 | K = inputs["data:aerodynamics:CDi:K"] 86 | 87 | # Parasitic drag parameters 88 | CD_0_guess = inputs["optimization:variables:aerodynamics:CD0:guess"] 89 | 90 | # Thrust calculation (equilibrium) 91 | TW_takeoff = ( 92 | q_takeoff * CD_0_guess / WS + K / q_takeoff * WS 93 | ) # thrust-to-weight ratio at takeoff [-] 94 | F_pro_takeoff = TW_takeoff * Weight / Npro # [N] Thrust per propeller for takeoff 95 | 96 | outputs["data:propulsion:%s:propeller:thrust:takeoff" % propulsion_id] = F_pro_takeoff 97 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/propulsion.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main module for propulsion system 3 | """ 4 | import fastoad.api as oad 5 | import openmdao.api as om 6 | 7 | from fastuav.models.propulsion.propeller.propeller import Propeller 8 | from fastuav.models.propulsion.motor.motor import Motor 9 | from fastuav.models.propulsion.gearbox.gearbox import Gearbox, NoGearbox 10 | from fastuav.models.propulsion.energy.battery.battery import Battery 11 | from fastuav.models.propulsion.esc.esc import ESC 12 | from fastuav.constants import FW_PROPULSION, MR_PROPULSION 13 | from fastuav.utils.configurations_versatility import promote_and_rename 14 | 15 | 16 | @oad.RegisterOpenMDAOSystem("fastuav.propulsion") 17 | class Propulsion(om.Group): 18 | """ 19 | Group containing the propulsion system calculations 20 | """ 21 | def initialize(self): 22 | self.options.declare("propulsion_id", 23 | default=None, 24 | values=[[MR_PROPULSION], [FW_PROPULSION], [MR_PROPULSION, FW_PROPULSION]]) 25 | 26 | # TODO: declare the following options for each propulsion system (e.g. for hybrid UAVs with 2 propulsions) 27 | # TODO: add option to provide paths to catalogues 28 | self.options.declare("off_the_shelf_propeller", default=False, types=bool) 29 | self.options.declare("off_the_shelf_motor", default=False, types=bool) 30 | self.options.declare("off_the_shelf_battery", default=False, types=bool) 31 | self.options.declare("off_the_shelf_esc", default=False, types=bool) 32 | self.options.declare("gearbox", default=False, types=bool) 33 | 34 | def setup(self): 35 | off_the_shelf_propeller = self.options["off_the_shelf_propeller"] 36 | off_the_shelf_motor = self.options["off_the_shelf_motor"] 37 | off_the_shelf_battery = self.options["off_the_shelf_battery"] 38 | off_the_shelf_esc = self.options["off_the_shelf_esc"] 39 | gearbox = self.options["gearbox"] 40 | for propulsion_id in self.options["propulsion_id"]: 41 | propulsion = self.add_subsystem(propulsion_id, 42 | om.Group(), 43 | ) 44 | propulsion.add_subsystem("propeller", 45 | Propeller(off_the_shelf=off_the_shelf_propeller), 46 | promotes=["*"]) 47 | if gearbox: 48 | propulsion.add_subsystem("motor", 49 | Motor(off_the_shelf=off_the_shelf_motor), 50 | promotes=["*"]) 51 | propulsion.add_subsystem("gearbox", Gearbox(), promotes=["*"]) 52 | else: 53 | propulsion.add_subsystem("no_gearbox", NoGearbox(), promotes=["*"]) 54 | propulsion.add_subsystem("motor", 55 | Motor(off_the_shelf=off_the_shelf_motor), 56 | promotes=["*"]) 57 | propulsion.add_subsystem("battery", 58 | Battery(off_the_shelf=off_the_shelf_battery), 59 | promotes=["*"]) 60 | propulsion.add_subsystem("esc", 61 | ESC(off_the_shelf=off_the_shelf_esc), 62 | promotes=["*"]) 63 | 64 | def configure(self): 65 | for propulsion_id in self.options["propulsion_id"]: 66 | old_patterns_list = [":propulsion", 67 | "mission:sizing:main_route:climb:rate", 68 | "mission:sizing:main_route:climb:speed", 69 | "mission:sizing:main_route:cruise:speed", 70 | "mission:sizing:main_route:stall:speed", 71 | "mission:sizing:payload:power", 72 | ] 73 | new_patterns_list = [":propulsion:%s" % propulsion_id, 74 | "mission:sizing:main_route:climb:rate:%s" % propulsion_id, 75 | "mission:sizing:main_route:climb:speed:%s" % propulsion_id, 76 | "mission:sizing:main_route:cruise:speed:%s" % propulsion_id, 77 | "mission:sizing:main_route:stall:speed:%s" % propulsion_id, 78 | "mission:sizing:payload:power:%s" % propulsion_id, 79 | ] 80 | promote_and_rename(group=self, 81 | subsys=getattr(self, propulsion_id), 82 | old_patterns_list=old_patterns_list, 83 | new_patterns_list=new_patterns_list) 84 | 85 | -------------------------------------------------------------------------------- /src/fastuav/models/aerodynamics/aerodynamics_hybrid.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hybrid VTOL Aerodynamics (external) 3 | """ 4 | import fastoad.api as oad 5 | import openmdao.api as om 6 | import numpy as np 7 | from fastuav.utils.uncertainty import ( 8 | add_subsystem_with_deviation, 9 | ) 10 | from fastuav.models.aerodynamics.aerodynamics_fixedwing import WingParasiticDrag, TailParasiticDrag, FuselageParasiticDrag, ParasiticDragConstraint, MaxLiftToDrag 11 | from fastuav.constants import MR_PROPULSION 12 | 13 | 14 | class StoppedPropellersAerodynamicsModel: 15 | """ 16 | Aerodynamics model for the stopped propellers drag 17 | """ 18 | 19 | @staticmethod 20 | def stopped_propeller_drag_coefficient(alpha, N_blades=3, Cl_alpha=0.04 * 180 / np.pi, A_blade=7): 21 | """ 22 | Computes the parasitic drag coefficient of a stopped VTOL propeller when the aircraft is in forward flight, 23 | with an angle of attack alpha (rad). 24 | """ 25 | BAR = N_blades / (np.pi * A_blade) # [-] blade area ratio 26 | CD_blade = 0.1 # feather blade parasitic drag coefficient 27 | # CD_blade = (0.1 + Cl_alpha ** 2 * alpha ** 2) # feathered blade drag coefficient (with induced drag) 28 | CD_prop = BAR * CD_blade # [-] drag coefficient of the propeller 29 | return CD_prop 30 | 31 | 32 | @oad.RegisterOpenMDAOSystem("fastuav.aerodynamics.hybrid") 33 | class Aerodynamics(om.Group): 34 | """ 35 | Group containing the external aerodynamics calculation 36 | """ 37 | 38 | def setup(self): 39 | 40 | # Parasitic drag calculations 41 | parasitic_drag = self.add_subsystem("parasitic_drag", om.Group(), promotes=["*"]) 42 | parasitic_drag.add_subsystem("wing", WingParasiticDrag(), promotes=["*"]) 43 | parasitic_drag.add_subsystem("horizontal_tail", TailParasiticDrag(tail="horizontal"), promotes=["*"]) 44 | parasitic_drag.add_subsystem("vertical_tail", TailParasiticDrag(tail="vertical"), promotes=["*"]) 45 | parasitic_drag.add_subsystem("fuselage", FuselageParasiticDrag(), promotes=["*"]) 46 | parasitic_drag.add_subsystem("stopped_propellers", StoppedPropellersParasiticDrag(), promotes=["*"]) 47 | add_subsystem_with_deviation( 48 | parasitic_drag, 49 | "parasitic_drag", 50 | ParasiticDrag(), 51 | uncertain_outputs={"data:aerodynamics:CD0": None}, 52 | ) 53 | parasitic_drag.add_subsystem("constraint", ParasiticDragConstraint(), promotes=["*"]) 54 | 55 | # Lift to drag 56 | self.add_subsystem("lift_to_drag", MaxLiftToDrag(), promotes=["*"]) 57 | 58 | 59 | class ParasiticDrag(om.ExplicitComponent): 60 | """ 61 | Sums up the individual parasitic drags at cruise conditions 62 | """ 63 | 64 | def setup(self): 65 | self.add_input("data:aerodynamics:CD0:stopped_propellers", val=np.nan, units=None) 66 | self.add_input("data:aerodynamics:CD0:wing", val=np.nan, units=None) 67 | self.add_input("data:aerodynamics:CD0:tail:horizontal", val=np.nan, units=None) 68 | self.add_input("data:aerodynamics:CD0:tail:vertical", val=np.nan, units=None) 69 | self.add_input("data:aerodynamics:CD0:fuselage", val=np.nan, units=None) 70 | self.add_output("data:aerodynamics:CD0", units=None, lower=0.0) 71 | 72 | def setup_partials(self): 73 | # Finite difference all partials. 74 | self.declare_partials("*", "*", method="fd") 75 | 76 | def compute(self, inputs, outputs): 77 | outputs["data:aerodynamics:CD0"] = inputs["data:aerodynamics:CD0:stopped_propellers"] \ 78 | + inputs["data:aerodynamics:CD0:wing"] \ 79 | + inputs["data:aerodynamics:CD0:tail:horizontal"] \ 80 | + inputs["data:aerodynamics:CD0:tail:vertical"] \ 81 | + inputs["data:aerodynamics:CD0:fuselage"] 82 | 83 | 84 | class StoppedPropellersParasiticDrag(om.ExplicitComponent): 85 | """ 86 | Computes parasitic drag of VTOL stopped propellers at cruise conditions 87 | """ 88 | 89 | def initialize(self): 90 | self.options.declare("propulsion_id", default=MR_PROPULSION, values=[MR_PROPULSION]) 91 | 92 | def setup(self): 93 | propulsion_id = self.options["propulsion_id"] 94 | self.add_input("data:propulsion:%s:propeller:number" % propulsion_id, val=np.nan, units=None) 95 | self.add_input("data:propulsion:%s:propeller:diameter" % propulsion_id, val=np.nan, units="m") 96 | self.add_input("data:geometry:wing:surface", val=np.nan, units="m**2") 97 | self.add_output("data:aerodynamics:CD0:stopped_propellers", units=None, lower=0.0) 98 | 99 | def setup_partials(self): 100 | # Finite difference all partials. 101 | self.declare_partials("*", "*", method="fd") 102 | 103 | def compute(self, inputs, outputs): 104 | propulsion_id = self.options["propulsion_id"] 105 | N_pro = inputs["data:propulsion:%s:propeller:number" % propulsion_id] 106 | D_pro = inputs["data:propulsion:%s:propeller:diameter" % propulsion_id] 107 | S_ref = inputs["data:geometry:wing:surface"] 108 | alpha = 0.0 # TODO: get trimmed aircraft angle of attack 109 | 110 | # Total area of propellers 111 | S_pro = N_pro * np.pi * (D_pro / 2) ** 2 112 | 113 | # Parasitic drag coefficient 114 | CD_0_pro = StoppedPropellersAerodynamicsModel.stopped_propeller_drag_coefficient(alpha) * (S_pro / S_ref) 115 | 116 | outputs["data:aerodynamics:CD0:stopped_propellers"] = CD_0_pro -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/esc/catalogue.py: -------------------------------------------------------------------------------- 1 | """ 2 | Off-the-shelf ESC selection. 3 | """ 4 | import os.path as pth 5 | import openmdao.api as om 6 | from fastuav.utils.catalogues.estimators import NearestNeighbor 7 | from fastoad.openmdao.validity_checker import ValidityDomainChecker 8 | import pandas as pd 9 | import numpy as np 10 | 11 | 12 | PATH = pth.join( 13 | pth.dirname(pth.abspath(__file__)), 14 | "", 15 | "..", 16 | "..", 17 | "..", 18 | "data", 19 | "catalogues", 20 | "ESC", 21 | "Non-Dominated-ESC.csv", 22 | ) 23 | DF = pd.read_csv(PATH, sep=";") 24 | 25 | 26 | @ValidityDomainChecker( 27 | { 28 | "data:propulsion:esc:power:max:estimated": (DF["Pmax_W"].min(), DF["Pmax_W"].max()), 29 | "data:propulsion:esc:voltage:estimated": (DF["Vmax_V"].min(), DF["Vmax_V"].max()), 30 | }, 31 | ) 32 | class ESCCatalogueSelection(om.ExplicitComponent): 33 | def initialize(self): 34 | """ 35 | ESC selection and component's parameters assignment: 36 | - If off_the_shelf is True, an ESC is selected from the provided catalogue, according to the definition 37 | parameters. The component is then fully described by the manufacturer's data. 38 | - Otherwise, the previously estimated parameters are kept to describe the component. 39 | """ 40 | self.options.declare("off_the_shelf", default=False, types=bool) 41 | Pmax_selection = "next" 42 | Vmax_selection = "next" 43 | self._clf = NearestNeighbor( 44 | df=DF, X_names=["Pmax_W", "Vmax_V"], crits=[Pmax_selection, Vmax_selection] 45 | ) 46 | self._clf.train() 47 | 48 | def setup(self): 49 | # inputs: estimated values 50 | self.add_input("data:propulsion:esc:power:max:estimated", val=np.nan, units="W") 51 | self.add_input("data:propulsion:esc:voltage:estimated", val=np.nan, units="V") 52 | self.add_input("data:weight:propulsion:esc:mass:estimated", val=np.nan, units="kg") 53 | self.add_input("data:propulsion:esc:efficiency:estimated", val=np.nan, units=None) 54 | # outputs: catalogue values if off_the_shelf is True 55 | if self.options["off_the_shelf"]: 56 | self.add_output("data:propulsion:esc:power:max:catalogue", units="W") 57 | self.add_output("data:propulsion:esc:voltage:catalogue", units="V") 58 | self.add_output("data:weight:propulsion:esc:mass:catalogue", units="kg") 59 | # self.add_output('data:propulsion:esc:efficiency:catalogue', units=None) 60 | # outputs: 'real' values (= estimated values if off_the_shelf is False, catalogue values else) 61 | self.add_output("data:propulsion:esc:power:max", units="W") 62 | self.add_output("data:propulsion:esc:voltage", units="V") 63 | self.add_output("data:weight:propulsion:esc:mass", units="kg") 64 | self.add_output("data:propulsion:esc:efficiency", units=None) 65 | 66 | def setup_partials(self): 67 | self.declare_partials( 68 | "data:propulsion:esc:power:max", 69 | "data:propulsion:esc:power:max:estimated", 70 | val=1.0, 71 | ) 72 | self.declare_partials( 73 | "data:propulsion:esc:voltage", 74 | "data:propulsion:esc:voltage:estimated", 75 | val=1.0, 76 | ) 77 | self.declare_partials( 78 | "data:weight:propulsion:esc:mass", 79 | "data:weight:propulsion:esc:mass:estimated", 80 | val=1.0, 81 | ) 82 | self.declare_partials( 83 | "data:propulsion:esc:efficiency", 84 | "data:propulsion:esc:efficiency:estimated", 85 | val=1.0, 86 | ) 87 | 88 | def compute(self, inputs, outputs): 89 | """ 90 | This method evaluates the decision tree 91 | """ 92 | 93 | # OFF-THE-SHELF COMPONENTS SELECTION 94 | if self.options["off_the_shelf"]: 95 | # Definition parameters for ESC selection 96 | P_esc_opt = inputs["data:propulsion:esc:power:max:estimated"] 97 | U_esc_opt = inputs["data:propulsion:esc:voltage:estimated"] 98 | 99 | # Get closest product 100 | df_y = self._clf.predict([P_esc_opt, U_esc_opt]) 101 | P_esc = df_y["Pmax_W"].iloc[0] # [W] ESC power 102 | U_esc = df_y["Vmax_V"].iloc[0] # [V] ESC voltage 103 | m_esc = df_y["Weight_g"].iloc[0] / 1000 # [kg] ESC mass 104 | 105 | # Outputs 106 | outputs["data:propulsion:esc:power:max"] = outputs[ 107 | "data:propulsion:esc:power:max:catalogue" 108 | ] = P_esc 109 | outputs["data:propulsion:esc:voltage"] = outputs[ 110 | "data:propulsion:esc:voltage:catalogue" 111 | ] = U_esc 112 | outputs["data:weight:propulsion:esc:mass"] = outputs["data:weight:propulsion:esc:mass:catalogue"] = m_esc 113 | outputs["data:propulsion:esc:efficiency"] = inputs[ 114 | "data:propulsion:esc:efficiency:estimated" 115 | ] 116 | 117 | # CUSTOM COMPONENTS (no change) 118 | else: 119 | outputs["data:propulsion:esc:power:max"] = inputs[ 120 | "data:propulsion:esc:power:max:estimated" 121 | ] 122 | outputs["data:propulsion:esc:voltage"] = inputs["data:propulsion:esc:voltage:estimated"] 123 | outputs["data:weight:propulsion:esc:mass"] = inputs["data:weight:propulsion:esc:mass:estimated"] 124 | outputs["data:propulsion:esc:efficiency"] = inputs[ 125 | "data:propulsion:esc:efficiency:estimated" 126 | ] 127 | -------------------------------------------------------------------------------- /src/fastuav/models/geometry/geometry_multirotor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Multirotor Airframe Geometry 3 | """ 4 | import fastoad.api as oad 5 | import openmdao.api as om 6 | import numpy as np 7 | from fastuav.constants import MR_PROPULSION 8 | 9 | 10 | @oad.RegisterOpenMDAOSystem("fastuav.geometry.multirotor") 11 | class Geometry(om.Group): 12 | """ 13 | Computes Multi-Rotor geometry 14 | """ 15 | 16 | def initialize(self): 17 | self.options.declare("propulsion_id", default=MR_PROPULSION, values=[MR_PROPULSION]) 18 | 19 | def setup(self): 20 | propulsion_id = self.options["propulsion_id"] 21 | self.add_subsystem("arms", ArmsGeometry(propulsion_id=propulsion_id), promotes=["*"]) 22 | # + Body geometry is estimated in the "Scenarios" module instead of here to avoid algebraic loop. 23 | # See 'ProjectedAreasGuess' class. 24 | # self.add_subsystem("body", BodyGeometry(), promotes=["*"]) 25 | 26 | 27 | class ArmsGeometry(om.ExplicitComponent): 28 | """ 29 | Computes arms geometry 30 | """ 31 | def initialize(self): 32 | self.options.declare("propulsion_id", default=MR_PROPULSION, values=[MR_PROPULSION]) 33 | 34 | def setup(self): 35 | propulsion_id = self.options["propulsion_id"] 36 | self.add_input("data:propulsion:%s:propeller:diameter" % propulsion_id, val=np.nan, units="m") 37 | self.add_input("data:propulsion:%s:propeller:number" % propulsion_id, val=np.nan, units=None) 38 | self.add_input("data:propulsion:%s:propeller:is_coaxial" % propulsion_id, val=np.nan, units=None) 39 | self.add_output("data:geometry:arms:prop_per_arm", units=None) 40 | self.add_output("data:geometry:arms:number", units=None) 41 | self.add_output("data:geometry:arms:length", units="m", lower=0.0) 42 | 43 | def setup_partials(self): 44 | # Finite difference all partials. 45 | self.declare_partials("*", "*", method="fd") 46 | 47 | def compute(self, inputs, outputs): 48 | propulsion_id = self.options["propulsion_id"] 49 | Dpro = inputs["data:propulsion:%s:propeller:diameter" % propulsion_id] 50 | N_pro = inputs["data:propulsion:%s:propeller:number" % propulsion_id] 51 | is_coaxial = inputs["data:propulsion:%s:propeller:is_coaxial" % propulsion_id] 52 | 53 | Npro_arm = 1 + is_coaxial 54 | Narm = N_pro / Npro_arm 55 | Larm = Dpro / 2 / (np.sin(np.pi / Narm)) # [m] length of the arm (minimum volume allocation) 56 | 57 | outputs["data:geometry:arms:prop_per_arm"] = Npro_arm 58 | outputs["data:geometry:arms:number"] = Narm 59 | outputs["data:geometry:arms:length"] = Larm 60 | 61 | 62 | class BodyGeometry(om.ExplicitComponent): 63 | """ 64 | Computes body's top and front areas. 65 | For now this is a simple duplicate of the projected areas estimates, 66 | such that no consistency constraint for the projected areas is needed. 67 | """ 68 | 69 | def setup(self): 70 | self.add_input("data:geometry:projected_area:top", val=np.nan, units="m**2") 71 | self.add_input("data:geometry:projected_area:front", val=np.nan, units="m**2") 72 | self.add_output("data:geometry:body:surface:top", units="m**2") 73 | self.add_output("data:geometry:body:surface:front", units="m**2") 74 | 75 | def setup_partials(self): 76 | self.declare_partials("*", "*", method="fd") 77 | 78 | def compute(self, inputs, outputs): 79 | outputs["data:geometry:body:surface:top"] = inputs["data:geometry:projected_area:top"] 80 | outputs["data:geometry:body:surface:front"] = inputs["data:geometry:projected_area:front"] 81 | 82 | 83 | class ProjectedAreasGuess(om.ExplicitComponent): 84 | """ 85 | Computes a rough estimate of the projected areas with scaling laws. 86 | This is used as a preliminary calculation for sizing scenarios. 87 | """ 88 | 89 | def setup(self): 90 | self.add_input("optimization:variables:weight:mtow:guess", val=np.nan, units="kg") 91 | self.add_input("models:geometry:body:surface:top:reference", val=np.nan, units="m**2") 92 | self.add_input("models:geometry:body:surface:front:reference", val=np.nan, units="m**2") 93 | self.add_input("models:weight:mtow:reference", val=np.nan, units="kg") 94 | self.add_input("optimization:variables:geometry:projected_area:top:k", val=1.0, units=None) 95 | self.add_input("optimization:variables:geometry:projected_area:front:k", val=1.0, units=None) 96 | self.add_output("data:geometry:projected_area:top", units="m**2") 97 | self.add_output("data:geometry:projected_area:front", units="m**2") 98 | 99 | def setup_partials(self): 100 | self.declare_partials("*", "*", method="fd") 101 | 102 | def compute(self, inputs, outputs): 103 | m_uav_guess = inputs["optimization:variables:weight:mtow:guess"] 104 | S_top_ref = inputs["models:geometry:body:surface:top:reference"] 105 | S_front_ref = inputs["models:geometry:body:surface:front:reference"] 106 | MTOW_ref = inputs["models:weight:mtow:reference"] 107 | k_top = inputs["optimization:variables:geometry:projected_area:top:k"] 108 | k_front = inputs["optimization:variables:geometry:projected_area:front:k"] 109 | 110 | S_top = k_top * S_top_ref * (m_uav_guess / MTOW_ref) ** ( 111 | 2 / 3 112 | ) # [m2] top surface estimation 113 | S_front = k_front * S_front_ref * (m_uav_guess / MTOW_ref) ** ( 114 | 2 / 3 115 | ) # [m2] front surface estimation 116 | 117 | outputs["data:geometry:projected_area:top"] = S_top 118 | outputs["data:geometry:projected_area:front"] = S_front -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/motor/definition_parameters.py: -------------------------------------------------------------------------------- 1 | """ 2 | Definition parameters for the motor. 3 | """ 4 | import openmdao.api as om 5 | import numpy as np 6 | from fastuav.utils.uncertainty import add_subsystem_with_deviation 7 | 8 | 9 | class MotorDefinitionParameters(om.Group): 10 | """ 11 | Group containing the calculation of the definition parameters for the motor. 12 | The definition parameters are independent variables that allow to derive all the other component's parameters, 13 | by using datasheets or estimation models. 14 | The definition parameters for the motor are the maximum torque and the torque coefficient. 15 | """ 16 | 17 | def setup(self): 18 | add_subsystem_with_deviation( 19 | self, 20 | "max_torque", 21 | MaxTorque(), 22 | uncertain_outputs={"data:propulsion:motor:torque:max:estimated": "N*m"}, 23 | ) 24 | 25 | add_subsystem_with_deviation( 26 | self, 27 | "velocity_constant", 28 | VelocityConstant(), 29 | uncertain_outputs={"data:propulsion:motor:speed:constant:estimated": "rad/V/s"}, 30 | ) 31 | 32 | 33 | class MaxTorque(om.ExplicitComponent): 34 | """ 35 | Estimates the maximum motor torque from the takeoff flight scenario. 36 | """ 37 | 38 | def setup(self): 39 | self.add_input("data:propulsion:gearbox:N_red", val=1.0, units=None) 40 | self.add_input("data:propulsion:propeller:torque:takeoff", val=np.nan, units="N*m") 41 | self.add_input("optimization:variables:propulsion:motor:torque:k", val=np.nan, units=None) 42 | self.add_output("data:propulsion:motor:torque:max:estimated", units="N*m") 43 | 44 | def setup_partials(self): 45 | self.declare_partials("*", "*", method="exact") 46 | 47 | def compute(self, inputs, outputs): 48 | N_red = inputs["data:propulsion:gearbox:N_red"] 49 | Q_pro_to = inputs["data:propulsion:propeller:torque:takeoff"] 50 | k_mot = inputs["optimization:variables:propulsion:motor:torque:k"] 51 | 52 | T_mot_to = Q_pro_to / N_red # [N.m] takeoff torque 53 | T_mot_max = k_mot * T_mot_to # [N.m] required motor nominal torque 54 | 55 | outputs["data:propulsion:motor:torque:max:estimated"] = T_mot_max 56 | 57 | def compute_partials(self, inputs, partials, discrete_inputs=None): 58 | N_red = inputs["data:propulsion:gearbox:N_red"] 59 | Q_pro_to = inputs["data:propulsion:propeller:torque:takeoff"] 60 | k_mot = inputs["optimization:variables:propulsion:motor:torque:k"] 61 | 62 | partials[ 63 | "data:propulsion:motor:torque:max:estimated", "data:propulsion:gearbox:N_red" 64 | ] = (-k_mot * Q_pro_to / N_red**2) 65 | partials[ 66 | "data:propulsion:motor:torque:max:estimated", 67 | "data:propulsion:propeller:torque:takeoff", 68 | ] = ( 69 | k_mot / N_red 70 | ) 71 | partials[ 72 | "data:propulsion:motor:torque:max:estimated", "optimization:variables:propulsion:motor:torque:k" 73 | ] = (Q_pro_to / N_red) 74 | 75 | 76 | class VelocityConstant(om.ExplicitComponent): 77 | """ 78 | Estimates the motor velocity constant 79 | """ 80 | 81 | def setup(self): 82 | self.add_input("data:propulsion:gearbox:N_red", val=1.0, units=None) 83 | self.add_input("data:propulsion:propeller:power:takeoff", val=np.nan, units="W") 84 | self.add_input("data:propulsion:propeller:speed:takeoff", val=np.nan, units="rad/s") 85 | self.add_input("optimization:variables:propulsion:motor:speed:k", val=np.nan, units=None) 86 | self.add_output("data:propulsion:motor:speed:constant:estimated", units="rad/V/s") 87 | 88 | def setup_partials(self): 89 | self.declare_partials("*", "*", method="exact") 90 | 91 | def compute(self, inputs, outputs): 92 | N_red = inputs["data:propulsion:gearbox:N_red"] 93 | W_pro_to = inputs["data:propulsion:propeller:speed:takeoff"] 94 | P_pro_to = inputs["data:propulsion:propeller:power:takeoff"] 95 | k_speed_mot = inputs["optimization:variables:propulsion:motor:speed:k"] 96 | 97 | # TODO: replace W_mot_to / U_bat_guess by Kv_hat = 41.59 T_nom ** (-0.35) (datasheet regression) 98 | W_mot_to = W_pro_to * N_red # [rad/s] Motor take-off speed 99 | U_bat_guess = 1.84 * P_pro_to ** 0.36 # [V] battery voltage estimation 100 | Kv = k_speed_mot * W_mot_to / U_bat_guess # [rad/V/s] 101 | 102 | outputs["data:propulsion:motor:speed:constant:estimated"] = Kv 103 | 104 | def compute_partials(self, inputs, partials, discrete_inputs=None): 105 | N_red = inputs["data:propulsion:gearbox:N_red"] 106 | P_pro_to = inputs["data:propulsion:propeller:power:takeoff"] 107 | W_pro_to = inputs["data:propulsion:propeller:speed:takeoff"] 108 | k_speed_mot = inputs["optimization:variables:propulsion:motor:speed:k"] 109 | 110 | U_bat_guess = 1.84 * P_pro_to ** 0.36 # [V] battery voltage estimation 111 | 112 | partials[ 113 | "data:propulsion:motor:speed:constant:estimated", "data:propulsion:gearbox:N_red" 114 | ] = k_speed_mot * W_pro_to / U_bat_guess 115 | 116 | partials[ 117 | "data:propulsion:motor:speed:constant:estimated", 118 | "data:propulsion:propeller:power:takeoff", 119 | ] = - 0.36 * k_speed_mot * W_pro_to * N_red * P_pro_to ** (-1.36) / 1.84 120 | 121 | partials[ 122 | "data:propulsion:motor:speed:constant:estimated", 123 | "data:propulsion:propeller:speed:takeoff", 124 | ] = k_speed_mot / U_bat_guess 125 | 126 | partials[ 127 | "data:propulsion:motor:speed:constant:estimated", "optimization:variables:propulsion:motor:speed:k" 128 | ] = W_pro_to * N_red / U_bat_guess -------------------------------------------------------------------------------- /src/fastuav/notebooks/data/source_files/problem_inputs_doe.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5.621522065691114 5 | 6 | 7 | 8 | 1.0 9 | 10 | 11 | 12 | 13 | 0.050525883729765 14 | 0.11368323839197124 15 | 16 | 17 | 18 | 19 | 20 | 1157.5996922738536 21 | 24.539307701323104 22 | 23 | 0.8 24 | 25 | 26 | 908.7055062731744 27 | 1203.518728858352 28 | 742.1372450560945 29 | 30 | 31 | 32 | 0.95 33 | 34 | 35 | 1.0 36 | 37 | 38 | 0.7459200490548663 39 | 40 | 69.32796005438682 41 | 42 | 43 | 0.0018119367048259004 44 | 45 | 46 | 47 | 0.3076179950398614 48 | 0.20889179004256567 49 | 12.0 50 | 51 | 52 | [0.01813, -0.06218, 0.35712, -0.23774, 0.00343, -0.1235, 0.0, 0.07549, 0.0, 0.0, 0.286, 0.993] 53 | 54 | 55 | 56 | 57 | [0.02791, 0.11867, 0.27334, -0.28852, -0.06543, -0.23504, 0.02104, 0.0, 0.0, 0.18677, 0.197, 1.094] 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 0.0 67 | 68 | 69 | 3.0 70 | 3.0 71 | 72 | 0.0 73 | 74 | 75 | 76 | 120.0 77 | 10000.0 78 | 15.0 79 | 80 | 0.0 81 | 82 | 83 | 84 | 2.0 85 | 86 | 0.0 87 | 88 | 89 | 90 | 2.0 91 | 92 | 93 | 0.0 94 | 95 | 96 | 97 | 98 | 99 | 2.0 100 | 101 | 102 | 103 | 120.0 104 | 1000.0 105 | 106 | 20.0 107 | 108 | 109 | 110 | 1.0 111 | 112 | 113 | 0.0 114 | 115 | 116 | 117 | 3.0 118 | 119 | 120 | 3.0 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/propeller/estimation_models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Estimation models for the propeller 3 | """ 4 | import openmdao.api as om 5 | import numpy as np 6 | from fastuav.utils.uncertainty import ( 7 | add_subsystem_with_deviation, 8 | add_model_deviation, 9 | ) 10 | from fastuav.models.propulsion.propeller.aerodynamics.surrogate_models import PropellerAerodynamicsModel 11 | from stdatm import AtmosphereSI 12 | import logging 13 | 14 | _LOGGER = logging.getLogger(__name__) # Logger for this module 15 | 16 | 17 | class PropellerEstimationModels(om.Group): 18 | """ 19 | Group containing the estimation models for the propeller. 20 | Estimation models take a reduced set of definition parameters and estimate the main component characteristics from it. 21 | """ 22 | 23 | def setup(self): 24 | add_subsystem_with_deviation( 25 | self, 26 | "diameter", 27 | Diameter(), 28 | uncertain_outputs={"data:propulsion:propeller:diameter:estimated": "m"}, 29 | ) 30 | 31 | add_subsystem_with_deviation( 32 | self, 33 | "weight", 34 | Weight(), 35 | uncertain_outputs={"data:weight:propulsion:propeller:mass:estimated": "kg"}, 36 | ) 37 | 38 | self.add_subsystem("figure_of_merit", FigureOfMerit(), promotes=["*"]) 39 | 40 | 41 | class Diameter(om.ExplicitComponent): 42 | """ 43 | Computes propeller diameter from the takeoff scenario. 44 | """ 45 | 46 | def setup(self): 47 | self.add_input("data:propulsion:propeller:thrust:takeoff", val=np.nan, units="N") 48 | self.add_input("mission:sizing:main_route:takeoff:altitude", val=0.0, units="m") 49 | self.add_input("mission:sizing:dISA", val=0.0, units="K") 50 | self.add_input("data:propulsion:propeller:beta:estimated", val=np.nan, units=None) 51 | self.add_input("data:propulsion:propeller:Ct:static:polynomial:estimated", shape_by_conn=True, val=np.nan, units=None) 52 | self.add_input("data:propulsion:propeller:Cp:static:polynomial:estimated", shape_by_conn=True, val=np.nan, units=None) 53 | self.add_input("data:propulsion:propeller:ND:takeoff", val=np.nan, units="m/s") 54 | self.add_output("data:propulsion:propeller:diameter:estimated", units="m") 55 | 56 | def setup_partials(self): 57 | # Finite difference all partials. 58 | self.declare_partials("*", "*", method="fd") 59 | 60 | def compute(self, inputs, outputs): 61 | F_pro_to = inputs["data:propulsion:propeller:thrust:takeoff"] 62 | ND_to = inputs["data:propulsion:propeller:ND:takeoff"] 63 | beta = inputs["data:propulsion:propeller:beta:estimated"] 64 | ct_model = inputs["data:propulsion:propeller:Ct:static:polynomial:estimated"] 65 | cp_model = inputs["data:propulsion:propeller:Cp:static:polynomial:estimated"] 66 | 67 | altitude_takeoff = inputs["mission:sizing:main_route:takeoff:altitude"] 68 | dISA = inputs["mission:sizing:dISA"] 69 | rho_air = AtmosphereSI( 70 | altitude_takeoff, dISA 71 | ).density # [kg/m3] Air density at takeoff level 72 | 73 | c_t, c_p = PropellerAerodynamicsModel.aero_coefficients_static(beta, 74 | ct_model=ct_model, 75 | cp_model=cp_model) 76 | 77 | Dpro = (F_pro_to / (c_t * rho_air * ND_to**2)) ** 0.5 # [m] Propeller diameter 78 | 79 | outputs["data:propulsion:propeller:diameter:estimated"] = Dpro 80 | 81 | 82 | class Weight(om.ExplicitComponent): 83 | """ 84 | Computes propeller weight 85 | """ 86 | 87 | def setup(self): 88 | self.add_input("data:propulsion:propeller:diameter:estimated", val=np.nan, units="m") 89 | self.add_input("models:propulsion:propeller:diameter:reference", val=np.nan, units="m") 90 | self.add_input("models:weight:propulsion:propeller:mass:reference", val=np.nan, units="kg") 91 | self.add_output("data:weight:propulsion:propeller:mass:estimated", units="kg") 92 | 93 | def setup_partials(self): 94 | # Finite difference all partials. 95 | self.declare_partials("*", "*", method="fd") 96 | 97 | def compute(self, inputs, outputs): 98 | Dpro = inputs["data:propulsion:propeller:diameter:estimated"] 99 | Dpro_ref = inputs["models:propulsion:propeller:diameter:reference"] 100 | m_pro_ref = inputs["models:weight:propulsion:propeller:mass:reference"] 101 | 102 | m_pro = m_pro_ref * (Dpro / Dpro_ref) ** 3 # [kg] Propeller mass 103 | 104 | outputs["data:weight:propulsion:propeller:mass:estimated"] = m_pro 105 | 106 | 107 | class FigureOfMerit(om.ExplicitComponent): 108 | """ 109 | Computes figure of merit of propeller. 110 | """ 111 | 112 | def setup(self): 113 | self.add_input("data:propulsion:propeller:beta:estimated", val=np.nan, units=None) 114 | self.add_input("data:propulsion:propeller:Ct:static:polynomial:estimated", shape_by_conn=True, val=np.nan, units=None) 115 | self.add_input("data:propulsion:propeller:Cp:static:polynomial:estimated", shape_by_conn=True, val=np.nan, units=None) 116 | self.add_output("data:propulsion:propeller:FoM:estimated", units=None) 117 | 118 | def setup_partials(self): 119 | # Finite difference all partials. 120 | self.declare_partials("*", "*", method="fd") 121 | 122 | def compute(self, inputs, outputs): 123 | beta = inputs["data:propulsion:propeller:beta:estimated"] 124 | ct_model = inputs["data:propulsion:propeller:Ct:static:polynomial:estimated"] 125 | cp_model = inputs["data:propulsion:propeller:Cp:static:polynomial:estimated"] 126 | 127 | c_t, c_p = PropellerAerodynamicsModel.aero_coefficients_static(beta, 128 | ct_model=ct_model, 129 | cp_model=cp_model) 130 | 131 | FoM = c_t ** (3/2) / c_p 132 | 133 | outputs["data:propulsion:propeller:FoM:estimated"] = FoM 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /src/fastuav/models/stability/static_longitudinal/center_of_gravity/components/cog_propulsion.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module containing the center of gravity calculations for all components, on the longitudinal axis 3 | """ 4 | import openmdao.api as om 5 | import numpy as np 6 | from fastuav.constants import FW_PROPULSION, MR_PROPULSION 7 | 8 | 9 | class CoG_propulsion_FW(om.ExplicitComponent): 10 | """ 11 | Computes the center of gravity of the fixed wing propulsion system 12 | """ 13 | 14 | def initialize(self): 15 | self.options.declare("propulsion_id", default=FW_PROPULSION, values=[FW_PROPULSION]) 16 | self.options.declare("propulsion_conf", default="tractor", values=["tractor", "pusher"]) 17 | 18 | def setup(self): 19 | propulsion_id = self.options["propulsion_id"] 20 | propulsion_conf = self.options["propulsion_conf"] 21 | 22 | self.add_input("data:geometry:wing:root:LE:x", val=np.nan, units="m") 23 | self.add_input("data:geometry:wing:root:TE:x", val=np.nan, units="m") 24 | self.add_input("data:propulsion:%s:motor:length:estimated" % propulsion_id, val=np.nan, units="m") 25 | if propulsion_conf == "pusher": 26 | self.add_input("data:geometry:fuselage:length", val=np.nan, units="m") 27 | self.add_input("data:propulsion:%s:propeller:number" % propulsion_id, val=np.nan, units=None) 28 | self.add_input("data:weight:propulsion:%s:propeller:mass" % propulsion_id, val=np.nan, units="kg") 29 | self.add_input("data:weight:propulsion:%s:motor:mass" % propulsion_id, val=np.nan, units="kg") 30 | self.add_input("data:weight:propulsion:%s:battery:mass" % propulsion_id, val=np.nan, units="kg") 31 | 32 | self.add_output("data:weight:propulsion:%s" % propulsion_id, units="kg") 33 | self.add_output("data:stability:CoG:propulsion:%s" % propulsion_id, units="m") 34 | 35 | def setup_partials(self): 36 | self.declare_partials("*", "*", method="fd") 37 | 38 | def compute(self, inputs, outputs): 39 | propulsion_id = self.options["propulsion_id"] 40 | propulsion_conf = self.options["propulsion_conf"] 41 | x_root_LE_w = inputs["data:geometry:wing:root:LE:x"] 42 | x_root_TE_w = inputs["data:geometry:wing:root:TE:x"] 43 | l_mot = inputs["data:propulsion:%s:motor:length:estimated" % propulsion_id] 44 | m_pro = inputs["data:weight:propulsion:%s:propeller:mass" % propulsion_id] 45 | m_mot = inputs["data:weight:propulsion:%s:motor:mass" % propulsion_id] 46 | m_bat = inputs["data:weight:propulsion:%s:battery:mass" % propulsion_id] 47 | N_pro = inputs["data:propulsion:%s:propeller:number" % propulsion_id] 48 | 49 | if propulsion_conf == "pusher": 50 | l_fus = inputs["data:geometry:fuselage:length"] 51 | x_cg_pro = l_fus # propeller located at fuselage rear [m] 52 | x_cg_mot = l_fus - l_mot / 2 # motor located at fuselage rear [m] 53 | else: 54 | x_cg_pro = 0 # propeller located at nose tip [m] 55 | x_cg_mot = l_mot / 2 # motor located at the nose tip [m] 56 | 57 | x_cg_bat = (x_root_LE_w + x_root_TE_w) / 2 # [m] wing-integrated or centered at wing position 58 | 59 | m_propulsion = N_pro * m_pro + N_pro * m_mot + m_bat 60 | x_cg_propulsion = (N_pro * x_cg_pro * m_pro + N_pro * x_cg_mot * m_mot + x_cg_bat * m_bat) / m_propulsion 61 | 62 | outputs["data:weight:propulsion:%s" % propulsion_id] = m_propulsion 63 | outputs["data:stability:CoG:propulsion:%s" % propulsion_id] = x_cg_propulsion 64 | 65 | 66 | class CoG_propulsion_MR(om.ExplicitComponent): 67 | """ 68 | Computes the center of gravity of the VTOL propulsion system 69 | """ 70 | 71 | def initialize(self): 72 | self.options.declare("propulsion_id", default=MR_PROPULSION, values=[MR_PROPULSION]) 73 | 74 | def setup(self): 75 | propulsion_id = self.options["propulsion_id"] 76 | 77 | self.add_input("data:geometry:%s:propeller:x:front" % propulsion_id, val=np.nan, units="m") 78 | self.add_input("data:geometry:%s:propeller:x:rear" % propulsion_id, val=np.nan, units="m") 79 | self.add_input("data:geometry:wing:root:LE:x", val=np.nan, units="m") 80 | self.add_input("data:geometry:wing:root:TE:x", val=np.nan, units="m") 81 | self.add_input("data:weight:propulsion:%s:propeller:mass" % propulsion_id, val=np.nan, units="kg") 82 | self.add_input("data:weight:propulsion:%s:motor:mass" % propulsion_id, val=np.nan, units="kg") 83 | self.add_input("data:weight:propulsion:%s:battery:mass" % propulsion_id, val=np.nan, units="kg") 84 | self.add_input("data:propulsion:%s:propeller:number" % propulsion_id, val=np.nan, units=None) 85 | 86 | self.add_output("data:weight:propulsion:%s" % propulsion_id, units="kg") 87 | self.add_output("data:stability:CoG:propulsion:%s" % propulsion_id, units="m") 88 | 89 | def setup_partials(self): 90 | self.declare_partials("*", "*", method="fd") 91 | 92 | def compute(self, inputs, outputs): 93 | propulsion_id = self.options["propulsion_id"] 94 | x_pro_front = inputs["data:geometry:%s:propeller:x:front" % propulsion_id] 95 | x_pro_rear = inputs["data:geometry:%s:propeller:x:rear" % propulsion_id] 96 | x_root_LE_w = inputs["data:geometry:wing:root:LE:x"] 97 | x_root_TE_w = inputs["data:geometry:wing:root:TE:x"] 98 | m_pro = inputs["data:weight:propulsion:%s:propeller:mass" % propulsion_id] 99 | m_mot = inputs["data:weight:propulsion:%s:motor:mass" % propulsion_id] 100 | m_bat = inputs["data:weight:propulsion:%s:battery:mass" % propulsion_id] 101 | N_pro = inputs["data:propulsion:%s:propeller:number" % propulsion_id] 102 | 103 | x_cg_pro = x_cg_mot = (x_pro_front + x_pro_rear) / 2 # [m] average position of the propellers / motors 104 | x_cg_bat = (x_root_LE_w + x_root_TE_w) / 2 # [m] wing-integrated or centered at wing position 105 | 106 | m_propulsion = N_pro * m_pro + N_pro * m_mot + m_bat 107 | x_cg_propulsion = (N_pro * x_cg_pro * m_pro + N_pro * x_cg_mot * m_mot + x_cg_bat * m_bat) / m_propulsion 108 | 109 | outputs["data:weight:propulsion:%s" % propulsion_id] = m_propulsion 110 | outputs["data:stability:CoG:propulsion:%s" % propulsion_id] = x_cg_propulsion 111 | -------------------------------------------------------------------------------- /src/fastuav/models/performance/range_and_endurance.py: -------------------------------------------------------------------------------- 1 | """ 2 | Calculations of the maximum range and endurance based on the sizing scenarios parameters. 3 | """ 4 | import fastoad.api as oad 5 | import openmdao.api as om 6 | import numpy as np 7 | from fastuav.constants import MR_PROPULSION, FW_PROPULSION, PROPULSION_ID_LIST, HOVER_TAG, CRUISE_TAG 8 | 9 | 10 | @oad.RegisterOpenMDAOSystem("fastuav.performance.endurance.multirotor") 11 | class EnduranceMultirotor(om.Group): 12 | """ 13 | Endurance and range calculations for multirotor UAVs 14 | """ 15 | 16 | def setup(self): 17 | self.add_subsystem("hover", 18 | Endurance(propulsion_id=MR_PROPULSION, phase_name=HOVER_TAG), 19 | promotes=["*"]) 20 | self.add_subsystem("cruise", 21 | Endurance(propulsion_id=MR_PROPULSION, phase_name=CRUISE_TAG), 22 | promotes=["*"]) 23 | 24 | 25 | @oad.RegisterOpenMDAOSystem("fastuav.performance.endurance.fixedwing") 26 | class EnduranceFixedWing(om.Group): 27 | """ 28 | Endurance and range calculations for multirotor UAVs 29 | """ 30 | 31 | def setup(self): 32 | self.add_subsystem("cruise", 33 | Endurance(propulsion_id=FW_PROPULSION, phase_name=CRUISE_TAG), 34 | promotes=["*"]) 35 | 36 | 37 | @oad.RegisterOpenMDAOSystem("fastuav.performance.endurance.hybrid") 38 | class EnduranceHybrid(om.Group): 39 | """ 40 | Endurance and range calculations for hybrid (fixed wing VTOL) UAVs 41 | """ 42 | 43 | def setup(self): 44 | self.add_subsystem("hover", 45 | Endurance(propulsion_id=MR_PROPULSION, phase_name=HOVER_TAG), 46 | promotes=["*"]) 47 | self.add_subsystem("cruise", 48 | Endurance(propulsion_id=FW_PROPULSION, phase_name=CRUISE_TAG), 49 | promotes=["*"]) 50 | 51 | 52 | class Endurance(om.ExplicitComponent): 53 | """ 54 | Endurance and range calculations for given flight scenario (at design payload). 55 | """ 56 | 57 | def initialize(self): 58 | self.options.declare("propulsion_id", 59 | default=FW_PROPULSION, values=PROPULSION_ID_LIST) 60 | self.options.declare("phase_name", 61 | default=HOVER_TAG, values=[HOVER_TAG, CRUISE_TAG]) 62 | 63 | def setup(self): 64 | propulsion_id = self.options["propulsion_id"] 65 | phase_name = self.options["phase_name"] 66 | self.add_input("data:propulsion:%s:battery:capacity" % propulsion_id, val=np.nan, units="A*s") 67 | self.add_input("data:propulsion:%s:battery:DoD:max" % propulsion_id, val=0.8, units=None) 68 | self.add_input("data:propulsion:%s:battery:current:%s" % (propulsion_id, phase_name), val=np.nan, 69 | units="A") 70 | if phase_name != HOVER_TAG: 71 | self.add_input("mission:sizing:main_route:%s:speed:%s" % (phase_name, propulsion_id), val=0.0, units="m/s") 72 | self.add_output("data:performance:range:%s" % phase_name, units="m") 73 | self.add_output("data:performance:endurance:%s" % phase_name, units="min") 74 | 75 | def setup_partials(self): 76 | self.declare_partials("*", "*", method="exact") 77 | 78 | def compute(self, inputs, outputs): 79 | propulsion_id = self.options["propulsion_id"] 80 | phase_name = self.options["phase_name"] 81 | C_ratio = inputs["data:propulsion:%s:battery:DoD:max" % propulsion_id] 82 | C_bat = inputs["data:propulsion:%s:battery:capacity" % propulsion_id] 83 | I_bat = inputs["data:propulsion:%s:battery:current:%s" % (propulsion_id, phase_name)] 84 | 85 | # Endurance calculation 86 | t_max = C_ratio * C_bat / I_bat if I_bat > 0 else 0.0 # [s] Max. cruise flight time at design payload 87 | 88 | # Range calculation 89 | if phase_name != HOVER_TAG: 90 | V = inputs["mission:sizing:main_route:%s:speed:%s" % (phase_name, propulsion_id)] 91 | D_max = V * t_max # [m] Max. Range at given cruise speed and design payload 92 | outputs["data:performance:range:%s" % phase_name] = D_max # [m] 93 | 94 | outputs["data:performance:endurance:%s" % phase_name] = t_max / 60.0 # [min] 95 | 96 | def compute_partials(self, inputs, partials, discrete_inputs=None): 97 | propulsion_id = self.options["propulsion_id"] 98 | phase_name = self.options["phase_name"] 99 | C_ratio = inputs["data:propulsion:%s:battery:DoD:max" % propulsion_id] 100 | C_bat = inputs["data:propulsion:%s:battery:capacity" % propulsion_id] 101 | I_bat = inputs["data:propulsion:%s:battery:current:%s" % (propulsion_id, phase_name)] 102 | t_max = C_ratio * C_bat / I_bat if I_bat > 0 else 0.0 103 | 104 | partials["data:performance:endurance:%s" % phase_name, 105 | "data:propulsion:%s:battery:DoD:max" % propulsion_id] = C_bat / I_bat / 60.0 if I_bat > 0 else 0.0 106 | partials["data:performance:endurance:%s" % phase_name, 107 | "data:propulsion:%s:battery:capacity" % propulsion_id] = C_ratio / I_bat / 60.0 if I_bat > 0 else 0.0 108 | partials[ 109 | "data:performance:endurance:%s" % phase_name, 110 | "data:propulsion:%s:battery:current:%s" % (propulsion_id, phase_name) 111 | ] = -C_ratio * C_bat / I_bat**2 / 60.0 112 | 113 | if phase_name != HOVER_TAG: 114 | V = inputs["mission:sizing:main_route:%s:speed:%s" % (phase_name, propulsion_id)] 115 | partials["data:performance:range:%s" % phase_name, 116 | "mission:sizing:main_route:%s:speed:%s" % (phase_name, propulsion_id)] = t_max 117 | partials["data:performance:range:%s" % phase_name, 118 | "data:propulsion:%s:battery:DoD:max" % propulsion_id] = C_bat / I_bat * V if I_bat > 0 else 0.0 119 | partials["data:performance:range:%s" % phase_name, 120 | "data:propulsion:%s:battery:capacity" % propulsion_id] = C_ratio / I_bat * V if I_bat > 0 else 0.0 121 | partials["data:performance:range:%s" % phase_name, 122 | "data:propulsion:%s:battery:current:%s" % (propulsion_id, phase_name)] = -V * C_ratio * C_bat / I_bat**2 if I_bat > 0 else 0.0 123 | 124 | 125 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/esc/definition_parameters.py: -------------------------------------------------------------------------------- 1 | """ 2 | Definition parameters for the Electronic Speed Controller (ESC). 3 | """ 4 | import openmdao.api as om 5 | import numpy as np 6 | from fastuav.utils.uncertainty import add_subsystem_with_deviation 7 | 8 | 9 | class ESCDefinitionParameters(om.Group): 10 | """ 11 | Group containing the calculation of the definition parameters for the ESC. 12 | The definition parameters are independent variables that allow to derive all the other component's parameters, 13 | by using datasheets or estimation models. 14 | The definition parameters for the ESC are the voltage and the apparent power. 15 | """ 16 | 17 | def setup(self): 18 | add_subsystem_with_deviation( 19 | self, 20 | "power", 21 | Power(), 22 | uncertain_outputs={"data:propulsion:esc:power:max:estimated": "W"}, 23 | ) 24 | 25 | add_subsystem_with_deviation( 26 | self, 27 | "voltage", 28 | Voltage(), 29 | uncertain_outputs={"data:propulsion:esc:voltage:estimated": "V"}, 30 | ) 31 | 32 | 33 | class Power(om.ExplicitComponent): 34 | """ 35 | Computes ESC maximum power. 36 | """ 37 | 38 | def setup(self): 39 | self.add_input("optimization:variables:propulsion:esc:power:k", val=1.0, units=None) 40 | self.add_input("data:propulsion:motor:power:takeoff", val=np.nan, units="W") 41 | self.add_input("data:propulsion:motor:voltage:takeoff", val=np.nan, units="V") 42 | self.add_input("data:propulsion:battery:voltage", val=np.nan, units="V") 43 | self.add_output("data:propulsion:esc:power:max:estimated", units="W") 44 | 45 | def setup_partials(self): 46 | self.declare_partials("*", "*", method="exact") 47 | 48 | def compute(self, inputs, outputs): 49 | k_pesc = inputs["optimization:variables:propulsion:esc:power:k"] 50 | P_mot_to = inputs["data:propulsion:motor:power:takeoff"] 51 | U_bat = inputs["data:propulsion:battery:voltage"] 52 | U_mot_to = inputs["data:propulsion:motor:voltage:takeoff"] 53 | 54 | P_esc = k_pesc * (P_mot_to / U_mot_to) * U_bat # [W] power electronic power max thrust 55 | 56 | outputs["data:propulsion:esc:power:max:estimated"] = P_esc 57 | 58 | def compute_partials(self, inputs, partials, discrete_inputs=None): 59 | k_pesc = inputs["optimization:variables:propulsion:esc:power:k"] 60 | P_mot_to = inputs["data:propulsion:motor:power:takeoff"] 61 | U_bat = inputs["data:propulsion:battery:voltage"] 62 | U_mot_to = inputs["data:propulsion:motor:voltage:takeoff"] 63 | 64 | partials["data:propulsion:esc:power:max:estimated", "optimization:variables:propulsion:esc:power:k"] = ( 65 | P_mot_to * U_bat / U_mot_to 66 | ) 67 | 68 | partials[ 69 | "data:propulsion:esc:power:max:estimated", "data:propulsion:motor:power:takeoff" 70 | ] = (k_pesc * U_bat / U_mot_to) 71 | 72 | partials["data:propulsion:esc:power:max:estimated", "data:propulsion:battery:voltage"] = ( 73 | k_pesc * P_mot_to / U_mot_to 74 | ) 75 | 76 | partials[ 77 | "data:propulsion:esc:power:max:estimated", "data:propulsion:motor:voltage:takeoff" 78 | ] = (-k_pesc * P_mot_to * U_bat / U_mot_to**2) 79 | 80 | 81 | class Voltage(om.ExplicitComponent): 82 | """ 83 | Computes ESC voltage 84 | """ 85 | 86 | def setup(self): 87 | self.add_input("optimization:variables:propulsion:esc:voltage:k", val=1.0, units=None) 88 | self.add_input("data:propulsion:battery:voltage", val=np.nan, units="V") 89 | self.add_output("data:propulsion:esc:voltage:estimated", units="V") 90 | 91 | def setup_partials(self): 92 | self.declare_partials("*", "*", method="exact") 93 | 94 | def compute(self, inputs, outputs): 95 | k_vesc = inputs["optimization:variables:propulsion:esc:voltage:k"] 96 | U_bat = inputs["data:propulsion:battery:voltage"] 97 | 98 | U_esc = k_vesc * U_bat # [V] ESC voltage rating 99 | 100 | outputs["data:propulsion:esc:voltage:estimated"] = U_esc 101 | 102 | def compute_partials(self, inputs, partials, discrete_inputs=None): 103 | k_vesc = inputs["optimization:variables:propulsion:esc:voltage:k"] 104 | U_bat = inputs["data:propulsion:battery:voltage"] 105 | 106 | partials[ 107 | "data:propulsion:esc:voltage:estimated", "optimization:variables:propulsion:esc:voltage:k" 108 | ] = U_bat 109 | 110 | partials[ 111 | "data:propulsion:esc:voltage:estimated", "data:propulsion:battery:voltage" 112 | ] = k_vesc 113 | 114 | 115 | class Voltage_2(om.ExplicitComponent): 116 | """ 117 | Computes ESC voltage 118 | """ 119 | 120 | def setup(self): 121 | self.add_input("models:propulsion:esc:power:reference", val=3180.0, units="W") 122 | self.add_input("models:propulsion:esc:voltage:reference", val=44.4, units="V") 123 | self.add_input("data:propulsion:esc:power:max:estimated", val=np.nan, units="W") 124 | self.add_output("data:propulsion:esc:voltage:estimated", units="V") 125 | 126 | def setup_partials(self): 127 | self.declare_partials("*", "*", method="exact") 128 | 129 | def compute(self, inputs, outputs): 130 | P_esc_ref = inputs["models:propulsion:esc:power:reference"] 131 | U_esc_ref = inputs["models:propulsion:esc:voltage:reference"] 132 | P_esc = inputs["data:propulsion:esc:power:max:estimated"] 133 | 134 | U_esc = U_esc_ref * (P_esc / P_esc_ref) ** (1 / 3) # [V] ESC voltage 135 | # U_esc = 1.84 * P_esc ** 0.36 # [V] ESC voltage 136 | 137 | outputs["data:propulsion:esc:voltage:estimated"] = U_esc 138 | 139 | def compute_partials(self, inputs, partials, discrete_inputs=None): 140 | P_esc_ref = inputs["models:propulsion:esc:power:reference"] 141 | U_esc_ref = inputs["models:propulsion:esc:voltage:reference"] 142 | P_esc = inputs["data:propulsion:esc:power:max:estimated"] 143 | 144 | partials[ 145 | "data:propulsion:esc:voltage:estimated", "data:propulsion:esc:power:max:estimated" 146 | ] = (1 / 3) * U_esc_ref / P_esc_ref ** (1 / 3) / P_esc ** (2 / 3) 147 | partials[ 148 | "models:propulsion:esc:voltage:estimated", "data:propulsion:esc:power:reference" 149 | ] = - (1 / 3) * U_esc_ref * P_esc ** (1 / 3) / P_esc_ref ** (4 / 3) 150 | partials[ 151 | "models:propulsion:esc:voltage:estimated", "data:propulsion:esc:voltage:reference" 152 | ] = (P_esc / P_esc_ref) ** (1 / 3) 153 | -------------------------------------------------------------------------------- /src/fastuav/models/propulsion/esc/constraints.py: -------------------------------------------------------------------------------- 1 | """ 2 | ESC constraints 3 | """ 4 | import openmdao.api as om 5 | import numpy as np 6 | 7 | 8 | class ESCConstraints(om.ExplicitComponent): 9 | """ 10 | Constraints definition of the ESC component 11 | """ 12 | 13 | def setup(self): 14 | self.add_input("data:propulsion:esc:power:max", val=np.nan, units="W") 15 | self.add_input("data:propulsion:esc:power:takeoff", val=np.nan, units="W") 16 | self.add_input("data:propulsion:esc:power:climb", val=np.nan, units="W") 17 | self.add_input("data:propulsion:esc:power:cruise", val=np.nan, units="W") 18 | self.add_input("data:propulsion:esc:voltage", val=np.nan, units="V") 19 | self.add_input("data:propulsion:battery:voltage", val=np.nan, units="V") 20 | self.add_input("models:propulsion:esc:voltage:tol", val=0.0, units="percent") 21 | self.add_output("optimization:constraints:propulsion:esc:power:takeoff", units=None) 22 | self.add_output("optimization:constraints:propulsion:esc:power:climb", units=None) 23 | self.add_output("optimization:constraints:propulsion:esc:power:cruise", units=None) 24 | self.add_output("optimization:constraints:propulsion:esc:voltage:battery", units=None) 25 | self.add_output("optimization:constraints:propulsion:esc:voltage:min", units=None) 26 | self.add_output("optimization:constraints:propulsion:esc:voltage:max", units=None) 27 | 28 | def setup_partials(self): 29 | self.declare_partials("*", "*", method="exact") 30 | 31 | def compute(self, inputs, outputs): 32 | P_esc = inputs["data:propulsion:esc:power:max"] 33 | P_esc_to = inputs["data:propulsion:esc:power:takeoff"] 34 | P_esc_cl = inputs["data:propulsion:esc:power:climb"] 35 | P_esc_cr = inputs["data:propulsion:esc:power:cruise"] 36 | U_esc = inputs["data:propulsion:esc:voltage"] 37 | U_bat = inputs["data:propulsion:battery:voltage"] 38 | k = 1 + inputs["models:propulsion:esc:voltage:tol"] / 100 # tolerance multiplier on prediction intervals 39 | 40 | # ESC power versus operating conditions 41 | ESC_con0 = (P_esc - P_esc_to) / P_esc 42 | ESC_con1 = (P_esc - P_esc_cl) / P_esc 43 | ESC_con2 = (P_esc - P_esc_cr) / P_esc 44 | 45 | # ESC voltage versus battery voltage 46 | ESC_con3 = (U_esc - U_bat) / U_esc 47 | 48 | # Voltage versus power : tolerance intervals 49 | U_hat = 1.84 * P_esc ** 0.36 # [V] ESC voltage-to-power regression 50 | eps_low = - 13.33 # 1st percentile on regression error (i.e., 99% of data are above this value) 51 | eps_up = 12.78 # 99th percentile on regression error (i.e., 99% of data are below this value) 52 | U_min = U_hat + k * eps_low # [V] minimum allowable voltage rating 53 | U_max = U_hat + k * eps_up # [V] maximum allowable voltage rating 54 | ESC_con4 = (U_esc - U_min) / U_esc 55 | ESC_con5 = (U_max - U_esc) / U_esc 56 | 57 | outputs["optimization:constraints:propulsion:esc:power:takeoff"] = ESC_con0 58 | outputs["optimization:constraints:propulsion:esc:power:climb"] = ESC_con1 59 | outputs["optimization:constraints:propulsion:esc:power:cruise"] = ESC_con2 60 | outputs["optimization:constraints:propulsion:esc:voltage:battery"] = ESC_con3 61 | outputs["optimization:constraints:propulsion:esc:voltage:min"] = ESC_con4 62 | outputs["optimization:constraints:propulsion:esc:voltage:max"] = ESC_con5 63 | 64 | def compute_partials(self, inputs, partials, discrete_inputs=None): 65 | P_esc = inputs["data:propulsion:esc:power:max"] 66 | P_esc_to = inputs["data:propulsion:esc:power:takeoff"] 67 | P_esc_cl = inputs["data:propulsion:esc:power:climb"] 68 | P_esc_cr = inputs["data:propulsion:esc:power:cruise"] 69 | U_esc = inputs["data:propulsion:esc:voltage"] 70 | U_bat = inputs["data:propulsion:battery:voltage"] 71 | k = 1 + inputs["models:propulsion:esc:voltage:tol"] / 100 # tolerance multiplier on prediction interval 72 | 73 | U_hat = 1.84 * P_esc ** 0.36 # [V] ESC voltage-to-power regression 74 | eps_low = - 13.33 # 1st percentile on regression error (i.e., 99% of data are above this value) 75 | eps_up = 12.78 # 99th percentile on regression error (i.e., 99% of data are below this value) 76 | U_min = U_hat + k * eps_low # [V] minimum allowable voltage rating 77 | U_max = U_hat + k * eps_up # [V] maximum allowable voltage rating 78 | 79 | # Takeoff power 80 | partials[ 81 | "optimization:constraints:propulsion:esc:power:takeoff", 82 | "data:propulsion:esc:power:max" 83 | ] = ( 84 | P_esc_to / P_esc ** 2 85 | ) 86 | partials[ 87 | "optimization:constraints:propulsion:esc:power:takeoff", 88 | "data:propulsion:esc:power:takeoff" 89 | ] = ( 90 | - 1.0 / P_esc 91 | ) 92 | 93 | # Climb power 94 | partials[ 95 | "optimization:constraints:propulsion:esc:power:climb", 96 | "data:propulsion:esc:power:max" 97 | ] = ( 98 | P_esc_cl / P_esc**2 99 | ) 100 | partials[ 101 | "optimization:constraints:propulsion:esc:power:climb", 102 | "data:propulsion:esc:power:climb" 103 | ] = ( 104 | -1.0 / P_esc 105 | ) 106 | 107 | # Cruise power 108 | partials[ 109 | "optimization:constraints:propulsion:esc:power:cruise", 110 | "data:propulsion:esc:power:max" 111 | ] = ( 112 | P_esc_cr / P_esc**2 113 | ) 114 | partials[ 115 | "optimization:constraints:propulsion:esc:power:cruise", 116 | "data:propulsion:esc:power:cruise" 117 | ] = ( 118 | - 1.0 / P_esc 119 | ) 120 | 121 | # Battery voltage 122 | partials["optimization:constraints:propulsion:esc:voltage:battery", 123 | "data:propulsion:battery:voltage"] = - 1.0 / U_esc 124 | partials["optimization:constraints:propulsion:esc:voltage:battery", 125 | "data:propulsion:esc:voltage"] = U_bat / U_esc**2 126 | 127 | # Tolerance intervals 128 | partials["optimization:constraints:propulsion:esc:voltage:min", 129 | "data:propulsion:esc:voltage"] = U_min / U_esc**2 130 | partials["optimization:constraints:propulsion:esc:voltage:min", 131 | "data:propulsion:esc:power:max"] = - 0.36 * P_esc ** (-1) * U_hat / U_esc 132 | partials["optimization:constraints:propulsion:esc:voltage:min", 133 | "models:propulsion:esc:voltage:tol"] = - eps_low / U_esc / 100 134 | 135 | partials["optimization:constraints:propulsion:esc:voltage:max", 136 | "data:propulsion:esc:voltage"] = - U_max / U_esc ** 2 137 | partials["optimization:constraints:propulsion:esc:voltage:max", 138 | "data:propulsion:esc:power:max"] = 0.36 * P_esc ** (-1) * U_hat / U_esc 139 | partials["optimization:constraints:propulsion:esc:voltage:max", 140 | "models:propulsion:esc:voltage:tol"] = eps_up / U_esc / 100 141 | 142 | --------------------------------------------------------------------------------