Skip to main content

qmbed/operator/
mod.rs

1use std::collections::HashMap;
2use std::marker::PhantomData;
3use std::sync::Arc;
4
5use num_complex::Complex64;
6use smallvec::SmallVec;
7
8pub use crate::backend::ShiftedLinearSolver;
9use crate::backend::factor_shifted_csc;
10use crate::basis::Basis;
11use crate::{QmbedError, Result};
12
13/// One typed local action in an operator product.
14///
15/// Compact character labels are parsed once into this representation before
16/// assembly; numerical kernels never dispatch on model or string names.
17#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
18pub enum LocalOperator {
19    Identity,
20    Number,
21    Z,
22    Raising,
23    Lowering,
24    X,
25    Y,
26    Custom(char),
27}
28
29impl LocalOperator {
30    /// Return the canonical one-character spelling used at basis boundaries.
31    pub const fn symbol(self) -> char {
32        match self {
33            Self::Identity => 'I',
34            Self::Number => 'n',
35            Self::Z => 'z',
36            Self::Raising => '+',
37            Self::Lowering => '-',
38            Self::X => 'x',
39            Self::Y => 'y',
40            Self::Custom(symbol) => symbol,
41        }
42    }
43}
44
45/// Typed ordered product of local actions.
46///
47/// `splits` identify factor boundaries. A single split retains the traditional
48/// spinful-fermion meaning; multiple (including repeated) boundaries describe
49/// arbitrary tensor products with empty factor actions.
50#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct OpProduct {
52    local: SmallVec<[LocalOperator; 8]>,
53    symbols: SmallVec<[char; 8]>,
54    split: Option<usize>,
55    splits: SmallVec<[usize; 4]>,
56    label: String,
57}
58
59impl OpProduct {
60    /// Parse QMBED's compact operator-label grammar.
61    ///
62    /// `|` records a tensor-factor boundary and every other character maps to
63    /// one built-in or custom local action.
64    pub fn parse(label: impl AsRef<str>) -> Result<Self> {
65        let mut splits = Vec::new();
66        let mut local = Vec::with_capacity(label.as_ref().chars().count());
67        for symbol in label.as_ref().chars() {
68            if symbol == '|' {
69                splits.push(local.len());
70                continue;
71            }
72            local.push(match symbol {
73                'I' => LocalOperator::Identity,
74                'n' => LocalOperator::Number,
75                'z' => LocalOperator::Z,
76                '+' => LocalOperator::Raising,
77                '-' => LocalOperator::Lowering,
78                'x' => LocalOperator::X,
79                'y' => LocalOperator::Y,
80                custom => LocalOperator::Custom(custom),
81            });
82        }
83        Self::with_splits(local, splits)
84    }
85
86    /// Construct a single-factor ordered product.
87    ///
88    /// The product must contain at least one local action. Sites are attached
89    /// later by [`Coupling`], allowing this parsed action to be reused.
90    pub fn new(local: impl IntoIterator<Item = LocalOperator>) -> Result<Self> {
91        Self::with_split(local, None)
92    }
93
94    /// Construct a two-species product and record the species boundary.
95    pub fn spinful(
96        up: impl IntoIterator<Item = LocalOperator>,
97        down: impl IntoIterator<Item = LocalOperator>,
98    ) -> Result<Self> {
99        let mut local: SmallVec<[LocalOperator; 8]> = up.into_iter().collect();
100        let split = local.len();
101        local.extend(down);
102        Self::with_split(local, Some(split))
103    }
104
105    /// Construct a product with zero or one explicit factor boundary.
106    pub fn with_split(
107        local: impl IntoIterator<Item = LocalOperator>,
108        split: Option<usize>,
109    ) -> Result<Self> {
110        Self::with_splits(local, split)
111    }
112
113    /// Construct an operator product with any number of ordered factor
114    /// boundaries. Duplicate boundaries preserve empty tensor factors such as
115    /// the middle factor in `x||z`.
116    pub fn with_splits(
117        local: impl IntoIterator<Item = LocalOperator>,
118        splits: impl IntoIterator<Item = usize>,
119    ) -> Result<Self> {
120        let local: SmallVec<[LocalOperator; 8]> = local.into_iter().collect();
121        if local.is_empty() {
122            return Err(QmbedError::InvalidOperator(
123                "operator products cannot be empty".into(),
124            ));
125        }
126        if local.contains(&LocalOperator::Custom('|')) {
127            return Err(QmbedError::InvalidOperator(
128                "the species separator is not a local operator".into(),
129            ));
130        }
131        let splits: SmallVec<[usize; 4]> = splits.into_iter().collect();
132        if splits.iter().any(|&boundary| boundary > local.len())
133            || splits.windows(2).any(|pair| pair[0] > pair[1])
134        {
135            return Err(QmbedError::InvalidOperator(
136                "operator factor boundaries must be ordered within the product arity".into(),
137            ));
138        }
139        let symbols: SmallVec<[char; 8]> = local.iter().map(|operator| operator.symbol()).collect();
140        let mut label = String::with_capacity(symbols.len() + splits.len());
141        let mut boundary = 0;
142        for position in 0..=symbols.len() {
143            while splits.get(boundary) == Some(&position) {
144                label.push('|');
145                boundary += 1;
146            }
147            if let Some(symbol) = symbols.get(position) {
148                label.push(*symbol);
149            }
150        }
151        let split = (splits.len() == 1).then(|| splits[0]);
152        Ok(Self {
153            local,
154            symbols,
155            split,
156            splits,
157            label,
158        })
159    }
160
161    /// Return the local actions in coupling-site order.
162    pub fn local_operators(&self) -> &[LocalOperator] {
163        &self.local
164    }
165
166    /// Return the single species boundary, when exactly one exists.
167    pub const fn split(&self) -> Option<usize> {
168        self.split
169    }
170
171    /// Return every ordered tensor-factor boundary.
172    pub fn splits(&self) -> &[usize] {
173        &self.splits
174    }
175
176    /// Return the canonical compact operator label.
177    pub fn label(&self) -> &str {
178        &self.label
179    }
180
181    pub(crate) fn symbols(&self) -> &[char] {
182        &self.symbols
183    }
184}
185
186/// One complex coefficient and its ordered zero-based sites.
187#[derive(Clone, Debug, PartialEq)]
188pub struct Coupling {
189    pub coefficient: Complex64,
190    pub sites: Vec<usize>,
191}
192
193impl Coupling {
194    /// Construct a coefficient with its ordered zero-based sites.
195    ///
196    /// Arity and site bounds are validated when the coupling is attached to a
197    /// product and assembled against a concrete basis.
198    pub fn new(coefficient: impl Into<Complex64>, sites: impl Into<Vec<usize>>) -> Self {
199        Self {
200            coefficient: coefficient.into(),
201            sites: sites.into(),
202        }
203    }
204}
205
206/// One typed local operator product and its spatial couplings.
207#[derive(Clone, Debug, PartialEq)]
208pub struct OperatorSpec {
209    product: OpProduct,
210    couplings: Vec<Coupling>,
211}
212
213impl OperatorSpec {
214    /// Parse a compact product label and attach its spatial couplings.
215    pub fn new(
216        label: impl AsRef<str>,
217        couplings: impl IntoIterator<Item = Coupling>,
218    ) -> Result<Self> {
219        Self::from_product(OpProduct::parse(label)?, couplings)
220    }
221
222    /// Attach spatial couplings to a parsed local operator product.
223    ///
224    /// Every coupling must have the same arity as `product`, and coefficients
225    /// must be finite.
226    pub fn from_product(
227        product: OpProduct,
228        couplings: impl IntoIterator<Item = Coupling>,
229    ) -> Result<Self> {
230        let arity = product.local_operators().len();
231        let couplings: Vec<_> = couplings.into_iter().collect();
232        for coupling in &couplings {
233            if coupling.sites.len() != arity {
234                return Err(QmbedError::InvalidCoupling(format!(
235                    "operator {:?} has arity {arity}, but a coupling has {} sites",
236                    product.label(),
237                    coupling.sites.len()
238                )));
239            }
240            if !coupling.coefficient.re.is_finite() || !coupling.coefficient.im.is_finite() {
241                return Err(QmbedError::InvalidCoupling(
242                    "coupling coefficients must be finite".into(),
243                ));
244            }
245        }
246        Ok(Self { product, couplings })
247    }
248
249    /// Return the canonical compact operator label.
250    pub fn operator(&self) -> &str {
251        self.product.label()
252    }
253
254    /// Return the typed, parsed local product.
255    pub const fn product(&self) -> &OpProduct {
256        &self.product
257    }
258
259    /// Return the spatial couplings in insertion order.
260    pub fn couplings(&self) -> &[Coupling] {
261        &self.couplings
262    }
263
264    /// Return the same local action after a bijective site relabeling.
265    ///
266    /// `permutation[old_site]` is the site used by the returned term. This
267    /// keeps frontend indexing conventions and lattice embeddings outside the
268    /// universal assembler.
269    pub fn with_site_permutation(&self, permutation: &[usize]) -> Result<Self> {
270        if permutation.iter().any(|&site| site >= permutation.len()) {
271            return Err(QmbedError::InvalidSite {
272                site: permutation
273                    .iter()
274                    .copied()
275                    .find(|&site| site >= permutation.len())
276                    .unwrap_or(permutation.len()),
277                sites: permutation.len(),
278            });
279        }
280        let unique = permutation
281            .iter()
282            .copied()
283            .collect::<std::collections::HashSet<_>>();
284        if unique.len() != permutation.len() {
285            return Err(QmbedError::InvalidOptions(
286                "site permutation must be bijective".into(),
287            ));
288        }
289        let couplings = self
290            .couplings
291            .iter()
292            .map(|coupling| {
293                let sites = coupling
294                    .sites
295                    .iter()
296                    .map(|&site| {
297                        permutation
298                            .get(site)
299                            .copied()
300                            .ok_or(QmbedError::InvalidSite {
301                                site,
302                                sites: permutation.len(),
303                            })
304                    })
305                    .collect::<Result<Vec<_>>>()?;
306                Ok(Coupling::new(coupling.coefficient, sites))
307            })
308            .collect::<Result<Vec<_>>>()?;
309        Self::from_product(self.product.clone(), couplings)
310    }
311
312    pub(crate) fn symbols(&self) -> &[char] {
313        self.product.symbols()
314    }
315
316    pub(crate) const fn split(&self) -> Option<usize> {
317        self.product.split()
318    }
319}
320
321/// Requested materialization backend.
322#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
323#[non_exhaustive]
324pub enum MatrixFormat {
325    /// Row-major dense storage.
326    Dense,
327    /// Compressed sparse columns, preferred for many Hermitian eigensolvers.
328    Csc,
329    /// Compressed sparse rows.
330    Csr,
331    /// Diagonal-offset storage.
332    Dia,
333    /// Delayed action without exposing one public sparse representation.
334    MatrixFree,
335}
336
337/// Rectangular-capable narrow waist shared by stored and matrix-free maps.
338pub trait LinearOperator: Send + Sync {
339    /// Return `(rows, columns)`.
340    fn shape(&self) -> (usize, usize);
341    /// Report the public materialization format.
342    fn format(&self) -> MatrixFormat;
343    /// Compute `output = A * input`.
344    ///
345    /// Implementations validate both vector lengths and overwrite every output
346    /// element.
347    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()>;
348
349    /// Whether this operator preserves real vectors exactly.
350    ///
351    /// Eigensolvers use this capability to retain the public complex-valued
352    /// interface while avoiding complex arithmetic for real Hamiltonians.
353    fn is_real(&self) -> bool {
354        false
355    }
356
357    /// Apply this operator to a real vector.
358    ///
359    /// Custom operators can opt into the real fast path by overriding
360    /// [`LinearOperator::is_real`] and this method. The compatibility fallback
361    /// remains correct, but stored operators provide an allocation-free
362    /// implementation below.
363    fn apply_real(&self, input: &[f64], output: &mut [f64]) -> Result<()> {
364        let shape = self.shape();
365        if input.len() != shape.1 || output.len() != shape.0 {
366            return Err(QmbedError::DimensionMismatch(format!(
367                "shape {shape:?} requires real input length {} and output length {}, got {} and {}",
368                shape.1,
369                shape.0,
370                input.len(),
371                output.len()
372            )));
373        }
374        let complex_input: Vec<_> = input
375            .iter()
376            .map(|&value| Complex64::new(value, 0.0))
377            .collect();
378        let mut complex_output = vec![Complex64::new(0.0, 0.0); shape.0];
379        self.apply(&complex_input, &mut complex_output)?;
380        for (real, complex) in output.iter_mut().zip(complex_output) {
381            if complex.im.abs() > 1.0e-12 {
382                return Err(QmbedError::UnsupportedBackend(
383                    "operator declared a real action but produced an imaginary component".into(),
384                ));
385            }
386            *real = complex.re;
387        }
388        Ok(())
389    }
390
391    /// Apply the algebraic transpose without conjugating either operand.
392    ///
393    /// Stored operators use their canonical triplets directly. A genuinely
394    /// matrix-free implementation may override this method; the default falls
395    /// back to column actions without ever retaining a full square matrix.
396    fn apply_transpose(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
397        let (rows, columns) = self.shape();
398        if input.len() != rows || output.len() != columns {
399            return Err(QmbedError::DimensionMismatch(format!(
400                "transpose of shape ({rows}, {columns}) requires input length {rows} and output length {columns}"
401            )));
402        }
403        output.fill(Complex64::new(0.0, 0.0));
404        if let Some(entries) = self.stored_triplets()? {
405            for (row, column, value) in entries {
406                output[column] += value * input[row];
407            }
408            return Ok(());
409        }
410
411        let mut basis_vector = vec![Complex64::new(0.0, 0.0); columns];
412        let mut column_values = vec![Complex64::new(0.0, 0.0); rows];
413        for column in 0..columns {
414            basis_vector.fill(Complex64::new(0.0, 0.0));
415            basis_vector[column] = Complex64::new(1.0, 0.0);
416            self.apply(&basis_vector, &mut column_values)?;
417            output[column] = column_values
418                .iter()
419                .zip(input.iter())
420                .map(|(value, input_value)| *value * *input_value)
421                .sum();
422        }
423        Ok(())
424    }
425
426    /// Apply the conjugate transpose.
427    fn apply_adjoint(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
428        let conjugated_input: Vec<_> = input.iter().map(|value| value.conj()).collect();
429        self.apply_transpose(&conjugated_input, output)?;
430        for value in output {
431            *value = value.conj();
432        }
433        Ok(())
434    }
435
436    /// Return canonical stored nonzeros when the representation already owns them.
437    fn stored_triplets(&self) -> Result<Option<Vec<(usize, usize, Complex64)>>> {
438        Ok(None)
439    }
440
441    /// Prepare a reusable solver for `(A - shift I) x = b`, when supported.
442    fn shifted_solver(&self, _shift: f64) -> Result<Option<Box<dyn ShiftedLinearSolver>>> {
443        Ok(None)
444    }
445}
446
447/// Narrow waist for operators whose action depends explicitly on time.
448pub trait TimeDependentOperator: Send + Sync {
449    fn shape(&self) -> (usize, usize);
450    fn apply_at(&self, time: f64, input: &[Complex64], output: &mut [Complex64]) -> Result<()>;
451}
452
453type TimedApply = Arc<dyn Fn(f64, &[Complex64], &mut [Complex64]) -> Result<()> + Send + Sync>;
454
455/// Composable explicitly time-dependent linear map.
456#[derive(Clone)]
457pub struct TimeOperator {
458    shape: (usize, usize),
459    action: TimedApply,
460}
461
462impl std::fmt::Debug for TimeOperator {
463    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464        formatter
465            .debug_struct("TimeOperator")
466            .field("shape", &self.shape)
467            .finish_non_exhaustive()
468    }
469}
470
471impl TimeOperator {
472    pub fn new<F>(shape: (usize, usize), action: F) -> Result<Self>
473    where
474        F: Fn(f64, &[Complex64], &mut [Complex64]) -> Result<()> + Send + Sync + 'static,
475    {
476        if shape.0 == 0 || shape.1 == 0 {
477            return Err(QmbedError::DimensionMismatch(
478                "time-dependent operator dimensions must be positive".into(),
479            ));
480        }
481        Ok(Self {
482            shape,
483            action: Arc::new(action),
484        })
485    }
486
487    pub fn from_operator<O>(operator: Arc<O>) -> Self
488    where
489        O: TimeDependentOperator + 'static,
490    {
491        let shape = operator.shape();
492        Self {
493            shape,
494            action: Arc::new(move |time, input, output| operator.apply_at(time, input, output)),
495        }
496    }
497
498    pub fn evaluate(&self, time: f64, format: MatrixFormat) -> Result<Operator> {
499        if !time.is_finite() {
500            return Err(QmbedError::InvalidOptions(
501                "evaluation time must be finite".into(),
502            ));
503        }
504        let mut input = vec![Complex64::new(0.0, 0.0); self.shape.1];
505        let mut output = vec![Complex64::new(0.0, 0.0); self.shape.0];
506        let mut triplets = Vec::new();
507        for column in 0..self.shape.1 {
508            input.fill(Complex64::new(0.0, 0.0));
509            input[column] = Complex64::new(1.0, 0.0);
510            self.apply_at(time, &input, &mut output)?;
511            for (row, value) in output.iter().copied().enumerate() {
512                if value.norm() > f64::EPSILON {
513                    triplets.push((row, column, value));
514                }
515            }
516        }
517        Operator::from_triplets(self.shape.0, self.shape.1, triplets, format)
518    }
519
520    pub fn scaled(&self, coefficient: impl Into<Complex64>) -> Result<Self> {
521        let coefficient = coefficient.into();
522        if !coefficient.re.is_finite() || !coefficient.im.is_finite() {
523            return Err(QmbedError::InvalidOptions(
524                "time-operator scale must be finite".into(),
525            ));
526        }
527        let operator = self.clone();
528        Self::new(self.shape, move |time, input, output| {
529            operator.apply_at(time, input, output)?;
530            for value in output {
531                *value *= coefficient;
532            }
533            Ok(())
534        })
535    }
536
537    pub fn add(&self, right: &Self) -> Result<Self> {
538        if self.shape != right.shape {
539            return Err(QmbedError::DimensionMismatch(
540                "time-dependent sums require equal shapes".into(),
541            ));
542        }
543        let left = self.clone();
544        let right = right.clone();
545        Self::new(self.shape, move |time, input, output| {
546            left.apply_at(time, input, output)?;
547            let mut contribution = vec![Complex64::new(0.0, 0.0); output.len()];
548            right.apply_at(time, input, &mut contribution)?;
549            for (value, addition) in output.iter_mut().zip(contribution) {
550                *value += addition;
551            }
552            Ok(())
553        })
554    }
555
556    pub fn subtract(&self, right: &Self) -> Result<Self> {
557        self.add(&right.scaled(-1.0)?)
558    }
559
560    pub fn product(&self, right: &Self) -> Result<Self> {
561        if self.shape.1 != right.shape.0 {
562            return Err(QmbedError::DimensionMismatch(
563                "time-dependent product inner dimensions do not match".into(),
564            ));
565        }
566        let left = self.clone();
567        let right = right.clone();
568        Self::new((self.shape.0, right.shape.1), move |time, input, output| {
569            let mut intermediate = vec![Complex64::new(0.0, 0.0); right.shape.0];
570            right.apply_at(time, input, &mut intermediate)?;
571            left.apply_at(time, &intermediate, output)
572        })
573    }
574
575    pub fn commutator(&self, right: &Self) -> Result<Self> {
576        self.product(right)?.subtract(&right.product(self)?)
577    }
578
579    pub fn anticommutator(&self, right: &Self) -> Result<Self> {
580        self.product(right)?.add(&right.product(self)?)
581    }
582
583    pub fn pow(&self, exponent: u32) -> Result<Self> {
584        if self.shape.0 != self.shape.1 {
585            return Err(QmbedError::DimensionMismatch(
586                "time-dependent powers require a square operator".into(),
587            ));
588        }
589        if exponent == 0 {
590            let dimension = self.shape.0;
591            return Self::new(self.shape, move |_time, input, output| {
592                check_apply_shape((dimension, dimension), input, output)?;
593                output.copy_from_slice(input);
594                Ok(())
595            });
596        }
597        let mut result = self.clone();
598        for _ in 1..exponent {
599            result = result.product(self)?;
600        }
601        Ok(result)
602    }
603
604    pub fn rotated(&self, unitary: &Operator, tolerance: f64) -> Result<Self> {
605        if self.shape.0 != self.shape.1 || unitary.shape() != self.shape {
606            return Err(QmbedError::DimensionMismatch(
607                "time-dependent rotation needs equal square shapes".into(),
608            ));
609        }
610        let adjoint = unitary.adjoint()?;
611        let identity = adjoint.product(unitary)?;
612        for row in 0..self.shape.0 {
613            for column in 0..self.shape.1 {
614                let expected = if row == column { 1.0 } else { 0.0 };
615                if (identity.value_at(row, column) - expected).norm() > tolerance {
616                    return Err(QmbedError::InvalidOptions(
617                        "rotation matrix must be unitary".into(),
618                    ));
619                }
620            }
621        }
622        let operator = self.clone();
623        let unitary = unitary.clone();
624        Self::new(self.shape, move |time, input, output| {
625            let mut rotated_input = vec![Complex64::new(0.0, 0.0); input.len()];
626            let mut applied = vec![Complex64::new(0.0, 0.0); output.len()];
627            unitary.apply(input, &mut rotated_input)?;
628            operator.apply_at(time, &rotated_input, &mut applied)?;
629            adjoint.apply(&applied, output)
630        })
631    }
632}
633
634impl TimeDependentOperator for TimeOperator {
635    fn shape(&self) -> (usize, usize) {
636        self.shape
637    }
638
639    fn apply_at(&self, time: f64, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
640        if !time.is_finite() {
641            return Err(QmbedError::InvalidOptions(
642                "time-dependent operator time must be finite".into(),
643            ));
644        }
645        check_apply_shape(self.shape, input, output)?;
646        (self.action)(time, input, output)
647    }
648}
649
650#[derive(Clone, Copy, Debug, Eq, PartialEq)]
651pub struct AssemblyChecks {
652    pub hermiticity: bool,
653    pub particle_conservation: bool,
654    pub symmetry_compatibility: bool,
655}
656
657impl AssemblyChecks {
658    pub const fn all() -> Self {
659        Self {
660            hermiticity: true,
661            particle_conservation: true,
662            symmetry_compatibility: true,
663        }
664    }
665
666    pub const fn rectangular() -> Self {
667        Self {
668            hermiticity: false,
669            particle_conservation: false,
670            symmetry_compatibility: true,
671        }
672    }
673
674    /// Disable Hamiltonian-specific validation for a general linear action.
675    ///
676    /// Individual local operators, cross-sector probes, and matrix-free
677    /// actions are not necessarily Hermitian or particle conserving.
678    pub const fn none() -> Self {
679        Self {
680            hermiticity: false,
681            particle_conservation: false,
682            symmetry_compatibility: false,
683        }
684    }
685}
686
687impl Default for AssemblyChecks {
688    fn default() -> Self {
689        Self::all()
690    }
691}
692
693#[derive(Clone, Debug)]
694struct Triplet {
695    row: usize,
696    column: usize,
697    value: Complex64,
698}
699
700#[derive(Clone, Debug)]
701enum Storage {
702    Dense(Vec<Complex64>),
703    Csc {
704        column_offsets: Vec<usize>,
705        row_indices: Vec<usize>,
706        values: Vec<Complex64>,
707    },
708    Csr {
709        row_offsets: Vec<usize>,
710        column_indices: Vec<usize>,
711        values: Vec<Complex64>,
712    },
713    Dia {
714        offsets: Vec<isize>,
715        values_by_row: Vec<Complex64>,
716    },
717    MatrixFree(Vec<Triplet>),
718}
719
720/// Concrete operator returned by universal assembly.
721#[derive(Clone, Debug)]
722pub struct Operator {
723    shape: (usize, usize),
724    format: MatrixFormat,
725    storage: Storage,
726    real: bool,
727}
728
729impl Operator {
730    /// Construct a row-major dense rectangular operator.
731    pub fn from_dense(
732        rows: usize,
733        columns: usize,
734        values_row_major: Vec<Complex64>,
735    ) -> Result<Self> {
736        if values_row_major.len() != rows.saturating_mul(columns) {
737            return Err(QmbedError::DimensionMismatch(format!(
738                "dense storage has {} entries for shape ({rows}, {columns})",
739                values_row_major.len()
740            )));
741        }
742        let storage = Storage::Dense(values_row_major);
743        Ok(Self {
744            shape: (rows, columns),
745            format: MatrixFormat::Dense,
746            real: storage_is_real(&storage),
747            storage,
748        })
749    }
750
751    /// Construct an operator from zero-based `(row, column, value)` entries.
752    ///
753    /// Duplicate coordinates are summed, exact numerical zeros are discarded,
754    /// and the result is canonicalized for `format`.
755    pub fn from_triplets(
756        rows: usize,
757        columns: usize,
758        triplets: impl IntoIterator<Item = (usize, usize, Complex64)>,
759        format: MatrixFormat,
760    ) -> Result<Self> {
761        if format == MatrixFormat::Dense {
762            let mut values = vec![Complex64::new(0.0, 0.0); rows.saturating_mul(columns)];
763            for (row, column, value) in triplets {
764                if row >= rows
765                    || column >= columns
766                    || !value.re.is_finite()
767                    || !value.im.is_finite()
768                {
769                    return Err(QmbedError::InvalidCoupling(
770                        "triplet index is out of bounds or its value is non-finite".into(),
771                    ));
772                }
773                values[row * columns + column] += value;
774            }
775            return Self::from_dense(rows, columns, values);
776        }
777        let mut accumulated = HashMap::new();
778        for (row, column, value) in triplets {
779            if row >= rows || column >= columns || !value.re.is_finite() || !value.im.is_finite() {
780                return Err(QmbedError::InvalidCoupling(
781                    "triplet index is out of bounds or its value is non-finite".into(),
782                ));
783            }
784            *accumulated
785                .entry((row, column))
786                .or_insert(Complex64::new(0.0, 0.0)) += value;
787        }
788        let mut entries: Vec<_> = accumulated
789            .into_iter()
790            .filter_map(|((row, column), value)| {
791                (value.norm() > f64::EPSILON).then_some(Triplet { row, column, value })
792            })
793            .collect();
794        match format {
795            MatrixFormat::Csc => entries.sort_by_key(|entry| (entry.column, entry.row)),
796            MatrixFormat::Csr => entries.sort_by_key(|entry| (entry.row, entry.column)),
797            MatrixFormat::Dia | MatrixFormat::MatrixFree => {
798                entries.sort_by_key(|entry| (entry.row, entry.column));
799            }
800            MatrixFormat::Dense => unreachable!(),
801        }
802        let shape = (rows, columns);
803        let storage = match format {
804            MatrixFormat::Csc => csc_storage(&entries, columns),
805            MatrixFormat::Csr => csr_storage(&entries, rows),
806            MatrixFormat::Dia => dia_storage(&entries, rows),
807            MatrixFormat::MatrixFree => Storage::MatrixFree(entries),
808            MatrixFormat::Dense => unreachable!(),
809        };
810        Ok(Self {
811            shape,
812            format,
813            real: storage_is_real(&storage),
814            storage,
815        })
816    }
817
818    /// Materialize a row-major dense copy of this operator.
819    pub fn to_dense(&self) -> Vec<Complex64> {
820        if let Storage::Dense(values) = &self.storage {
821            return values.clone();
822        }
823        let mut dense = vec![Complex64::new(0.0, 0.0); self.shape.0 * self.shape.1];
824        match &self.storage {
825            Storage::Dense(_) => unreachable!(),
826            Storage::Csc {
827                column_offsets,
828                row_indices,
829                values,
830            } => {
831                for column in 0..self.shape.1 {
832                    for position in column_offsets[column]..column_offsets[column + 1] {
833                        dense[row_indices[position] * self.shape.1 + column] = values[position];
834                    }
835                }
836            }
837            Storage::Csr {
838                row_offsets,
839                column_indices,
840                values,
841            } => {
842                for row in 0..self.shape.0 {
843                    for position in row_offsets[row]..row_offsets[row + 1] {
844                        dense[row * self.shape.1 + column_indices[position]] = values[position];
845                    }
846                }
847            }
848            Storage::Dia {
849                offsets,
850                values_by_row,
851            } => {
852                for (diagonal, &offset) in offsets.iter().enumerate() {
853                    for row in 0..self.shape.0 {
854                        let Some(column) = row.checked_add_signed(-offset) else {
855                            continue;
856                        };
857                        if column < self.shape.1 {
858                            dense[row * self.shape.1 + column] =
859                                values_by_row[diagonal * self.shape.0 + row];
860                        }
861                    }
862                }
863            }
864            Storage::MatrixFree(entries) => {
865                for entry in entries {
866                    dense[entry.row * self.shape.1 + entry.column] = entry.value;
867                }
868            }
869        }
870        dense
871    }
872
873    /// Return an elementwise complex-conjugated operator.
874    pub fn conjugated(&self) -> Result<Self> {
875        Self::from_triplets(
876            self.shape.0,
877            self.shape.1,
878            self.triplets()
879                .into_iter()
880                .map(|(row, column, value)| (row, column, value.conj())),
881            self.format,
882        )
883    }
884
885    /// Row-vector action `inputáµ€ A` without conjugating the input.
886    pub fn right_apply(&self, input: &[Complex64]) -> Result<Vec<Complex64>> {
887        if input.len() != self.shape.0 {
888            return Err(QmbedError::DimensionMismatch(
889                "right-apply input must match the operator row count".into(),
890            ));
891        }
892        let mut output = vec![Complex64::new(0.0, 0.0); self.shape.1];
893        self.apply_transpose(input, &mut output)?;
894        Ok(output)
895    }
896
897    /// Estimate bytes owned by the current materialized representation.
898    pub fn memory_bytes(&self) -> usize {
899        let complex = std::mem::size_of::<Complex64>();
900        let index = std::mem::size_of::<usize>();
901        match &self.storage {
902            Storage::Dense(values) => values.len() * complex,
903            Storage::Csc {
904                column_offsets,
905                row_indices,
906                values,
907            } => column_offsets.len() * index + row_indices.len() * index + values.len() * complex,
908            Storage::Csr {
909                row_offsets,
910                column_indices,
911                values,
912            } => row_offsets.len() * index + column_indices.len() * index + values.len() * complex,
913            Storage::Dia {
914                offsets,
915                values_by_row,
916            } => offsets.len() * std::mem::size_of::<isize>() + values_by_row.len() * complex,
917            Storage::MatrixFree(entries) => entries.len() * std::mem::size_of::<Triplet>(),
918        }
919    }
920
921    /// Count stored mathematical nonzeros.
922    pub fn nnz(&self) -> usize {
923        match &self.storage {
924            Storage::Dense(values) => values
925                .iter()
926                .filter(|value| value.norm() > f64::EPSILON)
927                .count(),
928            Storage::Csc { values, .. } | Storage::Csr { values, .. } => values.len(),
929            Storage::Dia { values_by_row, .. } => values_by_row
930                .iter()
931                .filter(|value| value.norm() > f64::EPSILON)
932                .count(),
933            Storage::MatrixFree(entries) => entries.len(),
934        }
935    }
936
937    /// Canonical nonzero `(row, column, value)` entries without dense materialization.
938    pub fn triplets(&self) -> Vec<(usize, usize, Complex64)> {
939        let mut entries = match &self.storage {
940            Storage::Dense(values) => values
941                .iter()
942                .enumerate()
943                .filter_map(|(index, value)| {
944                    (value.norm() > f64::EPSILON).then_some((
945                        index / self.shape.1,
946                        index % self.shape.1,
947                        *value,
948                    ))
949                })
950                .collect(),
951            Storage::Csc {
952                column_offsets,
953                row_indices,
954                values,
955            } => {
956                let mut entries = Vec::with_capacity(values.len());
957                for column in 0..self.shape.1 {
958                    for position in column_offsets[column]..column_offsets[column + 1] {
959                        entries.push((row_indices[position], column, values[position]));
960                    }
961                }
962                entries
963            }
964            Storage::Csr {
965                row_offsets,
966                column_indices,
967                values,
968            } => {
969                let mut entries = Vec::with_capacity(values.len());
970                for row in 0..self.shape.0 {
971                    for position in row_offsets[row]..row_offsets[row + 1] {
972                        entries.push((row, column_indices[position], values[position]));
973                    }
974                }
975                entries
976            }
977            Storage::Dia {
978                offsets,
979                values_by_row,
980            } => {
981                let mut entries = Vec::new();
982                for (diagonal, &offset) in offsets.iter().enumerate() {
983                    for row in 0..self.shape.0 {
984                        let Some(column) = row.checked_add_signed(-offset) else {
985                            continue;
986                        };
987                        if column < self.shape.1 {
988                            let value = values_by_row[diagonal * self.shape.0 + row];
989                            if value.norm() > f64::EPSILON {
990                                entries.push((row, column, value));
991                            }
992                        }
993                    }
994                }
995                entries
996            }
997            Storage::MatrixFree(entries) => entries
998                .iter()
999                .map(|entry| (entry.row, entry.column, entry.value))
1000                .collect(),
1001        };
1002        entries.sort_by_key(|(row, column, _)| (*row, *column));
1003        entries
1004    }
1005
1006    /// Convert the operator without changing its numerical values.
1007    pub fn converted(&self, format: MatrixFormat) -> Result<Self> {
1008        if format == self.format {
1009            return Ok(self.clone());
1010        }
1011        Self::from_triplets(self.shape.0, self.shape.1, self.triplets(), format)
1012    }
1013
1014    pub fn diagonal(&self) -> Vec<Complex64> {
1015        let dimension = self.shape.0.min(self.shape.1);
1016        (0..dimension)
1017            .map(|index| self.value_at(index, index))
1018            .collect()
1019    }
1020
1021    pub fn trace(&self) -> Result<Complex64> {
1022        if self.shape.0 != self.shape.1 {
1023            return Err(QmbedError::DimensionMismatch(
1024                "operator trace requires a square operator".into(),
1025            ));
1026        }
1027        Ok(self.diagonal().into_iter().sum())
1028    }
1029
1030    pub fn scaled(&self, coefficient: impl Into<Complex64>) -> Result<Self> {
1031        let coefficient = coefficient.into();
1032        if !coefficient.re.is_finite() || !coefficient.im.is_finite() {
1033            return Err(QmbedError::InvalidOptions(
1034                "operator scale must be finite".into(),
1035            ));
1036        }
1037        Self::from_triplets(
1038            self.shape.0,
1039            self.shape.1,
1040            self.triplets()
1041                .into_iter()
1042                .map(|(row, column, value)| (row, column, coefficient * value)),
1043            self.format,
1044        )
1045    }
1046
1047    pub fn add(&self, right: &Self) -> Result<Self> {
1048        self.combine(right, Complex64::new(1.0, 0.0))
1049    }
1050
1051    pub fn subtract(&self, right: &Self) -> Result<Self> {
1052        self.combine(right, Complex64::new(-1.0, 0.0))
1053    }
1054
1055    fn combine(&self, right: &Self, right_scale: Complex64) -> Result<Self> {
1056        if self.shape != right.shape {
1057            return Err(QmbedError::DimensionMismatch(
1058                "operator addition requires equal shapes".into(),
1059            ));
1060        }
1061        let entries = self.triplets().into_iter().chain(
1062            right
1063                .triplets()
1064                .into_iter()
1065                .map(|(row, column, value)| (row, column, right_scale * value)),
1066        );
1067        Self::from_triplets(self.shape.0, self.shape.1, entries, self.format)
1068    }
1069
1070    /// Matrix product `self * right`.
1071    pub fn product(&self, right: &Self) -> Result<Self> {
1072        if self.shape.1 != right.shape.0 {
1073            return Err(QmbedError::DimensionMismatch(
1074                "operator product has incompatible inner dimensions".into(),
1075            ));
1076        }
1077        let mut right_by_row = vec![Vec::new(); right.shape.0];
1078        for (row, column, value) in right.triplets() {
1079            right_by_row[row].push((column, value));
1080        }
1081        let mut accumulated = HashMap::new();
1082        for (row, middle, left_value) in self.triplets() {
1083            for &(column, right_value) in &right_by_row[middle] {
1084                *accumulated
1085                    .entry((row, column))
1086                    .or_insert(Complex64::new(0.0, 0.0)) += left_value * right_value;
1087            }
1088        }
1089        Self::from_triplets(
1090            self.shape.0,
1091            right.shape.1,
1092            accumulated
1093                .into_iter()
1094                .map(|((row, column), value)| (row, column, value)),
1095            self.format,
1096        )
1097    }
1098
1099    /// Project a square operator into the column space of `projector`.
1100    ///
1101    /// For a projector-like rectangular matrix `P` with shape `(N, M)`, this
1102    /// returns `P† self P` with shape `(M, M)`. The operation intentionally
1103    /// accepts any rectangular linear map; callers that require an isometry
1104    /// may validate `P†P = I` separately.
1105    pub fn projected_by(&self, projector: &Self) -> Result<Self> {
1106        if self.shape.0 != self.shape.1 || projector.shape.0 != self.shape.0 {
1107            return Err(QmbedError::DimensionMismatch(
1108                "operator projection requires a square source and matching projector rows".into(),
1109            ));
1110        }
1111        let applied = self.product(projector)?;
1112        projector.adjoint()?.product(&applied)
1113    }
1114
1115    pub fn pow(&self, exponent: u32) -> Result<Self> {
1116        if self.shape.0 != self.shape.1 {
1117            return Err(QmbedError::DimensionMismatch(
1118                "operator powers require a square operator".into(),
1119            ));
1120        }
1121        let dimension = self.shape.0;
1122        let mut result = Self::from_triplets(
1123            dimension,
1124            dimension,
1125            (0..dimension).map(|index| (index, index, Complex64::new(1.0, 0.0))),
1126            self.format,
1127        )?;
1128        let mut base = self.clone();
1129        let mut remaining = exponent;
1130        while remaining > 0 {
1131            if remaining & 1 == 1 {
1132                result = result.product(&base)?;
1133            }
1134            remaining >>= 1;
1135            if remaining > 0 {
1136                base = base.product(&base)?;
1137            }
1138        }
1139        result.converted(self.format)
1140    }
1141
1142    pub fn adjoint(&self) -> Result<Self> {
1143        Self::from_triplets(
1144            self.shape.1,
1145            self.shape.0,
1146            self.triplets()
1147                .into_iter()
1148                .map(|(row, column, value)| (column, row, value.conj())),
1149            self.format,
1150        )
1151    }
1152
1153    pub fn transpose(&self) -> Result<Self> {
1154        Self::from_triplets(
1155            self.shape.1,
1156            self.shape.0,
1157            self.triplets()
1158                .into_iter()
1159                .map(|(row, column, value)| (column, row, value)),
1160            self.format,
1161        )
1162    }
1163
1164    /// Similarity rotation `U† A U` after validating unitarity.
1165    pub fn rotated(&self, unitary: &Self, tolerance: f64) -> Result<Self> {
1166        if self.shape.0 != self.shape.1 || unitary.shape != self.shape {
1167            return Err(QmbedError::DimensionMismatch(
1168                "operator rotation requires equal square shapes".into(),
1169            ));
1170        }
1171        if !tolerance.is_finite() || tolerance <= 0.0 {
1172            return Err(QmbedError::InvalidOptions(
1173                "unitarity tolerance must be positive".into(),
1174            ));
1175        }
1176        let identity = unitary.adjoint()?.product(unitary)?;
1177        for row in 0..self.shape.0 {
1178            for column in 0..self.shape.1 {
1179                let expected = if row == column { 1.0 } else { 0.0 };
1180                if (identity.value_at(row, column) - expected).norm() > tolerance {
1181                    return Err(QmbedError::InvalidOptions(
1182                        "rotation matrix must be unitary".into(),
1183                    ));
1184                }
1185            }
1186        }
1187        unitary.adjoint()?.product(self)?.product(unitary)
1188    }
1189
1190    fn value_at(&self, row: usize, column: usize) -> Complex64 {
1191        match &self.storage {
1192            Storage::Dense(values) => values[row * self.shape.1 + column],
1193            Storage::Csc {
1194                column_offsets,
1195                row_indices,
1196                values,
1197            } => {
1198                let range = column_offsets[column]..column_offsets[column + 1];
1199                row_indices[range.clone()]
1200                    .binary_search(&row)
1201                    .map_or(Complex64::new(0.0, 0.0), |position| {
1202                        values[range.start + position]
1203                    })
1204            }
1205            Storage::Csr {
1206                row_offsets,
1207                column_indices,
1208                values,
1209            } => {
1210                let range = row_offsets[row]..row_offsets[row + 1];
1211                column_indices[range.clone()]
1212                    .binary_search(&column)
1213                    .map_or(Complex64::new(0.0, 0.0), |position| {
1214                        values[range.start + position]
1215                    })
1216            }
1217            Storage::Dia {
1218                offsets,
1219                values_by_row,
1220            } => {
1221                let offset = row as isize - column as isize;
1222                offsets
1223                    .binary_search(&offset)
1224                    .map_or(Complex64::new(0.0, 0.0), |diagonal| {
1225                        values_by_row[diagonal * self.shape.0 + row]
1226                    })
1227            }
1228            Storage::MatrixFree(entries) => entries
1229                .binary_search_by_key(&(row, column), |entry| (entry.row, entry.column))
1230                .map_or(Complex64::new(0.0, 0.0), |position| entries[position].value),
1231        }
1232    }
1233
1234    fn entry_is_hermitian(
1235        &self,
1236        row: usize,
1237        column: usize,
1238        value: Complex64,
1239        tolerance: f64,
1240    ) -> bool {
1241        (value - self.value_at(column, row).conj()).norm() <= tolerance
1242    }
1243
1244    fn apply_real_coefficients(&self, input: &[Complex64], output: &mut [Complex64]) {
1245        match &self.storage {
1246            Storage::Dense(values) => {
1247                for row in 0..self.shape.0 {
1248                    let target = &mut output[row];
1249                    for column in 0..self.shape.1 {
1250                        let coefficient = values[row * self.shape.1 + column].re;
1251                        target.re += coefficient * input[column].re;
1252                        target.im += coefficient * input[column].im;
1253                    }
1254                }
1255            }
1256            Storage::Csc {
1257                column_offsets,
1258                row_indices,
1259                values,
1260            } => {
1261                for column in 0..self.shape.1 {
1262                    let input_value = input[column];
1263                    for position in column_offsets[column]..column_offsets[column + 1] {
1264                        let target = &mut output[row_indices[position]];
1265                        let coefficient = values[position].re;
1266                        target.re += coefficient * input_value.re;
1267                        target.im += coefficient * input_value.im;
1268                    }
1269                }
1270            }
1271            Storage::Csr {
1272                row_offsets,
1273                column_indices,
1274                values,
1275            } => {
1276                for row in 0..self.shape.0 {
1277                    let target = &mut output[row];
1278                    for position in row_offsets[row]..row_offsets[row + 1] {
1279                        let input_value = input[column_indices[position]];
1280                        let coefficient = values[position].re;
1281                        target.re += coefficient * input_value.re;
1282                        target.im += coefficient * input_value.im;
1283                    }
1284                }
1285            }
1286            Storage::Dia {
1287                offsets,
1288                values_by_row,
1289            } => {
1290                for (diagonal, &offset) in offsets.iter().enumerate() {
1291                    for row in 0..self.shape.0 {
1292                        let Some(column) = row.checked_add_signed(-offset) else {
1293                            continue;
1294                        };
1295                        if column < self.shape.1 {
1296                            let input_value = input[column];
1297                            let coefficient = values_by_row[diagonal * self.shape.0 + row].re;
1298                            output[row].re += coefficient * input_value.re;
1299                            output[row].im += coefficient * input_value.im;
1300                        }
1301                    }
1302                }
1303            }
1304            Storage::MatrixFree(entries) => {
1305                for entry in entries {
1306                    output[entry.row].re += entry.value.re * input[entry.column].re;
1307                    output[entry.row].im += entry.value.re * input[entry.column].im;
1308                }
1309            }
1310        }
1311    }
1312
1313    pub fn is_hermitian(&self, tolerance: f64) -> bool {
1314        if self.shape.0 != self.shape.1 {
1315            return false;
1316        }
1317        match &self.storage {
1318            Storage::Dense(values) => {
1319                for row in 0..self.shape.0 {
1320                    for column in 0..self.shape.1 {
1321                        if !self.entry_is_hermitian(
1322                            row,
1323                            column,
1324                            values[row * self.shape.1 + column],
1325                            tolerance,
1326                        ) {
1327                            return false;
1328                        }
1329                    }
1330                }
1331            }
1332            Storage::Csc {
1333                column_offsets,
1334                row_indices,
1335                values,
1336            } => {
1337                for column in 0..self.shape.1 {
1338                    for position in column_offsets[column]..column_offsets[column + 1] {
1339                        if !self.entry_is_hermitian(
1340                            row_indices[position],
1341                            column,
1342                            values[position],
1343                            tolerance,
1344                        ) {
1345                            return false;
1346                        }
1347                    }
1348                }
1349            }
1350            Storage::Csr {
1351                row_offsets,
1352                column_indices,
1353                values,
1354            } => {
1355                for row in 0..self.shape.0 {
1356                    for position in row_offsets[row]..row_offsets[row + 1] {
1357                        if !self.entry_is_hermitian(
1358                            row,
1359                            column_indices[position],
1360                            values[position],
1361                            tolerance,
1362                        ) {
1363                            return false;
1364                        }
1365                    }
1366                }
1367            }
1368            Storage::Dia {
1369                offsets,
1370                values_by_row,
1371            } => {
1372                for (diagonal, &offset) in offsets.iter().enumerate() {
1373                    for row in 0..self.shape.0 {
1374                        let value = values_by_row[diagonal * self.shape.0 + row];
1375                        if value.norm() <= f64::EPSILON {
1376                            continue;
1377                        }
1378                        let Some(column) = row.checked_add_signed(-offset) else {
1379                            continue;
1380                        };
1381                        if column < self.shape.1
1382                            && !self.entry_is_hermitian(row, column, value, tolerance)
1383                        {
1384                            return false;
1385                        }
1386                    }
1387                }
1388            }
1389            Storage::MatrixFree(entries) => {
1390                for entry in entries {
1391                    if !self.entry_is_hermitian(entry.row, entry.column, entry.value, tolerance) {
1392                        return false;
1393                    }
1394                }
1395            }
1396        }
1397        true
1398    }
1399}
1400
1401impl LinearOperator for Operator {
1402    fn shape(&self) -> (usize, usize) {
1403        self.shape
1404    }
1405
1406    fn format(&self) -> MatrixFormat {
1407        self.format
1408    }
1409
1410    fn is_real(&self) -> bool {
1411        self.real
1412    }
1413
1414    fn apply_real(&self, input: &[f64], output: &mut [f64]) -> Result<()> {
1415        if input.len() != self.shape.1 || output.len() != self.shape.0 {
1416            return Err(QmbedError::DimensionMismatch(format!(
1417                "shape {:?} requires real input length {} and output length {}, got {} and {}",
1418                self.shape,
1419                self.shape.1,
1420                self.shape.0,
1421                input.len(),
1422                output.len()
1423            )));
1424        }
1425        if !self.real {
1426            return Err(QmbedError::UnsupportedBackend(
1427                "real action requires an operator with real-valued storage".into(),
1428            ));
1429        }
1430        output.fill(0.0);
1431        match &self.storage {
1432            Storage::Dense(values) => {
1433                for row in 0..self.shape.0 {
1434                    for column in 0..self.shape.1 {
1435                        output[row] += values[row * self.shape.1 + column].re * input[column];
1436                    }
1437                }
1438            }
1439            Storage::Csc {
1440                column_offsets,
1441                row_indices,
1442                values,
1443            } => {
1444                for column in 0..self.shape.1 {
1445                    let input_value = input[column];
1446                    for position in column_offsets[column]..column_offsets[column + 1] {
1447                        output[row_indices[position]] += values[position].re * input_value;
1448                    }
1449                }
1450            }
1451            Storage::Csr {
1452                row_offsets,
1453                column_indices,
1454                values,
1455            } => {
1456                for row in 0..self.shape.0 {
1457                    for position in row_offsets[row]..row_offsets[row + 1] {
1458                        output[row] += values[position].re * input[column_indices[position]];
1459                    }
1460                }
1461            }
1462            Storage::Dia {
1463                offsets,
1464                values_by_row,
1465            } => {
1466                for (diagonal, &offset) in offsets.iter().enumerate() {
1467                    for row in 0..self.shape.0 {
1468                        let Some(column) = row.checked_add_signed(-offset) else {
1469                            continue;
1470                        };
1471                        if column < self.shape.1 {
1472                            output[row] +=
1473                                values_by_row[diagonal * self.shape.0 + row].re * input[column];
1474                        }
1475                    }
1476                }
1477            }
1478            Storage::MatrixFree(entries) => {
1479                for entry in entries {
1480                    output[entry.row] += entry.value.re * input[entry.column];
1481                }
1482            }
1483        }
1484        Ok(())
1485    }
1486
1487    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
1488        check_apply_shape(self.shape, input, output)?;
1489        output.fill(Complex64::new(0.0, 0.0));
1490        if self.real {
1491            self.apply_real_coefficients(input, output);
1492            return Ok(());
1493        }
1494        match &self.storage {
1495            Storage::Dense(values) => {
1496                for row in 0..self.shape.0 {
1497                    for column in 0..self.shape.1 {
1498                        output[row] += values[row * self.shape.1 + column] * input[column];
1499                    }
1500                }
1501            }
1502            Storage::Csc {
1503                column_offsets,
1504                row_indices,
1505                values,
1506            } => {
1507                for column in 0..self.shape.1 {
1508                    let input_value = input[column];
1509                    for position in column_offsets[column]..column_offsets[column + 1] {
1510                        output[row_indices[position]] += values[position] * input_value;
1511                    }
1512                }
1513            }
1514            Storage::Csr {
1515                row_offsets,
1516                column_indices,
1517                values,
1518            } => {
1519                for row in 0..self.shape.0 {
1520                    for position in row_offsets[row]..row_offsets[row + 1] {
1521                        output[row] += values[position] * input[column_indices[position]];
1522                    }
1523                }
1524            }
1525            Storage::Dia {
1526                offsets,
1527                values_by_row,
1528            } => {
1529                for (diagonal, &offset) in offsets.iter().enumerate() {
1530                    for row in 0..self.shape.0 {
1531                        let Some(column) = row.checked_add_signed(-offset) else {
1532                            continue;
1533                        };
1534                        if column < self.shape.1 {
1535                            output[row] +=
1536                                values_by_row[diagonal * self.shape.0 + row] * input[column];
1537                        }
1538                    }
1539                }
1540            }
1541            Storage::MatrixFree(entries) => {
1542                for entry in entries {
1543                    output[entry.row] += entry.value * input[entry.column];
1544                }
1545            }
1546        }
1547        Ok(())
1548    }
1549
1550    fn stored_triplets(&self) -> Result<Option<Vec<(usize, usize, Complex64)>>> {
1551        Ok(Some(self.triplets()))
1552    }
1553
1554    fn shifted_solver(&self, shift: f64) -> Result<Option<Box<dyn ShiftedLinearSolver>>> {
1555        if self.shape.0 != self.shape.1 || !shift.is_finite() {
1556            return Err(QmbedError::InvalidOptions(
1557                "shifted factorization requires a square operator and finite shift".into(),
1558            ));
1559        }
1560        let Storage::Csc {
1561            column_offsets,
1562            row_indices,
1563            values,
1564        } = &self.storage
1565        else {
1566            return Ok(None);
1567        };
1568        factor_shifted_csc(
1569            self.shape.0,
1570            column_offsets,
1571            row_indices,
1572            values,
1573            shift,
1574            self.real,
1575        )
1576        .map(Some)
1577    }
1578}
1579
1580fn csc_storage(entries: &[Triplet], columns: usize) -> Storage {
1581    let mut column_offsets = vec![0_usize; columns + 1];
1582    for entry in entries {
1583        column_offsets[entry.column + 1] += 1;
1584    }
1585    for column in 0..columns {
1586        column_offsets[column + 1] += column_offsets[column];
1587    }
1588    Storage::Csc {
1589        column_offsets,
1590        row_indices: entries.iter().map(|entry| entry.row).collect(),
1591        values: entries.iter().map(|entry| entry.value).collect(),
1592    }
1593}
1594
1595fn storage_is_real(storage: &Storage) -> bool {
1596    match storage {
1597        Storage::Dense(values)
1598        | Storage::Csc { values, .. }
1599        | Storage::Csr { values, .. }
1600        | Storage::Dia {
1601            values_by_row: values,
1602            ..
1603        } => values.iter().all(|value| value.im == 0.0),
1604        Storage::MatrixFree(entries) => entries.iter().all(|entry| entry.value.im == 0.0),
1605    }
1606}
1607
1608fn csr_storage(entries: &[Triplet], rows: usize) -> Storage {
1609    let mut row_offsets = vec![0_usize; rows + 1];
1610    for entry in entries {
1611        row_offsets[entry.row + 1] += 1;
1612    }
1613    for row in 0..rows {
1614        row_offsets[row + 1] += row_offsets[row];
1615    }
1616    Storage::Csr {
1617        row_offsets,
1618        column_indices: entries.iter().map(|entry| entry.column).collect(),
1619        values: entries.iter().map(|entry| entry.value).collect(),
1620    }
1621}
1622
1623fn dia_storage(entries: &[Triplet], rows: usize) -> Storage {
1624    let mut offsets: Vec<_> = entries
1625        .iter()
1626        .map(|entry| entry.row as isize - entry.column as isize)
1627        .collect();
1628    offsets.sort_unstable();
1629    offsets.dedup();
1630    let diagonal_index: HashMap<_, _> = offsets
1631        .iter()
1632        .copied()
1633        .enumerate()
1634        .map(|(index, offset)| (offset, index))
1635        .collect();
1636    let mut values_by_row = vec![Complex64::new(0.0, 0.0); offsets.len() * rows];
1637    for entry in entries {
1638        let offset = entry.row as isize - entry.column as isize;
1639        let diagonal = diagonal_index[&offset];
1640        values_by_row[diagonal * rows + entry.row] = entry.value;
1641    }
1642    Storage::Dia {
1643        offsets,
1644        values_by_row,
1645    }
1646}
1647
1648pub(crate) fn check_apply_shape(
1649    shape: (usize, usize),
1650    input: &[Complex64],
1651    output: &[Complex64],
1652) -> Result<()> {
1653    if input.len() != shape.1 || output.len() != shape.0 {
1654        return Err(QmbedError::DimensionMismatch(format!(
1655            "shape {shape:?} requires input length {} and output length {}, got {} and {}",
1656            shape.1,
1657            shape.0,
1658            input.len(),
1659            output.len()
1660        )));
1661    }
1662    Ok(())
1663}
1664
1665pub(crate) fn materialize_dense(
1666    operator: &(impl LinearOperator + ?Sized),
1667) -> Result<Vec<Complex64>> {
1668    let (rows, columns) = operator.shape();
1669    let mut dense = vec![Complex64::new(0.0, 0.0); rows * columns];
1670    let mut input = vec![Complex64::new(0.0, 0.0); columns];
1671    let mut output = vec![Complex64::new(0.0, 0.0); rows];
1672    for column in 0..columns {
1673        input.fill(Complex64::new(0.0, 0.0));
1674        input[column] = Complex64::new(1.0, 0.0);
1675        operator.apply(&input, &mut output)?;
1676        for row in 0..rows {
1677            dense[row * columns + column] = output[row];
1678        }
1679    }
1680    Ok(dense)
1681}
1682
1683/// Shared scaled matrix-vector adapter corresponding to QuSpin's low-level
1684/// `matvec` helper. When `overwrite` is false the result is accumulated into
1685/// the existing output buffer.
1686pub fn matvec(
1687    operator: &(impl LinearOperator + ?Sized),
1688    input: &[Complex64],
1689    output: &mut [Complex64],
1690    coefficient: Complex64,
1691    overwrite: bool,
1692) -> Result<()> {
1693    if !coefficient.re.is_finite() || !coefficient.im.is_finite() {
1694        return Err(QmbedError::InvalidOptions(
1695            "matvec coefficient must be finite".into(),
1696        ));
1697    }
1698    if overwrite {
1699        operator.apply(input, output)?;
1700        for value in output {
1701            *value *= coefficient;
1702        }
1703        return Ok(());
1704    }
1705
1706    let mut contribution = vec![Complex64::new(0.0, 0.0); output.len()];
1707    operator.apply(input, &mut contribution)?;
1708    for (value, addition) in output.iter_mut().zip(contribution) {
1709        *value += coefficient * addition;
1710    }
1711    Ok(())
1712}
1713
1714/// Apply an operator to a column batch without constructing a dense matrix.
1715pub fn matmat(
1716    operator: &(impl LinearOperator + ?Sized),
1717    columns: &[Vec<Complex64>],
1718) -> Result<Vec<Vec<Complex64>>> {
1719    let (rows, input_dimension) = operator.shape();
1720    columns
1721        .iter()
1722        .map(|column| {
1723            if column.len() != input_dimension {
1724                return Err(QmbedError::DimensionMismatch(
1725                    "matrix column does not match the operator input dimension".into(),
1726                ));
1727            }
1728            let mut output = vec![Complex64::new(0.0, 0.0); rows];
1729            operator.apply(column, &mut output)?;
1730            Ok(output)
1731        })
1732        .collect()
1733}
1734
1735/// Row-vector action through the transpose narrow waist.
1736pub fn rmatvec(
1737    operator: &(impl LinearOperator + ?Sized),
1738    input: &[Complex64],
1739) -> Result<Vec<Complex64>> {
1740    let mut output = vec![Complex64::new(0.0, 0.0); operator.shape().1];
1741    operator.apply_transpose(input, &mut output)?;
1742    Ok(output)
1743}
1744
1745pub fn rmatmat(
1746    operator: &(impl LinearOperator + ?Sized),
1747    rows: &[Vec<Complex64>],
1748) -> Result<Vec<Vec<Complex64>>> {
1749    rows.iter().map(|row| rmatvec(operator, row)).collect()
1750}
1751
1752/// Reusable owned matrix-vector plan for callback-driven algorithms.
1753#[derive(Clone)]
1754pub struct MatVecPlan {
1755    operator: Arc<dyn LinearOperator>,
1756}
1757
1758impl std::fmt::Debug for MatVecPlan {
1759    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1760        formatter
1761            .debug_struct("MatVecPlan")
1762            .field("shape", &self.operator.shape())
1763            .finish()
1764    }
1765}
1766
1767impl MatVecPlan {
1768    pub fn new(operator: Arc<dyn LinearOperator>) -> Self {
1769        Self { operator }
1770    }
1771
1772    pub fn apply(
1773        &self,
1774        input: &[Complex64],
1775        output: &mut [Complex64],
1776        coefficient: Complex64,
1777        overwrite: bool,
1778    ) -> Result<()> {
1779        matvec(
1780            self.operator.as_ref(),
1781            input,
1782            output,
1783            coefficient,
1784            overwrite,
1785        )
1786    }
1787
1788    pub fn operator(&self) -> &Arc<dyn LinearOperator> {
1789        &self.operator
1790    }
1791}
1792
1793pub fn get_matvec_function(operator: Arc<dyn LinearOperator>) -> MatVecPlan {
1794    MatVecPlan::new(operator)
1795}
1796
1797/// Matrix-free algebraic transpose view of an owned linear operator.
1798#[derive(Clone)]
1799pub struct TransposedLinearOperator {
1800    operator: Arc<dyn LinearOperator>,
1801}
1802
1803impl TransposedLinearOperator {
1804    pub fn new(operator: Arc<dyn LinearOperator>) -> Self {
1805        Self { operator }
1806    }
1807}
1808
1809impl std::fmt::Debug for TransposedLinearOperator {
1810    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1811        formatter
1812            .debug_struct("TransposedLinearOperator")
1813            .field("shape", &self.shape())
1814            .finish()
1815    }
1816}
1817
1818impl LinearOperator for TransposedLinearOperator {
1819    fn shape(&self) -> (usize, usize) {
1820        let (rows, columns) = self.operator.shape();
1821        (columns, rows)
1822    }
1823
1824    fn format(&self) -> MatrixFormat {
1825        MatrixFormat::MatrixFree
1826    }
1827
1828    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
1829        self.operator.apply_transpose(input, output)
1830    }
1831
1832    fn apply_transpose(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
1833        self.operator.apply(input, output)
1834    }
1835
1836    fn stored_triplets(&self) -> Result<Option<Vec<(usize, usize, Complex64)>>> {
1837        Ok(self.operator.stored_triplets()?.map(|entries| {
1838            entries
1839                .into_iter()
1840                .map(|(row, column, value)| (column, row, value))
1841                .collect()
1842        }))
1843    }
1844}
1845
1846/// Matrix-free elementwise-conjugate view of an owned linear operator.
1847#[derive(Clone)]
1848pub struct ConjugatedLinearOperator {
1849    operator: Arc<dyn LinearOperator>,
1850}
1851
1852impl ConjugatedLinearOperator {
1853    pub fn new(operator: Arc<dyn LinearOperator>) -> Self {
1854        Self { operator }
1855    }
1856}
1857
1858impl std::fmt::Debug for ConjugatedLinearOperator {
1859    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1860        formatter
1861            .debug_struct("ConjugatedLinearOperator")
1862            .field("shape", &self.shape())
1863            .finish()
1864    }
1865}
1866
1867impl LinearOperator for ConjugatedLinearOperator {
1868    fn shape(&self) -> (usize, usize) {
1869        self.operator.shape()
1870    }
1871
1872    fn format(&self) -> MatrixFormat {
1873        MatrixFormat::MatrixFree
1874    }
1875
1876    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
1877        let conjugated_input: Vec<_> = input.iter().map(|value| value.conj()).collect();
1878        self.operator.apply(&conjugated_input, output)?;
1879        for value in output {
1880            *value = value.conj();
1881        }
1882        Ok(())
1883    }
1884
1885    fn stored_triplets(&self) -> Result<Option<Vec<(usize, usize, Complex64)>>> {
1886        Ok(self.operator.stored_triplets()?.map(|entries| {
1887            entries
1888                .into_iter()
1889                .map(|(row, column, value)| (row, column, value.conj()))
1890                .collect()
1891        }))
1892    }
1893}
1894
1895/// Matrix-free conjugate-transpose view of an owned linear operator.
1896#[derive(Clone)]
1897pub struct AdjointLinearOperator {
1898    operator: Arc<dyn LinearOperator>,
1899}
1900
1901impl AdjointLinearOperator {
1902    pub fn new(operator: Arc<dyn LinearOperator>) -> Self {
1903        Self { operator }
1904    }
1905}
1906
1907impl std::fmt::Debug for AdjointLinearOperator {
1908    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1909        formatter
1910            .debug_struct("AdjointLinearOperator")
1911            .field("shape", &self.shape())
1912            .finish()
1913    }
1914}
1915
1916impl LinearOperator for AdjointLinearOperator {
1917    fn shape(&self) -> (usize, usize) {
1918        let (rows, columns) = self.operator.shape();
1919        (columns, rows)
1920    }
1921
1922    fn format(&self) -> MatrixFormat {
1923        MatrixFormat::MatrixFree
1924    }
1925
1926    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
1927        self.operator.apply_adjoint(input, output)
1928    }
1929
1930    fn stored_triplets(&self) -> Result<Option<Vec<(usize, usize, Complex64)>>> {
1931        Ok(self.operator.stored_triplets()?.map(|entries| {
1932            entries
1933                .into_iter()
1934                .map(|(row, column, value)| (column, row, value.conj()))
1935                .collect()
1936        }))
1937    }
1938}
1939
1940/// Universal square or cross-sector operator builder.
1941pub struct OperatorBuilder<'a, Source, Target>
1942where
1943    Source: Basis,
1944    Target: Basis<State = Source::State>,
1945{
1946    source: &'a Source,
1947    target: &'a Target,
1948    terms: Vec<OperatorSpec>,
1949    checks: AssemblyChecks,
1950}
1951
1952#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1953struct ParticleConservationComponent {
1954    /// Cartesian operators share one normalization within a concrete basis,
1955    /// but that normalization can differ from explicit ladder operators.
1956    /// Keeping the degree in the key permits exact `xx + yy` cancellation
1957    /// without assuming a particular spin normalization.
1958    cartesian_degree: usize,
1959    operator: String,
1960    sites: Vec<usize>,
1961}
1962
1963fn definite_number_change_components(operator: &str) -> Result<Vec<(usize, String, Complex64)>> {
1964    let mut components = vec![(0_usize, String::new(), Complex64::new(1.0, 0.0))];
1965    for symbol in operator.chars() {
1966        match symbol {
1967            'x' | 'y' => {
1968                let mut expanded = Vec::with_capacity(components.len().saturating_mul(2));
1969                for (degree, prefix, coefficient) in components {
1970                    let mut raised = prefix.clone();
1971                    raised.push('+');
1972                    let mut lowered = prefix;
1973                    lowered.push('-');
1974                    let (raise_phase, lower_phase) = if symbol == 'x' {
1975                        (Complex64::new(1.0, 0.0), Complex64::new(1.0, 0.0))
1976                    } else {
1977                        (Complex64::new(0.0, -1.0), Complex64::new(0.0, 1.0))
1978                    };
1979                    expanded.push((degree + 1, raised, coefficient * raise_phase));
1980                    expanded.push((degree + 1, lowered, coefficient * lower_phase));
1981                }
1982                components = expanded;
1983            }
1984            'I' | 'n' | 'z' | '+' | '-' | '|' => {
1985                for (_, prefix, _) in &mut components {
1986                    prefix.push(symbol);
1987                }
1988            }
1989            _ => return Err(QmbedError::InvalidOperator(operator.into())),
1990        }
1991    }
1992    Ok(components)
1993}
1994
1995/// Check additive-sector conservation for a complete operator sum.
1996///
1997/// Cartesian products are first decomposed into definite raising/lowering
1998/// components. Coefficients are accumulated for identical spatial actions
1999/// before sector closure is tested, so cancellations such as `xx + yy` are
2000/// recognized. The Cartesian degree remains part of the canonical key because
2001/// a basis may normalize `x/y` differently from explicit `+/-` symbols.
2002pub fn operator_sum_preserves_particle_sector<B>(basis: &B, terms: &[OperatorSpec]) -> Result<bool>
2003where
2004    B: Basis,
2005{
2006    let mut components = HashMap::<ParticleConservationComponent, Complex64>::new();
2007    for term in terms {
2008        for coupling in term.couplings() {
2009            for (cartesian_degree, operator, coefficient) in
2010                definite_number_change_components(term.operator())?
2011            {
2012                *components
2013                    .entry(ParticleConservationComponent {
2014                        cartesian_degree,
2015                        operator,
2016                        sites: coupling.sites.clone(),
2017                    })
2018                    .or_insert(Complex64::new(0.0, 0.0)) += coupling.coefficient * coefficient;
2019            }
2020        }
2021    }
2022    for (component, coefficient) in components {
2023        if coefficient.norm() > f64::EPSILON
2024            && !basis.operator_preserves_particle_sector_on_sites(
2025                &component.operator,
2026                &component.sites,
2027            )?
2028        {
2029            return Ok(false);
2030        }
2031    }
2032    Ok(true)
2033}
2034
2035impl<'a, BasisType> OperatorBuilder<'a, BasisType, BasisType>
2036where
2037    BasisType: Basis,
2038{
2039    /// Start a square operator builder on one source and target basis.
2040    pub fn on(basis: &'a BasisType) -> Self {
2041        Self {
2042            source: basis,
2043            target: basis,
2044            terms: Vec::new(),
2045            checks: AssemblyChecks::all(),
2046        }
2047    }
2048}
2049
2050impl<'a, Source, Target> OperatorBuilder<'a, Source, Target>
2051where
2052    Source: Basis,
2053    Target: Basis<State = Source::State>,
2054{
2055    /// Start a rectangular builder mapping `source` coordinates into `target`.
2056    ///
2057    /// The two bases share a state representation but may select different
2058    /// particle-number or symmetry sectors.
2059    pub fn between(source: &'a Source, target: &'a Target) -> Self {
2060        Self {
2061            source,
2062            target,
2063            terms: Vec::new(),
2064            checks: AssemblyChecks::rectangular(),
2065        }
2066    }
2067
2068    /// Append several validated local terms.
2069    pub fn terms(mut self, terms: impl IntoIterator<Item = OperatorSpec>) -> Self {
2070        self.terms.extend(terms);
2071        self
2072    }
2073
2074    /// Append one validated local term.
2075    pub fn term(mut self, term: OperatorSpec) -> Self {
2076        self.terms.push(term);
2077        self
2078    }
2079
2080    /// Replace the default Hermiticity, conservation, and symmetry checks.
2081    pub const fn checks(mut self, checks: AssemblyChecks) -> Self {
2082        self.checks = checks;
2083        self
2084    }
2085
2086    /// Assemble all terms through the universal column-streaming path.
2087    ///
2088    /// The same routine supports deterministic and branching local actions,
2089    /// symmetry-reduced targets, rectangular sector changes, and every public
2090    /// storage format.
2091    pub fn build(self, format: MatrixFormat) -> Result<Operator> {
2092        let shape = (self.target.len(), self.source.len());
2093        if self.checks.particle_conservation
2094            && (!operator_sum_preserves_particle_sector(self.source, &self.terms)?
2095                || !operator_sum_preserves_particle_sector(self.target, &self.terms)?)
2096        {
2097            return Err(QmbedError::InvalidSector(
2098                "operator sum does not preserve the selected particle sector".into(),
2099            ));
2100        }
2101        // Local actions are generated one source column at a time. Preserve
2102        // that structure instead of hashing every `(row, column)` pair into a
2103        // global table and sorting it back into sparse-matrix order later.
2104        //
2105        // A reusable column buffer remains fully general: arbitrary terms may
2106        // still emit the same row repeatedly, including branching and
2107        // symmetry-reduced actions. Sorting and coalescing only the current
2108        // column bounds temporary memory by the local connectivity.
2109        let local_capacity = self.terms.iter().map(|term| term.couplings().len()).sum();
2110        let mut column_entries = Vec::<(usize, Complex64)>::with_capacity(local_capacity);
2111        let mut entries = Vec::<Triplet>::new();
2112        let mut csc_column_offsets = Vec::new();
2113        let mut csc_row_indices = Vec::new();
2114        let mut csc_values = Vec::new();
2115        if format == MatrixFormat::Csc {
2116            csc_column_offsets.reserve(shape.1 + 1);
2117            csc_column_offsets.push(0);
2118        }
2119        for column in 0..self.source.len() {
2120            column_entries.clear();
2121            let source_state = self.source.state(column)?;
2122            let source_orbit_size = self.source.transition_orbit_size(source_state)?;
2123            for term in &self.terms {
2124                for coupling in term.couplings() {
2125                    self.source.visit_preparsed_local_unreduced_transitions(
2126                        source_state,
2127                        term.operator(),
2128                        term.symbols(),
2129                        term.split(),
2130                        &coupling.sites,
2131                        |unreduced_target, local_amplitude| {
2132                            let Some((row, reduction_amplitude)) = self
2133                                .target
2134                                .index_transition(unreduced_target, source_orbit_size)?
2135                            else {
2136                                return Ok(());
2137                            };
2138                            column_entries.push((
2139                                row,
2140                                coupling.coefficient * local_amplitude * reduction_amplitude,
2141                            ));
2142                            Ok(())
2143                        },
2144                    )?;
2145                }
2146            }
2147
2148            column_entries.sort_unstable_by_key(|(row, _)| *row);
2149            let mut position = 0;
2150            while position < column_entries.len() {
2151                let row = column_entries[position].0;
2152                let mut value = column_entries[position].1;
2153                position += 1;
2154                while position < column_entries.len() && column_entries[position].0 == row {
2155                    value += column_entries[position].1;
2156                    position += 1;
2157                }
2158                if value.norm() <= f64::EPSILON {
2159                    continue;
2160                }
2161                if format == MatrixFormat::Csc {
2162                    csc_row_indices.push(row);
2163                    csc_values.push(value);
2164                } else {
2165                    entries.push(Triplet { row, column, value });
2166                }
2167            }
2168            if format == MatrixFormat::Csc {
2169                csc_column_offsets.push(csc_row_indices.len());
2170            }
2171        }
2172        match format {
2173            MatrixFormat::Csc => {}
2174            MatrixFormat::Csr => entries.sort_by_key(|entry| (entry.row, entry.column)),
2175            _ => entries.sort_by_key(|entry| (entry.row, entry.column)),
2176        }
2177        if format == MatrixFormat::Dia
2178            && entries
2179                .iter()
2180                .map(|entry| entry.row as isize - entry.column as isize)
2181                .collect::<std::collections::HashSet<_>>()
2182                .len()
2183                > shape.0.min(shape.1).saturating_mul(2).max(1)
2184        {
2185            return Err(QmbedError::UnsupportedBackend(
2186                "the requested operator is not usefully diagonal-banded".into(),
2187            ));
2188        }
2189        let storage = match format {
2190            MatrixFormat::Dense => {
2191                let mut dense = vec![Complex64::new(0.0, 0.0); shape.0 * shape.1];
2192                for entry in &entries {
2193                    dense[entry.row * shape.1 + entry.column] = entry.value;
2194                }
2195                Storage::Dense(dense)
2196            }
2197            MatrixFormat::Csc => Storage::Csc {
2198                column_offsets: csc_column_offsets,
2199                row_indices: csc_row_indices,
2200                values: csc_values,
2201            },
2202            MatrixFormat::Csr => csr_storage(&entries, shape.0),
2203            MatrixFormat::Dia => dia_storage(&entries, shape.0),
2204            MatrixFormat::MatrixFree => Storage::MatrixFree(entries),
2205        };
2206        let operator = Operator {
2207            shape,
2208            format,
2209            real: storage_is_real(&storage),
2210            storage,
2211        };
2212        if self.checks.hermiticity && !operator.is_hermitian(1.0e-12) {
2213            return Err(QmbedError::NonHermitian);
2214        }
2215        Ok(operator)
2216    }
2217
2218    /// Assemble static and driven terms through the same local-action path.
2219    pub fn build_dynamic(
2220        self,
2221        dynamic_terms: impl IntoIterator<Item = DynamicTerm>,
2222        format: MatrixFormat,
2223    ) -> Result<Hamiltonian<Dynamic>> {
2224        let Self {
2225            source,
2226            target,
2227            terms,
2228            checks,
2229        } = self;
2230        if source.len() != target.len() {
2231            return Err(QmbedError::DimensionMismatch(
2232                "a Hamiltonian must be square".into(),
2233            ));
2234        }
2235        let static_part = Self {
2236            source,
2237            target,
2238            terms,
2239            checks,
2240        }
2241        .build(format)?;
2242        let component_checks = AssemblyChecks {
2243            hermiticity: false,
2244            particle_conservation: checks.particle_conservation,
2245            symmetry_compatibility: checks.symmetry_compatibility,
2246        };
2247        let mut components = Vec::new();
2248        for dynamic_term in dynamic_terms {
2249            let (term, drive) = dynamic_term.into_parts();
2250            let operator = Self {
2251                source,
2252                target,
2253                terms: vec![term],
2254                checks: component_checks,
2255            }
2256            .build(format)?;
2257            components.push(DynamicComponent { operator, drive });
2258        }
2259        Hamiltonian::<Dynamic>::new(static_part, components)
2260    }
2261}
2262
2263#[derive(Clone, Debug, PartialEq)]
2264pub struct BraKetTransition<State> {
2265    pub bra: State,
2266    pub ket: State,
2267    pub matrix_element: Complex64,
2268}
2269
2270/// Raw local transition table, including branching callbacks and fermionic signs.
2271pub fn bra_ket_transitions<B>(
2272    basis: &B,
2273    operator: &str,
2274    sites: &[usize],
2275    coefficient: impl Into<Complex64>,
2276    kets: impl IntoIterator<Item = B::State>,
2277) -> Result<Vec<BraKetTransition<B::State>>>
2278where
2279    B: Basis,
2280{
2281    let coefficient = coefficient.into();
2282    if !coefficient.re.is_finite() || !coefficient.im.is_finite() {
2283        return Err(QmbedError::InvalidCoupling(
2284            "transition-table coefficient must be finite".into(),
2285        ));
2286    }
2287    let mut transitions = Vec::new();
2288    for ket in kets {
2289        basis.visit_local_unreduced_transitions(ket, operator, sites, |bra, amplitude| {
2290            let matrix_element = coefficient * amplitude;
2291            if matrix_element.norm() > f64::EPSILON {
2292                transitions.push(BraKetTransition {
2293                    bra,
2294                    ket,
2295                    matrix_element,
2296                });
2297            }
2298            Ok(())
2299        })?;
2300    }
2301    Ok(transitions)
2302}
2303
2304/// Apply parsed terms directly between sectors without materializing a matrix.
2305pub fn apply_sector_shift<Source, Target>(
2306    source: &Source,
2307    target: &Target,
2308    terms: &[OperatorSpec],
2309    input: &[Complex64],
2310    output: &mut [Complex64],
2311) -> Result<()>
2312where
2313    Source: Basis,
2314    Target: Basis<State = Source::State>,
2315{
2316    if input.len() != source.len() || output.len() != target.len() {
2317        return Err(QmbedError::DimensionMismatch(
2318            "sector-shift state dimensions do not match source and target bases".into(),
2319        ));
2320    }
2321    output.fill(Complex64::new(0.0, 0.0));
2322    for (column, input_value) in input.iter().copied().enumerate() {
2323        if input_value.norm() <= f64::EPSILON {
2324            continue;
2325        }
2326        let source_state = source.state(column)?;
2327        let source_orbit_size = source.transition_orbit_size(source_state)?;
2328        for term in terms {
2329            for coupling in term.couplings() {
2330                source.visit_local_unreduced_transitions(
2331                    source_state,
2332                    term.operator(),
2333                    &coupling.sites,
2334                    |unreduced_target, local_amplitude| {
2335                        let Some((row, reduction_amplitude)) =
2336                            target.index_transition(unreduced_target, source_orbit_size)?
2337                        else {
2338                            return Ok(());
2339                        };
2340                        output[row] += input_value
2341                            * coupling.coefficient
2342                            * local_amplitude
2343                            * reduction_amplitude;
2344                        Ok(())
2345                    },
2346                )?;
2347            }
2348        }
2349    }
2350    Ok(())
2351}
2352
2353type DriveFunction = Arc<dyn Fn(f64) -> Complex64 + Send + Sync>;
2354
2355/// A parsed local term multiplied by a scalar function of time.
2356#[derive(Clone)]
2357pub struct DynamicTerm {
2358    term: OperatorSpec,
2359    drive: DriveFunction,
2360}
2361
2362impl std::fmt::Debug for DynamicTerm {
2363    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2364        formatter
2365            .debug_struct("DynamicTerm")
2366            .field("term", &self.term)
2367            .field("drive", &"<callable>")
2368            .finish()
2369    }
2370}
2371
2372impl DynamicTerm {
2373    pub fn new<F>(term: OperatorSpec, drive: F) -> Self
2374    where
2375        F: Fn(f64) -> Complex64 + Send + Sync + 'static,
2376    {
2377        Self {
2378            term,
2379            drive: Arc::new(drive),
2380        }
2381    }
2382
2383    pub fn coefficient_at(&self, time: f64) -> Result<Complex64> {
2384        finite_drive_value(time, (self.drive)(time))
2385    }
2386
2387    fn into_parts(self) -> (OperatorSpec, DriveFunction) {
2388        (self.term, self.drive)
2389    }
2390}
2391
2392#[derive(Clone)]
2393pub struct DynamicComponent {
2394    operator: Operator,
2395    drive: DriveFunction,
2396}
2397
2398impl std::fmt::Debug for DynamicComponent {
2399    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2400        formatter
2401            .debug_struct("DynamicComponent")
2402            .field("operator", &self.operator)
2403            .field("drive", &"<callable>")
2404            .finish()
2405    }
2406}
2407
2408impl DynamicComponent {
2409    pub fn new<F>(operator: Operator, drive: F) -> Self
2410    where
2411        F: Fn(f64) -> Complex64 + Send + Sync + 'static,
2412    {
2413        Self {
2414            operator,
2415            drive: Arc::new(drive),
2416        }
2417    }
2418}
2419
2420#[derive(Clone, Copy, Debug)]
2421pub struct Static;
2422
2423#[derive(Clone, Copy, Debug)]
2424pub struct Dynamic;
2425
2426/// Static or explicitly time-dependent Hamiltonian with a type-state marker.
2427#[derive(Clone, Debug)]
2428pub struct Hamiltonian<Kind = Static> {
2429    static_part: Operator,
2430    dynamic_parts: Vec<DynamicComponent>,
2431    marker: PhantomData<Kind>,
2432}
2433
2434impl Hamiltonian<Static> {
2435    pub fn new(operator: Operator) -> Result<Self> {
2436        let shape = operator.shape();
2437        if shape.0 != shape.1 {
2438            return Err(QmbedError::DimensionMismatch(
2439                "a Hamiltonian must be square".into(),
2440            ));
2441        }
2442        Ok(Self {
2443            static_part: operator,
2444            dynamic_parts: Vec::new(),
2445            marker: PhantomData,
2446        })
2447    }
2448
2449    pub fn operator(&self) -> &Operator {
2450        &self.static_part
2451    }
2452
2453    pub fn transpose(&self) -> Result<Self> {
2454        Self::new(self.static_part.transpose()?)
2455    }
2456
2457    pub fn conjugated(&self) -> Result<Self> {
2458        Self::new(self.static_part.conjugated()?)
2459    }
2460
2461    pub fn adjoint(&self) -> Result<Self> {
2462        Self::new(self.static_part.adjoint()?)
2463    }
2464
2465    pub fn scaled(&self, coefficient: impl Into<Complex64>) -> Result<Self> {
2466        Self::new(self.static_part.scaled(coefficient)?)
2467    }
2468}
2469
2470impl Hamiltonian<Dynamic> {
2471    pub fn new(static_part: Operator, dynamic_parts: Vec<DynamicComponent>) -> Result<Self> {
2472        let shape = static_part.shape();
2473        if shape.0 != shape.1
2474            || dynamic_parts
2475                .iter()
2476                .any(|component| component.operator.shape() != shape)
2477        {
2478            return Err(QmbedError::DimensionMismatch(
2479                "all Hamiltonian components must share one square shape".into(),
2480            ));
2481        }
2482        Ok(Self {
2483            static_part,
2484            dynamic_parts,
2485            marker: PhantomData,
2486        })
2487    }
2488
2489    pub fn evaluate(&self, time: f64, format: MatrixFormat) -> Result<Operator> {
2490        if !time.is_finite() {
2491            return Err(QmbedError::InvalidOptions(
2492                "evaluation time must be finite".into(),
2493            ));
2494        }
2495        let shape = self.static_part.shape();
2496        let mut entries = self.static_part.triplets();
2497        for component in &self.dynamic_parts {
2498            let coefficient = finite_drive_value(time, (component.drive)(time))?;
2499            entries.extend(
2500                component
2501                    .operator
2502                    .triplets()
2503                    .into_iter()
2504                    .map(|(row, column, value)| (row, column, coefficient * value)),
2505            );
2506        }
2507        Operator::from_triplets(shape.0, shape.1, entries, format)
2508    }
2509
2510    pub fn dynamic_components(&self) -> usize {
2511        self.dynamic_parts.len()
2512    }
2513
2514    pub fn static_part(&self) -> &Operator {
2515        &self.static_part
2516    }
2517
2518    pub fn transpose(&self) -> Result<Self> {
2519        Self::new(
2520            self.static_part.transpose()?,
2521            self.dynamic_parts
2522                .iter()
2523                .map(|component| {
2524                    Ok(DynamicComponent {
2525                        operator: component.operator.transpose()?,
2526                        drive: component.drive.clone(),
2527                    })
2528                })
2529                .collect::<Result<_>>()?,
2530        )
2531    }
2532
2533    pub fn conjugated(&self) -> Result<Self> {
2534        Self::new(
2535            self.static_part.conjugated()?,
2536            self.dynamic_parts
2537                .iter()
2538                .map(|component| {
2539                    let drive = component.drive.clone();
2540                    Ok(DynamicComponent {
2541                        operator: component.operator.conjugated()?,
2542                        drive: Arc::new(move |time| drive(time).conj()),
2543                    })
2544                })
2545                .collect::<Result<_>>()?,
2546        )
2547    }
2548
2549    pub fn adjoint(&self) -> Result<Self> {
2550        Self::new(
2551            self.static_part.adjoint()?,
2552            self.dynamic_parts
2553                .iter()
2554                .map(|component| {
2555                    let drive = component.drive.clone();
2556                    Ok(DynamicComponent {
2557                        operator: component.operator.adjoint()?,
2558                        drive: Arc::new(move |time| drive(time).conj()),
2559                    })
2560                })
2561                .collect::<Result<_>>()?,
2562        )
2563    }
2564
2565    pub fn scaled(&self, coefficient: impl Into<Complex64>) -> Result<Self> {
2566        let coefficient = coefficient.into();
2567        if !coefficient.re.is_finite() || !coefficient.im.is_finite() {
2568            return Err(QmbedError::InvalidOptions(
2569                "Hamiltonian scale must be finite".into(),
2570            ));
2571        }
2572        Self::new(
2573            self.static_part.scaled(coefficient)?,
2574            self.dynamic_parts
2575                .iter()
2576                .map(|component| DynamicComponent {
2577                    operator: component.operator.clone(),
2578                    drive: {
2579                        let drive = component.drive.clone();
2580                        Arc::new(move |time| coefficient * drive(time))
2581                    },
2582                })
2583                .collect(),
2584        )
2585    }
2586}
2587
2588fn finite_drive_value(time: f64, value: Complex64) -> Result<Complex64> {
2589    if !time.is_finite() || !value.re.is_finite() || !value.im.is_finite() {
2590        return Err(QmbedError::InvalidOptions(
2591            "drive time and coefficient must be finite".into(),
2592        ));
2593    }
2594    Ok(value)
2595}
2596
2597impl LinearOperator for Hamiltonian<Static> {
2598    fn shape(&self) -> (usize, usize) {
2599        self.static_part.shape()
2600    }
2601
2602    fn format(&self) -> MatrixFormat {
2603        self.static_part.format()
2604    }
2605
2606    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
2607        self.static_part.apply(input, output)
2608    }
2609
2610    fn apply_transpose(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
2611        self.static_part.apply_transpose(input, output)
2612    }
2613
2614    fn apply_adjoint(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
2615        self.static_part.apply_adjoint(input, output)
2616    }
2617
2618    fn stored_triplets(&self) -> Result<Option<Vec<(usize, usize, Complex64)>>> {
2619        self.static_part.stored_triplets()
2620    }
2621
2622    fn shifted_solver(&self, shift: f64) -> Result<Option<Box<dyn ShiftedLinearSolver>>> {
2623        self.static_part.shifted_solver(shift)
2624    }
2625}
2626
2627impl TimeDependentOperator for Hamiltonian<Static> {
2628    fn shape(&self) -> (usize, usize) {
2629        self.static_part.shape()
2630    }
2631
2632    fn apply_at(&self, time: f64, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
2633        if !time.is_finite() {
2634            return Err(QmbedError::InvalidOptions(
2635                "evaluation time must be finite".into(),
2636            ));
2637        }
2638        self.static_part.apply(input, output)
2639    }
2640}
2641
2642impl TimeDependentOperator for Hamiltonian<Dynamic> {
2643    fn shape(&self) -> (usize, usize) {
2644        self.static_part.shape()
2645    }
2646
2647    fn apply_at(&self, time: f64, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
2648        check_apply_shape(self.static_part.shape(), input, output)?;
2649        self.static_part.apply(input, output)?;
2650        let mut driven = vec![Complex64::new(0.0, 0.0); output.len()];
2651        for component in &self.dynamic_parts {
2652            let coefficient = finite_drive_value(time, (component.drive)(time))?;
2653            component.operator.apply(input, &mut driven)?;
2654            for (value, contribution) in output.iter_mut().zip(&driven) {
2655                *value += coefficient * *contribution;
2656            }
2657        }
2658        Ok(())
2659    }
2660}
2661
2662#[derive(Clone, Debug)]
2663pub struct QuantumComponent {
2664    name: String,
2665    operator: Operator,
2666    default: Option<Complex64>,
2667}
2668
2669impl QuantumComponent {
2670    pub fn required(name: impl Into<String>, operator: Operator) -> Self {
2671        Self {
2672            name: name.into(),
2673            operator,
2674            default: None,
2675        }
2676    }
2677
2678    pub fn with_default(
2679        name: impl Into<String>,
2680        operator: Operator,
2681        default: impl Into<Complex64>,
2682    ) -> Self {
2683        Self {
2684            name: name.into(),
2685            operator,
2686            default: Some(default.into()),
2687        }
2688    }
2689
2690    /// Python-compatible parameter component: an omitted parameter equals one.
2691    pub fn parameter(name: impl Into<String>, operator: Operator) -> Self {
2692        Self::with_default(name, operator, Complex64::new(1.0, 0.0))
2693    }
2694
2695    pub fn name(&self) -> &str {
2696        &self.name
2697    }
2698
2699    pub fn operator(&self) -> &Operator {
2700        &self.operator
2701    }
2702
2703    pub const fn default(&self) -> Option<Complex64> {
2704        self.default
2705    }
2706}
2707
2708/// Named linear combination of operator components.
2709#[derive(Clone, Debug)]
2710pub struct QuantumOperator {
2711    components: Vec<QuantumComponent>,
2712    shape: (usize, usize),
2713}
2714
2715impl QuantumOperator {
2716    /// Construct a named linear family after validating names and shapes.
2717    pub fn new(components: impl IntoIterator<Item = QuantumComponent>) -> Result<Self> {
2718        let components: Vec<_> = components.into_iter().collect();
2719        let first = components.first().ok_or_else(|| {
2720            QmbedError::InvalidOptions("QuantumOperator requires at least one component".into())
2721        })?;
2722        let shape = first.operator.shape();
2723        let mut names = std::collections::HashSet::new();
2724        for component in &components {
2725            if component.name.is_empty() || !names.insert(component.name.clone()) {
2726                return Err(QmbedError::InvalidOptions(
2727                    "component names must be nonempty and unique".into(),
2728                ));
2729            }
2730            if component.operator.shape() != shape {
2731                return Err(QmbedError::DimensionMismatch(
2732                    "all parameterized components must have equal shapes".into(),
2733                ));
2734            }
2735            if component
2736                .default
2737                .is_some_and(|value| !value.re.is_finite() || !value.im.is_finite())
2738            {
2739                return Err(QmbedError::InvalidOptions(
2740                    "component defaults must be finite".into(),
2741                ));
2742            }
2743        }
2744        Ok(Self { components, shape })
2745    }
2746
2747    /// Return the common rectangular shape of every component.
2748    pub const fn shape(&self) -> (usize, usize) {
2749        self.shape
2750    }
2751
2752    /// Iterate component names in stable coefficient order.
2753    pub fn component_names(&self) -> impl Iterator<Item = &str> {
2754        self.components
2755            .iter()
2756            .map(|component| component.name.as_str())
2757    }
2758
2759    /// Return every named component in stable coefficient order.
2760    pub fn components(&self) -> &[QuantumComponent] {
2761        &self.components
2762    }
2763
2764    /// Look up one named component or return an invalid-options error.
2765    pub fn component(&self, name: &str) -> Result<&Operator> {
2766        self.components
2767            .iter()
2768            .find(|component| component.name == name)
2769            .map(|component| &component.operator)
2770            .ok_or_else(|| {
2771                QmbedError::InvalidOptions(format!("unknown operator component {name:?}"))
2772            })
2773    }
2774
2775    /// Resolve named parameters into the stable component order returned by
2776    /// [`Self::component_names`].
2777    ///
2778    /// This is the allocation-light coefficient boundary used by dynamic
2779    /// frontends: names are validated once at the edge, while repeated
2780    /// operator applications consume a compact coefficient slice.
2781    pub fn resolve_coefficients(
2782        &self,
2783        parameters: &HashMap<String, Complex64>,
2784    ) -> Result<Vec<Complex64>> {
2785        if let Some(name) = parameters.keys().find(|name| {
2786            !self
2787                .components
2788                .iter()
2789                .any(|component| &component.name == *name)
2790        }) {
2791            return Err(QmbedError::InvalidOptions(format!(
2792                "unknown operator parameter {name:?}"
2793            )));
2794        }
2795        self.components
2796            .iter()
2797            .map(|component| {
2798                let coefficient = parameters
2799                    .get(&component.name)
2800                    .copied()
2801                    .or(component.default)
2802                    .ok_or_else(|| {
2803                        QmbedError::InvalidOptions(format!(
2804                            "missing required operator parameter {:?}",
2805                            component.name
2806                        ))
2807                    })?;
2808                if !coefficient.re.is_finite() || !coefficient.im.is_finite() {
2809                    return Err(QmbedError::InvalidOptions(format!(
2810                        "operator parameter {:?} must be finite",
2811                        component.name
2812                    )));
2813                }
2814                Ok(coefficient)
2815            })
2816            .collect()
2817    }
2818
2819    /// Apply a compact ordered coefficient vector without materializing the
2820    /// linear combination.
2821    pub fn apply_coefficients(
2822        &self,
2823        coefficients: &[Complex64],
2824        input: &[Complex64],
2825        output: &mut [Complex64],
2826    ) -> Result<()> {
2827        if coefficients.len() != self.components.len() {
2828            return Err(QmbedError::DimensionMismatch(format!(
2829                "received {} operator coefficients for {} components",
2830                coefficients.len(),
2831                self.components.len()
2832            )));
2833        }
2834        check_apply_shape(self.shape, input, output)?;
2835        output.fill(Complex64::new(0.0, 0.0));
2836        let mut contribution = vec![Complex64::new(0.0, 0.0); output.len()];
2837        for (component, coefficient) in self.components.iter().zip(coefficients) {
2838            if !coefficient.re.is_finite() || !coefficient.im.is_finite() {
2839                return Err(QmbedError::InvalidOptions(format!(
2840                    "operator parameter {:?} must be finite",
2841                    component.name
2842                )));
2843            }
2844            component.operator.apply(input, &mut contribution)?;
2845            for (value, addition) in output.iter_mut().zip(&contribution) {
2846                *value += *coefficient * *addition;
2847            }
2848        }
2849        Ok(())
2850    }
2851
2852    /// Apply one named member of the family without assembling a temporary
2853    /// sparse matrix.
2854    pub fn apply(
2855        &self,
2856        parameters: &HashMap<String, Complex64>,
2857        input: &[Complex64],
2858        output: &mut [Complex64],
2859    ) -> Result<()> {
2860        let coefficients = self.resolve_coefficients(parameters)?;
2861        self.apply_coefficients(&coefficients, input, output)
2862    }
2863
2864    /// Materialize one parameter value in the requested format.
2865    ///
2866    /// For repeated evaluations prefer [`QuantumOperator::plan`], which
2867    /// retains a common sparse pattern and updates only numeric values.
2868    pub fn evaluate(
2869        &self,
2870        parameters: &HashMap<String, Complex64>,
2871        format: MatrixFormat,
2872    ) -> Result<Operator> {
2873        let coefficients = self.resolve_coefficients(parameters)?;
2874        let mut entries = Vec::new();
2875        for (component, coefficient) in self.components.iter().zip(coefficients) {
2876            entries.extend(
2877                component
2878                    .operator
2879                    .triplets()
2880                    .into_iter()
2881                    .map(|(row, column, value)| (row, column, coefficient * value)),
2882            );
2883        }
2884        Operator::from_triplets(self.shape.0, self.shape.1, entries, format)
2885    }
2886
2887    /// Prepare a fixed sparse pattern for repeated evaluations of this family.
2888    ///
2889    /// The plan aligns every component on one union pattern, reuses all index
2890    /// arrays, and mutates only a preallocated value buffer. It supports
2891    /// dense, CSC, CSR, DIA, and matrix-free output without a model-specific
2892    /// fast path.
2893    pub fn plan(&self, format: MatrixFormat) -> Result<QuantumOperatorPlan> {
2894        QuantumOperatorPlan::new(self, format)
2895    }
2896
2897    pub fn scaled(&self, coefficient: impl Into<Complex64>) -> Result<Self> {
2898        let coefficient = coefficient.into();
2899        let mut components = Vec::with_capacity(self.components.len());
2900        for component in &self.components {
2901            components.push(QuantumComponent {
2902                name: component.name.clone(),
2903                operator: component.operator.scaled(coefficient)?,
2904                default: component.default,
2905            });
2906        }
2907        Self::new(components)
2908    }
2909
2910    pub fn conjugated(&self) -> Result<Self> {
2911        let mut components = Vec::with_capacity(self.components.len());
2912        for component in &self.components {
2913            components.push(QuantumComponent {
2914                name: component.name.clone(),
2915                operator: component.operator.conjugated()?,
2916                default: component.default.map(|value| value.conj()),
2917            });
2918        }
2919        Self::new(components)
2920    }
2921
2922    pub fn add(&self, right: &Self) -> Result<Self> {
2923        if self.shape != right.shape {
2924            return Err(QmbedError::DimensionMismatch(
2925                "parameterized operators must have equal shapes".into(),
2926            ));
2927        }
2928        let mut components = self.components.clone();
2929        for right_component in &right.components {
2930            if let Some(left_component) = components
2931                .iter_mut()
2932                .find(|component| component.name == right_component.name)
2933            {
2934                left_component.operator = left_component.operator.add(&right_component.operator)?;
2935                if left_component.default != right_component.default {
2936                    left_component.default = None;
2937                }
2938            } else {
2939                components.push(right_component.clone());
2940            }
2941        }
2942        Self::new(components)
2943    }
2944
2945    pub fn adjoint(&self) -> Result<Self> {
2946        let mut components = Vec::with_capacity(self.components.len());
2947        for component in &self.components {
2948            components.push(QuantumComponent {
2949                name: component.name.clone(),
2950                operator: component.operator.adjoint()?,
2951                default: component.default.map(|value| value.conj()),
2952            });
2953        }
2954        Self::new(components)
2955    }
2956
2957    pub fn transpose(&self) -> Result<Self> {
2958        let mut components = Vec::with_capacity(self.components.len());
2959        for component in &self.components {
2960            components.push(QuantumComponent {
2961                name: component.name.clone(),
2962                operator: component.operator.transpose()?,
2963                default: component.default,
2964            });
2965        }
2966        Self::new(components)
2967    }
2968}
2969
2970/// Fixed-pattern materialization of a [`QuantumOperator`].
2971///
2972/// Construction aligns every component with the union sparsity pattern once.
2973/// Subsequent evaluations update only numerical values, preserving the sparse
2974/// indices and any backend analysis that depends on them.
2975#[derive(Clone, Debug)]
2976pub struct QuantumOperatorPlan {
2977    component_names: Vec<String>,
2978    defaults: Vec<Option<Complex64>>,
2979    component_values: Vec<Vec<Complex64>>,
2980    combined_values: Vec<Complex64>,
2981    operator: Operator,
2982}
2983
2984impl QuantumOperatorPlan {
2985    fn new(family: &QuantumOperator, format: MatrixFormat) -> Result<Self> {
2986        let mut entries = HashMap::<(usize, usize), Vec<Complex64>>::new();
2987        for (component_index, component) in family.components.iter().enumerate() {
2988            for (row, column, value) in component.operator.triplets() {
2989                entries
2990                    .entry((row, column))
2991                    .or_insert_with(|| vec![Complex64::new(0.0, 0.0); family.components.len()])
2992                    [component_index] += value;
2993            }
2994        }
2995        let mut coordinates: Vec<_> = entries.keys().copied().collect();
2996        match format {
2997            MatrixFormat::Csc => coordinates.sort_by_key(|&(row, column)| (column, row)),
2998            MatrixFormat::Csr | MatrixFormat::MatrixFree | MatrixFormat::Dia => {
2999                coordinates.sort_unstable()
3000            }
3001            MatrixFormat::Dense => {}
3002        }
3003
3004        let components = family.components.len();
3005        let zero = Complex64::new(0.0, 0.0);
3006        let (storage, component_values) = match format {
3007            MatrixFormat::Dense => {
3008                let slots = family.shape.0.saturating_mul(family.shape.1);
3009                let mut values = vec![vec![zero; slots]; components];
3010                for (&(row, column), aligned) in &entries {
3011                    let slot = row * family.shape.1 + column;
3012                    for (component, value) in aligned.iter().enumerate() {
3013                        values[component][slot] = *value;
3014                    }
3015                }
3016                (Storage::Dense(vec![zero; slots]), values)
3017            }
3018            MatrixFormat::Csc => {
3019                let mut column_offsets = vec![0usize; family.shape.1 + 1];
3020                let mut row_indices = Vec::with_capacity(coordinates.len());
3021                let mut values = vec![vec![zero; coordinates.len()]; components];
3022                for (slot, &(row, column)) in coordinates.iter().enumerate() {
3023                    column_offsets[column + 1] += 1;
3024                    row_indices.push(row);
3025                    for (component, value) in entries[&(row, column)].iter().enumerate() {
3026                        values[component][slot] = *value;
3027                    }
3028                }
3029                for column in 0..family.shape.1 {
3030                    column_offsets[column + 1] += column_offsets[column];
3031                }
3032                (
3033                    Storage::Csc {
3034                        column_offsets,
3035                        row_indices,
3036                        values: vec![zero; coordinates.len()],
3037                    },
3038                    values,
3039                )
3040            }
3041            MatrixFormat::Csr => {
3042                let mut row_offsets = vec![0usize; family.shape.0 + 1];
3043                let mut column_indices = Vec::with_capacity(coordinates.len());
3044                let mut values = vec![vec![zero; coordinates.len()]; components];
3045                for (slot, &(row, column)) in coordinates.iter().enumerate() {
3046                    row_offsets[row + 1] += 1;
3047                    column_indices.push(column);
3048                    for (component, value) in entries[&(row, column)].iter().enumerate() {
3049                        values[component][slot] = *value;
3050                    }
3051                }
3052                for row in 0..family.shape.0 {
3053                    row_offsets[row + 1] += row_offsets[row];
3054                }
3055                (
3056                    Storage::Csr {
3057                        row_offsets,
3058                        column_indices,
3059                        values: vec![zero; coordinates.len()],
3060                    },
3061                    values,
3062                )
3063            }
3064            MatrixFormat::MatrixFree => {
3065                let mut values = vec![vec![zero; coordinates.len()]; components];
3066                let pattern = coordinates
3067                    .iter()
3068                    .enumerate()
3069                    .map(|(slot, &(row, column))| {
3070                        for (component, value) in entries[&(row, column)].iter().enumerate() {
3071                            values[component][slot] = *value;
3072                        }
3073                        Triplet {
3074                            row,
3075                            column,
3076                            value: zero,
3077                        }
3078                    })
3079                    .collect();
3080                (Storage::MatrixFree(pattern), values)
3081            }
3082            MatrixFormat::Dia => {
3083                let mut offsets: Vec<_> = coordinates
3084                    .iter()
3085                    .map(|&(row, column)| row as isize - column as isize)
3086                    .collect();
3087                offsets.sort_unstable();
3088                offsets.dedup();
3089                let slots = offsets.len().saturating_mul(family.shape.0);
3090                let mut values = vec![vec![zero; slots]; components];
3091                for (&(row, column), aligned) in &entries {
3092                    let offset = row as isize - column as isize;
3093                    let diagonal = offsets.binary_search(&offset).map_err(|_| {
3094                        QmbedError::InternalState(
3095                            "parameterized diagonal pattern lost an offset".into(),
3096                        )
3097                    })?;
3098                    let slot = diagonal * family.shape.0 + row;
3099                    for (component, value) in aligned.iter().enumerate() {
3100                        values[component][slot] = *value;
3101                    }
3102                }
3103                (
3104                    Storage::Dia {
3105                        offsets,
3106                        values_by_row: vec![zero; slots],
3107                    },
3108                    values,
3109                )
3110            }
3111        };
3112        let combined_values =
3113            vec![Complex64::new(0.0, 0.0); component_values.first().map_or(0, Vec::len)];
3114        Ok(Self {
3115            component_names: family
3116                .components
3117                .iter()
3118                .map(|component| component.name.clone())
3119                .collect(),
3120            defaults: family
3121                .components
3122                .iter()
3123                .map(|component| component.default)
3124                .collect(),
3125            component_values,
3126            combined_values,
3127            operator: Operator {
3128                shape: family.shape,
3129                format,
3130                real: true,
3131                storage,
3132            },
3133        })
3134    }
3135
3136    /// Iterate component names in the coefficient order accepted by the plan.
3137    pub fn component_names(&self) -> impl Iterator<Item = &str> {
3138        self.component_names.iter().map(String::as_str)
3139    }
3140
3141    /// Borrow the most recently evaluated operator and its stable storage.
3142    pub fn operator(&self) -> &Operator {
3143        &self.operator
3144    }
3145
3146    /// Resolve a parameter map into the plan's stable coefficient order.
3147    pub fn resolve_coefficients(
3148        &self,
3149        parameters: &HashMap<String, Complex64>,
3150    ) -> Result<Vec<Complex64>> {
3151        if let Some(name) = parameters
3152            .keys()
3153            .find(|name| !self.component_names.contains(name))
3154        {
3155            return Err(QmbedError::InvalidOptions(format!(
3156                "unknown operator parameter {name:?}"
3157            )));
3158        }
3159        self.component_names
3160            .iter()
3161            .zip(&self.defaults)
3162            .map(|(name, default)| {
3163                let coefficient = parameters.get(name).copied().or(*default).ok_or_else(|| {
3164                    QmbedError::InvalidOptions(format!(
3165                        "missing required operator parameter {name:?}"
3166                    ))
3167                })?;
3168                if !coefficient.re.is_finite() || !coefficient.im.is_finite() {
3169                    return Err(QmbedError::InvalidOptions(format!(
3170                        "operator parameter {name:?} must be finite"
3171                    )));
3172                }
3173                Ok(coefficient)
3174            })
3175            .collect()
3176    }
3177
3178    /// Evaluate named parameters while reusing the plan's storage pattern.
3179    pub fn evaluate(&mut self, parameters: &HashMap<String, Complex64>) -> Result<&Operator> {
3180        let coefficients = self.resolve_coefficients(parameters)?;
3181        self.evaluate_coefficients(&coefficients)
3182    }
3183
3184    /// Evaluate coefficients already arranged in [`Self::component_names`] order.
3185    ///
3186    /// This is the lowest-overhead scan path: it performs no name lookup and
3187    /// allocates no matrix structure.
3188    pub fn evaluate_coefficients(&mut self, coefficients: &[Complex64]) -> Result<&Operator> {
3189        if coefficients.len() != self.component_values.len() {
3190            return Err(QmbedError::DimensionMismatch(
3191                "parameter coefficients have the wrong length".into(),
3192            ));
3193        }
3194        if coefficients
3195            .iter()
3196            .any(|value| !value.re.is_finite() || !value.im.is_finite())
3197        {
3198            return Err(QmbedError::InvalidOptions(
3199                "parameter coefficients must be finite".into(),
3200            ));
3201        }
3202        self.combined_values.fill(Complex64::new(0.0, 0.0));
3203        for (coefficient, values) in coefficients.iter().zip(&self.component_values) {
3204            for (target, value) in self.combined_values.iter_mut().zip(values) {
3205                *target += *coefficient * *value;
3206            }
3207        }
3208        match &mut self.operator.storage {
3209            Storage::Dense(values) | Storage::Csc { values, .. } | Storage::Csr { values, .. } => {
3210                values.copy_from_slice(&self.combined_values)
3211            }
3212            Storage::Dia { values_by_row, .. } => {
3213                values_by_row.copy_from_slice(&self.combined_values)
3214            }
3215            Storage::MatrixFree(entries) => {
3216                for (entry, value) in entries.iter_mut().zip(&self.combined_values) {
3217                    entry.value = *value;
3218                }
3219            }
3220        }
3221        self.operator.real = storage_is_real(&self.operator.storage);
3222        Ok(&self.operator)
3223    }
3224}
3225
3226/// Fixed matrix-free operator with a mutable diagonal correction.
3227#[derive(Clone, Debug)]
3228pub struct QuantumLinearOperator {
3229    operator: Operator,
3230    diagonal: Vec<Complex64>,
3231}
3232
3233impl QuantumLinearOperator {
3234    pub fn new(operator: Operator, diagonal: Vec<Complex64>) -> Result<Self> {
3235        let shape = operator.shape();
3236        if shape.0 != shape.1 || diagonal.len() != shape.0 {
3237            return Err(QmbedError::DimensionMismatch(
3238                "QuantumLinearOperator needs a square operator and one diagonal value per row"
3239                    .into(),
3240            ));
3241        }
3242        if diagonal
3243            .iter()
3244            .any(|value| !value.re.is_finite() || !value.im.is_finite())
3245        {
3246            return Err(QmbedError::InvalidOptions(
3247                "QuantumLinearOperator diagonal values must be finite".into(),
3248            ));
3249        }
3250        Ok(Self { operator, diagonal })
3251    }
3252
3253    pub fn from_operator(operator: Operator) -> Result<Self> {
3254        let dimension = operator.shape().0;
3255        Self::new(operator, vec![Complex64::new(0.0, 0.0); dimension])
3256    }
3257
3258    pub fn operator(&self) -> &Operator {
3259        &self.operator
3260    }
3261
3262    pub fn diagonal_correction(&self) -> &[Complex64] {
3263        &self.diagonal
3264    }
3265
3266    pub fn set_diagonal(&mut self, diagonal: Vec<Complex64>) -> Result<()> {
3267        if diagonal.len() != self.diagonal.len()
3268            || diagonal
3269                .iter()
3270                .any(|value| !value.re.is_finite() || !value.im.is_finite())
3271        {
3272            return Err(QmbedError::DimensionMismatch(
3273                "replacement diagonal has the wrong length or non-finite values".into(),
3274            ));
3275        }
3276        self.diagonal = diagonal;
3277        Ok(())
3278    }
3279
3280    pub fn materialize(&self, format: MatrixFormat) -> Result<Operator> {
3281        let dimension = self.diagonal.len();
3282        let entries = self.operator.triplets().into_iter().chain(
3283            self.diagonal
3284                .iter()
3285                .copied()
3286                .enumerate()
3287                .filter_map(|(index, value)| {
3288                    (value.norm() > f64::EPSILON).then_some((index, index, value))
3289                }),
3290        );
3291        Operator::from_triplets(dimension, dimension, entries, format)
3292    }
3293
3294    pub fn adjoint(&self) -> Result<Self> {
3295        Self::new(
3296            self.operator.adjoint()?,
3297            self.diagonal.iter().map(|value| value.conj()).collect(),
3298        )
3299    }
3300
3301    pub fn transpose(&self) -> Result<Self> {
3302        Self::new(self.operator.transpose()?, self.diagonal.clone())
3303    }
3304
3305    pub fn conjugated(&self) -> Result<Self> {
3306        Self::new(
3307            self.operator.conjugated()?,
3308            self.diagonal.iter().map(|value| value.conj()).collect(),
3309        )
3310    }
3311
3312    pub fn right_apply(&self, input: &[Complex64]) -> Result<Vec<Complex64>> {
3313        let mut output = vec![Complex64::new(0.0, 0.0); self.shape().1];
3314        self.apply_transpose(input, &mut output)?;
3315        Ok(output)
3316    }
3317}
3318
3319impl LinearOperator for QuantumLinearOperator {
3320    fn shape(&self) -> (usize, usize) {
3321        self.operator.shape()
3322    }
3323
3324    fn format(&self) -> MatrixFormat {
3325        MatrixFormat::MatrixFree
3326    }
3327
3328    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
3329        self.operator.apply(input, output)?;
3330        for ((value, diagonal), input_value) in output.iter_mut().zip(&self.diagonal).zip(input) {
3331            *value += *diagonal * *input_value;
3332        }
3333        Ok(())
3334    }
3335
3336    fn apply_transpose(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
3337        self.operator.apply_transpose(input, output)?;
3338        for ((value, diagonal), input_value) in output.iter_mut().zip(&self.diagonal).zip(input) {
3339            *value += *diagonal * *input_value;
3340        }
3341        Ok(())
3342    }
3343
3344    fn stored_triplets(&self) -> Result<Option<Vec<(usize, usize, Complex64)>>> {
3345        Ok(Some(self.materialize(MatrixFormat::Csc)?.triplets()))
3346    }
3347
3348    fn shifted_solver(&self, shift: f64) -> Result<Option<Box<dyn ShiftedLinearSolver>>> {
3349        self.materialize(MatrixFormat::Csc)?.shifted_solver(shift)
3350    }
3351}
3352
3353pub fn commutator(left: &Operator, right: &Operator) -> Result<Operator> {
3354    left.product(right)?.subtract(&right.product(left)?)
3355}
3356
3357pub fn anticommutator(left: &Operator, right: &Operator) -> Result<Operator> {
3358    left.product(right)?.add(&right.product(left)?)
3359}
3360
3361/// Immutable real grid multiplying an [`ExpOp`] exponent.
3362#[derive(Clone, Copy, Debug, PartialEq)]
3363pub struct ExpGrid {
3364    pub start: f64,
3365    pub stop: f64,
3366    pub points: usize,
3367    pub endpoint: bool,
3368}
3369
3370impl ExpGrid {
3371    pub fn new(start: f64, stop: f64, points: usize, endpoint: bool) -> Result<Self> {
3372        if !start.is_finite() || !stop.is_finite() || points == 0 {
3373            return Err(QmbedError::InvalidOptions(
3374                "exponential grid endpoints must be finite and points must be positive".into(),
3375            ));
3376        }
3377        Ok(Self {
3378            start,
3379            stop,
3380            points,
3381            endpoint,
3382        })
3383    }
3384
3385    pub fn values(&self) -> Vec<f64> {
3386        if self.points == 1 {
3387            return vec![self.start];
3388        }
3389        let intervals = if self.endpoint {
3390            self.points - 1
3391        } else {
3392            self.points
3393        };
3394        let step = (self.stop - self.start) / intervals as f64;
3395        (0..self.points)
3396            .map(|index| self.start + index as f64 * step)
3397            .collect()
3398    }
3399}
3400
3401/// Lazy state iterator over an exponential grid.
3402pub struct ExpOpGridIter {
3403    operator: ExpOp,
3404    input: Vec<Complex64>,
3405    scales: std::vec::IntoIter<f64>,
3406}
3407
3408impl Iterator for ExpOpGridIter {
3409    type Item = Result<Vec<Complex64>>;
3410
3411    fn next(&mut self) -> Option<Self::Item> {
3412        self.scales.next().map(|scale| {
3413            let mut operator = self.operator.clone();
3414            operator.set_exponent(scale * self.operator.exponent)?;
3415            let mut output = vec![Complex64::new(0.0, 0.0); self.input.len()];
3416            operator.apply(&self.input, &mut output)?;
3417            Ok(output)
3418        })
3419    }
3420}
3421
3422/// Matrix-free exponential `exp(exponent * A)`.
3423#[derive(Clone)]
3424pub struct ExpOp {
3425    operator: Arc<dyn LinearOperator>,
3426    exponent: Complex64,
3427    krylov_dimension: usize,
3428    tolerance: f64,
3429    max_substeps: usize,
3430    plan: crate::solve::ExpmActionPlan,
3431}
3432
3433impl std::fmt::Debug for ExpOp {
3434    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3435        formatter
3436            .debug_struct("ExpOp")
3437            .field("shape", &self.operator.shape())
3438            .field("exponent", &self.exponent)
3439            .field("krylov_dimension", &self.krylov_dimension)
3440            .field("tolerance", &self.tolerance)
3441            .field("max_substeps", &self.max_substeps)
3442            .finish()
3443    }
3444}
3445
3446impl ExpOp {
3447    pub fn new(
3448        operator: Arc<dyn LinearOperator>,
3449        exponent: Complex64,
3450        krylov_dimension: usize,
3451        tolerance: f64,
3452        max_substeps: usize,
3453    ) -> Result<Self> {
3454        let shape = operator.shape();
3455        if shape.0 != shape.1 {
3456            return Err(QmbedError::DimensionMismatch(
3457                "ExpOp requires a square operator".into(),
3458            ));
3459        }
3460        if !exponent.re.is_finite()
3461            || !exponent.im.is_finite()
3462            || krylov_dimension == 0
3463            || !tolerance.is_finite()
3464            || tolerance <= 0.0
3465            || max_substeps == 0
3466        {
3467            return Err(QmbedError::InvalidOptions(
3468                "invalid ExpOp coefficient or numerical controls".into(),
3469            ));
3470        }
3471        let plan = crate::solve::ExpmActionPlan::new(
3472            operator.as_ref(),
3473            exponent,
3474            krylov_dimension,
3475            tolerance,
3476            max_substeps,
3477        )?;
3478        Ok(Self {
3479            operator,
3480            exponent,
3481            krylov_dimension,
3482            tolerance,
3483            max_substeps,
3484            plan,
3485        })
3486    }
3487
3488    pub const fn exponent(&self) -> Complex64 {
3489        self.exponent
3490    }
3491
3492    pub fn generator(&self) -> &Arc<dyn LinearOperator> {
3493        &self.operator
3494    }
3495
3496    pub fn set_exponent(&mut self, exponent: Complex64) -> Result<()> {
3497        if !exponent.re.is_finite() || !exponent.im.is_finite() {
3498            return Err(QmbedError::InvalidOptions(
3499                "ExpOp coefficient must be finite".into(),
3500            ));
3501        }
3502        self.plan = crate::solve::ExpmActionPlan::new(
3503            self.operator.as_ref(),
3504            exponent,
3505            self.krylov_dimension,
3506            self.tolerance,
3507            self.max_substeps,
3508        )?;
3509        self.exponent = exponent;
3510        Ok(())
3511    }
3512
3513    pub fn sandwich(&self, state: &[Complex64]) -> Result<Complex64> {
3514        let mut output = vec![Complex64::new(0.0, 0.0); state.len()];
3515        self.apply(state, &mut output)?;
3516        Ok(state
3517            .iter()
3518            .zip(output)
3519            .map(|(left, right)| left.conj() * right)
3520            .sum())
3521    }
3522
3523    pub fn matrix(&self, format: MatrixFormat) -> Result<Operator> {
3524        let shape = self.shape();
3525        let mut input = vec![Complex64::new(0.0, 0.0); shape.1];
3526        let mut output = vec![Complex64::new(0.0, 0.0); shape.0];
3527        let mut triplets = Vec::new();
3528        for column in 0..shape.1 {
3529            input.fill(Complex64::new(0.0, 0.0));
3530            input[column] = Complex64::new(1.0, 0.0);
3531            self.apply(&input, &mut output)?;
3532            for (row, value) in output.iter().copied().enumerate() {
3533                if value.norm() > f64::EPSILON {
3534                    triplets.push((row, column, value));
3535                }
3536            }
3537        }
3538        Operator::from_triplets(shape.0, shape.1, triplets, format)
3539    }
3540
3541    pub fn iter_grid(&self, input: &[Complex64], grid: ExpGrid) -> Result<ExpOpGridIter> {
3542        if input.len() != self.shape().1 {
3543            return Err(QmbedError::DimensionMismatch(
3544                "ExpOp grid input must match the operator dimension".into(),
3545            ));
3546        }
3547        Ok(ExpOpGridIter {
3548            operator: self.clone(),
3549            input: input.to_vec(),
3550            scales: grid.values().into_iter(),
3551        })
3552    }
3553
3554    pub fn apply_grid(&self, input: &[Complex64], grid: ExpGrid) -> Result<Vec<Vec<Complex64>>> {
3555        self.iter_grid(input, grid)?.collect()
3556    }
3557
3558    pub fn transpose(&self) -> Result<Self> {
3559        Self::new(
3560            Arc::new(TransposedLinearOperator::new(self.operator.clone())),
3561            self.exponent,
3562            self.krylov_dimension,
3563            self.tolerance,
3564            self.max_substeps,
3565        )
3566    }
3567
3568    pub fn conjugated(&self) -> Result<Self> {
3569        Self::new(
3570            Arc::new(ConjugatedLinearOperator::new(self.operator.clone())),
3571            self.exponent.conj(),
3572            self.krylov_dimension,
3573            self.tolerance,
3574            self.max_substeps,
3575        )
3576    }
3577
3578    pub fn adjoint(&self) -> Result<Self> {
3579        Self::new(
3580            Arc::new(AdjointLinearOperator::new(self.operator.clone())),
3581            self.exponent.conj(),
3582            self.krylov_dimension,
3583            self.tolerance,
3584            self.max_substeps,
3585        )
3586    }
3587
3588    pub fn right_apply(&self, input: &[Complex64]) -> Result<Vec<Complex64>> {
3589        if input.len() != self.shape().0 {
3590            return Err(QmbedError::DimensionMismatch(
3591                "ExpOp right-apply input must match the operator dimension".into(),
3592            ));
3593        }
3594        let mut output = vec![Complex64::new(0.0, 0.0); self.shape().1];
3595        self.apply_transpose(input, &mut output)?;
3596        Ok(output)
3597    }
3598}
3599
3600impl LinearOperator for ExpOp {
3601    fn shape(&self) -> (usize, usize) {
3602        self.operator.shape()
3603    }
3604
3605    fn format(&self) -> MatrixFormat {
3606        MatrixFormat::MatrixFree
3607    }
3608
3609    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
3610        check_apply_shape(self.shape(), input, output)?;
3611        let result = self.plan.apply(self.operator.as_ref(), input)?;
3612        output.copy_from_slice(&result);
3613        Ok(())
3614    }
3615
3616    fn apply_transpose(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
3617        check_apply_shape((self.shape().1, self.shape().0), input, output)?;
3618        let transpose = TransposedLinearOperator::new(self.operator.clone());
3619        let result = crate::solve::expm_action_complex(
3620            &transpose,
3621            input,
3622            self.exponent,
3623            self.krylov_dimension,
3624            self.tolerance,
3625            self.max_substeps,
3626        )?;
3627        output.copy_from_slice(&result);
3628        Ok(())
3629    }
3630}
3631
3632pub fn is_exp_op(value: &dyn std::any::Any) -> bool {
3633    value.is::<ExpOp>()
3634}
3635
3636pub fn is_hamiltonian(value: &dyn std::any::Any) -> bool {
3637    value.is::<Hamiltonian<Static>>() || value.is::<Hamiltonian<Dynamic>>()
3638}
3639
3640pub fn is_quantum_operator(value: &dyn std::any::Any) -> bool {
3641    value.is::<QuantumOperator>()
3642}
3643
3644pub fn is_quantum_linear_operator(value: &dyn std::any::Any) -> bool {
3645    value.is::<QuantumLinearOperator>()
3646}