Skip to content

History codecs: storage and rotation of a history, type-erased at declaration (#216) - #224

Open
petlenz wants to merge 5 commits into
mainfrom
feature/issue-216
Open

petlenz wants to merge 5 commits into
mainfrom
feature/issue-216

Conversation

@petlenz

@petlenz petlenz commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Implements #216 (part of epic #221). No other PR has to merge first.

What and why

On main, statev_map knew three history shapes: a scalar, a symmetric rank-2 tensor stored like a strain (plus stress_like from #91), and a minor-symmetric rank-4 tensor. It rotated every tensor entry under DROT. Reference-configuration history (Cp, Fp), non-symmetric tensors (Fe), vectors and symmetric non-strains (back stress, conductivity) had no correct storage.

This PR adds history codecs. When a history is declared, both its value type and the way a host stores and rotates it are known. A type-erased handle records them once, at that point. After that, statev_map only loops over the handles. It needs no dynamic_cast, no switch and no kind enum, and scalars, rank-4 tensors and user types all take the same path.

core/history_codec.h (new, host-neutral)

A codec is a plain struct that says how one value type sits in a flat buffer and how a rotation R acts on it. It has width, read(v, buffer) and write(v, buffer, who), plus an optional tag for describe() and an optional rotate(v, R). A codec without rotate is never rotated.

codec value slots describe() tag DROT
scalar value_type 1 none none
strain tensor2 6, engineering shear [engineering shear] R·a·Rᵀ
stress tensor2 6, shear as is [stress] R·a·Rᵀ
reference_strain tensor2 6, engineering shear [engineering shear] [reference] none
reference_tensor tensor2 9, row-major [full] [reference] none
two_point tensor2 9, row-major [full] [two-point] R·a
vector tensor1 3 [vector] R·v
reference_vector tensor1 3 [vector] [reference] none
moduli tensor4 36 none all four legs
  • strain : reference_strain, two_point : reference_tensor and vector : reference_vector: each spatial codec is its reference codec plus rotate(). stress also covers spatial moduli such as a conductivity.
  • history_codec_for<C, V, T> is the concept: C stores a V in a T buffer (width, read, write). A codec that does not fit the value is a compile error, e.g. reference_strain on a vector.
  • default_codec<V> is the codec a history gets when it is declared without one: scalar for an arithmetic type, vector for tensor<T,3,1>, strain for tensor<T,3,2>, moduli for tensor<T,3,4>. For any other type it is void.
  • require_symmetric and require_minor_symmetric moved here from statev_map, with the same messages. They name the history (owner::name). require_minor_symmetric now reuses is_minor_symmetric.

core/history_slot.h (new): the type-erased handle

history_slot<T> uses the usual concept/model erasure. It holds a unique_ptr to a model<V, Codec>, which keeps a reference to the history_property<V>. It provides:

  • width();
  • unpack(), which sets the old and the new side;
  • pack(), which writes the new side;
  • rotate(R), which turns both sides and does nothing when the codec has no rotate;
  • rotates() and tag().

Declaration

  • material_interface::add_history<T>(name, data) records a slot with default_codec<T> if that codec fits the context's value_type.
  • A new overload, add_history<T>(name, data, Codec), is constrained by history_codec_for and records the given codec instead.
  • material_base::add_history_output<T>(name, Codec, initial = T{}) sits next to the existing (name, initial) overload and has the same constraint.
  • history_slot_of(property) returns the recorded slot, or null.
  • No code creates a history outside add_history (checked with grep).

Example: add_history_output<tensor2>("back_stress", codecs::stress{}).

umat/statev_map.h

  • An entry is {owner, name, offset, slot*}. add_entry asks the owning material for the slot.
  • A history without a slot throws the existing fatal_error, which names the property: "has a type that cannot be stored in STATEV — declare it with a codec (core/history_codec.h)". Two cases lead here: a type with no default codec that was declared without one, or an int history in a double buffer.
  • unpack, pack, rotate_history, has_rotatable_history, describe, nstatv and check_nstatv keep their public behaviour.
  • For strain, stress, scalar and rank-4 history, the STATEV layout, widths and describe() strings are the same as on main. Two shipped materials now declare codecs::stress (see Shipped materials), so their shear slots change.

Layering: core/voigt_buffer.h (new, host-neutral)

These functions moved from umat/tensor_conversion.h into numsim::materials, unchanged apart from the namespace:

  • canonical_width and the shear-convention notes;
  • strain_*_buffer, stress_*_buffer and tangent_*_buffer;
  • is_minor_symmetric;
  • full_*_buffer, which is new in this PR.

tensor_conversion.h includes the new header and re-exports these names into umat with using declarations. Every existing spelling (umat::strain_to_buffer, unqualified uses inside umat) compiles unchanged.

Shipped materials

Every history declared in include/ was checked. Three of them need a codec other than their type's default, and now declare it:

material history codec before
j2_kinematic_plasticity back_stress codecs::stress strain storage, shear doubled (2β₁₂)
incremental_linear_elasticity stress codecs::stress strain storage, shear doubled (2σ₁₂)
external_deformation_gradient_source deformation_gradient codecs::two_point strain storage: 6 slots, and pack threw on a sheared F

Everything else already gets the right codec by default:

  • plastic strains (j2_plasticity, j2_rk_plasticity, j2_kinematic_plasticity, drucker_prager_plasticity), the phase strains of mean_field_strain and external_strain_source::strain use strain;
  • every scalar history uses scalar. That covers the equivalent plastic strains, kappa, the curing and RK states and the scalar sources.

STATEV change for restart files. For the first two materials, the shear slots (4–6 of the entry) now hold β₁₂ and σ₁₂ instead of twice those values. Widths, offsets and rotation are unchanged. Only the stored shear values differ, so an old restart file of these two materials reads back with doubled shear stress. The deformation gradient source is excluded from STATEV under a host, so no host layout changes for it.

docs/plasticity.md and the j2_kinematic_plasticity header no longer say "until #91 lands".

Open, outside this PR: the history unknowns of system (#229) take the default codec, strain for a tensor. A stress-like unknown such as a back stress would need a codec field on system_unknown. Whichever of #224 and #229 merges second should add that field.

Changes since the previous revision (c149e55) and why

The maintainer found the kind design still too complex. It had legs × storage combinations, valid_for, rotates, used, to_string, history_kind_rank and default_history_kind, a slot class hierarchy inside statev_map, and a four-way dynamic_cast.

  • Removed:
    • core/history_kind.h and every use of it;
    • history_kind_of and the slot hierarchy in statev_map, with all its dynamic_casts;
    • the runtime "invalid kind" logic_error. A codec that does not fit is now a compile error.
  • Dropped: the material_moduli and spatial_moduli kinds. stress covers spatial moduli, and a reference-frame modulus can wait for a model that needs one.
  • Unchanged: core/stress_like.h stays removed. The STATEV: stress-like tensor history without the engineering-shear factor (#63) #91 probe now declares add_history_output<tensor2>("back_stress", codecs::stress{}).

Decisions and deviations from the brief

  • Rotation reuses core/elastic_constants.h. The codecs call the existing numsim::materials::rotate (rank 2: R·a·Rᵀ; rank 4: written with tmech otimesu/dcontract) instead of moving umat::rotate to core. Moving it would have put a second, overlapping rotate overload set into numsim::materials. umat::rotate stays where it was. The rank-4 result is the same rotation that main applies, and StatevMap.RotatesRankFourHistory passes unchanged.
  • The concept takes the buffer type: history_codec_for<Codec, V, T>. A user value such as a struct has no scalar type to infer the buffer from. The extra parameter also makes int history fail the scalar codec for a double buffer.
  • history_slot takes who from the property (traits().id), not as a constructor argument, so the name has one source.
  • The slots are stored in an unordered_map keyed by the property. This lets the codec overload simply replace the default slot. Pointers to the map's values stay valid.
  • Expected conflict with Partial derivatives: per-callback declarations and forward accumulation (#217) #227/Property engine: residual systems set up at run time (#218) #229: two added lines inside material_interface::add_history (the default-codec record), next to lines those PRs also change.
  • tag is optional like rotate, so scalar, moduli and a user codec can leave it out.

Tests

tests/test_history_codecs.cpp replaces test_history_kinds.cpp. It has 9 tests and compile-time checks:

  • static_assert:
    • history_codec_for<strain, tensor2> holds;
    • reference_strain on a vector and scalar on an int do not fit;
    • default_codec is scalar for value_type, vector for tensor1, strain for tensor2 and moduli for tensor4, and void for std::string.
  • Widths and describe() for all nine codecs in one probe.
  • DROT, old and new sides:
    • strain, stress, vector, two_point (R·F) and moduli are rotated;
    • reference_strain, reference_tensor, reference_vector and scalar come back unchanged.
  • reference_tensor and two_point keep all 9 non-symmetric components through pack and unpack, and a vector keeps all 3 (raw slots checked).
  • stress stores D₁₂ next to strain and reference_strain, which store 2ε₁₂.
  • Each symmetric codec rejects a skew part on pack, and the message names the property.
  • has_rotatable_history() is false when the only history is reference and scalar.
  • A consumer reads a reference_strain history through a plain tensor2 input.
  • A user-defined codec on a user struct (a direction that turns and a length that does not) round-trips through statev_map, with its own tag and rotation.
  • An int history (no codec for a double buffer) throws the statev_map fatal_error, which names it and mentions the codec.

One test per ported material, through statev_map:

  • J2Kinematic.BackStressIsStoredAsAStress: the entry is [stress], and slot 4 holds β₁₂.
  • IncrementalElasticity.StressIsStoredAsAStress: the entry is [stress], and slot 4 holds σ₁₂.
  • ExternalDeformationGradientSource.IsStoredAsATwoPointTensor: 9 slots, [full] [two-point], and a sheared F round-trips.

Each failed before its port. The first two showed [engineering shear]; the third showed 0..5 … [engineering shear] instead of 9 slots.

All existing tests pass unchanged: test_statev_map.cpp, the UMAT, CalculiX and evaluator tests, and #91's StressLikeHistoryKeepsItsShearComponentsForTheHost after the one-line migration.

Gate

GCC 14 gate at -O1, including the ASan/UBSan test targets: BUILD=0, 1510/1510 tests passed, 0 warnings (a6c4764).

core/tensor_kind.h grows stress_like into kinded<T, voigt, leg...> with the
aliases for reference strains, two-point and material tensors, vectors and
moduli, and rotated() acting on spatial legs only. statev_map builds every
kinded entry with one builder: 3/9 full slots for voigt::none, 6 otherwise,
a rotate step only when a leg is spatial, and describe() marks per kind.
Plain tensor2 history keeps its engineering, spatial meaning.
@petlenz

petlenz commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Merged main (#222, #223, #238) into this branch in 4c7caba; no conflicts. Local gate (g++-14, -O1): 1504 tests passed, 0 warnings. CI green on all four jobs, including the new ASan jobs.

The value of a history stays a plain tmech tensor; how a host stores and
rotates it is chosen when the history is declared.

- core/history_kind.h replaces core/tensor_kind.h: leg, storage
  {engineering, plain, full}, history_kind {store, legs} with valid_for,
  rotates, rotated (Q = R on a spatial leg, I on a material one) and tags;
  the named kinds in history_kinds and default_history_kind<V>.
- add_history_output<T>(name, kind, initial): an invalid kind throws
  logic_error at declaration; the material records the kind and
  history_kind_of() returns it.
- statev_map entries are slot objects (scalar, tensor_slot<1|2>, rank 4);
  it casts by value type only and takes the kind from the owning material,
  else default_history_kind. STATEV layout and describe() are unchanged.
- core/stress_like.h removed; the #91 probe declares
  history_kinds::stress_like.
@petlenz

petlenz commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Rework in c149e55. As the maintainer asked, the history kind is now given when the history is declared, instead of being carried by the value type. The description above has been updated to match.

  • core/history_kind.h replaces core/tensor_kind.h.
    • It defines leg, storage {engineering, plain, full} and history_kind {store, legs}.
    • history_kind provides valid_for(rank), rotates(rank), rotated(a, R) and tags(rank). rotated is one expression: Q·a, or Q₀·a·Q₁ᵀ, with Q = R on a spatial leg and I on a material one.
    • The issue's named kinds are in history_kinds, and default_history_kind<V> gives the kind of a history declared without one.
  • Declaration. add_history_output<T>(name, kind, initial) is a new overload; existing call sites are unchanged. An invalid kind throws std::logic_error at declaration, naming material::property and the kind: a kind on a scalar, engineering or plain storage on a vector, or six-slot storage of a two-point tensor. The material records the kind, and history_kind_of() returns it. The edits to material_interface.h and material_base.h are one block each, because of Property engine: iteration groups converge local edges inside update() (#214) #226, Partial derivatives: per-callback declarations and forward accumulation (#217) #227 and Property engine: residual systems set up at run time (#218) #229.
  • statev_map. Entries are now slot objects: scalar, tensor_slot<1|2> (holds the kind) and rank 4. The map casts by value type only and takes the kind from the owning material, or default_history_kind if none was recorded. try_kind, add_kinded, kind_tags and the 10-way cast list are gone. The STATEV layout, slot order, widths and describe() strings are the same as in the previous version.
  • Removed: core/stress_like.h and kinded. stress_like has no users outside this repository. The STATEV: stress-like tensor history without the engineering-shear factor (#63) #91 probe now declares add_history_output<tensor2>("back_stress", history_kinds::stress_like).
  • Tests. The probes are now plain tensors declared with kinds, and every earlier behaviour test is kept. The two TensorKind tests are replaced by HistoryKind tests of rotated() (each leg combination, at rank 1 and rank 2), valid_for/rotates, and the named and default kinds. New tests cover:
    • the error for an invalid kind at declaration;
    • a consumer reading a reference_strain history through a plain tensor2 input;
    • the default kinds: a tensor2 declared without a kind is [engineering shear], a vector is [vector], and both are rotated.
  • Gate: GCC 14 at -O1, BUILD=0, 1512/1512 tests passed, 0 warnings.

@petlenz petlenz changed the title History kinds: kinded<T, voigt, leg...> for rotation and STATEV storage History kinds: storage and rotation of a history given at declaration (#216) Oct 30, 2026
Replace the history kind (legs x storage) by codecs: plain structs that
say how one value type sits in a flat buffer and how a rotation acts on
it. add_history records a history_slot (concept/model erasure) with the
given codec, or default_codec<T>; statev_map only loops over the slots,
without dynamic_cast or kind enum.

- core/history_codec.h: scalar, strain, stress, reference_strain,
  reference_tensor, two_point, vector, reference_vector, moduli; the
  history_codec_for concept; default_codec; require_symmetric and
  require_minor_symmetric moved from statev_map.
- core/history_slot.h: the type-erased handle.
- core/voigt_buffer.h: the buffer conversions moved from
  umat/tensor_conversion.h, which re-exports them.
- material_interface/material_base: add_history and add_history_output
  overloads taking a codec; history_slot_of().
- core/history_kind.h removed; the #91 probe uses codecs::stress.
- tests/test_history_codecs.cpp replaces test_history_kinds.cpp.
@petlenz petlenz changed the title History kinds: storage and rotation of a history given at declaration (#216) History codecs: storage and rotation of a history, type-erased at declaration (#216) Oct 30, 2026
@petlenz

petlenz commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Round 2: history codecs, type-erased at declaration (43f9ed7).

The kind design of c149e55 is replaced. Where a history is declared, its value type and its codec (how it is stored in STATEV and how DROT acts on it) are captured once in a history_slot. statev_map only loops over these slots.

  • Removed:
    • core/history_kind.h, with its legs × storage combinations and valid_for/rotates/used/to_string/history_kind_rank/default_history_kind;
    • history_kind_of, the slot hierarchy and the four-way dynamic_cast in statev_map;
    • the material_moduli and spatial_moduli kinds (stress covers spatial moduli).
  • Added:
    • core/history_codec.h: nine plain-struct codecs, the concept history_codec_for<Codec, V, T>, default_codec<V>, and require_symmetric/require_minor_symmetric moved here from statev_map;
    • core/history_slot.h: about 70 lines of concept/model erasure;
    • core/voigt_buffer.h: the buffer conversions, moved verbatim from umat/tensor_conversion.h, which re-exports them.
  • Declaration: add_history_output<T>(name, Codec{}, initial); without a codec, default_codec<T>. A codec that does not fit the value is a compile error.
  • STATEV: the layout, widths and describe() strings for strain, stress, scalar and rank-4 history are as on main.
  • Deviations from the brief, all for simplicity:
    • the codecs reuse numsim::materials::rotate from core/elastic_constants.h instead of moving umat::rotate, which would have added a second, overlapping overload set to core;
    • the concept takes the buffer type as a third parameter;
    • history_slot takes who from the property's id;
    • the slots live in an unordered_map, so a declared codec replaces the default one.
  • Tests: test_history_codecs.cpp replaces test_history_kinds.cpp, with 9 tests plus static_asserts. The STATEV: stress-like tensor history without the engineering-shear factor (#63) #91 probe migrated to codecs::stress{}.
  • Gate: BUILD=0, 1504/1504 tests passed, 0 warnings (GCC 14, -O1, ASan/UBSan targets included).
  • Expected conflict: two added lines inside material_interface::add_history, where Partial derivatives: per-callback declarations and forward accumulation (#217) #227/Property engine: residual systems set up at run time (#218) #229 also change the code.

j2_kinematic_plasticity::back_stress and incremental_linear_elasticity::stress
are stress-like and now use codecs::stress: STATEV holds their shear as is
instead of doubled. external_deformation_gradient_source::deformation_gradient
uses codecs::two_point: all nine components, rotated as R F. Every other
shipped history already takes the right default codec.
@petlenz

petlenz commented Nov 2, 2026

Copy link
Copy Markdown
Member Author

a6c4764 sets explicit codecs on the shipped materials' histories. I checked every history declared in include/, and three needed a codec other than their type's default:

  • j2_kinematic_plasticity::back_stress → codecs::stress
  • incremental_linear_elasticity::stress → codecs::stress
  • external_deformation_gradient_source::deformation_gradient → codecs::two_point
    • Before this change it got 6 slots, and pack threw on a sheared F.
    • It is excluded from STATEV under a host, so no host layout changes.

STATEV change for the first two. Their shear slots now hold β₁₂ and σ₁₂ instead of twice those values. Widths and offsets are the same, but old restart files of these two materials read back with doubled shear.

All other shipped histories already get the right default codec: the plastic strains and phase strains use strain, and the scalars use scalar.

There is one new test per material. Each was red before its port. Gate: 1510/1510 tests, 0 warnings. CI: 4/4 green.

Follow-up: the history unknowns of system (#229) take the default codec. A stress-like unknown needs a codec field on system_unknown, which belongs in whichever of #224 and #229 merges second.

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.

1 participant