Skip to content

Commit dabde6e

Browse files
xoth42Krastanov
andauthored
Canonicalization (#34)
* Update version, changelog v0.1.3 * Canonicalize and cleanup circuits cleanup_two_measurements! unsafe_cleanup_untargeted_pairs! unsafe_cleanup_nonmeasurement_last_steps! cleanup_measurements_on_top_qubits! * canonicalization is split between safe/unsafe, and will do unsafe +safe the first half of running, and only safe on the second half. Co-authored-by: Stefan Krastanov <[email protected]>
1 parent 81ea2dc commit dabde6e

File tree

6 files changed

+384
-7
lines changed

6 files changed

+384
-7
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# News
22

3+
## v0.1.3 - 2025-08-22
4+
5+
- Generated circuits are 'cleaned up': canonicalized and edited if something is wrong. The first part of the optimization will run the 'unsafe' + 'safe' cleanups, and the second part only does the 'safe' ones.
6+
- 'safe' cleanups are when there is no risk of making the circuit worse, such as removing excess measurements on the same pairs, or removing measurements on the purified pairs.
7+
- 'unsafe' cleanups risk making a circuit worse, but can also solve issues that the circuit may have, such as 'using' an unused pair by adding random a CNOTPerm and measurement on it, or adding a measurement at the end of a pair's ops if there are none.
8+
- The switch from unsafe+safe to only safe happens half way through (Likely to change)
9+
- Adds Quantikz, BPGates 1.2.2 as deps
10+
311
## v0.1.2 - 2025-08-12
412

513
- Pluto reactivity. Running the pluto notebook is a lot more enjoyable from the user side. Configuration changes are buffered by a PlutoUI.confirm query, meaning many variables can be changed before any simulation is run. Restarting/running the simulation also are now buffered with buttons.

Project.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
name = "QEPOptimize"
22
uuid = "9f67e06f-6394-43be-a72d-a2000081c01d"
33
authors = ["Stefan Krastanov <[email protected]>", "QEPOptimize contributors"]
4-
version = "0.1.2"
4+
version = "0.1.3"
55

66
[deps]
77
BPGates = "2c1a90cd-bec4-4415-ae35-245016909a8f"
88
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
99
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
1010
OhMyThreads = "67456a42-1dca-4109-a031-0a68de7e3ad5"
11+
Quantikz = "b0d11df0-eea3-4d79-b4a5-421488cbf74b"
1112
ProgressLogging = "33c8b6b6-d38a-422a-b730-caa89a2f386c"
1213
QuantumClifford = "0525e862-1e90-11e9-3e4d-1b39d7109de1"
1314
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
1415
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
1516

1617
[compat]
17-
BPGates = "1.2"
18+
BPGates = "1.2.2"
1819
ProgressLogging = "0.1.4"
1920
DataStructures = "0.18.22, 0.19"
2021
Makie = "0.22.4, 0.23, 0.24"
2122
OhMyThreads = "0.8.3"
23+
Quantikz = "1.4.1"
2224
QuantumClifford = "0.9, 0.10"
2325
Random = "1"
2426
Statistics = "1.11.1"

src/QEPOptimize.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ using BPGates: mctrajectory!, continue_stat, PauliNoise, BellMeasure, CNOTPerm,
77
# TODO (mctrajectory!, continue_stat, PauliNoise) should be exported by default
88

99

10+
using QuantumClifford:AbstractMeasurement
11+
1012
using Makie
1113

1214
using Statistics: mean
@@ -15,7 +17,9 @@ using Random: randperm
1517

1618
using DataStructures: counter
1719

18-
using OhMyThreads: tmap,tmapreduce
20+
using OhMyThreads: tmap,tmap!,tmapreduce
21+
22+
using Quantikz:affectedqubits
1923

2024
using ProgressLogging: @progress
2125

@@ -31,5 +35,6 @@ include("performance_eval.jl")
3135
include("mutate.jl")
3236
include("evolve.jl")
3337
include("analysis.jl")
38+
include("canonicalization.jl")
3439

3540
end

src/canonicalization.jl

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Special thanks to Marius Minea for teaching a great class on higher-order functions! - xoth42
2+
3+
"""
4+
cleanup_two_measurements!(ops,num_pairs)
5+
6+
Remove unnecessary measurements (back to back on the same qubit)
7+
"""
8+
function cleanup_two_measurements!(ops,num_pairs)
9+
# This starts as [false,false...] for each pair/qubit.
10+
# for each op, mark this accordingly -> non-measure would set the qubits to false, and yes measure would set qubits to true. If we encounter a measure on something that already has true, it is a redundant back-to-back measure, so we delete that op.
11+
last_op_on_pair_was_measure = falses(num_pairs)
12+
13+
ops_to_delete = [] # this will collect the indexes of redundant measures to be removed
14+
15+
# go through the each operation
16+
for (op_index,op) in enumerate(ops)
17+
measure = typeof(op) <: AbstractMeasurement # is the op a measurement?
18+
for qubit in affectedqubits(op)
19+
if measure
20+
# measure found, is it redundant?
21+
if last_op_on_pair_was_measure[qubit]
22+
# redundant measure found. add index to delete list
23+
push!(ops_to_delete,op_index)
24+
else
25+
# non redundant, but mark measured array incase there is another
26+
last_op_on_pair_was_measure[qubit] = true
27+
end
28+
else
29+
# this op is not a measurement, mark measured array as false
30+
last_op_on_pair_was_measure[qubit] = false
31+
end
32+
end
33+
end
34+
35+
# Now act on the delete list
36+
return deleteat!(ops,ops_to_delete)
37+
end
38+
39+
"""
40+
get_used_qubits(ops)
41+
42+
Get a Set of all qubits that are affected by the ops.
43+
"""
44+
function get_used_qubits(ops)
45+
# for each op, get its affectedqubits. (map ops to qubits)
46+
# affected qubits can return Int, or Vector{Int}
47+
# expand these results (...), making a set of all ints
48+
# this effectively is the set of all 'affected/used' qubits/pairs
49+
return reduce((set,qubits) -> push!(set, affectedqubits(qubits)...), ops; init=Set())
50+
end
51+
52+
"""
53+
unsafe_cleanup_untargeted_pairs!(ops,num_pairs,num_purified)
54+
55+
UNSAFE
56+
Check if the circuit has an unused qubit (technically a pair), if so, add a random two qubit Bell preserving gate `CNOTPerm` and coincidence measure in a random basis, ie, use it in a "probably" good way.
57+
"""
58+
function unsafe_cleanup_untargeted_pairs!(ops,num_pairs,num_purified)
59+
if num_pairs <= num_purified
60+
# assumption for adding CNOTPerms.
61+
# if the user wants it anyway, just warn and return
62+
@warn "Not enough pairs - skipping canonicalization step cleanup_untargeted_pairs!"
63+
return ops
64+
end
65+
66+
# The pairs that should be used (all of them)
67+
pairs_to_use = Set([i for i in 1:num_pairs])
68+
69+
# pairs that are actually used
70+
pairs_used = get_used_qubits(ops)
71+
72+
# pairs that are unused, and others
73+
pairs_unused = symdiff(pairs_to_use,pairs_used)
74+
75+
# use any unused pairs
76+
for pair in pairs_unused
77+
# all pairs should be in the usable pairs
78+
@assert pair in pairs_to_use
79+
80+
# if it is a purified pair, use this pair as a control for a new CNOT
81+
if pair <= num_purified
82+
pushfirst!(ops,CNOTPerm(rand(1:6),rand(1:6),pair,rand(num_purified+1:num_pairs)))
83+
else
84+
# add a CNOT, and measure to the front. New ops becomes -> [CNOT, MEAS, ...]
85+
# using pushfirst! so this is done in opposite order.
86+
# control will be a purified pair, and target is this pair
87+
pushfirst!(ops,rand(BellMeasure,pair))
88+
pushfirst!(ops,rand(CNOTPerm,rand(1:num_purified),pair))
89+
end
90+
end
91+
92+
return ops
93+
end
94+
95+
"""
96+
cleanup_measurements_on_top_qubits!(ops,num_purified)
97+
98+
Checks for and removes any measurements on purified pairs
99+
"""
100+
function cleanup_measurements_on_top_qubits!(ops,num_purified)
101+
return filter!(op->
102+
!( # remove it if,
103+
typeof(op) <: AbstractMeasurement # it is a measurement and,
104+
&& op.sidx <= num_purified # it measures a purified pair
105+
), ops)
106+
end
107+
108+
"""
109+
unsafe_cleanup_nonmeasurement_last_steps!(ops,num_pairs,num_purified)
110+
111+
UNSAFE
112+
If there is a non-measurement in the last step of a non-purified pair, add random coincidence measurements.
113+
"""
114+
function unsafe_cleanup_nonmeasurement_last_steps!(ops,num_pairs,num_purified)
115+
if num_pairs <= num_purified
116+
@warn "Not enough pairs - skipping canonicalization step cleanup_nonmeasurement_last_steps!"
117+
return ops
118+
end
119+
120+
non_pure_last_steps = zeros(Int64,num_pairs - num_purified) # array id, plus the num_purified, is the non_pure pair number. The contents of the array
121+
notDone = true
122+
while notDone
123+
# get all of the last steps
124+
for (op_index,op) in enumerate(ops)
125+
qubits = affectedqubits(op)
126+
for qubit in qubits
127+
if qubit > num_purified
128+
# this is a non purified pair, mark a new last step
129+
non_pure_last_steps[qubit - num_purified] = op_index
130+
end
131+
end
132+
end
133+
134+
# check if any of the last ops are not measurements, and ignore zeros (no ops on that qubit)
135+
qubits_who_need_measurements = reduce((arr, (qubit, op_index)) ->
136+
(op_index == 0 # filter out zeros
137+
|| typeof(ops[op_index]) <: AbstractMeasurement) ? arr : # and filter out measurments
138+
push!(arr,qubit+num_purified), # otherwise, keep it and add 'num_purified' so the qubit index is correct
139+
enumerate(non_pure_last_steps);init=[])
140+
141+
# all of the marked ops are non_last measurements, so add measures to them
142+
for qubit in qubits_who_need_measurements
143+
push!(ops,rand(BellMeasure,qubit))
144+
end
145+
146+
# check if done
147+
if length(qubits_who_need_measurements) == 0
148+
notDone = false
149+
end
150+
end
151+
return ops
152+
end
153+
154+
"""
155+
canonicalize_cleanup!(pop::Population,num_pairs,num_purified;
156+
safe=true # true to only improve circuits, false to include canonicalizations that may make some circuits worse/make experimental changes to try to improve them
157+
)
158+
159+
Apply all of the canonicalization cleanup methods to a population with tmap. Defaults to safe canonicalizations.
160+
"""
161+
function canonicalize_cleanup!(pop::Population,num_pairs,num_purified;
162+
safe=true # :all (include canonicalizations that may make some circuits worse/make experimental changes to try to improve) or :safe (only improve circuits)
163+
)
164+
# Function to change the ops based on the canonicalizations requested
165+
cleanup! = safe ?
166+
(indiv) -> begin # safe is true
167+
# Safe canonicalization
168+
cleanup_measurements_on_top_qubits!(indiv.ops,num_purified)
169+
cleanup_two_measurements!(indiv.ops,num_pairs)
170+
end : (indiv) -> begin # safe is false
171+
# Unsafe canonicalization (plus safe checks)
172+
cleanup_measurements_on_top_qubits!(indiv.ops,num_purified)
173+
cleanup_two_measurements!(indiv.ops,num_pairs)
174+
unsafe_cleanup_nonmeasurement_last_steps!(indiv.ops,num_pairs,num_purified)
175+
unsafe_cleanup_untargeted_pairs!(indiv.ops,num_pairs,num_purified)
176+
end
177+
178+
tmap(cleanup!, pop.individuals)
179+
end

src/evolve.jl

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
p_mutate=0.1,
1414
p_gain=0.1,
1515
evolution_metric=:logical_qubit_fidelity,
16-
max_performance_calcs=10
16+
max_performance_calcs=10,
17+
safe_canonicalize=true
1718
)
1819
1920
Important: This calls sort and cull.
@@ -37,7 +38,8 @@ function step!(
3738
p_mutate=0.1,
3839
p_gain=0.1,
3940
evolution_metric=:logical_qubit_fidelity,
40-
max_performance_calcs=10
41+
max_performance_calcs=10,
42+
safe_canonicalize=true
4143
)
4244
# Mark existing individuals as survivors
4345
# Survivors ensure that some individuals are carried over unchanged, maintaining good solutions
@@ -49,6 +51,8 @@ function step!(
4951

5052
add_mutations!(population.individuals; max_ops, new_mutants, valid_pairs=1:number_registers) # TODO (low priority) decouple valid_pairs from number_registers
5153

54+
canonicalize_cleanup!(population,number_registers,purified_pairs;safe=safe_canonicalize)
55+
5256
# Sort the population by fitness and cull the excess individuals to maintain the population size
5357
simulate_and_sort!(
5458
population;
@@ -139,7 +143,8 @@ function multiple_steps_with_history!(
139143
p_mutate,
140144
p_gain,
141145
evolution_metric,
142-
max_performance_calcs
146+
max_performance_calcs,
147+
safe_canonicalize = i > steps/2 # first half of steps, do unsafe canonicalization. Then, stick to safe.
143148
)
144149

145150
# Check to make sure that the optimizer is not in the 'fitness = 1.0' failure mode
@@ -219,7 +224,7 @@ function add_mutations!(
219224

220225
# populate the thread_mutes by running the function on each thread
221226
mutants = tmapreduce(indiv_to_mutes,vcat,individuals; nchunks=max_threads) # TODO the reduce operation should be vcat
222-
227+
223228
## add all mutes back to the individuals vector
224229
append!(individuals, mutants)
225230
end

0 commit comments

Comments
 (0)