Quantum Computing Foundations: The Theory Behind QPL

A comprehensive guide to the quantum mechanics you need to understand relations-first quantum programming. From superposition to entanglement entropy.
quantum computing
theory
physics
tutorial
Author

David Coldeira

Published

December 13, 2025

Why This Post Exists

This is the prerequisite for understanding Stage Zero: Programming Quantum Reality Through Relations, Not Gates.

If you’re coming from classical programming and want to understand quantum computing—not just run circuits, but actually understand what’s happening—this guide is for you.

We’ll cover the physics that makes quantum computing possible:

  1. Quantum States - What |0⟩, |1⟩, and superposition actually mean
  2. Measurement & Basis - Why “measuring” depends on what question you ask
  3. Entanglement - The fundamental resource of quantum computing
  4. Correlations - Why entangled systems show different correlations in different bases
  5. Entanglement Entropy - How to quantify quantum correlation
  6. Basis Transformations - The mathematics behind multi-basis measurements

By the end, you’ll understand: - Why quantum computing is different from probabilistic classical computing - Why entanglement is a relation between systems, not a property of systems - Why measurement basis matters (the foundation of QPL’s QuantumQuestion abstraction) - The physics that QPL Stage Zero implements

Let’s start from first principles.


Prerequisites

pip install qiskit qiskit-aer matplotlib numpy
# Import libraries
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import Aer
from qiskit.visualization import plot_histogram, circuit_drawer
from qiskit.quantum_info import Statevector, partial_trace, entropy
import matplotlib.pyplot as plt
import numpy as np

%matplotlib inline
print("✅ Qiskit imported successfully!")

Part 1: Quantum States - The Foundation

Classical Bits vs Qubits

A classical bit is either 0 or 1. Period.

A qubit is a quantum system that, when measured, gives 0 or 1—but before measurement, it exists in a superposition of both states.

The Mathematical Representation

We write quantum states using Dirac notation (bra-ket notation):

\[|0\rangle = \begin{pmatrix} 1 \\ 0 \end{pmatrix}, \quad |1\rangle = \begin{pmatrix} 0 \\ 1 \end{pmatrix}\]

These are basis states (like unit vectors in 2D space).

A general qubit state is:

\[|\psi\rangle = \alpha|0\rangle + \beta|1\rangle = \begin{pmatrix} \alpha \\ \beta \end{pmatrix}\]

where \(\alpha, \beta\) are complex numbers satisfying \(|\alpha|^2 + |\beta|^2 = 1\) (normalization).

Why Complex Numbers?

Classical probability uses real numbers: P(heads) = 0.5, P(tails) = 0.5.

Quantum mechanics uses probability amplitudes (complex numbers). The probability of measuring a state is:

\[P(0) = |\alpha|^2, \quad P(1) = |\beta|^2\]

The complex phase matters for interference—the key to quantum algorithms.

Let’s see this in code:

# Create a qubit in state |0⟩
state_0 = Statevector([1, 0])
print("State |0⟩:")
print(state_0)
print(f"Probabilities: P(0) = {abs(state_0[0])**2}, P(1) = {abs(state_0[1])**2}")

print("\n" + "="*50 + "\n")

# Create a qubit in state |1⟩
state_1 = Statevector([0, 1])
print("State |1⟩:")
print(state_1)
print(f"Probabilities: P(0) = {abs(state_1[0])**2}, P(1) = {abs(state_1[1])**2}")

print("\n" + "="*50 + "\n")

# Create a superposition: (|0⟩ + |1⟩)/√2
state_plus = Statevector([1/np.sqrt(2), 1/np.sqrt(2)])
print("State |+⟩ = (|0⟩ + |1⟩)/√2:")
print(state_plus)
print(f"Probabilities: P(0) = {abs(state_plus[0])**2:.3f}, P(1) = {abs(state_plus[1])**2:.3f}")

Key insight: The state \(|+\rangle\) is NOT “50% chance of being 0, 50% chance of being 1.” It’s both simultaneously until measured. The probabilities only emerge upon measurement.


Part 2: Superposition - Both At Once

The Hadamard Gate: Creating Superposition

The Hadamard gate (H) transforms basis states into superposition:

