Repository navigation
Conversation
An output declared with constant_output(name, {reads}) depends only on the
parameters and those inputs. At build the engine marks it constant when
every read is a global edge from a constant property; a callback is run
once when the outputs of its material without a callback of their own are
constant too. set_parameter() and get_mutable() bump a revision counter,
after which the constants are recomputed on the next update.
Producers declare their pure outputs: constant_scalar, poisson_ratio_from_moduli,
linear_elasticity (tangent), isotropic_tangent, isotropic_moduli, the Eshelby
and depolarisation tensors, phase (not over a local edge), scalar_split,
weighted_sum (tangent) and mean_field_composite_global (tangent and
concentrations). The global mean-field composite of constant phases runs its
kernel once.
…erified in debug builds constant_output() takes the value add_output() returned instead of the property name; the address identifies the property, and a value that is not an output of the material throws at construction. Without a reads list the output reads all inputs of the material; a listed read must be one of them. Debug builds rerun the skipped constant callbacks on every update and throw when a constant output changes (ctx.verify_constants(bool) switches it).
written_by(output, writer) names the callback that fills an output without its own; runs_after(output, earlier) orders two callbacks of a material. A callback-less output is now ordered after its writer, or after every callback of its material when none is declared: before, a consumer could read it before the callback that writes it had run (mean_field_strain's strain_<inclusion>, depending on the hash order). A constant callback is skipped when every output it may write is constant. mean_field_strain computes its concentrations in their own callback, declared constant over the phase reads, and the strains after them: constant phases localise with the concentrations computed once.
add_callback(&callback, outputs...) binds a callback to several outputs (references, pointers with null ones skipped, or ranges of pointers) and returns a handle that declares the group constant and orders it after another group; it replaces binding the first output and naming the others written_by it. constant() skips null reads (an absent optional input). mean_field_strain, scalar_split, isotropic_moduli, phase and mean_field_composite_global use it.
The callback writes all outputs, so it is skipped only when all are constant; the group declared with all inputs as reads says exactly that: constant unless the tangent comes over a local edge.
Each output and the input it republishes are one relay; input(key) reads "<key>_source" and "<key>_property" and is null without a source, so the optional Eshelby and depolarisation tensors need no branches and the edge of the tangent comes from its input. Parameters are unchanged; an empty tangent_source or fraction_source now fails at construction instead of at finalize().
This was referenced Oct 21, 2026
Member
Author
|
Superseded by a split into three stacked PRs with the same final code (the tree of #165 is identical to 9ab90c0):
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Outputs that depend only on parameters, and on inputs that are themselves constant, are now computed once instead of on every update. The global mean-field composite of constant phases therefore runs its kernel once: per update, Mori–Tanaka goes from 7.0 to 0.09 µs and self-consistent from 12.6 to 0.5 µs, with identical results. Stacked on #159.
Why
property_engine::update()ran every callback on every update. The global mean-field composite reads its phase tangents, fractions and Eshelby tensors over global edges. It could not know they never change, so it ran its kernel on every update: a full fixed-point iteration for self-consistent, with an Eshelby evaluation per step. The same happened one level down for elastic tangents built from moduli and for the Eshelby tensors.Constness cannot be inferred from the graph, for two reasons:
linear_elasticity::tangentappears to depend on the strain.That is why producers declare it, per output.
What
material_base::constant_output(output[, {reads}]): declares that the output is a function of the parameters and ofreadsonly.add_outputreturned. Its address identifies the property, which the registry holds behind aunique_ptr. A value that is not an output of the material (a parameter, a local) throws at construction. There is no name to misspell and no lookup at build.phase,weighted_sum,linear_elasticitywith{}), and every listed read must be an input of the material.NDEBUGthe engine reruns the skipped callbacks on every update and throwslogic_errornaming the output when a constant output changes (relative tolerance √ε): a declaration that hides state or a read becomes a test failure.ctx.verify_constants(bool)switches it; tests that count calls turn it off.add_callback(&callback, outputs...): one callback filling a group of outputs (references, pointers with null ones skipped, or ranges of pointers). The returned handle declares the group.constant([reads])and orders it.runs_after(other). Null reads are skipped, so an absent optional input needs no filtering.mean_field_strain,scalar_split,isotropic_moduli,phaseand the global composite use it:written_by(output, writer)/runs_after(output, earlier): the layer underneathadd_callback: which callback fills an output that has no callback of its own, and the order of two callbacks of one material.The engine at build:
mean_field_strain'sstrain_<inclusion>ran afterstiff::stressin some hash orders. The extra edges cannot close a cycle: the material's global inputs already reach every output, so any path from the output back to its writer would already be a cycle.Recomputation:
set_parameter()andget_mutable()bump a revision counter. Its address is stable: it is heap-held and bound to the materials atfinalize().update()orupdate_property()reruns the constants.update_property()computes a constant that has not run yet.Producers declaring their pure outputs:
constant_scalarvaluepoisson_ratio_from_modulivaluelinear_elasticitytangenttensorisotropic_tangenttangentisotropic_modulitensorphase(one group: not constant when the tangent comes over a local edge, the previous pass's)scalar_splitweighted_sumtangentmean_field_composite_globalmean_field_strain: concentrations and strains are separate callbacks. The concentrations are declared constant over the phase reads, and the strains run after them. With constant phases the concentrations are computed once; a phase read over a local edge (nonlinear) recomputes them every pass.phasesimplified: each output and the input it republishes form arelay, andinput(key)reads the<key>_source/<key>_propertypair (null without a source). The optional Eshelby and depolarisation tensors need no branches, and the edge kind of the tangent comes from its input. The parameters are unchanged. An emptytangent_sourceorfraction_sourcenow fails at construction with a clear message instead of as a wiring error atfinalize().Not constant, as before:
dump()marks constant properties. README anddocs/mean_field.mddescribe the rule.Measured
Global composite: matrix plus aligned spheroids (aspect ratio 5, c = 0.2),
linear_stresson top, 2000 updates with a changing strain,-O2.Local localisation: the same phases through
mean_field_strain,linear_stressper phase andmean_field_composite_local_linear, 2000 committed steps,-O2 -DNDEBUG.Tests
test_constant_properties(new), with a counting test material:set_parameter()recomputes the constants (once, then they are skipped again);get_mutable()write recomputes them;update_property()computes a constant that has not run yet;runs_after);runs_afteris rejected;add_callback: a constant group runs once with its outputs ordered after it, a null output is left out, an output that has a callback already and an empty group throw.Phase.*(3 tests, written before the refactor): inconsistent parameters (initial tangent without a local edge, unknown edge, empty source, Eshelby in 2D), a negative fraction, and which tensors are relayed. Against the old code only the empty-source checks failed.MeanFieldLocal.ConstantPhasesAreLocalisedWithConcentrationsComputedOnce: a counting Mori–Tanaka kernel is called once over five committed steps, with stresses and strains equal to plain Mori–Tanaka. It failed before the split; the new API tests failed to compile.MeanField.ConstantPhasesAreHomogenisedOnce:kernel_registry::add, is called once over five updates with a changing strain;constant_outputdid not exist.Gates (GCC 14, fetched dependencies):
NDEBUG, so the whole suite ran with verification on: none of the declarations above changes on a rerun (self-consistent included).docs/materials/*/report.jsonchecks pass with it.Notes
test_mean_field_localandtest_short_fibre_compositewere repeated 10× each, all passing.