Skip to content

Add stress_from_state material-specific implementations for MaterialModelsBase.jl#12 - #13

Open
knutambot wants to merge 6 commits into
KnutAM:mainfrom
knutambot:calculate-current-stress
Open

knutambot wants to merge 6 commits into
KnutAM:mainfrom
knutambot:calculate-current-stress

Conversation

@knutambot

Copy link
Copy Markdown

Summary

  • Introduces calculate_current_stress(m, ϵ, state), prototyping MaterialModelsBase.jl#12: given a strain and an already-converged/current material state, it returns the corresponding stress without invoking any local Newton iteration that would advance history variables. Useful for postprocessing, e.g. recomputing stress at a strain that differs slightly from the one used to produce state.
  • Implemented for LinearElastic (via a generic NoMaterialState fallback), Plastic, GeneralizedMaxwell, and the RotatedMaterial wrapper.
  • Supports lower-dimensional stress from a lower-dimensional strain via MaterialModelsBase.ReducedStressState, for LinearElastic and Plastic. This replaces the ad hoc, per-material calculate_stress dispatch hack used for plane-stress postprocessing in FerriteAssembly.jl#94's mixed_materials tutorial (that tutorial is not changed here — a follow-up PR should call this function instead).

Review process

Went through two rounds of independent Codex review (same thread):

  • Plan review caught two real bugs before implementation: (1) the reduced-dimensional Plastic formula originally reduced the plastic strain before subtracting it, which discards its out-of-plane component and gives the wrong answer for PlaneStrain (fixed by expanding the total strain to 3d first, then subtracting the full 3d plastic strain); (2) the RotatedMaterial/ReducedStressState wrapper methods were ambiguous with the NoMaterialState fallback when wrapping a stateless material (fixed with explicit disambiguating overloads).
  • Diff review against the finished implementation: no further findings.

Test plan

  • Pkg.test() passes: all pre-existing tests plus 16 new tests in test/test_current_stress.jl, including "frozen-state" checks that evaluating at a different strain gives a purely elastic increment rather than triggering a fresh plastic/viscous correction.
  • docs/make.jl builds cleanly (only pre-existing, unrelated warnings).
  • This package has no literate tutorials/howtos, so that step doesn't apply here.

Remaining risks / deliberately out of scope

  • Reduced-dimensional support for GeneralizedMaxwell and for RotatedMaterial wrapping a stateful material is not implemented (calling calculate_current_stress on those combinations throws a clean MethodError rather than silently giving a wrong answer).
  • FiniteStrainPlastic, CrystalPlasticity, and the hyperelastic models are not covered, since the motivating use case (FerriteAssembly.jl#94's plasticity postprocessing) is small-strain.

🤖 Generated with Claude Code

https://claude.ai/code/session_0181fqMcFarfUzBir46jb2rp

ClaudeBot and others added 2 commits September 17, 2026 11:34
Detailed change:
Introduces `calculate_current_stress(m, ϵ, state)` as a prototype for
MaterialModelsBase.jl#12: given a strain and an already-converged/current
material state, it returns the corresponding stress without invoking any
local Newton iteration that would advance history variables. This is meant
for postprocessing, e.g. recomputing the stress at a slightly different
strain than the one that produced `state`.

Implemented for `LinearElastic` (via a generic `NoMaterialState` fallback),
`Plastic`, `GeneralizedMaxwell`, and the `RotatedMaterial` wrapper. Also
supports lower-dimensional stress/strain via `MaterialModelsBase.ReducedStressState`
for `LinearElastic` and `Plastic`, replacing the ad hoc per-material
`calculate_stress` dispatch hack used for plane-stress postprocessing in
FerriteAssembly.jl#94's mixed_materials tutorial (that tutorial itself is not
changed here; a follow-up PR should call this function instead).

Went through the dual-review workflow with two rounds of independent Codex
review (same thread). The first round caught two real bugs before
implementation: (1) the reduced-dimensional `Plastic` formula originally
reduced the plastic strain before subtracting it from the strain, which
discards its out-of-plane component and gives the wrong answer for
`PlaneStrain` (fixed by expanding the total strain to 3d first, then
subtracting the full 3d plastic strain); (2) the wrapper methods for
`RotatedMaterial`/`ReducedStressState` were ambiguous with the
`NoMaterialState` fallback when wrapping a stateless material (fixed by
adding explicit disambiguating overloads). The second round, run against the
finished diff, reported no further findings.

