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

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:

DeclarationconjRepresentation
@variables ω (or ω::Real)ωone real symbol
@variables η::Numberconj(η)one complex symbol
@variables ζ::Complexreal(ζ) - 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) * aa

See also trigonometric_form, exponential_form.

source

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)
true

Integer 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*ω)) * a

See also trigonometric_form, expim.

source
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*ω) * a

See also exponential_form, expim.

source

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.

source

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' * a

See also change_index, Displace.

source

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'])
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))2

See also get_prefactor, get_operators, substitute.

source

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
QTerm

Operator 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.

source
SecondQuantizedAlgebra.Coeff — Type
Coeff

Exact 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 Coefftrue

See also to_num, get_prefactor.

source
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 Complextrue

See also Coeff.

source

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) -> Bool

Whether 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.

source
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.

source
SecondQuantizedAlgebra.CoefficientTerm — Type
CoefficientTerm

One 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.

source

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 * a
secular(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.

See also harmonic, secular.

source
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.

source
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.

source
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' * a

See also simplify, substitute.

source

map_coefficients(f, expr) applies f to every scalar coefficient while preserving the operator structure.

SecondQuantizedAlgebra.coefficient — Function
coefficient(expr, term) -> Coeff

The 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)
source
coefficient(S::SuperOperator, sandwich::SuperOperator) -> Coeff

The 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.

source

coefficient(expr, product) reads the scalar multiplying a canonical operator product.