Structural Optimization of a wingbox#

In the previous notebook we have seen the structural analysis of a single wingbox cell. We would like however to use these tools to optimize the various parameters of the wingbox in order to minimize the weight of the structure. In order to do so let’s remind ourselves what exactly we can change in order to improve the performance of this wingbox.

In the previous notebook we had defined the following parameters in order to perform the analysis.

  • \(n_{cell}\) - Number of cells

  • \(\text{loc}_{sp}\) - The location of the spars

  • \(t_{sk}\) - The thickness of the skin in each cell

  • \(A_{str}\) - The area of the stringers

  • \(t_{sp}\) - The thickness of the spars

  • \(n_{str}\) - The amount of stringers per cell

These variables were then loaded in the API data structure as shown below:

attr_dict = {
    "n_cell":4,
    "spar_loc_nondim":[0.3, 0.5, 0.7],
    "t_sk_cell":[0.002,0.004,0.004, 0.004],
    "area_str":20e-6,
    "t_sp":0.008,
    "str_cell":[8,8,7,8]
}

wingbox_struct = tud.Wingbox(**attr_dict)

However, there will be one major change. For the structural analysis of a wingbox only the stringer area is relevant however for the constraints the geometry of said stringers is also important as you will see further up in this notebook.

So in the optimization of the wingbox we wil add the stringer height, width and thickness to the optimization parameters.

The parameters which we will optimize for are the skin thickness in each cell, the area of the stringers, the thickness of the spars and finally the stringer of the cell. This leaves the amount of cells, the spar locations and the bay length to be decided by the designers. The reasoning behind this is that usually your rib and spar locations are constrained the placement of other systems in the wing such as the flap and slat mechanism. Below a summary can be found of the fixed and optimization parameters:

Optimiziation Parameters

Fixed Parameters

Skin thickness in each cell

The amount of cells

Stringer width

Spar locations

Stringer height

Bay length

Stringer thickness

Spar thickness

The amount of stringers in each cell

Laying out a framework for a full wing optimization#

In order define our constraints later on we will have to divide the wing in sections called bays. Each bay is enclosed by two ribs, thus the length of these bays is for the designer to decide. Once, a bay is defined, we can further split this up in a collection of flat sheets. Where each sheet is in turn enclosed by stringers. The boundary conditions for these stringers have been decided to be simply supported. In figure 1 we can see the result of the partitioning that was just described. Although we will not model the effects of taper (Future implementation).



Figure 1: Wing modeled as a combination of simply supported sheets and simply supported bays. Figure taken from T.H.G Megson, Aircraft Structures For Engineering Students


Now that our wing has been divided into bays, and the bays respectively into sheets we can limit the scope of the optimization. Instead, of optimizing for the entire wing simultaneously we will optimize each bay. In figure 2 an overview is found on how the optimization in this notebook will tackle the sizing of the structural members in the wing.



Figure 2: An overview of the optimization procedure for an entire wing.


The following sections will now enclose how to optimize a single one of these bays, starting with constraining the design space.

Constraining the design#

During the optimization it is important we set the minimum constraints the design should meet. Otherwise, the optimum solution would be no wingbox at all. For the API documentation of all these constraints please visit constraints documentation. We’ll cover the most important ones in this document and link to the documentation for the remainder of the constraints.

Buckling#

As previously discussed, a section of the wingbox is divided up in plates. Knowing these plates are simply supported we can compute the critical instability in both shear and compression using equations 1 and 2, respectively:

\[ \tag{1} \sigma_{cr} = k_b \frac{pi^2 E}{12(1 - \nu)} \left(\frac{t_{sk}}{b}\right)^2 \]
\[ \tag{2} \sigma_{cr} = k_c \frac{pi^2 E}{12(1 - \nu)} \left(\frac{t_{sk}}{b}\right)^2 \]

For the specifics on \(k_b\) and \(k_c\), please review the API documentation.

Whilst, we could simply apply them individually these two critical loads are not independent from each other. In increased shear loading, decreases the compression capability of the sheet. Hence, we use an interaction curve as described in equation 3.

\[ \tag{3} \frac{N_x}{N_{x,crit}} + \left(\frac{N_{xy}}{N_{xy,crit}}\right)^2 < 1 \]

In equation 3, \(N_{x,crit}\) is computed with equation 2 and \(N_{xy,crit}\) with equation 1. \(N_x\) and \(N_{xy}\) are the loads specified by the user.

Finally, we also constrain the design using global skin buckling as a stiffened panel can also buckle as a whole. In this case, the width of the panel is utilized instead of the stringer pitch, and simply supported conditions can be assumed. The contribution of the stringers that still provide a stiffening effect can be considered by smearing their thickness to the skin thickness, as in Equation 47.

\[ \tag{47} t_{smeared} = \frac{t_{sk} \cdot b + N_{str} \cdot A_{st}}{b} \]

The smeared thickness is substituted in the equation for critical sheet compression. The constraint is expressed below.

\[ \sigma_{cr,glob} - \sigma_{cr,loc} \geq 0 \]

Von Mises failure Criterion#

Besides buckling we also do not want the wingbox to yield. Hence we also apply the von Mises yield criterion. Considering the direct stresses and plane stresses that occur in the wingbox we can derive equation 4.

\[\begin{split} \begin{align*} \sigma_y & \geq \sigma_v \\ \tag{4} \sigma_y & \geq \sqrt{\sigma_{11}^2 + 3\tau^2} \\ \end{align*} \end{split}\]

Abiding by this constraint ensures that the wingbox does not yield under the expected loads.

Other constraints#

Leveraging Python for the optimization#

In order to leverage Python for our optimization, we will first have to install two libraries into the google collab environment. These are tuduam and pymoo, tuduam is a library specifically tailored for this course and pymoo a library for multi-objective optimization. Please run the code cell below in order to do.

