Skip to content

Added a dispatch based Noisify API ( For Issue #729)#750

Open
brohan-byte wants to merge 8 commits into
QuantumSavory:masterfrom
brohan-byte:noisify
Open

Added a dispatch based Noisify API ( For Issue #729)#750
brohan-byte wants to merge 8 commits into
QuantumSavory:masterfrom
brohan-byte:noisify

Conversation

@brohan-byte

@brohan-byte brohan-byte commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Key Notes:

  • Added a CircuitNoise object in order to capture distinct noise models for single-qubit gates, two-qubit gates, idle qubits, measurements, and reset operations. This is used by the Noisify API later on.
  • Added a NoNoise Sentinel to simplify the dispatch of omitted noise.
  • Added a time-step/layer based idle noise functionality based on how Quantikz.jl handles circuit scheduling and gate parallelism.
  • Added a dispatch-based noisify API that supports both structured CircuitNoise models and individual AbstractNoise instances.
  • Added tests for noisify (these are pretty hardcoded, any suggestions on improving them would be appreciated)

Most of the code was written by me but was passed through an LLM to restructure. In particular, the function apply_idle_noise was revised with LLM help. I've checked and verified the functionality of it, and I'll make sure that the logic is clearly documented

Fixes #729

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (Julia v1)

Time benchmarks
master d9e18ab... master / d9e18ab...
circuitsim/compactification/compact 6.85 ± 0.018 ms 6.94 ± 0.018 ms 0.987 ± 0.0037
circuitsim/compactification/no_compact 6.91 ± 0.03 ms 7 ± 0.03 ms 0.988 ± 0.006
circuitsim/mctrajectories/q1001_r1 16.4 ± 0.77 ms 15.8 ± 0.36 ms 1.04 ± 0.055
circuitsim/mctrajectories/q101_r1 0.177 ± 0.011 ms 0.173 ± 0.011 ms 1.02 ± 0.095
circuitsim/mctrajectories_sumtype/q1001_r1 15.5 ± 0.66 ms 13.9 ± 1.9 ms 1.12 ± 0.16
circuitsim/mctrajectories_sumtype/q101_r1 0.122 ± 0.0027 ms 0.122 ± 0.004 ms 0.999 ± 0.04
circuitsim/mctrajectories_union/q1001_r1 15.5 ± 1.1 ms 13.6 ± 0.33 ms 1.14 ± 0.085
circuitsim/mctrajectories_union/q101_r1 0.121 ± 0.0022 ms 0.121 ± 0.0032 ms 1 ± 0.032
circuitsim/pftrajectories/q1001_r1 0.0805 ± 0.036 ms 0.0801 ± 0.034 ms 1.01 ± 0.62
circuitsim/pftrajectories/q1001_r100 0.188 ± 0.012 ms 0.199 ± 0.012 ms 0.944 ± 0.084
circuitsim/pftrajectories/q1001_r10000 1.15 ± 0.016 ms 1.25 ± 0.0094 ms 0.92 ± 0.015
circuitsim/pftrajectories/q101_r1 8.15 ± 3 μs 8.12 ± 3.1 μs 1 ± 0.53
circuitsim/pftrajectories_sumtype/q1001_r1 0.138 ± 0.0019 ms 0.164 ± 0.0032 ms 0.838 ± 0.02
circuitsim/pftrajectories_sumtype/q1001_r100 0.246 ± 0.0092 ms 0.283 ± 0.01 ms 0.87 ± 0.045
circuitsim/pftrajectories_sumtype/q1001_r10000 1.21 ± 0.012 ms 1.33 ± 0.011 ms 0.911 ± 0.012
circuitsim/pftrajectories_sumtype/q1001_r10000_fastrow 6.17 ± 0.14 ms 6.09 ± 0.028 ms 1.01 ± 0.023
circuitsim/pftrajectories_sumtype/q101_r1 13.9 ± 0.089 μs 16.6 ± 0.11 μs 0.837 ± 0.0078
circuitsim/pftrajectories_union/q1001_r1 23 ± 0.07 μs 23.6 ± 0.07 μs 0.974 ± 0.0041
circuitsim/pftrajectories_union/q1001_r100 0.134 ± 0.00076 ms 0.143 ± 0.00053 ms 0.934 ± 0.0064
circuitsim/pftrajectories_union/q1001_r10000 1.09 ± 0.0083 ms 1.18 ± 0.012 ms 0.921 ± 0.012
circuitsim/pftrajectories_union/q101_r1 2.38 ± 0.011 μs 2.44 ± 0.01 μs 0.975 ± 0.006
clifford/dense/cnot250_on_dense500_destab 12.4 ± 0.067 ms 11.3 ± 0.039 ms 1.09 ± 0.0071
clifford/dense/cnot250_on_dense500_stab 5.82 ± 0.025 ms 5.56 ± 0.028 ms 1.05 ± 0.0069
clifford/dense/cnot250_on_diag500_destab 1.13 ± 0.005 ms 0.977 ± 0.0035 ms 1.16 ± 0.0066
clifford/dense/cnot250_on_diag500_stab 0.494 ± 0.011 ms 0.569 ± 0.011 ms 0.867 ± 0.025
clifford/dense/cnot_on_dense500_destab 0.0449 ± 0.00042 ms 0.0451 ± 0.00046 ms 0.995 ± 0.014
clifford/dense/cnot_on_dense500_stab 22.8 ± 0.23 μs 21.2 ± 0.26 μs 1.07 ± 0.017
clifford/dense/cnot_on_diag500_destab 26.4 ± 0.48 μs 26.7 ± 0.4 μs 0.987 ± 0.023
clifford/dense/cnot_on_diag500_stab 13.5 ± 0.26 μs 13.1 ± 0.25 μs 1.03 ± 0.028
clifford/dense/dense500_on_dense500_destab 11.1 ± 0.053 ms 11.2 ± 0.038 ms 0.997 ± 0.0058
clifford/dense/dense500_on_dense500_stab 5.6 ± 0.031 ms 5.56 ± 0.026 ms 1.01 ± 0.0073
clifford/dense/dense500_on_diag500_destab 1.13 ± 0.0045 ms 1.13 ± 0.0047 ms 1 ± 0.0058
clifford/dense/dense500_on_diag500_stab 0.492 ± 0.011 ms 0.569 ± 0.011 ms 0.864 ± 0.026
clifford/symbolic/cnot250_on_dense500_destab 1.52 ± 0.014 ms 1.5 ± 0.012 ms 1.01 ± 0.013
clifford/symbolic/cnot250_on_dense500_stab 0.755 ± 0.0088 ms 0.749 ± 0.008 ms 1.01 ± 0.016
clifford/symbolic/cnot250_on_diag500_destab 1.24 ± 0.015 ms 1.23 ± 0.022 ms 1.01 ± 0.022
clifford/symbolic/cnot250_on_diag500_stab 0.621 ± 0.011 ms 0.622 ± 0.011 ms 0.999 ± 0.025
clifford/symbolic/cnot_on_dense500_destab 5 ± 0.14 μs 4.93 ± 0.1 μs 1.01 ± 0.035
clifford/symbolic/cnot_on_dense500_stab 2.5 ± 0.03 μs 2.5 ± 0.04 μs 1 ± 0.02
clifford/symbolic/cnot_on_diag500_destab 4.95 ± 0.071 μs 4.92 ± 0.061 μs 1.01 ± 0.019
clifford/symbolic/cnot_on_diag500_stab 2.47 ± 0.039 μs 2.5 ± 0.039 μs 0.988 ± 0.022
ecc/evaluate_decoder/shor_bp_comm 2.15 ± 0.067 ms 2.21 ± 0.065 ms 0.974 ± 0.042
ecc/evaluate_decoder/shor_bp_naivesyn 4.74 ± 0.14 ms 4.75 ± 0.14 ms 0.999 ± 0.042
ecc/evaluate_decoder/shor_bp_shorsyn 5.09 ± 0.12 ms 5.1 ± 0.12 ms 0.997 ± 0.033
ecc/evaluate_decoder/shor_pybp_comm 21.5 ± 0.94 ms 21.2 ± 1 ms 1.01 ± 0.067
ecc/evaluate_decoder/shor_pybp_naivesyn 0.0428 ± 0.0018 s 0.0419 ± 0.0019 s 1.02 ± 0.063
ecc/evaluate_decoder/shor_pybp_shorsyn 0.0437 ± 0.0021 s 0.043 ± 0.0023 s 1.02 ± 0.073
ecc/evaluate_decoder/shor_pybposd_comm 21.4 ± 1 ms 21 ± 1.1 ms 1.02 ± 0.073
ecc/evaluate_decoder/shor_pybposd_naivesyn 0.0431 ± 0.0019 s 0.0428 ± 0.0021 s 1.01 ± 0.067
ecc/evaluate_decoder/shor_pybposd_shorsyn 0.0434 ± 0.002 s 0.0436 ± 0.0027 s 0.994 ± 0.077
ecc/evaluate_decoder/shor_table_comm 0.296 ± 0.018 ms 0.293 ± 0.026 ms 1.01 ± 0.11
ecc/evaluate_decoder/shor_table_naivesyn 0.989 ± 0.0058 ms 0.979 ± 0.005 ms 1.01 ± 0.0079
ecc/evaluate_decoder/shor_table_shorsyn 1.38 ± 0.028 ms 1.36 ± 0.052 ms 1.01 ± 0.044
ecc/evaluate_decoder/toric8_bp_comm 0.873 ± 0.045 s 0.741 ± 0.03 s 1.18 ± 0.077
ecc/evaluate_decoder/toric8_bp_naivesyn 1.51 ± 0.04 s 1.48 ± 0.04 s 1.02 ± 0.039
ecc/evaluate_decoder/toric8_bp_shorsyn 1.52 ± 0.054 s 1.52 ± 0.068 s 0.998 ± 0.057
ecc/evaluate_decoder/toric8_pybp_comm 0.0651 ± 0.0023 s 0.0639 ± 0.0024 s 1.02 ± 0.053
ecc/evaluate_decoder/toric8_pybp_naivesyn 0.134 ± 0.0056 s 0.133 ± 0.0043 s 1.01 ± 0.053
ecc/evaluate_decoder/toric8_pybp_shorsyn 0.142 ± 0.0041 s 0.142 ± 0.0041 s 0.998 ± 0.041
ecc/evaluate_decoder/toric8_pybposd_comm 0.0646 ± 0.0026 s 0.0647 ± 0.0025 s 0.999 ± 0.056
ecc/evaluate_decoder/toric8_pybposd_naivesyn 0.134 ± 0.0034 s 0.135 ± 0.0063 s 0.99 ± 0.053
ecc/evaluate_decoder/toric8_pybposd_shorsyn 0.145 ± 0.0062 s 0.142 ± 0.0032 s 1.02 ± 0.05
ecc/evaluate_decoder/toric8_pymatch_comm 3.52 ± 0.07 ms 3.46 ± 0.048 ms 1.02 ± 0.025
ecc/evaluate_decoder/toric8_pymatch_naivesyn 13.5 ± 0.25 ms 13.3 ± 0.29 ms 1.02 ± 0.029
ecc/evaluate_decoder/toric8_pymatch_shorsyn 22.1 ± 1.1 ms 21.7 ± 1.1 ms 1.02 ± 0.073
ecc/evaluate_decoder/toric8_table_comm 3.77 ± 0.042 ms 3.76 ± 0.039 ms 1 ± 0.015
ecc/evaluate_decoder/toric8_table_naivesyn 13.4 ± 0.11 ms 13.3 ± 0.11 ms 1.01 ± 0.012
ecc/evaluate_decoder/toric8_table_shorsyn 22.1 ± 0.21 ms 21.8 ± 0.19 ms 1.01 ± 0.013
pauli/mul/100 0.04 ± 0 μs 0.04 ± 0 μs 1 ± 0
pauli/mul/1000 0.05 ± 0 μs 0.05 ± 0.01 μs 1 ± 0.2
pauli/mul/100000 0.822 ± 0.061 μs 0.821 ± 0.051 μs 1 ± 0.097
pauli/mul/20000000 0.174 ± 0.019 ms 0.158 ± 0.019 ms 1.1 ± 0.18
stabilizer/canon/cano500 3.16 ± 0.028 ms 3.12 ± 0.031 ms 1.01 ± 0.013
stabilizer/canon/diag_cano500 0.653 ± 0.012 ms 0.653 ± 0.013 ms 0.999 ± 0.026
stabilizer/canon/diag_gott500 2.55 ± 0.038 ms 2.54 ± 0.046 ms 1.01 ± 0.024
stabilizer/canon/diag_rref500 0.602 ± 0.013 ms 0.602 ± 0.013 ms 1 ± 0.03
stabilizer/canon/gott500 5.1 ± 0.24 ms 5.06 ± 0.22 ms 1.01 ± 0.065
stabilizer/canon/md_cano500 1.2 ± 0.016 ms 1.19 ± 0.015 ms 1.01 ± 0.019
stabilizer/canon/md_rref500 1.32 ± 0.019 ms 1.18 ± 0.015 ms 1.11 ± 0.021
stabilizer/canon/rref500 3.12 ± 0.031 ms 3.1 ± 0.018 ms 1.01 ± 0.012
stabilizer/project/destabilizer 16.4 ± 0.34 μs 17.7 ± 0.21 μs 0.927 ± 0.022
stabilizer/project/stabilizer 9.22 ± 0.13 μs 8.79 ± 0.18 μs 1.05 ± 0.026
stabilizer/tensor/diag_pow5_20 2.46 ± 1.1 ms 2.41 ± 1.3 ms 1.02 ± 0.72
stabilizer/tensor/pow5_20 2.98 ± 0.27 μs 4.54 ± 0.27 μs 0.658 ± 0.071
stabilizer/trace/destabilizer 25.5 ± 0.43 μs 22 ± 0.35 μs 1.16 ± 0.027
stabilizer/trace/stabilizer 24.6 ± 0.45 μs 24.5 ± 0.44 μs 1 ± 0.026
time_to_load 2.03 ± 0.017 s 2.03 ± 0.019 s 1 ± 0.012
Memory benchmarks
master d9e18ab... master / d9e18ab...
circuitsim/compactification/compact 0 allocs: 0 B 0 allocs: 0 B
circuitsim/compactification/no_compact 6 k allocs: 0.275 MB 6 k allocs: 0.275 MB 1
circuitsim/mctrajectories/q1001_r1 18 k allocs: 0.489 MB 18 k allocs: 0.489 MB 1
circuitsim/mctrajectories/q101_r1 1.82 k allocs: 0.0493 MB 1.82 k allocs: 0.0493 MB 1
circuitsim/mctrajectories_sumtype/q1001_r1 9 allocs: 0.484 kB 9 allocs: 0.484 kB 1
circuitsim/mctrajectories_sumtype/q101_r1 8 allocs: 0.25 kB 8 allocs: 0.25 kB 1
circuitsim/mctrajectories_union/q1001_r1 9 allocs: 0.484 kB 9 allocs: 0.484 kB 1
circuitsim/mctrajectories_union/q101_r1 8 allocs: 0.25 kB 8 allocs: 0.25 kB 1
circuitsim/pftrajectories/q1001_r1 2 k allocs: 0.0916 MB 2 k allocs: 0.0916 MB 1
circuitsim/pftrajectories/q1001_r100 2 k allocs: 0.0916 MB 2 k allocs: 0.0916 MB 1
circuitsim/pftrajectories/q1001_r10000 2 k allocs: 0.0916 MB 2 k allocs: 0.0916 MB 1
circuitsim/pftrajectories/q101_r1 0.201 k allocs: 9.42 kB 0.201 k allocs: 9.42 kB 1
circuitsim/pftrajectories_sumtype/q1001_r1 0 allocs: 0 B 0 allocs: 0 B
circuitsim/pftrajectories_sumtype/q1001_r100 0 allocs: 0 B 0 allocs: 0 B
circuitsim/pftrajectories_sumtype/q1001_r10000 0 allocs: 0 B 0 allocs: 0 B
circuitsim/pftrajectories_sumtype/q1001_r10000_fastrow 0 allocs: 0 B 0 allocs: 0 B
circuitsim/pftrajectories_sumtype/q101_r1 0 allocs: 0 B 0 allocs: 0 B
circuitsim/pftrajectories_union/q1001_r1 2 allocs: 0.0938 kB 2 allocs: 0.0938 kB 1
circuitsim/pftrajectories_union/q1001_r100 2 allocs: 0.0938 kB 2 allocs: 0.0938 kB 1
circuitsim/pftrajectories_union/q1001_r10000 2 allocs: 0.0938 kB 2 allocs: 0.0938 kB 1
circuitsim/pftrajectories_union/q101_r1 2 allocs: 0.0938 kB 2 allocs: 0.0938 kB 1
clifford/dense/cnot250_on_dense500_destab 0 allocs: 0 B 0 allocs: 0 B
clifford/dense/cnot250_on_dense500_stab 0 allocs: 0 B 0 allocs: 0 B
clifford/dense/cnot250_on_diag500_destab 0 allocs: 0 B 0 allocs: 0 B
clifford/dense/cnot250_on_diag500_stab 0 allocs: 0 B 0 allocs: 0 B
clifford/dense/cnot_on_dense500_destab 3 allocs: 0.0938 kB 3 allocs: 0.0938 kB 1
clifford/dense/cnot_on_dense500_stab 3 allocs: 0.0938 kB 3 allocs: 0.0938 kB 1
clifford/dense/cnot_on_diag500_destab 3 allocs: 0.0938 kB 3 allocs: 0.0938 kB 1
clifford/dense/cnot_on_diag500_stab 3 allocs: 0.0938 kB 3 allocs: 0.0938 kB 1
clifford/dense/dense500_on_dense500_destab 0 allocs: 0 B 0 allocs: 0 B
clifford/dense/dense500_on_dense500_stab 0 allocs: 0 B 0 allocs: 0 B
clifford/dense/dense500_on_diag500_destab 0 allocs: 0 B 0 allocs: 0 B
clifford/dense/dense500_on_diag500_stab 0 allocs: 0 B 0 allocs: 0 B
clifford/symbolic/cnot250_on_dense500_destab 0 allocs: 0 B 0 allocs: 0 B
clifford/symbolic/cnot250_on_dense500_stab 0 allocs: 0 B 0 allocs: 0 B
clifford/symbolic/cnot250_on_diag500_destab 0 allocs: 0 B 0 allocs: 0 B
clifford/symbolic/cnot250_on_diag500_stab 0 allocs: 0 B 0 allocs: 0 B
clifford/symbolic/cnot_on_dense500_destab 0 allocs: 0 B 0 allocs: 0 B
clifford/symbolic/cnot_on_dense500_stab 0 allocs: 0 B 0 allocs: 0 B
clifford/symbolic/cnot_on_diag500_destab 0 allocs: 0 B 0 allocs: 0 B
clifford/symbolic/cnot_on_diag500_stab 0 allocs: 0 B 0 allocs: 0 B
ecc/evaluate_decoder/shor_bp_comm 0.0397 M allocs: 1.61 MB 0.0396 M allocs: 1.61 MB 1
ecc/evaluate_decoder/shor_bp_naivesyn 0.0748 M allocs: 3.15 MB 0.0751 M allocs: 3.16 MB 0.996
ecc/evaluate_decoder/shor_bp_shorsyn 0.0753 M allocs: 3.21 MB 0.0752 M allocs: 3.21 MB 1
ecc/evaluate_decoder/shor_pybp_comm 0.0935 M allocs: 3.29 MB 0.0935 M allocs: 3.29 MB 1
ecc/evaluate_decoder/shor_pybp_naivesyn 0.182 M allocs: 6.49 MB 0.182 M allocs: 6.49 MB 1
ecc/evaluate_decoder/shor_pybp_shorsyn 0.182 M allocs: 6.55 MB 0.182 M allocs: 6.55 MB 1
ecc/evaluate_decoder/shor_pybposd_comm 0.0935 M allocs: 3.29 MB 0.0935 M allocs: 3.29 MB 1
ecc/evaluate_decoder/shor_pybposd_naivesyn 0.182 M allocs: 6.49 MB 0.182 M allocs: 6.49 MB 1
ecc/evaluate_decoder/shor_pybposd_shorsyn 0.182 M allocs: 6.55 MB 0.182 M allocs: 6.55 MB 1
ecc/evaluate_decoder/shor_table_comm 3.98 k allocs: 0.17 MB 3.98 k allocs: 0.17 MB 1
ecc/evaluate_decoder/shor_table_naivesyn 2.8 k allocs: 0.185 MB 2.8 k allocs: 0.185 MB 1
ecc/evaluate_decoder/shor_table_shorsyn 3.28 k allocs: 0.247 MB 3.28 k allocs: 0.247 MB 1
ecc/evaluate_decoder/toric8_bp_comm 1.03 M allocs: 0.169 GB 1.01 M allocs: 0.166 GB 1.02
ecc/evaluate_decoder/toric8_bp_naivesyn 2.04 M allocs: 0.334 GB 2.12 M allocs: 0.346 GB 0.964
ecc/evaluate_decoder/toric8_bp_shorsyn 2.09 M allocs: 0.34 GB 2.11 M allocs: 0.343 GB 0.992
ecc/evaluate_decoder/toric8_pybp_comm 0.103 M allocs: 4.18 MB 0.103 M allocs: 4.18 MB 1
ecc/evaluate_decoder/toric8_pybp_naivesyn 0.218 M allocs: 9.04 MB 0.218 M allocs: 9.04 MB 1
ecc/evaluate_decoder/toric8_pybp_shorsyn 0.233 M allocs: 10.7 MB 0.233 M allocs: 10.7 MB 1
ecc/evaluate_decoder/toric8_pybposd_comm 0.103 M allocs: 4.18 MB 0.103 M allocs: 4.18 MB 1
ecc/evaluate_decoder/toric8_pybposd_naivesyn 0.218 M allocs: 9.04 MB 0.218 M allocs: 9.04 MB 1
ecc/evaluate_decoder/toric8_pybposd_shorsyn 0.233 M allocs: 10.7 MB 0.233 M allocs: 10.7 MB 1
ecc/evaluate_decoder/toric8_pymatch_comm 14 k allocs: 1.05 MB 14 k allocs: 1.05 MB 1
ecc/evaluate_decoder/toric8_pymatch_naivesyn 0.0389 M allocs: 2.71 MB 0.0389 M allocs: 2.71 MB 1
ecc/evaluate_decoder/toric8_pymatch_shorsyn 0.054 M allocs: 4.41 MB 0.054 M allocs: 4.41 MB 1
ecc/evaluate_decoder/toric8_table_comm 13.9 k allocs: 0.835 MB 13.9 k allocs: 0.835 MB 1
ecc/evaluate_decoder/toric8_table_naivesyn 0.0388 M allocs: 2.28 MB 0.0388 M allocs: 2.28 MB 1
ecc/evaluate_decoder/toric8_table_shorsyn 0.0538 M allocs: 3.98 MB 0.0538 M allocs: 3.98 MB 1
pauli/mul/100 0 allocs: 0 B 0 allocs: 0 B
pauli/mul/1000 0 allocs: 0 B 0 allocs: 0 B
pauli/mul/100000 0 allocs: 0 B 0 allocs: 0 B
pauli/mul/20000000 0 allocs: 0 B 0 allocs: 0 B
stabilizer/canon/cano500 0 allocs: 0 B 0 allocs: 0 B
stabilizer/canon/diag_cano500 0 allocs: 0 B 0 allocs: 0 B
stabilizer/canon/diag_gott500 14.5 k allocs: 0.853 MB 14.5 k allocs: 0.853 MB 1
stabilizer/canon/diag_rref500 0 allocs: 0 B 0 allocs: 0 B
stabilizer/canon/gott500 14.5 k allocs: 0.854 MB 14.5 k allocs: 0.854 MB 1
stabilizer/canon/md_cano500 0 allocs: 0 B 0 allocs: 0 B
stabilizer/canon/md_rref500 0 allocs: 0 B 0 allocs: 0 B
stabilizer/canon/rref500 0 allocs: 0 B 0 allocs: 0 B
stabilizer/project/destabilizer 5 allocs: 0.281 kB 5 allocs: 0.281 kB 1
stabilizer/project/stabilizer 2 allocs: 0.0781 kB 2 allocs: 0.0781 kB 1
stabilizer/tensor/diag_pow5_20 0.032 k allocs: 24 MB 0.032 k allocs: 24 MB 1
stabilizer/tensor/pow5_20 29 allocs: 5.48 kB 29 allocs: 5.48 kB 1
stabilizer/trace/destabilizer 2 allocs: 0.0781 kB 2 allocs: 0.0781 kB 1
stabilizer/trace/stabilizer 3 allocs: 0.109 kB 3 allocs: 0.109 kB 1
time_to_load 0.149 k allocs: 11.1 kB 0.149 k allocs: 11.1 kB 1

@Krastanov Krastanov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks overall good, need to do another pass of review to focus on a few things.

If you need to figure out type trees, use TypeTree.jl and do something like: println(join(tt(supertype(some_starting_type))))

Comment thread docs/src/noisify.md Outdated
Idle noise requires the total number of qubits in the circuit to be specified.

```@example noisify
noisy_circuit = noisify(circuit, noise_model; nqubits=2)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nqubits can be deduced by doing something like maximum(affectedqubits.(circuit)).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is helpful, but would this still work when the circuit does not touch all qubits in the provided stabilizer state? In that case, inferring nqubits from the circuit would undercount the register size, so idle noise would not be applied to qubits outside the maximum qubit index appearing in the circuit.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that situation would be awkward to arise. Let's just warn in the docstring of idle noise / circuit noise that this is the case.

Comment thread docs/src/noisify.md
```@example noisify
register = Register(one(Stabilizer, 2), falses(1))

mctrajectories(register, noisy_circuit; trajectories=100)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's have two examples, one that uses pftrajectories as well

Comment thread src/noisify.jl Outdated
reset = noise
)

apply_idle_noise(circuit::AbstractVector, ::NoNoise, nqubits::Integer) = circuit

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename it to insert_...?

Comment thread src/noisify.jl
Comment on lines +94 to +97
noisify(op::AbstractSingleQubitOperator, ::NoNoise) = Any[op]
noisify(op::AbstractTwoQubitOperator, ::NoNoise) = Any[op]
noisify(op::AbstractMeasurement, ::NoNoise) = Any[op]
noisify(op::Reset, ::NoNoise) = Any[op]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't nonoise just have a single method?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to introduce separate dispatch methods for NoNoise because i defined NoNoise as a subtype of AbstractNoise. Since noisify already has methods specialized on AbstractNoise, those methods would be selected over the generic NoNoise handler unless explicit NoNoise dispatches were provided for the affected operation types.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, I guess this is something about method resolution ambiguities. Sounds good

Comment thread src/noisify.jl Outdated

function noisify(op::AbstractSingleQubitOperator, noise_model::CircuitNoise)
out = Any[]
add_noise_op!(out, noise_model.single_qubit, affectedqubits(op))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't this just be noisify(op, noise_model.single_qubit)?

Comment thread src/noisify.jl Outdated

function noisify(op::AbstractTwoQubitOperator, noise_model::CircuitNoise)
out = Any[]
add_noise_op!(out, noise_model.two_qubit, affectedqubits(op))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question here

Comment thread src/noisify.jl Outdated

function noisify(op::AbstractMeasurement, noise_model::CircuitNoise)
out = Any[]
add_noise_op!(out, noise_model.measurement, affectedqubits(op))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Comment thread src/noisify.jl Outdated
function noisify(op::Reset, noise_model::CircuitNoise)
out = Any[]
push!(out, op)
add_noise_op!(out, noise_model.reset, affectedqubits(op))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, and also just curious about the change of order

Comment thread docs/make.jl
"Perturbative Expansions" => "noisycircuits_perturb.md",
"ECC example" => "ecc_example_sim.md",
"Circuit Operations" => "noisycircuits_ops.md",
"Noisify" => "noisify.md"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this probably needs a better name -- seeing this in the tables of content would not mean much to a reader

Comment thread src/noisify.jl
layers = Dict{Int, Vector{Any}}()
active_qubits = Dict{Int, Set{Int}}()
for op in circuit
if op isa AbstractNoiseOp || op isa VerifyOp || op isa ClassicalXOR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is AbstractNoiseOp here?

More importantly, this seems to be not extendable. If a user or an external library creates a type that requires this same type of special treatment, they will not be able to use this noisify method.

Could you explain, maybe in a few drawings, why is this special cased in this fashion?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I special cased AbstractNoiseOp because I didn't want it to undergo idle noise (I'm still not sure if it is supposed to, do let me know). But I realize now that it can still follow the layering logic like a regular operation using affectedqubits, I'll make that change.

