Skip to content

Add Modular UMAT integration#61

Open
chemiskyy wants to merge 53 commits into
masterfrom
feature/modular
Open

Add Modular UMAT integration#61
chemiskyy wants to merge 53 commits into
masterfrom
feature/modular

Conversation

@chemiskyy

@chemiskyy chemiskyy commented Jan 28, 2026

Copy link
Copy Markdown
Member

Modular UMAT framework — composable constitutive laws (typed Tensor2/Tensor4 API)

Composable UMAT built from pluggable modules, dispatched as MODUL (id 200):

  • ElasticityModule — isotropic / cubic / transverse-isotropic / orthotropic; cached typed tensor4 stiffness/compliance (L_tensor()/M_tensor() return const&, built once per configure).
  • StrainMechanismsPlasticityMechanism (von Mises/Tresca/Drucker/Hill/DFA/Ani × linear/power-law/Voce/combined-Voce iso × Prager/AF/Chaboche kin), ViscoelasticMechanism (Prony_Nfast generalized Maxwell), DamageMechanism (linear/exponential/power-law/Weibull CDM).
  • ModularUMAT orchestrator — convex cutting plane + Fischer–Burmeister multi-constraint solve with 3-phase Jacobian assembly (per-mechanism constraints → cross-mechanism coupling → diagonal); namespaced internal variables (plast0_, visco0_, damage0_, …) with objectivity rotation and statev pack/unpack.
  • Python API (simcoon.modular) — frozen dataclasses (ModularMaterial, IsotropicElasticity, Plasticity, Viscoelasticity, Damage, hardening/yield classes) that assemble props/nstatev for sim.solver("MODUL", ...).

Tensor2/Tensor4 integration (this update)

Rebased-by-merge onto feature/Tensor (Kelvin–Mandel-internal tensor4). The typed layer follows a "typed interface, raw solver core" boundary, chosen by benchmark:

  • Cross-mechanism coupling operands are typed: dPhi_dsigma()/kappa() return const std::vector<tensor2>&, so each operand carries its Voigt convention (dPhi strain-typed; kappa stress-typed for plasticity/viscoelasticity, strain-typed for damage — an inherited convention mix now visible in the type system and documented, not silently altered).
  • The B-assembly contraction is the engineering-Voigt dot (dot(a.voigt(), b.voigt())) — bit-identical to the legacy numerics.
  • Native tensor2 fast path for von Mises (Mises/flow_normal on the 3×3, cone-vertex-safe); parametric criteria delegate to Eq_stress.
  • Typing the FB hot-loop members themselves was tried and reverted: reproducible +7% on a 600-increment elastoplastic benchmark (eng↔3×3 conversion at every raw-internals boundary, flow re-derived each iteration). Rationale recorded in plasticity_mechanism.hpp.

Bug fixes found while closing test gaps

  1. Viscoelastic consistent tangent was missing the −1/Δt term in Bhat (rebuilt locally instead of reusing the cached K_diag_) → singular Lt, mixed-control solver divergence. Fixed; test_viscoelastic_matches_pronk_reference pins MODUL ≡ reference PRONK to 1.8e-4 relative over a 600-increment relaxation history.
  2. CDM softening never reached the stress: the tangent applied (1−D) but the stress prediction used the undamaged L. Added StrainMechanism::stiffness_reduction() (damage → 1−D, multiplicative across mechanisms) applied to the elastic prediction; damage integration tests added (elastic uniaxial with unload-freeze, plasticity+damage combined, Python end-to-end).

Verification

  • 42 gtests (Tmodular_umat), 6 modular pytests (268 total in test_core), full ctest 41/41.
  • Golden-output harness: elastoplastic (Voce+Chaboche), viscoelastic (3-branch Prony), combined — 600 increments each; outputs bit-identical across the tensorization commits, timings within noise.

Known modeling items (documented, deliberately not changed here)

  • DamageMechanism::compute_jacobian_contribution uses a placeholder B(r,r) = 1.0.
  • Damage kappa is strain-typed (M·σ) — the phase-2 pairing for damage rows/columns is a strain·strain dot (established numerics, flagged in the headers).
  • Plasticity sees the nominal (not effective) stress under damage.

