Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ jobs:
name: Tests (Ubuntu)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: '3.12'
python-version: '3.14'
architecture: 'x64'
- name: Install requirements
run: |
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ local
.idea
.pytest_cache
obj
.venv
.venv
.vscode
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ List of projects:
* [Quantum Lookups](quantum_lookups/Quantum%20Lookups.ipynb) (2025)
* [Screening Task for QOSF Mentorship Program](qosf_tasks/2025-gate-tomography/Gate%20Tomography.ipynb) (2025)
* [Formal verification of Q# circuits](qsharp-verification/formal-verification-qsharp.ipynb) (2026)
* [Cirq Sparse Simulator](cirq_sparse_sim/) (2026)



Larger projects outside of this repository:
Expand Down
233 changes: 233 additions & 0 deletions cirq_sparse_sim/Demo.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "904fadff-b085-43ed-9a24-f07a892fb238",
"metadata": {},
"source": [
"### SparseSimulator\n",
"\n",
"This demo compares performance of cirq's built-in dense state simulator (cirq.Simulator) and my SparseSimulator."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "12210309-921d-461f-90eb-2ff6714f2355",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Simulator: n=3, time_elapsed=0.005978\n",
"Simulator: n=4, time_elapsed=0.042375\n",
"Simulator: n=5, time_elapsed=1.041485\n",
"SparseSimulator: n=10, time_elapsed=0.000767\n",
"SparseSimulator: n=100, time_elapsed=0.003140\n",
"SparseSimulator: n=1000, time_elapsed=0.032048\n",
"SparseSimulator: n=10000, time_elapsed=0.427906\n",
"SparseSimulator: n=50000, time_elapsed=4.819097\n"
]
}
],
"source": [
"from collections.abc import Iterator, Sequence\n",
"\n",
"import cirq\n",
"import random\n",
"import time\n",
"\n",
"from sparse_sim import SparseSimulator\n",
"\n",
"\n",
"def maj(a: cirq.Qid, b: cirq.Qid, c: cirq.Qid) -> Iterator[cirq.Operation]:\n",
" yield cirq.CNOT(c, a)\n",
" yield cirq.CNOT(c, b)\n",
" yield cirq.CCNOT(a, b, c)\n",
"\n",
"\n",
"def uma(a: cirq.Qid, b: cirq.Qid, c: cirq.Qid) -> Iterator[cirq.Operation]:\n",
" yield cirq.CCNOT(a, b, c)\n",
" yield cirq.CNOT(c, a)\n",
" yield cirq.CNOT(a, b)\n",
"\n",
"\n",
"class CuccaroAdder(cirq.Gate):\n",
" \"\"\"Adds two n-bit unsigned integers using the Cuccaro ripple-carry circuit.\n",
"\n",
" With little-endian registers, computes\n",
" (a, b, z) -> (a, (a + b) mod 2**n, z XOR floor((a + b) / 2**n)).\n",
"\n",
" Reference: Cuccaro, Draper, Kutin, and Moulton, \"A new quantum\n",
" ripple-carry addition circuit\" (2004), https://arxiv.org/abs/quant-ph/0410184\n",
" \"\"\"\n",
"\n",
" def __init__(self, n: int) -> None:\n",
" if n < 1:\n",
" raise ValueError(\"n must be positive\")\n",
" self.n = n\n",
"\n",
" @property\n",
" def input_sizes(self) -> tuple[int, int, int]:\n",
" # First addend, second addend, carry output.\n",
" return self.n, self.n, 1\n",
"\n",
" def _num_qubits_(self) -> int:\n",
" return 2 * self.n + 1\n",
"\n",
" def _decompose_with_context_(\n",
" self,\n",
" qubits: Sequence[cirq.Qid],\n",
" context: cirq.DecompositionContext,\n",
" ) -> Iterator[cirq.OP_TREE]:\n",
" a = qubits[: self.n]\n",
" b = qubits[self.n : 2 * self.n]\n",
" z = qubits[2 * self.n]\n",
" ancilla = context.qubit_manager.qalloc(1)\n",
" c = ancilla[0]\n",
"\n",
" try:\n",
" yield from maj(c, b[0], a[0])\n",
" for i in range(1, self.n):\n",
" yield from maj(a[i - 1], b[i], a[i])\n",
" yield cirq.CNOT(a[-1], z)\n",
" for i in range(self.n - 1, 0, -1):\n",
" yield from uma(a[i - 1], b[i], a[i])\n",
" yield from uma(c, b[0], a[0])\n",
" finally:\n",
" context.qubit_manager.qfree(ancilla)\n",
"\n",
"\n",
"\n",
"\n",
"rng = random.Random(0)\n",
"simulator = SparseSimulator()\n",
"#simulator = cirq.Simulator()\n",
"\n",
"def run_cuccaro_adder(n, sim): \n",
" a = cirq.LineQubit.range(n)\n",
" b = cirq.LineQubit.range(n, 2 * n)\n",
" z = cirq.LineQubit(2 * n)\n",
" qubits = (*a, *b, z)\n",
" adder = CuccaroAdder(n)\n",
"\n",
" a_value = rng.randint(0, 2**n-1)\n",
" b_value = rng.randint(0, 2**n-1)\n",
" expected_sum = a_value + b_value\n",
"\n",
" circuit = cirq.Circuit(\n",
" cirq.X(qubit)\n",
" for register_value, register in ((a_value, a), (b_value, b))\n",
" for bit, qubit in enumerate(register)\n",
" if (register_value >> bit) & 1\n",
" )\n",
" circuit.append(adder.on(*qubits))\n",
" circuit.append(\n",
" [\n",
" cirq.measure(*a, key=\"a\"),\n",
" cirq.measure(*b, key=\"sum\"),\n",
" cirq.measure(z, key=\"carry_out\"),\n",
" ]\n",
" )\n",
"\n",
" t0 = time.perf_counter()\n",
" result = sim.run(circuit, repetitions=1)\n",
" time_elapsed = time.perf_counter()-t0\n",
" sim_name = sim.__class__.__name__\n",
" print(f\"{sim_name}: {n=}, {time_elapsed=:.6f}\")\n",
" \n",
" measured_a = sum(int(bit) << i for i, bit in enumerate(result.measurements[\"a\"][0]))\n",
" measured_sum = sum(int(bit) << i for i, bit in enumerate(result.measurements[\"sum\"][0]))\n",
" measured_carry = int(result.measurements[\"carry_out\"][0, 0])\n",
" assert measured_a == a_value\n",
" assert measured_sum == expected_sum % 2**n\n",
" assert measured_carry == expected_sum // 2**n\n",
"\n",
"for n in [3,4,5]:\n",
" run_cuccaro_adder(n, cirq.Simulator())\n",
"\n",
"for n in [10, 100, 10**3, 10**4, 5 * 10**4]:\n",
" run_cuccaro_adder(n, SparseSimulator())"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "f13535e7-7bee-431e-a905-d35004c5fc39",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Simulator: n=4, time_elapsed=0.001794\n",
"SparseSimulator: n=4, time_elapsed=0.002759\n",
"Simulator: n=6, time_elapsed=0.002388\n",
"SparseSimulator: n=6, time_elapsed=0.002481\n",
"Simulator: n=8, time_elapsed=0.014807\n",
"SparseSimulator: n=8, time_elapsed=0.006620\n",
"Simulator: n=10, time_elapsed=0.030729\n",
"SparseSimulator: n=10, time_elapsed=0.031616\n",
"Simulator: n=12, time_elapsed=0.139986\n",
"SparseSimulator: n=12, time_elapsed=0.112313\n",
"Simulator: n=14, time_elapsed=0.497365\n",
"SparseSimulator: n=14, time_elapsed=0.507166\n",
"Simulator: n=16, time_elapsed=2.030843\n",
"SparseSimulator: n=16, time_elapsed=2.101261\n",
"Simulator: n=17, time_elapsed=6.624044\n",
"SparseSimulator: n=17, time_elapsed=6.571864\n"
]
}
],
"source": [
"def build_qft_circuit(n, input_value):\n",
" qubits = cirq.LineQubit.range(n)\n",
" circuit = cirq.Circuit(\n",
" cirq.X(qubit)\n",
" for bit, qubit in enumerate(qubits)\n",
" if (input_value >> bit) & 1\n",
" )\n",
" circuit.append(cirq.qft(*qubits))\n",
" return cirq.Circuit(cirq.decompose(circuit)), qubits\n",
"\n",
"\n",
"def benchmark_qft(n, sim):\n",
" input_value = rng.randint(0, 2**n-1)\n",
" circuit, qubits = build_qft_circuit(n, input_value)\n",
" start = time.perf_counter()\n",
" result = simulator.simulate(circuit, qubit_order=qubits)\n",
" time_elapsed = time.perf_counter() - start\n",
"\n",
" sim_name = sim.__class__.__name__\n",
" print(f\"{sim_name}: {n=}, {time_elapsed=:.6f}\")\n",
" return result.final_state_vector\n",
"\n",
"for n in [4, 6, 8, 10, 12, 14, 16, 17]:\n",
" benchmark_qft(n, cirq.Simulator())\n",
" benchmark_qft(n, SparseSimulator())\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
64 changes: 64 additions & 0 deletions cirq_sparse_sim/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Sparse simulator for Cirq

This is a simple implementation of a Sparse simulator for circuits in Cirq.

It is sparse in a sense that it maintains a list of basis states with only
non-zero amplitudes.

It supports unitary gates, computational basis measurements, and noisy channels,
and is designed primarily for simulating arithmetic circuits. Mixture and Kraus
channels are simulated as stochastic pure-state trajectories.

I was not able to find such a simulator in Cirq (maybe I didn't look had
enough), so I just wrote this one for my needs.

AI was used while writing this simulator.

## Usage

```python
import cirq

from cirq_sparse_sim.sparse_sim import SparseSimulator

simulator = SparseSimulator(seed=42)
q0, q1 = cirq.NamedQubit.range(2, prefix="q")
circuit = cirq.Circuit(cirq.H(q0), cirq.CNOT(q0, q1), cirq.measure(q0, q1, key="m"))

samples = simulator.run(circuit, repetitions=100).measurements["m"] # (100, 2)
trial = simulator.simulate(circuit)
wavefunction = trial.final_state_vector
steps = list(simulator.simulate_moment_steps(circuit))
```

The simulator implements `cirq.SimulatorBase` with sparse simulation states and
per-moment result snapshots. Each repetition starts in the all-zero state and
executes the same sparse gate and measurement logic. The simulator's
`basis_states`, `amplitudes`, `measurement_results`, and `read_register` expose
the last shot. Accessing a result's state vector explicitly materializes a dense
array; simulation itself remains sparse. Step-result sampling does not collapse
or otherwise change the saved state.

Circuits may use arbitrary dimension-2 Cirq Qids; `qubit_manager` remains
available for backwards compatibility and decomposition ancillas. Integer
initial states and returned wavefunctions use Cirq's big-endian `qubit_order`.
`read_register` is little-endian in the supplied register order, while sparse
basis-state bit positions use the simulation's compact internal qubit mapping.
Parameter resolvers and repeated measurement-key records follow Cirq's normal
contracts. Non-integer initial states are not supported. For backwards
compatibility, `run` accepts circuits without measurements.

## Tests

From the repository root, with Cirq, NumPy, SymPy, and pytest installed:

```sh
python -m pytest cirq_sparse_sim/sparse_sim_test.py
```

The tests cover measurement results and final states, seeded random circuits
against Cirq's dense and Clifford simulators, independent repetitions, moment
steps and state snapshots, arithmetic gates, classical controls, reset,
decomposition, qubit management, and error cases. Dense state
comparisons retain global phase; Clifford comparisons allow an overall global
phase because stabilizer representations need not preserve it.
Empty file added cirq_sparse_sim/__init__.py
Empty file.
Loading
Loading