Skip to main content

phasesmith_workflows/
rietveld_general_solver.rs

1//! Constraint-aware bounded solver over the complete native Rietveld layout.
2
3use 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/// Controls for the optional small parameter-space identifiability analysis.
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct RietveldCovarianceOptions {
24    /// Whether final Jacobian diagnostics are evaluated.
25    pub enabled: bool,
26    /// Maximum scaled-free dimension allowed for explicit diagnostics.
27    pub max_parameters: usize,
28    /// Absolute column correlation reported as unresolved.
29    pub unresolved_correlation: f64,
30}
31
32impl RietveldCovarianceOptions {
33    /// Construct validated covariance controls.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`RietveldGeneralRefinementError::InvalidCovarianceOptions`]
38    /// for a zero dimension or threshold outside `[0, 1]`.
39    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/// One nearly collinear pair of scaled-free Jacobian columns.
69#[derive(Clone, Debug, PartialEq)]
70pub struct RietveldParameterCorrelation {
71    /// First stable free-parameter identity.
72    pub left: ParameterKey,
73    /// Second stable free-parameter identity.
74    pub right: ParameterKey,
75    /// Signed normalized weighted-column dot product.
76    pub correlation: f64,
77}
78
79/// Square row-major covariance over the complete physical parameter order.
80///
81/// The inverse weighted normal matrix is returned without residual scaling
82/// when supplied uncertainties are active. Unit-weight fits are scaled by the
83/// final reduced chi-square because their noise scale is estimated from the fit.
84#[derive(Clone, Debug, PartialEq)]
85pub struct RietveldCovarianceMatrix {
86    /// Matrix dimension, equal to the complete physical parameter count.
87    pub size: usize,
88    /// Row-major values with length `size * size`.
89    pub values: Vec<f64>,
90}
91
92/// Complete accepted state for deterministic constrained continuation.
93#[derive(Clone, Debug, PartialEq)]
94pub struct RietveldGeneralCheckpoint {
95    /// Accepted iteration count.
96    pub completed_iterations: usize,
97    /// Last accepted complete request state.
98    pub input: RietveldInput,
99    /// Selection contract used to build the complete layout.
100    pub selection: RietveldParameterSelection,
101    /// Per-phase lattice bounds in phase order.
102    pub lattice_bounds: Vec<Option<LatticeBounds>>,
103    /// Ordered constraint graph.
104    pub constraints: Vec<Constraint>,
105    /// Last accepted complete physical parameters.
106    pub parameters: ParameterSet,
107    /// Last accepted objective.
108    pub objective: f64,
109    /// Damping for the next attempted iteration.
110    pub damping: f64,
111    /// Complete accepted history.
112    pub history: Vec<RietveldIterationRecord>,
113}
114
115impl RietveldGeneralCheckpoint {
116    /// Revalidate this accepted state against its requested solver contract.
117    ///
118    /// # Errors
119    ///
120    /// Returns [`RietveldGeneralRefinementError`] when the checkpoint is
121    /// internally invalid or no longer matches the requested input contract.
122    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/// Final complete native constrained-refinement result.
168#[derive(Clone, Debug, PartialEq)]
169pub struct RietveldGeneralRefinementResult {
170    /// Final display-ready calculation.
171    pub calculation: RietveldCalculation,
172    /// Final accepted complete request state.
173    pub input: RietveldInput,
174    /// Final complete physical parameters.
175    pub parameters: ParameterSet,
176    /// Stable scaled-free parameter order.
177    pub free_keys: Vec<ParameterKey>,
178    /// Complete accepted history.
179    pub history: Vec<RietveldIterationRecord>,
180    /// Stable bounded termination category.
181    pub termination_reason: TerminationReason,
182    /// Final restart checkpoint.
183    pub checkpoint: RietveldGeneralCheckpoint,
184    /// Model-product/evaluation count for this call.
185    pub evaluations: usize,
186    /// Rank of the final weighted scaled-free normal matrix when evaluated.
187    pub jacobian_rank: Option<usize>,
188    /// Full physical-parameter covariance when the free normal matrix is full rank.
189    pub covariance: Option<RietveldCovarianceMatrix>,
190    /// Nearly collinear scaled-free column pairs.
191    pub unresolved_correlations: Vec<RietveldParameterCorrelation>,
192}
193
194/// Refine the complete native layout with a process-local runtime.
195///
196/// # Errors
197///
198/// Returns [`RietveldGeneralRefinementError`] for invalid layouts, constraints,
199/// checkpoints, runtime state, or numerical products.
200#[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/// Refine the complete native layout through a caller-owned runtime.
225///
226/// # Errors
227///
228/// Returns [`RietveldGeneralRefinementError`] for invalid state or numerical
229/// products. Normal bounded stops return the last accepted state.
230#[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, &current_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(&current_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(&parameter);
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/// Invalid complete constrained Rietveld refinement state.
1093#[derive(Debug)]
1094pub enum RietveldGeneralRefinementError {
1095    /// Covariance controls are invalid.
1096    InvalidCovarianceOptions,
1097    /// Restart state is inconsistent with the request or solver contract.
1098    InvalidCheckpoint {
1099        /// Stable diagnostic explaining the rejected checkpoint contract.
1100        reason: &'static str,
1101    },
1102    /// Checked diagnostic allocation overflowed.
1103    AllocationOverflow,
1104    /// Internal validated layout invariant failed.
1105    InternalInvariant,
1106    /// The supplied physical state does not satisfy its constraint graph.
1107    UnsatisfiedConstraint {
1108        /// First inconsistent physical identity.
1109        key: ParameterKey,
1110    },
1111    /// Existing bounded solver or product failed.
1112    Refinement(RietveldRefinementError),
1113    /// Complete parameter layout or installation failed.
1114    Parameter(RietveldGeneralParameterError),
1115    /// Constraint graph or transform failed.
1116    Constraint(ConstraintError),
1117    /// Owned calculation state failed.
1118    Rietveld(crate::RietveldError),
1119    /// Complete objective failed.
1120    Objective(crate::RietveldGeneralObjectiveError),
1121    /// Runtime boundary failed.
1122    Runtime(RuntimeError),
1123    /// Residual evaluation failed.
1124    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);