Include 'simcoon.modular' in the conda.recipe/meta.yaml test.imports list so the conda build/test verifies the modular subpackage can be imported. This ensures the modular component is packaged and available during CI or conda package validation.
Introduce a new DamageMechanism implementing isotropic scalar continuum damage mechanics. Adds header and implementation files that integrate with StrainMechanism and InternalVariableCollection, register history variables (D, Y_max), compute the energy-based driving force Y = 0.5 * sigma : S : sigma, and evaluate damage via linear, exponential, power-law, and Weibull evolution laws. Includes damaged stiffness/tangent contributions, Jacobian/work computations, parameter configuration and basic validation, and caching of the compliance tensor.
Introduce PlasticityMechanism class (header + implementation) to provide a rate-independent plasticity strain mechanism for the modular UMAT. The new files combine a yield criterion, isotropic hardening and kinematic hardening (via factory-created hardening objects), register internal variables (p, EP and backstresses), and implement core routines: configure, register_variables, compute_constraints, compute_flow_directions, compute_jacobian_contribution, inelastic_strain, update, tangent_contribution and compute_work. The implementation caches flow direction and related quantities for Jacobian/tangent calculations, supports multiple yield/hardening types, and uses Armadillo vectors/matrices and the existing InternalVariableCollection API.
Introduce ElasticityModule (header + implementation) to encapsulate linear elastic behavior for the modular UMAT. Supports isotropic, cubic (EnuG or Cii conventions), transversely isotropic, and orthotropic configurations via configure_* helpers and a configure(type, props, offset) convenience. Stores 6x6 stiffness (L) and compliance (M) matrices, 6-component CTE vector (alpha), and provides derived helpers damaged_L(d) and thermal_strain(DT). Uses Armadillo types and existing constitutive functions (L_iso/M_iso, L_cubic/M_cubic, L_isotrans/M_isotrans, L_ortho/M_ortho). Adds basic validation (configured_ flag) and a constexpr props_count helper.
Introduce modular hardening framework: new header and implementation for isotropic and kinematic hardening models. Implements No/Linear/Power-law/Voce/Combined-Voce isotropic models and No/Prager/Armstrong–Frederick/Chaboche kinematic models, with factory create() functions, property parsing (configure), evaluation (R, dR/dp), internal-variable registration, backstress accumulation, flow rules and update routines. Uses Armadillo and InternalVariableCollection for vector storage and exposes props_count helpers to consume material property arrays.
Add a type-safe InternalVariable class to support constitutive-model state storage. New files include/include/simcoon/Continuum_mechanics/Umat/Modular/internal_variable.hpp and src/Continuum_mechanics/Umat/Modular/internal_variable.cpp implement scalar, 6-component Voigt vector, and 6x6 matrix types with start/current state tracking, delta computation, rotation/objectivity (uses rotate_strain / rotateL), and pack/unpack serialization to a flat statev array with configurable offset. Uses Armadillo, performs input validation, and throws on type mismatches. Intended for use in UMAT modular constitutive implementations.
Introduce InternalVariableCollection (header and implementation) to manage multiple InternalVariable instances for modular UMAT models. Provides named registration (scalar/6-vector/6x6-matrix), automatic statev offset computation, pack/unpack, rotation, start/reset helpers, iteration, and utility functions (names, clear). Uses unique_ptr storage (non-copyable, moveable), throws on invalid access/duplicate names, and depends on Armadillo.
Introduce a composable ModularUMAT class and standalone umat_modular function to orchestrate elasticity and multiple strain mechanisms (plasticity, viscoelasticity, damage). Adds public configuration APIs (set_elasticity, add_plasticity/add_viscoelasticity/add_damage, configure_from_props), initialization and state handling (internal variable collection), the main run() entry that performs elastic prediction, return mapping (Newton + Fischer–Burmeister), consistent tangent computation, and work calculations. Implements compute_tangent and return_mapping, and integrates with existing mechanism classes and numeric solver utilities. Files added: include/.../modular_umat.hpp and src/.../modular_umat.cpp (GPL header included).
Introduce a new header defining the StrainMechanism abstract base class for modular UMATs. Adds MechanismType enum and a comprehensive interface for pluggable strain mechanisms (configuration, variable registration, constraint evaluation, flow directions, Jacobian contributions, inelastic strain, internal variable updates, tangent contributions, and work decomposition). Uses Armadillo and InternalVariableCollection and includes GPL license header; intended to standardize integration of plasticity, viscoelasticity, damage, etc.
Introduce ViscoelasticMechanism implementing a generalized Maxwell (Prony series) viscoelastic branch model. Adds header and source with configuration, internal-variable registration, constraint evaluation, flow directions, Jacobian contribution, inelastic strain accumulation, update routine, tangent modification and work computation. Supports N Prony terms, registers one viscous strain vector per branch, reads g_i and tau_i from property vector, and uses an optional reference stiffness (L_0) / compliance for branch targets. Includes basic runtime checks (positive tau) and a simple algorithmic tangent and update/constraint approximations (implicit exponential integration and norm-based constraints). Files use Armadillo and include GPL license headers.
Introduce YieldCriterion class (header and implementation) to provide a modular wrapper around existing Eq_stress/dEq_stress criterion functions for UMAT. Adds YieldType enum (VON_MISES, TRESCA, DRUCKER, HILL, DFA, ANISOTROPIC), configuration helpers (including configure from a props vector with offset), parameter storage via arma::vec, and public APIs to compute equivalent_stress, flow_direction and plastic_flow (with optional backstress shift). Includes a constexpr props_count utility and runtime checks that the criterion is configured; integration relies on simcoon/Continuum_mechanics/Functions/criteria.hpp.
Introduce a new simcoon.modular module implementing a composable UMAT material system. Adds enums and dataclasses for elasticity (isotropic, cubic, transverse isotropic, orthotropic), yield criteria (VonMises, Tresca, Drucker, Hill, DFA, anisotropic), isotropic and kinematic hardening laws (linear, power-law, Voce, combined Voce, Prager, Armstrong-Frederick, Chaboche), and mechanisms (Plasticity, Viscoelasticity with Prony series, Damage). Implements ModularMaterial to assemble a flat props array (for the C++ "MODUL" UMAT), compute nprops/nstatev, provide human-readable summaries, and includes convenience factory functions (elastic_model, elastoplastic_model, viscoelastic_model).
Add an explicit `from simcoon import modular` to simcoon.__init__ so the `simcoon.modular` submodule is available to consumers when importing the package. This makes the modular API accessible without requiring an extra import.
Delete generated test output files from testBin/Umats/UMEXT/results: results_job_global-0.txt and results_job_local-0.txt. These are result artifacts (deleted mode 100755) and are not intended to be tracked in source control.
Add comprehensive GTest unit tests for the Modular UMAT implementation (test/Libraries/Umat/Modular/Tmodular_umat.cpp) and register the new test in test/CMakeLists.txt. The tests cover InternalVariable and InternalVariableCollection, ElasticityModule (isotropic/cubic/CTE), YieldCriterion (von Mises), Isotropic and Kinematic hardening models, PlasticityMechanism behavior, ModularUMAT initialization/run behavior, and an integration test for uniaxial tension. This provides broad verification of elastic/plastic responses, packing/unpacking of state variables, hardening laws, and consistency of tangents and stresses.
Integrate the new modular UMAT into the project: add modular_umat includes to the Python wrapper and smart UMAT selector, register the "MODUL" UMAT id in both mappings (30 for Python wrapper, 200 for smart selector), and add case branches to call umat_modular accordingly. Update CMakeLists to include the Modular UMAT source files (internal_variable, elasticity_module, yield_criterion, hardening, plasticity_mechanism, viscoelastic_mechanism, damage_mechanism, and modular_umat) so the modular implementation is built. This wires the composable constitutive model into the build, runtime selection, and Python bindings.
Brings the modular UMAT framework (ElasticityModule, PlasticityMechanism,
ViscoelasticMechanism, DamageMechanism, hardening, yield criteria,
InternalVariable/Collection, ModularUMAT orchestrator, MODUL UMAT name)
on top of the Tensor2/Tensor4/Rotation/Fastor stack.

