-
Notifications
You must be signed in to change notification settings - Fork 0
materials: props_scalar — deck constants read per call instead of baked in #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/json-model
Are you sure you want to change the base?
Changes from all commits
e0bf450
915026c
e0f7822
751f6f5
4420ded
56b7c89
0d60bac
c26bbbe
270c257
2b84ca2
9a93325
4355591
0653a71
e17e5f4
fa83292
50d5e47
c41eddf
c010efc
f67dc5c
be7e085
4867de6
76769d2
ef4ee2f
913f51f
cccfc46
9f058ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| #ifndef NUMSIM_MATERIALS_PROPS_SCALAR_H | ||
| #define NUMSIM_MATERIALS_PROPS_SCALAR_H | ||
|
|
||
| #include <cstddef> | ||
| #include <span> | ||
| #include <stdexcept> | ||
| #include <string> | ||
|
|
||
| #include "numsim-materials/core/material_base.h" | ||
|
|
||
| namespace numsim::materials { | ||
|
|
||
| /// A scalar taken from the host's material-constants array on every call. | ||
| /// | ||
| /// constant_scalar bakes its number in at construction, which is right when the | ||
| /// constants are fixed for a material name (Abaqus PROPS). This one records only | ||
| /// WHICH slot it owns, for hosts whose constants vary — CalculiX interpolates | ||
| /// them by temperature. | ||
| /// | ||
| /// Rebuilding the graph per call would also be correct, since nothing is | ||
| /// retained between calls, but it measures 22x an evaluation (6.7 us against | ||
| /// 302 ns) where reading a slot is free. | ||
| /// | ||
| /// bind() DEREFERENCES and keeps no pointer: a host array may be a per-call | ||
| /// temporary, and a kept pointer would read a dead stack slot next call — | ||
| /// usually returning the right number, because the slot is commonly reused. | ||
| /// | ||
| /// Plain property, no update callback, as constant_scalar: nothing reaches | ||
| /// statev_map, and the value is in place before ctx.update() so ordering | ||
| /// cannot matter. | ||
| /// | ||
| /// Parameters: | ||
| /// "name": material name | ||
| /// "index": which host constant this publishes, 0-based | ||
| template <typename Traits> | ||
| class props_scalar final : public material_base<props_scalar<Traits>, Traits> { | ||
| public: | ||
| using base = material_base<props_scalar<Traits>, Traits>; | ||
| using value_type = typename base::value_type; | ||
| using input_parameter_controller = typename base::input_parameter_controller; | ||
|
|
||
| template <typename... Args> | ||
| explicit props_scalar(Args&&... args) | ||
| : base(std::forward<Args>(args)...), | ||
| m_value(base::template add_output<value_type>("value")), | ||
| m_index(base::template get_parameter<std::size_t>("index")) { | ||
| // Until the first bind(), rather than whatever the storage held. | ||
| m_value = value_type{}; | ||
| } | ||
|
|
||
| static input_parameter_controller parameters() { | ||
| input_parameter_controller para{base::parameters()}; | ||
| para.template insert<std::size_t>("index").template add<is_required>(); | ||
| return para; | ||
| } | ||
|
|
||
| /// Copy this material's constant out of the host's array. | ||
| void bind(std::span<const value_type> props) { | ||
| if (m_index >= props.size()) | ||
| throw std::out_of_range( | ||
| "props_scalar '" + base::name() + "': wants constant " + | ||
| std::to_string(m_index) + " but only " + std::to_string(props.size()) + | ||
| " were supplied"); | ||
| m_value = props[m_index]; | ||
| } | ||
|
|
||
| [[nodiscard]] std::size_t index() const noexcept { return m_index; } | ||
|
|
||
| private: | ||
| value_type& m_value; | ||
| const std::size_t m_index; | ||
| }; | ||
|
|
||
| } // namespace numsim::materials | ||
|
|
||
| #endif // NUMSIM_MATERIALS_PROPS_SCALAR_H |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,6 +66,16 @@ class plane_stress_evaluator { | |
| /// Iterations the last evaluate() needed, for diagnostics. | ||
| [[nodiscard]] int last_iterations() const noexcept { return m_last_iters; } | ||
|
|
||
| /// Once for the whole out-of-plane solve: the constants do not depend on the | ||
| /// iterate. | ||
| void bind_props(std::span<const value_type> props) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Finding 3 (low): this forward is untested. Deleting the body would not fail a single test — every so this is a coverage gap rather than a defect. But it is the one place where binding interacts with an iterative evaluator, which makes it the most interesting path, not the least: if a future change moved the bind inside the out-of-plane loop, nothing here would notice. Worth one test that drives plane stress with live constants and asserts the condensed
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 751f6f5.
const T E = 9 * K * G / (3 * K + G);
const T nu = (3 * K - 2 * G) / (2 * (3 * K + G));
EXPECT_NEAR(ddsdde[0], E / (1 - nu * nu), 1e-9);
EXPECT_NEAR(stress[2], 0.0, 1e-10);Derived rather than hardcoded, so it fails if the constants don't reach the material at all — which is what a deleted forward looks like. Verified: replacing the body with Also asserts sigma_33 is driven to zero, so the test covers the out-of-plane solve actually converging with host-supplied moduli, not just the tangent value. |
||
| m_inner->bind_props(props); | ||
| } | ||
|
|
||
| [[nodiscard]] bool has_live_props() const noexcept { | ||
| return m_inner->has_live_props(); | ||
| } | ||
|
|
||
| void evaluate(const call& c) { | ||
| if (c.statev.size() < nstatv()) | ||
| throw fatal_error( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -179,10 +179,13 @@ class umat_registry { | |
| void evaluate(std::string_view cmname, std::span<const double> props, | ||
| const call& c) { | ||
| auto& ts = thread_state_for(cmname, props); | ||
| if (c.ec == element_case::plane_stress) | ||
| if (c.ec == element_case::plane_stress) { | ||
| ts.ps->bind_props(props); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Untested, and it is the production plane-stress path. Deleting this line passes 214/214. Last review I added The severity is limited by the guard added in the same round. With the line removed, a plane-stress call through So it aborts rather than running with moduli of zero. That is the never-bound check earning its keep on a path I had not considered when I added it — which is the argument for that kind of guard over a comment saying "remember to bind". Still worth closing: one test driving plane stress (NDI=2, NSHR=1) with live constants through the real entry point. I have the probe already; it returns
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. Verified load-bearing: with |
||
| ts.ps->evaluate(c); | ||
| else | ||
| } else { | ||
| ts.solid->bind_props(props); | ||
| ts.solid->evaluate(c); | ||
| } | ||
| } | ||
|
|
||
| /// Drop this thread's cached contexts. Only needed if models are | ||
|
|
@@ -207,10 +210,9 @@ class umat_registry { | |
| std::unique_ptr<context_type> ctx; | ||
| std::unique_ptr<evaluator_type> solid; | ||
| std::unique_ptr<ps_evaluator_type> ps; | ||
| /// How many constants the context was built from. The graph is built once | ||
| /// and reused, so a later call arriving with a different count would mean | ||
| /// the cached parameters no longer describe this material. | ||
| std::size_t nprops{0}; | ||
| /// What the context was built from; the graph is reused, so different | ||
| /// constants on a later call would no longer describe this material. | ||
| std::vector<double> props; | ||
| }; | ||
|
|
||
| static std::unordered_map<std::string, thread_state, transparent_string_hash, | ||
|
|
@@ -229,17 +231,33 @@ class umat_registry { | |
| const auto key = normalise_cmname(cmname.data(), cmname.size(), buf); | ||
| auto& cache = thread_cache(); | ||
| if (auto it = cache.find(key); it != cache.end()) { | ||
| // PROPS cannot vary for a given material name — two *MATERIAL blocks must | ||
| // have distinct names — so a changed count means the deck contradicts the | ||
| // cached graph. Checking the size is one comparison; checking the values | ||
| // is not worth it per integration point. | ||
| if (it->second.nprops != props.size()) | ||
| // PROPS cannot vary for one material name, so anything different | ||
| // contradicts the graph the constants were baked into. Values, not just | ||
| // the count: same length with different numbers is the case that reaches | ||
| // a material. Two faults, two messages — the check only ever explains. | ||
| if (it->second.props.size() != props.size()) | ||
| throw fatal_error( | ||
| "numsim UMAT: material '" + std::string(key) + | ||
| "' was built from " + std::to_string(it->second.nprops) + | ||
| " constants but this call supplies " + | ||
| std::to_string(props.size()) + | ||
| " — PROPS must be constant for a given material name"); | ||
| "numsim UMAT: material '" + std::string(key) + "' was built from " + | ||
| std::to_string(it->second.props.size()) + | ||
| " constants but this call supplies " + std::to_string(props.size()) + | ||
| " — NPROPS cannot vary for a given material name"); | ||
|
|
||
| // Then per SLOT, skipping the ones the model reads live. Asking | ||
| // has_live_props() for the whole model instead would wave a mixed | ||
| // document's BAKED constants through as well: they would keep the first | ||
| // call's values while the live ones tracked the deck, silently, which is | ||
| // the defect this check exists to catch. | ||
| for (std::size_t i = 0; i < props.size(); ++i) | ||
| if (!it->second.solid->is_live_prop(i) && | ||
| it->second.props[i] != props[i]) | ||
| throw fatal_error( | ||
| "numsim UMAT: material '" + std::string(key) + "' constant " + | ||
| std::to_string(i + 1) + " was baked into the graph as " + | ||
| std::to_string(it->second.props[i]) + " but this call supplies " + | ||
| std::to_string(props[i]) + | ||
| " — PROPS must be constant for a given material name; use " | ||
| "distinct *MATERIAL names for distinct constants, or a " | ||
| "props_scalar for a constant that genuinely varies per call"); | ||
| return it->second; | ||
| } | ||
|
|
||
|
|
@@ -269,7 +287,7 @@ class umat_registry { | |
| thread_state ts; | ||
| ts.ctx = std::make_unique<context_type>(); | ||
| m.build(*ts.ctx, props); | ||
| ts.nprops = props.size(); | ||
| ts.props.assign(props.begin(), props.end()); | ||
| if (!ts.ctx->is_finalized()) | ||
| throw fatal_error( | ||
| "the builder returned without calling finalize() on the context"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Finding 2 (medium): a forgotten
bind_propsis a singular tangent with no diagnostic.The early return is right for baked models, but there is no state saying "this model has readers and none of them have been bound yet". A direct C++ caller that never calls
bind_propsgets moduli of zero:The host then sees an all-zero
DDSDDEand fails to converge, with nothing pointing at the cause.PropsScalar.IsZeroBeforeTheFirstBindcurrently documents this as intended, which I now think is the wrong call — zero is a plausible-looking number for a modulus, and a degenerate one.The registry path always binds, so this is direct C++ use only. Still cheap to close:
One branch on a path that already costs ~300 ns. Alternatively initialise to NaN so it propagates loudly instead of quietly zeroing the stiffness, but an explicit error names the mistake.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 751f6f5.
m_props_bound, checked inevaluate_canonical— the single choke point beforectx.update(), so it covers the plane-stress path too:Deliberately catches never-bound, not stale. The plane-stress solve runs the graph repeatedly for one host call and must not re-bind per iterate, so "deliberately the same constants" and "forgot to re-bind" are indistinguishable from here. Under the registry that gap doesn't exist — it binds on every call. Said so in the comment rather than leaving it to be discovered.
I left
IsZeroBeforeTheFirstBindin place: the initial value is still worth pinning down, it just can no longer reach a host now that the evaluator refuses.EvaluatingBeforeBindingIsFatalalso asserts that binding afterwards works, so the guard isn't a one-way latch. Verified it fails with the check disabled.