\[H|0\rangle = \frac{|0\rangle + |1\rangle}{\sqrt{2}} = |+\rangle\] \[H|1\rangle = \frac{|0\rangle - |1\rangle}{\sqrt{2}} = |-\rangle\]

Mathematically, H is a matrix:

\[H = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & 1 \\ 1 & -1 \end{pmatrix}\]

Applying H to \(|0\rangle\):

\[H|0\rangle = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & 1 \\ 1 & -1 \end{pmatrix} \begin{pmatrix} 1 \\ 0 \end{pmatrix} = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 \\ 1 \end{pmatrix} = |+\rangle\]

# Create a circuit with Hadamard gate
qc = QuantumCircuit(1, 1)
qc.h(0)  # Apply Hadamard to qubit 0
qc.measure(0, 0)

# Draw the circuit
print("Circuit:")
display(qc.draw('mpl'))

# Simulate
simulator = Aer.get_backend('qasm_simulator')
job = simulator.run(qc, shots=1000)
counts = job.result().get_counts()

print("\nMeasurement results after H gate:")
print(counts)
plot_histogram(counts)

Result: Approximately 50% |0⟩ and 50% |1⟩.

But here’s the crucial difference from classical randomness:

  • Classical coin flip: The coin is either heads or tails before you look. You just don’t know which.
  • Quantum superposition: The qubit is actually both |0⟩ and |1⟩ until measured. This is verified by interference experiments.

Part 3: Measurement - Asking Questions, Not Reading Answers

Measurement Collapses the State

When you measure a qubit in superposition:

  1. Before measurement: \(|\psi\rangle = \alpha|0\rangle + \beta|1\rangle\) (superposition)
  2. Measurement: You get outcome 0 with probability \(|\alpha|^2\), or outcome 1 with probability \(|\beta|^2\)
  3. After measurement: The state collapses to \(|0\rangle\) or \(|1\rangle\) (superposition destroyed)

This is irreversible. You can’t “un-measure” a qubit.

The Z Basis (Computational Basis)

When we say “measure a qubit,” we usually mean measure in the Z basis (also called computational basis):

  • Eigenstates: \(|0\rangle\) (eigenvalue +1) and \(|1\rangle\) (eigenvalue -1)
  • Measurement operator: \(Z = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix}\)

But this is just one choice of basis. We could measure in other bases…


Part 4: Measurement Basis - Context Matters

The X Basis (Hadamard Basis)

We can also measure in the X basis:

  • Eigenstates: \(|+\rangle = \frac{|0\rangle + |1\rangle}{\sqrt{2}}\) and \(|-\rangle = \frac{|0\rangle - |1\rangle}{\sqrt{2}}\)
  • Measurement operator: \(X = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}\)

This is a completely different question to ask the qubit!

Same State, Different Measurements

Consider the state \(|0\rangle\):

Measured in Z basis: - \(|0\rangle\) is an eigenstate of Z - Result: 100% chance of outcome 0

Measured in X basis: - \(|0\rangle = \frac{|+\rangle + |-\rangle}{\sqrt{2}}\) (superposition in X basis!) - Result: 50% chance of + outcome, 50% chance of - outcome

The measurement outcome depends on which basis you choose. This is why QPL has QuantumQuestion with explicit basis—the question you ask determines the answer you get.

# Measure |0⟩ in Z basis (default)
qc_z = QuantumCircuit(1, 1)
# Qubit starts in |0⟩
qc_z.measure(0, 0)

print("Measuring |0⟩ in Z basis:")
job_z = simulator.run(qc_z, shots=1000)
counts_z = job_z.result().get_counts()
print(counts_z, "← 100% outcome 0")

print("\n" + "="*50 + "\n")

# Measure |0⟩ in X basis (apply H before measurement to rotate basis)
qc_x = QuantumCircuit(1, 1)
# Qubit starts in |0⟩
qc_x.h(0)  # Rotate from Z basis to X basis
qc_x.measure(0, 0)

print("Measuring |0⟩ in X basis:")
job_x = simulator.run(qc_x, shots=1000)
counts_x = job_x.result().get_counts()
print(counts_x, "← ~50% outcome 0, ~50% outcome 1")

# Visualize both
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
plot_histogram(counts_z, ax=ax1, title="Z Basis Measurement of |0⟩")
plot_histogram(counts_x, ax=ax2, title="X Basis Measurement of |0⟩")
plt.tight_layout()
plt.show()

