Skip to content

Commit a9a70b2

Browse files
authored
Performance calculations carry over from previous runs (#32)
* Performance evaluations carry over from previous. Warn user when sim is throttled. Small test added * v0.1.1, update changelog * Improve coverage
1 parent 5c3cd35 commit a9a70b2

File tree

8 files changed

+203
-19
lines changed

8 files changed

+203
-19
lines changed

CHANGELOG.md

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

3+
## v0.1.1 - 2025-07-29
4+
- Performance evaluations carry over from previous runs.
5+
- Warning when simulation is throttled (fidelity = 1)
6+
37
## v0.1.0 - 2025-06-13
48

59
- Simulation capabilities for commond gates.

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "QEPOptimize"
22
uuid = "9f67e06f-6394-43be-a72d-a2000081c01d"
33
authors = ["Stefan Krastanov <[email protected]>", "QEPOptimize contributors"]
4-
version = "0.1.0"
4+
version = "0.1.1"
55

66
[deps]
77
BPGates = "2c1a90cd-bec4-4415-ae35-245016909a8f"

example/pluto.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
### A Pluto.jl notebook ###
2-
# v0.20.10
2+
# v0.20.13
33

44
using Markdown
55
using InteractiveUtils
@@ -64,6 +64,8 @@ md"""
6464
6565
* Number of Simulations: $(@bind num_simulations PlutoUI.Slider(100:100:5000, default=1000, show_value=true))
6666
67+
* Max performance calculations per circuit: $(@bind max_perf_calcs PlutoUI.Slider(1:1:50, default=10, show_value=true))
68+
6769
* Population Size: $(@bind pop_size PlutoUI.Slider(10:10:100, default=20, show_value=true))
6870
6971
* Initial Operations: $(@bind start_ops PlutoUI.Slider(5:20, default=10, show_value=true))
@@ -73,7 +75,7 @@ md"""
7375
7476
* Number of Evolution Steps: $(@bind evolution_steps PlutoUI.Slider(50:150, default=80, show_value=true))
7577
76-
* New Mutants: $(@bind new_mutants PlutoUI.Slider(5:5:30, default=10, show_value=true))
78+
* New Mutants: $(@bind new_mutants PlutoUI.Slider(5:1:30, default=10, show_value=true))
7779
7880
* Drop Probability: $(@bind p_drop PlutoUI.Slider(0.0:0.05:0.5, default=0.1, show_value=true))
7981
@@ -92,6 +94,7 @@ begin
9294
code_distance=1, # Not needed to change for now
9395
pop_size=pop_size,
9496
noises=[NetworkFidelity(network_fidelity), PauliNoise(paulix, pauliy, pauliz)],
97+
max_performance_calcs=max_perf_calcs
9598
)
9699

