feiyang-cai's picture
Initial ZeroGPU Gradio Space
01a8278
Raw
History Blame Contribute Delete
33.7 kB
# --------------------- python packages ---------------------
# all these packages should be present in the system
import numpy as np
import matplotlib.pyplot as plt
from dataclasses import dataclass
from pathlib import Path
import warnings
from numpy.lib._iotools import ConversionWarning
from itertools import combinations_with_replacement
from math import comb
# --------------------- dataset parameters ---------------------
##MATERIAL_TYPES = ["CPP", "GPP", "CHDPE", "GHDPE"] # e.g. ["CPP", "GPP", "CHDPE", "GHDPE"]
#MATERIAL_TYPES = ["CPP", "GPP", "CHDPE", "GHDPE"]
#VOL_FRACTIONS = ["0.0924", "0.2155", "0.3079", "0.4002", "0.4926"] # e.g. ["0.0924", "0.2155", "0.3079", "0.4002", "0.4926"]
#
#
## Number of plies in the *upper* half of the symmetric laminate
#UPPER_LAYER_COUNTS = [1, 2, 4] # example: [1, 2, 3]
#
## Candidate angles (in degrees) each upper-half ply can take
#CANDIDATE_ANGLES = [0.0, 15, 30, 45, 60, 75, 90]
#MATERIAL_TYPES = ["CPP", "GPP", "CHDPE", "GHDPE"] # e.g. ["CPP", "GPP", "CHDPE", "GHDPE"]
MATERIAL_TYPES = [ "CPP",]
VOL_FRACTIONS = [ "0.2000",] # e.g. ["0.0924", "0.2155", "0.3079", "0.4002", "0.4926"]
# Number of plies in the *upper* half of the symmetric laminate
UPPER_LAYER_COUNTS = [9, ] # example: [1, 2, 3]
# Candidate angles (in degrees) each upper-half ply can take
#CANDIDATE_ANGLES = [0.0, 39, 59,73,90]
# Exact full stacking sequence for test runs
#FIXED_STACK_ANGLES = [11, -11, -11, 11]
#FIXED_STACK_ANGLES = [-85,85,-85,85,-90, 90, 90, -90,85,-85,85,-85]
FIXED_STACK_ANGLES = [-8,8,8,-8]
#FIXED_STACK_ANGLES = [-52, 52, -45,45,45,-45,52, -52]
# --------------------- input directory ---------------------
# Folder that contains RVE input files(3*100)(!!!update the path accordingly)
CURVE_DIR = Path(r"C:\Users\shahria\OneDrive - Clemson University\PhD\AIM Project\Post Meeting\Unstability_in_e33\RVE_datasets")
# --------------------- output directory ---------------------
# Folder that contains the laminate output files(!!!update the path accordingly)
OUT_DIR_ALL = Path(r"C:\Users\shahria\OneDrive - Clemson University\PhD\AIM Project\Post Meeting\Unstability_in_e33\Output_directory")
# --------------------- user-set Poisson ratios ---------------------
NU12 = 0.36
NU13 = 0.36
NU23 = 0.86
COND_MAX = 1e12 #maximum allowable condition number of the stiffness matrix
# ===============================================================
@dataclass
class Curve1D:
eps: np.ndarray
sig: np.ndarray # Pa
def __post_init__(self):
idx = np.argsort(self.eps)
self.eps = np.asarray(self.eps, float)[idx]
self.sig = np.asarray(self.sig, float)[idx]
def stress(self, e):
ea = np.clip(abs(e), self.eps[0], self.eps[-1])
sa = np.interp(ea, self.eps, self.sig)
return np.sign(e) * sa
def tangent(self, e):
ea = np.clip(abs(e), self.eps[0], self.eps[-1])
i = np.searchsorted(self.eps, ea) - 1
i = np.clip(i, 0, len(self.eps)-2)
de = self.eps[i+1] - self.eps[i]
ds = self.sig[i+1] - self.sig[i]
return (ds/de)
def smallest_positive_interval_slope(curve: Curve1D) -> float:
slopes = np.diff(curve.sig) / np.diff(curve.eps)
pos = slopes[slopes > 0.0]
if len(pos) == 0:
raise ValueError("No positive interval slopes found in the compression curve.")
return float(np.min(pos))
class BiCurve1D:
"""
Branch-aware 1D constitutive law:
- tension_curve -> used when e >= 0
- compression_curve -> used when e < 0
Both curves should be stored with positive strain magnitude
and positive stress magnitude.
"""
def __init__(self, tension_curve: Curve1D, compression_curve: Curve1D):
self.tension_curve = tension_curve
self.compression_curve = compression_curve
def stress(self, e):
if e >= 0.0:
return self.tension_curve.stress(e)
else:
# return signed compressive stress
return -self.compression_curve.stress(abs(e))
def tangent(self, e):
if e >= 0.0:
return self.tension_curve.tangent(e)
else:
# tangent remains positive if compression curve is stored
# as positive magnitude stress vs positive magnitude strain
return self.compression_curve.tangent(abs(e))
def read_dataset_metadata(prefix: str):
meta_file = CURVE_DIR / f"{prefix}_11.txt"
vol_frac = None
centers_str = None
with open(meta_file, "r") as f:
for line in f:
line = line.strip()
if line.startswith("volume fraction="):
vol_frac = float(line.split("=", 1)[1])
elif line.startswith("fiber_centers_YZ="):
centers_str = line.split("=", 1)[1].strip()
if vol_frac is None:
raise ValueError(f"volume fraction not found in {meta_file}")
if centers_str is None:
n_fibers = None
else:
n_fibers = centers_str.count("(")
return vol_frac, centers_str, n_fibers
def load_curve_from_file(filename, strain_col=1, stress_col=2, skiprows=1, is_shear=False):
"""
Load a 2-col curve file.
If is_shear=True, strain column is assumed 'tensorial' and converted to engineering γ by *2.0*.
"""
p = Path(filename)
if not p.exists():
raise FileNotFoundError(f"Missing curve file: {p.resolve()}")
with warnings.catch_warnings():
warnings.simplefilter("ignore", ConversionWarning)
arr = np.genfromtxt(
p,
dtype=float,
delimiter=None,
skip_header=skiprows,
invalid_raise=False # bad lines (like "volume fraction= ...") are skipped
)
eps = arr[:, strain_col]
if is_shear:
eps = 2.0 * eps # tensorial -> engineering γ
sig = 1e6 * arr[:, stress_col] # MPa -> Pa
return Curve1D(eps, sig)
def load_ud_material_from_files(prefix: str) -> "PlyMaterial":
"""
Build a PlyMaterial from three curve files with a common prefix, e.g.
prefix='CPP_0.1000' -> CPP_0.1000_11.txt, CPP_0.1000_22.txt, CPP_0.1000_12.txt
"""
# 11 curve: Strain_11, Stress_11, Strain_22, Strain_33
c11 = load_curve_from_file(
CURVE_DIR / f"{prefix}_11.txt",
strain_col=0, # Strain_11
stress_col=1, # Stress_11
skiprows=1
)
# 22 curve: Strain_22, Stress_22, Strain_11, Strain_33
# 22 tension curve
c22_t = load_curve_from_file(
CURVE_DIR / f"{prefix}_22.txt",
strain_col=0,
stress_col=1,
skiprows=1
)
# 22 compression curve
c22_c = load_curve_from_signed_compression_file(
CURVE_DIR / f"{prefix}_22c.txt",
strain_col=0,
stress_col=1,
skiprows=1
)
# branch-aware 22 law
c22 = BiCurve1D(c22_t, c22_c)
# reuse same branch-aware transverse law for 33
c33 = c22
E2_comp_min_pos = smallest_positive_interval_slope(c22_c)
# 12 shear curve: Strain_12, Stress_12
c12 = load_curve_from_file(
CURVE_DIR / f"{prefix}_12.txt",
strain_col=0, # Strain_12
stress_col=1, # Stress_12
skiprows=1,
is_shear=True
)
# Reuse 12 curve for 13 and 23
c13 = c12
c23 = c12
return PlyMaterial(
E1_curve=c11, E2_curve=c22, E3_curve=c33,
G12_curve=c12, G13_curve=c13, G23_curve=c23,
nu12=NU12, nu13=NU13, nu23=NU23,
E2_comp_min_pos=E2_comp_min_pos
)
def load_curve_from_signed_compression_file(filename, strain_col=0, stress_col=1, skiprows=1):
p = Path(filename)
if not p.exists():
raise FileNotFoundError(f"Missing curve file: {p.resolve()}")
with warnings.catch_warnings():
warnings.simplefilter("ignore", ConversionWarning)
arr = np.genfromtxt(
p,
dtype=float,
delimiter=None,
skip_header=skiprows,
invalid_raise=False
)
eps = np.abs(arr[:, strain_col])
sig = 1e6 * np.abs(arr[:, stress_col])
return Curve1D(eps, sig)
# ===============================================================
# 3D orthotropic C' (Voigt 11,22,33,23,13,12)
# (engineering shear convention: C66=G12, etc.)
# ===============================================================
def orthotropic_C_prime(E1,E2,E3,G12,G13,G23, nu12,nu13,nu23):
V = (1.0
- (E3/E2)*(nu23**2)
- (E3/E1)*(nu13**2)
- (E2/E1)*(nu12**2)
- 2*(E3/E1)*(nu12*nu13*nu23))
C11 = ((1.0 - (E3/E2)*nu23**2) * E1) / V
C22 = ((1.0 - (E3/E1)*nu13**2) * E2) / V
C33 = ((1.0 - (E2/E1)*nu12**2) * E3) / V
C12 = ((nu12 + (E3/E2)*nu13*nu23) * E2) / V
C13 = ((nu13 + nu12*nu23) * E3) / V
C23 = ((nu23 + (E2/E1)*nu12*nu13) * E3) / V
C = np.zeros((6,6))
C[0,0] = C11; C[1,1] = C22; C[2,2] = C33
C[0,1] = C12; C[1,0] = C12
C[0,2] = C13; C[2,0] = C13
C[1,2] = C23; C[2,1] = C23
C[3,3] = G23; C[4,4] = G13; C[5,5] = G12
return C
# ===============================================================
# Transformations (engineering shear)
# ===============================================================
def T_sigma(theta_deg):
th = np.radians(theta_deg); m, n = np.cos(th), np.sin(th)
return np.array([
[ m*m, n*n, 0, 0, 0, 2*m*n],
[ n*n, m*m, 0, 0, 0, -2*m*n],
[ 0, 0, 1, 0, 0, 0],
[ 0, 0, 0, m,-n, 0],
[ 0, 0, 0, n, m, 0],
[-m*n, m*n, 0, 0, 0, m*m-n*n]
], float)
def T_eps(theta_deg):
th = np.radians(theta_deg); m, n = np.cos(th), np.sin(th)
return np.array([
[ m*m, n*n, 0, 0, 0, m*n],
[ n*n, m*m, 0, 0, 0, -m*n],
[ 0, 0, 1, 0, 0, 0],
[ 0, 0, 0, m,-n, 0],
[ 0, 0, 0, n, m, 0],
[-2*m*n,2*m*n,0, 0, 0, m*m-n*n]
], float)
def T(theta_deg):
th = np.radians(theta_deg); m, n = np.cos(th), np.sin(th)
return np.array([
[ m*m, n*n, 0, 0, 0, 2*m*n],
[ n*n, m*m, 0, 0, 0, -2*m*n],
[ 0, 0, 1, 0, 0, 0],
[ 0, 0, 0, m,-n, 0],
[ 0, 0, 0, n, m, 0],
[-1*m*n,1*m*n,0, 0, 0, m*m-n*n]
], float)
# ===============================================================
# Ply material & ply
# ===============================================================
@dataclass
class PlyMaterial:
E1_curve: Curve1D
E2_curve: Curve1D
E3_curve: Curve1D
G12_curve: Curve1D
G13_curve: Curve1D
G23_curve: Curve1D
nu12: float
nu13: float
nu23: float
E2_comp_min_pos: float
def tangents_from_local_strain(self, e_local):
e11, e22, e33, g23, g13, g12 = e_local
floor = -1e12
E1 = max(self.E1_curve.tangent(e11), floor)
E2_raw = self.E2_curve.tangent(e22)
if e22 < 0.0:
E2 = max(E2_raw, self.E2_comp_min_pos)
else:
E2 = max(E2_raw, floor)
E3 = max(self.E3_curve.tangent(e33), floor)
G12 = max(self.G12_curve.tangent(g12), floor)
G13 = max(self.G13_curve.tangent(g13), floor)
G23 = max(self.G23_curve.tangent(g23), floor)
return E1, E2, E3, G12, G13, G23
#@dataclass
#class Ply:
# theta_deg: float # initial/reference fibre angle
# thickness: float
# mat: PlyMaterial
# e_prev: np.ndarray = None
# s_prev: np.ndarray = None
# theta_curr_deg: float = None # running (current) fibre angle
#
# def init_state(self):
# self.e_prev = np.zeros(6)
# self.s_prev = np.zeros(6)
# self.theta_curr_deg = float(self.theta_deg)
@dataclass
class Ply:
theta_deg: float
thickness: float
mat: PlyMaterial
e_prev: np.ndarray = None
s_prev: np.ndarray = None
theta_curr_deg: float = None
Cglob_prev: np.ndarray = None # <-- NEW: stored 6x6 global tangent from last increment
def init_state(self):
self.e_prev = np.zeros(6)
self.s_prev = np.zeros(6)
self.theta_curr_deg = float(self.theta_deg)
# initialize stored ply tangent stiffness at the initial strain state (zero)
theta = self.theta_curr_deg
T_e = T_eps(theta)
e_local = T_e @ self.e_prev # zeros initially
E1, E2, E3, G12, G13, G23 = self.mat.tangents_from_local_strain(e_local)
Cprime = orthotropic_C_prime(E1, E2, E3, G12, G13, G23,
self.mat.nu12, self.mat.nu13, self.mat.nu23)
Tt = T(theta)
self.Cglob_prev = np.linalg.inv(Tt) @ Cprime @ np.linalg.inv(Tt).T
class Laminate:
def __init__(self, plies, tol=1e-12):
self.plies = plies
for p in self.plies:
p.init_state()
t0 = self.plies[0].thickness
for i,p in enumerate(self.plies, 1):
if abs(p.thickness - t0) > tol:
raise ValueError(f"Equal-thickness assumption violated at ply {i}: {p.thickness} vs {t0}")
self.N = len(self.plies)
self.t = t0
self.total_t = self.N * self.t
# ---------- Incremental affine update using strain increments (engineering shear) ----------
def update_fiber_angles_incremental(self, d_ex, d_ey, d_gxy):
"""
a_{n+1} = ΔF a_n / ||ΔF a_n|| with ΔF = [[1+Δex, Δgxy/2],[Δgxy/2, 1+Δey]]
Updates each ply's theta_curr_deg in-place.
"""
Fd = np.array([[1.0 + d_ex, 0.5*d_gxy],
[0.5*d_gxy, 1.0 + d_ey]], dtype=float) #valid for small strain increments
for p in self.plies:
th = np.radians(p.theta_curr_deg)
a = np.array([np.cos(th), np.sin(th)])
a_new = Fd @ a
nrm = np.linalg.norm(a_new)
if nrm > 1e-14:
a_new /= nrm
p.theta_curr_deg = np.degrees(np.arctan2(a_new[1], a_new[0]))
# ---------- Build effective laminate C from PREVIOUS global strains ----------
#def effective_C_from_previous_strains(self, ex_prev, ey_prev, ezz_prev, g23_prev, g13_prev, gxy_prev):
# """
# Thickness-average of per-ply global tangential stiffness matrices,
# each built from tangents evaluated at the previous-step local strains.
# """
# e_prev_global = np.array([ex_prev, ey_prev, ezz_prev, g23_prev, g13_prev, gxy_prev], float)
# Csum = np.zeros((6,6), float)
# for p in self.plies:
# theta = p.theta_curr_deg
# T_e = T_eps(theta)
# T_e_inv = np.linalg.inv(T_e)
# T_s = T_sigma(theta)
#
# e_local_prev = T_e @ e_prev_global
#
#
# #print(f"theta={theta}")
#
# E1,E2,E3,G12,G13,G23 = p.mat.tangents_from_local_strain(e_local_prev)
# Cprime = orthotropic_C_prime(E1,E2,E3,G12,G13,G23, p.mat.nu12, p.mat.nu13, p.mat.nu23)
#
#
#
# Tt=T(theta)
# Cglob = np.linalg.inv(Tt) @ Cprime @ np.linalg.inv(Tt).T
#
# Csum += Cglob
# return Csum / self.N
def effective_C_from_previous_strains(self, ex_prev, ey_prev, ezz_prev, g23_prev, g13_prev, gxy_prev):
"""
Thickness-average of per-ply global tangential stiffness matrices,
each built from tangents evaluated at the previous-step local strains.
"""
u = [0, 1, 5] # (xx, yy, xy)
w = [2, 3, 4] # (zz, yz, xz) <-- reduced Eq.(11) solves these per ply
eps_u = np.array([ex_prev, ey_prev, gxy_prev], dtype=float)
I = [0, 1, 2, 5] # Voigt indices for 1,2,3,6 in your code ordering
z = 2
Vk_list = []
Ck_list = [] # this will hold the STORED Cglob_prev = C^k_(n)
Ck_next_list = [] # this will hold UPDATED tangents C^k_(n+1) to store back
for p in self.plies:
theta = p.theta_curr_deg
# 1) Use stored ply tangent from previous increment
Cprev = p.Cglob_prev
if Cprev is None:
raise RuntimeError("Ply.Cglob_prev is not initialized. Check Ply.init_state().")
Ck_list.append(Cprev)
Vk_list.append(1.0 / self.N)
# 2) reduced Eq.(11) with sigma_w = 0:
# eps_w^k = -(Cww^k)^(-1) * Cwu^k * eps_u
Cww = Cprev[np.ix_(w, w)] # 3x3
Cwu = Cprev[np.ix_(w, u)] # 3x3
eps_w = -np.linalg.solve(Cww, Cwu @ eps_u)
# 3) Construct the ply global strain state at "prev" step
e_global_k = np.array([ex_prev, ey_prev, eps_w[0], eps_w[1], eps_w[2], gxy_prev], dtype=float)
# 4) Evaluate ply tangents at that ply strain state -> build C^k_(n+1)
T_e = T_eps(theta)
e_local_k = T_e @ e_global_k
E1, E2, E3, G12, G13, G23 = p.mat.tangents_from_local_strain(e_local_k)
Cprime = orthotropic_C_prime(E1, E2, E3, G12, G13, G23,
p.mat.nu12, p.mat.nu13, p.mat.nu23)
Tt = T(theta)
Cglob_next = np.linalg.inv(Tt) @ Cprime @ np.linalg.inv(Tt).T
Ck_next_list.append(Cglob_next)
Vk = np.asarray(Vk_list, float)
# denom = sum_l V_l * (1/C33^l)
den = 0.0
# num[j] = sum_l V_l * (C3j^l / C33^l) for j in I
num = np.zeros(len(I), float)
for Ck, vk in zip(Ck_list, Vk):
C33 = Ck[z, z]
if abs(C33) < 1e-30:
raise ZeroDivisionError("Eq.(7) needs C33^k != 0; found near-zero Ck[2,2].")
den += vk * (1.0 / C33)
num += vk * (Ck[z, I] / C33) # Ck[z, I] is a length-4 row slice
if abs(den) < 1e-30:
raise ZeroDivisionError("Eq.(7) denominator sum_l V_l*(1/C33^l) is near zero.")
R = num / den # length-4 vector containing R_j for j in I
# ---------- PASS 2: assemble C*_{ij} for i,j in I using Eq.(7) ----------
Cstar = np.zeros((6, 6), float)
for Ck, vk in zip(Ck_list, Vk):
C33 = Ck[z, z]
CIJ = Ck[np.ix_(I, I)] # 4x4: C^k_{ij}
CI3 = Ck[np.ix_(I, [z])] # 4x1: C^k_{i3}
C3J = Ck[np.ix_([z], I)] # 1x4: C^k_{3j}
# First two terms: C^k_ij - (C^k_i3 C^k_3j)/C^k_33
term12 = CIJ - (CI3 @ C3J) / C33
# Third term: (C^k_i3 / C^k_33) * R_j
term3 = (CI3 / C33) @ R.reshape(1, -1) # (4x1)@(1x4) -> 4x4
Cstar[np.ix_(I, I)] += vk * (term12 + term3)
# ---------- Eq.(8): mixed (I,J) and (J,I) couplings are zero ----------
J = [3, 4] # Voigt indices for paper's (4,5) = (yz, xz)
Cstar[np.ix_(I, J)] = 0.0
Cstar[np.ix_(J, I)] = 0.0
# ---------- Eq.(9) and Eq.(10): assemble C*_{ij} for i,j in J ----------
# D_k = C44^k C55^k - C45^k C54^k
Dk_list = []
for Ck in Ck_list:
C44 = Ck[3, 3]
C45 = Ck[3, 4]
C54 = Ck[4, 3]
C55 = Ck[4, 4]
Dk = C44 * C55 - C45 * C54
if abs(Dk) < 1e-30:
raise ZeroDivisionError(
"Eq.(10) gave near-zero D_k = C44*C55 - C45*C54."
)
Dk_list.append(Dk)
# numerator matrix in Eq.(9): sum_k (V_k / D_k) * C^k_{JJ}
numJJ = np.zeros((2, 2), float)
for Ck, vk, Dk in zip(Ck_list, Vk, Dk_list):
numJJ += (vk / Dk) * Ck[np.ix_(J, J)]
# denominator scalar in Eq.(9):
# sum_k sum_l (V_k V_l / (D_k D_l)) * (C44^k C55^l - C45^k C54^l)
denJJ = 0.0
for Ck, vk, Dk in zip(Ck_list, Vk, Dk_list):
C44k = Ck[3, 3]
C45k = Ck[3, 4]
for Cl, vl, Dl in zip(Ck_list, Vk, Dk_list):
C55l = Cl[4, 4]
C54l = Cl[4, 3]
denJJ += (vk * vl / (Dk * Dl)) * (C44k * C55l - C45k * C54l)
if abs(denJJ) < 1e-30:
raise ZeroDivisionError("Eq.(9) denominator is near zero.")
Cstar[np.ix_(J, J)] = numJJ / denJJ
# Store updated ply tangents for the next increment (C^k_(n+1))
for p, Cnew in zip(self.plies, Ck_next_list):
p.Cglob_prev = Cnew
return Cstar
# ===============================================================
# Helpers for symmetric stacking from upper-half definition
# ===============================================================
def build_full_symmetric_stack(half_angles, center_angle=None):
"""
Given an ordered list of angles for the *first half* of the laminate,
build the full symmetric stack with the same angles mirrored in the second half.
Examples:
[30, -30] -> [30, -30, -30, 30]
[30, -30], 0 -> [30, -30, 0, -30, 30]
"""
half = list(half_angles)
mirrored_half = list(reversed(half))
if center_angle is None:
return half + mirrored_half
return half + [center_angle] + mirrored_half
def _format_angle_for_label(a):
"""
Format a ply angle for use in filenames:
- integer degrees
- explicit sign (+ or -)
Examples:
45.0 -> '+45'
-30.0 -> '-30'
0.0 -> '+0'
"""
a_int = int(round(float(a)))
sign = "+" if a_int >= 0 else "-"
return f"{sign}{abs(a_int)}"
def stack_label_from_upper(upper_angles):
"""
Return a human-readable description of the *upper-half* stacking sequence,
with comma separation, e.g. [0, 45, -45] -> '0, 45, -45'.
"""
parts = []
for a in upper_angles:
# show integer degrees without .0
if float(a).is_integer():
parts.append(f"{int(a)}")
else:
parts.append(f"{a}")
return ", ".join(parts)
# ===============================================================
# 5D uniaxial test driver (σy=τxy=σz=τyz=τxz=0), incremental: ds = Ceff_prev @ de
# ===============================================================
def run_uniaxial_test_from_files_5d(prefix: str, stack_angles, mode="11"):
# ---- Material from files ----
tply = 0.05 #dummy value, doesn't effect the result
# ---- Layup from full stacking sequence ----
plies = [Ply(angle_deg, tply, mat) for angle_deg in stack_angles]
lam = Laminate(plies)
# ---- Choose loading mode and strain path ----
if mode == "11":
main_index = 0 # ε11
eps_max = 0.10
elif mode == "22":
main_index = 1 # ε22
eps_max = 0.10
elif mode == "12":
main_index = 5 # γ12 (engineering shear internally)
eps_max = 0.20 # ⇒ tensorial E12 = γ12/2 goes to 0.10
else:
raise ValueError("mode must be '11', '22' or '12'")
main_steps = np.linspace(0.0, eps_max, 3500) #1500 strain increments are chosen for a converged result
# ---- Histories (totals) ----
ex_hist, sx_hist = [], [] # main component strain & stress
ey_hist, gxy_hist = [], []
ezz_hist, g23_hist, g13_hist = [], [], []
e11_hist = [] # always store total ε11
# ---- Previous-step totals ----
ex_prev = 0.0
ey_prev = 0.0
gxy_prev = 0.0
ezz_prev = 0.0
g23_prev = 0.0
g13_prev = 0.0
s1_prev = 0.0
for i in range(1, len(main_steps)):
main_target = main_steps[i]
# Previous value of the driven strain component
if main_index == 0:
main_prev = ex_prev
elif main_index == 1:
main_prev = ey_prev
else: # main_index == 5 (γ12)
main_prev = gxy_prev
# Increment in prescribed "main" strain (ε11 / ε22 / γ12)
dmain = main_target - main_prev
# Tangent Ceff at previous state from laminate
Ceff_prev = lam.effective_C_from_previous_strains(
ex_prev, ey_prev, ezz_prev, g23_prev, g13_prev, gxy_prev
)
# ---- Safeguard: check conditioning of Ceff_prev ----
cond = np.linalg.cond(Ceff_prev)
if not np.isfinite(cond) or cond > COND_MAX:
raise np.linalg.LinAlgError(
f"Effective C is ill-conditioned (cond={cond:.3e}) at step {i}"
)
# Compute ONLY the column of the compliance needed via solve,
# instead of inverting the full 6x6 matrix.
e_j = np.zeros(6)
e_j[main_index] = 1.0
try:
# S_col satisfies: Ceff_prev @ S_col = e_j
S_col = np.linalg.solve(Ceff_prev, e_j)
except np.linalg.LinAlgError as err:
raise np.linalg.LinAlgError(
f"Failed to solve for compliance column at step {i}: {err}"
)
Sjj = S_col[main_index]
if abs(Sjj) < 1e-20:
raise ZeroDivisionError(
f"Sjj is zero or too small at step {i} (Sjj={Sjj:.3e})."
)
ds1 = dmain / Sjj
de_vec = S_col * ds1
# enforce exactly the prescribed main increment
de_vec[main_index] = dmain
de1, de2, de3, de4, de5, de6 = de_vec
de4 = 0.0
de5 = 0.0
# (4) Update totals
s1 = s1_prev + ds1
ex = ex_prev + de1
ey = ey_prev + de2
ezz = ezz_prev + de3
g23 = g23_prev + de4
g13 = g13_prev + de5
gxy = gxy_prev + de6
# (5) Update fibre angles from INCREMENTS (Δex, Δey, Δγxy)
lam.update_fiber_angles_incremental(de1, de2, de6)
# (6) Commit for next step
ex_prev, ey_prev, ezz_prev = ex, ey, ezz
g23_prev, g13_prev, gxy_prev = g23, g13, gxy
s1_prev = s1
# (7) Save totals for output
# ex_hist stores the *driven* component: ε11, ε22, or *tensorial* E12 = γ12/2
if main_index == 0:
main_strain = ex
elif main_index == 1:
main_strain = ey
else: # shear
main_strain = 0.5 * gxy # convert engineering γ12 -> tensorial E12
ex_hist.append(main_strain)
sx_hist.append(s1)
ey_hist.append(ey); gxy_hist.append(gxy)
ezz_hist.append(ezz); g23_hist.append(g23); g13_hist.append(g13)
e11_hist.append(ex) # store true ε11 regardless of mode
angles_str = ", ".join(f"{p.theta_curr_deg:.2f}°" for p in lam.plies)
return (np.array(ex_hist), np.array(sx_hist),
np.array(ey_hist), np.array(gxy_hist),
np.array(ezz_hist), np.array(g23_hist), np.array(g13_hist),
np.array(e11_hist))
if __name__ == "__main__":
# ---- estimate total number of laminate output files ----
n_mat = len(MATERIAL_TYPES)
n_vf = len(VOL_FRACTIONS)
expected_files = n_mat * n_vf
print(f"Expected number of laminate output files: {expected_files}")
file_count = 0
for mat_type in MATERIAL_TYPES:
for vf in VOL_FRACTIONS:
vf_str = vf
prefix = f"{mat_type}_{vf_str}"
vf_meta, centers_meta, n_fibers = read_dataset_metadata(prefix)
mat = load_ud_material_from_files(prefix)
# ---- single fixed stacking sequence ----
full_angles = FIXED_STACK_ANGLES
# human-readable label for this stacking
stack_label_human = stack_label_from_upper(full_angles)
stack_label_file = "_".join(
s.strip().replace("+", "p").replace("-", "m")
for s in stack_label_human.split(",")
)
combined_blocks = {}
for mode in ("11", "22", "12"):
try:
# main strain, stress, ε22, γ12, ε33, γ23, γ13, ε11
ex, sx, ey, gxy, ezz, g23, g13, e11 = run_uniaxial_test_from_files_5d(
prefix,
full_angles,
mode=mode,
)
except Exception as e:
print(f"Skipping {prefix}, stack={stack_label_human} due to error: {e}")
continue
# 10 equally spaced output points in the driven (main) strain
N = 20
x_out = np.linspace(ex[0], ex[-1], N)
sx_out_MPa = np.interp(x_out, ex, sx / 1e6)
# Pick Voigt-notation headers and lateral strain based on mode
if mode == "11":
strain_label = "eps_11"
stress_label = "sig_11"
lateral_label = "eps_22"
lateral_series = ey
e33_label = "eps_33"
e33_series = ezz
elif mode == "22":
strain_label = "eps_22"
stress_label = "sig_22"
lateral_label = "eps_11"
lateral_series = e11
e33_label = "eps_33"
e33_series = ezz
elif mode == "12":
strain_label = "eps_12"
stress_label = "sig_12"
lateral_label = None
lateral_series = None
e33_label = None
e33_series = None
else:
raise ValueError(f"Unknown mode {mode}")
# Build lateral output (if any) on the same main-strain grid
if lateral_series is not None:
lateral_out = np.interp(x_out, ex, lateral_series)
else:
lateral_out = None
if e33_series is not None:
e33_out = np.interp(x_out, ex, e33_series)
else:
e33_out = None
# Build header line and numeric rows (strings) for this mode
if lateral_label is None:
# shear case (12): two columns
header_line = f"{strain_label:<8} {stress_label:<8}"
else:
# tensile 11 or 22: four columns
header_line = f"{strain_label:<8} {stress_label:<8} {lateral_label:<8} {e33_label:<8}"
rows = []
if lateral_label is None:
# two columns: eps, sig
for eps_val, sig_val in zip(x_out, sx_out_MPa):
line = f"{eps_val:8.6f} {sig_val:8.3f}"
rows.append(line)
else:
# four columns: eps, sig, lateral_eps, eps_33
for eps_val, sig_val, lat_val, e33_val in zip(x_out, sx_out_MPa, lateral_out, e33_out):
line = f"{eps_val:8.6f} {sig_val:8.3f} {lat_val:8.6f} {e33_val:8.6f}"
rows.append(line)
# store this block for the combined file
combined_blocks[mode] = (header_line, rows)
# ---- write combined file: 11, then 22, then 12, then common metadata ----
OUT_DIR_ALL.mkdir(parents=True, exist_ok=True)
combined_file = OUT_DIR_ALL / (
f"{mat_type}_{vf_str}_{stack_label_file}.txt"
)
with open(combined_file, "w") as fc:
# write 11, then 22, then 12 in order
for m in ("11", "22", "12"):
header_line, rows = combined_blocks[m]
fc.write(header_line + "\n")
for line in rows:
fc.write(line + "\n")
fc.write("\n")
# one common metadata block at the end
fc.write(f"volume fraction= {vf_meta:.6f}\n")
fc.write(f"material type= {mat_type}\n")
fc.write("loading modes= 11, 22, 12\n")
fc.write(f"stacking sequence= {stack_label_human}\n")
if n_fibers is not None:
fc.write(f"number of fibers= {n_fibers}\n")
if centers_meta is not None:
fc.write(f"fiber_centers_YZ={centers_meta}\n")
file_count += 1
print(f"Generated {file_count}/{expected_files} files")