Sizing procedure and optimization#

Written by Marc Budinger (INSA Toulouse) and Scott Delbecq (ISAE-SUPAERO), Toulouse, France

The objective of this notebook is to learn how to implement a sizing code and use a simple numerical optimization to find the optimal design of the system. The system studied is the TVC EMA of the VEGA launcher.

import numpy as np
import scipy.optimize
from scipy import log10
from math import pi
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In[1], line 3
      1 import numpy as np
      2 import scipy.optimize
----> 3 from scipy import log10
      4 from math import pi

ImportError: cannot import name 'log10' from 'scipy' (/opt/hostedtoolcache/Python/3.9.20/x64/lib/python3.9/site-packages/scipy/__init__.py)

Objectives and specifications#

The objective is to select the reduction ratio of a gear reducer in order to minimize the mass of the motor.

The application have to ensure at nozzle level :

  • a max torque \(T_{load}\) of \(48 kNm\) and a max acceleration of \(\dot{\omega}_{max}=811 °/s²\)

  • a max speed \(\omega_{max}\) of 9.24 °/s

  • a max magnitude \(\alpha_{max}\) of 5.7 °

We will give here an example based on a linear actuator with a preselected roller screw (pitch of 10 mm/rev). We assume here, for simplification, the efficiency equal to 70%.

EMA components:

EMA

We first define the specifications and assumptions for the sizing:

# Specifications
angular_magnitude_max = 5.7 * pi / 180  # [rad]
max_dyn_torque = 48e3  # [N.m]
max_speed_rot = 9.24 * pi / 180  # [rad/s]
max_acc_rot = 811 * pi / 180  # [rad/s²]

# Assumptions
pitch = 10e-3 / 2 / pi  # [m/rad]
nu_screw = 0.7  # [-]

# Security coefficient for mechanical components
k_sec = 2
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[2], line 2
      1 # Specifications
----> 2 angular_magnitude_max = 5.7 * pi / 180  # [rad]
      3 max_dyn_torque = 48e3  # [N.m]
      4 max_speed_rot = 9.24 * pi / 180  # [rad/s]

NameError: name 'pi' is not defined

We then define the main characteristics for the components for the scaling laws:

# Motor
T_mot_guess_max_ref = 13.4  # [N.m]
W_mot_max_ref = 754  # [rad/s]
J_mot_ref = 2.9e-4 / 2  # [kg.m²]
M_mot_ref = 3.8  # [kg]

# Rod end
F_rod_max_ref = 183e3  # [N]
M_rod_ref = 1.55  # [kg]
L_rod_ref = 0.061  # [m]

# Screw
M_nut_ref = 2.1  # [kg]
Ml_screw_ref = 9.4  # [kg/m]
D_nut_ref = 0.08  # [m]
L_nut_ref = 0.12 * 0.08 / 0.09  # [m]
F_screw_max_ref = 135e3  # [N]

# Bearing
M_bearing_ref = 5.05  # [kg]
L_bearing_ref = 0.072  # [m]
F_bearing_max_ref = 475e3  # [N]

Sizing code#

The sizing code is defined here in a function which can give an evaluation of the objective and of the constraints function of design variables.

The design variables of this sizing code are :

  • the reduction ratio of the reducer

  • an oversizing coefficient for the selection of the motor used to tacke an algebraic loop

  • the positions (\(d_1\) and \(d_2\)) of the actuator anchorages

New design variables Mechanical Loads

The objective is the global mass of the actuator.

The constraints which should be positives are here:

  • the speed margin, ie. the motor doesn’t exceed its maximum speed

  • the torque margin, ie. the motor doesn’t exceed its maximum torque

  • the length margin, ie. the global length of the actuator doesn’t exceed the distance between anchorage points

