stabilizer_qec 2.0.0

Quantum error-correction simulation and decoding: Stim-format circuits and error models, a bit-parallel sampler, sparse-blossom, correlated and belief matching, BP+OSD, window decoders, bivariate bicycle codes, lattice surgery, Clifford+T circuits with coherent noise, and sampling and BP on the GPU.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
// Ported from Fusion Blossom 0.2.13 (Yue Wu; MIT; see src/fusion/LICENSE and
// THIRD_PARTY_NOTICES.md): its default features, its dependencies replaced by src/fusion/shim.rs.

#[allow(unused_imports)]
use crate::fusion::shim::{serde_json};
use super::mwpm_solver::PrimalDualSolver;
use super::pointers::*;
use super::rand_xoshiro;
use crate::fusion::rand_xoshiro::rand_core::RngCore;
use std::collections::BTreeSet;
use std::fs::File;
use std::io::prelude::*;
use std::time::Instant;


        pub type Weight = isize;
    


        pub type EdgeIndex = usize;
        pub type VertexIndex = usize;
        pub type NodeIndex = VertexIndex;
        pub type DefectIndex = VertexIndex;
        pub type VertexNodeIndex = VertexIndex;  // must be same as VertexIndex, NodeIndex, DefectIndex
        pub type VertexNum = VertexIndex;
        pub type NodeNum = VertexIndex;
    


#[derive(Debug, Clone)]
pub struct SolverInitializer {
    /// the number of vertices
    pub vertex_num: VertexNum,
    /// weighted edges, where vertex indices are within the range [0, vertex_num)
    pub weighted_edges: Vec<(VertexIndex, VertexIndex, Weight)>,
    /// the virtual vertices
    pub virtual_vertices: Vec<VertexIndex>,
}


#[derive(Debug, Clone)]
pub struct SyndromePattern {
    /// the vertices corresponding to defect measurements
    pub defect_vertices: Vec<VertexIndex>,
    /// the edges that experience erasures, i.e. known errors;
    /// note that erasure decoding can also be implemented using `dynamic_weights`,
    /// but for user convenience we keep this interface
    pub erasures: Vec<EdgeIndex>,
    /// general dynamically weighted edges
    pub dynamic_weights: Vec<(EdgeIndex, Weight)>,
}

pub fn default_dynamic_weights() -> Vec<(EdgeIndex, Weight)> {
    vec![]
}

pub fn default_erasures() -> Vec<EdgeIndex> {
    vec![]
}

impl SyndromePattern {
    pub fn new(defect_vertices: Vec<VertexIndex>, erasures: Vec<EdgeIndex>) -> Self {
        Self {
            defect_vertices,
            erasures,
            dynamic_weights: vec![],
        }
    }
    pub fn new_dynamic_weights(
        defect_vertices: Vec<VertexIndex>,
        erasures: Vec<EdgeIndex>,
        dynamic_weights: Vec<(EdgeIndex, Weight)>,
    ) -> Self {
        Self {
            defect_vertices,
            erasures,
            dynamic_weights,
        }
    }
}

impl SyndromePattern {
    pub fn py_new(
        mut defect_vertices: Vec<VertexIndex>,
        erasures: Vec<EdgeIndex>,
        dynamic_weights: Vec<(EdgeIndex, Weight)>,
        syndrome_vertices: Option<Vec<VertexIndex>>,
    ) -> Self {
        if let Some(syndrome_vertices) = syndrome_vertices {
            assert!(
                defect_vertices.is_empty(),
                "do not pass both `syndrome_vertices` and `defect_vertices` since they're aliasing"
            );
            defect_vertices = syndrome_vertices;
        }
        assert!(
            erasures.is_empty() || dynamic_weights.is_empty(),
            "erasures and dynamic_weights cannot be provided at the same time"
        );
        Self::new_dynamic_weights(defect_vertices, erasures, dynamic_weights)
    }
    pub fn new_vertices(defect_vertices: Vec<VertexIndex>) -> Self {
        Self::new(defect_vertices, vec![])
    }
    pub fn new_empty() -> Self {
        Self::new(vec![], vec![])
    }
}

/// an efficient representation of partitioned vertices and erasures when they're ordered
#[derive(Debug, Clone)]
pub struct PartitionedSyndromePattern<'a> {
    /// the original syndrome pattern to be partitioned
    pub syndrome_pattern: &'a SyndromePattern,
    /// the defect range of this partition: it must be continuous if the defect vertices are ordered
    pub whole_defect_range: DefectRange,
}

impl<'a> PartitionedSyndromePattern<'a> {
    pub fn new(syndrome_pattern: &'a SyndromePattern) -> Self {
        assert!(
            syndrome_pattern.erasures.is_empty(),
            "erasure partition not supported yet;
        even if the edges in the erasure is well ordered, they may not be able to be represented as
        a single range simply because the partition is vertex-based. need more consideration"
        );
        Self {
            syndrome_pattern,
            whole_defect_range: DefectRange::new(0, syndrome_pattern.defect_vertices.len() as DefectIndex),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndexRange {
    pub range: [VertexNodeIndex; 2],
}

// just to distinguish them in code, essentially nothing different
pub type VertexRange = IndexRange;
pub type NodeRange = IndexRange;
pub type DefectRange = IndexRange;

impl IndexRange {
    pub fn new(start: VertexNodeIndex, end: VertexNodeIndex) -> Self {
        debug_assert!(end >= start, "invalid range [{}, {})", start, end);
        Self { range: [start, end] }
    }
    pub fn new_length(start: VertexNodeIndex, length: VertexNodeIndex) -> Self {
        Self::new(start, start + length)
    }
    pub fn is_empty(&self) -> bool {
        self.range[1] == self.range[0]
    }
    #[allow(clippy::unnecessary_cast)]
    pub fn len(&self) -> usize {
        (self.range[1] - self.range[0]) as usize
    }
    pub fn start(&self) -> VertexNodeIndex {
        self.range[0]
    }
    pub fn end(&self) -> VertexNodeIndex {
        self.range[1]
    }
    pub fn append_by(&mut self, append_count: VertexNodeIndex) {
        self.range[1] += append_count;
    }
    pub fn bias_by(&mut self, bias: VertexNodeIndex) {
        self.range[0] += bias;
        self.range[1] += bias;
    }
    pub fn sanity_check(&self) {
        assert!(self.start() <= self.end(), "invalid vertex range {:?}", self);
    }
    pub fn contains(&self, vertex_index: VertexNodeIndex) -> bool {
        vertex_index >= self.start() && vertex_index < self.end()
    }
    /// fuse two ranges together, returning (the whole range, the interfacing range)
    pub fn fuse(&self, other: &Self) -> (Self, Self) {
        self.sanity_check();
        other.sanity_check();
        assert!(self.range[1] <= other.range[0], "only lower range can fuse higher range");
        (
            Self::new(self.range[0], other.range[1]),
            Self::new(self.range[1], other.range[0]),
        )
    }
}

impl IndexRange {
    pub fn iter(&self) -> std::ops::Range<VertexNodeIndex> {
        self.range[0]..self.range[1]
    }
    pub fn contains_any(&self, vertex_indices: &[VertexNodeIndex]) -> bool {
        for vertex_index in vertex_indices.iter() {
            if self.contains(*vertex_index) {
                return true;
            }
        }
        false
    }
}

/// a general partition unit that could contain mirrored vertices
#[derive(Debug, Clone)]
pub struct PartitionUnit {
    /// unit index
    pub unit_index: usize,
    /// whether it's enabled; when disabled, the mirrored vertices behaves just like virtual vertices
    pub enabled: bool,
}

pub type PartitionUnitPtr = ArcManualSafeLock<PartitionUnit>;
pub type PartitionUnitWeak = WeakManualSafeLock<PartitionUnit>;

impl std::fmt::Debug for PartitionUnitPtr {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let partition_unit = self.read_recursive();
        write!(
            f,
            "{}{}",
            if partition_unit.enabled { "E" } else { "D" },
            partition_unit.unit_index
        )
    }
}

impl std::fmt::Debug for PartitionUnitWeak {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        self.upgrade_force().fmt(f)
    }
}

/// user input partition configuration
#[derive(Debug, Clone)]
pub struct PartitionConfig {
    /// the number of vertices
    pub vertex_num: VertexNum,
    /// detailed plan of partitioning serial modules: each serial module possesses a list of vertices, including all interface vertices
    pub partitions: Vec<VertexRange>,
    /// detailed plan of interfacing vertices
    pub fusions: Vec<(usize, usize)>,
}


impl PartitionConfig {
    pub fn new(vertex_num: VertexNum) -> Self {
        Self {
            vertex_num,
            partitions: vec![VertexRange::new(0, vertex_num as VertexIndex)],
            fusions: vec![],
        }
    }


