1use std::error::Error;
4use std::fmt::{Display, Formatter};
5use std::sync::Arc;
6
7use crate::fcj::{QUADRATURE_NODES, QUADRATURE_ORDER, QUADRATURE_WEIGHTS};
8use crate::profile::{
9 Accumulation, DenseJacobian, GridView, PatternDerivatives, ProfileError, SupportJacobian,
10 SupportRange, zeroed_f64_vec,
11};
12use crate::tch::{TchError, TchShape, TchWidths};
13use phasesmith_execution::ExecutionContext;
14
15const GAUSSIAN_FWHM_PER_SIGMA: f64 = 2.354_820_045_030_949_3;
16pub const TOF_GLOBAL_PARAMETER_COUNT: usize = 15;
19const LOCAL_PARAMETER_COUNT: usize = 2;
20const TOF_QUADRATURE_PANELS: usize = 8;
21const TOF_QUADRATURE_PANELS_F64: f64 = 8.0;
22const TOF_SUPPORT_QUADRATURE_PANELS: usize = 4;
23const TOF_SUPPORT_QUADRATURE_PANELS_F64: f64 = 4.0;
24const TOF_QUADRATURE_COUNT: usize = TOF_QUADRATURE_PANELS * QUADRATURE_ORDER;
25
26#[derive(Clone, Copy, Debug, PartialEq)]
28pub struct TofInstrument {
29 pub zero_us: f64,
31 pub difc_us_per_angstrom: f64,
33 pub difa_us_per_angstrom2: f64,
35 pub difb_us_angstrom: f64,
37 pub alpha_coefficient: f64,
39 pub beta0_per_us: f64,
41 pub beta1_angstrom4_per_us: f64,
43 pub betaq_angstrom2_per_us: f64,
45 pub sigma0_us2: f64,
47 pub sigma1_us2_per_angstrom2: f64,
49 pub sigma2_us2_per_angstrom4: f64,
51 pub sigmaq_us2_per_angstrom: f64,
53 pub x_us_per_angstrom: f64,
55 pub y_us_per_angstrom2: f64,
57 pub z_us: f64,
59}
60
61impl TofInstrument {
62 pub fn validate(self) -> Result<(), TofError> {
69 let values = [
70 self.zero_us,
71 self.difc_us_per_angstrom,
72 self.difa_us_per_angstrom2,
73 self.difb_us_angstrom,
74 self.alpha_coefficient,
75 self.beta0_per_us,
76 self.beta1_angstrom4_per_us,
77 self.betaq_angstrom2_per_us,
78 self.sigma0_us2,
79 self.sigma1_us2_per_angstrom2,
80 self.sigma2_us2_per_angstrom4,
81 self.sigmaq_us2_per_angstrom,
82 self.x_us_per_angstrom,
83 self.y_us_per_angstrom2,
84 self.z_us,
85 ];
86 if values.iter().any(|value| !value.is_finite()) {
87 return Err(TofError::NonFiniteInstrumentParameter);
88 }
89 if self.difc_us_per_angstrom <= 0.0 {
90 return Err(TofError::NonPositiveDifc);
91 }
92 Ok(())
93 }
94}
95
96#[derive(Clone, Copy, Debug, PartialEq)]
98pub struct TofProfileParameters {
99 pub position_us: f64,
101 pub alpha_per_us: f64,
103 pub beta_per_us: f64,
105 pub gaussian_variance_us2: f64,
107 pub gaussian_fwhm_us: f64,
109 pub lorentzian_fwhm_us: f64,
111 pub tch: TchShape,
113 pub d_position_d_d: f64,
115 pub d_alpha_d_d: f64,
117 pub d_beta_d_d: f64,
119 pub d_gaussian_fwhm_d_d: f64,
121 pub d_lorentzian_fwhm_d_d: f64,
123 pub d_position_d_instrument: [f64; TOF_GLOBAL_PARAMETER_COUNT],
125 pub d_alpha_d_instrument: [f64; TOF_GLOBAL_PARAMETER_COUNT],
127 pub d_beta_d_instrument: [f64; TOF_GLOBAL_PARAMETER_COUNT],
129 pub d_gaussian_fwhm_d_instrument: [f64; TOF_GLOBAL_PARAMETER_COUNT],
131 pub d_lorentzian_fwhm_d_instrument: [f64; TOF_GLOBAL_PARAMETER_COUNT],
133}
134
135impl TofProfileParameters {
136 pub fn from_instrument(d: f64, instrument: TofInstrument) -> Result<Self, TofError> {
143 instrument.validate()?;
144 if !d.is_finite() || d <= 0.0 {
145 return Err(TofError::InvalidDSpacing);
146 }
147 let d2 = d * d;
148 let d3 = d2 * d;
149 let d4 = d2 * d2;
150 let inverse_d = d.recip();
151 let inverse_d2 = inverse_d * inverse_d;
152 let inverse_d3 = inverse_d2 * inverse_d;
153 let inverse_d4 = inverse_d2 * inverse_d2;
154 let inverse_d5 = inverse_d4 * inverse_d;
155 let position_us = instrument.zero_us
156 + instrument.difc_us_per_angstrom * d
157 + instrument.difa_us_per_angstrom2 * d2
158 + instrument.difb_us_angstrom * inverse_d;
159 let alpha_per_us = instrument.alpha_coefficient * inverse_d;
160 let beta_per_us = instrument.beta0_per_us
161 + instrument.beta1_angstrom4_per_us * inverse_d4
162 + instrument.betaq_angstrom2_per_us * inverse_d2;
163 let gaussian_variance_us2 = instrument.sigma0_us2
164 + instrument.sigma1_us2_per_angstrom2 * d2
165 + instrument.sigma2_us2_per_angstrom4 * d4
166 + instrument.sigmaq_us2_per_angstrom * d;
167 let lorentzian_fwhm_us =
168 instrument.z_us + instrument.x_us_per_angstrom * d + instrument.y_us_per_angstrom2 * d2;
169 if !position_us.is_finite() {
170 return Err(TofError::InvalidPosition);
171 }
172 if !alpha_per_us.is_finite() || alpha_per_us <= 0.0 {
173 return Err(TofError::NonPositiveAlpha);
174 }
175 if !beta_per_us.is_finite() || beta_per_us <= 0.0 {
176 return Err(TofError::NonPositiveBeta);
177 }
178 if !gaussian_variance_us2.is_finite() || gaussian_variance_us2 <= 0.0 {
179 return Err(TofError::NonPositiveGaussianVariance);
180 }
181 if !lorentzian_fwhm_us.is_finite() || lorentzian_fwhm_us < 0.0 {
182 return Err(TofError::NegativeLorentzianFwhm);
183 }
184 let sigma = gaussian_variance_us2.sqrt();
185 let gaussian_fwhm_us = GAUSSIAN_FWHM_PER_SIGMA * sigma;
186 let tch = TchShape::from_component_fwhm(TchWidths {
187 gaussian_fwhm: gaussian_fwhm_us,
188 lorentzian_fwhm: lorentzian_fwhm_us,
189 })
190 .map_err(|reason| TofError::InvalidTch { reason })?;
191 let d_gaussian_d_variance = GAUSSIAN_FWHM_PER_SIGMA / (2.0 * sigma);
192 let d_variance_d_d = 2.0 * instrument.sigma1_us2_per_angstrom2 * d
193 + 4.0 * instrument.sigma2_us2_per_angstrom4 * d3
194 + instrument.sigmaq_us2_per_angstrom;
195
196 let mut d_position_d_instrument = [0.0; TOF_GLOBAL_PARAMETER_COUNT];
197 d_position_d_instrument[..4].copy_from_slice(&[1.0, d, d2, inverse_d]);
198 let mut d_alpha_d_instrument = [0.0; TOF_GLOBAL_PARAMETER_COUNT];
199 d_alpha_d_instrument[4] = inverse_d;
200 let mut d_beta_d_instrument = [0.0; TOF_GLOBAL_PARAMETER_COUNT];
201 d_beta_d_instrument[5..8].copy_from_slice(&[1.0, inverse_d4, inverse_d2]);
202 let mut d_gaussian_fwhm_d_instrument = [0.0; TOF_GLOBAL_PARAMETER_COUNT];
203 d_gaussian_fwhm_d_instrument[8..12].copy_from_slice(&[
204 d_gaussian_d_variance,
205 d_gaussian_d_variance * d2,
206 d_gaussian_d_variance * d4,
207 d_gaussian_d_variance * d,
208 ]);
209 let mut d_lorentzian_fwhm_d_instrument = [0.0; TOF_GLOBAL_PARAMETER_COUNT];
210 d_lorentzian_fwhm_d_instrument[12..15].copy_from_slice(&[d, d2, 1.0]);
211 Ok(Self {
212 position_us,
213 alpha_per_us,
214 beta_per_us,
215 gaussian_variance_us2,
216 gaussian_fwhm_us,
217 lorentzian_fwhm_us,
218 tch,
219 d_position_d_d: instrument.difc_us_per_angstrom
220 + 2.0 * instrument.difa_us_per_angstrom2 * d
221 - instrument.difb_us_angstrom * inverse_d2,
222 d_alpha_d_d: -instrument.alpha_coefficient * inverse_d2,
223 d_beta_d_d: -4.0 * instrument.beta1_angstrom4_per_us * inverse_d5
224 - 2.0 * instrument.betaq_angstrom2_per_us * inverse_d3,
225 d_gaussian_fwhm_d_d: d_gaussian_d_variance * d_variance_d_d,
226 d_lorentzian_fwhm_d_d: instrument.x_us_per_angstrom
227 + 2.0 * instrument.y_us_per_angstrom2 * d,
228 d_position_d_instrument,
229 d_alpha_d_instrument,
230 d_beta_d_instrument,
231 d_gaussian_fwhm_d_instrument,
232 d_lorentzian_fwhm_d_instrument,
233 })
234 }
235}
236
237#[derive(Clone, Copy, Debug, Default, PartialEq)]
239pub struct TofProfilePoint {
240 pub value: f64,
242 pub d_position: f64,
244 pub d_alpha: f64,
246 pub d_beta: f64,
248 pub d_gaussian_fwhm: f64,
250 pub d_lorentzian_fwhm: f64,
252}
253
254#[derive(Clone, Debug)]
256pub struct TofProfile {
257 shape: TchShape,
258 alpha: f64,
259 beta: f64,
260 quadrature: Arc<TofQuadrature>,
261}
262
263#[derive(Debug)]
264struct TofQuadrature {
265 tail_log: f64,
266 nodes: [f64; TOF_QUADRATURE_COUNT],
267 weights: [f64; TOF_QUADRATURE_COUNT],
268}
269
270impl TofQuadrature {
271 fn new(tail_log: f64) -> Result<Self, TofError> {
272 if !tail_log.is_finite() || tail_log <= 0.0 {
273 return Err(TofError::InvalidTailLog);
274 }
275 let mut nodes = [0.0; TOF_QUADRATURE_COUNT];
276 let mut weights = [0.0; TOF_QUADRATURE_COUNT];
277 let mut normalization = 0.0;
278 let panel_scale = TOF_QUADRATURE_PANELS_F64.recip();
279 let mut panel_offset = 0.0;
280 for panel in 0..TOF_QUADRATURE_PANELS {
281 for quadrature in 0..QUADRATURE_ORDER {
282 let index = panel * QUADRATURE_ORDER + quadrature;
283 let unit_node = panel_offset + panel_scale * QUADRATURE_NODES[quadrature];
284 nodes[index] = tail_log * unit_node;
285 weights[index] =
286 tail_log * panel_scale * QUADRATURE_WEIGHTS[quadrature] * (-nodes[index]).exp();
287 normalization += weights[index];
288 }
289 panel_offset += panel_scale;
290 }
291 if !normalization.is_finite() || normalization <= 0.0 {
292 return Err(TofError::InvalidQuadrature);
293 }
294 for weight in &mut weights {
295 *weight /= normalization;
296 }
297 Ok(Self {
298 tail_log,
299 nodes,
300 weights,
301 })
302 }
303}
304
305impl TofProfile {
306 pub fn new(
314 alpha_per_us: f64,
315 beta_per_us: f64,
316 widths: TchWidths,
317 tail_log: f64,
318 ) -> Result<Self, TofError> {
319 Self::validate_rates(alpha_per_us, beta_per_us)?;
320 let quadrature = Arc::new(TofQuadrature::new(tail_log)?);
321 Self::from_validated_rates(alpha_per_us, beta_per_us, widths, quadrature)
322 }
323
324 fn validate_rates(alpha_per_us: f64, beta_per_us: f64) -> Result<(), TofError> {
325 if !alpha_per_us.is_finite() || alpha_per_us <= 0.0 {
326 return Err(TofError::NonPositiveAlpha);
327 }
328 if !beta_per_us.is_finite() || beta_per_us <= 0.0 {
329 return Err(TofError::NonPositiveBeta);
330 }
331 Ok(())
332 }
333
334 fn from_validated_rates(
335 alpha_per_us: f64,
336 beta_per_us: f64,
337 widths: TchWidths,
338 quadrature: Arc<TofQuadrature>,
339 ) -> Result<Self, TofError> {
340 let shape = TchShape::from_component_fwhm(widths)
341 .map_err(|reason| TofError::InvalidTch { reason })?;
342 Ok(Self {
343 shape,
344 alpha: alpha_per_us,
345 beta: beta_per_us,
346 quadrature,
347 })
348 }
349
350 #[must_use]
352 pub fn evaluate(&self, x_minus_position_us: f64) -> TofProfilePoint {
353 self.evaluate_with_radius(x_minus_position_us, f64::INFINITY)
354 }
355
356 fn evaluate_with_radius(&self, delta: f64, base_radius: f64) -> TofProfilePoint {
357 if base_radius.is_finite() {
358 return self.evaluate_supported(delta, base_radius);
359 }
360 let sum = self.alpha + self.beta;
361 let left_fraction = self.beta / sum;
362 let right_fraction = self.alpha / sum;
363 let d_left_d_alpha = -self.beta / (sum * sum);
364 let d_left_d_beta = self.alpha / (sum * sum);
365 let mut left = TofProfilePoint::default();
366 let mut right = TofProfilePoint::default();
367 let mut left_alpha_shift = 0.0;
368 let mut right_beta_shift = 0.0;
369 for index in 0..TOF_QUADRATURE_COUNT {
370 let node = self.quadrature.nodes[index];
371 let weight = self.quadrature.weights[index];
372 let left_delta = delta + node / self.alpha;
373 if left_delta.abs() <= base_radius {
374 let point = self.shape.evaluate(left_delta);
375 left.value += weight * point.value;
376 left.d_position += weight * point.d_delta;
377 left.d_gaussian_fwhm += weight * point.d_gaussian_fwhm;
378 left.d_lorentzian_fwhm += weight * point.d_lorentzian_fwhm;
379 left_alpha_shift += weight * point.d_delta * (-node / self.alpha.powi(2));
380 }
381 let right_delta = delta - node / self.beta;
382 if right_delta.abs() <= base_radius {
383 let point = self.shape.evaluate(right_delta);
384 right.value += weight * point.value;
385 right.d_position += weight * point.d_delta;
386 right.d_gaussian_fwhm += weight * point.d_gaussian_fwhm;
387 right.d_lorentzian_fwhm += weight * point.d_lorentzian_fwhm;
388 right_beta_shift += weight * point.d_delta * (node / self.beta.powi(2));
389 }
390 }
391 TofProfilePoint {
392 value: left_fraction * left.value + right_fraction * right.value,
393 d_position: -(left_fraction * left.d_position + right_fraction * right.d_position),
394 d_alpha: d_left_d_alpha * left.value + left_fraction * left_alpha_shift
395 - d_left_d_alpha * right.value,
396 d_beta: d_left_d_beta * left.value + right_fraction * right_beta_shift
397 - d_left_d_beta * right.value,
398 d_gaussian_fwhm: left_fraction * left.d_gaussian_fwhm
399 + right_fraction * right.d_gaussian_fwhm,
400 d_lorentzian_fwhm: left_fraction * left.d_lorentzian_fwhm
401 + right_fraction * right.d_lorentzian_fwhm,
402 }
403 }
404
405 fn evaluate_supported(&self, delta: f64, base_radius: f64) -> TofProfilePoint {
406 let sum = self.alpha + self.beta;
407 let left_fraction = self.beta / sum;
408 let right_fraction = self.alpha / sum;
409 let d_left_d_alpha = -self.beta / (sum * sum);
410 let d_left_d_beta = self.alpha / (sum * sum);
411 let tail_log = self.quadrature.tail_log;
412 let normalization = 1.0 - (-tail_log).exp();
413 let left_low = (self.alpha * (-base_radius - delta)).clamp(0.0, tail_log);
414 let left_high = (self.alpha * (base_radius - delta)).clamp(0.0, tail_log);
415 let right_low = (self.beta * (delta - base_radius)).clamp(0.0, tail_log);
416 let right_high = (self.beta * (delta + base_radius)).clamp(0.0, tail_log);
417 let mut left = TofProfilePoint::default();
418 let mut right = TofProfilePoint::default();
419 let mut left_alpha_shift = 0.0;
420 let mut right_beta_shift = 0.0;
421
422 if left_low < left_high {
423 let panel_width = (left_high - left_low) / TOF_SUPPORT_QUADRATURE_PANELS_F64;
424 let mut panel_left = left_low;
425 for _ in 0..TOF_SUPPORT_QUADRATURE_PANELS {
426 for quadrature in 0..QUADRATURE_ORDER {
427 let node = panel_left + panel_width * QUADRATURE_NODES[quadrature];
428 let weight = panel_width * QUADRATURE_WEIGHTS[quadrature] * (-node).exp()
429 / normalization;
430 let point = self.shape.evaluate(delta + node / self.alpha);
431 left.value += weight * point.value;
432 left.d_position += weight * point.d_delta;
433 left.d_gaussian_fwhm += weight * point.d_gaussian_fwhm;
434 left.d_lorentzian_fwhm += weight * point.d_lorentzian_fwhm;
435 left_alpha_shift += weight * point.d_delta * (-node / self.alpha.powi(2));
436 }
437 panel_left += panel_width;
438 }
439 }
440 if right_low < right_high {
441 let panel_width = (right_high - right_low) / TOF_SUPPORT_QUADRATURE_PANELS_F64;
442 let mut panel_left = right_low;
443 for _ in 0..TOF_SUPPORT_QUADRATURE_PANELS {
444 for quadrature in 0..QUADRATURE_ORDER {
445 let node = panel_left + panel_width * QUADRATURE_NODES[quadrature];
446 let weight = panel_width * QUADRATURE_WEIGHTS[quadrature] * (-node).exp()
447 / normalization;
448 let point = self.shape.evaluate(delta - node / self.beta);
449 right.value += weight * point.value;
450 right.d_position += weight * point.d_delta;
451 right.d_gaussian_fwhm += weight * point.d_gaussian_fwhm;
452 right.d_lorentzian_fwhm += weight * point.d_lorentzian_fwhm;
453 right_beta_shift += weight * point.d_delta * (node / self.beta.powi(2));
454 }
455 panel_left += panel_width;
456 }
457 }
458 TofProfilePoint {
459 value: left_fraction * left.value + right_fraction * right.value,
460 d_position: -(left_fraction * left.d_position + right_fraction * right.d_position),
461 d_alpha: d_left_d_alpha * left.value + left_fraction * left_alpha_shift
462 - d_left_d_alpha * right.value,
463 d_beta: d_left_d_beta * left.value + right_fraction * right_beta_shift
464 - d_left_d_beta * right.value,
465 d_gaussian_fwhm: left_fraction * left.d_gaussian_fwhm
466 + right_fraction * right.d_gaussian_fwhm,
467 d_lorentzian_fwhm: left_fraction * left.d_lorentzian_fwhm
468 + right_fraction * right.d_lorentzian_fwhm,
469 }
470 }
471
472 fn support_range(&self, position: f64, base_radius: f64) -> SupportRange {
473 SupportRange {
474 left: position - base_radius - self.quadrature.tail_log / self.alpha,
475 right: position + base_radius + self.quadrature.tail_log / self.beta,
476 }
477 }
478}
479
480#[derive(Clone, Debug, PartialEq)]
482pub enum TofError {
483 NonFiniteInstrumentParameter,
485 NonPositiveDifc,
487 InvalidDSpacing,
489 InvalidPosition,
491 NonPositiveAlpha,
493 NonPositiveBeta,
495 NonPositiveGaussianVariance,
497 NegativeLorentzianFwhm,
499 InvalidTailLog,
501 InvalidQuadrature,
503 InvalidTch {
505 reason: TchError,
507 },
508 LengthMismatch,
510 NonFiniteIntensity {
512 reflection: usize,
514 },
515 AllocationOverflow,
517 Profile {
519 reason: ProfileError,
521 },
522}
523
524impl Display for TofError {
525 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
526 match self {
527 Self::NonFiniteInstrumentParameter => {
528 write!(formatter, "TOF coefficients must be finite")
529 }
530 Self::NonPositiveDifc => write!(formatter, "difC must be positive"),
531 Self::InvalidDSpacing => write!(formatter, "d-spacing must be positive and finite"),
532 Self::InvalidPosition => write!(formatter, "derived TOF position must be finite"),
533 Self::NonPositiveAlpha => write!(formatter, "TOF alpha must be positive and finite"),
534 Self::NonPositiveBeta => write!(formatter, "TOF beta must be positive and finite"),
535 Self::NonPositiveGaussianVariance => write!(
536 formatter,
537 "derived TOF Gaussian variance must be positive and finite"
538 ),
539 Self::NegativeLorentzianFwhm => write!(
540 formatter,
541 "derived TOF Lorentzian FWHM must be non-negative and finite"
542 ),
543 Self::InvalidTailLog => write!(formatter, "tail_log must be positive and finite"),
544 Self::InvalidQuadrature => write!(formatter, "TOF quadrature normalization is invalid"),
545 Self::InvalidTch { reason } => write!(formatter, "invalid TOF TCH widths: {reason}"),
546 Self::LengthMismatch => {
547 write!(formatter, "TOF reflection arrays must have equal length")
548 }
549 Self::NonFiniteIntensity { reflection } => write!(
550 formatter,
551 "reflection {reflection} intensity must be finite"
552 ),
553 Self::AllocationOverflow => write!(formatter, "TOF allocation size overflow"),
554 Self::Profile { reason } => Display::fmt(reason, formatter),
555 }
556 }
557}
558
559impl Error for TofError {}
560
561impl From<ProfileError> for TofError {
562 fn from(reason: ProfileError) -> Self {
563 Self::Profile { reason }
564 }
565}
566
567struct PreparedReflection {
568 parameters: TofProfileParameters,
569 profile: TofProfile,
570}
571
572struct TofReflectionBlock {
573 start: usize,
574 y: Vec<f64>,
575 local: Vec<f64>,
576 global: Vec<f64>,
577}
578
579#[allow(clippy::too_many_lines)]
586pub fn accumulate_tof_batch(
587 grid: GridView<'_>,
588 d_spacings: &[f64],
589 intensities: &[f64],
590 instrument: TofInstrument,
591 support_fwhm: f64,
592 tail_log: f64,
593) -> Result<Accumulation, TofError> {
594 accumulate_tof_batch_with_context(
595 grid,
596 d_spacings,
597 intensities,
598 instrument,
599 support_fwhm,
600 tail_log,
601 &ExecutionContext::serial(),
602 )
603}
604
605#[allow(clippy::too_many_lines)]
611pub fn accumulate_tof_batch_with_context(
612 grid: GridView<'_>,
613 d_spacings: &[f64],
614 intensities: &[f64],
615 instrument: TofInstrument,
616 support_fwhm: f64,
617 tail_log: f64,
618 execution: &ExecutionContext,
619) -> Result<Accumulation, TofError> {
620 if d_spacings.len() != intensities.len() {
621 return Err(TofError::LengthMismatch);
622 }
623 if !support_fwhm.is_finite() || support_fwhm <= 0.0 {
624 return Err(TofError::Profile {
625 reason: ProfileError::InvalidSupport,
626 });
627 }
628 instrument.validate()?;
629 let x = grid.as_slice();
630 let count = d_spacings.len();
631 let mut prepared = Vec::new();
632 let mut starts = Vec::new();
633 let mut offsets = Vec::new();
634 prepared
635 .try_reserve_exact(count)
636 .map_err(|_| TofError::AllocationOverflow)?;
637 starts
638 .try_reserve_exact(count)
639 .map_err(|_| TofError::AllocationOverflow)?;
640 let offset_count = count.checked_add(1).ok_or(TofError::AllocationOverflow)?;
641 offsets
642 .try_reserve_exact(offset_count)
643 .map_err(|_| TofError::AllocationOverflow)?;
644 offsets.push(0usize);
645 let mut shared_quadrature = None;
646 for reflection in 0..count {
647 if !intensities[reflection].is_finite() {
648 return Err(TofError::NonFiniteIntensity { reflection });
649 }
650 let parameters = TofProfileParameters::from_instrument(d_spacings[reflection], instrument)?;
651 TofProfile::validate_rates(parameters.alpha_per_us, parameters.beta_per_us)?;
652 let quadrature = if let Some(quadrature) = &shared_quadrature {
653 Arc::clone(quadrature)
654 } else {
655 let quadrature = Arc::new(TofQuadrature::new(tail_log)?);
656 shared_quadrature = Some(Arc::clone(&quadrature));
657 quadrature
658 };
659 let profile = TofProfile::from_validated_rates(
660 parameters.alpha_per_us,
661 parameters.beta_per_us,
662 TchWidths {
663 gaussian_fwhm: parameters.gaussian_fwhm_us,
664 lorentzian_fwhm: parameters.lorentzian_fwhm_us,
665 },
666 quadrature,
667 )?;
668 let base_radius = support_fwhm * parameters.tch.total_fwhm;
669 let range = profile.support_range(parameters.position_us, base_radius);
670 let lower = x.partition_point(|value| *value < range.left);
671 let upper = x.partition_point(|value| *value <= range.right);
672 offsets.push(
673 offsets[reflection]
674 .checked_add(upper - lower)
675 .ok_or(TofError::AllocationOverflow)?,
676 );
677 starts.push(lower);
678 prepared.push(PreparedReflection {
679 parameters,
680 profile,
681 });
682 }
683 let active = offsets.last().copied().unwrap_or(0);
684 let mut y = zeroed_f64_vec(x.len())?;
685 let mut local = zeroed_f64_vec(
686 active
687 .checked_mul(LOCAL_PARAMETER_COUNT)
688 .ok_or(TofError::AllocationOverflow)?,
689 )?;
690 let mut global = zeroed_f64_vec(
691 TOF_GLOBAL_PARAMETER_COUNT
692 .checked_mul(x.len())
693 .ok_or(TofError::AllocationOverflow)?,
694 )?;
695 if execution.threads() == 1 || count < 16 {
696 for reflection in 0..count {
697 let item = &prepared[reflection];
698 let intensity = intensities[reflection];
699 let base_radius = support_fwhm * item.parameters.tch.total_fwhm;
700 let begin = offsets[reflection];
701 let end = offsets[reflection + 1];
702 for active_index in begin..end {
703 let sample = starts[reflection] + active_index - begin;
704 let point = item
705 .profile
706 .evaluate_with_radius(x[sample] - item.parameters.position_us, base_radius);
707 y[sample] += intensity * point.value;
708 local[active_index * LOCAL_PARAMETER_COUNT] = point.value;
709 local[active_index * LOCAL_PARAMETER_COUNT + 1] = intensity
710 * (point.d_position * item.parameters.d_position_d_d
711 + point.d_alpha * item.parameters.d_alpha_d_d
712 + point.d_beta * item.parameters.d_beta_d_d
713 + point.d_gaussian_fwhm * item.parameters.d_gaussian_fwhm_d_d
714 + point.d_lorentzian_fwhm * item.parameters.d_lorentzian_fwhm_d_d);
715 for parameter in 0..TOF_GLOBAL_PARAMETER_COUNT {
716 let derivative = point.d_position
717 * item.parameters.d_position_d_instrument[parameter]
718 + point.d_alpha * item.parameters.d_alpha_d_instrument[parameter]
719 + point.d_beta * item.parameters.d_beta_d_instrument[parameter]
720 + point.d_gaussian_fwhm
721 * item.parameters.d_gaussian_fwhm_d_instrument[parameter]
722 + point.d_lorentzian_fwhm
723 * item.parameters.d_lorentzian_fwhm_d_instrument[parameter];
724 global[parameter * x.len() + sample] += intensity * derivative;
725 }
726 }
727 }
728 } else {
729 let blocks = execution.map_ordered(count, 16, |reflection| {
730 let item = &prepared[reflection];
731 let intensity = intensities[reflection];
732 let base_radius = support_fwhm * item.parameters.tch.total_fwhm;
733 let begin = offsets[reflection];
734 let end = offsets[reflection + 1];
735 let support_count = end - begin;
736 let mut block = TofReflectionBlock {
737 start: starts[reflection],
738 y: vec![0.0; support_count],
739 local: vec![0.0; support_count * LOCAL_PARAMETER_COUNT],
740 global: vec![0.0; support_count * TOF_GLOBAL_PARAMETER_COUNT],
741 };
742 for support_index in 0..support_count {
743 let sample = block.start + support_index;
744 let point = item
745 .profile
746 .evaluate_with_radius(x[sample] - item.parameters.position_us, base_radius);
747 block.y[support_index] = intensity * point.value;
748 block.local[support_index * LOCAL_PARAMETER_COUNT] = point.value;
749 block.local[support_index * LOCAL_PARAMETER_COUNT + 1] = intensity
750 * (point.d_position * item.parameters.d_position_d_d
751 + point.d_alpha * item.parameters.d_alpha_d_d
752 + point.d_beta * item.parameters.d_beta_d_d
753 + point.d_gaussian_fwhm * item.parameters.d_gaussian_fwhm_d_d
754 + point.d_lorentzian_fwhm * item.parameters.d_lorentzian_fwhm_d_d);
755 for parameter in 0..TOF_GLOBAL_PARAMETER_COUNT {
756 let derivative = point.d_position
757 * item.parameters.d_position_d_instrument[parameter]
758 + point.d_alpha * item.parameters.d_alpha_d_instrument[parameter]
759 + point.d_beta * item.parameters.d_beta_d_instrument[parameter]
760 + point.d_gaussian_fwhm
761 * item.parameters.d_gaussian_fwhm_d_instrument[parameter]
762 + point.d_lorentzian_fwhm
763 * item.parameters.d_lorentzian_fwhm_d_instrument[parameter];
764 block.global[parameter * support_count + support_index] =
765 intensity * derivative;
766 }
767 }
768 block
769 });
770 for (reflection, block) in blocks.into_iter().enumerate() {
771 let begin = offsets[reflection];
772 let support_count = block.y.len();
773 let local_begin = begin * LOCAL_PARAMETER_COUNT;
774 let local_end = local_begin + block.local.len();
775 local[local_begin..local_end].copy_from_slice(&block.local);
776 for support_index in 0..support_count {
777 let sample = block.start + support_index;
778 y[sample] += block.y[support_index];
779 for parameter in 0..TOF_GLOBAL_PARAMETER_COUNT {
780 global[parameter * x.len() + sample] +=
781 block.global[parameter * support_count + support_index];
782 }
783 }
784 }
785 }
786 Ok(Accumulation {
787 y,
788 derivatives: PatternDerivatives {
789 local: SupportJacobian {
790 starts,
791 offsets,
792 values: local,
793 parameter_count: LOCAL_PARAMETER_COUNT,
794 },
795 global: Some(DenseJacobian {
796 values: global,
797 parameter_count: TOF_GLOBAL_PARAMETER_COUNT,
798 sample_count: x.len(),
799 }),
800 },
801 sample_count: x.len(),
802 })
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808
809 fn instrument() -> TofInstrument {
810 TofInstrument {
811 zero_us: -0.773_346_536_757,
812 difc_us_per_angstrom: 5_084.827_630_65,
813 difa_us_per_angstrom2: -2.630_417_748_6,
814 difb_us_angstrom: 0.0,
815 alpha_coefficient: 5.0,
816 beta0_per_us: 0.033_276_398_966_5,
817 beta1_angstrom4_per_us: 0.000_964_057_827_372,
818 betaq_angstrom2_per_us: 0.0,
819 sigma0_us2: 0.0,
820 sigma1_us2_per_angstrom2: 15.140_286_726_8,
821 sigma2_us2_per_angstrom4: 0.0,
822 sigmaq_us2_per_angstrom: 0.0,
823 x_us_per_angstrom: 0.0,
824 y_us_per_angstrom2: 0.0,
825 z_us: 0.0,
826 }
827 }
828
829 #[test]
830 fn parameter_chains_match_centered_differences() {
831 let d = 1.7;
832 let step = 1.0e-6;
833 let actual = TofProfileParameters::from_instrument(d, instrument()).expect("parameters");
834 let plus = TofProfileParameters::from_instrument(d + step, instrument()).expect("plus");
835 let minus = TofProfileParameters::from_instrument(d - step, instrument()).expect("minus");
836 let finite = |high: f64, low: f64| (high - low) / (2.0 * step);
837 assert!((actual.d_position_d_d - finite(plus.position_us, minus.position_us)).abs() < 1e-6);
838 assert!((actual.d_alpha_d_d - finite(plus.alpha_per_us, minus.alpha_per_us)).abs() < 1e-9);
839 assert!((actual.d_beta_d_d - finite(plus.beta_per_us, minus.beta_per_us)).abs() < 1e-9);
840 assert!(
841 (actual.d_gaussian_fwhm_d_d - finite(plus.gaussian_fwhm_us, minus.gaussian_fwhm_us))
842 .abs()
843 < 1e-8
844 );
845 }
846
847 #[test]
848 fn profiles_can_share_quadrature_storage() {
849 let quadrature = Arc::new(TofQuadrature::new(20.0).expect("quadrature"));
850 let widths = TchWidths {
851 gaussian_fwhm: 22.0,
852 lorentzian_fwhm: 4.0,
853 };
854 let first = TofProfile::from_validated_rates(0.08, 0.03, widths, Arc::clone(&quadrature))
855 .expect("first profile");
856 let second = TofProfile::from_validated_rates(0.09, 0.04, widths, Arc::clone(&quadrature))
857 .expect("second profile");
858
859 assert!(Arc::ptr_eq(&first.quadrature, &second.quadrature));
860 assert!(std::mem::size_of::<TofProfile>() < 128);
861 }
862
863 #[test]
864 fn accumulation_blocks_are_bitwise_identical_across_worker_counts() {
865 let x = (0..=8_000)
866 .map(|index| 1_000.0 + 2.0 * f64::from(index))
867 .collect::<Vec<_>>();
868 let d_spacings = (0..36)
869 .map(|index| 0.5 + 0.065 * f64::from(index))
870 .collect::<Vec<_>>();
871 let intensities = (0..36)
872 .map(|index| 3.0 + 0.3 * f64::from(index))
873 .collect::<Vec<_>>();
874 let grid = GridView::new(&x).expect("grid");
875 let serial = ExecutionContext::serial();
876 let expected = accumulate_tof_batch_with_context(
877 grid,
878 &d_spacings,
879 &intensities,
880 instrument(),
881 20.0,
882 20.0,
883 &serial,
884 )
885 .expect("serial TOF");
886 for threads in [2, 3] {
887 let context = ExecutionContext::new(threads).expect("parallel context");
888 assert_eq!(
889 accumulate_tof_batch_with_context(
890 grid,
891 &d_spacings,
892 &intensities,
893 instrument(),
894 20.0,
895 20.0,
896 &context,
897 )
898 .expect("parallel TOF"),
899 expected
900 );
901 }
902 }
903
904 #[test]
905 fn direct_profile_derivatives_match_centered_differences() {
906 let alpha = 0.08;
907 let beta = 0.03;
908 let gaussian = 22.0;
909 let lorentzian = 4.0;
910 let delta = 5.0;
911 let tail = 20.0;
912 let point = TofProfile::new(
913 alpha,
914 beta,
915 TchWidths {
916 gaussian_fwhm: gaussian,
917 lorentzian_fwhm: lorentzian,
918 },
919 tail,
920 )
921 .expect("profile")
922 .evaluate(delta);
923 let step = 1.0e-6;
924 let value = |a, b, g, l, x| {
925 TofProfile::new(
926 a,
927 b,
928 TchWidths {
929 gaussian_fwhm: g,
930 lorentzian_fwhm: l,
931 },
932 tail,
933 )
934 .expect("profile")
935 .evaluate(x)
936 .value
937 };
938 let fd = |plus, minus| (plus - minus) / (2.0 * step);
939 assert!(
940 (point.d_position
941 - fd(
942 value(alpha, beta, gaussian, lorentzian, delta - step),
943 value(alpha, beta, gaussian, lorentzian, delta + step)
944 ))
945 .abs()
946 < 1e-8
947 );
948 assert!(
949 (point.d_alpha
950 - fd(
951 value(alpha + step, beta, gaussian, lorentzian, delta),
952 value(alpha - step, beta, gaussian, lorentzian, delta)
953 ))
954 .abs()
955 < 1e-7
956 );
957 assert!(
958 (point.d_beta
959 - fd(
960 value(alpha, beta + step, gaussian, lorentzian, delta),
961 value(alpha, beta - step, gaussian, lorentzian, delta)
962 ))
963 .abs()
964 < 1e-7
965 );
966 assert!(
967 (point.d_gaussian_fwhm
968 - fd(
969 value(alpha, beta, gaussian + step, lorentzian, delta),
970 value(alpha, beta, gaussian - step, lorentzian, delta)
971 ))
972 .abs()
973 < 1e-8
974 );
975 assert!(
976 (point.d_lorentzian_fwhm
977 - fd(
978 value(alpha, beta, gaussian, lorentzian + step, delta),
979 value(alpha, beta, gaussian, lorentzian - step, delta)
980 ))
981 .abs()
982 < 1e-8
983 );
984 }
985}