Conflict resolutions:
- conda.recipe/meta.yaml: keep both simcoon._core and simcoon.modular imports
- umat_smart.cpp / Python umat.cpp: merge SMA name expansion + MODUL registration
- internal_variable.cpp: replace removed rotateL() with
  Rotation::from_matrix(DR).apply_stiffness(L)
- *_mechanism.cpp / hardening.cpp / damage_mechanism.cpp: rename sim_iota
  to simcoon::iota (renamed during the Tensor migration)

Baseline tests pass: 63 Ttensor + 27 Tmodular_umat.
Adds typed Tensor2/Tensor4 accessors alongside the raw arma::vec/arma::mat
accessors so callers can use the type-safe Tensor API (mirrors the EPICP
plugin pattern):

- L_tensor()              -> Tensor4 (Tensor4Type::stiffness)
- M_tensor()              -> Tensor4 (Tensor4Type::compliance)
- alpha_tensor()          -> Tensor2 (VoigtType::strain)
- damaged_L_tensor(d)     -> Tensor4 (stiffness)
- thermal_strain_tensor(DT) -> Tensor2 (strain)

Backward-compatible: L(), M(), alpha(), damaged_L(), thermal_strain()
unchanged. Internal storage stays arma::mat/vec; the typed accessors
construct on demand.

Adds ElasticityModuleTest.TensorAccessors verifying numerical equivalence
and correct type tags, plus an end-to-end stress = L : eps round-trip
through the typed contract.
Adds typed views over the stored Voigt vector / 6x6 matrix:

- as_tensor2(VoigtType)  / as_strain() / as_stress()
- as_tensor4(Tensor4Type) / as_stiffness() / as_compliance()
- set_tensor2(t) / set_tensor4(t) for write-back

The caller picks the VoigtType matching what the stored variable
represents (plastic strain → strain, etc.) since the storage itself is
untyped. The default VoigtType::strain matches the existing rotate()
convention which already uses the strain-Voigt factor-2 layout.

Also adds rotate(const Rotation&) overload so callers with a Rotation
object don't need to round-trip through a 3x3 matrix.

Tests: TensorViews (round-trip + type-mismatch rejection) and
RotationOverload (Rotation overload matches mat overload bitwise).
Adds typed overloads:
- equivalent_stress(const tensor2&)             -> double
- equivalent_stress(const tensor2&, const tensor2&) -> double (with backstress)
- flow_direction(const tensor2&)                -> tensor2 (VoigtType::strain)
- flow_direction(const tensor2&, const tensor2&) -> tensor2 (with backstress)

Implementations delegate to the existing arma::vec overloads via .voigt().
The flow direction returns strain-Voigt (factor-2 on shear) to match the
simcoon convention where dEP = dp * Lambda updates plastic strain
additively in the strain-Voigt storage layout.

Test: VonMisesTensor2Match verifies bitwise equivalence with the arma
overloads and the correct VoigtType on the returned flow direction.
…back-strain refactor

C++ side of the modular UMAT framework:

- ViscoelasticMechanism: faithful port of Prony_Nfast kernel — per-branch
  L_i(E_i, nu_i) and H_i(etaB_i, etaS_i), strain-form equality constraint
  Phi_i = ||invH_i.L_i.(eps - EV_i)|| - dv_i/dt = 0, lead scalar v_i + EV_i
  per branch (7 statev each). Inelastic strain via sum_i (M_0.L_i).EV_i.
- StrainMechanism: cross-mechanism coupling via three new virtuals:
  dPhi_dsigma(), kappa(), K_cross(). Rate-dependent mechanisms with strain-
  form Phi return empty dPhi_dsigma; FB residual handles equality+complement
  uniformly.
- ModularUMAT::return_mapping reorganised into three phases:
  (1) per-mechanism constraint evaluation,
  (2) global B_lj = -dPhi^l/dsigma . kappa^j + K^lj cross assembly,
  (3) per-mechanism diagonal fill.
- IVC prefix-based namespacing (plast<i>_, visco<i>_, damage<i>_) lets
  multiple same-type mechanisms coexist; StrainMechanism::set_ivc_prefix
  propagates to KinematicHardening for the alpha keys.
