1use 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#[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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
45pub enum OperatorAction {
46 #[default]
47 Normal,
48 Transpose,
49 Conjugate,
50 Adjoint,
51}
52
53#[derive(Clone, Debug)]
61pub struct PackedTermComponent {
62 name: String,
63 terms: Vec<OperatorSpec>,
64 default: Option<Complex64>,
65}
66
67impl PackedTermComponent {
68 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 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 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#[derive(Clone, Debug)]
124pub struct PackedOperatorModel {
125 static_part: Operator,
126 parameterized_part: Option<QuantumOperator>,
127}
128
129impl PackedOperatorModel {
130 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 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 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 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 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 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 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 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 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(¶meterized.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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn materialize(&self, format: MatrixFormat) -> Result<Operator> {
960 self.materialize_with(&HashMap::new(), format)
961 }
962
963 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 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 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 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 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 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 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 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 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}