tensor4all_quanticstransform/fourier.rs
1//! Quantics Fourier Transform (QFT) operator
2//!
3//! This implements the Chen & Lindsey method for efficient QFT in tensor train form.
4//! Reference: J. Chen and M. Lindsey, "Direct Interpolative Construction of the
5//! Discrete Fourier Transform as a Matrix Product Operator", arXiv:2404.03182.
6
7use anyhow::Result;
8use num_complex::Complex64;
9use num_traits::Zero;
10use std::f64::consts::PI;
11use tensor4all_simplett::{
12 compression::{CompressionMethod, CompressionOptions},
13 types::tensor3_zeros,
14 Tensor3Ops, TensorTrain,
15};
16
17use crate::common::{tensortrain_to_linear_operator, QuanticsOperator};
18use tensor4all_simplett::tensor::Tensor4;
19
20/// Options for Fourier transform construction.
21///
22/// Controls the sign convention, compression parameters, and normalization
23/// of the quantics Fourier transform MPO.
24///
25/// # Examples
26///
27/// ```
28/// use tensor4all_quanticstransform::FourierOptions;
29///
30/// // Forward transform (default): sign = -1
31/// let fwd = FourierOptions::forward();
32/// assert_eq!(fwd.sign, -1.0);
33/// assert!(fwd.normalize);
34///
35/// // Inverse transform: sign = +1
36/// let inv = FourierOptions::inverse();
37/// assert_eq!(inv.sign, 1.0);
38///
39/// // Custom options
40/// let opts = FourierOptions {
41/// maxbonddim: 20,
42/// tolerance: 1e-12,
43/// ..FourierOptions::forward()
44/// };
45/// assert_eq!(opts.maxbonddim, 20);
46/// ```
47#[derive(Clone, Debug)]
48pub struct FourierOptions {
49 /// Sign in the exponent: -1.0 (forward) or 1.0 (inverse)
50 pub sign: f64,
51 /// Maximum bond dimension after compression
52 pub maxbonddim: usize,
53 /// Tolerance for compression
54 pub tolerance: f64,
55 /// Number of Chebyshev basis functions (K+1 points)
56 pub k: usize,
57 /// Whether to normalize as an isometry
58 pub normalize: bool,
59}
60
61impl Default for FourierOptions {
62 fn default() -> Self {
63 Self {
64 sign: -1.0,
65 maxbonddim: 12,
66 tolerance: 1e-14,
67 k: 25,
68 normalize: true,
69 }
70 }
71}
72
73impl FourierOptions {
74 /// Create options for forward Fourier transform.
75 pub fn forward() -> Self {
76 Self::default()
77 }
78
79 /// Create options for inverse Fourier transform.
80 pub fn inverse() -> Self {
81 Self {
82 sign: 1.0,
83 ..Self::default()
84 }
85 }
86}
87
88/// Convenience wrapper for forward/backward Fourier transform.
89///
90/// Caches the forward MPO so that repeated calls to [`FTCore::forward()`]
91/// and [`FTCore::backward()`] avoid redundant MPO construction.
92///
93/// # Examples
94///
95/// ```
96/// use tensor4all_quanticstransform::{FTCore, FourierOptions};
97///
98/// let ft = FTCore::new(4, FourierOptions::default()).unwrap();
99/// assert_eq!(ft.r(), 4);
100///
101/// let fwd_op = ft.forward().unwrap();
102/// assert_eq!(fwd_op.mpo.node_count(), 4);
103///
104/// let bwd_op = ft.backward().unwrap();
105/// assert_eq!(bwd_op.mpo.node_count(), 4);
106/// ```
107#[derive(Clone)]
108pub struct FTCore {
109 forward_mpo: TensorTrain<Complex64>,
110 r: usize,
111 options: FourierOptions,
112}
113
114impl FTCore {
115 /// Create a new FTCore for r bits.
116 pub fn new(r: usize, options: FourierOptions) -> Result<Self> {
117 if r < 2 {
118 anyhow::bail!("Number of sites must be at least 2, got {r}");
119 }
120 let forward_options = FourierOptions {
121 sign: -1.0,
122 ..options.clone()
123 };
124 let forward_mpo = quantics_fourier_mpo(r, &forward_options)?;
125 Ok(Self {
126 forward_mpo,
127 r,
128 options,
129 })
130 }
131
132 /// Get the forward Fourier transform operator.
133 pub fn forward(&self) -> Result<QuanticsOperator> {
134 let site_dims = vec![2; self.r];
135 tensortrain_to_linear_operator(&self.forward_mpo, &site_dims)
136 }
137
138 /// Get the backward (inverse) Fourier transform operator.
139 pub fn backward(&self) -> Result<QuanticsOperator> {
140 let inverse_options = FourierOptions {
141 sign: 1.0,
142 normalize: self.options.normalize,
143 ..self.options.clone()
144 };
145 let inverse_mpo = quantics_fourier_mpo(self.r, &inverse_options)?;
146 let site_dims = vec![2; self.r];
147 tensortrain_to_linear_operator(&inverse_mpo, &site_dims)
148 }
149
150 /// Get the number of bits.
151 pub fn r(&self) -> usize {
152 self.r
153 }
154}
155
156/// Create a Quantics Fourier Transform operator.
157///
158/// This implements the Chen & Lindsey construction of the DFT as a matrix product operator.
159/// The resulting operator transforms a quantics tensor train representing a function
160/// to its Fourier transform in quantics tensor train form.
161///
162/// # Index ordering
163///
164/// Before the Fourier transform, the leftmost index corresponds to the most significant
165/// bit (largest length scale). After transformation, the leftmost index corresponds to
166/// the least significant bit (smallest length scale) - this allows for a small bond
167/// dimension construction.
168///
169/// # Arguments
170/// * `r` - Number of bits
171/// * `options` - Fourier transform options
172///
173/// # Returns
174/// LinearOperator representing the QFT
175///
176/// # Examples
177///
178/// ```
179/// use tensor4all_quanticstransform::{quantics_fourier_operator, FourierOptions};
180///
181/// // Create a forward QFT operator for 4-bit quantics representation
182/// let op = quantics_fourier_operator(4, FourierOptions::forward()).unwrap();
183///
184/// // The operator has one MPO tensor per bit
185/// assert_eq!(op.mpo.node_count(), 4);
186/// ```
187pub fn quantics_fourier_operator(r: usize, options: FourierOptions) -> Result<QuanticsOperator> {
188 if r < 2 {
189 anyhow::bail!("Number of sites must be at least 2, got {r}");
190 }
191
192 let mpo = quantics_fourier_mpo(r, &options)?;
193 let site_dims = vec![2; r];
194 tensortrain_to_linear_operator(&mpo, &site_dims)
195}
196
197/// Create the QFT MPO as a TensorTrain using Chen & Lindsey construction.
198fn quantics_fourier_mpo(r: usize, options: &FourierOptions) -> Result<TensorTrain<Complex64>> {
199 if r < 2 {
200 anyhow::bail!("Number of sites must be at least 2, got {r}");
201 }
202
203 let k = options.k;
204 let sign = options.sign;
205
206 // Get Chebyshev grid and barycentric weights
207 let (grid, bary_weights) = chebyshev_grid(k);
208
209 // Build core tensor A[alpha, tau, sigma, beta]
210 // alpha, beta in 0..=K (K+1 values each)
211 // tau, sigma in 0..1 (2 values each)
212 let core_tensor = build_dft_core_tensor(&grid, &bary_weights, sign);
213
214 // Construct tensor train
215 let mut tensors = Vec::with_capacity(r);
216
217 // First tensor: sum over alpha (contract with ones vector)
218 // Shape: (1, 4, K+1) where 4 = 2*2 for (tau, sigma)
219 {
220 let mut t = tensor3_zeros(1, 4, k + 1);
221 for tau in 0..2 {
222 for sigma in 0..2 {
223 for beta in 0..=k {
224 let mut sum = Complex64::zero();
225 for alpha in 0..=k {
226 sum += core_tensor[[alpha, tau, sigma, beta]];
227 }
228 let s = tau * 2 + sigma;
229 t.set3(0, s, beta, sum);
230 }
231 }
232 }
233 tensors.push(t);
234 }
235
236 // Middle tensors: full core tensor
237 // Shape: (K+1, 4, K+1)
238 for _ in 1..r - 1 {
239 let mut t = tensor3_zeros(k + 1, 4, k + 1);
240 for alpha in 0..=k {
241 for tau in 0..2 {
242 for sigma in 0..2 {
243 for beta in 0..=k {
244 let s = tau * 2 + sigma;
245 t.set3(alpha, s, beta, core_tensor[[alpha, tau, sigma, beta]]);
246 }
247 }
248 }
249 }
250 tensors.push(t);
251 }
252
253 // Last tensor: select beta = 0
254 // Shape: (K+1, 4, 1)
255 if r > 1 {
256 let mut t = tensor3_zeros(k + 1, 4, 1);
257 for alpha in 0..=k {
258 for tau in 0..2 {
259 for sigma in 0..2 {
260 let s = tau * 2 + sigma;
261 t.set3(alpha, s, 0, core_tensor[[alpha, tau, sigma, 0]]);
262 }
263 }
264 }
265 tensors.push(t);
266 }
267
268 let mut tt = TensorTrain::new(tensors)
269 .map_err(|e| anyhow::anyhow!("Failed to create Fourier MPO: {}", e))?;
270
271 // Compress the tensor train
272 let compress_options = CompressionOptions {
273 method: CompressionMethod::LU,
274 tolerance: options.tolerance,
275 max_bond_dim: options.maxbonddim,
276 normalize_error: true,
277 };
278 let _ = tt.compress(&compress_options);
279
280 // Normalize if requested
281 if options.normalize {
282 let norm_factor = Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0);
283 for tensor in tt.site_tensors_mut() {
284 let (left_dim, site_dim, right_dim) =
285 (tensor.left_dim(), tensor.site_dim(), tensor.right_dim());
286 for l in 0..left_dim {
287 for s in 0..site_dim {
288 for r in 0..right_dim {
289 let val = *tensor.get3(l, s, r);
290 tensor.set3(l, s, r, val * norm_factor);
291 }
292 }
293 }
294 }
295 }
296
297 Ok(tt)
298}
299
300/// Get Chebyshev grid points and barycentric weights.
301///
302/// Returns (grid, bary_weights) where:
303/// - grid[j] = 0.5 * (1 - cos(π*j/K)) for j = 0, ..., K
304/// - bary_weights are the barycentric interpolation weights
305fn chebyshev_grid(k: usize) -> (Vec<f64>, Vec<f64>) {
306 let mut grid = Vec::with_capacity(k + 1);
307 let mut bary_weights = Vec::with_capacity(k + 1);
308
309 // Compute Chebyshev grid points
310 for j in 0..=k {
311 let x = 0.5 * (1.0 - (PI * j as f64 / k as f64).cos());
312 grid.push(x);
313 }
314
315 // Compute barycentric weights
316 for j in 0..=k {
317 let mut weight = 1.0;
318 for m in 0..=k {
319 if j != m {
320 weight /= grid[j] - grid[m];
321 }
322 }
323 bary_weights.push(weight);
324 }
325
326 (grid, bary_weights)
327}
328
329/// Evaluate Lagrange polynomial P_alpha(x).
330fn lagrange_polynomial(grid: &[f64], bary_weights: &[f64], alpha: usize, x: f64) -> f64 {
331 // Check if x is very close to grid[alpha]
332 if (x - grid[alpha]).abs() < 1e-14 {
333 return 1.0;
334 }
335
336 // Compute product term
337 let mut prod = 1.0;
338 for &g in grid {
339 prod *= x - g;
340 }
341
342 prod * bary_weights[alpha] / (x - grid[alpha])
343}
344
345/// Build the DFT core tensor A[alpha, tau, sigma, beta].
346///
347/// A[alpha, tau, sigma, beta] = P_alpha(x) * exp(2πi * sign * x * tau)
348/// where x = (sigma + grid[beta]) / 2
349///
350/// Returns tensor of shape (k+1, 2, 2, k+1)
351fn build_dft_core_tensor(grid: &[f64], bary_weights: &[f64], sign: f64) -> Tensor4<Complex64> {
352 let k = grid.len() - 1;
353
354 // tensor[alpha, tau, sigma, beta] - shape: (k+1, 2, 2, k+1)
355 let mut tensor = Tensor4::from_elem([k + 1, 2, 2, k + 1], Complex64::zero());
356
357 for alpha in 0..=k {
358 for tau in 0..2 {
359 for sigma in 0..2 {
360 for beta in 0..=k {
361 let x = (sigma as f64 + grid[beta]) / 2.0;
362 let p_alpha = lagrange_polynomial(grid, bary_weights, alpha, x);
363 let phase = 2.0 * PI * sign * x * tau as f64;
364 let exp_phase = Complex64::new(phase.cos(), phase.sin());
365 tensor[[alpha, tau, sigma, beta]] = Complex64::new(p_alpha, 0.0) * exp_phase;
366 }
367 }
368 }
369 }
370
371 tensor
372}
373
374#[cfg(test)]
375mod tests;