97100
init_config = (;

src/QEPOptimize.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module QEPOptimize
22

3+
import Base:+,==
4+
35
using BPGates
46
using BPGates: mctrajectory!, continue_stat, PauliNoise # TODO these should be exported by default
57

src/evolve.jl

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ function step!(
1919
p_drop=0.1,
2020
p_mutate=0.1,
2121
p_gain=0.1,
22+
max_performance_calcs=10
2223
)
2324
# Mark existing individuals as survivors
2425
# Survivors ensure that some individuals are carried over unchanged, maintaining good solutions
@@ -36,7 +37,8 @@ function step!(
3637
num_simulations,
3738
purified_pairs,
3839
number_registers, # TODO (low priority) this should be by-default derived from `indiv`
39-
noises=[NetworkFidelity(0.9)] # TODO configurable noise
40+
noises=[NetworkFidelity(0.9)], # TODO configurable noise
41+
max_performance_calcs
4042
)
4143
cull!(population, pop_size)
4244
end
@@ -50,8 +52,19 @@ function multiple_steps_with_history!(
5052
fitness_history[1, :] = [i.fitness for i in population.individuals]
5153
transition_counts = []
5254

55+
throttling_warned = 0
56+
5357
for i in 1:steps
5458
step!(population; step_config...)
59+
60+
# Check to make sure that the optimizer is not in the 'fitness = 1.0' failure mode
61+
if population.individuals[1].fitness == 1.0 && throttling_warned < THROTTLE_WARNINGS
62+
throttling_warned += 1
63+
@warn "Simulation is throttled: Increase simulation count, or decrease new mutants to fix. Top circuit has fitness = 1.0"
64+
# If fitness is 1, then all of the simulations done to evaluate a circuit show no errors. This implies the circuit is good, but stops the optimizer from performing well. Increasing simulation count can fix this issue
65+
# but, decreasing new mutants will also fix the issue. This is because fewer new circuits need to be simulated, causing more simulations on the current circuits to get a better non-1.0 fidelity.
66+
end
67+
5568
fitness_history[i+1,:] = [i.fitness for i in population.individuals]
5669
push!(transition_counts, counter([i.history for i in population.individuals]))
5770
step_callback()
@@ -138,21 +151,26 @@ function simulate_and_sort!(
138151
purified_pairs::Int=1,
139152
number_registers::Int=2, # TODO (low priority) this should be by-default derived from `indiv`
140153
code_distance::Int=1,
141-
noises=[NetworkFidelity(0.9)]
154+
noises=[NetworkFidelity(0.9)],
155+
max_performance_calcs::Int=10
142156
)
143157
# calculate and update each individual's performance
144158
function update!(indiv)
145-
calculate_performance!(indiv;
146-
num_simulations,
147-
purified_pairs,
148-
number_registers,
149-
code_distance,
150-
noises)
159+
# Restrict performance calculation if this indiv has already reached the max calcs.
160+
# However, if the calculated fidelity is undetermined (1.0), then it needs more calculations to try and get a non-one value (ie: 0.9999).
161+
# Otherwise, if circuits have a fidelity of 1.0, it is difficult to distinguish between ones that are better (f:0.9999) or worse (f:0.9997).
162+
if indiv.performance.num_calcs < max_performance_calcs || indiv.fitness == 1.0
163+
calculate_performance!(indiv;
164+
num_simulations,
165+
purified_pairs,
166+
number_registers,
167+
code_distance,
168+
noises)
169+
end
151170
indiv.fitness = indiv.performance.purified_pairs_fidelity # TODO make it possible to select the type of fitness to evaluate
152171
end
153172

154173
# Parallel processing for performance calculations. Max threads will be set by the threads specified when running julia. ex) julia -t 16
155-
max_threads::Int = Threads.nthreads()
156174

157175
tmap(update!,population.individuals)
158176

@@ -177,7 +195,8 @@ function initialize_pop!(
177195
num_simulations::Int=100,
178196
purified_pairs::Int=1,
179197
code_distance::Int=1,
180-
noises=[NetworkFidelity(0.9)]
198+
noises=[NetworkFidelity(0.9)],
199+
max_performance_calcs=10
181200
)
182201
valid_pairs=1:number_registers # TODO (low priority) decouple valid_pairs from number_registers
183202

@@ -197,7 +216,8 @@ function initialize_pop!(
197216
purified_pairs,
198217
number_registers,
199218
code_distance,
200-
noises
219+
noises,
220+
max_performance_calcs
201221
)
202222
cull!(population,pop_size)
203223
end

src/performance_eval.jl

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ function calculate_performance!(
1414

1515
count_success = 0
1616
counts_marginals = zeros(Int,purified_pairs) # an array to find F₁, F₂, …, Fₖ (tracks how often each purified bell pair is in the desired state)
17+
18+
# Edge case: purified pairs/registers have changed, and the circuit has previously had a performance calculation. In this case, the previous performance must be discarded, as its error probabilities are now invalid.
19+
if indiv.performance.num_calcs > 0 && length(indiv.performance.error_probabilities) != purified_pairs+1
20+
# signal the previous performance to be discarded
21+
indiv.performance = Performance() # implies it is a blank performance
22+
end
23+
1724
counts_nb_errors = zeros(Int,purified_pairs+1) # an array to find P₀, P₁, …, Pₖ (tracks how often a given total number of errors happens) Careful with indexing it as it includes a P₀!
1825

1926
noisy_purification_circuit = indiv.ops
@@ -39,8 +46,8 @@ function calculate_performance!(
3946
end
4047

4148
if count_success == 0
42-
indiv.performance = Performance(counts_nb_errors, 0,0, 0, 0) # TODO this is probably going to break the optimization runs and lead to picking low performing individuals for certain cost functions -- do this better
43-
return indiv.performance
49+
# TODO this is probably going to break the optimization runs and lead to picking low performing individuals for certain cost functions -- do this better
50+
return update_performance!(indiv,Performance(counts_nb_errors,0,0, 0, 0,1))
4451
end
4552

4653
p_success = count_success / num_simulations # proportion of successful simulations
@@ -50,7 +57,30 @@ function calculate_performance!(
5057
correctable_errors = div(code_distance - 1, 2) # maximum number of correctable errors based on code distance after teleportation
5158
indiv_logical_qubit_fidelity = sum(err_probs[1:min(end, correctable_errors+1)]) # calculates the logical qubit fidelity by summing the probabilities of correctable errors
5259

53-
indiv.performance = Performance(err_probs, err_probs[1], indiv_logical_qubit_fidelity, mean(marginals), p_success)
60+
return update_performance!(indiv,Performance(
61+
err_probs,
62+
err_probs[1],
63+
indiv_logical_qubit_fidelity,
64+
mean(marginals),
65+
p_success,
66+
1)
67+
)
68+
end
69+
70+
"""
71+
update_performance!(indiv::Individual,new::Performance)
72+
73+
Helper function to deal with circuit init, and performance averaging. The indiv given will have their performance averaged with the new performance, if their current performance is defined (nonzero). Otherwise, the new performance will overrite their current.
74+
"""
75+
function update_performance!(indiv::Individual,new::Performance)
76+
@assert new.num_calcs >= 1 # This should always be the case at this point. The new performance should not be empty.
5477

78+
if indiv.performance.num_calcs == 0
79+
# old perf has not been calculated yet/not usable, so use the new one
80+
indiv.performance = new
81+
else
82+
# old perf is calculated, so average the two
83+
indiv.performance += new
84+
end
5585
return indiv.performance
5686
end

src/types.jl

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# TODO (low priority) this would be a great place to use an Enum or, even better, an algebraic data type (ADT)
22
const HISTORIES = [:manual, :survivor, :random, :child, :drop, :gain, :swap, :opsmutate]
33

4+
const THROTTLE_WARNINGS = 10 # max amount of warnings per multiple_steps_with_history! call
5+
46
"A convenient structure to store various purification performance metrics."
57
struct Performance
68
"a list of probabilities as such `[probability for no errors, probability for one Bell pair to be erroneous, probability for two Bell pairs to be erroneous, ..., probability for all Bell pairs to be erroneous]`"
@@ -15,12 +17,62 @@ struct Performance
1517
average_marginal_fidelity::Float64
1618
"the proportion of runs of a given protocol that do not have detected errors (i.e. Alice and Bob do not measure any errors)"
1719
success_probability::Float64
20+
"the number of times that performance has been calculated and averaged for this circuit"
21+
num_calcs::Int64
1822
end
1923

20-
Performance() = Performance(Float64[], 0.0, 0.0, 0.0, 0.0)
24+
Performance() = Performance(Float64[], 0.0, 0.0, 0.0, 0.0,0)
25+
26+
Base.copy(p::Performance) = Performance(copy(p.error_probabilities), p.purified_pairs_fidelity, p.logical_qubit_fidelity, p.average_marginal_fidelity, p.success_probability,p.num_calcs)
27+
28+
"""
29+
+(a::Performance,b::Performance)
30+
31+
Add two performance points, and average them.
32+
"""
33+
function +(a::Performance,b::Performance)
34+
# start with using a's
35+
new_error_probabilities = a.error_probabilities
36+
37+
# if error probs are empty on a, use b. if b is also empty, dosen't matter, both are empty, use a.
38+
if isempty(a.error_probabilities)
39+
new_error_probabilities = b.error_probabilities
40+
elseif !isempty(b.error_probabilities)
41+
# make sure error probs are same shape
42+
if length(a.error_probabilities) != length(b.error_probabilities)
43+
@warn "Error probabilities mismatch, defaulting to new performance"
44+
# differing error_prob lengths should have been dealt with. This warning is only reached if there is a bug in the circuit performance calculator
45+
return b
46+
else
47+
# both error probs are nonempty, and same length. Sum and divide them
48+
for i in 1:length(a.error_probabilities)
49+
new_error_probabilities[i] = (a.error_probabilities[i] + b.error_probabilities[i]) / 2
50+
end
51+
end
52+
end
53+
54+
# Now average the rest of the attributes (floats)
55+
new_purified_pairs_fidelity = (a.purified_pairs_fidelity + b.purified_pairs_fidelity) / 2
56+
new_logical_qubit_fidelity = (a.logical_qubit_fidelity + b.logical_qubit_fidelity) / 2
57+
new_average_marginal_fidelity = (a.average_marginal_fidelity + b.average_marginal_fidelity) / 2
58+
new_success_probability = (a.success_probability + b.success_probability) / 2
59+
60+
# and mark that new calcs were added
61+
new_num_calcs = a.num_calcs + b.num_calcs
62+
return Performance(new_error_probabilities,new_purified_pairs_fidelity ,new_logical_qubit_fidelity,new_average_marginal_fidelity,new_success_probability,new_num_calcs)
63+
end
2164

22-
Base.copy(p::Performance) = Performance(copy(p.error_probabilities), p.purified_pairs_fidelity, p.logical_qubit_fidelity, p.average_marginal_fidelity, p.success_probability)
65+
"""
66+
==(a::Performance, b::Performance)
2367
68+
Compare equality of all internal values of a performance stuct. Used for testing.
69+
"""
70+
==(a::Performance, b::Performance) = (a.error_probabilities == b.error_probabilities &&
71+
a.purified_pairs_fidelity == b.purified_pairs_fidelity &&
72+
a.logical_qubit_fidelity == b.logical_qubit_fidelity &&
73+
a.average_marginal_fidelity == b.average_marginal_fidelity &&
74+
a.success_probability == b.success_probability &&
75+
a.num_calcs == b.num_calcs)
2476

2577
"An individual (circuit) in the population we are evolving"
2678
mutable struct Individual

test/test_types.jl

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using TestItems
2+
3+
@testitem "performance averaging" begin
4+
using QEPOptimize
5+
using QEPOptimize:Performance
6+
7+
low = Performance([0,0,1],0,0,0,0,1)
8+
high = Performance([1,0,0],1,1,1,1,1)
9+
mid = low + high
10+
@test mid == Performance([0.5,0,0.5],0.5,0.5,0.5,0.5,2)
11+
12+
# works with +=
13+
low = Performance([0,0,1],0,0,0,0,1)
14+
high = Performance([1,0,0],1,1,1,1,1)
15+
16+
low += high
17+
@test low == mid
18+
end
19+
20+
@testitem "Performance averages deal with low sim count/circuit failures" begin
21+
using QEPOptimize
22+
using QEPOptimize: initialize_pop!, step!, NetworkFidelity
23+
using BPGates: PauliNoise
24+
25+
config = (;
26+
num_simulations=5, # needs to be large enough to resolve circuit noise but you can make smaller too # TODO anneal this and save old results
27+
number_registers=4, # do 3 for something faster
28+
purified_pairs=1,
29+
code_distance=1,
30+
pop_size = 20,
31+
noises=[NetworkFidelity(0.5), PauliNoise(0.01/3, 0.01/3, 0.01/3)],
32+
)
33+
34+
init_config = (;
35+
start_ops = 10,
36+
start_pop_size = 1000,
37+
config...
38+
)
39+
40+
step_config = (;
41+
max_ops = 15, # do 5 for something faster
42+
new_mutants = 10,
43+
p_drop = 0.1,
44+
p_mutate = 0.1,
45+
p_gain = 0.1,
46+
config...
47+
)
48+
49+
pop = Population()
50+
51+
initialize_pop!(pop; init_config...)
52+
53+
_, fitness_history, transition_counts_matrix, transition_counts_keys = multiple_steps_with_history!(pop, 80; step_config...)
54+
55+
end
56+
57+
@testitem "performance averages deal with bad error_probs" begin
58+
using QEPOptimize
59+
using QEPOptimize:Performance
60+
61+
# defaults to the second performance if error probs are different
62+
normal = Performance([0,0,1],0,0,0,0,1)
63+
new_err_probs = Performance([1,0,0,0],1,1,1,1,1)
64+
result = normal + new_err_probs
65+
@test result == Performance([1,0,0,0],1,1,1,1,1)
66+
67+
# defaults to the second err_probs if no error probs, still averages
68+
normal = Performance([],0,0,0,0,1)
69+
new_err_probs = Performance([1,0,0,0],1,1,1,1,1)
70+
result = normal + new_err_probs
71+
@test result == Performance([1,0,0,0],0.5,0.5,.5,.5,2)
72+
73+
end

0 commit comments

Comments
 (0)