From e5dcfc2fbe193f10ea4fe51d58ca7b97e9a91873 Mon Sep 17 00:00:00 2001 From: mlund Date: Thu, 18 Jun 2026 18:21:06 +0200 Subject: [PATCH 1/3] Merge multiple --atomfile files; split gaussian_atoms.yaml Form-factor definitions can now be composed from several YAML files. Repeat --atomfile (or pass several paths), or give the Python Pripps constructor a list. Files are merged in order; on a duplicate species the last file wins and a warning is logged. Merging happens before symbolic resolution, so an !Alias/!United/!Linear in one file may reference a species defined in another. - formfactor.rs: split read_formfactors into parse_specs + finalize_specs; add read_formfactors_merged. - lib.rs: add Pripps::from_yaml_files; from_yaml delegates via from_formfactors. - cli.rs: --atomfile becomes Vec (repeatable, order-preserving). - pripps-py: Pripps(...) accepts a str or list[str]. Split gaussian_atoms.yaml into an atomic/united-atom-only table: drop the 20 coarse-grained residue beads (byte-identical duplicates of poly_amino_acid.yaml) and 8 unused residue-level aliases, and rewrite the header. Residue-bead consumers load poly_amino_acid.yaml (or merge it with the atomic file). This resolves the mixed-representation file and supersedes the rename-only approach in #11. Closes #7 --- README.md | 26 ++- assets/gaussian_atoms.yaml | 376 +------------------------------------ pripps-py/src/lib.rs | 23 ++- src/cli.rs | 10 +- src/formfactor.rs | 111 ++++++++++- src/lib.rs | 23 ++- 6 files changed, 180 insertions(+), 389 deletions(-) diff --git a/README.md b/README.md index f89b30d..38fa356 100644 --- a/README.md +++ b/README.md @@ -234,12 +234,33 @@ scaled by `volume_scale` — instead of building a Gaussian dummy from `radius`/`displaced_volume`. Every species present in the structure must define it when `--excluded per-kind` is selected. +#### Merging multiple files + +Several form-factor files can be supplied and are merged in the order +given — repeat `--atomfile` (or pass several paths) on the CLI, or pass a +list to the Python constructor: + +```bash +pripps --atomfile assets/gaussian_atoms.yaml --atomfile my_overrides.yaml ... +``` + +```python +pr = pripps.Pripps(["assets/gaussian_atoms.yaml", "my_overrides.yaml"]) +``` + +This lets you compose, e.g., a coarse-grained residue file with an atomic +file. **Duplicate policy:** if the same species is defined in more than one +file, the **last file listed wins** and a warning is logged naming the +overridden species. Merging happens before symbolic resolution, so an +`!Alias` / `!United` / `!Linear` entry in one file may reference a species +defined in another. + ### Shipped form-factor files | File | Description | Best for | |------|-------------|----------| | `assets/foxs_formfactors.yaml` | Cromer-Mann (Int. Tables Vol. C) atomic form factors, FoXS-compatible volumes and SASA radii | Atomic PDBs with `--excluded foxs` | -| `assets/gaussian_atoms.yaml` | Mixed table: residue single-bead polynomials plus atomic/united-atom Gaussians (CH/CH₂/NH/…) | Atomic PDBs with PepsiSAXS-style solvent models | +| `assets/gaussian_atoms.yaml` | Atomic and united-atom Gaussians (CH/CH₂/NH/…), PepsiSAXS-style (K = 4π) | Atomic PDBs with PepsiSAXS-style solvent models | | `assets/stovgaard2010.yaml` | Per-residue form-factor centroids (one-body model, q ∈ [0, 0.75] Å⁻¹) from Stovgaard et al. 2010, | Residue / Cα single-bead models | | `assets/poly_amino_acid.yaml` | Amino-acid bead polynomials with a separate `excluded_solvent` polynomial per bead | Cα / single-bead with `--excluded per-kind` | | `assets/poly_amino_acid_combined.yaml` | Amino-acid bead polynomials with the excluded solvent already folded into the form factor | Cα / single-bead with `--excluded disabled` | @@ -335,6 +356,9 @@ pripps --help # Debye formula (exact, O(N²)) pripps -a atoms.yaml -i structure.pdb debye +# Merge several form-factor files (last listed wins on duplicate species) +pripps -a base.yaml -a overrides.yaml -i structure.pdb debye + # Multipole expansion (linear in atoms, truncation auto-selected) pripps -a atoms.yaml -i structure.pdb multipole pripps -a atoms.yaml -i structure.pdb multipole --lmax 68 # fixed order diff --git a/assets/gaussian_atoms.yaml b/assets/gaussian_atoms.yaml index 5f072aa..1da56a2 100644 --- a/assets/gaussian_atoms.yaml +++ b/assets/gaussian_atoms.yaml @@ -1,364 +1,8 @@ -# Amino acid single bead approximation for atomistic scattering -ALA: - { - radius: 3.1, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 37.98651145079954, - 0.0, - -17.88881261159881, - -3.1097731825829333, - 8.846459092736989, - -2.3572911188270753, - 0.05331786541354333, - ], - }, - } -ARG: - { - radius: 4.0, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 83.96908927242093, - 0.0, - -278.81910994314467, - 490.5059319443547, - -383.9755997417938, - 143.65451627028935, - -20.783481179736413, - ], - }, - } -ASN: - { - radius: 3.6, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 59.98205560472268, - 0.0, - -40.54214890095476, - -32.39953367060282, - 73.4159357342896, - -37.17064504604371, - 6.101333446616579, - ], - }, - } -ASP: - { - radius: 3.6, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 59.98252699985404, - 0.0, - -37.42179690845112, - -37.97031252961563, - 77.22160318216388, - -38.25436485287656, - 6.198643717045879, - ], - }, - } -CYS: - { - radius: 3.6, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 53.98471591625841, - 0.0, - -28.55463591537983, - -34.95759123047967, - 63.496177867671385, - -29.839780719449596, - 4.609281628955767, - ], - }, - } -GLN: - { - radius: 3.8, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 68.00029704612041, - 0.0, - -97.52724702286758, - 63.863879205326334, - 13.865971310987792, - -22.96589467520148, - 5.296934807386833, - ], - }, - } -GLU: - { - radius: 3.8, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 68.00262098772043, - 0.0, - -95.50455833177392, - 59.449715597229726, - 18.625430344038303, - -25.334425917186397, - 5.722041637792017, - ], - }, - } -GLY: - { - radius: 2.9, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 29.99006405529222, - 0.0, - -11.572242769967232, - -0.9021293993630419, - 3.790951509543525, - -0.7238332866526376, - -0.05549468364095844, - ], - }, - } -HIS: - { - radius: 3.9, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 71.99289906060545, - 0.0, - -93.58672742143521, - 41.460338892548805, - 39.724339660983915, - -34.41529003289915, - 7.057596750057414, - ], - }, - } -ILE: - { - radius: 3.6, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 61.97933054508859, - 0.0, - -45.55749645626756, - -26.94147636420316, - 68.41091746940783, - -34.307325839550984, - 5.508363044131372, - ], - }, - } -LEU: - { - radius: 3.6, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 61.9874112464829, - 0.0, - -54.07829774966109, - -19.672724727921118, - 75.75933357599484, - -42.87909415253206, - 7.580643479500708, - ], - }, - } -LYS: - { - radius: 3.7, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 69.99129116923348, - 0.0, - -177.94423757968704, - 272.0256856055207, - -188.2920973172914, - 63.303038257186586, - -8.32612812430321, - ], - }, - } -MET: - { - radius: 3.8, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 70.00691278772638, - 0.0, - -104.22345654704631, - 73.82601789465483, - 10.126507627913185, - -23.351946238697035, - 5.598503080281802, - ], - }, - } -PHE: - { - radius: 3.9, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 77.99863587497389, - 0.0, - -135.30506501839736, - 122.61728717372222, - -26.829761151876106, - -9.267313845762843, - 3.479368368324131, - ], - }, - } -PRO: - { - radius: 3.4, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 52.98008056903471, - 0.0, - -28.025579140605455, - -17.945632640349288, - 36.70484993598286, - -16.388585233440434, - 2.3759607129713807, - ], - }, - } -SER: - { - radius: 3.3, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 45.98433274188738, - 0.0, - -23.004358312306444, - -13.52528557404974, - 25.318714967455264, - -9.60754708227119, - 1.0838180112873488, - ], - }, - } -THR: - { - radius: 3.5, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 53.98079904257712, - 0.0, - -30.205614210054552, - -20.68857584136233, - 39.80321996161699, - -16.614363714862087, - 2.188488144363049, - ], - }, - } -TRP: - { - radius: 4.3, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 98.00026950327825, - 0.0, - -239.82185506986724, - 306.5877727946093, - -164.7189974469082, - 39.67855896479587, - -3.263270784166963, - ], - }, - } -TYR: - { - radius: 4.1, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 85.99025648157047, - 0.0, - -205.8271529828311, - 282.4197901545501, - -174.14171820412088, - 52.3398359916699, - -6.16839805508144, - ], - }, - } -VAL: - { - radius: 3.4, - formfactor: - !Polynomial { - qrange: [0.0, 0.75], - coeff: - [ - 53.97802539665846, - 0.0, - -30.730380715977926, - -23.1259991191962, - 43.982242401804015, - -18.76619704074442, - 2.5506887609142446, - ], - }, - } +# Atomic / united-atom form factors for PRIPPS (PepsiSAXS-style K = 4π). +# +# Atomic-resolution structures only. Residue single-bead form factors live +# in assets/poly_amino_acid.yaml — pass several --atomfile paths to merge +# this atomic table with a coarse-grained residue file when needed. # Atomic properties for PRIPPS # @@ -810,17 +454,7 @@ I: CTR: { radius: 2.0, formfactor: !Alias C } NTR: { radius: 2.0, formfactor: !Alias N } HNTR: { ff: !Alias N } -HLYS: { ff: !Alias LYS } -HARG: { ff: !Alias ARG } -HHIS: { ff: !Alias HIS } -HGLU: { ff: !Alias GLU } -HASP: { ff: !Alias ASP } HCTR: { ff: !Alias C } -HLYS: { ff: !Alias LYS } -HLYS: { ff: !Alias LYS } -HTYR: { ff: !Alias TYR } -HCYS: { ff: !Alias CYS } -CYX: { ff: !Alias CYS } # PDB atom name aliases — map multi-character PDB names to their # element form factors so atomic-resolution structures work with diff --git a/pripps-py/src/lib.rs b/pripps-py/src/lib.rs index 0b7a728..aecfda5 100644 --- a/pripps-py/src/lib.rs +++ b/pripps-py/src/lib.rs @@ -259,7 +259,8 @@ impl PyFitResult { /// Scattering calculator loaded from a form-factor YAML file. /// -/// Construct with `Pripps(atomfile)`, then either build a cached model +/// Construct with `Pripps(atomfile)` — a single path or a list of paths +/// merged in order (latest wins on duplicate species) — then either build a cached model /// (`multipole_model(...)` / `debye_model(...)` / `direct_model(...)`) /// for repeated/fitting use, or call `intensity(...)` for a one-shot /// calculation (the only path that averages over an XTC trajectory). @@ -268,10 +269,24 @@ struct PyPripps(Pripps); #[pymethods] impl PyPripps { - /// Load a pripps instance from a form-factor YAML file. + /// Load a pripps instance from one or more form-factor YAML files. + /// + /// `atomfile` is either a single path (`Pripps("a.yaml")`) or a list + /// of paths (`Pripps(["base.yaml", "override.yaml"])`). Multiple files + /// are merged in order; when a species appears in more than one file + /// the last one listed wins (a warning is logged for each override). #[new] - fn new(atomfile: &str) -> PyResult { - Pripps::from_yaml(atomfile.as_ref()) + fn new(atomfile: &Bound<'_, PyAny>) -> PyResult { + let paths: Vec = if let Ok(single) = atomfile.extract::() { + vec![single.into()] + } else if let Ok(many) = atomfile.extract::>() { + many.into_iter().map(PathBuf::from).collect() + } else { + return Err(PyTypeError::new_err( + "`atomfile` must be a str or a list of str", + )); + }; + Pripps::from_yaml_files(&paths) .map(Self) .map_err(into_py_err) } diff --git a/src/cli.rs b/src/cli.rs index 67bab6b..eb5391b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -216,9 +216,11 @@ pub struct Args { #[clap(subcommand)] pub command: Commands, - /// Form factor definitions for species (atoms, residues, etc.) - #[clap(long, short = 'a')] - pub atomfile: PathBuf, + /// Form factor definitions for species (atoms, residues, etc.). + /// Repeat the flag or pass several paths to merge multiple files; + /// on a duplicate species the last file listed wins. + #[clap(long = "atomfile", short = 'a', num_args = 1.., required = true)] + pub atomfiles: Vec, /// Input structure (xyz, pdb, gro etc.) #[clap(short = 'i', long)] @@ -266,7 +268,7 @@ pub fn do_main() -> Result<()> { } pretty_env_logger::init(); - let pripps = Pripps::from_yaml(&args.atomfile)?; + let pripps = Pripps::from_yaml_files(&args.atomfiles)?; let (scheme, box_override, solvent) = command_to_scheme(args.command)?; if let Some(fit_path) = &args.fit { diff --git a/src/formfactor.rs b/src/formfactor.rs index 2eaf897..4a85305 100644 --- a/src/formfactor.rs +++ b/src/formfactor.rs @@ -18,7 +18,13 @@ use crate::{AtomKind, Error, Result}; use itertools::Itertools; use nalgebra::{DMatrix, DVector}; use serde::{Deserialize, Serialize}; -use std::{cmp::Ordering, collections::BTreeMap, fs::File, ops::Add, path::Path}; +use std::{ + cmp::Ordering, + collections::BTreeMap, + fs::File, + ops::Add, + path::{Path, PathBuf}, +}; /// A flat table pairing each [`AtomKind`] with its runtime [`FormFactor`], /// stored in atom-kind id order. Lookups by id are `O(1)` vector indexes. @@ -401,11 +407,19 @@ fn interpolate(table: &[(f64, f64)], x: f64) -> f64 { y0 + (x - x0) * (y1 - y0) / (x1 - x0) } -/// Read YAML file with form factors for different atom types (atomic or coarse grained) -pub fn read_formfactors(path: &Path) -> Result { +/// Parse a YAML form-factor file into its raw, still-symbolic spec map +/// (keyed by species name). The symbolic variants (`!Alias`, `!United`, +/// `!Linear`) are left unresolved so that [`read_formfactors_merged`] can +/// combine maps from several files *before* resolution — letting an alias +/// in one file reference a definition supplied by another. +fn parse_specs(path: &Path) -> Result> { let file = File::open(path).with_path(path)?; - let mut specs: BTreeMap = yaml_serde::from_reader(file)?; + Ok(yaml_serde::from_reader(file)?) +} +/// Resolve the symbolic specs in `specs` and flatten them into a runtime +/// [`FormFactorMap`]. The inverse second half of [`parse_specs`]. +fn finalize_specs(mut specs: BTreeMap) -> Result { resolve_symbolic_specs(&mut specs, SpecRole::FormFactor)?; resolve_symbolic_specs(&mut specs, SpecRole::ExcludedSolvent)?; @@ -425,7 +439,12 @@ pub fn read_formfactors(path: &Path) -> Result { }) .collect::>()?; - let formfactor = FormFactorMap::from_entries_with_excluded(entries); + Ok(FormFactorMap::from_entries_with_excluded(entries)) +} + +/// Read YAML file with form factors for different atom types (atomic or coarse grained) +pub fn read_formfactors(path: &Path) -> Result { + let formfactor = finalize_specs(parse_specs(path)?)?; log::info!( "Read form-factor definitions from {} for {} species", path.display(), @@ -434,6 +453,48 @@ pub fn read_formfactors(path: &Path) -> Result { Ok(formfactor) } +/// Read and merge several YAML form-factor files into one table. +/// +/// Files are parsed in the given order and their species merged by name. +/// When the same species appears in more than one file, the **later** file +/// wins (last definition supplied), and a warning is logged naming the +/// species and the file that overrode it. Merging happens before symbolic +/// resolution, so an `!Alias` / `!United` / `!Linear` spec in one file may +/// reference a definition contributed by an earlier file. +pub fn read_formfactors_merged(paths: &[PathBuf]) -> Result { + let [first, rest @ ..] = paths else { + return Err(Error::validation("no form-factor files supplied")); + }; + + let mut merged = parse_specs(first)?; + for path in rest { + for (name, spec) in parse_specs(path)? { + use std::collections::btree_map::Entry; + match merged.entry(name) { + Entry::Occupied(mut entry) => { + log::warn!( + "Duplicate species '{}' redefined by {}; using the latest definition", + entry.key(), + path.display(), + ); + entry.insert(spec); + } + Entry::Vacant(entry) => { + entry.insert(spec); + } + } + } + } + + let formfactor = finalize_specs(merged)?; + log::info!( + "Merged form-factor definitions from {} file(s) into {} species", + paths.len(), + formfactor.len(), + ); + Ok(formfactor) +} + /// Convert all formfactors to a TABLE and Write to YAML file /// /// The q-interval is hard-coded to [0.0, 0.5] with 100 points. @@ -751,6 +812,46 @@ ALA: assert_relative_eq!(excluded.eval(0.2).unwrap(), 3.5); } + #[test] + fn read_formfactors_merged_latest_wins_and_cross_file_alias() { + // `base` defines C; `override_` redefines C (latest wins) and adds + // an alias that resolves against the base file's C — exercising the + // pre-resolution merge. + let base = r" +C: + formfactor: !Constant 6.0 +N: + formfactor: !Constant 7.0 +"; + let override_ = r" +C: + formfactor: !Constant 60.0 +CA: + formfactor: !Alias N +"; + let dir = std::env::temp_dir(); + let base_path = dir.join("pripps_merge_base.yaml"); + let over_path = dir.join("pripps_merge_override.yaml"); + std::fs::write(&base_path, base).unwrap(); + std::fs::write(&over_path, override_).unwrap(); + + let ff = read_formfactors_merged(&[base_path, over_path]).unwrap(); + + // Union of species: C, N (from base) + CA (from override). + assert_eq!(ff.len(), 3); + let eval = |name: &str| { + ff.form_factor_by_name(name) + .unwrap_or_else(|| panic!("missing {name}")) + .eval(0.0) + .unwrap() + }; + // Duplicate C: the later file's value wins. + assert_relative_eq!(eval("C"), 60.0); + assert_relative_eq!(eval("N"), 7.0); + // Alias in the override resolves against base's N. + assert_relative_eq!(eval("CA"), 7.0); + } + #[test] fn read_formfactors_resolves_symbolic_excluded_solvent() { let yaml = r" diff --git a/src/lib.rs b/src/lib.rs index 0d7783b..2ff0fbf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,9 @@ pub use solvent::{ExcludedVolume, Hydration, SolventConfig}; // Crate-internal: the form-factor table and atom-kind types are part of // no public signature, so they stay off the public API surface. pub(crate) use atomkind::{AtomKind, FindByName}; -pub(crate) use formfactor::{FormFactorMap, read_formfactors, write_formfactors}; +pub(crate) use formfactor::{ + FormFactorMap, read_formfactors, read_formfactors_merged, write_formfactors, +}; /// Crate-internal short alias. Not re-exported; public signatures use /// `[f64; 3]` instead so the API doesn't leak `nalgebra` types. @@ -198,10 +200,21 @@ pub struct TrajectoryOptions<'a> { } impl Pripps { - /// Construct from a YAML form-factor definition file. + /// Construct from a single YAML form-factor definition file. pub fn from_yaml(path: &Path) -> Result { - let formfactors = read_formfactors(path)?; + Self::from_formfactors(read_formfactors(path)?) + } + + /// Construct from one or more YAML form-factor definition files, + /// merged in order. When a species is defined in more than one file + /// the later file wins and a warning is logged (see + /// [`read_formfactors_merged`]). Symbolic specs (`!Alias` / `!United` + /// / `!Linear`) may reference definitions from any of the files. + pub fn from_yaml_files(paths: &[std::path::PathBuf]) -> Result { + Self::from_formfactors(read_formfactors_merged(paths)?) + } + fn from_formfactors(formfactors: FormFactorMap) -> Result { if std::env::var("FF_TABLE").is_ok() { write_formfactors(&formfactors, Path::new("tabulated_formfactors.yaml"))?; } @@ -897,7 +910,9 @@ mod tests { /// rescales it back to a full average (not frame 0 × 1/N_frames). #[test] fn trajectory_stride_and_weights() { - let pripps = Pripps::from_yaml(Path::new("assets/gaussian_atoms.yaml")).unwrap(); + // 4lzt.xyz is a residue-bead structure, so it needs the CG + // single-bead form factors from poly_amino_acid.yaml. + let pripps = Pripps::from_yaml(Path::new("assets/poly_amino_acid.yaml")).unwrap(); let template = std::path::PathBuf::from("tests/lysozyme/4lzt.xyz"); let xtc = std::env::temp_dir().join("pripps_stride_test.xtc"); write_two_frame_xtc(&xtc); From 21580ec28c0f7a73a7fdceaac7e6ce44f874233d Mon Sep 17 00:00:00 2001 From: mlund Date: Fri, 19 Jun 2026 09:44:17 +0200 Subject: [PATCH 2/3] Address PR #14 review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename gaussian_atoms.yaml -> pepsi_atoms.yaml and document provenance: atomic five-Gaussians are Waasmaier & Kirfel (1995); united-atom groups and K=4π solvent convention are PepsiSAXS (Grudinin et al. 2017). Update all references (tests, script, README) and link DOIs inline. - Trim the README "Merging multiple files" section per review. - Use tempfile::tempdir() in the merge test to avoid fixed temp-file names colliding under parallel test runs. --- README.md | 27 ++++++------------- .../{gaussian_atoms.yaml => pepsi_atoms.yaml} | 9 ++++++- pripps-py/tests/test_smoke.py | 2 +- scripts/sasbdb_bench.py | 2 +- src/formfactor.rs | 6 ++--- src/lib.rs | 2 +- 6 files changed, 22 insertions(+), 26 deletions(-) rename assets/{gaussian_atoms.yaml => pepsi_atoms.yaml} (95%) diff --git a/README.md b/README.md index 38fa356..c140b1f 100644 --- a/README.md +++ b/README.md @@ -236,22 +236,11 @@ must define it when `--excluded per-kind` is selected. #### Merging multiple files -Several form-factor files can be supplied and are merged in the order -given — repeat `--atomfile` (or pass several paths) on the CLI, or pass a -list to the Python constructor: - -```bash -pripps --atomfile assets/gaussian_atoms.yaml --atomfile my_overrides.yaml ... -``` - -```python -pr = pripps.Pripps(["assets/gaussian_atoms.yaml", "my_overrides.yaml"]) -``` - -This lets you compose, e.g., a coarse-grained residue file with an atomic -file. **Duplicate policy:** if the same species is defined in more than one -file, the **last file listed wins** and a warning is logged naming the -overridden species. Merging happens before symbolic resolution, so an +Several form-factor files can be supplied and are merged in order — repeat +`--atomfile` on the CLI, or pass a list to the Python constructor — letting +you compose, e.g., a coarse-grained residue file with an atomic one. If the +same species appears in more than one file the **last file listed wins** and +a warning is logged. Merging happens before symbolic resolution, so an `!Alias` / `!United` / `!Linear` entry in one file may reference a species defined in another. @@ -260,8 +249,8 @@ defined in another. | File | Description | Best for | |------|-------------|----------| | `assets/foxs_formfactors.yaml` | Cromer-Mann (Int. Tables Vol. C) atomic form factors, FoXS-compatible volumes and SASA radii | Atomic PDBs with `--excluded foxs` | -| `assets/gaussian_atoms.yaml` | Atomic and united-atom Gaussians (CH/CH₂/NH/…), PepsiSAXS-style (K = 4π) | Atomic PDBs with PepsiSAXS-style solvent models | -| `assets/stovgaard2010.yaml` | Per-residue form-factor centroids (one-body model, q ∈ [0, 0.75] Å⁻¹) from Stovgaard et al. 2010, | Residue / Cα single-bead models | +| `assets/pepsi_atoms.yaml` | Atomic [Waasmaier-Kirfel](https://doi.org/10.1107/S0108767394013292) five-Gaussians + [PepsiSAXS](https://doi.org/10.1107/S2059798317005745) united-atom groups (CH/CH₂/NH/…), K = 4π | Atomic PDBs with PepsiSAXS-style solvent models | +| `assets/stovgaard2010.yaml` | Per-residue form-factor centroids (one-body model, q ∈ [0, 0.75] Å⁻¹) from [Stovgaard et al. 2010](https://doi.org/10.1186/1471-2105-11-429) | Residue / Cα single-bead models | | `assets/poly_amino_acid.yaml` | Amino-acid bead polynomials with a separate `excluded_solvent` polynomial per bead | Cα / single-bead with `--excluded per-kind` | | `assets/poly_amino_acid_combined.yaml` | Amino-acid bead polynomials with the excluded solvent already folded into the form factor | Cα / single-bead with `--excluded disabled` | | `assets/poly_martini.yaml` | Martini-3 bead polynomials with a separate `excluded_solvent` polynomial per bead | Martini-3 CG with `--excluded per-kind` | @@ -275,7 +264,7 @@ their respective tools. **Don't mix files with non-matching `--excluded` flags.** Each YAML + excluded-volume convention is a self-consistent bundle: `foxs_formfactors.yaml` was calibrated jointly with FoXS's -`K = 16π` Gaussian, while `gaussian_atoms.yaml` was calibrated with +`K = 16π` Gaussian, while `pepsi_atoms.yaml` was calibrated with PepsiSAXS's `K = 4π`. Swapping the denominator gives atomic and excluded contributions that were never fit together and yields subtly biased intensities. diff --git a/assets/gaussian_atoms.yaml b/assets/pepsi_atoms.yaml similarity index 95% rename from assets/gaussian_atoms.yaml rename to assets/pepsi_atoms.yaml index 1da56a2..bbf222a 100644 --- a/assets/gaussian_atoms.yaml +++ b/assets/pepsi_atoms.yaml @@ -1,4 +1,11 @@ -# Atomic / united-atom form factors for PRIPPS (PepsiSAXS-style K = 4π). +# Atomic / united-atom form factors for PRIPPS (PepsiSAXS-style, K = 4π). +# +# Atomic five-Gaussian scattering factors are the Waasmaier & Kirfel (1995) +# parametrization, valid for sinθ/λ ∈ [0, 6] Å⁻¹: +# https://doi.org/10.1107/S0108767394013292 +# combined with PepsiSAXS united-atom groups (CH/CH₂/NH/…) and the K = 4π +# solvent convention, Grudinin et al. (2017): +# https://doi.org/10.1107/S2059798317005745 # # Atomic-resolution structures only. Residue single-bead form factors live # in assets/poly_amino_acid.yaml — pass several --atomfile paths to merge diff --git a/pripps-py/tests/test_smoke.py b/pripps-py/tests/test_smoke.py index 39f1ac2..8b207c1 100644 --- a/pripps-py/tests/test_smoke.py +++ b/pripps-py/tests/test_smoke.py @@ -132,7 +132,7 @@ class TestPepsiFit(unittest.TestCase): def setUpClass(cls): cls.exp = load_lyzexp() q_max = float(cls.exp[-1, 0]) - pr = pripps.Pripps("../assets/gaussian_atoms.yaml") + pr = pripps.Pripps("../assets/pepsi_atoms.yaml") cls.model = pr.multipole_model( "../tests/lysozyme/4lzt.pdb", qmin=0.0, diff --git a/scripts/sasbdb_bench.py b/scripts/sasbdb_bench.py index 569529f..fe2c956 100755 --- a/scripts/sasbdb_bench.py +++ b/scripts/sasbdb_bench.py @@ -62,7 +62,7 @@ BENCH = REPO / "sasbdb_bench" CACHE = BENCH / "cache" FF_YAML_DEFAULT = REPO / "assets" / "foxs_formfactors.yaml" -# Override via --atomfile if you want gaussian_atoms.yaml for the +# Override via --atomfile if you want pepsi_atoms.yaml for the # PepsiSAXS-style K=4π convention. SASBDB_API = "https://www.sasbdb.org/rest-api/entry/summary/{}/" diff --git a/src/formfactor.rs b/src/formfactor.rs index 4a85305..6020549 100644 --- a/src/formfactor.rs +++ b/src/formfactor.rs @@ -829,9 +829,9 @@ C: CA: formfactor: !Alias N "; - let dir = std::env::temp_dir(); - let base_path = dir.join("pripps_merge_base.yaml"); - let over_path = dir.join("pripps_merge_override.yaml"); + let dir = tempfile::tempdir().unwrap(); + let base_path = dir.path().join("base.yaml"); + let over_path = dir.path().join("override.yaml"); std::fs::write(&base_path, base).unwrap(); std::fs::write(&over_path, override_).unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 2ff0fbf..061741b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -837,7 +837,7 @@ mod tests { // produce a physically reasonable profile. Not as good as // the FoXS pipeline (different coefficients, different // denominator) but the shape should be plausible. - let pripps = Pripps::from_yaml(std::path::Path::new("assets/gaussian_atoms.yaml")).unwrap(); + let pripps = Pripps::from_yaml(std::path::Path::new("assets/pepsi_atoms.yaml")).unwrap(); let pdb = std::path::PathBuf::from("tests/lysozyme/4lzt.pdb"); let solvent = SolventConfig { excluded: ExcludedVolume::Fraser { volume_scale: 1.0 }, From 851ad9bb4a565287a9fd55d5011dbd094d380371 Mon Sep 17 00:00:00 2001 From: mlund Date: Fri, 19 Jun 2026 10:00:19 +0200 Subject: [PATCH 3/3] Clarify gaussian ff in readme --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c140b1f..e6c1d9e 100644 --- a/README.md +++ b/README.md @@ -256,10 +256,12 @@ defined in another. | `assets/poly_martini.yaml` | Martini-3 bead polynomials with a separate `excluded_solvent` polynomial per bead | Martini-3 CG with `--excluded per-kind` | | `assets/poly_martini_combined.yaml` | Martini-3 bead polynomials with the excluded solvent already folded into the form factor | Martini-3 CG with `--excluded disabled` | -Gaussian files use b values in the standard s² = (q/4π)² convention -(International Tables / Cromer-Mann). The atomic coefficients differ -(Cromer-Mann vs Stovgaard-2010) and the volume/radius tables match -their respective tools. +Files using `!Gaussian` form factors (`foxs_formfactors.yaml`, +`pepsi_atoms.yaml`) give b values in the standard s² = (q/4π)² convention +(International Tables / Cromer-Mann), so published exponents can be used +directly. Their atomic coefficients come from different parametrizations +(Cromer-Mann for FoXS vs Waasmaier-Kirfel for Pepsi) and the volume/radius +tables match their respective tools. **Don't mix files with non-matching `--excluded` flags.** Each YAML + excluded-volume convention is a self-consistent bundle: