1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
use super::*;
/// One independently simulated structure contributing to the same measured χ.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct WeightedStructure {
/// Explicit atoms and fixed optional periodic cell.
pub configuration: Configuration,
/// Nonnegative mixture fraction. Fractions are normalized at session creation.
pub weight: f64,
/// Movable atom indices; None uses session indices (or all atoms), Some([]) fixes this structure.
pub movable_atoms: Option<Vec<usize>>,
}
/// EXAFS data plus an objective and independently chosen forward-model settings.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct RmcDataset {
/// Measured unweighted χ, fixed amplitude and noise scales, and initial
/// theoretical energy shift (fixed unless session refinement is enabled).
pub exafs: ExafsDataset,
/// Comparison space; K reproduces the original RMC objective.
pub objective: Objective,
/// Per-dataset ReFEFF settings; None uses calculator defaults.
pub refeff: Option<RefeffOptions>,
/// One absorber list per structure. Empty uses `exafs.absorbers` for every structure.
pub absorbers_by_structure: Vec<Vec<usize>>,
/// Captured processing state when built by [`Self::from_spectrum`]. Older
/// array-only jobs deserialize as None. Stored once in the problem/checkpoint,
/// never in individual trial states; no preprocessing runs during refinement.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<Box<RmcSpectrumSource>>,
}
impl From<ExafsDataset> for RmcDataset {
fn from(exafs: ExafsDataset) -> Self {
Self {
exafs,
objective: Objective::K,
refeff: None,
absorbers_by_structure: Vec::new(),
source: None,
}
}
}
/// A weighted mixture of structures jointly fitted to one or more datasets.
/// Components are physical mixture candidates, distinct from evolutionary individuals.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct EnsembleProblem {
/// Nonempty structures with stable component and atom indices.
pub structures: Vec<WeightedStructure>,
/// Nonempty datasets with unique names.
pub datasets: Vec<RmcDataset>,
}
impl EnsembleProblem {
/// Create a one-structure, one-dataset problem without changing its objective
/// or processing snapshot (since 0.2.10). Takes ownership, assigns unit mixture
/// weight, and uses the session's movable atoms. Validation runs at session
/// creation. Add further datasets/components through the public fields.
pub fn single(configuration: Configuration, dataset: RmcDataset) -> Self {
Self {
structures: vec![WeightedStructure {
configuration,
weight: 1.,
movable_atoms: None,
}],
datasets: vec![dataset],
}
}
}
impl From<RmcProblem> for EnsembleProblem {
fn from(problem: RmcProblem) -> Self {
Self {
structures: vec![WeightedStructure {
configuration: problem.configuration,
weight: 1.,
movable_atoms: None,
}],
datasets: problem.datasets.into_iter().map(Into::into).collect(),
}
}
}
/// Borrowed request for one absorber. The structure index identifies a pinned
/// reference structure in calculators that freeze scattering potentials.
#[derive(Clone, Copy)]
pub struct CalculationRequest<'a> {
/// Zero-based mixture component index, stable throughout a session.
pub structure: usize,
/// Current explicit geometry.
pub configuration: &'a Configuration,
/// Zero-based absorbing atom.
pub absorber: usize,
/// Absorption edge.
pub edge: crate::structure::Edge,
/// Strictly increasing theoretical wave numbers in Å⁻¹.
pub k: &'a [f64],
/// Dataset override of calculator defaults.
pub options: Option<&'a RefeffOptions>,
/// Request individual path contributions; unsupported backends must fail explicitly.
pub paths: bool,
}
/// One backend scattering path, including its contribution before absorber
/// averaging, mixture weighting and S₀². Degeneracy is already included in χ.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PathContribution {
/// Backend path number, local to this absorber calculation.
pub index: usize,
/// Number of propagation legs (two is single scattering).
pub legs: usize,
/// Equivalent-path multiplicity used by the backend.
pub degeneracy: f64,
/// Half of the total path length, in Å.
pub half_length: f64,
/// Unweighted dimensionless contribution on the request's k grid.
pub chi: Vec<f64>,
}
/// One absorber's calculated χ and optional decomposition.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CalculatedSpectrum {
/// Unweighted dimensionless χ, S₀²=1, no additional disorder damping.
pub chi: Vec<f64>,
/// Empty unless path output was requested.
pub paths: Vec<PathContribution>,
}
/// Path report with sufficient indices to reconstruct mixture contributions.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AbsorberPaths {
/// Dataset index in the problem.
pub dataset: usize,
/// Structure index in the mixture.
pub structure: usize,
/// Atom index of the absorber.
pub absorber: usize,
/// Paths before mixture/absorber/amplitude scaling.
pub paths: Vec<PathContribution>,
}
/// Evaluated mixture with cached component spectra. Weight moves need no new
/// scattering calculation; a coordinate move recalculates only its component.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EnsembleState {
/// Since 0.2.11: theoretical ΔE₀ for each dataset, in eV. Populated when energy
/// refinement is enabled. Empty historical/fixed states use the input values;
/// prefer [`Self::energy_shifts`] to resolve either representation.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub delta_e0: Vec<f64>,
/// Current configurations and normalized mixture fractions.
pub structures: Vec<WeightedStructure>,
/// Total objective, including structural penalties, and mixture spectra.
pub evaluation: Evaluation,
/// Structural contribution to `evaluation.score`.
pub penalty: f64,
/// Unscaled absorber-averaged χ indexed by dataset, structure, k point.
pub component_chi: Vec<Vec<Vec<f64>>>,
/// Optional per-absorber path output; enabled through session settings.
pub paths: Vec<AbsorberPaths>,
}
impl EnsembleState {
/// Resolve each dataset's theoretical energy shift in eV, including fixed
/// values from historical states. Returns an owned vector; neither input is
/// changed. Invalid state dimensions or nonfinite shifts return an error.
pub fn energy_shifts(&self, problem: &EnsembleProblem) -> Result<Vec<f64>, RmcError> {
let shifts = if self.delta_e0.is_empty() {
problem.datasets.iter().map(|d| d.exafs.delta_e0).collect()
} else {
require(
self.delta_e0.len() == problem.datasets.len(),
"state ΔE₀ count differs from datasets",
)?;
self.delta_e0.clone()
};
require(shifts.iter().all(|v| v.is_finite()), "nonfinite state ΔE₀")?;
Ok(shifts)
}
}