    #[allow(clippy::unnecessary_cast)]
    pub fn info(&self) -> PartitionInfo {
        assert!(!self.partitions.is_empty(), "at least one partition must exist");
        let mut whole_ranges = vec![];
        let mut owning_ranges = vec![];
        for &partition in self.partitions.iter() {
            partition.sanity_check();
            assert!(
                partition.end() <= self.vertex_num as VertexIndex,
                "invalid vertex index {} in partitions",
                partition.end()
            );
            whole_ranges.push(partition);
            owning_ranges.push(partition);
        }
        let unit_count = self.partitions.len() + self.fusions.len();
        let mut parents: Vec<Option<usize>> = (0..unit_count).map(|_| None).collect();
        for (fusion_index, (left_index, right_index)) in self.fusions.iter().enumerate() {
            let unit_index = fusion_index + self.partitions.len();
            assert!(
                *left_index < unit_index,
                "dependency wrong, {} depending on {}",
                unit_index,
                left_index
            );
            assert!(
                *right_index < unit_index,
                "dependency wrong, {} depending on {}",
                unit_index,
                right_index
            );
            assert!(parents[*left_index].is_none(), "cannot fuse {} twice", left_index);
            assert!(parents[*right_index].is_none(), "cannot fuse {} twice", right_index);
            parents[*left_index] = Some(unit_index);
            parents[*right_index] = Some(unit_index);
            // fusing range
            let (whole_range, interface_range) = whole_ranges[*left_index].fuse(&whole_ranges[*right_index]);
            whole_ranges.push(whole_range);
            owning_ranges.push(interface_range);
        }
        // check that all nodes except for the last one has been merged
        for (unit_index, parent) in parents.iter().enumerate().take(unit_count - 1) {
            assert!(parent.is_some(), "found unit {} without being fused", unit_index);
        }
        // check that the final node has the full range
        let last_unit_index = self.partitions.len() + self.fusions.len() - 1;
        assert!(
            whole_ranges[last_unit_index].start() == 0,
            "final range not covering all vertices {:?}",
            whole_ranges[last_unit_index]
        );
        assert!(
            whole_ranges[last_unit_index].end() == self.vertex_num as VertexIndex,
            "final range not covering all vertices {:?}",
            whole_ranges[last_unit_index]
        );
        // construct partition info
        let mut partition_unit_info: Vec<_> = (0..self.partitions.len() + self.fusions.len())
            .map(|i| PartitionUnitInfo {
                whole_range: whole_ranges[i],
                owning_range: owning_ranges[i],
                children: if i >= self.partitions.len() {
                    Some(self.fusions[i - self.partitions.len()])
                } else {
                    None
                },
                parent: parents[i],
                leaves: if i < self.partitions.len() { vec![i] } else { vec![] },
                descendants: BTreeSet::new(),
            })
            .collect();
        // build descendants
        for (fusion_index, (left_index, right_index)) in self.fusions.iter().enumerate() {
            let unit_index = fusion_index + self.partitions.len();
            let mut leaves = vec![];
            leaves.extend(partition_unit_info[*left_index].leaves.iter());
            leaves.extend(partition_unit_info[*right_index].leaves.iter());
            partition_unit_info[unit_index].leaves.extend(leaves.iter());
            let mut descendants = vec![];
            descendants.push(*left_index);
            descendants.push(*right_index);
            descendants.extend(partition_unit_info[*left_index].descendants.iter());
            descendants.extend(partition_unit_info[*right_index].descendants.iter());
            partition_unit_info[unit_index].descendants.extend(descendants.iter());
        }
        let mut vertex_to_owning_unit: Vec<_> = (0..self.vertex_num).map(|_| usize::MAX).collect();
        for (unit_index, unit_range) in partition_unit_info.iter().map(|x| x.owning_range).enumerate() {
            for vertex_index in unit_range.iter() {
                vertex_to_owning_unit[vertex_index as usize] = unit_index;
            }
        }
        PartitionInfo {
            config: self.clone(),
            units: partition_unit_info,
            vertex_to_owning_unit,
        }
    }
}

#[derive(Debug, Clone)]
pub struct PartitionInfo {
    /// the initial configuration that creates this info
    pub config: PartitionConfig,
    /// individual info of each unit
    pub units: Vec<PartitionUnitInfo>,
    /// the mapping from vertices to the owning unit: serial unit (holding real vertices) as well as parallel units (holding interfacing vertices);
    /// used for loading syndrome to the holding units
    pub vertex_to_owning_unit: Vec<usize>,
}


impl PartitionInfo {
    /// split a sequence of syndrome into multiple parts, each corresponds to a unit;
    /// this is a slow method and should only be used when the syndrome pattern is not well-ordered
    #[allow(clippy::unnecessary_cast)]
    pub fn partition_syndrome_unordered(&self, syndrome_pattern: &SyndromePattern) -> Vec<SyndromePattern> {
        let mut partitioned_syndrome: Vec<_> = (0..self.units.len()).map(|_| SyndromePattern::new_empty()).collect();
        for defect_vertex in syndrome_pattern.defect_vertices.iter() {
            let unit_index = self.vertex_to_owning_unit[*defect_vertex as usize];
            partitioned_syndrome[unit_index].defect_vertices.push(*defect_vertex);
        }
        // TODO: partition edges
        partitioned_syndrome
    }

}

impl<'a> PartitionedSyndromePattern<'a> {
    /// partition the syndrome pattern into 2 partitioned syndrome pattern and my whole range
    #[allow(clippy::unnecessary_cast)]
    pub fn partition(&self, partition_unit_info: &PartitionUnitInfo) -> (Self, (Self, Self)) {
        // first binary search the start of owning defect vertices
        let owning_start_index = {
            let mut left_index = self.whole_defect_range.start();
            let mut right_index = self.whole_defect_range.end();
            while left_index != right_index {
                let mid_index = (left_index + right_index) / 2;
                let mid_defect_vertex = self.syndrome_pattern.defect_vertices[mid_index as usize];
                if mid_defect_vertex < partition_unit_info.owning_range.start() {
                    left_index = mid_index + 1;
                } else {
                    right_index = mid_index;
                }
            }
            left_index
        };
        // second binary search the end of owning defect vertices
        let owning_end_index = {
            let mut left_index = self.whole_defect_range.start();
            let mut right_index = self.whole_defect_range.end();
            while left_index != right_index {
                let mid_index = (left_index + right_index) / 2;
                let mid_defect_vertex = self.syndrome_pattern.defect_vertices[mid_index as usize];
                if mid_defect_vertex < partition_unit_info.owning_range.end() {
                    left_index = mid_index + 1;
                } else {
                    right_index = mid_index;
                }
            }
            left_index
        };
        (
            Self {
                syndrome_pattern: self.syndrome_pattern,
                whole_defect_range: DefectRange::new(owning_start_index, owning_end_index),
            },
            (
                Self {
                    syndrome_pattern: self.syndrome_pattern,
                    whole_defect_range: DefectRange::new(self.whole_defect_range.start(), owning_start_index),
                },
                Self {
                    syndrome_pattern: self.syndrome_pattern,
                    whole_defect_range: DefectRange::new(owning_end_index, self.whole_defect_range.end()),
                },
            ),
        )
    }

