diff --git a/Project.toml b/Project.toml index febb764d..8d2aad07 100644 --- a/Project.toml +++ b/Project.toml @@ -29,7 +29,7 @@ Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" QuantumSavoryMakie = "Makie" [compat] -ConcurrentSim = "1.1" +ConcurrentSim = "1.4" DocStringExtensions = "0.9" Graphs = "1.7.3" IterTools = "1.4.0" diff --git a/src/QuantumSavory.jl b/src/QuantumSavory.jl index 9016928d..977265d1 100644 --- a/src/QuantumSavory.jl +++ b/src/QuantumSavory.jl @@ -14,6 +14,7 @@ export apply!, traceout!, removebackref! export project_traceout! #TODO should move to QuantumInterface import ConcurrentSim +using ResumableFunctions @reexport using QuantumSymbolics using QuantumSymbolics: @@ -29,6 +30,9 @@ export Qubit, Qumode, QuantumStateTrait, CliffordRepr, QuantumOpticsRepr, QuantumMCRepr, UseAsState, UseAsObservable, UseAsOperation, AbstractBackground +export QuantumChannel + + #TODO you can not assume you can always in-place modify a state. Have all these functions work on stateref, not stateref[] # basically all ::QuantumOptics... should be turned into ::Ref{...}... but an abstract ref @@ -330,6 +334,8 @@ include("concurrentsim.jl") include("plots.jl") +include("quantumchannel.jl") + include("CircuitZoo/CircuitZoo.jl") include("StatesZoo/StatesZoo.jl") diff --git a/src/baseops/subsystemcompose.jl b/src/baseops/subsystemcompose.jl index d3743d8b..1d44e3f2 100644 --- a/src/baseops/subsystemcompose.jl +++ b/src/baseops/subsystemcompose.jl @@ -6,10 +6,16 @@ nsubsystems(r::Register) = length(r.staterefs) nsubsystems(r::RegRef) = 1 nsubsystems(::Nothing) = 1 # TODO consider removing this and reworking the functions that depend on it. E.g., a reason to have it when performing a project_traceout measurement on a state that contains only one subsystem -function swap!(reg1::Register, reg2::Register, i1::Int, i2::Int) +function swap!(reg1::Register, reg2::Register, i1::Int, i2::Int; time=nothing) if reg1===reg2 && i1==i2 return end + if reg1.accesstimes[i1] != reg2.accesstimes[i2] + maxtime = max(reg1.accesstimes[i1], reg2.accesstimes[i2]) + maxtime = isnothing(time) ? maxtime : max(maxtime, time) + uptotime!(reg1[i1], maxtime) + uptotime!(reg2[i2], maxtime) + end state1, state2 = reg1.staterefs[i1], reg2.staterefs[i2] stateind1, stateind2 = reg1.stateindices[i1], reg2.stateindices[i2] reg1.staterefs[i1], reg2.staterefs[i2] = state2, state1 @@ -23,7 +29,7 @@ function swap!(reg1::Register, reg2::Register, i1::Int, i2::Int) state2.registerindices[stateind2] = i1 end end -swap!(r1::RegRef, r2::RegRef) = swap!(r1.reg, r2.reg, r1.idx, r2.idx) +swap!(r1::RegRef, r2::RegRef; time=nothing) = swap!(r1.reg, r2.reg, r1.idx, r2.idx; time) #subsystemcompose(s...) = reduce(subsystemcompose, s) diff --git a/src/baseops/uptotime.jl b/src/baseops/uptotime.jl index c2972e40..dbc4370a 100644 --- a/src/baseops/uptotime.jl +++ b/src/baseops/uptotime.jl @@ -34,7 +34,8 @@ end function uptotime!(registers, indices::Base.AbstractVecOrTuple{Int}, now) staterecords = [(state=r.staterefs[i], idx=r.stateindices[i], bg=r.backgrounds[i], t=r.accesstimes[i]) - for (r,i) in zip(registers, indices)] + for (r,i) in zip(registers, indices) + if isassigned(r,i)] for stategroup in groupby(x->x.state, staterecords) # TODO check this is grouping by ===... Actually, make sure that == for StateRef is the same as === state = stategroup[1].state timegroups = sort!(collect(groupby(x->x.t, stategroup)), by=x->x[1].t) diff --git a/src/concurrentsim.jl b/src/concurrentsim.jl index dd73b406..5bd4d279 100644 --- a/src/concurrentsim.jl +++ b/src/concurrentsim.jl @@ -3,7 +3,7 @@ import ConcurrentSim # Should be using using ConcurrentSim: Environment, request, release, now, active_process, timeout, Store, @process, Process, put, get using Printf -export @simlog, nongreedymultilock, spinlock, DelayChannel, get_time_tracker +export @simlog, nongreedymultilock, spinlock, get_time_tracker macro simlog(env, msg) # this should be part of @process or @resumable and just convert @info and company return :(@info(@sprintf("t=%.4f @ %05d : %s", now($(esc(env))), active_process($(esc(env))).bev.id, $(esc(msg))))) @@ -41,30 +41,6 @@ end end end -mutable struct DelayChannel{T} - delay::Float64 - store::Store{T} -end -function DelayChannel(env::Environment, delay::Float64) - return DelayChannel(delay, Store{Any}(env)) -end -function DelayChannel{T}(env::Environment, delay::Float64) where T - return DelayChannel(delay, Store{T}(env)) -end - -@resumable function latency(env::Environment, channel::DelayChannel, value) - @yield timeout(channel.store.env, channel.delay) - put(channel.store, value) -end - -function ConcurrentSim.put(channel::DelayChannel, value) # TODO rename to the ones from Base - @process latency(channel.store.env, channel, value) # results in the scheduling of all events generated by latency -end - -function ConcurrentSim.get(channel::DelayChannel) # TODO rename to the ones from Base - get(channel.store) # returns an element stored in the cable store -end - ## function get_time_tracker(rn::RegisterNet) diff --git a/src/quantumchannel.jl b/src/quantumchannel.jl new file mode 100644 index 00000000..dfb916a2 --- /dev/null +++ b/src/quantumchannel.jl @@ -0,0 +1,80 @@ +""" +Quantum channel for transmitting quantum states from one register to another. + +Delay and background noise processes are supported. + +The function `put!` is used to take the contents of a `RegRef` and put it in the channel. +That state can can then be received by a register (after a delay) using the `take!` method. + +```jldoctest; filter = r"(\\d{4})\\d+" => s"at some memory address" +julia> using QuantumSavory, ResumableFunctions, ConcurrentSim + +julia> regA = Register(1); regB = Register(1); + +julia> initialize!(regA[1], Z1); + +julia> sim = Simulation(); + +julia> qc = QuantumChannel(sim, 10.0) # a delay of 10 units +QuantumChannel(Qubit(), DelayQueue{Register}(QueueStore{Register, Int64}, 10.0), nothing) + +julia> @resumable function alice_node(env, qc) + println("Putting Alice's qubit in the channel at ", now(env)) + put!(qc, regA[1]) + end +alice_node (generic function with 1 method) + +julia> @resumable function bob_node(env, qc) + @yield take!(qc, regB[1]) + println("Taking the qubit from alice at ", now(env)) + end +bob_node (generic function with 1 method) + +julia> @process alice_node(sim, qc); @process bob_node(sim, qc); + +julia> run(sim) +Putting Alice's qubit in the channel at 0.0 +Taking the qubit from alice at 10.0 + +julia> regA +Register with 1 slots: [ Qubit ] + Slots: + nothing + +julia> regB +Register with 1 slots: [ Qubit ] +Slots: + Subsystem 1 of QuantumOpticsBase.Ket 7474956998997307987 +``` +""" +struct QuantumChannel{T} + trait::T + queue::ConcurrentSim.DelayQueue{Register} + background::Any +end + +QuantumChannel(queue::ConcurrentSim.DelayQueue{Register}, background=nothing, trait=Qubit()) = QuantumChannel(trait, queue, background) + +QuantumChannel(env::ConcurrentSim.Simulation, delay, background=nothing, trait=Qubit()) = QuantumChannel(ConcurrentSim.DelayQueue{Register}(env, delay), background, trait) +Register(qc::QuantumChannel) = Register([qc.trait], [qc.background]) + +function Base.put!(qc::QuantumChannel, rref::RegRef) + time = ConcurrentSim.now(qc.queue.store.env) + channel_reg = Register(qc) + swap!(rref, channel_reg[1]; time) + uptotime!(channel_reg[1], time+qc.queue.delay) + put!(qc.queue, channel_reg) +end + +@resumable function post_take(env, take_event, rref) + channel_reg = @yield take_event + if isassigned(rref) + error("A take! operation is being performed on a QuantumChannel in order to swap the state into a Register, but the target register slot is not empty (it is already initialized).") + end + swap!(channel_reg[1], rref; time=now(env)) +end + +function Base.take!(qc::QuantumChannel, rref::RegRef) + take_event = take!(qc.queue) + @process post_take(qc.queue.store.env, take_event, rref) +end diff --git a/test/Project.toml b/test/Project.toml index 79427e19..ce51c2e1 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -9,6 +9,7 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" diff --git a/test/runtests.jl b/test/runtests.jl index 21af6f0d..d23dd519 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -23,14 +23,19 @@ end println("Starting tests with $(Threads.nthreads()) threads out of `Sys.CPU_THREADS = $(Sys.CPU_THREADS)`...") +@doset "quantumchannel" @doset "register_interface" @doset "project_traceout" @doset "observable" @doset "noninstant_and_backgrounds_qubit" @doset "noninstant_and_backgrounds_qumode" -@doset "circuitzoo_superdense" + +@doset "circuitzoo_api" @doset "circuitzoo_purification" +@doset "circuitzoo_superdense" + @doset "stateszoo_api" + @doset "examples" get(ENV,"QUANTUMSAVORY_PLOT_TEST","")=="true" && @doset "plotting_cairo" get(ENV,"QUANTUMSAVORY_PLOT_TEST","")=="true" && @doset "plotting_gl" diff --git a/test/test_circuitzoo_api.jl b/test/test_circuitzoo_api.jl index 72cad647..ff993421 100644 --- a/test/test_circuitzoo_api.jl +++ b/test/test_circuitzoo_api.jl @@ -9,5 +9,7 @@ for T in subtypes(AbstractCircuit) ms = methods(circ) @test length(ms) == 1 # this can be relaxed one day, but for now it can check we are not doing weird stuff m = first(ms) - @test m.isva || inputqubits(circ) == m.nargs-1 + if hasmethod(inputqubits, Tuple{T}) # TODO should all of them have this method? + @test m.isva || inputqubits(circ) == m.nargs-1 + end end diff --git a/test/test_quantumchannel.jl b/test/test_quantumchannel.jl new file mode 100644 index 00000000..84df3895 --- /dev/null +++ b/test/test_quantumchannel.jl @@ -0,0 +1,111 @@ +using QuantumSavory +using ResumableFunctions +using ConcurrentSim +using Test + +bell = (Z1⊗Z1 + Z2⊗Z2)/sqrt(2.0) + +## Manually construct a QuantumChannel and test a simple put/take + +sim = Simulation() +regA = Register(1) +regB = Register(2) +initialize!((regA[1], regB[2]), bell) +# Delay queue for quantum channel +queue = DelayQueue{Register}(sim, 10.0) +qc = QuantumChannel(queue) + +@resumable function alice_node(env, qc) + put!(qc, regA[1]) +end + +@resumable function bob_node(env, qc) + @yield take!(qc, regB[1]) +end + +@process alice_node(sim, qc) +@process bob_node(sim, qc) + +run(sim) + +# the above code puts both the qubits of the state in the same register +sref = regB.staterefs[1] +@test sref.registers[1] == sref.registers[2] +@test !isassigned(regA, 1) + +## Test with the second constructor + +regA = Register(1) +regB = Register(2) +initialize!((regA[1], regB[2]), bell) +sim = Simulation() +qc = QuantumChannel(sim, 10.0) +@resumable function alice_node(env, qc) + put!(qc, regA[1]) +end +@resumable function bob_node(env, qc) + @yield take!(qc, regB[1]) +end +@process alice_node(sim, qc) +@process bob_node(sim, qc) +run(sim) +sref = regB.staterefs[1] +@test sref.registers[1] == sref.registers[2] +@test !isassigned(regA, 1) + +## Test with T1Decay + +regA = Register(1) +regB = Register(2) +initialize!((regA[1], regB[2]), bell) +sim = Simulation() +qc = QuantumChannel(sim, 10.0, T1Decay(0.1)) +@resumable function alice_node(env, qc) + put!(qc, regA[1]) +end +@resumable function bob_node(env, qc) + @yield take!(qc, regB[1]) +end +@process alice_node(sim, qc) +@process bob_node(sim, qc) +run(sim) + +# compare against a stationary qubit experiencing the same T1 decay +reg = Register([Qubit(), Qubit()], [T1Decay(0.1), nothing]) +initialize!(reg[1:2], bell) +uptotime!(reg[1], 10.0) + +@test observable(reg[1:2], projector(bell)) ≈ observable(regB[1:2], projector(bell)) + +## Test with T2Dephasing + +regA = Register(2) +regB = Register(2) +initialize!((regA[1], regB[2]), bell) +sim = Simulation() +qc = QuantumChannel(sim, 10.0, T2Dephasing(0.1)) +@resumable function alice_node(env, qc) + put!(qc, regA[1]) +end +@resumable function bob_node(env, qc) + @yield take!(qc, regB[1]) +end +@process alice_node(sim, qc) +@process bob_node(sim, qc) +run(sim) + +reg = Register([Qubit(), Qubit()], [T2Dephasing(0.1), nothing]) +initialize!(reg[1:2], bell) +uptotime!(reg[1], 10.0) + +@test observable(reg[1:2], projector(bell)) == observable(regB[1:2], projector(bell)) + +## Test for slot availability + +sim = Simulation() +qc = QuantumChannel(sim, 10.0, T2Dephasing(0.1)) +regC = Register(1) +initialize!(regC[1], Z1) +put!(qc, regC[1]) +take!(qc, regB[1]) +@test_throws "A take! operation is being performed on a QuantumChannel in order to swap the state into a Register, but the target register slot is not empty (it is already initialized)." run(sim)