!pip install tuduam
!pip install pymoo
!git clone https://github.com/saullocastro/tuduam.git
# If the following error occurs "fatal: destination path 'tuduam' already exists and is not an empty directory." please continue as your environment is already set up correctly
Requirement already satisfied: tuduam in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/tuduam-2026.8-py3.11.egg/ (2026.8)
Requirement already satisfied: numpy in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from tuduam) (2.4.2)
Requirement already satisfied: scipy in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from tuduam) (1.17.0)
Requirement already satisfied: pymoo in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from tuduam) (0.6.1.6)
Requirement already satisfied: moocore>=0.1.7 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo->tuduam) (0.2.0)
Requirement already satisfied: autograd>=1.4 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo->tuduam) (1.8.0)
Requirement already satisfied: cma>=3.2.2 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo->tuduam) (4.4.2)
Requirement already satisfied: matplotlib>=3 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo->tuduam) (3.10.8)
Requirement already satisfied: alive_progress in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo->tuduam) (3.3.0)
Requirement already satisfied: Deprecated in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo->tuduam) (1.3.1)
Requirement already satisfied: contourpy>=1.0.1 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo->tuduam) (1.3.3)
Requirement already satisfied: cycler>=0.10 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo->tuduam) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo->tuduam) (4.61.1)
Requirement already satisfied: kiwisolver>=1.3.1 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo->tuduam) (1.4.9)
Requirement already satisfied: packaging>=20.0 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo->tuduam) (26.0)
Requirement already satisfied: pillow>=8 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo->tuduam) (12.1.0)
Requirement already satisfied: pyparsing>=3 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo->tuduam) (3.3.2)
Requirement already satisfied: python-dateutil>=2.7 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo->tuduam) (2.9.0.post0)
Requirement already satisfied: cffi>=1.17.1 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from moocore>=0.1.7->pymoo->tuduam) (2.0.0)
Requirement already satisfied: platformdirs in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from moocore>=0.1.7->pymoo->tuduam) (4.5.1)
Requirement already satisfied: pycparser in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from cffi>=1.17.1->moocore>=0.1.7->pymoo->tuduam) (3.0)
Requirement already satisfied: six>=1.5 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from python-dateutil>=2.7->matplotlib>=3->pymoo->tuduam) (1.17.0)
Requirement already satisfied: about-time==4.2.1 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from alive_progress->pymoo->tuduam) (4.2.1)
Requirement already satisfied: graphemeu==0.7.2 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from alive_progress->pymoo->tuduam) (0.7.2)
Requirement already satisfied: wrapt<3,>=1.10 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from Deprecated->pymoo->tuduam) (2.1.1)
Requirement already satisfied: pymoo in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (0.6.1.6)
Requirement already satisfied: numpy>=1.19.3 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo) (2.4.2)
Requirement already satisfied: scipy>=1.1 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo) (1.17.0)
Requirement already satisfied: moocore>=0.1.7 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo) (0.2.0)
Requirement already satisfied: autograd>=1.4 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo) (1.8.0)
Requirement already satisfied: cma>=3.2.2 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo) (4.4.2)
Requirement already satisfied: matplotlib>=3 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo) (3.10.8)
Requirement already satisfied: alive_progress in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo) (3.3.0)
Requirement already satisfied: Deprecated in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from pymoo) (1.3.1)
Requirement already satisfied: contourpy>=1.0.1 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo) (1.3.3)
Requirement already satisfied: cycler>=0.10 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo) (4.61.1)
Requirement already satisfied: kiwisolver>=1.3.1 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo) (1.4.9)
Requirement already satisfied: packaging>=20.0 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo) (26.0)
Requirement already satisfied: pillow>=8 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo) (12.1.0)
Requirement already satisfied: pyparsing>=3 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo) (3.3.2)
Requirement already satisfied: python-dateutil>=2.7 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from matplotlib>=3->pymoo) (2.9.0.post0)
Requirement already satisfied: cffi>=1.17.1 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from moocore>=0.1.7->pymoo) (2.0.0)
Requirement already satisfied: platformdirs in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from moocore>=0.1.7->pymoo) (4.5.1)
Requirement already satisfied: pycparser in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from cffi>=1.17.1->moocore>=0.1.7->pymoo) (3.0)
Requirement already satisfied: six>=1.5 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from python-dateutil>=2.7->matplotlib>=3->pymoo) (1.17.0)
Requirement already satisfied: about-time==4.2.1 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from alive_progress->pymoo) (4.2.1)
Requirement already satisfied: graphemeu==0.7.2 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from alive_progress->pymoo) (0.7.2)
Requirement already satisfied: wrapt<3,>=1.10 in /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages (from Deprecated->pymoo) (2.1.1)
Cloning into 'tuduam'...
remote: Enumerating objects: 2073, done.
remote: Counting objects:   1% (1/83)
remote: Counting objects:   2% (2/83)
remote: Counting objects:   3% (3/83)
remote: Counting objects:   4% (4/83)
remote: Counting objects:   6% (5/83)
remote: Counting objects:   7% (6/83)
remote: Counting objects:   8% (7/83)
remote: Counting objects:   9% (8/83)
remote: Counting objects:  10% (9/83)
remote: Counting objects:  12% (10/83)
remote: Counting objects:  13% (11/83)
remote: Counting objects:  14% (12/83)
remote: Counting objects:  15% (13/83)
remote: Counting objects:  16% (14/83)
remote: Counting objects:  18% (15/83)
remote: Counting objects:  19% (16/83)
remote: Counting objects:  20% (17/83)
remote: Counting objects:  21% (18/83)
remote: Counting objects:  22% (19/83)
remote: Counting objects:  24% (20/83)
remote: Counting objects:  25% (21/83)
remote: Counting objects:  26% (22/83)
remote: Counting objects:  27% (23/83)
remote: Counting objects:  28% (24/83)
remote: Counting objects:  30% (25/83)
remote: Counting objects:  31% (26/83)
remote: Counting objects:  32% (27/83)
remote: Counting objects:  33% (28/83)
remote: Counting objects:  34% (29/83)
remote: Counting objects:  36% (30/83)
remote: Counting objects:  37% (31/83)
remote: Counting objects:  38% (32/83)
remote: Counting objects:  39% (33/83)
remote: Counting objects:  40% (34/83)
remote: Counting objects:  42% (35/83)
remote: Counting objects:  43% (36/83)
remote: Counting objects:  44% (37/83)
remote: Counting objects:  45% (38/83)
remote: Counting objects:  46% (39/83)
remote: Counting objects:  48% (40/83)
remote: Counting objects:  49% (41/83)
remote: Counting objects:  50% (42/83)
remote: Counting objects:  51% (43/83)
remote: Counting objects:  53% (44/83)
remote: Counting objects:  54% (45/83)
remote: Counting objects:  55% (46/83)
remote: Counting objects:  56% (47/83)
remote: Counting objects:  57% (48/83)
remote: Counting objects:  59% (49/83)
remote: Counting objects:  60% (50/83)
remote: Counting objects:  61% (51/83)
remote: Counting objects:  62% (52/83)
remote: Counting objects:  63% (53/83)
remote: Counting objects:  65% (54/83)
remote: Counting objects:  66% (55/83)
remote: Counting objects:  67% (56/83)
remote: Counting objects:  68% (57/83)
remote: Counting objects:  69% (58/83)
remote: Counting objects:  71% (59/83)
remote: Counting objects:  72% (60/83)
remote: Counting objects:  73% (61/83)
remote: Counting objects:  74% (62/83)
remote: Counting objects:  75% (63/83)
remote: Counting objects:  77% (64/83)
remote: Counting objects:  78% (65/83)
remote: Counting objects:  79% (66/83)
remote: Counting objects:  80% (67/83)
remote: Counting objects:  81% (68/83)
remote: Counting objects:  83% (69/83)
remote: Counting objects:  84% (70/83)
remote: Counting objects:  85% (71/83)
remote: Counting objects:  86% (72/83)
remote: Counting objects:  87% (73/83)
remote: Counting objects:  89% (74/83)
remote: Counting objects:  90% (75/83)
remote: Counting objects:  91% (76/83)
remote: Counting objects:  92% (77/83)
remote: Counting objects:  93% (78/83)
remote: Counting objects:  95% (79/83)
remote: Counting objects:  96% (80/83)
remote: Counting objects:  97% (81/83)
remote: Counting objects:  98% (82/83)
remote: Counting objects: 100% (83/83)
remote: Counting objects: 100% (83/83), done.
remote: Compressing objects:   1% (1/59)
remote: Compressing objects:   3% (2/59)
remote: Compressing objects:   5% (3/59)
remote: Compressing objects:   6% (4/59)
remote: Compressing objects:   8% (5/59)
remote: Compressing objects:  10% (6/59)
remote: Compressing objects:  11% (7/59)
remote: Compressing objects:  13% (8/59)
remote: Compressing objects:  15% (9/59)
remote: Compressing objects:  16% (10/59)
remote: Compressing objects:  18% (11/59)
remote: Compressing objects:  20% (12/59)
remote: Compressing objects:  22% (13/59)
remote: Compressing objects:  23% (14/59)
remote: Compressing objects:  25% (15/59)
remote: Compressing objects:  27% (16/59)
remote: Compressing objects:  28% (17/59)
remote: Compressing objects:  30% (18/59)
remote: Compressing objects:  32% (19/59)
remote: Compressing objects:  33% (20/59)
remote: Compressing objects:  35% (21/59)
remote: Compressing objects:  37% (22/59)
remote: Compressing objects:  38% (23/59)
remote: Compressing objects:  40% (24/59)
remote: Compressing objects:  42% (25/59)
remote: Compressing objects:  44% (26/59)
remote: Compressing objects:  45% (27/59)
remote: Compressing objects:  47% (28/59)
remote: Compressing objects:  49% (29/59)
remote: Compressing objects:  50% (30/59)
remote: Compressing objects:  52% (31/59)
remote: Compressing objects:  54% (32/59)
remote: Compressing objects:  55% (33/59)
remote: Compressing objects:  57% (34/59)
remote: Compressing objects:  59% (35/59)
remote: Compressing objects:  61% (36/59)
remote: Compressing objects:  62% (37/59)
remote: Compressing objects:  64% (38/59)
remote: Compressing objects:  66% (39/59)
remote: Compressing objects:  67% (40/59)
remote: Compressing objects:  69% (41/59)
remote: Compressing objects:  71% (42/59)
remote: Compressing objects:  72% (43/59)
remote: Compressing objects:  74% (44/59)
remote: Compressing objects:  76% (45/59)
remote: Compressing objects:  77% (46/59)
remote: Compressing objects:  79% (47/59)
remote: Compressing objects:  81% (48/59)
remote: Compressing objects:  83% (49/59)
remote: Compressing objects:  84% (50/59)
remote: Compressing objects:  86% (51/59)
remote: Compressing objects:  88% (52/59)
remote: Compressing objects:  89% (53/59)
remote: Compressing objects:  91% (54/59)
remote: Compressing objects:  93% (55/59)
remote: Compressing objects:  94% (56/59)
remote: Compressing objects:  96% (57/59)
remote: Compressing objects:  98% (58/59)
remote: Compressing objects: 100% (59/59)
remote: Compressing objects: 100% (59/59), done.
Receiving objects:   0% (1/2073)
Receiving objects:   1% (21/2073)
Receiving objects:   2% (42/2073)
Receiving objects:   3% (63/2073)
Receiving objects:   4% (83/2073)
Receiving objects:   5% (104/2073)
Receiving objects:   6% (125/2073)
Receiving objects:   7% (146/2073)
Receiving objects:   8% (166/2073)
Receiving objects:   9% (187/2073)
Receiving objects:  10% (208/2073)
Receiving objects:  11% (229/2073)
Receiving objects:  12% (249/2073)
Receiving objects:  13% (270/2073)
Receiving objects:  14% (291/2073)
Receiving objects:  15% (311/2073)
Receiving objects:  16% (332/2073)
Receiving objects:  17% (353/2073)
Receiving objects:  18% (374/2073)
Receiving objects:  19% (394/2073)
Receiving objects:  20% (415/2073)
Receiving objects:  21% (436/2073)
Receiving objects:  22% (457/2073)
Receiving objects:  23% (477/2073)
Receiving objects:  24% (498/2073)
Receiving objects:  25% (519/2073)
Receiving objects:  26% (539/2073)
Receiving objects:  27% (560/2073)
Receiving objects:  28% (581/2073)
Receiving objects:  29% (602/2073)
Receiving objects:  30% (622/2073)
Receiving objects:  31% (643/2073)
Receiving objects:  32% (664/2073)
Receiving objects:  33% (685/2073)
Receiving objects:  34% (705/2073)
Receiving objects:  35% (726/2073)
Receiving objects:  36% (747/2073)
Receiving objects:  37% (768/2073)
Receiving objects:  38% (788/2073)
Receiving objects:  39% (809/2073)
Receiving objects:  40% (830/2073)
Receiving objects:  41% (850/2073)
Receiving objects:  42% (871/2073)
Receiving objects:  43% (892/2073)
Receiving objects:  44% (913/2073)
Receiving objects:  45% (933/2073)
Receiving objects:  46% (954/2073)
Receiving objects:  47% (975/2073)
Receiving objects:  48% (996/2073)
Receiving objects:  49% (1016/2073)
Receiving objects:  50% (1037/2073)
Receiving objects:  51% (1058/2073)
Receiving objects:  52% (1078/2073)
Receiving objects:  53% (1099/2073)
Receiving objects:  54% (1120/2073)
Receiving objects:  55% (1141/2073)
Receiving objects:  56% (1161/2073)
Receiving objects:  57% (1182/2073)
Receiving objects:  58% (1203/2073)
Receiving objects:  59% (1224/2073)
Receiving objects:  60% (1244/2073)
Receiving objects:  61% (1265/2073)
Receiving objects:  62% (1286/2073)
Receiving objects:  63% (1306/2073)
Receiving objects:  64% (1327/2073)
Receiving objects:  65% (1348/2073)
Receiving objects:  66% (1369/2073)
Receiving objects:  67% (1389/2073)
Receiving objects:  68% (1410/2073)
Receiving objects:  69% (1431/2073)
Receiving objects:  70% (1452/2073)
Receiving objects:  71% (1472/2073)
Receiving objects:  72% (1493/2073)
Receiving objects:  73% (1514/2073)
Receiving objects:  74% (1535/2073)
Receiving objects:  75% (1555/2073)
Receiving objects:  76% (1576/2073)
Receiving objects:  77% (1597/2073)
Receiving objects:  78% (1617/2073)
Receiving objects:  79% (1638/2073)
Receiving objects:  80% (1659/2073)
Receiving objects:  81% (1680/2073)
Receiving objects:  82% (1700/2073)
Receiving objects:  83% (1721/2073)
Receiving objects:  84% (1742/2073)
Receiving objects:  85% (1763/2073)
Receiving objects:  86% (1783/2073)
Receiving objects:  87% (1804/2073)
Receiving objects:  88% (1825/2073)
Receiving objects:  89% (1845/2073)
Receiving objects:  90% (1866/2073)
Receiving objects:  91% (1887/2073)
Receiving objects:  92% (1908/2073)
Receiving objects:  93% (1928/2073)
Receiving objects:  94% (1949/2073)
Receiving objects:  95% (1970/2073)
Receiving objects:  96% (1991/2073)
Receiving objects:  97% (2011/2073)
Receiving objects:  98% (2032/2073)
Receiving objects:  99% (2053/2073)
remote: Total 2073 (delta 30), reused 53 (delta 21), pack-reused 1990 (from 1)
Receiving objects: 100% (2073/2073)
Receiving objects: 100% (2073/2073), 13.02 MiB | 26.04 MiB/s, done.
Resolving deltas:   0% (0/1248)
Resolving deltas:   1% (13/1248)
Resolving deltas:   2% (25/1248)
Resolving deltas:   3% (38/1248)
Resolving deltas:   4% (50/1248)
Resolving deltas:   5% (63/1248)
Resolving deltas:   6% (75/1248)
Resolving deltas:   7% (88/1248)
Resolving deltas:   8% (100/1248)
Resolving deltas:   9% (113/1248)
Resolving deltas:  10% (125/1248)
Resolving deltas:  11% (138/1248)
Resolving deltas:  12% (150/1248)
Resolving deltas:  13% (163/1248)
Resolving deltas:  14% (175/1248)
Resolving deltas:  15% (188/1248)
Resolving deltas:  16% (200/1248)
Resolving deltas:  17% (213/1248)
Resolving deltas:  18% (226/1248)
Resolving deltas:  19% (238/1248)
Resolving deltas:  20% (250/1248)
Resolving deltas:  21% (263/1248)
Resolving deltas:  22% (275/1248)
Resolving deltas:  23% (289/1248)
Resolving deltas:  24% (300/1248)
Resolving deltas:  25% (312/1248)
Resolving deltas:  26% (325/1248)
Resolving deltas:  27% (337/1248)
Resolving deltas:  28% (350/1248)
Resolving deltas:  29% (362/1248)
Resolving deltas:  30% (375/1248)
Resolving deltas:  31% (387/1248)
Resolving deltas:  32% (400/1248)
Resolving deltas:  33% (413/1248)
Resolving deltas:  34% (425/1248)
Resolving deltas:  35% (437/1248)
Resolving deltas:  36% (450/1248)
Resolving deltas:  37% (462/1248)
Resolving deltas:  38% (475/1248)
Resolving deltas:  39% (487/1248)
Resolving deltas:  40% (500/1248)
Resolving deltas:  41% (512/1248)
Resolving deltas:  42% (526/1248)
Resolving deltas:  43% (537/1248)
Resolving deltas:  44% (550/1248)
Resolving deltas:  45% (562/1248)
Resolving deltas:  46% (575/1248)
Resolving deltas:  47% (587/1248)
Resolving deltas:  48% (600/1248)
Resolving deltas:  49% (612/1248)
Resolving deltas:  50% (624/1248)
Resolving deltas:  51% (637/1248)
Resolving deltas:  52% (649/1248)
Resolving deltas:  53% (662/1248)
Resolving deltas:  54% (674/1248)
Resolving deltas:  55% (687/1248)
Resolving deltas:  56% (699/1248)
Resolving deltas:  57% (712/1248)
Resolving deltas:  58% (724/1248)
Resolving deltas:  59% (737/1248)
Resolving deltas:  60% (749/1248)
Resolving deltas:  61% (762/1248)
Resolving deltas:  62% (774/1248)
Resolving deltas:  63% (787/1248)
Resolving deltas:  64% (799/1248)
Resolving deltas:  65% (812/1248)
Resolving deltas:  66% (824/1248)
Resolving deltas:  67% (837/1248)
Resolving deltas:  68% (849/1248)
Resolving deltas:  69% (862/1248)
Resolving deltas:  70% (874/1248)
Resolving deltas:  71% (887/1248)
Resolving deltas:  72% (899/1248)
Resolving deltas:  73% (912/1248)
Resolving deltas:  74% (924/1248)
Resolving deltas:  75% (936/1248)
Resolving deltas:  76% (949/1248)
Resolving deltas:  77% (961/1248)
Resolving deltas:  78% (974/1248)
Resolving deltas:  79% (987/1248)
Resolving deltas:  80% (999/1248)
Resolving deltas:  81% (1011/1248)
Resolving deltas:  82% (1024/1248)
Resolving deltas:  83% (1036/1248)
Resolving deltas:  84% (1049/1248)
Resolving deltas:  85% (1061/1248)
Resolving deltas:  86% (1074/1248)
Resolving deltas:  87% (1086/1248)
Resolving deltas:  88% (1099/1248)
Resolving deltas:  89% (1111/1248)
Resolving deltas:  90% (1124/1248)
Resolving deltas:  91% (1136/1248)
Resolving deltas:  92% (1149/1248)
Resolving deltas:  93% (1161/1248)
Resolving deltas:  94% (1174/1248)
Resolving deltas:  95% (1186/1248)
Resolving deltas:  96% (1199/1248)
Resolving deltas:  97% (1211/1248)
Resolving deltas:  98% (1224/1248)
Resolving deltas:  99% (1237/1248)
Resolving deltas: 100% (1248/1248)
Resolving deltas: 100% (1248/1248), done.

