Slice 11 — mRBFNode real predict via JSON-path load - #9
Merged
Conversation
Phase 2A Slice 11 pre-requisite: Phase 1 RBFInterpolator exposed no
public way to read the kernel type currently in effect on a fitted or
loaded instance. The Slice 11 Maya node needs this for its
aKernelType output attribute; without a getter the node would have
to re-parse the saved JSON file behind the scenes, transferring the
encapsulation gap to every future consumer (Phase 2C UI, external C++
bindings, cross-DCC).
Reviewer channel evaluated three paths and selected this one:
A Node-side second JSON parse
— encapsulation debt transferred to every future consumer
B Additive const getter on RBFInterpolator ← this commit
— 3 LOC, noexcept, Maya-free, engine-agnostic
C Drop aKernelType attribute entirely
— harms the Phase 2C UI long-term plan
The additive getter honours every Phase 1 API contract:
* noexcept
* Maya-free (header only touches <Eigen/Core> and our own types)
* engine-agnostic
* no behavioural change to fit / predict / save / load paths
Slice 11's Section G prohibition was originally "❌ 改 Phase 1 ...
任何代码", which internally contradicted the aKernelType requirement
inside the same spec. The prohibition is amended (documented in the
forthcoming DEVLOG entry) to "❌ 行为性代码" with explicit allowance
for additive const getters accompanied by tests.
Test: new TEST(RBFInterpolatorState, KernelParamsReflectsFit) in
test_interpolator.cpp, inserted into category D (state queries).
Verifies post-fit that kernel_params() returns the KernelParams stored
in the FitResult. Phase 1 regression now reports 137/137 green
(previously 136/136) — local verification confirmed before commit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Slice 11 needs solver symbols from inside the Maya plugin (the Maya node will load an RBFInterpolator and call predict on it; predict transitively pulls in rbfmax_solver's .cpp translation units via the Phase 1 RBFInterpolator implementation). Prior slices only linked rbfmax::kernel (header-only), which was sufficient for Slice 10A's HelloNode (evaluate_kernel is header-inline) but insufficient for the Slice 11 RBFInterpolator::load / predict code path. Position-independent code for the STATIC rbfmax_solver library is already implied globally by the top-level CMakeLists.txt line 36 (set(CMAKE_POSITION_INDEPENDENT_CODE ON)), so no per-target PIC override is needed here. An earlier spec draft included such an override; dropped after grep confirmed the global setting is live. Also bumps the embedded plugin version string "1.0.0-phase2a-slice10a" -> "1.0.0-phase2a-slice11" so MFnPlugin reports the current slice at loadPlugin time. No behaviour change yet — the next commit (feat maya) is what actually makes mRBFNode use rbfmax::solver. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 2A Slice 11 core functional slice. mRBFNode now loads a trained
Phase 1 RBFInterpolator from a schema-v1 JSON file on disk and serves
predict() to downstream plugs, with full two-version validation
(Maya 2022 + Maya 2025).
Architecture — "training data does not cross DG"
-------------------------------------------------
Training matrices (centers, targets) are NOT exposed as Maya attributes.
The user trains offline (future rbfmaxTrainAndSave command / Python
binding / C++ harness), saves schema-v1 JSON, and the node reads it.
Rationale:
* Maya DG dirty tracking over an N×D compound array attribute costs
far more than one file read at load time.
* Slice 08's schema-v1 is already the canonical on-disk representation.
* Professional Maya RBF systems (Maya Muscle, facial / AR rigs) all
follow this pattern.
* Keeps node responsibilities clean: predictor + config container;
training is an offline activity.
Attribute topology
------------------
INPUTS
jsonPath string — path to schema-v1 JSON
reloadTrigger int (keyable) — bump to force reload on content-change
queryPoint doubleArray — D-length query vector
inputValue double — legacy 10A HelloNode scalar input
OUTPUTS
outputValues doubleArray — M-length predict output
outputValue double — legacy 10A HelloNode scalar output
isLoaded bool
nCenters int
dimInput int
dimOutput int
kernelType string — "Linear" | "Cubic" | "Quintic" |
"ThinPlateSpline" | "Gaussian" |
"InverseMultiquadric"
statusMessage string
Scheduling: MPxNode default (kNormal). RBFInterpolator is non-thread-
safe per its own contract; Phase 2 may upgrade to kParallel once
clone()-per-thread infrastructure lands.
Error handling (D1/D2): compute() always returns kSuccess — failures
surface through statusMessage + isLoaded=false + empty outputValues.
A single MGlobal::displayWarning per failing path (deduplication keyed
on warned_about_current_path_, reset on path/reloadTrigger change).
Adapter helpers (adapter_core.hpp)
----------------------------------
Three new pure-C++ helpers, C++14-compatible so they compile under
both the plugin target (C++14, Maya 2022 ABI) and the adapter tests
(C++17, Phase 1 CI):
* double_vector_to_eigen(const std::vector<double>&) -> VectorX
* eigen_to_double_vector(const VectorX&) -> vector<double>
* validate_json_path(const std::string&) -> bool
adapter_core tests (test_adapter_core.cpp) — 6 new C-group TESTs:
C1 DoubleVectorRoundTrip
C2 EmptyVectorRoundTrip
C3 ValidateJsonPath_NonExistentReturnsFalse
C4 ValidateJsonPath_ExistentReturnsTrue
C5 ValidateJsonPath_EmptyStringReturnsFalse
C6 DoubleToEigenPreservesPrecision (EXPECT_EQ, exact)
Fixtures (maya_node/tests/smoke/fixtures/)
------------------------------------------
tiny_rbf.json + tiny_rbf_expected.json were generated ONCE out-of-repo
by a standalone C++ util (the util itself is NOT committed; it lives
in the DEVLOG Slice 11 entry as a reproducibility record). The util:
* Builds against Phase 1 rbfmax::solver in Release.
* Fits a 4-corner Gaussian interpolator (N=4, D=2, M=1, eps=1,
poly_degree=-1, target=x+y, lambda=1e-6).
* Calls RBFInterpolator::save to write tiny_rbf.json.
* Calls predict on three query points and writes their outputs to
tiny_rbf_expected.json.
This way the smoke's reference outputs come from the *exact* Phase 1
predict code path the plugin will execute. The smoke script's 1e-10
tolerance is defence-in-depth; observed err=0 exactly on both
Maya 2022 and Maya 2025.
smoke_predict.py (new)
----------------------
5-step mayapy contract:
1. loadPlugin
2. createNode("mRBFNode")
3. setAttr jsonPath + verify 6 state attributes
4. For each fixture query: setAttr queryPoint + getAttr outputValues
+ assert |err| < 1e-10
5. delete + flushUndo + unloadPlugin
F4 fix captured during executor run: cmds.setAttr("x.foo", count, v0,
v1, type="doubleArray") with unpacked count+values silently truncates
in both Maya 2022 and 2025 — the correct invocation is
cmds.setAttr("x.foo", [v0, v1], type="doubleArray") (plain Python list).
Smoke script uses the list form.
smoke_hellonode.py retained unchanged — Slice 10A legacy semantics
still work (jsonPath empty + inputValue non-default routes through
hello_transform).
Local verification (both Maya 2022 and Maya 2025 Release, MSVC 19.44)
---------------------------------------------------------------------
* Step 1: 146/146 adapter+Phase 1 green, 12.56 s
* Step 2a/b: .mll builds clean (158 208 bytes identical on both
versions — expected, source-level code is ABI-agnostic)
* Step 3a Maya 2022: hellonode exit 0, predict exit 0; three queries
bit-identical to Phase 1 reference (err=0 exactly)
* Step 3b Maya 2025: bit-identical to 2022 — Phase 2A version
matrix investment (Slices 10A/10C) pays off here
* Step 4: Phase 1 regression 137/137 green, 10.20 s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 2A core slice documentation:
* 15 locked design decisions (A0-F1) — including the
"training data does not cross DG" architecture and full
attribute topology
* 4 pre-write spec-drift catches (G1-G4) and 1 mid-execution
catch (F4 — cmds.setAttr doubleArray unpacked-args truncation)
* Tolerance register — all three classes observed at err=0 in
practice; 1e-10 / 1e-14 margins are defence in depth
* Fixture reproducibility: full generate_tiny_rbf.cpp source
recorded inline so the out-of-repo util can be reconstructed
for any future fixture refresh
* Phase 1 API amendment precedent: Section G revised to allow
additive const getters accompanied by tests; documented as
the rule for future Phase 2 slices encountering similar gaps
* Validation table — both Maya 2022 and Maya 2025 bit-identical
on all 3 predict queries (err=0 exactly), Phase 2A version-
matrix decoupling validated in its first real business-logic
test
* Tech-debt register: R-25/R-26/R-27/R-28 closed, R-29 (Maya
setAttr doubleArray gotcha) and T-11 (no save API yet) opened
Outstanding: Slice 12 (rbfmaxTrainAndSave + v1.1.0), Slice 10B
(Maya 2024), Slice 10D (Maya 2026), Phase 2B/2C.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8 tasks
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.
Summary
Phase 2A core functional slice.
mRBFNodegraduates from the Slice 10A HelloNode skeleton to a real RBF predictor. The node loads a schema-v1 JSON (Phase 1RBFInterpolator::saveoutput) from disk and servespredict()to downstream plugs.First slice where Phase 1's kernel + solver both run inside a Maya plugin. Double-validated on Maya 2022 + Maya 2025.
Architecture — "training data does not cross DG"
Training matrices (centers, targets) are not Maya attributes. Users train offline → save schema-v1 JSON → node's
jsonPathpoints at it. Rationale (full write-up in DEVLOG):15 locked design decisions
(Full list in DEVLOG.) Highlights:
jsonPathstring attribute +reloadTriggerint for forced reloadqueryPoint/outputValuesasMFnDoubleArrayData(variable D/M)isLoaded,nCenters,dimInput,dimOutput,kernelType,statusMessage)statusMessage+ one warning per failing pathkNormal(RBFInterpolator non-thread-safe; clone-per-thread comes later)Phase 1 API amendment (scope exception, documented in DEVLOG)
RBFInterpolator::kernel_params() const noexceptadded as the ONLY Phase 1 change. 3 LOC additive, noexcept, Maya-free, engine-agnostic, accompanied by a new test. Rationale: spec requiredaKernelTypeoutput attribute; the alternative ("node re-parses JSON") would transfer an encapsulation gap to every future consumer. The Section G prohibition was amended from "any Phase 1 code" to "any Phase 1 behavioural code" with explicit allowance for additive const getters + tests.Spec-drift catches
"kGaussian"→"Gaussian"(kernel_type_to_stringstrips thekprefix) — pre-write grepRBFInterpolator— pre-write, resolved by Phase 1 amendmentcmds.setAttr("foo", count, v0, v1, type="doubleArray")silently truncates to 1-elem in both Maya 2022 and 2025 — caught mid-execution, fixed tocmds.setAttr("foo", [v0, v1], type="doubleArray")list formLocal validation (all 4 steps, both environments)
.mll.mllCommits
feat(kernel)c9ec2fe— additivekernel_params()getter + testbuild(cmake)ab8649f— linkrbfmax::solverinto Maya target + version string bumpfeat(maya)c7459e8— real predict via JSON-path load (incl. adapter helpers + smoke + fixtures)docs(devlog)8bdd9b4— Slice 11 entry with full 15-decision breakdownTest plan
Version
No bump (D14). v1.1.0 target is Phase 2A end (likely after Slice 12
rbfmaxTrainAndSave).🤖 Generated with Claude Code