Skip to content

History checkpoints within a step and cutbacks in the report loop - #225

Open
petlenz wants to merge 3 commits into
mainfrom
feature/issue-215
Open

petlenz wants to merge 3 commits into
mainfrom
feature/issue-215

Conversation

@petlenz

@petlenz petlenz commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Closes #215 (part of #221, #184 phase 2: checkpoints and cutbacks). Independent of the other epic PRs; it can merge in any order.

What and why

commit()/revert() give history two levels, enough for one Newton step but not for substepping: after two accepted substeps revert() cannot go back to tₙ. This adds a checkpoint within a step and uses it for cutbacks in the report loop.

Engine (core/property_engine.h)

  • history_checkpoint<Traits>: the serialized bytes of every history property, by position in the engine's history list, plus the owning engine. No keys, no lookups; property_base::serialize()/deserialize() carry both values, so numsim-core is unchanged.
  • checkpoint() const and restore(history_checkpoint const&). Restore brings old and new back exactly as they were and clears the failed state like revert(). A checkpoint of another context (or a default-constructed one), or one with a different number of history properties, throws std::logic_error. Only history properties are saved; state a material keeps outside them across calls is not restored.
  • The change is two new methods and one new class next to revert(); nothing existing is touched (keeps the overlap with Property engine: keep the typed edges, export the graph (DOT/JSON), name the cycle #213 small).

Forwarding: material_context::checkpoint()/restore() (with the finalize() gate, checkpoint_type alias) and material_point::checkpoint()/restore().

Report loop (report/case_runner.h)

  • A case may set "max_cutbacks" (default 0, 0 ... 20; cut_step rejects other values with std::invalid_argument). A step that does not converge, or whose evaluation throws a std::runtime_error (umat::cutback_error and the return-map failures of the materials, as UMAT treats them), goes back to the checkpoint taken at its start and runs again in 2, 4, ... substeps; every substep but the last is accepted. At the last level the failure is recorded (or rethrown) exactly as today.
  • load_path::substeps(step, n) splits a step from the end of the step before (along its segment); the last substep ends exactly at the step's load and fraction, and time and dt are split too.
  • step_record gets cutbacks and substeps (the Newton summary of each substep at the final level; result stays the last one). The CSV format is unchanged.

Decisions / deviations from the issue

  • max_cutbacks lives on the case (load_case, parsed in report_json.h, next to tangent_every and fd_step), not in report_spec.h: that header only merges includes and substitutes parameters, it has no spec struct. Studies inherit it from their case.
  • With max_cutbacks = 0 the loop takes the old path (no checkpoint is taken), so reports without the key run exactly as before.
  • Cutbacks trigger on a driver step that does not converge as well as on any std::runtime_error, matching UMAT (cutback_error is one; so are the Newton/return-map failures). umat::fatal_error and std::logic_error propagate without a retry.
  • checkpoint() on a context whose last update threw throws: a partly updated state is no point to come back to.
  • The CSV gets no substep column, so that existing CSVs stay byte-identical; step_record carries the substeps for sinks that want them.

Tests

  • test_history_checkpoint (new): bitwise restore after two committed steps with an int history (int_counter) and a rank-4 history (compared through save_state bytes, old and new); a continuation after restore matches the uninterrupted run; a failed update followed by restore() updates again; a foreign or empty checkpoint throws and leaves the context unchanged; finalize() gate; material_point forwarding.
  • test_report_case: load_path::substeps (segment crossing, unloaded start, one substep = the step bitwise); max_cutbacks parsing (default 0, negative/fraction/above 20 throw); an exponential-damage softening case under uniaxial stress (point max_iter 5) that fails at the full increment with max_cutbacks 0 and 1 and converges with 2, its result bitwise equal to the case run in four steps; max_cutbacks = 0 and the default give the same CSV, and cutbacks a case does not need change nothing; a cutback_error or runtime_error thrown by a material (a fixture limiting the strain increment) is rethrown without cutbacks and cut away with one; logic_error and fatal_error are thrown once and not retried; max_cutbacks -1, 21 and 40 throw invalid_argument; a step failing after accepted substeps returns to its start (history counter); finite-strain cutback; tangent check combined with cutbacks.
  • All docs/materials/*/report.json CSVs (14 reports) are byte-identical to main.

Gate

GCC 14 + ASan: BUILD=0 TEST=0, 0 warnings, 1474 tests (main: 1440).

property_engine::checkpoint()/restore() snapshot the serialized history
by position in the engine's history list; restore() returns old and new
values exactly, after any number of commits, clears a failed update like
revert() and refuses a checkpoint of another context. material_context
and material_point forward both.

A report case may set max_cutbacks (default 0, unchanged behaviour): a
step that does not converge or throws a cutback_error returns to the
checkpoint at its start and runs again in 2, 4, ... substeps. The
substeps are recorded in step_record; the CSV format is unchanged.
A report step is retried in more substeps on any std::runtime_error, as
UMAT does: the return maps throw runtime_error, not only cutback_error.
fatal_error and logic_error are rethrown without a retry. cut_step
rejects max_cutbacks outside 0 ... 20 (a negative value never reached
the last level and 1 << cut overflowed). restore() checks the number of
history properties and keeps the context marked failed until every one
is back. checkpoint() documents that only history properties are saved.
@petlenz

petlenz commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

Review fixes, commit 4665616.

  1. [major] The report loop caught only cutback_error. cut_step now rethrows umat::fatal_error, then retries on any std::runtime_error (covers cutback_error, the return-map/Newton failures and third-party materials, as UMAT does) unless it is the last level. std::logic_error is not caught. Tests: a runtime_error on large increments is cut back and succeeds (and is rethrown at max_cutbacks = 0); a logic_error and a fatal_error are thrown exactly once (throw counter), no retry. The PR body is updated accordingly.
  2. cut_step validates 0 <= max_cutbacks <= 20 and throws std::invalid_argument (the JSON reader already limited it; direct load_case users now get the same bound). Test: -1, 21 and 40.
  3. restore() throws std::logic_error if the checkpoint holds a different number of history properties than the engine, and the context stays marked failed while the loop runs, so a throw during deserialization leaves it failed and m_failed is cleared only after every property is back. The size mismatch is not constructible from a test (the bytes are private to the engine and a checkpoint of another engine is already refused by the owner check), so that check has no test.
  4. checkpoint() doc: only history properties are saved; state a material keeps outside them across calls is not restored.
  5. Test gaps closed: finite-strain path with max_cutbacks (F_11 ends at 1.004 in two substeps); a step failing at the second substep after the first was accepted (reaches level 2, a history counter proves the accepted substep was undone, columns equal the same case run in four steps); tangent_every = 1 with cutbacks (every cut step has a finite tangent error below 1e-5).

Red before the fix (gate fix225red, 1474 tests, 10 failed counting the ASan twins): CaseRunner.ARuntimeErrorCutsTheStepLikeInUmat ("C++ exception with description "increment too large" thrown in the test body"), CutbacksNeedBetweenZeroAndTwentyOfThem ("Expected: ... throws an exception of type std::invalid_argument. Actual: it throws nothing." for -1, 21, 40), and the same runtime_error escape in AStepThatFailsAfterAcceptedSubstepsReturnsToItsStart, TheTangentIsCheckedAfterACutStep, AFiniteStrainStepIsCutBackToo.

Gate after: BUILD=0 TEST=0, 0 warnings, 1474 tests (GCC 14 + ASan).

Design remarks, no code change:

  • Cutbacks are invisible in the CSV and the iteration output; only step_record carries cutbacks and substeps. A sink that wants them has to read the record.
  • refinement_study inherits max_cutbacks from its case, so a refined run can mix step refinement with internal cutbacks; a convergence study may want max_cutbacks: 0 to keep the step size meaningful.

# Conflicts:
#	include/numsim-materials/core/property_engine.h
@petlenz

petlenz commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Merged main (#222, #223) into this branch.

One conflict, in core/property_engine.h: this PR's history_checkpoint and #223's graph types (edge_kind, graph_node, property_graph) were added at the same place. Both are kept, the graph types first.

Gate: g++-14 -O1, 1520 tests, 0 warnings.

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.

History checkpoints within a step and cutbacks in the report loop

1 participant