Skip to main content

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;