treams_core/lattice/cell.rs
1//! The lattice and Bloch vector of a sum, and the automatic Ewald split.
2//!
3//! Upstream: `_check_eta` of `lattice/_esum.pyx` for the automatic split. The reduced
4//! bases are a treams-rs extension.
5
6use std::f64::consts::PI;
7
8use super::geometry::{reciprocal, reduce_basis, volume};
9use crate::{Complex, Error, Result};
10
11/// Row lattice vectors, a real Bloch vector `kpar` and their reduced bases in one, two or
12/// three dimensions.
13///
14/// They are bundled because their derivatives share one jet; treams passes the lattice
15/// vectors `a` and the Bloch vector `kpar` separately.
16///
17/// The lattice vectors and the Bloch vector are given in the lattice frame: Cartesian
18/// components along the lattice's axes, which are z for one-dimensional spherical sums,
19/// x for one-dimensional cylindrical sums, the xy plane in two dimensions and xyz in
20/// three. They are not fractional coordinates. Lattice-vector and Bloch derivatives use
21/// the same frame.
22///
23/// The constructor also reduces the direct and the reciprocal rows (`reduce_basis`),
24/// over which the Ewald parts sum. A reduction is stored as `None` where the given rows
25/// are already reduced, and then the parts sum over the given rows.
26#[derive(Clone, Debug)]
27pub struct BlochLattice {
28 pub(super) dim: usize,
29 /// Row vectors in lattice coordinates, zero outside the leading `dim` block.
30 pub(super) direct: [[f64; 3]; 3],
31 /// Reciprocal rows with `a_i · b_j = 2π δ_ij` in the leading block.
32 pub(super) reciprocal: [[f64; 3]; 3],
33 /// Bloch vector in the lattice frame, zero beyond the leading `dim` entries.
34 pub(super) kpar: [f64; 3],
35 pub(super) measure: f64,
36 /// Reduced basis of the direct rows for the real-space sum; `None` where the given
37 /// rows are already reduced (or need coefficients beyond `2^20`), so the sum uses
38 /// them as they are.
39 pub(super) direct_reduction: Option<Reduction>,
40 /// Independently reduced basis of the reciprocal rows for the reciprocal sum; `None`
41 /// as for `direct_reduction`.
42 pub(super) reciprocal_reduction: Option<Reduction>,
43}
44
45/// A reduced basis `U rows` of lattice `rows` from [`geometry::reduce_basis`]: the
46/// integer rows of `U` and of its inverse transpose `W`, stored exactly as `f64`.
47/// Ewald shells of a skewed basis are far from Euclidean shells and can stop before
48/// short vectors with large indices; shells of the reduced basis cannot.
49///
50/// [`geometry::reduce_basis`]: super::geometry::reduce_basis
51#[derive(Clone, Copy, Debug)]
52pub(super) struct Reduction {
53 pub(super) rows: [[f64; 3]; 3],
54 pub(super) dual: [[f64; 3]; 3],
55}
56
57impl Reduction {
58 #[allow(clippy::cast_precision_loss)] // Entries are bounded by 2^20.
59 fn new(rows: [[f64; 3]; 3], dim: usize) -> Option<Self> {
60 let [rows, dual] = reduce_basis(rows, dim)?.map(|m| m.map(|row| row.map(|x| x as f64)));
61 Some(Self { rows, dual })
62 }
63
64 /// Reduced-basis coordinates of the lattice point nearest to the point with
65 /// coordinates `fractional` in the given basis, rounding in reduced coordinates.
66 /// `None` stands for a given basis that is already reduced.
67 pub(super) fn round(reduction: Option<&Self>, fractional: [f64; 3], dim: usize) -> [f64; 3] {
68 match reduction {
69 None => fractional.map(f64::round),
70 Some(reduction) => std::array::from_fn(|i| {
71 (0..dim)
72 .map(|k| reduction.dual[i][k] * fractional[k])
73 .sum::<f64>()
74 .round()
75 }),
76 }
77 }
78
79 /// Given-basis coordinates `U^T c` of a point with reduced coordinates `c`. `None`
80 /// stands for a given basis that is already reduced, where both coincide.
81 pub(super) fn given(reduction: Option<&Self>, reduced: [f64; 3], dim: usize) -> [f64; 3] {
82 reduction.map_or(reduced, |reduction| {
83 std::array::from_fn(|k| (0..dim).map(|i| reduction.rows[i][k] * reduced[i]).sum())
84 })
85 }
86}
87
88impl BlochLattice {
89 /// Number of independent lattice vectors.
90 #[must_use]
91 pub fn dimension(&self) -> usize {
92 self.dim
93 }
94
95 /// Validate and precompute the reciprocal lattice. Rows are primitive vectors.
96 pub fn new(vectors: &[Vec<f64>], kpar: &[f64]) -> Result<Self> {
97 let dim = vectors.len();
98 if !(1..=3).contains(&dim)
99 || kpar.len() != dim
100 || vectors.iter().any(|row| row.len() != dim)
101 {
102 return Err(Error::InvalidInput(
103 "lattice must be a finite square 1D, 2D or 3D matrix, with matching Bloch vector"
104 .into(),
105 ));
106 }
107 Self::from_array(
108 std::array::from_fn(|i| {
109 std::array::from_fn(|j| {
110 if i < dim && j < dim {
111 vectors[i][j]
112 } else {
113 0.0
114 }
115 })
116 }),
117 std::array::from_fn(|i| if i < dim { kpar[i] } else { 0.0 }),
118 dim,
119 )
120 }
121
122 /// Construct from fixed storage without allocating intermediate row vectors.
123 ///
124 /// Only the leading `dim` block of `vectors` and the leading `dim` entries of `kpar`
125 /// count; the rest is stored as zero, so callers need not pad.
126 pub fn from_array(vectors: [[f64; 3]; 3], kpar: [f64; 3], dim: usize) -> Result<Self> {
127 if !(1..=3).contains(&dim) || kpar[..dim].iter().any(|x| !x.is_finite()) {
128 return Err(Error::InvalidInput(
129 "invalid lattice dimension or Bloch vector".into(),
130 ));
131 }
132 let kpar = std::array::from_fn(|i| if i < dim { kpar[i] } else { 0.0 });
133 let direct = std::array::from_fn(|i| {
134 std::array::from_fn(|j| {
135 if i < dim && j < dim {
136 vectors[i][j]
137 } else {
138 0.0
139 }
140 })
141 });
142 let reciprocal = reciprocal(direct, dim)?;
143 let measure = volume(direct, dim)?.abs();
144 if !measure.is_finite() {
145 return Err(Error::InvalidInput(
146 "lattice is numerically singular".into(),
147 ));
148 }
149 Ok(Self {
150 dim,
151 direct,
152 reciprocal,
153 kpar,
154 measure,
155 direct_reduction: Reduction::new(direct, dim),
156 reciprocal_reduction: Reduction::new(reciprocal, dim),
157 })
158 }
159}
160
161/// The Ewald split: `eta`, or for zero an automatic choice from `kL` with the cell
162/// length `L = measure^(1/dim)`. The exact sum is independent of the split, so it is
163/// held fixed during differentiation instead of differentiating the heuristic.
164pub(crate) fn resolve_split(k: Complex, lattice: &BlochLattice, eta: Complex) -> Complex {
165 if eta != Complex::default() {
166 return eta;
167 }
168 let length = match lattice.dim {
169 1 => lattice.measure,
170 2 => lattice.measure.sqrt(),
171 _ => lattice.measure.cbrt(),
172 };
173 let e = 1.0 / (k * length);
174 (2.0 * PI).sqrt() * e * (0.125 / e.norm()).max(1.0)
175}