Skip to main content

phasesmith_crystallography/
structure_factor.rs

1//! General-symmetry structure factors and integrated intensities.
2
3use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use crate::cell::{CELL_PARAMETER_COUNT, CellError, CellGeometry, UnitCell};
7use crate::p1::P1ParameterLayout;
8use crate::symmetry::{ExpandedSites, SpaceGroup, SymmetryError};
9use phasesmith_execution::ExecutionContext;
10
11const TWO_PI: f64 = 2.0 * std::f64::consts::PI;
12const TWO_PI_SQUARED: f64 = 2.0 * std::f64::consts::PI * std::f64::consts::PI;
13const METRIC_TOLERANCE: f64 = 1.0e-10;
14
15/// Borrowed arrays for one general-symmetry structural intensity batch.
16#[derive(Clone, Copy, Debug)]
17pub struct StructureFactorBatchView<'a> {
18    /// Canonical Miller indices, one per powder family.
19    pub hkl: &'a [[i32; 3]],
20    /// Powder multiplicity for every canonical family.
21    pub multiplicity: &'a [usize],
22    /// Fractional asymmetric-unit coordinates, one row per independent site.
23    pub fractional_xyz: &'a [[f64; 3]],
24    /// Fractional occupancy for every independent site.
25    pub occupancy: &'a [f64],
26    /// Isotropic displacement in square ångströms for every independent site.
27    pub u_iso_angstrom2: &'a [f64],
28    /// True for sites whose displacement is described by a CIF U tensor.
29    pub anisotropic_mask: &'a [bool],
30    /// CIF U tensors in component order `11,22,33,23,13,12`.
31    pub u_aniso_cif_angstrom2: &'a [[f64; 6]],
32    /// Reflection-major real scattering amplitudes, shape `R * S`.
33    pub scattering_real: &'a [f64],
34    /// Reflection-major imaginary scattering amplitudes, shape `R * S`.
35    pub scattering_imag: &'a [f64],
36    /// Reflection-major analytical derivatives `d Re(f) / ds`.
37    pub d_scattering_real_d_s: &'a [f64],
38    /// Reflection-major analytical derivatives `d Im(f) / ds`.
39    pub d_scattering_imag_d_s: &'a [f64],
40    /// Integrated-intensity correction `C_h`, one per reflection.
41    pub correction: &'a [f64],
42    /// Analytical `d C_h / d(q²)`, one per reflection.
43    pub d_correction_d_q_squared: &'a [f64],
44    /// Non-negative structural phase scale.
45    pub scale: f64,
46    /// Periodic tolerance used only to identify special-position duplicates.
47    pub coordinate_tolerance: f64,
48}
49
50/// General-symmetry values for one reflection batch.
51#[derive(Clone, Debug, PartialEq)]
52pub struct StructureFactorValues {
53    /// Real part of `F_h`.
54    pub f_real: Vec<f64>,
55    /// Imaginary part of `F_h`.
56    pub f_imag: Vec<f64>,
57    /// `|F_h|²` before scale, multiplicity, and correction.
58    pub f_squared: Vec<f64>,
59    /// Integrated reflection intensity.
60    pub intensity: Vec<f64>,
61    /// Reciprocal squared length `q² = 1/d²`.
62    pub q_squared_inverse_angstrom2: Vec<f64>,
63    /// Scattering-vector magnitude `s = sqrt(q²)/2`.
64    pub s_inverse_angstrom: Vec<f64>,
65}
66
67/// Values and bounded parameter-major analytical derivatives.
68#[derive(Clone, Debug, PartialEq)]
69pub struct StructureFactorDenseResult {
70    /// Calculated values.
71    pub values: StructureFactorValues,
72    /// Parameter-major derivative of real `F`, shape `(P, R)`.
73    pub d_f_real: Vec<f64>,
74    /// Parameter-major derivative of imaginary `F`, shape `(P, R)`.
75    pub d_f_imag: Vec<f64>,
76    /// Parameter-major derivative of integrated intensity, shape `(P, R)`.
77    pub d_intensity: Vec<f64>,
78    /// Stable cell/site/scale parameter layout.
79    pub layout: P1ParameterLayout,
80}
81
82/// Values and one forward structural derivative product.
83#[derive(Clone, Debug, PartialEq)]
84pub struct StructureFactorJvpResult {
85    /// Calculated values.
86    pub values: StructureFactorValues,
87    /// Directional derivative of real `F`.
88    pub d_f_real: Vec<f64>,
89    /// Directional derivative of imaginary `F`.
90    pub d_f_imag: Vec<f64>,
91    /// Directional derivative of integrated intensity.
92    pub d_intensity: Vec<f64>,
93}
94
95/// Values and one reverse product for integrated-intensity weights.
96#[derive(Clone, Debug, PartialEq)]
97pub struct StructureFactorVjpResult {
98    /// Calculated values.
99    pub values: StructureFactorValues,
100    /// `J_intensity^T weights` in stable structural parameter order.
101    pub gradient: Vec<f64>,
102    /// Stable cell/site/scale parameter layout.
103    pub layout: P1ParameterLayout,
104}
105
106/// Invalid general-symmetry structure-factor input.
107#[derive(Clone, Debug, PartialEq)]
108pub enum StructureFactorBatchError {
109    /// The unit cell is invalid.
110    Cell(CellError),
111    /// Symmetry expansion failed.
112    Symmetry(SymmetryError),
113    /// The cell metric is incompatible with the space-group rotations.
114    CellSymmetryMismatch,
115    /// Site arrays do not share one site count.
116    SiteLengthMismatch,
117    /// Reflection arrays do not share one reflection count.
118    ReflectionLengthMismatch,
119    /// Scattering arrays are not exactly reflection count times site count.
120    ScatteringShapeMismatch,
121    /// An input scalar or array entry is non-finite.
122    NonFiniteInput,
123    /// A physical scale, occupancy, displacement, correction, or multiplicity is invalid.
124    InvalidPhysicalParameter,
125    /// An `hkl = 0` row does not define a structural reflection.
126    ZeroReflection,
127    /// A requested output allocation overflowed addressable memory.
128    AllocationOverflow,
129    /// A forward tangent does not match the stable parameter layout.
130    TangentLengthMismatch,
131    /// Reverse weights do not match the reflection count.
132    WeightLengthMismatch,
133}
134
135impl Display for StructureFactorBatchError {
136    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
137        match self {
138            Self::Cell(error) => Display::fmt(error, formatter),
139            Self::Symmetry(error) => Display::fmt(error, formatter),
140            Self::CellSymmetryMismatch => {
141                formatter.write_str("unit-cell metric is incompatible with the space group")
142            }
143            Self::SiteLengthMismatch => {
144                formatter.write_str("structure-factor site arrays must have equal length")
145            }
146            Self::ReflectionLengthMismatch => formatter
147                .write_str("structure-factor reflection arrays must have equal length"),
148            Self::ScatteringShapeMismatch => formatter.write_str(
149                "structure-factor scattering arrays must have reflection_count * site_count elements",
150            ),
151            Self::NonFiniteInput => {
152                formatter.write_str("structure-factor inputs must contain only finite values")
153            }
154            Self::InvalidPhysicalParameter => formatter.write_str(
155                "scale, occupancy, U_iso, correction, and multiplicity must be physically valid",
156            ),
157            Self::ZeroReflection => formatter.write_str("hkl = (0, 0, 0) is not a reflection"),
158            Self::AllocationOverflow => {
159                formatter.write_str("structure-factor output allocation overflow")
160            }
161            Self::TangentLengthMismatch => formatter
162                .write_str("structure-factor tangent length must equal the parameter count"),
163            Self::WeightLengthMismatch => formatter
164                .write_str("structure-factor reverse weights must match the reflection count"),
165        }
166    }
167}
168
169impl Error for StructureFactorBatchError {}
170
171impl From<CellError> for StructureFactorBatchError {
172    fn from(value: CellError) -> Self {
173        Self::Cell(value)
174    }
175}
176
177impl From<SymmetryError> for StructureFactorBatchError {
178    fn from(value: SymmetryError) -> Self {
179        Self::Symmetry(value)
180    }
181}
182
183struct ValidatedStructure<'a> {
184    batch: StructureFactorBatchView<'a>,
185    geometry: CellGeometry,
186    expanded: ExpandedSites,
187    site_offsets: Vec<usize>,
188    layout: P1ParameterLayout,
189    reciprocal_axis_lengths: [f64; 3],
190    d_reciprocal_axis_lengths: [[f64; CELL_PARAMETER_COUNT]; 3],
191}
192
193#[derive(Clone, Copy)]
194struct SiteTerms {
195    symmetry_real: f64,
196    symmetry_imag: f64,
197    d_symmetry_real: [f64; 3],
198    d_symmetry_imag: [f64; 3],
199    d_symmetry_real_d_cell: [f64; CELL_PARAMETER_COUNT],
200    d_symmetry_imag_d_cell: [f64; CELL_PARAMETER_COUNT],
201}
202
203#[derive(Clone, Copy)]
204struct SiteVjpEvaluation {
205    terms: SiteTerms,
206    scattering: (f64, f64),
207    d_scattering: (f64, f64),
208    base: (f64, f64),
209    contribution: (f64, f64),
210}
211
212#[derive(Clone, Copy)]
213struct CellReflectionTerms {
214    q_squared: f64,
215    root_q: f64,
216    d_q_squared: [f64; CELL_PARAMETER_COUNT],
217    d_q_direction: f64,
218}
219
220/// Calculate values without materializing structural derivatives.
221///
222/// # Errors
223///
224/// Returns [`StructureFactorBatchError`] for invalid cells, symmetry, shapes,
225/// values, physical parameters, or output sizes.
226pub fn calculate_structure_factor_values(
227    cell: UnitCell,
228    space_group: &SpaceGroup,
229    batch: StructureFactorBatchView<'_>,
230) -> Result<StructureFactorValues, StructureFactorBatchError> {
231    calculate_structure_factor_values_with_context(
232        cell,
233        space_group,
234        batch,
235        &ExecutionContext::serial(),
236    )
237}
238
239/// Calculate values with an explicit bounded execution context.
240///
241/// # Errors
242///
243/// Returns [`StructureFactorBatchError`] for invalid inputs.
244pub fn calculate_structure_factor_values_with_context(
245    cell: UnitCell,
246    space_group: &SpaceGroup,
247    batch: StructureFactorBatchView<'_>,
248    execution: &ExecutionContext,
249) -> Result<StructureFactorValues, StructureFactorBatchError> {
250    let validated = validate(cell, space_group, batch)?;
251    let mut values = empty_values(batch.hkl.len());
252    let chunks = reflection_chunks(batch.hkl.len());
253    if execution.threads() == 1 || chunks.len() < 2 {
254        for reflection in 0..batch.hkl.len() {
255            evaluate_value_reflection(&validated, reflection, reflection, &mut values);
256        }
257        return Ok(values);
258    }
259    let partials = execution.map_ordered(chunks.len(), 2, |chunk| {
260        let range = chunks[chunk].clone();
261        let mut partial = empty_values(range.len());
262        for (local, reflection) in range.enumerate() {
263            evaluate_value_reflection(&validated, reflection, local, &mut partial);
264        }
265        partial
266    });
267    for (range, partial) in chunks.into_iter().zip(partials) {
268        copy_values_chunk(&mut values, &partial, range);
269    }
270    Ok(values)
271}
272
273/// Calculate values and a parameter-major dense structural Jacobian.
274///
275/// This allocation is intended for tests and small diagnostics. Production
276/// composition uses directional derivative products.
277///
278/// # Errors
279///
280/// Returns [`StructureFactorBatchError`] for invalid inputs or allocation
281/// overflow.
282pub fn calculate_structure_factor_dense(
283    cell: UnitCell,
284    space_group: &SpaceGroup,
285    batch: StructureFactorBatchView<'_>,
286) -> Result<StructureFactorDenseResult, StructureFactorBatchError> {
287    calculate_structure_factor_dense_with_context(
288        cell,
289        space_group,
290        batch,
291        &ExecutionContext::serial(),
292    )
293}
294
295/// Calculate values and a dense Jacobian with an explicit bounded context.
296///
297/// # Errors
298///
299/// Returns [`StructureFactorBatchError`] for invalid inputs or allocation
300/// overflow.
301pub fn calculate_structure_factor_dense_with_context(
302    cell: UnitCell,
303    space_group: &SpaceGroup,
304    batch: StructureFactorBatchView<'_>,
305    execution: &ExecutionContext,
306) -> Result<StructureFactorDenseResult, StructureFactorBatchError> {
307    let validated = validate(cell, space_group, batch)?;
308    let reflection_count = batch.hkl.len();
309    let parameter_count = validated.layout.parameter_count();
310    let element_count = parameter_count
311        .checked_mul(reflection_count)
312        .ok_or(StructureFactorBatchError::AllocationOverflow)?;
313    let mut result = StructureFactorDenseResult {
314        values: empty_values(reflection_count),
315        d_f_real: vec![0.0; element_count],
316        d_f_imag: vec![0.0; element_count],
317        d_intensity: vec![0.0; element_count],
318        layout: validated.layout,
319    };
320    let chunks = reflection_chunks(reflection_count);
321    if execution.threads() == 1 || chunks.len() < 2 {
322        for reflection in 0..reflection_count {
323            evaluate_dense_reflection(
324                &validated,
325                reflection,
326                reflection,
327                reflection_count,
328                &mut result,
329            );
330        }
331        return Ok(result);
332    }
333    let partials = execution.map_ordered(chunks.len(), 2, |chunk| {
334        let range = chunks[chunk].clone();
335        let local_count = range.len();
336        let mut partial = StructureFactorDenseResult {
337            values: empty_values(local_count),
338            d_f_real: vec![0.0; parameter_count * local_count],
339            d_f_imag: vec![0.0; parameter_count * local_count],
340            d_intensity: vec![0.0; parameter_count * local_count],
341            layout: validated.layout,
342        };
343        for (local, reflection) in range.enumerate() {
344            evaluate_dense_reflection(&validated, reflection, local, local_count, &mut partial);
345        }
346        partial
347    });
348    for (range, partial) in chunks.into_iter().zip(partials) {
349        copy_dense_chunk(&mut result, &partial, range);
350    }
351    Ok(result)
352}
353
354/// Calculate values and one forward derivative without a dense Jacobian.
355///
356/// # Errors
357///
358/// Returns [`StructureFactorBatchError`] for invalid batch data or a tangent
359/// that does not match the stable parameter layout.
360pub fn calculate_structure_factor_jvp(
361    cell: UnitCell,
362    space_group: &SpaceGroup,
363    batch: StructureFactorBatchView<'_>,
364    tangent: &[f64],
365) -> Result<StructureFactorJvpResult, StructureFactorBatchError> {
366    calculate_structure_factor_jvp_with_context(
367        cell,
368        space_group,
369        batch,
370        tangent,
371        &ExecutionContext::serial(),
372    )
373}
374
375/// Calculate one forward derivative with an explicit bounded context.
376///
377/// # Errors
378///
379/// Returns [`StructureFactorBatchError`] for invalid inputs or tangent shape.
380pub fn calculate_structure_factor_jvp_with_context(
381    cell: UnitCell,
382    space_group: &SpaceGroup,
383    batch: StructureFactorBatchView<'_>,
384    tangent: &[f64],
385    execution: &ExecutionContext,
386) -> Result<StructureFactorJvpResult, StructureFactorBatchError> {
387    let validated = validate(cell, space_group, batch)?;
388    if tangent.len() != validated.layout.parameter_count() {
389        return Err(StructureFactorBatchError::TangentLengthMismatch);
390    }
391    if tangent.iter().any(|value| !value.is_finite()) {
392        return Err(StructureFactorBatchError::NonFiniteInput);
393    }
394    let reflection_count = batch.hkl.len();
395    let mut result = StructureFactorJvpResult {
396        values: empty_values(reflection_count),
397        d_f_real: vec![0.0; reflection_count],
398        d_f_imag: vec![0.0; reflection_count],
399        d_intensity: vec![0.0; reflection_count],
400    };
401    let chunks = reflection_chunks(reflection_count);
402    if execution.threads() == 1 || chunks.len() < 2 {
403        for reflection in 0..reflection_count {
404            evaluate_jvp_reflection(&validated, reflection, reflection, tangent, &mut result);
405        }
406        return Ok(result);
407    }
408    let partials = execution.map_ordered(chunks.len(), 2, |chunk| {
409        let range = chunks[chunk].clone();
410        let local_count = range.len();
411        let mut partial = StructureFactorJvpResult {
412            values: empty_values(local_count),
413            d_f_real: vec![0.0; local_count],
414            d_f_imag: vec![0.0; local_count],
415            d_intensity: vec![0.0; local_count],
416        };
417        for (local, reflection) in range.enumerate() {
418            evaluate_jvp_reflection(&validated, reflection, local, tangent, &mut partial);
419        }
420        partial
421    });
422    for (range, partial) in chunks.into_iter().zip(partials) {
423        copy_jvp_chunk(&mut result, &partial, range);
424    }
425    Ok(result)
426}
427
428/// Calculate values and `J_intensity^T weights` without a dense Jacobian.
429///
430/// # Errors
431///
432/// Returns [`StructureFactorBatchError`] for invalid batch data or reverse
433/// weights that do not match the reflection count.
434pub fn calculate_structure_factor_intensity_vjp(
435    cell: UnitCell,
436    space_group: &SpaceGroup,
437    batch: StructureFactorBatchView<'_>,
438    weights: &[f64],
439) -> Result<StructureFactorVjpResult, StructureFactorBatchError> {
440    calculate_structure_factor_intensity_vjp_with_context(
441        cell,
442        space_group,
443        batch,
444        weights,
445        &ExecutionContext::serial(),
446    )
447}
448
449/// Calculate an intensity VJP with an explicit bounded context.
450///
451/// # Errors
452///
453/// Returns [`StructureFactorBatchError`] for invalid inputs or weight shape.
454pub fn calculate_structure_factor_intensity_vjp_with_context(
455    cell: UnitCell,
456    space_group: &SpaceGroup,
457    batch: StructureFactorBatchView<'_>,
458    weights: &[f64],
459    _execution: &ExecutionContext,
460) -> Result<StructureFactorVjpResult, StructureFactorBatchError> {
461    let validated = validate(cell, space_group, batch)?;
462    if weights.len() != batch.hkl.len() {
463        return Err(StructureFactorBatchError::WeightLengthMismatch);
464    }
465    if weights.iter().any(|value| !value.is_finite()) {
466        return Err(StructureFactorBatchError::NonFiniteInput);
467    }
468    let mut result = StructureFactorVjpResult {
469        values: empty_values(batch.hkl.len()),
470        gradient: vec![0.0; validated.layout.parameter_count()],
471        layout: validated.layout,
472    };
473    let mut site_evaluations = Vec::new();
474    site_evaluations
475        .try_reserve_exact(validated.layout.site_count)
476        .map_err(|_| StructureFactorBatchError::AllocationOverflow)?;
477    for (reflection, weight) in weights.iter().copied().enumerate() {
478        evaluate_vjp_reflection(
479            &validated,
480            reflection,
481            reflection,
482            weight,
483            &mut site_evaluations,
484            &mut result,
485        );
486    }
487    Ok(result)
488}
489
490fn validate<'a>(
491    cell: UnitCell,
492    space_group: &SpaceGroup,
493    batch: StructureFactorBatchView<'a>,
494) -> Result<ValidatedStructure<'a>, StructureFactorBatchError> {
495    let geometry = cell.geometry()?;
496    validate_metric_compatibility(
497        &geometry,
498        space_group.metric_constraints().equations.as_slice(),
499    )?;
500    let site_count = batch.fractional_xyz.len();
501    if batch.occupancy.len() != site_count
502        || batch.u_iso_angstrom2.len() != site_count
503        || batch.anisotropic_mask.len() != site_count
504        || batch.u_aniso_cif_angstrom2.len() != site_count
505    {
506        return Err(StructureFactorBatchError::SiteLengthMismatch);
507    }
508    let reflection_count = batch.hkl.len();
509    if batch.multiplicity.len() != reflection_count
510        || batch.correction.len() != reflection_count
511        || batch.d_correction_d_q_squared.len() != reflection_count
512    {
513        return Err(StructureFactorBatchError::ReflectionLengthMismatch);
514    }
515    if batch.hkl.contains(&[0, 0, 0]) {
516        return Err(StructureFactorBatchError::ZeroReflection);
517    }
518    let scattering_count = reflection_count
519        .checked_mul(site_count)
520        .ok_or(StructureFactorBatchError::AllocationOverflow)?;
521    if [
522        batch.scattering_real.len(),
523        batch.scattering_imag.len(),
524        batch.d_scattering_real_d_s.len(),
525        batch.d_scattering_imag_d_s.len(),
526    ]
527    .into_iter()
528    .any(|count| count != scattering_count)
529    {
530        return Err(StructureFactorBatchError::ScatteringShapeMismatch);
531    }
532    if !batch.scale.is_finite()
533        || batch
534            .fractional_xyz
535            .iter()
536            .flatten()
537            .chain(batch.occupancy)
538            .chain(batch.u_iso_angstrom2)
539            .chain(batch.u_aniso_cif_angstrom2.iter().flatten())
540            .chain(batch.scattering_real)
541            .chain(batch.scattering_imag)
542            .chain(batch.d_scattering_real_d_s)
543            .chain(batch.d_scattering_imag_d_s)
544            .chain(batch.correction)
545            .chain(batch.d_correction_d_q_squared)
546            .any(|value| !value.is_finite())
547    {
548        return Err(StructureFactorBatchError::NonFiniteInput);
549    }
550    if batch.scale < 0.0
551        || batch.occupancy.iter().any(|value| *value < 0.0)
552        || batch
553            .u_iso_angstrom2
554            .iter()
555            .zip(batch.anisotropic_mask)
556            .any(|(value, anisotropic)| !anisotropic && *value < 0.0)
557        || batch
558            .u_aniso_cif_angstrom2
559            .iter()
560            .zip(batch.anisotropic_mask)
561            .any(|(tensor, anisotropic)| *anisotropic && !valid_anisotropic_tensor(*tensor))
562        || batch.correction.iter().any(|value| *value < 0.0)
563        || batch.multiplicity.contains(&0)
564    {
565        return Err(StructureFactorBatchError::InvalidPhysicalParameter);
566    }
567    let expanded = space_group.expand_sites(batch.fractional_xyz, batch.coordinate_tolerance)?;
568    let mut site_offsets = vec![0; site_count + 1];
569    for source in &expanded.source_site {
570        site_offsets[*source + 1] += 1;
571    }
572    for site in 0..site_count {
573        site_offsets[site + 1] += site_offsets[site];
574    }
575    let reciprocal_axis_lengths = [
576        geometry.reciprocal_metric[0][0].sqrt(),
577        geometry.reciprocal_metric[1][1].sqrt(),
578        geometry.reciprocal_metric[2][2].sqrt(),
579    ];
580    let d_reciprocal_axis_lengths = if batch.anisotropic_mask.contains(&true) {
581        geometry.reciprocal_axis_lengths_and_derivatives().1
582    } else {
583        [[0.0; CELL_PARAMETER_COUNT]; 3]
584    };
585    Ok(ValidatedStructure {
586        batch,
587        geometry,
588        expanded,
589        site_offsets,
590        layout: P1ParameterLayout { site_count },
591        reciprocal_axis_lengths,
592        d_reciprocal_axis_lengths,
593    })
594}
595
596fn valid_anisotropic_tensor(tensor: [f64; 6]) -> bool {
597    let [u11, u22, u33, u23, u13, u12] = tensor;
598    let scale = tensor.iter().copied().map(f64::abs).fold(1.0_f64, f64::max);
599    let tolerance = 1.0e-12 * scale;
600    let minor_12 = u11 * u22 - u12 * u12;
601    let minor_13 = u11 * u33 - u13 * u13;
602    let minor_23 = u22 * u33 - u23 * u23;
603    let determinant = u11 * u22 * u33 + 2.0 * u12 * u13 * u23
604        - u11 * u23 * u23
605        - u22 * u13 * u13
606        - u33 * u12 * u12;
607    u11 >= -tolerance
608        && u22 >= -tolerance
609        && u33 >= -tolerance
610        && minor_12 >= -tolerance * scale
611        && minor_13 >= -tolerance * scale
612        && minor_23 >= -tolerance * scale
613        && determinant >= -tolerance * scale * scale
614}
615
616fn empty_values(reflection_count: usize) -> StructureFactorValues {
617    StructureFactorValues {
618        f_real: vec![0.0; reflection_count],
619        f_imag: vec![0.0; reflection_count],
620        f_squared: vec![0.0; reflection_count],
621        intensity: vec![0.0; reflection_count],
622        q_squared_inverse_angstrom2: vec![0.0; reflection_count],
623        s_inverse_angstrom: vec![0.0; reflection_count],
624    }
625}
626
627fn reflection_chunks(reflection_count: usize) -> Vec<std::ops::Range<usize>> {
628    const MIN_REFLECTIONS_PER_CHUNK: usize = 16;
629    const MAX_CHUNKS: usize = 64;
630    let reflections_per_chunk =
631        MIN_REFLECTIONS_PER_CHUNK.max(reflection_count.div_ceil(MAX_CHUNKS));
632    (0..reflection_count)
633        .step_by(reflections_per_chunk)
634        .map(|start| start..(start + reflections_per_chunk).min(reflection_count))
635        .collect()
636}
637
638fn copy_values_chunk(
639    target: &mut StructureFactorValues,
640    source: &StructureFactorValues,
641    range: std::ops::Range<usize>,
642) {
643    target.f_real[range.clone()].copy_from_slice(&source.f_real);
644    target.f_imag[range.clone()].copy_from_slice(&source.f_imag);
645    target.f_squared[range.clone()].copy_from_slice(&source.f_squared);
646    target.intensity[range.clone()].copy_from_slice(&source.intensity);
647    target.q_squared_inverse_angstrom2[range.clone()]
648        .copy_from_slice(&source.q_squared_inverse_angstrom2);
649    target.s_inverse_angstrom[range].copy_from_slice(&source.s_inverse_angstrom);
650}
651
652fn copy_dense_chunk(
653    target: &mut StructureFactorDenseResult,
654    source: &StructureFactorDenseResult,
655    range: std::ops::Range<usize>,
656) {
657    copy_values_chunk(&mut target.values, &source.values, range.clone());
658    let target_count = target.values.f_real.len();
659    let source_count = source.values.f_real.len();
660    for parameter in 0..target.layout.parameter_count() {
661        let target_start = parameter * target_count + range.start;
662        let target_end = target_start + source_count;
663        let source_start = parameter * source_count;
664        let source_end = source_start + source_count;
665        target.d_f_real[target_start..target_end]
666            .copy_from_slice(&source.d_f_real[source_start..source_end]);
667        target.d_f_imag[target_start..target_end]
668            .copy_from_slice(&source.d_f_imag[source_start..source_end]);
669        target.d_intensity[target_start..target_end]
670            .copy_from_slice(&source.d_intensity[source_start..source_end]);
671    }
672}
673
674fn copy_jvp_chunk(
675    target: &mut StructureFactorJvpResult,
676    source: &StructureFactorJvpResult,
677    range: std::ops::Range<usize>,
678) {
679    copy_values_chunk(&mut target.values, &source.values, range.clone());
680    target.d_f_real[range.clone()].copy_from_slice(&source.d_f_real);
681    target.d_f_imag[range.clone()].copy_from_slice(&source.d_f_imag);
682    target.d_intensity[range].copy_from_slice(&source.d_intensity);
683}
684
685fn evaluate_value_reflection(
686    validated: &ValidatedStructure<'_>,
687    reflection: usize,
688    output_reflection: usize,
689    values: &mut StructureFactorValues,
690) {
691    let batch = validated.batch;
692    let q_squared = validated.geometry.q_squared(batch.hkl[reflection]);
693    let s = 0.5 * q_squared.sqrt();
694    let mut f_real = 0.0;
695    let mut f_imag = 0.0;
696    for site in 0..validated.layout.site_count {
697        let (base_real, base_imag) = site_value_base(
698            validated,
699            reflection,
700            site,
701            batch.hkl[reflection],
702            q_squared,
703        );
704        f_real += batch.occupancy[site] * base_real;
705        f_imag += batch.occupancy[site] * base_imag;
706    }
707    set_values(
708        values,
709        batch,
710        reflection,
711        output_reflection,
712        q_squared,
713        s,
714        f_real,
715        f_imag,
716    );
717}
718
719fn site_value_base(
720    validated: &ValidatedStructure<'_>,
721    reflection: usize,
722    site: usize,
723    hkl: [i32; 3],
724    q_squared: f64,
725) -> (f64, f64) {
726    let anisotropic = validated.batch.anisotropic_mask[site];
727    let mut symmetry = (0.0, 0.0);
728    if anisotropic {
729        for expanded_index in validated.site_offsets[site]..validated.site_offsets[site + 1] {
730            let position = validated.expanded.fractional_xyz[expanded_index];
731            let phase = TWO_PI
732                * hkl
733                    .iter()
734                    .zip(position)
735                    .map(|(index, coordinate)| f64::from(*index) * coordinate)
736                    .sum::<f64>();
737            let (sin_phase, cos_phase) = phase.sin_cos();
738            let displacement = anisotropic_displacement_value(
739                validated,
740                hkl,
741                validated.expanded.representative_rotation[expanded_index],
742                validated.batch.u_aniso_cif_angstrom2[site],
743            );
744            symmetry.0 += displacement * cos_phase;
745            symmetry.1 += displacement * sin_phase;
746        }
747    } else {
748        for expanded_index in validated.site_offsets[site]..validated.site_offsets[site + 1] {
749            let position = validated.expanded.fractional_xyz[expanded_index];
750            let phase = TWO_PI
751                * hkl
752                    .iter()
753                    .zip(position)
754                    .map(|(index, coordinate)| f64::from(*index) * coordinate)
755                    .sum::<f64>();
756            let (sin_phase, cos_phase) = phase.sin_cos();
757            symmetry.0 += cos_phase;
758            symmetry.1 += sin_phase;
759        }
760        let displacement =
761            (-TWO_PI_SQUARED * validated.batch.u_iso_angstrom2[site] * q_squared).exp();
762        symmetry.0 *= displacement;
763        symmetry.1 *= displacement;
764    }
765    let index = reflection * validated.layout.site_count + site;
766    complex_multiply(
767        (
768            validated.batch.scattering_real[index],
769            validated.batch.scattering_imag[index],
770        ),
771        symmetry,
772    )
773}
774
775fn evaluate_dense_reflection(
776    validated: &ValidatedStructure<'_>,
777    reflection: usize,
778    output_reflection: usize,
779    output_reflection_count: usize,
780    result: &mut StructureFactorDenseResult,
781) {
782    let batch = validated.batch;
783    let (q_squared, d_q_squared) = validated
784        .geometry
785        .q_squared_and_derivatives(batch.hkl[reflection]);
786    let root_q = q_squared.sqrt();
787    let s = 0.5 * root_q;
788    let mut f_real = 0.0;
789    let mut f_imag = 0.0;
790    for site in 0..validated.layout.site_count {
791        let (contribution_real, contribution_imag) = accumulate_dense_site(
792            validated,
793            reflection,
794            output_reflection,
795            output_reflection_count,
796            site,
797            q_squared,
798            root_q,
799            d_q_squared,
800            result,
801        );
802        f_real += contribution_real;
803        f_imag += contribution_imag;
804    }
805    set_values(
806        &mut result.values,
807        batch,
808        reflection,
809        output_reflection,
810        q_squared,
811        s,
812        f_real,
813        f_imag,
814    );
815    let norm = f_real * f_real + f_imag * f_imag;
816    let multiplicity = multiplicity_f64(batch.multiplicity[reflection]);
817    let correction = batch.correction[reflection];
818    let q_derivatives = d_q_squared
819        .into_iter()
820        .chain(std::iter::repeat(0.0))
821        .take(validated.layout.parameter_count());
822    for (parameter, d_q) in q_derivatives.enumerate() {
823        let index = parameter * output_reflection_count + output_reflection;
824        let d_norm = 2.0 * (f_real * result.d_f_real[index] + f_imag * result.d_f_imag[index]);
825        let d_correction = batch.d_correction_d_q_squared[reflection] * d_q;
826        result.d_intensity[index] =
827            multiplicity * batch.scale * (correction * d_norm + d_correction * norm);
828    }
829    result.d_intensity[validated.layout.scale() * output_reflection_count + output_reflection] =
830        multiplicity * correction * norm;
831}
832
833#[allow(clippy::too_many_arguments)]
834fn accumulate_dense_site(
835    validated: &ValidatedStructure<'_>,
836    reflection: usize,
837    output_reflection: usize,
838    output_reflection_count: usize,
839    site: usize,
840    q_squared: f64,
841    root_q: f64,
842    d_q_squared: [f64; CELL_PARAMETER_COUNT],
843    result: &mut StructureFactorDenseResult,
844) -> (f64, f64) {
845    let batch = validated.batch;
846    let terms = symmetry_terms(
847        validated,
848        batch.hkl[reflection],
849        site,
850        q_squared,
851        Some(d_q_squared),
852    );
853    let (base_real, base_imag) = site_base(validated, reflection, site, terms);
854    let occupancy = batch.occupancy[site];
855    let contribution = (occupancy * base_real, occupancy * base_imag);
856    let scattering_index = reflection * validated.layout.site_count + site;
857    let scattering = (
858        batch.scattering_real[scattering_index],
859        batch.scattering_imag[scattering_index],
860    );
861    let d_scattering = (
862        batch.d_scattering_real_d_s[scattering_index],
863        batch.d_scattering_imag_d_s[scattering_index],
864    );
865    for (parameter, d_q) in d_q_squared.into_iter().enumerate() {
866        let d_s = d_q / (4.0 * root_q);
867        let scattering_derivative = (d_scattering.0 * d_s, d_scattering.1 * d_s);
868        let rotated_scattering = complex_multiply(
869            scattering_derivative,
870            (terms.symmetry_real, terms.symmetry_imag),
871        );
872        let rotated_displacement = complex_multiply(
873            scattering,
874            (
875                terms.d_symmetry_real_d_cell[parameter],
876                terms.d_symmetry_imag_d_cell[parameter],
877            ),
878        );
879        set_f_derivative(
880            result,
881            parameter,
882            output_reflection,
883            output_reflection_count,
884            occupancy * (rotated_scattering.0 + rotated_displacement.0),
885            occupancy * (rotated_scattering.1 + rotated_displacement.1),
886        );
887    }
888    for (component, (&d_real, &d_imag)) in terms
889        .d_symmetry_real
890        .iter()
891        .zip(&terms.d_symmetry_imag)
892        .enumerate()
893    {
894        let rotated = complex_multiply(scattering, (d_real, d_imag));
895        set_f_derivative(
896            result,
897            validated.layout.coordinate(site, component),
898            output_reflection,
899            output_reflection_count,
900            occupancy * rotated.0,
901            occupancy * rotated.1,
902        );
903    }
904    set_f_derivative(
905        result,
906        validated.layout.occupancy(site),
907        output_reflection,
908        output_reflection_count,
909        base_real,
910        base_imag,
911    );
912    if !batch.anisotropic_mask[site] {
913        set_f_derivative(
914            result,
915            validated.layout.u_iso(site),
916            output_reflection,
917            output_reflection_count,
918            -TWO_PI_SQUARED * q_squared * contribution.0,
919            -TWO_PI_SQUARED * q_squared * contribution.1,
920        );
921    }
922    contribution
923}
924
925fn evaluate_jvp_reflection(
926    validated: &ValidatedStructure<'_>,
927    reflection: usize,
928    output_reflection: usize,
929    tangent: &[f64],
930    result: &mut StructureFactorJvpResult,
931) {
932    let batch = validated.batch;
933    let (q_squared, d_q_squared) = validated
934        .geometry
935        .q_squared_and_derivatives(batch.hkl[reflection]);
936    let root_q = q_squared.sqrt();
937    let d_q_direction = d_q_squared
938        .iter()
939        .zip(&tangent[..CELL_PARAMETER_COUNT])
940        .map(|(derivative, direction)| derivative * direction)
941        .sum::<f64>();
942    let mut f = (0.0, 0.0);
943    let mut d_f = (0.0, 0.0);
944    let cell_terms = CellReflectionTerms {
945        q_squared,
946        root_q,
947        d_q_squared,
948        d_q_direction,
949    };
950    for site in 0..validated.layout.site_count {
951        let (contribution, derivative) = jvp_site(validated, reflection, site, cell_terms, tangent);
952        f.0 += contribution.0;
953        f.1 += contribution.1;
954        d_f.0 += derivative.0;
955        d_f.1 += derivative.1;
956    }
957    let s = 0.5 * root_q;
958    set_values(
959        &mut result.values,
960        batch,
961        reflection,
962        output_reflection,
963        q_squared,
964        s,
965        f.0,
966        f.1,
967    );
968    result.d_f_real[output_reflection] = d_f.0;
969    result.d_f_imag[output_reflection] = d_f.1;
970    let norm = f.0 * f.0 + f.1 * f.1;
971    let d_norm = 2.0 * (f.0 * d_f.0 + f.1 * d_f.1);
972    let correction = batch.correction[reflection];
973    let d_correction = batch.d_correction_d_q_squared[reflection] * d_q_direction;
974    result.d_intensity[output_reflection] = multiplicity_f64(batch.multiplicity[reflection])
975        * (tangent[validated.layout.scale()] * correction * norm
976            + batch.scale * (d_correction * norm + correction * d_norm));
977}
978
979fn jvp_site(
980    validated: &ValidatedStructure<'_>,
981    reflection: usize,
982    site: usize,
983    cell: CellReflectionTerms,
984    tangent: &[f64],
985) -> ((f64, f64), (f64, f64)) {
986    let batch = validated.batch;
987    let terms = symmetry_terms(
988        validated,
989        batch.hkl[reflection],
990        site,
991        cell.q_squared,
992        Some(cell.d_q_squared),
993    );
994    let scattering_index = reflection * validated.layout.site_count + site;
995    let scattering = (
996        batch.scattering_real[scattering_index],
997        batch.scattering_imag[scattering_index],
998    );
999    let d_scattering = (
1000        batch.d_scattering_real_d_s[scattering_index],
1001        batch.d_scattering_imag_d_s[scattering_index],
1002    );
1003    let symmetry = (terms.symmetry_real, terms.symmetry_imag);
1004    let base_rotated = complex_multiply(scattering, symmetry);
1005    let base = base_rotated;
1006    let occupancy = batch.occupancy[site];
1007    let contribution = (occupancy * base.0, occupancy * base.1);
1008
1009    let d_s = cell.d_q_direction / (4.0 * cell.root_q);
1010    let scattering_direction = (d_scattering.0 * d_s, d_scattering.1 * d_s);
1011    let mut displacement_direction = (0.0, 0.0);
1012    for (parameter, direction) in tangent[..CELL_PARAMETER_COUNT].iter().copied().enumerate() {
1013        displacement_direction.0 += direction * terms.d_symmetry_real_d_cell[parameter];
1014        displacement_direction.1 += direction * terms.d_symmetry_imag_d_cell[parameter];
1015    }
1016    let cell_scattering = complex_multiply(scattering_direction, symmetry);
1017    let cell_displacement = complex_multiply(scattering, displacement_direction);
1018    let d_symmetry = (0..3).fold((0.0, 0.0), |sum, component| {
1019        let direction = tangent[validated.layout.coordinate(site, component)];
1020        (
1021            sum.0 + direction * terms.d_symmetry_real[component],
1022            sum.1 + direction * terms.d_symmetry_imag[component],
1023        )
1024    });
1025    let coordinate_rotated = complex_multiply(scattering, d_symmetry);
1026    let occupancy_direction = tangent[validated.layout.occupancy(site)];
1027    let u_direction = if batch.anisotropic_mask[site] {
1028        0.0
1029    } else {
1030        tangent[validated.layout.u_iso(site)]
1031    };
1032    let derivative = (
1033        occupancy_direction * base.0
1034            + occupancy * (cell_scattering.0 + cell_displacement.0 + coordinate_rotated.0)
1035            - TWO_PI_SQUARED * cell.q_squared * u_direction * contribution.0,
1036        occupancy_direction * base.1
1037            + occupancy * (cell_scattering.1 + cell_displacement.1 + coordinate_rotated.1)
1038            - TWO_PI_SQUARED * cell.q_squared * u_direction * contribution.1,
1039    );
1040    (contribution, derivative)
1041}
1042
1043fn evaluate_vjp_reflection(
1044    validated: &ValidatedStructure<'_>,
1045    reflection: usize,
1046    output_reflection: usize,
1047    weight: f64,
1048    site_evaluations: &mut Vec<SiteVjpEvaluation>,
1049    result: &mut StructureFactorVjpResult,
1050) {
1051    let batch = validated.batch;
1052    let (q_squared, d_q_squared) = validated
1053        .geometry
1054        .q_squared_and_derivatives(batch.hkl[reflection]);
1055    let root_q = q_squared.sqrt();
1056    let mut f = (0.0, 0.0);
1057    site_evaluations.clear();
1058    for site in 0..validated.layout.site_count {
1059        let terms = symmetry_terms(
1060            validated,
1061            batch.hkl[reflection],
1062            site,
1063            q_squared,
1064            Some(d_q_squared),
1065        );
1066        let scattering_index = reflection * validated.layout.site_count + site;
1067        let scattering = (
1068            batch.scattering_real[scattering_index],
1069            batch.scattering_imag[scattering_index],
1070        );
1071        let d_scattering = (
1072            batch.d_scattering_real_d_s[scattering_index],
1073            batch.d_scattering_imag_d_s[scattering_index],
1074        );
1075        let rotated = complex_multiply(scattering, (terms.symmetry_real, terms.symmetry_imag));
1076        let base = rotated;
1077        let occupancy = batch.occupancy[site];
1078        let contribution = (occupancy * base.0, occupancy * base.1);
1079        f.0 += contribution.0;
1080        f.1 += contribution.1;
1081        site_evaluations.push(SiteVjpEvaluation {
1082            terms,
1083            scattering,
1084            d_scattering,
1085            base,
1086            contribution,
1087        });
1088    }
1089    set_values(
1090        &mut result.values,
1091        batch,
1092        reflection,
1093        output_reflection,
1094        q_squared,
1095        0.5 * root_q,
1096        f.0,
1097        f.1,
1098    );
1099    let norm = f.0 * f.0 + f.1 * f.1;
1100    let multiplicity = multiplicity_f64(batch.multiplicity[reflection]);
1101    let correction = batch.correction[reflection];
1102    let f_weight = 2.0 * weight * multiplicity * batch.scale * correction;
1103    for (site, evaluation) in site_evaluations.iter().copied().enumerate() {
1104        accumulate_vjp_site(
1105            validated,
1106            site,
1107            q_squared,
1108            root_q,
1109            d_q_squared,
1110            f,
1111            f_weight,
1112            evaluation,
1113            &mut result.gradient,
1114        );
1115    }
1116    let correction_weight =
1117        weight * multiplicity * batch.scale * batch.d_correction_d_q_squared[reflection] * norm;
1118    for (parameter, d_q) in d_q_squared.into_iter().enumerate() {
1119        result.gradient[parameter] += correction_weight * d_q;
1120    }
1121    result.gradient[validated.layout.scale()] += weight * multiplicity * correction * norm;
1122}
1123
1124#[allow(clippy::too_many_arguments)]
1125fn accumulate_vjp_site(
1126    validated: &ValidatedStructure<'_>,
1127    site: usize,
1128    q_squared: f64,
1129    root_q: f64,
1130    d_q_squared: [f64; CELL_PARAMETER_COUNT],
1131    f: (f64, f64),
1132    f_weight: f64,
1133    evaluation: SiteVjpEvaluation,
1134    gradient: &mut [f64],
1135) {
1136    let batch = validated.batch;
1137    let SiteVjpEvaluation {
1138        terms,
1139        scattering,
1140        d_scattering,
1141        base,
1142        contribution,
1143    } = evaluation;
1144    let symmetry = (terms.symmetry_real, terms.symmetry_imag);
1145    let occupancy = batch.occupancy[site];
1146    for (parameter, d_q) in d_q_squared.into_iter().enumerate() {
1147        let d_s = d_q / (4.0 * root_q);
1148        let scattering_derivative = (d_scattering.0 * d_s, d_scattering.1 * d_s);
1149        let rotated_scattering = complex_multiply(scattering_derivative, symmetry);
1150        let rotated_displacement = complex_multiply(
1151            scattering,
1152            (
1153                terms.d_symmetry_real_d_cell[parameter],
1154                terms.d_symmetry_imag_d_cell[parameter],
1155            ),
1156        );
1157        gradient[parameter] += f_weight
1158            * occupancy
1159            * (f.0 * (rotated_scattering.0 + rotated_displacement.0)
1160                + f.1 * (rotated_scattering.1 + rotated_displacement.1));
1161    }
1162    for (component, (&d_real, &d_imag)) in terms
1163        .d_symmetry_real
1164        .iter()
1165        .zip(&terms.d_symmetry_imag)
1166        .enumerate()
1167    {
1168        let rotated = complex_multiply(scattering, (d_real, d_imag));
1169        gradient[validated.layout.coordinate(site, component)] +=
1170            f_weight * occupancy * (f.0 * rotated.0 + f.1 * rotated.1);
1171    }
1172    gradient[validated.layout.occupancy(site)] += f_weight * (f.0 * base.0 + f.1 * base.1);
1173    if !batch.anisotropic_mask[site] {
1174        gradient[validated.layout.u_iso(site)] +=
1175            f_weight * -TWO_PI_SQUARED * q_squared * (f.0 * contribution.0 + f.1 * contribution.1);
1176    }
1177}
1178
1179fn symmetry_terms(
1180    validated: &ValidatedStructure<'_>,
1181    hkl: [i32; 3],
1182    site: usize,
1183    q_squared: f64,
1184    d_q_squared: Option<[f64; CELL_PARAMETER_COUNT]>,
1185) -> SiteTerms {
1186    let mut result = SiteTerms {
1187        symmetry_real: 0.0,
1188        symmetry_imag: 0.0,
1189        d_symmetry_real: [0.0; 3],
1190        d_symmetry_imag: [0.0; 3],
1191        d_symmetry_real_d_cell: [0.0; CELL_PARAMETER_COUNT],
1192        d_symmetry_imag_d_cell: [0.0; CELL_PARAMETER_COUNT],
1193    };
1194    let anisotropic = validated.batch.anisotropic_mask[site];
1195    let tensor = validated.batch.u_aniso_cif_angstrom2[site];
1196    for expanded_index in validated.site_offsets[site]..validated.site_offsets[site + 1] {
1197        let position = validated.expanded.fractional_xyz[expanded_index];
1198        let rotation = validated.expanded.representative_rotation[expanded_index];
1199        let phase = TWO_PI
1200            * hkl
1201                .iter()
1202                .zip(position)
1203                .map(|(index, coordinate)| f64::from(*index) * coordinate)
1204                .sum::<f64>();
1205        let (sin_phase, cos_phase) = phase.sin_cos();
1206        let (displacement, d_displacement) = if anisotropic {
1207            anisotropic_displacement(validated, hkl, rotation, tensor, d_q_squared.is_some())
1208        } else {
1209            (1.0, [0.0; CELL_PARAMETER_COUNT])
1210        };
1211        result.symmetry_real += displacement * cos_phase;
1212        result.symmetry_imag += displacement * sin_phase;
1213        for (component, (d_real, d_imag)) in result
1214            .d_symmetry_real
1215            .iter_mut()
1216            .zip(&mut result.d_symmetry_imag)
1217            .enumerate()
1218        {
1219            let phase_derivative = TWO_PI
1220                * (0..3)
1221                    .map(|row| f64::from(hkl[row]) * f64::from(rotation[row][component]))
1222                    .sum::<f64>();
1223            *d_real -= displacement * phase_derivative * sin_phase;
1224            *d_imag += displacement * phase_derivative * cos_phase;
1225        }
1226        for (parameter, derivative) in d_displacement.iter().copied().enumerate() {
1227            result.d_symmetry_real_d_cell[parameter] += derivative * cos_phase;
1228            result.d_symmetry_imag_d_cell[parameter] += derivative * sin_phase;
1229        }
1230    }
1231    if !anisotropic {
1232        let displacement =
1233            (-TWO_PI_SQUARED * validated.batch.u_iso_angstrom2[site] * q_squared).exp();
1234        let factor = -TWO_PI_SQUARED * validated.batch.u_iso_angstrom2[site];
1235        if let Some(derivatives) = d_q_squared {
1236            for (parameter, derivative) in derivatives.iter().copied().enumerate() {
1237                let d_displacement = factor * derivative * displacement;
1238                result.d_symmetry_real_d_cell[parameter] = d_displacement * result.symmetry_real;
1239                result.d_symmetry_imag_d_cell[parameter] = d_displacement * result.symmetry_imag;
1240            }
1241        }
1242        result.symmetry_real *= displacement;
1243        result.symmetry_imag *= displacement;
1244        for value in &mut result.d_symmetry_real {
1245            *value *= displacement;
1246        }
1247        for value in &mut result.d_symmetry_imag {
1248            *value *= displacement;
1249        }
1250    }
1251    result
1252}
1253
1254fn anisotropic_displacement(
1255    validated: &ValidatedStructure<'_>,
1256    hkl: [i32; 3],
1257    rotation: [[i32; 3]; 3],
1258    tensor: [f64; 6],
1259    calculate_derivatives: bool,
1260) -> (f64, [f64; CELL_PARAMETER_COUNT]) {
1261    let (transformed_hkl, vector) = transformed_reciprocal_vector(validated, hkl, rotation);
1262    let tensor_times_vector = symmetric_tensor_vector(tensor, vector);
1263    let quadratic = vector
1264        .iter()
1265        .zip(tensor_times_vector)
1266        .map(|(left, right)| left * right)
1267        .sum::<f64>();
1268    let displacement = (-TWO_PI_SQUARED * quadratic).exp();
1269    let mut derivatives = [0.0; CELL_PARAMETER_COUNT];
1270    if !calculate_derivatives {
1271        return (displacement, derivatives);
1272    }
1273    for (parameter, derivative) in derivatives.iter_mut().enumerate() {
1274        let d_vector = [
1275            f64::from(transformed_hkl[0]) * validated.d_reciprocal_axis_lengths[0][parameter],
1276            f64::from(transformed_hkl[1]) * validated.d_reciprocal_axis_lengths[1][parameter],
1277            f64::from(transformed_hkl[2]) * validated.d_reciprocal_axis_lengths[2][parameter],
1278        ];
1279        let d_quadratic = 2.0
1280            * d_vector
1281                .iter()
1282                .zip(tensor_times_vector)
1283                .map(|(left, right)| left * right)
1284                .sum::<f64>();
1285        *derivative = -TWO_PI_SQUARED * d_quadratic * displacement;
1286    }
1287    (displacement, derivatives)
1288}
1289
1290fn anisotropic_displacement_value(
1291    validated: &ValidatedStructure<'_>,
1292    hkl: [i32; 3],
1293    rotation: [[i32; 3]; 3],
1294    tensor: [f64; 6],
1295) -> f64 {
1296    let (_, vector) = transformed_reciprocal_vector(validated, hkl, rotation);
1297    let quadratic = vector
1298        .iter()
1299        .zip(symmetric_tensor_vector(tensor, vector))
1300        .map(|(left, right)| left * right)
1301        .sum::<f64>();
1302    (-TWO_PI_SQUARED * quadratic).exp()
1303}
1304
1305fn transformed_reciprocal_vector(
1306    validated: &ValidatedStructure<'_>,
1307    hkl: [i32; 3],
1308    rotation: [[i32; 3]; 3],
1309) -> ([i32; 3], [f64; 3]) {
1310    let transformed_hkl = [0, 1, 2].map(|component| {
1311        (0..3)
1312            .map(|row| hkl[row] * rotation[row][component])
1313            .sum::<i32>()
1314    });
1315    let reciprocal = validated.reciprocal_axis_lengths;
1316    let vector = [
1317        f64::from(transformed_hkl[0]) * reciprocal[0],
1318        f64::from(transformed_hkl[1]) * reciprocal[1],
1319        f64::from(transformed_hkl[2]) * reciprocal[2],
1320    ];
1321    (transformed_hkl, vector)
1322}
1323
1324fn symmetric_tensor_vector(tensor: [f64; 6], vector: [f64; 3]) -> [f64; 3] {
1325    let [u11, u22, u33, u23, u13, u12] = tensor;
1326    [
1327        u11 * vector[0] + u12 * vector[1] + u13 * vector[2],
1328        u12 * vector[0] + u22 * vector[1] + u23 * vector[2],
1329        u13 * vector[0] + u23 * vector[1] + u33 * vector[2],
1330    ]
1331}
1332
1333fn site_base(
1334    validated: &ValidatedStructure<'_>,
1335    reflection: usize,
1336    site: usize,
1337    terms: SiteTerms,
1338) -> (f64, f64) {
1339    let index = reflection * validated.layout.site_count + site;
1340    complex_multiply(
1341        (
1342            validated.batch.scattering_real[index],
1343            validated.batch.scattering_imag[index],
1344        ),
1345        (terms.symmetry_real, terms.symmetry_imag),
1346    )
1347}
1348
1349#[allow(clippy::too_many_arguments)]
1350fn set_values(
1351    values: &mut StructureFactorValues,
1352    batch: StructureFactorBatchView<'_>,
1353    input_reflection: usize,
1354    output_reflection: usize,
1355    q_squared: f64,
1356    s: f64,
1357    f_real: f64,
1358    f_imag: f64,
1359) {
1360    let norm = f_real * f_real + f_imag * f_imag;
1361    values.f_real[output_reflection] = f_real;
1362    values.f_imag[output_reflection] = f_imag;
1363    values.f_squared[output_reflection] = norm;
1364    values.intensity[output_reflection] = batch.scale
1365        * multiplicity_f64(batch.multiplicity[input_reflection])
1366        * batch.correction[input_reflection]
1367        * norm;
1368    values.q_squared_inverse_angstrom2[output_reflection] = q_squared;
1369    values.s_inverse_angstrom[output_reflection] = s;
1370}
1371
1372fn set_f_derivative(
1373    result: &mut StructureFactorDenseResult,
1374    parameter: usize,
1375    reflection: usize,
1376    reflection_count: usize,
1377    real: f64,
1378    imag: f64,
1379) {
1380    let index = parameter * reflection_count + reflection;
1381    result.d_f_real[index] += real;
1382    result.d_f_imag[index] += imag;
1383}
1384
1385fn complex_multiply(left: (f64, f64), right: (f64, f64)) -> (f64, f64) {
1386    (
1387        left.0 * right.0 - left.1 * right.1,
1388        left.0 * right.1 + left.1 * right.0,
1389    )
1390}
1391
1392#[allow(clippy::cast_precision_loss)]
1393fn multiplicity_f64(value: usize) -> f64 {
1394    value as f64
1395}
1396
1397#[allow(clippy::cast_precision_loss)]
1398fn validate_metric_compatibility(
1399    geometry: &CellGeometry,
1400    equations: &[[i64; 6]],
1401) -> Result<(), StructureFactorBatchError> {
1402    let metric = geometry.direct_metric;
1403    let components = [
1404        metric[0][0],
1405        metric[1][1],
1406        metric[2][2],
1407        metric[1][2],
1408        metric[0][2],
1409        metric[0][1],
1410    ];
1411    let scale = components
1412        .iter()
1413        .copied()
1414        .map(f64::abs)
1415        .fold(1.0_f64, f64::max);
1416    for equation in equations {
1417        let residual = equation
1418            .iter()
1419            .zip(components)
1420            .map(|(coefficient, value)| *coefficient as f64 * value)
1421            .sum::<f64>();
1422        let coefficient_scale = equation.iter().copied().map(i64::unsigned_abs).sum::<u64>() as f64;
1423        if residual.abs() > METRIC_TOLERANCE * scale * coefficient_scale.max(1.0) {
1424            return Err(StructureFactorBatchError::CellSymmetryMismatch);
1425        }
1426    }
1427    Ok(())
1428}
1429
1430#[cfg(test)]
1431mod tests {
1432    use super::*;
1433    use crate::symmetry::{Rational, SymmetryOperation};
1434
1435    fn cubic_cell(a: f64) -> UnitCell {
1436        UnitCell {
1437            a_angstrom: a,
1438            b_angstrom: a,
1439            c_angstrom: a,
1440            alpha_deg: 90.0,
1441            beta_deg: 90.0,
1442            gamma_deg: 90.0,
1443        }
1444    }
1445
1446    fn p1() -> SpaceGroup {
1447        SpaceGroup::new(vec![SymmetryOperation::identity()]).expect("P1")
1448    }
1449
1450    fn inversion() -> SpaceGroup {
1451        SpaceGroup::new(vec![
1452            SymmetryOperation::identity(),
1453            SymmetryOperation::new([[-1, 0, 0], [0, -1, 0], [0, 0, -1]], [Rational::zero(); 3])
1454                .expect("inversion"),
1455        ])
1456        .expect("P-1")
1457    }
1458
1459    fn axis_swap_group() -> SpaceGroup {
1460        SpaceGroup::new(vec![
1461            SymmetryOperation::identity(),
1462            SymmetryOperation::new([[0, 1, 0], [1, 0, 0], [0, 0, -1]], [Rational::zero(); 3])
1463                .expect("axis swap"),
1464        ])
1465        .expect("closed axis-swap group")
1466    }
1467
1468    #[test]
1469    fn inversion_values_and_special_positions_have_closed_forms() {
1470        let hkl = [[1, 2, 1]];
1471        let multiplicity = [2];
1472        let xyz = [[0.13, 0.21, 0.07], [0.0, 0.0, 0.0]];
1473        let occupancy = [0.8, 0.5];
1474        let u_iso = [0.0, 0.0];
1475        let real = [3.0, 2.0];
1476        let zero = [0.0, 0.0];
1477        let correction = [1.25];
1478        let values = calculate_structure_factor_values(
1479            cubic_cell(5.0),
1480            &inversion(),
1481            StructureFactorBatchView {
1482                hkl: &hkl,
1483                multiplicity: &multiplicity,
1484                fractional_xyz: &xyz,
1485                occupancy: &occupancy,
1486                u_iso_angstrom2: &u_iso,
1487                anisotropic_mask: &[false, false],
1488                u_aniso_cif_angstrom2: &[[0.0; 6]; 2],
1489                scattering_real: &real,
1490                scattering_imag: &zero,
1491                d_scattering_real_d_s: &zero,
1492                d_scattering_imag_d_s: &zero,
1493                correction: &correction,
1494                d_correction_d_q_squared: &[0.0],
1495                scale: 1.4,
1496                coordinate_tolerance: 1.0e-10,
1497            },
1498        )
1499        .expect("structure factors");
1500        let phase = TWO_PI * (0.13 + 2.0 * 0.21 + 0.07);
1501        let expected_f = 0.8 * 3.0 * 2.0 * phase.cos() + 0.5 * 2.0;
1502        assert!((values.f_real[0] - expected_f).abs() < 2.0e-14);
1503        assert!(values.f_imag[0].abs() < 2.0e-14);
1504        assert!((values.intensity[0] - 1.4 * 2.0 * 1.25 * expected_f.powi(2)).abs() < 1.0e-12);
1505    }
1506
1507    #[test]
1508    fn anisotropic_symmetry_mates_use_rotated_reciprocal_indices() {
1509        let hkl = [[2, 1, 1]];
1510        let xyz = [[0.17, 0.29, 0.11]];
1511        let tensor = [[0.020, 0.010, 0.030, 0.002, 0.001, 0.003]];
1512        let values = calculate_structure_factor_values(
1513            cubic_cell(5.0),
1514            &axis_swap_group(),
1515            StructureFactorBatchView {
1516                hkl: &hkl,
1517                multiplicity: &[1],
1518                fractional_xyz: &xyz,
1519                occupancy: &[0.8],
1520                u_iso_angstrom2: &[0.0],
1521                anisotropic_mask: &[true],
1522                u_aniso_cif_angstrom2: &tensor,
1523                scattering_real: &[3.0],
1524                scattering_imag: &[0.0],
1525                d_scattering_real_d_s: &[0.0],
1526                d_scattering_imag_d_s: &[0.0],
1527                correction: &[1.0],
1528                d_correction_d_q_squared: &[0.0],
1529                scale: 1.0,
1530                coordinate_tolerance: 1.0e-10,
1531            },
1532        )
1533        .expect("anisotropic structure factor");
1534        let reciprocal = 0.2;
1535        let expected = [[2.0, 1.0, 1.0], [1.0, 2.0, -1.0]]
1536            .into_iter()
1537            .zip([[0.17, 0.29, 0.11], [0.29, 0.17, -0.11]])
1538            .map(|(indices, position)| {
1539                let vector = indices.map(|value| value * reciprocal);
1540                let quadratic = vector
1541                    .iter()
1542                    .zip(symmetric_tensor_vector(tensor[0], vector))
1543                    .map(|(left, right)| left * right)
1544                    .sum::<f64>();
1545                let phase = TWO_PI
1546                    * hkl[0]
1547                        .iter()
1548                        .zip(position)
1549                        .map(|(index, coordinate)| f64::from(*index) * coordinate)
1550                        .sum::<f64>();
1551                let displacement = (-TWO_PI_SQUARED * quadratic).exp();
1552                (displacement * phase.cos(), displacement * phase.sin())
1553            })
1554            .fold((0.0, 0.0), |sum, value| (sum.0 + value.0, sum.1 + value.1));
1555        assert!((values.f_real[0] - 2.4 * expected.0).abs() < 3.0e-14);
1556        assert!((values.f_imag[0] - 2.4 * expected.1).abs() < 3.0e-14);
1557    }
1558
1559    #[test]
1560    fn anisotropic_cell_derivatives_match_centered_differences() {
1561        let hkl = [[2, 1, 3]];
1562        let xyz = [[0.17, 0.23, 0.31]];
1563        let tensor = [[0.020, 0.013, 0.027, 0.002, 0.001, 0.003]];
1564        let evaluate = |cell| {
1565            calculate_structure_factor_dense(
1566                cell,
1567                &p1(),
1568                StructureFactorBatchView {
1569                    hkl: &hkl,
1570                    multiplicity: &[2],
1571                    fractional_xyz: &xyz,
1572                    occupancy: &[0.8],
1573                    u_iso_angstrom2: &[0.37],
1574                    anisotropic_mask: &[true],
1575                    u_aniso_cif_angstrom2: &tensor,
1576                    scattering_real: &[3.0],
1577                    scattering_imag: &[0.2],
1578                    d_scattering_real_d_s: &[0.0],
1579                    d_scattering_imag_d_s: &[0.0],
1580                    correction: &[1.0],
1581                    d_correction_d_q_squared: &[0.0],
1582                    scale: 1.0,
1583                    coordinate_tolerance: 1.0e-10,
1584                },
1585            )
1586            .expect("anisotropic dense result")
1587        };
1588        let cell = UnitCell {
1589            a_angstrom: 4.3,
1590            b_angstrom: 5.1,
1591            c_angstrom: 6.2,
1592            alpha_deg: 78.0,
1593            beta_deg: 83.0,
1594            gamma_deg: 71.0,
1595        };
1596        let actual = evaluate(cell);
1597        for parameter in 0..CELL_PARAMETER_COUNT {
1598            let step = if parameter < 3 { 1.0e-6 } else { 1.0e-5 };
1599            let mut plus = cell;
1600            let mut minus = cell;
1601            perturb_cell(&mut plus, parameter, step);
1602            perturb_cell(&mut minus, parameter, -step);
1603            let plus = evaluate(plus);
1604            let minus = evaluate(minus);
1605            let expected_real = (plus.values.f_real[0] - minus.values.f_real[0]) / (2.0 * step);
1606            let expected_intensity =
1607                (plus.values.intensity[0] - minus.values.intensity[0]) / (2.0 * step);
1608            assert!((actual.d_f_real[parameter] - expected_real).abs() < 2.0e-8);
1609            assert!((actual.d_intensity[parameter] - expected_intensity).abs() < 3.0e-7);
1610        }
1611        assert_eq!(actual.d_f_real[actual.layout.u_iso(0)].to_bits(), 0);
1612        assert_eq!(actual.d_f_imag[actual.layout.u_iso(0)].to_bits(), 0);
1613        assert_eq!(actual.d_intensity[actual.layout.u_iso(0)].to_bits(), 0);
1614    }
1615
1616    #[test]
1617    #[allow(clippy::too_many_lines)]
1618    fn dense_derivatives_include_symmetry_scattering_and_correction_chains() {
1619        let group = inversion();
1620        let hkl = [[2, 1, 1], [1, 3, 2]];
1621        let multiplicity = [4, 2];
1622        let xyz = [[0.17, 0.23, 0.31]];
1623        let occupancy = [0.72];
1624        let u_iso = [0.013];
1625        let scale = 1.6;
1626
1627        let evaluate =
1628            |cell: UnitCell, xyz: &[[f64; 3]], occupancy: &[f64], u_iso: &[f64], scale| {
1629                let geometry = cell.geometry().expect("geometry");
1630                let q_squared: Vec<f64> = hkl
1631                    .iter()
1632                    .map(|&indices| geometry.q_squared(indices))
1633                    .collect();
1634                let s: Vec<f64> = q_squared.iter().map(|value| 0.5 * value.sqrt()).collect();
1635                let real: Vec<f64> = s.iter().map(|value| 4.0 - 0.3 * value).collect();
1636                let imag: Vec<f64> = s.iter().map(|value| 0.2 + 0.1 * value).collect();
1637                let d_real = vec![-0.3; hkl.len()];
1638                let d_imag = vec![0.1; hkl.len()];
1639                let correction: Vec<f64> =
1640                    q_squared.iter().map(|value| 1.0 + 0.2 * value).collect();
1641                let d_correction = vec![0.2; hkl.len()];
1642                calculate_structure_factor_dense(
1643                    cell,
1644                    &group,
1645                    StructureFactorBatchView {
1646                        hkl: &hkl,
1647                        multiplicity: &multiplicity,
1648                        fractional_xyz: xyz,
1649                        occupancy,
1650                        u_iso_angstrom2: u_iso,
1651                        anisotropic_mask: &[false],
1652                        u_aniso_cif_angstrom2: &[[0.0; 6]],
1653                        scattering_real: &real,
1654                        scattering_imag: &imag,
1655                        d_scattering_real_d_s: &d_real,
1656                        d_scattering_imag_d_s: &d_imag,
1657                        correction: &correction,
1658                        d_correction_d_q_squared: &d_correction,
1659                        scale,
1660                        coordinate_tolerance: 1.0e-10,
1661                    },
1662                )
1663                .expect("dense result")
1664            };
1665
1666        let cell = cubic_cell(4.8);
1667        let actual = evaluate(cell, &xyz, &occupancy, &u_iso, scale);
1668        let layout = actual.layout;
1669        let step = 1.0e-6;
1670        for parameter in 0..layout.parameter_count() {
1671            let mut plus_cell = cell;
1672            let mut minus_cell = cell;
1673            let mut plus_xyz = xyz;
1674            let mut minus_xyz = xyz;
1675            let mut plus_occupancy = occupancy;
1676            let mut minus_occupancy = occupancy;
1677            let mut plus_u = u_iso;
1678            let mut minus_u = u_iso;
1679            let mut plus_scale = scale;
1680            let mut minus_scale = scale;
1681            match parameter {
1682                0..=5 => {
1683                    perturb_cell(&mut plus_cell, parameter, step);
1684                    perturb_cell(&mut minus_cell, parameter, -step);
1685                }
1686                value if (CELL_PARAMETER_COUNT..CELL_PARAMETER_COUNT + 3).contains(&value) => {
1687                    let component = value - CELL_PARAMETER_COUNT;
1688                    plus_xyz[0][component] += step;
1689                    minus_xyz[0][component] -= step;
1690                }
1691                value if value == layout.occupancy(0) => {
1692                    plus_occupancy[0] += step;
1693                    minus_occupancy[0] -= step;
1694                }
1695                value if value == layout.u_iso(0) => {
1696                    plus_u[0] += step;
1697                    minus_u[0] -= step;
1698                }
1699                value if value == layout.scale() => {
1700                    plus_scale += step;
1701                    minus_scale -= step;
1702                }
1703                _ => continue,
1704            }
1705            let plus = evaluate(plus_cell, &plus_xyz, &plus_occupancy, &plus_u, plus_scale);
1706            let minus = evaluate(
1707                minus_cell,
1708                &minus_xyz,
1709                &minus_occupancy,
1710                &minus_u,
1711                minus_scale,
1712            );
1713            for reflection in 0..hkl.len() {
1714                let index = parameter * hkl.len() + reflection;
1715                let expected_real = (plus.values.f_real[reflection]
1716                    - minus.values.f_real[reflection])
1717                    / (2.0 * step);
1718                let expected_imag = (plus.values.f_imag[reflection]
1719                    - minus.values.f_imag[reflection])
1720                    / (2.0 * step);
1721                let expected_intensity = (plus.values.intensity[reflection]
1722                    - minus.values.intensity[reflection])
1723                    / (2.0 * step);
1724                assert!((actual.d_f_real[index] - expected_real).abs() < 3.0e-7);
1725                assert!((actual.d_f_imag[index] - expected_imag).abs() < 3.0e-7);
1726                assert!((actual.d_intensity[index] - expected_intensity).abs() < 3.0e-5);
1727            }
1728        }
1729    }
1730
1731    #[test]
1732    fn jvp_and_vjp_match_dense_and_are_adjoint_consistent() {
1733        let hkl = [[1, 2, 1], [2, 1, 3], [3, 2, 1]];
1734        let multiplicity = [2, 4, 2];
1735        let xyz = [[0.17, 0.23, 0.31]];
1736        let occupancy = [0.81];
1737        let u_iso = [0.014];
1738        let scattering_real = [3.9, 3.7, 3.5];
1739        let scattering_imag = [0.1, 0.12, 0.15];
1740        let d_scattering_real = [-0.2, -0.2, -0.2];
1741        let d_scattering_imag = [0.05, 0.05, 0.05];
1742        let correction = [1.1, 1.2, 1.3];
1743        let d_correction = [0.2, 0.2, 0.2];
1744        let batch = StructureFactorBatchView {
1745            hkl: &hkl,
1746            multiplicity: &multiplicity,
1747            fractional_xyz: &xyz,
1748            occupancy: &occupancy,
1749            u_iso_angstrom2: &u_iso,
1750            anisotropic_mask: &[false],
1751            u_aniso_cif_angstrom2: &[[0.0; 6]],
1752            scattering_real: &scattering_real,
1753            scattering_imag: &scattering_imag,
1754            d_scattering_real_d_s: &d_scattering_real,
1755            d_scattering_imag_d_s: &d_scattering_imag,
1756            correction: &correction,
1757            d_correction_d_q_squared: &d_correction,
1758            scale: 1.4,
1759            coordinate_tolerance: 1.0e-10,
1760        };
1761        let cell = cubic_cell(4.7);
1762        let group = inversion();
1763        let dense = calculate_structure_factor_dense(cell, &group, batch).expect("dense");
1764        let tangent: Vec<f64> = (0..dense.layout.parameter_count())
1765            .map(|index| f64::from(u32::try_from(index + 1).expect("small index")) * 1.0e-4)
1766            .collect();
1767        let weights = [0.7, -0.2, 1.1];
1768        let jvp = calculate_structure_factor_jvp(cell, &group, batch, &tangent).expect("JVP");
1769        let vjp =
1770            calculate_structure_factor_intensity_vjp(cell, &group, batch, &weights).expect("VJP");
1771        for reflection in 0..hkl.len() {
1772            let expected_f_real = tangent
1773                .iter()
1774                .enumerate()
1775                .map(|(parameter, value)| {
1776                    value * dense.d_f_real[parameter * hkl.len() + reflection]
1777                })
1778                .sum::<f64>();
1779            let expected_f_imag = tangent
1780                .iter()
1781                .enumerate()
1782                .map(|(parameter, value)| {
1783                    value * dense.d_f_imag[parameter * hkl.len() + reflection]
1784                })
1785                .sum::<f64>();
1786            let expected_intensity = tangent
1787                .iter()
1788                .enumerate()
1789                .map(|(parameter, value)| {
1790                    value * dense.d_intensity[parameter * hkl.len() + reflection]
1791                })
1792                .sum::<f64>();
1793            assert!((jvp.d_f_real[reflection] - expected_f_real).abs() < 2.0e-13);
1794            assert!((jvp.d_f_imag[reflection] - expected_f_imag).abs() < 2.0e-13);
1795            assert!((jvp.d_intensity[reflection] - expected_intensity).abs() < 2.0e-11);
1796        }
1797        for (parameter, actual) in vjp.gradient.iter().copied().enumerate() {
1798            let expected = weights
1799                .iter()
1800                .enumerate()
1801                .map(|(reflection, weight)| {
1802                    weight * dense.d_intensity[parameter * hkl.len() + reflection]
1803                })
1804                .sum::<f64>();
1805            assert!((actual - expected).abs() < 2.0e-10);
1806        }
1807        let forward_dot = jvp
1808            .d_intensity
1809            .iter()
1810            .zip(weights)
1811            .map(|(value, weight)| value * weight)
1812            .sum::<f64>();
1813        let reverse_dot = tangent
1814            .iter()
1815            .zip(vjp.gradient)
1816            .map(|(value, gradient)| value * gradient)
1817            .sum::<f64>();
1818        assert!((forward_dot - reverse_dot).abs() < 2.0e-12);
1819    }
1820
1821    #[test]
1822    fn fixed_chunks_are_bitwise_identical_across_worker_counts() {
1823        let hkl = (0_i32..49)
1824            .map(|index| [index % 5 + 1, (index / 5) % 5, index / 25 + 1])
1825            .collect::<Vec<_>>();
1826        let multiplicity = (0..hkl.len())
1827            .map(|index| 2 + 2 * (index % 3))
1828            .collect::<Vec<_>>();
1829        let site_count = 2;
1830        let scattering_count = u32::try_from(hkl.len() * site_count).expect("small batch");
1831        let scattering_real = (0..scattering_count)
1832            .map(|index| 3.0 + 0.003 * f64::from(index))
1833            .collect::<Vec<_>>();
1834        let scattering_imag = (0..scattering_count)
1835            .map(|index| 0.05 - 0.0002 * f64::from(index))
1836            .collect::<Vec<_>>();
1837        let d_scattering_real = vec![-0.17; hkl.len() * site_count];
1838        let d_scattering_imag = vec![0.03; hkl.len() * site_count];
1839        let reflection_count = u32::try_from(hkl.len()).expect("small batch");
1840        let correction = (0..reflection_count)
1841            .map(|index| 1.0 + 0.001 * f64::from(index))
1842            .collect::<Vec<_>>();
1843        let d_correction = vec![0.04; hkl.len()];
1844        let batch = StructureFactorBatchView {
1845            hkl: &hkl,
1846            multiplicity: &multiplicity,
1847            fractional_xyz: &[[0.13, 0.21, 0.07], [0.31, 0.11, 0.19]],
1848            occupancy: &[0.8, 0.65],
1849            u_iso_angstrom2: &[0.012, 0.018],
1850            anisotropic_mask: &[false, false],
1851            u_aniso_cif_angstrom2: &[[0.0; 6]; 2],
1852            scattering_real: &scattering_real,
1853            scattering_imag: &scattering_imag,
1854            d_scattering_real_d_s: &d_scattering_real,
1855            d_scattering_imag_d_s: &d_scattering_imag,
1856            correction: &correction,
1857            d_correction_d_q_squared: &d_correction,
1858            scale: 1.7,
1859            coordinate_tolerance: 1.0e-10,
1860        };
1861        let cell = cubic_cell(7.3);
1862        let group = inversion();
1863        let serial = ExecutionContext::serial();
1864        let two = ExecutionContext::new(2).expect("two-thread pool");
1865        let three = ExecutionContext::new(3).expect("three-thread pool");
1866        let contexts = [&two, &three];
1867
1868        let expected_values =
1869            calculate_structure_factor_values_with_context(cell, &group, batch, &serial)
1870                .expect("serial values");
1871        let expected_dense =
1872            calculate_structure_factor_dense_with_context(cell, &group, batch, &serial)
1873                .expect("serial dense");
1874        let tangent = (0..expected_dense.layout.parameter_count())
1875            .map(|index| {
1876                1.0e-5 * f64::from(u32::try_from(index + 1).expect("small parameter count"))
1877            })
1878            .collect::<Vec<_>>();
1879        let weights = (0..reflection_count)
1880            .map(|index| 0.2 - 0.01 * f64::from(index))
1881            .collect::<Vec<_>>();
1882        let expected_jvp =
1883            calculate_structure_factor_jvp_with_context(cell, &group, batch, &tangent, &serial)
1884                .expect("serial JVP");
1885        let expected_vjp = calculate_structure_factor_intensity_vjp_with_context(
1886            cell, &group, batch, &weights, &serial,
1887        )
1888        .expect("serial VJP");
1889
1890        for context in contexts {
1891            assert_eq!(
1892                calculate_structure_factor_values_with_context(cell, &group, batch, context)
1893                    .expect("parallel values"),
1894                expected_values
1895            );
1896            assert_eq!(
1897                calculate_structure_factor_dense_with_context(cell, &group, batch, context)
1898                    .expect("parallel dense"),
1899                expected_dense
1900            );
1901            assert_eq!(
1902                calculate_structure_factor_jvp_with_context(
1903                    cell, &group, batch, &tangent, context,
1904                )
1905                .expect("parallel JVP"),
1906                expected_jvp
1907            );
1908            assert_eq!(
1909                calculate_structure_factor_intensity_vjp_with_context(
1910                    cell, &group, batch, &weights, context,
1911                )
1912                .expect("parallel VJP"),
1913                expected_vjp
1914            );
1915        }
1916    }
1917
1918    fn perturb_cell(cell: &mut UnitCell, parameter: usize, change: f64) {
1919        let value = match parameter {
1920            0 => &mut cell.a_angstrom,
1921            1 => &mut cell.b_angstrom,
1922            2 => &mut cell.c_angstrom,
1923            3 => &mut cell.alpha_deg,
1924            4 => &mut cell.beta_deg,
1925            5 => &mut cell.gamma_deg,
1926            _ => panic!("invalid cell parameter"),
1927        };
1928        *value += change;
1929    }
1930
1931    #[test]
1932    fn invalid_shapes_zero_reflections_and_metric_mismatch_are_errors() {
1933        let base = StructureFactorBatchView {
1934            hkl: &[[1, 0, 0]],
1935            multiplicity: &[1],
1936            fractional_xyz: &[[0.0, 0.0, 0.0]],
1937            occupancy: &[1.0],
1938            u_iso_angstrom2: &[0.0],
1939            anisotropic_mask: &[false],
1940            u_aniso_cif_angstrom2: &[[0.0; 6]],
1941            scattering_real: &[1.0],
1942            scattering_imag: &[0.0],
1943            d_scattering_real_d_s: &[0.0],
1944            d_scattering_imag_d_s: &[0.0],
1945            correction: &[1.0],
1946            d_correction_d_q_squared: &[0.0],
1947            scale: 1.0,
1948            coordinate_tolerance: 1.0e-10,
1949        };
1950        let bad_scattering = StructureFactorBatchView {
1951            scattering_real: &[],
1952            ..base
1953        };
1954        assert_eq!(
1955            calculate_structure_factor_values(cubic_cell(4.0), &p1(), bad_scattering),
1956            Err(StructureFactorBatchError::ScatteringShapeMismatch)
1957        );
1958        let zero = [[0, 0, 0]];
1959        let zero_reflection = StructureFactorBatchView { hkl: &zero, ..base };
1960        assert_eq!(
1961            calculate_structure_factor_values(cubic_cell(4.0), &p1(), zero_reflection),
1962            Err(StructureFactorBatchError::ZeroReflection)
1963        );
1964        let invalid_tensor = StructureFactorBatchView {
1965            anisotropic_mask: &[true],
1966            u_aniso_cif_angstrom2: &[[-0.01, 0.01, 0.01, 0.0, 0.0, 0.0]],
1967            ..base
1968        };
1969        assert_eq!(
1970            calculate_structure_factor_values(cubic_cell(4.0), &p1(), invalid_tensor),
1971            Err(StructureFactorBatchError::InvalidPhysicalParameter)
1972        );
1973        let incompatible_cell = UnitCell {
1974            a_angstrom: 4.0,
1975            b_angstrom: 5.0,
1976            c_angstrom: 6.0,
1977            alpha_deg: 90.0,
1978            beta_deg: 90.0,
1979            gamma_deg: 90.0,
1980        };
1981        assert_eq!(
1982            calculate_structure_factor_values(incompatible_cell, &axis_swap_group(), base),
1983            Err(StructureFactorBatchError::CellSymmetryMismatch)
1984        );
1985        assert_eq!(
1986            calculate_structure_factor_jvp(cubic_cell(4.0), &p1(), base, &[]),
1987            Err(StructureFactorBatchError::TangentLengthMismatch)
1988        );
1989        assert_eq!(
1990            calculate_structure_factor_intensity_vjp(cubic_cell(4.0), &p1(), base, &[]),
1991            Err(StructureFactorBatchError::WeightLengthMismatch)
1992        );
1993    }
1994}