- Backstress refactored: store back-strain alpha (true thermodynamic
  internal variable, strain-like Voigt) instead of X (derived force).
  total_backstress() returns X = (2/3) C alpha derived. Matches the
  original plastic_chaboche_ccp.cpp convention. file-local helper
  backstress_t(C, alpha) centralises the conjugacy.
- InternalVariable: vtype_ (was rotation_kind_) is single VoigtType
  source of truth; rotate(Rotation&) routes through tensor2.rotate(R)
  so strain/stress dispatch is in one place. as_tensor2() defaults to
  stored vtype_.
- Mechanism kernels in hardening / damage migrated to typed tensor2 /
  tensor4 algebra where natural; viscoelastic stays raw arma because
  L_i / H_i are not modelled by tensor4.
- Hot-path: cached IVC keys at register_variables time across all
  mechanisms; cached mech_offset_ on ModularUMAT; dPhi_dsigma/kappa
  return const-ref into mechanism-local cache buffers; eliminates
  ~5-8x of allocator pressure in the FB hot loop.
- Tests (Tmodular_umat.cpp): TwoPlasticityEquivalentToOne,
  TwoPlasticityMechanismsInitialize, ArbitraryMultiMechanismInitialize,
  ViscoelasticRelaxation, ChabocheDuplicateEqualsDoubledAF,
  CombinedVoceDuplicateEqualsDoubled, RotationOverload, TensorViews,
  YieldAccessors. Total: 38 tests passing.
- simcoon/modular.py:
  * Viscoelasticity dataclass now takes (E, nu, etaB, etaS) per Prony
    branch (was (g, tau)) — matches the Prony_Nfast C++ kernel.
    nstatev = 7 * N (was 6 * N): adds the lead scalar v_i per branch.
  * __post_init__ rejects the legacy 2-tuple form and AF/Prager called
    with list arguments, with pointed error messages.
  * ModularMaterial.summary() prints per-branch viscoelastic parameters
    instead of just "N Prony terms".
  * viscoelastic_model() factory updated to the new 4-tuple layout.

- examples/umats/MODUL.py: declarative-API end-to-end example
  (ModularMaterial -> sim.solver -> stress-strain plot). Uses isotropic
  elasticity + von Mises + Voce hardening on a monotonic ramp + 2 cyclic
  blocks.
- examples/umats/data/MODUL_path.txt: companion path file.

- test_modular.py: pytest regression for the modular Python path.
  test_two_plasticity_equivalent_to_one_linear protects the FB cross-
  Jacobian fix (would have caught the pre-fix 19000 MPa nonphysical
  stress regression). Three other tests cover input validation and
  Voce smoke.
Conflict resolutions:
- pybind umat.cpp: MODUL renumbered 30 -> 200 (feature/Tensor took 30 for
  NEOHI); map + dispatch case updated to match the C++ id
- umat_smart.cpp: feature/Tensor map (SMRDI..=9, EPTRI=17) + {MODUL,200}
- umat_modular gains trailing tangent_mode param (unused) to match the
  unified small-strain UMAT pointer signature
- test/CMakeLists.txt: union (Ttangent_* + Tmodular_umat)
- MODUL example relocated to examples/mechanical + examples/data, test
  paths updated accordingly
tensor4 now stores Kelvin-Mandel internally; mat() recomputes the
engineering form, exact only to ~1 ulp of the entries (~2.5e-11 abs for
a 210 GPa stiffness). 1e-12 was calibrated to the old zero-conversion
storage; 1e-9 keeps the check meaningful (1e-14 relative).
tangent_contribution rebuilt K(i,i) as -dot(dPhi_i_dv, kappa_i) alone,
dropping the -1/DTime term that compute_constraints caches in K_diag_.
The Prony_Nfast reference keeps that term (Bhat = -K); without it the
rank-one correction roughly doubles and Lt turns singular/indefinite,
so any mixed-control solver run diverged (inv(): matrix is singular).
Reuse the cached K_diag_ instead.

Add test_viscoelastic_matches_pronk_reference: MODUL 3-branch Prony vs
the reference PRONK UMAT through sim.solver on PRONK_path.txt — max
stress deviation now 1.8e-4 relative (was: divergence). Closes the
missing viscoelastic end-to-end coverage.
StrainMechanism::dPhi_dsigma()/kappa() now return const
std::vector<tensor2>& so each cross-Jacobian operand carries its Voigt
convention: dPhi strain-typed (dEq_stress), kappa stress-typed for
plasticity/viscoelasticity, and strain-typed for damage (M.sigma
product) — the damage convention mix in the B assembly is now visible
in the type system and documented as a deliberate modeling item.

The orchestrator contracts on engineering Voigt components
(dot of .voigt()), the work-conjugate pairing — bit-identical to the
previous raw dots (strain voigt round-trips /2 then *2 exactly in FP).
Also hoist the per-mechanism kappa() fetch out of the phase-2 row loop
(it was re-invoked for every constraint row).

Goldens (elastoplastic/viscoelastic/combined, 600 incr each): outputs
bit-identical, timings within noise. gtests 38/38, pytest 5/5.
chemiskyy added 5 commits July 3, 2026 14:53
L_tensor()/M_tensor() built a tensor4 (eng->Mandel congruence) on every
call; with the typed plasticity core about to contract L_tensor() inside
the FB loop that would be a per-iteration cost. Build the typed mirrors
once in refresh_tensors(), called by every configure_* path; accessors
now return const&. Goldens bit-identical, timings in noise.
…hmarked)

Add KinematicHardening::total_backstress_t() (strain-convention tensor2,
documenting the back-strain Voigt layout the raw accessor returns) and
tensor2 convenience overloads of hardening_modulus()/update() that
delegate to the raw virtuals.

