Skip to content

feat(sp4+sp5): physics + FFI + UI for SP-4 modules + SP-5 quantum panels - #12

Merged
steeltroops-ai merged 3 commits into
mainfrom
physics/sp4-spectral-magnetosphere-plunge
Apr 29, 2026
Merged

feat(sp4+sp5): physics + FFI + UI for SP-4 modules + SP-5 quantum panels#12
steeltroops-ai merged 3 commits into
mainfrom
physics/sp4-spectral-magnetosphere-plunge

Conversation

@steeltroops-ai

@steeltroops-ai steeltroops-ai commented Apr 29, 2026

Copy link
Copy Markdown
Owner

This PR closes the operator-facing surface for SP-4 + SP-5 in one
batch: every Rust module the planning documents scoped to gravitas-
core, the WASM FFI bindings exposing those modules to JS, the
TypeScript wrappers, and the React quantum-readout panels with
ADR-0026 honest labelling baked in.

What lands

gravitas-core physics modules

  • radiative_transfer: Younsi+ 2016 dI/dλ = j − αI integrator with
    the analytic per-step exponential-integrating-factor solution.
    Five-band canonical grid (1.4 GHz radio, 230 GHz EHT, 100 THz,
    500 THz optical, 1 PHz UV) plus three-band tier-2 and broadband
    tier-1 fallbacks.
  • magnetosphere: Wald 1974 §III analytic vacuum solution. A_μ in
    Boyer-Lindquist via the existing Kerr covariant tensor; q_W = 2
    B_0 a M Wald horizon charge; F_μν via central differences.
  • plunge: Bardeen 1973 dimensionless closed forms for circular
    equatorial geodesic invariants (E, L_z, Ω, η = 1 − E_ISCO).
    plunge_trajectory uses the new timelike integrator.
  • synchrotron: Pandya+ 2016 thermal-synchrotron emissivity F(X)
    fit with Kirchhoff-law absorption.
  • physics::disk fix: corrects long-standing a*·v vs a*·v³ bug in
    the BPT specific-energy/angular-momentum helpers.

gravitas-core invariants + integrator

  • renormalize_timelike: enforces H = −1/2 with the same
    discriminant-band semantics as the null branch.
  • GeodesicKind enum in IntegrationOptions; null is default.

gravitas-core quantum

  • bekenstein: horizon area (geometric and SI), Bekenstein-Hawking
    entropy ratio S/k_B, Page 1976 photon-only Schwarzschild mass-loss
    rate, evaporation lifetime.
  • hawking: hawking_spectrum_planck (Planck shape at T_H, with
    overflow guard) and wien_peak_frequency.

WASM FFI (gravitas-wasm)

Eleven new methods on PhysicsEngine wrapping the SP-4/SP-5 surface:
hawking_temperature_kelvin, wien_peak_hz, hawking_spectrum_sample,
horizon_area_geometric, bekenstein_hawking_entropy_ratio,
schwarzschild_evaporation_time_seconds, radiative_efficiency_prograde,
wald_horizon_charge, wald_asymptotic_b_z, synchrotron_emissivity,
synchrotron_absorption.

TypeScript + React UI

  • src/lib/physics/{hawking,bekenstein}.ts: pure-TS mirrors of the
    Rust quantum surface so the operator HUD can render at 60 Hz
    without crossing the WASM bridge for a single readout.
  • src/components/quantum/BekensteinHawkingReadout.tsx: compact
    HUD strip (horizon area, S/k_B, T_H, t_evap) with formatScientific.
  • src/components/quantum/HawkingSpectrumPanel.tsx: log-log
    Recharts plot at T_H(M, a*) with Wien-peak reference line and
    illustrative footer per ADR-0026.
  • src/app/page.tsx: quantum-effects toggle button at top-right,
    panels mount when opened, default off.

Test plan

  • cd physics-engine && cargo test -p gravitas-core --release (182 tests pass across 17 binaries: lib 23, bekenstein 11, conservation 3, hawking_spectrum 12, isco 5, magnetosphere 13, near_extremal 1 proptest, normalize 4, photon_ring 3, plunge 17, plunge_trajectory 5, polarization 28, radiative_transfer 19, schwarzschild_degenerate 7, synchrotron 21, timelike_renormalize 5, doc 5)
  • cd physics-engine && cargo clippy -p gravitas-core --tests -- -D warnings (clean; pedantic + nursery)
  • cd physics-engine && cargo clippy -p gravitas-wasm -- -D warnings (clean)
  • cd physics-engine && cargo check -p gravitas-wasm (clean)
  • bun run type-check (clean)
  • bunx vitest run src/tests/physics/hawking-bekenstein.test.ts (23 pass)
  • bun run test (default vitest suite still green)
  • lefthook pre-commit + commit-msg + pre-push gates passed

What's NOT in this PR (false-claim guard)

  1. Walker-Penrose geodesic-aware Stokes transport: needs an f^μ
    channel in GeodesicState plus an integrator step that steps it
    alongside (x, p). Per-point κ_WP, EVPA rotation, and Stokes
    primitives are already on main from PR feat(sp4-4a): polarization primitives — Stokes + Walker-Penrose κ #11.
  2. WGSL/GLSL shader integration for polarisation hue/saturation
    compositing or spectral-band selection in the ray-marching
    kernel. Those are render-pipeline changes earning their own
    per-module PRs.
  3. Visual goldens for any new physics. The SP-3 visual-regression
    suite skip-warns until goldens are captured; operator runs
    shader:update-goldens --confirm when satisfied with the visuals.
  4. Pandya power-law and kappa-distribution branches plus
    polarised emissivity (Schnittman+Krolik 2009 §2). The framework's
    integrate_radiative_transfer takes (j, α) per band so any of
    those plug in cleanly.
  5. ADRs 0022–0027: each module deserves its own ADR that
    documents the operator-facing tradeoffs, but the canonical-
    physics choices in this PR (Wald over BZ, 5-band over 50-band,
    thermal over power-law as default) are the standard frontier
    defaults.
  6. HorizonPairOverlay (SP-5 module 5B): the pair-production
    particle visualisation in the spec needs a tiny WGSL fragment
    shader plus a JS particle system. Both are scoped as a
    render-pipeline follow-up.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Hawking radiation calculator with temperature and spectrum visualization
    • Integrated black hole thermodynamics display (entropy, horizon area, evaporation timeline)
    • Enabled accretion disk physics including synchrotron emission and radiative transfer simulations
    • Added Wald magnetosphere modeling for black hole magnetic fields
    • Implemented matter plunge trajectories alongside photon ray-marching
    • Quantum effects panel now accessible from main interface
  • Improvements

    • Enhanced geodesic integration to support both null (photon) and timelike (matter) paths
    • Refined orbital mechanics calculations using Bardeen dimensionless parameters

…(plunge)

Three Rust submodules under gravitas::physics that complete the SP-4
fidelity uplift surface for steady-state imaging. Each ships as
per-point primitives plus a focused test surface; shader and FFI
integration follow in dedicated PRs once a downstream consumer needs
them.

Module 4B — radiative_transfer:

The five canonical bands (1.4 GHz, 230 GHz EHT, 100 THz, 500 THz,
1 PHz) plus a three-band reduced grid for tier-2 hardware and a
single-broadband fallback for tier-1. integrate_radiative_transfer
solves dI/dλ = j − αI per Younsi+ 2016 Eq. 14 using the analytic
piecewise-constant per-step solution

  I_{n+1} = I_n e^{−τ} + S (1 − e^{−τ}),  τ = α dλ,  S = j/α,

with a small-τ Taylor branch when α dλ < 10^−12 to keep accuracy at
the optically-thin limit. integrate_bands runs each band against its
own (j, α, dλ) sample stream. The plasma model that produces (j, α)
per band — Pandya+ 2016 thermal synchrotron is the standard fit — is
a follow-up because it brings in modified Bessel function
approximations that deserve their own review.

