# --------------------- 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")