Files
claude-scientific-skills/scientific-skills/cirq/references/experiments.md

573 lines
15 KiB
Markdown

# Running Quantum Experiments
This guide covers designing and executing quantum experiments, including parameter sweeps, data collection, and using the ReCirq framework.
## Experiment Design
### Basic Experiment Structure
```python
import cirq
import numpy as np
import pandas as pd
class QuantumExperiment:
"""Base class for quantum experiments."""
def __init__(self, qubits, simulator=None):
self.qubits = qubits
self.simulator = simulator or cirq.Simulator()
self.results = []
def build_circuit(self, **params):
"""Build circuit with given parameters."""
raise NotImplementedError
def run(self, params_list, repetitions=1000):
"""Run experiment with parameter sweep."""
for params in params_list:
circuit = self.build_circuit(**params)
result = self.simulator.run(circuit, repetitions=repetitions)
self.results.append({
'params': params,
'result': result
})
return self.results
def analyze(self):
"""Analyze experimental results."""
raise NotImplementedError
```
### Parameter Sweeps
```python
import sympy
# Define parameters
theta = sympy.Symbol('theta')
phi = sympy.Symbol('phi')
# Create parameterized circuit
def parameterized_circuit(qubits, theta, phi):
return cirq.Circuit(
cirq.ry(theta)(qubits[0]),
cirq.rz(phi)(qubits[1]),
cirq.CNOT(qubits[0], qubits[1]),
cirq.measure(*qubits, key='result')
)
# Define sweep
sweep = cirq.Product(
cirq.Linspace('theta', 0, np.pi, 20),
cirq.Linspace('phi', 0, 2*np.pi, 20)
)
# Run sweep
circuit = parameterized_circuit(cirq.LineQubit.range(2), theta, phi)
results = cirq.Simulator().run_sweep(circuit, params=sweep, repetitions=1000)
```
### Data Collection
```python
def collect_experiment_data(circuit, sweep, simulator, repetitions=1000):
"""Collect and organize experimental data."""
data = []
results = simulator.run_sweep(circuit, params=sweep, repetitions=repetitions)
for params, result in zip(sweep, results):
# Extract parameters
param_dict = {k: v for k, v in params.param_dict.items()}
# Extract measurements
counts = result.histogram(key='result')
# Store in structured format
data.append({
**param_dict,
'counts': counts,
'total': repetitions
})
return pd.DataFrame(data)
# Collect data
df = collect_experiment_data(circuit, sweep, cirq.Simulator())
# Save to file
df.to_csv('experiment_results.csv', index=False)
```
## ReCirq Framework
ReCirq provides a structured framework for reproducible quantum experiments.
### ReCirq Experiment Structure
```python
"""
Standard ReCirq experiment structure:
experiment_name/
├── __init__.py
├── experiment.py # Main experiment code
├── tasks.py # Data generation tasks
├── data_collection.py # Parallel data collection
├── analysis.py # Data analysis
└── plots.py # Visualization
"""
```
### Task-Based Data Collection
```python
from dataclasses import dataclass
from typing import List
import cirq
@dataclass
class ExperimentTask:
"""Single task in parameter sweep."""
theta: float
phi: float
repetitions: int = 1000
def build_circuit(self, qubits):
"""Build circuit for this task."""
return cirq.Circuit(
cirq.ry(self.theta)(qubits[0]),
cirq.rz(self.phi)(qubits[1]),
cirq.CNOT(qubits[0], qubits[1]),
cirq.measure(*qubits, key='result')
)
def run(self, qubits, simulator):
"""Execute task."""
circuit = self.build_circuit(qubits)
result = simulator.run(circuit, repetitions=self.repetitions)
return {
'theta': self.theta,
'phi': self.phi,
'result': result
}
# Create tasks
tasks = [
ExperimentTask(theta=t, phi=p)
for t in np.linspace(0, np.pi, 10)
for p in np.linspace(0, 2*np.pi, 10)
]
# Execute tasks
qubits = cirq.LineQubit.range(2)
simulator = cirq.Simulator()
results = [task.run(qubits, simulator) for task in tasks]
```
### Parallel Data Collection
```python
from multiprocessing import Pool
import functools
def run_task_parallel(task, qubits, simulator):
"""Run single task (for parallel execution)."""
return task.run(qubits, simulator)
def collect_data_parallel(tasks, qubits, simulator, n_workers=4):
"""Collect data using parallel processing."""
# Create partial function with fixed arguments
run_func = functools.partial(
run_task_parallel,
qubits=qubits,
simulator=simulator
)
# Run in parallel
with Pool(n_workers) as pool:
results = pool.map(run_func, tasks)
return results
# Use parallel collection
results = collect_data_parallel(tasks, qubits, cirq.Simulator(), n_workers=8)
```
## Common Quantum Algorithms
### Variational Quantum Eigensolver (VQE)
```python
import scipy.optimize
def vqe_experiment(hamiltonian, ansatz_func, initial_params):
"""Run VQE to find ground state energy."""
def cost_function(params):
"""Energy expectation value."""
circuit = ansatz_func(params)
# Measure expectation value of Hamiltonian
simulator = cirq.Simulator()
result = simulator.simulate(circuit)
energy = hamiltonian.expectation_from_state_vector(
result.final_state_vector,
qubit_map={q: i for i, q in enumerate(circuit.all_qubits())}
)
return energy.real
# Optimize parameters
result = scipy.optimize.minimize(
cost_function,
initial_params,
method='COBYLA'
)
return result
# Example: H2 molecule
def h2_ansatz(params, qubits):
"""UCC ansatz for H2."""
theta = params[0]
return cirq.Circuit(
cirq.X(qubits[1]),
cirq.ry(theta)(qubits[0]),
cirq.CNOT(qubits[0], qubits[1])
)
# Define Hamiltonian (simplified)
qubits = cirq.LineQubit.range(2)
hamiltonian = cirq.PauliSum.from_pauli_strings([
cirq.PauliString({qubits[0]: cirq.Z}),
cirq.PauliString({qubits[1]: cirq.Z}),
cirq.PauliString({qubits[0]: cirq.Z, qubits[1]: cirq.Z})
])
# Run VQE
result = vqe_experiment(
hamiltonian,
lambda p: h2_ansatz(p, qubits),
initial_params=[0.0]
)
print(f"Ground state energy: {result.fun}")
print(f"Optimal parameters: {result.x}")
```
### Quantum Approximate Optimization Algorithm (QAOA)
```python
def qaoa_circuit(graph, params, p_layers):
"""QAOA circuit for MaxCut problem."""
qubits = cirq.LineQubit.range(graph.number_of_nodes())
circuit = cirq.Circuit()
# Initial superposition
circuit.append(cirq.H(q) for q in qubits)
# QAOA layers
for layer in range(p_layers):
gamma = params[layer]
beta = params[p_layers + layer]
# Problem Hamiltonian (cost)
for edge in graph.edges():
i, j = edge
circuit.append(cirq.ZZPowGate(exponent=gamma)(qubits[i], qubits[j]))
# Mixer Hamiltonian
circuit.append(cirq.rx(2 * beta)(q) for q in qubits)
circuit.append(cirq.measure(*qubits, key='result'))
return circuit
# Run QAOA
import networkx as nx
graph = nx.cycle_graph(4)
p_layers = 2
def qaoa_cost(params):
"""Evaluate QAOA cost function."""
circuit = qaoa_circuit(graph, params, p_layers)
simulator = cirq.Simulator()
result = simulator.run(circuit, repetitions=1000)
# Calculate MaxCut objective
total_cost = 0
counts = result.histogram(key='result')
for bitstring, count in counts.items():
cost = 0
bits = [(bitstring >> i) & 1 for i in range(graph.number_of_nodes())]
for edge in graph.edges():
i, j = edge
if bits[i] != bits[j]:
cost += 1
total_cost += cost * count
return -total_cost / 1000 # Maximize cut
# Optimize
initial_params = np.random.random(2 * p_layers) * np.pi
result = scipy.optimize.minimize(qaoa_cost, initial_params, method='COBYLA')
print(f"Optimal cost: {-result.fun}")
print(f"Optimal parameters: {result.x}")
```
### Quantum Phase Estimation
```python
def qpe_circuit(unitary, eigenstate_prep, n_counting_qubits):
"""Quantum Phase Estimation circuit."""
counting_qubits = cirq.LineQubit.range(n_counting_qubits)
target_qubit = cirq.LineQubit(n_counting_qubits)
circuit = cirq.Circuit()
# Prepare eigenstate
circuit.append(eigenstate_prep(target_qubit))
# Apply Hadamard to counting qubits
circuit.append(cirq.H(q) for q in counting_qubits)
# Controlled unitaries
for i, q in enumerate(counting_qubits):
power = 2 ** (n_counting_qubits - 1 - i)
# Apply controlled-U^power
for _ in range(power):
circuit.append(cirq.ControlledGate(unitary)(q, target_qubit))
# Inverse QFT on counting qubits
circuit.append(inverse_qft(counting_qubits))
# Measure counting qubits
circuit.append(cirq.measure(*counting_qubits, key='phase'))
return circuit
def inverse_qft(qubits):
"""Inverse Quantum Fourier Transform."""
n = len(qubits)
ops = []
for i in range(n // 2):
ops.append(cirq.SWAP(qubits[i], qubits[n - i - 1]))
for i in range(n):
for j in range(i):
ops.append(cirq.CZPowGate(exponent=-1/2**(i-j))(qubits[j], qubits[i]))
ops.append(cirq.H(qubits[i]))
return ops
```
## Data Analysis
### Statistical Analysis
```python
def analyze_measurement_statistics(results):
"""Analyze measurement statistics."""
counts = results.histogram(key='result')
total = sum(counts.values())
# Calculate probabilities
probabilities = {state: count/total for state, count in counts.items()}
# Shannon entropy
entropy = -sum(p * np.log2(p) for p in probabilities.values() if p > 0)
# Most likely outcome
most_likely = max(counts.items(), key=lambda x: x[1])
return {
'probabilities': probabilities,
'entropy': entropy,
'most_likely_state': most_likely[0],
'most_likely_probability': most_likely[1] / total
}
```
### Expectation Value Calculation
```python
def calculate_expectation_value(circuit, observable, simulator):
"""Calculate expectation value of observable."""
# Remove measurements
circuit_no_measure = cirq.Circuit(
m for m in circuit if not isinstance(m, cirq.MeasurementGate)
)
result = simulator.simulate(circuit_no_measure)
state_vector = result.final_state_vector
# Calculate ⟨ψ|O|ψ⟩
expectation = observable.expectation_from_state_vector(
state_vector,
qubit_map={q: i for i, q in enumerate(circuit.all_qubits())}
)
return expectation.real
```
### Fidelity Estimation
```python
def state_fidelity(state1, state2):
"""Calculate fidelity between two states."""
return np.abs(np.vdot(state1, state2)) ** 2
def process_fidelity(result1, result2):
"""Calculate process fidelity from measurement results."""
counts1 = result1.histogram(key='result')
counts2 = result2.histogram(key='result')
# Normalize to probabilities
total1 = sum(counts1.values())
total2 = sum(counts2.values())
probs1 = {k: v/total1 for k, v in counts1.items()}
probs2 = {k: v/total2 for k, v in counts2.items()}
# Classical fidelity (Bhattacharyya coefficient)
all_states = set(probs1.keys()) | set(probs2.keys())
fidelity = sum(np.sqrt(probs1.get(s, 0) * probs2.get(s, 0))
for s in all_states) ** 2
return fidelity
```
## Visualization
### Plot Parameter Landscapes
```python
import matplotlib.pyplot as plt
def plot_parameter_landscape(theta_vals, phi_vals, energies):
"""Plot 2D parameter landscape."""
plt.figure(figsize=(10, 8))
plt.contourf(theta_vals, phi_vals, energies, levels=50, cmap='viridis')
plt.colorbar(label='Energy')
plt.xlabel('θ')
plt.ylabel('φ')
plt.title('Energy Landscape')
plt.show()
```
### Plot Convergence
```python
def plot_optimization_convergence(optimization_history):
"""Plot optimization convergence."""
iterations = range(len(optimization_history))
energies = [result['energy'] for result in optimization_history]
plt.figure(figsize=(10, 6))
plt.plot(iterations, energies, 'b-', linewidth=2)
plt.xlabel('Iteration')
plt.ylabel('Energy')
plt.title('Optimization Convergence')
plt.grid(True)
plt.show()
```
### Plot Measurement Distributions
```python
def plot_measurement_distribution(results):
"""Plot measurement outcome distribution."""
counts = results.histogram(key='result')
plt.figure(figsize=(12, 6))
plt.bar(counts.keys(), counts.values())
plt.xlabel('Measurement Outcome')
plt.ylabel('Counts')
plt.title('Measurement Distribution')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
```
## Best Practices
1. **Structure experiments clearly**: Use ReCirq patterns for reproducibility
2. **Separate tasks**: Divide data generation, collection, and analysis
3. **Use parameter sweeps**: Explore parameter space systematically
4. **Save intermediate results**: Don't lose expensive computation
5. **Parallelize when possible**: Use multiprocessing for independent tasks
6. **Track metadata**: Record experiment conditions, timestamps, versions
7. **Validate on simulators**: Test experimental code before hardware
8. **Implement error handling**: Robust code for long-running experiments
9. **Version control data**: Track experimental data alongside code
10. **Document thoroughly**: Clear documentation for reproducibility
## Example: Complete Experiment
```python
# Full experimental workflow
class VQEExperiment(QuantumExperiment):
"""Complete VQE experiment."""
def __init__(self, hamiltonian, ansatz, qubits):
super().__init__(qubits)
self.hamiltonian = hamiltonian
self.ansatz = ansatz
self.history = []
def build_circuit(self, params):
return self.ansatz(params, self.qubits)
def cost_function(self, params):
circuit = self.build_circuit(params)
result = self.simulator.simulate(circuit)
energy = self.hamiltonian.expectation_from_state_vector(
result.final_state_vector,
qubit_map={q: i for i, q in enumerate(self.qubits)}
)
self.history.append({'params': params, 'energy': energy.real})
return energy.real
def run(self, initial_params):
result = scipy.optimize.minimize(
self.cost_function,
initial_params,
method='COBYLA',
options={'maxiter': 100}
)
return result
def analyze(self):
# Plot convergence
energies = [h['energy'] for h in self.history]
plt.plot(energies)
plt.xlabel('Iteration')
plt.ylabel('Energy')
plt.title('VQE Convergence')
plt.show()
return {
'final_energy': self.history[-1]['energy'],
'optimal_params': self.history[-1]['params'],
'num_iterations': len(self.history)
}
# Run experiment
experiment = VQEExperiment(hamiltonian, h2_ansatz, qubits)
result = experiment.run(initial_params=[0.0])
analysis = experiment.analyze()
```