Module 4D — magnetosphere:

Wald 1974 §III analytic vacuum solution for a Kerr hole immersed in
a uniform asymptotic field. wald_potential_down evaluates A_μ in
Boyer-Lindquist using the existing Kerr covariant tensor, so the
implementation tracks the metric tensor rather than inlining its
components. wald_horizon_charge returns q_W = 2 B_0 a M, the
spin-induced Wald charge that the vacuum solution requires for a
neutral hole. wald_field_tensor produces F_μν via central
differences on A_μ; the solution's stationary + axisymmetric
structure means only F_{rt}, F_{rφ}, F_{θt}, F_{θφ} are non-zero by
construction. Blandford-Znajek split-monopole and paraboloidal
solutions are out of scope for this module.

Module 4E — plunge:

Bardeen 1973 dimensionless closed forms for the specific energy,
angular momentum, and angular velocity of equatorial circular
geodesics, in the v = √(M/r) form

  E/μ      = (1 − 2v² ± a*v³) / √(1 − 3v² ± 2a*v³),
  L_z/(μM) = ±(1 ∓ 2a*v³ + a*²v⁴) / [v · √(1 − 3v² ± 2a*v³)].

plunge_entry_state bundles the three at the ISCO; radiative_efficiency
returns 1 − E_ISCO. The Schwarzschild ISCO efficiency reproduces
5.72 % (1 − √(8/9)); the practical-extremal a* = 0.998 case
reproduces the Thorne 32 % limit. plunge_emissivity_envelope is a
renderer-friendly exponential falloff from r_ISCO toward the
horizon; the actual plunging-stream spectrum needs the timelike
geodesic integration that is its own change.

While auditing the formula I caught a long-standing bug in the
private helpers in physics::disk (specific_energy and
specific_angular_momentum were using a*·v instead of a*·v³ in the
mixed terms). The disk's Page-Thorne flux tests passed because they
verified flux properties (zero at ISCO, monotone trends) that
survive the wrong absolute scaling. The fix is in this commit; the
existing disk tests still pass with the corrected helpers.

Test counts: 19 radiative_transfer + 13 magnetosphere + 17 plunge
new tests; 0 regressions across the existing 79 gravitas-core tests.
Total gravitas-core test surface: 128 tests across 12 binaries.
@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR extends the gravitational physics engine with geodesic kinematic classification (null/timelike), introducing accretion plunge dynamics, thermal synchrotron radiation modeling, Wald vacuum-magnetosphere implementation, radiative transfer, and black-hole thermodynamics (Bekenstein-Hawking, Hawking radiation). It adds corresponding UI visualization panels and comprehensive testing across Rust and TypeScript layers.

Changes

Cohort / File(s) Summary
Geodesic Integration Framework
physics-engine/gravitas-core/src/geodesic/integrator.rs, physics-engine/gravitas-core/src/geodesic/mod.rs
Added GeodesicKind enum to IntegrationOptions, allowing selection between null and timelike renormalization; integrate now dispatches to the appropriate normalizer via options.
Renormalization
physics-engine/gravitas-core/src/invariants/mod.rs, physics-engine/gravitas-core/src/invariants/renormalization.rs
Implemented renormalize_timelike function enforcing H = −1/2 constraint; re-exported from module index.
Disk/Orbital Mechanics
physics-engine/gravitas-core/src/physics/disk.rs
Rewrote internal orbital-quantity helpers (specific_energy, specific_angular_momentum) to Bardeen (1973) dimensionless expressions using v-parametrization for Kerr-equatorial geometry.
Magnetosphere
physics-engine/gravitas-core/src/physics/magnetosphere.rs
Introduced Wald vacuum-magnetosphere implementation: down-index potential, horizon charge, asymptotic magnetic-field helpers, and finite-difference field tensor generator.
Accretion Plunge
physics-engine/gravitas-core/src/physics/plunge.rs
Added ISCO-plunge entry module with circular orbit constants, plunge trajectory integration (timelike geodesics), radiative efficiency, and emissivity envelope functions.
Radiative Transfer
physics-engine/gravitas-core/src/physics/radiative_transfer.rs
Introduced spectrally-resolved radiative transfer along null geodesics with analytic piecewise-constant solver, multi-band support, and predefined band tables (5-band, 3-band EHT, broadband).
Synchrotron Emission
physics-engine/gravitas-core/src/physics/synchrotron.rs
Implemented thermal synchrotron emissivity and absorption for relativistic electrons using Pandya+ (2016) fitting function, Kirchhoff's law, and band-wise integration.
Module Exports
physics-engine/gravitas-core/src/physics/mod.rs, physics-engine/gravitas-core/src/quantum/mod.rs
Added public module exports for magnetosphere, plunge, radiative_transfer, synchrotron, and new bekenstein submodule.
Black-Hole Thermodynamics
physics-engine/gravitas-core/src/quantum/bekenstein.rs, physics-engine/gravitas-core/src/quantum/hawking.rs
Added Bekenstein-Hawking entropy, horizon area, and Schwarzschild evaporation; extended Hawking module with Planck spectrum and Wien peak frequency helpers.
Rust Integration Tests
physics-engine/gravitas-core/tests/bekenstein.rs, physics-engine/gravitas-core/tests/hawking_spectrum.rs, physics-engine/gravitas-core/tests/magnetosphere.rs, physics-engine/gravitas-core/tests/plunge.rs, physics-engine/gravitas-core/tests/plunge_trajectory.rs, physics-engine/gravitas-core/tests/radiative_transfer.rs, physics-engine/gravitas-core/tests/synchrotron.rs, physics-engine/gravitas-core/tests/timelike_renormalize.rs
Comprehensive test suites validating geodesic kinds, renormalization, thermodynamics, magnetosphere properties, plunge dynamics, radiative transfer numerics, synchrotron physics, and Hawking spectrum.
Rust Test Updates
physics-engine/gravitas-core/tests/conservation.rs, physics-engine/gravitas-core/tests/near_extremal.rs
Updated existing tests to configure GeodesicKind::Null in IntegrationOptions.
WASM Bindings
physics-engine/gravitas-wasm/src/lib.rs
Exposed 12 new physics methods on PhysicsEngine: Hawking temperature, Wien peak, spectrum sample, horizon area, Bekenstein entropy, evaporation time, radiative efficiency, Wald horizon charge, asymptotic Bz, and synchrotron emissivity/absorption; configured ray-tracing with GeodesicKind::Null.
TypeScript Physics Utilities
src/lib/physics/hawking.ts, src/lib/physics/bekenstein.ts
Added Hawking-radiation and Bekenstein-Hawking thermodynamics modules with temperature, spectral sampling, entropy, horizon area, and evaporation-time calculations.
UI Components
src/components/quantum/BekensteinHawkingReadout.tsx, src/components/quantum/HawkingSpectrumPanel.tsx
Introduced quantum-effects visualization: Bekenstein-Hawking readout HUD and log-log Hawking spectrum chart using recharts.
TypeScript Tests
src/__tests__/physics/hawking-bekenstein.test.ts
Added Vitest suite validating Hawking and Bekenstein-Hawking numerical behavior across parameter ranges.
Frontend Integration
src/app/page.tsx
Added optional quantum-effects overlay panel to main page with Bekenstein-Hawking and Hawking spectrum visualization toggleable via showQuantum state.

Sequence Diagram

