From 079ea5e6f3c14b34a791aef99704035f1841fb29 Mon Sep 17 00:00:00 2001 From: Andrew Kille <68079167+apkille@users.noreply.github.com> Date: Fri, 28 Jun 2024 21:27:57 -0400 Subject: [PATCH] express, qsimplify, and latexify updates (#57) * latexify additions * updated express and new doc * qsimplify updates * doctest fix * rearranging maketerm defs and doctest change * clean docs * update changelop and doc * minor fix * update project.toml --- CHANGELOG.md | 7 +++ Project.toml | 2 +- docs/src/express.md | 59 ++++++++++++++++++++ ext/QuantumCliffordExt/QuantumCliffordExt.jl | 3 +- src/QSymbolicsBase/QSymbolicsBase.jl | 5 +- src/QSymbolicsBase/basic_ops_homogeneous.jl | 18 ++++-- src/QSymbolicsBase/express.jl | 7 +++ src/QSymbolicsBase/latexify.jl | 30 +++++++--- src/QSymbolicsBase/predefined.jl | 2 +- src/QSymbolicsBase/rules.jl | 10 ++-- test/test_express_cliff.jl | 28 ++++++++++ 11 files changed, 149 insertions(+), 22 deletions(-) create mode 100644 docs/src/express.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 38ea614..f2ad3a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # News +## v0.3.2 - 2024-06-28 + +- Added documentation for `express`. +- `qsimplify` can now traverse through subexpressions using Prewalk from SymbolicUtils.jl. +- Updated `latexify` capabilities. +- **(fix)** There was a bug for latexifying dagger objects. + ## v0.3.1 - 2024-06-21 - Macros for defining symbolic quantum objects. diff --git a/Project.toml b/Project.toml index 09a301c..2ed9266 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "QuantumSymbolics" uuid = "efa7fd63-0460-4890-beb7-be1bbdfbaeae" authors = ["QuantumSymbolics.jl contributors"] -version = "0.3.1" +version = "0.3.2" [deps] Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" diff --git a/docs/src/express.md b/docs/src/express.md new file mode 100644 index 0000000..8fbb1e8 --- /dev/null +++ b/docs/src/express.md @@ -0,0 +1,59 @@ +# Express functionality + +```@meta +DocTestSetup = quote + using QuantumSymbolics, QuantumOptics, QuantumClifford +end +``` + +A principle feature of `QuantumSymbolics` is to numerically represent symbolic quantum expressions in various formalisms using [`express`](@ref). In particular, one can translate symbolic logic to back-end toolboxes such as `QuantumOptics.jl` or `QuantumClifford.jl` for simulating quantum systems with great flexibiity. + +As a straightforward example, consider the spin-up state $|\uparrow\rangle = |0\rangle$, the eigenstate of the Pauli operator $Z$, which can be expressed in `QuantumSymbolics` as follows: + +```@example 1 +using QuantumSymbolics, QuantumClifford, QuantumOptics # hide +ψ = Z1 +``` +Using [`express`](@ref), we can translate this symbolic object into its numerical state vector form in `QuantumOptics.jl`. + +```@example 1 +express(ψ) +``` + +By default, [`express`](@ref) converts a quantum object with `QuantumOpticRepr`. It should be noted that [`express`](@ref) automatically caches this particular conversion of `ψ`. Thus, after running the above example, the numerical representation of the spin-up state is stored in the metadata of `ψ`. + +```@example 1 +ψ.metadata +``` + +The caching feature of [`express`](@ref) prevents a specific representation for a symbolic quantum object from being computed more than once. This becomes handy for translations of more complex operations, which can become computationally expensive. We also have the ability to express $|Z_1\rangle$ in the Clifford formalism with `QuantumClifford.jl`: + +```@example 1 +express(ψ, CliffordRepr()) +``` + +Here, we specified an instance of `CliffordRepr` in the second argument to convert `ψ` into a tableau of Pauli operators containing its stabilizer and destabilizer states. Now, both the state vector and Clifford representation of `ψ` have been cached: + +```@example 1 +ψ.metadata +``` + +More involved examples can be explored. For instance, say we want to apply the tensor product $X\otimes Y$ of the Pauli operators $X$ and $Y$ to the Bell state $|\Phi^{+}\rangle = \dfrac{1}{\sqrt{2}}\left(|00\rangle + |11\rangle\right)$, and numerically express the result in the quantum optics formalism. This would be done as follows: + +```@example 2 +using QuantumSymbolics, QuantumClifford, QuantumOptics # hide +bellstate = (Z1⊗Z1+Z2⊗Z2)/√2 +tp = σˣ⊗σʸ +express(tp*bellstate) +``` + +## Examples of Edge Cases +For Pauli operators, additional flexibility is given for translations to the Clifford formalism. Users have the option to convert a multi-qubit Pauli operator to an observable or operation with instances of `UseAsObservable` and `UseAsOperation`, respectively. Take the Pauli operator $Y$, for example, which in `QuantumSymbolics` is the constants `Y` or `σʸ`: + +```jldoctest +julia> express(σʸ, CliffordRepr(), UseAsObservable()) ++ Y + +julia> express(σʸ, CliffordRepr(), UseAsOperation()) +sY +``` \ No newline at end of file diff --git a/ext/QuantumCliffordExt/QuantumCliffordExt.jl b/ext/QuantumCliffordExt/QuantumCliffordExt.jl index 1ef9376..dd1ee9b 100644 --- a/ext/QuantumCliffordExt/QuantumCliffordExt.jl +++ b/ext/QuantumCliffordExt/QuantumCliffordExt.jl @@ -47,7 +47,7 @@ end express_nolookup(::XGate, ::CliffordRepr, ::UseAsOperation) = QuantumClifford.sX express_nolookup(::YGate, ::CliffordRepr, ::UseAsOperation) = QuantumClifford.sY express_nolookup(::ZGate, ::CliffordRepr, ::UseAsOperation) = QuantumClifford.sZ -express_nolookup(x::STensorOperator,r::CliffordRepr,u::UseAsOperation) = QCGateSequence([express(t,r,u) for t in x.terms]) +express_nolookup(x::STensorOperator, r::CliffordRepr, u::UseAsOperation) = QCGateSequence([express(t,r,u) for t in x.terms]) express_nolookup(op::QuantumClifford.PauliOperator, ::CliffordRepr, ::UseAsObservable) = op express_nolookup(op::STensorOperator, r::CliffordRepr, u::UseAsObservable) = QuantumClifford.tensor(express.(arguments(op),(r,),(u,))...) @@ -55,6 +55,7 @@ express_nolookup(::XGate, ::CliffordRepr, ::UseAsObservable) = QuantumClifford.P express_nolookup(::YGate, ::CliffordRepr, ::UseAsObservable) = QuantumClifford.P"Y" express_nolookup(::ZGate, ::CliffordRepr, ::UseAsObservable) = QuantumClifford.P"Z" express_nolookup(op::SScaledOperator, r::CliffordRepr, u::UseAsObservable) = arguments(op)[1] * express(arguments(op)[2],r,u) +express_nolookup(x::SMulOperator, r::CliffordRepr, u::UseAsObservable) = (*)((express(t,r,u) for t in arguments(x))...) express_nolookup(op, ::CliffordRepr, ::UseAsObservable) = error("Can not convert $(op) into a `PauliOperator`, which is the only observable that can be computed for QuantumClifford objects. Consider defining `express_nolookup(op, ::CliffordRepr, ::UseAsObservable)::PauliOperator` for this object.") struct QCRandomSampler # TODO specify types diff --git a/src/QSymbolicsBase/QSymbolicsBase.jl b/src/QSymbolicsBase/QSymbolicsBase.jl index ac50922..bb913bc 100644 --- a/src/QSymbolicsBase/QSymbolicsBase.jl +++ b/src/QSymbolicsBase/QSymbolicsBase.jl @@ -1,9 +1,9 @@ using Symbolics import Symbolics: simplify using SymbolicUtils -import SymbolicUtils: Symbolic, _isone, flatten_term, isnotflat, Chain, Fixpoint +import SymbolicUtils: Symbolic, _isone, flatten_term, isnotflat, Chain, Fixpoint, Prewalk using TermInterface -import TermInterface: isexpr, head, iscall, children, operation, arguments, metadata +import TermInterface: isexpr, head, iscall, children, operation, arguments, metadata, maketerm using LinearAlgebra import LinearAlgebra: eigvecs, ishermitian, inv @@ -141,6 +141,7 @@ Base.:(-)(x::SymQObj) = (-1)*x Base.:(-)(x::SymQObj,y::SymQObj) = x + (-y) Base.hash(x::SymQObj, h::UInt) = isexpr(x) ? hash((head(x), arguments(x)), h) : hash((typeof(x),symbollabel(x),basis(x)), h) +maketerm(::Type{<:SymQObj}, f, a, t, m) = f(a...) function Base.isequal(x::X,y::Y) where {X<:SymQObj, Y<:SymQObj} if X==Y diff --git a/src/QSymbolicsBase/basic_ops_homogeneous.jl b/src/QSymbolicsBase/basic_ops_homogeneous.jl index 176feeb..63aa58a 100644 --- a/src/QSymbolicsBase/basic_ops_homogeneous.jl +++ b/src/QSymbolicsBase/basic_ops_homogeneous.jl @@ -30,14 +30,20 @@ arguments(x::SScaled) = [x.coeff,x.obj] operation(x::SScaled) = * head(x::SScaled) = :* children(x::SScaled) = [:*,x.coeff,x.obj] -Base.:(*)(c, x::Symbolic{T}) where {T<:QObj} = iszero(c) || iszero(x) ? SZero{T}() : SScaled{T}(c, x) +function Base.:(*)(c, x::Symbolic{T}) where {T<:QObj} + if iszero(c) || iszero(x) + SZero{T}() + else + x isa SScaled ? SScaled{T}(c*x.coeff, x.obj) : SScaled{T}(c, x) + end +end Base.:(*)(x::Symbolic{T}, c) where {T<:QObj} = c*x Base.:(/)(x::Symbolic{T}, c) where {T<:QObj} = iszero(c) ? throw(DomainError(c,"cannot divide QSymbolics expressions by zero")) : (1/c)*x basis(x::SScaled) = basis(x.obj) const SScaledKet = SScaled{AbstractKet} function Base.show(io::IO, x::SScaledKet) - if x.coeff isa Number + if x.coeff isa Real print(io, "$(x.coeff)$(x.obj)") else print(io, "($(x.coeff))$(x.obj)") @@ -45,7 +51,7 @@ function Base.show(io::IO, x::SScaledKet) end const SScaledOperator = SScaled{AbstractOperator} function Base.show(io::IO, x::SScaledOperator) - if x.coeff isa Number + if x.coeff isa Real print(io, "$(x.coeff)$(x.obj)") else print(io, "($(x.coeff))$(x.obj)") @@ -53,7 +59,7 @@ function Base.show(io::IO, x::SScaledOperator) end const SScaledBra = SScaled{AbstractBra} function Base.show(io::IO, x::SScaledBra) - if x.coeff isa Number + if x.coeff isa Real print(io, "$(x.coeff)$(x.obj)") else print(io, "($(x.coeff))$(x.obj)") @@ -171,14 +177,14 @@ function ⊗(xs::Symbolic{T}...) where {T<:QObj} end basis(x::STensor) = tensor(basis.(x.terms)...) +const STensorBra = STensor{AbstractBra} +Base.show(io::IO, x::STensorBra) = print(io, join(map(string, arguments(x)),"")) const STensorKet = STensor{AbstractKet} Base.show(io::IO, x::STensorKet) = print(io, join(map(string, arguments(x)),"")) const STensorOperator = STensor{AbstractOperator} Base.show(io::IO, x::STensorOperator) = print(io, join(map(string, arguments(x)),"⊗")) const STensorSuperOperator = STensor{AbstractSuperOperator} Base.show(io::IO, x::STensorSuperOperator) = print(io, join(map(string, arguments(x)),"⊗")) -const STensorBra = STensor{AbstractBra} -Base.show(io::IO, x::STensorBra) = print(io, join(map(string, arguments(x)),"")) """Symbolic commutator of two operators diff --git a/src/QSymbolicsBase/express.jl b/src/QSymbolicsBase/express.jl index ea983c6..e091591 100644 --- a/src/QSymbolicsBase/express.jl +++ b/src/QSymbolicsBase/express.jl @@ -24,8 +24,15 @@ julia> express(X1, CliffordRepr()) 𝒮𝓉𝒶𝒷 + X +julia> express(QuantumSymbolics.X) +Operator(dim=2x2) + basis: Spin(1/2)sparse([2, 1], [1, 2], ComplexF64[1.0 + 0.0im, 1.0 + 0.0im], 2, 2) + julia> express(QuantumSymbolics.X, CliffordRepr(), UseAsOperation()) sX + +julia> express(QuantumSymbolics.X, CliffordRepr(), UseAsObservable()) ++ X ``` """ function express end diff --git a/src/QSymbolicsBase/latexify.jl b/src/QSymbolicsBase/latexify.jl index dfd68fb..bcc585a 100644 --- a/src/QSymbolicsBase/latexify.jl +++ b/src/QSymbolicsBase/latexify.jl @@ -20,25 +20,38 @@ function num_to_sub(n::Int) "0"=>"₀", ) end - +@latexrecipe function f(x::SBra) + return Expr(:latexifymerge, "\\left\\langle ", symbollabel(x), "\\right|") +end @latexrecipe function f(x::Union{SpecialKet,SKet}) return Expr(:latexifymerge, "\\left|", symbollabel(x), "\\right\\rangle") end -@latexrecipe function f(x::Union{SOperator,AbstractSingleQubitOp,AbstractTwoQubitOp,AbstractSingleBosonGate}) +@latexrecipe function f(x::Union{SOperator,SHermitianOperator,SUnitaryOperator,SHermitianUnitaryOperator,AbstractSingleQubitOp,AbstractTwoQubitOp,AbstractSingleBosonGate}) return LaTeXString("\\hat $(symbollabel(x))") end +@latexrecipe function f(x::SZero) + return LaTeXString("\\bm{O}") +end @latexrecipe function f(x::SDagger) - if isexpr(x.ket) - return Expr(:latexifymerge, "\\left( ", x.ket, "\\right)^\\dagger") + if isexpr(x.obj) + return Expr(:latexifymerge, "\\left( ", latexify(x.obj), "\\right)^\\dagger") else - return Expr(:latexifymerge, "\\left\\langle ", symbollabel(x), "\\right|") + return Expr(:latexifymerge, latexify(x.obj), "\\^\\dagger") end end -@latexrecipe function f(x::SScaled) +@latexrecipe function f(x::Union{SScaled,SMulOperator,SOuterKetBra,SApplyKet,SApplyBra}) cdot --> false return _toexpr(x) end - +@latexrecipe function f(x::SCommutator) + return Expr(:latexifymerge, "\\left\\lbrack", latexify(x.op1), ",", latexify(x.op2), "\\right\\rbrack") +end +@latexrecipe function f(x::SAnticommutator) + return Expr(:latexifymerge, "\\left\\{", latexify(x.op1), ",", latexify(x.op2), "\\right\\}") +end +@latexrecipe function f(x::SBraKet) + return Expr(:latexifymerge, "\\left\\langle ", symbollabel(x.bra), "\\mid ", symbollabel(x.ket), "\\right\\rangle") +end @latexrecipe function f(x::MixedState) return LaTeXString("\\mathbb{M}") end @@ -46,6 +59,9 @@ end @latexrecipe function f(x::IdentityOp) return LaTeXString("\\mathbb{I}") end +@latexrecipe function f(x::SInvOperator) + return Expr(:latexifymerge, latexify(x.op), "\\^{-1}") +end function _toexpr(x) if isexpr(x) diff --git a/src/QSymbolicsBase/predefined.jl b/src/QSymbolicsBase/predefined.jl index 182e8bc..e7c28c3 100644 --- a/src/QSymbolicsBase/predefined.jl +++ b/src/QSymbolicsBase/predefined.jl @@ -236,7 +236,7 @@ end julia> @ket a; @op A; julia> dagger(2*im*A*a) -0 - 2im|a⟩†A† +(0 - 2im)|a⟩†A† julia> @op B; diff --git a/src/QSymbolicsBase/rules.jl b/src/QSymbolicsBase/rules.jl index 6a8d6b7..ef4f78c 100644 --- a/src/QSymbolicsBase/rules.jl +++ b/src/QSymbolicsBase/rules.jl @@ -121,6 +121,9 @@ If the keyword `rewriter` is not specified, then `qsimplify` will apply every de For performance or single-purpose motivations, the user has the option to define a specific rewriter for `qsimplify` to apply to the expression. ```jldoctest +julia> qsimplify(σʸ*commutator(σˣ*σᶻ, σᶻ)) +(0 - 2im)Z + julia> qsimplify(anticommutator(σˣ, σˣ), rewriter=qsimplify_anticommutator) 2𝕀 ``` @@ -128,12 +131,11 @@ julia> qsimplify(anticommutator(σˣ, σˣ), rewriter=qsimplify_anticommutator) function qsimplify(s; rewriter=nothing) if QuantumSymbolics.isexpr(s) if isnothing(rewriter) - Fixpoint(Chain(RULES_ALL))(s) + Fixpoint(Prewalk(Chain(RULES_ALL)))(s) else - Fixpoint(rewriter)(s) + Fixpoint(Prewalk(rewriter))(s) end else error("Object $(s) of type $(typeof(s)) is not an expression.") end -end - +end \ No newline at end of file diff --git a/test/test_express_cliff.jl b/test/test_express_cliff.jl index e4c584d..3375283 100644 --- a/test/test_express_cliff.jl +++ b/test/test_express_cliff.jl @@ -15,3 +15,31 @@ withcache = @timed express(state2, CliffordRepr()) @test withcache.bytes <= 200 results = Set([express(state2, CliffordRepr()) for i in 1:20]) @test length(results)==2 + +CR = CliffordRepr() +UseOp = UseAsOperation() +UseObs = UseAsObservable() + +@testset "Clifford representations for basis states" begin + isequal(express(X1, CR), MixedDestabilizer(S"X")) + isequal(express(X2, CR), MixedDestabilizer(S"-X")) + isequal(express(Y1, CR), MixedDestabilizer(S"Y")) + isequal(express(Y2, CR), MixedDestabilizer(S"-Y")) + isequal(express(Z1, CR), MixedDestabilizer(S"Z")) + isequal(express(Z2, CR), MixedDestabilizer(S"-Z")) +end + +@testset "Clifford representations as observables" begin + isequal(express(σˣ, CR, UseObs), P"X") + isequal(express(σʸ, CR, UseObs), P"Y") + isequal(express(σᶻ, CR, UseObs), P"Z") + isequal(express(im*σˣ, CR, UseObs), im*P"X") + isequal(express(σˣ⊗σʸ⊗σᶻ), P"X"⊗P"Y"⊗P"Z") + isequal(express(σˣ*σʸ*σᶻ), P"X"*P"Y"*P"Z") +end + +@testset "Clifford representations as operations" begin + isequal(express(σˣ, CR, UseOp), sX) + isequal(express(σʸ, CR, UseOp), sY) + isequal(express(σᶻ, CR, UseOp), sZ) +end \ No newline at end of file