Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Rust generator function for QuantumVolume class #13283

Merged
merged 4 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 28 additions & 19 deletions qiskit/circuit/library/quantum_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,39 +83,48 @@ def __init__(
depth = depth or num_qubits # how many layers of SU(4)
width = num_qubits // 2 # how many SU(4)s fit in each layer
rng = seed if isinstance(seed, np.random.Generator) else np.random.default_rng(seed)
seed_name = seed
if seed is None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it would be clearer to check if seed_name is None instead, since the purpose of this branch is to make sure it has a value.

# Get the internal entropy used to seed the default RNG, if no seed was given. This
# stays in the output name, so effectively stores a way of regenerating the circuit.
# This is just best-effort only, for backwards compatibility, and isn't critical (if
# someone needs full reproducibility, they should be manually controlling the seeding).
seed = getattr(getattr(rng.bit_generator, "seed_seq", None), "entropy", None)
seed_name = getattr(getattr(rng.bit_generator, "seed_seq", None), "entropy", None)

super().__init__(
num_qubits, name="quantum_volume_" + str([num_qubits, depth, seed]).replace(" ", "")
num_qubits,
name="quantum_volume_" + str([num_qubits, depth, seed_name]).replace(" ", ""),
)
base = self if flatten else QuantumCircuit(num_qubits, name=self.name)

# For each layer, generate a permutation of qubits
# Then generate and apply a Haar-random SU(4) to each pair
unitaries = scipy.stats.unitary_group.rvs(4, depth * width, rng).reshape(depth, width, 4, 4)
qubits = tuple(base.qubits)
for row in unitaries:
perm = rng.permutation(num_qubits)
if classical_permutation:
for w, unitary in enumerate(row):
gate = UnitaryGate(unitary, check_input=False, num_qubits=2)
qubit = 2 * w
base._append(
CircuitInstruction(gate, (qubits[perm[qubit]], qubits[perm[qubit + 1]]))
)
if classical_permutation:
if seed is not None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be slower than usual as I'm reviewing this before my first coffee, but should this be the following?

Suggested change
if seed is not None:
if seed is None:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually correct. This code path is to normalize the seeds we pass to rust so it will fit in a uint64 (although I realized I used int64 by mistake, I'll fix that). The typing on the python class was a python int which was doesn't have an upper bound and it was possible to pass in an value greater than 18446744073709551615 which would cause an error when one didn't happen before. To solve that I used a numpy rng to select a random int from the provided seed value that we then pass to rust as a seed for it's internal rng.

When seed is None we can just pass that to rust because it will use system entropy to initialize the rust rng.

seed = rng.integers(0, dtype=np.int64)
qv_circ = quantum_volume(num_qubits, depth, seed)
qv_circ.name = self.name
if flatten:
self.compose(qv_circ, inplace=True)
else:
self._append(CircuitInstruction(qv_circ.to_instruction(), tuple(self.qubits)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we instead just early-return here instead of having the else on line 107? That way, it's a bit clearer that the handling of classical permutations is treated specially and forwarded to the Rust quantum_volume.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't return because __init__ normally doesn't have a return; that's an implicit None but there is a pylint rule that doesn't like an explicit return in one path and not another.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think pylint is fine with bare return for early return, it's return None it has a problem with (unless it's changed).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's probably it, I normally would do return None in this case. I can change it to a bare return if there is a strong preference here.

else:
if seed is None:
seed = seed_name

base = self if flatten else QuantumCircuit(num_qubits, name=self.name)

# For each layer, generate a permutation of qubits
# Then generate and apply a Haar-random SU(4) to each pair
unitaries = scipy.stats.unitary_group.rvs(4, depth * width, rng).reshape(
depth, width, 4, 4
)
qubits = tuple(base.qubits)
for row in unitaries:
perm = rng.permutation(num_qubits)
base._append(CircuitInstruction(PermutationGate(perm), qubits))
for w, unitary in enumerate(row):
gate = UnitaryGate(unitary, check_input=False, num_qubits=2)
qubit = 2 * w
base._append(CircuitInstruction(gate, qubits[qubit : qubit + 2]))
if not flatten:
self._append(CircuitInstruction(base.to_instruction(), tuple(self.qubits)))
if not flatten:
self._append(CircuitInstruction(base.to_instruction(), tuple(self.qubits)))


def quantum_volume(
Expand Down
17 changes: 17 additions & 0 deletions releasenotes/notes/add-qv-function-a8990e248d5e7e1a.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,20 @@ features_circuits:
a :class:`.QuantumCircuit` object instead of building a subclass object. The second is
that this new function is multithreaded and implemented in rust so it generates the output
circuit ~10x faster than the :class:`.QuantumVolume` class.
- |
Improved the runtime performance of constructing the
:class:`.QuantumVolume` class with the ``classical_permutation`` argument set
to ``True``. Internally it now calls the :func:`.quantum_volume`
function which is written in Rust which is ~10x faster to generate a
quantum volume circuit.

upgrade_circuits:
- |
The :class:`.QuantumVolume` class will generate circuits with
different unitary matrices and permutations for a given seed value from
the previous Qiskit release. This is due to using a new internal random
number generator for the circuit generation that will generate the circuit
more quickly. If you need an exact circuit with the same seed you can
use the previous release of Qiskit and generate the circuit with the
``flatten=True`` argument and export the circuit with :func:`.qpy.dump`
and then load it with this release.
Loading