Skip to main content

qmbed/
interop.rs

1//! Runtime-owned exact-diagonalization model shared by language frontends.
2//!
3//! The native generic API remains the zero-cost path. This module provides a
4//! small owned narrow waist for frontends that select a packed basis at
5//! runtime and need to reuse one mathematical model across materialization and
6//! solver operations.
7
8use std::collections::{HashMap, HashSet};
9use std::hash::Hash;
10use std::sync::{Arc, Mutex};
11
12use num_complex::Complex64;
13
14use crate::archive::OperatorArchive;
15use crate::basis::{Basis, BasisProjector, PackedBasis, ReductionImage, WidePackedBasis};
16use crate::block::{BlockOps, ProjectedBlockOps};
17use crate::operator::{
18    AssemblyChecks, BraKetTransition, LinearOperator, MatrixFormat, Operator, OperatorBuilder,
19    OperatorSpec, QuantumComponent, QuantumOperator, TimeOperator, apply_sector_shift,
20};
21use crate::solve::{
22    Eigensystem, EighOptions, EigshOptions, EvolutionOptions, StateBatchTrajectory,
23    eigh_with_options, eigsh, eigsh_with_initial, evolve_batch as evolve_operator_batch,
24    evolve_time_dependent_batch_from,
25};
26use crate::{QmbedError, Result};
27
28/// One owned basis and operator specification reusable across frontend calls.
29#[derive(Clone, Debug)]
30pub struct EdModel<B> {
31    basis: B,
32    terms: Vec<OperatorSpec>,
33    components: Vec<PackedTermComponent>,
34    checks: AssemblyChecks,
35    site_permutation: Option<Vec<usize>>,
36    operators: Arc<Mutex<HashMap<MatrixFormat, Arc<Operator>>>>,
37    operator_families: Arc<Mutex<HashMap<MatrixFormat, Arc<PackedOperatorModel>>>>,
38}
39
40pub type PackedEdModel = EdModel<PackedBasis>;
41pub type WideEdModel = EdModel<WidePackedBasis>;
42
43/// Algebraic view used when applying a temporary operator.
44#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
45pub enum OperatorAction {
46    #[default]
47    Normal,
48    Transpose,
49    Conjugate,
50    Adjoint,
51}
52
53/// One named operator component expressed in the local terms of a packed
54/// basis.
55///
56/// The component stays as a basis-level specification until a storage format
57/// is requested. This lets the same parameterized model reuse the universal
58/// [`OperatorBuilder`] for dense, sparse, and matrix-free execution instead of
59/// introducing a frontend-specific dynamic-Hamiltonian path.
60#[derive(Clone, Debug)]
61pub struct PackedTermComponent {
62    name: String,
63    terms: Vec<OperatorSpec>,
64    default: Option<Complex64>,
65}
66
67impl PackedTermComponent {
68    /// Construct a component whose coefficient must be supplied by name.
69    pub fn required(
70        name: impl Into<String>,
71        terms: impl IntoIterator<Item = OperatorSpec>,
72    ) -> Self {
73        Self {
74            name: name.into(),
75            terms: terms.into_iter().collect(),
76            default: None,
77        }
78    }
79
80    /// Construct a component with a finite default coefficient.
81    pub fn with_default(
82        name: impl Into<String>,
83        terms: impl IntoIterator<Item = OperatorSpec>,
84        default: impl Into<Complex64>,
85    ) -> Self {
86        Self {
87            name: name.into(),
88            terms: terms.into_iter().collect(),
89            default: Some(default.into()),
90        }
91    }
92
93    /// Python-compatible parameter component: an omitted coefficient equals
94    /// one.
95    pub fn parameter(
96        name: impl Into<String>,
97        terms: impl IntoIterator<Item = OperatorSpec>,
98    ) -> Self {
99        Self::with_default(name, terms, Complex64::new(1.0, 0.0))
100    }
101
102    pub fn name(&self) -> &str {
103        &self.name
104    }
105
106    pub fn terms(&self) -> &[OperatorSpec] {
107        &self.terms
108    }
109
110    pub const fn default(&self) -> Option<Complex64> {
111        self.default
112    }
113}
114
115/// Runtime-owned finite operator family independent of any local-basis
116/// representation.
117///
118/// This is the language-neutral model for operators supplied directly as
119/// dense or sparse matrices. A fixed part may be combined with named
120/// parameterized components, and every frontend operation evaluates the same
121/// family before applying, materializing, or solving it. Basis-aware local
122/// term assembly remains in [`PackedEdModel`].
123#[derive(Clone, Debug)]
124pub struct PackedOperatorModel {
125    static_part: Operator,
126    parameterized_part: Option<QuantumOperator>,
127}
128
129impl PackedOperatorModel {
130    /// Construct a fixed square operator model.
131    pub fn new(static_part: Operator) -> Result<Self> {
132        let shape = static_part.shape();
133        if shape.0 != shape.1 {
134            return Err(QmbedError::DimensionMismatch(
135                "a persistent operator model must be square".into(),
136            ));
137        }
138        Ok(Self {
139            static_part,
140            parameterized_part: None,
141        })
142    }
143
144    /// Construct a named operator family with an explicit fixed part.
145    pub fn with_components(
146        static_part: Operator,
147        components: impl IntoIterator<Item = QuantumComponent>,
148    ) -> Result<Self> {
149        let mut model = Self::new(static_part)?;
150        let parameterized_part = QuantumOperator::new(components)?;
151        if parameterized_part.shape() != model.static_part.shape() {
152            return Err(QmbedError::DimensionMismatch(
153                "fixed and parameterized operators must have equal shapes".into(),
154            ));
155        }
156        model.parameterized_part = Some(parameterized_part);
157        Ok(model)
158    }
159
160    /// Construct a purely parameterized operator family.
161    pub fn parameterized(
162        components: impl IntoIterator<Item = QuantumComponent>,
163        format: MatrixFormat,
164    ) -> Result<Self> {
165        let parameterized_part = QuantumOperator::new(components)?;
166        let shape = parameterized_part.shape();
167        let static_part = Operator::from_triplets(shape.0, shape.1, std::iter::empty(), format)?;
168        Ok(Self {
169            static_part,
170            parameterized_part: Some(parameterized_part),
171        })
172    }
173
174    pub fn dimension(&self) -> usize {
175        self.static_part.shape().0
176    }
177
178    pub fn component_names(&self) -> impl Iterator<Item = &str> {
179        self.parameterized_part
180            .iter()
181            .flat_map(QuantumOperator::component_names)
182    }
183
184    /// Return one named component without evaluating the fixed part.
185    pub fn component_operator(&self, name: &str, format: MatrixFormat) -> Result<Operator> {
186        let parameterized = self.parameterized_part.as_ref().ok_or_else(|| {
187            QmbedError::InvalidOptions(format!("fixed operator model has no component {name:?}"))
188        })?;
189        parameterized.component(name)?.converted(format)
190    }
191
192    /// Export the named part of this family as a storage-preserving archive.
193    ///
194    /// Component formats may be selected independently. Names omitted from
195    /// `formats` retain their current representation. Unknown names are
196    /// rejected so a frontend typo cannot silently produce a different
197    /// archive layout.
198    pub fn component_archive(
199        &self,
200        formats: &HashMap<String, MatrixFormat>,
201    ) -> Result<OperatorArchive> {
202        if !self.static_part.triplets().is_empty() {
203            return Err(QmbedError::InvalidOptions(
204                "the named-component archive cannot omit a nonzero fixed operator".into(),
205            ));
206        }
207        let parameterized = self.parameterized_part.as_ref().ok_or_else(|| {
208            QmbedError::InvalidOptions(
209                "an operator archive requires at least one named component".into(),
210            )
211        })?;
212        if let Some(name) = formats.keys().find(|name| {
213            !parameterized
214                .component_names()
215                .any(|candidate| candidate == name.as_str())
216        }) {
217            return Err(QmbedError::InvalidOptions(format!(
218                "unknown operator component format {name:?}"
219            )));
220        }
221        let mut archive = OperatorArchive::new();
222        for component in parameterized.components() {
223            let operator = match formats.get(component.name()) {
224                Some(format) => component.operator().converted(*format)?,
225                None => component.operator().clone(),
226            };
227            archive.insert(component.name(), operator, component.default())?;
228        }
229        Ok(archive)
230    }
231
232    /// Reconstruct a basis-independent parameterized family from an archive.
233    ///
234    /// Archived component formats are retained. The fixed part is the exact
235    /// zero operator because [`OperatorArchive`] represents named families;
236    /// fixed-plus-parameterized snapshots use a separate model archive
237    /// contract.
238    pub fn from_component_archive(
239        archive: OperatorArchive,
240        static_format: MatrixFormat,
241    ) -> Result<Self> {
242        let operator = archive.into_quantum_operator()?;
243        Self::parameterized(operator.components().iter().cloned(), static_format)
244    }
245
246    /// Project the fixed part and every named component through the same
247    /// rectangular map, preserving component names and default coefficients.
248    pub fn projected_by(&self, projector: &Operator) -> Result<Self> {
249        let static_part = self.static_part.projected_by(projector)?;
250        let Some(parameterized) = &self.parameterized_part else {
251            return Self::new(static_part);
252        };
253        let components = parameterized
254            .components()
255            .iter()
256            .map(|component| {
257                let operator = component.operator().projected_by(projector)?;
258                Ok(match component.default() {
259                    Some(default) => {
260                        QuantumComponent::with_default(component.name(), operator, default)
261                    }
262                    None => QuantumComponent::required(component.name(), operator),
263                })
264            })
265            .collect::<Result<Vec<_>>>()?;
266        Self::with_components(static_part, components)
267    }
268
269    fn combine_block_families<F>(
270        blocks: Vec<Self>,
271        lift_operators: F,
272        format: MatrixFormat,
273    ) -> Result<Self>
274    where
275        F: Fn(Vec<Operator>) -> Result<Operator>,
276    {
277        if blocks.is_empty() {
278            return Err(QmbedError::InvalidOptions(
279                "a block family requires at least one sector model".into(),
280            ));
281        }
282        let static_part = lift_operators(
283            blocks
284                .iter()
285                .map(|model| model.static_part.clone())
286                .collect(),
287        )?;
288
289        let mut names = Vec::<String>::new();
290        let mut defaults = HashMap::<String, Option<Complex64>>::new();
291        for model in &blocks {
292            let Some(parameterized) = &model.parameterized_part else {
293                continue;
294            };
295            for component in parameterized.components() {
296                match defaults.get(component.name()) {
297                    Some(existing) if *existing != component.default() => {
298                        return Err(QmbedError::InvalidOptions(format!(
299                            "block component {:?} has inconsistent defaults",
300                            component.name()
301                        )));
302                    }
303                    Some(_) => {}
304                    None => {
305                        names.push(component.name().to_owned());
306                        defaults.insert(component.name().to_owned(), component.default());
307                    }
308                }
309            }
310        }
311        if names.is_empty() {
312            return Self::new(static_part);
313        }
314
315        let components = names
316            .into_iter()
317            .map(|name| {
318                let operators = blocks
319                    .iter()
320                    .map(|model| {
321                        model
322                            .parameterized_part
323                            .as_ref()
324                            .and_then(|operator| {
325                                operator
326                                    .components()
327                                    .iter()
328                                    .find(|component| component.name() == name)
329                            })
330                            .map(|component| component.operator().clone())
331                            .map_or_else(
332                                || {
333                                    Operator::from_triplets(
334                                        model.dimension(),
335                                        model.dimension(),
336                                        std::iter::empty(),
337                                        format,
338                                    )
339                                },
340                                Ok,
341                            )
342                    })
343                    .collect::<Result<Vec<_>>>()?;
344                let operator = lift_operators(operators)?;
345                Ok(match defaults[&name] {
346                    Some(default) => QuantumComponent::with_default(name, operator, default),
347                    None => QuantumComponent::required(name, operator),
348                })
349            })
350            .collect::<Result<Vec<_>>>()?;
351        Self::with_components(static_part, components)
352    }
353
354    /// Assemble the direct sum of independent fixed or parameterized models.
355    ///
356    /// Named components are combined by name and retain one shared coefficient
357    /// contract, so the result can be evaluated, evolved, or exponentiated
358    /// through the ordinary persistent-model API.
359    pub fn from_blocks(
360        blocks: impl IntoIterator<Item = Self>,
361        format: MatrixFormat,
362    ) -> Result<Self> {
363        let blocks: Vec<_> = blocks.into_iter().collect();
364        Self::combine_block_families(
365            blocks,
366            |operators| {
367                let operators = operators
368                    .into_iter()
369                    .map(|operator| Arc::new(operator) as Arc<dyn LinearOperator>)
370                    .collect::<Vec<_>>();
371                BlockOps::new(operators)?.materialize(format)
372            },
373            format,
374        )
375    }
376
377    /// Assemble a shared parent-space family from independently reduced
378    /// sector models and their embedding projectors.
379    ///
380    /// The result preserves the fixed part and the union of named components.
381    /// Components with the same name are lifted from every sector into one
382    /// parent-space operator; a sector which does not define that name
383    /// contributes an exact zero block. Defaults for a shared name must agree,
384    /// because one parameter value controls that physical component across all
385    /// sectors.
386    pub fn from_projected_blocks(
387        blocks: impl IntoIterator<Item = (Self, Operator)>,
388        tolerance: f64,
389        format: MatrixFormat,
390    ) -> Result<Self> {
391        let blocks: Vec<_> = blocks.into_iter().collect();
392        if blocks.is_empty() {
393            return Err(QmbedError::InvalidOptions(
394                "a projected block family requires at least one sector model".into(),
395            ));
396        }
397        let projectors: Vec<_> = blocks
398            .iter()
399            .map(|(_, projector)| Arc::new(projector.clone()) as Arc<dyn LinearOperator>)
400            .collect();
401        let models = blocks.into_iter().map(|(model, _)| model).collect();
402        Self::combine_block_families(
403            models,
404            |operators| {
405                let operators = operators
406                    .into_iter()
407                    .map(|operator| Arc::new(operator) as Arc<dyn LinearOperator>)
408                    .collect::<Vec<_>>();
409                ProjectedBlockOps::new(operators, projectors.clone(), tolerance)?
410                    .materialize(format)
411            },
412            format,
413        )
414    }
415
416    pub fn materialize(
417        &self,
418        parameters: &HashMap<String, Complex64>,
419        format: MatrixFormat,
420    ) -> Result<Operator> {
421        let fixed = self.static_part.converted(format)?;
422        match &self.parameterized_part {
423            Some(parameterized) => fixed.add(&parameterized.evaluate(parameters, format)?),
424            None if parameters.is_empty() => Ok(fixed),
425            None => {
426                let name = parameters.keys().next().expect("nonempty map was checked");
427                Err(QmbedError::InvalidOptions(format!(
428                    "unknown operator parameter {name:?}"
429                )))
430            }
431        }
432    }
433
434    pub fn apply_batch(
435        &self,
436        parameters: &HashMap<String, Complex64>,
437        inputs: &[Vec<Complex64>],
438        action: OperatorAction,
439    ) -> Result<Vec<Vec<Complex64>>> {
440        if action != OperatorAction::Normal {
441            let operator = self.materialize(parameters, MatrixFormat::MatrixFree)?;
442            return apply_operator_batch(&operator, inputs, action);
443        }
444        inputs
445            .iter()
446            .map(|input| {
447                let mut output = vec![Complex64::new(0.0, 0.0); self.dimension()];
448                self.apply(parameters, input, &mut output)?;
449                Ok(output)
450            })
451            .collect()
452    }
453
454    /// Apply one evaluated family member directly, without assembling its
455    /// weighted sparse sum.
456    pub fn apply(
457        &self,
458        parameters: &HashMap<String, Complex64>,
459        input: &[Complex64],
460        output: &mut [Complex64],
461    ) -> Result<()> {
462        let Some(parameterized) = &self.parameterized_part else {
463            if let Some(name) = parameters.keys().next() {
464                return Err(QmbedError::InvalidOptions(format!(
465                    "unknown operator parameter {name:?}"
466                )));
467            }
468            self.static_part.apply(input, output)?;
469            return Ok(());
470        };
471        let coefficients = parameterized.resolve_coefficients(parameters)?;
472        self.apply_coefficients(&coefficients, input, output)
473    }
474
475    /// Apply ordered component coefficients directly. The order is exactly
476    /// [`Self::component_names`].
477    pub fn apply_coefficients(
478        &self,
479        coefficients: &[Complex64],
480        input: &[Complex64],
481        output: &mut [Complex64],
482    ) -> Result<()> {
483        self.static_part.apply(input, output)?;
484        let Some(parameterized) = &self.parameterized_part else {
485            if coefficients.is_empty() {
486                return Ok(());
487            }
488            return Err(QmbedError::DimensionMismatch(
489                "a fixed operator model accepts no component coefficients".into(),
490            ));
491        };
492        let mut contribution = vec![Complex64::new(0.0, 0.0); output.len()];
493        parameterized.apply_coefficients(coefficients, input, &mut contribution)?;
494        for (value, addition) in output.iter_mut().zip(contribution) {
495            *value += addition;
496        }
497        Ok(())
498    }
499
500    /// Construct a matrix-free time operator whose callback fills coefficients
501    /// in the stable component order.
502    pub fn time_operator<F>(&self, coefficients_at: F) -> Result<TimeOperator>
503    where
504        F: Fn(f64, &mut [Complex64]) -> Result<()> + Send + Sync + 'static,
505    {
506        self.time_operator_scaled(Complex64::new(1.0, 0.0), coefficients_at)
507    }
508
509    /// Construct a scaled matrix-free time operator. The scale applies to the
510    /// complete fixed-plus-parameterized family.
511    pub fn time_operator_scaled<F>(
512        &self,
513        operator_scale: Complex64,
514        coefficients_at: F,
515    ) -> Result<TimeOperator>
516    where
517        F: Fn(f64, &mut [Complex64]) -> Result<()> + Send + Sync + 'static,
518    {
519        if !operator_scale.re.is_finite() || !operator_scale.im.is_finite() {
520            return Err(QmbedError::InvalidOptions(
521                "time-dependent operator scale must be finite".into(),
522            ));
523        }
524        let component_count = self.component_names().count();
525        if component_count == 0 {
526            return Err(QmbedError::InvalidOptions(
527                "time-dependent evolution requires at least one operator component".into(),
528            ));
529        }
530        let model = self.clone();
531        TimeOperator::new(
532            (self.dimension(), self.dimension()),
533            move |time, input, output| {
534                let mut coefficients = vec![Complex64::new(f64::NAN, f64::NAN); component_count];
535                coefficients_at(time, &mut coefficients)?;
536                if coefficients
537                    .iter()
538                    .any(|value| !value.re.is_finite() || !value.im.is_finite())
539                {
540                    return Err(QmbedError::InvalidOptions(format!(
541                        "time-dependent coefficient callback did not return {component_count} finite values"
542                    )));
543                }
544                model.apply_coefficients(&coefficients, input, output)?;
545                for value in output {
546                    *value *= operator_scale;
547                }
548                Ok(())
549            },
550        )
551    }
552
553    /// Evolve a batch while evaluating named component coefficients at the
554    /// integrator's internal physical times.
555    pub fn evolve_time_dependent_batch<F>(
556        &self,
557        initial_columns: &[Vec<Complex64>],
558        initial_time: f64,
559        options: EvolutionOptions,
560        coefficients_at: F,
561    ) -> Result<StateBatchTrajectory>
562    where
563        F: Fn(f64, &mut [Complex64]) -> Result<()> + Send + Sync + 'static,
564    {
565        self.evolve_time_dependent_batch_scaled(
566            initial_columns,
567            initial_time,
568            options,
569            Complex64::new(1.0, 0.0),
570            coefficients_at,
571        )
572    }
573
574    pub fn evolve_time_dependent_batch_scaled<F>(
575        &self,
576        initial_columns: &[Vec<Complex64>],
577        initial_time: f64,
578        options: EvolutionOptions,
579        operator_scale: Complex64,
580        coefficients_at: F,
581    ) -> Result<StateBatchTrajectory>
582    where
583        F: Fn(f64, &mut [Complex64]) -> Result<()> + Send + Sync + 'static,
584    {
585        let operator = self.time_operator_scaled(operator_scale, coefficients_at)?;
586        evolve_time_dependent_batch_from(&operator, initial_columns, initial_time, options)
587    }
588
589    pub fn eigh(
590        &self,
591        parameters: &HashMap<String, Complex64>,
592        options: EighOptions,
593    ) -> Result<Eigensystem> {
594        let operator = self.materialize(parameters, MatrixFormat::Dense)?;
595        if !operator.is_hermitian(1.0e-12) {
596            return Err(QmbedError::NonHermitian);
597        }
598        eigh_with_options(&operator, options)
599    }
600
601    pub fn eigsh(
602        &self,
603        parameters: &HashMap<String, Complex64>,
604        format: MatrixFormat,
605        options: EigshOptions,
606    ) -> Result<Eigensystem> {
607        let operator = self.materialize(parameters, format)?;
608        if !operator.is_hermitian(1.0e-12) {
609            return Err(QmbedError::NonHermitian);
610        }
611        eigsh(&operator, options)
612    }
613
614    pub fn eigsh_with_initial(
615        &self,
616        parameters: &HashMap<String, Complex64>,
617        format: MatrixFormat,
618        options: EigshOptions,
619        initial: &[Complex64],
620    ) -> Result<Eigensystem> {
621        let operator = self.materialize(parameters, format)?;
622        if !operator.is_hermitian(1.0e-12) {
623            return Err(QmbedError::NonHermitian);
624        }
625        eigsh_with_initial(&operator, options, initial)
626    }
627
628    pub fn evolve_batch(
629        &self,
630        parameters: &HashMap<String, Complex64>,
631        initial_columns: &[Vec<Complex64>],
632        options: EvolutionOptions,
633    ) -> Result<StateBatchTrajectory> {
634        let operator = self.materialize(parameters, MatrixFormat::MatrixFree)?;
635        if options.hamiltonian && !operator.is_hermitian(1.0e-12) {
636            return Err(QmbedError::NonHermitian);
637        }
638        evolve_operator_batch(&operator, initial_columns, options)
639    }
640}
641
642impl<B> EdModel<B>
643where
644    B: Basis + Clone,
645    B::State: Hash + Ord + 'static,
646{
647    pub fn new(basis: impl Into<B>, terms: impl IntoIterator<Item = OperatorSpec>) -> Self {
648        Self {
649            basis: basis.into(),
650            terms: terms.into_iter().collect(),
651            components: Vec::new(),
652            checks: AssemblyChecks::all(),
653            site_permutation: None,
654            operators: Arc::new(Mutex::new(HashMap::new())),
655            operator_families: Arc::new(Mutex::new(HashMap::new())),
656        }
657    }
658
659    pub fn with_checks(mut self, checks: AssemblyChecks) -> Self {
660        self.checks = checks;
661        self.operators = Arc::new(Mutex::new(HashMap::new()));
662        self.operator_families = Arc::new(Mutex::new(HashMap::new()));
663        self
664    }
665
666    /// Add named local-term components to this basis-owned model.
667    ///
668    /// Names and defaults are validated before any operator assembly. The
669    /// components may be non-Hermitian individually; Hermiticity is checked
670    /// after evaluating a concrete parameter set.
671    pub fn with_components(
672        mut self,
673        components: impl IntoIterator<Item = PackedTermComponent>,
674    ) -> Result<Self> {
675        let components: Vec<_> = components.into_iter().collect();
676        validate_term_components(&components)?;
677        self.components = components;
678        self.operator_families = Arc::new(Mutex::new(HashMap::new()));
679        Ok(self)
680    }
681
682    pub fn with_site_permutation(mut self, permutation: &[usize]) -> Result<Self> {
683        validate_site_permutation(permutation)?;
684        self.terms = self
685            .terms
686            .iter()
687            .map(|term| term.with_site_permutation(permutation))
688            .collect::<Result<Vec<_>>>()?;
689        self.components = self
690            .components
691            .into_iter()
692            .map(|mut component| {
693                component.terms = component
694                    .terms
695                    .iter()
696                    .map(|term| term.with_site_permutation(permutation))
697                    .collect::<Result<Vec<_>>>()?;
698                Ok(component)
699            })
700            .collect::<Result<Vec<_>>>()?;
701        self.site_permutation = Some(match self.site_permutation {
702            Some(previous) => previous.into_iter().map(|site| permutation[site]).collect(),
703            None => permutation.to_vec(),
704        });
705        self.operators = Arc::new(Mutex::new(HashMap::new()));
706        self.operator_families = Arc::new(Mutex::new(HashMap::new()));
707        Ok(self)
708    }
709
710    pub const fn basis(&self) -> &B {
711        &self.basis
712    }
713
714    pub fn terms(&self) -> &[OperatorSpec] {
715        &self.terms
716    }
717
718    pub fn components(&self) -> &[PackedTermComponent] {
719        &self.components
720    }
721
722    pub fn component_names(&self) -> impl Iterator<Item = &str> {
723        self.components.iter().map(PackedTermComponent::name)
724    }
725
726    pub fn dimension(&self) -> usize {
727        self.basis.len()
728    }
729
730    pub fn states(&self) -> Result<Vec<B::State>> {
731        (0..self.basis.len())
732            .map(|index| self.basis.state(index))
733            .collect()
734    }
735
736    /// Scatter a basis-ordered vector into packed-state index order.
737    ///
738    /// Explicit tensor-product parents may expose a frontend-compatible row
739    /// ordering rather than ascending packed states. Subsystem kernels use the
740    /// packed state itself as the mixed-radix row index, so this method makes
741    /// that conversion explicit and reusable.
742    pub fn scatter_state_vector(
743        &self,
744        values: &[Complex64],
745        full_dimension: usize,
746    ) -> Result<Vec<Complex64>>
747    where
748        usize: TryFrom<B::State>,
749    {
750        if values.len() != self.dimension() {
751            return Err(QmbedError::DimensionMismatch(
752                "state vector does not match the basis dimension".into(),
753            ));
754        }
755        let mut scattered = vec![Complex64::new(0.0, 0.0); full_dimension];
756        for (row, value) in values.iter().enumerate() {
757            let state = usize::try_from(self.basis.state(row)?).map_err(|_| {
758                QmbedError::DimensionMismatch(
759                    "packed state does not fit the tensor-product index space".into(),
760                )
761            })?;
762            if state >= full_dimension {
763                return Err(QmbedError::DimensionMismatch(
764                    "packed state exceeds the tensor-product index space".into(),
765                ));
766            }
767            scattered[state] = *value;
768        }
769        Ok(scattered)
770    }
771
772    /// Scatter both axes of a row-major density matrix by packed state.
773    pub fn scatter_density(
774        &self,
775        values: &[Complex64],
776        full_dimension: usize,
777    ) -> Result<Vec<Complex64>>
778    where
779        usize: TryFrom<B::State>,
780    {
781        let dimension = self.dimension();
782        if values.len() != dimension.saturating_mul(dimension) {
783            return Err(QmbedError::DimensionMismatch(
784                "density matrix does not match the basis dimension".into(),
785            ));
786        }
787        let mut indices = Vec::with_capacity(dimension);
788        for state in self.states()? {
789            let state = usize::try_from(state).map_err(|_| {
790                QmbedError::DimensionMismatch(
791                    "packed state does not fit the tensor-product index space".into(),
792                )
793            })?;
794            if state >= full_dimension {
795                return Err(QmbedError::DimensionMismatch(
796                    "packed state exceeds the tensor-product index space".into(),
797                ));
798            }
799            indices.push(state);
800        }
801        let mut scattered =
802            vec![Complex64::new(0.0, 0.0); full_dimension.saturating_mul(full_dimension)];
803        for row in 0..dimension {
804            for column in 0..dimension {
805                scattered[indices[row] * full_dimension + indices[column]] =
806                    values[row * dimension + column];
807            }
808        }
809        Ok(scattered)
810    }
811
812    /// Query canonical representatives and normalized orbit coefficients.
813    ///
814    /// This works for both explicit and symmetry-reduced bases and does not
815    /// materialize an operator or projector.
816    pub fn reduction_images(
817        &self,
818        states: &[B::State],
819    ) -> Result<Vec<Option<ReductionImage<B::State>>>> {
820        states
821            .iter()
822            .map(|&state| self.basis.reduction_image(state))
823            .collect()
824    }
825
826    /// Build the sparse isometry from this model's basis into an explicit
827    /// parent model's basis.
828    ///
829    /// The parent is deliberately explicit: frontends may choose either a
830    /// particle-conserving parent or the unrestricted physical Hilbert space
831    /// without encoding either policy in the Rust core.
832    pub fn projector_to(&self, parent: &Self) -> Result<BasisProjector> {
833        self.ensure_same_site_convention(parent)?;
834        BasisProjector::between(&self.basis, &parent.basis)
835    }
836
837    /// Build a one-hot embedding when this basis is an explicitly selected
838    /// subset of a parent that uses the same physical state identifiers.
839    ///
840    /// Unlike [`PackedEdModel::projector_to`], this does not apply symmetry
841    /// orbit amplitudes. Frontends use it for filters such as a fixed total
842    /// excitation inside an otherwise identical product basis.
843    pub fn embedding_to(&self, parent: &Self) -> Result<BasisProjector> {
844        self.ensure_same_site_convention(parent)?;
845        BasisProjector::from_embedding(&self.basis, &parent.basis)
846    }
847
848    /// Lift a batch of reduced-space vectors into an explicit parent model.
849    pub fn lift_to_batch(
850        &self,
851        parent: &Self,
852        vectors: &[Vec<Complex64>],
853    ) -> Result<Vec<Vec<Complex64>>> {
854        self.projector_to(parent)?.lift_batch(vectors)
855    }
856
857    /// Project a batch of parent-space vectors into this model's basis.
858    pub fn project_from_batch(
859        &self,
860        parent: &Self,
861        vectors: &[Vec<Complex64>],
862    ) -> Result<Vec<Vec<Complex64>>> {
863        self.projector_to(parent)?.project_batch(vectors)
864    }
865
866    /// Apply temporary terms directly from a source model into this target
867    /// model without materializing either physical parent space.
868    pub fn apply_terms_from_batch(
869        &self,
870        source: &Self,
871        terms: impl IntoIterator<Item = OperatorSpec>,
872        inputs: &[Vec<Complex64>],
873    ) -> Result<Vec<Vec<Complex64>>> {
874        self.ensure_same_site_convention(source)?;
875        let terms = self.prepare_terms(terms)?;
876        inputs
877            .iter()
878            .map(|input| {
879                let mut output = vec![Complex64::new(0.0, 0.0); self.dimension()];
880                apply_sector_shift(&source.basis, &self.basis, &terms, input, &mut output)?;
881                Ok(output)
882            })
883            .collect()
884    }
885
886    /// Apply temporary local terms between arbitrary isometric subspaces.
887    ///
888    /// `source` and `target` own the physical local algebras. Optional
889    /// projectors describe reduced coordinates inside those explicit parent
890    /// bases. Omitting a projector selects the entire corresponding parent
891    /// basis, so this one operation covers ordinary sector shifts,
892    /// reduced-to-full, full-to-reduced, and reduced-to-reduced actions:
893    ///
894    /// `output = P_target† O P_source input`.
895    ///
896    /// The local operator is streamed through [`PackedEdModel::apply_terms_from_batch`];
897    /// neither a square parent-space operator nor a dense projector product is
898    /// materialized.
899    pub fn apply_terms_between_subspaces_batch(
900        source: &Self,
901        source_projector: Option<&BasisProjector>,
902        target: &Self,
903        target_projector: Option<&BasisProjector>,
904        terms: impl IntoIterator<Item = OperatorSpec>,
905        inputs: &[Vec<Complex64>],
906    ) -> Result<Vec<Vec<Complex64>>> {
907        if source_projector
908            .is_some_and(|projector| projector.source_dimension() != source.dimension())
909        {
910            return Err(QmbedError::DimensionMismatch(
911                "source projector rows do not match the source parent basis".into(),
912            ));
913        }
914        if target_projector
915            .is_some_and(|projector| projector.source_dimension() != target.dimension())
916        {
917            return Err(QmbedError::DimensionMismatch(
918                "target projector rows do not match the target parent basis".into(),
919            ));
920        }
921        let lifted;
922        let parent_inputs = match source_projector {
923            Some(projector) => {
924                lifted = projector.lift_batch(inputs)?;
925                lifted.as_slice()
926            }
927            None => inputs,
928        };
929        let parent_outputs = target.apply_terms_from_batch(source, terms, parent_inputs)?;
930        match target_projector {
931            Some(projector) => projector.project_batch(&parent_outputs),
932            None => Ok(parent_outputs),
933        }
934    }
935
936    /// Return the assembled operator shared by all calls using this model.
937    ///
938    /// Assembly is performed at most once per storage format. Clones of an
939    /// unchanged model share the same cache; model-transforming builders reset
940    /// it before changing checks or site labels.
941    pub fn materialized(&self, format: MatrixFormat) -> Result<Arc<Operator>> {
942        let mut operators = self.operators.lock().map_err(|_| {
943            QmbedError::InternalState("materialized-operator cache lock is poisoned".into())
944        })?;
945        if let Some(operator) = operators.get(&format) {
946            return Ok(Arc::clone(operator));
947        }
948        let operator = Arc::new(
949            OperatorBuilder::on(&self.basis)
950                .terms(self.terms.clone())
951                .checks(self.checks)
952                .build(format)?,
953        );
954        operators.insert(format, Arc::clone(&operator));
955        Ok(operator)
956    }
957
958    /// Materialize an owned operator for callers that do not need reuse.
959    pub fn materialize(&self, format: MatrixFormat) -> Result<Operator> {
960        self.materialize_with(&HashMap::new(), format)
961    }
962
963    /// Evaluate this basis-owned fixed/parameterized operator family.
964    pub fn materialize_with(
965        &self,
966        parameters: &HashMap<String, Complex64>,
967        format: MatrixFormat,
968    ) -> Result<Operator> {
969        if self.components.is_empty() {
970            if let Some(name) = parameters.keys().next() {
971                return Err(QmbedError::InvalidOptions(format!(
972                    "unknown operator parameter {name:?}"
973                )));
974            }
975            return Ok((*self.materialized(format)?).clone());
976        }
977        self.operator_family(format)?
978            .materialize(parameters, format)
979    }
980
981    /// Assemble caller-supplied terms on this model's already-owned basis.
982    ///
983    /// This is the native narrow waist for low-level basis operations. The
984    /// terms use the model's original site convention and are relabeled by the
985    /// same permutation as its persistent terms.
986    pub fn assemble_terms(
987        &self,
988        terms: impl IntoIterator<Item = OperatorSpec>,
989        checks: AssemblyChecks,
990        format: MatrixFormat,
991    ) -> Result<Operator> {
992        OperatorBuilder::on(&self.basis)
993            .terms(self.prepare_terms(terms)?)
994            .checks(checks)
995            .build(format)
996    }
997
998    /// Apply one temporary operator to a batch of column vectors.
999    ///
1000    /// The operator is assembled once for the whole batch and never converted
1001    /// to a dense matrix.
1002    pub fn apply_terms_batch(
1003        &self,
1004        terms: impl IntoIterator<Item = OperatorSpec>,
1005        inputs: &[Vec<Complex64>],
1006        action: OperatorAction,
1007    ) -> Result<Vec<Vec<Complex64>>> {
1008        let operator =
1009            self.assemble_terms(terms, AssemblyChecks::none(), MatrixFormat::MatrixFree)?;
1010        apply_operator_batch(&operator, inputs, action)
1011    }
1012
1013    /// Apply the model's persistent terms without dense materialization.
1014    ///
1015    /// The matrix-free representation is cached after the first call and
1016    /// reused by subsequent vectors and algebraic views.
1017    pub fn apply_batch(
1018        &self,
1019        inputs: &[Vec<Complex64>],
1020        action: OperatorAction,
1021    ) -> Result<Vec<Vec<Complex64>>> {
1022        self.apply_batch_with(&HashMap::new(), inputs, action)
1023    }
1024
1025    /// Apply one evaluated member of this basis-owned operator family.
1026    pub fn apply_batch_with(
1027        &self,
1028        parameters: &HashMap<String, Complex64>,
1029        inputs: &[Vec<Complex64>],
1030        action: OperatorAction,
1031    ) -> Result<Vec<Vec<Complex64>>> {
1032        if self.components.is_empty() {
1033            if let Some(name) = parameters.keys().next() {
1034                return Err(QmbedError::InvalidOptions(format!(
1035                    "unknown operator parameter {name:?}"
1036                )));
1037            }
1038            let operator = self.materialized(MatrixFormat::MatrixFree)?;
1039            return apply_operator_batch(operator.as_ref(), inputs, action);
1040        }
1041        self.operator_family(MatrixFormat::MatrixFree)?
1042            .apply_batch(parameters, inputs, action)
1043    }
1044
1045    /// Return raw local transitions grouped by input ket.
1046    ///
1047    /// Unlike square operator assembly, this operation intentionally does not
1048    /// reduce destination states into the model's symmetry sector.
1049    pub fn bra_ket_terms(
1050        &self,
1051        terms: impl IntoIterator<Item = OperatorSpec>,
1052        kets: &[B::State],
1053    ) -> Result<Vec<Vec<BraKetTransition<B::State>>>> {
1054        let terms = self.prepare_terms(terms)?;
1055        kets.iter()
1056            .copied()
1057            .map(|ket| {
1058                let mut transitions = Vec::new();
1059                for term in &terms {
1060                    for coupling in term.couplings() {
1061                        self.basis.visit_preparsed_local_unreduced_transitions(
1062                            ket,
1063                            term.operator(),
1064                            term.symbols(),
1065                            term.split(),
1066                            &coupling.sites,
1067                            |bra, amplitude| {
1068                                let matrix_element = coupling.coefficient * amplitude;
1069                                if matrix_element.norm() > f64::EPSILON {
1070                                    transitions.push(BraKetTransition {
1071                                        bra,
1072                                        ket,
1073                                        matrix_element,
1074                                    });
1075                                }
1076                                Ok(())
1077                            },
1078                        )?;
1079                    }
1080                }
1081                Ok(transitions)
1082            })
1083            .collect()
1084    }
1085
1086    pub fn eigh(&self, options: EighOptions) -> Result<Eigensystem> {
1087        self.eigh_with(&HashMap::new(), options)
1088    }
1089
1090    pub fn eigh_with(
1091        &self,
1092        parameters: &HashMap<String, Complex64>,
1093        options: EighOptions,
1094    ) -> Result<Eigensystem> {
1095        if self.components.is_empty() {
1096            if let Some(name) = parameters.keys().next() {
1097                return Err(QmbedError::InvalidOptions(format!(
1098                    "unknown operator parameter {name:?}"
1099                )));
1100            }
1101            let operator = self.materialized(MatrixFormat::Dense)?;
1102            return eigh_with_options(operator.as_ref(), options);
1103        }
1104        self.operator_family(MatrixFormat::Dense)?
1105            .eigh(parameters, options)
1106    }
1107
1108    pub fn eigsh(&self, format: MatrixFormat, options: EigshOptions) -> Result<Eigensystem> {
1109        self.eigsh_with(&HashMap::new(), format, options)
1110    }
1111
1112    pub fn eigsh_with(
1113        &self,
1114        parameters: &HashMap<String, Complex64>,
1115        format: MatrixFormat,
1116        options: EigshOptions,
1117    ) -> Result<Eigensystem> {
1118        self.eigsh_with_optional_initial(parameters, format, options, None)
1119    }
1120
1121    pub fn eigsh_with_initial(
1122        &self,
1123        format: MatrixFormat,
1124        options: EigshOptions,
1125        initial: &[Complex64],
1126    ) -> Result<Eigensystem> {
1127        self.eigsh_with_initial_and_parameters(&HashMap::new(), format, options, initial)
1128    }
1129
1130    pub fn eigsh_with_initial_and_parameters(
1131        &self,
1132        parameters: &HashMap<String, Complex64>,
1133        format: MatrixFormat,
1134        options: EigshOptions,
1135        initial: &[Complex64],
1136    ) -> Result<Eigensystem> {
1137        self.eigsh_with_optional_initial(parameters, format, options, Some(initial))
1138    }
1139
1140    fn eigsh_with_optional_initial(
1141        &self,
1142        parameters: &HashMap<String, Complex64>,
1143        format: MatrixFormat,
1144        options: EigshOptions,
1145        initial: Option<&[Complex64]>,
1146    ) -> Result<Eigensystem> {
1147        if self.components.is_empty() {
1148            if let Some(name) = parameters.keys().next() {
1149                return Err(QmbedError::InvalidOptions(format!(
1150                    "unknown operator parameter {name:?}"
1151                )));
1152            }
1153            let operator = self.materialized(format)?;
1154            return match initial {
1155                Some(initial) => eigsh_with_initial(operator.as_ref(), options, initial),
1156                None => eigsh(operator.as_ref(), options),
1157            };
1158        }
1159        let family = self.operator_family(format)?;
1160        match initial {
1161            Some(initial) => family.eigsh_with_initial(parameters, format, options, initial),
1162            None => family.eigsh(parameters, format, options),
1163        }
1164    }
1165
1166    /// Evolve independent state columns under this model's static Hamiltonian.
1167    ///
1168    /// The matrix-free operator is cached and shared with normal operator
1169    /// applications. Numerical evolution remains a Rust-core capability; the
1170    /// language bindings only convert arrays and time grids.
1171    pub fn evolve_batch(
1172        &self,
1173        initial_columns: &[Vec<Complex64>],
1174        options: EvolutionOptions,
1175    ) -> Result<StateBatchTrajectory> {
1176        self.evolve_batch_with(&HashMap::new(), initial_columns, options)
1177    }
1178
1179    pub fn evolve_batch_with(
1180        &self,
1181        parameters: &HashMap<String, Complex64>,
1182        initial_columns: &[Vec<Complex64>],
1183        options: EvolutionOptions,
1184    ) -> Result<StateBatchTrajectory> {
1185        if self.components.is_empty() {
1186            if let Some(name) = parameters.keys().next() {
1187                return Err(QmbedError::InvalidOptions(format!(
1188                    "unknown operator parameter {name:?}"
1189                )));
1190            }
1191            let operator = self.materialized(MatrixFormat::MatrixFree)?;
1192            return evolve_operator_batch(operator.as_ref(), initial_columns, options);
1193        }
1194        self.operator_family(MatrixFormat::MatrixFree)?
1195            .evolve_batch(parameters, initial_columns, options)
1196    }
1197
1198    /// Convert this basis-owned model into a basis-independent operator family
1199    /// without losing named components or their defaults.
1200    pub fn operator_model(&self, format: MatrixFormat) -> Result<PackedOperatorModel> {
1201        if self.components.is_empty() {
1202            return PackedOperatorModel::new(self.materialized(format)?.as_ref().clone());
1203        }
1204        Ok(self.operator_family(format)?.as_ref().clone())
1205    }
1206
1207    /// Evolve this basis-owned parameterized family with coefficients supplied
1208    /// at the integrator's internal physical times.
1209    pub fn evolve_time_dependent_batch<F>(
1210        &self,
1211        initial_columns: &[Vec<Complex64>],
1212        initial_time: f64,
1213        options: EvolutionOptions,
1214        coefficients_at: F,
1215    ) -> Result<StateBatchTrajectory>
1216    where
1217        F: Fn(f64, &mut [Complex64]) -> Result<()> + Send + Sync + 'static,
1218    {
1219        self.evolve_time_dependent_batch_scaled(
1220            initial_columns,
1221            initial_time,
1222            options,
1223            Complex64::new(1.0, 0.0),
1224            coefficients_at,
1225        )
1226    }
1227
1228    pub fn evolve_time_dependent_batch_scaled<F>(
1229        &self,
1230        initial_columns: &[Vec<Complex64>],
1231        initial_time: f64,
1232        options: EvolutionOptions,
1233        operator_scale: Complex64,
1234        coefficients_at: F,
1235    ) -> Result<StateBatchTrajectory>
1236    where
1237        F: Fn(f64, &mut [Complex64]) -> Result<()> + Send + Sync + 'static,
1238    {
1239        self.operator_family(MatrixFormat::MatrixFree)?
1240            .evolve_time_dependent_batch_scaled(
1241                initial_columns,
1242                initial_time,
1243                options,
1244                operator_scale,
1245                coefficients_at,
1246            )
1247    }
1248
1249    fn operator_family(&self, format: MatrixFormat) -> Result<Arc<PackedOperatorModel>> {
1250        let mut families = self.operator_families.lock().map_err(|_| {
1251            QmbedError::InternalState("parameterized-operator cache lock is poisoned".into())
1252        })?;
1253        if let Some(family) = families.get(&format) {
1254            return Ok(Arc::clone(family));
1255        }
1256        let component_checks = AssemblyChecks {
1257            hermiticity: false,
1258            particle_conservation: self.checks.particle_conservation,
1259            symmetry_compatibility: self.checks.symmetry_compatibility,
1260        };
1261        let components = self
1262            .components
1263            .iter()
1264            .map(|component| {
1265                let operator = OperatorBuilder::on(&self.basis)
1266                    .terms(component.terms.clone())
1267                    .checks(component_checks)
1268                    .build(format)?;
1269                Ok(match component.default {
1270                    Some(default) => {
1271                        QuantumComponent::with_default(component.name.clone(), operator, default)
1272                    }
1273                    None => QuantumComponent::required(component.name.clone(), operator),
1274                })
1275            })
1276            .collect::<Result<Vec<_>>>()?;
1277        let family = Arc::new(PackedOperatorModel::with_components(
1278            (*self.materialized(format)?).clone(),
1279            components,
1280        )?);
1281        families.insert(format, Arc::clone(&family));
1282        Ok(family)
1283    }
1284
1285    fn prepare_terms(
1286        &self,
1287        terms: impl IntoIterator<Item = OperatorSpec>,
1288    ) -> Result<Vec<OperatorSpec>> {
1289        let terms = terms.into_iter();
1290        match &self.site_permutation {
1291            Some(permutation) => terms
1292                .map(|term| term.with_site_permutation(permutation))
1293                .collect(),
1294            None => Ok(terms.collect()),
1295        }
1296    }
1297
1298    fn ensure_same_site_convention(&self, other: &Self) -> Result<()> {
1299        if self.site_permutation != other.site_permutation {
1300            return Err(QmbedError::InvalidOptions(
1301                "models must use the same site permutation for cross-basis operations".into(),
1302            ));
1303        }
1304        Ok(())
1305    }
1306}
1307
1308fn validate_term_components(components: &[PackedTermComponent]) -> Result<()> {
1309    let mut names = HashSet::new();
1310    for component in components {
1311        if component.name.is_empty() || !names.insert(component.name.clone()) {
1312            return Err(QmbedError::InvalidOptions(
1313                "component names must be nonempty and unique".into(),
1314            ));
1315        }
1316        if component
1317            .default
1318            .is_some_and(|value| !value.re.is_finite() || !value.im.is_finite())
1319        {
1320            return Err(QmbedError::InvalidOptions(
1321                "component defaults must be finite".into(),
1322            ));
1323        }
1324    }
1325    Ok(())
1326}
1327
1328fn validate_site_permutation(permutation: &[usize]) -> Result<()> {
1329    if let Some(site) = permutation
1330        .iter()
1331        .copied()
1332        .find(|&site| site >= permutation.len())
1333    {
1334        return Err(QmbedError::InvalidSite {
1335            site,
1336            sites: permutation.len(),
1337        });
1338    }
1339    if permutation.iter().copied().collect::<HashSet<_>>().len() != permutation.len() {
1340        return Err(QmbedError::InvalidOptions(
1341            "site permutation must be bijective".into(),
1342        ));
1343    }
1344    Ok(())
1345}
1346
1347fn apply_operator_batch(
1348    operator: &dyn LinearOperator,
1349    inputs: &[Vec<Complex64>],
1350    action: OperatorAction,
1351) -> Result<Vec<Vec<Complex64>>> {
1352    let (rows, columns) = operator.shape();
1353    let (input_dimension, output_dimension) = match action {
1354        OperatorAction::Normal | OperatorAction::Conjugate => (columns, rows),
1355        OperatorAction::Transpose | OperatorAction::Adjoint => (rows, columns),
1356    };
1357    inputs
1358        .iter()
1359        .map(|input| {
1360            if input.len() != input_dimension {
1361                return Err(QmbedError::DimensionMismatch(format!(
1362                    "operator action needs input length {input_dimension}, got {}",
1363                    input.len()
1364                )));
1365            }
1366            let mut output = vec![Complex64::new(0.0, 0.0); output_dimension];
1367            match action {
1368                OperatorAction::Normal => operator.apply(input, &mut output)?,
1369                OperatorAction::Transpose => operator.apply_transpose(input, &mut output)?,
1370                OperatorAction::Conjugate => {
1371                    let conjugated: Vec<_> = input.iter().map(|value| value.conj()).collect();
1372                    operator.apply(&conjugated, &mut output)?;
1373                    output.iter_mut().for_each(|value| *value = value.conj());
1374                }
1375                OperatorAction::Adjoint => operator.apply_adjoint(input, &mut output)?,
1376            }
1377            Ok(output)
1378        })
1379        .collect()
1380}