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
18pub struct DriveStep {
20 pub hamiltonian: Arc<dyn LinearOperator>,
22 pub duration: f64,
24}
25
26pub struct CallableDriveStep {
28 pub hamiltonian: Arc<dyn TimeDependentOperator>,
30 pub duration: f64,
32}
33
34impl CallableDriveStep {
35 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 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
81pub struct Floquet {
83 steps: Vec<FloquetStep>,
84 dimension: usize,
85 evolution: EvolutionOptions,
86 analysis_period: Option<f64>,
87}
88
89impl Floquet {
90 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 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 pub fn with_evolution_options(mut self, options: EvolutionOptions) -> Self {
142 self.evolution = options;
143 self.evolution.hamiltonian = true;
144 self
145 }
146
147 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 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 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 pub fn period(&self) -> f64 {
233 self.analysis_period
234 .unwrap_or_else(|| self.protocol_duration())
235 }
236
237 pub fn protocol_duration(&self) -> f64 {
239 self.steps.iter().map(FloquetStep::duration).sum()
240 }
241
242 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 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 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 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 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 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#[derive(Clone, Debug)]
522pub struct FloquetEigensystem {
523 pub quasienergies: Vec<f64>,
525 pub eigenvalues: Vec<Complex64>,
527 pub eigenvectors: Vec<Vec<Complex64>>,
529 pub residuals: Vec<f64>,
531}
532
533#[derive(Clone, Debug, PartialEq)]
535#[non_exhaustive]
536pub struct FloquetSpectrumOptions {
537 pub eigenpairs: usize,
539 pub target_quasienergy: f64,
541 pub search_dimension: Option<usize>,
544 pub krylov_dimension: Option<usize>,
546 pub tolerance: f64,
548 pub max_iterations: usize,
550 pub seed: u64,
552}
553
554impl FloquetSpectrumOptions {
555 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 #[must_use]
571 pub fn with_search_dimension(mut self, dimension: usize) -> Self {
572 self.search_dimension = Some(dimension);
573 self
574 }
575
576 #[must_use]
578 pub fn with_krylov_dimension(mut self, dimension: usize) -> Self {
579 self.krylov_dimension = Some(dimension);
580 self
581 }
582
583 #[must_use]
585 pub fn with_tolerance(mut self, tolerance: f64) -> Self {
586 self.tolerance = tolerance;
587 self
588 }
589
590 #[must_use]
592 pub fn with_max_iterations(mut self, iterations: usize) -> Self {
593 self.max_iterations = iterations;
594 self
595 }
596
597 #[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#[derive(Clone, Debug)]
646pub struct FloquetSpectrum {
647 pub period: f64,
649 pub protocol_duration: f64,
651 pub unitary: Operator,
653 pub unitarity_residual: f64,
655 pub eigensystem: FloquetEigensystem,
657}
658
659#[derive(Clone, Debug)]
661pub struct FloquetAnalysis {
662 pub period: f64,
664 pub protocol_duration: f64,
666 pub unitary: Operator,
668 pub unitarity_residual: f64,
670 pub eigensystem: FloquetEigensystem,
672 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
760pub 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 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#[derive(Clone, Debug, PartialEq)]
906#[non_exhaustive]
907pub struct SpectrumOptions {
908 pub frequencies: Vec<f64>,
910 pub reference_energy: f64,
912 pub broadening: f64,
914 pub krylov_dimension: usize,
916 pub tolerance: f64,
918}
919
920impl SpectrumOptions {
921 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 #[must_use]
934 pub fn with_krylov_dimension(mut self, dimension: usize) -> Self {
935 self.krylov_dimension = dimension;
936 self
937 }
938
939 #[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
964pub 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
1022pub 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}