Skill Tracer
Skills › Scientific Agent Skills › cirq › experiments.md

experiments.md

215 lines · skills/cirq/references/experiments.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Running quantum experiments
2
3These examples target Cirq 1.7.0 and small local circuits. Keep circuit generation,
4execution, and analysis separate. Record the question, observable, units, qubit
5order, seed, shot count, package version, parameters, and calibration provenance
6before interpreting an optimizer or benchmark result.
7

Running quantum experiments

These examples target Cirq 1.7.0 and small local circuits. Keep circuit generation, execution, and analysis separate. Record the question, observable, units, qubit order, seed, shot count, package version, parameters, and calibration provenance before interpreting an optimizer or benchmark result.

Raw lines 1 to 7
8## Sweeps and reproducible data collection
9
10```python
11import cirq
12import numpy as np
13import sympy
14import scipy.optimize
15import networkx as nx
17q0, q1 = qubits = list(cirq.LineQubit.range(2))
18theta, phi = sympy.symbols("theta phi")
19circuit = cirq.Circuit(
20 cirq.ry(theta)(q0), cirq.rx(phi)(q1), cirq.CNOT(q0, q1),
21 cirq.measure(*qubits, key="result"),
23sweep = cirq.Product(cirq.Linspace("theta", 0, np.pi, 3),
24 cirq.Linspace("phi", 0, np.pi, 3))
25sampler = cirq.Simulator(seed=42)
26results = sampler.run_sweep(circuit, params=sweep, repetitions=100)
27# Cirq JSON preserves result measurement arrays and ParamResolvers.
28archive = cirq.to_json({"circuit": circuit, "results": results,
29 "qubit_order": qubits, "cirq_version": cirq.__version__})
30restored = cirq.read_json(json_text=archive)
31assert len(restored["results"]) == 9
32```
34Write the archive to a chosen output path after the experiment; retain raw data
35alongside summaries. CSV cells containing a printed Counter are not a robust
36lossless result format. For shot estimates include binomial/multinomial sampling
37uncertainty and independent repeats when drift or optimizer noise matters.

Sweeps and reproducible data collection

import cirq
import numpy as np
import sympy
import scipy.optimize
import networkx as nx

q0, q1 = qubits = list(cirq.LineQubit.range(2))
theta, phi = sympy.symbols("theta phi")
circuit = cirq.Circuit(
    cirq.ry(theta)(q0), cirq.rx(phi)(q1), cirq.CNOT(q0, q1),
    cirq.measure(*qubits, key="result"),
)
sweep = cirq.Product(cirq.Linspace("theta", 0, np.pi, 3),
                     cirq.Linspace("phi", 0, np.pi, 3))
sampler = cirq.Simulator(seed=42)
results = sampler.run_sweep(circuit, params=sweep, repetitions=100)
# Cirq JSON preserves result measurement arrays and ParamResolvers.
archive = cirq.to_json({"circuit": circuit, "results": results,
                        "qubit_order": qubits, "cirq_version": cirq.__version__})
restored = cirq.read_json(json_text=archive)
assert len(restored["results"]) == 9

Write the archive to a chosen output path after the experiment; retain raw data alongside summaries. CSV cells containing a printed Counter are not a robust lossless result format. For shot estimates include binomial/multinomial sampling uncertainty and independent repeats when drift or optimizer noise matters.