def sizing_code(param, arg="print"):
    #  Design variables
    reduction_ratio = param[0]  # Reduction ratio of the reducer
    k_oversizing = param[1]  # Oversizing of the motor (algebraic loop)
    d1 = param[2] / 100  # position of the anchorage (cm --> meter)
    d2 = param[3] / 100  # position of the anchorage (cm --> meter)

    # --------------------------------------
    # Force, torque and speed calculations

    # Lever arm
    lever_arm = (
        (
            -(-0.9744 * d1 - 1.372)
            * (0.2248 * d1 - 0.3757)
            * ((0.2248 * d1 - 0.3757) ** 2 + (-0.9744 * d1 + d2 - 1.172) ** 2) ** (-0.5)
            + (0.2248 * d1 + 0.9823)
            * ((0.2248 * d1 - 0.3757) ** 2 + (-0.9744 * d1 + d2 - 1.172) ** 2) ** (-0.5)
            * (-0.9744 * d1 + d2 - 1.172)
        )
        ** 2
    ) ** 0.5

    # Length of actuator
    actuator_length = ((0.2248 * d1 - 0.3757) ** 2 + (-0.9744 * d1 + d2 - 1.172) ** 2) ** 0.5

    # Stroke of actuator
    stroke = angular_magnitude_max * 2 * lever_arm

    # Load, speed
    max_speed = max_speed_rot * lever_arm  # [m/s]
    max_load = max_dyn_torque / lever_arm  # [N]

    # Torque motor estimation
    T_mot_guess = k_oversizing * max_load * pitch / reduction_ratio / nu_screw

    # Max static force (stall force) with 100% efficiency assumption
    max_stall_force = T_mot_guess / pitch * reduction_ratio
    max_mech_force = k_sec * max_stall_force

    # --------------------------------------
    # Parameter estimation with scaling laws

    # Motor
    M_mot = M_mot_ref * (T_mot_guess / T_mot_guess_max_ref) ** (3 / 3.5)
    J_mot = J_mot_ref * (T_mot_guess / T_mot_guess_max_ref) ** (5 / 3.5)
    W_mot = W_mot_max_ref * (T_mot_guess / T_mot_guess_max_ref) ** (-1 / 3.5)

    # Rod end
    M_rod = M_rod_ref * (max_mech_force / F_rod_max_ref) ** (3 / 2)
    L_rod = L_rod_ref * (max_mech_force / F_rod_max_ref) ** (1 / 2)

    # Nut
    M_nut = M_nut_ref * (max_mech_force / F_screw_max_ref) ** (3 / 2)
    M_screw = Ml_screw_ref * (max_mech_force / F_screw_max_ref) ** (2 / 2) * actuator_length / 2
    D_nut = D_nut_ref * (max_mech_force / F_screw_max_ref) ** (1 / 2)
    L_nut = L_nut_ref * (max_mech_force / F_screw_max_ref) ** (1 / 2)

    # Bearing
    M_bearing = M_bearing_ref * (max_mech_force / F_bearing_max_ref) ** (3 / 2)
    L_bearing = L_bearing_ref * (max_mech_force / F_bearing_max_ref) ** (1 / 2)

    # --------------------------------------
    # Exact torque calculation with motor inertia
    T_mot_real = (
        max_load * pitch / reduction_ratio / nu_screw
        + J_mot * max_acc_rot * lever_arm * reduction_ratio / pitch
    )

    # --------------------------------------
    # Objectives and constrants calculations

    # Objective = total mass
    objective = M_mot + M_bearing + 2 * M_rod + M_screw + M_nut

    # Constraints (should be >=0)
    C1 = W_mot - reduction_ratio * max_speed / pitch  # speed margin
    C2 = T_mot_guess - T_mot_real  # Torque margin
    C3 = actuator_length - stroke - L_nut - L_bearing - 2 * L_rod  # Length margin

    # --------------------------------------
    # Objective and constraints
    if arg == "objective":
        return objective / 100
    if arg == "objectiveP":
        if (C1 < 0.0) | (C2 < 0.0) | (C3 < 0.0):
            # If constraints are not respected we penalize
            # the objective for contraint less algorithms
            return objective * 1e5
        else:
            return objective / 100
    elif arg == "print":
        print("Objective:")
        print("     Total mass = %.2f kg" % (M_mot + M_bearing + 2 * M_rod + M_screw + M_nut))
        print("Design variables:")
        print("     reduction_ratio =  %.2f" % reduction_ratio)
        print("     k_oversizing =  %.2f" % k_oversizing)
        print("     d_1 =  %.2f m" % d1)
        print("     d_2 =  %.2f m" % d2)
        print("Performances:")
        print("     Stroke = %.2f m" % stroke)
        print("     Max load = %.0f N" % max_load)
        print("     Stall load = %.0f N" % max_stall_force)
        print("Components characteristics:")
        print("     Lever arm = %.2f m" % lever_arm)
        print("     Actuator length = %.2f m" % actuator_length)
        print("     Motor mass = %.2f kg" % M_mot)
        print("     Max Tem = %.2f N.m" % T_mot_real)
        print("     Rod-end mass = %.2f kg" % (2 * M_rod))
        print("     Rod-end length = %.2f m" % L_rod)
        print("     Screw mass = %.2f kg" % M_screw)
        print("     Nut mass = %.2f kg" % (2 * M_nut))
        print("     Nut length = %.2f m" % L_nut)
        print("     Bearing length = %.2f m" % L_bearing)
        print("Constraints (should be >= 0):")
        print("     Speed margin: W_mot-reduction_ratio*max_speed/pitch= %.3f" % C1)
        print("     Torque margin: T_mot_guess-T_mot_real= %.3f " % C2)
        print("     Length margin: actuator_length-stroke-L_nut-L_bearing-2*L_rod =  %.3f" % C3)
    elif arg == "constraints":
        return [C1, C2, C3]
    else:
        raise TypeError(
            "Unknown argument for sizing_code: use 'print', 'objective', 'objectiveP' or 'contraints'"
        )

