Tutorial: Ply tailoring using GA for a simply-supported plate under normal load#
Date: 4 February 2026
Author: Saullo G. P. Castro
Cite this tutorial as:
Saullo G. P. Castro. (2026). General-purpose finite element solver based on Python and Cython (Version 0.7.0). Zenodo. DOI: https://doi.org/10.5281/zenodo.6573489.
Installing required modules#
[1]:
!python -m pip install numpy pyvista pymoo scipy pyfe3d > tmp.txt
DEPRECATION: Loading egg at /Users/saullogiovanip/miniconda3/lib/python3.13/site-packages/panels-0.5.0-py3.13-macosx-11.1-arm64.egg is deprecated. pip 25.1 will enforce this behaviour change. A possible replacement is to use pip for package installation. Discussion can be found at https://github.com/pypa/pip/issues/12330
DEPRECATION: Loading egg at /Users/saullogiovanip/miniconda3/lib/python3.13/site-packages/tuduam-2026.3-py3.13.egg is deprecated. pip 25.1 will enforce this behaviour change. A possible replacement is to use pip for package installation. Discussion can be found at https://github.com/pypa/pip/issues/12330
[2]:
import numpy as np
import pyvista as pv
from pymoo.core.problem import ElementwiseProblem
from pymoo.core.callback import Callback
from pymoo.algorithms.soo.nonconvex.ga import GA
from pymoo.optimize import minimize
from pymoo.operators.crossover.sbx import SBX
from pymoo.operators.mutation.pm import PM
from pymoo.operators.sampling.rnd import IntegerRandomSampling
from pymoo.operators.repair.rounding import RoundingRepair
from pymoo.termination import get_termination
from scipy.sparse.linalg import spsolve
from scipy.sparse import coo_matrix
from pyfe3d.shellprop import ShellProp
from pyfe3d import Quad4, Quad4Data, Quad4Probe, INT, DOUBLE, DOF
def get_Q_matrix(E1, E2, nu12, G12, G13, G23, theta_deg):
"""
Calculate the transformed reduced stiffness matrix Q_bar for a given angle.
"""
nu21 = nu12 * E2 / E1
denom = 1 - nu12 * nu21
Q11 = E1 / denom
Q22 = E2 / denom
Q12 = nu12 * E2 / denom
Q66 = G12
c = np.cos(np.radians(theta_deg))
s = np.sin(np.radians(theta_deg))
c2 = c**2; s2 = s**2; c4 = c**4; s4 = s**4
Q_bar_11 = Q11*c4 + 2*(Q12 + 2*Q66)*s2*c2 + Q22*s4
Q_bar_22 = Q11*s4 + 2*(Q12 + 2*Q66)*s2*c2 + Q22*c4
Q_bar_12 = (Q11 + Q22 - 4*Q66)*s2*c2 + Q12*(s4 + c4)
Q_bar_66 = (Q11 + Q22 - 2*Q12 - 2*Q66)*s2*c2 + Q66*(s4 + c4)
Q_bar_44 = G23 * c2 + G13 * s2
Q_bar_55 = G13 * c2 + G23 * s2
Q_bar_45 = (G13 - G23) * c * s
Q_bar = np.array([
[Q_bar_11, Q_bar_12, 0, 0, 0, 0],
[Q_bar_12, Q_bar_22, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, Q_bar_44, Q_bar_45, 0],
[0, 0, 0, Q_bar_45, Q_bar_55, 0],
[0, 0, 0, 0, 0, Q_bar_66]
])
return Q_bar
class SmearedPlateOptimization(ElementwiseProblem):
def __init__(self, nx, ny, a, b, load_val, max_deflection, thicknesses, mat_data):
self.nx = nx
self.ny = ny
self.n_elems = (self.nx - 1)*(self.ny - 1)
self.a = a
self.b = b
self.load_val = load_val
self.max_deflection = max_deflection
self.rho = mat_data['rho']
# Pre-calculate Base Q matrices
E1, E2 = mat_data['E1'], mat_data['E2']
nu12, G12 = mat_data['nu12'], mat_data['G12']
G13, G23 = mat_data['G13'], mat_data['G23']
num_orientations = 3
self.Q0 = get_Q_matrix(E1, E2, nu12, G12, G13, G23, 0)
self.Q90 = get_Q_matrix(E1, E2, nu12, G12, G13, G23, 90)
self.Q45p = get_Q_matrix(E1, E2, nu12, G12, G13, G23, 45)
self.Q45m = get_Q_matrix(E1, E2, nu12, G12, G13, G23, -45)
self.Q45_sum = self.Q45p + self.Q45m
self.thicknesses = thicknesses
self.eid_prop = {}
n_var = num_orientations*self.n_elems
self._init_fe_model()
super().__init__(n_var=n_var,
n_obj=1,
n_ieq_constr=1,
xl=0,
xu=len(thicknesses) - 1,
vtype=int)
def _init_fe_model(self):
data = Quad4Data()
probe = Quad4Probe()
xtmp = np.linspace(0, self.a/2, self.nx)
ytmp = np.linspace(0, self.b/2, self.ny)
xmesh, ymesh = np.meshgrid(xtmp, ytmp)
ncoords = np.vstack((xmesh.T.flatten(), ymesh.T.flatten(), np.zeros_like(ymesh.T.flatten()))).T
self.ncoords = ncoords
x = ncoords[:, 0]
y = ncoords[:, 1]
z = ncoords[:, 2]
ncoords_flatten = ncoords.flatten()
self.ncoords_flatten = ncoords_flatten
nids = 1 + np.arange(ncoords.shape[0])
nid_pos = dict(zip(nids, np.arange(len(nids))))
nids_mesh = nids.reshape(self.nx, self.ny)
n1s = nids_mesh[:-1, :-1].flatten()
n2s = nids_mesh[1:, :-1].flatten()
n3s = nids_mesh[1:, 1:].flatten()
n4s = nids_mesh[:-1, 1:].flatten()
num_elements = len(n1s)
self.KC0r = np.zeros(data.KC0_SPARSE_SIZE*num_elements, dtype=INT)
self.KC0c = np.zeros(data.KC0_SPARSE_SIZE*num_elements, dtype=INT)
self.KC0v = np.zeros(data.KC0_SPARSE_SIZE*num_elements, dtype=DOUBLE)
self.N = DOF*self.nx*self.ny
self.quads = []
init_k_KC0 = 0
eid = -1
for n1, n2, n3, n4 in zip(n1s, n2s, n3s, n4s):
#NOTE dummy property
prop = ShellProp()
prop.scf_k13 = 5/6.
prop.scf_k23 = 5/6.
eid += 1
self.eid_prop[eid] = prop
pos1 = nid_pos[n1]
pos2 = nid_pos[n2]
pos3 = nid_pos[n3]
pos4 = nid_pos[n4]
r1 = ncoords[pos1]
r2 = ncoords[pos2]
r3 = ncoords[pos3]
normal = np.cross(r2 - r1, r3 - r2)[2]
assert normal > 0
quad = Quad4(probe)
quad.eid = eid
quad.n1 = n1
quad.n2 = n2
quad.n3 = n3
quad.n4 = n4
quad.c1 = DOF*nid_pos[n1]
quad.c2 = DOF*nid_pos[n2]
quad.c3 = DOF*nid_pos[n3]
quad.c4 = DOF*nid_pos[n4]
quad.init_k_KC0 = init_k_KC0
quad.update_rotation_matrix(ncoords_flatten)
quad.update_probe_xe(ncoords_flatten)
quad.update_KC0(self.KC0r, self.KC0c, self.KC0v, prop)
self.quads.append(quad)
init_k_KC0 += data.KC0_SPARSE_SIZE
def _evaluate(self, var, out, *args, **kwargs):
total_volume = 0.0
#NOTE important line below to zero the sparse matrix container
self.KC0v *= 0
for i, quad in enumerate(self.quads):
prop = self.eid_prop[quad.eid]
h0 = self.thicknesses[var[3*i]]
h45 = self.thicknesses[var[3*i + 1]]
h90 = self.thicknesses[var[3*i + 2]]
h_total = h0 + 2*h45 + h90
# Smeared Stiffness Calculation
A = (h0 * self.Q0) + (h90 * self.Q90) + (h45 * self.Q45_sum)
D = (h_total**2 / 12.0) * A
# NOTE using the full Voigt notation indices
# membrane stiffness
prop.A11 = A[0, 0]
prop.A12 = A[0, 1]
prop.A22 = A[1, 1]
prop.A66 = A[5, 5]
# transverse shear stiffness, the SCF is applied in pyfe3d
prop.E44 = A[3, 3]
prop.E45 = A[3, 4]
prop.E55 = A[4, 4]
# bending stiffness
prop.D11 = D[0, 0]
prop.D12 = D[0, 1]
prop.D22 = D[1, 1]
prop.D66 = D[5, 5]
prop.h = h_total
quad.update_probe_xe(self.ncoords_flatten)
quad.update_KC0(self.KC0r, self.KC0c, self.KC0v, prop, update_KC0v_only=1)
total_volume += quad.area * h_total
KC0 = coo_matrix((self.KC0v, (self.KC0r, self.KC0c)), shape=(self.N, self.N)).tocsc()
bk = np.zeros(self.N, dtype=bool)
x = self.ncoords[:, 0]
y = self.ncoords[:, 1]
z = self.ncoords[:, 2]
# removing in-plane motion
bk[0::DOF] = True #NOTE u displacements
bk[1::DOF] = True #NOTE v displacements
at_edges = np.isclose(x, 0.) | np.isclose(y, 0.)
bk[2::DOF][at_edges] = True #NOTE w displacements
at_sym_along_x = np.isclose(x, self.a/2)
bk[5::DOF][at_sym_along_x] = True
at_sym_along_y = np.isclose(y, self.b/2)
bk[4::DOF][at_sym_along_y] = True
bu = ~bk
# point load at center node
fext = np.zeros(self.N)
fmid = self.load_val
check = np.isclose(x, self.a/2) & np.isclose(y, self.b/2)
fext[2::DOF][check] = fmid
KC0uu = KC0[bu, :][:, bu]
# checking load balance
assert fext[bu].sum() == fmid
# solving the static system
uu = spsolve(KC0uu, fext[bu])
u = np.zeros(self.N)
u[bu] = uu
# reading the deflections in the z direction
w = u[2::DOF].reshape(self.nx, self.ny).T
self.result_w = u[2::DOF]
# reading the maximum absolute deflection
w_max = np.max(np.abs(w))
out["F"] = total_volume
out["G"] = [w_max/self.max_deflection - 1]
class HistorySaver(Callback):
def __init__(self):
super().__init__()
self.n_evals = []
self.F = [] # Objective History
self.G = [] # Constraint History
self.best_X = None # Store the best design variables here
self.best_F = None # Store best mass
self.best_G = None # Store best constraint
def notify(self, algorithm):
# Access the "optimum" (best individuals found so far)
if len(algorithm.opt) > 0:
best_ind = algorithm.opt[0]
# Save history for plotting
self.n_evals.append(algorithm.evaluator.n_eval)
self.F.append(best_ind.F[0])
g_val = best_ind.G[0] if best_ind.G is not None else 0.0
self.G.append(g_val)
# Save the specific values for the final result
# We use .copy() because X is a numpy array and we want the snapshot
self.best_X = best_ind.X.copy()
self.best_F = best_ind.F[0]
self.best_G = g_val
[3]:
if __name__ == "__main__":
# Material Data (Carbon/Epoxy approx)
mat_data = {
'E1': 140e9,
'E2': 10e9,
'nu12': 0.3,
'G12': 5e9,
'G13': 5e9,
'G23': 5e9,
'rho': 1600.0
}
# Problem definition
# allowed discrete thickness values
thicknesses = 0.125*1e-3*np.arange(1, 21)
problem = SmearedPlateOptimization(
nx=11, ny=6,
a=2.0, b=1.0,
load_val=80.0,
max_deflection=0.001,
thicknesses=thicknesses,
mat_data=mat_data
)
# Genetic Algorithm Setup
# NOTE about the eta parameter
# a low value (e.g., 2–5) creates a more flat and wide distribution, favours exploration: Offspring can be very far from parents.
# a high value (e.g., 20–30) creates a taller and narrower distribution, favours exploitation: Offspring stay very close to parents.
algorithm = GA(
pop_size=200,
sampling=IntegerRandomSampling(),
crossover=SBX(prob=1.0, eta=2, repair=RoundingRepair()),
mutation=PM(prob=0.1, eta=10, repair=RoundingRepair()),
eliminate_duplicates=True
)
# Termination criteria
termination = get_termination("n_gen", 100)
history = HistorySaver()
# Execute
res = minimize(problem,
algorithm,
termination,
seed=7,
verbose=True,
save_history=False,
callback=history)
print("Optimization Complete.")
print(f"Best Mass: {history.best_F:.4f} kg")
print(f"Max Deflection: {(history.best_G + 1)*problem.max_deflection:.4e}")
print(f"Max Deflection constraint violation: {history.best_G:.4e}")
=================================================================================
n_gen | n_eval | cv_min | cv_avg | f_avg | f_min
=================================================================================
1 | 200 | 0.3074207616 | 0.9498645000 | - | -
2 | 400 | 0.3074207616 | 0.6819816624 | - | -
3 | 600 | 0.2718360156 | 0.5820539527 | - | -
4 | 800 | 0.2524409595 | 0.5028083060 | - | -
5 | 1000 | 0.2423526994 | 0.4302689021 | - | -
6 | 1200 | 0.1309755566 | 0.3666742314 | - | -
7 | 1400 | 0.1079086246 | 0.3164371509 | - | -
8 | 1600 | 0.1051699700 | 0.2683484572 | - | -
9 | 1800 | 0.0858380415 | 0.2314240666 | - | -
10 | 2000 | 0.0546235208 | 0.1976557083 | - | -
11 | 2200 | 0.000000E+00 | 0.1674253846 | 0.0030475000 | 0.0030475000
12 | 2400 | 0.000000E+00 | 0.1362575453 | 0.0030187500 | 0.0029900000
13 | 2600 | 0.000000E+00 | 0.1117937157 | 0.0030787500 | 0.0029900000
14 | 2800 | 0.000000E+00 | 0.0918143173 | 0.0030900000 | 0.0029900000
15 | 3000 | 0.000000E+00 | 0.0735079364 | 0.0030925000 | 0.0029725000
16 | 3200 | 0.000000E+00 | 0.0557056066 | 0.0030882353 | 0.0029725000
17 | 3400 | 0.000000E+00 | 0.0393841119 | 0.0030803241 | 0.0029725000
18 | 3600 | 0.000000E+00 | 0.0232537506 | 0.0030798370 | 0.0029637500
19 | 3800 | 0.000000E+00 | 0.0120233624 | 0.0030811932 | 0.0029637500
20 | 4000 | 0.000000E+00 | 0.0040504034 | 0.0030804915 | 0.0029637500
21 | 4200 | 0.000000E+00 | 0.0001728106 | 0.0030758092 | 0.0029637500
22 | 4400 | 0.000000E+00 | 0.000000E+00 | 0.0030573000 | 0.0029637500
23 | 4600 | 0.000000E+00 | 0.000000E+00 | 0.0030375750 | 0.0029500000
24 | 4800 | 0.000000E+00 | 0.000000E+00 | 0.0030198125 | 0.0029262500
25 | 5000 | 0.000000E+00 | 0.000000E+00 | 0.0030030125 | 0.0028912500
26 | 5200 | 0.000000E+00 | 0.000000E+00 | 0.0029871687 | 0.0028775000
27 | 5400 | 0.000000E+00 | 0.000000E+00 | 0.0029701125 | 0.0028100000
28 | 5600 | 0.000000E+00 | 0.000000E+00 | 0.0029558812 | 0.0028100000
29 | 5800 | 0.000000E+00 | 0.000000E+00 | 0.0029397875 | 0.0027762500
30 | 6000 | 0.000000E+00 | 0.000000E+00 | 0.0029244812 | 0.0027762500
31 | 6200 | 0.000000E+00 | 0.000000E+00 | 0.0029112625 | 0.0027762500
32 | 6400 | 0.000000E+00 | 0.000000E+00 | 0.0028969312 | 0.0027762500
33 | 6600 | 0.000000E+00 | 0.000000E+00 | 0.0028832187 | 0.0027762500
34 | 6800 | 0.000000E+00 | 0.000000E+00 | 0.0028691562 | 0.0027237500
35 | 7000 | 0.000000E+00 | 0.000000E+00 | 0.0028550062 | 0.0027237500
36 | 7200 | 0.000000E+00 | 0.000000E+00 | 0.0028420937 | 0.0027237500
37 | 7400 | 0.000000E+00 | 0.000000E+00 | 0.0028307062 | 0.0027175000
38 | 7600 | 0.000000E+00 | 0.000000E+00 | 0.0028184062 | 0.0027162500
39 | 7800 | 0.000000E+00 | 0.000000E+00 | 0.0028072375 | 0.0027037500
40 | 8000 | 0.000000E+00 | 0.000000E+00 | 0.0027960812 | 0.0026975000
41 | 8200 | 0.000000E+00 | 0.000000E+00 | 0.0027867062 | 0.0026975000
42 | 8400 | 0.000000E+00 | 0.000000E+00 | 0.0027750312 | 0.0026975000
43 | 8600 | 0.000000E+00 | 0.000000E+00 | 0.0027632375 | 0.0026762500
44 | 8800 | 0.000000E+00 | 0.000000E+00 | 0.0027497937 | 0.0026762500
45 | 9000 | 0.000000E+00 | 0.000000E+00 | 0.0027382687 | 0.0026725000
46 | 9200 | 0.000000E+00 | 0.000000E+00 | 0.0027269562 | 0.0026487500
47 | 9400 | 0.000000E+00 | 0.000000E+00 | 0.0027147000 | 0.0026487500
48 | 9600 | 0.000000E+00 | 0.000000E+00 | 0.0027045687 | 0.0026375000
49 | 9800 | 0.000000E+00 | 0.000000E+00 | 0.0026956875 | 0.0025937500
50 | 10000 | 0.000000E+00 | 0.000000E+00 | 0.0026867187 | 0.0025937500
51 | 10200 | 0.000000E+00 | 0.000000E+00 | 0.0026776437 | 0.0025937500
52 | 10400 | 0.000000E+00 | 0.000000E+00 | 0.0026684687 | 0.0025937500
53 | 10600 | 0.000000E+00 | 0.000000E+00 | 0.0026595312 | 0.0025912500
54 | 10800 | 0.000000E+00 | 0.000000E+00 | 0.0026476375 | 0.0025812500
55 | 11000 | 0.000000E+00 | 0.000000E+00 | 0.0026412875 | 0.0025812500
56 | 11200 | 0.000000E+00 | 0.000000E+00 | 0.0026334562 | 0.0025625000
57 | 11400 | 0.000000E+00 | 0.000000E+00 | 0.0026245625 | 0.0025462500
58 | 11600 | 0.000000E+00 | 0.000000E+00 | 0.0026172875 | 0.0025462500
59 | 11800 | 0.000000E+00 | 0.000000E+00 | 0.0026108312 | 0.0025075000
60 | 12000 | 0.000000E+00 | 0.000000E+00 | 0.0026028187 | 0.0025075000
61 | 12200 | 0.000000E+00 | 0.000000E+00 | 0.0025956812 | 0.0025075000
62 | 12400 | 0.000000E+00 | 0.000000E+00 | 0.0025893375 | 0.0025075000
63 | 12600 | 0.000000E+00 | 0.000000E+00 | 0.0025811625 | 0.0024875000
64 | 12800 | 0.000000E+00 | 0.000000E+00 | 0.0025728625 | 0.0024800000
65 | 13000 | 0.000000E+00 | 0.000000E+00 | 0.0025627500 | 0.0024725000
66 | 13200 | 0.000000E+00 | 0.000000E+00 | 0.0025540500 | 0.0024725000
67 | 13400 | 0.000000E+00 | 0.000000E+00 | 0.0025460625 | 0.0024700000
68 | 13600 | 0.000000E+00 | 0.000000E+00 | 0.0025380312 | 0.0024675000
69 | 13800 | 0.000000E+00 | 0.000000E+00 | 0.0025295250 | 0.0024675000
70 | 14000 | 0.000000E+00 | 0.000000E+00 | 0.0025168750 | 0.0024462500
71 | 14200 | 0.000000E+00 | 0.000000E+00 | 0.0025042750 | 0.0024275000
72 | 14400 | 0.000000E+00 | 0.000000E+00 | 0.0024955437 | 0.0024275000
73 | 14600 | 0.000000E+00 | 0.000000E+00 | 0.0024878062 | 0.0024275000
74 | 14800 | 0.000000E+00 | 0.000000E+00 | 0.0024790375 | 0.0024275000
75 | 15000 | 0.000000E+00 | 0.000000E+00 | 0.0024738562 | 0.0024275000
76 | 15200 | 0.000000E+00 | 0.000000E+00 | 0.0024692750 | 0.0024275000
77 | 15400 | 0.000000E+00 | 0.000000E+00 | 0.0024647875 | 0.0024275000
78 | 15600 | 0.000000E+00 | 0.000000E+00 | 0.0024611687 | 0.0024087500
79 | 15800 | 0.000000E+00 | 0.000000E+00 | 0.0024564750 | 0.0024087500
80 | 16000 | 0.000000E+00 | 0.000000E+00 | 0.0024537000 | 0.0024087500
81 | 16200 | 0.000000E+00 | 0.000000E+00 | 0.0024493062 | 0.0024025000
82 | 16400 | 0.000000E+00 | 0.000000E+00 | 0.0024456125 | 0.0023962500
83 | 16600 | 0.000000E+00 | 0.000000E+00 | 0.0024418687 | 0.0023962500
84 | 16800 | 0.000000E+00 | 0.000000E+00 | 0.0024387750 | 0.0023962500
85 | 17000 | 0.000000E+00 | 0.000000E+00 | 0.0024342500 | 0.0023750000
86 | 17200 | 0.000000E+00 | 0.000000E+00 | 0.0024301187 | 0.0023750000
87 | 17400 | 0.000000E+00 | 0.000000E+00 | 0.0024283750 | 0.0023750000
88 | 17600 | 0.000000E+00 | 0.000000E+00 | 0.0024252625 | 0.0023750000
89 | 17800 | 0.000000E+00 | 0.000000E+00 | 0.0024231375 | 0.0023750000
90 | 18000 | 0.000000E+00 | 0.000000E+00 | 0.0024193500 | 0.0023750000
91 | 18200 | 0.000000E+00 | 0.000000E+00 | 0.0024158750 | 0.0023650000
92 | 18400 | 0.000000E+00 | 0.000000E+00 | 0.0024123875 | 0.0023650000
93 | 18600 | 0.000000E+00 | 0.000000E+00 | 0.0024090062 | 0.0023650000
94 | 18800 | 0.000000E+00 | 0.000000E+00 | 0.0024063375 | 0.0023650000
95 | 19000 | 0.000000E+00 | 0.000000E+00 | 0.0024029125 | 0.0023487500
96 | 19200 | 0.000000E+00 | 0.000000E+00 | 0.0024002187 | 0.0023487500
97 | 19400 | 0.000000E+00 | 0.000000E+00 | 0.0023969250 | 0.0023437500
98 | 19600 | 0.000000E+00 | 0.000000E+00 | 0.0023940500 | 0.0023437500
99 | 19800 | 0.000000E+00 | 0.000000E+00 | 0.0023911250 | 0.0023437500
100 | 20000 | 0.000000E+00 | 0.000000E+00 | 0.0023888687 | 0.0023437500
Optimization Complete.
Best Mass: 0.0023 kg
Max Deflection: 9.9921e-04
Max Deflection constraint violation: -7.9037e-04
[4]:
def plot_stacked_results(problem, history):
"""
Plot the optimised thickness distributions for h0, h45, and h90
as three stacked layers using PyVista.
"""
# 1. Retrieve Geometry from the Problem Object
ncoords = problem.ncoords
# 2. Build Connectivity for PyVista
# Determine the actual number of elements (quads) in the FE model
n_elems = len(problem.quads)
faces_quad = []
for quad in problem.quads:
# converting nid to index in the points array
faces_quad.append([4, quad.n1 - 1, quad.n2 - 1, quad.n3 - 1, quad.n4 - 1])
faces_quad = np.array(faces_quad)
base_grid = pv.PolyData(ncoords, faces_quad)
# 3. Extract Optimization Results
x_best = history.best_X
problem._evaluate(x_best, out={})
# Slice the variables with stride 3, then TRUNCATE to match the number of elements
# This discards the "ghost" variables created for the extra nodes
h0_vals = x_best[0::3]
h45_vals = x_best[1::3]
h90_vals = x_best[2::3]
# 4. Create Stacked Grids
z_offset = min(problem.a, problem.b) * 0.5
# --- Layer 0: deflection ---
grid_w = base_grid.copy()
grid_w.points[:, 2] -= z_offset
grid_w.point_data["Deflection [m]"] = problem.result_w
grid_w = grid_w.warp_by_scalar("Deflection [m]", factor=100.0)
# --- Layer 1: 0 degrees ---
grid_h0 = base_grid.copy()
grid_h0.cell_data["Thickness [m]"] = h0_vals
# --- Layer 2: +/- 45 degrees ---
grid_h45 = base_grid.copy()
grid_h45.points[:, 2] += z_offset
grid_h45.cell_data["Thickness [m]"] = h45_vals
# --- Layer 3: 90 degrees ---
grid_h90 = base_grid.copy()
grid_h90.points[:, 2] += 2 * z_offset
grid_h90.cell_data["Thickness [m]"] = h90_vals
plotter = pv.Plotter()
cmap = "viridis"
plotter.add_mesh(grid_w, scalars="Deflection [m]", cmap=cmap, show_edges=True)
plotter.add_mesh(grid_h0, scalars="Thickness [m]", cmap=cmap, show_edges=True)
plotter.add_mesh(grid_h45, scalars="Thickness [m]", cmap=cmap, show_edges=True)
plotter.add_mesh(grid_h90, scalars="Thickness [m]", cmap=cmap, show_edges=True)
label_x = -problem.a * 0.1
label_y = problem.b / 2
labels = ["deflection", "h_0", "h_45", "h_90"]
label_coords = [
[label_x, label_y, -z_offset],
[label_x, label_y, 0],
[label_x, label_y, z_offset],
[label_x, label_y, 2 * z_offset]
]
plotter.add_point_labels(
label_coords,
labels,
font_size=20,
point_size=0,
text_color="black",
always_visible=True
)
plotter.add_axes()
plotter.show_grid()
plotter.view_isometric()
plotter.show(title="Optimized Thickness Distribution per Orientation")
plot_stacked_results(problem, history)
/Users/saullogiovanip/miniconda3/lib/python3.13/site-packages/pyvista/jupyter/notebook.py:56: UserWarning: Failed to use notebook backend:
cannot import name 'vtk' from 'trame.widgets' (/Users/saullogiovanip/miniconda3/lib/python3.13/site-packages/trame/widgets/__init__.py)
Falling back to a static output.
warnings.warn(