1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use crate::cell::{CELL_PARAMETER_COUNT, CellError, CellGeometry, UnitCell};
7
8const TWO_PI: f64 = 2.0 * std::f64::consts::PI;
9const TWO_PI_SQUARED: f64 = 2.0 * std::f64::consts::PI * std::f64::consts::PI;
10
11#[derive(Clone, Copy, Debug)]
13pub struct P1BatchView<'a> {
14 pub hkl: &'a [[i32; 3]],
16 pub fractional_xyz: &'a [[f64; 3]],
18 pub occupancy: &'a [f64],
20 pub u_iso_angstrom2: &'a [f64],
22 pub scattering_real: &'a [f64],
24 pub scattering_imag: &'a [f64],
26 pub scale: f64,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct P1ParameterLayout {
33 pub site_count: usize,
35}
36
37impl P1ParameterLayout {
38 #[must_use]
41 pub const fn parameter_count(self) -> usize {
42 CELL_PARAMETER_COUNT + 5 * self.site_count + 1
43 }
44
45 #[must_use]
47 pub const fn coordinate(self, site: usize, component: usize) -> usize {
48 CELL_PARAMETER_COUNT + 3 * site + component
49 }
50
51 #[must_use]
53 pub const fn occupancy(self, site: usize) -> usize {
54 CELL_PARAMETER_COUNT + 3 * self.site_count + site
55 }
56
57 #[must_use]
59 pub const fn u_iso(self, site: usize) -> usize {
60 CELL_PARAMETER_COUNT + 4 * self.site_count + site
61 }
62
63 #[must_use]
65 pub const fn scale(self) -> usize {
66 CELL_PARAMETER_COUNT + 5 * self.site_count
67 }
68}
69
70#[derive(Clone, Debug, PartialEq)]
72pub struct P1Values {
73 pub f_real: Vec<f64>,
75 pub f_imag: Vec<f64>,
77 pub intensity: Vec<f64>,
79}
80
81#[derive(Clone, Debug, PartialEq)]
83pub struct P1DenseResult {
84 pub values: P1Values,
86 pub d_f_real: Vec<f64>,
88 pub d_f_imag: Vec<f64>,
90 pub d_intensity: Vec<f64>,
92 pub layout: P1ParameterLayout,
94}
95
96#[derive(Clone, Debug, PartialEq)]
98pub struct P1JvpResult {
99 pub values: P1Values,
101 pub d_f_real: Vec<f64>,
103 pub d_f_imag: Vec<f64>,
105 pub d_intensity: Vec<f64>,
107}
108
109#[derive(Clone, Debug, PartialEq)]
111pub struct P1VjpResult {
112 pub values: P1Values,
114 pub gradient: Vec<f64>,
116 pub layout: P1ParameterLayout,
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub enum P1BatchError {
123 Cell(CellError),
125 SiteLengthMismatch,
127 ScatteringShapeMismatch,
129 NonFiniteInput,
131 NegativePhysicalParameter,
133 TangentLengthMismatch,
135 WeightLengthMismatch,
137 AllocationOverflow,
139}
140
141impl Display for P1BatchError {
142 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
143 match self {
144 Self::Cell(error) => Display::fmt(error, formatter),
145 Self::SiteLengthMismatch => {
146 formatter.write_str("P1 site arrays must have equal length")
147 }
148 Self::ScatteringShapeMismatch => formatter
149 .write_str("P1 scattering arrays must have reflection_count * site_count elements"),
150 Self::NonFiniteInput => {
151 formatter.write_str("P1 inputs must contain only finite values")
152 }
153 Self::NegativePhysicalParameter => {
154 formatter.write_str("P1 occupancy, U_iso, and scale must be non-negative")
155 }
156 Self::TangentLengthMismatch => {
157 formatter.write_str("P1 tangent length must equal the parameter count")
158 }
159 Self::WeightLengthMismatch => {
160 formatter.write_str("P1 reverse weights must match the reflection count")
161 }
162 Self::AllocationOverflow => formatter.write_str("P1 derivative allocation overflow"),
163 }
164 }
165}
166
167impl Error for P1BatchError {}
168
169impl From<CellError> for P1BatchError {
170 fn from(value: CellError) -> Self {
171 Self::Cell(value)
172 }
173}
174
175struct ValidatedP1<'a> {
176 batch: P1BatchView<'a>,
177 geometry: CellGeometry,
178 layout: P1ParameterLayout,
179}
180
181pub fn calculate_p1_values(
188 cell: UnitCell,
189 batch: P1BatchView<'_>,
190) -> Result<P1Values, P1BatchError> {
191 let validated = validate(cell, batch)?;
192 let mut values = empty_values(batch.hkl.len());
193 for reflection in 0..batch.hkl.len() {
194 let (f_real, f_imag) = reflection_value(&validated, reflection);
195 values.f_real[reflection] = f_real;
196 values.f_imag[reflection] = f_imag;
197 values.intensity[reflection] = batch.scale * (f_real * f_real + f_imag * f_imag);
198 }
199 Ok(values)
200}
201
202pub fn calculate_p1_dense(
209 cell: UnitCell,
210 batch: P1BatchView<'_>,
211) -> Result<P1DenseResult, P1BatchError> {
212 let validated = validate(cell, batch)?;
213 let reflection_count = batch.hkl.len();
214 let parameter_count = validated.layout.parameter_count();
215 let element_count = parameter_count
216 .checked_mul(reflection_count)
217 .ok_or(P1BatchError::AllocationOverflow)?;
218 let mut result = P1DenseResult {
219 values: empty_values(reflection_count),
220 d_f_real: vec![0.0; element_count],
221 d_f_imag: vec![0.0; element_count],
222 d_intensity: vec![0.0; element_count],
223 layout: validated.layout,
224 };
225 for reflection in 0..reflection_count {
226 calculate_dense_reflection(&validated, reflection, &mut result);
227 }
228 Ok(result)
229}
230
231pub fn calculate_p1_jvp(
239 cell: UnitCell,
240 batch: P1BatchView<'_>,
241 tangent: &[f64],
242) -> Result<P1JvpResult, P1BatchError> {
243 let validated = validate(cell, batch)?;
244 if tangent.len() != validated.layout.parameter_count() {
245 return Err(P1BatchError::TangentLengthMismatch);
246 }
247 if tangent.iter().any(|value| !value.is_finite()) {
248 return Err(P1BatchError::NonFiniteInput);
249 }
250 let reflection_count = batch.hkl.len();
251 let mut result = P1JvpResult {
252 values: empty_values(reflection_count),
253 d_f_real: vec![0.0; reflection_count],
254 d_f_imag: vec![0.0; reflection_count],
255 d_intensity: vec![0.0; reflection_count],
256 };
257 for reflection in 0..reflection_count {
258 calculate_jvp_reflection(&validated, reflection, tangent, &mut result);
259 }
260 Ok(result)
261}
262
263pub fn calculate_p1_intensity_vjp(
271 cell: UnitCell,
272 batch: P1BatchView<'_>,
273 weights: &[f64],
274) -> Result<P1VjpResult, P1BatchError> {
275 let validated = validate(cell, batch)?;
276 if weights.len() != batch.hkl.len() {
277 return Err(P1BatchError::WeightLengthMismatch);
278 }
279 if weights.iter().any(|value| !value.is_finite()) {
280 return Err(P1BatchError::NonFiniteInput);
281 }
282 let mut result = P1VjpResult {
283 values: empty_values(batch.hkl.len()),
284 gradient: vec![0.0; validated.layout.parameter_count()],
285 layout: validated.layout,
286 };
287 let mut site_amplitudes = Vec::new();
288 site_amplitudes
289 .try_reserve_exact(validated.layout.site_count)
290 .map_err(|_| P1BatchError::AllocationOverflow)?;
291 for (reflection, weight) in weights.iter().copied().enumerate() {
292 calculate_vjp_reflection(
293 &validated,
294 reflection,
295 weight,
296 &mut site_amplitudes,
297 &mut result,
298 );
299 }
300 Ok(result)
301}
302
303fn validate(cell: UnitCell, batch: P1BatchView<'_>) -> Result<ValidatedP1<'_>, P1BatchError> {
304 let geometry = cell.geometry()?;
305 let site_count = batch.fractional_xyz.len();
306 if batch.occupancy.len() != site_count || batch.u_iso_angstrom2.len() != site_count {
307 return Err(P1BatchError::SiteLengthMismatch);
308 }
309 let scattering_count = batch
310 .hkl
311 .len()
312 .checked_mul(site_count)
313 .ok_or(P1BatchError::AllocationOverflow)?;
314 if batch.scattering_real.len() != scattering_count
315 || batch.scattering_imag.len() != scattering_count
316 {
317 return Err(P1BatchError::ScatteringShapeMismatch);
318 }
319 if !batch.scale.is_finite()
320 || batch
321 .fractional_xyz
322 .iter()
323 .flatten()
324 .chain(batch.occupancy)
325 .chain(batch.u_iso_angstrom2)
326 .chain(batch.scattering_real)
327 .chain(batch.scattering_imag)
328 .any(|value| !value.is_finite())
329 {
330 return Err(P1BatchError::NonFiniteInput);
331 }
332 if batch.scale < 0.0
333 || batch.occupancy.iter().any(|value| *value < 0.0)
334 || batch.u_iso_angstrom2.iter().any(|value| *value < 0.0)
335 {
336 return Err(P1BatchError::NegativePhysicalParameter);
337 }
338 Ok(ValidatedP1 {
339 batch,
340 geometry,
341 layout: P1ParameterLayout { site_count },
342 })
343}
344
345fn empty_values(reflection_count: usize) -> P1Values {
346 P1Values {
347 f_real: vec![0.0; reflection_count],
348 f_imag: vec![0.0; reflection_count],
349 intensity: vec![0.0; reflection_count],
350 }
351}
352
353fn atom_amplitude(
354 validated: &ValidatedP1<'_>,
355 reflection: usize,
356 site: usize,
357 q_squared: f64,
358) -> (f64, f64, f64, f64) {
359 let batch = validated.batch;
360 let scattering_index = reflection * validated.layout.site_count + site;
361 let phase = TWO_PI
362 * batch.hkl[reflection]
363 .iter()
364 .zip(batch.fractional_xyz[site])
365 .map(|(index, coordinate)| f64::from(*index) * coordinate)
366 .sum::<f64>();
367 let (sin_phase, cos_phase) = phase.sin_cos();
368 let scattering_real = batch.scattering_real[scattering_index];
369 let scattering_imag = batch.scattering_imag[scattering_index];
370 let rotated_real = scattering_real * cos_phase - scattering_imag * sin_phase;
371 let rotated_imag = scattering_real * sin_phase + scattering_imag * cos_phase;
372 let displacement = (-TWO_PI_SQUARED * batch.u_iso_angstrom2[site] * q_squared).exp();
373 let base_real = displacement * rotated_real;
374 let base_imag = displacement * rotated_imag;
375 (
376 base_real,
377 base_imag,
378 batch.occupancy[site] * base_real,
379 batch.occupancy[site] * base_imag,
380 )
381}
382
383fn reflection_value(validated: &ValidatedP1<'_>, reflection: usize) -> (f64, f64) {
384 let q_squared = validated
385 .geometry
386 .q_squared(validated.batch.hkl[reflection]);
387 let mut f_real = 0.0;
388 let mut f_imag = 0.0;
389 for site in 0..validated.layout.site_count {
390 let (_, _, contribution_real, contribution_imag) =
391 atom_amplitude(validated, reflection, site, q_squared);
392 f_real += contribution_real;
393 f_imag += contribution_imag;
394 }
395 (f_real, f_imag)
396}
397
398fn calculate_dense_reflection(
399 validated: &ValidatedP1<'_>,
400 reflection: usize,
401 result: &mut P1DenseResult,
402) {
403 let batch = validated.batch;
404 let reflection_count = batch.hkl.len();
405 let (q_squared, d_q_squared) = validated
406 .geometry
407 .q_squared_and_derivatives(batch.hkl[reflection]);
408 let mut f_real = 0.0;
409 let mut f_imag = 0.0;
410 for site in 0..validated.layout.site_count {
411 let (base_real, base_imag, contribution_real, contribution_imag) =
412 atom_amplitude(validated, reflection, site, q_squared);
413 f_real += contribution_real;
414 f_imag += contribution_imag;
415 for (parameter, d_q) in d_q_squared.iter().copied().enumerate() {
416 let factor = -TWO_PI_SQUARED * batch.u_iso_angstrom2[site] * d_q;
417 set_derivative(
418 result,
419 parameter,
420 reflection,
421 reflection_count,
422 factor * contribution_real,
423 factor * contribution_imag,
424 );
425 }
426 for component in 0..3 {
427 let factor = TWO_PI * f64::from(batch.hkl[reflection][component]);
428 set_derivative(
429 result,
430 validated.layout.coordinate(site, component),
431 reflection,
432 reflection_count,
433 -factor * contribution_imag,
434 factor * contribution_real,
435 );
436 }
437 set_derivative(
438 result,
439 validated.layout.occupancy(site),
440 reflection,
441 reflection_count,
442 base_real,
443 base_imag,
444 );
445 let displacement_factor = -TWO_PI_SQUARED * q_squared;
446 set_derivative(
447 result,
448 validated.layout.u_iso(site),
449 reflection,
450 reflection_count,
451 displacement_factor * contribution_real,
452 displacement_factor * contribution_imag,
453 );
454 }
455 let norm = f_real * f_real + f_imag * f_imag;
456 result.values.f_real[reflection] = f_real;
457 result.values.f_imag[reflection] = f_imag;
458 result.values.intensity[reflection] = batch.scale * norm;
459 for parameter in 0..validated.layout.parameter_count() {
460 let index = parameter * reflection_count + reflection;
461 result.d_intensity[index] =
462 2.0 * batch.scale * (f_real * result.d_f_real[index] + f_imag * result.d_f_imag[index]);
463 }
464 result.d_intensity[validated.layout.scale() * reflection_count + reflection] = norm;
465}
466
467fn set_derivative(
468 result: &mut P1DenseResult,
469 parameter: usize,
470 reflection: usize,
471 reflection_count: usize,
472 real: f64,
473 imag: f64,
474) {
475 let index = parameter * reflection_count + reflection;
476 result.d_f_real[index] += real;
477 result.d_f_imag[index] += imag;
478}
479
480fn calculate_jvp_reflection(
481 validated: &ValidatedP1<'_>,
482 reflection: usize,
483 tangent: &[f64],
484 result: &mut P1JvpResult,
485) {
486 let batch = validated.batch;
487 let (q_squared, d_q_squared) = validated
488 .geometry
489 .q_squared_and_derivatives(batch.hkl[reflection]);
490 let d_q_direction = d_q_squared
491 .iter()
492 .zip(&tangent[..CELL_PARAMETER_COUNT])
493 .map(|(derivative, direction)| derivative * direction)
494 .sum::<f64>();
495 let mut f_real = 0.0;
496 let mut f_imag = 0.0;
497 let mut d_f_real = 0.0;
498 let mut d_f_imag = 0.0;
499 for site in 0..validated.layout.site_count {
500 let (base_real, base_imag, contribution_real, contribution_imag) =
501 atom_amplitude(validated, reflection, site, q_squared);
502 f_real += contribution_real;
503 f_imag += contribution_imag;
504 let phase_direction = TWO_PI
505 * (0..3)
506 .map(|component| {
507 f64::from(batch.hkl[reflection][component])
508 * tangent[validated.layout.coordinate(site, component)]
509 })
510 .sum::<f64>();
511 let displacement_direction = -TWO_PI_SQUARED
512 * (batch.u_iso_angstrom2[site] * d_q_direction
513 + q_squared * tangent[validated.layout.u_iso(site)]);
514 let occupancy_direction = tangent[validated.layout.occupancy(site)];
515 d_f_real += occupancy_direction * base_real + displacement_direction * contribution_real
516 - phase_direction * contribution_imag;
517 d_f_imag += occupancy_direction * base_imag
518 + displacement_direction * contribution_imag
519 + phase_direction * contribution_real;
520 }
521 let norm = f_real * f_real + f_imag * f_imag;
522 result.values.f_real[reflection] = f_real;
523 result.values.f_imag[reflection] = f_imag;
524 result.values.intensity[reflection] = batch.scale * norm;
525 result.d_f_real[reflection] = d_f_real;
526 result.d_f_imag[reflection] = d_f_imag;
527 result.d_intensity[reflection] = 2.0 * batch.scale * (f_real * d_f_real + f_imag * d_f_imag)
528 + tangent[validated.layout.scale()] * norm;
529}
530
531fn calculate_vjp_reflection(
532 validated: &ValidatedP1<'_>,
533 reflection: usize,
534 weight: f64,
535 site_amplitudes: &mut Vec<(f64, f64, f64, f64)>,
536 result: &mut P1VjpResult,
537) {
538 let batch = validated.batch;
539 let (q_squared, d_q_squared) = validated
540 .geometry
541 .q_squared_and_derivatives(batch.hkl[reflection]);
542 let mut f_real = 0.0;
543 let mut f_imag = 0.0;
544 site_amplitudes.clear();
545 for site in 0..validated.layout.site_count {
546 let amplitude = atom_amplitude(validated, reflection, site, q_squared);
547 f_real += amplitude.2;
548 f_imag += amplitude.3;
549 site_amplitudes.push(amplitude);
550 }
551 let norm = f_real * f_real + f_imag * f_imag;
552 result.values.f_real[reflection] = f_real;
553 result.values.f_imag[reflection] = f_imag;
554 result.values.intensity[reflection] = batch.scale * norm;
555 let intensity_factor = 2.0 * batch.scale * weight;
556 for (site, &(base_real, base_imag, contribution_real, contribution_imag)) in
557 site_amplitudes.iter().enumerate()
558 {
559 for (parameter, d_q) in d_q_squared.iter().copied().enumerate() {
560 let factor = -TWO_PI_SQUARED * batch.u_iso_angstrom2[site] * d_q;
561 result.gradient[parameter] += intensity_factor
562 * factor
563 * (f_real * contribution_real + f_imag * contribution_imag);
564 }
565 for component in 0..3 {
566 let factor = TWO_PI * f64::from(batch.hkl[reflection][component]);
567 let d_real = -factor * contribution_imag;
568 let d_imag = factor * contribution_real;
569 result.gradient[validated.layout.coordinate(site, component)] +=
570 intensity_factor * (f_real * d_real + f_imag * d_imag);
571 }
572 result.gradient[validated.layout.occupancy(site)] +=
573 intensity_factor * (f_real * base_real + f_imag * base_imag);
574 let displacement_factor = -TWO_PI_SQUARED * q_squared;
575 result.gradient[validated.layout.u_iso(site)] += intensity_factor
576 * displacement_factor
577 * (f_real * contribution_real + f_imag * contribution_imag);
578 }
579 result.gradient[validated.layout.scale()] += weight * norm;
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 fn cell() -> UnitCell {
587 UnitCell {
588 a_angstrom: 4.2,
589 b_angstrom: 5.1,
590 c_angstrom: 6.3,
591 alpha_deg: 79.0,
592 beta_deg: 83.0,
593 gamma_deg: 74.0,
594 }
595 }
596
597 #[test]
598 fn one_origin_atom_has_closed_form_value() {
599 let hkl = [[1, 2, 3], [0, 0, 0]];
600 let xyz = [[0.0, 0.0, 0.0]];
601 let occupancy = [0.75];
602 let u_iso = [0.0];
603 let real = [2.0, 3.0];
604 let imag = [-0.5, 0.25];
605 let values = calculate_p1_values(
606 cell(),
607 P1BatchView {
608 hkl: &hkl,
609 fractional_xyz: &xyz,
610 occupancy: &occupancy,
611 u_iso_angstrom2: &u_iso,
612 scattering_real: &real,
613 scattering_imag: &imag,
614 scale: 2.0,
615 },
616 )
617 .expect("P1 values");
618 assert_eq!(values.f_real, vec![1.5, 2.25]);
619 assert_eq!(values.f_imag, vec![-0.375, 0.1875]);
620 assert!((values.intensity[0] - 2.0 * (1.5_f64.powi(2) + 0.375_f64.powi(2))).abs() < 1e-15);
621 }
622
623 #[test]
624 fn lattice_translation_and_origin_shift_preserve_intensity() {
625 let hkl = [[1, -2, 3], [2, 1, -1]];
626 let occupancy = [0.8, 0.6];
627 let u_iso = [0.01, 0.02];
628 let real = [2.0, 1.0, 1.5, 0.7];
629 let imag = [0.1, -0.2, 0.05, 0.15];
630 let original_xyz = [[0.17, 0.29, 0.43], [0.61, 0.11, 0.37]];
631 let shifted_xyz = [[1.37, -0.01, 2.83], [1.81, -0.19, 2.77]];
632 let original = calculate_p1_values(
633 cell(),
634 P1BatchView {
635 hkl: &hkl,
636 fractional_xyz: &original_xyz,
637 occupancy: &occupancy,
638 u_iso_angstrom2: &u_iso,
639 scattering_real: &real,
640 scattering_imag: &imag,
641 scale: 1.3,
642 },
643 )
644 .expect("original");
645 let shifted = calculate_p1_values(
646 cell(),
647 P1BatchView {
648 hkl: &hkl,
649 fractional_xyz: &shifted_xyz,
650 occupancy: &occupancy,
651 u_iso_angstrom2: &u_iso,
652 scattering_real: &real,
653 scattering_imag: &imag,
654 scale: 1.3,
655 },
656 )
657 .expect("shifted");
658 for (left, right) in original.intensity.iter().zip(shifted.intensity) {
659 assert!((left - right).abs() < 2.0e-13 * left.abs().max(1.0));
660 }
661 }
662
663 #[test]
664 fn jvp_and_vjp_match_dense_jacobian() {
665 let hkl = [[1, 0, 1], [2, -1, 3], [-1, 2, 2]];
666 let xyz = [[0.17, 0.29, 0.43], [0.61, 0.11, 0.37]];
667 let occupancy = [0.8, 0.6];
668 let u_iso = [0.01, 0.02];
669 let real = [2.0, 1.0, 1.5, 0.7, 0.9, 1.2];
670 let imag = [0.1, -0.2, 0.05, 0.15, -0.1, 0.2];
671 let batch = P1BatchView {
672 hkl: &hkl,
673 fractional_xyz: &xyz,
674 occupancy: &occupancy,
675 u_iso_angstrom2: &u_iso,
676 scattering_real: &real,
677 scattering_imag: &imag,
678 scale: 1.3,
679 };
680 let dense = calculate_p1_dense(cell(), batch).expect("dense");
681 let tangent: Vec<f64> = (0..dense.layout.parameter_count())
682 .map(|index| {
683 (f64::from(u32::try_from(index).expect("small test parameter count")) + 1.0)
684 * 1.0e-3
685 })
686 .collect();
687 let weights = [0.3, -0.7, 1.1];
688 let jvp = calculate_p1_jvp(cell(), batch, &tangent).expect("jvp");
689 let vjp = calculate_p1_intensity_vjp(cell(), batch, &weights).expect("vjp");
690 for reflection in 0..hkl.len() {
691 let expected = (0..dense.layout.parameter_count())
692 .map(|parameter| {
693 dense.d_intensity[parameter * hkl.len() + reflection] * tangent[parameter]
694 })
695 .sum::<f64>();
696 assert!((jvp.d_intensity[reflection] - expected).abs() < 2.0e-12);
697 }
698 for parameter in 0..dense.layout.parameter_count() {
699 let expected = (0..hkl.len())
700 .map(|reflection| {
701 dense.d_intensity[parameter * hkl.len() + reflection] * weights[reflection]
702 })
703 .sum::<f64>();
704 assert!((vjp.gradient[parameter] - expected).abs() < 2.0e-12);
705 }
706 let forward_dot = jvp
707 .d_intensity
708 .iter()
709 .zip(weights)
710 .map(|(value, weight)| value * weight)
711 .sum::<f64>();
712 let reverse_dot = vjp
713 .gradient
714 .iter()
715 .zip(tangent)
716 .map(|(value, direction)| value * direction)
717 .sum::<f64>();
718 assert!((forward_dot - reverse_dot).abs() < 2.0e-12);
719 }
720
721 #[test]
722 #[allow(clippy::too_many_lines)]
723 fn dense_derivatives_match_centered_differences() {
724 let hkl = [[1, 2, -1]];
725 let xyz = [[0.17, 0.29, 0.43]];
726 let occupancy = [0.8];
727 let u_iso = [0.01];
728 let real = [2.0];
729 let imag = [0.1];
730 let base_cell = cell();
731 let layout = P1ParameterLayout { site_count: 1 };
732 let evaluate =
733 |cell: UnitCell, xyz: &[[f64; 3]], occupancy: &[f64], u_iso: &[f64], scale| {
734 calculate_p1_values(
735 cell,
736 P1BatchView {
737 hkl: &hkl,
738 fractional_xyz: xyz,
739 occupancy,
740 u_iso_angstrom2: u_iso,
741 scattering_real: &real,
742 scattering_imag: &imag,
743 scale,
744 },
745 )
746 .expect("values")
747 .intensity[0]
748 };
749 let batch = P1BatchView {
750 hkl: &hkl,
751 fractional_xyz: &xyz,
752 occupancy: &occupancy,
753 u_iso_angstrom2: &u_iso,
754 scattering_real: &real,
755 scattering_imag: &imag,
756 scale: 1.3,
757 };
758 let dense = calculate_p1_dense(base_cell, batch).expect("dense");
759 for parameter in 0..layout.parameter_count() {
760 let step = if parameter < 3 { 1e-6 } else { 1e-7 };
761 let mut plus_cell = base_cell;
762 let mut minus_cell = base_cell;
763 let mut plus_xyz = xyz;
764 let mut minus_xyz = xyz;
765 let mut plus_occupancy = occupancy;
766 let mut minus_occupancy = occupancy;
767 let mut plus_u = u_iso;
768 let mut minus_u = u_iso;
769 let mut plus_scale = 1.3;
770 let mut minus_scale = 1.3;
771 match parameter {
772 0 => {
773 plus_cell.a_angstrom += step;
774 minus_cell.a_angstrom -= step;
775 }
776 1 => {
777 plus_cell.b_angstrom += step;
778 minus_cell.b_angstrom -= step;
779 }
780 2 => {
781 plus_cell.c_angstrom += step;
782 minus_cell.c_angstrom -= step;
783 }
784 3 => {
785 plus_cell.alpha_deg += step;
786 minus_cell.alpha_deg -= step;
787 }
788 4 => {
789 plus_cell.beta_deg += step;
790 minus_cell.beta_deg -= step;
791 }
792 5 => {
793 plus_cell.gamma_deg += step;
794 minus_cell.gamma_deg -= step;
795 }
796 value if value == layout.coordinate(0, 0) => {
797 plus_xyz[0][0] += step;
798 minus_xyz[0][0] -= step;
799 }
800 value if value == layout.coordinate(0, 1) => {
801 plus_xyz[0][1] += step;
802 minus_xyz[0][1] -= step;
803 }
804 value if value == layout.coordinate(0, 2) => {
805 plus_xyz[0][2] += step;
806 minus_xyz[0][2] -= step;
807 }
808 value if value == layout.occupancy(0) => {
809 plus_occupancy[0] += step;
810 minus_occupancy[0] -= step;
811 }
812 value if value == layout.u_iso(0) => {
813 plus_u[0] += step;
814 minus_u[0] -= step;
815 }
816 value if value == layout.scale() => {
817 plus_scale += step;
818 minus_scale -= step;
819 }
820 _ => unreachable!(),
821 }
822 let plus = evaluate(plus_cell, &plus_xyz, &plus_occupancy, &plus_u, plus_scale);
823 let minus = evaluate(
824 minus_cell,
825 &minus_xyz,
826 &minus_occupancy,
827 &minus_u,
828 minus_scale,
829 );
830 let finite = (plus - minus) / (2.0 * step);
831 assert!((dense.d_intensity[parameter] - finite).abs() < 3.0e-7 * finite.abs().max(1.0));
832 }
833 }
834
835 #[test]
836 fn invalid_batch_shapes_are_rejected() {
837 let hkl = [[1, 0, 0]];
838 let xyz = [[0.0, 0.0, 0.0]];
839 let occupancy = [1.0];
840 let empty = [];
841 let error = calculate_p1_values(
842 cell(),
843 P1BatchView {
844 hkl: &hkl,
845 fractional_xyz: &xyz,
846 occupancy: &occupancy,
847 u_iso_angstrom2: &empty,
848 scattering_real: &empty,
849 scattering_imag: &empty,
850 scale: 1.0,
851 },
852 )
853 .expect_err("invalid lengths");
854 assert_eq!(error, P1BatchError::SiteLengthMismatch);
855 }
856}