Skip to content

refactor(linking): own fixed-anchor transform in Rust - #755

Merged
seonghobae merged 1 commit into
mainfrom
fix/linking-fixed-anchor-rust-green
Aug 11, 2026
Merged

refactor(linking): own fixed-anchor transform in Rust#755
seonghobae merged 1 commit into
mainfrom
fix/linking-fixed-anchor-rust-green

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Why

Draft #737 is a RED ownership contract (test-only). Production fixed-anchor linking still computes scale/shift and transforms parameters in Python.

What

  • Rust link_fixed_item_parameters owns affine coefficients and theta/alpha/b transformation
  • PyO3 binding returns transport dict consumed by the public Python API
  • Ownership sentinel + known scale/shift recovery unit tests
  • Changelog fragment + APA 7th doctoring (Kolen & Brennan 2014)

Verification

  • cargo test -p mlsirm-core --lib fixed_anchor (2 pass)
  • pytest tests/test_linking_fixed_anchor_rust_ownership.py + linking suite (24 pass, 1 skip)

Supersedes #737 once product gates are green.

Summary by CodeRabbit

  • New Features

    • Added fixed-anchor parameter linking for multidimensional models.
    • Returns transformed person and item parameters along with scale and shift evidence.
    • Supports dimensions without anchors while preserving their parameters.
  • Bug Fixes

    • Added validation for parameter shapes, anchor indices, factor assignments, and finite values.
  • Documentation

    • Documented fixed-anchor linking behavior and implementation details.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e1ef1db3-4cff-4dd6-88a6-5b777b365e8d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR moves fixed-anchor affine linking calculations and parameter transformation into Rust. It adds a public Rust API, PyO3 exposure, Python delegation, ownership documentation, changelog entries, and Rust and Python verification tests.

Changes

Fixed-anchor linking

Layer / File(s) Summary
Rust linking contract and algorithm
crates/mlsirm-core/src/linking.rs
Adds FixedAnchorLinkResult and fixed-anchor linking. The implementation validates inputs, computes scale and shift values, transforms parameters, preserves unanchored dimensions, and tests identity and known transformations.
PyO3 binding and module export
crates/fast-mlsirm-py/src/lib.rs
Adds the Python binding, reshapes linked trait values, returns transformed parameters and affine evidence, and exports the function through _core.
Public API delegation and ownership verification
python/fast_mlsirm/linking.py, tests/test_linking_fixed_anchor_rust_ownership.py, CHANGELOG.md, docs/changelog.d/737-linking-fixed-anchor-rust-ownership.md, docs/doctoring/linking_fixed_anchor_rust_ownership.md
Routes the public API through Rust, preserves returned linking data, and documents and tests the Rust ownership boundary.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PublicAPI as Python link_fixed_item_parameters
  participant Binding as PyO3 _core binding
  participant RustCore as Rust fixed-anchor linker
  PublicAPI->>PublicAPI: Validate inputs and prepare contiguous arrays
  PublicAPI->>Binding: Call link_fixed_item_parameters
  Binding->>RustCore: Validate shapes and compute linking
  RustCore-->>Binding: Return theta, alpha, b, scale, shift
  Binding-->>PublicAPI: Return reshaped linked parameters
  PublicAPI-->>PublicAPI: Reconstruct affine evidence
Loading

Possibly related issues

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: moving fixed-anchor linking transformations into Rust.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/linking-fixed-anchor-rust-green
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/linking-fixed-anchor-rust-green

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Review exact current head 51c2d11f8026182e1380b785bd7e0c168c627948 only. Focus on Rust/PyO3 fixed-anchor linking ownership, exact public semantic parity, affine transform/sign convention, malformed/non-finite boundaries, caller immutability, and whether any Python production linking arithmetic remains. Do not mutate the branch or treat predecessor-head evidence as current.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== HEAD =="
git rev-parse HEAD
echo "== Status =="
git status --short
echo "== Commit =="
git show -s --format='%H%n%P%n%s' HEAD
echo "== Changed paths against first parent =="
git diff --name-status HEAD^ HEAD
echo "== Rust/Python outline candidates =="
git diff --name-only HEAD^ HEAD | while IFS= read -r path; do
  case "$path" in
    *.rs|*.py)
      echo "--- $path"
      ast-grep outline "$path" --items all 2>/dev/null | sed -n '1,180p' || true
      ;;
  esac
done

Length of output: 10341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Exact relevant diff =="
git diff --unified=80 HEAD^ HEAD -- \
  crates/mlsirm-core/src/linking.rs \
  crates/fast-mlsirm-py/src/lib.rs \
  python/fast_mlsirm/linking.py \
  tests/test_linking_fixed_anchor_rust_ownership.py

echo "== Core fixed-anchor implementation =="
sed -n '400,635p' crates/mlsirm-core/src/linking.rs

echo "== PyO3 binding implementation and module export =="
sed -n '5010,5130p' crates/fast-mlsirm-py/src/lib.rs
rg -n -C 3 'link_fixed_item_parameters' crates/fast-mlsirm-py/src/lib.rs

echo "== Python public API and parameter type =="
sed -n '1,145p' python/fast_mlsirm/linking.py
rg -n -C 4 'class MLSIRMParams|MLSIRMParams' python/fast_mlsirm/types.py

echo "== Production fixed-anchor callers and possible affine arithmetic =="
rg -n -C 4 --glob '*.py' --glob '!tests/**' \
  'link_fixed_item_parameters\s*\(|(?:scale|shift)\s*=|theta\s*\*|alpha\s*/|intercepts?.*(?:scale|shift)' \
  python

echo "== Ownership test =="
cat -n tests/test_linking_fixed_anchor_rust_ownership.py

Length of output: 93495


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== All fixed-anchor tests and assertions =="
rg -n -C 6 --glob '*.py' --glob '*.rs' \
  '(link_fixed_item_parameters|anchor_items|factor_id)' \
  tests crates/mlsirm-core/src

echo "== MLSIRMParams definition and copy contract =="
sed -n '1,55p' python/fast_mlsirm/types.py

echo "== Parent public implementation =="
git show HEAD^:python/fast_mlsirm/linking.py | sed -n '1,105p'

echo "== Direct binding call sites =="
rg -n -C 3 'core\.link_fixed_item_parameters\s*\(|link_fixed_item_parameters\s*\(' \
  --glob '*.py' --glob '*.rs' \
  python tests crates

echo "== Validation and overflow-relevant source lines =="
rg -n -C 4 --glob '*.rs' \
  '(is_finite\(\)|\.exp\(\)|non-finite|source theta|target anchor)' \
  crates/mlsirm-core/src/linking.rs crates/fast-mlsirm-py/src/lib.rs

echo "== Commit file statistics =="
git diff --check HEAD^ HEAD

Length of output: 161878


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Fixed-anchor public tests =="
sed -n '1,145p' tests/test_cov_a_linking.py
sed -n '450,505p' tests/test_irt_stability.py
sed -n '1,145p' tests/test_fixed_item_parameter_linking_robustness.py

echo "== Immutability or aliasing assertions =="
rg -n -i -C 5 \
  '(immutable|mutat|unchanged|deep copy|shares_memory|may_share_memory|source\.theta|target\.theta)' \
  tests/test_cov_a_linking.py \
  tests/test_irt_stability.py \
  tests/test_fixed_item_parameter_linking_robustness.py \
  tests/test_linking_fixed_anchor_rust_ownership.py

echo "== Changed documentation claims =="
git diff --unified=20 HEAD^ HEAD -- \
  CHANGELOG.md \
  docs/changelog.d/737-linking-fixed-anchor-rust-ownership.md \
  docs/doctoring/linking_fixed_anchor_rust_ownership.md

Length of output: 24184


@seonghobae No findings for commit 51c2d11f8026182e1380b785bd7e0c168c627948.

I verified the following:

  • Rust owns fixed-anchor scale, shift, and theta/alpha/b transformation.
  • PyO3 returns the Rust transport dictionary.
  • Python performs validation, parameter-object reconstruction, and evidence packaging only.
  • No Python production fixed-anchor transform arithmetic remains.
  • The transform preserves eta = exp(alpha) * theta + b.
  • The scale and shift sign convention matches the existing public behavior.
  • The wrapper returns a deep-copied parameter object. Rust only reads caller input buffers.
  • Malformed inputs, non-finite item parameters, and underflowed anchor slopes retain guarded failure behavior.
  • Exact-transform, partial-anchor, single-anchor, and Rust-ownership tests cover the primary semantic contract.
  • git diff --check passed.

I could not run the Rust or Python tests in this sandbox.

You are interacting with an AI system.

Copy link
Copy Markdown
Contributor Author

@opencode-agent address

Current-main reconciliation lease for PR #755 only. Refetch immediately before writing and proceed only while source head is exactly 51c2d11f8026182e1380b785bd7e0c168c627948, protected main is exactly bf51cc1849c6258d54b8e2f8c67be8ce02575114, and no other writer has appeared on this branch. The branch is now ahead 1 / behind 3 with merge base 8a56d7b1b024e18a5a96a94db4ede1bf6648ead1; predecessor CI/review evidence is not merge acceptance.

Non-destructively reconcile current protected main while preserving all newly integrated #761 item-bank and #762 canonical-documentation/changelog material and only #755's fixed-anchor linking delta. Do not force-push, use ours/theirs wholesale, or touch unrelated numerical semantics. Re-render the managed CHANGELOG from the complete current fragment set, run the focused fixed-anchor ownership/parity/recovery tests and Rust tests, then full applicable gates. Keep the exact public affine/sign convention and Rust/PyO3 ownership intact. Stop writing if head/base/writer state moves; report exact new head and verification evidence, but do not merge.

Copy link
Copy Markdown
Contributor Author

@opencode-agent address

Previous #755 reconciliation lease is stale because protected main advanced after #753 merged. Re-establish the sole #755 source lease only if the exact current source head is still 3288d564c6896c9b37d42eac159ee4c234d5b877, protected main is exactly d6ff8b1f5f8625ff9088d860cd7b6cefa3809d0f, and no other writer has appeared. Preserve the now-integrated Rust fixed-form ATA/PyO3/CHANGELOG material from #753 as well as #761/#762, while retaining only #755's fixed-anchor linking delta. Non-destructive current-main reconciliation only; re-render CHANGELOG, rerun focused Rust/linking ownership/parity/recovery tests, and leave Draft until fresh exact-head CI/security/review complete. Stop immediately if either ref moves again; do not merge.

Public link_fixed_item_parameters validates and marshals only; affine
scale/shift and parameter transformation move to the Rust core with
ownership sentinel tests, recovery unit tests, changelog, and APA doctoring.

Supersedes draft #737 once green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/mlsirm-core/src/linking.rs`:
- Around line 542-552: Update the parameter transformation loops in the linking
function to reject any non-finite linked_theta, linked_alpha, linked_a, or
linked_b values before returning. Handle sh == 0.0 by preserving source_b[i]
directly instead of computing linked_a * sh, while retaining the existing affine
transformations for other cases.
- Around line 453-455: Update the theta-shape validation around
source_theta.len() to compute n_persons * n_dims with checked_mul before
comparing lengths. Treat a multiplication overflow as the same validation error
as an invalid theta shape, ensuring caller-controlled dimensions cannot panic or
wrap.
- Around line 589-597: Update the comment above source_theta in the linking test
to describe shift=1.0, matching the formula and assertions; leave the test
values and implementation unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9825a4b7-047b-4a86-a5d0-f59fe3cea04c

📥 Commits

Reviewing files that changed from the base of the PR and between bf51cc1 and 3288d56.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • crates/fast-mlsirm-py/src/lib.rs
  • crates/mlsirm-core/src/linking.rs
  • docs/changelog.d/737-linking-fixed-anchor-rust-ownership.md
  • docs/doctoring/linking_fixed_anchor_rust_ownership.md
  • python/fast_mlsirm/linking.py
  • tests/test_linking_fixed_anchor_rust_ownership.py

