1use 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#[derive(Clone, Copy, Debug)]
17pub struct StructureFactorBatchView<'a> {
18 pub hkl: &'a [[i32; 3]],
20 pub multiplicity: &'a [usize],
22 pub fractional_xyz: &'a [[f64; 3]],
24 pub occupancy: &'a [f64],
26 pub u_iso_angstrom2: &'a [f64],
28 pub anisotropic_mask: &'a [bool],
30 pub u_aniso_cif_angstrom2: &'a [[f64; 6]],
32 pub scattering_real: &'a [f64],
34 pub scattering_imag: &'a [f64],
36 pub d_scattering_real_d_s: &'a [f64],
38 pub d_scattering_imag_d_s: &'a [f64],
40 pub correction: &'a [f64],
42 pub d_correction_d_q_squared: &'a [f64],
44 pub scale: f64,
46 pub coordinate_tolerance: f64,
48}
49
50#[derive(Clone, Debug, PartialEq)]
52pub struct StructureFactorValues {
53 pub f_real: Vec<f64>,
55 pub f_imag: Vec<f64>,
57 pub f_squared: Vec<f64>,
59 pub intensity: Vec<f64>,
61 pub q_squared_inverse_angstrom2: Vec<f64>,
63 pub s_inverse_angstrom: Vec<f64>,
65}
66
67#[derive(Clone, Debug, PartialEq)]
69pub struct StructureFactorDenseResult {
70 pub values: StructureFactorValues,
72 pub d_f_real: Vec<f64>,
74 pub d_f_imag: Vec<f64>,
76 pub d_intensity: Vec<f64>,
78 pub layout: P1ParameterLayout,
80}
81
82#[derive(Clone, Debug, PartialEq)]
84pub struct StructureFactorJvpResult {
85 pub values: StructureFactorValues,
87 pub d_f_real: Vec<f64>,
89 pub d_f_imag: Vec<f64>,
91 pub d_intensity: Vec<f64>,
93}
94
95#[derive(Clone, Debug, PartialEq)]
97pub struct StructureFactorVjpResult {
98 pub values: StructureFactorValues,
100 pub gradient: Vec<f64>,
102 pub layout: P1ParameterLayout,
104}
105
106#[derive(Clone, Debug, PartialEq)]
108pub enum StructureFactorBatchError {
109 Cell(CellError),
111 Symmetry(SymmetryError),
113 CellSymmetryMismatch,
115 SiteLengthMismatch,
117 ReflectionLengthMismatch,
119 ScatteringShapeMismatch,
121 NonFiniteInput,
123 InvalidPhysicalParameter,
125 ZeroReflection,
127 AllocationOverflow,
129 TangentLengthMismatch,
131 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
220pub 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
239pub 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
273pub 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
295pub 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
354pub 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
375pub 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
428pub 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
449pub 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}