Skip to content

Gaussian math: inverse_cdf#429

Merged
immrsd merged 11 commits into
mainfrom
fp-impl-inverse-cdf
Jul 3, 2026
Merged

Gaussian math: inverse_cdf#429
immrsd merged 11 commits into
mainfrom
fp-impl-inverse-cdf

Conversation

@immrsd

@immrsd immrsd commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added inverse CDF (standard-normal quantile) support for signed and unsigned fixed-point types, including domain handling for probabilities and upper-half evaluation with reflection.
    • Added deterministic saturation and symmetry behavior to match quantile expectations.
  • Documentation

    • Documented the new inverse_cdf API, accuracy/behavior guarantees, and code generation workflow for quantile support.
  • Tests

    • Added new unit tests and oracle-based test vectors covering accuracy, monotonicity, symmetry, round-trip behavior, and determinism.
  • Chores / CI

    • Extended code generation validation in CI and added inverse-CDF coefficient/vector drift checks.

@immrsd immrsd self-assigned this Jul 2, 2026
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 769acbb9-6d38-4ce4-b134-ecd16287d1f0

📥 Commits

Reviewing files that changed from the base of the PR and between 62c2494 and 2c438ee.

📒 Files selected for processing (4)
  • math/fixed_point/tests/sd29x9_tests/inverse_cdf_test_vectors.move
  • math/fixed_point/tests/ud30x9_tests/inverse_cdf_test_vectors.move
  • scripts/gaussian_codegen/Makefile
  • scripts/gaussian_codegen/inverse_cdf/emit_test_vectors.py
✅ Files skipped from review due to trivial changes (2)
  • math/fixed_point/tests/sd29x9_tests/inverse_cdf_test_vectors.move
  • math/fixed_point/tests/ud30x9_tests/inverse_cdf_test_vectors.move
🚧 Files skipped from review as they are similar to previous changes (2)
  • scripts/gaussian_codegen/Makefile
  • scripts/gaussian_codegen/inverse_cdf/emit_test_vectors.py

📝 Walkthrough

Walkthrough

This PR adds standard-normal inverse CDF support to the fixed-point math library. It includes offline Python tooling for two-region coefficient generation and validation, new generated Move coefficient/evaluator modules, public inverse_cdf APIs on SD29x9 and UD30x9, expanded tests, and documentation updates.

Changes

Inverse CDF feature

Layer / File(s) Summary
Offline coefficient derivation and arithmetic mirror
scripts/gaussian_codegen/inverse_cdf/derive.py, scripts/gaussian_codegen/shared/arithmetic.py, scripts/gaussian_codegen/shared/constants.py, scripts/gaussian_codegen/shared/reference.py, scripts/gaussian_codegen/inverse_cdf/.derive_output.json
Fits central and tail rational approximations for Φ⁻¹(p), adds the tail-transform arithmetic mirror, quantile bounds, an oracle, and derived approximation data.
Emission, validation, and generator wiring
scripts/gaussian_codegen/inverse_cdf/emit_coefficients.py, scripts/gaussian_codegen/inverse_cdf/emit_test_vectors.py, scripts/gaussian_codegen/inverse_cdf/validate.py, scripts/gaussian_codegen/tests/test_inverse_cdf.py, scripts/gaussian_codegen/Makefile, scripts/gaussian_codegen/pyproject.toml, .github/workflows/test.yml, scripts/gaussian_codegen/README.md
Generates committed Move sources, validates them against the oracle, wires CI and package targets, and documents the pipeline.
Move coefficient module and core evaluator
math/fixed_point/sources/internal/inverse_cdf_coefficients.move, math/fixed_point/sources/internal/inverse_cdf.move
Adds the generated coefficient tables and the Move evaluator for the upper-half inverse CDF, including clamps and integrity checks.
Public API wiring for SD29x9 and UD30x9
math/fixed_point/sources/sd29x9/sd29x9.move, math/fixed_point/sources/sd29x9/sd29x9_base.move, math/fixed_point/sources/ud30x9/ud30x9.move, math/fixed_point/sources/ud30x9/ud30x9_base.move
Exposes inverse_cdf on both fixed-point types with range validation and lower-half reflection behavior.
Move and codegen test suites
math/fixed_point/tests/sd29x9_tests/inverse_cdf*.move, math/fixed_point/tests/ud30x9_tests/inverse_cdf*.move, scripts/gaussian_codegen/tests/test_inverse_cdf.py
Adds oracle-driven test vectors and unit tests covering correctness, saturation, symmetry, monotonicity, round-trips, guardrails, and dispatch.
Fixed-point and codegen documentation
math/fixed_point/README.md, scripts/gaussian_codegen/README.md
Documents the new quantile function, generation commands, and generated artifacts.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant SD29x9
  participant sd29x9_base
  participant inverse_cdf_upper_raw
  participant inverse_cdf_coefficients

  Caller->>SD29x9: inverse_cdf(p)
  SD29x9->>sd29x9_base: inverse_cdf(p)
  sd29x9_base->>sd29x9_base: validate p in [0,1]
  alt p >= 0.5
    sd29x9_base->>inverse_cdf_upper_raw: p_raw
  else p < 0.5
    sd29x9_base->>inverse_cdf_upper_raw: 1 - p_raw
  end
  inverse_cdf_upper_raw->>inverse_cdf_coefficients: read central/tail coefficients
  inverse_cdf_upper_raw->>inverse_cdf_upper_raw: evaluate rational and clamp
  inverse_cdf_upper_raw-->>sd29x9_base: z_raw
  sd29x9_base-->>Caller: wrapped result
