Created
December 8, 2020 22:34
-
-
Save bbrelje/947ef6ff401a201812fde465518b74ff to your computer and use it in GitHub Desktop.
Validate simple laminate model versus finite element model from Argonne (10.1016/j.ijhydene.2017.08.123)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import numpy as np | |
from matplotlib import pyplot as plt | |
import openmdao.api as om | |
def rotations(theta): | |
# define rotation matrices per http://web.mit.edu/course/3/3.11/www/modules/laminates.pdf | |
s = np.sin(theta) | |
c = np.cos(theta) | |
# stress in fiber axis = A * stress in xy axis | |
A = np.array([[c**2, s**2, 2*s*c],[s**2, c**2, -2*s*c],[-s*c, s*c, c**2-s**2]]) | |
Ainv = np.array([[c**2, s**2, -2*s*c],[s**2, c**2, 2*s*c],[s*c, -s*c, c**2-s**2]]) | |
R = np.diag([1, 1, 2]) | |
Rinv = np.diag([1, 1, 1/2]) | |
return A, Ainv, R, Rinv | |
def analyze_tank_structure(theta_design, t_overall, length, radius, pressure_design=70): | |
""" | |
Computes stresses and failure criterion in a composite cylindrical pressure tank | |
Inputs | |
------ | |
theta_design : float | |
Composite fiber angle (in rad) relative to hoop direction. | |
Angle relative to axial direction is pi/2 - theta_design | |
t_overall : float | |
Thickness (in m) of the overall composite laminate (all plies) | |
length : float | |
Length (in m) of the cylindrical portion of the tank (not including the spherical endcaps) | |
radius : float | |
Radius (in m) of the spherical endcap and cylindrical tank. | |
pressure_design : float | |
Design pressure rating (in MPa). 2.35 safety factor applied. | |
Outputs | |
------- | |
TW_failure : float | |
Vector of Tsai-Wu failure criterion in each ply (vector, 4 plies) | |
weight : float | |
Tank weight (in kg) | |
""" | |
# Material properties for Toray 2510 material system (with T700G UD fibers) | |
# https://www.toraycma.com/files/library/3a5c79558ad487cc.pdf | |
# all MPa | |
Xt = 2172 # tensile strength, fiber direction | |
Yt = 44.3 # tensile strength, transverse dir | |
Xc = 1400 # compressive strength, fiber direction | |
Yc = 283 # compressive strength, transverse dir | |
ShearYield = 154 # shear strength in plane, ultimate | |
E11 = 125000 # tensile modulus, fiber dir | |
E22 = 8410 # tensile modulus, transverse dir | |
G12 = 4230 # shear modulus in plane | |
v12 = 0.3 # poisson's ratio | |
# define constants from Tsai-Wu failure criterion | |
F11 = 1/Xt/Xc | |
F22 = 1/Yt/Yc | |
F12 = -(1/2)*(Xt*Xc*Yt*Yc)**(-1/2) | |
F66 = ShearYield**-2 | |
F1 = 1/Xt - 1/Xc | |
F2 = 1/Yt - 1/Yc | |
F6 = 0.0 | |
rho = 1517 # material density, kg/m3 | |
P = pressure_design * 2.25 # tank pressure, MPa, safety factor 2.35 | |
# define stress state in the cylindrical portion of the tank | |
load_hoop = P*radius # MN / m | |
load_ax = P*radius/2 # MN / m | |
# define a symmetric and balanced laminate with one design angle | |
thetas = [theta_design, -theta_design, -theta_design, theta_design] | |
# Composite analysis notes from: | |
# http://web.mit.edu/course/3/3.11/www/modules/laminates.pdf | |
# theta is defined from the hoop direction (90 degrees from the axial) as illustrated below | |
# | |
#[cylinder wall] | |
#======================= ) | |
# |theta/ ) | |
# | / ) | |
# | / ) [tank end cap] | |
# | / ) | |
# | / ) | |
#======================= ) | |
t_one_layer = t_overall / 4 | |
# define ply interface height (0.0 is midline) | |
z = np.array([-t_overall/2, -t_one_layer, 0.0, | |
t_one_layer, t_overall / 2]) # meters | |
# material property for carbon fiber system in the fiber axis | |
Q11 = E11 ** 2 / (E11 - v12**2 * E22) | |
Q12 = v12 * E11 * E22 / (E11 - v12**2 * E22) | |
Q22 = E11 * E22 / (E11 - v12**2 * E22) | |
Q66 = G12 | |
# ply stiffness matrix in fiber axis | |
D = np.array([[Q11, Q12, 0.],[Q12, Q22, 0.],[0., 0., Q66]]) | |
# ply compliance matrix in fiber axis | |
S = np.array([[1/E11, -v12/E11, 0.],[-v12/E11, 1/E22, 0.],[0.,0.,1/G12]]) | |
# these are inverses of each other | |
# assemble the ABD matrices of the laminate stack | |
Amat = np.zeros((3,3)) | |
Bmat = np.zeros((3,3)) | |
Dmat = np.zeros((3,3)) | |
for i in range(len(thetas)): | |
theta_this_layer = thetas[i] | |
A, Ainv, R, Rinv = rotations(theta_this_layer) | |
# strain in xy axis = Sbar * stress in xy | |
# Sbar = R @ Ainv @ Rinv @ S @ A | |
# stress in xy axis = Dbar * strain in xy | |
Dbar = Ainv @ D @ R @ A @ Rinv | |
Amat += Dbar * (z[i+1]-z[i]) | |
# these are zero in this balanced symm layups | |
Bmat += 0.5 * Dbar * (z[i+1]**2-z[i]**2) | |
# don't care about the moments for this problem | |
Dmat += (1/3) * Dbar * (z[i+1]**3-z[i]**3) | |
if np.sum(np.abs(Bmat)) > 1e-12: | |
raise ValueError('B matrix is nonzero - something is wrong in laminate calculations') | |
# N = Nx, Ny, Nxy | |
# define the load state on the laminate in terms of hoop load (force / meter) and axial load | |
N = np.array([load_hoop, load_ax, 0.0]) | |
# get the whole-laminate strain state | |
strain_xy = np.linalg.inv(Amat) @ N | |
# get stress and compute Tsai-Wu failure criterion in one layer (they'll all be identical) | |
TW_failure = np.zeros(4) | |
for i in range(4): | |
theta_this_layer = thetas[i] | |
A, Ainv, R, Rinv = rotations(theta_this_layer) | |
stress_mat_axes = D @ R @ A @ Rinv @ strain_xy | |
s_1 = stress_mat_axes[0] | |
s_2 = stress_mat_axes[1] | |
t_12 = stress_mat_axes[2] | |
TW = (F1 * s_1 + | |
F2 * s_2 + | |
F11 * s_1 * s_1 + | |
F22 * s_2 * s_2 + | |
2 * F12 * s_1 * s_2 + | |
F66 * t_12**2) | |
TW_failure[i] = TW | |
# these should all be uniform unless something is wrong | |
if np.sum(np.abs(TW_failure-np.mean(TW_failure))) > 1e-10: | |
raise ValueError('All the failure criteria in each ply should be identical for this layup') | |
# compute tank weight | |
surface_area = np.pi * (length * 2 * radius + 4 * radius ** 2) | |
weight = surface_area * t_overall * rho | |
return TW_failure, weight | |
class TankComp(om.ExplicitComponent): | |
""" | |
A thin wrapper of the analyze_tank_structure method | |
Finite difference derivatives because the model is cheap | |
""" | |
def setup(self): | |
self.add_input('theta_design', val=20.0, units='deg') | |
self.add_input('thickness', val=0.04, units='m') | |
self.add_input('length', val=1.0, units='m') | |
self.add_input('radius', val=0.25, units='m') | |
self.add_input('design_pressure', val=70.0, units='MPa') | |
self.add_input('hydrogen_density', val=42.0, units='kg/m**3') | |
self.add_output('tsai_wu_failure', val=np.ones((4,))) | |
self.add_output('tank_weight', val=10., units='kg') | |
self.add_output('vol_wt_ratio', val=1.0, units='m**3/kg') | |
self.add_output('volume', val=1.0, units='m**3') | |
self.add_output('grav_pct', val=0.05) | |
self.add_output('hydrogen_weight', val=1., units='kg') | |
self.add_output('loverd', val=1., units=None) | |
self.declare_partials(['*'],['*'], method='fd',step=1e-6) | |
def compute(self, inputs, outputs): | |
# do the structural analysis | |
tws, tank_weight = analyze_tank_structure(inputs['theta_design'][0]*np.pi/180, | |
inputs['thickness'][0], | |
inputs['length'][0], | |
inputs['radius'][0], | |
inputs['design_pressure'][0] | |
) | |
outputs['tsai_wu_failure'] = tws | |
outputs['tank_weight'] = tank_weight | |
# compute some auxiliary numbers | |
# tank volume | |
volume = np.pi * (inputs['radius']**2 * inputs['length'] + 4/3*inputs['radius']**3) | |
outputs['volume'] = volume | |
# volume to weight ratio | |
outputs['vol_wt_ratio'] = volume / tank_weight | |
# fuel weight in the tank at design pressure | |
H2_wt = volume * inputs['hydrogen_density'][0] | |
outputs['hydrogen_weight'] = H2_wt | |
# gravimetric percentage (fuel / tank weight) | |
outputs['grav_pct'] = H2_wt / tank_weight | |
outputs['loverd'] = (inputs['length'] + 2 * inputs['radius'])/2/inputs['radius'] | |
if __name__ == "__main__": | |
# The purpose of this model is to find the optimal | |
# design of a composite wrapped Hydrogen tank | |
# We are only considering the tank structure for simplicity | |
# (not liner or fittings, which are significant) | |
prob = om.Problem() | |
# the design pressure is an important parameter | |
ivc = om.IndepVarComp() | |
ivc.add_output('design_pressure', val=70., units='MPa') | |
prob.model.add_subsystem('iv', ivc) | |
# compute hydrogen density at 298K using a curve fit | |
# data from https://h2tools.org/hyarc/hydrogen-data/hydrogen-density-different-temperatures-and-pressures | |
h2density = om.MetaModelStructuredComp() | |
h2density.add_input('pressure', | |
training_data=np.array([0.1, 1., 5., 10., 30., 50., 70., 100.]), | |
units='MPa') | |
h2density.add_output('density', | |
training_data=np.array([0.0813, 0.8085, 3.9490, 7.6711, 20.537, 30.811, 42.0, 49.424]), | |
units='kg/m**3') | |
prob.model.add_subsystem('h2density', h2density) | |
# do the structural and weight analysis | |
prob.model.add_subsystem('tank', TankComp()) | |
prob.model.connect('iv.design_pressure', ['tank.design_pressure','h2density.pressure']) | |
prob.model.connect('h2density.density', 'tank.hydrogen_density') | |
# setup the optimization | |
prob.driver = om.ScipyOptimizeDriver() | |
prob.driver.options['optimizer'] = 'SLSQP' | |
# the tank fiber orientation angle in degrees | |
prob.model.add_design_var('tank.theta_design', lower=0.0, upper=75., ref=40.) | |
# tank wall thickness in m | |
# this must be minimum-gauged for crash and projectile resistance hence 1cm lower bound | |
prob.model.add_design_var('tank.thickness', lower=0.01, upper=0.1, ref=0.02) | |
# tank length | |
prob.model.add_design_var('tank.length', lower=0.2, upper=2.0, ref=1.0) | |
# radius of the cylinder in m | |
prob.model.add_design_var('tank.radius', lower=0.02, upper=20.0, ref=0.2) | |
# tank design pressure in MPa (multiply by 10 to get bar) | |
prob.model.add_design_var('iv.design_pressure', lower=70., upper=70., ref=10.) | |
# minimize the weight of the tank subject to structural sizing and fuel wt requirement | |
prob.model.add_objective('tank.tank_weight', scaler=0.05) | |
prob.model.add_constraint('tank.tsai_wu_failure', upper=np.ones((4,))) | |
prob.model.add_constraint('tank.hydrogen_weight', lower=5.8) | |
prob.model.add_constraint('tank.loverd', upper=3.0) | |
prob.setup() | |
prob.run_driver() | |
print('Pressure ', prob['iv.design_pressure']) | |
print('Theta ', prob['tank.theta_design']) | |
print('Thickness ', 1000*prob['tank.thickness'], ' mm') | |
print('Length ', prob['tank.length']) | |
print('Radius ', prob['tank.radius']) | |
print('Tank Weight ', prob['tank.tank_weight']) | |
print('Grav pct ', prob['tank.grav_pct']) | |
print('Failure crit ', prob['tank.tsai_wu_failure']) | |
print('Tank Volume', prob['tank.volume']) | |
prob.model.list_outputs(units=True) | |
prob.model.list_inputs(units=True) | |
# The obtained tank structure weight, 124.45 kg, is within 15% of the published value, 106 kg, | |
# from the Argonne paper 10.1016/j.ijhydene.2017.08.123. | |
# using essentially the same material system under the same conditions | |
# Including the valves, regulators, and fittings, the published figure for total empty tank wt (127.8) | |
# is only 2.7% higher. Variation may be attributable to thinning in the dome (due to smaller loads) | |
# the shorter dome section, and higher-fidelity analysis. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment