feat: add beta(q) decoupling parameter to Multipole and Direct CSV output#4
Draft
pinedaps wants to merge 4 commits into
Draft
feat: add beta(q) decoupling parameter to Multipole and Direct CSV output#4pinedaps wants to merge 4 commits into
pinedaps wants to merge 4 commits into
Conversation
…CSS alias - `--box` for `direct` is now optional (no default_value in clap); when omitted without an XTC the 200 Å fallback is applied in lib.rs, preserving existing single-frame behaviour - Removes the hard error that fired even when -b was not supplied, allowing trajectory-averaged SAXS via `direct` without specifying a box - Add CSS (disulfide-bonded cysteine) as an alias for CYS in single_bead.yaml Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exposes β(q) = ⟨F⟩²_Ω / P(q) = 4π|A₀⁰|² / P(q) for all multipole paths (vacuum and solvent-corrected). This is the decoupling parameter needed to convert a true S(q) into the experimentally measured effective structure factor: S_eff(q) = 1 + β(q)·(S(q) − 1), required for concentrated-solution and mixture modelling. - Add `beta: Option<Vec<f64>>` to `Intensity` struct - Compute beta inline in `vacuum_intensity` (atoms-only path) - Compute beta inline in `MultipoleModel::intensity` (solvent path), using the combined A₀⁰ − v·C₀⁰ + ρc·B₀⁰ monopole coefficient - Update `write_csv` to emit a `beta` column when present; Debye and Direct paths are unaffected (beta stays None) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Collaborator
Author
|
Follow-up if approved: can we also expose |
Extends the decoupling parameter β(q) = ⟨F⟩²_Ω / P(q) to the direct (Miller-index) scheme, for both vacuum and solvent-corrected paths. ⟨F(q)⟩_Ω is computed analytically as Σ_i f_i·sinc(q|r_i − center|), the isotropic (l=0) component — equivalent to the multipole A₀⁰ route. All contributions (atoms, excluded volume, hydration) are centred on the protein geometric centroid so that β is translation-invariant. β → 1 at q = 0 and decreases with q, matching the multipole values. - Add `mean_sinc_amplitude(q, pos, ff, center)` and `centroid()` helpers - `vacuum_intensities`: compute beta after Miller-direction averaging - `solvent_intensities`: compute combined mean amplitude using protein centroid for atoms, excluded-volume, and hydration dummies - Update `lib.rs` doc to note Direct also populates `beta` - Add unit test verifying β ≈ 1 at low q and β < 0.5 at high q Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…method Two bugs in the beta(q) calculation for XTC trajectories: 1. Averaging formula: was accumulating per-frame β_t = ⟨F⟩²/P_t and averaging those, which is biased when P_t fluctuates (rotating protein sampled through few Miller directions). Correct formula is β = (⟨⟨F⟩_Ω⟩_conf)² / ⟨P⟩_conf. Fix: accumulate mean sinc amplitude A_t = sqrt(β_t·P_t) per frame, then compute β = ⟨A⟩²/⟨P⟩ after the loop. 2. PBC wrapping: mean_sinc_amplitude uses physical distances |r_i − centroid|, which are corrupted when the protein crosses a periodic box boundary. complex_amplitude is immune (exp(iq·r) is periodic for integer Miller indices), but the sinc-based ⟨F⟩_Ω is not. Fix: unwrap the molecule using minimum-image convention before computing the centroid and distances. Per-frame β now stays ≈ 0.9999 at low q instead of dropping to 0.03–0.24 in frames where the protein straddled the boundary. Also: β is suppressed for multi-protein boxes (frame atom count ≠ topology atom count), where it is physically meaningless. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds the decoupling parameter β(q) as an optional beta column to Pripps’ Multipole and Direct outputs, including both vacuum and solvent-corrected paths, and introduces PBC-unwrapping support for the Direct mean-sinc amplitude used in β.
Changes:
- Extend
Intensitywithbeta: Option<Vec<f64>>and emitbetafor Multipole and Direct computations. - Update CSV writing to conditionally include
beta(with/without solvent contribution columns). - Add PBC-aware mean-sinc amplitude helpers in the Direct implementation and adjust trajectory averaging logic for β.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/trajectory.rs |
Adds trajectory-time accumulation logic intended to compute β via averaged amplitudes. |
src/multipole/mod.rs |
Computes β from the monopole coefficient (vacuum + solvent-corrected multipole). |
src/lib.rs |
Adds beta to Intensity and applies a default box for non-trajectory runs when none is provided. |
src/explicit.rs |
Implements mean-sinc amplitude + PBC unwrapping and computes β for Direct (vacuum + solvent). Adds a unit test. |
src/cli.rs |
Makes --box optional for Direct and extends CSV output to include the beta column when present. |
assets/single_bead.yaml |
Adds a CSS alias entry to the form-factor YAML. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+180
to
+185
| // Accumulate mean sinc amplitude (= sqrt(β·P)) rather than β itself. | ||
| // After the loop, β = (Σ w·<F>)² / (Σ w·P) is computed from these. | ||
| if let (Some(acc), Some(src_beta)) = (mean.beta.as_mut(), curve.beta.as_ref()) { | ||
| for ((a, &beta_t), &p_t) in acc.iter_mut().zip(src_beta).zip(&curve.total) { | ||
| *a += weight * (beta_t * p_t).sqrt(); | ||
| } |
Comment on lines
161
to
+173
| @@ -166,15 +167,34 @@ pub fn trajectory_average( | |||
| if mean.q.is_empty() { | |||
| mean.q = curve.q.clone(); | |||
| mean.total = vec![0.0; curve.total.len()]; | |||
| // β(q) is only meaningful for a single-protein box | |||
| if frame.natoms() == single_protein && curve.beta.is_some() { | |||
| mean.beta = Some(vec![0.0; curve.total.len()]); | |||
| } | |||
Comment on lines
+52
to
+69
| /// Unwrap PBC-wrapped positions so the whole molecule lies in one image. | ||
| /// | ||
| /// Anchors on the first atom and folds each subsequent atom to the nearest | ||
| /// image of the previous one. Returns the atom positions with intra-molecular | ||
| /// distances preserved. | ||
| fn unwrap_positions(positions: &[Vector3], box_sides: &Vector3) -> Vec<Vector3> { | ||
| if positions.is_empty() { | ||
| return vec![]; | ||
| } | ||
| let mut out = Vec::with_capacity(positions.len()); | ||
| out.push(positions[0]); | ||
| for r in &positions[1..] { | ||
| let prev = *out.last().unwrap(); | ||
| let delta = minimum_image(&(r - prev), box_sides); | ||
| out.push(prev + delta); | ||
| } | ||
| out | ||
| } |
Comment on lines
+76
to
+95
| fn mean_sinc_amplitude( | ||
| q_norm: f64, | ||
| positions: &[Vector3], | ||
| form_factors: &[f64], | ||
| box_sides: Option<&Vector3>, | ||
| ) -> f64 { | ||
| let (pos, center) = match box_sides { | ||
| Some(b) => { | ||
| let unwrapped = unwrap_positions(positions, b); | ||
| let c = centroid(&unwrapped); | ||
| (unwrapped, c) | ||
| } | ||
| None => (positions.to_vec(), centroid(positions)), | ||
| }; | ||
| pos.iter().zip(form_factors).map(|(r, &f)| { | ||
| let qr = q_norm * (r - center).norm(); | ||
| let sinc = if qr < 1e-10 { 1.0 } else { qr.sin() / qr }; | ||
| f * sinc | ||
| }).sum() | ||
| } |
Comment on lines
+357
to
+359
| let form_factors = vec![1.0, 1.0]; | ||
| let center = centroid(&positions); | ||
|
|
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
betacolumn in the Multipole and Direct CSV outputsPhysics
β(q) is the decoupling parameter needed to convert a true S(q) into the experimentally measured effective structure factor:
Multipole path: ⟨F(q)⟩_Ω = √(4π)·A₀⁰(q), so β(q) = 4π|A₀⁰|²/P(q)
Direct path: ⟨F(q)⟩_Ω = Σ_i f_i·sinc(q|r_i − centroid|), so β(q) = ⟨F⟩²/P(q)
Both routes give the same physical quantity. β(q) → 1 at q = 0 and decreases with q for asymmetric proteins.
Trajectory averaging (Direct + XTC)
For a trajectory the correct formula is β = (⟨F⟩_conf)² / ⟨P⟩_conf. Averaging per-frame β_t = ⟨F⟩_t² / P_t is biased when P_t fluctuates (e.g. a rotating protein sampled through a small number of Miller directions). Instead:
Single-protein detection: β is only written when the XTC frame atom count equals the topology atom count. For multi-protein boxes P(q) includes inter-protein terms, making β meaningless.
PBC fix
complex_amplitudeused for I(q) is immune to PBC wrapping (exp(iq·r) is periodic for integer Miller indices). Butmean_sinc_amplitudeuses physical distances |r_i − centroid|, which are corrupted when the protein crosses a box boundary. Fix: unwrap the molecule using the minimum-image convention before computing the centroid and distances. Per-frame β now stays ≈ 0.9999 at low q across all 100 frames instead of dropping to 0.03–0.24 in frames where the protein straddled the boundary.CSV output
Vacuum direct/multipole (single protein, XTC or static structure):
Solvent-corrected path:
Test plan
direct_beta_near_one_at_low_q_and_decays🤖 Generated with Claude Code