Initialisation of parameters#

To start the initialisation of the optimisation that we are going to perform there are a two parameters that require a definition as these are designer choices.

import os
import sys
# sys.path.append("/content/tuduam")
# os.chdir("/content/tuduam")
import tuduam
# import importlib
# importlib.reload(tuduam)  # Force reload of the module
from tuduam.data_structures import Wingbox, Material




attr_dict = {
    "n_cell":4,
    "spar_loc_nondim":[0.3, 0.5, 0.75],
    "str_cell": [9,7,9,8]
}

mat_dict = {
        "young_modulus":3e9,
        "shear_modulus":80e9,
        "safety_factor":1.5,
        "load_factor":1.0,
        "poisson":0.3,
        "density":1600,
        "beta_crippling":1.42,
        "sigma_ultimate":407000000.0,
        "sigma_yield":407000000.0,
        "g_crippling":5
    }

mat_struct = Material(**mat_dict)
wingbox_struct = Wingbox(**attr_dict)

Having this definitnon we can now start the optimization. Before continuing it is probably useful to first take a look through the API documentation for the class and its methods that we will be using. The class that we will be using is SectionOpt which is situated in the structuressubpackage in tuduam. The link to the API docs can be found here.

Before iterating for an entire wing, let us first do an example for a single section. We’ll take a section of the wing where the mean chord, \(\bar{c} = 2\) and the length of this section \(b = 1.2\). For the airfoil, we’ll use th NACA 4412. For the amount of cells and spar locations we’ll use the previously defined values.

