1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use crate::cw::{ConstantWavelengthInstrument, CwBatchError, CwError, CwProfileParameters};
7use crate::fcj::{FcjError, FcjGeometry, FcjProfile, FcjProfilePoint};
8use crate::profile::{
9 Accumulation, DenseJacobian, GridView, PatternDerivatives, ProfileError, SupportJacobian,
10 SupportPolicy, zeroed_f64_vec,
11};
12use crate::tch::{TchShape, TchWidths};
13use phasesmith_execution::ExecutionContext;
14
15const GAUSSIAN_FWHM_PER_SIGMA: f64 = 2.354_820_045_030_949_3;
16const INSTRUMENT_PARAMETER_COUNT: usize = 5;
17const FCJ_PARAMETER_COUNT: usize = 2;
18const LOCAL_PARAMETER_COUNT: usize = 2;
19
20#[derive(Clone, Copy, Debug)]
22pub struct CwContributionsView<'a> {
23 gaussian_variance_deg2: &'a [f64],
24 lorentzian_fwhm_deg: &'a [f64],
25 intensity_multiplier: &'a [f64],
26 d_gaussian_variance_d_position: &'a [f64],
27 d_lorentzian_fwhm_d_position: &'a [f64],
28 d_intensity_multiplier_d_position: &'a [f64],
29 d_gaussian_variance_d_parameters: &'a [f64],
30 d_lorentzian_fwhm_d_parameters: &'a [f64],
31 d_intensity_multiplier_d_parameters: &'a [f64],
32 parameter_count: usize,
33 reflection_count: usize,
34}
35
36#[derive(Clone, Copy, Debug)]
38pub struct CwContributionArrays<'a> {
39 pub gaussian_variance_deg2: &'a [f64],
41 pub lorentzian_fwhm_deg: &'a [f64],
43 pub intensity_multiplier: &'a [f64],
45 pub d_gaussian_variance_d_position: &'a [f64],
47 pub d_lorentzian_fwhm_d_position: &'a [f64],
49 pub d_intensity_multiplier_d_position: &'a [f64],
51 pub d_gaussian_variance_d_parameters: &'a [f64],
53 pub d_lorentzian_fwhm_d_parameters: &'a [f64],
55 pub d_intensity_multiplier_d_parameters: &'a [f64],
57}
58
59#[derive(Clone, Debug, Default, PartialEq)]
61pub struct OwnedCwContributionArrays {
62 pub gaussian_variance_deg2: Vec<f64>,
64 pub lorentzian_fwhm_deg: Vec<f64>,
66 pub intensity_multiplier: Vec<f64>,
68 pub d_gaussian_variance_d_position: Vec<f64>,
70 pub d_lorentzian_fwhm_d_position: Vec<f64>,
72 pub d_intensity_multiplier_d_position: Vec<f64>,
74 pub d_gaussian_variance_d_parameters: Vec<f64>,
76 pub d_lorentzian_fwhm_d_parameters: Vec<f64>,
78 pub d_intensity_multiplier_d_parameters: Vec<f64>,
80}
81
82impl OwnedCwContributionArrays {
83 fn as_borrowed(&self) -> CwContributionArrays<'_> {
84 CwContributionArrays {
85 gaussian_variance_deg2: &self.gaussian_variance_deg2,
86 lorentzian_fwhm_deg: &self.lorentzian_fwhm_deg,
87 intensity_multiplier: &self.intensity_multiplier,
88 d_gaussian_variance_d_position: &self.d_gaussian_variance_d_position,
89 d_lorentzian_fwhm_d_position: &self.d_lorentzian_fwhm_d_position,
90 d_intensity_multiplier_d_position: &self.d_intensity_multiplier_d_position,
91 d_gaussian_variance_d_parameters: &self.d_gaussian_variance_d_parameters,
92 d_lorentzian_fwhm_d_parameters: &self.d_lorentzian_fwhm_d_parameters,
93 d_intensity_multiplier_d_parameters: &self.d_intensity_multiplier_d_parameters,
94 }
95 }
96}
97
98#[derive(Clone, Debug, PartialEq)]
100pub struct OwnedCwContributions {
101 reflection_count: usize,
102 parameter_count: usize,
103 arrays: OwnedCwContributionArrays,
104}
105
106impl OwnedCwContributions {
107 pub fn new(
114 reflection_count: usize,
115 parameter_count: usize,
116 arrays: OwnedCwContributionArrays,
117 ) -> Result<Self, CwContributionsError> {
118 CwContributionsView::new(reflection_count, parameter_count, arrays.as_borrowed())?;
119 Ok(Self {
120 reflection_count,
121 parameter_count,
122 arrays,
123 })
124 }
125
126 #[must_use]
128 pub fn neutral(reflection_count: usize) -> Self {
129 Self {
130 reflection_count,
131 parameter_count: 0,
132 arrays: OwnedCwContributionArrays {
133 gaussian_variance_deg2: vec![0.0; reflection_count],
134 lorentzian_fwhm_deg: vec![0.0; reflection_count],
135 intensity_multiplier: vec![1.0; reflection_count],
136 d_gaussian_variance_d_position: vec![0.0; reflection_count],
137 d_lorentzian_fwhm_d_position: vec![0.0; reflection_count],
138 d_intensity_multiplier_d_position: vec![0.0; reflection_count],
139 ..OwnedCwContributionArrays::default()
140 },
141 }
142 }
143
144 #[must_use]
146 pub fn as_view(&self) -> CwContributionsView<'_> {
147 CwContributionsView {
148 gaussian_variance_deg2: &self.arrays.gaussian_variance_deg2,
149 lorentzian_fwhm_deg: &self.arrays.lorentzian_fwhm_deg,
150 intensity_multiplier: &self.arrays.intensity_multiplier,
151 d_gaussian_variance_d_position: &self.arrays.d_gaussian_variance_d_position,
152 d_lorentzian_fwhm_d_position: &self.arrays.d_lorentzian_fwhm_d_position,
153 d_intensity_multiplier_d_position: &self.arrays.d_intensity_multiplier_d_position,
154 d_gaussian_variance_d_parameters: &self.arrays.d_gaussian_variance_d_parameters,
155 d_lorentzian_fwhm_d_parameters: &self.arrays.d_lorentzian_fwhm_d_parameters,
156 d_intensity_multiplier_d_parameters: &self.arrays.d_intensity_multiplier_d_parameters,
157 parameter_count: self.parameter_count,
158 reflection_count: self.reflection_count,
159 }
160 }
161
162 #[must_use]
164 pub const fn reflection_count(&self) -> usize {
165 self.reflection_count
166 }
167
168 #[must_use]
170 pub const fn parameter_count(&self) -> usize {
171 self.parameter_count
172 }
173
174 #[must_use]
176 pub const fn arrays(&self) -> &OwnedCwContributionArrays {
177 &self.arrays
178 }
179}
180
181fn validate_reflection_arrays(
182 reflection_count: usize,
183 arrays: CwContributionArrays<'_>,
184) -> Result<(), CwContributionsError> {
185 for (name, values) in [
186 ("gaussian_variance_deg2", arrays.gaussian_variance_deg2),
187 ("lorentzian_fwhm_deg", arrays.lorentzian_fwhm_deg),
188 ("intensity_multiplier", arrays.intensity_multiplier),
189 (
190 "d_gaussian_variance_d_position",
191 arrays.d_gaussian_variance_d_position,
192 ),
193 (
194 "d_lorentzian_fwhm_d_position",
195 arrays.d_lorentzian_fwhm_d_position,
196 ),
197 (
198 "d_intensity_multiplier_d_position",
199 arrays.d_intensity_multiplier_d_position,
200 ),
201 ] {
202 if values.len() != reflection_count {
203 return Err(CwContributionsError::LengthMismatch { name });
204 }
205 }
206 for reflection in 0..reflection_count {
207 for (quantity, value, non_negative) in [
208 (
209 "gaussian_variance_deg2",
210 arrays.gaussian_variance_deg2[reflection],
211 true,
212 ),
213 (
214 "lorentzian_fwhm_deg",
215 arrays.lorentzian_fwhm_deg[reflection],
216 true,
217 ),
218 (
219 "intensity_multiplier",
220 arrays.intensity_multiplier[reflection],
221 true,
222 ),
223 (
224 "d_gaussian_variance_d_position",
225 arrays.d_gaussian_variance_d_position[reflection],
226 false,
227 ),
228 (
229 "d_lorentzian_fwhm_d_position",
230 arrays.d_lorentzian_fwhm_d_position[reflection],
231 false,
232 ),
233 (
234 "d_intensity_multiplier_d_position",
235 arrays.d_intensity_multiplier_d_position[reflection],
236 false,
237 ),
238 ] {
239 if !value.is_finite() || (non_negative && value < 0.0) {
240 return Err(CwContributionsError::InvalidContribution {
241 reflection,
242 quantity,
243 });
244 }
245 }
246 }
247 Ok(())
248}
249
250fn validate_parameter_arrays(
251 reflection_count: usize,
252 parameter_count: usize,
253 arrays: CwContributionArrays<'_>,
254) -> Result<(), CwContributionsError> {
255 let derivative_count = parameter_count
256 .checked_mul(reflection_count)
257 .ok_or(CwContributionsError::AllocationOverflow)?;
258 let derivative_arrays = [
259 (
260 "d_gaussian_variance_d_parameters",
261 arrays.d_gaussian_variance_d_parameters,
262 ),
263 (
264 "d_lorentzian_fwhm_d_parameters",
265 arrays.d_lorentzian_fwhm_d_parameters,
266 ),
267 (
268 "d_intensity_multiplier_d_parameters",
269 arrays.d_intensity_multiplier_d_parameters,
270 ),
271 ];
272 for (name, values) in derivative_arrays {
273 if values.len() != derivative_count {
274 return Err(CwContributionsError::LengthMismatch { name });
275 }
276 if let Some(index) = values.iter().position(|value| !value.is_finite()) {
277 return Err(CwContributionsError::InvalidDerivative {
278 parameter: index / reflection_count.max(1),
279 reflection: index % reflection_count.max(1),
280 quantity: name,
281 });
282 }
283 }
284 Ok(())
285}
286
287impl<'a> CwContributionsView<'a> {
288 pub fn new(
295 reflection_count: usize,
296 parameter_count: usize,
297 arrays: CwContributionArrays<'a>,
298 ) -> Result<Self, CwContributionsError> {
299 validate_reflection_arrays(reflection_count, arrays)?;
300 validate_parameter_arrays(reflection_count, parameter_count, arrays)?;
301 Ok(Self {
302 gaussian_variance_deg2: arrays.gaussian_variance_deg2,
303 lorentzian_fwhm_deg: arrays.lorentzian_fwhm_deg,
304 intensity_multiplier: arrays.intensity_multiplier,
305 d_gaussian_variance_d_position: arrays.d_gaussian_variance_d_position,
306 d_lorentzian_fwhm_d_position: arrays.d_lorentzian_fwhm_d_position,
307 d_intensity_multiplier_d_position: arrays.d_intensity_multiplier_d_position,
308 d_gaussian_variance_d_parameters: arrays.d_gaussian_variance_d_parameters,
309 d_lorentzian_fwhm_d_parameters: arrays.d_lorentzian_fwhm_d_parameters,
310 d_intensity_multiplier_d_parameters: arrays.d_intensity_multiplier_d_parameters,
311 parameter_count,
312 reflection_count,
313 })
314 }
315
316 #[must_use]
318 pub const fn parameter_count(self) -> usize {
319 self.parameter_count
320 }
321
322 const fn derivative_index(self, parameter: usize, reflection: usize) -> usize {
323 parameter * self.reflection_count + reflection
324 }
325}
326
327#[derive(Clone, Debug, PartialEq, Eq)]
329pub enum CwContributionsError {
330 LengthMismatch {
332 name: &'static str,
334 },
335 InvalidContribution {
337 reflection: usize,
339 quantity: &'static str,
341 },
342 InvalidDerivative {
344 parameter: usize,
346 reflection: usize,
348 quantity: &'static str,
350 },
351 Cw {
353 reason: CwBatchError,
355 },
356 Fcj {
358 reflection: usize,
360 reason: FcjError,
362 },
363 AllocationOverflow,
365 Profile {
367 reason: ProfileError,
369 },
370}
371
372impl Display for CwContributionsError {
373 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
374 match self {
375 Self::LengthMismatch { name } => write!(formatter, "{name} has an inconsistent length"),
376 Self::InvalidContribution {
377 reflection,
378 quantity,
379 } => write!(
380 formatter,
381 "reflection {reflection} has an invalid {quantity} contribution"
382 ),
383 Self::InvalidDerivative {
384 parameter,
385 reflection,
386 quantity,
387 } => write!(
388 formatter,
389 "provider parameter {parameter}, reflection {reflection} has an invalid {quantity} derivative"
390 ),
391 Self::Cw { reason } => Display::fmt(reason, formatter),
392 Self::Fcj { reflection, reason } => {
393 write!(
394 formatter,
395 "reflection {reflection} has invalid FCJ geometry: {reason}"
396 )
397 }
398 Self::AllocationOverflow => write!(formatter, "contribution allocation size overflow"),
399 Self::Profile { reason } => Display::fmt(reason, formatter),
400 }
401 }
402}
403
404impl Error for CwContributionsError {}
405
406impl From<ProfileError> for CwContributionsError {
407 fn from(reason: ProfileError) -> Self {
408 Self::Profile { reason }
409 }
410}
411
412#[derive(Clone, Debug)]
413struct PreparedProfile {
414 tch: TchShape,
415 fcj: Option<FcjProfile>,
416 support_radius_deg: f64,
417 d_gaussian_d_instrument: [f64; INSTRUMENT_PARAMETER_COUNT],
418 d_lorentzian_d_instrument: [f64; INSTRUMENT_PARAMETER_COUNT],
419 d_gaussian_d_position: f64,
420 d_lorentzian_d_position: f64,
421 d_gaussian_d_variance: f64,
422}
423
424impl PreparedProfile {
425 fn evaluate(&self, x_deg: f64, position_deg: f64) -> FcjProfilePoint {
426 if let Some(fcj) = &self.fcj {
427 return fcj.evaluate_supported(x_deg, self.support_radius_deg);
428 }
429 let point = self.tch.evaluate(x_deg - position_deg);
430 FcjProfilePoint {
431 value: point.value,
432 d_position: -point.d_delta,
433 d_gaussian_fwhm: point.d_gaussian_fwhm,
434 d_lorentzian_fwhm: point.d_lorentzian_fwhm,
435 d_sample_over_radius: 0.0,
436 d_detector_over_radius: 0.0,
437 }
438 }
439}
440
441struct PreparedBatch {
442 profiles: Vec<PreparedProfile>,
443 starts: Vec<usize>,
444 offsets: Vec<usize>,
445}
446
447struct ReflectionBlock {
448 start: usize,
449 y: Vec<f64>,
450 local: Vec<f64>,
451 global: Vec<f64>,
452}
453
454fn prepare_profile(
455 reflection: usize,
456 position: f64,
457 instrument: ConstantWavelengthInstrument,
458 contributions: CwContributionsView<'_>,
459 geometry: Option<FcjGeometry>,
460 support: SupportPolicy,
461) -> Result<PreparedProfile, CwContributionsError> {
462 let base =
463 CwProfileParameters::from_validated_instrument(position, instrument).map_err(|reason| {
464 CwContributionsError::Cw {
465 reason: CwBatchError::InvalidReflection { reflection, reason },
466 }
467 })?;
468 let variance = base.gaussian_variance_deg2 + contributions.gaussian_variance_deg2[reflection];
469 if !variance.is_finite() || variance <= 0.0 {
470 return Err(CwContributionsError::Cw {
471 reason: CwBatchError::InvalidReflection {
472 reflection,
473 reason: CwError::NonPositiveGaussianVariance,
474 },
475 });
476 }
477 let gaussian = GAUSSIAN_FWHM_PER_SIGMA * variance.sqrt();
478 let lorentzian = base.lorentzian_fwhm_deg + contributions.lorentzian_fwhm_deg[reflection];
479 let tch = TchShape::from_component_fwhm(TchWidths {
480 gaussian_fwhm: gaussian,
481 lorentzian_fwhm: lorentzian,
482 })
483 .map_err(|_| CwContributionsError::Cw {
484 reason: CwBatchError::InvalidReflection {
485 reflection,
486 reason: CwError::InvalidTchTransform,
487 },
488 })?;
489 let instrument_gaussian_scale = base.gaussian_fwhm_deg / gaussian;
490 let d_gaussian_d_instrument = base
491 .d_gaussian_fwhm_d_instrument
492 .map(|value| value * instrument_gaussian_scale);
493 let d_gaussian_d_variance = GAUSSIAN_FWHM_PER_SIGMA / (2.0 * variance.sqrt());
494 let support_radius_deg = support.radius(tch.total_fwhm);
495 let fcj = geometry
496 .map(|geometry| {
497 FcjProfile::new(
498 position,
499 TchWidths {
500 gaussian_fwhm: gaussian,
501 lorentzian_fwhm: lorentzian,
502 },
503 geometry,
504 )
505 .map_err(|reason| CwContributionsError::Fcj { reflection, reason })
506 })
507 .transpose()?;
508 Ok(PreparedProfile {
509 tch,
510 fcj,
511 support_radius_deg,
512 d_gaussian_d_instrument,
513 d_lorentzian_d_instrument: base.d_lorentzian_fwhm_d_instrument,
514 d_gaussian_d_position: base.d_gaussian_fwhm_d_two_theta * instrument_gaussian_scale
515 + d_gaussian_d_variance * contributions.d_gaussian_variance_d_position[reflection],
516 d_lorentzian_d_position: base.d_lorentzian_fwhm_d_two_theta
517 + contributions.d_lorentzian_fwhm_d_position[reflection],
518 d_gaussian_d_variance,
519 })
520}
521
522fn prepare_batch(
523 x: &[f64],
524 positions_deg: &[f64],
525 instrument: ConstantWavelengthInstrument,
526 contributions: CwContributionsView<'_>,
527 geometry: Option<FcjGeometry>,
528 support: SupportPolicy,
529) -> Result<PreparedBatch, CwContributionsError> {
530 let reflection_count = positions_deg.len();
531 let mut profiles = Vec::new();
532 let mut starts: Vec<usize> = Vec::new();
533 let mut offsets: Vec<usize> = Vec::new();
534 profiles
535 .try_reserve_exact(reflection_count)
536 .map_err(|_| CwContributionsError::AllocationOverflow)?;
537 starts
538 .try_reserve_exact(reflection_count)
539 .map_err(|_| CwContributionsError::AllocationOverflow)?;
540 offsets
541 .try_reserve_exact(
542 reflection_count
543 .checked_add(1)
544 .ok_or(CwContributionsError::AllocationOverflow)?,
545 )
546 .map_err(|_| CwContributionsError::AllocationOverflow)?;
547 offsets.push(0);
548 for reflection in 0..reflection_count {
549 let profile = prepare_profile(
550 reflection,
551 positions_deg[reflection],
552 instrument,
553 contributions,
554 geometry,
555 support,
556 )?;
557 let range = match &profile.fcj {
558 Some(fcj) => fcj.support_range(profile.support_radius_deg),
559 None => support.range(positions_deg[reflection], profile.tch.total_fwhm),
560 };
561 let lower = x.partition_point(|value| *value < range.left);
562 let upper = x.partition_point(|value| *value <= range.right);
563 offsets.push(
564 offsets[reflection]
565 .checked_add(upper - lower)
566 .ok_or(CwContributionsError::AllocationOverflow)?,
567 );
568 profiles.push(profile);
569 starts.push(lower);
570 }
571 Ok(PreparedBatch {
572 profiles,
573 starts,
574 offsets,
575 })
576}
577
578pub fn accumulate_cw_contributions_batch(
587 grid: GridView<'_>,
588 positions_deg: &[f64],
589 base_intensities: &[f64],
590 instrument: ConstantWavelengthInstrument,
591 contributions: CwContributionsView<'_>,
592 support: SupportPolicy,
593) -> Result<Accumulation, CwContributionsError> {
594 accumulate_cw_contributions_batch_with_context(
595 grid,
596 positions_deg,
597 base_intensities,
598 instrument,
599 contributions,
600 support,
601 &ExecutionContext::serial(),
602 )
603}
604
605pub fn accumulate_cw_contributions_batch_with_context(
611 grid: GridView<'_>,
612 positions_deg: &[f64],
613 base_intensities: &[f64],
614 instrument: ConstantWavelengthInstrument,
615 contributions: CwContributionsView<'_>,
616 support: SupportPolicy,
617 execution: &ExecutionContext,
618) -> Result<Accumulation, CwContributionsError> {
619 accumulate_cw_contributions_impl(
620 grid,
621 positions_deg,
622 base_intensities,
623 instrument,
624 contributions,
625 None,
626 support,
627 execution,
628 )
629}
630
631pub fn accumulate_cw_fcj_contributions_batch(
641 grid: GridView<'_>,
642 positions_deg: &[f64],
643 base_intensities: &[f64],
644 instrument: ConstantWavelengthInstrument,
645 contributions: CwContributionsView<'_>,
646 geometry: FcjGeometry,
647 support: SupportPolicy,
648) -> Result<Accumulation, CwContributionsError> {
649 accumulate_cw_fcj_contributions_batch_with_context(
650 grid,
651 positions_deg,
652 base_intensities,
653 instrument,
654 contributions,
655 geometry,
656 support,
657 &ExecutionContext::serial(),
658 )
659}
660
661#[allow(clippy::too_many_arguments)]
667pub fn accumulate_cw_fcj_contributions_batch_with_context(
668 grid: GridView<'_>,
669 positions_deg: &[f64],
670 base_intensities: &[f64],
671 instrument: ConstantWavelengthInstrument,
672 contributions: CwContributionsView<'_>,
673 geometry: FcjGeometry,
674 support: SupportPolicy,
675 execution: &ExecutionContext,
676) -> Result<Accumulation, CwContributionsError> {
677 accumulate_cw_contributions_impl(
678 grid,
679 positions_deg,
680 base_intensities,
681 instrument,
682 contributions,
683 Some(geometry),
684 support,
685 execution,
686 )
687}
688
689#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
690fn accumulate_cw_contributions_impl(
691 grid: GridView<'_>,
692 positions_deg: &[f64],
693 base_intensities: &[f64],
694 instrument: ConstantWavelengthInstrument,
695 contributions: CwContributionsView<'_>,
696 geometry: Option<FcjGeometry>,
697 support: SupportPolicy,
698 execution: &ExecutionContext,
699) -> Result<Accumulation, CwContributionsError> {
700 support.validate()?;
701 let reflections = crate::cw::CwReflectionBatchView::new(positions_deg, base_intensities)
702 .map_err(|reason| CwContributionsError::Cw { reason })?;
703 instrument
704 .validate()
705 .map_err(|reason| CwContributionsError::Cw {
706 reason: CwBatchError::InvalidInstrument { reason },
707 })?;
708 if contributions.reflection_count != reflections.len() {
709 return Err(CwContributionsError::LengthMismatch {
710 name: "contribution reflection count",
711 });
712 }
713 let x = grid.as_slice();
714 let reflection_count = reflections.len();
715 let axial_parameter_count = if geometry.is_some() {
716 FCJ_PARAMETER_COUNT
717 } else {
718 0
719 };
720 let global_parameter_count = INSTRUMENT_PARAMETER_COUNT
721 .checked_add(axial_parameter_count)
722 .ok_or(CwContributionsError::AllocationOverflow)?
723 .checked_add(contributions.parameter_count)
724 .ok_or(CwContributionsError::AllocationOverflow)?;
725 let prepared = prepare_batch(
726 x,
727 positions_deg,
728 instrument,
729 contributions,
730 geometry,
731 support,
732 )?;
733
734 let active_count = prepared.offsets.last().copied().unwrap_or(0);
735 let local_count = active_count
736 .checked_mul(LOCAL_PARAMETER_COUNT)
737 .ok_or(CwContributionsError::AllocationOverflow)?;
738 let global_count = global_parameter_count
739 .checked_mul(x.len())
740 .ok_or(CwContributionsError::AllocationOverflow)?;
741 let mut y = zeroed_f64_vec(x.len())?;
742 let mut local_values = zeroed_f64_vec(local_count)?;
743 let mut global_values = zeroed_f64_vec(global_count)?;
744 if execution.threads() == 1 || reflection_count < 16 {
745 for reflection in 0..reflection_count {
746 let profile = &prepared.profiles[reflection];
747 let base_intensity = base_intensities[reflection];
748 let multiplier = contributions.intensity_multiplier[reflection];
749 let effective_intensity = base_intensity * multiplier;
750 if !effective_intensity.is_finite() {
751 return Err(CwContributionsError::InvalidContribution {
752 reflection,
753 quantity: "effective_intensity",
754 });
755 }
756 let begin = prepared.offsets[reflection];
757 let end = prepared.offsets[reflection + 1];
758 for active in begin..end {
759 let sample = prepared.starts[reflection] + active - begin;
760 let point = profile.evaluate(x[sample], positions_deg[reflection]);
761 y[sample] += effective_intensity * point.value;
762 let local = active * LOCAL_PARAMETER_COUNT;
763 local_values[local] = multiplier * point.value;
764 local_values[local + 1] = base_intensity
765 * (contributions.d_intensity_multiplier_d_position[reflection] * point.value
766 + multiplier
767 * (point.d_position
768 + point.d_gaussian_fwhm * profile.d_gaussian_d_position
769 + point.d_lorentzian_fwhm * profile.d_lorentzian_d_position));
770 for parameter in 0..INSTRUMENT_PARAMETER_COUNT {
771 let derivative = point.d_gaussian_fwhm
772 * profile.d_gaussian_d_instrument[parameter]
773 + point.d_lorentzian_fwhm * profile.d_lorentzian_d_instrument[parameter];
774 global_values[parameter * x.len() + sample] += effective_intensity * derivative;
775 }
776 if geometry.is_some() {
777 global_values[INSTRUMENT_PARAMETER_COUNT * x.len() + sample] +=
778 effective_intensity * point.d_sample_over_radius;
779 global_values[(INSTRUMENT_PARAMETER_COUNT + 1) * x.len() + sample] +=
780 effective_intensity * point.d_detector_over_radius;
781 }
782 for parameter in 0..contributions.parameter_count {
783 let index = contributions.derivative_index(parameter, reflection);
784 let d_multiplier = contributions.d_intensity_multiplier_d_parameters[index];
785 let d_gaussian = profile.d_gaussian_d_variance
786 * contributions.d_gaussian_variance_d_parameters[index];
787 let d_lorentzian = contributions.d_lorentzian_fwhm_d_parameters[index];
788 let derivative = base_intensity
789 * (d_multiplier * point.value
790 + multiplier
791 * (point.d_gaussian_fwhm * d_gaussian
792 + point.d_lorentzian_fwhm * d_lorentzian));
793 global_values[(INSTRUMENT_PARAMETER_COUNT
794 + axial_parameter_count
795 + parameter)
796 * x.len()
797 + sample] += derivative;
798 }
799 }
800 }
801 } else {
802 let blocks = execution.map_ordered(reflection_count, 16, |reflection| {
803 let profile = &prepared.profiles[reflection];
804 let base_intensity = base_intensities[reflection];
805 let multiplier = contributions.intensity_multiplier[reflection];
806 let effective_intensity = base_intensity * multiplier;
807 if !effective_intensity.is_finite() {
808 return Err(CwContributionsError::InvalidContribution {
809 reflection,
810 quantity: "effective_intensity",
811 });
812 }
813 let begin = prepared.offsets[reflection];
814 let end = prepared.offsets[reflection + 1];
815 let support_count = end - begin;
816 let mut block = ReflectionBlock {
817 start: prepared.starts[reflection],
818 y: zeroed_f64_vec(support_count)?,
819 local: zeroed_f64_vec(
820 support_count
821 .checked_mul(LOCAL_PARAMETER_COUNT)
822 .ok_or(CwContributionsError::AllocationOverflow)?,
823 )?,
824 global: zeroed_f64_vec(
825 support_count
826 .checked_mul(global_parameter_count)
827 .ok_or(CwContributionsError::AllocationOverflow)?,
828 )?,
829 };
830 for support_index in 0..support_count {
831 let sample = block.start + support_index;
832 let point = profile.evaluate(x[sample], positions_deg[reflection]);
833 block.y[support_index] = effective_intensity * point.value;
834 let local = support_index * LOCAL_PARAMETER_COUNT;
835 block.local[local] = multiplier * point.value;
836 block.local[local + 1] = base_intensity
837 * (contributions.d_intensity_multiplier_d_position[reflection] * point.value
838 + multiplier
839 * (point.d_position
840 + point.d_gaussian_fwhm * profile.d_gaussian_d_position
841 + point.d_lorentzian_fwhm * profile.d_lorentzian_d_position));
842 for parameter in 0..INSTRUMENT_PARAMETER_COUNT {
843 let derivative = point.d_gaussian_fwhm
844 * profile.d_gaussian_d_instrument[parameter]
845 + point.d_lorentzian_fwhm * profile.d_lorentzian_d_instrument[parameter];
846 block.global[parameter * support_count + support_index] =
847 effective_intensity * derivative;
848 }
849 if geometry.is_some() {
850 block.global[INSTRUMENT_PARAMETER_COUNT * support_count + support_index] =
851 effective_intensity * point.d_sample_over_radius;
852 block.global
853 [(INSTRUMENT_PARAMETER_COUNT + 1) * support_count + support_index] =
854 effective_intensity * point.d_detector_over_radius;
855 }
856 for parameter in 0..contributions.parameter_count {
857 let index = contributions.derivative_index(parameter, reflection);
858 let d_multiplier = contributions.d_intensity_multiplier_d_parameters[index];
859 let d_gaussian = profile.d_gaussian_d_variance
860 * contributions.d_gaussian_variance_d_parameters[index];
861 let d_lorentzian = contributions.d_lorentzian_fwhm_d_parameters[index];
862 let derivative = base_intensity
863 * (d_multiplier * point.value
864 + multiplier
865 * (point.d_gaussian_fwhm * d_gaussian
866 + point.d_lorentzian_fwhm * d_lorentzian));
867 block.global[(INSTRUMENT_PARAMETER_COUNT
868 + axial_parameter_count
869 + parameter)
870 * support_count
871 + support_index] = derivative;
872 }
873 }
874 Ok(block)
875 });
876 for (reflection, block) in blocks.into_iter().enumerate() {
877 let block = block?;
878 let begin = prepared.offsets[reflection];
879 let support_count = block.y.len();
880 let local_begin = begin * LOCAL_PARAMETER_COUNT;
881 let local_end = local_begin + block.local.len();
882 local_values[local_begin..local_end].copy_from_slice(&block.local);
883 for support_index in 0..support_count {
884 let sample = block.start + support_index;
885 y[sample] += block.y[support_index];
886 for parameter in 0..global_parameter_count {
887 global_values[parameter * x.len() + sample] +=
888 block.global[parameter * support_count + support_index];
889 }
890 }
891 }
892 }
893 Ok(Accumulation {
894 y,
895 derivatives: PatternDerivatives {
896 local: SupportJacobian {
897 starts: prepared.starts,
898 offsets: prepared.offsets,
899 values: local_values,
900 parameter_count: LOCAL_PARAMETER_COUNT,
901 },
902 global: Some(DenseJacobian {
903 values: global_values,
904 parameter_count: global_parameter_count,
905 sample_count: x.len(),
906 }),
907 },
908 sample_count: x.len(),
909 })
910}
911
912#[cfg(test)]
913mod tests {
914 use super::*;
915 use crate::cw::{CwReflectionBatchView, accumulate_cw_batch};
916
917 fn instrument() -> ConstantWavelengthInstrument {
918 ConstantWavelengthInstrument {
919 wavelength_angstrom: 1.540_56,
920 u_deg2: 2.0e-4,
921 v_deg2: -1.0e-4,
922 w_deg2: 1.2e-4,
923 x_deg: 1.5e-3,
924 y_deg: 3.0e-3,
925 }
926 }
927
928 #[test]
929 fn neutral_contributions_match_the_instrument_path_exactly() {
930 let x: Vec<f64> = (0..=2_000)
931 .map(|index| 39.0 + f64::from(index) * 0.001)
932 .collect();
933 let positions = [39.8, 40.2];
934 let intensities = [12.0, 7.0];
935 let zeros = [0.0, 0.0];
936 let ones = [1.0, 1.0];
937 let arrays = CwContributionArrays {
938 gaussian_variance_deg2: &zeros,
939 lorentzian_fwhm_deg: &zeros,
940 intensity_multiplier: &ones,
941 d_gaussian_variance_d_position: &zeros,
942 d_lorentzian_fwhm_d_position: &zeros,
943 d_intensity_multiplier_d_position: &zeros,
944 d_gaussian_variance_d_parameters: &[],
945 d_lorentzian_fwhm_d_parameters: &[],
946 d_intensity_multiplier_d_parameters: &[],
947 };
948 let contributions = CwContributionsView::new(2, 0, arrays).expect("neutral");
949 let grid = GridView::new(&x).expect("grid");
950 let support = SupportPolicy::FwhmMultiple(20.0);
951 let expected = accumulate_cw_batch(
952 grid,
953 CwReflectionBatchView::new(&positions, &intensities).expect("reflections"),
954 instrument(),
955 support,
956 )
957 .expect("CW");
958 let actual = accumulate_cw_contributions_batch(
959 grid,
960 &positions,
961 &intensities,
962 instrument(),
963 contributions,
964 support,
965 )
966 .expect("contributions");
967 assert_eq!(actual, expected);
968 }
969
970 #[test]
971 fn symmetric_and_fcj_blocks_are_bitwise_identical_across_worker_counts() {
972 let x = (0..=30_000)
973 .map(|index| 20.0 + f64::from(index) * 0.003)
974 .collect::<Vec<_>>();
975 let positions = (0..48)
976 .map(|index| 25.0 + f64::from(index) * 1.6)
977 .collect::<Vec<_>>();
978 let intensities = (0..48)
979 .map(|index| 5.0 + 0.2 * f64::from(index))
980 .collect::<Vec<_>>();
981 let zero = vec![0.0; positions.len()];
982 let one = vec![1.0; positions.len()];
983 let provider = (0..48)
984 .map(|index| 1.0e-5 * f64::from(index + 1))
985 .collect::<Vec<_>>();
986 let arrays = CwContributionArrays {
987 gaussian_variance_deg2: &zero,
988 lorentzian_fwhm_deg: &zero,
989 intensity_multiplier: &one,
990 d_gaussian_variance_d_position: &zero,
991 d_lorentzian_fwhm_d_position: &zero,
992 d_intensity_multiplier_d_position: &zero,
993 d_gaussian_variance_d_parameters: &provider,
994 d_lorentzian_fwhm_d_parameters: &zero,
995 d_intensity_multiplier_d_parameters: &zero,
996 };
997 let contributions =
998 CwContributionsView::new(positions.len(), 1, arrays).expect("contributions");
999 let grid = GridView::new(&x).expect("grid");
1000 let support = SupportPolicy::FwhmMultiple(20.0);
1001 let serial = ExecutionContext::serial();
1002 let two = ExecutionContext::new(2).expect("two threads");
1003 let three = ExecutionContext::new(3).expect("three threads");
1004
1005 let expected = accumulate_cw_contributions_batch_with_context(
1006 grid,
1007 &positions,
1008 &intensities,
1009 instrument(),
1010 contributions,
1011 support,
1012 &serial,
1013 )
1014 .expect("serial symmetric");
1015 let expected_fcj = accumulate_cw_fcj_contributions_batch_with_context(
1016 grid,
1017 &positions,
1018 &intensities,
1019 instrument(),
1020 contributions,
1021 FcjGeometry {
1022 sample_over_radius: 0.002,
1023 detector_over_radius: 0.003,
1024 },
1025 support,
1026 &serial,
1027 )
1028 .expect("serial FCJ");
1029 for context in [&two, &three] {
1030 assert_eq!(
1031 accumulate_cw_contributions_batch_with_context(
1032 grid,
1033 &positions,
1034 &intensities,
1035 instrument(),
1036 contributions,
1037 support,
1038 context,
1039 )
1040 .expect("parallel symmetric"),
1041 expected
1042 );
1043 assert_eq!(
1044 accumulate_cw_fcj_contributions_batch_with_context(
1045 grid,
1046 &positions,
1047 &intensities,
1048 instrument(),
1049 contributions,
1050 FcjGeometry {
1051 sample_over_radius: 0.002,
1052 detector_over_radius: 0.003,
1053 },
1054 support,
1055 context,
1056 )
1057 .expect("parallel FCJ"),
1058 expected_fcj
1059 );
1060 }
1061 }
1062
1063 #[test]
1064 fn position_and_provider_derivatives_match_centered_differences() {
1065 let x: Vec<f64> = (0..=2_000)
1066 .map(|index| 49.5 + f64::from(index) * 0.000_5)
1067 .collect();
1068 let intensity = [8.0];
1069 let support = SupportPolicy::FwhmMultiple(100.0);
1070 let calculate = |position: f64, amplitude: f64| {
1071 let normalized = position / 100.0;
1072 let variance = [amplitude * normalized * normalized];
1073 let zeros = [0.0];
1074 let ones = [1.0];
1075 let d_variance_d_position = [2.0 * amplitude * normalized / 100.0];
1076 let d_variance_d_amplitude = [normalized * normalized];
1077 let arrays = CwContributionArrays {
1078 gaussian_variance_deg2: &variance,
1079 lorentzian_fwhm_deg: &zeros,
1080 intensity_multiplier: &ones,
1081 d_gaussian_variance_d_position: &d_variance_d_position,
1082 d_lorentzian_fwhm_d_position: &zeros,
1083 d_intensity_multiplier_d_position: &zeros,
1084 d_gaussian_variance_d_parameters: &d_variance_d_amplitude,
1085 d_lorentzian_fwhm_d_parameters: &zeros,
1086 d_intensity_multiplier_d_parameters: &zeros,
1087 };
1088 accumulate_cw_contributions_batch(
1089 GridView::new(&x).expect("grid"),
1090 &[position],
1091 &intensity,
1092 instrument(),
1093 CwContributionsView::new(1, 1, arrays).expect("contributions"),
1094 support,
1095 )
1096 .expect("contribution accumulation")
1097 };
1098
1099 let position = 50.0;
1100 let amplitude = 3.0e-4;
1101 let baseline = calculate(position, amplitude);
1102 let position_step = 1.0e-6;
1103 let position_plus = calculate(position + position_step, amplitude);
1104 let position_minus = calculate(position - position_step, amplitude);
1105 let dense = baseline
1106 .derivatives
1107 .local
1108 .to_dense(x.len())
1109 .expect("dense local derivatives");
1110 for (sample, (&plus_value, &minus_value)) in
1111 position_plus.y.iter().zip(&position_minus.y).enumerate()
1112 {
1113 let finite_difference = (plus_value - minus_value) / (2.0 * position_step);
1114 let analytical = dense[x.len() + sample];
1115 assert!(
1116 (analytical - finite_difference).abs() < 8.0e-6 * finite_difference.abs().max(1.0)
1117 );
1118 }
1119
1120 let amplitude_step = 1.0e-8;
1121 let amplitude_plus = calculate(position, amplitude + amplitude_step);
1122 let amplitude_minus = calculate(position, amplitude - amplitude_step);
1123 let global = baseline.derivatives.global.as_ref().expect("global");
1124 for (sample, (&plus_value, &minus_value)) in
1125 amplitude_plus.y.iter().zip(&litude_minus.y).enumerate()
1126 {
1127 let finite_difference = (plus_value - minus_value) / (2.0 * amplitude_step);
1128 let analytical = global.values[INSTRUMENT_PARAMETER_COUNT * x.len() + sample];
1129 assert!(
1130 (analytical - finite_difference).abs() < 8.0e-6 * finite_difference.abs().max(1.0)
1131 );
1132 }
1133 }
1134
1135 #[test]
1136 fn zero_fcj_geometry_exactly_matches_symmetric_contributions() {
1137 let x: Vec<f64> = (0..=2_000)
1138 .map(|index| 39.0 + f64::from(index) * 0.001)
1139 .collect();
1140 let positions = [39.8, 40.2];
1141 let intensities = [12.0, 7.0];
1142 let variance = [2.0e-5, 3.0e-5];
1143 let lorentzian = [1.0e-3, 2.0e-3];
1144 let multiplier = [0.8, 1.2];
1145 let zeros = [0.0, 0.0];
1146 let provider = [0.1, 0.2];
1147 let arrays = CwContributionArrays {
1148 gaussian_variance_deg2: &variance,
1149 lorentzian_fwhm_deg: &lorentzian,
1150 intensity_multiplier: &multiplier,
1151 d_gaussian_variance_d_position: &zeros,
1152 d_lorentzian_fwhm_d_position: &zeros,
1153 d_intensity_multiplier_d_position: &zeros,
1154 d_gaussian_variance_d_parameters: &zeros,
1155 d_lorentzian_fwhm_d_parameters: &zeros,
1156 d_intensity_multiplier_d_parameters: &provider,
1157 };
1158 let contributions = CwContributionsView::new(2, 1, arrays).expect("contributions");
1159 let grid = GridView::new(&x).expect("grid");
1160 let support = SupportPolicy::FwhmMultiple(20.0);
1161 let symmetric = accumulate_cw_contributions_batch(
1162 grid,
1163 &positions,
1164 &intensities,
1165 instrument(),
1166 contributions,
1167 support,
1168 )
1169 .expect("symmetric");
1170 let fcj = accumulate_cw_fcj_contributions_batch(
1171 grid,
1172 &positions,
1173 &intensities,
1174 instrument(),
1175 contributions,
1176 FcjGeometry {
1177 sample_over_radius: 0.0,
1178 detector_over_radius: 0.0,
1179 },
1180 support,
1181 )
1182 .expect("FCJ");
1183 assert_eq!(fcj.y, symmetric.y);
1184 assert_eq!(fcj.derivatives.local, symmetric.derivatives.local);
1185 let symmetric_global = symmetric.derivatives.global.expect("symmetric global");
1186 let fcj_global = fcj.derivatives.global.expect("FCJ global");
1187 assert_eq!(
1188 &fcj_global.values[..INSTRUMENT_PARAMETER_COUNT * x.len()],
1189 &symmetric_global.values[..INSTRUMENT_PARAMETER_COUNT * x.len()]
1190 );
1191 assert_eq!(
1192 &fcj_global.values[(INSTRUMENT_PARAMETER_COUNT + FCJ_PARAMETER_COUNT) * x.len()..],
1193 &symmetric_global.values[INSTRUMENT_PARAMETER_COUNT * x.len()..]
1194 );
1195 }
1196
1197 #[test]
1198 fn fcj_geometry_and_provider_derivatives_match_centered_differences() {
1199 let x: Vec<f64> = (0..=2_000)
1200 .map(|index| 49.5 + f64::from(index) * 0.000_5)
1201 .collect();
1202 let position = [50.0];
1203 let intensity = [8.0];
1204 let support = SupportPolicy::FwhmMultiple(100.0);
1205 let calculate = |sample: f64, detector: f64, amplitude: f64| {
1206 let variance = [amplitude * 0.25];
1207 let zeros = [0.0];
1208 let ones = [1.0];
1209 let d_variance = [0.25];
1210 let arrays = CwContributionArrays {
1211 gaussian_variance_deg2: &variance,
1212 lorentzian_fwhm_deg: &zeros,
1213 intensity_multiplier: &ones,
1214 d_gaussian_variance_d_position: &zeros,
1215 d_lorentzian_fwhm_d_position: &zeros,
1216 d_intensity_multiplier_d_position: &zeros,
1217 d_gaussian_variance_d_parameters: &d_variance,
1218 d_lorentzian_fwhm_d_parameters: &zeros,
1219 d_intensity_multiplier_d_parameters: &zeros,
1220 };
1221 accumulate_cw_fcj_contributions_batch(
1222 GridView::new(&x).expect("grid"),
1223 &position,
1224 &intensity,
1225 instrument(),
1226 CwContributionsView::new(1, 1, arrays).expect("contributions"),
1227 FcjGeometry {
1228 sample_over_radius: sample,
1229 detector_over_radius: detector,
1230 },
1231 support,
1232 )
1233 .expect("FCJ contributions")
1234 };
1235 let sample = 0.013;
1236 let detector = 0.009;
1237 let amplitude = 3.0e-4;
1238 let baseline = calculate(sample, detector, amplitude);
1239 let global = baseline.derivatives.global.as_ref().expect("global");
1240 for (parameter, step, plus, minus) in [
1241 (
1242 INSTRUMENT_PARAMETER_COUNT,
1243 1.0e-7,
1244 calculate(sample + 1.0e-7, detector, amplitude),
1245 calculate(sample - 1.0e-7, detector, amplitude),
1246 ),
1247 (
1248 INSTRUMENT_PARAMETER_COUNT + 1,
1249 1.0e-7,
1250 calculate(sample, detector + 1.0e-7, amplitude),
1251 calculate(sample, detector - 1.0e-7, amplitude),
1252 ),
1253 (
1254 INSTRUMENT_PARAMETER_COUNT + FCJ_PARAMETER_COUNT,
1255 1.0e-8,
1256 calculate(sample, detector, amplitude + 1.0e-8),
1257 calculate(sample, detector, amplitude - 1.0e-8),
1258 ),
1259 ] {
1260 for sample_index in 0..x.len() {
1261 let finite_difference =
1262 (plus.y[sample_index] - minus.y[sample_index]) / (2.0 * step);
1263 let analytical = global.values[parameter * x.len() + sample_index];
1264 assert!(
1265 (analytical - finite_difference).abs()
1266 < 2.0e-4 * finite_difference.abs().max(1.0),
1267 "parameter {parameter}, sample {sample_index}: {analytical} != {finite_difference}"
1268 );
1269 }
1270 }
1271 }
1272
1273 #[test]
1274 fn owned_contributions_validate_once_and_reborrow_without_changes() {
1275 let owned = OwnedCwContributions::new(
1276 2,
1277 1,
1278 OwnedCwContributionArrays {
1279 gaussian_variance_deg2: vec![0.0, 0.25],
1280 lorentzian_fwhm_deg: vec![0.1, 0.2],
1281 intensity_multiplier: vec![1.0, 0.5],
1282 d_gaussian_variance_d_position: vec![0.0, 0.0],
1283 d_lorentzian_fwhm_d_position: vec![0.0, 0.0],
1284 d_intensity_multiplier_d_position: vec![0.0, 0.0],
1285 d_gaussian_variance_d_parameters: vec![0.3, 0.4],
1286 d_lorentzian_fwhm_d_parameters: vec![0.0, 0.0],
1287 d_intensity_multiplier_d_parameters: vec![0.0, 0.0],
1288 },
1289 )
1290 .expect("owned contributions");
1291
1292 assert_eq!(owned.reflection_count(), 2);
1293 assert_eq!(owned.parameter_count(), 1);
1294 assert_eq!(owned.as_view().parameter_count(), 1);
1295 assert_eq!(owned.arrays().intensity_multiplier, [1.0, 0.5]);
1296 }
1297
1298 #[test]
1299 fn neutral_owned_contributions_have_valid_identity_values() {
1300 let owned = OwnedCwContributions::neutral(3);
1301 assert_eq!(owned.reflection_count(), 3);
1302 assert_eq!(owned.parameter_count(), 0);
1303 assert_eq!(owned.arrays().gaussian_variance_deg2, [0.0; 3]);
1304 assert_eq!(owned.arrays().intensity_multiplier, [1.0; 3]);
1305 assert_eq!(owned.as_view().parameter_count(), 0);
1306 }
1307
1308 #[test]
1309 fn owned_contributions_reject_invalid_arrays_before_storage() {
1310 assert!(matches!(
1311 OwnedCwContributions::new(
1312 1,
1313 0,
1314 OwnedCwContributionArrays {
1315 gaussian_variance_deg2: vec![-1.0],
1316 lorentzian_fwhm_deg: vec![0.0],
1317 intensity_multiplier: vec![1.0],
1318 d_gaussian_variance_d_position: vec![0.0],
1319 d_lorentzian_fwhm_d_position: vec![0.0],
1320 d_intensity_multiplier_d_position: vec![0.0],
1321 ..OwnedCwContributionArrays::default()
1322 },
1323 ),
1324 Err(CwContributionsError::InvalidContribution {
1325 quantity: "gaussian_variance_deg2",
1326 ..
1327 })
1328 ));
1329 }
1330}