diff --git a/CHANGELOG.md b/CHANGELOG.md index be3b3cf3..fb0f502d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,18 @@ # News -## v0.5.0 - 2024-08-11 -- `observable` now takes a default value as a kwarg, i.e., you need to make the substitution `observable(regs, obs, 0.0; time)` ↦ `observable(regs, obs; something=0.0, time)` +## v0.5.0 - 2024-09-05 + +- Develop `CutoffProt` to deal with deadlocks in a simulation +- Expand `SwapperProt` with `agelimit` to permit cutoff policies (with `CutoffProt`) +- Tutorial and interactive examples for entanglement distribution on a grid with local-only knowledge +- **(breaking)** `observable` now takes a default value as a kwarg, i.e., you need to make the substitution `observable(regs, obs, 0.0; time)` ↦ `observable(regs, obs; something=0.0, time)` - Bump QuantumSymbolics and QuantumOpticsBase compat bound and bump julia compat to 1.10. +## v0.4.2 - 2024-08-13 + +- Incorrect breaking release. It should have been 0.5 (see above). + ## v0.4.1 - 2024-06-05 - Significant improvements to the performance of `query`. diff --git a/Project.toml b/Project.toml index b9900ff9..c0217394 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "QuantumSavory" uuid = "2de2e421-972c-4cb5-a0c3-999c85908079" authors = ["Stefan Krastanov "] -version = "0.4.2" +version = "0.5" [deps] Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" diff --git a/docs/make.jl b/docs/make.jl index b2a54835..22ef6f8a 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -4,6 +4,7 @@ push!(LOAD_PATH,"../src/") using Documenter using DocumenterCitations using QuantumSavory +using QuantumSavory.ProtocolZoo # TODO is this the correct place to place this to ensure cross_references work DocMeta.setdocmeta!(QuantumSavory, :DocTestSetup, :(using QuantumSavory); recursive=true) diff --git a/docs/src/howto/repeatergrid/repeatergrid.md b/docs/src/howto/repeatergrid/repeatergrid.md new file mode 100644 index 00000000..5f96d907 --- /dev/null +++ b/docs/src/howto/repeatergrid/repeatergrid.md @@ -0,0 +1,173 @@ +# [Entanglement Generation On A Repeater Grid](@id Entanglement-Generation-On-A-Repeater-Grid) + +```@meta +DocTestSetup = quote + using QuantumSavory +end +``` + +This section provides a detailed walkthrough of how QuantumSavory.jl can be used to simulate entanglement generation on a network of repeaters where each repeater relies only on local knowledge of the network. +This is only one of many ways in which such a network can be set up, focusing on one particular "no global knowledge" approach. + +A complete implementation of the simulation described here is available in the `examples/repeatergrid` folder of the `QuantumSavory` repository. + +For this example, we consider a square grid topology in which each node is connected to its nearest neighbors. +The registers act as repeater nodes. The nodes on the diagonal corners are Alice and Bob, the two special nodes that the network is trying to entangle through generating link-level entanglement at each edge and performing appropriate swaps at each node. + +The goal is to establish entanglement between Alice and Bob by routing entanglement through any of the possible paths (horizontal or vertical) formed by local entanglement links and then swapping those links by performing entanglement swaps. + +This employs functionality from the `ProtocolZoo` module of QuantumSavory to run the following Quantum Networking protocols: + +- [`EntanglerProt`](@ref): Entangler protocol to produce link-level entanglement at each edge in the network + +- [`SwapperProt`](@ref): Swapper protocol runs at each node except at the Alice and Bob nodes, to perform swaps. The swaps are performed only if a query deems them useful for propagating entanglement closer and closer to Alice and Bob. + +- [`EntanglementTracker`](@ref): Entanglement Tracker protocol to keep track of and update the local link state (classical knowledge) by listening for "entanglement update" messages generated by the other protocols (`SwapperProt` & `CutoffProt` specifically). + +- [`CutoffProt`](@ref): As the simulation progresses, the unused entangled pairs generated by `EntanglerProt` need to be discarded due to the loss of fidelity under noise as they might not be suitable for networking applications beyond a certain cutoff interval of time from their instantiation. The `CutoffProt` is instantiated with a `retention_time` parameter that discards such qubits in each node. + +- [`EntanglementConsumer`](@ref): This protocol runs between the end nodes and consumes the final entanglement generated as a result of all of the above protocols, which is supposed to represent the qubits being consumed in a networking application. + +All of the above protocols rely on the query and tagging functionality as described in the [Tagging and Querying](@ref tagging-and-querying) section. + +Other than that, `ConcurrentSim` and `ResumableFunctions` are used in the backend to run the discrete event simulation. `Graphs` helps with some functionality needed for the `RegisterNet` datastructure that forms the grid. `GLMakie` and `NetworkLayout` are used for visualization along with the visualization functionality implemented in `QuantumSavory` itself. + +# Custom Predicate And Choosing function + +```julia +function check_nodes(net, c_node, node; low=true) + n = Int(sqrt(size(net.graph)[1])) # grid size + c_x = c_node%n == 0 ? c_node ÷ n : (c_node ÷ n) + 1 + c_y = c_node - n*(c_x-1) + x = node%n == 0 ? node ÷ n : (node ÷ n) + 1 + y = node - n*(x-1) + return low ? (c_x - x) >= 0 && (c_y - y) >= 0 : (c_x - x) <= 0 && (c_y - y) <= 0 +end +``` + +The Swapper Protocol is initialized with a custom predicate function which is then placed in a call to `queryall` inside the Swapper to pick the nodes that are suitable to perform a swap with. The criteria for "suitability" is described below. + +This predicate function encodes most of the "logic" a local node will be performing. + +The custom predicate function shown above is parametrized with `net` and `c_node` along with the keyword argument `low`, when initializing the Swapper Protocol. This predicate function `Int->Bool` selects the target remote nodes for which a swap is appropriate. The arguments are: + +- `net`: The network of register nodes representing the graph structure, an instance of `RegisterNet`. + +- `c_node`: The node in which the Swapper protocol would be running. + +- `node`: As the [`queryall`](@ref) function goes through all the nodes linked with the current node, the custom predicate filters them depending on whether the node is suitable for a swap or not. + +- `low`: The nodes in the grid are numbered as consecutive integers starting from 1. If the Swapper is running at some node n, we want a link closest to Alice and another closest to Bob to perform a swap. We communicate whether we are looking for nodes of the first kind or the latter with the `low` keyword. + +Out of all the links at some node, the suitable ones are picked by computing the difference between the coordinates of the current node with the coordinates of the candidate node. A `low` node should have both of the `x` and `y` coordinate difference positive and vice versa for a non-`low` node. + +As the Swapper gets a list of suitable candidates for a swap in each direction, the one with the furthest distance from the current node is chosen by summing the x distance and y-distance. + +```julia +function choose_node(net, node, arr; low=true) + grid_size = Int(sqrt(size(net.graph)[1])) + return low ? argmax((distance.(grid_size, node, arr))) : argmin((distance.(grid_size, node, arr))) +end + +function distance(n, a, b) + x1 = a%n == 0 ? a ÷ n : (a ÷ n) + 1 + x2 = b%n == 0 ? b ÷ n : (b ÷ n) + 1 + y1 = a - n*(x1-1) + y2 = b - n*(x2-1) + + return x1 - x2 + y1 - y2 +end +``` + +# Simulation and Visualization + +```julia +n = 6 # the size of the square grid network (n × n) +regsize = 8 # the size of the quantum registers at each node + +graph = grid([n,n]) + +net = RegisterNet(graph, [Register(regsize, fill(5.0, regsize)) for i in 1:n^2]) + +sim = get_time_tracker(net) + +# each edge is capable of generating raw link-level entanglement +for (;src, dst) in edges(net) + eprot = EntanglerProt(sim, net, src, dst; rounds=-1, randomize=true) + @process eprot() +end + +# each node except the corners on one of the diagonals is capable of swapping entanglement +for i in 2:(n^2 - 1) + l(x) = check_nodes(net, i, x) + h(x) = check_nodes(net, i, x; low=false) + cL(arr) = choose_node(net, i, arr) + cH(arr) = choose_node(net, i, arr; low=false) + swapper = SwapperProt(sim, net, i; nodeL = l, nodeH = h, chooseL = cL, chooseH = cH, rounds=-1) + @process swapper() +end + +for v in vertices(net) + tracker = EntanglementTracker(sim, net, v) + @process tracker() +end + +# Entanglement usage/consumption by the network end nodes + +consumer = EntanglementConsumer(sim, net, 1, n^2) +@process consumer() + +# decoherence protocol runs at each node to free up slots that haven't been used past the retention time +for v in vertices(net) + decprot = DecoherenceProt(sim, net, v) + @process decprot() +end +``` + +We set up the simulation to run with a 6x6 grid of nodes above. Here, each node has 8 qubit slots. +Each vertical and horizontal edge runs an entanglement generation protocol. +Each node in the network runs an entanglement tracker protocol and all of the nodes except the nodes that we're trying to connect, +i.e., Alice's and Bob's nodes which are at the diagonal ends of the grid run the swapper protocol. +The code that runs and visualizes this simulation is shown below + +```julia +fig = Figure(;size=(600, 600)) + +# the network part of the visualization +layout = SquareGrid(cols=:auto, dx=10.0, dy=-10.0)(graph) # provided by NetworkLayout, meant to simplify plotting of graphs in 2D +_, ax, _, obs = registernetplot_axis(fig[1:2,1], net;registercoords=layout) + +# the performance log part of the visualization +entlog = Observable(consumer.log) # Observables are used by Makie to update the visualization in real-time in an automated reactive way +ts = @lift [e[1] for e in $entlog] # TODO this needs a better interface, something less cluncky, maybe also a whole Makie recipe +tzzs = @lift [Point2f(e[1],e[2]) for e in $entlog] +txxs = @lift [Point2f(e[1],e[3]) for e in $entlog] +Δts = @lift length($ts)>1 ? $ts[2:end] .- $ts[1:end-1] : [0.0] +entlogaxis = Axis(fig[1,2], xlabel="Time", ylabel="Entanglement", title="Entanglement Successes") +ylims!(entlogaxis, (-1.04,1.04)) +stem!(entlogaxis, tzzs) +histaxis = Axis(fig[2,2], xlabel="ΔTime", title="Histogram of Time to Successes") +hist!(histaxis, Δts) + +display(fig) + +step_ts = range(0, 200, step=0.1) +record(fig, "grid_sim6x6hv.mp4", step_ts; framerate=10, visible=true) do t + run(sim, t) + notify.((obs,entlog)) + ylims!(entlogaxis, (-1.04,1.04)) + xlims!(entlogaxis, max(0,t-50), 1+t) + autolimits!(histaxis) +end + +``` + +# Result + +```@repl +include("../../../../examples/repeatergrid/1a_async_interactive_visualization.jl") # hide +``` + +```@raw html + +``` \ No newline at end of file diff --git a/docs/src/symbolics.md b/docs/src/symbolics.md index e35ff03a..2b13c67a 100644 --- a/docs/src/symbolics.md +++ b/docs/src/symbolics.md @@ -202,5 +202,4 @@ express(MixedState(X1)/2+SProjector(Z1)/2, CliffordRepr()) ``` !!! warning "Stabilizer state expressions" - The state written as $\frac{|Z₁⟩⊗|Z₁⟩+|Z₂⟩⊗|Z₂⟩}{√2}$ is a well known stabilizer state, namely a Bell state. However, automatically expressing it as a stabilizer is a prohibitively expensive computational operation in general. We do not perform that computation automatically. If you want to ensure that states you define can be automatically converted to tableaux for Clifford simulations, avoid using summation of kets. On the other hand, in all of our Clifford Monte-Carlo simulations, `⊗` is fully supported, as well as [`SProjector`](@ref), [`MixedState`](@ref), [`StabilizerState`](@ref), and summation of density matrices. diff --git a/docs/src/visualizations.md b/docs/src/visualizations.md index 74be69b1..3ca2289e 100644 --- a/docs/src/visualizations.md +++ b/docs/src/visualizations.md @@ -18,6 +18,7 @@ The [`registernetplot_axis`](@ref) function can be used to draw a given set of r ```@example vis using GLMakie +GLMakie.activate!() using QuantumSavory # create a network of qubit registers @@ -57,8 +58,12 @@ initialize!(network[1,1]) # hide initialize!(network[2,3], X₁) # hide initialize!((network[3,1],network[4,2]), X₁⊗Z₂) # hide apply!((network[2,3],network[3,1]), CNOT) # hide -fig = Figure(size=(400,400)) # hide -_, _, plt, obs = registernetplot_axis(fig[1,1],network) # hide +fig = Figure(size=(700,400)) # hide +_, ax, plt, obs = registernetplot_axis(fig[1,1],network) # hide +fig +``` + +```@example vis QuantumSavory.showmetadata(fig,ax,plt,1,1) fig ``` @@ -66,8 +71,8 @@ fig And here with some extra tag metadata. ```@example vis -tag!(net[2,3], :specialplace, 1, 2) -tag!(net[2,3], :otherdata, 3, 4) +tag!(network[2,3], :specialplace, 1, 2) +tag!(network[2,3], :otherdata, 3, 4) QuantumSavory.showmetadata(fig,ax,plt,2,3) fig ``` diff --git a/examples/congestionchain/1_visualization.jl b/examples/congestionchain/1_visualization.jl index ab58e4b6..c1df5d8d 100644 --- a/examples/congestionchain/1_visualization.jl +++ b/examples/congestionchain/1_visualization.jl @@ -45,13 +45,16 @@ scatter!(ax_fidZZ,ts,fidZZ,label="ZZ",color=(c2,0.1)) display(fig) step_ts = range(0, 1000, step=0.1) -for t in step_ts + +record(fig, "congestionchain.mp4", step_ts; framerate=50, visible=true) do t run(sim, t) ax.title = "t=$(t)" - notify(obs) - notify(ts) - autolimits!(ax_fidXX) - autolimits!(ax_fidZZ) + if length(ts[])>2 # to avoid failing autolimits on empty plots + notify(obs) + notify(ts) + autolimits!(ax_fidXX) + autolimits!(ax_fidZZ) + end end ## diff --git a/examples/repeatergrid/1a_async_interactive_visualization.jl b/examples/repeatergrid/1a_async_interactive_visualization.jl new file mode 100644 index 00000000..821382d7 --- /dev/null +++ b/examples/repeatergrid/1a_async_interactive_visualization.jl @@ -0,0 +1,79 @@ +using GLMakie +# TODO significant code duplication with the other examples + +include("setup.jl") + +sim, net, graph, consumer, params... = prepare_simulation() + +fig = Figure(;size=(1200, 1100)) + +# the network part of the visualization +layout = SquareGrid(cols=:auto, dx=30.0, dy=-30.0)(graph) # provided by NetworkLayout, meant to simplify plotting of graphs in 2D +_, ax, _, obs = registernetplot_axis(fig[1:2,1], net;registercoords=layout) + +# the performance log part of the visualization +entlog = Observable(consumer.log) # Observables are used by Makie to update the visualization in real-time in an automated reactive way +ts = @lift [e[1] for e in $entlog] # TODO this needs a better interface, something less cluncky, maybe also a whole Makie recipe +tzzs = @lift [Point2f(e[1],e[2]) for e in $entlog] +txxs = @lift [Point2f(e[1],e[3]) for e in $entlog] +Δts = @lift length($ts)>1 ? $ts[2:end] .- $ts[1:end-1] : [0.0] +entlogaxis = Axis(fig[1,2], xlabel="Time", ylabel="Entanglement", title="Entanglement Successes") +ylims!(entlogaxis, (-1.04,1.04)) +stem!(entlogaxis, txxs) +histaxis = Axis(fig[2,2], xlabel="ΔTime", title="Histogram of Time to Successes") +hist!(histaxis, Δts) + +avg_fids = @lift cumsum([e[3] for e in $entlog])./cumsum(ones(length($entlog))) #avg fidelity per unit time +fid_info = @lift [Point2f(t,f) for (t,f) in zip($ts, $avg_fids)] +fid_axis = Axis(fig[3,1], xlabel="Time", ylabel="Avg. Fidelity", title="Time evolution of Average Fidelity") +lines!(fid_axis, fid_info) + +num_epr = @lift cumsum(ones(length($entlog)))./($ts) #avg number of pairs per unit time +num_epr_info = @lift [Point2f(t,n) for (t,n) in zip($ts, $num_epr)] +num_epr_axis = Axis(fig[3,2], xlabel="Time", title="Avg. Number of Entangled Pairs between Alice and Bob") +lines!(num_epr_axis, num_epr_info) + + +# sliders +sg = SliderGrid( # TODO significant code duplication with the other examples + fig[4,:], + (label="Probability of success of Entanglement generation at each attempt", + range=0.001:0.05:1.0, format="{:.3f}", startvalue=0.001), + (label="Local busy time for swapper", + range=0.001:0.5:10.0, format="{:.3f}", startvalue=0.001), + (label="Wait time after failure to lock qubits for a swap", + range=0.1:0.05:1.0, format="{:.2f}", startvalue=0.1), + (label="Retention time for an unused qubit", + range=0.1:0.1:10.0, format="{:.2f}", startvalue=5.0), + (label="Time before a qubit's retention time runs out (for `agelimit`)", + range=0.1:0.5:10.0, format="{:.2f}", startvalue=0.5), + (label="Period of time between subsequent queries at the consumer", + range=0.001:0.05:1.0, format="{:.3f}", startvalue=0.001), + (label="Period of time between subsequent queries at the DecoherenceProtocol", + range=0.001:0.05:1.0, format="{:.3f}", startvalue=0.001), + + width = 600, + tellheight = false) + +for (param, slider) in zip(params, sg.sliders) + on(slider.value) do val + param[] = val + end +end + + +display(fig) + +step_ts = range(0, 50, step=0.1) +record(fig, "grid_sim6x6hv.mp4", step_ts; framerate=10, visible=true) do t + run(sim, t) + notify.((obs,entlog)) + notify.(params) + ylims!(entlogaxis, (-1.04,1.04)) + xlims!(entlogaxis, max(0,t-50), 1+t) + ylims!(fid_axis, (0, 1.04)) + xlims!(fid_axis, max(0, t-50), 1+t) + autolimits!(histaxis) + ylims!(num_epr_axis, (0, 4)) + xlims!(num_epr_axis, max(0, t-50), 1+t) +end diff --git a/examples/repeatergrid/1b_async_wglmakie_interactive.jl b/examples/repeatergrid/1b_async_wglmakie_interactive.jl new file mode 100644 index 00000000..bf991666 --- /dev/null +++ b/examples/repeatergrid/1b_async_wglmakie_interactive.jl @@ -0,0 +1,155 @@ +using WGLMakie +WGLMakie.activate!() +import Bonito +using Markdown +# TODO significant code duplication with the other examples + +include("setup.jl") + +const custom_css = Bonito.DOM.style("ul {list-style: circle !important;}") # TODO remove after fix of bug in JSServe https://github.com/SimonDanisch/JSServe.jl/issues/178 + +function prepare_singlerun() + # Prepare all of the simulation components (while all visualization components are prepared in the rest of this function) + sim, net, graph, consumer, params... = prepare_simulation() + + # Prepare the main figure + fig = Figure(;size=(1200, 1100)) + # the network part of the visualization + layout = SquareGrid(cols=:auto, dx=30.0, dy=-30.0)(graph) # provided by NetworkLayout, meant to simplify plotting of graphs in 2D + _, ax, _, obs = registernetplot_axis(fig[1:2,1], net;registercoords=layout) + + # the performance log part of the visualization + entlog = Observable(consumer.log) # Observables are used by Makie to update the visualization in real-time in an automated reactive way + ts = @lift [e[1] for e in $entlog] # TODO this needs a better interface, something less cluncky, maybe also a whole Makie recipe + tzzs = @lift [Point2f(e[1],e[2]) for e in $entlog] + txxs = @lift [Point2f(e[1],e[3]) for e in $entlog] + Δts = @lift length($ts)>1 ? $ts[2:end] .- $ts[1:end-1] : [0.0] + entlogaxis = Axis(fig[1,2], xlabel="Time", ylabel="Entanglement", title="Entanglement Successes") + ylims!(entlogaxis, (-1.04,1.04)) + stem!(entlogaxis, tzzs) + histaxis = Axis(fig[2,2], xlabel="ΔTime", title="Histogram of Time to Successes") + hist!(histaxis, Δts) + + avg_fids = @lift cumsum([e[3] for e in $entlog])./cumsum(ones(length($entlog))) #avg fidelity per unit time + fid_info = @lift [Point2f(t,f) for (t,f) in zip($ts, $avg_fids)] + fid_axis = Axis(fig[3,1], xlabel="Time", ylabel="Avg. Fidelity", title="Time evolution of Average Fidelity") + lines!(fid_axis, fid_info) + + num_epr = @lift cumsum(ones(length($entlog)))./($ts) #avg number of pairs per unit time + num_epr_info = @lift [Point2f(t,n) for (t,n) in zip($ts, $num_epr)] + num_epr_axis = Axis(fig[3,2], xlabel="Time", title="Avg. Number of Entangled Pairs between Alice and Bob") + lines!(num_epr_axis, num_epr_info) + + # sliders + sg = SliderGrid( # TODO significant code duplication with the other examples + fig[4,:], + (label="Probability of success of Entanglement generation at each attempt", + range=0.001:0.05:1.0, format="{:.3f}", startvalue=0.001), + (label="Local busy time for swapper", + range=0.001:0.5:10.0, format="{:.3f}", startvalue=0.001), + (label="Wait time after failure to lock qubits for a swap", + range=0.1:0.05:1.0, format="{:.2f}", startvalue=0.1), + (label="Retention time for an unused qubit", + range=0.1:0.1:10.0, format="{:.2f}", startvalue=5.0), + (label="Time before a qubit's retention time runs out (for `agelimit`)", + range=0.1:0.5:10.0, format="{:.2f}", startvalue=0.5), + (label="Period of time between subsequent queries at the consumer", + range=0.001:0.05:1.0, format="{:.3f}", startvalue=0.001), + (label="Period of time between subsequent queries at the DecoherenceProtocol", + range=0.001:0.05:1.0, format="{:.3f}", startvalue=0.001), + + width = 600, + tellheight = false) + + for (param, slider) in zip(params, sg.sliders) + on(slider.value) do val + param[] = val + end + end + + return sim, net, obs, entlog, entlogaxis, histaxis, fig, params +end + +# All the calls that happen in the main event loop of the simulation, +# encapsulated here so that we can conveniently pause the simulation from the WGLMakie app. +function continue_singlerun!(sim, obs, entlog, params, entlogaxis, histaxis, running) + step_ts = range(0, 1000, step=0.1) + for t in step_ts + run(sim, t) + notify.((obs,entlog)) + notify.(params) + ylims!(entlogaxis, (-1.04,1.04)) + xlims!(entlogaxis, max(0,t-50), 1+t) + ylims!(fid_axis, (0, 1.04)) + xlims!(fid_axis, max(0, t-50), 1+t) + autolimits!(histaxis) + ylims!(num_epr_axis, (0, 4)) + xlims!(num_epr_axis, max(0, t-50), 1+t) + end + running[] = nothing +end + +# +landing = Bonito.App() do + + sim, net, obs, entlog, entlogaxis, histaxis, fig, params = prepare_singlerun() + + running = Observable{Any}(false) + fig[4,1] = buttongrid = GridLayout(tellwidth = false) + buttongrid[1,1] = b = Makie.Button(fig, label = @lift(isnothing($running) ? "Done" : $running ? "Running..." : "Run once"), height=30, tellwidth=false) + + on(b.clicks) do _ + if !running[] + running[] = true + end + end + on(running) do r + if r + Threads.@spawn begin + continue_singlerun!( + sim, obs, entlog, params, entlogaxis, histaxis, running) + end + end + end + + + content = md""" + Pick simulation settings and hit run (see below for technical details). + + $(fig.scene) + + # Simulations of Entanglement Distribution in a Grid with Asynchronous Messaging + + The end nodes(Alice and Bob) are located on the diagonal corners of the grid. + Each node runs control protocols like entanglement tracking and handling of outdated entangled pairs through decoherence protocol. + Each horizontal and vertical edge between adjacent/neighboring nodes runs an entanglement generation protocol. + All nodes except the end nodes run the swapper protocol to establish entanglement between Alice and Bob by extending the raw link level entanglement between each pair of nodes + through a series of swaps until it reaches Alice and Bob. + At the end nodes we run an entanglement consumer protocol which consumes the epr pair between them and logs the fidelity of the final epr pair along with the time it took to generate it. + Both of these are presented in the top and bottom graphs on the right above respectively. + + All the classical information about the entanglement status of nodes after swaps or deletions(decoherence protocols) is communicated through + asynchronous messaging with the help of tags and queries and handled by the entanglement tracker. With this the swapper protocol(`SwapperProt`) + considers all the proposed candidates for a swap, relying on the messages sent by the decoherence protocol to the entanglement tracker to delete any qubits that might have taken part + in a swap, while their entangled pair got deleted due to decoherence. If this happens all the qubits involved in the swap need to be discarded by forwarding the deletion message to the + respective nodes. + + [See and modify the code for this simulation on github.](https://github.com/QuantumSavory/QuantumSavory.jl/tree/master/examples/repeatergrid/1b_async_wglmakie_interactive.jl) + """ + return Bonito.DOM.div(Bonito.MarkdownCSS, Bonito.Styling, custom_css, content) +end; + +# +# Serve the Makie app + +isdefined(Main, :server) && close(server); +port = parse(Int, get(ENV, "QS_ASYNC_REPEATERGRID_PORT", "8888")) +interface = get(ENV, "QS_ASYNC_REPEATERGRID_IP", "127.0.0.1") +proxy_url = get(ENV, "QS_ASYNC_REPEATERGRID_PROXY", "") +server = Bonito.Server(interface, port; proxy_url); +Bonito.HTTPServer.start(server) +Bonito.route!(server, "/" => landing); + +## + +wait(server) diff --git a/examples/repeatergrid/2a_sync_interactive_visualization.jl b/examples/repeatergrid/2a_sync_interactive_visualization.jl new file mode 100644 index 00000000..276335ae --- /dev/null +++ b/examples/repeatergrid/2a_sync_interactive_visualization.jl @@ -0,0 +1,78 @@ +using GLMakie +# TODO significant code duplication with the other examples + +include("setup.jl") + +sim, net, graph, consumer, params... = prepare_simulation(;announce=false) + +fig = Figure(;size=(1200, 1100)) + +# the network part of the visualization +layout = SquareGrid(cols=:auto, dx=30.0, dy=-30.0)(graph) # provided by NetworkLayout, meant to simplify plotting of graphs in 2D +_, ax, _, obs = registernetplot_axis(fig[1:2,1], net;registercoords=layout) + +# the performance log part of the visualization +entlog = Observable(consumer.log) # Observables are used by Makie to update the visualization in real-time in an automated reactive way +ts = @lift [e[1] for e in $entlog] # TODO this needs a better interface, something less cluncky, maybe also a whole Makie recipe +tzzs = @lift [Point2f(e[1],e[2]) for e in $entlog] +txxs = @lift [Point2f(e[1],e[3]) for e in $entlog] +Δts = @lift length($ts)>1 ? $ts[2:end] .- $ts[1:end-1] : [0.0] +entlogaxis = Axis(fig[1,2], xlabel="Time", ylabel="Entanglement", title="Entanglement Successes") +ylims!(entlogaxis, (-1.04,1.04)) +stem!(entlogaxis, tzzs) +histaxis = Axis(fig[2,2], xlabel="ΔTime", title="Histogram of Time to Successes") +hist!(histaxis, Δts) + +avg_fids = @lift cumsum([e[3] for e in $entlog])./cumsum(ones(length($entlog))) #avg fidelity per unit time +fid_info = @lift [Point2f(t,f) for (t,f) in zip($ts, $avg_fids)] +fid_axis = Axis(fig[3,1], xlabel="Time", ylabel="Avg. Fidelity", title="Time evolution of Average Fidelity") +lines!(fid_axis, fid_info) + +num_epr = @lift cumsum(ones(length($entlog)))./($ts) #avg number of pairs per unit time +num_epr_info = @lift [Point2f(t,n) for (t,n) in zip($ts, $num_epr)] +num_epr_axis = Axis(fig[3,2], xlabel="Time", title="Avg. Number of Entangled Pairs between Alice and Bob") +lines!(num_epr_axis, num_epr_info) + +# sliders +sg = SliderGrid( # TODO significant code duplication with the other examples + fig[4,:], + (label="Probability of success of Entanglement generation at each attempt", + range=0.001:0.05:1.0, format="{:.3f}", startvalue=0.001), + (label="Local busy time for swapper", + range=0.001:0.5:10.0, format="{:.3f}", startvalue=0.001), + (label="Wait time after failure to lock qubits for a swap", + range=0.1:0.05:1.0, format="{:.2f}", startvalue=0.1), + (label="Retention time for an unused qubit", + range=0.1:0.1:10.0, format="{:.2f}", startvalue=5.0), + (label="Time before a qubit's retention time runs out (for `agelimit`)", + range=0.1:0.5:10.0, format="{:.2f}", startvalue=0.5), + (label="Period of time between subsequent queries at the consumer", + range=0.001:0.05:1.0, format="{:.3f}", startvalue=0.001), + (label="Period of time between subsequent queries at the DecoherenceProtocol", + range=0.001:0.05:1.0, format="{:.3f}", startvalue=0.001), + + width = 600, + tellheight = false) + +for (param, slider) in zip(params, sg.sliders) + on(slider.value) do val + param[] = val + end +end + + +display(fig) + +step_ts = range(0, 50, step=0.1) +record(fig, "grid_sim6x6hv.mp4", step_ts; framerate=10, visible=true) do t + run(sim, t) + notify.((obs,entlog)) + notify.(params) + ylims!(entlogaxis, (-1.04,1.04)) + xlims!(entlogaxis, max(0,t-50), 1+t) + ylims!(fid_axis, (0, 1.04)) + xlims!(fid_axis, max(0, t-50), 1+t) + autolimits!(histaxis) + ylims!(num_epr_axis, (0, 4)) + xlims!(num_epr_axis, max(0, t-50), 1+t) +end diff --git a/examples/repeatergrid/2b_sync_wglmakie_interactive.jl b/examples/repeatergrid/2b_sync_wglmakie_interactive.jl new file mode 100644 index 00000000..f79ef371 --- /dev/null +++ b/examples/repeatergrid/2b_sync_wglmakie_interactive.jl @@ -0,0 +1,154 @@ +using WGLMakie +WGLMakie.activate!() +import Bonito +using Markdown +# TODO significant code duplication with the other examples + +include("setup.jl") + +const custom_css = Bonito.DOM.style("ul {list-style: circle !important;}") # TODO remove after fix of bug in JSServe https://github.com/SimonDanisch/JSServe.jl/issues/178 + +function prepare_singlerun() + # Prepare all of the simulation components (while all visualization components are prepared in the rest of this function) + sim, net, graph, consumer, params... = prepare_simulation(;announce=false) + + # Prepare the main figure + fig = Figure(;size=(1200, 1100)) + # the network part of the visualization + layout = SquareGrid(cols=:auto, dx=30.0, dy=-30.0)(graph) # provided by NetworkLayout, meant to simplify plotting of graphs in 2D + _, ax, _, obs = registernetplot_axis(fig[1:2,1], net;registercoords=layout) + + # the performance log part of the visualization + entlog = Observable(consumer.log) # Observables are used by Makie to update the visualization in real-time in an automated reactive way + ts = @lift [e[1] for e in $entlog] # TODO this needs a better interface, something less cluncky, maybe also a whole Makie recipe + tzzs = @lift [Point2f(e[1],e[2]) for e in $entlog] + txxs = @lift [Point2f(e[1],e[3]) for e in $entlog] + Δts = @lift length($ts)>1 ? $ts[2:end] .- $ts[1:end-1] : [0.0] + entlogaxis = Axis(fig[1,2], xlabel="Time", ylabel="Entanglement", title="Entanglement Successes") + ylims!(entlogaxis, (-1.04,1.04)) + stem!(entlogaxis, tzzs) + histaxis = Axis(fig[2,2], xlabel="ΔTime", title="Histogram of Time to Successes") + hist!(histaxis, Δts) + + avg_fids = @lift cumsum([e[3] for e in $entlog])./cumsum(ones(length($entlog))) #avg fidelity per unit time + fid_info = @lift [Point2f(t,f) for (t,f) in zip($ts, $avg_fids)] + fid_axis = Axis(fig[3,1], xlabel="Time", ylabel="Avg. Fidelity", title="Time evolution of Average Fidelity") + lines!(fid_axis, fid_info) + + num_epr = @lift cumsum(ones(length($entlog)))./($ts) #avg number of pairs per unit time + num_epr_info = @lift [Point2f(t,n) for (t,n) in zip($ts, $num_epr)] + num_epr_axis = Axis(fig[3,2], xlabel="Time", title="Avg. Number of Entangled Pairs between Alice and Bob") + lines!(num_epr_axis, num_epr_info) + + # sliders + sg = SliderGrid( + fig[4,:], + (label="Probability of success of Entanglement generation at each attempt", + range=0.001:0.05:1.0, format="{:.3f}", startvalue=0.001), + (label="Local busy time for swapper", + range=0.001:0.5:10.0, format="{:.3f}", startvalue=0.001), + (label="Wait time after failure to lock qubits for a swap", + range=0.1:0.05:1.0, format="{:.2f}", startvalue=0.1), + (label="Retention time for an unused qubit", + range=0.1:0.1:10.0, format="{:.2f}", startvalue=5.0), + (label="Time before a qubit's retention time runs out (for `agelimit`)", + range=0.1:0.5:10.0, format="{:.2f}", startvalue=0.5), + (label="Period of time between subsequent queries at the consumer", + range=0.001:0.05:1.0, format="{:.3f}", startvalue=0.001), + (label="Period of time between subsequent queries at the DecoherenceProtocol", + range=0.001:0.05:1.0, format="{:.3f}", startvalue=0.001), + + width = 600, + tellheight = false) + + for (param, slider) in zip(params, sg.sliders) + on(slider.value) do val + param[] = val + end + end + + return sim, net, obs, entlog, entlogaxis, histaxis, fig, params +end + +# All the calls that happen in the main event loop of the simulation, +# encapsulated here so that we can conveniently pause the simulation from the WGLMakie app. +function continue_singlerun!(sim, obs, entlog, params, entlogaxis, histaxis, running) + step_ts = range(0, 1000, step=0.1) + for t in step_ts + run(sim, t) + notify.((obs,entlog)) + notify.(params) + ylims!(entlogaxis, (-1.04,1.04)) + xlims!(entlogaxis, max(0,t-50), 1+t) + ylims!(fid_axis, (0, 1.04)) + xlims!(fid_axis, max(0, t-50), 1+t) + autolimits!(histaxis) + ylims!(num_epr_axis, (0, 4)) + xlims!(num_epr_axis, max(0, t-50), 1+t) + end + running[] = nothing +end + +# +landing = Bonito.App() do + + sim, net, obs, entlog, entlogaxis, histaxis, fig, params = prepare_singlerun() + + running = Observable{Any}(false) + fig[4,1] = buttongrid = GridLayout(tellwidth = false) + buttongrid[1,1] = b = Makie.Button(fig, label = @lift(isnothing($running) ? "Done" : $running ? "Running..." : "Run once"), height=30, tellwidth=false) + + on(b.clicks) do _ + if !running[] + running[] = true + end + end + on(running) do r + if r + Threads.@spawn begin + continue_singlerun!( + sim, obs, entlog, params, entlogaxis, histaxis, running) + end + end + end + + + content = md""" + Pick simulation settings and hit run (see below for technical details). + + $(fig.scene) + + # Simulations of Entanglement Distribution in a Grid with Synchronization + + The end nodes(Alice and Bob) are located on the diagonal corners of the grid. + Each node runs control protocols like entanglement tracking and handling of outdated entangled pairs through decoherence protocol. + Each horizontal and vertical edge between adjacent/neighboring nodes runs an entanglement generation protocol. + All nodes except the end nodes run the swapper protocol to establish entanglement between Alice and Bob by extending the raw link level entanglement between each pair of nodes + through a series of swaps until it reaches Alice and Bob. + At the end nodes we run an entanglement consumer protocol which consumes the epr pair between them and logs the fidelity of the final epr pair along with the time it took to generate it. + Both of these are presented in the top and bottom graphs on the right above respectively. + + All the classical information about the entanglement status of nodes after swaps is communicated through + asynchronous messaging with the help of tags and queries and handled by the entanglement tracker. The decoherence protocol doesn't generate + any messages here. Hence, the swapper protocol (`SwapperProt` with `agelimit`) checks every proposed candidate to be coherent before its used. Thus the swapper and decoherence protocol + interact in a synchronous manner here. + + [See and modify the code for this simulation on github.](https://github.com/QuantumSavory/QuantumSavory.jl/tree/master/examples/repeatergrid/2b_sync_wglmakie_interactive.jl) + """ + return Bonito.DOM.div(Bonito.MarkdownCSS, Bonito.Styling, custom_css, content) +end; + +# +# Serve the Makie app + +isdefined(Main, :server) && close(server); +port = parse(Int, get(ENV, "QS_SYNC_REPEATERGRID_PORT", "8888")) +interface = get(ENV, "QS_SYNC_REPEATERGRID_IP", "127.0.0.1") +proxy_url = get(ENV, "QS_SYNC_REPEATERGRID_PROXY", "") +server = Bonito.Server(interface, port; proxy_url); +Bonito.HTTPServer.start(server) +Bonito.route!(server, "/" => landing); + +## + +wait(server) diff --git a/examples/repeatergrid/Project.toml b/examples/repeatergrid/Project.toml new file mode 100644 index 00000000..2eb8c024 --- /dev/null +++ b/examples/repeatergrid/Project.toml @@ -0,0 +1,13 @@ +[deps] +Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8" +ConcurrentSim = "6ed1e86c-fcaf-46a9-97e0-2b26a2cdb499" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +NetworkLayout = "46757867-2c16-5918-afeb-47bfcb05e46a" +QuantumSavory = "2de2e421-972c-4cb5-a0c3-999c85908079" +ResumableFunctions = "c5292f4c-5179-55e1-98c5-05642aab7184" +Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" +WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" + +[compat] +Bonito = "3.1" \ No newline at end of file diff --git a/examples/repeatergrid/README.md b/examples/repeatergrid/README.md new file mode 100644 index 00000000..3ff8f902 --- /dev/null +++ b/examples/repeatergrid/README.md @@ -0,0 +1,19 @@ +# Large Grid Network with Classical Synchronization of Messages and only "Local Knowledge" Control + +Our goal is to simply entangle two specific clients on a grid network. +All nodes of the network are capable of running nearest-neighbor entanglement generation, swaps, potentially cutoff-time deletion of old qubits, all of the classical communication machinery to distribute the necessary metadata among neighbors. + +This is very much a simple **local knowledge and NO global controller** setup for network control. + +This example visualizes a quantum network attempting to distribute between the Alice and Bob user pair located on the diagonal of a grid topology. +Asynchronous messaging and queries are used for classical communication of entanglement information between nodes using protocols like [`EntanglementTracker`](@ref) and [`SwapperProt`](@ref). +Link-level-entanglement is generated between all the horizontal and vertical neighbors using an entanglement generation protocol called [`EntanglerProt`](@ref). +As an entanglement link is established between the end users, it is consumed by a protocol named [`EntanglementConsumer`](@ref) which records the fidelity and time of consumption of the pair. +The qubits that remain unused beyond their `retention_time` are discarded by the [`CutoffProt`](@ref) + + +This module provides two ways of running the simulation: + +- [`SwapperProt`](@ref) and [`CutoffProt`](@ref) in an asynchronous manner where they run independently and all the classical information about the quantum states is reconciled using asynchronous messages sent to the tracker. + +- [`SwapperProt`](@ref) does not use qubits that are too old (and independently expected to be thrown away by the [`CutoffProt`](@ref)), by checking their `agelimit` parameter passed to it during initialization. Here, there are no outgoing messages from the [`CutoffProt`](@ref). \ No newline at end of file diff --git a/examples/repeatergrid/setup.jl b/examples/repeatergrid/setup.jl new file mode 100644 index 00000000..fd083b0d --- /dev/null +++ b/examples/repeatergrid/setup.jl @@ -0,0 +1,98 @@ +using QuantumSavory +using QuantumSavory.ProtocolZoo +using Graphs +using ConcurrentSim +using ResumableFunctions +using NetworkLayout + +"""Predicate function for swap decisions + +Checks if a remote node is in the appropriate quadrant with respect to the local node.""" +function check_nodes(net, c_node, node; low=true) + n = Int(sqrt(size(net.graph)[1])) # grid size + c_x = c_node%n == 0 ? c_node ÷ n : (c_node ÷ n) + 1 + c_y = c_node - n*(c_x-1) + x = node%n == 0 ? node ÷ n : (node ÷ n) + 1 + y = node - n*(x-1) + return low ? (c_x - x) >= 0 && (c_y - y) >= 0 : (c_x - x) <= 0 && (c_y - y) <= 0 +end + +"""Choosing function to pick from swap candidates + +Chooses the node in the appropriate quadrant that is furthest from the local node.""" +function choose_node(net, node, arr; low=true) + grid_size = Int(sqrt(size(net.graph)[1])) + return low ? argmax((distance.(grid_size, node, arr))) : argmin((distance.(grid_size, node, arr))) +end + +"""A "cost" function for choosing the furthest node in the appropriate quadrant.""" +function distance(n, a, b) + x1 = a%n == 0 ? a ÷ n : (a ÷ n) + 1 + x2 = b%n == 0 ? b ÷ n : (b ÷ n) + 1 + y1 = a - n*(x1-1) + y2 = b - n*(x2-1) + return x1 - x2 + y1 - y2 +end + +# Simulation setup + +function prepare_simulation(;announce=true) + n = 6 # number of nodes on each row and column for a 6x6 grid + regsize = 20 # memory slots in each node + + # The graph of network connectivity + graph = grid([n,n]) + + net = RegisterNet(graph, [Register(regsize, T1Decay(10.0)) for i in 1:n^2]) + sim = get_time_tracker(net) + + ##Setup the networking protocols running between each of the nodes + + # Entanglement generation + succ_prob = Observable(0.001) + for (;src, dst) in edges(net) + eprot = EntanglerProt(sim, net, src, dst; rounds=-1, randomize=true, success_prob=succ_prob[]) + @process eprot() + end + + # Swapper + local_busy_time = Observable(0.0) + retry_lock_time = Observable(0.1) + retention_time = Observable(5.0) + buffer_time = Observable(0.5) + + for i in 2:(n^2 - 1) + l(x) = check_nodes(net, i, x) + h(x) = check_nodes(net, i, x; low=false) + cL(arr) = choose_node(net, i, arr) + cH(arr) = choose_node(net, i, arr; low=false) + swapper = SwapperProt( + sim, net, i; + nodeL = l, nodeH = h, + chooseL = cL, chooseH = cH, + rounds=-1, local_busy_time=local_busy_time[], + retry_lock_time=retry_lock_time[], + agelimit=announce ? nothing : retention_time[]-buffer_time[]) + @process swapper() + end + + # Entanglement Tracking + for v in vertices(net) + tracker = EntanglementTracker(sim, net, v) + @process tracker() + end + + # Entanglement usage/consumption by the network end nodes + period_cons = Observable(0.1) + consumer = EntanglementConsumer(sim, net, 1, n^2; period=period_cons[]) + @process consumer() + + # decoherence protocol runs at each node to free up slots that haven't been used past the retention time + period_dec = Observable(0.1) + for v in vertices(net) + decprot = CutoffProt(sim, net, v; announce, period=period_dec[]) # TODO default and slider for retention_time + @process decprot() + end + + return sim, net, graph, consumer, succ_prob, local_busy_time, retry_lock_time, retention_time, buffer_time, period_cons, period_dec +end diff --git a/ext/QuantumSavoryMakie/QuantumSavoryMakie.jl b/ext/QuantumSavoryMakie/QuantumSavoryMakie.jl index 2aa65390..799eca76 100644 --- a/ext/QuantumSavoryMakie/QuantumSavoryMakie.jl +++ b/ext/QuantumSavoryMakie/QuantumSavoryMakie.jl @@ -240,7 +240,7 @@ end function get_state_vis_string(backrefs, i) state, register, registeridx, slot, subsystem = backrefs[i] - tags = register.tags[slot] + tags = [ti.tag for ti in values(register.tag_info) if ti.slot==slot] tags_str = if isempty(tags) "not tagged" else @@ -264,15 +264,15 @@ function Makie.process_interaction(handler::RNHandler, event::Makie.MouseEvent, #else if plot===rn[:register_slots_scatterplot][] register, registeridx, slot = rn[:register_slots_coords_backref][][index] - run(`clear`) + try run(`clear`) catch end println("Register $registeridx | Slot $(slot)\n Details: $(register)") elseif plot===rn[:state_scatterplot][] state, reg, registeridx, slot, subsystem = rn[:state_coords_backref][][index] - run(`clear`) + try run(`clear`) catch end println("Subsystem stored in Register $(registeridx) | Slot $(slot)\n Subsystem $(subsystem) of $(state)") elseif plot===rn[:observables_scatterplot][] o, val = rn[:observables_backref][][index] - run(`clear`) + try run(`clear`) catch end println("Observable $(o) has value $(val)") end false diff --git a/src/ProtocolZoo/ProtocolZoo.jl b/src/ProtocolZoo/ProtocolZoo.jl index 8f2f37ab..c661e511 100644 --- a/src/ProtocolZoo/ProtocolZoo.jl +++ b/src/ProtocolZoo/ProtocolZoo.jl @@ -1,7 +1,7 @@ module ProtocolZoo using QuantumSavory -import QuantumSavory: get_time_tracker, Tag +import QuantumSavory: get_time_tracker, Tag, isolderthan using QuantumSavory: Wildcard using QuantumSavory.CircuitZoo: EntanglementSwap, LocalEntanglementSwap @@ -16,7 +16,7 @@ import SumTypes export # protocols - EntanglerProt, SwapperProt, EntanglementTracker, EntanglementConsumer, + EntanglerProt, SwapperProt, EntanglementTracker, EntanglementConsumer, CutoffProt, # tags EntanglementCounterpart, EntanglementHistory, EntanglementUpdateX, EntanglementUpdateZ, # from Switches @@ -124,6 +124,30 @@ Tag(tag::EntanglementUpdateZ) = Tag(EntanglementUpdateZ, tag.past_local_node, ta """ $TYPEDEF +This tag arrives as a message from a remote node's Cutoff Protocol to which the current node was entangled, +to update the classical metadata of the entangled slot and empty it. +It is also stored at a node to handle incoming `EntanglementUpdate` and `EntanglementDelete` messages. + +$TYPEDFIELDS + +See also: [`CutoffProt`](@ref) +""" +@kwdef struct EntanglementDelete + "the node that sent the deletion announcement message after they delete their local qubit" + send_node::Int + "the sender's slot containing the decohered qubit" + send_slot::Int + "the node receiving the message for qubit deletion" + rec_node::Int + "the slot containing decohered qubit" + rec_slot::Int +end +Base.show(io::IO, tag::EntanglementDelete) = print(io, "Deleted $(tag.send_node).$(tag.send_slot) which was entangled to $(tag.rec_node).$(tag.rec_slot)") +Tag(tag::EntanglementDelete) = Tag(EntanglementDelete, tag.send_node, tag.send_slot, tag.rec_node, tag.rec_slot) + +""" +$TYPEDEF + A protocol that generates entanglement between two nodes. Whenever a pair of empty slots is available, the protocol locks them and starts probabilistic attempts to establish entanglement. @@ -220,102 +244,6 @@ end end end -function random_index(arr) - return rand(keys(arr)) -end - -""" -$TYPEDEF - -A protocol, running at a given node, that finds swappable entangled pairs and performs the swap. - -$TYPEDFIELDS -""" -@kwdef struct SwapperProt{NL,NH,CL,CH,LT} <: AbstractProtocol where {NL<:Union{Int,<:Function,Wildcard}, NH<:Union{Int,<:Function,Wildcard}, CL<:Function, CH<:Function, LT<:Union{Float64,Nothing}} - """time-and-schedule-tracking instance from `ConcurrentSim`""" - sim::Simulation - """a network graph of registers""" - net::RegisterNet - """the vertex of the node where swapping is happening""" - node::Int - """the vertex of one of the remote nodes for the swap, arbitrarily referred to as the "low" node (or a predicate function or a wildcard); if you are working on a repeater chain, a good choice is `<(current_node)`, i.e. any node to the "left" of the current node""" - nodeL::NL = ❓ - """the vertex of the other remote node for the swap, the "high" counterpart of `nodeL`; if you are working on a repeater chain, a good choice is `>(current_node)`, i.e. any node to the "right" of the current node""" - nodeH::NH = ❓ - """the `nodeL` predicate can return many positive candidates; `chooseL` picks one of them (by index into the array of filtered `nodeL` results), defaults to a random pick `arr->rand(keys(arr))`; if you are working on a repeater chain a good choice is `argmin`, i.e. the node furthest to the "left" """ - chooseL::CL = random_index - """the `nodeH` counterpart for `chooseH`; if you are working on a repeater chain a good choice is `argmax`, i.e. the node furthest to the "right" """ - chooseH::CH = random_index - """fixed "busy time" duration immediately before starting entanglement generation attempts""" - local_busy_time::Float64 = 0.0 # TODO the gates should have that busy time built in - """how long to wait before retrying to lock qubits if no qubits are available (`nothing` for queuing up and waiting)""" - retry_lock_time::LT = 0.1 - """how many rounds of this protocol to run (`-1` for infinite))""" - rounds::Int = -1 -end - -#TODO "convenience constructor for the missing things and finish this docstring" -function SwapperProt(sim::Simulation, net::RegisterNet, node::Int; kwargs...) - return SwapperProt(;sim, net, node, kwargs...) -end - -@resumable function (prot::SwapperProt)() - rounds = prot.rounds - round = 1 - while rounds != 0 - reg = prot.net[prot.node] - qubit_pair = findswapablequbits(prot.net, prot.node, prot.nodeL, prot.nodeH, prot.chooseL, prot.chooseH) - if isnothing(qubit_pair) - isnothing(prot.retry_lock_time) && error("We do not yet support waiting on register to make qubits available") # TODO - @yield timeout(prot.sim, prot.retry_lock_time) - continue - end - - (q1, id1, tag1) = qubit_pair[1].slot, qubit_pair[1].id, qubit_pair[1].tag - (q2, id2, tag2) = qubit_pair[2].slot, qubit_pair[2].id, qubit_pair[2].tag - @yield lock(q1) & lock(q2) # this should not really need a yield thanks to `findswapablequbits`, but it is better to be defensive - @yield timeout(prot.sim, prot.local_busy_time) - - untag!(q1, id1) - # store a history of whom we were entangled to: remote_node_idx, remote_slot_idx, remote_swapnode_idx, remote_swapslot_idx, local_swap_idx - tag!(q1, EntanglementHistory, tag1[2], tag1[3], tag2[2], tag2[3], q2.idx) - - untag!(q2, id2) - # store a history of whom we were entangled to: remote_node_idx, remote_slot_idx, remote_swapnode_idx, remote_swapslot_idx, local_swap_idx - tag!(q2, EntanglementHistory, tag2[2], tag2[3], tag1[2], tag1[3], q1.idx) - - uptotime!((q1, q2), now(prot.sim)) - swapcircuit = LocalEntanglementSwap() - xmeas, zmeas = swapcircuit(q1, q2) - # send from here to new entanglement counterpart: - # tag with EntanglementUpdateX past_local_node, past_local_slot_idx past_remote_slot_idx new_remote_node, new_remote_slot, correction - msg1 = Tag(EntanglementUpdateX, prot.node, q1.idx, tag1[3], tag2[2], tag2[3], xmeas) - put!(channel(prot.net, prot.node=>tag1[2]; permit_forward=true), msg1) - @debug "SwapperProt @$(prot.node)|round $(round): Send message to $(tag1[2]) | message=`$msg1`" - # send from here to new entanglement counterpart: - # tag with EntanglementUpdateZ past_local_node, past_local_slot_idx past_remote_slot_idx new_remote_node, new_remote_slot, correction - msg2 = Tag(EntanglementUpdateZ, prot.node, q2.idx, tag2[3], tag1[2], tag1[3], zmeas) - put!(channel(prot.net, prot.node=>tag2[2]; permit_forward=true), msg2) - @debug "SwapperProt @$(prot.node)|round $(round): Send message to $(tag2[2]) | message=`$msg2`" - unlock(q1) - unlock(q2) - rounds==-1 || (rounds -= 1) - round += 1 - end -end - -function findswapablequbits(net, node, pred_low, pred_high, choose_low, choose_high) - reg = net[node] - - low_nodes = queryall(reg, EntanglementCounterpart, pred_low, ❓; locked=false, assigned=true) - high_nodes = queryall(reg, EntanglementCounterpart, pred_high, ❓; locked=false, assigned=true) - - (isempty(low_nodes) || isempty(high_nodes)) && return nothing - il = choose_low((n.tag[2] for n in low_nodes)) # TODO make [2] into a nice named property - ih = choose_high((n.tag[2] for n in high_nodes)) - return (low_nodes[il], high_nodes[ih]) -end - """ $TYPEDEF @@ -340,54 +268,93 @@ end workwasdone = true # waiting is not enough because we might have multiple rounds of work to do while workwasdone workwasdone = false - for (updatetagsymbol, updategate) in ((EntanglementUpdateX, Z), (EntanglementUpdateZ, X)) - # look for EntanglementUpdate? past_remote_slot_idx local_slot_idx, new_remote_node, new_remote_slot_idx correction - msg = querydelete!(mb, updatetagsymbol, ❓, ❓, ❓, ❓, ❓, ❓) - isnothing(msg) && continue - @debug "EntanglementTracker @$(prot.node): Received from $(msg.src).$(msg.tag[3]) | message=`$(msg.tag)`" + for (updatetagsymbol, updategate) in ((EntanglementUpdateX, Z), (EntanglementUpdateZ, X), (EntanglementDelete, nothing)) # TODO this is getting ugly. Refactor EntanglementUpdateX and EntanglementUpdateZ to be the same parameterized tag + # look for EntanglementUpdate? or EntanglementDelete message sent to us + if !isnothing(updategate) # EntanglementUpdate + msg = querydelete!(mb, updatetagsymbol, ❓, ❓, ❓, ❓, ❓, ❓) + isnothing(msg) && continue + (src, (_, pastremotenode, pastremoteslotid, localslotid, newremotenode, newremoteslotid, correction)) = msg + else # EntanglementDelete + msg = querydelete!(mb, updatetagsymbol, ❓, ❓, ❓, ❓) + isnothing(msg) && continue + (src, (_, pastremotenode, pastremoteslotid, _, localslotid)) = msg + end + + @debug "EntanglementTracker @$(prot.node): Received from $(msg.src).$(msg.tag[3]) | message=`$(msg.tag)` | time=$(now(prot.sim))" workwasdone = true - (src, (_, pastremotenode, pastremoteslotid, localslotid, newremotenode, newremoteslotid, correction)) = msg localslot = nodereg[localslotid] + # Check if the local slot is still present and believed to be entangled. - # We will need to perform a correction operation due to the swap, + # We will need to perform a correction operation due to the swap or a deletion due to the qubit being thrown out, # but there will be no message forwarding necessary. + @debug "EntanglementTracker @$(prot.node): EntanglementCounterpart requesting lock at $(now(prot.sim))" + @yield lock(localslot) + @debug "EntanglementTracker @$(prot.node): EntanglementCounterpart getting lock at $(now(prot.sim))" counterpart = querydelete!(localslot, EntanglementCounterpart, pastremotenode, pastremoteslotid) + unlock(localslot) if !isnothing(counterpart) - time_before_lock = now(prot.sim) - @debug "EntanglementTracker @$(prot.node): EntanglementCounterpart requesting lock at $(now(prot.sim))" + # time_before_lock = now(prot.sim) @yield lock(localslot) - @debug "EntanglementTracker @$(prot.node): EntanglementCounterpart getting lock at $(now(prot.sim))" - time_after_lock = now(prot.sim) - time_before_lock != time_after_lock && @debug "EntanglementTracker @$(prot.node): Needed Δt=$(time_after_lock-time_before_lock) to get a lock" + # time_after_lock = now(prot.sim) + # time_before_lock != time_after_lock && @debug "EntanglementTracker @$(prot.node): Needed Δt=$(time_after_lock-time_before_lock) to get a lock" if !isassigned(localslot) unlock(localslot) - error("There was an error in the entanglement tracking protocol `EntanglementTracker`. We were attempting to forward a classical message from a node that performed a swap to the remote entangled node. However, on reception of that message it was found that the remote node has lost track of its part of the entangled state although it still keeps a `Tag` as a record of it being present.") + error("There was an error in the entanglement tracking protocol `EntanglementTracker`. We were attempting to forward a classical message from a node that performed a swap to the remote entangled node. However, on reception of that message it was found that the remote node has lost track of its part of the entangled state although it still keeps a `Tag` as a record of it being present.") # TODO make it configurable whether an error is thrown and plug it into the logging module end - # Pauli frame correction gate - if correction==2 - apply!(localslot, updategate) + if !isnothing(updategate) # EntanglementUpdate + # Pauli frame correction gate + if correction==2 + apply!(localslot, updategate) + end + # tag local with updated EntanglementCounterpart new_remote_node new_remote_slot_idx + tag!(localslot, EntanglementCounterpart, newremotenode, newremoteslotid) + else # EntanglementDelete + traceout!(localslot) end - # tag local with updated EntanglementCounterpart new_remote_node new_remote_slot_idx - tag!(localslot, EntanglementCounterpart, newremotenode, newremoteslotid) unlock(localslot) continue end - # If not, check if we have a record of the entanglement being swapped to a different remote node, + + # If there is nothing still stored locally, check if we have a record of the entanglement being swapped to a different remote node, # and forward the message to that node. history = querydelete!(localslot, EntanglementHistory, pastremotenode, pastremoteslotid, # who we were entangled to (node, slot) ❓, ❓, # who we swapped with (node, slot) ❓) # which local slot used to be entangled with whom we swapped with if !isnothing(history) - # @debug "tracker @$(prot.node) history: $(history) | msg: $msg" _, _, _, whoweswappedwith_node, whoweswappedwith_slotidx, swappedlocal_slotidx = history.tag - tag!(localslot, EntanglementHistory, newremotenode, newremoteslotid, whoweswappedwith_node, whoweswappedwith_slotidx, swappedlocal_slotidx) - @debug "EntanglementTracker @$(prot.node): history=`$(history)` | message=`$msg` | Sending to $(whoweswappedwith_node).$(whoweswappedwith_slotidx)" - msghist = Tag(updatetagsymbol, pastremotenode, pastremoteslotid, whoweswappedwith_slotidx, newremotenode, newremoteslotid, correction) - put!(channel(prot.net, prot.node=>whoweswappedwith_node; permit_forward=true), msghist) + if !isnothing(updategate) # EntanglementUpdate + # Forward the update tag to the swapped node and store a new history tag so that we can forward the next update tag to the new node + tag!(localslot, EntanglementHistory, newremotenode, newremoteslotid, whoweswappedwith_node, whoweswappedwith_slotidx, swappedlocal_slotidx) + @debug "EntanglementTracker @$(prot.node): history=`$(history)` | message=`$msg` | Sending to $(whoweswappedwith_node).$(whoweswappedwith_slotidx)" + msghist = Tag(updatetagsymbol, pastremotenode, pastremoteslotid, whoweswappedwith_slotidx, newremotenode, newremoteslotid, correction) + put!(channel(prot.net, prot.node=>whoweswappedwith_node; permit_forward=true), msghist) + else # EntanglementDelete + # We have a delete message but the qubit was swapped so add a tag and forward to swapped node + @debug "EntanglementTracker @$(prot.node): history=`$(history)` | message=`$msg` | Sending to $(whoweswappedwith_node).$(whoweswappedwith_slotidx)" + msghist = Tag(updatetagsymbol, pastremotenode, pastremoteslotid, whoweswappedwith_node, whoweswappedwith_slotidx) + tag!(localslot, updatetagsymbol, prot.node, localslot.idx, whoweswappedwith_node, whoweswappedwith_slotidx) + put!(channel(prot.net, prot.node=>whoweswappedwith_node; permit_forward=true), msghist) + end continue end - error("`EntanglementTracker` on node $(prot.node) received a message $(msg) that it does not know how to handle (due to the absence of corresponding `EntanglementCounterpart` or `EntanglementHistory` tags). This is a bug in the protocol and should not happen -- please report an issue at QuantumSavory's repository.") + + # Finally, if there the history of a swap is not present in the log anymore, + # it must be because a delete message was received, and forwarded, + # and the entanglement history was deleted, and replaced with an entanglement delete tag. + if !isnothing(querydelete!(localslot, EntanglementDelete, prot.node, localslot.idx, pastremotenode, pastremoteslotid)) #deletion from both sides of the swap, deletion msg when both qubits of a pair are deleted, or when EU arrives after ED at swap node with two simultaneous swaps and deletion on one side + if !(isnothing(updategate)) # EntanglementUpdate + # to handle a possible delete-swap-swap case, we need to update the EntanglementDelete tag + tag!(localslot, EntanglementDelete, prot.node, localslot.idx, newremotenode, newremoteslotid) + @debug "EntanglementTracker @$(prot.node): message=`$msg` for deleted qubit handled and EntanglementDelete tag updated" + else # EntanglementDelete + # when the message is EntanglementDelete and the slot history also has an EntanglementDelete tag (both qubits were deleted), do nothing + @debug "EntanglementTracker @$(prot.node): message=`$msg` is for a deleted qubit and is thus dropped" + end + continue + end + + error("`EntanglementTracker` on node $(prot.node) received a message $(msg) that it does not know how to handle (due to the absence of corresponding `EntanglementCounterpart` or `EntanglementHistory` or `EntanglementDelete` tags). This might have happened due to `CutoffProt` deleting qubits while swaps are happening. Make sure that the retention times in `CutoffProt` are sufficiently larger than the `agelimit` in `SwapperProt`. Otherwise, this is a bug in the protocol and should not happen -- please report an issue at QuantumSavory's repository.") end end @debug "EntanglementTracker @$(prot.node): Starting message wait at $(now(prot.sim)) with MessageBuffer containing: $(mb.buffer)" @@ -437,7 +404,6 @@ end continue else query2 = query(prot.net[prot.nodeB], EntanglementCounterpart, prot.nodeA, query1.slot.idx; locked=false, assigned=true) - if isnothing(query2) # in case EntanglementUpdate hasn't reached the second node yet, but the first node has the EntanglementCounterpart @debug "EntanglementConsumer between $(prot.nodeA) and $(prot.nodeB): query on second node found no entanglement (yet...)" @yield timeout(prot.sim, prot.period) @@ -449,10 +415,10 @@ end q2 = query2.slot @yield lock(q1) & lock(q2) - @debug "EntanglementConsumer between $(prot.nodeA) and $(prot.nodeB): queries successful, consuming entanglement" + @debug "EntanglementConsumer between $(prot.nodeA) and $(prot.nodeB): queries successful, consuming entanglement between .$(q1.idx) and .$(q2.idx) @ $(now(prot.sim))" untag!(q1, query1.id) untag!(q2, query2.id) - # TODO do we need to add EntanglementHistory and should that be a different EntanglementHistory since the current one is specifically for SwapperProt + # TODO do we need to add EntanglementHistory or EntanglementDelete and should that be a different EntanglementHistory since the current one is specifically for Swapper # TODO currently when calculating the observable we assume that EntanglerProt.pairstate is always (|00⟩ + |11⟩)/√2, make it more general for other states ob1 = real(observable((q1, q2), Z⊗Z)) ob2 = real(observable((q1, q2), X⊗X)) @@ -466,6 +432,8 @@ end end +include("cutoff.jl") +include("swapping.jl") include("switches.jl") using .Switches diff --git a/src/ProtocolZoo/cutoff.jl b/src/ProtocolZoo/cutoff.jl new file mode 100644 index 00000000..da17356c --- /dev/null +++ b/src/ProtocolZoo/cutoff.jl @@ -0,0 +1,77 @@ +""" +$TYPEDEF + +A protocol running at a node, +checking periodically for any qubits in the node that +have remained unused for more than the retention period of the qubit +and emptying such slots. + +If coordination messages are exchanged during deletions +(instances of the type `EntanglementDelete`), +then a [`EntanglementTracker`](@ref) protocol needs to also run, +to act on such messages. + +$FIELDS +""" +@kwdef struct CutoffProt{LT} <: AbstractProtocol where {LT<:Union{Float64,Nothing}} + """time-and-schedule-tracking instance from `ConcurrentSim`""" + sim::Simulation + """a network graph of registers""" + net::RegisterNet + """the vertex index of the node on which the protocol is running""" + node::Int + """time period between successive queries on the node (`nothing` for queuing up)""" + period::LT = 0.1 + """time after which a slot is emptied""" + retention_time::Float64 = 5.0 + """if `true`, synchronization messages are sent after a deletion to the node containing the other entangled qubit""" + announce::Bool = true +end + +function CutoffProt(sim::Simulation, net::RegisterNet, node::Int; kwargs...) + return CutoffProt(;sim, net, node, kwargs...) +end + +@resumable function (prot::CutoffProt)() + if isnothing(prot.period) + error("In `CutoffProt` we do not yet support quing up and waiting on register") # TODO + end + reg = prot.net[prot.node] + while true + for slot in reg # TODO these should be done in parallel, otherwise we will be waiting on each slot, greatly slowing down the cutoffs + islocked(slot) && continue + @yield lock(slot) + info = query(slot, EntanglementCounterpart, ❓, ❓) + if isnothing(info) + unlock(slot) + continue + end + if now(prot.sim) - reg.tag_info[info.id][3] > prot.retention_time # TODO this should be part of the query interface, not using non-public implementation details + untag!(slot, info.id) + traceout!(slot) + msg = Tag(EntanglementDelete, prot.node, slot.idx, info.tag[2], info.tag[3]) + tag!(slot, msg) + (prot.announce) && put!(channel(prot.net, prot.node=>msg[4]; permit_forward=true), msg) + @debug "CutoffProt @$(prot.node): Send message to $(msg[4]) | message=`$msg` | time=$(now(prot.sim))" + end + + # TODO the tag deletions below are not necessary when announce=true and EntanglementTracker is running on other nodes. Verify the veracity of that statement, make tests for both cases, and document. + + # delete old history tags + info = query(slot, EntanglementHistory, ❓, ❓, ❓, ❓, ❓;filo=false) # TODO we should have a warning if `queryall` returns more than one result -- what does it even mean to have multiple history tags here + if !isnothing(info) && now(prot.sim) - reg.tag_info[info.id][3] > prot.retention_time # TODO this should be part of the query interface, not using non-public implementation details + untag!(slot, info.id) + end + + # delete old EntanglementDelete tags + # TODO Why do we have separate entanglementhistory and entanglementupdate but we have only a single entanglementdelete that serves both roles? We should probably have both be pairs of tags, for consistency and ease of reasoning + info = query(slot, EntanglementDelete, prot.node, slot.idx , ❓, ❓) # TODO we should have a warning if `queryall` returns more than one result -- what does it even mean to have multiple delete tags here + if !isnothing(info) && now(prot.sim) - reg.tag_info[info.id][3] > prot.retention_time # TODO this should be part of the query interface, not using non-public implementation details + untag!(slot, info.id) + end + + unlock(slot) + end + @yield timeout(prot.sim, prot.period) + end +end diff --git a/src/ProtocolZoo/swapping.jl b/src/ProtocolZoo/swapping.jl new file mode 100644 index 00000000..fec23c85 --- /dev/null +++ b/src/ProtocolZoo/swapping.jl @@ -0,0 +1,107 @@ +function random_index(arr) + return rand(keys(arr)) +end + + +function findswapablequbits(net, node, pred_low, pred_high, choose_low, choose_high; agelimit=nothing) + reg = net[node] + low_nodes = [ + n for n in queryall(reg, EntanglementCounterpart, pred_low, ❓; locked=false, assigned=true) + if isnothing(agelimit) || !isolderthan(n.slot, agelimit) # TODO add age limit to query and queryall + ] + high_nodes = [ + n for n in queryall(reg, EntanglementCounterpart, pred_high, ❓; locked=false, assigned=true) + if isnothing(agelimit) || !isolderthan(n.slot, agelimit) # TODO add age limit to query and queryall + ] + + (isempty(low_nodes) || isempty(high_nodes)) && return nothing + il = choose_low((n.tag[2] for n in low_nodes)) # TODO make [2] into a nice named property + ih = choose_high((n.tag[2] for n in high_nodes)) + return (low_nodes[il], high_nodes[ih]) +end + + +""" +$TYPEDEF + +A protocol, running at a given node, that finds swappable entangled pairs and performs the swap. + +Consider setting an `agelimit` on qubits +and using it together with the cutoff protocol, [`CutoffProt`](@ref), +which deletes qubits that are about to go past their cutoff/retention time. + +$TYPEDFIELDS +""" +@kwdef struct SwapperProt{NL,NH,CL,CH,LT} <: AbstractProtocol where {NL<:Union{Int,<:Function,Wildcard}, NH<:Union{Int,<:Function,Wildcard}, CL<:Function, CH<:Function, LT<:Union{Float64,Nothing}} + """time-and-schedule-tracking instance from `ConcurrentSim`""" + sim::Simulation + """a network graph of registers""" + net::RegisterNet + """the vertex of the node where swapping is happening""" + node::Int + """the vertex of one of the remote nodes for the swap, arbitrarily referred to as the "low" node (or a predicate function or a wildcard); if you are working on a repeater chain, a good choice is `<(current_node)`, i.e. any node to the "left" of the current node""" + nodeL::NL = ❓ + """the vertex of the other remote node for the swap, the "high" counterpart of `nodeL`; if you are working on a repeater chain, a good choice is `>(current_node)`, i.e. any node to the "right" of the current node""" + nodeH::NH = ❓ + """the `nodeL` predicate can return many positive candidates; `chooseL` picks one of them (by index into the array of filtered `nodeL` results), defaults to a random pick `arr->rand(keys(arr))`; if you are working on a repeater chain a good choice is `argmin`, i.e. the node furthest to the "left" """ + chooseL::CL = random_index + """the `nodeH` counterpart for `chooseH`; if you are working on a repeater chain a good choice is `argmax`, i.e. the node furthest to the "right" """ + chooseH::CH = random_index + """fixed "busy time" duration immediately before starting entanglement generation attempts""" + local_busy_time::Float64 = 0.0 # TODO the gates should have that busy time built in + """how long to wait before retrying to lock qubits if no qubits are available (`nothing` for queuing up and waiting)""" + retry_lock_time::LT = 0.1 + """how many rounds of this protocol to run (`-1` for infinite))""" + rounds::Int = -1 + """what is the oldest a qubit should be to be picked for a swap (to avoid swapping with qubits that are about to be deleted, the agelimit should be shorter than the retention time of the cutoff protocol) (`nothing` for no limit) -- you probably want to use [`CutoffProt`](@ref) if you have an agelimit""" + agelimit::Union{Float64,Nothing} = nothing +end + +#TODO "convenience constructor for the missing things and finish this docstring" +function SwapperProt(sim::Simulation, net::RegisterNet, node::Int; kwargs...) + return SwapperProt(;sim, net, node, kwargs...) +end + +@resumable function (prot::SwapperProt)() + rounds = prot.rounds + round = 1 + while rounds != 0 + qubit_pair = findswapablequbits(prot.net, prot.node, prot.nodeL, prot.nodeH, prot.chooseL, prot.chooseH; agelimit=prot.agelimit) + if isnothing(qubit_pair) + isnothing(prot.retry_lock_time) && error("We do not yet support waiting on register to make qubits available") # TODO + @yield timeout(prot.sim, prot.retry_lock_time) + continue + end + + (q1, id1, tag1) = qubit_pair[1].slot, qubit_pair[1].id, qubit_pair[1].tag + (q2, id2, tag2) = qubit_pair[2].slot, qubit_pair[2].id, qubit_pair[2].tag + + @yield lock(q1) & lock(q2) # this should not really need a yield thanks to `findswapablequbits` which queries only for unlocked qubits, but it is better to be defensive + untag!(q1, id1) + # store a history of whom we were entangled to: remote_node_idx, remote_slot_idx, remote_swapnode_idx, remote_swapslot_idx, local_swap_idx + tag!(q1, EntanglementHistory, tag1[2], tag1[3], tag2[2], tag2[3], q2.idx) + + untag!(q2, id2) + # store a history of whom we were entangled to: remote_node_idx, remote_slot_idx, remote_swapnode_idx, remote_swapslot_idx, local_swap_idx + tag!(q2, EntanglementHistory, tag2[2], tag2[3], tag1[2], tag1[3], q1.idx) + + uptotime!((q1, q2), now(prot.sim)) + swapcircuit = LocalEntanglementSwap() + xmeas, zmeas = swapcircuit(q1, q2) + # send from here to new entanglement counterpart: + # tag with EntanglementUpdateX past_local_node, past_local_slot_idx past_remote_slot_idx new_remote_node, new_remote_slot, correction + msg1 = Tag(EntanglementUpdateX, prot.node, q1.idx, tag1[3], tag2[2], tag2[3], xmeas) + put!(channel(prot.net, prot.node=>tag1[2]; permit_forward=true), msg1) + @debug "SwapperProt @$(prot.node)|round $(round): Send message to $(tag1[2]) | message=`$msg1` | time = $(now(prot.sim))" + # send from here to new entanglement counterpart: + # tag with EntanglementUpdateZ past_local_node, past_local_slot_idx past_remote_slot_idx new_remote_node, new_remote_slot, correction + msg2 = Tag(EntanglementUpdateZ, prot.node, q2.idx, tag2[3], tag1[2], tag1[3], zmeas) + put!(channel(prot.net, prot.node=>tag2[2]; permit_forward=true), msg2) + @debug "SwapperProt @$(prot.node)|round $(round): Send message to $(tag2[2]) | message=`$msg2` | time = $(now(prot.sim))" + @yield timeout(prot.sim, prot.local_busy_time) + unlock(q1) + unlock(q2) + rounds==-1 || (rounds -= 1) + round += 1 + end +end diff --git a/src/queries.jl b/src/queries.jl index 47ebddce..56aee963 100644 --- a/src/queries.jl +++ b/src/queries.jl @@ -407,6 +407,24 @@ function findfreeslot(reg::Register; randomize=false, margin=0) end end +struct NotAssignedError <: Exception # TODO use this in all places where we are throwing something on isassigned (maybe rename to IsAssignedError and check whether we need to keep `f` as part of it (might already be provided by the stacktrace) and check it does not allocate even when the error is not triggered) + msg + f +end + +function Base.showerror(io::IO, err::NotAssignedError) + print(io, "NotAssignedError: ") + println(io, err.msg) + println("In function: $(err.f)") +end + +function isolderthan(slot::RegRef, age::Float64) + if !isassigned(slot) throw(NotAssignedError("Slot must be assigned with a quantum state before checking coherence.", isolderthan)) end + id = query(slot, QuantumSavory.ProtocolZoo.EntanglementCounterpart, ❓, ❓).id + slot_time = slot.reg.tag_info[id][3] + return (now(get_time_tracker(slot))) - slot_time > age +end + function Base.isassigned(r::Register,i::Int) # TODO erase r.stateindices[i] != 0 # TODO this also usually means r.staterefs[i] !== nothing - choose one and make things consistent diff --git a/src/tags.jl b/src/tags.jl index f5ecf531..73971513 100644 --- a/src/tags.jl +++ b/src/tags.jl @@ -10,7 +10,7 @@ julia> Tag(:sometagdescriptor, 1, 2, -3) SymbolIntIntInt(:sometagdescriptor, 1, 2, -3)::Tag ``` -A tag can have a custom `DataType` as first argument, in which case additional customability in printing is available. E.g. consider the [`EntanglementHistory`] tag used to track how pairs were entangled before a swap happened. +A tag can have a custom `DataType` as first argument, in which case additional customizability in printing is available. E.g. consider the [`EntanglementHistory`] tag used to track how pairs were entangled before a swap happened. ```jldoctest julia> using QuantumSavory.ProtocolZoo: EntanglementHistory diff --git a/test/Project.toml b/test/Project.toml index 2d98fc3a..744b5544 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -15,6 +15,7 @@ JumpProcesses = "ccbc3e58-028d-4f4c-8cd5-9ae44345cda5" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +NetworkLayout = "46757867-2c16-5918-afeb-47bfcb05e46a" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Quantikz = "b0d11df0-eea3-4d79-b4a5-421488cbf74b" QuantumClifford = "0525e862-1e90-11e9-3e4d-1b39d7109de1" diff --git a/test/runtests.jl b/test/runtests.jl index 4a2e9c62..942565ec 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -37,6 +37,7 @@ println("Starting tests with $(Threads.nthreads()) threads out of `Sys.CPU_THREA @doset "protocolzoo_entanglement_tracker_grid" @doset "protocolzoo_switch" @doset "protocolzoo_throws" +@doset "protocolzoo_cutoffprot" @doset "circuitzoo_api" @doset "circuitzoo_ent_swap" diff --git a/test/test_examples.jl b/test/test_examples.jl index fca719e7..502776a8 100644 --- a/test/test_examples.jl +++ b/test/test_examples.jl @@ -36,6 +36,13 @@ end end end +@safetestset "repeatergrid" begin + if get(ENV, "QUANTUMSAVORY_PLOT_TEST","")=="true" + include("../examples/repeatergrid/1a_async_interactive_visualization.jl") + include("../examples/repeatergrid/2a_sync_interactive_visualization.jl") + end +end + if get(ENV,"QUANTUMSAVORY_PLOT_TEST","")=="true" import GLMakie GLMakie.closeall() # to avoid errors when running headless diff --git a/test/test_jet.jl b/test/test_jet.jl index 529a73ba..69333797 100644 --- a/test/test_jet.jl +++ b/test/test_jet.jl @@ -37,5 +37,6 @@ rep = report_package("QuantumSavory"; @show length(JET.get_reports(rep)) @show rep -@test length(JET.get_reports(rep)) <= 133 + +@test length(JET.get_reports(rep)) <= 146 @test_broken length(JET.get_reports(rep)) == 0 diff --git a/test/test_plotting_gl.jl b/test/test_plotting_gl.jl index c9acda48..b1683348 100644 --- a/test/test_plotting_gl.jl +++ b/test/test_plotting_gl.jl @@ -8,3 +8,23 @@ end @testset "arguments and observables and tags" begin include("test_plotting_2_tags_observables.jl") end + +using QuantumSavory + +@testset "data inspectors" begin # only available in GLMakie + # create a network of qubit registers + net = RegisterNet([Register(2),Register(3),Register(2),Register(5)]) + + # add some states, entangle a few slots, perform some gates + initialize!(net[1,1]) + initialize!(net[2,3], X₁) + initialize!((net[3,1],net[4,2]), X₁⊗Z₂) + apply!((net[2,3],net[3,1]), CNOT) + + # create the plot + fig = Figure(size=(800,400)) + _, ax, plt, obs = registernetplot_axis(fig[1,1],net) + + # check the data inspector tooltip functionality + @test Base.get_extension(QuantumSavory, :QuantumSavoryMakie).get_state_vis_string(plt.state_coords_backref[],1) == "Subsystem 1 of a state of 1 subsystems, stored in\nRegister 1 | Slot 1\n not tagged" +end diff --git a/test/test_protocolzoo_cutoffprot.jl b/test/test_protocolzoo_cutoffprot.jl new file mode 100644 index 00000000..1bdfd65c --- /dev/null +++ b/test/test_protocolzoo_cutoffprot.jl @@ -0,0 +1,31 @@ +using QuantumSavory +using QuantumSavory.ProtocolZoo + +using ConcurrentSim +using ResumableFunctions + +using Test + +if isinteractive() + using Logging + logger = ConsoleLogger(Logging.Debug; meta_formatter=(args...)->(:black,"","")) + global_logger(logger) + println("Logger set to debug") +end + +net = RegisterNet([Register(1), Register(1)]) +sim = get_time_tracker(net) +initialize!((net[1][1], net[2][1]), (Z1⊗Z1+Z2⊗Z2)/(sqrt(2.0))) +tag!(net[1][1], EntanglementCounterpart, 2, 1) +tag!(net[2][1], EntanglementCounterpart, 1, 1) + +cprot = CutoffProt(sim, net, 1; retention_time=3.0) +@process cprot() + +run(sim, 2.0) +@test isassigned(net[1][1]) +@test isassigned(net[2][1]) + +run(sim, 6.0) +@test !isassigned(net[1][1]) +@test isassigned(net[2][1]) \ No newline at end of file diff --git a/test/test_protocolzoo_entanglement_consumer.jl b/test/test_protocolzoo_entanglement_consumer.jl index 4581bc68..bc3878f4 100644 --- a/test/test_protocolzoo_entanglement_consumer.jl +++ b/test/test_protocolzoo_entanglement_consumer.jl @@ -13,7 +13,8 @@ end for n in 3:30 - net = RegisterNet([Register(10) for j in 1:n]) + regsize = 10 + net = RegisterNet([Register(regsize) for j in 1:n]) sim = get_time_tracker(net) for e in edges(net) @@ -38,10 +39,8 @@ for n in 3:30 for i in 1:length(econ.log) - if !isnothing(econ.log[i][2]) - @test econ.log[i][2] ≈ 1.0 - @test econ.log[i][3] ≈ 1.0 - end + @test econ.log[i][2] ≈ 1.0 + @test econ.log[i][3] ≈ 1.0 end end diff --git a/test/test_protocolzoo_entanglement_tracker_grid.jl b/test/test_protocolzoo_entanglement_tracker_grid.jl index 51865585..9fbee78d 100644 --- a/test/test_protocolzoo_entanglement_tracker_grid.jl +++ b/test/test_protocolzoo_entanglement_tracker_grid.jl @@ -228,5 +228,49 @@ end # More tests of 2D rectangular grids with the full stack of protocols, # but also now with an unlimited number of rounds and an entanglement consumer. -#TODO -@test_broken false + +n = 6 # the size of the square grid network (n × n) +regsize = 20 # the size of the quantum registers at each node + +graph = grid([n,n]) +net = RegisterNet(graph, [Register(regsize) for i in 1:n^2]) + +sim = get_time_tracker(net) + +# each edge is capable of generating raw link-level entanglement +for (;src, dst) in edges(net) + eprot = EntanglerProt(sim, net, src, dst; rounds=-1, randomize=true) + @process eprot() +end + +# each node except the corners on one of the diagonals is capable of swapping entanglement +for i in 2:(n^2 - 1) + l(x) = check_nodes(net, i, x) + h(x) = check_nodes(net, i, x; low=false) + cL(arr) = choose_node(net, i, arr) + cH(arr) = choose_node(net, i, arr; low=false) + swapper = SwapperProt(sim, net, i; nodeL = l, nodeH = h, chooseL = cL, chooseH = cH, rounds=-1) + @process swapper() +end + +# each node is running entanglement tracking to keep track of classical data about the entanglement +for v in vertices(net) + tracker = EntanglementTracker(sim, net, v) + @process tracker() +end + +# a mock entanglement consumer between the two corners of the grid +consumer = EntanglementConsumer(sim, net, 1, n^2) +@process consumer() + +# at each node we discard the qubits that have decohered after a certain cutoff time +for v in vertices(net) + cutoffprot = CutoffProt(sim, net, v) + @process cutoffprot() +end +run(sim, 400) + +for i in 1:length(consumer.log) + @test consumer.log[i][2] ≈ 1.0 + @test consumer.log[i][3] ≈ 1.0 +end \ No newline at end of file