from tuduam.structures import SectionOpt
import os


chord = 3 # Chord length
coord_path = os.path.realpath(os.path.join(os.path.abspath('.'), 'tuduam',  'examples', 'naca_4412.txt')) # Path to airfoil coordinates
len_sec = .2 # Lenght of the section

opt_obj = SectionOpt(coord_path, chord, len_sec, wingbox_struct, mat_struct)

Remember figure 2 which portrayed the top view of the optimization loop. Let us start at the core, i.e the optimization that is run for a fixed amount of stringers. For us these stringers were defined wingbox_struct. This optimization in the library is defined in the method GA_optimize, here you can find the specifics.

Let’s run the optimization below with some typical values for the loads! Also note that we specify the upper and lower boundsFeel free to alter them and watch the design change.

import numpy as np

shear_y = 30e3
shear_x = 15000
moment_y = 4e2
moment_x = 30e3
applied_loc = 0.45
opt = SectionOpt(coord_path, chord, len_sec, wingbox_struct, mat_struct)

upper_bnds = 8*[0.012]
lower_bnds = 4*[0.0001] +  [0.003] + [0.001] + 2*[0.003]
res =  opt.GA_optimize(shear_y, shear_x, moment_y, moment_x, applied_loc, upper_bnds, lower_bnds, pop=20, n_gen= 60,multiprocess=True, verbose= True)

