Skip to main content

qmbed/dynamics/
mod.rs

1use std::f64::consts::PI;
2use std::sync::Arc;
3
4use num_complex::Complex64;
5
6use crate::backend;
7use crate::operator::{
8    LinearOperator, MatrixFormat, Operator, TimeDependentOperator, materialize_dense,
9};
10use crate::solve::{
11    EigshOptions, EvolutionOptions, SpectrumTarget, eigsh, evolve, evolve_time_dependent,
12    expm_action, hermitian_eigenpairs_all, lanczos_spectral_measure,
13};
14use crate::{QmbedError, Result};
15
16const DENSE_PROPAGATOR_CUTOFF: usize = 128;
17
18/// One piecewise-constant Hamiltonian and its physical duration.
19pub struct DriveStep {
20    /// Square Hermitian generator.
21    pub hamiltonian: Arc<dyn LinearOperator>,
22    /// Nonnegative evolution duration.
23    pub duration: f64,
24}
25
26/// One explicitly time-dependent Hamiltonian and its physical duration.
27pub struct CallableDriveStep {
28    /// Square callable generator evaluated at internal integration times.
29    pub hamiltonian: Arc<dyn TimeDependentOperator>,
30    /// Nonnegative evolution duration.
31    pub duration: f64,
32}
33
34impl CallableDriveStep {
35    /// Validate a callable square Hamiltonian and nonnegative duration.
36    pub fn new(hamiltonian: Arc<dyn TimeDependentOperator>, duration: f64) -> Result<Self> {
37        let shape = hamiltonian.shape();
38        if shape.0 != shape.1 {
39            return Err(QmbedError::DimensionMismatch(
40                "a callable drive Hamiltonian must be square".into(),
41            ));
42        }
43        if !duration.is_finite() || duration < 0.0 {
44            return Err(QmbedError::InvalidOptions(
45                "drive duration must be finite and nonnegative".into(),
46            ));
47        }
48        Ok(Self {
49            hamiltonian,
50            duration,
51        })
52    }
53}
54
55enum FloquetStep {
56    Static(DriveStep),
57    Callable(CallableDriveStep),
58}
59
60impl DriveStep {
61    /// Validate a static square Hamiltonian and nonnegative duration.
62    pub fn new(hamiltonian: Arc<dyn LinearOperator>, duration: f64) -> Result<Self> {
63        let shape = hamiltonian.shape();
64        if shape.0 != shape.1 {
65            return Err(QmbedError::DimensionMismatch(
66                "a drive Hamiltonian must be square".into(),
67            ));
68        }
69        if !duration.is_finite() || duration < 0.0 {
70            return Err(QmbedError::InvalidOptions(
71                "drive duration must be finite and nonnegative".into(),
72            ));
73        }
74        Ok(Self {
75            hamiltonian,
76            duration,
77        })
78    }
79}
80
81/// One period of a piecewise-constant drive.
82pub struct Floquet {
83    steps: Vec<FloquetStep>,
84    dimension: usize,
85    evolution: EvolutionOptions,
86    analysis_period: Option<f64>,
87}
88
89impl Floquet {
90    /// Construct one period from ordered piecewise-constant drive steps.
91    pub fn new(steps: impl IntoIterator<Item = DriveStep>) -> Result<Self> {
92        let steps: Vec<_> = steps.into_iter().map(FloquetStep::Static).collect();
93        let first = steps.first().ok_or_else(|| {
94            QmbedError::InvalidOptions("Floquet requires at least one drive step".into())
95        })?;
96        let dimension = first.shape().0;
97        if steps
98            .iter()
99            .any(|step| step.shape() != (dimension, dimension))
100        {
101            return Err(QmbedError::DimensionMismatch(
102                "all drive steps must have the same square shape".into(),
103            ));
104        }
105        Ok(Self {
106            steps,
107            dimension,
108            evolution: EvolutionOptions::new([0.0])
109                .with_krylov_dimension(64)
110                .with_tolerance(1.0e-12),
111            analysis_period: None,
112        })
113    }
114
115    /// Construct one period from ordered callable drive steps.
116    pub fn from_callable(steps: impl IntoIterator<Item = CallableDriveStep>) -> Result<Self> {
117        let steps: Vec<_> = steps.into_iter().map(FloquetStep::Callable).collect();
118        let first = steps.first().ok_or_else(|| {
119            QmbedError::InvalidOptions("Floquet requires at least one drive step".into())
120        })?;
121        let dimension = first.shape().0;
122        if steps
123            .iter()
124            .any(|step| step.shape() != (dimension, dimension))
125        {
126            return Err(QmbedError::DimensionMismatch(
127                "all drive steps must have the same square shape".into(),
128            ));
129        }
130        Ok(Self {
131            steps,
132            dimension,
133            evolution: EvolutionOptions::new([0.0])
134                .with_krylov_dimension(64)
135                .with_tolerance(1.0e-12),
136            analysis_period: None,
137        })
138    }
139
140    /// Replace the Krylov controls used to apply each drive step.
141    pub fn with_evolution_options(mut self, options: EvolutionOptions) -> Self {
142        self.evolution = options;
143        self.evolution.hamiltonian = true;
144        self
145    }
146
147    /// Override the physical Floquet period used for quasienergies and the
148    /// effective Hamiltonian without changing the step durations.
149    ///
150    /// This supports kicked protocols whose explicit evolution intervals do
151    /// not add up to the declared drive period.
152    pub fn with_period(mut self, period: f64) -> Result<Self> {
153        if !period.is_finite() || period <= 0.0 {
154            return Err(QmbedError::InvalidOptions(
155                "Floquet analysis period must be finite and positive".into(),
156            ));
157        }
158        self.analysis_period = Some(period);
159        Ok(self)
160    }
161
162    /// Apply one ordered drive period without materializing the full unitary.
163    pub fn apply_period(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
164        if input.len() != self.dimension || output.len() != self.dimension {
165            return Err(QmbedError::DimensionMismatch(
166                "Floquet input or output length does not match".into(),
167            ));
168        }
169        let mut state = input.to_vec();
170        for step in &self.steps {
171            match step {
172                FloquetStep::Static(step) => {
173                    state = expm_action(
174                        step.hamiltonian.as_ref(),
175                        &state,
176                        step.duration,
177                        &self.evolution,
178                    )?;
179                }
180                FloquetStep::Callable(step) => {
181                    let mut options = self.evolution.clone();
182                    options.times = vec![step.duration];
183                    state = evolve_time_dependent(step.hamiltonian.as_ref(), &state, options)?
184                        .states
185                        .pop()
186                        .ok_or(QmbedError::NonConvergence {
187                            iterations: 0,
188                            residual: f64::INFINITY,
189                        })?;
190                }
191            }
192        }
193        output.copy_from_slice(&state);
194        Ok(())
195    }
196
197    /// Apply the adjoint one-period propagator without materializing it.
198    ///
199    /// The reverse-time action is exact for the static piecewise-constant
200    /// protocol represented by [`DriveStep`]. Callable drives currently fail
201    /// explicitly because their adjoint requires reverse-time evaluation of
202    /// the original time-dependent generator.
203    pub fn apply_adjoint_period(
204        &self,
205        input: &[Complex64],
206        output: &mut [Complex64],
207    ) -> Result<()> {
208        if input.len() != self.dimension || output.len() != self.dimension {
209            return Err(QmbedError::DimensionMismatch(
210                "Floquet input or output length does not match".into(),
211            ));
212        }
213        let mut state = input.to_vec();
214        for step in self.steps.iter().rev() {
215            let FloquetStep::Static(step) = step else {
216                return Err(QmbedError::UnsupportedBackend(
217                    "matrix-free Floquet adjoints currently require static drive steps".into(),
218                ));
219            };
220            state = expm_action(
221                step.hamiltonian.as_ref(),
222                &state,
223                -step.duration,
224                &self.evolution,
225            )?;
226        }
227        output.copy_from_slice(&state);
228        Ok(())
229    }
230
231    /// Return the declared analysis period or the sum of step durations.
232    pub fn period(&self) -> f64 {
233        self.analysis_period
234            .unwrap_or_else(|| self.protocol_duration())
235    }
236
237    /// Sum of the explicit piecewise evolution intervals.
238    pub fn protocol_duration(&self) -> f64 {
239        self.steps.iter().map(FloquetStep::duration).sum()
240    }
241
242    /// Materialize the complete period propagator in the requested format.
243    ///
244    /// This operation scales quadratically in memory and is intended for
245    /// workflows that explicitly require the full Floquet unitary.
246    pub fn full_unitary(&self, format: MatrixFormat) -> Result<Operator> {
247        let dense = if self
248            .steps
249            .iter()
250            .all(|step| matches!(step, FloquetStep::Static(_)))
251            && self.dimension <= DENSE_PROPAGATOR_CUTOFF
252        {
253            let mut total = vec![Complex64::new(0.0, 0.0); self.dimension * self.dimension];
254            for index in 0..self.dimension {
255                total[index * self.dimension + index] = Complex64::new(1.0, 0.0);
256            }
257            for step in &self.steps {
258                let FloquetStep::Static(step) = step else {
259                    unreachable!("all Floquet steps were checked as static");
260                };
261                let hamiltonian = materialize_dense(step.hamiltonian.as_ref())?;
262                let propagator = backend::hermitian_exponential(
263                    &hamiltonian,
264                    self.dimension,
265                    Complex64::new(0.0, -step.duration),
266                )?;
267                total = backend::square_matmul(&propagator, &total, self.dimension)?;
268            }
269            total
270        } else {
271            materialize_dense(self)?
272        };
273        Operator::from_dense(self.dimension, self.dimension, dense)?.converted(format)
274    }
275
276    /// Compute sorted quasienergies, unit-circle eigenvalues, vectors, and residuals.
277    pub fn eigensystem(&self) -> Result<FloquetEigensystem> {
278        let period = self.period();
279        if period <= 0.0 {
280            return Err(QmbedError::InvalidOptions(
281                "Floquet eigensystems require a positive period".into(),
282            ));
283        }
284        let dense_unitary = self.full_unitary(MatrixFormat::Dense)?.to_dense();
285        floquet_eigensystem_from_dense(&dense_unitary, self.dimension, period)
286    }
287
288    /// Compute selected Floquet eigenpairs near one target quasienergy without
289    /// materializing the complete period propagator.
290    ///
291    /// The method solves the Hermitian phase filter
292    /// `Re(exp(-i*target_phase) U)` with the ordinary matrix-free eigensolver,
293    /// then diagonalizes `U` inside the converged candidate subspace. The
294    /// second step separates phase branches that share one cosine-filter
295    /// value.
296    pub fn selected_eigensystem(
297        &self,
298        options: FloquetSpectrumOptions,
299    ) -> Result<FloquetEigensystem> {
300        options.validate(self.dimension)?;
301        let period = self.period();
302        if !period.is_finite() || period <= 0.0 {
303            return Err(QmbedError::InvalidOptions(
304                "selected Floquet spectra require a finite positive period".into(),
305            ));
306        }
307        if self
308            .steps
309            .iter()
310            .any(|step| matches!(step, FloquetStep::Callable(_)))
311        {
312            return Err(QmbedError::UnsupportedBackend(
313                "selected matrix-free Floquet spectra currently require static drive steps".into(),
314            ));
315        }
316        let search_dimension = options.resolved_search_dimension(self.dimension);
317        let target_phase = -options.target_quasienergy * period;
318        let filter = FloquetPhaseFilter {
319            floquet: self,
320            target_phase,
321        };
322        let mut eigsh_options =
323            EigshOptions::new(search_dimension, SpectrumTarget::LargestAlgebraic)
324                .with_tolerance(options.tolerance)
325                .with_max_iterations(options.max_iterations)
326                .with_seed(options.seed);
327        if let Some(krylov_dimension) = options.krylov_dimension {
328            eigsh_options = eigsh_options.with_krylov_dimension(krylov_dimension);
329        }
330        let candidates = eigsh(&filter, eigsh_options)?;
331        let rank = candidates.eigenvectors.len();
332        let mut applied_columns = Vec::with_capacity(rank);
333        for vector in &candidates.eigenvectors {
334            let mut applied = vec![Complex64::new(0.0, 0.0); self.dimension];
335            self.apply_period(vector, &mut applied)?;
336            applied_columns.push(applied);
337        }
338        let mut projected = vec![Complex64::new(0.0, 0.0); rank * rank];
339        for row in 0..rank {
340            for column in 0..rank {
341                projected[row * rank + column] = candidates.eigenvectors[row]
342                    .iter()
343                    .zip(&applied_columns[column])
344                    .map(|(left, right)| left.conj() * *right)
345                    .sum();
346            }
347        }
348        let projected_eigensystem = backend::complex_eigenpairs(&projected, rank)?;
349        let mut selected = Vec::with_capacity(rank);
350        for coefficients in projected_eigensystem.eigenvectors {
351            let mut vector = vec![Complex64::new(0.0, 0.0); self.dimension];
352            for (coefficient, candidate) in coefficients.iter().zip(&candidates.eigenvectors) {
353                for (value, basis_value) in vector.iter_mut().zip(candidate) {
354                    *value += *coefficient * *basis_value;
355                }
356            }
357            let norm = vector.iter().map(Complex64::norm_sqr).sum::<f64>().sqrt();
358            if !norm.is_finite() || norm <= f64::EPSILON {
359                continue;
360            }
361            for value in &mut vector {
362                *value /= norm;
363            }
364            let mut applied = vec![Complex64::new(0.0, 0.0); self.dimension];
365            self.apply_period(&vector, &mut applied)?;
366            let rayleigh = vector
367                .iter()
368                .zip(&applied)
369                .map(|(left, right)| left.conj() * *right)
370                .sum::<Complex64>();
371            if !rayleigh.re.is_finite()
372                || !rayleigh.im.is_finite()
373                || rayleigh.norm() <= f64::EPSILON
374            {
375                continue;
376            }
377            let eigenvalue = rayleigh / rayleigh.norm();
378            let quasienergy = -eigenvalue.arg() / period;
379            let residual = applied
380                .iter()
381                .zip(&vector)
382                .map(|(actual, component)| (*actual - eigenvalue * *component).norm_sqr())
383                .sum::<f64>()
384                .sqrt();
385            let distance = wrapped_phase(eigenvalue.arg() - target_phase).abs();
386            selected.push((distance, quasienergy, eigenvalue, vector, residual));
387        }
388        selected.sort_by(|left, right| {
389            left.0
390                .total_cmp(&right.0)
391                .then_with(|| left.1.total_cmp(&right.1))
392        });
393        if selected.len() < options.eigenpairs {
394            return Err(QmbedError::NonConvergence {
395                iterations: candidates.iterations,
396                residual: selected
397                    .iter()
398                    .map(|entry| entry.4)
399                    .fold(f64::INFINITY, f64::min),
400            });
401        }
402        selected.truncate(options.eigenpairs);
403        Ok(FloquetEigensystem {
404            quasienergies: selected.iter().map(|entry| entry.1).collect(),
405            eigenvalues: selected.iter().map(|entry| entry.2).collect(),
406            eigenvectors: selected.iter().map(|entry| entry.3.clone()).collect(),
407            residuals: selected.into_iter().map(|entry| entry.4).collect(),
408        })
409    }
410
411    /// Construct the principal-branch effective Hamiltonian.
412    pub fn effective_hamiltonian(&self, format: MatrixFormat) -> Result<Operator> {
413        let eigensystem = self.eigensystem()?;
414        self.effective_hamiltonian_from_eigensystem(&eigensystem, format)
415    }
416
417    fn effective_hamiltonian_from_eigensystem(
418        &self,
419        eigensystem: &FloquetEigensystem,
420        format: MatrixFormat,
421    ) -> Result<Operator> {
422        floquet_effective_hamiltonian(eigensystem, self.dimension, format)
423    }
424
425    /// Materialize one period once and compute its complete dense spectrum.
426    ///
427    /// The returned object owns both products, so callers that need unitarity
428    /// and quasienergy checks never need to rebuild the propagator or bring a
429    /// second dense eigensolver into the workflow.
430    pub fn spectrum(&self, format: MatrixFormat) -> Result<FloquetSpectrum> {
431        let period = self.period();
432        if period <= 0.0 {
433            return Err(QmbedError::InvalidOptions(
434                "Floquet spectra require a positive period".into(),
435            ));
436        }
437        let dense_unitary = self.full_unitary(MatrixFormat::Dense)?.to_dense();
438        let unitarity_residual = backend::unitarity_residual(&dense_unitary, self.dimension)?;
439        let eigensystem = floquet_eigensystem_from_dense(&dense_unitary, self.dimension, period)?;
440        Ok(FloquetSpectrum {
441            period,
442            protocol_duration: self.protocol_duration(),
443            unitary: Operator::from_dense(self.dimension, self.dimension, dense_unitary)?
444                .converted(format)?,
445            unitarity_residual,
446            eigensystem,
447        })
448    }
449
450    /// Compute the period propagator and all spectral products while
451    /// materializing the propagator only once.
452    pub fn analyze(&self, format: MatrixFormat) -> Result<FloquetAnalysis> {
453        let spectrum = self.spectrum(format)?;
454        let effective_hamiltonian =
455            self.effective_hamiltonian_from_eigensystem(&spectrum.eigensystem, format)?;
456        Ok(FloquetAnalysis {
457            period: spectrum.period,
458            protocol_duration: spectrum.protocol_duration,
459            unitary: spectrum.unitary,
460            unitarity_residual: spectrum.unitarity_residual,
461            eigensystem: spectrum.eigensystem,
462            effective_hamiltonian,
463        })
464    }
465}
466
467fn wrapped_phase(value: f64) -> f64 {
468    (value + PI).rem_euclid(2.0 * PI) - PI
469}
470
471struct FloquetPhaseFilter<'a> {
472    floquet: &'a Floquet,
473    target_phase: f64,
474}
475
476impl LinearOperator for FloquetPhaseFilter<'_> {
477    fn shape(&self) -> (usize, usize) {
478        (self.floquet.dimension, self.floquet.dimension)
479    }
480
481    fn format(&self) -> MatrixFormat {
482        MatrixFormat::MatrixFree
483    }
484
485    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
486        if input.len() != self.floquet.dimension || output.len() != self.floquet.dimension {
487            return Err(QmbedError::DimensionMismatch(
488                "Floquet phase-filter vector length does not match".into(),
489            ));
490        }
491        let mut forward = vec![Complex64::new(0.0, 0.0); self.floquet.dimension];
492        let mut adjoint = vec![Complex64::new(0.0, 0.0); self.floquet.dimension];
493        self.floquet.apply_period(input, &mut forward)?;
494        self.floquet.apply_adjoint_period(input, &mut adjoint)?;
495        let forward_phase = Complex64::from_polar(0.5, -self.target_phase);
496        let adjoint_phase = Complex64::from_polar(0.5, self.target_phase);
497        for ((value, forward), adjoint) in output.iter_mut().zip(forward).zip(adjoint) {
498            *value = forward_phase * forward + adjoint_phase * adjoint;
499        }
500        Ok(())
501    }
502}
503
504impl FloquetStep {
505    fn shape(&self) -> (usize, usize) {
506        match self {
507            Self::Static(step) => step.hamiltonian.shape(),
508            Self::Callable(step) => step.hamiltonian.shape(),
509        }
510    }
511
512    fn duration(&self) -> f64 {
513        match self {
514            Self::Static(step) => step.duration,
515            Self::Callable(step) => step.duration,
516        }
517    }
518}
519
520/// Complete eigensystem of a one-period unitary.
521#[derive(Clone, Debug)]
522pub struct FloquetEigensystem {
523    /// Quasienergies ordered on the selected phase branch.
524    pub quasienergies: Vec<f64>,
525    /// Unit-circle eigenvalues of the period propagator.
526    pub eigenvalues: Vec<Complex64>,
527    /// Corresponding column eigenvectors.
528    pub eigenvectors: Vec<Vec<Complex64>>,
529    /// Norm of `U v - λ v` for each pair.
530    pub residuals: Vec<f64>,
531}
532
533/// Controls a matrix-free Floquet eigensolve near one target quasienergy.
534#[derive(Clone, Debug, PartialEq)]
535#[non_exhaustive]
536pub struct FloquetSpectrumOptions {
537    /// Number of returned Floquet eigenpairs.
538    pub eigenpairs: usize,
539    /// Target quasienergy on the principal branch.
540    pub target_quasienergy: f64,
541    /// Candidate phase-filter subspace size before projected unitary
542    /// diagonalization.
543    pub search_dimension: Option<usize>,
544    /// Optional Lanczos restart window used by the Hermitian phase filter.
545    pub krylov_dimension: Option<usize>,
546    /// Required Hermitian-filter residual tolerance.
547    pub tolerance: f64,
548    /// Maximum eigensolver iteration budget.
549    pub max_iterations: usize,
550    /// Deterministic initial-vector seed.
551    pub seed: u64,
552}
553
554impl FloquetSpectrumOptions {
555    /// Construct controls for `eigenpairs` nearest `target_quasienergy`.
556    pub fn new(eigenpairs: usize, target_quasienergy: f64) -> Self {
557        Self {
558            eigenpairs,
559            target_quasienergy,
560            search_dimension: None,
561            krylov_dimension: None,
562            tolerance: 1.0e-10,
563            max_iterations: 1_000,
564            seed: 0,
565        }
566    }
567
568    /// Set the number of phase-filter candidates retained for projected
569    /// unitary diagonalization.
570    #[must_use]
571    pub fn with_search_dimension(mut self, dimension: usize) -> Self {
572        self.search_dimension = Some(dimension);
573        self
574    }
575
576    /// Set the optional Lanczos/restart window.
577    #[must_use]
578    pub fn with_krylov_dimension(mut self, dimension: usize) -> Self {
579        self.krylov_dimension = Some(dimension);
580        self
581    }
582
583    /// Set the required phase-filter tolerance.
584    #[must_use]
585    pub fn with_tolerance(mut self, tolerance: f64) -> Self {
586        self.tolerance = tolerance;
587        self
588    }
589
590    /// Set the maximum eigensolver iteration budget.
591    #[must_use]
592    pub fn with_max_iterations(mut self, iterations: usize) -> Self {
593        self.max_iterations = iterations;
594        self
595    }
596
597    /// Set the deterministic initial-vector seed.
598    #[must_use]
599    pub fn with_seed(mut self, seed: u64) -> Self {
600        self.seed = seed;
601        self
602    }
603
604    fn resolved_search_dimension(&self, dimension: usize) -> usize {
605        self.search_dimension
606            .unwrap_or_else(|| self.eigenpairs.saturating_mul(2).max(self.eigenpairs + 4))
607            .min(dimension - 1)
608    }
609
610    fn validate(&self, dimension: usize) -> Result<()> {
611        if self.eigenpairs == 0 || self.eigenpairs >= dimension {
612            return Err(QmbedError::InvalidOptions(
613                "Floquet eigenpairs must be positive and smaller than the dimension".into(),
614            ));
615        }
616        if !self.target_quasienergy.is_finite()
617            || !self.tolerance.is_finite()
618            || self.tolerance <= 0.0
619            || self.max_iterations == 0
620        {
621            return Err(QmbedError::InvalidOptions(
622                "invalid Floquet target or numerical controls".into(),
623            ));
624        }
625        let search_dimension = self.resolved_search_dimension(dimension);
626        if search_dimension < self.eigenpairs {
627            return Err(QmbedError::InvalidOptions(
628                "Floquet search_dimension must cover every requested eigenpair".into(),
629            ));
630        }
631        if self
632            .krylov_dimension
633            .is_some_and(|krylov| krylov <= search_dimension || krylov > dimension)
634        {
635            return Err(QmbedError::InvalidOptions(
636                "Floquet krylov_dimension must exceed the search dimension and fit the operator"
637                    .into(),
638            ));
639        }
640        Ok(())
641    }
642}
643
644/// One-materialization Floquet unitary and its complete spectrum.
645#[derive(Clone, Debug)]
646pub struct FloquetSpectrum {
647    /// Physical period used to convert phases into quasienergies.
648    pub period: f64,
649    /// Sum of explicit evolution intervals.
650    pub protocol_duration: f64,
651    /// Materialized period propagator.
652    pub unitary: Operator,
653    /// Norm measuring deviation from `U†U = I`.
654    pub unitarity_residual: f64,
655    /// Complete eigensystem of `unitary`.
656    pub eigensystem: FloquetEigensystem,
657}
658
659/// Floquet spectrum together with the principal-branch effective Hamiltonian.
660#[derive(Clone, Debug)]
661pub struct FloquetAnalysis {
662    /// Physical analysis period.
663    pub period: f64,
664    /// Sum of explicit drive-step durations.
665    pub protocol_duration: f64,
666    /// Materialized period propagator.
667    pub unitary: Operator,
668    /// Norm measuring deviation from exact unitarity.
669    pub unitarity_residual: f64,
670    /// Complete unitary eigensystem.
671    pub eigensystem: FloquetEigensystem,
672    /// Hermitian generator reconstructed on the principal quasienergy branch.
673    pub effective_hamiltonian: Operator,
674}
675
676fn floquet_eigensystem_from_dense(
677    unitary: &[Complex64],
678    dimension: usize,
679    period: f64,
680) -> Result<FloquetEigensystem> {
681    let eigensystem = backend::complex_eigenpairs(unitary, dimension)?;
682    let mut entries = Vec::with_capacity(dimension);
683    for column in 0..dimension {
684        let eigenvalue = eigensystem.eigenvalues[column];
685        if (eigenvalue.norm() - 1.0).abs() > 1.0e-8 {
686            return Err(QmbedError::NonConvergence {
687                iterations: 1,
688                residual: (eigenvalue.norm() - 1.0).abs(),
689            });
690        }
691        let vector = eigensystem.eigenvectors[column].clone();
692        let mut applied = vec![Complex64::new(0.0, 0.0); dimension];
693        for row in 0..dimension {
694            applied[row] = (0..dimension)
695                .map(|inner| unitary[row * dimension + inner] * vector[inner])
696                .sum();
697        }
698        let residual = applied
699            .iter()
700            .zip(&vector)
701            .map(|(actual, component)| (*actual - eigenvalue * *component).norm_sqr())
702            .sum::<f64>()
703            .sqrt();
704        entries.push((-eigenvalue.arg() / period, eigenvalue, vector, residual));
705    }
706    entries.sort_by(|left, right| left.0.total_cmp(&right.0));
707    Ok(FloquetEigensystem {
708        quasienergies: entries.iter().map(|entry| entry.0).collect(),
709        eigenvalues: entries.iter().map(|entry| entry.1).collect(),
710        eigenvectors: entries.iter().map(|entry| entry.2.clone()).collect(),
711        residuals: entries.into_iter().map(|entry| entry.3).collect(),
712    })
713}
714
715fn floquet_effective_hamiltonian(
716    eigensystem: &FloquetEigensystem,
717    dimension: usize,
718    format: MatrixFormat,
719) -> Result<Operator> {
720    let mut values = vec![Complex64::new(0.0, 0.0); dimension * dimension];
721    for (energy, vector) in eigensystem
722        .quasienergies
723        .iter()
724        .zip(&eigensystem.eigenvectors)
725    {
726        for row in 0..dimension {
727            for column in 0..dimension {
728                values[row * dimension + column] += *energy * vector[row] * vector[column].conj();
729            }
730        }
731    }
732    Operator::from_dense(dimension, dimension, values)?.converted(format)
733}
734
735fn analyze_floquet_dense_unitary(
736    dense_unitary: Vec<Complex64>,
737    dimension: usize,
738    period: f64,
739    protocol_duration: f64,
740    format: MatrixFormat,
741) -> Result<FloquetAnalysis> {
742    if !period.is_finite() || period <= 0.0 {
743        return Err(QmbedError::InvalidOptions(
744            "Floquet analyses require a finite positive period".into(),
745        ));
746    }
747    let unitarity_residual = backend::unitarity_residual(&dense_unitary, dimension)?;
748    let eigensystem = floquet_eigensystem_from_dense(&dense_unitary, dimension, period)?;
749    let effective_hamiltonian = floquet_effective_hamiltonian(&eigensystem, dimension, format)?;
750    Ok(FloquetAnalysis {
751        period,
752        protocol_duration,
753        unitary: Operator::from_dense(dimension, dimension, dense_unitary)?.converted(format)?,
754        unitarity_residual,
755        eigensystem,
756        effective_hamiltonian,
757    })
758}
759
760/// Analyze an already-constructed one-period propagator.
761///
762/// This is the common boundary for continuous-time integrators, tensor-network
763/// propagators, and externally supplied unitaries.
764pub fn analyze_floquet_unitary(
765    unitary: &dyn LinearOperator,
766    period: f64,
767    format: MatrixFormat,
768) -> Result<FloquetAnalysis> {
769    let (rows, columns) = unitary.shape();
770    if rows != columns {
771        return Err(QmbedError::DimensionMismatch(
772            "a Floquet propagator must be square".into(),
773        ));
774    }
775    analyze_floquet_dense_unitary(materialize_dense(unitary)?, rows, period, period, format)
776}
777
778#[derive(Clone, Copy, Debug, PartialEq)]
779pub struct FloquetCoordinate {
780    pub cycle: usize,
781    pub within_cycle: f64,
782}
783
784#[derive(Clone, Debug, PartialEq)]
785pub struct FloquetTimeVector {
786    period: f64,
787    cycles: usize,
788    points_per_cycle: usize,
789    times: Vec<f64>,
790}
791
792impl FloquetTimeVector {
793    pub fn new(
794        period: f64,
795        cycles: usize,
796        points_per_cycle: usize,
797        include_endpoint: bool,
798    ) -> Result<Self> {
799        if !period.is_finite() || period <= 0.0 || cycles == 0 || points_per_cycle == 0 {
800            return Err(QmbedError::InvalidOptions(
801                "Floquet time-vector controls must be positive".into(),
802            ));
803        }
804        let points = cycles
805            .checked_mul(points_per_cycle)
806            .and_then(|value| value.checked_add(usize::from(include_endpoint)))
807            .ok_or_else(|| QmbedError::InvalidOptions("Floquet time-vector overflow".into()))?;
808        let step = period / points_per_cycle as f64;
809        let times = (0..points).map(|index| index as f64 * step).collect();
810        Ok(Self {
811            period,
812            cycles,
813            points_per_cycle,
814            times,
815        })
816    }
817
818    /// Time grid for ramp-up, constant, and ramp-down Floquet stages.
819    ///
820    /// The grid starts at `-ramp_up_cycles * period`, includes the final
821    /// endpoint, and retains a uniform number of points per period.
822    pub fn staged(
823        period: f64,
824        ramp_up_cycles: usize,
825        constant_cycles: usize,
826        ramp_down_cycles: usize,
827        points_per_cycle: usize,
828    ) -> Result<Self> {
829        let cycles = ramp_up_cycles
830            .checked_add(constant_cycles)
831            .and_then(|value| value.checked_add(ramp_down_cycles))
832            .ok_or_else(|| QmbedError::InvalidOptions("Floquet cycle count overflow".into()))?;
833        if !period.is_finite()
834            || period <= 0.0
835            || constant_cycles == 0
836            || cycles == 0
837            || points_per_cycle == 0
838        {
839            return Err(QmbedError::InvalidOptions(
840                "Floquet staged-grid controls must be positive".into(),
841            ));
842        }
843        let points = cycles
844            .checked_mul(points_per_cycle)
845            .and_then(|value| value.checked_add(1))
846            .ok_or_else(|| QmbedError::InvalidOptions("Floquet time-vector overflow".into()))?;
847        let step = period / points_per_cycle as f64;
848        let start = -(ramp_up_cycles as f64) * period;
849        let times = (0..points)
850            .map(|index| start + index as f64 * step)
851            .collect();
852        Ok(Self {
853            period,
854            cycles,
855            points_per_cycle,
856            times,
857        })
858    }
859
860    pub const fn period(&self) -> f64 {
861        self.period
862    }
863
864    pub const fn cycles(&self) -> usize {
865        self.cycles
866    }
867
868    pub const fn points_per_cycle(&self) -> usize {
869        self.points_per_cycle
870    }
871
872    pub fn times(&self) -> &[f64] {
873        &self.times
874    }
875
876    pub fn coordinate(&self, index: usize) -> Result<FloquetCoordinate> {
877        if index >= self.times.len() {
878            return Err(QmbedError::InvalidOptions(
879                "Floquet time index is out of bounds".into(),
880            ));
881        }
882        Ok(FloquetCoordinate {
883            cycle: index / self.points_per_cycle,
884            within_cycle: (index % self.points_per_cycle) as f64 * self.period
885                / self.points_per_cycle as f64,
886        })
887    }
888}
889
890impl LinearOperator for Floquet {
891    fn shape(&self) -> (usize, usize) {
892        (self.dimension, self.dimension)
893    }
894
895    fn format(&self) -> MatrixFormat {
896        MatrixFormat::MatrixFree
897    }
898
899    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
900        self.apply_period(input, output)
901    }
902}
903
904/// Frequency grid and Lanczos controls for broadened spectral functions.
905#[derive(Clone, Debug, PartialEq)]
906#[non_exhaustive]
907pub struct SpectrumOptions {
908    /// Frequencies at which the response is evaluated.
909    pub frequencies: Vec<f64>,
910    /// Reference-state energy subtracted from the resolvent.
911    pub reference_energy: f64,
912    /// Positive Lorentzian broadening.
913    pub broadening: f64,
914    /// Maximum Krylov dimension.
915    pub krylov_dimension: usize,
916    /// Continued-fraction termination tolerance.
917    pub tolerance: f64,
918}
919
920impl SpectrumOptions {
921    /// Construct spectral-function controls for a frequency grid.
922    pub fn new(frequencies: impl Into<Vec<f64>>, reference_energy: f64, broadening: f64) -> Self {
923        Self {
924            frequencies: frequencies.into(),
925            reference_energy,
926            broadening,
927            krylov_dimension: 64,
928            tolerance: 1.0e-10,
929        }
930    }
931
932    /// Set the maximum Krylov dimension.
933    #[must_use]
934    pub fn with_krylov_dimension(mut self, dimension: usize) -> Self {
935        self.krylov_dimension = dimension;
936        self
937    }
938
939    /// Set the continued-fraction termination tolerance.
940    #[must_use]
941    pub fn with_tolerance(mut self, tolerance: f64) -> Self {
942        self.tolerance = tolerance;
943        self
944    }
945
946    fn validate(&self) -> Result<()> {
947        if self.frequencies.is_empty()
948            || self.frequencies.iter().any(|value| !value.is_finite())
949            || !self.reference_energy.is_finite()
950            || !self.broadening.is_finite()
951            || self.broadening <= 0.0
952            || self.krylov_dimension == 0
953            || !self.tolerance.is_finite()
954            || self.tolerance <= 0.0
955        {
956            return Err(QmbedError::InvalidOptions(
957                "invalid spectrum frequency grid or numerical controls".into(),
958            ));
959        }
960        Ok(())
961    }
962}
963
964/// Lorentzian-broadened spectral density in a same or different target sector.
965pub fn spectral_function<H, P>(
966    target_hamiltonian: &H,
967    source: &[Complex64],
968    probe: &P,
969    options: SpectrumOptions,
970) -> Result<Vec<f64>>
971where
972    H: LinearOperator + ?Sized,
973    P: LinearOperator + ?Sized,
974{
975    options.validate()?;
976    let target_shape = target_hamiltonian.shape();
977    let probe_shape = probe.shape();
978    if target_shape.0 != target_shape.1
979        || probe_shape.0 != target_shape.0
980        || probe_shape.1 != source.len()
981    {
982        return Err(QmbedError::DimensionMismatch(
983            "target Hamiltonian, source, and probe shapes are incompatible".into(),
984        ));
985    }
986    let mut created = vec![Complex64::new(0.0, 0.0); probe_shape.0];
987    probe.apply(source, &mut created)?;
988    let (energies, weights) = if target_shape.0 <= 128 {
989        let (energies, eigenvectors) = hermitian_eigenpairs_all(target_hamiltonian)?;
990        let weights = eigenvectors
991            .iter()
992            .map(|vector| {
993                vector
994                    .iter()
995                    .zip(&created)
996                    .map(|(left, right)| left.conj() * *right)
997                    .sum::<Complex64>()
998                    .norm_sqr()
999            })
1000            .collect();
1001        (energies, weights)
1002    } else {
1003        lanczos_spectral_measure(target_hamiltonian, &created, options.krylov_dimension)?
1004    };
1005    Ok(options
1006        .frequencies
1007        .iter()
1008        .map(|&frequency| {
1009            energies
1010                .iter()
1011                .zip(&weights)
1012                .map(|(&energy, &weight)| {
1013                    let detuning = frequency + options.reference_energy - energy;
1014                    weight * options.broadening
1015                        / (PI * (detuning * detuning + options.broadening.powi(2)))
1016                })
1017                .sum()
1018        })
1019        .collect())
1020}
1021
1022/// Real-time two-point function `<psi|A(t) B(0)|psi>`.
1023pub fn dynamical_correlator<H, A, B>(
1024    hamiltonian: &H,
1025    state: &[Complex64],
1026    left_probe: &A,
1027    right_probe: &B,
1028    mut options: EvolutionOptions,
1029) -> Result<Vec<Complex64>>
1030where
1031    H: LinearOperator + ?Sized,
1032    A: LinearOperator + ?Sized,
1033    B: LinearOperator + ?Sized,
1034{
1035    let dimension = state.len();
1036    if hamiltonian.shape() != (dimension, dimension)
1037        || left_probe.shape() != (dimension, dimension)
1038        || right_probe.shape() != (dimension, dimension)
1039    {
1040        return Err(QmbedError::DimensionMismatch(
1041            "correlator Hamiltonian, probes, and state dimensions do not match".into(),
1042        ));
1043    }
1044    options.hamiltonian = true;
1045    let mut created = vec![Complex64::new(0.0, 0.0); dimension];
1046    right_probe.apply(state, &mut created)?;
1047    let reference = evolve(hamiltonian, state, options.clone())?;
1048    let excited = evolve(hamiltonian, &created, options)?;
1049    reference
1050        .states
1051        .iter()
1052        .zip(excited.states)
1053        .map(|(reference_state, excited_state)| {
1054            let mut probed = vec![Complex64::new(0.0, 0.0); dimension];
1055            left_probe.apply(&excited_state, &mut probed)?;
1056            Ok(reference_state
1057                .iter()
1058                .zip(probed)
1059                .map(|(left, right)| left.conj() * right)
1060                .sum())
1061        })
1062        .collect()
1063}