Coming from Qiskit Aer¶
Run existing Qiskit code on Maestro by changing an import. The qiskit-maestro package gives Qiskit a Maestro backend, Sampler and Estimator:
pip install qiskit-maestro
Qiskit |
qiskit-maestro |
|---|---|
|
|
|
|
|
|
from qiskit import QuantumCircuit, transpile
from qiskit.quantum_info import SparsePauliOp
from qiskit_maestro import MaestroBackend, MaestroEstimator, MaestroSampler
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
measured = qc.measure_all(inplace=False)
backend = MaestroBackend(seed_simulator=7) # was AerSimulator(seed_simulator=7)
print(backend.run(transpile(measured, backend), shots=1000).result().get_counts())
sampler = MaestroSampler(seed=7) # was StatevectorSampler(seed=7)
print(sampler.run([measured], shots=1000).result()[0].data.meas.get_counts())
estimator = MaestroEstimator() # was StatevectorEstimator()
observables = [
SparsePauliOp("IIZ"),
SparsePauliOp("XXX"),
SparsePauliOp(["IZZ", "XXX"], coeffs=[0.5, 0.3]),
]
print(estimator.run([(qc, observables)]).result()[0].data.evs)
{'000': 489, '111': 511}
{'111': 511, '000': 489}
[0. 1. 0.8]
The plugin is a separate package,
qiskit-maestro, which installs
qoro-maestro for you. It needs Python 3.10 or later and works with Qiskit 1.1
and later, including 2.x. Everything stays in Qiskit’s conventions: bit order,
SparsePauliOp observables with coefficients, classical registers, PUBs and
broadcasting. transpile and generate_preset_pass_manager target Maestro’s
native gates, and Qiskit’s BackendSamplerV2 works with MaestroBackend.
If you call Maestro directly rather than through Qiskit, Translate to Maestro’s API maps Aer code onto Maestro’s own API.
Parameter sets and VQE¶
Parameterised circuits take parameter values as in any EstimatorV2, with
Qiskit’s broadcasting rules. qiskit-maestro converts the circuit once and runs
the parameter sets in parallel, so scanning a VQE ansatz is one call:
import numpy as np
from qiskit.circuit.library import efficient_su2
n = 6
ansatz = efficient_su2(n, reps=2, entanglement="linear")
hamiltonian = SparsePauliOp.from_sparse_list(
[("ZZ", [q, q + 1], -1.0) for q in range(n - 1)] + [("X", [q], -0.5) for q in range(n)],
num_qubits=n,
)
parameter_values = np.random.default_rng(0).uniform(0, np.pi, (8, ansatz.num_parameters))
result = estimator.run([(ansatz, hamiltonian, parameter_values)]).result()
print(result[0].data.evs) # One energy per parameter set.
[ 0.68924685 0.36821318 0.39232302 -0.02954506 0.28569697 -0.06558148
0.63825606 0.21059443]
The example notebook runs a whole VQE loop on a 100-qubit Ising chain on Aer and on Maestro, both with matrix product states.
Choose the simulation method¶
Options take Aer’s names, and Aer’s longer MPS option names are accepted too. An MPS handles many qubits when entanglement is limited:
mps_estimator = MaestroEstimator(
method="matrix_product_state",
matrix_product_state_max_bond_dimension=64,
matrix_product_state_truncation_threshold=1e-10,
)
wide = QuantumCircuit(120)
wide.h(0)
for q in range(119):
wide.cx(q, q + 1)
zz = SparsePauliOp.from_sparse_list([("ZZ", [0, 119], 1.0)], 120)
print(mps_estimator.run([(wide, zz)]).result()[0].data.evs)
1.0000000000000002
Option |
Values |
|---|---|
|
|
|
|
|
MPS truncation; Aer’s |
|
|
|
Reproducible results |
|
Threads for circuits and parameter sets; |
|
A dictionary of any other SimulatorConfig field |
Pass the options to MaestroBackend, MaestroSampler or MaestroEstimator.
The primitives also accept the options={"backend_options": ...} dictionary of
Aer’s primitives unchanged. Maestro may run a circuit on a faster engine than
the one you asked for, such as a Clifford circuit on the stabilizer engine;
results are unaffected, and each result’s metadata records what ran.
How MPS expectation values compare with Aer’s on 100+ qubits is on the benchmarks page: 6 to 7.5 times faster for a VQE energy evaluation.
Time evolution¶
When the PUBs of one call are one circuit per time point of the same Trotter
step, MaestroEstimator simulates the longest circuit once with
incremental evolution and reads every time point
on the way. Measuring after each of N steps then costs N steps instead of about
N²/2. MaestroEstimator.run_evolution takes the initial circuit and the step
directly:
n = 40
step = QuantumCircuit(n) # One Trotter step of an Ising chain.
for start in (0, 1):
for q in range(start, n - 1, 2):
step.rzz(-0.2, q, q + 1)
step.rx(-0.14, range(n))
magnetisation = SparsePauliOp.from_sparse_list([("Z", [q], 1 / n) for q in range(n)], n)
# One circuit per time point, as for any estimator.
circuits, circuit = [], QuantumCircuit(n)
for k in range(11):
circuits.append(circuit.copy())
circuit.compose(step, inplace=True)
results = mps_estimator.run([(c, magnetisation) for c in circuits]).result()
print("incremental:", results[0].metadata["maestro"]["incremental_evolution"])
print(np.array([r.data.evs for r in results]).round(4))
# The same evolution, given as an initial circuit and a step.
evolution = mps_estimator.run_evolution(QuantumCircuit(n), step, magnetisation, steps=range(11))
print(evolution.result()[0].data.evs.round(4))
incremental: True
[1. 0.9902 0.9625 0.9215 0.8734 0.8248 0.7811 0.7458 0.7196 0.7014
0.6889]
[1. 0.9902 0.9625 0.9215 0.8734 0.8248 0.7811 0.7458 0.7196 0.7014
0.6889]
PauliEvolutionGate circuits synthesised with LieTrotter(reps=k) are
recognised too. On the benchmarks page, measuring every step this way is 4.9
times faster than Aer at 120 qubits and 7 times faster at 200. Incremental
evolution isn’t used for PUBs with arrays of parameter values or for circuits
that reset qubits, and incremental_evolution=False turns it off.
Run on a GPU¶
Requires GPU access
device="GPU" needs the GPU plugin and a licence. See Get GPU access.
# Not executed in the docs build: requires GPU access.
gpu_backend = MaestroBackend(device="GPU")
gpu_mps = MaestroEstimator(device="GPU", method="matrix_product_state", max_bond_dimension=256)
The GPU runs the statevector, MPS, tensor network and Pauli propagation
methods. Maestro reads the licence key from MAESTRO_LICENSE_KEY itself. If no
GPU is available or the licence isn’t accepted, qiskit-maestro raises
MaestroGpuError rather than running on the CPU.
What isn’t supported yet¶
qiskit-maestro doesn’t yet translate Aer noise models, for and while loops,
switch, or Aer’s save_* instructions, and Estimator circuits can’t contain
measurements, as in StatevectorEstimator. Its
README
lists everything that isn’t supported yet.
Translate to Maestro’s API¶
The rest of this page maps Aer code onto Maestro’s own API, for code that calls Maestro directly.
Warning
Maestro puts qubit 0 first, on the left, in bitstrings and Pauli strings.
Qiskit puts it last, on the right. Flipping only qubit 0 of three reads "100"
in Maestro and "001" in Qiskit, and Z on qubit 0 is "ZII" in Maestro and
"IIZ" in a Qiskit SparsePauliOp.
from maestro.circuits import QuantumCircuit
flip = QuantumCircuit()
flip.x(0)
flip.measure([(0, 0), (1, 1), (2, 2)])
counts = flip.execute(shots=10)["counts"]
print("Maestro order:", counts)
print("Qiskit order: ", {bits[::-1]: n for bits, n in counts.items()})
Maestro order: {'100': 10}
Qiskit order: {'001': 10}
Each section below shows the Aer code next to its Maestro equivalent. The Aer code was checked against Qiskit 2.6 and Qiskit Aer 0.18 but isn’t run in the docs build; the Maestro code is.
Statevector¶
AerSimulator(method="statevector") with save_statevector() becomes get_statevector(). Both use the same amplitude order, with qubit 0 as the least significant bit of the index.
Qiskit Aer
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
qc.save_statevector()
sim = AerSimulator(method="statevector")
state = sim.run(qc).result().get_statevector()
print(state)
Maestro
from maestro.circuits import QuantumCircuit
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
state = qc.get_statevector()
print(state)
Output of the Maestro code, run when these docs were built:
[(0.7071067811865475+0j), 0j, 0j, 0j, 0j, 0j, 0j, (0.7071067811865475+0j)]
Matrix product state¶
Aer’s matrix_product_state_* options become SimulatorConfig fields. Both default to the discarded-weight truncation convention, so thresholds mean the same thing.
Qiskit Aer
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
qc = QuantumCircuit(50)
qc.h(0)
for q in range(49):
qc.cx(q, q + 1)
qc.measure_all()
sim = AerSimulator(
method="matrix_product_state",
matrix_product_state_max_bond_dimension=64,
matrix_product_state_truncation_threshold=1e-10,
)
result = sim.run(qc, shots=1000).result()
print(len(result.get_counts()), "distinct outcomes")
Maestro
import maestro
from maestro.circuits import QuantumCircuit
qc = QuantumCircuit()
qc.h(0)
for q in range(49):
qc.cx(q, q + 1)
qc.measure_all()
mps = maestro.SimulationType.MatrixProductState
config = maestro.SimulatorConfig(
simulation_type=mps,
max_bond_dimension=64,
singular_value_threshold=1e-10,
)
result = qc.execute(shots=1000, config=config)
print(len(result["counts"]), "distinct outcomes")
Output of the Maestro code, run when these docs were built:
2 distinct outcomes
Shots and counts¶
Shots go to the call in both. Maestro needs no transpile step, and seed_simulator becomes seed. The counts below are the same Bell outcomes, but compare bit order before comparing longer bitstrings.
Qiskit Aer
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
sim = AerSimulator(seed_simulator=7)
job = sim.run(transpile(qc, sim), shots=1000)
print(job.result().get_counts())
Maestro
import maestro
from maestro.circuits import QuantumCircuit
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
config = maestro.SimulatorConfig(seed=7)
result = qc.execute(shots=1000, config=config)
print(result["counts"])
Output of the Maestro code, run when these docs were built:
{'11': 484, '00': 516}
Expectation values (Estimator)¶
EstimatorV2 becomes estimate. Pauli labels are reversed, and a weighted sum such as a SparsePauliOp with coefficients is estimated term by term.
Qiskit Aer
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit_aer.primitives import EstimatorV2
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
# Qiskit puts qubit 0 on the right:
# "IIZ" is Z on qubit 0.
observables = [
SparsePauliOp("IIZ"),
SparsePauliOp("XXX"),
SparsePauliOp(["IZZ", "XXX"], coeffs=[0.5, 0.3]),
]
job = EstimatorV2().run([(qc, observables)])
print(job.result()[0].data.evs)
Maestro
from maestro.circuits import QuantumCircuit
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
# Maestro puts qubit 0 on the left:
# "ZII" is Z on qubit 0.
result = qc.estimate(["ZII", "XXX", "ZZI"])
z0, xxx, zz01 = result["expectation_values"]
# Weight Hamiltonian terms yourself.
print([z0, xxx, 0.5 * zz01 + 0.3 * xxx])
Output of the Maestro code, run when these docs were built:
[0.0, 0.9999999999999998, 0.7999999999999998]
Noise models¶
Aer attaches errors to gate names; Maestro attaches them to qubits, after every gate that touches the qubit. With a Pauli channel on both qubits of each CX, the two models below are the same.
Qiskit Aer
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel, pauli_error
p = 0.05
one_qubit = pauli_error([
("X", p / 3), ("Y", p / 3),
("Z", p / 3), ("I", 1 - p),
])
two_qubit = one_qubit.tensor(one_qubit)
noise = NoiseModel()
noise.add_all_qubit_quantum_error(one_qubit, ["h"])
noise.add_all_qubit_quantum_error(two_qubit, ["cx"])
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
sim = AerSimulator(noise_model=noise,
seed_simulator=7)
result = sim.run(qc, shots=4000).result()
print(result.get_counts())
Maestro
import maestro
from maestro.circuits import QuantumCircuit
p = 0.05
# After every gate, on each qubit it touches.
noise = maestro.NoiseModel()
noise.set_all_depolarizing(num_qubits=2, p=p)
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
# One noise realization per shot, as in Aer.
result = maestro.noisy_execute(
qc, noise, shots=4000,
noise_realizations=4000, noise_seed=7,
)
print(result["counts"])
Output of the Maestro code, run when these docs were built:
{'01': 109, '10': 125, '00': 1860, '11': 1906}
Run an existing Qiskit circuit¶
MaestroBackend and the qiskit-maestro primitives take Qiskit circuits as they
are. Without the package, export the circuit
to OpenQASM 2.0 with qiskit.qasm2.dumps(qc) and run the text with
maestro.simple_execute. Qiskit’s
measure_all() output, barrier included, runs as is:
import maestro
exported = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[3];
creg meas[3];
h q[0];
cx q[0],q[1];
rz(0.3) q[2];
barrier q[0],q[1],q[2];
measure q[0] -> meas[0];
measure q[1] -> meas[1];
measure q[2] -> meas[2];
""" # qasm2.dumps() output for a Qiskit circuit
print(maestro.simple_execute(exported, shots=1000)["counts"])
{'110': 488, '000': 512}
Qiskit would print the "110" outcome above as "011".
Simulator options¶
Qiskit Aer |
Maestro |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
To keep Aer’s engine while using Maestro’s interface, set
simulator_type=SimulatorType.QiskitAer.
What doesn’t map directly¶
These differences apply to Maestro’s own API. qiskit-maestro takes care of the
bit order, weighted observables, primitives, registers and if_test for you.
Bit and qubit order. Counts and Pauli labels are reversed; see the warning at the top of this page.
Weighted observables.
estimatetakes Pauli strings only. Estimate each term of aSparsePauliOpand weight them yourself, as in Expectation values (Estimator).AdjointProgramdoes accept(coefficient, string)terms.Statevector width.
get_statevectorleaves out qubits that no gate touches, so a 3-qubit circuit that never uses qubit 1 returns 4 amplitudes, and qubit 2 becomes bit 1 of the index.Depolarizing strength. Maestro’s
pis the total Pauli error probability, p/3 each for X, Y and Z. Aer’sdepolarizing_error(λ, 1)corresponds top = 3λ/4.Noise sampling in
noisy_execute. Aer draws noise for every shot.noisy_executedrawsnoise_realizationsnoise samples (64 by default) and splits the shots among them, so counts vary more between runs unless you setnoise_realizations=shots.No primitives or
save_*instructions. Callexecute,estimateorget_statevectoron the circuit. Results are plain dictionaries; see The result dictionary.No registers.
measuretakes (qubit, classical bit) pairs, and counts are single bitstrings with no spaces between registers.Classical control.
QuantumCircuithas noif_testorc_if. Write feed-forward in OpenQASM 2.0 and run it withsimple_execute; see Mid-circuit measurement and reset.