tensor4all_quanticstransform/cumsum.rs
1//! Cumulative sum operator
2//!
3//! This transformation computes cumulative sums: y_i = Σ_{j < i} x_j
4
5use anyhow::Result;
6use num_complex::Complex64;
7use num_traits::{One, Zero};
8use tensor4all_simplett::{types::tensor3_zeros, Tensor3Ops, TensorTrain};
9
10use crate::common::{tensortrain_to_linear_operator, QuanticsOperator};
11
12/// Type of triangular matrix for cumsum-like operators.
13///
14/// # Variants
15///
16/// - **`Lower`**: Strict lower triangle, M\[i,j\] = 1 when i > j.
17/// Computes the prefix sum: y\_i = sum of x\_j for j < i.
18/// - **`Upper`**: Strict upper triangle, M\[i,j\] = 1 when i < j.
19/// Computes the suffix sum: y\_i = sum of x\_j for j > i.
20///
21/// # Examples
22///
23/// ```
24/// use tensor4all_quanticstransform::{triangle_operator, TriangleType};
25///
26/// // Prefix sum (lower triangle)
27/// let lower = triangle_operator(4, TriangleType::Lower).unwrap();
28/// assert_eq!(lower.mpo.node_count(), 4);
29///
30/// // Suffix sum (upper triangle)
31/// let upper = triangle_operator(4, TriangleType::Upper).unwrap();
32/// assert_eq!(upper.mpo.node_count(), 4);
33/// ```
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum TriangleType {
36 /// Strict lower triangle: M\[i,j\] = 1 when i > j.
37 /// Computes y\_i = sum of x\_j for j < i (prefix sum).
38 Lower,
39 /// Strict upper triangle: M\[i,j\] = 1 when i < j.
40 /// Computes y\_i = sum of x\_j for j > i (suffix sum).
41 Upper,
42}
43
44/// Create a cumulative sum operator: y_i = Σ_{j < i} x_j
45///
46/// This MPO implements a strict lower triangular matrix filled with ones.
47/// For a function g defined on {0, 1, ..., 2^R - 1}, it computes:
48/// f(i) = Σ_{j < i} g(j)
49///
50/// # Arguments
51/// * `r` - Number of bits (sites). Must be at least 2.
52///
53/// # Returns
54/// LinearOperator representing the cumulative sum
55///
56/// # Examples
57///
58/// ```
59/// use tensor4all_quanticstransform::cumsum_operator;
60///
61/// let op = cumsum_operator(4).unwrap();
62/// assert_eq!(op.mpo.node_count(), 4);
63///
64/// // Requires at least 2 sites
65/// assert!(cumsum_operator(1).is_err());
66/// assert!(cumsum_operator(0).is_err());
67/// ```
68pub fn cumsum_operator(r: usize) -> Result<QuanticsOperator> {
69 if r < 2 {
70 anyhow::bail!("Number of sites must be at least 2, got {r}");
71 }
72
73 let mpo = cumsum_mpo(r)?;
74 let site_dims = vec![2; r];
75 tensortrain_to_linear_operator(&mpo, &site_dims)
76}
77
78/// Create a triangular matrix operator with the specified triangle type.
79///
80/// - `Lower`: M\[i,j\] = 1 when i > j (prefix sum: y\_i = sum of x\_j for j < i)
81/// - `Upper`: M\[i,j\] = 1 when i < j (suffix sum: y\_i = sum of x\_j for j > i)
82///
83/// # Arguments
84/// * `r` - Number of bits (sites). Must be at least 2.
85/// * `triangle` - Which triangle to use
86///
87/// # Examples
88///
89/// ```
90/// use tensor4all_quanticstransform::{triangle_operator, TriangleType};
91///
92/// let op = triangle_operator(4, TriangleType::Lower).unwrap();
93/// assert_eq!(op.mpo.node_count(), 4);
94///
95/// // Requires at least 2 sites
96/// assert!(triangle_operator(1, TriangleType::Lower).is_err());
97/// ```
98pub fn triangle_operator(r: usize, triangle: TriangleType) -> Result<QuanticsOperator> {
99 if r < 2 {
100 anyhow::bail!("Number of sites must be at least 2, got {r}");
101 }
102
103 let mpo = triangle_mpo(r, triangle)?;
104 let site_dims = vec![2; r];
105 tensortrain_to_linear_operator(&mpo, &site_dims)
106}
107
108/// Create the cumulative sum MPO as a TensorTrain.
109///
110/// The cumulative sum is implemented as an upper triangular matrix.
111/// The MPO tracks whether a comparison has been made:
112/// - State 0: No comparison yet (y and x equal so far)
113/// - State 1: Comparison made (y > x, so this entry is 1)
114///
115/// Tensor entries t[left, right, y, x]:
116/// - t[0, 0, y, x] = 1 if y == x (both 0 or both 1)
117/// - t[0, 1, 1, 0] = 1 (y > x at this position)
118/// - t[1, 1, *, *] = 1 (comparison already made)
119#[allow(clippy::needless_range_loop)]
120fn cumsum_mpo(r: usize) -> Result<TensorTrain<Complex64>> {
121 if r < 2 {
122 anyhow::bail!("Number of sites must be at least 2, got {r}");
123 }
124
125 let single_tensor = upper_triangle_tensor();
126 let mut tensors = Vec::with_capacity(r);
127
128 for n in 0..r {
129 if n == 0 {
130 // First tensor: start in state 0 (no comparison yet)
131 // Contract with [1, 0] on left link to select state 0
132 let mut t = tensor3_zeros(1, 4, 2);
133 for cout in 0..2 {
134 for y_bit in 0..2 {
135 for x_bit in 0..2 {
136 let val = single_tensor[0][cout][y_bit][x_bit];
137 let s = y_bit * 2 + x_bit;
138 t.set3(0, s, cout, val);
139 }
140 }
141 }
142 tensors.push(t);
143 } else if n == r - 1 {
144 // Last tensor: select entries where state is 1 (y > x)
145 // The output is 1 only if comparison was made (state 1)
146 // For upper triangle (strict), diagonal is excluded
147 let mut t = tensor3_zeros(2, 4, 1);
148 for cin in 0..2 {
149 for y_bit in 0..2 {
150 for x_bit in 0..2 {
151 // Sum over cout states, weighted by whether comparison was made
152 let mut sum = Complex64::zero();
153 for cout in 0..2 {
154 let val = single_tensor[cin][cout][y_bit][x_bit];
155 // Only count if we end in state 1 (comparison made)
156 if cout == 1 {
157 sum += val;
158 }
159 }
160 let s = y_bit * 2 + x_bit;
161 t.set3(cin, s, 0, sum);
162 }
163 }
164 }
165 tensors.push(t);
166 } else {
167 // Middle tensors: full tensor
168 let mut t = tensor3_zeros(2, 4, 2);
169 for cin in 0..2 {
170 for cout in 0..2 {
171 for y_bit in 0..2 {
172 for x_bit in 0..2 {
173 let val = single_tensor[cin][cout][y_bit][x_bit];
174 let s = y_bit * 2 + x_bit;
175 t.set3(cin, s, cout, val);
176 }
177 }
178 }
179 }
180 tensors.push(t);
181 }
182 }
183
184 TensorTrain::new(tensors).map_err(|e| anyhow::anyhow!("Failed to create cumsum MPO: {}", e))
185}
186
187/// Create a triangular matrix MPO as a TensorTrain.
188///
189/// This is a generalization of `cumsum_mpo` that supports both upper and lower triangles.
190#[allow(clippy::needless_range_loop)]
191fn triangle_mpo(r: usize, triangle: TriangleType) -> Result<TensorTrain<Complex64>> {
192 if r < 2 {
193 anyhow::bail!("Number of sites must be at least 2, got {r}");
194 }
195
196 // upper_triangle_tensor() has y>x transition → M[i,j]=1 when i>j = Lower triangle
197 // lower_triangle_tensor() has y<x transition → M[i,j]=1 when i<j = Upper triangle
198 let single_tensor = match triangle {
199 TriangleType::Lower => upper_triangle_tensor(),
200 TriangleType::Upper => lower_triangle_tensor(),
201 };
202 let mut tensors = Vec::with_capacity(r);
203
204 for n in 0..r {
205 if n == 0 {
206 let mut t = tensor3_zeros(1, 4, 2);
207 for cout in 0..2 {
208 for y_bit in 0..2 {
209 for x_bit in 0..2 {
210 let val = single_tensor[0][cout][y_bit][x_bit];
211 let s = y_bit * 2 + x_bit;
212 t.set3(0, s, cout, val);
213 }
214 }
215 }
216 tensors.push(t);
217 } else if n == r - 1 {
218 let mut t = tensor3_zeros(2, 4, 1);
219 for cin in 0..2 {
220 for y_bit in 0..2 {
221 for x_bit in 0..2 {
222 let mut sum = Complex64::zero();
223 for cout in 0..2 {
224 let val = single_tensor[cin][cout][y_bit][x_bit];
225 if cout == 1 {
226 sum += val;
227 }
228 }
229 let s = y_bit * 2 + x_bit;
230 t.set3(cin, s, 0, sum);
231 }
232 }
233 }
234 tensors.push(t);
235 } else {
236 let mut t = tensor3_zeros(2, 4, 2);
237 for cin in 0..2 {
238 for cout in 0..2 {
239 for y_bit in 0..2 {
240 for x_bit in 0..2 {
241 let val = single_tensor[cin][cout][y_bit][x_bit];
242 let s = y_bit * 2 + x_bit;
243 t.set3(cin, s, cout, val);
244 }
245 }
246 }
247 }
248 tensors.push(t);
249 }
250 }
251
252 TensorTrain::new(tensors).map_err(|e| anyhow::anyhow!("Failed to create triangle MPO: {}", e))
253}
254
255/// Create the single-site tensor for upper triangular matrix.
256///
257/// Returns a 4D tensor [cin][cout][y_bit][x_bit] where:
258/// - cin: input state (0 = no comparison yet, 1 = comparison made)
259/// - cout: output state
260/// - y_bit: output (row) bit
261/// - x_bit: input (column) bit
262///
263/// The tensor implements strict upper triangle comparison:
264/// - State 0: Comparing bits. If y > x, transition to state 1.
265/// - State 1: Comparison made. All remaining entries are 1.
266fn upper_triangle_tensor() -> [[[[Complex64; 2]; 2]; 2]; 2] {
267 let mut tensor = [[[[Complex64::zero(); 2]; 2]; 2]; 2];
268
269 // State 0 -> State 0: y == x (continue comparing)
270 tensor[0][0][0][0] = Complex64::one(); // y=0, x=0
271 tensor[0][0][1][1] = Complex64::one(); // y=1, x=1
272
273 // State 0 -> State 1: y > x (comparison made, y is greater)
274 tensor[0][1][1][0] = Complex64::one(); // y=1, x=0 (y > x at this bit)
275
276 // State 0 -> nowhere: y < x (this entry is 0, not in upper triangle)
277 // tensor[0][*][0][1] = 0 (implicit)
278
279 // State 1 -> State 1: Comparison already made, all entries are 1
280 tensor[1][1][0][0] = Complex64::one();
281 tensor[1][1][0][1] = Complex64::one();
282 tensor[1][1][1][0] = Complex64::one();
283 tensor[1][1][1][1] = Complex64::one();
284
285 tensor
286}
287
288/// Create the single-site tensor for strict upper triangle (i < j).
289///
290/// M[i,j] = 1 when i < j (suffix sum: y_i = Σ_{j > i} x_j).
291///
292/// The tensor is identical to lower_triangle but with the transition
293/// on y < x (y=0, x=1) instead of y > x (y=1, x=0).
294fn lower_triangle_tensor() -> [[[[Complex64; 2]; 2]; 2]; 2] {
295 let mut tensor = [[[[Complex64::zero(); 2]; 2]; 2]; 2];
296
297 // State 0 -> State 0: y == x (continue comparing)
298 tensor[0][0][0][0] = Complex64::one(); // y=0, x=0
299 tensor[0][0][1][1] = Complex64::one(); // y=1, x=1
300
301 // State 0 -> State 1: y < x (comparison made, x is greater)
302 tensor[0][1][0][1] = Complex64::one(); // y=0, x=1 (y < x at this bit)
303
304 // State 1 -> State 1: Comparison already made, all entries are 1
305 tensor[1][1][0][0] = Complex64::one();
306 tensor[1][1][0][1] = Complex64::one();
307 tensor[1][1][1][0] = Complex64::one();
308 tensor[1][1][1][1] = Complex64::one();
309
310 tensor
311}
312
313#[cfg(test)]
314mod tests;