Skip to main content

phasesmith_workflows/
rietveld_general_objective.rs

1//! Complete matrix-free Rietveld objective with small explicit global columns.
2
3use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use crate::{
7    ConstraintDerivativeMatrix, DifferentiableBackground, LatticeError, LatticeParameterization,
8    PreparedRietveldObjective, RietveldCalculation, RietveldCalculationOptions, RietveldError,
9    RietveldGeneralParameterError, RietveldInput, RietveldInstrumentParameter,
10    RietveldObjectiveError, RietveldParameterLayout, calculate_rietveld_pattern,
11};
12
13/// Default upper bound for reusable native structural Jacobians.
14///
15/// Ten million `f64` elements require at most about 80 MB before the smaller
16/// complete-parameter columns are composed. Larger requests retain the
17/// matrix-free objective.
18pub const DEFAULT_MAX_LINEARIZATION_ELEMENTS: usize = 10_000_000;
19
20/// Complete weighted Jacobian in scaled free-parameter coordinates.
21///
22/// Rows are free parameters and columns are pattern samples. Masked samples
23/// are zero and included samples are divided by their uncertainty when the
24/// objective uses uncertainty weighting. This matches the dense scripting
25/// optimizer contract, so its JVP and VJP need no further weighting.
26pub struct PreparedGeneralFreeLinearization {
27    calculation: RietveldCalculation,
28    weighted_jacobian: Vec<f64>,
29    sample_scale: Vec<f64>,
30    parameter_count: usize,
31}
32
33impl PreparedGeneralFreeLinearization {
34    /// Borrow the calculation produced by the same fused native pass.
35    #[must_use]
36    pub const fn calculation(&self) -> &RietveldCalculation {
37        &self.calculation
38    }
39
40    /// Return the scaled free-parameter count.
41    #[must_use]
42    pub const fn parameter_count(&self) -> usize {
43        self.parameter_count
44    }
45
46    /// Apply the weighted free Jacobian.
47    ///
48    /// # Errors
49    ///
50    /// Returns [`RietveldGeneralParameterError::ValueLengthMismatch`] for a
51    /// direction with the wrong free dimension.
52    pub fn jvp(&self, direction: &[f64]) -> Result<Vec<f64>, RietveldGeneralObjectiveError> {
53        if direction.len() != self.parameter_count {
54            return Err(RietveldGeneralParameterError::ValueLengthMismatch.into());
55        }
56        Ok(dense_forward_product(
57            &self.weighted_jacobian,
58            direction,
59            self.sample_scale.len(),
60        ))
61    }
62
63    /// Apply the weighted free Jacobian transpose.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`RietveldGeneralObjectiveError::SampleLengthMismatch`] for a
68    /// vector with the wrong sample dimension.
69    pub fn vjp(&self, samples: &[f64]) -> Result<Vec<f64>, RietveldGeneralObjectiveError> {
70        if samples.len() != self.sample_scale.len() {
71            return Err(RietveldGeneralObjectiveError::SampleLengthMismatch);
72        }
73        Ok(dense_reverse_product(
74            &self.weighted_jacobian,
75            samples,
76            self.parameter_count,
77        ))
78    }
79
80    /// Return `J_w^T J_w direction + damping direction` in scaled free coordinates.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`RietveldGeneralObjectiveError`] for invalid damping or shape.
85    pub fn normal_product(
86        &self,
87        direction: &[f64],
88        damping: f64,
89    ) -> Result<Vec<f64>, RietveldGeneralObjectiveError> {
90        if !damping.is_finite() || damping < 0.0 {
91            return Err(RietveldGeneralObjectiveError::InvalidDamping);
92        }
93        let product = self.jvp(direction)?;
94        let mut result = self.vjp(&product)?;
95        for (value, direction) in result.iter_mut().zip(direction) {
96            *value += damping * direction;
97        }
98        Ok(result)
99    }
100
101    /// Calculate the scaled free gradient for the stored calculation.
102    ///
103    /// # Errors
104    ///
105    /// Returns [`RietveldGeneralObjectiveError::SampleLengthMismatch`] for an
106    /// observed vector with the wrong sample dimension.
107    pub fn gradient(&self, observed: &[f64]) -> Result<Vec<f64>, RietveldGeneralObjectiveError> {
108        if observed.len() != self.sample_scale.len() {
109            return Err(RietveldGeneralObjectiveError::SampleLengthMismatch);
110        }
111        let weighted_residual = self
112            .calculation
113            .y
114            .iter()
115            .zip(observed)
116            .zip(&self.sample_scale)
117            .map(|((calculated, observed), scale)| (calculated - observed) * scale)
118            .collect::<Vec<_>>();
119        self.vjp(&weighted_residual)
120    }
121}
122
123/// Reusable complete physical objective for one accepted native state.
124pub struct PreparedGeneralRietveldObjective {
125    input: RietveldInput,
126    options: RietveldCalculationOptions,
127    layout: RietveldParameterLayout,
128    structural: PreparedRietveldObjective,
129    calculation: RietveldCalculation,
130    dense_structural_jacobian: Option<Vec<f64>>,
131    explicit_columns: Vec<(usize, Vec<f64>)>,
132}
133
134impl PreparedGeneralRietveldObjective {
135    /// Prepare structural products and explicit global/background columns.
136    ///
137    /// The structural Jacobian is materialized when it fits under the default
138    /// memory ceiling. Larger objectives retain matrix-free structural
139    /// products; selected experiment/background columns remain explicit.
140    ///
141    /// # Errors
142    ///
143    /// Returns [`RietveldGeneralObjectiveError`] for invalid state or derivative
144    /// layout mismatches.
145    pub fn new(
146        input: RietveldInput,
147        options: RietveldCalculationOptions,
148        layout: RietveldParameterLayout,
149    ) -> Result<Self, RietveldGeneralObjectiveError> {
150        Self::new_with_max_linearization_elements(
151            input,
152            options,
153            layout,
154            DEFAULT_MAX_LINEARIZATION_ELEMENTS,
155        )
156    }
157
158    /// Prepare a complete objective under an explicit dense-memory ceiling.
159    ///
160    /// A zero ceiling forces matrix-free products. Requests whose native
161    /// structural Jacobian fits within the ceiling materialize it once and
162    /// reuse it for every gradient and normal product at this state.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`RietveldGeneralObjectiveError`] for invalid state, derivative
167    /// layout mismatches, or failed structural products.
168    pub fn new_with_max_linearization_elements(
169        input: RietveldInput,
170        options: RietveldCalculationOptions,
171        layout: RietveldParameterLayout,
172        max_linearization_elements: usize,
173    ) -> Result<Self, RietveldGeneralObjectiveError> {
174        let structural = PreparedRietveldObjective::new(
175            input.clone(),
176            options.clone(),
177            layout.structural_layout().clone(),
178        )?;
179        let dense_enabled = max_linearization_elements > 0
180            && structural.dense_element_count()? <= max_linearization_elements;
181        let (calculation, dense_structural_jacobian) = if dense_enabled {
182            let linearization = structural.linearize()?;
183            (linearization.calculation, Some(linearization.jacobian))
184        } else {
185            (calculate_rietveld_pattern(&input, &options)?, None)
186        };
187        let mut explicit_columns = instrument_columns(&input, &calculation, &layout)?;
188        append_sample_physics_columns(&input, &calculation, &layout, &mut explicit_columns)?;
189        append_background_columns(&input, &layout, &mut explicit_columns)?;
190        Ok(Self {
191            input,
192            options,
193            layout,
194            structural,
195            calculation,
196            dense_structural_jacobian,
197            explicit_columns,
198        })
199    }
200
201    /// Return whether this state reuses a bounded dense structural Jacobian.
202    #[must_use]
203    pub const fn uses_dense_linearization(&self) -> bool {
204        self.dense_structural_jacobian.is_some()
205    }
206
207    /// Return expensive model products consumed while preparing the gradient.
208    #[must_use]
209    pub const fn preparation_evaluation_count(&self) -> usize {
210        if self.uses_dense_linearization() {
211            1
212        } else {
213            2
214        }
215    }
216
217    /// Return expensive model products consumed by one normal-product call.
218    #[must_use]
219    pub const fn normal_product_evaluation_count(&self) -> usize {
220        if self.uses_dense_linearization() {
221            0
222        } else {
223            2
224        }
225    }
226
227    /// Return expensive model products consumed by one forward-product call.
228    #[must_use]
229    pub const fn jvp_evaluation_count(&self) -> usize {
230        if self.uses_dense_linearization() {
231            0
232        } else {
233            1
234        }
235    }
236
237    /// Borrow the complete stable physical layout.
238    #[must_use]
239    pub const fn layout(&self) -> &RietveldParameterLayout {
240        &self.layout
241    }
242
243    /// Borrow the accepted-state calculation used for global columns.
244    #[must_use]
245    pub const fn calculation(&self) -> &RietveldCalculation {
246        &self.calculation
247    }
248
249    /// Project the cached dense objective into weighted scaled-free rows.
250    ///
251    /// Returns `None` for the bounded matrix-free fallback. The derivative
252    /// matrix maps scaled free coordinates to the complete physical layout.
253    ///
254    /// # Errors
255    ///
256    /// Returns [`RietveldGeneralObjectiveError`] for incompatible dimensions
257    /// or failed dense products.
258    pub fn free_linearization(
259        &self,
260        derivative: &ConstraintDerivativeMatrix,
261    ) -> Result<Option<PreparedGeneralFreeLinearization>, RietveldGeneralObjectiveError> {
262        if !self.uses_dense_linearization() {
263            return Ok(None);
264        }
265        if derivative.rows != self.layout.parameters().specs().len() {
266            return Err(RietveldGeneralParameterError::ValueLengthMismatch.into());
267        }
268        let sample_count = self.input.pattern.sample_count();
269        let sample_scale = self.sample_scale();
270        let element_count = derivative
271            .columns
272            .checked_mul(sample_count)
273            .ok_or(RietveldObjectiveError::AllocationOverflow)?;
274        let mut weighted_jacobian = Vec::with_capacity(element_count);
275        for free in 0..derivative.columns {
276            let physical = derivative
277                .values
278                .chunks_exact(derivative.columns)
279                .map(|row| row[free])
280                .collect::<Vec<_>>();
281            let (_, mut row) = self.jvp(&physical)?;
282            for (value, scale) in row.iter_mut().zip(&sample_scale) {
283                *value *= scale;
284            }
285            weighted_jacobian.extend(row);
286        }
287        Ok(Some(PreparedGeneralFreeLinearization {
288            calculation: self.calculation.clone(),
289            weighted_jacobian,
290            sample_scale,
291            parameter_count: derivative.columns,
292        }))
293    }
294
295    /// Calculate profile values and one complete physical directional derivative.
296    ///
297    /// # Errors
298    ///
299    /// Returns [`RietveldGeneralObjectiveError`] for a wrong direction or
300    /// numerical product failure.
301    pub fn jvp(
302        &self,
303        direction: &[f64],
304    ) -> Result<(Vec<f64>, Vec<f64>), RietveldGeneralObjectiveError> {
305        if direction.len() != self.layout.parameters().specs().len() {
306            return Err(RietveldGeneralParameterError::ValueLengthMismatch.into());
307        }
308        let structural_direction = self.layout.structural_direction(direction)?;
309        let (profile, mut derivative) = if let Some(jacobian) = &self.dense_structural_jacobian {
310            (
311                self.calculation.profile_y.clone(),
312                dense_forward_product(
313                    jacobian,
314                    &structural_direction,
315                    self.input.pattern.sample_count(),
316                ),
317            )
318        } else {
319            self.structural.jvp(&structural_direction)?
320        };
321        for (parameter, column) in &self.explicit_columns {
322            let coefficient = direction[*parameter];
323            for (target, value) in derivative.iter_mut().zip(column) {
324                *target += coefficient * value;
325            }
326        }
327        Ok((profile, derivative))
328    }
329
330    /// Apply the complete physical Jacobian transpose.
331    ///
332    /// # Errors
333    ///
334    /// Returns [`RietveldGeneralObjectiveError`] for sample shape or engine
335    /// product failures.
336    pub fn vjp(&self, sample_weights: &[f64]) -> Result<Vec<f64>, RietveldGeneralObjectiveError> {
337        if sample_weights.len() != self.input.pattern.sample_count() {
338            return Err(RietveldGeneralObjectiveError::SampleLengthMismatch);
339        }
340        let structural = if let Some(jacobian) = &self.dense_structural_jacobian {
341            dense_reverse_product(
342                jacobian,
343                sample_weights,
344                self.layout.structural_layout().parameters().specs().len(),
345            )
346        } else {
347            self.structural.vjp(sample_weights)?
348        };
349        let mut result = self.layout.expand_structural_gradient(&structural)?;
350        for (parameter, column) in &self.explicit_columns {
351            result[*parameter] += column
352                .iter()
353                .zip(sample_weights)
354                .map(|(left, right)| left * right)
355                .sum::<f64>();
356        }
357        Ok(result)
358    }
359
360    /// Apply `J^T W J + damping I` in complete physical coordinates.
361    ///
362    /// # Errors
363    ///
364    /// Returns [`RietveldGeneralObjectiveError`] for invalid damping or product
365    /// state.
366    pub fn normal_product(
367        &self,
368        direction: &[f64],
369        damping: f64,
370    ) -> Result<Vec<f64>, RietveldGeneralObjectiveError> {
371        if !damping.is_finite() || damping < 0.0 {
372            return Err(RietveldGeneralObjectiveError::InvalidDamping);
373        }
374        let (_, derivative) = self.jvp(direction)?;
375        let weighted = self.weight_samples(&derivative);
376        let mut result = self.vjp(&weighted)?;
377        for (value, direction) in result.iter_mut().zip(direction) {
378            *value += damping * direction;
379        }
380        Ok(result)
381    }
382
383    /// Calculate the complete accepted-state gradient.
384    ///
385    /// # Errors
386    ///
387    /// Returns [`RietveldGeneralObjectiveError`] for residual or product state.
388    pub fn gradient(&self) -> Result<(Vec<f64>, Vec<f64>), RietveldGeneralObjectiveError> {
389        let observed = self
390            .input
391            .pattern
392            .observed_y
393            .as_ref()
394            .ok_or(RietveldError::MissingObservations)?;
395        let residual = self
396            .calculation
397            .y
398            .iter()
399            .zip(observed)
400            .map(|(calculated, observed)| calculated - observed)
401            .collect::<Vec<_>>();
402        Ok((
403            self.calculation.y.clone(),
404            self.vjp(&self.weight_samples(&residual))?,
405        ))
406    }
407
408    fn weight_samples(&self, values: &[f64]) -> Vec<f64> {
409        let mask = self.input.pattern.mask.as_deref();
410        let uncertainty = self
411            .options
412            .use_uncertainty
413            .then_some(self.input.pattern.uncertainty.as_deref())
414            .flatten();
415        values
416            .iter()
417            .enumerate()
418            .map(|(index, value)| {
419                if mask.is_some_and(|mask| !mask[index]) {
420                    0.0
421                } else if let Some(sigma) = uncertainty {
422                    value / (sigma[index] * sigma[index])
423                } else {
424                    *value
425                }
426            })
427            .collect()
428    }
429
430    fn sample_scale(&self) -> Vec<f64> {
431        let mask = self.input.pattern.mask.as_deref();
432        let uncertainty = self
433            .options
434            .use_uncertainty
435            .then_some(self.input.pattern.uncertainty.as_deref())
436            .flatten();
437        (0..self.input.pattern.sample_count())
438            .map(|index| {
439                if mask.is_some_and(|mask| !mask[index]) {
440                    0.0
441                } else {
442                    uncertainty.map_or(1.0, |sigma| sigma[index].recip())
443                }
444            })
445            .collect()
446    }
447}
448
449fn dense_forward_product(jacobian: &[f64], direction: &[f64], sample_count: usize) -> Vec<f64> {
450    let mut result = vec![0.0; sample_count];
451    for (coefficient, row) in direction.iter().zip(jacobian.chunks_exact(sample_count)) {
452        if *coefficient == 0.0 {
453            continue;
454        }
455        for (target, value) in result.iter_mut().zip(row) {
456            *target += coefficient * value;
457        }
458    }
459    result
460}
461
462fn dense_reverse_product(
463    jacobian: &[f64],
464    sample_weights: &[f64],
465    parameter_count: usize,
466) -> Vec<f64> {
467    jacobian
468        .chunks_exact(sample_weights.len())
469        .take(parameter_count)
470        .map(|row| {
471            row.iter()
472                .zip(sample_weights)
473                .map(|(left, right)| left * right)
474                .sum()
475        })
476        .collect()
477}
478
479fn instrument_columns(
480    input: &RietveldInput,
481    calculation: &RietveldCalculation,
482    layout: &RietveldParameterLayout,
483) -> Result<Vec<(usize, Vec<f64>)>, RietveldGeneralObjectiveError> {
484    let sample_count = input.pattern.sample_count();
485    let mut result = Vec::new();
486    for (parameter, index) in layout.instrument_indices() {
487        let row = instrument_global_row(input, *parameter)?;
488        let mut column = vec![0.0; sample_count];
489        for phase in &calculation.phases {
490            let global = phase
491                .result
492                .accumulation
493                .derivatives
494                .global
495                .as_ref()
496                .ok_or(RietveldGeneralObjectiveError::MissingGlobalDerivatives)?;
497            if row >= global.parameter_count || global.sample_count != sample_count {
498                return Err(RietveldGeneralObjectiveError::GlobalDerivativeShape);
499            }
500            let values = global
501                .values
502                .get(row * sample_count..(row + 1) * sample_count)
503                .ok_or(RietveldGeneralObjectiveError::GlobalDerivativeShape)?;
504            for (target, value) in column.iter_mut().zip(values) {
505                *target += value;
506            }
507        }
508        result.push((*index, column));
509    }
510    Ok(result)
511}
512
513fn append_sample_physics_columns(
514    input: &RietveldInput,
515    calculation: &RietveldCalculation,
516    layout: &RietveldParameterLayout,
517    explicit_columns: &mut Vec<(usize, Vec<f64>)>,
518) -> Result<(), RietveldGeneralObjectiveError> {
519    let mut terms = layout
520        .sample_physics_indices()
521        .map(|(phase, name, parameter)| (phase, name.to_owned(), parameter, 1.0))
522        .collect::<Vec<_>>();
523    for (phase_index, phase) in input.phases.iter().enumerate() {
524        let (_, names) =
525            phase.resolved_sample_physics(input.instrument, input.position_correction)?;
526        let cell_prefixes = ["march_dollase", "stephens"]
527            .into_iter()
528            .filter(|prefix| {
529                names
530                    .iter()
531                    .any(|name| name.starts_with(&format!("{prefix}.cell.")))
532            })
533            .collect::<Vec<_>>();
534        if cell_prefixes.is_empty() {
535            continue;
536        }
537        let parameterization = LatticeParameterization::new(
538            phase.definition().space_group.clone(),
539            phase.definition().cell,
540        )?;
541        let lattice_values = parameterization.values_from_cell(phase.definition().cell)?;
542        let jacobian = parameterization.cell_jacobian(&lattice_values)?;
543        let columns = lattice_values.len();
544        for (parameter_index, spec) in layout.parameters().specs().iter().enumerate() {
545            if spec.key().module() != "lattice"
546                || spec.key().owner_id() != phase.phase_id().as_str()
547            {
548                continue;
549            }
550            let column = parameterization
551                .parameter_names()
552                .iter()
553                .position(|name| name == spec.key().name())
554                .ok_or(RietveldGeneralObjectiveError::GlobalDerivativeShape)?;
555            for prefix in &cell_prefixes {
556                for (cell_row, name) in [
557                    "a_angstrom",
558                    "b_angstrom",
559                    "c_angstrom",
560                    "alpha_deg",
561                    "beta_deg",
562                    "gamma_deg",
563                ]
564                .iter()
565                .enumerate()
566                {
567                    let coefficient = jacobian[cell_row * columns + column];
568                    if coefficient != 0.0 {
569                        terms.push((
570                            phase_index,
571                            format!("{prefix}.cell.{name}"),
572                            parameter_index,
573                            coefficient,
574                        ));
575                    }
576                }
577            }
578        }
579    }
580    for (phase_index, name, parameter_index, coefficient) in terms {
581        append_sample_physics_column(
582            input,
583            calculation,
584            explicit_columns,
585            phase_index,
586            &name,
587            parameter_index,
588            coefficient,
589        )?;
590    }
591    Ok(())
592}
593
594#[allow(clippy::too_many_arguments)]
595fn append_sample_physics_column(
596    input: &RietveldInput,
597    calculation: &RietveldCalculation,
598    explicit_columns: &mut Vec<(usize, Vec<f64>)>,
599    phase_index: usize,
600    name: &str,
601    parameter_index: usize,
602    coefficient: f64,
603) -> Result<(), RietveldGeneralObjectiveError> {
604    let phase = input
605        .phases
606        .get(phase_index)
607        .ok_or(RietveldGeneralObjectiveError::GlobalDerivativeShape)?;
608    let (_, names) = phase.resolved_sample_physics(input.instrument, input.position_correction)?;
609    let provider_row = names
610        .iter()
611        .position(|candidate| candidate == name)
612        .ok_or(RietveldGeneralObjectiveError::GlobalDerivativeShape)?;
613    let sample_count = input.pattern.sample_count();
614    let row = sample_physics_global_start(input) + provider_row;
615    let global = calculation.phases[phase_index]
616        .result
617        .accumulation
618        .derivatives
619        .global
620        .as_ref()
621        .ok_or(RietveldGeneralObjectiveError::MissingGlobalDerivatives)?;
622    let values = global
623        .values
624        .get(row * sample_count..(row + 1) * sample_count)
625        .ok_or(RietveldGeneralObjectiveError::GlobalDerivativeShape)?;
626    if let Some((_, column)) = explicit_columns
627        .iter_mut()
628        .find(|(index, _)| *index == parameter_index)
629    {
630        for (target, value) in column.iter_mut().zip(values) {
631            *target += coefficient * value;
632        }
633    } else {
634        explicit_columns.push((
635            parameter_index,
636            values.iter().map(|value| coefficient * value).collect(),
637        ));
638    }
639    Ok(())
640}
641
642fn append_background_columns(
643    input: &RietveldInput,
644    layout: &RietveldParameterLayout,
645    explicit_columns: &mut Vec<(usize, Vec<f64>)>,
646) -> Result<(), RietveldGeneralObjectiveError> {
647    if layout.background_indices().is_empty() {
648        return Ok(());
649    }
650    let background = input
651        .background
652        .as_ref()
653        .ok_or(RietveldGeneralParameterError::MissingBackground)?;
654    let basis = background.basis(&input.pattern.x_deg)?;
655    if basis.columns != layout.background_indices().len()
656        || basis.rows != input.pattern.sample_count()
657    {
658        return Err(RietveldGeneralObjectiveError::BackgroundDerivativeShape);
659    }
660    for (column_index, parameter_index) in layout.background_indices().iter().enumerate() {
661        explicit_columns.push((
662            *parameter_index,
663            basis
664                .column(column_index)
665                .ok_or(RietveldGeneralObjectiveError::BackgroundDerivativeShape)?,
666        ));
667    }
668    Ok(())
669}
670
671fn sample_physics_global_start(input: &RietveldInput) -> usize {
672    7 - usize::from(input.fixed_spectrum.is_some())
673        + usize::from(input.position_correction.bragg_brentano_mm.is_some())
674        + 2 * usize::from(
675            input
676                .position_correction
677                .debye_scherrer_micrometre
678                .is_some(),
679        )
680        + 2 * usize::from(input.axial_geometry.is_some())
681}
682
683fn instrument_global_row(
684    input: &RietveldInput,
685    parameter: RietveldInstrumentParameter,
686) -> Result<usize, RietveldGeneralObjectiveError> {
687    let monochromatic_row = match parameter {
688        RietveldInstrumentParameter::UDeg2 => 0,
689        RietveldInstrumentParameter::VDeg2 => 1,
690        RietveldInstrumentParameter::WDeg2 => 2,
691        RietveldInstrumentParameter::XDeg => 3,
692        RietveldInstrumentParameter::YDeg => 4,
693        RietveldInstrumentParameter::WavelengthAngstrom => 5,
694        RietveldInstrumentParameter::ZeroShiftDeg => 6,
695        RietveldInstrumentParameter::SampleDisplacementMm => {
696            if input.position_correction.bragg_brentano_mm.is_none() {
697                return Err(RietveldGeneralParameterError::InstrumentGeometryMismatch.into());
698            }
699            7
700        }
701        RietveldInstrumentParameter::DisplaceXMicrometre => {
702            if input
703                .position_correction
704                .debye_scherrer_micrometre
705                .is_none()
706            {
707                return Err(RietveldGeneralParameterError::InstrumentGeometryMismatch.into());
708            }
709            7
710        }
711        RietveldInstrumentParameter::DisplaceYMicrometre => {
712            if input
713                .position_correction
714                .debye_scherrer_micrometre
715                .is_none()
716            {
717                return Err(RietveldGeneralParameterError::InstrumentGeometryMismatch.into());
718            }
719            8
720        }
721    };
722    if input.fixed_spectrum.is_some() {
723        if parameter == RietveldInstrumentParameter::WavelengthAngstrom {
724            return Err(RietveldGeneralParameterError::SpectrumWavelengthRefinement.into());
725        }
726        Ok(monochromatic_row - usize::from(monochromatic_row > 5))
727    } else {
728        Ok(monochromatic_row)
729    }
730}
731
732/// Invalid complete native Rietveld objective state.
733#[derive(Debug)]
734pub enum RietveldGeneralObjectiveError {
735    /// Complete parameter state is invalid.
736    Parameter(RietveldGeneralParameterError),
737    /// Structural objective state is invalid.
738    Structural(RietveldObjectiveError),
739    /// Request/calculation state is invalid.
740    Rietveld(RietveldError),
741    /// Lattice-to-sample derivative mapping failed.
742    Lattice(LatticeError),
743    /// Engine did not expose required global derivatives.
744    MissingGlobalDerivatives,
745    /// An engine global derivative matrix has an unexpected shape.
746    GlobalDerivativeShape,
747    /// A background basis has an unexpected shape.
748    BackgroundDerivativeShape,
749    /// Sample reverse-product length is wrong.
750    SampleLengthMismatch,
751    /// Damping must be finite and non-negative.
752    InvalidDamping,
753}
754
755impl Display for RietveldGeneralObjectiveError {
756    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
757        match self {
758            Self::Parameter(error) => Display::fmt(error, formatter),
759            Self::Structural(error) => Display::fmt(error, formatter),
760            Self::Rietveld(error) => Display::fmt(error, formatter),
761            Self::Lattice(error) => Display::fmt(error, formatter),
762            Self::MissingGlobalDerivatives => {
763                formatter.write_str("native engine omitted Rietveld global derivatives")
764            }
765            Self::GlobalDerivativeShape => {
766                formatter.write_str("native Rietveld global derivative shape is invalid")
767            }
768            Self::BackgroundDerivativeShape => {
769                formatter.write_str("native Rietveld background derivative shape is invalid")
770            }
771            Self::SampleLengthMismatch => {
772                formatter.write_str("native Rietveld sample weight length is wrong")
773            }
774            Self::InvalidDamping => {
775                formatter.write_str("native Rietveld damping must be finite and non-negative")
776            }
777        }
778    }
779}
780
781impl Error for RietveldGeneralObjectiveError {
782    fn source(&self) -> Option<&(dyn Error + 'static)> {
783        match self {
784            Self::Parameter(error) => Some(error),
785            Self::Structural(error) => Some(error),
786            Self::Rietveld(error) => Some(error),
787            Self::Lattice(error) => Some(error),
788            Self::MissingGlobalDerivatives
789            | Self::GlobalDerivativeShape
790            | Self::BackgroundDerivativeShape
791            | Self::SampleLengthMismatch
792            | Self::InvalidDamping => None,
793        }
794    }
795}
796
797impl From<RietveldGeneralParameterError> for RietveldGeneralObjectiveError {
798    fn from(value: RietveldGeneralParameterError) -> Self {
799        Self::Parameter(value)
800    }
801}
802impl From<RietveldObjectiveError> for RietveldGeneralObjectiveError {
803    fn from(value: RietveldObjectiveError) -> Self {
804        Self::Structural(value)
805    }
806}
807impl From<RietveldError> for RietveldGeneralObjectiveError {
808    fn from(value: RietveldError) -> Self {
809        Self::Rietveld(value)
810    }
811}
812impl From<LatticeError> for RietveldGeneralObjectiveError {
813    fn from(value: LatticeError) -> Self {
814        Self::Lattice(value)
815    }
816}
817impl From<crate::BackgroundError> for RietveldGeneralObjectiveError {
818    fn from(value: crate::BackgroundError) -> Self {
819        Self::Parameter(RietveldGeneralParameterError::Background(value))
820    }
821}