Raw lines 8 to 38
39## VQE with explicit observable ordering
41A local state-vector expectation is exact up to numerical precision and does not
42include finite-shot or hardware error. This is a **toy Pauli Hamiltonian**, not
43an H2 electronic-structure calculation or a UCC chemistry ansatz.
45```python
46def toy_ansatz(params, qubits):
47 return cirq.Circuit(cirq.ry(params[0])(qubits[0]),
48 cirq.ry(params[1])(qubits[1]),
49 cirq.CNOT(qubits[0], qubits[1]))
51def vqe_experiment(hamiltonian, ansatz_func, initial_params, qubit_order):
52 simulator = cirq.Simulator(dtype=np.complex128)
53 history = []
54 def objective(params):
55 preparation = ansatz_func(params)
56 if preparation.has_measurements():
57 raise ValueError("VQE expectation requires a measurement-free ansatz")
58 energy = simulator.simulate_expectation_values(
59 preparation, observables=[hamiltonian], qubit_order=qubit_order
60 )[0]
61 energy = float(np.real(energy))
62 history.append({"params": np.array(params, copy=True), "energy": energy})
63 return energy
64 optimum = scipy.optimize.minimize(
65 objective, initial_params, method="COBYLA", options={"maxiter": 100}
66 )
67 return optimum, history
69hamiltonian = cirq.Z(q0) + cirq.Z(q1) + cirq.Z(q0)*cirq.Z(q1)
70optimum, history = vqe_experiment(
71 hamiltonian, lambda params: toy_ansatz(params, qubits), [0.2, 0.3], qubits
73exact_ground = np.linalg.eigvalsh(hamiltonian.matrix(qubits=qubits)).min()
74assert optimum.fun >= exact_ground - 1e-8
75print(optimum.fun, optimum.success, optimum.message)
76```
78Use the optimizer's `x` and `fun`, not the last history entry, as its returned
79optimum. Inspect convergence and multiple starts. For a manually evaluated
80`expectation_from_state_vector`, construct `qubit_map` from exactly the
81`qubit_order` passed to simulation; `circuit.all_qubits()` is a set. The variational
82bound only holds for a valid state and the intended Hamiltonian, not arbitrary
83mitigated estimates or an incorrect qubit map.

VQE with explicit observable ordering

A local state-vector expectation is exact up to numerical precision and does not include finite-shot or hardware error. This is a toy Pauli Hamiltonian, not an H2 electronic-structure calculation or a UCC chemistry ansatz.

def toy_ansatz(params, qubits):
    return cirq.Circuit(cirq.ry(params[0])(qubits[0]),
                        cirq.ry(params[1])(qubits[1]),
                        cirq.CNOT(qubits[0], qubits[1]))

def vqe_experiment(hamiltonian, ansatz_func, initial_params, qubit_order):
    simulator = cirq.Simulator(dtype=np.complex128)
    history = []
    def objective(params):
        preparation = ansatz_func(params)
        if preparation.has_measurements():
            raise ValueError("VQE expectation requires a measurement-free ansatz")
        energy = simulator.simulate_expectation_values(
            preparation, observables=[hamiltonian], qubit_order=qubit_order
        )[0]
        energy = float(np.real(energy))
        history.append({"params": np.array(params, copy=True), "energy": energy})
        return energy
    optimum = scipy.optimize.minimize(
        objective, initial_params, method="COBYLA", options={"maxiter": 100}
    )
    return optimum, history

hamiltonian = cirq.Z(q0) + cirq.Z(q1) + cirq.Z(q0)*cirq.Z(q1)
optimum, history = vqe_experiment(
    hamiltonian, lambda params: toy_ansatz(params, qubits), [0.2, 0.3], qubits
)
exact_ground = np.linalg.eigvalsh(hamiltonian.matrix(qubits=qubits)).min()
assert optimum.fun >= exact_ground - 1e-8
print(optimum.fun, optimum.success, optimum.message)

Use the optimizer's x and fun, not the last history entry, as its returned optimum. Inspect convergence and multiple starts. For a manually evaluated expectation_from_state_vector, construct qubit_map from exactly the qubit_order passed to simulation; circuit.all_qubits() is a set. The variational bound only holds for a valid state and the intended Hamiltonian, not arbitrary mitigated estimates or an incorrect qubit map.

