Generalized qubit representation in bb84 simulation
Following my previous post on simulating the BB84 protocol here, I have updated the implementation to use a more generalized representation for qubits and measurement bases. This approach moves beyond the fixed rectilinear and diagonal bases, allowing for bases defined by arbitrary angles.
Parameterized measurement basis
Instead of predefined bases, I now define a measurement basis using a single angle parameter \theta. The measurement corresponds to the observable:
\mathbf{O}(\theta) = \cos(\theta) \boldsymbol{\sigma}_x + \sin(\theta) \boldsymbol{\sigma}_z
where \boldsymbol{\sigma}_x and \boldsymbol{\sigma}_z are the Pauli X and Z matrices, respectively. The measurement outcomes correspond to the eigenvalues of this observable, and the post-measurement states are its eigenstates.
In Python, I implemented functions to calculate this observable and its normalized eigenstates for a given angle \theta (in degrees):
def observable(theta_deg):
# Observable defined as cos(θ)·X + sin(θ)·Z
theta = np.deg2rad(theta_deg)
return np.cos(theta) * np.array([[0, 1], [1, 0]], dtype=complex) + \
np.sin(theta) * np.array([[1, 0], [0, -1]], dtype=complex)
def eigenstates(theta_deg):
# Return eigenstates (normalized) of the above observable
obs = observable(theta_deg)
_, eigvecs = np.linalg.eigh(obs)
# Return the two orthogonal eigenstates
return normalize(eigvecs[:, 0:1]), normalize(eigvecs[:, 1:2])
For the standard BB84, the angles \theta=90^\circ and \theta=0^\circ correspond to the rectilinear (Z-basis) and diagonal (X-basis), respectively ("BASIS": [0, 90]).
Qubit preparation
The BB84Qubit class now initializes the quantum state based on the input classical bit (0 or 1) and the preparation angle \theta. The state is set to the corresponding eigenstate of \mathbf{O}(\theta).
class BB84Qubit:
def __init__(self, bit: int, angle_deg: float):
# classical bit: 0 or 1
self._mBit = bit
# basis angle, e.g., 0° (X-like), 90° (Z-like)
self._mAngle = angle_deg
# initial quantum state (ket)
self._mState = self._prepareQubit(bit, angle_deg)
# ... properties ...
def _prepareQubit(self, bit, theta_deg):
# Get the two eigenstates for the preparation angle
v0, v1 = eigenstates(theta_deg)
# Select the eigenstate corresponding to the bit value
# Note: The assignment of bit '0' or '1' to a specific eigenstate
# depends on the eigenvalues; here it implicitly uses the
# order from np.linalg.eigh.
state_vector = v0 if bit == 0 else v1
# The state is represented in the computational basis |0⟩, |1⟩
return state_vector[0, 0] * ket0() + state_vector[1, 0] * ket1()
The state self._mState is stored as a complex vector in the standard computational basis \{|0\rangle, |1\rangle\}.
Generalized measurement
Measurement is performed relative to a basis defined by a potentially different angle, theta_deg. The measureQubit function calculates the probability of projecting the current state |\boldsymbol \psi\rangle = \alpha|\mathbf 0\rangle + \beta|\mathbf 1\rangle onto each eigenstate (|\mathbf v_0\rangle, |\mathbf v_1\rangle) of the measurement observable \mathbf{O}(\theta_{\text{measure}}). The probabilities are given by Born’s rule: \mathcal P_i = |\langle \mathbf v_i | \boldsymbol \psi \rangle|^2. A random outcome (0 or 1) is chosen based on these probabilities, and the qubit’s state collapses to the corresponding eigenstate |\mathbf v_{\text{outcome}}\rangle.
def measureQubit(state, theta_deg):
# Perform a measurement of 'state' in the basis defined by theta_deg
obs = observable(theta_deg)
# Eigenvalues and eigenvectors (eigenstates)
_, eigvecs = np.linalg.eigh(obs)
basis = [eigvecs[:, 0:1], eigvecs[:, 1:2]] # The eigenstates |v₀⟩, |v₁⟩
# Calculate projection probabilities: Pᵢ = |⟨vᵢ|ψ⟩|²
probs = [np.abs(b.conj().T @ state)[0, 0]**2 for b in basis]
# Handle potential floating point inaccuracies
probs = [max(0.0, np.real(p)) for p in probs]
total = sum(probs)
probs = [p / total if total > 0 else 0.5 for p in probs] # Normalize probabilities
# Simulate measurement outcome based on probabilities
outcome = np.random.choice([0, 1], p=probs)
# Return classical outcome (0 or 1) and the collapsed state |v_outcome⟩
return outcome, basis[outcome]
# Inside BB84Qubit class:
def Measure(self, theta_deg):
# Perform measurement in specified basis angle
result, collapsed_state = measureQubit(self._mState, theta_deg)
# Update the qubit's internal state to the post-measurement state
self._mState = collapsed_state
# Return the classical outcome
return result
If the measurement angle matches the preparation angle (theta_deg == self._mAngle), the outcome deterministically matches the original bit (self._mBit), assuming perfect state preparation and measurement. If the angles differ, the outcome is probabilistic.
Protocol integration
The BB84Protocol class uses these generalized BB84Qubit objects. Alice generates qubits using random bits and randomly chosen angles from the configured BASIS list. Bob measures using his own randomly chosen angles from the same list. The reconciliation and QBER check proceed as before, comparing results only when Alice and Bob happened to choose the same angle (basis). Eavesdropping simulation also uses this generalized measurement.
# In BB84Protocol.generateKey:
angles = np.random.choice(self._cfg.BASIS, L) # Use configured angles
self._qubits_a = [BB84Qubit(b, theta) for b, theta in zip(bits, angles)]
# In BB84Protocol.reconcileKey:
self._angles_b = np.random.choice(self._cfg.BASIS, length) # Bob chooses angles
bits_b = [q.Measure(theta) for q, theta in zip(self._qubits_b, self._angles_b)]
# ... basis comparison uses angles_a[i] == self._angles_b[i] ...
This updated simulation provides a more flexible framework for exploring QKD concepts by parameterizing the measurement basis, while retaining the core logic of the BB84 protocol.
For access to the complete simulation code, please visit the GitHub repository here.