1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use crate::rietveld_solver::{
7 ConjugateGradientError, conjugate_gradient_core, norm, topology_change,
8};
9use crate::{
10 CancellationToken, Constraint, ConstraintDerivativeMatrix, ConstraintError,
11 ConstraintTransform, DiagnosticValue, JointRietveldError, JointRietveldHistogram,
12 JointRietveldLayout, ParameterChange, ParameterError, ParameterKey, ParameterSet,
13 PreparedJointRietveldObjective, RefinementEventKind, RefinementLimits, RefinementRuntime,
14 RietveldCalculation, RietveldTopologyChange, RuntimeError, TerminationReason,
15 calculate_rietveld_pattern,
16};
17
18#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct JointRietveldRefinementOptions {
21 pub limits: RefinementLimits,
23 pub min_iterations: usize,
25 pub objective_tolerance: f64,
27 pub parameter_tolerance: f64,
29 pub initial_damping: f64,
31 pub damping_increase: f64,
33 pub damping_decrease: f64,
35 pub cg_tolerance: f64,
37 pub max_cg_iterations: usize,
39 pub max_scaled_parameter_step: f64,
41 pub max_backtracks: usize,
43}
44
45impl JointRietveldRefinementOptions {
46 #[allow(clippy::too_many_arguments)]
53 pub fn new(
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, JointRietveldRefinementError> {
66 let result = Self {
67 limits,
68 min_iterations,
69 objective_tolerance,
70 parameter_tolerance,
71 initial_damping,
72 damping_increase,
73 damping_decrease,
74 cg_tolerance,
75 max_cg_iterations,
76 max_scaled_parameter_step,
77 max_backtracks,
78 };
79 result.validate()?;
80 Ok(result)
81 }
82
83 pub fn validate(self) -> Result<(), JointRietveldRefinementError> {
90 let positive = [
91 self.objective_tolerance,
92 self.parameter_tolerance,
93 self.initial_damping,
94 self.damping_increase,
95 self.damping_decrease,
96 self.cg_tolerance,
97 self.max_scaled_parameter_step,
98 ];
99 if self.min_iterations == 0
100 || self.min_iterations > self.limits.max_iterations()
101 || self.max_cg_iterations == 0
102 || positive
103 .iter()
104 .any(|value| !value.is_finite() || *value <= 0.0)
105 || self.damping_increase <= 1.0
106 || self.damping_decrease >= 1.0
107 {
108 return Err(JointRietveldRefinementError::InvalidOptions);
109 }
110 Ok(())
111 }
112}
113
114#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct JointRietveldTopologyChange {
117 pub histogram_id: phasesmith_model::RecordId,
119 pub change: RietveldTopologyChange,
121}
122
123#[derive(Clone, Copy, Debug, PartialEq)]
125pub struct JointRietveldMetrics {
126 pub included_samples: usize,
128 pub rp: f64,
130 pub rwp: f64,
132 pub chi_square: f64,
134 pub reduced_chi_square: f64,
136}
137
138#[derive(Clone, Debug, PartialEq)]
140pub struct JointRietveldIterationRecord {
141 pub iteration: usize,
143 pub objective: f64,
145 pub objective_change: f64,
147 pub scaled_step_norm: f64,
149 pub damping: f64,
151 pub cg_iterations: usize,
153 pub backtracks: usize,
155 pub parameter_changes: Vec<ParameterChange>,
157 pub topology_changes: Vec<JointRietveldTopologyChange>,
159 pub metrics: JointRietveldMetrics,
161}
162
163#[derive(Clone, Debug, PartialEq)]
165pub struct JointRietveldCheckpoint {
166 pub request: Vec<JointRietveldHistogram>,
168 pub histograms: Vec<JointRietveldHistogram>,
170 pub constraints: Vec<Constraint>,
172 pub parameters: ParameterSet,
174 pub objective: f64,
176 pub damping: f64,
178 pub history: Vec<JointRietveldIterationRecord>,
180}
181
182impl JointRietveldCheckpoint {
183 #[must_use]
185 pub fn completed_iterations(&self) -> usize {
186 self.history.len()
187 }
188
189 pub fn validate_for(
196 &self,
197 request: &[JointRietveldHistogram],
198 constraints: &[Constraint],
199 ) -> Result<(), JointRietveldRefinementError> {
200 if self.request != request {
201 return Err(JointRietveldRefinementError::InvalidCheckpoint {
202 reason: "request contract changed",
203 });
204 }
205 if self.constraints != constraints {
206 return Err(JointRietveldRefinementError::InvalidCheckpoint {
207 reason: "constraint graph changed",
208 });
209 }
210 if !self.objective.is_finite()
211 || self.objective < 0.0
212 || !self.damping.is_finite()
213 || self.damping <= 0.0
214 || !valid_history(&self.history)
215 || self
216 .history
217 .last()
218 .is_some_and(|row| row.objective.to_bits() != self.objective.to_bits())
219 {
220 return Err(JointRietveldRefinementError::InvalidCheckpoint {
221 reason: "accepted numerical state is invalid",
222 });
223 }
224 let request_layout = JointRietveldLayout::new(request)?;
225 if !parameter_contract_matches(request_layout.parameters(), &self.parameters) {
226 return Err(JointRietveldRefinementError::InvalidCheckpoint {
227 reason: "parameter contract changed",
228 });
229 }
230 let layout = JointRietveldLayout::new(&self.histograms)?;
231 if !parameter_values_match(layout.parameters(), &self.parameters) {
232 return Err(JointRietveldRefinementError::InvalidCheckpoint {
233 reason: "accepted parameters do not match histogram state",
234 });
235 }
236 let transform = ConstraintTransform::new(self.parameters.clone(), constraints.to_vec())?;
237 validate_constraint_state(layout.parameters(), &transform)?;
238 Ok(())
239 }
240}
241
242#[derive(Clone, Debug, PartialEq)]
244pub struct JointRietveldRefinementResult {
245 pub histograms: Vec<JointRietveldHistogram>,
247 pub calculations: Vec<RietveldCalculation>,
249 pub metrics: JointRietveldMetrics,
251 pub parameters: ParameterSet,
253 pub free_keys: Vec<ParameterKey>,
255 pub history: Vec<JointRietveldIterationRecord>,
257 pub termination_reason: TerminationReason,
259 pub checkpoint: JointRietveldCheckpoint,
261 pub evaluations: usize,
263}
264
265pub fn refine_joint_rietveld(
272 histograms: &[JointRietveldHistogram],
273 constraints: &[Constraint],
274 options: JointRietveldRefinementOptions,
275 checkpoint: Option<&JointRietveldCheckpoint>,
276 cancellation: Option<CancellationToken>,
277) -> Result<JointRietveldRefinementResult, JointRietveldRefinementError> {
278 let mut runtime = RefinementRuntime::new(options.limits, cancellation)?;
279 refine_joint_rietveld_with_runtime(histograms, constraints, options, checkpoint, &mut runtime)
280}
281
282#[allow(clippy::too_many_lines)]
291pub fn refine_joint_rietveld_with_runtime(
292 histograms: &[JointRietveldHistogram],
293 constraints: &[Constraint],
294 options: JointRietveldRefinementOptions,
295 checkpoint: Option<&JointRietveldCheckpoint>,
296 runtime: &mut RefinementRuntime<JointRietveldCheckpoint>,
297) -> Result<JointRietveldRefinementResult, JointRietveldRefinementError> {
298 options.validate()?;
299 let initial_layout = JointRietveldLayout::new(histograms)?;
300 let initial_transform =
301 ConstraintTransform::new(initial_layout.parameters().clone(), constraints.to_vec())?;
302 validate_constraint_state(initial_layout.parameters(), &initial_transform)?;
303 let (mut live, mut history, mut damping, parameter_template) =
304 if let Some(checkpoint) = checkpoint {
305 checkpoint.validate_for(histograms, constraints)?;
306 runtime.resume_accepted(checkpoint.completed_iterations())?;
307 (
308 checkpoint.histograms.clone(),
309 checkpoint.history.clone(),
310 checkpoint.damping,
311 checkpoint.parameters.clone(),
312 )
313 } else {
314 (
315 histograms.to_vec(),
316 Vec::new(),
317 options.initial_damping,
318 initial_layout.parameters().clone(),
319 )
320 };
321 runtime.emit(
322 RefinementEventKind::Start,
323 "joint_rietveld",
324 "native joint Rietveld refinement started",
325 vec![(
326 "histograms".to_owned(),
327 DiagnosticValue::Integer(i64::try_from(histograms.len()).unwrap_or(i64::MAX)),
328 )],
329 )?;
330 let has_observations = histograms.iter().any(|histogram| {
331 histogram
332 .input
333 .pattern
334 .mask
335 .as_ref()
336 .is_none_or(|mask| mask.iter().any(|included| *included))
337 });
338 let mut termination = if has_observations {
339 TerminationReason::MaxIterations
340 } else {
341 TerminationReason::NoObservations
342 };
343 let first_iteration = history.len() + 1;
344 let last_iteration = if has_observations {
345 options.limits.max_iterations()
346 } else {
347 history.len()
348 };
349 'iterations: for iteration in first_iteration..=last_iteration {
350 if let Err(error) = runtime.begin_iteration(iteration) {
351 termination = normal_stop(error)?;
352 break;
353 }
354 let layout = JointRietveldLayout::new(&live)?;
355 let solver_parameters = parameter_template.replace_values(&layout.parameters().values())?;
356 let transform = ConstraintTransform::new(solver_parameters.clone(), constraints.to_vec())?;
357 if transform.free_keys().is_empty() {
358 termination = TerminationReason::Converged;
359 break;
360 }
361 let derivative = transform.derivative_matrix()?;
362 let objective = PreparedJointRietveldObjective::new(live.clone(), layout.clone())?;
363 if let Err(error) = reserve_products(runtime, objective.preparation_evaluation_count()) {
364 match error {
365 JointRietveldRefinementError::Runtime(RuntimeError::Stopped(stop)) => {
366 termination = stop.reason;
367 break;
368 }
369 other => return Err(other),
370 }
371 }
372 let evaluated = objective.gradient()?;
373 let current_objective = evaluated.objective;
374 let scaled_gradient = transpose_product(&derivative, &evaluated.gradient);
375 let right_hand_side = scaled_gradient
376 .iter()
377 .map(|value| -value)
378 .collect::<Vec<_>>();
379 let solve = conjugate_gradient(
380 &right_hand_side,
381 options.cg_tolerance,
382 options.max_cg_iterations,
383 |direction| {
384 reserve_products(runtime, objective.normal_product_evaluation_count())?;
385 let physical = forward_product(&derivative, direction);
386 let physical_product = objective.normal_product(&physical, 0.0)?;
387 let mut result = transpose_product(&derivative, &physical_product);
388 for (value, direction) in result.iter_mut().zip(direction) {
389 *value += damping * direction;
390 }
391 Ok(result)
392 },
393 );
394 let (mut step, cg_iterations) = match solve {
395 Ok(result) => result,
396 Err(JointRietveldRefinementError::Runtime(RuntimeError::Stopped(stop))) => {
397 termination = stop.reason;
398 break 'iterations;
399 }
400 Err(error) => return Err(error),
401 };
402 let mut step_norm = norm(&step);
403 if step_norm > options.max_scaled_parameter_step {
404 let factor = options.max_scaled_parameter_step / step_norm;
405 for value in &mut step {
406 *value *= factor;
407 }
408 step_norm = options.max_scaled_parameter_step;
409 }
410 if step_norm < options.parameter_tolerance {
411 termination = TerminationReason::Converged;
412 break;
413 }
414 let current_values = solver_parameters
415 .specs()
416 .iter()
417 .map(crate::ParameterSpec::value)
418 .collect::<Vec<_>>();
419 let packed = transform.pack()?;
420 let mut accepted = None;
421 for backtrack in 0..=options.max_backtracks {
422 let factor = 0.5_f64.powi(i32::try_from(backtrack).unwrap_or(i32::MAX));
423 let trial_free = packed
424 .iter()
425 .zip(&step)
426 .map(|(value, step)| value + factor * step)
427 .collect::<Vec<_>>();
428 let trial_map = match transform.unpack(&trial_free, true) {
429 Ok(values) => values,
430 Err(ConstraintError::ExpandedValueOutsideBounds { .. }) => {
431 emit_rejected_trial(runtime, "constraint result outside physical bounds")?;
432 if let Err(error) = runtime.reject_step() {
433 termination = normal_stop(error)?;
434 break 'iterations;
435 }
436 continue;
437 }
438 Err(error) => return Err(error.into()),
439 };
440 let trial_values = solver_parameters
441 .specs()
442 .iter()
443 .map(|spec| {
444 trial_map
445 .get(spec.key())
446 .copied()
447 .ok_or(JointRietveldRefinementError::InternalInvariant)
448 })
449 .collect::<Result<Vec<_>, _>>()?;
450 let Ok(trial) = layout.apply_values(&live, &trial_values) else {
451 emit_rejected_trial(runtime, "trial outside the numerical model domain")?;
452 if let Err(error) = runtime.reject_step() {
453 termination = normal_stop(error)?;
454 break 'iterations;
455 }
456 continue;
457 };
458 if let Err(error) = runtime.begin_evaluation() {
459 termination = normal_stop(error)?;
460 break 'iterations;
461 }
462 let (trial_calculations, trial_metrics) =
463 calculate_joint(&trial, transform.free_keys().len())?;
464 let trial_objective = 0.5 * trial_metrics.chi_square;
465 runtime.emit(
466 RefinementEventKind::Trial,
467 "joint_rietveld_step",
468 "native joint Rietveld trial evaluated",
469 vec![(
470 "objective".to_owned(),
471 DiagnosticValue::Float(trial_objective),
472 )],
473 )?;
474 if trial_objective < current_objective {
475 accepted = Some((
476 backtrack,
477 factor,
478 trial_values,
479 trial,
480 trial_calculations,
481 trial_metrics,
482 trial_objective,
483 ));
484 break;
485 }
486 if let Err(error) = runtime.reject_step() {
487 termination = normal_stop(error)?;
488 break 'iterations;
489 }
490 }
491 let Some((
492 backtracks,
493 factor,
494 trial_values,
495 trial,
496 _,
497 accepted_metrics,
498 accepted_objective,
499 )) = accepted
500 else {
501 damping *= options.damping_increase;
502 if termination == TerminationReason::MaxIterations {
503 continue;
504 }
505 break;
506 };
507 let objective_change = current_objective - accepted_objective;
508 let parameter_changes = solver_parameters
509 .specs()
510 .iter()
511 .zip(¤t_values)
512 .zip(&trial_values)
513 .filter(|((_, before), after)| before.to_bits() != after.to_bits())
514 .map(|((spec, before), after)| ParameterChange {
515 key: spec.key().clone(),
516 before: *before,
517 after: *after,
518 scaled_change: (after - before) / spec.scale(),
519 })
520 .collect();
521 let topology_changes = collect_topology_changes(&live, &trial);
522 history.push(JointRietveldIterationRecord {
523 iteration: history.len() + 1,
524 objective: accepted_objective,
525 objective_change,
526 scaled_step_norm: factor * step_norm,
527 damping,
528 cg_iterations,
529 backtracks,
530 parameter_changes,
531 topology_changes: topology_changes.clone(),
532 metrics: accepted_metrics,
533 });
534 live = trial;
535 damping = (damping * options.damping_decrease).max(1.0e-18);
536 let accepted_layout = JointRietveldLayout::new(&live)?;
537 let accepted_parameters =
538 parameter_template.replace_values(&accepted_layout.parameters().values())?;
539 let state = JointRietveldCheckpoint {
540 request: histograms.to_vec(),
541 histograms: live.clone(),
542 constraints: constraints.to_vec(),
543 parameters: accepted_parameters,
544 objective: accepted_objective,
545 damping,
546 history: history.clone(),
547 };
548 runtime.accept_step(Some(&state))?;
549 runtime.emit(
550 RefinementEventKind::StepAccepted,
551 "joint_rietveld_step",
552 "native joint Rietveld step accepted",
553 vec![
554 (
555 "objective".to_owned(),
556 DiagnosticValue::Float(accepted_objective),
557 ),
558 (
559 "topology_changes".to_owned(),
560 DiagnosticValue::Integer(
561 i64::try_from(topology_changes.len()).unwrap_or(i64::MAX),
562 ),
563 ),
564 ],
565 )?;
566 if history.len() >= options.min_iterations
567 && objective_change <= options.objective_tolerance * accepted_objective.max(1.0)
568 {
569 termination = TerminationReason::Converged;
570 break;
571 }
572 }
573 let final_layout = JointRietveldLayout::new(&live)?;
574 let final_parameters =
575 parameter_template.replace_values(&final_layout.parameters().values())?;
576 let final_transform = ConstraintTransform::new(final_parameters.clone(), constraints.to_vec())?;
577 runtime.begin_evaluation().or_else(|error| match error {
578 RuntimeError::Stopped(_) => Ok(()),
579 other => Err(other),
580 })?;
581 let (calculations, metrics) = calculate_joint(&live, final_transform.free_keys().len())?;
582 let objective = 0.5 * metrics.chi_square;
583 let checkpoint = JointRietveldCheckpoint {
584 request: histograms.to_vec(),
585 histograms: live.clone(),
586 constraints: constraints.to_vec(),
587 parameters: final_parameters.clone(),
588 objective,
589 damping,
590 history: history.clone(),
591 };
592 checkpoint.validate_for(histograms, constraints)?;
593 runtime.emit(
594 RefinementEventKind::Termination,
595 "joint_rietveld",
596 "native joint Rietveld refinement terminated",
597 vec![(
598 "reason".to_owned(),
599 DiagnosticValue::String(termination.as_str().to_owned()),
600 )],
601 )?;
602 Ok(JointRietveldRefinementResult {
603 histograms: live,
604 calculations,
605 metrics,
606 parameters: final_parameters,
607 free_keys: final_transform.free_keys().to_vec(),
608 history,
609 termination_reason: termination,
610 checkpoint,
611 evaluations: runtime.evaluations(),
612 })
613}
614
615fn calculate_joint(
616 histograms: &[JointRietveldHistogram],
617 parameter_count: usize,
618) -> Result<(Vec<RietveldCalculation>, JointRietveldMetrics), JointRietveldRefinementError> {
619 let calculations = histograms
620 .iter()
621 .map(|histogram| calculate_rietveld_pattern(&histogram.input, &histogram.calculation))
622 .collect::<Result<Vec<_>, _>>()?;
623 let metrics = joint_metrics(histograms, &calculations, parameter_count)?;
624 Ok((calculations, metrics))
625}
626
627fn joint_metrics(
628 histograms: &[JointRietveldHistogram],
629 calculations: &[RietveldCalculation],
630 parameter_count: usize,
631) -> Result<JointRietveldMetrics, JointRietveldRefinementError> {
632 let mut included_samples = 0_usize;
633 let mut absolute_residual_sum = 0.0;
634 let mut absolute_observed_sum = 0.0;
635 let mut weighted_observed_square_sum = 0.0;
636 let mut chi_square = 0.0;
637 for (histogram, calculation) in histograms.iter().zip(calculations) {
638 let observed = histogram
639 .input
640 .pattern
641 .observed_y
642 .as_ref()
643 .ok_or(crate::RietveldError::MissingObservations)?;
644 let uncertainty = histogram
645 .calculation
646 .use_uncertainty
647 .then_some(histogram.input.pattern.uncertainty.as_deref())
648 .flatten();
649 for index in 0..histogram.input.pattern.sample_count() {
650 if histogram
651 .input
652 .pattern
653 .mask
654 .as_ref()
655 .is_some_and(|mask| !mask[index])
656 {
657 continue;
658 }
659 included_samples += 1;
660 let residual = calculation.y[index] - observed[index];
661 absolute_residual_sum += residual.abs();
662 absolute_observed_sum += observed[index].abs();
663 let (weighted_residual, weighted_observed) = uncertainty.map_or_else(
664 || (residual, observed[index]),
665 |sigma| (residual / sigma[index], observed[index] / sigma[index]),
666 );
667 chi_square += weighted_residual * weighted_residual;
668 weighted_observed_square_sum += weighted_observed * weighted_observed;
669 }
670 }
671 let degrees_of_freedom = included_samples.checked_sub(parameter_count);
672 Ok(JointRietveldMetrics {
673 included_samples,
674 rp: if absolute_observed_sum == 0.0 {
675 f64::INFINITY
676 } else {
677 absolute_residual_sum / absolute_observed_sum
678 },
679 rwp: if weighted_observed_square_sum == 0.0 {
680 f64::INFINITY
681 } else {
682 (chi_square / weighted_observed_square_sum).sqrt()
683 },
684 chi_square,
685 reduced_chi_square: match degrees_of_freedom {
686 Some(degrees) if degrees > 0 => chi_square / count_as_f64(degrees),
687 _ => f64::INFINITY,
688 },
689 })
690}
691
692#[allow(clippy::cast_precision_loss)]
693fn count_as_f64(value: usize) -> f64 {
694 value as f64
695}
696
697fn collect_topology_changes(
698 before: &[JointRietveldHistogram],
699 after: &[JointRietveldHistogram],
700) -> Vec<JointRietveldTopologyChange> {
701 before
702 .iter()
703 .zip(after)
704 .flat_map(|(before, after)| {
705 before
706 .input
707 .phases
708 .iter()
709 .zip(&after.input.phases)
710 .filter_map(|(left, right)| topology_change(left, right))
711 .map(|change| JointRietveldTopologyChange {
712 histogram_id: after.histogram_id.clone(),
713 change,
714 })
715 })
716 .collect()
717}
718
719fn validate_constraint_state(
720 parameters: &ParameterSet,
721 transform: &ConstraintTransform,
722) -> Result<(), JointRietveldRefinementError> {
723 let constrained = transform.unpack(&transform.pack()?, false)?;
724 for spec in parameters.specs() {
725 let value = constrained
726 .get(spec.key())
727 .copied()
728 .ok_or(JointRietveldRefinementError::InternalInvariant)?;
729 if (value - spec.value()).abs() > 2.0e-12 {
730 return Err(JointRietveldRefinementError::UnsatisfiedConstraint {
731 key: spec.key().clone(),
732 });
733 }
734 }
735 Ok(())
736}
737
738fn parameter_values_match(domain: &ParameterSet, stored: &ParameterSet) -> bool {
739 domain.specs().len() == stored.specs().len()
740 && domain
741 .specs()
742 .iter()
743 .zip(stored.specs())
744 .all(|(domain, stored)| {
745 domain.key() == stored.key() && domain.value().to_bits() == stored.value().to_bits()
746 })
747}
748
749fn parameter_contract_matches(domain: &ParameterSet, stored: &ParameterSet) -> bool {
750 domain.specs().len() == stored.specs().len()
751 && domain
752 .specs()
753 .iter()
754 .zip(stored.specs())
755 .all(|(domain, stored)| {
756 domain.key() == stored.key()
757 && domain.unit() == stored.unit()
758 && domain.bounds() == stored.bounds()
759 && domain.refine() == stored.refine()
760 && domain.scale().to_bits() == stored.scale().to_bits()
761 })
762}
763
764fn valid_history(history: &[JointRietveldIterationRecord]) -> bool {
765 history.iter().enumerate().all(|(index, row)| {
766 row.iteration == index + 1
767 && row.objective.is_finite()
768 && row.objective >= 0.0
769 && row.objective_change.is_finite()
770 && row.objective_change >= 0.0
771 && row.scaled_step_norm.is_finite()
772 && row.scaled_step_norm >= 0.0
773 && row.damping.is_finite()
774 && row.damping > 0.0
775 && row.metrics.chi_square.is_finite()
776 && row.metrics.chi_square >= 0.0
777 && !row.metrics.rp.is_nan()
778 && row.metrics.rp >= 0.0
779 && !row.metrics.rwp.is_nan()
780 && row.metrics.rwp >= 0.0
781 && !row.metrics.reduced_chi_square.is_nan()
782 && row.metrics.reduced_chi_square >= 0.0
783 && row.parameter_changes.iter().all(|change| {
784 change.before.is_finite()
785 && change.after.is_finite()
786 && change.scaled_change.is_finite()
787 })
788 })
789}
790
791fn forward_product(derivative: &ConstraintDerivativeMatrix, free: &[f64]) -> Vec<f64> {
792 debug_assert_eq!(free.len(), derivative.columns);
793 derivative
794 .values
795 .chunks_exact(derivative.columns)
796 .map(|row| row.iter().zip(free).map(|(left, right)| left * right).sum())
797 .collect()
798}
799
800fn transpose_product(derivative: &ConstraintDerivativeMatrix, physical: &[f64]) -> Vec<f64> {
801 debug_assert_eq!(physical.len(), derivative.rows);
802 let mut result = vec![0.0; derivative.columns];
803 for (coefficient, row) in physical
804 .iter()
805 .zip(derivative.values.chunks_exact(derivative.columns))
806 {
807 for (target, value) in result.iter_mut().zip(row) {
808 *target += coefficient * value;
809 }
810 }
811 result
812}
813
814fn reserve_products(
815 runtime: &mut RefinementRuntime<JointRietveldCheckpoint>,
816 count: usize,
817) -> Result<(), JointRietveldRefinementError> {
818 for _ in 0..count {
819 runtime.begin_evaluation()?;
820 }
821 Ok(())
822}
823
824fn conjugate_gradient(
825 right_hand_side: &[f64],
826 tolerance: f64,
827 max_iterations: usize,
828 operator: impl FnMut(&[f64]) -> Result<Vec<f64>, JointRietveldRefinementError>,
829) -> Result<(Vec<f64>, usize), JointRietveldRefinementError> {
830 conjugate_gradient_core(right_hand_side, tolerance, max_iterations, operator).map_err(|error| {
831 match error {
832 ConjugateGradientError::Operator(error) => error,
833 ConjugateGradientError::NonPositiveOperator
834 | ConjugateGradientError::NonFiniteState => {
835 JointRietveldRefinementError::NumericalBreakdown
836 }
837 }
838 })
839}
840
841fn emit_rejected_trial(
842 runtime: &mut RefinementRuntime<JointRietveldCheckpoint>,
843 reason: &str,
844) -> Result<(), JointRietveldRefinementError> {
845 runtime.emit(
846 RefinementEventKind::StepRejected,
847 "joint_rietveld_step",
848 "native joint Rietveld trial rejected",
849 vec![(
850 "reason".to_owned(),
851 DiagnosticValue::String(reason.to_owned()),
852 )],
853 )?;
854 Ok(())
855}
856
857fn normal_stop(error: RuntimeError) -> Result<TerminationReason, JointRietveldRefinementError> {
858 match error {
859 RuntimeError::Stopped(stop) => Ok(stop.reason),
860 other => Err(other.into()),
861 }
862}
863
864#[derive(Debug)]
866pub enum JointRietveldRefinementError {
867 InvalidOptions,
869 InvalidCheckpoint {
871 reason: &'static str,
873 },
874 UnsatisfiedConstraint {
876 key: ParameterKey,
878 },
879 InternalInvariant,
881 NumericalBreakdown,
883 Joint(JointRietveldError),
885 Parameter(ParameterError),
887 Constraint(ConstraintError),
889 Rietveld(crate::RietveldError),
891 Runtime(RuntimeError),
893}
894
895impl Display for JointRietveldRefinementError {
896 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
897 match self {
898 Self::InvalidOptions => {
899 formatter.write_str("native joint Rietveld solver options are invalid")
900 }
901 Self::InvalidCheckpoint { reason } => {
902 write!(
903 formatter,
904 "native joint Rietveld checkpoint is invalid: {reason}"
905 )
906 }
907 Self::UnsatisfiedConstraint { key } => {
908 write!(
909 formatter,
910 "joint Rietveld parameter {key} does not satisfy its constraint"
911 )
912 }
913 Self::InternalInvariant => {
914 formatter.write_str("native joint Rietveld parameter invariant failed")
915 }
916 Self::NumericalBreakdown => {
917 formatter.write_str("native joint Rietveld linear solve broke down")
918 }
919 Self::Joint(error) => Display::fmt(error, formatter),
920 Self::Parameter(error) => Display::fmt(error, formatter),
921 Self::Constraint(error) => Display::fmt(error, formatter),
922 Self::Rietveld(error) => Display::fmt(error, formatter),
923 Self::Runtime(error) => Display::fmt(error, formatter),
924 }
925 }
926}
927
928impl Error for JointRietveldRefinementError {
929 fn source(&self) -> Option<&(dyn Error + 'static)> {
930 match self {
931 Self::Joint(error) => Some(error),
932 Self::Parameter(error) => Some(error),
933 Self::Constraint(error) => Some(error),
934 Self::Rietveld(error) => Some(error),
935 Self::Runtime(error) => Some(error),
936 Self::InvalidOptions
937 | Self::InvalidCheckpoint { .. }
938 | Self::UnsatisfiedConstraint { .. }
939 | Self::InternalInvariant
940 | Self::NumericalBreakdown => None,
941 }
942 }
943}
944
945impl From<JointRietveldError> for JointRietveldRefinementError {
946 fn from(value: JointRietveldError) -> Self {
947 Self::Joint(value)
948 }
949}
950
951impl From<ParameterError> for JointRietveldRefinementError {
952 fn from(value: ParameterError) -> Self {
953 Self::Parameter(value)
954 }
955}
956
957impl From<ConstraintError> for JointRietveldRefinementError {
958 fn from(value: ConstraintError) -> Self {
959 Self::Constraint(value)
960 }
961}
962
963impl From<crate::RietveldError> for JointRietveldRefinementError {
964 fn from(value: crate::RietveldError) -> Self {
965 Self::Rietveld(value)
966 }
967}
968
969impl From<RuntimeError> for JointRietveldRefinementError {
970 fn from(value: RuntimeError) -> Self {
971 Self::Runtime(value)
972 }
973}