feat(sp4+sp5): physics + FFI + UI for SP-4 modules + SP-5 quantum panels - #12
Conversation
…(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.
📝 WalkthroughWalkthroughThis 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
💡 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".
| if tau.abs() < OPTICAL_DEPTH_FLOOR { | ||
| intensity += sample.j_nu * sample.d_lambda; | ||
| } else { |
There was a problem hiding this comment.
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.
…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.
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (7)
physics-engine/gravitas-core/src/physics/radiative_transfer.rs (1)
160-165: Consider guarding against zero frequency inwavelength_metres.If
band.freq_hzis zero, this returnsf64::INFINITY. While the canonicalBandconstants have positive frequencies, callers constructing customBandvalues could trigger this. A guard returning0.0orf64::INFINITYexplicitly 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 absoluteTIGHT = 1e-12tolerance 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 extremala*=±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 usingtoPrecision(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 bothschwarzschildMassLossRate(line 56-57) andschwarzschildEvaporationTimeSeconds(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 extractingPlasmaStateconstruction.Both
synchrotron_emissivityandsynchrotron_absorptionconstruct identicalPlasmaStatestructs 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
📒 Files selected for processing (30)
physics-engine/gravitas-core/src/geodesic/integrator.rsphysics-engine/gravitas-core/src/geodesic/mod.rsphysics-engine/gravitas-core/src/invariants/mod.rsphysics-engine/gravitas-core/src/invariants/renormalization.rsphysics-engine/gravitas-core/src/physics/disk.rsphysics-engine/gravitas-core/src/physics/magnetosphere.rsphysics-engine/gravitas-core/src/physics/mod.rsphysics-engine/gravitas-core/src/physics/plunge.rsphysics-engine/gravitas-core/src/physics/radiative_transfer.rsphysics-engine/gravitas-core/src/physics/synchrotron.rsphysics-engine/gravitas-core/src/quantum/bekenstein.rsphysics-engine/gravitas-core/src/quantum/hawking.rsphysics-engine/gravitas-core/src/quantum/mod.rsphysics-engine/gravitas-core/tests/bekenstein.rsphysics-engine/gravitas-core/tests/conservation.rsphysics-engine/gravitas-core/tests/hawking_spectrum.rsphysics-engine/gravitas-core/tests/magnetosphere.rsphysics-engine/gravitas-core/tests/near_extremal.rsphysics-engine/gravitas-core/tests/plunge.rsphysics-engine/gravitas-core/tests/plunge_trajectory.rsphysics-engine/gravitas-core/tests/radiative_transfer.rsphysics-engine/gravitas-core/tests/synchrotron.rsphysics-engine/gravitas-core/tests/timelike_renormalize.rsphysics-engine/gravitas-wasm/src/lib.rssrc/__tests__/physics/hawking-bekenstein.test.tssrc/app/page.tsxsrc/components/quantum/BekensteinHawkingReadout.tsxsrc/components/quantum/HawkingSpectrumPanel.tsxsrc/lib/physics/bekenstein.tssrc/lib/physics/hawking.ts
| // 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, | ||
| }; |
There was a problem hiding this comment.
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.
| //! 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. |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
| //! 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. |
There was a problem hiding this comment.
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.
| //! 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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| matches!( | ||
| traj.termination, | ||
| TerminationReason::Horizon | ||
| | TerminationReason::MaxSteps | ||
| | TerminationReason::NormalizationFailure | ||
| ), |
There was a problem hiding this comment.
“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.
| 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.
| 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); | ||
| }); |
There was a problem hiding this comment.
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.
| 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.
| // 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); |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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.
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
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.
Boyer-Lindquist via the existing Kerr covariant tensor; q_W = 2
B_0 a M Wald horizon charge; F_μν via central differences.
equatorial geodesic invariants (E, L_z, Ω, η = 1 − E_ISCO).
plunge_trajectory uses the new timelike integrator.
fit with Kirchhoff-law absorption.
the BPT specific-energy/angular-momentum helpers.
gravitas-core invariants + integrator
discriminant-band semantics as the null branch.
gravitas-core quantum
entropy ratio S/k_B, Page 1976 photon-only Schwarzschild mass-loss
rate, evaporation lifetime.
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
Rust quantum surface so the operator HUD can render at 60 Hz
without crossing the WASM bridge for a single readout.
HUD strip (horizon area, S/k_B, T_H, t_evap) with formatScientific.
Recharts plot at T_H(M, a*) with Wien-peak reference line and
illustrative footer per ADR-0026.
panels mount when opened, default off.
Test plan
What's NOT in this PR (false-claim guard)
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.
compositing or spectral-band selection in the ray-marching
kernel. Those are render-pipeline changes earning their own
per-module PRs.
suite skip-warns until goldens are captured; operator runs
shader:update-goldens --confirm when satisfied with the visuals.
polarised emissivity (Schnittman+Krolik 2009 §2). The framework's
integrate_radiative_transfer takes (j, α) per band so any of
those plug in cleanly.
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.
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
Improvements