Skip to main content

phasesmith_workflows/
rietveld_solver.rs

1//! Bounded matrix-free native structural Rietveld refinement.
2
3use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use crate::{
7    CancellationToken, DiagnosticValue, LatticeBounds, ParameterChange, ParameterSet,
8    ParameterSpec, PreparedRietveldObjective, RefinementEventKind, RefinementLimits,
9    RefinementRuntime, ResidualOptions, RietveldCalculation, RietveldCalculationOptions,
10    RietveldError, RietveldInput, RietveldObjectiveError, RietveldParameterError, RietveldPhase,
11    RietveldStructuralLayout, RietveldStructuralSelection, RietveldTopologyChange, RuntimeError,
12    TerminationReason, calculate_rietveld_pattern, evaluate_residuals,
13};
14
15/// Numerical and bounded-runtime controls for native structural refinement.
16#[derive(Clone, Debug, PartialEq)]
17pub struct RietveldRefinementOptions {
18    /// Forward-calculation and execution controls.
19    pub calculation: RietveldCalculationOptions,
20    /// Hard iteration/evaluation/time/rejection limits.
21    pub limits: RefinementLimits,
22    /// Minimum accepted iterations before objective convergence.
23    pub min_iterations: usize,
24    /// Relative accepted objective-change tolerance.
25    pub objective_tolerance: f64,
26    /// Scaled step-norm tolerance.
27    pub parameter_tolerance: f64,
28    /// Initial positive Levenberg damping in scaled coordinates.
29    pub initial_damping: f64,
30    /// Multiplier applied after an unsuccessful iteration.
31    pub damping_increase: f64,
32    /// Multiplier applied after an accepted iteration.
33    pub damping_decrease: f64,
34    /// Relative conjugate-gradient residual tolerance.
35    pub cg_tolerance: f64,
36    /// Maximum conjugate-gradient iterations per attempted step.
37    pub max_cg_iterations: usize,
38    /// Maximum Euclidean scaled step norm.
39    pub max_scaled_parameter_step: f64,
40    /// Number of half-step retries after the full trial.
41    pub max_backtracks: usize,
42}
43
44impl RietveldRefinementOptions {
45    /// Construct and validate native solver controls.
46    ///
47    /// # Errors
48    ///
49    /// Returns [`RietveldRefinementError::InvalidOptions`] for inconsistent
50    /// tolerances, damping, iteration, or step controls.
51    #[allow(clippy::too_many_arguments)]
52    pub fn new(
53        calculation: RietveldCalculationOptions,
54        limits: RefinementLimits,
55        min_iterations: usize,
56        objective_tolerance: f64,
57        parameter_tolerance: f64,
58        initial_damping: f64,
59        damping_increase: f64,
60        damping_decrease: f64,
61        cg_tolerance: f64,
62        max_cg_iterations: usize,
63        max_scaled_parameter_step: f64,
64        max_backtracks: usize,
65    ) -> Result<Self, RietveldRefinementError> {
66        let result = Self {
67            calculation,
68            limits,
69            min_iterations,
70            objective_tolerance,
71            parameter_tolerance,
72            initial_damping,
73            damping_increase,
74            damping_decrease,
75            cg_tolerance,
76            max_cg_iterations,
77            max_scaled_parameter_step,
78            max_backtracks,
79        };
80        result.validate()?;
81        Ok(result)
82    }
83
84    /// Revalidate adapter-decoded numerical and runtime controls.
85    ///
86    /// # Errors
87    ///
88    /// Returns [`RietveldRefinementError::InvalidOptions`] for inconsistent
89    /// tolerances, damping, iteration, or step controls.
90    pub fn validate(&self) -> Result<(), RietveldRefinementError> {
91        let positive = [
92            self.objective_tolerance,
93            self.parameter_tolerance,
94            self.initial_damping,
95            self.damping_increase,
96            self.damping_decrease,
97            self.cg_tolerance,
98            self.max_scaled_parameter_step,
99        ];
100        if self.min_iterations == 0
101            || self.min_iterations > self.limits.max_iterations()
102            || self.max_cg_iterations == 0
103            || positive
104                .iter()
105                .any(|value| !value.is_finite() || *value <= 0.0)
106            || self.damping_increase <= 1.0
107            || self.damping_decrease >= 1.0
108        {
109            return Err(RietveldRefinementError::InvalidOptions);
110        }
111        Ok(())
112    }
113}
114
115/// One accepted native Rietveld Gauss--Newton iteration.
116#[derive(Clone, Debug, PartialEq)]
117pub struct RietveldIterationRecord {
118    /// One-based attempted iteration.
119    pub iteration: usize,
120    /// Accepted weighted residual sum of squares divided by two.
121    pub objective: f64,
122    /// Previous objective minus accepted objective.
123    pub objective_change: f64,
124    /// Accepted scaled Euclidean step norm.
125    pub scaled_step_norm: f64,
126    /// Damping used to construct the accepted step.
127    pub damping: f64,
128    /// Conjugate-gradient iterations used.
129    pub cg_iterations: usize,
130    /// Half-step backtracks used.
131    pub backtracks: usize,
132    /// Accepted physical parameter changes.
133    pub parameter_changes: Vec<ParameterChange>,
134    /// Reflection families added or removed by accepted lattice motion.
135    pub topology_changes: Vec<RietveldTopologyChange>,
136    /// Accepted Rwp.
137    pub rwp: f64,
138    /// Accepted unweighted profile residual.
139    pub rp: f64,
140    /// Accepted weighted residual sum of squares.
141    pub chi_square: f64,
142    /// Accepted chi-square divided by residual degrees of freedom.
143    pub reduced_chi_square: f64,
144}
145
146/// Complete accepted state for deterministic native continuation.
147#[derive(Clone, Debug, PartialEq)]
148pub struct RietveldCheckpoint {
149    /// Accepted iteration count.
150    pub completed_iterations: usize,
151    /// Last accepted phase state.
152    pub phases: Vec<RietveldPhase>,
153    /// Last accepted physical parameters.
154    pub parameters: ParameterSet,
155    /// Last accepted objective.
156    pub objective: f64,
157    /// Damping for the next attempted iteration.
158    pub damping: f64,
159    /// Complete accepted history.
160    pub history: Vec<RietveldIterationRecord>,
161}
162
163impl RietveldCheckpoint {
164    fn validate(&self, input: &RietveldInput) -> Result<(), RietveldRefinementError> {
165        let phase_ids = input
166            .phases
167            .iter()
168            .map(RietveldPhase::phase_id)
169            .collect::<std::collections::BTreeSet<_>>();
170        if self.completed_iterations != self.history.len()
171            || self.phases.len() != input.phases.len()
172            || self
173                .phases
174                .iter()
175                .zip(&input.phases)
176                .any(|(stored, requested)| !stored.restart_compatible(requested))
177            || !self.objective.is_finite()
178            || self.objective < 0.0
179            || !self.damping.is_finite()
180            || self.damping <= 0.0
181            || self.history.iter().enumerate().any(|(index, row)| {
182                row.iteration != index + 1
183                    || !row.objective.is_finite()
184                    || row.objective < 0.0
185                    || !row.objective_change.is_finite()
186                    || row.objective_change < 0.0
187                    || !row.scaled_step_norm.is_finite()
188                    || row.scaled_step_norm < 0.0
189                    || !row.damping.is_finite()
190                    || row.damping <= 0.0
191                    || !row.rwp.is_finite()
192                    || !row.rp.is_finite()
193                    || !row.chi_square.is_finite()
194                    || row.chi_square < 0.0
195                    || row.reduced_chi_square.is_nan()
196                    || row.reduced_chi_square < 0.0
197                    || row.parameter_changes.iter().any(|change| {
198                        !change.before.is_finite()
199                            || !change.after.is_finite()
200                            || !change.scaled_change.is_finite()
201                    })
202                    || row.topology_changes.iter().any(|change| {
203                        let added = change
204                            .added_reflection_ids
205                            .iter()
206                            .collect::<std::collections::BTreeSet<_>>();
207                        let removed = change
208                            .removed_reflection_ids
209                            .iter()
210                            .collect::<std::collections::BTreeSet<_>>();
211                        !phase_ids.contains(&change.phase_id)
212                            || added.len() != change.added_reflection_ids.len()
213                            || removed.len() != change.removed_reflection_ids.len()
214                            || !added.is_disjoint(&removed)
215                    })
216            })
217        {
218            return Err(RietveldRefinementError::InvalidCheckpoint);
219        }
220        Ok(())
221    }
222}
223
224/// Final accepted native structural refinement result.
225#[derive(Clone, Debug, PartialEq)]
226pub struct RietveldRefinementResult {
227    /// Final display-ready calculation.
228    pub calculation: RietveldCalculation,
229    /// Final accepted phases.
230    pub phases: Vec<RietveldPhase>,
231    /// Final accepted physical parameters.
232    pub parameters: ParameterSet,
233    /// Complete accepted history.
234    pub history: Vec<RietveldIterationRecord>,
235    /// Stable bounded termination category.
236    pub termination_reason: TerminationReason,
237    /// Final restart checkpoint.
238    pub checkpoint: RietveldCheckpoint,
239    /// Model-product/evaluation count for this call.
240    pub evaluations: usize,
241}
242
243/// Refine with a process-local runtime and optional cancellation token.
244///
245/// # Errors
246///
247/// Returns [`RietveldRefinementError`] for invalid input, checkpoint, model, or
248/// numerical solver state.
249pub fn refine_rietveld(
250    input: &RietveldInput,
251    selection: RietveldStructuralSelection,
252    lattice_bounds: &[Option<LatticeBounds>],
253    options: &RietveldRefinementOptions,
254    checkpoint: Option<&RietveldCheckpoint>,
255    cancellation: Option<CancellationToken>,
256) -> Result<RietveldRefinementResult, RietveldRefinementError> {
257    let mut runtime = RefinementRuntime::new(options.limits, cancellation)?;
258    refine_rietveld_with_runtime(
259        input,
260        selection,
261        lattice_bounds,
262        options,
263        checkpoint,
264        &mut runtime,
265    )
266}
267
268/// Refine through a caller-owned runtime for events and durable checkpoints.
269///
270/// # Errors
271///
272/// Returns [`RietveldRefinementError`] for invalid state, non-normal runtime
273/// failures, or numerical products.
274#[allow(clippy::too_many_lines)]
275pub fn refine_rietveld_with_runtime(
276    input: &RietveldInput,
277    selection: RietveldStructuralSelection,
278    lattice_bounds: &[Option<LatticeBounds>],
279    options: &RietveldRefinementOptions,
280    checkpoint: Option<&RietveldCheckpoint>,
281    runtime: &mut RefinementRuntime<RietveldCheckpoint>,
282) -> Result<RietveldRefinementResult, RietveldRefinementError> {
283    input.validate()?;
284    options.validate()?;
285    let (mut phases, mut history, mut damping) = if let Some(checkpoint) = checkpoint {
286        checkpoint.validate(input)?;
287        let expected =
288            RietveldStructuralLayout::new(&checkpoint.phases, selection, lattice_bounds)?;
289        if expected.parameters() != &checkpoint.parameters {
290            return Err(RietveldRefinementError::InvalidCheckpoint);
291        }
292        runtime.resume_accepted(checkpoint.completed_iterations)?;
293        (
294            checkpoint.phases.clone(),
295            checkpoint.history.clone(),
296            checkpoint.damping,
297        )
298    } else {
299        (input.phases.clone(), Vec::new(), options.initial_damping)
300    };
301    runtime.emit(
302        RefinementEventKind::Start,
303        "rietveld",
304        "native structural refinement started",
305        Vec::new(),
306    )?;
307    let has_observations = input
308        .pattern
309        .mask
310        .as_ref()
311        .is_none_or(|mask| mask.iter().any(|included| *included));
312    let mut termination = if has_observations {
313        TerminationReason::MaxIterations
314    } else {
315        TerminationReason::NoObservations
316    };
317    let first_iteration = history.len() + 1;
318    let last_iteration = if has_observations {
319        options.limits.max_iterations()
320    } else {
321        history.len()
322    };
323    'iterations: for iteration in first_iteration..=last_iteration {
324        if let Err(error) = runtime.begin_iteration(iteration) {
325            termination = normal_stop(&error)?;
326            break;
327        }
328        let layout = RietveldStructuralLayout::new(&phases, selection, lattice_bounds)?;
329        let specs = layout.parameters().specs();
330        if specs.is_empty() {
331            termination = TerminationReason::Converged;
332            break;
333        }
334        let live_input = replace_phases(input, phases.clone())?;
335        let objective = PreparedRietveldObjective::new(
336            live_input.clone(),
337            options.calculation.clone(),
338            layout.clone(),
339        )?;
340        if let Err(error) = reserve_products(runtime, 2) {
341            termination = normal_stop(&error)?;
342            break;
343        }
344        let (_, physical_gradient) = objective.gradient()?;
345        let scales = specs.iter().map(ParameterSpec::scale).collect::<Vec<_>>();
346        let scaled_gradient = physical_gradient
347            .iter()
348            .zip(&scales)
349            .map(|(gradient, scale)| gradient * scale)
350            .collect::<Vec<_>>();
351        let right_hand_side = scaled_gradient
352            .iter()
353            .map(|value| -value)
354            .collect::<Vec<_>>();
355        let solve = conjugate_gradient(
356            &right_hand_side,
357            options.cg_tolerance,
358            options.max_cg_iterations,
359            |direction| {
360                reserve_products(runtime, 2)?;
361                let physical = direction
362                    .iter()
363                    .zip(&scales)
364                    .map(|(value, scale)| value * scale)
365                    .collect::<Vec<_>>();
366                let product = objective.normal_product(&physical, 0.0)?;
367                Ok(product
368                    .iter()
369                    .zip(&scales)
370                    .zip(direction)
371                    .map(|((value, scale), direction)| value * scale + damping * direction)
372                    .collect())
373            },
374        );
375        let (mut step, cg_iterations) = match solve {
376            Ok(result) => result,
377            Err(RietveldRefinementError::Runtime(RuntimeError::Stopped(stop))) => {
378                termination = stop.reason;
379                break 'iterations;
380            }
381            Err(error) => return Err(error),
382        };
383        let mut step_norm = norm(&step);
384        if step_norm > options.max_scaled_parameter_step {
385            let factor = options.max_scaled_parameter_step / step_norm;
386            for value in &mut step {
387                *value *= factor;
388            }
389            step_norm = options.max_scaled_parameter_step;
390        }
391        if step_norm < options.parameter_tolerance {
392            termination = TerminationReason::Converged;
393            break;
394        }
395        if let Err(error) = runtime.begin_evaluation() {
396            termination = normal_stop(&error)?;
397            break;
398        }
399        let current_calculation = calculate_rietveld_pattern(&live_input, &options.calculation)?;
400        let current_objective = 0.5 * current_calculation.metrics.chi_square;
401        let current_values = specs.iter().map(ParameterSpec::value).collect::<Vec<_>>();
402        let mut accepted = None;
403        for backtrack in 0..=options.max_backtracks {
404            let factor = 0.5_f64.powi(i32::try_from(backtrack).unwrap_or(i32::MAX));
405            let trial_values = specs
406                .iter()
407                .zip(&current_values)
408                .zip(step.iter().zip(&scales))
409                .map(|((spec, current), (step, scale))| {
410                    spec.bounds().clip(current + factor * step * scale)
411                })
412                .collect::<Vec<_>>();
413            let trial_phases = layout.apply_values(&phases, &trial_values)?;
414            if let Err(error) = runtime.begin_evaluation() {
415                termination = normal_stop(&error)?;
416                break 'iterations;
417            }
418            let trial_input = replace_phases(input, trial_phases.clone())?;
419            let trial_calculation = calculate_rietveld_pattern(&trial_input, &options.calculation)?;
420            let trial_objective = 0.5 * trial_calculation.metrics.chi_square;
421            runtime.emit(
422                RefinementEventKind::Trial,
423                "rietveld_step",
424                "native structural trial evaluated",
425                vec![(
426                    "objective".to_owned(),
427                    DiagnosticValue::Float(trial_objective),
428                )],
429            )?;
430            if trial_objective < current_objective {
431                accepted = Some((
432                    backtrack,
433                    factor,
434                    trial_values,
435                    trial_phases,
436                    trial_calculation,
437                    trial_objective,
438                ));
439                break;
440            }
441            if let Err(error) = runtime.reject_step() {
442                termination = normal_stop(&error)?;
443                break;
444            }
445        }
446        let Some((backtracks, factor, trial_values, trial_phases, trial_calculation, objective)) =
447            accepted
448        else {
449            damping *= options.damping_increase;
450            if termination == TerminationReason::MaxIterations {
451                continue;
452            }
453            break;
454        };
455        let objective_change = current_objective - objective;
456        let parameter_changes = specs
457            .iter()
458            .zip(&current_values)
459            .zip(&trial_values)
460            .filter(|((_, before), after)| before.to_bits() != after.to_bits())
461            .map(|((spec, before), after)| ParameterChange {
462                key: spec.key().clone(),
463                before: *before,
464                after: *after,
465                scaled_change: (after - before) / spec.scale(),
466            })
467            .collect::<Vec<_>>();
468        let topology_changes = phases
469            .iter()
470            .zip(&trial_phases)
471            .filter_map(|(before, after)| topology_change(before, after))
472            .collect::<Vec<_>>();
473        let accepted_metrics = evaluate_residuals(
474            &input.pattern,
475            &trial_calculation.y,
476            ResidualOptions {
477                use_uncertainty: options.calculation.use_uncertainty,
478                parameter_count: specs.len(),
479            },
480        )?;
481        history.push(RietveldIterationRecord {
482            iteration: history.len() + 1,
483            objective,
484            objective_change,
485            scaled_step_norm: factor * step_norm,
486            damping,
487            cg_iterations,
488            backtracks,
489            parameter_changes,
490            topology_changes: topology_changes.clone(),
491            rwp: accepted_metrics.rwp,
492            rp: accepted_metrics.rp,
493            chi_square: accepted_metrics.chi_square,
494            reduced_chi_square: accepted_metrics.reduced_chi_square,
495        });
496        phases = trial_phases;
497        damping = (damping * options.damping_decrease).max(1.0e-18);
498        let accepted_layout = RietveldStructuralLayout::new(&phases, selection, lattice_bounds)?;
499        let state = RietveldCheckpoint {
500            completed_iterations: history.len(),
501            phases: phases.clone(),
502            parameters: accepted_layout.parameters().clone(),
503            objective,
504            damping,
505            history: history.clone(),
506        };
507        runtime.accept_step(Some(&state))?;
508        runtime.emit(
509            RefinementEventKind::StepAccepted,
510            "rietveld_step",
511            "native structural step accepted",
512            vec![
513                ("objective".to_owned(), DiagnosticValue::Float(objective)),
514                (
515                    "topology_changes".to_owned(),
516                    DiagnosticValue::Integer(
517                        i64::try_from(topology_changes.len()).unwrap_or(i64::MAX),
518                    ),
519                ),
520            ],
521        )?;
522        if history.len() >= options.min_iterations
523            && objective_change <= options.objective_tolerance * objective.max(1.0)
524        {
525            termination = TerminationReason::Converged;
526            break;
527        }
528    }
529    let final_layout = RietveldStructuralLayout::new(&phases, selection, lattice_bounds)?;
530    let final_input = replace_phases(input, phases.clone())?;
531    runtime.begin_evaluation().or_else(|error| match error {
532        RuntimeError::Stopped(_) => Ok(()),
533        other => Err(other),
534    })?;
535    let mut calculation = calculate_rietveld_pattern(&final_input, &options.calculation)?;
536    calculation.metrics = evaluate_residuals(
537        &input.pattern,
538        &calculation.y,
539        ResidualOptions {
540            use_uncertainty: options.calculation.use_uncertainty,
541            parameter_count: final_layout.parameters().specs().len(),
542        },
543    )?;
544    let checkpoint = RietveldCheckpoint {
545        completed_iterations: history.len(),
546        phases: phases.clone(),
547        parameters: final_layout.parameters().clone(),
548        objective: 0.5 * calculation.metrics.chi_square,
549        damping,
550        history: history.clone(),
551    };
552    checkpoint.validate(input)?;
553    runtime.emit(
554        RefinementEventKind::Termination,
555        "rietveld",
556        "native structural refinement terminated",
557        vec![(
558            "reason".to_owned(),
559            DiagnosticValue::String(termination.as_str().to_owned()),
560        )],
561    )?;
562    Ok(RietveldRefinementResult {
563        calculation,
564        phases,
565        parameters: final_layout.parameters().clone(),
566        history,
567        termination_reason: termination,
568        checkpoint,
569        evaluations: runtime.evaluations(),
570    })
571}
572
573pub(crate) fn topology_change(
574    before: &RietveldPhase,
575    after: &RietveldPhase,
576) -> Option<RietveldTopologyChange> {
577    let previous = before
578        .reflection_ids()
579        .iter()
580        .collect::<std::collections::BTreeSet<_>>();
581    let current = after
582        .reflection_ids()
583        .iter()
584        .collect::<std::collections::BTreeSet<_>>();
585    let added_reflection_ids = after
586        .reflection_ids()
587        .iter()
588        .filter(|id| !previous.contains(id))
589        .cloned()
590        .collect::<Vec<_>>();
591    let removed_reflection_ids = before
592        .reflection_ids()
593        .iter()
594        .filter(|id| !current.contains(id))
595        .cloned()
596        .collect::<Vec<_>>();
597    if added_reflection_ids.is_empty() && removed_reflection_ids.is_empty() {
598        return None;
599    }
600    Some(RietveldTopologyChange {
601        phase_id: after.phase_id().clone(),
602        added_reflection_ids,
603        removed_reflection_ids,
604        preserved_reflection_count: current.intersection(&previous).count(),
605    })
606}
607
608fn replace_phases(
609    input: &RietveldInput,
610    phases: Vec<RietveldPhase>,
611) -> Result<RietveldInput, RietveldError> {
612    let mut replaced = input.clone();
613    replaced.phases = phases;
614    replaced.validate()?;
615    Ok(replaced)
616}
617
618pub(crate) fn reserve_products<T>(
619    runtime: &mut RefinementRuntime<T>,
620    count: usize,
621) -> Result<(), RuntimeError> {
622    for _ in 0..count {
623        runtime.begin_evaluation()?;
624    }
625    Ok(())
626}
627
628pub(crate) fn conjugate_gradient(
629    right_hand_side: &[f64],
630    tolerance: f64,
631    max_iterations: usize,
632    operator: impl FnMut(&[f64]) -> Result<Vec<f64>, RietveldRefinementError>,
633) -> Result<(Vec<f64>, usize), RietveldRefinementError> {
634    conjugate_gradient_core(right_hand_side, tolerance, max_iterations, operator).map_err(|error| {
635        match error {
636            ConjugateGradientError::Operator(error) => error,
637            ConjugateGradientError::NonPositiveOperator => {
638                RietveldRefinementError::NonPositiveNormalOperator
639            }
640            ConjugateGradientError::NonFiniteState => RietveldRefinementError::NonFiniteSolve,
641        }
642    })
643}
644
645pub(crate) enum ConjugateGradientError<E> {
646    Operator(E),
647    NonPositiveOperator,
648    NonFiniteState,
649}
650
651pub(crate) fn conjugate_gradient_core<E>(
652    right_hand_side: &[f64],
653    tolerance: f64,
654    max_iterations: usize,
655    mut operator: impl FnMut(&[f64]) -> Result<Vec<f64>, E>,
656) -> Result<(Vec<f64>, usize), ConjugateGradientError<E>> {
657    let mut solution = vec![0.0; right_hand_side.len()];
658    let mut residual = right_hand_side.to_vec();
659    let mut direction = residual.clone();
660    let mut squared = dot(&residual, &residual);
661    let target = tolerance * norm(right_hand_side).max(1.0);
662    if squared.sqrt() <= target {
663        return Ok((solution, 0));
664    }
665    for iteration in 1..=max_iterations {
666        let product = operator(&direction).map_err(ConjugateGradientError::Operator)?;
667        let denominator = dot(&direction, &product);
668        if !denominator.is_finite() || denominator <= 0.0 {
669            return Err(ConjugateGradientError::NonPositiveOperator);
670        }
671        let alpha = squared / denominator;
672        for index in 0..solution.len() {
673            solution[index] += alpha * direction[index];
674            residual[index] -= alpha * product[index];
675        }
676        let next_squared = dot(&residual, &residual);
677        if !next_squared.is_finite() {
678            return Err(ConjugateGradientError::NonFiniteState);
679        }
680        if next_squared.sqrt() <= target {
681            return Ok((solution, iteration));
682        }
683        let beta = next_squared / squared;
684        for index in 0..direction.len() {
685            direction[index] = residual[index] + beta * direction[index];
686        }
687        squared = next_squared;
688    }
689    Ok((solution, max_iterations))
690}
691
692fn dot(left: &[f64], right: &[f64]) -> f64 {
693    left.iter()
694        .zip(right)
695        .map(|(left, right)| left * right)
696        .sum()
697}
698
699pub(crate) fn norm(values: &[f64]) -> f64 {
700    dot(values, values).sqrt()
701}
702
703pub(crate) fn normal_stop(
704    error: &RuntimeError,
705) -> Result<TerminationReason, RietveldRefinementError> {
706    if let RuntimeError::Stopped(stop) = error {
707        Ok(stop.reason)
708    } else {
709        Err(RietveldRefinementError::RuntimeMessage(error.to_string()))
710    }
711}
712
713/// Invalid native structural refinement state.
714#[derive(Debug)]
715pub enum RietveldRefinementError {
716    /// Solver controls are invalid.
717    InvalidOptions,
718    /// Restart state is inconsistent with the request.
719    InvalidCheckpoint,
720    /// Matrix-free normal operator lost positive definiteness.
721    NonPositiveNormalOperator,
722    /// Conjugate gradients produced non-finite state.
723    NonFiniteSolve,
724    /// A non-normal runtime failure occurred.
725    RuntimeMessage(String),
726    /// Owned calculation state is invalid.
727    Rietveld(RietveldError),
728    /// Structural parameter transform failed.
729    Parameter(RietveldParameterError),
730    /// Complete parameter layout or installation failed.
731    GeneralParameter(crate::RietveldGeneralParameterError),
732    /// Prepared objective failed.
733    Objective(RietveldObjectiveError),
734    /// Complete prepared objective failed.
735    GeneralObjective(crate::RietveldGeneralObjectiveError),
736    /// Constraint graph or transform failed.
737    Constraint(crate::ConstraintError),
738    /// Runtime boundary failed.
739    Runtime(RuntimeError),
740    /// Residual evaluation failed.
741    Residual(crate::ResidualError),
742}
743
744impl Display for RietveldRefinementError {
745    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
746        match self {
747            Self::InvalidOptions => formatter.write_str("native Rietveld options are invalid"),
748            Self::InvalidCheckpoint => formatter.write_str("native Rietveld checkpoint is invalid"),
749            Self::NonPositiveNormalOperator => {
750                formatter.write_str("native Rietveld normal operator is not positive definite")
751            }
752            Self::NonFiniteSolve => formatter.write_str("native Rietveld solve became non-finite"),
753            Self::RuntimeMessage(message) => formatter.write_str(message),
754            Self::Rietveld(error) => Display::fmt(error, formatter),
755            Self::Parameter(error) => Display::fmt(error, formatter),
756            Self::GeneralParameter(error) => Display::fmt(error, formatter),
757            Self::Objective(error) => Display::fmt(error, formatter),
758            Self::GeneralObjective(error) => Display::fmt(error, formatter),
759            Self::Constraint(error) => Display::fmt(error, formatter),
760            Self::Runtime(error) => Display::fmt(error, formatter),
761            Self::Residual(error) => Display::fmt(error, formatter),
762        }
763    }
764}
765
766impl Error for RietveldRefinementError {
767    fn source(&self) -> Option<&(dyn Error + 'static)> {
768        match self {
769            Self::Rietveld(error) => Some(error),
770            Self::Parameter(error) => Some(error),
771            Self::GeneralParameter(error) => Some(error),
772            Self::Objective(error) => Some(error),
773            Self::GeneralObjective(error) => Some(error),
774            Self::Constraint(error) => Some(error),
775            Self::Runtime(error) => Some(error),
776            Self::Residual(error) => Some(error),
777            Self::InvalidOptions
778            | Self::InvalidCheckpoint
779            | Self::NonPositiveNormalOperator
780            | Self::NonFiniteSolve
781            | Self::RuntimeMessage(_) => None,
782        }
783    }
784}
785impl From<RietveldError> for RietveldRefinementError {
786    fn from(value: RietveldError) -> Self {
787        Self::Rietveld(value)
788    }
789}
790impl From<RietveldParameterError> for RietveldRefinementError {
791    fn from(value: RietveldParameterError) -> Self {
792        Self::Parameter(value)
793    }
794}
795impl From<RietveldObjectiveError> for RietveldRefinementError {
796    fn from(value: RietveldObjectiveError) -> Self {
797        Self::Objective(value)
798    }
799}
800impl From<crate::RietveldGeneralParameterError> for RietveldRefinementError {
801    fn from(value: crate::RietveldGeneralParameterError) -> Self {
802        Self::GeneralParameter(value)
803    }
804}
805impl From<crate::RietveldGeneralObjectiveError> for RietveldRefinementError {
806    fn from(value: crate::RietveldGeneralObjectiveError) -> Self {
807        Self::GeneralObjective(value)
808    }
809}
810impl From<crate::ConstraintError> for RietveldRefinementError {
811    fn from(value: crate::ConstraintError) -> Self {
812        Self::Constraint(value)
813    }
814}
815impl From<RuntimeError> for RietveldRefinementError {
816    fn from(value: RuntimeError) -> Self {
817        Self::Runtime(value)
818    }
819}
820impl From<crate::ResidualError> for RietveldRefinementError {
821    fn from(value: crate::ResidualError) -> Self {
822        Self::Residual(value)
823    }
824}