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