Test results: `Pkg.test()` passes (all pre-existing tests plus 16 new tests
in test/test_current_stress.jl, including "frozen-state" checks that a
different strain gives a purely elastic increment rather than triggering a
fresh plastic/viscous correction). `docs/make.jl` builds cleanly (only
pre-existing, unrelated warnings). This package has no literate
tutorials/howtos, so that R2 step is not applicable here.

Remaining/deferred: reduced-dimensional support for `GeneralizedMaxwell` and
for `RotatedMaterial` wrapping a stateful material was not implemented in
this prototype (calling `calculate_current_stress` on those combinations
throws a clean MethodError rather than silently giving a wrong answer).
FiniteStrainPlastic, CrystalPlasticity, and the hyperelastic models are also
not covered, since the motivating FerriteAssembly.jl#94 use case and the
issue's plasticity postprocessing scenario are small-strain.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Detailed change:
Extends the `calculate_current_stress` prototype (MaterialModelsBase.jl#12)
from the previous commit to the finite-strain models in this package:

- `NeoHooke`, `CompressibleNeoHooke`, `SaintVenant`: required no new code,
  for both full-3d and `ReducedStressState`-wrapped use (e.g. plane stress) —
  already covered by the existing generic `NoMaterialState` fallbacks, since
  MaterialModelsBase's stress-state iteration machinery already supports
  finite-strain (`Tensor{2,3}`-based) reduced states generically. Added tests
  to confirm this.
- `FiniteStrainPlastic`: added a full-3d method that reuses the existing
  internal `calculate_PKstress(m, state, F)` helper, which already computes
  the frozen-`Fp` (converged state, no local Newton re-solve) 1st
  Piola-Kirchhoff stress. Added reduced-dimensional support via a small
  internal `FrozenStressMaterial <: AbstractMaterial` wrapper that packages
  the frozen PK-stress closure as a material, so it can ride
  MaterialModelsBase's existing stress-state Newton iteration (e.g. for
  `PlaneStress`); the tangent needed for that iteration is obtained via
  `Tensors.gradient` automatic differentiation.

Not covered (documented as a limitation in the docstring): `CrystalPlasticity`
(small-strain despite its docstring mentioning a finite-strain framework —
a separate, pre-existing gap), `ReducedStressState` for `GeneralizedMaxwell`,
and `RotatedMaterial` wrapping a finite-strain material (the latter already
errors in `RotatedMaterial`'s own `material_response` due to a hard
`::SymmetricTensor{2,3}` type assertion, independent of this change).

Went through the dual-review workflow (same Codex thread as the prior
change's plan review... actually a new thread this session). The plan review
caught a real bug before implementation: the planned
`MMB.NoMaterialState()` call has no zero-arg constructor
(`NoMaterialState{T}` is parametric), which would throw a `MethodError`
before any stress iteration starts. Fixed by removing the default argument
from `FrozenStressMaterial`'s `material_response` (the state is now always
passed explicitly) and constructing `MMB.NoMaterialState{eltype(F)}()`
explicitly at the call site. The diff review after implementation reported
no further findings.

Test results: `Pkg.test()` passes (27 tests in the `calculate_current_stress`
testset, up from 16; full suite green). `docs/make.jl` builds cleanly (only
pre-existing, unrelated warnings).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.18%. Comparing base (7c09cff) to head (501a024).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #13      +/-   ##
==========================================
+ Coverage   96.10%   96.18%   +0.07%     
==========================================
  Files          19       20       +1     
  Lines         745      760      +15     
==========================================
+ Hits          716      731      +15     
  Misses         29       29              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread src/CurrentStress.jl Outdated
Comment on lines +92 to +96
function calculate_current_stress(stress_state::MMB.AbstractStressState, m::FiniteStrainPlastic, F, state::FiniteStrainPlasticState)
frozen = FrozenStressMaterial(F_ -> calculate_PKstress(m, state, F_))
σ, _, _, _ = MMB.material_response(stress_state, frozen, F, MMB.NoMaterialState{eltype(F)}())
return σ
end

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function calculate_current_stress(stress_state::MMB.AbstractStressState, m::FiniteStrainPlastic, F, state::FiniteStrainPlasticState)
frozen = FrozenStressMaterial(F_ -> calculate_PKstress(m, state, F_))
σ, _, _, _ = MMB.material_response(stress_state, frozen, F, MMB.NoMaterialState{eltype(F)}())
return σ
end
function calculate_current_stress(stress_state::MMB.AbstractStressState, m::AbstractMaterial, strain, state::AbstractMaterialState)
frozen = FrozenStressMaterial(e -> calculate_current_stress(m, state, e))
σ, _, _, _ = MMB.material_response(stress_state, frozen, strain, MMB.NoMaterialState{eltype(F)}())
return σ
end