    #[allow(clippy::unnecessary_cast)]
    pub fn expand(&self) -> SyndromePattern {
        let mut defect_vertices = Vec::with_capacity(self.whole_defect_range.len());
        for defect_index in self.whole_defect_range.iter() {
            defect_vertices.push(self.syndrome_pattern.defect_vertices[defect_index as usize]);
        }
        SyndromePattern::new(defect_vertices, vec![])
    }
}

#[derive(Debug, Clone)]
pub struct PartitionUnitInfo {
    /// the whole range of units
    pub whole_range: VertexRange,
    /// the owning range of units, meaning vertices inside are exclusively belonging to the unit
    pub owning_range: VertexRange,
    /// left and right
    pub children: Option<(usize, usize)>,
    /// parent dual module
    pub parent: Option<usize>,
    /// all the leaf dual modules
    pub leaves: Vec<usize>,
    /// all the descendants
    pub descendants: BTreeSet<usize>,
}



#[derive(Debug, Clone)]
pub struct PartitionedSolverInitializer {
    /// unit index
    pub unit_index: usize,
    /// the number of all vertices (including those partitioned into other serial modules)
    pub vertex_num: VertexNum,
    /// the number of all edges (including those partitioned into other serial modules)
    pub edge_num: usize,
    /// vertices exclusively owned by this partition; this part must be a continuous range
    pub owning_range: VertexRange,
    /// applicable when all the owning vertices are partitioned (i.e. this belongs to a fusion unit)
    pub owning_interface: Option<PartitionUnitWeak>,
    /// if applicable, parent interface comes first, then the grandparent interface, ... note that some ancestor might be skipped because it has no mirrored vertices;
    /// we skip them because if the partition is in a chain, most of them would only have to know two interfaces on the left and on the right; nothing else necessary.
    /// (unit_index, list of vertices owned by this ancestor unit and should be mirrored at this partition and whether it's virtual)
    pub interfaces: Vec<(PartitionUnitWeak, Vec<(VertexIndex, bool)>)>,
    /// weighted edges, where the first vertex index is within the range [vertex_index_bias, vertex_index_bias + vertex_num) and
    /// the second is either in [vertex_index_bias, vertex_index_bias + vertex_num) or inside
    pub weighted_edges: Vec<(VertexIndex, VertexIndex, Weight, EdgeIndex)>,
    /// the virtual vertices
    pub virtual_vertices: Vec<VertexIndex>,
}

/// perform index transformation
#[allow(clippy::unnecessary_cast)]
pub fn build_old_to_new(reordered_vertices: &[VertexIndex]) -> Vec<Option<VertexIndex>> {
    let mut old_to_new: Vec<Option<VertexIndex>> = (0..reordered_vertices.len()).map(|_| None).collect();
    for (new_index, old_index) in reordered_vertices.iter().enumerate() {
        assert_eq!(old_to_new[*old_index as usize], None, "duplicate vertex found {}", old_index);
        old_to_new[*old_index as usize] = Some(new_index as VertexIndex);
    }
    old_to_new
}

/// translate defect vertices into the current new index given reordered_vertices
#[allow(clippy::unnecessary_cast)]
pub fn translated_defect_to_reordered(
    reordered_vertices: &[VertexIndex],
    old_defect_vertices: &[VertexIndex],
) -> Vec<VertexIndex> {
    let old_to_new = build_old_to_new(reordered_vertices);
    old_defect_vertices
        .iter()
        .map(|old_index| old_to_new[*old_index as usize].unwrap())
        .collect()
}

impl SolverInitializer {
    pub fn new(
        vertex_num: VertexNum,
        weighted_edges: Vec<(VertexIndex, VertexIndex, Weight)>,
        virtual_vertices: Vec<VertexIndex>,
    ) -> SolverInitializer {
        SolverInitializer {
            vertex_num,
            weighted_edges,
            virtual_vertices,
        }
    }
}

impl SolverInitializer {
    #[allow(clippy::unnecessary_cast)]
    pub fn syndrome_of(&self, subgraph: &[EdgeIndex]) -> BTreeSet<VertexIndex> {
        let mut defects = BTreeSet::new();
        for edge_index in subgraph {
            let (left, right, _weight) = self.weighted_edges[*edge_index as usize];
            for vertex_index in [left, right] {
                if defects.contains(&vertex_index) {
                    defects.remove(&vertex_index);
                } else {
                    defects.insert(vertex_index);
                }
            }
        }
        // remove virtual vertices
        for vertex_index in self.virtual_vertices.iter() {
            defects.remove(vertex_index);
        }
        defects
    }
}

/// timestamp type determines how many fast clear before a hard clear is required, see [`FastClear`]
pub type FastClearTimestamp = usize;

#[allow(dead_code)]
/// use Xoshiro256StarStar for deterministic random number generator
pub type DeterministicRng = rand_xoshiro::Xoshiro256StarStar;

pub trait F64Rng {
    fn next_f64(&mut self) -> f64;
}

impl F64Rng for DeterministicRng {
    fn next_f64(&mut self) -> f64 {
        f64::from_bits(0x3FF << 52 | self.next_u64() >> 12) - 1.
    }
}

/// record the decoding time of multiple syndrome patterns
pub struct BenchmarkProfiler {
    /// each record corresponds to a different syndrome pattern
    pub records: Vec<BenchmarkProfilerEntry>,
    /// summation of all decoding time
    pub sum_round_time: f64,
    /// syndrome count
    pub sum_syndrome: usize,
    /// noisy measurement round
    pub noisy_measurements: VertexNum,
    /// the file to output the profiler results
    pub benchmark_profiler_output: Option<File>,
}

impl BenchmarkProfiler {
    pub fn new(noisy_measurements: VertexNum, detail_log_file: Option<(String, &PartitionInfo)>) -> Self {
        let benchmark_profiler_output = detail_log_file.map(|(filename, partition_info)| {
            let mut file = File::create(filename).unwrap();
            file.write_all(serde_json::to_string(&partition_info.config).unwrap().as_bytes())
                .unwrap();
            file.write_all(b"\n").unwrap();
            file.write_all(
                serde_json::to_string(&json!({
                    "noisy_measurements": noisy_measurements,
                }))
                .unwrap()
                .as_bytes(),
            )
            .unwrap();
            file.write_all(b"\n").unwrap();
            file
        });
        Self {
            records: vec![],
            sum_round_time: 0.,
            sum_syndrome: 0,
            noisy_measurements,
            benchmark_profiler_output,
        }
    }
    /// record the beginning of a decoding procedure
    pub fn begin(&mut self, syndrome_pattern: &SyndromePattern) {
        // sanity check last entry, if exists, is complete
        if let Some(last_entry) = self.records.last() {
            assert!(
                last_entry.is_complete(),
                "the last benchmark profiler entry is not complete, make sure to call `begin` and `end` in pairs"
            );
        }
        let entry = BenchmarkProfilerEntry::new(syndrome_pattern);
        self.records.push(entry);
        self.records.last_mut().unwrap().record_begin();
    }
    pub fn event(&mut self, event_name: String) {
        let last_entry = self
            .records
            .last_mut()
            .expect("last entry not exists, call `begin` before `end`");
        last_entry.record_event(event_name);
    }
    /// record the ending of a decoding procedure
    pub fn end(&mut self, solver: Option<&dyn PrimalDualSolver>) {
        let last_entry = self
            .records
            .last_mut()
            .expect("last entry not exists, call `begin` before `end`");
        last_entry.record_end();
        self.sum_round_time += last_entry.round_time.unwrap();
        self.sum_syndrome += last_entry.syndrome_pattern.defect_vertices.len();
        if let Some(file) = self.benchmark_profiler_output.as_mut() {
            let mut events = serde_json::Map::new();
            for (event_name, time) in last_entry.events.iter() {
                events.insert(event_name.clone(), json!(time));
            }
            let mut value = json!({
                "round_time": last_entry.round_time.unwrap(),
                "defect_num": last_entry.syndrome_pattern.defect_vertices.len(),
                "events": events,
            });
            if let Some(solver) = solver {
                let solver_profile = solver.generate_profiler_report();
                value
                    .as_object_mut()
                    .unwrap()
                    .insert("solver_profile".to_string(), solver_profile);
            }
            file.write_all(serde_json::to_string(&value).unwrap().as_bytes()).unwrap();
            file.write_all(b"\n").unwrap();
        }
    }
    /// print out a brief one-line statistics
    pub fn brief(&self) -> String {
        let total = self.sum_round_time / (self.records.len() as f64);
        let per_round = total / (1. + self.noisy_measurements as f64);
        let per_defect = self.sum_round_time / (self.sum_syndrome as f64);
        format!("total: {total:.3e}, round: {per_round:.3e}, defect: {per_defect:.3e},")
    }
}

pub struct BenchmarkProfilerEntry {
    /// the syndrome pattern of this decoding problem
    pub syndrome_pattern: SyndromePattern,
    /// the time of beginning a decoding procedure
    begin_time: Option<Instant>,
    /// record additional events
    pub events: Vec<(String, f64)>,
    /// interval between calling [`Self::record_begin`] to calling [`Self::record_end`]
    pub round_time: Option<f64>,
}

impl BenchmarkProfilerEntry {
    pub fn new(syndrome_pattern: &SyndromePattern) -> Self {
        Self {
            syndrome_pattern: syndrome_pattern.clone(),
            begin_time: None,
            events: vec![],
            round_time: None,
        }
    }
    /// record the beginning of a decoding procedure
    pub fn record_begin(&mut self) {
        assert_eq!(self.begin_time, None, "do not call `record_begin` twice on the same entry");
        self.begin_time = Some(Instant::now());
    }
    /// record the ending of a decoding procedure
    pub fn record_end(&mut self) {
        let begin_time = self
            .begin_time
            .as_ref()
            .expect("make sure to call `record_begin` before calling `record_end`");
        self.round_time = Some(begin_time.elapsed().as_secs_f64());
    }
    pub fn record_event(&mut self, event_name: String) {
        let begin_time = self
            .begin_time
            .as_ref()
            .expect("make sure to call `record_begin` before calling `record_end`");
        self.events.push((event_name, begin_time.elapsed().as_secs_f64()));
    }
    pub fn is_complete(&self) -> bool {
        self.round_time.is_some()
    }
}

/**
 * If you want to modify a field of a Rust struct, it will return a copy of it to avoid memory unsafety.
 * Thus, typical way of modifying a python field doesn't work, e.g. `obj.a.b.c = 1` won't actually modify `obj`.
 * This helper class is used to modify a field easier; but please note this can be very time consuming if not optimized well.
 *
 * Example:
 * with PyMut(code, "vertices") as vertices:
 *     with fb.PyMut(vertices[0], "position") as position:
 *         position.i = 100
*/







#[cfg(test)]
pub mod tests {
    use super::*;