Typing the PlasticityMechanism hot members (flow_dir_/kappa_ as tensor2,
EPICP-style) was tried and REVERTED: the FB return-map re-derives the
flow every iteration against raw yield/hardening internals, so typed
members forced eng<->3x3 conversions at each boundary — a reproducible
+7% on the elastoplastic golden (vs noise +-3%). The typed view is
produced once per iteration at the dPhi_dsigma()/kappa() interface
instead; rationale recorded in plasticity_mechanism.hpp. This is the
'typed cores, raw solver' boundary the EPICP plugin pattern implies for
a per-iteration CCP loop.

Goldens bit-identical; timings back in noise. gtests 38/38, pytest 5/5.
YieldCriterion tensor2 overloads no longer round-trip through
.to_arma_voigt() for VON_MISES: equivalent_stress uses Mises(tensor2)
and flow_direction uses flow_normal(tensor2), both evaluated directly
on the 3x3 (the kernels added for the EPICP typed plugin). Parametric
criteria (Drucker = J2-J3 form, not Drucker-Prager; Hill/DFA/Ani)
keep delegating to the raw Eq_stress dispatch.

Tests: DruckerTensor2Delegates pins the delegate equivalence;
VonMisesNearZeroStress pins the cone-vertex behavior and documents the
intentional difference from the raw path there (flow_normal zeroes the
normal below iota, eta_stress normalizes noise for any n > 0).
gtests 40/40, pytest 5/5, goldens bit-identical.
…tests

DamageMechanism evolved D and softened the TANGENT ((1-D)*Lt in
tangent_contribution) but the orchestrator's stress prediction ignored
damage entirely — el_pred always used the undamaged L, so the composed
response never softened and Newton saw an inconsistent tangent/residual
pair. Add StrainMechanism::stiffness_reduction() (default 1; damage
returns 1-D; factors compose multiplicatively) and scale the elastic
prediction sigma = f * L : Eel at both return_mapping stress sites.

New coverage for the previously untested composed-damage paths:
- gtest DamageElasticUniaxial: D monotone in [0, D_c], stress matches
  (1-D)*L:E within 5%, D frozen on unloading (Y_max history)
- gtest PlasticityDamageCombined: plasticity+damage both activate
  (exercises phase-2 cross-assembly with the strain-typed damage kappa),
  softened vs plasticity-only reference
- pytest test_damage_softens_end_to_end through sim.solver

gtests 42/42, pytest 6/6 (test_core 268), goldens bit-identical,
timings in noise.
Move conceptual documentation into headers, per house style (.hpp =
Doxygen contract, .cpp = load-bearing implementation notes only):
- DamageType enum now carries the four evolution-law formulas in LaTeX
  (with the Y_eff = max(Y, Y_max) / threshold / D_c cap semantics);
  drop the .cpp per-case restatements
- PragerHardening class doc gains the back-strain convention block
  (alpha is the stored internal variable, X = (2/3) C alpha is derived)
  formerly an implementation comment in hardening.cpp
- Delete narration one-liners in elasticity_module.cpp (Compute
  stiffness/compliance/CTE) and the factory-method banners

Behavior-neutral: gtests 42/42, pytest 6/6.
@chemiskyy chemiskyy changed the base branch from master to feature/Tensor July 3, 2026 13:53
chemiskyy added 3 commits July 3, 2026 18:41
…anch

conda.recipe/meta.yaml selects its source via CONDA_BUILD_LOCAL:
  git_url: ...   # [not environ.get('CONDA_BUILD_LOCAL')]
  path: ..       # [environ.get('CONDA_BUILD_LOCAL')]
No workflow ever set it, so every conda-packaging run cloned the
repository default branch (master) and built THAT against the PR's
recipe. PR #61's recipe adds 'import simcoon.modular' to the test
imports -> all three OS jobs failed with ModuleNotFoundError even
though the module exists on the branch (the built package even shipped
master-only FTensor headers, confirming the wrong source).

Set CONDA_BUILD_LOCAL=1 at the job level: PR runs now validate the PR
source, and the workflow_call 'ref' input actually affects what is
built.
tangent_mode now reaches the orchestrator (umat_modular -> run ->
compute_tangent; umat_smart passes umat_M->tangent_mode). Mode 1
assembles the Simo-Hughes consistent tangent of the coupled sub-system
through master's multi-mechanism assemble_algorithmic_tangent(): the
phase-2/3 Jacobian assembly is factored into assemble_jacobian(), the
opted-in sub-block is handed over as Bhat = -B with the mechanisms'
kappa / dPhi_dsigma caches and the new flow Hessians. Mechanisms whose
flow is stress-independent (Prony viscoelastic, scalar damage) keep
their continuum tangent_contribution in every mode - exact, not a
fallback.

New interface (also the tangent_mode==2 preparation, aligned with
return_mapping.hpp on feature/algorithmic_tangent):
- StrainMechanism::dLambda_dsigma(): flow Hessians per constraint,
  compliance-typed tensor4 (stress->strain map: Mandel congruence
  carries the Hessian shear factors by construction). Plasticity
  implements it at the shifted stress via YieldCriterion::flow_hessian
  (deta_stress / ddHill_stress / ddDFA_stress / ddAni_stress); Tresca
  and Drucker have no analytic Hessian and opt out.
- StrainMechanism::refresh_state(): backward-Euler state rebuild from
  the IVC start values under total Ds (the CPP update_state contract).
  Plasticity implements it with the AF/Chaboche closed forms
  (alpha = (alpha_n + dp n)/(1 + D dp)) and a fixed point on the
  n <-> X coupling; KinematicHardening::refresh_state added.
