Skip to main content

bloq_graph/
height.rs

1//! Cube height: a symbolic linear function of the code distance.
2//!
3//! Writing a cube's height as `k*d + n` splits two things a plain integer
4//! multiplier conflated — **cells**, the lattice cells it occupies along the
5//! time axis, and **rounds**, the syndrome rounds it compiles to — so a cube can
6//! ask for fewer rounds than its footprint suggests. `height=2d/3` occupies one cell
7//! but compiles to `ceil(2d/3)` rounds, which is what makes TELS expressible: a
8//! CCZ factory's Z-readout layers carry a second distance-2 discard redundancy
9//! and so do not need the full `d` rounds a bare merge does.
10
11use std::fmt;
12use std::str::FromStr;
13
14use crate::BlockError;
15
16/// Largest supported cube footprint along the time axis, in cells.
17///
18/// A cube eagerly materializes one reserved offset per occupied cell, so this
19/// limit bounds allocations from both parsed and programmatic graph input. It
20/// also keeps [`CubeHeight::rounds`] inside `i64`: rounds are at most
21/// `cells * distance`, and `65535 * u32::MAX` is comfortably representable.
22pub const MAX_CUBE_HEIGHT_CELLS: u32 = 65_535;
23
24/// A cube's height, written `k*d + n` over the code distance `d`.
25///
26/// `k = numerator / denominator` is a positive rational kept in lowest terms so
27/// that `2d/4 == d/2`; `n` is a signed integer offset. Blog syntax is
28/// `height=<expr>` — `d`, `2d`, `d/2`, `3d/2`, `3d+2`, `d/2 - 1` — defaulting to
29/// `height=d`. [`Ord`] is structural, present only to give [`Block`](crate::Block) a
30/// total order; it does not compare realized heights, which depend on `d`.
31///
32/// ```
33/// use bloq_graph::CubeHeight;
34///
35/// let half: CubeHeight = "d/2".parse()?;
36/// assert_eq!((half.cells(), half.rounds(7)), (1, 4)); // one cell, ceil(7/2) rounds
37/// # Ok::<(), bloq_graph::BlockError>(())
38/// ```
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
40pub struct CubeHeight {
41    numerator: u32,
42    denominator: u32,
43    offset: i32,
44}
45
46impl CubeHeight {
47    /// `height=d`: one code distance of rounds, one cell.
48    pub const DEFAULT: Self = Self {
49        numerator: 1,
50        denominator: 1,
51        offset: 0,
52    };
53
54    /// Constructs `numerator/denominator * d + offset`, reduced to lowest terms.
55    ///
56    /// # Errors
57    ///
58    /// Returns [`BlockError::NonPositiveCubeHeight`] if either `numerator` or
59    /// `denominator` is zero (`k <= 0` is meaningless), or
60    /// [`BlockError::CubeHeightExceedsRange`] if the footprint would exceed
61    /// [`MAX_CUBE_HEIGHT_CELLS`].
62    pub fn new(numerator: u32, denominator: u32, offset: i32) -> Result<Self, BlockError> {
63        if numerator == 0 || denominator == 0 {
64            return Err(BlockError::NonPositiveCubeHeight {
65                numerator,
66                denominator,
67            });
68        }
69        let divisor = gcd(numerator, denominator);
70        let height = Self {
71            numerator: numerator / divisor,
72            denominator: denominator / divisor,
73            offset,
74        };
75        if height.cells() > MAX_CUBE_HEIGHT_CELLS {
76            return Err(BlockError::CubeHeightExceedsRange {
77                cells: height.cells(),
78            });
79        }
80        Ok(height)
81    }
82
83    /// The reduced numerator of `k`.
84    pub const fn numerator(self) -> u32 {
85        self.numerator
86    }
87
88    /// The reduced denominator of `k`.
89    pub const fn denominator(self) -> u32 {
90        self.denominator
91    }
92
93    /// The integer offset `n`.
94    pub const fn offset(self) -> i32 {
95        self.offset
96    }
97
98    /// Whether this is `height=d`, the height a cube has when nothing is written.
99    pub const fn is_default(self) -> bool {
100        self.numerator == 1 && self.denominator == 1 && self.offset == 0
101    }
102
103    /// Cells occupied along the time axis: `ceil(k)`, always at least 1.
104    ///
105    /// The offset `n` deliberately does not participate. `n` buys or refunds
106    /// individual syndrome rounds inside the cube's own worldline; it does not
107    /// change which lattice cells the cube claims, so `height=d-1` and `height=d+1` are
108    /// both single-cell cubes.
109    pub const fn cells(self) -> u32 {
110        self.numerator.div_ceil(self.denominator)
111    }
112
113    /// Compiled syndrome rounds at `distance`: `ceil(k*d) + n`.
114    ///
115    /// Since `n` is an integer this equals `ceil(k*d + n)` exactly, and it is
116    /// computed that way — integer `div_ceil` on the numerator, then the offset
117    /// — rather than through floating point.
118    ///
119    /// The result is signed because a large negative `n` can drive it below
120    /// zero; callers that need a usable round count must check it (a cube needs
121    /// at least 2 rounds, one for its initialization stage and one for its
122    /// measurement stage).
123    pub const fn rounds(self, distance: u32) -> i64 {
124        let scaled = (self.numerator as u64 * distance as u64).div_ceil(self.denominator as u64);
125        // `scaled <= cells() * distance <= 65535 * u32::MAX`, so the cast is
126        // lossless and the sum cannot overflow `i64`.
127        scaled as i64 + self.offset as i64
128    }
129}
130
131/// Greatest common divisor, for reducing `k` at construction.
132const fn gcd(mut a: u32, mut b: u32) -> u32 {
133    while b != 0 {
134        let remainder = a % b;
135        a = b;
136        b = remainder;
137    }
138    a
139}
140
141impl Default for CubeHeight {
142    fn default() -> Self {
143        Self::DEFAULT
144    }
145}
146
147impl fmt::Display for CubeHeight {
148    /// Emits the canonical form: the fraction reduced, a `/1` denominator and a
149    /// `+0` offset omitted. This is exactly what the blog writer puts after
150    /// `height=`, so it must re-parse to an equal value.
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        if self.numerator != 1 {
153            write!(f, "{}", self.numerator)?;
154        }
155        f.write_str("d")?;
156        if self.denominator != 1 {
157            write!(f, "/{}", self.denominator)?;
158        }
159        match self.offset {
160            0 => Ok(()),
161            n if n > 0 => write!(f, "+{n}"),
162            n => write!(f, "-{}", n.unsigned_abs()),
163        }
164    }
165}
166
167impl FromStr for CubeHeight {
168    type Err = BlockError;
169
170    /// Parses `k*d + n`, sharing the blog grammar so the two cannot drift.
171    fn from_str(s: &str) -> Result<Self, Self::Err> {
172        crate::parser::cube_height_from_str(s)
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn accepted_spellings_parse_to_the_documented_value() {
182        for (text, (numerator, denominator, offset)) in [
183            ("d", (1, 1, 0)),
184            ("2d", (2, 1, 0)),
185            ("3d", (3, 1, 0)),
186            ("d/2", (1, 2, 0)),
187            ("3d/2", (3, 2, 0)),
188            ("2d/3", (2, 3, 0)),
189            ("3d+2", (3, 1, 2)),
190            ("d/2 + 1", (1, 2, 1)),
191            ("d - 1", (1, 1, -1)),
192            (" 2 d / 4 ", (1, 2, 0)),
193            ("D/2", (1, 2, 0)),
194        ] {
195            let height: CubeHeight = text.parse().unwrap_or_else(|e| panic!("{text}: {e}"));
196            assert_eq!(
197                (height.numerator(), height.denominator(), height.offset()),
198                (numerator, denominator, offset),
199                "parsing {text}"
200            );
201        }
202    }
203
204    #[test]
205    fn non_positive_and_malformed_expressions_are_rejected() {
206        for text in [
207            "0d",
208            "d/0",
209            "0d/0",
210            "2",
211            "",
212            "dd",
213            "d/",
214            "d+",
215            "d/2/3",
216            "d*2",
217            "d+2147483648",
218            "d-2147483649",
219        ] {
220            assert!(
221                text.parse::<CubeHeight>().is_err(),
222                "{text} should be rejected"
223            );
224        }
225    }
226
227    #[test]
228    fn display_is_canonical_and_round_trips() {
229        for (input, canonical) in [
230            ("d", "d"),
231            ("1d/1", "d"),
232            ("2d/4", "d/2"),
233            ("2d", "2d"),
234            ("3d/2", "3d/2"),
235            ("3d + 2", "3d+2"),
236            ("d/2 - 1", "d/2-1"),
237            ("d+0", "d"),
238            ("d+2147483647", "d+2147483647"),
239            ("d-2147483648", "d-2147483648"),
240        ] {
241            let height: CubeHeight = input.parse().expect("valid height");
242            assert_eq!(height.to_string(), canonical);
243            assert_eq!(
244                canonical
245                    .parse::<CubeHeight>()
246                    .expect("canonical re-parses"),
247                height
248            );
249        }
250    }
251
252    #[test]
253    fn cells_is_ceil_k_and_ignores_the_offset() {
254        for (text, cells) in [
255            ("d", 1),
256            ("d/2", 1),
257            ("2d/3", 1),
258            ("2d", 2),
259            ("3d/2", 2),
260            ("3d", 3),
261            ("d-1", 1),
262            ("d+5", 1),
263        ] {
264            assert_eq!(
265                text.parse::<CubeHeight>().expect("valid").cells(),
266                cells,
267                "cells of {text}"
268            );
269        }
270    }
271
272    #[test]
273    fn rounds_is_ceil_kd_plus_the_offset() {
274        for (text, distance, rounds) in [
275            ("d", 7, 7),
276            ("2d", 7, 14),
277            ("d/2", 7, 4),
278            ("d/2", 8, 4),
279            ("3d+2", 7, 23),
280            ("d/2 + 1", 7, 5),
281            ("2d/3", 7, 5),
282            // A negative offset can drive the count below the two-round floor,
283            // and even below zero; the compiler is what rejects that.
284            ("d-1", 3, 2),
285            ("d/2-3", 3, -1),
286        ] {
287            assert_eq!(
288                text.parse::<CubeHeight>().expect("valid").rounds(distance),
289                rounds,
290                "rounds of {text} at d={distance}"
291            );
292        }
293    }
294
295    #[test]
296    fn footprint_is_bounded_before_reserved_offset_allocation() {
297        CubeHeight::new(MAX_CUBE_HEIGHT_CELLS, 1, 0).unwrap();
298        assert!(matches!(
299            CubeHeight::new(MAX_CUBE_HEIGHT_CELLS + 1, 1, 0),
300            Err(BlockError::CubeHeightExceedsRange { cells }) if cells == MAX_CUBE_HEIGHT_CELLS + 1
301        ));
302        // A huge numerator is fine as long as the reduced ceiling is small.
303        CubeHeight::new(u32::MAX, u32::MAX, 0).unwrap();
304    }
305}