Key insight: The state doesn’t change, but the measurement basis does. This is contextuality—a fundamental feature of quantum mechanics that QPL makes explicit.


Part 5: Entanglement - Relations Between Systems

What is Entanglement?

Entanglement is when the quantum state of a multi-particle system cannot be factored into independent single-particle states.

Separable (not entangled): \[|\psi\rangle = |0\rangle_A \otimes |0\rangle_B = |00\rangle\] Alice has \(|0\rangle\), Bob has \(|0\rangle\). These are independent.

Entangled (Bell state): \[|\Phi^+\rangle = \frac{|00\rangle + |11\rangle}{\sqrt{2}}\] You cannot write this as \(|\psi\rangle_A \otimes |\phi\rangle_B\). Alice and Bob’s states are fundamentally correlated.

Creating a Bell State

The standard recipe: 1. Start with \(|00\rangle\) 2. Apply Hadamard to first qubit: \(\frac{|0\rangle + |1\rangle}{\sqrt{2}} \otimes |0\rangle = \frac{|00\rangle + |10\rangle}{\sqrt{2}}\) 3. Apply CNOT (controlled-NOT): \(\frac{|00\rangle + |11\rangle}{\sqrt{2}} = |\Phi^+\rangle\)

# Create Bell state circuit
qc_bell = QuantumCircuit(2, 2)
qc_bell.h(0)           # Hadamard on qubit 0
qc_bell.cx(0, 1)       # CNOT from qubit 0 to 1
qc_bell.measure([0, 1], [0, 1])

print("Bell State Circuit:")
display(qc_bell.draw('mpl'))

# Simulate
job_bell = simulator.run(qc_bell, shots=1000)
counts_bell = job_bell.result().get_counts()

print("\nMeasurement results:")
print(counts_bell)
plot_histogram(counts_bell)

Result: ~50% |00⟩ and ~50% |11⟩. Never |01⟩ or |10⟩.

Why This is Weird

Before measurement: - Alice’s qubit is not “definitely 0” or “definitely 1” - Bob’s qubit is not “definitely 0” or “definitely 1” - But they are perfectly correlated

When Alice measures her qubit: - If she gets 0, Bob will definitely get 0 - If she gets 1, Bob will definitely get 1

This correlation exists regardless of how far apart they are. This is quantum entanglement.

In QPL, this is why entanglement is a QuantumRelation—it’s fundamentally about the relationship between systems, not properties of individual systems.


Part 6: Measuring Entangled Systems - When Basis Matters

Same Basis: Perfect Correlation

If Alice and Bob both measure in Z basis on a Bell state: - 100% correlation (always get same result)

We just saw this above.

# Create Bell state and measure both in Z basis
qc_zz = QuantumCircuit(2, 2)
qc_zz.h(0)
qc_zz.cx(0, 1)
# Measure both in Z basis (default)
qc_zz.measure([0, 1], [0, 1])

job_zz = simulator.run(qc_zz, shots=1000)
counts_zz = job_zz.result().get_counts()

print("Bell state, both measured in Z basis:")
print(counts_zz)

# Compute correlation
same_results = counts_zz.get('00', 0) + counts_zz.get('11', 0)
correlation_zz = same_results / 1000
print(f"\nCorrelation (same outcome rate): {correlation_zz*100:.1f}%")

Different Basis: Random Correlation

If Alice measures in Z basis and Bob measures in X basis: - ~50% correlation (random, uncorrelated)

This is critical for understanding QPL Stage Zero! The bug that was fixed was exactly this: cross-basis measurements were showing 100% correlation when they should show 50%.

# Create Bell state, Alice measures Z, Bob measures X
qc_zx = QuantumCircuit(2, 2)
qc_zx.h(0)
qc_zx.cx(0, 1)

# Alice measures in Z basis (default)
qc_zx.measure(0, 0)

# Bob measures in X basis (apply H first to rotate basis)
qc_zx.h(1)
qc_zx.measure(1, 1)

job_zx = simulator.run(qc_zx, shots=1000)
counts_zx = job_zx.result().get_counts()

print("Bell state, Alice: Z basis, Bob: X basis:")
print(counts_zx)

# Compute correlation
same_results = counts_zx.get('00', 0) + counts_zx.get('11', 0)
correlation_zx = same_results / 1000
print(f"\nCorrelation (same outcome rate): {correlation_zx*100:.1f}%")