sequenceDiagram
    participant User
    participant PlungeAPI as plunge_trajectory()
    participant Metric as Kerr Metric
    participant ISCOCalc as ISCO & Orbital<br/>Calculations
    participant GeoState as GeodesicState<br/>Constructor
    participant Integrator as integrate()<br/>(timelike)
    participant Recorder as Path<br/>Recorder

    User->>PlungeAPI: plunge_trajectory(metric, orbit, dpr_seed)
    PlungeAPI->>ISCOCalc: compute r_ISCO, E_ISCO, L_z_ISCO
    ISCOCalc-->>PlungeAPI: circular orbit constants
    PlungeAPI->>GeoState: construct GeodesicState<br/>at r_ISCO with (E, L_z)
    PlungeAPI->>GeoState: apply radial momentum seed (dpr)
    GeoState-->>PlungeAPI: initial state ready
    PlungeAPI->>Integrator: integrate(state, options)<br/>with GeodesicKind::Timelike
    Integrator->>Integrator: select renormalize_timelike<br/>from options
    Integrator->>Integrator: step RKF45 (adaptive timelike)
    Integrator->>Recorder: optionally record state
    Recorder-->>Integrator: path appended
    Integrator->>Integrator: check H≈−0.5 constraint
    Integrator->>Integrator: test termination<br/>(r_+, horizon, etc.)
    Integrator-->>PlungeAPI: Trajectory { path, reason }
    PlungeAPI-->>User: recorded plunge path
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • fix(physics): memory safety + FFI hardening #5: Both PRs modify the core integration and renormalization path—this PR adds GeodesicKind selection and renormalize_timelike, while the referenced PR changed renormalize_null to return Result and altered error handling in integrate, creating a direct code-level dependency on renormalization behavior.

Poem