- ModularUMAT::set_solver_params(maxiter, precision).

BUG FIX exposed by the mode-1 finite-difference test: the plasticity
local Jacobian carried +H_total instead of -H_total (dPhi/dp = -H; the
plastic_isotropic_ccp convention is K = -H, B = -n:kappa - H). The
wrong Newton slope skewed the FB error metric, so the loop reported
convergence prematurely: 4.4% peak-stress error vs the EPCHA reference
on the cyclic Voce+Chaboche path (31.9 MPa at 728 MPa peak), invisible
to all monotonic tests. Fixed, MODUL now matches EPCHA to 3.7e-4
relative over 600 cyclic increments.

Tests: AlgorithmicMatchesFiniteDifference (mode-1 tangent = FD Jacobian
of the discrete update to <1e-3, continuum >10x worse),
RefreshStateBackwardEulerAF (implicit relation satisfied to 1e-10),
test_tangent_mode_1_same_converged_response (solver mode 0 == mode 1),
test_chaboche_matches_epcha_reference (regression for the sign bug).
gtests 44/44, pytest 8/8 (test_core 273). Goldens re-recorded post-fix
(old elastoplastic golden was 4.4% off the reference; combined case now
converges 3x faster with the honest slope).
@chemiskyy chemiskyy changed the base branch from feature/Tensor to master July 4, 2026 22:10
chemiskyy added 19 commits July 5, 2026 00:32
The header documented a variable-term layout (props[4]=N_iso,
props[5]=N_kin, per-term tables, nstatev = 8 + 6 N_kin = 20) that the
code never implemented: umat_plasticity_chaboche_CCP reads a FIXED
10-prop layout (E, nu, alpha, sigmaY, Voce Q, b, C_1, D_1, C_2, D_2 —
one Voce term in rate form dHp = b(Q-Hp) dp, exactly two AF
backstresses) and 33 statev (T_init, p, EP, back-strains a_1/a_2,
backstresses X_1/X_2, Hp). Following the stale table (nstatev=20)
aborts with an out-of-bounds statev read.

Rewritten: props table (fixed indices), statev table (full 33-entry
layout, a_i vs X_i distinction), H_tot formula, Voce rate form, and
the @code example (props(10), zeros(33)). Points users needing a
variable number of terms to the modular UMAT (MODUL).
…n; IV hardening

BUG FIX (found by the review's typed-rotation test, which built its
fixture by copying the module's call): configure_orthotropic passed
(E1, nu12, nu13, E2, nu23, E3, ...) to L_ortho/M_ortho, whose EnuG slot
order is (E1, E2, E3, nu12, nu13, nu23, ...) — Poisson ratios landed in
the Young's-moduli slots, producing an indefinite stiffness
(cond ~ 3.5e9, negative eigenvalue) for every orthotropic modular
material. The props chain (Python dataclass -> configure -> named
params) was correct; only the call site was wrong. Orthotropic was the
one elasticity type without a configuration test — added
OrthotropicConfiguration (match vs direct L_ortho + positive
definiteness).

InternalVariable review batch:
- MATRIX_6x6 rotation now routes through tensor4 with a new stored
  Tensor4Type (t4type_, ctor/add_mat param, default stiffness) — the
  correct congruence for stiffness- AND compliance-like storage
  (Matrix66TypedRotation pins M_rot == inv(L_rot) for orthotropic L);
  as_tensor4() defaults to the stored type, mirroring as_tensor2().
- set_tensor2/set_tensor4 re-express the value in the variable's own
  convention (via the 3x3 / Mandel form) instead of copying raw
  components — assigning a stress-typed tensor2 to a strain-typed
  variable can no longer silently drop the factor-2 shear convention
  (SetTensor2ReexpressesConvention).
- pack/unpack throw on out-of-bounds offsets instead of silently
  truncating (PackUnpackOutOfBoundsThrows).
- rotate_all builds the Rotation once instead of re-extracting the
  quaternion from the same 3x3 for every variable.

gtests 48/48, pytest 273, goldens bit-identical.
ElasticityModule review batch:
- refresh_tensors() now rejects a non-positive-definite stiffness via a
  Cholesky test (cheap SPD check; umat_modular rebuilds the module every
  integration-point call, so a full eig_sym there was measurable). This
  turns an inadmissible material - a wrong parameter order (cf. the
  orthotropic arg-order bug) or physically impossible constants (e.g.
  nuTL^2 * EL/ET too large for transverse isotropy) - into an immediate,
  attributable error instead of an indefinite L that fails deep in the
  return mapping.
- configure_transverse_isotropic validates the axis (1/2/3) BEFORE
  calling L_isotrans, which otherwise terminates the process with
  exit(0) on an invalid axis - uncatchable from Python or a solver.
- Doc note: the cubic Cii form is reachable only through the direct C++
  API; the props-driven configure() and the Python dataclasses use EnuG.

Tests: RejectsInadmissibleMaterial (indefinite ortho, invalid axis,
inadmissible transverse-iso all throw; admissible carbon-fibre-like
constants accepted). gtests 49/49, pytest 273, goldens in noise.

Note: damaged_L / damaged_L_tensor are now exercised only by their own
unit test - the damage mechanism computes (1-D)L inline and the
orchestrator uses stiffness_reduction(). Kept as public convenience
accessors.
yield_criterion review: the criterion params (Drucker b/n, Hill FGHLMN,
DFA +K, Ani 9-term) were verified against criteria.cpp P_Hill/P_DFA/
P_Ani layouts (all correct), but no anisotropic criterion had ANY
end-to-end modular coverage — the same gap that hid the orthotropic
arg-order bug. Add test_hill_matches_ephil_reference: MODUL Hill+linear
hardening == EPHIL to machine precision (max |dsig11| = 0).

Uses linear hardening (m=1) on purpose: the power-law tangent
dR/dp = m k p^(m-1) is singular at p=0, so with m<1 the two CCP loops
differ by a re-converging transient (~1.4% at yield onset, verified
harmless); m=1 removes it and isolates the Hill criterion, which is
exact. pytest 9/9.
…rror)