print(f"\n\n======== Results ==========")
print(f"Skin thickness = {np.array(res.X[:4])*1000} mm" )
print(f"Spar thickness = {res.X[4]*1000} mm")
print(f"Stringer thickness = {res.X[5]*1000} mm")
print(f"Stringer  width= {res.X[6]*1000} mm")
print(f"Stringer height = {res.X[7]*1000} mm")

# res =  opt_obj.full_section_opt(, pop=100, pop_full= 10, n_gen= 5 ,n_gen_full= 2)
/opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/tuduam-2026.8-py3.11.egg/tuduam/structures/optimization.py:618: UserWarning: pymoo StarmapParallelization not available in this pymoo version; running without multiprocessing.
/opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/tuduam-2026.8-py3.11.egg/tuduam/structures/wingbox.py:1389: UserWarning: 9 was not an even number and will be floored for conservative reasons
/opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/tuduam-2026.8-py3.11.egg/tuduam/structures/wingbox.py:1389: UserWarning: 7 was not an even number and will be floored for conservative reasons
/opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/tuduam-2026.8-py3.11.egg/tuduam/structures/wingbox.py:331: UserWarning: The thickness of stringer is larger than than the height of the stringer perhaps resulting in a negative area.
/opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/tuduam-2026.8-py3.11.egg/tuduam/structures/wingbox.py:334: UserWarning: The thickness of stringer is larger than than the width of the stringer resulting in nonsensical geometries.
/opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/tuduam-2026.8-py3.11.egg/tuduam/structures/wingbox.py:339: UserWarning: The stringer area is negative, please see previous error
/opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/tuduam-2026.8-py3.11.egg/tuduam/structures/constraints.py:437: RuntimeWarning: invalid value encountered in scalar power
=================================================================================
n_gen  |  n_eval  |     cv_min    |     cv_avg    |     f_avg     |     f_min    
=================================================================================
     1 |       20 |           nan |           nan |             - |             -
     2 |       40 |  3.124475E+05 |  1.581670E+06 |             - |             -
     3 |       60 |  1.967514E+05 |  8.721081E+05 |             - |             -
     4 |       80 |  4.974931E+01 |  3.411562E+05 |             - |             -
     5 |      100 |  4.973213E+01 |  8.181937E+04 |             - |             -
     6 |      120 |  4.973213E+01 |  5.059239E+02 |             - |             -
     7 |      140 |  4.179461E+01 |  2.124282E+02 |             - |             -
     8 |      160 |  3.994965E+01 |  5.384557E+01 |             - |             -
     9 |      180 |  1.146001E+01 |  3.896513E+01 |             - |             -
    10 |      200 |  1.106959E+01 |  3.073917E+01 |             - |             -
    11 |      220 |  9.0397564671 |  1.767104E+01 |             - |             -
    12 |      240 |  4.9992011941 |  1.108185E+01 |             - |             -
    13 |      260 |  4.9992011941 |  8.2869620634 |             - |             -
    14 |      280 |  1.1312434930 |  5.8253064960 |             - |             -
    15 |      300 |  0.0010583673 |  4.1940742481 |             - |             -
    16 |      320 |  0.0007603402 |  2.4769300555 |             - |             -
    17 |      340 |  0.0001142241 |  0.5117517691 |             - |             -
    18 |      360 |  0.0001142241 |  0.0008003192 |             - |             -
    19 |      380 |  0.0001039980 |  0.0005229120 |             - |             -
    20 |      400 |  0.000000E+00 |  0.0003459834 |  0.0408617457 |  0.0408617457
    21 |      420 |  0.000000E+00 |  0.0001840332 |  0.0405002293 |  0.0398670421
    22 |      440 |  0.000000E+00 |  0.0000639682 |  0.0405846230 |  0.0398174106
    23 |      460 |  0.000000E+00 |  0.000000E+00 |  0.0403925272 |  0.0397944723
    24 |      480 |  0.000000E+00 |  0.000000E+00 |  0.0400458024 |  0.0397944723
    25 |      500 |  0.000000E+00 |  0.000000E+00 |  0.0399167639 |  0.0397944723
    26 |      520 |  0.000000E+00 |  0.000000E+00 |  0.0397680219 |  0.0390697731
    27 |      540 |  0.000000E+00 |  0.000000E+00 |  0.0397000627 |  0.0390697731
    28 |      560 |  0.000000E+00 |  0.000000E+00 |  0.0394871157 |  0.0390355987
    29 |      580 |  0.000000E+00 |  0.000000E+00 |  0.0393660474 |  0.0386382257
    30 |      600 |  0.000000E+00 |  0.000000E+00 |  0.0390947292 |  0.0386382257
    31 |      620 |  0.000000E+00 |  0.000000E+00 |  0.0389476069 |  0.0386288914
    32 |      640 |  0.000000E+00 |  0.000000E+00 |  0.0388201701 |  0.0386238803
    33 |      660 |  0.000000E+00 |  0.000000E+00 |  0.0387276301 |  0.0386238803
    34 |      680 |  0.000000E+00 |  0.000000E+00 |  0.0386018879 |  0.0380682464
    35 |      700 |  0.000000E+00 |  0.000000E+00 |  0.0385194164 |  0.0380682464
    36 |      720 |  0.000000E+00 |  0.000000E+00 |  0.0384130114 |  0.0379608271
    37 |      740 |  0.000000E+00 |  0.000000E+00 |  0.0382254157 |  0.0379572670
    38 |      760 |  0.000000E+00 |  0.000000E+00 |  0.0380096466 |  0.0375285619
    39 |      780 |  0.000000E+00 |  0.000000E+00 |  0.0379017994 |  0.0374398807
    40 |      800 |  0.000000E+00 |  0.000000E+00 |  0.0377893966 |  0.0374398807
    41 |      820 |  0.000000E+00 |  0.000000E+00 |  0.0376694973 |  0.0372546393
    42 |      840 |  0.000000E+00 |  0.000000E+00 |  0.0375277824 |  0.0372546393
    43 |      860 |  0.000000E+00 |  0.000000E+00 |  0.0373917647 |  0.0372546393
    44 |      880 |  0.000000E+00 |  0.000000E+00 |  0.0373246662 |  0.0371866475
    45 |      900 |  0.000000E+00 |  0.000000E+00 |  0.0372442466 |  0.0371146880
    46 |      920 |  0.000000E+00 |  0.000000E+00 |  0.0371735743 |  0.0366751864
    47 |      940 |  0.000000E+00 |  0.000000E+00 |  0.0370927213 |  0.0366751864
    48 |      960 |  0.000000E+00 |  0.000000E+00 |  0.0369817672 |  0.0366751864
    49 |      980 |  0.000000E+00 |  0.000000E+00 |  0.0368707281 |  0.0366751864
    50 |     1000 |  0.000000E+00 |  0.000000E+00 |  0.0367998894 |  0.0366545298
    51 |     1020 |  0.000000E+00 |  0.000000E+00 |  0.0367391182 |  0.0365531983
    52 |     1040 |  0.000000E+00 |  0.000000E+00 |  0.0366872155 |  0.0365531983
    53 |     1060 |  0.000000E+00 |  0.000000E+00 |  0.0366404731 |  0.0365239606
    54 |     1080 |  0.000000E+00 |  0.000000E+00 |  0.0366103000 |  0.0365239606
    55 |     1100 |  0.000000E+00 |  0.000000E+00 |  0.0365627600 |  0.0364333112
    56 |     1120 |  0.000000E+00 |  0.000000E+00 |  0.0365306851 |  0.0364333112
    57 |     1140 |  0.000000E+00 |  0.000000E+00 |  0.0364787116 |  0.0363396022
    58 |     1160 |  0.000000E+00 |  0.000000E+00 |  0.0364455249 |  0.0363387818
    59 |     1180 |  0.000000E+00 |  0.000000E+00 |  0.0364235888 |  0.0363387818
    60 |     1200 |  0.000000E+00 |  0.000000E+00 |  0.0364048380 |  0.0363387034


