01 getting started
Using propaq¶
Welcome to the propaq user guide! This notebook will help you get started with using propaq for quantum circuit simulation. This covers the basics of propaq, we encourage the user to explore the documentation and other notebooks for more advanced features.
Basic usage¶
In order to run a Heisenberg simulation with propaq, you will need a circuit, observable, and a state. For this example, we'll be using Qiskit to create a simple circuit and observable.
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit.circuit.library import (
XXPlusYYGate,
PhaseGate,
RZGate,
CPhaseGate,
SwapGate,
XGate
)
import numpy as np
GATES = [
(lambda: XXPlusYYGate(
np.random.uniform(0, 2 * np.pi),
np.random.uniform(0, 2 * np.pi)
), 2),
(lambda: PhaseGate(np.random.uniform(0, 2 * np.pi)), 1),
(lambda: RZGate(np.random.uniform(0, 2 * np.pi)), 1),
(lambda: CPhaseGate(np.random.uniform(0, 2 * np.pi)), 2),
(lambda: SwapGate(), 2),
(lambda: XGate(), 1)
]
qc = QuantumCircuit(4)
for _ in range(10):
factory, nq = GATES[np.random.randint(len(GATES))]
gate = factory()
qubits = np.random.choice(4, size=nq, replace=False).tolist()
qc.append(gate, qubits)
observable = SparsePauliOp.from_list([
("XIII", 1.0),
("IXII", 1.0),
("IIXI", 1.0),
("IIIX", 1.0)
])
Then, we need to convert them into objects recognized by propaq's internals. For this example, we'll implement Majorana propagation.
from propaq.circuits import MajoranaCircuit
from propaq.datatypes import MajoranaTermSum
mc = MajoranaCircuit.from_qiskit(qc, n_modes = 2 * qc.num_qubits)
mts = MajoranaTermSum.from_sparse_pauli_op(observable)
Now, let's add noise and a truncation strategy, and build the propagator.
from propaq.noise import UniformNoiseModel
from propaq.truncation import TruncationPolicy
noise = UniformNoiseModel(damping=0.001) # Uniform depolarizing noise with damping parameter 0.001
truncation_policy = TruncationPolicy(
weight_cutoff=10, # Max weight of terms to keep
coeff_cutoff=1e-5, # Min coefficient magnitude to keep
min_terms=100_000 # Suppress weight/coefficient truncation below this many live terms
)
from propaq.propagators import MajoranaPropagator
prop = MajoranaPropagator(
noise = noise,
truncation = truncation_policy,
n_threads = 4, # Number of threads to use for parallelization
progress_bar=True
)
Now, we can compute the expectation value of the observable by back-propagating it through the circuit and evaluating the resulting term sum.
result = prop.expectation_value(mts, mc, initial_state=0)
print("Expectation value:", result.expectation_value)
Propagating: 0%| | 0/25 [00:00<?, ?gate/s]
Expectation value: 0.0
Logging¶
To gain more information about the propagation process, you can enable logging.
from propaq import Logger, LogParser
logger = Logger(filename="propaq.log", log_every=5) # log every 5 gates
prop_log = MajoranaPropagator(
noise = noise,
truncation = truncation_policy,
progress_bar=True,
logger = logger
)
result = prop_log.expectation_value(mts, mc, initial_state=0)
Propagating: 0%| | 0/25 [00:00<?, ?gate/s]
We should now have a file called propaq.log in the current directory, which contains JSON lines of the main propagation events. Each line corresponds to either a gate application or a truncation event, and contains relevant information about the event. This is rather unpleasant to read as-is, so we can use the LogParser to extract and visualize the information.
parser = LogParser("propaq.log")
First, let's look at the gate events. This allows us to see how many terms are being generated in the hashmap and outbox at each step of the propagation.
parser.gate_events
[GateEvent(gate_idx=0, layer_idx=0, terms=5, ms_per_gate=1.336, qiskit_gate_idx=9, monomials=None), GateEvent(gate_idx=5, layer_idx=1, terms=14, ms_per_gate=1.272, qiskit_gate_idx=8, monomials=None), GateEvent(gate_idx=10, layer_idx=2, terms=52, ms_per_gate=1.123, qiskit_gate_idx=7, monomials=None), GateEvent(gate_idx=15, layer_idx=3, terms=60, ms_per_gate=0.7797, qiskit_gate_idx=5, monomials=None), GateEvent(gate_idx=20, layer_idx=4, terms=61, ms_per_gate=1.213, qiskit_gate_idx=2, monomials=None)]
We can also look at the truncation events, which contain information on the number of terms discarded, the coefficients of the discarded terms, and the truncation thresholds. This can be useful for debugging and tuning the truncation strategy.
parser.truncation_events
[TruncationEvent(gate_idx=0, layer_idx=0, trigger='emit', terms_before=4, terms_after=5, terms_gained=1, terms_discarded=0, discarded_coeff_l1=0.0, discarded_coeff_max=0.0, weight_cutoff=10, coeff_cutoff=1e-05, elapsed_ms=1.336, qiskit_gate_idx=9), TruncationEvent(gate_idx=5, layer_idx=1, trigger='emit', terms_before=9, terms_after=14, terms_gained=5, terms_discarded=0, discarded_coeff_l1=0.0, discarded_coeff_max=0.0, weight_cutoff=10, coeff_cutoff=1e-05, elapsed_ms=1.272, qiskit_gate_idx=8), TruncationEvent(gate_idx=10, layer_idx=2, trigger='emit', terms_before=36, terms_after=52, terms_gained=16, terms_discarded=0, discarded_coeff_l1=0.0, discarded_coeff_max=0.0, weight_cutoff=10, coeff_cutoff=1e-05, elapsed_ms=1.123, qiskit_gate_idx=7), TruncationEvent(gate_idx=15, layer_idx=3, trigger='emit', terms_before=60, terms_after=60, terms_gained=0, terms_discarded=0, discarded_coeff_l1=0.0, discarded_coeff_max=0.0, weight_cutoff=10, coeff_cutoff=1e-05, elapsed_ms=0.7797, qiskit_gate_idx=5), TruncationEvent(gate_idx=20, layer_idx=4, trigger='emit', terms_before=60, terms_after=61, terms_gained=1, terms_discarded=0, discarded_coeff_l1=0.0, discarded_coeff_max=0.0, weight_cutoff=10, coeff_cutoff=1e-05, elapsed_ms=1.213, qiskit_gate_idx=2)]
The complete list of available logged information is as follows:
properties = [
name
for name, value in LogParser.__dict__.items()
if isinstance(value, property)
]
properties
['gate_events', 'truncation_events', 'surrogate_merge_events', 'engine_phases_events', 'gate_indices', 'terms', 'monomials', 'terms_before', 'terms_after', 'terms_gained', 'terms_discarded', 'discarded_coeff_l1', 'discarded_coeff_max', 'qiskit_gate_indices', 'ms_per_gate', 'elapsed_ms', 'monomials_before', 'monomials_after', 'monomials_discarded']
Cirq¶
propaq also optionally supports Cirq circuits! This requires installing an extra dependency -
!pip install propaq[cirq] --quiet
Now, construct an example circuit and observable using Cirq, and follow the same steps as above to compute the expectation value of the observable!
import cirq
CIRQ_GATES = [
(lambda: cirq.PhasedISwapPowGate(
phase_exponent=np.random.uniform(0, 2 * np.pi),
exponent=np.random.uniform(0, 2 * np.pi)
), 2),
(lambda: cirq.ZPowGate(exponent=np.random.uniform(0, 2 * np.pi)), 1),
(lambda: cirq.CZPowGate(exponent=np.random.uniform(0, 2 * np.pi)), 2),
(lambda: cirq.SWAP, 2),
(lambda: cirq.X, 1)
]
qubits = cirq.LineQubit.range(4)
cirq_qc = cirq.Circuit()
for _ in range(10):
factory, nq = CIRQ_GATES[np.random.randint(len(CIRQ_GATES))]
gate = factory()
selected = np.random.choice(4, size=nq, replace=False).tolist()
cirq_qc.append(gate.on(*[qubits[q] for q in selected]))
cirq_observable = SparsePauliOp.from_list([
("XIII", 1.0),
("IXII", 1.0),
("IIXI", 1.0),
("IIIX", 1.0)
])
cirq_mc = MajoranaCircuit.from_cirq(cirq_qc, n_modes=2 * len(qubits))
cirq_mts = MajoranaTermSum.from_sparse_pauli_op(cirq_observable)
cirq_result = prop.expectation_value(cirq_mts, cirq_mc, initial_state=0)
print("Expectation value:", cirq_result.expectation_value)
Propagating: 0%| | 0/24 [00:00<?, ?gate/s]
Expectation value: 0.0