Skip to content

Slice 11 — mRBFNode real predict via JSON-path load - #9

Merged
891458249 merged 4 commits into
mainfrom
slice-11-mrbfnode-predict
Apr 21, 2026
Merged

Slice 11 — mRBFNode real predict via JSON-path load#9
891458249 merged 4 commits into
mainfrom
slice-11-mrbfnode-predict

Conversation

@891458249

Copy link
Copy Markdown
Owner

Summary

Phase 2A core functional slice. mRBFNode graduates from the Slice 10A HelloNode skeleton to a real RBF predictor. The node loads a schema-v1 JSON (Phase 1 RBFInterpolator::save output) from disk and serves predict() 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 jsonPath points at it. Rationale (full write-up in DEVLOG):

  • DG dirty tracking over an N×D compound array costs more than one file read
  • Slice 08 schema-v1 is already canonical on disk
  • Professional Maya RBF systems follow this pattern
  • Keeps node responsibilities clean (predictor + config, not trainer)

15 locked design decisions

(Full list in DEVLOG.) Highlights:

  • jsonPath string attribute + reloadTrigger int for forced reload
  • queryPoint / outputValues as MFnDoubleArrayData (variable D/M)
  • 6 readable state attributes (isLoaded, nCenters, dimInput, dimOutput, kernelType, statusMessage)
  • Load lazily on first compute after a dirty path / trigger
  • Failure mode: kSuccess + empty outputs + statusMessage + one warning per failing path
  • Scheduling kNormal (RBFInterpolator non-thread-safe; clone-per-thread comes later)

Phase 1 API amendment (scope exception, documented in DEVLOG)

RBFInterpolator::kernel_params() const noexcept added as the ONLY Phase 1 change. 3 LOC additive, noexcept, Maya-free, engine-agnostic, accompanied by a new test. Rationale: spec required aKernelType output 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

  • G1 "kGaussian""Gaussian" (kernel_type_to_string strips the k prefix) — pre-write grep
  • G2/G3 No public kernel-type getter on RBFInterpolator — pre-write, resolved by Phase 1 amendment
  • G4 Redundant PIC setup — pre-write, PIC already global
  • F4 cmds.setAttr("foo", count, v0, v1, type="doubleArray") silently truncates to 1-elem in both Maya 2022 and 2025 — caught mid-execution, fixed to cmds.setAttr("foo", [v0, v1], type="doubleArray") list form

Local validation (all 4 steps, both environments)

Step Result
1 — adapter + Phase 1 146/146 green, 12.56s (137 + 3 H + 6 C)
2a — Maya 2022 .mll 0 warn 0 err, 158 208 bytes
2b — Maya 2025 .mll 0 warn 0 err, 158 208 bytes (byte-identical)
3a — Maya 2022 smokes hellonode + predict both exit 0; all 3 queries err=0 exactly
3b — Maya 2025 smokes bit-identical to 2022
4 — Phase 1 regression 137/137 green, 10.20s

Commits

  1. feat(kernel) c9ec2fe — additive kernel_params() getter + test
  2. build(cmake) ab8649f — link rbfmax::solver into Maya target + version string bump
  3. feat(maya) c7459e8 — real predict via JSON-path load (incl. adapter helpers + smoke + fixtures)
  4. docs(devlog) 8bdd9b4 — Slice 11 entry with full 15-decision breakdown

Test plan

  • Local adapter + Phase 1 regression: 146/146
  • Local Maya 2022 smoke: hellonode + predict exit 0
  • Local Maya 2025 smoke: hellonode + predict exit 0 (bit-identical)
  • Local Phase 1 regression: 137/137
  • CI Windows MSVC Release (Phase 1 + 1 new test = 137)
  • CI Windows MSVC Debug
  • CI Ubuntu GCC 11 Release

Version

No bump (D14). v1.1.0 target is Phase 2A end (likely after Slice 12 rbfmaxTrainAndSave).

🤖 Generated with Claude Code

d891458249-rgb and others added 4 commits April 21, 2026 15:09
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>
@891458249
891458249 merged commit 1c16130 into main Apr 21, 2026
4 checks passed
@891458249
891458249 deleted the slice-11-mrbfnode-predict branch April 21, 2026 08:04
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.

2 participants