diff --git a/CHANGELOG.md b/CHANGELOG.md index d231ec1ae..49e8cbeef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +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]) +- Implement Bloch Sphere rendering and align style with qutip. ([#472], [#480]) ## [v0.31.1] Release date: 2025-05-16 @@ -235,3 +235,4 @@ Release date: 2024-11-13 [#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 +[#480]: https://github.com/qutip/QuantumToolbox.jl/issues/480 diff --git a/Project.toml b/Project.toml index 3159c0b3a..6f53877f9 100644 --- a/Project.toml +++ b/Project.toml @@ -12,6 +12,7 @@ Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" IncompleteLU = "40713840-3770-5561-ab4c-a76e7d0d7895" +LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae" OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8" @@ -51,6 +52,7 @@ GPUArrays = "10, 11" Graphs = "1.7" IncompleteLU = "0.2" KernelAbstractions = "0.9.2" +LaTeXStrings = "1.2" LinearAlgebra = "1" LinearSolve = "2, 3" Makie = "0.20, 0.21, 0.22" diff --git a/docs/make.jl b/docs/make.jl index 6769ec0ea..50f4696f8 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -18,8 +18,8 @@ end DocMeta.setdocmeta!(QuantumToolbox, :DocTestSetup, doctest_setup; recursive = true) # some options for `makedocs` -const DRAFT = false # set `true` to disable cell evaluation -const DOCTEST = true # set `false` to skip doc tests +const DRAFT = get(ENV, "DRAFT", false) == "true" # `DRAFT = true` disables cell evaluation +const DOCTEST = get(ENV, "DOCTEST", true) == true # `DOCTEST = false` skips doc tests # generate bibliography bib = CitationBibliography( @@ -51,7 +51,6 @@ 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", @@ -65,6 +64,7 @@ const PAGES = [ "Hierarchical Equations of Motion" => "users_guide/HEOM.md", "Solving for Steady-State Solutions" => "users_guide/steadystate.md", "Two-time correlation functions" => "users_guide/two_time_corr_func.md", + "Plotting on the Bloch Sphere" => "users_guide/plotting_the_bloch_sphere.md", "QuantumToolbox Settings" => "users_guide/settings.md", "Extensions" => [ "Extension for CUDA.jl" => "users_guide/extensions/cuda.md", diff --git a/docs/src/resources/api.md b/docs/src/resources/api.md index e99247123..6b3b4584d 100644 --- a/docs/src/resources/api.md +++ b/docs/src/resources/api.md @@ -319,13 +319,18 @@ meshgrid ```@docs plot_wigner plot_fock_distribution -plot_bloch +``` + +### [Bloch Sphere](@id doc-API:Bloch-Sphere) + +```@docs Bloch +plot_bloch render add_points! add_vectors! add_line! add_arc! -clear! add_states! -``` +clear! +``` \ No newline at end of file diff --git a/docs/src/users_guide/plotting_the_bloch_sphere.md b/docs/src/users_guide/plotting_the_bloch_sphere.md index 44ff42c3e..4bed2b208 100644 --- a/docs/src/users_guide/plotting_the_bloch_sphere.md +++ b/docs/src/users_guide/plotting_the_bloch_sphere.md @@ -7,61 +7,65 @@ using CairoMakie CairoMakie.enable_only_mime!(MIME"image/svg+xml"()) ``` -## [Introduction](@id doc:Bloch_sphere_rendering) +## Introduction 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). +In [`QuantumToolbox`](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: +In [`QuantumToolbox`](https://qutip.org/QuantumToolbox.jl/), creating a [`Bloch`](@ref) sphere is accomplished by calling either: ```@example Bloch_sphere_rendering -b = Bloch(); +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, _ = render(b) fig ``` -## Add a Single Data Point +See the [API documentation for Bloch sphere](@ref doc-API:Bloch-Sphere) for a full list of other available functions. + +## 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); +pnt = [1 / sqrt(3), 1 / sqrt(3), 1 / sqrt(3)] +add_points!(b, pnt) +fig, _ = render(b) fig ``` -## Add a Single Vector +## Add a single vector -and then a single vector via [`add_vectors!`](@ref): +Add a single vector via [`add_vectors!`](@ref): ```@example Bloch_sphere_rendering -vec = [0, 1, 0]; +vec = [0, 1, 0] add_vectors!(b, vec) fig, _ = render(b) fig ``` -and then add another vector corresponding to the ``|0\rangle`` state: +## Add a single quantum state + +Add another vector corresponding to the ``|0\rangle`` state: ```@example Bloch_sphere_rendering -x = basis(2, 0) -add_states!(b, [x]) +z0 = basis(2, 0) +add_states!(b, z0) fig, _ = render(b) fig ``` -## Add Multiple Vectors +## Add multiple data -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: +We can also plot multiple points, vectors, and states at the same time by passing arrays instead of individual elements via [`add_points!`](@ref), [`add_vectors!`](@ref), and [`add_states!`](@ref), respectively. 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) @@ -73,14 +77,16 @@ Now on the same [`Bloch`](@ref) sphere, we can plot the three states via [`add_s ```@example Bloch_sphere_rendering x = basis(2, 0) + basis(2, 1) -y = basis(2, 0) - im * 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 ``` +!!! note "State normalization" + The function [`add_states!`](@ref) will automatically normalize the given quantum state(s), while [`add_vectors!`](@ref) does not normalize the given vectors. + A similar method works for adding vectors: ```@example Bloch_sphere_rendering @@ -91,70 +97,107 @@ fig, _ = render(b) fig ``` -# Add Arc, Line, and Vector +# Add lines and arcs 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]) +add_line!(b, x, y) +add_arc!(b, y, z) fig, _ = render(b) fig ``` -## Add Multiple Points +## 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); + +th = LinRange(0, 2π, 20) +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 +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) function: ```@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); +xz = zeros(20) +yz = sin.(th) +zz = cos.(th) +add_points!(b, [xz, yz, zz]) +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. +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. Again, we have had to call [`add_points!`](@ref) twice because adding more than one set of multiple data points is not supported by the [`add_points!`](@ref) function. -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. +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` field (see [Configuring the Bloch sphere](@ref doc:Configuring-the-Bloch-sphere) below). Again, after [`clear!`](@ref): ```@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); + +xp = cos.(th) +yp = sin.(th) +zp = zeros(20) +pnts = [xp, yp, zp] +add_points!(b, pnts, meth=:m) # add `meth=:m` to signify 'multi' colored points +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); +pnts = [xz, yz, zz] +add_points!(b, pnts) # no `meth=:m` +fig, ax = render(b) fig ``` + +## [Configuring the Bloch sphere](@id doc:Configuring-the-Bloch-sphere) + +At the end of the last section we saw that the colors and marker shapes of the data plotted on the Bloch sphere are automatically varied according to the number of points and vectors added. But what if you want a different choice of color, or you want your sphere to be purple with different axes labels? Well then you are in luck as the Bloch class has many attributes which one can control. Assuming `b = Bloch()`: + +| **Field** | **Description** | **Default setting** | +|:----------|:----------------|:--------------------| +| `b.points` | Points to plot on the Bloch sphere (3D coordinates) | `Vector{Matrix{Float64}}()` (empty) | +| `b.vectors` | Vectors to plot on the Bloch sphere | `Vector{Vector{Float64}}()` (empty) | +| `b.lines` | Lines to draw on the sphere (points, style, properties) | `Vector{Tuple{Vector{Vector{Float64}},String}}()` (empty) | +| `b.arcs` | Arcs to draw on the sphere | `Vector{Vector{Vector{Float64}}}()` (empty) | +| `b.font_color` | Color of axis labels and text | `"black"` | +| `b.font_size` | Font size for labels | `15` | +| `b.frame_alpha` | Transparency of the frame background | `0.1` | +| `b.frame_color` | Background color of the frame | `"gray"` | +| `b.frame_limit` | Axis limits for the 3D frame (symmetric around origin) | `1.2` | +| `b.point_default_color` | Default color cycle for points | `["blue", "red", "green", "#CC6600"]` | +| `b.point_color` | List of colors for Bloch point markers to cycle through | `Union{Nothing,String}[]` | +| `b.point_marker` | List of point marker shapes to cycle through | `[:circle, :rect, :diamond, :utriangle]` | +| `b.point_size` | List of point marker sizes (not all markers look the same size when plotted) | `[5.5, 6.2, 6.5, 7.5]` | +| `b.point_style` | List of marker styles | `Symbol[]` | +| `b.point_alpha` | List of marker transparencies | `Float64[]` | +| `b.sphere_color` | Color of Bloch sphere surface | `0.2` | +| `b.sphere_alpha` | Transparency of sphere surface | `"#FFDDDD"` | +| `b.vector_color` | Colors for vectors | `["green", "#CC6600", "blue", "red"]` | +| `b.vector_width` | Width of vectors | `0.025` | +| `b.vector_arrowsize` | Arrow size parameters as (head length, head width, stem width) | `[0.07, 0.08, 0.08]` | +| `b.view` | Azimuthal and elevation viewing angles in degrees | `[30, 30]` | +| `b.xlabel` | Labels for x-axis | `[L"x", ""]` (``+x`` and ``-x``) | +| `b.xlpos` | Positions of x-axis labels | `[1.0, -1.0]` | +| `b.ylabel` | Labels for y-axis | `[L"y", ""]` (``+y`` and ``-y``) | +| `b.ylpos` | Positions of y-axis labels | `[1.0, -1.0]` | +| `b.zlabel` | Labels for z-axis | `[L"\|0\rangle", L"\|1\rangle]"` (``+z`` and ``-z``) | +| `b.zlpos` | Positions of z-axis labels | `[1.0, -1.0]` | + +These properties can also be accessed via the `print` command: + +```@example Bloch_sphere_rendering +b = Bloch() +print(b) +``` \ No newline at end of file diff --git a/ext/QuantumToolboxMakieExt.jl b/ext/QuantumToolboxMakieExt.jl index 352e1ec85..8e4b83f98 100644 --- a/ext/QuantumToolboxMakieExt.jl +++ b/ext/QuantumToolboxMakieExt.jl @@ -1,8 +1,10 @@ module QuantumToolboxMakieExt using QuantumToolbox -using LinearAlgebra: cross, deg2rad, normalize, size -using Makie: +import QuantumToolbox: _state_to_bloch + +import LinearAlgebra: cross, deg2rad, normalize, size +import Makie: Axis, Axis3, Colorbar, @@ -315,66 +317,6 @@ 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) @@ -384,97 +326,21 @@ function _render_bloch_makie(bloch_vec::Vector{Float64}; location = nothing, kwa 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(b::Bloch; location=nothing) -Render the Bloch sphere visualization from the given `Bloch` object `b`. +Render the Bloch sphere visualization from the given [`Bloch`](@ref) 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. +- `b::Bloch`: The Bloch sphere object containing states, vectors, and settings to visualize. +- `location`: 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. +- 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) @@ -532,10 +398,12 @@ function _setup_bloch_plot!(b::Bloch, location) yspinesvisible = false, zspinesvisible = false, protrusions = (0, 0, 0, 0), + perspectiveness = 0.2, viewmode = :fit, ) - ax.azimuth[] = deg2rad(b.view_angles[1]) - ax.elevation[] = deg2rad(b.view_angles[2]) + length(b.view) == 2 || throw(ArgumentError("The length of `Bloch.view` must be 2.")) + ax.azimuth[] = deg2rad(b.view[1]) + ax.elevation[] = deg2rad(b.view[2]) return fig, ax end @@ -551,7 +419,7 @@ function _draw_bloch_sphere!(b::Bloch, ax) 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) + mesh!(ax, sphere_mesh; color = sphere_color, shading = NoShading, transparency = true, rasterize = 3) θ_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) @@ -586,9 +454,9 @@ function _draw_reference_circles!(ax) φ = 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 + [Point3f(cos(φi), sin(φi), 0) for φi in φ], # XY + [Point3f(0, cos(φi), sin(φi)) for φi in φ], # YZ + [Point3f(cos(φi), 0, sin(φi)) for φi in φ], # XZ ] for circle in circles lines!(ax, circle; color = wire_color, linewidth = 1.0) @@ -609,9 +477,9 @@ 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 + ([Point3f(1.0, 0, 0), Point3f(-1.0, 0, 0)], "x"), # X-axis + ([Point3f(0, 1.0, 0), Point3f(0, -1.0, 0)], "y"), # Y-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) @@ -637,8 +505,8 @@ function _plot_points!(b::Bloch, ax) marker = b.point_marker[mod1(k, length(b.point_marker))] N = size(pts, 2) - raw_x = pts[2, :] - raw_y = -pts[1, :] + raw_x = pts[1, :] + raw_y = pts[2, :] raw_z = pts[3, :] ds = vec(sqrt.(sum(abs2, pts; dims = 1))) @@ -741,10 +609,10 @@ function _plot_arcs!(b::Bloch, ax) θ = 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π - θ) + 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] + 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 @@ -766,7 +634,7 @@ function _plot_vectors!(b::Bloch, ax) 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]) + vec = Vec3f(v...) length = norm(vec) max_length = r * 0.90 vec = length > max_length ? (vec/length) * max_length : vec @@ -778,6 +646,7 @@ function _plot_vectors!(b::Bloch, ax) linewidth = b.vector_width, arrowsize = arrowsize_vec, arrowcolor = color, + rasterize = 3, ) end end @@ -793,112 +662,87 @@ Add axis labels and state labels to the Bloch sphere. Positions standard labels `(x, y, |0⟩, |1⟩)` at appropriate locations. """ function _add_labels!(b::Bloch, ax) + length(b.xlabel) == 2 || throw(ArgumentError("The length of `Bloch.xlabel` must be 2.")) + length(b.ylabel) == 2 || throw(ArgumentError("The length of `Bloch.ylabel` must be 2.")) + length(b.zlabel) == 2 || throw(ArgumentError("The length of `Bloch.zlabel` must be 2.")) + length(b.xlpos) == 2 || throw(ArgumentError("The length of `Bloch.xlpos` must be 2.")) + length(b.ylpos) == 2 || throw(ArgumentError("The length of `Bloch.ylpos` must be 2.")) + length(b.zlpos) == 2 || throw(ArgumentError("The length of `Bloch.zlpos` must be 2.")) + 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 + + (b.xlabel[1] == "") || text!( + ax, + b.xlabel[1], + position = Point3f(offset_scale * b.xlpos[1], 0, 0), + color = label_color, + fontsize = label_size, + align = (:center, :center), + ) + (b.xlabel[2] == "") || text!( + ax, + b.xlabel[2], + position = Point3f(offset_scale * b.xlpos[2], 0, 0), + color = label_color, + fontsize = label_size, + align = (:center, :center), + ) + (b.ylabel[1] == "") || text!( + ax, + b.ylabel[1], + position = Point3f(0, offset_scale * b.ylpos[1], 0), + color = label_color, + fontsize = label_size, + align = (:center, :center), + ) + (b.ylabel[2] == "") || text!( + ax, + b.ylabel[2], + position = Point3f(0, offset_scale * b.ylpos[2], 0), + color = label_color, + fontsize = label_size, + align = (:center, :center), + ) + (b.zlabel[1] == "") || text!( + ax, + b.zlabel[1], + position = Point3f(0, 0, offset_scale * b.zlpos[1]), + color = label_color, + fontsize = label_size, + align = (:center, :center), + ) + (b.zlabel[2] == "") || text!( + ax, + b.zlabel[2], + position = Point3f(0, 0, offset_scale * b.zlpos[2]), + color = label_color, + fontsize = label_size, + align = (:center, :center), + ) + return nothing end @doc raw""" - plot_bloch(::Val{:Makie}, state::QuantumObject{<:Union{Ket,Bra}}; kwargs...) + plot_bloch(::Val{:Makie}, state::QuantumObject; 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`). +- `state::QuantumObject{<:Union{Ket,Bra}}`: The quantum state ([`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref)) to be visualized. - `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 +function QuantumToolbox.plot_bloch( + ::Val{:Makie}, + state::QuantumObject{OpType}; + kwargs..., +) where {OpType<:Union{Ket,Bra,Operator}} 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/QuantumToolbox.jl b/src/QuantumToolbox.jl index 0944175b3..915c4abe8 100644 --- a/src/QuantumToolbox.jl +++ b/src/QuantumToolbox.jl @@ -58,6 +58,7 @@ import Distributed: RemoteChannel import FFTW: fft, ifft, fftfreq, fftshift import Graphs: connected_components, DiGraph import IncompleteLU: ilu +import LaTeXStrings: @L_str import Pkg import Random: AbstractRNG, default_rng, seed! import SpecialFunctions: loggamma diff --git a/src/settings.jl b/src/settings.jl index b9be9b0b2..14f693af8 100644 --- a/src/settings.jl +++ b/src/settings.jl @@ -4,9 +4,11 @@ Base.@kwdef mutable struct Settings end function Base.show(io::IO, s::Settings) + # To align the output and make it easier to read + # we use rpad `11`, which is the length of string: `auto_tidyup` println(io, "QuantumToolbox.jl Settings") println(io, "--------------------------") - map(x -> println(io, "$x = ", getfield(s, x)), fieldnames(Settings)) + map(n -> println(io, rpad("$n", 11, " "), " = ", getfield(s, n)), fieldnames(Settings)) return nothing end diff --git a/src/visualization.jl b/src/visualization.jl index 71c6fc499..a75aab922 100644 --- a/src/visualization.jl +++ b/src/visualization.jl @@ -1,14 +1,6 @@ -export plot_wigner, - plot_fock_distribution, - plot_bloch, - Bloch, - render, - add_points!, - add_vectors!, - add_line!, - add_arc!, - clear!, - add_states! +export plot_wigner +export plot_fock_distribution +export plot_bloch, Bloch, render, add_points!, add_vectors!, add_line!, add_arc!, clear!, add_states! @doc raw""" plot_wigner( @@ -88,7 +80,7 @@ A structure representing a Bloch sphere visualization for quantum states. ## Style properties - `font_color::String`: Color of axis labels and text -- `font_size::Int`: Font size for labels (default: 15) +- `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) @@ -96,46 +88,46 @@ A structure representing a Bloch sphere visualization for quantum states. ## 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 +- `point_color::Vector{String}}`: List of colors for Bloch point markers to cycle through +- `point_marker::Vector{Symbol}}`: List of point marker shapes to cycle through. Default: `[:circle, :rect, :diamond, :utriangle]` +- `point_size::Vector{Int}}`: List of point marker sizes (not all markers look the same size when plotted) +- `point_style::Vector{Symbol}}`: List of marker styles +- `point_alpha::Vector{Float64}}`: List of marker transparencies ## Sphere properties - `sphere_color::String`: Color of Bloch sphere surface -- `sphere_alpha::Float64`: Transparency of sphere surface (default: 0.2) +- `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) +- `vector_arrowsize`::Vector{Float64}: 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)) +- `view::Vector{Int}`: Azimuthal and elevation viewing angles in degrees. Default: `[30, 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]) +- `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_color::String = "black" 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"] + frame_alpha::Float64 = 0.1 + frame_color::String = "gray" + frame_limit::Float64 = 1.2 + point_default_color::Vector{String} = ["blue", "red", "green", "#CC6600"] 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] @@ -143,18 +135,35 @@ A structure representing a Bloch sphere visualization for quantum states. point_alpha::Vector{Float64} = Float64[] sphere_alpha::Float64 = 0.2 sphere_color::String = "#FFDDDD" - vector_color::Vector{String} = ["green", "orange", "blue", "red"] + vector_color::Vector{String} = ["green", "#CC6600", "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", ""] + vector_arrowsize::Vector{Float64} = [0.07, 0.08, 0.08] + view::Vector{Int} = [30, 30] + xlabel::Vector{AbstractString} = [L"x", ""] xlpos::Vector{Float64} = [1.0, -1.0] - ylabel::Vector{AbstractString} = ["y", ""] + ylabel::Vector{AbstractString} = [L"y", ""] ylpos::Vector{Float64} = [1.0, -1.0] - zlabel::Vector{AbstractString} = ["|0\rangle", "|1\rangle"] + zlabel::Vector{AbstractString} = [L"|0\rangle", L"|1\rangle"] zlpos::Vector{Float64} = [1.0, -1.0] end +const BLOCH_DATA_FIELDS = (:points, :vectors, :lines, :arcs) +function Base.show(io::IO, b::Bloch) + # To align the output and make it easier to read + # we use rpad `17` and `19` for Bloch sphere data and properties, respectively + # 17 is the length of string: `Number of vectors` + # 19 is the length of string: `point_default_color` + println(io, "Bloch Sphere\n") + println(io, "data:") + println(io, "-----") + map(n -> println(io, rpad("Number of $n", 17, " "), " = ", length(getfield(b, n))), BLOCH_DATA_FIELDS) + println(io, "") + println(io, "properties:") + println(io, "-----------") + map(n -> (n ∉ BLOCH_DATA_FIELDS) && (println(io, rpad("$n", 19, " "), " = ", getfield(b, n))), fieldnames(Bloch)) + return nothing +end + @doc raw""" add_vectors!(b::Bloch, vec::Vector{<:Real}) @@ -185,13 +194,8 @@ julia> add_vectors!(b, [[1, 0, 0], [0, 1, 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 +add_vectors!(b::Bloch, vec::Vector{<:Real}) = push!(b.vectors, convert(Vector{Float64}, vec)) +add_vectors!(b::Bloch, vecs::Vector{<:Vector{<:Real}}) = append!(b.vectors, [convert(Vector{Float64}, v) for v in vecs]) @doc raw""" add_points!(b::Bloch, pnt::Vector{<:Real}; meth::Symbol = :s, color = "blue", alpha = 1.0) @@ -199,11 +203,11 @@ end 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) +- `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` means opaque and `0.0` means 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) @@ -219,11 +223,11 @@ 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) +- `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` means opaque and `0.0` means transparent) ``` """ function add_points!( @@ -233,20 +237,13 @@ function add_points!( 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 + (size(pnts, 1) == 3) || throw(ArgumentError("Points must be a 3×N matrix where each column is [x; y; z]")) + (meth in (:s, :m, :l)) || throw(ArgumentError("`meth` must be :s, :m, or :l")) + 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 + push!(b.point_color, color) return nothing end @@ -256,22 +253,61 @@ end 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) +- `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]] + (length(p1) != 3 || length(p2) != 3) && throw(ArgumentError("Points must be 3D vectors")) + x = [p1[1], p2[1]] + y = [p1[2], p2[2]] z = [p1[3], p2[3]] - push!(b.lines, (([x, y, z]), fmt)) + push!(b.lines, ([x, y, z], fmt)) return b end +@doc raw""" + add_line!( + b::Bloch, + start_point::QuantumObject, + end_point::QuantumObject; + fmt = "k" + ) + +Add a line between two quantum states on the Bloch sphere visualization. + +# Arguments + +- `b::Bloch`: The Bloch sphere object to modify. +- `start_point::QuantumObject`: The starting quantum state. Can be a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref). +- `end_point::QuantumObject`: The ending quantum state. Can be a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref). +- `fmt::String="k"`: (optional) A format string specifying the line style and color (default is black `"k"`). + +# Description + +This function converts the given quantum states 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 add_line!( + b::Bloch, + start_point::QuantumObject{OpType1}, + end_point::QuantumObject{OpType2}; + fmt = "k", +) where {OpType1<:Union{Ket,Bra,Operator},OpType2<:Union{Ket,Bra,Operator}} + coords1 = _state_to_bloch(start_point) + coords2 = _state_to_bloch(end_point) + return add_line!(b, coords1, coords2; fmt = fmt) +end + @doc raw""" add_arc!(b::Bloch, p1::Vector{<:Real}, p2::Vector{<:Real}, p3::Vector{<:Real}) @@ -279,10 +315,10 @@ 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 +- `b::Bloch`: The Bloch sphere object to modify +- `p1::Vector{<:Real}`: Starting 3D point +- `p2::Vector{<:Real}`: [Optional] Middle 3D point +- `p3::Vector{<:Real}`: Ending 3D point # Examples @@ -295,34 +331,168 @@ julia> add_arc!(b, [1, 0, 0], [0, 1, 0], [0, 0, 1]) ``` """ function add_arc!(b::Bloch, p1::Vector{<:Real}, p2::Vector{<:Real}) + (length(p1) != 3 || length(p2) != 3) && throw(ArgumentError("Points must be 3D vectors")) 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}) + (length(p1) != 3 || length(p2) != 3 || length(p3) != 3) && throw(ArgumentError("Points must be 3D vectors")) 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_arc!( + b::Bloch, + start_point::QuantumObject, + middle_point::QuantumObject, + end_point::QuantumObject + ) -Add one or more quantum states to the Bloch sphere visualization by converting them into Bloch vectors. +Add a circular arc through three points on the Bloch sphere. # Arguments +- `b::Bloch`: The Bloch sphere object to modify. +- `start_point::QuantumObject`: The starting quantum state. Can be a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref). +- `middle_point::QuantumObject`: [Optional] The middle quantum state. Can be a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref). +- `end_point::QuantumObject`: The ending quantum state. Can be a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref). + +# Description + +This function converts the given quantum states into their Bloch vector representations and adds a arc between these two (or three) points on the Bloch sphere visualization. +""" +function add_arc!( + b::Bloch, + start_point::QuantumObject{OpType1}, + end_point::QuantumObject{OpType2}, +) where {OpType1<:Union{Ket,Bra,Operator},OpType2<:Union{Ket,Bra,Operator}} + coords1 = _state_to_bloch(start_point) + coords2 = _state_to_bloch(end_point) + return add_arc!(b, coords1, coords2) +end +function add_arc!( + b::Bloch, + start_point::QuantumObject{OpType1}, + middle_point::QuantumObject{OpType2}, + end_point::QuantumObject{OpType3}, +) where {OpType1<:Union{Ket,Bra,Operator},OpType2<:Union{Ket,Bra,Operator},OpType3<:Union{Ket,Bra,Operator}} + coords1 = _state_to_bloch(start_point) + coords2 = _state_to_bloch(middle_point) + coords3 = _state_to_bloch(end_point) + return add_arc!(b, coords1, coords2, coords3) +end + +@doc raw""" + add_states!(b::Bloch, states::Vector{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) +- `states::Vector{QuantumObject}`: One or more quantum states ([`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref)) + +# 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 add_states!(b::Bloch, states::Vector{<:QuantumObject}) + vecs = map(state -> _state_to_bloch(state), states) + append!(b.vectors, vecs) + return b.vectors +end + +function add_states!(b::Bloch, state::QuantumObject) + push!(b.vectors, _state_to_bloch(state)) + return b.vectors +end + +_state_to_bloch(state::QuantumObject{Ket}) = _ket_to_bloch(state) +_state_to_bloch(state::QuantumObject{Bra}) = _ket_to_bloch(state') +_state_to_bloch(state::QuantumObject{Operator}) = _dm_to_bloch(state) + +raw""" + _ket_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 _ket_to_bloch(state::QuantumObject{Ket}) + (size(state) == (2,)) || + throw(ArgumentError("Bloch sphere visualization is only supported for qubit states (2-level systems)")) + + state_norm = norm(state) + if !isapprox(state_norm, 1.0, atol = 1e-6) + @warn "State is not normalized. Normalizing before Bloch vector conversion." + ψ = state.data / state_norm + else + ψ = state.data + end + + c = conj(ψ[1]) * ψ[2] + x = 2 * real(c) + y = 2 * imag(c) + 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 add_states! end +function _dm_to_bloch(ρ::QuantumObject{Operator}) + (size(ρ) == (2, 2)) || + throw(ArgumentError("Bloch sphere visualization is only supported for qubit states (2-level systems)")) + + ishermitian(ρ) || (@warn "Density matrix is not Hermitian. Results may not be meaningful.") + + state_norm = norm(ρ) + if !isapprox(state_norm, 1.0, atol = 1e-6) + @warn "State is not normalized. Normalizing before Bloch vector conversion." + ρ2 = ρ / state_norm + else + ρ2 = ρ + end + x = real(ρ2[1, 2] + ρ2[2, 1]) + y = imag(ρ2[2, 1] - ρ2[1, 2]) + z = real(ρ2[1, 1] - ρ2[2, 2]) + return [x, y, z] +end @doc raw""" clear!(b::Bloch) -Clear all graphical elements (points, vectors, lines, arcs) from the given Bloch sphere object `b`. +Clear all graphical elements (points, vectors, lines, arcs) from the given [`Bloch`](@ref) sphere object `b`. # Arguments -- `b::Bloch` - The Bloch sphere instance whose contents will be cleared. +- `b::Bloch`: The Bloch sphere instance whose contents will be cleared. # Returns @@ -340,31 +510,27 @@ function clear!(b::Bloch) end @doc raw""" - render(b::QuantumToolbox.Bloch; location=nothing) + render(b::Bloch; location=nothing) -Render the Bloch sphere visualization from the given `Bloch` object `b`. +Render the Bloch sphere visualization from the given [`Bloch`](@ref) 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. +- `b::Bloch`: The Bloch sphere object containing states, vectors, and settings to visualize. +- `location`: 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. +- 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}}; + state::QuantumObject; library::Union{Symbol, Val} = :Makie, kwargs... ) @@ -374,8 +540,8 @@ 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`. +- `state::QuantumObject`: The quantum state to be visualized. Can be a [`Ket`](@ref), [`Bra`](@ref), or [`Operator`](@ref). +- `library::Union{Val,Symbol}`: The plotting library to use. Default is `Val(:Makie)`. - `kwargs...`: Additional keyword arguments passed to the specific plotting implementation. !!! note "Import library first" @@ -384,30 +550,10 @@ The `library` keyword argument specifies the plotting backend to use. The defaul !!! 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 +plot_bloch( + state::QuantumObject{OpType}; + library::Union{Symbol,Val} = Val(:Makie), + kwargs..., +) where {OpType<:Union{Ket,Bra,Operator}} = plot_bloch(makeVal(library), state; kwargs...) +plot_bloch(::Val{T}, state::QuantumObject{OpType}; kwargs...) where {T,OpType<:Union{Ket,Bra,Operator}} = + throw(ArgumentError("The specified plotting library $T is not available. Try running `using $T` first.")) diff --git a/test/ext-test/cpu/makie/makie_ext.jl b/test/ext-test/cpu/makie/makie_ext.jl index 7887c9da0..60f247be9 100644 --- a/test/ext-test/cpu/makie/makie_ext.jl +++ b/test/ext-test/cpu/makie/makie_ext.jl @@ -3,9 +3,10 @@ xvec = yvec = -15.0:0.1:15.0 wig = transpose(wigner(ψ, xvec, yvec)) + # Makie unload errors @test_throws ArgumentError plot_wigner(ψ; library = :Makie, xvec = xvec, yvec = yvec) - @test_throws ArgumentError plot_fock_distribution(ψ; library = :Makie) + @test_throws ArgumentError plot_bloch(ψ; library = :Makie) using Makie @@ -63,12 +64,12 @@ end @testset "Makie Bloch sphere" begin - ρ = 0.7*ket2dm(basis(2, 0)) + 0.3*ket2dm(basis(2, 1)) + ρ = 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 + ψ = (basis(2, 0) + basis(2, 1)) / √2 fig, ax = plot_bloch(ψ) @test fig isa Figure @test ax isa Axis3 @@ -92,25 +93,37 @@ end add_points!(b, hcat(pts...)) @test length(b.points) == 2 @test b.points[2] ≈ hcat(pts...) + @test_throws ArgumentError add_points!(b, [1 2 3 4]) + @test_throws ArgumentError add_points!(b, pts; meth = :wrong) b = Bloch() add_vectors!(b, [1.0, 1.0, 0.0]) @test length(b.vectors) == 1 - @test isapprox(norm(b.vectors[1]), 1.0) + @test isapprox(norm(b.vectors[1]), √2) 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) + @test isapprox(norm(b.vectors[2]), 1.0) + @test isapprox(norm(b.vectors[3]), 1.0) + vec_correct = [1, 0, 0] + vec_wrong = [1, 0] 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] + @test_throws ArgumentError add_line!(b, vec_wrong, vec_correct) + @test_throws ArgumentError add_line!(b, vec_correct, vec_wrong) 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] + @test_throws ArgumentError add_arc!(b, vec_wrong, vec_correct) + @test_throws ArgumentError add_arc!(b, vec_correct, vec_wrong) + @test_throws ArgumentError add_arc!(b, vec_wrong, vec_correct, vec_correct) + @test_throws ArgumentError add_arc!(b, vec_correct, vec_wrong, vec_correct) + @test_throws ArgumentError add_arc!(b, vec_correct, vec_correct, vec_wrong) b = Bloch() add_points!(b, [0.0, 0.0, 1.0]) @@ -147,12 +160,22 @@ end @test false @info "Render threw unexpected error" exception=e end + + # test `state to Bloch vector` conversion and `add_states!` function 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); + Pauli_Ops = [sigmax(), sigmay(), sigmaz()] + ψ = rand_ket(2) + ρ = rand_dm(2) + x = basis(2, 0) + basis(2, 1) # unnormalized Ket + ρ1 = 0.3 * rand_dm(2) + 0.4 * rand_dm(2) # unnormalized density operator + ρ2 = Qobj(rand(ComplexF64, 2, 2)) # unnormalized and non-Hermitian Operator + add_states!(b, [ψ, ρ]) + @test_logs (:warn,) (:warn,) (:warn,) (:warn,) add_states!(b, [x, ρ1, ρ2]) + @test all(expect(Pauli_Ops, ψ) .≈ (b.vectors[1])) + @test all(expect(Pauli_Ops, ρ) .≈ (b.vectors[2])) + @test length(b.vectors) == 5 + + th = range(0, 2π; length = 20) xp = cos.(th); yp = sin.(th); zp = zeros(20); @@ -174,7 +197,10 @@ end b = Bloch() ψ₁ = normalize(basis(2, 0) + basis(2, 1)) ψ₂ = normalize(basis(2, 0) - im * basis(2, 1)) + ψ₃ = basis(2, 0) add_line!(b, ψ₁, ψ₂; fmt = "r--") + add_arc!(b, ψ₁, ψ₂) + add_arc!(b, ψ₂, ψ₃, ψ₁) try fig, ax = render(b) @test !isnothing(fig)