Comment thread src/CurrentStress.jl Outdated
struct FrozenStressMaterial{F} <: AbstractMaterial
f::F # F::Tensor{2,3} -> P::Tensor{2,3}
end
function MMB.material_response(fm::FrozenStressMaterial, F::Tensor{2,3}, old::MMB.AbstractMaterialState, args...)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function MMB.material_response(fm::FrozenStressMaterial, F::Tensor{2,3}, old::MMB.AbstractMaterialState, args...)
function MMB.material_response(fm::FrozenStressMaterial, F::SecondOrderTensor{3}, old::MMB.AbstractMaterialState, args::Vararg{N}) where {N}

Comment thread src/CurrentStress.jl Outdated
Comment on lines +38 to +43
# Generic fallback for genuinely stateless materials: since there is no history
# to accidentally advance, delegating to `material_response` is safe and exact.
function calculate_current_stress(m::AbstractMaterial, ϵ, state::MMB.NoMaterialState)
σ, _, _ = MMB.material_response(m, ϵ, state)
return σ
end

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be removed, since we want to special case to avoid calculating the gradient in this case.

Detailed change:
Addresses KnutAM's review feedback on PR KnutAM#13: a material-model developer
should only need to implement the full-dimensional
calculate_current_stress(m, ϵ, state), and ReducedStressState support should
then "work by itself" - this is intended to be upstreamed to
MaterialModelsBase.jl later.

- Added a generic reduced-dimensional fallback,
  calculate_current_stress(stress_state::AbstractStressState,
  m::AbstractMaterial, strain, state::AbstractMaterialState), which wraps the
  model's own full-dimensional calculate_current_stress in the existing
  FrozenStressMaterial helper and rides MaterialModelsBase's stress-state
  Newton iteration (e.g. PlaneStress), autodiff-ing through it for the
  tangent.
- Removed the now-redundant FiniteStrainPlastic-specific reduced method (no
  efficiency loss, since it already used FrozenStressMaterial + autodiff
  internally).
- Generalized FrozenStressMaterial's material_response from F::Tensor{2,3}
  to strain::SecondOrderTensor{3}, so the same wrapper serves both finite-
  and small-strain materials.
- As a consequence, GeneralizedMaxwell and RotatedMaterial wrapping a
  stateful small-strain material now get reduced-dimensional support "for
  free" (previously unsupported); added tests for both.
- Kept the reduced-dimensional NoMaterialState and Plastic-specific fast
  paths, since both avoid autodiff via a cheaper analytic alternative
  (Julia's dispatch picks the more specific method automatically, no
  ambiguity). Removed the now-unnecessary RotatedMaterial/ReducedStressState
  NoMaterialState disambiguation overloads, since the broad full-dimensional
  fallback they guarded against no longer exists (see next point).
- Removed the full-3d generic NoMaterialState fallback entirely, replacing
  it with two dedicated, gradient-free methods: LinearElastic
  (calculate_stress(m, ϵ), no autodiff at all) and AbstractHyperElastic
  (F ⋅ compute_stress(m, tdot(F)), one unavoidable gradient instead of the
  two that material_response/compute_stress_and_tangent would otherwise
  compute and discard).

