diff --git a/CHANGELOG.md b/CHANGELOG.md index 09480f72..8ba9107f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## v0.5.1-dev +- Simplify one of the switch protocols to avoid dependence on GraphMatching.jl which does not install well on non-linux systems. Do not rely on the default `SimpleSwitchDiscreteProt` for the time being. +- Implement a network control protocol that is connection-oriented, centralized and non-distributed +- Implement protocols: request generator and request tracker for simulation with the above control protocol in an asynchronous way. +- Add `PhysicalGraph` struct for storing network metadata as the simulation evolves. +- New tags: `EntanglementRequest`,`SwapRequest`, `DistributionRequest` and `RequestCompletion` - Add `classical_delay` and `quantum_delay` as keyword arguments to the `RegisterNet` constructor to set a default global network edge latency. ## v0.5.0 - 2024-10-16 diff --git a/examples/congestionchain/1_visualization.jl b/examples/congestionchain/1_visualization.jl index c1df5d8d..2e1790dd 100644 --- a/examples/congestionchain/1_visualization.jl +++ b/examples/congestionchain/1_visualization.jl @@ -44,7 +44,7 @@ scatter!(ax_fidZZ,ts,fidZZ,label="ZZ",color=(c2,0.1)) display(fig) -step_ts = range(0, 1000, step=0.1) +step_ts = range(0.0, 1000.0, step=0.1) record(fig, "congestionchain.mp4", step_ts; framerate=50, visible=true) do t run(sim, t) diff --git a/examples/controlplane/1a_cdd_interactive.jl b/examples/controlplane/1a_cdd_interactive.jl new file mode 100644 index 00000000..4c3ca8e4 --- /dev/null +++ b/examples/controlplane/1a_cdd_interactive.jl @@ -0,0 +1,46 @@ +include("setup.jl") + +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 + +local_busy_time = Observable(0.0) +retry_lock_time = Observable(0.1) +for node in 2:7 + swapper = SwapperProt(sim, net, node; nodeL = <(node), nodeH = >(node), chooseL = argmin, chooseH = argmax, rounds=-1, local_busy_time=local_busy_time[], + retry_lock_time=retry_lock_time[]) + @process swapper() +end + +for v in vertices(net) + tracker = EntanglementTracker(sim, net, v) + @process tracker() +end + +period_cons = Observable(0.1) +consumer = EntanglementConsumer(sim, net, 1, 8; period=period_cons[]) +@process consumer() + +period_dec = Observable(0.1) +for v in vertices(net) + cutoff = CutoffProt(sim, net, v; period=period_dec[]) + @process cutoff() +end +params = [succ_prob, local_busy_time, retry_lock_time, period_cons, period_dec] +sim, net, obs, entlog, entlogaxis, fid_axis, histaxis, num_epr_axis, fig = prepare_vis(consumer, params) + +step_ts = range(0.0, 1000.0, step=0.1) +record(fig, "sim.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 \ No newline at end of file diff --git a/examples/controlplane/1b_cdd_wglmakie.jl b/examples/controlplane/1b_cdd_wglmakie.jl new file mode 100644 index 00000000..32bc5bc6 --- /dev/null +++ b/examples/controlplane/1b_cdd_wglmakie.jl @@ -0,0 +1,115 @@ +using WGLMakie +WGLMakie.activate!() +import Bonito +using Markdown + +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 + +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 + +local_busy_time = Observable(0.0) +retry_lock_time = Observable(0.1) +for node in 2:7 + swapper = SwapperProt(sim, net, node; nodeL = <(node), nodeH = >(node), chooseL = argmin, chooseH = argmax, rounds=-1, local_busy_time=local_busy_time[], + retry_lock_time=retry_lock_time[]) + @process swapper() +end + +for v in vertices(net) + tracker = EntanglementTracker(sim, net, v) + @process tracker() +end + +period_cons = Observable(0.1) +consumer = EntanglementConsumer(sim, net, 1, 8; period=period_cons[]) +@process consumer() + +period_dec = Observable(0.1) +for v in vertices(net) + cutoff = CutoffProt(sim, net, v; period=period_dec[]) + @process cutoff() +end +params = [succ_prob, local_busy_time, retry_lock_time, period_cons, period_dec] + +# 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, fid_axis, histaxis, num_epr_axis, 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, fid_axis, histaxis, num_epr_axis, fig = prepare_vis(consumer, params) + + running = Observable{Any}(false) + fig[5,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, fid_axis, histaxis, num_epr_axis, running) + end + end + end + + + content = md""" + Pick simulation settings and hit run (see below for technical details). + + $(fig.scene) + + # Connectionless, Distributed and Decentralized Control Plane for Entanglement Distribution + + The above simulation visualizes entanglement distribution between Alice and Bob on an arbitrary network topology + given by the adjacency matrix of the graph. The control plane architecture used for this simulation is connectionless, + distributed and decentralized. The node representing Alice is the node on the top left and the bottom right is Bob. + The actual connectivity of the physical graph isn't fully captured by the visualization above as we use edges only to + show the virtual graph. + + [See and modify the code for this simulation on github.](https://github.com/QuantumSavory/QuantumSavory.jl/tree/master/examples/controlplane/1b_cdd_wglmakie.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, "CDD_PORT", "8888")) +interface = get(ENV, "CDD_IP", "127.0.0.1") +proxy_url = get(ENV, "CDD_PROXY", "") +server = Bonito.Server(interface, port; proxy_url); +Bonito.HTTPServer.start(server) +Bonito.route!(server, "/" => landing); + +## + +wait(server) \ No newline at end of file diff --git a/examples/controlplane/2a_cnc_interactive.jl b/examples/controlplane/2a_cnc_interactive.jl new file mode 100644 index 00000000..19f14c35 --- /dev/null +++ b/examples/controlplane/2a_cnc_interactive.jl @@ -0,0 +1,40 @@ +include("setup.jl") + +controller = Controller(sim, net, 6, fill(nothing, 8, 8)) +@process controller() + +req_gen = RequestGenerator(sim, net, 1, 8, 6) +@process req_gen() + +consumer = EntanglementConsumer(sim, net, 1, 8) +@process consumer() + +for node in 1:7 + tracker = RequestTracker(sim, net, node) + @process tracker() +end + +for v in 1:8 + tracker = EntanglementTracker(sim, net, v) + @process tracker() +end + +for v in 1:8 + c_prot = CutoffProt(sim, net, v) + @process c_prot() +end + +sim, net, obs, entlog, entlogaxis, fid_axis, histaxis, num_epr_axis, fig = prepare_vis(consumer) + +step_ts = range(0.0, 1000.0, step=0.1) +record(fig, "sim.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) + 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 \ No newline at end of file diff --git a/examples/controlplane/2b_cnc_wglmakie.jl b/examples/controlplane/2b_cnc_wglmakie.jl new file mode 100644 index 00000000..99d840f5 --- /dev/null +++ b/examples/controlplane/2b_cnc_wglmakie.jl @@ -0,0 +1,107 @@ +using WGLMakie +WGLMakie.activate!() +import Bonito +using Markdown + +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 + +controller = Controller(sim, net, 6, zeros(8,8)) +@process controller() + +req_gen = RequestGenerator(sim, net, 1, 8, 6) +@process req_gen() + +consumer = EntanglementConsumer(sim, net, 1, 8) +@process consumer() + +for node in 1:7 + tracker = RequestTracker(sim, net, node) + @process tracker() +end + +for v in 1:8 + tracker = EntanglementTracker(sim, net, v) + @process tracker() +end + +for v in 1:8 + c_prot = CutoffProt(sim, net, v) + @process c_prot() +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, entlogaxis, fid_axis, histaxis, num_epr_axis, running) + step_ts = range(0, 1000, step=0.1) + for t in step_ts + run(sim, t) + notify.((obs,entlog)) + 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, fid_axis, histaxis, num_epr_axis, fig = prepare_vis(consumer) + + running = Observable{Any}(false) + fig[5,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, entlogaxis, fid_axis, histaxis, num_epr_axis, running) + end + end + end + + + content = md""" + Pick simulation settings and hit run (see below for technical details). + + $(fig.scene) + + # Connection-Oriented, Non-Distributed and Centralized Control Plane for Entanglement Distribution + + The above simulation visualizes entanglement distribution between Alice and Bob on an arbitrary network topology + given by the adjacency matrix of the graph. The control plane architecture used for this simulation is connection-oriented, + non-distributed and centralized. The node representing Alice is the node on the top left and the bottom right is Bob. + The actual connectivity of the physical graph isn't fully captured by the visualization above as we use edges only to + show the virtual graph. + + [See and modify the code for this simulation on github.](https://github.com/QuantumSavory/QuantumSavory.jl/tree/master/examples/controlplane/2b_cnc_wglmakie.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, "CNC_PORT", "8888")) +interface = get(ENV, "CNC_IP", "127.0.0.1") +proxy_url = get(ENV, "CNC_PROXY", "") +server = Bonito.Server(interface, port; proxy_url); +Bonito.HTTPServer.start(server) +Bonito.route!(server, "/" => landing); + +## + +wait(server) diff --git a/examples/controlplane/Project.toml b/examples/controlplane/Project.toml new file mode 100644 index 00000000..6e6b0c0e --- /dev/null +++ b/examples/controlplane/Project.toml @@ -0,0 +1,15 @@ +[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" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +QuantumOptics = "6e0679c1-51ea-5a7c-ac74-d61b76210b0c" +QuantumSavory = "2de2e421-972c-4cb5-a0c3-999c85908079" +QuantumSymbolics = "efa7fd63-0460-4890-beb7-be1bbdfbaeae" +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/controlplane/Readme.md b/examples/controlplane/Readme.md new file mode 100644 index 00000000..5356e174 --- /dev/null +++ b/examples/controlplane/Readme.md @@ -0,0 +1,26 @@ +Different control plane architectures with arbitrary network topologies can be simulated using QuantumSavory. The `setup.jl` contains basic functionality required by the simulations. The other files are described below: + +1a. A simulation that generates an interactive visualization for a connectionless, distributed and decentralized entanglement distribution network + +1b. An interactive web app with the same simulation as 1a + +2a. A simulation that generates an interactive visualization for a connection-oriented, non-distributed and centralized entanglement distribution network + +2b. An interactive web app with the same simulation as 2a + +The control protocol is illustrated at a high level by the sequence diagram below: + +```mermaid +sequenceDiagram +Alice(Request Generator)->>+Controller: DistributionRequest(path_id) +Controller-->>+RequestTracker1: EntanglementRequest +Controller-->>+RequestTracker2: EntanglementRequest +Controller-->>+RequestTrackerN: EntanglementRequest +Controller-->>+RequestTracker1: SwapRequest +Controller-->>+RequestTracker2: SwapRequest +Controller-->>+RequestTrackerN: SwapRequest +Controller-->>-Alice(Request Generator): RequestCompletion(path_id) +``` + +The `RequestGenerator` (Alice) sends a message to the controller, requesting entanglement generation with an end node (Bob). +The message contains index of the path selected by Alice and the controller sends `EntanglementRequest`s to the nodes on the path followed by `SwapRequest`s \ No newline at end of file diff --git a/examples/controlplane/setup.jl b/examples/controlplane/setup.jl new file mode 100644 index 00000000..6f8e48c2 --- /dev/null +++ b/examples/controlplane/setup.jl @@ -0,0 +1,86 @@ +using QuantumSavory +using QuantumSavory.ProtocolZoo +using ConcurrentSim +using ResumableFunctions + +using Graphs +using GLMakie +GLMakie.activate!() + +using NetworkLayout +using Random + +adjm = [0 1 0 0 1 0 0 0 + 1 0 1 0 0 0 0 0 + 0 1 0 1 0 1 0 0 + 0 0 1 0 0 0 1 1 + 1 0 0 0 0 1 0 1 + 0 0 1 0 1 0 1 0 + 0 0 0 1 0 1 0 1 + 0 0 0 1 1 0 1 0] +graph = SimpleGraph(adjm) + +regsize = 20 +net = RegisterNet(graph, [Register(regsize, T1Decay(10.0)) for i in 1:8]) +sim = get_time_tracker(net) + +function prepare_vis(consumer::EntanglementConsumer, params=nothing) + ### + 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) + + if !isnothing(params) + # sliders + sg = SliderGrid( + fig[4,1], + (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="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 = true) + + for (param, slider) in zip(params, sg.sliders) + on(slider.value) do val + param[] = val + end + end + end + + + display(fig) + + return consumer.sim, consumer.net, obs, entlog, entlogaxis, fid_axis, histaxis, num_epr_axis, fig +end \ No newline at end of file diff --git a/src/ProtocolZoo/ProtocolZoo.jl b/src/ProtocolZoo/ProtocolZoo.jl index dee968ac..a450b040 100644 --- a/src/ProtocolZoo/ProtocolZoo.jl +++ b/src/ProtocolZoo/ProtocolZoo.jl @@ -7,20 +7,25 @@ using QuantumSavory.CircuitZoo: EntanglementSwap, LocalEntanglementSwap using DocStringExtensions -using Distributions: Geometric +using Distributions: Geometric, Exponential using ConcurrentSim: Simulation, @yield, timeout, @process, now import ConcurrentSim: Process import ResumableFunctions using ResumableFunctions: @resumable import SumTypes +using Graphs export # protocols - EntanglerProt, SwapperProt, EntanglementTracker, EntanglementConsumer, CutoffProt, + EntanglerProt, SwapperProt, EntanglementTracker, EntanglementConsumer, CutoffProt, RequestTracker, RequestGenerator, # tags - EntanglementCounterpart, EntanglementHistory, EntanglementUpdateX, EntanglementUpdateZ, + EntanglementCounterpart, EntanglementHistory, EntanglementUpdateX, EntanglementUpdateZ, EntanglementRequest, SwapRequest, DistributionRequest, # from Switches - SimpleSwitchDiscreteProt, SwitchRequest + SimpleSwitchDiscreteProt, SwitchRequest, + # controllers + NetController, Controller, + #utils + PathMetadata, path_selection abstract type AbstractProtocol end @@ -145,6 +150,68 @@ 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 message sent from a controller to the [`RequestTracker`](@ref) at a node requesting the generation of an entanglement link between the receiving node +and one of its next-hop neighbors on the physical graph, as mentioned in the request + +$TYPEDFIELDS + +See also: [EntanglerProt](@ref), [`EntanglementTracker`](@ref), [`SwapRequest`](@ref) +""" +@kwdef struct EntanglementRequest + "The id of the node receiving the request" + receiver::Int + "The id of the node with which the entanglement link should be established" + neighbor::Int + "The number of rounds the Entangler should run for" + rounds::Int +end +Base.show(io::IO, tag::EntanglementRequest) = print(io, "$(tag.receiver) attempt entanglement generation with $(tag.neighbor)") +Tag(tag::EntanglementRequest) = Tag(EntanglementRequest, tag.receiver, tag.neighbor, tag.rounds) + +""" +$TYPEDEF + +A message sent from a controller to the [`RequestTracker`](@ref) at a node requesting it to perform a swap + +$TYPEDFIELDS + +See also: [`SwapperProt`](@ref), [`EntanglementTracker`](@ref), [`EntanglementRequest`](@ref) +""" +@kwdef struct SwapRequest + """The id of the node instructed to perform a swap""" + swapping_node::Int + """The number of rounds the swapper should run for""" + rounds::Int + """source node for the `DistributionRequest`""" + src::Int + """destination node for the `DistributionRequest`""" + dst::Int +end +Base.show(io::IO, tag::SwapRequest) = print(io, "Node $(tag.swapping_node) perform a swap") +Tag(tag::SwapRequest) = Tag(SwapRequest, tag.swapping_node, tag.rounds) + +""" +$TYPEDEF + +A message sent from a node to a control protocol requesting bipartite entanglement with a remote destination node through entanglement distribution. + +$TYPEDFIELDS + +See also: [`EntanglementRequest`](@ref), [`SwapRequest`] +""" +@kwdef struct DistributionRequest + """The node generating the request""" + src::Int + """The node with which entanglement is to be generated""" + dst::Int +end +Base.show(io::IO, tag::DistributionRequest) = print(io, "Node $(tag.src) requesting entanglement with $(tag.dst)") +Tag(tag::DistributionRequest) = Tag(DistributionRequest, tag.src, tag.dst) + + """ $TYPEDEF @@ -372,7 +439,7 @@ $TYPEDEF A protocol running between two nodes, checking periodically for any entangled pairs between the two nodes and consuming/emptying the qubit slots. -$FIELDS +$TYPEDFIELDS """ @kwdef struct EntanglementConsumer{LT} <: AbstractProtocol where {LT<:Union{Float64,Nothing}} """time-and-schedule-tracking instance from `ConcurrentSim`""" @@ -435,9 +502,102 @@ end end end +""" +$TYPEDEF + +A protocol running at a node, listening for incoming entanglement generation and swap requests and serving +them in an asynchronous way, without waiting for the completion of the instantiated entanglement generation or swapping processes to complete + +$TYPEDFIELDS +""" +@kwdef struct RequestTracker <: AbstractProtocol + """time-and-schedule-tracking instance from `ConcurrentSim`""" + sim::Simulation + """a network graph of registers""" + net::RegisterNet + """the vertex of the node where the tracker is working""" + node::Int +end + +@resumable function (prot::RequestTracker)() + mb = messagebuffer(prot.net, prot.node) + while true + workwasdone = true # waiting is not enough because we might have multiple rounds of work to do + while workwasdone + workwasdone = false # if there is nothing in the mb queue(querydelete returns nothing) we skip to waiting, otherwise we keep querying until the queue is empty + for requesttagsymbol in (EntanglementRequest, SwapRequest) + if requesttagsymbol == EntanglementRequest + msg = querydelete!(mb, requesttagsymbol, ❓, ❓, ❓) + @debug "RequestTracker @$(prot.node): Received $msg" + isnothing(msg) && continue + workwasdone = true + (src, (_, _, neighbor, rounds)) = msg + @debug "RequestTracker @$(prot.node): Generating entanglement with $(neighbor)" + entangler = EntanglerProt(prot.sim, prot.net, prot.node, neighbor; rounds=rounds, randomize=true) + @process entangler() + else + msg = querydelete!(mb, requesttagsymbol, ❓, ❓, ❓, ❓) + @debug "RequestTracker @$(prot.node): Received $msg" + isnothing(msg) && continue + workwasdone = true + (msg_src, (_, _, rounds, req_src, req_dst)) = msg + @debug "RequestTracker @$(prot.node): Performing a swap" + swapper = SwapperProt(prot.sim, prot.net, prot.node; nodeL = req_src, nodeH = req_dst, rounds=rounds) + @process swapper() + end + end + end + @debug "RequestTracker @$(prot.node): Starting message wait at $(now(prot.sim)) with MessageBuffer containing: $(mb.buffer)" + @yield wait(mb) + @debug "RequestTracker @$(prot.node): Message wait ends at $(now(prot.sim))" + end +end + +include("utils.jl") + +""" +$TYPEDEF + +Protocol for the simulation of request traffic for a controller in a connection-oriented network for bipartite entanglement distribution. The requests are considered to be generated according to the Poisson model with rate λ, hence the inter-arrival time is +sampled from an exponential distribution. Physically, the request is generated at the source node(Alice) and is classically communicated to the node where the controller is located. Multiple `RequestGenerator`s can be instantiated for simulation with multiple +user pairs in the same network. + +$TYPEDFIELDS +""" +@kwdef struct RequestGenerator <: AbstractProtocol # TODO Should path_selection be a parameter here, so that it can be customized by the user? + """time-and-schedule-tracking instance from `ConcurrentSim`""" + sim::Simulation + """a network graph of registers""" + net::RegisterNet + """The source node(and the node where this protocol runs) of the user pair, commonly called Alice""" + src::Int + """The destination node, commonly called Bob""" + dst::Int + """The node at which the controller is located""" + controller::Int + """rate of arrival of requests/number of requests sent unit time""" + λ::Int = 3 +end + +function RequestGenerator(sim, net, src, dst, controller; kwargs...) + return RequestGenerator(;sim, net, src, dst, controller, kwargs...) +end + +@resumable function (prot::RequestGenerator)() + d = Exponential(inv(prot.λ)) # Parametrized with the scale which is inverse of the rate + mb = messagebuffer(prot.net, prot.src) + while true + msg = Tag(DistributionRequest, prot.src, prot.dst) + put!(channel(prot.net, prot.src=>prot.controller; permit_forward=true), msg) + + @yield timeout(prot.sim, rand(d)) + end +end + include("cutoff.jl") include("swapping.jl") +include("controllers.jl") include("switches.jl") using .Switches diff --git a/src/ProtocolZoo/controllers.jl b/src/ProtocolZoo/controllers.jl new file mode 100644 index 00000000..cf73f7a2 --- /dev/null +++ b/src/ProtocolZoo/controllers.jl @@ -0,0 +1,121 @@ +""" +$TYPEDEF + +A network control protocol that is connection oriented, non-distributed and centralized. The generation of +random requests is abstracted with picking a random path from all available paths in the arbitrary network +between Alice and Bob. The controller is located at one of the nodes in the network from where it messages all +the other nodes. + +$TYPEDFIELDS + +See also [`RequestTracker`](@ref) +""" +@kwdef struct NetController <: AbstractProtocol + """Time-and-schedule-tracking instance from `ConcurrentSim`""" + sim::Simulation + """A network graph of registers""" + net::RegisterNet + """The number of requests to be generated per cycle""" + n::Int + """The node in the network where the control protocol is physically located, ideally a centrally located node""" + node::Int + """duration of a single full cycle of entanglement generation and swapping along a specific path""" + ticktock::Float64 +end + +@resumable function (prot::NetController)() + paths = collect(Graphs.all_simple_paths(prot.net.graph, 1, 8)) + n_reg = length(prot.net.registers) + mb = messagebuffer(prot.net, prot.node) + while true + draw = (randperm(n_reg))[1:prot.n] + for i in 1:prot.n + path = paths[draw[i]] + @debug "Running Entanglement Distribution on path $(path) @ $(now(prot.sim))" + for i in 1:length(path)-1 + msg = Tag(EntanglementRequest, path[i], path[i+1], 1) + if prot.node == path[i] + put!(mb, msg) + else + put!(channel(prot.net, prot.node=>msg[2]; permit_forward=true), msg) + end + end + + for i in 2:length(path)-1 + msg = Tag(SwapRequest, path[i], 1) + if prot.node == path[i] + put!(mb, msg) + else + put!(channel(prot.net, prot.node=>msg[2];permit_forward=true), msg) + end + end + @yield timeout(prot.sim, prot.ticktock) + end + end +end + +""" +$TYPEDEF + +A network control protocol that is connection oriented, non-distributed and centralized. The controller is located at one of the nodes in the network from where it messages all +the other nodes' [`RequestTracker`](@ref) protocols when it receives [`DistributionRequest`](@ref) from the [`RequestGenerator`](@ref). + +$TYPEDFIELDS + +See also [`RequestGenerator`](@ref), [`RequestTracker`](@ref) +""" +@kwdef struct Controller <: AbstractProtocol + """Time-and-schedule-tracking instance from `ConcurrentSim`""" + sim::Simulation + """A network graph of registers""" + net::RegisterNet + """The node in the network where the control protocol is physically located, ideally a centrally located node""" + node::Int + """A matrix for the object containing physical graph metadata for the network""" + path_mat::Matrix{Union{Nothing, PathMetadata}} +end + +@resumable function (prot::Controller)() + mb = messagebuffer(prot.net, prot.node) + while true + workwasdone = true + while workwasdone + workwasdone = false + msg = querydelete!(mb, DistributionRequest, ❓, ❓) + if !isnothing(msg) + (msg_src, (_, src, dst)) = msg + if isnothing(prot.path_mat[src, dst]) + prot.path_mat[src, dst] = PathMetadata(prot.net.graph, src, dst, Int(length(prot.net[1].staterefs)/2)) + end + path_id = path_selection(prot.sim, prot.path_mat[src, dst]) + path = prot.path_mat[src, dst].paths[path_id] + if isnothing(path_id) + @debug "Request failed, all paths reserved" + end + + @debug "Running Entanglement Distribution on path $(path) @ $(now(prot.sim))" + for i in 1:length(path)-1 + msg = Tag(EntanglementRequest, path[i], path[i+1], 1) + if prot.node == path[i] + put!(mb, msg) + else + put!(channel(prot.net, prot.node=>msg[2]; permit_forward=true), msg) + end + end + + for i in 2:length(path)-1 + last = i == length(path) - 1 ? 1 : 0 + msg = Tag(SwapRequest, path[i], 1, path[i-1], path[i+1]) + if prot.node == path[i] + put!(mb, msg) + else + put!(channel(prot.net, prot.node=>msg[2];permit_forward=true), msg) + end + end + end + @debug "Controller @$(prot.node): Starting message wait at $(now(prot.sim)) with MessageBuffer containing: $(mb.buffer)" + @yield wait(mb) + @debug "Controller @$(prot.node): Message wait ends at $(now(prot.sim))" + end + end +end \ No newline at end of file diff --git a/src/ProtocolZoo/swapping.jl b/src/ProtocolZoo/swapping.jl index 948556b8..978fabe3 100644 --- a/src/ProtocolZoo/swapping.jl +++ b/src/ProtocolZoo/swapping.jl @@ -1,26 +1,3 @@ -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 diff --git a/src/ProtocolZoo/utils.jl b/src/ProtocolZoo/utils.jl new file mode 100644 index 00000000..c1e618b2 --- /dev/null +++ b/src/ProtocolZoo/utils.jl @@ -0,0 +1,87 @@ +""" +$TYPEDEF + +A struct containing the physical graph metadata for a network. The latest workload data is only available +at the node where the [`RequestGenerator`](@ref) runs, but every node has access to a copy for referencing paths based on indices +passed through the `DistributionRequest` tag/message. + +$TYPEDFIELDS +""" +@kwdef struct PathMetadata + """The vector of paths between the user pair""" + paths::Vector{Vector{Int}} + """The vector containing the workload information of a path""" + workloads::Dict{Int, Int} + """The number of slots available at each node. Scalar if all are same, otherwise a dictionary.""" + capacity::Union{Dict{Int, Int}} + """Number of failed requests due to high request traffic""" + failures::Ref{Int} +end + +function PathMetadata(graph::SimpleGraph{Int64}, src::Int, dst::Int, caps::Union{Dict{Int, Int}, Int}; failures=Ref{Int}(0)) + paths = sort(collect(all_simple_paths(graph, src, dst)); by = x->length(x)) + src = paths[1][1] + dst = paths[1][end] + workloads = Dict{Int, Int}() + capacity = isa(caps, Number) ? Dict{Int, Int}() : caps + for node in 1:size(graph)[1] + if !(node == src || node == dst) + workloads[node] = 0 + if isa(caps, Number) + capacity[node] = caps + end + end + end + PathMetadata(paths, workloads, capacity, failures) +end + + +""" +A simple path selection algorithm for connection oriented networks. +""" +function path_selection(sim, pathobj::PathMetadata) + for (ind, path) in pairs(pathobj.paths) + if all([pathobj.workloads[node] < pathobj.capacity[node] for node in path[2:end-1]]) + for node in path[2:end-1] + pathobj.capacity[node] += 1 + end + @process unreserve_path(sim, pathobj, ind) + return ind + end + end + pathobj.failures +=1 + return nothing +end + +@resumable function unreserve_path(sim, pathobj::PathMetadata, i) + @yield timeout(sim, 0.5) + @debug "Path $(pathobj.paths[i]) workload reduced" + for node in pathobj.paths[i][2:end-1] + pathobj.capacity[node] -= 1 + end +end + + +function random_index(arr) + return rand(keys(arr)) +end + +""" +Find a qubit pair in a register that is suitable for performing a swap by [`SwapperProt`](@ref) according to the given predicate and choosing functions, satisfying the agelimit(if any) of the qubits +""" +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) + ] + high_nodes = [ + n for n in queryall(reg, EntanglementCounterpart, pred_high, ❓; locked=false, assigned=true) + if isnothing(agelimit) || !isolderthan(n.slot, agelimit) + ] + + (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 \ No newline at end of file diff --git a/test/test_controlplane.jl b/test/test_controlplane.jl new file mode 100644 index 00000000..68064bc9 --- /dev/null +++ b/test/test_controlplane.jl @@ -0,0 +1,60 @@ +@testitem "Control Protocol" tags=[:controlplane] begin + +using QuantumSavory +using QuantumSavory.ProtocolZoo +using ConcurrentSim +using ResumableFunctions + +using Graphs + +if isinteractive() + using Logging + logger = ConsoleLogger(Logging.Warn; meta_formatter=(args...)->(:black,"","")) + global_logger(logger) + println("Logger set to debug") +end + +adjm = [0 1 0 0 1 0 0 0 + 1 0 1 0 0 0 0 0 + 0 1 0 1 0 1 0 0 + 0 0 1 0 0 0 1 1 + 1 0 0 0 0 1 0 1 + 0 0 1 0 1 0 1 0 + 0 0 0 1 0 1 0 1 + 0 0 0 1 1 0 1 0] +graph = SimpleGraph(adjm) + +regsize = 20 +net = RegisterNet(graph, [Register(regsize) for i in 1:8]) +sim = get_time_tracker(net) + +# controller +controller = Controller(sim, net, 6, fill(nothing, 8, 8)) +@process controller() + +# RequestGenerator for the user pair (1,8) +req_gen = RequestGenerator(sim, net, 1, 8, 6) +@process req_gen() + +# consumer +consumer = EntanglementConsumer(sim, net, 1, 8) +@process consumer() + +# entanglement and request trackers, cutoff protocol +for v in 1:8 + etracker = EntanglementTracker(sim, net, v) + rtracker = RequestTracker(sim, net, v) + cutoff = CutoffProt(sim, net, v) + @process etracker() + @process rtracker() + @process cutoff() +end + +run(sim, 1000) + +for i in 1:length(consumer.log) + @test consumer.log[i][2] ≈ 1.0 + @test consumer.log[i][3] ≈ 1.0 +end + +end \ No newline at end of file diff --git a/test/test_examples.jl b/test/test_examples.jl index e9bbba30..260a9828 100644 --- a/test/test_examples.jl +++ b/test/test_examples.jl @@ -41,7 +41,7 @@ end end end -@safetestset "repeatergrid" begin +@testitem "Examples - repeatergrid" tags=[:examples] begin if get(ENV, "QUANTUMSAVORY_PLOT_TEST","")=="true" include("setup_plotting.jl") include("../examples/repeatergrid/1a_async_interactive_visualization.jl") @@ -49,6 +49,13 @@ end end end +@testitem "Examples - controlplane" tags=[:examples] begin + if get(ENV, "QUANTUMSAVORY_PLOT_TEST","")=="true" + include("../examples/controlplane/1a_cdd_interactive.jl") + include("../examples/controlplane/2a_cnc_interactive.jl") + end +end + if get(ENV,"QUANTUMSAVORY_PLOT_TEST","")=="true" import GLMakie GLMakie.closeall() # to avoid errors when running headless