Repository navigation
Conversation
…on (#217) Callbacks declare partials() / independent_of() next to reads(); the engine plans a requested derivative over the target's upstream and accumulates the Mandel-packed partials. Hosts request derivatives via material_context and material_point. check_partials() verifies declared partials by central differences. thermal_eigenstrain and linear_elasticity declare their partials, giving the first DDSDDT.
This was referenced Oct 28, 2026
With the partials requested, a rerun at a perturbed read refilled the blocks, so a later partial of the same callback was compared at the perturbed state. Found while building #218.
Member
Author
|
Pushed a fix to |
…, fill at once check_partials perturbs every read declared independent_of() and requires the outputs of the callback to stay put; its step follows the read block, its roundoff allowance is bounded by the analytic block, and the reads and outputs are restored when a perturbed rerun throws. derivative<R> accepts only the scalar or a tensor of the context's dimension, refuses calls from a callback, builds its message on the error path only, and on an evaluated context reruns the callbacks of the new plan so the result is filled at once. has_path() tells independence from a typo.
Member
Author
|
Review findings, addressed in f7af4ae (gate: GCC 14, -O1, 0 warnings, 1524/1524 tests).
Design remark: |
This was referenced Oct 29, 2026
Member
Author
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.
Closes #217 (epic #221). Merge after #223 (
feature/graph-export): this branch is stacked on it and uses its typed edges and theupstream_of()cache.What and why
Partial derivatives are declared per callback, next to the reads, and the engine assembles requested derivatives by forward accumulation over the graph. Hosts ask for a derivative the way they ask for an output. Opt-in: no material changes behaviour, and nothing is computed unless a derivative is requested.
core/material_base.h,core/material_interface.h):callback_handle::partials(output, reads...)andindependent_of(reads...)afterreads(). Reads are inputs or outputs of the material, as references, pointers (null skipped) or pointer ranges. A read the callback does not declare inreads(), an output of another callback, a partial declared twice or a read both inpartials()andindependent_of()throws at construction.base::partial(output, read)returns the stored tmech value (scalar x scalar:value_type; otherwisetensor<T, Dim, rank(out) + rank(read)>).base::want_partials()is true only while a callback on a requested path runs.core/derivatives.h, new): one Mandel index per index pair in tmech's order with the sqrt(2) weights ofsolvers/unknown_layout.h(width 1, 6 or 36), each pair projected onto its symmetric part. Every row of the contraction table becomesD[out] += P * D[in]. tmech'sconvert_tensor_to_mandelhandles rank 2 and 4 only and does not project, so the packer here covers rank 0 to 8 itself.core/property_engine.h):derivative<R>(target, seed)plans once. It walks the target's upstream (upstream_of()) from the seed in topological order. Every node a derivative reaches needs a partial (or anindependent_of()) for each read that carries one. Otherwise it throws and names the whole path, e.g.strain::strain -> a::y -> b::y -> c::y. A node reached over an edge that is not a declared read (material_ref, drives, a callback without declared reads) throws the same way.differentiate()packs every partial on a plan once and multiplies the blocks with Eigen. The callbacks declaring partials are wrapped atbuild()sowant_partials()follows the request. These are new members, andupdate()is untouched.material_context::update()callsdifferentiate()after the values (a no-op without requests);material_context::derivative<R>(material, property, seed_material, seed_property).material_point::derivative<T>("material::output", "input")resolves an input to its history output (strain::strain,temperature::state), or takesmaterial::property.core/partial_check.h, new):check_partials(ctx, material)compares every declared partial with central differences of that one callback. It perturbs one Mandel component of the read at a time, so a symmetric tensor moves by (e_ij + e_ji)/2. It also perturbs every read declaredindependent_of()and requires every output of the callback to stay unchanged. The step is sqrt(eps) times the largest component of the read block, and the roundoff allowance is bounded by the analytic block. It restores the state, also when a perturbed rerun throws, and returns one line per wrong declaration.thermal_eigenstrain(d eps_th / d alpha, d eps_th / d T) andlinear_elasticity(d sigma / d eps = C, d sigma / d eps_eig = -C, d sigma / d C = Isym (x) (eps - eps_eig)). The values and the tangent output are unchanged. This gives the firstDDSDDT:derivative("elastic::stress", "temperature") = -C : alpha I.Decisions and deviations from the issue
linear_elasticitydeclares the true d sigma / d C (rank 6), notindependent_of(m_C)as in the issue'slinear_stresssketch.independent_of()means "zero", and a tangent that depends on the state (damage) would make that wrong without an error. It also puts the last row of the table (rank 6, 6x36 blocks) in a real material.linear_stressis not migrated; one material plus the eigenstrain is enough to prove the end-to-end path, and Migrate materials to declared partials and residual systems #219 migrates the rest.derivative<R>takes the value type or a tensor of rank 2 to 8 of the context's dimension (derivatives::derivative_value); on an evaluated context the callbacks of the new plan run once more, so the result is filled at once.has_path()tells independence from a typo.independent_of()covers every output of the callback. A read cannot be in bothpartials()andindependent_of().EdgeKind::Local) carry no derivative. They are solver internals, which Property engine: residual systems set up at run time (unknowns and equations from any nodes, Newton/Picard, implicit function theorem) #218 differentiates by the implicit function theorem. Old values (input_history::old_value()) are constants of the step. Partials are with respect to the new value.unknown_layout.h), so every rank-2/4 node is minor-symmetric here. A tag for the 9-wide layout can come with the first finite-strain derivative.NUMSIM_MATERIALS_DEBUG_CHECKSfinite-difference fallback for a missing partial is not implemented. It would need a second execution path in the planner and indifferentiate().check_partials()already gives per-node FD, so the fallback can follow if prototyping asks for it.reactivate()), so a folded callback fills its partials too.Tests
tests/test_partial_derivatives.cpp(new):independent_of()gives zero; a result type of the wrong rank throws; every declaration mistake throwscheck_partials()passes correct partials of every kind and names a wrong one, restoring the statematerial_point:derivative("elastic::stress", "temperature")matches central differences of the whole evaluation and equals -3 K alpha I.derivative("elastic::stress", "strain")equals thetangentoutput.tests/test_material_conformance.cpp: every header declaring.partials(or.independent_of(must be listed, and all their partials passcheck_partials()in a model built from JSON.Gate
GCC 14, reports on,
-j4: build 0 warnings,ctest100% tests passed, 0 tests failed out of 1524.