Optimization with SLSQP algorithm#

We will now use the opmization algorithms of the Scipy package to solve and optimize the configuration. We will first use the SLSQP algorithm without explicit expression of the gradient (Jacobian).

The first step is to give an initial value of optimisation variables:

# Optimization variables
# Reduction ratio
reduction_ratio_init = 1  # [-]
reduction_ratio_min = 0.1  # [-]
reduction_ratio_max = 10  # [-]

# Oversizing coefficient for multidisciplinary coupling
k_oversizing_init = 1  # [-]
k_oversizing_min = 0.2  # [-]
k_oversizing_max = 5  # [-]

# Anchorage positions
d1_init = 0  # [cm]
d1_min = -80  # [cm]
d1_max = 80  # [cm]

d2_init = 0  # [cm]
d2_min = -20  # [cm]
d2_max = 20  # [cm]

# Initial values vector for design variables
parameters = np.array((reduction_ratio_init, k_oversizing_init, d1_init, d2_init))

We can print of the characterisitcs of the problem before optimization with the intitial vector of optimization variables:

# Initial characteristics before optimization
print("-----------------------------------------------")
print("Initial characteristics before optimization :")

sizing_code(parameters, "print")
print("-----------------------------------------------")
-----------------------------------------------
Initial characteristics before optimization :
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[6], line 5
      2 print("-----------------------------------------------")
      3 print("Initial characteristics before optimization :")
----> 5 sizing_code(parameters, "print")
      6 print("-----------------------------------------------")

Cell In[4], line 28, in sizing_code(param, arg)
     25 actuator_length = ((0.2248 * d1 - 0.3757) ** 2 + (-0.9744 * d1 + d2 - 1.172) ** 2) ** 0.5
     27 # Stroke of actuator
---> 28 stroke = angular_magnitude_max * 2 * lever_arm
     30 # Load, speed
     31 max_speed = max_speed_rot * lever_arm  # [m/s]

NameError: name 'angular_magnitude_max' is not defined

We can see that the consistency constraint that is used to solve the coupling of the motor torque/inertia is not respected.

Then we can solve the problem and print of the optimized solution:

import time

# Resolution of the problem

contrainte = lambda x: sizing_code(x, "constraints")
objectif = lambda x: sizing_code(x, "objective")

start = time.time()
result = scipy.optimize.fmin_slsqp(
    func=objectif,
    x0=parameters,
    bounds=[
        (reduction_ratio_min, reduction_ratio_max),
        (k_oversizing_min, k_oversizing_max),
        (d1_min, d1_max),
        (d2_min, d2_max),
    ],
    f_ieqcons=contrainte,
    iter=100,
    acc=1e-8,
)
end = time.time()