======== Results ==========
Skin thickness = [6.29669944 6.59177534 5.91015319 4.36598275] mm
Spar thickness = 5.7720044356115405 mm
Stringer thickness = 1.3346322166553304 mm
Stringer  width= 3.3949805122081163 mm
Stringer height = 10.317599816412146 mm

We can also get to know more about the history of each iteration through the res variable which is an instance of the pymoo.core.result.Resultclass. In this class the objective functions is described with F and the design vector with X. The object contains the history of each generation which we can access like shown below where we access the first generation.

pop_lst = [gen.pop for gen in res.history]
print("Generation 1\nt_sk_cell1 - t_sk_cell2 - t_sk_cell3 - t_sk_cell4 - t_sp -  t_st - w_st - h_st")
print(f"=====================================================================\n\n {pop_lst[0].get('X')}")
Generation 1
t_sk_cell1 - t_sk_cell2 - t_sk_cell3 - t_sk_cell4 - t_sp -  t_st - w_st - h_st
=====================================================================

 [[0.00619068 0.01141052 0.0018155  0.01138893 0.00580648 0.00565659
  0.01044932 0.00668279]
 [0.00664016 0.00042795 0.00906681 0.00650391 0.00596759 0.00967272
  0.00572875 0.00708148]
 [0.0016951  0.00489704 0.00252112 0.00322153 0.00975328 0.0040845
  0.00736672 0.01182663]
 [0.01154372 0.008725   0.0065406  0.00339501 0.00444587 0.01166918
  0.00764462 0.00404279]
 [0.00751953 0.00934253 0.00739474 0.01101584 0.00335634 0.00681448
  0.00713402 0.00356115]
 [0.00773181 0.01024633 0.007156   0.00319516 0.01055893 0.00660445
  0.007598   0.00977727]
 [0.00186027 0.00985356 0.00823111 0.00946645 0.00472455 0.00982601
  0.00472192 0.00373397]
 [0.0102772  0.01034927 0.01053079 0.00571573 0.00546644 0.00107801
  0.00881149 0.00947918]
 [0.01004327 0.00345435 0.0026611  0.00770804 0.01024549 0.01160038
  0.00435472 0.00733991]
 [0.01074712 0.00513033 0.00711507 0.00039144 0.00906114 0.01110997
  0.01044143 0.01096968]
 [0.00795823 0.00302207 0.00924535 0.00261893 0.01048147 0.0016899
  0.01042939 0.00448057]
 [0.00456425 0.00386918 0.00832691 0.00222501 0.00656631 0.00106407
  0.00536245 0.0067907 ]
 [0.00136046 0.0076346  0.00462705 0.008731   0.00888479 0.00574349
  0.01080588 0.00868922]
 [0.00974226 0.00416736 0.00656966 0.00243593 0.01196527 0.00367537
  0.00531181 0.00365871]
 [0.00316786 0.00918123 0.00840493 0.00163121 0.00638615 0.00563014
  0.00898486 0.00710336]
 [0.00707957 0.01009225 0.00874504 0.00444359 0.00703557 0.0050447
  0.00398761 0.00482917]
 [0.0034773  0.00383819 0.00382527 0.00696273 0.01174521 0.00952131
  0.01012021 0.00983342]
 [0.00720415 0.01102054 0.0083066  0.00605424 0.00369375 0.00637294
  0.00491548 0.00419427]
 [0.00612217 0.00944251 0.00361058 0.00924838 0.00773067 0.00263953
  0.01168471 0.00661473]
 [0.00361329 0.01017928 0.00158108 0.00882973 0.00469042 0.00531741
  0.0050871  0.01057105]]

We can also plot the average value of variables over time and optimization function over time, this gives us an idea of how the variables effect the design. Run the code below:

import matplotlib.pyplot as plt

t_sk_cell1_lst = [np.average(pop.get("X")[:][0]) for pop in pop_lst]
t_sk_cell2_lst = [np.average(pop.get("X")[:][1]) for pop in pop_lst]
t_sk_cell3_lst = [np.average(pop.get("X")[:][2]) for pop in pop_lst]
t_sk_cell4_lst = [np.average(pop.get("X")[:][3]) for pop in pop_lst]
t_sp_lst = [np.average(pop.get("X")[:][4]) for pop in pop_lst]
t_st_lst = [np.average(pop.get("X")[:][5]) for pop in pop_lst]
w_st_lst = [np.average(pop.get("X")[:][6]) for pop in pop_lst]
h_st__lst = [np.average(pop.get("X")[:][7]) for pop in pop_lst]

f_lst = [np.average(pop.get("F")) for pop in pop_lst]

fig, (ax1, ax2) = plt.subplots(1, 2)
fig.set_size_inches(18.5, 10.5)

ax1.plot(t_sk_cell1_lst, label="T sk cell 1")
ax1.plot(t_sk_cell2_lst, label="T sk cell 2")
ax1.plot(t_sk_cell3_lst, label="T sk cell 3")
ax1.plot(t_sk_cell4_lst, label="T sk cell 4")
ax1.plot(t_sp_lst, label="T sp")
ax1.plot(t_st_lst, label="T st")
ax1.plot(w_st_lst, label="W st")
ax1.plot(h_st__lst, label="H st")

ax2.plot(f_lst, label="Weight")

ax1.set_title("Average value of variables over time")
ax2.set_title("Average value of the optimization function over time")


ax1.set_xlabel("Generation")
ax2.set_xlabel("Generation")
ax1.set_ylabel("Thickness (mm)")
ax1.legend()
ax2.legend()
ax1.grid()
ax2.grid()

plt.show()
../_images/840e443b5f6d70edec75cde85b95cffb50e00d04d6d3296dc887f3ba1fa20daa.png

As you can see, it gets quite messy with all variables in there. One could also argue the information gets lost when taking the average and only plotting the optimum solution would give a better idea. It is left up to the reader to implement this.

Changing the amount of stringers#

In the previous optimization we only optimized for the various thicknesses, however a better solution would be possible when different configuration of stringers are used, or even a different layout of ribs.

TUDuam also provides a method for optimizing for different stringer configuration. This is the outer optimization loop as has been shown earlier in this notebook in figure 2. The API documentation for this functionality can be found here. SectionOpt.full_section_opt interally also used the optimization which we experimented with previously. However, it calls this function for every stringer configuration, hence it is signficantly more computationally expensive.

Additionally, stringers often run along multiple sections and hence we’re not completely free in the amount of stringer in each section.

