Skip to main content

qmbed/measure/
mod.rs

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/// Gauge-independent finite-dimensional subspace.
11#[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
77/// Mean squared principal-angle cosine between two subspaces.
78pub 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
124/// Variance `||A psi||^2 - |<psi|A|psi>|^2` for a normalized state.
125pub 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
148/// Raw pure-state fluctuation
149/// `⟨ψ|A²|ψ⟩ - ⟨ψ|A|ψ⟩²` without normalizing `ψ`.
150///
151/// This preserves array-oriented compatibility semantics, including the zero
152/// vector, while [`quantum_fluctuation`] remains the strict normalized-state
153/// variance API.
154pub 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
172/// Reduced density matrix of the first factor of a pure bipartite state.
173pub 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
212/// Reduced density matrix of the first factor for a row-major mixed state.
213pub 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
339/// Tensor-product dimensions selected by an arbitrary retained site set.
340pub 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
348/// Fock-space signs induced by grouping selected fermionic modes into a
349/// subsystem. The returned phase is indexed by the original packed state.
350///
351/// Modes inside the environment and subsystem keep the orders supplied by the
352/// ordinary subsystem layout; only the swaps needed to group the two factors
353/// contribute a sign.
354pub 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/// One disjoint set of binary modes sharing an exchange phase.
363#[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
395/// Exchange phases induced by regrouping a tensor product with selected
396/// mutually anticommuting site groups.
397///
398/// Sites outside these groups commute. Distinct groups also commute with one
399/// another, while occupied binary sites inside the same group contribute one
400/// minus sign for every order inversion. This is the minimal generalization
401/// needed for mixed spin/boson/fermion user bases without assigning global
402/// fermionic statistics to the entire product space.
403pub 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
416/// General exchange phases induced by regrouping disjoint binary-mode groups.
417///
418/// Each inversion inside a group contributes that group's unit-modulus phase.
419/// Distinct groups and sites outside all groups commute.
420pub 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
503/// Apply arbitrary selected exchange phases to a pure state.
504pub 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
526/// Apply selected noncommuting-group exchange phases to a pure state.
527pub 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
546/// Apply fermionic subsystem-ordering phases to a packed pure state.
547pub 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
564/// Apply selected noncommuting-group exchange phases to both density axes.
565pub 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
587/// Apply arbitrary selected exchange phases to both density-matrix axes.
588pub 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
613/// Apply the same fermionic mode permutation to both density-matrix axes.
614pub 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
634/// Reduced density matrix for an arbitrary retained set of mixed-radix sites.
635/// Site zero is the least-significant local digit, matching the basis encodings.
636pub 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
675/// Mixed-state partial trace for an arbitrary retained site set.
676pub 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
908/// Reduced density matrix of a pure state stored in an arbitrary binary
909/// sector basis.
910///
911/// The contraction groups amplitudes by the environment bit pattern.  It
912/// never enumerates or allocates the unrestricted `2^total_sites` parent
913/// space; memory is proportional to the supplied sector and the requested
914/// dense subsystem matrix.
915pub 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
957/// Reduced density matrix of a mixed state stored in an arbitrary binary
958/// sector basis.
959///
960/// Only pairs with the same environment key are visited.  The input remains a
961/// dense matrix in the selected sector, but no unrestricted parent density
962/// matrix is formed.
963pub 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
1025/// Entanglement spectrum of a pure state represented in a binary sector.
1026pub 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
1055/// Entanglement entropy of a pure state represented in a binary sector.
1056pub 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
1085/// Entropy of an already validated density-matrix spectrum.
1086pub 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
1142/// Canonical Schmidt spectrum for a pure bipartition.
1143///
1144/// A pure state has the same nonzero reduced spectrum on both sides of a
1145/// bipartition. Computing two density-matrix decompositions independently is
1146/// both wasteful and capable of producing last-bit entropy differences. This
1147/// routine chooses the smaller factor, with a lexicographic tie-break on
1148/// sorted site sets, so complementary calls take the identical numerical
1149/// path. Optional noncommuting groups are reordered against that same
1150/// canonical factor before the spectrum is evaluated.
1151pub 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
1252/// Sorted eigenvalue spectrum of a row-major positive semidefinite density matrix.
1253pub 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
1360/// Density-matrix fluctuation `Tr(ρ A²) - Tr(ρ A)²`.
1361///
1362/// The density matrix follows the same row-major convention as
1363/// [`density_expectation`]. Normalization is deliberately not imposed: callers
1364/// may use normalized density matrices or preserve QuSpin's direct linear
1365/// algebra semantics for weighted ensembles.
1366pub 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
1419/// Exact time evolution from a complete eigendecomposition.
1420pub 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
1462/// Density-matrix counterpart of [`ed_state_vs_time`], with row-major input
1463/// and output matrices.
1464pub 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
1646/// Analyze one or more diagonal probability distributions in a shared
1647/// eigensystem. Optional observable statistics are evaluated matrix-free in
1648/// that eigensystem, so stored and matrix-free operators share one path.
1649pub 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
1913/// Mean adjacent-gap ratio of an ordered spectrum.
1914pub 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
2005/// Convert integer states to most-significant-site-first binary rows.
2006pub 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
2028/// Convert most-significant-site-first binary rows to integer states.
2029pub 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
2051/// Compute `P† A P` one reduced column at a time.
2052pub 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}