1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use nalgebra::DMatrix;
7
8use crate::rietveld_solver::{
9 conjugate_gradient, norm, normal_stop, reserve_products, topology_change,
10};
11use crate::{
12 BackgroundModel, CancellationToken, Constraint, ConstraintDerivativeMatrix, ConstraintError,
13 ConstraintTransform, DiagnosticValue, LatticeBounds, ParameterChange, ParameterKey,
14 ParameterSet, PreparedGeneralRietveldObjective, RefinementEventKind, RefinementRuntime,
15 ResidualOptions, RietveldCalculation, RietveldGeneralParameterError, RietveldInput,
16 RietveldInstrumentParameter, RietveldIterationRecord, RietveldParameterLayout,
17 RietveldParameterSelection, RietveldRefinementError, RietveldRefinementOptions, RuntimeError,
18 TerminationReason, calculate_rietveld_pattern, evaluate_residuals,
19};
20
21#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct RietveldCovarianceOptions {
24 pub enabled: bool,
26 pub max_parameters: usize,
28 pub unresolved_correlation: f64,
30}
31
32impl RietveldCovarianceOptions {
33 pub fn new(
40 enabled: bool,
41 max_parameters: usize,
42 unresolved_correlation: f64,
43 ) -> Result<Self, RietveldGeneralRefinementError> {
44 if max_parameters == 0
45 || !unresolved_correlation.is_finite()
46 || !(0.0..=1.0).contains(&unresolved_correlation)
47 {
48 return Err(RietveldGeneralRefinementError::InvalidCovarianceOptions);
49 }
50 Ok(Self {
51 enabled,
52 max_parameters,
53 unresolved_correlation,
54 })
55 }
56}
57
58impl Default for RietveldCovarianceOptions {
59 fn default() -> Self {
60 Self {
61 enabled: true,
62 max_parameters: 64,
63 unresolved_correlation: 1.0 - 1.0e-10,
64 }
65 }
66}
67
68#[derive(Clone, Debug, PartialEq)]
70pub struct RietveldParameterCorrelation {
71 pub left: ParameterKey,
73 pub right: ParameterKey,
75 pub correlation: f64,
77}
78
79#[derive(Clone, Debug, PartialEq)]
85pub struct RietveldCovarianceMatrix {
86 pub size: usize,
88 pub values: Vec<f64>,
90}
91
92#[derive(Clone, Debug, PartialEq)]
94pub struct RietveldGeneralCheckpoint {
95 pub completed_iterations: usize,
97 pub input: RietveldInput,
99 pub selection: RietveldParameterSelection,
101 pub lattice_bounds: Vec<Option<LatticeBounds>>,
103 pub constraints: Vec<Constraint>,
105 pub parameters: ParameterSet,
107 pub objective: f64,
109 pub damping: f64,
111 pub history: Vec<RietveldIterationRecord>,
113}
114
115impl RietveldGeneralCheckpoint {
116 pub fn validate_for(
123 &self,
124 requested: &RietveldInput,
125 selection: &RietveldParameterSelection,
126 lattice_bounds: &[Option<LatticeBounds>],
127 constraints: &[Constraint],
128 ) -> Result<(), RietveldGeneralRefinementError> {
129 self.input.validate()?;
130 let invalid = if &self.selection != selection {
131 Some("parameter selection changed")
132 } else if self.lattice_bounds != lattice_bounds {
133 Some("lattice bounds changed")
134 } else if self.constraints != constraints {
135 Some("constraints changed")
136 } else if !checkpoint_request_compatible(&self.input, requested, selection) {
137 Some("request contract changed")
138 } else if self.completed_iterations != self.history.len() {
139 Some("accepted iteration count does not match history")
140 } else if !self.objective.is_finite() || self.objective < 0.0 {
141 Some("objective is invalid")
142 } else if !self.damping.is_finite() || self.damping <= 0.0 {
143 Some("damping is invalid")
144 } else if !valid_history(&self.history, requested) {
145 Some("history is invalid")
146 } else {
147 None
148 };
149 if let Some(reason) = invalid {
150 return Err(RietveldGeneralRefinementError::InvalidCheckpoint { reason });
151 }
152 let accepted_layout = RietveldParameterLayout::new(&self.input, selection, lattice_bounds)?;
153 let requested_layout = RietveldParameterLayout::new(requested, selection, lattice_bounds)?;
154 if !parameter_contract_matches(requested_layout.parameters(), &self.parameters)
155 || !parameter_values_match(accepted_layout.parameters(), &self.parameters)
156 {
157 return Err(RietveldGeneralRefinementError::InvalidCheckpoint {
158 reason: "parameter contract changed",
159 });
160 }
161 let transform = ConstraintTransform::new(self.parameters.clone(), constraints.to_vec())?;
162 validate_constraint_state(&accepted_layout, &transform)?;
163 Ok(())
164 }
165}
166
167#[derive(Clone, Debug, PartialEq)]
169pub struct RietveldGeneralRefinementResult {
170 pub calculation: RietveldCalculation,
172 pub input: RietveldInput,
174 pub parameters: ParameterSet,
176 pub free_keys: Vec<ParameterKey>,
178 pub history: Vec<RietveldIterationRecord>,
180 pub termination_reason: TerminationReason,
182 pub checkpoint: RietveldGeneralCheckpoint,
184 pub evaluations: usize,
186 pub jacobian_rank: Option<usize>,
188 pub covariance: Option<RietveldCovarianceMatrix>,
190 pub unresolved_correlations: Vec<RietveldParameterCorrelation>,
192}
193
194#[allow(clippy::too_many_arguments)]
201pub fn refine_general_rietveld(
202 input: &RietveldInput,
203 selection: &RietveldParameterSelection,
204 lattice_bounds: &[Option<LatticeBounds>],
205 constraints: &[Constraint],
206 options: &RietveldRefinementOptions,
207 covariance: RietveldCovarianceOptions,
208 checkpoint: Option<&RietveldGeneralCheckpoint>,
209 cancellation: Option<CancellationToken>,
210) -> Result<RietveldGeneralRefinementResult, RietveldGeneralRefinementError> {
211 let mut runtime = RefinementRuntime::new(options.limits, cancellation)?;
212 refine_general_rietveld_with_runtime(
213 input,
214 selection,
215 lattice_bounds,
216 constraints,
217 options,
218 covariance,
219 checkpoint,
220 &mut runtime,
221 )
222}
223
224#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
231pub fn refine_general_rietveld_with_runtime(
232 input: &RietveldInput,
233 selection: &RietveldParameterSelection,
234 lattice_bounds: &[Option<LatticeBounds>],
235 constraints: &[Constraint],
236 options: &RietveldRefinementOptions,
237 covariance: RietveldCovarianceOptions,
238 checkpoint: Option<&RietveldGeneralCheckpoint>,
239 runtime: &mut RefinementRuntime<RietveldGeneralCheckpoint>,
240) -> Result<RietveldGeneralRefinementResult, RietveldGeneralRefinementError> {
241 input.validate()?;
242 options.validate()?;
243 covariance.validate()?;
244 let initial_layout = RietveldParameterLayout::new(input, selection, lattice_bounds)?;
245 let initial_transform =
246 ConstraintTransform::new(initial_layout.parameters().clone(), constraints.to_vec())?;
247 validate_constraint_state(&initial_layout, &initial_transform)?;
248 let stable_layout = initial_layout.clone();
249 let (mut live_input, mut history, mut damping, mut live_parameters) =
250 if let Some(checkpoint) = checkpoint {
251 checkpoint.validate_for(input, selection, lattice_bounds, constraints)?;
252 runtime.resume_accepted(checkpoint.completed_iterations)?;
253 (
254 checkpoint.input.clone(),
255 checkpoint.history.clone(),
256 checkpoint.damping,
257 checkpoint.parameters.clone(),
258 )
259 } else {
260 (
261 input.clone(),
262 Vec::new(),
263 options.initial_damping,
264 initial_layout.parameters().clone(),
265 )
266 };
267 runtime.emit(
268 RefinementEventKind::Start,
269 "rietveld",
270 "native complete Rietveld refinement started",
271 Vec::new(),
272 )?;
273 let has_observations = input
274 .pattern
275 .mask
276 .as_ref()
277 .is_none_or(|mask| mask.iter().any(|included| *included));
278 let mut termination = if has_observations {
279 TerminationReason::MaxIterations
280 } else {
281 TerminationReason::NoObservations
282 };
283 let first_iteration = history.len() + 1;
284 let last_iteration = if has_observations {
285 options.limits.max_iterations()
286 } else {
287 history.len()
288 };
289 let mut prepared_objective = None;
290 let mut final_calculation = None;
291 'iterations: for iteration in first_iteration..=last_iteration {
292 if let Err(error) = runtime.begin_iteration(iteration) {
293 termination = normal_stop(&error)?;
294 break;
295 }
296 let objective = if let Some(objective) = prepared_objective.take() {
297 objective
298 } else {
299 let objective = PreparedGeneralRietveldObjective::new(
300 live_input.clone(),
301 options.calculation.clone(),
302 stable_layout.clone(),
303 )?;
304 if let Err(error) = reserve_products(runtime, objective.preparation_evaluation_count())
305 {
306 termination = normal_stop(&error)?;
307 break;
308 }
309 objective
310 };
311 final_calculation = Some(objective.calculation().clone());
312 let layout = objective.layout().clone();
313 let solver_parameters = live_parameters.clone();
314 let transform = ConstraintTransform::new(solver_parameters.clone(), constraints.to_vec())?;
315 if transform.free_keys().is_empty() {
316 termination = TerminationReason::Converged;
317 break;
318 }
319 let derivative = transform.derivative_matrix()?;
320 let free_linearization = objective.free_linearization(&derivative)?;
321 let scaled_gradient = if let Some(linearization) = &free_linearization {
322 let observed = input
323 .pattern
324 .observed_y
325 .as_deref()
326 .ok_or(RietveldGeneralRefinementError::InternalInvariant)?;
327 linearization.gradient(observed)?
328 } else {
329 let (_, physical_gradient) = objective.gradient()?;
330 transpose_product(&derivative, &physical_gradient)
331 };
332 let right_hand_side = scaled_gradient
333 .iter()
334 .map(|value| -value)
335 .collect::<Vec<_>>();
336 let solve = conjugate_gradient(
337 &right_hand_side,
338 options.cg_tolerance,
339 options.max_cg_iterations,
340 |direction| {
341 if let Some(linearization) = &free_linearization {
342 Ok(linearization.normal_product(direction, damping)?)
343 } else {
344 reserve_products(runtime, objective.normal_product_evaluation_count())?;
345 let physical = forward_product(&derivative, direction);
346 let physical_product = objective.normal_product(&physical, 0.0)?;
347 let mut result = transpose_product(&derivative, &physical_product);
348 for (value, direction) in result.iter_mut().zip(direction) {
349 *value += damping * direction;
350 }
351 Ok(result)
352 }
353 },
354 );
355 let (mut step, cg_iterations) = match solve {
356 Ok(result) => result,
357 Err(RietveldRefinementError::Runtime(RuntimeError::Stopped(stop))) => {
358 termination = stop.reason;
359 break 'iterations;
360 }
361 Err(error) => return Err(error.into()),
362 };
363 let mut step_norm = norm(&step);
364 if step_norm > options.max_scaled_parameter_step {
365 let factor = options.max_scaled_parameter_step / step_norm;
366 for value in &mut step {
367 *value *= factor;
368 }
369 step_norm = options.max_scaled_parameter_step;
370 }
371 if step_norm < options.parameter_tolerance {
372 termination = TerminationReason::Converged;
373 break;
374 }
375 let current_calculation = objective.calculation().clone();
376 let current_objective = 0.5 * current_calculation.metrics.chi_square;
377 let current_values = solver_parameters
378 .specs()
379 .iter()
380 .map(crate::ParameterSpec::value)
381 .collect::<Vec<_>>();
382 let packed = transform.pack()?;
383 let mut accepted = None;
384 for backtrack in 0..=options.max_backtracks {
385 let factor = 0.5_f64.powi(i32::try_from(backtrack).unwrap_or(i32::MAX));
386 let trial_free = packed
387 .iter()
388 .zip(&step)
389 .map(|(value, step)| value + factor * step)
390 .collect::<Vec<_>>();
391 let trial_map = match transform.unpack(&trial_free, true) {
392 Ok(values) => values,
393 Err(ConstraintError::ExpandedValueOutsideBounds { .. }) => {
394 emit_rejected_trial(runtime, "constraint result outside physical bounds")?;
395 if let Err(error) = runtime.reject_step() {
396 termination = normal_stop(&error)?;
397 break 'iterations;
398 }
399 continue;
400 }
401 Err(error) => return Err(error.into()),
402 };
403 let trial_values = solver_parameters
404 .specs()
405 .iter()
406 .map(|spec| {
407 trial_map
408 .get(spec.key())
409 .copied()
410 .ok_or(RietveldGeneralRefinementError::InternalInvariant)
411 })
412 .collect::<Result<Vec<_>, _>>()?;
413 let Ok(trial_input) =
414 layout.apply_value_change(&live_input, ¤t_values, &trial_values)
415 else {
416 emit_rejected_trial(runtime, "trial outside the numerical model domain")?;
417 if let Err(error) = runtime.reject_step() {
418 termination = normal_stop(&error)?;
419 break 'iterations;
420 }
421 continue;
422 };
423 let (trial_calculation, trial_objective_state) = if objective.uses_dense_linearization()
424 {
425 if let Err(error) = runtime.begin_evaluation() {
426 termination = normal_stop(&error)?;
427 break 'iterations;
428 }
429 let Ok(trial_objective) = PreparedGeneralRietveldObjective::new(
430 trial_input.clone(),
431 options.calculation.clone(),
432 stable_layout.clone(),
433 ) else {
434 emit_rejected_trial(runtime, "trial outside the calculation domain")?;
435 if let Err(error) = runtime.reject_step() {
436 termination = normal_stop(&error)?;
437 break 'iterations;
438 }
439 continue;
440 };
441 (trial_objective.calculation().clone(), Some(trial_objective))
442 } else {
443 if let Err(error) = runtime.begin_evaluation() {
444 termination = normal_stop(&error)?;
445 break 'iterations;
446 }
447 let Ok(trial_calculation) =
448 calculate_rietveld_pattern(&trial_input, &options.calculation)
449 else {
450 emit_rejected_trial(runtime, "trial outside the calculation domain")?;
451 if let Err(error) = runtime.reject_step() {
452 termination = normal_stop(&error)?;
453 break 'iterations;
454 }
455 continue;
456 };
457 (trial_calculation, None)
458 };
459 let trial_objective = 0.5 * trial_calculation.metrics.chi_square;
460 runtime.emit(
461 RefinementEventKind::Trial,
462 "rietveld_step",
463 "native complete Rietveld trial evaluated",
464 vec![(
465 "objective".to_owned(),
466 DiagnosticValue::Float(trial_objective),
467 )],
468 )?;
469 if trial_objective < current_objective {
470 accepted = Some((
471 backtrack,
472 factor,
473 trial_values,
474 trial_input,
475 trial_calculation,
476 trial_objective_state,
477 trial_objective,
478 ));
479 break;
480 }
481 if let Err(error) = runtime.reject_step() {
482 termination = normal_stop(&error)?;
483 break;
484 }
485 }
486 let Some((
487 backtracks,
488 factor,
489 trial_values,
490 trial_input,
491 trial_calculation,
492 trial_objective_state,
493 objective,
494 )) = accepted
495 else {
496 damping *= options.damping_increase;
497 if termination == TerminationReason::MaxIterations {
498 continue;
499 }
500 break;
501 };
502 let objective_change = current_objective - objective;
503 let parameter_changes = solver_parameters
504 .specs()
505 .iter()
506 .zip(¤t_values)
507 .zip(&trial_values)
508 .filter(|((_, before), after)| before.to_bits() != after.to_bits())
509 .map(|((spec, before), after)| ParameterChange {
510 key: spec.key().clone(),
511 before: *before,
512 after: *after,
513 scaled_change: (after - before) / spec.scale(),
514 })
515 .collect::<Vec<_>>();
516 let topology_changes = live_input
517 .phases
518 .iter()
519 .zip(&trial_input.phases)
520 .filter_map(|(before, after)| topology_change(before, after))
521 .collect::<Vec<_>>();
522 let accepted_metrics = evaluate_residuals(
523 &input.pattern,
524 &trial_calculation.y,
525 ResidualOptions {
526 use_uncertainty: options.calculation.use_uncertainty,
527 parameter_count: transform.free_keys().len(),
528 },
529 )?;
530 history.push(RietveldIterationRecord {
531 iteration: history.len() + 1,
532 objective,
533 objective_change,
534 scaled_step_norm: factor * step_norm,
535 damping,
536 cg_iterations,
537 backtracks,
538 parameter_changes,
539 topology_changes: topology_changes.clone(),
540 rwp: accepted_metrics.rwp,
541 rp: accepted_metrics.rp,
542 chi_square: accepted_metrics.chi_square,
543 reduced_chi_square: accepted_metrics.reduced_chi_square,
544 });
545 live_input = trial_input;
546 final_calculation = Some(trial_calculation.clone());
547 let accepted_layout = RietveldParameterLayout::new(&live_input, selection, lattice_bounds)?;
548 live_parameters = stable_layout
549 .parameters()
550 .replace_values(&accepted_layout.parameters().values())
551 .map_err(RietveldGeneralParameterError::Parameter)?;
552 prepared_objective = trial_objective_state;
553 damping = (damping * options.damping_decrease).max(1.0e-18);
554 let state = RietveldGeneralCheckpoint {
555 completed_iterations: history.len(),
556 input: live_input.clone(),
557 selection: selection.clone(),
558 lattice_bounds: lattice_bounds.to_vec(),
559 constraints: constraints.to_vec(),
560 parameters: live_parameters.clone(),
561 objective,
562 damping,
563 history: history.clone(),
564 };
565 runtime.accept_step(Some(&state))?;
566 runtime.emit(
567 RefinementEventKind::StepAccepted,
568 "rietveld_step",
569 "native complete Rietveld step accepted",
570 vec![
571 ("objective".to_owned(), DiagnosticValue::Float(objective)),
572 (
573 "topology_changes".to_owned(),
574 DiagnosticValue::Integer(
575 i64::try_from(topology_changes.len()).unwrap_or(i64::MAX),
576 ),
577 ),
578 ],
579 )?;
580 if history.len() >= options.min_iterations
581 && objective_change <= options.objective_tolerance * objective.max(1.0)
582 {
583 termination = TerminationReason::Converged;
584 break;
585 }
586 }
587 let final_layout = stable_layout;
588 let final_domain_layout = RietveldParameterLayout::new(&live_input, selection, lattice_bounds)?;
589 let final_parameters = final_layout
590 .parameters()
591 .replace_values(&final_domain_layout.parameters().values())
592 .map_err(RietveldGeneralParameterError::Parameter)?;
593 let final_transform = ConstraintTransform::new(final_parameters.clone(), constraints.to_vec())?;
594 let mut calculation = if let Some(calculation) = final_calculation {
595 calculation
596 } else {
597 runtime.begin_evaluation().or_else(|error| match error {
598 RuntimeError::Stopped(_) => Ok(()),
599 other => Err(other),
600 })?;
601 calculate_rietveld_pattern(&live_input, &options.calculation)?
602 };
603 calculation.metrics = evaluate_residuals(
604 &input.pattern,
605 &calculation.y,
606 ResidualOptions {
607 use_uncertainty: options.calculation.use_uncertainty,
608 parameter_count: final_transform.free_keys().len(),
609 },
610 )?;
611 let checkpoint = RietveldGeneralCheckpoint {
612 completed_iterations: history.len(),
613 input: live_input.clone(),
614 selection: selection.clone(),
615 lattice_bounds: lattice_bounds.to_vec(),
616 constraints: constraints.to_vec(),
617 parameters: final_parameters.clone(),
618 objective: 0.5 * calculation.metrics.chi_square,
619 damping,
620 history: history.clone(),
621 };
622 checkpoint.validate_for(input, selection, lattice_bounds, constraints)?;
623 let diagnostics = match covariance_diagnostics(
624 &live_input,
625 &final_layout,
626 &final_transform,
627 options,
628 covariance,
629 &calculation,
630 runtime,
631 ) {
632 Ok(value) => value,
633 Err(RietveldGeneralRefinementError::Runtime(RuntimeError::Stopped(_))) => {
634 CovarianceDiagnostics::default()
635 }
636 Err(error) => return Err(error),
637 };
638 runtime.emit(
639 RefinementEventKind::Termination,
640 "rietveld",
641 "native complete Rietveld refinement terminated",
642 vec![(
643 "reason".to_owned(),
644 DiagnosticValue::String(termination.as_str().to_owned()),
645 )],
646 )?;
647 Ok(RietveldGeneralRefinementResult {
648 calculation,
649 input: live_input,
650 parameters: final_parameters,
651 free_keys: final_transform.free_keys().to_vec(),
652 history,
653 termination_reason: termination,
654 checkpoint,
655 evaluations: runtime.evaluations(),
656 jacobian_rank: diagnostics.rank,
657 covariance: diagnostics.covariance,
658 unresolved_correlations: diagnostics.correlations,
659 })
660}
661
662impl RietveldCovarianceOptions {
663 fn validate(self) -> Result<(), RietveldGeneralRefinementError> {
664 Self::new(
665 self.enabled,
666 self.max_parameters,
667 self.unresolved_correlation,
668 )
669 .map(|_| ())
670 }
671}
672
673fn emit_rejected_trial(
674 runtime: &mut RefinementRuntime<RietveldGeneralCheckpoint>,
675 reason: &str,
676) -> Result<(), RietveldGeneralRefinementError> {
677 runtime.emit(
678 RefinementEventKind::StepRejected,
679 "rietveld_step",
680 "native complete Rietveld trial rejected",
681 vec![(
682 "reason".to_owned(),
683 DiagnosticValue::String(reason.to_owned()),
684 )],
685 )?;
686 Ok(())
687}
688
689fn validate_constraint_state(
690 layout: &RietveldParameterLayout,
691 transform: &ConstraintTransform,
692) -> Result<(), RietveldGeneralRefinementError> {
693 let constrained = transform.unpack(&transform.pack()?, false)?;
694 for spec in layout.parameters().specs() {
695 let value = constrained
696 .get(spec.key())
697 .copied()
698 .ok_or(RietveldGeneralRefinementError::InternalInvariant)?;
699 if (value - spec.value()).abs() > 2.0e-12 {
700 return Err(RietveldGeneralRefinementError::UnsatisfiedConstraint {
701 key: spec.key().clone(),
702 });
703 }
704 }
705 Ok(())
706}
707
708fn parameter_contract_matches(domain: &ParameterSet, stored: &ParameterSet) -> bool {
709 domain.specs().len() == stored.specs().len()
710 && domain
711 .specs()
712 .iter()
713 .zip(stored.specs())
714 .all(|(domain, stored)| {
715 domain.key() == stored.key()
716 && domain.unit() == stored.unit()
717 && domain.bounds() == stored.bounds()
718 && domain.refine() == stored.refine()
719 && domain.scale().to_bits() == stored.scale().to_bits()
720 })
721}
722
723fn parameter_values_match(domain: &ParameterSet, stored: &ParameterSet) -> bool {
724 domain.specs().len() == stored.specs().len()
725 && domain
726 .specs()
727 .iter()
728 .zip(stored.specs())
729 .all(|(domain, stored)| {
730 domain.key() == stored.key() && domain.value().to_bits() == stored.value().to_bits()
731 })
732}
733
734fn forward_product(derivative: &ConstraintDerivativeMatrix, free: &[f64]) -> Vec<f64> {
735 debug_assert_eq!(free.len(), derivative.columns);
736 derivative
737 .values
738 .chunks_exact(derivative.columns)
739 .map(|row| row.iter().zip(free).map(|(left, right)| left * right).sum())
740 .collect()
741}
742
743fn transpose_product(derivative: &ConstraintDerivativeMatrix, physical: &[f64]) -> Vec<f64> {
744 debug_assert_eq!(physical.len(), derivative.rows);
745 let mut result = vec![0.0; derivative.columns];
746 for (coefficient, row) in physical
747 .iter()
748 .zip(derivative.values.chunks_exact(derivative.columns))
749 {
750 for (target, value) in result.iter_mut().zip(row) {
751 *target += coefficient * value;
752 }
753 }
754 result
755}
756
757#[derive(Default)]
758struct CovarianceDiagnostics {
759 rank: Option<usize>,
760 covariance: Option<RietveldCovarianceMatrix>,
761 correlations: Vec<RietveldParameterCorrelation>,
762}
763
764#[allow(clippy::too_many_arguments)]
765fn covariance_diagnostics(
766 input: &RietveldInput,
767 layout: &RietveldParameterLayout,
768 transform: &ConstraintTransform,
769 options: &RietveldRefinementOptions,
770 covariance_options: RietveldCovarianceOptions,
771 calculation: &RietveldCalculation,
772 runtime: &mut RefinementRuntime<RietveldGeneralCheckpoint>,
773) -> Result<CovarianceDiagnostics, RietveldGeneralRefinementError> {
774 let free_count = transform.free_keys().len();
775 if !covariance_options.enabled
776 || free_count == 0
777 || free_count > covariance_options.max_parameters
778 {
779 return Ok(CovarianceDiagnostics::default());
780 }
781 let objective = PreparedGeneralRietveldObjective::new(
782 input.clone(),
783 options.calculation.clone(),
784 layout.clone(),
785 )?;
786 reserve_products(runtime, objective.preparation_evaluation_count())?;
787 let derivative = transform.derivative_matrix()?;
788 let sample_count = input.pattern.sample_count();
789 let element_count = sample_count
790 .checked_mul(free_count)
791 .ok_or(RietveldGeneralRefinementError::AllocationOverflow)?;
792 let mut columns = vec![0.0; element_count];
793 for column in 0..free_count {
794 reserve_products(runtime, objective.jvp_evaluation_count())?;
795 let mut basis = vec![0.0; free_count];
796 basis[column] = 1.0;
797 let physical = forward_product(&derivative, &basis);
798 let (_, values) = objective.jvp(&physical)?;
799 for (sample, value) in values.into_iter().enumerate() {
800 let included = input.pattern.mask.as_ref().is_none_or(|mask| mask[sample]);
801 let weighted = if !included {
802 0.0
803 } else if options.calculation.use_uncertainty {
804 input
805 .pattern
806 .uncertainty
807 .as_ref()
808 .map_or(value, |sigma| value / sigma[sample])
809 } else {
810 value
811 };
812 columns[sample * free_count + column] = weighted;
813 }
814 }
815 let jacobian = DMatrix::from_row_slice(sample_count, free_count, &columns);
816 let normal = jacobian.transpose() * &jacobian;
817 let rank = matrix_rank(&normal);
818 let correlations = unresolved_correlations(
819 &columns,
820 sample_count,
821 transform.free_keys(),
822 covariance_options.unresolved_correlation,
823 );
824 if rank != free_count {
825 return Ok(CovarianceDiagnostics {
826 rank: Some(rank),
827 covariance: None,
828 correlations,
829 });
830 }
831 let Some(mut free_covariance) = normal.try_inverse() else {
832 return Ok(CovarianceDiagnostics {
833 rank: Some(rank),
834 covariance: None,
835 correlations,
836 });
837 };
838 let known_uncertainties =
839 options.calculation.use_uncertainty && input.pattern.uncertainty.is_some();
840 if !known_uncertainties && calculation.metrics.reduced_chi_square.is_finite() {
841 free_covariance *= calculation.metrics.reduced_chi_square;
842 }
843 let covariance_count = derivative
844 .rows
845 .checked_mul(derivative.rows)
846 .ok_or(RietveldGeneralRefinementError::AllocationOverflow)?;
847 let chain = DMatrix::from_row_slice(derivative.rows, derivative.columns, &derivative.values);
848 let physical = &chain * free_covariance * chain.transpose();
849 let mut values = Vec::with_capacity(covariance_count);
850 for row in 0..physical.nrows() {
851 for column in 0..physical.ncols() {
852 values.push(physical[(row, column)]);
853 }
854 }
855 if values.iter().any(|value| !value.is_finite()) {
856 return Ok(CovarianceDiagnostics {
857 rank: Some(rank),
858 covariance: None,
859 correlations,
860 });
861 }
862 Ok(CovarianceDiagnostics {
863 rank: Some(rank),
864 covariance: Some(RietveldCovarianceMatrix {
865 size: derivative.rows,
866 values,
867 }),
868 correlations,
869 })
870}
871
872fn unresolved_correlations(
873 columns: &[f64],
874 sample_count: usize,
875 free_keys: &[ParameterKey],
876 threshold: f64,
877) -> Vec<RietveldParameterCorrelation> {
878 let free_count = free_keys.len();
879 let norms = (0..free_count)
880 .map(|column| {
881 (0..sample_count)
882 .map(|sample| columns[sample * free_count + column].powi(2))
883 .sum::<f64>()
884 .sqrt()
885 })
886 .collect::<Vec<_>>();
887 let mut result = Vec::new();
888 for left in 0..free_count {
889 if norms[left] == 0.0 {
890 continue;
891 }
892 for right in left + 1..free_count {
893 if norms[right] == 0.0 {
894 continue;
895 }
896 let correlation = (0..sample_count)
897 .map(|sample| {
898 columns[sample * free_count + left] * columns[sample * free_count + right]
899 })
900 .sum::<f64>()
901 / (norms[left] * norms[right]);
902 let correlation = correlation.clamp(-1.0, 1.0);
903 if correlation.abs() >= threshold {
904 result.push(RietveldParameterCorrelation {
905 left: free_keys[left].clone(),
906 right: free_keys[right].clone(),
907 correlation,
908 });
909 }
910 }
911 }
912 result
913}
914
915fn matrix_rank(matrix: &DMatrix<f64>) -> usize {
916 let singular = matrix.clone().svd(false, false).singular_values;
917 let maximum = singular.iter().copied().fold(0.0_f64, f64::max);
918 let dimension = u32::try_from(matrix.nrows().max(matrix.ncols())).unwrap_or(u32::MAX);
919 let tolerance = f64::from(dimension) * f64::EPSILON * maximum;
920 singular.iter().filter(|value| **value > tolerance).count()
921}
922
923fn checkpoint_request_compatible(
924 accepted: &RietveldInput,
925 requested: &RietveldInput,
926 selection: &RietveldParameterSelection,
927) -> bool {
928 accepted.pattern == requested.pattern
929 && accepted.axial_geometry == requested.axial_geometry
930 && accepted.phases.len() == requested.phases.len()
931 && accepted
932 .phases
933 .iter()
934 .zip(&requested.phases)
935 .all(|(accepted, requested)| {
936 accepted.restart_compatible_with_wavelength(
937 requested,
938 selection
939 .instrument
940 .contains(&RietveldInstrumentParameter::WavelengthAngstrom),
941 ) && phase_values_compatible(accepted, requested, selection)
942 })
943 && background_compatible(
944 accepted.background.as_ref(),
945 requested.background.as_ref(),
946 selection.background,
947 )
948 && instrument_compatible(accepted, requested, selection)
949}
950
951fn phase_values_compatible(
952 accepted: &crate::RietveldPhase,
953 requested: &crate::RietveldPhase,
954 selection: &RietveldParameterSelection,
955) -> bool {
956 let left = accepted.definition();
957 let right = requested.definition();
958 (selection.structural.phase_scale || left.scale.to_bits() == right.scale.to_bits())
959 && (selection.structural.lattice || left.cell == right.cell)
960 && (selection.structural.coordinates || left.fractional_xyz == right.fractional_xyz)
961 && (selection.structural.occupancy || left.occupancy == right.occupancy)
962 && (selection.structural.u_iso || left.u_iso_angstrom2 == right.u_iso_angstrom2)
963 && (selection.sample_physics || accepted.sample_physics() == requested.sample_physics())
964 && (accepted.reflection_domain().is_some()
965 || accepted.sample_physics().is_some()
966 || accepted.contributions() == requested.contributions())
967}
968
969fn background_compatible(
970 accepted: Option<&BackgroundModel>,
971 requested: Option<&BackgroundModel>,
972 selected: bool,
973) -> bool {
974 match (accepted, requested) {
975 (None, None) => true,
976 (Some(accepted), Some(requested)) => {
977 if selected {
978 accepted.restart_compatible(requested)
979 } else {
980 accepted == requested
981 }
982 }
983 _ => false,
984 }
985}
986
987fn instrument_compatible(
988 accepted: &RietveldInput,
989 requested: &RietveldInput,
990 selection: &RietveldParameterSelection,
991) -> bool {
992 let selected = |parameter| selection.instrument.contains(¶meter);
993 let left = accepted.instrument;
994 let right = requested.instrument;
995 (selected(RietveldInstrumentParameter::UDeg2)
996 || left.u_deg2.to_bits() == right.u_deg2.to_bits())
997 && (selected(RietveldInstrumentParameter::VDeg2)
998 || left.v_deg2.to_bits() == right.v_deg2.to_bits())
999 && (selected(RietveldInstrumentParameter::WDeg2)
1000 || left.w_deg2.to_bits() == right.w_deg2.to_bits())
1001 && (selected(RietveldInstrumentParameter::XDeg)
1002 || left.x_deg.to_bits() == right.x_deg.to_bits())
1003 && (selected(RietveldInstrumentParameter::YDeg)
1004 || left.y_deg.to_bits() == right.y_deg.to_bits())
1005 && (selected(RietveldInstrumentParameter::WavelengthAngstrom)
1006 || left.wavelength_angstrom.to_bits() == right.wavelength_angstrom.to_bits())
1007 && position_correction_compatible(accepted, requested, &selected)
1008}
1009
1010fn position_correction_compatible(
1011 accepted: &RietveldInput,
1012 requested: &RietveldInput,
1013 selected: &impl Fn(RietveldInstrumentParameter) -> bool,
1014) -> bool {
1015 let left = accepted.position_correction;
1016 let right = requested.position_correction;
1017 if !selected(RietveldInstrumentParameter::ZeroShiftDeg)
1018 && left.zero_shift_deg.to_bits() != right.zero_shift_deg.to_bits()
1019 {
1020 return false;
1021 }
1022 let bragg = match (left.bragg_brentano_mm, right.bragg_brentano_mm) {
1023 (None, None) => true,
1024 (Some(left), Some(right)) => {
1025 left.1.to_bits() == right.1.to_bits()
1026 && (selected(RietveldInstrumentParameter::SampleDisplacementMm)
1027 || left.0.to_bits() == right.0.to_bits())
1028 }
1029 _ => false,
1030 };
1031 let debye = match (
1032 left.debye_scherrer_micrometre,
1033 right.debye_scherrer_micrometre,
1034 ) {
1035 (None, None) => true,
1036 (Some(left), Some(right)) => {
1037 left.2.to_bits() == right.2.to_bits()
1038 && (selected(RietveldInstrumentParameter::DisplaceXMicrometre)
1039 || left.0.to_bits() == right.0.to_bits())
1040 && (selected(RietveldInstrumentParameter::DisplaceYMicrometre)
1041 || left.1.to_bits() == right.1.to_bits())
1042 }
1043 _ => false,
1044 };
1045 bragg && debye
1046}
1047
1048fn valid_history(history: &[RietveldIterationRecord], input: &RietveldInput) -> bool {
1049 let phase_ids = input
1050 .phases
1051 .iter()
1052 .map(crate::RietveldPhase::phase_id)
1053 .collect::<std::collections::BTreeSet<_>>();
1054 history.iter().enumerate().all(|(index, row)| {
1055 row.iteration == index + 1
1056 && row.objective.is_finite()
1057 && row.objective >= 0.0
1058 && row.objective_change.is_finite()
1059 && row.objective_change >= 0.0
1060 && row.scaled_step_norm.is_finite()
1061 && row.scaled_step_norm >= 0.0
1062 && row.damping.is_finite()
1063 && row.damping > 0.0
1064 && row.rwp.is_finite()
1065 && row.rp.is_finite()
1066 && row.chi_square.is_finite()
1067 && row.chi_square >= 0.0
1068 && !row.reduced_chi_square.is_nan()
1069 && row.reduced_chi_square >= 0.0
1070 && row.parameter_changes.iter().all(|change| {
1071 change.before.is_finite()
1072 && change.after.is_finite()
1073 && change.scaled_change.is_finite()
1074 })
1075 && row.topology_changes.iter().all(|change| {
1076 let added = change
1077 .added_reflection_ids
1078 .iter()
1079 .collect::<std::collections::BTreeSet<_>>();
1080 let removed = change
1081 .removed_reflection_ids
1082 .iter()
1083 .collect::<std::collections::BTreeSet<_>>();
1084 phase_ids.contains(&change.phase_id)
1085 && added.len() == change.added_reflection_ids.len()
1086 && removed.len() == change.removed_reflection_ids.len()
1087 && added.is_disjoint(&removed)
1088 })
1089 })
1090}
1091
1092#[derive(Debug)]
1094pub enum RietveldGeneralRefinementError {
1095 InvalidCovarianceOptions,
1097 InvalidCheckpoint {
1099 reason: &'static str,
1101 },
1102 AllocationOverflow,
1104 InternalInvariant,
1106 UnsatisfiedConstraint {
1108 key: ParameterKey,
1110 },
1111 Refinement(RietveldRefinementError),
1113 Parameter(RietveldGeneralParameterError),
1115 Constraint(ConstraintError),
1117 Rietveld(crate::RietveldError),
1119 Objective(crate::RietveldGeneralObjectiveError),
1121 Runtime(RuntimeError),
1123 Residual(crate::ResidualError),
1125}
1126
1127impl Display for RietveldGeneralRefinementError {
1128 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
1129 match self {
1130 Self::InvalidCovarianceOptions => {
1131 formatter.write_str("native Rietveld covariance options are invalid")
1132 }
1133 Self::InvalidCheckpoint { reason } => write!(
1134 formatter,
1135 "native complete Rietveld checkpoint is invalid: {reason}"
1136 ),
1137 Self::AllocationOverflow => {
1138 formatter.write_str("native Rietveld diagnostic allocation overflowed")
1139 }
1140 Self::InternalInvariant => {
1141 formatter.write_str("native complete Rietveld invariant failed")
1142 }
1143 Self::UnsatisfiedConstraint { key } => write!(
1144 formatter,
1145 "initial physical value does not satisfy the constraint for {}",
1146 key.label()
1147 ),
1148 Self::Refinement(error) => Display::fmt(error, formatter),
1149 Self::Parameter(error) => Display::fmt(error, formatter),
1150 Self::Constraint(error) => Display::fmt(error, formatter),
1151 Self::Rietveld(error) => Display::fmt(error, formatter),
1152 Self::Objective(error) => Display::fmt(error, formatter),
1153 Self::Runtime(error) => Display::fmt(error, formatter),
1154 Self::Residual(error) => Display::fmt(error, formatter),
1155 }
1156 }
1157}
1158
1159impl Error for RietveldGeneralRefinementError {
1160 fn source(&self) -> Option<&(dyn Error + 'static)> {
1161 match self {
1162 Self::Refinement(error) => Some(error),
1163 Self::Parameter(error) => Some(error),
1164 Self::Constraint(error) => Some(error),
1165 Self::Rietveld(error) => Some(error),
1166 Self::Objective(error) => Some(error),
1167 Self::Runtime(error) => Some(error),
1168 Self::Residual(error) => Some(error),
1169 Self::InvalidCovarianceOptions
1170 | Self::InvalidCheckpoint { .. }
1171 | Self::AllocationOverflow
1172 | Self::InternalInvariant
1173 | Self::UnsatisfiedConstraint { .. } => None,
1174 }
1175 }
1176}
1177
1178macro_rules! from_error {
1179 ($source:ty, $variant:ident) => {
1180 impl From<$source> for RietveldGeneralRefinementError {
1181 fn from(value: $source) -> Self {
1182 Self::$variant(value)
1183 }
1184 }
1185 };
1186}
1187
1188from_error!(RietveldRefinementError, Refinement);
1189from_error!(RietveldGeneralParameterError, Parameter);
1190from_error!(ConstraintError, Constraint);
1191from_error!(crate::RietveldError, Rietveld);
1192from_error!(crate::RietveldGeneralObjectiveError, Objective);
1193from_error!(RuntimeError, Runtime);
1194from_error!(crate::ResidualError, Residual);