BUG #6 (pre-existing, found by the review's shear cross-check vs EPCHA):
the backstress X = (2/3) C a is a STRESS-like tensor - it shifts the
stress in the yield function (sigma - X) and pairs with the strain-like
flow n in X:n - but it was carried in the back-strain (engineering
strain) Voigt convention with a factor-2 on shear. Under shear the
residual therefore subtracted a doubled X: 24.7% peak-stress error vs
the reference EPCHA (sig12 287 vs 230 MPa). Invisible on the uniaxial
path (shear components zero), so the uniaxial EPCHA test passed at
3.7e-4 and hid it - and isotropic von Mises under shear was already
bit-identical, pinning the defect to the kinematic term.

Two sites, both needed:
- backstress_t(): re-tag (2/3) C a as stress (via the convention-free
  3x3) so its Voigt form has no factor-2 shear.
- ChabocheHardening::total_backstress(): accumulate STRESS-typed; a
  strain-typed zero accumulator re-tagged the sum and put the factor-2
  back (this is the single-AF-via-Chaboche path too).
hardening_modulus() X:n and compute_work() X:dEP are fixed for free
(both now dot a stress-typed X against strain-typed n / dEP).

Now MODUL Chaboche under shear matches EPCHA to 3.5e-5. New regression
test_chaboche_shear_matches_epcha_reference on a pure-shear path (E12
driven, 2 backstresses) + examples/data/SHEAR_path.txt. Uniaxial
goldens bit-identical (zero shear). gtests 49/49, pytest 10/10.
…flow_directions)

strain_mechanism review — two dead-API removals, zero callers anywhere
(orchestrator, tests, pybind, Python):

- active_ / is_active() / set_active(): a settable flag the orchestrator
  never reads. set_active(false) silently left the mechanism fully
  contributing — worse than dead code (a misleading no-op). Toggling a
  mechanism needs real offset/statev handling; removed rather than left
  as a trap.
- compute_flow_directions(): pure virtual + 3 implementations, never
  invoked. Flow directions are computed inline in compute_constraints
  (cached as flow_dir_ / Lambda_i_); this was vestigial from an earlier
  design where they were a separate pass.

Behavior-neutral. gtests 49/49, pytest 10/10.
… vars + docs

viscoelastic_mechanism: correct the misleading 'filled in a separate
commit' note — the constraint Jacobian is diagonal BY STRUCTURE
(Phi_i depends on EV_i alone; dPhi_i/dv_j = 0 for j != i). Branch
coupling enters only the consistent tangent. Validated by the PRONK
equivalence test (3-branch, 1.8e-4).

damage_mechanism:
- remove dead D_new (computed, never used) and the unused dY multiplier
  read in update().
- document what the code actually does: damage is integrated EXPLICITLY
  (update() sets Y_max = max(Y, Y_max), D = f(Y_max)); the FB multiplier
  is unused and B(r,r) = 1.0 is a nominal unit slope for the history-type
  row, which self-satisfies (Phi = Y - Y_max -> 0 after the Y_max
  update). Cross-mechanism plasticity<->damage coupling still flows
  through the orchestrator's off-diagonal dPhi_dsigma . kappa terms.

Behavior-neutral. gtests 49/49, pytest 10/10.
The orchestrator committed whatever the FB loop produced, even a
diverged NaN/Inf state, without signalling the global solver — a hard
increment would silently propagate garbage (and never retry). Now run()
checks sigma.is_finite() after return_mapping and, on failure, sets
tnew_dt = 0.5 (halve the increment and retry), skips pack_all so statev
stays at its incoming values for the retry, and sets Lt elastic to keep
the rejected-step output well-defined.

Trigger is non-finite, NOT error > precision_: a FINITE unconverged-at-
maxiter state is still committed, matching the reference CCP UMATs
(UNUSED(tnew_dt)) — in particular the damage row is integrated
explicitly and never drives the FB error to precision_, so an
error-based cut would wrongly reject every damage step.

Behavior-neutral on all converging cases: gtests 49/49, pytest 275,
goldens bit-identical.
required_nstatev() hand-counts the statev size per mechanism type so
callers can size their statev array before initialize(). That duplicates
the authoritative count register_variables() produces, and could drift
silently (add a variable to a mechanism, forget the hand-count -> a
too-small statev and corrupted state downstream). initialize() already
has both numbers; it now throws if they disagree, so any drift fails
loudly on the first modular run instead of much later. gtests 49/49,
pytest 10/10.
…der shear

