Skip to main content

qmbed/basis/
mod.rs

1use std::cmp::Ordering;
2use std::collections::{HashMap, HashSet, VecDeque};
3use std::hash::Hash;
4use std::sync::{Arc, RwLock};
5
6use num_bigint::BigUint;
7use num_complex::Complex64;
8use smallvec::SmallVec;
9
10use crate::operator::{LinearOperator, MatrixFormat, Operator, check_apply_shape};
11use crate::{QmbedError, Result};
12
13/// Compact collection of local-operator destinations.
14///
15/// The common zero-, one-, and two-destination cases stay inline. Operators
16/// with wider branching use the same interface and spill to heap storage
17/// automatically.
18pub type LocalTransitions<State> = SmallVec<[(State, Complex64); 2]>;
19
20/// Integer-like binary occupation state used by sector-native subsystem
21/// contractions.
22///
23/// The trait deliberately exposes only one bit query.  Subsystem algorithms
24/// therefore work with packed `u128`, fixed-width wide states, and the
25/// runtime-erased state without depending on their storage representation.
26pub trait BinaryState: Copy + Eq + Hash {
27    /// Return whether the binary mode at `index` is occupied.
28    fn bit(&self, index: usize) -> Result<bool>;
29}
30
31impl BinaryState for u128 {
32    fn bit(&self, index: usize) -> Result<bool> {
33        if index >= u128::BITS as usize {
34            return Err(QmbedError::InvalidSite {
35                site: index,
36                sites: u128::BITS as usize,
37            });
38        }
39        Ok(*self & (1_u128 << index) != 0)
40    }
41}
42
43/// Canonical image of one physical state under a basis reduction.
44///
45/// `phase / sqrt(orbit_size)` is the coefficient of `state` in the normalized
46/// reduced vector labelled by `representative`. Keeping this convention at the
47/// basis boundary lets projectors, cross-sector operators, and language
48/// bindings share one source of truth.
49#[derive(Clone, Copy, Debug, PartialEq)]
50pub struct ReductionImage<State> {
51    representative: State,
52    phase: Complex64,
53    orbit_size: usize,
54}
55
56impl<State> ReductionImage<State> {
57    /// Validate and store the canonical representative, unit phase, and orbit size.
58    ///
59    /// The phase must be finite with unit magnitude and `orbit_size` must be
60    /// positive. These invariants make [`ReductionImage::amplitude`] safe to
61    /// use in projectors and cross-sector assembly.
62    pub fn new(representative: State, phase: Complex64, orbit_size: usize) -> Result<Self> {
63        if orbit_size == 0 || !phase.re.is_finite() || !phase.im.is_finite() {
64            return Err(QmbedError::InternalState(
65                "a reduction image requires a positive orbit and finite phase".into(),
66            ));
67        }
68        if (phase.norm() - 1.0).abs() > 1.0e-10 {
69            return Err(QmbedError::InternalState(
70                "a reduction-image phase must have unit magnitude".into(),
71            ));
72        }
73        Ok(Self {
74            representative,
75            phase,
76            orbit_size,
77        })
78    }
79
80    /// Return the canonical state labelling the reduced-basis vector.
81    pub const fn representative(&self) -> &State {
82        &self.representative
83    }
84
85    /// Return the unit-modulus physical-to-canonical phase.
86    pub const fn phase(&self) -> Complex64 {
87        self.phase
88    }
89
90    /// Return the number of physical states in the symmetry orbit.
91    pub const fn orbit_size(&self) -> usize {
92        self.orbit_size
93    }
94
95    /// Normalized coefficient of the physical state in its reduced vector.
96    pub fn amplitude(&self) -> Complex64 {
97        self.phase / (self.orbit_size as f64).sqrt()
98    }
99}
100
101/// Finite Hilbert-space basis and its local operator semantics.
102pub trait Basis: Send + Sync {
103    /// Integer-like state representation used by this basis.
104    type State: Copy + Eq + Send + Sync;
105
106    /// Return the number of vectors in the basis.
107    fn len(&self) -> usize;
108    /// Return the encoded state at a basis index.
109    ///
110    /// Implementations return [`QmbedError::StateNotInBasis`] for an invalid
111    /// row rather than panicking.
112    fn state(&self, index: usize) -> Result<Self::State>;
113    /// Locate an encoded state in the basis.
114    fn index(&self, state: Self::State) -> Result<usize>;
115    /// Apply one local operator string to one encoded state.
116    ///
117    /// The result is `None` when the action is exactly zero. Bases whose
118    /// actions branch override [`Basis::apply_local_transitions`].
119    fn apply_local(
120        &self,
121        state: Self::State,
122        operator: &str,
123        sites: &[usize],
124    ) -> Result<Option<(Self::State, Complex64)>>;
125
126    /// Applies a local operator and returns every nonzero destination.
127    ///
128    /// Most spin-one-half, boson ladder, and fermion strings are deterministic,
129    /// so the default implementation wraps [`Basis::apply_local`]. Higher-spin
130    /// Cartesian operators and general user-defined local matrices can branch
131    /// and override this method without changing the universal assembler.
132    fn apply_local_transitions(
133        &self,
134        state: Self::State,
135        operator: &str,
136        sites: &[usize],
137    ) -> Result<LocalTransitions<Self::State>> {
138        Ok(self
139            .apply_local(state, operator, sites)?
140            .into_iter()
141            .collect())
142    }
143
144    /// Local action before symmetry-sector reduction. Cross-sector builders
145    /// use this path and let the target basis perform the reduction.
146    fn apply_local_unreduced_transitions(
147        &self,
148        state: Self::State,
149        operator: &str,
150        sites: &[usize],
151    ) -> Result<LocalTransitions<Self::State>> {
152        self.apply_local_transitions(state, operator, sites)
153    }
154
155    /// Streams unreduced destinations directly to a consumer.
156    ///
157    /// This is the universal hot-path interface used by assemblers. The
158    /// default covers deterministic bases without constructing an intermediate
159    /// collection; branching and symmetry-reduced bases override it while
160    /// preserving the same consumer contract.
161    fn visit_local_unreduced_transitions<F>(
162        &self,
163        state: Self::State,
164        operator: &str,
165        sites: &[usize],
166        mut visit: F,
167    ) -> Result<()>
168    where
169        Self: Sized,
170        F: FnMut(Self::State, Complex64) -> Result<()>,
171    {
172        if let Some((target, amplitude)) = self.apply_local(state, operator, sites)? {
173            visit(target, amplitude)?;
174        }
175        Ok(())
176    }
177
178    /// Streams a local action whose operator symbols were parsed at the API
179    /// boundary.
180    ///
181    /// The default preserves compatibility with custom bases. Built-in bases
182    /// override this method so repeated state/coupling actions do not rescan
183    /// the same operator string.
184    #[doc(hidden)]
185    fn visit_preparsed_local_unreduced_transitions<F>(
186        &self,
187        state: Self::State,
188        operator: &str,
189        symbols: &[char],
190        split: Option<usize>,
191        sites: &[usize],
192        visit: F,
193    ) -> Result<()>
194    where
195        Self: Sized,
196        F: FnMut(Self::State, Complex64) -> Result<()>,
197    {
198        let _ = (symbols, split);
199        self.visit_local_unreduced_transitions(state, operator, sites, visit)
200    }
201
202    /// Orbit size of a canonical source state used in projector normalization.
203    fn transition_orbit_size(&self, _state: Self::State) -> Result<usize> {
204        Ok(1)
205    }
206
207    /// Return the canonical reduction metadata for a physical state.
208    ///
209    /// Explicit bases use a one-state orbit. Symmetry-reduced bases override
210    /// this query without exposing their lookup-table representation.
211    fn reduction_image(&self, state: Self::State) -> Result<Option<ReductionImage<Self::State>>> {
212        match self.index(state) {
213            Ok(_) => Ok(Some(ReductionImage::new(
214                state,
215                Complex64::new(1.0, 0.0),
216                1,
217            )?)),
218            Err(QmbedError::StateNotInBasis) => Ok(None),
219            Err(error) => Err(error),
220        }
221    }
222
223    /// Map an unreduced physical target state into this basis.
224    fn reduce_transition(
225        &self,
226        state: Self::State,
227        source_orbit_size: usize,
228    ) -> Result<Option<(Self::State, Complex64)>> {
229        Ok(self.reduction_image(state)?.map(|image| {
230            (
231                image.representative,
232                (source_orbit_size as f64 / image.orbit_size as f64).sqrt() * image.phase.conj(),
233            )
234        }))
235    }
236
237    /// Reduces a physical target and locates its row in one operation.
238    ///
239    /// Assemblers use this fused boundary to avoid looking up the same state
240    /// once during reduction and again during indexing.
241    fn index_transition(
242        &self,
243        state: Self::State,
244        source_orbit_size: usize,
245    ) -> Result<Option<(usize, Complex64)>> {
246        let Some((representative, amplitude)) = self.reduce_transition(state, source_orbit_size)?
247        else {
248            return Ok(None);
249        };
250        Ok(Some((self.index(representative)?, amplitude)))
251    }
252
253    /// Whether a local operator string preserves the particle-sector
254    /// constraints represented by this basis. Unconstrained and custom bases
255    /// accept every syntactically valid string by default.
256    fn operator_preserves_particle_sector(&self, _operator: &str) -> Result<bool> {
257        Ok(true)
258    }
259
260    /// Site-aware particle-sector check for bases whose local labels encode
261    /// species as well as position.
262    ///
263    /// Most bases depend only on the operator string and inherit this default.
264    /// Multi-species bases may override it so a unified orbital convention
265    /// remains physically meaningful without inserting a species separator.
266    fn operator_preserves_particle_sector_on_sites(
267        &self,
268        operator: &str,
269        _sites: &[usize],
270    ) -> Result<bool> {
271        self.operator_preserves_particle_sector(operator)
272    }
273
274    /// Return whether the basis contains no vectors.
275    fn is_empty(&self) -> bool {
276        self.len() == 0
277    }
278}
279
280fn operator_number_change(operator: &str) -> Result<Option<i32>> {
281    let mut change = 0_i32;
282    for character in operator.chars().filter(|character| *character != '|') {
283        match character {
284            '+' => change += 1,
285            '-' => change -= 1,
286            'x' | 'y' => return Ok(None),
287            'I' | 'n' | 'z' => {}
288            _ => return Err(QmbedError::InvalidOperator(operator.into())),
289        }
290    }
291    Ok(Some(change))
292}
293
294fn operator_number_changes(operator: &str) -> Result<Vec<i32>> {
295    let mut changes = vec![0_i32];
296    for character in operator.chars().filter(|character| *character != '|') {
297        match character {
298            '+' => {
299                for change in &mut changes {
300                    *change += 1;
301                }
302            }
303            '-' => {
304                for change in &mut changes {
305                    *change -= 1;
306                }
307            }
308            'x' | 'y' => {
309                let mut lowered = changes.clone();
310                for change in &mut changes {
311                    *change += 1;
312                }
313                for change in &mut lowered {
314                    *change -= 1;
315                }
316                changes.extend(lowered);
317                changes.sort_unstable();
318                changes.dedup();
319            }
320            'I' | 'n' | 'z' => {}
321            _ => return Err(QmbedError::InvalidOperator(operator.into())),
322        }
323    }
324    Ok(changes)
325}
326
327fn canonical_particle_sectors(
328    sectors: impl IntoIterator<Item = usize>,
329    maximum: usize,
330    family: &str,
331) -> Result<Vec<usize>> {
332    let mut sectors = sectors.into_iter().collect::<Vec<_>>();
333    if sectors.is_empty() {
334        return Err(QmbedError::InvalidSector(format!(
335            "{family} particle-sector union must be nonempty"
336        )));
337    }
338    if sectors.iter().any(|&sector| sector > maximum) {
339        return Err(QmbedError::InvalidSector(format!(
340            "{family} particle sector exceeds the local capacity"
341        )));
342    }
343    sectors.sort_unstable();
344    sectors.dedup();
345    Ok(sectors)
346}
347
348fn selected_sectors_preserve_changes(
349    fixed: Option<usize>,
350    sectors: Option<&[usize]>,
351    maximum: usize,
352    changes: &[i32],
353) -> bool {
354    let contains = |sector: usize| {
355        sectors.map_or_else(
356            || fixed == Some(sector),
357            |selected| selected.binary_search(&sector).is_ok(),
358        )
359    };
360    let preserves_source = |source: usize| {
361        changes.iter().copied().all(|change| {
362            let target = source as i128 + i128::from(change);
363            target < 0 || target > maximum as i128 || contains(target as usize)
364        })
365    };
366    match (fixed, sectors) {
367        (_, Some(sectors)) => sectors.iter().copied().all(preserves_source),
368        (Some(fixed), None) => preserves_source(fixed),
369        (None, None) => true,
370    }
371}
372
373fn fixed_weight_states(sites: usize, particles: Option<usize>) -> Result<Vec<u128>> {
374    if sites > 128 {
375        return Err(QmbedError::UnsupportedBackend(
376            "the initial u128 state backend supports at most 128 orbitals".into(),
377        ));
378    }
379    if particles.is_some_and(|count| count > sites) {
380        return Err(QmbedError::InvalidSector(
381            "particle count exceeds site count".into(),
382        ));
383    }
384    let Some(particles) = particles else {
385        let limit = 1_u128
386            .checked_shl(u32::try_from(sites).unwrap_or(u32::MAX))
387            .ok_or_else(|| {
388                QmbedError::UnsupportedBackend(
389                    "enumerating the unconstrained 128-site Hilbert space is infeasible".into(),
390                )
391            })?;
392        return Ok((0..limit).collect());
393    };
394    if particles == 0 {
395        return Ok(vec![0]);
396    }
397    if particles == sites {
398        let state = if sites == 128 {
399            u128::MAX
400        } else {
401            (1_u128 << sites) - 1
402        };
403        return Ok(vec![state]);
404    }
405
406    // Gosper's hack enumerates only C(sites, particles) states instead of
407    // scanning the complete 2^sites parent space.
408    let mut state = (1_u128 << particles) - 1;
409    let limit = (sites < 128).then(|| 1_u128 << sites);
410    let mut states = Vec::new();
411    loop {
412        states.push(state);
413        let low_bit = state & state.wrapping_neg();
414        let Some(ripple) = state.checked_add(low_bit) else {
415            break;
416        };
417        let next = (((ripple ^ state) >> 2) / low_bit) | ripple;
418        if limit.is_some_and(|upper| next >= upper) {
419            break;
420        }
421        state = next;
422    }
423    Ok(states)
424}
425
426fn fixed_weight_sector_states(
427    sites: usize,
428    particles: Option<usize>,
429    particle_sectors: Option<&[usize]>,
430) -> Result<Vec<u128>> {
431    let Some(sectors) = particle_sectors else {
432        return fixed_weight_states(sites, particles);
433    };
434    let mut states = Vec::new();
435    for &sector in sectors {
436        states.extend(fixed_weight_states(sites, Some(sector))?);
437    }
438    states.sort_unstable();
439    Ok(states)
440}
441
442fn fixed_digit_sum_states(
443    sites: usize,
444    states_per_site: usize,
445    total: Option<usize>,
446) -> Result<Vec<u128>> {
447    if sites == 0 || states_per_site == 0 {
448        return Err(QmbedError::InvalidSector(
449            "sites and local state count must be positive".into(),
450        ));
451    }
452    if total.is_some_and(|value| value > sites.saturating_mul(states_per_site - 1)) {
453        return Err(QmbedError::InvalidSector(
454            "requested occupation exceeds the local spin capacity".into(),
455        ));
456    }
457    let base = states_per_site as u128;
458    let exponent = u32::try_from(sites)
459        .map_err(|_| QmbedError::UnsupportedBackend("site count is too large".into()))?;
460    let limit = base.checked_pow(exponent).ok_or_else(|| {
461        QmbedError::UnsupportedBackend("mixed-radix state encoding overflow".into())
462    })?;
463    if total.is_none() {
464        return Ok((0..limit).collect());
465    }
466
467    fn enumerate(
468        site: usize,
469        sites: usize,
470        states_per_site: usize,
471        remaining: usize,
472        place: u128,
473        encoded: u128,
474        output: &mut Vec<u128>,
475    ) {
476        if site == sites {
477            if remaining == 0 {
478                output.push(encoded);
479            }
480            return;
481        }
482        let remaining_sites = sites - site - 1;
483        let maximum_tail = remaining_sites.saturating_mul(states_per_site - 1);
484        for digit in 0..states_per_site {
485            if digit > remaining || remaining - digit > maximum_tail {
486                continue;
487            }
488            enumerate(
489                site + 1,
490                sites,
491                states_per_site,
492                remaining - digit,
493                place * states_per_site as u128,
494                encoded + digit as u128 * place,
495                output,
496            );
497        }
498    }
499
500    let mut states = Vec::new();
501    enumerate(
502        0,
503        sites,
504        states_per_site,
505        total.unwrap_or_default(),
506        1,
507        0,
508        &mut states,
509    );
510    states.sort_unstable();
511    Ok(states)
512}
513
514fn fixed_digit_sum_sector_states(
515    sites: usize,
516    states_per_site: usize,
517    total: Option<usize>,
518    particle_sectors: Option<&[usize]>,
519) -> Result<Vec<u128>> {
520    let Some(sectors) = particle_sectors else {
521        return fixed_digit_sum_states(sites, states_per_site, total);
522    };
523    let mut states = Vec::new();
524    for &sector in sectors {
525        states.extend(fixed_digit_sum_states(
526            sites,
527            states_per_site,
528            Some(sector),
529        )?);
530    }
531    states.sort_unstable();
532    Ok(states)
533}
534
535/// Site-local constraint for packed binary species.
536///
537/// A local state is encoded as a bit mask over species: bit `s` is the
538/// occupation of species `s` at the site. This represents exclusions such as
539/// no-double-occupancy without embedding a model-specific predicate in the
540/// fermion basis or an interop layer.
541#[derive(Clone, Debug, Eq, PartialEq)]
542pub struct LocalOccupationConstraint {
543    species: usize,
544    allowed_local_states: Vec<usize>,
545}
546
547impl LocalOccupationConstraint {
548    pub fn new(
549        species: usize,
550        allowed_local_states: impl IntoIterator<Item = usize>,
551    ) -> Result<Self> {
552        let local_dimension = 1_usize
553            .checked_shl(u32::try_from(species).unwrap_or(u32::MAX))
554            .ok_or_else(|| {
555                QmbedError::UnsupportedBackend(
556                    "the local binary-species dimension exceeds usize".into(),
557                )
558            })?;
559        if species == 0 {
560            return Err(QmbedError::InvalidSector(
561                "a local occupation constraint needs at least one species".into(),
562            ));
563        }
564        let mut allowed_local_states = allowed_local_states.into_iter().collect::<Vec<_>>();
565        if allowed_local_states.is_empty() {
566            return Err(QmbedError::InvalidSector(
567                "a local occupation constraint must allow at least one state".into(),
568            ));
569        }
570        if allowed_local_states
571            .iter()
572            .any(|&state| state >= local_dimension)
573        {
574            return Err(QmbedError::InvalidSector(
575                "an allowed local occupation is outside the binary-species space".into(),
576            ));
577        }
578        allowed_local_states.sort_unstable();
579        allowed_local_states.dedup();
580        Ok(Self {
581            species,
582            allowed_local_states,
583        })
584    }
585
586    pub const fn species(&self) -> usize {
587        self.species
588    }
589
590    pub fn allowed_local_states(&self) -> &[usize] {
591        &self.allowed_local_states
592    }
593
594    pub fn accepts_packed_state(&self, state: u128, sites: usize) -> Result<bool> {
595        let orbitals = self.species.checked_mul(sites).ok_or_else(|| {
596            QmbedError::UnsupportedBackend("binary-species orbital count overflow".into())
597        })?;
598        if orbitals > 128 {
599            return Err(QmbedError::UnsupportedBackend(
600                "the packed local-constraint backend supports at most 128 orbitals".into(),
601            ));
602        }
603        for site in 0..sites {
604            let mut local_state = 0_usize;
605            for species in 0..self.species {
606                let orbital = species * sites + site;
607                if state & (1_u128 << orbital) != 0 {
608                    local_state |= 1_usize << species;
609                }
610            }
611            if self
612                .allowed_local_states
613                .binary_search(&local_state)
614                .is_err()
615            {
616                return Ok(false);
617            }
618        }
619        Ok(true)
620    }
621}
622
623fn state_index(states: &[u128], state: u128) -> Result<usize> {
624    states
625        .binary_search(&state)
626        .map_err(|_| QmbedError::StateNotInBasis)
627}
628
629fn direct_state_index(states: &[u128], state: u128) -> Result<usize> {
630    let index = usize::try_from(state).map_err(|_| QmbedError::StateNotInBasis)?;
631    if index < states.len() {
632        Ok(index)
633    } else {
634        Err(QmbedError::StateNotInBasis)
635    }
636}
637
638/// Rank a fixed-weight bit string in the colexicographic order generated by
639/// Gosper's hack in [`fixed_weight_states`].
640fn fixed_weight_state_index(state: u128, sites: usize, particles: usize) -> Result<usize> {
641    if particles > sites
642        || state.count_ones() as usize != particles
643        || (sites < 128 && state >= (1_u128 << sites))
644    {
645        return Err(QmbedError::StateNotInBasis);
646    }
647    let mut rank = 0_usize;
648    let mut ordinal = 1_usize;
649    let mut remaining = state;
650    while remaining != 0 {
651        let position = remaining.trailing_zeros() as usize;
652        if position >= ordinal {
653            rank = rank.saturating_add(binomial(position, ordinal));
654        }
655        ordinal += 1;
656        remaining &= remaining - 1;
657    }
658    Ok(rank)
659}
660
661fn rotate_lattice_state(state: u128, shift: usize, sites: usize, base: u128) -> u128 {
662    if sites == 0 {
663        return state;
664    }
665    let shift = shift % sites;
666    if shift == 0 {
667        return state;
668    }
669    if base == 2 {
670        let mask = if sites == 128 {
671            u128::MAX
672        } else {
673            (1_u128 << sites) - 1
674        };
675        return ((state << shift) & mask) | (state >> (sites - shift));
676    }
677    let mut translated = 0_u128;
678    let mut source_place = 1_u128;
679    for site in 0..sites {
680        let digit = (state / source_place) % base;
681        let target_site = (site + shift) % sites;
682        translated += digit * base.pow(u32::try_from(target_site).unwrap_or(u32::MAX));
683        source_place *= base;
684    }
685    translated
686}
687
688fn reflect_lattice_state(state: u128, sites: usize, base: u128) -> u128 {
689    let mut reflected = 0_u128;
690    let mut source_place = 1_u128;
691    for site in 0..sites {
692        let digit = (state / source_place) % base;
693        let target_site = sites - site - 1;
694        reflected += digit * base.pow(u32::try_from(target_site).unwrap_or(u32::MAX));
695        source_place *= base;
696    }
697    reflected
698}
699
700#[derive(Clone, Copy, Debug)]
701struct SymmetryImage {
702    representative: u128,
703    phase: Complex64,
704    orbit_size: usize,
705}
706
707type SymmetrySectorData = (
708    Vec<u128>,
709    Vec<usize>,
710    HashMap<u128, SymmetryImage>,
711    Option<usize>,
712    Option<i8>,
713);
714
715fn spin_symmetry_sector(
716    parent_states: Vec<u128>,
717    sites: usize,
718    base: u128,
719    momentum: Option<i32>,
720    parity: Option<i8>,
721) -> Result<SymmetrySectorData> {
722    if sites == 0 {
723        return Err(QmbedError::InvalidSector(
724            "symmetry sectors require at least one site".into(),
725        ));
726    }
727    if parity.is_some_and(|value| value != -1 && value != 1) {
728        return Err(QmbedError::InvalidSector(
729            "parity must be either -1 or +1".into(),
730        ));
731    }
732    let sites_i64 = i64::try_from(sites)
733        .map_err(|_| QmbedError::UnsupportedBackend("site count is too large".into()))?;
734    let normalized_momentum = momentum.map(|value| i64::from(value).rem_euclid(sites_i64) as usize);
735    if parity.is_some() && normalized_momentum.is_some_and(|value| value != 0 && 2 * value != sites)
736    {
737        return Err(QmbedError::IncompatibleSymmetry(
738            "parity can share a one-dimensional sector with momentum only at k=0 or k=pi".into(),
739        ));
740    }
741
742    if momentum.is_none() && parity.is_none() {
743        let orbit_sizes = vec![1; parent_states.len()];
744        let lookup = parent_states
745            .iter()
746            .copied()
747            .map(|state| {
748                (
749                    state,
750                    SymmetryImage {
751                        representative: state,
752                        phase: Complex64::new(1.0, 0.0),
753                        orbit_size: 1,
754                    },
755                )
756            })
757            .collect();
758        return Ok((parent_states, orbit_sizes, lookup, None, None));
759    }
760
761    let parent_lookup: HashSet<_> = parent_states.iter().copied().collect();
762    let translations = if momentum.is_some() { sites } else { 1 };
763    let mut visited = HashSet::with_capacity(parent_states.len());
764    let mut sectors = Vec::<(u128, usize)>::new();
765    let mut lookup = HashMap::with_capacity(parent_states.len());
766
767    for seed in parent_states {
768        if visited.contains(&seed) {
769            continue;
770        }
771        let mut orbit = HashSet::new();
772        for shift in 0..translations {
773            let translated = rotate_lattice_state(seed, shift, sites, base);
774            orbit.insert(translated);
775            if parity.is_some() {
776                orbit.insert(reflect_lattice_state(translated, sites, base));
777            }
778        }
779        if orbit.iter().any(|state| !parent_lookup.contains(state)) {
780            return Err(QmbedError::IncompatibleSymmetry(
781                "symmetry map leaves the selected magnetization sector".into(),
782            ));
783        }
784        visited.extend(orbit.iter().copied());
785        let representative = *orbit
786            .iter()
787            .min()
788            .ok_or_else(|| QmbedError::InvalidSector("symmetry generated an empty orbit".into()))?;
789
790        let mut coefficients = HashMap::<u128, Complex64>::new();
791        for shift in 0..translations {
792            let angle = normalized_momentum.map_or(0.0, |value| {
793                -std::f64::consts::TAU * (value * shift) as f64 / sites as f64
794            });
795            let character = Complex64::from_polar(1.0, angle);
796            let translated = rotate_lattice_state(representative, shift, sites, base);
797            *coefficients
798                .entry(translated)
799                .or_insert(Complex64::new(0.0, 0.0)) += character;
800            if let Some(parity_value) = parity {
801                let reflected = reflect_lattice_state(translated, sites, base);
802                *coefficients
803                    .entry(reflected)
804                    .or_insert(Complex64::new(0.0, 0.0)) += f64::from(parity_value) * character;
805            }
806        }
807        coefficients.retain(|_, coefficient| coefficient.norm() > 1.0e-12);
808        if coefficients.is_empty() {
809            continue;
810        }
811        let representative_coefficient =
812            coefficients
813                .get(&representative)
814                .copied()
815                .ok_or(QmbedError::IncompatibleSymmetry(
816                    "symmetry projection removed its orbit representative".into(),
817                ))?;
818        let gauge = representative_coefficient / representative_coefficient.norm();
819        let norm = coefficients
820            .values()
821            .map(Complex64::norm_sqr)
822            .sum::<f64>()
823            .sqrt();
824        let orbit_size = coefficients.len();
825        let expected_magnitude = 1.0 / (orbit_size as f64).sqrt();
826        for (&state, coefficient) in &coefficients {
827            let normalized = *coefficient / (gauge * norm);
828            if (normalized.norm() - expected_magnitude).abs() > 1.0e-10 {
829                return Err(QmbedError::IncompatibleSymmetry(
830                    "symmetry projection does not define a one-dimensional orbit sector".into(),
831                ));
832            }
833            lookup.insert(
834                state,
835                SymmetryImage {
836                    representative,
837                    phase: normalized / expected_magnitude,
838                    orbit_size,
839                },
840            );
841        }
842        sectors.push((representative, orbit_size));
843    }
844
845    sectors.sort_by_key(|(representative, _)| *representative);
846    let (states, orbit_sizes) = sectors.into_iter().unzip();
847    Ok((states, orbit_sizes, lookup, normalized_momentum, parity))
848}
849
850fn translate_fermion_state(state: u128, shift: usize, sites: usize) -> (u128, f64) {
851    let normalized = shift % sites;
852    if normalized == 0 {
853        return (state, 1.0);
854    }
855    let translated = rotate_lattice_state(state, normalized, sites, 2);
856    let wrapped_mask = ((1_u128 << normalized) - 1) << (sites - normalized);
857    let wrapped = (state & wrapped_mask).count_ones() as usize;
858    let retained = state.count_ones() as usize - wrapped;
859    let sign = if wrapped * retained % 2 == 0 {
860        1.0
861    } else {
862        -1.0
863    };
864    (translated, sign)
865}
866
867type FermionTranslationSector = (
868    Vec<u128>,
869    Vec<usize>,
870    HashMap<u128, SymmetryImage>,
871    Option<usize>,
872);
873
874fn fermion_translation_sector(
875    parent_states: Vec<u128>,
876    sites: usize,
877    momentum: Option<i32>,
878) -> Result<FermionTranslationSector> {
879    if momentum.is_none() {
880        let orbit_sizes = vec![1; parent_states.len()];
881        let lookup = parent_states
882            .iter()
883            .copied()
884            .map(|state| {
885                (
886                    state,
887                    SymmetryImage {
888                        representative: state,
889                        phase: Complex64::new(1.0, 0.0),
890                        orbit_size: 1,
891                    },
892                )
893            })
894            .collect();
895        return Ok((parent_states, orbit_sizes, lookup, None));
896    }
897    if sites == 0 {
898        return Err(QmbedError::InvalidSector(
899            "translation sectors require at least one site".into(),
900        ));
901    }
902    let sites_i64 = i64::try_from(sites)
903        .map_err(|_| QmbedError::UnsupportedBackend("site count is too large".into()))?;
904    let normalized = i64::from(momentum.unwrap_or_default()).rem_euclid(sites_i64) as usize;
905    let mut visited = HashSet::with_capacity(parent_states.len());
906    let mut sectors = Vec::<(u128, usize)>::new();
907    let mut lookup = HashMap::with_capacity(parent_states.len());
908
909    for seed in parent_states {
910        if visited.contains(&seed) {
911            continue;
912        }
913        let orbit: HashSet<_> = (0..sites)
914            .map(|shift| translate_fermion_state(seed, shift, sites).0)
915            .collect();
916        visited.extend(orbit.iter().copied());
917        let representative = *orbit.iter().min().ok_or_else(|| {
918            QmbedError::InvalidSector("translation generated an empty fermion orbit".into())
919        })?;
920        let mut coefficients = HashMap::<u128, Complex64>::new();
921        for shift in 0..sites {
922            let (translated, sign) = translate_fermion_state(representative, shift, sites);
923            let angle = -std::f64::consts::TAU * (normalized * shift) as f64 / sites as f64;
924            *coefficients
925                .entry(translated)
926                .or_insert(Complex64::new(0.0, 0.0)) += sign * Complex64::from_polar(1.0, angle);
927        }
928        coefficients.retain(|_, coefficient| coefficient.norm() > 1.0e-12);
929        if coefficients.is_empty() {
930            continue;
931        }
932        let representative_coefficient =
933            coefficients
934                .get(&representative)
935                .copied()
936                .ok_or(QmbedError::IncompatibleSymmetry(
937                    "translation projection removed its fermion representative".into(),
938                ))?;
939        let gauge = representative_coefficient / representative_coefficient.norm();
940        let norm = coefficients
941            .values()
942            .map(Complex64::norm_sqr)
943            .sum::<f64>()
944            .sqrt();
945        let orbit_size = coefficients.len();
946        let expected_magnitude = 1.0 / (orbit_size as f64).sqrt();
947        for (&state, coefficient) in &coefficients {
948            let projected = *coefficient / (gauge * norm);
949            if (projected.norm() - expected_magnitude).abs() > 1.0e-10 {
950                return Err(QmbedError::IncompatibleSymmetry(
951                    "fermion translation does not define a one-dimensional orbit sector".into(),
952                ));
953            }
954            lookup.insert(
955                state,
956                SymmetryImage {
957                    representative,
958                    phase: projected / expected_magnitude,
959                    orbit_size,
960                },
961            );
962        }
963        sectors.push((representative, orbit_size));
964    }
965    sectors.sort_by_key(|(representative, _)| *representative);
966    if sectors.is_empty() {
967        return Err(QmbedError::InvalidSector(
968            "the requested fermion momentum sector is empty".into(),
969        ));
970    }
971    let (states, orbit_sizes) = sectors.into_iter().unzip();
972    Ok((states, orbit_sizes, lookup, Some(normalized)))
973}
974
975fn checked_site(site: usize, sites: usize) -> Result<()> {
976    if site >= sites {
977        Err(QmbedError::InvalidSite { site, sites })
978    } else {
979        Ok(())
980    }
981}
982
983fn operator_chars(operator: &str, sites: &[usize]) -> Result<SmallVec<[char; 8]>> {
984    let chars: SmallVec<[char; 8]> = operator
985        .chars()
986        .filter(|character| *character != '|')
987        .collect();
988    if chars.len() != sites.len() {
989        return Err(QmbedError::InvalidCoupling(format!(
990            "operator arity {} does not match {} sites",
991            chars.len(),
992            sites.len()
993        )));
994    }
995    Ok(chars)
996}
997
998/// Normalization of local spin operators.
999///
1000/// The distinction matters for spin one-half because two common interfaces
1001/// assign different meanings to the ladder symbols. `PauliCartesian` keeps
1002/// the conventional unit-amplitude sigma-plus/minus operators, while `Pauli`
1003/// scales every non-identity spin symbol by two relative to angular-momentum
1004/// operators.
1005#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1006pub enum SpinNormalization {
1007    /// Conventional angular-momentum operators `Sx`, `Sy`, `Sz`, and `S±`.
1008    #[default]
1009    AngularMomentum,
1010    /// Scale every non-identity spin-half action by two.
1011    Pauli,
1012    /// Scale Cartesian spin-half actions as Pauli matrices but retain unit ladder actions.
1013    PauliCartesian,
1014}
1015
1016/// Spin-chain basis for the full or fixed-magnetization spin space.
1017#[derive(Clone, Debug)]
1018pub struct SpinBasis1D {
1019    sites: usize,
1020    spin_twice: u16,
1021    states_per_site: u128,
1022    radix_bits: Option<u32>,
1023    up: Option<usize>,
1024    particle_sectors: Option<Vec<usize>>,
1025    normalization: SpinNormalization,
1026    place_values: Vec<u128>,
1027    z_factors: Vec<f64>,
1028    raise_factors: Vec<f64>,
1029    lower_factors: Vec<f64>,
1030    momentum: Option<usize>,
1031    parity: Option<i8>,
1032    orbit_lengths: Vec<usize>,
1033    symmetry_lookup: HashMap<u128, SymmetryImage>,
1034    states: Vec<u128>,
1035}
1036
1037impl SpinBasis1D {
1038    /// Start a spin-basis builder for `sites` lattice positions.
1039    ///
1040    /// The default is an unrestricted spin-half basis with angular-momentum
1041    /// normalization and no spatial symmetry reduction.
1042    pub fn builder(sites: usize) -> SpinBasisBuilder {
1043        SpinBasisBuilder {
1044            sites,
1045            spin_twice: 1,
1046            up: None,
1047            particle_sectors: None,
1048            momentum: None,
1049            parity: None,
1050            normalization: SpinNormalization::AngularMomentum,
1051        }
1052    }
1053
1054    /// Return the number of lattice sites.
1055    pub const fn sites(&self) -> usize {
1056        self.sites
1057    }
1058
1059    /// Return twice the local spin quantum number.
1060    pub const fn spin_twice(&self) -> u16 {
1061        self.spin_twice
1062    }
1063
1064    /// Return the selected additive spin-occupation sector, if unique.
1065    pub const fn up(&self) -> Option<usize> {
1066        self.up
1067    }
1068
1069    /// Allowed additive spin-occupation sectors when the basis is a union.
1070    pub fn particle_sectors(&self) -> Option<&[usize]> {
1071        self.particle_sectors.as_deref()
1072    }
1073
1074    /// Report whether a Pauli-scaled spin-half convention is active.
1075    pub const fn pauli(&self) -> bool {
1076        !matches!(self.normalization, SpinNormalization::AngularMomentum)
1077    }
1078
1079    /// Return the exact local spin-operator normalization.
1080    pub const fn normalization(&self) -> SpinNormalization {
1081        self.normalization
1082    }
1083
1084    /// Return the canonical translation-momentum sector, if selected.
1085    pub const fn momentum(&self) -> Option<usize> {
1086        self.momentum
1087    }
1088
1089    /// Return the reflection eigenvalue, if selected.
1090    pub const fn parity(&self) -> Option<i8> {
1091        self.parity
1092    }
1093
1094    fn unreduced_local_transitions(
1095        &self,
1096        state: u128,
1097        operator: &str,
1098        sites: &[usize],
1099    ) -> Result<LocalTransitions<u128>> {
1100        let mut transitions = LocalTransitions::new();
1101        self.visit_unreduced_local_transitions(state, operator, sites, |target, amplitude| {
1102            transitions.push((target, amplitude));
1103            Ok(())
1104        })?;
1105        Ok(transitions)
1106    }
1107
1108    fn visit_unreduced_local_transitions<F>(
1109        &self,
1110        state: u128,
1111        operator: &str,
1112        sites: &[usize],
1113        visit: F,
1114    ) -> Result<()>
1115    where
1116        F: FnMut(u128, Complex64) -> Result<()>,
1117    {
1118        let symbols = operator_chars(operator, sites)?;
1119        self.visit_unreduced_local_transitions_with_symbols(state, &symbols, sites, visit)
1120    }
1121
1122    fn visit_unreduced_local_transitions_with_symbols<F>(
1123        &self,
1124        state: u128,
1125        symbols: &[char],
1126        sites: &[usize],
1127        mut visit: F,
1128    ) -> Result<()>
1129    where
1130        F: FnMut(u128, Complex64) -> Result<()>,
1131    {
1132        if symbols.len() != sites.len() {
1133            return Err(QmbedError::InvalidCoupling(format!(
1134                "operator arity {} does not match {} sites",
1135                symbols.len(),
1136                sites.len()
1137            )));
1138        }
1139        let mut pending = SmallVec::<[(u128, Complex64, usize); 2]>::new();
1140        pending.push((state, Complex64::new(1.0, 0.0), symbols.len()));
1141        while let Some((mut encoded, mut amplitude, mut remaining)) = pending.pop() {
1142            loop {
1143                if remaining == 0 {
1144                    visit(encoded, amplitude)?;
1145                    break;
1146                }
1147                let position = remaining - 1;
1148                let site = sites[position];
1149                let op = symbols[position];
1150                checked_site(site, self.sites)?;
1151                let place = self.place_values[site];
1152                let encoded_digit = self.radix_bits.map_or_else(
1153                    || (encoded / place) % self.states_per_site,
1154                    |bits| {
1155                        (encoded >> (bits * u32::try_from(site).unwrap_or(u32::MAX)))
1156                            & (self.states_per_site - 1)
1157                    },
1158                );
1159                let digit =
1160                    usize::try_from(encoded_digit).map_err(|_| QmbedError::StateNotInBasis)?;
1161                let raise_factor = self.raise_factors[digit];
1162                let lower_factor = self.lower_factors[digit];
1163                match op {
1164                    'I' => {}
1165                    'z' => {
1166                        let factor = self.z_factors[digit];
1167                        if factor == 0.0 {
1168                            break;
1169                        }
1170                        amplitude *= factor;
1171                    }
1172                    '+' => {
1173                        if raise_factor == 0.0 {
1174                            break;
1175                        }
1176                        encoded += place;
1177                        if raise_factor != 1.0 {
1178                            amplitude *= raise_factor;
1179                        }
1180                    }
1181                    '-' => {
1182                        if lower_factor == 0.0 {
1183                            break;
1184                        }
1185                        encoded -= place;
1186                        if lower_factor != 1.0 {
1187                            amplitude *= lower_factor;
1188                        }
1189                    }
1190                    'x' | 'y' => {
1191                        let scale = match self.normalization {
1192                            SpinNormalization::AngularMomentum | SpinNormalization::Pauli => 0.5,
1193                            SpinNormalization::PauliCartesian => 1.0,
1194                        };
1195                        let raise_phase = if op == 'x' {
1196                            Complex64::new(scale, 0.0)
1197                        } else {
1198                            Complex64::new(0.0, -scale)
1199                        };
1200                        let lower_phase = if op == 'x' {
1201                            Complex64::new(scale, 0.0)
1202                        } else {
1203                            Complex64::new(0.0, scale)
1204                        };
1205                        match (raise_factor != 0.0, lower_factor != 0.0) {
1206                            (true, true) => {
1207                                pending.push((
1208                                    encoded - place,
1209                                    amplitude * lower_phase * lower_factor,
1210                                    position,
1211                                ));
1212                                encoded += place;
1213                                amplitude *= raise_phase * raise_factor;
1214                            }
1215                            (true, false) => {
1216                                encoded += place;
1217                                amplitude *= raise_phase * raise_factor;
1218                            }
1219                            (false, true) => {
1220                                encoded -= place;
1221                                amplitude *= lower_phase * lower_factor;
1222                            }
1223                            (false, false) => break,
1224                        }
1225                    }
1226                    _ => return Err(QmbedError::InvalidOperator(op.to_string())),
1227                }
1228                remaining = position;
1229            }
1230        }
1231        Ok(())
1232    }
1233}
1234
1235/// Builder for full, conserved, or symmetry-reduced spin-chain bases.
1236#[derive(Clone, Debug)]
1237pub struct SpinBasisBuilder {
1238    sites: usize,
1239    spin_twice: u16,
1240    up: Option<usize>,
1241    particle_sectors: Option<Vec<usize>>,
1242    momentum: Option<i32>,
1243    parity: Option<i8>,
1244    normalization: SpinNormalization,
1245}
1246
1247impl SpinBasisBuilder {
1248    /// Select the local spin as twice its quantum number.
1249    ///
1250    /// For example, `1` selects spin one half and `2` selects spin one.
1251    pub const fn spin_twice(mut self, spin_twice: u16) -> Self {
1252        self.spin_twice = spin_twice;
1253        self
1254    }
1255
1256    /// Restrict the basis to one additive spin-occupation sector.
1257    pub fn up(mut self, up: usize) -> Self {
1258        self.up = Some(up);
1259        self.particle_sectors = None;
1260        self
1261    }
1262
1263    /// Alias for [`SpinBasisBuilder::up`].
1264    pub fn magnetization(mut self, up: usize) -> Self {
1265        self.up = Some(up);
1266        self.particle_sectors = None;
1267        self
1268    }
1269
1270    /// Select a union of additive spin-occupation sectors.
1271    pub fn particle_sectors(mut self, sectors: impl IntoIterator<Item = usize>) -> Self {
1272        self.particle_sectors = Some(sectors.into_iter().collect());
1273        self.up = None;
1274        self
1275    }
1276
1277    /// Select a translation-momentum sector.
1278    ///
1279    /// Negative values are canonicalized modulo the lattice length when the
1280    /// basis is built.
1281    pub const fn momentum(mut self, momentum: i32) -> Self {
1282        self.momentum = Some(momentum);
1283        self
1284    }
1285
1286    /// Select a reflection sector with eigenvalue `-1` or `1`.
1287    pub const fn parity(mut self, parity: i8) -> Self {
1288        self.parity = Some(parity);
1289        self
1290    }
1291
1292    /// Select Cartesian Pauli normalization for a spin-half basis.
1293    pub const fn pauli(mut self, pauli: bool) -> Self {
1294        self.normalization = if pauli {
1295            SpinNormalization::PauliCartesian
1296        } else {
1297            SpinNormalization::AngularMomentum
1298        };
1299        self
1300    }
1301
1302    /// Select an explicit local spin normalization.
1303    pub const fn normalization(mut self, normalization: SpinNormalization) -> Self {
1304        self.normalization = normalization;
1305        self
1306    }
1307
1308    /// Validate the requested sectors and enumerate the resulting basis.
1309    ///
1310    /// Fixed additive sectors are generated combinatorially. Translation and
1311    /// parity filters are then applied to physical orbits.
1312    pub fn build(self) -> Result<SpinBasis1D> {
1313        if self.spin_twice == 0 {
1314            return Err(QmbedError::InvalidSector(
1315                "spin_twice must be positive".into(),
1316            ));
1317        }
1318        if self.normalization != SpinNormalization::AngularMomentum && self.spin_twice != 1 {
1319            return Err(QmbedError::InvalidOptions(
1320                "the Pauli convention is defined only for spin one-half".into(),
1321            ));
1322        }
1323        let states_per_site = usize::from(self.spin_twice) + 1;
1324        let maximum_particles = self.sites.saturating_mul(usize::from(self.spin_twice));
1325        let particle_sectors = self
1326            .particle_sectors
1327            .map(|sectors| canonical_particle_sectors(sectors, maximum_particles, "spin"))
1328            .transpose()?;
1329        let states_per_site_u128 = states_per_site as u128;
1330        let radix_bits = states_per_site_u128
1331            .is_power_of_two()
1332            .then_some(states_per_site_u128.trailing_zeros());
1333        let place_values = (0..self.sites)
1334            .map(|site| {
1335                states_per_site_u128
1336                    .checked_pow(u32::try_from(site).unwrap_or(u32::MAX))
1337                    .ok_or_else(|| {
1338                        QmbedError::UnsupportedBackend(
1339                            "spin-state place value exceeds the u128 backend".into(),
1340                        )
1341                    })
1342            })
1343            .collect::<Result<Vec<_>>>()?;
1344        let spin = f64::from(self.spin_twice) * 0.5;
1345        let mut z_factors = Vec::with_capacity(states_per_site);
1346        let mut raise_factors = Vec::with_capacity(states_per_site);
1347        let mut lower_factors = Vec::with_capacity(states_per_site);
1348        for digit in 0..states_per_site {
1349            let magnetic = digit as f64 - spin;
1350            z_factors.push(
1351                if self.normalization == SpinNormalization::AngularMomentum {
1352                    magnetic
1353                } else {
1354                    2.0 * magnetic
1355                },
1356            );
1357            let ladder_scale = if self.normalization == SpinNormalization::Pauli {
1358                2.0
1359            } else {
1360                1.0
1361            };
1362            raise_factors.push(
1363                if digit + 1 < states_per_site {
1364                    (spin * (spin + 1.0) - magnetic * (magnetic + 1.0)).sqrt()
1365                } else {
1366                    0.0
1367                } * ladder_scale,
1368            );
1369            lower_factors.push(
1370                if digit > 0 {
1371                    (spin * (spin + 1.0) - magnetic * (magnetic - 1.0)).sqrt()
1372                } else {
1373                    0.0
1374                } * ladder_scale,
1375            );
1376        }
1377        let parent_states = if self.spin_twice == 1 {
1378            fixed_weight_sector_states(self.sites, self.up, particle_sectors.as_deref())?
1379        } else {
1380            fixed_digit_sum_sector_states(
1381                self.sites,
1382                states_per_site,
1383                self.up,
1384                particle_sectors.as_deref(),
1385            )?
1386        };
1387        let (states, orbit_lengths, symmetry_lookup, momentum, parity) = spin_symmetry_sector(
1388            parent_states,
1389            self.sites,
1390            states_per_site_u128,
1391            self.momentum,
1392            self.parity,
1393        )?;
1394        Ok(SpinBasis1D {
1395            sites: self.sites,
1396            spin_twice: self.spin_twice,
1397            states_per_site: states_per_site_u128,
1398            radix_bits,
1399            up: self.up,
1400            particle_sectors,
1401            normalization: self.normalization,
1402            place_values,
1403            z_factors,
1404            raise_factors,
1405            lower_factors,
1406            momentum,
1407            parity,
1408            orbit_lengths,
1409            symmetry_lookup,
1410            states,
1411        })
1412    }
1413}
1414
1415impl Basis for SpinBasis1D {
1416    type State = u128;
1417
1418    fn len(&self) -> usize {
1419        self.states.len()
1420    }
1421
1422    fn state(&self, index: usize) -> Result<Self::State> {
1423        self.states
1424            .get(index)
1425            .copied()
1426            .ok_or(QmbedError::StateNotInBasis)
1427    }
1428
1429    fn index(&self, state: Self::State) -> Result<usize> {
1430        if self.momentum.is_none() && self.parity.is_none() {
1431            if self.spin_twice == 1 && self.particle_sectors.is_none() {
1432                return self.up.map_or_else(
1433                    || direct_state_index(&self.states, state),
1434                    |up| fixed_weight_state_index(state, self.sites, up),
1435                );
1436            }
1437            if self.up.is_none() && self.particle_sectors.is_none() {
1438                return direct_state_index(&self.states, state);
1439            }
1440        }
1441        state_index(&self.states, state)
1442    }
1443
1444    fn apply_local(
1445        &self,
1446        state: Self::State,
1447        operator: &str,
1448        sites: &[usize],
1449    ) -> Result<Option<(Self::State, Complex64)>> {
1450        let transitions = self.apply_local_transitions(state, operator, sites)?;
1451        match transitions.as_slice() {
1452            [] => Ok(None),
1453            [transition] => Ok(Some(*transition)),
1454            _ => Err(QmbedError::UnsupportedBackend(
1455                "this higher-spin local action branches; use apply_local_transitions".into(),
1456            )),
1457        }
1458    }
1459
1460    fn apply_local_transitions(
1461        &self,
1462        state: Self::State,
1463        operator: &str,
1464        sites: &[usize],
1465    ) -> Result<LocalTransitions<Self::State>> {
1466        let source_state = state;
1467        let branches = self.unreduced_local_transitions(state, operator, sites)?;
1468        if self.momentum.is_some() || self.parity.is_some() {
1469            let source_index = self.index(source_state)?;
1470            let source_orbit = self.orbit_lengths[source_index];
1471            let mut reduced = HashMap::<u128, Complex64>::new();
1472            for (encoded, mut amplitude) in branches {
1473                let Some(image) = self.symmetry_lookup.get(&encoded) else {
1474                    continue;
1475                };
1476                amplitude *=
1477                    (source_orbit as f64 / image.orbit_size as f64).sqrt() * image.phase.conj();
1478                *reduced
1479                    .entry(image.representative)
1480                    .or_insert(Complex64::new(0.0, 0.0)) += amplitude;
1481            }
1482            let mut transitions: LocalTransitions<_> = reduced
1483                .into_iter()
1484                .filter(|(_, amplitude)| amplitude.norm() > f64::EPSILON)
1485                .collect();
1486            transitions.sort_by_key(|(encoded, _)| *encoded);
1487            return Ok(transitions);
1488        }
1489        Ok(branches)
1490    }
1491
1492    fn apply_local_unreduced_transitions(
1493        &self,
1494        state: Self::State,
1495        operator: &str,
1496        sites: &[usize],
1497    ) -> Result<LocalTransitions<Self::State>> {
1498        self.unreduced_local_transitions(state, operator, sites)
1499    }
1500
1501    fn visit_local_unreduced_transitions<F>(
1502        &self,
1503        state: Self::State,
1504        operator: &str,
1505        sites: &[usize],
1506        visit: F,
1507    ) -> Result<()>
1508    where
1509        F: FnMut(Self::State, Complex64) -> Result<()>,
1510    {
1511        self.visit_unreduced_local_transitions(state, operator, sites, visit)
1512    }
1513
1514    fn visit_preparsed_local_unreduced_transitions<F>(
1515        &self,
1516        state: Self::State,
1517        _operator: &str,
1518        symbols: &[char],
1519        _split: Option<usize>,
1520        sites: &[usize],
1521        visit: F,
1522    ) -> Result<()>
1523    where
1524        F: FnMut(Self::State, Complex64) -> Result<()>,
1525    {
1526        self.visit_unreduced_local_transitions_with_symbols(state, symbols, sites, visit)
1527    }
1528
1529    fn transition_orbit_size(&self, state: Self::State) -> Result<usize> {
1530        if self.momentum.is_none() && self.parity.is_none() {
1531            return Ok(1);
1532        }
1533        Ok(self.orbit_lengths[self.index(state)?])
1534    }
1535
1536    fn reduction_image(&self, state: Self::State) -> Result<Option<ReductionImage<Self::State>>> {
1537        let Some(image) = self.symmetry_lookup.get(&state) else {
1538            return Ok(None);
1539        };
1540        Ok(Some(ReductionImage::new(
1541            image.representative,
1542            image.phase,
1543            image.orbit_size,
1544        )?))
1545    }
1546
1547    fn reduce_transition(
1548        &self,
1549        state: Self::State,
1550        source_orbit_size: usize,
1551    ) -> Result<Option<(Self::State, Complex64)>> {
1552        let Some(image) = self.symmetry_lookup.get(&state) else {
1553            return Ok(None);
1554        };
1555        Ok(Some((
1556            image.representative,
1557            (source_orbit_size as f64 / image.orbit_size as f64).sqrt() * image.phase.conj(),
1558        )))
1559    }
1560
1561    fn index_transition(
1562        &self,
1563        state: Self::State,
1564        source_orbit_size: usize,
1565    ) -> Result<Option<(usize, Complex64)>> {
1566        if self.momentum.is_none() && self.parity.is_none() {
1567            return match self.index(state) {
1568                Ok(index) => Ok(Some((index, Complex64::new(1.0, 0.0)))),
1569                Err(QmbedError::StateNotInBasis) => Ok(None),
1570                Err(error) => Err(error),
1571            };
1572        }
1573        let Some(image) = self.symmetry_lookup.get(&state) else {
1574            return Ok(None);
1575        };
1576        Ok(Some((
1577            self.index(image.representative)?,
1578            (source_orbit_size as f64 / image.orbit_size as f64).sqrt() * image.phase.conj(),
1579        )))
1580    }
1581
1582    fn operator_preserves_particle_sector(&self, operator: &str) -> Result<bool> {
1583        Ok(selected_sectors_preserve_changes(
1584            self.up,
1585            self.particle_sectors.as_deref(),
1586            self.sites.saturating_mul(usize::from(self.spin_twice)),
1587            &operator_number_changes(operator)?,
1588        ))
1589    }
1590}
1591
1592/// Truncated on-site boson basis.
1593#[derive(Clone, Debug)]
1594pub struct BosonBasis1D {
1595    sites: usize,
1596    particles: Option<usize>,
1597    particle_sectors: Option<Vec<usize>>,
1598    states_per_site: usize,
1599    states: Vec<u128>,
1600}
1601
1602impl BosonBasis1D {
1603    /// Start a boson-basis builder with a finite local occupation cutoff.
1604    ///
1605    /// `states_per_site` permits occupations from zero through one less than
1606    /// the supplied value.
1607    pub fn builder(sites: usize, states_per_site: usize) -> BosonBasisBuilder {
1608        BosonBasisBuilder {
1609            sites,
1610            particles: None,
1611            particle_sectors: None,
1612            states_per_site,
1613        }
1614    }
1615
1616    /// Return the number of lattice sites.
1617    pub const fn sites(&self) -> usize {
1618        self.sites
1619    }
1620
1621    /// Return the fixed total particle number, if selected.
1622    pub const fn particles(&self) -> Option<usize> {
1623        self.particles
1624    }
1625
1626    /// Allowed total-occupation sectors when the basis is a union.
1627    pub fn particle_sectors(&self) -> Option<&[usize]> {
1628        self.particle_sectors.as_deref()
1629    }
1630
1631    /// Return the number of allowed local occupation states.
1632    pub const fn states_per_site(&self) -> usize {
1633        self.states_per_site
1634    }
1635
1636    fn apply_local_symbols(
1637        &self,
1638        mut state: u128,
1639        symbols: &[char],
1640        sites: &[usize],
1641    ) -> Result<Option<(u128, Complex64)>> {
1642        if symbols.len() != sites.len() {
1643            return Err(QmbedError::InvalidCoupling(format!(
1644                "operator arity {} does not match {} sites",
1645                symbols.len(),
1646                sites.len()
1647            )));
1648        }
1649        let base = self.states_per_site as u128;
1650        let mut amplitude = Complex64::new(1.0, 0.0);
1651        for (&site, &op) in sites.iter().zip(symbols).rev() {
1652            checked_site(site, self.sites)?;
1653            let place = base.pow(u32::try_from(site).unwrap_or(u32::MAX));
1654            let occupation = (state / place) % base;
1655            match op {
1656                'I' => {}
1657                'n' => amplitude *= occupation as f64,
1658                'z' => {
1659                    amplitude *=
1660                        occupation as f64 - 0.5 * (self.states_per_site.saturating_sub(1)) as f64;
1661                }
1662                '+' if occupation + 1 < base => {
1663                    state += place;
1664                    amplitude *= ((occupation + 1) as f64).sqrt();
1665                }
1666                '-' if occupation > 0 => {
1667                    state -= place;
1668                    amplitude *= (occupation as f64).sqrt();
1669                }
1670                '+' | '-' => return Ok(None),
1671                _ => return Err(QmbedError::InvalidOperator(op.to_string())),
1672            }
1673        }
1674        Ok(Some((state, amplitude)))
1675    }
1676}
1677
1678/// Builder for a full or fixed-particle bosonic lattice basis.
1679#[derive(Clone, Debug)]
1680pub struct BosonBasisBuilder {
1681    sites: usize,
1682    particles: Option<usize>,
1683    particle_sectors: Option<Vec<usize>>,
1684    states_per_site: usize,
1685}
1686
1687impl BosonBasisBuilder {
1688    /// Restrict the basis to a fixed total boson number.
1689    pub fn particles(mut self, particles: usize) -> Self {
1690        self.particles = Some(particles);
1691        self.particle_sectors = None;
1692        self
1693    }
1694
1695    /// Select a union of total boson-occupation sectors.
1696    pub fn particle_sectors(mut self, sectors: impl IntoIterator<Item = usize>) -> Self {
1697        self.particle_sectors = Some(sectors.into_iter().collect());
1698        self.particles = None;
1699        self
1700    }
1701
1702    /// Validate the cutoff and enumerate the requested bosonic sector.
1703    pub fn build(self) -> Result<BosonBasis1D> {
1704        if self.sites == 0 || self.states_per_site == 0 {
1705            return Err(QmbedError::InvalidSector(
1706                "boson sites and states_per_site must be positive".into(),
1707            ));
1708        }
1709        let maximum_particles = self.sites * (self.states_per_site - 1);
1710        if self
1711            .particles
1712            .is_some_and(|count| count > maximum_particles)
1713        {
1714            return Err(QmbedError::InvalidSector(
1715                "particle count exceeds the local cutoff".into(),
1716            ));
1717        }
1718        let particle_sectors = self
1719            .particle_sectors
1720            .map(|sectors| canonical_particle_sectors(sectors, maximum_particles, "boson"))
1721            .transpose()?;
1722        let states = fixed_digit_sum_sector_states(
1723            self.sites,
1724            self.states_per_site,
1725            self.particles,
1726            particle_sectors.as_deref(),
1727        )?;
1728        if states.is_empty() {
1729            return Err(QmbedError::InvalidSector("empty boson sector".into()));
1730        }
1731        Ok(BosonBasis1D {
1732            sites: self.sites,
1733            particles: self.particles,
1734            particle_sectors,
1735            states_per_site: self.states_per_site,
1736            states,
1737        })
1738    }
1739}
1740
1741impl Basis for BosonBasis1D {
1742    type State = u128;
1743
1744    fn len(&self) -> usize {
1745        self.states.len()
1746    }
1747
1748    fn state(&self, index: usize) -> Result<Self::State> {
1749        self.states
1750            .get(index)
1751            .copied()
1752            .ok_or(QmbedError::StateNotInBasis)
1753    }
1754
1755    fn index(&self, state: Self::State) -> Result<usize> {
1756        if self.particles.is_none() && self.particle_sectors.is_none() {
1757            return direct_state_index(&self.states, state);
1758        }
1759        state_index(&self.states, state)
1760    }
1761
1762    fn apply_local(
1763        &self,
1764        state: Self::State,
1765        operator: &str,
1766        sites: &[usize],
1767    ) -> Result<Option<(Self::State, Complex64)>> {
1768        let symbols = operator_chars(operator, sites)?;
1769        self.apply_local_symbols(state, &symbols, sites)
1770    }
1771
1772    fn visit_preparsed_local_unreduced_transitions<F>(
1773        &self,
1774        state: Self::State,
1775        _operator: &str,
1776        symbols: &[char],
1777        _split: Option<usize>,
1778        sites: &[usize],
1779        mut visit: F,
1780    ) -> Result<()>
1781    where
1782        F: FnMut(Self::State, Complex64) -> Result<()>,
1783    {
1784        if let Some((target, amplitude)) = self.apply_local_symbols(state, symbols, sites)? {
1785            visit(target, amplitude)?;
1786        }
1787        Ok(())
1788    }
1789
1790    fn operator_preserves_particle_sector(&self, operator: &str) -> Result<bool> {
1791        Ok(selected_sectors_preserve_changes(
1792            self.particles,
1793            self.particle_sectors.as_deref(),
1794            self.sites * (self.states_per_site - 1),
1795            &operator_number_changes(operator)?,
1796        ))
1797    }
1798}
1799
1800/// Single-flavor fermion basis.
1801#[derive(Clone, Debug)]
1802pub struct SpinlessFermionBasis1D {
1803    sites: usize,
1804    particles: Option<usize>,
1805    particle_sectors: Option<Vec<usize>>,
1806    momentum: Option<usize>,
1807    orbit_lengths: Vec<usize>,
1808    symmetry_lookup: HashMap<u128, SymmetryImage>,
1809    states: Vec<u128>,
1810}
1811
1812impl SpinlessFermionBasis1D {
1813    /// Start a spinless-fermion basis builder.
1814    pub fn builder(sites: usize) -> SpinlessFermionBasisBuilder {
1815        SpinlessFermionBasisBuilder {
1816            sites,
1817            particles: None,
1818            particle_sectors: None,
1819            momentum: None,
1820        }
1821    }
1822
1823    /// Return the number of fermionic orbitals.
1824    pub const fn sites(&self) -> usize {
1825        self.sites
1826    }
1827
1828    /// Return the fixed particle number, if selected.
1829    pub const fn particles(&self) -> Option<usize> {
1830        self.particles
1831    }
1832
1833    /// Allowed particle-number sectors when the basis is a union.
1834    pub fn particle_sectors(&self) -> Option<&[usize]> {
1835        self.particle_sectors.as_deref()
1836    }
1837
1838    /// Return the canonical translation-momentum sector, if selected.
1839    pub const fn momentum(&self) -> Option<usize> {
1840        self.momentum
1841    }
1842
1843    fn unreduced_local_transition(
1844        &self,
1845        state: u128,
1846        operator: &str,
1847        sites: &[usize],
1848    ) -> Result<Option<(u128, Complex64)>> {
1849        let symbols = operator_chars(operator, sites)?;
1850        self.unreduced_local_transition_with_symbols(state, &symbols, sites)
1851    }
1852
1853    fn unreduced_local_transition_with_symbols(
1854        &self,
1855        mut state: u128,
1856        symbols: &[char],
1857        sites: &[usize],
1858    ) -> Result<Option<(u128, Complex64)>> {
1859        if symbols.len() != sites.len() {
1860            return Err(QmbedError::InvalidCoupling(format!(
1861                "operator arity {} does not match {} sites",
1862                symbols.len(),
1863                sites.len()
1864            )));
1865        }
1866        let mut amplitude = Complex64::new(1.0, 0.0);
1867        for (&site, &op) in sites.iter().zip(symbols).rev() {
1868            checked_site(site, self.sites)?;
1869            let Some((next, local)) = apply_fermion(state, site, op)? else {
1870                return Ok(None);
1871            };
1872            state = next;
1873            amplitude *= local;
1874        }
1875        Ok(Some((state, amplitude)))
1876    }
1877}
1878
1879/// Builder for full, fixed-number, or momentum-reduced spinless Fock spaces.
1880#[derive(Clone, Debug)]
1881pub struct SpinlessFermionBasisBuilder {
1882    sites: usize,
1883    particles: Option<usize>,
1884    particle_sectors: Option<Vec<usize>>,
1885    momentum: Option<i32>,
1886}
1887
1888impl SpinlessFermionBasisBuilder {
1889    /// Restrict the basis to a fixed fermion number.
1890    pub fn particles(mut self, particles: usize) -> Self {
1891        self.particles = Some(particles);
1892        self.particle_sectors = None;
1893        self
1894    }
1895
1896    /// Select a union of particle-number sectors.
1897    pub fn particle_sectors(mut self, sectors: impl IntoIterator<Item = usize>) -> Self {
1898        self.particle_sectors = Some(sectors.into_iter().collect());
1899        self.particles = None;
1900        self
1901    }
1902
1903    /// Select a translation-momentum sector.
1904    pub const fn momentum(mut self, momentum: i32) -> Self {
1905        self.momentum = Some(momentum);
1906        self
1907    }
1908
1909    /// Validate the sector and enumerate the spinless-fermion basis.
1910    pub fn build(self) -> Result<SpinlessFermionBasis1D> {
1911        let particle_sectors = self
1912            .particle_sectors
1913            .map(|sectors| canonical_particle_sectors(sectors, self.sites, "fermion"))
1914            .transpose()?;
1915        let parent_states =
1916            fixed_weight_sector_states(self.sites, self.particles, particle_sectors.as_deref())?;
1917        let (states, orbit_lengths, symmetry_lookup, momentum) =
1918            fermion_translation_sector(parent_states, self.sites, self.momentum)?;
1919        Ok(SpinlessFermionBasis1D {
1920            sites: self.sites,
1921            particles: self.particles,
1922            particle_sectors,
1923            momentum,
1924            orbit_lengths,
1925            symmetry_lookup,
1926            states,
1927        })
1928    }
1929}
1930
1931fn apply_fermion(mut state: u128, orbital: usize, op: char) -> Result<Option<(u128, Complex64)>> {
1932    let mask = 1_u128 << orbital;
1933    let occupied = state & mask != 0;
1934    let prior_mask = mask - 1;
1935    let sign = if (state & prior_mask).count_ones() % 2 == 0 {
1936        1.0
1937    } else {
1938        -1.0
1939    };
1940    let amplitude = match op {
1941        'I' => 1.0,
1942        'n' => return Ok(occupied.then_some((state, Complex64::new(1.0, 0.0)))),
1943        'z' => {
1944            return Ok(Some((
1945                state,
1946                Complex64::new(if occupied { 0.5 } else { -0.5 }, 0.0),
1947            )));
1948        }
1949        '+' if !occupied => {
1950            state |= mask;
1951            sign
1952        }
1953        '-' if occupied => {
1954            state &= !mask;
1955            sign
1956        }
1957        'x' => {
1958            state ^= mask;
1959            sign
1960        }
1961        'y' => {
1962            state ^= mask;
1963            return Ok(Some((
1964                state,
1965                Complex64::new(0.0, if occupied { -sign } else { sign }),
1966            )));
1967        }
1968        '+' | '-' => return Ok(None),
1969        _ => return Err(QmbedError::InvalidOperator(op.to_string())),
1970    };
1971    Ok(Some((state, Complex64::new(amplitude, 0.0))))
1972}
1973
1974impl Basis for SpinlessFermionBasis1D {
1975    type State = u128;
1976
1977    fn len(&self) -> usize {
1978        self.states.len()
1979    }
1980
1981    fn state(&self, index: usize) -> Result<Self::State> {
1982        self.states
1983            .get(index)
1984            .copied()
1985            .ok_or(QmbedError::StateNotInBasis)
1986    }
1987
1988    fn index(&self, state: Self::State) -> Result<usize> {
1989        if self.momentum.is_none() {
1990            if self.particle_sectors.is_some() {
1991                return state_index(&self.states, state);
1992            }
1993            return self.particles.map_or_else(
1994                || direct_state_index(&self.states, state),
1995                |particles| fixed_weight_state_index(state, self.sites, particles),
1996            );
1997        }
1998        state_index(&self.states, state)
1999    }
2000
2001    fn apply_local(
2002        &self,
2003        mut state: Self::State,
2004        operator: &str,
2005        sites: &[usize],
2006    ) -> Result<Option<(Self::State, Complex64)>> {
2007        let source_state = state;
2008        let chars = operator_chars(operator, sites)?;
2009        let mut amplitude = Complex64::new(1.0, 0.0);
2010        for (&site, op) in sites.iter().zip(chars).rev() {
2011            checked_site(site, self.sites)?;
2012            let Some((next, local)) = apply_fermion(state, site, op)? else {
2013                return Ok(None);
2014            };
2015            state = next;
2016            amplitude *= local;
2017        }
2018        if self.momentum.is_some() {
2019            let source_index = self.index(source_state)?;
2020            let source_orbit = self.orbit_lengths[source_index];
2021            let Some(image) = self.symmetry_lookup.get(&state) else {
2022                return Ok(None);
2023            };
2024            amplitude *=
2025                (source_orbit as f64 / image.orbit_size as f64).sqrt() * image.phase.conj();
2026            state = image.representative;
2027        }
2028        Ok(Some((state, amplitude)))
2029    }
2030
2031    fn apply_local_unreduced_transitions(
2032        &self,
2033        state: Self::State,
2034        operator: &str,
2035        sites: &[usize],
2036    ) -> Result<LocalTransitions<Self::State>> {
2037        Ok(self
2038            .unreduced_local_transition(state, operator, sites)?
2039            .into_iter()
2040            .collect())
2041    }
2042
2043    fn visit_local_unreduced_transitions<F>(
2044        &self,
2045        state: Self::State,
2046        operator: &str,
2047        sites: &[usize],
2048        mut visit: F,
2049    ) -> Result<()>
2050    where
2051        F: FnMut(Self::State, Complex64) -> Result<()>,
2052    {
2053        if let Some((target, amplitude)) =
2054            self.unreduced_local_transition(state, operator, sites)?
2055        {
2056            visit(target, amplitude)?;
2057        }
2058        Ok(())
2059    }
2060
2061    fn visit_preparsed_local_unreduced_transitions<F>(
2062        &self,
2063        state: Self::State,
2064        _operator: &str,
2065        symbols: &[char],
2066        _split: Option<usize>,
2067        sites: &[usize],
2068        mut visit: F,
2069    ) -> Result<()>
2070    where
2071        F: FnMut(Self::State, Complex64) -> Result<()>,
2072    {
2073        if let Some((target, amplitude)) =
2074            self.unreduced_local_transition_with_symbols(state, symbols, sites)?
2075        {
2076            visit(target, amplitude)?;
2077        }
2078        Ok(())
2079    }
2080
2081    fn transition_orbit_size(&self, state: Self::State) -> Result<usize> {
2082        if self.momentum.is_none() {
2083            return Ok(1);
2084        }
2085        Ok(self.orbit_lengths[self.index(state)?])
2086    }
2087
2088    fn reduction_image(&self, state: Self::State) -> Result<Option<ReductionImage<Self::State>>> {
2089        let Some(image) = self.symmetry_lookup.get(&state) else {
2090            return Ok(None);
2091        };
2092        Ok(Some(ReductionImage::new(
2093            image.representative,
2094            image.phase,
2095            image.orbit_size,
2096        )?))
2097    }
2098
2099    fn reduce_transition(
2100        &self,
2101        state: Self::State,
2102        source_orbit_size: usize,
2103    ) -> Result<Option<(Self::State, Complex64)>> {
2104        let Some(image) = self.symmetry_lookup.get(&state) else {
2105            return Ok(None);
2106        };
2107        Ok(Some((
2108            image.representative,
2109            (source_orbit_size as f64 / image.orbit_size as f64).sqrt() * image.phase.conj(),
2110        )))
2111    }
2112
2113    fn index_transition(
2114        &self,
2115        state: Self::State,
2116        source_orbit_size: usize,
2117    ) -> Result<Option<(usize, Complex64)>> {
2118        if self.momentum.is_none() {
2119            return match self.index(state) {
2120                Ok(index) => Ok(Some((index, Complex64::new(1.0, 0.0)))),
2121                Err(QmbedError::StateNotInBasis) => Ok(None),
2122                Err(error) => Err(error),
2123            };
2124        }
2125        let Some(image) = self.symmetry_lookup.get(&state) else {
2126            return Ok(None);
2127        };
2128        Ok(Some((
2129            self.index(image.representative)?,
2130            (source_orbit_size as f64 / image.orbit_size as f64).sqrt() * image.phase.conj(),
2131        )))
2132    }
2133
2134    fn operator_preserves_particle_sector(&self, operator: &str) -> Result<bool> {
2135        Ok(selected_sectors_preserve_changes(
2136            self.particles,
2137            self.particle_sectors.as_deref(),
2138            self.sites,
2139            &operator_number_changes(operator)?,
2140        ))
2141    }
2142}
2143
2144/// Two-flavor fermion basis with all up orbitals ordered before all down orbitals.
2145///
2146/// Rows follow the direct-product convention `up ⊗ down`: the up-basis row
2147/// is the major index and the down-basis row is the minor index.
2148#[derive(Clone, Debug)]
2149pub struct SpinfulFermionBasis1D {
2150    sites: usize,
2151    particles_up: Option<usize>,
2152    particles_down: Option<usize>,
2153    particle_sectors: Option<Vec<(usize, usize)>>,
2154    local_occupation_constraint: Option<LocalOccupationConstraint>,
2155    states: Vec<u128>,
2156    indices: Option<HashMap<u128, usize>>,
2157}
2158
2159impl SpinfulFermionBasis1D {
2160    /// Start a two-species fermion basis builder.
2161    pub fn builder(sites: usize) -> SpinfulFermionBasisBuilder {
2162        SpinfulFermionBasisBuilder {
2163            sites,
2164            particles_up: None,
2165            particles_down: None,
2166            particle_sectors: None,
2167            local_occupation_constraint: None,
2168        }
2169    }
2170
2171    /// Return the number of spatial sites per species.
2172    pub const fn sites(&self) -> usize {
2173        self.sites
2174    }
2175
2176    /// Return the fixed up-species particle number, if unique.
2177    pub const fn particles_up(&self) -> Option<usize> {
2178        self.particles_up
2179    }
2180
2181    /// Return the fixed down-species particle number, if unique.
2182    pub const fn particles_down(&self) -> Option<usize> {
2183        self.particles_down
2184    }
2185
2186    /// Return the selected union of `(up, down)` particle sectors.
2187    pub fn particle_sectors(&self) -> Option<&[(usize, usize)]> {
2188        self.particle_sectors.as_deref()
2189    }
2190
2191    /// Return the site-local occupation filter, if configured.
2192    pub const fn local_occupation_constraint(&self) -> Option<&LocalOccupationConstraint> {
2193        self.local_occupation_constraint.as_ref()
2194    }
2195
2196    fn apply_local_symbols(
2197        &self,
2198        mut state: u128,
2199        symbols: &[char],
2200        split: Option<usize>,
2201        sites: &[usize],
2202    ) -> Result<Option<(u128, Complex64)>> {
2203        if symbols.len() != sites.len() || split.is_some_and(|value| value > symbols.len()) {
2204            return Err(QmbedError::InvalidCoupling(format!(
2205                "operator arity {} does not match {} sites",
2206                symbols.len(),
2207                sites.len()
2208            )));
2209        }
2210        let mut amplitude = Complex64::new(1.0, 0.0);
2211        for (position, (&site, &op)) in sites.iter().zip(symbols).enumerate().rev() {
2212            let orbital = match split {
2213                Some(boundary) => {
2214                    checked_site(site, self.sites)?;
2215                    if position < boundary {
2216                        site
2217                    } else {
2218                        self.sites + site
2219                    }
2220                }
2221                None => {
2222                    let orbitals = self.sites.checked_mul(2).ok_or_else(|| {
2223                        QmbedError::UnsupportedBackend("spinful orbital count is too large".into())
2224                    })?;
2225                    checked_site(site, orbitals)?;
2226                    site
2227                }
2228            };
2229            let Some((next, local)) = apply_fermion(state, orbital, op)? else {
2230                return Ok(None);
2231            };
2232            state = next;
2233            amplitude *= local;
2234        }
2235        Ok(Some((state, amplitude)))
2236    }
2237}
2238
2239/// Builder for full or number-conserving two-species fermion spaces.
2240#[derive(Clone, Debug)]
2241pub struct SpinfulFermionBasisBuilder {
2242    sites: usize,
2243    particles_up: Option<usize>,
2244    particles_down: Option<usize>,
2245    particle_sectors: Option<Vec<(usize, usize)>>,
2246    local_occupation_constraint: Option<LocalOccupationConstraint>,
2247}
2248
2249impl SpinfulFermionBasisBuilder {
2250    /// Restrict only the up-species particle number.
2251    pub const fn particles_up(mut self, particles: usize) -> Self {
2252        self.particles_up = Some(particles);
2253        self
2254    }
2255
2256    /// Restrict only the down-species particle number.
2257    pub const fn particles_down(mut self, particles: usize) -> Self {
2258        self.particles_down = Some(particles);
2259        self
2260    }
2261
2262    /// Restrict both species to one fixed `(up, down)` sector.
2263    pub fn particles(mut self, up: usize, down: usize) -> Self {
2264        self.particles_up = Some(up);
2265        self.particles_down = Some(down);
2266        self.particle_sectors = None;
2267        self
2268    }
2269
2270    /// Select a union of fixed `(N_up, N_down)` sectors.
2271    pub fn particle_sectors(mut self, sectors: impl IntoIterator<Item = (usize, usize)>) -> Self {
2272        self.particle_sectors = Some(sectors.into_iter().collect());
2273        self.particles_up = None;
2274        self.particles_down = None;
2275        self
2276    }
2277
2278    /// Restrict the allowed site-local occupation masks for the two binary
2279    /// fermion species.
2280    pub fn local_occupation_constraint(mut self, constraint: LocalOccupationConstraint) -> Self {
2281        self.local_occupation_constraint = Some(constraint);
2282        self
2283    }
2284
2285    /// Validate species sectors and enumerate the spinful Fock basis.
2286    pub fn build(self) -> Result<SpinfulFermionBasis1D> {
2287        if self.sites > 64 {
2288            return Err(QmbedError::UnsupportedBackend(
2289                "the packed spinful backend supports at most 64 sites".into(),
2290            ));
2291        }
2292        if self
2293            .local_occupation_constraint
2294            .as_ref()
2295            .is_some_and(|constraint| constraint.species() != 2)
2296        {
2297            return Err(QmbedError::InvalidSector(
2298                "spinful fermions require a two-species local occupation constraint".into(),
2299            ));
2300        }
2301        let sectors = match &self.particle_sectors {
2302            Some(sectors) if sectors.is_empty() => {
2303                return Err(QmbedError::InvalidSector(
2304                    "spinful particle-sector union must be nonempty".into(),
2305                ));
2306            }
2307            Some(sectors) => sectors.clone(),
2308            None => vec![(
2309                self.particles_up.unwrap_or(usize::MAX),
2310                self.particles_down.unwrap_or(usize::MAX),
2311            )],
2312        };
2313        let mut states = Vec::new();
2314        for (up_count, down_count) in sectors {
2315            let up_states =
2316                fixed_weight_states(self.sites, (up_count != usize::MAX).then_some(up_count))?;
2317            let down_states =
2318                fixed_weight_states(self.sites, (down_count != usize::MAX).then_some(down_count))?;
2319            states.reserve(up_states.len().saturating_mul(down_states.len()));
2320            for down in down_states {
2321                for &up in &up_states {
2322                    states.push(up | (down << self.sites));
2323                }
2324            }
2325        }
2326        // Keep the row order identical to the direct product
2327        // `up_basis ⊗ down_basis`: the up-sector row is the major index and
2328        // the down-sector row is the minor index. This is observable for
2329        // state vectors and density matrices, and avoids making a spinful
2330        // basis silently disagree with the equivalent `PackedTensorBasis`.
2331        let mask = if self.sites == 128 {
2332            u128::MAX
2333        } else {
2334            (1_u128 << self.sites) - 1
2335        };
2336        states.sort_unstable_by_key(|state| ((*state & mask), (*state >> self.sites)));
2337        states.dedup();
2338        if let Some(constraint) = &self.local_occupation_constraint {
2339            let mut filtered = Vec::with_capacity(states.len());
2340            for state in states {
2341                if constraint.accepts_packed_state(state, self.sites)? {
2342                    filtered.push(state);
2343                }
2344            }
2345            states = filtered;
2346        }
2347        let indices = (self.particle_sectors.is_some()
2348            || self.local_occupation_constraint.is_some())
2349        .then(|| {
2350            states
2351                .iter()
2352                .copied()
2353                .enumerate()
2354                .map(|(index, state)| (state, index))
2355                .collect()
2356        });
2357        Ok(SpinfulFermionBasis1D {
2358            sites: self.sites,
2359            particles_up: self.particles_up,
2360            particles_down: self.particles_down,
2361            particle_sectors: self.particle_sectors,
2362            local_occupation_constraint: self.local_occupation_constraint,
2363            states,
2364            indices,
2365        })
2366    }
2367}
2368
2369impl Basis for SpinfulFermionBasis1D {
2370    type State = u128;
2371
2372    fn len(&self) -> usize {
2373        self.states.len()
2374    }
2375
2376    fn state(&self, index: usize) -> Result<Self::State> {
2377        self.states
2378            .get(index)
2379            .copied()
2380            .ok_or(QmbedError::StateNotInBasis)
2381    }
2382
2383    fn index(&self, state: Self::State) -> Result<usize> {
2384        if self.particle_sectors.is_none() && self.local_occupation_constraint.is_none() {
2385            let mask = if self.sites == 128 {
2386                u128::MAX
2387            } else {
2388                (1_u128 << self.sites) - 1
2389            };
2390            let up_state = state & mask;
2391            let down_state = state >> self.sites;
2392            let up_index = self.particles_up.map_or_else(
2393                || usize::try_from(up_state).map_err(|_| QmbedError::StateNotInBasis),
2394                |particles| fixed_weight_state_index(up_state, self.sites, particles),
2395            )?;
2396            let down_index = self.particles_down.map_or_else(
2397                || usize::try_from(down_state).map_err(|_| QmbedError::StateNotInBasis),
2398                |particles| fixed_weight_state_index(down_state, self.sites, particles),
2399            )?;
2400            let down_dimension = match self.particles_down {
2401                Some(particles) => binomial(self.sites, particles),
2402                None => 1_usize
2403                    .checked_shl(u32::try_from(self.sites).unwrap_or(u32::MAX))
2404                    .ok_or(QmbedError::StateNotInBasis)?,
2405            };
2406            let index = up_index
2407                .checked_mul(down_dimension)
2408                .and_then(|offset| offset.checked_add(down_index))
2409                .ok_or(QmbedError::StateNotInBasis)?;
2410            if index < self.states.len() {
2411                return Ok(index);
2412            }
2413            return Err(QmbedError::StateNotInBasis);
2414        }
2415        self.indices
2416            .as_ref()
2417            .and_then(|indices| indices.get(&state).copied())
2418            .ok_or(QmbedError::StateNotInBasis)
2419    }
2420
2421    fn apply_local(
2422        &self,
2423        state: Self::State,
2424        operator: &str,
2425        sites: &[usize],
2426    ) -> Result<Option<(Self::State, Complex64)>> {
2427        let symbols = operator_chars(operator, sites)?;
2428        let split = operator
2429            .find('|')
2430            .map(|position| operator[..position].chars().count());
2431        self.apply_local_symbols(state, &symbols, split, sites)
2432    }
2433
2434    fn visit_preparsed_local_unreduced_transitions<F>(
2435        &self,
2436        state: Self::State,
2437        _operator: &str,
2438        symbols: &[char],
2439        split: Option<usize>,
2440        sites: &[usize],
2441        mut visit: F,
2442    ) -> Result<()>
2443    where
2444        F: FnMut(Self::State, Complex64) -> Result<()>,
2445    {
2446        if let Some((target, amplitude)) = self.apply_local_symbols(state, symbols, split, sites)? {
2447            visit(target, amplitude)?;
2448        }
2449        Ok(())
2450    }
2451
2452    fn operator_preserves_particle_sector(&self, operator: &str) -> Result<bool> {
2453        let (up_operator, down_operator) = operator.split_once('|').unwrap_or((operator, ""));
2454        if down_operator.contains('|') {
2455            return Err(QmbedError::InvalidOperator(operator.into()));
2456        }
2457        let Some(up_change) = operator_number_change(up_operator)? else {
2458            return Ok(self.particles_up.is_none()
2459                && self.particle_sectors.is_none()
2460                && self.particles_down.is_none());
2461        };
2462        let Some(down_change) = operator_number_change(down_operator)? else {
2463            return Ok(self.particles_up.is_none()
2464                && self.particle_sectors.is_none()
2465                && self.particles_down.is_none());
2466        };
2467        if let Some(sectors) = &self.particle_sectors {
2468            let sectors: HashSet<_> = sectors.iter().copied().collect();
2469            return Ok(sectors.iter().all(|&(up, down)| {
2470                let target_up = up as i32 + up_change;
2471                let target_down = down as i32 + down_change;
2472                target_up >= 0
2473                    && target_down >= 0
2474                    && target_up <= self.sites as i32
2475                    && target_down <= self.sites as i32
2476                    && sectors.contains(&(target_up as usize, target_down as usize))
2477            }));
2478        }
2479        Ok(self.particles_up.is_none_or(|_| up_change == 0)
2480            && self.particles_down.is_none_or(|_| down_change == 0))
2481    }
2482
2483    fn operator_preserves_particle_sector_on_sites(
2484        &self,
2485        operator: &str,
2486        sites: &[usize],
2487    ) -> Result<bool> {
2488        if operator.contains('|') {
2489            return self.operator_preserves_particle_sector(operator);
2490        }
2491        let symbols = operator_chars(operator, sites)?;
2492        let mut changes = [Some(0_i32), Some(0_i32)];
2493        for (&symbol, &orbital) in symbols.iter().zip(sites) {
2494            let orbitals = self.sites.checked_mul(2).ok_or_else(|| {
2495                QmbedError::UnsupportedBackend("spinful orbital count is too large".into())
2496            })?;
2497            checked_site(orbital, orbitals)?;
2498            let species = usize::from(orbital >= self.sites);
2499            match symbol {
2500                '+' => {
2501                    if let Some(change) = &mut changes[species] {
2502                        *change += 1;
2503                    }
2504                }
2505                '-' => {
2506                    if let Some(change) = &mut changes[species] {
2507                        *change -= 1;
2508                    }
2509                }
2510                'x' | 'y' => changes[species] = None,
2511                'I' | 'n' | 'z' => {}
2512                _ => return Err(QmbedError::InvalidOperator(operator.into())),
2513            }
2514        }
2515        if let Some(sectors) = &self.particle_sectors {
2516            let sectors: HashSet<_> = sectors.iter().copied().collect();
2517            return Ok(sectors.iter().all(|&(up, down)| {
2518                let Some(target_up) = changes[0].and_then(|change| (up as i32).checked_add(change))
2519                else {
2520                    return false;
2521                };
2522                let Some(target_down) =
2523                    changes[1].and_then(|change| (down as i32).checked_add(change))
2524                else {
2525                    return false;
2526                };
2527                target_up >= 0
2528                    && target_down >= 0
2529                    && target_up <= self.sites as i32
2530                    && target_down <= self.sites as i32
2531                    && sectors.contains(&(target_up as usize, target_down as usize))
2532            }));
2533        }
2534        Ok(self
2535            .particles_up
2536            .is_none_or(|_| changes[0].is_some_and(|change| change == 0))
2537            && self
2538                .particles_down
2539                .is_none_or(|_| changes[1].is_some_and(|change| change == 0)))
2540    }
2541}
2542
2543type UserAction<State> = Arc<
2544    dyn Fn(State, usize, &mut dyn FnMut(State, Complex64) -> Result<()>) -> Result<()>
2545        + Send
2546        + Sync,
2547>;
2548type UserStateFactory<State> = Arc<dyn Fn() -> Result<Vec<State>> + Send + Sync>;
2549
2550/// Callback-defined constrained basis using the same assembly path as built-ins.
2551#[derive(Clone)]
2552pub struct UserBasis<State>
2553where
2554    State: Copy + Eq + Hash + Send + Sync,
2555{
2556    sites: usize,
2557    states: Vec<State>,
2558    indices: HashMap<State, usize>,
2559    operators: HashMap<char, UserAction<State>>,
2560}
2561
2562impl<State> std::fmt::Debug for UserBasis<State>
2563where
2564    State: Copy + Eq + Hash + Send + Sync,
2565{
2566    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2567        let mut operators: Vec<_> = self.operators.keys().copied().collect();
2568        operators.sort_unstable();
2569        formatter
2570            .debug_struct("UserBasis")
2571            .field("sites", &self.sites)
2572            .field("states", &self.states.len())
2573            .field("operators", &operators)
2574            .finish()
2575    }
2576}
2577
2578impl<State> UserBasis<State>
2579where
2580    State: Copy + Eq + Hash + Send + Sync + 'static,
2581{
2582    pub fn builder(sites: usize) -> UserBasisBuilder<State> {
2583        UserBasisBuilder {
2584            sites,
2585            states: Vec::new(),
2586            state_factory: None,
2587            operators: HashMap::new(),
2588        }
2589    }
2590
2591    pub const fn sites(&self) -> usize {
2592        self.sites
2593    }
2594
2595    /// Reuse the same callback-defined local algebra on another explicit
2596    /// state set.
2597    ///
2598    /// This separates the local operator semantics from state-space
2599    /// constraints. Language bindings use it to construct the full,
2600    /// constrained, and symmetry-reduced views required by projection and
2601    /// subsystem analysis without duplicating callback adapters.
2602    pub fn with_states(&self, states: impl IntoIterator<Item = State>) -> Result<UserBasis<State>> {
2603        let mut indices = HashMap::new();
2604        let states: Vec<_> = states.into_iter().collect();
2605        if states.is_empty() {
2606            return Err(QmbedError::InvalidSector(
2607                "UserBasis requires at least one accepted state".into(),
2608            ));
2609        }
2610        indices.reserve(states.len());
2611        for (index, state) in states.iter().copied().enumerate() {
2612            if indices.insert(state, index).is_some() {
2613                return Err(QmbedError::InvalidSector(
2614                    "UserBasis states must be unique".into(),
2615                ));
2616            }
2617        }
2618        Ok(UserBasis {
2619            sites: self.sites,
2620            states,
2621            indices,
2622            operators: self.operators.clone(),
2623        })
2624    }
2625}
2626
2627pub struct UserBasisBuilder<State>
2628where
2629    State: Copy + Eq + Hash + Send + Sync,
2630{
2631    sites: usize,
2632    states: Vec<State>,
2633    state_factory: Option<UserStateFactory<State>>,
2634    operators: HashMap<char, UserAction<State>>,
2635}
2636
2637impl<State> UserBasisBuilder<State>
2638where
2639    State: Copy + Eq + Hash + Send + Sync + 'static,
2640{
2641    pub fn states(mut self, states: impl IntoIterator<Item = State>) -> Self {
2642        self.states = states.into_iter().collect();
2643        self.state_factory = None;
2644        self
2645    }
2646
2647    /// Defer potentially expensive state enumeration until `materialize` or
2648    /// `build` is called.
2649    pub fn deferred_states<F>(mut self, factory: F) -> Self
2650    where
2651        F: Fn() -> Result<Vec<State>> + Send + Sync + 'static,
2652    {
2653        self.states.clear();
2654        self.state_factory = Some(Arc::new(factory));
2655        self
2656    }
2657
2658    pub fn operator<F>(mut self, name: char, action: F) -> Self
2659    where
2660        F: Fn(State, usize) -> Result<Option<(State, Complex64)>> + Send + Sync + 'static,
2661    {
2662        self.operators.insert(
2663            name,
2664            Arc::new(move |state, site, visit| {
2665                if let Some((target, amplitude)) = action(state, site)? {
2666                    visit(target, amplitude)?;
2667                }
2668                Ok(())
2669            }),
2670        );
2671        self
2672    }
2673
2674    /// Register a local action with more than one nonzero destination.
2675    pub fn branching_operator<F>(mut self, name: char, action: F) -> Self
2676    where
2677        F: Fn(State, usize) -> Result<Vec<(State, Complex64)>> + Send + Sync + 'static,
2678    {
2679        self.operators.insert(
2680            name,
2681            Arc::new(move |state, site, visit| {
2682                for (target, amplitude) in action(state, site)? {
2683                    visit(target, amplitude)?;
2684                }
2685                Ok(())
2686            }),
2687        );
2688        self
2689    }
2690
2691    pub fn build(mut self) -> Result<UserBasis<State>> {
2692        if self.states.is_empty() {
2693            if let Some(factory) = self.state_factory.take() {
2694                self.states = factory()?;
2695            }
2696        }
2697        if self.states.is_empty() {
2698            return Err(QmbedError::InvalidSector(
2699                "UserBasis requires at least one accepted state".into(),
2700            ));
2701        }
2702        let mut indices = HashMap::with_capacity(self.states.len());
2703        for (index, state) in self.states.iter().copied().enumerate() {
2704            if indices.insert(state, index).is_some() {
2705                return Err(QmbedError::InvalidSector(
2706                    "UserBasis states must be unique".into(),
2707                ));
2708            }
2709        }
2710        Ok(UserBasis {
2711            sites: self.sites,
2712            states: self.states,
2713            indices,
2714            operators: self.operators,
2715        })
2716    }
2717
2718    pub fn materialize(self) -> Result<UserBasis<State>> {
2719        self.build()
2720    }
2721}
2722
2723impl UserBasisBuilder<u128> {
2724    pub fn state_filter<F>(mut self, keep: F) -> Result<Self>
2725    where
2726        F: Fn(u128) -> bool,
2727    {
2728        if self.sites > 127 {
2729            return Err(QmbedError::UnsupportedBackend(
2730                "u128 UserBasis filters support at most 127 sites".into(),
2731            ));
2732        }
2733        let limit = 1_u128 << self.sites;
2734        self.states = (0..limit).filter(|state| keep(*state)).collect();
2735        Ok(self)
2736    }
2737
2738    /// Deterministically enumerate a filtered binary state space in parallel.
2739    pub fn state_filter_parallel<F>(mut self, keep: F) -> Result<Self>
2740    where
2741        F: Fn(u128) -> bool + Sync,
2742    {
2743        if self.sites > 127 {
2744            return Err(QmbedError::UnsupportedBackend(
2745                "u128 UserBasis filters support at most 127 sites".into(),
2746            ));
2747        }
2748        let limit = 1_u128 << self.sites;
2749        let workers = std::thread::available_parallelism()
2750            .map_or(1, std::num::NonZeroUsize::get)
2751            .min(usize::try_from(limit).unwrap_or(usize::MAX).max(1));
2752        let stride = limit.div_ceil(workers as u128);
2753        let mut chunks = std::thread::scope(|scope| {
2754            let mut handles = Vec::with_capacity(workers);
2755            let keep = &keep;
2756            for worker in 0..workers {
2757                let start = worker as u128 * stride;
2758                let end = (start + stride).min(limit);
2759                handles.push(scope.spawn(move || {
2760                    (start..end)
2761                        .filter(|state| keep(*state))
2762                        .collect::<Vec<_>>()
2763                }));
2764            }
2765            handles
2766                .into_iter()
2767                .map(|handle| handle.join().unwrap_or_default())
2768                .collect::<Vec<_>>()
2769        });
2770        self.states.clear();
2771        for chunk in &mut chunks {
2772            self.states.append(chunk);
2773        }
2774        Ok(self)
2775    }
2776}
2777
2778impl<State> UserBasis<State>
2779where
2780    State: Copy + Eq + Hash + Send + Sync + 'static,
2781{
2782    fn visit_user_transitions<F>(
2783        &self,
2784        state: State,
2785        operator: &str,
2786        sites: &[usize],
2787        visit: F,
2788    ) -> Result<()>
2789    where
2790        F: FnMut(State, Complex64) -> Result<()>,
2791    {
2792        let symbols = operator_chars(operator, sites)?;
2793        self.visit_user_transitions_with_symbols(state, &symbols, sites, visit)
2794    }
2795
2796    fn visit_user_transitions_with_symbols<F>(
2797        &self,
2798        state: State,
2799        symbols: &[char],
2800        sites: &[usize],
2801        mut visit: F,
2802    ) -> Result<()>
2803    where
2804        F: FnMut(State, Complex64) -> Result<()>,
2805    {
2806        if symbols.len() != sites.len() {
2807            return Err(QmbedError::InvalidCoupling(format!(
2808                "operator arity {} does not match {} sites",
2809                symbols.len(),
2810                sites.len()
2811            )));
2812        }
2813        self.visit_user_branch(
2814            state,
2815            Complex64::new(1.0, 0.0),
2816            symbols,
2817            sites,
2818            symbols.len(),
2819            &mut visit,
2820        )
2821    }
2822
2823    fn visit_user_branch<F>(
2824        &self,
2825        state: State,
2826        amplitude: Complex64,
2827        chars: &[char],
2828        sites: &[usize],
2829        remaining: usize,
2830        visit: &mut F,
2831    ) -> Result<()>
2832    where
2833        F: FnMut(State, Complex64) -> Result<()>,
2834    {
2835        if remaining == 0 {
2836            return visit(state, amplitude);
2837        }
2838        let position = remaining - 1;
2839        let site = sites[position];
2840        let op = chars[position];
2841        checked_site(site, self.sites)?;
2842        let action = self
2843            .operators
2844            .get(&op)
2845            .ok_or_else(|| QmbedError::InvalidOperator(op.to_string()))?;
2846        action(state, site, &mut |target, local| {
2847            if local.norm() <= f64::EPSILON {
2848                return Ok(());
2849            }
2850            self.visit_user_branch(target, amplitude * local, chars, sites, position, visit)
2851        })
2852    }
2853}
2854
2855impl<State> Basis for UserBasis<State>
2856where
2857    State: Copy + Eq + Hash + Send + Sync + 'static,
2858{
2859    type State = State;
2860
2861    fn len(&self) -> usize {
2862        self.states.len()
2863    }
2864
2865    fn state(&self, index: usize) -> Result<Self::State> {
2866        self.states
2867            .get(index)
2868            .copied()
2869            .ok_or(QmbedError::StateNotInBasis)
2870    }
2871
2872    fn index(&self, state: Self::State) -> Result<usize> {
2873        self.indices
2874            .get(&state)
2875            .copied()
2876            .ok_or(QmbedError::StateNotInBasis)
2877    }
2878
2879    fn apply_local(
2880        &self,
2881        state: Self::State,
2882        operator: &str,
2883        sites: &[usize],
2884    ) -> Result<Option<(Self::State, Complex64)>> {
2885        let transitions = self.apply_local_transitions(state, operator, sites)?;
2886        match transitions.as_slice() {
2887            [] => Ok(None),
2888            [transition] => Ok(Some(*transition)),
2889            _ => Err(QmbedError::UnsupportedBackend(
2890                "this user local action branches; use apply_local_transitions".into(),
2891            )),
2892        }
2893    }
2894
2895    fn apply_local_transitions(
2896        &self,
2897        state: Self::State,
2898        operator: &str,
2899        sites: &[usize],
2900    ) -> Result<LocalTransitions<Self::State>> {
2901        let mut accumulated = HashMap::<State, Complex64>::new();
2902        self.visit_user_transitions(state, operator, sites, |target, amplitude| {
2903            *accumulated
2904                .entry(target)
2905                .or_insert(Complex64::new(0.0, 0.0)) += amplitude;
2906            Ok(())
2907        })?;
2908        Ok(accumulated
2909            .into_iter()
2910            .filter(|(_, amplitude)| amplitude.norm() > f64::EPSILON)
2911            .collect())
2912    }
2913
2914    fn visit_local_unreduced_transitions<F>(
2915        &self,
2916        state: Self::State,
2917        operator: &str,
2918        sites: &[usize],
2919        visit: F,
2920    ) -> Result<()>
2921    where
2922        F: FnMut(Self::State, Complex64) -> Result<()>,
2923    {
2924        self.visit_user_transitions(state, operator, sites, visit)
2925    }
2926
2927    fn visit_preparsed_local_unreduced_transitions<F>(
2928        &self,
2929        state: Self::State,
2930        _operator: &str,
2931        symbols: &[char],
2932        _split: Option<usize>,
2933        sites: &[usize],
2934        visit: F,
2935    ) -> Result<()>
2936    where
2937        F: FnMut(Self::State, Complex64) -> Result<()>,
2938    {
2939        self.visit_user_transitions_with_symbols(state, symbols, sites, visit)
2940    }
2941}
2942
2943/// Finite symmetry action, including any phase acquired by the state.
2944pub trait SymmetryMap<State>: Send + Sync {
2945    fn period(&self) -> usize;
2946    fn apply(&self, state: State) -> Result<(State, Complex64)>;
2947}
2948
2949/// Exchange statistics used when a lattice map reorders local degrees of freedom.
2950#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2951pub enum ExchangeStatistics {
2952    Distinguishable,
2953    Fermionic,
2954}
2955
2956/// Runtime-owned finite symmetry of a packed lattice state.
2957///
2958/// `destinations[source]` gives the target site. Each source site also owns a
2959/// permutation of its local digits, allowing the same representation to cover
2960/// translations, reflections, sublattice maps, and local spin inversion.
2961/// Fermionic maps compute the parity of the originally occupied-orbital
2962/// permutation instead of requiring a frontend-provided phase callback.
2963/// Binary local permutations may also exchange empty and occupied digits.
2964/// Such particle-hole flips change the mapped occupation but introduce no
2965/// additional Fock-state phase; the orbital reordering alone fixes the sign.
2966#[derive(Clone, Debug)]
2967pub struct LatticeSymmetryMap {
2968    states_per_site: usize,
2969    destinations: Vec<usize>,
2970    local_permutations: Vec<Vec<usize>>,
2971    statistics: ExchangeStatistics,
2972    place_values: Option<Vec<u128>>,
2973    state_limit: Option<u128>,
2974    period: usize,
2975}
2976
2977impl LatticeSymmetryMap {
2978    pub fn new(
2979        states_per_site: usize,
2980        destinations: impl Into<Vec<usize>>,
2981        local_permutations: Option<Vec<Vec<usize>>>,
2982        statistics: ExchangeStatistics,
2983    ) -> Result<Self> {
2984        let destinations = destinations.into();
2985        if destinations.is_empty() || states_per_site == 0 {
2986            return Err(QmbedError::InvalidSector(
2987                "a lattice symmetry requires sites and local states".into(),
2988            ));
2989        }
2990        let unique_destinations: HashSet<_> = destinations.iter().copied().collect();
2991        if unique_destinations.len() != destinations.len()
2992            || destinations
2993                .iter()
2994                .any(|&destination| destination >= destinations.len())
2995        {
2996            return Err(QmbedError::IncompatibleSymmetry(
2997                "symmetry site destinations must be a bijection".into(),
2998            ));
2999        }
3000
3001        let identity = (0..states_per_site).collect::<Vec<_>>();
3002        let local_permutations =
3003            local_permutations.unwrap_or_else(|| vec![identity.clone(); destinations.len()]);
3004        if local_permutations.len() != destinations.len()
3005            || local_permutations.iter().any(|permutation| {
3006                permutation.len() != states_per_site
3007                    || permutation.iter().copied().collect::<HashSet<_>>().len() != states_per_site
3008                    || permutation.iter().any(|&digit| digit >= states_per_site)
3009            })
3010        {
3011            return Err(QmbedError::IncompatibleSymmetry(
3012                "every local-state map must be a permutation".into(),
3013            ));
3014        }
3015        if statistics == ExchangeStatistics::Fermionic && states_per_site != 2 {
3016            return Err(QmbedError::InvalidOptions(
3017                "fermionic lattice maps require binary occupation".into(),
3018            ));
3019        }
3020
3021        let base = states_per_site as u128;
3022        let mut packed_place_values = Vec::with_capacity(destinations.len());
3023        let mut place = Some(1_u128);
3024        for site in 0..destinations.len() {
3025            if let Some(value) = place {
3026                packed_place_values.push(value);
3027            }
3028            if site + 1 < destinations.len() {
3029                place = place.and_then(|value| value.checked_mul(base));
3030            }
3031        }
3032        let place_values =
3033            (packed_place_values.len() == destinations.len()).then_some(packed_place_values);
3034        let state_limit = place.and_then(|value| value.checked_mul(base));
3035        let exact_full_range = base.is_power_of_two()
3036            && (base.trailing_zeros() as usize).checked_mul(destinations.len()) == Some(128);
3037        let wide_binary = states_per_site == 2 && destinations.len() <= 16_384;
3038        if place_values.is_none() && !wide_binary {
3039            return Err(QmbedError::UnsupportedBackend(
3040                "lattice-symmetry state encoding exceeds the supported fixed-width backends".into(),
3041            ));
3042        }
3043        if place_values.is_some() && state_limit.is_none() && !exact_full_range {
3044            return Err(QmbedError::UnsupportedBackend(
3045                "lattice-symmetry state encoding exceeds u128".into(),
3046            ));
3047        }
3048        let period =
3049            combined_permutation_period(&destinations, &local_permutations, states_per_site)?;
3050        Ok(Self {
3051            states_per_site,
3052            destinations,
3053            local_permutations,
3054            statistics,
3055            place_values,
3056            state_limit,
3057            period,
3058        })
3059    }
3060
3061    pub fn site_permutation(
3062        states_per_site: usize,
3063        destinations: impl Into<Vec<usize>>,
3064    ) -> Result<Self> {
3065        Self::new(
3066            states_per_site,
3067            destinations,
3068            None,
3069            ExchangeStatistics::Distinguishable,
3070        )
3071    }
3072
3073    pub fn fermionic_orbital_permutation(destinations: impl Into<Vec<usize>>) -> Result<Self> {
3074        Self::new(2, destinations, None, ExchangeStatistics::Fermionic)
3075    }
3076
3077    pub fn sites(&self) -> usize {
3078        self.destinations.len()
3079    }
3080
3081    pub const fn states_per_site(&self) -> usize {
3082        self.states_per_site
3083    }
3084
3085    pub fn destinations(&self) -> &[usize] {
3086        &self.destinations
3087    }
3088
3089    pub fn local_permutations(&self) -> &[Vec<usize>] {
3090        &self.local_permutations
3091    }
3092
3093    pub const fn statistics(&self) -> ExchangeStatistics {
3094        self.statistics
3095    }
3096
3097    pub const fn period(&self) -> usize {
3098        self.period
3099    }
3100
3101    fn mapped_state(&self, state: u128) -> Result<(u128, Complex64)> {
3102        let place_values = self.place_values.as_ref().ok_or_else(|| {
3103            QmbedError::UnsupportedBackend(
3104                "this lattice symmetry requires a wide fixed-width state".into(),
3105            )
3106        })?;
3107        if self.state_limit.is_some_and(|limit| state >= limit) {
3108            return Err(QmbedError::StateNotInBasis);
3109        }
3110        let base = self.states_per_site as u128;
3111        let mut mapped = 0_u128;
3112        let mut occupied_destinations = 0_u128;
3113        let mut odd_fermion_permutation = false;
3114        for source in 0..self.destinations.len() {
3115            let digit = usize::try_from((state / place_values[source]) % base)
3116                .map_err(|_| QmbedError::StateNotInBasis)?;
3117            let mapped_digit = self.local_permutations[source][digit];
3118            let destination = self.destinations[source];
3119            mapped += mapped_digit as u128 * place_values[destination];
3120            if self.statistics == ExchangeStatistics::Fermionic && digit == 1 {
3121                let occupied_after = if destination == 127 {
3122                    0
3123                } else {
3124                    (occupied_destinations >> (destination + 1)).count_ones()
3125                };
3126                odd_fermion_permutation ^= occupied_after % 2 == 1;
3127                occupied_destinations |= 1_u128 << destination;
3128            }
3129        }
3130        Ok((
3131            mapped,
3132            Complex64::new(if odd_fermion_permutation { -1.0 } else { 1.0 }, 0.0),
3133        ))
3134    }
3135}
3136
3137impl SymmetryMap<u128> for LatticeSymmetryMap {
3138    fn period(&self) -> usize {
3139        self.period
3140    }
3141
3142    fn apply(&self, state: u128) -> Result<(u128, Complex64)> {
3143        self.mapped_state(state)
3144    }
3145}
3146
3147impl<const WORDS: usize> SymmetryMap<WideState<WORDS>> for LatticeSymmetryMap {
3148    fn period(&self) -> usize {
3149        self.period
3150    }
3151
3152    fn apply(&self, state: WideState<WORDS>) -> Result<(WideState<WORDS>, Complex64)> {
3153        if self.states_per_site != 2
3154            || self.destinations.len() > WideState::<WORDS>::capacity_bits()
3155        {
3156            return Err(QmbedError::UnsupportedBackend(
3157                "wide lattice symmetries currently require a binary fixed-width state".into(),
3158            ));
3159        }
3160        if state.has_bits_at_or_above(self.destinations.len()) {
3161            return Err(QmbedError::StateNotInBasis);
3162        }
3163        let mut mapped = WideState::<WORDS>::zero();
3164        let mut occupied_destinations = WideState::<WORDS>::zero();
3165        let mut odd_fermion_permutation = false;
3166        for source in 0..self.destinations.len() {
3167            let occupied = state.bit(source)?;
3168            let digit = usize::from(occupied);
3169            let mapped_digit = self.local_permutations[source][digit];
3170            let destination = self.destinations[source];
3171            mapped = mapped.with_bit(destination, mapped_digit != 0)?;
3172            if self.statistics == ExchangeStatistics::Fermionic && occupied {
3173                odd_fermion_permutation ^=
3174                    occupied_destinations.count_ones_after(destination) % 2 == 1;
3175                occupied_destinations = occupied_destinations.with_bit(destination, true)?;
3176            }
3177        }
3178        Ok((
3179            mapped,
3180            Complex64::new(if odd_fermion_permutation { -1.0 } else { 1.0 }, 0.0),
3181        ))
3182    }
3183}
3184
3185impl SymmetryMap<ErasedState> for LatticeSymmetryMap {
3186    fn period(&self) -> usize {
3187        self.period
3188    }
3189
3190    fn apply(&self, state: ErasedState) -> Result<(ErasedState, Complex64)> {
3191        if state.width_bits != self.destinations.len() {
3192            return Err(QmbedError::StateNotInBasis);
3193        }
3194        let (value, phase) = match state.value {
3195            ErasedStateValue::U128(value) => {
3196                let (value, phase) = self.mapped_state(value)?;
3197                (ErasedStateValue::U128(value), phase)
3198            }
3199            ErasedStateValue::U256(value) => {
3200                let (value, phase) = <Self as SymmetryMap<U256>>::apply(self, value)?;
3201                (ErasedStateValue::U256(value), phase)
3202            }
3203            ErasedStateValue::U1024(value) => {
3204                let (value, phase) = <Self as SymmetryMap<U1024>>::apply(self, value)?;
3205                (ErasedStateValue::U1024(value), phase)
3206            }
3207            ErasedStateValue::U4096(value) => {
3208                let (value, phase) = <Self as SymmetryMap<U4096>>::apply(self, value)?;
3209                (ErasedStateValue::U4096(value), phase)
3210            }
3211            ErasedStateValue::U16384(value) => {
3212                let (value, phase) = <Self as SymmetryMap<U16384>>::apply(self, value)?;
3213                (ErasedStateValue::U16384(value), phase)
3214            }
3215        };
3216        Ok((
3217            ErasedState {
3218                width_bits: state.width_bits,
3219                value,
3220            },
3221            phase,
3222        ))
3223    }
3224}
3225
3226fn combined_permutation_period(
3227    destinations: &[usize],
3228    local_permutations: &[Vec<usize>],
3229    states_per_site: usize,
3230) -> Result<usize> {
3231    let elements = destinations
3232        .len()
3233        .checked_mul(states_per_site)
3234        .ok_or_else(|| {
3235            QmbedError::UnsupportedBackend("lattice-symmetry permutation is too large".into())
3236        })?;
3237    let mut visited = vec![false; elements];
3238    let mut period = 1_usize;
3239    for seed in 0..elements {
3240        if visited[seed] {
3241            continue;
3242        }
3243        let mut current = seed;
3244        let mut cycle = 0_usize;
3245        loop {
3246            if visited[current] {
3247                if current != seed {
3248                    return Err(QmbedError::IncompatibleSymmetry(
3249                        "lattice symmetry does not decompose into closed cycles".into(),
3250                    ));
3251                }
3252                break;
3253            }
3254            visited[current] = true;
3255            cycle += 1;
3256            let source = current / states_per_site;
3257            let digit = current % states_per_site;
3258            current = destinations[source] * states_per_site + local_permutations[source][digit];
3259        }
3260        period = checked_lcm(period, cycle)?;
3261    }
3262    Ok(period)
3263}
3264
3265fn checked_lcm(left: usize, right: usize) -> Result<usize> {
3266    fn gcd(mut left: usize, mut right: usize) -> usize {
3267        while right != 0 {
3268            (left, right) = (right, left % right);
3269        }
3270        left
3271    }
3272
3273    left.checked_div(gcd(left, right))
3274        .and_then(|reduced| reduced.checked_mul(right))
3275        .ok_or_else(|| QmbedError::UnsupportedBackend("symmetry period is too large".into()))
3276}
3277
3278type SymmetryAction<State> = Arc<dyn Fn(State) -> Result<(State, Complex64)> + Send + Sync>;
3279
3280/// Closure-backed finite map for lattice, particle-hole, or user symmetries.
3281pub struct ClosureSymmetryMap<State> {
3282    period: usize,
3283    action: SymmetryAction<State>,
3284}
3285
3286impl<State> ClosureSymmetryMap<State> {
3287    /// Wrap a finite-order state action as a symmetry generator.
3288    ///
3289    /// `period` is the smallest known positive power returning every state to
3290    /// itself. The callback returns the mapped state and physical
3291    /// unit-modulus phase.
3292    pub fn new<F>(period: usize, action: F) -> Result<Self>
3293    where
3294        F: Fn(State) -> Result<(State, Complex64)> + Send + Sync + 'static,
3295    {
3296        if period == 0 {
3297            return Err(QmbedError::InvalidSector(
3298                "a symmetry-map period must be positive".into(),
3299            ));
3300        }
3301        Ok(Self {
3302            period,
3303            action: Arc::new(action),
3304        })
3305    }
3306}
3307
3308impl<State> SymmetryMap<State> for ClosureSymmetryMap<State>
3309where
3310    State: Copy,
3311{
3312    fn period(&self) -> usize {
3313        self.period
3314    }
3315
3316    fn apply(&self, state: State) -> Result<(State, Complex64)> {
3317        (self.action)(state)
3318    }
3319}
3320
3321#[derive(Clone)]
3322struct SymmetryGenerator<State> {
3323    map: Arc<dyn SymmetryMap<State>>,
3324    sector: i32,
3325}
3326
3327/// Finite generators and sector phases that extend to a one-dimensional
3328/// character of the generated group.
3329#[derive(Clone)]
3330pub struct SymmetryReducer<State> {
3331    generators: Vec<SymmetryGenerator<State>>,
3332    orbit_cache: Arc<RwLock<HashMap<State, Arc<SymmetryTrace<State>>>>>,
3333}
3334
3335impl<State> std::fmt::Debug for SymmetryReducer<State> {
3336    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3337        formatter
3338            .debug_struct("SymmetryReducer")
3339            .field("generators", &self.generators.len())
3340            .field("cached_orbits", &self.cached_orbits())
3341            .finish()
3342    }
3343}
3344
3345impl<State> SymmetryReducer<State> {
3346    /// Create a reducer with no generators and an empty shared orbit cache.
3347    pub fn new() -> Self {
3348        Self {
3349            generators: Vec::new(),
3350            orbit_cache: Arc::new(RwLock::new(HashMap::new())),
3351        }
3352    }
3353
3354    /// Add a finite symmetry generator and its one-dimensional character sector.
3355    ///
3356    /// Adding a generator defines a different group action, so the returned
3357    /// reducer starts with a fresh cache. Clones made after construction share
3358    /// cached orbit traces.
3359    pub fn with_map<M>(mut self, map: M, sector: i32) -> Self
3360    where
3361        M: SymmetryMap<State> + 'static,
3362    {
3363        // A changed generator list defines a different group action.  Clones
3364        // of an unchanged reducer share their cache; derived reducers do not.
3365        self.orbit_cache = Arc::new(RwLock::new(HashMap::new()));
3366        self.generators.push(SymmetryGenerator {
3367            map: Arc::new(map),
3368            sector,
3369        });
3370        self
3371    }
3372
3373    /// Return the number of configured finite generators.
3374    pub fn generators(&self) -> usize {
3375        self.generators.len()
3376    }
3377
3378    /// Return the number of canonical orbit traces currently cached.
3379    ///
3380    /// This diagnostic is safe to call across cloned reducers. A poisoned
3381    /// cache reports zero rather than exposing lock internals.
3382    pub fn cached_orbits(&self) -> usize {
3383        self.orbit_cache.read().map_or(0, |cache| cache.len())
3384    }
3385
3386    /// Discard every cached orbit trace shared by clones of this reducer.
3387    pub fn clear_cache(&self) {
3388        if let Ok(mut cache) = self.orbit_cache.write() {
3389            cache.clear();
3390        }
3391    }
3392
3393    /// Product of declared generator periods.
3394    ///
3395    /// This is not generally the order of a non-Abelian generated group. It is
3396    /// exposed because compatibility layers such as QuSpin define their
3397    /// historical normalization convention from this product.
3398    pub fn period_product(&self) -> Result<usize> {
3399        self.generators
3400            .iter()
3401            .try_fold(1_usize, |product, generator| {
3402                product.checked_mul(generator.map.period()).ok_or_else(|| {
3403                    QmbedError::UnsupportedBackend("symmetry period product is too large".into())
3404                })
3405            })
3406    }
3407}
3408
3409impl<State> Default for SymmetryReducer<State> {
3410    fn default() -> Self {
3411        Self::new()
3412    }
3413}
3414
3415/// Result of reducing one physical state without materializing a basis.
3416#[derive(Clone, Debug, PartialEq)]
3417pub struct SymmetryOrbit<State> {
3418    representative: State,
3419    orbit_size: usize,
3420    compatible: bool,
3421    phase: Option<Complex64>,
3422    physical_phase_to_representative: Complex64,
3423    generator_word: Vec<usize>,
3424}
3425
3426impl<State> SymmetryOrbit<State> {
3427    pub const fn representative(&self) -> &State {
3428        &self.representative
3429    }
3430
3431    pub const fn orbit_size(&self) -> usize {
3432        self.orbit_size
3433    }
3434
3435    pub const fn is_compatible(&self) -> bool {
3436        self.compatible
3437    }
3438
3439    /// Unit phase of the queried physical state in the representative vector.
3440    pub const fn phase(&self) -> Option<Complex64> {
3441        self.phase
3442    }
3443
3444    /// Physical map phase acquired along the returned generator word.
3445    pub const fn physical_phase_to_representative(&self) -> Complex64 {
3446        self.physical_phase_to_representative
3447    }
3448
3449    /// Generator indices applied in order to reach the representative.
3450    pub fn generator_word(&self) -> &[usize] {
3451        &self.generator_word
3452    }
3453}
3454
3455#[derive(Clone, Copy, Debug)]
3456struct GeneralSymmetryImage<State> {
3457    representative: State,
3458    phase: Complex64,
3459    orbit_size: usize,
3460}
3461
3462#[derive(Clone)]
3463struct SymmetryTrace<State> {
3464    coefficients: HashMap<State, Complex64>,
3465    physical_phases: HashMap<State, Complex64>,
3466    predecessors: HashMap<State, (State, usize)>,
3467    compatible: bool,
3468}
3469
3470fn trace_symmetry_orbit<State>(
3471    state: State,
3472    generators: &[SymmetryGenerator<State>],
3473) -> Result<SymmetryTrace<State>>
3474where
3475    State: Copy + Eq + Hash,
3476{
3477    let mut sector_steps = Vec::with_capacity(generators.len());
3478    for generator in generators {
3479        let period = generator.map.period();
3480        if period == 0 {
3481            return Err(QmbedError::InvalidSector(
3482                "a symmetry-map period must be positive".into(),
3483            ));
3484        }
3485        let normalized_sector = i64::from(generator.sector).rem_euclid(period as i64) as usize;
3486        let angle = -std::f64::consts::TAU * normalized_sector as f64 / period as f64;
3487        sector_steps.push(Complex64::from_polar(1.0, angle));
3488
3489        let mut closure_state = state;
3490        let mut closure_phase = Complex64::new(1.0, 0.0);
3491        for _ in 0..period {
3492            let (next, phase) = generator.map.apply(closure_state)?;
3493            if !phase.re.is_finite() || !phase.im.is_finite() {
3494                return Err(QmbedError::IncompatibleSymmetry(
3495                    "a symmetry map returned a non-finite phase".into(),
3496                ));
3497            }
3498            closure_state = next;
3499            closure_phase *= phase;
3500        }
3501        if closure_state != state || (closure_phase - Complex64::new(1.0, 0.0)).norm() > 1.0e-10 {
3502            return Err(QmbedError::IncompatibleSymmetry(
3503                "a symmetry map does not close at its declared period".into(),
3504            ));
3505        }
3506    }
3507
3508    let mut coefficients = HashMap::new();
3509    coefficients.insert(state, Complex64::new(1.0, 0.0));
3510    let mut physical_phases = HashMap::new();
3511    physical_phases.insert(state, Complex64::new(1.0, 0.0));
3512    let mut predecessors = HashMap::new();
3513    let mut queue = VecDeque::from([state]);
3514    let mut compatible = true;
3515    while let Some(source) = queue.pop_front() {
3516        let source_coefficient = coefficients[&source];
3517        let source_physical_phase = physical_phases[&source];
3518        for (generator_index, (generator, &sector_step)) in
3519            generators.iter().zip(&sector_steps).enumerate()
3520        {
3521            let (target, map_phase) = generator.map.apply(source)?;
3522            if !map_phase.re.is_finite() || !map_phase.im.is_finite() {
3523                return Err(QmbedError::IncompatibleSymmetry(
3524                    "a symmetry map returned a non-finite phase".into(),
3525                ));
3526            }
3527            let target_coefficient = source_coefficient * map_phase * sector_step;
3528            if let Some(previous) = coefficients.get(&target) {
3529                if (*previous - target_coefficient).norm() > 1.0e-10 {
3530                    compatible = false;
3531                }
3532            } else {
3533                coefficients.insert(target, target_coefficient);
3534                physical_phases.insert(target, source_physical_phase * map_phase);
3535                predecessors.insert(target, (source, generator_index));
3536                queue.push_back(target);
3537            }
3538        }
3539    }
3540    Ok(SymmetryTrace {
3541        coefficients,
3542        physical_phases,
3543        predecessors,
3544        compatible,
3545    })
3546}
3547
3548impl<State> SymmetryReducer<State>
3549where
3550    State: Copy + Eq + Hash + Ord,
3551{
3552    fn trace(&self, state: State) -> Result<Arc<SymmetryTrace<State>>> {
3553        if let Some(trace) = self
3554            .orbit_cache
3555            .read()
3556            .map_err(|_| QmbedError::InternalState("symmetry orbit cache was poisoned".into()))?
3557            .get(&state)
3558            .cloned()
3559        {
3560            return Ok(trace);
3561        }
3562        let trace = Arc::new(trace_symmetry_orbit(state, &self.generators)?);
3563        self.orbit_cache
3564            .write()
3565            .map_err(|_| QmbedError::InternalState("symmetry orbit cache was poisoned".into()))?
3566            .entry(state)
3567            .or_insert_with(|| trace.clone());
3568        Ok(trace)
3569    }
3570
3571    /// Enumerate the finite physical orbit of one state in canonical order.
3572    ///
3573    /// This stays independent of any materialized basis. Deferred operator
3574    /// paths use it to test whether an output orbit intersects the seed
3575    /// constraint, including when the canonical representative itself lies
3576    /// outside that constraint.
3577    pub fn orbit_states(&self, state: State) -> Result<Vec<State>> {
3578        Ok(self.orbit_with_states(state)?.1)
3579    }
3580
3581    /// Reduce a state and return its physical orbit from one group traversal.
3582    ///
3583    /// Consumers that need both results should prefer this method over
3584    /// separate [`SymmetryReducer::orbit`] and
3585    /// [`SymmetryReducer::orbit_states`] calls.
3586    pub fn orbit_with_states_and_ordering(
3587        &self,
3588        state: State,
3589        ordering: RepresentativeOrdering,
3590    ) -> Result<(SymmetryOrbit<State>, Vec<State>)> {
3591        let trace = self.trace(state)?;
3592        let representative = *match ordering {
3593            RepresentativeOrdering::Minimum => trace.coefficients.keys().min(),
3594            RepresentativeOrdering::Maximum => trace.coefficients.keys().max(),
3595        }
3596        .ok_or_else(|| QmbedError::InternalState("a symmetry orbit contains no states".into()))?;
3597        let representative_coefficient = trace.coefficients[&representative];
3598        let phase = trace.compatible.then(|| {
3599            let gauge = representative_coefficient / representative_coefficient.norm();
3600            gauge.conj()
3601        });
3602        let mut generator_word = Vec::new();
3603        let mut cursor = representative;
3604        while cursor != state {
3605            let &(previous, generator) = trace.predecessors.get(&cursor).ok_or_else(|| {
3606                QmbedError::InternalState(
3607                    "a symmetry-orbit representative has no predecessor path".into(),
3608                )
3609            })?;
3610            generator_word.push(generator);
3611            cursor = previous;
3612        }
3613        generator_word.reverse();
3614        let mut states: Vec<_> = trace.coefficients.keys().copied().collect();
3615        states.sort_unstable();
3616        Ok((
3617            SymmetryOrbit {
3618                representative,
3619                orbit_size: trace.coefficients.len(),
3620                compatible: trace.compatible,
3621                phase,
3622                physical_phase_to_representative: trace.physical_phases[&representative],
3623                generator_word,
3624            },
3625            states,
3626        ))
3627    }
3628
3629    pub fn orbit_with_states(&self, state: State) -> Result<(SymmetryOrbit<State>, Vec<State>)> {
3630        self.orbit_with_states_and_ordering(state, RepresentativeOrdering::Minimum)
3631    }
3632
3633    /// Reduce one state using only the finite group action.
3634    ///
3635    /// This query does not enumerate a parent Hilbert space or construct the
3636    /// reduced basis. Incompatible character requests still return the
3637    /// canonical orbit representative, but have no reduced-vector phase.
3638    pub fn orbit(&self, state: State) -> Result<SymmetryOrbit<State>> {
3639        Ok(self.orbit_with_states(state)?.0)
3640    }
3641
3642    /// Reduce one state using an explicit representative convention.
3643    pub fn orbit_with_ordering(
3644        &self,
3645        state: State,
3646        ordering: RepresentativeOrdering,
3647    ) -> Result<SymmetryOrbit<State>> {
3648        Ok(self.orbit_with_states_and_ordering(state, ordering)?.0)
3649    }
3650}
3651
3652#[derive(Clone)]
3653struct MatrixSymmetryGenerator<State> {
3654    map: Arc<dyn SymmetryMap<State>>,
3655    representation: Vec<Complex64>,
3656}
3657
3658/// Finite symmetry generators carrying a common unitary matrix
3659/// representation.
3660///
3661/// Scalar characters are the one-dimensional special case handled more
3662/// efficiently by [`SymmetryReducer`]. This type covers irreducible
3663/// representations of arbitrary finite dimension and constructs one selected
3664/// representation row without assuming that the generators commute.
3665#[derive(Clone)]
3666pub struct MatrixSymmetryReducer<State> {
3667    dimension: usize,
3668    selected_row: usize,
3669    generators: Vec<MatrixSymmetryGenerator<State>>,
3670}
3671
3672impl<State> std::fmt::Debug for MatrixSymmetryReducer<State> {
3673    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3674        formatter
3675            .debug_struct("MatrixSymmetryReducer")
3676            .field("dimension", &self.dimension)
3677            .field("selected_row", &self.selected_row)
3678            .field("generators", &self.generators.len())
3679            .finish()
3680    }
3681}
3682
3683impl<State> MatrixSymmetryReducer<State> {
3684    pub fn new(dimension: usize, selected_row: usize) -> Result<Self> {
3685        if dimension == 0 || selected_row >= dimension {
3686            return Err(QmbedError::InvalidSector(
3687                "a matrix symmetry representation needs a positive dimension and valid row".into(),
3688            ));
3689        }
3690        Ok(Self {
3691            dimension,
3692            selected_row,
3693            generators: Vec::new(),
3694        })
3695    }
3696
3697    pub const fn dimension(&self) -> usize {
3698        self.dimension
3699    }
3700
3701    pub const fn selected_row(&self) -> usize {
3702        self.selected_row
3703    }
3704
3705    pub fn with_map<M>(mut self, map: M, representation: impl Into<Vec<Complex64>>) -> Result<Self>
3706    where
3707        M: SymmetryMap<State> + 'static,
3708    {
3709        let representation = representation.into();
3710        let entries = self.dimension.checked_mul(self.dimension).ok_or_else(|| {
3711            QmbedError::UnsupportedBackend("matrix symmetry representation is too large".into())
3712        })?;
3713        if representation.len() != entries {
3714            return Err(QmbedError::InvalidSector(format!(
3715                "matrix symmetry generator has {} entries, expected {entries}",
3716                representation.len()
3717            )));
3718        }
3719        validate_unitary_matrix(&representation, self.dimension)?;
3720        let mut power = identity_matrix(self.dimension);
3721        for _ in 0..map.period() {
3722            power = multiply_square_matrices(&representation, &power, self.dimension);
3723        }
3724        if !matrices_close(
3725            &power,
3726            &identity_matrix(self.dimension),
3727            MATRIX_SYMMETRY_TOLERANCE,
3728        ) {
3729            return Err(QmbedError::IncompatibleSymmetry(
3730                "a representation generator does not close at the physical map period".into(),
3731            ));
3732        }
3733        self.generators.push(MatrixSymmetryGenerator {
3734            map: Arc::new(map),
3735            representation,
3736        });
3737        Ok(self)
3738    }
3739}
3740
3741const MATRIX_SYMMETRY_TOLERANCE: f64 = 1.0e-10;
3742const MAX_MATRIX_SYMMETRY_GROUP_ORDER: usize = 65_536;
3743
3744fn identity_matrix(dimension: usize) -> Vec<Complex64> {
3745    let mut matrix = vec![Complex64::new(0.0, 0.0); dimension * dimension];
3746    for index in 0..dimension {
3747        matrix[index * dimension + index] = Complex64::new(1.0, 0.0);
3748    }
3749    matrix
3750}
3751
3752fn multiply_square_matrices(
3753    left: &[Complex64],
3754    right: &[Complex64],
3755    dimension: usize,
3756) -> Vec<Complex64> {
3757    let mut product = vec![Complex64::new(0.0, 0.0); dimension * dimension];
3758    for row in 0..dimension {
3759        for middle in 0..dimension {
3760            let coefficient = left[row * dimension + middle];
3761            if coefficient.norm() <= f64::EPSILON {
3762                continue;
3763            }
3764            for column in 0..dimension {
3765                product[row * dimension + column] +=
3766                    coefficient * right[middle * dimension + column];
3767            }
3768        }
3769    }
3770    product
3771}
3772
3773fn matrices_close(left: &[Complex64], right: &[Complex64], tolerance: f64) -> bool {
3774    left.len() == right.len()
3775        && left
3776            .iter()
3777            .zip(right)
3778            .all(|(left, right)| (*left - *right).norm() <= tolerance)
3779}
3780
3781fn validate_unitary_matrix(matrix: &[Complex64], dimension: usize) -> Result<()> {
3782    if matrix
3783        .iter()
3784        .any(|value| !value.re.is_finite() || !value.im.is_finite())
3785    {
3786        return Err(QmbedError::InvalidSector(
3787            "matrix symmetry generators must contain finite values".into(),
3788        ));
3789    }
3790    for left in 0..dimension {
3791        for right in 0..dimension {
3792            let overlap = (0..dimension).fold(Complex64::new(0.0, 0.0), |sum, row| {
3793                sum + matrix[row * dimension + left].conj() * matrix[row * dimension + right]
3794            });
3795            let expected = if left == right {
3796                Complex64::new(1.0, 0.0)
3797            } else {
3798                Complex64::new(0.0, 0.0)
3799            };
3800            if (overlap - expected).norm() > MATRIX_SYMMETRY_TOLERANCE {
3801                return Err(QmbedError::InvalidSector(
3802                    "matrix symmetry generators must be unitary".into(),
3803                ));
3804            }
3805        }
3806    }
3807    Ok(())
3808}
3809
3810#[derive(Clone)]
3811struct OrbitGroupElement {
3812    destinations: Vec<usize>,
3813    phases: Vec<Complex64>,
3814    representation: Vec<Complex64>,
3815}
3816
3817type OrbitColumn<State> = (State, Vec<(State, Complex64)>);
3818
3819fn orbit_group_elements_close(left: &OrbitGroupElement, right: &OrbitGroupElement) -> bool {
3820    left.destinations == right.destinations
3821        && matrices_close(&left.phases, &right.phases, MATRIX_SYMMETRY_TOLERANCE)
3822        && matrices_close(
3823            &left.representation,
3824            &right.representation,
3825            MATRIX_SYMMETRY_TOLERANCE,
3826        )
3827}
3828
3829fn compose_orbit_group_elements(
3830    left: &OrbitGroupElement,
3831    right: &OrbitGroupElement,
3832    representation_dimension: usize,
3833) -> OrbitGroupElement {
3834    let mut destinations = Vec::with_capacity(right.destinations.len());
3835    let mut phases = Vec::with_capacity(right.phases.len());
3836    for source in 0..right.destinations.len() {
3837        let middle = right.destinations[source];
3838        destinations.push(left.destinations[middle]);
3839        phases.push(right.phases[source] * left.phases[middle]);
3840    }
3841    OrbitGroupElement {
3842        destinations,
3843        phases,
3844        representation: multiply_square_matrices(
3845            &left.representation,
3846            &right.representation,
3847            representation_dimension,
3848        ),
3849    }
3850}
3851
3852/// Orthonormal columns spanning one selected row of a finite-group matrix
3853/// representation.
3854#[derive(Clone, Debug)]
3855pub struct MatrixSymmetrySubspace<State> {
3856    physical_states: Vec<State>,
3857    labels: Vec<State>,
3858    columns: Vec<Vec<(State, Complex64)>>,
3859}
3860
3861impl<State> MatrixSymmetrySubspace<State>
3862where
3863    State: Copy + Eq + Hash + Ord,
3864{
3865    pub fn dimension(&self) -> usize {
3866        self.columns.len()
3867    }
3868
3869    pub fn physical_dimension(&self) -> usize {
3870        self.physical_states.len()
3871    }
3872
3873    pub fn physical_states(&self) -> &[State] {
3874        &self.physical_states
3875    }
3876
3877    /// Deterministic physical seed label attached to each orthonormal column.
3878    pub fn labels(&self) -> &[State] {
3879        &self.labels
3880    }
3881
3882    pub fn columns(&self) -> &[Vec<(State, Complex64)>] {
3883        &self.columns
3884    }
3885
3886    /// Embed the selected representation row into an explicit parent basis.
3887    pub fn projector<Parent>(&self, parent: &Parent, format: MatrixFormat) -> Result<Operator>
3888    where
3889        Parent: Basis<State = State>,
3890    {
3891        Operator::from_triplets(
3892            parent.len(),
3893            self.columns.len(),
3894            self.columns
3895                .iter()
3896                .enumerate()
3897                .flat_map(|(column, entries)| {
3898                    entries
3899                        .iter()
3900                        .map(move |&(state, value)| (state, column, value))
3901                })
3902                .map(|(state, column, value)| Ok((parent.index(state)?, column, value)))
3903                .collect::<Result<Vec<_>>>()?,
3904            format,
3905        )
3906    }
3907}
3908
3909impl<State> MatrixSymmetryReducer<State>
3910where
3911    State: Copy + Eq + Hash + Ord,
3912{
3913    fn physical_orbit(&self, seed: State) -> Result<Vec<State>> {
3914        let mut visited = HashSet::from([seed]);
3915        let mut queue = VecDeque::from([seed]);
3916        while let Some(source) = queue.pop_front() {
3917            for generator in &self.generators {
3918                let (target, phase) = generator.map.apply(source)?;
3919                if !phase.re.is_finite() || !phase.im.is_finite() {
3920                    return Err(QmbedError::IncompatibleSymmetry(
3921                        "a symmetry map returned a non-finite phase".into(),
3922                    ));
3923                }
3924                if visited.insert(target) {
3925                    queue.push_back(target);
3926                }
3927            }
3928        }
3929        let mut orbit: Vec<_> = visited.into_iter().collect();
3930        orbit.sort_unstable();
3931        Ok(orbit)
3932    }
3933
3934    fn orbit_group(&self, orbit: &[State]) -> Result<Vec<OrbitGroupElement>> {
3935        let indices: HashMap<_, _> = orbit
3936            .iter()
3937            .copied()
3938            .enumerate()
3939            .map(|(index, state)| (state, index))
3940            .collect();
3941        let mut generators = Vec::with_capacity(self.generators.len());
3942        for generator in &self.generators {
3943            let mut destinations = Vec::with_capacity(orbit.len());
3944            let mut phases = Vec::with_capacity(orbit.len());
3945            for &state in orbit {
3946                let (target, phase) = generator.map.apply(state)?;
3947                let target = indices.get(&target).copied().ok_or_else(|| {
3948                    QmbedError::IncompatibleSymmetry(
3949                        "a matrix symmetry generator leaves its physical orbit".into(),
3950                    )
3951                })?;
3952                destinations.push(target);
3953                phases.push(phase);
3954            }
3955            generators.push(OrbitGroupElement {
3956                destinations,
3957                phases,
3958                representation: generator.representation.clone(),
3959            });
3960        }
3961
3962        let identity = OrbitGroupElement {
3963            destinations: (0..orbit.len()).collect(),
3964            phases: vec![Complex64::new(1.0, 0.0); orbit.len()],
3965            representation: identity_matrix(self.dimension),
3966        };
3967        let mut group = vec![identity];
3968        let mut cursor = 0;
3969        while cursor < group.len() {
3970            let current = group[cursor].clone();
3971            cursor += 1;
3972            for generator in &generators {
3973                let candidate = compose_orbit_group_elements(generator, &current, self.dimension);
3974                if group
3975                    .iter()
3976                    .any(|element| orbit_group_elements_close(element, &candidate))
3977                {
3978                    continue;
3979                }
3980                if group.len() >= MAX_MATRIX_SYMMETRY_GROUP_ORDER {
3981                    return Err(QmbedError::UnsupportedBackend(
3982                        "matrix symmetry group exceeds the finite closure limit".into(),
3983                    ));
3984                }
3985                group.push(candidate);
3986            }
3987        }
3988        Ok(group)
3989    }
3990
3991    fn selected_orbit_columns(&self, orbit: &[State]) -> Result<Vec<OrbitColumn<State>>> {
3992        let group = self.orbit_group(orbit)?;
3993        let scale = self.dimension as f64 / group.len() as f64;
3994        let mut projector = vec![vec![Complex64::new(0.0, 0.0); orbit.len()]; orbit.len()];
3995        let diagonal = self.selected_row * self.dimension + self.selected_row;
3996        for element in &group {
3997            let weight = scale * element.representation[diagonal].conj();
3998            for source in 0..orbit.len() {
3999                projector[element.destinations[source]][source] += weight * element.phases[source];
4000            }
4001        }
4002
4003        let mut orthonormal = Vec::<(State, Vec<Complex64>)>::new();
4004        for source in 0..orbit.len() {
4005            let mut vector: Vec<_> = projector.iter().map(|row| row[source]).collect();
4006            // A second modified-Gram-Schmidt pass keeps nearly dependent
4007            // projector columns stable on short symmetry orbits.
4008            for _ in 0..2 {
4009                for (_, previous) in &orthonormal {
4010                    let overlap = previous
4011                        .iter()
4012                        .zip(&vector)
4013                        .fold(Complex64::new(0.0, 0.0), |sum, (left, right)| {
4014                            sum + left.conj() * right
4015                        });
4016                    for (value, basis_value) in vector.iter_mut().zip(previous) {
4017                        *value -= overlap * basis_value;
4018                    }
4019                }
4020            }
4021            let norm = vector.iter().map(Complex64::norm_sqr).sum::<f64>().sqrt();
4022            if norm <= MATRIX_SYMMETRY_TOLERANCE {
4023                continue;
4024            }
4025            vector.iter_mut().for_each(|value| *value /= norm);
4026            if let Some(pivot) = vector
4027                .iter()
4028                .copied()
4029                .find(|value| value.norm() > MATRIX_SYMMETRY_TOLERANCE)
4030            {
4031                let gauge = pivot / pivot.norm();
4032                vector.iter_mut().for_each(|value| *value *= gauge.conj());
4033            }
4034            orthonormal.push((orbit[source], vector));
4035        }
4036
4037        Ok(orthonormal
4038            .into_iter()
4039            .map(|(label, vector)| {
4040                (
4041                    label,
4042                    orbit
4043                        .iter()
4044                        .copied()
4045                        .zip(vector)
4046                        .filter(|(_, value)| value.norm() > MATRIX_SYMMETRY_TOLERANCE)
4047                        .collect(),
4048                )
4049            })
4050            .collect())
4051    }
4052
4053    /// Build the selected representation row from constrained seed states.
4054    ///
4055    /// Each seed orbit is completed before projection, so generators may
4056    /// exchange additive sectors without forcing an unrestricted seed
4057    /// enumeration.
4058    pub fn subspace<Seed>(&self, seeds: &Seed) -> Result<MatrixSymmetrySubspace<State>>
4059    where
4060        Seed: Basis<State = State>,
4061    {
4062        if self.generators.is_empty() {
4063            return Err(QmbedError::InvalidSector(
4064                "a matrix symmetry representation requires at least one generator".into(),
4065            ));
4066        }
4067        let mut visited = HashSet::new();
4068        let mut physical_states = Vec::new();
4069        let mut labels = Vec::new();
4070        let mut columns = Vec::new();
4071        for index in 0..seeds.len() {
4072            let seed = seeds.state(index)?;
4073            if visited.contains(&seed) {
4074                continue;
4075            }
4076            let orbit = self.physical_orbit(seed)?;
4077            visited.extend(orbit.iter().copied());
4078            physical_states.extend(orbit.iter().copied());
4079            for (label, column) in self.selected_orbit_columns(&orbit)? {
4080                labels.push(label);
4081                columns.push(column);
4082            }
4083        }
4084        physical_states.sort_unstable();
4085        physical_states.dedup();
4086        Ok(MatrixSymmetrySubspace {
4087            physical_states,
4088            labels,
4089            columns,
4090        })
4091    }
4092}
4093
4094/// Arbitrary finite-map reduction of any concrete parent basis.
4095#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
4096pub enum RepresentativeOrdering {
4097    #[default]
4098    Minimum,
4099    Maximum,
4100}
4101
4102#[derive(Clone, Debug)]
4103pub struct GeneralBasis<Parent>
4104where
4105    Parent: Basis,
4106    Parent::State: Hash + Ord,
4107{
4108    parent: Parent,
4109    reducer: SymmetryReducer<Parent::State>,
4110    states: Vec<Parent::State>,
4111    indices: HashMap<Parent::State, usize>,
4112    orbit_lengths: Vec<usize>,
4113    lookup: HashMap<Parent::State, GeneralSymmetryImage<Parent::State>>,
4114}
4115
4116impl<Parent> GeneralBasis<Parent>
4117where
4118    Parent: Basis,
4119    Parent::State: Hash + Ord,
4120{
4121    pub fn new(parent: Parent, sector: SymmetryReducer<Parent::State>) -> Result<Self> {
4122        Self::from_reducer(parent, sector)
4123    }
4124
4125    /// Materialize a reduced basis from an independently reusable reducer.
4126    pub fn from_reducer(parent: Parent, reducer: SymmetryReducer<Parent::State>) -> Result<Self> {
4127        Self::from_reducer_with_ordering(parent, reducer, RepresentativeOrdering::Minimum)
4128    }
4129
4130    /// Materialize a reduced basis with an explicit canonical representative
4131    /// convention. The native default is the minimum state; compatibility
4132    /// frontends with descending basis order may select the maximum state
4133    /// without changing the physical symmetry subspace.
4134    pub fn from_reducer_with_ordering(
4135        parent: Parent,
4136        reducer: SymmetryReducer<Parent::State>,
4137        ordering: RepresentativeOrdering,
4138    ) -> Result<Self> {
4139        Self::from_reducer_impl(parent, reducer, true, ordering)
4140    }
4141
4142    /// Materialize symmetry vectors whose seed states come from `parent`.
4143    ///
4144    /// Unlike [`GeneralBasis::from_reducer`], the finite symmetry orbit may
4145    /// contain states outside the parent's enumerated constraint. The parent
4146    /// still supplies the local operator algebra, while its state list selects
4147    /// which physical orbits enter the reduced space. This is the natural
4148    /// construction for particle-hole or species-exchange symmetries acting
4149    /// on a fixed additive sector: one seed sector selects an orbit, and the
4150    /// normalized reduced vector contains the complete symmetry orbit.
4151    ///
4152    /// The parent's local action must therefore be defined for every state
4153    /// generated by the reducer. Built-in packed bases and callback bases
4154    /// satisfy this contract because local actions operate on the encoded
4155    /// state rather than its row index.
4156    pub fn from_orbit_seeds(
4157        parent: Parent,
4158        reducer: SymmetryReducer<Parent::State>,
4159    ) -> Result<Self> {
4160        Self::from_orbit_seeds_with_ordering(parent, reducer, RepresentativeOrdering::Minimum)
4161    }
4162
4163    pub fn from_orbit_seeds_with_ordering(
4164        parent: Parent,
4165        reducer: SymmetryReducer<Parent::State>,
4166        ordering: RepresentativeOrdering,
4167    ) -> Result<Self> {
4168        Self::from_reducer_impl(parent, reducer, false, ordering)
4169    }
4170
4171    fn from_reducer_impl(
4172        parent: Parent,
4173        reducer: SymmetryReducer<Parent::State>,
4174        require_parent_membership: bool,
4175        ordering: RepresentativeOrdering,
4176    ) -> Result<Self> {
4177        if reducer.generators.is_empty() {
4178            let mut states = Vec::with_capacity(parent.len());
4179            let mut lookup = HashMap::with_capacity(parent.len());
4180            for index in 0..parent.len() {
4181                let state = parent.state(index)?;
4182                states.push(state);
4183                lookup.insert(
4184                    state,
4185                    GeneralSymmetryImage {
4186                        representative: state,
4187                        phase: Complex64::new(1.0, 0.0),
4188                        orbit_size: 1,
4189                    },
4190                );
4191            }
4192            let indices = states
4193                .iter()
4194                .copied()
4195                .enumerate()
4196                .map(|(index, state)| (state, index))
4197                .collect();
4198            return Ok(Self {
4199                parent,
4200                reducer,
4201                orbit_lengths: vec![1; states.len()],
4202                states,
4203                indices,
4204                lookup,
4205            });
4206        }
4207
4208        let mut visited = HashSet::with_capacity(parent.len());
4209        let mut representatives = Vec::new();
4210        let mut lookup = HashMap::with_capacity(parent.len());
4211        for index in 0..parent.len() {
4212            let seed = parent.state(index)?;
4213            if visited.contains(&seed) {
4214                continue;
4215            }
4216            let trace = reducer.trace(seed)?;
4217            for state in trace.coefficients.keys() {
4218                if require_parent_membership {
4219                    parent.index(*state).map_err(|_| {
4220                        QmbedError::IncompatibleSymmetry(
4221                            "a symmetry map leaves the parent basis".into(),
4222                        )
4223                    })?;
4224                }
4225                visited.insert(*state);
4226            }
4227            if !trace.compatible {
4228                continue;
4229            }
4230            let representative = *match ordering {
4231                RepresentativeOrdering::Minimum => trace.coefficients.keys().min(),
4232                RepresentativeOrdering::Maximum => trace.coefficients.keys().max(),
4233            }
4234            .ok_or_else(|| {
4235                QmbedError::InvalidSector("symmetry projection generated no state".into())
4236            })?;
4237            let representative_coefficient = trace.coefficients[&representative];
4238            let gauge = representative_coefficient / representative_coefficient.norm();
4239            let norm = trace
4240                .coefficients
4241                .values()
4242                .map(Complex64::norm_sqr)
4243                .sum::<f64>()
4244                .sqrt();
4245            let orbit_size = trace.coefficients.len();
4246            let expected_magnitude = 1.0 / (orbit_size as f64).sqrt();
4247            for (&state, &coefficient) in &trace.coefficients {
4248                let normalized = coefficient / (gauge * norm);
4249                if (normalized.norm() - expected_magnitude).abs() > 1.0e-10 {
4250                    return Err(QmbedError::IncompatibleSymmetry(
4251                        "symmetry maps do not define a one-dimensional orbit sector".into(),
4252                    ));
4253                }
4254                lookup.insert(
4255                    state,
4256                    GeneralSymmetryImage {
4257                        representative,
4258                        phase: normalized / expected_magnitude,
4259                        orbit_size,
4260                    },
4261                );
4262            }
4263            representatives.push((representative, orbit_size));
4264        }
4265        // A basis row is an ordered coordinate, not merely a packed integer.
4266        // Preserve the parent basis' public row convention whenever every
4267        // selected representative belongs to it. This matters for composite
4268        // encodings such as spinful fermions, whose native integer layout is
4269        // intentionally different from their `up ⊗ down` row order.
4270        let parent_rows = representatives
4271            .iter()
4272            .map(|(state, _)| parent.index(*state).ok())
4273            .collect::<Vec<_>>();
4274        if parent_rows.iter().all(Option::is_some) {
4275            let mut with_rows = representatives
4276                .into_iter()
4277                .zip(parent_rows)
4278                .map(|((state, orbit_size), row)| (row.unwrap(), state, orbit_size))
4279                .collect::<Vec<_>>();
4280            with_rows.sort_by_key(|(row, _, _)| *row);
4281            representatives = with_rows
4282                .into_iter()
4283                .map(|(_, state, orbit_size)| (state, orbit_size))
4284                .collect();
4285        } else {
4286            // Orbit-seed reductions may deliberately choose a representative
4287            // outside the enumerated parent sector. There is then no parent
4288            // row to inherit, so retain the state type's canonical ordering.
4289            representatives.sort_by_key(|(state, _)| *state);
4290        }
4291        let (states, orbit_lengths): (Vec<Parent::State>, Vec<usize>) =
4292            representatives.into_iter().unzip();
4293        let indices = states
4294            .iter()
4295            .copied()
4296            .enumerate()
4297            .map(|(index, state)| (state, index))
4298            .collect();
4299        Ok(Self {
4300            parent,
4301            reducer,
4302            states,
4303            indices,
4304            orbit_lengths,
4305            lookup,
4306        })
4307    }
4308
4309    pub fn parent(&self) -> &Parent {
4310        &self.parent
4311    }
4312
4313    pub const fn reducer(&self) -> &SymmetryReducer<Parent::State> {
4314        &self.reducer
4315    }
4316
4317    pub fn representative(&self, state: Parent::State) -> Result<Parent::State> {
4318        self.lookup
4319            .get(&state)
4320            .map(|image| image.representative)
4321            .ok_or(QmbedError::StateNotInBasis)
4322    }
4323
4324    pub fn orbit_size(&self, state: Parent::State) -> Result<usize> {
4325        self.lookup
4326            .get(&state)
4327            .map(|image| image.orbit_size)
4328            .ok_or(QmbedError::StateNotInBasis)
4329    }
4330
4331    /// Normalized coefficient of a parent state in its reduced representative.
4332    pub fn symmetry_amplitude(&self, state: Parent::State) -> Result<Complex64> {
4333        self.lookup
4334            .get(&state)
4335            .map(|image| image.phase / (image.orbit_size as f64).sqrt())
4336            .ok_or(QmbedError::StateNotInBasis)
4337    }
4338}
4339
4340impl<Parent> Basis for GeneralBasis<Parent>
4341where
4342    Parent: Basis,
4343    Parent::State: Hash + Ord + 'static,
4344{
4345    type State = Parent::State;
4346
4347    fn len(&self) -> usize {
4348        self.states.len()
4349    }
4350
4351    fn state(&self, index: usize) -> Result<Self::State> {
4352        self.states
4353            .get(index)
4354            .copied()
4355            .ok_or(QmbedError::StateNotInBasis)
4356    }
4357
4358    fn index(&self, state: Self::State) -> Result<usize> {
4359        self.indices
4360            .get(&state)
4361            .copied()
4362            .ok_or(QmbedError::StateNotInBasis)
4363    }
4364
4365    fn apply_local(
4366        &self,
4367        state: Self::State,
4368        operator: &str,
4369        sites: &[usize],
4370    ) -> Result<Option<(Self::State, Complex64)>> {
4371        let transitions = self.apply_local_transitions(state, operator, sites)?;
4372        match transitions.as_slice() {
4373            [] => Ok(None),
4374            [transition] => Ok(Some(*transition)),
4375            _ => Err(QmbedError::UnsupportedBackend(
4376                "this reduced local action branches; use apply_local_transitions".into(),
4377            )),
4378        }
4379    }
4380
4381    fn apply_local_transitions(
4382        &self,
4383        state: Self::State,
4384        operator: &str,
4385        sites: &[usize],
4386    ) -> Result<LocalTransitions<Self::State>> {
4387        let source_index = self.index(state)?;
4388        let source_orbit = self.orbit_lengths[source_index];
4389        let mut reduced = HashMap::<Self::State, Complex64>::new();
4390        for (target, mut amplitude) in self
4391            .parent
4392            .apply_local_transitions(state, operator, sites)?
4393        {
4394            let Some(image) = self.lookup.get(&target) else {
4395                continue;
4396            };
4397            amplitude *=
4398                (source_orbit as f64 / image.orbit_size as f64).sqrt() * image.phase.conj();
4399            *reduced
4400                .entry(image.representative)
4401                .or_insert(Complex64::new(0.0, 0.0)) += amplitude;
4402        }
4403        let mut transitions: LocalTransitions<_> = reduced
4404            .into_iter()
4405            .filter(|(_, amplitude)| amplitude.norm() > f64::EPSILON)
4406            .collect();
4407        transitions.sort_by_key(|(state, _)| *state);
4408        Ok(transitions)
4409    }
4410
4411    fn apply_local_unreduced_transitions(
4412        &self,
4413        state: Self::State,
4414        operator: &str,
4415        sites: &[usize],
4416    ) -> Result<LocalTransitions<Self::State>> {
4417        self.parent
4418            .apply_local_unreduced_transitions(state, operator, sites)
4419    }
4420
4421    fn visit_local_unreduced_transitions<F>(
4422        &self,
4423        state: Self::State,
4424        operator: &str,
4425        sites: &[usize],
4426        visit: F,
4427    ) -> Result<()>
4428    where
4429        F: FnMut(Self::State, Complex64) -> Result<()>,
4430    {
4431        self.parent
4432            .visit_local_unreduced_transitions(state, operator, sites, visit)
4433    }
4434
4435    fn visit_preparsed_local_unreduced_transitions<F>(
4436        &self,
4437        state: Self::State,
4438        operator: &str,
4439        symbols: &[char],
4440        split: Option<usize>,
4441        sites: &[usize],
4442        visit: F,
4443    ) -> Result<()>
4444    where
4445        F: FnMut(Self::State, Complex64) -> Result<()>,
4446    {
4447        self.parent.visit_preparsed_local_unreduced_transitions(
4448            state, operator, symbols, split, sites, visit,
4449        )
4450    }
4451
4452    fn transition_orbit_size(&self, state: Self::State) -> Result<usize> {
4453        Ok(self.orbit_lengths[self.index(state)?])
4454    }
4455
4456    fn reduction_image(&self, state: Self::State) -> Result<Option<ReductionImage<Self::State>>> {
4457        let Some(image) = self.lookup.get(&state) else {
4458            return Ok(None);
4459        };
4460        Ok(Some(ReductionImage::new(
4461            image.representative,
4462            image.phase,
4463            image.orbit_size,
4464        )?))
4465    }
4466
4467    fn reduce_transition(
4468        &self,
4469        state: Self::State,
4470        source_orbit_size: usize,
4471    ) -> Result<Option<(Self::State, Complex64)>> {
4472        let Some(image) = self.lookup.get(&state) else {
4473            return Ok(None);
4474        };
4475        Ok(Some((
4476            image.representative,
4477            (source_orbit_size as f64 / image.orbit_size as f64).sqrt() * image.phase.conj(),
4478        )))
4479    }
4480
4481    fn index_transition(
4482        &self,
4483        state: Self::State,
4484        source_orbit_size: usize,
4485    ) -> Result<Option<(usize, Complex64)>> {
4486        let Some(image) = self.lookup.get(&state) else {
4487            return Ok(None);
4488        };
4489        Ok(Some((
4490            self.index(image.representative)?,
4491            (source_orbit_size as f64 / image.orbit_size as f64).sqrt() * image.phase.conj(),
4492        )))
4493    }
4494
4495    fn operator_preserves_particle_sector(&self, operator: &str) -> Result<bool> {
4496        self.parent.operator_preserves_particle_sector(operator)
4497    }
4498
4499    fn operator_preserves_particle_sector_on_sites(
4500        &self,
4501        operator: &str,
4502        sites: &[usize],
4503    ) -> Result<bool> {
4504        self.parent
4505            .operator_preserves_particle_sector_on_sites(operator, sites)
4506    }
4507}
4508
4509pub type SpinBasisGeneral = GeneralBasis<SpinBasis1D>;
4510pub type BosonBasisGeneral = GeneralBasis<BosonBasis1D>;
4511pub type SpinlessFermionBasisGeneral = GeneralBasis<SpinlessFermionBasis1D>;
4512pub type SpinfulFermionBasisGeneral = GeneralBasis<SpinfulFermionBasis1D>;
4513pub type UserBasisGeneral = GeneralBasis<UserBasis<u128>>;
4514
4515/// Direct-product basis. Operator strings use `left|right` factor syntax.
4516#[derive(Clone, Debug)]
4517pub struct TensorBasis<Left, Right> {
4518    left: Left,
4519    right: Right,
4520}
4521
4522impl<Left, Right> TensorBasis<Left, Right>
4523where
4524    Left: Basis,
4525    Right: Basis,
4526{
4527    pub fn new(left: Left, right: Right) -> Result<Self> {
4528        left.len()
4529            .checked_mul(right.len())
4530            .ok_or_else(|| QmbedError::UnsupportedBackend("tensor-basis size overflow".into()))?;
4531        Ok(Self { left, right })
4532    }
4533
4534    pub fn left(&self) -> &Left {
4535        &self.left
4536    }
4537
4538    pub fn right(&self) -> &Right {
4539        &self.right
4540    }
4541}
4542
4543impl<Left, Right> Basis for TensorBasis<Left, Right>
4544where
4545    Left: Basis,
4546    Right: Basis,
4547    Left::State: 'static,
4548    Right::State: 'static,
4549{
4550    type State = (Left::State, Right::State);
4551
4552    fn len(&self) -> usize {
4553        self.left.len() * self.right.len()
4554    }
4555
4556    fn state(&self, index: usize) -> Result<Self::State> {
4557        if index >= self.len() {
4558            return Err(QmbedError::StateNotInBasis);
4559        }
4560        Ok((
4561            self.left.state(index / self.right.len())?,
4562            self.right.state(index % self.right.len())?,
4563        ))
4564    }
4565
4566    fn index(&self, state: Self::State) -> Result<usize> {
4567        Ok(self.left.index(state.0)? * self.right.len() + self.right.index(state.1)?)
4568    }
4569
4570    fn apply_local(
4571        &self,
4572        state: Self::State,
4573        operator: &str,
4574        sites: &[usize],
4575    ) -> Result<Option<(Self::State, Complex64)>> {
4576        let transitions = self.apply_local_transitions(state, operator, sites)?;
4577        match transitions.as_slice() {
4578            [] => Ok(None),
4579            [transition] => Ok(Some(*transition)),
4580            _ => Err(QmbedError::UnsupportedBackend(
4581                "this tensor local action branches; use apply_local_transitions".into(),
4582            )),
4583        }
4584    }
4585
4586    fn apply_local_transitions(
4587        &self,
4588        state: Self::State,
4589        operator: &str,
4590        sites: &[usize],
4591    ) -> Result<LocalTransitions<Self::State>> {
4592        let (left_operator, right_operator) = operator.split_once('|').ok_or_else(|| {
4593            QmbedError::InvalidOperator(
4594                "tensor-basis operator strings must contain one `|` separator".into(),
4595            )
4596        })?;
4597        if right_operator.contains('|') {
4598            return Err(QmbedError::InvalidOperator(
4599                "a two-factor tensor operator contains too many separators".into(),
4600            ));
4601        }
4602        let left_arity = left_operator.chars().count();
4603        let right_arity = right_operator.chars().count();
4604        if sites.len() != left_arity + right_arity {
4605            return Err(QmbedError::InvalidCoupling(
4606                "tensor operator arity does not match its sites".into(),
4607            ));
4608        }
4609        let left_transitions = if left_operator.is_empty() {
4610            LocalTransitions::from_iter([(state.0, Complex64::new(1.0, 0.0))])
4611        } else {
4612            self.left
4613                .apply_local_transitions(state.0, left_operator, &sites[..left_arity])?
4614        };
4615        let right_transitions = if right_operator.is_empty() {
4616            LocalTransitions::from_iter([(state.1, Complex64::new(1.0, 0.0))])
4617        } else {
4618            self.right
4619                .apply_local_transitions(state.1, right_operator, &sites[left_arity..])?
4620        };
4621        let mut transitions = LocalTransitions::new();
4622        for &(left_state, left_amplitude) in &left_transitions {
4623            for &(right_state, right_amplitude) in &right_transitions {
4624                transitions.push(((left_state, right_state), left_amplitude * right_amplitude));
4625            }
4626        }
4627        Ok(transitions)
4628    }
4629
4630    fn visit_local_unreduced_transitions<F>(
4631        &self,
4632        state: Self::State,
4633        operator: &str,
4634        sites: &[usize],
4635        mut visit: F,
4636    ) -> Result<()>
4637    where
4638        F: FnMut(Self::State, Complex64) -> Result<()>,
4639    {
4640        for (target, amplitude) in self.apply_local_transitions(state, operator, sites)? {
4641            visit(target, amplitude)?;
4642        }
4643        Ok(())
4644    }
4645
4646    fn operator_preserves_particle_sector(&self, operator: &str) -> Result<bool> {
4647        let (left_operator, right_operator) = operator
4648            .split_once('|')
4649            .ok_or_else(|| QmbedError::InvalidOperator(operator.into()))?;
4650        if right_operator.contains('|') {
4651            return Err(QmbedError::InvalidOperator(operator.into()));
4652        }
4653        Ok(self
4654            .left
4655            .operator_preserves_particle_sector(left_operator)?
4656            && self
4657                .right
4658                .operator_preserves_particle_sector(right_operator)?)
4659    }
4660}
4661
4662/// Matter basis tensored with one truncated photon mode, optionally at fixed
4663/// total excitation number.
4664pub struct PhotonBasis<Matter>
4665where
4666    Matter: Basis,
4667    Matter::State: Hash,
4668{
4669    tensor: TensorBasis<Matter, BosonBasis1D>,
4670    states: Vec<(Matter::State, u128)>,
4671    indices: HashMap<(Matter::State, u128), usize>,
4672    total_excitations: Option<usize>,
4673}
4674
4675impl<Matter> PhotonBasis<Matter>
4676where
4677    Matter: Basis,
4678    Matter::State: Hash + 'static,
4679{
4680    pub fn new(matter: Matter, photon: BosonBasis1D) -> Result<Self> {
4681        Self::build(matter, photon, None, |_| 0)
4682    }
4683
4684    pub fn fixed_total_excitations<F>(
4685        matter: Matter,
4686        photon: BosonBasis1D,
4687        total: usize,
4688        matter_excitations: F,
4689    ) -> Result<Self>
4690    where
4691        F: Fn(Matter::State) -> usize,
4692    {
4693        Self::build(matter, photon, Some(total), matter_excitations)
4694    }
4695
4696    fn build<F>(
4697        matter: Matter,
4698        photon: BosonBasis1D,
4699        total_excitations: Option<usize>,
4700        matter_excitations: F,
4701    ) -> Result<Self>
4702    where
4703        F: Fn(Matter::State) -> usize,
4704    {
4705        if photon.sites() != 1 {
4706            return Err(QmbedError::InvalidSector(
4707                "PhotonBasis requires a one-mode boson basis".into(),
4708            ));
4709        }
4710        let tensor = TensorBasis::new(matter, photon)?;
4711        let mut states = Vec::new();
4712        for index in 0..tensor.len() {
4713            let state = tensor.state(index)?;
4714            if total_excitations
4715                .is_none_or(|total| matter_excitations(state.0) + state.1 as usize == total)
4716            {
4717                states.push(state);
4718            }
4719        }
4720        if states.is_empty() {
4721            return Err(QmbedError::InvalidSector(
4722                "the requested photon sector is empty".into(),
4723            ));
4724        }
4725        let indices = states
4726            .iter()
4727            .copied()
4728            .enumerate()
4729            .map(|(index, state)| (state, index))
4730            .collect();
4731        Ok(Self {
4732            tensor,
4733            states,
4734            indices,
4735            total_excitations,
4736        })
4737    }
4738
4739    pub const fn total_excitations(&self) -> Option<usize> {
4740        self.total_excitations
4741    }
4742
4743    pub fn matter(&self) -> &Matter {
4744        self.tensor.left()
4745    }
4746
4747    pub fn photon(&self) -> &BosonBasis1D {
4748        self.tensor.right()
4749    }
4750}
4751
4752impl<Matter> Basis for PhotonBasis<Matter>
4753where
4754    Matter: Basis,
4755    Matter::State: Hash + 'static,
4756{
4757    type State = (Matter::State, u128);
4758
4759    fn len(&self) -> usize {
4760        self.states.len()
4761    }
4762
4763    fn state(&self, index: usize) -> Result<Self::State> {
4764        self.states
4765            .get(index)
4766            .copied()
4767            .ok_or(QmbedError::StateNotInBasis)
4768    }
4769
4770    fn index(&self, state: Self::State) -> Result<usize> {
4771        self.indices
4772            .get(&state)
4773            .copied()
4774            .ok_or(QmbedError::StateNotInBasis)
4775    }
4776
4777    fn apply_local(
4778        &self,
4779        state: Self::State,
4780        operator: &str,
4781        sites: &[usize],
4782    ) -> Result<Option<(Self::State, Complex64)>> {
4783        let transitions = self.apply_local_transitions(state, operator, sites)?;
4784        match transitions.as_slice() {
4785            [] => Ok(None),
4786            [transition] => Ok(Some(*transition)),
4787            _ => Err(QmbedError::UnsupportedBackend(
4788                "this photon-basis action branches; use apply_local_transitions".into(),
4789            )),
4790        }
4791    }
4792
4793    fn apply_local_transitions(
4794        &self,
4795        state: Self::State,
4796        operator: &str,
4797        sites: &[usize],
4798    ) -> Result<LocalTransitions<Self::State>> {
4799        Ok(self
4800            .tensor
4801            .apply_local_transitions(state, operator, sites)?
4802            .into_iter()
4803            .filter(|(target, _)| self.indices.contains_key(target))
4804            .collect())
4805    }
4806
4807    fn visit_local_unreduced_transitions<F>(
4808        &self,
4809        state: Self::State,
4810        operator: &str,
4811        sites: &[usize],
4812        mut visit: F,
4813    ) -> Result<()>
4814    where
4815        F: FnMut(Self::State, Complex64) -> Result<()>,
4816    {
4817        for (target, amplitude) in self.apply_local_transitions(state, operator, sites)? {
4818            visit(target, amplitude)?;
4819        }
4820        Ok(())
4821    }
4822
4823    fn operator_preserves_particle_sector(&self, operator: &str) -> Result<bool> {
4824        Ok(self.total_excitations.is_none() || operator_number_change(operator)? == Some(0))
4825    }
4826}
4827
4828#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4829pub enum StateStorage {
4830    U128,
4831    U256,
4832    U1024,
4833    U4096,
4834    U16384,
4835}
4836
4837impl StateStorage {
4838    pub fn for_bits(bits: usize) -> Result<Self> {
4839        match bits {
4840            1..=128 => Ok(Self::U128),
4841            129..=256 => Ok(Self::U256),
4842            257..=1024 => Ok(Self::U1024),
4843            1025..=4096 => Ok(Self::U4096),
4844            4097..=16384 => Ok(Self::U16384),
4845            0 => Err(QmbedError::InvalidSector(
4846                "a basis state needs at least one bit".into(),
4847            )),
4848            _ => Err(QmbedError::UnsupportedBackend(
4849                "basis state requires more than 16384 bits".into(),
4850            )),
4851        }
4852    }
4853
4854    pub const fn capacity_bits(self) -> usize {
4855        match self {
4856            Self::U128 => 128,
4857            Self::U256 => 256,
4858            Self::U1024 => 1024,
4859            Self::U4096 => 4096,
4860            Self::U16384 => 16384,
4861        }
4862    }
4863}
4864
4865/// Fixed-width state used by wide user and general bases.
4866#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
4867pub struct WideState<const WORDS: usize> {
4868    words: [u64; WORDS],
4869}
4870
4871impl<const WORDS: usize> Ord for WideState<WORDS> {
4872    fn cmp(&self, other: &Self) -> Ordering {
4873        self.words.iter().rev().cmp(other.words.iter().rev())
4874    }
4875}
4876
4877impl<const WORDS: usize> PartialOrd for WideState<WORDS> {
4878    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
4879        Some(self.cmp(other))
4880    }
4881}
4882
4883impl<const WORDS: usize> WideState<WORDS> {
4884    pub const fn zero() -> Self {
4885        Self { words: [0; WORDS] }
4886    }
4887
4888    pub const fn capacity_bits() -> usize {
4889        WORDS * 64
4890    }
4891
4892    pub fn from_words(words: [u64; WORDS]) -> Self {
4893        Self { words }
4894    }
4895
4896    pub fn words(&self) -> &[u64; WORDS] {
4897        &self.words
4898    }
4899
4900    pub fn bit(&self, index: usize) -> Result<bool> {
4901        if index >= Self::capacity_bits() {
4902            return Err(QmbedError::InvalidSite {
4903                site: index,
4904                sites: Self::capacity_bits(),
4905            });
4906        }
4907        Ok(self.words[index / 64] & (1_u64 << (index % 64)) != 0)
4908    }
4909
4910    pub fn with_bit(mut self, index: usize, occupied: bool) -> Result<Self> {
4911        if index >= Self::capacity_bits() {
4912            return Err(QmbedError::InvalidSite {
4913                site: index,
4914                sites: Self::capacity_bits(),
4915            });
4916        }
4917        let mask = 1_u64 << (index % 64);
4918        if occupied {
4919            self.words[index / 64] |= mask;
4920        } else {
4921            self.words[index / 64] &= !mask;
4922        }
4923        Ok(self)
4924    }
4925
4926    pub fn count_ones(&self) -> usize {
4927        self.words
4928            .iter()
4929            .map(|word| word.count_ones() as usize)
4930            .sum()
4931    }
4932
4933    pub fn count_ones_after(&self, index: usize) -> usize {
4934        if index + 1 >= Self::capacity_bits() {
4935            return 0;
4936        }
4937        let first = (index + 1) / 64;
4938        let offset = (index + 1) % 64;
4939        let mut count = 0_usize;
4940        if offset != 0 {
4941            count += (self.words[first] & (!0_u64 << offset)).count_ones() as usize;
4942        } else {
4943            count += self.words[first].count_ones() as usize;
4944        }
4945        count
4946            + self.words[first + 1..]
4947                .iter()
4948                .map(|word| word.count_ones() as usize)
4949                .sum::<usize>()
4950    }
4951
4952    pub fn has_bits_at_or_above(&self, index: usize) -> bool {
4953        if index >= Self::capacity_bits() {
4954            return false;
4955        }
4956        let first = index / 64;
4957        let offset = index % 64;
4958        let first_mask = !0_u64 << offset;
4959        self.words[first] & first_mask != 0 || self.words[first + 1..].iter().any(|word| *word != 0)
4960    }
4961
4962    pub fn bitwise_and(self, right: Self) -> Self {
4963        Self::from_words(std::array::from_fn(|index| {
4964            self.words[index] & right.words[index]
4965        }))
4966    }
4967
4968    pub fn bitwise_or(self, right: Self) -> Self {
4969        Self::from_words(std::array::from_fn(|index| {
4970            self.words[index] | right.words[index]
4971        }))
4972    }
4973
4974    pub fn bitwise_xor(self, right: Self) -> Self {
4975        Self::from_words(std::array::from_fn(|index| {
4976            self.words[index] ^ right.words[index]
4977        }))
4978    }
4979
4980    pub fn bitwise_not(self) -> Self {
4981        Self::from_words(std::array::from_fn(|index| !self.words[index]))
4982    }
4983
4984    pub fn left_shift(self, shift: usize) -> Self {
4985        if shift >= Self::capacity_bits() {
4986            return Self::zero();
4987        }
4988        let word_shift = shift / 64;
4989        let bit_shift = shift % 64;
4990        let mut words = [0_u64; WORDS];
4991        for target in (word_shift..WORDS).rev() {
4992            let source = target - word_shift;
4993            words[target] |= self.words[source] << bit_shift;
4994            if bit_shift > 0 && source > 0 {
4995                words[target] |= self.words[source - 1] >> (64 - bit_shift);
4996            }
4997        }
4998        Self::from_words(words)
4999    }
5000
5001    pub fn right_shift(self, shift: usize) -> Self {
5002        if shift >= Self::capacity_bits() {
5003            return Self::zero();
5004        }
5005        let word_shift = shift / 64;
5006        let bit_shift = shift % 64;
5007        let mut words = [0_u64; WORDS];
5008        for (target, word) in words.iter_mut().enumerate().take(WORDS - word_shift) {
5009            let source = target + word_shift;
5010            *word |= self.words[source] >> bit_shift;
5011            if bit_shift > 0 && source + 1 < WORDS {
5012                *word |= self.words[source + 1] << (64 - bit_shift);
5013            }
5014        }
5015        Self::from_words(words)
5016    }
5017}
5018
5019impl<const WORDS: usize> BinaryState for WideState<WORDS> {
5020    fn bit(&self, index: usize) -> Result<bool> {
5021        WideState::bit(self, index)
5022    }
5023}
5024
5025pub type U256 = WideState<4>;
5026pub type U1024 = WideState<16>;
5027pub type U4096 = WideState<64>;
5028pub type U16384 = WideState<256>;
5029
5030/// Runtime-selected fixed-width basis state.
5031///
5032/// This is the type-erased state boundary used by language bindings and
5033/// runtime-selected bases. Arithmetic remains delegated to the same concrete
5034/// fixed-width implementations used by native Rust callers.
5035#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5036pub struct ErasedState {
5037    width_bits: usize,
5038    value: ErasedStateValue,
5039}
5040
5041#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5042#[allow(clippy::large_enum_variant)] // Fixed-width values stay Copy and allocation-free at the FFI boundary.
5043enum ErasedStateValue {
5044    U128(u128),
5045    U256(U256),
5046    U1024(U1024),
5047    U4096(U4096),
5048    U16384(U16384),
5049}
5050
5051impl Ord for ErasedState {
5052    fn cmp(&self, other: &Self) -> Ordering {
5053        self.width_bits
5054            .cmp(&other.width_bits)
5055            .then_with(|| match (&self.value, &other.value) {
5056                (ErasedStateValue::U128(left), ErasedStateValue::U128(right)) => left.cmp(right),
5057                (ErasedStateValue::U256(left), ErasedStateValue::U256(right)) => left.cmp(right),
5058                (ErasedStateValue::U1024(left), ErasedStateValue::U1024(right)) => left.cmp(right),
5059                (ErasedStateValue::U4096(left), ErasedStateValue::U4096(right)) => left.cmp(right),
5060                (ErasedStateValue::U16384(left), ErasedStateValue::U16384(right)) => {
5061                    left.cmp(right)
5062                }
5063                (left, right) => erased_state_value_rank(left).cmp(&erased_state_value_rank(right)),
5064            })
5065    }
5066}
5067
5068impl PartialOrd for ErasedState {
5069    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
5070        Some(self.cmp(other))
5071    }
5072}
5073
5074impl std::fmt::Display for ErasedState {
5075    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5076        formatter.write_str(&self.to_decimal())
5077    }
5078}
5079
5080const fn erased_state_value_rank(value: &ErasedStateValue) -> u8 {
5081    match value {
5082        ErasedStateValue::U128(_) => 0,
5083        ErasedStateValue::U256(_) => 1,
5084        ErasedStateValue::U1024(_) => 2,
5085        ErasedStateValue::U4096(_) => 3,
5086        ErasedStateValue::U16384(_) => 4,
5087    }
5088}
5089
5090trait FixedWidthState: Copy {
5091    const STORAGE: StateStorage;
5092
5093    fn into_erased_value(self) -> ErasedStateValue;
5094    fn from_erased_value(value: ErasedStateValue) -> Option<Self>;
5095}
5096
5097macro_rules! impl_fixed_width_state {
5098    ($state:ty, $storage:ident, $variant:ident) => {
5099        impl FixedWidthState for $state {
5100            const STORAGE: StateStorage = StateStorage::$storage;
5101
5102            fn into_erased_value(self) -> ErasedStateValue {
5103                ErasedStateValue::$variant(self)
5104            }
5105
5106            fn from_erased_value(value: ErasedStateValue) -> Option<Self> {
5107                match value {
5108                    ErasedStateValue::$variant(value) => Some(value),
5109                    _ => None,
5110                }
5111            }
5112        }
5113    };
5114}
5115
5116impl_fixed_width_state!(U256, U256, U256);
5117impl_fixed_width_state!(U1024, U1024, U1024);
5118impl_fixed_width_state!(U4096, U4096, U4096);
5119impl_fixed_width_state!(U16384, U16384, U16384);
5120
5121fn erase_fixed_width_state<State: FixedWidthState>(width_bits: usize, state: State) -> ErasedState {
5122    ErasedState {
5123        width_bits,
5124        value: state.into_erased_value(),
5125    }
5126}
5127
5128fn restore_fixed_width_state<State: FixedWidthState>(
5129    state: ErasedState,
5130    width_bits: usize,
5131) -> Result<State> {
5132    if state.width_bits != width_bits || state.storage() != State::STORAGE {
5133        return Err(QmbedError::StateNotInBasis);
5134    }
5135    State::from_erased_value(state.value).ok_or(QmbedError::StateNotInBasis)
5136}
5137
5138impl ErasedState {
5139    pub fn from_decimal(width_bits: usize, value: &str) -> Result<Self> {
5140        let value = BigUint::parse_bytes(value.as_bytes(), 10).ok_or_else(|| {
5141            QmbedError::InvalidOptions(format!(
5142                "basis state {value:?} is not a nonnegative decimal integer"
5143            ))
5144        })?;
5145        Self::from_biguint(width_bits, &value)
5146    }
5147
5148    pub fn from_biguint(width_bits: usize, value: &BigUint) -> Result<Self> {
5149        if value.bits() > u64::try_from(width_bits).unwrap_or(u64::MAX) {
5150            return Err(QmbedError::UnsupportedBackend(format!(
5151                "integer needs {} bits but the requested state width is {width_bits}",
5152                value.bits()
5153            )));
5154        }
5155        let storage = StateStorage::for_bits(width_bits)?;
5156        let value = match storage {
5157            StateStorage::U128 => {
5158                let digits = value.to_u64_digits();
5159                let low = u128::from(digits.first().copied().unwrap_or_default());
5160                let high = u128::from(digits.get(1).copied().unwrap_or_default()) << 64;
5161                ErasedStateValue::U128(low | high)
5162            }
5163            StateStorage::U256 => ErasedStateValue::U256(state_from_biguint(value)?),
5164            StateStorage::U1024 => ErasedStateValue::U1024(state_from_biguint(value)?),
5165            StateStorage::U4096 => ErasedStateValue::U4096(state_from_biguint(value)?),
5166            StateStorage::U16384 => ErasedStateValue::U16384(state_from_biguint(value)?),
5167        };
5168        Ok(Self { width_bits, value })
5169    }
5170
5171    pub const fn width_bits(&self) -> usize {
5172        self.width_bits
5173    }
5174
5175    pub const fn storage(&self) -> StateStorage {
5176        match self.value {
5177            ErasedStateValue::U128(_) => StateStorage::U128,
5178            ErasedStateValue::U256(_) => StateStorage::U256,
5179            ErasedStateValue::U1024(_) => StateStorage::U1024,
5180            ErasedStateValue::U4096(_) => StateStorage::U4096,
5181            ErasedStateValue::U16384(_) => StateStorage::U16384,
5182        }
5183    }
5184
5185    pub fn to_biguint(&self) -> BigUint {
5186        match &self.value {
5187            ErasedStateValue::U128(value) => BigUint::from(*value),
5188            ErasedStateValue::U256(value) => state_to_biguint(*value),
5189            ErasedStateValue::U1024(value) => state_to_biguint(*value),
5190            ErasedStateValue::U4096(value) => state_to_biguint(*value),
5191            ErasedStateValue::U16384(value) => state_to_biguint(*value),
5192        }
5193    }
5194
5195    pub fn to_decimal(&self) -> String {
5196        self.to_biguint().to_str_radix(10)
5197    }
5198
5199    /// Return whether one logical binary mode is occupied.
5200    pub fn bit(&self, index: usize) -> Result<bool> {
5201        if index >= self.width_bits {
5202            return Err(QmbedError::InvalidSite {
5203                site: index,
5204                sites: self.width_bits,
5205            });
5206        }
5207        match &self.value {
5208            ErasedStateValue::U128(value) => BinaryState::bit(value, index),
5209            ErasedStateValue::U256(value) => value.bit(index),
5210            ErasedStateValue::U1024(value) => value.bit(index),
5211            ErasedStateValue::U4096(value) => value.bit(index),
5212            ErasedStateValue::U16384(value) => value.bit(index),
5213        }
5214    }
5215
5216    fn ensure_compatible(&self, right: &Self) -> Result<()> {
5217        if self.width_bits != right.width_bits || self.storage() != right.storage() {
5218            return Err(QmbedError::DimensionMismatch(
5219                "bitwise basis states must have the same logical width".into(),
5220            ));
5221        }
5222        Ok(())
5223    }
5224
5225    fn remask(self) -> Result<Self> {
5226        let mask = (BigUint::from(1_u8) << self.width_bits) - BigUint::from(1_u8);
5227        Self::from_biguint(self.width_bits, &(self.to_biguint() & mask))
5228    }
5229
5230    pub fn bitwise_and(&self, right: &Self) -> Result<Self> {
5231        self.ensure_compatible(right)?;
5232        let value = match (&self.value, &right.value) {
5233            (ErasedStateValue::U128(left), ErasedStateValue::U128(right)) => {
5234                ErasedStateValue::U128(left & right)
5235            }
5236            (ErasedStateValue::U256(left), ErasedStateValue::U256(right)) => {
5237                ErasedStateValue::U256(left.bitwise_and(*right))
5238            }
5239            (ErasedStateValue::U1024(left), ErasedStateValue::U1024(right)) => {
5240                ErasedStateValue::U1024(left.bitwise_and(*right))
5241            }
5242            (ErasedStateValue::U4096(left), ErasedStateValue::U4096(right)) => {
5243                ErasedStateValue::U4096(left.bitwise_and(*right))
5244            }
5245            (ErasedStateValue::U16384(left), ErasedStateValue::U16384(right)) => {
5246                ErasedStateValue::U16384(left.bitwise_and(*right))
5247            }
5248            _ => unreachable!("compatible erased states have matching storage"),
5249        };
5250        Ok(Self {
5251            width_bits: self.width_bits,
5252            value,
5253        })
5254    }
5255
5256    pub fn bitwise_or(&self, right: &Self) -> Result<Self> {
5257        self.ensure_compatible(right)?;
5258        let value = match (&self.value, &right.value) {
5259            (ErasedStateValue::U128(left), ErasedStateValue::U128(right)) => {
5260                ErasedStateValue::U128(left | right)
5261            }
5262            (ErasedStateValue::U256(left), ErasedStateValue::U256(right)) => {
5263                ErasedStateValue::U256(left.bitwise_or(*right))
5264            }
5265            (ErasedStateValue::U1024(left), ErasedStateValue::U1024(right)) => {
5266                ErasedStateValue::U1024(left.bitwise_or(*right))
5267            }
5268            (ErasedStateValue::U4096(left), ErasedStateValue::U4096(right)) => {
5269                ErasedStateValue::U4096(left.bitwise_or(*right))
5270            }
5271            (ErasedStateValue::U16384(left), ErasedStateValue::U16384(right)) => {
5272                ErasedStateValue::U16384(left.bitwise_or(*right))
5273            }
5274            _ => unreachable!("compatible erased states have matching storage"),
5275        };
5276        Ok(Self {
5277            width_bits: self.width_bits,
5278            value,
5279        })
5280    }
5281
5282    pub fn bitwise_xor(&self, right: &Self) -> Result<Self> {
5283        self.ensure_compatible(right)?;
5284        let value = match (&self.value, &right.value) {
5285            (ErasedStateValue::U128(left), ErasedStateValue::U128(right)) => {
5286                ErasedStateValue::U128(left ^ right)
5287            }
5288            (ErasedStateValue::U256(left), ErasedStateValue::U256(right)) => {
5289                ErasedStateValue::U256(left.bitwise_xor(*right))
5290            }
5291            (ErasedStateValue::U1024(left), ErasedStateValue::U1024(right)) => {
5292                ErasedStateValue::U1024(left.bitwise_xor(*right))
5293            }
5294            (ErasedStateValue::U4096(left), ErasedStateValue::U4096(right)) => {
5295                ErasedStateValue::U4096(left.bitwise_xor(*right))
5296            }
5297            (ErasedStateValue::U16384(left), ErasedStateValue::U16384(right)) => {
5298                ErasedStateValue::U16384(left.bitwise_xor(*right))
5299            }
5300            _ => unreachable!("compatible erased states have matching storage"),
5301        };
5302        Ok(Self {
5303            width_bits: self.width_bits,
5304            value,
5305        })
5306    }
5307
5308    pub fn bitwise_not(&self) -> Result<Self> {
5309        let value = match &self.value {
5310            ErasedStateValue::U128(value) => ErasedStateValue::U128(!value),
5311            ErasedStateValue::U256(value) => ErasedStateValue::U256(value.bitwise_not()),
5312            ErasedStateValue::U1024(value) => ErasedStateValue::U1024(value.bitwise_not()),
5313            ErasedStateValue::U4096(value) => ErasedStateValue::U4096(value.bitwise_not()),
5314            ErasedStateValue::U16384(value) => ErasedStateValue::U16384(value.bitwise_not()),
5315        };
5316        Self {
5317            width_bits: self.width_bits,
5318            value,
5319        }
5320        .remask()
5321    }
5322
5323    pub fn left_shift(&self, shift: usize) -> Result<Self> {
5324        let value = match &self.value {
5325            ErasedStateValue::U128(value) => ErasedStateValue::U128(
5326                u32::try_from(shift)
5327                    .ok()
5328                    .and_then(|shift| value.checked_shl(shift))
5329                    .unwrap_or_default(),
5330            ),
5331            ErasedStateValue::U256(value) => ErasedStateValue::U256(value.left_shift(shift)),
5332            ErasedStateValue::U1024(value) => ErasedStateValue::U1024(value.left_shift(shift)),
5333            ErasedStateValue::U4096(value) => ErasedStateValue::U4096(value.left_shift(shift)),
5334            ErasedStateValue::U16384(value) => ErasedStateValue::U16384(value.left_shift(shift)),
5335        };
5336        Self {
5337            width_bits: self.width_bits,
5338            value,
5339        }
5340        .remask()
5341    }
5342
5343    pub fn right_shift(&self, shift: usize) -> Self {
5344        let value = match &self.value {
5345            ErasedStateValue::U128(value) => ErasedStateValue::U128(
5346                u32::try_from(shift)
5347                    .ok()
5348                    .and_then(|shift| value.checked_shr(shift))
5349                    .unwrap_or_default(),
5350            ),
5351            ErasedStateValue::U256(value) => ErasedStateValue::U256(value.right_shift(shift)),
5352            ErasedStateValue::U1024(value) => ErasedStateValue::U1024(value.right_shift(shift)),
5353            ErasedStateValue::U4096(value) => ErasedStateValue::U4096(value.right_shift(shift)),
5354            ErasedStateValue::U16384(value) => ErasedStateValue::U16384(value.right_shift(shift)),
5355        };
5356        Self {
5357            width_bits: self.width_bits,
5358            value,
5359        }
5360    }
5361}
5362
5363impl BinaryState for ErasedState {
5364    fn bit(&self, index: usize) -> Result<bool> {
5365        ErasedState::bit(self, index)
5366    }
5367}
5368
5369/// Spin-half basis backed by a fixed-width state, including sites above 127.
5370#[derive(Clone, Debug)]
5371pub struct WideSpinBasis<const WORDS: usize> {
5372    sites: usize,
5373    particle_sectors: Option<Vec<usize>>,
5374    normalization: SpinNormalization,
5375    states: Vec<WideState<WORDS>>,
5376}
5377
5378impl<const WORDS: usize> WideSpinBasis<WORDS> {
5379    pub fn new(sites: usize, particles: Option<usize>, pauli: bool) -> Result<Self> {
5380        let normalization = if pauli {
5381            SpinNormalization::PauliCartesian
5382        } else {
5383            SpinNormalization::AngularMomentum
5384        };
5385        match particles {
5386            Some(particles) => {
5387                Self::from_particle_sectors_with_normalization(sites, [particles], normalization)
5388            }
5389            None => Self::from_optional_particle_sectors(sites, None, normalization),
5390        }
5391    }
5392
5393    /// Construct a wide basis from a nonempty union of fixed-particle sectors.
5394    pub fn from_particle_sectors(
5395        sites: usize,
5396        sectors: impl IntoIterator<Item = usize>,
5397        pauli: bool,
5398    ) -> Result<Self> {
5399        let normalization = if pauli {
5400            SpinNormalization::PauliCartesian
5401        } else {
5402            SpinNormalization::AngularMomentum
5403        };
5404        Self::from_particle_sectors_with_normalization(sites, sectors, normalization)
5405    }
5406
5407    pub fn with_normalization(
5408        sites: usize,
5409        particles: Option<usize>,
5410        normalization: SpinNormalization,
5411    ) -> Result<Self> {
5412        match particles {
5413            Some(particles) => {
5414                Self::from_particle_sectors_with_normalization(sites, [particles], normalization)
5415            }
5416            None => Self::from_optional_particle_sectors(sites, None, normalization),
5417        }
5418    }
5419
5420    pub fn from_particle_sectors_with_normalization(
5421        sites: usize,
5422        sectors: impl IntoIterator<Item = usize>,
5423        normalization: SpinNormalization,
5424    ) -> Result<Self> {
5425        let mut sectors: Vec<_> = sectors.into_iter().collect();
5426        sectors.sort_unstable();
5427        sectors.dedup();
5428        if sectors.is_empty() {
5429            return Err(QmbedError::InvalidSector(
5430                "wide spin sector union must be nonempty".into(),
5431            ));
5432        }
5433        Self::from_optional_particle_sectors(sites, Some(sectors), normalization)
5434    }
5435
5436    /// Construct the same spin algebra on an explicitly selected physical
5437    /// state set.
5438    ///
5439    /// This is useful for finite-group orbit completions whose physical span
5440    /// is enumerable even when the unrestricted `2^sites` parent is not. The
5441    /// selected states remain an ordinary basis: local actions leaving the set
5442    /// are rejected by assembly in exactly the same way as any sector basis.
5443    pub fn from_explicit_states_with_normalization(
5444        sites: usize,
5445        states: impl IntoIterator<Item = WideState<WORDS>>,
5446        normalization: SpinNormalization,
5447    ) -> Result<Self> {
5448        if sites == 0 || sites > WideState::<WORDS>::capacity_bits() {
5449            return Err(QmbedError::UnsupportedBackend(format!(
5450                "wide spin basis needs 1..={} sites",
5451                WideState::<WORDS>::capacity_bits()
5452            )));
5453        }
5454        let mut states: Vec<_> = states.into_iter().collect();
5455        if states.iter().any(|state| state.has_bits_at_or_above(sites)) {
5456            return Err(QmbedError::InvalidSector(
5457                "an explicit wide spin state exceeds the requested site width".into(),
5458            ));
5459        }
5460        states.sort_unstable();
5461        states.dedup();
5462        let mut particle_sectors: Vec<_> = states.iter().map(WideState::count_ones).collect();
5463        particle_sectors.sort_unstable();
5464        particle_sectors.dedup();
5465        Ok(Self {
5466            sites,
5467            particle_sectors: Some(particle_sectors),
5468            normalization,
5469            states,
5470        })
5471    }
5472
5473    fn from_optional_particle_sectors(
5474        sites: usize,
5475        particle_sectors: Option<Vec<usize>>,
5476        normalization: SpinNormalization,
5477    ) -> Result<Self> {
5478        if sites == 0 || sites > WideState::<WORDS>::capacity_bits() {
5479            return Err(QmbedError::UnsupportedBackend(format!(
5480                "wide spin basis needs 1..={} sites",
5481                WideState::<WORDS>::capacity_bits()
5482            )));
5483        }
5484        if particle_sectors
5485            .as_ref()
5486            .is_some_and(|sectors| sectors.iter().any(|&count| count > sites))
5487        {
5488            return Err(QmbedError::InvalidSector(
5489                "particle count exceeds the wide spin site count".into(),
5490            ));
5491        }
5492        let mut states = Vec::new();
5493        if let Some(sectors) = &particle_sectors {
5494            fn enumerate<const WORDS: usize>(
5495                next_site: usize,
5496                sites: usize,
5497                remaining: usize,
5498                state: WideState<WORDS>,
5499                output: &mut Vec<WideState<WORDS>>,
5500            ) -> Result<()> {
5501                if remaining == 0 {
5502                    output.push(state);
5503                    return Ok(());
5504                }
5505                if sites.saturating_sub(next_site) < remaining {
5506                    return Ok(());
5507                }
5508                for site in next_site..=sites - remaining {
5509                    enumerate(
5510                        site + 1,
5511                        sites,
5512                        remaining - 1,
5513                        state.with_bit(site, true)?,
5514                        output,
5515                    )?;
5516                }
5517                Ok(())
5518            }
5519            for &count in sectors {
5520                enumerate(0, sites, count, WideState::zero(), &mut states)?;
5521            }
5522        } else {
5523            if sites > 24 {
5524                return Err(QmbedError::InvalidOptions(
5525                    "an unrestricted wide spin basis above 24 sites is not enumerable; select a particle sector"
5526                        .into(),
5527                ));
5528            }
5529            let limit = 1_u128 << sites;
5530            states.extend((0..limit).map(python_int_to_basis_int));
5531        }
5532        states.sort_unstable();
5533        Ok(Self {
5534            sites,
5535            particle_sectors,
5536            normalization,
5537            states,
5538        })
5539    }
5540
5541    pub const fn sites(&self) -> usize {
5542        self.sites
5543    }
5544
5545    pub fn particles(&self) -> Option<usize> {
5546        match self.particle_sectors.as_deref() {
5547            Some([particles]) => Some(*particles),
5548            _ => None,
5549        }
5550    }
5551
5552    pub fn particle_sectors(&self) -> Option<&[usize]> {
5553        self.particle_sectors.as_deref()
5554    }
5555
5556    pub const fn normalization(&self) -> SpinNormalization {
5557        self.normalization
5558    }
5559}
5560
5561impl<const WORDS: usize> Basis for WideSpinBasis<WORDS> {
5562    type State = WideState<WORDS>;
5563
5564    fn len(&self) -> usize {
5565        self.states.len()
5566    }
5567
5568    fn state(&self, index: usize) -> Result<Self::State> {
5569        self.states
5570            .get(index)
5571            .copied()
5572            .ok_or(QmbedError::StateNotInBasis)
5573    }
5574
5575    fn index(&self, state: Self::State) -> Result<usize> {
5576        self.states
5577            .binary_search(&state)
5578            .map_err(|_| QmbedError::StateNotInBasis)
5579    }
5580
5581    fn apply_local(
5582        &self,
5583        mut state: Self::State,
5584        operator: &str,
5585        sites: &[usize],
5586    ) -> Result<Option<(Self::State, Complex64)>> {
5587        let chars = operator_chars(operator, sites)?;
5588        let mut amplitude = Complex64::new(1.0, 0.0);
5589        for (&site, op) in sites.iter().zip(chars).rev() {
5590            checked_site(site, self.sites)?;
5591            let occupied = state.bit(site)?;
5592            let cartesian_scale = match self.normalization {
5593                SpinNormalization::AngularMomentum | SpinNormalization::Pauli => 0.5,
5594                SpinNormalization::PauliCartesian => 1.0,
5595            };
5596            let z_scale = if self.normalization == SpinNormalization::AngularMomentum {
5597                0.5
5598            } else {
5599                1.0
5600            };
5601            let ladder_scale = if self.normalization == SpinNormalization::Pauli {
5602                2.0
5603            } else {
5604                1.0
5605            };
5606            match op {
5607                'I' => {}
5608                'n' => {
5609                    if !occupied {
5610                        return Ok(None);
5611                    }
5612                }
5613                'z' => amplitude *= if occupied { z_scale } else { -z_scale },
5614                '+' if !occupied => {
5615                    state = state.with_bit(site, true)?;
5616                    amplitude *= ladder_scale;
5617                }
5618                '-' if occupied => {
5619                    state = state.with_bit(site, false)?;
5620                    amplitude *= ladder_scale;
5621                }
5622                'x' => {
5623                    state = state.with_bit(site, !occupied)?;
5624                    amplitude *= cartesian_scale;
5625                }
5626                'y' => {
5627                    state = state.with_bit(site, !occupied)?;
5628                    amplitude *= Complex64::new(
5629                        0.0,
5630                        if occupied {
5631                            cartesian_scale
5632                        } else {
5633                            -cartesian_scale
5634                        },
5635                    );
5636                }
5637                '+' | '-' => return Ok(None),
5638                _ => return Err(QmbedError::InvalidOperator(op.to_string())),
5639            }
5640        }
5641        Ok(Some((state, amplitude)))
5642    }
5643
5644    fn operator_preserves_particle_sector(&self, operator: &str) -> Result<bool> {
5645        Ok(self.particle_sectors.is_none() || operator_number_change(operator)? == Some(0))
5646    }
5647}
5648
5649pub type WideSpinBasis256 = WideSpinBasis<4>;
5650pub type WideSpinBasis1024 = WideSpinBasis<16>;
5651pub type WideSpinBasis4096 = WideSpinBasis<64>;
5652pub type WideSpinBasis16384 = WideSpinBasis<256>;
5653pub type WideSpinBasisGeneral256 = GeneralBasis<WideSpinBasis256>;
5654pub type WideSpinBasisGeneral1024 = GeneralBasis<WideSpinBasis1024>;
5655pub type WideSpinBasisGeneral4096 = GeneralBasis<WideSpinBasis4096>;
5656pub type WideSpinBasisGeneral16384 = GeneralBasis<WideSpinBasis16384>;
5657
5658/// Runtime-selected wide spin basis used by persistent language-neutral models.
5659///
5660/// The enum erases only the fixed-width state storage. Local operator
5661/// semantics, symmetry reduction, and universal assembly remain implemented
5662/// by the same typed [`WideSpinBasis`] and [`GeneralBasis`] kernels used by
5663/// native Rust callers.
5664#[derive(Clone, Debug)]
5665pub enum WidePackedBasis {
5666    Spin256(WideSpinBasis256),
5667    Spin1024(WideSpinBasis1024),
5668    Spin4096(WideSpinBasis4096),
5669    Spin16384(WideSpinBasis16384),
5670    GeneralSpin256(WideSpinBasisGeneral256),
5671    GeneralSpin1024(WideSpinBasisGeneral1024),
5672    GeneralSpin4096(WideSpinBasisGeneral4096),
5673    GeneralSpin16384(WideSpinBasisGeneral16384),
5674    Reversed(Box<WidePackedBasis>),
5675}
5676
5677impl WidePackedBasis {
5678    pub fn reversed(self) -> Self {
5679        match self {
5680            Self::Reversed(inner) => *inner,
5681            basis => Self::Reversed(Box::new(basis)),
5682        }
5683    }
5684
5685    /// Reuse this runtime-selected spin algebra on an explicitly selected set
5686    /// of physical states while preserving its public ordering convention.
5687    pub fn explicit_spin_subspace(&self, states: &[ErasedState]) -> Result<Self> {
5688        macro_rules! explicit {
5689            ($basis:expr, $state:ty, $variant:ident) => {{
5690                let basis = $basis;
5691                let typed = states
5692                    .iter()
5693                    .copied()
5694                    .map(|state| restore_fixed_width_state::<$state>(state, basis.sites()))
5695                    .collect::<Result<Vec<_>>>()?;
5696                Ok(Self::$variant(
5697                    WideSpinBasis::from_explicit_states_with_normalization(
5698                        basis.sites(),
5699                        typed,
5700                        basis.normalization(),
5701                    )?,
5702                ))
5703            }};
5704        }
5705        match self {
5706            Self::Spin256(basis) => explicit!(basis, U256, Spin256),
5707            Self::Spin1024(basis) => explicit!(basis, U1024, Spin1024),
5708            Self::Spin4096(basis) => explicit!(basis, U4096, Spin4096),
5709            Self::Spin16384(basis) => explicit!(basis, U16384, Spin16384),
5710            Self::Reversed(inner) => Ok(inner.explicit_spin_subspace(states)?.reversed()),
5711            Self::GeneralSpin256(_)
5712            | Self::GeneralSpin1024(_)
5713            | Self::GeneralSpin4096(_)
5714            | Self::GeneralSpin16384(_) => Err(QmbedError::InvalidOptions(
5715                "an explicit wide spin subspace must start from an unreduced spin algebra".into(),
5716            )),
5717        }
5718    }
5719
5720    pub fn width_bits(&self) -> usize {
5721        match self {
5722            Self::Spin256(basis) => basis.sites(),
5723            Self::Spin1024(basis) => basis.sites(),
5724            Self::Spin4096(basis) => basis.sites(),
5725            Self::Spin16384(basis) => basis.sites(),
5726            Self::GeneralSpin256(basis) => basis.parent().sites(),
5727            Self::GeneralSpin1024(basis) => basis.parent().sites(),
5728            Self::GeneralSpin4096(basis) => basis.parent().sites(),
5729            Self::GeneralSpin16384(basis) => basis.parent().sites(),
5730            Self::Reversed(basis) => basis.width_bits(),
5731        }
5732    }
5733
5734    pub fn storage(&self) -> StateStorage {
5735        match self {
5736            Self::Spin256(_) | Self::GeneralSpin256(_) => StateStorage::U256,
5737            Self::Spin1024(_) | Self::GeneralSpin1024(_) => StateStorage::U1024,
5738            Self::Spin4096(_) | Self::GeneralSpin4096(_) => StateStorage::U4096,
5739            Self::Spin16384(_) | Self::GeneralSpin16384(_) => StateStorage::U16384,
5740            Self::Reversed(basis) => basis.storage(),
5741        }
5742    }
5743}
5744
5745macro_rules! dispatch_wide_basis {
5746    ($basis:expr, $inner:ident => $body:expr) => {
5747        match $basis {
5748            WidePackedBasis::Spin256($inner) => $body,
5749            WidePackedBasis::Spin1024($inner) => $body,
5750            WidePackedBasis::Spin4096($inner) => $body,
5751            WidePackedBasis::Spin16384($inner) => $body,
5752            WidePackedBasis::GeneralSpin256($inner) => $body,
5753            WidePackedBasis::GeneralSpin1024($inner) => $body,
5754            WidePackedBasis::GeneralSpin4096($inner) => $body,
5755            WidePackedBasis::GeneralSpin16384($inner) => $body,
5756            WidePackedBasis::Reversed(_) => {
5757                unreachable!("reversed wide bases are handled before typed dispatch")
5758            }
5759        }
5760    };
5761}
5762
5763impl Basis for WidePackedBasis {
5764    type State = ErasedState;
5765
5766    fn len(&self) -> usize {
5767        if let Self::Reversed(basis) = self {
5768            return basis.len();
5769        }
5770        dispatch_wide_basis!(self, basis => basis.len())
5771    }
5772
5773    fn state(&self, index: usize) -> Result<Self::State> {
5774        if let Self::Reversed(basis) = self {
5775            let reversed = basis
5776                .len()
5777                .checked_sub(index + 1)
5778                .ok_or(QmbedError::StateNotInBasis)?;
5779            return basis.state(reversed);
5780        }
5781        let width_bits = self.width_bits();
5782        dispatch_wide_basis!(
5783            self,
5784            basis => basis
5785                .state(index)
5786                .map(|state| erase_fixed_width_state(width_bits, state))
5787        )
5788    }
5789
5790    fn index(&self, state: Self::State) -> Result<usize> {
5791        if let Self::Reversed(basis) = self {
5792            return basis.index(state).map(|index| basis.len() - index - 1);
5793        }
5794        let width_bits = self.width_bits();
5795        dispatch_wide_basis!(
5796            self,
5797            basis => basis.index(restore_fixed_width_state(state, width_bits)?)
5798        )
5799    }
5800
5801    fn apply_local(
5802        &self,
5803        state: Self::State,
5804        operator: &str,
5805        sites: &[usize],
5806    ) -> Result<Option<(Self::State, Complex64)>> {
5807        if let Self::Reversed(basis) = self {
5808            return basis.apply_local(state, operator, sites);
5809        }
5810        let width_bits = self.width_bits();
5811        dispatch_wide_basis!(
5812            self,
5813            basis => basis
5814                .apply_local(
5815                    restore_fixed_width_state(state, width_bits)?,
5816                    operator,
5817                    sites,
5818                )
5819                .map(|transition| {
5820                    transition.map(|(target, amplitude)| {
5821                        (
5822                            erase_fixed_width_state(width_bits, target),
5823                            amplitude,
5824                        )
5825                    })
5826                })
5827        )
5828    }
5829
5830    fn apply_local_transitions(
5831        &self,
5832        state: Self::State,
5833        operator: &str,
5834        sites: &[usize],
5835    ) -> Result<LocalTransitions<Self::State>> {
5836        if let Self::Reversed(basis) = self {
5837            return basis.apply_local_transitions(state, operator, sites);
5838        }
5839        let width_bits = self.width_bits();
5840        dispatch_wide_basis!(
5841            self,
5842            basis => basis
5843                .apply_local_transitions(
5844                    restore_fixed_width_state(state, width_bits)?,
5845                    operator,
5846                    sites,
5847                )
5848                .map(|transitions| {
5849                    transitions
5850                        .into_iter()
5851                        .map(|(target, amplitude)| {
5852                            (
5853                                erase_fixed_width_state(width_bits, target),
5854                                amplitude,
5855                            )
5856                        })
5857                        .collect()
5858                })
5859        )
5860    }
5861
5862    fn apply_local_unreduced_transitions(
5863        &self,
5864        state: Self::State,
5865        operator: &str,
5866        sites: &[usize],
5867    ) -> Result<LocalTransitions<Self::State>> {
5868        if let Self::Reversed(basis) = self {
5869            return basis.apply_local_unreduced_transitions(state, operator, sites);
5870        }
5871        let width_bits = self.width_bits();
5872        dispatch_wide_basis!(
5873            self,
5874            basis => basis
5875                .apply_local_unreduced_transitions(
5876                    restore_fixed_width_state(state, width_bits)?,
5877                    operator,
5878                    sites,
5879                )
5880                .map(|transitions| {
5881                    transitions
5882                        .into_iter()
5883                        .map(|(target, amplitude)| {
5884                            (
5885                                erase_fixed_width_state(width_bits, target),
5886                                amplitude,
5887                            )
5888                        })
5889                        .collect()
5890                })
5891        )
5892    }
5893
5894    fn visit_local_unreduced_transitions<F>(
5895        &self,
5896        state: Self::State,
5897        operator: &str,
5898        sites: &[usize],
5899        mut visit: F,
5900    ) -> Result<()>
5901    where
5902        F: FnMut(Self::State, Complex64) -> Result<()>,
5903    {
5904        if let Self::Reversed(basis) = self {
5905            return basis.visit_local_unreduced_transitions(state, operator, sites, visit);
5906        }
5907        let width_bits = self.width_bits();
5908        dispatch_wide_basis!(
5909            self,
5910            basis => basis.visit_local_unreduced_transitions(
5911                restore_fixed_width_state(state, width_bits)?,
5912                operator,
5913                sites,
5914                |target, amplitude| {
5915                    visit(erase_fixed_width_state(width_bits, target), amplitude)
5916                },
5917            )
5918        )
5919    }
5920
5921    fn visit_preparsed_local_unreduced_transitions<F>(
5922        &self,
5923        state: Self::State,
5924        operator: &str,
5925        symbols: &[char],
5926        split: Option<usize>,
5927        sites: &[usize],
5928        mut visit: F,
5929    ) -> Result<()>
5930    where
5931        F: FnMut(Self::State, Complex64) -> Result<()>,
5932    {
5933        if let Self::Reversed(basis) = self {
5934            return basis.visit_preparsed_local_unreduced_transitions(
5935                state, operator, symbols, split, sites, visit,
5936            );
5937        }
5938        let width_bits = self.width_bits();
5939        dispatch_wide_basis!(
5940            self,
5941            basis => basis.visit_preparsed_local_unreduced_transitions(
5942                restore_fixed_width_state(state, width_bits)?,
5943                operator,
5944                symbols,
5945                split,
5946                sites,
5947                |target, amplitude| {
5948                    visit(erase_fixed_width_state(width_bits, target), amplitude)
5949                },
5950            )
5951        )
5952    }
5953
5954    fn reduction_image(&self, state: Self::State) -> Result<Option<ReductionImage<Self::State>>> {
5955        if let Self::Reversed(basis) = self {
5956            return basis.reduction_image(state);
5957        }
5958        let width_bits = self.width_bits();
5959        dispatch_wide_basis!(
5960            self,
5961            basis => basis
5962                .reduction_image(restore_fixed_width_state(state, width_bits)?)?
5963                .map(|image| {
5964                    ReductionImage::new(
5965                        erase_fixed_width_state(width_bits, *image.representative()),
5966                        image.phase(),
5967                        image.orbit_size(),
5968                    )
5969                })
5970                .transpose()
5971        )
5972    }
5973
5974    fn transition_orbit_size(&self, state: Self::State) -> Result<usize> {
5975        if let Self::Reversed(basis) = self {
5976            return basis.transition_orbit_size(state);
5977        }
5978        let width_bits = self.width_bits();
5979        dispatch_wide_basis!(
5980            self,
5981            basis => basis.transition_orbit_size(restore_fixed_width_state(state, width_bits)?)
5982        )
5983    }
5984
5985    fn reduce_transition(
5986        &self,
5987        state: Self::State,
5988        source_orbit_size: usize,
5989    ) -> Result<Option<(Self::State, Complex64)>> {
5990        if let Self::Reversed(basis) = self {
5991            return basis.reduce_transition(state, source_orbit_size);
5992        }
5993        let width_bits = self.width_bits();
5994        dispatch_wide_basis!(
5995            self,
5996            basis => Ok(basis
5997                .reduce_transition(
5998                    restore_fixed_width_state(state, width_bits)?,
5999                    source_orbit_size,
6000                )?
6001                .map(|(representative, amplitude)| {
6002                    (
6003                        erase_fixed_width_state(width_bits, representative),
6004                        amplitude,
6005                    )
6006                }))
6007        )
6008    }
6009
6010    fn index_transition(
6011        &self,
6012        state: Self::State,
6013        source_orbit_size: usize,
6014    ) -> Result<Option<(usize, Complex64)>> {
6015        if let Self::Reversed(basis) = self {
6016            return Ok(basis
6017                .index_transition(state, source_orbit_size)?
6018                .map(|(index, amplitude)| (basis.len() - index - 1, amplitude)));
6019        }
6020        let width_bits = self.width_bits();
6021        dispatch_wide_basis!(
6022            self,
6023            basis => basis.index_transition(
6024                restore_fixed_width_state(state, width_bits)?,
6025                source_orbit_size,
6026            )
6027        )
6028    }
6029
6030    fn operator_preserves_particle_sector(&self, operator: &str) -> Result<bool> {
6031        if let Self::Reversed(basis) = self {
6032            return basis.operator_preserves_particle_sector(operator);
6033        }
6034        dispatch_wide_basis!(
6035            self,
6036            basis => basis.operator_preserves_particle_sector(operator)
6037        )
6038    }
6039}
6040
6041impl From<WideSpinBasis256> for WidePackedBasis {
6042    fn from(basis: WideSpinBasis256) -> Self {
6043        Self::Spin256(basis)
6044    }
6045}
6046
6047impl From<WideSpinBasis1024> for WidePackedBasis {
6048    fn from(basis: WideSpinBasis1024) -> Self {
6049        Self::Spin1024(basis)
6050    }
6051}
6052
6053impl From<WideSpinBasis4096> for WidePackedBasis {
6054    fn from(basis: WideSpinBasis4096) -> Self {
6055        Self::Spin4096(basis)
6056    }
6057}
6058
6059impl From<WideSpinBasis16384> for WidePackedBasis {
6060    fn from(basis: WideSpinBasis16384) -> Self {
6061        Self::Spin16384(basis)
6062    }
6063}
6064
6065impl From<WideSpinBasisGeneral256> for WidePackedBasis {
6066    fn from(basis: WideSpinBasisGeneral256) -> Self {
6067        Self::GeneralSpin256(basis)
6068    }
6069}
6070
6071impl From<WideSpinBasisGeneral1024> for WidePackedBasis {
6072    fn from(basis: WideSpinBasisGeneral1024) -> Self {
6073        Self::GeneralSpin1024(basis)
6074    }
6075}
6076
6077impl From<WideSpinBasisGeneral4096> for WidePackedBasis {
6078    fn from(basis: WideSpinBasisGeneral4096) -> Self {
6079        Self::GeneralSpin4096(basis)
6080    }
6081}
6082
6083impl From<WideSpinBasisGeneral16384> for WidePackedBasis {
6084    fn from(basis: WideSpinBasisGeneral16384) -> Self {
6085        Self::GeneralSpin16384(basis)
6086    }
6087}
6088
6089pub fn basis_zeros<const WORDS: usize>(length: usize) -> Vec<WideState<WORDS>> {
6090    vec![WideState::zero(); length]
6091}
6092
6093pub fn basis_ones<const WORDS: usize>(length: usize) -> Vec<WideState<WORDS>> {
6094    vec![WideState::zero().bitwise_not(); length]
6095}
6096
6097pub fn bitwise_and<const WORDS: usize>(
6098    left: WideState<WORDS>,
6099    right: WideState<WORDS>,
6100) -> WideState<WORDS> {
6101    left.bitwise_and(right)
6102}
6103
6104pub fn bitwise_or<const WORDS: usize>(
6105    left: WideState<WORDS>,
6106    right: WideState<WORDS>,
6107) -> WideState<WORDS> {
6108    left.bitwise_or(right)
6109}
6110
6111pub fn bitwise_xor<const WORDS: usize>(
6112    left: WideState<WORDS>,
6113    right: WideState<WORDS>,
6114) -> WideState<WORDS> {
6115    left.bitwise_xor(right)
6116}
6117
6118pub fn bitwise_not<const WORDS: usize>(value: WideState<WORDS>) -> WideState<WORDS> {
6119    value.bitwise_not()
6120}
6121
6122pub fn bitwise_leftshift<const WORDS: usize>(
6123    value: WideState<WORDS>,
6124    shift: usize,
6125) -> WideState<WORDS> {
6126    value.left_shift(shift)
6127}
6128
6129pub fn bitwise_rightshift<const WORDS: usize>(
6130    value: WideState<WORDS>,
6131    shift: usize,
6132) -> WideState<WORDS> {
6133    value.right_shift(shift)
6134}
6135
6136pub fn python_int_to_basis_int<const WORDS: usize>(value: u128) -> WideState<WORDS> {
6137    let mut words = [0_u64; WORDS];
6138    if WORDS > 0 {
6139        words[0] = value as u64;
6140    }
6141    if WORDS > 1 {
6142        words[1] = (value >> 64) as u64;
6143    }
6144    WideState::from_words(words)
6145}
6146
6147pub fn basis_int_to_python_int<const WORDS: usize>(value: WideState<WORDS>) -> Result<u128> {
6148    if value.words.iter().skip(2).any(|word| *word != 0) {
6149        return Err(QmbedError::UnsupportedBackend(
6150            "wide basis integer does not fit into a Python-compatible u128".into(),
6151        ));
6152    }
6153    Ok(u128::from(value.words.first().copied().unwrap_or_default())
6154        | (u128::from(value.words.get(1).copied().unwrap_or_default()) << 64))
6155}
6156
6157/// Convert an arbitrary-precision nonnegative integer into a fixed-width basis
6158/// state without truncating high words.
6159pub fn state_from_biguint<const WORDS: usize>(value: &BigUint) -> Result<WideState<WORDS>> {
6160    let digits = value.to_u64_digits();
6161    if digits.len() > WORDS {
6162        return Err(QmbedError::UnsupportedBackend(format!(
6163            "integer needs {} bits but this state stores {} bits",
6164            value.bits(),
6165            WideState::<WORDS>::capacity_bits()
6166        )));
6167    }
6168    let mut words = [0_u64; WORDS];
6169    words[..digits.len()].copy_from_slice(&digits);
6170    Ok(WideState::from_words(words))
6171}
6172
6173/// Convert a fixed-width state to an arbitrary-precision integer.
6174pub fn state_to_biguint<const WORDS: usize>(value: WideState<WORDS>) -> BigUint {
6175    let bytes: Vec<_> = value
6176        .words()
6177        .iter()
6178        .flat_map(|word| word.to_le_bytes())
6179        .collect();
6180    BigUint::from_bytes_le(&bytes)
6181}
6182
6183pub fn get_basis_type(
6184    sites: usize,
6185    _particles: Option<usize>,
6186    states_per_site: usize,
6187) -> Result<StateStorage> {
6188    if states_per_site < 2 {
6189        return Err(QmbedError::InvalidSector(
6190            "states_per_site must be at least two".into(),
6191        ));
6192    }
6193    let bits_per_site =
6194        usize::try_from(usize::BITS - (states_per_site - 1).leading_zeros()).unwrap_or(usize::MAX);
6195    let bits = sites
6196        .checked_mul(bits_per_site)
6197        .ok_or_else(|| QmbedError::UnsupportedBackend("basis bit width overflow".into()))?;
6198    StateStorage::for_bits(bits.max(1))
6199}
6200
6201pub fn coherent_state(amplitude: Complex64, states: usize) -> Result<Vec<Complex64>> {
6202    if states == 0 || !amplitude.re.is_finite() || !amplitude.im.is_finite() {
6203        return Err(QmbedError::InvalidOptions(
6204            "coherent-state amplitude must be finite and the cutoff positive".into(),
6205        ));
6206    }
6207    let mut coefficients = Vec::with_capacity(states);
6208    let mut coefficient = Complex64::new((-0.5 * amplitude.norm_sqr()).exp(), 0.0);
6209    coefficients.push(coefficient);
6210    for occupation in 1..states {
6211        coefficient *= amplitude / (occupation as f64).sqrt();
6212        coefficients.push(coefficient);
6213    }
6214    Ok(coefficients)
6215}
6216
6217fn binomial(n: usize, k: usize) -> usize {
6218    let k = k.min(n.saturating_sub(k));
6219    (0..k).fold(1_usize, |value, index| {
6220        value.saturating_mul(n - index) / (index + 1)
6221    })
6222}
6223
6224/// Dimension of a spin-half chain plus one photon mode at fixed excitation.
6225pub fn photon_hspace_dim(
6226    sites: usize,
6227    total_excitations: Option<usize>,
6228    photon_cutoff: Option<usize>,
6229) -> Result<usize> {
6230    match (total_excitations, photon_cutoff) {
6231        (None, Some(cutoff)) => 1_usize
6232            .checked_shl(u32::try_from(sites).unwrap_or(u32::MAX))
6233            .and_then(|matter| matter.checked_mul(cutoff.saturating_add(1)))
6234            .ok_or_else(|| QmbedError::UnsupportedBackend("photon dimension overflow".into())),
6235        (Some(total), cutoff) => {
6236            let minimum_matter =
6237                cutoff.map_or(0, |maximum_photons| total.saturating_sub(maximum_photons));
6238            let maximum_matter = sites.min(total);
6239            Ok((minimum_matter..=maximum_matter)
6240                .map(|matter| binomial(sites, matter))
6241                .sum())
6242        }
6243        (None, None) => Err(QmbedError::InvalidSector(
6244            "either total excitation or photon cutoff must be finite".into(),
6245        )),
6246    }
6247}
6248
6249/// Sparse isometric lift from a symmetry-reduced basis to its parent basis.
6250#[derive(Clone, Debug)]
6251pub struct BasisProjector {
6252    source_dimension: usize,
6253    reduced_dimension: usize,
6254    column_offsets: Vec<usize>,
6255    row_indices: Vec<usize>,
6256    values: Vec<Complex64>,
6257}
6258
6259impl BasisProjector {
6260    fn from_columns(
6261        source_dimension: usize,
6262        mut columns: Vec<Vec<(usize, Complex64)>>,
6263    ) -> Result<Self> {
6264        if columns.iter().flatten().any(|(row, value)| {
6265            *row >= source_dimension || !value.re.is_finite() || !value.im.is_finite()
6266        }) {
6267            return Err(QmbedError::DimensionMismatch(
6268                "projector contains an invalid parent-space row or coefficient".into(),
6269            ));
6270        }
6271        let reduced_dimension = columns.len();
6272        let mut column_offsets = Vec::with_capacity(reduced_dimension + 1);
6273        let mut row_indices = Vec::new();
6274        let mut values = Vec::new();
6275        column_offsets.push(0);
6276        for column in &mut columns {
6277            column.sort_by_key(|(row, _)| *row);
6278            for &(row, value) in column.iter() {
6279                row_indices.push(row);
6280                values.push(value);
6281            }
6282            column_offsets.push(row_indices.len());
6283        }
6284        Ok(Self {
6285            source_dimension,
6286            reduced_dimension,
6287            column_offsets,
6288            row_indices,
6289            values,
6290        })
6291    }
6292
6293    /// Convert any explicit rectangular isometry into a reusable basis
6294    /// projector.
6295    ///
6296    /// This is the narrow waist for reduced spaces whose columns are not
6297    /// representable by a one-dimensional orbit character, including
6298    /// higher-dimensional finite-group representations.
6299    pub fn from_operator(
6300        projector: &(impl LinearOperator + ?Sized),
6301        tolerance: f64,
6302    ) -> Result<Self> {
6303        if !tolerance.is_finite() || tolerance <= 0.0 {
6304            return Err(QmbedError::InvalidOptions(
6305                "projector-isometry tolerance must be positive and finite".into(),
6306            ));
6307        }
6308        let (source_dimension, reduced_dimension) = projector.shape();
6309        let mut columns = vec![Vec::new(); reduced_dimension];
6310        if let Some(entries) = projector.stored_triplets()? {
6311            for (row, column, value) in entries {
6312                if value.norm() > f64::EPSILON {
6313                    columns[column].push((row, value));
6314                }
6315            }
6316        } else {
6317            let mut input = vec![Complex64::new(0.0, 0.0); reduced_dimension];
6318            let mut output = vec![Complex64::new(0.0, 0.0); source_dimension];
6319            for column in 0..reduced_dimension {
6320                input.fill(Complex64::new(0.0, 0.0));
6321                input[column] = Complex64::new(1.0, 0.0);
6322                projector.apply(&input, &mut output)?;
6323                columns[column].extend(
6324                    output
6325                        .iter()
6326                        .copied()
6327                        .enumerate()
6328                        .filter(|(_, value)| value.norm() > f64::EPSILON),
6329                );
6330            }
6331        }
6332        let projector = Self::from_columns(source_dimension, columns)?;
6333        let mut coordinate = vec![Complex64::new(0.0, 0.0); reduced_dimension];
6334        let mut parent = vec![Complex64::new(0.0, 0.0); source_dimension];
6335        let mut recovered = vec![Complex64::new(0.0, 0.0); reduced_dimension];
6336        for column in 0..reduced_dimension {
6337            coordinate.fill(Complex64::new(0.0, 0.0));
6338            coordinate[column] = Complex64::new(1.0, 0.0);
6339            projector.apply(&coordinate, &mut parent)?;
6340            projector.project(&parent, &mut recovered)?;
6341            for (row, value) in recovered.iter().copied().enumerate() {
6342                let expected = if row == column {
6343                    Complex64::new(1.0, 0.0)
6344                } else {
6345                    Complex64::new(0.0, 0.0)
6346                };
6347                if (value - expected).norm() > tolerance {
6348                    return Err(QmbedError::InvalidOptions(format!(
6349                        "operator columns are not isometric within tolerance {tolerance}"
6350                    )));
6351                }
6352            }
6353        }
6354        Ok(projector)
6355    }
6356
6357    /// One-hot embedding of a selected basis into a compatible parent basis.
6358    pub fn from_embedding<Reduced, Parent>(reduced: &Reduced, parent: &Parent) -> Result<Self>
6359    where
6360        Reduced: Basis,
6361        Parent: Basis<State = Reduced::State>,
6362    {
6363        let columns = (0..reduced.len())
6364            .map(|column| {
6365                let state = reduced.state(column)?;
6366                Ok(vec![(parent.index(state)?, Complex64::new(1.0, 0.0))])
6367            })
6368            .collect::<Result<Vec<_>>>()?;
6369        Self::from_columns(parent.len(), columns)
6370    }
6371
6372    /// Isometric lift from any basis into an explicitly selected parent basis.
6373    ///
6374    /// The reduced basis owns the symmetry-reduction convention through
6375    /// [`Basis::reduction_image`]. Iterating the explicit parent states makes
6376    /// this equally useful for built-in and runtime symmetry sectors, fixed
6377    /// particle subspaces, and unrestricted parent spaces.
6378    pub fn between<Reduced, Parent>(reduced: &Reduced, parent: &Parent) -> Result<Self>
6379    where
6380        Reduced: Basis,
6381        Parent: Basis<State = Reduced::State>,
6382    {
6383        let mut columns = vec![Vec::<(usize, Complex64)>::new(); reduced.len()];
6384        for row in 0..parent.len() {
6385            let state = parent.state(row)?;
6386            let Some(image) = reduced.reduction_image(state)? else {
6387                continue;
6388            };
6389            let column = reduced.index(*image.representative())?;
6390            columns[column].push((row, image.amplitude()));
6391        }
6392        Self::from_columns(parent.len(), columns)
6393    }
6394
6395    pub fn from_general<Parent>(basis: &GeneralBasis<Parent>) -> Result<Self>
6396    where
6397        Parent: Basis,
6398        Parent::State: Hash + Ord + 'static,
6399    {
6400        Self::between(basis, &basis.parent)
6401    }
6402
6403    pub const fn source_dimension(&self) -> usize {
6404        self.source_dimension
6405    }
6406
6407    pub const fn reduced_dimension(&self) -> usize {
6408        self.reduced_dimension
6409    }
6410
6411    /// Apply the adjoint projector to a parent-space vector.
6412    pub fn project(&self, parent: &[Complex64], reduced: &mut [Complex64]) -> Result<()> {
6413        if parent.len() != self.source_dimension || reduced.len() != self.reduced_dimension {
6414            return Err(QmbedError::DimensionMismatch(
6415                "projector input or output length does not match".into(),
6416            ));
6417        }
6418        reduced.fill(Complex64::new(0.0, 0.0));
6419        for (column, reduced_value) in reduced.iter_mut().enumerate() {
6420            for position in self.column_offsets[column]..self.column_offsets[column + 1] {
6421                *reduced_value += self.values[position].conj() * parent[self.row_indices[position]];
6422            }
6423        }
6424        Ok(())
6425    }
6426
6427    pub fn lifted(&self, reduced: &[Complex64]) -> Result<Vec<Complex64>> {
6428        let mut parent = vec![Complex64::new(0.0, 0.0); self.source_dimension];
6429        self.apply(reduced, &mut parent)?;
6430        Ok(parent)
6431    }
6432
6433    pub fn projected(&self, parent: &[Complex64]) -> Result<Vec<Complex64>> {
6434        let mut reduced = vec![Complex64::new(0.0, 0.0); self.reduced_dimension];
6435        self.project(parent, &mut reduced)?;
6436        Ok(reduced)
6437    }
6438
6439    pub fn lift_batch(&self, reduced: &[Vec<Complex64>]) -> Result<Vec<Vec<Complex64>>> {
6440        reduced.iter().map(|state| self.lifted(state)).collect()
6441    }
6442
6443    pub fn project_batch(&self, parent: &[Vec<Complex64>]) -> Result<Vec<Vec<Complex64>>> {
6444        parent.iter().map(|state| self.projected(state)).collect()
6445    }
6446
6447    /// Lift a row-major reduced density matrix as `P ρ P†`.
6448    pub fn lift_density(&self, reduced: &[Complex64]) -> Result<Vec<Complex64>> {
6449        if reduced.len()
6450            != self
6451                .reduced_dimension
6452                .saturating_mul(self.reduced_dimension)
6453        {
6454            return Err(QmbedError::DimensionMismatch(
6455                "reduced density matrix does not match the projector domain".into(),
6456            ));
6457        }
6458        let mut parent =
6459            vec![Complex64::new(0.0, 0.0); self.source_dimension * self.source_dimension];
6460        for reduced_row in 0..self.reduced_dimension {
6461            for reduced_column in 0..self.reduced_dimension {
6462                let density = reduced[reduced_row * self.reduced_dimension + reduced_column];
6463                if density.norm_sqr() <= f64::EPSILON {
6464                    continue;
6465                }
6466                for left in self.column_offsets[reduced_row]..self.column_offsets[reduced_row + 1] {
6467                    for right in
6468                        self.column_offsets[reduced_column]..self.column_offsets[reduced_column + 1]
6469                    {
6470                        let row = self.row_indices[left];
6471                        let column = self.row_indices[right];
6472                        parent[row * self.source_dimension + column] +=
6473                            self.values[left] * density * self.values[right].conj();
6474                    }
6475                }
6476            }
6477        }
6478        Ok(parent)
6479    }
6480
6481    /// Frobenius norm of `(I - P P†) A P`, evaluated one reduced column at a
6482    /// time. Zero means the parent-space operator preserves this symmetry
6483    /// sector; no parent-space square projector is formed.
6484    pub fn symmetry_leakage_norm(&self, operator: &(impl LinearOperator + ?Sized)) -> Result<f64> {
6485        if operator.shape() != (self.source_dimension, self.source_dimension) {
6486            return Err(QmbedError::DimensionMismatch(
6487                "symmetry check requires a square parent-space operator".into(),
6488            ));
6489        }
6490        let mut total = 0.0;
6491        let mut reduced_basis = vec![Complex64::new(0.0, 0.0); self.reduced_dimension];
6492        let mut applied = vec![Complex64::new(0.0, 0.0); self.source_dimension];
6493        for column in 0..self.reduced_dimension {
6494            reduced_basis.fill(Complex64::new(0.0, 0.0));
6495            reduced_basis[column] = Complex64::new(1.0, 0.0);
6496            let lifted = self.lifted(&reduced_basis)?;
6497            operator.apply(&lifted, &mut applied)?;
6498            let projected = self.projected(&applied)?;
6499            let invariant_component = self.lifted(&projected)?;
6500            total += applied
6501                .iter()
6502                .zip(invariant_component)
6503                .map(|(value, invariant)| (*value - invariant).norm_sqr())
6504                .sum::<f64>();
6505        }
6506        Ok(total.sqrt())
6507    }
6508
6509    pub fn preserves_operator_symmetry(
6510        &self,
6511        operator: &(impl LinearOperator + ?Sized),
6512        tolerance: f64,
6513    ) -> Result<bool> {
6514        if !tolerance.is_finite() || tolerance < 0.0 {
6515            return Err(QmbedError::InvalidOptions(
6516                "symmetry-check tolerance must be finite and nonnegative".into(),
6517            ));
6518        }
6519        Ok(self.symmetry_leakage_norm(operator)? <= tolerance)
6520    }
6521}
6522
6523impl LinearOperator for BasisProjector {
6524    fn shape(&self) -> (usize, usize) {
6525        (self.source_dimension, self.reduced_dimension)
6526    }
6527
6528    fn format(&self) -> MatrixFormat {
6529        MatrixFormat::Csc
6530    }
6531
6532    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
6533        check_apply_shape(self.shape(), input, output)?;
6534        output.fill(Complex64::new(0.0, 0.0));
6535        for (column, &input_value) in input.iter().enumerate() {
6536            for position in self.column_offsets[column]..self.column_offsets[column + 1] {
6537                output[self.row_indices[position]] += self.values[position] * input_value;
6538            }
6539        }
6540        Ok(())
6541    }
6542
6543    fn stored_triplets(&self) -> Result<Option<Vec<(usize, usize, Complex64)>>> {
6544        let mut entries = Vec::with_capacity(self.values.len());
6545        for column in 0..self.reduced_dimension {
6546            for position in self.column_offsets[column]..self.column_offsets[column + 1] {
6547                entries.push((self.row_indices[position], column, self.values[position]));
6548            }
6549        }
6550        Ok(Some(entries))
6551    }
6552}
6553
6554/// Owned type-erased basis for frontends that choose a built-in basis at runtime.
6555///
6556/// Native Rust callers can continue using the concrete generic basis types.
6557/// Language bindings, configuration-driven workflows, and other runtime
6558/// frontends use this enum without duplicating the universal assembly logic.
6559#[derive(Clone, Debug)]
6560pub struct PackedTensorBasis {
6561    factors: Vec<PackedBasis>,
6562    dimensions: Vec<usize>,
6563    strides: Vec<usize>,
6564    dimension: usize,
6565}
6566
6567impl PackedTensorBasis {
6568    pub fn new(factors: impl IntoIterator<Item = PackedBasis>) -> Result<Self> {
6569        let mut flattened = Vec::new();
6570        for factor in factors {
6571            match factor {
6572                PackedBasis::Tensor(tensor) => flattened.extend(tensor.factors),
6573                other => flattened.push(other),
6574            }
6575        }
6576        if flattened.len() < 2 {
6577            return Err(QmbedError::InvalidSector(
6578                "a packed tensor basis requires at least two factors".into(),
6579            ));
6580        }
6581        let dimensions: Vec<_> = flattened.iter().map(Basis::len).collect();
6582        if dimensions.contains(&0) {
6583            return Err(QmbedError::InvalidSector(
6584                "tensor-basis factors must be nonempty".into(),
6585            ));
6586        }
6587        let mut strides = vec![1; dimensions.len()];
6588        let mut dimension = 1_usize;
6589        for index in (0..dimensions.len()).rev() {
6590            strides[index] = dimension;
6591            dimension = dimension.checked_mul(dimensions[index]).ok_or_else(|| {
6592                QmbedError::UnsupportedBackend("tensor-basis size overflow".into())
6593            })?;
6594        }
6595        Ok(Self {
6596            factors: flattened,
6597            dimensions,
6598            strides,
6599            dimension,
6600        })
6601    }
6602
6603    pub fn factors(&self) -> &[PackedBasis] {
6604        &self.factors
6605    }
6606
6607    pub fn dimensions(&self) -> &[usize] {
6608        &self.dimensions
6609    }
6610
6611    fn row(&self, state: u128) -> Result<usize> {
6612        let row = usize::try_from(state).map_err(|_| QmbedError::StateNotInBasis)?;
6613        (row < self.dimension)
6614            .then_some(row)
6615            .ok_or(QmbedError::StateNotInBasis)
6616    }
6617
6618    fn factor_rows(&self, state: u128) -> Result<Vec<usize>> {
6619        let row = self.row(state)?;
6620        Ok(self
6621            .dimensions
6622            .iter()
6623            .zip(&self.strides)
6624            .map(|(&dimension, &stride)| (row / stride) % dimension)
6625            .collect())
6626    }
6627
6628    fn operator_factors<'a>(&self, operator: &'a str) -> Result<Vec<&'a str>> {
6629        let factors: Vec<_> = operator.split('|').collect();
6630        if factors.len() != self.factors.len() {
6631            return Err(QmbedError::InvalidOperator(format!(
6632                "tensor operator has {} factors, expected {}",
6633                factors.len(),
6634                self.factors.len()
6635            )));
6636        }
6637        Ok(factors)
6638    }
6639}
6640
6641impl Basis for PackedTensorBasis {
6642    type State = u128;
6643
6644    fn len(&self) -> usize {
6645        self.dimension
6646    }
6647
6648    fn state(&self, index: usize) -> Result<Self::State> {
6649        if index >= self.dimension {
6650            return Err(QmbedError::StateNotInBasis);
6651        }
6652        Ok(index as u128)
6653    }
6654
6655    fn index(&self, state: Self::State) -> Result<usize> {
6656        self.row(state)
6657    }
6658
6659    fn apply_local(
6660        &self,
6661        state: Self::State,
6662        operator: &str,
6663        sites: &[usize],
6664    ) -> Result<Option<(Self::State, Complex64)>> {
6665        let transitions = self.apply_local_transitions(state, operator, sites)?;
6666        match transitions.as_slice() {
6667            [] => Ok(None),
6668            [transition] => Ok(Some(*transition)),
6669            _ => Err(QmbedError::UnsupportedBackend(
6670                "this tensor local action branches; use apply_local_transitions".into(),
6671            )),
6672        }
6673    }
6674
6675    fn apply_local_transitions(
6676        &self,
6677        state: Self::State,
6678        operator: &str,
6679        sites: &[usize],
6680    ) -> Result<LocalTransitions<Self::State>> {
6681        let operators = self.operator_factors(operator)?;
6682        let source_rows = self.factor_rows(state)?;
6683        let expected_arity: usize = operators.iter().map(|part| part.chars().count()).sum();
6684        if sites.len() != expected_arity {
6685            return Err(QmbedError::InvalidCoupling(format!(
6686                "tensor operator arity {expected_arity} does not match {} sites",
6687                sites.len()
6688            )));
6689        }
6690
6691        let mut partial = vec![(0_usize, Complex64::new(1.0, 0.0))];
6692        let mut site_offset = 0;
6693        for (factor_index, ((factor, part), &source_row)) in self
6694            .factors
6695            .iter()
6696            .zip(&operators)
6697            .zip(&source_rows)
6698            .enumerate()
6699        {
6700            let arity = part.chars().count();
6701            let source_state = factor.state(source_row)?;
6702            let transitions = if part.is_empty() {
6703                LocalTransitions::from_iter([(source_state, Complex64::new(1.0, 0.0))])
6704            } else {
6705                factor.apply_local_transitions(
6706                    source_state,
6707                    part,
6708                    &sites[site_offset..site_offset + arity],
6709                )?
6710            };
6711            site_offset += arity;
6712            let mut next = Vec::with_capacity(partial.len().saturating_mul(transitions.len()));
6713            for &(row, amplitude) in &partial {
6714                for &(target_state, local) in &transitions {
6715                    let target_row = factor.index(target_state)?;
6716                    next.push((
6717                        row + target_row * self.strides[factor_index],
6718                        amplitude * local,
6719                    ));
6720                }
6721            }
6722            partial = next;
6723        }
6724
6725        let mut accumulated = HashMap::<usize, Complex64>::new();
6726        for (row, amplitude) in partial {
6727            *accumulated.entry(row).or_insert(Complex64::new(0.0, 0.0)) += amplitude;
6728        }
6729        let mut transitions: Vec<_> = accumulated
6730            .into_iter()
6731            .filter(|(_, amplitude)| amplitude.norm() > f64::EPSILON)
6732            .collect();
6733        transitions.sort_unstable_by_key(|(row, _)| *row);
6734        Ok(transitions
6735            .into_iter()
6736            .map(|(row, amplitude)| (row as u128, amplitude))
6737            .collect())
6738    }
6739
6740    fn visit_local_unreduced_transitions<F>(
6741        &self,
6742        state: Self::State,
6743        operator: &str,
6744        sites: &[usize],
6745        mut visit: F,
6746    ) -> Result<()>
6747    where
6748        F: FnMut(Self::State, Complex64) -> Result<()>,
6749    {
6750        for (target, amplitude) in self.apply_local_transitions(state, operator, sites)? {
6751            visit(target, amplitude)?;
6752        }
6753        Ok(())
6754    }
6755
6756    fn operator_preserves_particle_sector(&self, operator: &str) -> Result<bool> {
6757        self.operator_factors(operator)?
6758            .into_iter()
6759            .zip(&self.factors)
6760            .try_fold(true, |preserves, (part, factor)| {
6761                Ok(preserves && factor.operator_preserves_particle_sector(part)?)
6762            })
6763    }
6764
6765    fn operator_preserves_particle_sector_on_sites(
6766        &self,
6767        operator: &str,
6768        sites: &[usize],
6769    ) -> Result<bool> {
6770        let operators = self.operator_factors(operator)?;
6771        let expected_arity: usize = operators.iter().map(|part| part.chars().count()).sum();
6772        if sites.len() != expected_arity {
6773            return Err(QmbedError::InvalidCoupling(
6774                "tensor operator arity does not match its sites".into(),
6775            ));
6776        }
6777        let mut offset = 0;
6778        for (part, factor) in operators.into_iter().zip(&self.factors) {
6779            let arity = part.chars().count();
6780            if !factor
6781                .operator_preserves_particle_sector_on_sites(part, &sites[offset..offset + arity])?
6782            {
6783                return Ok(false);
6784            }
6785            offset += arity;
6786        }
6787        Ok(true)
6788    }
6789}
6790
6791/// Runtime-erased matter basis tensored with one truncated photon mode.
6792///
6793/// Public states are rows in the unfiltered direct product, with the matter
6794/// row as the major index and photon occupation as the minor index. Keeping
6795/// these identifiers unchanged when a fixed-total-excitation filter is
6796/// applied lets [`BasisProjector`] connect the filtered and full spaces
6797/// without a photon-specific projection path.
6798#[derive(Clone, Debug)]
6799pub struct PackedPhotonBasis {
6800    matter: Box<PackedBasis>,
6801    photon: BosonBasis1D,
6802    photon_dimension: usize,
6803    full_dimension: usize,
6804    states: Vec<u128>,
6805    indices: HashMap<u128, usize>,
6806    total_excitations: Option<usize>,
6807}
6808
6809impl PackedPhotonBasis {
6810    /// Construct a matter-photon product with photon occupations
6811    /// `0..=photon_cutoff`, optionally filtered by a total additive quantum
6812    /// number.
6813    pub fn new(
6814        matter: PackedBasis,
6815        photon_cutoff: usize,
6816        total_excitations: Option<usize>,
6817    ) -> Result<Self> {
6818        let photon_dimension = photon_cutoff
6819            .checked_add(1)
6820            .ok_or_else(|| QmbedError::UnsupportedBackend("photon cutoff is too large".into()))?;
6821        let photon = BosonBasis1D::builder(1, photon_dimension).build()?;
6822        let full_dimension = matter.len().checked_mul(photon_dimension).ok_or_else(|| {
6823            QmbedError::UnsupportedBackend("matter-photon basis size overflow".into())
6824        })?;
6825
6826        let mut states = Vec::with_capacity(full_dimension);
6827        for matter_row in 0..matter.len() {
6828            let matter_state = matter.state(matter_row)?;
6829            let matter_excitations = matter.additive_quantum_number(matter_state)?;
6830            for photon_occupation in 0..photon_dimension {
6831                if total_excitations.is_none_or(|total| {
6832                    matter_excitations
6833                        .checked_add(photon_occupation)
6834                        .is_some_and(|value| value == total)
6835                }) {
6836                    states.push(Self::encode_product_state(
6837                        matter_state,
6838                        photon_occupation,
6839                        photon_dimension,
6840                    )?);
6841                }
6842            }
6843        }
6844        if states.is_empty() {
6845            return Err(QmbedError::InvalidSector(
6846                "the requested total-excitation photon sector is empty".into(),
6847            ));
6848        }
6849        let indices = states
6850            .iter()
6851            .copied()
6852            .enumerate()
6853            .map(|(index, state)| (state, index))
6854            .collect();
6855        Ok(Self {
6856            matter: Box::new(matter),
6857            photon,
6858            photon_dimension,
6859            full_dimension,
6860            states,
6861            indices,
6862            total_excitations,
6863        })
6864    }
6865
6866    pub fn matter(&self) -> &PackedBasis {
6867        &self.matter
6868    }
6869
6870    pub const fn photon(&self) -> &BosonBasis1D {
6871        &self.photon
6872    }
6873
6874    pub const fn photon_dimension(&self) -> usize {
6875        self.photon_dimension
6876    }
6877
6878    pub const fn full_dimension(&self) -> usize {
6879        self.full_dimension
6880    }
6881
6882    pub const fn total_excitations(&self) -> Option<usize> {
6883        self.total_excitations
6884    }
6885
6886    fn encode_product_state(
6887        matter_state: u128,
6888        photon_occupation: usize,
6889        photon_dimension: usize,
6890    ) -> Result<u128> {
6891        matter_state
6892            .checked_mul(photon_dimension as u128)
6893            .and_then(|value| value.checked_add(photon_occupation as u128))
6894            .ok_or_else(|| {
6895                QmbedError::UnsupportedBackend("matter-photon state encoding exceeds u128".into())
6896            })
6897    }
6898
6899    fn product_state(&self, state: u128) -> Result<(u128, usize)> {
6900        let photon_occupation = usize::try_from(state % self.photon_dimension as u128)
6901            .map_err(|_| QmbedError::StateNotInBasis)?;
6902        Ok((state / self.photon_dimension as u128, photon_occupation))
6903    }
6904
6905    fn product_rows(&self, state: u128) -> Result<(usize, usize)> {
6906        let (matter_state, photon_occupation) = self.product_state(state)?;
6907        Ok((self.matter.index(matter_state)?, photon_occupation))
6908    }
6909
6910    fn operator_parts<'a>(&self, operator: &'a str) -> Result<(&'a str, &'a str)> {
6911        let (matter, photon) = operator
6912            .split_once('|')
6913            .ok_or_else(|| QmbedError::InvalidOperator(operator.into()))?;
6914        if photon.contains('|') {
6915            return Err(QmbedError::InvalidOperator(operator.into()));
6916        }
6917        Ok((matter, photon))
6918    }
6919
6920    fn raw_local_transitions(
6921        &self,
6922        state: u128,
6923        operator: &str,
6924        sites: &[usize],
6925    ) -> Result<LocalTransitions<u128>> {
6926        self.index(state)?;
6927        let (matter_operator, photon_operator) = self.operator_parts(operator)?;
6928        let matter_arity = matter_operator.chars().count();
6929        let photon_arity = photon_operator.chars().count();
6930        if sites.len() != matter_arity + photon_arity {
6931            return Err(QmbedError::InvalidCoupling(format!(
6932                "matter-photon operator arity {} does not match {} sites",
6933                matter_arity + photon_arity,
6934                sites.len()
6935            )));
6936        }
6937        let (matter_row, photon_row) = self.product_rows(state)?;
6938        let matter_state = self.matter.state(matter_row)?;
6939        let photon_state = self.photon.state(photon_row)?;
6940        let matter_transitions = if matter_operator.is_empty() {
6941            LocalTransitions::from_iter([(matter_state, Complex64::new(1.0, 0.0))])
6942        } else {
6943            self.matter.apply_local_transitions(
6944                matter_state,
6945                matter_operator,
6946                &sites[..matter_arity],
6947            )?
6948        };
6949        let photon_transitions = if photon_operator.is_empty() {
6950            LocalTransitions::from_iter([(photon_state, Complex64::new(1.0, 0.0))])
6951        } else {
6952            self.photon.apply_local_transitions(
6953                photon_state,
6954                photon_operator,
6955                &sites[matter_arity..],
6956            )?
6957        };
6958
6959        let mut accumulated = HashMap::<u128, Complex64>::new();
6960        for &(target_matter, matter_amplitude) in &matter_transitions {
6961            for &(target_photon, photon_amplitude) in &photon_transitions {
6962                let target_photon_row = self.photon.index(target_photon)?;
6963                let target = Self::encode_product_state(
6964                    target_matter,
6965                    target_photon_row,
6966                    self.photon_dimension,
6967                )?;
6968                *accumulated
6969                    .entry(target)
6970                    .or_insert(Complex64::new(0.0, 0.0)) += matter_amplitude * photon_amplitude;
6971            }
6972        }
6973        let mut transitions: Vec<_> = accumulated
6974            .into_iter()
6975            .filter(|(_, amplitude)| amplitude.norm() > f64::EPSILON)
6976            .collect();
6977        transitions.sort_unstable_by_key(|(row, _)| *row);
6978        Ok(transitions.into())
6979    }
6980}
6981
6982impl Basis for PackedPhotonBasis {
6983    type State = u128;
6984
6985    fn len(&self) -> usize {
6986        self.states.len()
6987    }
6988
6989    fn state(&self, index: usize) -> Result<Self::State> {
6990        self.states
6991            .get(index)
6992            .copied()
6993            .ok_or(QmbedError::StateNotInBasis)
6994    }
6995
6996    fn index(&self, state: Self::State) -> Result<usize> {
6997        self.indices
6998            .get(&state)
6999            .copied()
7000            .ok_or(QmbedError::StateNotInBasis)
7001    }
7002
7003    fn apply_local(
7004        &self,
7005        state: Self::State,
7006        operator: &str,
7007        sites: &[usize],
7008    ) -> Result<Option<(Self::State, Complex64)>> {
7009        let transitions = self.apply_local_transitions(state, operator, sites)?;
7010        match transitions.as_slice() {
7011            [] => Ok(None),
7012            [transition] => Ok(Some(*transition)),
7013            _ => Err(QmbedError::UnsupportedBackend(
7014                "this matter-photon action branches; use apply_local_transitions".into(),
7015            )),
7016        }
7017    }
7018
7019    fn apply_local_transitions(
7020        &self,
7021        state: Self::State,
7022        operator: &str,
7023        sites: &[usize],
7024    ) -> Result<LocalTransitions<Self::State>> {
7025        Ok(self
7026            .raw_local_transitions(state, operator, sites)?
7027            .into_iter()
7028            .filter(|(target, _)| self.indices.contains_key(target))
7029            .collect())
7030    }
7031
7032    fn apply_local_unreduced_transitions(
7033        &self,
7034        state: Self::State,
7035        operator: &str,
7036        sites: &[usize],
7037    ) -> Result<LocalTransitions<Self::State>> {
7038        self.raw_local_transitions(state, operator, sites)
7039    }
7040
7041    fn visit_local_unreduced_transitions<F>(
7042        &self,
7043        state: Self::State,
7044        operator: &str,
7045        sites: &[usize],
7046        mut visit: F,
7047    ) -> Result<()>
7048    where
7049        F: FnMut(Self::State, Complex64) -> Result<()>,
7050    {
7051        for (target, amplitude) in self.raw_local_transitions(state, operator, sites)? {
7052            visit(target, amplitude)?;
7053        }
7054        Ok(())
7055    }
7056
7057    fn transition_orbit_size(&self, state: Self::State) -> Result<usize> {
7058        let (matter_state, _) = self.product_state(state)?;
7059        self.matter.transition_orbit_size(matter_state)
7060    }
7061
7062    fn reduction_image(&self, state: Self::State) -> Result<Option<ReductionImage<Self::State>>> {
7063        let (matter_state, photon_occupation) = self.product_state(state)?;
7064        let matter_excitations = match self.matter.additive_quantum_number(matter_state) {
7065            Ok(value) => value,
7066            Err(QmbedError::StateNotInBasis) => return Ok(None),
7067            Err(error) => return Err(error),
7068        };
7069        if self.total_excitations.is_some_and(|total| {
7070            matter_excitations
7071                .checked_add(photon_occupation)
7072                .is_none_or(|value| value != total)
7073        }) {
7074            return Ok(None);
7075        }
7076        let Some(matter_image) = self.matter.reduction_image(matter_state)? else {
7077            return Ok(None);
7078        };
7079        let representative = Self::encode_product_state(
7080            *matter_image.representative(),
7081            photon_occupation,
7082            self.photon_dimension,
7083        )?;
7084        if !self.indices.contains_key(&representative) {
7085            return Ok(None);
7086        }
7087        Ok(Some(ReductionImage::new(
7088            representative,
7089            matter_image.phase(),
7090            matter_image.orbit_size(),
7091        )?))
7092    }
7093
7094    fn operator_preserves_particle_sector(&self, operator: &str) -> Result<bool> {
7095        self.operator_parts(operator)?;
7096        Ok(self.total_excitations.is_none() || operator_number_change(operator)? == Some(0))
7097    }
7098}
7099
7100#[derive(Clone, Debug)]
7101pub enum PackedBasis {
7102    Spin(SpinBasis1D),
7103    Boson(BosonBasis1D),
7104    SpinlessFermion(SpinlessFermionBasis1D),
7105    SpinfulFermion(SpinfulFermionBasis1D),
7106    GeneralSpin(SpinBasisGeneral),
7107    GeneralBoson(BosonBasisGeneral),
7108    GeneralSpinlessFermion(SpinlessFermionBasisGeneral),
7109    GeneralSpinfulFermion(SpinfulFermionBasisGeneral),
7110    Tensor(Box<PackedTensorBasis>),
7111    Photon(Box<PackedPhotonBasis>),
7112    User(Box<UserBasisGeneral>),
7113    Reversed(Box<PackedBasis>),
7114}
7115
7116impl PackedBasis {
7117    /// Reverse the public basis-vector order without changing physical states.
7118    ///
7119    /// This is a general permutation view: `state`, `index`, transition row
7120    /// lookup, and universal operator assembly all observe the same order.
7121    pub fn reversed(self) -> Self {
7122        match self {
7123            Self::Reversed(inner) => *inner,
7124            basis => Self::Reversed(Box::new(basis)),
7125        }
7126    }
7127
7128    /// Additive occupation/excitation quantum number of a concrete state.
7129    ///
7130    /// This is the runtime-selected counterpart of the typed sector metadata
7131    /// used by concrete bases. Composite bases sum their factor quantum
7132    /// numbers, so consumers such as [`PackedPhotonBasis`] do not need to
7133    /// special-case spin, boson, or fermion encodings.
7134    pub fn additive_quantum_number(&self, state: u128) -> Result<usize> {
7135        fn digit_sum(mut state: u128, sites: usize, base: usize) -> Result<usize> {
7136            let base = base as u128;
7137            let mut total = 0_usize;
7138            for _ in 0..sites {
7139                total = total.checked_add((state % base) as usize).ok_or_else(|| {
7140                    QmbedError::UnsupportedBackend("additive quantum number overflow".into())
7141                })?;
7142                state /= base;
7143            }
7144            Ok(total)
7145        }
7146
7147        match self {
7148            Self::Spin(basis) => {
7149                basis.index(state)?;
7150                digit_sum(state, basis.sites(), usize::from(basis.spin_twice()) + 1)
7151            }
7152            Self::Boson(basis) => {
7153                basis.index(state)?;
7154                digit_sum(state, basis.sites(), basis.states_per_site())
7155            }
7156            Self::SpinlessFermion(basis) => {
7157                basis.index(state)?;
7158                Ok(state.count_ones() as usize)
7159            }
7160            Self::SpinfulFermion(basis) => {
7161                basis.index(state)?;
7162                Ok(state.count_ones() as usize)
7163            }
7164            Self::GeneralSpin(basis) => {
7165                basis.parent().index(state)?;
7166                digit_sum(
7167                    state,
7168                    basis.parent().sites(),
7169                    usize::from(basis.parent().spin_twice()) + 1,
7170                )
7171            }
7172            Self::GeneralBoson(basis) => {
7173                basis.parent().index(state)?;
7174                digit_sum(
7175                    state,
7176                    basis.parent().sites(),
7177                    basis.parent().states_per_site(),
7178                )
7179            }
7180            Self::GeneralSpinlessFermion(basis) => {
7181                basis.parent().index(state)?;
7182                Ok(state.count_ones() as usize)
7183            }
7184            Self::GeneralSpinfulFermion(basis) => {
7185                basis.parent().index(state)?;
7186                Ok(state.count_ones() as usize)
7187            }
7188            Self::Tensor(basis) => {
7189                basis.index(state)?;
7190                let rows = basis.factor_rows(state)?;
7191                basis
7192                    .factors()
7193                    .iter()
7194                    .zip(rows)
7195                    .try_fold(0_usize, |total, (factor, row)| {
7196                        let factor_state = factor.state(row)?;
7197                        total
7198                            .checked_add(factor.additive_quantum_number(factor_state)?)
7199                            .ok_or_else(|| {
7200                                QmbedError::UnsupportedBackend(
7201                                    "additive quantum number overflow".into(),
7202                                )
7203                            })
7204                    })
7205            }
7206            Self::Photon(basis) => {
7207                let (matter_state, photon_row) = basis.product_state(state)?;
7208                basis
7209                    .matter
7210                    .additive_quantum_number(matter_state)?
7211                    .checked_add(photon_row)
7212                    .ok_or_else(|| {
7213                        QmbedError::UnsupportedBackend("additive quantum number overflow".into())
7214                    })
7215            }
7216            Self::User(basis) => {
7217                basis.parent().index(state)?;
7218                Err(QmbedError::UnsupportedBackend(
7219                    "a callback-defined basis must provide an additive quantum-number callback"
7220                        .into(),
7221                ))
7222            }
7223            Self::Reversed(basis) => basis.additive_quantum_number(state),
7224        }
7225    }
7226}
7227
7228impl From<SpinBasis1D> for PackedBasis {
7229    fn from(basis: SpinBasis1D) -> Self {
7230        Self::Spin(basis)
7231    }
7232}
7233
7234impl From<BosonBasis1D> for PackedBasis {
7235    fn from(basis: BosonBasis1D) -> Self {
7236        Self::Boson(basis)
7237    }
7238}
7239
7240impl From<SpinlessFermionBasis1D> for PackedBasis {
7241    fn from(basis: SpinlessFermionBasis1D) -> Self {
7242        Self::SpinlessFermion(basis)
7243    }
7244}
7245
7246impl From<SpinfulFermionBasis1D> for PackedBasis {
7247    fn from(basis: SpinfulFermionBasis1D) -> Self {
7248        Self::SpinfulFermion(basis)
7249    }
7250}
7251
7252impl From<SpinBasisGeneral> for PackedBasis {
7253    fn from(basis: SpinBasisGeneral) -> Self {
7254        Self::GeneralSpin(basis)
7255    }
7256}
7257
7258impl From<BosonBasisGeneral> for PackedBasis {
7259    fn from(basis: BosonBasisGeneral) -> Self {
7260        Self::GeneralBoson(basis)
7261    }
7262}
7263
7264impl From<SpinlessFermionBasisGeneral> for PackedBasis {
7265    fn from(basis: SpinlessFermionBasisGeneral) -> Self {
7266        Self::GeneralSpinlessFermion(basis)
7267    }
7268}
7269
7270impl From<SpinfulFermionBasisGeneral> for PackedBasis {
7271    fn from(basis: SpinfulFermionBasisGeneral) -> Self {
7272        Self::GeneralSpinfulFermion(basis)
7273    }
7274}
7275
7276impl From<PackedTensorBasis> for PackedBasis {
7277    fn from(basis: PackedTensorBasis) -> Self {
7278        Self::Tensor(Box::new(basis))
7279    }
7280}
7281
7282impl From<PackedPhotonBasis> for PackedBasis {
7283    fn from(basis: PackedPhotonBasis) -> Self {
7284        Self::Photon(Box::new(basis))
7285    }
7286}
7287
7288impl From<UserBasisGeneral> for PackedBasis {
7289    fn from(basis: UserBasisGeneral) -> Self {
7290        Self::User(Box::new(basis))
7291    }
7292}
7293
7294impl Basis for PackedBasis {
7295    type State = u128;
7296
7297    fn len(&self) -> usize {
7298        match self {
7299            Self::Spin(basis) => basis.len(),
7300            Self::Boson(basis) => basis.len(),
7301            Self::SpinlessFermion(basis) => basis.len(),
7302            Self::SpinfulFermion(basis) => basis.len(),
7303            Self::GeneralSpin(basis) => basis.len(),
7304            Self::GeneralBoson(basis) => basis.len(),
7305            Self::GeneralSpinlessFermion(basis) => basis.len(),
7306            Self::GeneralSpinfulFermion(basis) => basis.len(),
7307            Self::Tensor(basis) => basis.len(),
7308            Self::Photon(basis) => basis.len(),
7309            Self::User(basis) => basis.len(),
7310            Self::Reversed(basis) => basis.len(),
7311        }
7312    }
7313
7314    fn state(&self, index: usize) -> Result<Self::State> {
7315        match self {
7316            Self::Spin(basis) => basis.state(index),
7317            Self::Boson(basis) => basis.state(index),
7318            Self::SpinlessFermion(basis) => basis.state(index),
7319            Self::SpinfulFermion(basis) => basis.state(index),
7320            Self::GeneralSpin(basis) => basis.state(index),
7321            Self::GeneralBoson(basis) => basis.state(index),
7322            Self::GeneralSpinlessFermion(basis) => basis.state(index),
7323            Self::GeneralSpinfulFermion(basis) => basis.state(index),
7324            Self::Tensor(basis) => basis.state(index),
7325            Self::Photon(basis) => basis.state(index),
7326            Self::User(basis) => basis.state(index),
7327            Self::Reversed(basis) => {
7328                let reversed = basis
7329                    .len()
7330                    .checked_sub(index + 1)
7331                    .ok_or(QmbedError::StateNotInBasis)?;
7332                basis.state(reversed)
7333            }
7334        }
7335    }
7336
7337    fn index(&self, state: Self::State) -> Result<usize> {
7338        match self {
7339            Self::Spin(basis) => basis.index(state),
7340            Self::Boson(basis) => basis.index(state),
7341            Self::SpinlessFermion(basis) => basis.index(state),
7342            Self::SpinfulFermion(basis) => basis.index(state),
7343            Self::GeneralSpin(basis) => basis.index(state),
7344            Self::GeneralBoson(basis) => basis.index(state),
7345            Self::GeneralSpinlessFermion(basis) => basis.index(state),
7346            Self::GeneralSpinfulFermion(basis) => basis.index(state),
7347            Self::Tensor(basis) => basis.index(state),
7348            Self::Photon(basis) => basis.index(state),
7349            Self::User(basis) => basis.index(state),
7350            Self::Reversed(basis) => basis.index(state).map(|index| basis.len() - index - 1),
7351        }
7352    }
7353
7354    fn apply_local(
7355        &self,
7356        state: Self::State,
7357        operator: &str,
7358        sites: &[usize],
7359    ) -> Result<Option<(Self::State, Complex64)>> {
7360        match self {
7361            Self::Spin(basis) => basis.apply_local(state, operator, sites),
7362            Self::Boson(basis) => basis.apply_local(state, operator, sites),
7363            Self::SpinlessFermion(basis) => basis.apply_local(state, operator, sites),
7364            Self::SpinfulFermion(basis) => basis.apply_local(state, operator, sites),
7365            Self::GeneralSpin(basis) => basis.apply_local(state, operator, sites),
7366            Self::GeneralBoson(basis) => basis.apply_local(state, operator, sites),
7367            Self::GeneralSpinlessFermion(basis) => basis.apply_local(state, operator, sites),
7368            Self::GeneralSpinfulFermion(basis) => basis.apply_local(state, operator, sites),
7369            Self::Tensor(basis) => basis.apply_local(state, operator, sites),
7370            Self::Photon(basis) => basis.apply_local(state, operator, sites),
7371            Self::User(basis) => basis.apply_local(state, operator, sites),
7372            Self::Reversed(basis) => basis.apply_local(state, operator, sites),
7373        }
7374    }
7375
7376    fn apply_local_transitions(
7377        &self,
7378        state: Self::State,
7379        operator: &str,
7380        sites: &[usize],
7381    ) -> Result<LocalTransitions<Self::State>> {
7382        match self {
7383            Self::Spin(basis) => basis.apply_local_transitions(state, operator, sites),
7384            Self::Boson(basis) => basis.apply_local_transitions(state, operator, sites),
7385            Self::SpinlessFermion(basis) => basis.apply_local_transitions(state, operator, sites),
7386            Self::SpinfulFermion(basis) => basis.apply_local_transitions(state, operator, sites),
7387            Self::GeneralSpin(basis) => basis.apply_local_transitions(state, operator, sites),
7388            Self::GeneralBoson(basis) => basis.apply_local_transitions(state, operator, sites),
7389            Self::GeneralSpinlessFermion(basis) => {
7390                basis.apply_local_transitions(state, operator, sites)
7391            }
7392            Self::GeneralSpinfulFermion(basis) => {
7393                basis.apply_local_transitions(state, operator, sites)
7394            }
7395            Self::Tensor(basis) => basis.apply_local_transitions(state, operator, sites),
7396            Self::Photon(basis) => basis.apply_local_transitions(state, operator, sites),
7397            Self::User(basis) => basis.apply_local_transitions(state, operator, sites),
7398            Self::Reversed(basis) => basis.apply_local_transitions(state, operator, sites),
7399        }
7400    }
7401
7402    fn apply_local_unreduced_transitions(
7403        &self,
7404        state: Self::State,
7405        operator: &str,
7406        sites: &[usize],
7407    ) -> Result<LocalTransitions<Self::State>> {
7408        match self {
7409            Self::Spin(basis) => basis.apply_local_unreduced_transitions(state, operator, sites),
7410            Self::Boson(basis) => basis.apply_local_unreduced_transitions(state, operator, sites),
7411            Self::SpinlessFermion(basis) => {
7412                basis.apply_local_unreduced_transitions(state, operator, sites)
7413            }
7414            Self::SpinfulFermion(basis) => {
7415                basis.apply_local_unreduced_transitions(state, operator, sites)
7416            }
7417            Self::GeneralSpin(basis) => {
7418                basis.apply_local_unreduced_transitions(state, operator, sites)
7419            }
7420            Self::GeneralBoson(basis) => {
7421                basis.apply_local_unreduced_transitions(state, operator, sites)
7422            }
7423            Self::GeneralSpinlessFermion(basis) => {
7424                basis.apply_local_unreduced_transitions(state, operator, sites)
7425            }
7426            Self::GeneralSpinfulFermion(basis) => {
7427                basis.apply_local_unreduced_transitions(state, operator, sites)
7428            }
7429            Self::Tensor(basis) => basis.apply_local_unreduced_transitions(state, operator, sites),
7430            Self::Photon(basis) => basis.apply_local_unreduced_transitions(state, operator, sites),
7431            Self::User(basis) => basis.apply_local_unreduced_transitions(state, operator, sites),
7432            Self::Reversed(basis) => {
7433                basis.apply_local_unreduced_transitions(state, operator, sites)
7434            }
7435        }
7436    }
7437
7438    fn visit_local_unreduced_transitions<F>(
7439        &self,
7440        state: Self::State,
7441        operator: &str,
7442        sites: &[usize],
7443        visit: F,
7444    ) -> Result<()>
7445    where
7446        Self: Sized,
7447        F: FnMut(Self::State, Complex64) -> Result<()>,
7448    {
7449        match self {
7450            Self::Spin(basis) => {
7451                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7452            }
7453            Self::Boson(basis) => {
7454                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7455            }
7456            Self::SpinlessFermion(basis) => {
7457                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7458            }
7459            Self::SpinfulFermion(basis) => {
7460                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7461            }
7462            Self::GeneralSpin(basis) => {
7463                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7464            }
7465            Self::GeneralBoson(basis) => {
7466                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7467            }
7468            Self::GeneralSpinlessFermion(basis) => {
7469                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7470            }
7471            Self::GeneralSpinfulFermion(basis) => {
7472                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7473            }
7474            Self::Tensor(basis) => {
7475                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7476            }
7477            Self::Photon(basis) => {
7478                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7479            }
7480            Self::User(basis) => {
7481                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7482            }
7483            Self::Reversed(basis) => {
7484                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7485            }
7486        }
7487    }
7488
7489    fn visit_preparsed_local_unreduced_transitions<F>(
7490        &self,
7491        state: Self::State,
7492        operator: &str,
7493        symbols: &[char],
7494        split: Option<usize>,
7495        sites: &[usize],
7496        visit: F,
7497    ) -> Result<()>
7498    where
7499        Self: Sized,
7500        F: FnMut(Self::State, Complex64) -> Result<()>,
7501    {
7502        match self {
7503            Self::Spin(basis) => basis.visit_preparsed_local_unreduced_transitions(
7504                state, operator, symbols, split, sites, visit,
7505            ),
7506            Self::Boson(basis) => basis.visit_preparsed_local_unreduced_transitions(
7507                state, operator, symbols, split, sites, visit,
7508            ),
7509            Self::SpinlessFermion(basis) => basis.visit_preparsed_local_unreduced_transitions(
7510                state, operator, symbols, split, sites, visit,
7511            ),
7512            Self::SpinfulFermion(basis) => basis.visit_preparsed_local_unreduced_transitions(
7513                state, operator, symbols, split, sites, visit,
7514            ),
7515            Self::GeneralSpin(basis) => basis.visit_preparsed_local_unreduced_transitions(
7516                state, operator, symbols, split, sites, visit,
7517            ),
7518            Self::GeneralBoson(basis) => basis.visit_preparsed_local_unreduced_transitions(
7519                state, operator, symbols, split, sites, visit,
7520            ),
7521            Self::GeneralSpinlessFermion(basis) => basis
7522                .visit_preparsed_local_unreduced_transitions(
7523                    state, operator, symbols, split, sites, visit,
7524                ),
7525            Self::GeneralSpinfulFermion(basis) => basis
7526                .visit_preparsed_local_unreduced_transitions(
7527                    state, operator, symbols, split, sites, visit,
7528                ),
7529            Self::Tensor(basis) => {
7530                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7531            }
7532            Self::Photon(basis) => {
7533                basis.visit_local_unreduced_transitions(state, operator, sites, visit)
7534            }
7535            Self::User(basis) => basis.visit_preparsed_local_unreduced_transitions(
7536                state, operator, symbols, split, sites, visit,
7537            ),
7538            Self::Reversed(basis) => basis.visit_preparsed_local_unreduced_transitions(
7539                state, operator, symbols, split, sites, visit,
7540            ),
7541        }
7542    }
7543
7544    fn transition_orbit_size(&self, state: Self::State) -> Result<usize> {
7545        match self {
7546            Self::Spin(basis) => basis.transition_orbit_size(state),
7547            Self::Boson(basis) => basis.transition_orbit_size(state),
7548            Self::SpinlessFermion(basis) => basis.transition_orbit_size(state),
7549            Self::SpinfulFermion(basis) => basis.transition_orbit_size(state),
7550            Self::GeneralSpin(basis) => basis.transition_orbit_size(state),
7551            Self::GeneralBoson(basis) => basis.transition_orbit_size(state),
7552            Self::GeneralSpinlessFermion(basis) => basis.transition_orbit_size(state),
7553            Self::GeneralSpinfulFermion(basis) => basis.transition_orbit_size(state),
7554            Self::Tensor(basis) => basis.transition_orbit_size(state),
7555            Self::Photon(basis) => basis.transition_orbit_size(state),
7556            Self::User(basis) => basis.transition_orbit_size(state),
7557            Self::Reversed(basis) => basis.transition_orbit_size(state),
7558        }
7559    }
7560
7561    fn reduction_image(&self, state: Self::State) -> Result<Option<ReductionImage<Self::State>>> {
7562        match self {
7563            Self::Spin(basis) => basis.reduction_image(state),
7564            Self::Boson(basis) => basis.reduction_image(state),
7565            Self::SpinlessFermion(basis) => basis.reduction_image(state),
7566            Self::SpinfulFermion(basis) => basis.reduction_image(state),
7567            Self::GeneralSpin(basis) => basis.reduction_image(state),
7568            Self::GeneralBoson(basis) => basis.reduction_image(state),
7569            Self::GeneralSpinlessFermion(basis) => basis.reduction_image(state),
7570            Self::GeneralSpinfulFermion(basis) => basis.reduction_image(state),
7571            Self::Tensor(basis) => basis.reduction_image(state),
7572            Self::Photon(basis) => basis.reduction_image(state),
7573            Self::User(basis) => basis.reduction_image(state),
7574            Self::Reversed(basis) => basis.reduction_image(state),
7575        }
7576    }
7577
7578    fn reduce_transition(
7579        &self,
7580        state: Self::State,
7581        source_orbit_size: usize,
7582    ) -> Result<Option<(Self::State, Complex64)>> {
7583        match self {
7584            Self::Spin(basis) => basis.reduce_transition(state, source_orbit_size),
7585            Self::Boson(basis) => basis.reduce_transition(state, source_orbit_size),
7586            Self::SpinlessFermion(basis) => basis.reduce_transition(state, source_orbit_size),
7587            Self::SpinfulFermion(basis) => basis.reduce_transition(state, source_orbit_size),
7588            Self::GeneralSpin(basis) => basis.reduce_transition(state, source_orbit_size),
7589            Self::GeneralBoson(basis) => basis.reduce_transition(state, source_orbit_size),
7590            Self::GeneralSpinlessFermion(basis) => {
7591                basis.reduce_transition(state, source_orbit_size)
7592            }
7593            Self::GeneralSpinfulFermion(basis) => basis.reduce_transition(state, source_orbit_size),
7594            Self::Tensor(basis) => basis.reduce_transition(state, source_orbit_size),
7595            Self::Photon(basis) => basis.reduce_transition(state, source_orbit_size),
7596            Self::User(basis) => basis.reduce_transition(state, source_orbit_size),
7597            Self::Reversed(basis) => basis.reduce_transition(state, source_orbit_size),
7598        }
7599    }
7600
7601    fn index_transition(
7602        &self,
7603        state: Self::State,
7604        source_orbit_size: usize,
7605    ) -> Result<Option<(usize, Complex64)>> {
7606        match self {
7607            Self::Spin(basis) => basis.index_transition(state, source_orbit_size),
7608            Self::Boson(basis) => basis.index_transition(state, source_orbit_size),
7609            Self::SpinlessFermion(basis) => basis.index_transition(state, source_orbit_size),
7610            Self::SpinfulFermion(basis) => basis.index_transition(state, source_orbit_size),
7611            Self::GeneralSpin(basis) => basis.index_transition(state, source_orbit_size),
7612            Self::GeneralBoson(basis) => basis.index_transition(state, source_orbit_size),
7613            Self::GeneralSpinlessFermion(basis) => basis.index_transition(state, source_orbit_size),
7614            Self::GeneralSpinfulFermion(basis) => basis.index_transition(state, source_orbit_size),
7615            Self::Tensor(basis) => basis.index_transition(state, source_orbit_size),
7616            Self::Photon(basis) => basis.index_transition(state, source_orbit_size),
7617            Self::User(basis) => basis.index_transition(state, source_orbit_size),
7618            Self::Reversed(basis) => Ok(basis
7619                .index_transition(state, source_orbit_size)?
7620                .map(|(index, amplitude)| (basis.len() - index - 1, amplitude))),
7621        }
7622    }
7623
7624    fn operator_preserves_particle_sector(&self, operator: &str) -> Result<bool> {
7625        match self {
7626            Self::Spin(basis) => basis.operator_preserves_particle_sector(operator),
7627            Self::Boson(basis) => basis.operator_preserves_particle_sector(operator),
7628            Self::SpinlessFermion(basis) => basis.operator_preserves_particle_sector(operator),
7629            Self::SpinfulFermion(basis) => basis.operator_preserves_particle_sector(operator),
7630            Self::GeneralSpin(basis) => basis.operator_preserves_particle_sector(operator),
7631            Self::GeneralBoson(basis) => basis.operator_preserves_particle_sector(operator),
7632            Self::GeneralSpinlessFermion(basis) => {
7633                basis.operator_preserves_particle_sector(operator)
7634            }
7635            Self::GeneralSpinfulFermion(basis) => {
7636                basis.operator_preserves_particle_sector(operator)
7637            }
7638            Self::Tensor(basis) => basis.operator_preserves_particle_sector(operator),
7639            Self::Photon(basis) => basis.operator_preserves_particle_sector(operator),
7640            Self::User(basis) => basis.operator_preserves_particle_sector(operator),
7641            Self::Reversed(basis) => basis.operator_preserves_particle_sector(operator),
7642        }
7643    }
7644
7645    fn operator_preserves_particle_sector_on_sites(
7646        &self,
7647        operator: &str,
7648        sites: &[usize],
7649    ) -> Result<bool> {
7650        match self {
7651            Self::Spin(basis) => basis.operator_preserves_particle_sector_on_sites(operator, sites),
7652            Self::Boson(basis) => {
7653                basis.operator_preserves_particle_sector_on_sites(operator, sites)
7654            }
7655            Self::SpinlessFermion(basis) => {
7656                basis.operator_preserves_particle_sector_on_sites(operator, sites)
7657            }
7658            Self::SpinfulFermion(basis) => {
7659                basis.operator_preserves_particle_sector_on_sites(operator, sites)
7660            }
7661            Self::GeneralSpin(basis) => {
7662                basis.operator_preserves_particle_sector_on_sites(operator, sites)
7663            }
7664            Self::GeneralBoson(basis) => {
7665                basis.operator_preserves_particle_sector_on_sites(operator, sites)
7666            }
7667            Self::GeneralSpinlessFermion(basis) => {
7668                basis.operator_preserves_particle_sector_on_sites(operator, sites)
7669            }
7670            Self::GeneralSpinfulFermion(basis) => {
7671                basis.operator_preserves_particle_sector_on_sites(operator, sites)
7672            }
7673            Self::Tensor(basis) => {
7674                basis.operator_preserves_particle_sector_on_sites(operator, sites)
7675            }
7676            Self::Photon(basis) => {
7677                basis.operator_preserves_particle_sector_on_sites(operator, sites)
7678            }
7679            Self::User(basis) => basis.operator_preserves_particle_sector_on_sites(operator, sites),
7680            Self::Reversed(basis) => {
7681                basis.operator_preserves_particle_sector_on_sites(operator, sites)
7682            }
7683        }
7684    }
7685}