Parameters and coefficients
The coefficients of an operator expression are scalars: exact numbers or symbolic expressions from Symbolics.jl. They multiply operators without touching the operator algebra, and simplify applies symbolic identities to them.
- Symbolic parameters
- Unit phases
- Coefficient identities
- Substitution
- Taking expressions apart
- Harmonic projection
Symbolic parameters
Parameters are declared with @variables, which is re-exported from Symbolics. A plain declaration is real, so it is its own complex conjugate.
using SecondQuantizedAlgebraa = Destroy(FockSpace(:cavity), :a)@variables ω η::Number ζ::Complex(ω * a)'\[\omega a^{\dagger}\]
A parameter declared ::Number is complex but stays a single symbol.
(η * a)'\[\mathrm{conj}\left( \eta \right) a^{\dagger}\]
A parameter declared ::Complex is split into independent real and imaginary parts.
(ζ * a)'\[\left(\mathrm{real}\left( \zeta \right) - \mathrm{imag}\left( \zeta \right) ~ \mathit{i}\right) a^{\dagger}\]
In summary:
| Declaration | conj | Representation |
|---|---|---|
@variables ω (or ω::Real) | ω | one real symbol |
@variables η::Number | conj(η) | one complex symbol |
@variables ζ::Complex | real(ζ) - im*imag(ζ) | independent real and imaginary parts |
A complex amplitude such as a drive is best declared ::Number, which keeps every product to a single symbol. ::Complex carries both parts through every product, and suits a parameter whose real and imaginary parts are wanted as independent unknowns.
Unit phases
Time-dependent drives and rotating frames produce phase factors $e^{i\phi}$. These are written with expim, which keeps them exact. The argument has to be real, since the identities $\overline{e^{ix}} = e^{-ix}$ and $|e^{ix}| = 1$ hold only for real $x$.
SecondQuantizedAlgebra.expim — Method
expim(x)Exact unit phase with real argument x.
expim(x) stands for $e^{ix}$ and is kept as an exact symbol rather than a generic exponential. Multiplying phases adds their arguments and opposite phases cancel, so the factors produced by drives and frame changes do not pile up.
The argument has to be real; for a complex exponent use exp. Fractional powers are refused, since they would have to pick a branch.
Examples
julia> @variables ω t θ;julia> expim(ω * t) * expim(θ)exp(im*(θ + t*ω))julia> conj(expim(ω * t)) * expim(ω * t)1julia> a = Destroy(FockSpace(:cavity), :a);julia> expim(ω * t) * expim(-ω * t) * aaSee also trigonometric_form, exponential_form.
The same phase is recognized however its argument is written.
using SecondQuantizedAlgebrausing SecondQuantizedAlgebra: exponential_form, trigonometric_formimport Symbolics@variables ω t θ ra = Destroy(FockSpace(:cavity), :a)expim((ω + 2θ) * t) == expim(ω * t + 2θ * t)trueInteger powers, negative ones included, and division also stay a single phase.
expim(θ)^3 / expim(ω * t), expim(θ)^-2(exp(im*(3θ - t*ω)), exp(-im*2θ))A fractional power would have to pick a branch, so sqrt(expim(θ)) and expim(θ)^(1//2) have no method rather than being rewritten as expim(θ/2).
Substitution rebuilds the phase from its new argument.
substitute(expim(ω * t), Dict(ω => 2ω))\[e^{i 2 ~ t ~ \omega}\]
Arguments are compared algebraically, not modulo $2\pi$, so expim(θ + 2π) is not recognized as expim(θ).
Differentiation follows the chain rule through the argument.
Symbolics.derivative(expim(ω * t), t)\[e^{i t ~ \omega} ~ \omega ~ \mathit{i}\]
A coefficient that is exactly one unit phase has the familiar trigonometric projections.
real(expim(ω * t)), imag(expim(ω * t)), abs(expim(ω * t)), abs2(expim(ω * t))(cos(t*ω), sin(t*ω), 1, 1)A phase keeps the form it was written in, so positive and negative frequencies are not merged into a cosine.
(expim(ω * t) + expim(-ω * t)) * a\[\left(e^{i t ~ \omega} + e^{-i t ~ \omega}\right) a\]
Switching between the exponential and the trigonometric form is explicit, in both directions. Neither conversion is applied by display or by simplify, and expim(cos(θ)) stands for $e^{i\cos\theta}$, not for a conversion of the cosine.
SecondQuantizedAlgebra.exponential_form — Function
exponential_form(x)Rewrite cosines and sines in x as unit phases.
x can be a coefficient or a whole operator expression. The function is not exported.
Examples
julia> using SecondQuantizedAlgebra: exponential_formjulia> @variables ω t;julia> a = Destroy(FockSpace(:cavity), :a);julia> exponential_form(cos(ω * t) * a)((1//2)*exp(im*t*ω) + (1//2)*exp(-im*t*ω)) * aSee also trigonometric_form, expim.
SecondQuantizedAlgebra.trigonometric_form — Function
trigonometric_form(x)Rewrite unit phases in x as cosines and sines.
Each phase expim(θ) becomes $\cos θ + i \sin θ$, so opposite phases combine into real functions. x can be a coefficient or a whole operator expression. The function is not exported.
Examples
julia> using SecondQuantizedAlgebra: trigonometric_formjulia> @variables ω t;julia> a = Destroy(FockSpace(:cavity), :a);julia> trigonometric_form((expim(ω * t) + expim(-ω * t)) * a)2cos(t*ω) * aSee also exponential_form, expim.
Besides cosines and sines, exponential_form also recognizes exp(im * θ) and cis(θ).
exponential_form(cis(θ)) * a\[e^{i \theta} a\]
A coefficient built from phases splits into its Fourier components.
SecondQuantizedAlgebra.phase_terms — Function
phase_terms(c)Split a coefficient into its Fourier components.
The coefficient is written as a sum of amplitude * expim(phase), and each term of that sum is returned with the fields amplitude and phase. Cosines and sines have to be turned into phases first with exponential_form. A coefficient that is not a finite sum of phases throws an ArgumentError. The function is not exported.
Examples
julia> using SecondQuantizedAlgebra: phase_terms, exponential_formjulia> @variables ω t g;julia> c = exponential_form(g * cos(ω * t));julia> terms = phase_terms(c); length(terms)2julia> terms[2].amplitude, terms[2].phase((1//2)*g, t*ω)See also exponential_form, expim.
SecondQuantizedAlgebra.PhaseTerm — Type
PhaseTermOne Fourier component of a coefficient, as returned by phase_terms.
It stands for amplitude * expim(phase), with the fields amplitude, a Coeff, and phase, a real symbolic expression.
See also phase_terms.
Coefficient identities
Symbolic identities are applied only by simplify. It covers trigonometric and hyperbolic identities, also with composite arguments, and leaves a coefficient unchanged when an identity cannot be applied without blowing up the expression.
simplify((cosh(r)^2 - sinh(r)^2) * (a + a'))\[a + a^{\dagger}\]
Substitution
Parameters and operators are replaced in a single call, with all rules applied at once. Shifting a mode by a coherent amplitude while fixing a frequency gives the displaced Hamiltonian.
@variables α::Numbersubstitute(ω * a' * a, Dict(a => a + α, ω => 2))\[2 ~ \mathrm{conj}\left( \alpha \right) ~ \alpha + 2 ~ \mathrm{conj}\left( \alpha \right) a + 2 ~ \alpha a^{\dagger} + 2 a^{\dagger}a\]
SymbolicUtils.substitute — Method
substitute(expr::QField, rules::AbstractDict; replace_adjoint=true)Replace parameters and operators in expr according to rules.
Keys may be parameters or operators, mixed freely. Operator rules act on the original expression all at once, so a rule whose replacement contains its own key, such as the shift a => a + α, is applied exactly once. A rule for a also covers a', unless a' has a rule of its own or replace_adjoint = false.
Examples
julia> a = Destroy(FockSpace(:cavity), :a);julia> @variables Δ α::Number;julia> substitute(Δ * a' * a, Dict(Δ => 2))2 * a' * ajulia> substitute(a' * a, Dict(a => a + α))conj(α)*α + conj(α) * a + α * a' + a' * aSee also change_index, Displace.
Taking expressions apart
To hand an expression to other code, for instance to build a numerical function from its coefficients, its parts can be read off directly. An expression first splits into its terms, and each term into a coefficient and operators.
using SecondQuantizedAlgebra: sorted_argumentsterms = sorted_arguments(ω * a' * a + η * a)\[ \left[ \begin{array}{c} \eta a \\ \omega a^{\dagger}a \\ \end{array} \right] \]
get_prefactor(terms[2]), get_operators(terms[2])(ω, Op[a, a'])SecondQuantizedAlgebra.sorted_arguments — Function
sorted_arguments(expr)Terms of expr, each as an expression of its own, in a fixed order.
Each term can then be read with get_prefactor and get_operators. The function is not exported.
Examples
julia> using SecondQuantizedAlgebra: sorted_argumentsjulia> a = Destroy(FockSpace(:cavity), :a);julia> @variables ω g;julia> foreach(println, sorted_arguments(ω * a' * a + g * (a + a')))g * ag * a'ω * a' * aSee also get_prefactor, get_operators.
SecondQuantizedAlgebra.get_prefactor — Function
get_prefactor(expr)Coefficient of a single-term expression expr, as a Complex{Num}.
An expression with more than one term throws an ArgumentError.
Examples
julia> a = Destroy(FockSpace(:cavity), :a);julia> @variables g;julia> get_prefactor(2g * a' * a)2gSee also get_operators, get_variables.
SecondQuantizedAlgebra.get_operators — Function
get_operators(expr)Distinct operators occurring in expr, in canonical order.
An operator and its adjoint count as two entries, and repeated factors appear once.
Examples
julia> a = Destroy(FockSpace(:cavity), :a);julia> @variables g;julia> get_operators(g * a' * a + a)2-element Vector{Op}: a a'See also get_variables, unique_up_to_adjoint.
Symbolics.get_variables — Method
get_variables(expr)Symbolic parameters occurring in the coefficients of expr.
The result can be passed to Symbolics functions such as build_function. An optional second argument restricts it to a given list of variables.
Examples
julia> a = Destroy(FockSpace(:cavity), :a);julia> @variables ω g;julia> H = ω * a' * a + g * (a + a');julia> length(get_variables(H))2See also get_prefactor, get_operators, substitute.
Iterating an expression gives its terms as pairs of an operator product and a coefficient, kept in exact form. to_num converts a coefficient to Symbolics.
using SecondQuantizedAlgebra: to_num[(term.ops, to_num(c)) for (term, c) in 3ω * a' * a]1-element Vector{Tuple{Vector{Op}, Complex{Symbolics.Num}}}:
([a', a], 3ω)SecondQuantizedAlgebra.QTerm — Type
QTermOperator product of one term of an expression.
Iterating an expression gives pairs term => coefficient of a QTerm and a Coeff. The field ops holds the operators in canonical order, and ne the pairs of indices the term requires to differ. It is public but not exported.
Examples
julia> a = Destroy(FockSpace(:cavity), :a);julia> [term.ops for (term, c) in 2 * a' * a]1-element Vector{Vector{Op}}: [a', a]See also Coeff, get_operators.
SecondQuantizedAlgebra.Coeff — Type
CoeffExact coefficient of an operator term.
Iterating an expression and exponential_form return coefficients in this form. It holds exact numbers, such as 1//2 + im, polynomials in the parameters, and any other symbolic expression, and it supports the usual arithmetic with numbers, parameters and other coefficients. to_num converts it to Symbolics. It is public but not exported.
Examples
julia> using SecondQuantizedAlgebra: exponential_form, Coeffjulia> @variables θ;julia> exponential_form(cos(θ)) isa CoefftrueSee also to_num, get_prefactor.
SecondQuantizedAlgebra.to_num — Function
to_num(c::Coeff)Coefficient as a Complex{Num} of Symbolics.
Use it to pass a coefficient read from an expression to code built on Symbolics, such as build_function. It is public but not exported.
Examples
julia> using SecondQuantizedAlgebra: exponential_form, to_numjulia> @variables θ;julia> to_num(exponential_form(cos(θ))) isa ComplextrueSee also Coeff.
coefficient_terms splits a coefficient into its terms, each an exact or floating scalar times powers of parameters, without going through Symbolics.
using SecondQuantizedAlgebra: coefficient_termscoefficient_terms(3ω^2 - 1 // 2)2-element Vector{SecondQuantizedAlgebra.CoefficientTerm}:
SecondQuantizedAlgebra.CoefficientTerm(-1//2 + 0//1*im, Pair{Symbolics.Num, Rational{Int64}}[])
SecondQuantizedAlgebra.CoefficientTerm(3//1 + 0//1*im, Pair{Symbolics.Num, Rational{Int64}}[ω => 2])SecondQuantizedAlgebra.has_coefficient_terms — Function
has_coefficient_terms(c) -> BoolWhether the coefficient c is stored as a sum of CoefficientTerms, so that coefficient_terms can read it. Numbers and Laurent polynomials in parameters, radicals and function calls such as sin(q) are. A quotient by a sum or a root of a sum, such as p / (1 + q) or sqrt(p + q), is kept as one symbolic expression and is not.
Examples
julia> using SecondQuantizedAlgebra: has_coefficient_termsjulia> using Symbolics: @variablesjulia> @variables p q;julia> has_coefficient_terms(2p^2 + sin(q)), has_coefficient_terms(p / (1 + q))(true, false)See also coefficient_terms.
SecondQuantizedAlgebra.coefficient_terms — Function
coefficient_terms(c) -> Vector{CoefficientTerm}The terms of the coefficient c, without a zero term. A number gives one term with no powers, and zero gives no term. Throws an ArgumentError when c has no has_coefficient_terms form.
Examples
julia> using SecondQuantizedAlgebra: coefficient_termsjulia> using Symbolics: @variablesjulia> @variables p;julia> [(t.scalar, t.powers) for t in coefficient_terms(2p^2 + 1 // 3)]2-element Vector{Tuple{Complex{Rational{BigInt}}, Vector{Pair{Symbolics.Num, Rational{Int64}}}}}: (1//3 + 0//1*im, []) (2//1 + 0//1*im, [p => 2])See also CoefficientTerm, has_coefficient_terms.
SecondQuantizedAlgebra.CoefficientTerm — Type
CoefficientTermOne term scalar * ∏ xᵢ^eᵢ of a coefficient, as returned by coefficient_terms. The scalar is a Complex{Rational{BigInt}} when exact and a ComplexF64 when inexact. powers holds the atom => exponent pairs with nonzero rational exponents, possibly negative. An atom is a parameter, a prime under a radical (√2 is 2 => 1//2), or a symbolic subexpression the coefficient keeps whole, such as sin(g). The order of powers is not specified.
Examples
julia> using SecondQuantizedAlgebra: coefficient_terms, CoefficientTermjulia> using Symbolics: @variablesjulia> @variables p;julia> only(coefficient_terms(3p^2))CoefficientTerm(3//1 + 0//1*im, Pair{Symbolics.Num, Rational{Int64}}[p => 2])See also coefficient_terms.
A complex amplitude declared as @variables β::Number stays a single atom next to a phase, and its conjugate is the symbolic conj(β). Prefer it over ::Complex, which splits the parameter into real(β) + im*imag(β) in every coefficient:
using SecondQuantizedAlgebrah = FockSpace(:harmonic_example)@qnumbers a::Destroy(h)@variables ω::Real t::Real@variables β::Numberβ * expim(ω * t) * a + conj(β) * expim(-ω * t) * a'\[e^{i t ~ \omega} ~ \beta a + \mathrm{conj}\left( \beta \right) ~ e^{-i t ~ \omega} a^{\dagger}\]
Harmonic projection
harmonics splits an expression into its Fourier components in a base frequency ω, the step behind a rotating-wave or secular approximation. harmonic returns one component, the coefficient of expim(n * ω * t), and secular the static one:
using SecondQuantizedAlgebrah = FockSpace(:harmonic_example)@qnumbers a::Destroy(h)@variables ω::Real t::Realimport SecondQuantizedAlgebra: harmonic, harmonics, secular@variables ω0::Real ωd::Real F::Real τ::RealH = ω0 * a' * a - F * (a + a') * (expim(ωd * τ) + expim(-ωd * τ)) / 2Hrot = transform(H, Rotation(a, ωd * τ, τ))harmonics(Hrot, ωd, τ)Dict{Int64, SecondQuantizedAlgebra.QAdd} with 3 entries:
0 => (-1//2)*F * a - (1//2)*F * a' + (ω0 - ωd) * a' * a
2 => (-1//2)*F * a'
-2 => (-1//2)*F * asecular(Hrot, ωd, τ)\[ - \frac{1}{2} ~ F a - \frac{1}{2} ~ F a^{\dagger} + \left(\mathtt{{\omega}0} - \mathtt{{\omega}d}\right) a^{\dagger}a\]
The decomposition is exact: summing harmonic(x, ω, t, n) * expim(n * ω * t) over the returned orders gives exponential_form(x). A phase that is not an exact integer multiple of ω * t plus a constant, or a time dependence outside a phase, throws an ArgumentError rather than being dropped, because assigning it would assume a relation between frequencies. The same functions act on SuperOperators and Lindbladians.
SecondQuantizedAlgebra.harmonics — Function
harmonics(x, ω, t) -> Dict{Int}Decompose x into its Fourier components in the base frequency ω, so that
x == sum(xₙ * expim(n * ω * t) for (n, xₙ) in harmonics(x, ω, t))after exponential_form. x is a coefficient, a quantum expression, a SuperOperator or a Lindbladian; the components have the type of x (a Coeff for a scalar, a QAdd for an operator). Zero components are omitted.
Trigonometric factors are converted to expim first, as in phase_terms. Every phase must be n * ω * t + φ with an exact integer n and a t-independent offset φ, which stays in the component as expim(φ). Anything else throws an ArgumentError instead of being dropped or assigned to a guessed harmonic: a frequency that is not an exact integer multiple of ω (such as ω' or ω/2), a phase nonlinear in t, or a t-dependence outside a phase. Choose ω as the common base frequency of all phases.
For a Lindbladian the projection acts on the GKSL form, H, the rates and the jumps, and returns a Lindbladian; a jump J = Σ Jₘ expim(m ω t) with partner K = Σ Kₗ expim(l ω t) contributes 𝒟[Jₘ, Kₗ] to harmonic m - l. Only the secular (n = 0) part is again Hermiticity preserving in general.
SecondQuantizedAlgebra.harmonic — Function
harmonic(x, ω, t, n::Integer)The coefficient of expim(n * ω * t) in x, with the same type as x (a Coeff for a scalar). Every phase of x must be an exact integer multiple of ω * t plus a constant; see harmonics for the precise contract. A component that is not of this form throws an ArgumentError even when n is a different order, because it could not be excluded from harmonic n without assuming a relation between frequencies.
Examples
@variables ω0::Real ωd::Real F::Real t::Real@qnumbers a::Destroy(FockSpace(:a))H = ω0 * a' * a - F * (a + a') * (expim(ωd * t) + expim(-ωd * t)) / 2Ht = transform(H, Rotation(a, ωd * t, t))secular(Ht, ωd, t) # (ω0 - ωd) a'a - F/2 (a + a')harmonic(Ht, ωd, t, 2) # -F/2 a'See also secular, phase_terms.
SecondQuantizedAlgebra.secular — Function
secular(x, ω, t)The time-independent (rotating-wave) part of x, harmonic(x, ω, t, 0). Every oscillating component must be an exact integer harmonic of ω; otherwise an ArgumentError is thrown. For a Lindbladian the result is again a Lindbladian. Its rate matrices are time averages of those of L, so they stay positive semidefinite when those of L are at all times. See harmonics.
SecondQuantizedAlgebra.map_coefficients — Function
map_coefficients(f, x)Apply f to every coefficient of x and rebuild it in canonical form. x is a QAdd (or a single operator), a SuperOperator or a polynomial Lindbladian. f receives a Coeff and may return a Number, a Symbolics Num or a Coeff; the result is converted exactly. Terms that map to zero are dropped and like terms merge.
f acts per coefficient, so the operators are kept as they are. Summation scopes are kept: a term whose coefficient loses its dependence on a summation index is multiplied by that index's count, as with substitute, so Σᵢ gᵢ a'a with gᵢ ↦ g gives N g a'a. f must not introduce a summation index into a term it does not bind; that throws an ArgumentError.
For a Lindbladian, f maps the coefficients of the Hamiltonian, of every jump and every rate, and each channel is renormalized. This agrees with mapping the lowered SuperOperator when f is a substitution of real parameters or preserves values, such as simplify; a general f need not commute with lowering, because a lowered coefficient γ c_J conj(c_K) is a product of the stored ones.
Examples
julia> h = FockSpace(:f);julia> @qnumbers a::Destroy(h);julia> @variables x;julia> SecondQuantizedAlgebra.map_coefficients(c -> substitute(c, Dict(x => 2)), x * a' * a + a)a + 2 * a' * aSee also simplify, substitute.
map_coefficients(f, expr) applies f to every scalar coefficient while preserving the operator structure.
SecondQuantizedAlgebra.coefficient — Function
coefficient(expr, term) -> CoeffThe coefficient of the operator product term in the polynomial expr, or zero when expr has no such term. term is an operator, a product such as a' * a, or 1 for the scalar part. It must canonicalize to a single product with unit coefficient, so 2a, a + a' and a * a' (which is a'a + 1) are refused. Index inequalities are part of a term: a product that expr stores under other constraints, or under several, throws instead of reading as zero; build term with assume_distinct_index to match a constrained product. Summed expressions are refused.
Unlike coefficient_terms, which splits a scalar into its parameter monomials, coefficient reads the scalar that multiplies an operator product.
julia> using SecondQuantizedAlgebra: coefficientjulia> @qnumbers a::Destroy(FockSpace(:c));julia> H = 3 * a' * a + 2 * a' + 2 * a + 5;julia> coefficient(H, a' * a), coefficient(H, a'), coefficient(H, 1), coefficient(H, a' * a')(3, 2, 5, 0)coefficient(S::SuperOperator, sandwich::SuperOperator) -> CoeffThe coefficient of the single sandwich sandwich, such as sprepost(a, a') or spre(a' * a), in the map S, or zero when S has no such sandwich. The rules for operator products apply to both sides: the sandwich must be one canonical term with unit coefficient, index constraints must match, and summed maps are refused.
coefficient(expr, product) reads the scalar multiplying a canonical operator product.