6.24. Sizing of a multirotor drone (ISAE)#
Written by Marc Budinger (INSA Toulouse), Scott Delbecq (ISAE-SUPAERO) and Félix Pollet (ISAE-SUPAERO), Toulouse, France.
The objective of this notebook is to select the best compromise of components (propeller, motor, ESC, battery) of a multi-rotor drone for given specifiations.
# Import libraries
import numpy as np
import openmdao.api as om
6.24.1. Introduction#
We have seen that even at the component level the set of equations of a sizing code can generate typical issues such :
Underconstrained set of equations: the lacking equations can come from additional scenarios, estimation models or additional sizing variable
Overconstrained equations often due to the selection of a component on multiple critera: the adding of over-sizing coefficients and constraints in the optimization problem can generally fix this issue
Algebraic loops often due to selection criteria requiring informations generally available after the selection
The presence of singularities is emphasized when assembling component sizing codes to build a sizing code for the whole system.
Concerning overconstraints components, we have here:
Brushless motors with multiple torque and voltage constraints (hover and transient vertical displacement)
Multiple algebraic loops appears in the sizing problem:
The thrust depends of the total mass which depend of components required for generating this thrust
More details in the setting up of sizing code can be found in the following paper:
Reysset, A., Budinger, M., & Maré, J. C. (2015). Computer-aided definition of sizing procedures and optimization problems of mechatronic systems. Concurrent Engineering, 23(4), 320-332.
6.24.2. Design variables#
Name |
Unit |
Description |
---|---|---|
\(\beta_{pro}\) |
[-] |
\(\beta_{pro}=pitch/diameter\) ratio to define the propeller |
6.24.3. Constraints#
Name |
Unit |
Description |
---|---|---|
cons_1 |
[-] |
\(cons_1 = M_{total}-M_{total,real}\) |
6.24.4. Objectives#
6.24.5. Workflow#
6.24.6. Sizing code#
Lets now implement the code to size the multirotor. For this purpose we use the MDAO framework OpenMDAO.
To stay simple first, we will embed the sizing in a simple OpenMDAO ExplicitComponent
which is not the most efficient way of solving the problem.
The best practice would be to decompose the problem into several ExplicitComponent
to make the code more modular and the problem solving more efficient.
For this purpose you can refer to the following paper:
Delbecq, S., Budinger, M., & Reysset, A. (2020). Benchmarking of monolithic MDO formulations and derivative computation techniques using OpenMDAO. Structural and Multidisciplinary Optimization, 62(2), 645-666.
6.24.6.1. Specifications#
The first step is to provide the specifications (top-level requirements) for the drone.
Main specifications :
a load (video, control card) of mass \(M_{load}\).
an autonomy \(t_{hf}\) for the hover flight.
an acceleration to take off \(a_{to}\).
# SPECIFICATIONS
# Load
M_pay = 50.0 # [kg] load mass
# Acceleration during take off
a_to = 0.25 * 9.81 # [m/s**2] acceleration
# Autonomy
t_hov_spec = 25.0 # [min] time of hover flight
# MTOW
MTOW = 360.0 # [kg] maximal mass allowed
6.24.6.2. Architecture definition and design assumptions#
Then, we must provide the architecture definition and design assumptions for the models.
# ARCHITECTURE of the multi-rotor drone (4,6, 8 arms, ...)
N_arm = 4 # [-] number of arms
N_pro_arm = 1 # [-] number of propeller per arm (1 or 2)
# BATTERY AND ESC : reference parameters for scaling laws
# Ref : MK-quadro
M_bat_ref = 0.329 # [kg] mass
E_bat_ref = 220.0 * 3600.0 * 0.329 # [J]
C_bat_ref = 5 # [Ah] Capacity
I_bat_max_ref = 50 * C_bat_ref # [A] max discharge current
# Ref : Turnigy K_Force 70HV
P_esc_ref = 3108.0 # [W] Power
M_esc_ref = 0.115 # [kg] Mass
# MOTOR : reference parameters for scaling laws
# Ref : AXI 5325/16 GOLD LINE
T_nom_mot_ref = 2.32 # [N*m] rated torque
T_max_mot_ref = 85.0 / 70.0 * T_nom_mot_ref # [N*m] max torque
R_mot_ref = 0.03 # [ohm] resistance
M_mot_ref = 0.575 # [kg] mass
K_T_ref = 0.03 # [N*m/A] torque coefficient
T_mot_fr_ref = 0.03 # [N*m] friction torque (zero load, nominal speed)
# FRAME
sigma_max = (
280e6 / 4.0
) # [Pa] Composite max stress (2 reduction for dynamic, 2 reduction for stress concentration)
rho_s = 1700.0 # [kg/m**3] Volumic mass of aluminum
# PROPELLER
# Specifications
rho_air = 1.18 # [kg/m**3] Air density
ND_max = 105000.0 / 60.0 * 0.0254 # [Hz.m] Max speed limit (N.D max) for APC MR propellers
# Reference parameters for scaling laws
D_pro_ref = 11.0 * 0.0254 # [m] Reference propeller diameter
M_pro_ref = 0.53 * 0.0283 # [kg] Reference propeller mass
6.24.6.3. Optimization variables#
The next step is to give an initial value for the optimisation variables:
# Optimisation variables : initial values
beta_pro = 0.33 # pitch/diameter ratio of the propeller
k_os = 3.2 # over sizing coefficient on the load mass
k_ND = 1.2 # slow down propeller coef : ND = NDmax / k_ND
k_mot = 1.0 # over sizing coefficient on the motor torque
k_speed_mot = 1.2 # adaption winding coef on the motor speed
k_mb = 1.0 # ratio battery/load mass
k_vb = 1.0 # oversizing coefficient for voltage evaluation
k_D = 0.99 # aspect ratio D_in/D_out for the beam of the frame
6.24.6.4. Sizing code#
Now, complete the following sizing code with the missing design variables (inputs), constraints/objective (outputs) and equations:
class SizingCode(om.ExplicitComponent):
"""
Sizing code of the multirotor UAV.
"""
def setup(self):
self.add_input("beta_pro", val=0.0)
self.add_output("t_hov", val=0.0)
self.add_output("M_total_real", val=0.0)
self.add_output("cons_1", val=0.0)
def setup_partials(self):
# Finite difference all partials.
self.declare_partials("*", "*", method="cs")
def compute(self, inputs, outputs):
beta_pro = inputs["beta_pro"]
#% OBJECTIVES
# ---
t_hov = C_bat / I_bat_hov / 60.0 # [min] Hover time
M_total_real = (M_esc + M_pro + M_mot) * N_pro + M_pay + M_bat + M_frame # [kg] Total mass
#% CONSTRAINTS
cons_1 = M_total - M_total_real
outputs["t_hov"] = t_hov
outputs["M_total_real"] = M_total_real
outputs["cons_1"] = cons_1
Now that the ExplicitComponent
is defined we have to add it to a Group
, itself added to a Problem
.
prob = om.Problem()
group = om.Group()
group.add_subsystem("sizing_code", SizingCode(), promotes=["*"])
prob.model = group
prob.driver = om.ScipyOptimizeDriver()
prob.driver.options["optimizer"] = "SLSQP"
prob.driver.options["maxiter"] = 100
prob.driver.options["tol"] = 1e-8
prob.model.add_design_var("beta_pro", lower=0.3, upper=0.6)
prob.model.add_constraint("cons_1", lower=0)
# prob.model.add_objective('t_hov', scaler=-1)
prob.model.add_objective("M_total_real", scaler=0.1)
# Ask OpenMDAO to finite-difference across the model to compute the gradients for the optimizer
prob.model.approx_totals()
prob.setup()
# Setup initial values
prob.set_val("beta_pro", beta_pro)
prob.run_driver()
print("Design variables")
print("beta_pro :", prob.get_val("beta_pro"))
print("Constraints")
print("cons_1 :", prob.get_val("cons_1"))
print("Objective")
print("t_hov: ", prob.get_val("t_hov"))
print("M_total_real: ", prob.get_val("M_total_real"))
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/core/system.py:2738, in System._call_user_function(self, fname, protect_inputs, protect_outputs, protect_residuals)
2737 try:
-> 2738 yield
2739 except Exception:
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/core/explicitcomponent.py:271, in ExplicitComponent._compute_wrapper(self)
270 else:
--> 271 self.compute(self._inputs, self._outputs)
Cell In[5], line 22, in SizingCode.compute(self, inputs, outputs)
20 #% OBJECTIVES
21 # ---
---> 22 t_hov = C_bat / I_bat_hov / 60.0 # [min] Hover time
23 M_total_real = (M_esc + M_pro + M_mot) * N_pro + M_pay + M_bat + M_frame # [kg] Total mass
NameError: name 'C_bat' is not defined
During handling of the above exception, another exception occurred:
NameError Traceback (most recent call last)
Cell In[6], line 29
25 # Setup initial values
26 prob.set_val("beta_pro", beta_pro)
---> 29 prob.run_driver()
31 print("Design variables")
32 print("beta_pro :", prob.get_val("beta_pro"))
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/utils/hooks.py:370, in _HookDecorator.__call__(self, *args, **kwargs)
354 """
355 Run the function with any pre and post hooks.
356
(...)
367 The return value of the function.
368 """
369 self._run_hooks(self.pre_hooks, args, kwargs)
--> 370 ret = self.func(*args, **kwargs)
371 self._run_hooks(self.post_hooks, args, kwargs, ret)
372 return ret
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/core/problem.py:749, in Problem.run_driver(self, case_prefix, reset_iter_counts)
745 record_model_options(self, self._run_counter)
747 model._clear_iprint()
--> 749 return driver._run()
750 finally:
751 self._recording_iter.prefix = old_prefix
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/core/driver.py:821, in Driver._run(self)
819 else:
820 with SaveOptResult(self):
--> 821 self.result.success = not self.run()
823 return self.result
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/drivers/scipy_optimizer.py:268, in ScipyOptimizeDriver.run(self)
266 with RecordingDebugging(self._get_name(), self.iter_count, self):
267 with model._relevance.nonlinear_active('iter'):
--> 268 self._run_solve_nonlinear()
269 self.iter_count += 1
271 self._con_cache = self.get_constraint_values()
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/core/driver.py:177, in DriverResult.track_stats.<locals>._track_time.<locals>.wrapper(*args, **kwargs)
174 @functools.wraps(func)
175 def wrapper(*args, **kwargs):
176 start_time = time.perf_counter()
--> 177 ret = func(*args, **kwargs)
178 end_time = time.perf_counter()
179 result = args[0].result
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/core/driver.py:1267, in Driver._run_solve_nonlinear(self)
1265 @DriverResult.track_stats(kind='model')
1266 def _run_solve_nonlinear(self):
-> 1267 return self._problem().model.run_solve_nonlinear()
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/core/system.py:4749, in System.run_solve_nonlinear(self)
4743 """
4744 Compute outputs.
4745
4746 This calls _solve_nonlinear, but with the model assumed to be in an unscaled state.
4747 """
4748 with self._scaled_context_all():
-> 4749 self._solve_nonlinear()
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/core/group.py:3529, in Group._solve_nonlinear(self)
3527 with Recording(name + '._solve_nonlinear', self.iter_count, self):
3528 with self._relevance.active(self._nonlinear_solver.use_relevance()):
-> 3529 self._nonlinear_solver._solve_with_cache_check()
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/solvers/nonlinear/nonlinear_runonce.py:26, in NonlinearRunOnce._solve_with_cache_check(self)
25 def _solve_with_cache_check(self):
---> 26 self.solve()
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/solvers/nonlinear/nonlinear_runonce.py:45, in NonlinearRunOnce.solve(self)
41 subsys._solve_nonlinear()
43 # If this is not a parallel group, transfer for each subsystem just prior to running it.
44 else:
---> 45 self._gs_iter()
47 rec.abs = 0.0
48 rec.rel = 0.0
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/solvers/solver.py:897, in NonlinearSolver._gs_iter(self)
895 if subsys._is_local:
896 try:
--> 897 subsys._solve_nonlinear()
898 except AnalysisError as err:
899 if 'reraise_child_analysiserror' not in self.options or \
900 self.options['reraise_child_analysiserror']:
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/core/explicitcomponent.py:297, in ExplicitComponent._solve_nonlinear(self)
295 with self._unscaled_context(outputs=[self._outputs], residuals=[self._residuals]):
296 self._residuals.set_val(0.0)
--> 297 self._compute_wrapper()
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/core/explicitcomponent.py:271, in ExplicitComponent._compute_wrapper(self)
268 self.compute(self._inputs, self._outputs,
269 self._discrete_inputs, self._discrete_outputs)
270 else:
--> 271 self.compute(self._inputs, self._outputs)
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/contextlib.py:137, in _GeneratorContextManager.__exit__(self, typ, value, traceback)
135 value = typ()
136 try:
--> 137 self.gen.throw(typ, value, traceback)
138 except StopIteration as exc:
139 # Suppress StopIteration *unless* it's the same exception that
140 # was passed to throw(). This prevents a StopIteration
141 # raised inside the "with" statement from being suppressed.
142 return exc is not value
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/core/system.py:2744, in System._call_user_function(self, fname, protect_inputs, protect_outputs, protect_residuals)
2742 raise
2743 else:
-> 2744 raise err_type(
2745 f"{self.msginfo}: Error calling {fname}(), {err}").with_traceback(trace)
2746 finally:
2747 self._inputs.read_only = False
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/core/system.py:2738, in System._call_user_function(self, fname, protect_inputs, protect_outputs, protect_residuals)
2735 self._residuals.read_only = protect_residuals
2737 try:
-> 2738 yield
2739 except Exception:
2740 err_type, err, trace = sys.exc_info()
File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/openmdao/core/explicitcomponent.py:271, in ExplicitComponent._compute_wrapper(self)
268 self.compute(self._inputs, self._outputs,
269 self._discrete_inputs, self._discrete_outputs)
270 else:
--> 271 self.compute(self._inputs, self._outputs)
Cell In[5], line 22, in SizingCode.compute(self, inputs, outputs)
18 beta_pro = inputs["beta_pro"]
20 #% OBJECTIVES
21 # ---
---> 22 t_hov = C_bat / I_bat_hov / 60.0 # [min] Hover time
23 M_total_real = (M_esc + M_pro + M_mot) * N_pro + M_pay + M_bat + M_frame # [kg] Total mass
25 #% CONSTRAINTS
NameError: 'sizing_code' <class SizingCode>: Error calling compute(), name 'C_bat' is not defined