Skip to main content

qmbed/workflow/
mod.rs

1use std::sync::Arc;
2
3use num_complex::Complex64;
4
5use crate::operator::{LinearOperator, MatrixFormat, check_apply_shape, materialize_dense};
6use crate::{QmbedError, Result};
7
8/// Matrix-free Lindblad generator over column-major vectorized density matrices.
9pub struct LindbladGenerator {
10    hamiltonian: Vec<Complex64>,
11    jumps: Vec<LindbladJump>,
12    dimension: usize,
13}
14
15struct LindbladJump {
16    operator: Vec<Complex64>,
17    adjoint: Vec<Complex64>,
18    product: Vec<Complex64>,
19}
20
21impl LindbladGenerator {
22    pub fn new(
23        hamiltonian: Arc<dyn LinearOperator>,
24        jumps: Vec<Arc<dyn LinearOperator>>,
25    ) -> Result<Self> {
26        let shape = hamiltonian.shape();
27        if shape.0 != shape.1 {
28            return Err(QmbedError::DimensionMismatch(
29                "the Lindblad Hamiltonian must be square".into(),
30            ));
31        }
32        if jumps.iter().any(|jump| jump.shape() != shape) {
33            return Err(QmbedError::DimensionMismatch(
34                "all Lindblad jumps must match the Hamiltonian".into(),
35            ));
36        }
37        let dimension = shape.0;
38        let hamiltonian = materialize_dense(hamiltonian.as_ref())?;
39        let jumps = jumps
40            .into_iter()
41            .map(|jump| {
42                let operator = materialize_dense(jump.as_ref())?;
43                let adjoint = adjoint(&operator, dimension);
44                let product = multiply(&adjoint, &operator, dimension);
45                Ok(LindbladJump {
46                    operator,
47                    adjoint,
48                    product,
49                })
50            })
51            .collect::<Result<Vec<_>>>()?;
52        Ok(Self {
53            hamiltonian,
54            jumps,
55            dimension,
56        })
57    }
58}
59
60fn multiply(left: &[Complex64], right: &[Complex64], dimension: usize) -> Vec<Complex64> {
61    let mut product = vec![Complex64::new(0.0, 0.0); dimension * dimension];
62    for row in 0..dimension {
63        for middle in 0..dimension {
64            for column in 0..dimension {
65                product[row * dimension + column] +=
66                    left[row * dimension + middle] * right[middle * dimension + column];
67            }
68        }
69    }
70    product
71}
72
73fn adjoint(matrix: &[Complex64], dimension: usize) -> Vec<Complex64> {
74    let mut result = vec![Complex64::new(0.0, 0.0); dimension * dimension];
75    for row in 0..dimension {
76        for column in 0..dimension {
77            result[row * dimension + column] = matrix[column * dimension + row].conj();
78        }
79    }
80    result
81}
82
83fn column_major_to_row_major(vector: &[Complex64], dimension: usize) -> Vec<Complex64> {
84    let mut matrix = vec![Complex64::new(0.0, 0.0); vector.len()];
85    for row in 0..dimension {
86        for column in 0..dimension {
87            matrix[row * dimension + column] = vector[row + column * dimension];
88        }
89    }
90    matrix
91}
92
93fn row_major_to_column_major(matrix: &[Complex64], dimension: usize) -> Vec<Complex64> {
94    let mut vector = vec![Complex64::new(0.0, 0.0); matrix.len()];
95    for row in 0..dimension {
96        for column in 0..dimension {
97            vector[row + column * dimension] = matrix[row * dimension + column];
98        }
99    }
100    vector
101}
102
103impl LinearOperator for LindbladGenerator {
104    fn shape(&self) -> (usize, usize) {
105        let size = self.dimension * self.dimension;
106        (size, size)
107    }
108
109    fn format(&self) -> MatrixFormat {
110        MatrixFormat::MatrixFree
111    }
112
113    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
114        check_apply_shape(self.shape(), input, output)?;
115        let dimension = self.dimension;
116        let density = column_major_to_row_major(input, dimension);
117        let h_rho = multiply(&self.hamiltonian, &density, dimension);
118        let rho_h = multiply(&density, &self.hamiltonian, dimension);
119        let mut derivative: Vec<_> = h_rho
120            .iter()
121            .zip(&rho_h)
122            .map(|(left, right)| Complex64::new(0.0, -1.0) * (*left - *right))
123            .collect();
124        for jump in &self.jumps {
125            let gain = multiply(
126                &multiply(&jump.operator, &density, dimension),
127                &jump.adjoint,
128                dimension,
129            );
130            let loss_left = multiply(&jump.product, &density, dimension);
131            let loss_right = multiply(&density, &jump.product, dimension);
132            for index in 0..derivative.len() {
133                derivative[index] += gain[index] - 0.5 * (loss_left[index] + loss_right[index]);
134            }
135        }
136        output.copy_from_slice(&row_major_to_column_major(&derivative, dimension));
137        Ok(())
138    }
139}
140
141#[derive(Clone, Debug)]
142pub struct StateTrackingResult {
143    /// `permutation[previous_index]` is the matched current-state index.
144    pub permutation: Vec<usize>,
145    /// Multiply each matched current state by this phase to align gauges.
146    pub phases: Vec<Complex64>,
147    pub overlaps: Vec<f64>,
148    pub ambiguous: Vec<usize>,
149}
150
151fn state_inner(left: &[Complex64], right: &[Complex64]) -> Complex64 {
152    left.iter()
153        .zip(right)
154        .map(|(left, right)| left.conj() * *right)
155        .sum()
156}
157
158/// Match two equal-rank eigenvector frames by globally maximizing absolute
159/// overlaps, then return gauge-aligning phases and ambiguity diagnostics.
160pub fn track_states(
161    previous: &[Vec<Complex64>],
162    current: &[Vec<Complex64>],
163    ambiguity_tolerance: f64,
164) -> Result<StateTrackingResult> {
165    let rank = previous.len();
166    if rank == 0
167        || current.len() != rank
168        || !ambiguity_tolerance.is_finite()
169        || ambiguity_tolerance < 0.0
170    {
171        return Err(QmbedError::InvalidOptions(
172            "state tracking requires equal nonzero ranks and a nonnegative tolerance".into(),
173        ));
174    }
175    let dimension = previous[0].len();
176    if dimension == 0
177        || previous.iter().any(|vector| vector.len() != dimension)
178        || current.iter().any(|vector| vector.len() != dimension)
179    {
180        return Err(QmbedError::DimensionMismatch(
181            "tracked state vectors must have equal nonzero dimensions".into(),
182        ));
183    }
184    let overlaps: Vec<Vec<_>> = previous
185        .iter()
186        .map(|left| {
187            current
188                .iter()
189                .map(|right| state_inner(left, right))
190                .collect()
191        })
192        .collect();
193
194    // Hungarian algorithm for the minimum cost `-abs(overlap)` assignment.
195    let mut row_potential = vec![0.0_f64; rank + 1];
196    let mut column_potential = vec![0.0_f64; rank + 1];
197    let mut matched_row = vec![0_usize; rank + 1];
198    let mut predecessor = vec![0_usize; rank + 1];
199    for row in 1..=rank {
200        matched_row[0] = row;
201        let mut column = 0;
202        let mut minimum = vec![f64::INFINITY; rank + 1];
203        let mut used = vec![false; rank + 1];
204        loop {
205            used[column] = true;
206            let active_row = matched_row[column];
207            let mut delta = f64::INFINITY;
208            let mut next_column = 0;
209            for candidate in 1..=rank {
210                if used[candidate] {
211                    continue;
212                }
213                let cost = -overlaps[active_row - 1][candidate - 1].norm()
214                    - row_potential[active_row]
215                    - column_potential[candidate];
216                if cost < minimum[candidate] {
217                    minimum[candidate] = cost;
218                    predecessor[candidate] = column;
219                }
220                if minimum[candidate] < delta {
221                    delta = minimum[candidate];
222                    next_column = candidate;
223                }
224            }
225            for candidate in 0..=rank {
226                if used[candidate] {
227                    row_potential[matched_row[candidate]] += delta;
228                    column_potential[candidate] -= delta;
229                } else {
230                    minimum[candidate] -= delta;
231                }
232            }
233            column = next_column;
234            if matched_row[column] == 0 {
235                break;
236            }
237        }
238        loop {
239            let previous_column = predecessor[column];
240            matched_row[column] = matched_row[previous_column];
241            column = previous_column;
242            if column == 0 {
243                break;
244            }
245        }
246    }
247    let mut permutation = vec![0_usize; rank];
248    for column in 1..=rank {
249        permutation[matched_row[column] - 1] = column - 1;
250    }
251    let mut phases = Vec::with_capacity(rank);
252    let mut assigned_overlaps = Vec::with_capacity(rank);
253    let mut ambiguous = Vec::new();
254    for row in 0..rank {
255        let overlap = overlaps[row][permutation[row]];
256        let magnitude = overlap.norm();
257        phases.push(if magnitude > f64::EPSILON {
258            overlap.conj() / magnitude
259        } else {
260            Complex64::new(1.0, 0.0)
261        });
262        assigned_overlaps.push(magnitude);
263        let alternative = overlaps[row]
264            .iter()
265            .enumerate()
266            .filter(|(column, _)| *column != permutation[row])
267            .map(|(_, value)| value.norm())
268            .fold(0.0_f64, f64::max);
269        if magnitude - alternative <= ambiguity_tolerance {
270            ambiguous.push(row);
271        }
272    }
273    Ok(StateTrackingResult {
274        permutation,
275        phases,
276        overlaps: assigned_overlaps,
277        ambiguous,
278    })
279}