The method does come with a multiprocess flag which does allow it to see major performance boosts on more powerful platforms. So feel free to give it a try on your local machine. The setup is very similar to GA_optimize.

Unlike for the stringer configuration, this repository does not implememt an optimization of the spanwise rib distribution.

Example: Optimizing the wing of a Cessna 172R#

Let us do an example of how we would design the structure of a complete wing. To keep it computationally bounded we are assuming a 3 cell wingbox with a fixed stringer distribution of 10 stringers per cell. Furthermore, here is some important data on the Cessna 172

Characteristic

Value

Wingspan

11 m

Aspect ratio

7.32

Surface Area

16.2 \(m^2\)

Gross weight

1110 kg

To come up with the correct loading per bay we make some oversimplified assumptions to make our life easier for this notebook.

Assumptions#

  1. Constant spanwise drag distribution of 230 N/m

  2. Therefore a linear Mydistribution (see below)

  3. A linear lift distribution which is maximum at the root of the wing.

  4. Hence, a cubic Mx distribution

  5. Ignoring the weight of the wing (conservative)

The functions below will be used to get the correct internal forces for each rib section, as can be inferred from average_fun we assume a constant force over each section. This constant force is taken to be the average value over the entire section.

from scipy.integrate import quad
import matplotlib.pyplot as plt
import numpy as np

def average_fun(fun,
                x_start: float,
                x_end: float
                ):
    """
    Calculate the average value of a function `func` over the interval [a, b].

    Parameters:
    func (function): A function that takes a float and returns a float.
    a (float): Lower bound of the interval.
    b (float): Upper bound of the interval.

    Returns:
    float: The average value of the function over [a, b].
    """
    val, _ = quad(fun, x_start, x_end)
    return val / (x_end - x_start)

def shear_y(span):
  return 5444 - quad(lambda x: 1979 - 1979/5.5*x,  0, span)[0]

def moment_y(span):
  return -9980 + quad(shear_y, 0, span)[0]

def shear_x(span):
  return -1265 + 230*span

def moment_x(span):
  return 3478.75 + quad(shear_x, 0, span)[0]

# print(average_fun(moment_x, 5, 5.5))
# print(moment_y(3))

# x = np.linspace(0,5.5,1000)
# y = np.vectorize(moment_x)(x)
# plt.plot(x,y)
# plt.show()

Creating the section#

We must now create the section to iterate over. See the code below whwere we create the rib locations. You can change the exponent of the slicer variable to alter the distribution of the rib locations

import numpy as np


span = 5.5
S = 16.2
n_sec = 8
chord = S/span
slicer = np.linspace(0,1, n_sec)**1.6
rib_loc = slicer*span

print(rib_loc)
[0.         0.24445888 0.74106076 1.41775058 2.24647615 3.21039028
 4.29781608 5.5       ]

Iterating over each section#

With the rib locations and loads specified we can iterate over all the sections. Please see the code below.

from tuduam.data_structures import Wingbox, Material
import warnings

warnings.simplefilter("ignore")
res_lst = []

for idx, rib_1 in enumerate(rib_loc, start=1):
  if idx == len(rib_loc):
    break
  else:
    rib_2 = rib_loc[idx]

  attr_dict = {
      "n_cell":3,
      "spar_loc_nondim":[0.3,  0.7],
      "str_cell": [10,10,10]
  }

  wingbox_struct = Wingbox(**attr_dict)

  shear_y_sec = average_fun(shear_y, rib_1, rib_2)
  shear_x_sec = average_fun(shear_x, rib_1, rib_2)
  moment_y_sec = average_fun(moment_y, rib_1, rib_2)
  moment_x_sec = average_fun(moment_x, rib_1, rib_2)

  applied_loc = 0.45
  opt = SectionOpt(coord_path, chord, len_sec, wingbox_struct, mat_struct)

  upper_bnds = 7*[0.012]
  lower_bnds = 3*[0.0001] +  [0.003] + [0.001] + 2*[0.003]
  res =  opt.GA_optimize(shear_y_sec, shear_x_sec, moment_y_sec, moment_x_sec, applied_loc, upper_bnds, lower_bnds, pop=20, n_gen= 60,multiprocess= False, verbose= False)
  res_lst.append(res)

  print(f"\n\n======== Results rib section {idx} rib {round(rib_1,2)} to {round(rib_2,2)} ==========")
  print(f"Skin thickness = {np.round(np.array(res.X[:3])*1000,2)} mm" )
  print(f"Spar thickness = {round(res.X[3]*1000,2)} mm")
  print(f"Stringer thickness = {round(res.X[4]*1000,2)} mm")
  print(f"Stringer  width= {round(res.X[5]*1000,2)} mm")
  print(f"Stringer height = {round(res.X[6]*1000,2)} mm")
======== Results rib section 1 rib 0.0 to 0.24 ==========
Skin thickness = [3.13 2.9  2.54] mm
Spar thickness = 3.37 mm
Stringer thickness = 1.0 mm
Stringer  width= 3.15 mm
Stringer height = 6.08 mm
======== Results rib section 2 rib 0.24 to 0.74 ==========
Skin thickness = [2.62 2.83 2.39] mm
Spar thickness = 3.19 mm
Stringer thickness = 1.0 mm
Stringer  width= 3.22 mm
Stringer height = 9.19 mm
======== Results rib section 3 rib 0.74 to 1.42 ==========
Skin thickness = [3.27 2.54 2.03] mm
Spar thickness = 3.02 mm
Stringer thickness = 2.1 mm
Stringer  width= 7.36 mm
Stringer height = 8.75 mm
======== Results rib section 4 rib 1.42 to 2.25 ==========
Skin thickness = [2.26 2.36 1.75] mm
Spar thickness = 3.0 mm
Stringer thickness = 2.78 mm
Stringer  width= 6.16 mm
Stringer height = 8.46 mm
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[9], line 31
     29 upper_bnds = 7*[0.012]
     30 lower_bnds = 3*[0.0001] +  [0.003] + [0.001] + 2*[0.003]
---> 31 res =  opt.GA_optimize(shear_y_sec, shear_x_sec, moment_y_sec, moment_x_sec, applied_loc, upper_bnds, lower_bnds, pop=20, n_gen= 60,multiprocess= False, verbose= False)
     32 res_lst.append(res)
     34 print(f"\n\n======== Results rib section {idx} rib {round(rib_1,2)} to {round(rib_2,2)} ==========")

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/tuduam-2026.8-py3.11.egg/tuduam/structures/optimization.py:637, in SectionOpt.GA_optimize(self, shear_y, shear_x, moment_y, moment_x, applied_loc, upper_bnds, lower_bnds, n_gen, pop, verbose, seed, multiprocess, cores, save_hist)
    621     problem = ProblemFixedPanel(
    622         shear_y,
    623         shear_x,
   (...)    633         self.path_coord,
    634     )
    636 method = GA(pop_size=pop, eliminate_duplicates=True)
--> 637 resGA = minimize(
    638     problem,
    639     method,
    640     termination=("n_gen", n_gen),
    641     seed=seed,
    642     save_history=save_hist,
    643     verbose=verbose,
    644 )
    645 return resGA

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/pymoo/optimize.py:67, in minimize(problem, algorithm, termination, copy_algorithm, copy_termination, **kwargs)
     64     algorithm.setup(problem, **kwargs)
     66 # actually execute the algorithm