# Compare both
print("\n" + "="*50)
print(f"Z-Z correlation: {correlation_zz*100:.1f}% (perfect correlation)")
print(f"Z-X correlation: {correlation_zx*100:.1f}% (random, uncorrelated)")
print("="*50)

This is the physics that QPL Stage Zero implements correctly.

The measurement basis changes the physics. This is why QuantumQuestion in QPL has an explicit basis parameter—because quantum mechanics is contextual.


Part 7: Entanglement Entropy - Quantifying Correlation

How Do We Measure “How Entangled” Two Systems Are?

For a pure bipartite state (2 subsystems), we use entanglement entropy (also called von Neumann entropy).

The Math: Schmidt Decomposition

Any 2-qubit pure state can be written as:

\[|\psi\rangle = \sum_i \lambda_i |i\rangle_A \otimes |i\rangle_B\]

where \(\lambda_i \geq 0\) are the Schmidt coefficients (singular values).

The entanglement entropy is:

\[S = -\sum_i \lambda_i^2 \log_2(\lambda_i^2)\]

Interpretation: - \(S = 0\): No entanglement (separable state) - \(S = 1\): Maximal entanglement (Bell state) - \(0 < S < 1\): Partial entanglement

Examples

Separable state: \(|00\rangle\) - Schmidt decomposition: \(\lambda_1 = 1, \lambda_2 = 0\) - \(S = -(1^2 \log_2(1^2) + 0) = 0\)

Bell state: \(\frac{|00\rangle + |11\rangle}{\sqrt{2}}\) - Schmidt decomposition: \(\lambda_1 = \lambda_2 = \frac{1}{\sqrt{2}}\) - \(S = -(\frac{1}{2}\log_2(\frac{1}{2}) + \frac{1}{2}\log_2(\frac{1}{2})) = 1\)

Let’s compute this in Qiskit:

# Separable state: |00⟩
separable_state = Statevector([1, 0, 0, 0])  # |00⟩
rho_A_sep = partial_trace(separable_state, [1])  # Trace out qubit 1
S_sep = entropy(rho_A_sep, base=2)

print("Separable state |00⟩:")
print(f"Entanglement entropy: {S_sep:.6f}")

print("\n" + "="*50 + "\n")

# Bell state: (|00⟩ + |11⟩)/√2
bell_state = Statevector([1/np.sqrt(2), 0, 0, 1/np.sqrt(2)])  # |Φ+⟩
rho_A_bell = partial_trace(bell_state, [1])  # Trace out qubit 1
S_bell = entropy(rho_A_bell, base=2)

print("Bell state (|00⟩ + |11⟩)/√2:")
print(f"Entanglement entropy: {S_bell:.6f}")

print("\n" + "="*50 + "\n")

# Partially entangled state: 0.9|00⟩ + 0.436|11⟩
partial_state = Statevector([0.9, 0, 0, 0.436])
partial_state = partial_state / np.linalg.norm(partial_state.data)  # Normalize
rho_A_partial = partial_trace(partial_state, [1])
S_partial = entropy(rho_A_partial, base=2)

print("Partially entangled state:")
print(f"Entanglement entropy: {S_partial:.6f}")

print("\n" + "="*50)
print("Summary:")
print(f"  Separable:  S = {S_sep:.3f} (no entanglement)")
print(f"  Partial:    S = {S_partial:.3f} (some entanglement)")
print(f"  Maximal:    S = {S_bell:.3f} (Bell state)")
print("="*50)

This is the math that QPL Stage Zero computes automatically.

Every QuantumRelation object in QPL has an entanglement_entropy field that’s computed via Schmidt decomposition. This lets you see the entanglement structure of your quantum program.


Part 8: Basis Transformations - The Key to Multi-Basis Measurement

The Problem

Quantum circuits naturally measure in the Z (computational) basis. But what if we want to measure in the X basis? Or an arbitrary basis at angle \(\theta\)?

The Solution: Unitary Rotation

To measure in basis defined by unitary \(U\), we: 1. Apply \(U^\dagger\) (conjugate transpose) to rotate state to computational basis 2. Measure in computational basis 3. Interpret result as measurement in original basis

Example: X basis measurement