The part I'm still unsure about is how to handle ops such as ClassicalXOR or VerifyOp. The idle-noise insertion pass first schedules the circuit into timesteps/layers using the filled_up_to vector, so the resulting circuit is effectively reordered according to layer structure rather than preserving the original linear ordering. For ordinary operations this is fine. However, it’s not clear to me how VerifyOp and ClassicalXOR should interact with this scheduling pass. However, for VerifyOp and ClassicalXOR, I’m not sure what the correct scheduling semantics should be, or even how their timestep should be determined. Should they participate in the layer assignment process somehow, or should they instead preserve their position in the original circuit?

@brohan-byte brohan-byte Jun 13, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dilemma is as follows:

Suppose our circuit was: [H(1), ClassicalXOR(1), H(2)]

The current layering logic would have re order this according to the time steps as:
t=1: H(1), H(2)
t=2:ClassicalXOR(...)

In other words, it doesn't take place in any of the layering logic and is just placed in the current maximum timestep.

Another possible model could be that the circuit should be re-ordered as:
t=1: H(1)
t=2: ClassicalXOR(...)
t=3:H(2)

and similarly for VerifyOp.
The issue is that, unlike ordinary quantum operations, how should I assign a timestep to ClassicalXOR and VerifyOp and other possible ops in the future. As a result, I’m unsure whether they should participate in the layering process at all, or whether they should instead preserve their position in the original circuit.