Raw lines 39 to 84
85## QAOA for unweighted MaxCut
87Use `C = sum_edges (I-Z_i Z_j)/2` and the convention
88`exp(-i gamma C)` followed by `exp(-i beta sum X)`, with angles in **radians**.
89Cirq's `ZZPowGate` exponent is a half-turn parameter, so its exponent here is
90`-gamma/pi` (up to global phase). Record this sign/unit convention with results.
92```python
93def qaoa_circuit(graph, params, p_layers, *, measure=True):
94 if graph.is_directed() or graph.is_multigraph() or nx.number_of_selfloops(graph):
95 raise ValueError("Expected a simple undirected graph without self-loops")
96 if p_layers < 1 or len(params) != 2*p_layers:
97 raise ValueError("Expected gamma then beta for each layer")
98 nodes = list(graph.nodes())
99 qubits = list(cirq.LineQubit.range(len(nodes)))
100 node_qubit = dict(zip(nodes, qubits))
101 circuit = cirq.Circuit(cirq.H.on_each(*qubits))
102 for layer in range(p_layers):
103 gamma, beta = params[layer], params[p_layers+layer]
104 circuit.append(cirq.ZZPowGate(exponent=-gamma/np.pi)(node_qubit[u], node_qubit[v])
105 for u, v in graph.edges())
106 circuit.append(cirq.rx(2*beta).on_each(*qubits))
107 if measure:
108 circuit.append(cirq.measure(*qubits, key="result"))
109 return circuit, nodes, qubits
111def mean_maxcut(result, graph, nodes):
112 columns = {node: i for i, node in enumerate(nodes)}
113 bits = result.measurements["result"]
114 if bits.shape[1] != len(nodes) or len(bits) == 0:
115 raise ValueError("Measurement dimensions do not match the graph")
116 cut_per_shot = np.zeros(len(bits))
117 for u, v in graph.edges():
118 cut_per_shot += bits[:, columns[u]] != bits[:, columns[v]]
119 return float(np.mean(cut_per_shot))
121graph = nx.Graph([("left", "center"), ("center", "right")])
122qaoa, nodes, order = qaoa_circuit(graph, [0.4, 0.2], p_layers=1)
123qaoa_result = cirq.Simulator(seed=42).run(qaoa, repetitions=1000)
124cut = mean_maxcut(qaoa_result, graph, nodes)
125assert 0 <= cut <= graph.number_of_edges()
126```
128Raw measurement columns avoid accidentally reversing big-endian histogram bits.
129Node labels need not be consecutive integers. The example treats every edge as
130weight 1; implement both weighted cost evolution and weighted scoring if weights
131are scientifically relevant. For optimization minimize `-mean_maxcut(...)` and
132validate the chosen parameters on independent shots. A noisy optimizer result
133alone does not establish superiority over a classical baseline.

QAOA for unweighted MaxCut

Use C = sum_edges (I-Z_i Z_j)/2 and the convention exp(-i gamma C) followed by exp(-i beta sum X), with angles in radians. Cirq's ZZPowGate exponent is a half-turn parameter, so its exponent here is -gamma/pi (up to global phase). Record this sign/unit convention with results.

def qaoa_circuit(graph, params, p_layers, *, measure=True):
    if graph.is_directed() or graph.is_multigraph() or nx.number_of_selfloops(graph):
        raise ValueError("Expected a simple undirected graph without self-loops")
    if p_layers < 1 or len(params) != 2*p_layers:
        raise ValueError("Expected gamma then beta for each layer")
    nodes = list(graph.nodes())
    qubits = list(cirq.LineQubit.range(len(nodes)))
    node_qubit = dict(zip(nodes, qubits))
    circuit = cirq.Circuit(cirq.H.on_each(*qubits))
    for layer in range(p_layers):
        gamma, beta = params[layer], params[p_layers+layer]
        circuit.append(cirq.ZZPowGate(exponent=-gamma/np.pi)(node_qubit[u], node_qubit[v])
                       for u, v in graph.edges())
        circuit.append(cirq.rx(2*beta).on_each(*qubits))
    if measure:
        circuit.append(cirq.measure(*qubits, key="result"))
    return circuit, nodes, qubits

def mean_maxcut(result, graph, nodes):
    columns = {node: i for i, node in enumerate(nodes)}
    bits = result.measurements["result"]
    if bits.shape[1] != len(nodes) or len(bits) == 0:
        raise ValueError("Measurement dimensions do not match the graph")
    cut_per_shot = np.zeros(len(bits))
    for u, v in graph.edges():
        cut_per_shot += bits[:, columns[u]] != bits[:, columns[v]]
    return float(np.mean(cut_per_shot))

graph = nx.Graph([("left", "center"), ("center", "right")])
qaoa, nodes, order = qaoa_circuit(graph, [0.4, 0.2], p_layers=1)
qaoa_result = cirq.Simulator(seed=42).run(qaoa, repetitions=1000)
cut = mean_maxcut(qaoa_result, graph, nodes)
assert 0 <= cut <= graph.number_of_edges()

Raw measurement columns avoid accidentally reversing big-endian histogram bits. Node labels need not be consecutive integers. The example treats every edge as weight 1; implement both weighted cost evolution and weighted scoring if weights are scientifically relevant. For optimization minimize -mean_maxcut(...) and validate the chosen parameters on independent shots. A noisy optimizer result alone does not establish superiority over a classical baseline.

Raw lines 85 to 134
135## Quantum phase estimation
137This example accepts a one-qubit unitary gate and a one-qubit eigenstate
138preparation. Counting qubits are ordered most-significant first; the measured
139integer k estimates the eigenphase as `k / 2**n` for eigenvalue `exp(2*pi*i*phase)`.
141```python
142def qpe_circuit(unitary, eigenstate_prep, n_counting_qubits):
143 if n_counting_qubits < 1 or cirq.num_qubits(unitary) != 1 or not cirq.has_unitary(unitary):
144 raise ValueError("Expected a one-qubit unitary and positive counting width")
145 counting = list(cirq.LineQubit.range(n_counting_qubits))
146 target = cirq.LineQubit(n_counting_qubits)
147 circuit = cirq.Circuit(eigenstate_prep(target), cirq.H.on_each(*counting))
148 for i, control in enumerate(counting):
149 power = 2**(n_counting_qubits-1-i)
150 circuit.append((unitary**power).on(target).controlled_by(control))
151 circuit.append(cirq.qft(*counting, inverse=True))
152 circuit.append(cirq.measure(*counting, key="phase"))
153 return circuit
155phase_circuit = qpe_circuit(cirq.ZPowGate(exponent=0.75), cirq.X, 3)
156phase_result = cirq.Simulator(seed=42).run(phase_circuit, repetitions=100)
157assert phase_result.histogram(key="phase") == {3: 100} # phase = 3/8
158```
160If the input is a superposition of eigenstates, outcomes follow their weights;
161nonrepresentable phases spread across nearby bins. Test these cases before using
162phase estimation on a scientific Hamiltonian. The short example does not supply
163Hamiltonian simulation or arbitrary multi-qubit controlled powers.

Quantum phase estimation

This example accepts a one-qubit unitary gate and a one-qubit eigenstate preparation. Counting qubits are ordered most-significant first; the measured integer k estimates the eigenphase as k / 2**n for eigenvalue exp(2*pi*i*phase).

def qpe_circuit(unitary, eigenstate_prep, n_counting_qubits):
    if n_counting_qubits < 1 or cirq.num_qubits(unitary) != 1 or not cirq.has_unitary(unitary):
        raise ValueError("Expected a one-qubit unitary and positive counting width")
    counting = list(cirq.LineQubit.range(n_counting_qubits))
    target = cirq.LineQubit(n_counting_qubits)
    circuit = cirq.Circuit(eigenstate_prep(target), cirq.H.on_each(*counting))
    for i, control in enumerate(counting):
        power = 2**(n_counting_qubits-1-i)
        circuit.append((unitary**power).on(target).controlled_by(control))
    circuit.append(cirq.qft(*counting, inverse=True))
    circuit.append(cirq.measure(*counting, key="phase"))
    return circuit

phase_circuit = qpe_circuit(cirq.ZPowGate(exponent=0.75), cirq.X, 3)
phase_result = cirq.Simulator(seed=42).run(phase_circuit, repetitions=100)
assert phase_result.histogram(key="phase") == {3: 100}  # phase = 3/8

If the input is a superposition of eigenstates, outcomes follow their weights; nonrepresentable phases spread across nearby bins. Test these cases before using phase estimation on a scientific Hamiltonian. The short example does not supply Hamiltonian simulation or arbitrary multi-qubit controlled powers.

