View on QuantumAI
|
Run in Google Colab
|
View source on GitHub
|
|
The qsim library provides a Python interface to Cirq in the qsimcirq PyPI package.
Setup
Install the Cirq and qsimcirq packages:
import time
try:
import cirq
except ImportError:
!pip install cirq --quiet
import cirq
try:
import qsimcirq
except ImportError:
!pip install qsimcirq --quiet
import qsimcirq
Simulating Cirq circuits with qsim is easy: just define the circuit as you normally would, then create a QSimSimulator to perform the simulation. This object implements Cirq's simulator.py interfaces, so you can drop it in anywhere the basic Cirq simulator is used.
Full state-vector simulation
qsim is optimized for computing the final state vector of a circuit. Try it by running the example below.
# Define qubits and a short circuit.
q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit(cirq.H(q0), cirq.CX(q0, q1))
print("Circuit:")
print(circuit)
print()
# Simulate the circuit with Cirq and return the full state vector.
print("Cirq results:")
cirq_simulator = cirq.Simulator()
cirq_results = cirq_simulator.simulate(circuit)
print(cirq_results)
print()
# Simulate the circuit with qsim and return the full state vector.
print("qsim results:")
qsim_simulator = qsimcirq.QSimSimulator()
qsim_results = qsim_simulator.simulate(circuit)
print(qsim_results)
Circuit:
0: ───H───@───
│
1: ───────X───
Cirq results:
measurements: (no measurements)
qubits: (cirq.LineQubit(0), cirq.LineQubit(1))
output vector: 0.707|00⟩ + 0.707|11⟩
phase:
output vector: |⟩
qsim results:
measurements: (no measurements)
qubits: (cirq.LineQubit(0), cirq.LineQubit(1))
output vector: 0.707|00⟩ + 0.707|11⟩
To sample from this state, you can invoke Cirq's sample_state_vector method:
samples = cirq.sample_state_vector(
qsim_results.state_vector(), indices=[0, 1], repetitions=10
)
print(samples)
[[0 0] [0 0] [1 1] [1 1] [1 1] [0 0] [1 1] [1 1] [0 0] [1 1]]
Measurement sampling
qsim also supports sampling from user-defined measurement gates.
# Define a circuit with measurements.
q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit(
cirq.H(q0),
cirq.X(q1),
cirq.CX(q0, q1),
cirq.measure(q0, key="qubit_0"),
cirq.measure(q1, key="qubit_1"),
)
print("Circuit:")
print(circuit)
print()
# Simulate the circuit with Cirq and return just the measurement values.
print("Cirq results:")
cirq_simulator = cirq.Simulator()
cirq_results = cirq_simulator.run(circuit, repetitions=5)
print(cirq_results)
print()
# Simulate the circuit with qsim and return just the measurement values.
print("qsim results:")
qsim_simulator = qsimcirq.QSimSimulator()
qsim_results = qsim_simulator.run(circuit, repetitions=5)
print(qsim_results)
Circuit:
0: ───H───@───M('qubit_0')───
│
1: ───X───X───M('qubit_1')───
Cirq results:
qubit_0=10001
qubit_1=01110
qsim results:
qubit_0=00011
qubit_1=11100
The warning above highlights an important distinction between the simulate and run methods:
simulateonly executes the circuit once.- Sampling from the resulting state is fast, but if there are intermediate measurements the final state vector depends on the results of those measurements.
runwill execute the circuit once for each repetition requested.- As a result, sampling is much slower, but intermediate measurements are re-sampled for each repetition. If there are no intermediate measurements,
runredirects tosimulatefor faster execution.
- As a result, sampling is much slower, but intermediate measurements are re-sampled for each repetition. If there are no intermediate measurements,
The warning goes away if intermediate measurements are present:
# Define a circuit with intermediate measurements.
q0 = cirq.LineQubit(0)
circuit = cirq.Circuit(
cirq.X(q0) ** 0.5,
cirq.measure(q0, key="m0"),
cirq.X(q0) ** 0.5,
cirq.measure(q0, key="m1"),
cirq.X(q0) ** 0.5,
cirq.measure(q0, key="m2"),
)
print("Circuit:")
print(circuit)
print()
# Simulate the circuit with qsim and return just the measurement values.
print("qsim results:")
qsim_simulator = qsimcirq.QSimSimulator()
qsim_results = qsim_simulator.run(circuit, repetitions=5)
print(qsim_results)
Circuit:
0: ───X^0.5───M('m0')───X^0.5───M('m1')───X^0.5───M('m2')───
qsim results:
m0=01010
m1=11101
m2=11000
Amplitude evaluation
qsim can also calculate amplitudes for specific output bitstrings.
# Define a simple circuit.
q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit(cirq.H(q0), cirq.CX(q0, q1))
print("Circuit:")
print(circuit)
print()
# Simulate the circuit with qsim and return the amplitudes for |00) and |01).
print("Cirq results:")
cirq_simulator = cirq.Simulator()
cirq_results = cirq_simulator.compute_amplitudes(circuit, bitstrings=[0b00, 0b01])
print(cirq_results)
print()
# Simulate the circuit with qsim and return the amplitudes for |00) and |01).
print("qsim results:")
qsim_simulator = qsimcirq.QSimSimulator()
qsim_results = qsim_simulator.compute_amplitudes(circuit, bitstrings=[0b00, 0b01])
print(qsim_results)
Circuit:
0: ───H───@───
│
1: ───────X───
Cirq results:
[(0.7071067690849304+0j), 0j]
qsim results:
[(0.7071067690849304+0j), 0j]
Performance benchmark
The code below generates a depth-16 circuit on a 4x5 qubit grid, then runs it against the basic Cirq simulator. For a circuit of this size, the difference in runtime can be significant - try it out!
# Get a rectangular grid of qubits.
qubits = cirq.GridQubit.rect(4, 5)
# Generates a random circuit on the provided qubits.
circuit = cirq.experiments.random_rotations_between_grid_interaction_layers_circuit(
qubits=qubits, depth=16
)
# Simulate the circuit with Cirq and print the runtime.
cirq_simulator = cirq.Simulator()
cirq_start = time.time()
cirq_results = cirq_simulator.simulate(circuit)
cirq_elapsed = time.time() - cirq_start
print(f"Cirq runtime: {cirq_elapsed} seconds.")
print()
# Simulate the circuit with qsim and print the runtime.
qsim_simulator = qsimcirq.QSimSimulator()
qsim_start = time.time()
qsim_results = qsim_simulator.simulate(circuit)
qsim_elapsed = time.time() - qsim_start
print(f"qsim runtime: {qsim_elapsed} seconds.")
Cirq runtime: 2.2518844604492188 seconds. qsim runtime: 0.12251019477844238 seconds.
qsim performance can be tuned further by passing options to the simulator constructor. These options use the same format as the qsim_base binary - a full description can be found in the qsim usage doc. The example below demonstrates enabling multithreading in qsim; for best performance, use the same number of threads as the number of cores (or virtual cores) on your machine.
# Use eight threads to parallelize simulation.
options = {"t": 8}
qsim_simulator = qsimcirq.QSimSimulator(options)
qsim_start = time.time()
qsim_results = qsim_simulator.simulate(circuit)
qsim_elapsed = time.time() - qsim_start
print(f"qsim runtime: {qsim_elapsed} seconds.")
qsim runtime: 0.09598278999328613 seconds.
Another option is to adjust the maximum number of qubits over which to fuse gates. Increasing this value (as demonstrated below) increases arithmetic intensity, which may improve performance with the right environment settings.
# Increase maximum fused gate size to three qubits.
options = {"f": 3}
qsim_simulator = qsimcirq.QSimSimulator(options)
qsim_start = time.time()
qsim_results = qsim_simulator.simulate(circuit)
qsim_elapsed = time.time() - qsim_start
print(f"qsim runtime: {qsim_elapsed} seconds.")
qsim runtime: 0.12220454216003418 seconds.
View on QuantumAI
Run in Google Colab
View source on GitHub