---> 67 res = algorithm.run()
     69 # store the deep copied algorithm in the result object
     70 res.algorithm = algorithm

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/pymoo/core/algorithm.py:133, in Algorithm.run(self)
    131 def run(self):
    132     while self.has_next():
--> 133         self.next()
    134     return self.result()

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/pymoo/core/algorithm.py:153, in Algorithm.next(self)
    151 # call the advance with them after evaluation
    152 if infills is not None:
--> 153     self.evaluator.eval(self.problem, infills, algorithm=self)
    154     self.advance(infills=infills)
    156 # if the algorithm does not follow the infill-advance scheme just call advance
    157 else:

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/pymoo/core/evaluator.py:69, in Evaluator.eval(self, problem, pop, skip_already_evaluated, evaluate_values_of, count_evals, **kwargs)
     65 # evaluate the solutions (if there are any)
     66 if len(I) > 0:
     67 
     68     # do the actual evaluation - call the sub-function to set the corresponding values to the population
---> 69     self._eval(problem, pop[I], evaluate_values_of, **kwargs)
     71 # update the function evaluation counter
     72 if count_evals:

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/pymoo/core/evaluator.py:90, in Evaluator._eval(self, problem, pop, evaluate_values_of, **kwargs)
     87 X = pop.get("X")
     89 # call the problem to evaluate the solutions
---> 90 out = problem.evaluate(X, return_values_of=evaluate_values_of, return_as_dictionary=True, **kwargs)
     92 # for each of the attributes set it to the problem
     93 for key, val in out.items():

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/pymoo/core/problem.py:170, in Problem.evaluate(self, X, return_values_of, return_as_dictionary, *args, **kwargs)
    167     only_single_value = not (isinstance(X, list) or isinstance(X, np.ndarray))
    169 # this is where the actual evaluation takes place
--> 170 _out = self.do(X, return_values_of, *args, **kwargs)
    172 out = {}
    173 for k, v in _out.items():
    174 
    175     # copy it to a numpy array (it might be one of jax at this point)

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/pymoo/core/problem.py:210, in Problem.do(self, X, return_values_of, *args, **kwargs)
    208 # do the function evaluation
    209 if self.elementwise:
--> 210     self._evaluate_elementwise(X, out, *args, **kwargs)
    211 else:
    212     self._evaluate_vectorized(X, out, *args, **kwargs)

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/pymoo/core/problem.py:228, in Problem._evaluate_elementwise(self, X, out, *args, **kwargs)
    225 f = self.elementwise_func(self, args, kwargs)
    227 # execute the runner
--> 228 elems = self.elementwise_runner(f, X)
    230 # for each evaluation call
    231 for elem in elems:
    232 
    233     # for each key stored for this evaluation

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/pymoo/core/problem.py:16, in LoopedElementwiseEvaluation.__call__(self, f, X)
     15 def __call__(self, f, X):
---> 16     return [f(x) for x in X]

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/pymoo/core/problem.py:16, in <listcomp>(.0)
     15 def __call__(self, f, X):
---> 16     return [f(x) for x in X]

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/pymoo/core/problem.py:29, in ElementwiseEvaluationFunction.__call__(self, x)
     27 def __call__(self, x):
     28     out = dict()
---> 29     self.problem._evaluate(x, out, *self.args, **self.kwargs)
     30     return out

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/tuduam-2026.8-py3.11.egg/tuduam/structures/optimization.py:437, in ProblemFixedPanel._evaluate(self, x, out, *args, **kwargs)
    434 w_st = x[n_cell + 2]
    435 h_st = x[n_cell + 3]
--> 437 box_copy.load_new_gauge(t_sk_cell, t_sp, t_st, w_st, h_st)
    439 # Perform stress analysis
    440 box_copy.stress_analysis(
    441     self.shear_y,
    442     self.shear_x,
   (...)    446     self.mat_struct.shear_modulus,
    447 )

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/tuduam-2026.8-py3.11.egg/tuduam/structures/wingbox.py:685, in IdealWingbox.load_new_gauge(self, t_sk_cell, t_sp, t_st, w_st, h_st)
    682         idx = pnl.get_cell_idx(self.wingbox_struct, self.chord) # Find which cell  panel is in
    683         pnl.t_pnl = t_sk_cell[idx] # Assign new thickness
--> 685 self._compute_boom_areas(self.chord)
    686 self.Ixx = self._set_Ixx()
    687 self.Iyy = self._set_Iyy()

File /opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/tuduam-2026.8-py3.11.egg/tuduam/structures/wingbox.py:632, in IdealWingbox._compute_boom_areas(self, chord)
    630 boom.A = boom_area
    631 # check if it is not a spar boom, this will get a flange addition in the future maybe
--> 632 if not any(np.isclose(boom.x, spar_loc_abs )):
    633     boom.A += self.area_str
    634 if boom.A < 0:

KeyboardInterrupt: 

Viewing the results#

Above you have now seen the final result of each section. The history of each section as we have plotted previously, is also saved in the res_lst variable. The previous code we utilized is wrapped in a function below for easy access such that you can compare the the various sections. Now feel free to change the variables or use the API for your optimizations!

from pymoo.core.result import Result


def plot_history(res: Result):
  pop_lst = [gen.pop for gen in res.history]
  t_sk_cell1_lst = [np.average(pop.get("X")[:][0]) for pop in pop_lst]
  t_sk_cell2_lst = [np.average(pop.get("X")[:][1]) for pop in pop_lst]
  t_sk_cell3_lst = [np.average(pop.get("X")[:][2]) for pop in pop_lst]
  t_sk_cell4_lst = [np.average(pop.get("X")[:][3]) for pop in pop_lst]
  t_sp_lst = [np.average(pop.get("X")[:][4]) for pop in pop_lst]
  t_st_lst = [np.average(pop.get("X")[:][5]) for pop in pop_lst]
  w_st_lst = [np.average(pop.get("X")[:][6]) for pop in pop_lst]
  h_st__lst = [np.average(pop.get("X")[:][7]) for pop in pop_lst]

  f_lst = [np.average(pop.get("F")) for pop in pop_lst]

  fig, (ax1, ax2) = plt.subplots(1, 2)
  fig.set_size_inches(18.5, 10.5)

  ax1.plot(t_sk_cell1_lst, label="T sk cell 1")
  ax1.plot(t_sk_cell2_lst, label="T sk cell 2")
  ax1.plot(t_sk_cell3_lst, label="T sk cell 3")
  ax1.plot(t_sk_cell4_lst, label="T sk cell 4")
  ax1.plot(t_sp_lst, label="T sp")
  ax1.plot(t_st_lst, label="T st")
  ax1.plot(w_st_lst, label="W st")
  ax1.plot(h_st__lst, label="H st")

  ax2.plot(f_lst, label="Weight")

  ax1.set_title("Average value of variables over time")
  ax2.set_title("Average value of the optimization function over time")


  ax1.set_xlabel("Generation")
  ax2.set_xlabel("Generation")
  ax1.set_ylabel("Thickness (mm)")
  ax1.legend()
  ax2.legend()
  ax1.grid()
  ax2.grid()

  plt.show()

plot_history(res_lst[3])
../_images/7e4cc15336e75f1e7505d8674f1a2765d27880c599e3ead103378796a06b1e56.png