diff --git a/.gitignore b/.gitignore index aec029e..d5292d8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,22 @@ __pycache__ results target notes +data/*.old +data/transpiled/python-version/ +data/transpiled_truncated/ +wisq +related +tableau +data/qasmbench/ +data/native_benchpress/ +data/pm_paper_circuits/ +data/lssp-data-silva-et-al/ +data/custom_qft_benchmarks/ +data/all_compiled_truncated/ +data/all_wisq/ +data/benchpress/ +data/clifford_rz/ +data/transpiled/max-weight-vary/ +data_processing/*out +data_processing/out_dir +data/*out \ No newline at end of file diff --git a/data/all_compiled/qft_N008.cliffordt.qasm b/data/all_compiled/qft_N008.cliffordt.qasm new file mode 100644 index 0000000..bb7ff7c --- /dev/null +++ b/data/all_compiled/qft_N008.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0aabbd7574697c86af5fe4a0becf314ccc65e226477b1c8bea89dbd7c04de157 +size 289605 diff --git a/data/all_compiled/qft_N008.qasm b/data/all_compiled/qft_N008.qasm new file mode 100644 index 0000000..b5d0408 --- /dev/null +++ b/data/all_compiled/qft_N008.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95a7e8adacf8a82fd9504d8a95bb36cb66c850a603a067ab4681ddba24fabcf1 +size 2564 diff --git a/data/all_compiled/qft_N024.cliffordt.qasm b/data/all_compiled/qft_N024.cliffordt.qasm new file mode 100644 index 0000000..c560345 --- /dev/null +++ b/data/all_compiled/qft_N024.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0fc16c27b3615a66d7a0f1aefa72152b1a94eacf291469145c3767d61f79146 +size 2669736 diff --git a/data/all_compiled/qft_N024.qasm b/data/all_compiled/qft_N024.qasm new file mode 100644 index 0000000..90e0dc9 --- /dev/null +++ b/data/all_compiled/qft_N024.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12cadd769e9f7659971ae6c3cdb59a0b83b16ba4a14f107b7ade3db1b822c3f3 +size 24694 diff --git a/data/all_compiled/qft_N032.cliffordt.qasm b/data/all_compiled/qft_N032.cliffordt.qasm new file mode 100644 index 0000000..96950b0 --- /dev/null +++ b/data/all_compiled/qft_N032.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e175d1ab48ccf541f010f4359766437dd4ca6ca5dcd430340fbe6e92ed1da4e2 +size 3984573 diff --git a/data/all_compiled/qft_N032.qasm b/data/all_compiled/qft_N032.qasm new file mode 100644 index 0000000..26a98f6 --- /dev/null +++ b/data/all_compiled/qft_N032.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d16ee71c325dbac6070d3f9783c92e68ef8e1bec1e8c8fec277dea45b5f1fe0 +size 45359 diff --git a/data/all_compiled/qft_N040.cliffordt.qasm b/data/all_compiled/qft_N040.cliffordt.qasm new file mode 100644 index 0000000..e805f82 --- /dev/null +++ b/data/all_compiled/qft_N040.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:124de72dca2198140fd52dd27daf1656d9ebd93464ebf268882147c51457783e +size 5321394 diff --git a/data/all_compiled/qft_N040.qasm b/data/all_compiled/qft_N040.qasm new file mode 100644 index 0000000..c052dcf --- /dev/null +++ b/data/all_compiled/qft_N040.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e57d6eafc11d4d7b2d59f2a7cb6d872d8d7266ce25d9ceeedbea78079a30aa72 +size 73020 diff --git a/data/all_compiled/qft_N048.cliffordt.qasm b/data/all_compiled/qft_N048.cliffordt.qasm new file mode 100644 index 0000000..a3154de --- /dev/null +++ b/data/all_compiled/qft_N048.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf3f6587c4d891911fe3292a8a2f73cfc5e9f9b687bcb43ab78aec1d2a3886dc +size 6714507 diff --git a/data/all_compiled/qft_N048.qasm b/data/all_compiled/qft_N048.qasm new file mode 100644 index 0000000..d0f2347 --- /dev/null +++ b/data/all_compiled/qft_N048.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2960243961ae72383e6e4e8366e80c61821e16be7a47b31e47b9287460c8bba +size 106800 diff --git a/data/all_compiled/qft_N056.cliffordt.qasm b/data/all_compiled/qft_N056.cliffordt.qasm new file mode 100644 index 0000000..d454d76 --- /dev/null +++ b/data/all_compiled/qft_N056.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a6358a013b60987b55de3c9ce8cf4c30c890bfaffd17905b3cf803815737747 +size 7996316 diff --git a/data/all_compiled/qft_N056.qasm b/data/all_compiled/qft_N056.qasm new file mode 100644 index 0000000..11dab26 --- /dev/null +++ b/data/all_compiled/qft_N056.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a325c84160226f678e68d2fa00f6fabfdaf64e1e37cddbb1afc84c685b75361e +size 145124 diff --git a/data/all_compiled/qft_N064.cliffordt.qasm b/data/all_compiled/qft_N064.cliffordt.qasm new file mode 100644 index 0000000..c9441b6 --- /dev/null +++ b/data/all_compiled/qft_N064.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fa0f385c257fb659ffcd5520ec4c142c0e36551608093505f4f4c960cf24e58 +size 9366271 diff --git a/data/all_compiled/qft_N064.qasm b/data/all_compiled/qft_N064.qasm new file mode 100644 index 0000000..1fcf396 --- /dev/null +++ b/data/all_compiled/qft_N064.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c3a7dc93ab7cb89d800025fe087c2379d8b8334fa96ba796566505c255d38cf +size 187992 diff --git a/data/all_compiled/qft_N072.cliffordt.qasm b/data/all_compiled/qft_N072.cliffordt.qasm new file mode 100644 index 0000000..63d6969 --- /dev/null +++ b/data/all_compiled/qft_N072.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce9186c2cd34c87e10c3934485a9609fffb6ea4e16fff93c9ef1ba06b67db5ab +size 10677787 diff --git a/data/all_compiled/qft_N072.qasm b/data/all_compiled/qft_N072.qasm new file mode 100644 index 0000000..fe181f6 --- /dev/null +++ b/data/all_compiled/qft_N072.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81c75c276278da2e814d59654e9d963d3696c12929fc6d3aa238f471a87cf035 +size 235404 diff --git a/data/all_compiled/qft_N080.cliffordt.qasm b/data/all_compiled/qft_N080.cliffordt.qasm new file mode 100644 index 0000000..d193b07 --- /dev/null +++ b/data/all_compiled/qft_N080.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d485c5be1a5839ec1cd81b81691858ab7d1bbd169f228827853038360a579267 +size 11989783 diff --git a/data/all_compiled/qft_N080.qasm b/data/all_compiled/qft_N080.qasm new file mode 100644 index 0000000..e7ec268 --- /dev/null +++ b/data/all_compiled/qft_N080.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da0e2ea808f65d6643066c6abbb395c11824a6464c29f37056edcfc1db75b993 +size 287360 diff --git a/data/all_compiled/qft_N088.cliffordt.qasm b/data/all_compiled/qft_N088.cliffordt.qasm new file mode 100644 index 0000000..ab33bfd --- /dev/null +++ b/data/all_compiled/qft_N088.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3def7da1f2db90f1352363cbb62275d1a3cfe4430ccbc3d094e14b169ada81d4 +size 13362315 diff --git a/data/all_compiled/qft_N088.qasm b/data/all_compiled/qft_N088.qasm new file mode 100644 index 0000000..0438a4a --- /dev/null +++ b/data/all_compiled/qft_N088.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:139576376b16ef87a229f501bcba0d90e66715414139ac5a89cadbde0c07dafe +size 343860 diff --git a/data/all_compiled/qft_N096.cliffordt.qasm b/data/all_compiled/qft_N096.cliffordt.qasm new file mode 100644 index 0000000..5abf696 --- /dev/null +++ b/data/all_compiled/qft_N096.cliffordt.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbc2176d478e863aa4d52ee82289b9b501f0919c8b87fae41d21a83ad48daea5 +size 14739112 diff --git a/data/all_compiled/qft_N096.qasm b/data/all_compiled/qft_N096.qasm new file mode 100644 index 0000000..55dcafa --- /dev/null +++ b/data/all_compiled/qft_N096.qasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7df0634cfab702eb0299742c3acac959dee983fa598e5dc99dcb18318008fdf7 +size 404904 diff --git a/data/transpiled/max-weight-0/qft_N008.trans b/data/transpiled/max-weight-0/qft_N008.trans new file mode 100644 index 0000000..a4cb784 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N008.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dba8ecdee03eb33b43a02c4d9e56a4f69c290097d9e7c00e4d6d1acd3f99c6c +size 183339 diff --git a/data/transpiled/max-weight-0/qft_N024.trans b/data/transpiled/max-weight-0/qft_N024.trans new file mode 100644 index 0000000..45d0468 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N024.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b953b930851ca9ec893a780caead2b52d8af58e81dfa0ae958e914227c7cc0de +size 3529851 diff --git a/data/transpiled/max-weight-0/qft_N032.trans b/data/transpiled/max-weight-0/qft_N032.trans new file mode 100644 index 0000000..a1011f3 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N032.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b5a6ba790106bc13388c6eb1200611cb5731a8cb380378437c6d9455b992595 +size 6601318 diff --git a/data/transpiled/max-weight-0/qft_N040.trans b/data/transpiled/max-weight-0/qft_N040.trans new file mode 100644 index 0000000..82fc162 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N040.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2612dcd4f86d24761a18d193d0644a48a22373140e5d1d437a17b7383d6fa31b +size 10637415 diff --git a/data/transpiled/max-weight-0/qft_N048.trans b/data/transpiled/max-weight-0/qft_N048.trans new file mode 100644 index 0000000..7a242fc --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N048.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d17a995a2ba9f51a06c8af4d6abaa68db9ae668cffbd4cf62ffb53f186322413 +size 15718157 diff --git a/data/transpiled/max-weight-0/qft_N056.trans b/data/transpiled/max-weight-0/qft_N056.trans new file mode 100644 index 0000000..d71ef67 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N056.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9713c16c49a9f0744c0fee8eb611416a48f15b46e4c80bf47fb8a203d9bbddd +size 21436925 diff --git a/data/transpiled/max-weight-0/qft_N064.trans b/data/transpiled/max-weight-0/qft_N064.trans new file mode 100644 index 0000000..4c88c7d --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N064.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce03d15b3dde2a8b9c47aba9117518e1f1e3d4e8146d36a1d488b1c5a22e2e5f +size 28321326 diff --git a/data/transpiled/max-weight-0/qft_N072.trans b/data/transpiled/max-weight-0/qft_N072.trans new file mode 100644 index 0000000..4f7f725 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N072.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdc41173bec438bba1457d77c2bf9d3fb0f21cf98e9a62f2d2bbac7878e732ef +size 35928508 diff --git a/data/transpiled/max-weight-0/qft_N080.trans b/data/transpiled/max-weight-0/qft_N080.trans new file mode 100644 index 0000000..7ff5133 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N080.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ef669cb40e5a82c36b9b11989c173e2599042298647d2f7a09364e8cbdb9d04 +size 44431795 diff --git a/data/transpiled/max-weight-0/qft_N088.trans b/data/transpiled/max-weight-0/qft_N088.trans new file mode 100644 index 0000000..bd8b0c7 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N088.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4cb20d949af31418e7953628640b7a42c31846f25589f5c739ba4b16d2688ae +size 54086382 diff --git a/data/transpiled/max-weight-0/qft_N096.trans b/data/transpiled/max-weight-0/qft_N096.trans new file mode 100644 index 0000000..6dd8e64 --- /dev/null +++ b/data/transpiled/max-weight-0/qft_N096.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61f87dd21a24abdd3404fffd84482ce8b635234d78387509e4240ee75cd1c4f8 +size 64688783 diff --git a/data/transpiled/max-weight-1/qft_N008.trans b/data/transpiled/max-weight-1/qft_N008.trans new file mode 100644 index 0000000..6d66742 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N008.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7450bc69c00a9f5760660ef36da07585c10f2757b8dede0a3911c4ab96747610 +size 189300 diff --git a/data/transpiled/max-weight-1/qft_N024.trans b/data/transpiled/max-weight-1/qft_N024.trans new file mode 100644 index 0000000..dbebb04 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N024.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad118ed8ed1fef8eb39b7c0dab3c0b838e1a4332ce2a7e7fe7017b413cc52e0e +size 3702451 diff --git a/data/transpiled/max-weight-1/qft_N032.trans b/data/transpiled/max-weight-1/qft_N032.trans new file mode 100644 index 0000000..79d4bb4 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N032.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05f438ec2621ec995c1d727d82bbfad95c126d04c74e50ea843ab25a23ce1733 +size 6937937 diff --git a/data/transpiled/max-weight-1/qft_N040.trans b/data/transpiled/max-weight-1/qft_N040.trans new file mode 100644 index 0000000..a5efd6c --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N040.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39a728df129220825a004ef98cdebe08c839697d35446707b35ec567d5486599 +size 11189536 diff --git a/data/transpiled/max-weight-1/qft_N048.trans b/data/transpiled/max-weight-1/qft_N048.trans new file mode 100644 index 0000000..842afab --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N048.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c47afbd912e46a2157b135f16294c0359b74c376718f41a2977ae226acc9e2be +size 16580130 diff --git a/data/transpiled/max-weight-1/qft_N056.trans b/data/transpiled/max-weight-1/qft_N056.trans new file mode 100644 index 0000000..c64fd0c --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N056.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36ea45bcb4bbc8bc14c0ba10c526362881a7981d6998af9bc18e143bc1285af5 +size 22622149 diff --git a/data/transpiled/max-weight-1/qft_N064.trans b/data/transpiled/max-weight-1/qft_N064.trans new file mode 100644 index 0000000..dcb2416 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N064.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2a87053110609284e42d4f649f440385c0c576c82831d28f3d4b1c48653dd92 +size 29920391 diff --git a/data/transpiled/max-weight-1/qft_N072.trans b/data/transpiled/max-weight-1/qft_N072.trans new file mode 100644 index 0000000..e050edd --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N072.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34c6fe7fdd9ade3c1fcbda064c793203392c2b6f149f422a73ff17380788fc55 +size 38014731 diff --git a/data/transpiled/max-weight-1/qft_N080.trans b/data/transpiled/max-weight-1/qft_N080.trans new file mode 100644 index 0000000..13dcf38 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N080.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b696554dda8ade431d36b0f921cee0ba4a2c62b1454cf39d20e7d03aba36a3f8 +size 47101096 diff --git a/data/transpiled/max-weight-1/qft_N088.trans b/data/transpiled/max-weight-1/qft_N088.trans new file mode 100644 index 0000000..696e3f3 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N088.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc267c16c22ec32fb6e714ee26e0254f8d0672ff938790d50688b6fbf77d0251 +size 57371652 diff --git a/data/transpiled/max-weight-1/qft_N096.trans b/data/transpiled/max-weight-1/qft_N096.trans new file mode 100644 index 0000000..1e17aa2 --- /dev/null +++ b/data/transpiled/max-weight-1/qft_N096.trans @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45bdd8cba3859f1c20835cc2e2d4a8efab61897b7174e167697fd7f671adf143 +size 68669290 diff --git a/data_processing/plot_puremagic.py b/data_processing/plot_puremagic.py index cdf253a..e57e4fe 100755 --- a/data_processing/plot_puremagic.py +++ b/data_processing/plot_puremagic.py @@ -89,6 +89,7 @@ class Series: is_ratio: bool = False ratio_label: Optional[str] = None point_labels: Optional[list] = None + y_key: Optional[str] = None # --------------------------------------------------------------------------- @@ -216,7 +217,7 @@ def _flush(): cur["avg_cultivation_time"] = float(m.group(1)) in_cultivation_block = False - if m := re.match(r"Min volume estimate:\s+(\d+)", s): + if m := re.match(r"Volume estimate:\s+(\d+)", s): cur["min_volume"] = int(m.group(1)) if m := re.match(r"Products per layer:\s+([0-9.eE+\-]+)\s+avg", s): @@ -348,6 +349,8 @@ def prettify_circuit_name(name): # separate trailing _n or _N qubit count: foo_n8 -> foo(8) name = re.sub(r"[_-][nN](\d+)$", lambda mo: f"({mo.group(1)})", name) + name = re.sub(r"(?i)hubbard_18", "hubbard(18)", name) + return name @@ -473,18 +476,18 @@ def parse_flasq_file(filepath): FLASQ Lower Bound for Layout: ... ======================================================================== - Max simultaneous qubit usage (Q) : + Circuit qubits (n_qubits) : ... FLASQ spacetime volume (S, blocks) : ======================================================================== The circuit name is taken from the "FLASQ Lower Bound for" header line, - Q from the "Max simultaneous qubit usage" row, and the two volumes from - the "FLASQ spacetime volume" row. + n_data_qubits from the "Circuit qubits (n_qubits)" row, and the two volumes + from the "FLASQ spacetime volume" row. """ entries = [] current_circuit = None - current_q = None + current_n_qubits = None with open(filepath) as f: for line in f: s = line.strip() @@ -492,12 +495,12 @@ def parse_flasq_file(filepath): m = re.match(r"^FLASQ Lower Bound for\s+(.+)$", s) if m: current_circuit = m.group(1).strip() - current_q = None + current_n_qubits = None continue - # Q row: "Max simultaneous qubit usage (Q) : " - m = re.match(r"Max simultaneous qubit usage \(Q\)\s*:\s*(\d+)", s) + # n_qubits row: "Circuit qubits (n_qubits) : " + m = re.match(r"Circuit qubits \(n_qubits\)\s*:\s*(\d+)", s) if m and current_circuit is not None: - current_q = int(m.group(1)) + current_n_qubits = int(m.group(1)) continue # Volume row: "FLASQ spacetime volume (S, blocks) : " m = re.match( @@ -508,11 +511,11 @@ def parse_flasq_file(filepath): try: vol_cons = float(m.group(1)) vol_opt = float(m.group(2)) - entries.append((current_circuit, vol_cons, vol_opt, current_q)) + entries.append((current_circuit, vol_cons, vol_opt, current_n_qubits)) except ValueError: pass current_circuit = None - current_q = None + current_n_qubits = None return entries @@ -667,7 +670,7 @@ def main(): action="store_true", default=False, help=( - "Read the 'Min volume estimate' line from each data file and plot it as " + "Read the 'Volume estimate' line from each data file and plot it as " "an additional dashed series alongside each regular volume series. " "Only meaningful when -y is 'volume'." ), @@ -901,6 +904,7 @@ def load_series(y_keys_for_axis, label_suffix=None): circuits=merged["circuit"].tolist(), is_ratio=True, ratio_label=ratio_label, + y_key=y_key, ) ) else: @@ -931,6 +935,7 @@ def load_series(y_keys_for_axis, label_suffix=None): ys=ys_vals, circuits=d1["circuit"].fillna("").tolist(), point_labels=pt_labels, + y_key=y_key, ) ) @@ -952,7 +957,9 @@ def load_series(y_keys_for_axis, label_suffix=None): def draw_series(ax, series_list, yk_list, colour_offset=0): y_key = yk_list[0] if isinstance(yk_list, list) else yk_list draw_lines = args.lines or args.lines_with_markers - show_markers = args.lines_with_markers or is_cultivation_x or is_weight_x + show_markers = args.lines_with_markers or ( + (is_cultivation_x or is_weight_x) and not args.lines + ) is_timing_y = y_key == "timing" is_total_qubits_y = y_key == "total_qubits" is_ancilla_qubits_y = "ancilla_qubits" in ( @@ -1075,8 +1082,10 @@ def draw_series(ax, series_list, yk_list, colour_offset=0): ratio_series = [s for s in series_list if s.is_ratio] non_ratio_series = [s for s in series_list if not s.is_ratio] if ratio_series: - # Use the first ratio series (there is typically only one per axis) - s = ratio_series[0] + # In slash mode multiple ratio series share the axis; pick the one for + # ancilla_qubits, falling back to the first if none is tagged. + ancilla_rs = [s for s in ratio_series if s.y_key == "ancilla_qubits"] + s = ancilla_rs[0] if ancilla_rs else ratio_series[0] rx = np.array(s.xs, float) ry = np.array(s.ys, float) elif len(non_ratio_series) >= 2: @@ -1101,7 +1110,7 @@ def draw_series(ax, series_list, yk_list, colour_offset=0): # Least-squares fit for y = A / sqrt(x): # minimise sum((y - A/sqrt(x))^2) => A = sum(y/sqrt(x)) / sum(1/x) A = np.sum(sy / np.sqrt(sx)) / np.sum(1.0 / sx) - # A *= 0.48 + # A *= 0.85 y_pred = A / np.sqrt(sx) ss_res = np.sum((sy - y_pred) ** 2) ss_tot = np.sum((sy - sy.mean()) ** 2) @@ -1276,7 +1285,7 @@ def draw_series(ax, series_list, yk_list, colour_offset=0): if d1.empty: minvol_series.append(None) continue - mv_label = f"{file_label} Min volume" if file_label else "Min volume" + mv_label = f"{file_label} Volume" if file_label else "Volume" mv_ys = d1["min_volume"].tolist() if args.max_efficiency: mv_ys = [1.0 / v if v else float("nan") for v in mv_ys] diff --git a/src/circuit.rs b/src/circuit.rs index 88cba1c..a6bde3e 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -1,5 +1,6 @@ use crate::fn_timer; -use crate::pauliproduct::{GateType, PauliProduct}; +use crate::pauliproduct::{GateType, Operator, PauliProduct}; +use std::collections::HashMap; use plotters::coord::types::{RangedCoordf64, RangedCoordusize}; use plotters::prelude::*; use rand::Rng; @@ -386,10 +387,32 @@ impl Circuit { /// (mirroring [`Circuit::gen_deps`]); the original `self.pps` and `self.layers` cache /// are never modified. pub(crate) fn estimate_num_layers(&self, rng: &mut StdRng, no_t_failures: bool) -> usize { + // Per-qubit S^k correction power (0=none, 1=S, 2=Z, 3=S†). + // Incremented on each T gate failure; cleared when a Clifford is encountered. + let mut correction_power: HashMap = HashMap::new(); + // Build the expanded product list. let mut expanded: Vec = Vec::with_capacity(self.pps.len() * 4); for pp in &self.pps { if pp.gate_type.is_cx() { + // Before a CX, emit S corrections for any tracked qubits. + for op in &pp.operators { + let power = *correction_power.get(&op.qubit).unwrap_or(&0); + if power == 1 || power == 3 { + for _ in 0..3 { + let corr = PauliProduct { + id: expanded.len() as i32, + gate_type: GateType::S, + operators: vec![Operator { qubit: op.qubit, basis: op.basis }], + parents: vec![], + children: vec![], + max_qubit: op.qubit, + }; + expanded.push(corr); + } + } + correction_power.insert(op.qubit, 0); + } // CX → 2 sequential copies for _ in 0..2 { let mut copy = pp.clone(); @@ -399,6 +422,24 @@ impl Circuit { expanded.push(copy); } } else if pp.gate_type.is_s() || pp.gate_type.is_sx() { + // Before an S/SX, emit S correction for the tracked qubit if needed. + if let Some(op) = pp.operators.first() { + let power = *correction_power.get(&op.qubit).unwrap_or(&0); + if power == 1 || power == 3 { + for _ in 0..3 { + let corr = PauliProduct { + id: expanded.len() as i32, + gate_type: GateType::S, + operators: vec![Operator { qubit: op.qubit, basis: op.basis }], + parents: vec![], + children: vec![], + max_qubit: op.qubit, + }; + expanded.push(corr); + } + } + correction_power.insert(op.qubit, 0); + } // S / SX → 3 sequential copies for _ in 0..3 { let mut copy = pp.clone(); @@ -408,23 +449,28 @@ impl Circuit { expanded.push(copy); } } else if pp.gate_type.is_t() { - // T → 1 copy; if T failures are enabled, 50% chance of an S correction + // Apply S^k conjugation just-in-time: swap X↔Y for qubits with odd power. let mut t_copy = pp.clone(); t_copy.id = expanded.len() as i32; t_copy.parents.clear(); t_copy.children.clear(); - expanded.push(t_copy); + for op in &mut t_copy.operators { + let power = *correction_power.get(&op.qubit).unwrap_or(&0); + if power % 2 == 1 { + op.basis = + if op.basis == 'X' { 'Y' } else if op.basis == 'Y' { 'X' } else { op.basis }; + } + } + expanded.push(t_copy.clone()); if !no_t_failures && rng.gen_bool(0.5) { - // S correction uses the same qubits as the T gate - let mut s_copy = pp.clone(); - s_copy.id = expanded.len() as i32; - s_copy.gate_type = GateType::S; - s_copy.parents.clear(); - s_copy.children.clear(); - expanded.push(s_copy); + // T gate failed: increment correction power for each operator qubit. + for op in &t_copy.operators { + let entry = correction_power.entry(op.qubit).or_insert(0); + *entry = (*entry + 1) % 4; + } } } else { - // M and anything else → 1 copy + // M and anything else → 1 copy (no correction emission for measurements). let mut copy = pp.clone(); copy.id = expanded.len() as i32; copy.parents.clear(); @@ -1019,8 +1065,12 @@ mod tests { #[test] fn n_cycles_t_gate_s_insertion_varies_across_seeds() { - // With enough T gates, different seeds should produce different layer counts. - let lines: Vec<&str> = vec!["+X_"; 20]; + // With the lazy correction model, S corrections are emitted only when a Clifford + // gate is encountered. Use a chain of T gates followed by an S gate so that the + // accumulated correction is flushed before the S, producing different layer counts + // depending on how many T gates failed (odd failures → extra 3-layer correction). + let mut lines: Vec<&str> = vec!["+X_"; 19]; + lines.push("+X_"); let f = make_circuit_file(&lines); let mut c = Circuit::new(&f.path().to_string_lossy().to_string()); c.load_circuit().unwrap(); diff --git a/src/puremagic.rs b/src/puremagic.rs index 9675c6d..94dcc77 100644 --- a/src/puremagic.rs +++ b/src/puremagic.rs @@ -67,6 +67,9 @@ struct Args { /// Disable T gate failures (every T gate succeeds on first attempt) #[arg(short = 'F', long)] no_t_failures: bool, + /// Record normalized cultivation-time distribution to .cultivation_dist + #[arg(short = 'C', long)] + record_cultivation_dist: bool, /// Number of ancilla between each data patch (all magic routing only) #[arg(short, long, default_value = "1")] ancilla_rows: usize, @@ -108,6 +111,7 @@ fn main() -> Result<(), Box> { let mut circuit = Circuit::new(&circuit_fname); circuit.load_circuit()?; let n_products = circuit.n_products(); + let n_sx_cliffords = circuit.pps.iter().filter(|pp| pp.gate_type.is_s() || pp.gate_type.is_sx()).count(); let _n_layers = circuit.print_statistics(); #[cfg(debug_assertions)] circuit.print()?; @@ -146,12 +150,22 @@ fn main() -> Result<(), Box> { args.plot.join(" "), args.rseed, args.no_t_failures, + args.record_cultivation_dist, ); let (tot_lcycles, n_scheduled) = sched.sched_circuit()?; assert!(n_scheduled >= n_products); let volume = n_qubits * tot_lcycles; println!("Scheduled {} in {} logical cycles, volume {}", n_scheduled, tot_lcycles, volume); + if !args.no_t_failures && n_sx_cliffords > 0 { + let corrections = sched.correction_gates_emitted; + println!( + "S correction gates: {} / {} S/SX Cliffords ({:.1}%)", + corrections, + n_sx_cliffords, + corrections as f64 * 100.0 / n_sx_cliffords as f64 + ); + } println!("Parallelism: {:.3}x", n_scheduled as f64 / tot_lcycles as f64); //let min_layers = if args.no_t_failures { n_layers } else { (n_layers * 3) / 2 }; diff --git a/src/scheduler.rs b/src/scheduler.rs index 7967f47..7acd3dd 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -6,7 +6,7 @@ use crate::debug_sched; use crate::fn_timer; use crate::info_sched; use crate::node::NodeType; -use crate::pauliproduct::{Operator, PauliProduct}; +use crate::pauliproduct::{GateType, Operator, PauliProduct}; use crate::steinertree::SteinerTree; use crate::topograph::TopoGraph; use crate::treegraph::TreeGraph; @@ -17,7 +17,7 @@ use indexmap::{IndexMap, IndexSet}; use rand::Rng; use rand::SeedableRng; use rand::rngs::StdRng; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io::{self, BufWriter, Write}; use std::path::Path; @@ -183,12 +183,12 @@ pub(crate) struct Scheduler { /// (remaining_lcycles, product, node_ids, opt_tree). /// CX occupies 2 lcycles; S/SX occupy 3. clifford_paths: IndexMap, Option>)>, - /// T gates that failed the 50% coin flip; held for one recovery lcycle. - failed_t_paths: IndexMap, Option>)>, pub(crate) t_gate_failures: usize, + pub(crate) correction_gates_emitted: usize, pub(crate) stree_computation: SteinerTree, pub(crate) astar: AStar, no_t_failures: bool, + record_cultivation_dist: bool, /// Reusable buffers to avoid per-lcycle allocations. terminals_buf: Vec, scheduled_ids_buf: Vec, @@ -207,15 +207,30 @@ pub(crate) struct Scheduler { remaining_parents: Vec, pub(crate) pp_paths: Vec<(i32, Option>)>, current_lcycle: usize, - /// T gate IDs that completed a recovery lcycle this cycle (populated by complete_lcycle). - recovery_t_ids: Vec, + /// Per-qubit accumulated S^k correction power (0=none, 1=S, 2=Z, 3=S†). + /// Incremented on T gate failure; cleared when a Clifford is encountered or power cycles to 0. + correction_power: Vec, + /// Pauli basis of the most-recent conjugated T-gate operator per qubit. + /// Used as the operator basis when emitting an S correction before a Clifford. + correction_basis: Vec, + /// Next ID to assign when creating a dynamic S correction product. + next_correction_id: i32, + /// Maps product_id → (qubit, new_basis) pairs for T gates whose operators were + /// conjugated this lcycle. Populated in sched_remaining; consumed in process_t_gate_outcomes. + t_conjugated_bases: HashMap>, + /// Set of T gate product IDs whose precomputed_terminals/root_info currently reflect a + /// conjugated (non-original) operator basis. Needed to trigger a restore call to + /// apply_t_conjugation when correction_power later drops back to zero. + conjugated_t_products: HashSet, } impl Scheduler { pub(crate) fn new( circuit: Circuit, topo: TopoGraph, magic_state_lambda: f64, log_level: &str, - plot_option: String, rseed: u32, no_t_failures: bool, + plot_option: String, rseed: u32, no_t_failures: bool, record_cultivation_dist: bool, ) -> Self { + let n_qubits = circuit.n_qubits; + let n_initial_products = circuit.n_products() as i32; if log_level != "none" { let trace_fname = format!("{}.sched_trace", circuit_stem(&circuit.circuit_fname)); let level_filter = match log_level.to_lowercase().as_str() { @@ -244,11 +259,12 @@ impl Scheduler { scheduled_products: IndexSet::new(), used: vec![false; n_nodes], clifford_paths: IndexMap::new(), - failed_t_paths: IndexMap::new(), t_gate_failures: 0, + correction_gates_emitted: 0, stree_computation: SteinerTree::new(n_nodes), astar: AStar::new(n_nodes), no_t_failures, + record_cultivation_dist, terminals_buf: Vec::new(), scheduled_ids_buf: Vec::new(), children_buf: Vec::new(), @@ -263,7 +279,11 @@ impl Scheduler { remaining_parents: Vec::new(), pp_paths: Vec::new(), current_lcycle: 0, - recovery_t_ids: Vec::new(), + correction_power: vec![0u8; n_qubits], + correction_basis: vec!['X'; n_qubits], + next_correction_id: n_initial_products, + t_conjugated_bases: HashMap::new(), + conjugated_t_products: HashSet::new(), } } @@ -306,7 +326,6 @@ impl Scheduler { self.pp_paths = Vec::new(); while !self.pps_pending.is_empty() || !self.clifford_paths.is_empty() - || !self.failed_t_paths.is_empty() { self.timers.start(self.loop_timer); self.current_lcycle += 1; @@ -328,22 +347,23 @@ impl Scheduler { // Show a progress bar during the non-plotting middle section of the run. // The first `plot_lcycles` and last `plot_lcycles` are plotted instead. if self.current_lcycle >= plot_lcycles - && (tot_pps_to_sched - n_scheduled >= plot_lcycles) + && n_scheduled + plot_lcycles <= tot_pps_to_sched { if self.current_lcycle == plot_lcycles { print!("Scheduling {} products: ", tot_pps_to_sched); } - let pct_complete = (n_scheduled * 100) / tot_pps_to_sched; + let pct_complete = + ((n_scheduled * 100) / tot_pps_to_sched).min(100); if pct_complete > prev_pct_complete { // \x08 is backspace; overwrite the previous "XX%" in-place. print!("\x08\x08\x08{:02}%", pct_complete); std::io::stdout().flush()?; prev_pct_complete = pct_complete; } - if tot_pps_to_sched - n_scheduled == plot_lcycles { + if n_scheduled + plot_lcycles == tot_pps_to_sched { print!("\n"); } - } else { + } else if plotting { let plot_info_str = self.stats.plot_info_str(); assert!(!plot_info_str.is_empty()); let fname_added = format!(".{}", self.current_lcycle); @@ -375,7 +395,7 @@ impl Scheduler { } } } else if pp.gate_type.is_t() { - if self.recovery_t_ids.contains(pp_id) { 2 } else { 1 } + 1 } else { 1 }; @@ -391,10 +411,14 @@ impl Scheduler { } } else { debug_sched!("Could not schedule anything on lcycle {}", self.current_lcycle); - // If nothing is cultivating, no magic state will ever become ready, - // so the layout is fundamentally unable to make progress. - if !(0..self.input.topo.n_nodes) - .any(|node_i| self.input.topo.is_cultivating(node_i as u16)) + // If nothing is cultivating, no magic state will ever become ready — + // but only T gates need magic. If all pending products are Cliffords + // (e.g. correction S gate emission temporarily failed), they don't need + // magic and will route once the area frees up; don't error in that case. + let has_pending_t = self.pps_pending.iter().any(|pp| pp.gate_type.is_t()); + if has_pending_t + && !(0..self.input.topo.n_nodes) + .any(|node_i| self.input.topo.is_cultivating(node_i as u16)) { return Err(io::Error::new( io::ErrorKind::Other, @@ -409,9 +433,7 @@ impl Scheduler { self.check_clifford_repetitions()?; #[cfg(debug_assertions)] self.check_schedule()?; - // Return total lcycles and total "attempts" (successes + failures), so the - // caller can compute the true T-gate count including failed first attempts. - Ok((self.current_lcycle, self.scheduled_products.len() + self.t_gate_failures)) + Ok((self.current_lcycle, self.scheduled_products.len())) } fn init_magic_nodes(&mut self) { @@ -457,7 +479,11 @@ impl Scheduler { return false; } let (_, preferred, side) = &root_info[i]; - if preferred.iter().all(|&rid| self.used[rid as usize]) + // Only reject when there are root candidates and all are occupied. + // Empty preferred+side means the gate uses sched_s_sx (no root needed), + // so the vacuous-truth case must not fire here. + if (!preferred.is_empty() || !side.is_empty()) + && preferred.iter().all(|&rid| self.used[rid as usize]) && side.iter().all(|&rid| self.used[rid as usize]) { info_sched!(" No unused root candidates for node {}", node_id); @@ -541,6 +567,92 @@ impl Scheduler { println!("Precomputed {} multi-term Clifford trees", n_precomputed); } + /// Computes root candidate info for a set of already-computed terminal node IDs. + /// Extracted from `precompute_terminals_and_roots` so it can be called for dynamically + /// constructed operator lists (e.g. after S^k conjugation of T gate operators). + fn compute_root_info_for_terminals( + topo: &TopoGraph, terminals: &[u16], + ) -> Vec<(bool, Vec, Vec)> { + let mut root_info = Vec::with_capacity(terminals.len()); + for &term_id in terminals { + let node = topo.node(term_id); + let is_paired = + node.paired_data_id.map(|pid| terminals.contains(&pid)).unwrap_or(false); + let mut preferred = Vec::new(); + let mut side = Vec::new(); + if is_paired { + let is_x = topo.label(term_id).contains('X'); + for &nb_id in node.nbs_slice() { + let nb = topo.node(nb_id); + if !nb.is_routing() { + continue; + } + if (is_x && nb.pos.1 < node.pos.1) || (!is_x && nb.pos.1 > node.pos.1) { + preferred.push(nb_id); + } else if nb.pos.0 != node.pos.0 && nb.pos.1 == node.pos.1 { + side.push(nb_id); + } + } + } else { + for &nb_id in node.nbs_slice() { + let nb = topo.node(nb_id); + if nb.is_routing() && nb.pos.0 != node.pos.0 && nb.pos.1 == node.pos.1 { + preferred.push(nb_id); + } + } + } + root_info.push((is_paired, preferred, side)); + } + root_info + } + + /// Applies the current per-qubit S^k conjugation to a T gate's operators just-in-time, + /// updates `precomputed_terminals` and `precomputed_root_info` for the product, and + /// returns the list of (qubit, new_basis) pairs where a swap occurred. + /// + /// Always recomputes from the circuit's original operators so that correction_power + /// changes between lcycles are always reflected correctly. + fn apply_t_conjugation(&mut self, pp_id: i32) -> Vec<(u16, char)> { + // Copy original operator data to release the borrow on self.input.circuit. + let orig_ops: Vec<(u16, char)> = self + .input + .circuit + .product(pp_id) + .operators + .iter() + .map(|op| (op.qubit, op.basis)) + .collect(); + // Apply S^k conjugation in a single pass: odd power swaps X↔Y (Z unchanged). + let mut new_ops = Vec::with_capacity(orig_ops.len()); + let mut swapped = Vec::new(); + for &(qubit, basis) in &orig_ops { + let new_basis = if self.correction_power[qubit as usize] % 2 == 1 { + if basis == 'X' { 'Y' } else if basis == 'Y' { 'X' } else { basis } + } else { + basis + }; + if new_basis != basis { + swapped.push((qubit, new_basis)); + } + new_ops.push(Operator { qubit, basis: new_basis }); + } + #[cfg(debug_assertions)] + for &(q, new_basis) in &swapped { + info_sched!( + " T gate {}: qubit {} conjugated to {} (S^{} correction active)", + pp_id, + q, + new_basis, + self.correction_power[q as usize] + ); + } + let terminals = operators_to_node_ids(&self.input.topo, &new_ops); + let root_info = Self::compute_root_info_for_terminals(&self.input.topo, &terminals); + self.precomputed_terminals[pp_id as usize] = terminals; + self.precomputed_root_info[pp_id as usize] = root_info; + swapped + } + /// Precomputes terminal node IDs and root candidates for every product. /// /// For each terminal data node the root candidates are split into: @@ -625,7 +737,7 @@ impl Scheduler { let initial_magic = n_avail_magic; self.pp_paths.clear(); self.used.fill(false); - // Collect carry-forward data before the loop to release borrows on the maps. + // Collect carry-forward data before the loop to release borrows on the map. let clifford_carry: Vec<(i32, Vec, Option>)> = self .clifford_paths .values() @@ -633,20 +745,10 @@ impl Scheduler { (pp.id, node_ids.clone(), opt_tree.as_ref().map(Rc::clone)) }) .collect(); - let failed_t_carry: Vec<(i32, Vec, Option>)> = self - .failed_t_paths - .values() - .map(|(pp, node_ids, opt_tree)| { - (pp.id, node_ids.clone(), opt_tree.as_ref().map(Rc::clone)) - }) - .collect(); for (pp_id, node_ids, opt_tree) in clifford_carry { self.carry_forward_path(pp_id, &node_ids, opt_tree); } - for (pp_id, node_ids, opt_tree) in failed_t_carry { - self.carry_forward_path(pp_id, &node_ids, opt_tree); - } - let carry_forward_count = self.clifford_paths.len() + self.failed_t_paths.len(); + let carry_forward_count = self.clifford_paths.len(); let tot_available = carry_forward_count + self.pps_pending.len(); info_sched!(" Remaining to schedule: {}", self.pps_pending.len()); self.sched_precomputed(plotting); @@ -662,20 +764,27 @@ impl Scheduler { ); if self.pp_paths.is_empty() { if n_avail_magic > 0 { - panic!( - "{}", - format!( - "lcycle {}: Cannot schedule products [{}] on current layout ({} magic)", - self.current_lcycle, - self.pps_pending - .iter() - .map(|pp| pp.to_operator_str()) - .collect::>() - .join(", "), - n_avail_magic, - ) - .red() - ); + // Only panic when T gates are pending but couldn't consume available magic. + // If all pending products are Cliffords whose correction S gate emission failed + // this lcycle (e.g. routing area temporarily congested), they will retry next + // lcycle — that is not a bug. + let has_pending_t = self.pps_pending.iter().any(|pp| pp.gate_type.is_t()); + if has_pending_t { + panic!( + "{}", + format!( + "lcycle {}: Cannot schedule products [{}] on current layout ({} magic)", + self.current_lcycle, + self.pps_pending + .iter() + .map(|pp| pp.to_operator_str()) + .collect::>() + .join(", "), + n_avail_magic, + ) + .red() + ); + } } false } else { @@ -741,10 +850,92 @@ impl Scheduler { } } + /// Attempts to emit a physical S correction gate for `qubit` when `correction_power[qubit]` + /// is non-zero. Returns `true` if the qubit's correction was handled (gate emitted or power=2 + /// classical-only), `false` if scheduling the physical S gate failed this lcycle. + /// + /// On success the correction power is cleared to 0. On failure (NoPath), the circuit push is + /// rolled back and the correction power is left unchanged so the caller can retry next lcycle. + fn try_emit_s_correction(&mut self, qubit: u16, plotting: bool) -> bool { + let q = qubit as usize; + let power = self.correction_power[q]; + info_sched!( + " Correction emit for qubit {} (power={}, basis={})", + qubit, + power, + self.correction_basis[q] + ); + if power == 0 { + return true; + } + if power == 2 { + // Z correction: tracked classically — no physical gate needed. + info_sched!(" Qubit {}: Z correction (classical), clearing power", qubit); + self.correction_power[q] = 0; + return true; + } + // power 1 (S) or 3 (S†): emit a physical S gate on the qubit. + let s_id = self.next_correction_id; + let basis = self.correction_basis[q]; + let s_pp = PauliProduct { + id: s_id, + gate_type: GateType::S, + operators: vec![Operator { qubit, basis }], + parents: vec![], + children: vec![], + max_qubit: qubit, + }; + self.input.circuit.pps.push(s_pp); + let new_ops = vec![Operator { qubit, basis }]; + let terminals = operators_to_node_ids(&self.input.topo, &new_ops); + let root_info = Self::compute_root_info_for_terminals(&self.input.topo, &terminals); + self.precomputed_terminals.push(terminals); + self.precomputed_root_info.push(root_info); + + if !self.terminal_nodes(s_id) { + self.input.circuit.pps.pop(); + self.precomputed_terminals.pop(); + self.precomputed_root_info.pop(); + return false; + } + let result = self.sched_s_sx(s_id, plotting); + match result { + PathResult::PathFound(opt_graph) => { + info_sched!( + " Emitted correction S gate id={} for qubit {} (power={}), clearing", + s_id, + qubit, + power + ); + self.next_correction_id += 1; + self.correction_gates_emitted += 1; + if let Some(ref g) = opt_graph { + let node_ids: Vec = g.iter_nodes().collect(); + self.mark_nodes_used(&node_ids); + } + self.pp_paths.push((s_id, opt_graph.map(Rc::new))); + self.correction_power[q] = 0; + true + } + PathResult::NoPath => { + info_sched!( + " Correction S gate emit for qubit {} (power={}) failed (NoPath), will retry next lcycle", + qubit, + power + ); + self.input.circuit.pps.pop(); + self.precomputed_terminals.pop(); + self.precomputed_root_info.pop(); + false + } + } + } + /// Second pass: greedily schedule T gates, measurements, and S/SX gates. /// T gates are skipped when no magic state is available. fn sched_remaining(&mut self, n_avail_magic: &mut usize, plotting: bool) { let _timer = accum_start!(self.timers); + self.t_conjugated_bases.clear(); for i in 0..self.pps_pending.len() { let pp_id = self.pps_pending[i].id; let pp = self.input.circuit.product(pp_id); @@ -752,6 +943,49 @@ impl Scheduler { continue; } let (pp_id, gate_type) = (pp.id, pp.gate_type); + // For single-qubit Clifford gates (S/SX), check for pending correction and emit + // a physical S gate before proceeding. CX is multi-qubit and handled by + // sched_precomputed, so it never appears here. + if gate_type.is_s() || gate_type.is_sx() { + let op_qubit = self.input.circuit.product(pp_id).operators[0].qubit; + if self.correction_power[op_qubit as usize] != 0 { + info_sched!( + " Clifford {:?} {} (qubit {}) deferred: pending correction power={}", + gate_type, + pp_id, + op_qubit, + self.correction_power[op_qubit as usize] + ); + let emitted = self.try_emit_s_correction(op_qubit, plotting); + // Block the Clifford's qubit node only when the correction S gate was + // successfully placed; if emission failed, leave the node free so T gates + // can still route through it this lcycle (avoids deadlock). + if emitted { + let pp = self.input.circuit.product(pp_id); + Self::mark_blocked_product_as_used(&mut self.used, &self.input.topo, pp); + } + continue; + } + } + // For T gates, apply just-in-time S^k conjugation (X↔Y swap for odd power). + // Skip entirely when no qubit in the product has an odd correction power AND + // the precomputed terminals are not stale from a prior conjugation — this is + // the common case and avoids Vec allocations + topology lookups every lcycle. + if gate_type.is_t() { + let any_odd_correction = self.input.circuit.product(pp_id).operators + .iter() + .any(|op| self.correction_power[op.qubit as usize] % 2 == 1); + let was_conjugated = self.conjugated_t_products.contains(&pp_id); + if any_odd_correction || was_conjugated { + let swapped = self.apply_t_conjugation(pp_id); + if swapped.is_empty() { + self.conjugated_t_products.remove(&pp_id); + } else { + self.conjugated_t_products.insert(pp_id); + self.t_conjugated_bases.insert(pp_id, swapped); + } + } + } if *n_avail_magic > 0 || !gate_type.is_t() { info_sched!(" Trying to schedule product {}", self.input.circuit.product(pp_id)); let result = if !self.terminal_nodes(pp_id) { @@ -913,21 +1147,15 @@ impl Scheduler { self.scheduled_ids_buf.extend(self.pp_paths.iter().map(|(id, _)| *id)); self.pps_pending.retain(|pp| !self.scheduled_ids_buf.contains(&pp.id)); debug_sched!("After purge, pps_to_sched len {}", self.pps_pending.len()); - // Only count T gates that are newly scheduled (not recovery lcycles) to - // keep the pool-size estimate accurate. let t_newly_scheduled = self .pp_paths .iter() - .filter(|(id, _)| { - self.input.circuit.product(*id).gate_type.is_t() - && !self.failed_t_paths.contains_key(id) - }) + .filter(|(id, _)| self.input.circuit.product(*id).gate_type.is_t()) .count(); self.cultivation.t_products_remaining = self.cultivation.t_products_remaining.saturating_sub(t_newly_scheduled); - let (t_failed_ids, t_recovery_ids) = self.process_t_gate_outcomes(); - self.recovery_t_ids = t_recovery_ids; - self.unlock_children(&t_failed_ids); + self.process_t_gate_outcomes(); + self.unlock_children(); self.advance_clifford_state(); debug_sched!( "After inserting previous lcycle cliffords, pps_to_sched len {}", @@ -940,83 +1168,60 @@ impl Scheduler { self.children_buf.len(), self.pps_pending.len() ); - let lcycle_ids: Vec = self - .pp_paths - .iter() - .filter(|(id, _)| !t_failed_ids.contains(id)) - .map(|(id, _)| *id) - .collect(); + let lcycle_ids: Vec = self.pp_paths.iter().map(|(id, _)| *id).collect(); self.lcycle_scheduled.push((self.current_lcycle, lcycle_ids)); #[cfg(debug_assertions)] - self.check_lcycle(&t_failed_ids, &self.recovery_t_ids)?; - self.scheduled_products.extend( - self.pp_paths.iter().filter(|(id, _)| !t_failed_ids.contains(id)).map(|(id, _)| *id), - ); + self.check_lcycle()?; + self.scheduled_products.extend(self.pp_paths.iter().map(|(id, _)| *id)); Ok(()) } - /// Coin-flip T gate outcomes; updates `failed_t_paths`; returns (failed_ids, recovery_ids). + /// Coin-flip T gate outcomes and update per-qubit correction tracking. /// - /// First-attempt T gates succeed with 50% probability (or always if `no_t_failures`). - /// Recovery-lcycle T gates (already in `failed_t_paths`) always succeed. - /// Failed gates are stored in `failed_t_paths` with the magic root trimmed off - /// so the routing subtree can be reused in the recovery lcycle. - fn process_t_gate_outcomes(&mut self) -> (Vec, Vec) { - let mut t_failed_ids: Vec = Vec::new(); - let mut t_recovery_ids: Vec = Vec::new(); + /// T gates complete in one lcycle regardless of outcome. A failed T gate + /// increments `correction_power` for each of its qubits (tracked classically); + /// no recovery lcycle is needed. + fn process_t_gate_outcomes(&mut self) { let pp_ids: Vec = self.pp_paths.iter().map(|(id, _)| *id).collect(); - for pp_id in &pp_ids { - let pp_id = *pp_id; - let pp = self.input.circuit.product(pp_id); - if pp.gate_type.is_t() { - if self.failed_t_paths.contains_key(&pp_id) { - t_recovery_ids.push(pp_id); - info_sched!(" T gate {} recovery lcycle succeeded", pp_id); - } else if self.no_t_failures || self.rng_uniform.gen_bool(0.5) { - info_sched!(" T gate {} succeeded on first attempt", pp_id); - } else { - t_failed_ids.push(pp_id); - self.t_gate_failures += 1; - info_sched!( - " T gate {} failed (50% probability), recovery lcycle next", - pp_id - ); - } - } - } - let pp_paths_snapshot: Vec<(i32, Option>)> = - self.pp_paths.iter().map(|(id, opt)| (*id, opt.as_ref().map(Rc::clone))).collect(); - for (pp_id, opt_pp_path) in &pp_paths_snapshot { - let pp_id = *pp_id; + for pp_id in pp_ids { let pp = self.input.circuit.product(pp_id); if !pp.gate_type.is_t() { continue; } - if t_failed_ids.contains(&pp_id) { - // Store the routing subtree without the magic root so the recovery - // lcycle can reuse the same data/routing nodes while finding a new - // magic state. When not plotting, fall back to just the terminal IDs. - let trimmed_opt_tree: Option> = opt_pp_path.as_ref().map(|tree| { - let mut t = (**tree).clone(); - t.trim_magic_root(); - Rc::new(t) - }); - let node_ids: Vec = if let Some(ref trimmed) = trimmed_opt_tree { - trimmed.iter_nodes().collect() - } else { - self.precomputed_terminals[pp_id as usize].clone() - }; - self.failed_t_paths.insert(pp_id, (pp.clone(), node_ids, trimmed_opt_tree)); + if self.no_t_failures || self.rng_uniform.gen_bool(0.5) { + info_sched!(" T gate {} succeeded", pp_id); } else { - self.failed_t_paths.swap_remove(&pp_id); + self.t_gate_failures += 1; + info_sched!(" T gate {} failed (50% probability), S correction tracked", pp_id); + // Update per-qubit correction tracking using the conjugated operator bases + // recorded in sched_remaining. Each failed qubit increments its S^k power. + let pp = self.input.circuit.product(pp_id); + let conjugated_map: HashMap = self + .t_conjugated_bases + .get(&pp_id) + .map(|v| v.iter().copied().collect()) + .unwrap_or_default(); + let orig_ops: Vec<(u16, char)> = + pp.operators.iter().map(|op| (op.qubit, op.basis)).collect(); + for (qubit, orig_basis) in orig_ops { + let q = qubit as usize; + self.correction_power[q] = (self.correction_power[q] + 1) % 4; + self.correction_basis[q] = + conjugated_map.get(&qubit).copied().unwrap_or(orig_basis); + info_sched!( + " Qubit {} correction: power now {} (basis={})", + qubit, + self.correction_power[q], + self.correction_basis[q] + ); + } } } - (t_failed_ids, t_recovery_ids) } /// Decrements `remaining_parents` for each completed product and collects /// newly-ready children into `children_buf`. - fn unlock_children(&mut self, t_failed_ids: &[i32]) { + fn unlock_children(&mut self) { self.children_buf.clear(); for i in 0..self.pp_paths.len() { let pp_id = self.pp_paths[i].0; @@ -1036,9 +1241,6 @@ impl Scheduler { _ => {} } } - if gate_type.is_t() && t_failed_ids.contains(&pp_id) { - continue; - } let children: Vec = self.input.circuit.product(pp_id).children.clone(); for child_id in children { self.remaining_parents[child_id as usize] -= 1; @@ -1109,11 +1311,12 @@ impl Scheduler { println!("Steiner tree computation called {} times", self.stree_computation.n_calls); println!("A* computation called {} times", self.astar.n_calls); - // Write normalized cultivation-time distribution to file. - let dist_fname = format!("{}.cultivation_dist", self.input.circuit_stem()); - match self.write_cultivation_dist(&dist_fname, min, max) { - Ok(()) => println!("Cultivation time distribution written to {}", dist_fname), - Err(e) => eprintln!("Warning: could not write cultivation dist: {}", e), + if self.record_cultivation_dist { + let dist_fname = format!("{}.cultivation_dist", self.input.circuit_stem()); + match self.write_cultivation_dist(&dist_fname, min, max) { + Ok(()) => println!("Cultivation time distribution written to {}", dist_fname), + Err(e) => eprintln!("Warning: could not write cultivation dist: {}", e), + } } } @@ -1137,7 +1340,7 @@ impl Scheduler { } #[cfg(debug_assertions)] - fn check_lcycle(&self, _t_failed_ids: &[i32], t_recovery_ids: &[i32]) -> io::Result<()> { + fn check_lcycle(&self) -> io::Result<()> { let mut lcycle_used = vec![false; self.input.topo.n_nodes]; for &(pp_id, ref opt_tree) in &self.pp_paths { let Some(tree) = opt_tree else { continue }; @@ -1157,7 +1360,7 @@ impl Scheduler { )); } } - for nid in operators_to_node_ids(&self.input.topo, &pp.operators) { + for &nid in &self.precomputed_terminals[pp_id as usize] { if !tree.contains_node(nid) { return Err(io::Error::new( io::ErrorKind::Other, @@ -1169,7 +1372,7 @@ impl Scheduler { )); } } - if pp.gate_type.is_t() && !t_recovery_ids.contains(&pp_id) { + if pp.gate_type.is_t() { match tree.root_node_id { None => { return Err(io::Error::new( @@ -1421,7 +1624,7 @@ mod tests { let mut topo = TopoGraph::new(); topo.set_topo(4, &"dummy".to_string(), &"".to_string(), &0, true, 1, false); let mut sched = - Scheduler::new(circuit, topo, 0.0387396, "none", String::new(), rseed, false); + Scheduler::new(circuit, topo, 0.0387396, "none", String::new(), rseed, false, false); sched.sched_circuit().expect("sched_circuit failed"); sched } @@ -1494,28 +1697,19 @@ mod tests { } #[test] - fn failed_t_paths_empty_after_schedule_completes() { - let lines = &["+X___", "-_X__", "+__X_", "-___X"]; - let sched = run_scheduler(lines, 0); - assert!( - sched.failed_t_paths.is_empty(), - "failed_t_paths not empty after schedule_circuit: {:?}", - sched.failed_t_paths.keys().collect::>() - ); - } - - #[test] - fn lcycle_count_bounded_by_t_gate_failure_overhead() { + fn lcycle_count_not_inflated_by_t_gate_failures() { + // T gate failures are tracked classically (correction_power); they do NOT + // add extra scheduling lcycles. All N independent T gates complete in at + // most N lcycles regardless of failure count. let lines = &["+X___", "-_X__", "+__X_", "-___X"]; let sched = run_scheduler(lines, 5); let n_t = 4usize; let active_lcycles = sched.lcycle_scheduled.len(); assert!( - active_lcycles <= n_t + sched.t_gate_failures, - "active lcycles {} > n_t {} + failures {}", + active_lcycles <= n_t, + "active lcycles {} > n_t {} (failures should not add extra lcycles)", active_lcycles, n_t, - sched.t_gate_failures ); } } diff --git a/src/treegraph.rs b/src/treegraph.rs index a45c078..eba974b 100644 --- a/src/treegraph.rs +++ b/src/treegraph.rs @@ -107,6 +107,7 @@ impl TreeGraph { /// Removes the magic root and trims dangling routing nodes. /// Routing nodes whose sole remaining nb is a data node are preserved. + #[cfg(test)] pub(crate) fn trim_magic_root(&mut self) { let root_id = match self.root_node_id.take() { Some(id) => id,