1use std::collections::HashMap;
2
3use num_complex::Complex64;
4
5use crate::basis::{BasisProjector, BinaryState};
6use crate::operator::{LinearOperator, MatrixFormat, Operator};
7use crate::solve::StateTrajectory;
8use crate::{QmbedError, Result};
9
10#[derive(Clone, Debug)]
12pub struct Subspace {
13 ambient_dimension: usize,
14 columns: Vec<Vec<Complex64>>,
15}
16
17impl Subspace {
18 pub fn from_columns(
19 ambient_dimension: usize,
20 rank: usize,
21 column_major_vectors: Vec<Complex64>,
22 ) -> Result<Self> {
23 if ambient_dimension == 0
24 || rank == 0
25 || column_major_vectors.len() != ambient_dimension.saturating_mul(rank)
26 {
27 return Err(QmbedError::DimensionMismatch(
28 "subspace storage must contain ambient_dimension * rank entries".into(),
29 ));
30 }
31 let mut columns: Vec<Vec<Complex64>> = Vec::with_capacity(rank);
32 for column in 0..rank {
33 let mut vector = column_major_vectors
34 [column * ambient_dimension..(column + 1) * ambient_dimension]
35 .to_vec();
36 for previous in &columns {
37 let overlap = inner(previous, &vector);
38 for (value, basis_value) in vector.iter_mut().zip(previous) {
39 *value -= overlap * *basis_value;
40 }
41 }
42 let norm = vector.iter().map(Complex64::norm_sqr).sum::<f64>().sqrt();
43 if norm <= 1.0e-13 {
44 return Err(QmbedError::RankDeficient);
45 }
46 for value in &mut vector {
47 *value /= norm;
48 }
49 columns.push(vector);
50 }
51 Ok(Self {
52 ambient_dimension,
53 columns,
54 })
55 }
56
57 pub const fn ambient_dimension(&self) -> usize {
58 self.ambient_dimension
59 }
60
61 pub fn rank(&self) -> usize {
62 self.columns.len()
63 }
64
65 pub fn columns(&self) -> &[Vec<Complex64>] {
66 &self.columns
67 }
68}
69
70fn inner(left: &[Complex64], right: &[Complex64]) -> Complex64 {
71 left.iter()
72 .zip(right)
73 .map(|(left_value, right_value)| left_value.conj() * *right_value)
74 .sum()
75}
76
77pub fn subspace_fidelity(left: &Subspace, right: &Subspace) -> Result<f64> {
79 if left.ambient_dimension != right.ambient_dimension {
80 return Err(QmbedError::DimensionMismatch(
81 "subspaces must share an ambient dimension".into(),
82 ));
83 }
84 let denominator = left.rank().min(right.rank());
85 if denominator == 0 {
86 return Err(QmbedError::RankDeficient);
87 }
88 let overlap_norm: f64 = left
89 .columns
90 .iter()
91 .flat_map(|left_vector| {
92 right
93 .columns
94 .iter()
95 .map(move |right_vector| inner(left_vector, right_vector).norm_sqr())
96 })
97 .sum();
98 Ok((overlap_norm / denominator as f64).clamp(0.0, 1.0))
99}
100
101pub fn matrix_element(
102 left: &[Complex64],
103 operator: &(impl LinearOperator + ?Sized),
104 right: &[Complex64],
105) -> Result<Complex64> {
106 let shape = operator.shape();
107 if left.len() != shape.0 || right.len() != shape.1 {
108 return Err(QmbedError::DimensionMismatch(
109 "matrix-element vectors do not match the operator shape".into(),
110 ));
111 }
112 let mut applied = vec![Complex64::new(0.0, 0.0); shape.0];
113 operator.apply(right, &mut applied)?;
114 Ok(inner(left, &applied))
115}
116
117pub fn expectation(
118 operator: &(impl LinearOperator + ?Sized),
119 state: &[Complex64],
120) -> Result<Complex64> {
121 matrix_element(state, operator, state)
122}
123
124pub fn quantum_fluctuation(
126 operator: &(impl LinearOperator + ?Sized),
127 state: &[Complex64],
128) -> Result<f64> {
129 let shape = operator.shape();
130 if shape.0 != shape.1 || state.len() != shape.0 {
131 return Err(QmbedError::DimensionMismatch(
132 "quantum fluctuation requires a square operator matching the state".into(),
133 ));
134 }
135 let norm = inner(state, state).re;
136 if !norm.is_finite() || norm <= f64::EPSILON {
137 return Err(QmbedError::InvalidOptions(
138 "state must have positive finite norm".into(),
139 ));
140 }
141 let mut applied = vec![Complex64::new(0.0, 0.0); state.len()];
142 operator.apply(state, &mut applied)?;
143 let mean = inner(state, &applied) / norm;
144 let second = inner(&applied, &applied).re / norm;
145 Ok((second - mean.norm_sqr()).max(0.0))
146}
147
148pub fn raw_quantum_fluctuation(
155 operator: &(impl LinearOperator + ?Sized),
156 state: &[Complex64],
157) -> Result<Complex64> {
158 let shape = operator.shape();
159 if shape.0 != shape.1 || state.len() != shape.0 {
160 return Err(QmbedError::DimensionMismatch(
161 "raw quantum fluctuation requires a square operator matching the state".into(),
162 ));
163 }
164 let mut applied = vec![Complex64::new(0.0, 0.0); state.len()];
165 let mut applied_twice = vec![Complex64::new(0.0, 0.0); state.len()];
166 operator.apply(state, &mut applied)?;
167 operator.apply(&applied, &mut applied_twice)?;
168 let mean = inner(state, &applied);
169 Ok(inner(state, &applied_twice) - mean * mean)
170}
171
172pub fn partial_trace(
174 state: &[Complex64],
175 subsystem_dimension: usize,
176 environment_dimension: usize,
177) -> Result<Vec<Complex64>> {
178 if subsystem_dimension == 0
179 || environment_dimension == 0
180 || state.len()
181 != subsystem_dimension
182 .checked_mul(environment_dimension)
183 .ok_or_else(|| {
184 QmbedError::DimensionMismatch("tensor-product dimension overflow".into())
185 })?
186 {
187 return Err(QmbedError::DimensionMismatch(
188 "state length must equal subsystem_dimension * environment_dimension".into(),
189 ));
190 }
191 let norm = state.iter().map(Complex64::norm_sqr).sum::<f64>();
192 if !norm.is_finite() || norm <= f64::EPSILON {
193 return Err(QmbedError::InvalidOptions(
194 "state must have positive finite norm".into(),
195 ));
196 }
197 let mut density = vec![Complex64::new(0.0, 0.0); subsystem_dimension * subsystem_dimension];
198 for left in 0..subsystem_dimension {
199 for right in 0..subsystem_dimension {
200 density[left * subsystem_dimension + right] = (0..environment_dimension)
201 .map(|environment| {
202 state[left * environment_dimension + environment]
203 * state[right * environment_dimension + environment].conj()
204 / norm
205 })
206 .sum();
207 }
208 }
209 Ok(density)
210}
211
212pub fn partial_trace_density(
214 density: &[Complex64],
215 subsystem_dimension: usize,
216 environment_dimension: usize,
217) -> Result<Vec<Complex64>> {
218 let dimension = subsystem_dimension
219 .checked_mul(environment_dimension)
220 .ok_or_else(|| QmbedError::DimensionMismatch("tensor-product dimension overflow".into()))?;
221 if subsystem_dimension == 0
222 || environment_dimension == 0
223 || density.len() != dimension.saturating_mul(dimension)
224 {
225 return Err(QmbedError::DimensionMismatch(
226 "density shape must match the bipartite Hilbert space".into(),
227 ));
228 }
229 let trace: Complex64 = (0..dimension)
230 .map(|index| density[index * dimension + index])
231 .sum();
232 if trace.im.abs() > 1.0e-10 || !trace.re.is_finite() || trace.re <= f64::EPSILON {
233 return Err(QmbedError::InvalidOptions(
234 "density matrix must have a positive real trace".into(),
235 ));
236 }
237 for row in 0..dimension {
238 for column in 0..dimension {
239 if (density[row * dimension + column] - density[column * dimension + row].conj()).norm()
240 > 1.0e-10
241 {
242 return Err(QmbedError::InvalidOptions(
243 "density matrix must be Hermitian".into(),
244 ));
245 }
246 }
247 }
248 let mut reduced = vec![Complex64::new(0.0, 0.0); subsystem_dimension * subsystem_dimension];
249 for left in 0..subsystem_dimension {
250 for right in 0..subsystem_dimension {
251 for environment in 0..environment_dimension {
252 let row = left * environment_dimension + environment;
253 let column = right * environment_dimension + environment;
254 reduced[left * subsystem_dimension + right] +=
255 density[row * dimension + column] / trace.re;
256 }
257 }
258 }
259 Ok(reduced)
260}
261
262#[derive(Clone, Debug)]
263struct SubsystemIndexMap {
264 full_dimension: usize,
265 subsystem_dimension: usize,
266 environment_dimension: usize,
267 subsystem_indices: Vec<usize>,
268 environment_indices: Vec<usize>,
269}
270
271fn subsystem_index_map(
272 local_dimensions: &[usize],
273 retained_sites: &[usize],
274) -> Result<SubsystemIndexMap> {
275 if local_dimensions.is_empty() || local_dimensions.contains(&0) {
276 return Err(QmbedError::InvalidOptions(
277 "local dimensions must be a nonempty list of positive values".into(),
278 ));
279 }
280 let mut retained = vec![false; local_dimensions.len()];
281 for &site in retained_sites {
282 if site >= local_dimensions.len() {
283 return Err(QmbedError::InvalidSite {
284 site,
285 sites: local_dimensions.len(),
286 });
287 }
288 if std::mem::replace(&mut retained[site], true) {
289 return Err(QmbedError::InvalidOptions(
290 "retained subsystem sites must be unique".into(),
291 ));
292 }
293 }
294 let product = |sites: &[usize]| -> Result<usize> {
295 sites.iter().try_fold(1_usize, |dimension, &site| {
296 dimension
297 .checked_mul(local_dimensions[site])
298 .ok_or_else(|| QmbedError::DimensionMismatch("Hilbert-space size overflow".into()))
299 })
300 };
301 let environment_sites: Vec<_> = (0..local_dimensions.len())
302 .filter(|site| !retained[*site])
303 .collect();
304 let subsystem_dimension = product(retained_sites)?;
305 let environment_dimension = product(&environment_sites)?;
306 let full_dimension = subsystem_dimension
307 .checked_mul(environment_dimension)
308 .ok_or_else(|| QmbedError::DimensionMismatch("Hilbert-space size overflow".into()))?;
309
310 let mut subsystem_indices = vec![0; full_dimension];
311 let mut environment_indices = vec![0; full_dimension];
312 for global in 0..full_dimension {
313 let mut value = global;
314 let mut digits = Vec::with_capacity(local_dimensions.len());
315 for &dimension in local_dimensions {
316 digits.push(value % dimension);
317 value /= dimension;
318 }
319 let mut stride = 1;
320 for &site in retained_sites {
321 subsystem_indices[global] += digits[site] * stride;
322 stride *= local_dimensions[site];
323 }
324 stride = 1;
325 for &site in &environment_sites {
326 environment_indices[global] += digits[site] * stride;
327 stride *= local_dimensions[site];
328 }
329 }
330 Ok(SubsystemIndexMap {
331 full_dimension,
332 subsystem_dimension,
333 environment_dimension,
334 subsystem_indices,
335 environment_indices,
336 })
337}
338
339pub fn subsystem_dimensions(
341 local_dimensions: &[usize],
342 retained_sites: &[usize],
343) -> Result<(usize, usize)> {
344 let layout = subsystem_index_map(local_dimensions, retained_sites)?;
345 Ok((layout.subsystem_dimension, layout.environment_dimension))
346}
347
348pub fn fermionic_subsystem_phases(
355 local_dimensions: &[usize],
356 retained_sites: &[usize],
357) -> Result<Vec<f64>> {
358 let all_sites = (0..local_dimensions.len()).collect::<Vec<_>>();
359 noncommuting_subsystem_phases(local_dimensions, retained_sites, &[all_sites])
360}
361
362#[derive(Clone, Debug, PartialEq)]
364pub struct NoncommutingGroup {
365 sites: Vec<usize>,
366 exchange_phase: Complex64,
367}
368
369impl NoncommutingGroup {
370 pub fn new(sites: impl Into<Vec<usize>>, exchange_phase: impl Into<Complex64>) -> Result<Self> {
371 let exchange_phase = exchange_phase.into();
372 if !exchange_phase.re.is_finite()
373 || !exchange_phase.im.is_finite()
374 || (exchange_phase.norm() - 1.0).abs() > 1.0e-12
375 {
376 return Err(QmbedError::InvalidOptions(
377 "a noncommuting exchange phase must be finite and have unit magnitude".into(),
378 ));
379 }
380 Ok(Self {
381 sites: sites.into(),
382 exchange_phase,
383 })
384 }
385
386 pub fn sites(&self) -> &[usize] {
387 &self.sites
388 }
389
390 pub const fn exchange_phase(&self) -> Complex64 {
391 self.exchange_phase
392 }
393}
394
395pub fn noncommuting_subsystem_phases(
404 local_dimensions: &[usize],
405 retained_sites: &[usize],
406 noncommuting_groups: &[Vec<usize>],
407) -> Result<Vec<f64>> {
408 let groups = noncommuting_groups
409 .iter()
410 .map(|sites| NoncommutingGroup::new(sites.clone(), Complex64::new(-1.0, 0.0)))
411 .collect::<Result<Vec<_>>>()?;
412 noncommuting_subsystem_exchange_phases(local_dimensions, retained_sites, &groups)
413 .map(|phases| phases.into_iter().map(|phase| phase.re).collect::<Vec<_>>())
414}
415
416pub fn noncommuting_subsystem_exchange_phases(
421 local_dimensions: &[usize],
422 retained_sites: &[usize],
423 noncommuting_groups: &[NoncommutingGroup],
424) -> Result<Vec<Complex64>> {
425 let layout = subsystem_index_map(local_dimensions, retained_sites)?;
426 let mut retained = vec![false; local_dimensions.len()];
427 for &site in retained_sites {
428 retained[site] = true;
429 }
430 let mut new_order = (0..local_dimensions.len())
431 .filter(|site| !retained[*site])
432 .collect::<Vec<_>>();
433 new_order.extend_from_slice(retained_sites);
434 let mut new_position = vec![0; local_dimensions.len()];
435 for (position, &site) in new_order.iter().enumerate() {
436 new_position[site] = position;
437 }
438 let mut group_for_site = vec![None; local_dimensions.len()];
439 for (group, noncommuting_group) in noncommuting_groups.iter().enumerate() {
440 let mut seen = std::collections::HashSet::with_capacity(noncommuting_group.sites().len());
441 for &site in noncommuting_group.sites() {
442 if site >= local_dimensions.len() {
443 return Err(QmbedError::InvalidSite {
444 site,
445 sites: local_dimensions.len(),
446 });
447 }
448 if local_dimensions[site] != 2 {
449 return Err(QmbedError::InvalidOptions(format!(
450 "noncommuting site {site} must have binary local dimension"
451 )));
452 }
453 if !seen.insert(site) {
454 return Err(QmbedError::InvalidOptions(
455 "a noncommuting group cannot repeat a site".into(),
456 ));
457 }
458 if group_for_site[site].replace(group).is_some() {
459 return Err(QmbedError::InvalidOptions(
460 "noncommuting groups must be disjoint".into(),
461 ));
462 }
463 }
464 }
465 let mut strides = Vec::with_capacity(local_dimensions.len());
466 let mut stride = 1_usize;
467 for &dimension in local_dimensions {
468 strides.push(stride);
469 stride = stride
470 .checked_mul(dimension)
471 .ok_or_else(|| QmbedError::DimensionMismatch("Hilbert-space size overflow".into()))?;
472 }
473
474 Ok((0..layout.full_dimension)
475 .map(|state| {
476 let mut inversions = vec![0_u32; noncommuting_groups.len()];
477 for left in 0..local_dimensions.len() {
478 let Some(group) = group_for_site[left] else {
479 continue;
480 };
481 if (state / strides[left]) % local_dimensions[left] == 0 {
482 continue;
483 }
484 for right in (left + 1)..local_dimensions.len() {
485 if group_for_site[right] == Some(group)
486 && (state / strides[right]) % local_dimensions[right] != 0
487 && new_position[left] > new_position[right]
488 {
489 inversions[group] += 1;
490 }
491 }
492 }
493 inversions
494 .into_iter()
495 .zip(noncommuting_groups)
496 .fold(Complex64::new(1.0, 0.0), |phase, (count, group)| {
497 phase * group.exchange_phase().powu(count)
498 })
499 })
500 .collect())
501}
502
503pub fn apply_noncommuting_subsystem_exchange_phases(
505 state: &mut [Complex64],
506 local_dimensions: &[usize],
507 retained_sites: &[usize],
508 noncommuting_groups: &[NoncommutingGroup],
509) -> Result<()> {
510 let phases = noncommuting_subsystem_exchange_phases(
511 local_dimensions,
512 retained_sites,
513 noncommuting_groups,
514 )?;
515 if state.len() != phases.len() {
516 return Err(QmbedError::DimensionMismatch(
517 "state length does not match the noncommuting subsystem layout".into(),
518 ));
519 }
520 for (value, phase) in state.iter_mut().zip(phases) {
521 *value *= phase;
522 }
523 Ok(())
524}
525
526pub fn apply_noncommuting_subsystem_phases(
528 state: &mut [Complex64],
529 local_dimensions: &[usize],
530 retained_sites: &[usize],
531 noncommuting_groups: &[Vec<usize>],
532) -> Result<()> {
533 let phases =
534 noncommuting_subsystem_phases(local_dimensions, retained_sites, noncommuting_groups)?;
535 if state.len() != phases.len() {
536 return Err(QmbedError::DimensionMismatch(
537 "state length does not match the noncommuting subsystem layout".into(),
538 ));
539 }
540 for (value, phase) in state.iter_mut().zip(phases) {
541 *value *= phase;
542 }
543 Ok(())
544}
545
546pub fn apply_fermionic_subsystem_phases(
548 state: &mut [Complex64],
549 local_dimensions: &[usize],
550 retained_sites: &[usize],
551) -> Result<()> {
552 let phases = fermionic_subsystem_phases(local_dimensions, retained_sites)?;
553 if state.len() != phases.len() {
554 return Err(QmbedError::DimensionMismatch(
555 "state length does not match the fermionic subsystem layout".into(),
556 ));
557 }
558 for (value, phase) in state.iter_mut().zip(phases) {
559 *value *= phase;
560 }
561 Ok(())
562}
563
564pub fn apply_noncommuting_subsystem_phases_density(
566 density: &mut [Complex64],
567 local_dimensions: &[usize],
568 retained_sites: &[usize],
569 noncommuting_groups: &[Vec<usize>],
570) -> Result<()> {
571 let phases =
572 noncommuting_subsystem_phases(local_dimensions, retained_sites, noncommuting_groups)?;
573 let dimension = phases.len();
574 if density.len() != dimension.saturating_mul(dimension) {
575 return Err(QmbedError::DimensionMismatch(
576 "density matrix does not match the noncommuting subsystem layout".into(),
577 ));
578 }
579 for row in 0..dimension {
580 for column in 0..dimension {
581 density[row * dimension + column] *= phases[row] * phases[column];
582 }
583 }
584 Ok(())
585}
586
587pub fn apply_noncommuting_subsystem_exchange_phases_density(
589 density: &mut [Complex64],
590 local_dimensions: &[usize],
591 retained_sites: &[usize],
592 noncommuting_groups: &[NoncommutingGroup],
593) -> Result<()> {
594 let phases = noncommuting_subsystem_exchange_phases(
595 local_dimensions,
596 retained_sites,
597 noncommuting_groups,
598 )?;
599 let dimension = phases.len();
600 if density.len() != dimension.saturating_mul(dimension) {
601 return Err(QmbedError::DimensionMismatch(
602 "density matrix does not match the noncommuting subsystem layout".into(),
603 ));
604 }
605 for row in 0..dimension {
606 for column in 0..dimension {
607 density[row * dimension + column] *= phases[row] * phases[column].conj();
608 }
609 }
610 Ok(())
611}
612
613pub fn apply_fermionic_subsystem_phases_density(
615 density: &mut [Complex64],
616 local_dimensions: &[usize],
617 retained_sites: &[usize],
618) -> Result<()> {
619 let phases = fermionic_subsystem_phases(local_dimensions, retained_sites)?;
620 let dimension = phases.len();
621 if density.len() != dimension.saturating_mul(dimension) {
622 return Err(QmbedError::DimensionMismatch(
623 "density matrix does not match the fermionic subsystem layout".into(),
624 ));
625 }
626 for row in 0..dimension {
627 for column in 0..dimension {
628 density[row * dimension + column] *= phases[row] * phases[column];
629 }
630 }
631 Ok(())
632}
633
634pub fn partial_trace_subsystem(
637 state: &[Complex64],
638 local_dimensions: &[usize],
639 retained_sites: &[usize],
640) -> Result<Vec<Complex64>> {
641 let layout = subsystem_index_map(local_dimensions, retained_sites)?;
642 if state.len() != layout.full_dimension {
643 return Err(QmbedError::DimensionMismatch(
644 "state length does not match the product of local dimensions".into(),
645 ));
646 }
647 let norm = state.iter().map(Complex64::norm_sqr).sum::<f64>();
648 if !norm.is_finite() || norm <= f64::EPSILON {
649 return Err(QmbedError::InvalidOptions(
650 "state must have positive finite norm".into(),
651 ));
652 }
653 let mut amplitudes = vec![Complex64::new(0.0, 0.0); layout.full_dimension];
654 for (global, &value) in state.iter().enumerate() {
655 let subsystem = layout.subsystem_indices[global];
656 let environment = layout.environment_indices[global];
657 amplitudes[subsystem * layout.environment_dimension + environment] = value;
658 }
659 let mut reduced =
660 vec![Complex64::new(0.0, 0.0); layout.subsystem_dimension * layout.subsystem_dimension];
661 for left in 0..layout.subsystem_dimension {
662 for right in 0..layout.subsystem_dimension {
663 reduced[left * layout.subsystem_dimension + right] = (0..layout.environment_dimension)
664 .map(|environment| {
665 amplitudes[left * layout.environment_dimension + environment]
666 * amplitudes[right * layout.environment_dimension + environment].conj()
667 / norm
668 })
669 .sum();
670 }
671 }
672 Ok(reduced)
673}
674
675pub fn partial_trace_density_subsystem(
677 density: &[Complex64],
678 local_dimensions: &[usize],
679 retained_sites: &[usize],
680) -> Result<Vec<Complex64>> {
681 let layout = subsystem_index_map(local_dimensions, retained_sites)?;
682 if density.len() != layout.full_dimension.saturating_mul(layout.full_dimension) {
683 return Err(QmbedError::DimensionMismatch(
684 "density shape does not match the product of local dimensions".into(),
685 ));
686 }
687 let trace: Complex64 = (0..layout.full_dimension)
688 .map(|index| density[index * layout.full_dimension + index])
689 .sum();
690 if trace.im.abs() > 1.0e-10 || !trace.re.is_finite() || trace.re <= f64::EPSILON {
691 return Err(QmbedError::InvalidOptions(
692 "density matrix must have a positive real trace".into(),
693 ));
694 }
695 for row in 0..layout.full_dimension {
696 for column in 0..layout.full_dimension {
697 if (density[row * layout.full_dimension + column]
698 - density[column * layout.full_dimension + row].conj())
699 .norm()
700 > 1.0e-10
701 {
702 return Err(QmbedError::InvalidOptions(
703 "density matrix must be Hermitian".into(),
704 ));
705 }
706 }
707 }
708 let mut reduced =
709 vec![Complex64::new(0.0, 0.0); layout.subsystem_dimension * layout.subsystem_dimension];
710 for row in 0..layout.full_dimension {
711 for column in 0..layout.full_dimension {
712 if layout.environment_indices[row] == layout.environment_indices[column] {
713 let reduced_row = layout.subsystem_indices[row];
714 let reduced_column = layout.subsystem_indices[column];
715 reduced[reduced_row * layout.subsystem_dimension + reduced_column] +=
716 density[row * layout.full_dimension + column] / trace.re;
717 }
718 }
719 }
720 Ok(reduced)
721}
722
723#[derive(Clone, Debug)]
724struct BinarySectorSubsystemLayout {
725 total_sites: usize,
726 retained_sites: Vec<usize>,
727 environment_sites: Vec<usize>,
728 subsystem_dimension: usize,
729 group_for_site: Vec<Option<usize>>,
730 new_position: Vec<usize>,
731}
732
733#[derive(Clone, Debug)]
734struct SplitBinarySectorState {
735 subsystem: usize,
736 environment: Vec<u64>,
737 exchange_phase: Complex64,
738}
739
740fn binary_sector_subsystem_layout(
741 total_sites: usize,
742 retained_sites: &[usize],
743 noncommuting_groups: &[NoncommutingGroup],
744) -> Result<BinarySectorSubsystemLayout> {
745 if total_sites == 0 {
746 return Err(QmbedError::InvalidOptions(
747 "a binary subsystem requires at least one site".into(),
748 ));
749 }
750 let mut retained = vec![false; total_sites];
751 for &site in retained_sites {
752 if site >= total_sites {
753 return Err(QmbedError::InvalidSite {
754 site,
755 sites: total_sites,
756 });
757 }
758 if std::mem::replace(&mut retained[site], true) {
759 return Err(QmbedError::InvalidOptions(
760 "retained subsystem sites must be unique".into(),
761 ));
762 }
763 }
764 let subsystem_dimension =
765 1_usize
766 .checked_shl(u32::try_from(retained_sites.len()).map_err(|_| {
767 QmbedError::DimensionMismatch("retained subsystem is too large".into())
768 })?)
769 .ok_or_else(|| {
770 QmbedError::DimensionMismatch(
771 "retained subsystem is too large for a dense reduced density matrix".into(),
772 )
773 })?;
774 subsystem_dimension
775 .checked_mul(subsystem_dimension)
776 .ok_or_else(|| {
777 QmbedError::DimensionMismatch("reduced density-matrix allocation would overflow".into())
778 })?;
779
780 let environment_sites = (0..total_sites)
781 .filter(|site| !retained[*site])
782 .collect::<Vec<_>>();
783 let mut new_order = environment_sites.clone();
784 new_order.extend_from_slice(retained_sites);
785 let mut new_position = vec![0; total_sites];
786 for (position, &site) in new_order.iter().enumerate() {
787 new_position[site] = position;
788 }
789 let mut group_for_site = vec![None; total_sites];
790 for (group, noncommuting_group) in noncommuting_groups.iter().enumerate() {
791 let mut seen = std::collections::HashSet::with_capacity(noncommuting_group.sites().len());
792 for &site in noncommuting_group.sites() {
793 if site >= total_sites {
794 return Err(QmbedError::InvalidSite {
795 site,
796 sites: total_sites,
797 });
798 }
799 if !seen.insert(site) {
800 return Err(QmbedError::InvalidOptions(
801 "a noncommuting group cannot repeat a site".into(),
802 ));
803 }
804 if group_for_site[site].replace(group).is_some() {
805 return Err(QmbedError::InvalidOptions(
806 "noncommuting groups must be disjoint".into(),
807 ));
808 }
809 }
810 }
811 Ok(BinarySectorSubsystemLayout {
812 total_sites,
813 retained_sites: retained_sites.to_vec(),
814 environment_sites,
815 subsystem_dimension,
816 group_for_site,
817 new_position,
818 })
819}
820
821fn split_binary_sector_state<State>(
822 state: State,
823 layout: &BinarySectorSubsystemLayout,
824 noncommuting_groups: &[NoncommutingGroup],
825) -> Result<SplitBinarySectorState>
826where
827 State: BinaryState,
828{
829 let mut subsystem = 0_usize;
830 for (position, &site) in layout.retained_sites.iter().enumerate() {
831 if state.bit(site)? {
832 subsystem |= 1_usize << position;
833 }
834 }
835 let mut environment = vec![0_u64; layout.environment_sites.len().div_ceil(64)];
836 for (position, &site) in layout.environment_sites.iter().enumerate() {
837 if state.bit(site)? {
838 environment[position / 64] |= 1_u64 << (position % 64);
839 }
840 }
841 let mut inversions = vec![0_u32; noncommuting_groups.len()];
842 for left in 0..layout.total_sites {
843 let Some(group) = layout.group_for_site[left] else {
844 continue;
845 };
846 if !state.bit(left)? {
847 continue;
848 }
849 for right in (left + 1)..layout.total_sites {
850 if layout.group_for_site[right] == Some(group)
851 && state.bit(right)?
852 && layout.new_position[left] > layout.new_position[right]
853 {
854 inversions[group] += 1;
855 }
856 }
857 }
858 let exchange_phase = inversions
859 .into_iter()
860 .zip(noncommuting_groups)
861 .fold(Complex64::new(1.0, 0.0), |phase, (count, group)| {
862 phase * group.exchange_phase().powu(count)
863 });
864 Ok(SplitBinarySectorState {
865 subsystem,
866 environment,
867 exchange_phase,
868 })
869}
870
871fn split_binary_sector_states<State>(
872 states: &[State],
873 layout: &BinarySectorSubsystemLayout,
874 noncommuting_groups: &[NoncommutingGroup],
875) -> Result<Vec<SplitBinarySectorState>>
876where
877 State: BinaryState,
878{
879 let mut seen = std::collections::HashSet::with_capacity(states.len());
880 states
881 .iter()
882 .copied()
883 .map(|state| {
884 if !seen.insert(state) {
885 return Err(QmbedError::InvalidOptions(
886 "sector basis states must be unique".into(),
887 ));
888 }
889 split_binary_sector_state(state, layout, noncommuting_groups)
890 })
891 .collect()
892}
893
894fn zero_complex_square(dimension: usize) -> Result<Vec<Complex64>> {
895 let length = dimension.checked_mul(dimension).ok_or_else(|| {
896 QmbedError::DimensionMismatch("reduced density-matrix allocation would overflow".into())
897 })?;
898 let mut values = Vec::new();
899 values.try_reserve_exact(length).map_err(|_| {
900 QmbedError::UnsupportedBackend(format!(
901 "a dense {dimension}x{dimension} reduced density matrix does not fit in memory"
902 ))
903 })?;
904 values.resize(length, Complex64::new(0.0, 0.0));
905 Ok(values)
906}
907
908pub fn partial_trace_sector_state<State>(
916 amplitudes: &[Complex64],
917 basis_states: &[State],
918 total_sites: usize,
919 retained_sites: &[usize],
920 noncommuting_groups: &[NoncommutingGroup],
921) -> Result<Vec<Complex64>>
922where
923 State: BinaryState,
924{
925 if amplitudes.len() != basis_states.len() {
926 return Err(QmbedError::DimensionMismatch(
927 "sector amplitudes and basis states must have the same length".into(),
928 ));
929 }
930 let norm = amplitudes.iter().map(Complex64::norm_sqr).sum::<f64>();
931 if !norm.is_finite() || norm <= f64::EPSILON {
932 return Err(QmbedError::InvalidOptions(
933 "state must have positive finite norm".into(),
934 ));
935 }
936 let layout = binary_sector_subsystem_layout(total_sites, retained_sites, noncommuting_groups)?;
937 let split = split_binary_sector_states(basis_states, &layout, noncommuting_groups)?;
938 let mut by_environment = std::collections::HashMap::<Vec<u64>, Vec<(usize, Complex64)>>::new();
939 for (state, amplitude) in split.iter().zip(amplitudes) {
940 by_environment
941 .entry(state.environment.clone())
942 .or_default()
943 .push((state.subsystem, state.exchange_phase * *amplitude));
944 }
945 let mut reduced = zero_complex_square(layout.subsystem_dimension)?;
946 for amplitudes in by_environment.values() {
947 for &(left, left_value) in amplitudes {
948 for &(right, right_value) in amplitudes {
949 reduced[left * layout.subsystem_dimension + right] +=
950 left_value * right_value.conj() / norm;
951 }
952 }
953 }
954 Ok(reduced)
955}
956
957pub fn partial_trace_sector_density<State>(
964 density: &[Complex64],
965 basis_states: &[State],
966 total_sites: usize,
967 retained_sites: &[usize],
968 noncommuting_groups: &[NoncommutingGroup],
969) -> Result<Vec<Complex64>>
970where
971 State: BinaryState,
972{
973 let dimension = basis_states.len();
974 if density.len() != dimension.saturating_mul(dimension) {
975 return Err(QmbedError::DimensionMismatch(
976 "sector density matrix does not match the basis dimension".into(),
977 ));
978 }
979 let trace = (0..dimension)
980 .map(|index| density[index * dimension + index])
981 .sum::<Complex64>();
982 if trace.im.abs() > 1.0e-10 || !trace.re.is_finite() || trace.re <= f64::EPSILON {
983 return Err(QmbedError::InvalidOptions(
984 "density matrix must have a positive real trace".into(),
985 ));
986 }
987 for row in 0..dimension {
988 for column in 0..dimension {
989 if (density[row * dimension + column] - density[column * dimension + row].conj()).norm()
990 > 1.0e-10
991 {
992 return Err(QmbedError::InvalidOptions(
993 "density matrix must be Hermitian".into(),
994 ));
995 }
996 }
997 }
998 let layout = binary_sector_subsystem_layout(total_sites, retained_sites, noncommuting_groups)?;
999 let split = split_binary_sector_states(basis_states, &layout, noncommuting_groups)?;
1000 let mut rows_by_environment =
1001 std::collections::HashMap::<Vec<u64>, Vec<usize>>::with_capacity(dimension);
1002 for (row, state) in split.iter().enumerate() {
1003 rows_by_environment
1004 .entry(state.environment.clone())
1005 .or_default()
1006 .push(row);
1007 }
1008 let mut reduced = zero_complex_square(layout.subsystem_dimension)?;
1009 for rows in rows_by_environment.values() {
1010 for &row in rows {
1011 for &column in rows {
1012 let left = &split[row];
1013 let right = &split[column];
1014 reduced[left.subsystem * layout.subsystem_dimension + right.subsystem] += left
1015 .exchange_phase
1016 * density[row * dimension + column]
1017 * right.exchange_phase.conj()
1018 / trace.re;
1019 }
1020 }
1021 }
1022 Ok(reduced)
1023}
1024
1025pub fn entanglement_spectrum_sector<State>(
1027 amplitudes: &[Complex64],
1028 basis_states: &[State],
1029 total_sites: usize,
1030 retained_sites: &[usize],
1031 noncommuting_groups: &[NoncommutingGroup],
1032) -> Result<Vec<f64>>
1033where
1034 State: BinaryState,
1035{
1036 let dimension = 1_usize
1037 .checked_shl(u32::try_from(retained_sites.len()).unwrap_or(u32::MAX))
1038 .ok_or_else(|| {
1039 QmbedError::DimensionMismatch(
1040 "retained subsystem is too large for a dense entanglement spectrum".into(),
1041 )
1042 })?;
1043 density_matrix_spectrum(
1044 partial_trace_sector_state(
1045 amplitudes,
1046 basis_states,
1047 total_sites,
1048 retained_sites,
1049 noncommuting_groups,
1050 )?,
1051 dimension,
1052 )
1053}
1054
1055pub fn entanglement_entropy_sector<State>(
1057 amplitudes: &[Complex64],
1058 basis_states: &[State],
1059 total_sites: usize,
1060 retained_sites: &[usize],
1061 noncommuting_groups: &[NoncommutingGroup],
1062 order: EntropyOrder,
1063) -> Result<f64>
1064where
1065 State: BinaryState,
1066{
1067 entropy_from_spectrum(
1068 &entanglement_spectrum_sector(
1069 amplitudes,
1070 basis_states,
1071 total_sites,
1072 retained_sites,
1073 noncommuting_groups,
1074 )?,
1075 order,
1076 )
1077}
1078
1079#[derive(Clone, Copy, Debug, PartialEq)]
1080pub enum EntropyOrder {
1081 VonNeumann,
1082 Renyi(f64),
1083}
1084
1085pub fn entropy_from_spectrum(probabilities: &[f64], order: EntropyOrder) -> Result<f64> {
1087 match order {
1088 EntropyOrder::VonNeumann => Ok(-probabilities
1089 .iter()
1090 .copied()
1091 .filter(|probability| *probability > f64::EPSILON)
1092 .map(|probability| probability * probability.ln())
1093 .sum::<f64>()),
1094 EntropyOrder::Renyi(alpha)
1095 if alpha.is_finite() && alpha > 0.0 && (alpha - 1.0).abs() > 1.0e-12 =>
1096 {
1097 Ok(probabilities
1098 .iter()
1099 .copied()
1100 .map(|probability| probability.powf(alpha))
1101 .sum::<f64>()
1102 .ln()
1103 / (1.0 - alpha))
1104 }
1105 EntropyOrder::Renyi(_) => Err(QmbedError::InvalidOptions(
1106 "Renyi order must be positive, finite, and different from one".into(),
1107 )),
1108 }
1109}
1110
1111pub fn entanglement_entropy(
1112 state: &[Complex64],
1113 subsystem_dimension: usize,
1114 environment_dimension: usize,
1115 order: EntropyOrder,
1116) -> Result<f64> {
1117 if matches!(order, EntropyOrder::Renyi(alpha) if !alpha.is_finite() || alpha <= 0.0 || (alpha - 1.0).abs() <= 1.0e-12)
1118 {
1119 return Err(QmbedError::InvalidOptions(
1120 "Renyi order must be positive, finite, and different from one".into(),
1121 ));
1122 }
1123 let probabilities = density_matrix_spectrum(
1124 partial_trace(state, subsystem_dimension, environment_dimension)?,
1125 subsystem_dimension,
1126 )?;
1127 entropy_from_spectrum(&probabilities, order)
1128}
1129
1130pub fn entanglement_spectrum_subsystem(
1131 state: &[Complex64],
1132 local_dimensions: &[usize],
1133 retained_sites: &[usize],
1134) -> Result<Vec<f64>> {
1135 let layout = subsystem_index_map(local_dimensions, retained_sites)?;
1136 density_matrix_spectrum(
1137 partial_trace_subsystem(state, local_dimensions, retained_sites)?,
1138 layout.subsystem_dimension,
1139 )
1140}
1141
1142pub fn canonical_schmidt_spectrum_subsystem(
1152 state: &[Complex64],
1153 local_dimensions: &[usize],
1154 retained_sites: &[usize],
1155 noncommuting_groups: &[Vec<usize>],
1156) -> Result<Vec<f64>> {
1157 let groups = noncommuting_groups
1158 .iter()
1159 .map(|sites| NoncommutingGroup::new(sites.clone(), Complex64::new(-1.0, 0.0)))
1160 .collect::<Result<Vec<_>>>()?;
1161 canonical_schmidt_spectrum_subsystem_with_exchange_phases(
1162 state,
1163 local_dimensions,
1164 retained_sites,
1165 &groups,
1166 )
1167}
1168
1169pub fn canonical_schmidt_spectrum_subsystem_with_exchange_phases(
1170 state: &[Complex64],
1171 local_dimensions: &[usize],
1172 retained_sites: &[usize],
1173 noncommuting_groups: &[NoncommutingGroup],
1174) -> Result<Vec<f64>> {
1175 let layout = subsystem_index_map(local_dimensions, retained_sites)?;
1176 let mut retained = retained_sites.to_vec();
1177 retained.sort_unstable();
1178 let mut selected = vec![false; local_dimensions.len()];
1179 for &site in &retained {
1180 selected[site] = true;
1181 }
1182 let environment = (0..local_dimensions.len())
1183 .filter(|site| !selected[*site])
1184 .collect::<Vec<_>>();
1185 let canonical_sites = if layout.subsystem_dimension < layout.environment_dimension
1186 || (layout.subsystem_dimension == layout.environment_dimension && retained <= environment)
1187 {
1188 retained
1189 } else {
1190 environment
1191 };
1192 let mut canonical_state = state.to_vec();
1193 if !noncommuting_groups.is_empty() {
1194 apply_noncommuting_subsystem_exchange_phases(
1195 &mut canonical_state,
1196 local_dimensions,
1197 &canonical_sites,
1198 noncommuting_groups,
1199 )?;
1200 }
1201 entanglement_spectrum_subsystem(&canonical_state, local_dimensions, &canonical_sites)
1202}
1203
1204pub fn entanglement_spectrum_density_subsystem(
1205 density: &[Complex64],
1206 local_dimensions: &[usize],
1207 retained_sites: &[usize],
1208) -> Result<Vec<f64>> {
1209 let layout = subsystem_index_map(local_dimensions, retained_sites)?;
1210 density_matrix_spectrum(
1211 partial_trace_density_subsystem(density, local_dimensions, retained_sites)?,
1212 layout.subsystem_dimension,
1213 )
1214}
1215
1216pub fn entanglement_entropy_subsystem(
1217 state: &[Complex64],
1218 local_dimensions: &[usize],
1219 retained_sites: &[usize],
1220 order: EntropyOrder,
1221) -> Result<f64> {
1222 entropy_from_spectrum(
1223 &entanglement_spectrum_subsystem(state, local_dimensions, retained_sites)?,
1224 order,
1225 )
1226}
1227
1228pub fn entanglement_entropy_density(
1229 density: &[Complex64],
1230 subsystem_dimension: usize,
1231 environment_dimension: usize,
1232 order: EntropyOrder,
1233) -> Result<f64> {
1234 entropy_from_spectrum(
1235 &entanglement_spectrum_density(density, subsystem_dimension, environment_dimension)?,
1236 order,
1237 )
1238}
1239
1240pub fn entanglement_entropy_density_subsystem(
1241 density: &[Complex64],
1242 local_dimensions: &[usize],
1243 retained_sites: &[usize],
1244 order: EntropyOrder,
1245) -> Result<f64> {
1246 entropy_from_spectrum(
1247 &entanglement_spectrum_density_subsystem(density, local_dimensions, retained_sites)?,
1248 order,
1249 )
1250}
1251
1252pub fn density_matrix_spectrum(density: Vec<Complex64>, dimension: usize) -> Result<Vec<f64>> {
1254 if density.len() != dimension.saturating_mul(dimension) {
1255 return Err(QmbedError::DimensionMismatch(
1256 "density matrix shape does not match its dimension".into(),
1257 ));
1258 }
1259 let mut trace = Complex64::new(0.0, 0.0);
1260 let mut scale = 0.0_f64;
1261 for row in 0..dimension {
1262 trace += density[row * dimension + row];
1263 for column in 0..dimension {
1264 let value = density[row * dimension + column];
1265 if !value.re.is_finite() || !value.im.is_finite() {
1266 return Err(QmbedError::InvalidOptions(
1267 "density matrix entries must be finite".into(),
1268 ));
1269 }
1270 scale = scale.max(value.norm());
1271 if (value - density[column * dimension + row].conj()).norm() > 1.0e-10 * scale.max(1.0)
1272 {
1273 return Err(QmbedError::InvalidOptions(
1274 "density matrix must be Hermitian".into(),
1275 ));
1276 }
1277 }
1278 }
1279 let mut probabilities = crate::backend::singular_values(&density, dimension, dimension)?;
1280 probabilities.sort_by(f64::total_cmp);
1281 let nuclear_norm = probabilities.iter().sum::<f64>();
1282 let tolerance =
1283 (256.0 * f64::EPSILON * dimension as f64 * nuclear_norm.max(scale).max(1.0)).max(1.0e-10);
1284 if trace.im.abs() > tolerance
1285 || trace.re < -tolerance
1286 || (nuclear_norm - trace.re).abs() > tolerance
1287 {
1288 if trace.re >= -tolerance && nuclear_norm >= trace.re {
1289 let negative_mass = 0.5 * (nuclear_norm - trace.re);
1290 return Err(QmbedError::InvalidOptions(format!(
1291 "density matrix is not positive semidefinite; negative spectral mass is \
1292 {negative_mass:e}"
1293 )));
1294 }
1295 return Err(QmbedError::InvalidOptions(
1296 "density matrix must have a finite nonnegative real trace".into(),
1297 ));
1298 }
1299 Ok(probabilities)
1300}
1301
1302pub fn entanglement_spectrum(
1303 state: &[Complex64],
1304 subsystem_dimension: usize,
1305 environment_dimension: usize,
1306) -> Result<Vec<f64>> {
1307 density_matrix_spectrum(
1308 partial_trace(state, subsystem_dimension, environment_dimension)?,
1309 subsystem_dimension,
1310 )
1311}
1312
1313pub fn entanglement_spectrum_density(
1314 density: &[Complex64],
1315 subsystem_dimension: usize,
1316 environment_dimension: usize,
1317) -> Result<Vec<f64>> {
1318 density_matrix_spectrum(
1319 partial_trace_density(density, subsystem_dimension, environment_dimension)?,
1320 subsystem_dimension,
1321 )
1322}
1323
1324pub fn entanglement_entropy_batch(
1325 states: &[Vec<Complex64>],
1326 subsystem_dimension: usize,
1327 environment_dimension: usize,
1328 order: EntropyOrder,
1329) -> Result<Vec<f64>> {
1330 states
1331 .iter()
1332 .map(|state| entanglement_entropy(state, subsystem_dimension, environment_dimension, order))
1333 .collect()
1334}
1335
1336pub fn density_expectation(
1337 operator: &(impl LinearOperator + ?Sized),
1338 density: &[Complex64],
1339) -> Result<Complex64> {
1340 let shape = operator.shape();
1341 if shape.0 != shape.1 || density.len() != shape.0.saturating_mul(shape.0) {
1342 return Err(QmbedError::DimensionMismatch(
1343 "density expectation requires a square operator and matching density".into(),
1344 ));
1345 }
1346 let dimension = shape.0;
1347 let mut column = vec![Complex64::new(0.0, 0.0); dimension];
1348 let mut applied = vec![Complex64::new(0.0, 0.0); dimension];
1349 let mut trace = Complex64::new(0.0, 0.0);
1350 for density_column in 0..dimension {
1351 for row in 0..dimension {
1352 column[row] = density[row * dimension + density_column];
1353 }
1354 operator.apply(&column, &mut applied)?;
1355 trace += applied[density_column];
1356 }
1357 Ok(trace)
1358}
1359
1360pub fn density_quantum_fluctuation(
1367 operator: &(impl LinearOperator + ?Sized),
1368 density: &[Complex64],
1369) -> Result<Complex64> {
1370 let shape = operator.shape();
1371 if shape.0 != shape.1 || density.len() != shape.0.saturating_mul(shape.0) {
1372 return Err(QmbedError::DimensionMismatch(
1373 "density fluctuation requires a square operator and matching density".into(),
1374 ));
1375 }
1376 let dimension = shape.0;
1377 let mean = density_expectation(operator, density)?;
1378 let mut column = vec![Complex64::new(0.0, 0.0); dimension];
1379 let mut applied = vec![Complex64::new(0.0, 0.0); dimension];
1380 let mut applied_twice = vec![Complex64::new(0.0, 0.0); dimension];
1381 let mut second = Complex64::new(0.0, 0.0);
1382 for density_column in 0..dimension {
1383 for row in 0..dimension {
1384 column[row] = density[row * dimension + density_column];
1385 }
1386 operator.apply(&column, &mut applied)?;
1387 operator.apply(&applied, &mut applied_twice)?;
1388 second += applied_twice[density_column];
1389 }
1390 Ok(second - mean * mean)
1391}
1392
1393pub fn observables_vs_time(
1394 trajectory: &StateTrajectory,
1395 observables: &[(String, &dyn LinearOperator)],
1396) -> Result<HashMap<String, Vec<Complex64>>> {
1397 if trajectory.times.len() != trajectory.states.len() {
1398 return Err(QmbedError::DimensionMismatch(
1399 "trajectory times and states must have equal lengths".into(),
1400 ));
1401 }
1402 let mut result = HashMap::with_capacity(observables.len());
1403 for (name, operator) in observables {
1404 if name.is_empty() || result.contains_key(name) {
1405 return Err(QmbedError::InvalidOptions(
1406 "observable names must be nonempty and unique".into(),
1407 ));
1408 }
1409 let values = trajectory
1410 .states
1411 .iter()
1412 .map(|state| expectation(*operator, state))
1413 .collect::<Result<_>>()?;
1414 result.insert(name.clone(), values);
1415 }
1416 Ok(result)
1417}
1418
1419pub fn ed_state_vs_time(
1421 initial: &[Complex64],
1422 eigenvalues: &[f64],
1423 eigenvectors: &[Vec<Complex64>],
1424 times: &[f64],
1425) -> Result<StateTrajectory> {
1426 let dimension = initial.len();
1427 if times.is_empty()
1428 || times.iter().any(|time| !time.is_finite())
1429 || eigenvalues.len() != dimension
1430 || eigenvectors.len() != dimension
1431 || eigenvectors.iter().any(|vector| vector.len() != dimension)
1432 {
1433 return Err(QmbedError::DimensionMismatch(
1434 "complete eigensystem, state, and finite nonempty times are required".into(),
1435 ));
1436 }
1437 let coefficients: Vec<_> = eigenvectors
1438 .iter()
1439 .map(|vector| inner(vector, initial))
1440 .collect();
1441 let states = times
1442 .iter()
1443 .map(|time| {
1444 let mut state = vec![Complex64::new(0.0, 0.0); dimension];
1445 for ((energy, vector), coefficient) in
1446 eigenvalues.iter().zip(eigenvectors).zip(&coefficients)
1447 {
1448 let weight = coefficient * Complex64::new(0.0, -*time * energy).exp();
1449 for (value, eigenvector_value) in state.iter_mut().zip(vector) {
1450 *value += weight * *eigenvector_value;
1451 }
1452 }
1453 state
1454 })
1455 .collect();
1456 Ok(StateTrajectory {
1457 times: times.to_vec(),
1458 states,
1459 })
1460}
1461
1462pub fn ed_density_vs_time(
1465 initial: &[Complex64],
1466 eigenvalues: &[f64],
1467 eigenvectors: &[Vec<Complex64>],
1468 times: &[f64],
1469) -> Result<Vec<Vec<Complex64>>> {
1470 let dimension = eigenvalues.len();
1471 if initial.len() != dimension.saturating_mul(dimension)
1472 || eigenvectors.len() != dimension
1473 || eigenvectors.iter().any(|vector| vector.len() != dimension)
1474 || times.is_empty()
1475 || times.iter().any(|time| !time.is_finite())
1476 {
1477 return Err(QmbedError::DimensionMismatch(
1478 "density matrix and complete eigensystem dimensions do not match".into(),
1479 ));
1480 }
1481 let mut eigen_density = vec![Complex64::new(0.0, 0.0); dimension * dimension];
1482 for left in 0..dimension {
1483 for right in 0..dimension {
1484 for row in 0..dimension {
1485 for column in 0..dimension {
1486 eigen_density[left * dimension + right] += eigenvectors[left][row].conj()
1487 * initial[row * dimension + column]
1488 * eigenvectors[right][column];
1489 }
1490 }
1491 }
1492 }
1493 Ok(times
1494 .iter()
1495 .map(|time| {
1496 let mut density = vec![Complex64::new(0.0, 0.0); dimension * dimension];
1497 for row in 0..dimension {
1498 for column in 0..dimension {
1499 for left in 0..dimension {
1500 for right in 0..dimension {
1501 let phase = Complex64::new(
1502 0.0,
1503 -*time * (eigenvalues[left] - eigenvalues[right]),
1504 )
1505 .exp();
1506 density[row * dimension + column] += eigenvectors[left][row]
1507 * phase
1508 * eigen_density[left * dimension + right]
1509 * eigenvectors[right][column].conj();
1510 }
1511 }
1512 }
1513 }
1514 density
1515 })
1516 .collect())
1517}
1518
1519#[derive(Clone, Debug)]
1520pub struct DiagonalEnsemble {
1521 pub probabilities: Vec<f64>,
1522 pub mean_energy: f64,
1523 pub energy_variance: f64,
1524 pub entropy: f64,
1525}
1526
1527#[derive(Clone, Debug)]
1528pub struct DiagonalEnsembleColumn {
1529 pub ensemble: DiagonalEnsemble,
1530 pub diagonal_entropy: f64,
1531 pub observable: Option<f64>,
1532 pub temporal_fluctuation: Option<f64>,
1533 pub quantum_fluctuation: Option<f64>,
1534}
1535
1536fn summarize_diagonal_probabilities(
1537 eigenvalues: &[f64],
1538 mut probabilities: Vec<f64>,
1539) -> Result<DiagonalEnsemble> {
1540 let probability_sum = probabilities.iter().sum::<f64>();
1541 if probability_sum <= f64::EPSILON || !probability_sum.is_finite() {
1542 return Err(QmbedError::InvalidOptions(
1543 "eigenvectors have no finite overlap with the initial state".into(),
1544 ));
1545 }
1546 for probability in &mut probabilities {
1547 *probability /= probability_sum;
1548 }
1549 let mean_energy = probabilities
1550 .iter()
1551 .zip(eigenvalues)
1552 .map(|(probability, energy)| probability * energy)
1553 .sum::<f64>();
1554 let energy_variance = probabilities
1555 .iter()
1556 .zip(eigenvalues)
1557 .map(|(probability, energy)| probability * (energy - mean_energy).powi(2))
1558 .sum();
1559 let entropy = -probabilities
1560 .iter()
1561 .filter(|probability| **probability > f64::EPSILON)
1562 .map(|probability| probability * probability.ln())
1563 .sum::<f64>();
1564 Ok(DiagonalEnsemble {
1565 probabilities,
1566 mean_energy,
1567 energy_variance,
1568 entropy,
1569 })
1570}
1571
1572pub fn diagonal_ensemble_from_probabilities(
1573 eigenvalues: &[f64],
1574 probabilities: &[f64],
1575) -> Result<DiagonalEnsemble> {
1576 if eigenvalues.len() != probabilities.len()
1577 || probabilities
1578 .iter()
1579 .any(|probability| !probability.is_finite() || *probability < 0.0)
1580 {
1581 return Err(QmbedError::InvalidOptions(
1582 "diagonal probabilities must be finite, nonnegative, and match the spectrum".into(),
1583 ));
1584 }
1585 summarize_diagonal_probabilities(eigenvalues, probabilities.to_vec())
1586}
1587
1588pub fn diagonal_density_matrix(
1589 eigenvectors: &[Vec<Complex64>],
1590 probabilities: &[f64],
1591) -> Result<Vec<Complex64>> {
1592 let dimension = eigenvectors.len();
1593 if probabilities.len() != dimension
1594 || eigenvectors.iter().any(|vector| vector.len() != dimension)
1595 {
1596 return Err(QmbedError::DimensionMismatch(
1597 "diagonal density probabilities and eigenvectors do not match".into(),
1598 ));
1599 }
1600 if probabilities
1601 .iter()
1602 .any(|probability| !probability.is_finite() || *probability < 0.0)
1603 {
1604 return Err(QmbedError::InvalidOptions(
1605 "diagonal density probabilities must be finite and nonnegative".into(),
1606 ));
1607 }
1608 let mut density = vec![Complex64::new(0.0, 0.0); dimension * dimension];
1609 for (probability, vector) in probabilities.iter().zip(eigenvectors) {
1610 for row in 0..dimension {
1611 for column in 0..dimension {
1612 density[row * dimension + column] +=
1613 *probability * vector[row] * vector[column].conj();
1614 }
1615 }
1616 }
1617 Ok(density)
1618}
1619
1620fn distribution_entropy(probabilities: &[f64], alpha: f64) -> Result<f64> {
1621 if !alpha.is_finite() || alpha < 0.0 {
1622 return Err(QmbedError::InvalidOptions(
1623 "Renyi alpha must be finite and nonnegative".into(),
1624 ));
1625 }
1626 if (alpha - 1.0).abs() <= f64::EPSILON {
1627 return Ok(-probabilities
1628 .iter()
1629 .filter(|probability| **probability > f64::EPSILON)
1630 .map(|probability| probability * probability.ln())
1631 .sum::<f64>());
1632 }
1633 let moment = probabilities
1634 .iter()
1635 .filter(|probability| **probability > f64::EPSILON)
1636 .map(|probability| probability.powf(alpha))
1637 .sum::<f64>();
1638 if moment <= 0.0 || !moment.is_finite() {
1639 return Err(QmbedError::InvalidOptions(
1640 "Renyi probability moment is not positive and finite".into(),
1641 ));
1642 }
1643 Ok(moment.ln() / (1.0 - alpha))
1644}
1645
1646pub fn analyze_diagonal_ensemble(
1650 eigenvalues: &[f64],
1651 eigenvectors: &[Vec<Complex64>],
1652 probability_columns: &[Vec<f64>],
1653 observable: Option<&dyn LinearOperator>,
1654 alpha: f64,
1655) -> Result<Vec<DiagonalEnsembleColumn>> {
1656 let dimension = eigenvalues.len();
1657 let mut sorted_eigenvalues = eigenvalues.to_vec();
1658 sorted_eigenvalues.sort_by(f64::total_cmp);
1659 if sorted_eigenvalues.windows(2).any(|window| {
1660 let scale = window[0].abs().max(window[1].abs()).max(1.0);
1661 (window[1] - window[0]).abs() <= 1.0e-12 * scale
1662 }) {
1663 return Err(QmbedError::InvalidOptions(
1664 "diagonal-ensemble formulas require a nondegenerate spectrum".into(),
1665 ));
1666 }
1667 if eigenvectors.len() != dimension
1668 || eigenvectors.iter().any(|vector| vector.len() != dimension)
1669 {
1670 return Err(QmbedError::DimensionMismatch(
1671 "diagonal-ensemble eigensystem must be square".into(),
1672 ));
1673 }
1674 if probability_columns.is_empty() {
1675 return Err(QmbedError::InvalidOptions(
1676 "diagonal-ensemble analysis needs at least one probability column".into(),
1677 ));
1678 }
1679
1680 type ObservableStatistics = (Vec<f64>, Vec<Vec<f64>>, Vec<f64>);
1681 let observable_statistics = observable
1682 .map(|observable| -> Result<ObservableStatistics> {
1683 if observable.shape() != (dimension, dimension) {
1684 return Err(QmbedError::DimensionMismatch(
1685 "diagonal-ensemble observable and eigensystem do not match".into(),
1686 ));
1687 }
1688 let mut applied = Vec::with_capacity(dimension);
1689 for vector in eigenvectors {
1690 let mut output = vec![Complex64::new(0.0, 0.0); dimension];
1691 observable.apply(vector, &mut output)?;
1692 applied.push(output);
1693 }
1694 let mut diagonal = vec![0.0; dimension];
1695 let mut squared_elements = vec![vec![0.0; dimension]; dimension];
1696 let mut squared_diagonal = vec![0.0; dimension];
1697 for row in 0..dimension {
1698 for column in 0..dimension {
1699 let element = inner(&eigenvectors[row], &applied[column]);
1700 if row == column {
1701 if element.im.abs() > 1.0e-9 {
1702 return Err(QmbedError::InvalidOptions(
1703 "diagonal-ensemble observable must be Hermitian".into(),
1704 ));
1705 }
1706 diagonal[row] = element.re;
1707 }
1708 let magnitude = element.norm_sqr();
1709 squared_elements[row][column] = magnitude;
1710 squared_diagonal[row] += magnitude;
1711 }
1712 }
1713 Ok((diagonal, squared_elements, squared_diagonal))
1714 })
1715 .transpose()?;
1716
1717 probability_columns
1718 .iter()
1719 .map(|probabilities| {
1720 let ensemble = diagonal_ensemble_from_probabilities(eigenvalues, probabilities)?;
1721 let diagonal_entropy = distribution_entropy(&ensemble.probabilities, alpha)?;
1722 let (observable, temporal_fluctuation, quantum_fluctuation) = if let Some((
1723 diagonal,
1724 squared_elements,
1725 squared_diagonal,
1726 )) =
1727 &observable_statistics
1728 {
1729 let expectation = diagonal
1730 .iter()
1731 .zip(&ensemble.probabilities)
1732 .map(|(value, probability)| value * probability)
1733 .sum::<f64>();
1734 let mut temporal_variance = 0.0;
1735 for (row, squared_row) in squared_elements.iter().enumerate() {
1736 for (column, squared_element) in squared_row.iter().enumerate() {
1737 if row != column {
1738 temporal_variance += ensemble.probabilities[row]
1739 * squared_element
1740 * ensemble.probabilities[column];
1741 }
1742 }
1743 }
1744 let total_second_moment = squared_diagonal
1745 .iter()
1746 .zip(&ensemble.probabilities)
1747 .map(|(value, probability)| value * probability)
1748 .sum::<f64>();
1749 let quantum_variance =
1750 total_second_moment - temporal_variance - expectation.powi(2);
1751 (
1752 Some(expectation),
1753 Some(temporal_variance.max(0.0).sqrt()),
1754 Some(quantum_variance.max(0.0).sqrt()),
1755 )
1756 } else {
1757 (None, None, None)
1758 };
1759 Ok(DiagonalEnsembleColumn {
1760 ensemble,
1761 diagonal_entropy,
1762 observable,
1763 temporal_fluctuation,
1764 quantum_fluctuation,
1765 })
1766 })
1767 .collect()
1768}
1769
1770pub fn diagonal_ensemble(
1771 eigenvalues: &[f64],
1772 eigenvectors: &[Vec<Complex64>],
1773 initial: &[Complex64],
1774) -> Result<DiagonalEnsemble> {
1775 if eigenvalues.len() != eigenvectors.len()
1776 || eigenvectors
1777 .iter()
1778 .any(|vector| vector.len() != initial.len())
1779 {
1780 return Err(QmbedError::DimensionMismatch(
1781 "eigensystem and initial state dimensions do not match".into(),
1782 ));
1783 }
1784 let initial_norm = inner(initial, initial).re;
1785 if !initial_norm.is_finite() || initial_norm <= f64::EPSILON {
1786 return Err(QmbedError::InvalidOptions(
1787 "initial state must have positive finite norm".into(),
1788 ));
1789 }
1790 let probabilities: Vec<_> = eigenvectors
1791 .iter()
1792 .map(|vector| inner(vector, initial).norm_sqr() / initial_norm)
1793 .collect();
1794 summarize_diagonal_probabilities(eigenvalues, probabilities)
1795}
1796
1797pub fn diagonal_ensemble_density(
1798 eigenvalues: &[f64],
1799 eigenvectors: &[Vec<Complex64>],
1800 initial_density: &[Complex64],
1801) -> Result<DiagonalEnsemble> {
1802 let dimension = eigenvalues.len();
1803 if eigenvectors.len() != dimension
1804 || eigenvectors.iter().any(|vector| vector.len() != dimension)
1805 || initial_density.len() != dimension.saturating_mul(dimension)
1806 {
1807 return Err(QmbedError::DimensionMismatch(
1808 "eigensystem and initial density dimensions do not match".into(),
1809 ));
1810 }
1811 let trace: Complex64 = (0..dimension)
1812 .map(|index| initial_density[index * dimension + index])
1813 .sum();
1814 if trace.im.abs() > 1.0e-10 || !trace.re.is_finite() || trace.re <= f64::EPSILON {
1815 return Err(QmbedError::InvalidOptions(
1816 "initial density must have a positive real trace".into(),
1817 ));
1818 }
1819 let probabilities = eigenvectors
1820 .iter()
1821 .map(|vector| {
1822 let mut value = Complex64::new(0.0, 0.0);
1823 for row in 0..dimension {
1824 for column in 0..dimension {
1825 value += vector[row].conj()
1826 * initial_density[row * dimension + column]
1827 * vector[column];
1828 }
1829 }
1830 if value.im.abs() > 1.0e-10 || value.re < -1.0e-10 {
1831 Err(QmbedError::InvalidOptions(
1832 "initial density is not positive in the supplied eigenbasis".into(),
1833 ))
1834 } else {
1835 Ok(value.re.max(0.0) / trace.re)
1836 }
1837 })
1838 .collect::<Result<Vec<_>>>()?;
1839 summarize_diagonal_probabilities(eigenvalues, probabilities)
1840}
1841
1842pub fn diagonal_ensemble_observable(
1843 ensemble: &DiagonalEnsemble,
1844 eigenvectors: &[Vec<Complex64>],
1845 observable: &(impl LinearOperator + ?Sized),
1846) -> Result<Complex64> {
1847 if ensemble.probabilities.len() != eigenvectors.len()
1848 || eigenvectors.iter().any(|vector| {
1849 vector.len() != observable.shape().0 || observable.shape().0 != observable.shape().1
1850 })
1851 {
1852 return Err(QmbedError::DimensionMismatch(
1853 "diagonal ensemble, eigenvectors, and observable do not match".into(),
1854 ));
1855 }
1856 ensemble
1857 .probabilities
1858 .iter()
1859 .zip(eigenvectors)
1860 .try_fold(Complex64::new(0.0, 0.0), |total, (probability, vector)| {
1861 Ok(total + *probability * expectation(observable, vector)?)
1862 })
1863}
1864
1865pub fn energy_window_indices(
1866 eigenvalues: &[f64],
1867 center: f64,
1868 half_width: f64,
1869) -> Result<Vec<usize>> {
1870 if !center.is_finite()
1871 || !half_width.is_finite()
1872 || half_width < 0.0
1873 || eigenvalues.iter().any(|value| !value.is_finite())
1874 {
1875 return Err(QmbedError::InvalidOptions(
1876 "energy-window inputs must be finite and the half-width nonnegative".into(),
1877 ));
1878 }
1879 Ok(eigenvalues
1880 .iter()
1881 .enumerate()
1882 .filter_map(|(index, value)| ((*value - center).abs() <= half_width).then_some(index))
1883 .collect())
1884}
1885
1886pub fn kl_divergence(left: &[f64], right: &[f64]) -> Result<f64> {
1887 if left.len() != right.len()
1888 || left.is_empty()
1889 || left
1890 .iter()
1891 .chain(right)
1892 .any(|value| !value.is_finite() || *value <= 0.0)
1893 {
1894 return Err(QmbedError::InvalidOptions(
1895 "KL distributions must be nonempty, equal-length, finite, and strictly positive".into(),
1896 ));
1897 }
1898 let left_sum = left.iter().sum::<f64>();
1899 let right_sum = right.iter().sum::<f64>();
1900 if (left_sum - 1.0).abs() > 1.0e-13 || (right_sum - 1.0).abs() > 1.0e-13 {
1901 return Err(QmbedError::InvalidOptions(
1902 "KL distributions must be normalized".into(),
1903 ));
1904 }
1905 Ok(left
1906 .iter()
1907 .zip(right)
1908 .map(|(probability, reference)| probability * (probability / reference).ln())
1909 .sum::<f64>()
1910 .max(0.0))
1911}
1912
1913pub fn mean_level_spacing(eigenvalues: &[f64]) -> Result<f64> {
1915 if eigenvalues.len() < 3 || eigenvalues.iter().any(|value| !value.is_finite()) {
1916 return Err(QmbedError::InvalidOptions(
1917 "level-spacing statistics require at least three finite values".into(),
1918 ));
1919 }
1920 if eigenvalues.windows(2).any(|pair| pair[0] > pair[1]) {
1921 return Err(QmbedError::InvalidOptions(
1922 "level spectrum must be sorted in ascending order".into(),
1923 ));
1924 }
1925 let gaps: Vec<_> = eigenvalues
1926 .windows(2)
1927 .map(|pair| pair[1] - pair[0])
1928 .collect();
1929 if gaps.contains(&0.0) {
1930 return Ok(f64::NAN);
1931 }
1932 Ok(gaps
1933 .windows(2)
1934 .map(|pair| pair[0].min(pair[1]) / pair[0].max(pair[1]))
1935 .sum::<f64>()
1936 / (gaps.len() - 1) as f64)
1937}
1938
1939pub fn states_to_array(
1940 states: &[u128],
1941 sites: usize,
1942 local_dimension: usize,
1943) -> Result<Vec<Vec<usize>>> {
1944 if local_dimension < 2 {
1945 return Err(QmbedError::InvalidSector(
1946 "local dimension must be at least two".into(),
1947 ));
1948 }
1949 let base = local_dimension as u128;
1950 let mut result = Vec::with_capacity(states.len());
1951 for &state in states {
1952 let mut value = state;
1953 let mut occupations = Vec::with_capacity(sites);
1954 for _ in 0..sites {
1955 occupations.push((value % base) as usize);
1956 value /= base;
1957 }
1958 if value != 0 {
1959 return Err(QmbedError::StateNotInBasis);
1960 }
1961 result.push(occupations);
1962 }
1963 Ok(result)
1964}
1965
1966pub fn array_to_states(arrays: &[Vec<usize>], local_dimension: usize) -> Result<Vec<u128>> {
1967 if local_dimension < 2 {
1968 return Err(QmbedError::InvalidSector(
1969 "local dimension must be at least two".into(),
1970 ));
1971 }
1972 let sites = arrays.first().map_or(0, Vec::len);
1973 let base = local_dimension as u128;
1974 arrays
1975 .iter()
1976 .map(|occupations| {
1977 if occupations.len() != sites
1978 || occupations
1979 .iter()
1980 .any(|occupation| *occupation >= local_dimension)
1981 {
1982 return Err(QmbedError::InvalidSector(
1983 "occupation arrays must have equal length and valid digits".into(),
1984 ));
1985 }
1986 let mut state = 0_u128;
1987 let mut place = 1_u128;
1988 for &occupation in occupations {
1989 state = state
1990 .checked_add(place.checked_mul(occupation as u128).ok_or_else(|| {
1991 QmbedError::UnsupportedBackend("state encoding overflow".into())
1992 })?)
1993 .ok_or_else(|| {
1994 QmbedError::UnsupportedBackend("state encoding overflow".into())
1995 })?;
1996 place = place.checked_mul(base).ok_or_else(|| {
1997 QmbedError::UnsupportedBackend("state encoding overflow".into())
1998 })?;
1999 }
2000 Ok(state)
2001 })
2002 .collect()
2003}
2004
2005pub fn ints_to_array(states: &[u128], sites: usize) -> Result<Vec<Vec<u8>>> {
2007 if sites > 128 {
2008 return Err(QmbedError::UnsupportedBackend(
2009 "u128 binary conversion supports at most 128 sites".into(),
2010 ));
2011 }
2012 if states
2013 .iter()
2014 .any(|state| sites < 128 && *state >= (1_u128 << sites))
2015 {
2016 return Err(QmbedError::StateNotInBasis);
2017 }
2018 Ok(states
2019 .iter()
2020 .map(|state| {
2021 (0..sites)
2022 .map(|column| ((state >> (sites - column - 1)) & 1) as u8)
2023 .collect()
2024 })
2025 .collect())
2026}
2027
2028pub fn array_to_ints(arrays: &[Vec<u8>]) -> Result<Vec<u128>> {
2030 let sites = arrays.first().map_or(0, Vec::len);
2031 if sites > 128 {
2032 return Err(QmbedError::UnsupportedBackend(
2033 "u128 binary conversion supports at most 128 sites".into(),
2034 ));
2035 }
2036 arrays
2037 .iter()
2038 .map(|row| {
2039 if row.len() != sites || row.iter().any(|bit| *bit > 1) {
2040 return Err(QmbedError::InvalidSector(
2041 "binary state rows must have equal lengths and contain only zero or one".into(),
2042 ));
2043 }
2044 Ok(row
2045 .iter()
2046 .fold(0_u128, |state, bit| (state << 1) | u128::from(*bit)))
2047 })
2048 .collect()
2049}
2050
2051pub fn project_operator(
2053 operator: &(impl LinearOperator + ?Sized),
2054 projector: &BasisProjector,
2055 format: MatrixFormat,
2056) -> Result<Operator> {
2057 let source_dimension = projector.source_dimension();
2058 let reduced_dimension = projector.reduced_dimension();
2059 let operator_dimension = operator.shape();
2060 if operator_dimension.0 != operator_dimension.1
2061 || (operator_dimension.0 != source_dimension && operator_dimension.0 != reduced_dimension)
2062 {
2063 return Err(QmbedError::DimensionMismatch(
2064 "operator dimension must match the parent or reduced projector space".into(),
2065 ));
2066 }
2067 if operator_dimension.0 == reduced_dimension {
2068 let mut parent_input = vec![Complex64::new(0.0, 0.0); source_dimension];
2069 let mut reduced_input = vec![Complex64::new(0.0, 0.0); reduced_dimension];
2070 let mut reduced_output = vec![Complex64::new(0.0, 0.0); reduced_dimension];
2071 let mut parent_output = vec![Complex64::new(0.0, 0.0); source_dimension];
2072 let mut triplets = Vec::new();
2073 for column in 0..source_dimension {
2074 parent_input.fill(Complex64::new(0.0, 0.0));
2075 parent_input[column] = Complex64::new(1.0, 0.0);
2076 projector.project(&parent_input, &mut reduced_input)?;
2077 operator.apply(&reduced_input, &mut reduced_output)?;
2078 projector.apply(&reduced_output, &mut parent_output)?;
2079 for (row, &value) in parent_output.iter().enumerate() {
2080 if value.norm() > f64::EPSILON {
2081 triplets.push((row, column, value));
2082 }
2083 }
2084 }
2085 return Operator::from_triplets(source_dimension, source_dimension, triplets, format);
2086 }
2087 let mut reduced_input = vec![Complex64::new(0.0, 0.0); reduced_dimension];
2088 let mut parent_input = vec![Complex64::new(0.0, 0.0); source_dimension];
2089 let mut parent_output = vec![Complex64::new(0.0, 0.0); source_dimension];
2090 let mut reduced_output = vec![Complex64::new(0.0, 0.0); reduced_dimension];
2091 let mut triplets = Vec::new();
2092 for column in 0..reduced_dimension {
2093 reduced_input.fill(Complex64::new(0.0, 0.0));
2094 reduced_input[column] = Complex64::new(1.0, 0.0);
2095 projector.apply(&reduced_input, &mut parent_input)?;
2096 operator.apply(&parent_input, &mut parent_output)?;
2097 projector.project(&parent_output, &mut reduced_output)?;
2098 for (row, &value) in reduced_output.iter().enumerate() {
2099 if value.norm() > f64::EPSILON {
2100 triplets.push((row, column, value));
2101 }
2102 }
2103 }
2104 Operator::from_triplets(reduced_dimension, reduced_dimension, triplets, format)
2105}