The X basis has eigenstates \(|+\rangle\) and \(|-\rangle\). To measure in X basis: - Apply \(H\) (Hadamard) before measurement - \(H\) is its own inverse: \(H^\dagger = H\) - \(H\) rotates X basis to Z basis

The Mathematics

For a state \(|\psi\rangle\) and measurement basis \(U\):

\[|\psi'\rangle = U^\dagger |\psi\rangle\]

Then measure \(|\psi'\rangle\) in computational basis.

This is exactly what QPL Stage Zero implements in measurement.py:

def compute_subsystem_probabilities(state, basis, subsystem_idx, num_qubits):
    state_matrix = state.reshape(2, 2)
    
    # Apply U† to transform to measurement basis
    if not np.allclose(basis, np.eye(2)):
        if subsystem_idx == 0:
            state_matrix = basis.T.conj() @ state_matrix
        else:
            state_matrix = state_matrix @ basis.T.conj()
    
    # Compute probabilities in transformed basis
    # ...

Let’s verify this works:

# Define X basis matrix
H_matrix = np.array([[1, 1], [1, -1]]) / np.sqrt(2)

# State |0⟩
state_0 = np.array([1, 0])

# Transform to X basis: H† |0⟩
state_in_x_basis = H_matrix.T.conj() @ state_0

print("State |0⟩ transformed to X basis:")
print(state_in_x_basis)
print(f"\nThis is |+⟩ = (|0⟩ + |1⟩)/√2")
print(f"\nProbabilities when measured in computational basis:")
print(f"P(0) = {abs(state_in_x_basis[0])**2:.3f}")
print(f"P(1) = {abs(state_in_x_basis[1])**2:.3f}")
print("\n→ This means 50% + outcome, 50% - outcome in X basis")

This is textbook quantum mechanics (Nielsen & Chuang, Chapter 2), but it’s the foundation of QPL’s multi-basis measurement system.


Part 9: Why This Matters for QPL

Everything we’ve covered is implemented in QPL Stage Zero:

1. Quantum States → QuantumRelation

bell_pair = program.entangle(alice, bob)
# bell_pair.state is the 4D state vector

2. Superposition → First-Class

# Superposition is natural in QPL
system = program.create_system()  # |0⟩
# In Stage 1+: apply_hadamard(system)  # |+⟩

3. Measurement Basis → QuantumQuestion

question_z = create_question(QuestionType.SPIN_Z)  # Z basis
question_x = create_question(QuestionType.SPIN_X)  # X basis

4. Contextual Measurement → ask(relation, question, perspective)

result = program.ask(bell_pair, question_z, perspective="alice")
# Explicitly: who asks, what they ask, what they ask it of

5. Entanglement Tracking → Automatic

print(bell_pair.entanglement_entropy)  # → 1.0 (maximal)

6. Basis Transformation → Built-In

# QPL automatically applies U† when you specify basis in QuantumQuestion
# You don't write the matrix multiplication—it's in the semantics

7. Cross-Basis Correlations → Correct Physics

# After the bug fix:
alice_z = ask(bell_pair, SPIN_Z, subsystem=0, perspective="alice")
bob_x = ask(remaining, SPIN_X, subsystem=1, perspective="bob")
# Correlation: ~50% (correct!)

Summary: The Theory You Need

To understand QPL Stage Zero, you need to understand:

Quantum states are superpositions of basis states, not classical probabilities

Measurement collapses superposition and is contextual (basis-dependent)

Entanglement is a relation between systems, not a property of systems

Same-basis measurements of entangled systems show 100% correlation

Different-basis measurements of entangled systems show ~50% correlation

Entanglement entropy quantifies correlation via Schmidt decomposition

Basis transformations (\(U^\dagger\)) enable measurement in arbitrary bases

With this foundation, you can now understand why QPL’s abstractions—QuantumRelation, QuantumQuestion, Perspective—match the physics of quantum mechanics better than gate-based frameworks.


Next: Read Stage Zero

Now you’re ready: Stage Zero: Programming Quantum Reality Through Relations, Not Gates

You’ll see how all this theory translates into a working quantum programming language where: - Relations are first-class citizens - Questions (measurements) are explicit and contextual - Entanglement entropy is tracked automatically - The code reads like physics, not circuit design

Welcome to relations-first quantum programming.