Fix vibrational/rotational energy sampling cutoff for many DOF (#64) - #21
Open
stanmoore1 wants to merge 3 commits into
Open
Fix vibrational/rotational energy sampling cutoff for many DOF (#64)#21stanmoore1 wants to merge 3 commits into
stanmoore1 wants to merge 3 commits into
Conversation
Particle::evib() and Particle::erot() sampled internal energy from the equilibrium distribution x^a*exp(-x) (x = E/kT, a = dof/2 - 1) using acceptance-rejection with candidate energies drawn uniformly on [0, 10 kT]. The fixed 10 kT cutoff is below the mean of the distribution (mean = a+1) once the number of degrees of freedom is large. For highly polyatomic species such as SF6, this truncates the high-energy tail of the distribution, systematically biasing the sampled vibrational energy low and distorting its shape (violating detailed balance). Scale the candidate range with the number of degrees of freedom using mean + ~9 standard deviations (a + 1 + 9*sqrt(a+1)) so the cutoff always covers the tail while keeping rejection efficiency roughly constant. The rotational path is bounded (rotdof <= 3) so it was latent there, but is fixed for consistency. Fixes sparta#64. Co-Authored-By: stanmoore1 <stanmoore1@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4ebZSujHcJJhY6bNMnmZ6
stanmoore1
force-pushed
the
claude/issue-64-physics-analysis-xu7wax
branch
from
June 26, 2026 21:13
f9cc1c3 to
bc95f93
Compare
The KOKKOS GPU paths mirror the CPU samplers and had the same fixed 10 kT candidate cutoff in ParticleKokkos::erot/evib and the inlined erot/evib in SurfCollideDiffuseKokkos. Scale the cutoff with the number of degrees of freedom (a + 1 + 9*sqrt(a+1)) to match the CPU fix, so CPU and GPU runs sample the same vibrational/rotational energy distribution. Co-Authored-By: stanmoore1 <stanmoore1@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4ebZSujHcJJhY6bNMnmZ6
The vibrational energy sampling cutoff change shifts the RNG stream in the vibrate example (CO2 has vibdof=8, which is sampled via the rejection branch during create_particles), so the mpi_1 and mpi_4 gold logs no longer match. Regenerate both reference logs with the fixed code. Verified reproducible: an unchanged example (circle.diffuse) still passes against its committed gold at 1e-7 in this environment, and the new logs match independently-generated runs exactly. Co-Authored-By: stanmoore1 <stanmoore1@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4ebZSujHcJJhY6bNMnmZ6
stanmoore1
pushed a commit
that referenced
this pull request
Jul 25, 2026
Task #21. The per-step refresh of a depositing surface's collision geometry now covers 3d as well as 2d and axisymmetric, so all three resolve the growth the same way. MarchingCubes::invoke()'s per-cell work is factored into cell_surfs(), mirroring what was already done for MarchingSquares: the corner values, the twelve edge interpolations, the case and configuration lookup and the tiling switch all move into one routine that the rebuild and the refresh share. Keeping a single copy is the point; two copies of a 256-case table that drifted apart would tear the surface between cells. All the working state was already class member data, so the extraction is mechanical, and in.ablation.3d and in.ablation.multi.inner.3d reproduce their reference logs byte for byte. refresh_surfs() then evaluates cells with either marching squares or marching cubes, storing up to 12 triangles per cell, and the move loop tests refreshed triangles with line_tri_intersect() and reflects off their recomputed normals. The safety argument is unchanged and still carries in 3d: the field is extrapolated forward so the refreshed surface always holds at least as much material as the committed one, and the interpolation fraction is capped, globally, before any cell holding surface would close. Kokkos deliberately still uses the regeneration-time path. Its particle move is a separate implementation whose kernels would need the refreshed geometry mirrored into device views, which cannot be exercised here with a Serial-only build; deposition under Kokkos remains correct, just resolved once per Nevery. Documented on the fix ablate page. Verified: 3d deposition loses zero particles at Nevery 1, 5 and 20 with increments up to 2.0 per regeneration, with fix grid/check error active; 2d and axisymmetric unchanged and clean; conservation identity holds on 1 and 4 ranks; all three ablation examples byte-identical to their mpi_1 and mpi_4 reference logs; Kokkos serial with SPARTA_KOKKOS_EXACT builds and runs clean; tools/testing/deposition passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J2aoVbCK69c9CEiap5WJnL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes sparta#64 ("Random vibrational energy generation").
Particle::evib()(and the structurally identicalParticle::erot()) sample a particle's internal energy from the equilibrium distributionusing acceptance–rejection. The acceptance function
b = (x/a)^a · exp(a−x)is correct, but the candidate energies were drawn uniformly on a fixed range[0, 10 kT]:This distribution is
Gamma(shape = a+1), with mean = a+1 = dof/2. The fixed10 kTceiling is only safe while the mean sits well below 10. As the number of degrees of freedom grows, the cutoff falls below the mean of the distribution, so the sampler can never propose the energies where most of the probability mass lives. The result is vibrational energy that is biased low and whose distribution shape is destroyed (detailed balance is violated). For SF₆-class polyatomics (~30 vibrational DOF) the bias is severe.This affects every code path that initializes vibrational energy at a temperature — inflow/emit boundaries (
fix_emit_*) and diffuse/CLL/TD surface models (surf_collide_*).Fix
Scale the candidate range with the number of degrees of freedom so it always covers the high-energy tail. The mode is at
x = a, the mean ata+1, the std dev√(a+1); using mean + ~9 std devs:This keeps the rejection efficiency roughly constant (~18–20%) instead of letting it collapse, and reduces to the original behavior for small molecules. The rotational path is bounded (
rotdof ≤ 3, soa ≤ 0.5), meaning the bug was latent there, but the same fix is applied for consistency.Changed file:
src/particle.cpp(evib()anderot()).Verification
A standalone Monte-Carlo test (
verification/issue64/verify_evib.cpp) reproduces the old and new samplers verbatim, drawing 2×10⁶ samples per case, and compares the sampled mean and variance ofE/kTagainst the analytic values (mean = variance = vibdof/2).Findings:
vibdof=30, −54% atvibdof=40) and asymptotes toward ~9–10 because returned energies are clamped to[0, 10 kT]. The variance collapses even harder, confirming the shape is wrong, not just the mean.vibdof=40(its[0,10]window rarely lands near the mode ata=19), whereas the new sampler holds ~18–20% throughout.vibdof = 4, 8), confirming the fix doesn't perturb cases that already worked.The analytic percent-error curves agree exactly with the Monte-Carlo points:
The
verification/issue64/directory (test, plot script, figure) is verification-only and can be dropped before merge if preferred.🤖 Generated with Claude Code
Generated by Claude Code