From ae6da1d43ca7d9046238a5c50f83f44d1b932d4a Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 6 Nov 2026 10:42:41 +0100 Subject: [PATCH 01/12] Trim temporaries and per-call overhead across the kernels The micro-optimisation commits of perf/micro-optim (01-08) rebased onto the 2.2 cleanup, with their comments trimmed to the house style: - Trim temporaries from the shared tensor kernels - Skip masked tangent products and heap temporaries - Remove repeated work from mechanical kernels - Build anisotropic criteria operators on the stack - Evaluate SMA criteria once per local iteration - Drop duplicate flow evaluations in thermal J2 laws - Trim the modular engine's per-iteration overhead - Rebuild finite-strain kernels on the stack Results bit-identical to the base (solver and batch dumps). Serial us/pt against the base: MODUL plastic 4.74 -> 3.06, EPKCP 5.76 -> 3.60, EPICP 1.93 -> 1.55, MODUL elastic 0.37 -> 0.31, NEOHC 2.76 -> 1.30, fedoo UL MODUL 7.3 -> 5.1. --- .../Functions/fastor_bridge.hpp | 7 +- .../Umat/Modular/damage_mechanism.hpp | 10 + .../Umat/Modular/hardening.hpp | 6 +- .../Umat/Modular/plasticity_mechanism.hpp | 3 + .../Umat/Modular/viscoelastic_mechanism.hpp | 2 + .../Umat/Modular/yield_criterion.hpp | 14 + .../Functions/contimech.cpp | 211 +++++--- .../Functions/criteria.cpp | 132 +++-- .../Functions/hyperelastic.cpp | 464 +++++++++++++----- .../Functions/objective_rates.cpp | 456 ++++++++++++----- src/Continuum_mechanics/Functions/tensor.cpp | 71 +-- .../Umat/Finite/Neo_hookean_incomp.cpp | 6 +- .../Umat/Finite/generic_hyper_pstretch.cpp | 3 +- .../Umat/Mechanical/Damage/damage_LLD_0.cpp | 49 +- .../Plasticity/plastic_chaboche.cpp | 35 +- .../Plasticity/plastic_johnson_cook_ccp.cpp | 10 +- .../Umat/Mechanical/SMA/SMA_mono.cpp | 43 +- .../Umat/Mechanical/SMA/unified_T.cpp | 70 ++- .../Umat/Mechanical/SMA/unified_TR.cpp | 74 +-- .../Mechanical/Viscoelasticity/Zener_fast.cpp | 10 +- .../Viscoelasticity/linear_viscoelastic.cpp | 47 +- .../Umat/Modular/damage_mechanism.cpp | 44 +- .../Umat/Modular/hardening.cpp | 124 ++++- .../Umat/Modular/internal_variable.cpp | 13 +- .../Umat/Modular/legacy_adapters.cpp | 29 +- .../Umat/Modular/modular_umat.cpp | 127 +++-- .../Umat/Modular/plasticity_mechanism.cpp | 93 +++- .../Umat/Modular/viscoelastic_mechanism.cpp | 21 +- .../Umat/Modular/yield_criterion.cpp | 99 +++- .../Plasticity/plastic_isotropic.cpp | 5 +- .../Plasticity/plastic_kin_iso.cpp | 12 +- .../Umat/Thermomechanical/SMA/unified_T.cpp | 75 ++- src/Continuum_mechanics/Umat/fea_transfer.cpp | 17 +- .../Umat/return_mapping.cpp | 7 +- .../Umat/tangent_assembly.cpp | 53 +- .../Umat/umat_L_elastic.cpp | 4 +- src/Continuum_mechanics/Umat/umat_smart.cpp | 19 +- src/Simulation/Maths/num_solve.cpp | 143 +++--- src/Simulation/Maths/rotation.cpp | 37 +- 39 files changed, 1783 insertions(+), 862 deletions(-) diff --git a/include/simcoon/Continuum_mechanics/Functions/fastor_bridge.hpp b/include/simcoon/Continuum_mechanics/Functions/fastor_bridge.hpp index 47746851a..6e02e915b 100644 --- a/include/simcoon/Continuum_mechanics/Functions/fastor_bridge.hpp +++ b/include/simcoon/Continuum_mechanics/Functions/fastor_bridge.hpp @@ -98,8 +98,8 @@ inline arma::mat::fixed<3,3> fastor2_to_arma(const Fastor::Tensor &T * or the type-free Mandel pair mandel_to_fastor4 / fastor4_to_mandel. */ inline Fastor::Tensor voigt_to_fastor4(const arma::mat::fixed<6,6> &L) { + // The four minor-symmetric writes below cover all 81 entries: no zero fill. Fastor::Tensor C; - C.zeros(); for (int i = 0; i < 3; ++i) { for (int j = i; j < 3; ++j) { @@ -210,9 +210,9 @@ inline Fastor::Tensor push_forward_4( const Fastor::Tensor &C, const Fastor::Tensor &F) { + // Every entry of each pass is written exactly once: no zero fill. // Pass 1: T1(i,J,M,N) = sum_L F(i,L) * C(L,J,M,N) Fastor::Tensor T1; - T1.zeros(); for (int i = 0; i < 3; ++i) for (int J = 0; J < 3; ++J) for (int M = 0; M < 3; ++M) @@ -225,7 +225,6 @@ inline Fastor::Tensor push_forward_4( // Pass 2: T2(i,s,M,N) = sum_J F(s,J) * T1(i,J,M,N) Fastor::Tensor T2; - T2.zeros(); for (int i = 0; i < 3; ++i) for (int s = 0; s < 3; ++s) for (int M = 0; M < 3; ++M) @@ -238,7 +237,6 @@ inline Fastor::Tensor push_forward_4( // Pass 3: T3(i,s,r,N) = sum_M F(r,M) * T2(i,s,M,N) Fastor::Tensor T3; - T3.zeros(); for (int i = 0; i < 3; ++i) for (int s = 0; s < 3; ++s) for (int r = 0; r < 3; ++r) @@ -251,7 +249,6 @@ inline Fastor::Tensor push_forward_4( // Pass 4: result(i,s,r,p) = sum_N F(p,N) * T3(i,s,r,N) Fastor::Tensor result; - result.zeros(); for (int i = 0; i < 3; ++i) for (int s = 0; s < 3; ++s) for (int r = 0; r < 3; ++r) diff --git a/include/simcoon/Continuum_mechanics/Umat/Modular/damage_mechanism.hpp b/include/simcoon/Continuum_mechanics/Umat/Modular/damage_mechanism.hpp index 7a0f3001d..5bb82f378 100644 --- a/include/simcoon/Continuum_mechanics/Umat/Modular/damage_mechanism.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Modular/damage_mechanism.hpp @@ -107,6 +107,16 @@ class DamageMechanism final : public StrainMechanism { mutable bool M_cached_valid_; ///< Whether the cached compliances are valid mutable arma::mat L_cached_; ///< Stiffness the cached compliance was inverted from std::vector kappa_cache_{tensor2::zeros(Tensor2Type::strain)}; ///< zero: no multiplier carried + size_t D_index_ = 0; ///< ivc_ index of "D", resolved by register_variables + size_t Y_max_index_ = 0; ///< ivc_ index of "Y_max", resolved by register_variables + + // last driving force and the effective stress it was evaluated at, for the cached compliance (reset with it) + mutable arma::vec::fixed<6> Y_sigma_; + mutable double Y_sigma_value_ = 0.0; + mutable bool Y_sigma_valid_ = false; + + /// compute_driving_force(sigma_eff, M_cached_), memoised on the last stress + double driving_force(const arma::vec& sigma_eff) const; public: /** diff --git a/include/simcoon/Continuum_mechanics/Umat/Modular/hardening.hpp b/include/simcoon/Continuum_mechanics/Umat/Modular/hardening.hpp index 37a97b019..5fa38fe2a 100644 --- a/include/simcoon/Continuum_mechanics/Umat/Modular/hardening.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Modular/hardening.hpp @@ -231,7 +231,8 @@ class KinematicHardening { /** * @brief Register internal variables for this hardening model - * @param ivc The owning mechanism's internal variable collection + * @param ivc The owning mechanism's internal variable collection; every other member taking a + * collection must get this same one (read by the indices resolved here, debug-checked). */ virtual void register_variables(InternalVariableCollection& ivc) = 0; @@ -410,6 +411,7 @@ class PragerHardening final : public KinematicHardening { private: double C_; ///< Kinematic hardening modulus std::string a_key_{"a"}; ///< IVC key for the back-strain α + size_t a_index_ = 0; ///< its index in the collection, resolved by register_variables public: PragerHardening() : C_(0.0) {} void configure(const arma::vec& props, int& offset) override; @@ -434,6 +436,7 @@ class ArmstrongFrederickHardening final : public KinematicHardening { double C_; ///< Hardening parameter double D_; ///< Dynamic recovery parameter std::string a_key_{"a"}; ///< IVC key for the back-strain α + size_t a_index_ = 0; ///< its index in the collection, resolved by register_variables public: ArmstrongFrederickHardening() : C_(0.0), D_(0.0) {} void configure(const arma::vec& props, int& offset) override; @@ -461,6 +464,7 @@ class ChabocheHardening final : public KinematicHardening { arma::vec C_; ///< Hardening parameters arma::vec D_; ///< Dynamic recovery parameters std::vector a_keys_; ///< IVC keys for back-strains α_i ("a_0", "a_1", ...) + std::vector a_index_; ///< their indices in the collection, resolved by register_variables public: explicit ChabocheHardening(int N = 1) : N_(N), C_(arma::zeros(N)), D_(arma::zeros(N)) {} void configure(const arma::vec& props, int& offset) override; diff --git a/include/simcoon/Continuum_mechanics/Umat/Modular/plasticity_mechanism.hpp b/include/simcoon/Continuum_mechanics/Umat/Modular/plasticity_mechanism.hpp index 49eb6f8a7..ac232f3d4 100644 --- a/include/simcoon/Continuum_mechanics/Umat/Modular/plasticity_mechanism.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Modular/plasticity_mechanism.hpp @@ -114,6 +114,9 @@ class PlasticityMechanism final : public StrainMechanism { mutable double H_total_{0.0}; ///< Hardening modulus (iso + kin) std::vector cpp_cache_{ClosestPointIngredients{}}; ///< built by refresh_state, read by closest_point_ingredients + /// Total backstress (engineering stress Voigt), summed in the X_branches_ buffer it overwrites (no allocation) + [[nodiscard]] arma::vec total_backstress_voigt() const; + public: /** * @brief Constructor diff --git a/include/simcoon/Continuum_mechanics/Umat/Modular/viscoelastic_mechanism.hpp b/include/simcoon/Continuum_mechanics/Umat/Modular/viscoelastic_mechanism.hpp index 2a5709d77..d0f6e0f13 100644 --- a/include/simcoon/Continuum_mechanics/Umat/Modular/viscoelastic_mechanism.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Modular/viscoelastic_mechanism.hpp @@ -69,6 +69,8 @@ class ViscoelasticMechanism final : public StrainMechanism { // inside the FB hot loop). std::vector ev_key_; ///< "EV_" per branch std::vector v_key_; ///< "v_" per branch + std::vector ev_index_; ///< ivc_ index of "EV_", resolved by register_variables + std::vector v_index_; ///< ivc_ index of "v_", resolved by register_variables // Per-iteration caches (CCP: frozen flow direction from previous iteration) mutable std::vector kappa_t_; ///< Zero fluxes: the rows carry no multiplier diff --git a/include/simcoon/Continuum_mechanics/Umat/Modular/yield_criterion.hpp b/include/simcoon/Continuum_mechanics/Umat/Modular/yield_criterion.hpp index 812162577..a82071c9c 100644 --- a/include/simcoon/Continuum_mechanics/Umat/Modular/yield_criterion.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Modular/yield_criterion.hpp @@ -55,6 +55,8 @@ class YieldCriterion { private: YieldType type_; arma::vec params_; ///< Criterion-specific parameters + arma::mat P_; ///< HILL / DFA / ANISOTROPIC: quadratic-form matrix of params_ + ///< (P_Hill / P_DFA / P_Ani), built once by configure_* bool configured_; /** @@ -63,6 +65,18 @@ class YieldCriterion { */ std::string type_string() const; + /** + * @brief equivalent_stress(sigma) and flow_direction(sigma) in one call (the quadratic criteria share P sigma). + */ + void equivalent_stress_and_flow(const arma::vec& sigma, double& sigma_eq, arma::vec& flow) const; + + /** + * @brief The same plus flow_hessian(sigma) (criteria with a flow Hessian only, else its exception). + */ + void equivalent_stress_flow_hessian(const arma::vec& sigma, double& sigma_eq, arma::vec& flow, + arma::mat& hessian) const; + friend class PlasticityMechanism; + public: /** * @brief Default constructor (von Mises) diff --git a/src/Continuum_mechanics/Functions/contimech.cpp b/src/Continuum_mechanics/Functions/contimech.cpp index 89b604283..2d06ea039 100755 --- a/src/Continuum_mechanics/Functions/contimech.cpp +++ b/src/Continuum_mechanics/Functions/contimech.cpp @@ -33,10 +33,55 @@ using namespace arma; namespace simcoon{ +namespace { + +// dev(vec) of a Voigt 6-vector and its partner with shears scaled by s_shear (2 stress, 0.5 strain) +inline void dev_voigt(const vec &v, const double &s_shear, double d[6], double d2[6]) { + const double sph = (1./3.)*(v(0) + v(1) + v(2)); + for (int i=0; i<3; i++) { + d[i] = v(i) - sph; + d2[i] = d[i]; + } + for (int i=3; i<6; i++) { + d[i] = v(i); + d2[i] = s_shear*v(i); + } +} + +// sum(a % b) with the two interleaved partial sums of Armadillo's dot (same rounding as sum()) +inline double dot_voigt(const double a[6], const double b[6]) { + double val1 = 0.; + double val2 = 0.; + for (int i=0; i<6; i+=2) { + val1 += (a[i] * b[i]); + val2 += (a[i+1] * b[i+1]); + } + return (val1 + val2); +} + +} // namespace + mat dev(const mat &m) { assert((m.n_cols==3)&&(m.n_rows==3)); - return m - sph(m); + if ((m.n_rows != 3) || (m.n_cols != 3)) { + return m - sph(m); + } + // m - sph(m) entry by entry; off-diagonals subtract 0*tr/3 to keep signed zeros and non-finite traces + double val1 = 0.; + double val2 = 0.; + val1 += m(0,0); + val2 += m(1,1); + val1 += m(2,2); + const double k = (1./3.)*(val1 + val2); + const double zero_k = 0.*k; + mat d(3, 3, fill::none); + for (int j=0; j<3; j++) { + for (int i=0; i<3; i++) { + d(i,j) = m(i,j) - ((i == j) ? k : zero_k); + } + } + return d; } mat sph(const mat &m) { @@ -70,26 +115,27 @@ vec dev(const vec &v) { double Mises_stress(const vec &v) { assert(v.size()==6); - vec vdev = dev(v); - vec vdev2 = vdev; - for (int i=3; i<6; i++) - vdev2(i) = 2.*vdev2(i); + double vdev[6], vdev2[6]; + dev_voigt(v, 2., vdev, vdev2); - return sqrt(3./2.*sum(vdev%vdev2)); + return sqrt(3./2.*dot_voigt(vdev, vdev2)); } vec eta_stress(const vec &v) { assert(v.size()==6); - vec vdev = dev(v); - vec vdev2 = vdev; - for (int i=3; i<6; i++) - vdev2(i) = 2.*vdev2(i); + double vdev[6], vdev2[6]; + dev_voigt(v, 2., vdev, vdev2); - double n = sqrt(3./2.*sum(vdev%vdev2)); + double n = sqrt(3./2.*dot_voigt(vdev, vdev2)); - if (n > 0.) { - return (3./2.)*vdev2*(1./n); + if (n > 0.) { + // (3/2) vdev2 (1/n), evaluated as the expression template did + const double inv_n = 1./n; + vec eta(6, fill::none); + for (int i=0; i<6; i++) + eta(i) = ((3./2.)*vdev2[i])*inv_n; + return eta; } else { return zeros(6); @@ -100,33 +146,52 @@ vec eta_stress(const vec &v) { mat deta_stress(const vec &v) { assert(v.size()==6); - double sigeq = Mises_stress(v); + double vdev[6], vdev2[6]; + dev_voigt(v, 2., vdev, vdev2); + double sigeq = sqrt(3./2.*dot_voigt(vdev, vdev2)); if (sigeq < simcoon::iota) { return zeros(6,6); } - vec eta = eta_stress(v); + double eta[6]; + const double inv_n = 1./sigeq; + for (int i=0; i<6; i++) + eta[i] = (sigeq > 0.) ? ((3./2.)*vdev2[i])*inv_n : 0.; // P_Mises in simcoon's engineering-shear Voigt basis: normal block = (3/2) dev, // shear diagonal = 3 (the factor 2 is already embedded, as in eta_stress / Mises_stress). - mat Pm = { { 1., -0.5, -0.5, 0., 0., 0.}, - {-0.5, 1., -0.5, 0., 0., 0.}, - {-0.5, -0.5, 1., 0., 0., 0.}, - { 0., 0., 0., 3., 0., 0.}, - { 0., 0., 0., 0., 3., 0.}, - { 0., 0., 0., 0., 0., 3.} }; - return (Pm - eta*eta.t())/sigeq; + // the product stays its own statement: rounded before the subtraction, as in the matrix temporary + static const double Pm[6][6] = { { 1., -0.5, -0.5, 0., 0., 0.}, + {-0.5, 1., -0.5, 0., 0., 0.}, + {-0.5, -0.5, 1., 0., 0., 0.}, + { 0., 0., 0., 3., 0., 0.}, + { 0., 0., 0., 0., 3., 0.}, + { 0., 0., 0., 0., 0., 3.} }; + mat deta(6, 6, fill::none); + for (int j=0; j<6; j++) { + for (int i=0; i<6; i++) { + const double eta_ij = eta[i]*eta[j]; + deta(i,j) = (Pm[i][j] - eta_ij)/sigeq; + } + } + return deta; } vec eta_norm_stress(const vec &v) { assert(v.size()==6); - vec v2 = v; - for (int i=3; i<6; i++) - v2(i) = 2.*v2(i); + double v1[6], v2[6]; + for (int i=0; i<6; i++) { + v1[i] = v(i); + v2[i] = (i < 3) ? v1[i] : 2.*v1[i]; + } - double n = sqrt(sum(v%v2)); + double n = sqrt(dot_voigt(v1, v2)); if (n > 0.) { - return v2*(1./n); + const double inv_n = 1./n; + vec eta(6, fill::none); + for (int i=0; i<6; i++) + eta(i) = v2[i]*inv_n; + return eta; } else { return zeros(6); @@ -136,14 +201,20 @@ vec eta_norm_stress(const vec &v) { vec eta_norm_strain(const vec &v) { assert(v.size()==6); - vec v2 = v; - for (int i=3; i<6; i++) - v2(i) = 0.5*v2(i); + double v1[6], v2[6]; + for (int i=0; i<6; i++) { + v1[i] = v(i); + v2[i] = (i < 3) ? v1[i] : 0.5*v1[i]; + } - double n = sqrt(sum(v%v2)); + double n = sqrt(dot_voigt(v1, v2)); if (n > 0.) { - return v*(1./n); + const double inv_n = 1./n; + vec eta(6, fill::none); + for (int i=0; i<6; i++) + eta(i) = v1[i]*inv_n; + return eta; } else { return zeros(6); @@ -154,47 +225,52 @@ double norm_stress(const vec &v) { assert(v.size()==6); - vec v2 = v; - for (int i=3; i<6; i++) - v2(i) = 2.*v2(i); + double v1[6], v2[6]; + for (int i=0; i<6; i++) { + v1[i] = v(i); + v2[i] = (i < 3) ? v1[i] : 2.*v1[i]; + } - return sqrt(sum(v%v2)); + return sqrt(dot_voigt(v1, v2)); } double norm_strain(const vec &v) { assert(v.size()==6); - vec v2 = v; - for (int i=3; i<6; i++) - v2(i) = 0.5*v2(i); + double v1[6], v2[6]; + for (int i=0; i<6; i++) { + v1[i] = v(i); + v2[i] = (i < 3) ? v1[i] : 0.5*v1[i]; + } - return sqrt(sum(v%v2)); + return sqrt(dot_voigt(v1, v2)); } double Mises_strain(const vec &v) { assert(v.size()==6); - vec vdev = dev(v); - vec vdev2 = vdev; - for (int i=3; i<6; i++) - vdev2(i) = 0.5*vdev2(i); + double vdev[6], vdev2[6]; + dev_voigt(v, 0.5, vdev, vdev2); - return sqrt(2./3.*sum(vdev%vdev2)); + return sqrt(2./3.*dot_voigt(vdev, vdev2)); } vec eta_strain(const vec &v) { assert(v.size()==6); - vec vdev = dev(v); - vec vdev2 = vdev; - for (int i=3; i<6; i++) - vdev2(i) = 0.5*vdev2(i); + double vdev[6], vdev2[6]; + dev_voigt(v, 0.5, vdev, vdev2); - double n = sqrt(2./3.*sum(vdev%vdev2)); + double n = sqrt(2./3.*dot_voigt(vdev, vdev2)); if (n > 0.) { - return (2./3.)*vdev*(1./n); + // (2/3) vdev (1/n), evaluated as the expression template did + const double inv_n = 1./n; + vec eta(6, fill::none); + for (int i=0; i<6; i++) + eta(i) = ((2./3.)*vdev[i])*inv_n; + return eta; } else { return zeros(6); @@ -205,24 +281,20 @@ vec eta_strain(const vec &v) { double J2_stress(const vec &v) { assert(v.size()==6); - vec vdev = dev(v); - vec vdev2 = vdev; - for (int i=3; i<6; i++) - vdev2(i) = 2.*vdev2(i); + double vdev[6], vdev2[6]; + dev_voigt(v, 2., vdev, vdev2); - return 0.5*sum(vdev%vdev2); + return 0.5*dot_voigt(vdev, vdev2); } double J2_strain(const vec &v) { assert(v.size()==6); - vec vdev = dev(v); - vec vdev2 = vdev; - for (int i=3; i<6; i++) - vdev2(i) = 0.5*vdev2(i); + double vdev[6], vdev2[6]; + dev_voigt(v, 0.5, vdev, vdev2); - return 0.5*sum(vdev%vdev2); + return 0.5*dot_voigt(vdev, vdev2); } double J3_stress(const vec &v) { @@ -347,18 +419,29 @@ mat p_ikjl(const vec &a) { return F; } +// outer product entry by entry: one rounded product per entry, as the rank-one BLAS call +static mat outer_voigt(const vec &a, const vec &b) { + mat C(6, 6, fill::none); + for (int j=0; j<6; j++) { + for (int i=0; i<6; i++) { + C(i,j) = a(i)*b(j); + } + } + return C; +} + mat auto_sym_dyadic(const mat &A) { vec A_v = t2v_sym(A); - return A_v * A_v.t(); + return outer_voigt(A_v, A_v); } mat sym_dyadic(const mat &A, const mat &B) { vec A_v = t2v_sym(A); vec B_v = t2v_sym(B); - - return A_v * B_v.t(); + + return outer_voigt(A_v, B_v); } mat auto_dyadic(const mat &A) { diff --git a/src/Continuum_mechanics/Functions/criteria.cpp b/src/Continuum_mechanics/Functions/criteria.cpp index e403b07dd..d477a0ae3 100755 --- a/src/Continuum_mechanics/Functions/criteria.cpp +++ b/src/Continuum_mechanics/Functions/criteria.cpp @@ -45,19 +45,20 @@ namespace simcoon assert(n > 0.); double temp; double m; + const double Mises = Mises_stress(v); // Below iota the invariants are roundoff and 1 + b J3/J2^1.5 can leave its bounds: NaN - if (Mises_stress(v) > simcoon::iota) + if (Mises > simcoon::iota) { if (n < 10.) { m = 1. / n; - temp = Mises_stress(v) * pow((1. + b * J3_stress(v) / pow(J2_stress(v), 1.5)), m); + temp = Mises * pow((1. + b * J3_stress(v) / pow(J2_stress(v), 1.5)), m); } else { m = 0.; - temp = Mises_stress(v); + temp = Mises; } } else @@ -75,19 +76,6 @@ namespace simcoon assert(n > 0.); double Mises = Mises_stress(v); - vec dMises = eta_stress(v); - - double J2 = J2_stress(v); - double J3 = J3_stress(v); - double J2_3_2 = pow(J2, 1.5); - double J2_5_2 = pow(J2, 2.5); - - mat sigma = v2t_stress(v); - - mat dJ2_mat=dJ2DS(sigma); - mat dJ3_mat=dJ3DS(sigma); - vec dJ2 = t2v_strain(dJ2_mat); - vec dJ3 = t2v_strain(dJ3_mat); double m; vec temp; @@ -98,6 +86,17 @@ namespace simcoon { m = 1. / n; + vec dMises = eta_stress(v); + double J2 = J2_stress(v); + double J3 = J3_stress(v); + double J2_3_2 = pow(J2, 1.5); + double J2_5_2 = pow(J2, 2.5); + mat sigma = v2t_stress(v); + mat dJ2_mat=dJ2DS(sigma); + mat dJ3_mat=dJ3DS(sigma); + vec dJ2 = t2v_strain(dJ2_mat); + vec dJ3 = t2v_strain(dJ3_mat); + temp = pow((1. + b * J3 / J2_3_2), m) * dMises + Mises * b * m * (pow((1. + b * J3 / J2_3_2), (m-1))* (dJ3/J2_3_2-1.5*J3*dJ2/J2_5_2)); } else @@ -227,20 +226,6 @@ namespace simcoon assert(n > 0.); double Dfa_stress = DFA_stress(v, params); - vec dDfa_stress = dDFA_stress(v,params); - - double J2 = J2_stress(v); - double J3 = J3_stress(v); - double J2_3_2 = pow(J2, 1.5); - double J2_5_2 = pow(J2, 2.5); - - mat sigma = v2t_stress(v); - - mat dJ2_mat=dJ2DS(sigma); - mat dJ3_mat=dJ3DS(sigma); - vec dJ2 = t2v_strain(dJ2_mat); - vec dJ3 = t2v_strain(dJ3_mat); - double m; vec temp; @@ -251,6 +236,17 @@ namespace simcoon { m = 1. / n; + vec dDfa_stress = dDFA_stress(v,params); + double J2 = J2_stress(v); + double J3 = J3_stress(v); + double J2_3_2 = pow(J2, 1.5); + double J2_5_2 = pow(J2, 2.5); + mat sigma = v2t_stress(v); + mat dJ2_mat=dJ2DS(sigma); + mat dJ3_mat=dJ3DS(sigma); + vec dJ2 = t2v_strain(dJ2_mat); + vec dJ3 = t2v_strain(dJ3_mat); + temp = pow((1. + b * J3 / J2_3_2), m) * dDfa_stress + Dfa_stress * b * m * (pow((1. + b * J3 / J2_3_2), (m-1))* (dJ3/J2_3_2-1.5*J3*dJ2/J2_5_2)); } else @@ -267,10 +263,12 @@ namespace simcoon return temp; } - mat P_Ani(const vec ¶ms) + namespace { + // anisotropy operators into a caller-provided 6x6 (stack in the criteria, heap in P_Ani/P_Hill/P_DFA) + void fill_P_Ani(const vec ¶ms, mat &P) { assert(params.n_elem == 9); // P_11,P_22,P_33,P_12,P_13,P_23,P_44,P_55,P_66 - mat P = zeros(6, 6); + P.zeros(); P(0, 0) = params(0); // P_11 P(1, 1) = params(1); // P_22 P(2, 2) = params(2); // P_33 @@ -283,14 +281,12 @@ namespace simcoon P(3, 3) = 2. * params(6); // P_44 = P_1212 P(4, 4) = 2. * params(7); // P_55 = P_1313 P(5, 5) = 2. * params(8); // P_66 = P_2323 - - return P; } - mat P_Hill(const vec ¶ms) + void fill_P_Hill(const vec ¶ms, mat &P) { assert(params.n_elem == 6); // F,G,H,L,M,N - mat P = zeros(6, 6); + P.zeros(); // params = (F, G, H, L, M, N) P(0, 0) = params(1) + params(2); // P_11 = G+H P(1, 1) = params(0) + params(2); // P_22 = F+H @@ -304,14 +300,12 @@ namespace simcoon P(3, 3) = 2. * params(5); // P_44 = N P(4, 4) = 2. * params(4); // P_55 = M P(5, 5) = 2. * params(3); // P_66 = L - - return P; } - mat P_DFA(const vec ¶ms) + void fill_P_DFA(const vec ¶ms, mat &P) { // Deshpande–Fleck–Ashby assert(params.n_elem == 7); // F,G,H,L,M,N,K - mat P = zeros(6, 6); + P.zeros(); // params = (F, G, H, L, M, N, K) P(0, 0) = params(1) + params(2) + params(6) / 9.; // P_11 = G+H+K/9 P(1, 1) = params(0) + params(2) + params(6) / 9.; // P_22 = F+H+K/9 @@ -325,7 +319,27 @@ namespace simcoon P(3, 3) = 2. * params(5); // P_44 = N P(4, 4) = 2. * params(4); // P_55 = M P(5, 5) = 2. * params(3); // P_66 = L + } + } + + mat P_Ani(const vec ¶ms) + { + mat P(6, 6); + fill_P_Ani(params, P); + return P; + } + mat P_Hill(const vec ¶ms) + { + mat P(6, 6); + fill_P_Hill(params, P); + return P; + } + + mat P_DFA(const vec ¶ms) + { + mat P(6, 6); + fill_P_DFA(params, P); return P; } @@ -347,7 +361,8 @@ namespace simcoon if (norm(v, 2) > simcoon::iota) { - return (H * v) / Eq_stress_P(v, H); + const vec Hv = H * v; // shared by the gradient and the norm (Eq_stress_P) + return Hv / pow(sum(v % Hv), 0.5); } else { @@ -361,63 +376,76 @@ namespace simcoon { return zeros(6, 6); } - double sigeq = Eq_stress_P(v, H); + const vec Hv = H * v; + double sigeq = pow(sum(v % Hv), 0.5); // Eq_stress_P(v, H) if (sigeq < simcoon::iota) { return zeros(6, 6); } - vec Lambda = (H * v) / sigeq; // == dEq_stress_P(v, H) + vec Lambda = Hv / sigeq; // == dEq_stress_P(v, H) return (H - Lambda * Lambda.t()) / sigeq; } mat ddHill_stress(const vec &v, const vec ¶ms) { - return ddEq_stress_P(v, P_Hill(params)); + mat::fixed<6, 6> P; + fill_P_Hill(params, P); + return ddEq_stress_P(v, P); } mat ddDFA_stress(const vec &v, const vec ¶ms) { - return ddEq_stress_P(v, P_DFA(params)); + mat::fixed<6, 6> P; + fill_P_DFA(params, P); + return ddEq_stress_P(v, P); } mat ddAni_stress(const vec &v, const vec ¶ms) { - return ddEq_stress_P(v, P_Ani(params)); + mat::fixed<6, 6> P; + fill_P_Ani(params, P); + return ddEq_stress_P(v, P); } double Hill_stress(const vec &v, const vec ¶ms) { - mat P = P_Hill(params); + mat::fixed<6, 6> P; + fill_P_Hill(params, P); return Eq_stress_P(v, P); } vec dHill_stress(const vec &v, const vec ¶ms) { - mat P = P_Hill(params); + mat::fixed<6, 6> P; + fill_P_Hill(params, P); return dEq_stress_P(v, P); } double DFA_stress(const vec &v, const vec ¶ms) { - mat P = P_DFA(params); + mat::fixed<6, 6> P; + fill_P_DFA(params, P); return Eq_stress_P(v, P); } vec dDFA_stress(const vec &v, const vec ¶ms) { - mat P = P_DFA(params); + mat::fixed<6, 6> P; + fill_P_DFA(params, P); return dEq_stress_P(v, P); } double Ani_stress(const vec &v, const vec ¶ms) { - mat P = P_Ani(params); + mat::fixed<6, 6> P; + fill_P_Ani(params, P); return Eq_stress_P(v, P); } vec dAni_stress(const vec &v, const vec ¶ms) { - mat P = P_Ani(params); + mat::fixed<6, 6> P; + fill_P_Ani(params, P); return dEq_stress_P(v, P); } diff --git a/src/Continuum_mechanics/Functions/hyperelastic.cpp b/src/Continuum_mechanics/Functions/hyperelastic.cpp index d09eb6999..7b53014aa 100755 --- a/src/Continuum_mechanics/Functions/hyperelastic.cpp +++ b/src/Continuum_mechanics/Functions/hyperelastic.cpp @@ -34,27 +34,277 @@ using namespace arma; namespace simcoon{ -vec isochoric_invariants(const mat &b, const double &mJ) { +namespace { + +// Fixed-size kernels of the isochoric-invariant functions. Each one reproduces the armadillo +// expression it replaces operation by operation (same products, summed in the same order), with +// the 3x3 and 6x6 temporaries on the stack. Bit identity with the armadillo expressions was +// established with Apple clang (-ffp-contract=on: a product is fused only with an addition of the +// same statement) and Armadillo 15.6.0 (the simcoon-prof environment); it rests on internal details +// of that version (tiny-matrix product kernel, two-accumulator trace, diagonal shortcut of powmat). +// A compiler that contracts across statements (GCC's default -ffp-contract=fast in gnu++ mode, on +// FMA targets) or another Armadillo may round differently, at the epsilon level; the +// tolerance-based tests remain the guarantee of correctness. +typedef mat::fixed<3,3> m33; +typedef mat::fixed<6,6> m66; + +// trace(X), summed as armadillo's trace (15.6.0): two accumulators, (X00 + X22) + X11 +inline double trace3(const m33 &X) { + double val1 = 0.; + double val2 = 0.; + val1 += X(0,0); + val2 += X(1,1); + val1 += X(2,2); + return val1 + val2; +} + +// dev(X) = X - ((1/3) tr X) I +inline m33 dev3(const m33 &X) { + const double s = (1./3.)*trace3(X); + m33 D; + for (int j = 0; j < 3; ++j) { + for (int i = 0; i < 3; ++i) { + const double e = (i == j) ? 1. : 0.; + const double sph = e*s; + D(i,j) = X(i,j) - sph; + } + } + return D; +} +// powmat(X, 2): X*X as armadillo's tiny-matrix kernel (15.6.0); a diagonal X takes armadillo's own +// shortcut (a pow call per entry, not always the rounded square) +inline m33 square3(const m33 &X) { + m33 P; + if (X.is_diagmat()) { + P = powmat(X, 2); + return P; + } + for (int j = 0; j < 3; ++j) { + const double x0 = X(0,j), x1 = X(1,j), x2 = X(2,j); + for (int i = 0; i < 3; ++i) { + P(i,j) = X(i,0)*x0 + X(i,1)*x1 + X(i,2)*x2; + } + } + return P; +} + +// t2v_sym(A) +inline void sym_voigt(const m33 &A, double v[6]) { + v[0] = A(0,0); + v[1] = A(1,1); + v[2] = A(2,2); + v[3] = 0.5*(A(0,1) + A(1,0)); + v[4] = 0.5*(A(0,2) + A(2,0)); + v[5] = 0.5*(A(1,2) + A(2,1)); +} + +// t2v_sym of the identity +const double I_voigt[6] = {1., 1., 1., 0., 0., 0.}; + +// Idev(), Ivol() and Ireal() entries (constitutive.cpp) +inline double Idev_at(const int i, const int j) { + if (i < 3 && j < 3) return (i == j) ? 1.*(1.-1./3.) : 1.*(0.-1./3.); + return (i == j) ? 0.5 : 0.; +} +inline double Ivol_at(const int i, const int j) { + return (i < 3 && j < 3) ? (1./3.) : 0.; +} +inline double Ireal_at(const int i, const int j) { + return (i == j) ? ((i < 3) ? 1. : 0.5) : 0.; +} + +// b_bar = J^(-2/3) b +inline m33 b_bar_of(const mat &b, const double &J) { + const double k = pow(J,-2./3.); + m33 b_bar; + for (int e = 0; e < 9; ++e) b_bar(e) = b(e)*k; + return b_bar; +} + +// The first two isochoric invariants of b_bar, as isochoric_invariants +inline void bar_invariants(const m33 &b_bar, double &I1, double &I2) { + I1 = trace3(b_bar); + I2 = 0.5*(pow(trace3(b_bar),2.) - trace3(square3(b_bar))); +} + +double J_of_b(const mat &b, const double &mJ, const char *where) { double J=mJ; if (fabs(mJ) < simcoon::iota) { try { J = sqrt(det(b)); } catch (const std::runtime_error &e) { cerr << "Error in det: " << e.what() << endl; - throw simcoon::exception_det("Error in det function inside isochoric_invariants."); - } + throw simcoon::exception_det(std::string("Error in det function inside ") + where + "."); + } } - mat b_bar = pow(J,-2./3.)*b; - vec I = zeros(3); + return J; +} - I(0) = trace(b_bar); - try { - I(1) = 0.5*(pow(trace(b_bar),2.)-trace(powmat(b_bar,2))); - } catch (const std::runtime_error &e) { - cerr << "Error in det: " << e.what() << endl; - throw simcoon::exception_powmat("Error in powmat function inside isochoric_invariants."); - } +// 2 dW/dI1 dev(b_bar) + 2 dW/dI2 (tr(b_bar) dev(b_bar) - dev(b_bar^2)); the I_2 part is skipped +// when its derivative is zero (an exact zero term) +m33 tau_iso_invariants_fixed(const double &dWdI_1_bar, const double &dWdI_2_bar, const m33 &b_bar) { + const m33 dev_b_bar = dev3(b_bar); + const double c1 = 2.*dWdI_1_bar; + m33 tau; + for (int e = 0; e < 9; ++e) tau(e) = dev_b_bar(e)*c1; + if (dWdI_2_bar != 0.) { + const m33 dev_b_bar2 = dev3(square3(b_bar)); + const double tr = trace3(b_bar); + const double c2 = 2.*dWdI_2_bar; + for (int e = 0; e < 9; ++e) { + const double p = dev_b_bar(e)*tr; + const double d = p - dev_b_bar2(e); + const double q = d*c2; + tau(e) = tau(e) + q; + } + } + return tau; +} + +// gamma_linear: (4/3) (tr_A I_dev - (dev_A (x) I + I (x) dev_A)), scaled by d1, added to Lt +// (Lt = that product when first), with dev_A in Voigt form +void add_gamma_linear(m66 &Lt, const double a[6], const double &tr_A, const double &d1, const bool &first) { + for (int j = 0; j < 6; ++j) { + for (int i = 0; i < 6; ++i) { + const double s1 = a[i]*I_voigt[j]; + const double s2 = I_voigt[i]*a[j]; + const double s = s1 + s2; + const double x = Idev_at(i,j)*tr_A; + const double g = (x - s)*(4./3.); + const double t = g*d1; + Lt(i,j) = first ? t : Lt(i,j) + t; + } + } +} + +// Lt += (4 a (x) a) d2, auto_sym_dyadic of the Voigt vector a +void add_gamma_quadratic(m66 &Lt, const double a[6], const double &d2) { + for (int j = 0; j < 6; ++j) { + for (int i = 0; i < 6; ++i) { + const double g = (a[i]*a[j])*4.; + const double t = g*d2; + Lt(i,j) = Lt(i,j) + t; + } + } +} + +// L_iso_hyper_invariants without the 1/J, on the stack +m66 L_iso_invariants_fixed(const double &dWdI_1_bar, const double &dWdI_2_bar, const double &dW2dI_11_bar, const double &dW2dI_12_bar, const double &dW2dI_22_bar, const m33 &b_bar) { + const m33 dev_b_bar = dev3(b_bar); + double I1, I2; + bar_invariants(b_bar, I1, I2); + double a[6]; + sym_voigt(dev_b_bar, a); + + // Only the terms of a non-zero potential derivative are built, summed in the order of the + // full expression (a skipped term adds an exact zero): a Neo-Hookean potential needs gamma_1 + // alone, and the I_2 terms cost b_bar^2 and five more dyadic products. + m66 Lt; + add_gamma_linear(Lt, a, I1, dWdI_1_bar, true); + if ((dWdI_2_bar != 0.) || (dW2dI_12_bar != 0.) || (dW2dI_22_bar != 0.)) { + const m33 dev_b_bar2 = dev3(square3(b_bar)); + m33 devdevbb2; // I1 dev(b_bar) - dev(b_bar^2) + for (int e = 0; e < 9; ++e) { + const double p = dev_b_bar(e)*I1; + devdevbb2(e) = p - dev_b_bar2(e); + } + if (dWdI_2_bar != 0.) { + double a2[6], bv[6]; + sym_voigt(dev_b_bar2, a2); + sym_voigt(b_bar, bv); + static const int Id[3][3] = {{0, 3, 4}, {3, 1, 5}, {4, 5, 2}}; + m66 H_bar; // auto_sym_dyadic_operator(b_bar) + for (int i=0; i<3; i++) { + for (int j=i; j<3; j++) { + for (int k=0; k<3; k++) { + for (int l=k; l<3; l++) { + H_bar(Id[i][j],Id[k][l]) = 0.5*(b_bar(i,k)*b_bar(j,l) + b_bar(i,l)*b_bar(j,k)); + } + } + } + } + // gamma_2 = (8/3) (I2 (I_real - 2 I_vol) - I1 (dev(b_bar) (x) I + I (x) dev(b_bar)) + // + (dev(b_bar^2) (x) I + I (x) dev(b_bar^2))) + 4 (b_bar (x) b_bar - H_bar) + for (int j = 0; j < 6; ++j) { + for (int i = 0; i < 6; ++i) { + const double v2 = Ivol_at(i,j)*2.; + const double r = Ireal_at(i,j) - v2; + const double X = r*I2; + const double s1 = a[i]*I_voigt[j]; + const double s2 = I_voigt[i]*a[j]; + const double Y = (s1 + s2)*I1; + const double z1 = a2[i]*I_voigt[j]; + const double z2 = I_voigt[i]*a2[j]; + const double Z = z1 + z2; + const double T = ((X - Y) + Z)*(8./3.); + const double bb = bv[i]*bv[j]; + const double U = (bb - H_bar(i,j))*4.; + const double g = T + U; + const double t = g*dWdI_2_bar; + Lt(i,j) = Lt(i,j) + t; + } + } + } + if (dW2dI_11_bar != 0.) { + add_gamma_quadratic(Lt, a, dW2dI_11_bar); + } + double dd[6]; + sym_voigt(devdevbb2, dd); + if (dW2dI_12_bar != 0.) { + for (int j = 0; j < 6; ++j) { + for (int i = 0; i < 6; ++i) { + const double s1 = a[i]*dd[j]; + const double s2 = dd[i]*a[j]; + const double g = (s1 + s2)*4.; + const double t = g*dW2dI_12_bar; + Lt(i,j) = Lt(i,j) + t; + } + } + } + if (dW2dI_22_bar != 0.) { + add_gamma_quadratic(Lt, dd, dW2dI_22_bar); + } + } + else if (dW2dI_11_bar != 0.) { + add_gamma_quadratic(Lt, a, dW2dI_11_bar); + } + return Lt; +} + +// L_vol_hyper on the stack: (dU/dJ + J d2U/dJ2) 3 I_vol - 2 dU/dJ I_real +m66 L_vol_fixed(const double &dUdJ, const double &dU2dJ2, const double &J) { + const double k1 = (dUdJ+dU2dJ2*J)*3.; + const double k2 = 2.*dUdJ; + m66 L; + for (int j = 0; j < 6; ++j) { + for (int i = 0; i < 6; ++i) { + const double p1 = Ivol_at(i,j)*k1; + const double p2 = Ireal_at(i,j)*k2; + L(i,j) = p1 - p2; + } + } + return L; +} + +// N_k = n_k n_k^T, the eigenprojectors of the columns of n (n_k(a) n_k(b), as their outer product) +void eigen_projectors(std::vector &N_projectors, const mat &n) { + for (int k = 0; k < 3; k++) { + mat P(3,3); + for (int q = 0; q < 3; q++) + for (int p = 0; p < 3; p++) + P(p,q) = n(p,k)*n(q,k); + N_projectors[k] = P; + } +} + +} // namespace + +vec isochoric_invariants(const mat &b, const double &mJ) { + + const double J = J_of_b(b, mJ, "isochoric_invariants"); + vec I = zeros(3); + bar_invariants(b_bar_of(b, J), I(0), I(1)); I(2) = 1.; return I; } @@ -167,9 +417,7 @@ void pstretch(vec &lambda, mat &n_pvectors, const mat &input, const string &inpu void pstretch(vec &lambda, mat &n_pvectors, std::vector &N_projectors, const mat &input, const string &input_tensor, const double &mJ) { pstretch(lambda, n_pvectors, input, input_tensor, mJ); - N_projectors[0] = n_pvectors.col(0)*(n_pvectors.col(0)).t(); - N_projectors[1] = n_pvectors.col(1)*(n_pvectors.col(1)).t(); - N_projectors[2] = n_pvectors.col(2)*(n_pvectors.col(2)).t(); + eigen_projectors(N_projectors, n_pvectors); } void isochoric_pstretch(vec &lambda_bar, mat &n_pvectors, const mat &input, const string &input_tensor, const double &mJ) { @@ -215,9 +463,7 @@ void isochoric_pstretch(vec &lambda_bar, mat &n_pvectors, const mat &input, cons void isochoric_pstretch(vec &lambda_bar, mat &n_pvectors, std::vector &N_projectors, const mat &input, const string &input_tensor, const double &mJ) { isochoric_pstretch(lambda_bar, n_pvectors, input, input_tensor, mJ); - N_projectors[0] = n_pvectors.col(0)*(n_pvectors.col(0)).t(); - N_projectors[1] = n_pvectors.col(1)*(n_pvectors.col(1)).t(); - N_projectors[2] = n_pvectors.col(2)*(n_pvectors.col(2)).t(); + eigen_projectors(N_projectors, n_pvectors); } vec beta_coefs(const vec &dWdlambda_bar, const vec &lambda_bar) { @@ -338,26 +584,8 @@ vec delta_coefs(const vec &a_coefs, const vec &b_coefs, const mat &b) { }*/ mat tau_iso_hyper_invariants(const double &dWdI_1_bar, const double &dWdI_2_bar, const mat &b, const double &mJ) { - double J=mJ; - if (fabs(mJ) < simcoon::iota) { - try { - J = sqrt(det(b)); - } catch (const std::runtime_error &e) { - cerr << "Error in det: " << e.what() << endl; - throw simcoon::exception_det("Error in det function inside tau_iso_hyper_invariants."); - } - } - mat b_bar = pow(J,-2./3.)*b; - - mat b_bar2; - try { - b_bar2 = powmat(b_bar,2); - } catch (const std::runtime_error &e) { - cerr << "Error in det: " << e.what() << endl; - throw simcoon::exception_powmat("Error in powmat function inside tau_iso_hyper_invariants."); - } - - return 2.*dWdI_1_bar*dev(b_bar) + 2.*dWdI_2_bar*(trace(b_bar)*dev(b_bar) - dev(b_bar2)); + const double J = J_of_b(b, mJ, "tau_iso_hyper_invariants"); + return tau_iso_invariants_fixed(dWdI_1_bar, dWdI_2_bar, b_bar_of(b, J)); } mat tau_vol_hyper(const double &dUdJ, const mat &b, const double &mJ) { @@ -376,25 +604,12 @@ mat tau_vol_hyper(const double &dUdJ, const mat &b, const double &mJ) { } mat sigma_iso_hyper_invariants(const double &dWdI_1_bar, const double &dWdI_2_bar, const mat &b, const double &mJ) { - double J=mJ; - if (fabs(mJ) < simcoon::iota) { - try { - J = sqrt(det(b)); - } catch (const std::runtime_error &e) { - cerr << "Error in det: " << e.what() << endl; - throw simcoon::exception_det("Error in det function inside sigma_iso_hyper_invariants."); - } - } - mat b_bar = pow(J,-2./3.)*b; - mat b_bar2; - try { - b_bar2 = powmat(b_bar,2); - } catch (const std::runtime_error &e) { - cerr << "Error in det: " << e.what() << endl; - throw simcoon::exception_powmat("Error in powmat function inside sigma_iso_hyper_invariants."); - } - - return (1./J)*(2.*dWdI_1_bar*dev(b_bar) + 2.*dWdI_2_bar*(trace(b_bar)*dev(b_bar) - dev(b_bar2))); + const double J = J_of_b(b, mJ, "sigma_iso_hyper_invariants"); + const m33 tau = tau_iso_invariants_fixed(dWdI_1_bar, dWdI_2_bar, b_bar_of(b, J)); + const double invJ = 1./J; + mat sigma(3,3); + for (int e = 0; e < 9; ++e) sigma(e) = tau(e)*invJ; + return sigma; } mat sigma_vol_hyper(const double &dUdJ, const mat &b, const double &mJ) { @@ -466,15 +681,31 @@ mat L_iso_hyper_pstretch(const vec &dWdlambda_bar, const mat &dW2dlambda_bar2, c vec beta = beta_coefs(dWdlambda_bar, lambda_bar); mat gamma = gamma_coefs(dWdlambda_bar, dW2dlambda_bar2, lambda_bar); - mat c = zeros(6,6); mat delta = eye(3,3); + const m33 n(n_pvectors); + // c accumulated on the stack, the dyadic products built in place: the same products, rounded + // and summed in the same order as dyadic_4vectors_sym and auto_sym_dyadic (bit identity: see the + // note on the fixed-size kernels at the top of this file) + static const int Id[3][3] = {{0, 3, 4}, {3, 1, 5}, {4, 5, 2}}; + m66 c(fill::zeros); double factor = 0.; for(unsigned int i=0; i<3; i++) { for(unsigned int j=0; j<3; j++) { - c += (gamma(i,j) - 2*delta(i,j)*beta(i))*dyadic_4vectors_sym(n_pvectors.col(i), n_pvectors.col(j), "aabb") ; - } + const double coef = (gamma(i,j) - 2*delta(i,j)*beta(i)); + for (int p=0; p<3; p++) { + for (int q=p; q<3; q++) { + for (int k=0; k<3; k++) { + for (int l=k; l<3; l++) { + const double D = n(p,i)*n(q,i)*n(k,j)*n(l,j); // aabb dyadic of (n_i, n_j) + const double t = D*coef; + c(Id[p][q],Id[k][l]) = c(Id[p][q],Id[k][l]) + t; + } + } + } + } + } // ordered pairs a!=b: the symmetrized dyadic collapses to s s^T, s = sym dyadic of (n_i n_j^T + n_j n_i^T) for(unsigned int j=i+1; j<3; j++) { @@ -485,10 +716,28 @@ mat L_iso_hyper_pstretch(const vec &dWdlambda_bar, const mat &dW2dlambda_bar2, c factor = (beta(j)*pow(lambda_bar(i),2.) - beta(i)*pow(lambda_bar(j),2.))/(pow(lambda_bar(j),2.)-pow(lambda_bar(i),2.)); } - c += factor*auto_sym_dyadic(n_pvectors.col(i)*(n_pvectors.col(j)).t() + n_pvectors.col(j)*(n_pvectors.col(i)).t()); + m33 M; + for (int q=0; q<3; q++) { + for (int p=0; p<3; p++) { + const double m1 = n(p,i)*n(q,j); + const double m2 = n(p,j)*n(q,i); + M(p,q) = m1 + m2; + } + } + double s[6]; + sym_voigt(M, s); + for (int q=0; q<6; q++) { + for (int p=0; p<6; p++) { + const double t = (s[p]*s[q])*factor; + c(p,q) = c(p,q) + t; + } + } } } - return (1./J)*c; + const double invJ = 1./J; + mat L(6,6); + for (int e = 0; e < 36; ++e) L(e) = c(e)*invJ; + return L; } mat L_iso_hyper_pstretch(const vec &dWdlambda_bar, const mat &dW2dlambda_bar2, const mat &b, const double &mJ) { @@ -522,75 +771,16 @@ static mat gamma_quadratic(const mat &dev_A) { } mat L_iso_hyper_invariants(const double &dWdI_1_bar, const double &dWdI_2_bar, const double &dW2dI_11_bar, const double &dW2dI_12_bar, const double &dW2dI_22_bar, const mat &b, const double &mJ) { - double J=mJ; - if (fabs(mJ) < simcoon::iota) { - try { - J = sqrt(det(b)); - } catch (const std::runtime_error &e) { - cerr << "Error in det: " << e.what() << endl; - throw simcoon::exception_det("Error in det function inside L_iso_hyper_invariants."); - } - } - mat b_bar = pow(J,-2./3.)*b; - mat dev_b_bar = dev(b_bar); - vec I_bar = isochoric_invariants(b,J); - mat Id = eye(3,3); - - // no static arma object: its destructor runs at DLL unload (Windows) - mat I_dev = Idev(); - - // terms of a zero potential derivative skipped (exact zeros) - mat Lt = gamma_linear(dev_b_bar, I_bar(0), I_dev, Id)*dWdI_1_bar; - if ((dWdI_2_bar != 0.) || (dW2dI_12_bar != 0.) || (dW2dI_22_bar != 0.)) { - mat b_bar2; - try { - b_bar2 = powmat(b_bar,2); - } catch (const std::runtime_error &e) { - cerr << "Error in det: " << e.what() << endl; - throw simcoon::exception_powmat("Error in powmat function inside L_iso_hyper_invariants."); - } - mat dev_b_bar2 = dev(b_bar2); - mat devdevbb2 = I_bar(0)*dev(b_bar) - dev(b_bar2); - if (dWdI_2_bar != 0.) { - mat I_real = Ireal(); - mat I_vol = Ivol(); - mat H_bar = auto_sym_dyadic_operator(b_bar); - mat gamma_2 = (8./3.)*(I_bar(1)*(I_real - 2.*I_vol) - I_bar(0)*(sym_dyadic(dev_b_bar,Id)+sym_dyadic(Id,dev_b_bar)) - + (sym_dyadic(dev_b_bar2,Id)+sym_dyadic(Id,dev_b_bar2))) + 4*(auto_sym_dyadic(b_bar)-H_bar); - Lt += gamma_2*dWdI_2_bar; - } - if (dW2dI_11_bar != 0.) { - Lt += gamma_quadratic(dev_b_bar)*dW2dI_11_bar; - } - if (dW2dI_12_bar != 0.) { - mat gamma_12 = 4.*(sym_dyadic(dev_b_bar,devdevbb2)+sym_dyadic(devdevbb2,dev_b_bar)); - Lt += gamma_12*dW2dI_12_bar; - } - if (dW2dI_22_bar != 0.) { - mat gamma_22 = 4.*auto_sym_dyadic(devdevbb2); - Lt += gamma_22*dW2dI_22_bar; - } - } - else if (dW2dI_11_bar != 0.) { - Lt += gamma_quadratic(dev_b_bar)*dW2dI_11_bar; - } - - return (1./J)*Lt; + const double J = J_of_b(b, mJ, "L_iso_hyper_invariants"); + const m66 Lt = L_iso_invariants_fixed(dWdI_1_bar, dWdI_2_bar, dW2dI_11_bar, dW2dI_12_bar, dW2dI_22_bar, b_bar_of(b, J)); + const double invJ = 1./J; + mat L(6,6); + for (int e = 0; e < 36; ++e) L(e) = Lt(e)*invJ; + return L; } mat L_vol_hyper(const double &dUdJ, const double &dU2dJ2, const mat &b, const double &mJ) { - double J=mJ; - if (fabs(mJ) < simcoon::iota) { - try { - J = sqrt(det(b)); - } catch (const std::runtime_error &e) { - cerr << "Error in det: " << e.what() << endl; - throw simcoon::exception_det("Error in det function inside L_vol_hyper."); - } - } - mat I_real = Ireal(); // never static: see L_iso_hyper above - mat I_vol = Ivol(); - return (dUdJ+dU2dJ2*J)*3.*I_vol - 2.*dUdJ*I_real; + return L_vol_fixed(dUdJ, dU2dJ2, J_of_b(b, mJ, "L_vol_hyper")); } namespace { @@ -838,15 +1028,22 @@ void hyper_invariants_response(const hyper_invariants_dW &dW, const mat &b, cons } // Kirchhoff throughout: Cauchy = tau/J only at the output boundary - mat m_tau_iso = tau_iso_hyper_invariants(dW.dWdI_1_bar, dW.dWdI_2_bar, b, J); + const double Jb = J_of_b(b, J, "hyper_invariants_response"); + const m33 b_bar = b_bar_of(b, Jb); + mat m_tau_iso = tau_iso_invariants_fixed(dW.dWdI_1_bar, dW.dWdI_2_bar, b_bar); mat m_tau_vol = tau_vol_hyper(dW.dUdJ, b, J); mat m_tau = m_tau_iso + m_tau_vol; // builders return c = (1/J) d(L_v tau)/dD (not d(L_v sigma)/dD), with J written asymmetrically // (1/J in L_iso, none in L_vol): scale them together as a sum, never one at a time - mat Lt_iso = L_iso_hyper_invariants(dW.dWdI_1_bar, dW.dWdI_2_bar, dW.dW2dI_11_bar, dW.dW2dI_12_bar, dW.dW2dI_22_bar, b, J); - mat Lt_vol = L_vol_hyper(dW.dUdJ, dW.dU2dJ2, b, J); - mat Lt_spatial = Lt_iso + Lt_vol; + const m66 Lt_iso = L_iso_invariants_fixed(dW.dWdI_1_bar, dW.dWdI_2_bar, dW.dW2dI_11_bar, dW.dW2dI_12_bar, dW.dW2dI_22_bar, b_bar); + const m66 Lt_vol = L_vol_fixed(dW.dUdJ, dW.dU2dJ2, Jb); + const double invJb = 1./Jb; + m66 Lt_spatial; + for (int e = 0; e < 36; ++e) { + const double l_iso = Lt_iso(e)*invJb; + Lt_spatial(e) = l_iso + Lt_vol(e); + } // fibre terms: I_1 algebra with b_bar -> structure tensor (kappa_d folded into A); zero below the switch if (!A.empty()) { @@ -868,7 +1065,8 @@ void hyper_invariants_response(const hyper_invariants_dW &dW, const mat &b, cons // Box tangent Lt = d(tau_hat)/d(De) in corate_type (Kirchhoff, no J). J*Lt_spatial is the // Kirchhoff-Lie tangent d(L_v tau)/dD that the map consumes. - Lt_box = Dtau_LieDD_2_DtauDe_corate(J*Lt_spatial, corate_type, F, m_tau); + for (int e = 0; e < 36; ++e) Lt_spatial(e) = Lt_spatial(e)*J; + Lt_box = Dtau_LieDD_2_DtauDe_corate(Lt_spatial, corate_type, F, m_tau); } } //namespace simcoon diff --git a/src/Continuum_mechanics/Functions/objective_rates.cpp b/src/Continuum_mechanics/Functions/objective_rates.cpp index c253e51ac..12bac8b1a 100755 --- a/src/Continuum_mechanics/Functions/objective_rates.cpp +++ b/src/Continuum_mechanics/Functions/objective_rates.cpp @@ -70,12 +70,192 @@ static inline Fastor::Tensor pullback_4( return push_forward_4(C, invF); } -// Helper: 4th-order push-forward DSDE_ijkl = F_iL F_jH F_kM F_lN C_LHMN -static inline Fastor::Tensor pushforward_4( - const Fastor::Tensor &C, - const Fastor::Tensor &F) -{ - return push_forward_4(C, F); +typedef mat::fixed<3,3> m33; + +// Inline 3x3 products, the same arithmetic as armadillo's tiny-matrix kernel (each entry the +// three-term dot product A(i,0)*x0 + A(i,1)*x1 + A(i,2)*x2 written as it does) without its call and +// dispatch overhead. A chain A*B*C is (A*B)*C, as armadillo evaluates it for square operands. +// Bit identity with the armadillo expressions, here and in the Voigt-native push-forward and +// tangent corrections below (which reproduce the Fastor loops they replace), was established with +// Apple clang (-ffp-contract=on: a product is fused only with an addition of the same statement) +// and Armadillo 15.6.0 (the simcoon-prof environment), whose kernel shapes it relies on. A compiler +// that contracts across statements (GCC's default -ffp-contract=fast in gnu++ mode, on FMA targets) +// or another Armadillo may round differently, at the epsilon level; the tolerance-based tests +// remain the guarantee of correctness. +static inline m33 mm(const m33 &A, const m33 &B) { // A*B + m33 C; + for (int j = 0; j < 3; ++j) { + const double x0 = B(0,j), x1 = B(1,j), x2 = B(2,j); + for (int i = 0; i < 3; ++i) C(i,j) = A(i,0)*x0 + A(i,1)*x1 + A(i,2)*x2; + } + return C; +} +static inline m33 mtm(const m33 &A, const m33 &B) { // A^T*B + m33 C; + for (int j = 0; j < 3; ++j) { + const double x0 = B(0,j), x1 = B(1,j), x2 = B(2,j); + for (int i = 0; i < 3; ++i) C(i,j) = A(0,i)*x0 + A(1,i)*x1 + A(2,i)*x2; + } + return C; +} +static inline m33 mmt(const m33 &A, const m33 &B) { // A*B^T + m33 C; + for (int j = 0; j < 3; ++j) { + const double x0 = B(j,0), x1 = B(j,1), x2 = B(j,2); + for (int i = 0; i < 3; ++i) C(i,j) = A(i,0)*x0 + A(i,1)*x1 + A(i,2)*x2; + } + return C; +} + +// alpha*X entrywise: the scaling armadillo applies after a product carrying a scalar factor +static inline m33 alpha_times(const double alpha, const m33 &X) { + m33 Y; + for (int e = 0; e < 9; ++e) Y(e) = alpha*X(e); + return Y; +} + +// 2nd-order centered velocity gradient L = (2/dt)(F1-F0)(F0+F1)^-1, as the armadillo expression +// (the inverse throws std::runtime_error on a singular F0+F1) +static inline m33 velocity_gradient(const mat &F0, const mat &F1, const double &DTime) { + const m33 Fsum_inv = inv(F0+F1); + return alpha_times(2./DTime, mm(m33(F1-F0), Fsum_inv)); +} + +// Helper: push-forward of a stiffness-convention Voigt 6x6, C'_isrp = F_iL F_sJ F_rM F_pN C_LJMN, +// the same as push_forward_4(voigt_to_fastor4(L), F) (to the bit: see the note above): the four +// one-index contractions run in the same order over the same values, but only for the entries a +// Voigt result reads. C is symmetric in (M,N) exactly, and so are the first two partial +// contractions; only the pairs i <= s of the result are read. 198 contracted entries instead of +// 324, no 3x3x3x3 temporaries. +// R[is][r][p] = C'(i,s,r,p) for the Voigt pair is = (i <= s), unaveraged. +static void push_forward_voigt_core(double R[6][3][3], const mat::fixed<6,6> &L, const mat::fixed<3,3> &F) { + static const int vm[3][3] = {{0, 3, 4}, {3, 1, 5}, {4, 5, 2}}; // Voigt index of (i,j) + static const int vi[6] = {0, 1, 2, 0, 0, 1}, vj[6] = {0, 1, 2, 1, 2, 2}; + + // T1(i,J,MN) = sum_L F(i,L) C(L,J,M,N) + double T1[3][3][6]; + for (int i = 0; i < 3; ++i) + for (int J = 0; J < 3; ++J) + for (int MN = 0; MN < 6; ++MN) { + double sum = 0.0; + for (int l = 0; l < 3; ++l) + sum += F(i,l) * L(vm[l][J], MN); + T1[i][J][MN] = sum; + } + // T2(is,MN) = sum_J F(s,J) T1(i,J,MN), i <= s + double T2[6][6]; + for (int is = 0; is < 6; ++is) + for (int MN = 0; MN < 6; ++MN) { + const int i = vi[is], s = vj[is]; + double sum = 0.0; + for (int J = 0; J < 3; ++J) + sum += F(s,J) * T1[i][J][MN]; + T2[is][MN] = sum; + } + // T3(is,r,N) = sum_M F(r,M) T2(is,M,N) + double T3[6][3][3]; + for (int is = 0; is < 6; ++is) + for (int r = 0; r < 3; ++r) + for (int N = 0; N < 3; ++N) { + double sum = 0.0; + for (int M = 0; M < 3; ++M) + sum += F(r,M) * T2[is][vm[M][N]]; + T3[is][r][N] = sum; + } + // R(is,r,p) = sum_N F(p,N) T3(is,r,N) + for (int is = 0; is < 6; ++is) + for (int r = 0; r < 3; ++r) + for (int p = 0; p < 3; ++p) { + double sum = 0.0; + for (int N = 0; N < 3; ++N) + sum += F(p,N) * T3[is][r][N]; + R[is][r][p] = sum; + } +} + +// The Voigt average of the minor pair (r,p), as fastor4_to_voigt +static mat::fixed<6,6> voigt_average(const double R[6][3][3]) { + static const int vi[6] = {0, 1, 2, 0, 0, 1}, vj[6] = {0, 1, 2, 1, 2, 2}; + mat::fixed<6,6> out; + for (int I = 0; I < 6; ++I) + for (int J = 0; J < 6; ++J) + out(I, J) = 0.5 * (R[I][vi[J]][vj[J]] + R[I][vj[J]][vi[J]]); + return out; +} + +// fastor4_to_voigt(push_forward_4(voigt_to_fastor4(L), F)) (to the bit, see the note above) +static mat::fixed<6,6> push_forward_voigt(const mat::fixed<6,6> &L, const mat::fixed<3,3> &F) { + double R[6][3][3]; + push_forward_voigt_core(R, L, F); + return voigt_average(R); +} + +// The Jaumann correction of apply_jaumann_correction on the entries a Voigt result reads: +// R[is][p][r] += sign*corr(i,s,p,r), the same terms added in the same order. tau_f is indexed as +// the Fastor copy of tau (to_fastor2 with the symmetric memcpy: tau_f(a,b) = tau(b,a)). +static void add_jaumann_correction(double R[6][3][3], const mat::fixed<3,3> &tau_f, const bool add) { + static const int vi[6] = {0, 1, 2, 0, 0, 1}, vj[6] = {0, 1, 2, 1, 2, 2}; + const double sign = add ? 0.5 : -0.5; + for (int is = 0; is < 6; ++is) { + const int i = vi[is], s = vj[is]; + for (int p = 0; p < 3; ++p) + for (int r = 0; r < 3; ++r) { + double corr = 0.0; + if (i == r) corr += tau_f(p,s); + if (i == p) corr += tau_f(r,s); + if (s == p) corr += tau_f(i,r); + if (s == r) corr += tau_f(i,p); + R[is][p][r] += sign * corr; + } + } +} + +// R[is][p][r] = A(i,s,p,r) of voigt_to_fastor4(L), for the Voigt pairs is = (i <= s) +static void voigt_expand(double R[6][3][3], const mat::fixed<6,6> &L) { + static const int vm[3][3] = {{0, 3, 4}, {3, 1, 5}, {4, 5, 2}}; + for (int is = 0; is < 6; ++is) + for (int p = 0; p < 3; ++p) + for (int r = 0; r < 3; ++r) + R[is][p][r] = L(is, vm[p][r]); +} + +// The B-correction of apply_B_correction on the entries a Voigt result reads (pairs i <= j): +// R[ij][k][l] = A(i,j,k,l) + sign*(sum1 + sum2), with B - I and the sums as there; A and B are +// stiffness-convention Voigt 6x6, tau_f indexed as the Fastor copy of tau. +static void add_B_correction(double R[6][3][3], const mat::fixed<6,6> &A, const mat::fixed<6,6> &B, const mat::fixed<3,3> &tau_f, const bool add) { + static const int vm[3][3] = {{0, 3, 4}, {3, 1, 5}, {4, 5, 2}}; + static const int vi[6] = {0, 1, 2, 0, 0, 1}, vj[6] = {0, 1, 2, 1, 2, 2}; + // B - I, I the symmetric 4th-order identity of sym_identity_4 (entries 0, 0.5 or 1) + auto BmI = [&](const int a, const int b, const int c, const int d) { + double I = 0.; + if (c == a && d == b) I += 0.5; + if (c == b && d == a) I += 0.5; + return B(vm[a][b], vm[c][d]) - I; + }; + const double sign = add ? 1.0 : -1.0; + for (int ij = 0; ij < 6; ++ij) { + const int i = vi[ij], j = vj[ij]; + for (int k = 0; k < 3; ++k) + for (int l = 0; l < 3; ++l) { + double sum1 = 0.0, sum2 = 0.0; + for (int p = 0; p < 3; ++p) { + sum1 += BmI(i,p,k,l) * tau_f(p,j); + sum2 += tau_f(i,p) * BmI(j,p,k,l); + } + double r = A(ij, vm[k][l]); + r += sign * (sum1 + sum2); + R[ij][k][l] = r; + } + } +} + +// The Fastor copy of a symmetric 3x3 (to_fastor2 with the symmetric memcpy), as an arma matrix +static inline mat::fixed<3,3> fastor_sym_copy(const mat &tau) { + return mat::fixed<3,3>(tau).t(); +} + +static inline bool is_exactly_symmetric(const mat &X) { + return X(0,1) == X(1,0) && X(0,2) == X(2,0) && X(1,2) == X(2,1); } // Helper: B-correction for objective rate conversions @@ -185,7 +365,7 @@ void Jaumann(mat &DR, mat &D, mat &W, const double &DTime, const mat &F0, const if(DTime > simcoon::iota) { try { // 2nd-order midpoint L = (2/dt)(F1-F0)(F0+F1)^-1, shared by every rate so D = sym(L) agrees - L = (2./DTime)*(F1-F0)*inv(F0+F1); + L = velocity_gradient(F0, F1, DTime); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside Jaumann (L)."); @@ -206,7 +386,7 @@ void Green_Naghdi(mat &DR, mat &D, mat &Omega, const double &DTime, const mat &F try { // Same 2nd-order centered velocity gradient as the other rate functions (see Jaumann), // so that D=sym(L) matches across all rates. - L = (2./DTime)*(F1-F0)*inv(F0+F1); + L = velocity_gradient(F0, F1, DTime); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside Green_Naghdi (L)."); @@ -227,7 +407,7 @@ void logarithmic_R_kinematics(mat &DR, mat &D, mat &Omega, vec::fixed<3> &b1, ma if(DTime > simcoon::iota) { try { // 2nd-order centered velocity gradient (see Jaumann); D=sym(L) is shared by all rates. - L = (2./DTime)*(F1-F0)*inv(F0+F1); + L = velocity_gradient(F0, F1, DTime); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside logarithmic_R (L)."); @@ -242,8 +422,8 @@ void logarithmic_R_kinematics(mat &DR, mat &D, mat &Omega, vec::fixed<3> &b1, ma vec::fixed<3> c0; RU_spectral(R0, c0, N0, F0); RU_spectral(R1, b1, N1, F1); - DR = R1*R0.t(); - Nb = R1*N1; // B1 = R1 C1 R1^T + DR = mmt(R1, R0); + Nb = mm(R1, N1); // B1 = R1 C1 R1^T if (DTime <= simcoon::iota) { Omega = zeros(3,3); @@ -254,7 +434,7 @@ void logarithmic_R_kinematics(mat &DR, mat &D, mat &Omega, vec::fixed<3> &b1, ma if (!inv(DRpI_inv, DRpI)) { throw simcoon::exception_inv("Error in inv function inside finite_rotation."); } - Omega = (2./DTime)*(DR-eye(3,3))*DRpI_inv; + Omega = alpha_times(2./DTime, mm(m33(DR-eye(3,3)), DRpI_inv)); } // De = A^R : D on the eigenbasis (b, Nb) of B, by congruence (t/sinh t kernel) @@ -262,7 +442,7 @@ mat::fixed<3,3> A_R_D(const vec::fixed<3> &b, const mat::fixed<3,3> &Nb, const m mat::fixed<3,3> K; for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) - K(i, j) = kappa_R(b(i), b(j)); + K(i, j) = (i != j) ? kappa_R(b(i), b(j)) : 1.; // kappa_R(b, b) = 1 exactly return spectral_congruence(Nb, K, D); } @@ -281,7 +461,7 @@ void logarithmic_F(mat &DF, mat &N_1, mat &N_2, mat &D, mat &L, const double &DT try { // 2nd-order centered velocity gradient (see Jaumann): D=sym(L) is shared by all rates, // and exp(L*dt) approximates F1 F0^-1 used below for the transport DF. - L = (2./DTime)*(F1-F0)*inv(F0+F1); + L = velocity_gradient(F0, F1, DTime); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside logarithmic_F (L)."); @@ -306,7 +486,7 @@ void Truesdell(mat &DF, mat &D, mat &L, const double &DTime, const mat &F0, cons try { // 2nd-order centered velocity gradient (see Jaumann): D=sym(L) is shared by all rates, // and exp(L*dt) approximates F1 F0^-1 used below for the transport DF. - L = (2./DTime)*(F1-F0)*inv(F0+F1); + L = velocity_gradient(F0, F1, DTime); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside Truesdell (L)."); @@ -393,7 +573,7 @@ mat A_F(const mat &F) { mat::fixed<6,6> AF(fill::zeros); for (unsigned int i=0; i<3; i++) { for (unsigned int j=0; j<3; j++) { - double t = 0.5*log(bi(i)/bi(j)); + double t = (i != j) ? 0.5*log(bi(i)/bi(j)) : 0.; // log(b/b) = 0 exactly double tcoth; if (fabs(t) > 1.e-4) { tcoth = t/tanh(t); @@ -414,7 +594,7 @@ void logarithmic(mat &DR, mat &D, mat &Omega, const double &DTime, const mat &F0 if(DTime > simcoon::iota) { try { // 2nd-order centered velocity gradient (see Jaumann); D=sym(L) is shared by all rates. - L = (2./DTime)*(F1-F0)*inv(F0+F1); + L = velocity_gradient(F0, F1, DTime); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside logarithmic (L)."); @@ -439,8 +619,8 @@ void logarithmic(mat &DR, mat &D, mat &Omega, const double &DTime, const mat &F0 } mat Delta_log_strain(const mat &D, const mat &Omega, const double &DTime) { - const mat DR = Hughes_Winget(Omega, DTime); - return 0.5*(D+(DR*D*DR.t()))*DTime; + const m33 DR = Hughes_Winget(Omega, DTime), D_(D); + return 0.5*(D_+mmt(mm(DR, D_), DR))*DTime; } mat Delta_log_strain_F(const mat &D, const mat &L, const double &DTime) { @@ -474,7 +654,8 @@ void corate_kinematics(const int &corate_type, mat &DR, mat &D, mat &Omega, mat mat::fixed<3,3> Nb; logarithmic_R_kinematics(DR, D, Omega, b1, Nb, DTime, F0, F1); const mat::fixed<3,3> Dh = A_R_D(b1, Nb, D); - De = 0.5*(Dh + DR*Dh*DR.t())*DTime; + const m33 DR_(DR); + De = 0.5*(Dh + mmt(mm(DR_, Dh), DR_))*DTime; return; } corate_kinematics(corate_type, DR, D, Omega, F0, F1, DTime); @@ -531,7 +712,7 @@ double Delta_work_conjugacy(const vec &tau_start, const vec &tau_start_tr, const mat::fixed<3,3> Fsum_inv; if (!inv(Fsum_inv, mat::fixed<3,3>(F1 + F0))) return 0.; - const mat::fixed<3,3> LDt = 2.*(F1 - F0)*Fsum_inv; // midpoint velocity gradient times DTime + const mat::fixed<3,3> LDt = alpha_times(2., mm(m33(F1 - F0), Fsum_inv)); // midpoint velocity gradient times DTime // both stresses in the lab frame: a start stress carried to the end frame against a midpoint // D would leave a first-order [W, tau] : D error, even for isotropic elasticity return 0.5*dot(tau_start + tau, t2v_strain(0.5*(LDt + LDt.t()))) @@ -548,12 +729,12 @@ struct dk_spectral { arma::mat::fixed<3,3> N; // eigenvectors of C (columns) arma::mat::fixed<3,3> G_h; // DK coefficients of h(C) = 1/2 ln C arma::mat::fixed<3,3> G_iU; // DK coefficients of C^{-1/2} - arma::mat::fixed<3,3> U; // C^{1/2} + arma::mat::fixed<3,3> U; // C^{1/2} (built when asked: only the inverse map reads it) arma::mat::fixed<3,3> invU; // C^{-1/2} arma::mat::fixed<3,3> R; // polar rotation F U^{-1} arma::mat::fixed<3,3> tau_hat; // corotational Kirchhoff stress R^T tau R - void build(const arma::mat &F, const arma::mat &tau) { + void build(const arma::mat &F, const arma::mat &tau, const bool &with_U = true) { const arma::mat::fixed<3,3> F_(F); const bool success_eig_sym = eig_sym_3x3(lam, N, F_.t()*F_); if (!success_eig_sym) { @@ -569,10 +750,10 @@ struct dk_spectral { [](double l) { return 0.5 / l; }); G_iU = gmat([](double l) { return 1.0 / std::sqrt(l); }, [](double l) { return -0.5 * std::pow(l, -1.5); }); - U = spectral_compose_3x3(N, sqrt(lam)); + if (with_U) U = spectral_compose_3x3(N, sqrt(lam)); invU = spectral_compose_3x3(N, 1.0 / sqrt(lam)); - R = F_ * invU; - tau_hat = R.t() * arma::mat::fixed<3,3>(tau) * R; + R = mm(F_, invU); + tau_hat = mm(mtm(R, arma::mat::fixed<3,3>(tau)), R); } // f' at the midpoint under the 1e-4 relative coincidence guard (as get_BBBB / A_R) @@ -598,6 +779,7 @@ struct dk_spectral { // stack and the 6x6 box applied by hand (a BLAS gemv call per column otherwise). typedef arma::mat::fixed<3,3> m33; + // engineering-Voigt unit strain e_j as a tensor (v2t_strain of the j-th basis vector) inline m33 unit_strain_tensor(const int j) { static const int ia[6] = {0, 1, 2, 0, 0, 1}, ib[6] = {0, 1, 2, 1, 2, 2}; @@ -686,11 +868,19 @@ struct logU_spectral { R = F_*spectral_compose_3x3(N, 1.0/sqrt(c)); for (int a = 0; a < 3; ++a) { for (int b = 0; b < 3; ++b) { - kappa(a, b) = kappa_of(c(a), c(b)); + kappa(a, b) = (a != b) ? kappa_of(c(a), c(b)) : 1.; // kappa_of(c, c) = 1 exactly if (!tangent) continue; G(a, b) = kappa(a, b)/(2.*std::sqrt(c(a)*c(b))); - for (int k = 0; k < 3; ++k) { - Gam[a][b][k] = dd2(c(a), c(b), c(k)); + } + } + if (!tangent) return; + // dd2 sorts its arguments, so Gam is the same, to the bit, for every permutation of + // (a, b, k): the 10 index multisets are evaluated once each, instead of 27 calls + for (int a = 0; a < 3; ++a) { + for (int b = a; b < 3; ++b) { + for (int k = b; k < 3; ++k) { + const double g = dd2(c(a), c(b), c(k)); + Gam[a][b][k] = Gam[a][k][b] = Gam[b][a][k] = Gam[b][k][a] = Gam[k][a][b] = Gam[k][b][a] = g; } } } @@ -723,13 +913,16 @@ mat logU_box_2_DSDE(const mat &Lt, const mat &F, const mat &tau) { logU_spectral sp; sp.build(F, true); const mat That = sp.That_of_tau(tau); + const arma::mat::fixed<6,6> Lt_(Lt); mat DSDE(6, 6); vec ej(6); for (int j = 0; j < 6; ++j) { ej.zeros(); ej(j) = 1.; const mat dCp = sp.N.t()*(2.*v2t_strain(ej))*sp.N; const mat dH = sp.N*(sp.G % dCp)*sp.N.t(); - const mat dTp = sp.N.t()*v2t_stress(Lt*t2v_strain(dH))*sp.N; + double y[6]; + box_apply(Lt_, dH, y); // Lt t2v_strain(dH) by hand (a BLAS gemv call per column otherwise) + const mat dTp = sp.N.t()*v2t_stress_arr(y)*sp.N; DSDE.col(j) = t2v_stress(2.*sp.N*(sp.G % dTp + sp.geometric(That, dCp))*sp.N.t()); } return DSDE; @@ -740,12 +933,15 @@ mat DSDE_2_logU_box(const mat &DSDE, const mat &F, const mat &tau) { logU_spectral sp; sp.build(F, true); const mat That = sp.That_of_tau(tau); + const arma::mat::fixed<6,6> DSDE_(DSDE); mat Lt(6, 6); vec ej(6); for (int j = 0; j < 6; ++j) { ej.zeros(); ej(j) = 1.; const mat dCp = (sp.N.t()*v2t_strain(ej)*sp.N)/sp.G; - const mat dS = v2t_stress(DSDE*t2v_strain(0.5*sp.N*dCp*sp.N.t())); + double y[6]; + box_apply(DSDE_, m33(0.5*sp.N*dCp*sp.N.t()), y); // DSDE t2v_strain(.) by hand, as above + const mat dS = v2t_stress_arr(y); const mat dTp = (0.5*sp.N.t()*dS*sp.N - sp.geometric(That, dCp))/sp.G; Lt.col(j) = t2v_stress(sp.N*dTp*sp.N.t()); } @@ -770,22 +966,23 @@ mat Kirchoff2LogStressU(const mat &tau, const mat &F) { mat DtauDe_2_DSDE(const mat &Lt, const mat &F, const mat &tau){ dk_spectral sp; - sp.build(F, tau); + sp.build(F, tau, false); const arma::mat::fixed<6,6> Lt_(Lt); + const m33 invU_tau_hat = mm(sp.invU, sp.tau_hat); // column-independent factor of dS mat DSDE(6, 6); double y[6]; for (int j = 0; j < 6; ++j) { const m33 dC = 2. * unit_strain_tensor(j); // engineering Voigt basis - const m33 dCp = sp.N.t() * dC * sp.N; // shared eigenbasis transform - const m33 dh = sp.N * (sp.G_h % dCp) * sp.N.t(); // d(1/2 ln C) = d(ln U) + const m33 dCp = mm(mtm(sp.N, dC), sp.N); // shared eigenbasis transform + const m33 dh = mmt(mm(sp.N, m33(sp.G_h % dCp)), sp.N); // d(1/2 ln C) = d(ln U) // The box acts in the spatial frame (d ln V = R d(ln U) R^T at fixed R): // conjugate in, apply Lt, conjugate back to the corotational frame. - box_apply(Lt_, sp.R * dh * sp.R.t(), y); - const m33 dtau_hat = sp.R.t() * v2t_stress_arr(y) * sp.R; - const m33 dinvU = sp.N * (sp.G_iU % dCp) * sp.N.t(); // d(C^{-1/2}) - const m33 dS = sp.invU * dtau_hat * sp.invU - + dinvU * sp.tau_hat * sp.invU + sp.invU * sp.tau_hat * dinvU; + box_apply(Lt_, mmt(mm(sp.R, dh), sp.R), y); + const m33 dtau_hat = mm(mtm(sp.R, v2t_stress_arr(y)), sp.R); + const m33 dinvU = mmt(mm(sp.N, m33(sp.G_iU % dCp)), sp.N); // d(C^{-1/2}) + const m33 dS = mm(mm(sp.invU, dtau_hat), sp.invU) + + mm(mm(dinvU, sp.tau_hat), sp.invU) + mm(invU_tau_hat, dinvU); set_stress_col(DSDE, j, dS); } return DSDE; @@ -799,12 +996,9 @@ mat Dtau_LieDD_2_DSDE(const mat &Lt, const mat &F){ } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside Dtau_LieDD_2_DSDE."); - } + } - auto invF_ = to_fastor2(invF, false); - auto Dtau_LieDD = to_fastor4(Lt); - auto DSDE = pullback_4(Dtau_LieDD, invF_); - return fastor4_to_voigt(DSDE); + return push_forward_voigt(mat::fixed<6,6>(Lt), mat::fixed<3,3>(invF)); // pull-back by F^-1 } mat DtauDe_JaumannDD_2_DSDE(const mat &Lt, const mat &F, const mat &tau){ @@ -816,6 +1010,22 @@ mat DtauDe_JaumannDD_2_DSDE(const mat &Lt, const mat &F, const mat &tau){ cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside DtauDe_JaumannDD_2_DSDE."); } + if (is_exactly_symmetric(tau)) { + // Dtau_LieDD = Dtau_JaumannDD - Jaumann correction: with a symmetric tau it keeps both minor + // symmetries exactly, so its Voigt form (the corrected entries i <= s) pulled back is the + // Fastor route (to the bit with Apple clang, see the note on the inline products) + double R[6][3][3]; + voigt_expand(R, mat::fixed<6,6>(Lt)); + add_jaumann_correction(R, fastor_sym_copy(tau), false); + static const int vm[3][3] = {{0, 3, 4}, {3, 1, 5}, {4, 5, 2}}; + mat::fixed<6,6> Dtau_LieDD; + for (int is = 0; is < 6; ++is) + for (int p = 0; p < 3; ++p) + for (int r = p; r < 3; ++r) + Dtau_LieDD(is, vm[p][r]) = R[is][p][r]; + return push_forward_voigt(Dtau_LieDD, mat::fixed<3,3>(invF)); // pull-back by F^-1 + } + auto invF_ = to_fastor2(invF, false); auto tau_ = to_fastor2(tau); auto Dtau_JaumannDD = to_fastor4(Lt); @@ -872,6 +1082,20 @@ mat DtauDe_GreenNaghdiDD_2_DSDE(const mat &Lt, const mat &F, const mat &tau){ cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside DtauDe_GreenNaghdiDD_2_DSDE."); } + if (is_exactly_symmetric(tau)) { + // with a symmetric tau the corrected tangent keeps both minor symmetries exactly: its Voigt + // form (the corrected entries i <= j), pulled back, is the Fastor route (to the bit with + // Apple clang, see the note on the inline products) + double R[6][3][3]; + add_B_correction(R, mat::fixed<6,6>(Lt), mat::fixed<6,6>(get_BBBB_GN(F)), fastor_sym_copy(tau), true); + static const int vm[3][3] = {{0, 3, 4}, {3, 1, 5}, {4, 5, 2}}; + mat::fixed<6,6> Dtau_LieDD; + for (int ij = 0; ij < 6; ++ij) + for (int k = 0; k < 3; ++k) + for (int l = k; l < 3; ++l) + Dtau_LieDD(ij, vm[k][l]) = R[ij][k][l]; + return push_forward_voigt(Dtau_LieDD, mat::fixed<3,3>(invF)); // pull-back by F^-1 + } auto invF_ = to_fastor2(invF, false); auto tau_ = to_fastor2(tau); auto Dtau_GN = to_fastor4(Lt); @@ -912,20 +1136,21 @@ mat DSDE_2_DtauDe(const mat &DSDE, const mat &F, const mat &tau) { const m33 G_h_inv = 1.0 / sp.G_h; // elementwise; G_h > 0 (1/2 ln strictly increasing) const m33 G_comp = sp.G_iU % G_h_inv; // composed d(C^{-1/2}) o [d(1/2 ln C)]^{-1} const arma::mat::fixed<6,6> DSDE_(0.5 * DSDE); + const m33 invU_tau_hat = mm(sp.invU, sp.tau_hat); // column-independent factor mat Lt(6, 6); double y[6]; for (int j = 0; j < 6; ++j) { // box argument is ln V: pull dh back by R (inverse of the forward R conjugation) - const m33 dh = sp.R.t() * unit_strain_tensor(j) * sp.R; - const m33 dhp = sp.N.t() * dh * sp.N; // eigenframe - const m33 dC = sp.N * (G_h_inv % dhp) * sp.N.t(); // inverse of d(1/2 ln C) + const m33 dh = mm(mtm(sp.R, unit_strain_tensor(j)), sp.R); + const m33 dhp = mm(mtm(sp.N, dh), sp.N); // eigenframe + const m33 dC = mmt(mm(sp.N, m33(G_h_inv % dhp)), sp.N); // inverse of d(1/2 ln C) box_apply(DSDE_, dC, y); const m33 dS = v2t_stress_arr(y); - const m33 dinvU = sp.N * (G_comp % dhp) * sp.N.t(); - const m33 dtau_hat = - sp.U * (dS - dinvU * sp.tau_hat * sp.invU - sp.invU * sp.tau_hat * dinvU) * sp.U; - set_stress_col(Lt, j, sp.R * dtau_hat * sp.R.t()); + const m33 dinvU = mmt(mm(sp.N, m33(G_comp % dhp)), sp.N); + const m33 inner = dS - mm(mm(dinvU, sp.tau_hat), sp.invU) - mm(invU_tau_hat, dinvU); + const m33 dtau_hat = mm(mm(sp.U, inner), sp.U); + set_stress_col(Lt, j, mmt(mm(sp.R, dtau_hat), sp.R)); } return Lt; } @@ -944,10 +1169,7 @@ mat DSDE_2_DsigmaDe(const mat &DSDE, const mat &F, const mat &sigma) { mat DSDE_2_Dtau_LieDD(const mat &DSDE, const mat &F) { - auto F_ = to_fastor2(F, false); - auto DSDE_ = to_fastor4(DSDE); - auto C = pushforward_4(DSDE_, F_); - return fastor4_to_voigt(C); + return push_forward_voigt(mat::fixed<6,6>(DSDE), mat::fixed<3,3>(F)); } mat DSDE_2_Dsigma_LieDD(const mat &DSDE, const mat &F) { @@ -959,19 +1181,17 @@ mat DSDE_2_Dsigma_LieDD(const mat &DSDE, const mat &F) { cerr << "Error in det: " << e.what() << endl; throw simcoon::exception_det("Error in det function inside DSDE_2_DsigmaDe_LieDD."); } - return (1./J)*DSDE_2_Dtau_LieDD(DSDE, F); + return (1./J)*push_forward_voigt(mat::fixed<6,6>(DSDE), mat::fixed<3,3>(F)); // DSDE_2_Dtau_LieDD / J } mat DSDE_2_Dtau_JaumannDD(const mat &DSDE, const mat &F, const mat &tau) { - auto F_ = to_fastor2(F, false); - auto tau_ = to_fastor2(tau); - auto DSDE_ = to_fastor4(DSDE); - - // Push-forward + Jaumann correction - auto Dtau_LieDD = pushforward_4(DSDE_, F_); - auto C = apply_jaumann_correction(Dtau_LieDD, tau_, true); - return fastor4_to_voigt(C); + // Push-forward + Jaumann correction, on the Voigt entries only (the same sums as the + // 3x3x3x3 Fastor route) + double R[6][3][3]; + push_forward_voigt_core(R, mat::fixed<6,6>(DSDE), mat::fixed<3,3>(F)); + add_jaumann_correction(R, fastor_sym_copy(tau), true); + return voigt_average(R); } mat DSDE_2_Dsigma_JaumannDD(const mat &DSDE, const mat &F, const mat &sigma) { @@ -1112,7 +1332,7 @@ mat Dtau_LieDD_2_DtauDe_stretch(const mat &Dtau_LieDD, const vec::fixed<3> &e, c for (int j = 0; j < 6; ++j) { const m33 dt = spectral_congruence(N, H, unit_strain_tensor(j)); box_apply(Jc, dt, y); - const m33 dtau = v2t_stress_arr(y) + dt * tau_ + tau_ * dt; + const m33 dtau = v2t_stress_arr(y) + mm(dt, tau_) + mm(tau_, dt); set_stress_col(Lt, j, dtau); } return Lt; @@ -1126,21 +1346,20 @@ mat box_DtauDe_from_dSdE(const mat &dSdE, const mat &F, const vec &sigma) { mat Dtau_LieDD_Dtau_JaumannDD(const mat &Dtau_LieDD, const mat &tau) { - auto tau_ = to_fastor2(tau); - auto Dtau_LieDD_ = to_fastor4(Dtau_LieDD); - auto result = apply_jaumann_correction(Dtau_LieDD_, tau_, true); - return fastor4_to_voigt(result); + // the correction on the Voigt entries only (the same sums as the 3x3x3x3 Fastor route) + double R[6][3][3]; + voigt_expand(R, mat::fixed<6,6>(Dtau_LieDD)); + add_jaumann_correction(R, fastor_sym_copy(tau), true); + return voigt_average(R); } mat Dtau_LieDD_Dtau_objectiveDD(const mat &Dtau_LieDD, const mat &B, const mat &tau) { - auto tau_ = to_fastor2(tau); - auto Dtau_LieDD_ = to_fastor4(Dtau_LieDD); - auto B_ = to_fastor4(B); - - // Dtau_objectiveDD = Dtau_LieDD - B-correction - auto result = apply_B_correction(Dtau_LieDD_, B_, tau_, false); - return fastor4_to_voigt(result); + // Dtau_objectiveDD = Dtau_LieDD - B-correction, on the Voigt entries only (the same sums as + // the 3x3x3x3 Fastor route) + double R[6][3][3]; + add_B_correction(R, mat::fixed<6,6>(Dtau_LieDD), mat::fixed<6,6>(B), fastor_sym_copy(tau), false); + return voigt_average(R); } mat Dtau_LieDD_Dtau_GreenNaghdiDD(const mat &Dtau_LieDD, const mat &F, const mat &tau) { @@ -1157,21 +1376,19 @@ mat Dtau_LieDD_Dtau_logarithmicDD(const mat &Dtau_LieDD, const mat &F, const mat mat Dsigma_LieDD_Dsigma_JaumannDD(const mat &Dsigma_LieDD, const mat &sigma) { - auto sigma_ = to_fastor2(sigma); - auto Dsigma_LieDD_ = to_fastor4(Dsigma_LieDD); - auto result = apply_jaumann_correction(Dsigma_LieDD_, sigma_, true); - return fastor4_to_voigt(result); + // as Dtau_LieDD_Dtau_JaumannDD + double R[6][3][3]; + voigt_expand(R, mat::fixed<6,6>(Dsigma_LieDD)); + add_jaumann_correction(R, fastor_sym_copy(sigma), true); + return voigt_average(R); } mat Dsigma_LieDD_Dsigma_objectiveDD(const mat &Dsigma_LieDD, const mat &B, const mat &sigma) { - auto sigma_ = to_fastor2(sigma); - auto Dsigma_LieDD_ = to_fastor4(Dsigma_LieDD); - auto B_ = to_fastor4(B); - - // Dsigma_objectiveDD = Dsigma_LieDD - B-correction - auto result = apply_B_correction(Dsigma_LieDD_, B_, sigma_, false); - return fastor4_to_voigt(result); + // Dsigma_objectiveDD = Dsigma_LieDD - B-correction, as Dtau_LieDD_Dtau_objectiveDD + double R[6][3][3]; + add_B_correction(R, mat::fixed<6,6>(Dsigma_LieDD), mat::fixed<6,6>(B), fastor_sym_copy(sigma), false); + return voigt_average(R); } mat Dsigma_LieDD_Dsigma_GreenNaghdiDD(const mat &Dsigma_LieDD, const mat &F, const mat &sigma) { @@ -1196,35 +1413,50 @@ mat Dsigma_LieDD_Dsigma_logarithmicDD(const mat &Dsigma_LieDD, const mat &F, con mat DSDE_DBiotStressDU(const mat &DSDE, const mat &U, const mat &S) { - auto U_ = to_fastor2(U); - auto S_ = to_fastor2(S); - auto DSDE_ = to_fastor4(DSDE); - // C_sjpr = 0.5*δ_is*(U_lj*U_mp*δ_nr*DSDE_ilmn + U_lj*δ_mr*U_np*DSDE_ilmn) // + 0.5*S_sp*δ_rj + 0.5*S_rj*δ_sp - Fastor::Tensor C; - C.zeros(); - for (int s = 0; s < 3; ++s) - for (int j = 0; j < 3; ++j) - for (int p = 0; p < 3; ++p) - for (int r = 0; r < 3; ++r) { - double sum = 0.0; - // i=s (delta_is), contract over l,m,n - for (int l = 0; l < 3; ++l) - for (int m = 0; m < 3; ++m) - for (int n = 0; n < 3; ++n) { - double dsde_val = DSDE_(s,l,m,n); - // term1: U_lj * U_mp * δ_nr - if (n == r) sum += 0.5 * U_(l,j) * U_(m,p) * dsde_val; - // term2: U_lj * δ_mr * U_np - if (m == r) sum += 0.5 * U_(l,j) * U_(n,p) * dsde_val; + // Computed straight from the Voigt DSDE and for the entries the Voigt result reads only + // (s <= j), visiting the (l, m, n) terms that the deltas keep in their original order: the + // same sums as the full 3x3x3x3 Fastor contraction this replaces (to the bit with Apple clang, + // see the note on the inline products). + static const int vm[3][3] = {{0, 3, 4}, {3, 1, 5}, {4, 5, 2}}; + static const int vi[6] = {0, 1, 2, 0, 0, 1}, vj[6] = {0, 1, 2, 1, 2, 2}; + const mat::fixed<6,6> D(DSDE); + // U and S as the Fastor copies read them (a plain memcpy of the column-major storage) + const mat::fixed<3,3> U_t = mat::fixed<3,3>(U).t(), S_t = mat::fixed<3,3>(S).t(); + double C[6][3][3]; + for (int I = 0; I < 6; ++I) { + const int s = vi[I], j = vj[I]; + for (int p = 0; p < 3; ++p) { + for (int r = 0; r < 3; ++r) { + double sum = 0.0; + for (int l = 0; l < 3; ++l) { + for (int m = 0; m < 3; ++m) { + if (m != r) { // only n == r contributes + const int n = r; + const double dsde_val = D(vm[s][l], vm[m][n]); + sum += 0.5 * U_t(l,j) * U_t(m,p) * dsde_val; + continue; + } + for (int n = 0; n < 3; ++n) { + const double dsde_val = D(vm[s][l], vm[m][n]); + if (n == r) sum += 0.5 * U_t(l,j) * U_t(m,p) * dsde_val; + sum += 0.5 * U_t(l,j) * U_t(n,p) * dsde_val; + } + } + } + // Jaumann-like terms + if (r == j) sum += 0.5 * S_t(s,p); + if (s == p) sum += 0.5 * S_t(r,j); + C[I][p][r] = sum; + } } - // Jaumann-like terms - if (r == j) sum += 0.5 * S_(s,p); - if (s == p) sum += 0.5 * S_(r,j); - C(s,j,p,r) = sum; } - return fastor4_to_voigt(C); + mat L(6,6); + for (int I = 0; I < 6; ++I) + for (int J = 0; J < 6; ++J) + L(I, J) = 0.5 * (C[I][vi[J]][vj[J]] + C[I][vj[J]][vi[J]]); + return L; } diff --git a/src/Continuum_mechanics/Functions/tensor.cpp b/src/Continuum_mechanics/Functions/tensor.cpp index 77fd14ceb..5828f52a3 100644 --- a/src/Continuum_mechanics/Functions/tensor.cpp +++ b/src/Continuum_mechanics/Functions/tensor.cpp @@ -174,22 +174,29 @@ tensor2::tensor2(const arma::mat &m, Tensor2Type vtype) _mat = m; } -tensor2 tensor2::from_voigt(const arma::vec::fixed<6> &v, Tensor2Type vtype) { - arma::mat::fixed<3,3> m; +// engineering Voigt (6 contiguous values) -> symmetric 3x3 of a stress/strain tensor; false for none +static bool fill_from_voigt(arma::mat::fixed<3,3> &m, const double *v, Tensor2Type vtype) { if (vtype == Tensor2Type::strain) { - m(0,0) = v(0); m(1,1) = v(1); m(2,2) = v(2); - m(0,1) = 0.5 * v(3); m(1,0) = 0.5 * v(3); - m(0,2) = 0.5 * v(4); m(2,0) = 0.5 * v(4); - m(1,2) = 0.5 * v(5); m(2,1) = 0.5 * v(5); - } else if (vtype == Tensor2Type::stress || vtype == Tensor2Type::symmetric) { - m(0,0) = v(0); m(1,1) = v(1); m(2,2) = v(2); - m(0,1) = v(3); m(1,0) = v(3); - m(0,2) = v(4); m(2,0) = v(4); - m(1,2) = v(5); m(2,1) = v(5); - } else { - throw std::runtime_error("Cannot construct tensor2 from Voigt with Tensor2Type::none"); + m(0,0) = v[0]; m(1,1) = v[1]; m(2,2) = v[2]; + m(0,1) = 0.5 * v[3]; m(1,0) = 0.5 * v[3]; + m(0,2) = 0.5 * v[4]; m(2,0) = 0.5 * v[4]; + m(1,2) = 0.5 * v[5]; m(2,1) = 0.5 * v[5]; + return true; + } + if (vtype == Tensor2Type::stress || vtype == Tensor2Type::symmetric) { + m(0,0) = v[0]; m(1,1) = v[1]; m(2,2) = v[2]; + m(0,1) = v[3]; m(1,0) = v[3]; + m(0,2) = v[4]; m(2,0) = v[4]; + m(1,2) = v[5]; m(2,1) = v[5]; + return true; } - tensor2 result(m, vtype); + return false; +} + +tensor2 tensor2::from_voigt(const arma::vec::fixed<6> &v, Tensor2Type vtype) { + tensor2 result(vtype); + if (!fill_from_voigt(result._mat, v.memptr(), vtype)) + throw std::runtime_error("Cannot construct tensor2 from Voigt with Tensor2Type::none"); return result; } @@ -197,8 +204,10 @@ tensor2 tensor2::from_voigt(const arma::vec &v, Tensor2Type vtype) { if (v.n_elem != 6) throw std::invalid_argument("tensor2: expected 6-element vector, got " + std::to_string(v.n_elem)); - arma::vec::fixed<6> vf(v.memptr()); - return from_voigt(vf, vtype); + tensor2 result(vtype); + if (!fill_from_voigt(result._mat, v.memptr(), vtype)) + throw std::runtime_error("Cannot construct tensor2 from Voigt with Tensor2Type::none"); + return result; } tensor2 tensor2::from_voigt(const arma::vec &v, const std::string &type_str) { @@ -250,27 +259,16 @@ void tensor2::set_mat(const arma::mat &m) { } void tensor2::set_voigt(const arma::vec::fixed<6> &v) { - if (_vtype == Tensor2Type::strain) { - _mat(0,0) = v(0); _mat(1,1) = v(1); _mat(2,2) = v(2); - _mat(0,1) = 0.5 * v(3); _mat(1,0) = 0.5 * v(3); - _mat(0,2) = 0.5 * v(4); _mat(2,0) = 0.5 * v(4); - _mat(1,2) = 0.5 * v(5); _mat(2,1) = 0.5 * v(5); - } else if (_vtype == Tensor2Type::stress || _vtype == Tensor2Type::symmetric) { - _mat(0,0) = v(0); _mat(1,1) = v(1); _mat(2,2) = v(2); - _mat(0,1) = v(3); _mat(1,0) = v(3); - _mat(0,2) = v(4); _mat(2,0) = v(4); - _mat(1,2) = v(5); _mat(2,1) = v(5); - } else { + if (!fill_from_voigt(_mat, v.memptr(), _vtype)) throw std::runtime_error("Cannot set_voigt on Tensor2Type::none tensor"); - } } void tensor2::set_voigt(const arma::vec &v) { if (v.n_elem != 6) throw std::invalid_argument("tensor2::set_voigt: expected 6-element vector, got " + std::to_string(v.n_elem)); - arma::vec::fixed<6> vf(v.memptr()); - set_voigt(vf); + if (!fill_from_voigt(_mat, v.memptr(), _vtype)) + throw std::runtime_error("Cannot set_voigt on Tensor2Type::none tensor"); } arma::vec::fixed<6> tensor2::voigt() const { @@ -583,12 +581,20 @@ tensor4::tensor4(Tensor4Type type) : _mandel(arma::fill::zeros), _type(type) {} tensor4::tensor4(const arma::mat::fixed<6,6> &m, Tensor4Type type) : _mandel(eng_to_mandel(m, type)), _type(type) {} +// eng_to_mandel in place on the stored matrix (same products) +static void eng_to_mandel_inplace(arma::mat::fixed<6,6> &X, Tensor4Type type) { + double r, c; mandel_factors(type, r, c); + for (int I = 3; I < 6; ++I) X.row(I) *= r; + for (int J = 3; J < 6; ++J) X.col(J) *= c; +} + tensor4::tensor4(const arma::mat &m, Tensor4Type type) : _type(type) { if (m.n_rows != 6 || m.n_cols != 6) throw std::invalid_argument("tensor4: expected 6x6 matrix, got " + std::to_string(m.n_rows) + "x" + std::to_string(m.n_cols)); - _mandel = eng_to_mandel(arma::mat::fixed<6,6>(m), type); + _mandel = m; + eng_to_mandel_inplace(_mandel, type); } tensor4::tensor4(const arma::mat &m, const std::string &type_str) @@ -646,7 +652,8 @@ void tensor4::set_mat(const arma::mat &m) { if (m.n_rows != 6 || m.n_cols != 6) throw std::invalid_argument("tensor4: expected 6x6 matrix, got " + std::to_string(m.n_rows) + "x" + std::to_string(m.n_cols)); - _mandel = eng_to_mandel(arma::mat::fixed<6,6>(m), _type); + _mandel = m; + eng_to_mandel_inplace(_mandel, _type); _invalidate_fastor(); } diff --git a/src/Continuum_mechanics/Umat/Finite/Neo_hookean_incomp.cpp b/src/Continuum_mechanics/Umat/Finite/Neo_hookean_incomp.cpp index 0cc5067fe..35c3e5ced 100755 --- a/src/Continuum_mechanics/Umat/Finite/Neo_hookean_incomp.cpp +++ b/src/Continuum_mechanics/Umat/Finite/Neo_hookean_incomp.cpp @@ -114,8 +114,10 @@ void umat_neo_hookean_incomp(const string &umat_name, const vec &etot, const vec double dU2dJ2 = 2./D_1; // The sum is the spatial elasticity c = (1/J) d(L_v tau)/dD; the J that turns it into the // Kirchhoff-Lie tangent is applied once here -- see the note on L_vol_hyper. - mat Lt_spatial = L_iso_hyper_invariants(dWdI_1_bar, 0., 0., 0., 0., b, J) + L_vol_hyper(dUdJ, dU2dJ2, b, J); - Lt = Dtau_LieDD_2_DtauDe_corate(J*Lt_spatial, corate_type, F1, tau_t); + mat Lt_spatial = L_iso_hyper_invariants(dWdI_1_bar, 0., 0., 0., 0., b, J); + Lt_spatial += L_vol_hyper(dUdJ, dU2dJ2, b, J); + Lt_spatial *= J; // the Kirchhoff-Lie tangent, in place + Lt = Dtau_LieDD_2_DtauDe_corate(Lt_spatial, corate_type, F1, tau_t); if(start) { L = Lt; diff --git a/src/Continuum_mechanics/Umat/Finite/generic_hyper_pstretch.cpp b/src/Continuum_mechanics/Umat/Finite/generic_hyper_pstretch.cpp index 276a1d99c..91879a986 100755 --- a/src/Continuum_mechanics/Umat/Finite/generic_hyper_pstretch.cpp +++ b/src/Continuum_mechanics/Umat/Finite/generic_hyper_pstretch.cpp @@ -127,7 +127,8 @@ void umat_generic_hyper_pstretch(const std::string &umat_name, const vec &etot, mat Lt_spatial = Lt_iso + Lt_vol; // Standardize to the canonical box convention Lt = d(tau_hat)/d(De) -- see generic_hyper_invariants. - Lt = Dtau_LieDD_2_DtauDe_corate(J*Lt_spatial, corate_type, F1, m_tau); + Lt_spatial *= J; // the Kirchhoff-Lie tangent, in place + Lt = Dtau_LieDD_2_DtauDe_corate(Lt_spatial, corate_type, F1, m_tau); if(start) { L = Lt; diff --git a/src/Continuum_mechanics/Umat/Mechanical/Damage/damage_LLD_0.cpp b/src/Continuum_mechanics/Umat/Mechanical/Damage/damage_LLD_0.cpp index 3873e58b8..e17395aa2 100755 --- a/src/Continuum_mechanics/Umat/Mechanical/Damage/damage_LLD_0.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/Damage/damage_LLD_0.cpp @@ -47,15 +47,16 @@ constexpr double d_broken = 0.99; struct damaged_elasticity { double EL, ET, nuTL, nuTT, GLT; - mat L(const double &d22, const double &d12) const { + mat::fixed<6, 6> L(const double &d22, const double &d12) const { const double ET_d = ET*(1. - d22); mat::fixed<3, 3> S; S(0, 0) = 1./EL; S(0, 1) = S(1, 0) = S(0, 2) = S(2, 0) = -nuTL/ET; S(1, 1) = S(2, 2) = 1./ET_d; S(1, 2) = S(2, 1) = -nuTT/ET_d; - mat L = zeros(6, 6); - L.submat(0, 0, 2, 2) = inv(S); + mat::fixed<6, 6> L = zeros(6, 6); + const mat::fixed<3, 3> S_inv = inv(S); + L.submat(0, 0, 2, 2) = S_inv; L(3, 3) = L(4, 4) = GLT*(1. - d12); L(5, 5) = ET_d/(2.*(1. + nuTT)); return L; @@ -175,7 +176,7 @@ void umat_damage_LLD_0(const string &umat_name, const vec &Etot, const vec &DEto sigma = zeros(6); } - const mat L0 = C.L(0., 0.); // undamaged: the effective-stress plasticity + const mat::fixed<6, 6> L0 = C.L(0., 0.); // undamaged: the effective-stress plasticity vec alpha = alphaT*Ith(); alpha(0) += alphaL - alphaT; @@ -193,7 +194,8 @@ void umat_damage_LLD_0(const string &umat_name, const vec &Etot, const vec &DEto const vec theta = {0., A_ts, A_ts, 1., 1., 0.}; // the diagonal of Theta // ---- 1. Plasticity in effective stress (independent of the damage) ---- - vec Eel = Etot_f + DEtot_f - alpha*(T + DT - Tinit) - EP; + const vec Eel_0 = Etot_f + DEtot_f - alpha*(T + DT - Tinit); // the strain minus its thermal part + vec Eel = Eel_0 - EP; vec Lambda_p = zeros(6); vec Phi_p = zeros(1), Dp = zeros(1), dp = zeros(1); const vec Y_pcrit = {sigma_ts_0}; @@ -208,7 +210,7 @@ void umat_damage_LLD_0(const string &umat_name, const vec &Etot, const vec &DEto Fischer_Burmeister_m(Phi_p, Y_pcrit, denom_p, Dp, dp, error); p_ts += dp(0); EP += dp(0)*Lambda_p; - Eel = Etot_f + DEtot_f - alpha*(T + DT - Tinit) - EP; + Eel = Eel_0 - EP; } if (compteur == simcoon::maxiter_umat) tnew_dt = 0.2; @@ -219,12 +221,12 @@ void umat_damage_LLD_0(const string &umat_name, const vec &Etot, const vec &DEto const double Y0[2] = {Y_22_0, Y_12_0}; vec d = {d_22, d_12}; vec Dd_all = zeros(2); - mat L_t = C.L(d(0), d(1)); + mat::fixed<6, 6> L_t = C.L(d(0), d(1)); sigma = L_t*Eel; - // dPhi_l/dd_j at fixed strain: through sigma = L~(d) Eel and explicitly through Y_ts - auto dPhi_dd = [&](const driving_force &Y, const std::array, 2> &Lambda_d, const int &l, const int &j) { + // dPhi_l/dd_j at fixed strain, via sigma = L~(d) Eel and explicitly via Y_ts; LLambda_d[j] = L~ Lambda_j + auto dPhi_dd = [&](const driving_force &Y, const std::array, 2> &LLambda_d, const int &l, const int &j) { const double dYts_dd = (j == 0) ? Y.dYts_dd22 : Y.dYts_dd12; - double J = ((Y.Y_ts > Y0[l]) ? (-sum(Y.dYts_dsigma % (L_t*Lambda_d[j])) + dYts_dd)/Yc[l] : 0.); + double J = ((Y.Y_ts > Y0[l]) ? (-sum(Y.dYts_dsigma % LLambda_d[j]) + dYts_dd)/Yc[l] : 0.); if (l == j) J -= dlagrange_pow_1(d(l), c_lambda, p0_lambda, n_lambda, alpha_lambda) + 1.; return J; @@ -240,9 +242,18 @@ void umat_damage_LLD_0(const string &umat_name, const vec &Etot, const vec &DEto sigma = L_t*Eel; continue; } - const std::vector mech = broken_22 ? std::vector{1} : std::vector{0, 1}; - const int n = mech.size(); + const int mech[2] = {broken_22 ? 1 : 0, 1}; + const int n = broken_22 ? 1 : 2; const std::array, 2> Lambda_d = {C.Lambda_22(sigma, d(0)), C.Lambda_12(sigma, d(1))}; + // L~ Lambda_j enters a row only above its damage threshold (dPhi_dd) + bool above = false; + for (int a = 0; a < n; a++) + above = above || (Y.Y_ts > Y0[mech[a]]); + std::array, 2> LLambda_d; + if (above) { + for (int a = 0; a < n; a++) + LLambda_d[mech[a]] = L_t*Lambda_d[mech[a]]; + } vec Phi_d(n), Y_dcrit = ones(n), Dd(n), dd(n); mat denom_d(n, n); for (int a = 0; a < n; a++) { @@ -250,7 +261,7 @@ void umat_damage_LLD_0(const string &umat_name, const vec &Etot, const vec &DEto Phi_d(a) = Macaulay_p(Y.Y_ts - Y0[l])/Yc[l] - lagrange_pow_1(d(l), c_lambda, p0_lambda, n_lambda, alpha_lambda) - d(l); Dd(a) = Dd_all(l); for (int c = 0; c < n; c++) - denom_d(a, c) = dPhi_dd(Y, Lambda_d, l, mech[c]); + denom_d(a, c) = dPhi_dd(Y, LLambda_d, l, mech[c]); } Fischer_Burmeister_m(Phi_d, Y_dcrit, denom_d, Dd, dd, error); for (int a = 0; a < n; a++) { @@ -267,7 +278,7 @@ void umat_damage_LLD_0(const string &umat_name, const vec &Etot, const vec &DEto // ---- 3. Tangent of the two-stage update (plasticity, then damage) ---- // plastic flow direction held fixed (continuum), then the active damage set linearized exactly - mat A = eye(6, 6); + mat::fixed<6, 6> A = eye(6, 6); if (Dp(0) > simcoon::iota) { const double h_p = sum(Lambda_p % (L0*Lambda_p)) + dhardening(p_ts); A -= Lambda_p*(L0*Lambda_p).t()/h_p; @@ -282,15 +293,19 @@ void umat_damage_LLD_0(const string &umat_name, const vec &Etot, const vec &DEto act.push_back(l); const int n = act.size(); if (n > 0) { + std::array, 2> LLambda_d; + for (int a = 0; a < n; a++) + LLambda_d[act[a]] = L_t*Lambda_d[act[a]]; + const rowvec dYts_deps = (A.t()*(L_t*Y.dYts_dsigma)).t(); // the same for every row mat J(n, n), G(n, 6); for (int a = 0; a < n; a++) { for (int c = 0; c < n; c++) - J(a, c) = dPhi_dd(Y, Lambda_d, act[a], act[c]); - G.row(a) = (A.t()*(L_t*Y.dYts_dsigma)).t()/Yc[act[a]]; + J(a, c) = dPhi_dd(Y, LLambda_d, act[a], act[c]); + G.row(a) = dYts_deps/Yc[act[a]]; } const mat dd_deps = -solve(J, G); for (int a = 0; a < n; a++) - Lt -= (L_t*Lambda_d[act[a]])*dd_deps.row(a); + Lt -= LLambda_d[act[a]]*dd_deps.row(a); } } diff --git a/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_chaboche.cpp b/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_chaboche.cpp index 82c6e07b7..0f82ec150 100755 --- a/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_chaboche.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_chaboche.cpp @@ -180,12 +180,11 @@ void umat_plasticity_chaboche(const string &umat_name, const vec &Etot, const ve vec dPhida_1 = zeros(6); vec dPhida_2 = zeros(6); vec dPhidsigma = zeros(6); - double dPhidtheta = 0.; //Compute the explicit flow direction vec Lambdap = eta_stress(stress-X); - vec Lambdaa_1 = eta_stress(stress-X) - D_1*a_1; - vec Lambdaa_2 = eta_stress(stress-X) - D_2*a_2; + vec Lambdaa_1 = Lambdap - D_1*a_1; + vec Lambdaa_2 = Lambdap - D_2*a_2; std::vector kappa_j(1); kappa_j[0] = L*Lambdap; mat K = zeros(1,1); @@ -227,23 +226,26 @@ void umat_plasticity_chaboche(const string &umat_name, const vec &Etot, const ve } } else { + const vec Ir05_v = Ir05(); + const vec Eel_0 = Etot + DEtot - alpha*(T + DT - T_init); //Loop for (compteur = 0; ((compteur < simcoon::maxiter_umat) && (error > simcoon::precision_umat)); compteur++) { p = s_j(0); Hp = hardening.R(p); dHpdp = hardening.dR_dp(p); - dPhidsigma = eta_stress(stress-X); + const vec sigma_eff = stress-X; + dPhidsigma = eta_stress(sigma_eff); dPhidp = -1.*dHpdp; - dPhida_1 = -1.*(2./3.)*C_1*(eta_stress(stress-X)%Ir05()); - dPhida_2 = -1.*(2./3.)*C_2*(eta_stress(stress-X)%Ir05()); + dPhida_1 = -1.*(2./3.)*C_1*(dPhidsigma%Ir05_v); + dPhida_2 = -1.*(2./3.)*C_2*(dPhidsigma%Ir05_v); //compute Phi and the derivatives - Phi(0) = Mises_stress(stress-X) - Hp - sigmaY; + Phi(0) = Mises_stress(sigma_eff) - Hp - sigmaY; - Lambdap = eta_stress(stress-X); - Lambdaa_1 = eta_stress(stress-X) - D_1*a_1; - Lambdaa_2 = eta_stress(stress-X) - D_2*a_2; + Lambdap = dPhidsigma; + Lambdaa_1 = dPhidsigma - D_1*a_1; + Lambdaa_2 = dPhidsigma - D_2*a_2; kappa_j[0] = L*Lambdap; K(0,0) = dPhidp + sum(dPhida_1%Lambdaa_1) + sum(dPhida_2%Lambdaa_2); @@ -256,12 +258,12 @@ void umat_plasticity_chaboche(const string &umat_name, const vec &Etot, const ve EP = EP + ds_j(0)*Lambdap; a_1 = a_1 + ds_j(0)*Lambdaa_1; a_2 = a_2 + ds_j(0)*Lambdaa_2; - X_1 += ds_j(0)*(2./3.)*C_1*(Lambdaa_1%Ir05()); - X_2 += ds_j(0)*(2./3.)*C_2*(Lambdaa_2%Ir05()); + X_1 += ds_j(0)*(2./3.)*C_1*(Lambdaa_1%Ir05_v); + X_2 += ds_j(0)*(2./3.)*C_2*(Lambdaa_2%Ir05_v); X = X_1 + X_2; //the stress is now computed using the relationship stress = L(E-Ep) - Eel = Etot + DEtot - alpha*(T + DT - T_init) - EP; + Eel = Eel_0 - EP; stress = el_pred(L, Eel); } // the loop reads p at the top and corrects s_j at the bottom @@ -270,7 +272,6 @@ void umat_plasticity_chaboche(const string &umat_name, const vec &Etot, const ve } //Computation of the increments of variables - vec Dsigma = stress - stress_start; vec DEP = EP - EP_start; double Dp = Ds_j[0]; vec Da_1 = a_1 - a_1start; @@ -285,13 +286,11 @@ void umat_plasticity_chaboche(const string &umat_name, const vec &Etot, const ve tangent_mode, Bhat, kappa_j, dPhidsigma_l, Ds_j, L, [&]() -> std::vector { // lazy: evaluated only in algorithmic mode // J2 flow on sigma-X; the backstress coupling is only in the closest-point branch - const std::vector dLambda_dsigma_l = { deta_stress(stress - X) }; + std::vector dLambda_dsigma_l(1); + dLambda_dsigma_l[0] = deta_stress(stress - X); return dLambda_dsigma_l; }); Lt = ct.Lt; - - std::vector P_theta(1); - P_theta[0] = dPhidtheta - sum(dPhidsigma%(L*alpha)); double A_p = -Hp; vec A_a1 = -X_1; diff --git a/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_johnson_cook_ccp.cpp b/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_johnson_cook_ccp.cpp index 4e1ea827b..5ad3de93f 100644 --- a/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_johnson_cook_ccp.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_johnson_cook_ccp.cpp @@ -131,7 +131,8 @@ void umat_plasticity_johnson_cook_CCP(const string &umat_name, const vec &Etot, vec ds_j = zeros(1); //Elastic prediction - Accounting for the thermal prediction - vec Eel = Etot + DEtot - alpha*(T+DT-T_init) - EP; + const vec Eel_0 = Etot + DEtot - alpha*(T+DT-T_init); + vec Eel = Eel_0 - EP; sigma = el_pred(L, Eel); //Define the plastic function and the stress @@ -159,7 +160,7 @@ void umat_plasticity_johnson_cook_CCP(const string &umat_name, const vec &Etot, Ds_j(0) = x0; s_j(0) += x0; EP = EP + x0*Lambda_trial; - Eel = Etot + DEtot - alpha*(T + DT - T_init) - EP; + Eel = Eel_0 - EP; sigma = el_pred(L, Eel); } } @@ -215,7 +216,7 @@ void umat_plasticity_johnson_cook_CCP(const string &umat_name, const vec &Etot, EP = EP + ds_j(0)*Lambdap; //the stress is now computed using the relationship sigma = L(E-Ep) - Eel = Etot + DEtot - alpha*(T + DT - T_init) - EP; + Eel = Eel_0 - EP; sigma = el_pred(L, Eel); } @@ -248,7 +249,8 @@ void umat_plasticity_johnson_cook_CCP(const string &umat_name, const vec &Etot, tangent_mode, Bhat, kappa_j, dPhidsigma_l, Ds_j, L, [&]() -> std::vector { // lazy: evaluated only in algorithmic mode // J2 associated flow: dLambda/dsigma = deta_stress, complete for one isotropic mechanism - const std::vector dLambda_dsigma_l = { deta_stress(sigma) }; + std::vector dLambda_dsigma_l(1); + dLambda_dsigma_l[0] = deta_stress(sigma); return dLambda_dsigma_l; }); Lt = ct.Lt; diff --git a/src/Continuum_mechanics/Umat/Mechanical/SMA/SMA_mono.cpp b/src/Continuum_mechanics/Umat/Mechanical/SMA/SMA_mono.cpp index d29208543..0deb0c74a 100644 --- a/src/Continuum_mechanics/Umat/Mechanical/SMA/SMA_mono.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/SMA/SMA_mono.cpp @@ -179,7 +179,6 @@ void umat_sma_mono(const string &umat_name, const vec &Etot, const vec &DEtot, v vec ET_start = ET; double xi = statev(nvariants+7); - double xi_start = xi; double dxi = 0.; //Rotation of internal variables (tensors) @@ -211,7 +210,7 @@ void umat_sma_mono(const string &umat_name, const vec &Etot, const vec &DEtot, v vec PhiR(nvariants); vec PhiR_start(nvariants); - std::vector transfo_actif(24); + std::vector transfo_actif(nvariants); ///Elastic prediction - Accounting for the thermal prediction vec Eel = Etot + DEtot - alpha*(T+DT-T_init) - ET; @@ -299,6 +298,8 @@ void umat_sma_mono(const string &umat_name, const vec &Etot, const vec &DEtot, v lambda1 = lagrange_pow_1(xi, c_lambda1, p0_lambda1, n_lambda1, alpha_lambda1); dlambda1dxin = dlagrange_pow_1(xi, c_lambda1, p0_lambda1, n_lambda1, alpha_lambda1); + Hnmxim = Hnm*xin; + for(int i=0; i 1.E-8) { - - xi_path = xi - xi_start; - - for(int i=0; i 0.) { @@ -420,13 +390,6 @@ void umat_sma_mono(const string &umat_name, const vec &Etot, const vec &DEtot, v throw simcoon::exception_inv("Error in inv function inside umat_sma_mono."); } - -/* ///Note : To use with Self-consistent micromechanics only! - vec eigval = eig_sym(Lt); - while(eigval(0) < 0.) { - Lt = 0.99*Lt + 0.01*Lt_eff; - eigval = eig_sym(Lt); - }*/ } else { diff --git a/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_T.cpp b/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_T.cpp index ca059cb95..ce38784ad 100755 --- a/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_T.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_T.cpp @@ -266,9 +266,9 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt vec lambdaTR = -1.*ETMean; - //Definition of the modified Y function - double YtF = Y0t + D*Hcur*Mises_stress(stress); - double YtR = Y0t + D*sum(stress%ETMean); + //Modified Y functions, Lagrange multipliers and criteria: set by the transformation loop before any use + double YtF = 0.; + double YtR = 0.; double HfF = 0.; double HfR = 0.; @@ -304,9 +304,9 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt } } - //Set the Lagrange multiplliers due to Physical limitations - double lambda0 = -1.*lagrange_pow_0(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); - double lambda1 = lagrange_pow_1(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); + //Lagrange multiplliers due to Physical limitations + double lambda0 = 0.; + double lambda1 = 0.; //Define the value of DM_sig vec DM_sig = (DM*stress_start); @@ -320,14 +320,8 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt double A_xiR_start = -1.*rhoDs0*(T) + rhoDE0 - 0.5*sum(stress_start%DM_sig) - sum(stress_start%Dalpha)*(T-T_init) + HfR; //Transformation criteria - double PhihatF; - if (aniso_criteria) { - PhihatF = Hcur*Drucker_ani_stress(stress, DFA_params, prager_b, prager_n); - } - else { - PhihatF = Hcur*Drucker_stress(stress, prager_b, prager_n); - } - double PhihatR = sum(stress%ETMean); + double PhihatF = 0.; + double PhihatR = 0.; //Variables required for the loop vec s_j = zeros(2); @@ -541,14 +535,22 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt L = inv_cubic(M_eff); DM_sig = DM*stress; - Dalpha_T = Dalpha*(T+DT-T_init); + // equivalent stresses and gradients at the current stress, shared below (Dalpha_T is loop-invariant) + const double sig_eq = Mises_stress(stress); + const vec eta_sig = eta_stress(stress); + double Dr_sig; + vec dDr_sig; if (aniso_criteria) { - lambdaTF = Hcur * dDrucker_ani_stress(stress, DFA_params, prager_b, prager_n); + Dr_sig = Drucker_ani_stress(stress, DFA_params, prager_b, prager_n); + dDr_sig = dDrucker_ani_stress(stress, DFA_params, prager_b, prager_n); } else { - lambdaTF = Hcur * dDrucker_stress(stress, prager_b, prager_n); + Dr_sig = Drucker_stress(stress, prager_b, prager_n); + dDr_sig = dDrucker_stress(stress, prager_b, prager_n); } + + lambdaTF = Hcur * dDr_sig; lambdaTR = -1. * ETMean; kappa_j[0] = L*(lambdaTF + DM_sig + Dalpha_T); @@ -586,23 +588,18 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt } // Find Hcur explicit - if (Mises_stress(stress) > sigmacrit) - sigmastar = Mises_stress(stress) - sigmacrit; + if (sig_eq > sigmacrit) + sigmastar = sig_eq - sigmacrit; else sigmastar = 0.; Hcur = Hmin + (Hmax - Hmin)*(1. - exp(-1.*k1*sigmastar)); //Forward transformation thermodynamic force - if (aniso_criteria) { - PhihatF = Hcur*Drucker_ani_stress(stress, DFA_params, prager_b, prager_n); - } - else { - PhihatF = Hcur*Drucker_stress(stress, prager_b, prager_n); - } + PhihatF = Hcur*Dr_sig; A_xiF = rhoDs0*(T + DT) - rhoDE0 + 0.5*sum(stress%DM_sig) + sum(stress%Dalpha)*(T + DT - T_init) - HfF; lambda1 = lagrange_pow_1(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); - const double DHM = D*Hcur*Mises_stress(stress); + const double DHM = D*Hcur*sig_eq; YtF = Y0t + DHM; Phi(0) = PhihatF + A_xiF - lambda1 - YtF; @@ -644,27 +641,22 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt } } - dHcurdsigma = k1*(Hmax - Hmin)*exp(-1.*k1*sigmastar)*eta_stress(stress); + dHcurdsigma = k1*(Hmax - Hmin)*exp(-1.*k1*sigmastar)*eta_sig; //Related to forward transformation - if (aniso_criteria) { - dPhihatFdsigma = dHcurdsigma * Drucker_ani_stress(stress, DFA_params, prager_b, prager_n) + Hcur * dDrucker_ani_stress(stress, DFA_params, prager_b, prager_n); - } - else { - dPhihatFdsigma = dHcurdsigma * Drucker_stress(stress, prager_b, prager_n) + Hcur * dDrucker_stress(stress, prager_b, prager_n); - } + dPhihatFdsigma = dHcurdsigma * Dr_sig + Hcur * dDr_sig; dPhihatFdxiF = 0.; dPhihatFdxiR = 0.; - dA_xiFdsigma = DM_sig + Dalpha*(T+DT-T_init); + dA_xiFdsigma = DM_sig + Dalpha_T; dA_xiFdxiF = -dHfF; dA_xiFdxiR = dHfF; dlambda1dsigma = zeros(6); dlambda1dxiF = dlagrange_pow_1(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); - dlambda1dxiR = -1.*dlagrange_pow_1(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); + dlambda1dxiR = -1.*dlambda1dxiF; - dYtFdsigma = D*(dHcurdsigma * Mises_stress(stress) + Hcur * eta_stress(stress)); + dYtFdsigma = D*(dHcurdsigma * sig_eq + Hcur * eta_sig); dYtFdxiF = 0.; dYtFdxiR = 0.; @@ -674,13 +666,13 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt //Relative to reverse transformation dPhihatRdsigma = ETMean; - dA_xiRdsigma = -1.*DM_sig -1.*Dalpha*(T+DT-T_init); + dA_xiRdsigma = -1.*DM_sig - Dalpha_T; dA_xiRdxiF = dHfR; dA_xiRdxiR = -dHfR; dlambda0dsigma = zeros(6); - dlambda0dxiF = -1.*dlagrange_pow_0(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); - dlambda0dxiR = dlagrange_pow_0(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); + dlambda0dxiR = dlagrange_pow_0(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); + dlambda0dxiF = -1.*dlambda0dxiR; dYtRdsigma = D*ETMean; dPhiRdsigma = -1.*dPhihatRdsigma + dA_xiRdsigma + dlambda0dsigma - dYtRdsigma; diff --git a/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_TR.cpp b/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_TR.cpp index e82a1a9c3..72853dcd9 100644 --- a/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_TR.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_TR.cpp @@ -264,19 +264,9 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE double lambda1Reo = lagrange_pow_1(a_eq/ETRmax, c_lambdaReo, p0_lambdaReo, n_lambdaReo, alpha_lambdaReo); vec sigma_eff = stress - (1. + lambda1Reo) * X; - vec lambdaReo; - vec etaReo; - if (Mises_stress(sigma_eff) > simcoon::iota) { - if (aniso_criteria) { - lambdaReo = dDrucker_ani_stress(sigma_eff, DFA_params, prager_b, prager_n); - } else { - lambdaReo = dDrucker_stress(sigma_eff, prager_b, prager_n); - } - etaReo = lambdaReo; - } else { - lambdaReo = zeros(6); - etaReo = zeros(6); - } + // reorientation flow and criterion gradient: set by the local loop before any use + vec lambdaReo = zeros(6); + vec etaReo = zeros(6); // Back-strain channel flows: each shadows its own leading-mechanism flow vec lambda_areo = lambdaReo; @@ -301,8 +291,9 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE double HfF = 0., HfR = 0.; compute_Hf(xi, HfF, HfR); - double lambda0 = -1.*lagrange_pow_0(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); - double lambda1 = lagrange_pow_1(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); + // Lagrange multipliers and criteria: set by the local loop before any use + double lambda0 = 0.; + double lambda1 = 0.; vec DM_sig = DM*stress_start; vec Dalpha_T = Dalpha*(T+DT-T_init); @@ -312,8 +303,8 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE double A_xiR = -1.*rhoDs0*(T+DT) + rhoDE0 - 0.5*sum(stress%DM_sig) - sum(stress%Dalpha)*(T+DT-T_init) + HfR; double A_xiR_start = -1.*rhoDs0*(T) + rhoDE0 - 0.5*sum(stress_start%DM_sig) - sum(stress_start%Dalpha)*(T-T_init) + HfR; - double PhihatF = build_PhihatF(stress); - double PhihatR = sum(stress%ETMean); + double PhihatF = 0.; + double PhihatR = 0.; // Variables required for the local loop — 3 leading mechanisms vec s_j = zeros(3); @@ -576,6 +567,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE } } else { + const vec Ir05_v = Ir05(); // engineering -> tensorial shear for (compteur = 0; ((compteur < simcoon::maxiter_umat) && (error > simcoon::precision_umat)); compteur++) { // Reuss mixing @@ -583,18 +575,30 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE L = inv_cubic(M_eff); DM_sig = DM*stress; - Dalpha_T = Dalpha*(T+DT-T_init); + + // equivalent stresses and gradients at the current stress, shared below (Dalpha_T is loop-invariant) + const double sig_eq = Mises_stress(stress); + const vec eta_sig = eta_stress(stress); + double Dr_sig; + vec dDr_sig; + if (aniso_criteria) { + Dr_sig = Drucker_ani_stress(stress, DFA_params, prager_b, prager_n); + dDr_sig = dDrucker_ani_stress(stress, DFA_params, prager_b, prager_n); + } else { + Dr_sig = Drucker_stress(stress, prager_b, prager_n); + dDr_sig = dDrucker_stress(stress, prager_b, prager_n); + } // Hcur explicit (depends on \sigma ) - if (Mises_stress(stress) > sigmacrit) sigmastar = Mises_stress(stress) - sigmacrit; - else sigmastar = 0.; + if (sig_eq > sigmacrit) sigmastar = sig_eq - sigmacrit; + else sigmastar = 0.; Hcur = Hmin + (Hmax - Hmin)*(1. - exp(-1.*k1*sigmastar)); - // Re-evaluate flows at current \sigma , areo, \lambda_1Reo - lambdaTF = build_lambdaTF(stress); + // Re-evaluate flows at current \sigma , areo, \lambda_1Reo (build_lambdaTF at stress) + lambdaTF = Hcur * dDr_sig; lambdaTR = -1. * ETMean; - X = HReo * (areo % Ir05()); + X = HReo * (areo % Ir05_v); a_eq = Mises_strain(areo); // intrinsic saturation argument ||v^re||/ETRmax, no xi: the penalty would blow up as xi -> 0 lambda1Reo = lagrange_pow_1(a_eq/ETRmax, c_lambdaReo, p0_lambdaReo, n_lambdaReo, alpha_lambdaReo); @@ -623,11 +627,11 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE // Hardening compute_Hf(xi, HfF, HfR); - // Forward thermodynamic force - PhihatF = build_PhihatF(stress); + // Forward thermodynamic force (build_PhihatF at stress) + PhihatF = Hcur * Dr_sig; A_xiF = rhoDs0*(T + DT) - rhoDE0 + 0.5*sum(stress%DM_sig) + sum(stress%Dalpha)*(T + DT - T_init) - HfF; lambda1 = lagrange_pow_1(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); - const double DHM = D*Hcur*Mises_stress(stress); + const double DHM = D*Hcur*sig_eq; double YtF = Y0t + DHM; Phi(0) = PhihatF + A_xiF - lambda1 - YtF; @@ -653,17 +657,13 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE else dHfR = 1.E12; // d(Hcur)/d\sigma - dHcurdsigma = k1*(Hmax - Hmin)*exp(-1.*k1*sigmastar)*eta_stress(stress); + dHcurdsigma = k1*(Hmax - Hmin)*exp(-1.*k1*sigmastar)*eta_sig; // ---- Forward derivatives ---- - if (aniso_criteria) { - dPhihatFdsigma = dHcurdsigma * Drucker_ani_stress(stress, DFA_params, prager_b, prager_n) + Hcur * dDrucker_ani_stress(stress, DFA_params, prager_b, prager_n); - } else { - dPhihatFdsigma = dHcurdsigma * Drucker_stress(stress, prager_b, prager_n) + Hcur * dDrucker_stress(stress, prager_b, prager_n); - } - dA_xiFdsigma = DM_sig + Dalpha*(T+DT-T_init); + dPhihatFdsigma = dHcurdsigma * Dr_sig + Hcur * dDr_sig; + dA_xiFdsigma = DM_sig + Dalpha_T; dlambda1dsigma = zeros(6); - dYtFdsigma = D*(dHcurdsigma * Mises_stress(stress) + Hcur * eta_stress(stress)); + dYtFdsigma = D*(dHcurdsigma * sig_eq + Hcur * eta_sig); dPhiFdsigma = dPhihatFdsigma + dA_xiFdsigma - dlambda1dsigma - dYtFdsigma; double dlambda1dxi = dlagrange_pow_1(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); @@ -674,7 +674,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE // ---- Reverse derivatives ---- const double xi_div = xi_safe(xi); dPhihatRdsigma = ETMean; - dA_xiRdsigma = -1.*DM_sig - 1.*Dalpha*(T+DT-T_init); + dA_xiRdsigma = -1.*DM_sig - Dalpha_T; double dlambda0dxi = dlagrange_pow_0(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); dlambda0dsigma = zeros(6); dlambda0dxiF = -1.*dlambda0dxi; @@ -694,12 +694,12 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE dlambda1Reo_arg = dlagrange_pow_1(a_eq/ETRmax, c_lambdaReo, p0_lambdaReo, n_lambdaReo, alpha_lambdaReo); const double eta_dot_X = sum(etaReo % X); // \eta_Reo : X (tensor double contraction) // eta_strain has engineering shears; the gradient of a_eq carries half shears, hence % Ir05 - const vec eta_strain_areo = (a_eq > simcoon::iota) ? vec(eta_strain(areo) % Ir05()) : vec(zeros(6)); + const vec eta_strain_areo = (a_eq > simcoon::iota) ? vec(eta_strain(areo) % Ir05_v) : vec(zeros(6)); const vec dlambda1Reo_dareo = dlambda1Reo_arg * (1./ETRmax) * eta_strain_areo; dPhiReodsigma = etaReo; - dPhiReodareo = -(1. + lambda1Reo) * HReo * (etaReo % Ir05()) + dPhiReodareo = -(1. + lambda1Reo) * HReo * (etaReo % Ir05_v) - eta_dot_X * dlambda1Reo_dareo; dPhiReodpTR = 0.; diff --git a/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_fast.cpp b/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_fast.cpp index 3124a1d1a..dc741eb4b 100755 --- a/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_fast.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_fast.cpp @@ -79,9 +79,11 @@ void umat_zener_fast(const string &umat_name, const vec &Etot, const vec &DEtot, //Define the viscoelastic stiffness mat L0 = L_iso(E0, nu0, "Enu"); L = L0; - mat L1 = L_iso(E1, nu1, "Enu"); - - mat H1 = H_iso(etaB1, etaS1); //dimension of stiffness tensor + // the branch operators are built in the lists kelvin_series_step reads (no copies) + std::vector L_branch(1), H_branch(1); + L_branch[0] = L_iso(E1, nu1, "Enu"); + H_branch[0] = H_iso(etaB1, etaS1); //dimension of stiffness tensor + const mat &L1 = L_branch[0]; if(start) { //Initialization T_init = T; @@ -101,7 +103,7 @@ void umat_zener_fast(const string &umat_name, const vec &Etot, const vec &DEtot, // backward Euler in closed form, with its consistent tangent (linear_viscoelastic.hpp) const vec eps_e = Etot + DEtot - alpha*(T + DT - T_init); - const LinearViscoStep st = kelvin_series_step(L0, {L1}, {H1}, {EV1_start}, eps_e, alpha, DTime); + const LinearViscoStep st = kelvin_series_step(L0, L_branch, H_branch, {EV1_start}, eps_e, alpha, DTime); EV1 = st.EV_i[0]; const vec DEV1 = EV1 - EV1_start; v += norm_strain(DEV1); diff --git a/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/linear_viscoelastic.cpp b/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/linear_viscoelastic.cpp index 1230a5127..bed0f8630 100644 --- a/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/linear_viscoelastic.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/linear_viscoelastic.cpp @@ -18,6 +18,7 @@ ///@file linear_viscoelastic.cpp ///@brief Closed-form backward-Euler step of the linear viscoelastic rheologies (see the header) +#include #include #include #include @@ -29,10 +30,34 @@ namespace simcoon { namespace { -// B_i^-1 EV = (H + dt L)^-1 H EV and C_i = dt (H + dt L)^-1; dt = 0 gives EV and 0 (inactive branch) +// per-thread memo of the step inverses keyed on the matrix bits: inv() is deterministic, a hit is exact +struct inverse_memo { + static constexpr int size = 8; + double key[size][36]; + double value[size][36]; + int used; + int next; +}; +thread_local inverse_memo memo; + +mat::fixed<6,6> memo_inv(const mat::fixed<6,6> &M) { + for (int k = 0; k < memo.used; k++) { + if (std::memcmp(memo.key[k], M.memptr(), sizeof(memo.key[k])) == 0) + return mat::fixed<6,6>(memo.value[k]); + } + const mat::fixed<6,6> M_inv = inv(M); // throws on a singular matrix: nothing is stored + std::memcpy(memo.key[memo.next], M.memptr(), sizeof(memo.key[memo.next])); + std::memcpy(memo.value[memo.next], M_inv.memptr(), sizeof(memo.value[memo.next])); + memo.next = (memo.next + 1) % inverse_memo::size; + if (memo.used < inverse_memo::size) memo.used++; + return M_inv; +} + +// B_i^-1 EV = (H + dt L)^-1 H EV and C_i = dt (H + dt L)^-1; dt = 0 gives EV and 0 (inactive branch). void branch_operators(const mat &L, const mat &H, const vec &EV_start, const double &DTime, vec &EV_relaxed, mat &C) { - const mat HdtL_inv = inv(H + DTime*L); + const mat::fixed<6,6> HdtL = H + DTime*L; + const mat::fixed<6,6> HdtL_inv = memo_inv(HdtL); EV_relaxed = HdtL_inv*(H*EV_start); C = DTime*HdtL_inv; } @@ -43,21 +68,22 @@ LinearViscoStep kelvin_series_step(const mat &L0, const vector &L_i, const const vector &EV_i_start, const vec &eps_e, const vec &alpha, const double &DTime) { const size_t N = L_i.size(); - vector C(N); + vector> C(N); vector EV_relaxed(N); // B_i^-1 EV_i,n - mat sumC = zeros(6,6); + mat::fixed<6,6> sumC = zeros(6,6); vec rhs = eps_e; for (size_t i=0; i S = eye(6,6) + L0*sumC; + const mat::fixed<6,6> S_inv = memo_inv(S); LinearViscoStep st; st.sigma = S_inv*(L0*rhs); - st.dSdE = S_inv*L0; - st.dSdE = 0.5*(st.dSdE + st.dSdE.t()); // (L0^-1 + sum C)^-1: symmetric up to round-off + const mat::fixed<6,6> dSdE = S_inv*L0; + st.dSdE = 0.5*(dSdE + dSdE.t()); // (L0^-1 + sum C)^-1: symmetric up to round-off st.dSdT = -st.dSdE*alpha; st.EV_i.resize(N); st.dEVdE_i.resize(N); @@ -82,16 +108,17 @@ LinearViscoStep maxwell_parallel_step(const mat &L0, const vector &L_i, con st.dEVdT_i.resize(N); for (size_t i=0; i C; branch_operators(L_i[i], H_i[i], EV_i_start[i], DTime, EV_relaxed, C); - const mat G = C*L_i[i]; + const mat::fixed<6,6> G = C*L_i[i]; st.EV_i[i] = EV_relaxed + G*eps_e; st.dEVdE_i[i] = G; st.dEVdT_i[i] = -G*alpha; st.sigma -= L_i[i]*st.EV_i[i]; st.dSdE -= L_i[i]*G; } - st.dSdE = 0.5*(st.dSdE + st.dSdE.t()); // L0 - sum L C L: symmetric up to round-off + const mat::fixed<6,6> dSdE = st.dSdE; + st.dSdE = 0.5*(dSdE + dSdE.t()); // L0 - sum L C L: symmetric up to round-off st.dSdT = -st.dSdE*alpha; return st; } diff --git a/src/Continuum_mechanics/Umat/Modular/damage_mechanism.cpp b/src/Continuum_mechanics/Umat/Modular/damage_mechanism.cpp index 89c3bbde6..051f44c53 100644 --- a/src/Continuum_mechanics/Umat/Modular/damage_mechanism.cpp +++ b/src/Continuum_mechanics/Umat/Modular/damage_mechanism.cpp @@ -25,6 +25,7 @@ along with simcoon. If not, see . #include #include #include +#include namespace simcoon { @@ -89,6 +90,8 @@ void DamageMechanism::configure(const arma::vec& props, int& offset) { void DamageMechanism::register_variables() { ivc_.add_scalar("D", 0.0); ivc_.add_scalar("Y_max", 0.0); + D_index_ = ivc_.index_of("D"); + Y_max_index_ = ivc_.index_of("Y_max"); } // ========== Constitutive Computations ========== @@ -98,6 +101,18 @@ double DamageMechanism::compute_driving_force(const arma::vec& sigma_eff, const return 0.5 * arma::dot(sigma_eff, (M_cached_t_ * stress(sigma_eff)).to_arma_voigt()); } +double DamageMechanism::driving_force(const arma::vec& sigma_eff) const { + if (sigma_eff.n_elem != 6) { + return compute_driving_force(sigma_eff, M_cached_); + } + if (!Y_sigma_valid_ || std::memcmp(Y_sigma_.memptr(), sigma_eff.memptr(), 6 * sizeof(double)) != 0) { + Y_sigma_value_ = compute_driving_force(sigma_eff, M_cached_); + std::memcpy(Y_sigma_.memptr(), sigma_eff.memptr(), 6 * sizeof(double)); + Y_sigma_valid_ = true; + } + return Y_sigma_value_; +} + double DamageMechanism::compute_damage(double Y_eff) const { // No damage below threshold if (Y_eff <= Y_0_) { @@ -129,7 +144,7 @@ double DamageMechanism::compute_damage(double Y_eff) const { } double DamageMechanism::get_damage() const { - return ivc_.get("D").scalar(); + return ivc_.at(D_index_).scalar(); } void DamageMechanism::compute_constraints( @@ -144,8 +159,8 @@ void DamageMechanism::compute_constraints( Y_crit.set_size(1); // Get current damage and history - D_current_ = ivc_.get("D").scalar(); - double Y_max = ivc_.get("Y_max").scalar(); + D_current_ = ivc_.at(D_index_).scalar(); + double Y_max = ivc_.at(Y_max_index_).scalar(); // compliance cache keyed on L: a hyperelastic block moves L at every refresh, a linear one inverts once if (!M_cached_valid_ || !arma::approx_equal(L, L_cached_, "absdiff", 0.0)) { @@ -153,10 +168,11 @@ void DamageMechanism::compute_constraints( M_cached_ = arma::inv(L); M_cached_t_ = tensor4(M_cached_, Tensor4Type::compliance); M_cached_valid_ = true; + Y_sigma_valid_ = false; } // Compute current driving force - Y_current_ = compute_driving_force(sigma, M_cached_); + Y_current_ = driving_force(sigma); const double Y_eff = std::max(Y_current_, Y_max); @@ -169,7 +185,7 @@ void DamageMechanism::compute_constraints( // dD/dY for the tangent, only while damage grows in this increment (Y beyond the history // maximum at the start of the step): under unloading the response is (1-D) L, no softening - const bool loading = Y_current_ > ivc_.get("Y_max").scalar_start(); + const bool loading = Y_current_ > ivc_.at(Y_max_index_).scalar_start(); if (loading && Y_eff > Y_0_ && D_current_ < D_c_) { switch (damage_type_) { case DamageType::LINEAR: @@ -220,23 +236,23 @@ double DamageMechanism::consistency_residual(const arma::vec& sigma_eff) const { if (!M_cached_valid_) { return 0.; } - const double Y = compute_driving_force(sigma_eff, M_cached_); - const double Y_used = ivc_.get("Y_max").scalar(); - const double target = std::max(ivc_.get("Y_max").scalar_start(), Y); + const double Y = driving_force(sigma_eff); + const double Y_used = ivc_.at(Y_max_index_).scalar(); + const double target = std::max(ivc_.at(Y_max_index_).scalar_start(), Y); return std::abs(target - Y_used) / std::max({std::abs(target), Y_0_, 1e-12}); } arma::mat DamageMechanism::stress_map(const arma::vec& sigma_eff) const { // sigma = (1 - D) sigma_eff with dD = D'(Y) (S sigma_eff) . d sigma_eff while damage grows arma::mat Q = stiffness_reduction() * arma::eye(6, 6); - if (dD_dY_ > simcoon::iota && ivc_.get("D").scalar() < D_c_ && M_cached_valid_) { + if (dD_dY_ > simcoon::iota && ivc_.at(D_index_).scalar() < D_c_ && M_cached_valid_) { Q -= dD_dY_ * (sigma_eff * (M_cached_ * sigma_eff).t()); } return Q; } double DamageMechanism::stiffness_reduction() const { - return 1.0 - ivc_.get("D").scalar(); + return 1.0 - ivc_.at(D_index_).scalar(); } arma::vec DamageMechanism::inelastic_strain() const { @@ -250,9 +266,9 @@ void DamageMechanism::update( int /*offset*/ ) { // history from the step START, never the running max over iterates (an overshooting iterate freezes damage) - double& Y_max = ivc_.get("Y_max").scalar(); - Y_max = std::max(ivc_.get("Y_max").scalar_start(), Y_current_); - double& D = ivc_.get("D").scalar(); + double& Y_max = ivc_.at(Y_max_index_).scalar(); + Y_max = std::max(ivc_.at(Y_max_index_).scalar_start(), Y_current_); + double& D = ivc_.at(D_index_).scalar(); D = compute_damage(Y_max); } @@ -277,7 +293,7 @@ void DamageMechanism::compute_work( double& Wm_d ) const { // Get damage increment - double dD = ivc_.get("D").delta_scalar(); + double dD = ivc_.at(D_index_).delta_scalar(); Wm_r = 0.0; Wm_ir = 0.0; diff --git a/src/Continuum_mechanics/Umat/Modular/hardening.cpp b/src/Continuum_mechanics/Umat/Modular/hardening.cpp index 650aeb219..237274ebe 100644 --- a/src/Continuum_mechanics/Umat/Modular/hardening.cpp +++ b/src/Continuum_mechanics/Umat/Modular/hardening.cpp @@ -23,16 +23,91 @@ along with simcoon. If not, see . #include #include #include +#include #include #include +#include namespace simcoon { namespace { +// typed-path operations in order, one rounding per statement: bit-identical unless FMA crosses statements + +// back-strain registered under key at index; the name is checked in debug builds only +template +auto& back_strain(Collection& ivc, size_t index, const std::string& key) { + assert(index < ivc.size() && ivc.at(index).name() == key); + (void)key; + return ivc.at(index); +} + +// tensor2::from_voigt(v, Tensor2Type::strain).mat() +inline arma::mat::fixed<3,3> strain_mat(const arma::vec& v) { + if (v.n_elem != 6) { + throw std::invalid_argument("tensor2: expected 6-element vector, got " + std::to_string(v.n_elem)); + } + arma::mat::fixed<3,3> m; + m(0,0) = v(0); m(1,1) = v(1); m(2,2) = v(2); + m(0,1) = 0.5 * v(3); m(1,0) = 0.5 * v(3); + m(0,2) = 0.5 * v(4); m(2,0) = 0.5 * v(4); + m(1,2) = 0.5 * v(5); m(2,1) = 0.5 * v(5); + return m; +} + +// tensor2(m, Tensor2Type::strain).voigt() +inline void store_strain(const arma::mat::fixed<3,3>& m, arma::vec& v) { + v.set_size(6); + v(0) = m(0,0); v(1) = m(1,1); v(2) = m(2,2); + v(3) = m(0,1) + m(1,0); + v(4) = m(0,2) + m(2,0); + v(5) = m(1,2) + m(2,1); +} + // X = (2/3) C alpha, re-tagged STRESS through the 3x3: alpha is strain-typed (factor-2 shear), // so a strain-typed X would double-count the shear terms in sigma - X and X:n -inline tensor2 backstress_t(double C, const tensor2& alpha) { - return stress(arma::mat::fixed<3,3>(((2.0 / 3.0) * C * alpha).mat())); +inline tensor2 backstress_of(double C, const arma::vec& a) { + const double k = (2.0 / 3.0) * C; + arma::mat::fixed<3,3> X = strain_mat(a); + for (arma::uword i = 0; i < 9; ++i) { + X(i) = X(i) * k; + } + return stress(X); +} + +// alpha <- alpha_from + dp n +inline void add_flow(const arma::vec& alpha_from, double dp, const tensor2& n, arma::vec& alpha) { + arma::mat::fixed<3,3> a = strain_mat(alpha_from); + const arma::mat::fixed<3,3>& nm = n.mat(); + for (arma::uword i = 0; i < 9; ++i) { + const double t = nm(i) * dp; + a(i) = a(i) + t; + } + store_strain(a, alpha); +} + +// alpha <- alpha + dp (n - D alpha) +inline void add_flow_recovery(InternalVariable& a_var, double dp, double D, const tensor2& n) { + arma::mat::fixed<3,3> a = strain_mat(a_var.raw_voigt()); + const arma::mat::fixed<3,3>& nm = n.mat(); + for (arma::uword i = 0; i < 9; ++i) { + const double t1 = a(i) * D; + const double t2 = nm(i) - t1; + const double t3 = t2 * dp; + a(i) = a(i) + t3; + } + store_strain(a, a_var.raw_voigt()); +} + +// alpha <- s (alpha_n + dp n), the Armstrong-Frederick backward-Euler closed form +inline void refresh_from_start(InternalVariable& a_var, double dp, double s, const tensor2& n) { + arma::mat::fixed<3,3> a = strain_mat(a_var.raw_voigt_start()); + const arma::mat::fixed<3,3>& nm = n.mat(); + for (arma::uword i = 0; i < 9; ++i) { + const double t = nm(i) * dp; + const double u = a(i) + t; + a(i) = u * s; + } + store_strain(a, a_var.raw_voigt()); } } // namespace @@ -168,12 +243,13 @@ void PragerHardening::configure(const arma::vec& props, int& offset) { void PragerHardening::register_variables(InternalVariableCollection& ivc) { ivc.add_vec(a_key_, arma::zeros(6), true); // back-strain (strain-like) + a_index_ = ivc.index_of(a_key_); } void PragerHardening::compute_backstresses(const InternalVariableCollection& ivc, std::vector& X_i) const { X_i.resize(1, tensor2(Tensor2Type::stress)); - X_i[0] = backstress_t(C_, ivc.get(a_key_).as_tensor2()); + X_i[0] = backstress_of(C_, back_strain(ivc, a_index_, a_key_).raw_voigt()); } double PragerHardening::hardening_modulus(const tensor2& n, @@ -183,15 +259,15 @@ double PragerHardening::hardening_modulus(const tensor2& n, } void PragerHardening::update(double dp, const tensor2& n, InternalVariableCollection& ivc) { - auto& a_var = ivc.get(a_key_); - a_var.set_tensor2(a_var.as_tensor2() + dp * n); + auto& a_var = back_strain(ivc, a_index_, a_key_); + add_flow(a_var.raw_voigt(), dp, n, a_var.raw_voigt()); // alpha + dp n } void PragerHardening::refresh_state(double dp, const tensor2& n, InternalVariableCollection& ivc) const { // Linear: alpha = alpha_n + dp n (exact backward Euler). - auto& a_var = ivc.get(a_key_); - a_var.set_tensor2(a_var.as_tensor2_start() + dp * n); + auto& a_var = back_strain(ivc, a_index_, a_key_); + add_flow(a_var.raw_voigt_start(), dp, n, a_var.raw_voigt()); } void PragerHardening::backward_euler_factors(double dp, const arma::vec& n, @@ -210,34 +286,32 @@ void ArmstrongFrederickHardening::configure(const arma::vec& props, int& offset) void ArmstrongFrederickHardening::register_variables(InternalVariableCollection& ivc) { ivc.add_vec(a_key_, arma::zeros(6), true); // back-strain (strain-like) + a_index_ = ivc.index_of(a_key_); } void ArmstrongFrederickHardening::compute_backstresses( const InternalVariableCollection& ivc, std::vector& X_i) const { X_i.resize(1, tensor2(Tensor2Type::stress)); - X_i[0] = backstress_t(C_, ivc.get(a_key_).as_tensor2()); + X_i[0] = backstress_of(C_, back_strain(ivc, a_index_, a_key_).raw_voigt()); } double ArmstrongFrederickHardening::hardening_modulus( const tensor2& n, const std::vector& X_i) const { // H_kin = (2/3) C - D (X:n); X:n = stress-Voigt . engineering-strain-Voigt const double nn = arma::accu(n.mat() % n.mat()); - return (2.0 / 3.0) * C_ * nn - D_ * arma::dot(X_i[0].to_arma_voigt(), n.to_arma_voigt()); + return (2.0 / 3.0) * C_ * nn - D_ * arma::dot(X_i[0].voigt(), n.voigt()); } void ArmstrongFrederickHardening::update(double dp, const tensor2& n, InternalVariableCollection& ivc) { // dα = (n − D α) dp (AF in α-form; equivalent to dX/dp = (2/3)C n − D X) - auto& a_var = ivc.get(a_key_); - const tensor2 a_t = a_var.as_tensor2(); - a_var.set_tensor2(a_t + dp * (n - D_ * a_t)); + add_flow_recovery(back_strain(ivc, a_index_, a_key_), dp, D_, n); } void ArmstrongFrederickHardening::refresh_state(double dp, const tensor2& n, InternalVariableCollection& ivc) const { // Backward Euler closed form: alpha = (alpha_n + dp n) / (1 + D dp). - auto& a_var = ivc.get(a_key_); - a_var.set_tensor2((1.0 / (1.0 + D_ * dp)) * (a_var.as_tensor2_start() + dp * n)); + refresh_from_start(back_strain(ivc, a_index_, a_key_), dp, (1.0 / (1.0 + D_ * dp)), n); } void ArmstrongFrederickHardening::backward_euler_factors(double dp, const arma::vec& n, @@ -245,7 +319,7 @@ void ArmstrongFrederickHardening::backward_euler_factors(double dp, const arma:: double& gamma, arma::vec& beta) const { const double f = (2.0 / 3.0) * C_ / (1.0 + D_ * dp); gamma = dp * f; - beta = f * (n - D_ * ivc.get(a_key_).raw_voigt()); + beta = f * (n - D_ * back_strain(ivc, a_index_, a_key_).raw_voigt()); } // ChabocheHardening @@ -259,18 +333,20 @@ void ChabocheHardening::configure(const arma::vec& props, int& offset) { void ChabocheHardening::register_variables(InternalVariableCollection& ivc) { a_keys_.resize(static_cast(N_)); + a_index_.resize(static_cast(N_)); for (int i = 0; i < N_; ++i) { a_keys_[i] = "a_" + std::to_string(i); ivc.add_vec(a_keys_[i], arma::zeros(6), true); + a_index_[i] = ivc.index_of(a_keys_[i]); } } void ChabocheHardening::compute_backstresses(const InternalVariableCollection& ivc, std::vector& X_i) const { - // X_i stress-typed (backstress_t): a strain-typed carrier would put the factor-2 shear back + // X_i stress-typed (backstress_of): a strain-typed carrier would put the factor-2 shear back X_i.resize(static_cast(N_), tensor2(Tensor2Type::stress)); for (int i = 0; i < N_; ++i) { - X_i[static_cast(i)] = backstress_t(C_(i), ivc.get(a_keys_[i]).as_tensor2()); + X_i[static_cast(i)] = backstress_of(C_(i), back_strain(ivc, a_index_[i], a_keys_[i]).raw_voigt()); } } @@ -278,12 +354,12 @@ double ChabocheHardening::hardening_modulus(const tensor2& n, const std::vector& X_i) const { // H_kin = Σ_i [ (2/3) C_i − D_i (X_i : n) ] — exact slope per // branch (see ArmstrongFrederick). - const arma::vec n_v = n.to_arma_voigt(); + const arma::vec::fixed<6> n_v = n.voigt(); const double nn = arma::accu(n.mat() % n.mat()); double H_kin = 0.0; for (int i = 0; i < N_; ++i) { H_kin += (2.0 / 3.0) * C_(i) * nn - - D_(i) * arma::dot(X_i[static_cast(i)].to_arma_voigt(), n_v); + - D_(i) * arma::dot(X_i[static_cast(i)].voigt(), n_v); } return H_kin; } @@ -292,9 +368,7 @@ void ChabocheHardening::update(double dp, const tensor2& n, InternalVariableCollection& ivc) { // dα_i = (n − D_i α_i) dp for (int i = 0; i < N_; ++i) { - auto& a_var = ivc.get(a_keys_[i]); - const tensor2 a_t = a_var.as_tensor2(); - a_var.set_tensor2(a_t + dp * (n - D_(i) * a_t)); + add_flow_recovery(back_strain(ivc, a_index_[i], a_keys_[i]), dp, D_(i), n); } } @@ -302,9 +376,7 @@ void ChabocheHardening::refresh_state(double dp, const tensor2& n, InternalVariableCollection& ivc) const { // Per-branch backward Euler closed form (see ArmstrongFrederick). for (int i = 0; i < N_; ++i) { - auto& a_var = ivc.get(a_keys_[i]); - a_var.set_tensor2((1.0 / (1.0 + D_(i) * dp)) - * (a_var.as_tensor2_start() + dp * n)); + refresh_from_start(back_strain(ivc, a_index_[i], a_keys_[i]), dp, (1.0 / (1.0 + D_(i) * dp)), n); } } @@ -316,7 +388,7 @@ void ChabocheHardening::backward_euler_factors(double dp, const arma::vec& n, for (int i = 0; i < N_; ++i) { const double f = (2.0 / 3.0) * C_(i) / (1.0 + D_(i) * dp); gamma += dp * f; - beta += f * (n - D_(i) * ivc.get(a_keys_[i]).raw_voigt()); + beta += f * (n - D_(i) * back_strain(ivc, a_index_[i], a_keys_[i]).raw_voigt()); } } diff --git a/src/Continuum_mechanics/Umat/Modular/internal_variable.cpp b/src/Continuum_mechanics/Umat/Modular/internal_variable.cpp index ec7dab910..a7290e425 100644 --- a/src/Continuum_mechanics/Umat/Modular/internal_variable.cpp +++ b/src/Continuum_mechanics/Umat/Modular/internal_variable.cpp @@ -23,6 +23,7 @@ along with simcoon. If not, see . #include #include #include +#include namespace simcoon { @@ -312,7 +313,11 @@ void InternalVariable::pack(arma::vec& statev) const { statev(offset) = scalar_value_; break; case IVarType::VECTOR_6: - statev.subvec(offset, offset + 5) = vec_value_; + if (vec_value_.n_elem == 6) { + std::copy(vec_value_.begin(), vec_value_.end(), statev.begin() + offset); + } else { + statev.subvec(offset, offset + 5) = vec_value_; // reports the size mismatch + } break; case IVarType::MATRIX_6x6: for (unsigned int i = 0; i < 6; ++i) { @@ -337,8 +342,10 @@ void InternalVariable::unpack(const arma::vec& statev) { scalar_start_ = scalar_value_; break; case IVarType::VECTOR_6: - vec_value_ = statev.subvec(offset, offset + 5); - vec_start_ = vec_value_; + vec_value_.set_size(6); + vec_start_.set_size(6); + std::copy(statev.begin() + offset, statev.begin() + offset + 6, vec_value_.begin()); + std::copy(statev.begin() + offset, statev.begin() + offset + 6, vec_start_.begin()); break; case IVarType::MATRIX_6x6: for (unsigned int i = 0; i < 6; ++i) { diff --git a/src/Continuum_mechanics/Umat/Modular/legacy_adapters.cpp b/src/Continuum_mechanics/Umat/Modular/legacy_adapters.cpp index 02a14c976..566029a03 100644 --- a/src/Continuum_mechanics/Umat/Modular/legacy_adapters.cpp +++ b/src/Continuum_mechanics/Umat/Modular/legacy_adapters.cpp @@ -22,8 +22,10 @@ along with simcoon. If not, see . #include #include +#include #include #include +#include using namespace arma; @@ -200,12 +202,29 @@ void umat_legacy_modular( const int& ndi, const int& nshr, const bool& start, double& tnew_dt, const int& tangent_mode, const int& ncp) { - const auto it = registry().find(umat_name); - if (it == registry().end()) { - throw std::invalid_argument("umat_legacy_modular: no adapter registered for '" - + umat_name + "'"); + // per-thread memo of the last (name, props) translation; the key is written last so a throw leaves no stale pair + struct translation { + std::string name; + std::vector props; + vec props_mod; + }; + thread_local translation last; + const bool hit = !last.name.empty() && umat_name == last.name && props.n_elem == last.props.size() + && (last.props.empty() + || std::memcmp(last.props.data(), props.memptr(), last.props.size() * sizeof(double)) == 0); + if (!hit) { + const auto it = registry().find(umat_name); + if (it == registry().end()) { + throw std::invalid_argument("umat_legacy_modular: no adapter registered for '" + + umat_name + "'"); + } + vec props_mod = it->second(props); + last.name.clear(); + last.props.assign(props.begin(), props.end()); + last.props_mod = std::move(props_mod); + last.name = umat_name; } - const vec props_mod = it->second(props); + const vec& props_mod = last.props_mod; try { umat_modular(umat_name, Etot, DEtot, sigma, Lt, L, DR, static_cast(props_mod.n_elem), props_mod, diff --git a/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp b/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp index 6e82b574e..9b69eaa62 100644 --- a/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp +++ b/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp @@ -35,10 +35,30 @@ along with simcoon. If not, see . #include #include #include +#include #include namespace simcoon { +namespace { + +/// approx_equal(DR, eye(3, 3), "absdiff", 0.0) without building the identity (NaN or wrong size: false) +bool is_identity_rotation(const arma::mat& DR) { + if (DR.n_rows != 3 || DR.n_cols != 3) { + return false; + } + for (arma::uword j = 0; j < 3; ++j) { + for (arma::uword i = 0; i < 3; ++i) { + if (DR(i, j) != ((i == j) ? 1.0 : 0.0)) { + return false; + } + } + } + return true; +} + +} // namespace + // ========== Constructor ========== ModularUMAT::ModularUMAT() @@ -312,7 +332,7 @@ void ModularUMAT::run( } // Apply rotation for objectivity (DR = I under small strain: nothing to do) - const bool rotate = !arma::approx_equal(DR, arma::eye(3, 3), "absdiff", 0.0); + const bool rotate = !is_identity_rotation(DR); if (rotate) { for (auto& mech : mechanisms_) { mech->rotate(DR); @@ -371,11 +391,14 @@ void ModularUMAT::run( // Wm_r closes the balance Wm = Wm_r + Wm_ir + Wm_d double Wm_ir_inc = 0.0, Wm_d_inc = 0.0; - for (const auto& mech : mechanisms_) { - double Wm_r_m = 0.0, Wm_ir_m = 0.0, Wm_d_m = 0.0; - mech->compute_work(sigma_start_, sigma, Etot, Etot + DEtot, Wm_r_m, Wm_ir_m, Wm_d_m); - Wm_ir_inc += Wm_ir_m; - Wm_d_inc += Wm_d_m; + if (!mechanisms_.empty()) { + const arma::vec::fixed<6> Etot_end = Etot + DEtot; // once, not per mechanism + for (const auto& mech : mechanisms_) { + double Wm_r_m = 0.0, Wm_ir_m = 0.0, Wm_d_m = 0.0; + mech->compute_work(sigma_start_, sigma, Etot, Etot_end, Wm_r_m, Wm_ir_m, Wm_d_m); + Wm_ir_inc += Wm_ir_m; + Wm_d_inc += Wm_d_m; + } } Wm += Wm_inc; @@ -392,11 +415,18 @@ void ModularUMAT::run( void ModularUMAT::refresh_stress(const arma::vec& Etot_end, double DT_init, int ndi, int nshr, arma::vec& sigma) { - arma::vec E_inel = arma::zeros(6); - for (const auto& mech : mechanisms_) { - E_inel += mech->inelastic_strain(); + // no mechanism: the inelastic sum is +0 and x - (+0) = x bit for bit, so it is skipped + if (!elasticity_.is_configured()) { + throw std::runtime_error("ElasticityModule: not configured"); // thermal_strain's guard + } + arma::vec::fixed<6> Eel = Etot_end - elasticity_.alpha() * DT_init; + if (!mechanisms_.empty()) { + arma::vec::fixed<6> E_inel(arma::fill::zeros); + for (const auto& mech : mechanisms_) { + E_inel += mech->inelastic_strain(); + } + Eel -= E_inel; } - const arma::vec Eel = Etot_end - elasticity_.thermal_strain(DT_init) - E_inel; elasticity_.evaluate(Eel, ndi, nshr, sigma_eff_, L_cur_); // strain equivalence (Lemaitre): the mechanisms work on sigma_eff_, damage scales it sigma = stiffness_reduction() * sigma_eff_; @@ -424,7 +454,7 @@ bool ModularUMAT::return_mapping( } // with no constraints this elastic call is the whole response - const arma::vec Etot_end = Etot + DEtot; + const arma::vec::fixed<6> Etot_end = Etot + DEtot; for (auto& mech : mechanisms_) { mech->predict(Etot_end, DTime); // closed-form parts first (viscoelastic branches) } @@ -514,8 +544,15 @@ void ModularUMAT::return_mapping_ccp( hooks.update = [&](const arma::vec& ds) { for (size_t m = 0; m < mechanisms_.size(); ++m) mechanisms_[m]->update(ds, mech_offset_[m]); }; - // D may have evolved in update: the reduction factor is re-evaluated with the stress - hooks.refresh = [&]() { refresh_stress(Etot_end, DT_init, ndi, nshr, sigma); }; + // D may have evolved in update: the reduction factor is re-evaluated with the stress. + // one captured reference keeps the closure in std::function's small buffer (no heap per call) + struct refresh_args { + const arma::vec& Etot_end; + double DT_init; + int ndi, nshr; + arma::vec& sigma; + } const ra{Etot_end, DT_init, ndi, nshr, sigma}; + hooks.refresh = [this, &ra]() { refresh_stress(ra.Etot_end, ra.DT_init, ra.ndi, ra.nshr, ra.sigma); }; hooks.consistency = [&]() { double res = 0.0; for (const auto& mech : mechanisms_) res += mech->consistency_residual(sigma_eff_); @@ -658,28 +695,30 @@ void ModularUMAT::assemble_jacobian( // Phase 2: B_lj = -dPhi^l/dsigma . kappa^j + K^lj (engineering Voigt dot); strain-form Phi // (viscoelastic) returns an empty dPhi_dsigma and contributes no row B.zeros(); - // Fetch each mechanism's kappa list once — the const-refs stay valid - // for the whole phase (no compute_constraints call in between). - std::vector*> kappa_all(mechanisms_.size()); - for (size_t jm = 0; jm < mechanisms_.size(); ++jm) { - kappa_all[jm] = &mechanisms_[jm]->kappa(sigma_eff_, DT, L_cur_); - } - for (size_t lm = 0; lm < mechanisms_.size(); ++lm) { - const auto& dPhi_l_all = mechanisms_[lm]->dPhi_dsigma(sigma_eff_); - if (dPhi_l_all.empty()) continue; - for (size_t l_c = 0; l_c < dPhi_l_all.size(); ++l_c) { - const int row = mech_offset_[lm] + static_cast(l_c); - const arma::vec::fixed<6> dPhi_l = dPhi_l_all[l_c].voigt(); - for (size_t jm = 0; jm < mechanisms_.size(); ++jm) { - const auto& kappa_j_all = *kappa_all[jm]; - for (size_t j_c = 0; j_c < kappa_j_all.size(); ++j_c) { - if (lm == jm && l_c == j_c) continue; // diagonal last - const int col = mech_offset_[jm] + static_cast(j_c); - B(row, col) = -arma::dot(dPhi_l, kappa_j_all[j_c].voigt()) - + mechanisms_[lm]->K_cross( - static_cast(l_c), - *mechanisms_[jm], - static_cast(j_c)); + // single row: no off-diagonal entry, phase 2 skipped (compute_tangent fetches dPhi and kappa itself) + if (B.n_rows > 1) { + // kappa lists fetched once: the refs stay valid, nothing below calls compute_constraints + std::vector*> kappa_all(mechanisms_.size()); + for (size_t jm = 0; jm < mechanisms_.size(); ++jm) { + kappa_all[jm] = &mechanisms_[jm]->kappa(sigma_eff_, DT, L_cur_); + } + for (size_t lm = 0; lm < mechanisms_.size(); ++lm) { + const auto& dPhi_l_all = mechanisms_[lm]->dPhi_dsigma(sigma_eff_); + if (dPhi_l_all.empty()) continue; + for (size_t l_c = 0; l_c < dPhi_l_all.size(); ++l_c) { + const int row = mech_offset_[lm] + static_cast(l_c); + const arma::vec::fixed<6> dPhi_l = dPhi_l_all[l_c].voigt(); + for (size_t jm = 0; jm < mechanisms_.size(); ++jm) { + const auto& kappa_j_all = *kappa_all[jm]; + for (size_t j_c = 0; j_c < kappa_j_all.size(); ++j_c) { + if (lm == jm && l_c == j_c) continue; // diagonal last + const int col = mech_offset_[jm] + static_cast(j_c); + B(row, col) = -arma::dot(dPhi_l, kappa_j_all[j_c].voigt()) + + mechanisms_[lm]->K_cross( + static_cast(l_c), + *mechanisms_[jm], + static_cast(j_c)); + } } } } @@ -726,20 +765,20 @@ void ModularUMAT::compute_tangent( // mechanisms see sigma_eff_; damage enters last through its stress_map(). const arma::vec& sigma_eff = sigma_eff_; - // algorithmic assembly only for stress-dependent Phi with an analytic flow Hessian - std::vector algo; + // algorithmic assembly only for stress-dependent Phi with an analytic flow Hessian (the Hessians are kept) + std::vector*>> algo; if (tangent_mode == tangent_closest_point) { // Exact operator of the closest-point solve; every multiplier-carrying row is inside it. for (size_t m = 0; m < mechanisms_.size(); ++m) { - if (mechanisms_[m]->carries_multipliers()) algo.push_back(m); + if (mechanisms_[m]->carries_multipliers()) algo.emplace_back(m, nullptr); } Lt = cpp_consistent_tangent(cpp_result_, L_cur_).Lt; } else if (tangent_mode == tangent_algorithmic && arma::any(Ds_total > simcoon::iota)) { // no active multiplier: the assembly masks everything and returns L, skip the Hessians for (size_t m = 0; m < mechanisms_.size(); ++m) { - if (mechanisms_[m]->dLambda_dsigma(sigma_eff) != nullptr && - !mechanisms_[m]->dPhi_dsigma(sigma_eff).empty()) { - algo.push_back(m); + const std::vector* hess = mechanisms_[m]->dLambda_dsigma(sigma_eff); + if (hess != nullptr && !mechanisms_[m]->dPhi_dsigma(sigma_eff).empty()) { + algo.emplace_back(m, hess); } } } @@ -757,10 +796,10 @@ void ModularUMAT::compute_tangent( std::vector kappa_j; std::vector dPhi_l; std::vector dLambda_l; - for (size_t m : algo) { + for (const auto& [m, hess] : algo) { const auto& dPhi_all = mechanisms_[m]->dPhi_dsigma(sigma_eff); const auto& kappa_all = mechanisms_[m]->kappa(sigma_eff, 0.0, L_cur_); - const auto& hess_all = *mechanisms_[m]->dLambda_dsigma(sigma_eff); + const auto& hess_all = *hess; for (size_t c = 0; c < dPhi_all.size(); ++c) { rows.push_back(mech_offset_[m] + static_cast(c)); dPhi_l.emplace_back(dPhi_all[c].voigt()); @@ -783,7 +822,7 @@ void ModularUMAT::compute_tangent( } for (size_t m = 0; m < mechanisms_.size(); ++m) { - if (std::find(algo.begin(), algo.end(), m) != algo.end()) { + if (std::find_if(algo.begin(), algo.end(), [m](const auto& a) { return a.first == m; }) != algo.end()) { continue; } mechanisms_[m]->tangent_contribution( diff --git a/src/Continuum_mechanics/Umat/Modular/plasticity_mechanism.cpp b/src/Continuum_mechanics/Umat/Modular/plasticity_mechanism.cpp index 270ed1597..b60300d37 100644 --- a/src/Continuum_mechanics/Umat/Modular/plasticity_mechanism.cpp +++ b/src/Continuum_mechanics/Umat/Modular/plasticity_mechanism.cpp @@ -32,8 +32,27 @@ along with simcoon. If not, see . namespace simcoon { namespace { + // state_drift denominator floor: rejection at near-zero Dp needs a flow-rule error > tol * 1e-4 strain constexpr double drift_scale_floor = 1.0e-4; + +// HT = H.each_row() % T.t(), A = I + gamma HT in the expressions' order: bit-identical unless FMA crosses statements +void fill_HT(const arma::mat& H, const arma::vec& T, arma::mat::fixed<6,6>& HT) { + for (arma::uword j = 0; j < 6; ++j) { + for (arma::uword i = 0; i < 6; ++i) { + HT(i, j) = H(i, j) * T(j); + } + } +} + +void fill_I_plus_gamma(const arma::mat::fixed<6,6>& HT, double gamma, arma::mat::fixed<6,6>& A) { + for (arma::uword j = 0; j < 6; ++j) { + for (arma::uword i = 0; i < 6; ++i) { + const double g = HT(i, j) * gamma; + A(i, j) = ((i == j) ? 1.0 : 0.0) + g; + } + } +} } // namespace // ========== Constructor ========== @@ -110,11 +129,20 @@ void PlasticityMechanism::register_variables() { // ========== Yield-side queries ========== +arma::vec PlasticityMechanism::total_backstress_voigt() const { + kin_hard_->compute_backstresses(ivc_, X_branches_); + tensor2 X_t = tensor2::zeros(Tensor2Type::stress); + for (const auto& x : X_branches_) { + X_t += x; + } + return X_t.to_arma_voigt(); +} + double PlasticityMechanism::equivalent_stress(const arma::vec& sigma) const { if (kin_hard_->num_backstresses() == 0) { return yield_->equivalent_stress(sigma); } - return yield_->equivalent_stress(sigma - kin_hard_->total_backstress(ivc_).to_arma_voigt()); + return yield_->equivalent_stress(sigma - total_backstress_voigt()); } double PlasticityMechanism::equivalent_stress(const tensor2& sigma) const { @@ -134,7 +162,7 @@ arma::vec PlasticityMechanism::flow_direction(const arma::vec& sigma) const { if (kin_hard_->num_backstresses() == 0) { return yield_->flow_direction(sigma); } - return yield_->flow_direction(sigma - kin_hard_->total_backstress(ivc_).to_arma_voigt()); + return yield_->flow_direction(sigma - total_backstress_voigt()); } tensor2 PlasticityMechanism::flow_direction(const tensor2& sigma) const { @@ -164,11 +192,9 @@ void PlasticityMechanism::compute_constraints( X_t += x; } const arma::vec sigma_eff = sigma - X_t.to_arma_voigt(); - sigma_eq = yield_->equivalent_stress(sigma_eff); - flow_dir_ = yield_->flow_direction(sigma_eff); + yield_->equivalent_stress_and_flow(sigma_eff, sigma_eq, flow_dir_); } else { - sigma_eq = yield_->equivalent_stress(sigma); - flow_dir_ = yield_->flow_direction(sigma); + yield_->equivalent_stress_and_flow(sigma, sigma_eq, flow_dir_); } const double R = iso_hard_->R(p); @@ -208,7 +234,7 @@ const std::vector* PlasticityMechanism::dLambda_dsigma( } const bool has_kin = kin_hard_->num_backstresses() > 0; const arma::vec sigma_eff = - has_kin ? arma::vec(sigma - kin_hard_->total_backstress(ivc_).to_arma_voigt()) : sigma; + has_kin ? arma::vec(sigma - total_backstress_voigt()) : sigma; hessian_cache_[0] = tensor4(arma::mat(yield_->flow_hessian(sigma_eff)), Tensor4Type::compliance); return &hessian_cache_; @@ -223,36 +249,41 @@ bool PlasticityMechanism::refresh_state( const arma::vec& Ds_total, int offset) { const double dp = std::max(Ds_total(offset), 0.0); // the integrator projects Ds >= 0 too - const arma::vec T = Ir05(); // engineering -> tensorial shear + static const arma::vec T = Ir05(); // engineering -> tensorial shear const bool has_kin = kin_hard_->num_backstresses() > 0; auto& p_var = ivc_.at(p_index_); p_var.scalar() = p_var.scalar_start() + dp; - // Newton on the n <-> X coupling (class note in the header). + // Newton on the n <-> X coupling (see header); flow and Hessian of the final xi feed the derivatives below arma::vec n(6), xi(6); double gamma = 0.0; arma::vec beta = arma::zeros(6); + double sigma_eq = 0.0; + arma::mat H; if (!has_kin) { - n = yield_->flow_direction(sigma); xi = sigma; + yield_->equivalent_stress_flow_hessian(xi, sigma_eq, n, H); } else { // Start = normal of the dp-relaxed start backstress: exact for von Mises (zero steps); // warm-starting from the previous normal measured worse (J2 pays real steps). - kin_hard_->refresh_state(dp, strain(arma::vec(arma::zeros(6))), ivc_); // alpha_i^n / (1 + D_i dp) - n = yield_->flow_direction(sigma - kin_hard_->total_backstress(ivc_).to_arma_voigt()); + kin_hard_->refresh_state(dp, tensor2::zeros(Tensor2Type::strain), ivc_); // alpha_i^n / (1 + D_i dp) + n = yield_->flow_direction(sigma - total_backstress_voigt()); bool converged = false; for (int it = 0; it < 25; ++it) { kin_hard_->refresh_state(dp, strain(n), ivc_); - xi = sigma - kin_hard_->total_backstress(ivc_).to_arma_voigt(); - const arma::vec G = n - yield_->flow_direction(xi); + xi = sigma - total_backstress_voigt(); + arma::vec flow; + yield_->equivalent_stress_flow_hessian(xi, sigma_eq, flow, H); + const arma::vec G = n - flow; if (arma::norm(G, 2) <= 1e-12 * std::max(1.0, arma::norm(n, 2))) { converged = true; break; } kin_hard_->backward_euler_factors(dp, n, ivc_, gamma, beta); - const arma::mat H = yield_->flow_hessian(xi); - const arma::mat A = arma::eye(6, 6) + gamma * (H.each_row() % T.t()); // I + gamma H T + arma::mat::fixed<6,6> HT, A; + fill_HT(H, T, HT); + fill_I_plus_gamma(HT, gamma, A); // I + gamma H T small_lu<6> lu_A; // on the stack: a LAPACK 6x6 solve per step otherwise if (!lu_A.factor(A)) { return false; @@ -270,9 +301,8 @@ bool PlasticityMechanism::refresh_state( // Total derivatives at this state (class note in the header). ClosestPointIngredients& out = cpp_cache_[0]; - const arma::mat H = yield_->flow_hessian(xi); const double dR_dp = iso_hard_->dR_dp(p_var.scalar()); - out.Phi = yield_->equivalent_stress(xi) - iso_hard_->R(p_var.scalar()) - sigma_Y_; + out.Phi = sigma_eq - iso_hard_->R(p_var.scalar()) - sigma_Y_; out.Lambda = n; if (!has_kin) { out.dPhi_dsigma = n; @@ -281,9 +311,11 @@ bool PlasticityMechanism::refresh_state( out.K = -dR_dp; return true; } - const arma::mat HT = H.each_row() % T.t(); + arma::mat::fixed<6,6> HT, A; + fill_HT(H, T, HT); + fill_I_plus_gamma(HT, gamma, A); small_lu<6> lu_A; // A = I + gamma H T on the stack: solves instead of a LAPACK inverse - if (!lu_A.factor(arma::eye(6, 6) + gamma * HT)) { + if (!lu_A.factor(A)) { return false; } const arma::vec Tn = T % n; @@ -402,24 +434,31 @@ void PlasticityMechanism::compute_work( ) const { // Get increments double Dp = ivc_.at(p_index_).delta_scalar(); - arma::vec DEP = ivc_.at(EP_index_).delta_vec(); - if (Dp == 0.0 && DEP.is_zero()) { - Wm_r = Wm_ir = Wm_d = 0.0; // both dots below would be (signed) zeros - return; + const auto& EP_var = ivc_.at(EP_index_); + if (Dp == 0.0) { + // DEP.is_zero() on the differences, before building DEP (a NaN difference is not zero) + bool moved = false; + for (arma::uword i = 0; i < 6 && !moved; ++i) { + const double d = EP_var.raw_voigt()(i) - EP_var.raw_voigt_start()(i); + moved = (d != 0.0); + } + if (!moved) { + Wm_r = Wm_ir = Wm_d = 0.0; // both dots below would be (signed) zeros + return; + } } + arma::vec DEP = EP_var.delta_vec(); // Average stress during increment arma::vec sigma_avg = 0.5 * (sigma_start + sigma); - // Get backstress (if any) - const arma::vec X = kin_hard_->total_backstress(ivc_).to_arma_voigt(); - // Dissipated work: sigma : dEP Wm_d = arma::dot(sigma_avg, DEP); // For kinematic hardening, part of the work is stored // Stored work in backstress: X : dEP (approximately) if (kin_hard_->num_backstresses() > 0) { + const arma::vec X = total_backstress_voigt(); Wm_ir = arma::dot(X, DEP); Wm_d -= Wm_ir; } else { diff --git a/src/Continuum_mechanics/Umat/Modular/viscoelastic_mechanism.cpp b/src/Continuum_mechanics/Umat/Modular/viscoelastic_mechanism.cpp index 9f37b0f63..ee2536a28 100644 --- a/src/Continuum_mechanics/Umat/Modular/viscoelastic_mechanism.cpp +++ b/src/Continuum_mechanics/Umat/Modular/viscoelastic_mechanism.cpp @@ -42,6 +42,8 @@ ViscoelasticMechanism::ViscoelasticMechanism(int N_prony) , M0_L_i_(N_prony) , ev_key_(N_prony) , v_key_(N_prony) + , ev_index_(N_prony, 0) + , v_index_(N_prony, 0) , kappa_t_(N_prony, tensor2::zeros(Tensor2Type::stress)) , dEVtilde_dE_(arma::zeros(6, 6)) { @@ -67,13 +69,13 @@ void ViscoelasticMechanism::configure(const arma::vec& props, int& offset) { } void ViscoelasticMechanism::register_variables() { - // Resolve and cache the keys once — they're used every FB iteration in - // compute_constraints / inelastic_strain / update. for (int i = 0; i < N_prony_; ++i) { v_key_[i] = "v_" + std::to_string(i); ev_key_[i] = "EV_" + std::to_string(i); ivc_.add_scalar(v_key_[i], 0.0); ivc_.add_vec (ev_key_[i], arma::zeros(6), true); + v_index_[i] = ivc_.index_of(v_key_[i]); + ev_index_[i] = ivc_.index_of(ev_key_[i]); } } @@ -122,7 +124,7 @@ arma::vec ViscoelasticMechanism::inelastic_strain() const { // EV_tilde = sum_i (M_0 L_i) EV_i (Prony_Nfast form); M0_L_i_ precomputed in set_reference_stiffness arma::vec EV_tilde = arma::zeros(6); for (int i = 0; i < N_prony_; ++i) { - EV_tilde += M0_L_i_[i] * ivc_.get(ev_key_[i]).raw_voigt(); + EV_tilde += M0_L_i_[i] * ivc_.at(ev_index_[i]).raw_voigt(); } return EV_tilde; } @@ -138,15 +140,16 @@ void ViscoelasticMechanism::predict(const arma::vec& E_total_end, double DTime) // closed-form backward Euler from the rotated start, final before the other mechanisms; DTime = 0 -> inactive std::vector EV_start(N_prony_); for (int i = 0; i < N_prony_; ++i) { - EV_start[i] = ivc_.get(ev_key_[i]).raw_voigt_start(); + EV_start[i] = ivc_.at(ev_index_[i]).raw_voigt_start(); } - const LinearViscoStep st = maxwell_parallel_step(L_0_.is_empty() ? arma::mat(arma::zeros(6, 6)) : L_0_, - L_i_, H_i_, EV_start, E_total_end, arma::zeros(6), DTime); + static const arma::mat zero_L0(6, 6, arma::fill::zeros); + const arma::mat& L0 = L_0_.is_empty() ? zero_L0 : L_0_; // a reference: no 6x6 copy per call + const LinearViscoStep st = maxwell_parallel_step(L0, L_i_, H_i_, EV_start, E_total_end, arma::zeros(6), DTime); dEVtilde_dE_.zeros(); for (int i = 0; i < N_prony_; ++i) { - InternalVariable& ev = ivc_.get(ev_key_[i]); + InternalVariable& ev = ivc_.at(ev_index_[i]); ev.raw_voigt() = st.EV_i[i]; - InternalVariable& v = ivc_.get(v_key_[i]); + InternalVariable& v = ivc_.at(v_index_[i]); v.scalar() = v.scalar_start() + norm_strain(st.EV_i[i] - EV_start[i]); dEVtilde_dE_ += M0_L_i_[i] * st.dEVdE_i[i]; } @@ -183,7 +186,7 @@ void ViscoelasticMechanism::compute_work( Wm_d = 0.0; for (int i = 0; i < N_prony_; ++i) { - const InternalVariable& ev = ivc_.get(ev_key_[i]); + const InternalVariable& ev = ivc_.at(ev_index_[i]); const arma::vec A_start = L_i_[i] * (E_start - ev.raw_voigt_start()); const arma::vec A_end = L_i_[i] * (E_end - ev.raw_voigt()); Wm_d += 0.5 * arma::dot(A_start + A_end, ev.delta_vec()); diff --git a/src/Continuum_mechanics/Umat/Modular/yield_criterion.cpp b/src/Continuum_mechanics/Umat/Modular/yield_criterion.cpp index 9a1f21347..5a277bdb8 100644 --- a/src/Continuum_mechanics/Umat/Modular/yield_criterion.cpp +++ b/src/Continuum_mechanics/Umat/Modular/yield_criterion.cpp @@ -23,6 +23,8 @@ along with simcoon. If not, see . #include #include #include +#include +#include #include namespace simcoon { @@ -62,30 +64,35 @@ std::string YieldCriterion::type_string() const { void YieldCriterion::configure_von_mises() { type_ = YieldType::VON_MISES; params_ = arma::zeros(1); // No parameters needed + P_.reset(); configured_ = true; } void YieldCriterion::configure_tresca() { type_ = YieldType::TRESCA; params_ = arma::zeros(1); // No parameters needed + P_.reset(); configured_ = true; } void YieldCriterion::configure_drucker(double b, double n) { type_ = YieldType::DRUCKER; params_ = arma::vec({b, n}); + P_.reset(); configured_ = true; } void YieldCriterion::configure_hill(double F, double G, double H, double L, double M, double N) { type_ = YieldType::HILL; params_ = arma::vec({F, G, H, L, M, N}); + P_ = P_Hill(params_); configured_ = true; } void YieldCriterion::configure_dfa(double F, double G, double H, double L, double M, double N, double K) { type_ = YieldType::DFA; params_ = arma::vec({F, G, H, L, M, N, K}); + P_ = P_DFA(params_); configured_ = true; } @@ -94,6 +101,7 @@ void YieldCriterion::configure_anisotropic(double P11, double P22, double P33, double P44, double P55, double P66) { type_ = YieldType::ANISOTROPIC; params_ = arma::vec({P11, P22, P33, P12, P13, P23, P44, P55, P66}); + P_ = P_Ani(params_); configured_ = true; } @@ -158,18 +166,101 @@ void YieldCriterion::configure(YieldType type, const arma::vec& props, int& offs // ========== Yield Function Computations ========== +// the criteria.cpp kernels called by type, the quadratic ones on P_ built at configuration + double YieldCriterion::equivalent_stress(const arma::vec& sigma) const { if (!configured_) { throw std::invalid_argument("YieldCriterion: not configured"); } - return Eq_stress(sigma, type_string(), params_); + switch (type_) { + case YieldType::TRESCA: + return Tresca_stress(sigma); + case YieldType::DRUCKER: + return Drucker_stress(sigma, params_(0), params_(1)); + case YieldType::HILL: + case YieldType::DFA: + case YieldType::ANISOTROPIC: + return Eq_stress_P(sigma, P_); + default: // VON_MISES (type_string's fallback too) + return Mises_stress(sigma); + } } arma::vec YieldCriterion::flow_direction(const arma::vec& sigma) const { if (!configured_) { throw std::invalid_argument("YieldCriterion: not configured"); } - return dEq_stress(sigma, type_string(), params_); + switch (type_) { + case YieldType::TRESCA: + return dTresca_stress(sigma); + case YieldType::DRUCKER: + return dDrucker_stress(sigma, params_(0), params_(1)); + case YieldType::HILL: + case YieldType::DFA: + case YieldType::ANISOTROPIC: + return dEq_stress_P(sigma, P_); + default: // VON_MISES + return eta_stress(sigma); + } +} + +void YieldCriterion::equivalent_stress_and_flow(const arma::vec& sigma, double& sigma_eq, + arma::vec& flow) const { + switch (type_) { + case YieldType::HILL: + case YieldType::DFA: + case YieldType::ANISOTROPIC: { + if (!configured_) { + throw std::runtime_error("YieldCriterion: not configured"); + } + // Eq_stress_P and dEq_stress_P on one P sigma: the latter is P sigma over the former + if (arma::norm(sigma, 2) > simcoon::iota) { + const arma::vec P_sigma = P_ * sigma; + sigma_eq = std::pow(arma::sum(sigma % P_sigma), 0.5); + flow = P_sigma / sigma_eq; + } else { + sigma_eq = 0.; + flow = arma::zeros(6); + } + return; + } + default: + sigma_eq = equivalent_stress(sigma); + flow = flow_direction(sigma); + } +} + +void YieldCriterion::equivalent_stress_flow_hessian(const arma::vec& sigma, double& sigma_eq, + arma::vec& flow, arma::mat& hessian) const { + switch (type_) { + case YieldType::HILL: + case YieldType::DFA: + case YieldType::ANISOTROPIC: { + if (!configured_) { + throw std::runtime_error("YieldCriterion: not configured"); + } + // Eq_stress_P, dEq_stress_P and ddEq_stress_P(sigma, P_) (criteria.cpp) on one P sigma + if (arma::norm(sigma, 2) > simcoon::iota) { + const arma::vec P_sigma = P_ * sigma; + sigma_eq = std::pow(arma::sum(sigma % P_sigma), 0.5); + flow = P_sigma / sigma_eq; + if (sigma_eq < simcoon::iota) { + hessian = arma::zeros(6, 6); + } else { + hessian = (P_ - flow * flow.t()) / sigma_eq; // flow is ddEq_stress_P's Lambda + } + } else { + sigma_eq = 0.; + flow = arma::zeros(6); + hessian = arma::zeros(6, 6); + } + return; + } + default: + sigma_eq = equivalent_stress(sigma); + flow = flow_direction(sigma); + hessian = flow_hessian(sigma); + } } arma::vec YieldCriterion::plastic_flow(const arma::vec& sigma) const { @@ -212,11 +303,9 @@ arma::mat YieldCriterion::flow_hessian(const arma::vec& sigma) const { case YieldType::VON_MISES: return deta_stress(sigma); case YieldType::HILL: - return ddHill_stress(sigma, params_); case YieldType::DFA: - return ddDFA_stress(sigma, params_); case YieldType::ANISOTROPIC: - return ddAni_stress(sigma, params_); + return ddEq_stress_P(sigma, P_); // ddHill_stress / ddDFA_stress / ddAni_stress default: throw std::invalid_argument( "YieldCriterion: no analytic flow Hessian for " + type_string()); diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.cpp index 1777ab4d3..eef97e06f 100755 --- a/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.cpp @@ -59,9 +59,8 @@ void umat_plasticity_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, double //definition of the CTE tensor vec alpha = alpha_iso*Ith(); - //Elastic stiffness tensor and thermal tensor + //Elastic stiffness tensor mat L = L_iso(E, nu, "Enu"); - mat M = M_iso(E, nu, "Enu"); ///@brief Temperature initialization double T_init = statev(0); @@ -185,7 +184,7 @@ void umat_plasticity_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, double //compute Phi and the derivatives Phi(0) = Mises_stress(sigma) - Hp - sigmaY; - Lambdap = eta_stress(sigma); + Lambdap = dPhidsigma; // associated J2 flow kappa_j[0] = L*Lambdap; K(0,0) = dPhidp; diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_kin_iso.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_kin_iso.cpp index 4f2fc978b..c53184398 100755 --- a/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_kin_iso.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_kin_iso.cpp @@ -60,9 +60,8 @@ void umat_plasticity_kin_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, do //definition of the CTE tensor vec alpha = alpha_iso*Ith(); - //Elastic stiffness tensor and thermal tensor + //Elastic stiffness tensor mat L = L_iso(E, nu, "Enu"); - mat M = M_iso(E, nu, "Enu"); ///@brief Temperature initialization double T_init = statev(0); @@ -151,7 +150,7 @@ void umat_plasticity_kin_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, do //Compute the explicit flow direction vec Lambdap = eta_stress(sigma-X); - vec Lambdaa = eta_stress(sigma-X); + vec Lambdaa = Lambdap; std::vector kappa_j(1); kappa_j[0] = L*Lambdap; mat K = zeros(1,1); @@ -199,15 +198,16 @@ void umat_plasticity_kin_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, do p = s_j(0); Hp = hardening.R(p); dHpdp = hardening.dR_dp(p); + // associated J2 flow on sigma - X, shared by the criterion gradient and both flows dPhidsigma = eta_stress(sigma-X); dPhidp = -1.*dHpdp; - dPhida = -1.*kX*(eta_stress(sigma - X)%Ir05()); + dPhida = -1.*kX*(dPhidsigma%Ir05()); //compute Phi and the derivatives Phi(0) = Mises_stress(sigma-X) - Hp - sigmaY; - Lambdap = eta_stress(sigma-X); - Lambdaa = eta_stress(sigma-X); + Lambdap = dPhidsigma; + Lambdaa = dPhidsigma; kappa_j[0] = L*Lambdap; K(0,0) = dPhidp + sum(dPhida%Lambdaa); diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/SMA/unified_T.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/SMA/unified_T.cpp index 1119a5178..12474e17f 100755 --- a/src/Continuum_mechanics/Umat/Thermomechanical/SMA/unified_T.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/SMA/unified_T.cpp @@ -272,9 +272,9 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D vec lambdaTR = -1.*ETMean; - //Definition of the modified Y function - double YtF = Y0t + D*Hcur*Mises_stress(sigma); - double YtR = Y0t + D*sum(sigma%ETMean); //*D Changed by D. + //Modified Y functions, Lagrange multipliers and criteria: set by the transformation loop before any use + double YtF = 0.; + double YtR = 0.; double HfF = 0.; double HfR = 0.; @@ -297,9 +297,9 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D HfR = - a3; } - //Set the Lagrange multiplliers due to Physical limitations - double lambda0 = -1.*lagrange_pow_0(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); - double lambda1 = lagrange_pow_1(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); + //Lagrange multiplliers due to Physical limitations + double lambda0 = 0.; + double lambda1 = 0.; //Define the value of DM_sig vec DM_sig = (DM*sigma_start); @@ -313,14 +313,8 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D double A_xiR_start = -1.*rhoDs0*(T) + rhoDE0 - 0.5*sum(sigma_start%DM_sig) - sum(sigma_start%Dalpha)*(T-T_init) + HfR; //Transformation criteria - double PhihatF; - if (aniso_criteria) { - PhihatF = Hcur*Drucker_ani_stress(sigma, DFA_params, prager_b, prager_n); - } - else { - PhihatF = Hcur*Drucker_stress(sigma, prager_b, prager_n); - } - double PhihatR = sum(sigma%ETMean); + double PhihatF = 0.; + double PhihatR = 0.; //Variables required for the loop vec s_j = zeros(2); @@ -390,14 +384,22 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D L = inv_cubic(M_eff); DM_sig = DM*sigma; - Dalpha_T = Dalpha*(T+DT-T_init); + // equivalent stresses and gradients at the current stress, shared below (Dalpha_T is loop-invariant) + const double sig_eq = Mises_stress(sigma); + const vec eta_sig = eta_stress(sigma); + double Dr_sig; + vec dDr_sig; if (aniso_criteria) { - lambdaTF = Hcur * dDrucker_ani_stress(sigma, DFA_params, prager_b, prager_n); + Dr_sig = Drucker_ani_stress(sigma, DFA_params, prager_b, prager_n); + dDr_sig = dDrucker_ani_stress(sigma, DFA_params, prager_b, prager_n); } else { - lambdaTF = Hcur * dDrucker_stress(sigma, prager_b, prager_n); + Dr_sig = Drucker_stress(sigma, prager_b, prager_n); + dDr_sig = dDrucker_stress(sigma, prager_b, prager_n); } + + lambdaTF = Hcur * dDr_sig; lambdaTR = -1. * ETMean; kappa_j[0] = L*(lambdaTF + DM_sig + Dalpha_T); @@ -430,23 +432,18 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D HfR = - a3; // Find Hcur explicit - if (Mises_stress(sigma) > sigmacrit) - sigmastar = Mises_stress(sigma) - sigmacrit; + if (sig_eq > sigmacrit) + sigmastar = sig_eq - sigmacrit; else sigmastar = 0.; Hcur = Hmin + (Hmax - Hmin)*(1. - exp(-1.*k1*sigmastar)); //Forward transformation thermodynamic force - if (aniso_criteria) { - PhihatF = Hcur*Drucker_ani_stress(sigma, DFA_params, prager_b, prager_n); - } - else { - PhihatF = Hcur*Drucker_stress(sigma, prager_b, prager_n); - } + PhihatF = Hcur*Dr_sig; A_xiF = rhoDs0*(T + DT) - rhoDE0 + 0.5*sum(sigma%DM_sig) + sum(sigma%Dalpha)*(T + DT - T_init) - HfF; lambda1 = lagrange_pow_1(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); - const double DHM = D*Hcur*Mises_stress(sigma); + const double DHM = D*Hcur*sig_eq; YtF = Y0t + DHM; Phi(0) = PhihatF + A_xiF - lambda1 - YtF; @@ -485,27 +482,22 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D dHfR = 0.; } - dHcurdsigma = k1*(Hmax - Hmin)*exp(-1.*k1*sigmastar)*eta_stress(sigma); + dHcurdsigma = k1*(Hmax - Hmin)*exp(-1.*k1*sigmastar)*eta_sig; //Related to forward transformation - if (aniso_criteria) { - dPhihatFdsigma = dHcurdsigma * Drucker_ani_stress(sigma, DFA_params, prager_b, prager_n) + Hcur * dDrucker_ani_stress(sigma, DFA_params, prager_b, prager_n); - } - else { - dPhihatFdsigma = dHcurdsigma * Drucker_stress(sigma, prager_b, prager_n) + Hcur * dDrucker_stress(sigma, prager_b, prager_n); - } + dPhihatFdsigma = dHcurdsigma * Dr_sig + Hcur * dDr_sig; dPhihatFdxiF = 0.; dPhihatFdxiR = 0.; - dA_xiFdsigma = DM_sig + Dalpha*(T+DT-T_init); + dA_xiFdsigma = DM_sig + Dalpha_T; dA_xiFdxiF = -dHfF; dA_xiFdxiR = dHfF; dlambda1dsigma = zeros(6); dlambda1dxiF = dlagrange_pow_1(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); - dlambda1dxiR = -1.*dlagrange_pow_1(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); + dlambda1dxiR = -1.*dlambda1dxiF; - dYtFdsigma = D*(dHcurdsigma * Mises_stress(sigma) + Hcur * eta_stress(sigma)); + dYtFdsigma = D*(dHcurdsigma * sig_eq + Hcur * eta_sig); dYtFdxiF = 0.; dYtFdxiR = 0.; @@ -515,13 +507,13 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D //Relative to reverse transformation dPhihatRdsigma = ETMean; - dA_xiRdsigma = -1.*DM_sig -1.*Dalpha*(T+DT-T_init); + dA_xiRdsigma = -1.*DM_sig - Dalpha_T; dA_xiRdxiF = dHfR; dA_xiRdxiR = -dHfR; dlambda0dsigma = zeros(6); - dlambda0dxiF = -1.*dlagrange_pow_0(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); - dlambda0dxiR = dlagrange_pow_0(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); + dlambda0dxiR = dlagrange_pow_0(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); + dlambda0dxiF = -1.*dlambda0dxiR; dYtRdsigma = D*ETMean; dPhiRdsigma = -1.*dPhihatRdsigma + dA_xiRdsigma + dlambda0dsigma - dYtRdsigma; @@ -629,8 +621,9 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D // Delta s = invBhat * rhs, Bhat(l, j) = criterion l, mechanism j std::vector P_theta(2); - const double rhsF = dPhiFdtheta - sum(dPhiFdsigma%(L*alpha)); - const double rhsR = dPhiRdtheta - sum(dPhiRdsigma%(L*alpha)); + const vec L_alpha = L*alpha; + const double rhsF = dPhiFdtheta - sum(dPhiFdsigma%L_alpha); + const double rhsR = dPhiRdtheta - sum(dPhiRdsigma%L_alpha); P_theta[0] = invBhat(0, 0)*rhsF + invBhat(0, 1)*rhsR; P_theta[1] = invBhat(1, 0)*rhsF + invBhat(1, 1)*rhsR; diff --git a/src/Continuum_mechanics/Umat/fea_transfer.cpp b/src/Continuum_mechanics/Umat/fea_transfer.cpp index a9400da62..530a3c469 100644 --- a/src/Continuum_mechanics/Umat/fea_transfer.cpp +++ b/src/Continuum_mechanics/Umat/fea_transfer.cpp @@ -194,10 +194,19 @@ bool abaqus_nlgeom(const double *dfgrd1) { } mat abaqus_jacobian(const mat &Lt, const vec &sigma) { - // symmetric part of sigma (x) I: what Abaqus's default (symmetric) solver keeps - const vec I = {1., 1., 1., 0., 0., 0.}; - const mat sigma_I = sigma * I.t(); - return Lt + 0.5*(sigma_I + sigma_I.t()); + // symmetric part of sigma (x) I: what Abaqus's default (symmetric) solver keeps, built in + // place (the products and sums of Lt + 0.5*(sigma I^T + I sigma^T), without the temporaries) + static const double I[6] = {1., 1., 1., 0., 0., 0.}; + mat J(6, 6); + for (int j = 0; j < 6; j++) { + for (int i = 0; i < 6; i++) { + const double s1 = sigma(i)*I[j]; + const double s2 = sigma(j)*I[i]; + const double h = (s1 + s2)*0.5; + J(i, j) = Lt(i, j) + h; + } + } + return J; } int abaqus_kinematic_form(const char *cmname) { diff --git a/src/Continuum_mechanics/Umat/return_mapping.cpp b/src/Continuum_mechanics/Umat/return_mapping.cpp index 2119d599e..2986707a5 100644 --- a/src/Continuum_mechanics/Umat/return_mapping.cpp +++ b/src/Continuum_mechanics/Umat/return_mapping.cpp @@ -111,12 +111,12 @@ ReturnMappingResult closest_point_return_mapping( // Tangent ingredients at the converged iterate from the pieces the Newton just built // (kappa_eff = kappa + c: the multiplier-side state chain enters through the flux). - auto fill_tangent_pieces = [&](const mat &K, const std::vector &D, const std::vector &c) { + auto fill_tangent_pieces = [&](const mat &K, std::vector &&D, const std::vector &c) { r.Bhat_continuum.set_size(N, N); r.kappa_j.assign(kappa.begin(), kappa.end()); for (int j = 0; j < N; j++) r.kappa_j[j] += c[j]; r.dPhidsigma_l.assign(n_l.begin(), n_l.end()); - r.dLambda_dsigma_l = D; + r.dLambda_dsigma_l = std::move(D); for (int l = 0; l < N; l++) { for (int j = 0; j < N; j++) { r.Bhat_continuum(l, j) = sum(n_l[l] % r.kappa_j[j]) - K(l, j); @@ -133,6 +133,7 @@ ReturnMappingResult closest_point_return_mapping( } const int snap_niter = 20; // plain Newton gets this many iterations before the snap may act + r.error_history.reserve(8); // a converging solve takes a handful of iterations // buffers sized once; M factorized on the stack (small_lu) std::vector c(N, zeros(6)); @@ -183,7 +184,7 @@ ReturnMappingResult closest_point_return_mapping( r.error_history.push_back(err); r.error = err; if (err < precision) { - fill_tangent_pieces(K, D_j, c); + fill_tangent_pieces(K, std::move(D_j), c); r.converged = true; return r; } diff --git a/src/Continuum_mechanics/Umat/tangent_assembly.cpp b/src/Continuum_mechanics/Umat/tangent_assembly.cpp index b81c91099..c76a6acdd 100644 --- a/src/Continuum_mechanics/Umat/tangent_assembly.cpp +++ b/src/Continuum_mechanics/Umat/tangent_assembly.cpp @@ -65,6 +65,23 @@ ContinuumTangent assemble_tangent( } arma::mat invBhat = mask % invBbar; // zero out inactive rows/cols + // elastic increment: every product below sums signed zeros, so Lt = L bit for bit; skipped when no term can be inf/NaN + if (!any(op) && Lt_dPhi.is_finite()) { + double max_dPhi = 0.; + bool finite = true; + for (arma::uword m = 0; m < Nmech; ++m) { + finite = finite && dPhidsigma_l[m].is_finite() && kappa_j[m].is_finite(); + if (finite) max_dPhi = std::max(max_dPhi, arma::abs(dPhidsigma_l[m]).max()); + } + if (finite && arma::abs(Lt_dPhi).max()*max_dPhi < 1.E300) { + ContinuumTangent out; + out.Lt = L; + out.P_epsilon.assign(Nmech, arma::zeros(6)); + out.invBhat = std::move(invBhat); + return out; + } + } + // P = (L dPhi) invBhat^T: Bhat rows = criteria, columns = mechanisms arma::mat DP(6, Nmech); for (arma::uword m = 0; m < Nmech; ++m) DP.col(m) = dPhidsigma_l[m]; @@ -74,8 +91,9 @@ ContinuumTangent assemble_tangent( arma::mat K6(6, Nmech); for (arma::uword l = 0; l < Nmech; ++l) K6.col(l) = kappa_j[l]; + const arma::mat::fixed<6,6> KP = K6 * P.t(); ContinuumTangent out; - out.Lt = L - K6 * P.t(); + out.Lt = L - KP; out.P_epsilon.resize(Nmech); for (arma::uword l = 0; l < Nmech; ++l) out.P_epsilon[l] = P.col(l); out.invBhat = std::move(invBhat); @@ -135,8 +153,8 @@ ContinuumTangent assemble_algorithmic_tangent( assert(dLambda_dsigma_l.size() == Nmech); // M = I + L \cdot \sum_j (\Delta s^j \cdot \partial \Lambda^j/\partial \sigma ), summed over the active set only. - const arma::mat I6 = arma::eye(6, 6); - arma::mat sumDsLambdaSigma = arma::zeros(6, 6); + const arma::mat::fixed<6,6> I6 = arma::eye(6, 6); + arma::mat::fixed<6,6> sumDsLambdaSigma = arma::zeros(6, 6); for (arma::uword j = 0; j < Nmech; ++j) { if (Ds_j(j) > simcoon::iota) { sumDsLambdaSigma += Ds_j(j) * dLambda_dsigma_l[j]; @@ -149,28 +167,32 @@ ContinuumTangent assemble_algorithmic_tangent( } // singular M (FD dLambda/dsigma near activation, LAPACK-dependent): continuum fallback - arma::mat Minv; - if (!arma::inv(Minv, I6 + L * sumDsLambdaSigma)) { + arma::mat::fixed<6,6> M = L * sumDsLambdaSigma; + M += I6; + arma::mat::fixed<6,6> Minv; + if (!arma::inv(Minv, M)) { return assemble_continuum_tangent(Bhat_continuum, kappa_j, dPhidsigma_l, Ds_j, L); } // Algorithmic operators: \tilde{L} = M^{-1} L, \tilde{\kappa}^j = M^{-1} \kappa^j, // \tilde{B}^{lj} = Bhat_continuum^{lj} + \partial \Phi^l \cdot (M^{-1} - I) \cdot \kappa^j (Bhat_continuum = \partial \Phi \cdot \kappa - K). - const arma::mat L_tilde = Minv * L; + const arma::mat::fixed<6,6> L_tilde = Minv * L; std::vector kappa_tilde(Nmech); for (arma::uword j = 0; j < Nmech; ++j) { kappa_tilde[j] = Minv * kappa_j[j]; } arma::mat Bhat_tilde = Bhat_continuum; - const arma::mat Minv_minus_I = Minv - I6; - for (arma::uword l = 0; l < Nmech; ++l) { - for (arma::uword j = 0; j < Nmech; ++j) { - Bhat_tilde(l, j) += arma::dot(dPhidsigma_l[l], Minv_minus_I * kappa_j[j]); + const arma::mat::fixed<6,6> Minv_minus_I = Minv - I6; + for (arma::uword j = 0; j < Nmech; ++j) { + const arma::vec::fixed<6> dkappa_j = Minv_minus_I * kappa_j[j]; // shared by every row l + for (arma::uword l = 0; l < Nmech; ++l) { + Bhat_tilde(l, j) += arma::dot(dPhidsigma_l[l], dkappa_j); } } // L~ = M^-1 L is not symmetric: its transpose acts on dPhi - return assemble_tangent(Bhat_tilde, kappa_tilde, dPhidsigma_l, Ds_j, L_tilde, L_tilde.t()); + const arma::mat::fixed<6,6> L_tilde_t = L_tilde.t(); + return assemble_tangent(Bhat_tilde, kappa_tilde, dPhidsigma_l, Ds_j, L_tilde, L_tilde_t); } ContinuumTangent assemble_algorithmic_tangent( @@ -236,9 +258,12 @@ ContinuumTangent compute_tangent_operator( } if (tangent_mode == simcoon::tangent_algorithmic || tangent_mode == simcoon::tangent_closest_point) { // closest-point kernels use cpp_consistent_tangent() and never come here; for the others mode 3 = algorithmic - if (!dLambda_dsigma) { - // Flow independent of stress: the algorithmic operator IS the - // continuum one. + bool active = false; + for (arma::uword j = 0; j < Ds_j.n_elem; ++j) { + active = active || (Ds_j(j) > simcoon::iota); + } + if (!dLambda_dsigma || !active) { + // stress-independent flow or no active mechanism: the algorithmic operator IS the continuum one return assemble_continuum_tangent(Bhat, kappa_j, dPhidsigma_l, Ds_j, L); } return assemble_algorithmic_tangent(Bhat, kappa_j, dPhidsigma_l, Ds_j, L, diff --git a/src/Continuum_mechanics/Umat/umat_L_elastic.cpp b/src/Continuum_mechanics/Umat/umat_L_elastic.cpp index b5b7641c5..ef69cbc4a 100755 --- a/src/Continuum_mechanics/Umat/umat_L_elastic.cpp +++ b/src/Continuum_mechanics/Umat/umat_L_elastic.cpp @@ -131,7 +131,7 @@ void get_L_elastic(phase_characteristics &rve) } Lt_Self_Consistent(rve, n_matrix, false, self_consistent_start(rve)); umat_M->Lt = zeros(6,6); - for (auto r : rve.sub_phases) { + for (auto &r : rve.sub_phases) { umat_sub_phases_M = std::dynamic_pointer_cast(r.sptr_sv_global); umat_M->Lt += r.sptr_shape->concentration*(umat_sub_phases_M->Lt*r.sptr_multi->A); } @@ -190,7 +190,7 @@ void get_L_elastic(phase_characteristics &rve) // Compute the effective tangent modulus, and the effective stress umat_M->Lt = zeros(6,6); - for (auto r : rve.sub_phases) { + for (auto &r : rve.sub_phases) { umat_sub_phases_M = std::dynamic_pointer_cast(r.sptr_sv_global); umat_M->Lt += r.sptr_shape->concentration*(umat_sub_phases_M->Lt*r.sptr_multi->A); } diff --git a/src/Continuum_mechanics/Umat/umat_smart.cpp b/src/Continuum_mechanics/Umat/umat_smart.cpp index 942bbaece..4edc1a8cc 100644 --- a/src/Continuum_mechanics/Umat/umat_smart.cpp +++ b/src/Continuum_mechanics/Umat/umat_smart.cpp @@ -98,7 +98,7 @@ namespace simcoon{ void size_statev(phase_characteristics &rve, unsigned int &size) { - for (auto r:rve.sub_phases) { + for (auto &r:rve.sub_phases) { // by reference: a copy would duplicate the whole sub-tree size = size + r.sptr_sv_local->nstatev + 57; size_statev(r,size); } @@ -106,7 +106,7 @@ void size_statev(phase_characteristics &rve, unsigned int &size) { void statev_2_phases(phase_characteristics &rve, unsigned int &pos, const vec &statev) { - for(auto r : rve.sub_phases) { + for(auto &r : rve.sub_phases) { shared_ptr umat_phase_M = std::dynamic_pointer_cast(r.sptr_sv_global); unsigned int nstatev = umat_phase_M->statev.n_elem; @@ -130,7 +130,7 @@ void statev_2_phases(phase_characteristics &rve, unsigned int &pos, const vec &s void phases_2_statev(vec &statev, unsigned int &pos, const phase_characteristics &rve) { - for(auto r : rve.sub_phases) { + for(const auto &r : rve.sub_phases) { shared_ptr umat_phase_M = std::dynamic_pointer_cast(r.sptr_sv_global); unsigned int nstatev = umat_phase_M->statev.n_elem; @@ -229,7 +229,11 @@ const std::map &umat_conventions() } // namespace -umat_convention output_convention_of(const std::string &umat_name) +namespace { + +// The declaration itself, by reference into the table (no copy of its statev tensor list on the +// per-point finite dispatch) +const umat_convention &declared_convention(const std::string &umat_name) { const auto &conventions = umat_conventions(); auto it = conventions.find(umat_name); @@ -243,6 +247,13 @@ umat_convention output_convention_of(const std::string &umat_name) return it->second; } +} // namespace + +umat_convention output_convention_of(const std::string &umat_name) +{ + return declared_convention(umat_name); +} + bool stress_output_is_kirchhoff(const std::string &umat_name) { // shares the table instead of calling output_convention_of: no thrown exception per call diff --git a/src/Simulation/Maths/num_solve.cpp b/src/Simulation/Maths/num_solve.cpp index 134d6eac3..a8dcfa492 100755 --- a/src/Simulation/Maths/num_solve.cpp +++ b/src/Simulation/Maths/num_solve.cpp @@ -249,6 +249,40 @@ void Fischer_Burmeister_limits(const vec &Phi_p, const vec &Phi_l, const vec &Y_ } +namespace { + +// row i of the normalised FB system (Dp* = Dp |B_ii|): residual and Jacobian entry, bit for bit +inline double fb_m_value(const double &Phi, const double &Dpstar) +{ + if ((fabs(Phi) > 0.)&&(fabs(Dpstar) > 0.)) { + return sqrt(pow(Phi,2.) + pow(Dpstar,2.)) + Phi - Dpstar; + } + else if(fabs(Phi) > 0.) { + return sqrt(pow(Phi,2.)) + Phi; + } + else if(fabs(Dpstar) > 0.) { + return sqrt(pow(Dpstar,2.)) - Dpstar; + } + return 0.; +} + +inline double fb_m_jacobian(const double &Phi, const double &Dpstar, const double &dDpstar, const double &denom_ij, const bool &diag) +{ + const double delta = diag ? 1. : 0.; + if ((fabs(Phi) > 0.)&&(fabs(Dpstar) > 0.)) { + return (Phi/(sqrt(pow(Phi,2.) + pow(Dpstar,2.)))+1.)*denom_ij + delta*dDpstar*(Dpstar/(sqrt(pow(Phi,2.) + pow(Dpstar,2.))) - 1.); + } + else if(fabs(Phi) > 0.) { + return (Phi/(sqrt(pow(Phi,2.)))+1.)*denom_ij - delta*dDpstar; + } + else if(fabs(Dpstar) > 0.) { + return denom_ij + delta*dDpstar*(Dpstar/(sqrt(pow(Dpstar,2.))) - 1.); + } + return diag ? 1.E12 : 0.; +} + +} // namespace + double Fischer_Burmeister_residual(const vec &Phi, const vec &Y_crit, const mat &denom, const vec &Dp) { // Same branches and expressions as the FB vector of Fischer_Burmeister_m below. @@ -281,9 +315,44 @@ void Fischer_Burmeister_m(const vec &Phi, const vec &Y_crit, const mat &denom, v assert(fabs(Y_crit(i)) > 0); } + // one or two mechanisms: system on the stack, closed forms, no LAPACK call + if (n == 1 || n == 2) { + double FB[2], denomFB[2][2]; + for (int i=0; i simcoon::limit) { + dp(0) = -FB[0]/a; + } + } + else { + const double det_FB = denomFB[0][0]*denomFB[1][1] - denomFB[0][1]*denomFB[1][0]; + if (fabs(det_FB) > simcoon::limit) { + dp(0) = -(denomFB[1][1]*FB[0] - denomFB[0][1]*FB[1])/det_FB; // Cramer + dp(1) = -(denomFB[0][0]*FB[1] - denomFB[1][0]*FB[0])/det_FB; + } + } + //New update of the transformation/orientation multipliers + Dp += dp; + // the entering iterate's residual: Fischer_Burmeister_residual, kept in step + error = 0.; + for (int i=0; i 0.)&&(fabs(Dpstar(i)) > 0.)) { - FB(i) = sqrt(pow(Phi(i),2.) + pow(Dpstar(i),2.)) + Phi(i) - Dpstar(i); - } - else if(fabs(Phi(i)) > 0.) { - FB(i) = sqrt(pow(Phi(i),2.)) + Phi(i); - } - else if(fabs(Dpstar(i)) > 0.) { - FB(i) = sqrt(pow(Dpstar(i),2.)) - Dpstar(i); - } - else { - FB(i) = 0.; - } - + FB(i) = fb_m_value(Phi(i), Dpstar(i)); for (int j=0; j 0.)&&(fabs(Dpstar(i)) > 0.)) { - denomFB(i,j) = (Phi(i)/(sqrt(pow(Phi(i),2.) + pow(Dpstar(i),2.)))+1.)*denom(i,j) + delta(i,j)*dDpstar(i)*(Dpstar(i)/(sqrt(pow(Phi(i),2.) + pow(Dpstar(i),2.))) - 1.); - } - else if(fabs(Phi(i)) > 0.) { - denomFB(i,j) = (Phi(i)/(sqrt(pow(Phi(i),2.)))+1.)*denom(i,j) - delta(i,j)*dDpstar(i); - } - else if(fabs(Dpstar(i)) > 0.) { - denomFB(i,j) = denom(i,j) + delta(i,j)*dDpstar(i)*(Dpstar(i)/(sqrt(pow(Dpstar(i),2.))) - 1.); - } - else { - if (i==j) - denomFB(i,j) = 1.E12; - else - denomFB(i,j) = 0.; - } + denomFB(i,j) = fb_m_jacobian(Phi(i), Dpstar(i), dDpstar(i), denom(i,j), i == j); } - } - // One or two mechanisms (the common cases): closed forms, no LAPACK call. - if (n == 1) { - const double a = denomFB(0,0); - if (fabs(a) > simcoon::limit) { - dp.set_size(1); - dp(0) = -FB(0)/a; - } - else { - dp = zeros(n); - } - } - else if (n == 2) { - const double det_FB = denomFB(0,0)*denomFB(1,1) - denomFB(0,1)*denomFB(1,0); - if (fabs(det_FB) > simcoon::limit) { - dp.set_size(2); - dp(0) = -(denomFB(1,1)*FB(0) - denomFB(0,1)*FB(1))/det_FB; // Cramer - dp(1) = -(denomFB(0,0)*FB(1) - denomFB(1,0)*FB(0))/det_FB; + try { + if (fabs(det(denomFB)) > simcoon::limit) { + dp = -1.*solve(denomFB, FB); } else { dp = zeros(n); } + } catch (const std::runtime_error &e) { + cerr << "Error in det or inv: throw inv exception " << e.what() << endl; + throw simcoon::exception_det("Error in det function inside Fischer_Burmeister_m."); } - else { - try { - if (fabs(det(denomFB)) > simcoon::limit) { - dp = -1.*solve(denomFB, FB); - } - else { - dp = zeros(n); - } - } catch (const std::runtime_error &e) { - cerr << "Error in det or inv: throw inv exception " << e.what() << endl; - throw simcoon::exception_det("Error in det function inside Fischer_Burmeister_m."); - } - } - + //New update of the transformation/orientation multipliers Dp = Dp + dp; @@ -374,7 +391,7 @@ void Fischer_Burmeister_m(const vec &Phi, const vec &Y_crit, const mat &denom, v for (int i=0; i vs = fill_voigt_stress(as_matrix(), active); return vs * sigma; } @@ -747,8 +746,7 @@ vec Rotation::apply_strain(const vec& epsilon, bool active) const { if (epsilon.n_elem != 6) { throw invalid_argument("Strain vector must have 6 elements"); } - mat R = as_matrix(); - mat ve = fill_voigt_strain(R, active); + const mat::fixed<6,6> ve = fill_voigt_strain(as_matrix(), active); return ve * epsilon; } @@ -763,9 +761,10 @@ mat Rotation::apply_stiffness(const mat& L, bool active) const { if (L.n_rows != 6 || L.n_cols != 6) { throw invalid_argument("Stiffness matrix must be 6x6"); } - mat R = as_matrix(); - mat vs = fill_voigt_stress(R, active); - return vs * (L * trans(vs)); + // fixed-size operator and inner product (no heap temporaries), same association + const mat::fixed<6,6> vs = fill_voigt_stress(as_matrix(), active); + const mat::fixed<6,6> tmp = L * trans(vs); + return vs * tmp; } mat::fixed<6,6> Rotation::apply_compliance(const mat::fixed<6,6>& M, bool active) const { @@ -779,9 +778,9 @@ mat Rotation::apply_compliance(const mat& M, bool active) const { if (M.n_rows != 6 || M.n_cols != 6) { throw invalid_argument("Compliance matrix must be 6x6"); } - mat R = as_matrix(); - mat ve = fill_voigt_strain(R, active); - return ve * (M * trans(ve)); + const mat::fixed<6,6> ve = fill_voigt_strain(as_matrix(), active); + const mat::fixed<6,6> tmp = M * trans(ve); + return ve * tmp; } mat::fixed<6,6> Rotation::apply_strain_concentration(const mat::fixed<6,6>& A, bool active) const { @@ -796,10 +795,11 @@ mat Rotation::apply_strain_concentration(const mat& A, bool active) const { if (A.n_rows != 6 || A.n_cols != 6) { throw invalid_argument("Strain concentration tensor must be 6x6"); } - mat R = as_matrix(); - mat ve = fill_voigt_strain(R, active); - mat vs = fill_voigt_stress(R, active); - return ve * (A * trans(vs)); + const mat::fixed<3,3> R = as_matrix(); + const mat::fixed<6,6> ve = fill_voigt_strain(R, active); + const mat::fixed<6,6> vs = fill_voigt_stress(R, active); + const mat::fixed<6,6> tmp = A * trans(vs); + return ve * tmp; } mat::fixed<6,6> Rotation::apply_stress_concentration(const mat::fixed<6,6>& B, bool active) const { @@ -814,10 +814,11 @@ mat Rotation::apply_stress_concentration(const mat& B, bool active) const { if (B.n_rows != 6 || B.n_cols != 6) { throw invalid_argument("Stress concentration tensor must be 6x6"); } - mat R = as_matrix(); - mat vs = fill_voigt_stress(R, active); - mat ve = fill_voigt_strain(R, active); - return vs * (B * trans(ve)); + const mat::fixed<3,3> R = as_matrix(); + const mat::fixed<6,6> vs = fill_voigt_stress(R, active); + const mat::fixed<6,6> ve = fill_voigt_strain(R, active); + const mat::fixed<6,6> tmp = B * trans(ve); + return vs * tmp; } // ============================================================================= From 2b8e4ce6648e849a6450731c1b81865984ca685a Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Tue, 3 Nov 2026 23:08:27 +0100 Subject: [PATCH 02/12] Pair the rows of G in the Eshelby quadrature The accumulation pass of Gauss now holds the 36 sums of G as 18 pairs of rows in two-lane values (a GCC/Clang vector type, a plain pair elsewhere), so that they stay in registers and every operation serves two entries. Each lane goes through the scalar expressions, contracted as before, and the sum over the points of a ring stays sequential, so G is bitwise unchanged with Apple clang (conda-forge clang 22 contracts the former scalar loop differently: round-off, at most 2e-15 relative); the 50x50 integration drops from 36 to 25 us. The contraction of G in Eshelby and T_II runs on plain arrays instead of a bounds-checked Mat table. --- .../Homogenization/eshelby.cpp | 237 +++++++----------- 1 file changed, 97 insertions(+), 140 deletions(-) diff --git a/src/Continuum_mechanics/Homogenization/eshelby.cpp b/src/Continuum_mechanics/Homogenization/eshelby.cpp index 8572eac34..1d986c7f5 100755 --- a/src/Continuum_mechanics/Homogenization/eshelby.cpp +++ b/src/Continuum_mechanics/Homogenization/eshelby.cpp @@ -202,7 +202,23 @@ void calG(const double &pt, const double &a1, const double &a2, const double &a3 //Voigt index of the pair (i,j): the table that Eshelby and T_II hand to Gauss as Id static const int Voigt_index[3][3] = {{0,3,4},{3,1,5},{4,5,2}}; -// sum of calG in the per-entry operation order of the point loop (bitwise); sin/cos hoisted +// two rows of G per operation (vector type on GCC/Clang, else a pair); each lane runs the scalar expressions +#if defined(__GNUC__) || defined(__clang__) +typedef double lane2 __attribute__((vector_size(16))); +static inline lane2 lanes(const double &a, const double &b) { lane2 r = {a, b}; return r; } +static inline lane2 mul(const lane2 &a, const lane2 &b) { return a*b; } +static inline lane2 mul_add(const lane2 &a, const lane2 &b, const lane2 &c) { return a*b + c; } +#else +struct lane2 { + double v[2]; + double operator[](int i) const { return v[i]; } +}; +static inline lane2 lanes(const double &a, const double &b) { lane2 r = {{a, b}}; return r; } +static inline lane2 mul(const lane2 &a, const lane2 &b) { lane2 r = {{a.v[0]*b.v[0], a.v[1]*b.v[1]}}; return r; } +static inline lane2 mul_add(const lane2 &a, const lane2 &b, const lane2 &c) { lane2 r = {{a.v[0]*b.v[0] + c.v[0], a.v[1]*b.v[1] + c.v[1]}}; return r; } +#endif + +// sum of calG in the per-entry operation order of the point loop (bitwise); sin/cos hoisted, two rows of G at a time void Gauss(Mat &Id, const mat &Lt, mat &G, const double &a1, const double &a2, const double &a3, const vec &x, const vec &wx, const vec &y, const vec &wy, const int &mp, const int &np) { (void)Id; //always the Voigt table (Voigt_index) @@ -210,10 +226,13 @@ void Gauss(Mat &Id, const mat &Lt, mat &G, const double &a1, const double & for (int i=0; i<6; i++) for (int j=0; j<6; j++) L[i][j] = Lt(i,j); - double Gl[6][6]; - for (int i=0; i<6; i++) - for (int j=0; j<6; j++) - Gl[i][j] = G(i,j); + //rows ij of G in pairs, (00, 01), (02, 11), (12, 22); columns kl in the order 00, 01, 02, 11, 12, 22 + static const int row_ij[6] = {0, 3, 4, 1, 5, 2}; + static const int col_kl[6] = {0, 3, 4, 1, 5, 2}; + lane2 Gp[3][6]; + for (int q=0; q<3; q++) + for (int c=0; c<6; c++) + Gp[q][c] = lanes(G(row_ij[2*q],col_kl[c]), G(row_ij[2*q+1],col_kl[c])); std::vector cos_y(np), sin_y(np); for (int i=0; i &Id, const mat &Lt, mat &G, const double &a1, const double & const double inv_a1 = 1./a1; const double inv_a2 = 1./a2; const double inv_a3 = 1./a3; + const double *wy_ = wy.memptr(); //per point of the ring: X and the upper triangle of inv(K), K_ik = L_ijkl X_j X_l std::vector ring(9*np); double *X[3] = {&ring[0], &ring[np], &ring[2*np]}; double *R00 = &ring[3*np], *R01 = &ring[4*np], *R02 = &ring[5*np]; double *R11 = &ring[6*np], *R12 = &ring[7*np], *R22 = &ring[8*np]; - const double *R[3][3] = {{R00, R01, R02}, {nullptr, R11, R12}, {nullptr, nullptr, R22}}; + const double *R[6] = {R00, R01, R02, R11, R12, R22}; //rows in the order of row_ij for (int l=0; l &Id, const mat &Lt, mat &G, const double &a1, const double & R22[p] = rn22*inv_D; } - const double wx_l = wx(l); + const lane2 wx_l = lanes(wx(l), wx(l)); + const lane2 zero = lanes(0., 0.); + //a sequential sum per entry: an ordered vectorised reduction over p could split the + //contracted multiply-adds +#if defined(__clang__) + #pragma clang loop vectorize(disable) +#endif for (int p=0; p Id(6,6); - Id.zeros(); - - mat S = zeros(6,6); - int ij=0; - int mn=0; - int pq=0; - int ip=0; - int jq=0; - int jp=0; - int iq=0; - - Id(0,0) = 0; - Id(0,1) = 3; - Id(0,2) = 4; - Id(1,0) = 3; - Id(1,1) = 1; - Id(1,2) = 5; - Id(2,0) = 4; - Id(2,1) = 5; - Id(2,2) = 2; - - Gauss(Id, Lt, G, a1, a2, a3, x, wx, y, wy, mp, np); - - for (int i=0; i<3; i++) { - for (int j=i; j<3; j++) { - ij = Id(i,j); - for (int m=0; m<3; m++) { - for (int n=m; n<3; n++) { - mn = Id(m,n); - for (int p=0; p<3; p++) { - for (int q=0; q<3; q++) { - pq = Id(p,q); - ip = Id(i,p); - jq = Id(j,q); - jp = Id(j,p); - iq = Id(i,q); - S(ij,mn) = S(ij,mn)+Lt(pq,mn)*(G(ip,jq)+G(jp,iq)); - } - } - } - } - } - } - - for (int i=0; i<3; i++) { - for (int j=0; j<6; j++) { - S(i,j) = S(i,j)*(1./(8.*simcoon::pi)); - } - } - - for (int i=3; i<6; i++) { - for (int j=0; j<6; j++) { - S(i,j) = S(i,j)*(1./(4.*simcoon::pi)); - } - } - return S; + Mat Id; //unused by Gauss (always the Voigt table) + Gauss(Id, Lt, G, a1, a2, a3, x, wx, y, wy, mp, np); + return contract_G(Lt, G); } mat Eshelby(const mat &Lt, const double &a1, const double &a2, const double &a3, const int &mp, const int &np) { @@ -362,67 +375,11 @@ mat Eshelby(const mat &Lt, const double &a1, const double &a2, const double &a3, mat T_II(const mat &Lt, const double &a1, const double &a2, const double &a3, const vec &x, const vec &wx, const vec &y, const vec &wy, const int &mp, const int &np) -{ - mat G = zeros(6,6); - Mat Id(6,6); - Id.zeros(); - - mat T_II = zeros(6,6); - mat I = eye(6,6); - - int ij=0; - int mn=0; - int pq=0; - int ip=0; - int jq=0; - int jp=0; - int iq=0; - - Id(0,0) = 0; - Id(0,1) = 3; - Id(0,2) = 4; - Id(1,0) = 3; - Id(1,1) = 1; - Id(1,2) = 5; - Id(2,0) = 4; - Id(2,1) = 5; - Id(2,2) = 2; - - Gauss(Id, Lt, G, a1, a2, a3, x, wx, y, wy, mp, np); - - for (int i=0; i<3; i++) { - for (int j=i; j<3; j++) { - ij = Id(i,j); - for (int m=0; m<3; m++) { - for (int n=m; n<3; n++) { - mn = Id(m,n); - for (int p=0; p<3; p++) { - for (int q=0; q<3; q++) { - pq = Id(p,q); - ip = Id(i,p); - jq = Id(j,q); - jp = Id(j,p); - iq = Id(i,q); - T_II(ij,mn) = T_II(ij,mn)+I(pq,mn)*(G(ip,jq)+G(jp,iq)); - } - } - } - } - } - } - - for (int i=0; i<3; i++) { - for (int j=0; j<6; j++) { - T_II(i,j) = T_II(i,j)*(1./(8.*simcoon::pi)); - } - } - - for (int i=3; i<6; i++) { - for (int j=0; j<6; j++) { - T_II(i,j) = T_II(i,j)*(1./(4.*simcoon::pi)); - } - } - return T_II; +{ + mat G = zeros(6,6); + Mat Id; //unused by Gauss (always the Voigt table) + Gauss(Id, Lt, G, a1, a2, a3, x, wx, y, wy, mp, np); + return contract_G(eye(6,6), G); } mat T_II(const mat &Lt, const double &a1, const double &a2, const double &a3, const int &mp, const int &np) { From fc89e3fa6b3aad9534d561a02a7cc617b875cf30 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Tue, 3 Nov 2026 23:08:27 +0100 Subject: [PATCH 03/12] Trim per-pass overhead of the mean-field schemes Mori-Tanaka keeps the last inverse of its normalisation tensor, which an elastic medium hands back bitwise at every pass, and builds its sums in fixed-size matrices; the periodic-layer scheme builds the Voigt operators of each layer once per call instead of at each use (same operators, same products, no identity shortcut); the loops over the sub-phases take them by reference instead of copying them. Results are bitwise unchanged; an elastic Mori-Tanaka increment is 1.5 times faster, a laminate with a plastic layer 1.2 times. --- .../Micromechanics/multiphase.cpp | 10 +- .../Micromechanics/schemes.cpp | 124 +++++++++++------- 2 files changed, 79 insertions(+), 55 deletions(-) diff --git a/src/Continuum_mechanics/Micromechanics/multiphase.cpp b/src/Continuum_mechanics/Micromechanics/multiphase.cpp index 1792eeff5..e25affad7 100755 --- a/src/Continuum_mechanics/Micromechanics/multiphase.cpp +++ b/src/Continuum_mechanics/Micromechanics/multiphase.cpp @@ -164,8 +164,7 @@ void umat_multi(phase_characteristics &phase, const mat &DR, const double &Time, while ((error > simcoon::precision_micro)&&(nbiter <= simcoon::maxiter_micro)) { for(int i=0; i(phase.sub_phases[i].sptr_sv_global); - DE_N[i] = sv_r->DEtot; + DE_N[i] = phase.sub_phases[i].sptr_sv_global->DEtot; //a state_variables member: no cast } //Compute the strain concentration tensor for each phase: @@ -210,8 +209,7 @@ void umat_multi(phase_characteristics &phase, const mat &DR, const double &Time, error = 0.; for(int i=0; i(phase.sub_phases[i].sptr_sv_global); - error += norm(DE_N[i] - sv_r->DEtot,2); + error += norm(DE_N[i] - phase.sub_phases[i].sptr_sv_global->DEtot,2); } error*=(1./nphases); nbiter++; @@ -249,13 +247,13 @@ void umat_multi(phase_characteristics &phase, const mat &DR, const double &Time, // Homogenization //Compute the effective stress umat_phase_M->sigma = zeros(6); - for (auto r : phase.sub_phases) { + for (auto &r : phase.sub_phases) { umat_phase_M->sigma += r.sptr_shape->concentration*r.sptr_sv_global->sigma; } umat_phase_M->Lt = zeros(6,6); // Compute the effective tangent modulus, and the effective stress - for (auto r : phase.sub_phases) { + for (auto &r : phase.sub_phases) { umat_sub_phases_M = std::dynamic_pointer_cast(r.sptr_sv_global); umat_phase_M->Lt += r.sptr_shape->concentration*(umat_sub_phases_M->Lt*r.sptr_multi->A); } diff --git a/src/Continuum_mechanics/Micromechanics/schemes.cpp b/src/Continuum_mechanics/Micromechanics/schemes.cpp index d77b4466f..59707a5b3 100755 --- a/src/Continuum_mechanics/Micromechanics/schemes.cpp +++ b/src/Continuum_mechanics/Micromechanics/schemes.cpp @@ -26,6 +26,8 @@ #include #include #include +#include +#include #include #include #include @@ -53,11 +55,33 @@ static const eshelby_quadrature &scheme_quadrature(const phase_characteristics & return eshelby_quadrature_points(static_cast(phase.sptr_matprops->props(0)), static_cast(phase.sptr_matprops->props(1)), opt.rule, opt.closed_form == 1); } - + +//inv(sumT) of the Mori-Tanaka passes, the last one of this thread kept: an unchanged medium +//(elastic phases: the localization and the tangent passes of every call) hands bitwise the same +//sumT to each pass, whose inverse is then the stored one. inv throws on a singular sumT, and +//nothing is stored then. +static void inv_sumT_memoized(const mat &sumT, mat &inv_sumT) +{ + thread_local bool valid = false; + thread_local double key[36]; + thread_local double value[36]; + if (valid && sumT.n_elem == 36 && std::memcmp(key, sumT.memptr(), sizeof(key)) == 0) { + inv_sumT.set_size(6,6); + std::memcpy(inv_sumT.memptr(), value, sizeof(value)); + return; + } + inv_sumT = inv(sumT); + if (sumT.n_elem == 36) { + std::memcpy(key, sumT.memptr(), sizeof(key)); + std::memcpy(value, inv_sumT.memptr(), sizeof(value)); + valid = true; + } +} + void Lt_Homogeneous_E(phase_characteristics &phase) { //Compute the strain concentration tensor A - for(auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { r.sptr_multi->A = eye(6,6); } } @@ -68,7 +92,7 @@ void DE_Homogeneous_E(phase_characteristics &phase) { std::shared_ptr sv_eff = std::dynamic_pointer_cast(phase.sptr_sv_local); //Compute the strain concentration tensor A - for(auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); r.sptr_multi->A = eye(6,6); sv_r->DEtot = r.sptr_multi->A*sv_eff->DEtot; //Recall that the global coordinates of subphases is the local coordinates of the generic phase @@ -77,8 +101,8 @@ void DE_Homogeneous_E(phase_characteristics &phase) { void Lt_Mori_Tanaka(phase_characteristics &phase, const int &n_matrix) { - mat sumT = zeros(6,6); - mat inv_sumT = zeros(6,6); + mat::fixed<6,6> sumT(fill::zeros); //fixed size: no heap allocation per pass + mat::fixed<6,6> inv_sumT(fill::zeros); std::shared_ptr elli_multi; std::shared_ptr elli; @@ -91,7 +115,7 @@ void Lt_Mori_Tanaka(phase_characteristics &phase, const int &n_matrix) { //(one Eshelby integration per distinct inclusion geometry in this pass: eshelby_memo) eshelby_memo memo; const eshelby_quadrature &quad = scheme_quadrature(phase); - for(auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { elli_multi = std::dynamic_pointer_cast(r.sptr_multi); elli = std::dynamic_pointer_cast(r.sptr_shape); sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); @@ -108,14 +132,14 @@ void Lt_Mori_Tanaka(phase_characteristics &phase, const int &n_matrix) { } try { - inv_sumT = inv(sumT); + inv_sumT_memoized(sumT, inv_sumT); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside Lt_Mori_Tanaka."); } //Compute the strain concentration tensor A - for(auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { elli_multi = std::dynamic_pointer_cast(r.sptr_multi); elli_multi->A = elli_multi->T*inv_sumT; } @@ -123,8 +147,8 @@ void Lt_Mori_Tanaka(phase_characteristics &phase, const int &n_matrix) { void Lt_Mori_Tanaka_iso(phase_characteristics &phase, const int &n_matrix) { - mat sumT = zeros(6,6); - mat inv_sumT = zeros(6,6); + mat::fixed<6,6> sumT(fill::zeros); //fixed size: no heap allocation per pass + mat::fixed<6,6> inv_sumT(fill::zeros); std::shared_ptr elli_multi; std::shared_ptr elli; @@ -137,7 +161,7 @@ void Lt_Mori_Tanaka_iso(phase_characteristics &phase, const int &n_matrix) { //(one Eshelby integration per distinct inclusion geometry in this pass: eshelby_memo) eshelby_memo memo; const eshelby_quadrature &quad = scheme_quadrature(phase); - for(auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { elli_multi = std::dynamic_pointer_cast(r.sptr_multi); elli = std::dynamic_pointer_cast(r.sptr_shape); sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); @@ -154,14 +178,14 @@ void Lt_Mori_Tanaka_iso(phase_characteristics &phase, const int &n_matrix) { } try { - inv_sumT = inv(sumT); + inv_sumT_memoized(sumT, inv_sumT); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside Lt_Mori_Tanaka_iso."); } //Compute the strain concentration tensor A - for(auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { elli_multi = std::dynamic_pointer_cast(r.sptr_multi); elli_multi->A = elli_multi->T*inv_sumT; } @@ -169,8 +193,8 @@ void Lt_Mori_Tanaka_iso(phase_characteristics &phase, const int &n_matrix) { void DE_Mori_Tanaka(phase_characteristics &phase, const int &n_matrix) { - mat sumT = zeros(6,6); - mat inv_sumT = zeros(6,6); + mat::fixed<6,6> sumT(fill::zeros); //fixed size: no heap allocation per pass + mat::fixed<6,6> inv_sumT(fill::zeros); std::shared_ptr elli_multi; std::shared_ptr elli; @@ -183,7 +207,7 @@ void DE_Mori_Tanaka(phase_characteristics &phase, const int &n_matrix) { //(one Eshelby integration per distinct inclusion geometry in this pass: eshelby_memo) eshelby_memo memo; const eshelby_quadrature &quad = scheme_quadrature(phase); - for(auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { elli_multi = std::dynamic_pointer_cast(r.sptr_multi); elli = std::dynamic_pointer_cast(r.sptr_shape); sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); @@ -199,14 +223,14 @@ void DE_Mori_Tanaka(phase_characteristics &phase, const int &n_matrix) { } try { - inv_sumT = inv(sumT); + inv_sumT_memoized(sumT, inv_sumT); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside DE_Mori_Tanaka."); } //Compute the strain concentration tensor A - for(auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { elli_multi = std::dynamic_pointer_cast(r.sptr_multi); sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); @@ -217,8 +241,8 @@ void DE_Mori_Tanaka(phase_characteristics &phase, const int &n_matrix) { void DE_Mori_Tanaka_iso(phase_characteristics &phase, const int &n_matrix) { - mat sumT = zeros(6,6); - mat inv_sumT = zeros(6,6); + mat::fixed<6,6> sumT(fill::zeros); //fixed size: no heap allocation per pass + mat::fixed<6,6> inv_sumT(fill::zeros); std::shared_ptr elli_multi; std::shared_ptr elli; @@ -231,7 +255,7 @@ void DE_Mori_Tanaka_iso(phase_characteristics &phase, const int &n_matrix) { //(one Eshelby integration per distinct inclusion geometry in this pass: eshelby_memo) eshelby_memo memo; const eshelby_quadrature &quad = scheme_quadrature(phase); - for(auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { elli_multi = std::dynamic_pointer_cast(r.sptr_multi); elli = std::dynamic_pointer_cast(r.sptr_shape); sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); @@ -247,14 +271,14 @@ void DE_Mori_Tanaka_iso(phase_characteristics &phase, const int &n_matrix) { } try { - inv_sumT = inv(sumT); + inv_sumT_memoized(sumT, inv_sumT); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside DE_Mori_Tanaka_iso."); } //Compute the strain concentration tensor A - for(auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { elli_multi = std::dynamic_pointer_cast(r.sptr_multi); sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); @@ -293,7 +317,7 @@ void Lt_Self_Consistent(phase_characteristics &phase, const int &n_matrix, const sv_eff->Lt = Lt_eff; } - mat sumA = zeros(6,6); + mat::fixed<6,6> sumA(fill::zeros); //fixed size: no heap allocation per pass //Compute the Eshelby tensor and the interaction tensor for each phase //(one Eshelby integration per distinct inclusion geometry in this pass: eshelby_memo) eshelby_memo memo; @@ -347,14 +371,14 @@ void DE_Self_Consistent(phase_characteristics &phase, const int &n_matrix, const //Compute the effective tensor from the previous strain localization tensors mat Lt_eff = zeros(6,6); - for(auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); Lt_eff += r.sptr_shape->concentration*r.sptr_multi->A*sv_r->Lt; } sv_eff->Lt = Lt_eff; } - mat sumA = zeros(6,6); + mat::fixed<6,6> sumA(fill::zeros); //fixed size: no heap allocation per pass //Compute the Eshelby tensor and the interaction tensor for each phase //(one Eshelby integration per distinct inclusion geometry in this pass: eshelby_memo) eshelby_memo memo; @@ -400,7 +424,7 @@ void dE_Periodic_Layer(phase_characteristics &phase, const int &nbiter) { mat Lt_loc = zeros(6,6); if (nbiter == 0) { - for (auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { r.sptr_sv_global->DEtot = sv_eff->DEtot; } } @@ -408,13 +432,20 @@ void dE_Periodic_Layer(phase_characteristics &phase, const int &nbiter) { //Compute the increment of strain mat sumDnn = zeros(3,3); vec sumcDsig = zeros(3); - for(auto r : phase.sub_phases) { + //Voigt operators of each layer, built once per call: the matrices that + //Rotation::apply_stiffness, apply_stress and apply_strain build at each use, applied + //through the same products + std::vector strain_to_global; + strain_to_global.reserve(phase.sub_phases.size()); + for(auto &r : phase.sub_phases) { sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); lay_multi = std::dynamic_pointer_cast(r.sptr_multi); lay = std::dynamic_pointer_cast(r.sptr_shape); - Rotation rot_layer = Rotation::from_euler(lay->psi_geom, lay->theta_geom, lay->phi_geom, "zxz"); - Lt_loc = rot_layer.inv().apply_stiffness(sv_r->Lt); + const Rotation rot_layer = Rotation::from_euler(lay->psi_geom, lay->theta_geom, lay->phi_geom, "zxz"); + const mat vs_to_local = rot_layer.inv().as_voigt_stress_rotation(); + strain_to_global.push_back(rot_layer.as_voigt_strain_rotation()); + Lt_loc = vs_to_local * (sv_r->Lt * trans(vs_to_local)); lay_multi->Dnn(0,0) = Lt_loc(0,0); lay_multi->Dnn(0,1) = Lt_loc(0,3); @@ -426,14 +457,14 @@ void dE_Periodic_Layer(phase_characteristics &phase, const int &nbiter) { lay_multi->Dnn(2,1) = Lt_loc(4,3); lay_multi->Dnn(2,2) = Lt_loc(4,4); - vec sigma_local = rot_layer.inv().apply_stress(sv_r->sigma); + vec sigma_local = vs_to_local * sv_r->sigma; lay_multi->sigma_hat(0) = sigma_local(0); lay_multi->sigma_hat(1) = sigma_local(3); lay_multi->sigma_hat(2) = sigma_local(4); } try { - for(auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { lay_multi = std::dynamic_pointer_cast(r.sptr_multi); lay = std::dynamic_pointer_cast(r.sptr_shape); const mat inv_Dnn = inv(lay_multi->Dnn); @@ -453,7 +484,8 @@ void dE_Periodic_Layer(phase_characteristics &phase, const int &nbiter) { throw simcoon::exception_inv("Error in inv function inside dE_Periodic_Layer."); } - for(auto r : phase.sub_phases) { + for(unsigned int i=0; i(r.sptr_sv_global); lay_multi = std::dynamic_pointer_cast(r.sptr_multi); @@ -464,14 +496,12 @@ void dE_Periodic_Layer(phase_characteristics &phase, const int &nbiter) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside dE_Periodic_Layer."); } - lay = std::dynamic_pointer_cast(r.sptr_shape); vec dEtot_local = zeros(6); dEtot_local(0) = lay_multi->dzdx1(0); dEtot_local(3) = lay_multi->dzdx1(1); dEtot_local(4) = lay_multi->dzdx1(2); - Rotation rot_l2g = Rotation::from_euler(lay->psi_geom, lay->theta_geom, lay->phi_geom, "zxz"); - vec dEtot_global = rot_l2g.apply_strain(dEtot_local); + vec dEtot_global = strain_to_global[i] * dEtot_local; sv_r->DEtot += dEtot_global; } @@ -488,15 +518,15 @@ void Lt_Periodic_Layer(phase_characteristics &phase) { mat A_loc = zeros(6,6); //Compute the strain concentration tensor A - for(auto r : phase.sub_phases) { + std::vector rotations; //of each layer, for both of its uses + rotations.reserve(phase.sub_phases.size()); + for(auto &r : phase.sub_phases) { sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); lay_multi = std::dynamic_pointer_cast(r.sptr_multi); lay = std::dynamic_pointer_cast(r.sptr_shape); - { - Rotation rot = Rotation::from_euler(lay->psi_geom, lay->theta_geom, lay->phi_geom, "zxz"); - Lt_loc = rot.inv().apply_stiffness(sv_r->Lt); - } + rotations.push_back(Rotation::from_euler(lay->psi_geom, lay->theta_geom, lay->phi_geom, "zxz")); + Lt_loc = rotations.back().inv().apply_stiffness(sv_r->Lt); lay_multi->Dnn(0,0) = Lt_loc(0,0); lay_multi->Dnn(0,1) = Lt_loc(0,3); @@ -523,7 +553,7 @@ void Lt_Periodic_Layer(phase_characteristics &phase) { mat sumDnt = zeros(3,3); try { - for(auto r : phase.sub_phases) { + for(auto &r : phase.sub_phases) { lay_multi = std::dynamic_pointer_cast(r.sptr_multi); lay = std::dynamic_pointer_cast(r.sptr_shape); sumDnn += lay->concentration*inv(lay_multi->Dnn); @@ -544,9 +574,8 @@ void Lt_Periodic_Layer(phase_characteristics &phase) { mat m_t = m_n*sumDnt; - for(auto r : phase.sub_phases) { - lay_multi = std::dynamic_pointer_cast(r.sptr_multi); - lay = std::dynamic_pointer_cast(r.sptr_shape); + for(unsigned int i=0; i(phase.sub_phases[i].sptr_multi); try { lay_multi->dXn = inv(lay_multi->Dnn)*(m_n-lay_multi->Dnn); @@ -579,10 +608,7 @@ void Lt_Periodic_Layer(phase_characteristics &phase) { A_loc(4,4) += lay_multi->dXn(2,2); A_loc(4,5) += lay_multi->dXt(2,2); - { - Rotation rot = Rotation::from_euler(lay->psi_geom, lay->theta_geom, lay->phi_geom, "zxz"); - lay_multi->A = rot.apply_strain_concentration(A_loc); - } + lay_multi->A = rotations[i].apply_strain_concentration(A_loc); } } From 33cb3ff1ca2888b5d8b483e31c301749507c0fd9 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Tue, 3 Nov 2026 23:08:27 +0100 Subject: [PATCH 04/12] Trim the per-call plumbing of phase state variables to_start and set_start of a phase dispatch through the virtual methods instead of casting shared pointers, local2global and global2local cast references (no atomic reference counts on every constitutive call), and the state variables skip the rotation calls when the material frame is the identity. The sub-phase outputs iterate by reference. Results are bitwise unchanged. --- .../Phase/phase_characteristics.cpp | 54 ++++++------------- src/Simulation/Phase/state_variables.cpp | 4 ++ 2 files changed, 21 insertions(+), 37 deletions(-) diff --git a/src/Simulation/Phase/phase_characteristics.cpp b/src/Simulation/Phase/phase_characteristics.cpp index 3c27476d7..a385be4bb 100755 --- a/src/Simulation/Phase/phase_characteristics.cpp +++ b/src/Simulation/Phase/phase_characteristics.cpp @@ -192,18 +192,11 @@ void phase_characteristics::to_start() { switch (sv_type) { - case 1: { - auto sv_M_g = std::dynamic_pointer_cast(sptr_sv_global); - auto sv_M_l = std::dynamic_pointer_cast(sptr_sv_local); - sv_M_g->to_start(); - sv_M_l->to_start(); - break; - } + case 1: case 2: { - auto sv_T_g = std::dynamic_pointer_cast(sptr_sv_global); - auto sv_T_l = std::dynamic_pointer_cast(sptr_sv_local); - sv_T_g->to_start(); - sv_T_l->to_start(); + //virtual: the rollback of state_variables_M or state_variables_T + sptr_sv_global->to_start(); + sptr_sv_local->to_start(); break; } default: { @@ -223,18 +216,11 @@ void phase_characteristics::set_start(const int &corate_type) //---------------------------------------------------------------------- { switch (sv_type) { - case 1: { - auto sv_M_g = std::dynamic_pointer_cast(sptr_sv_global); - auto sv_M_l = std::dynamic_pointer_cast(sptr_sv_local); - sv_M_g->set_start(corate_type); - sv_M_l->set_start(corate_type); - break; - } + case 1: case 2: { - auto sv_T_g = std::dynamic_pointer_cast(sptr_sv_global); - auto sv_T_l = std::dynamic_pointer_cast(sptr_sv_local); - sv_T_g->set_start(corate_type); - sv_T_l->set_start(corate_type); + //virtual: the acceptance of state_variables_M or state_variables_T + sptr_sv_global->set_start(corate_type); + sptr_sv_local->set_start(corate_type); break; } default: { @@ -255,15 +241,12 @@ void phase_characteristics::local2global() //Switch case for the state_variables type of the phase switch (sv_type) { case 1: { - auto sv_M_g = std::dynamic_pointer_cast(sptr_sv_global); - auto sv_M_l = std::dynamic_pointer_cast(sptr_sv_local); - sv_M_g->rotate_l2g(*sv_M_l, sptr_matprops->frame()); + //casts of the references: no shared_ptr copies (atomic counts) on this per-call path + dynamic_cast(*sptr_sv_global).rotate_l2g(dynamic_cast(*sptr_sv_local), sptr_matprops->frame()); break; } case 2: { - auto sv_T_g = std::dynamic_pointer_cast(sptr_sv_global); - auto sv_T_l = std::dynamic_pointer_cast(sptr_sv_local); - sv_T_g->rotate_l2g(*sv_T_l, sptr_matprops->frame()); + dynamic_cast(*sptr_sv_global).rotate_l2g(dynamic_cast(*sptr_sv_local), sptr_matprops->frame()); break; } default: { @@ -281,15 +264,12 @@ void phase_characteristics::global2local() //Switch case for the state_variables type of the phase switch (sv_type) { case 1: { - auto sv_M_g = std::dynamic_pointer_cast(sptr_sv_global); - auto sv_M_l = std::dynamic_pointer_cast(sptr_sv_local); - sv_M_l->rotate_g2l(*sv_M_g, sptr_matprops->frame()); + //casts of the references: no shared_ptr copies (atomic counts) on this per-call path + dynamic_cast(*sptr_sv_local).rotate_g2l(dynamic_cast(*sptr_sv_global), sptr_matprops->frame()); break; } case 2: { - auto sv_T_g = std::dynamic_pointer_cast(sptr_sv_global); - auto sv_T_l = std::dynamic_pointer_cast(sptr_sv_local); - sv_T_l->rotate_g2l(*sv_T_g, sptr_matprops->frame()); + dynamic_cast(*sptr_sv_local).rotate_g2l(dynamic_cast(*sptr_sv_global), sptr_matprops->frame()); break; } default: { @@ -589,7 +569,7 @@ void phase_characteristics::output(const solver_output &so, const int &kblock, c } *sptr_out_global << endl; - for(auto r : sub_phases) { + for(auto &r : sub_phases) { r.output(so, kblock, kcycle, kstep, kinc, Time, "global"); } } @@ -796,7 +776,7 @@ void phase_characteristics::output(const solver_output &so, const int &kblock, c } *sptr_out_local << endl; - for(auto r : sub_phases) { + for(auto &r : sub_phases) { r.output(so, kblock, kcycle, kstep, kinc, Time, "local"); } @@ -821,7 +801,7 @@ ostream& operator << (ostream& s, const phase_characteristics& pc) s << "Display local state variables:\n"; s << *pc.sptr_sv_local; - for(auto r : pc.sub_phases) { + for(const auto &r : pc.sub_phases) { s << r; } diff --git a/src/Simulation/Phase/state_variables.cpp b/src/Simulation/Phase/state_variables.cpp index 1ea756222..20508ffab 100755 --- a/src/Simulation/Phase/state_variables.cpp +++ b/src/Simulation/Phase/state_variables.cpp @@ -440,6 +440,8 @@ state_variables& state_variables::rotate_l2g(const state_variables& sv, const fr statev = sv.statev; statev_start = sv.statev_start; + if (frame.is_identity()) //every rotation below is then a no-op + return *this; frame.rotate_strain(Etot); frame.rotate_strain(DEtot); frame.rotate_strain(etot); @@ -485,6 +487,8 @@ state_variables& state_variables::rotate_g2l(const state_variables& sv, const fr statev_start = sv.statev_start; // inverse operators: global -> local + if (frame.is_identity()) //every rotation below is then a no-op + return *this; frame.rotate_strain(Etot, frame_rotation::inverse); frame.rotate_strain(DEtot, frame_rotation::inverse); frame.rotate_strain(etot, frame_rotation::inverse); From f5a4d67289fce1835f3a31fd7020aaf381aefa7c Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Tue, 3 Nov 2026 23:08:27 +0100 Subject: [PATCH 05/12] Skip the redundant rollback of the first Newton trial Every attempt of an increment starts from the start state: set_start has just accepted the previous increment, or assess_inc has rolled back a cut one. The to_start that preceded each constitutive call of the Newton loops therefore copied identical values on the first trial; it now runs from the second trial on, and the single call of the fully strain-driven mechanical path no longer rolls back (the thermomechanical one keeps its to_start). The invariant is documented where it is relied on and asserted in debug builds. Lt_2_K copies the stress-controlled rows element by element instead of through row subviews. Results are bitwise unchanged. An elastic increment is about 7 percent faster, a logarithmic-control one about 2.5 percent. --- src/Simulation/Solver/solver.cpp | 41 +++++++++++++++++++++-- src/Simulation/Solver/solver_assembly.cpp | 6 ++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/Simulation/Solver/solver.cpp b/src/Simulation/Solver/solver.cpp index b410dc128..7fd1cda88 100755 --- a/src/Simulation/Solver/solver.cpp +++ b/src/Simulation/Solver/solver.cpp @@ -21,6 +21,7 @@ ///@version 1.9 #include +#include #include #include #include @@ -174,6 +175,23 @@ void restore_box_tangent(material_tangent_state &mt, simcoon::state_variables_M mt.held = false; } +// The rollback the Newton loops skip on their first trial (see the to_start calls below) relies +// on this invariant: every attempt of an increment begins at the start state, the current fields +// that to_start restores (PKII, tau, sigma, statev, and per type sigma_in, Wm, Wt; recursively in +// the sub-phases) being bit for bit their *_start copies. set_start (the previous increment +// accepted, or the priming call of a block) and assess_inc (set_start, or to_start on a cut, also +// after a caught exception) both end there, and nothing in between writes those fields: the solver +// only sets DEtot, Detot, DT, DR, F0, F1, U1 and Lt, and the sinks read. Debug check on the +// global state variables. +[[maybe_unused]] bool at_start_state(const simcoon::state_variables &sv) { + auto same = [](const arma::mat &a, const arma::mat &b) { + return a.n_elem == b.n_elem + && (a.n_elem == 0 || std::memcmp(a.memptr(), b.memptr(), a.n_elem*sizeof(double)) == 0); + }; + return same(sv.PKII, sv.PKII_start) && same(sv.tau, sv.tau_start) + && same(sv.sigma, sv.sigma_start) && same(sv.statev, sv.statev_start); +} + } // namespace namespace simcoon{ @@ -591,7 +609,9 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou DTime = Dtinc*sptr_meca->times(inc); finite_kinematics(*sv_M, blocks[i].control_type, sptr_meca->BC_w, sptr_meca->BC_R, DTime, corate_type, DR); } - rve.to_start(); + // no rve.to_start(): the single kernel call of this path is the first trial of the + // attempt, which begins at the start state (invariant: see at_start_state) + assert(at_start_state(*rve.sptr_sv_global)); run_umat_M(rve, sv_M->DR, Time, DTime, ndi, nshr, start, solver_type, blocks[i].control_type, corate_type, tnew_dt); if (!sv_M->tau.is_finite()) { // non-finite answer: cut, or give up at Dn_mini if (Dtinc_cur == sptr_meca->Dn_mini) { @@ -655,7 +675,15 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou else { finite_kinematics(*sv_M, blocks[i].control_type, sptr_meca->BC_w, sptr_meca->BC_R, DTime, corate_type, DR); } - rve.to_start(); + // compteur counts the trials of this attempt (0 on entry, see the end of the + // attempt and step_cut_or_rethrow): the first one is already at the start + // state (invariant: see at_start_state), the next ones roll back + if (compteur > 0) { + rve.to_start(); + } + else { + assert(at_start_state(*rve.sptr_sv_global)); + } run_umat_M(rve, sv_M->DR, Time, DTime, ndi, nshr, start, solver_type, blocks[i].control_type, corate_type, tnew_dt, ask_material_tangent); if (ask_material_tangent) { native_Lt.held = true; @@ -1077,7 +1105,14 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou if (finite_ct) finite_kinematics(*sv_T, blocks[i].control_type, sptr_thermomeca->BC_w, sptr_thermomeca->BC_R, DTime, corate_type, DR); - rve.to_start(); + // as the mechanical loop: the first trial of the attempt is at the start state + // (invariant: see at_start_state), the next ones roll back + if (compteur > 0) { + rve.to_start(); + } + else { + assert(at_start_state(*rve.sptr_sv_global)); + } run_umat_T(rve, (blocks[i].control_type == 1) ? DR : sv_T->DR, Time, DTime, ndi, nshr, start, solver_type, blocks[i].control_type, corate_type, tnew_dt); // -r - DTime Q_imposed = 0, r the heat of the increment diff --git a/src/Simulation/Solver/solver_assembly.cpp b/src/Simulation/Solver/solver_assembly.cpp index 4c89e2025..52444b279 100644 --- a/src/Simulation/Solver/solver_assembly.cpp +++ b/src/Simulation/Solver/solver_assembly.cpp @@ -37,11 +37,13 @@ namespace simcoon{ void Lt_2_K(const mat &Lt, mat &K, const Col &cBC_meca, const double &lambda) { - K = zeros(6,6); + K.zeros(6,6); + // element by element: a subview row copy per stress-controlled row cost more than the copy for (int i=0; i<6; i++) { if (cBC_meca(i)) { - K.row(i) = Lt.row(i); + for (int j=0; j<6; j++) + K(i,j) = Lt(i,j); } else K(i,i) = lambda; From ee952ea944174d9986539336ba0c09b0e106e542 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Tue, 3 Nov 2026 23:08:27 +0100 Subject: [PATCH 06/12] Record the solver history straight into its arrays solver_run recorded through solver_memory_sink, which builds one arma object per field and per record (a heap block for every 6x6 tangent), then copied them row by row into the arrays returned to Python. The binding now records into its own sink: one column-major (N, n) buffer per field, sized for the increments of a complete run and filled in place, so that the buffer itself becomes the NumPy array; it grows if need be and is cut to the records of an aborted run. Keys, shapes, memory order and values of the result are unchanged; solver_memory_sink stays as it was for the C++ API. A new test pins the recorded history to the contract of solver_memory_sink (one record per increment, sigma = tau / det(F1), fields per state type, layout). An elastic increment is about 30 percent faster, a plastic EPICP or a logarithmic-control one about 10 percent, and the peak memory of a 1e6-increment run drops from 3.2 to 1.0 GiB. --- .../Libraries/Solver/solver_run.cpp | 215 +++++++++++++++--- .../test/test_core/test_solver_history.py | 111 +++++++++ 2 files changed, 297 insertions(+), 29 deletions(-) create mode 100644 simcoon-python-builder/test/test_core/test_solver_history.py diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Solver/solver_run.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Solver/solver_run.cpp index 42f2723f7..c541f06e6 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Solver/solver_run.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Solver/solver_run.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -22,6 +24,8 @@ #include #include #include +#include +#include #include #include #include @@ -124,17 +128,6 @@ void fill_step_common(StepPtr &sptr, const py::dict &sd, const unsigned int &con } } -//one row per record, vecs/mats flattened row-major (matrices reshape to -//(N, n_rows, n_cols) C-order on the Python side). -template -py::array_t rows_to_arr(const std::vector &v) { - arma::mat M(v.size(), v.empty() ? 0 : v[0].n_elem); - for (size_t i = 0; i < v.size(); i++) { - M.row(i) = arma::vectorise(v[i].t()).t(); - } - return simpy::arma_to_numpy::mat_to_arr(std::move(M)); // the heap matrix is handed over, not copied -} - template py::array_t scalars_to_arr(const std::vector &v) { py::array_t a(v.size()); @@ -142,6 +135,169 @@ py::array_t scalars_to_arr(const std::vector &v) { return a; } +//History of one recorded vector or matrix, as the (N, n_comp) Fortran-ordered array it is +//returned as: one column per component (a matrix flattened row by row), one row per record, +//filled in place. The buffer is sized at once for the expected count of records, which every +//complete run reaches exactly, so that it becomes the array itself: no copy, no growth peak +//(a doubling buffer cost 60 % more peak memory and 25 % more time on a 1e6-increment run). Its +//untouched pages cost address space only, so a run planned long that stops early stays small; +//beyond 1 GiB per field the buffer starts smaller and doubles instead. It grows on if need be +//and is cut to the records of an aborted run. A std::vector of arma objects constructed (and later destroyed) +//one object per field and record, with a heap block for each 6x6 tangent, and its +//conversion into arrays, were a quarter of an elastic increment. +class flat_history { +public: + void set_capacity(const std::size_t &n_records) { capacity_ = n_records; } + + void push(const arma::mat &m) { + if (n_records_ == 0) { + n_comp_ = m.n_elem; + const std::size_t bounded = std::max(first_rows, max_doubles/std::max(n_comp_, 1)); + buf_.set_size(std::max(std::min(capacity_, bounded), 1), n_comp_); + } + else if (m.n_elem != n_comp_) { + throw std::length_error("solver_run: a recorded quantity changed size during the run"); + } + if (n_records_ == buf_.n_rows) { + grow(); + } + const arma::uword ld = buf_.n_rows; + double *out = buf_.memptr() + n_records_; + if (m.n_rows == 1 || m.n_cols == 1) { + const double *in = m.memptr(); + for (arma::uword k = 0; k < n_comp_; k++) + out[k*ld] = in[k]; + } + else { + for (arma::uword a = 0; a < m.n_rows; a++) + for (arma::uword b = 0; b < m.n_cols; b++, out += ld) + *out = m(a, b); + } + n_records_++; + } + + //The (N, n_comp) array (Fortran order): the Python side takes .T of a vector history and + //reshapes a matrix one to (N, n_rows, n_cols), then (n_rows, n_cols, N), both views + py::array_t columns() { + if (n_records_ == 0 || n_comp_ == 0) { + return simpy::arma_to_numpy::mat_to_arr(arma::mat(n_records_, n_comp_)); + } + if (n_records_ < buf_.n_rows) { + buf_.shed_rows(n_records_, buf_.n_rows - 1); + } + return simpy::arma_to_numpy::mat_to_arr(std::move(buf_)); // handed over, not copied + } + +private: + //doubles, but stops at the expected count when it lies in between + void grow() { + std::size_t rows = 2*buf_.n_rows; + if (capacity_ > buf_.n_rows && capacity_ < rows) rows = capacity_; + arma::mat bigger(rows, n_comp_, arma::fill::none); + for (arma::uword k = 0; k < n_comp_; k++) + std::memcpy(bigger.colptr(k), buf_.colptr(k), n_records_*sizeof(double)); + buf_ = std::move(bigger); + } + + static constexpr std::size_t first_rows = 1024; ///< smallest first allocation (records) + static constexpr std::size_t max_doubles = std::size_t(1) << 27; ///< 1 GiB: larger expected histories double from here + arma::mat buf_; + std::size_t capacity_ = 0; + std::size_t n_comp_ = 0; + std::size_t n_records_ = 0; +}; + +//The memory sink of the bindings: the converged state of every output point, as +//simcoon::solver_memory_sink captures it, in flat histories that become the arrays of the +//result dict. +class history_sink : public simcoon::solver_results_sink { +public: + bool record_tangent = true; + int sv_type = 0; ///< 1: mechanical, 2: thermomechanical (set by init()) + + explicit history_sink(const std::size_t &n_hint) : n_hint_(n_hint) {} + + void init(simcoon::phase_characteristics &rve) override { + auto *sv = rve.sptr_sv_global.get(); + if (dynamic_cast(sv) != nullptr) + sv_type = 2; + else if (dynamic_cast(sv) != nullptr) + sv_type = 1; + for (auto *v : {&blocks_i, &cycles_i, &steps_i, &incs_i}) v->reserve(n_hint_); + for (auto *v : {&time, &T, &Q, &r}) v->reserve(n_hint_); + for (auto *h : {&Etot, &etot, &PKII, &tau, &sigma, &F1, &R, &DR, &statev, &Wm, &Wt, + &Lt, &dSdE, &dSdT, &drdE, &drdT}) h->set_capacity(n_hint_); + } + + void record(simcoon::phase_characteristics &rve, const simcoon::solver_output &, const int &kblock, + const int &kcycle, const int &kstep, const int &kinc, const double &Time) override { + simcoon::state_variables *sv = rve.sptr_sv_global.get(); + // defensive re-detection of the state-variable type, as solver_memory_sink + auto *sv_T = dynamic_cast(sv); + auto *sv_M = (sv_T == nullptr) ? dynamic_cast(sv) : nullptr; + sv_type = (sv_T != nullptr) ? 2 : ((sv_M != nullptr) ? 1 : 0); + + blocks_i.push_back(kblock); + cycles_i.push_back(kcycle); + steps_i.push_back(kstep); + incs_i.push_back(kinc); + time.push_back(Time); + + Etot.push(sv->Etot); + etot.push(sv->etot); + PKII.push(sv->PKII); + tau.push(sv->tau); + // Cauchy stress, as solver_memory_sink (o_stress_type 4 of phase_characteristics::output) + sigma.push(sv->tau / det(sv->F1)); + F1.push(sv->F1); + R.push(sv->R); + DR.push(sv->DR); + T.push_back(sv->T); + statev.push(sv->statev); + + if (sv_T != nullptr) { + Wm.push(sv_T->Wm); + Wt.push(sv_T->Wt); + Q.push_back(sv_T->Q); + r.push_back(sv_T->r); + if (record_tangent) { + dSdE.push(sv_T->dSdE); + dSdT.push(sv_T->dSdT); + drdE.push(sv_T->drdE); + drdT.push(sv_T->drdT); + } + } + else if (sv_M != nullptr) { + Wm.push(sv_M->Wm); + if (record_tangent) { + Lt.push(sv_M->Lt); + } + } + } + + std::vector blocks_i, cycles_i, steps_i, incs_i; + std::vector time, T, Q, r; + flat_history Etot, etot, PKII, tau, sigma, F1, R, DR, statev, Wm, Wt, Lt, dSdE, dSdT, drdE, drdT; + +private: + std::size_t n_hint_; +}; + +//Records of a complete run, every increment being an output point here (as step::generate() +//counts the increments): the capacity of the histories +std::size_t expected_records(const std::vector &blocks) { + std::size_t n = 0; + for (const auto &bl : blocks) { + std::size_t n_inc = 0; + for (const auto &sptr : bl.steps) { + if (sptr->mode == 3) n_inc += sptr->tab_data.n_rows; + else if (sptr->Dn_inc > 0.) n_inc += static_cast(std::round(1./sptr->Dn_inc)); + } + n += n_inc*bl.ncycle; + } + return n; +} + } //anonymous namespace py::dict solver_run(const py::list &blocks_py, const double &T_init, @@ -235,7 +391,7 @@ py::dict solver_run(const py::list &blocks_py, const double &T_init, } simcoon::check_path_output(blocks, so); - simcoon::solver_memory_sink sink; + history_sink sink(expected_records(blocks)); sink.record_tangent = record_tangent; //Sub-phases of a mean-field model, built HERE, while the GIL is still held: they are read @@ -251,7 +407,8 @@ py::dict solver_run(const py::list &blocks_py, const double &T_init, sub_phases); } - //Assemble the results (canonical state; measure transforms happen in Python) + //Assemble the results (canonical state; measure transforms happen in Python): one row per + //record, (N, n) in Fortran order, a matrix flattened row by row py::dict res; res["status"] = status; res["sv_type"] = sink.sv_type; @@ -260,30 +417,30 @@ py::dict solver_run(const py::list &blocks_py, const double &T_init, res["step"] = scalars_to_arr(sink.steps_i); res["inc"] = scalars_to_arr(sink.incs_i); res["time"] = scalars_to_arr(sink.time); - res["Etot"] = rows_to_arr(sink.Etot); - res["etot"] = rows_to_arr(sink.etot); - res["PKII"] = rows_to_arr(sink.PKII); - res["tau"] = rows_to_arr(sink.tau); - res["sigma"] = rows_to_arr(sink.sigma); - res["F1"] = rows_to_arr(sink.F1); - res["R"] = rows_to_arr(sink.R); - res["DR"] = rows_to_arr(sink.DR); + res["Etot"] = sink.Etot.columns(); + res["etot"] = sink.etot.columns(); + res["PKII"] = sink.PKII.columns(); + res["tau"] = sink.tau.columns(); + res["sigma"] = sink.sigma.columns(); + res["F1"] = sink.F1.columns(); + res["R"] = sink.R.columns(); + res["DR"] = sink.DR.columns(); res["T"] = scalars_to_arr(sink.T); - res["Wm"] = rows_to_arr(sink.Wm); - res["statev"] = rows_to_arr(sink.statev); + res["Wm"] = sink.Wm.columns(); + res["statev"] = sink.statev.columns(); if (sink.sv_type == 2) { res["Q"] = scalars_to_arr(sink.Q); res["r"] = scalars_to_arr(sink.r); - res["Wt"] = rows_to_arr(sink.Wt); + res["Wt"] = sink.Wt.columns(); if (record_tangent) { - res["dSdE"] = rows_to_arr(sink.dSdE); - res["dSdT"] = rows_to_arr(sink.dSdT); - res["drdE"] = rows_to_arr(sink.drdE); - res["drdT"] = rows_to_arr(sink.drdT); + res["dSdE"] = sink.dSdE.columns(); + res["dSdT"] = sink.dSdT.columns(); + res["drdE"] = sink.drdE.columns(); + res["drdT"] = sink.drdT.columns(); } } else if (record_tangent) { - res["Lt"] = rows_to_arr(sink.Lt); + res["Lt"] = sink.Lt.columns(); } return res; } diff --git a/simcoon-python-builder/test/test_core/test_solver_history.py b/simcoon-python-builder/test/test_core/test_solver_history.py new file mode 100644 index 000000000..1bf936ede --- /dev/null +++ b/simcoon-python-builder/test/test_core/test_solver_history.py @@ -0,0 +1,111 @@ +"""The recorded history of _core.solver_run, field by field. + +The binding records the solver state into its own sink (solver_run.cpp, history_sink) rather +than through simcoon::solver_memory_sink, the sink of the C++ API. Both capture the same +converged state at every output point; these tests pin that contract on the arrays the binding +returns: one record per increment, the Cauchy stress as tau / det(F1), matrices flattened row +by row, the fields of each state-variable type, and the (N, n) Fortran layout SolverResults +takes views of. +""" + +import numpy as np +import pytest + +import simcoon as sim +from simcoon import _core +from simcoon.solver import Block, StepMeca, StepThermomeca, SolverResults + +MIXED = ["strain"] + ["stress"] * 5 +EPICP_PROPS = [70000.0, 0.3, 1.0e-5, 300.0, 1000.0, 0.3] +EPICP_T_PROPS = [7800.0, 500.0, 70000.0, 0.3, 1.0e-5, 300.0, 1000.0, 0.3] +VECTORS_M = {"Etot": 6, "etot": 6, "PKII": 6, "tau": 6, "sigma": 6, "Wm": 4} +MATRICES = {"F1": 9, "R": 9, "DR": 9} + + +def run(blocks, umat, props, nstatev, record_tangent=True, corate=3, **params): + blocks_py = [b.to_dict(290.0) for b in blocks] + return _core.solver_run(blocks_py, 290.0, umat, np.asarray(props, dtype=float), nstatev, None, 0, + corate, dict(tangent_mode=2, **params), record_tangent, None) + + +def check_common(raw, n, nstatev): + for key in ("block", "cycle", "step", "inc", "time", "T"): + assert raw[key].shape == (n,) + for key, ncomp in {**VECTORS_M, **MATRICES}.items(): + a = raw[key] + assert a.shape == (n, ncomp), key + assert a.flags.f_contiguous, key + assert raw["statev"].shape == (n, nstatev) + # the Cauchy stress of every record is tau / det(F1) + F = raw["F1"].reshape(n, 3, 3) + np.testing.assert_allclose(raw["sigma"], raw["tau"] / np.linalg.det(F)[:, None], rtol=1e-14, atol=0.0) + + +CASES = { + "small_strain": (lambda: [Block(steps=[StepMeca(control=MIXED, value=[0.01, 0, 0, 0, 0, 0], ninc=12), + StepMeca(control=MIXED, value=[-0.005, 0, 0, 0, 0, 0], ninc=8)], + ncycle=2)], 40), + "logarithmic": (lambda: [Block(steps=[StepMeca(control=MIXED, value=[0.2, 0, 0, 0, 0, 0], ninc=15)], + control_type="logarithmic")], 15), +} + + +@pytest.mark.parametrize("case", sorted(CASES)) +@pytest.mark.parametrize("record_tangent", [True, False]) +def test_mechanical_history(case, record_tangent): + make, n = CASES[case] + raw = run(make(), "EPICP", EPICP_PROPS, 8, record_tangent) + assert raw["status"] == 0 and raw["sv_type"] == 1 + check_common(raw, n, 8) + assert "dSdE" not in raw and "Q" not in raw + if record_tangent: + assert raw["Lt"].shape == (n, 36) and raw["Lt"].flags.f_contiguous + else: + assert "Lt" not in raw + res = SolverResults(raw, [case != "small_strain"], 3) + assert res["Stress"].shape == (6, n) and res["Stress"].flags.c_contiguous + assert np.shares_memory(res["Stress"], raw["sigma"]) + + +def test_matrices_are_flattened_row_by_row(): + # a non-symmetric deformation gradient prescribed in full: the last F1 is the target, + # written row by row in BC_meca and in the record alike + target = np.array([[1.2, 0.1, 0.0], [0.05, 0.9, 0.02], [0.0, 0.03, 0.95]]) + raw = run([Block(steps=[StepMeca(control="F", value=target.ravel(), ninc=10)], control_type="F")], + "NEOHC", [1000.0, 10000.0], 1) + assert raw["status"] == 0 + check_common(raw, 10, 1) + np.testing.assert_allclose(raw["F1"][-1].reshape(3, 3), target, rtol=0.0, atol=1e-12) + res = SolverResults(raw, [True], 3) + np.testing.assert_array_equal(res["F"][:, :, -1], raw["F1"][-1].reshape(3, 3)) + np.testing.assert_array_equal(res["TangentMatrix"][:, :, 3], raw["Lt"][3].reshape(6, 6)) + + +@pytest.mark.parametrize("record_tangent", [True, False]) +def test_thermomechanical_history(record_tangent): + n = 10 + step = StepThermomeca(control=MIXED, value=[0.01, 0, 0, 0, 0, 0], ninc=n, T_final=320.0) + raw = run([Block(steps=[step])], "EPICP", EPICP_T_PROPS, 8, record_tangent) + assert raw["status"] == 0 and raw["sv_type"] == 2 + check_common(raw, n, 8) + assert raw["Q"].shape == (n,) and raw["r"].shape == (n,) and raw["Wt"].shape == (n, 3) + assert "Lt" not in raw + if record_tangent: + assert raw["dSdE"].shape == (n, 36) + for key in ("dSdT", "drdE"): + assert raw[key].shape == (n, 6) + assert raw["drdT"].shape == (n, 1) + else: + assert "dSdE" not in raw + + +def test_aborted_run_keeps_its_records(): + # a stress beyond the saturation of a perfectly plastic law, without inforce: the run stops + # early and every history holds the same records + step = StepMeca(control=["stress"] * 6, value=[900.0, 0, 0, 0, 0, 0], ninc=50, Dn_mini=0.25) + raw = run([Block(steps=[step])], "EPICP", [70000.0, 0.3, 1.0e-5, 300.0, 0.0, 1.0], 8, inforce=0) + assert raw["status"] == 1 + n = raw["time"].shape[0] + assert 0 < n < 50 + check_common(raw, n, 8) + assert raw["Lt"].shape == (n, 36) From d8598a75a852184d01d2a5a701af6fc4b27babee Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Tue, 3 Nov 2026 23:08:28 +0100 Subject: [PATCH 07/12] Call Python laws with a vectorcall and interned keywords The PYEXT bridge called the registered law with fifteen keyword arguments through pybind11, which builds a kwargs dict key by key at every call: a third of the bridge. It now uses PyObject_Vectorcall with a tuple of the keyword names interned once. The law receives the same values in the same order, and the bridge now holds its own reference to the law for the duration of the call, which may unregister or replace it. The bridge and solver overhead per call drops from 4.4 to 1.9 us; an increment of a trivial Python law from 6.8 to 4.1 us. --- .../Libraries/Continuum_mechanics/pyumat.cpp | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/pyumat.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/pyumat.cpp index 8c39d5325..34504f610 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/pyumat.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/pyumat.cpp @@ -51,6 +51,26 @@ struct step_cut_tag {}; PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store g_step_cut; static py::object *g_fn = nullptr; // touched only with the GIL held +// The keyword names of the call, interned once (same lifetime rule as g_step_cut). The call +// passes them with the values in one vector (PyObject_Vectorcall): building a kwargs dict +// key by key, as a pybind11 keyword call does, was a third of the bridge. +constexpr const char *k_kwnames[] = {"Etot", "DEtot", "sigma", "DR", "props", "statev", "T", "DT", + "Time", "DTime", "Wm", "ndi", "nshr", "start", "tangent_mode"}; +constexpr std::size_t k_nkw = sizeof(k_kwnames) / sizeof(k_kwnames[0]); +PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store g_kwnames; + +const py::tuple &kwnames() { + return g_kwnames.call_once_and_store_result([]() { + py::tuple names(k_nkw); + for (std::size_t k = 0; k < k_nkw; k++) { + PyObject *s = PyUnicode_InternFromString(k_kwnames[k]); + if (!s) throw py::error_already_set(); + names[k] = py::reinterpret_steal(s); + } + return names; + }).get_stored(); +} + using farr = py::array_t; // (rows, cols) column-major copy — cheaper than simpy::arma_to_numpy::mat_to_arr(arma::mat(m)), which @@ -126,20 +146,28 @@ void python_umat_bridge(const std::string &umat_name, const arma::vec &Etot, con if (!g_fn || g_fn->is_none()) { throw simcoon::exception_solver("PYEXT: no Python UMAT registered"); } + // own reference for the call: a law that unregisters or replaces itself must stay alive + const py::object fn = *g_fn; py::array_t Wm4(4); { double *w = Wm4.mutable_data(); w[0] = Wm; w[1] = Wm_r; w[2] = Wm_ir; w[3] = Wm_d; } + // keyword arguments, in the order of k_kwnames + const py::object values[k_nkw] = {np1d(Etot), np1d(DEtot), np1d(sigma), np2d(DR), np1d(props), + np1d(statev), py::float_(T), py::float_(DT), py::float_(Time), + py::float_(DTime), Wm4, py::int_(ndi), py::int_(nshr), + py::bool_(start), py::int_(tangent_mode)}; py::object ret; try { - ret = (*g_fn)("Etot"_a = np1d(Etot), "DEtot"_a = np1d(DEtot), "sigma"_a = np1d(sigma), - "DR"_a = np2d(DR), "props"_a = np1d(props), - "statev"_a = np1d(statev), - "T"_a = T, "DT"_a = DT, "Time"_a = Time, "DTime"_a = DTime, - "Wm"_a = Wm4, "ndi"_a = ndi, "nshr"_a = nshr, "start"_a = start, - "tangent_mode"_a = tangent_mode); + PyObject *argv[k_nkw + 1]; // argv[0]: the slot PY_VECTORCALL_ARGUMENTS_OFFSET lends the callee + argv[0] = nullptr; + for (std::size_t k = 0; k < k_nkw; k++) argv[k + 1] = values[k].ptr(); + PyObject *res = PyObject_Vectorcall(fn.ptr(), argv + 1, PY_VECTORCALL_ARGUMENTS_OFFSET, + kwnames().ptr()); + if (!res) throw py::error_already_set(); + ret = py::reinterpret_steal(res); } catch (py::error_already_set &e) { if (e.matches(g_step_cut.get_stored())) { // Requested step cut: leave sigma/statev/Wm untouched (the solver restores the From 3f54267027a70bd72db1335ba4e942ce2dd32949 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Tue, 3 Nov 2026 23:08:28 +0100 Subject: [PATCH 08/12] Borrow the inputs of the Tensor2 and Tensor4 batch calls The Python side passes Fortran-ordered cubes so that the binding can borrow them, but the batch entries copied them, and copied and transposed the (N, 6) Voigt arrays. Read-only cubes are now borrowed, and a C-ordered (N, 6) array is read as the column-major (6, N) matrix it is; other layouts are still copied. Outputs are unchanged. Batch Tensor2 rotations, push-forwards and pull-backs are 26 to 34 percent faster, Tensor4 ones 8 to 11 percent. --- .../Libraries/Continuum_mechanics/tensor.cpp | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/tensor.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/tensor.cpp index eca251647..d3ef3c50b 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/tensor.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/tensor.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -38,8 +39,14 @@ namespace { to_string(cols) + "), got (" + to_string(shape[0]) + ", " + to_string(shape[1]) + ")"); } } - // Helper: numpy (N,6) → arma mat (6,N) — copy + transpose + // Helper: numpy (N,6) → arma mat (6,N), read only. A C-ordered (N,6) buffer is the + // column-major (6,N) matrix itself: borrowed (the caller keeps it alive for the call); + // any other layout is copied and transposed mat np2d_to_mat6N(const py::array_t& arr) { + if (arr.ndim() == 2 && arr.size() > 0 && (arr.flags() & py::array::c_style) && arr.writeable() + && reinterpret_cast(arr.data()) % alignof(double) == 0) { + return mat(const_cast(arr.data()), arr.shape(1), arr.shape(0), false, true); + } mat m = simpy::numpy_to_arma::arr_to_mat(arr); // numpy (N,6) → arma (N,6) return m.t(); // → arma (6,N) } @@ -330,8 +337,8 @@ void register_tensor(py::module_& m) { // ================================================================ // Batch operations // Python side prepares: - // voigt as (N,6) C-order → converted via np2d_to_mat6N (copy + .t()) - // cubes as (R,C,N) F-order → zero-copy via simpy::numpy_to_arma::arr_to_cube + // voigt as (N,6) C-order → borrowed as (6,N) by np2d_to_mat6N + // cubes as (R,C,N) F-order → borrowed via simpy::numpy_to_arma::arr_to_cube_view // Each name is a pybind overload set (tensor2 + tensor4 variants), mirroring // the C++ batch_* overloads; dispatch is disambiguated by the enum argument. @@ -340,7 +347,7 @@ void register_tensor(py::module_& m) { py::array_t voigt, simcoon::Tensor2Type vtype, py::array_t rot_matrices, bool active) { mat v_cpp = np2d_to_mat6N(voigt); - cube r_cpp = simpy::numpy_to_arma::arr_to_cube(rot_matrices); + cube r_cpp = simpy::numpy_to_arma::arr_to_cube_view(rot_matrices); mat result; { py::gil_scoped_release release; @@ -355,7 +362,7 @@ void register_tensor(py::module_& m) { py::array_t voigt, simcoon::Tensor2Type vtype, py::array_t F_arr, bool metric) { mat v_cpp = np2d_to_mat6N(voigt); - cube f_cpp = simpy::numpy_to_arma::arr_to_cube(F_arr); + cube f_cpp = simpy::numpy_to_arma::arr_to_cube_view(F_arr); mat result; { py::gil_scoped_release release; @@ -370,7 +377,7 @@ void register_tensor(py::module_& m) { py::array_t voigt, simcoon::Tensor2Type vtype, py::array_t F_arr, bool metric) { mat v_cpp = np2d_to_mat6N(voigt); - cube f_cpp = simpy::numpy_to_arma::arr_to_cube(F_arr); + cube f_cpp = simpy::numpy_to_arma::arr_to_cube_view(F_arr); mat result; { py::gil_scoped_release release; @@ -400,7 +407,7 @@ void register_tensor(py::module_& m) { []( py::array_t t4_arr, simcoon::Tensor4Type t4type, py::array_t t2_arr, simcoon::Tensor2Type t2_vtype) { - cube t4_cpp = simpy::numpy_to_arma::arr_to_cube(t4_arr); + cube t4_cpp = simpy::numpy_to_arma::arr_to_cube_view(t4_arr); mat t2_cpp = np2d_to_mat6N(t2_arr); mat result; { @@ -416,8 +423,8 @@ void register_tensor(py::module_& m) { []( py::array_t t4_arr, simcoon::Tensor4Type t4type, py::array_t rot_matrices, bool active) { - cube t4_cpp = simpy::numpy_to_arma::arr_to_cube(t4_arr); - cube r_cpp = simpy::numpy_to_arma::arr_to_cube(rot_matrices); + cube t4_cpp = simpy::numpy_to_arma::arr_to_cube_view(t4_arr); + cube r_cpp = simpy::numpy_to_arma::arr_to_cube_view(rot_matrices); cube result; { py::gil_scoped_release release; @@ -431,8 +438,8 @@ void register_tensor(py::module_& m) { []( py::array_t t4_arr, simcoon::Tensor4Type t4type, py::array_t F_arr, bool metric) { - cube t4_cpp = simpy::numpy_to_arma::arr_to_cube(t4_arr); - cube f_cpp = simpy::numpy_to_arma::arr_to_cube(F_arr); + cube t4_cpp = simpy::numpy_to_arma::arr_to_cube_view(t4_arr); + cube f_cpp = simpy::numpy_to_arma::arr_to_cube_view(F_arr); cube result; { py::gil_scoped_release release; @@ -446,8 +453,8 @@ void register_tensor(py::module_& m) { []( py::array_t t4_arr, simcoon::Tensor4Type t4type, py::array_t F_arr, bool metric) { - cube t4_cpp = simpy::numpy_to_arma::arr_to_cube(t4_arr); - cube f_cpp = simpy::numpy_to_arma::arr_to_cube(F_arr); + cube t4_cpp = simpy::numpy_to_arma::arr_to_cube_view(t4_arr); + cube f_cpp = simpy::numpy_to_arma::arr_to_cube_view(F_arr); cube result; { py::gil_scoped_release release; @@ -460,7 +467,7 @@ void register_tensor(py::module_& m) { m.def("_batch_inverse", []( py::array_t t4_arr, simcoon::Tensor4Type t4type) { - cube t4_cpp = simpy::numpy_to_arma::arr_to_cube(t4_arr); + cube t4_cpp = simpy::numpy_to_arma::arr_to_cube_view(t4_arr); cube result; { py::gil_scoped_release release; From d91804af56ff6f2bc549d3cb8a02c6dcf185fd52 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Tue, 3 Nov 2026 23:08:28 +0100 Subject: [PATCH 09/12] Read orientation angles without building rotations euler_angles, called by every solve and for every phase handed to L_eff or solve, validated Euler angles by building a scipy rotation it then discarded, and decomposed the identity of every default phase orientation. Angles are now checked directly, and Rotation.identity() switches the class of scipy's exact unit quaternions instead of a renormalizing round trip, and marks a single identity so that its angles are returned without decomposition. Results and errors are unchanged. euler_angles drops from 15 to 1.2 us for given angles, a phase object builds in 3.6 instead of 15 us, a two-phase Mori-Tanaka L_eff takes 17 instead of 37 us, and every solve call saves about 14 us. --- python-setup/simcoon/rotation.py | 42 +++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/python-setup/simcoon/rotation.py b/python-setup/simcoon/rotation.py index 70dd44a04..06c1ab7bc 100644 --- a/python-setup/simcoon/rotation.py +++ b/python-setup/simcoon/rotation.py @@ -7,6 +7,7 @@ methods (``apply_stress``, ``apply_stiffness``, etc.). """ +import math import warnings from typing import Dict, Sequence, Union @@ -81,7 +82,16 @@ def from_mrp(cls, mrp): @classmethod def identity(cls, num=None): - return cls._ensure_cls(super().identity(num)) + r = super().identity(num) + if type(r) is not cls: + # exact unit quaternions: nothing to renormalize through from_quat (the default + # orientation of every phase, built with each phase object) + r.__class__ = cls + if num is None: + # a single rotation cannot be modified in place: euler_angles reads its angles + # without decomposing it + r._exact_identity = True + return r @classmethod def random(cls, num=None, random_state=None): @@ -501,17 +511,23 @@ def as_rotation(value: Orientation) -> Rotation: return value if isinstance(value, ScipyRotation): return Rotation.from_scipy(value) + return Rotation.from_euler(EULER_SEQ, _euler_triplet(value), degrees=True) + + +def _euler_triplet(value): + """The Euler angles of a ``{"psi", "theta", "phi"}`` dict (missing angles are 0) or of a + 3-sequence, in degrees, checked: unknown keys, a count other than 3 or a non-numeric value + raise ``ValueError``.""" if isinstance(value, dict): unknown = set(value) - set(_ANGLES) if unknown: raise ValueError(f"orientation: unknown keys {sorted(unknown)}; expected {_ANGLES}") - angles = [float(value.get(k, 0.0)) for k in _ANGLES] - else: - angles = np.asarray(value, dtype=float).ravel() - if angles.size != 3: - raise ValueError(f"orientation: 3 Euler angles (psi, theta, phi) in degrees " - f"expected, got {angles.size} values") - return Rotation.from_euler(EULER_SEQ, angles, degrees=True) + return [float(value.get(k, 0.0)) for k in _ANGLES] + angles = np.asarray(value, dtype=float).ravel() + if angles.size != 3: + raise ValueError(f"orientation: 3 Euler angles (psi, theta, phi) in degrees " + f"expected, got {angles.size} values") + return angles def euler_angles(rotation: Orientation) -> Dict[str, float]: @@ -525,11 +541,15 @@ def euler_angles(rotation: Orientation) -> Dict[str, float]: if not isinstance(rotation, ScipyRotation) and rotation is not None: # angles given as angles are written as given: no detour through a quaternion # (float noise, and a gimbal-locked triplet rewritten) for a no-op - as_rotation(rotation) # validates the dict keys / the 3 values - values = ([float(rotation.get(k, 0.0)) for k in _ANGLES] if isinstance(rotation, dict) - else [float(a) for a in np.asarray(rotation, dtype=float).ravel()]) + values = [float(a) for a in _euler_triplet(rotation)] # validates the dict keys / the 3 values + if not all(math.isfinite(a) for a in values): + as_rotation(rotation) # the error of a rotation built from non-finite angles return dict(zip(_ANGLES, values)) rot = as_rotation(rotation) + if getattr(rot, "_exact_identity", False): + # Rotation.identity() (the default orientation of every phase): the angles scipy + # returns for it, +0, without the decomposition + return {'psi': 0.0, 'theta': 0.0, 'phi': 0.0} with warnings.catch_warnings(): warnings.simplefilter('ignore') # scipy's "Gimbal lock detected" - see above psi, theta, phi = rot.as_euler(EULER_SEQ, degrees=True) From d87635479bdd804a44e6ed7eab896bf1a891a5a1 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 6 Nov 2026 10:43:46 +0100 Subject: [PATCH 10/12] Use fixed-order 6x6 kernels in the MODUL tangent Armadillo hands every product above 4x4 to BLAS/LAPACK, whose call costs more than the arithmetic at this size and whose summation order depends on the backend: - assemble_algorithmic_tangent: M^-1 [L | kappa] through small_lu<6> instead of a LAPACK inverse; L * sum(Ds dLambda/dsigma) through mul66 - assemble_tangent: the P / Lt products as fixed-order loops - ElasticityModule::evaluate and PlasticityMechanism::compute_constraints: the 6x6 matrix-vector products through mul6 - small_dense.hpp gathers mul6 and the mul66 / mul66_ABt of tensor.cpp - compute_tangent reserves its per-mechanism vectors Not bit-identical: Lt within 1.5e-14 relative in the batch dumps, solver paths within 3e-14 of each array's scale (Q of thermomechanical SMA_T 4e-11, a small difference of terms of order 30). Serial us/pt: MODUL plastic 3.06 -> 2.16, EPKCP 3.60 -> 2.70, MODUL elastic 0.31 -> 0.28. --- .../simcoon/Simulation/Maths/small_dense.hpp | 85 +++++++++++++++++++ src/Continuum_mechanics/Functions/tensor.cpp | 25 +----- .../Umat/Modular/elasticity_module.cpp | 7 +- .../Umat/Modular/modular_umat.cpp | 5 ++ .../Umat/Modular/plasticity_mechanism.cpp | 7 +- .../Umat/tangent_assembly.cpp | 50 ++++++----- 6 files changed, 133 insertions(+), 46 deletions(-) create mode 100644 include/simcoon/Simulation/Maths/small_dense.hpp diff --git a/include/simcoon/Simulation/Maths/small_dense.hpp b/include/simcoon/Simulation/Maths/small_dense.hpp new file mode 100644 index 000000000..0a24b3c3b --- /dev/null +++ b/include/simcoon/Simulation/Maths/small_dense.hpp @@ -0,0 +1,85 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file small_dense.hpp +///@brief Fixed-order 6x6 products for the per-point kernels +///@version 1.0 + +#pragma once +#include + +namespace simcoon{ + +/** @addtogroup maths + * @{ + */ + +/** + * @brief \f$ \mathbf{y} = \mathbf{A}\,\mathbf{x} \f$ for a column-major 6x6 @p A and a 6-vector @p x. + * + * Armadillo hands every product above 4x4 to BLAS; at this size the call costs more than the + * 36 multiply-adds, and the summation order depends on the BLAS backend. This loop has a fixed + * order (column by column), so the result is the same on every platform. + * @p y may alias @p x. + */ +inline void mul6(const double *A, const double *x, double *y) { + double r[6]; + for (int i = 0; i < 6; ++i) r[i] = A[i]*x[0]; + for (int k = 1; k < 6; ++k) + for (int i = 0; i < 6; ++i) r[i] += A[i + 6*k]*x[k]; + for (int i = 0; i < 6; ++i) y[i] = r[i]; +} + +/// \f$ \mathbf{y} = \mathbf{A}\,\mathbf{x} \f$ for a 6x6 @p A and a 6-vector @p x (see the pointer form). +inline void mul6(const arma::mat &A, const arma::vec &x, arma::vec &y) { + if (&y != &x) y.set_size(6); + mul6(A.memptr(), x.memptr(), y.memptr()); +} + +/// \f$ \mathbf{C} = \mathbf{A}\,\mathbf{B} \f$ for 6x6 operands, fixed summation order (see mul6). +inline arma::mat::fixed<6,6> mul66(const arma::mat &A, const arma::mat &B) { + const double *a = A.memptr(); + const double *b = B.memptr(); + arma::mat::fixed<6,6> C; + double *c = C.memptr(); + for (int j = 0; j < 6; ++j) + for (int i = 0; i < 6; ++i) { + double s = 0.0; + for (int k = 0; k < 6; ++k) s += a[i + 6*k]*b[k + 6*j]; + c[i + 6*j] = s; + } + return C; +} + +/// \f$ \mathbf{C} = \mathbf{A}\,\mathbf{B}^T \f$ for 6x6 operands, without forming the transpose. +inline arma::mat::fixed<6,6> mul66_ABt(const arma::mat &A, const arma::mat &B) { + const double *a = A.memptr(); + const double *b = B.memptr(); + arma::mat::fixed<6,6> C; + double *c = C.memptr(); + for (int j = 0; j < 6; ++j) + for (int i = 0; i < 6; ++i) { + double s = 0.0; + for (int k = 0; k < 6; ++k) s += a[i + 6*k]*b[j + 6*k]; + c[i + 6*j] = s; + } + return C; +} + +/** @} */ // end of maths group + +} //namespace simcoon diff --git a/src/Continuum_mechanics/Functions/tensor.cpp b/src/Continuum_mechanics/Functions/tensor.cpp index 5828f52a3..e0cb7ce52 100644 --- a/src/Continuum_mechanics/Functions/tensor.cpp +++ b/src/Continuum_mechanics/Functions/tensor.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -124,30 +125,6 @@ static arma::mat::fixed<6,6> mandel_rotation(const arma::mat::fixed<3,3> &Q) { return R6; } -// scalar 6x6 products: dgemm at this size costs more than the flops and contends across threads -static arma::mat::fixed<6,6> mul66(const arma::mat::fixed<6,6> &A, const arma::mat::fixed<6,6> &B) { - arma::mat::fixed<6,6> C; - for (int j = 0; j < 6; ++j) - for (int i = 0; i < 6; ++i) { - double s = 0.0; - for (int k = 0; k < 6; ++k) s += A(i,k) * B(k,j); - C(i,j) = s; - } - return C; -} - -// A * B^T without forming the transpose. -static arma::mat::fixed<6,6> mul66_ABt(const arma::mat::fixed<6,6> &A, const arma::mat::fixed<6,6> &B) { - arma::mat::fixed<6,6> C; - for (int j = 0; j < 6; ++j) - for (int i = 0; i < 6; ++i) { - double s = 0.0; - for (int k = 0; k < 6; ++k) s += A(i,k) * B(j,k); - C(i,j) = s; - } - return C; -} - // Volumetric projector (Mandel == engineering here: it has no shear entries). static arma::mat::fixed<6,6> base_volumetric() { arma::mat::fixed<6,6> m; diff --git a/src/Continuum_mechanics/Umat/Modular/elasticity_module.cpp b/src/Continuum_mechanics/Umat/Modular/elasticity_module.cpp index 991572003..d07628a29 100644 --- a/src/Continuum_mechanics/Umat/Modular/elasticity_module.cpp +++ b/src/Continuum_mechanics/Umat/Modular/elasticity_module.cpp @@ -20,6 +20,7 @@ along with simcoon. If not, see . * @brief Implementation of ElasticityModule class */ +#include #include #include #include @@ -290,7 +291,11 @@ void ElasticityModule::evaluate(const arma::vec& eps_el, int ndi, int nshr, throw std::invalid_argument("ElasticityModule: not configured"); } if (type_ != ElasticityType::HYPER_INVARIANTS) { - sigma = L_ * eps_el; + if (L_.n_rows == 6 && L_.n_cols == 6 && eps_el.n_elem == 6 ) { + mul6(L_, eps_el, sigma); + } else { + sigma = L_ * eps_el; + } Lt = L_; return; } diff --git a/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp b/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp index 9b69eaa62..3c4d40a70 100644 --- a/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp +++ b/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp @@ -767,6 +767,7 @@ void ModularUMAT::compute_tangent( // algorithmic assembly only for stress-dependent Phi with an analytic flow Hessian (the Hessians are kept) std::vector*>> algo; + algo.reserve(mechanisms_.size()); if (tangent_mode == tangent_closest_point) { // Exact operator of the closest-point solve; every multiplier-carrying row is inside it. for (size_t m = 0; m < mechanisms_.size(); ++m) { @@ -796,6 +797,10 @@ void ModularUMAT::compute_tangent( std::vector kappa_j; std::vector dPhi_l; std::vector dLambda_l; + rows.reserve(n_total); + kappa_j.reserve(n_total); + dPhi_l.reserve(n_total); + dLambda_l.reserve(n_total); for (const auto& [m, hess] : algo) { const auto& dPhi_all = mechanisms_[m]->dPhi_dsigma(sigma_eff); const auto& kappa_all = mechanisms_[m]->kappa(sigma_eff, 0.0, L_cur_); diff --git a/src/Continuum_mechanics/Umat/Modular/plasticity_mechanism.cpp b/src/Continuum_mechanics/Umat/Modular/plasticity_mechanism.cpp index b60300d37..b73f71c01 100644 --- a/src/Continuum_mechanics/Umat/Modular/plasticity_mechanism.cpp +++ b/src/Continuum_mechanics/Umat/Modular/plasticity_mechanism.cpp @@ -20,6 +20,7 @@ along with simcoon. If not, see . * @brief Implementation of PlasticityMechanism class */ +#include #include #include #include @@ -207,7 +208,11 @@ void PlasticityMechanism::compute_constraints( Y_crit.set_size(1); Y_crit(0) = std::max(sigma_Y_, 1.0); - kappa_ = L * flow_dir_; + if (L.n_rows == 6 && L.n_cols == 6 && flow_dir_.n_elem == 6) { + mul6(L, flow_dir_, kappa_); + } else { + kappa_ = L * flow_dir_; + } H_total_ = dR_dp + (has_kin ? kin_hard_->hardening_modulus(strain(flow_dir_), X_branches_) : 0.0); diff --git a/src/Continuum_mechanics/Umat/tangent_assembly.cpp b/src/Continuum_mechanics/Umat/tangent_assembly.cpp index c76a6acdd..d09d21a7a 100644 --- a/src/Continuum_mechanics/Umat/tangent_assembly.cpp +++ b/src/Continuum_mechanics/Umat/tangent_assembly.cpp @@ -25,6 +25,8 @@ #include #include +#include +#include #include namespace simcoon { @@ -82,20 +84,27 @@ ContinuumTangent assemble_tangent( } } - // P = (L dPhi) invBhat^T: Bhat rows = criteria, columns = mechanisms - arma::mat DP(6, Nmech); - for (arma::uword m = 0; m < Nmech; ++m) DP.col(m) = dPhidsigma_l[m]; - const arma::mat P = (Lt_dPhi * DP) * invBhat.t(); // 6 \times Nmech - - // Stack \kappa^l into a 6\times Nmech matrix for the rank-Nmech update Lt = L - \kappa P^T. - arma::mat K6(6, Nmech); - for (arma::uword l = 0; l < Nmech; ++l) K6.col(l) = kappa_j[l]; - - const arma::mat::fixed<6,6> KP = K6 * P.t(); + // P^l = sum_m invBhat(l,m) Lt_dPhi dPhi^m (Bhat rows = criteria, columns = mechanisms), then Lt = L - sum_l kappa^l P^l^T + assert(L.n_rows == 6 && L.n_cols == 6 && Lt_dPhi.n_rows == 6 && Lt_dPhi.n_cols == 6); + arma::mat LD(6, Nmech); + for (arma::uword m = 0; m < Nmech; ++m) mul6(Lt_dPhi.memptr(), dPhidsigma_l[m].memptr(), LD.colptr(m)); ContinuumTangent out; - out.Lt = L - KP; - out.P_epsilon.resize(Nmech); - for (arma::uword l = 0; l < Nmech; ++l) out.P_epsilon[l] = P.col(l); + out.P_epsilon.assign(Nmech, arma::vec(6, arma::fill::zeros)); + for (arma::uword l = 0; l < Nmech; ++l) { + double* p = out.P_epsilon[l].memptr(); + for (arma::uword m = 0; m < Nmech; ++m) { + const double b = invBhat(l, m); + const double* d = LD.colptr(m); + for (int i = 0; i < 6; ++i) p[i] += d[i]*b; + } + } + out.Lt = L; + for (arma::uword j = 0; j < 6; ++j) + for (arma::uword i = 0; i < 6; ++i) { + double s = 0.; + for (arma::uword l = 0; l < Nmech; ++l) s += kappa_j[l](i)*out.P_epsilon[l](j); + out.Lt(i, j) -= s; + } out.invBhat = std::move(invBhat); return out; } @@ -167,24 +176,25 @@ ContinuumTangent assemble_algorithmic_tangent( } // singular M (FD dLambda/dsigma near activation, LAPACK-dependent): continuum fallback - arma::mat::fixed<6,6> M = L * sumDsLambdaSigma; + arma::mat::fixed<6,6> M = mul66(L, sumDsLambdaSigma); M += I6; - arma::mat::fixed<6,6> Minv; - if (!arma::inv(Minv, M)) { + small_lu<6> lu; + if (!lu.factor(M)) { return assemble_continuum_tangent(Bhat_continuum, kappa_j, dPhidsigma_l, Ds_j, L); } // Algorithmic operators: \tilde{L} = M^{-1} L, \tilde{\kappa}^j = M^{-1} \kappa^j, // \tilde{B}^{lj} = Bhat_continuum^{lj} + \partial \Phi^l \cdot (M^{-1} - I) \cdot \kappa^j (Bhat_continuum = \partial \Phi \cdot \kappa - K). - const arma::mat::fixed<6,6> L_tilde = Minv * L; + arma::mat::fixed<6,6> L_tilde = L; + for (int c = 0; c < 6; ++c) lu.solve_in_place(L_tilde.colptr(c)); std::vector kappa_tilde(Nmech); for (arma::uword j = 0; j < Nmech; ++j) { - kappa_tilde[j] = Minv * kappa_j[j]; + kappa_tilde[j] = kappa_j[j]; + lu.solve_in_place(kappa_tilde[j].memptr()); } arma::mat Bhat_tilde = Bhat_continuum; - const arma::mat::fixed<6,6> Minv_minus_I = Minv - I6; for (arma::uword j = 0; j < Nmech; ++j) { - const arma::vec::fixed<6> dkappa_j = Minv_minus_I * kappa_j[j]; // shared by every row l + const arma::vec::fixed<6> dkappa_j = kappa_tilde[j] - kappa_j[j]; // (M^-1 - I) kappa_j, shared by every row l for (arma::uword l = 0; l < Nmech; ++l) { Bhat_tilde(l, j) += arma::dot(dPhidsigma_l[l], dkappa_j); } From afb947caa8e79c1d5fb0d1d5612cebdd344abbc9 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 6 Nov 2026 11:21:47 +0100 Subject: [PATCH 11/12] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78011f0ba..50cbcb35e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,10 @@ Known gaps and planned work, not yet in a release. An item moves to - The external plugins (UMEXT, UMABA, UMANS) serve the small-strain route only: a finite-strain entry in the plugin API (law fed the logarithmic strain, returning tau, as the internal kernels) would let them run under NLGEOM. +- `tangent_assembly` takes per-mechanism `std::vector` / `std::vector` + (κ, ∂Φ/∂σ, ∂Λ/∂σ): every MODUL tangent copies the 6×6 flow Hessians to the heap, the + largest remaining allocation source in MODUL plasticity. Fixed-size or view-based arguments + need an API change of `assemble_*_tangent`. - Migration of the laws to the modular engine (MODUL), state in `benchmark/README.md`. Routed through the adapter (code 201): ELISO, ELIST, ELORT, EPKCP, EPHIL, EPTRI, EPHAC, EPANI, EPDFA, EPCHG, EPHIN. To do: From 2724196d9786fa89db39bf0791932b0895990030 Mon Sep 17 00:00:00 2001 From: Yves Chemisky Date: Fri, 6 Nov 2026 19:01:50 +0100 Subject: [PATCH 12/12] Optimize constitutive ops and plastic Umat Add small_dense.hpp and optimize 6x6 tensor operations and congruence transforms. Use mul6 for fast 6-vector multiplications in plastic_isotropic (mechanical and thermomechanical) and replace several L*vec calls with mul6; compute L*alpha once into L_alpha and use it in thermomech flow. Introduce congruence_unit_strain and precompute Q/W/RU/T2 to simplify and speed up DtauDe_2_DSDE and DSDE_2_DtauDe, reducing repeated matrix products and clarifying tensor algebra. Remove an unnecessary reserve() in modular_umat to avoid redundant allocation. --- .../Functions/constitutive.cpp | 18 ++++++- .../Functions/objective_rates.cpp | 48 ++++++++++++------- .../Plasticity/plastic_isotropic.cpp | 9 ++-- .../Umat/Modular/modular_umat.cpp | 1 - .../Plasticity/plastic_isotropic.cpp | 11 +++-- 5 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/Continuum_mechanics/Functions/constitutive.cpp b/src/Continuum_mechanics/Functions/constitutive.cpp index 6e7863c2b..10bb91224 100755 --- a/src/Continuum_mechanics/Functions/constitutive.cpp +++ b/src/Continuum_mechanics/Functions/constitutive.cpp @@ -30,6 +30,7 @@ along with simcoon. If not, see . #include #include #include +#include using namespace std; using namespace arma; @@ -615,15 +616,28 @@ static vec condensed_pred(const mat &L, const vec &E, const int &ndi, const int vec el_pred(const vec &sigma_start, const mat &L, const vec &DE_el, const int &ndi, const int &nshr) { - if (ndi == 3) + if (ndi == 3) { + if (L.n_rows == 6 && L.n_cols == 6 && DE_el.n_elem == 6 && sigma_start.n_elem == 6) { + vec sigma(6); + mul6(L.memptr(), DE_el.memptr(), sigma.memptr()); + sigma += sigma_start; + return sigma; + } return sigma_start + L*DE_el; + } return sigma_start + condensed_pred(L, DE_el, ndi, nshr); } vec el_pred(const mat &L, const vec &E_el, const int &ndi, const int &nshr) { - if (ndi == 3) + if (ndi == 3) { + if (L.n_rows == 6 && L.n_cols == 6 && E_el.n_elem == 6) { + vec sigma(6); + mul6(L.memptr(), E_el.memptr(), sigma.memptr()); + return sigma; + } return L*E_el; + } return condensed_pred(L, E_el, ndi, nshr); } diff --git a/src/Continuum_mechanics/Functions/objective_rates.cpp b/src/Continuum_mechanics/Functions/objective_rates.cpp index 12bac8b1a..d8250173b 100755 --- a/src/Continuum_mechanics/Functions/objective_rates.cpp +++ b/src/Continuum_mechanics/Functions/objective_rates.cpp @@ -790,6 +790,17 @@ inline m33 unit_strain_tensor(const int j) { return e; } +// Q^T e_j Q for the unit strain e_j of unit_strain_tensor, from two rows of Q (e_j has one or two entries) +inline m33 congruence_unit_strain(const m33 &Q, const int j) { + static const int ia[6] = {0, 1, 2, 0, 0, 1}, ib[6] = {0, 1, 2, 1, 2, 2}; + const int a = ia[j], b = ib[j]; + m33 D; + for (int l = 0; l < 3; ++l) + for (int k = 0; k < 3; ++k) + D(k, l) = (j < 3) ? Q(a, k)*Q(a, l) : 0.5*(Q(a, k)*Q(b, l) + Q(b, k)*Q(a, l)); + return D; +} + // y = Lt t2v_strain(e) inline void box_apply(const arma::mat::fixed<6,6> &Lt, const m33 &e, double y[6]) { const double x[6] = {e(0,0), e(1,1), e(2,2), e(0,1) + e(1,0), e(0,2) + e(2,0), e(1,2) + e(2,1)}; @@ -969,20 +980,20 @@ mat DtauDe_2_DSDE(const mat &Lt, const mat &F, const mat &tau){ sp.build(F, tau, false); const arma::mat::fixed<6,6> Lt_(Lt); const m33 invU_tau_hat = mm(sp.invU, sp.tau_hat); // column-independent factor of dS + // column-independent products: Q = R N (spatial frame of the eigenbasis), W = R U^-1, T2 = N^T tau_hat U^-1 + const m33 Q = mm(sp.R, sp.N); + const m33 W = mm(sp.R, sp.invU); + const m33 T2 = mm(mtm(sp.N, sp.tau_hat), sp.invU); mat DSDE(6, 6); double y[6]; for (int j = 0; j < 6; ++j) { - const m33 dC = 2. * unit_strain_tensor(j); // engineering Voigt basis - const m33 dCp = mm(mtm(sp.N, dC), sp.N); // shared eigenbasis transform - const m33 dh = mmt(mm(sp.N, m33(sp.G_h % dCp)), sp.N); // d(1/2 ln C) = d(ln U) - // The box acts in the spatial frame (d ln V = R d(ln U) R^T at fixed R): - // conjugate in, apply Lt, conjugate back to the corotational frame. - box_apply(Lt_, mmt(mm(sp.R, dh), sp.R), y); - const m33 dtau_hat = mm(mtm(sp.R, v2t_stress_arr(y)), sp.R); - const m33 dinvU = mmt(mm(sp.N, m33(sp.G_iU % dCp)), sp.N); // d(C^{-1/2}) - const m33 dS = mm(mm(sp.invU, dtau_hat), sp.invU) - + mm(mm(dinvU, sp.tau_hat), sp.invU) + mm(invU_tau_hat, dinvU); + const m33 dCp = 2. * congruence_unit_strain(sp.N, j); // N^T dC N, dC = 2 e_j (engineering Voigt) + // the box acts in the spatial frame: d ln V = R d(ln U) R^T = Q (G_h % dCp) Q^T + box_apply(Lt_, mmt(mm(Q, m33(sp.G_h % dCp)), Q), y); + const m33 A = mm(sp.N, m33(sp.G_iU % dCp)); // d(C^{-1/2}) = A N^T + // dS = U^-1 dtau_hat U^-1 + d(U^-1) tau_hat U^-1 + U^-1 tau_hat d(U^-1), dtau_hat = R^T dtau R + const m33 dS = mm(mtm(W, v2t_stress_arr(y)), W) + mm(A, T2) + mm(invU_tau_hat, mmt(A, sp.N)); set_stress_col(DSDE, j, dS); } return DSDE; @@ -1137,20 +1148,21 @@ mat DSDE_2_DtauDe(const mat &DSDE, const mat &F, const mat &tau) { const m33 G_comp = sp.G_iU % G_h_inv; // composed d(C^{-1/2}) o [d(1/2 ln C)]^{-1} const arma::mat::fixed<6,6> DSDE_(0.5 * DSDE); const m33 invU_tau_hat = mm(sp.invU, sp.tau_hat); // column-independent factor + // column-independent products: Q = R N, RU = R U, T2 = N^T tau_hat U^-1 + const m33 Q = mm(sp.R, sp.N); + const m33 RU = mm(sp.R, sp.U); + const m33 T2 = mm(mtm(sp.N, sp.tau_hat), sp.invU); mat Lt(6, 6); double y[6]; for (int j = 0; j < 6; ++j) { - // box argument is ln V: pull dh back by R (inverse of the forward R conjugation) - const m33 dh = mm(mtm(sp.R, unit_strain_tensor(j)), sp.R); - const m33 dhp = mm(mtm(sp.N, dh), sp.N); // eigenframe + // box argument is ln V: its eigenframe image N^T R^T e_j R N = Q^T e_j Q + const m33 dhp = congruence_unit_strain(Q, j); const m33 dC = mmt(mm(sp.N, m33(G_h_inv % dhp)), sp.N); // inverse of d(1/2 ln C) box_apply(DSDE_, dC, y); - const m33 dS = v2t_stress_arr(y); - const m33 dinvU = mmt(mm(sp.N, m33(G_comp % dhp)), sp.N); - const m33 inner = dS - mm(mm(dinvU, sp.tau_hat), sp.invU) - mm(invU_tau_hat, dinvU); - const m33 dtau_hat = mm(mm(sp.U, inner), sp.U); - set_stress_col(Lt, j, mmt(mm(sp.R, dtau_hat), sp.R)); + const m33 A = mm(sp.N, m33(G_comp % dhp)); // d(C^{-1/2}) = A N^T + const m33 inner = v2t_stress_arr(y) - mm(A, T2) - mm(invU_tau_hat, mmt(A, sp.N)); + set_stress_col(Lt, j, mmt(mm(RU, inner), RU)); // R U inner U R^T } return Lt; } diff --git a/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_isotropic.cpp b/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_isotropic.cpp index f64765a91..4800360be 100755 --- a/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_isotropic.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_isotropic.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -133,7 +134,7 @@ void umat_plasticity_iso(const string &umat_name, const vec &Etot, const vec &DE //Compute the explicit flow direction vec Lambdap = eta_stress(stress); std::vector kappa_j(1); - kappa_j[0] = L*Lambdap; + mul6(L, Lambdap, kappa_j[0]); mat K = zeros(1,1); //Loop parameters @@ -178,7 +179,7 @@ void umat_plasticity_iso(const string &umat_name, const vec &Etot, const vec &DE Phi(0) = Mises_stress(stress) - Hp - sigmaY; Lambdap = eta_stress(stress); - kappa_j[0] = L*Lambdap; + mul6(L, Lambdap, kappa_j[0]); K(0,0) = dPhidp; B(0, 0) = -1.*sum(dPhidsigma%kappa_j[0]) + K(0,0); @@ -241,7 +242,9 @@ void umat_plasticity_iso(const string &umat_name, const vec &Etot, const vec &DE } std::vector P_theta(1); - P_theta[0] = dPhidtheta - sum(dPhidsigma%(L*alpha)); + vec L_alpha; + mul6(L, alpha, L_alpha); + P_theta[0] = dPhidtheta - sum(dPhidsigma%L_alpha); double A_p = -Hp; double Dgamma_loc = 0.5*sum((stress_start+stress)%DEP) + 0.5*(A_p_start + A_p)*Dp; diff --git a/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp b/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp index 3c4d40a70..a4435c8c1 100644 --- a/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp +++ b/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp @@ -767,7 +767,6 @@ void ModularUMAT::compute_tangent( // algorithmic assembly only for stress-dependent Phi with an analytic flow Hessian (the Hessians are kept) std::vector*>> algo; - algo.reserve(mechanisms_.size()); if (tangent_mode == tangent_closest_point) { // Exact operator of the closest-point solve; every multiplier-carrying row is inside it. for (size_t m = 0; m < mechanisms_.size(); ++m) { diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.cpp index eef97e06f..526684547 100755 --- a/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -136,7 +137,7 @@ void umat_plasticity_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, double //Compute the explicit flow direction vec Lambdap = eta_stress(sigma); std::vector kappa_j(1); - kappa_j[0] = L*Lambdap; + mul6(L, Lambdap, kappa_j[0]); mat K = zeros(1,1); //Loop parameters @@ -185,7 +186,7 @@ void umat_plasticity_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, double Phi(0) = Mises_stress(sigma) - Hp - sigmaY; Lambdap = dPhidsigma; // associated J2 flow - kappa_j[0] = L*Lambdap; + mul6(L, Lambdap, kappa_j[0]); K(0,0) = dPhidp; B(0, 0) = -1.*sum(dPhidsigma%kappa_j[0]) + K(0,0); @@ -227,9 +228,11 @@ void umat_plasticity_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, double std::vector P_theta(1); // P_theta = invBhat (dPhi/dtheta - dPhi/dsigma:L:alpha), 1/K; invBhat masked so elastic gives 0 - P_theta[0] = ct.invBhat(0, 0) * (dPhidtheta - sum(dPhidsigma%(L*alpha))); + vec L_alpha; + mul6(L, alpha, L_alpha); + P_theta[0] = ct.invBhat(0, 0) * (dPhidtheta - sum(dPhidsigma%L_alpha)); - dSdT = -1.*L*alpha - (kappa_j[0]*P_theta[0]); + dSdT = -1.*L_alpha - (kappa_j[0]*P_theta[0]); //computation of the internal energy production double eta_r = c_0*log((T+DT)/T_init) + sum(alpha%sigma);