├── 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 |
--------------------------------------------------------------------------------