# What is the most efficient way to run several simulations of parameterized quantum circuits in Qiskit?

Suppose I am given a parameterized quantum circuit in Qiskit: as an example, here I consider a ZZFeatureMap ansatz, commonly used for data embedding in quantum machine learning appications:

from qiskit.circuit.library import ZZFeatureMap

num_features = 3
pqc = ZZFeatureMap(num_features, reps=1).decompose()
pqc.measure_all()

pqc.draw('mpl')


What I want to do is to run nsims full simulations of the above circuit passing each time a different set of values to be assigned to the ansatz free parameters. Moreover, each single simulation has to be repeated a number of times equals to shots in order to get a statistically robust final outcome for the measurement operation.

The most straightforward way to do it in Qiskit should be something like the following:

import numpy as np
from qiskit import Aer

nsims = 1000
shots = 2048
parameter_values = np.random.rand(nsims, num_features)

simulator = Aer.get_backend('aer_simulator')
for vals in parameter_values:
job = simulator.run(pqc.assign_parameters(vals), shots=shots)


However, on my local CPU, this took more than 10 seconds already. I guess that here the problem is that every time I call the assign_parameters method inside the loop, Qiskit is actually creating a deepcopy of the whole circuit to make the parameter binding possible.

Another possibility I tried out is based on the Sampler primitive, provided by Qiskit itself:

from qiskit.primitives import Sampler

sampler = Sampler()
circuits = [pqc] * nsims
job = sampler.run(circuits, parameter_values, shots=shots)


This solution is slightly better that the first one but still it looks pretty slow and far from being optimal. So, how can I make it faster? Is there an efficient way that maybe leverages more advanced Qiskit features?

If you're running simulations of circuits with few qubits (like your example above), the Sampler should be the fastest out-of-the-box method to run the circuit. It's internally relying on the Statevector class, which just applies the gates and computes the full statevector -- this has almost no overhead so it's fast for small circuit, but starting from say 10+ qubits or really deep circuits this is likely going to be slower than Aer.

To marginally speed this approach up you could try using putting the CX P(theta) CX sequence in a two-qubit gate with a to_matrix method, so the Statevector can apply the full block at once instead of 3 individual gates.

If you want to avoid the deep copy, which you correctly identified as the bottleneck, you could use a hacky approach:

1. Create the circuit with fixed values of the first simulation
2. Evaluate the single circuit with the Sampler
3. Replace the gate angles of the P gate by iterating over circuit.data, finding the PhaseGates and replacing PhaseGate.params with the new value
4. Go to 2 :)

This is a bit hacky as you have to manually compute the gate angles and iterate over the circuit data -- but it avoids the expensive copy. FYI, we're currently working on making circuits lightweight and copies cheap, so in the future this is hopefully not going to be a problem anymore.

I hope that helps!