Skip to main content

qec_code/codes/
built_in_css.rs

1use std::collections::HashSet;
2
3use super::apm::{AffinePermutation, ApmCssManifestEntry, build_apm_css_checks};
4use super::color_666::{Color666FamilySpec, Color666Layout, color_666_sparse_checks};
5use super::toric_3d::{Toric3dSpec, toric_3d_css_checks};
6use crate::error::{QecError, Result};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct BuiltInCssChecks {
10    pub code_id: &'static str,
11    pub num_cols: usize,
12    pub hx: Vec<Vec<usize>>,
13    pub hz: Vec<Vec<usize>>,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct BuiltInCssCatalogEntry {
18    pub spec: &'static str,
19    pub description: &'static str,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum BuiltInCssCodeSpec {
24    Fixed {
25        code_id: &'static str,
26    },
27    Family {
28        family: BuiltInCssFamily,
29        params: BuiltInCssParams,
30    },
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum BuiltInCssFamily {
35    RepetitionX,
36    RepetitionZ,
37    SurfaceRotated,
38    Color666,
39    Toric,
40    Toric3d,
41    BivariateBicycle,
42    ApmKasai,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum BuiltInCssParams {
47    Distance { distance: usize },
48    Toric3d(Toric3dSpec),
49    BivariateBicycle(BivariateBicycleParams),
50    ApmKasai { p: usize },
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct BivariateBicycleParams {
55    pub lx: usize,
56    pub ly: usize,
57    pub a_terms: Vec<(usize, usize)>,
58    pub b_terms: Vec<(usize, usize)>,
59}
60
61const BUILT_IN_CSS_CATALOG: &[BuiltInCssCatalogEntry] = &[
62    BuiltInCssCatalogEntry {
63        spec: "steane",
64        description: "fixed [[7,1,3]] CSS code",
65    },
66    BuiltInCssCatalogEntry {
67        spec: "bb72",
68        description: "fixed [[72,12,6]] bivariate-bicycle CSS code",
69    },
70    BuiltInCssCatalogEntry {
71        spec: "apm_kasai:p=96",
72        description: "fixed Table A1 P=96 APM-CSS code",
73    },
74    BuiltInCssCatalogEntry {
75        spec: "apm_kasai:p=192",
76        description: "fixed Table A1 P=192 APM-CSS code",
77    },
78    BuiltInCssCatalogEntry {
79        spec: "bb:lx=<period-x>,ly=<period-y>,a=<dx>:<dy>|...,b=<dx>:<dy>|...",
80        description: "bivariate-bicycle CSS family over periodic lattice",
81    },
82    BuiltInCssCatalogEntry {
83        spec: "repetition_x:d=<distance>",
84        description: "X-check chain, distance >= 2",
85    },
86    BuiltInCssCatalogEntry {
87        spec: "repetition_z:d=<distance>",
88        description: "Z-check chain, distance >= 2",
89    },
90    BuiltInCssCatalogEntry {
91        spec: "surface_rotated:d=<distance>",
92        description: "rotated surface CSS code, distance >= 2",
93    },
94    BuiltInCssCatalogEntry {
95        spec: "color_666:d=<distance>",
96        description: "triangular 6.6.6 color CSS code, odd distance >= 3",
97    },
98    BuiltInCssCatalogEntry {
99        spec: "toric:d=<distance>",
100        description: "periodic square-lattice toric CSS code, distance >= 2",
101    },
102    BuiltInCssCatalogEntry {
103        spec: "toric_3d:lx=<period-x>,ly=<period-y>,lz=<period-z>",
104        description: "periodic cubic 3D toric CSS code, periods >= 3",
105    },
106];
107
108pub fn built_in_css_catalog() -> &'static [BuiltInCssCatalogEntry] {
109    BUILT_IN_CSS_CATALOG
110}
111
112pub fn parse_built_in_css_code_spec(input: &str) -> Result<BuiltInCssCodeSpec> {
113    if let Some((family_name, params_text)) = input.split_once(':') {
114        return parse_built_in_css_family_spec(family_name, params_text);
115    }
116
117    match input {
118        "steane" => Ok(BuiltInCssCodeSpec::Fixed { code_id: "steane" }),
119        "bb72" => Ok(BuiltInCssCodeSpec::Fixed { code_id: "bb72" }),
120        "apm_kasai" => Err(QecError::MissingBuiltInCssParameter {
121            family: input.to_owned(),
122            parameter: "p".to_owned(),
123        }),
124        "repetition_x" | "repetition_z" | "surface_rotated" | "color_666" | "toric" => {
125            Err(QecError::MissingBuiltInCssParameter {
126                family: input.to_owned(),
127                parameter: "d".to_owned(),
128            })
129        }
130        "toric_3d" => Err(QecError::MissingBuiltInCssParameter {
131            family: input.to_owned(),
132            parameter: "lx".to_owned(),
133        }),
134        "bb" => Err(QecError::MissingBuiltInCssParameter {
135            family: input.to_owned(),
136            parameter: "lx".to_owned(),
137        }),
138        _ => Err(QecError::UnknownBuiltInCssCode {
139            code_id: input.to_owned(),
140        }),
141    }
142}
143
144fn parse_built_in_css_family_spec(
145    family_name: &str,
146    params_text: &str,
147) -> Result<BuiltInCssCodeSpec> {
148    match family_name {
149        "repetition_x" => {
150            parse_distance_family_spec(family_name, BuiltInCssFamily::RepetitionX, params_text)
151        }
152        "repetition_z" => {
153            parse_distance_family_spec(family_name, BuiltInCssFamily::RepetitionZ, params_text)
154        }
155        "surface_rotated" => {
156            parse_distance_family_spec(family_name, BuiltInCssFamily::SurfaceRotated, params_text)
157        }
158        "color_666" => {
159            parse_distance_family_spec(family_name, BuiltInCssFamily::Color666, params_text)
160        }
161        "toric" => parse_distance_family_spec(family_name, BuiltInCssFamily::Toric, params_text),
162        "toric_3d" => {
163            let spec = parse_toric_3d_params(family_name, params_text)?;
164            Ok(BuiltInCssCodeSpec::Family {
165                family: BuiltInCssFamily::Toric3d,
166                params: BuiltInCssParams::Toric3d(spec),
167            })
168        }
169        "apm_kasai" => {
170            let p = parse_apm_kasai_params(family_name, params_text)?;
171            Ok(BuiltInCssCodeSpec::Family {
172                family: BuiltInCssFamily::ApmKasai,
173                params: BuiltInCssParams::ApmKasai { p },
174            })
175        }
176        "bb" => {
177            let params = parse_bivariate_bicycle_params(family_name, params_text)?;
178            Ok(BuiltInCssCodeSpec::Family {
179                family: BuiltInCssFamily::BivariateBicycle,
180                params: BuiltInCssParams::BivariateBicycle(params),
181            })
182        }
183        _ => Err(QecError::UnknownBuiltInCssFamily {
184            family: family_name.to_owned(),
185        }),
186    }
187}
188
189fn parse_distance_family_spec(
190    family_name: &str,
191    family: BuiltInCssFamily,
192    params_text: &str,
193) -> Result<BuiltInCssCodeSpec> {
194    let distance = parse_repetition_distance(family_name, params_text)?;
195
196    Ok(BuiltInCssCodeSpec::Family {
197        family,
198        params: BuiltInCssParams::Distance { distance },
199    })
200}
201
202fn parse_repetition_distance(family_name: &str, params_text: &str) -> Result<usize> {
203    if params_text.is_empty() {
204        return Err(QecError::MissingBuiltInCssParameter {
205            family: family_name.to_owned(),
206            parameter: "d".to_owned(),
207        });
208    }
209
210    let mut distance = None;
211
212    for pair in params_text.split(',') {
213        let Some((key, value)) = pair.split_once('=') else {
214            return Err(QecError::UnexpectedBuiltInCssParameter {
215                family: family_name.to_owned(),
216                parameter: pair.to_owned(),
217            });
218        };
219
220        match key {
221            "d" => {
222                if distance.is_some() {
223                    return Err(QecError::DuplicateBuiltInCssParameter {
224                        family: family_name.to_owned(),
225                        parameter: "d".to_owned(),
226                    });
227                }
228
229                let parsed = value.parse::<usize>().map_err(|_| {
230                    QecError::InvalidBuiltInCssIntegerParameter {
231                        family: family_name.to_owned(),
232                        parameter: "d".to_owned(),
233                        value: value.to_owned(),
234                    }
235                })?;
236
237                if parsed == 0 {
238                    return Err(QecError::OutOfRangeBuiltInCssIntegerParameter {
239                        family: family_name.to_owned(),
240                        parameter: "d".to_owned(),
241                        value: parsed,
242                    });
243                }
244
245                distance = Some(parsed);
246            }
247            _ => {
248                return Err(QecError::UnexpectedBuiltInCssParameter {
249                    family: family_name.to_owned(),
250                    parameter: key.to_owned(),
251                });
252            }
253        }
254    }
255
256    distance.ok_or_else(|| QecError::MissingBuiltInCssParameter {
257        family: family_name.to_owned(),
258        parameter: "d".to_owned(),
259    })
260}
261
262fn parse_toric_3d_params(family_name: &str, params_text: &str) -> Result<Toric3dSpec> {
263    if params_text.is_empty() {
264        return Err(QecError::MissingBuiltInCssParameter {
265            family: family_name.to_owned(),
266            parameter: "lx".to_owned(),
267        });
268    }
269
270    let mut lx = None;
271    let mut ly = None;
272    let mut lz = None;
273    for pair in params_text.split(',') {
274        let Some((key, value)) = pair.split_once('=') else {
275            return Err(QecError::UnexpectedBuiltInCssParameter {
276                family: family_name.to_owned(),
277                parameter: pair.to_owned(),
278            });
279        };
280
281        match key {
282            "lx" => parse_unique_positive_usize_param(family_name, "lx", value, &mut lx)?,
283            "ly" => parse_unique_positive_usize_param(family_name, "ly", value, &mut ly)?,
284            "lz" => parse_unique_positive_usize_param(family_name, "lz", value, &mut lz)?,
285            _ => {
286                return Err(QecError::UnexpectedBuiltInCssParameter {
287                    family: family_name.to_owned(),
288                    parameter: key.to_owned(),
289                });
290            }
291        }
292    }
293
294    let spec = Toric3dSpec {
295        lx: lx.ok_or_else(|| QecError::MissingBuiltInCssParameter {
296            family: family_name.to_owned(),
297            parameter: "lx".to_owned(),
298        })?,
299        ly: ly.ok_or_else(|| QecError::MissingBuiltInCssParameter {
300            family: family_name.to_owned(),
301            parameter: "ly".to_owned(),
302        })?,
303        lz: lz.ok_or_else(|| QecError::MissingBuiltInCssParameter {
304            family: family_name.to_owned(),
305            parameter: "lz".to_owned(),
306        })?,
307    };
308    toric_3d_css_checks(spec)?;
309    Ok(spec)
310}
311
312fn parse_bivariate_bicycle_params(
313    family_name: &str,
314    params_text: &str,
315) -> Result<BivariateBicycleParams> {
316    if params_text.is_empty() {
317        return Err(QecError::MissingBuiltInCssParameter {
318            family: family_name.to_owned(),
319            parameter: "lx".to_owned(),
320        });
321    }
322
323    let mut lx = None;
324    let mut ly = None;
325    let mut a_terms = None;
326    let mut b_terms = None;
327
328    for pair in params_text.split(',') {
329        let Some((key, value)) = pair.split_once('=') else {
330            return Err(QecError::UnexpectedBuiltInCssParameter {
331                family: family_name.to_owned(),
332                parameter: pair.to_owned(),
333            });
334        };
335
336        match key {
337            "lx" => parse_unique_positive_usize_param(family_name, "lx", value, &mut lx)?,
338            "ly" => parse_unique_positive_usize_param(family_name, "ly", value, &mut ly)?,
339            "a" => parse_unique_bivariate_bicycle_terms(family_name, "a", value, &mut a_terms)?,
340            "b" => parse_unique_bivariate_bicycle_terms(family_name, "b", value, &mut b_terms)?,
341            _ => {
342                return Err(QecError::UnexpectedBuiltInCssParameter {
343                    family: family_name.to_owned(),
344                    parameter: key.to_owned(),
345                });
346            }
347        }
348    }
349
350    let params = BivariateBicycleParams {
351        lx: lx.ok_or_else(|| QecError::MissingBuiltInCssParameter {
352            family: family_name.to_owned(),
353            parameter: "lx".to_owned(),
354        })?,
355        ly: ly.ok_or_else(|| QecError::MissingBuiltInCssParameter {
356            family: family_name.to_owned(),
357            parameter: "ly".to_owned(),
358        })?,
359        a_terms: a_terms.ok_or_else(|| QecError::MissingBuiltInCssParameter {
360            family: family_name.to_owned(),
361            parameter: "a".to_owned(),
362        })?,
363        b_terms: b_terms.ok_or_else(|| QecError::MissingBuiltInCssParameter {
364            family: family_name.to_owned(),
365            parameter: "b".to_owned(),
366        })?,
367    };
368
369    validate_bivariate_bicycle_params(&params)?;
370    Ok(params)
371}
372
373const APM_KASAI_SUPPORTED_P_VALUES: &str = "96, 192";
374const APM_KASAI_P96_CODE_ID: &str = "apm_kasai:p=96";
375const APM_KASAI_P192_CODE_ID: &str = "apm_kasai:p=192";
376const APM_KASAI_P96_P: u64 = 96;
377const APM_KASAI_P192_P: u64 = 192;
378const APM_KASAI_J: u64 = 3;
379const APM_KASAI_L: u64 = 12;
380const APM_KASAI_P96_F: &[(u64, u64)] = &[(5, 41), (85, 77), (73, 66), (1, 0), (1, 72), (37, 9)];
381const APM_KASAI_P96_G: &[(u64, u64)] = &[(61, 15), (1, 24), (89, 62), (25, 22), (85, 93), (25, 78)];
382const APM_KASAI_P192_F: &[(u64, u64)] = &[
383    (71, 127),
384    (97, 80),
385    (67, 117),
386    (163, 165),
387    (25, 60),
388    (187, 33),
389];
390const APM_KASAI_P192_G: &[(u64, u64)] = &[
391    (163, 165),
392    (55, 183),
393    (167, 79),
394    (139, 41),
395    (109, 78),
396    (31, 27),
397];
398
399fn parse_apm_kasai_params(family_name: &str, params_text: &str) -> Result<usize> {
400    if params_text.is_empty() {
401        return Err(QecError::MissingBuiltInCssParameter {
402            family: family_name.to_owned(),
403            parameter: "p".to_owned(),
404        });
405    }
406
407    let mut p = None;
408    for pair in params_text.split(',') {
409        let Some((key, value)) = pair.split_once('=') else {
410            return Err(QecError::UnexpectedBuiltInCssParameter {
411                family: family_name.to_owned(),
412                parameter: pair.to_owned(),
413            });
414        };
415
416        match key {
417            "p" => {
418                if p.is_some() {
419                    return Err(QecError::DuplicateBuiltInCssParameter {
420                        family: family_name.to_owned(),
421                        parameter: "p".to_owned(),
422                    });
423                }
424                p = Some(value.parse::<usize>().map_err(|_| {
425                    QecError::InvalidBuiltInCssIntegerParameter {
426                        family: family_name.to_owned(),
427                        parameter: "p".to_owned(),
428                        value: value.to_owned(),
429                    }
430                })?);
431            }
432            _ => {
433                return Err(QecError::UnexpectedBuiltInCssParameter {
434                    family: family_name.to_owned(),
435                    parameter: key.to_owned(),
436                });
437            }
438        }
439    }
440
441    Ok(p.expect("apm_kasai parameter parser should require p before success"))
442}
443
444fn apm_kasai_css_checks(p: usize) -> Result<BuiltInCssChecks> {
445    let entry = match p {
446        96 => apm_kasai_manifest_entry(
447            APM_KASAI_P96_CODE_ID,
448            APM_KASAI_P96_P,
449            APM_KASAI_P96_F,
450            APM_KASAI_P96_G,
451        ),
452        192 => apm_kasai_manifest_entry(
453            APM_KASAI_P192_CODE_ID,
454            APM_KASAI_P192_P,
455            APM_KASAI_P192_F,
456            APM_KASAI_P192_G,
457        ),
458        _ => {
459            return Err(QecError::UnsupportedBuiltInCssIntegerParameter {
460                family: "apm_kasai".to_owned(),
461                parameter: "p".to_owned(),
462                value: p,
463                supported: APM_KASAI_SUPPORTED_P_VALUES.to_owned(),
464                note: "available Table A1 APM-CSS instances".to_owned(),
465            });
466        }
467    };
468
469    Ok(build_apm_css_checks(&entry).expect("pinned APM Kasai manifest must build"))
470}
471
472fn apm_kasai_manifest_entry(
473    code_id: &'static str,
474    p: u64,
475    f_params: &[(u64, u64)],
476    g_params: &[(u64, u64)],
477) -> ApmCssManifestEntry {
478    let affine = |slope, offset| {
479        AffinePermutation::new(p, slope, offset)
480            .expect("pinned APM Kasai affine maps must be permutations")
481    };
482    let f = f_params
483        .iter()
484        .map(|&(slope, offset)| affine(slope, offset))
485        .collect();
486    let g = g_params
487        .iter()
488        .map(|&(slope, offset)| affine(slope, offset))
489        .collect();
490
491    ApmCssManifestEntry::new(code_id, p, APM_KASAI_J, APM_KASAI_L, f, g)
492        .expect("pinned APM Kasai manifest must satisfy invariants")
493}
494
495fn parse_unique_positive_usize_param(
496    family_name: &str,
497    parameter: &'static str,
498    value: &str,
499    slot: &mut Option<usize>,
500) -> Result<()> {
501    if slot.is_some() {
502        return Err(QecError::DuplicateBuiltInCssParameter {
503            family: family_name.to_owned(),
504            parameter: parameter.to_owned(),
505        });
506    }
507
508    let parsed =
509        value
510            .parse::<usize>()
511            .map_err(|_| QecError::InvalidBuiltInCssIntegerParameter {
512                family: family_name.to_owned(),
513                parameter: parameter.to_owned(),
514                value: value.to_owned(),
515            })?;
516
517    if parsed == 0 {
518        return Err(QecError::OutOfRangeBuiltInCssIntegerParameter {
519            family: family_name.to_owned(),
520            parameter: parameter.to_owned(),
521            value: parsed,
522        });
523    }
524
525    *slot = Some(parsed);
526    Ok(())
527}
528
529fn parse_unique_bivariate_bicycle_terms(
530    family_name: &str,
531    parameter: &'static str,
532    value: &str,
533    slot: &mut Option<Vec<(usize, usize)>>,
534) -> Result<()> {
535    if slot.is_some() {
536        return Err(QecError::DuplicateBuiltInCssParameter {
537            family: family_name.to_owned(),
538            parameter: parameter.to_owned(),
539        });
540    }
541
542    let mut terms = Vec::new();
543    for term in value.split('|') {
544        let Some((dx_text, dy_text)) = term.split_once(':') else {
545            return Err(QecError::InvalidBuiltInCssIntegerParameter {
546                family: family_name.to_owned(),
547                parameter: parameter.to_owned(),
548                value: term.to_owned(),
549            });
550        };
551
552        let dx =
553            dx_text
554                .parse::<usize>()
555                .map_err(|_| QecError::InvalidBuiltInCssIntegerParameter {
556                    family: family_name.to_owned(),
557                    parameter: parameter.to_owned(),
558                    value: dx_text.to_owned(),
559                })?;
560        let dy =
561            dy_text
562                .parse::<usize>()
563                .map_err(|_| QecError::InvalidBuiltInCssIntegerParameter {
564                    family: family_name.to_owned(),
565                    parameter: parameter.to_owned(),
566                    value: dy_text.to_owned(),
567                })?;
568
569        terms.push((dx, dy));
570    }
571
572    *slot = Some(terms);
573    Ok(())
574}
575
576const STEANE_ROW_SUPPORTS: &[&[usize]] = &[&[0, 3, 5, 6], &[1, 3, 4, 6], &[2, 4, 5, 6]];
577
578const BB72_LX: usize = 6;
579const BB72_LY: usize = 6;
580const BB72_A_TERMS: &[(usize, usize)] = &[(3, 0), (0, 1), (0, 2)];
581const BB72_B_TERMS: &[(usize, usize)] = &[(0, 3), (1, 0), (2, 0)];
582
583fn bb72_bivariate_bicycle_params() -> BivariateBicycleParams {
584    BivariateBicycleParams {
585        lx: BB72_LX,
586        ly: BB72_LY,
587        a_terms: BB72_A_TERMS.to_vec(),
588        b_terms: BB72_B_TERMS.to_vec(),
589    }
590}
591
592fn bivariate_bicycle_checks(
593    lx: usize,
594    ly: usize,
595    a_terms: &[(usize, usize)],
596    b_terms: &[(usize, usize)],
597) -> (Vec<Vec<usize>>, Vec<Vec<usize>>) {
598    let block = lx * ly;
599    let index = |x: usize, y: usize| -> usize { (x % lx) * ly + (y % ly) };
600    let periodic_add = |coord: usize, shift: usize, period: usize| -> usize {
601        if shift == 0 {
602            coord
603        } else if coord >= period - shift {
604            coord - (period - shift)
605        } else {
606            coord + shift
607        }
608    };
609    let mut hx = Vec::with_capacity(block);
610    let mut hz = Vec::with_capacity(block);
611
612    for x in 0..lx {
613        for y in 0..ly {
614            let mut x_row = Vec::new();
615            for &(dx, dy) in a_terms {
616                let dx = dx % lx;
617                let dy = dy % ly;
618                x_row.push(index(periodic_add(x, dx, lx), periodic_add(y, dy, ly)));
619            }
620            for &(dx, dy) in b_terms {
621                let dx = dx % lx;
622                let dy = dy % ly;
623                x_row.push(block + index(periodic_add(x, dx, lx), periodic_add(y, dy, ly)));
624            }
625            x_row.sort_unstable();
626            hx.push(x_row);
627
628            let mut z_row = Vec::new();
629            for &(dx, dy) in b_terms {
630                z_row.push(index((x + lx - dx % lx) % lx, (y + ly - dy % ly) % ly));
631            }
632            for &(dx, dy) in a_terms {
633                z_row.push(block + index((x + lx - dx % lx) % lx, (y + ly - dy % ly) % ly));
634            }
635            z_row.sort_unstable();
636            hz.push(z_row);
637        }
638    }
639
640    (hx, hz)
641}
642
643pub fn bivariate_bicycle_css_checks(params: BivariateBicycleParams) -> Result<BuiltInCssChecks> {
644    validate_bivariate_bicycle_params(&params)?;
645
646    let (hx, hz) = bivariate_bicycle_checks(params.lx, params.ly, &params.a_terms, &params.b_terms);
647
648    Ok(BuiltInCssChecks {
649        code_id: "bb",
650        num_cols: 2 * params.lx * params.ly,
651        hx,
652        hz,
653    })
654}
655
656fn validate_bivariate_bicycle_params(params: &BivariateBicycleParams) -> Result<()> {
657    if params.lx == 0 {
658        return Err(QecError::OutOfRangeBuiltInCssIntegerParameter {
659            family: "bb".to_owned(),
660            parameter: "lx".to_owned(),
661            value: 0,
662        });
663    }
664
665    if params.ly == 0 {
666        return Err(QecError::OutOfRangeBuiltInCssIntegerParameter {
667            family: "bb".to_owned(),
668            parameter: "ly".to_owned(),
669            value: 0,
670        });
671    }
672
673    validate_bivariate_bicycle_terms("bb", "a_terms", params.lx, params.ly, &params.a_terms)?;
674    validate_bivariate_bicycle_terms("bb", "b_terms", params.lx, params.ly, &params.b_terms)?;
675    Ok(())
676}
677
678fn validate_bivariate_bicycle_terms(
679    family: &'static str,
680    parameter: &'static str,
681    lx: usize,
682    ly: usize,
683    terms: &[(usize, usize)],
684) -> Result<()> {
685    if terms.is_empty() {
686        return Err(QecError::MissingBuiltInCssParameter {
687            family: family.to_owned(),
688            parameter: parameter.to_owned(),
689        });
690    }
691
692    let mut seen = HashSet::new();
693    for &(dx, dy) in terms {
694        let normalized = (dx % lx, dy % ly);
695        if !seen.insert(normalized) {
696            return Err(QecError::DuplicateBuiltInCssParameter {
697                family: family.to_owned(),
698                parameter: parameter.to_owned(),
699            });
700        }
701    }
702
703    Ok(())
704}
705
706pub fn built_in_css_checks(code_id: &str) -> Result<BuiltInCssChecks> {
707    match parse_built_in_css_code_spec(code_id)? {
708        BuiltInCssCodeSpec::Fixed { code_id } => fixed_built_in_css_checks(code_id),
709        BuiltInCssCodeSpec::Family {
710            family: BuiltInCssFamily::BivariateBicycle,
711            params: BuiltInCssParams::BivariateBicycle(params),
712        } => bivariate_bicycle_css_checks(params),
713        BuiltInCssCodeSpec::Family { family, params } => family_css_checks(family, params),
714    }
715}
716
717fn fixed_built_in_css_checks(code_id: &'static str) -> Result<BuiltInCssChecks> {
718    match code_id {
719        "steane" => {
720            let hx = STEANE_ROW_SUPPORTS
721                .iter()
722                .map(|row| row.to_vec())
723                .collect::<Vec<_>>();
724
725            Ok(BuiltInCssChecks {
726                code_id: "steane",
727                num_cols: 7,
728                hx: hx.clone(),
729                hz: hx,
730            })
731        }
732        "bb72" => {
733            let mut checks = bivariate_bicycle_css_checks(bb72_bivariate_bicycle_params())?;
734            checks.code_id = "bb72";
735            Ok(checks)
736        }
737        _ => Err(QecError::UnknownBuiltInCssCode {
738            code_id: code_id.to_owned(),
739        }),
740    }
741}
742
743fn family_css_checks(
744    family: BuiltInCssFamily,
745    params: BuiltInCssParams,
746) -> Result<BuiltInCssChecks> {
747    match family {
748        BuiltInCssFamily::RepetitionX => {
749            let BuiltInCssParams::Distance { distance } = params else {
750                unreachable!("repetition_x only uses distance params");
751            };
752            let hx = chain_supports("repetition_x", distance)?;
753            Ok(BuiltInCssChecks {
754                code_id: "repetition_x",
755                num_cols: distance,
756                hx,
757                hz: vec![],
758            })
759        }
760        BuiltInCssFamily::RepetitionZ => {
761            let BuiltInCssParams::Distance { distance } = params else {
762                unreachable!("repetition_z only uses distance params");
763            };
764            let hz = chain_supports("repetition_z", distance)?;
765            Ok(BuiltInCssChecks {
766                code_id: "repetition_z",
767                num_cols: distance,
768                hx: vec![],
769                hz,
770            })
771        }
772        BuiltInCssFamily::SurfaceRotated => {
773            let BuiltInCssParams::Distance { distance } = params else {
774                unreachable!("surface_rotated only uses distance params");
775            };
776            surface_rotated_css_checks(distance)
777        }
778        BuiltInCssFamily::Color666 => {
779            let BuiltInCssParams::Distance { distance } = params else {
780                unreachable!("color_666 only uses distance params");
781            };
782            let checks = color_666_sparse_checks(&Color666FamilySpec {
783                distance,
784                layout: Color666Layout::Triangular,
785            })?;
786            Ok(BuiltInCssChecks {
787                code_id: "color_666",
788                num_cols: checks.num_cols,
789                hx: checks.rows.clone(),
790                hz: checks.rows,
791            })
792        }
793        BuiltInCssFamily::Toric => {
794            let BuiltInCssParams::Distance { distance } = params else {
795                unreachable!("toric only uses distance params");
796            };
797            toric_css_checks(distance)
798        }
799        BuiltInCssFamily::Toric3d => {
800            let BuiltInCssParams::Toric3d(spec) = params else {
801                unreachable!("toric_3d only uses toric_3d params");
802            };
803            let checks = toric_3d_css_checks(spec)?;
804            Ok(BuiltInCssChecks {
805                code_id: "toric_3d",
806                num_cols: checks.num_cols,
807                hx: checks.hx,
808                hz: checks.hz,
809            })
810        }
811        BuiltInCssFamily::ApmKasai => {
812            let BuiltInCssParams::ApmKasai { p } = params else {
813                unreachable!("apm_kasai only uses p params");
814            };
815            apm_kasai_css_checks(p)
816        }
817        BuiltInCssFamily::BivariateBicycle => unreachable!("bb specs are parser-only here"),
818    }
819}
820
821fn chain_supports(family: &'static str, distance: usize) -> Result<Vec<Vec<usize>>> {
822    if distance < 2 {
823        return Err(QecError::OutOfRangeBuiltInCssIntegerParameter {
824            family: family.to_owned(),
825            parameter: "d".to_owned(),
826            value: distance,
827        });
828    }
829
830    Ok((0..distance - 1).map(|col| vec![col, col + 1]).collect())
831}
832
833fn surface_rotated_css_checks(distance: usize) -> Result<BuiltInCssChecks> {
834    if distance < 2 {
835        return Err(QecError::OutOfRangeBuiltInCssIntegerParameter {
836            family: "surface_rotated".to_owned(),
837            parameter: "d".to_owned(),
838            value: distance,
839        });
840    }
841
842    let (hx, hz) = rotated_surface_supports(distance);
843
844    Ok(BuiltInCssChecks {
845        code_id: "surface_rotated",
846        num_cols: distance * distance,
847        hx,
848        hz,
849    })
850}
851
852fn rotated_surface_supports(distance: usize) -> (Vec<Vec<usize>>, Vec<Vec<usize>>) {
853    let mut hx = Vec::new();
854    let mut hz = Vec::new();
855
856    for ax in 0..=distance {
857        for ay in 0..=distance {
858            let on_boundary_1 = ax == 0 || ax == distance;
859            let on_boundary_2 = ay == 0 || ay == distance;
860            let parity = (ax % 2) != (ay % 2);
861            if on_boundary_1 && parity {
862                continue;
863            }
864            if on_boundary_2 && !parity {
865                continue;
866            }
867
868            let support = rotated_surface_measure_support(distance, ax, ay);
869            if support.is_empty() {
870                continue;
871            }
872
873            if parity {
874                hx.push(support);
875            } else {
876                hz.push(support);
877            }
878        }
879    }
880
881    (hx, hz)
882}
883
884fn rotated_surface_measure_support(distance: usize, ax: usize, ay: usize) -> Vec<usize> {
885    let mut support = Vec::new();
886    let mx = (2 * ax) as isize;
887    let my = (2 * ay) as isize;
888
889    for (dx, dy) in [(1isize, 1isize), (1, -1), (-1, 1), (-1, -1)] {
890        let x = mx + dx;
891        let y = my + dy;
892        if x >= 1
893            && x <= (2 * distance - 1) as isize
894            && y >= 1
895            && y <= (2 * distance - 1) as isize
896            && x % 2 == 1
897            && y % 2 == 1
898        {
899            let qx = ((x - 1) / 2) as usize;
900            let qy = ((y - 1) / 2) as usize;
901            if qx < distance && qy < distance {
902                support.push(rotated_surface_data_index(distance, qx, qy));
903            }
904        }
905    }
906
907    support.sort_unstable();
908    support.dedup();
909    support
910}
911
912fn rotated_surface_data_index(distance: usize, x: usize, y: usize) -> usize {
913    x * distance + y
914}
915
916fn toric_css_checks(distance: usize) -> Result<BuiltInCssChecks> {
917    if distance < 2 {
918        return Err(QecError::OutOfRangeBuiltInCssIntegerParameter {
919            family: "toric".to_owned(),
920            parameter: "d".to_owned(),
921            value: distance,
922        });
923    }
924
925    let (hx, hz) = toric_supports(distance);
926
927    Ok(BuiltInCssChecks {
928        code_id: "toric",
929        num_cols: 2 * distance * distance,
930        hx,
931        hz,
932    })
933}
934
935fn toric_supports(distance: usize) -> (Vec<Vec<usize>>, Vec<Vec<usize>>) {
936    let mut hx = Vec::with_capacity(distance * distance);
937    let mut hz = Vec::with_capacity(distance * distance);
938
939    for x in 0..distance {
940        for y in 0..distance {
941            hx.push(toric_x_check_support(distance, x, y));
942            hz.push(toric_z_check_support(distance, x, y));
943        }
944    }
945
946    (hx, hz)
947}
948
949fn toric_x_check_support(distance: usize, x: usize, y: usize) -> Vec<usize> {
950    sorted_toric_row([
951        toric_horizontal_index(distance, x, y),
952        toric_horizontal_index(distance, x, wrap_prev(y, distance)),
953        toric_vertical_index(distance, x, y),
954        toric_vertical_index(distance, wrap_prev(x, distance), y),
955    ])
956}
957
958fn toric_z_check_support(distance: usize, x: usize, y: usize) -> Vec<usize> {
959    sorted_toric_row([
960        toric_horizontal_index(distance, x, y),
961        toric_horizontal_index(distance, wrap_next(x, distance), y),
962        toric_vertical_index(distance, x, y),
963        toric_vertical_index(distance, x, wrap_next(y, distance)),
964    ])
965}
966
967fn sorted_toric_row(mut row: [usize; 4]) -> Vec<usize> {
968    row.sort_unstable();
969    row.to_vec()
970}
971
972fn toric_horizontal_index(distance: usize, x: usize, y: usize) -> usize {
973    x * distance + y
974}
975
976fn toric_vertical_index(distance: usize, x: usize, y: usize) -> usize {
977    distance * distance + x * distance + y
978}
979
980fn wrap_prev(value: usize, distance: usize) -> usize {
981    (value + distance - 1) % distance
982}
983
984fn wrap_next(value: usize, distance: usize) -> usize {
985    (value + 1) % distance
986}
987
988#[cfg(test)]
989mod tests {
990    use super::*;
991
992    #[test]
993    #[should_panic(expected = "color_666 only uses distance params")]
994    fn color_666_family_checks_reject_mismatched_internal_params() {
995        let _ = family_css_checks(
996            BuiltInCssFamily::Color666,
997            BuiltInCssParams::ApmKasai { p: 96 },
998        );
999    }
1000
1001    #[test]
1002    #[should_panic(expected = "toric_3d only uses toric_3d params")]
1003    fn toric_3d_family_checks_reject_mismatched_internal_params() {
1004        let _ = family_css_checks(
1005            BuiltInCssFamily::Toric3d,
1006            BuiltInCssParams::ApmKasai { p: 96 },
1007        );
1008    }
1009}