modular.py review: props/nstatev assembly matches configure_from_props /
register_variables for every dataclass (verified — and now guarded at
runtime by the initialize() consistency check). The one kinematic path
not covered by an equivalence test was the ArmstrongFrederickHardening
dataclass, which routes to a DISTINCT C++ class (not Chaboche). Lock it:
AF == single-term Chaboche bit-for-bit under shear, exercising the AF
total_backstress path (backstress_t, no accumulator). pytest 11/11.
Add runtime validation for elasticity configuration and corresponding tests. Include <stdexcept> and check that the assembled stiffness matrix L_ is strictly positive definite in refresh_tensors(), throwing a runtime_error if not (to prevent downstream failures from indefinite stiffness). Validate the transverse-isotropic axis in configure_transverse_isotropic() and throw on invalid axis instead of allowing L_isotrans to terminate the process. Minor refactor of a switch default comment and a note about cubic Cii API surface. Update unit tests to assert that inadmissible material configurations and invalid axis values are rejected (EXPECT_THROW) and that valid axis usage does not throw.
Remove deprecated damaged-stiffness helpers and update tests. Deleted the damaged_stiffness static method from DamageMechanism (header + source) and removed damaged_L / damaged_L_tensor from ElasticityModule (header + source). Updated unit test to drop assertions that relied on the removed API. This is an API cleanup — no other behavior changes are introduced.
Migrate KinematicHardening and its implementations to use typed tensor2 for backstress/flow normals instead of raw arma::vec Voigt vectors. Header changes update signatures for total_backstress, hardening_modulus, update and refresh_state to accept/return tensor2 and clarify strain/stress typing in docs. Implementations (Prager, Armstrong‑Frederick, Chaboche) were adapted to operate on tensor2, use strain/ to_arma_voigt conversions where needed, and fix accumulation/contracting to preserve stress vs. strain Voigt conventions. PlasticityMechanism call sites were updated to convert total_backstress to arma::vec when subtracting from sigma and to pass strain-typed normals into kinematic routines. Tests were updated to expect stress-typed total_backstress and to use strain-typed flow normals. Overall this enforces correct work-conjugacy and Voigt convention handling across kinematic hardening code.
Rename InternalVariable accessors to make raw Voigt/6x6 semantics explicit: vec() -> raw_voigt(), vec_start() -> raw_voigt_start(), mat() -> raw_mat(), mat_start() -> raw_mat_start(). Update header docstrings to explain the "raw" escape-hatch semantics and encourage using set_tensor2()/set_tensor4() for typed sources. Apply corresponding call-site updates across hardening, plasticity, viscoelastic, internal_variable_collection, internal_variable implementation, and unit tests to use the new accessors.
Replace the old requires_rotation API with an explicit is_objective flag to distinguish quantities that are frame-indifferent from rate-like quantities that require custom transport. Scalars are now always objective and their constructor no longer takes a rotate flag. Vector/matrix constructors and InternalVariableCollection registration functions use an objective boolean (instead of rotate).

Add typed START accessors (as_tensor2_start, as_tensor4_start) so mechanisms can reconstruct start-of-increment tensorial values safely (used for backward-Euler closed forms). Internals: rename member requires_rotation_ -> is_objective_, update rotate() behaviour to be a no-op for non-objective variables, and add related documentation updates.

Refactor InternalVariableCollection to store InternalVariable by-value in a std::deque (registration order == statev layout, stable references) and simplify its API: value-based storage, changed add_* signatures, simplified packing/unpacking/rotation/to_start logic, and removal of several previously-unused helpers. Update callers across mechanisms and tests to the new API and use the new typed start views in hardening closed-form updates.
Move internal-variable ownership into each StrainMechanism and remove the global InternalVariableCollection from ModularUMAT. Mechanisms now own an InternalVariableCollection (ivc_) and provide non-virtual helpers for statev serialization (compute_offsets, pack, unpack, rotate, to_start, variables, statev_size). Registration APIs were simplified to register_variables() with no external ivc argument; many mechanism methods had their InternalVariableCollection parameters removed (compute_constraints, compute_jacobian_contribution, dPhi_dsigma, kappa, inelastic_strain, update, tangent_contribution, compute_work, stiffness_reduction, etc.). ModularUMAT now composes statev as [T_init | mech0 | mech1 | ...], instructs mechanisms to register and compute offsets, and delegates pack/unpack/rotate/to_start per mechanism. Supporting changes: removal of ivc_prefix/key utilities, localized IVC key usage inside mechanisms, small hardening key defaults, and related adjustments across headers and sources to use the new API and ivc_ members. This improves encapsulation, avoids cross-mechanism key collisions, and simplifies hot-loop key handling.
Rename set_solver_params to set_return_mapping_params and clarify comments to indicate these control the local return-mapping (material-point Newton) loop. Remove the older set_max_iterations and set_precision convenience setters. Update tests to call the new API and adjust member comment to "Return-mapping controls".
Pin scikit-build-core to <1 in the GitHub Actions editable install and add --no-build-isolation so the venv's pinned backend builds the editable wheel; include a comment explaining the Windows DLL-vendoring break with 1.x. Also include simcoon/parameter.hpp and replace M_PI with simcoon::pi in a test to avoid MSVC missing M_PI (and add a brief explanatory comment).
Replace the old total_backstress/hardening_modulus API with per-branch compute_backstresses(...) and hardening_modulus(..., X_i) so branch backstresses are built once and passed to the modulus calculation. Add convenience wrappers that sum branch backstresses or compute modulus from ivc for cold paths. Update Prager, Armstrong–Frederick and Chaboche implementations to produce stress-typed branch backstresses and compute the exact tensorial hardening slope (including the <n,n> factor) to avoid Newton overshoots and incorrect tangents. PlasticityMechanism now caches X_branches_, invokes compute_backstresses once per FB iteration and reuses the branches for sigma_eff and hardening modulus. Also remove the conditional gating around applying FB iteration corrections so per-iteration corrections (including negative ds) are always applied; comments explain rationale.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant