1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use nalgebra::{DMatrix, DVector};
7use phasesmith_core::{TofInstrument, TofInstrumentParameter, TofProfileParameters};
8use phasesmith_crystallography::UnitCell;
9use phasesmith_model::RecordId;
10
11use crate::tof_lebail::{initialize_intensities, normal_tof_stop};
12use crate::tof_multibank::{
13 AcceptedBankState, MultiBankCycleCandidate, MultiBankCycleOutcome, aggregate_metrics,
14 calculate_states, evaluate_bank_metrics, fitted_parameter_count, prepare_multibank_cycle,
15 reflection_intensities,
16};
17use crate::tof_multibank_instrument::{
18 PackedInstruments, instrument_system, live_input, pack_instruments, unpack_instruments,
19};
20use crate::tof_multibank_lattice::{
21 PackedLattice, apply_cells, lattice_changes, lattice_system, pack_lattice, unpack_lattice,
22};
23use crate::{
24 DiagnosticValue, RefinementEventKind, RefinementLimits, RefinementRuntime, ResidualEvaluation,
25 RuntimeError, TerminationReason, TofBankInstrumentModel, TofBankInstrumentState,
26 TofChebyshevBackground, TofInstrumentParameterChange, TofLatticeParameterChange,
27 TofLeBailError, TofLeBailOptions, TofLeBailPhase, TofMultiBankError,
28 TofMultiBankInstrumentError, TofMultiBankInstrumentInput, TofMultiBankLatticeError,
29 TofMultiBankLatticeInput, TofMultiBankMetrics, TofMultiBankResultBank, TofSharedLatticeState,
30 tof_lattice_geometry,
31};
32
33#[derive(Clone, Debug, PartialEq)]
35pub struct TofMultiBankGeometryInput {
36 pub lattice: TofMultiBankLatticeInput,
38 pub instrument_models: Vec<TofBankInstrumentModel>,
40}
41
42impl TofMultiBankGeometryInput {
43 pub fn validate(&self) -> Result<(), TofMultiBankGeometryError> {
50 self.lattice.validate()?;
51 self.instrument_input().validate()?;
52 Ok(())
53 }
54
55 fn instrument_input(&self) -> TofMultiBankInstrumentInput {
56 TofMultiBankInstrumentInput {
57 multibank: self.lattice.multibank.clone(),
58 instrument_models: self.instrument_models.clone(),
59 }
60 }
61
62 fn parameter_count(&self) -> Result<usize, TofMultiBankGeometryError> {
63 self.lattice
64 .parameter_count()?
65 .checked_add(self.instrument_input().parameter_count()?)
66 .ok_or(TofMultiBankGeometryError::AllocationOverflow)
67 }
68}
69
70#[derive(Clone, Debug, PartialEq)]
72pub struct TofMultiBankGeometryOptions {
73 pub lebail: TofLeBailOptions,
75 pub geometry_damping: f64,
77 pub max_scaled_geometry_step: f64,
79 pub max_geometry_backtracks: usize,
81 pub unresolved_correlation: f64,
83}
84
85impl TofMultiBankGeometryOptions {
86 pub fn new(
92 lebail: TofLeBailOptions,
93 geometry_damping: f64,
94 max_scaled_geometry_step: f64,
95 max_geometry_backtracks: usize,
96 unresolved_correlation: f64,
97 ) -> Result<Self, TofMultiBankGeometryError> {
98 let result = Self {
99 lebail,
100 geometry_damping,
101 max_scaled_geometry_step,
102 max_geometry_backtracks,
103 unresolved_correlation,
104 };
105 result.validate()?;
106 Ok(result)
107 }
108
109 pub fn validate(&self) -> Result<(), TofMultiBankGeometryError> {
115 self.lebail.validate()?;
116 if !self.geometry_damping.is_finite()
117 || self.geometry_damping < 0.0
118 || !self.max_scaled_geometry_step.is_finite()
119 || self.max_scaled_geometry_step <= 0.0
120 || !self.unresolved_correlation.is_finite()
121 || !(0.0..=1.0).contains(&self.unresolved_correlation)
122 {
123 return Err(TofMultiBankGeometryError::InvalidOptions);
124 }
125 Ok(())
126 }
127}
128
129#[derive(Clone, Debug, PartialEq, Eq)]
131pub enum TofGeometryParameterKey {
132 Lattice {
134 phase_id: RecordId,
136 parameter_name: String,
138 },
139 Instrument {
141 bank_id: RecordId,
143 parameter: TofInstrumentParameter,
145 },
146}
147
148#[derive(Clone, Debug, PartialEq)]
150pub struct TofGeometryCorrelation {
151 pub left: TofGeometryParameterKey,
153 pub right: TofGeometryParameterKey,
155 pub correlation: f64,
157}
158
159#[derive(Clone, Debug, PartialEq)]
161pub struct TofGeometryDiagnostics {
162 pub parameter_count: usize,
164 pub jacobian_rank: usize,
166 pub maximum_absolute_correlation: Option<f64>,
168 pub unresolved_correlations: Vec<TofGeometryCorrelation>,
170}
171
172#[derive(Clone, Debug, PartialEq)]
174pub struct TofMultiBankGeometryIterationRecord {
175 pub iteration: usize,
177 pub bank_metrics: Vec<ResidualEvaluation>,
179 pub metrics: TofMultiBankMetrics,
181 pub maximum_relative_intensity_change: f64,
183 pub maximum_absolute_background_change: f64,
185 pub scaled_geometry_step_norm: f64,
187 pub lattice_parameter_changes: Vec<TofLatticeParameterChange>,
189 pub instrument_parameter_changes: Vec<TofInstrumentParameterChange>,
191}
192
193#[derive(Clone, Debug, PartialEq)]
195pub struct TofMultiBankGeometryCheckpointBank {
196 pub bank_id: RecordId,
198 pub instrument: TofInstrument,
200 pub phases: Vec<TofLeBailPhase>,
202 pub background: Option<TofChebyshevBackground>,
204}
205
206#[derive(Clone, Debug, PartialEq)]
208pub struct TofMultiBankGeometryCheckpoint {
209 pub completed_iterations: usize,
211 pub banks: Vec<TofMultiBankGeometryCheckpointBank>,
213 pub lattice_phases: Vec<TofSharedLatticeState>,
215 pub history: Vec<TofMultiBankGeometryIterationRecord>,
217}
218
219#[derive(Clone, Debug, PartialEq)]
221pub struct TofMultiBankGeometryResult {
222 pub banks: Vec<TofMultiBankResultBank>,
224 pub lattice_phases: Vec<TofSharedLatticeState>,
226 pub instruments: Vec<TofBankInstrumentState>,
228 pub metrics: TofMultiBankMetrics,
230 pub diagnostics: TofGeometryDiagnostics,
232 pub history: Vec<TofMultiBankGeometryIterationRecord>,
234 pub termination_reason: TerminationReason,
236 pub checkpoint: TofMultiBankGeometryCheckpoint,
238}
239
240pub fn refine_tof_multibank_geometry(
247 input: &TofMultiBankGeometryInput,
248 options: &TofMultiBankGeometryOptions,
249) -> Result<TofMultiBankGeometryResult, TofMultiBankGeometryError> {
250 input.validate()?;
251 options.validate()?;
252 let evaluations_per_cycle = options
253 .max_geometry_backtracks
254 .checked_add(4)
255 .ok_or(TofMultiBankGeometryError::AllocationOverflow)?;
256 let max_evaluations = options
257 .lebail
258 .cycles
259 .checked_mul(evaluations_per_cycle)
260 .ok_or(TofMultiBankGeometryError::AllocationOverflow)?;
261 let limits = RefinementLimits::new(options.lebail.cycles, max_evaluations, None, 1)?;
262 let mut runtime = RefinementRuntime::new(limits, None)?;
263 refine_tof_multibank_geometry_with_runtime(input, options, None, &mut runtime)
264}
265
266#[allow(clippy::too_many_lines)]
276pub fn refine_tof_multibank_geometry_with_runtime(
277 input: &TofMultiBankGeometryInput,
278 options: &TofMultiBankGeometryOptions,
279 checkpoint: Option<&TofMultiBankGeometryCheckpoint>,
280 runtime: &mut RefinementRuntime<TofMultiBankGeometryCheckpoint>,
281) -> Result<TofMultiBankGeometryResult, TofMultiBankGeometryError> {
282 input.validate()?;
283 options.validate()?;
284 let parameter_count = input.parameter_count()?;
285 let restored = restore_state(input, options, checkpoint)?;
286 let mut states = restored.states;
287 let mut cells = restored.cells;
288 let mut instruments = restored.instruments;
289 let mut history = restored.history;
290 if let Some(checkpoint) = checkpoint {
291 runtime.resume_accepted(checkpoint.completed_iterations)?;
292 }
293 runtime.emit(
294 RefinementEventKind::Start,
295 "tof_multibank_geometry",
296 "joint shared-cell and bank-local TOF instrument refinement started",
297 vec![(
298 "geometry_parameter_count".to_owned(),
299 DiagnosticValue::Unsigned(parameter_count as u64),
300 )],
301 )?;
302 let mut accepted_calculations = None;
303 let mut termination = TerminationReason::MaxIterations;
304 for iteration in restored.first_iteration..=options.lebail.cycles {
305 if let Err(error) = runtime.begin_iteration(iteration) {
306 termination = normal_tof_stop(error)?;
307 break;
308 }
309 let instrument_input = input.instrument_input();
310 let live = live_input(&instrument_input, &instruments)?;
311 let candidate = match prepare_multibank_cycle(
312 &live,
313 &states,
314 &options.lebail,
315 parameter_count,
316 runtime,
317 )? {
318 MultiBankCycleOutcome::Candidate(candidate) => candidate,
319 MultiBankCycleOutcome::Stopped(reason) => {
320 termination = reason;
321 break;
322 }
323 };
324 let updated =
325 match geometry_update(input, options, &cells, &instruments, candidate, runtime)? {
326 GeometryUpdateOutcome::Candidate(candidate) => candidate,
327 GeometryUpdateOutcome::Stopped(reason) => {
328 termination = reason;
329 break;
330 }
331 };
332 states = updated.cycle.states;
333 cells = updated.cells;
334 instruments = updated.instruments;
335 accepted_calculations = Some(updated.cycle.calculations.clone());
336 history.push(TofMultiBankGeometryIterationRecord {
337 iteration,
338 bank_metrics: updated.cycle.bank_metrics.clone(),
339 metrics: updated.cycle.metrics,
340 maximum_relative_intensity_change: updated.cycle.maximum_relative_intensity_change,
341 maximum_absolute_background_change: updated.cycle.maximum_absolute_background_change,
342 scaled_geometry_step_norm: updated.step_norm,
343 lattice_parameter_changes: updated.lattice_changes,
344 instrument_parameter_changes: updated.instrument_changes,
345 });
346 let accepted = checkpoint_from_state(input, &states, &cells, &instruments, &history);
347 runtime.accept_step(Some(&accepted))?;
348 runtime.emit(
349 RefinementEventKind::Iteration,
350 "tof_multibank_geometry_iteration",
351 "joint multi-bank TOF geometry cycle accepted",
352 vec![
353 (
354 "rwp".to_owned(),
355 DiagnosticValue::Float(updated.cycle.metrics.rwp),
356 ),
357 (
358 "scaled_geometry_step_norm".to_owned(),
359 DiagnosticValue::Float(updated.step_norm),
360 ),
361 ],
362 )?;
363 }
364 let instrument_input = input.instrument_input();
365 let live = live_input(&instrument_input, &instruments)?;
366 let calculations = match accepted_calculations {
367 Some(calculations) => calculations,
368 None => calculate_states(&live, &states, &options.lebail)?,
369 };
370 let bank_metrics = evaluate_bank_metrics(&live, &states, &calculations, &options.lebail)?;
371 let fitted = fitted_parameter_count(&states)?
372 .checked_add(parameter_count)
373 .ok_or(TofMultiBankGeometryError::AllocationOverflow)?;
374 let metrics = aggregate_metrics(&live, &calculations, &options.lebail, fitted)?;
375 let packed = pack_geometry(input, &cells, &instruments)?;
376 let final_cycle = cycle_view(
377 states.clone(),
378 calculations.clone(),
379 bank_metrics.clone(),
380 metrics,
381 );
382 let (jacobian, _) = geometry_system(input, &live, &cells, &final_cycle, &packed, options)?;
383 let diagnostics = geometry_diagnostics(input, &jacobian, options.unresolved_correlation)?;
384 let checkpoint = checkpoint_from_state(input, &states, &cells, &instruments, &history);
385 checkpoint.validate_for(input, options)?;
386 let banks = live
387 .banks
388 .iter()
389 .zip(states)
390 .zip(calculations)
391 .zip(bank_metrics)
392 .map(
393 |(((bank, state), calculation), metrics)| TofMultiBankResultBank {
394 bank_id: bank.bank_id.clone(),
395 calculation,
396 metrics,
397 intensities: reflection_intensities(&state.phases),
398 phases: state.phases,
399 background: state.background,
400 },
401 )
402 .collect();
403 let lattice_phases = lattice_states(input, &cells);
404 let instruments = instrument_states(input, &instruments);
405 runtime.emit(
406 RefinementEventKind::Termination,
407 "tof_multibank_geometry",
408 "joint multi-bank TOF geometry refinement terminated",
409 vec![
410 (
411 "termination_reason".to_owned(),
412 DiagnosticValue::String(termination.as_str().to_owned()),
413 ),
414 (
415 "jacobian_rank".to_owned(),
416 DiagnosticValue::Unsigned(diagnostics.jacobian_rank as u64),
417 ),
418 ],
419 )?;
420 Ok(TofMultiBankGeometryResult {
421 banks,
422 lattice_phases,
423 instruments,
424 metrics,
425 diagnostics,
426 history,
427 termination_reason: termination,
428 checkpoint,
429 })
430}
431
432fn cycle_view(
433 states: Vec<AcceptedBankState>,
434 calculations: Vec<crate::TofLeBailCalculation>,
435 bank_metrics: Vec<ResidualEvaluation>,
436 metrics: TofMultiBankMetrics,
437) -> MultiBankCycleCandidate {
438 MultiBankCycleCandidate {
439 states,
440 calculations,
441 bank_metrics,
442 metrics,
443 maximum_relative_intensity_change: 0.0,
444 maximum_absolute_background_change: 0.0,
445 }
446}
447
448struct RestoredState {
449 states: Vec<AcceptedBankState>,
450 cells: Vec<UnitCell>,
451 instruments: Vec<TofInstrument>,
452 history: Vec<TofMultiBankGeometryIterationRecord>,
453 first_iteration: usize,
454}
455
456fn restore_state(
457 input: &TofMultiBankGeometryInput,
458 options: &TofMultiBankGeometryOptions,
459 checkpoint: Option<&TofMultiBankGeometryCheckpoint>,
460) -> Result<RestoredState, TofMultiBankGeometryError> {
461 let Some(checkpoint) = checkpoint else {
462 let states = input
463 .lattice
464 .multibank
465 .banks
466 .iter()
467 .map(|bank| {
468 Ok(AcceptedBankState {
469 phases: initialize_intensities(&bank.input, &options.lebail)?,
470 background: bank.input.background.clone(),
471 })
472 })
473 .collect::<Result<Vec<_>, TofMultiBankGeometryError>>()?;
474 let cells = input
475 .lattice
476 .lattice_phases
477 .iter()
478 .map(crate::TofSharedLatticePhase::initial_cell)
479 .collect::<Vec<_>>();
480 return Ok(RestoredState {
481 states: apply_cells(&input.lattice, &states, &cells)?,
482 cells,
483 instruments: input
484 .lattice
485 .multibank
486 .banks
487 .iter()
488 .map(|bank| bank.input.instrument)
489 .collect(),
490 history: Vec::new(),
491 first_iteration: 1,
492 });
493 };
494 checkpoint.validate_for(input, options)?;
495 Ok(RestoredState {
496 states: checkpoint
497 .banks
498 .iter()
499 .map(|bank| AcceptedBankState {
500 phases: bank.phases.clone(),
501 background: bank.background.clone(),
502 })
503 .collect(),
504 cells: checkpoint
505 .lattice_phases
506 .iter()
507 .map(|phase| phase.cell)
508 .collect(),
509 instruments: checkpoint
510 .banks
511 .iter()
512 .map(|bank| bank.instrument)
513 .collect(),
514 history: checkpoint.history.clone(),
515 first_iteration: checkpoint
516 .completed_iterations
517 .checked_add(1)
518 .ok_or(TofMultiBankGeometryError::AllocationOverflow)?,
519 })
520}
521
522struct PackedGeometry {
523 lattice: PackedLattice,
524 instrument: PackedInstruments,
525 values: Vec<f64>,
526 scales: Vec<f64>,
527 lower: Vec<f64>,
528 upper: Vec<f64>,
529}
530
531fn pack_geometry(
532 input: &TofMultiBankGeometryInput,
533 cells: &[UnitCell],
534 instruments: &[TofInstrument],
535) -> Result<PackedGeometry, TofMultiBankGeometryError> {
536 let lattice = pack_lattice(&input.lattice, cells)?;
537 let instrument_input = input.instrument_input();
538 let instrument = pack_instruments(&instrument_input, instruments)?;
539 let mut values = lattice.values.clone();
540 values.extend_from_slice(&instrument.values);
541 let mut scales = lattice.scales.clone();
542 scales.extend_from_slice(&instrument.scales);
543 let mut lower = lattice.lower.clone();
544 lower.extend_from_slice(&instrument.lower);
545 let mut upper = lattice.upper.clone();
546 upper.extend_from_slice(&instrument.upper);
547 if values.len() != input.parameter_count()? {
548 return Err(TofMultiBankGeometryError::InternalInvariant);
549 }
550 Ok(PackedGeometry {
551 lattice,
552 instrument,
553 values,
554 scales,
555 lower,
556 upper,
557 })
558}
559
560struct GeometryUpdate {
561 cycle: MultiBankCycleCandidate,
562 cells: Vec<UnitCell>,
563 instruments: Vec<TofInstrument>,
564 step_norm: f64,
565 lattice_changes: Vec<TofLatticeParameterChange>,
566 instrument_changes: Vec<TofInstrumentParameterChange>,
567}
568
569enum GeometryUpdateOutcome {
570 Candidate(Box<GeometryUpdate>),
571 Stopped(TerminationReason),
572}
573
574#[allow(clippy::too_many_lines)]
575fn geometry_update<C>(
576 input: &TofMultiBankGeometryInput,
577 options: &TofMultiBankGeometryOptions,
578 cells: &[UnitCell],
579 instruments: &[TofInstrument],
580 mut cycle: MultiBankCycleCandidate,
581 runtime: &mut RefinementRuntime<C>,
582) -> Result<GeometryUpdateOutcome, TofMultiBankGeometryError> {
583 let packed = pack_geometry(input, cells, instruments)?;
584 let instrument_input = input.instrument_input();
585 let live = live_input(&instrument_input, instruments)?;
586 let (jacobian, residual) = geometry_system(input, &live, cells, &cycle, &packed, options)?;
587 let normal = jacobian.transpose() * &jacobian;
588 let rhs = jacobian.transpose() * &residual;
589 let mut regularized = normal;
590 for parameter in 0..regularized.nrows() {
591 regularized[(parameter, parameter)] += options.geometry_damping;
592 }
593 let mut step = if let Some(solution) = regularized.lu().solve(&rhs) {
594 solution
595 } else {
596 jacobian
597 .svd(true, true)
598 .solve(&residual, f64::EPSILON)
599 .map_err(|_| TofMultiBankGeometryError::LinearSolve)?
600 };
601 if step.iter().any(|value| !value.is_finite()) {
602 return Err(TofMultiBankGeometryError::LinearSolve);
603 }
604 for parameter in 0..step.len() {
605 let current = packed.values[parameter] / packed.scales[parameter];
606 let lower = (packed.lower[parameter] / packed.scales[parameter] - current)
607 .max(-options.max_scaled_geometry_step);
608 let upper = (packed.upper[parameter] / packed.scales[parameter] - current)
609 .min(options.max_scaled_geometry_step);
610 step[parameter] = step[parameter].clamp(lower, upper);
611 }
612 if step.norm() == 0.0 {
613 return Ok(GeometryUpdateOutcome::Candidate(Box::new(GeometryUpdate {
614 cycle,
615 cells: cells.to_vec(),
616 instruments: instruments.to_vec(),
617 step_norm: 0.0,
618 lattice_changes: Vec::new(),
619 instrument_changes: Vec::new(),
620 })));
621 }
622 let parameter_count = input.parameter_count()?;
623 let local_parameter_count = fitted_parameter_count(&cycle.states)?;
624 let total_parameter_count = local_parameter_count
625 .checked_add(parameter_count)
626 .ok_or(TofMultiBankGeometryError::AllocationOverflow)?;
627 let lattice_count = packed.lattice.values.len();
628 let mut factor = 1.0;
629 for _ in 0..=options.max_geometry_backtracks {
630 let trial_values = packed
631 .values
632 .iter()
633 .zip(&packed.scales)
634 .zip(step.iter())
635 .map(|((value, scale), step)| value + factor * scale * step)
636 .collect::<Vec<_>>();
637 let trial_cells = match unpack_lattice(&input.lattice, &trial_values[..lattice_count]) {
638 Ok(cells) => cells,
639 Err(error) if recoverable_lattice_error(&error) => {
640 factor *= 0.5;
641 continue;
642 }
643 Err(error) => return Err(error.into()),
644 };
645 let trial_instruments = match unpack_instruments(
646 &instrument_input,
647 instruments,
648 &trial_values[lattice_count..],
649 ) {
650 Ok(instruments) => instruments,
651 Err(error) if recoverable_instrument_error(&error) => {
652 factor *= 0.5;
653 continue;
654 }
655 Err(error) => return Err(error.into()),
656 };
657 let trial_live = match live_input(&instrument_input, &trial_instruments) {
658 Ok(input) => input,
659 Err(error) if recoverable_instrument_error(&error) => {
660 factor *= 0.5;
661 continue;
662 }
663 Err(error) => return Err(error.into()),
664 };
665 let trial_lattice = TofMultiBankLatticeInput {
666 multibank: trial_live.clone(),
667 lattice_phases: input.lattice.lattice_phases.clone(),
668 };
669 let trial_states = match apply_cells(&trial_lattice, &cycle.states, &trial_cells) {
670 Ok(states) => states,
671 Err(error) if recoverable_lattice_error(&error) => {
672 factor *= 0.5;
673 continue;
674 }
675 Err(error) => return Err(error.into()),
676 };
677 if let Err(error) = runtime.begin_evaluation() {
678 return Ok(GeometryUpdateOutcome::Stopped(normal_tof_stop(error)?));
679 }
680 let trial_calculations = match calculate_states(&trial_live, &trial_states, &options.lebail)
681 {
682 Ok(calculations) => calculations,
683 Err(error) if recoverable_multibank_error(&error) => {
684 factor *= 0.5;
685 continue;
686 }
687 Err(error) => return Err(error.into()),
688 };
689 let trial_metrics = aggregate_metrics(
690 &trial_live,
691 &trial_calculations,
692 &options.lebail,
693 total_parameter_count,
694 )?;
695 if trial_metrics.chi_square < cycle.metrics.chi_square {
696 let trial_bank_metrics = evaluate_bank_metrics(
697 &trial_live,
698 &trial_states,
699 &trial_calculations,
700 &options.lebail,
701 )?;
702 let lattice_parameter_changes = lattice_changes(
703 &input.lattice,
704 &packed.lattice.values,
705 &trial_values[..lattice_count],
706 &packed.lattice.scales,
707 )?;
708 let instrument_parameter_changes = geometry_instrument_changes(
709 input,
710 &packed.instrument,
711 &trial_values[lattice_count..],
712 )?;
713 cycle.states = trial_states;
714 cycle.calculations = trial_calculations;
715 cycle.bank_metrics = trial_bank_metrics;
716 cycle.metrics = trial_metrics;
717 return Ok(GeometryUpdateOutcome::Candidate(Box::new(GeometryUpdate {
718 cycle,
719 cells: trial_cells,
720 instruments: trial_instruments,
721 step_norm: factor * step.norm(),
722 lattice_changes: lattice_parameter_changes,
723 instrument_changes: instrument_parameter_changes,
724 })));
725 }
726 factor *= 0.5;
727 }
728 Ok(GeometryUpdateOutcome::Candidate(Box::new(GeometryUpdate {
729 cycle,
730 cells: cells.to_vec(),
731 instruments: instruments.to_vec(),
732 step_norm: 0.0,
733 lattice_changes: Vec::new(),
734 instrument_changes: Vec::new(),
735 })))
736}
737
738fn geometry_system(
739 input: &TofMultiBankGeometryInput,
740 live: &crate::TofMultiBankInput,
741 cells: &[UnitCell],
742 cycle: &MultiBankCycleCandidate,
743 packed: &PackedGeometry,
744 options: &TofMultiBankGeometryOptions,
745) -> Result<(DMatrix<f64>, DVector<f64>), TofMultiBankGeometryError> {
746 let live_lattice = TofMultiBankLatticeInput {
747 multibank: live.clone(),
748 lattice_phases: input.lattice.lattice_phases.clone(),
749 };
750 let (lattice, lattice_residual) = lattice_system(
751 &live_lattice,
752 cells,
753 cycle,
754 &packed.lattice.scales,
755 &options.lebail,
756 )?;
757 let instrument_input = TofMultiBankInstrumentInput {
758 multibank: live.clone(),
759 instrument_models: input.instrument_models.clone(),
760 };
761 let (instrument, instrument_residual) = instrument_system(
762 &instrument_input,
763 live,
764 &cycle.calculations,
765 &packed.instrument.scales,
766 &options.lebail,
767 )?;
768 if lattice.nrows() != instrument.nrows()
769 || lattice_residual.len() != instrument_residual.len()
770 || lattice_residual
771 .iter()
772 .zip(instrument_residual.iter())
773 .any(|(left, right)| left.to_bits() != right.to_bits())
774 {
775 return Err(TofMultiBankGeometryError::InternalInvariant);
776 }
777 let mut joint = DMatrix::zeros(lattice.nrows(), lattice.ncols() + instrument.ncols());
778 joint.view_mut((0, 0), lattice.shape()).copy_from(&lattice);
779 joint
780 .view_mut((0, lattice.ncols()), instrument.shape())
781 .copy_from(&instrument);
782 Ok((joint, lattice_residual))
783}
784
785fn geometry_instrument_changes(
786 input: &TofMultiBankGeometryInput,
787 packed: &PackedInstruments,
788 after: &[f64],
789) -> Result<Vec<TofInstrumentParameterChange>, TofMultiBankGeometryError> {
790 if packed.values.len() != after.len() || packed.values.len() != packed.scales.len() {
791 return Err(TofMultiBankGeometryError::InternalInvariant);
792 }
793 let mut changes = Vec::new();
794 let mut offset = 0;
795 for model in &input.instrument_models {
796 for bound in model.bounds() {
797 if packed.values[offset].to_bits() != after[offset].to_bits() {
798 changes.push(TofInstrumentParameterChange {
799 bank_id: model.bank_id().clone(),
800 parameter: bound.parameter,
801 before: packed.values[offset],
802 after: after[offset],
803 scaled_change: (after[offset] - packed.values[offset]) / packed.scales[offset],
804 });
805 }
806 offset += 1;
807 }
808 }
809 Ok(changes)
810}
811
812fn geometry_keys(input: &TofMultiBankGeometryInput) -> Vec<TofGeometryParameterKey> {
813 let mut keys = Vec::new();
814 for phase in &input.lattice.lattice_phases {
815 for parameter_name in phase.parameterization().parameter_names() {
816 keys.push(TofGeometryParameterKey::Lattice {
817 phase_id: phase.phase_id().clone(),
818 parameter_name: parameter_name.clone(),
819 });
820 }
821 }
822 for model in &input.instrument_models {
823 for bound in model.bounds() {
824 keys.push(TofGeometryParameterKey::Instrument {
825 bank_id: model.bank_id().clone(),
826 parameter: bound.parameter,
827 });
828 }
829 }
830 keys
831}
832
833fn geometry_diagnostics(
834 input: &TofMultiBankGeometryInput,
835 jacobian: &DMatrix<f64>,
836 threshold: f64,
837) -> Result<TofGeometryDiagnostics, TofMultiBankGeometryError> {
838 let parameter_count = input.parameter_count()?;
839 if jacobian.ncols() != parameter_count {
840 return Err(TofMultiBankGeometryError::InternalInvariant);
841 }
842 let singular = jacobian.clone().svd(false, false).singular_values;
843 let maximum = singular.iter().copied().fold(0.0_f64, f64::max);
844 let dimension = u32::try_from(jacobian.nrows().max(jacobian.ncols())).unwrap_or(u32::MAX);
845 let tolerance = f64::from(dimension) * f64::EPSILON * maximum;
846 let jacobian_rank = singular.iter().filter(|value| **value > tolerance).count();
847 let keys = geometry_keys(input);
848 let norms = (0..parameter_count)
849 .map(|column| jacobian.column(column).norm())
850 .collect::<Vec<_>>();
851 let mut maximum_absolute_correlation = None::<f64>;
852 let mut unresolved_correlations = Vec::new();
853 for left in 0..parameter_count {
854 if norms[left] == 0.0 {
855 continue;
856 }
857 for right in left + 1..parameter_count {
858 if norms[right] == 0.0 {
859 continue;
860 }
861 let correlation = (jacobian.column(left).dot(&jacobian.column(right))
862 / (norms[left] * norms[right]))
863 .clamp(-1.0, 1.0);
864 let absolute = correlation.abs();
865 maximum_absolute_correlation =
866 Some(maximum_absolute_correlation.map_or(absolute, |value| value.max(absolute)));
867 if absolute >= threshold {
868 unresolved_correlations.push(TofGeometryCorrelation {
869 left: keys[left].clone(),
870 right: keys[right].clone(),
871 correlation,
872 });
873 }
874 }
875 }
876 Ok(TofGeometryDiagnostics {
877 parameter_count,
878 jacobian_rank,
879 maximum_absolute_correlation,
880 unresolved_correlations,
881 })
882}
883
884fn checkpoint_from_state(
885 input: &TofMultiBankGeometryInput,
886 states: &[AcceptedBankState],
887 cells: &[UnitCell],
888 instruments: &[TofInstrument],
889 history: &[TofMultiBankGeometryIterationRecord],
890) -> TofMultiBankGeometryCheckpoint {
891 TofMultiBankGeometryCheckpoint {
892 completed_iterations: history.len(),
893 banks: input
894 .lattice
895 .multibank
896 .banks
897 .iter()
898 .zip(states)
899 .zip(instruments)
900 .map(
901 |((bank, state), instrument)| TofMultiBankGeometryCheckpointBank {
902 bank_id: bank.bank_id.clone(),
903 instrument: *instrument,
904 phases: state.phases.clone(),
905 background: state.background.clone(),
906 },
907 )
908 .collect(),
909 lattice_phases: lattice_states(input, cells),
910 history: history.to_vec(),
911 }
912}
913
914fn lattice_states(
915 input: &TofMultiBankGeometryInput,
916 cells: &[UnitCell],
917) -> Vec<TofSharedLatticeState> {
918 input
919 .lattice
920 .lattice_phases
921 .iter()
922 .zip(cells)
923 .map(|(phase, cell)| TofSharedLatticeState {
924 phase_id: phase.phase_id().clone(),
925 cell: *cell,
926 })
927 .collect()
928}
929
930fn instrument_states(
931 input: &TofMultiBankGeometryInput,
932 instruments: &[TofInstrument],
933) -> Vec<TofBankInstrumentState> {
934 input
935 .lattice
936 .multibank
937 .banks
938 .iter()
939 .zip(instruments)
940 .map(|(bank, instrument)| TofBankInstrumentState {
941 bank_id: bank.bank_id.clone(),
942 instrument: *instrument,
943 })
944 .collect()
945}
946
947fn recoverable_lattice_error(error: &TofMultiBankLatticeError) -> bool {
948 matches!(
949 error,
950 TofMultiBankLatticeError::Lattice(_)
951 | TofMultiBankLatticeError::InvalidModel(_)
952 | TofMultiBankLatticeError::MultiBank(TofMultiBankError::LeBail(
953 TofLeBailError::Profile(_) | TofLeBailError::InvalidPhase(_)
954 ))
955 )
956}
957
958fn recoverable_instrument_error(error: &TofMultiBankInstrumentError) -> bool {
959 matches!(
960 error,
961 TofMultiBankInstrumentError::Profile(_)
962 | TofMultiBankInstrumentError::MultiBank(TofMultiBankError::LeBail(
963 TofLeBailError::Profile(_) | TofLeBailError::InvalidPhase(_)
964 ))
965 )
966}
967
968fn recoverable_multibank_error(error: &TofMultiBankError) -> bool {
969 matches!(
970 error,
971 TofMultiBankError::LeBail(TofLeBailError::Profile(_) | TofLeBailError::InvalidPhase(_))
972 )
973}
974
975impl TofMultiBankGeometryCheckpoint {
976 pub fn validate_for(
983 &self,
984 input: &TofMultiBankGeometryInput,
985 options: &TofMultiBankGeometryOptions,
986 ) -> Result<(), TofMultiBankGeometryError> {
987 input.validate()?;
988 options.validate()?;
989 if self.completed_iterations != self.history.len()
990 || self.completed_iterations > options.lebail.cycles
991 || self.banks.len() != input.lattice.multibank.banks.len()
992 || self.lattice_phases.len() != input.lattice.lattice_phases.len()
993 {
994 return Err(TofMultiBankGeometryError::InvalidCheckpoint(
995 "joint TOF geometry checkpoint counts differ from the request",
996 ));
997 }
998 for (saved, model) in self
999 .lattice_phases
1000 .iter()
1001 .zip(&input.lattice.lattice_phases)
1002 {
1003 if &saved.phase_id != model.phase_id() {
1004 return Err(TofMultiBankGeometryError::InvalidCheckpoint(
1005 "joint TOF geometry checkpoint lattice phase order changed",
1006 ));
1007 }
1008 model.validate_cell(saved.cell)?;
1009 }
1010 for (saved, original) in self.banks.iter().zip(&input.lattice.multibank.banks) {
1011 validate_checkpoint_bank(saved, original, input, &self.lattice_phases)?;
1012 }
1013 if self.history.iter().enumerate().any(|(index, record)| {
1014 record.iteration != index + 1
1015 || record.bank_metrics.len() != input.lattice.multibank.banks.len()
1016 }) {
1017 return Err(TofMultiBankGeometryError::InvalidCheckpoint(
1018 "joint TOF geometry checkpoint history is not contiguous and bank-aligned",
1019 ));
1020 }
1021 Ok(())
1022 }
1023}
1024
1025fn validate_checkpoint_bank(
1026 saved: &TofMultiBankGeometryCheckpointBank,
1027 original: &crate::TofLeBailBank,
1028 input: &TofMultiBankGeometryInput,
1029 cells: &[TofSharedLatticeState],
1030) -> Result<(), TofMultiBankGeometryError> {
1031 saved.instrument.validate()?;
1032 if saved.bank_id != original.bank_id || saved.phases.len() != original.input.phases.len() {
1033 return Err(TofMultiBankGeometryError::InvalidCheckpoint(
1034 "joint TOF geometry checkpoint bank identity or phase count changed",
1035 ));
1036 }
1037 for (saved_phase, original_phase) in saved.phases.iter().zip(&original.input.phases) {
1038 saved_phase.validate()?;
1039 if saved_phase.phase_id() != original_phase.phase_id()
1040 || saved_phase.name() != original_phase.name()
1041 || saved_phase.reflection_ids() != original_phase.reflection_ids()
1042 || saved_phase.hkl() != original_phase.hkl()
1043 || saved_phase.scale().to_bits() != original_phase.scale().to_bits()
1044 {
1045 return Err(TofMultiBankGeometryError::InvalidCheckpoint(
1046 "joint TOF geometry checkpoint bank topology changed",
1047 ));
1048 }
1049 if let Some((model, cell)) = input
1050 .lattice
1051 .lattice_phases
1052 .iter()
1053 .zip(cells)
1054 .find(|(model, _)| model.phase_id() == saved_phase.phase_id())
1055 {
1056 let expected = tof_lattice_geometry(
1057 model.parameterization(),
1058 cell.cell,
1059 saved_phase.hkl(),
1060 saved.instrument,
1061 )?;
1062 if saved_phase.d_spacing_angstrom() != expected.d_spacing_angstrom {
1063 return Err(TofMultiBankGeometryError::InvalidCheckpoint(
1064 "joint TOF geometry checkpoint d-spacings disagree with its cell",
1065 ));
1066 }
1067 } else if saved_phase.d_spacing_angstrom() != original_phase.d_spacing_angstrom() {
1068 return Err(TofMultiBankGeometryError::InvalidCheckpoint(
1069 "fixed TOF phase d-spacings changed in a joint checkpoint",
1070 ));
1071 }
1072 for d_spacing in saved_phase.d_spacing_angstrom() {
1073 TofProfileParameters::from_instrument(*d_spacing, saved.instrument)?;
1074 }
1075 }
1076 validate_checkpoint_instrument(saved, original, input)?;
1077 match (&saved.background, &original.input.background) {
1078 (None, None) => Ok(()),
1079 (Some(saved), Some(original))
1080 if saved.background_id() == original.background_id()
1081 && saved
1082 .domain_us()
1083 .iter()
1084 .zip(original.domain_us())
1085 .all(|(saved, original)| saved.to_bits() == original.to_bits())
1086 && saved.coefficients().len() == original.coefficients().len() =>
1087 {
1088 Ok(())
1089 }
1090 _ => Err(TofMultiBankGeometryError::InvalidCheckpoint(
1091 "joint TOF geometry checkpoint background contract changed",
1092 )),
1093 }
1094}
1095
1096fn validate_checkpoint_instrument(
1097 saved: &TofMultiBankGeometryCheckpointBank,
1098 original: &crate::TofLeBailBank,
1099 input: &TofMultiBankGeometryInput,
1100) -> Result<(), TofMultiBankGeometryError> {
1101 let selected = input
1102 .instrument_models
1103 .iter()
1104 .find(|model| model.bank_id() == &saved.bank_id);
1105 let saved_values = saved.instrument.values();
1106 let original_values = original.input.instrument.values();
1107 for parameter in TofInstrumentParameter::ALL {
1108 let bound = selected.and_then(|model| {
1109 model
1110 .bounds()
1111 .iter()
1112 .find(|bound| bound.parameter == parameter)
1113 });
1114 match bound {
1115 Some(bound)
1116 if saved_values[parameter.index()] >= bound.lower
1117 && saved_values[parameter.index()] <= bound.upper => {}
1118 Some(_) => {
1119 return Err(TofMultiBankGeometryError::InvalidCheckpoint(
1120 "joint TOF geometry checkpoint instrument lies outside its bounds",
1121 ));
1122 }
1123 None if saved_values[parameter.index()].to_bits()
1124 == original_values[parameter.index()].to_bits() => {}
1125 None => {
1126 return Err(TofMultiBankGeometryError::InvalidCheckpoint(
1127 "unselected TOF instrument coefficient changed in a joint checkpoint",
1128 ));
1129 }
1130 }
1131 }
1132 Ok(())
1133}
1134
1135#[derive(Debug)]
1137pub enum TofMultiBankGeometryError {
1138 Lattice(TofMultiBankLatticeError),
1140 Instrument(TofMultiBankInstrumentError),
1142 MultiBank(TofMultiBankError),
1144 InvalidOptions,
1146 LinearSolve,
1148 InvalidCheckpoint(&'static str),
1150 AllocationOverflow,
1152 Runtime(RuntimeError),
1154 InternalInvariant,
1156}
1157
1158impl Display for TofMultiBankGeometryError {
1159 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
1160 match self {
1161 Self::Lattice(error) => Display::fmt(error, formatter),
1162 Self::Instrument(error) => Display::fmt(error, formatter),
1163 Self::MultiBank(error) => Display::fmt(error, formatter),
1164 Self::InvalidOptions => formatter.write_str("invalid joint TOF geometry options"),
1165 Self::LinearSolve => formatter.write_str("joint TOF geometry linear solve failed"),
1166 Self::InvalidCheckpoint(message) => formatter.write_str(message),
1167 Self::AllocationOverflow => {
1168 formatter.write_str("joint TOF geometry allocation overflow")
1169 }
1170 Self::Runtime(error) => Display::fmt(error, formatter),
1171 Self::InternalInvariant => {
1172 formatter.write_str("joint TOF geometry internal array invariant failed")
1173 }
1174 }
1175 }
1176}
1177
1178impl Error for TofMultiBankGeometryError {
1179 fn source(&self) -> Option<&(dyn Error + 'static)> {
1180 match self {
1181 Self::Lattice(error) => Some(error),
1182 Self::Instrument(error) => Some(error),
1183 Self::MultiBank(error) => Some(error),
1184 Self::Runtime(error) => Some(error),
1185 _ => None,
1186 }
1187 }
1188}
1189
1190impl From<TofMultiBankLatticeError> for TofMultiBankGeometryError {
1191 fn from(value: TofMultiBankLatticeError) -> Self {
1192 Self::Lattice(value)
1193 }
1194}
1195
1196impl From<TofMultiBankInstrumentError> for TofMultiBankGeometryError {
1197 fn from(value: TofMultiBankInstrumentError) -> Self {
1198 Self::Instrument(value)
1199 }
1200}
1201
1202impl From<TofMultiBankError> for TofMultiBankGeometryError {
1203 fn from(value: TofMultiBankError) -> Self {
1204 Self::MultiBank(value)
1205 }
1206}
1207
1208impl From<TofLeBailError> for TofMultiBankGeometryError {
1209 fn from(value: TofLeBailError) -> Self {
1210 Self::MultiBank(TofMultiBankError::LeBail(value))
1211 }
1212}
1213
1214impl From<phasesmith_core::TofError> for TofMultiBankGeometryError {
1215 fn from(value: phasesmith_core::TofError) -> Self {
1216 Self::Instrument(TofMultiBankInstrumentError::Profile(value))
1217 }
1218}
1219
1220impl From<crate::LatticeError> for TofMultiBankGeometryError {
1221 fn from(value: crate::LatticeError) -> Self {
1222 Self::Lattice(TofMultiBankLatticeError::Lattice(value))
1223 }
1224}
1225
1226impl From<RuntimeError> for TofMultiBankGeometryError {
1227 fn from(value: RuntimeError) -> Self {
1228 Self::Runtime(value)
1229 }
1230}