    /// test syndrome partition utilities
    #[test]
    fn util_partitioned_syndrome_pattern_1() {
        // cargo test util_partitioned_syndrome_pattern_1 -- --nocapture
        let mut partition_config = PartitionConfig::new(132);
        partition_config.partitions = vec![
            VertexRange::new(0, 72),   // unit 0
            VertexRange::new(84, 132), // unit 1
        ];
        partition_config.fusions = vec![
            (0, 1), // unit 2, by fusing 0 and 1
        ];
        let partition_info = partition_config.info();
        let tests = vec![
            (vec![10, 11, 12, 71, 72, 73, 84, 85, 111], DefectRange::new(4, 6)),
            (vec![10, 11, 12, 13, 71, 72, 73, 84, 85, 111], DefectRange::new(5, 7)),
            (vec![10, 11, 12, 71, 72, 73, 83, 84, 85, 111], DefectRange::new(4, 7)),
            (
                vec![10, 11, 12, 71, 72, 73, 84, 85, 100, 101, 102, 103, 111],
                DefectRange::new(4, 6),
            ),
        ];
        for (defect_vertices, expected_defect_range) in tests.into_iter() {
            let syndrome_pattern = SyndromePattern::new(defect_vertices, vec![]);
            let partitioned_syndrome_pattern = PartitionedSyndromePattern::new(&syndrome_pattern);
            let (owned_partitioned, (_left_partitioned, _right_partitioned)) =
                partitioned_syndrome_pattern.partition(&partition_info.units[2]);
            println!("defect_range: {:?}", owned_partitioned.whole_defect_range);
            assert_eq!(owned_partitioned.whole_defect_range, expected_defect_range);
        }
    }
}