1use std::collections::BTreeSet;
4use std::error::Error;
5use std::fmt::{Display, Formatter};
6
7use phasesmith_core::{
8 TOF_GLOBAL_PARAMETER_COUNT, TofBankGeometry, TofError, TofInstrument, TofInstrumentParameter,
9};
10use phasesmith_crystallography::{IntegratedIntensityCorrectionModel, P1ParameterLayout};
11use phasesmith_engine::{
12 BuiltInScatteringModel, StructuralTofError, StructuralTofInputView, StructuralTofResult,
13 calculate_structural_tof_pattern_jvp_with_context,
14 calculate_structural_tof_pattern_vjp_with_context,
15 calculate_structural_tof_pattern_with_context,
16};
17use phasesmith_execution::ExecutionPolicy;
18use phasesmith_model::{DomainError, RecordId, TofPatternRecord};
19
20use crate::{
21 LatticeBounds, ParameterBounds, ParameterError, ParameterKey, ParameterSet, ParameterSpec,
22 ResidualError, ResidualEvaluation, ResidualOptions, RietveldError, RietveldParameterError,
23 RietveldPhase, RietveldStructuralLayout, RietveldStructuralSelection, TofChebyshevBackground,
24 TofInstrumentParameterBound, TofLeBailError, evaluate_tof_residuals,
25};
26
27#[derive(Clone, Debug, PartialEq)]
29pub struct StructuralTofBank {
30 pub bank_id: RecordId,
32 pub pattern: TofPatternRecord,
34 pub instrument: TofInstrument,
36 pub geometry: TofBankGeometry,
38 pub correction_model: IntegratedIntensityCorrectionModel,
40 pub scale: f64,
42 pub scale_bounds: ParameterBounds,
44 pub refine_scale: bool,
46 pub background: Option<TofChebyshevBackground>,
48 pub refine_background: bool,
50 pub instrument_bounds: Vec<TofInstrumentParameterBound>,
52}
53
54#[derive(Clone, Debug, PartialEq)]
56pub struct StructuralTofMultiBankInput {
57 pub phase: RietveldPhase,
59 pub structural_selection: RietveldStructuralSelection,
61 pub lattice_bounds: Option<LatticeBounds>,
63 pub banks: Vec<StructuralTofBank>,
65 pub support_fwhm: f64,
67 pub tail_log: f64,
69 pub use_uncertainty: bool,
71 pub execution: ExecutionPolicy,
73}
74
75impl StructuralTofMultiBankInput {
76 pub fn validate(&self) -> Result<(), StructuralTofMultiBankError> {
83 self.phase.validate()?;
84 let definition = self.phase.definition();
85 if definition.scattering_model != BuiltInScatteringModel::NeutronNuclear
86 || !definition.scattering_real_offset.is_empty()
87 || !definition.scattering_imag_offset.is_empty()
88 {
89 return Err(StructuralTofMultiBankError::InvalidPhaseContract(
90 "structural TOF requires built-in neutron scattering without X-ray offsets",
91 ));
92 }
93 if definition.correction_model != IntegratedIntensityCorrectionModel::Neutral
94 || definition.scale.to_bits() != 1.0_f64.to_bits()
95 {
96 return Err(StructuralTofMultiBankError::InvalidPhaseContract(
97 "the shared phase must use neutral correction and unit placeholder scale",
98 ));
99 }
100 if self.phase.sample_physics().is_some()
101 || self.phase.reflection_domain().is_some()
102 || self.phase.contributions()
103 != &phasesmith_core::OwnedCwContributions::neutral(definition.hkl.len())
104 {
105 return Err(StructuralTofMultiBankError::InvalidPhaseContract(
106 "CW sample physics and dynamic topology are not part of structural TOF",
107 ));
108 }
109 if self.structural_selection.phase_scale {
110 return Err(StructuralTofMultiBankError::InvalidPhaseContract(
111 "structural TOF phase scale is bank-local and cannot be shared",
112 ));
113 }
114 if !self.support_fwhm.is_finite()
115 || self.support_fwhm <= 0.0
116 || !self.tail_log.is_finite()
117 || self.tail_log <= 0.0
118 {
119 return Err(StructuralTofMultiBankError::InvalidSupport);
120 }
121 if self.banks.is_empty() {
122 return Err(StructuralTofMultiBankError::TooFewBanks);
123 }
124 if self
125 .banks
126 .iter()
127 .map(|bank| &bank.bank_id)
128 .collect::<BTreeSet<_>>()
129 .len()
130 != self.banks.len()
131 {
132 return Err(StructuralTofMultiBankError::DuplicateBankId);
133 }
134 for bank in &self.banks {
135 validate_bank(bank)?;
136 }
137 RietveldStructuralLayout::new(
138 std::slice::from_ref(&self.phase),
139 self.structural_selection,
140 std::slice::from_ref(&self.lattice_bounds),
141 )?;
142 Ok(())
143 }
144}
145
146#[derive(Clone, Debug, PartialEq)]
147struct BankParameterMapping {
148 bank_id: RecordId,
149 pattern: TofPatternRecord,
150 geometry: TofBankGeometry,
151 correction_model: IntegratedIntensityCorrectionModel,
152 scale_bounds: ParameterBounds,
153 scale: Option<usize>,
154 instrument_bounds: Vec<TofInstrumentParameterBound>,
155 instrument: Vec<(TofInstrumentParameter, usize)>,
156 background_contract: Option<(RecordId, [f64; 2], usize)>,
157 background: Vec<usize>,
158}
159
160#[derive(Clone, Debug, PartialEq)]
162pub struct StructuralTofMultiBankLayout {
163 parameters: ParameterSet,
164 structural: RietveldStructuralLayout,
165 structural_count: usize,
166 structural_selection: RietveldStructuralSelection,
167 lattice_bounds: Option<LatticeBounds>,
168 reflection_ids: Vec<String>,
169 support_fwhm: f64,
170 tail_log: f64,
171 use_uncertainty: bool,
172 execution: ExecutionPolicy,
173 banks: Vec<BankParameterMapping>,
174}
175
176impl StructuralTofMultiBankLayout {
177 pub fn new(input: &StructuralTofMultiBankInput) -> Result<Self, StructuralTofMultiBankError> {
183 input.validate()?;
184 let structural = RietveldStructuralLayout::new(
185 std::slice::from_ref(&input.phase),
186 input.structural_selection,
187 std::slice::from_ref(&input.lattice_bounds),
188 )?;
189 let mut specs = structural.parameters().specs().to_vec();
190 let structural_count = specs.len();
191 let mut banks = Vec::with_capacity(input.banks.len());
192 for bank in &input.banks {
193 let owner = bank.bank_id.as_str();
194 let scale = if bank.refine_scale {
195 let index = specs.len();
196 specs.push(ParameterSpec::new(
197 ParameterKey::new("tof_scale", owner, "scale")?,
198 bank.scale,
199 "relative",
200 bank.scale_bounds,
201 bank.scale.abs().max(1.0),
202 true,
203 )?);
204 Some(index)
205 } else {
206 None
207 };
208 let instrument_values = bank.instrument.values();
209 let mut instrument = Vec::with_capacity(bank.instrument_bounds.len());
210 for bound in &bank.instrument_bounds {
211 let index = specs.len();
212 let value = instrument_values[bound.parameter.index()];
213 let half_span = 0.5 * (bound.upper - bound.lower);
214 specs.push(ParameterSpec::new(
215 ParameterKey::new("tof_instrument", owner, bound.parameter.name())?,
216 value,
217 instrument_unit(bound.parameter),
218 ParameterBounds::new(bound.lower, bound.upper)?,
219 value.abs().max(half_span).max(f64::EPSILON.sqrt()),
220 true,
221 )?);
222 instrument.push((bound.parameter, index));
223 }
224 let mut background = Vec::new();
225 if bank.refine_background {
226 let model = bank.background.as_ref().ok_or(
227 StructuralTofMultiBankError::InvalidBankContract(
228 "refine_background requires a background model",
229 ),
230 )?;
231 for (order, value) in model.coefficients().iter().copied().enumerate() {
232 let index = specs.len();
233 specs.push(ParameterSpec::new(
234 ParameterKey::new("tof_background", owner, format!("coefficient_{order}"))?,
235 value,
236 "intensity",
237 ParameterBounds::default(),
238 value.abs().max(1.0),
239 true,
240 )?);
241 background.push(index);
242 }
243 }
244 banks.push(BankParameterMapping {
245 bank_id: bank.bank_id.clone(),
246 pattern: bank.pattern.clone(),
247 geometry: bank.geometry,
248 correction_model: bank.correction_model,
249 scale_bounds: bank.scale_bounds,
250 scale,
251 instrument_bounds: bank.instrument_bounds.clone(),
252 instrument,
253 background_contract: bank.background.as_ref().map(|model| {
254 (
255 model.background_id().clone(),
256 model.domain_us(),
257 model.coefficients().len(),
258 )
259 }),
260 background,
261 });
262 }
263 Ok(Self {
264 parameters: ParameterSet::new(specs)?,
265 structural,
266 structural_count,
267 structural_selection: input.structural_selection,
268 lattice_bounds: input.lattice_bounds.clone(),
269 reflection_ids: input.phase.reflection_ids().to_vec(),
270 support_fwhm: input.support_fwhm,
271 tail_log: input.tail_log,
272 use_uncertainty: input.use_uncertainty,
273 execution: input.execution.clone(),
274 banks,
275 })
276 }
277
278 #[must_use]
280 pub const fn parameters(&self) -> &ParameterSet {
281 &self.parameters
282 }
283
284 pub fn apply_values(
291 &self,
292 input: &StructuralTofMultiBankInput,
293 values: &[f64],
294 ) -> Result<StructuralTofMultiBankInput, StructuralTofMultiBankError> {
295 let current = self
296 .parameters
297 .specs()
298 .iter()
299 .map(ParameterSpec::value)
300 .collect::<Vec<_>>();
301 self.apply_value_change(input, ¤t, values)
302 }
303
304 pub fn apply_value_change(
315 &self,
316 input: &StructuralTofMultiBankInput,
317 current_values: &[f64],
318 values: &[f64],
319 ) -> Result<StructuralTofMultiBankInput, StructuralTofMultiBankError> {
320 self.validate_contract(input)?;
321 if current_values.len() != self.parameters.specs().len()
322 || values.len() != self.parameters.specs().len()
323 || current_values.iter().any(|value| !value.is_finite())
324 {
325 return Err(StructuralTofMultiBankError::ParameterLengthMismatch);
326 }
327 for (spec, value) in self.parameters.specs().iter().zip(values) {
328 if !value.is_finite() || !spec.bounds().contains(*value) {
329 return Err(StructuralTofMultiBankError::Parameter(
330 ParameterError::ValueOutsideBounds {
331 key: spec.key().clone(),
332 value: *value,
333 },
334 ));
335 }
336 }
337 let phase = self
338 .structural
339 .apply_value_change(
340 std::slice::from_ref(&input.phase),
341 ¤t_values[..self.structural_count],
342 &values[..self.structural_count],
343 )?
344 .into_iter()
345 .next()
346 .ok_or(StructuralTofMultiBankError::InternalInvariant)?;
347 let mut result = input.clone();
348 result.phase = phase;
349 for ((bank, mapping), original) in
350 result.banks.iter_mut().zip(&self.banks).zip(&input.banks)
351 {
352 if let Some(index) = mapping.scale {
353 bank.scale = values[index];
354 }
355 let mut instrument_values = original.instrument.values();
356 for &(parameter, index) in &mapping.instrument {
357 instrument_values[parameter.index()] = values[index];
358 }
359 bank.instrument = TofInstrument::from_values(instrument_values)?;
360 if !mapping.background.is_empty() {
361 let coefficients = mapping
362 .background
363 .iter()
364 .map(|index| values[*index])
365 .collect();
366 bank.background = Some(
367 original
368 .background
369 .as_ref()
370 .ok_or(StructuralTofMultiBankError::InternalInvariant)?
371 .with_coefficients(coefficients)?,
372 );
373 }
374 }
375 result.validate()?;
376 Ok(result)
377 }
378
379 fn validate_contract(
380 &self,
381 input: &StructuralTofMultiBankInput,
382 ) -> Result<(), StructuralTofMultiBankError> {
383 input.validate()?;
384 self.structural
385 .validate_phases(std::slice::from_ref(&input.phase))?;
386 if input.structural_selection != self.structural_selection
387 || input.lattice_bounds != self.lattice_bounds
388 || input.phase.reflection_ids() != self.reflection_ids
389 || input.support_fwhm.to_bits() != self.support_fwhm.to_bits()
390 || input.tail_log.to_bits() != self.tail_log.to_bits()
391 || input.use_uncertainty != self.use_uncertainty
392 || input.execution != self.execution
393 || input.banks.len() != self.banks.len()
394 {
395 return Err(StructuralTofMultiBankError::BankContractMismatch);
396 }
397 for (bank, mapping) in input.banks.iter().zip(&self.banks) {
398 let background_contract = bank.background.as_ref().map(|model| {
399 (
400 model.background_id().clone(),
401 model.domain_us(),
402 model.coefficients().len(),
403 )
404 });
405 if bank.bank_id != mapping.bank_id
406 || bank.pattern != mapping.pattern
407 || bank.geometry != mapping.geometry
408 || bank.correction_model != mapping.correction_model
409 || bank.scale_bounds != mapping.scale_bounds
410 || bank.refine_scale != mapping.scale.is_some()
411 || bank.instrument_bounds != mapping.instrument_bounds
412 || bank.refine_background == mapping.background.is_empty()
413 || background_contract != mapping.background_contract
414 {
415 return Err(StructuralTofMultiBankError::BankContractMismatch);
416 }
417 }
418 Ok(())
419 }
420
421 fn native_tangent(
422 &self,
423 bank_index: usize,
424 direction: &[f64],
425 native_count: usize,
426 ) -> Result<Vec<f64>, StructuralTofMultiBankError> {
427 if direction.len() != self.parameters.specs().len() {
428 return Err(StructuralTofMultiBankError::ParameterLengthMismatch);
429 }
430 let mut tangent = self
431 .structural
432 .native_tangents(&direction[..self.structural_count])?
433 .into_iter()
434 .next()
435 .ok_or(StructuralTofMultiBankError::InternalInvariant)?;
436 if tangent.len() != native_count {
437 return Err(StructuralTofMultiBankError::InternalInvariant);
438 }
439 if let Some(index) = self.banks[bank_index].scale {
440 tangent[native_count - 1] = direction[index];
441 }
442 Ok(tangent)
443 }
444
445 fn scatter_native_gradient(
446 &self,
447 bank_index: usize,
448 native: &[f64],
449 output: &mut [f64],
450 ) -> Result<(), StructuralTofMultiBankError> {
451 let shared = self.structural.project_native_gradients(&[native])?;
452 for (target, value) in output[..self.structural_count].iter_mut().zip(shared) {
453 *target += value;
454 }
455 if let Some(index) = self.banks[bank_index].scale {
456 output[index] += native
457 .last()
458 .copied()
459 .ok_or(StructuralTofMultiBankError::InternalInvariant)?;
460 }
461 Ok(())
462 }
463}
464
465#[derive(Clone, Debug, PartialEq)]
467pub struct StructuralTofBankCalculation {
468 pub bank_id: RecordId,
470 pub y: Vec<f64>,
472 pub profile_y: Vec<f64>,
474 pub background_y: Vec<f64>,
476 pub structural: StructuralTofResult,
478 pub metrics: ResidualEvaluation,
480}
481
482#[derive(Clone, Debug, PartialEq)]
484pub struct StructuralTofMultiBankCalculation {
485 pub banks: Vec<StructuralTofBankCalculation>,
487 pub objective: f64,
489}
490
491#[derive(Clone, Debug, PartialEq)]
493pub struct StructuralTofMultiBankProduct {
494 pub bank_id: RecordId,
496 pub y: Vec<f64>,
498 pub derivative: Vec<f64>,
500}
501
502#[derive(Clone, Debug, PartialEq)]
504pub struct StructuralTofMultiBankGradient {
505 pub calculation: StructuralTofMultiBankCalculation,
507 pub gradient: Vec<f64>,
509}
510
511pub struct PreparedStructuralTofMultiBankObjective {
513 input: StructuralTofMultiBankInput,
514 layout: StructuralTofMultiBankLayout,
515 background_bases: Vec<Option<crate::TofChebyshevBasis>>,
516}
517
518impl PreparedStructuralTofMultiBankObjective {
519 pub fn new(input: StructuralTofMultiBankInput) -> Result<Self, StructuralTofMultiBankError> {
525 let layout = StructuralTofMultiBankLayout::new(&input)?;
526 let background_bases = input
527 .banks
528 .iter()
529 .map(|bank| {
530 bank.background
531 .as_ref()
532 .map(|model| model.basis(&bank.pattern.tof_us))
533 .transpose()
534 })
535 .collect::<Result<Vec<_>, _>>()?;
536 Ok(Self {
537 input,
538 layout,
539 background_bases,
540 })
541 }
542
543 #[must_use]
545 pub const fn input(&self) -> &StructuralTofMultiBankInput {
546 &self.input
547 }
548
549 #[must_use]
551 pub const fn layout(&self) -> &StructuralTofMultiBankLayout {
552 &self.layout
553 }
554
555 pub fn calculate(
561 &self,
562 ) -> Result<StructuralTofMultiBankCalculation, StructuralTofMultiBankError> {
563 let mut banks = Vec::with_capacity(self.input.banks.len());
564 let mut objective = 0.0;
565 for (index, bank) in self.input.banks.iter().enumerate() {
566 let structural = calculate_bank(&self.input, bank)?;
567 let profile_y = structural.accumulation.y.clone();
568 let background_y = background_values(bank, self.background_bases[index].as_ref())?;
569 let y = profile_y
570 .iter()
571 .zip(&background_y)
572 .map(|(profile, background)| profile + background)
573 .collect::<Vec<_>>();
574 let metrics = evaluate_tof_residuals(
575 &bank.pattern,
576 &y,
577 ResidualOptions {
578 use_uncertainty: self.input.use_uncertainty,
579 parameter_count: self.layout.parameters.specs().len(),
580 },
581 )?;
582 objective += 0.5 * metrics.chi_square;
583 banks.push(StructuralTofBankCalculation {
584 bank_id: bank.bank_id.clone(),
585 y,
586 profile_y,
587 background_y,
588 structural,
589 metrics,
590 });
591 }
592 Ok(StructuralTofMultiBankCalculation { banks, objective })
593 }
594
595 pub fn jvp(
601 &self,
602 direction: &[f64],
603 ) -> Result<Vec<StructuralTofMultiBankProduct>, StructuralTofMultiBankError> {
604 let native_count = P1ParameterLayout {
605 site_count: self.input.phase.definition().fractional_xyz.len(),
606 }
607 .parameter_count();
608 let mut products = Vec::with_capacity(self.input.banks.len());
609 for (bank_index, bank) in self.input.banks.iter().enumerate() {
610 let tangent = self
611 .layout
612 .native_tangent(bank_index, direction, native_count)?;
613 let species = species(self.input.phase.definition());
614 let view = bank_view(&self.input, bank, &species);
615 let forward = calculate_structural_tof_pattern_jvp_with_context(
616 self.input.phase.definition().cell,
617 &self.input.phase.definition().space_group,
618 &view,
619 &tangent,
620 self.input.execution.context(),
621 )?;
622 let mut derivative = forward.d_y;
623 add_instrument_jvp(
624 &mut derivative,
625 &forward.result,
626 &self.layout.banks[bank_index],
627 direction,
628 )?;
629 add_background_jvp(
630 &mut derivative,
631 self.background_bases[bank_index].as_ref(),
632 &self.layout.banks[bank_index],
633 direction,
634 )?;
635 let background = background_values(bank, self.background_bases[bank_index].as_ref())?;
636 let y = forward
637 .result
638 .accumulation
639 .y
640 .iter()
641 .zip(background)
642 .map(|(profile, background)| profile + background)
643 .collect();
644 products.push(StructuralTofMultiBankProduct {
645 bank_id: bank.bank_id.clone(),
646 y,
647 derivative,
648 });
649 }
650 Ok(products)
651 }
652
653 pub fn vjp(&self, weights: &[Vec<f64>]) -> Result<Vec<f64>, StructuralTofMultiBankError> {
659 if weights.len() != self.input.banks.len() {
660 return Err(StructuralTofMultiBankError::BankWeightCountMismatch);
661 }
662 let mut result = vec![0.0; self.layout.parameters.specs().len()];
663 for (bank_index, (bank, weights)) in self.input.banks.iter().zip(weights).enumerate() {
664 let species = species(self.input.phase.definition());
665 let view = bank_view(&self.input, bank, &species);
666 let reverse = calculate_structural_tof_pattern_vjp_with_context(
667 self.input.phase.definition().cell,
668 &self.input.phase.definition().space_group,
669 &view,
670 weights,
671 self.input.execution.context(),
672 )?;
673 self.layout
674 .scatter_native_gradient(bank_index, &reverse.gradient, &mut result)?;
675 add_instrument_vjp(
676 &mut result,
677 &reverse.result,
678 &self.layout.banks[bank_index],
679 weights,
680 )?;
681 add_background_vjp(
682 &mut result,
683 self.background_bases[bank_index].as_ref(),
684 &self.layout.banks[bank_index],
685 weights,
686 )?;
687 }
688 Ok(result)
689 }
690
691 pub fn gradient(&self) -> Result<StructuralTofMultiBankGradient, StructuralTofMultiBankError> {
697 let calculation = self.calculate()?;
698 let weights = self
699 .input
700 .banks
701 .iter()
702 .zip(&calculation.banks)
703 .map(|(bank, calculation)| {
704 objective_weights(bank, &calculation.metrics, self.input.use_uncertainty)
705 })
706 .collect::<Vec<_>>();
707 let gradient = self.vjp(&weights)?;
708 Ok(StructuralTofMultiBankGradient {
709 calculation,
710 gradient,
711 })
712 }
713
714 pub fn normal_product(
720 &self,
721 direction: &[f64],
722 damping: f64,
723 ) -> Result<Vec<f64>, StructuralTofMultiBankError> {
724 if !damping.is_finite() || damping < 0.0 {
725 return Err(StructuralTofMultiBankError::InvalidDamping);
726 }
727 let products = self.jvp(direction)?;
728 let weights = self
729 .input
730 .banks
731 .iter()
732 .zip(products)
733 .map(|(bank, product)| {
734 weighted_direction(bank, product.derivative, self.input.use_uncertainty)
735 })
736 .collect::<Vec<_>>();
737 let mut result = self.vjp(&weights)?;
738 for (value, direction) in result.iter_mut().zip(direction) {
739 *value += damping * direction;
740 }
741 Ok(result)
742 }
743}
744
745fn validate_bank(bank: &StructuralTofBank) -> Result<(), StructuralTofMultiBankError> {
746 bank.pattern.validate()?;
747 if bank.pattern.observed_y.is_none() {
748 return Err(StructuralTofMultiBankError::MissingObservations);
749 }
750 bank.instrument.validate()?;
751 bank.geometry.validate()?;
752 if !bank.scale.is_finite() || bank.scale < 0.0 || !bank.scale_bounds.contains(bank.scale) {
753 return Err(StructuralTofMultiBankError::InvalidBankContract(
754 "bank scale must be finite, non-negative, and inside its bounds",
755 ));
756 }
757 match bank.correction_model {
758 IntegratedIntensityCorrectionModel::Neutral => {}
759 IntegratedIntensityCorrectionModel::TimeOfFlightNeutronLorentz { two_theta_deg }
760 if two_theta_deg.to_bits() == bank.geometry.two_theta_deg.to_bits() => {}
761 _ => {
762 return Err(StructuralTofMultiBankError::InvalidBankContract(
763 "bank correction must be neutral or match the bank angle exactly",
764 ));
765 }
766 }
767 let mut selected = BTreeSet::new();
768 let values = bank.instrument.values();
769 for bound in &bank.instrument_bounds {
770 if !bound.lower.is_finite()
771 || !bound.upper.is_finite()
772 || bound.lower >= bound.upper
773 || !selected.insert(bound.parameter)
774 || !(bound.lower..=bound.upper).contains(&values[bound.parameter.index()])
775 {
776 return Err(StructuralTofMultiBankError::InvalidBankContract(
777 "instrument selections require unique finite bounds containing the current value",
778 ));
779 }
780 }
781 if let Some(background) = &bank.background {
782 background.validate()?;
783 background.basis(&bank.pattern.tof_us)?;
784 } else if bank.refine_background {
785 return Err(StructuralTofMultiBankError::InvalidBankContract(
786 "refine_background requires a background model",
787 ));
788 }
789 Ok(())
790}
791
792fn species(definition: &phasesmith_engine::StructuralPhaseDefinition) -> Vec<&str> {
793 definition
794 .scattering_species
795 .iter()
796 .map(String::as_str)
797 .collect()
798}
799
800fn bank_view<'a>(
801 input: &'a StructuralTofMultiBankInput,
802 bank: &'a StructuralTofBank,
803 species: &'a [&'a str],
804) -> StructuralTofInputView<'a> {
805 let definition = input.phase.definition();
806 StructuralTofInputView {
807 tof_us: &bank.pattern.tof_us,
808 hkl: &definition.hkl,
809 multiplicity: &definition.multiplicity,
810 fractional_xyz: &definition.fractional_xyz,
811 occupancy: &definition.occupancy,
812 u_iso_angstrom2: &definition.u_iso_angstrom2,
813 anisotropic_mask: &definition.anisotropic_mask,
814 u_aniso_cif_angstrom2: &definition.u_aniso_cif_angstrom2,
815 scattering_species: species,
816 scale: bank.scale,
817 coordinate_tolerance: definition.coordinate_tolerance,
818 correction_model: bank.correction_model,
819 bank_geometry: bank.geometry,
820 instrument: bank.instrument,
821 support_fwhm: input.support_fwhm,
822 tail_log: input.tail_log,
823 }
824}
825
826fn calculate_bank(
827 input: &StructuralTofMultiBankInput,
828 bank: &StructuralTofBank,
829) -> Result<StructuralTofResult, StructuralTofMultiBankError> {
830 let species = species(input.phase.definition());
831 Ok(calculate_structural_tof_pattern_with_context(
832 input.phase.definition().cell,
833 &input.phase.definition().space_group,
834 &bank_view(input, bank, &species),
835 input.execution.context(),
836 )?)
837}
838
839fn background_values(
840 bank: &StructuralTofBank,
841 basis: Option<&crate::TofChebyshevBasis>,
842) -> Result<Vec<f64>, StructuralTofMultiBankError> {
843 let mut values = if let (Some(model), Some(basis)) = (&bank.background, basis) {
844 model.calculate_from_basis(basis)?
845 } else {
846 vec![0.0; bank.pattern.sample_count()]
847 };
848 for (value, fixed) in values.iter_mut().zip(&bank.pattern.background_y) {
849 *value += fixed;
850 }
851 Ok(values)
852}
853
854fn add_instrument_jvp(
855 derivative: &mut [f64],
856 result: &StructuralTofResult,
857 mapping: &BankParameterMapping,
858 direction: &[f64],
859) -> Result<(), StructuralTofMultiBankError> {
860 let global = result
861 .accumulation
862 .derivatives
863 .global
864 .as_ref()
865 .ok_or(StructuralTofMultiBankError::InternalInvariant)?;
866 if global.parameter_count != TOF_GLOBAL_PARAMETER_COUNT
867 || global.values.len() != TOF_GLOBAL_PARAMETER_COUNT * derivative.len()
868 {
869 return Err(StructuralTofMultiBankError::InternalInvariant);
870 }
871 for &(parameter, index) in &mapping.instrument {
872 let row = &global.values
873 [parameter.index() * derivative.len()..(parameter.index() + 1) * derivative.len()];
874 for (target, value) in derivative.iter_mut().zip(row) {
875 *target += direction[index] * value;
876 }
877 }
878 Ok(())
879}
880
881fn add_instrument_vjp(
882 output: &mut [f64],
883 result: &StructuralTofResult,
884 mapping: &BankParameterMapping,
885 weights: &[f64],
886) -> Result<(), StructuralTofMultiBankError> {
887 let global = result
888 .accumulation
889 .derivatives
890 .global
891 .as_ref()
892 .ok_or(StructuralTofMultiBankError::InternalInvariant)?;
893 if global.values.len() != TOF_GLOBAL_PARAMETER_COUNT * weights.len() {
894 return Err(StructuralTofMultiBankError::InternalInvariant);
895 }
896 for &(parameter, index) in &mapping.instrument {
897 let row = &global.values
898 [parameter.index() * weights.len()..(parameter.index() + 1) * weights.len()];
899 output[index] += row.iter().zip(weights).map(|(a, b)| a * b).sum::<f64>();
900 }
901 Ok(())
902}
903
904fn add_background_jvp(
905 derivative: &mut [f64],
906 basis: Option<&crate::TofChebyshevBasis>,
907 mapping: &BankParameterMapping,
908 direction: &[f64],
909) -> Result<(), StructuralTofMultiBankError> {
910 if mapping.background.is_empty() {
911 return Ok(());
912 }
913 let basis = basis.ok_or(StructuralTofMultiBankError::InternalInvariant)?;
914 if basis.rows != derivative.len() || basis.columns != mapping.background.len() {
915 return Err(StructuralTofMultiBankError::InternalInvariant);
916 }
917 for (sample, row) in basis.values.chunks_exact(basis.columns).enumerate() {
918 derivative[sample] += row
919 .iter()
920 .zip(&mapping.background)
921 .map(|(value, index)| value * direction[*index])
922 .sum::<f64>();
923 }
924 Ok(())
925}
926
927fn add_background_vjp(
928 output: &mut [f64],
929 basis: Option<&crate::TofChebyshevBasis>,
930 mapping: &BankParameterMapping,
931 weights: &[f64],
932) -> Result<(), StructuralTofMultiBankError> {
933 if mapping.background.is_empty() {
934 return Ok(());
935 }
936 let basis = basis.ok_or(StructuralTofMultiBankError::InternalInvariant)?;
937 if basis.rows != weights.len() || basis.columns != mapping.background.len() {
938 return Err(StructuralTofMultiBankError::InternalInvariant);
939 }
940 for (sample, row) in basis.values.chunks_exact(basis.columns).enumerate() {
941 for (value, index) in row.iter().zip(&mapping.background) {
942 output[*index] += weights[sample] * value;
943 }
944 }
945 Ok(())
946}
947
948fn objective_weights(
949 bank: &StructuralTofBank,
950 metrics: &ResidualEvaluation,
951 use_uncertainty: bool,
952) -> Vec<f64> {
953 (0..bank.pattern.sample_count())
954 .map(|sample| {
955 if !metrics.included[sample] {
956 0.0
957 } else if use_uncertainty {
958 let sigma = bank.pattern.uncertainty.as_ref().map_or(1.0, |v| v[sample]);
959 metrics.residual[sample] / (sigma * sigma)
960 } else {
961 metrics.residual[sample]
962 }
963 })
964 .collect()
965}
966
967fn weighted_direction(
968 bank: &StructuralTofBank,
969 direction: Vec<f64>,
970 use_uncertainty: bool,
971) -> Vec<f64> {
972 direction
973 .into_iter()
974 .enumerate()
975 .map(|(sample, value)| {
976 if bank.pattern.mask.as_ref().is_some_and(|mask| !mask[sample]) {
977 0.0
978 } else if use_uncertainty {
979 let sigma = bank.pattern.uncertainty.as_ref().map_or(1.0, |v| v[sample]);
980 value / (sigma * sigma)
981 } else {
982 value
983 }
984 })
985 .collect()
986}
987
988const fn instrument_unit(parameter: TofInstrumentParameter) -> &'static str {
989 match parameter {
990 TofInstrumentParameter::Zero | TofInstrumentParameter::Z => "microsecond",
991 TofInstrumentParameter::Difc | TofInstrumentParameter::X => "microsecond/angstrom",
992 TofInstrumentParameter::Difa | TofInstrumentParameter::Y => "microsecond/angstrom^2",
993 TofInstrumentParameter::Difb => "microsecond*angstrom",
994 TofInstrumentParameter::Alpha => "microsecond^-1*angstrom",
995 TofInstrumentParameter::Beta0 => "microsecond^-1",
996 TofInstrumentParameter::Beta1 => "angstrom^4/microsecond",
997 TofInstrumentParameter::Betaq => "angstrom^2/microsecond",
998 TofInstrumentParameter::Sigma0 => "microsecond^2",
999 TofInstrumentParameter::Sigma1 => "microsecond^2/angstrom^2",
1000 TofInstrumentParameter::Sigma2 => "microsecond^2/angstrom^4",
1001 TofInstrumentParameter::Sigmaq => "microsecond^2/angstrom",
1002 }
1003}
1004
1005#[derive(Debug)]
1007pub enum StructuralTofMultiBankError {
1008 TooFewBanks,
1010 DuplicateBankId,
1012 MissingObservations,
1014 InvalidPhaseContract(&'static str),
1016 InvalidBankContract(&'static str),
1018 InvalidSupport,
1020 BankContractMismatch,
1022 ParameterLengthMismatch,
1024 BankWeightCountMismatch,
1026 InvalidDamping,
1028 InternalInvariant,
1030 Pattern(DomainError),
1032 Rietveld(RietveldError),
1034 StructuralParameters(RietveldParameterError),
1036 Parameter(ParameterError),
1038 Background(TofLeBailError),
1040 Structural(StructuralTofError),
1042 Tof(TofError),
1044 Residual(ResidualError),
1046}
1047
1048impl Display for StructuralTofMultiBankError {
1049 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
1050 match self {
1051 Self::TooFewBanks => formatter.write_str("structural TOF requires at least one bank"),
1052 Self::DuplicateBankId => formatter.write_str("structural TOF bank IDs must be unique"),
1053 Self::MissingObservations => {
1054 formatter.write_str("every structural TOF bank requires observations")
1055 }
1056 Self::InvalidPhaseContract(reason) | Self::InvalidBankContract(reason) => {
1057 formatter.write_str(reason)
1058 }
1059 Self::InvalidSupport => {
1060 formatter.write_str("structural TOF support controls must be finite and positive")
1061 }
1062 Self::BankContractMismatch => formatter
1063 .write_str("structural TOF bank contract changed under the prepared layout"),
1064 Self::ParameterLengthMismatch => {
1065 formatter.write_str("structural TOF parameter value/direction length is wrong")
1066 }
1067 Self::BankWeightCountMismatch => formatter
1068 .write_str("structural TOF reverse products require one weight vector per bank"),
1069 Self::InvalidDamping => {
1070 formatter.write_str("structural TOF damping must be finite and non-negative")
1071 }
1072 Self::InternalInvariant => {
1073 formatter.write_str("structural TOF internal shape invariant failed")
1074 }
1075 Self::Pattern(error) => Display::fmt(error, formatter),
1076 Self::Rietveld(error) => Display::fmt(error, formatter),
1077 Self::StructuralParameters(error) => Display::fmt(error, formatter),
1078 Self::Parameter(error) => Display::fmt(error, formatter),
1079 Self::Background(error) => Display::fmt(error, formatter),
1080 Self::Structural(error) => Display::fmt(error, formatter),
1081 Self::Tof(error) => Display::fmt(error, formatter),
1082 Self::Residual(error) => Display::fmt(error, formatter),
1083 }
1084 }
1085}
1086
1087impl Error for StructuralTofMultiBankError {}
1088
1089impl From<DomainError> for StructuralTofMultiBankError {
1090 fn from(value: DomainError) -> Self {
1091 Self::Pattern(value)
1092 }
1093}
1094impl From<RietveldError> for StructuralTofMultiBankError {
1095 fn from(value: RietveldError) -> Self {
1096 Self::Rietveld(value)
1097 }
1098}
1099impl From<RietveldParameterError> for StructuralTofMultiBankError {
1100 fn from(value: RietveldParameterError) -> Self {
1101 Self::StructuralParameters(value)
1102 }
1103}
1104impl From<ParameterError> for StructuralTofMultiBankError {
1105 fn from(value: ParameterError) -> Self {
1106 Self::Parameter(value)
1107 }
1108}
1109impl From<TofLeBailError> for StructuralTofMultiBankError {
1110 fn from(value: TofLeBailError) -> Self {
1111 Self::Background(value)
1112 }
1113}
1114impl From<StructuralTofError> for StructuralTofMultiBankError {
1115 fn from(value: StructuralTofError) -> Self {
1116 Self::Structural(value)
1117 }
1118}
1119impl From<TofError> for StructuralTofMultiBankError {
1120 fn from(value: TofError) -> Self {
1121 Self::Tof(value)
1122 }
1123}
1124impl From<ResidualError> for StructuralTofMultiBankError {
1125 fn from(value: ResidualError) -> Self {
1126 Self::Residual(value)
1127 }
1128}