Let me know if I was able to get my thoughts across clearly!

Comment thread src/noisify.jl
Comment on lines +35 to +36
layers = Dict{Int, Vector{Any}}()
active_qubits = Dict{Int, Set{Int}}()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the meaning of these two? Why are they needed in addition to filled_up_to?

@brohan-byte brohan-byte Jun 13, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the insert_idle_noise function, I use these layers and active qubits in line 64 in order to compute idle qubits for each time step layer ( this function makes the assumption that each gate only takes up one step in time, let me know if this is inaccurate). During the initial pass through the circuit, filled_up_to tracks the next available layer for each qubit, allowing operations to be assigned to their respective layers. layers records which operations occur in each layer so that the circuit can be reconstructed after idle-noise insertion. active_qubits records which qubits participate in operations within each layer. While active_qubits is not strictly necessary (it could be recomputed from layers), storing it avoids additional work during the reconstruction pass.

@Krastanov Krastanov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good, some very minor things to change, and fixing the doc build

I want to see the docs before merging.

Comment thread docs/src/noisify.md
Reset(reset_state, [2]),
VerifyOp(one(Stabilizer, 1), [2]),
]
noisy = noisify(circuit, PauliNoise(1e-3, 1e-3, 1e-3))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when the doc ci is fixed, I want to look at how this renders in the docs