Loading

Possibly related PRs

Suggested reviewers: 0xNeshi, bidzyyys, ericnordelo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is missing and does not include the required issue reference, change summary, or checklist items. Add the template sections: Resolves #..., a brief change summary, and the PR checklist entries for tests, documentation, and changelog.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately names the main addition: inverse CDF Gaussian math.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fp-impl-inverse-cdf

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

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.62%. Comparing base (cde1812) to head (83df836).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #429      +/-   ##
==========================================
+ Coverage   96.54%   96.62%   +0.07%     
==========================================
  Files          32       34       +2     
  Lines        3328     3405      +77     
  Branches      782      794      +12     
==========================================
+ Hits         3213     3290      +77     
  Misses         70       70              
  Partials       45       45              
Flag Coverage Δ
contracts/access 65.46% <ø> (ø)
contracts/allowance 52.40% <ø> (ø)
contracts/finance 26.07% <ø> (ø)
contracts/timelock 54.57% <ø> (ø)
contracts/utils 44.09% <ø> (ø)
math/core 86.97% <ø> (ø)
math/fixed_point 63.70% <100.00%> (+2.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/gaussian_codegen/Makefile (1)

30-47: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Split the expanded check target to satisfy checkmake.

The added inverse-CDF commands push check past the configured target-body limit. Factor per-family checks and let ci depend on check instead of duplicating the commands.

Proposed fix
-check: ## Drift-check generated files vs the emitters (both exact; needs prettier)
+check: check-cdf check-pdf check-inverse-cdf ## Drift-check generated files vs the emitters (both exact; needs prettier)
+
+check-cdf:
 	$(PYTHON) -m gaussian_codegen.cdf.emit_coefficients --check
 	$(PYTHON) -m gaussian_codegen.cdf.emit_test_vectors --check
+
+check-pdf:
 	$(PYTHON) -m gaussian_codegen.pdf.emit_coefficients --check
 	$(PYTHON) -m gaussian_codegen.pdf.emit_test_vectors --check
+
+check-inverse-cdf:
 	$(PYTHON) -m gaussian_codegen.inverse_cdf.emit_coefficients --check
 	$(PYTHON) -m gaussian_codegen.inverse_cdf.emit_test_vectors --check

-ci: validate test ## Exactly what CI runs
-	$(PYTHON) -m gaussian_codegen.cdf.emit_coefficients --check
-	$(PYTHON) -m gaussian_codegen.cdf.emit_test_vectors --check
-	$(PYTHON) -m gaussian_codegen.pdf.emit_coefficients --check
-	$(PYTHON) -m gaussian_codegen.pdf.emit_test_vectors --check
-	$(PYTHON) -m gaussian_codegen.inverse_cdf.emit_coefficients --check
-	$(PYTHON) -m gaussian_codegen.inverse_cdf.emit_test_vectors --check
+ci: validate test check ## Exactly what CI runs
🤖 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 `@scripts/gaussian_codegen/Makefile` around lines 30 - 47, The check target
body is now too large, so split the repeated generator checks in Makefile into
smaller per-family targets referenced by check. Move the cdf, pdf, and
inverse_cdf --check commands into separate helper targets, then have check
depend on them and update ci to depend on check instead of duplicating the same
commands. Use the existing check, ci, and the gaussian_codegen.*.emit_* entry
points to keep the structure easy to locate.

Source: Linters/SAST tools

🧹 Nitpick comments (4)
math/fixed_point/sources/sd29x9/sd29x9_base.move (1)

319-319: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the existing half_raw() helper instead of common::scale!() / 2.

half_raw is already imported here (Line 6) and used for the same 0.5-probability threshold in cdf() (Line 205). Recomputing it inline duplicates the magic value and diverges from the module's own convention.

♻️ Proposed fix
-    if (p_raw >= common::scale!() / 2) {
+    if (p_raw >= half_raw()) {
🤖 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 `@math/fixed_point/sources/sd29x9/sd29x9_base.move` at line 319, The threshold
check in the sd29x9 fixed-point logic should reuse the existing half_raw()
helper instead of recomputing common::scale!() / 2 inline. Update the branch in
the same area as the probability rounding logic to call half_raw(), matching the
convention already used by cdf() and keeping the threshold consistent and
centralized.
math/fixed_point/sources/ud30x9/ud30x9_base.move (1)

280-283: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse half_raw() instead of common::scale!() / 2.

Same duplication as in sd29x9_base::inverse_cdf. Importing half_raw from cdf (already used for this exact 0.5 threshold) avoids the repeated magic value.

♻️ Proposed fix
-use openzeppelin_fp_math::cdf::cdf_nonneg_raw;
+use openzeppelin_fp_math::cdf::{cdf_nonneg_raw, half_raw};
-    assert!(p_raw >= common::scale!() / 2, EProbabilityBelowHalf); // p ≥ 0.5
+    assert!(p_raw >= half_raw(), EProbabilityBelowHalf); // p ≥ 0.5
🤖 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 `@math/fixed_point/sources/ud30x9/ud30x9_base.move` around lines 280 - 283, The
`inverse_cdf` probability threshold check duplicates the 0.5 constant by using
`common::scale!() / 2` directly. Update the `ud30x9_base::inverse_cdf` logic to
reuse `half_raw()` from `cdf`, matching the existing pattern used in
`sd29x9_base::inverse_cdf`, and keep the same `EProbabilityBelowHalf` validation
with the shared helper.
scripts/gaussian_codegen/inverse_cdf/emit_coefficients.py (1)

127-131: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Render doc-comment bounds from the emitted values.

These generated comments hard-code 975_000_000 and 6_300_000_000, while the constants come from .derive_output.json. If the fit changes, docs drift even though emitted constants stay correct.

Proposed fix
-/// Central-vs-tail probability split at the raw `10^9` scale (`975_000_000`).
+/// Central-vs-tail probability split at the raw `10^9` scale (`{fmt_u128(threshold_raw)}`).
 public(package) fun central_threshold_raw(): u128 {{ CENTRAL_THRESHOLD_RAW }}

-/// Output saturation clamp `|z|` at the raw `10^9` scale (`6_300_000_000`).
+/// Output saturation clamp `|z|` at the raw `10^9` scale (`{fmt_u128(max_z_raw)}`).
 public(package) fun max_z_raw(): u128 {{ MAX_Z_RAW }}
🤖 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 `@scripts/gaussian_codegen/inverse_cdf/emit_coefficients.py` around lines 127 -
131, The generated doc-comments for central_threshold_raw and max_z_raw are
hard-coding numeric bounds instead of reflecting the emitted constants. Update
emit_coefficients.py so the comments are rendered from the same
CENTRAL_THRESHOLD_RAW and MAX_Z_RAW values used in the generated functions,
keeping the documentation in sync with the values derived from
.derive_output.json even if the fit changes.
scripts/gaussian_codegen/inverse_cdf/emit_test_vectors.py (1)

130-162: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Emit STYLEGUIDE section order in generated test-vector modules.

The generated Move currently emits constants before errors and places #[test] functions without a // === Test-Only Helpers === section. Update both renderers to emit // === Errors ===, // === Constants ===, // === Structs ===, then // === Test-Only Helpers ===.

As per coding guidelines, **/*.move files must follow STYLEGUIDE.md section ordering and testing standards. As per path instructions, new inverse-CDF .move modules should be reviewed against STYLEGUIDE.md as the source of truth.

Also applies to: 178-208

🤖 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 `@scripts/gaussian_codegen/inverse_cdf/emit_test_vectors.py` around lines 130 -
162, The generated inverse-CDF test-vector module is missing the required
STYLEGUIDE section ordering. Update the Move renderers in emit_test_vectors.py
so they emit sections in the order `// === Errors ===`, `// === Constants ===`,
`// === Structs ===`, then `// === Test-Only Helpers ===`, with the `#[test]`
function placed under the test-only helpers section. Ensure the generator still
renders the same `TestCase`, `TOLERANCE`, and `ETestCaseFailed` symbols, but
reordered to match the standard used by the other renderer.

Sources: Coding guidelines, Path instructions

🤖 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 `@math/fixed_point/tests/sd29x9_tests/inverse_cdf_test_vectors.move`:
- Around line 13-15: The test vector error string in the inverse_cdf generated
constants still embeds the implementation identifier TOLERANCE, so update the
source template in emit_test_vectors.py rather than the generated Move files.
Change the ETestCaseFailed message to plain English only, and make the same
template update for the UD30x9 inverse_cdf vector generation so both outputs
stay consistent.

---

Outside diff comments:
In `@scripts/gaussian_codegen/Makefile`:
- Around line 30-47: The check target body is now too large, so split the
repeated generator checks in Makefile into smaller per-family targets referenced
by check. Move the cdf, pdf, and inverse_cdf --check commands into separate
helper targets, then have check depend on them and update ci to depend on check
instead of duplicating the same commands. Use the existing check, ci, and the
gaussian_codegen.*.emit_* entry points to keep the structure easy to locate.

---

Nitpick comments:
In `@math/fixed_point/sources/sd29x9/sd29x9_base.move`:
- Line 319: The threshold check in the sd29x9 fixed-point logic should reuse the
existing half_raw() helper instead of recomputing common::scale!() / 2 inline.
Update the branch in the same area as the probability rounding logic to call
half_raw(), matching the convention already used by cdf() and keeping the
threshold consistent and centralized.

In `@math/fixed_point/sources/ud30x9/ud30x9_base.move`:
- Around line 280-283: The `inverse_cdf` probability threshold check duplicates
the 0.5 constant by using `common::scale!() / 2` directly. Update the
`ud30x9_base::inverse_cdf` logic to reuse `half_raw()` from `cdf`, matching the
existing pattern used in `sd29x9_base::inverse_cdf`, and keep the same
`EProbabilityBelowHalf` validation with the shared helper.

In `@scripts/gaussian_codegen/inverse_cdf/emit_coefficients.py`:
- Around line 127-131: The generated doc-comments for central_threshold_raw and
max_z_raw are hard-coding numeric bounds instead of reflecting the emitted
constants. Update emit_coefficients.py so the comments are rendered from the
same CENTRAL_THRESHOLD_RAW and MAX_Z_RAW values used in the generated functions,
keeping the documentation in sync with the values derived from
.derive_output.json even if the fit changes.

In `@scripts/gaussian_codegen/inverse_cdf/emit_test_vectors.py`:
- Around line 130-162: The generated inverse-CDF test-vector module is missing
the required STYLEGUIDE section ordering. Update the Move renderers in
emit_test_vectors.py so they emit sections in the order `// === Errors ===`, `//
=== Constants ===`, `// === Structs ===`, then `// === Test-Only Helpers ===`,
with the `#[test]` function placed under the test-only helpers section. Ensure
the generator still renders the same `TestCase`, `TOLERANCE`, and
`ETestCaseFailed` symbols, but reordered to match the standard used by the other
renderer.
🪄 Autofix (Beta)

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

Run ID: 7afe7894-f1e2-4ff3-b1c1-5d602f187509

📥 Commits

Reviewing files that changed from the base of the PR and between 85448ce and 62c2494.

📒 Files selected for processing (25)
  • .github/workflows/test.yml
  • math/fixed_point/README.md
  • math/fixed_point/sources/internal/inverse_cdf.move
  • math/fixed_point/sources/internal/inverse_cdf_coefficients.move
  • math/fixed_point/sources/sd29x9/sd29x9.move
  • math/fixed_point/sources/sd29x9/sd29x9_base.move
  • math/fixed_point/sources/ud30x9/ud30x9.move
  • math/fixed_point/sources/ud30x9/ud30x9_base.move
  • math/fixed_point/tests/sd29x9_tests/inverse_cdf_test_vectors.move
  • math/fixed_point/tests/sd29x9_tests/inverse_cdf_tests.move
  • math/fixed_point/tests/ud30x9_tests/inverse_cdf_test_vectors.move
  • math/fixed_point/tests/ud30x9_tests/inverse_cdf_tests.move
  • scripts/gaussian_codegen/Makefile
  • scripts/gaussian_codegen/README.md
  • scripts/gaussian_codegen/inverse_cdf/.derive_output.json
  • scripts/gaussian_codegen/inverse_cdf/__init__.py
  • scripts/gaussian_codegen/inverse_cdf/derive.py
  • scripts/gaussian_codegen/inverse_cdf/emit_coefficients.py
  • scripts/gaussian_codegen/inverse_cdf/emit_test_vectors.py
  • scripts/gaussian_codegen/inverse_cdf/validate.py
  • scripts/gaussian_codegen/pyproject.toml
  • scripts/gaussian_codegen/shared/arithmetic.py
  • scripts/gaussian_codegen/shared/constants.py
  • scripts/gaussian_codegen/shared/reference.py
  • scripts/gaussian_codegen/tests/test_inverse_cdf.py

Comment thread math/fixed_point/tests/sd29x9_tests/inverse_cdf_test_vectors.move Outdated
@bidzyyys bidzyyys linked an issue Jul 2, 2026 that may be closed by this pull request
@bidzyyys

bidzyyys commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator
  • 🟡 inverse_cdf_coefficients.move:67 + sd29x9_base/ud30x9_base inverse_cdf docsMAX_Z_RAW = 6_300_000_000 and the doc claims "6.3 matches the CDF domain bound, so the two functions agree at the corner". PR Gaussian math: cdf and pdf improvements (Step-1) #431 moves the CDF saturation to 6.109410205, so once both land that claim is false and cdf/inverse_cdf no longer agree at the corner (inverse_cdf saturates to 6.3 while cdf already treats ≥6.109 as 1.0). Reconcile: either align inverse_cdf saturation to the new CDF bound, or document the intentional divergence.

@bidzyyys bidzyyys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm

@immrsd

immrsd commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

Good catch — will fix this in #431 rather than here:

  • On current main the claim still holds: cdf's bound is 6.3 today.
  • Aligning now would break the corner instead of fixing it — today's cdf returns 0.999999999 at z = 6.109410205, so cdf(inverse_cdf(1)) would stop being 1 until Gaussian math: cdf and pdf improvements (Step-1) #431 lands.
  • Gaussian math: cdf and pdf improvements (Step-1) #431 already has to touch inverse_cdf (new horner_eval! parameter), so it will move the sentinel to 6.109410205 there (constants regenerated, fit unchanged) and add a test asserting inverse_cdf_coefficients::max_z_raw() == cdf_coefficients::max_z_raw() and cdf(inverse_cdf(1)) == 1 — so the two bounds can't drift apart again.

@ericnordelo ericnordelo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@immrsd immrsd merged commit 42a3b91 into main Jul 3, 2026
32 checks passed
@immrsd immrsd deleted the fp-impl-inverse-cdf branch July 3, 2026 09:20
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.

[Feature]: inverse_cdf for fixed-point math library

3 participants