1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use phasesmith_core::{
7 Accumulation, ConstantWavelengthInstrument, CwContributionsError, CwContributionsView, CwError,
8 FcjGeometry, GridView, ProfileError, SupportPolicy,
9 accumulate_cw_contributions_batch_with_context,
10 accumulate_cw_fcj_contributions_batch_with_context,
11};
12use phasesmith_crystallography::{
13 CellError, IntegratedIntensityCorrection, IntegratedIntensityCorrectionError,
14 IntegratedIntensityCorrectionModel, PreparedNeutronScattering, PreparedXrayScattering,
15 ScatteringBatch, ScatteringError, SpaceGroup, StructureFactorBatchError,
16 StructureFactorBatchView, StructureFactorValues, UnitCell,
17 calculate_structure_factor_dense_with_context,
18 calculate_structure_factor_intensity_vjp_with_context,
19 calculate_structure_factor_jvp_with_context, calculate_structure_factor_values_with_context,
20};
21use phasesmith_execution::ExecutionContext;
22
23const CELL_PARAMETER_COUNT: usize = 6;
24pub(crate) const CW_INSTRUMENT_PARAMETER_COUNT: usize = 5;
25const DEGREES_PER_RADIAN: f64 = 180.0 / std::f64::consts::PI;
26
27#[derive(Clone, Copy, Debug, PartialEq)]
29pub struct MonochromaticPositionCorrection {
30 pub zero_shift_deg: f64,
32 pub bragg_brentano_mm: Option<(f64, f64)>,
34 pub debye_scherrer_micrometre: Option<(f64, f64, f64)>,
37}
38
39#[derive(Clone, Debug, PartialEq)]
41pub struct MonochromaticReflectionGeometry {
42 pub d_spacing_angstrom: Vec<f64>,
44 pub two_theta_deg: Vec<f64>,
46}
47
48pub fn calculate_monochromatic_reflection_geometry(
55 cell: UnitCell,
56 hkl: &[[i32; 3]],
57 instrument: ConstantWavelengthInstrument,
58 position_correction: MonochromaticPositionCorrection,
59) -> Result<MonochromaticReflectionGeometry, StructuralPatternError> {
60 instrument
61 .validate()
62 .map_err(StructuralPatternError::InvalidInstrument)?;
63 validate_position_correction(position_correction)?;
64 let geometry = cell
65 .geometry()
66 .map_err(StructureFactorBatchError::Cell)
67 .map_err(StructuralPatternError::StructureFactor)?;
68 let mut d_spacing_angstrom = Vec::with_capacity(hkl.len());
69 let mut two_theta_deg = Vec::with_capacity(hkl.len());
70 for &reflection in hkl {
71 let q_value = geometry.q_squared(reflection);
72 if !q_value.is_finite() || q_value <= 0.0 {
73 return Err(StructuralPatternError::ReflectionOutsideAngularDomain);
74 }
75 let root_q = q_value.sqrt();
76 let sin_theta = 0.5 * instrument.wavelength_angstrom * root_q;
77 if !(0.0..1.0).contains(&sin_theta) {
78 return Err(StructuralPatternError::ReflectionOutsideAngularDomain);
79 }
80 d_spacing_angstrom.push(root_q.recip());
81 two_theta_deg.push(
82 corrected_monochromatic_position(2.0 * sin_theta.asin(), position_correction)
83 .position_deg,
84 );
85 }
86 Ok(MonochromaticReflectionGeometry {
87 d_spacing_angstrom,
88 two_theta_deg,
89 })
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum BuiltInScatteringModel {
95 XrayNonResonant,
97 NeutronNuclear,
99}
100
101#[derive(Clone, Copy, Debug)]
103pub struct StructuralPatternInputView<'a> {
104 pub x_deg: &'a [f64],
106 pub hkl: &'a [[i32; 3]],
108 pub multiplicity: &'a [usize],
110 pub fractional_xyz: &'a [[f64; 3]],
112 pub occupancy: &'a [f64],
114 pub u_iso_angstrom2: &'a [f64],
116 pub anisotropic_mask: &'a [bool],
118 pub u_aniso_cif_angstrom2: &'a [[f64; 6]],
120 pub scattering_species: &'a [&'a str],
122 pub scattering_real_offset: &'a [f64],
124 pub scattering_imag_offset: &'a [f64],
126 pub scale: f64,
128 pub coordinate_tolerance: f64,
130 pub instrument: ConstantWavelengthInstrument,
132 pub axial_geometry: Option<FcjGeometry>,
134 pub position_correction: MonochromaticPositionCorrection,
136 pub correction_model: IntegratedIntensityCorrectionModel,
138 pub scattering_model: BuiltInScatteringModel,
140 pub contributions: CwContributionsView<'a>,
142 pub support: SupportPolicy,
144}
145
146#[derive(Clone, Debug, PartialEq)]
148pub struct StructuralPatternResult {
149 pub structure_factors: StructureFactorValues,
151 pub d_spacing_angstrom: Vec<f64>,
153 pub two_theta_deg: Vec<f64>,
155 pub accumulation: Accumulation,
157}
158
159#[derive(Clone, Debug, PartialEq)]
161pub struct StructuralPatternJvpResult {
162 pub result: StructuralPatternResult,
164 pub d_y: Vec<f64>,
166 pub d_integrated_intensity: Vec<f64>,
168 pub d_two_theta_deg: Vec<f64>,
170}
171
172#[derive(Clone, Debug, PartialEq)]
174pub struct StructuralPatternDenseResult {
175 pub result: StructuralPatternResult,
177 pub d_y: Vec<f64>,
179 pub parameter_count: usize,
181}
182
183#[derive(Clone, Debug, PartialEq)]
185pub struct StructuralPatternVjpResult {
186 pub result: StructuralPatternResult,
188 pub gradient: Vec<f64>,
190}
191
192#[derive(Debug)]
194pub enum StructuralPatternError {
195 InvalidCell(CellError),
197 ReflectionLengthMismatch,
199 SiteLengthMismatch,
201 SpeciesLengthMismatch,
203 ScatteringOffsetLengthMismatch,
205 NonFiniteScatteringOffset,
207 UnsupportedScatteringOffset,
209 Scattering(ScatteringError),
211 Correction(IntegratedIntensityCorrectionError),
213 StructureFactor(StructureFactorBatchError),
215 Profile(ProfileError),
217 Contributions(CwContributionsError),
219 ReflectionOutsideAngularDomain,
221 InvalidInstrument(CwError),
223 InvalidPositionCorrection,
225 PatternWeightLengthMismatch,
227 NonFinitePatternWeight,
229}
230
231impl Display for StructuralPatternError {
232 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
233 match self {
234 Self::InvalidCell(error) => Display::fmt(error, formatter),
235 Self::ReflectionLengthMismatch => {
236 formatter.write_str("hkl and multiplicity must have the same reflection count")
237 }
238 Self::SiteLengthMismatch => {
239 formatter.write_str("all structural site arrays must have the same site count")
240 }
241 Self::SpeciesLengthMismatch => {
242 formatter.write_str("scattering species must contain one key per asymmetric site")
243 }
244 Self::ScatteringOffsetLengthMismatch => formatter
245 .write_str("scattering offset vectors must both be empty or match the site count"),
246 Self::NonFiniteScatteringOffset => {
247 formatter.write_str("scattering offsets must be finite")
248 }
249 Self::UnsupportedScatteringOffset => formatter
250 .write_str("fixed scattering offsets are supported only for X-ray scattering"),
251 Self::Scattering(error) => Display::fmt(error, formatter),
252 Self::Correction(error) => Display::fmt(error, formatter),
253 Self::StructureFactor(error) => Display::fmt(error, formatter),
254 Self::Profile(error) => Display::fmt(error, formatter),
255 Self::Contributions(error) => Display::fmt(error, formatter),
256 Self::ReflectionOutsideAngularDomain => formatter.write_str(
257 "structural CW reflections must lie strictly within 0 < 2theta < 180 degrees",
258 ),
259 Self::InvalidInstrument(error) => Display::fmt(error, formatter),
260 Self::InvalidPositionCorrection => formatter.write_str(
261 "position corrections must be finite and goniometer radius must be positive",
262 ),
263 Self::PatternWeightLengthMismatch => {
264 formatter.write_str("pattern reverse weights must match the sample count")
265 }
266 Self::NonFinitePatternWeight => {
267 formatter.write_str("pattern reverse weights must be finite")
268 }
269 }
270 }
271}
272
273impl Error for StructuralPatternError {}
274
275struct PreparedNumerics {
276 scattering: ScatteringBatch,
277 correction: IntegratedIntensityCorrection,
278 d_spacing: Vec<f64>,
279 two_theta_deg: Vec<f64>,
280 d_two_theta_d_cell: Vec<[f64; CELL_PARAMETER_COUNT]>,
281 d_two_theta_d_wavelength: Vec<f64>,
282 d_two_theta_d_sample_displacement: Option<Vec<f64>>,
283 d_two_theta_d_displace_x: Option<Vec<f64>>,
284 d_two_theta_d_displace_y: Option<Vec<f64>>,
285}
286
287#[derive(Clone, Copy, Debug)]
288struct CorrectedPosition {
289 position_deg: f64,
290 d_position_d_base: f64,
291 d_position_d_sample_displacement: Option<f64>,
292 d_position_d_displace_x: Option<f64>,
293 d_position_d_displace_y: Option<f64>,
294}
295
296fn corrected_monochromatic_position(
297 base_position_radians: f64,
298 correction: MonochromaticPositionCorrection,
299) -> CorrectedPosition {
300 let mut result = CorrectedPosition {
301 position_deg: base_position_radians.to_degrees() + correction.zero_shift_deg,
302 d_position_d_base: 1.0,
303 d_position_d_sample_displacement: None,
304 d_position_d_displace_x: None,
305 d_position_d_displace_y: None,
306 };
307 if let Some((displacement, radius)) = correction.bragg_brentano_mm {
308 let theta = 0.5 * base_position_radians;
309 result.position_deg -= 2.0 * displacement / radius * theta.cos() * DEGREES_PER_RADIAN;
310 result.d_position_d_base += displacement / radius * theta.sin();
311 result.d_position_d_sample_displacement =
312 Some(-2.0 / radius * theta.cos() * DEGREES_PER_RADIAN);
313 }
314 if let Some((displace_x, displace_y, radius)) = correction.debye_scherrer_micrometre {
315 let (sin_position, cos_position) = base_position_radians.sin_cos();
316 let displacement_scale = 0.18 / (std::f64::consts::PI * radius);
317 result.position_deg -=
318 displacement_scale * (displace_x * cos_position + displace_y * sin_position);
319 result.d_position_d_base += displacement_scale.to_radians()
320 * (displace_x * sin_position - displace_y * cos_position);
321 result.d_position_d_displace_x = Some(-displacement_scale * cos_position);
322 result.d_position_d_displace_y = Some(-displacement_scale * sin_position);
323 }
324 result
325}
326
327fn validate_position_correction(
328 correction: MonochromaticPositionCorrection,
329) -> Result<(), StructuralPatternError> {
330 let invalid = !correction.zero_shift_deg.is_finite()
331 || (correction.bragg_brentano_mm.is_some()
332 && correction.debye_scherrer_micrometre.is_some())
333 || correction
334 .bragg_brentano_mm
335 .is_some_and(|(displacement, radius)| {
336 !displacement.is_finite() || !radius.is_finite() || radius <= 0.0
337 })
338 || correction
339 .debye_scherrer_micrometre
340 .is_some_and(|(displace_x, displace_y, radius)| {
341 !displace_x.is_finite()
342 || !displace_y.is_finite()
343 || !radius.is_finite()
344 || radius <= 0.0
345 });
346 if invalid {
347 return Err(StructuralPatternError::InvalidPositionCorrection);
348 }
349 Ok(())
350}
351
352fn validate_scattering_offsets(
353 input: &StructuralPatternInputView<'_>,
354) -> Result<(), StructuralPatternError> {
355 if input.scattering_real_offset.is_empty() && input.scattering_imag_offset.is_empty() {
356 return Ok(());
357 }
358 if input.scattering_real_offset.len() != input.fractional_xyz.len()
359 || input.scattering_imag_offset.len() != input.fractional_xyz.len()
360 {
361 return Err(StructuralPatternError::ScatteringOffsetLengthMismatch);
362 }
363 if input
364 .scattering_real_offset
365 .iter()
366 .chain(input.scattering_imag_offset)
367 .any(|value| !value.is_finite())
368 {
369 return Err(StructuralPatternError::NonFiniteScatteringOffset);
370 }
371 if input.scattering_model == BuiltInScatteringModel::NeutronNuclear
372 && input
373 .scattering_real_offset
374 .iter()
375 .chain(input.scattering_imag_offset)
376 .any(|value| *value != 0.0)
377 {
378 return Err(StructuralPatternError::UnsupportedScatteringOffset);
379 }
380 Ok(())
381}
382
383fn apply_scattering_offsets(
384 scattering: &mut ScatteringBatch,
385 input: &StructuralPatternInputView<'_>,
386) {
387 if input.scattering_real_offset.is_empty() {
388 return;
389 }
390 for reflection in 0..scattering.reflection_count {
391 for site in 0..scattering.site_count {
392 let index = reflection * scattering.site_count + site;
393 scattering.real[index] += input.scattering_real_offset[site];
394 scattering.imag[index] += input.scattering_imag_offset[site];
395 }
396 }
397}
398
399impl PreparedNumerics {
400 fn structure_batch<'a>(
401 &'a self,
402 input: &StructuralPatternInputView<'a>,
403 ) -> StructureFactorBatchView<'a> {
404 StructureFactorBatchView {
405 hkl: input.hkl,
406 multiplicity: input.multiplicity,
407 fractional_xyz: input.fractional_xyz,
408 occupancy: input.occupancy,
409 u_iso_angstrom2: input.u_iso_angstrom2,
410 anisotropic_mask: input.anisotropic_mask,
411 u_aniso_cif_angstrom2: input.u_aniso_cif_angstrom2,
412 scattering_real: &self.scattering.real,
413 scattering_imag: &self.scattering.imag,
414 d_scattering_real_d_s: &self.scattering.d_real_d_s,
415 d_scattering_imag_d_s: &self.scattering.d_imag_d_s,
416 correction: &self.correction.values,
417 d_correction_d_q_squared: &self.correction.d_values_d_q_squared,
418 scale: input.scale,
419 coordinate_tolerance: input.coordinate_tolerance,
420 }
421 }
422}
423
424pub fn calculate_structural_pattern(
431 cell: UnitCell,
432 space_group: &SpaceGroup,
433 input: &StructuralPatternInputView<'_>,
434) -> Result<StructuralPatternResult, StructuralPatternError> {
435 calculate_structural_pattern_with_context(cell, space_group, input, &ExecutionContext::serial())
436}
437
438pub fn calculate_structural_pattern_with_context(
444 cell: UnitCell,
445 space_group: &SpaceGroup,
446 input: &StructuralPatternInputView<'_>,
447 execution: &ExecutionContext,
448) -> Result<StructuralPatternResult, StructuralPatternError> {
449 let prepared = prepare(cell, input)?;
450 calculate_values(cell, space_group, input, &prepared, execution)
451}
452
453pub fn calculate_structural_pattern_dense(
459 cell: UnitCell,
460 space_group: &SpaceGroup,
461 input: &StructuralPatternInputView<'_>,
462) -> Result<StructuralPatternDenseResult, StructuralPatternError> {
463 calculate_structural_pattern_dense_with_context(
464 cell,
465 space_group,
466 input,
467 &ExecutionContext::serial(),
468 )
469}
470
471pub fn calculate_structural_pattern_dense_with_context(
477 cell: UnitCell,
478 space_group: &SpaceGroup,
479 input: &StructuralPatternInputView<'_>,
480 execution: &ExecutionContext,
481) -> Result<StructuralPatternDenseResult, StructuralPatternError> {
482 let prepared = prepare(cell, input)?;
483 let structural = calculate_structure_factor_dense_with_context(
484 cell,
485 space_group,
486 prepared.structure_batch(input),
487 execution,
488 )
489 .map_err(StructuralPatternError::StructureFactor)?;
490 let parameter_count = structural.layout.parameter_count();
491 let sample_count = input.x_deg.len();
492 let element_count =
493 parameter_count
494 .checked_mul(sample_count)
495 .ok_or(StructuralPatternError::Contributions(
496 CwContributionsError::AllocationOverflow,
497 ))?;
498 let mut accumulation = accumulate(
499 input,
500 &prepared.two_theta_deg,
501 &structural.values.intensity,
502 execution,
503 )?;
504 append_instrument_derivatives(&mut accumulation, &structural.values, input, &prepared)?;
505 let reflection_count = input.hkl.len();
506 let local = &accumulation.derivatives.local;
507 let d_y = if execution.threads() == 1 || parameter_count < 2 {
508 let mut values = zeroed_values(element_count)?;
509 for reflection in 0..reflection_count {
510 let begin = local.offsets[reflection];
511 let end = local.offsets[reflection + 1];
512 for active in begin..end {
513 let sample = local.starts[reflection] + active - begin;
514 let local_base = 2 * active;
515 for parameter in 0..parameter_count {
516 let structural_index = parameter * reflection_count + reflection;
517 let position_derivative = if parameter < CELL_PARAMETER_COUNT {
518 prepared.d_two_theta_d_cell[reflection][parameter]
519 } else {
520 0.0
521 };
522 values[parameter * sample_count + sample] += local.values[local_base]
523 * structural.d_intensity[structural_index]
524 + local.values[local_base + 1] * position_derivative;
525 }
526 }
527 }
528 values
529 } else {
530 let rows = execution.map_ordered(parameter_count, 2, |parameter| {
531 let mut row = zeroed_values(sample_count)?;
532 for reflection in 0..reflection_count {
533 let begin = local.offsets[reflection];
534 let end = local.offsets[reflection + 1];
535 for active in begin..end {
536 let sample = local.starts[reflection] + active - begin;
537 let local_base = 2 * active;
538 let structural_index = parameter * reflection_count + reflection;
539 let position_derivative = if parameter < CELL_PARAMETER_COUNT {
540 prepared.d_two_theta_d_cell[reflection][parameter]
541 } else {
542 0.0
543 };
544 row[sample] += local.values[local_base]
545 * structural.d_intensity[structural_index]
546 + local.values[local_base + 1] * position_derivative;
547 }
548 }
549 Ok::<_, StructuralPatternError>(row)
550 });
551 let mut values = Vec::new();
552 values.try_reserve_exact(element_count).map_err(|_| {
553 StructuralPatternError::Contributions(CwContributionsError::AllocationOverflow)
554 })?;
555 for row in rows {
556 values.extend(row?);
557 }
558 values
559 };
560 Ok(StructuralPatternDenseResult {
561 result: StructuralPatternResult {
562 structure_factors: structural.values,
563 d_spacing_angstrom: prepared.d_spacing,
564 two_theta_deg: prepared.two_theta_deg,
565 accumulation,
566 },
567 d_y,
568 parameter_count,
569 })
570}
571
572fn zeroed_values(count: usize) -> Result<Vec<f64>, StructuralPatternError> {
573 let mut values = Vec::new();
574 values.try_reserve_exact(count).map_err(|_| {
575 StructuralPatternError::Contributions(CwContributionsError::AllocationOverflow)
576 })?;
577 values.resize(count, 0.0);
578 Ok(values)
579}
580
581pub fn calculate_structural_pattern_jvp(
587 cell: UnitCell,
588 space_group: &SpaceGroup,
589 input: &StructuralPatternInputView<'_>,
590 tangent: &[f64],
591) -> Result<StructuralPatternJvpResult, StructuralPatternError> {
592 calculate_structural_pattern_jvp_with_context(
593 cell,
594 space_group,
595 input,
596 tangent,
597 &ExecutionContext::serial(),
598 )
599}
600
601pub fn calculate_structural_pattern_jvp_with_context(
607 cell: UnitCell,
608 space_group: &SpaceGroup,
609 input: &StructuralPatternInputView<'_>,
610 tangent: &[f64],
611 execution: &ExecutionContext,
612) -> Result<StructuralPatternJvpResult, StructuralPatternError> {
613 let prepared = prepare(cell, input)?;
614 let structural = calculate_structure_factor_jvp_with_context(
615 cell,
616 space_group,
617 prepared.structure_batch(input),
618 tangent,
619 execution,
620 )
621 .map_err(StructuralPatternError::StructureFactor)?;
622 let d_two_theta_deg = prepared
623 .d_two_theta_d_cell
624 .iter()
625 .map(|derivatives| {
626 derivatives
627 .iter()
628 .zip(&tangent[..CELL_PARAMETER_COUNT])
629 .map(|(derivative, direction)| derivative * direction)
630 .sum::<f64>()
631 })
632 .collect::<Vec<_>>();
633 let mut accumulation = accumulate(
634 input,
635 &prepared.two_theta_deg,
636 &structural.values.intensity,
637 execution,
638 )?;
639 append_instrument_derivatives(&mut accumulation, &structural.values, input, &prepared)?;
640 let d_y = chain_pattern_jvp(&accumulation, &structural.d_intensity, &d_two_theta_deg);
641 Ok(StructuralPatternJvpResult {
642 result: StructuralPatternResult {
643 structure_factors: structural.values,
644 d_spacing_angstrom: prepared.d_spacing,
645 two_theta_deg: prepared.two_theta_deg,
646 accumulation,
647 },
648 d_y,
649 d_integrated_intensity: structural.d_intensity,
650 d_two_theta_deg,
651 })
652}
653
654pub fn calculate_structural_pattern_vjp(
660 cell: UnitCell,
661 space_group: &SpaceGroup,
662 input: &StructuralPatternInputView<'_>,
663 sample_weights: &[f64],
664) -> Result<StructuralPatternVjpResult, StructuralPatternError> {
665 calculate_structural_pattern_vjp_with_context(
666 cell,
667 space_group,
668 input,
669 sample_weights,
670 &ExecutionContext::serial(),
671 )
672}
673
674pub fn calculate_structural_pattern_vjp_with_context(
680 cell: UnitCell,
681 space_group: &SpaceGroup,
682 input: &StructuralPatternInputView<'_>,
683 sample_weights: &[f64],
684 execution: &ExecutionContext,
685) -> Result<StructuralPatternVjpResult, StructuralPatternError> {
686 if sample_weights.len() != input.x_deg.len() {
687 return Err(StructuralPatternError::PatternWeightLengthMismatch);
688 }
689 if sample_weights.iter().any(|value| !value.is_finite()) {
690 return Err(StructuralPatternError::NonFinitePatternWeight);
691 }
692 let prepared = prepare(cell, input)?;
693 let values = calculate_structure_factor_values_with_context(
694 cell,
695 space_group,
696 prepared.structure_batch(input),
697 execution,
698 )
699 .map_err(StructuralPatternError::StructureFactor)?;
700 let mut accumulation =
701 accumulate(input, &prepared.two_theta_deg, &values.intensity, execution)?;
702 append_instrument_derivatives(&mut accumulation, &values, input, &prepared)?;
703 let (intensity_weights, position_weights) =
704 local_transpose_weights(&accumulation, sample_weights);
705 let mut structural = calculate_structure_factor_intensity_vjp_with_context(
706 cell,
707 space_group,
708 prepared.structure_batch(input),
709 &intensity_weights,
710 execution,
711 )
712 .map_err(StructuralPatternError::StructureFactor)?;
713 for (reflection, weight) in position_weights.into_iter().enumerate() {
714 for parameter in 0..CELL_PARAMETER_COUNT {
715 structural.gradient[parameter] +=
716 weight * prepared.d_two_theta_d_cell[reflection][parameter];
717 }
718 }
719 Ok(StructuralPatternVjpResult {
720 result: StructuralPatternResult {
721 structure_factors: values,
722 d_spacing_angstrom: prepared.d_spacing,
723 two_theta_deg: prepared.two_theta_deg,
724 accumulation,
725 },
726 gradient: structural.gradient,
727 })
728}
729
730fn prepare(
731 cell: UnitCell,
732 input: &StructuralPatternInputView<'_>,
733) -> Result<PreparedNumerics, StructuralPatternError> {
734 if input.scattering_species.len() != input.fractional_xyz.len() {
735 return Err(StructuralPatternError::SpeciesLengthMismatch);
736 }
737 validate_scattering_offsets(input)?;
738 input
739 .instrument
740 .validate()
741 .map_err(StructuralPatternError::InvalidInstrument)?;
742 validate_position_correction(input.position_correction)?;
743 let geometry = cell
744 .geometry()
745 .map_err(StructureFactorBatchError::Cell)
746 .map_err(StructuralPatternError::StructureFactor)?;
747 let mut q_squared = Vec::with_capacity(input.hkl.len());
748 let mut d_spacing = Vec::with_capacity(input.hkl.len());
749 let mut two_theta_deg = Vec::with_capacity(input.hkl.len());
750 let mut d_two_theta_d_cell = Vec::with_capacity(input.hkl.len());
751 let mut d_two_theta_d_wavelength = Vec::with_capacity(input.hkl.len());
752 let mut d_two_theta_d_sample_displacement = input
753 .position_correction
754 .bragg_brentano_mm
755 .map(|_| Vec::with_capacity(input.hkl.len()));
756 let mut d_two_theta_d_displace_x = input
757 .position_correction
758 .debye_scherrer_micrometre
759 .map(|_| Vec::with_capacity(input.hkl.len()));
760 let mut d_two_theta_d_displace_y = input
761 .position_correction
762 .debye_scherrer_micrometre
763 .map(|_| Vec::with_capacity(input.hkl.len()));
764 for &hkl in input.hkl {
765 let (q_value, d_q) = geometry.q_squared_and_derivatives(hkl);
766 if !q_value.is_finite() || q_value <= 0.0 {
767 return Err(StructuralPatternError::ReflectionOutsideAngularDomain);
768 }
769 let root_q = q_value.sqrt();
770 let sin_theta = 0.5 * input.instrument.wavelength_angstrom * root_q;
771 if !(0.0..1.0).contains(&sin_theta) {
772 return Err(StructuralPatternError::ReflectionOutsideAngularDomain);
773 }
774 let theta = sin_theta.asin();
775 let corrected = corrected_monochromatic_position(2.0 * theta, input.position_correction);
776 let d_position_factor =
777 corrected.d_position_d_base * input.instrument.wavelength_angstrom * DEGREES_PER_RADIAN
778 / (2.0 * root_q * theta.cos());
779 let d_position_d_wavelength =
780 corrected.d_position_d_base * DEGREES_PER_RADIAN * root_q / theta.cos();
781 q_squared.push(q_value);
782 d_spacing.push(root_q.recip());
783 two_theta_deg.push(corrected.position_deg);
784 d_two_theta_d_cell.push(d_q.map(|derivative| d_position_factor * derivative));
785 d_two_theta_d_wavelength.push(d_position_d_wavelength);
786 if let (Some(values), Some(derivative)) = (
787 d_two_theta_d_sample_displacement.as_mut(),
788 corrected.d_position_d_sample_displacement,
789 ) {
790 values.push(derivative);
791 }
792 if let (Some(values), Some(derivative)) = (
793 d_two_theta_d_displace_x.as_mut(),
794 corrected.d_position_d_displace_x,
795 ) {
796 values.push(derivative);
797 }
798 if let (Some(values), Some(derivative)) = (
799 d_two_theta_d_displace_y.as_mut(),
800 corrected.d_position_d_displace_y,
801 ) {
802 values.push(derivative);
803 }
804 }
805 let s: Vec<f64> = q_squared.iter().map(|value| 0.5 * value.sqrt()).collect();
806 let mut scattering = match input.scattering_model {
807 BuiltInScatteringModel::XrayNonResonant => {
808 PreparedXrayScattering::new(input.scattering_species.iter().copied())
809 .and_then(|model| model.evaluate(&s))
810 }
811 BuiltInScatteringModel::NeutronNuclear => {
812 PreparedNeutronScattering::new(input.scattering_species.iter().copied())
813 .and_then(|model| model.evaluate(&s))
814 }
815 }
816 .map_err(StructuralPatternError::Scattering)?;
817 apply_scattering_offsets(&mut scattering, input);
818 let correction = input
819 .correction_model
820 .evaluate(&q_squared)
821 .map_err(StructuralPatternError::Correction)?;
822 Ok(PreparedNumerics {
823 scattering,
824 correction,
825 d_spacing,
826 two_theta_deg,
827 d_two_theta_d_cell,
828 d_two_theta_d_wavelength,
829 d_two_theta_d_sample_displacement,
830 d_two_theta_d_displace_x,
831 d_two_theta_d_displace_y,
832 })
833}
834
835fn calculate_values(
836 cell: UnitCell,
837 space_group: &SpaceGroup,
838 input: &StructuralPatternInputView<'_>,
839 prepared: &PreparedNumerics,
840 execution: &ExecutionContext,
841) -> Result<StructuralPatternResult, StructuralPatternError> {
842 let structure_factors = calculate_structure_factor_values_with_context(
843 cell,
844 space_group,
845 prepared.structure_batch(input),
846 execution,
847 )
848 .map_err(StructuralPatternError::StructureFactor)?;
849 let mut accumulation = accumulate(
850 input,
851 &prepared.two_theta_deg,
852 &structure_factors.intensity,
853 execution,
854 )?;
855 append_instrument_derivatives(&mut accumulation, &structure_factors, input, prepared)?;
856 Ok(StructuralPatternResult {
857 structure_factors,
858 d_spacing_angstrom: prepared.d_spacing.clone(),
859 two_theta_deg: prepared.two_theta_deg.clone(),
860 accumulation,
861 })
862}
863
864fn append_instrument_derivatives(
865 accumulation: &mut Accumulation,
866 structure_factors: &StructureFactorValues,
867 input: &StructuralPatternInputView<'_>,
868 prepared: &PreparedNumerics,
869) -> Result<(), StructuralPatternError> {
870 let sample_count = accumulation.sample_count;
871 let extra_count = 2
872 + usize::from(prepared.d_two_theta_d_sample_displacement.is_some())
873 + usize::from(prepared.d_two_theta_d_displace_x.is_some())
874 + usize::from(prepared.d_two_theta_d_displace_y.is_some());
875 let global = accumulation
876 .derivatives
877 .global
878 .as_mut()
879 .expect("CW contribution accumulation always has global derivatives");
880 let old_values = std::mem::take(&mut global.values);
881 let mut combined = Vec::new();
882 combined
883 .try_reserve(old_values.len() + extra_count * sample_count)
884 .map_err(|_| {
885 StructuralPatternError::Contributions(CwContributionsError::AllocationOverflow)
886 })?;
887 let mut wavelength = vec![0.0; sample_count];
888 let mut zero_shift = vec![0.0; sample_count];
889 let mut sample_displacement = prepared
890 .d_two_theta_d_sample_displacement
891 .as_ref()
892 .map(|_| vec![0.0; sample_count]);
893 let mut displace_x = prepared
894 .d_two_theta_d_displace_x
895 .as_ref()
896 .map(|_| vec![0.0; sample_count]);
897 let mut displace_y = prepared
898 .d_two_theta_d_displace_y
899 .as_ref()
900 .map(|_| vec![0.0; sample_count]);
901 let local = &accumulation.derivatives.local;
902 for reflection in 0..local.peak_count() {
903 #[allow(clippy::cast_precision_loss)]
904 let multiplicity = input.multiplicity[reflection] as f64;
905 let d_intensity_d_wavelength = input.scale
906 * multiplicity
907 * structure_factors.f_squared[reflection]
908 * prepared.correction.d_values_d_wavelength[reflection];
909 let begin = local.offsets[reflection];
910 let end = local.offsets[reflection + 1];
911 for active in begin..end {
912 let sample = local.starts[reflection] + active - begin;
913 let base = 2 * active;
914 let d_intensity = local.values[base];
915 let d_position = local.values[base + 1];
916 wavelength[sample] += d_intensity * d_intensity_d_wavelength
917 + d_position * prepared.d_two_theta_d_wavelength[reflection];
918 zero_shift[sample] += d_position;
919 if let (Some(values), Some(derivatives)) = (
920 sample_displacement.as_mut(),
921 prepared.d_two_theta_d_sample_displacement.as_ref(),
922 ) {
923 values[sample] += d_position * derivatives[reflection];
924 }
925 if let (Some(values), Some(derivatives)) = (
926 displace_x.as_mut(),
927 prepared.d_two_theta_d_displace_x.as_ref(),
928 ) {
929 values[sample] += d_position * derivatives[reflection];
930 }
931 if let (Some(values), Some(derivatives)) = (
932 displace_y.as_mut(),
933 prepared.d_two_theta_d_displace_y.as_ref(),
934 ) {
935 values[sample] += d_position * derivatives[reflection];
936 }
937 }
938 }
939 let instrument_end = CW_INSTRUMENT_PARAMETER_COUNT * sample_count;
940 combined.extend_from_slice(&old_values[..instrument_end]);
941 combined.extend(wavelength);
942 combined.extend(zero_shift);
943 if let Some(values) = sample_displacement {
944 combined.extend(values);
945 }
946 if let Some(values) = displace_x {
947 combined.extend(values);
948 }
949 if let Some(values) = displace_y {
950 combined.extend(values);
951 }
952 combined.extend_from_slice(&old_values[instrument_end..]);
953 global.values = combined;
954 global.parameter_count += extra_count;
955 Ok(())
956}
957
958fn accumulate(
959 input: &StructuralPatternInputView<'_>,
960 two_theta_deg: &[f64],
961 intensities: &[f64],
962 execution: &ExecutionContext,
963) -> Result<Accumulation, StructuralPatternError> {
964 let grid = GridView::new(input.x_deg).map_err(StructuralPatternError::Profile)?;
965 let result = match input.axial_geometry {
966 Some(geometry) => accumulate_cw_fcj_contributions_batch_with_context(
967 grid,
968 two_theta_deg,
969 intensities,
970 input.instrument,
971 input.contributions,
972 geometry,
973 input.support,
974 execution,
975 ),
976 None => accumulate_cw_contributions_batch_with_context(
977 grid,
978 two_theta_deg,
979 intensities,
980 input.instrument,
981 input.contributions,
982 input.support,
983 execution,
984 ),
985 };
986 result.map_err(StructuralPatternError::Contributions)
987}
988
989fn chain_pattern_jvp(
990 accumulation: &Accumulation,
991 d_intensity: &[f64],
992 d_position: &[f64],
993) -> Vec<f64> {
994 let local = &accumulation.derivatives.local;
995 let mut result = vec![0.0; accumulation.sample_count];
996 for reflection in 0..local.peak_count() {
997 let begin = local.offsets[reflection];
998 let end = local.offsets[reflection + 1];
999 for active in begin..end {
1000 let sample = local.starts[reflection] + active - begin;
1001 let base = 2 * active;
1002 result[sample] += local.values[base] * d_intensity[reflection]
1003 + local.values[base + 1] * d_position[reflection];
1004 }
1005 }
1006 result
1007}
1008
1009fn local_transpose_weights(
1010 accumulation: &Accumulation,
1011 sample_weights: &[f64],
1012) -> (Vec<f64>, Vec<f64>) {
1013 let local = &accumulation.derivatives.local;
1014 let mut intensity = vec![0.0; local.peak_count()];
1015 let mut position = vec![0.0; local.peak_count()];
1016 for reflection in 0..local.peak_count() {
1017 let begin = local.offsets[reflection];
1018 let end = local.offsets[reflection + 1];
1019 for active in begin..end {
1020 let sample = local.starts[reflection] + active - begin;
1021 let base = 2 * active;
1022 intensity[reflection] += local.values[base] * sample_weights[sample];
1023 position[reflection] += local.values[base + 1] * sample_weights[sample];
1024 }
1025 }
1026 (intensity, position)
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031 use super::*;
1032 use phasesmith_core::CwContributionArrays;
1033 use phasesmith_crystallography::{P1ParameterLayout, Rational, SymmetryOperation};
1034
1035 fn cell() -> UnitCell {
1036 UnitCell {
1037 a_angstrom: 4.7,
1038 b_angstrom: 5.1,
1039 c_angstrom: 6.2,
1040 alpha_deg: 82.0,
1041 beta_deg: 87.0,
1042 gamma_deg: 74.0,
1043 }
1044 }
1045
1046 fn group() -> SpaceGroup {
1047 SpaceGroup::new(vec![
1048 SymmetryOperation::identity(),
1049 SymmetryOperation::new([[-1, 0, 0], [0, -1, 0], [0, 0, -1]], [Rational::zero(); 3])
1050 .expect("inversion"),
1051 ])
1052 .expect("P-1")
1053 }
1054
1055 fn instrument() -> ConstantWavelengthInstrument {
1056 ConstantWavelengthInstrument {
1057 wavelength_angstrom: 1.5406,
1058 u_deg2: 2.0e-4,
1059 v_deg2: -1.0e-4,
1060 w_deg2: 1.2e-4,
1061 x_deg: 1.5e-3,
1062 y_deg: 3.0e-3,
1063 }
1064 }
1065
1066 #[allow(clippy::too_many_arguments)]
1067 fn calculate_case(
1068 selected_cell: UnitCell,
1069 x: &[f64],
1070 hkl: &[[i32; 3]],
1071 multiplicity: &[usize],
1072 xyz: &[[f64; 3]],
1073 occupancy: &[f64],
1074 u_iso: &[f64],
1075 scale: f64,
1076 multiplier: &[f64],
1077 ) -> StructuralPatternResult {
1078 let zeros = vec![0.0; hkl.len()];
1079 let contributions = CwContributionsView::new(
1080 hkl.len(),
1081 0,
1082 CwContributionArrays {
1083 gaussian_variance_deg2: &zeros,
1084 lorentzian_fwhm_deg: &zeros,
1085 intensity_multiplier: multiplier,
1086 d_gaussian_variance_d_position: &zeros,
1087 d_lorentzian_fwhm_d_position: &zeros,
1088 d_intensity_multiplier_d_position: &zeros,
1089 d_gaussian_variance_d_parameters: &[],
1090 d_lorentzian_fwhm_d_parameters: &[],
1091 d_intensity_multiplier_d_parameters: &[],
1092 },
1093 )
1094 .expect("contributions");
1095 calculate_structural_pattern(
1096 selected_cell,
1097 &group(),
1098 &StructuralPatternInputView {
1099 x_deg: x,
1100 hkl,
1101 multiplicity,
1102 fractional_xyz: xyz,
1103 occupancy,
1104 u_iso_angstrom2: u_iso,
1105 anisotropic_mask: &[false, false],
1106 u_aniso_cif_angstrom2: &[[0.0; 6]; 2],
1107 scattering_species: &["Si", "O"],
1108 scattering_real_offset: &[],
1109 scattering_imag_offset: &[],
1110 scale,
1111 coordinate_tolerance: 1.0e-10,
1112 instrument: instrument(),
1113 axial_geometry: None,
1114 position_correction: MonochromaticPositionCorrection {
1115 zero_shift_deg: 0.0,
1116 bragg_brentano_mm: None,
1117 debye_scherrer_micrometre: None,
1118 },
1119 correction_model: IntegratedIntensityCorrectionModel::Neutral,
1120 scattering_model: BuiltInScatteringModel::XrayNonResonant,
1121 contributions,
1122 support: SupportPolicy::FwhmMultiple(20.0),
1123 },
1124 )
1125 .expect("structural pattern")
1126 }
1127
1128 #[test]
1129 fn fused_values_apply_sample_intensity_multiplier_exactly_once() {
1130 let x: Vec<f64> = (0..9_001)
1131 .map(|index| 10.0 + f64::from(index) * 0.01)
1132 .collect();
1133 let hkl = [[1, 0, 1], [2, 1, 1], [1, 2, 3]];
1134 let multiplicity = [2, 4, 2];
1135 let xyz = [[0.17, 0.23, 0.31], [0.37, 0.11, 0.19]];
1136 let occupancy = [0.82, 0.55];
1137 let u_iso = [0.012, 0.018];
1138 let neutral = calculate_case(
1139 cell(),
1140 &x,
1141 &hkl,
1142 &multiplicity,
1143 &xyz,
1144 &occupancy,
1145 &u_iso,
1146 1.4,
1147 &[1.0; 3],
1148 );
1149 let doubled = calculate_case(
1150 cell(),
1151 &x,
1152 &hkl,
1153 &multiplicity,
1154 &xyz,
1155 &occupancy,
1156 &u_iso,
1157 1.4,
1158 &[2.0; 3],
1159 );
1160 assert_eq!(
1161 neutral.structure_factors.intensity,
1162 doubled.structure_factors.intensity
1163 );
1164 for (left, right) in neutral.accumulation.y.iter().zip(&doubled.accumulation.y) {
1165 assert!((2.0 * left - right).abs() < 2.0e-15 * right.abs().max(1.0));
1166 }
1167 }
1168
1169 #[test]
1170 fn reflection_geometry_helper_matches_the_fused_structural_positions() {
1171 let x = (0..9_001)
1172 .map(|index| 10.0 + f64::from(index) * 0.01)
1173 .collect::<Vec<_>>();
1174 let hkl = [[1, 0, 1], [2, 1, 1], [1, 2, 3]];
1175 let correction = MonochromaticPositionCorrection {
1176 zero_shift_deg: 0.0,
1177 bragg_brentano_mm: None,
1178 debye_scherrer_micrometre: None,
1179 };
1180 let fused = calculate_case(
1181 cell(),
1182 &x,
1183 &hkl,
1184 &[2, 4, 2],
1185 &[[0.17, 0.23, 0.31], [0.37, 0.11, 0.19]],
1186 &[0.82, 0.55],
1187 &[0.012, 0.018],
1188 1.4,
1189 &[1.0; 3],
1190 );
1191 let geometry =
1192 calculate_monochromatic_reflection_geometry(cell(), &hkl, instrument(), correction)
1193 .unwrap();
1194 assert_eq!(geometry.d_spacing_angstrom, fused.d_spacing_angstrom);
1195 assert_eq!(geometry.two_theta_deg, fused.two_theta_deg);
1196 }
1197
1198 #[test]
1199 #[allow(clippy::too_many_lines)]
1200 fn fused_jvp_vjp_match_pattern_finite_differences_and_adjoint_identity() {
1201 let x: Vec<f64> = (0..9_001)
1202 .map(|index| 10.0 + f64::from(index) * 0.01)
1203 .collect();
1204 let hkl = [[1, 0, 1], [2, 1, 1], [1, 2, 3]];
1205 let multiplicity = [2, 4, 2];
1206 let xyz = [[0.17, 0.23, 0.31], [0.37, 0.11, 0.19]];
1207 let occupancy = [0.82, 0.55];
1208 let u_iso = [0.012, 0.018];
1209 let scale = 1.4;
1210 let zeros = [0.0; 3];
1211 let ones = [1.0; 3];
1212 let contributions = CwContributionsView::new(
1213 hkl.len(),
1214 0,
1215 CwContributionArrays {
1216 gaussian_variance_deg2: &zeros,
1217 lorentzian_fwhm_deg: &zeros,
1218 intensity_multiplier: &ones,
1219 d_gaussian_variance_d_position: &zeros,
1220 d_lorentzian_fwhm_d_position: &zeros,
1221 d_intensity_multiplier_d_position: &zeros,
1222 d_gaussian_variance_d_parameters: &[],
1223 d_lorentzian_fwhm_d_parameters: &[],
1224 d_intensity_multiplier_d_parameters: &[],
1225 },
1226 )
1227 .expect("contributions");
1228 let input = StructuralPatternInputView {
1229 x_deg: &x,
1230 hkl: &hkl,
1231 multiplicity: &multiplicity,
1232 fractional_xyz: &xyz,
1233 occupancy: &occupancy,
1234 u_iso_angstrom2: &u_iso,
1235 anisotropic_mask: &[false, false],
1236 u_aniso_cif_angstrom2: &[[0.0; 6]; 2],
1237 scattering_species: &["Si", "O"],
1238 scattering_real_offset: &[],
1239 scattering_imag_offset: &[],
1240 scale,
1241 coordinate_tolerance: 1.0e-10,
1242 instrument: instrument(),
1243 axial_geometry: None,
1244 position_correction: MonochromaticPositionCorrection {
1245 zero_shift_deg: 0.0,
1246 bragg_brentano_mm: None,
1247 debye_scherrer_micrometre: None,
1248 },
1249 correction_model: IntegratedIntensityCorrectionModel::Neutral,
1250 scattering_model: BuiltInScatteringModel::XrayNonResonant,
1251 contributions,
1252 support: SupportPolicy::FwhmMultiple(20.0),
1253 };
1254 let layout = P1ParameterLayout { site_count: 2 };
1255 let tangent: Vec<f64> = (0..layout.parameter_count())
1256 .map(|index| f64::from(u32::try_from(index + 1).expect("small index")) * 2.0e-5)
1257 .collect();
1258 let jvp = calculate_structural_pattern_jvp(cell(), &group(), &input, &tangent)
1259 .expect("structural JVP");
1260 let dense = calculate_structural_pattern_dense(cell(), &group(), &input)
1261 .expect("structural dense linearization");
1262 assert_eq!(dense.parameter_count, tangent.len());
1263 for sample in 0..x.len() {
1264 let product = tangent
1265 .iter()
1266 .enumerate()
1267 .map(|(parameter, direction)| direction * dense.d_y[parameter * x.len() + sample])
1268 .sum::<f64>();
1269 assert!((product - jvp.d_y[sample]).abs() < 2.0e-11 * product.abs().max(1.0));
1270 }
1271 let step = 1.0e-5;
1272 let mut plus_cell = cell();
1273 let mut minus_cell = cell();
1274 for (parameter, direction) in tangent
1275 .iter()
1276 .copied()
1277 .take(CELL_PARAMETER_COUNT)
1278 .enumerate()
1279 {
1280 perturb_cell(&mut plus_cell, parameter, step * direction);
1281 perturb_cell(&mut minus_cell, parameter, -step * direction);
1282 }
1283 let mut plus_xyz = xyz;
1284 let mut minus_xyz = xyz;
1285 for site in 0..2 {
1286 for component in 0..3 {
1287 let direction = tangent[layout.coordinate(site, component)];
1288 plus_xyz[site][component] += step * direction;
1289 minus_xyz[site][component] -= step * direction;
1290 }
1291 }
1292 let mut plus_occupancy = occupancy;
1293 let mut minus_occupancy = occupancy;
1294 let mut plus_u = u_iso;
1295 let mut minus_u = u_iso;
1296 for site in 0..2 {
1297 plus_occupancy[site] += step * tangent[layout.occupancy(site)];
1298 minus_occupancy[site] -= step * tangent[layout.occupancy(site)];
1299 plus_u[site] += step * tangent[layout.u_iso(site)];
1300 minus_u[site] -= step * tangent[layout.u_iso(site)];
1301 }
1302 let plus = calculate_case(
1303 plus_cell,
1304 &x,
1305 &hkl,
1306 &multiplicity,
1307 &plus_xyz,
1308 &plus_occupancy,
1309 &plus_u,
1310 scale + step * tangent[layout.scale()],
1311 &ones,
1312 );
1313 let minus = calculate_case(
1314 minus_cell,
1315 &x,
1316 &hkl,
1317 &multiplicity,
1318 &minus_xyz,
1319 &minus_occupancy,
1320 &minus_u,
1321 scale - step * tangent[layout.scale()],
1322 &ones,
1323 );
1324 for ((actual, plus_value), minus_value) in jvp
1325 .d_y
1326 .iter()
1327 .zip(&plus.accumulation.y)
1328 .zip(&minus.accumulation.y)
1329 {
1330 let finite_difference = (plus_value - minus_value) / (2.0 * step);
1331 assert!((actual - finite_difference).abs() < 3.0e-5 * finite_difference.abs().max(1.0));
1332 }
1333 let sample_weights: Vec<f64> = x.iter().map(|value| (0.17 * value).sin()).collect();
1334 let vjp = calculate_structural_pattern_vjp(cell(), &group(), &input, &sample_weights)
1335 .expect("structural VJP");
1336 let forward = jvp
1337 .d_y
1338 .iter()
1339 .zip(&sample_weights)
1340 .map(|(derivative, weight)| derivative * weight)
1341 .sum::<f64>();
1342 let reverse = tangent
1343 .iter()
1344 .zip(&vjp.gradient)
1345 .map(|(direction, gradient)| direction * gradient)
1346 .sum::<f64>();
1347 assert!((forward - reverse).abs() < 2.0e-10 * forward.abs().max(1.0));
1348 for (parameter, actual) in vjp.gradient.iter().copied().enumerate() {
1349 let expected = dense.d_y[parameter * x.len()..(parameter + 1) * x.len()]
1350 .iter()
1351 .zip(&sample_weights)
1352 .map(|(derivative, weight)| derivative * weight)
1353 .sum::<f64>();
1354 assert!((actual - expected).abs() < 2.0e-10 * expected.abs().max(1.0));
1355 }
1356 }
1357
1358 fn perturb_cell(cell: &mut UnitCell, parameter: usize, change: f64) {
1359 let value = match parameter {
1360 0 => &mut cell.a_angstrom,
1361 1 => &mut cell.b_angstrom,
1362 2 => &mut cell.c_angstrom,
1363 3 => &mut cell.alpha_deg,
1364 4 => &mut cell.beta_deg,
1365 5 => &mut cell.gamma_deg,
1366 _ => panic!("invalid cell parameter"),
1367 };
1368 *value += change;
1369 }
1370}