Comment thread docs/src/noisify.md

When the `idle_noise` option is configured in `CircuitNoise`, noise is inserted on qubits that are not participating in any operation during a given circuit layer.

Idle noise requires the total number of qubits in the circuit to be specified.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not true anymore?

Comment thread docs/src/noisify.md
The same noisy circuit can also be simulated using pauli-frame trajectories.

```@example noisify
register = Register(one(Stabilizer, 2), falses(2))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pftrajectories does not take a register -- you can probably just directly give it the circuit

Comment thread src/noisify.jl
Comment on lines +84 to +86
noisify(op::AbstractNoiseOp, noise::AbstractNoise) = Any[op]
noisify(op::ClassicalXOR, noise::AbstractNoise) = Any[op]
noisify(op::VerifyOp, noise::AbstractNoise) = Any[op]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these do not seem to be used, unless there is some method ambiguity -- if there is method ambiguity, leave a comment mentioning ambiguity with which other method

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a simpler solution to fixing all the ambiguity is to NOT use NoNoise <: AbstractNoise, but rather to just use Nothing

Comment thread src/noisify.jl


function noisify(circuit::AbstractVector, noise_model::CircuitNoise)
if noise_model.idle_noise isa NoNoise

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generally never use isa in Julia -- it makes things unextendable. It is there for the rare need for introspection.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks to line 29, you can just skip this

Comment thread src/noisify.jl

insert_idle_noise(circuit::AbstractVector, ::NoNoise) = circuit

function insert_idle_noise(circuit::AbstractVector, idle_noise::AbstractNoise)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding CNOT 2 3 to a circuit for which filled_up_to looks like this:
* * * * *
*
* * *

would result into a:

filled_up_to is modified to
* * * * *
* /  /  * (where / denotes "skip")
* * * *
idling noise will be added to each skip with push!(noisy_circuit, idlenoiseop)
push!(noisy_circuit, sCNOT(2,3))

And then for some specific operations, it just directly goes to push! without looking at filled_up_to. Instead of isa though, lets use a private method skip_idling_noise(_) = false and skip_idling_noise(::ParticularType) = true. Keep the method private but documented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a dispatch-based API for turning noiseless circuits into noisy circuits

2 participants