1use std::collections::{BTreeMap, BTreeSet};
4use std::error::Error;
5use std::fmt::{Display, Formatter};
6
7use phasesmith_model::RecordId;
8
9use crate::{
10 LatticeBounds, ParameterError, ParameterKey, ParameterSet, ParameterSpec,
11 PreparedGeneralRietveldObjective, RietveldCalculationOptions, RietveldGeneralObjectiveError,
12 RietveldGeneralParameterError, RietveldInput, RietveldParameterLayout,
13 RietveldParameterSelection,
14};
15
16#[derive(Clone, Debug, PartialEq)]
18pub struct JointRietveldHistogram {
19 pub histogram_id: RecordId,
21 pub input: RietveldInput,
23 pub selection: RietveldParameterSelection,
25 pub lattice_bounds: Vec<Option<LatticeBounds>>,
27 pub calculation: RietveldCalculationOptions,
29}
30
31#[derive(Clone, Debug, PartialEq)]
33pub struct JointRietveldProduct {
34 pub histogram_id: RecordId,
36 pub profile: Vec<f64>,
38 pub derivative: Vec<f64>,
40}
41
42#[derive(Clone, Debug, PartialEq)]
44pub struct JointRietveldGradient {
45 pub calculated: Vec<Vec<f64>>,
47 pub objective: f64,
49 pub gradient: Vec<f64>,
51}
52
53#[derive(Clone, Debug, PartialEq)]
55pub struct JointRietveldLayout {
56 parameters: ParameterSet,
57 histogram_ids: Vec<RecordId>,
58 local_layouts: Vec<RietveldParameterLayout>,
59 local_to_joint: Vec<Vec<usize>>,
60}
61
62impl JointRietveldLayout {
63 pub fn new(histograms: &[JointRietveldHistogram]) -> Result<Self, JointRietveldError> {
74 validate_histograms(histograms)?;
75 let local_layouts = histograms
76 .iter()
77 .map(|histogram| {
78 RietveldParameterLayout::new(
79 &histogram.input,
80 &histogram.selection,
81 &histogram.lattice_bounds,
82 )
83 })
84 .collect::<Result<Vec<_>, _>>()?;
85
86 let mut specs = Vec::new();
87 let mut shared = BTreeMap::<ParameterKey, usize>::new();
88 let mut local_to_joint = Vec::with_capacity(histograms.len());
89 for (histogram, layout) in histograms.iter().zip(&local_layouts) {
90 let mut mapping = Vec::with_capacity(layout.parameters().specs().len());
91 for spec in layout.parameters().specs() {
92 let joint_index = if is_shared(spec.key()) {
93 if let Some(index) = shared.get(spec.key()).copied() {
94 if specs[index] != *spec {
95 return Err(JointRietveldError::SharedParameterMismatch {
96 key: spec.key().clone(),
97 });
98 }
99 index
100 } else {
101 let index = specs.len();
102 specs.push(spec.clone());
103 shared.insert(spec.key().clone(), index);
104 index
105 }
106 } else {
107 let key = ParameterKey::new(
108 spec.key().module(),
109 format!("{}/{}", histogram.histogram_id, spec.key().owner_id()),
110 spec.key().name(),
111 )?;
112 let index = specs.len();
113 specs.push(ParameterSpec::new(
114 key,
115 spec.value(),
116 spec.unit(),
117 spec.bounds(),
118 spec.scale(),
119 spec.refine(),
120 )?);
121 index
122 };
123 mapping.push(joint_index);
124 }
125 local_to_joint.push(mapping);
126 }
127 Ok(Self {
128 parameters: ParameterSet::new(specs)?,
129 histogram_ids: histograms
130 .iter()
131 .map(|histogram| histogram.histogram_id.clone())
132 .collect(),
133 local_layouts,
134 local_to_joint,
135 })
136 }
137
138 #[must_use]
140 pub const fn parameters(&self) -> &ParameterSet {
141 &self.parameters
142 }
143
144 #[must_use]
146 pub fn histogram_ids(&self) -> &[RecordId] {
147 &self.histogram_ids
148 }
149
150 pub fn apply_values(
157 &self,
158 histograms: &[JointRietveldHistogram],
159 values: &[f64],
160 ) -> Result<Vec<JointRietveldHistogram>, JointRietveldError> {
161 self.validate_contract(histograms)?;
162 if values.len() != self.parameters.specs().len() {
163 return Err(JointRietveldError::ValueLengthMismatch);
164 }
165 histograms
166 .iter()
167 .enumerate()
168 .map(|(histogram_index, histogram)| {
169 let local_values = self.local_to_joint[histogram_index]
170 .iter()
171 .map(|index| values[*index])
172 .collect::<Vec<_>>();
173 let mut updated = histogram.clone();
174 updated.input = self.local_layouts[histogram_index]
175 .apply_values(&histogram.input, &local_values)?;
176 Ok(updated)
177 })
178 .collect()
179 }
180
181 fn validate_contract(
182 &self,
183 histograms: &[JointRietveldHistogram],
184 ) -> Result<(), JointRietveldError> {
185 if histograms.len() != self.histogram_ids.len()
186 || histograms
187 .iter()
188 .zip(&self.histogram_ids)
189 .any(|(histogram, expected)| histogram.histogram_id != *expected)
190 {
191 return Err(JointRietveldError::HistogramContractMismatch);
192 }
193 validate_histograms(histograms)?;
194 for ((histogram, expected_layout), expected_mapping) in histograms
195 .iter()
196 .zip(&self.local_layouts)
197 .zip(&self.local_to_joint)
198 {
199 let current_layout = RietveldParameterLayout::new(
200 &histogram.input,
201 &histogram.selection,
202 &histogram.lattice_bounds,
203 )?;
204 if current_layout != *expected_layout
205 || current_layout.parameters().specs().len() != expected_mapping.len()
206 {
207 return Err(JointRietveldError::HistogramContractMismatch);
208 }
209 }
210 Ok(())
211 }
212
213 fn local_direction(
214 &self,
215 histogram_index: usize,
216 direction: &[f64],
217 ) -> Result<Vec<f64>, JointRietveldError> {
218 if direction.len() != self.parameters.specs().len() {
219 return Err(JointRietveldError::ValueLengthMismatch);
220 }
221 Ok(self.local_to_joint[histogram_index]
222 .iter()
223 .map(|index| direction[*index])
224 .collect())
225 }
226
227 fn scatter_add(
228 &self,
229 histogram_index: usize,
230 local: &[f64],
231 joint: &mut [f64],
232 ) -> Result<(), JointRietveldError> {
233 if local.len() != self.local_to_joint[histogram_index].len() {
234 return Err(JointRietveldError::LocalProductLengthMismatch);
235 }
236 for (value, index) in local.iter().zip(&self.local_to_joint[histogram_index]) {
237 joint[*index] += value;
238 }
239 Ok(())
240 }
241}
242
243pub struct PreparedJointRietveldObjective {
245 histograms: Vec<JointRietveldHistogram>,
246 layout: JointRietveldLayout,
247 objectives: Vec<PreparedGeneralRietveldObjective>,
248}
249
250impl PreparedJointRietveldObjective {
251 pub fn new(
257 histograms: Vec<JointRietveldHistogram>,
258 layout: JointRietveldLayout,
259 ) -> Result<Self, JointRietveldError> {
260 layout.validate_contract(&histograms)?;
261 let objectives = histograms
262 .iter()
263 .zip(&layout.local_layouts)
264 .map(|(histogram, local_layout)| {
265 PreparedGeneralRietveldObjective::new(
266 histogram.input.clone(),
267 histogram.calculation.clone(),
268 local_layout.clone(),
269 )
270 })
271 .collect::<Result<Vec<_>, _>>()?;
272 Ok(Self {
273 histograms,
274 layout,
275 objectives,
276 })
277 }
278
279 #[must_use]
281 pub const fn layout(&self) -> &JointRietveldLayout {
282 &self.layout
283 }
284
285 #[must_use]
287 pub fn preparation_evaluation_count(&self) -> usize {
288 self.objectives
289 .iter()
290 .map(PreparedGeneralRietveldObjective::preparation_evaluation_count)
291 .sum()
292 }
293
294 #[must_use]
296 pub fn normal_product_evaluation_count(&self) -> usize {
297 self.objectives
298 .iter()
299 .map(PreparedGeneralRietveldObjective::normal_product_evaluation_count)
300 .sum()
301 }
302
303 pub fn jvp(&self, direction: &[f64]) -> Result<Vec<JointRietveldProduct>, JointRietveldError> {
309 self.objectives
310 .iter()
311 .enumerate()
312 .map(|(index, objective)| {
313 let local = self.layout.local_direction(index, direction)?;
314 let (profile, derivative) = objective.jvp(&local)?;
315 Ok(JointRietveldProduct {
316 histogram_id: self.histograms[index].histogram_id.clone(),
317 profile,
318 derivative,
319 })
320 })
321 .collect()
322 }
323
324 pub fn vjp(&self, sample_weights: &[Vec<f64>]) -> Result<Vec<f64>, JointRietveldError> {
333 if sample_weights.len() != self.objectives.len() {
334 return Err(JointRietveldError::HistogramProductCountMismatch);
335 }
336 let mut result = vec![0.0; self.layout.parameters.specs().len()];
337 for (index, (objective, weights)) in self.objectives.iter().zip(sample_weights).enumerate()
338 {
339 self.layout
340 .scatter_add(index, &objective.vjp(weights)?, &mut result)?;
341 }
342 Ok(result)
343 }
344
345 pub fn normal_product(
351 &self,
352 direction: &[f64],
353 damping: f64,
354 ) -> Result<Vec<f64>, JointRietveldError> {
355 if !damping.is_finite() || damping < 0.0 {
356 return Err(JointRietveldError::InvalidDamping);
357 }
358 let mut result = vec![0.0; self.layout.parameters.specs().len()];
359 for (index, objective) in self.objectives.iter().enumerate() {
360 let local = self.layout.local_direction(index, direction)?;
361 let local_product = objective.normal_product(&local, 0.0)?;
362 self.layout
363 .scatter_add(index, &local_product, &mut result)?;
364 }
365 for (value, direction) in result.iter_mut().zip(direction) {
366 *value += damping * direction;
367 }
368 Ok(result)
369 }
370
371 pub fn gradient(&self) -> Result<JointRietveldGradient, JointRietveldError> {
377 let mut calculated = Vec::with_capacity(self.objectives.len());
378 let mut gradient = vec![0.0; self.layout.parameters.specs().len()];
379 let mut value = 0.0;
380 for (index, objective) in self.objectives.iter().enumerate() {
381 let (profile, local_gradient) = objective.gradient()?;
382 value += 0.5 * objective.calculation().metrics.chi_square;
383 calculated.push(profile);
384 self.layout
385 .scatter_add(index, &local_gradient, &mut gradient)?;
386 }
387 Ok(JointRietveldGradient {
388 calculated,
389 objective: value,
390 gradient,
391 })
392 }
393}
394
395fn is_shared(key: &ParameterKey) -> bool {
396 matches!(key.module(), "lattice" | "site")
397}
398
399fn validate_histograms(histograms: &[JointRietveldHistogram]) -> Result<(), JointRietveldError> {
400 if histograms.len() < 2 {
401 return Err(JointRietveldError::TooFewHistograms);
402 }
403 if histograms
404 .iter()
405 .map(|histogram| &histogram.histogram_id)
406 .collect::<BTreeSet<_>>()
407 .len()
408 != histograms.len()
409 {
410 return Err(JointRietveldError::DuplicateHistogramId);
411 }
412 let shared_selection = histograms[0].selection.structural;
413 if histograms.iter().skip(1).any(|histogram| {
414 let selection = histogram.selection.structural;
415 selection.lattice != shared_selection.lattice
416 || selection.coordinates != shared_selection.coordinates
417 || selection.occupancy != shared_selection.occupancy
418 || selection.u_iso != shared_selection.u_iso
419 }) {
420 return Err(JointRietveldError::SharedSelectionMismatch);
421 }
422 let mut phases = BTreeMap::new();
423 for histogram in histograms {
424 histogram.input.validate()?;
425 for phase in &histogram.input.phases {
426 let definition = phase.definition();
427 let contract = (
428 phase.site_ids(),
429 definition.cell,
430 &definition.space_group,
431 &definition.fractional_xyz,
432 &definition.occupancy,
433 &definition.u_iso_angstrom2,
434 &definition.anisotropic_mask,
435 &definition.u_aniso_cif_angstrom2,
436 definition.coordinate_tolerance.to_bits(),
437 );
438 if let Some(previous) = phases.insert(phase.phase_id().clone(), contract) {
439 if previous != contract {
440 return Err(JointRietveldError::SharedPhaseMismatch {
441 phase_id: phase.phase_id().clone(),
442 });
443 }
444 }
445 }
446 }
447 Ok(())
448}
449
450#[derive(Debug)]
452pub enum JointRietveldError {
453 TooFewHistograms,
455 DuplicateHistogramId,
457 SharedSelectionMismatch,
459 SharedPhaseMismatch {
461 phase_id: RecordId,
463 },
464 SharedParameterMismatch {
466 key: ParameterKey,
468 },
469 HistogramContractMismatch,
471 ValueLengthMismatch,
473 HistogramProductCountMismatch,
475 LocalProductLengthMismatch,
477 InvalidDamping,
479 Parameter(ParameterError),
481 GeneralParameter(RietveldGeneralParameterError),
483 GeneralObjective(RietveldGeneralObjectiveError),
485}
486
487impl Display for JointRietveldError {
488 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
489 match self {
490 Self::TooFewHistograms => {
491 formatter.write_str("joint Rietveld objective requires at least two histograms")
492 }
493 Self::DuplicateHistogramId => {
494 formatter.write_str("joint Rietveld histogram IDs must be unique")
495 }
496 Self::SharedSelectionMismatch => {
497 formatter.write_str("joint Rietveld shared structural selections must match")
498 }
499 Self::SharedPhaseMismatch { phase_id } => write!(
500 formatter,
501 "joint Rietveld phase {phase_id:?} has incompatible shared structural state"
502 ),
503 Self::SharedParameterMismatch { key } => {
504 write!(
505 formatter,
506 "joint Rietveld shared parameter {key} is incompatible"
507 )
508 }
509 Self::HistogramContractMismatch => {
510 formatter.write_str("joint Rietveld histogram contract changed under the layout")
511 }
512 Self::ValueLengthMismatch => {
513 formatter.write_str("joint Rietveld value/direction length is wrong")
514 }
515 Self::HistogramProductCountMismatch => {
516 formatter.write_str("joint Rietveld histogram reverse-product count is wrong")
517 }
518 Self::LocalProductLengthMismatch => {
519 formatter.write_str("joint Rietveld local product length is wrong")
520 }
521 Self::InvalidDamping => {
522 formatter.write_str("joint Rietveld damping must be finite and non-negative")
523 }
524 Self::Parameter(error) => Display::fmt(error, formatter),
525 Self::GeneralParameter(error) => Display::fmt(error, formatter),
526 Self::GeneralObjective(error) => Display::fmt(error, formatter),
527 }
528 }
529}
530
531impl Error for JointRietveldError {
532 fn source(&self) -> Option<&(dyn Error + 'static)> {
533 match self {
534 Self::Parameter(error) => Some(error),
535 Self::GeneralParameter(error) => Some(error),
536 Self::GeneralObjective(error) => Some(error),
537 Self::TooFewHistograms
538 | Self::DuplicateHistogramId
539 | Self::SharedSelectionMismatch
540 | Self::SharedPhaseMismatch { .. }
541 | Self::SharedParameterMismatch { .. }
542 | Self::HistogramContractMismatch
543 | Self::ValueLengthMismatch
544 | Self::HistogramProductCountMismatch
545 | Self::LocalProductLengthMismatch
546 | Self::InvalidDamping => None,
547 }
548 }
549}
550
551impl From<ParameterError> for JointRietveldError {
552 fn from(value: ParameterError) -> Self {
553 Self::Parameter(value)
554 }
555}
556
557impl From<RietveldGeneralParameterError> for JointRietveldError {
558 fn from(value: RietveldGeneralParameterError) -> Self {
559 Self::GeneralParameter(value)
560 }
561}
562
563impl From<RietveldGeneralObjectiveError> for JointRietveldError {
564 fn from(value: RietveldGeneralObjectiveError) -> Self {
565 Self::GeneralObjective(value)
566 }
567}
568
569impl From<crate::RietveldError> for JointRietveldError {
570 fn from(value: crate::RietveldError) -> Self {
571 Self::GeneralParameter(RietveldGeneralParameterError::Rietveld(value))
572 }
573}