#!/usr/bin/env python
# coding: utf-8
Code provided under BSD 3-Clause license, all other content under a Creative Commons Attribution license, CC-BY 4.0. (c) 2023 Francesco Mario Antonio Mitrotta.
# # Proof-of-concept of a Nonlinear Structural Stability Constraint for Aeroelastic Optimizations
#
# ***
#
# This notebook shows how to produce the figures published in the paper titled _Proof-of-concept of a Nonlinear Structural Stability Constraint for Aeroelastic Optimizations_, presented at the RAeS 8th Aircraft Structural Design Conference in October 2023 and authored by Francesco M. A. Mitrotta, Alberto Pirrera, Terence Macquart, Jonathan E. Cooper, Alex Pereira do Prado and Pedro Higino Cabral.
#
# * [Nonlinear structural stability of canonical examples](#canonical-examples)
# * [Supercritical pitchfork bifurcation](#supercritical-pitchfork)
# * [Broken supercritical pitchfork](#broken-pitchfork)
# * [Limit point bifurcation](#limit-point)
# * [Nonlinear structural stability of a CRM-like box beam](#crm-like-box-beam)
# * [Optimization of the CRM-like box beam with nonlinear structural stability constraints](#optimization)
# In[1]:
import os
# Define name of directory where to save analyses and figures
analysis_directory_name = "RAeS_2023"
ANALYSIS_DIRECTORY_PATH = os.path.join(os.getcwd(), "analyses", analysis_directory_name)
# ## Nonlinear structural stability of canonical examples
#
# ***
#
# ### Supercritical pitchfork bifurcation
# In[2]:
get_ipython().run_line_magic('matplotlib', 'widget')
import matplotlib.pyplot as plt # package for creating figures and plots
import tol_colors as tc # package for colorblind-friendly colors
import numpy as np # package for scientific computing
textwidth_inches = 5.146571 # paper textwidth in inches
fig_height_inches = textwidth_inches*(4.8/6.4) # default figure height in inches
plt.rc("axes", prop_cycle=plt.cycler("color", list(tc.tol_cset("bright")))) # set default color cycle to TOL bright
plt.rcParams["figure.dpi"] = 120 # set default dpi of figures
plt.rcParams.update({"text.usetex": True, "font.family": "serif"}) # set default font to serif
default_font_size = 8 # default font size of figures
plt.rcParams["font.size"] = default_font_size # set default font size of figures
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] # retrieve list with succession of standard matplotlib colors
UNSTABLE_COLOR = colors[1] # red
GLASS_CEILING_COLOR = colors[2] # green
del colors[1:3] # delete green and red from list of colors
# Plot trivial stable solution of perfect system
scale = 0.75 # scale of figure
fig, ax = plt.subplots(figsize=(textwidth_inches*scale, fig_height_inches*scale))
theta_trivial_stable = np.array([0., 0.]) # deg
load_trivial_stable = np.array([0., 1.])
stable_line = ax.plot(theta_trivial_stable, load_trivial_stable, label="stable")
# Plot nontrivial stable solution of perfect system
theta_max = 120 # deg
theta_nontrivial = np.linspace(-theta_max, theta_max) # deg
load_nontrivial = np.deg2rad(theta_nontrivial)/np.sin(np.deg2rad(theta_nontrivial))
ax.plot(theta_nontrivial, load_nontrivial, color=stable_line[0].get_color())
# Plot trivial unstable solution of perfect system
theta_trivial_unstable = np.array([0., 0.]) # deg
load_trivial_unstable = np.array([1., np.max(load_nontrivial)])
ax.plot(theta_trivial_unstable, load_trivial_unstable, UNSTABLE_COLOR, linestyle="--", label="unstable")
# Plot glass ceiling of linear buckling
ax.axhline(y=1, color=GLASS_CEILING_COLOR, linestyle="-.", label="glass ceiling of linear buckling")
# Plot bifurcation point
ax.plot(0, 1, "o", mec=UNSTABLE_COLOR, mfc="w", label="bifurcation point")
# Set plot appearance
ax.set_xlabel("$\\theta,\,\mathrm{deg}$")
ax.set_ylabel("$P/P_c$")
ax.set_ylim([0, 2])
ax.legend()
ax.grid()
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "CollinearRodsPitchfork.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# ### Broken supercritical pitchfork
# In[3]:
from matplotlib.lines import Line2D # package for custom legend
color_starting_index = 1 # index of first color to use for plotting
imperfections = [1, 5, 10, 15] # [deg]
fig, ax = plt.subplots(figsize=(textwidth_inches*scale, fig_height_inches*scale))
for count, theta_0 in enumerate(imperfections): # iterate through initial angles theta_0
theta_negative = np.arange(-theta_max, 0)
theta_positive = np.arange(theta_0, theta_max + 1)
load_theta_negative = np.deg2rad(theta_negative - theta_0)/np.sin(np.deg2rad(theta_negative))
load_theta_positive = np.deg2rad(theta_positive - theta_0)/np.sin(np.deg2rad(theta_positive))
stability_theta_negative = np.deg2rad(theta_negative - theta_0)/np.tan(np.deg2rad(theta_negative))
ax.plot(theta_negative[stability_theta_negative<1], load_theta_negative[stability_theta_negative<1],
color=colors[color_starting_index + count], label=f"$\\theta_0={theta_0:d}\,\mathrm{{deg}}$, stable")
ax.plot(theta_negative[stability_theta_negative>1], load_theta_negative[stability_theta_negative>1], UNSTABLE_COLOR, linestyle="--")
ax.plot(theta_positive, load_theta_positive, color=colors[color_starting_index + count])
ax.set_xlabel("$\\theta,\,\mathrm{deg}$")
ax.set_ylabel("$P/P_c$")
ax.set_ylim([0, 2])
handles, labels = ax.get_legend_handles_labels()
handles.append(Line2D([0], [0], color=UNSTABLE_COLOR, linestyle="--", label="unstable"))
labels.append("unstable")
ax.legend(handles=handles, labels=labels)
ax.grid()
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "CollinearRodsBrokenPitchfork.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# ### Limit point bifurcation
# In[4]:
alpha_0 = 30 # deg
theta_max = 37 # deg
step = 0.1 # deg
theta = np.arange(-theta_max, theta_max + step, step) # deg
nondimensional_load = 4 * (np.sin(np.deg2rad(theta)) - np.cos(np.deg2rad(alpha_0)) * np.tan(np.deg2rad(theta)))
stability = (np.cos(np.deg2rad(alpha_0)) - np.cos(np.deg2rad(theta))**3) / np.cos(np.deg2rad(theta))
# Create an array for alpha_0 - theta
alpha_minus_theta = alpha_0 - theta
# Split data into segments with the same sign of stability
segments = []
current_segment_indices = []
limit_point_indices = []
for i in range(len(alpha_minus_theta)):
if i == 0 or np.sign(stability[i]) == np.sign(stability[i - 1]):
current_segment_indices.append(i)
else:
segments.append(current_segment_indices)
current_segment_indices = [i]
limit_point_indices.append(i - 1) # the limit point is taken as the last point with same stability for visualization purposes
segments.append(current_segment_indices) # Append the last segment
# Create a figure and axis
fig, ax = plt.subplots(figsize=(textwidth_inches*scale, fig_height_inches*scale))
# Plot line segments separately based on stability sign
for segment in segments:
if stability[segment[0]] >= 0:
ax.plot(alpha_minus_theta[segment], nondimensional_load[segment], color=colors[0])
else:
ax.plot(alpha_minus_theta[segment], nondimensional_load[segment], color=UNSTABLE_COLOR, linestyle="--")
ax.plot(alpha_minus_theta[limit_point_indices], nondimensional_load[limit_point_indices], "o", mec=UNSTABLE_COLOR, mfc="w")
# Label the axes and set limts
plt.xlabel("$\\alpha_0 - \\theta,\,\mathrm{deg}$")
plt.ylabel("$P/kl$")
plt.ylim([-0.2, 0.2])
# Create proxy artists for the legend
stable_line = Line2D([0], [0], color=colors[0], label="stable")
unstable_line = Line2D([0], [0], color=UNSTABLE_COLOR, linestyle="--", label="unstable")
limit_point = Line2D([0], [0], marker="o", mec=UNSTABLE_COLOR, mfc="w", linestyle="None", label="limit point")
# Display the legend with the proxy artists
plt.legend(handles=[stable_line, unstable_line, limit_point], loc="upper right", bbox_to_anchor=(0.85, 1.))
# Show the plot
plt.grid(True)
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "InclinedRodsLimitPoint.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# Plot two limit point bifurcations side by side.
# In[5]:
# Create a figure and two subplots side by side, sharing the y-axis
fig, axs = plt.subplots(1, 2, sharey=True, figsize=(textwidth_inches, fig_height_inches*scale))
# Plot line segments separately based on stability sign in both subplots
for ax in axs:
for segment in segments:
if stability[segment[0]] >= 0:
ax.plot(alpha_minus_theta[segment], nondimensional_load[segment], color=colors[0])
else:
ax.plot(alpha_minus_theta[segment], nondimensional_load[segment], color=UNSTABLE_COLOR, linestyle="--")
ax.plot(alpha_minus_theta[limit_point_indices], nondimensional_load[limit_point_indices], "o", mec=UNSTABLE_COLOR, mfc="w")
# Label the axes and set limits for both subplots
ax.set_xlabel("$\\alpha_0 - \\theta,\,\mathrm{deg}$")
ax.set_ylim([-0.2, 0.2])
ax.grid(True)
# Label the y-axis only for the left subplot
axs[0].set_ylabel("$P/kl$")
# Show the plot
plt.grid(True)
plt.show()
# Save the figure (You might need to define ANALYSIS_DIRECTORY_PATH)
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "LimitPointBifurcationSideBySide.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# ## Nonlinear structural stability of a CRM-like box beam
#
# ***
#
# Geometrical and material properties.
# In[6]:
l = 29.38e3 # [mm] box beam length
w = 3.41e3 # [mm] box beam width
h = 0.77e3 # [mm] box beam height
t = h/100 # [mm] initial box beam thickness
stiffeners_height = h/10 # [mm] stiffeners height
no_stiffeners = 2 # number of stiffeners
stiffeners_x_locations = np.linspace(0, w, no_stiffeners + 2)[1:-1] # [mm] stiffeners x-coordinates
stiffeners_spacing = w/(no_stiffeners + 1) # [mm] stiffeners spacing
ribs_spacing = stiffeners_spacing*1.4 # [mm] ribs spacing
no_ribs = round(l/ribs_spacing) + 1 # number of ribs
ribs_y_locations = np.linspace(0, l, no_ribs) # [mm] ribs y-coordinates
print(f"Number of stiffeners per skin: {no_stiffeners:d}\nNumber of ribs: {no_ribs:d}") # print number of stiffeners and ribs
rho = 2780e-12 # density [tons/mm^3]
E = 73.1e3 # Young's modulus [MPa]
nu = 0.3 # Poisson's ratio
# Plot the external geometry.
# In[7]:
import pyvista
from resources import box_beam_utils
pyvista.rcParams["transparent_background"] = True
# Discretize top and bottom skin
top_skin = pyvista.Plane(center=[w/2, l/2, h/2], direction=[0, 0, 1], i_size=w, j_size=l, i_resolution=1, j_resolution=1)
bottom_skin = pyvista.Plane(center=[w/2, l/2, -h/2], direction=[0, 0, -1], i_size=w, j_size=l, i_resolution=1, j_resolution=1)
# Discretize front and rear spar
front_spar = pyvista.Plane(center=[0, l/2, 0], direction=[-1, 0, 0], i_size=h, j_size=l, i_resolution=1, j_resolution=1)
rear_spar = pyvista.Plane(center=[w, l/2, 0], direction=[1, 0, 0], i_size=h, j_size=l, i_resolution=1, j_resolution=1)
# Discretize root and tip ribs
root_rib = pyvista.Plane(center=[w/2, 0, 0], direction=[0, -1, 0], i_size=w, j_size=h, i_resolution=1, j_resolution=1)
tip_rib = pyvista.Plane(center=[w/2, l, 0], direction=[0, 1, 0], i_size=w, j_size=h, i_resolution=1, j_resolution=1)
# Merge parts together and plot
merged_parts = top_skin.merge([bottom_skin] + [front_spar] + [rear_spar] + [root_rib] + [tip_rib])
pl = pyvista.Plotter(notebook=True, window_size=[4000, 4000])
pl.add_mesh(merged_parts, show_edges=True, line_width=5)
pl.camera.azimuth = 80
pl.show(jupyter_backend="static", screenshot=os.path.join(ANALYSIS_DIRECTORY_PATH, "ExternalGeometry.png"))
# Plot internal geometry.
# In[8]:
# Initialize lists of the PolyData objects corresponding to the box segments and to the ribs
ribs = []
rib_segments_x_coordinates = np.concatenate(([0.], stiffeners_x_locations, [w])) # create array of the x-coordiantes defining the rib segments
rib_segments_widths = np.ediff1d(rib_segments_x_coordinates) # calculate the width of each rib segment
# Iterate through the y-coordinates of the rib, except last one
for y in ribs_y_locations:
# Discretize current rib and add PolyData object to the list
ribs = ribs + [pyvista.Plane(center=[w/2, y, 0], direction=[0, 1, 0], i_size=w, j_size=h, i_resolution=1, j_resolution=1)]
# Initialize lists of the PolyData objects corresponding to the stiffeners
top_stiffeners = []
bottom_stiffeners = []
# Iterate through the x-coordinates of the stiffeners, except last one
for count, x in enumerate(stiffeners_x_locations):
# Discretize top stiffener
top_stiffeners.append(pyvista.Plane(center=[x, l/2, h/2 - stiffeners_height/2], direction=[1, 0, 0], i_size=stiffeners_height, j_size=l,
i_resolution=1, j_resolution=1))
# Discretize bottom stiffener
bottom_stiffeners.append(pyvista.Plane(center=[x, l/2, -h/2 + stiffeners_height/2], direction=[1, 0, 0], i_size=stiffeners_height,
j_size=l, i_resolution=1, j_resolution=1))
# Merge all box segments and ribs together
merged_parts = ribs[0].merge(ribs[1:] + top_stiffeners + bottom_stiffeners)
pl = pyvista.Plotter(notebook=True, window_size=[4000, 4000])
pl.add_mesh(merged_parts, show_edges=True, line_width=5)
pl.camera.azimuth = 80
pl.show(jupyter_backend="static", screenshot=os.path.join(ANALYSIS_DIRECTORY_PATH, "InternalGeometry.png"))
# Plot mesh convergence study results
# In[9]:
from resources import pynastran_utils
def apply_tip_concentrated_load(bdf_input, force_id):
# Add master node at the center of the tip section
master_node_id = len(bdf_input.nodes) + 1
bdf_input.add_grid(master_node_id, [w/2, l, 0.])
# Find id of the nodes on the edge of the tip rib
tolerance = t/10 # we define a geometric tolerance to find the nodes on the edge of the tip rib equal to 1/10 of the cross-sectional thickness
tip_edge_nodes_ids = [nid for nid in bdf_input.nodes if (np.abs(bdf_input.nodes[nid].xyz[1] - l) < tolerance) &
(np.abs((bdf_input.nodes[nid].xyz[0]) < tolerance) | (np.abs(bdf_input.nodes[nid].xyz[0] - w) < tolerance) |
(np.abs(bdf_input.nodes[nid].xyz[2] - h/2) < tolerance) | (np.abs(bdf_input.nodes[nid].xyz[2] + h/2) < tolerance))]
# Add RBE3 to connect master node with edge nodes of tip rib
rbe3_eid = len(bdf_input.elements) + 1
bdf_input.add_rbe3(eid=rbe3_eid, refgrid=master_node_id, refc='123456', weights=[1.]*len(tip_edge_nodes_ids),
comps=['123456']*len(tip_edge_nodes_ids), Gijs=tip_edge_nodes_ids)
# Add concentrated force
force_direction = [0., 0., 1.]
pynastran_utils.add_unitary_force(bdf_object=bdf_input, nodes_ids=[master_node_id], set_id=force_id, direction_vector=force_direction)
# Return id of master node
return master_node_id
force_set_id = 11 # define FORCE card identification number
eigenvalue_calculation_subcase_id = 2 # define subcase id of eigenvalue calculation
# Define shell elements' lengths to be used for the mesh convergence study and print them to screen
shell_element_lengths = np.geomspace(h/2, stiffeners_height/8, 10) # [m]
print("Prescribed length of shell elements for mesh convergence study [mm]:")
print(shell_element_lengths)
# Initialize arrays with number of elements, number of degrees of freedom and linear buckling loads
no_elements = np.empty(np.shape(shell_element_lengths), dtype=int)
dofs = np.empty(np.shape(shell_element_lengths))
linear_buckling_loads = np.empty(np.shape(shell_element_lengths))
# Iterate through the different edge lengths
for count, element_length in enumerate(shell_element_lengths):
# Generate base bdf input
box_beam_mesh = box_beam_utils.mesh_stiffened_box_beam_with_pyvista(width=w, height=h, ribs_y_coordinates=ribs_y_locations,
stiffeners_x_coordinates=stiffeners_x_locations,
stiffeners_height=stiffeners_height, element_length=element_length)
nodes_xyz_array = box_beam_mesh.points
nodes_connectivity_matrix = box_beam_mesh.faces.reshape(-1, 5)[:, 1:]
linear_buckling_bdf = box_beam_utils.create_base_bdf_input(young_modulus=E, poisson_ratio=nu, density=rho, shell_thickness=t,
nodes_xyz_array=nodes_xyz_array,
nodes_connectivity_matrix=nodes_connectivity_matrix)
# Apply concetrated load at the tip
apply_tip_concentrated_load(linear_buckling_bdf, force_set_id)
# Store number of elements and number of degrees of freedom of current model
no_elements[count] = len(linear_buckling_bdf.elements)
constrained_nodes_ids = next(iter(linear_buckling_bdf.spcs.values()))[0].node_ids
dofs[count] = (len(linear_buckling_bdf.nodes) - len(constrained_nodes_ids))*6
# Run SOL 105 and return OP2 object
input_name = f"linear_buckling_{no_elements[count]}_elements"
sol_105_op2 = pynastran_utils.run_sol_105_buckling_analysis(bdf_object=linear_buckling_bdf, static_load_set_id=force_set_id,
analysis_directory_path=ANALYSIS_DIRECTORY_PATH, input_name=input_name,
run_flag=False)
# Find critical buckling load and print it
print(f"\n\033[1mResults of model with:\n- {element_length:.0f} mm prescribed element length\n- {no_elements[count]:.0f} elements\033[0m\n",
f"\033[1m- {dofs[count]:.0f} degrees of freedom\033[0m")
linear_buckling_loads[count] = sol_105_op2.eigenvectors[eigenvalue_calculation_subcase_id].eigr
print(f"Buckling load: {linear_buckling_loads[count]:.0f} N")
# In[10]:
# Plot buckling loads vs degrees of freedom with a log scale along the x-axis
fig, ax1 = plt.subplots(figsize=(textwidth_inches*scale, fig_height_inches*scale)) # create a figure and set of axes
buckling_load_line = ax1.semilogx(dofs, linear_buckling_loads, 'o-')
# Create a twin set of axes to plot percentage difference vs degrees of freedom
ax2 = ax1.twinx()
percentage_diff = (linear_buckling_loads/linear_buckling_loads[-1]-1)*100
percentage_diff_line = ax2.plot(dofs, percentage_diff, "o-")
# Set plot appearance
ax1.set_xlabel("Degrees of freedom")
ax1.set_ylabel("Buckling load, N")
ax2.set_ylabel("Difference w.r.t. finest mesh, \%")
ax1.grid(True)
ax2.grid(True)
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "MeshConvergenceStudy.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# In[11]:
converged_mesh_index = np.where(np.abs(percentage_diff) < 1)[0][0] # find index corresponding to first percentage difference below 1
element_length_converged_mesh = shell_element_lengths[converged_mesh_index] # store shell length of converged mesh
no_elements_converged_mesh = no_elements[converged_mesh_index] # store number of elements of converged mesh
sol_105_buckling_load = linear_buckling_loads[converged_mesh_index] # store buckling load of converged mesh
print(f"The mesh is converged for a target shell element length of {element_length_converged_mesh:.0f} mm, ",
f"corresponidng to {no_elements_converged_mesh} elements, ",
f"{dofs[converged_mesh_index]:.0f} degrees of freedom and to a linear buckling load of {sol_105_buckling_load:.0f} N.")
# Plot buckling mode of converged mesh.
# In[12]:
from pyNastran.op2.op2 import read_op2
# Define function to adjust axes ticks and labels' position
def adjust_3d_plot(axes, colorbar=None):
axes.locator_params(axis="x", nbins=3) # set number of ticks of x-axis
axes.locator_params(axis="z", nbins=2) # set number of ticks of z-axis
axes.tick_params(axis="y", which="major", pad=20) # adjust position of ticks' label of y-axis
axes.tick_params(axis="z", which="major", pad=6) # adjust position of ticks' label of z-axis
axes.yaxis.labelpad = 70 # adjust position of y-axis's label
axes.zaxis.labelpad = 10 # adjust position of z-axis's label
if colorbar is not None:
colorbar.ax.set_position(colorbar.ax.get_position().shrunk(1.0, .66)) # decrease colorbar size
colorbar.ax.set_position(colorbar.ax.get_position().translated(0, .14)) # move colorbar upwards
# Plot buckling mode
plt.rcParams.update({"font.size": default_font_size*1.9}) # increase default font size
amplification_factor = 2 # amplification factor for displacements
input_name = f"linear_buckling_{no_elements_converged_mesh}_elements"
sol_105_op2_filepath = os.path.join(ANALYSIS_DIRECTORY_PATH, input_name + ".op2")
sol_105_op2 = read_op2(op2_filename=sol_105_op2_filepath, load_geometry=True, debug=None)
fig, ax, cbar = pynastran_utils.plot_buckling_mode(op2_object=sol_105_op2, subcase_id=eigenvalue_calculation_subcase_id, displacement_component="tz", colormap="sunset",
length_unit='m', displacement_amplification_factor=amplification_factor, unit_scale_factor=1e-3)
# Plot node where maximum displacement occurs
max_displacement_index = np.argmax(np.linalg.norm(sol_105_op2.eigenvectors[eigenvalue_calculation_subcase_id].data[0, :, 0:3], axis=1)) # find index of max displacement magnitude
max_displacement_node_id = sol_105_op2.eigenvectors[eigenvalue_calculation_subcase_id].node_gridtype[max_displacement_index, 0]
max_displacement_node_xyz = sol_105_op2.nodes[max_displacement_node_id].xyz*1e-3 + sol_105_op2.eigenvectors[
eigenvalue_calculation_subcase_id].data[0, max_displacement_index, 0:3]*amplification_factor # add displacement to node position and convert to m
ax.plot(max_displacement_node_xyz[0], max_displacement_node_xyz[1], max_displacement_node_xyz[2], "x",
label=f"Node {max_displacement_node_id:d} (max displacement)", zorder=4)
ax.legend()
# Adjust plot, show it and save figure
adjust_3d_plot(ax, cbar)
plt.show()
bbox = fig.bbox_inches.from_bounds(.5, .5, 6.5, 4.2) # create bounding box for figure
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "BoxBeamCriticalBucklingMode.png"), format="png", bbox_inches=bbox, pad_inches=0, dpi=500)
# Run nonlinear analysis with $P/P_\text{SOL 105}=2$ and plot load-displacement diagram in terms of local displacement $u_{z,\,287}$ nondimensionalized by the box beam width $w$.
# In[13]:
# Create mesh with converged target element length
box_beam_mesh = box_beam_utils.mesh_stiffened_box_beam_with_pyvista(width=w, height=h, ribs_y_coordinates=ribs_y_locations,
stiffeners_x_coordinates=stiffeners_x_locations,
stiffeners_height=stiffeners_height,
element_length=element_length_converged_mesh)
# Create BDF obejct and apply concentrated load at the tip
box_beam_bdf = box_beam_utils.create_base_bdf_input(E, nu, rho, t, box_beam_mesh.points, box_beam_mesh.faces.reshape(-1, 5)[:, 1:])
tip_master_node_id = apply_tip_concentrated_load(box_beam_bdf, force_set_id)
# Setup SOL 106 with arc-length method using parameters for fine resolution of equilibrium path
fine_nlparm_id = 1 # id of NLPARM card with fine arc-length method parameters
pynastran_utils.set_up_arc_length_method(bdf_object=box_beam_bdf, nlparm_id=fine_nlparm_id, eps_p=1e-3, eps_w=1e-7, ninc=100, max_bisect=10,
minalr=.01, maxalr=1.0001, desiter=5, maxinc=2000)
# Apply load
load_set_id = force_set_id + 1 # id of load set
scale_factor = sol_105_buckling_load*2 # scale factor to apply to the load set
box_beam_bdf.add_load(sid=load_set_id, scale=1., scale_factors=[scale_factor], load_ids=[force_set_id]) # add LOAD card
loading_subcase_id = 1 # id of first subcase
pynastran_utils.create_static_load_subcase(bdf_object=box_beam_bdf, subcase_id=loading_subcase_id, load_set_id=load_set_id) # create subcase with static load
# Run analysis
method_set_id = load_set_id + 1
no_eigenvalues = 20
input_name = f"nonlinear_analysis_{no_elements_converged_mesh}_elements"
sol_106_op2 = pynastran_utils.run_tangent_stiffness_matrix_eigenvalue_calculation(
bdf_object=box_beam_bdf.__deepcopy__({}), method_set_id=method_set_id, no_eigenvalues=no_eigenvalues,
analysis_directory_path=ANALYSIS_DIRECTORY_PATH, input_name=input_name, run_flag=False)
# Read load and displacement history
_, applied_loads, local_displacements = pynastran_utils.read_load_displacement_history_from_op2(op2_object=sol_106_op2,
displacement_node_id=max_displacement_node_id)
z_component_index = 2
nondimensional_applied_loads = applied_loads[loading_subcase_id][:, z_component_index]/sol_105_buckling_load
nondimensional_local_displacements = local_displacements[loading_subcase_id][:, z_component_index]/w
# Read eigenvalues obtained with refence mesh
f06_path = os.path.join(ANALYSIS_DIRECTORY_PATH, input_name + ".f06") # path to .f06 file
eigenvalues = pynastran_utils.read_kllrh_lowest_eigenvalues_from_f06(f06_path)
# Create boolean mask for negative eigenvalues in any column
negative_eigenvalues_mask = (eigenvalues < 0).any(axis=0)
# Define function to plot load-displacement diagram segment by segment
def plot_segments(eigenvalues_mask, axes, disp, loads, marker, color):
unstable_segments = [] # list to store indices of unstable segments
stable_segments = [] # list to store indices of stable segments
# Initialize the start index of the current segment
start_idx = 0
# Loop through the negative_eigenvalues_mask to identify and plot segments
for i, is_negative in enumerate(eigenvalues_mask):
if is_negative:
if stable_segments:
# Plot the stable segment if there was one before
stable_segments.append(i) # make the stable segment finish at the first point of the unstable segment
axes.plot(disp[stable_segments], loads[stable_segments], marker + "-", color=color)
stable_segments = [] # reset the stable segment indices
unstable_segments.append(i) # add the current index to the unstable segment, this will overwrite the blue point with a red one
else:
if unstable_segments:
# Plot the unstable segment if there was one before
unstable_segments.append(i) # make the unstable segment finish at the first point of the stable segment
axes.plot(disp[unstable_segments], loads[unstable_segments], marker + "--", color=UNSTABLE_COLOR)
unstable_segments = [] # reset the unstable segment indices
stable_segments.append(i) # add the current index to the stable segment, this will overwrite the red point with a blue one
# Plot the remaining segments if any
if stable_segments:
axes.plot(disp[stable_segments], loads[stable_segments], marker + "-", color=color)
if unstable_segments:
axes.plot(disp[unstable_segments], loads[unstable_segments], marker + "--", color=UNSTABLE_COLOR)
# Plot load-displacement diagram
plt.rcParams.update({"font.size": default_font_size}) # reset default font size
plt.rcParams["lines.markersize"] = 3 # set default marker size of lines
fig, ax = plt.subplots(figsize=(textwidth_inches*scale, fig_height_inches*scale))
plot_segments(negative_eigenvalues_mask, ax, nondimensional_local_displacements, nondimensional_applied_loads, "o", colors[0])
# Plot glass ceiling of linear buckling
glass_ceiling_handle = ax.axhline(y=1, color=GLASS_CEILING_COLOR, linestyle="-.", label="glass ceiling of linear buckling")
# Create proxy artists for the legend
stable_line = Line2D([0], [0], linestyle="-", marker="o", color=colors[0], label="stable")
unstable_line = Line2D([0], [0], linestyle="--", marker="o", color=UNSTABLE_COLOR, label="unstable")
# Display the legend with the proxy artists
ax.legend(handles=[stable_line, unstable_line, glass_ceiling_handle])
# Set plot appearance
ax.set_xlabel(f"$u_{{z,\,{max_displacement_node_id:d}}}/w$")
ax.set_ylabel("$P/P_\mathrm{SOL\,105}$")
ax.grid()
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "InitialLocalDisplacement.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# Plot deformation over the box beam root before and after the snap.
# In[14]:
# Plot deformation just before limit point
plt.rcParams.update({"font.size": default_font_size*2}) # increase default font size because figures are plotted side by side
first_negative_eigenvalue_index = np.where(negative_eigenvalues_mask)[0][0]
nondimensional_applied_load_before_limit_point = nondimensional_applied_loads[first_negative_eigenvalue_index-1]
print(f"Loss of stability at P/P_SOL/105 = {nondimensional_applied_load_before_limit_point:.2f}")
print("Deformation before snap:")
amplification_factor = 50 # amplification factor for displacements
fig, ax, cbar = pynastran_utils.plot_static_deformation(op2_object=sol_106_op2, subcase_id=loading_subcase_id, load_step=first_negative_eigenvalue_index,
displacement_component="rx", colormap="sunset", length_unit="m", unit_scale_factor=1e-3,
displacement_amplification_factor=amplification_factor) # plot buckling mode converting from mm to m
adjust_3d_plot(ax)
cbar.ax.set_position(cbar.ax.get_position().shrunk(1.0, .82)) # decrease colorbar size
cbar.ax.set_position(cbar.ax.get_position().translated(0, .14)) # move colorbar upwards
ax.set_xlim(0, w*1e-3)
ax.set_ylim(0, 4*h*1e-3)
ax.set_zlim(-h/2*1e-3, h*1e-3)
ax.set_box_aspect([ub - lb for lb, ub in (getattr(ax, f'get_{a}lim')() for a in 'xyz')])
plt.axis("off")
ax.view_init(40, -20)
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "BeforeSnap.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# Find closest converged iteration to applied load after limit point
subsequent_values = nondimensional_applied_loads[first_negative_eigenvalue_index + 10:]
closest_index_in_subsequent = np.argmin(np.abs(subsequent_values - nondimensional_applied_load_before_limit_point))
closest_index = first_negative_eigenvalue_index + closest_index_in_subsequent
print("Deformation after snap:")
# Plot deformation after limit point
fig, ax, cbar = pynastran_utils.plot_static_deformation(op2_object=sol_106_op2, subcase_id=loading_subcase_id, load_step=closest_index + 1,
displacement_component="rx", colormap="sunset", length_unit="m", unit_scale_factor=1e-3,
displacement_amplification_factor=amplification_factor) # plot buckling mode converting from mm to m
adjust_3d_plot(ax)
cbar.ax.set_position(cbar.ax.get_position().shrunk(1.0, .82)) # decrease colorbar size
cbar.ax.set_position(cbar.ax.get_position().translated(0, .14)) # move colorbar upwards
ax.set_xlim(0, w*1e-3)
ax.set_ylim(0, 4*h*1e-3)
ax.set_zlim(-h/2*1e-3, h*1e-3)
ax.set_box_aspect([ub - lb for lb, ub in (getattr(ax, f'get_{a}lim')() for a in 'xyz')])
plt.axis("off")
ax.view_init(40, -20)
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "AfterSnap.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# Plot the eigenvalues of the tangent stiffness matrix.
# In[15]:
from mpl_toolkits.axes_grid1.inset_locator import mark_inset, zoomed_inset_axes
# Create the figure
plt.rcParams.update({"font.size": default_font_size}) # reset default font size
fig, ax = plt.subplots(figsize=(textwidth_inches, fig_height_inches*1.5))
fig.subplots_adjust(top=.59)
# Plot eigenvalues vs applied load
plt.plot(nondimensional_applied_loads, eigenvalues.T*1e3, "o") # convert eigenvalues from N/mm to N/m
plt.ylabel("$\lambda,\,\mathrm{N/m}$")
plt.xlabel("$P/P_\mathrm{SOL\,105}$")
plt.grid(True)
# Define zoom level
zoom_level = 3.5
# Create the first zoomed inset
x1, x2 = .81, 1.19 # region of interest for the first zoomed inset
axins1 = zoomed_inset_axes(ax, zoom_level, loc="upper left", bbox_to_anchor=(-.01, 1.82), bbox_transform=ax.transAxes)
axins1.plot(nondimensional_applied_loads, eigenvalues.T * 1e3, "o")
axins1.set_xlim(x1, x2) # Adjust the limits as needed
axins1.set_ylim(-500, 1500) # Adjust the limits as needed
axins1.set_xticks([0.9, 1.0, 1.1])
mark_inset(ax, axins1, loc1=3, loc2=4, fc="none", ec="0.5", linewidth=2, zorder=2) # Connect to the region of interest
# Create the second zoomed inset
x1, x2 = 1.29, 1.44 # region of interest for the second zoomed inset
axins2 = zoomed_inset_axes(ax, zoom_level, loc="upper right", bbox_to_anchor=(1.01, 1.82), bbox_transform=ax.transAxes)
axins2.plot(nondimensional_applied_loads, eigenvalues.T * 1e3, "o")
axins2.set_xlim(x1, x2) # Adjust the limits based on your region of interest
axins2.set_ylim(-1500, 1500) # Adjust the limits as needed
axins2.tick_params(labelleft=False, labelbottom=True)
mark_inset(ax, axins2, loc1=2, loc2=4, fc="none", ec="0.5", linewidth=2, zorder=2) # Connect to the region of interest
# Set the appearance of the insets
axins1.grid(True)
axins2.grid(True)
# Adjust layout for better appearance
# plt.tight_layout()
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "Eigenvalues.svg"), format="svg", pad_inches=0)
# Plot load-displacement diagram in terms of tip displacement $u_{z,\,\mathrm{tip}}$ nondimensionalized by the box beam length $l$.
# In[16]:
# Read tip displacement history
_, _, tip_displacements = pynastran_utils.read_load_displacement_history_from_op2(op2_object=sol_106_op2, displacement_node_id=tip_master_node_id)
nondimensional_tip_displacements = tip_displacements[loading_subcase_id][:, z_component_index]/l
# Plot load-displacement diagram
fig, ax = plt.subplots(figsize=(textwidth_inches*scale, fig_height_inches*scale))
plot_segments(negative_eigenvalues_mask, ax, nondimensional_tip_displacements, nondimensional_applied_loads, "o", colors[0])
# Plot glass ceiling of linear buckling
glass_ceiling_handle = ax.axhline(y=1, color=GLASS_CEILING_COLOR, linestyle="-.", label="glass ceiling of linear buckling")
# Display the legend with the proxy artists
ax.legend(handles=[stable_line, unstable_line, glass_ceiling_handle])
# Set plot appearance
ax.set_xlabel("$u_{z,\,\mathrm{tip}}/l$")
ax.set_ylabel("$P/P_\mathrm{SOL\,105}$")
ax.grid()
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "InitialTipDisplacement.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# Run nonlinear analysis with coarse arc-length step size and plot 3D load-displacement diagram.
# In[17]:
from matplotlib import ticker # import ticker module to set custom tick labels
markers = list(Line2D.markers.keys())[2:] # list of marker characters
# Define coarse arc-length method parameters
coarse_nlparm_id = 2
pynastran_utils.set_up_arc_length_method(bdf_object=box_beam_bdf, nlparm_id=coarse_nlparm_id, subcase_id=loading_subcase_id)
# Run analysis
sol_106_op2 = {"natural path": sol_106_op2}
analysis_label = "coarse arc-length step size"
input_name = "nonlinear_analysis_coarse_arclength"
sol_106_op2[analysis_label] = pynastran_utils.run_tangent_stiffness_matrix_eigenvalue_calculation(
bdf_object=box_beam_bdf.__deepcopy__({}), method_set_id=method_set_id, no_eigenvalues=no_eigenvalues,
analysis_directory_path=ANALYSIS_DIRECTORY_PATH, input_name=input_name, run_flag=False)
# Read load and displacement history
_, applied_loads, local_displacements = pynastran_utils.read_load_displacement_history_from_op2(op2_object=sol_106_op2[analysis_label],
displacement_node_id=max_displacement_node_id)
_, _, tip_displacements = pynastran_utils.read_load_displacement_history_from_op2(op2_object=sol_106_op2[analysis_label],
displacement_node_id=tip_master_node_id)
nondimensional_applied_loads = {"natural path": nondimensional_applied_loads, analysis_label: applied_loads[loading_subcase_id][:, z_component_index]/sol_105_buckling_load}
nondimensional_local_displacements = {"natural path": nondimensional_local_displacements, analysis_label: local_displacements[loading_subcase_id][:, z_component_index]/w}
nondimensional_tip_displacements = {"natural path": nondimensional_tip_displacements, analysis_label: tip_displacements[loading_subcase_id][:, z_component_index]/l}
# Read eigenvalues obtained with refence mesh
f06_path = os.path.join(ANALYSIS_DIRECTORY_PATH, input_name + ".f06") # path to .f06 file
eigenvalues = pynastran_utils.read_kllrh_lowest_eigenvalues_from_f06(f06_path)
# Create boolean mask for negative eigenvalues in any column
negative_eigenvalues_mask = {"natural path": negative_eigenvalues_mask, analysis_label: (eigenvalues < 0).any(axis=0)}
# Define function to plot 3D load-displacement diagram segment by segment
def plot_3d_segments(eigenvalues_mask, axes, local_disp, tip_disp, loads, marker, color):
unstable_segments = [] # list to store indices of unstable segments
stable_segments = [] # list to store indices of stable segments
# Initialize the start index of the current segment
start_idx = 0
# Loop through the negative_eigenvalues_mask to identify and plot segments
for i, is_negative in enumerate(eigenvalues_mask):
if is_negative:
if stable_segments:
# Plot the stable segment if there was one before
stable_segments.append(i) # make the stable segment finish at the first point of the unstable segment
axes.plot3D(local_disp[stable_segments], tip_disp[stable_segments], loads[stable_segments], marker + "-", color=color)
stable_segments = [] # reset the stable segment indices
unstable_segments.append(i) # add the current index to the unstable segment, this will overwrite the blue point with a red one
else:
if unstable_segments:
# Plot the unstable segment if there was one before
unstable_segments.append(i) # make the unstable segment finish at the first point of the stable segment
axes.plot3D(local_disp[unstable_segments], tip_disp[unstable_segments], loads[unstable_segments], marker + "--", color=UNSTABLE_COLOR)
unstable_segments = [] # reset the unstable segment indices
stable_segments.append(i) # add the current index to the stable segment, this will overwrite the red point with a blue one
# Plot the remaining segments if any
if stable_segments:
axes.plot3D(local_disp[stable_segments], tip_disp[stable_segments], loads[stable_segments], marker + "-", color=color)
if unstable_segments:
axes.plot3D(local_disp[unstable_segments], tip_disp[unstable_segments], loads[unstable_segments], marker + "--", color=UNSTABLE_COLOR)
# Plot load-displacement diagram
plt.rcParams.update({"font.size": default_font_size*1.3}) # increase default font size because 3d plot will be scaled down
fig = plt.figure()
ax_3d = plt.axes(projection="3d")
stable_lines = [] # list to store Line2D objects for stable segments
for count, key in enumerate(nondimensional_local_displacements):
plot_3d_segments(negative_eigenvalues_mask[key], ax_3d, nondimensional_local_displacements[key], nondimensional_tip_displacements[key], nondimensional_applied_loads[key],
markers[count], colors[count])
stable_lines.append(Line2D([0], [0], linestyle="-", marker=markers[count], color=colors[count], label=key)) # create proxy artist for the legend
# Add proxy artist for unstable lines and create legend
unstable_line = Line2D([0], [0], linestyle="--", color=UNSTABLE_COLOR, label="unstable")
fig.legend(handles=stable_lines + [unstable_line], loc="upper center", bbox_to_anchor=(0.5, 0.95))
# Set plot appearance
ax_3d.set_xlabel(f"$u_{{z,\,{max_displacement_node_id:d}}}/w$")
ax_3d.set_ylabel("$u_{z,\,\mathrm{tip}}/l$", labelpad=10)
ax_3d.set_zlabel("$P/P_\mathrm{SOL\,105}$")
ax_3d.locator_params(axis="x", nbins=6) # set number of ticks of x-axis
ax_3d.grid(visible=True)
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "CoarseArcLength.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# Unload structure from last equilibrium point of previous analysis and plot 3D load-displacement diagram.
# In[18]:
# Define second subcase
zero_load_set_id = load_set_id + 1 # id of LOAD card
box_beam_bdf.add_load(zero_load_set_id, scale=1., scale_factors=[0.], load_ids=[force_set_id]) # add LOAD card with zero applied load
unloading_subcase_id = 2 # id of second subcase
pynastran_utils.create_static_load_subcase(bdf_object=box_beam_bdf, subcase_id=unloading_subcase_id, load_set_id=zero_load_set_id) # create subcase with zero applied load
box_beam_bdf.case_control_deck.subcases[unloading_subcase_id].add_integer_type('NLPARM', fine_nlparm_id) # use NLPARM with fine arc-length method parameters in second subcase
# Run analysis
analysis_label = "unloading from coarse arc-length result"
input_name = "equilibrium_path_verification"
sol_106_op2[analysis_label] = pynastran_utils.run_tangent_stiffness_matrix_eigenvalue_calculation(
bdf_object=box_beam_bdf.__deepcopy__({}), method_set_id=method_set_id, no_eigenvalues=no_eigenvalues,
analysis_directory_path=ANALYSIS_DIRECTORY_PATH, input_name=input_name, run_flag=False)
# Read load and displacement history
_, applied_loads, local_displacements = pynastran_utils.read_load_displacement_history_from_op2(op2_object=sol_106_op2[analysis_label],
displacement_node_id=max_displacement_node_id)
_, _, tip_displacements = pynastran_utils.read_load_displacement_history_from_op2(op2_object=sol_106_op2[analysis_label],
displacement_node_id=tip_master_node_id)
nondimensional_applied_loads[analysis_label] = applied_loads[unloading_subcase_id][:, z_component_index]/sol_105_buckling_load
nondimensional_local_displacements[analysis_label] = local_displacements[unloading_subcase_id][:, z_component_index]/w
nondimensional_tip_displacements[analysis_label] = tip_displacements[unloading_subcase_id][:, z_component_index]/l
# Read eigenvalues obtained with refence mesh
f06_path = os.path.join(ANALYSIS_DIRECTORY_PATH, input_name + ".f06") # path to .f06 file
eigenvalues = pynastran_utils.read_kllrh_lowest_eigenvalues_from_f06(f06_path)
# Create boolean mask for negative eigenvalues in any column
negative_eigenvalues_mask[analysis_label] = (eigenvalues[:, -len(nondimensional_applied_loads[analysis_label]):] < 0).any(axis=0)
# Plot load-displacement diagram
fig = plt.figure()
ax_3d = plt.axes(projection="3d")
labels = ["natural path", analysis_label]
stable_lines = [] # list to store Line2D objects for stable segments
for count, key in enumerate(labels):
plot_3d_segments(negative_eigenvalues_mask[key], ax_3d, nondimensional_local_displacements[key], nondimensional_tip_displacements[key], nondimensional_applied_loads[key],
markers[count], colors[count])
stable_lines.append(Line2D([0], [0], linestyle="-", marker=markers[count], color=colors[count], label=key)) # create proxy artist for the legend
# Create legend
fig.legend(handles=stable_lines + [unstable_line], loc="upper center", bbox_to_anchor=(0.5, 0.95))
# Set plot appearance
ax_3d.set_xlabel(f"$u_{{z,\,{max_displacement_node_id:d}}}/w$")
ax_3d.locator_params(axis="x", nbins=6) # set number of ticks of x-axis
ax_3d.set_ylabel("$u_{z,\,\mathrm{tip}}/l$", labelpad=10)
ax_3d.set_zlabel("$P/P_\mathrm{SOL\,105}$")
ax_3d.grid(visible=True)
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "Unloading.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# Repeat analysis with $MAXITER=2$ and plot 3D load-displacement diagram.
# In[19]:
# Change nonlinear analysis parameters of second subcase
box_beam_bdf.nlparms[fine_nlparm_id].max_iter = 2 # set maximum number of iterations to 2
# Run analysis
analysis_label = "$MAXITER=2$"
input_name = "equilibrium_path_verification_maxiter2"
sol_106_op2[analysis_label] = pynastran_utils.run_tangent_stiffness_matrix_eigenvalue_calculation(
bdf_object=box_beam_bdf.__deepcopy__({}), method_set_id=method_set_id, no_eigenvalues=no_eigenvalues,
analysis_directory_path=ANALYSIS_DIRECTORY_PATH, input_name=input_name, run_flag=False)
# Read load and displacement history
_, applied_loads, local_displacements = pynastran_utils.read_load_displacement_history_from_op2(op2_object=sol_106_op2[analysis_label],
displacement_node_id=max_displacement_node_id)
_, _, tip_displacements = pynastran_utils.read_load_displacement_history_from_op2(op2_object=sol_106_op2[analysis_label],
displacement_node_id=tip_master_node_id)
nondimensional_applied_loads[analysis_label] = applied_loads[unloading_subcase_id][:, z_component_index]/sol_105_buckling_load
nondimensional_local_displacements[analysis_label] = local_displacements[unloading_subcase_id][:, z_component_index]/w
nondimensional_tip_displacements[analysis_label] = tip_displacements[unloading_subcase_id][:, z_component_index]/l
# Read eigenvalues obtained with refence mesh
f06_path = os.path.join(ANALYSIS_DIRECTORY_PATH, input_name + ".f06") # path to .f06 file
eigenvalues = pynastran_utils.read_kllrh_lowest_eigenvalues_from_f06(f06_path)
# Create boolean mask for negative eigenvalues in any column
negative_eigenvalues_mask[analysis_label] = (eigenvalues[:, -len(nondimensional_applied_loads[analysis_label]):] < 0).any(axis=0)
# Plot load-displacement diagram
fig = plt.figure()
ax_3d = plt.axes(projection="3d")
labels = ["natural path", analysis_label]
stable_lines = [] # list to store Line2D objects for stable segments
for count, key in enumerate(labels):
plot_3d_segments(negative_eigenvalues_mask[key], ax_3d, nondimensional_local_displacements[key], nondimensional_tip_displacements[key], nondimensional_applied_loads[key],
markers[count], colors[count])
stable_lines.append(Line2D([0], [0], linestyle="-", marker=markers[count], color=colors[count], label=key)) # create proxy artist for the legend
# Create legend
fig.legend(handles=stable_lines + [unstable_line], loc="upper center", bbox_to_anchor=(0.5, 0.95))
# Set plot appearance
ax_3d.set_xlabel(f"$u_{{z,\,{max_displacement_node_id:d}}}/w$")
ax_3d.locator_params(axis="x", nbins=6) # set number of ticks of x-axis
ax_3d.set_ylabel("$u_{z,\,\mathrm{tip}}/l$", labelpad=10)
ax_3d.set_zlabel("$P/P_\mathrm{SOL\,105}$")
ax_3d.grid(visible=True)
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "Maxiter2.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# ## Optimization of the CRM-like box beam with nonlinear structural stability constraints
#
# ***
#
# Optimization results are taken from the notebook titled [_One-variable Optimization of the CRM-like Box Beam_](19_One-variable_Optimization_of_the_CRM-like_Box_Beam.ipynb).
# In[20]:
import openmdao.api as om # make available the most common OpenMDAO classes and functions
from matplotlib.ticker import MaxNLocator # import class to set the number of ticks of an axis
# Instantiate CaseReader object
optimization_results_directory_name = "19_One-variable_Optimization_of_the_CRM-like_Box_Beam"
optimization_results_directory_path = os.path.join(os.getcwd(), "analyses", optimization_results_directory_name) # path to optimization results
cr = om.CaseReader(os.path.join(optimization_results_directory_path, "first_optimization.sql")) # path to cases database
# Get driver cases (do not recurse to system/solver cases) - driver cases represent the snapshots of all the variable values, metadata, and options of the model
driver_cases = cr.get_cases("driver", recurse=False)
# Retrieve the optimization history in terms of objective, design variable and constraints
keys = ["sol_106.mass", "sol_106.t", "sol_106.ks_stability", "sol_106.ks_stress", "sol_106.applied_load"] # keys of the functions to retrieve
histories = {key: np.stack([case[key][0] for case in driver_cases], axis=0) for key in keys} # retrieve histories of the functions
# Print mass percentage variation w.r.t. initial design and final value of thickness and constraints
mass_0 = histories["sol_106.mass"][0]
mass_percentage_variation = (histories["sol_106.mass"][-1]/mass_0 - 1)*100
print(f"""Mass variation: {mass_percentage_variation:.1f} %
Final thickness: {histories["sol_106.t"][-1]:.1f} mm
Final KS value of nonlinear structural stability: {histories["sol_106.ks_stability"][-1]:.2f} N/m
Final KS value for stress: {histories["sol_106.ks_stress"][-1]:.0f} MPa""")
# Plot optimization history.
# In[21]:
# Create figure with five subplots sharing the x-axis
plt.rcParams.update({"font.size": default_font_size}) # reset default font size
fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots(5, 1, sharex=True, figsize=(textwidth_inches, fig_height_inches))
fig.subplots_adjust(left=None, bottom=None, right=None, top=None, wspace=None, hspace=0.5)
# Plot mass history
iterations_array = np.arange(len(histories["sol_106.mass"]))
ax1.plot(iterations_array, histories["sol_106.mass"])
ax1.set(ylabel="$m,\,\mathrm{ton}$")
ax1.grid()
# Plot thickness history
ax2.plot(iterations_array, histories["sol_106.t"])
ax2.set(ylabel="$t,\,\mathrm{mm}$")
ax2.grid()
# Plot nonlinear structural stability KS function history
ax3.plot(iterations_array, histories["sol_106.ks_stability"])
ax3.set(ylabel="$KS_{\lambda},\,\mathrm{N/m}$")
ax3.grid()
# Plot stress KS function history
ax4.plot(iterations_array, histories["sol_106.ks_stress"])
ax4.set(ylabel="$KS_{\sigma},\,\mathrm{MPa}$")
ax4.grid()
# Plot applied load history
ax5.plot(iterations_array, histories["sol_106.applied_load"]/sol_105_buckling_load)
ax5.set(xlabel="Iterations", ylabel="$P_\mathrm{end}/P_\mathrm{design}$")
ax5.xaxis.set_major_locator(MaxNLocator(integer=True))
ax5.grid()
# Show plot
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "OptimizationHistory.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# Plot load-displacement diagram comparing initial and optimized structure.
# In[22]:
# Read linear buckling load of optimized structure
sol_105_op2_filepath = os.path.join(optimization_results_directory_path, "box_beam_opt_sol_105.op2")
sol_105_op2 = read_op2(op2_filename=sol_105_op2_filepath, debug=None)
nondimensional_linear_buckling_load = sol_105_op2.eigenvectors[eigenvalue_calculation_subcase_id].eigr
print(f"Linear buckling load of optimized structure: P_SOL105/P_design={nondimensional_linear_buckling_load:.2f}")
# Define a dictionary for input names
input_name_dict = {"box_beam_sol_106_start": "initial structure", "box_beam_sol_106": "optimized structure"}
# Create one figure with two subplots side by side
fig, axes = plt.subplots(1, 2, sharey=True, figsize=(textwidth_inches, fig_height_inches*scale))
# Loop through input names and perform data processing and plotting
sol_106_op2 = {} # dictionary to store OP2 objects
stable_lines = [] # list to store Line2D objects for stable segments
for count, key in enumerate(input_name_dict.keys()):
sol_106_op2_filepath = os.path.join(optimization_results_directory_path, key + ".op2")
sol_106_op2[key] = read_op2(op2_filename=sol_106_op2_filepath, load_geometry=True, debug=None)
# Read and store applied loads and local displacements
_, applied_loads, local_displacements = pynastran_utils.read_load_displacement_history_from_op2(
op2_object=sol_106_op2[key],
displacement_node_id=max_displacement_node_id
)
nondimensional_applied_loads = applied_loads[loading_subcase_id][:, z_component_index] / sol_105_buckling_load
nondimensional_local_displacements = local_displacements[loading_subcase_id][:, z_component_index] / w
# Read eigenvalues obtained with refence mesh
f06_path = os.path.join(optimization_results_directory_path, key + ".f06") # path to .f06 file
eigenvalues = pynastran_utils.read_kllrh_lowest_eigenvalues_from_f06(f06_path)
# Create boolean mask for negative eigenvalues in any column
negative_eigenvalues_mask = (eigenvalues < 0).any(axis=0)
# Plot on the first subplot
plot_segments(negative_eigenvalues_mask, axes[0], nondimensional_local_displacements, nondimensional_applied_loads, markers[count], colors[count])
stable_lines.append(Line2D([0], [0], linestyle="-", marker=markers[count], color=colors[count], label=input_name_dict[key])) # create proxy artist for the legend
# Read and store tip displacements
_, _, tip_displacements = pynastran_utils.read_load_displacement_history_from_op2(
op2_object=sol_106_op2[key],
displacement_node_id=tip_master_node_id
)
nondimensional_tip_displacements = tip_displacements[loading_subcase_id][:, z_component_index] / l
# Plot on the second subplot
plot_segments(negative_eigenvalues_mask, axes[1], nondimensional_tip_displacements, nondimensional_applied_loads, markers[count], colors[count])
# Plot the line of the glass ceiling of linear buckling for final design on both subplots
for ax in axes:
ax.axhline(y=nondimensional_linear_buckling_load, color=GLASS_CEILING_COLOR, linestyle="-.")
# Create legend
glass_ceiling_line = Line2D([0], [0], linestyle="-.", color=GLASS_CEILING_COLOR, label="glass ceiling of linear buckling (optimized structure)")
fig.legend(handles=stable_lines + [glass_ceiling_line], loc="upper left", bbox_to_anchor=(0.25, 1.16))
# Set plot appearance for the first subplot
axes[0].set_xlabel(f"$u_{{z,\,{max_displacement_node_id:d}}}/w$")
axes[0].set_ylabel("$P/P_\mathrm{design}$")
axes[0].grid(visible=True)
# Set plot appearance for the second subplot
axes[1].set_xlabel("$u_{z,\,\mathrm{tip}}/l$")
axes[1].grid(visible=True)
plt.tight_layout() # Ensures proper spacing between subplots
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, "OptimizedLoadDisplacement.svg"), format="svg", bbox_inches="tight", pad_inches=0)
# Compare deformation at design load of initial and optimized structure.
# In[23]:
plt.rcParams.update({"font.size": default_font_size*2}) # increase default font size because figures are rendered side by side in the paper
for count, key in enumerate(input_name_dict.keys()):
print(f"Deformation of {input_name_dict[key]}:")
fig, ax, cbar = pynastran_utils.plot_static_deformation(op2_object=sol_106_op2[key], subcase_id=loading_subcase_id, displacement_component="rx",
colormap="sunset", length_unit="m", unit_scale_factor=1e-3) # plot deformation converting from mm to m
ax.locator_params(axis="x", nbins=3) # set number of ticks of x-axis
ax.set_zticks([0]) # set ticks of z-axis
ax.tick_params(axis="y", which="major", pad=40) # adjust position of ticks' label of y-axis
ax.tick_params(axis="z", which="major", pad=10) # adjust position of ticks' label of z-axis
ax.xaxis.labelpad = 10 # adjust position of x-axis's label
ax.yaxis.labelpad = 150 # adjust position of y-axis's label
ax.zaxis.labelpad = 20 # adjust position of z-axis's label
cbar.ax.set_position(cbar.ax.get_position().shrunk(1.0, .66)) # decrease colorbar size
cbar.ax.set_position(cbar.ax.get_position().translated(0, .14)) # move colorbar upwards
plt.show()
fig.savefig(os.path.join(ANALYSIS_DIRECTORY_PATH, f"deformation_{key}.png"), format="png", bbox_inches=bbox, pad_inches=0, dpi=500)
# In[ ]: