Skip to content

cds2.quantum

Dense statevector quantum circuit simulation with common gates and measurement.

Quantum circuit simulation on dense statevectors.

QuantumCircuit

Dense statevector simulator over n qubits (little-endian ordering).

Source code in src/cds2/quantum.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
class QuantumCircuit:
    """Dense statevector simulator over ``n`` qubits (little-endian ordering)."""

    def __init__(self, n_qubits: int) -> None:
        if not 1 <= n_qubits <= 16:
            msg = "n_qubits must be between 1 and 16"
            raise ValueError(msg)
        self.n_qubits = n_qubits
        self._state: ComplexVector | None = None
        self.reset()

    def reset(self) -> QuantumCircuit:
        """Return to the all-zero computational basis state."""
        self._state = np.zeros(2**self.n_qubits, dtype=np.complex128)
        self._state[0] = 1.0
        return self

    def _require_state(self) -> ComplexVector:
        if self._state is None:
            self.reset()
        assert self._state is not None
        return self._state

    def _validate_target(self, qubit: int) -> int:
        if not 0 <= qubit < self.n_qubits:
            msg = f"qubit index out of range: {qubit}"
            raise ValueError(msg)
        return qubit

    def _apply_single(self, matrix: NDArray[np.complex128], target: int) -> None:
        state = self._require_state()
        step = 1 << target
        for base in range(0, state.size, step * 2):
            for offset in range(step):
                low = base + offset
                high = low + step
                low_amplitude = state[low]
                high_amplitude = state[high]
                state[low] = matrix[0, 0] * low_amplitude + matrix[0, 1] * high_amplitude
                state[high] = matrix[1, 0] * low_amplitude + matrix[1, 1] * high_amplitude

    def gate(self, name: str, qubit: int, theta: float | None = None) -> QuantumCircuit:
        """Apply a named single-qubit gate (optionally parameterized)."""
        self._apply_single(_single_qubit_matrix(name, theta), self._validate_target(qubit))
        return self

    def x(self, qubit: int) -> QuantumCircuit:
        """Pauli-X bit flip."""
        return self.gate("X", qubit)

    def y(self, qubit: int) -> QuantumCircuit:
        """Pauli-Y."""
        return self.gate("Y", qubit)

    def z(self, qubit: int) -> QuantumCircuit:
        """Pauli-Z phase flip."""
        return self.gate("Z", qubit)

    def h(self, qubit: int) -> QuantumCircuit:
        """Hadamard superposition gate."""
        return self.gate("H", qubit)

    def s(self, qubit: int) -> QuantumCircuit:
        """Phase gate S."""
        return self.gate("S", qubit)

    def t(self, qubit: int) -> QuantumCircuit:
        """T (pi/8) gate."""
        return self.gate("T", qubit)

    def rx(self, qubit: int, theta: float) -> QuantumCircuit:
        """Rotation about the X axis."""
        return self.gate("RX", qubit, theta)

    def ry(self, qubit: int, theta: float) -> QuantumCircuit:
        """Rotation about the Y axis."""
        return self.gate("RY", qubit, theta)

    def rz(self, qubit: int, theta: float) -> QuantumCircuit:
        """Rotation about the Z axis."""
        return self.gate("RZ", qubit, theta)

    def cnot(self, control: int, target: int) -> QuantumCircuit:
        """Controlled-NOT between two distinct qubits."""
        control_index = self._validate_target(control)
        target_index = self._validate_target(target)
        if control_index == target_index:
            msg = "control and target must differ"
            raise ValueError(msg)
        state = self._require_state()
        control_mask = 1 << control_index
        target_mask = 1 << target_index
        for basis in range(state.size):
            # outer test guarantees the target bit is clear, so partner is
            # always the larger index here
            if basis & control_mask and not basis & target_mask:
                partner = basis | target_mask
                state[basis], state[partner] = state[partner], state[basis]
        return self

    def cz(self, control: int, target: int) -> QuantumCircuit:
        """Controlled-Z phase kick on the |11> amplitude."""
        control_index = self._validate_target(control)
        target_index = self._validate_target(target)
        if control_index == target_index:
            msg = "control and target must differ"
            raise ValueError(msg)
        state = self._require_state()
        mask = (1 << control_index) | (1 << target_index)
        for basis in range(state.size):
            if basis & mask == mask:
                state[basis] *= -1.0
        return self

    def swap(self, first: int, second: int) -> QuantumCircuit:
        """Exchange two qubit registers via three CNOTs."""
        first_index = self._validate_target(first)
        second_index = self._validate_target(second)
        if first_index == second_index:
            return self
        return (
            self.cnot(first_index, second_index)
            .cnot(second_index, first_index)
            .cnot(first_index, second_index)
        )

    def statevector(self) -> ComplexVector:
        """Copy of the current amplitude vector."""
        copied: ComplexVector = self._require_state().copy()
        return copied

    def probabilities(self) -> NDArray[np.float64]:
        """Measurement probabilities over all computational basis states."""
        magnitudes: NDArray[np.float64] = np.asarray(
            np.abs(self._require_state()) ** 2, dtype=float
        )
        return magnitudes

    def measure(self, shots: int = 1000, seed: int | None = None) -> dict[int, int]:
        """Sample ``shots`` computational-basis outcomes with a seeded RNG."""
        if shots < 1:
            msg = "shots must be at least 1"
            raise ValueError(msg)
        rng_values = np.random.default_rng(seed)
        draws = rng_values.choice(self.probabilities().size, size=shots, p=self.probabilities())
        counts: dict[int, int] = {}
        for outcome in draws.tolist():
            counts[int(outcome)] = counts.get(int(outcome), 0) + 1
        return dict(sorted(counts.items()))

reset()

Return to the all-zero computational basis state.

Source code in src/cds2/quantum.py
76
77
78
79
80
def reset(self) -> QuantumCircuit:
    """Return to the all-zero computational basis state."""
    self._state = np.zeros(2**self.n_qubits, dtype=np.complex128)
    self._state[0] = 1.0
    return self

gate(name, qubit, theta=None)

Apply a named single-qubit gate (optionally parameterized).

Source code in src/cds2/quantum.py
106
107
108
109
def gate(self, name: str, qubit: int, theta: float | None = None) -> QuantumCircuit:
    """Apply a named single-qubit gate (optionally parameterized)."""
    self._apply_single(_single_qubit_matrix(name, theta), self._validate_target(qubit))
    return self

x(qubit)

Pauli-X bit flip.

Source code in src/cds2/quantum.py
111
112
113
def x(self, qubit: int) -> QuantumCircuit:
    """Pauli-X bit flip."""
    return self.gate("X", qubit)

y(qubit)

Pauli-Y.

Source code in src/cds2/quantum.py
115
116
117
def y(self, qubit: int) -> QuantumCircuit:
    """Pauli-Y."""
    return self.gate("Y", qubit)

z(qubit)

Pauli-Z phase flip.

Source code in src/cds2/quantum.py
119
120
121
def z(self, qubit: int) -> QuantumCircuit:
    """Pauli-Z phase flip."""
    return self.gate("Z", qubit)

h(qubit)

Hadamard superposition gate.

Source code in src/cds2/quantum.py
123
124
125
def h(self, qubit: int) -> QuantumCircuit:
    """Hadamard superposition gate."""
    return self.gate("H", qubit)

s(qubit)

Phase gate S.

Source code in src/cds2/quantum.py
127
128
129
def s(self, qubit: int) -> QuantumCircuit:
    """Phase gate S."""
    return self.gate("S", qubit)

t(qubit)

T (pi/8) gate.

Source code in src/cds2/quantum.py
131
132
133
def t(self, qubit: int) -> QuantumCircuit:
    """T (pi/8) gate."""
    return self.gate("T", qubit)

rx(qubit, theta)

Rotation about the X axis.

