Tutorial 1: Getting started
yaw is a Python-based language for doing algebraic quantum
programming. This tutorial explains how to use yaw and describes
some basic concepts, assuming a first course in quantum computing (at
the level of Nielsen and Chuang, chapters 1–2) and some Python
programming experience.
1 Getting started
There are three ways to use yaw: using the yaw REPL (interpreted),
using .yaw files and y2py (compiled), and finally as a package in Python. The
first two options use streamlined yaw syntax, but for tasks
requiring Python integration the latter is useful. We'll focus on
the first two in this tutorial. But first, we'll see how to install!
1.1 Installation
The first step is to clone the yaw repository via git:
git clone https://github.com/torsor-io/yaw.git cd yaw pip install -r requirements.txt # Just sympy and numpy for now
You now have access to all the files you need!
1.2 REPL
To use the REPL, navigate to the top level folder and run
python yaw/yaw_repl.py
This shows a banner followed by a command prompt:
┓ ┓ •
┓┏┏┓┓┏┏ ┏┓┃┏┓┏┓┣┓┏┓┏┓┓┏ ┓┏┏┏┓┓┏
┗┫┗┻┗┻┛ ┗┻┗┗┫┗ ┗┛┛ ┗┻┗┗ ┗┻┛┗┻┗┫
┛ ┛ ┛
Yaw: Algebraic QC w/ context management
Version 0.1.0
Torsor Labs, 2025
────────────────────────────────────────────────────────────
Type 'help' or 'credits' for more information.
yaw> $alg = qubit()
Created algebra: <X, Z | herm, unit, anti>
Here, we've defined the algebra of a qubit, $alg = qubit(). We'll
explain the output below.
1.3 Compiler
Alternatively, we can save a .yaw file containing the single line:
$alg = qubit()
Let's call this qubit.yaw. We can compile this to Python and run
in one fell swoop using
python yaw/y2py.py qubit.yaw -o qubit.py python qubit.py
We won't get any output this time, since print statements like
Created algebra: <X, Z | herm, unit, anti> are REPL-only verbosity, and we
haven't done anything but define a lone qubit.
1.4 Package
Finally, we can use yaw as a package inside Python. The syntax is
clunkier but it can interface directly with other Python packages.
from yaw import * pauli = Algebra(gens=['X', 'Z'], rels=['herm', 'unit', 'anti'])
In this tutorial, we will largely focus on native yaw syntax;
this is the way the language is intended to be used.
2 Statics
In this section, we'll define the basic objects of algebraic quantum programming: algebras, operators, and states, and the basic operations on them. Any math we need we will introduce as we go along.
2.1 Algebras
An algebra \(\mathcal{A}\) is a collection of observables. We can take complex linear combinations of observables, multiply them, and take Hermitian conjugates, aka adjoints. Thus, for operators \(A, B \in \mathcal{A}\), \[ \alpha A + \beta B, \quad A \cdot B, \quad A^*, B^* \in \mathcal{A}, \] for any \(\alpha, \beta \in \mathcal{C}\). We also assume a multiplicative identity \(I \in\mathcal{A}\). I won't go into the properties of these operations, since these are exactly what you expect from ordinary dealings with Hilbert space. Crucially, operators also have a size satisfying \(\Vert A \Vert = \Vert A^* \Vert \geq 0\), but we won't worry about this for now. Already, the situation is a bit easier than when we deal with normalized states in Hilbert space; we can freely add without having to re-normalize anything.
To define an algebra in yaw, we use generators and
relations. Generators are the symbolic alphabet we use to write down
all operators, like \(x, y, z, \ldots\) of high school algebra, while
relations are equations these generators satisfy. The simplest example
is the algebra of a qubit, called the Pauli algebra. The smallest
generating set is just \(X\) and \(Z\), subject to the relations
\[
X^* = X, Z^* = Z, \quad X^*X = Z^*Z= I, \quad ZX = -XZ.
\]
Put differently, \(X\) and \(Z\) are Hermitian, unitary, and anticommute.
The notation for generators and relations is \(\mathcal{A} =
\langle\mathcal{G}|\mathcal{R}\rangle\), where \(\mathcal{G}\) is a set
of generators and \(\mathcal{R}\) relations.
We adopt this in yaw, and simply write the Pauli algebra as
yaw> $alg = < X, Z | herm, unit, anti > Created algebra: <X, Z | herm, unit, anti>
where herm and unit means all generators are respectively
Hermitian and unitary, and anti means they anticommute.
Note that being Hermitian and unitary also implies \(A^2 = A^*A = I\),
i.e. operators square to the identity, which is expressed by the relation pow(2).
This is automatically inferred.
Finally, the notation $alg is meant to evoke global variables in
UNIX; here, the algebra is a global object.
We can specify individual operator relations simply by appending symbols, e.g. the Pauli algebra can also be written
yaw> $alg = < X, Z | herm(X, Z), unit(X, Z), anti(X, Z) >
Other relation builders include comm (for commuting) and braid
(for braiding relations which generalize both comm and anti).
This means we can define qudits immediately, though to save the user
trouble, we have shortcuts
yaw> $alg = qubit()
and more generally
yaw> $alg = qudit(d)
We will cover the algebra of qudits, and the generalization to
oscillators, in more detail elsewhere, but for the record, qudit(d)
stands for
yaw> <X, Z | herm, unit, pow(d), braid(exp(2*pi*I/d))>
and braid(ω) means \(XZ = \omega ZX\), with \(X\) being "to the left" of
\(Z\) in the generator ordering meaning that we take the braiding in
this direction.
2.2 Operators
Once we have an algebra, we can start to build things with the basic operators at our disposal. We can recover familiar single-qubit operators, for instance
yaw> Y = 1j*X*Z yaw> H = (X + Z)/sqrt(2)
We use "engineering" notation 1j for the imaginary unit
\(\sqrt{-1}\).
We can check these satisfy the usual relations, for instance
Currently, there are (harmless) bugs in the REPL output meaning we
sometimes get 1.0 instead of I.
yaw> Y*Y 1 yaw> Y*X -1j*Z yaw> H*X == Z*H True yaw> H.d == H True
The notation .d stands for "dagger", or adjoint. Operators have a
spectrum of eigenvalues; in the absence of vectors (note that we haven't defined
these yet!) we say \(\lambda \in \text{spec}(A)\) is an eigenvalue just in case
\[
(A - \lambda I)^{-1}
\]
does not exist, i.e. \(A - \lambda I\) has no multiplicative inverse.
This spectrum is obtained numerically via
yaw> spec(Y) [1, -1]
A shortcut to finding eigenvalues is the minimal polynomial that \(A\) is a root of: \[ p_\min^{(A)}(A) = \sum_{k=0}^n c_k A^k = 0, \] which can produce using relations, e.g. \(X^2 - I = 0\) for the Pauli \(X\). The eigenvalues are the roots of \(p_\min^{(A)}(x)\). The polynomial and its solutions are produced as follows:
yaw> minimal_poly(Y)[0] Poly(x**2 - 1, x, domain='RR')
Observables correspond to self-adjoint operators, which in turn have a real spectrum.
2.3 States
The last entry in our cast of characters is the state. In the algebraic approach, states are not defined as normalized vectors in a Hilbert space; we haven't even defined a Hilbert space! But the transition isn't radical. Instead of thinking about the vector \(|\psi\rangle\), we think about the expectation it induces on operators: \[|\psi\rangle \in \mathcal{H} \quad \longrightarrow \quad \langle \psi | \cdot |\psi\rangle = \psi(\cdot) \in \mathcal{L}(\mathcal{A},\mathbb{C}), \] where \(\mathcal{L}(\mathcal{A},\mathbb{C})\) indicates the set of linear functions from \(\mathcal{A}\) to \(\mathbb{C}\), also called linear functionals on \(\mathcal{A}\). An important class is eigenstates of operators. If \(|\psi\rangle\) is an eigenstate of \(A \in\mathcal{A}\), then for any \(B \in\mathcal{A}\), we have the defining property \[ \langle \psi | AB|\psi\rangle = \langle \psi | A|\psi\rangle\langle \psi| B|\psi\rangle = \langle \psi | BA|\psi\rangle \] or \(\psi(AB)=\psi(A)\psi(B)\). The expectation \(\psi(A) \in \text{spec}(A)\), so we indicate eigenstates not by eigenvalue, but by the spot on the list of eigenvalues (in decreasing order). For instance, the eigenstate
yaw> char(Z, 0)
has eigenvalue \(1\), the largest eigenvalue of the Pauli
\(Z\); in the conventional approach this eigenstate is denoted
\(|0\rangle\), but in the algebraic approach, we label the functional by
\(\pi_0\). We can combine states, but since they are now interpreted as
expectations, we cannot take arbitrary linear combinations; instead,
we must respect probability and take convex combinations, i.e.
\[
\rho = p \pi + (1 - p) \pi'
\]
for \(p \in [0, 1]\). This is nothing other than mixing states.
The careless linear combination of states is a source of many bugs
in ordinary quantum code, so we make it more or less impossible by design.
yaw performs this mixture in a natural way, for example
yaw> rho = 0.3*char(Z,0) + 0.7*char(Z,1) 0.300*char(Z, 0) + 0.700*char(Z, 1)
We have said that a state is algebraically viewed as an
expectation. To take expectations like \(\psi(A) = \langle
\psi|A|\rangle\), or more generally \(\rho(A) = \mbox{Tr}[\rho A]\), we
use the vertical bar | and postfix notation:
yaw> Z | rho -0.400 yaw> Y | rho 0.0
As with generators and relations, or conditional probability, the bar
means "given"; in this case, the value of an observable given a state
as probabilistic context. This also recalls the pipe notation from
UNIX, and indeed, we are piping the operator as an argument to a
function. So it works on both levels!
3 Dynamics
We've dealt so far with the "statics" of algebraic quantum programming, the basic objects we define and use. Now, we turn to "dynamics": the art of making things change, from which algorithms emerge.
3.1 Conjugation
One of the simplest ways to modify an operator is a change of basis,
otherwise called conjugation:
\[
A \mapsto B^* A B = B \triangleright A.
\]
This satisfies various laws, most importantly
\[
B \triangleright (B' \triangleright A) = BB' \triangleright A.
\]
In quantum computing, \(U\) is often a unitary. For instance, the
standard recipe is to start with a state \(|0\rangle\), describe a
circuit \(U\), and measure a fixed observable \(M\) in state
\(U|0\rangle\). If \(M\) is an projector-valued measurement (we explain
more below) with projectors \(\Pi_i\), then the resulting distribution
has probabilities
\[
p_i = \langle 0| U^* \Pi_i U |0\rangle = \langle 0| (U\triangleright \Pi_i)|0\rangle.
\]
Alternatively, we can view \(\langle 0| \cdot |0\rangle\)
as a functional, and define conjugation of a state via
\[
U \triangleleft \langle 0| \cdot |0\rangle = \langle 0|U^* \cdot U|0\rangle.
\]
Thus, if \(\pi_0(\cdot) = \langle 0| \cdot |0\rangle\), then
\[
p_i = \pi_0(U \triangleright \Pi_i) = [U \triangleleft \pi_0](\Pi_i).
\]
This models the general situation:
\[
\pi(B \triangleright A) = \pi(B^* A B) = [B \triangleleft \pi](A).
\]
In yaw, we replace \(\triangleright\) and \(\triangleleft\) with >> and <<:
yaw> H >> X Z yaw> Z | X << char(Z, 0) -1 yaw> Z | X << char(Z, 0) == X >> Z | char(Z, 0) True
So, now we have a basic way to transform operators and states.
3.2 Tensor products
Conjugation provides compositional structure, which is effective structure in time. The easiest way to add structure in space is to make a composite systems using the tensor product, \(\mathcal{A} \otimes \mathcal{A}'\). This consists of all linear combinations of operators of the form \(A \otimes A'\) for \(A \in\mathcal{A}, A' \in \mathcal{A}'\). Technically, we have to fill in "holes" with respect to a norm function. This is quite involved, so we won't go into the details. The tensor product of the set of states is exactly the set of states of the tensor product. More precisely, \[ \mathcal{L}(\mathcal{A}\otimes \mathcal{A}',\mathbb{C}) \cong \mathcal{L}(\mathcal{A},\mathbb{C}) \otimes \mathcal{L}(\mathcal{A}',\mathbb{C}), \] where \(\cong\) indicates these structures are isomorphic. Thus, we can cobble together tensor products of states and get every state on the tensor product.
For now, we focus on multiple copies of the same algebra, written
\(\mathcal{A}^{\otimes n} = \mathcal{A} \otimes \cdots \otimes
\mathcal{A}\). This is the situation we typically encounter with, say,
\(n\) qubits.
In yaw, tensor products are invoked with @. You can add and
multiply as usual:
yaw> (X @ X)*(X @ X) I @ I yaw> 2*(X@X) - (X@X) X @ X
We can conjugate tensor products of both states and operators:
yaw> (H @ H) >> (X @ X) Z @ Z yaw> (X @ X) << (char(Z, 0) @ char(Z, 0)) char(Z, 1) @ char(Z, 1)
So now we have structure in both time and space!
3.3 Selection
Conjugation composes operations in time, tensor products in
space. Another important operation is selection. This is implemented
by projectors, also called projection operators, satisfying
\[
\Pi^2 = \Pi, \quad \Pi^* = \Pi,
\]
i.e. Hermitian idempotents.
These are the operator analogue of Boolean variables, since they have
precisely two eigenvalues, corresponding to Boolean values:
\[
\Pi^2 - \Pi = 0 \quad \Longrightarrow \quad \text{spec}(\Pi) = \{0, 1\}.
\]
We can always decompose a well-behaved operator \(N\)
By the spectral theorem, any normal operator (which commutes with its
adjoint) can be so decomposed.
into a sum of orthogonal projectors, each associated to a
distinct eigenvalue \(\lambda_i\):
\[
N = \sum_i \lambda_i \Pi_i, \quad \Pi_i \Pi_j = \delta_{ij} \Pi_i.
\]
In yaw, we can pluck out the relevant projector \(\Pi_i\) using the
ordering of eigenvalues, in the same way we label eigenstates:
yaw> proj0 = proj(Z,0) proj(Z, 0) yaw> pi0 = char(Z,0) char(Z, 0) yaw> proj0 | pi0 1
Optional. Under the hood, we actually construct projectors from the minimal polynomial; if you find the details boring, feel free to skip to the next section. When \(N\) is diagonalizable, the minimal polynomial takes the form \[ p_\min^{(N)}(x) = \prod_i (x - \lambda_i) \] for distinct \(\lambda_i\). Focusing on \(\lambda_j\), we can divide out the associated linear factor: \[ p_j^{(N)}(x) = \prod_{i\neq j} (x - \lambda_i). \] Note that \[ p_\min^{(N)}(N) = 0 \quad \Longrightarrow \quad p_j^{(N)}(N)N = \lambda_j p_j^{(N)}(N). \] Thus, \(p_j^{(N)}(N)\) resembles \(\Pi_j\), which also acts as \(\Pi_j N = \lambda_j\Pi_j\). To nail this identification, we need to check the \(p_j^{(N)}(N)\) are orthogonal and idempotent. Orthogonality follows immediately since \[ 0 = p_\min^{(N)}(N) | p_j^{(N)}(N) p_k^{(N)}(N) \] for any \(j \neq k\). For idempotence, suppose \[ p_j^{(N)}(x) = \sum_{k=0}^d c_k x^k. \] Then, using \(p_j^{(N)}(N)N =\lambda_j p_j^{(N)}(N)\), the square \[ \left[p_j^{(N)}(N)\right]^2 = p_j^{(N)}(N) \sum_{k=0}^d c_k N^k = p_j^{(N)}(N) \sum_{k=0}^d c_k \lambda_j^k = p_j^{(N)}(N) p_j^{(N)}(\lambda_j). \] Thus, the idempotent variable is not \(p_j^{(N)}(N)\) itself, but the scaled version \[ \Pi_j = \frac{p_j^{(N)}(N)}{p_j^{(N)}(\lambda_j)}. \] Technically, this is just an instance of the Lagrange interpolation formula, but that's another story!
3.4 Control
A naturally corollary to selection is controlled operations. Usually, these
are introduced in Hilbert space, and in a basis-dependent way. Here,
we use a completely algebraic formulation which is independent of
basis. The idea is simple: instead of controlling on the state of a
qubit, with \(|0\rangle\) ignoring a target system and \(|1\rangle\)
performing operation \(U\), we can project on the control system, and write a
combined operator
\[
\text{C}U = \Pi_0 \otimes I + \Pi_1 \otimes U.
\]
We can implement the \(\text{CNOT}\) (\(\text{C}X\)) operation as well as
as the Toffoli (\(\text{CCNOT}\)) in yaw:
yaw> CNOT = ctrl(Z,[I,X]) proj(Z, 0) @ I + proj(Z, 1) @ X yaw> CCNOT = ctrl(Z,[I @ I,CNOT]) proj(Z, 0) @ I @ I + proj(Z, 1) @ CNOT
If we want to replace a qubit with a qudit, decomposed as \(N = \sum_i \lambda_i \Pi_i\), and \(U\) with a collection \(U_i\), then the corresponding controlled operator is \[ \text{C}U = \sum_i \Pi_i \otimes U_i. \] For instance, the qutrit version of \(\text{C}X\) is
yaw> $alg = qudit(3) Created algebra: <X, Z | herm, unit, pow(3), braid(exp(2*pi*I/3))> yaw> CX = ctrl(Z, [I, X, X**2]) proj(Z, 0) @ I + proj(Z, 1) @ X + proj(Z, 2) @ X**2
We will explore further applications of this idea in future tutorials.
3.5 Example: Bell states
We finish with a simple example that combines all of the above ideas: generating entanglement. The famous entangled state \[ |\phi\rangle = \frac{1}{\sqrt{2}}(|00\rangle + |11\rangle) \] is actually one of a family of quadruplets, individually entangled but mutually orthogonal, called the Bell states: \[|\phi_{ab}\rangle = \frac{1}{\sqrt{2}}(|0b\rangle + (-1)^a |1\overline{b}\rangle), \] where \(\overline{b} = 1 - b\). The way to prepare these states is to apply the usual Bell state-preparing circuit, but to \(|ab\rangle\): \[| \phi_{ab}\rangle = \text{CNOT} (H \otimes I) |ab\rangle, \] which I leave as an exercise. In the algebraic setting, the circuit \(U\) acting on \(|0\rangle\) becomes conjugation of \(\pi_0(\cdot) = \langle 0|\cdot|0 \rangle\) by \(U\), so
yaw> def phi(a, b): ... init = X**a @ X**b << pi0 @ pi0 ... return CNOT*(H @ I) << init yaw> Z @ Z | phi(0,0) 1
To emphasize the power of yaw's basic combinators, let's write
the full (compiler) code to generate the canonical Bell state \(\phi\):
$alg = <X, Z | herm, unit, anti> H = (X + Z)/sqrt(2) pi0 = char(Z, 0) CNOT = ctrl(Z, [I, X]) phi = CNOT*(H @ I) << pi0 @ pi0
That's it; five lines, and we've defined everything from scratch. Next time, we'll use these states to perform a magic trick called teleportation!
4 References
- Quantum Computation and Quantum Information (2000), Michael Nielsen and Isaac Chuang. The classic introduction to the quantum.
- “SIQP I: Foundations” (2025), David Wakeham. The (much longer) mathematical companion to these tutorials.