Raw lines 135 to 164
165## Expectation values and fidelity
167```python
168def calculate_expectation_value(circuit, observable, simulator, qubit_order):
169 if not circuit.are_all_measurements_terminal():
170 raise ValueError("Cannot remove mid-circuit measurements for this observable")
171 preparation = cirq.drop_terminal_measurements(circuit)
172 return float(np.real(simulator.simulate_expectation_values(
173 preparation, observables=[observable], qubit_order=qubit_order
174 )[0]))
176def classical_distribution_fidelity(counts1, counts2):
177 total1, total2 = sum(counts1.values()), sum(counts2.values())
178 if total1 <= 0 or total2 <= 0:
179 raise ValueError("Expected nonempty histograms")
180 states = set(counts1) | set(counts2)
181 return float(sum(np.sqrt(counts1.get(s, 0)/total1 * counts2.get(s, 0)/total2)
182 for s in states)**2)
184bell_plus = np.array([1, 0, 0, 1]) / np.sqrt(2)
185bell_minus = np.array([1, 0, 0, -1]) / np.sqrt(2)
186assert np.isclose(cirq.fidelity(bell_plus, bell_minus), 0)
187assert classical_distribution_fidelity({0: 50, 3: 50}, {0: 50, 3: 50}) == 1
188```
190Identical measurement histograms in one basis do not establish state fidelity or
191process fidelity. Quantum state/process estimation needs sufficient measurement
192settings and assumptions. The example above has orthogonal states but identical
193computational-basis histograms. Plot convergence with evaluation index, and
194landscapes with axes matching the parameter-array shape and units.

Expectation values and fidelity

def calculate_expectation_value(circuit, observable, simulator, qubit_order):
    if not circuit.are_all_measurements_terminal():
        raise ValueError("Cannot remove mid-circuit measurements for this observable")
    preparation = cirq.drop_terminal_measurements(circuit)
    return float(np.real(simulator.simulate_expectation_values(
        preparation, observables=[observable], qubit_order=qubit_order
    )[0]))

def classical_distribution_fidelity(counts1, counts2):
    total1, total2 = sum(counts1.values()), sum(counts2.values())
    if total1 <= 0 or total2 <= 0:
        raise ValueError("Expected nonempty histograms")
    states = set(counts1) | set(counts2)
    return float(sum(np.sqrt(counts1.get(s, 0)/total1 * counts2.get(s, 0)/total2)
                     for s in states)**2)

bell_plus = np.array([1, 0, 0, 1]) / np.sqrt(2)
bell_minus = np.array([1, 0, 0, -1]) / np.sqrt(2)
assert np.isclose(cirq.fidelity(bell_plus, bell_minus), 0)
assert classical_distribution_fidelity({0: 50, 3: 50}, {0: 50, 3: 50}) == 1

Identical measurement histograms in one basis do not establish state fidelity or process fidelity. Quantum state/process estimation needs sufficient measurement settings and assumptions. The example above has orthogonal states but identical computational-basis histograms. Plot convergence with evaluation index, and landscapes with axes matching the parameter-array shape and units.

Raw lines 165 to 195
196## ReCirq and parallel execution
198[ReCirq](https://github.com/quantumlib/ReCirq) is a collection of research
199applications/experiments, not a universal base class with a mandated project
200layout. Pick the relevant upstream experiment and inspect its own dependencies
201and data schemas; no ReCirq package environment was executed in this review.
203A useful project layout separates task descriptions (immutable parameters and
204seeds), circuit generation, execution, persisted results, and analysis. On macOS
205or other spawn-based multiprocessing environments, put worker functions at module
206scope and call pools under `if __name__ == "__main__":`. Give each worker its own
207simulator and distinct reproducible seed; do not copy a seeded sampler into every
208worker and count duplicated random streams as independent experiments. Hardware
209clients require provider-specific concurrency limits and durable job tracking.
211Sources: [parameter sweeps](https://quantumai.google/cirq/simulate/params),
212[expectation values](https://quantumai.google/reference/python/cirq/Simulator),
213[QFT](https://quantumai.google/reference/python/cirq/qft),
214[quantum fidelity](https://quantumai.google/reference/python/cirq/fidelity),
215[ReCirq repository](https://github.com/quantumlib/ReCirq).

ReCirq and parallel execution

ReCirq is a collection of research applications/experiments, not a universal base class with a mandated project layout. Pick the relevant upstream experiment and inspect its own dependencies and data schemas; no ReCirq package environment was executed in this review.

A useful project layout separates task descriptions (immutable parameters and seeds), circuit generation, execution, persisted results, and analysis. On macOS or other spawn-based multiprocessing environments, put worker functions at module scope and call pools under if __name__ == "__main__":. Give each worker its own simulator and distinct reproducible seed; do not copy a seeded sampler into every worker and count duplicated random streams as independent experiments. Hardware clients require provider-specific concurrency limits and durable job tracking.

Sources: parameter sweeps, expectation values, QFT, quantum fidelity, ReCirq repository.

Raw lines 196 to 215
Starts an agent spawn :205