# Final characteristics after optimization
print("-----------------------------------------------")
print("Final characteristics after optimization :")

sizing_code(result, "print")
print("-----------------------------------------------")
print("Calculation time:\n", end - start, "s")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 9
      6 objectif = lambda x: sizing_code(x, "objective")
      8 start = time.time()
----> 9 result = scipy.optimize.fmin_slsqp(
     10     func=objectif,
     11     x0=parameters,
     12     bounds=[
     13         (reduction_ratio_min, reduction_ratio_max),
     14         (k_oversizing_min, k_oversizing_max),
     15         (d1_min, d1_max),
     16         (d2_min, d2_max),
     17     ],
     18     f_ieqcons=contrainte,
     19     iter=100,
     20     acc=1e-8,
     21 )
     22 end = time.time()
     24 # Final characteristics after optimization

File /opt/hostedtoolcache/Python/3.9.20/x64/lib/python3.9/site-packages/scipy/optimize/_slsqp_py.py:210, in fmin_slsqp(func, x0, eqcons, f_eqcons, ieqcons, f_ieqcons, bounds, fprime, fprime_eqcons, fprime_ieqcons, args, iter, acc, iprint, disp, full_output, epsilon, callback)
    206 if f_ieqcons:
    207     cons += ({'type': 'ineq', 'fun': f_ieqcons, 'jac': fprime_ieqcons,
    208               'args': args}, )
--> 210 res = _minimize_slsqp(func, x0, args, jac=fprime, bounds=bounds,
    211                       constraints=cons, **opts)
    212 if full_output:
    213     return res['x'], res['fun'], res['nit'], res['status'], res['message']

File /opt/hostedtoolcache/Python/3.9.20/x64/lib/python3.9/site-packages/scipy/optimize/_slsqp_py.py:338, in _minimize_slsqp(func, x0, args, jac, bounds, constraints, maxiter, ftol, iprint, disp, eps, callback, finite_diff_rel_step, **unknown_options)
    334 # Set the parameters that SLSQP will need
    335 # meq, mieq: number of equality and inequality constraints
    336 meq = sum(map(len, [atleast_1d(c['fun'](x, *c['args']))
    337           for c in cons['eq']]))
--> 338 mieq = sum(map(len, [atleast_1d(c['fun'](x, *c['args']))
    339            for c in cons['ineq']]))
    340 # m = The total number of constraints
    341 m = meq + mieq

File /opt/hostedtoolcache/Python/3.9.20/x64/lib/python3.9/site-packages/scipy/optimize/_slsqp_py.py:338, in <listcomp>(.0)
    334 # Set the parameters that SLSQP will need
    335 # meq, mieq: number of equality and inequality constraints
    336 meq = sum(map(len, [atleast_1d(c['fun'](x, *c['args']))
    337           for c in cons['eq']]))
--> 338 mieq = sum(map(len, [atleast_1d(c['fun'](x, *c['args']))
    339            for c in cons['ineq']]))
    340 # m = The total number of constraints
    341 m = meq + mieq

Cell In[7], line 5, in <lambda>(x)
      1 import time
      3 # Resolution of the problem
----> 5 contrainte = lambda x: sizing_code(x, "constraints")
      6 objectif = lambda x: sizing_code(x, "objective")
      8 start = time.time()

Cell In[4], line 28, in sizing_code(param, arg)
     25 actuator_length = ((0.2248 * d1 - 0.3757) ** 2 + (-0.9744 * d1 + d2 - 1.172) ** 2) ** 0.5
     27 # Stroke of actuator
---> 28 stroke = angular_magnitude_max * 2 * lever_arm
     30 # Load, speed
     31 max_speed = max_speed_rot * lever_arm  # [m/s]

NameError: name 'angular_magnitude_max' is not defined

Optimization with differential evolution algorithm#

We will now use a differential evolution algorithm. Differential Evolution is stochastic in nature (does not use gradient methods) to find the minimium, and can search large areas of candidate space, but often requires larger numbers of function evaluations than conventional gradient based techniques. Differential evolution algorithms don’t manage directly constraints functions. A penalty method replaces the previous objective function.

# Resolution of the problem

