Skip to main content

qudit_core/
radices.rs

1// TODO: remove all mentions of the old macro radices!
2// TODO: Order mods: implementations; python; strategies; test
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::radix::Radix;
8use crate::{CompactStorage, CompactVec, QuditSystem};
9
10/// The number of basis states for each qudit (or dit) in a quantum system.
11///
12/// This object represents the radix -- sometimes called the base, level
13/// or ditness -- of each qudit in a qudit system or dit in a classical system,
14/// and is implemented as a sequence of unsigned, byte-sized integers.
15/// A qubit is a two-level qudit or a qudit with radix two, while a
16/// qutrit is a three-level qudit. Two qutrits together are represented
17/// by the [3, 3] radices object.
18///
19/// No radix can be less than 2, as this would not be a valid.
20/// As each radix is represented by a byte, this implementation does not
21/// support individual radices greater than 255.
22///
23/// ## Ordering and Endianness
24///
25/// Indices are counted left to right, for example a [2, 3] qudit
26/// radices is interpreted as a qubit as the first qudit and a qutrit as
27/// the second one. OpenQudit uses big-endian ordering, so the qubit in
28/// the previous example is the most significant qudit and the qutrit is
29/// the least significant qudit. For example, in the same system, a state
30/// |10> would be represented by the decimal number 3.
31#[derive(Hash, PartialEq, Eq, Clone, Serialize, Deserialize)]
32pub struct Radices(CompactVec<Radix>);
33
34impl Radices {
35    /// Constructs a Radices object from the given input.
36    ///
37    /// # Arguments
38    ///
39    /// * `radices` - A slice detailing the radices of a qudit system.
40    ///
41    /// # Panics
42    ///
43    /// If radices does not represent a valid system. This can happen
44    /// if any of the radices are 0 or 1.
45    ///
46    /// # Examples
47    ///
48    /// ```
49    /// # use qudit_core::Radices;
50    /// let three_qubits = Radices::new([2; 3]);
51    /// let qubit_qutrit = Radices::new([2, 3]);
52    /// ```
53    pub fn new<T: Into<Radices>>(radices: T) -> Self {
54        radices.into()
55    }
56
57    /// Attempts to determine the radices for a qudit system with given dimension.
58    ///
59    /// It first tries powers of two, failing that powers of three. If neither
60    /// then it will return a QuditRadices with only one qudit of dimension
61    /// equal to the input.
62    pub fn guess(dimension: usize) -> Radices {
63        if dimension < 2 {
64            panic!("Invalid dimension in Radices");
65        }
66
67        // Is a power of two?
68        if dimension & (dimension - 1) == 0 {
69            let num_qudits = dimension.trailing_zeros() as usize;
70            return vec![2; num_qudits].into();
71        }
72
73        let mut n = dimension;
74        let mut power = 0;
75        while n > 1 {
76            if !n.is_multiple_of(3) {
77                return [dimension].into();
78            }
79            n /= 3;
80            power += 1;
81        }
82        vec![3; power].into()
83    }
84
85    /// Construct the expanded form of an index in this numbering system.
86    ///
87    /// # Arguments
88    ///
89    /// * `index` - The number to expand.
90    ///
91    /// # Returns
92    ///
93    /// A vector of coefficients for each qudit in the system. Note that
94    /// the coefficients are in big-endian order, that is, the first
95    /// coefficient is for the most significant qudit.
96    ///
97    /// # Panics
98    ///
99    /// If `index` is too large for this system, that is, if it is greater
100    /// than the product of the radices.
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// # use qudit_core::Radices;
106    ///
107    /// let hybrid_system = Radices::new([2, 3]);
108    /// assert_eq!(hybrid_system.expand(3), vec![1, 0]);
109    ///
110    /// let four_qubits = Radices::new([2, 2, 2, 2]);
111    /// assert_eq!(four_qubits.expand(7), vec![0, 1, 1, 1]);
112    ///
113    /// let two_qutrits = Radices::new([3, 3]);
114    /// assert_eq!(two_qutrits.expand(7), vec![2, 1]);
115    ///
116    /// let hybrid_system = Radices::new([3, 2, 3]);
117    /// assert_eq!(hybrid_system.expand(17), vec![2, 1, 2]);
118    /// ```
119    ///
120    /// # See Also
121    ///
122    /// * [Self::compress] - The inverse of this function.
123    /// * [Self::place_values] - The place values for each position in the expansion.
124    #[track_caller]
125    pub fn expand(&self, mut index: usize) -> Vec<usize> {
126        if index >= self.dimension() {
127            panic!(
128                "Provided index {} is too large for this system with radices: {:#?}",
129                index, self
130            );
131        }
132
133        let mut expansion = vec![0; self.num_qudits()];
134
135        for (idx, radix) in self.iter().enumerate().rev() {
136            let casted_radix: usize = (*radix).into();
137            let coef: usize = index % casted_radix;
138            index -= coef;
139            index /= casted_radix;
140            expansion[idx] = coef;
141        }
142
143        expansion
144    }
145
146    /// Destruct an expanded form of an index back into its base 10 number.
147    ///
148    /// # Arguments
149    ///
150    /// * `expansion` - The expansion to compress.
151    ///
152    /// # Panics
153    ///
154    /// If `expansion` has a mismatch in length or radices.
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// # use qudit_core::Radices;
160    ///
161    /// let hybrid_system = Radices::new([2, 3]);
162    /// assert_eq!(hybrid_system.compress(&vec![1, 0]), 3);
163    ///
164    /// let four_qubits = Radices::new([2, 2, 2, 2]);
165    /// assert_eq!(four_qubits.compress(&vec![0, 1, 1, 1]), 7);
166    ///
167    /// let two_qutrits = Radices::new([3, 3]);
168    /// assert_eq!(two_qutrits.compress(&vec![2, 1]), 7);
169    ///
170    /// let hybrid_system = Radices::new([3, 2, 3]);
171    /// assert_eq!(hybrid_system.compress(&vec![2, 1, 2]), 17);
172    /// ```
173    ///
174    /// # See Also
175    ///
176    /// * [Self::expand] - The inverse of this function.
177    /// * [Self::place_values] - The place values for each position in the expansion.
178    #[track_caller]
179    pub fn compress(&self, expansion: &[usize]) -> usize {
180        if self.len() != expansion.len() {
181            panic!("Invalid expansion: incorrect number of qudits.")
182        }
183
184        if expansion
185            .iter()
186            .enumerate()
187            .any(|(index, coef)| *coef >= usize::from(self[index]))
188        {
189            panic!("Invalid expansion: mismatch in qudit radices.")
190        }
191
192        if expansion.is_empty() {
193            return 0;
194        }
195
196        let mut acm_val = expansion[self.num_qudits() - 1];
197        let mut acm_base = usize::from(self[self.num_qudits() - 1]);
198
199        for coef_index in (0..expansion.len() - 1).rev() {
200            let coef = expansion[coef_index];
201            acm_val += coef * acm_base;
202            acm_base *= usize::from(self[coef_index]);
203        }
204
205        acm_val
206    }
207
208    /// Calculate the value for each expansion position in this numbering system.
209    ///
210    /// # Examples
211    ///
212    /// ```
213    /// # use qudit_core::Radices;
214    ///
215    /// let two_qubits = Radices::new([2, 2]);
216    /// assert_eq!(two_qubits.place_values(), vec![2, 1]);
217    ///
218    /// let two_qutrits = Radices::new([3, 3]);
219    /// assert_eq!(two_qutrits.place_values(), vec![3, 1]);
220    ///
221    /// let hybrid_system = Radices::new([3, 2, 3]);
222    /// assert_eq!(hybrid_system.place_values(), vec![6, 3, 1]);
223    /// ```
224    ///
225    /// # See Also
226    /// * [Self::expand] - Expand an decimal value into this numbering system.
227    /// * [Self::compress] - Compress an expansion in this numbering system to decimal.
228    pub fn place_values(&self) -> Vec<usize> {
229        let mut place_values = vec![0; self.num_qudits()];
230        let mut acm = 1;
231        for (idx, r) in self.iter().enumerate().rev() {
232            place_values[idx] = acm;
233            acm *= usize::from(*r);
234        }
235        place_values
236    }
237
238    /// Concatenates two QuditRadices objects into a new object.
239    ///
240    /// # Arguments
241    ///
242    /// * `other` - The other QuditRadices object to concatenate with `self`.
243    ///
244    /// # Examples
245    ///
246    /// ```
247    /// # use qudit_core::Radices;
248    ///
249    /// let two_qubits = Radices::new([2, 2]);
250    /// let two_qutrits = Radices::new([3, 3]);
251    /// let four_qudits = Radices::new([2, 2, 3, 3]);
252    /// assert_eq!(two_qubits.concat(&two_qutrits), four_qudits);
253    ///
254    /// let hybrid_system = Radices::new([3, 2, 3]);
255    /// let two_qutrits = Radices::new([3, 3]);
256    /// let five_qudits = Radices::new([3, 2, 3, 3, 3]);
257    /// assert_eq!(hybrid_system.concat(&two_qutrits), five_qudits);
258    /// ```
259    #[inline(always)]
260    pub fn concat(&self, other: &Radices) -> Radices {
261        self.iter().chain(other.iter()).copied().collect()
262    }
263
264    /// Returns the number of each radix in the system.
265    ///
266    /// # Examples
267    ///
268    /// ```
269    /// # use qudit_core::Radices;
270    /// # use std::collections::HashMap;
271    /// let two_qubits = Radices::new([2, 2]);
272    /// let counts = two_qubits.counts();
273    /// assert_eq!(counts.get(&(2.into())), Some(&2));
274    ///
275    /// let two_qutrits = Radices::new([3, 3]);
276    /// let counts = two_qutrits.counts();
277    /// assert_eq!(counts.get(&(3.into())), Some(&2));
278    ///
279    /// let hybrid_system = Radices::new([3, 2, 3]);
280    /// let counts = hybrid_system.counts();
281    /// assert_eq!(counts.get(&(3.into())), Some(&2));
282    /// assert_eq!(counts.get(&(2.into())), Some(&1));
283    /// ```
284    pub fn counts(&self) -> HashMap<Radix, usize> {
285        let mut counts = HashMap::new();
286        for radix in self.iter() {
287            *counts.entry(*radix).or_insert(0) += 1;
288        }
289        counts
290    }
291
292    /// Returns the length of the radices object.
293    pub fn len(&self) -> usize {
294        self.num_qudits()
295    }
296
297    /// Returns true is the system is empty, false otherwise.
298    pub fn is_empty(&self) -> bool {
299        self.len() == 0
300    }
301}
302
303impl crate::QuditSystem for Radices {
304    /// Returns the radices of the system.
305    ///
306    /// See [`QuditSystem`] for more information.
307    #[inline(always)]
308    fn radices(&self) -> Radices {
309        self.clone()
310    }
311
312    /// Returns the dimension of a system described by these radices.
313    ///
314    /// # Examples
315    ///
316    /// ```
317    /// # use qudit_core::Radices;
318    /// # use qudit_core::QuditSystem;
319    ///
320    /// let two_qubits = Radices::new([2, 2]);
321    /// assert_eq!(two_qubits.dimension(), 4);
322    ///
323    /// let two_qutrits = Radices::new([3, 3]);
324    /// assert_eq!(two_qutrits.dimension(), 9);
325    ///
326    /// let hybrid_system = Radices::new([3, 2, 3]);
327    /// assert_eq!(hybrid_system.dimension(), 18);
328    /// ```
329    #[inline(always)]
330    fn dimension(&self) -> usize {
331        self.iter().product::<usize>()
332    }
333
334    /// Returns the number of qudits represented by these radices.
335    ///
336    /// # Examples
337    ///
338    /// ```
339    /// # use qudit_core::Radices;
340    /// # use qudit_core::QuditSystem;
341    ///
342    /// let two_qubits = Radices::new([2, 2]);
343    /// assert_eq!(two_qubits.num_qudits(), 2);
344    ///
345    /// let two_qutrits = Radices::new([3, 3]);
346    /// assert_eq!(two_qutrits.num_qudits(), 2);
347    ///
348    /// let hybrid_system = Radices::new([3, 2, 3]);
349    /// assert_eq!(hybrid_system.num_qudits(), 3);
350    ///
351    /// let ten_qubits = Radices::new(vec![2; 10]);
352    /// assert_eq!(ten_qubits.num_qudits(), 10);
353    /// ```
354    #[inline(always)]
355    fn num_qudits(&self) -> usize {
356        self.0.len()
357    }
358
359    /// Returns true if these radices describe a qubit-only system.
360    ///
361    /// # Examples
362    ///
363    /// ```
364    /// # use qudit_core::Radices;
365    /// # use qudit_core::QuditSystem;
366    ///
367    /// let two_qubits = Radices::new([2, 2]);
368    /// assert!(two_qubits.is_qubit_only());
369    ///
370    /// let qubit_qutrit = Radices::new([2, 3]);
371    /// assert!(!qubit_qutrit.is_qubit_only());
372    ///
373    /// let two_qutrits = Radices::new([3, 3]);
374    /// assert!(!two_qutrits.is_qubit_only());
375    /// ```
376    #[inline(always)]
377    fn is_qubit_only(&self) -> bool {
378        self.iter().all(|r| *r == 2)
379    }
380
381    /// Returns true if these radices describe a qutrit-only system.
382    ///
383    /// # Examples
384    ///
385    /// ```
386    /// # use qudit_core::Radices;
387    /// # use qudit_core::QuditSystem;
388    ///
389    /// let two_qubits = Radices::new([2, 2]);
390    /// assert!(!two_qubits.is_qutrit_only());
391    ///
392    /// let qubit_qutrit = Radices::new([2, 3]);
393    /// assert!(!qubit_qutrit.is_qutrit_only());
394    ///
395    /// let two_qutrits = Radices::new([3, 3]);
396    /// assert!(two_qutrits.is_qutrit_only());
397    /// ```
398    #[inline(always)]
399    fn is_qutrit_only(&self) -> bool {
400        self.iter().all(|r| *r == 3)
401    }
402
403    /// Returns true if these radices describe a `radix`-only system.
404    ///
405    /// # Arguments
406    ///
407    /// * `radix` - The radix to check for.
408    ///
409    /// # Examples
410    ///
411    /// ```
412    /// # use qudit_core::Radices;
413    /// # use qudit_core::QuditSystem;
414    ///
415    /// let two_qubits = Radices::new([2, 2]);
416    /// assert!(two_qubits.is_qudit_only(2));
417    /// assert!(!two_qubits.is_qudit_only(3));
418    ///
419    /// let mixed_qudits = Radices::new([2, 3]);
420    /// assert!(!mixed_qudits.is_qudit_only(2));
421    /// assert!(!mixed_qudits.is_qudit_only(3));
422    /// ```
423    #[inline(always)]
424    fn is_qudit_only<T: Into<Radix>>(&self, radix: T) -> bool {
425        let radix = radix.into();
426        self.iter().all(|r| *r == radix)
427    }
428
429    /// Returns true if these radices describe a homogenous system.
430    ///
431    /// # Examples
432    ///
433    /// ```
434    /// # use qudit_core::Radices;
435    /// # use qudit_core::QuditSystem;
436    ///
437    /// let two_qubits = Radices::new([2, 2]);
438    /// assert!(two_qubits.is_homogenous());
439    ///
440    /// let mixed_qudits = Radices::new([2, 3]);
441    /// assert!(!mixed_qudits.is_homogenous());
442    /// ```
443    #[inline(always)]
444    fn is_homogenous(&self) -> bool {
445        self.is_qudit_only(self[0])
446    }
447}
448
449impl<T: Into<Radix>> core::iter::FromIterator<T> for Radices {
450    /// Creates a new QuditRadices object from an iterator.
451    ///
452    /// # Arguments
453    ///
454    /// * `iter` - An iterator over the radices of a qudit system.
455    ///
456    /// # Panics
457    ///
458    /// If radices does not represent a valid system. This can happen
459    /// if any of the radices are 0 or 1.
460    ///
461    /// # Examples
462    ///
463    /// ```
464    /// # use qudit_core::Radices;
465    ///
466    /// let qubit_qutrit = Radices::from_iter(2..4);
467    /// assert_eq!(qubit_qutrit, Radices::new([2, 3]));
468    ///
469    /// let two_qutrits = Radices::from_iter(vec![3, 3]);
470    /// assert_eq!(two_qutrits, Radices::new([3, 3]));
471    ///
472    /// // Ten qubits then ten qutrits
473    /// let mixed_system = Radices::from_iter(vec![2; 10].into_iter()
474    ///                         .chain(vec![3; 10].into_iter()));
475    ///
476    /// // Using .collect()
477    /// let from_collect: Radices = (2..5).collect();
478    /// assert_eq!(from_collect, Radices::new([2, 3, 4]));
479    /// ```
480    ///
481    /// # Note
482    ///
483    /// This will attempt to avoid an allocation when possible.
484    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
485        let vec: CompactVec<Radix> = iter.into_iter().map(|r| r.into()).collect();
486        Radices(vec)
487    }
488}
489
490impl core::ops::Deref for Radices {
491    type Target = [Radix];
492
493    #[inline(always)]
494    fn deref(&self) -> &[Radix] {
495        // Safety Radix is a transparent wrapper around u8
496        unsafe {
497            std::mem::transmute(
498                std::mem::transmute::<&CompactVec<Radix>, &CompactVec<u8>>(&self.0).deref(),
499            )
500        }
501    }
502}
503
504impl<T: Into<Radix> + CompactStorage> From<CompactVec<T>> for Radices {
505    #[inline(always)]
506    fn from(value: CompactVec<T>) -> Self {
507        value.into_iter().map(|x| x.into()).collect()
508    }
509}
510
511impl<T: Into<Radix> + 'static> From<Vec<T>> for Radices {
512    #[inline(always)]
513    fn from(value: Vec<T>) -> Self {
514        let vec = if std::any::TypeId::of::<T>() == std::any::TypeId::of::<Radix>() {
515            // If T is radix, then doing CompactVec::from will provide move semantics efficiently
516            // Safety: T == Radix, so Vec<T> == Vec<Radix>
517            CompactVec::<Radix>::from(unsafe { std::mem::transmute::<Vec<T>, Vec<Radix>>(value) })
518        } else {
519            value.into_iter().map(|x| x.into()).collect()
520        };
521
522        Radices(vec)
523    }
524}
525
526impl<T: Into<Radix> + Copy> From<&[T]> for Radices {
527    #[inline(always)]
528    fn from(value: &[T]) -> Self {
529        value.iter().copied().collect()
530    }
531}
532
533impl<T: Into<Radix>, const N: usize> From<[T; N]> for Radices {
534    #[inline(always)]
535    fn from(value: [T; N]) -> Self {
536        value.into_iter().collect()
537    }
538}
539
540impl<'a, T: Into<Radix> + Copy, const N: usize> From<&'a [T; N]> for Radices {
541    #[inline(always)]
542    fn from(value: &'a [T; N]) -> Self {
543        value.iter().copied().collect()
544    }
545}
546
547impl From<Radix> for Radices {
548    fn from(value: Radix) -> Self {
549        [value].into()
550    }
551}
552
553impl core::fmt::Debug for Radices {
554    /// Formats the radices as a string.
555    ///
556    /// See Display for more information.
557    #[inline(always)]
558    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> std::fmt::Result {
559        <Radices as core::fmt::Display>::fmt(self, f)
560    }
561}
562
563impl core::fmt::Display for Radices {
564    /// Formats the radices as a string.
565    ///
566    /// # Examples
567    ///
568    /// ```
569    /// # use qudit_core::Radices;
570    /// let two_qubits = Radices::new([2, 2]);
571    /// let two_qutrits = Radices::new([3, 3]);
572    /// let qubit_qutrit = Radices::new([2, 3]);
573    ///
574    /// assert_eq!(format!("{}", two_qubits), "[2, 2]");
575    /// assert_eq!(format!("{}", two_qutrits), "[3, 3]");
576    /// assert_eq!(format!("{}", qubit_qutrit), "[2, 3]");
577    /// ```
578    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> std::fmt::Result {
579        write!(f, "[")?;
580        let enum_iter = self.iter().enumerate();
581        for (i, r) in enum_iter {
582            if i == self.len() - 1 {
583                write!(f, "{}", r)?;
584            } else {
585                write!(f, "{}, ", r)?;
586            }
587        }
588
589        write!(f, "]")?;
590        Ok(())
591    }
592}
593
594#[cfg(test)]
595pub mod strategies {
596    use proptest::prelude::*;
597
598    use super::*;
599
600    impl Arbitrary for Radices {
601        type Parameters = (core::ops::Range<u8>, core::ops::Range<usize>);
602        type Strategy = BoxedStrategy<Self>;
603
604        /// Generate a random Radices object.
605        ///
606        /// By default, the number of radices is chosen randomly between
607        /// 1 and 4, and the radices themselves are chosen randomly
608        /// between 2 and 4.
609        fn arbitrary() -> Self::Strategy {
610            Self::arbitrary_with((2..5u8, 1..5))
611        }
612
613        /// Generate a random Radices object with the given parameters.
614        ///
615        /// # Arguments
616        ///
617        /// * `args` - A tuple of ranges for the number of radices and the
618        ///   radices themselves. The first range is for the number
619        ///   of radices, and the second range is for the radices
620        ///   themselves.
621        fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
622            prop::collection::vec(args.0, args.1)
623                .prop_map(Radices::new)
624                .boxed()
625        }
626    }
627}
628
629#[cfg(feature = "python")]
630mod python {
631    use super::*;
632    use pyo3::{exceptions::PyTypeError, prelude::*};
633    use pyo3_stub_gen::impl_stub_type;
634
635    impl_stub_type!(Radices = usize | Vec<usize>);
636
637    impl<'a, 'py> FromPyObject<'a, 'py> for Radices {
638        type Error = PyErr;
639
640        fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
641            if let Ok(num) = ob.extract::<usize>() {
642                Ok(Radices::new([num]))
643            } else if let Ok(nums) = ob.extract::<Vec<usize>>() {
644                Ok(Radices::new(nums))
645            } else {
646                Err(PyTypeError::new_err(
647                    "Expected a list of integers for radices.",
648                ))
649            }
650        }
651    }
652
653    impl<'py> IntoPyObject<'py> for Radices {
654        type Target = pyo3::types::PyList;
655        type Output = Bound<'py, Self::Target>;
656        type Error = PyErr;
657
658        /// Converts into a Python list of ints, mirroring `FromPyObject`.
659        ///
660        /// This only exists so that `pyo3-stub-gen` can render `Radices`
661        /// default parameter values (e.g. `Radices::new([2])`) in generated
662        /// `.pyi` stubs; `Radices` is otherwise only ever a pyfunction
663        /// input, never a return value.
664        fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
665            let ints: Vec<usize> = self.iter().map(|&r| usize::from(r)).collect();
666            pyo3::types::PyList::new(py, ints)
667        }
668    }
669}
670
671#[cfg(test)]
672mod tests {
673    use super::*;
674    use proptest::prelude::*;
675
676    proptest! {
677        /// An expansion should compress to the same value.
678        #[test]
679        fn test_expand_compress(radices in any::<Radices>()) {
680            for index in 0..radices.dimension() {
681                let exp = radices.expand(index);
682                assert_eq!(radices.compress(&exp), index);
683            }
684        }
685    }
686
687    #[test]
688    fn test_slice_ops() {
689        let rdx = Radices::new(vec![2, 3, 4usize]);
690        assert_eq!(rdx.len(), 3);
691        assert_eq!(rdx[1], 3);
692        assert_eq!(rdx[1..], [3, 4]);
693        assert_eq!(rdx.clone(), rdx);
694    }
695}