Source code in src/cds2/quantum.py
135
136
137
def rx(self, qubit: int, theta: float) -> QuantumCircuit:
    """Rotation about the X axis."""
    return self.gate("RX", qubit, theta)

ry(qubit, theta)

Rotation about the Y axis.

Source code in src/cds2/quantum.py
139
140
141
def ry(self, qubit: int, theta: float) -> QuantumCircuit:
    """Rotation about the Y axis."""
    return self.gate("RY", qubit, theta)

rz(qubit, theta)

Rotation about the Z axis.

Source code in src/cds2/quantum.py
143
144
145
def rz(self, qubit: int, theta: float) -> QuantumCircuit:
    """Rotation about the Z axis."""
    return self.gate("RZ", qubit, theta)

cnot(control, target)

Controlled-NOT between two distinct qubits.

Source code in src/cds2/quantum.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def cnot(self, control: int, target: int) -> QuantumCircuit:
    """Controlled-NOT between two distinct qubits."""
    control_index = self._validate_target(control)
    target_index = self._validate_target(target)
    if control_index == target_index:
        msg = "control and target must differ"
        raise ValueError(msg)
    state = self._require_state()
    control_mask = 1 << control_index
    target_mask = 1 << target_index
    for basis in range(state.size):
        # outer test guarantees the target bit is clear, so partner is
        # always the larger index here
        if basis & control_mask and not basis & target_mask:
            partner = basis | target_mask
            state[basis], state[partner] = state[partner], state[basis]
    return self

cz(control, target)

Controlled-Z phase kick on the |11> amplitude.

Source code in src/cds2/quantum.py
165
166
167
168
169
170
171
172
173
174
175
176
177
def cz(self, control: int, target: int) -> QuantumCircuit:
    """Controlled-Z phase kick on the |11> amplitude."""
    control_index = self._validate_target(control)
    target_index = self._validate_target(target)
    if control_index == target_index:
        msg = "control and target must differ"
        raise ValueError(msg)
    state = self._require_state()
    mask = (1 << control_index) | (1 << target_index)
    for basis in range(state.size):
        if basis & mask == mask:
            state[basis] *= -1.0
    return self

swap(first, second)

Exchange two qubit registers via three CNOTs.

Source code in src/cds2/quantum.py
179
180
181
182
183
184
185
186
187
188
189
def swap(self, first: int, second: int) -> QuantumCircuit:
    """Exchange two qubit registers via three CNOTs."""
    first_index = self._validate_target(first)
    second_index = self._validate_target(second)
    if first_index == second_index:
        return self
    return (
        self.cnot(first_index, second_index)
        .cnot(second_index, first_index)
        .cnot(first_index, second_index)
    )

statevector()

Copy of the current amplitude vector.

Source code in src/cds2/quantum.py
191
192
193
194
def statevector(self) -> ComplexVector:
    """Copy of the current amplitude vector."""
    copied: ComplexVector = self._require_state().copy()
    return copied

probabilities()

Measurement probabilities over all computational basis states.

Source code in src/cds2/quantum.py
196
197
198
199
200
201
def probabilities(self) -> NDArray[np.float64]:
    """Measurement probabilities over all computational basis states."""
    magnitudes: NDArray[np.float64] = np.asarray(
        np.abs(self._require_state()) ** 2, dtype=float
    )
    return magnitudes

measure(shots=1000, seed=None)

Sample shots computational-basis outcomes with a seeded RNG.

Source code in src/cds2/quantum.py
203
204
205
206
207
208
209
210
211
212
213
def measure(self, shots: int = 1000, seed: int | None = None) -> dict[int, int]:
    """Sample ``shots`` computational-basis outcomes with a seeded RNG."""
    if shots < 1:
        msg = "shots must be at least 1"
        raise ValueError(msg)
    rng_values = np.random.default_rng(seed)
    draws = rng_values.choice(self.probabilities().size, size=shots, p=self.probabilities())
    counts: dict[int, int] = {}
    for outcome in draws.tolist():
        counts[int(outcome)] = counts.get(int(outcome), 0) + 1
    return dict(sorted(counts.items()))