From 27dc6dcfbae46daa0980a8762b753c02be2077b8 Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Mon, 2 Feb 2026 12:14:28 -0500 Subject: [PATCH 01/16] Add Makie Bloch sphere extension using Ket and update Project.toml --- Project.toml | 10 ++++++ ext/QuantumOpticsBaseMakieExt.jl | 60 ++++++++++++++++++++++++++++++++ src/QuantumOpticsBase.jl | 8 +++++ 3 files changed, 78 insertions(+) create mode 100644 ext/QuantumOpticsBaseMakieExt.jl diff --git a/Project.toml b/Project.toml index cd89448f..c43567c8 100644 --- a/Project.toml +++ b/Project.toml @@ -8,8 +8,10 @@ FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" FastExpm = "7868e603-8603-432e-a1a1-694bd70b01f2" FastGaussQuadrature = "442a2c76-b920-505d-bb47-c5924d526838" FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" QuantumInterface = "5717a53b-5d69-4fa3-b976-0bf2f97ca1e5" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" @@ -17,6 +19,13 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Strided = "5e0ebb24-38b0-5f93-81fe-25c709ecae67" UnsafeArrays = "c4a57d5a-5b31-53a6-b365-19f8c011fbd6" +[weakdeps] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" + +[extensions] +QuantumOpticsBaseMakieExt = ["Makie"] + [compat] Adapt = "1, 2, 3.3, 4" FFTW = "1.2" @@ -25,6 +34,7 @@ FastGaussQuadrature = "0.5, 1" FillArrays = "0.13, 1" LRUCache = "1" LinearAlgebra = "1" +Makie = "0.24.7" QuantumInterface = "0.4" Random = "1" RecursiveArrayTools = "3" diff --git a/ext/QuantumOpticsBaseMakieExt.jl b/ext/QuantumOpticsBaseMakieExt.jl new file mode 100644 index 00000000..678e8e9c --- /dev/null +++ b/ext/QuantumOpticsBaseMakieExt.jl @@ -0,0 +1,60 @@ +module QuantumOpticsBaseMakieExt + +import QuantumOpticsBase: blochsphere, Ket +using Makie +using GeometryBasics +using LinearAlgebra: normalize + +# Configure axis visibility safely +function configure_axis!(ax, showaxes::Bool) + if !showaxes + hidexdecorations!(ax) + hideydecorations!(ax) + hidezdecorations!(ax) + end +end + +function blochsphere(state::Ket; arrowcolor=:red, spherealpha=0.35, showaxes=true) + length(state.data) == 2 || error("Bloch sphere only supports spin-1/2 states") + + # Compute Bloch vector + α, β = state.data + x = 2 * real(conj(α) * β) + y = 2 * imag(conj(α) * β) + z = abs2(α) - abs2(β) + blochvec = Vec3f(x, y, z) + + origin = Point3f(0,0,0) + + # Axes + state vector + dirs = [ + Vec3f(1,0,0), + Vec3f(0,1,0), + Vec3f(0,0,1), + blochvec + ] + tips = [Point3f(origin .+ d) for d in dirs] + + # Figure & axis + f = Figure() + ax = Axis3(f[1,1]; title="Bloch Sphere", aspect=:data) + configure_axis!(ax, showaxes) + + # Sphere + mesh!(ax, Sphere(origin, 1f0); color=:white, alpha=spherealpha, transparency=true) + + # Draw arrows + for (tail, tip) in zip(fill(origin, length(dirs)), tips) + arrows3d!(ax, [tail], [tip]; + shaftradius=0.02, + tipradius=0.06, + tiplength=0.1, + color = tip == tips[end] ? arrowcolor : :black) + end + + limits!(ax, -1.2,1.2,-1.2,1.2,-1.2,1.2) + + return f +end + +end # module diff --git a/src/QuantumOpticsBase.jl b/src/QuantumOpticsBase.jl index d3669efa..243f97b1 100644 --- a/src/QuantumOpticsBase.jl +++ b/src/QuantumOpticsBase.jl @@ -74,6 +74,14 @@ export Basis, GenericBasis, CompositeBasis, basis, #apply apply! + ########### Bloch Sphere API Stub ########### + +function blochsphere(state) +error("Blochsphere visualization requires a Makie extension.\n" * + "Load QuantumOpticsBase together with CairoMakie or GLMakie.") +end + + include("bases.jl") include("states.jl") include("operators.jl") From 52258f4c0cca74f5faa0e921e80f6d7ced0057c8 Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Mon, 2 Feb 2026 12:27:14 -0500 Subject: [PATCH 02/16] Add Makie Bloch sphere extension using Ket --- ~/.julia/dev/QuantumOpticsBase/Project.toml | 1 + 1 file changed, 1 insertion(+) create mode 100644 ~/.julia/dev/QuantumOpticsBase/Project.toml diff --git a/~/.julia/dev/QuantumOpticsBase/Project.toml b/~/.julia/dev/QuantumOpticsBase/Project.toml new file mode 100644 index 00000000..81648c0b --- /dev/null +++ b/~/.julia/dev/QuantumOpticsBase/Project.toml @@ -0,0 +1 @@ +[deps] From 041567bf221a0abd71b0eb7a8534ab02e28e3ec0 Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Wed, 4 Mar 2026 17:01:21 -0500 Subject: [PATCH 03/16] Made requested changes to Bloch sphere extension --- Project.toml | 9 +-- ext/QuantumOpticsBaseMakieExt.jl | 133 ++++++++++++++++++++----------- src/QuantumOpticsBase.jl | 2 +- 3 files changed, 92 insertions(+), 52 deletions(-) diff --git a/Project.toml b/Project.toml index c43567c8..a9ea3abd 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ version = "0.5.9" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" FastExpm = "7868e603-8603-432e-a1a1-694bd70b01f2" FastGaussQuadrature = "442a2c76-b920-505d-bb47-c5924d526838" @@ -19,19 +20,17 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Strided = "5e0ebb24-38b0-5f93-81fe-25c709ecae67" UnsafeArrays = "c4a57d5a-5b31-53a6-b365-19f8c011fbd6" -[weakdeps] -CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" -GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" - [extensions] -QuantumOpticsBaseMakieExt = ["Makie"] +QuantumOpticsBaseMakieExt = ["Makie", "GeometryBasics"] [compat] Adapt = "1, 2, 3.3, 4" +CairoMakie = "0.15.8" FFTW = "1.2" FastExpm = "1.1.0" FastGaussQuadrature = "0.5, 1" FillArrays = "0.13, 1" +GeometryBasics = "0.5.10" LRUCache = "1" LinearAlgebra = "1" Makie = "0.24.7" diff --git a/ext/QuantumOpticsBaseMakieExt.jl b/ext/QuantumOpticsBaseMakieExt.jl index 678e8e9c..ee766a81 100644 --- a/ext/QuantumOpticsBaseMakieExt.jl +++ b/ext/QuantumOpticsBaseMakieExt.jl @@ -1,60 +1,101 @@ module QuantumOpticsBaseMakieExt +import QuantumOpticsBase +import QuantumOpticsBase: Ket +import Makie +import Makie: convert_arguments +using Makie: Figure, @recipe, Attributes, Axis3, mesh!, arrows3d!, hidexdecorations!, hideydecorations!, hidezdecorations! +using GeometryBasics: Point3f, Vec3f, Sphere -import QuantumOpticsBase: blochsphere, Ket -using Makie -using GeometryBasics -using LinearAlgebra: normalize - -# Configure axis visibility safely -function configure_axis!(ax, showaxes::Bool) - if !showaxes - hidexdecorations!(ax) - hideydecorations!(ax) - hidezdecorations!(ax) - end +@recipe(BlochSpherePlot, state) do scene + Attributes( + arrowcolor = :red, + spherealpha = 0.30, + showaxes = true, + title = "Bloch Sphere", + shaftradius = 0.018, + tipradius = 0.050, + tiplength = 0.10, + lim = 1.2, + ) end -function blochsphere(state::Ket; arrowcolor=:red, spherealpha=0.35, showaxes=true) - length(state.data) == 2 || error("Bloch sphere only supports spin-1/2 states") +function convert_arguments(::Type{<:Makie.Plot{blochsphereplot}}, state::Ket) + return (state,) +end - # Compute Bloch vector - α, β = state.data - x = 2 * real(conj(α) * β) - y = 2 * imag(conj(α) * β) - z = abs2(α) - abs2(β) - blochvec = Vec3f(x, y, z) - origin = Point3f(0,0,0) +# Walk up the parent chain until we find an Axis3 (or give up). +function _parent_axis3(x) + cur = x + for _ in 1:10 + cur isa Axis3 && return cur + cur = Makie.parent(cur) + cur === nothing && break + end + return nothing +end - # Axes + state vector - dirs = [ - Vec3f(1,0,0), - Vec3f(0,1,0), - Vec3f(0,0,1), - blochvec - ] - tips = [Point3f(origin .+ d) for d in dirs] +function Makie.plot!(p::BlochSpherePlot) + state_obs = p[1] # Observable{Ket} - # Figure & axis - f = Figure() - ax = Axis3(f[1,1]; title="Bloch Sphere", aspect=:data) - configure_axis!(ax, showaxes) + origin = Point3f(0, 0, 0) - # Sphere - mesh!(ax, Sphere(origin, 1f0); color=:white, alpha=spherealpha, transparency=true) - - # Draw arrows - for (tail, tip) in zip(fill(origin, length(dirs)), tips) - arrows3d!(ax, [tail], [tip]; - shaftradius=0.02, - tipradius=0.06, - tiplength=0.1, - color = tip == tips[end] ? arrowcolor : :black) + # Bloch vector as an Observable (reactive) + blochvec = Makie.@lift begin + s = $state_obs + length(s.data) == 2 || error("BlochSphere only supports spin-1/2 states (2 amplitudes)") + α, β = s.data + x = 2 * real(conj(α) * β) + y = 2 * imag(conj(α) * β) + z = abs2(α) - abs2(β) + Vec3f(x, y, z) end - limits!(ax, -1.2,1.2,-1.2,1.2,-1.2,1.2) + # Sphere + mesh!(p, Sphere(origin, 1f0); + color = :white, + alpha = p[:spherealpha], + transparency = true, + shading = true + ) + + # Axes + state vector arrows (dirs/colors reactive where needed) + dirs = Makie.@lift Vec3f[Vec3f(1,0,0), Vec3f(0,1,0), Vec3f(0,0,1), $blochvec] + tails = fill(origin, 4) - return f + colors = Makie.@lift Any[:black, :black, :black, $(p[:arrowcolor])] + + arrows3d!(p, tails, dirs; + shaftradius = p[:shaftradius], + tipradius = p[:tipradius], + tiplength = p[:tiplength], + color = colors + ) + + ax = _parent_axis3(p) + if ax !== nothing + ax.aspect = (1,1,1) + lim = p[:lim][] + Makie.limits!(ax, -lim, lim, -lim, lim, -lim, lim) + ax.title = p[:title][] + + if !p[:showaxes][] + hidexdecorations!(ax) + hideydecorations!(ax) + hidezdecorations!(ax) + end + end + + return p +end +function QuantumOpticsBase.blochsphere(state::Ket; kwargs...) + fig = Figure(size = (700, 700)) + ax = Axis3(fig[1,1]; + aspect = :data, + viewmode = :fit + ) + blochsphereplot!(ax, state; kwargs...) + return fig end -end # module +end diff --git a/src/QuantumOpticsBase.jl b/src/QuantumOpticsBase.jl index 243f97b1..77d3cfed 100644 --- a/src/QuantumOpticsBase.jl +++ b/src/QuantumOpticsBase.jl @@ -70,7 +70,7 @@ export Basis, GenericBasis, CompositeBasis, basis, PauliBasis, PauliTransferMatrix, DensePauliTransferMatrix, ChiMatrix, DenseChiMatrix, avg_gate_fidelity, SumBasis, directsum, ⊕, LazyDirectSum, getblock, setblock!, - qfunc, wigner, coherentspinstate, qfuncsu2, wignersu2 + qfunc, wigner, coherentspinstate, qfuncsu2, wignersu2, blochsphere #apply apply! From 5cacd7e3946e6e76d2639609cfadf7d2228f7ef9 Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Thu, 30 Apr 2026 14:29:24 -0400 Subject: [PATCH 04/16] Update Bloch sphere extension and add TestItemRunner tests --- Project.toml | 16 +-- ext/QuantumOpticsBaseMakieExt.jl | 184 ++++++++++++++++++++----------- test/Project.toml | 1 + test/test_plotting.jl | 177 +++++++++++++++++++++++++++++ 4 files changed, 306 insertions(+), 72 deletions(-) create mode 100644 test/test_plotting.jl diff --git a/Project.toml b/Project.toml index a9ea3abd..9e8bee5d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,10 +1,9 @@ name = "QuantumOpticsBase" uuid = "4f57444f-1401-5e15-980d-4471b28d5678" version = "0.5.9" - + [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" -CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" FastExpm = "7868e603-8603-432e-a1a1-694bd70b01f2" FastGaussQuadrature = "442a2c76-b920-505d-bb47-c5924d526838" @@ -12,7 +11,6 @@ FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" QuantumInterface = "5717a53b-5d69-4fa3-b976-0bf2f97ca1e5" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" @@ -20,12 +18,14 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Strided = "5e0ebb24-38b0-5f93-81fe-25c709ecae67" UnsafeArrays = "c4a57d5a-5b31-53a6-b365-19f8c011fbd6" +[weakdeps] +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" + [extensions] -QuantumOpticsBaseMakieExt = ["Makie", "GeometryBasics"] - +QuantumOpticsBaseMakieExt = ["Makie"] + [compat] Adapt = "1, 2, 3.3, 4" -CairoMakie = "0.15.8" FFTW = "1.2" FastExpm = "1.1.0" FastGaussQuadrature = "0.5, 1" @@ -33,11 +33,11 @@ FillArrays = "0.13, 1" GeometryBasics = "0.5.10" LRUCache = "1" LinearAlgebra = "1" -Makie = "0.24.7" +Makie = "0.24" QuantumInterface = "0.4" Random = "1" RecursiveArrayTools = "3" SparseArrays = "1" Strided = "1, 2" UnsafeArrays = "1" -julia = "1.10" +julia = "1.10" \ No newline at end of file diff --git a/ext/QuantumOpticsBaseMakieExt.jl b/ext/QuantumOpticsBaseMakieExt.jl index ee766a81..01f32811 100644 --- a/ext/QuantumOpticsBaseMakieExt.jl +++ b/ext/QuantumOpticsBaseMakieExt.jl @@ -2,100 +2,156 @@ module QuantumOpticsBaseMakieExt import QuantumOpticsBase import QuantumOpticsBase: Ket import Makie -import Makie: convert_arguments -using Makie: Figure, @recipe, Attributes, Axis3, mesh!, arrows3d!, hidexdecorations!, hideydecorations!, hidezdecorations! -using GeometryBasics: Point3f, Vec3f, Sphere +using Makie: Figure, @recipe, Attributes, Axis3 +using Makie: surface!, arrows3d!, lines!, text!, meshscatter! +using Makie: Point3f, Vec3f +export blochsphereplot, blochsphereplot! + @recipe(BlochSpherePlot, state) do scene Attributes( - arrowcolor = :red, - spherealpha = 0.30, - showaxes = true, - title = "Bloch Sphere", - shaftradius = 0.018, - tipradius = 0.050, - tiplength = 0.10, - lim = 1.2, + arrowcolor = :red, + spherecolor = :lightblue, + spherealpha = 0.15, + showwireframe = true, + showaxes = true, + showlabels = true, + labelsize = 18, + title = "Bloch Sphere", + shaftradius = 0.018, + tipradius = 0.050, + tiplength = 0.10, + lim = 1.6, ) end - -function convert_arguments(::Type{<:Makie.Plot{blochsphereplot}}, state::Ket) + +function Makie.convert_arguments(::Type{<:Makie.Plot{blochsphereplot}}, state::Ket) return (state,) end + - -# Walk up the parent chain until we find an Axis3 (or give up). -function _parent_axis3(x) - cur = x - for _ in 1:10 - cur isa Axis3 && return cur - cur = Makie.parent(cur) - cur === nothing && break - end - return nothing -end - + function Makie.plot!(p::BlochSpherePlot) - state_obs = p[1] # Observable{Ket} - - origin = Point3f(0, 0, 0) - - # Bloch vector as an Observable (reactive) + state_obs = p[1] + blochvec = Makie.@lift begin s = $state_obs - length(s.data) == 2 || error("BlochSphere only supports spin-1/2 states (2 amplitudes)") + length(s.data) == 2 || + error("BlochSphere requires a 2-level (spin-1/2) state") α, β = s.data - x = 2 * real(conj(α) * β) - y = 2 * imag(conj(α) * β) - z = abs2(α) - abs2(β) - Vec3f(x, y, z) + Vec3f( + Float32(2 * real(conj(α) * β)), + Float32(2 * imag(conj(α) * β)), + Float32(abs2(α) - abs2(β)), + ) end + + let npts = 200 + θ = LinRange(0f0, 2f0π, npts) + φ = LinRange(0f0, Float32(π), npts) + xs = Float32[cos(t) * sin(q) for t in θ, q in φ] + ys = Float32[sin(t) * sin(q) for t in θ, q in φ] + zs = Float32[cos(q) for _ in θ, q in φ] + c = Makie.to_color(p[:spherecolor][]) + α = Float32(p[:spherealpha][]) + rgba = Makie.RGBAf(Makie.red(c), Makie.green(c), Makie.blue(c), α) + surface!(p, xs, ys, zs; + color = fill(rgba, npts, npts), + transparency = true, - # Sphere - mesh!(p, Sphere(origin, 1f0); - color = :white, - alpha = p[:spherealpha], - transparency = true, - shading = true - ) - # Axes + state vector arrows (dirs/colors reactive where needed) - dirs = Makie.@lift Vec3f[Vec3f(1,0,0), Vec3f(0,1,0), Vec3f(0,0,1), $blochvec] - tails = fill(origin, 4) + ) + end + - colors = Makie.@lift Any[:black, :black, :black, $(p[:arrowcolor])] + if p[:showwireframe][] + ncirc = 120 + θc = LinRange(0f0, 2f0π, ncirc) + for pts in ( + [Point3f( cos(t), sin(t), 0f0) for t in θc], + [Point3f( cos(t), 0f0, sin(t)) for t in θc], + [Point3f(0f0, cos(t), sin(t)) for t in θc], + ) + lines!(p, pts; color = (:black, 0.55), linewidth = 1.2) + end + end + - arrows3d!(p, tails, dirs; + if p[:showaxes][] + r = 1.18f0 + for (a, b) in ( + (Point3f(-r, 0, 0), Point3f(r, 0, 0)), + (Point3f(0, -r, 0), Point3f(0, r, 0)), + (Point3f(0, 0, -r), Point3f(0, 0, r)), + ) + lines!(p, [a, b]; color = :black, linewidth = 1, linestyle = :dash) + end + end + + arrows3d!(p, + [Point3f(0, 0, 0)], + Makie.@lift([$blochvec]); shaftradius = p[:shaftradius], tipradius = p[:tipradius], tiplength = p[:tiplength], - color = colors + color = p[:arrowcolor], ) + + meshscatter!(p, + Makie.@lift([Point3f($blochvec)]); + color = p[:arrowcolor], + markersize = 0.06, + ) + - ax = _parent_axis3(p) - if ax !== nothing - ax.aspect = (1,1,1) - lim = p[:lim][] - Makie.limits!(ax, -lim, lim, -lim, lim, -lim, lim) - ax.title = p[:title][] - - if !p[:showaxes][] - hidexdecorations!(ax) - hideydecorations!(ax) - hidezdecorations!(ax) + if p[:showlabels][] + ls = p[:labelsize][] + off = 1.40f0 + for (pos, lbl, align) in ( + (Point3f( 0f0, 0f0, off), "|0⟩", (:center, :bottom)), + (Point3f( 0f0, 0f0, -off), "|1⟩", (:center, :top )), + (Point3f( off, 0f0, 0f0), "|+⟩", (:left, :center)), + (Point3f(-off, 0f0, 0f0), "|-⟩", (:right, :center)), + (Point3f( 0f0, off, 0f0), "|+i⟩", (:left, :center)), + (Point3f( 0f0, -off, 0f0), "|-i⟩", (:right, :center)), + ) + text!(p, pos; text = lbl, fontsize = ls, align = align) end end - + return p end + + + function QuantumOpticsBase.blochsphere(state::Ket; kwargs...) fig = Figure(size = (700, 700)) - ax = Axis3(fig[1,1]; + ax = Axis3(fig[1, 1]; aspect = :data, - viewmode = :fit + viewmode = :fit, + xticksvisible = false, + yticksvisible = false, + zticksvisible = false, + xticklabelsvisible = false, + yticklabelsvisible = false, + zticklabelsvisible = false, + xlabelvisible = false, + ylabelvisible = false, + zlabelvisible = false, + xspinesvisible = false, + yspinesvisible = false, + zspinesvisible = false, + xgridvisible = false, + ygridvisible = false, + zgridvisible = false, + xypanelvisible = false, + xzpanelvisible = false, + yzpanelvisible = false, ) + lim = Float32(get(kwargs, :lim, 1.6)) + Makie.limits!(ax, -lim, lim, -lim, lim, -lim, lim) blochsphereplot!(ax, state; kwargs...) return fig end - -end + +end \ No newline at end of file diff --git a/test/Project.toml b/test/Project.toml index a89d1bfb..7b9df2fa 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,6 +1,7 @@ [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" diff --git a/test/test_plotting.jl b/test/test_plotting.jl new file mode 100644 index 00000000..318fc6f2 --- /dev/null +++ b/test/test_plotting.jl @@ -0,0 +1,177 @@ +@testitem "Bloch Sphere Plotting" tags=[:plotting] begin + using QuantumOpticsBase + using CairoMakie + + b = SpinBasis(1//2) + + + function bloch_vector(ψ::Ket) + α, β = ψ.data + x = 2 * real(conj(α) * β) + y = 2 * imag(conj(α) * β) + z = abs2(α) - abs2(β) + return (x, y, z) + end + + + @testset "blochsphere returns a Figure" begin + @test blochsphere(spinup(b)) isa Figure + end + + @testset "blochsphereplot! returns a Plot" begin + fig = Figure() + ax = Axis3(fig[1, 1]) + @test blochsphereplot!(ax, spinup(b)) isa Makie.Plot + end + + + @testset "Bloch vector |0⟩ north pole" begin + x, y, z = bloch_vector(spinup(b)) + @test x ≈ 0 atol=1e-10 + @test y ≈ 0 atol=1e-10 + @test z ≈ 1 atol=1e-10 + end + + @testset "Bloch vector |1⟩ south pole" begin + x, y, z = bloch_vector(spindown(b)) + @test x ≈ 0 atol=1e-10 + @test y ≈ 0 atol=1e-10 + @test z ≈ -1 atol=1e-10 + end + + @testset "Bloch vector |+⟩ +x pole" begin + x, y, z = bloch_vector(normalize(spinup(b) + spindown(b))) + @test x ≈ 1 atol=1e-10 + @test y ≈ 0 atol=1e-10 + @test z ≈ 0 atol=1e-10 + end + + @testset "Bloch vector |-⟩ -x pole" begin + x, y, z = bloch_vector(normalize(spinup(b) - spindown(b))) + @test x ≈ -1 atol=1e-10 + @test y ≈ 0 atol=1e-10 + @test z ≈ 0 atol=1e-10 + end + + @testset "Bloch vector |+i⟩ +y pole" begin + x, y, z = bloch_vector(normalize(spinup(b) + im*spindown(b))) + @test x ≈ 0 atol=1e-10 + @test y ≈ 1 atol=1e-10 + @test z ≈ 0 atol=1e-10 + end + + @testset "Bloch vector |-i⟩ -y pole" begin + x, y, z = bloch_vector(normalize(spinup(b) - im*spindown(b))) + @test x ≈ 0 atol=1e-10 + @test y ≈ -1 atol=1e-10 + @test z ≈ 0 atol=1e-10 + end + + + @testset "Bloch vector matches θ/φ parameterisation" begin + for (θ, φ) in [(π/3, π/4), (π/2, π/3), (2π/3, 3π/4), (π/4, 7π/6)] + ψ = cos(θ/2)*spinup(b) + exp(im*φ)*sin(θ/2)*spindown(b) + x, y, z = bloch_vector(ψ) + @test x ≈ sin(θ)*cos(φ) atol=1e-10 + @test y ≈ sin(θ)*sin(φ) atol=1e-10 + @test z ≈ cos(θ) atol=1e-10 + end + end + + @testset "Bloch vector has unit length for any pure state" begin + for (θ, φ) in [(0.0, 0.0), (π/4, π/3), (π/2, 0.0), (π/2, π), (π, 0.0)] + ψ = cos(θ/2)*spinup(b) + exp(im*φ)*sin(θ/2)*spindown(b) + x, y, z = bloch_vector(ψ) + @test x^2 + y^2 + z^2 ≈ 1 atol=1e-10 + end + end + + + @testset "|0⟩ renders without error" begin + fig = blochsphere(spinup(b)) + save("test_bloch_0.png", fig) + @test isfile("test_bloch_0.png") + rm("test_bloch_0.png") + end + + @testset "|1⟩ renders without error" begin + fig = blochsphere(spindown(b)) + save("test_bloch_1.png", fig) + @test isfile("test_bloch_1.png") + rm("test_bloch_1.png") + end + + @testset "|+⟩ renders without error" begin + fig = blochsphere(normalize(spinup(b) + spindown(b))) + save("test_bloch_plus.png", fig) + @test isfile("test_bloch_plus.png") + rm("test_bloch_plus.png") + end + + @testset "|-⟩ renders without error" begin + fig = blochsphere(normalize(spinup(b) - spindown(b))) + save("test_bloch_minus.png", fig) + @test isfile("test_bloch_minus.png") + rm("test_bloch_minus.png") + end + + @testset "|+i⟩ renders without error" begin + fig = blochsphere(normalize(spinup(b) + im*spindown(b))) + save("test_bloch_plusi.png", fig) + @test isfile("test_bloch_plusi.png") + rm("test_bloch_plusi.png") + end + + @testset "|-i⟩ renders without error" begin + fig = blochsphere(normalize(spinup(b) - im*spindown(b))) + save("test_bloch_minusi.png", fig) + @test isfile("test_bloch_minusi.png") + rm("test_bloch_minusi.png") + end + + + @testset "Arbitrary state θ=π/3 φ=π/4 renders without error" begin + ψ = cos(π/6)*spinup(b) + exp(im*π/4)*sin(π/6)*spindown(b) + fig = blochsphere(ψ) + save("test_bloch_arb1.png", fig) + @test isfile("test_bloch_arb1.png") + rm("test_bloch_arb1.png") + end + + @testset "Arbitrary state θ=π/2 φ=π/3 renders without error" begin + ψ = cos(π/4)*spinup(b) + exp(im*π/3)*sin(π/4)*spindown(b) + fig = blochsphere(ψ) + save("test_bloch_arb2.png", fig) + @test isfile("test_bloch_arb2.png") + rm("test_bloch_arb2.png") + end + + + @testset "Custom arrowcolor and spherealpha" begin + fig = blochsphere(spinup(b); arrowcolor=:blue, spherealpha=0.3) + save("test_bloch_custom_color.png", fig) + @test isfile("test_bloch_custom_color.png") + rm("test_bloch_custom_color.png") + end + + @testset "Custom spherecolor" begin + fig = blochsphere(spindown(b); spherecolor=:pink, spherealpha=0.2) + save("test_bloch_custom_sphere.png", fig) + @test isfile("test_bloch_custom_sphere.png") + rm("test_bloch_custom_sphere.png") + end + + @testset "Wireframe, labels and axes toggled off" begin + fig = blochsphere(spinup(b); showwireframe=false, showlabels=false, showaxes=false) + save("test_bloch_minimal.png", fig) + @test isfile("test_bloch_minimal.png") + rm("test_bloch_minimal.png") + end + + + @testset "Wrong dimension state throws error" begin + b3 = SpinBasis(1) + ψ_3 = basisstate(b3, 1) + @test_throws ErrorException blochsphere(ψ_3) + end +end \ No newline at end of file From acf1ca9905f91685c7d57e5388af0054bd11d3be Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Tue, 5 May 2026 12:22:18 -0400 Subject: [PATCH 05/16] Remove accidentally committed local dev Project.toml --- ~/.julia/dev/QuantumOpticsBase/Project.toml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 ~/.julia/dev/QuantumOpticsBase/Project.toml diff --git a/~/.julia/dev/QuantumOpticsBase/Project.toml b/~/.julia/dev/QuantumOpticsBase/Project.toml deleted file mode 100644 index 81648c0b..00000000 --- a/~/.julia/dev/QuantumOpticsBase/Project.toml +++ /dev/null @@ -1 +0,0 @@ -[deps] From b4e919dba60da75504742bf4cdc8c197673bdac9 Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Tue, 5 May 2026 12:23:05 -0400 Subject: [PATCH 06/16] Add .gitignore to exclude local Julia dev paths --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2b4563d9..4470972e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /docs/build/ Manifest.toml .vscode +~/.julia/ From ac3a93cf64837f82db223f9f254ecf15e5056d74 Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Wed, 6 May 2026 12:37:41 -0400 Subject: [PATCH 07/16] Address all PR review comments --- Project.toml | 4 +- ext/QuantumOpticsBaseMakieExt.jl | 72 ++++++++++++++++++-------------- src/QuantumOpticsBase.jl | 8 +--- src/visualization.jl | 21 ++++++++++ 4 files changed, 65 insertions(+), 40 deletions(-) create mode 100644 src/visualization.jl diff --git a/Project.toml b/Project.toml index 9e8bee5d..561124d2 100644 --- a/Project.toml +++ b/Project.toml @@ -1,14 +1,12 @@ name = "QuantumOpticsBase" uuid = "4f57444f-1401-5e15-980d-4471b28d5678" -version = "0.5.9" - +version = "0.5.9" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" FastExpm = "7868e603-8603-432e-a1a1-694bd70b01f2" FastGaussQuadrature = "442a2c76-b920-505d-bb47-c5924d526838" FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" -GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" QuantumInterface = "5717a53b-5d69-4fa3-b976-0bf2f97ca1e5" diff --git a/ext/QuantumOpticsBaseMakieExt.jl b/ext/QuantumOpticsBaseMakieExt.jl index 01f32811..59f4071a 100644 --- a/ext/QuantumOpticsBaseMakieExt.jl +++ b/ext/QuantumOpticsBaseMakieExt.jl @@ -1,39 +1,38 @@ module QuantumOpticsBaseMakieExt + import QuantumOpticsBase import QuantumOpticsBase: Ket import Makie using Makie: Figure, @recipe, Attributes, Axis3 using Makie: surface!, arrows3d!, lines!, text!, meshscatter! -using Makie: Point3f, Vec3f - +using Makie: Point3f, Vec3f # re-exported by Makie from GeometryBasics — no separate dep needed + export blochsphereplot, blochsphereplot! +# ─── Recipe definition ──────────────────────────────────────────────────────── + @recipe(BlochSpherePlot, state) do scene Attributes( arrowcolor = :red, - spherecolor = :lightblue, - spherealpha = 0.15, - showwireframe = true, - showaxes = true, - showlabels = true, + spherecolor = :lightblue, # any Makie-compatible colour value + spherealpha = 0.15, # low enough that back-facing labels show through + showwireframe = true, # equator + two meridian great circles + showaxes = true, # bidirectional dashed x/y/z lines + showlabels = true, # |0⟩ |1⟩ |+⟩ |-⟩ |+i⟩ |-i⟩ pole labels labelsize = 18, - title = "Bloch Sphere", shaftradius = 0.018, tipradius = 0.050, tiplength = 0.10, - lim = 1.6, ) end -function Makie.convert_arguments(::Type{<:Makie.Plot{blochsphereplot}}, state::Ket) - return (state,) -end - - +# ─── Main recipe ────────────────────────────────────────────────────────────── function Makie.plot!(p::BlochSpherePlot) - state_obs = p[1] - + state_obs = p[1] # Observable{Ket} + + # ── Bloch vector from |ψ⟩ = α|0⟩ + β|1⟩ ───────────────────────────────── + # x = 2 Re(ᾱβ) y = 2 Im(ᾱβ) z = |α|² – |β|² blochvec = Makie.@lift begin s = $state_obs length(s.data) == 2 || @@ -46,6 +45,10 @@ function Makie.plot!(p::BlochSpherePlot) ) end + # ── Smooth sphere surface ───────────────────────────────────────────────── + # High resolution (200×200) makes individual UV cells too fine to see in + # CairoMakie's flat-polygon renderer, avoiding the visible mesh-grid artifact. + # We combine spherecolor + spherealpha into a single RGBAf at plot time. let npts = 200 θ = LinRange(0f0, 2f0π, npts) φ = LinRange(0f0, Float32(π), npts) @@ -58,25 +61,25 @@ function Makie.plot!(p::BlochSpherePlot) surface!(p, xs, ys, zs; color = fill(rgba, npts, npts), transparency = true, - - + # shading is intentionally omitted — FastShading was deprecated as a + # plot attribute in Makie 0.24; the default scene shading is sufficient ) end - + # ── Wireframe great circles ─────────────────────────────────────────────── if p[:showwireframe][] ncirc = 120 θc = LinRange(0f0, 2f0π, ncirc) for pts in ( - [Point3f( cos(t), sin(t), 0f0) for t in θc], - [Point3f( cos(t), 0f0, sin(t)) for t in θc], - [Point3f(0f0, cos(t), sin(t)) for t in θc], + [Point3f( cos(t), sin(t), 0f0) for t in θc], # equator (xy) + [Point3f( cos(t), 0f0, sin(t)) for t in θc], # meridian (xz) + [Point3f(0f0, cos(t), sin(t)) for t in θc], # meridian (yz) ) - lines!(p, pts; color = (:black, 0.55), linewidth = 1.2) + lines!(p, pts; color = (:black, 0.70), linewidth = 1.2) end end - + # ── Bidirectional dashed axis lines ────────────────────────────────────── if p[:showaxes][] r = 1.18f0 for (a, b) in ( @@ -88,6 +91,7 @@ function Makie.plot!(p::BlochSpherePlot) end end + # ── State-vector arrow (reactive to state changes) ──────────────────────── arrows3d!(p, [Point3f(0, 0, 0)], Makie.@lift([$blochvec]); @@ -97,16 +101,20 @@ function Makie.plot!(p::BlochSpherePlot) color = p[:arrowcolor], ) + # ── Dot at the state point on the sphere surface ────────────────────────── + # meshscatter! places a true 3D sphere marker in scene space, so it sits + # correctly on the surface regardless of camera angle — unlike scatter! + # which uses flat 2D screen-space markers that get occluded by the arrow tip. meshscatter!(p, Makie.@lift([Point3f($blochvec)]); color = p[:arrowcolor], markersize = 0.06, ) - + # ── Pole labels ─────────────────────────────────────────────────────────── if p[:showlabels][] ls = p[:labelsize][] - off = 1.40f0 + off = 1.40f0 # outside the sphere surface; lim=1.6 gives enough room for (pos, lbl, align) in ( (Point3f( 0f0, 0f0, off), "|0⟩", (:center, :bottom)), (Point3f( 0f0, 0f0, -off), "|1⟩", (:center, :top )), @@ -122,13 +130,14 @@ function Makie.plot!(p::BlochSpherePlot) return p end - +# ─── Convenience constructor ────────────────────────────────────────────────── function QuantumOpticsBase.blochsphere(state::Ket; kwargs...) fig = Figure(size = (700, 700)) ax = Axis3(fig[1, 1]; aspect = :data, viewmode = :fit, + # Hide Makie's own ticks and axis labels — we draw our own pole labels xticksvisible = false, yticksvisible = false, zticksvisible = false, @@ -138,6 +147,7 @@ function QuantumOpticsBase.blochsphere(state::Ket; kwargs...) xlabelvisible = false, ylabelvisible = false, zlabelvisible = false, + # Hide the bounding-box spines, interior grid planes, and background panels xspinesvisible = false, yspinesvisible = false, zspinesvisible = false, @@ -148,10 +158,10 @@ function QuantumOpticsBase.blochsphere(state::Ket; kwargs...) xzpanelvisible = false, yzpanelvisible = false, ) - lim = Float32(get(kwargs, :lim, 1.6)) + lim = Float32(get(kwargs, :limits, 1.6)) Makie.limits!(ax, -lim, lim, -lim, lim, -lim, lim) - blochsphereplot!(ax, state; kwargs...) - return fig + plt = blochsphereplot!(ax, state; kwargs...) + return fig, ax, plt end -end \ No newline at end of file +end # module \ No newline at end of file diff --git a/src/QuantumOpticsBase.jl b/src/QuantumOpticsBase.jl index 77d3cfed..a0631d9a 100644 --- a/src/QuantumOpticsBase.jl +++ b/src/QuantumOpticsBase.jl @@ -74,12 +74,7 @@ export Basis, GenericBasis, CompositeBasis, basis, #apply apply! - ########### Bloch Sphere API Stub ########### - -function blochsphere(state) -error("Blochsphere visualization requires a Makie extension.\n" * - "Load QuantumOpticsBase together with CairoMakie or GLMakie.") -end + #visualizations include("bases.jl") @@ -109,5 +104,6 @@ include("spinors.jl") include("phasespace.jl") include("printing.jl") include("apply.jl") +include("visualization.jl") end # module diff --git a/src/visualization.jl b/src/visualization.jl new file mode 100644 index 00000000..cb959f44 --- /dev/null +++ b/src/visualization.jl @@ -0,0 +1,21 @@ +""" + blochsphere(state::Ket; kwargs...) -> (Figure, Axis3, Plot) + +Visualize a pure qubit state on a Bloch sphere using Makie. + +The Bloch vector components are the expectation values of the Pauli operators: +x = 2Re(ᾱβ), y = 2Im(ᾱβ), z = |α|² - |β|² + +# Keyword Arguments +- `arrowcolor`: color of the state vector arrow (default `:red`) +- `spherecolor`: color of the sphere surface (default `:lightblue`) +- `spherealpha`: transparency of the sphere (default `0.15`) +- `showwireframe`: show equator and meridian circles (default `true`) +- `showaxes`: show dashed x/y/z axis lines (default `true`) +- `showlabels`: show pole labels |0⟩ |1⟩ |+⟩ |-⟩ |+i⟩ |-i⟩ (default `true`) +- `labelsize`: font size for pole labels (default `18`) +- `limits`: axis range, sphere has radius 1 (default `1.6`) + +Requires a Makie backend (e.g. `using CairoMakie` or `using GLMakie`). +""" +function blochsphere end \ No newline at end of file From f1c60fe7bc724cbf0d9dd93f0d732a398b02778e Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Wed, 6 May 2026 12:49:54 -0400 Subject: [PATCH 08/16] Resolve Project.toml merge conflict with upstream --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index 3bc5d145..a973e8e3 100644 --- a/Project.toml +++ b/Project.toml @@ -28,6 +28,7 @@ Adapt = "3.3, 4" FFTW = "1.2" FastExpm = "1.1.0" FastGaussQuadrature = "0.5, 1" +GeometryBasics = "0.5.10" FillArrays = "1.9" LRUCache = "1" LinearAlgebra = "1" From 85d0a335ca6771406b11b9d8043786e171c728ca Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Wed, 6 May 2026 12:59:36 -0400 Subject: [PATCH 09/16] Update ext, test, and QuantumOpticsBase files with all review fixes --- ext/QuantumOpticsBaseMakieExt.jl | 82 ++++++--------- src/QuantumOpticsBase.jl | 3 +- test/test_plotting.jl | 168 ++++--------------------------- 3 files changed, 56 insertions(+), 197 deletions(-) diff --git a/ext/QuantumOpticsBaseMakieExt.jl b/ext/QuantumOpticsBaseMakieExt.jl index 59f4071a..79bd7cd2 100644 --- a/ext/QuantumOpticsBaseMakieExt.jl +++ b/ext/QuantumOpticsBaseMakieExt.jl @@ -1,38 +1,36 @@ module QuantumOpticsBaseMakieExt - + import QuantumOpticsBase import QuantumOpticsBase: Ket import Makie using Makie: Figure, @recipe, Attributes, Axis3 using Makie: surface!, arrows3d!, lines!, text!, meshscatter! -using Makie: Point3f, Vec3f # re-exported by Makie from GeometryBasics — no separate dep needed - +using Makie: Point3f, Vec3f + export blochsphereplot, blochsphereplot! - -# ─── Recipe definition ──────────────────────────────────────────────────────── - + + + @recipe(BlochSpherePlot, state) do scene Attributes( arrowcolor = :red, - spherecolor = :lightblue, # any Makie-compatible colour value - spherealpha = 0.15, # low enough that back-facing labels show through - showwireframe = true, # equator + two meridian great circles - showaxes = true, # bidirectional dashed x/y/z lines - showlabels = true, # |0⟩ |1⟩ |+⟩ |-⟩ |+i⟩ |-i⟩ pole labels + spherecolor = :lightblue, + spherealpha = 0.15, + showwireframe = true, + showaxes = true, + showlabels = true, labelsize = 18, shaftradius = 0.018, tipradius = 0.050, tiplength = 0.10, ) end - -# ─── Main recipe ────────────────────────────────────────────────────────────── - + + + function Makie.plot!(p::BlochSpherePlot) - state_obs = p[1] # Observable{Ket} - - # ── Bloch vector from |ψ⟩ = α|0⟩ + β|1⟩ ───────────────────────────────── - # x = 2 Re(ᾱβ) y = 2 Im(ᾱβ) z = |α|² – |β|² + state_obs = p[1] + blochvec = Makie.@lift begin s = $state_obs length(s.data) == 2 || @@ -44,11 +42,7 @@ function Makie.plot!(p::BlochSpherePlot) Float32(abs2(α) - abs2(β)), ) end - - # ── Smooth sphere surface ───────────────────────────────────────────────── - # High resolution (200×200) makes individual UV cells too fine to see in - # CairoMakie's flat-polygon renderer, avoiding the visible mesh-grid artifact. - # We combine spherecolor + spherealpha into a single RGBAf at plot time. + let npts = 200 θ = LinRange(0f0, 2f0π, npts) φ = LinRange(0f0, Float32(π), npts) @@ -61,25 +55,22 @@ function Makie.plot!(p::BlochSpherePlot) surface!(p, xs, ys, zs; color = fill(rgba, npts, npts), transparency = true, - # shading is intentionally omitted — FastShading was deprecated as a - # plot attribute in Makie 0.24; the default scene shading is sufficient + ) end - - # ── Wireframe great circles ─────────────────────────────────────────────── + if p[:showwireframe][] ncirc = 120 θc = LinRange(0f0, 2f0π, ncirc) for pts in ( - [Point3f( cos(t), sin(t), 0f0) for t in θc], # equator (xy) - [Point3f( cos(t), 0f0, sin(t)) for t in θc], # meridian (xz) - [Point3f(0f0, cos(t), sin(t)) for t in θc], # meridian (yz) + [Point3f( cos(t), sin(t), 0f0) for t in θc], + [Point3f( cos(t), 0f0, sin(t)) for t in θc], + [Point3f(0f0, cos(t), sin(t)) for t in θc], ) lines!(p, pts; color = (:black, 0.70), linewidth = 1.2) end end - - # ── Bidirectional dashed axis lines ────────────────────────────────────── + if p[:showaxes][] r = 1.18f0 for (a, b) in ( @@ -90,8 +81,7 @@ function Makie.plot!(p::BlochSpherePlot) lines!(p, [a, b]; color = :black, linewidth = 1, linestyle = :dash) end end - - # ── State-vector arrow (reactive to state changes) ──────────────────────── + arrows3d!(p, [Point3f(0, 0, 0)], Makie.@lift([$blochvec]); @@ -100,21 +90,16 @@ function Makie.plot!(p::BlochSpherePlot) tiplength = p[:tiplength], color = p[:arrowcolor], ) - - # ── Dot at the state point on the sphere surface ────────────────────────── - # meshscatter! places a true 3D sphere marker in scene space, so it sits - # correctly on the surface regardless of camera angle — unlike scatter! - # which uses flat 2D screen-space markers that get occluded by the arrow tip. + meshscatter!(p, Makie.@lift([Point3f($blochvec)]); color = p[:arrowcolor], markersize = 0.06, ) - - # ── Pole labels ─────────────────────────────────────────────────────────── + if p[:showlabels][] ls = p[:labelsize][] - off = 1.40f0 # outside the sphere surface; lim=1.6 gives enough room + off = 1.40f0 for (pos, lbl, align) in ( (Point3f( 0f0, 0f0, off), "|0⟩", (:center, :bottom)), (Point3f( 0f0, 0f0, -off), "|1⟩", (:center, :top )), @@ -126,18 +111,16 @@ function Makie.plot!(p::BlochSpherePlot) text!(p, pos; text = lbl, fontsize = ls, align = align) end end - + return p end - -# ─── Convenience constructor ────────────────────────────────────────────────── - + + function QuantumOpticsBase.blochsphere(state::Ket; kwargs...) fig = Figure(size = (700, 700)) ax = Axis3(fig[1, 1]; aspect = :data, viewmode = :fit, - # Hide Makie's own ticks and axis labels — we draw our own pole labels xticksvisible = false, yticksvisible = false, zticksvisible = false, @@ -147,7 +130,6 @@ function QuantumOpticsBase.blochsphere(state::Ket; kwargs...) xlabelvisible = false, ylabelvisible = false, zlabelvisible = false, - # Hide the bounding-box spines, interior grid planes, and background panels xspinesvisible = false, yspinesvisible = false, zspinesvisible = false, @@ -163,5 +145,5 @@ function QuantumOpticsBase.blochsphere(state::Ket; kwargs...) plt = blochsphereplot!(ax, state; kwargs...) return fig, ax, plt end - -end # module \ No newline at end of file + +end diff --git a/src/QuantumOpticsBase.jl b/src/QuantumOpticsBase.jl index a0631d9a..84202e15 100644 --- a/src/QuantumOpticsBase.jl +++ b/src/QuantumOpticsBase.jl @@ -70,11 +70,12 @@ export Basis, GenericBasis, CompositeBasis, basis, PauliBasis, PauliTransferMatrix, DensePauliTransferMatrix, ChiMatrix, DenseChiMatrix, avg_gate_fidelity, SumBasis, directsum, ⊕, LazyDirectSum, getblock, setblock!, - qfunc, wigner, coherentspinstate, qfuncsu2, wignersu2, blochsphere + qfunc, wigner, coherentspinstate, qfuncsu2, wignersu2, #apply apply! #visualizations + blochsphere include("bases.jl") diff --git a/test/test_plotting.jl b/test/test_plotting.jl index 318fc6f2..927941c9 100644 --- a/test/test_plotting.jl +++ b/test/test_plotting.jl @@ -1,177 +1,53 @@ @testitem "Bloch Sphere Plotting" tags=[:plotting] begin using QuantumOpticsBase using CairoMakie - - b = SpinBasis(1//2) - - - function bloch_vector(ψ::Ket) - α, β = ψ.data - x = 2 * real(conj(α) * β) - y = 2 * imag(conj(α) * β) - z = abs2(α) - abs2(β) - return (x, y, z) - end - - - @testset "blochsphere returns a Figure" begin - @test blochsphere(spinup(b)) isa Figure - end - - @testset "blochsphereplot! returns a Plot" begin - fig = Figure() - ax = Axis3(fig[1, 1]) - @test blochsphereplot!(ax, spinup(b)) isa Makie.Plot - end - - - @testset "Bloch vector |0⟩ north pole" begin - x, y, z = bloch_vector(spinup(b)) - @test x ≈ 0 atol=1e-10 - @test y ≈ 0 atol=1e-10 - @test z ≈ 1 atol=1e-10 - end - - @testset "Bloch vector |1⟩ south pole" begin - x, y, z = bloch_vector(spindown(b)) - @test x ≈ 0 atol=1e-10 - @test y ≈ 0 atol=1e-10 - @test z ≈ -1 atol=1e-10 - end - - @testset "Bloch vector |+⟩ +x pole" begin - x, y, z = bloch_vector(normalize(spinup(b) + spindown(b))) - @test x ≈ 1 atol=1e-10 - @test y ≈ 0 atol=1e-10 - @test z ≈ 0 atol=1e-10 - end - - @testset "Bloch vector |-⟩ -x pole" begin - x, y, z = bloch_vector(normalize(spinup(b) - spindown(b))) - @test x ≈ -1 atol=1e-10 - @test y ≈ 0 atol=1e-10 - @test z ≈ 0 atol=1e-10 - end - - @testset "Bloch vector |+i⟩ +y pole" begin - x, y, z = bloch_vector(normalize(spinup(b) + im*spindown(b))) - @test x ≈ 0 atol=1e-10 - @test y ≈ 1 atol=1e-10 - @test z ≈ 0 atol=1e-10 - end - - @testset "Bloch vector |-i⟩ -y pole" begin - x, y, z = bloch_vector(normalize(spinup(b) - im*spindown(b))) - @test x ≈ 0 atol=1e-10 - @test y ≈ -1 atol=1e-10 - @test z ≈ 0 atol=1e-10 - end - - @testset "Bloch vector matches θ/φ parameterisation" begin - for (θ, φ) in [(π/3, π/4), (π/2, π/3), (2π/3, 3π/4), (π/4, 7π/6)] - ψ = cos(θ/2)*spinup(b) + exp(im*φ)*sin(θ/2)*spindown(b) - x, y, z = bloch_vector(ψ) - @test x ≈ sin(θ)*cos(φ) atol=1e-10 - @test y ≈ sin(θ)*sin(φ) atol=1e-10 - @test z ≈ cos(θ) atol=1e-10 - end - end - - @testset "Bloch vector has unit length for any pure state" begin - for (θ, φ) in [(0.0, 0.0), (π/4, π/3), (π/2, 0.0), (π/2, π), (π, 0.0)] - ψ = cos(θ/2)*spinup(b) + exp(im*φ)*sin(θ/2)*spindown(b) - x, y, z = bloch_vector(ψ) - @test x^2 + y^2 + z^2 ≈ 1 atol=1e-10 - end - end - + b = SpinBasis(1//2) - @testset "|0⟩ renders without error" begin - fig = blochsphere(spinup(b)) - save("test_bloch_0.png", fig) - @test isfile("test_bloch_0.png") - rm("test_bloch_0.png") - end - - @testset "|1⟩ renders without error" begin - fig = blochsphere(spindown(b)) - save("test_bloch_1.png", fig) - @test isfile("test_bloch_1.png") - rm("test_bloch_1.png") - end - - @testset "|+⟩ renders without error" begin - fig = blochsphere(normalize(spinup(b) + spindown(b))) - save("test_bloch_plus.png", fig) - @test isfile("test_bloch_plus.png") - rm("test_bloch_plus.png") + # ── Return types ────────────────────────────────────────────────────────── + @testset "blochsphere returns Figure, Axis3, and plot object" begin + fig, ax, plt = blochsphere(spinup(b)) + @test fig isa Figure + @test ax isa Axis3 + @test plt isa Makie.Plot end - - @testset "|-⟩ renders without error" begin - fig = blochsphere(normalize(spinup(b) - spindown(b))) - save("test_bloch_minus.png", fig) - @test isfile("test_bloch_minus.png") - rm("test_bloch_minus.png") - end - - @testset "|+i⟩ renders without error" begin - fig = blochsphere(normalize(spinup(b) + im*spindown(b))) - save("test_bloch_plusi.png", fig) - @test isfile("test_bloch_plusi.png") - rm("test_bloch_plusi.png") - end - - @testset "|-i⟩ renders without error" begin - fig = blochsphere(normalize(spinup(b) - im*spindown(b))) - save("test_bloch_minusi.png", fig) - @test isfile("test_bloch_minusi.png") - rm("test_bloch_minusi.png") - end - - @testset "Arbitrary state θ=π/3 φ=π/4 renders without error" begin + # ── Render test ─────────────────────────────────────────────────────────── + @testset "arbitrary state renders without error" begin ψ = cos(π/6)*spinup(b) + exp(im*π/4)*sin(π/6)*spindown(b) - fig = blochsphere(ψ) - save("test_bloch_arb1.png", fig) - @test isfile("test_bloch_arb1.png") - rm("test_bloch_arb1.png") + fig, _, _ = blochsphere(ψ) + save("test_bloch.png", fig) + @test isfile("test_bloch.png") + rm("test_bloch.png") end - - @testset "Arbitrary state θ=π/2 φ=π/3 renders without error" begin - ψ = cos(π/4)*spinup(b) + exp(im*π/3)*sin(π/4)*spindown(b) - fig = blochsphere(ψ) - save("test_bloch_arb2.png", fig) - @test isfile("test_bloch_arb2.png") - rm("test_bloch_arb2.png") - end - + # ── Render tests: custom attributes ────────────────────────────────────── @testset "Custom arrowcolor and spherealpha" begin - fig = blochsphere(spinup(b); arrowcolor=:blue, spherealpha=0.3) + fig, _, _ = blochsphere(spinup(b); arrowcolor=:blue, spherealpha=0.3) save("test_bloch_custom_color.png", fig) @test isfile("test_bloch_custom_color.png") rm("test_bloch_custom_color.png") end - + @testset "Custom spherecolor" begin - fig = blochsphere(spindown(b); spherecolor=:pink, spherealpha=0.2) + fig, _, _ = blochsphere(spindown(b); spherecolor=:pink, spherealpha=0.2) save("test_bloch_custom_sphere.png", fig) @test isfile("test_bloch_custom_sphere.png") rm("test_bloch_custom_sphere.png") end - + @testset "Wireframe, labels and axes toggled off" begin - fig = blochsphere(spinup(b); showwireframe=false, showlabels=false, showaxes=false) + fig, _, _ = blochsphere(spinup(b); showwireframe=false, showlabels=false, showaxes=false) save("test_bloch_minimal.png", fig) @test isfile("test_bloch_minimal.png") rm("test_bloch_minimal.png") end - + # ── Error handling ──────────────────────────────────────────────────────── @testset "Wrong dimension state throws error" begin + # spin-1 has 3 levels — the recipe requires exactly 2 b3 = SpinBasis(1) ψ_3 = basisstate(b3, 1) @test_throws ErrorException blochsphere(ψ_3) end -end \ No newline at end of file +end From b92a70d8fad3f226ef253afe77a221e1b74c013b Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Mon, 11 May 2026 23:45:36 -0400 Subject: [PATCH 10/16] Fix export block and remove GeometryBasics from compat --- Project.toml | 1 - src/QuantumOpticsBase.jl | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index a973e8e3..3bc5d145 100644 --- a/Project.toml +++ b/Project.toml @@ -28,7 +28,6 @@ Adapt = "3.3, 4" FFTW = "1.2" FastExpm = "1.1.0" FastGaussQuadrature = "0.5, 1" -GeometryBasics = "0.5.10" FillArrays = "1.9" LRUCache = "1" LinearAlgebra = "1" diff --git a/src/QuantumOpticsBase.jl b/src/QuantumOpticsBase.jl index 84202e15..4459ebcf 100644 --- a/src/QuantumOpticsBase.jl +++ b/src/QuantumOpticsBase.jl @@ -72,7 +72,7 @@ export Basis, GenericBasis, CompositeBasis, basis, SumBasis, directsum, ⊕, LazyDirectSum, getblock, setblock!, qfunc, wigner, coherentspinstate, qfuncsu2, wignersu2, #apply - apply! + apply!, #visualizations blochsphere From 726c762c07c66b49883d3bab9a58dda7e314ac8b Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Sun, 17 May 2026 21:41:16 -0400 Subject: [PATCH 11/16] Add explicit Makie import for CI compatibility --- test/test_plotting.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_plotting.jl b/test/test_plotting.jl index 927941c9..a1a6989b 100644 --- a/test/test_plotting.jl +++ b/test/test_plotting.jl @@ -1,6 +1,7 @@ @testitem "Bloch Sphere Plotting" tags=[:plotting] begin using QuantumOpticsBase using CairoMakie + import Makie b = SpinBasis(1//2) From 70ebacd13930abb89ec6dd787c99ecc7624a5497 Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Sun, 17 May 2026 22:13:16 -0400 Subject: [PATCH 12/16] fix: use AbstractPlot instead of Makie.Plot to avoid bare Makie import --- test/test_plotting.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_plotting.jl b/test/test_plotting.jl index a1a6989b..8f92b157 100644 --- a/test/test_plotting.jl +++ b/test/test_plotting.jl @@ -1,7 +1,7 @@ @testitem "Bloch Sphere Plotting" tags=[:plotting] begin using QuantumOpticsBase - using CairoMakie - import Makie + using CairoMakie # re-exports all of Makie's exported names, including AbstractPlot + b = SpinBasis(1//2) @@ -10,7 +10,7 @@ fig, ax, plt = blochsphere(spinup(b)) @test fig isa Figure @test ax isa Axis3 - @test plt isa Makie.Plot + @test plt isa AbstractPlot end # ── Render test ─────────────────────────────────────────────────────────── From 2da4ae645a25b2175fe8892c4cc904c60f53a2a9 Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Sun, 17 May 2026 22:15:37 -0400 Subject: [PATCH 13/16] fix: use AbstractPlot instead of Makie.Plot to avoid bare Makie import --- test/test_plotting.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_plotting.jl b/test/test_plotting.jl index 8f92b157..301e594e 100644 --- a/test/test_plotting.jl +++ b/test/test_plotting.jl @@ -2,7 +2,6 @@ using QuantumOpticsBase using CairoMakie # re-exports all of Makie's exported names, including AbstractPlot - b = SpinBasis(1//2) # ── Return types ────────────────────────────────────────────────────────── From dd571945e8aa7b92df66f6d5df02051ae84fc0f8 Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Mon, 18 May 2026 11:31:44 -0400 Subject: [PATCH 14/16] fix: extract limits kwarg explicitly, use c.r/g/b instead of Makie.red/green/blue --- ext/QuantumOpticsBaseMakieExt.jl | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/ext/QuantumOpticsBaseMakieExt.jl b/ext/QuantumOpticsBaseMakieExt.jl index 79bd7cd2..7c8339f7 100644 --- a/ext/QuantumOpticsBaseMakieExt.jl +++ b/ext/QuantumOpticsBaseMakieExt.jl @@ -5,7 +5,7 @@ import QuantumOpticsBase: Ket import Makie using Makie: Figure, @recipe, Attributes, Axis3 using Makie: surface!, arrows3d!, lines!, text!, meshscatter! -using Makie: Point3f, Vec3f +using Makie: Point3f, Vec3f export blochsphereplot, blochsphereplot! @@ -14,11 +14,11 @@ export blochsphereplot, blochsphereplot! @recipe(BlochSpherePlot, state) do scene Attributes( arrowcolor = :red, - spherecolor = :lightblue, - spherealpha = 0.15, - showwireframe = true, - showaxes = true, - showlabels = true, + spherecolor = :lightblue, + spherealpha = 0.15, + showwireframe = true, + showaxes = true, + showlabels = true, labelsize = 18, shaftradius = 0.018, tipradius = 0.050, @@ -29,7 +29,7 @@ end function Makie.plot!(p::BlochSpherePlot) - state_obs = p[1] + state_obs = p[1] blochvec = Makie.@lift begin s = $state_obs @@ -51,7 +51,7 @@ function Makie.plot!(p::BlochSpherePlot) zs = Float32[cos(q) for _ in θ, q in φ] c = Makie.to_color(p[:spherecolor][]) α = Float32(p[:spherealpha][]) - rgba = Makie.RGBAf(Makie.red(c), Makie.green(c), Makie.blue(c), α) + rgba = Makie.RGBAf(c.r, c.g, c.b, α) surface!(p, xs, ys, zs; color = fill(rgba, npts, npts), transparency = true, @@ -63,9 +63,9 @@ function Makie.plot!(p::BlochSpherePlot) ncirc = 120 θc = LinRange(0f0, 2f0π, ncirc) for pts in ( - [Point3f( cos(t), sin(t), 0f0) for t in θc], - [Point3f( cos(t), 0f0, sin(t)) for t in θc], - [Point3f(0f0, cos(t), sin(t)) for t in θc], + [Point3f( cos(t), sin(t), 0f0) for t in θc], + [Point3f( cos(t), 0f0, sin(t)) for t in θc], + [Point3f(0f0, cos(t), sin(t)) for t in θc], ) lines!(p, pts; color = (:black, 0.70), linewidth = 1.2) end @@ -99,7 +99,7 @@ function Makie.plot!(p::BlochSpherePlot) if p[:showlabels][] ls = p[:labelsize][] - off = 1.40f0 + off = 1.40f0 for (pos, lbl, align) in ( (Point3f( 0f0, 0f0, off), "|0⟩", (:center, :bottom)), (Point3f( 0f0, 0f0, -off), "|1⟩", (:center, :top )), @@ -116,7 +116,7 @@ function Makie.plot!(p::BlochSpherePlot) end -function QuantumOpticsBase.blochsphere(state::Ket; kwargs...) +function QuantumOpticsBase.blochsphere(state::Ket; limits=1.6, kwargs...) fig = Figure(size = (700, 700)) ax = Axis3(fig[1, 1]; aspect = :data, @@ -140,10 +140,10 @@ function QuantumOpticsBase.blochsphere(state::Ket; kwargs...) xzpanelvisible = false, yzpanelvisible = false, ) - lim = Float32(get(kwargs, :limits, 1.6)) + lim = Float32(limits) Makie.limits!(ax, -lim, lim, -lim, lim, -lim, lim) plt = blochsphereplot!(ax, state; kwargs...) return fig, ax, plt end -end +end From 8dd3d5ae39afd8f913d56a80780626a5820d51b8 Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Tue, 19 May 2026 22:28:00 -0400 Subject: [PATCH 15/16] refactor: put recipe functions in QuantumOpticsBase namespace, rename blochsphere to blochsphereplot_axis, add Observable reactivity test --- .gitignore | 1 - ext/QuantumOpticsBaseMakieExt.jl | 23 +++++++++----------- src/QuantumOpticsBase.jl | 2 +- src/visualization.jl | 36 +++++++++++++++++++------------- test/test_plotting.jl | 32 +++++++++++++++++++--------- 5 files changed, 54 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 4470972e..2b4563d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ /docs/build/ Manifest.toml .vscode -~/.julia/ diff --git a/ext/QuantumOpticsBaseMakieExt.jl b/ext/QuantumOpticsBaseMakieExt.jl index 7c8339f7..c6eecf06 100644 --- a/ext/QuantumOpticsBaseMakieExt.jl +++ b/ext/QuantumOpticsBaseMakieExt.jl @@ -1,16 +1,12 @@ module QuantumOpticsBaseMakieExt import QuantumOpticsBase -import QuantumOpticsBase: Ket +import QuantumOpticsBase: Ket, blochsphereplot, blochsphereplot!, blochsphereplot_axis import Makie using Makie: Figure, @recipe, Attributes, Axis3 using Makie: surface!, arrows3d!, lines!, text!, meshscatter! using Makie: Point3f, Vec3f -export blochsphereplot, blochsphereplot! - - - @recipe(BlochSpherePlot, state) do scene Attributes( arrowcolor = :red, @@ -26,8 +22,6 @@ export blochsphereplot, blochsphereplot! ) end - - function Makie.plot!(p::BlochSpherePlot) state_obs = p[1] @@ -55,7 +49,6 @@ function Makie.plot!(p::BlochSpherePlot) surface!(p, xs, ys, zs; color = fill(rgba, npts, npts), transparency = true, - ) end @@ -116,7 +109,13 @@ function Makie.plot!(p::BlochSpherePlot) end -function QuantumOpticsBase.blochsphere(state::Ket; limits=1.6, kwargs...) +function QuantumOpticsBase.blochsphereplot_axis(ax::Makie.AbstractAxis, state::Ket; limits=1.6, kwargs...) + lim = Float32(limits) + Makie.limits!(ax, -lim, lim, -lim, lim, -lim, lim) + blochsphereplot!(ax, state; kwargs...) +end + +function QuantumOpticsBase.blochsphereplot_axis(state::Ket; limits=1.6, kwargs...) fig = Figure(size = (700, 700)) ax = Axis3(fig[1, 1]; aspect = :data, @@ -140,10 +139,8 @@ function QuantumOpticsBase.blochsphere(state::Ket; limits=1.6, kwargs...) xzpanelvisible = false, yzpanelvisible = false, ) - lim = Float32(limits) - Makie.limits!(ax, -lim, lim, -lim, lim, -lim, lim) - plt = blochsphereplot!(ax, state; kwargs...) + plt = QuantumOpticsBase.blochsphereplot_axis(ax, state; limits, kwargs...) return fig, ax, plt end -end +end # module \ No newline at end of file diff --git a/src/QuantumOpticsBase.jl b/src/QuantumOpticsBase.jl index 4459ebcf..f289952e 100644 --- a/src/QuantumOpticsBase.jl +++ b/src/QuantumOpticsBase.jl @@ -75,7 +75,7 @@ export Basis, GenericBasis, CompositeBasis, basis, apply!, #visualizations - blochsphere + blochsphereplot, blochsphereplot!, blochsphereplot_axis include("bases.jl") diff --git a/src/visualization.jl b/src/visualization.jl index cb959f44..d11c80b7 100644 --- a/src/visualization.jl +++ b/src/visualization.jl @@ -1,21 +1,27 @@ """ - blochsphere(state::Ket; kwargs...) -> (Figure, Axis3, Plot) + blochsphereplot(state::Ket; kwargs...) -Visualize a pure qubit state on a Bloch sphere using Makie. +Visualize a pure qubit state as an arrow on a Bloch sphere. -The Bloch vector components are the expectation values of the Pauli operators: -x = 2Re(ᾱβ), y = 2Im(ᾱβ), z = |α|² - |β|² +Requires a Makie backend be already imported. +""" +function blochsphereplot end + +""" + blochsphereplot!(ax, state::Ket; kwargs...) + +In-place version of [`blochsphereplot`](@ref). Plots onto an existing Makie axis. + +Requires a Makie backend be already imported. +""" +function blochsphereplot! end + +""" + blochsphereplot_axis([ax,] state::Ket; kwargs...) -> (Figure, Axis3, Plot) -# Keyword Arguments -- `arrowcolor`: color of the state vector arrow (default `:red`) -- `spherecolor`: color of the sphere surface (default `:lightblue`) -- `spherealpha`: transparency of the sphere (default `0.15`) -- `showwireframe`: show equator and meridian circles (default `true`) -- `showaxes`: show dashed x/y/z axis lines (default `true`) -- `showlabels`: show pole labels |0⟩ |1⟩ |+⟩ |-⟩ |+i⟩ |-i⟩ (default `true`) -- `labelsize`: font size for pole labels (default `18`) -- `limits`: axis range, sphere has radius 1 (default `1.6`) +Visualize a pure qubit state on a Bloch sphere, creating a new Figure and Axis3 +or plotting onto an existing one. -Requires a Makie backend (e.g. `using CairoMakie` or `using GLMakie`). +Requires a Makie backend be already imported. """ -function blochsphere end \ No newline at end of file +function blochsphereplot_axis end \ No newline at end of file diff --git a/test/test_plotting.jl b/test/test_plotting.jl index 301e594e..1b4dd315 100644 --- a/test/test_plotting.jl +++ b/test/test_plotting.jl @@ -1,12 +1,12 @@ @testitem "Bloch Sphere Plotting" tags=[:plotting] begin using QuantumOpticsBase - using CairoMakie # re-exports all of Makie's exported names, including AbstractPlot - + using CairoMakie + b = SpinBasis(1//2) # ── Return types ────────────────────────────────────────────────────────── - @testset "blochsphere returns Figure, Axis3, and plot object" begin - fig, ax, plt = blochsphere(spinup(b)) + @testset "blochsphereplot_axis returns Figure, Axis3, and plot object" begin + fig, ax, plt = blochsphereplot_axis(spinup(b)) @test fig isa Figure @test ax isa Axis3 @test plt isa AbstractPlot @@ -15,7 +15,7 @@ # ── Render test ─────────────────────────────────────────────────────────── @testset "arbitrary state renders without error" begin ψ = cos(π/6)*spinup(b) + exp(im*π/4)*sin(π/6)*spindown(b) - fig, _, _ = blochsphere(ψ) + fig, _, _ = blochsphereplot_axis(ψ) save("test_bloch.png", fig) @test isfile("test_bloch.png") rm("test_bloch.png") @@ -23,21 +23,21 @@ # ── Render tests: custom attributes ────────────────────────────────────── @testset "Custom arrowcolor and spherealpha" begin - fig, _, _ = blochsphere(spinup(b); arrowcolor=:blue, spherealpha=0.3) + fig, _, _ = blochsphereplot_axis(spinup(b); arrowcolor=:blue, spherealpha=0.3) save("test_bloch_custom_color.png", fig) @test isfile("test_bloch_custom_color.png") rm("test_bloch_custom_color.png") end @testset "Custom spherecolor" begin - fig, _, _ = blochsphere(spindown(b); spherecolor=:pink, spherealpha=0.2) + fig, _, _ = blochsphereplot_axis(spindown(b); spherecolor=:pink, spherealpha=0.2) save("test_bloch_custom_sphere.png", fig) @test isfile("test_bloch_custom_sphere.png") rm("test_bloch_custom_sphere.png") end @testset "Wireframe, labels and axes toggled off" begin - fig, _, _ = blochsphere(spinup(b); showwireframe=false, showlabels=false, showaxes=false) + fig, _, _ = blochsphereplot_axis(spinup(b); showwireframe=false, showlabels=false, showaxes=false) save("test_bloch_minimal.png", fig) @test isfile("test_bloch_minimal.png") rm("test_bloch_minimal.png") @@ -45,9 +45,21 @@ # ── Error handling ──────────────────────────────────────────────────────── @testset "Wrong dimension state throws error" begin - # spin-1 has 3 levels — the recipe requires exactly 2 b3 = SpinBasis(1) ψ_3 = basisstate(b3, 1) - @test_throws ErrorException blochsphere(ψ_3) + @test_throws ErrorException blochsphereplot_axis(ψ_3) + end + + # ── Observable reactivity ───────────────────────────────────────────────── + @testset "Observable state updates reactively" begin + using Makie: Observable + state_obs = Observable(spinup(b)) + fig = Figure(size = (700, 700)) + ax = Axis3(fig[1, 1]) + blochsphereplot!(ax, state_obs) + state_obs[] = spindown(b) + save("test_bloch_observable.png", fig) + @test isfile("test_bloch_observable.png") + rm("test_bloch_observable.png") end end From 7ff3bfa2b4d772bff22eaba4e4a00b08a5805710 Mon Sep 17 00:00:00 2001 From: Aniket-umass Date: Wed, 20 May 2026 14:12:17 -0400 Subject: [PATCH 16/16] refactor: put recipe functions in QuantumOpticsBase namespace, rename blochsphere to blochsphereplot_axis, add Observable reactivity test --- ext/QuantumOpticsBaseMakieExt.jl | 6 ++++-- test/test_plotting.jl | 4 +--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/QuantumOpticsBaseMakieExt.jl b/ext/QuantumOpticsBaseMakieExt.jl index c6eecf06..926b1f47 100644 --- a/ext/QuantumOpticsBaseMakieExt.jl +++ b/ext/QuantumOpticsBaseMakieExt.jl @@ -109,17 +109,19 @@ function Makie.plot!(p::BlochSpherePlot) end -function QuantumOpticsBase.blochsphereplot_axis(ax::Makie.AbstractAxis, state::Ket; limits=1.6, kwargs...) +function QuantumOpticsBase.blochsphereplot_axis(ax::Makie.AbstractAxis, state; limits=1.6, kwargs...) + ax.perspectiveness = 0f0 lim = Float32(limits) Makie.limits!(ax, -lim, lim, -lim, lim, -lim, lim) blochsphereplot!(ax, state; kwargs...) end -function QuantumOpticsBase.blochsphereplot_axis(state::Ket; limits=1.6, kwargs...) +function QuantumOpticsBase.blochsphereplot_axis(state; limits=1.6, kwargs...) fig = Figure(size = (700, 700)) ax = Axis3(fig[1, 1]; aspect = :data, viewmode = :fit, + perspectiveness = 0f0, xticksvisible = false, yticksvisible = false, zticksvisible = false, diff --git a/test/test_plotting.jl b/test/test_plotting.jl index 1b4dd315..9a9035f0 100644 --- a/test/test_plotting.jl +++ b/test/test_plotting.jl @@ -54,9 +54,7 @@ @testset "Observable state updates reactively" begin using Makie: Observable state_obs = Observable(spinup(b)) - fig = Figure(size = (700, 700)) - ax = Axis3(fig[1, 1]) - blochsphereplot!(ax, state_obs) + fig, ax, _ = blochsphereplot_axis(state_obs) state_obs[] = spindown(b) save("test_bloch_observable.png", fig) @test isfile("test_bloch_observable.png")