Normal ordering, expectation values, and trace¶
This tutorial explains how the CCR layer rewrites operator expressions into normally ordered form and how scalar quantities such as overlaps, expectation values, traces, and partial traces are extracted symbolically.
The key idea is that Symop does not evaluate matrices directly. Instead, it rewrites ladder-operator words using commutation relations and keeps the result as symbolic sums of normally ordered monomials.
Background¶
In the CCR layer, ladder operators satisfy commutation relations of the form
where the overlap on the right-hand side may be nontrivial when the underlying modes are not orthogonal.
A general operator word is rewritten into a sum of normally ordered monomials,
where each monomial \(M_k\) stores all creators on the left and all annihilators on the right.
This symbolic representation is the basis for:
ket construction and ket multiplication
overlaps and inner products
density operators and traces
symbolic operator actions
Setup¶
import symop.viz as viz
from symop.core.operators import ModeOp
from symop.modes.labels import ModeLabel
from symop.modes.labels.path import Path
from symop.modes.labels.polarization import Polarization
from symop.modes.envelopes import GaussianEnvelope
from symop.ccr.algebra.ket.poly import KetPoly
from symop.ccr.algebra.op.poly import OpPoly
from symop.ccr.algebra.density.poly import DensityPoly
env = GaussianEnvelope(omega0=1.0, sigma=1.0, tau=0.0)
mode = ModeOp(
label=ModeLabel(
path=Path("A"),
polarization=Polarization.H(),
envelope=env,
),
user_label="a",
)
Normal ordering example: \(a a^\dagger\)¶
A simple example is the word
From the commutation relation we expect
expr = KetPoly.from_word(ops=(mode.ann, mode.cre))
viz.display(expr)
The result contains:
an identity contribution (scalar term)
the reordered monomial \(a^\dagger a\)
This illustrates the core mechanism: non-normal-ordered expressions expand into normally ordered terms plus contraction contributions.
Algorithmic picture¶
The normal-ordering algorithm proceeds left-to-right.
For each operator:
annihilators are appended to the annihilation block
creators are appended to the creation block
each creator also contracts with existing annihilators
Each contraction contributes a scalar
This produces the exact normal-ordered expansion without constructing matrices.
Step-by-step normal ordering¶
It is useful to see explicitly how the symbolic rewrite proceeds on a small word. Consider
There are two cases:
if the modes are orthogonal, the commutator vanishes and the word is only reordered
if the modes overlap, an additional scalar contraction term appears
For orthogonal modes, the result is just the reordered monomial:
mode_b = ModeOp(
label=ModeLabel(
path=Path("B"),
polarization=Polarization.H(),
envelope=env,
),
user_label="b",
)
expr_orth = KetPoly.from_word(ops=(mode.ann, mode_b.cre))
viz.display(expr_orth)
Since the modes are distinct, there is no contraction and the output contains only the normally ordered term.
For the same mode, the contraction is nonzero:
expr_same = KetPoly.from_word(ops=(mode.ann, mode.cre))
viz.display(expr_same)
This corresponds to
Algorithmically, the rewrite can be read as:
start from the empty monomial
append \(a_a\) to the annihilator list
insert \(a_a^\dagger\)
keep the reordered term \(a_a^\dagger a_a\)
contract once with the existing annihilator to produce the scalar term
In other words, normal ordering produces both:
the pass-through reordered monomial
every allowed single contraction contribution
Two-mode expectation values¶
Expectation values become especially informative when two modes are involved. Consider the number operator in mode \(a\) acting on a one-particle state in mode \(b\):
If the two modes are orthogonal, the expectation value should vanish:
Construct the state and operator symbolically:
psi_b = KetPoly.from_ops(creators=(mode_b.cre,))
n_a = OpPoly.n(mode)
psi_b_out = n_a @ psi_b
viz.display(psi_b)
viz.display(psi_b_out)
Now compute the expectation value:
psi_b.inner(psi_b_out)
0j
The result is zero because no identity contribution survives after symbolic normal ordering: the operator counts excitations in mode \(a\), but the state occupies mode \(b\).
For comparison, the number operator in the matching mode gives a nonzero value:
psi_a = KetPoly.from_ops(creators=(mode.cre,))
psi_a_out = n_a @ psi_a
viz.display(psi_a_out)
psi_a.inner(psi_a_out)
(1+0j)
This shows the symbolic logic of expectation-value evaluation:
apply the operator polynomial to the ket
expand the result into normally ordered monomials
form the symbolic inner product with the original ket
extract the identity contribution
Already normal-ordered input¶
If the input is already normal ordered, no contraction occurs.
ordered = KetPoly.from_word(ops=(mode.cre, mode.ann))
viz.display(ordered)
Constructing from operators vs words¶
KetPoly.from_opsassumes normal orderingKetPoly.from_wordperforms CCR rewriting
psi_ops = KetPoly.from_ops(creators=(mode.cre,), annihilators=(mode.ann,))
psi_word = KetPoly.from_word(ops=(mode.cre, mode.ann))
viz.display(psi_ops)
viz.display(psi_word)
Scalar overlaps¶
Scalar quantities come from the identity monomial.
psi = KetPoly.from_ops(creators=(mode.cre,))
psi.inner(psi)
(1+0j)
Only the identity contribution survives.
Operator action on kets¶
Operator polynomials act using @.
a = OpPoly.a(mode)
adag = OpPoly.adag(mode)
vacuum = KetPoly.from_ops(coeff=1.0)
created = adag @ vacuum
viz.display(created)
acted = a @ created
viz.display(acted)
This reflects:
creation → append operator
annihilation → contraction + reordered term
Expectation values¶
Expectation values are computed via symbolic contraction.
For a single-photon state in the matching mode, the expected value is 1.
psi = KetPoly.from_ops(creators=(mode.cre,))
n_op = OpPoly.n(mode)
psi_out = n_op @ psi
viz.display(psi)
viz.display(psi_out)
psi.inner(psi_out)
(1+0j)
The scalar result comes from identity extraction after normal ordering.
From kets to density operators¶
A pure density operator is
rho = DensityPoly.pure(created)
viz.display(rho)
Trace¶
The trace is
rho.trace()
(1+0j)
Purity¶
rho.purity()
1.0
Left and right operator action¶
Right action is implemented internally via daggered reversed words.
rho_left = rho.apply_left((mode.cre,))
viz.display(rho_left)
rho_right = rho.apply_right((mode.ann,))
viz.display(rho_right)
Partial trace¶
Partial trace contracts a subset of modes.
mode_b = ModeOp(
label=ModeLabel(
path=Path("B"),
polarization=Polarization.H(),
envelope=env,
),
user_label="b",
)
psi = KetPoly.from_ops(creators=(mode.cre, mode_b.cre))
rho = DensityPoly.pure(psi)
viz.display(rho)
rho_reduced = rho.partial_trace((mode_b,))
viz.display(rho_reduced)
The traced mode is contracted, leaving a reduced operator on the remaining mode.
End-to-end example¶
psi0 = KetPoly.from_ops(coeff=1.0)
psi1 = adag @ psi0
rho1 = DensityPoly.pure(psi1)
viz.display(psi1)
viz.display(rho1)
rho1.trace(), rho1.purity()
((1+0j), 1.0)
Summary¶
The CCR layer operates entirely symbolically:
operator words are rewritten via commutation
results are stored as normally ordered monomials
scalar quantities come from identity contributions
density operations are built on the same mechanism
This enables overlaps, expectation values, traces, and reductions without matrix representations.