objective = lambda x: sizing_code(x, "objectiveP")

start = time.time()
result = scipy.optimize.differential_evolution(
    func=objective,
    bounds=[
        (reduction_ratio_min, reduction_ratio_max),
        (k_oversizing_min, k_oversizing_max),
        (d1_min, d1_max),
        (d2_min, d2_max),
    ],
    tol=1e-5,
)
end = time.time()

# Final characteristics after optimization
print("-----------------------------------------------")
print("Final characteristics after optimization :")

sizing_code(result.x, "print")
print("-----------------------------------------------")
print("Calculation time:\n", end - start, "s")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[8], line 6
      3 objective = lambda x: sizing_code(x, "objectiveP")
      5 start = time.time()
----> 6 result = scipy.optimize.differential_evolution(
      7     func=objective,
      8     bounds=[
      9         (reduction_ratio_min, reduction_ratio_max),
     10         (k_oversizing_min, k_oversizing_max),
     11         (d1_min, d1_max),
     12         (d2_min, d2_max),
     13     ],
     14     tol=1e-5,
     15 )
     16 end = time.time()
     18 # Final characteristics after optimization

File /opt/hostedtoolcache/Python/3.9.20/x64/lib/python3.9/site-packages/scipy/optimize/_differentialevolution.py:502, in differential_evolution(func, bounds, args, strategy, maxiter, popsize, tol, mutation, recombination, seed, callback, disp, polish, init, atol, updating, workers, constraints, x0, integrality, vectorized)
    485 # using a context manager means that any created Pool objects are
    486 # cleared up.
    487 with DifferentialEvolutionSolver(func, bounds, args=args,
    488                                  strategy=strategy,
    489                                  maxiter=maxiter,
   (...)
    500                                  integrality=integrality,
    501                                  vectorized=vectorized) as solver:
--> 502     ret = solver.solve()
    504 return ret

File /opt/hostedtoolcache/Python/3.9.20/x64/lib/python3.9/site-packages/scipy/optimize/_differentialevolution.py:1155, in DifferentialEvolutionSolver.solve(self)
   1150     self.feasible, self.constraint_violation = (
   1151         self._calculate_population_feasibilities(self.population))
   1153     # only work out population energies for feasible solutions
   1154     self.population_energies[self.feasible] = (
-> 1155         self._calculate_population_energies(
   1156             self.population[self.feasible]))
   1158     self._promote_lowest_energy()
   1160 # do the optimization.

File /opt/hostedtoolcache/Python/3.9.20/x64/lib/python3.9/site-packages/scipy/optimize/_differentialevolution.py:1315, in DifferentialEvolutionSolver._calculate_population_energies(self, population)
   1313 parameters_pop = self._scale_parameters(population)
   1314 try:
-> 1315     calc_energies = list(
   1316         self._mapwrapper(self.func, parameters_pop[0:S])
   1317     )
   1318     calc_energies = np.squeeze(calc_energies)
   1319 except (TypeError, ValueError) as e:
   1320     # wrong number of arguments for _mapwrapper
   1321     # or wrong length returned from the mapper

File /opt/hostedtoolcache/Python/3.9.20/x64/lib/python3.9/site-packages/scipy/_lib/_util.py:441, in _FunctionWrapper.__call__(self, x)
    440 def __call__(self, x):
--> 441     return self.f(x, *self.args)

Cell In[8], line 3, in <lambda>(x)
      1 # Resolution of the problem
----> 3 objective = lambda x: sizing_code(x, "objectiveP")
      5 start = time.time()
      6 result = scipy.optimize.differential_evolution(
      7     func=objective,
      8     bounds=[
   (...)
     14     tol=1e-5,
     15 )

Cell In[4], line 28, in sizing_code(param, arg)
     25 actuator_length = ((0.2248 * d1 - 0.3757) ** 2 + (-0.9744 * d1 + d2 - 1.172) ** 2) ** 0.5
     27 # Stroke of actuator
---> 28 stroke = angular_magnitude_max * 2 * lever_arm
     30 # Load, speed
     31 max_speed = max_speed_rot * lever_arm  # [m/s]

NameError: name 'angular_magnitude_max' is not defined