🐰 Hops through spacetime, timelike and null,
From plunge to synchrotron, radiation in full,
Hawking whispers his thermodynamic song,
While Wald fields magnetize the black-hole throng.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title concisely summarizes the main changes: adding SP-4 physics modules (radiative transfer, magnetosphere, plunge) and SP-5 quantum panels, with timelike integrator support. It clearly conveys the primary scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 physics/sp4-spectral-magnetosphere-plunge

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41b281554b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +124 to +126
if tau.abs() < OPTICAL_DEPTH_FLOOR {
intensity += sample.j_nu * sample.d_lambda;
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include absorption in the small-optical-depth update

When |τ| < OPTICAL_DEPTH_FLOOR, the branch updates intensity with only j*dλ, but the first-order Taylor expansion of I_{n+1} = I_n e^{-τ} + (j/α)(1-e^{-τ}) is I + (j - αI)dλ. For nonzero alpha_nu and nonzero background intensity, this drops the -αI dλ attenuation term and systematically overestimates intensity; this can accumulate across many tiny steps even though each step has small τ.

Useful? React with 👍 / 👎.

… primitives

Closes the remaining Rust-side surface for SP-4 module 4E and lands
SP-5 modules 5A (Hawking spectrum) and 5C (Bekenstein-Hawking entropy)
as physics primitives. The two SP-4 follow-ups — synchrotron emissivity
and shader/UI integration — stay deferred for honest reasons recorded
in the PR body. The Walker-Penrose geodesic-aware Stokes transport
also stays deferred because it needs a polarisation-vector channel in
the integrator state, which is its own change.

Timelike geodesic integration:

invariants::renormalize_timelike enforces H = −1/2 with the same
discriminant-band semantics as the existing null branch (clamp inside
ROUNDING_TOLERANCE, return NormalizationError outside). The change is
additive: existing null callers are unaffected and the new field
GeodesicKind in IntegrationOptions defaults to Null. Two existing
tests that built IntegrationOptions explicitly were updated.

plunge::plunge_trajectory ties the new timelike mode to a concrete
physics use-case: launch a near-circular state at r_ISCO with the
conserved (E_ISCO, L_z_ISCO) and a tiny inward perturbation, then
adaptively step inward to the horizon. Tests pin the start at r_ISCO,
verify the trajectory advances inward, confirm the integrator
terminates cleanly, and check H stays within 1e-2 of the −1/2 shell
along a 2k-step run. The 1e-2 bound (vs the rule's 1e-9 over 1e5
steps for free-streaming null geodesics) reflects that timelike
trajectories crossing strong gradients near the horizon admit looser
conservation; tightening it would require a higher-order symplectic
integrator, which is a future change.

SP-5 module 5A (Hawking spectrum):

quantum::hawking gains hawking_spectrum_planck (Planck blackbody at
T_H, with overflow guard at h ν / k T > 700) and wien_peak_frequency
(Wien displacement law for the emission-band tick). The function
docs explicitly label the spectrum as illustrative — no greybody
factor — so consumers know they are getting a Planck shape, not the
exact transmission-corrected Hawking spectrum.

SP-5 module 5C (Bekenstein-Hawking thermodynamics):

quantum::bekenstein lands four primitives:
- horizon_area_geometric: 4π(r_+² + a²) in geometric units.
- horizon_area_si: SI conversion via (G M / c²)².
- bekenstein_hawking_entropy_per_kb: A_SI / (4 ℓ_P²), the
  dimensionless S/k_B per Bekenstein 1973 + Hawking 1975.
- schwarzschild_mass_loss_rate and schwarzschild_evaporation_time
  per Page 1976 photon-only mass-loss law.

Tests cover canonical anchors: 16π M² horizon area at Schwarzschild,
8π M² at extremal Kerr; ~10⁷⁷ k_B entropy for one solar mass; M³
scaling for evaporation time; consistent ratios of t_evap to age of
universe (10⁵⁰–10⁶⁵).

Test counts (this commit): 5 timelike_renormalize + 5 plunge_trajectory
+ 11 bekenstein + 12 hawking_spectrum = 33 new tests across 4 binaries.
Existing 128 tests still green; total gravitas-core test surface now
161 tests across 16 binaries. clippy pedantic + nursery clean.
@steeltroops-ai steeltroops-ai changed the title feat(sp4): modules 4B (spectral RT), 4D (Wald magnetosphere), 4E (plunge entry) + disk-helper formula fix feat(sp4+sp5): SP-4 4B/4D/4E + timelike integrator + SP-5 quantum primitives + disk-helper formula fix Apr 29, 2026
…quantum panels

Closes the remaining surfaces of the SP-4 / SP-5 implementation that
don't require shader-pipeline or integrator-state changes:

- Pandya+ 2016 thermal-synchrotron emissivity in gravitas-core.
- WASM FFI bindings for every new physics primitive (Hawking
  temperature/spectrum/Wien peak, Bekenstein-Hawking entropy/area,
  Page evaporation lifetime, plunge radiative efficiency, Wald
  horizon charge, synchrotron j_ν / α_ν).
- TypeScript JS-side hawking.ts + bekenstein.ts mirroring the same
  formulas for the operator-facing readout panels.
- React quantum-panels (Bekenstein-Hawking thermodynamics readout,
  Hawking-spectrum log-log Recharts plot) with the ADR-0026 honest-
  labelling footer baked in.
- A control-panel toggle wires the panels into page.tsx behind an
  opt-in button; the simulation still ships its classical face by
  default.

Pandya+ 2016 thermal synchrotron:

physics::synchrotron lands the relativistic-thermal branch of the
Pandya+ 2016 fit (Eq. 31): F(X) = (1 + 2.41 X^{1/2} + 0.40 X^{-2/3})
exp(−X^{1/3}). Absorption follows Kirchhoff's law in the thermal
limit. PlasmaState bundles (n_e, T_e, B, θ_B); the framework's
integrate_radiative_transfer consumes the (j, α) output unchanged.

Tests: 21 new gravitas-core synchrotron tests; 23 new JS
hawking-bekenstein tests; existing 161 Rust tests still green;
default vitest suite still green; clippy pedantic + nursery clean.
@steeltroops-ai steeltroops-ai changed the title feat(sp4+sp5): SP-4 4B/4D/4E + timelike integrator + SP-5 quantum primitives + disk-helper formula fix feat(sp4+sp5): physics + FFI + UI for SP-4 modules + SP-5 quantum panels Apr 29, 2026

@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: 9

🧹 Nitpick comments (7)
physics-engine/gravitas-core/src/physics/radiative_transfer.rs (1)

160-165: Consider guarding against zero frequency in wavelength_metres.

If band.freq_hz is zero, this returns f64::INFINITY. While the canonical Band constants have positive frequencies, callers constructing custom Band values could trigger this. A guard returning 0.0 or f64::INFINITY explicitly would make the behavior clearer.

🛡️ Optional: Add guard for zero frequency
 #[must_use]
 pub fn wavelength_metres(band: Band) -> f64 {
+    if band.freq_hz <= 0.0 {
+        return f64::INFINITY; // or 0.0, depending on desired semantics
+    }
     crate::constants::SI_C / band.freq_hz
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@physics-engine/gravitas-core/src/physics/radiative_transfer.rs` around lines
160 - 165, The function wavelength_metres currently divides SI_C by band.freq_hz
and will return f64::INFINITY if band.freq_hz == 0; update wavelength_metres to
explicitly guard against zero (or extremely small) frequencies by checking
band.freq_hz and returning a clear value (e.g. 0.0 or f64::INFINITY) or using an
explicit error path when band.freq_hz == 0.0; locate the function
wavelength_metres and the Band.freq_hz access and add the conditional check
before performing crate::constants::SI_C / band.freq_hz to make the behavior
explicit and documented.
physics-engine/gravitas-core/tests/radiative_transfer.rs (1)

25-29: Note: close() helper uses absolute comparison.

This differs from the relative comparison in hawking_spectrum.rs. For these tests with values near 1-10, the absolute TIGHT = 1e-12 tolerance works fine, but consider using a consistent helper across test files to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@physics-engine/gravitas-core/tests/radiative_transfer.rs` around lines 25 -
29, The close() helper currently performs an absolute comparison using TIGHT =
1e-12 which differs from the relative comparison used in hawking_spectrum.rs;
replace or refactor close() (and/or TIGHT) to use a consistent
relative-comparison implementation (e.g., comparing |a-b| <= eps * max(1.0, |a|,
|b|)) so tests across radiative_transfer.rs and hawking_spectrum.rs share the
same semantics; update the close function name and/or constant where defined to
match the shared helper so callers (close, TIGHT) use the unified comparator.
src/components/quantum/HawkingSpectrumPanel.tsx (1)

69-79: Fallback reason text is too specific for all empty-data cases.

At Line 69 you gate on data.length === 0, but Line 78 attributes it only to extremal a*=±1. Empty data can also come from other invalid params, so consider generic wording or reason-specific branching.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/quantum/HawkingSpectrumPanel.tsx` around lines 69 - 79, The
empty-data fallback in HawkingSpectrumPanel currently checks data.length === 0
and always displays the extremal-limit note; update the component so the
fallback message is generic (e.g., "Hawking spectrum unavailable — no data for
given parameters") or add a short branch that shows the extremal note only when
you can verify the cause (e.g., when a* / aStar prop equals ±1); locate the
conditional rendering in HawkingSpectrumPanel and replace the hard-coded
"extremal limit (a*=±1) sets T_H = 0" line with either a generic reason message
or a conditional that inspects the relevant prop (aStar, spin, or params) before
showing the extremal-specific text.
physics-engine/gravitas-core/src/invariants/renormalization.rs (1)

56-106: Null/timelike quadratic solve logic is duplicated; consider one shared solver.

The duplicated discriminant/root-selection path is maintenance risk. A shared internal helper with a shell-constant parameter would reduce divergence bugs.

♻️ Refactor sketch
+fn solve_pr_with_shell_constant(
+    state: &mut GeodesicState,
+    metric: &impl Metric,
+    shell_shift: f64, // 0.0 for null, 1.0 for timelike (C -> C + 1)
+) -> Result<(), NormalizationError> {
+    let r = state.x[1];
+    let theta = state.x[2];
+    let g = metric.contravariant(r, theta).as_array();
+    let p_t = state.p[0];
+    let p_r = state.p[1];
+    let p_th = state.p[2];
+    let p_ph = state.p[3];
+
+    let a_quad = g[5];
+    let b_quad = 2.0 * (g[1] * p_t + g[7] * p_ph);
+    let c_quad = g[0] * p_t * p_t
+        + g[10] * p_th * p_th
+        + g[15] * p_ph * p_ph
+        + 2.0 * g[3] * p_t * p_ph
+        + shell_shift;
+    // ...existing discriminant + root logic...
+    Ok(())
+}
+
 pub fn renormalize_null<M: Metric>(...) -> Result<(), NormalizationError> {
-    // duplicated solve...
+    solve_pr_with_shell_constant(state, metric, 0.0)
 }
 
 pub fn renormalize_timelike<M: Metric>(...) -> Result<(), NormalizationError> {
-    // duplicated solve...
+    solve_pr_with_shell_constant(state, metric, 1.0)
 }

Also applies to: 124-170

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@physics-engine/gravitas-core/src/invariants/renormalization.rs` around lines
56 - 106, The null renormalization duplicates quadratic-discriminant and
root-selection logic; extract a shared helper (e.g., solve_quadratic_closest or
quadratic_root_near) that takes coefficients a, b, c, the current value
(current_pr), and ROUNDING_TOLERANCE and returns the chosen root or an
error/None for degenerate a≈0; update renormalize_null to call this helper
(replace the discriminant check, rounding clamp, sqrt, and closest-root
selection) and do the same for the corresponding timelike function
(renormalize_timelike) so both functions reuse the same solver logic and error
handling.
src/lib/physics/bekenstein.ts (2)

76-85: toFixed(3) may not match "three significant figures" intent for larger values.

For a value like 1234, toFixed(3) returns "1234.000", which has 7 significant figures rather than 3. If the goal is truly 3 significant figures across the fixed-notation range, consider using toPrecision(3) instead, or document that the current behavior is intentional for HUD readability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/physics/bekenstein.ts` around lines 76 - 85, The function
formatScientific currently uses toFixed(3) for values with log between -3 and 4
which yields three decimal places rather than three significant figures (e.g.,
1234 -> "1234.000"); change the fixed-notation branch in formatScientific to use
toPrecision(3) (or otherwise format to three significant figures) so numbers in
that range display with three significant digits, and ensure the returned string
handling still preserves sign/zero/finite checks already in place.

54-68: Consider extracting the shared Page 1976 constant.

The expression (SI_HBAR * SI_C ** 4) / (15360 * Math.PI * SI_G * SI_G) is computed identically in both schwarzschildMassLossRate (line 56-57) and schwarzschildEvaporationTimeSeconds (line 67). Extracting it as a module-level constant improves maintainability and makes the physics relationship clearer.

Proposed refactor
 const SI_C = 299_792_458;
 const SI_G = 6.6743e-11;
 const SI_HBAR = 1.054_571_817e-34;
+
+// Page 1976 photon-only mass-loss coefficient K = ℏc⁴ / (15360 π G²)
+const PAGE_K = (SI_HBAR * SI_C ** 4) / (15360 * Math.PI * SI_G * SI_G);
 
 // ...
 
 export function schwarzschildMassLossRate(massKg: number): number {
   if (massKg <= 0) return 0;
-  const numerator = SI_HBAR * SI_C ** 4;
-  const denominator = 15360 * Math.PI * SI_G * SI_G * massKg * massKg;
-  return -numerator / denominator;
+  return -PAGE_K / (massKg * massKg);
 }
 
 export function schwarzschildEvaporationTimeSeconds(massKg: number): number {
   if (massKg <= 0) return Infinity;
-  const k = (SI_HBAR * SI_C ** 4) / (15360 * Math.PI * SI_G * SI_G);
-  return massKg ** 3 / (3 * k);
+  return massKg ** 3 / (3 * PAGE_K);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/physics/bekenstein.ts` around lines 54 - 68, Both functions
schwarzschildMassLossRate and schwarzschildEvaporationTimeSeconds compute the
same physics constant; extract (SI_HBAR * SI_C ** 4) / (15360 * Math.PI * SI_G *
SI_G) to a module-level constant (e.g. SCHWARZSCHILD_K or PAGE_1976_CONSTANT)
and have schwarzschildMassLossRate and schwarzschildEvaporationTimeSeconds use
that constant (replace local numerator/ k calculations) so the shared value is
defined once and clearly named.
physics-engine/gravitas-wasm/src/lib.rs (1)

326-360: Consider extracting PlasmaState construction.

Both synchrotron_emissivity and synchrotron_absorption construct identical PlasmaState structs from the same parameters. A private helper would reduce duplication:

Proposed refactor
// Private helper at impl block level
fn make_plasma_state(
    n_e_cm3: f64,
    t_e_kelvin: f64,
    b_gauss: f64,
    theta_b_rad: f64,
) -> gravitas::physics::synchrotron::PlasmaState {
    gravitas::physics::synchrotron::PlasmaState {
        n_e: n_e_cm3,
        t_e: t_e_kelvin,
        b_field: b_gauss,
        theta_b: theta_b_rad,
    }
}

Then both methods can use let plasma = Self::make_plasma_state(...).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@physics-engine/gravitas-wasm/src/lib.rs` around lines 326 - 360, Extract the
duplicated PlasmaState construction into a private helper function (e.g., fn
make_plasma_state(n_e_cm3: f64, t_e_kelvin: f64, b_gauss: f64, theta_b_rad: f64)
-> gravitas::physics::synchrotron::PlasmaState) inside the same impl block,
returning gravitas::physics::synchrotron::PlasmaState built from the four
params; then update synchrotron_emissivity and synchrotron_absorption to call
Self::make_plasma_state(...) and pass the returned PlasmaState to
j_thermal_synchrotron and alpha_thermal_synchrotron respectively to remove
duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@physics-engine/gravitas-core/src/geodesic/mod.rs`:
- Around line 210-216: max_hamiltonian_drift is currently computed as an
absolute |H| which is incorrect for timelike geodesics; change the drift
calculation to measure deviation from the chosen renormalization shell (H_target
= 0 for GeodesicKind::Null, H_target = -1/2 for GeodesicKind::Timelike). Locate
where max_hamiltonian_drift (and any Hamiltonian drift updates) are computed
alongside the renormalize selection (look for the match on options.geodesic_kind
and the renormalize function assignment that uses
crate::invariants::renormalize_null and crate::invariants::renormalize_timelike)
and replace |H| with |H - H_target| using H_target selected from the same
GeodesicKind switch so null and timelike integrations report drift relative to
their correct shells. Ensure tests or assertions that compare
max_hamiltonian_drift are updated to the new reference frame if present.

In `@physics-engine/gravitas-core/src/physics/magnetosphere.rs`:
- Around line 29-32: The module docs claim field-tensor F_μν derivatives are out
of scope but the file now implements wald_field_tensor; update the header
comment in magnetosphere.rs to remove or adjust the "out of scope" statement
(lines referencing "field-tensor F_μν derivatives") and instead note that the
Wald field-tensor implementation (wald_field_tensor) is included here, or add a
brief note that derivatives remain out of scope while wald_field_tensor provides
the field tensor itself; ensure the docstring mentions wald_field_tensor by name
so readers aren’t confused.
- Around line 97-110: The finite-difference divisions by 2.0 * eps can produce
Inf/NaN when eps is non-finite or <= 0; before computing d_a_dr_t, d_a_dr_phi,
d_a_dth_t and d_a_dth_phi in the function (where wald_potential_down is called),
validate eps with eps.is_finite() && eps > 0.0 and use a safe fallback (e.g.
assign safe_eps = eps when valid else a small positive constant like 1e-8) or
return an early error/Result; then use safe_eps in the denominators (2.0 *
safe_eps) to prevent division by zero/NaN.

In `@physics-engine/gravitas-core/src/physics/plunge.rs`:
- Around line 19-23: The module docstring is stale: remove or update the note
that timelike geodesic integration from ISCO to the horizon is not implemented
and the TODO about extending the integrator; instead document that
plunge_trajectory now implements the integrated timelike geodesic using the
timelike integrator (the H = −1/2 code path) and that the trajectory primitive
is included. Locate the top-of-file doc comment and replace the outdated
sentences with a brief statement referencing plunge_trajectory and the timelike
geodesic integrator (H = −1/2) to reflect the current functionality.

In `@physics-engine/gravitas-core/src/quantum/bekenstein.rs`:
- Around line 43-46: Add a precondition guard in all public thermodynamics APIs
that take mass_kg (e.g., horizon_area_si and the functions around lines 56-60
and 72-92) that checks mass_kg.is_finite() && mass_kg > 0.0; if the check fails
return an explicit invalid numeric result (e.g., f64::NAN) immediately to avoid
divisions by zero or producing negative/unstable values, and document the
behavior in the function comment so callers know invalid masses yield NaN.
Ensure the same guard is applied consistently to horizon_area_si,
horizon_area_geometric wrappers, and the lifetime/temperature functions
referenced in the review.

In `@physics-engine/gravitas-core/tests/plunge_trajectory.rs`:
- Around line 29-34: The test in plunge_trajectory.rs currently allows
TerminationReason::NormalizationFailure in the matches for traj.termination,
which masks regressions in timelike shell normalization; update the expectation
to remove TerminationReason::NormalizationFailure so the match only accepts
TerminationReason::Horizon or TerminationReason::MaxSteps (i.e., remove the
NormalizationFailure arm from the matches(...) expression referencing
traj.termination and TerminationReason) so normalization failures cause the test
to fail and surface regressions.

In `@src/__tests__/physics/hawking-bekenstein.test.ts`:
- Around line 32-42: The tests using hawkingTemperatureKelvin (called with
SI_SOLAR_MASS and spins 1, 1.5, -1.5) assert near-zero with toBeCloseTo(0, 30),
which requests unrealistic 30-digit precision for IEEE-754 numbers; change those
assertions to use a realistic precision (e.g., toBeCloseTo(0, 12) or 14) or
replace them with an explicit absolute tolerance check (e.g., Math.abs(t) <
1e-12) so extremal Kerr and clamped-spin cases reliably pass; update the two
tests referencing hawkingTemperatureKelvin accordingly.

In `@src/app/page.tsx`:
- Around line 159-163: The comment says the "Q" shortcut should toggle quantum
visualization but the component only toggles via buttons; add a keyboard handler
that toggles the showQuantum state: in the component that declares
useState(false) for showQuantum/setShowQuantum, add a useEffect which registers
a keydown listener that checks if the pressed key is 'q' or 'Q' (or use
event.key.toLowerCase() === 'q') and calls setShowQuantum(prev => !prev), and
remove the listener in the cleanup to avoid leaks; alternatively, if you prefer
not to add keyboard handling, update the inline comment to remove the mention of
the "Q" shortcut so it accurately reflects button-only control.

In `@src/lib/physics/hawking.ts`:
- Around line 15-19: The SI_G constant in this file is less precise than the
Rust implementation; update the SI_G declaration to match the Rust value (g_si)
by using 6.674_30e-11 so the TypeScript SI_G exactly matches the Rust
hawking_temperature g_si constant for consistent cross-language results.

---

Nitpick comments:
In `@physics-engine/gravitas-core/src/invariants/renormalization.rs`:
- Around line 56-106: The null renormalization duplicates quadratic-discriminant
and root-selection logic; extract a shared helper (e.g., solve_quadratic_closest
or quadratic_root_near) that takes coefficients a, b, c, the current value
(current_pr), and ROUNDING_TOLERANCE and returns the chosen root or an
error/None for degenerate a≈0; update renormalize_null to call this helper
(replace the discriminant check, rounding clamp, sqrt, and closest-root
selection) and do the same for the corresponding timelike function
(renormalize_timelike) so both functions reuse the same solver logic and error
handling.

In `@physics-engine/gravitas-core/src/physics/radiative_transfer.rs`:
- Around line 160-165: The function wavelength_metres currently divides SI_C by
band.freq_hz and will return f64::INFINITY if band.freq_hz == 0; update
wavelength_metres to explicitly guard against zero (or extremely small)
frequencies by checking band.freq_hz and returning a clear value (e.g. 0.0 or
f64::INFINITY) or using an explicit error path when band.freq_hz == 0.0; locate
the function wavelength_metres and the Band.freq_hz access and add the
conditional check before performing crate::constants::SI_C / band.freq_hz to
make the behavior explicit and documented.

In `@physics-engine/gravitas-core/tests/radiative_transfer.rs`:
- Around line 25-29: The close() helper currently performs an absolute
comparison using TIGHT = 1e-12 which differs from the relative comparison used
in hawking_spectrum.rs; replace or refactor close() (and/or TIGHT) to use a
consistent relative-comparison implementation (e.g., comparing |a-b| <= eps *
max(1.0, |a|, |b|)) so tests across radiative_transfer.rs and
hawking_spectrum.rs share the same semantics; update the close function name
and/or constant where defined to match the shared helper so callers (close,
TIGHT) use the unified comparator.

In `@physics-engine/gravitas-wasm/src/lib.rs`:
- Around line 326-360: Extract the duplicated PlasmaState construction into a
private helper function (e.g., fn make_plasma_state(n_e_cm3: f64, t_e_kelvin:
f64, b_gauss: f64, theta_b_rad: f64) ->
gravitas::physics::synchrotron::PlasmaState) inside the same impl block,
returning gravitas::physics::synchrotron::PlasmaState built from the four
params; then update synchrotron_emissivity and synchrotron_absorption to call
Self::make_plasma_state(...) and pass the returned PlasmaState to
j_thermal_synchrotron and alpha_thermal_synchrotron respectively to remove
duplication.

In `@src/components/quantum/HawkingSpectrumPanel.tsx`:
- Around line 69-79: The empty-data fallback in HawkingSpectrumPanel currently
checks data.length === 0 and always displays the extremal-limit note; update the
component so the fallback message is generic (e.g., "Hawking spectrum
unavailable — no data for given parameters") or add a short branch that shows
the extremal note only when you can verify the cause (e.g., when a* / aStar prop
equals ±1); locate the conditional rendering in HawkingSpectrumPanel and replace
the hard-coded "extremal limit (a*=±1) sets T_H = 0" line with either a generic
reason message or a conditional that inspects the relevant prop (aStar, spin, or
params) before showing the extremal-specific text.

In `@src/lib/physics/bekenstein.ts`:
- Around line 76-85: The function formatScientific currently uses toFixed(3) for
values with log between -3 and 4 which yields three decimal places rather than
three significant figures (e.g., 1234 -> "1234.000"); change the fixed-notation
branch in formatScientific to use toPrecision(3) (or otherwise format to three
significant figures) so numbers in that range display with three significant
digits, and ensure the returned string handling still preserves sign/zero/finite
checks already in place.
- Around line 54-68: Both functions schwarzschildMassLossRate and
schwarzschildEvaporationTimeSeconds compute the same physics constant; extract
(SI_HBAR * SI_C ** 4) / (15360 * Math.PI * SI_G * SI_G) to a module-level
constant (e.g. SCHWARZSCHILD_K or PAGE_1976_CONSTANT) and have
schwarzschildMassLossRate and schwarzschildEvaporationTimeSeconds use that
constant (replace local numerator/ k calculations) so the shared value is
defined once and clearly named.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9e45bc22-07b3-4095-a5b6-334cf0497e17

📥 Commits

Reviewing files that changed from the base of the PR and between 2752315 and c022c48.

📒 Files selected for processing (30)
  • physics-engine/gravitas-core/src/geodesic/integrator.rs
  • physics-engine/gravitas-core/src/geodesic/mod.rs
  • physics-engine/gravitas-core/src/invariants/mod.rs
  • physics-engine/gravitas-core/src/invariants/renormalization.rs
  • physics-engine/gravitas-core/src/physics/disk.rs
  • physics-engine/gravitas-core/src/physics/magnetosphere.rs
  • physics-engine/gravitas-core/src/physics/mod.rs
  • physics-engine/gravitas-core/src/physics/plunge.rs
  • physics-engine/gravitas-core/src/physics/radiative_transfer.rs
  • physics-engine/gravitas-core/src/physics/synchrotron.rs
  • physics-engine/gravitas-core/src/quantum/bekenstein.rs
  • physics-engine/gravitas-core/src/quantum/hawking.rs
  • physics-engine/gravitas-core/src/quantum/mod.rs
  • physics-engine/gravitas-core/tests/bekenstein.rs
  • physics-engine/gravitas-core/tests/conservation.rs
  • physics-engine/gravitas-core/tests/hawking_spectrum.rs
  • physics-engine/gravitas-core/tests/magnetosphere.rs
  • physics-engine/gravitas-core/tests/near_extremal.rs
  • physics-engine/gravitas-core/tests/plunge.rs
  • physics-engine/gravitas-core/tests/plunge_trajectory.rs
  • physics-engine/gravitas-core/tests/radiative_transfer.rs
  • physics-engine/gravitas-core/tests/synchrotron.rs
  • physics-engine/gravitas-core/tests/timelike_renormalize.rs
  • physics-engine/gravitas-wasm/src/lib.rs
  • src/__tests__/physics/hawking-bekenstein.test.ts
  • src/app/page.tsx
  • src/components/quantum/BekensteinHawkingReadout.tsx
  • src/components/quantum/HawkingSpectrumPanel.tsx
  • src/lib/physics/bekenstein.ts
  • src/lib/physics/hawking.ts

Comment on lines +210 to +216
// Pick the renormalization shell once per call. Null enforces
// H = 0 (photons); Timelike enforces H = -1/2 (unit-mass matter).
let renormalize: fn(&mut GeodesicState, &M) -> Result<(), crate::invariants::NormalizationError> =
match options.geodesic_kind {
GeodesicKind::Null => crate::invariants::renormalize_null,
GeodesicKind::Timelike => crate::invariants::renormalize_timelike,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

max_hamiltonian_drift becomes misleading for timelike integrations.

After introducing timelike mode, drift should be measured from the correct shell (H = -1/2), not always |H|. As-is, timelike trajectories report large artificial drift even when valid.

💡 Proposed fix
-        let h_val = crate::invariants::hamiltonian(&state, metric).abs();
-        if h_val > max_drift {
-            max_drift = h_val;
-        }
+        let h_val = crate::invariants::hamiltonian(&state, metric);
+        let shell_drift = match options.geodesic_kind {
+            GeodesicKind::Null => h_val.abs(),
+            GeodesicKind::Timelike => (h_val + 0.5).abs(),
+        };
+        if shell_drift > max_drift {
+            max_drift = shell_drift;
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@physics-engine/gravitas-core/src/geodesic/mod.rs` around lines 210 - 216,
max_hamiltonian_drift is currently computed as an absolute |H| which is
incorrect for timelike geodesics; change the drift calculation to measure
deviation from the chosen renormalization shell (H_target = 0 for
GeodesicKind::Null, H_target = -1/2 for GeodesicKind::Timelike). Locate where
max_hamiltonian_drift (and any Hamiltonian drift updates) are computed alongside
the renormalize selection (look for the match on options.geodesic_kind and the
renormalize function assignment that uses crate::invariants::renormalize_null
and crate::invariants::renormalize_timelike) and replace |H| with |H - H_target|
using H_target selected from the same GeodesicKind switch so null and timelike
integrations report drift relative to their correct shells. Ensure tests or
assertions that compare max_hamiltonian_drift are updated to the new reference
frame if present.

Comment on lines +29 to +32
//! Out of scope here: the Blandford-Znajek 1977 split-monopole and
//! paraboloidal solutions for jet launching, the field-line
//! visualisation, and the field-tensor F_μν derivatives. Those land
//! in follow-up changes once a downstream consumer needs them.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Module docs are stale relative to implementation.

Line 31–Line 32 says field-tensor derivatives are out of scope, but this file now implements wald_field_tensor. Update the header docs to avoid reader confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@physics-engine/gravitas-core/src/physics/magnetosphere.rs` around lines 29 -
32, The module docs claim field-tensor F_μν derivatives are out of scope but the
file now implements wald_field_tensor; update the header comment in
magnetosphere.rs to remove or adjust the "out of scope" statement (lines
referencing "field-tensor F_μν derivatives") and instead note that the Wald
field-tensor implementation (wald_field_tensor) is included here, or add a brief
note that derivatives remain out of scope while wald_field_tensor provides the
field tensor itself; ensure the docstring mentions wald_field_tensor by name so
readers aren’t confused.

Comment on lines +97 to +110
eps: f64,
) -> [[f64; 4]; 4] {
let a_plus_r = wald_potential_down(metric, b0, r + eps, theta);
let a_minus_r = wald_potential_down(metric, b0, r - eps, theta);
let a_plus_th = wald_potential_down(metric, b0, r, theta + eps);
let a_minus_th = wald_potential_down(metric, b0, r, theta - eps);

let mut f = [[0.0_f64; 4]; 4];

// ∂_r A_μ = (A_μ(r+ε) - A_μ(r-ε)) / (2ε); μ ∈ {t, φ}.
let d_a_dr_t = (a_plus_r[0] - a_minus_r[0]) / (2.0 * eps);
let d_a_dr_phi = (a_plus_r[3] - a_minus_r[3]) / (2.0 * eps);
let d_a_dth_t = (a_plus_th[0] - a_minus_th[0]) / (2.0 * eps);
let d_a_dth_phi = (a_plus_th[3] - a_minus_th[3]) / (2.0 * eps);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard eps before finite-difference division.

At Line 107–Line 110, derivatives divide by 2.0 * eps with no validation. eps <= 0 (or non-finite) will inject Inf/NaN into F_{μν} silently.

🛡️ Suggested fix
 pub fn wald_field_tensor(
     metric: &Kerr,
     b0: f64,
     r: f64,
     theta: f64,
     eps: f64,
 ) -> [[f64; 4]; 4] {
+    assert!(eps.is_finite() && eps > 0.0, "eps must be finite and > 0");
+
     let a_plus_r = wald_potential_down(metric, b0, r + eps, theta);
     let a_minus_r = wald_potential_down(metric, b0, r - eps, theta);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@physics-engine/gravitas-core/src/physics/magnetosphere.rs` around lines 97 -
110, The finite-difference divisions by 2.0 * eps can produce Inf/NaN when eps
is non-finite or <= 0; before computing d_a_dr_t, d_a_dr_phi, d_a_dth_t and
d_a_dth_phi in the function (where wald_potential_down is called), validate eps
with eps.is_finite() && eps > 0.0 and use a safe fallback (e.g. assign safe_eps
= eps when valid else a small positive constant like 1e-8) or return an early
error/Result; then use safe_eps in the denominators (2.0 * safe_eps) to prevent
division by zero/NaN.

Comment on lines +19 to +23
//! What it does NOT ship: the integrated timelike geodesic from ISCO
//! to the horizon. That requires extending the integrator to
//! propagate timelike orbits with H = −1/2 (the existing code path
//! enforces null normalisation H = 0). The trajectory primitive is
//! its own change.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Stale module documentation: trajectory is now implemented.

The docstring states "What it does NOT ship: the integrated timelike geodesic from ISCO to the horizon" and mentions extending the integrator. However, plunge_trajectory (lines 165-196) now implements exactly this using the timelike geodesic integrator. Update the documentation to reflect the current state.

Suggested documentation update
-//! What it does NOT ship: the integrated timelike geodesic from ISCO
-//! to the horizon. That requires extending the integrator to
-//! propagate timelike orbits with H = −1/2 (the existing code path
-//! enforces null normalisation H = 0). The trajectory primitive is
-//! its own change.
+//! Additionally ships: `plunge_trajectory`, which integrates the
+//! timelike geodesic from ISCO to the horizon using H = −1/2
+//! renormalisation (introduced alongside this module).
📝 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
//! What it does NOT ship: the integrated timelike geodesic from ISCO
//! to the horizon. That requires extending the integrator to
//! propagate timelike orbits with H = −1/2 (the existing code path
//! enforces null normalisation H = 0). The trajectory primitive is
//! its own change.
//! Additionally ships: `plunge_trajectory`, which integrates the
//! timelike geodesic from ISCO to the horizon using H = −1/2
//! renormalisation (introduced alongside this module).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@physics-engine/gravitas-core/src/physics/plunge.rs` around lines 19 - 23, The
module docstring is stale: remove or update the note that timelike geodesic
integration from ISCO to the horizon is not implemented and the TODO about
extending the integrator; instead document that plunge_trajectory now implements
the integrated timelike geodesic using the timelike integrator (the H = −1/2
code path) and that the trajectory primitive is included. Locate the top-of-file
doc comment and replace the outdated sentences with a brief statement
referencing plunge_trajectory and the timelike geodesic integrator (H = −1/2) to
reflect the current functionality.

Comment on lines +43 to +46
pub fn horizon_area_si(metric: &Kerr, mass_kg: f64) -> f64 {
let length_scale = SI_G * mass_kg / (SI_C * SI_C);
horizon_area_geometric(metric) * length_scale * length_scale
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add non-positive/non-finite mass guards on public thermodynamics APIs.

For mass_kg <= 0 (or NaN/Inf), these functions currently return unphysical or unstable outputs (e.g., division by zero at Line 74, negative lifetime at Line 91). Please enforce a valid-mass precondition consistently.

🛡️ Suggested guard pattern
 pub fn horizon_area_si(metric: &Kerr, mass_kg: f64) -> f64 {
+    if !mass_kg.is_finite() || mass_kg <= 0.0 {
+        return 0.0;
+    }
     let length_scale = SI_G * mass_kg / (SI_C * SI_C);
     horizon_area_geometric(metric) * length_scale * length_scale
 }
 
 pub fn bekenstein_hawking_entropy_per_kb(metric: &Kerr, mass_kg: f64) -> f64 {
+    if !mass_kg.is_finite() || mass_kg <= 0.0 {
+        return 0.0;
+    }
     let area_si = horizon_area_si(metric, mass_kg);
     let planck_length_sq = SI_HBAR * SI_G / (SI_C.powi(3));
     area_si / (4.0 * planck_length_sq)
 }
 
 pub fn schwarzschild_mass_loss_rate(mass_kg: f64) -> f64 {
+    if !mass_kg.is_finite() || mass_kg <= 0.0 {
+        return 0.0;
+    }
     let numerator = SI_HBAR * SI_C.powi(4);
     let denominator = 15360.0 * std::f64::consts::PI * SI_G * SI_G * mass_kg * mass_kg;
     -numerator / denominator
 }
 
 pub fn schwarzschild_evaporation_time(mass_kg: f64) -> f64 {
+    if !mass_kg.is_finite() || mass_kg <= 0.0 {
+        return 0.0;
+    }
     let numerator = SI_HBAR * SI_C.powi(4);
     let k = numerator / (15360.0 * std::f64::consts::PI * SI_G * SI_G);
     mass_kg.powi(3) / (3.0 * k)
 }

Also applies to: 56-60, 72-92

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@physics-engine/gravitas-core/src/quantum/bekenstein.rs` around lines 43 - 46,
Add a precondition guard in all public thermodynamics APIs that take mass_kg
(e.g., horizon_area_si and the functions around lines 56-60 and 72-92) that
checks mass_kg.is_finite() && mass_kg > 0.0; if the check fails return an
explicit invalid numeric result (e.g., f64::NAN) immediately to avoid divisions
by zero or producing negative/unstable values, and document the behavior in the
function comment so callers know invalid masses yield NaN. Ensure the same guard
is applied consistently to horizon_area_si, horizon_area_geometric wrappers, and
the lifetime/temperature functions referenced in the review.

Comment on lines +29 to +34
matches!(
traj.termination,
TerminationReason::Horizon
| TerminationReason::MaxSteps
| TerminationReason::NormalizationFailure
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

“Clean termination” test currently masks normalization regressions.

Allowing TerminationReason::NormalizationFailure makes this test pass even when the timelike shell maintenance breaks.

✅ Tighten expected termination outcomes
         matches!(
             traj.termination,
-            TerminationReason::Horizon
-                | TerminationReason::MaxSteps
-                | TerminationReason::NormalizationFailure
+            TerminationReason::Horizon | TerminationReason::MaxSteps
         ),
📝 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
matches!(
traj.termination,
TerminationReason::Horizon
| TerminationReason::MaxSteps
| TerminationReason::NormalizationFailure
),
matches!(
traj.termination,
TerminationReason::Horizon | TerminationReason::MaxSteps
),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@physics-engine/gravitas-core/tests/plunge_trajectory.rs` around lines 29 -
34, The test in plunge_trajectory.rs currently allows
TerminationReason::NormalizationFailure in the matches for traj.termination,
which masks regressions in timelike shell normalization; update the expectation
to remove TerminationReason::NormalizationFailure so the match only accepts
TerminationReason::Horizon or TerminationReason::MaxSteps (i.e., remove the
NormalizationFailure arm from the matches(...) expression referencing
traj.termination and TerminationReason) so normalization failures cause the test
to fail and surface regressions.

Comment on lines +32 to +42
it("extremal Kerr T_H collapses to zero", () => {
const t = hawkingTemperatureKelvin(SI_SOLAR_MASS, 1);
expect(t).toBeCloseTo(0, 30);
});

it("clamps spin outside [-1, 1] to the boundary", () => {
const t_pos = hawkingTemperatureKelvin(SI_SOLAR_MASS, 1.5);
const t_neg = hawkingTemperatureKelvin(SI_SOLAR_MASS, -1.5);
expect(t_pos).toBeCloseTo(0, 30);
expect(t_neg).toBeCloseTo(0, 30);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

toBeCloseTo(0, 30) precision is unrealistic for IEEE 754 doubles.

The second argument to toBeCloseTo is numDigits, meaning the tolerance is 10^(-numDigits) / 2. With numDigits = 30, the tolerance is 5e-31, which is far below JavaScript's Number.EPSILON (~2.2e-16). Any floating-point rounding will cause these assertions to fail.

For checking values that should be approximately zero, use a realistic precision like 12-14 digits, or use an absolute tolerance check.

Proposed fix
   it("extremal Kerr T_H collapses to zero", () => {
     const t = hawkingTemperatureKelvin(SI_SOLAR_MASS, 1);
-    expect(t).toBeCloseTo(0, 30);
+    expect(t).toBeCloseTo(0, 14);
   });

   it("clamps spin outside [-1, 1] to the boundary", () => {
     const t_pos = hawkingTemperatureKelvin(SI_SOLAR_MASS, 1.5);
     const t_neg = hawkingTemperatureKelvin(SI_SOLAR_MASS, -1.5);
-    expect(t_pos).toBeCloseTo(0, 30);
-    expect(t_neg).toBeCloseTo(0, 30);
+    expect(t_pos).toBeCloseTo(0, 14);
+    expect(t_neg).toBeCloseTo(0, 14);
   });
📝 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
it("extremal Kerr T_H collapses to zero", () => {
const t = hawkingTemperatureKelvin(SI_SOLAR_MASS, 1);
expect(t).toBeCloseTo(0, 30);
});
it("clamps spin outside [-1, 1] to the boundary", () => {
const t_pos = hawkingTemperatureKelvin(SI_SOLAR_MASS, 1.5);
const t_neg = hawkingTemperatureKelvin(SI_SOLAR_MASS, -1.5);
expect(t_pos).toBeCloseTo(0, 30);
expect(t_neg).toBeCloseTo(0, 30);
});
it("extremal Kerr T_H collapses to zero", () => {
const t = hawkingTemperatureKelvin(SI_SOLAR_MASS, 1);
expect(t).toBeCloseTo(0, 14);
});
it("clamps spin outside [-1, 1] to the boundary", () => {
const t_pos = hawkingTemperatureKelvin(SI_SOLAR_MASS, 1.5);
const t_neg = hawkingTemperatureKelvin(SI_SOLAR_MASS, -1.5);
expect(t_pos).toBeCloseTo(0, 14);
expect(t_neg).toBeCloseTo(0, 14);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/physics/hawking-bekenstein.test.ts` around lines 32 - 42, The
tests using hawkingTemperatureKelvin (called with SI_SOLAR_MASS and spins 1,
1.5, -1.5) assert near-zero with toBeCloseTo(0, 30), which requests unrealistic
30-digit precision for IEEE-754 numbers; change those assertions to use a
realistic precision (e.g., toBeCloseTo(0, 12) or 14) or replace them with an
explicit absolute tolerance check (e.g., Math.abs(t) < 1e-12) so extremal Kerr
and clamped-spin cases reliably pass; update the two tests referencing
hawkingTemperatureKelvin accordingly.

Comment thread src/app/page.tsx
Comment on lines +159 to +163
// Quantum visualization toggle (illustrative, per ADR-0026). Default
// off; the operator opts in via the keyboard shortcut Q. The panels
// render Bekenstein-Hawking thermodynamics (entropy, area, T_H,
// evaporation lifetime) and the Planck-shape Hawking spectrum.
const [showQuantum, setShowQuantum] = useState(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Comment and behavior are out of sync for the “Q” shortcut.

Line 160 says opt-in is via keyboard shortcut Q, but this component only wires button-based toggling. Either wire Q into this state or adjust the comment to avoid misleading operators.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/page.tsx` around lines 159 - 163, The comment says the "Q" shortcut
should toggle quantum visualization but the component only toggles via buttons;
add a keyboard handler that toggles the showQuantum state: in the component that
declares useState(false) for showQuantum/setShowQuantum, add a useEffect which
registers a keydown listener that checks if the pressed key is 'q' or 'Q' (or
use event.key.toLowerCase() === 'q') and calls setShowQuantum(prev => !prev),
and remove the listener in the cleanup to avoid leaks; alternatively, if you
prefer not to add keyboard handling, update the inline comment to remove the
mention of the "Q" shortcut so it accurately reflects button-only control.

Comment on lines +15 to +19
const SI_C = 299_792_458;
const SI_KB = 1.380649e-23;
const SI_HBAR = 1.054_571_817e-34;
const SI_G = 6.6743e-11;
const WIEN_FREQUENCY_CONSTANT_HZ_PER_K = 5.878_925_757e10;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor: SI_G precision differs from Rust implementation.

TypeScript uses SI_G = 6.6743e-11 while the Rust hawking_temperature function uses g_si = 6.674_30e-11 (context snippet 1). This small difference (~0.0004%) won't affect visualization but could cause subtle divergence in cross-validation tests. Consider aligning to the same precision for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/physics/hawking.ts` around lines 15 - 19, The SI_G constant in this
file is less precise than the Rust implementation; update the SI_G declaration
to match the Rust value (g_si) by using 6.674_30e-11 so the TypeScript SI_G
exactly matches the Rust hawking_temperature g_si constant for consistent
cross-language results.

@steeltroops-ai
steeltroops-ai merged commit 459c15a into main Apr 29, 2026
2 checks passed
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