Comment on lines +453 to +455
if source_theta.len() != n_persons * n_dims {
return Err("source theta length must equal n_persons * n_dims".into());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject overflowing theta shapes.

Line 453 multiplies two caller-controlled usize values without a checked operation. A direct Rust caller can trigger an overflow panic in debug builds or a wrapped length in release builds.

Use checked_mul before comparing the slice length.

Proposed fix
-    if source_theta.len() != n_persons * n_dims {
+    let expected_theta_len = n_persons
+        .checked_mul(n_dims)
+        .ok_or_else(|| "source theta shape is too large".to_owned())?;
+    if source_theta.len() != expected_theta_len {
         return Err("source theta length must equal n_persons * n_dims".into());
     }

As per coding guidelines, “For fuzzed arbitrary input, code must either succeed or raise a documented benign exception; panics, hangs, AssertionError, KeyError, IndexError, and TypeError are bugs.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if source_theta.len() != n_persons * n_dims {
return Err("source theta length must equal n_persons * n_dims".into());
}
let expected_theta_len = n_persons
.checked_mul(n_dims)
.ok_or_else(|| "source theta shape is too large".to_owned())?;
if source_theta.len() != expected_theta_len {
return Err("source theta length must equal n_persons * n_dims".into());
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/mlsirm-core/src/linking.rs` around lines 453 - 455, Update the
theta-shape validation around source_theta.len() to compute n_persons * n_dims
with checked_mul before comparing lengths. Treat a multiplication overflow as
the same validation error as an invalid theta shape, ensuring caller-controlled
dimensions cannot panic or wrap.

Source: Coding guidelines

Comment on lines +542 to +552
for p in 0..n_persons {
let idx = p * n_dims + dim;
linked_theta[idx] = s * source_theta[idx] + sh;
}
for i in 0..n_items {
if factors[i] != dim_i {
continue;
}
linked_alpha[i] = source_alpha[i] - log_s;
let linked_a = linked_alpha[i].exp();
linked_b[i] = source_b[i] - linked_a * sh;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject non-finite transformed parameters.

Finite inputs can produce non-finite outputs. For example, a non-anchor source_alpha of 1000.0 with sh == 0.0 makes linked_a infinite, then linked_a * sh becomes NaN. A large finite source_theta can also overflow during the affine transform.

Validate each transformed value before returning it. Preserve source_b[i] directly when sh == 0.0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/mlsirm-core/src/linking.rs` around lines 542 - 552, Update the
parameter transformation loops in the linking function to reject any non-finite
linked_theta, linked_alpha, linked_a, or linked_b values before returning.
Handle sh == 0.0 by preserving source_b[i] directly instead of computing
linked_a * sh, while retaining the existing affine transformations for other
cases.

Source: Coding guidelines

Comment on lines +589 to +597
// target a = source a / 2, target b = source b - source_a * 0.5
// implies scale=2, shift=0.5 when linking source onto target metric.
let source_theta = [0.0_f64, 1.0];
let source_alpha = [0.0_f64]; // a=1
let source_b = [0.0_f64];
let target_alpha = [(-(2.0_f64).ln())]; // a=0.5
let target_b = [-0.5_f64]; // b_s - a_t * shift with shift=1? use formula
// scale = exp(mean(log(1/0.5))) = 2
// shift = mean((0 - (-0.5)) / 0.5) = 1.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the shift description.

Line 590 states shift=0.5, but the formula and assertions calculate shift=1.0. Update the comment to match the test.

-        // implies scale=2, shift=0.5 when linking source onto target metric.
+        // implies scale=2, shift=1.0 when linking source onto target metric.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// target a = source a / 2, target b = source b - source_a * 0.5
// implies scale=2, shift=0.5 when linking source onto target metric.
let source_theta = [0.0_f64, 1.0];
let source_alpha = [0.0_f64]; // a=1
let source_b = [0.0_f64];
let target_alpha = [(-(2.0_f64).ln())]; // a=0.5
let target_b = [-0.5_f64]; // b_s - a_t * shift with shift=1? use formula
// scale = exp(mean(log(1/0.5))) = 2
// shift = mean((0 - (-0.5)) / 0.5) = 1.0
// target a = source a / 2, target b = source b - source_a * 0.5
// implies scale=2, shift=1.0 when linking source onto target metric.
let source_theta = [0.0_f64, 1.0];
let source_alpha = [0.0_f64]; // a=1
let source_b = [0.0_f64];
let target_alpha = [(-(2.0_f64).ln())]; // a=0.5
let target_b = [-0.5_f64]; // b_s - a_t * shift with shift=1? use formula
// scale = exp(mean(log(1/0.5))) = 2
// shift = mean((0 - (-0.5)) / 0.5) = 1.0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/mlsirm-core/src/linking.rs` around lines 589 - 597, Update the comment
above source_theta in the linking test to describe shift=1.0, matching the
formula and assertions; leave the test values and implementation unchanged.

@seonghobae
seonghobae force-pushed the fix/linking-fixed-anchor-rust-green branch from 3288d56 to e497647 Compare August 11, 2026 18:49
@seonghobae
seonghobae marked this pull request as ready for review August 11, 2026 18:50
@seonghobae
seonghobae merged commit 692bb95 into main Aug 11, 2026
20 checks passed
@seonghobae
seonghobae deleted the fix/linking-fixed-anchor-rust-green branch August 11, 2026 18:50

Copy link
Copy Markdown
Contributor Author

@opencode-agent address

Exact current-head review-repair lease for #755 only. Before writing, refetch and proceed only while source head is exactly e4976472c69bfbf19e837a613d510e2825018b59, protected main is exactly d6ff8b1f5f8625ff9088d860cd7b6cefa3809d0f, and no other writer has appeared. The branch is clean-lineage (ahead 1 / behind 0). CodeRabbit has now posted three current, unresolved findings; repair them test-first/minimally:

  1. In mlsirm-core fixed-anchor linking, replace caller-controlled n_persons * n_dims with checked multiplication and a stable benign validation error on overflow; add direct Rust regression for overflow without allocating.
  2. Add fail-closed finite-output protection for transformed theta/alpha/discrimination/b. Cover finite extreme inputs that would overflow the affine or exp(alpha) path. Preserve source_b exactly when shift is zero so inf * 0 cannot manufacture NaN, and reject any other non-finite transformed result before transport.
  3. Correct the stale test comment from shift 0.5 to the actually asserted shift 1.0.

Also re-run the repository's changed/public docstring coverage; CodeRabbit currently reports 75%. Fix only genuine documentation coverage gaps introduced/owned by this slice (for example a missing module/public API docstring), not unrelated files. Then run focused Rust fixed-anchor tests, Python ownership/parity/linking tests, formatting/lint/diff checks and the full applicable suite; re-render CHANGELOG only through the repository renderer if source changes require it. Resolve only review threads whose findings are actually fixed. Keep Draft and do not merge. Stop immediately if head/base/writer state moves.

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.

1 participant