Went through the dual-review workflow (new Codex thread). The plan review
caught a real issue with my initial reading of the repo owner's 3rd review
comment ("Can be removed, since we want to special case to avoid calculating
the gradient in this case", on the full-3d NoMaterialState fallback): I
initially assumed this was a misplaced GitHub comment anchor, since that
fallback computes no gradient directly. Codex correctly identified that it
computes one indirectly, for hyperelastic materials, and fixed the plan
accordingly (see above). The diff review after implementation reported no
further findings.

Test results: Pkg.test() passes (29 tests in the calculate_current_stress
testset, up from 27; full suite green). docs/make.jl builds cleanly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@knutambot

Copy link
Copy Markdown
Author

Addressed in 06f737d: added a generic calculate_current_stress(stress_state, m::AbstractMaterial, strain, state::AbstractMaterialState) fallback that wraps a model's own full-dimensional calculate_current_stress in FrozenStressMaterial and rides MaterialModelsBase's existing stress-state Newton iteration, autodiff-ing through it for the tangent — so a ReducedStressState now "just works" for any material once its full-dimensional method exists (the FiniteStrainPlastic-specific reduced method is gone, replaced by this).

Also removed the full-3d generic NoMaterialState fallback and replaced it with dedicated gradient-free methods for LinearElastic and AbstractHyperElasticmaterial_response was computing (and calculate_current_stress discarding) an unnecessary tangent for the hyperelastic models via compute_stress_and_tangent's second differentiation.

Bonus from the generalization: GeneralizedMaxwell and RotatedMaterial wrapping a stateful material now get ReducedStressState support for free too (previously unsupported) — added tests for both.

🤖 Generated with Claude Code

Detailed change:
The postprocessing interface prototyped in this repo (calculate_current_stress)
has been upstreamed into MaterialModelsBase.jl itself as stress_from_state
(KnutAM/MaterialModelsBase.jl#21, currently open, on branch
knutambot/MaterialModelsBase.jl#cb/calculate_current_stress). This keeps only
the material-specific implementations here, extending MMB.stress_from_state:

- src/CurrentStress.jl renamed to src/StressFromState.jl, trimmed to only:
  LinearElastic and AbstractHyperElastic (gradient-free NoMaterialState
  overrides - upstream's own generic NoMaterialState fallback deliberately
  discards a tangent for stateless materials, matching this repo's earlier
  design choice; a material-specific override remains the recommended,
  opt-in way to avoid that), Plastic (full-dimensional formula plus a
  reduced-dimensional fast path, both cheaper than upstream's
  autodiff-based generic fallback), GeneralizedMaxwell and FiniteStrainPlastic
  (full-dimensional only now - their reduced-dimensional support comes
  entirely from upstream's generic fallback, which uses its own
  FrozenStressMaterial), and RotatedMaterial (full-dimensional wrapper,
  keeping the generic + NoMaterialState-disambiguation two-method pattern,
  since upstream's own NoMaterialState fallback would otherwise be ambiguous
  against a single generic method here).
- Deleted: the local FrozenStressMaterial struct, the generic
  reduced-dimensional fallback, and ReducedStressState delegation - all now
  live in MaterialModelsBase.jl.
- Project.toml and docs/Project.toml: temporarily pin [sources] for
  MaterialModelsBase to the cb/calculate_current_stress branch, with an
  inline comment marking this temporary and noting to revert (and tighten
  [compat]) once MaterialModelsBase.jl#21 merges and releases.
- Renamed calculate_current_stress -> stress_from_state throughout (source,
  tests, docs); dropped the export line, since stress_from_state is not
  re-exported by this package, matching how material_response itself isn't
  re-exported either.
- docs/src/small_strains.md and docs/src/finite_strains.md: replaced the
  local @docs stress_from_state blocks with prose, since the canonical
  docstring now lives upstream and this package's
  makedocs(modules=[MechanicalMaterialModels]) call wouldn't surface it
  anyway.

Went through the dual-review workflow (new Codex thread). The plan review
caught 4 real issues before implementation: (1) RotatedMaterial becomes
ambiguous against upstream's own NoMaterialState fallback for a stateless
wrapped material - fixed by restoring the two-method disambiguation pattern;
(2) docs/Project.toml needs its own [sources] override too, since dependency
[sources] entries aren't inherited into sibling environments - fixed; (3)
removing the local docstring would leave @docs stress_from_state unable to
find any docstring, since makedocs filters docstrings to the listed modules
- fixed by using prose instead of @docs; (4) the unchanged [compat] range
("0.3, 0.4") doesn't protect downstream users once this merges for real -
documented as a follow-up requirement, since there's no real release to
reference yet. The diff review after implementation reported no further
findings (it noted one pre-existing, unrelated upstream dispatch ambiguity
in get_drdx for finite-strain GeneralStressState, not touched by or
affecting this change).

Test results: Pkg.test() passes against the upstream branch (29 tests in the
stress_from_state testset, full suite green). docs/make.jl builds cleanly
(only pre-existing, unrelated warnings).

Remaining risk: this branch temporarily depends on an unmerged,
force-pushable fork branch. If that branch is rewritten or deleted, this
repo's CI/tests will break until the [sources] override is reverted or
updated - explicitly acknowledged as intentional/temporary, per instruction,
not a bug. This must be reverted before merging MaterialModelsBase.jl#21.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@knutambot knutambot changed the title Add calculate_current_stress prototype for MaterialModelsBase.jl#12 Add stress_from_state material-specific implementations for MaterialModelsBase.jl#12 Sep 21, 2026
@knutambot

Copy link
Copy Markdown
Author

Migrated to build on top of the upstreamed interface: MaterialModelsBase.jl#21 now provides stress_from_state (the renamed, generalized version of what was prototyped here), including the generic machinery (FrozenStressMaterial, the generic reduced-dimensional fallback, ReducedStressState delegation).

This PR now keeps only the material-specific implementations, extending MaterialModelsBase.stress_from_state:

  • LinearElastic/AbstractHyperElastic: gradient-free NoMaterialState overrides.
  • Plastic: full + reduced-dim analytic fast path.
  • GeneralizedMaxwell, FiniteStrainPlastic: full-dimensional only — reduced-dim now comes for free from upstream's generic fallback.
  • RotatedMaterial: full-dimensional wrapper.

⚠️ Temporary, must revert before merging MMB#21: Project.toml and docs/Project.toml currently pin MaterialModelsBase via [sources] to knutambot/MaterialModelsBase.jl#cb/calculate_current_stress (unmerged), so this branch can build/test/doc against the new interface ahead of that PR merging, per your request. Both entries are commented # TEMPORARY. Once MMB#21 merges and releases, these need reverting to the registry-tracked package and the [compat] bound tightened to exclude older releases lacking stress_from_state.

Pkg.test() and docs/make.jl both pass against that branch.

🤖 Generated with Claude Code

Detailed change:
MaterialModelsBase's generic reduced-dimensional fallback (autodiff-ing
through the full-dimensional stress_from_state(m::Plastic, ϵ, state)) already
gives the same result as the removed Plastic-specific fast path: the
underlying formula, calculate_stress(m.elastic, ϵ - state.ϵp), is linear in
ϵ, so the autodiff-derived tangent used in the generic fallback's Newton
iteration is exact, same as m.elastic's own analytic stiffness. The dedicated
method was therefore unnecessary duplication.

Pkg.test() confirms all 29 stress_from_state tests still pass unchanged,
including the PlaneStress/PlaneStrain reduced-dimensional Plastic tests,
now exercising the generic fallback. docs/make.jl builds cleanly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread test/test_stress_from_state.jl Outdated
F1 = Tensor{2,3}((i, j) -> i == j ? (i == 1 ? 1.02 : 1.0) : 0.0)
P1, _, state1 = material_response(m, F1, state0, nothing)
@test calculate_current_stress(m, F1, state1) ≈ P1
@test state1.Fp != state0.Fp # sanity: this test only matters if plastic loading occurred

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@test state1.Fp != state0.Fp # sanity: this test only matters if plastic loading occurred
@assert state1.Fp ! state0.Fp # sanity: this test only matters if plastic loading occurred

Comment thread test/test_stress_from_state.jl Outdated
σ2_frozen = calculate_current_stress(m, F2, state1)
σ2_true, _, state2_true = material_response(m, F2, state1, nothing)
@test !(σ2_true ≈ σ2_frozen) # material_response would further evolve plastically
@test state2_true.Fp != state1.Fp

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@test state2_true.Fp != state1.Fp
@assert state2_true.Fp ! state1.Fp

…sserts

Detailed change:
Deletes src/StressFromState.jl and moves each material-specific
MaterialModelsBase.stress_from_state method into its own model's existing
source file, alongside its other AbstractMaterial interface implementations
(material_response, initial_material_state, etc.): LinearElastic ->
src/Elastic.jl, AbstractHyperElastic -> src/hyper_elasticity/HyperElastic.jl,
Plastic -> src/Plastic.jl, GeneralizedMaxwell -> src/ViscoElastic.jl,
FiniteStrainPlastic -> src/FiniteStrainPlastic.jl, RotatedMaterial (both
methods plus the shared rotation helper) -> src/RotatedMaterial.jl. No
behavioral change - same method bodies and signatures, just relocated, since
implementing stress_from_state is part of the AbstractMaterial interface
each model already implements, not a separate cross-cutting concern.

Also addresses 2 minor review comments on test/test_stress_from_state.jl:
changed two "sanity" checks (verifying the test setup itself produced
plastic loading, not the feature under test) from @test x != y to
@Assert x ≉ y. @Assert because a failure there means the test data is
malformed, not a feature regression, so it shouldn't count toward the test
summary; approximate inequality (not exact) since these compare
floating-point tensors. The reviewer's literal suggested text used `!≈`,
which is not valid Julia syntax (confirmed by a ParseError); used the
correct, semantically equivalent operator `≉` (Base's negation of `≈`)
instead.

Went through the dual-review workflow (new Codex thread); both the plan
review and the diff review after implementation reported no findings.

Test results: Pkg.test() passes (27 tests in the stress_from_state testset,
down from 29 as expected since two @tests became @asserts; full suite
green). docs/make.jl builds cleanly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants