diff --git a/CHANGELOG.md b/CHANGELOG.md index e6e3dea72..d231ec1ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Introduce `Lanczos` solver for `spectrum`. ([#476]) - Add Bloch-Redfield master equation solver. ([#473]) +- Implement Bloch Sphere rendering. ([#472]) ## [v0.31.1] Release date: 2025-05-16 @@ -231,5 +232,6 @@ Release date: 2024-11-13 [#455]: https://github.com/qutip/QuantumToolbox.jl/issues/455 [#456]: https://github.com/qutip/QuantumToolbox.jl/issues/456 [#460]: https://github.com/qutip/QuantumToolbox.jl/issues/460 +[#472]: https://github.com/qutip/QuantumToolbox.jl/issues/472 [#473]: https://github.com/qutip/QuantumToolbox.jl/issues/473 [#476]: https://github.com/qutip/QuantumToolbox.jl/issues/476 diff --git a/docs/make.jl b/docs/make.jl index d07b3e2dc..6769ec0ea 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -51,6 +51,7 @@ const PAGES = [ ], "Manipulating States and Operators" => "users_guide/states_and_operators.md", "Tensor Products and Partial Traces" => "users_guide/tensor.md", + "Plotting on the Bloch Sphere" => "users_guide/plotting_the_bloch_sphere.md", "Time Evolution and Dynamics" => [ "Introduction" => "users_guide/time_evolution/intro.md", "Time Evolution Solutions" => "users_guide/time_evolution/solution.md", diff --git a/docs/src/resources/api.md b/docs/src/resources/api.md index 7f3d49e73..e99247123 100644 --- a/docs/src/resources/api.md +++ b/docs/src/resources/api.md @@ -319,4 +319,13 @@ meshgrid ```@docs plot_wigner plot_fock_distribution +plot_bloch +Bloch +render +add_points! +add_vectors! +add_line! +add_arc! +clear! +add_states! ``` diff --git a/docs/src/users_guide/extensions/cairomakie.md b/docs/src/users_guide/extensions/cairomakie.md index af332ba11..aa4512f06 100644 --- a/docs/src/users_guide/extensions/cairomakie.md +++ b/docs/src/users_guide/extensions/cairomakie.md @@ -19,4 +19,5 @@ The supported plotting functions are listed as follows: | **Plotting Function** | **Description** | |:----------------------|:----------------| | [`plot_wigner`](@ref) | [Wigner quasipropability distribution](https://en.wikipedia.org/wiki/Wigner_quasiprobability_distribution) | -| [`plot_fock_distribution`](@ref) | [Fock state](https://en.wikipedia.org/wiki/Fock_state) distribution | \ No newline at end of file +| [`plot_fock_distribution`](@ref) | [Fock state](https://en.wikipedia.org/wiki/Fock_state) distribution | +| [`plot_bloch`](@ref) | [Plotting on the Bloch Sphere](@ref doc:Plotting-on-the-Bloch-Sphere) | diff --git a/docs/src/users_guide/plotting_the_bloch_sphere.md b/docs/src/users_guide/plotting_the_bloch_sphere.md new file mode 100644 index 000000000..44ff42c3e --- /dev/null +++ b/docs/src/users_guide/plotting_the_bloch_sphere.md @@ -0,0 +1,160 @@ +# [Plotting on the Bloch Sphere](@id doc:Plotting-on-the-Bloch-Sphere) + +```@setup Bloch_sphere_rendering +using QuantumToolbox + +using CairoMakie +CairoMakie.enable_only_mime!(MIME"image/svg+xml"()) +``` + +## [Introduction](@id doc:Bloch_sphere_rendering) + +When studying the dynamics of a two-level system, it's often convenient to visualize the state of the system by plotting the state vector or density matrix on the Bloch sphere. + +In [QuantumToolbox.jl](https://qutip.org/QuantumToolbox.jl/), this can be done using the [`Bloch`](@ref) or [`plot_bloch`](@ref) methods that provide same syntax as [QuTiP](https://qutip.readthedocs.io/en/stable/guide/guide-bloch.html). + +## Create a Bloch Sphere + +In [QuantumToolbox.jl](https://qutip.org/QuantumToolbox.jl/), creating a [`Bloch`](@ref) sphere is accomplished by calling either: + +```@example Bloch_sphere_rendering +b = Bloch(); +``` + +which will load an instance of [`Bloch`](@ref). Before getting into the details of these objects, we can simply plot the blank [`Bloch`](@ref) sphere associated with these instances via: + +```@example Bloch_sphere_rendering +fig, _ = render(b); +fig +``` + +## Add a Single Data Point + +As an example, we can add a single data point via [`add_points!`](@ref): + +```@example Bloch_sphere_rendering +pnt = [1 / sqrt(3), 1 / sqrt(3), 1 / sqrt(3)]; +add_points!(b, pnt); +fig, _ = render(b); +fig +``` + +## Add a Single Vector + +and then a single vector via [`add_vectors!`](@ref): + +```@example Bloch_sphere_rendering +vec = [0, 1, 0]; +add_vectors!(b, vec) +fig, _ = render(b) +fig +``` + +and then add another vector corresponding to the ``|0\rangle`` state: + +```@example Bloch_sphere_rendering +x = basis(2, 0) +add_states!(b, [x]) +fig, _ = render(b) +fig +``` + +## Add Multiple Vectors + +We can also plot multiple points, vectors, and states at the same time by passing arrays instead of individual elements via [`add_vectors!](@ref). Before giving an example, we can use [`clear!`](@ref) to remove the current data from our [`Bloch`](@ref) sphere instead of creating a new instance: + +```@example Bloch_sphere_rendering +clear!(b) +fig, _ = render(b) +fig +``` + +Now on the same [`Bloch`](@ref) sphere, we can plot the three states via [`add_states!`](@ref) associated with the `x`, `y`, and `z` directions: + +```@example Bloch_sphere_rendering +x = basis(2, 0) + basis(2, 1) +y = basis(2, 0) - im * basis(2, 1) +z = basis(2, 0) +b = Bloch() +add_states!(b, [x, y, z]) +fig, _ = render(b) +fig +``` + +A similar method works for adding vectors: + +```@example Bloch_sphere_rendering +clear!(b) +vecs = [[1, 0, 0], [0, 1, 0], [0, 0, 1]] +add_vectors!(b, vecs) +fig, _ = render(b) +fig +``` + +# Add Arc, Line, and Vector + +You can also add lines and arcs via [`add_line!`](@ref) and [`add_arc!`](@ref) respectively: + +```@example Bloch_sphere_rendering +clear!(b) +vec = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; +add_vectors!(b, vec); +add_line!(b, [1,0,0], [0,1,0]) +add_arc!(b, [1, 0, 0], [0, 1, 0], [0, 0, 1]) +fig, _ = render(b) +fig +``` + +## Add Multiple Points + +Adding multiple points to the [`Bloch`](@ref) sphere works slightly differently than adding multiple states or vectors. For example, lets add a set of `20` points around the equator (after calling [`clear!`](@ref)): + +```@example Bloch_sphere_rendering +th = range(0, 2π; length=20); +clear!(b) +xp = cos.(th); +yp = sin.(th); +zp = zeros(20); +pnts = [xp, yp, zp]; +add_points!(b, pnts); +fig, ax = render(b); +fig +``` + +Notice that, in contrast to states or vectors, each point remains the same color as the initial point. This is because adding multiple data points using [`add_points!`](@ref) is interpreted, by default, to correspond to a single data point (single qubit state) plotted at different times. This is very useful when visualizing the dynamics of a qubit. If we want to plot additional qubit states we can call additional [`add_points!`](@ref): + +## Add Another Set of Points + +```@example Bloch_sphere_rendering +xz = zeros(20); +yz = sin.(th); +zz = cos.(th); +pnts = [xz, yz, zz]; +add_points!(b, pnts); +fig, ax = render(b); +fig +``` + +The color and shape of the data points is varied automatically by [`Bloch`](@ref). Notice how the color and point markers change for each set of data. + +What if we want to vary the color of our points. We can tell [`Bloch`](@ref) to vary the color of each point according to the colors listed in the `point_color` attribute. + +```@example Bloch_sphere_rendering +clear!(b) +xp = cos.(th); +yp = sin.(th); +zp = zeros(20); +pnts = [xp, yp, zp]; +add_points!(b, pnts, meth=:m); +fig, ax = render(b); +fig +``` + +Now, the data points cycle through a variety of predefined colors. Now lets add another set of points, but this time we want the set to be a single color, representing say a qubit going from the ``|0\rangle`` state to the ``|1\rangle`` state in the `y-z` plane: + +```@example Bloch_sphere_rendering +pnts = [xz, yz, zz] ; +add_points!(b, pnts); +fig, ax = render(b); +fig +``` diff --git a/ext/QuantumToolboxMakieExt.jl b/ext/QuantumToolboxMakieExt.jl index 41aefe2b9..352e1ec85 100644 --- a/ext/QuantumToolboxMakieExt.jl +++ b/ext/QuantumToolboxMakieExt.jl @@ -1,8 +1,32 @@ module QuantumToolboxMakieExt using QuantumToolbox +using LinearAlgebra: cross, deg2rad, normalize, size using Makie: - Axis, Axis3, Colorbar, Figure, GridLayout, heatmap!, surface!, barplot!, GridPosition, @L_str, Reverse, ylims! + Axis, + Axis3, + Colorbar, + Figure, + GridLayout, + heatmap!, + surface!, + barplot!, + GridPosition, + @L_str, + Reverse, + ylims!, + RGBAf, + Sphere, + lines!, + scatter!, + arrows!, + text!, + Point3f, + mesh!, + RGBf, + Vec3f, + Point3f, + NoShading @doc raw""" plot_wigner( @@ -291,4 +315,590 @@ raw""" """ _figFromChildren(::Nothing) = throw(ArgumentError("No Figure has been found at the top of the layout hierarchy.")) +raw""" + _state_to_bloch(state::QuantumObject{<:Ket}) -> Vector{Float64} + +Convert a pure qubit state (`Ket`) to its Bloch vector representation. + +If the state is not normalized, it is automatically normalized before conversion. + +# Arguments +- `state`: A `Ket` representing a pure quantum state. + +# Returns +A 3-element `Vector{Float64}` representing the Bloch vector `[x, y, z]`. + +# Throws +- `ArgumentError` if the state dimension is not 2. +""" +function _state_to_bloch(state::QuantumObject{<:Ket}) + if !isapprox(norm(state), 1.0, atol = 1e-6) + @warn "State is not normalized. Normalizing before Bloch vector conversion." + state = normalize(state) + end + ψ = state.data + if length(ψ) != 2 + error("Bloch sphere visualization is only supported for qubit states (2-level systems)") + end + x = 2 * real(ψ[1] * conj(ψ[2])) + y = 2 * imag(ψ[1] * conj(ψ[2])) + z = abs2(ψ[1]) - abs2(ψ[2]) + return [x, y, z] +end + +raw""" + _dm_to_bloch(ρ::QuantumObject{<:Operator}) -> Vector{Float64} + +Convert a qubit density matrix (`Operator`) to its Bloch vector representation. + +This function assumes the input is Hermitian. If the density matrix is not Hermitian, a warning is issued. + +# Arguments +- `ρ`: A density matrix (`Operator`) representing a mixed or pure quantum state. + +# Returns +A 3-element `Vector{Float64}` representing the Bloch vector `[x, y, z]`. + +# Throws +- `ArgumentError` if the matrix dimension is not 2. +""" +function _dm_to_bloch(ρ::QuantumObject{<:Operator}) + if !ishermitian(ρ) + @warn "Density matrix is not Hermitian. Results may not be meaningful." + end + if size(ρ, 1) != 2 + error("Bloch sphere visualization is only supported for qubit states (2-level systems)") + end + x = real(ρ[1, 2] + ρ[2, 1]) + y = imag(ρ[2, 1] - ρ[1, 2]) + z = real(ρ[1, 1] - ρ[2, 2]) + return [x, y, z] +end + +function _render_bloch_makie(bloch_vec::Vector{Float64}; location = nothing, kwargs...) + b = Bloch() + add_vectors!(b, bloch_vec) + fig, location = _getFigAndLocation(location) + fig, ax = render(b; location = location, kwargs...) + return fig, ax +end + +@doc raw""" + add_line!( + b::QuantumToolbox.Bloch, + start_point_point::QuantumToolbox.QuantumObject{<:Union{QuantumToolbox.Ket, QuantumToolbox.Bra, QuantumToolbox.Operator}}, + end_point::QuantumToolbox.QuantumObject{<:Union{QuantumToolbox.Ket, QuantumToolbox.Bra, QuantumToolbox.Operator}}; + fmt = "k" + ) + +Add a line between two quantum states or operators on the Bloch sphere visualization. + +# Arguments + +- `b::Bloch`: The Bloch sphere object to modify. +- `start_point_point::QuantumObject`: The start_point_pointing quantum state or operator. Can be a `Ket`, `Bra`, or `Operator`. +- `end_point::QuantumObject`: The ending quantum state or operator. Can be a `Ket`, `Bra`, or `Operator`. +- `fmt::String="k"`: (optional) A format string specifying the line style and color (default is black `"k"`). + +# Description + +This function converts the given quantum objects (states or operators) into their Bloch vector representations and adds a line between these two points on the Bloch sphere visualization. + +# Example + +```julia +b = Bloch() +ψ₁ = normalize(basis(2, 0) + basis(2, 1)) +ψ₂ = normalize(basis(2, 0) - im * basis(2, 1)) +add_line!(b, ψ₁, ψ₂; fmt = "r--") +``` +""" +function QuantumToolbox.add_line!( + b::Bloch, + p1::QuantumObject{<:Union{Ket,Bra,Operator}}, + p2::QuantumObject{<:Union{Ket,Bra,Operator}}; + fmt = "k", +) + coords1 = isket(p1) || isbra(p1) ? _state_to_bloch(p1) : _dm_to_bloch(p1) + coords2 = isket(p2) || isbra(p2) ? _state_to_bloch(p2) : _dm_to_bloch(p2) + return add_line!(b, coords1, coords2; fmt = fmt) +end + +@doc raw""" + QuantumToolbox.add_states!(b::Bloch, states::QuantumObject...) + +Add one or more quantum states to the Bloch sphere visualization by converting them into Bloch vectors. + +# Arguments +- `b::Bloch`: The Bloch sphere object to modify +- `states::QuantumObject...`: One or more quantum states (Ket, Bra, or Operator) + +# Example + +```julia +x = basis(2, 0) + basis(2, 1); +y = basis(2, 0) - im * basis(2, 1); +z = basis(2, 0); +b = Bloch(); +add_states!(b, [x, y, z]) +``` +""" +function QuantumToolbox.add_states!(b::Bloch, states::Vector{<:QuantumObject}) + vecs = map(states) do state + if isket(state) || isbra(state) + return _state_to_bloch(state) + else + return _dm_to_bloch(state) + end + end + append!(b.vectors, [normalize(v) for v in vecs]) + return b.vectors +end + +@doc raw""" + render(b::QuantumToolbox.Bloch; location=nothing) + +Render the Bloch sphere visualization from the given `Bloch` object `b`. + +# Arguments + +- `b::QuantumToolbox.Bloch` + The Bloch sphere object containing states, vectors, and settings to visualize. + +- `location` (optional) + Specifies where to display or save the rendered figure. + - If `nothing` (default), the figure is displayed interactively. + - If a file path (String), the figure is saved to the specified location. + - Other values depend on backend support. + +# Returns + +- A tuple `(fig, axis)` where `fig` is the figure object and `axis` is the axis object used for plotting. + These can be further manipulated or saved by the user. +""" +function QuantumToolbox.render(b::Bloch; location = nothing) + fig, ax = _setup_bloch_plot!(b, location) + _draw_bloch_sphere!(b, ax) + _draw_reference_circles!(ax) + _draw_axes!(ax) + _plot_points!(b, ax) + _plot_lines!(b, ax) + _plot_arcs!(b, ax) + _plot_vectors!(b, ax) + _add_labels!(b, ax) + return fig, ax +end + +raw""" + _setup_bloch_plot!(b::Bloch, location) -> (fig, ax) + +Initialize the figure and `3D` axis for Bloch sphere visualization. + +# Arguments +- `b::Bloch`: Bloch sphere object containing view parameters +- `location`: Figure layout position specification + +# Returns +- `fig`: Created Makie figure +- `ax`: Configured Axis3 object + +Sets up the `3D` coordinate system with appropriate limits and view angles. +""" +function _setup_bloch_plot!(b::Bloch, location) + fig, location = _getFigAndLocation(location) + bg_color = parse(RGBf, b.frame_color) + frame_color = RGBAf(bg_color, b.frame_alpha) + ax = Axis3( + location; + aspect = :data, + limits = (-b.frame_limit, b.frame_limit, -b.frame_limit, b.frame_limit, -b.frame_limit, b.frame_limit), + xgridvisible = false, + ygridvisible = false, + zgridvisible = false, + xticklabelsvisible = false, + yticklabelsvisible = false, + zticklabelsvisible = false, + xticksvisible = false, + yticksvisible = false, + zticksvisible = false, + xlabel = "", + ylabel = "", + zlabel = "", + backgroundcolor = frame_color, + xypanelvisible = false, + xzpanelvisible = false, + yzpanelvisible = false, + xspinesvisible = false, + yspinesvisible = false, + zspinesvisible = false, + protrusions = (0, 0, 0, 0), + viewmode = :fit, + ) + ax.azimuth[] = deg2rad(b.view_angles[1]) + ax.elevation[] = deg2rad(b.view_angles[2]) + return fig, ax +end + +raw""" + _draw_bloch_sphere!(b, ax) + +Draw the translucent sphere representing the Bloch sphere surface. +""" +function _draw_bloch_sphere!(b::Bloch, ax) + n_lon = 4 + n_lat = 4 + radius = 1.0f0 + base_color = parse(RGBf, b.sphere_color) + sphere_color = RGBAf(base_color, b.sphere_alpha) + sphere_mesh = Sphere(Point3f(0), radius) + mesh!(ax, sphere_mesh; color = sphere_color, shading = NoShading, transparency = true) + θ_vals = range(0.0f0, 2π, length = n_lon + 1)[1:(end-1)] + φ_curve = range(0.0f0, π, length = 600) + line_alpha = max(0.05, b.sphere_alpha * 0.5) + for θi in θ_vals + x_line = [radius * sin(ϕ) * cos(θi) for ϕ in φ_curve] + y_line = [radius * sin(ϕ) * sin(θi) for ϕ in φ_curve] + z_line = [radius * cos(ϕ) for ϕ in φ_curve] + lines!(ax, x_line, y_line, z_line; color = RGBAf(0.5, 0.5, 0.5, line_alpha), linewidth = 1, transparency = true) + end + φ_vals = range(0.0f0, π, length = n_lat + 2) + θ_curve = range(0.0f0, 2π, length = 600) + for ϕ in φ_vals + x_ring = [radius * sin(ϕ) * cos(θi) for θi in θ_curve] + y_ring = [radius * sin(ϕ) * sin(θi) for θi in θ_curve] + z_ring = fill(radius * cos(ϕ), length(θ_curve)) + lines!(ax, x_ring, y_ring, z_ring; color = RGBAf(0.5, 0.5, 0.5, line_alpha), linewidth = 1, transparency = true) + end +end + +raw""" + _draw_reference_circles!(ax) + +Draw the three great circles `(XY, YZ, XZ planes)` on the Bloch sphere. + +# Arguments +- `ax`: Makie Axis3 object for drawing + +Adds faint circular guidelines representing the three principal planes. +""" +function _draw_reference_circles!(ax) + wire_color = RGBAf(0.5, 0.5, 0.5, 0.4) + φ = range(0, 2π, length = 100) + # XY, YZ, XZ circles + circles = [ + [Point3f(sin(φi), -cos(φi), 0) for φi in φ], # XY + [Point3f(0, -cos(φi), sin(φi)) for φi in φ], # YZ + [Point3f(sin(φi), 0, cos(φi)) for φi in φ], # XZ + ] + for circle in circles + lines!(ax, circle; color = wire_color, linewidth = 1.0) + end +end + +raw""" + _draw_axes!(ax) + +Draw the three principal axes `(x, y, z)` of the Bloch sphere. + +# Arguments +- `ax`: Makie Axis3 object for drawing + +Creates visible axis lines extending slightly beyond the unit sphere. +""" +function _draw_axes!(ax) + axis_color = RGBAf(0.3, 0.3, 0.3, 0.8) + axis_width = 0.8 + axes = [ + ([Point3f(0, -1.0, 0), Point3f(0, 1.0, 0)], "y"), # Y-axis + ([Point3f(-1.0, 0, 0), Point3f(1.0, 0, 0)], "x"), # X-axis + ([Point3f(0, 0, -1.0), Point3f(0, 0, 1.0)], "z"), # Z-axis + ] + for (points, _) in axes + lines!(ax, points; color = axis_color, linewidth = axis_width) + end +end + +raw""" + _plot_points!(b::Bloch, ax) + +Plot all quantum state points on the Bloch sphere. + +# Arguments +- `b::Bloch`: Contains point data and styling information +- `ax`: Axis3 object for plotting + +Handles both scatter points and line traces based on style specifications. +""" +function _plot_points!(b::Bloch, ax) + for k in 1:length(b.points) + pts = b.points[k] + style = b.point_style[k] + alpha = b.point_alpha[k] + marker = b.point_marker[mod1(k, length(b.point_marker))] + N = size(pts, 2) + + raw_x = pts[2, :] + raw_y = -pts[1, :] + raw_z = pts[3, :] + + ds = vec(sqrt.(sum(abs2, pts; dims = 1))) + if !all(isapprox.(ds, ds[1]; rtol = 1e-12)) + indperm = sortperm(ds) + else + indperm = collect(1:N) + end + this_color = b.point_color[k] + if style == :m + defaults = b.point_default_color + L = length(defaults) + times = ceil(Int, N / L) + big_colors = repeat(b.point_default_color, times)[1:N] + big_colors = big_colors[indperm] + colors = big_colors + else + if this_color === nothing + defaults = b.point_default_color + colors = defaults[mod1(k, length(defaults))] + else + colors = this_color + end + end + if style in (:s, :m) + xs = raw_x[indperm] + ys = raw_y[indperm] + zs = raw_z[indperm] + scatter!( + ax, + xs, + ys, + zs; + color = colors, + markersize = b.point_size[mod1(k, length(b.point_size))], + marker = marker, + transparency = alpha < 1.0, + alpha = alpha, + strokewidth = 0.0, + ) + + elseif style == :l + xs = raw_x + ys = raw_y + zs = raw_z + c = isa(colors, Vector) ? colors[1] : colors + lines!(ax, xs, ys, zs; color = c, linewidth = 2.0, transparency = alpha < 1.0, alpha = alpha) + end + end +end + +raw""" + _plot_lines!(b::Bloch, ax) + +Draw all connecting lines between points on the Bloch sphere. + +# Arguments +- `b::Bloch`: Contains line data and formatting +- `ax`: Axis3 object for drawing + +Processes line style specifications and color mappings. +""" +function _plot_lines!(b::Bloch, ax) + color_map = + Dict("k" => :black, "r" => :red, "g" => :green, "b" => :blue, "c" => :cyan, "m" => :magenta, "y" => :yellow) + for (line, fmt) in b.lines + x, y, z = line + color_char = first(fmt) + color = get(color_map, color_char, :black) + linestyle = if occursin("--", fmt) + :dash + elseif occursin(":", fmt) + :dot + elseif occursin("-.", fmt) + :dashdot + else + :solid + end + lines!(ax, x, y, z; color = color, linewidth = 1.0, linestyle = linestyle) + end +end + +raw""" + _plot_arcs!(b::Bloch, ax) + +Draw circular arcs connecting points on the Bloch sphere surface. + +# Arguments +- `b::Bloch`: Contains arc data points +- `ax`: Axis3 object for drawing + +Calculates great circle arcs between specified points. +""" +function _plot_arcs!(b::Bloch, ax) + for arc_pts in b.arcs + length(arc_pts) >= 2 || continue + v1 = normalize(arc_pts[1]) + v2 = normalize(arc_pts[end]) + n = normalize(cross(v1, v2)) + θ = acos(clamp(dot(v1, v2), -1.0, 1.0)) + if length(arc_pts) == 3 + vm = normalize(arc_pts[2]) + dot(cross(v1, vm), n) < 0 && (θ = 2π - θ) + end + t_range = range(0, θ, length = 100) + arc_points = [Point3f((v1*cos(t) + cross(n, v1)*sin(t))...) for t in t_range] + lines!(ax, arc_points; color = RGBAf(0.8, 0.4, 0.1, 0.9), linewidth = 2.0, linestyle = :solid) + end +end + +raw""" + _plot_vectors!(b::Bloch, ax) + +Draw vectors from origin representing quantum states. + +# Arguments +- `b::Bloch`: Contains vector data +- `ax`: Axis3 object for drawing + +Scales vectors appropriately and adds `3D` arrow markers. +""" +function _plot_vectors!(b::Bloch, ax) + isempty(b.vectors) && return + arrowsize_vec = Vec3f(b.vector_arrowsize...) + r = 1.0 + for (i, v) in enumerate(b.vectors) + color = get(b.vector_color, i, RGBAf(0.2, 0.5, 0.8, 0.9)) + vec = Vec3f(v[2], -v[1], v[3]) + length = norm(vec) + max_length = r * 0.90 + vec = length > max_length ? (vec/length) * max_length : vec + arrows!( + ax, + [Point3f(0, 0, 0)], + [vec], + color = color, + linewidth = b.vector_width, + arrowsize = arrowsize_vec, + arrowcolor = color, + ) + end +end + +raw""" + _add_labels!(ax) + +Add axis labels and state labels to the Bloch sphere. + +# Arguments +- `ax`: Axis3 object for text placement + +Positions standard labels `(x, y, |0⟩, |1⟩)` at appropriate locations. +""" +function _add_labels!(b::Bloch, ax) + label_color = parse(RGBf, b.font_color) + label_size = b.font_size + offset_scale = b.frame_limit + if !isempty(b.xlabel) && !isempty(b.xlabel[1]) + text!( + ax, + L"\textbf{x}", + position = Point3f(0, -offset_scale * b.xlpos[1], 0), + color = label_color, + fontsize = label_size, + align = (:center, :center), + ) + end + if length(b.xlabel) > 1 && !isempty(b.xlabel[2]) + text!( + ax, + L"\textbf{-x}", + position = Point3f(0, -offset_scale * b.xlpos[2], 0), + color = label_color, + fontsize = label_size, + align = (:center, :center), + ) + end + if !isempty(b.ylabel) && !isempty(b.ylabel[1]) + text!( + ax, + L"\textbf{y}", + position = Point3f(offset_scale * b.ylpos[1], 0, 0), + color = label_color, + fontsize = label_size, + align = (:center, :center), + ) + end + if length(b.ylabel) > 1 && !isempty(b.ylabel[2]) + text!( + ax, + L"\textbf{-y}", + position = Point3f(offset_scale * b.ylpos[2], 0, 0), + color = label_color, + fontsize = label_size, + align = (:center, :center), + ) + end + if !isempty(b.zlabel) && !isempty(b.zlabel[1]) + text!( + ax, + L"\mathbf{|0\rangle}", + position = Point3f(0, 0, offset_scale * b.zlpos[1]), + color = label_color, + fontsize = label_size, + align = (:center, :center), + ) + end + if length(b.zlabel) > 1 && !isempty(b.zlabel[2]) + text!( + ax, + L"\mathbf{|1\rangle}", + position = Point3f(0, 0, offset_scale * b.zlpos[2]), + color = label_color, + fontsize = label_size, + align = (:center, :center), + ) + end +end + +@doc raw""" + plot_bloch(::Val{:Makie}, state::QuantumObject{<:Union{Ket,Bra}}; kwargs...) + +Plot a pure quantum state on the Bloch sphere using the `Makie` backend. + +# Arguments +- `state::QuantumObject{<:Union{Ket,Bra}}`: The quantum state to be visualized (`ket` or `bra`). +- `kwargs...`: Additional keyword arguments passed to `_render_bloch_makie`. + +# Details + +Converts the state to its Bloch vector representation and renders it on the Bloch sphere. +If the input is a bra, it is automatically converted to a ket before processing. + +!!! note "Internal function" + This is the `Makie`-specific implementation called by the main `plot_bloch` function. +""" +function QuantumToolbox.plot_bloch(::Val{:Makie}, state::QuantumObject{<:Union{Ket,Bra}}; kwargs...) + state = isbra(state) ? state' : state + bloch_vec = _state_to_bloch(state) + return _render_bloch_makie(bloch_vec; kwargs...) +end + +@doc raw""" + plot_bloch(::Val{:Makie}, ρ::QuantumObject{<:Operator}; kwargs...) + +Plot a density matrix on the Bloch sphere using the Makie backend. + +# Arguments +- `ρ::QuantumObject{<:Operator}`: The density matrix to be visualized. +- `kwargs...`: Additional keyword arguments passed to `_render_bloch_makie`. + +# Details +Converts the density matrix to its Bloch vector representation and renders it on the Bloch sphere. + +!!! note "Internal function" + This is the Makie-specific implementation called by the main `plot_bloch` function. +""" +function QuantumToolbox.plot_bloch(::Val{:Makie}, ρ::QuantumObject{<:Operator}; kwargs...) + bloch_vec = _dm_to_bloch(ρ) + return _render_bloch_makie(bloch_vec; kwargs...) +end + end diff --git a/src/visualization.jl b/src/visualization.jl index e43fb10dd..71c6fc499 100644 --- a/src/visualization.jl +++ b/src/visualization.jl @@ -1,4 +1,14 @@ -export plot_wigner, plot_fock_distribution +export plot_wigner, + plot_fock_distribution, + plot_bloch, + Bloch, + render, + add_points!, + add_vectors!, + add_line!, + add_arc!, + clear!, + add_states! @doc raw""" plot_wigner( @@ -61,3 +71,343 @@ plot_fock_distribution( plot_fock_distribution(::Val{T}, ρ::QuantumObject{SType}; kwargs...) where {T,SType<:Union{Bra,Ket,Operator}} = throw(ArgumentError("The specified plotting library $T is not available. Try running `using $T` first.")) + +@doc raw""" + Bloch() + +A structure representing a Bloch sphere visualization for quantum states. + +# Fields + +## Data storage +- `points::Vector{Matrix{Float64}}`: Points to plot on the Bloch sphere (3D coordinates) +- `vectors::Vector{Vector{Float64}}}`: Vectors to plot on the Bloch sphere +- `lines::Vector{Tuple{Vector{Vector{Float64}},String,Dict{Any,Any}}}`: Lines to draw on the sphere (points, style, properties) +- `arcs::Vector{Vector{Vector{Float64}}}}`: Arcs to draw on the sphere + +## Style properties + +- `font_color::String`: Color of axis labels and text +- `font_size::Int`: Font size for labels (default: 15) +- `frame_alpha::Float64`: Transparency of the frame background +- `frame_color::String`: Background color of the frame +- `frame_limit::Float64`: Axis limits for the 3D frame (symmetric around origin) + +## Point properties + +- `point_default_color::Vector{String}}`: Default color cycle for points +- `point_color::Vector{String}}`: Colors for point markers +- `point_marker::Vector{Symbol}}`: Marker shapes (default: [:circle, :rect, :diamond, :utriangle]) +- `point_size::Vector{Int}}`: Marker sizes +- `point_style::Vector{Symbol}}`: Marker styles +- `point_alpha::Vector{Float64}}`: Marker transparencies + +## Sphere properties + +- `sphere_color::String`: Color of Bloch sphere surface +- `sphere_alpha::Float64`: Transparency of sphere surface (default: 0.2) + +# Vector properties + +- `vector_color`::Vector{String}: Colors for vectors +- `vector_width`::Float64: Width of vectors +- `vector_arrowsize`::NTuple{3, Real}: Arrow size parameters as (head length, head width, stem width) + +## Layout properties + +- `view_angles::Tuple{Int,Int}}`: Azimuthal and elevation viewing angles in degrees (default: (-60, 30)) + +## Label properties +- `xlabel::Vector{AbstractString}}`: Labels for x-axis (default: [L"x", ""]) +- `xlpos::Vector{Float64}}`: Positions of x-axis labels (default: [1.0, -1.0]) +- `ylabel::Vector{AbstractString}}`: Labels for y-axis (default: [L"y", ""]) +- `ylpos::Vector{Float64}}`: Positions of y-axis labels (default: [1.0, -1.0]) +- `zlabel::Vector{AbstractString}}`: Labels for z-axis (default: [L"|0\rangle", L"|1\rangle"]) +- `zlpos::Vector{Float64}}`: Positions of z-axis labels (default: [1.0, -1.0]) +""" +@kwdef mutable struct Bloch + points::Vector{Matrix{Float64}} = Vector{Matrix{Float64}}() + vectors::Vector{Vector{Float64}} = Vector{Vector{Float64}}() + lines::Vector{Tuple{Vector{Vector{Float64}},String}} = Vector{Tuple{Vector{Vector{Float64}},String}}() + arcs::Vector{Vector{Vector{Float64}}} = Vector{Vector{Vector{Float64}}}() + font_color::String = "#333333" + font_size::Int = 15 + frame_alpha::Float64 = 0.0 + frame_color::String = "white" + frame_limit::Float64 = 1.14 + point_default_color::Vector{String} = ["blue", "red", "green", "orange"] + point_color::Vector{Union{Nothing,String}} = Union{Nothing,String}[] + point_marker::Vector{Symbol} = [:circle, :rect, :diamond, :utriangle] + point_size::Vector{Float64} = [5.5, 6.2, 6.5, 7.5] + point_style::Vector{Symbol} = Symbol[] + point_alpha::Vector{Float64} = Float64[] + sphere_alpha::Float64 = 0.2 + sphere_color::String = "#FFDDDD" + vector_color::Vector{String} = ["green", "orange", "blue", "red"] + vector_width::Float64 = 0.025 + vector_arrowsize::NTuple{3,Real} = (0.07, 0.08, 0.08) + view_angles::Tuple{Int,Int} = (-60, 30) + xlabel::Vector{AbstractString} = ["x", ""] + xlpos::Vector{Float64} = [1.0, -1.0] + ylabel::Vector{AbstractString} = ["y", ""] + ylpos::Vector{Float64} = [1.0, -1.0] + zlabel::Vector{AbstractString} = ["|0\rangle", "|1\rangle"] + zlpos::Vector{Float64} = [1.0, -1.0] +end + +@doc raw""" + add_vectors!(b::Bloch, vec::Vector{<:Real}) + +Add a single normalized vector to the Bloch sphere visualization. + +# Arguments +- `b::Bloch`: The Bloch sphere object to modify +- `vec::Vector{<:Real}`: A 3D vector to add (will be normalized) +- `vecs::Vector{<:Vector{<:Real}}}`: List of 3D vectors to add (each will be normalized) + +# Example +```jldoctest +julia> b = Bloch(); + +julia> add_vectors!(b, [1, 0, 0]) +1-element Vector{Vector{Float64}}: + [1.0, 0.0, 0.0] +``` + +We can also add multiple normalized vectors to the Bloch sphere visualization. + +```jldoctest +julia> b = Bloch(); + +julia> add_vectors!(b, [[1, 0, 0], [0, 1, 0]]) +2-element Vector{Vector{Float64}}: + [1.0, 0.0, 0.0] + [0.0, 1.0, 0.0] +``` +""" +function add_vectors!(b::Bloch, vec::Vector{<:Real}) + normalized_vec = normalize(convert(Vector{Float64}, vec)) + return push!(b.vectors, normalized_vec) +end +function add_vectors!(b::Bloch, vecs::Vector{<:Vector{<:Real}}) + return append!(b.vectors, [normalize(convert(Vector{Float64}, v)) for v in vecs]) +end + +@doc raw""" + add_points!(b::Bloch, pnt::Vector{<:Real}; meth::Symbol = :s, color = "blue", alpha = 1.0) + +Add a single point to the Bloch sphere visualization. + +# Arguments +- b::Bloch: The Bloch sphere object to modify +- pnt::Vector{Float64}: A 3D point to add +- meth::Symbol=:s: Display method (:s for single point, :m for multiple, :l for line) +- color: Color of the point (defaults to first default color if nothing) +- alpha=1.0: Transparency (1.0 = opaque, 0.0 = transparent) +""" +function add_points!(b::Bloch, pnt::Vector{Float64}; meth::Symbol = :s, color = nothing, alpha = 1.0) + return add_points!(b, reshape(pnt, 3, 1); meth, color, alpha) +end +function add_points!(b::Bloch, pnts::Vector{Vector{Float64}}; meth::Symbol = :s, color = nothing, alpha = 1.0) + return add_points!(b, Matrix(hcat(pnts...)'); meth, color, alpha) +end + +@doc raw""" + add_points!(b::Bloch, pnts::Matrix{Float64}; meth::Symbol = :s, color = nothing, alpha = 1.0) + +Add multiple points to the Bloch sphere visualization. + +# Arguments + +- b::Bloch: The Bloch sphere object to modify +- pnts::Matrix{Float64}: 3×N matrix of points (each column is a point) +- meth::Symbol=:s: Display method (:s for single point, :m for multiple, :l for line) +- color: Color of the points (defaults to first default color if nothing) +- alpha=1.0: Transparency (1.0 = opaque, 0.0 = transparent) +``` +""" +function add_points!( + b::Bloch, + pnts::Matrix{<:Real}; + meth::Symbol = :s, + color::Union{Nothing,String} = nothing, + alpha::Float64 = 1.0, +) + if size(pnts, 1) != 3 + error("Points must be a 3×N matrix where each column is [x; y; z]") + end + if !(meth in (:s, :m, :l)) + error("`meth` must be :s, :m, or :l") + end + push!(b.points, convert(Matrix{Float64}, pnts)) + push!(b.point_style, meth) + push!(b.point_alpha, alpha) + if color === nothing + push!(b.point_color, nothing) + else + push!(b.point_color, color) + end + return nothing +end + +@doc raw""" + add_line!(b::Bloch, p1::Vector{<:Real}, p2::Vector{<:Real}; fmt = "k", kwargs...) + +Add a line between two points on the Bloch sphere. + +# Arguments +- b::Bloch: The Bloch sphere object to modify +- p1::Vector{<:Real}: First 3D point +- p2::Vector{<:Real}: Second 3D point +- fmt="k": Line format string (matplotlib style) +""" +function add_line!(b::Bloch, p1::Vector{<:Real}, p2::Vector{<:Real}; fmt = "k") + if length(p1) != 3 || length(p2) != 3 + error("Points must be 3D vectors") + end + x = [p1[2], p2[2]] + y = [-p1[1], -p2[1]] + z = [p1[3], p2[3]] + push!(b.lines, (([x, y, z]), fmt)) + return b +end + +@doc raw""" + add_arc!(b::Bloch, p1::Vector{<:Real}, p2::Vector{<:Real}, p3::Vector{<:Real}) + +Add a circular arc through three points on the Bloch sphere. + +# Arguments + +- b::Bloch: The Bloch sphere object to modify +- p1::Vector{<:Real}: First 3D point +- p2::Vector{<:Real}: Second 3D point (middle point) +- p3::Vector{<:Real}: Third 3D point + +# Examples + +```jldoctest +julia> b = Bloch(); + +julia> add_arc!(b, [1, 0, 0], [0, 1, 0], [0, 0, 1]) +1-element Vector{Vector{Vector{Float64}}}: + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] +``` +""" +function add_arc!(b::Bloch, p1::Vector{<:Real}, p2::Vector{<:Real}) + return push!(b.arcs, [convert(Vector{Float64}, p1), convert(Vector{Float64}, p2)]) +end +function add_arc!(b::Bloch, p1::Vector{<:Real}, p2::Vector{<:Real}, p3::Vector{<:Real}) + return push!(b.arcs, [convert(Vector{Float64}, p1), convert(Vector{Float64}, p2), convert(Vector{Float64}, p3)]) +end + +@doc raw""" + QuantumToolbox.add_states!(b::Bloch, states::QuantumObject...) + +Add one or more quantum states to the Bloch sphere visualization by converting them into Bloch vectors. + +# Arguments + +- `b::Bloch`: The Bloch sphere object to modify +- `states::QuantumObject...`: One or more quantum states (Ket, Bra, or Operator) + +""" +function add_states! end + +@doc raw""" + clear!(b::Bloch) + +Clear all graphical elements (points, vectors, lines, arcs) from the given Bloch sphere object `b`. + +# Arguments + +- `b::Bloch` + The Bloch sphere instance whose contents will be cleared. + +# Returns + +- The updated `Bloch` object `b` with all points, vectors, lines, and arcs removed. +""" +function clear!(b::Bloch) + empty!(b.points) + empty!(b.point_color) + empty!(b.point_style) + empty!(b.point_alpha) + empty!(b.vectors) + empty!(b.lines) + empty!(b.arcs) + return b +end + +@doc raw""" + render(b::QuantumToolbox.Bloch; location=nothing) + +Render the Bloch sphere visualization from the given `Bloch` object `b`. + +# Arguments + +- `b::QuantumToolbox.Bloch` + The Bloch sphere object containing states, vectors, and settings to visualize. + +- `location` (optional) + Specifies where to display or save the rendered figure. + - If `nothing` (default), the figure is displayed interactively. + - If a file path (String), the figure is saved to the specified location. + - Other values depend on backend support. + +# Returns + +- A tuple `(fig, axis)` where `fig` is the figure object and `axis` is the axis object used for plotting. + These can be further manipulated or saved by the user. +""" +function render end + +@doc raw""" + plot_bloch( + state::QuantumObject{<:Union{Ket,Bra,Operator}}; + library::Union{Symbol, Val} = :Makie, + kwargs... + ) + +Plot the state of a two-level quantum system on the Bloch sphere. + +The `library` keyword argument specifies the plotting backend to use. The default is `:Makie`, which uses the [`Makie.jl`](https://github.com/MakieOrg/Makie.jl) plotting library. This function internally dispatches to a type-stable version based on `Val(:Makie)` or other plotting backends. + +# Arguments +- `state::QuantumObject`: The quantum state to be visualized. Can be a ket, bra, or operator. +- `library::Union{Symbol, Val}`: The plotting backend, either as a `Symbol` (e.g. `:Makie`) or a `Val` (e.g. `Val(:Makie)`). Default is `:Makie`. +- `kwargs...`: Additional keyword arguments passed to the specific plotting implementation. + +!!! note "Import library first" + The plotting backend library must be imported before use. + +!!! warning "Beware of type-stability!" + For improved performance and type-stability, prefer passing `Val(:Makie)` instead of `:Makie`. See [Performance Tips](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) for details. +""" +function plot_bloch(state::QuantumObject{<:Union{Ket,Bra,Operator}}; library::Union{Symbol,Val} = :Makie, kwargs...) + lib_val = library isa Symbol ? Val(library) : library + return plot_bloch(lib_val, state; kwargs...) +end + +@doc raw""" + plot_bloch(::Val{T}, state::QuantumObject; kwargs...) where {T} + +Fallback implementation for unsupported plotting backends. + +# Arguments +- `::Val{T}`: The unsupported backend specification. +- `state::QuantumObject`: The quantum state that was attempted to be plotted. +- `kwargs...`: Ignored keyword arguments. + +# Throws +- `ErrorException`: Always throws an error indicating the backend `T` is unsupported. + +# Note +This function serves as a fallback when an unsupported backend is requested. Currently supported backends include: +- `:Makie` (using `Makie.jl`) + +See the main `plot_bloch` documentation for supported backends. +""" +function plot_bloch(::Val{T}, state::QuantumObject; kwargs...) where {T} + return error("Unsupported backend: $T. Try :Makie or another supported library.") +end diff --git a/test/ext-test/cpu/makie/makie_ext.jl b/test/ext-test/cpu/makie/makie_ext.jl index 4f68e696f..7887c9da0 100644 --- a/test/ext-test/cpu/makie/makie_ext.jl +++ b/test/ext-test/cpu/makie/makie_ext.jl @@ -61,3 +61,126 @@ pos = fig[2, 3] fig1, ax = @test_logs (:warn,) plot_fock_distribution(ψ * 2; library = Val(:Makie), location = pos) end + +@testset "Makie Bloch sphere" begin + ρ = 0.7*ket2dm(basis(2, 0)) + 0.3*ket2dm(basis(2, 1)) + fig, ax = plot_bloch(ρ) + @test fig isa Figure + @test ax isa Axis3 + + ψ = (basis(2, 0) + basis(2, 1))/√2 + fig, ax = plot_bloch(ψ) + @test fig isa Figure + @test ax isa Axis3 + + ϕ = dag(ψ) + fig, ax = plot_bloch(ϕ) + @test fig isa Figure + @test ax isa Axis3 + + fig = Figure() + pos = fig[1, 1] + fig1, ax = plot_bloch(ψ; location = pos) + @test fig1 === fig + + b = Bloch() + add_points!(b, [0.0, 0.0, 1.0]) + @test length(b.points) == 1 + @test b.points[1] ≈ [0.0, 0.0, 1.0] + + pts = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] + add_points!(b, hcat(pts...)) + @test length(b.points) == 2 + @test b.points[2] ≈ hcat(pts...) + + b = Bloch() + add_vectors!(b, [1.0, 1.0, 0.0]) + @test length(b.vectors) == 1 + @test isapprox(norm(b.vectors[1]), 1.0) + + vecs = [[0.0, 0.0, 1.0], [1.0, 0.0, 0.0]] + add_vectors!(b, vecs) + @test length(b.vectors) == 3 + @test all(norm(v) ≈ 1.0 for v in b.vectors) + + b = Bloch() + add_line!(b, [0, 0, 0], [1, 0, 0]) + @test length(b.lines) == 1 + @test b.lines[1][1][3] ≈ [0.0, 0.0] + + add_arc!(b, [0, 0, 1], [0, 1, 0], [1, 0, 0]) + @test length(b.arcs) == 1 + @test b.arcs[1][3] == [1.0, 0.0, 0.0] + + b = Bloch() + add_points!(b, [0.0, 0.0, 1.0]) + add_vectors!(b, [1.0, 0.0, 0.0]) + add_line!(b, [0, 0, 0], [1, 0, 0]) + add_arc!(b, [0, 1, 0], [0, 0, 1], [1, 0, 0]) + clear!(b) + @test isempty(b.points) + @test isempty(b.vectors) + @test isempty(b.lines) + @test isempty(b.arcs) + b = Bloch() + add_points!(b, hcat([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0])) + add_vectors!(b, [[1, 1, 0], [0, 1, 1]]) + add_line!(b, [0, 0, 0], [1, 1, 1]) + add_arc!(b, [1, 0, 0], [0, 1, 0], [0, 0, 1]) + try + fig, ax = QuantumToolbox.render(b) + @test !isnothing(fig) + @test !isnothing(ax) + catch e + @test false + @info "Render threw unexpected error" exception=e + end + b = Bloch() + ψ₁ = normalize(basis(2, 0) + basis(2, 1)) + ψ₂ = normalize(basis(2, 0) - im * basis(2, 1)) + add_line!(b, ψ₁, ψ₂; fmt = "r--") + try + fig, ax = QuantumToolbox.render(b) + @test !isnothing(fig) + @test !isnothing(ax) + catch e + @test false + @info "Render threw unexpected error" exception=e + end + b = Bloch() + x = basis(2, 0) + basis(2, 1) + y = basis(2, 0) - im * basis(2, 1) + z = basis(2, 0) + add_states!(b, [x, y, z]) + th = range(0, 2π; length = 20); + xp = cos.(th); + yp = sin.(th); + zp = zeros(20); + pnts = [xp, yp, zp]; + pnts = Matrix(hcat(xp, yp, zp)'); + add_points!(b, pnts); + vec = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; + add_vectors!(b, vec); + add_line!(b, [1, 0, 0], [0, 1, 0]) + add_arc!(b, [1, 0, 0], [0, 1, 0], [0, 0, 1]) + try + fig, ax = render(b) + @test !isnothing(fig) + @test !isnothing(ax) + catch e + @test false + @info "Render threw unexpected error" exception=e + end + b = Bloch() + ψ₁ = normalize(basis(2, 0) + basis(2, 1)) + ψ₂ = normalize(basis(2, 0) - im * basis(2, 1)) + add_line!(b, ψ₁, ψ₂; fmt = "r--") + try + fig, ax = render(b) + @test !isnothing(fig) + @test !isnothing(ax) + catch e + @test false + @info "Render threw unexpected error" exception=e + end +end