Add infrastructure for gates, instruction, and operations in Rust (#12459)

* Add infrastructure for gates, instruction, and operations in Rust

This commit adds a native representation of Gates, Instruction, and
Operations to rust's circuit module. At a high level this works by
either wrapping the Python object in a rust wrapper struct that tracks
metadata about the operations (name, num_qubits, etc) and then for other
details it calls back to Python to get dynamic details like the
definition, matrix, etc. For standard library gates like Swap, CX, H,
etc this replaces the on-circuit representation with a new rust enum
StandardGate. The enum representation is much more efficient and has a
minimal memory footprint (just the enum variant and then any parameters
or other mutable state stored in the circuit instruction). All the gate
properties such as the matrix, definiton, name, etc are statically
defined in rust code based on the enum variant (which represents the
gate).

The use of an enum to represent standard gates does mean a change in
what we store on a CircuitInstruction. To represent a standard gate
fully we need to store the mutable properties of the existing Gate class
on the circuit instruction as the gate by itself doesn't contain this
detail. That means, the parameters, label, unit, duration, and condition
are added to the rust side of circuit instrucion. However no Python side
access methods are added for these as they're internal only to the Rust
code. In Qiskit 2.0 to simplify this storage we'll be able to drop, unit,
duration, and condition from the api leaving only label and parameters.
But for right now we're tracking all of the fields.

To facilitate working with circuits and gates full from rust the
setting the `operation` attribute of a `CircuitInstruction` object now
transltates the python object to an internal rust representation.
For standard gates this translates it to the enum form described earlier,
and for other circuit operations 3 new Rust structs: PyGate,
PyInstruction, and PyOperation are used to wrap the underlying Python
object in a Rust api. These structs cache some commonly accessed static
properties of the operation, such as the name, number of qubits, etc.
However for dynamic pieces, such as the definition or matrix, callback
to python to get a rust representation for those.

Similarly whenever the `operation` attribute is accessed from Python
it converts it back to the normal Python object representation. For
standard gates this involves creating a new instance of a Python object
based on it's internal rust representation. For the wrapper structs a
reference to the wrapped PyObject is returned.

To manage the 4 variants of operation (`StandardGate`, `PyGate`,
`PyInstruction`, and `PyOperation`) a new Rust trait `Operation` is
created that defines a standard interface for getting the properties
of a given circuit operation. This common interface is implemented for
the 4 variants as well as the `OperationType` enum which wraps all 4
(and is used as the type for `CircuitInstruction.operation` in the
rust code.

As everything in the `QuantumCircuit` data model is quite coupled moving
the source of truth for the operations to exist in Rust means that more
of the underlying `QuantumCircuit`'s responsibility has to move to Rust
as well. Primarily this involves the `ParameterTable` which was an
internal class for tracking which instructions in the circuit have a
`ParameterExpression` parameter so that when we go to bind parameters we
can lookup which operations need to be updated with the bind value.
Since the representation of those instructions now lives in Rust and
Python only recieves a ephemeral copy of the instructions the
ParameterTable had to be reimplemented in Rust to track the
instructions. This new parameter table maps the Parameter's uuid (as a
u128) as a unique identifier for each parameter and maps this to a
positional index in the circuit data to the underlying instruction using
that parameter. This is a bit different from the Python parameter table
which was mapping a parameter object to the id of the operation object
using that parmaeter. This also leads to a difference in the binding
mechanics as the parameter assignment was done by reference in the old
model, but now we need to update the entire instruction more explicitly
in rust. Additionally, because the global phase of a circuit can be
parameterized the ownership of global phase is moved from Python into
Rust in this commit as well.

After this commit the only properties of a circuit that are not defined
in Rust for the source of truth are the bits (and vars) of the circuit,
and when creating circuits from rust this is what causes a Python
interaction to still be required.

This commit does not translate the full standard library of gates as
that would make the pull request huge, instead this adds the basic
infrastructure for having a more efficient standard gate representation
on circuits. There will be follow up pull requests to add the missing
gates and round out support in rust.

The goal of this pull request is primarily to add the infrastructure for
representing the full circuit model (and dag model in the future) in
rust. By itself this is not expected to improve runtime performance (if
anything it will probably hurt performance because of extra type
conversions) but it is intended to enable writing native circuit
manipulations in Rust, including transpiler passes without needing
involvement from Python. Longer term this should greatly improve the
runtime performance and reduce the memory overhead of Qiskit. But,
this is just an early step towards that goal, and is more about
unlocking the future capability. The next steps after this commit are
to finish migrating the standard gate library and also update the
`QuantumCircuit` methods to better leverage the more complete rust
representation (which should help offset the performance penalty
introduced by this).

Fixes: #12205

* Fix Python->Rust Param conversion

This commit adds a custom implementation of the FromPyObject trait for
the Param enum. Previously, the Param trait derived it's impl of the
trait, but this logic wasn't perfect. In cases whern a
ParameterExpression was effectively a constant (such as `0 * x`) the
trait's attempt to coerce to a float first would result in those
ParameterExpressions being dropped from the circuit at insertion time.
This was a change in behavior from before having gates in Rust as the
parameters would disappear from the circuit at insertion time instead of
at bind time. This commit fixes this by having a custom impl for
FromPyObject that first tries to figure out if the parameter is a
ParameterExpression (or a QuantumCircuit) by using a Python isinstance()
check, then tries to extract it as a float, and finally stores a
non-parameter object; which is a new variant in the Param enum. This
new variant also lets us simplify the logic around adding gates to the
parameter table as we're able to know ahead of time which gate
parameters are `ParameterExpression`s and which are other objects (and
don't need to be tracked in the parameter table.

Additionally this commit tweaks two tests, the first is
test.python.circuit.library.test_nlocal.TestNLocal.test_parameters_setter
which was adjusted in the previous commit to workaround the bug fixed
by this commit. The second is test.python.circuit.test_parameters which
was testing that a bound ParameterExpression with a value of 0 defaults
to an int which was a side effect of passing an int input to symengine
for the bind value and not part of the api and didn't need to be
checked. This assertion was removed from the test because the rust
representation is only storing f64 values for the numeric parameters
and it is never an int after binding from the Python perspective it
isn't any different to have float(0) and int(0) unless you explicit
isinstance check like the test previously was.

* Fix qasm3 exporter for std gates without stdgates.inc

This commit fixes the handling of standard gates in Qiskit when the user
specifies excluding the use of the stdgates.inc file from the exported
qasm. Previously the object id of the standard gates were used to
maintain a lookup table of the global definitions for all the standard
gates explicitly in the file. However, the rust refactor means that
every time the exporter accesses `circuit.data[x].operation` a new
instance is returned. This means that on subsequent lookups for the
definition the gate definitions are never found. To correct this issue
this commit adds to the lookup table a fallback of the gate name +
parameters to do the lookup for. This should be unique for any standard
gate and not interfere with the previous logic that's still in place and
functional for other custom gate definitions.

While this fixes the logic in the exporter the test is still failing
because the test is asserting the object ids are the same in the qasm3
file, which isn't the case anymore. The test will be updated in a
subsequent commit to validate the qasm3 file is correct without using
a hardcoded object id.

* Fix base scheduler analysis pass duration setting

When ALAPScheduleAnalysis and ASAPScheduleAnalysis were setting the
duration of a gate they were doing `node.op.duration = duration` this
wasn't always working because if `node.op` was a standard gate it
returned a new Python object created from the underlying rust
representation. This commit fixes the passes so that they modify the
duration and then explicit set the operation to update it's rust
representation.

* Fix python lint

* Fix last failing qasm3 test for std gates without stdgates.inc

While the logic for the qasm3 exporter was fixed
in commit a6e69ba4c9 to handle the edge
case of a user specifying that the qasm exporter does not use the
stdgates.inc include file in the output, but also has qiskit's standard
gates in their circuit being exported. The one unit test to provide
coverage for that scenario was not passing because when an id was used
for the gate definitions in the qasm3 file it was being referenced
against a temporary created by accessing a standard gate from the
circuit and the ids weren't the same so the reference string didn't
match what the exporter generated. This commit fixes this by changing
the test to not do an exact string comparison, but instead a line by
line comparison that either does exact equality check or a regex search
for the expected line and the ids are checked as being any 15 character
integer.

* Remove superfluous comment

* Cache imported classes with GILOnceCell

* Remove unused python variables

* Add missing file

* Update QuantumCircuit gate methods to bypass Python object

This commit updates the QuantumCircuit gate methods which add a given
gate to the circuit to bypass the python gate object creation and
directly insert a rust representation of the gate. This avoids a
conversion in the rust side of the code. While in practice this is just
the Python side object creation and a getattr for the rust code to
determine it's a standard gate that we're skipping. This may add up over
time if there are a lot of gates being created by the method.

To accomplish this the rust code handling the mapping of rust
StandardGate variants to the Python classes that represent those gates
needed to be updated as well. By bypassing the python object creation
we need a fallback to populate the gate class for when a user access the
operation object from Python. Previously this mapping was only being
populated at insertion time and if we never insert the python object
(for a circuit created only via the methods) then we need a way to find
what the gate class is. A static lookup table of import paths and class names
are added to `qiskit_circuit::imports` module to faciliate this and
helper functions are added to facilitate interacting with the class
objects that represent each gate.

* Deduplicate gate matrix definitions

* Fix lint

* Attempt to fix qasm3 test failure

* Add compile time option to cache py gate returns for rust std gates

This commit adds a new rust crate feature flag for the qiskit-circuits
and qiskit-pyext that enables caching the output from
CircuitInstruction.operation to python space. Previously, for memory
efficiency we were reconstructing the python object on demand for every
access. This was to avoid carrying around an extra pointer and keeping
the ephemeral python object around longer term if it's only needed once.
But right now nothing is directly using the rust representation yet and
everything is accessing via the python interface, so recreating gate
objects on the fly has a huge performance penalty. To avoid that this
adds caching by default as a temporary solution to avoid this until we
have more usage of the rust representation of gates.

There is an inherent tension between an optimal rust representation
and something that is performant for Python access and there isn't a
clear cut answer on which one is better to optimize for. A build time
feature lets the user pick, if what we settle on for the default doesn't
agree with their priorities or use case. Personally I'd like to see us
disable the caching longer term (hopefully before releasing this
functionality), but that's dependent on a sufficent level of usage from
rust superseding the current Python space usage in the core of Qiskit.

* Add num_nonlocal_gates implementation in rust

This commit adds a native rust implementation to rust for the
num_nonlocal_gates method on QuantumCircuit. Now that we have a rust
representation of gates it is potentially faster to do the count because
the iteration and filtering is done rust side.

* Performance tuning circuit construction

This commit fixes some performance issues with the addition of standard
gates to a circuit. To workaround potential reference cycles in Python
when calling rust we need to check the parameters of the operation. This
was causing our fast path for standard gates to access the `operation`
attribute to get the parameters. This causes the gate to be eagerly
constructed on the getter. However, the reference cycle case can only
happen in situations without a standard gate, and the fast path for
adding standard gates directly won't need to run this so a skip is added
if we're adding a standard gate.

* Add back validation of parameters on gate methods

In the previous commit a side effect of the accidental eager operation
creation was that the parameter input for gates were being validated by
that. By fixing that in the previous commit the validation of input
parameters on the circuit methods was broken. This commit fixes that
oversight and adds back the validation.

* Skip validation on gate creation from rust

* Offload operation copying to rust

This commit fixes a performance regression in the
`QuantumCircuit.copy()` method which was previously using Python to copy
the operations which had extra overhead to go from rust to python and
vice versa. This moves that logic to exist in rust and improve the copy
performance.

* Fix lint

* Perform deepcopy in rust

This commit moves the deepcopy handling to occur solely in Rust.
Previously each instruction would be directly deepcopied by iterating
over the circuit data. However, we can do this rust side now and doing
this is more efficient because while we need to rely on Python to run a
deepcopy we can skip it for the Rust standard gates and rely on Rust to
copy those gates.

* Fix QuantumCircuit.compose() performance regression

This commit fixes a performance regression in the compose() method. This
was caused by the checking for classical conditions in the method
requiring eagerly converting all standard gates to a Python object. This
changes the logic to do this only if we know we have a condition (which
we can determine Python side now).

* Fix map_ops test case with no caching case

* Fix typos in docs

This commit fixes several docs typos that were caught during code review.

Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com>

* Shrink memory usage for extra mutable instruction state

This commit changes how we store the extra mutable instruction state
(condition, duration, unit, and label) for each `CircuitInstruction`
and `PackedInstruction` in the circuit. Previously it was all stored
as separate `Option<T>` fields on the struct, which required at least
a pointer's width for each field which was wasted space the majority of
the time as using these fields are not common. To optimize the memory
layout of the struct this moves these attributes to a new struct which
is put in an `Option<Box<_>>` which reduces it from 4 pointer widths
down to 1 per object. This comes from extra runtime cost from the extra
layer of pointer indirection but as this is the uncommon path this
tradeoff is fine.

* Remove Option<> from params field in CircuitInstruction

This commit removes the Option<> from the params field in
CircuitInstruction. There is no real distinction between an empty vec
and None in this case, so the option just added another layer in the API
that we didn't need to deal with. Also depending on the memory alignment
using an Option<T> might have ended up in a little extra memory usage
too, so removing it removes that potential source of overhead.

* Eagerly construct rust python wrappers in .append()

This commit updates the Python code in QuantumCircuit.append() method
to eagerly construct the rust wrapper objects for python defined circuit
operations.

* Simplify code around handling python errors in rust

* Revert "Skip validation on gate creation from rust"

This reverts commit 2f81bde8bf. The
validation skipping was unsound in some cases and could lead to invalid
circuit being generated. If we end up needing this as an optimization we
can remove this in the future in a follow-up PR that explores this in
isolation.

* Temporarily use git for qasm3 import

In Qiskit/qiskit-qasm3-import#34 the issue we're hitting caused by
qiskit-qasm3-import using the private circuit attributes removed in this
PR was fixed. This commit temporarily moves to installing it from git so
we can fully run CI. When qiskit-qasm3-import is released we should
revert this commit.

* Fix lint

* Fix lint for real (we really need to use a py312 compatible version of pylint)

* Fix test failure caused by incorrect lint fix

* Relax trait-method typing requirements

* Encapsulate `GILOnceCell` initialisers to local logic

* Simplify Interface for building circuit of standard gates in rust

* Simplify complex64 creation in gate_matrix.rs

This just switches Complex64::new(re, im) to be c64(re, im) to reduce
the amount of typing. c64 needs to be defined inplace so it can be a
const fn.

* Simplify initialization of array of elements that are not Copy (#28)

* Simplify initialization of array of elements that are not Copy

* Only generate array when necessary

* Fix doc typos

Co-authored-by: Kevin Hartman <kevin@hart.mn>

* Add conversion trait for OperationType -> OperationInput and simplify CircuitInstruction::replace()

* Use destructuring for operation_type_to_py extra attr handling

* Simplify trait bounds for map_indices()

The map_indices() method previously specified both Iterator and
ExactSizeIterator for it's trait bounds, but Iterator is a supertrait of
ExactSizeIterator and we don't need to explicitly list both. This commit
removes the duplicate trait bound.

* Make Qubit and Clbit newtype member public

As we start to use Qubit and Clbit for creating circuits from accelerate
and other crates in the Qiskit workspace we need to be able to create
instances of them. However, the newtype member BitType was not public
which prevented creating new Qubits. This commit fixes this by making it
public.

* Use snakecase for gate matrix names

* Remove pointless underscore prefix

* Use downcast instead of bound

* Rwork _append reference cycle handling

This commit reworks the multiple borrow handling in the _append() method
to leveraging `Bound.try_borrow()` to return a consistent error message
if we're unable to borrow a CircuitInstruction in the rust code meaning
there is a cyclical reference in the code. Previously we tried to detect
this cycle up-front which added significant overhead for a corner case.

* Make CircuitData.global_phase_param_index a class attr

* Use &[Param] instead of &SmallVec<..> for operation_type_and_data_to_py

* Have get_params_unsorted return a set

* Use lookup table for static property methods of StandardGate

* Use PyTuple::empty_bound()

* Fix lint

* Add missing test method docstring

* Reuse allocations in parameter table update

* Remove unnecessary global phase zeroing

* Move manually set params to a separate function

* Fix release note typo

* Use constant for global-phase index

* Switch requirement to release version

---------

Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com>
Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
Co-authored-by: John Lapeyre <jlapeyre@users.noreply.github.com>
Co-authored-by: Kevin Hartman <kevin@hart.mn>
This commit is contained in:
Matthew Treinish 2024-06-13 06:48:40 -04:00 committed by GitHub
parent 439de04e88
commit f304a4b4f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 3780 additions and 936 deletions

View File

@ -36,6 +36,15 @@ jobs:
python -m pip install -U -r requirements.txt -c constraints.txt
python -m pip install -U -r requirements-dev.txt -c constraints.txt
python -m pip install -c constraints.txt -e .
if: matrix.python-version == '3.10'
env:
QISKIT_NO_CACHE_GATES: 1
- name: 'Install dependencies'
run: |
python -m pip install -U -r requirements.txt -c constraints.txt
python -m pip install -U -r requirements-dev.txt -c constraints.txt
python -m pip install -c constraints.txt -e .
if: matrix.python-version == '3.12'
- name: 'Install optionals'
run: |
python -m pip install -r requirements-optional.txt -c constraints.txt

View File

@ -135,6 +135,18 @@ Note that in order to run `python setup.py ...` commands you need have build
dependency packages installed in your environment, which are listed in the
`pyproject.toml` file under the `[build-system]` section.
### Compile time options
When building qiskit from source there are options available to control how
Qiskit is built. Right now the only option is if you set the environment
variable `QISKIT_NO_CACHE_GATES=1` this will disable runtime caching of
Python gate objects when accessing them from a `QuantumCircuit` or `DAGCircuit`.
This makes a tradeoff between runtime performance for Python access and memory
overhead. Caching gates will result in better runtime for users of Python at
the cost of increased memory consumption. If you're working with any custom
transpiler passes written in python or are otherwise using a workflow that
repeatedly accesses the `operation` attribute of a `CircuitInstruction` or `op`
attribute of `DAGOpNode` enabling caching is recommended.
## Issues and pull requests

4
Cargo.lock generated
View File

@ -1196,7 +1196,11 @@ name = "qiskit-circuit"
version = "1.2.0"
dependencies = [
"hashbrown 0.14.5",
"ndarray",
"num-complex",
"numpy",
"pyo3",
"smallvec",
]
[[package]]

View File

@ -16,6 +16,11 @@ license = "Apache-2.0"
[workspace.dependencies]
indexmap.version = "2.2.6"
hashbrown.version = "0.14.0"
num-complex = "0.4"
ndarray = "^0.15.6"
numpy = "0.21.0"
smallvec = "1.13"
# Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an
# actual C extension (the feature disables linking in `libpython`, which is forbidden in Python
# distributions). We only activate that feature when building the C extension module; we still need

View File

@ -11,13 +11,13 @@ doctest = false
[dependencies]
rayon = "1.10"
numpy = "0.21.0"
numpy.workspace = true
rand = "0.8"
rand_pcg = "0.3"
rand_distr = "0.4.3"
ahash = "0.8.11"
num-traits = "0.2"
num-complex = "0.4"
num-complex.workspace = true
num-bigint = "0.4"
rustworkx-core = "0.14"
faer = "0.19.0"
@ -25,7 +25,7 @@ itertools = "0.13.0"
qiskit-circuit.workspace = true
[dependencies.smallvec]
version = "1.13"
workspace = true
features = ["union"]
[dependencies.pyo3]
@ -33,7 +33,7 @@ workspace = true
features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"]
[dependencies.ndarray]
version = "^0.15.6"
workspace = true
features = ["rayon", "approx-0_5"]
[dependencies.approx]

View File

@ -23,7 +23,7 @@ use itertools::Itertools;
use ndarray::prelude::*;
use numpy::{IntoPyArray, PyReadonlyArray1, PyReadonlyArray2};
use crate::two_qubit_decompose::ONE_QUBIT_IDENTITY;
use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY;
/// Find special unitary matrix that maps [c0,c1] to [r,0] or [0,r] if basis_state=0 or
/// basis_state=1 respectively

View File

@ -51,6 +51,7 @@ use rand::prelude::*;
use rand_distr::StandardNormal;
use rand_pcg::Pcg64Mcg;
use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE};
use qiskit_circuit::SliceOrInt;
const PI2: f64 = PI / 2.0;
@ -60,11 +61,6 @@ const TWO_PI: f64 = 2.0 * PI;
const C1: c64 = c64 { re: 1.0, im: 0.0 };
pub static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = [
[Complex64::new(1., 0.), Complex64::new(0., 0.)],
[Complex64::new(0., 0.), Complex64::new(1., 0.)],
];
static B_NON_NORMALIZED: [[Complex64; 4]; 4] = [
[
Complex64::new(1.0, 0.),
@ -342,54 +338,6 @@ fn rz_matrix(theta: f64) -> Array2<Complex64> {
]
}
static HGATE: [[Complex64; 2]; 2] = [
[
Complex64::new(FRAC_1_SQRT_2, 0.),
Complex64::new(FRAC_1_SQRT_2, 0.),
],
[
Complex64::new(FRAC_1_SQRT_2, 0.),
Complex64::new(-FRAC_1_SQRT_2, 0.),
],
];
static CXGATE: [[Complex64; 4]; 4] = [
[
Complex64::new(1., 0.),
Complex64::new(0., 0.),
Complex64::new(0., 0.),
Complex64::new(0., 0.),
],
[
Complex64::new(0., 0.),
Complex64::new(0., 0.),
Complex64::new(0., 0.),
Complex64::new(1., 0.),
],
[
Complex64::new(0., 0.),
Complex64::new(0., 0.),
Complex64::new(1., 0.),
Complex64::new(0., 0.),
],
[
Complex64::new(0., 0.),
Complex64::new(1., 0.),
Complex64::new(0., 0.),
Complex64::new(0., 0.),
],
];
static SXGATE: [[Complex64; 2]; 2] = [
[Complex64::new(0.5, 0.5), Complex64::new(0.5, -0.5)],
[Complex64::new(0.5, -0.5), Complex64::new(0.5, 0.5)],
];
static XGATE: [[Complex64; 2]; 2] = [
[Complex64::new(0., 0.), Complex64::new(1., 0.)],
[Complex64::new(1., 0.), Complex64::new(0., 0.)],
];
fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2<Complex64> {
let identity = aview2(&ONE_QUBIT_IDENTITY);
let phase = Complex64::new(0., global_phase).exp();
@ -402,10 +350,10 @@ fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2<
// sequence. If we get a different gate this is getting called
// by something else and is invalid.
let gate_matrix = match inst.0.as_ref() {
"sx" => aview2(&SXGATE).to_owned(),
"sx" => aview2(&SX_GATE).to_owned(),
"rz" => rz_matrix(inst.1[0]),
"cx" => aview2(&CXGATE).to_owned(),
"x" => aview2(&XGATE).to_owned(),
"cx" => aview2(&CX_GATE).to_owned(),
"x" => aview2(&X_GATE).to_owned(),
_ => unreachable!("Undefined gate"),
};
(gate_matrix, &inst.2)
@ -1481,7 +1429,7 @@ impl TwoQubitBasisDecomposer {
} else {
euler_matrix_q0 = rz_matrix(euler_q0[0][2] + euler_q0[1][0]).dot(&euler_matrix_q0);
}
euler_matrix_q0 = aview2(&HGATE).dot(&euler_matrix_q0);
euler_matrix_q0 = aview2(&H_GATE).dot(&euler_matrix_q0);
self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix_q0.view(), 0);
let rx_0 = rx_matrix(euler_q1[0][0]);
@ -1489,7 +1437,7 @@ impl TwoQubitBasisDecomposer {
let rx_1 = rx_matrix(euler_q1[0][2] + euler_q1[1][0]);
let mut euler_matrix_q1 = rz.dot(&rx_0);
euler_matrix_q1 = rx_1.dot(&euler_matrix_q1);
euler_matrix_q1 = aview2(&HGATE).dot(&euler_matrix_q1);
euler_matrix_q1 = aview2(&H_GATE).dot(&euler_matrix_q1);
self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix_q1.view(), 1);
gates.push(("cx".to_string(), smallvec![], smallvec![1, 0]));
@ -1550,12 +1498,12 @@ impl TwoQubitBasisDecomposer {
return None;
}
gates.push(("cx".to_string(), smallvec![], smallvec![1, 0]));
let mut euler_matrix = rz_matrix(euler_q0[2][2] + euler_q0[3][0]).dot(&aview2(&HGATE));
let mut euler_matrix = rz_matrix(euler_q0[2][2] + euler_q0[3][0]).dot(&aview2(&H_GATE));
euler_matrix = rx_matrix(euler_q0[3][1]).dot(&euler_matrix);
euler_matrix = rz_matrix(euler_q0[3][2]).dot(&euler_matrix);
self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix.view(), 0);
let mut euler_matrix = rx_matrix(euler_q1[2][2] + euler_q1[3][0]).dot(&aview2(&HGATE));
let mut euler_matrix = rx_matrix(euler_q1[2][2] + euler_q1[3][0]).dot(&aview2(&H_GATE));
euler_matrix = rz_matrix(euler_q1[3][1]).dot(&euler_matrix);
euler_matrix = rx_matrix(euler_q1[3][2]).dot(&euler_matrix);
self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix.view(), 1);

View File

@ -11,4 +11,17 @@ doctest = false
[dependencies]
hashbrown.workspace = true
pyo3.workspace = true
num-complex.workspace = true
ndarray.workspace = true
numpy.workspace = true
[dependencies.pyo3]
workspace = true
features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"]
[dependencies.smallvec]
workspace = true
features = ["union"]
[features]
cache_pygates = []

View File

@ -4,3 +4,66 @@ The Rust-based data structures for circuits.
This currently defines the core data collections for `QuantumCircuit`, but may expand in the future to back `DAGCircuit` as well.
This crate is a very low part of the Rust stack, if not the very lowest.
The data model exposed by this crate is as follows.
## CircuitData
The core representation of a quantum circuit in Rust is the `CircuitData` struct. This containts the list
of instructions that are comprising the circuit. Each element in this list is modeled by a
`CircuitInstruction` struct. The `CircuitInstruction` contains the operation object and it's operands.
This includes the parameters and bits. It also contains the potential mutable state of the Operation representation from the legacy Python data model; namely `duration`, `unit`, `condition`, and `label`.
In the future we'll be able to remove all of that except for label.
At rest a `CircuitInstruction` is compacted into a `PackedInstruction` which caches reused qargs
in the instructions to reduce the memory overhead of `CircuitData`. The `PackedInstruction` objects
get unpacked back to `CircuitInstruction` when accessed for a more convienent working form.
Additionally the `CircuitData` contains a `param_table` field which is used to track parameterized
instructions that are using python defined `ParameterExpression` objects for any parameters and also
a global phase field which is used to track the global phase of the circuit.
## Operation Model
In the circuit crate all the operations used in a `CircuitInstruction` are part of the `OperationType`
enum. The `OperationType` enum has four variants which are used to define the different types of
operation objects that can be on a circuit:
- `StandardGate`: a rust native representation of a member of the Qiskit standard gate library. This is
an `enum` that enumerates all the gates in the library and statically defines all the gate properties
except for gates that take parameters,
- `PyGate`: A struct that wraps a gate outside the standard library defined in Python. This struct wraps
a `Gate` instance (or subclass) as a `PyObject`. The static properties of this object (such as name,
number of qubits, etc) are stored in Rust for performance but the dynamic properties such as
the matrix or definition are accessed by calling back into Python to get them from the stored
`PyObject`
- `PyInstruction`: A struct that wraps an instruction defined in Python. This struct wraps an
`Instruction` instance (or subclass) as a `PyObject`. The static properties of this object (such as
name, number of qubits, etc) are stored in Rust for performance but the dynamic properties such as
the definition are accessed by calling back into Python to get them from the stored `PyObject`. As
the primary difference between `Gate` and `Instruction` in the python data model are that `Gate` is a
specialized `Instruction` subclass that represents unitary operations the primary difference between
this and `PyGate` are that `PyInstruction` will always return `None` when it's matrix is accessed.
- `PyOperation`: A struct that wraps an operation defined in Python. This struct wraps an `Operation`
instance (or subclass) as a `PyObject`. The static properties of this object (such as name, number
of qubits, etc) are stored in Rust for performance. As `Operation` is the base abstract interface
definition of what can be put on a circuit this is mostly just a container for custom Python objects.
Anything that's operating on a bare operation will likely need to access it via the `PyObject`
manually because the interface doesn't define many standard properties outside of what's cached in
the struct.
There is also an `Operation` trait defined which defines the common access pattern interface to these
4 types along with the `OperationType` parent. This trait defines methods to access the standard data
model attributes of operations in Qiskit. This includes things like the name, number of qubits, the matrix, the definition, etc.
## ParamTable
The `ParamTable` struct is used to track which circuit instructions are using `ParameterExpression`
objects for any of their parameters. The Python space `ParameterExpression` is comprised of a symengine
symbolic expression that defines operations using `Parameter` objects. Each `Parameter` is modeled by
a uuid and a name to uniquely identify it. The parameter table maps the `Parameter` objects to the
`CircuitInstruction` in the `CircuitData` that are using them. The `Parameter` comprised of 3 `HashMaps` internally that map the uuid (as `u128`, which is accesible in Python by using `uuid.int`) to the `ParamEntry`, the `name` to the uuid, and the uuid to the PyObject for the actual `Parameter`.
The `ParamEntry` is just a `HashSet` of 2-tuples with usize elements. The two usizes represent the instruction index in the `CircuitData` and the index of the `CircuitInstruction.params` field of
a give instruction where the given `Parameter` is used in the circuit. If the instruction index is
`GLOBAL_PHASE_MAX`, that points to the global phase property of the circuit instead of a `CircuitInstruction`.

View File

@ -12,7 +12,7 @@
use crate::BitType;
use hashbrown::HashMap;
use pyo3::exceptions::{PyRuntimeError, PyValueError};
use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::PyList;
use std::fmt::Debug;
@ -83,6 +83,15 @@ pub(crate) struct BitData<T> {
pub(crate) struct BitNotFoundError<'py>(pub(crate) Bound<'py, PyAny>);
impl<'py> From<BitNotFoundError<'py>> for PyErr {
fn from(error: BitNotFoundError) -> Self {
PyKeyError::new_err(format!(
"Bit {:?} has not been added to this circuit.",
error.0
))
}
}
impl<T> BitData<T>
where
T: From<BitType> + Copy,
@ -142,7 +151,7 @@ where
/// Map the provided native indices to the corresponding Python
/// bit instances.
/// Panics if any of the indices are out of range.
pub fn map_indices(&self, bits: &[T]) -> impl Iterator<Item = &Py<PyAny>> + ExactSizeIterator {
pub fn map_indices(&self, bits: &[T]) -> impl ExactSizeIterator<Item = &Py<PyAny>> {
let v: Vec<_> = bits.iter().map(|i| self.get(*i).unwrap()).collect();
v.into_iter()
}

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,45 @@
// that they have been altered from the originals.
use pyo3::basic::CompareOp;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::{PyList, PyTuple};
use pyo3::{PyObject, PyResult};
use pyo3::types::{IntoPyDict, PyList, PyTuple, PyType};
use pyo3::{intern, IntoPy, PyObject, PyResult};
use smallvec::{smallvec, SmallVec};
use crate::imports::{
get_std_gate_class, populate_std_gate_map, GATE, INSTRUCTION, OPERATION,
SINGLETON_CONTROLLED_GATE, SINGLETON_GATE,
};
use crate::interner::Index;
use crate::operations::{OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate};
/// These are extra mutable attributes for a circuit instruction's state. In general we don't
/// typically deal with this in rust space and the majority of the time they're not used in Python
/// space either. To save memory these are put in a separate struct and are stored inside a
/// `Box` on `CircuitInstruction` and `PackedInstruction`.
#[derive(Debug, Clone)]
pub struct ExtraInstructionAttributes {
pub label: Option<String>,
pub duration: Option<PyObject>,
pub unit: Option<String>,
pub condition: Option<PyObject>,
}
/// Private type used to store instructions with interned arg lists.
#[derive(Clone, Debug)]
pub(crate) struct PackedInstruction {
/// The Python-side operation instance.
pub op: OperationType,
/// The index under which the interner has stored `qubits`.
pub qubits_id: Index,
/// The index under which the interner has stored `clbits`.
pub clbits_id: Index,
pub params: SmallVec<[Param; 3]>,
pub extra_attrs: Option<Box<ExtraInstructionAttributes>>,
#[cfg(feature = "cache_pygates")]
pub py_op: Option<PyObject>,
}
/// A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and
/// various operands.
@ -47,28 +83,45 @@ use pyo3::{PyObject, PyResult};
/// mutations of the object do not invalidate the types, nor the restrictions placed on it by
/// its context. Typically this will mean, for example, that :attr:`qubits` must be a sequence
/// of distinct items, with no duplicates.
#[pyclass(
freelist = 20,
sequence,
get_all,
module = "qiskit._accelerate.circuit"
)]
#[pyclass(freelist = 20, sequence, module = "qiskit._accelerate.circuit")]
#[derive(Clone, Debug)]
pub struct CircuitInstruction {
/// The logical operation that this instruction represents an execution of.
pub operation: PyObject,
pub operation: OperationType,
/// A sequence of the qubits that the operation is applied to.
#[pyo3(get)]
pub qubits: Py<PyTuple>,
/// A sequence of the classical bits that this operation reads from or writes to.
#[pyo3(get)]
pub clbits: Py<PyTuple>,
pub params: SmallVec<[Param; 3]>,
pub extra_attrs: Option<Box<ExtraInstructionAttributes>>,
#[cfg(feature = "cache_pygates")]
pub py_op: Option<PyObject>,
}
/// This enum is for backwards compatibility if a user was doing something from
/// Python like CircuitInstruction(SXGate(), [qr[0]], []) by passing a python
/// gate object directly to a CircuitInstruction. In this case we need to
/// create a rust side object from the pyobject in CircuitInstruction.new()
/// With the `Object` variant which will convert the python object to a rust
/// `OperationType`
#[derive(FromPyObject, Debug)]
pub enum OperationInput {
Standard(StandardGate),
Gate(PyGate),
Instruction(PyInstruction),
Operation(PyOperation),
Object(PyObject),
}
impl CircuitInstruction {
pub fn new<T1, T2, U1, U2>(
py: Python,
operation: PyObject,
operation: OperationType,
qubits: impl IntoIterator<Item = T1, IntoIter = U1>,
clbits: impl IntoIterator<Item = T2, IntoIter = U2>,
params: SmallVec<[Param; 3]>,
extra_attrs: Option<Box<ExtraInstructionAttributes>>,
) -> Self
where
T1: ToPyObject,
@ -80,19 +133,41 @@ impl CircuitInstruction {
operation,
qubits: PyTuple::new_bound(py, qubits).unbind(),
clbits: PyTuple::new_bound(py, clbits).unbind(),
params,
extra_attrs,
#[cfg(feature = "cache_pygates")]
py_op: None,
}
}
}
impl From<OperationType> for OperationInput {
fn from(value: OperationType) -> Self {
match value {
OperationType::Standard(op) => Self::Standard(op),
OperationType::Gate(gate) => Self::Gate(gate),
OperationType::Instruction(inst) => Self::Instruction(inst),
OperationType::Operation(op) => Self::Operation(op),
}
}
}
#[pymethods]
impl CircuitInstruction {
#[allow(clippy::too_many_arguments)]
#[new]
#[pyo3(signature = (operation, qubits=None, clbits=None, params=smallvec![], label=None, duration=None, unit=None, condition=None))]
pub fn py_new(
py: Python<'_>,
operation: PyObject,
operation: OperationInput,
qubits: Option<&Bound<PyAny>>,
clbits: Option<&Bound<PyAny>>,
) -> PyResult<Py<Self>> {
params: SmallVec<[Param; 3]>,
label: Option<String>,
duration: Option<PyObject>,
unit: Option<String>,
condition: Option<PyObject>,
) -> PyResult<Self> {
fn as_tuple(py: Python<'_>, seq: Option<&Bound<PyAny>>) -> PyResult<Py<PyTuple>> {
match seq {
None => Ok(PyTuple::empty_bound(py).unbind()),
@ -116,14 +191,136 @@ impl CircuitInstruction {
}
}
Py::new(
py,
CircuitInstruction {
operation,
qubits: as_tuple(py, qubits)?,
clbits: as_tuple(py, clbits)?,
},
)
let extra_attrs =
if label.is_some() || duration.is_some() || unit.is_some() || condition.is_some() {
Some(Box::new(ExtraInstructionAttributes {
label,
duration,
unit,
condition,
}))
} else {
None
};
match operation {
OperationInput::Standard(operation) => {
let operation = OperationType::Standard(operation);
Ok(CircuitInstruction {
operation,
qubits: as_tuple(py, qubits)?,
clbits: as_tuple(py, clbits)?,
params,
extra_attrs,
#[cfg(feature = "cache_pygates")]
py_op: None,
})
}
OperationInput::Gate(operation) => {
let operation = OperationType::Gate(operation);
Ok(CircuitInstruction {
operation,
qubits: as_tuple(py, qubits)?,
clbits: as_tuple(py, clbits)?,
params,
extra_attrs,
#[cfg(feature = "cache_pygates")]
py_op: None,
})
}
OperationInput::Instruction(operation) => {
let operation = OperationType::Instruction(operation);
Ok(CircuitInstruction {
operation,
qubits: as_tuple(py, qubits)?,
clbits: as_tuple(py, clbits)?,
params,
extra_attrs,
#[cfg(feature = "cache_pygates")]
py_op: None,
})
}
OperationInput::Operation(operation) => {
let operation = OperationType::Operation(operation);
Ok(CircuitInstruction {
operation,
qubits: as_tuple(py, qubits)?,
clbits: as_tuple(py, clbits)?,
params,
extra_attrs,
#[cfg(feature = "cache_pygates")]
py_op: None,
})
}
OperationInput::Object(old_op) => {
let op = convert_py_to_operation_type(py, old_op.clone_ref(py))?;
let extra_attrs = if op.label.is_some()
|| op.duration.is_some()
|| op.unit.is_some()
|| op.condition.is_some()
{
Some(Box::new(ExtraInstructionAttributes {
label: op.label,
duration: op.duration,
unit: op.unit,
condition: op.condition,
}))
} else {
None
};
match op.operation {
OperationType::Standard(operation) => {
let operation = OperationType::Standard(operation);
Ok(CircuitInstruction {
operation,
qubits: as_tuple(py, qubits)?,
clbits: as_tuple(py, clbits)?,
params: op.params,
extra_attrs,
#[cfg(feature = "cache_pygates")]
py_op: Some(old_op.clone_ref(py)),
})
}
OperationType::Gate(operation) => {
let operation = OperationType::Gate(operation);
Ok(CircuitInstruction {
operation,
qubits: as_tuple(py, qubits)?,
clbits: as_tuple(py, clbits)?,
params: op.params,
extra_attrs,
#[cfg(feature = "cache_pygates")]
py_op: Some(old_op.clone_ref(py)),
})
}
OperationType::Instruction(operation) => {
let operation = OperationType::Instruction(operation);
Ok(CircuitInstruction {
operation,
qubits: as_tuple(py, qubits)?,
clbits: as_tuple(py, clbits)?,
params: op.params,
extra_attrs,
#[cfg(feature = "cache_pygates")]
py_op: Some(old_op.clone_ref(py)),
})
}
OperationType::Operation(operation) => {
let operation = OperationType::Operation(operation);
Ok(CircuitInstruction {
operation,
qubits: as_tuple(py, qubits)?,
clbits: as_tuple(py, clbits)?,
params: op.params,
extra_attrs,
#[cfg(feature = "cache_pygates")]
py_op: Some(old_op.clone_ref(py)),
})
}
}
}
}
}
/// Returns a shallow copy.
@ -134,28 +331,127 @@ impl CircuitInstruction {
self.clone()
}
/// The logical operation that this instruction represents an execution of.
#[cfg(not(feature = "cache_pygates"))]
#[getter]
pub fn operation(&self, py: Python) -> PyResult<PyObject> {
operation_type_to_py(py, self)
}
#[cfg(feature = "cache_pygates")]
#[getter]
pub fn operation(&mut self, py: Python) -> PyResult<PyObject> {
Ok(match &self.py_op {
Some(op) => op.clone_ref(py),
None => {
let op = operation_type_to_py(py, self)?;
self.py_op = Some(op.clone_ref(py));
op
}
})
}
/// Creates a shallow copy with the given fields replaced.
///
/// Returns:
/// CircuitInstruction: A new instance with the given fields replaced.
#[allow(clippy::too_many_arguments)]
pub fn replace(
&self,
py: Python<'_>,
operation: Option<PyObject>,
operation: Option<OperationInput>,
qubits: Option<&Bound<PyAny>>,
clbits: Option<&Bound<PyAny>>,
) -> PyResult<Py<Self>> {
params: Option<SmallVec<[Param; 3]>>,
label: Option<String>,
duration: Option<PyObject>,
unit: Option<String>,
condition: Option<PyObject>,
) -> PyResult<Self> {
let operation = operation.unwrap_or_else(|| self.operation.clone().into());
let params = match params {
Some(params) => params,
None => self.params.clone(),
};
let label = match label {
Some(label) => Some(label),
None => match &self.extra_attrs {
Some(extra_attrs) => extra_attrs.label.clone(),
None => None,
},
};
let duration = match duration {
Some(duration) => Some(duration),
None => match &self.extra_attrs {
Some(extra_attrs) => extra_attrs.duration.clone(),
None => None,
},
};
let unit: Option<String> = match unit {
Some(unit) => Some(unit),
None => match &self.extra_attrs {
Some(extra_attrs) => extra_attrs.unit.clone(),
None => None,
},
};
let condition: Option<PyObject> = match condition {
Some(condition) => Some(condition),
None => match &self.extra_attrs {
Some(extra_attrs) => extra_attrs.condition.clone(),
None => None,
},
};
CircuitInstruction::py_new(
py,
operation.unwrap_or_else(|| self.operation.clone_ref(py)),
operation,
Some(qubits.unwrap_or_else(|| self.qubits.bind(py))),
Some(clbits.unwrap_or_else(|| self.clbits.bind(py))),
params,
label,
duration,
unit,
condition,
)
}
fn __getnewargs__(&self, py: Python<'_>) -> PyResult<PyObject> {
fn __getstate__(&self, py: Python<'_>) -> PyResult<PyObject> {
Ok((
self.operation.bind(py),
operation_type_to_py(py, self)?,
self.qubits.bind(py),
self.clbits.bind(py),
)
.into_py(py))
}
fn __setstate__(&mut self, py: Python<'_>, state: &Bound<PyTuple>) -> PyResult<()> {
let op = convert_py_to_operation_type(py, state.get_item(0)?.into())?;
self.operation = op.operation;
self.params = op.params;
self.qubits = state.get_item(1)?.extract()?;
self.clbits = state.get_item(2)?.extract()?;
if op.label.is_some()
|| op.duration.is_some()
|| op.unit.is_some()
|| op.condition.is_some()
{
self.extra_attrs = Some(Box::new(ExtraInstructionAttributes {
label: op.label,
duration: op.duration,
unit: op.unit,
condition: op.condition,
}));
}
Ok(())
}
pub fn __getnewargs__(&self, py: Python<'_>) -> PyResult<PyObject> {
Ok((
operation_type_to_py(py, self)?,
self.qubits.bind(py),
self.clbits.bind(py),
)
@ -172,7 +468,7 @@ impl CircuitInstruction {
, clbits={}\
)",
type_name,
r.operation.bind(py).repr()?,
operation_type_to_py(py, &r)?,
r.qubits.bind(py).repr()?,
r.clbits.bind(py).repr()?
))
@ -184,23 +480,50 @@ impl CircuitInstruction {
// the interface to behave exactly like the old 3-tuple `(inst, qargs, cargs)` if it's treated
// like that via unpacking or similar. That means that the `parameters` field is completely
// absent, and the qubits and clbits must be converted to lists.
pub fn _legacy_format<'py>(&self, py: Python<'py>) -> Bound<'py, PyTuple> {
PyTuple::new_bound(
#[cfg(not(feature = "cache_pygates"))]
pub fn _legacy_format<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
let op = operation_type_to_py(py, self)?;
Ok(PyTuple::new_bound(
py,
[
self.operation.bind(py),
&self.qubits.bind(py).to_list(),
&self.clbits.bind(py).to_list(),
],
)
[op, self.qubits.to_object(py), self.clbits.to_object(py)],
))
}
#[cfg(feature = "cache_pygates")]
pub fn _legacy_format<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
let op = match &self.py_op {
Some(op) => op.clone_ref(py),
None => {
let op = operation_type_to_py(py, self)?;
self.py_op = Some(op.clone_ref(py));
op
}
};
Ok(PyTuple::new_bound(
py,
[op, self.qubits.to_object(py), self.clbits.to_object(py)],
))
}
#[cfg(not(feature = "cache_pygates"))]
pub fn __getitem__(&self, py: Python<'_>, key: &Bound<PyAny>) -> PyResult<PyObject> {
Ok(self._legacy_format(py).as_any().get_item(key)?.into_py(py))
Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py))
}
#[cfg(feature = "cache_pygates")]
pub fn __getitem__(&mut self, py: Python<'_>, key: &Bound<PyAny>) -> PyResult<PyObject> {
Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py))
}
#[cfg(not(feature = "cache_pygates"))]
pub fn __iter__(&self, py: Python<'_>) -> PyResult<PyObject> {
Ok(self._legacy_format(py).as_any().iter()?.into_py(py))
Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py))
}
#[cfg(feature = "cache_pygates")]
pub fn __iter__(&mut self, py: Python<'_>) -> PyResult<PyObject> {
Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py))
}
pub fn __len__(&self) -> usize {
@ -227,16 +550,94 @@ impl CircuitInstruction {
let other: PyResult<Bound<CircuitInstruction>> = other.extract();
return other.map_or(Ok(Some(false)), |v| {
let v = v.try_borrow()?;
let op_eq = match &self_.operation {
OperationType::Standard(op) => {
if let OperationType::Standard(other) = &v.operation {
if op != other {
false
} else {
let other_params = &v.params;
let mut out = true;
for (param_a, param_b) in self_.params.iter().zip(other_params)
{
match param_a {
Param::Float(val_a) => {
if let Param::Float(val_b) = param_b {
if val_a != val_b {
out = false;
break;
}
} else {
out = false;
break;
}
}
Param::ParameterExpression(val_a) => {
if let Param::ParameterExpression(val_b) = param_b {
if !val_a.bind(py).eq(val_b.bind(py))? {
out = false;
break;
}
} else {
out = false;
break;
}
}
Param::Obj(val_a) => {
if let Param::Obj(val_b) = param_b {
if !val_a.bind(py).eq(val_b.bind(py))? {
out = false;
break;
}
} else {
out = false;
break;
}
}
}
}
out
}
} else {
false
}
}
OperationType::Gate(op) => {
if let OperationType::Gate(other) = &v.operation {
op.gate.bind(py).eq(other.gate.bind(py))?
} else {
false
}
}
OperationType::Instruction(op) => {
if let OperationType::Instruction(other) = &v.operation {
op.instruction.bind(py).eq(other.instruction.bind(py))?
} else {
false
}
}
OperationType::Operation(op) => {
if let OperationType::Operation(other) = &v.operation {
op.operation.bind(py).eq(other.operation.bind(py))?
} else {
false
}
}
};
Ok(Some(
self_.clbits.bind(py).eq(v.clbits.bind(py))?
&& self_.qubits.bind(py).eq(v.qubits.bind(py))?
&& self_.operation.bind(py).eq(v.operation.bind(py))?,
&& op_eq,
))
});
}
if other.is_instance_of::<PyTuple>() {
return Ok(Some(self_._legacy_format(py).eq(other)?));
#[cfg(feature = "cache_pygates")]
let mut self_ = self_.clone();
let legacy_format = self_._legacy_format(py)?;
return Ok(Some(legacy_format.eq(other)?));
}
Ok(None)
@ -255,3 +656,222 @@ impl CircuitInstruction {
}
}
}
/// Take a reference to a `CircuitInstruction` and convert the operation
/// inside that to a python side object.
pub(crate) fn operation_type_to_py(
py: Python,
circuit_inst: &CircuitInstruction,
) -> PyResult<PyObject> {
let (label, duration, unit, condition) = match &circuit_inst.extra_attrs {
None => (None, None, None, None),
Some(extra_attrs) => (
extra_attrs.label.clone(),
extra_attrs.duration.clone(),
extra_attrs.unit.clone(),
extra_attrs.condition.clone(),
),
};
operation_type_and_data_to_py(
py,
&circuit_inst.operation,
&circuit_inst.params,
&label,
&duration,
&unit,
&condition,
)
}
/// Take an OperationType and the other mutable state fields from a
/// rust instruction representation and return a PyObject representing
/// a Python side full-fat Qiskit operation as a PyObject. This is typically
/// used by accessor functions that need to return an operation to Qiskit, such
/// as accesing `CircuitInstruction.operation`.
pub(crate) fn operation_type_and_data_to_py(
py: Python,
operation: &OperationType,
params: &[Param],
label: &Option<String>,
duration: &Option<PyObject>,
unit: &Option<String>,
condition: &Option<PyObject>,
) -> PyResult<PyObject> {
match &operation {
OperationType::Standard(op) => {
let gate_class: &PyObject = &get_std_gate_class(py, *op)?;
let args = if params.is_empty() {
PyTuple::empty_bound(py)
} else {
PyTuple::new_bound(py, params)
};
let kwargs = [
("label", label.to_object(py)),
("unit", unit.to_object(py)),
("duration", duration.to_object(py)),
]
.into_py_dict_bound(py);
let mut out = gate_class.call_bound(py, args, Some(&kwargs))?;
if condition.is_some() {
out = out.call_method0(py, "to_mutable")?;
out.setattr(py, "condition", condition.to_object(py))?;
}
Ok(out)
}
OperationType::Gate(gate) => Ok(gate.gate.clone_ref(py)),
OperationType::Instruction(inst) => Ok(inst.instruction.clone_ref(py)),
OperationType::Operation(op) => Ok(op.operation.clone_ref(py)),
}
}
/// A container struct that contains the output from the Python object to
/// conversion to construct a CircuitInstruction object
#[derive(Debug)]
pub(crate) struct OperationTypeConstruct {
pub operation: OperationType,
pub params: SmallVec<[Param; 3]>,
pub label: Option<String>,
pub duration: Option<PyObject>,
pub unit: Option<String>,
pub condition: Option<PyObject>,
}
/// Convert an inbound Python object for a Qiskit operation and build a rust
/// representation of that operation. This will map it to appropriate variant
/// of operation type based on class
pub(crate) fn convert_py_to_operation_type(
py: Python,
py_op: PyObject,
) -> PyResult<OperationTypeConstruct> {
let attr = intern!(py, "_standard_gate");
let py_op_bound = py_op.clone_ref(py).into_bound(py);
// Get PyType from either base_class if it exists, or if not use the
// class/type info from the pyobject
let binding = py_op_bound.getattr(intern!(py, "base_class")).ok();
let op_obj = py_op_bound.get_type();
let raw_op_type: Py<PyType> = match binding {
Some(base_class) => base_class.downcast()?.clone().unbind(),
None => op_obj.unbind(),
};
let op_type: Bound<PyType> = raw_op_type.into_bound(py);
let mut standard: Option<StandardGate> = match op_type.getattr(attr) {
Ok(stdgate) => match stdgate.extract().ok() {
Some(gate) => gate,
None => None,
},
Err(_) => None,
};
// If the input instruction is a standard gate and a singleton instance
// we should check for mutable state. A mutable instance should be treated
// as a custom gate not a standard gate because it has custom properties.
//
// In the futuer we can revisit this when we've dropped `duration`, `unit`,
// and `condition` from the api as we should own the label in the
// `CircuitInstruction`. The other piece here is for controlled gates there
// is the control state, so for `SingletonControlledGates` we'll still need
// this check.
if standard.is_some() {
let mutable: bool = py_op.getattr(py, intern!(py, "mutable"))?.extract(py)?;
if mutable
&& (py_op_bound.is_instance(SINGLETON_GATE.get_bound(py))?
|| py_op_bound.is_instance(SINGLETON_CONTROLLED_GATE.get_bound(py))?)
{
standard = None;
}
}
if let Some(op) = standard {
let base_class = op_type.to_object(py);
populate_std_gate_map(py, op, base_class);
return Ok(OperationTypeConstruct {
operation: OperationType::Standard(op),
params: py_op.getattr(py, intern!(py, "params"))?.extract(py)?,
label: py_op.getattr(py, intern!(py, "label"))?.extract(py)?,
duration: py_op.getattr(py, intern!(py, "duration"))?.extract(py)?,
unit: py_op.getattr(py, intern!(py, "unit"))?.extract(py)?,
condition: py_op.getattr(py, intern!(py, "condition"))?.extract(py)?,
});
}
if op_type.is_subclass(GATE.get_bound(py))? {
let params = py_op.getattr(py, intern!(py, "params"))?.extract(py)?;
let label = py_op.getattr(py, intern!(py, "label"))?.extract(py)?;
let duration = py_op.getattr(py, intern!(py, "duration"))?.extract(py)?;
let unit = py_op.getattr(py, intern!(py, "unit"))?.extract(py)?;
let condition = py_op.getattr(py, intern!(py, "condition"))?.extract(py)?;
let out_op = PyGate {
qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?,
clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?,
params: py_op
.getattr(py, intern!(py, "params"))?
.downcast_bound::<PyList>(py)?
.len() as u32,
op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?,
gate: py_op,
};
return Ok(OperationTypeConstruct {
operation: OperationType::Gate(out_op),
params,
label,
duration,
unit,
condition,
});
}
if op_type.is_subclass(INSTRUCTION.get_bound(py))? {
let params = py_op.getattr(py, intern!(py, "params"))?.extract(py)?;
let label = py_op.getattr(py, intern!(py, "label"))?.extract(py)?;
let duration = py_op.getattr(py, intern!(py, "duration"))?.extract(py)?;
let unit = py_op.getattr(py, intern!(py, "unit"))?.extract(py)?;
let condition = py_op.getattr(py, intern!(py, "condition"))?.extract(py)?;
let out_op = PyInstruction {
qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?,
clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?,
params: py_op
.getattr(py, intern!(py, "params"))?
.downcast_bound::<PyList>(py)?
.len() as u32,
op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?,
instruction: py_op,
};
return Ok(OperationTypeConstruct {
operation: OperationType::Instruction(out_op),
params,
label,
duration,
unit,
condition,
});
}
if op_type.is_subclass(OPERATION.get_bound(py))? {
let params = match py_op.getattr(py, intern!(py, "params")) {
Ok(value) => value.extract(py)?,
Err(_) => smallvec![],
};
let label = None;
let duration = None;
let unit = None;
let condition = None;
let out_op = PyOperation {
qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?,
clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?,
params: match py_op.getattr(py, intern!(py, "params")) {
Ok(value) => value.downcast_bound::<PyList>(py)?.len() as u32,
Err(_) => 0,
},
op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?,
operation: py_op,
};
return Ok(OperationTypeConstruct {
operation: OperationType::Operation(out_op),
params,
label,
duration,
unit,
condition,
});
}
Err(PyValueError::new_err(format!("Invalid input: {}", py_op)))
}

View File

@ -10,7 +10,11 @@
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
use crate::circuit_instruction::CircuitInstruction;
use crate::circuit_instruction::{
convert_py_to_operation_type, operation_type_to_py, CircuitInstruction,
ExtraInstructionAttributes,
};
use crate::operations::Operation;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple};
use pyo3::{intern, PyObject, PyResult};
@ -106,13 +110,33 @@ impl DAGOpNode {
}
None => qargs.str()?.into_any(),
};
let res = convert_py_to_operation_type(py, op.clone_ref(py))?;
let extra_attrs = if res.label.is_some()
|| res.duration.is_some()
|| res.unit.is_some()
|| res.condition.is_some()
{
Some(Box::new(ExtraInstructionAttributes {
label: res.label,
duration: res.duration,
unit: res.unit,
condition: res.condition,
}))
} else {
None
};
Ok((
DAGOpNode {
instruction: CircuitInstruction {
operation: op,
operation: res.operation,
qubits: qargs.unbind(),
clbits: cargs.unbind(),
params: res.params,
extra_attrs,
#[cfg(feature = "cache_pygates")]
py_op: Some(op),
},
sort_key: sort_key.unbind(),
},
@ -120,18 +144,18 @@ impl DAGOpNode {
))
}
fn __reduce__(slf: PyRef<Self>, py: Python) -> PyObject {
fn __reduce__(slf: PyRef<Self>, py: Python) -> PyResult<PyObject> {
let state = (slf.as_ref()._node_id, &slf.sort_key);
(
Ok((
py.get_type_bound::<Self>(),
(
&slf.instruction.operation,
operation_type_to_py(py, &slf.instruction)?,
&slf.instruction.qubits,
&slf.instruction.clbits,
),
state,
)
.into_py(py)
.into_py(py))
}
fn __setstate__(mut slf: PyRefMut<Self>, state: &Bound<PyAny>) -> PyResult<()> {
@ -142,13 +166,31 @@ impl DAGOpNode {
}
#[getter]
fn get_op(&self, py: Python) -> PyObject {
self.instruction.operation.clone_ref(py)
fn get_op(&self, py: Python) -> PyResult<PyObject> {
operation_type_to_py(py, &self.instruction)
}
#[setter]
fn set_op(&mut self, op: PyObject) {
self.instruction.operation = op;
fn set_op(&mut self, py: Python, op: PyObject) -> PyResult<()> {
let res = convert_py_to_operation_type(py, op)?;
self.instruction.operation = res.operation;
self.instruction.params = res.params;
let extra_attrs = if res.label.is_some()
|| res.duration.is_some()
|| res.unit.is_some()
|| res.condition.is_some()
{
Some(Box::new(ExtraInstructionAttributes {
label: res.label,
duration: res.duration,
unit: res.unit,
condition: res.condition,
}))
} else {
None
};
self.instruction.extra_attrs = extra_attrs;
Ok(())
}
#[getter]
@ -173,29 +215,27 @@ impl DAGOpNode {
/// Returns the Instruction name corresponding to the op for this node
#[getter]
fn get_name(&self, py: Python) -> PyResult<PyObject> {
Ok(self
.instruction
.operation
.bind(py)
.getattr(intern!(py, "name"))?
.unbind())
fn get_name(&self, py: Python) -> PyObject {
self.instruction.operation.name().to_object(py)
}
/// Sets the Instruction name corresponding to the op for this node
#[setter]
fn set_name(&self, py: Python, new_name: PyObject) -> PyResult<()> {
self.instruction
.operation
.bind(py)
.setattr(intern!(py, "name"), new_name)
fn set_name(&mut self, py: Python, new_name: PyObject) -> PyResult<()> {
let op = operation_type_to_py(py, &self.instruction)?;
op.bind(py).setattr(intern!(py, "name"), new_name)?;
let res = convert_py_to_operation_type(py, op)?;
self.instruction.operation = res.operation;
Ok(())
}
/// Returns a representation of the DAGOpNode
fn __repr__(&self, py: Python) -> PyResult<String> {
Ok(format!(
"DAGOpNode(op={}, qargs={}, cargs={})",
self.instruction.operation.bind(py).repr()?,
operation_type_to_py(py, &self.instruction)?
.bind(py)
.repr()?,
self.instruction.qubits.bind(py).repr()?,
self.instruction.clbits.bind(py).repr()?
))

View File

@ -0,0 +1,224 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2023
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
use num_complex::Complex64;
use std::f64::consts::FRAC_1_SQRT_2;
// num-complex exposes an equivalent function but it's not a const function
// so it's not compatible with static definitions. This is a const func and
// just reduces the amount of typing we need.
#[inline(always)]
const fn c64(re: f64, im: f64) -> Complex64 {
Complex64::new(re, im)
}
pub static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] =
[[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(1., 0.)]];
#[inline]
pub fn rx_gate(theta: f64) -> [[Complex64; 2]; 2] {
let half_theta = theta / 2.;
let cos = c64(half_theta.cos(), 0.);
let isin = c64(0., -half_theta.sin());
[[cos, isin], [isin, cos]]
}
#[inline]
pub fn ry_gate(theta: f64) -> [[Complex64; 2]; 2] {
let half_theta = theta / 2.;
let cos = c64(half_theta.cos(), 0.);
let sin = c64(half_theta.sin(), 0.);
[[cos, -sin], [sin, cos]]
}
#[inline]
pub fn rz_gate(theta: f64) -> [[Complex64; 2]; 2] {
let ilam2 = c64(0., 0.5 * theta);
[[(-ilam2).exp(), c64(0., 0.)], [c64(0., 0.), ilam2.exp()]]
}
pub static H_GATE: [[Complex64; 2]; 2] = [
[c64(FRAC_1_SQRT_2, 0.), c64(FRAC_1_SQRT_2, 0.)],
[c64(FRAC_1_SQRT_2, 0.), c64(-FRAC_1_SQRT_2, 0.)],
];
pub static CX_GATE: [[Complex64; 4]; 4] = [
[c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)],
[c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)],
[c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)],
[c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)],
];
pub static SX_GATE: [[Complex64; 2]; 2] = [
[c64(0.5, 0.5), c64(0.5, -0.5)],
[c64(0.5, -0.5), c64(0.5, 0.5)],
];
pub static X_GATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(1., 0.)], [c64(1., 0.), c64(0., 0.)]];
pub static Z_GATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(-1., 0.)]];
pub static Y_GATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(0., -1.)], [c64(0., 1.), c64(0., 0.)]];
pub static CZ_GATE: [[Complex64; 4]; 4] = [
[c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)],
[c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)],
[c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)],
[c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(-1., 0.)],
];
pub static CY_GATE: [[Complex64; 4]; 4] = [
[c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)],
[c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(0., -1.)],
[c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)],
[c64(0., 0.), c64(0., 1.), c64(0., 0.), c64(0., 0.)],
];
pub static CCX_GATE: [[Complex64; 8]; 8] = [
[
c64(1., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
],
[
c64(0., 0.),
c64(1., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
],
[
c64(0., 0.),
c64(0., 0.),
c64(1., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
],
[
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(1., 0.),
],
[
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(1., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
],
[
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(1., 0.),
c64(0., 0.),
c64(0., 0.),
],
[
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(1., 0.),
c64(0., 0.),
],
[
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(1., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
c64(0., 0.),
],
];
pub static ECR_GATE: [[Complex64; 4]; 4] = [
[
c64(0., 0.),
c64(FRAC_1_SQRT_2, 0.),
c64(0., 0.),
c64(0., FRAC_1_SQRT_2),
],
[
c64(FRAC_1_SQRT_2, 0.),
c64(0., 0.),
c64(0., -FRAC_1_SQRT_2),
c64(0., 0.),
],
[
c64(0., 0.),
c64(0., FRAC_1_SQRT_2),
c64(0., 0.),
c64(FRAC_1_SQRT_2, 0.),
],
[
c64(0., -FRAC_1_SQRT_2),
c64(0., 0.),
c64(FRAC_1_SQRT_2, 0.),
c64(0., 0.),
],
];
pub static SWAP_GATE: [[Complex64; 4]; 4] = [
[c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)],
[c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)],
[c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)],
[c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)],
];
#[inline]
pub fn global_phase_gate(theta: f64) -> [[Complex64; 1]; 1] {
[[c64(0., theta).exp()]]
}
#[inline]
pub fn phase_gate(lam: f64) -> [[Complex64; 2]; 2] {
[
[c64(1., 0.), c64(0., 0.)],
[c64(0., 0.), c64(0., lam).exp()],
]
}
#[inline]
pub fn u_gate(theta: f64, phi: f64, lam: f64) -> [[Complex64; 2]; 2] {
let cos = (theta / 2.).cos();
let sin = (theta / 2.).sin();
[
[c64(cos, 0.), (-c64(0., lam).exp()) * sin],
[c64(0., phi).exp() * sin, c64(0., phi + lam).exp() * cos],
]
}

View File

@ -0,0 +1,168 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2024
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
// This module contains objects imported from Python that are reused. These are
// typically data model classes that are used to identify an object, or for
// python side casting
use pyo3::prelude::*;
use pyo3::sync::GILOnceCell;
use crate::operations::{StandardGate, STANDARD_GATE_SIZE};
/// Helper wrapper around `GILOnceCell` instances that are just intended to store a Python object
/// that is lazily imported.
pub struct ImportOnceCell {
module: &'static str,
object: &'static str,
cell: GILOnceCell<Py<PyAny>>,
}
impl ImportOnceCell {
const fn new(module: &'static str, object: &'static str) -> Self {
Self {
module,
object,
cell: GILOnceCell::new(),
}
}
/// Get the underlying GIL-independent reference to the contained object, importing if
/// required.
#[inline]
pub fn get(&self, py: Python) -> &Py<PyAny> {
self.cell.get_or_init(py, || {
py.import_bound(self.module)
.unwrap()
.getattr(self.object)
.unwrap()
.unbind()
})
}
/// Get a GIL-bound reference to the contained object, importing if required.
#[inline]
pub fn get_bound<'py>(&self, py: Python<'py>) -> &Bound<'py, PyAny> {
self.get(py).bind(py)
}
}
pub static BUILTIN_LIST: ImportOnceCell = ImportOnceCell::new("builtins", "list");
pub static OPERATION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.operation", "Operation");
pub static INSTRUCTION: ImportOnceCell =
ImportOnceCell::new("qiskit.circuit.instruction", "Instruction");
pub static GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.gate", "Gate");
pub static QUBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.quantumregister", "Qubit");
pub static CLBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.classicalregister", "Clbit");
pub static PARAMETER_EXPRESSION: ImportOnceCell =
ImportOnceCell::new("qiskit.circuit.parameterexpression", "ParameterExpression");
pub static QUANTUM_CIRCUIT: ImportOnceCell =
ImportOnceCell::new("qiskit.circuit.quantumcircuit", "QuantumCircuit");
pub static SINGLETON_GATE: ImportOnceCell =
ImportOnceCell::new("qiskit.circuit.singleton", "SingletonGate");
pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell =
ImportOnceCell::new("qiskit.circuit.singleton", "SingletonControlledGate");
/// A mapping from the enum variant in crate::operations::StandardGate to the python
/// module path and class name to import it. This is used to populate the conversion table
/// when a gate is added directly via the StandardGate path and there isn't a Python object
/// to poll the _standard_gate attribute for.
///
/// NOTE: the order here is significant it must match the StandardGate variant's number must match
/// index of it's entry in this table. This is all done statically for performance
static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [
// ZGate = 0
["qiskit.circuit.library.standard_gates.z", "ZGate"],
// YGate = 1
["qiskit.circuit.library.standard_gates.y", "YGate"],
// XGate = 2
["qiskit.circuit.library.standard_gates.x", "XGate"],
// CZGate = 3
["qiskit.circuit.library.standard_gates.z", "CZGate"],
// CYGate = 4
["qiskit.circuit.library.standard_gates.y", "CYGate"],
// CXGate = 5
["qiskit.circuit.library.standard_gates.x", "CXGate"],
// CCXGate = 6
["qiskit.circuit.library.standard_gates.x", "CCXGate"],
// RXGate = 7
["qiskit.circuit.library.standard_gates.rx", "RXGate"],
// RYGate = 8
["qiskit.circuit.library.standard_gates.ry", "RYGate"],
// RZGate = 9
["qiskit.circuit.library.standard_gates.rz", "RZGate"],
// ECRGate = 10
["qiskit.circuit.library.standard_gates.ecr", "ECRGate"],
// SwapGate = 11
["qiskit.circuit.library.standard_gates.swap", "SwapGate"],
// SXGate = 12
["qiskit.circuit.library.standard_gates.sx", "SXGate"],
// GlobalPhaseGate = 13
[
"qiskit.circuit.library.standard_gates.global_phase",
"GlobalPhaseGate",
],
// IGate = 14
["qiskit.circuit.library.standard_gates.i", "IGate"],
// HGate = 15
["qiskit.circuit.library.standard_gates.h", "HGate"],
// PhaseGate = 16
["qiskit.circuit.library.standard_gates.p", "PhaseGate"],
// UGate = 17
["qiskit.circuit.library.standard_gates.u", "UGate"],
];
/// A mapping from the enum variant in crate::operations::StandardGate to the python object for the
/// class that matches it. This is typically used when we need to convert from the internal rust
/// representation to a Python object for a python user to interact with.
///
/// NOTE: the order here is significant it must match the StandardGate variant's number must match
/// index of it's entry in this table. This is all done statically for performance
static mut STDGATE_PYTHON_GATES: GILOnceCell<[Option<PyObject>; STANDARD_GATE_SIZE]> =
GILOnceCell::new();
#[inline]
pub fn populate_std_gate_map(py: Python, rs_gate: StandardGate, py_gate: PyObject) {
let gate_map = unsafe {
match STDGATE_PYTHON_GATES.get_mut() {
Some(gate_map) => gate_map,
None => {
let array: [Option<PyObject>; STANDARD_GATE_SIZE] = std::array::from_fn(|_| None);
STDGATE_PYTHON_GATES.set(py, array).unwrap();
STDGATE_PYTHON_GATES.get_mut().unwrap()
}
}
};
let gate_cls = &gate_map[rs_gate as usize];
if gate_cls.is_none() {
gate_map[rs_gate as usize] = Some(py_gate.clone_ref(py));
}
}
#[inline]
pub fn get_std_gate_class(py: Python, rs_gate: StandardGate) -> PyResult<PyObject> {
let gate_map =
unsafe { STDGATE_PYTHON_GATES.get_or_init(py, || std::array::from_fn(|_| None)) };
let gate = &gate_map[rs_gate as usize];
let populate = gate.is_none();
let out_gate = match gate {
Some(gate) => gate.clone_ref(py),
None => {
let [py_mod, py_class] = STDGATE_IMPORT_PATHS[rs_gate as usize];
py.import_bound(py_mod)?.getattr(py_class)?.unbind()
}
};
if populate {
populate_std_gate_map(py, rs_gate, out_gate.clone_ref(py));
}
Ok(out_gate)
}

View File

@ -10,11 +10,13 @@
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
use hashbrown::HashMap;
use pyo3::{IntoPy, PyObject, Python};
use std::hash::Hash;
use std::sync::Arc;
use hashbrown::HashMap;
use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
#[derive(Clone, Copy, Debug)]
pub struct Index(u32);
@ -42,6 +44,12 @@ impl IntoPy<PyObject> for Index {
pub struct CacheFullError;
impl From<CacheFullError> for PyErr {
fn from(_: CacheFullError) -> Self {
PyRuntimeError::new_err("The bit operands cache is full!")
}
}
/// An append-only data structure for interning generic
/// Rust types.
#[derive(Clone, Debug)]

View File

@ -13,10 +13,13 @@
pub mod circuit_data;
pub mod circuit_instruction;
pub mod dag_node;
pub mod gate_matrix;
pub mod imports;
pub mod operations;
pub mod parameter_table;
mod bit_data;
mod interner;
mod packed_instruction;
use pyo3::prelude::*;
use pyo3::types::PySlice;
@ -33,9 +36,9 @@ pub enum SliceOrInt<'a> {
pub type BitType = u32;
#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
pub struct Qubit(BitType);
pub struct Qubit(pub BitType);
#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
pub struct Clbit(BitType);
pub struct Clbit(pub BitType);
impl From<BitType> for Qubit {
fn from(value: BitType) -> Self {
@ -69,5 +72,9 @@ pub fn circuit(m: Bound<PyModule>) -> PyResult<()> {
m.add_class::<dag_node::DAGOutNode>()?;
m.add_class::<dag_node::DAGOpNode>()?;
m.add_class::<circuit_instruction::CircuitInstruction>()?;
m.add_class::<operations::StandardGate>()?;
m.add_class::<operations::PyInstruction>()?;
m.add_class::<operations::PyGate>()?;
m.add_class::<operations::PyOperation>()?;
Ok(())
}

View File

@ -0,0 +1,786 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2024
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
use std::f64::consts::PI;
use crate::circuit_data::CircuitData;
use crate::imports::{PARAMETER_EXPRESSION, QUANTUM_CIRCUIT};
use crate::{gate_matrix, Qubit};
use ndarray::{aview2, Array2};
use num_complex::Complex64;
use numpy::IntoPyArray;
use numpy::PyReadonlyArray2;
use pyo3::prelude::*;
use pyo3::{intern, IntoPy, Python};
use smallvec::smallvec;
/// Valid types for an operation field in a CircuitInstruction
///
/// These are basically the types allowed in a QuantumCircuit
#[derive(FromPyObject, Clone, Debug)]
pub enum OperationType {
Standard(StandardGate),
Instruction(PyInstruction),
Gate(PyGate),
Operation(PyOperation),
}
impl Operation for OperationType {
fn name(&self) -> &str {
match self {
Self::Standard(op) => op.name(),
Self::Gate(op) => op.name(),
Self::Instruction(op) => op.name(),
Self::Operation(op) => op.name(),
}
}
fn num_qubits(&self) -> u32 {
match self {
Self::Standard(op) => op.num_qubits(),
Self::Gate(op) => op.num_qubits(),
Self::Instruction(op) => op.num_qubits(),
Self::Operation(op) => op.num_qubits(),
}
}
fn num_clbits(&self) -> u32 {
match self {
Self::Standard(op) => op.num_clbits(),
Self::Gate(op) => op.num_clbits(),
Self::Instruction(op) => op.num_clbits(),
Self::Operation(op) => op.num_clbits(),
}
}
fn num_params(&self) -> u32 {
match self {
Self::Standard(op) => op.num_params(),
Self::Gate(op) => op.num_params(),
Self::Instruction(op) => op.num_params(),
Self::Operation(op) => op.num_params(),
}
}
fn matrix(&self, params: &[Param]) -> Option<Array2<Complex64>> {
match self {
Self::Standard(op) => op.matrix(params),
Self::Gate(op) => op.matrix(params),
Self::Instruction(op) => op.matrix(params),
Self::Operation(op) => op.matrix(params),
}
}
fn control_flow(&self) -> bool {
match self {
Self::Standard(op) => op.control_flow(),
Self::Gate(op) => op.control_flow(),
Self::Instruction(op) => op.control_flow(),
Self::Operation(op) => op.control_flow(),
}
}
fn definition(&self, params: &[Param]) -> Option<CircuitData> {
match self {
Self::Standard(op) => op.definition(params),
Self::Gate(op) => op.definition(params),
Self::Instruction(op) => op.definition(params),
Self::Operation(op) => op.definition(params),
}
}
fn standard_gate(&self) -> Option<StandardGate> {
match self {
Self::Standard(op) => op.standard_gate(),
Self::Gate(op) => op.standard_gate(),
Self::Instruction(op) => op.standard_gate(),
Self::Operation(op) => op.standard_gate(),
}
}
fn directive(&self) -> bool {
match self {
Self::Standard(op) => op.directive(),
Self::Gate(op) => op.directive(),
Self::Instruction(op) => op.directive(),
Self::Operation(op) => op.directive(),
}
}
}
/// Trait for generic circuit operations these define the common attributes
/// needed for something to be addable to the circuit struct
pub trait Operation {
fn name(&self) -> &str;
fn num_qubits(&self) -> u32;
fn num_clbits(&self) -> u32;
fn num_params(&self) -> u32;
fn control_flow(&self) -> bool;
fn matrix(&self, params: &[Param]) -> Option<Array2<Complex64>>;
fn definition(&self, params: &[Param]) -> Option<CircuitData>;
fn standard_gate(&self) -> Option<StandardGate>;
fn directive(&self) -> bool;
}
#[derive(Clone, Debug)]
pub enum Param {
ParameterExpression(PyObject),
Float(f64),
Obj(PyObject),
}
impl<'py> FromPyObject<'py> for Param {
fn extract_bound(b: &Bound<'py, PyAny>) -> Result<Self, PyErr> {
Ok(
if b.is_instance(PARAMETER_EXPRESSION.get_bound(b.py()))?
|| b.is_instance(QUANTUM_CIRCUIT.get_bound(b.py()))?
{
Param::ParameterExpression(b.clone().unbind())
} else if let Ok(val) = b.extract::<f64>() {
Param::Float(val)
} else {
Param::Obj(b.clone().unbind())
},
)
}
}
impl IntoPy<PyObject> for Param {
fn into_py(self, py: Python) -> PyObject {
match &self {
Self::Float(val) => val.to_object(py),
Self::ParameterExpression(val) => val.clone_ref(py),
Self::Obj(val) => val.clone_ref(py),
}
}
}
impl ToPyObject for Param {
fn to_object(&self, py: Python) -> PyObject {
match self {
Self::Float(val) => val.to_object(py),
Self::ParameterExpression(val) => val.clone_ref(py),
Self::Obj(val) => val.clone_ref(py),
}
}
}
#[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)]
#[pyclass(module = "qiskit._accelerate.circuit")]
pub enum StandardGate {
ZGate = 0,
YGate = 1,
XGate = 2,
CZGate = 3,
CYGate = 4,
CXGate = 5,
CCXGate = 6,
RXGate = 7,
RYGate = 8,
RZGate = 9,
ECRGate = 10,
SwapGate = 11,
SXGate = 12,
GlobalPhaseGate = 13,
IGate = 14,
HGate = 15,
PhaseGate = 16,
UGate = 17,
}
static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] =
[1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1];
static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] =
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3];
static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [
"z",
"y",
"x",
"cz",
"cy",
"cx",
"ccx",
"rx",
"ry",
"rz",
"ecr",
"swap",
"sx",
"global_phase",
"id",
"h",
"p",
"u",
];
#[pymethods]
impl StandardGate {
pub fn copy(&self) -> Self {
*self
}
// These pymethods are for testing:
pub fn _to_matrix(&self, py: Python, params: Vec<Param>) -> Option<PyObject> {
self.matrix(&params)
.map(|x| x.into_pyarray_bound(py).into())
}
pub fn _num_params(&self) -> u32 {
self.num_params()
}
pub fn _get_definition(&self, params: Vec<Param>) -> Option<CircuitData> {
self.definition(&params)
}
#[getter]
pub fn get_num_qubits(&self) -> u32 {
self.num_qubits()
}
#[getter]
pub fn get_num_clbits(&self) -> u32 {
self.num_clbits()
}
#[getter]
pub fn get_num_params(&self) -> u32 {
self.num_params()
}
#[getter]
pub fn get_name(&self) -> &str {
self.name()
}
}
// This must be kept up-to-date with `StandardGate` when adding or removing
// gates from the enum
//
// Remove this when std::mem::variant_count() is stabilized (see
// https://github.com/rust-lang/rust/issues/73662 )
pub const STANDARD_GATE_SIZE: usize = 18;
impl Operation for StandardGate {
fn name(&self) -> &str {
STANDARD_GATE_NAME[*self as usize]
}
fn num_qubits(&self) -> u32 {
STANDARD_GATE_NUM_QUBITS[*self as usize]
}
fn num_params(&self) -> u32 {
STANDARD_GATE_NUM_PARAMS[*self as usize]
}
fn num_clbits(&self) -> u32 {
0
}
fn control_flow(&self) -> bool {
false
}
fn directive(&self) -> bool {
false
}
fn matrix(&self, params: &[Param]) -> Option<Array2<Complex64>> {
match self {
Self::ZGate => match params {
[] => Some(aview2(&gate_matrix::Z_GATE).to_owned()),
_ => None,
},
Self::YGate => match params {
[] => Some(aview2(&gate_matrix::Y_GATE).to_owned()),
_ => None,
},
Self::XGate => match params {
[] => Some(aview2(&gate_matrix::X_GATE).to_owned()),
_ => None,
},
Self::CZGate => match params {
[] => Some(aview2(&gate_matrix::CZ_GATE).to_owned()),
_ => None,
},
Self::CYGate => match params {
[] => Some(aview2(&gate_matrix::CY_GATE).to_owned()),
_ => None,
},
Self::CXGate => match params {
[] => Some(aview2(&gate_matrix::CX_GATE).to_owned()),
_ => None,
},
Self::CCXGate => match params {
[] => Some(aview2(&gate_matrix::CCX_GATE).to_owned()),
_ => None,
},
Self::RXGate => match params {
[Param::Float(theta)] => Some(aview2(&gate_matrix::rx_gate(*theta)).to_owned()),
_ => None,
},
Self::RYGate => match params {
[Param::Float(theta)] => Some(aview2(&gate_matrix::ry_gate(*theta)).to_owned()),
_ => None,
},
Self::RZGate => match params {
[Param::Float(theta)] => Some(aview2(&gate_matrix::rz_gate(*theta)).to_owned()),
_ => None,
},
Self::ECRGate => match params {
[] => Some(aview2(&gate_matrix::ECR_GATE).to_owned()),
_ => None,
},
Self::SwapGate => match params {
[] => Some(aview2(&gate_matrix::SWAP_GATE).to_owned()),
_ => None,
},
Self::SXGate => match params {
[] => Some(aview2(&gate_matrix::SX_GATE).to_owned()),
_ => None,
},
Self::GlobalPhaseGate => match params {
[Param::Float(theta)] => {
Some(aview2(&gate_matrix::global_phase_gate(*theta)).to_owned())
}
_ => None,
},
Self::IGate => match params {
[] => Some(aview2(&gate_matrix::ONE_QUBIT_IDENTITY).to_owned()),
_ => None,
},
Self::HGate => match params {
[] => Some(aview2(&gate_matrix::H_GATE).to_owned()),
_ => None,
},
Self::PhaseGate => match params {
[Param::Float(theta)] => Some(aview2(&gate_matrix::phase_gate(*theta)).to_owned()),
_ => None,
},
Self::UGate => match params {
[Param::Float(theta), Param::Float(phi), Param::Float(lam)] => {
Some(aview2(&gate_matrix::u_gate(*theta, *phi, *lam)).to_owned())
}
_ => None,
},
}
}
fn definition(&self, params: &[Param]) -> Option<CircuitData> {
match self {
Self::ZGate => Python::with_gil(|py| -> Option<CircuitData> {
Some(
CircuitData::from_standard_gates(
py,
1,
[(
Self::PhaseGate,
smallvec![Param::Float(PI)],
smallvec![Qubit(0)],
)],
FLOAT_ZERO,
)
.expect("Unexpected Qiskit python bug"),
)
}),
Self::YGate => Python::with_gil(|py| -> Option<CircuitData> {
Some(
CircuitData::from_standard_gates(
py,
1,
[(
Self::UGate,
smallvec![
Param::Float(PI),
Param::Float(PI / 2.),
Param::Float(PI / 2.),
],
smallvec![Qubit(0)],
)],
FLOAT_ZERO,
)
.expect("Unexpected Qiskit python bug"),
)
}),
Self::XGate => Python::with_gil(|py| -> Option<CircuitData> {
Some(
CircuitData::from_standard_gates(
py,
1,
[(
Self::UGate,
smallvec![Param::Float(PI), Param::Float(0.), Param::Float(PI)],
smallvec![Qubit(0)],
)],
FLOAT_ZERO,
)
.expect("Unexpected Qiskit python bug"),
)
}),
Self::CZGate => Python::with_gil(|py| -> Option<CircuitData> {
let q1 = smallvec![Qubit(1)];
let q0_1 = smallvec![Qubit(0), Qubit(1)];
Some(
CircuitData::from_standard_gates(
py,
2,
[
(Self::HGate, smallvec![], q1.clone()),
(Self::CXGate, smallvec![], q0_1),
(Self::HGate, smallvec![], q1),
],
FLOAT_ZERO,
)
.expect("Unexpected Qiskit python bug"),
)
}),
Self::CYGate => todo!("Add when we have S and S dagger"),
Self::CXGate => None,
Self::CCXGate => todo!("Add when we have T and TDagger"),
Self::RXGate => todo!("Add when we have R"),
Self::RYGate => todo!("Add when we have R"),
Self::RZGate => Python::with_gil(|py| -> Option<CircuitData> {
match &params[0] {
Param::Float(theta) => Some(
CircuitData::from_standard_gates(
py,
1,
[(
Self::PhaseGate,
smallvec![Param::Float(*theta)],
smallvec![Qubit(0)],
)],
Param::Float(-0.5 * theta),
)
.expect("Unexpected Qiskit python bug"),
),
Param::ParameterExpression(theta) => Some(
CircuitData::from_standard_gates(
py,
1,
[(
Self::PhaseGate,
smallvec![Param::ParameterExpression(theta.clone_ref(py))],
smallvec![Qubit(0)],
)],
Param::ParameterExpression(
theta
.call_method1(py, intern!(py, "__rmul__"), (-0.5,))
.expect("Parameter expression for global phase failed"),
),
)
.expect("Unexpected Qiskit python bug"),
),
Param::Obj(_) => unreachable!(),
}
}),
Self::ECRGate => todo!("Add when we have RZX"),
Self::SwapGate => Python::with_gil(|py| -> Option<CircuitData> {
Some(
CircuitData::from_standard_gates(
py,
2,
[
(Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]),
(Self::CXGate, smallvec![], smallvec![Qubit(1), Qubit(0)]),
(Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]),
],
FLOAT_ZERO,
)
.expect("Unexpected Qiskit python bug"),
)
}),
Self::SXGate => todo!("Add when we have S dagger"),
Self::GlobalPhaseGate => Python::with_gil(|py| -> Option<CircuitData> {
Some(
CircuitData::from_standard_gates(py, 0, [], params[0].clone())
.expect("Unexpected Qiskit python bug"),
)
}),
Self::IGate => None,
Self::HGate => Python::with_gil(|py| -> Option<CircuitData> {
Some(
CircuitData::from_standard_gates(
py,
1,
[(
Self::UGate,
smallvec![Param::Float(PI / 2.), Param::Float(0.), Param::Float(PI)],
smallvec![Qubit(0)],
)],
FLOAT_ZERO,
)
.expect("Unexpected Qiskit python bug"),
)
}),
Self::PhaseGate => Python::with_gil(|py| -> Option<CircuitData> {
Some(
CircuitData::from_standard_gates(
py,
1,
[(
Self::UGate,
smallvec![Param::Float(0.), Param::Float(0.), params[0].clone()],
smallvec![Qubit(0)],
)],
FLOAT_ZERO,
)
.expect("Unexpected Qiskit python bug"),
)
}),
Self::UGate => None,
}
}
fn standard_gate(&self) -> Option<StandardGate> {
Some(*self)
}
}
const FLOAT_ZERO: Param = Param::Float(0.0);
/// This class is used to wrap a Python side Instruction that is not in the standard library
#[derive(Clone, Debug)]
#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")]
pub struct PyInstruction {
pub qubits: u32,
pub clbits: u32,
pub params: u32,
pub op_name: String,
pub instruction: PyObject,
}
#[pymethods]
impl PyInstruction {
#[new]
fn new(op_name: String, qubits: u32, clbits: u32, params: u32, instruction: PyObject) -> Self {
PyInstruction {
qubits,
clbits,
params,
op_name,
instruction,
}
}
}
impl Operation for PyInstruction {
fn name(&self) -> &str {
self.op_name.as_str()
}
fn num_qubits(&self) -> u32 {
self.qubits
}
fn num_clbits(&self) -> u32 {
self.clbits
}
fn num_params(&self) -> u32 {
self.params
}
fn control_flow(&self) -> bool {
false
}
fn matrix(&self, _params: &[Param]) -> Option<Array2<Complex64>> {
None
}
fn definition(&self, _params: &[Param]) -> Option<CircuitData> {
Python::with_gil(|py| -> Option<CircuitData> {
match self.instruction.getattr(py, intern!(py, "definition")) {
Ok(definition) => {
let res: Option<PyObject> = definition.call0(py).ok()?.extract(py).ok();
match res {
Some(x) => {
let out: CircuitData =
x.getattr(py, intern!(py, "data")).ok()?.extract(py).ok()?;
Some(out)
}
None => None,
}
}
Err(_) => None,
}
})
}
fn standard_gate(&self) -> Option<StandardGate> {
None
}
fn directive(&self) -> bool {
Python::with_gil(|py| -> bool {
match self.instruction.getattr(py, intern!(py, "_directive")) {
Ok(directive) => {
let res: bool = directive.extract(py).unwrap();
res
}
Err(_) => false,
}
})
}
}
/// This class is used to wrap a Python side Gate that is not in the standard library
#[derive(Clone, Debug)]
#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")]
pub struct PyGate {
pub qubits: u32,
pub clbits: u32,
pub params: u32,
pub op_name: String,
pub gate: PyObject,
}
#[pymethods]
impl PyGate {
#[new]
fn new(op_name: String, qubits: u32, clbits: u32, params: u32, gate: PyObject) -> Self {
PyGate {
qubits,
clbits,
params,
op_name,
gate,
}
}
}
impl Operation for PyGate {
fn name(&self) -> &str {
self.op_name.as_str()
}
fn num_qubits(&self) -> u32 {
self.qubits
}
fn num_clbits(&self) -> u32 {
self.clbits
}
fn num_params(&self) -> u32 {
self.params
}
fn control_flow(&self) -> bool {
false
}
fn matrix(&self, _params: &[Param]) -> Option<Array2<Complex64>> {
Python::with_gil(|py| -> Option<Array2<Complex64>> {
match self.gate.getattr(py, intern!(py, "to_matrix")) {
Ok(to_matrix) => {
let res: Option<PyObject> = to_matrix.call0(py).ok()?.extract(py).ok();
match res {
Some(x) => {
let array: PyReadonlyArray2<Complex64> = x.extract(py).ok()?;
Some(array.as_array().to_owned())
}
None => None,
}
}
Err(_) => None,
}
})
}
fn definition(&self, _params: &[Param]) -> Option<CircuitData> {
Python::with_gil(|py| -> Option<CircuitData> {
match self.gate.getattr(py, intern!(py, "definition")) {
Ok(definition) => {
let res: Option<PyObject> = definition.call0(py).ok()?.extract(py).ok();
match res {
Some(x) => {
let out: CircuitData =
x.getattr(py, intern!(py, "data")).ok()?.extract(py).ok()?;
Some(out)
}
None => None,
}
}
Err(_) => None,
}
})
}
fn standard_gate(&self) -> Option<StandardGate> {
Python::with_gil(|py| -> Option<StandardGate> {
match self.gate.getattr(py, intern!(py, "_standard_gate")) {
Ok(stdgate) => match stdgate.extract(py) {
Ok(out_gate) => out_gate,
Err(_) => None,
},
Err(_) => None,
}
})
}
fn directive(&self) -> bool {
false
}
}
/// This class is used to wrap a Python side Operation that is not in the standard library
#[derive(Clone, Debug)]
#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")]
pub struct PyOperation {
pub qubits: u32,
pub clbits: u32,
pub params: u32,
pub op_name: String,
pub operation: PyObject,
}
#[pymethods]
impl PyOperation {
#[new]
fn new(op_name: String, qubits: u32, clbits: u32, params: u32, operation: PyObject) -> Self {
PyOperation {
qubits,
clbits,
params,
op_name,
operation,
}
}
}
impl Operation for PyOperation {
fn name(&self) -> &str {
self.op_name.as_str()
}
fn num_qubits(&self) -> u32 {
self.qubits
}
fn num_clbits(&self) -> u32 {
self.clbits
}
fn num_params(&self) -> u32 {
self.params
}
fn control_flow(&self) -> bool {
false
}
fn matrix(&self, _params: &[Param]) -> Option<Array2<Complex64>> {
None
}
fn definition(&self, _params: &[Param]) -> Option<CircuitData> {
None
}
fn standard_gate(&self) -> Option<StandardGate> {
None
}
fn directive(&self) -> bool {
Python::with_gil(|py| -> bool {
match self.operation.getattr(py, intern!(py, "_directive")) {
Ok(directive) => {
let res: bool = directive.extract(py).unwrap();
res
}
Err(_) => false,
}
})
}
}

View File

@ -1,25 +0,0 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2024
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
use crate::interner::Index;
use pyo3::prelude::*;
/// Private type used to store instructions with interned arg lists.
#[derive(Clone, Debug)]
pub(crate) struct PackedInstruction {
/// The Python-side operation instance.
pub op: PyObject,
/// The index under which the interner has stored `qubits`.
pub qubits_id: Index,
/// The index under which the interner has stored `clbits`.
pub clbits_id: Index,
}

View File

@ -0,0 +1,173 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2024
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
use pyo3::prelude::*;
use pyo3::{import_exception, intern, PyObject};
import_exception!(qiskit.circuit.exceptions, CircuitError);
use hashbrown::{HashMap, HashSet};
/// The index value in a `ParamEntry` that indicates the global phase.
pub const GLOBAL_PHASE_INDEX: usize = usize::MAX;
#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")]
pub(crate) struct ParamEntryKeys {
keys: Vec<(usize, usize)>,
iter_pos: usize,
}
#[pymethods]
impl ParamEntryKeys {
fn __iter__(slf: PyRef<Self>) -> Py<ParamEntryKeys> {
slf.into()
}
fn __next__(mut slf: PyRefMut<Self>) -> Option<(usize, usize)> {
if slf.iter_pos < slf.keys.len() {
let res = Some(slf.keys[slf.iter_pos]);
slf.iter_pos += 1;
res
} else {
None
}
}
}
#[derive(Clone, Debug)]
#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")]
pub(crate) struct ParamEntry {
/// Mapping of tuple of instruction index (in CircuitData) and parameter index to the actual
/// parameter object
pub index_ids: HashSet<(usize, usize)>,
}
impl ParamEntry {
pub fn add(&mut self, inst_index: usize, param_index: usize) {
self.index_ids.insert((inst_index, param_index));
}
pub fn discard(&mut self, inst_index: usize, param_index: usize) {
self.index_ids.remove(&(inst_index, param_index));
}
}
#[pymethods]
impl ParamEntry {
#[new]
pub fn new(inst_index: usize, param_index: usize) -> Self {
ParamEntry {
index_ids: HashSet::from([(inst_index, param_index)]),
}
}
pub fn __len__(&self) -> usize {
self.index_ids.len()
}
pub fn __contains__(&self, key: (usize, usize)) -> bool {
self.index_ids.contains(&key)
}
pub fn __iter__(&self) -> ParamEntryKeys {
ParamEntryKeys {
keys: self.index_ids.iter().copied().collect(),
iter_pos: 0,
}
}
}
#[derive(Clone, Debug)]
#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")]
pub(crate) struct ParamTable {
/// Mapping of parameter uuid (as an int) to the Parameter Entry
pub table: HashMap<u128, ParamEntry>,
/// Mapping of parameter name to uuid as an int
pub names: HashMap<String, u128>,
/// Mapping of uuid to a parameter object
pub uuid_map: HashMap<u128, PyObject>,
}
impl ParamTable {
pub fn insert(&mut self, py: Python, parameter: PyObject, entry: ParamEntry) -> PyResult<()> {
let uuid: u128 = parameter
.getattr(py, intern!(py, "_uuid"))?
.getattr(py, intern!(py, "int"))?
.extract(py)?;
let name: String = parameter.getattr(py, intern!(py, "name"))?.extract(py)?;
if self.names.contains_key(&name) && !self.table.contains_key(&uuid) {
return Err(CircuitError::new_err(format!(
"Name conflict on adding parameter: {}",
name
)));
}
self.table.insert(uuid, entry);
self.names.insert(name, uuid);
self.uuid_map.insert(uuid, parameter);
Ok(())
}
pub fn discard_references(
&mut self,
uuid: u128,
inst_index: usize,
param_index: usize,
name: String,
) {
if let Some(refs) = self.table.get_mut(&uuid) {
if refs.__len__() == 1 {
self.table.remove(&uuid);
self.names.remove(&name);
self.uuid_map.remove(&uuid);
} else {
refs.discard(inst_index, param_index);
}
}
}
}
#[pymethods]
impl ParamTable {
#[new]
pub fn new() -> Self {
ParamTable {
table: HashMap::new(),
names: HashMap::new(),
uuid_map: HashMap::new(),
}
}
pub fn clear(&mut self) {
self.table.clear();
self.names.clear();
self.uuid_map.clear();
}
pub fn pop(&mut self, key: u128, name: String) -> Option<ParamEntry> {
self.names.remove(&name);
self.uuid_map.remove(&key);
self.table.remove(&key)
}
fn set(&mut self, uuid: u128, name: String, param: PyObject, refs: ParamEntry) {
self.names.insert(name, uuid);
self.table.insert(uuid, refs);
self.uuid_map.insert(uuid, param);
}
pub fn get_param_from_name(&self, py: Python, name: String) -> Option<PyObject> {
self.names
.get(&name)
.map(|x| self.uuid_map.get(x).map(|y| y.clone_ref(py)))?
}
}

View File

@ -17,6 +17,7 @@ crate-type = ["cdylib"]
# crates as standalone binaries, executables, we need `libpython` to be linked in, so we make the
# feature a default, and run `cargo test --no-default-features` to turn it off.
default = ["pyo3/extension-module"]
cache_pygates = ["pyo3/extension-module", "qiskit-circuit/cache_pygates"]
[dependencies]
pyo3.workspace = true

View File

@ -57,7 +57,9 @@ class CircuitScopeInterface(abc.ABC):
"""Indexable view onto the :class:`.CircuitInstruction`s backing this scope."""
@abc.abstractmethod
def append(self, instruction: CircuitInstruction) -> CircuitInstruction:
def append(
self, instruction: CircuitInstruction, *, _standard_gate=False
) -> CircuitInstruction:
"""Low-level 'append' primitive; this may assume that the qubits, clbits and operation are
all valid for the circuit.
@ -420,7 +422,9 @@ class ControlFlowBuilderBlock(CircuitScopeInterface):
" because it is not in a loop."
)
def append(self, instruction: CircuitInstruction) -> CircuitInstruction:
def append(
self, instruction: CircuitInstruction, *, _standard_gate: bool = False
) -> CircuitInstruction:
if self._forbidden_message is not None:
raise CircuitError(self._forbidden_message)
if not self._allow_jumps:

View File

@ -58,6 +58,7 @@ class Instruction(Operation):
# Class attribute to treat like barrier for transpiler, unroller, drawer
# NOTE: Using this attribute may change in the future (See issue # 5811)
_directive = False
_standard_gate = None
def __init__(self, name, num_qubits, num_clbits, params, duration=None, unit="dt", label=None):
"""Create a new instruction.

View File

@ -140,13 +140,12 @@ class InstructionSet:
)
if self._requester is not None:
classical = self._requester(classical)
for instruction in self._instructions:
for idx, instruction in enumerate(self._instructions):
if isinstance(instruction, CircuitInstruction):
updated = instruction.operation.c_if(classical, val)
if updated is not instruction.operation:
raise CircuitError(
"SingletonGate instances can only be added to InstructionSet via _add_ref"
)
self._instructions[idx] = instruction.replace(
operation=updated, condition=updated.condition
)
else:
data, idx = instruction
instruction = data[idx]

View File

@ -17,7 +17,7 @@ from abc import ABC, abstractmethod
from qiskit._accelerate.circuit import CircuitData
from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.parametertable import ParameterTable, ParameterView
from qiskit.circuit.parametertable import ParameterView
class BlueprintCircuit(QuantumCircuit, ABC):
@ -68,7 +68,6 @@ class BlueprintCircuit(QuantumCircuit, ABC):
def _invalidate(self) -> None:
"""Invalidate the current circuit build."""
self._data = CircuitData(self._data.qubits, self._data.clbits)
self._parameter_table = ParameterTable()
self.global_phase = 0
self._is_built = False
@ -88,7 +87,6 @@ class BlueprintCircuit(QuantumCircuit, ABC):
self._ancillas = []
self._qubit_indices = {}
self._data = CircuitData(clbits=self._data.clbits)
self._parameter_table = ParameterTable()
self.global_phase = 0
self._is_built = False
@ -122,10 +120,10 @@ class BlueprintCircuit(QuantumCircuit, ABC):
self._build()
return super().parameters
def _append(self, instruction, _qargs=None, _cargs=None):
def _append(self, instruction, _qargs=None, _cargs=None, *, _standard_gate=False):
if not self._is_built:
self._build()
return super()._append(instruction, _qargs, _cargs)
return super()._append(instruction, _qargs, _cargs, _standard_gate=_standard_gate)
def compose(
self,

View File

@ -17,6 +17,7 @@ import numpy as np
from qiskit.circuit._utils import with_gate_array
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key
from qiskit._accelerate.circuit import StandardGate
from .rzx import RZXGate
from .x import XGate
@ -84,6 +85,8 @@ class ECRGate(SingletonGate):
\end{pmatrix}
"""
_standard_gate = StandardGate.ECRGate
def __init__(self, label=None, *, duration=None, unit="dt"):
"""Create new ECR gate."""
super().__init__("ecr", 2, [], label=label, duration=duration, unit=unit)

View File

@ -20,6 +20,7 @@ from qiskit.circuit.gate import Gate
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit.circuit.parameterexpression import ParameterValueType
from qiskit._accelerate.circuit import StandardGate
class GlobalPhaseGate(Gate):
@ -36,6 +37,8 @@ class GlobalPhaseGate(Gate):
\end{pmatrix}
"""
_standard_gate = StandardGate.GlobalPhaseGate
def __init__(
self, phase: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt"
):

View File

@ -17,6 +17,7 @@ import numpy
from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array
from qiskit._accelerate.circuit import StandardGate
_H_ARRAY = 1 / sqrt(2) * numpy.array([[1, 1], [1, -1]], dtype=numpy.complex128)
@ -51,6 +52,8 @@ class HGate(SingletonGate):
\end{pmatrix}
"""
_standard_gate = StandardGate.HGate
def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"):
"""Create new H gate."""
super().__init__("h", 1, [], label=label, duration=duration, unit=unit)

View File

@ -15,6 +15,7 @@
from typing import Optional
from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key
from qiskit.circuit._utils import with_gate_array
from qiskit._accelerate.circuit import StandardGate
@with_gate_array([[1, 0], [0, 1]])
@ -45,6 +46,8 @@ class IGate(SingletonGate):
"""
_standard_gate = StandardGate.IGate
def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"):
"""Create new Identity gate."""
super().__init__("id", 1, [], label=label, duration=duration, unit=unit)

View File

@ -19,6 +19,7 @@ from qiskit.circuit.controlledgate import ControlledGate
from qiskit.circuit.gate import Gate
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit.parameterexpression import ParameterValueType
from qiskit._accelerate.circuit import StandardGate
class PhaseGate(Gate):
@ -75,6 +76,8 @@ class PhaseGate(Gate):
`1612.00858 <https://arxiv.org/abs/1612.00858>`_
"""
_standard_gate = StandardGate.PhaseGate
def __init__(
self, theta: ParameterValueType, label: str | None = None, *, duration=None, unit="dt"
):

View File

@ -21,6 +21,7 @@ from qiskit.circuit.controlledgate import ControlledGate
from qiskit.circuit.gate import Gate
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit.parameterexpression import ParameterValueType
from qiskit._accelerate.circuit import StandardGate
class RXGate(Gate):
@ -50,6 +51,8 @@ class RXGate(Gate):
\end{pmatrix}
"""
_standard_gate = StandardGate.RXGate
def __init__(
self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt"
):

View File

@ -20,6 +20,7 @@ from qiskit.circuit.controlledgate import ControlledGate
from qiskit.circuit.gate import Gate
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit.parameterexpression import ParameterValueType
from qiskit._accelerate.circuit import StandardGate
class RYGate(Gate):
@ -49,6 +50,8 @@ class RYGate(Gate):
\end{pmatrix}
"""
_standard_gate = StandardGate.RYGate
def __init__(
self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt"
):

View File

@ -17,6 +17,7 @@ from qiskit.circuit.gate import Gate
from qiskit.circuit.controlledgate import ControlledGate
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit.parameterexpression import ParameterValueType
from qiskit._accelerate.circuit import StandardGate
class RZGate(Gate):
@ -59,6 +60,8 @@ class RZGate(Gate):
`1612.00858 <https://arxiv.org/abs/1612.00858>`_
"""
_standard_gate = StandardGate.RZGate
def __init__(
self, phi: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt"
):

View File

@ -17,6 +17,7 @@ import numpy
from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array
from qiskit._accelerate.circuit import StandardGate
_SWAP_ARRAY = numpy.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])
@ -58,6 +59,8 @@ class SwapGate(SingletonGate):
|a, b\rangle \rightarrow |b, a\rangle
"""
_standard_gate = StandardGate.SwapGate
def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"):
"""Create new SWAP gate."""
super().__init__("swap", 2, [], label=label, duration=duration, unit=unit)

View File

@ -17,6 +17,7 @@ from typing import Optional, Union
from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array
from qiskit._accelerate.circuit import StandardGate
_SX_ARRAY = [[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]]
@ -62,6 +63,8 @@ class SXGate(SingletonGate):
"""
_standard_gate = StandardGate.SXGate
def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"):
"""Create new SX gate."""
super().__init__("sx", 1, [], label=label, duration=duration, unit=unit)

View File

@ -21,6 +21,7 @@ from qiskit.circuit.controlledgate import ControlledGate
from qiskit.circuit.gate import Gate
from qiskit.circuit.parameterexpression import ParameterValueType
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit._accelerate.circuit import StandardGate
class UGate(Gate):
@ -68,6 +69,8 @@ class UGate(Gate):
U(\theta, 0, 0) = RY(\theta)
"""
_standard_gate = StandardGate.UGate
def __init__(
self,
theta: ParameterValueType,

View File

@ -19,6 +19,7 @@ from qiskit.circuit.controlledgate import ControlledGate
from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit._utils import _ctrl_state_to_int, with_gate_array, with_controlled_gate_array
from qiskit._accelerate.circuit import StandardGate
_X_ARRAY = [[0, 1], [1, 0]]
@ -70,6 +71,8 @@ class XGate(SingletonGate):
|1\rangle \rightarrow |0\rangle
"""
_standard_gate = StandardGate.XGate
def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"):
"""Create new X gate."""
super().__init__("x", 1, [], label=label, duration=duration, unit=unit)
@ -212,6 +215,8 @@ class CXGate(SingletonControlledGate):
`|a, b\rangle \rightarrow |a, a \oplus b\rangle`
"""
_standard_gate = StandardGate.CXGate
def __init__(
self,
label: Optional[str] = None,
@ -362,6 +367,8 @@ class CCXGate(SingletonControlledGate):
"""
_standard_gate = StandardGate.CCXGate
def __init__(
self,
label: Optional[str] = None,

View File

@ -19,6 +19,7 @@ from typing import Optional, Union
from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array
from qiskit._accelerate.circuit import StandardGate
_Y_ARRAY = [[0, -1j], [1j, 0]]
@ -70,6 +71,8 @@ class YGate(SingletonGate):
|1\rangle \rightarrow -i|0\rangle
"""
_standard_gate = StandardGate.YGate
def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"):
"""Create new Y gate."""
super().__init__("y", 1, [], label=label, duration=duration, unit=unit)
@ -197,6 +200,8 @@ class CYGate(SingletonControlledGate):
"""
_standard_gate = StandardGate.CYGate
def __init__(
self,
label: Optional[str] = None,

View File

@ -20,6 +20,7 @@ import numpy
from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array
from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit._accelerate.circuit import StandardGate
from .p import PhaseGate
@ -73,6 +74,8 @@ class ZGate(SingletonGate):
|1\rangle \rightarrow -|1\rangle
"""
_standard_gate = StandardGate.ZGate
def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"):
"""Create new Z gate."""
super().__init__("z", 1, [], label=label, duration=duration, unit=unit)
@ -181,6 +184,8 @@ class CZGate(SingletonControlledGate):
the target qubit if the control qubit is in the :math:`|1\rangle` state.
"""
_standard_gate = StandardGate.CZGate
def __init__(
self,
label: Optional[str] = None,

View File

@ -12,197 +12,8 @@
"""
Look-up table for variable parameters in QuantumCircuit.
"""
import operator
import typing
from collections.abc import MappingView, MutableMapping, MutableSet
class ParameterReferences(MutableSet):
"""A set of instruction parameter slot references.
Items are expected in the form ``(instruction, param_index)``. Membership
testing is overridden such that items that are otherwise value-wise equal
are still considered distinct if their ``instruction``\\ s are referentially
distinct.
In the case of the special value :attr:`.ParameterTable.GLOBAL_PHASE` for ``instruction``, the
``param_index`` should be ``None``.
"""
def _instance_key(self, ref):
return (id(ref[0]), ref[1])
def __init__(self, refs):
self._instance_ids = {}
for ref in refs:
if not isinstance(ref, tuple) or len(ref) != 2:
raise ValueError("refs must be in form (instruction, param_index)")
k = self._instance_key(ref)
self._instance_ids[k] = ref[0]
def __getstate__(self):
# Leave behind the reference IDs (keys of _instance_ids) since they'll
# be incorrect after unpickling on the other side.
return list(self)
def __setstate__(self, refs):
# Recompute reference IDs for the newly unpickled instructions.
self._instance_ids = {self._instance_key(ref): ref[0] for ref in refs}
def __len__(self):
return len(self._instance_ids)
def __iter__(self):
for (_, idx), instruction in self._instance_ids.items():
yield (instruction, idx)
def __contains__(self, x) -> bool:
return self._instance_key(x) in self._instance_ids
def __repr__(self) -> str:
return f"ParameterReferences({repr(list(self))})"
def add(self, value):
"""Adds a reference to the listing if it's not already present."""
k = self._instance_key(value)
self._instance_ids[k] = value[0]
def discard(self, value):
k = self._instance_key(value)
self._instance_ids.pop(k, None)
def copy(self):
"""Create a shallow copy."""
return ParameterReferences(self)
class ParameterTable(MutableMapping):
"""Class for tracking references to circuit parameters by specific
instruction instances.
Keys are parameters. Values are of type :class:`~ParameterReferences`,
which overrides membership testing to be referential for instructions,
and is set-like. Elements of :class:`~ParameterReferences`
are tuples of ``(instruction, param_index)``.
"""
__slots__ = ["_table", "_keys", "_names"]
class _GlobalPhaseSentinel:
__slots__ = ()
def __copy__(self):
return self
def __deepcopy__(self, memo=None):
return self
def __reduce__(self):
return (operator.attrgetter("GLOBAL_PHASE"), (ParameterTable,))
def __repr__(self):
return "<global-phase sentinel>"
GLOBAL_PHASE = _GlobalPhaseSentinel()
"""Tracking object to indicate that a reference refers to the global phase of a circuit."""
def __init__(self, mapping=None):
"""Create a new instance, initialized with ``mapping`` if provided.
Args:
mapping (Mapping[Parameter, ParameterReferences]):
Mapping of parameter to the set of parameter slots that reference
it.
Raises:
ValueError: A value in ``mapping`` is not a :class:`~ParameterReferences`.
"""
if mapping is not None:
if any(not isinstance(refs, ParameterReferences) for refs in mapping.values()):
raise ValueError("Values must be of type ParameterReferences")
self._table = mapping.copy()
else:
self._table = {}
self._keys = set(self._table)
self._names = {x.name: x for x in self._table}
def __getitem__(self, key):
return self._table[key]
def __setitem__(self, parameter, refs):
"""Associate a parameter with the set of parameter slots ``(instruction, param_index)``
that reference it.
.. note::
Items in ``refs`` are considered unique if their ``instruction`` is referentially
unique. See :class:`~ParameterReferences` for details.
Args:
parameter (Parameter): the parameter
refs (Union[ParameterReferences, Iterable[(Instruction, int)]]): the parameter slots.
If this is an iterable, a new :class:`~ParameterReferences` is created from its
contents.
"""
if not isinstance(refs, ParameterReferences):
refs = ParameterReferences(refs)
self._table[parameter] = refs
self._keys.add(parameter)
self._names[parameter.name] = parameter
def get_keys(self):
"""Return a set of all keys in the parameter table
Returns:
set: A set of all the keys in the parameter table
"""
return self._keys
def get_names(self):
"""Return a set of all parameter names in the parameter table
Returns:
set: A set of all the names in the parameter table
"""
return self._names.keys()
def parameter_from_name(self, name: str, default: typing.Any = None):
"""Get a :class:`.Parameter` with references in this table by its string name.
If the parameter is not present, return the ``default`` value.
Args:
name: The name of the :class:`.Parameter`
default: The object that should be returned if the parameter is missing.
"""
return self._names.get(name, default)
def discard_references(self, expression, key):
"""Remove all references to parameters contained within ``expression`` at the given table
``key``. This also discards parameter entries from the table if they have no further
references. No action is taken if the object is not tracked."""
for parameter in expression.parameters:
if (refs := self._table.get(parameter)) is not None:
if len(refs) == 1:
del self[parameter]
else:
refs.discard(key)
def __delitem__(self, key):
del self._table[key]
self._keys.discard(key)
del self._names[key.name]
def __iter__(self):
return iter(self._table)
def __len__(self):
return len(self._table)
def __repr__(self):
return f"ParameterTable({repr(self._table)})"
from collections.abc import MappingView
class ParameterView(MappingView):

View File

@ -37,6 +37,7 @@ from typing import (
)
import numpy as np
from qiskit._accelerate.circuit import CircuitData
from qiskit._accelerate.circuit import StandardGate, PyGate, PyInstruction, PyOperation
from qiskit.exceptions import QiskitError
from qiskit.utils.multiprocessing import is_main_process
from qiskit.circuit.instruction import Instruction
@ -57,7 +58,7 @@ from .classical import expr, types
from .parameterexpression import ParameterExpression, ParameterValueType
from .quantumregister import QuantumRegister, Qubit, AncillaRegister, AncillaQubit
from .classicalregister import ClassicalRegister, Clbit
from .parametertable import ParameterReferences, ParameterTable, ParameterView
from .parametertable import ParameterView
from .parametervector import ParameterVector
from .instructionset import InstructionSet
from .operation import Operation
@ -1124,14 +1125,10 @@ class QuantumCircuit:
self._calibrations: DefaultDict[str, dict[tuple, Any]] = defaultdict(dict)
self.add_register(*regs)
# Parameter table tracks instructions with variable parameters.
self._parameter_table = ParameterTable()
# Cache to avoid re-sorting parameters
self._parameters = None
self._layout = None
self._global_phase: ParameterValueType = 0
self.global_phase = global_phase
# Add classical variables. Resolve inputs and captures first because they can't depend on
@ -1159,6 +1156,15 @@ class QuantumCircuit:
Qiskit will not examine the content of this mapping, but it will pass it through the
transpiler and reattach it to the output, so you can track your own metadata."""
@classmethod
def _from_circuit_data(cls, data: CircuitData) -> typing.Self:
"""A private constructor from rust space circuit data."""
out = QuantumCircuit()
out.add_bits(data.qubits)
out.add_bits(data.clbits)
out._data = data
return out
@staticmethod
def from_instructions(
instructions: Iterable[
@ -1259,7 +1265,6 @@ class QuantumCircuit:
data_input = list(data_input)
self._data.clear()
self._parameters = None
self._parameter_table = ParameterTable()
# Repopulate the parameter table with any global-phase entries.
self.global_phase = self.global_phase
if not data_input:
@ -1382,12 +1387,11 @@ class QuantumCircuit:
# Avoids pulling self._data into a Python list
# like we would when pickling.
result._data = self._data.copy()
result._data = self._data.copy(deepcopy=True)
result._data.replace_bits(
qubits=_copy.deepcopy(self._data.qubits, memo),
clbits=_copy.deepcopy(self._data.clbits, memo),
)
result._data.map_ops(lambda op: _copy.deepcopy(op, memo))
return result
@classmethod
@ -1896,7 +1900,7 @@ class QuantumCircuit:
clbits = self.clbits[: other.num_clbits]
if front:
# Need to keep a reference to the data for use after we've emptied it.
old_data = dest._data.copy()
old_data = dest._data.copy(copy_instructions=copy)
dest.clear()
dest.append(other, qubits, clbits, copy=copy)
for instruction in old_data:
@ -2024,14 +2028,14 @@ class QuantumCircuit:
)
return n_op.copy() if n_op is op and copy else n_op
instructions = source._data.copy()
instructions = source._data.copy(copy_instructions=copy)
instructions.replace_bits(qubits=new_qubits, clbits=new_clbits)
instructions.map_ops(map_vars)
dest._current_scope().extend(instructions)
append_existing = None
if front:
append_existing = dest._data.copy()
append_existing = dest._data.copy(copy_instructions=copy)
dest.clear()
copy_with_remapping(
other,
@ -2296,6 +2300,35 @@ class QuantumCircuit:
clbit_representation, self.clbits, self._clbit_indices, Clbit
)
def _append_standard_gate(
self,
op: StandardGate,
params: Sequence[ParameterValueType] | None = None,
qargs: Sequence[QubitSpecifier] | None = None,
cargs: Sequence[ClbitSpecifier] | None = None,
label: str | None = None,
) -> InstructionSet:
"""An internal method to bypass some checking when directly appending a standard gate."""
circuit_scope = self._current_scope()
if params is None:
params = []
expanded_qargs = [self.qbit_argument_conversion(qarg) for qarg in qargs or []]
expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []]
if params is not None:
for param in params:
Gate.validate_parameter(op, param)
instructions = InstructionSet(resource_requester=circuit_scope.resolve_classical_resource)
broadcast_iter = Gate.broadcast_arguments(op, expanded_qargs, expanded_cargs)
for qarg, carg in broadcast_iter:
self._check_dups(qarg)
instruction = CircuitInstruction(op, qarg, carg, params=params, label=label)
circuit_scope.append(instruction, _standard_gate=True)
instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1)
return instructions
def append(
self,
instruction: Operation | CircuitInstruction,
@ -2393,16 +2426,47 @@ class QuantumCircuit:
if isinstance(operation, Instruction)
else Instruction.broadcast_arguments(operation, expanded_qargs, expanded_cargs)
)
params = None
if isinstance(operation, Gate):
params = operation.params
operation = PyGate(
operation.name,
operation.num_qubits,
operation.num_clbits,
len(params),
operation,
)
elif isinstance(operation, Instruction):
params = operation.params
operation = PyInstruction(
operation.name,
operation.num_qubits,
operation.num_clbits,
len(params),
operation,
)
elif isinstance(operation, Operation):
params = getattr(operation, "params", ())
operation = PyOperation(
operation.name,
operation.num_qubits,
operation.num_clbits,
len(params),
operation,
)
for qarg, carg in broadcast_iter:
self._check_dups(qarg)
instruction = CircuitInstruction(operation, qarg, carg)
instruction = CircuitInstruction(operation, qarg, carg, params=params)
circuit_scope.append(instruction)
instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1)
return instructions
# Preferred new style.
@typing.overload
def _append(self, instruction: CircuitInstruction) -> CircuitInstruction: ...
def _append(
self, instruction: CircuitInstruction, *, _standard_gate: bool
) -> CircuitInstruction: ...
# To-be-deprecated old style.
@typing.overload
@ -2413,7 +2477,7 @@ class QuantumCircuit:
cargs: Sequence[Clbit],
) -> Operation: ...
def _append(self, instruction, qargs=(), cargs=()):
def _append(self, instruction, qargs=(), cargs=(), *, _standard_gate: bool = False):
"""Append an instruction to the end of the circuit, modifying the circuit in place.
.. warning::
@ -2454,40 +2518,39 @@ class QuantumCircuit:
:meta public:
"""
if _standard_gate:
new_param = self._data.append(instruction)
if new_param:
self._parameters = None
self.duration = None
self.unit = "dt"
return instruction
old_style = not isinstance(instruction, CircuitInstruction)
if old_style:
instruction = CircuitInstruction(instruction, qargs, cargs)
self._data.append(instruction)
self._track_operation(instruction.operation)
return instruction.operation if old_style else instruction
# If there is a reference to the outer circuit in an
# instruction param the inner rust append method will raise a runtime error.
# When this happens we need to handle the parameters separately.
# This shouldn't happen in practice but 2 tests were doing this and it's not
# explicitly prohibted by the API so this and the `params` optional argument
# path guard against it.
try:
new_param = self._data.append(instruction)
except RuntimeError:
params = []
for idx, param in enumerate(instruction.operation.params):
if isinstance(param, (ParameterExpression, QuantumCircuit)):
params.append((idx, list(set(param.parameters))))
new_param = self._data.append(instruction, params)
if new_param:
# clear cache if new parameter is added
self._parameters = None
def _track_operation(self, operation: Operation):
"""Sync all non-data-list internal data structures for a newly tracked operation."""
if isinstance(operation, Instruction):
self._update_parameter_table(operation)
# Invalidate whole circuit duration if an instruction is added
self.duration = None
self.unit = "dt"
def _update_parameter_table(self, instruction: Instruction):
for param_index, param in enumerate(instruction.params):
if isinstance(param, (ParameterExpression, QuantumCircuit)):
# Scoped constructs like the control-flow ops use QuantumCircuit as a parameter.
atomic_parameters = set(param.parameters)
else:
atomic_parameters = set()
for parameter in atomic_parameters:
if parameter in self._parameter_table:
self._parameter_table[parameter].add((instruction, param_index))
else:
if parameter.name in self._parameter_table.get_names():
raise CircuitError(f"Name conflict on adding parameter: {parameter.name}")
self._parameter_table[parameter] = ParameterReferences(
((instruction, param_index),)
)
# clear cache if new parameter is added
self._parameters = None
return instruction.operation if old_style else instruction
@typing.overload
def get_parameter(self, name: str, default: T) -> Union[Parameter, T]: ...
@ -2538,7 +2601,7 @@ class QuantumCircuit:
A similar method, but for :class:`.expr.Var` run-time variables instead of
:class:`.Parameter` compile-time parameters.
"""
if (parameter := self._parameter_table.parameter_from_name(name, None)) is None:
if (parameter := self._data.get_param_from_name(name)) is None:
if default is Ellipsis:
raise KeyError(f"no parameter named '{name}' is present")
return default
@ -3415,13 +3478,7 @@ class QuantumCircuit:
Conditional nonlocal gates are also included.
"""
multi_qubit_gates = 0
for instruction in self._data:
if instruction.operation.num_qubits > 1 and not getattr(
instruction.operation, "_directive", False
):
multi_qubit_gates += 1
return multi_qubit_gates
return self._data.num_nonlocal_gates()
def get_instructions(self, name: str) -> list[CircuitInstruction]:
"""Get instructions matching name.
@ -3535,29 +3592,6 @@ class QuantumCircuit:
"""
cpy = self.copy_empty_like(name)
cpy._data = self._data.copy()
# The special global-phase sentinel doesn't need copying, but it's
# added here to ensure it's recognised. The global phase itself was
# already copied over in `copy_empty_like`.
operation_copies = {id(ParameterTable.GLOBAL_PHASE): ParameterTable.GLOBAL_PHASE}
def memo_copy(op):
if (out := operation_copies.get(id(op))) is not None:
return out
copied = op.copy()
operation_copies[id(op)] = copied
return copied
cpy._data.map_ops(memo_copy)
cpy._parameter_table = ParameterTable(
{
param: ParameterReferences(
(operation_copies[id(operation)], param_index)
for operation, param_index in self._parameter_table[param]
)
for param in self._parameter_table
}
)
return cpy
def copy_empty_like(
@ -3636,12 +3670,9 @@ class QuantumCircuit:
else: # pragma: no cover
raise ValueError(f"unknown vars_mode: '{vars_mode}'")
cpy._parameter_table = ParameterTable()
for parameter in getattr(cpy.global_phase, "parameters", ()):
cpy._parameter_table[parameter] = ParameterReferences(
[(ParameterTable.GLOBAL_PHASE, None)]
)
cpy._data = CircuitData(self._data.qubits, self._data.clbits)
cpy._data = CircuitData(
self._data.qubits, self._data.clbits, global_phase=self._data.global_phase
)
cpy._calibrations = _copy.deepcopy(self._calibrations)
cpy._metadata = _copy.deepcopy(self._metadata)
@ -3661,7 +3692,6 @@ class QuantumCircuit:
quantum and classical typed data, but without mutating the original circuit.
"""
self._data.clear()
self._parameter_table.clear()
# Repopulate the parameter table with any phase symbols.
self.global_phase = self.global_phase
@ -3945,10 +3975,9 @@ class QuantumCircuit:
circ._clbit_indices = {}
# Clear instruction info
circ._data = CircuitData(qubits=circ._data.qubits, reserve=len(circ._data))
circ._parameter_table.clear()
# Repopulate the parameter table with any global-phase entries.
circ.global_phase = circ.global_phase
circ._data = CircuitData(
qubits=circ._data.qubits, reserve=len(circ._data), global_phase=circ.global_phase
)
# We must add the clbits first to preserve the original circuit
# order. This way, add_register never adds clbits and just
@ -4021,7 +4050,7 @@ class QuantumCircuit:
"""The global phase of the current circuit scope in radians."""
if self._control_flow_scopes:
return self._control_flow_scopes[-1].global_phase
return self._global_phase
return self._data.global_phase
@global_phase.setter
def global_phase(self, angle: ParameterValueType):
@ -4032,23 +4061,18 @@ class QuantumCircuit:
"""
# If we're currently parametric, we need to throw away the references. This setter is
# called by some subclasses before the inner `_global_phase` is initialised.
global_phase_reference = (ParameterTable.GLOBAL_PHASE, None)
if isinstance(previous := getattr(self, "_global_phase", None), ParameterExpression):
if isinstance(getattr(self._data, "global_phase", None), ParameterExpression):
self._parameters = None
self._parameter_table.discard_references(previous, global_phase_reference)
if isinstance(angle, ParameterExpression) and angle.parameters:
for parameter in angle.parameters:
if parameter not in self._parameter_table:
self._parameters = None
self._parameter_table[parameter] = ParameterReferences(())
self._parameter_table[parameter].add(global_phase_reference)
if isinstance(angle, ParameterExpression):
if angle.parameters:
self._parameters = None
else:
angle = _normalize_global_phase(angle)
if self._control_flow_scopes:
self._control_flow_scopes[-1].global_phase = angle
else:
self._global_phase = angle
self._data.global_phase = angle
@property
def parameters(self) -> ParameterView:
@ -4118,7 +4142,7 @@ class QuantumCircuit:
@property
def num_parameters(self) -> int:
"""The number of parameter objects in the circuit."""
return len(self._parameter_table)
return self._data.num_params()
def _unsorted_parameters(self) -> set[Parameter]:
"""Efficiently get all parameters in the circuit, without any sorting overhead.
@ -4131,7 +4155,7 @@ class QuantumCircuit:
"""
# This should be free, by accessing the actual backing data structure of the table, but that
# means that we need to copy it if adding keys from the global phase.
return self._parameter_table.get_keys()
return self._data.get_params_unsorted()
@overload
def assign_parameters(
@ -4280,7 +4304,7 @@ class QuantumCircuit:
target._parameters = None
# This is deliberately eager, because we want the side effect of clearing the table.
all_references = [
(parameter, value, target._parameter_table.pop(parameter, ()))
(parameter, value, target._data.pop_param(parameter.uuid.int, parameter.name, ()))
for parameter, value in parameter_binds.items()
]
seen_operations = {}
@ -4291,20 +4315,28 @@ class QuantumCircuit:
if isinstance(bound_value, ParameterExpression)
else ()
)
for operation, index in references:
seen_operations[id(operation)] = operation
if operation is ParameterTable.GLOBAL_PHASE:
for inst_index, index in references:
if inst_index == self._data.global_phase_param_index:
operation = None
seen_operations[inst_index] = None
assignee = target.global_phase
validate = _normalize_global_phase
else:
operation = target._data[inst_index].operation
seen_operations[inst_index] = operation
assignee = operation.params[index]
validate = operation.validate_parameter
if isinstance(assignee, ParameterExpression):
new_parameter = assignee.assign(to_bind, bound_value)
for parameter in update_parameters:
if parameter not in target._parameter_table:
target._parameter_table[parameter] = ParameterReferences(())
target._parameter_table[parameter].add((operation, index))
if not target._data.contains_param(parameter.uuid.int):
target._data.add_new_parameter(parameter, inst_index, index)
else:
target._data.update_parameter_entry(
parameter.uuid.int,
inst_index,
index,
)
if not new_parameter.parameters:
new_parameter = validate(new_parameter.numeric())
elif isinstance(assignee, QuantumCircuit):
@ -4316,12 +4348,18 @@ class QuantumCircuit:
f"Saw an unknown type during symbolic binding: {assignee}."
" This may indicate an internal logic error in symbol tracking."
)
if operation is ParameterTable.GLOBAL_PHASE:
if inst_index == self._data.global_phase_param_index:
# We've already handled parameter table updates in bulk, so we need to skip the
# public setter trying to do it again.
target._global_phase = new_parameter
target._data.global_phase = new_parameter
else:
operation.params[index] = new_parameter
temp_params = operation.params
temp_params[index] = new_parameter
operation.params = temp_params
target._data.setitem_no_param_table_update(
inst_index,
target._data[inst_index].replace(operation=operation, params=temp_params),
)
# After we've been through everything at the top level, make a single visit to each
# operation we've seen, rebinding its definition if necessary.
@ -4368,6 +4406,7 @@ class QuantumCircuit:
for gate, calibrations in target._calibrations.items()
),
)
target._parameters = None
return None if inplace else target
def _unroll_param_dict(
@ -4450,9 +4489,7 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.h import HGate
return self.append(HGate(), [qubit], [], copy=False)
return self._append_standard_gate(StandardGate.HGate, [], qargs=[qubit])
def ch(
self,
@ -4496,9 +4533,7 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.i import IGate
return self.append(IGate(), [qubit], [], copy=False)
return self._append_standard_gate(StandardGate.IGate, None, qargs=[qubit])
def ms(self, theta: ParameterValueType, qubits: Sequence[QubitSpecifier]) -> InstructionSet:
"""Apply :class:`~qiskit.circuit.library.MSGate`.
@ -4529,9 +4564,7 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.p import PhaseGate
return self.append(PhaseGate(theta), [qubit], [], copy=False)
return self._append_standard_gate(StandardGate.PhaseGate, [theta], qargs=[qubit])
def cp(
self,
@ -4712,9 +4745,7 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.rx import RXGate
return self.append(RXGate(theta, label=label), [qubit], [], copy=False)
return self._append_standard_gate(StandardGate.RXGate, [theta], [qubit], None, label=label)
def crx(
self,
@ -4783,9 +4814,7 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.ry import RYGate
return self.append(RYGate(theta, label=label), [qubit], [], copy=False)
return self._append_standard_gate(StandardGate.RYGate, [theta], [qubit], None, label=label)
def cry(
self,
@ -4851,9 +4880,7 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.rz import RZGate
return self.append(RZGate(phi), [qubit], [], copy=False)
return self._append_standard_gate(StandardGate.RZGate, [phi], [qubit], None)
def crz(
self,
@ -4937,9 +4964,9 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.ecr import ECRGate
return self.append(ECRGate(), [qubit1, qubit2], [], copy=False)
return self._append_standard_gate(
StandardGate.ECRGate, [], qargs=[qubit1, qubit2], cargs=None
)
def s(self, qubit: QubitSpecifier) -> InstructionSet:
"""Apply :class:`~qiskit.circuit.library.SGate`.
@ -5044,9 +5071,12 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.swap import SwapGate
return self.append(SwapGate(), [qubit1, qubit2], [], copy=False)
return self._append_standard_gate(
StandardGate.SwapGate,
[],
qargs=[qubit1, qubit2],
cargs=None,
)
def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet:
"""Apply :class:`~qiskit.circuit.library.iSwapGate`.
@ -5107,9 +5137,7 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.sx import SXGate
return self.append(SXGate(), [qubit], [], copy=False)
return self._append_standard_gate(StandardGate.SXGate, None, qargs=[qubit])
def sxdg(self, qubit: QubitSpecifier) -> InstructionSet:
"""Apply :class:`~qiskit.circuit.library.SXdgGate`.
@ -5207,9 +5235,7 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.u import UGate
return self.append(UGate(theta, phi, lam), [qubit], [], copy=False)
return self._append_standard_gate(StandardGate.UGate, [theta, phi, lam], qargs=[qubit])
def cu(
self,
@ -5262,9 +5288,7 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.x import XGate
return self.append(XGate(label=label), [qubit], [], copy=False)
return self._append_standard_gate(StandardGate.XGate, None, qargs=[qubit], label=label)
def cx(
self,
@ -5288,14 +5312,17 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
if ctrl_state is not None:
from .library.standard_gates.x import CXGate
from .library.standard_gates.x import CXGate
return self.append(
CXGate(label=label, ctrl_state=ctrl_state),
[control_qubit, target_qubit],
[],
copy=False,
return self.append(
CXGate(label=label, ctrl_state=ctrl_state),
[control_qubit, target_qubit],
[],
copy=False,
)
return self._append_standard_gate(
StandardGate.CXGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label
)
def dcx(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet:
@ -5336,13 +5363,20 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.x import CCXGate
if ctrl_state is not None:
from .library.standard_gates.x import CCXGate
return self.append(
CCXGate(ctrl_state=ctrl_state),
[control_qubit1, control_qubit2, target_qubit],
return self.append(
CCXGate(ctrl_state=ctrl_state),
[control_qubit1, control_qubit2, target_qubit],
[],
copy=False,
)
return self._append_standard_gate(
StandardGate.CCXGate,
[],
copy=False,
qargs=[control_qubit1, control_qubit2, target_qubit],
cargs=None,
)
def mcx(
@ -5440,9 +5474,7 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.y import YGate
return self.append(YGate(), [qubit], [], copy=False)
return self._append_standard_gate(StandardGate.YGate, None, qargs=[qubit])
def cy(
self,
@ -5466,13 +5498,18 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.y import CYGate
if ctrl_state is not None:
from .library.standard_gates.y import CYGate
return self.append(
CYGate(label=label, ctrl_state=ctrl_state),
[control_qubit, target_qubit],
[],
copy=False,
return self.append(
CYGate(label=label, ctrl_state=ctrl_state),
[control_qubit, target_qubit],
[],
copy=False,
)
return self._append_standard_gate(
StandardGate.CYGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label
)
def z(self, qubit: QubitSpecifier) -> InstructionSet:
@ -5486,9 +5523,7 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.z import ZGate
return self.append(ZGate(), [qubit], [], copy=False)
return self._append_standard_gate(StandardGate.ZGate, None, qargs=[qubit])
def cz(
self,
@ -5512,13 +5547,18 @@ class QuantumCircuit:
Returns:
A handle to the instructions created.
"""
from .library.standard_gates.z import CZGate
if ctrl_state is not None:
from .library.standard_gates.z import CZGate
return self.append(
CZGate(label=label, ctrl_state=ctrl_state),
[control_qubit, target_qubit],
[],
copy=False,
return self.append(
CZGate(label=label, ctrl_state=ctrl_state),
[control_qubit, target_qubit],
[],
copy=False,
)
return self._append_standard_gate(
StandardGate.CZGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label
)
def ccz(
@ -5907,36 +5947,9 @@ class QuantumCircuit:
if not self._data:
raise CircuitError("This circuit contains no instructions.")
instruction = self._data.pop()
if isinstance(instruction.operation, Instruction):
self._update_parameter_table_on_instruction_removal(instruction)
self._parameters = None
return instruction
def _update_parameter_table_on_instruction_removal(self, instruction: CircuitInstruction):
"""Update the :obj:`.ParameterTable` of this circuit given that an instance of the given
``instruction`` has just been removed from the circuit.
.. note::
This does not account for the possibility for the same instruction instance being added
more than once to the circuit. At the time of writing (2021-11-17, main commit 271a82f)
there is a defensive ``deepcopy`` of parameterised instructions inside
:meth:`.QuantumCircuit.append`, so this should be safe. Trying to account for it would
involve adding a potentially quadratic-scaling loop to check each entry in ``data``.
"""
atomic_parameters: list[tuple[Parameter, int]] = []
for index, parameter in enumerate(instruction.operation.params):
if isinstance(parameter, (ParameterExpression, QuantumCircuit)):
atomic_parameters.extend((p, index) for p in parameter.parameters)
for atomic_parameter, index in atomic_parameters:
new_entries = self._parameter_table[atomic_parameter].copy()
new_entries.discard((instruction.operation, index))
if not new_entries:
del self._parameter_table[atomic_parameter]
# Invalidate cache.
self._parameters = None
else:
self._parameter_table[atomic_parameter] = new_entries
@typing.overload
def while_loop(
self,
@ -6582,13 +6595,15 @@ class _OuterCircuitScopeInterface(CircuitScopeInterface):
def instructions(self):
return self.circuit._data
def append(self, instruction):
def append(self, instruction, *, _standard_gate: bool = False):
# QuantumCircuit._append is semi-public, so we just call back to it.
return self.circuit._append(instruction)
return self.circuit._append(instruction, _standard_gate=_standard_gate)
def extend(self, data: CircuitData):
self.circuit._data.extend(data)
data.foreach_op(self.circuit._track_operation)
self.circuit._parameters = None
self.circuit.duration = None
self.circuit.unit = "dt"
def resolve_classical_resource(self, specifier):
# This is slightly different to cbit_argument_conversion, because it should not

View File

@ -45,8 +45,6 @@ class QuantumCircuitData(MutableSequence):
operation, qargs, cargs = value
value = self._resolve_legacy_value(operation, qargs, cargs)
self._circuit._data[key] = value
if isinstance(value.operation, Instruction):
self._circuit._update_parameter_table(value.operation)
def _resolve_legacy_value(self, operation, qargs, cargs) -> CircuitInstruction:
"""Resolve the old-style 3-tuple into the new :class:`CircuitInstruction` type."""
@ -76,7 +74,7 @@ class QuantumCircuitData(MutableSequence):
return CircuitInstruction(operation, tuple(qargs), tuple(cargs))
def insert(self, index, value):
self._circuit._data.insert(index, CircuitInstruction(None, (), ()))
self._circuit._data.insert(index, value.replace(qubits=(), clbits=()))
try:
self[index] = value
except CircuitError:

View File

@ -11,7 +11,6 @@
# that they have been altered from the originals.
"""Helper function for converting a circuit to an instruction."""
from qiskit.circuit.parametertable import ParameterTable, ParameterReferences
from qiskit.exceptions import QiskitError
from qiskit.circuit.instruction import Instruction
from qiskit.circuit.quantumregister import QuantumRegister
@ -121,7 +120,7 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None
regs.append(creg)
clbit_map = {bit: creg[idx] for idx, bit in enumerate(circuit.clbits)}
operation_map = {id(ParameterTable.GLOBAL_PHASE): ParameterTable.GLOBAL_PHASE}
operation_map = {}
def fix_condition(op):
original_id = id(op)
@ -149,15 +148,6 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None
qc = QuantumCircuit(*regs, name=out_instruction.name)
qc._data = data
qc._parameter_table = ParameterTable(
{
param: ParameterReferences(
(operation_map[id(operation)], param_index)
for operation, param_index in target._parameter_table[param]
)
for param in target._parameter_table
}
)
if circuit.global_phase:
qc.global_phase = circuit.global_phase

View File

@ -252,6 +252,9 @@ class GlobalNamespace:
def __setitem__(self, name_str, instruction):
self._data[name_str] = instruction.base_class
self._data[id(instruction)] = name_str
ctrl_state = str(getattr(instruction, "ctrl_state", ""))
self._data[f"{instruction.name}_{ctrl_state}_{instruction.params}"] = name_str
def __getitem__(self, key):
if isinstance(key, Instruction):
@ -262,7 +265,9 @@ class GlobalNamespace:
pass
# Built-in gates.
if key.name not in self._data:
raise KeyError(key)
# Registerd qiskit standard gate without stgates.inc
ctrl_state = str(getattr(key, "ctrl_state", ""))
return self._data[f"{key.name}_{ctrl_state}_{key.params}"]
return key.name
return self._data[key]
@ -1102,7 +1107,8 @@ def _infer_variable_declaration(
# _should_ be an intrinsic part of the parameter, or somewhere publicly accessible, but
# Terra doesn't have those concepts yet. We can only try and guess at the type by looking
# at all the places it's used in the circuit.
for instruction, index in circuit._parameter_table[parameter]:
for instr_index, index in circuit._data._get_param(parameter.uuid.int):
instruction = circuit.data[instr_index].operation
if isinstance(instruction, ForLoopOp):
# The parameters of ForLoopOp are (indexset, loop_parameter, body).
if index == 1:

View File

@ -452,8 +452,7 @@ class CNOTDihedral(BaseOperator, AdjointMixin):
new_qubits = [bit_indices[tup] for tup in instruction.qubits]
if instruction.operation.name == "p":
params = 2 * np.pi - instruction.operation.params[0]
instruction.operation.params[0] = params
new_circ.append(instruction.operation, new_qubits)
new_circ.p(params, new_qubits)
elif instruction.operation.name == "t":
instruction.operation.name = "tdg"
new_circ.append(instruction.operation, new_qubits)

View File

@ -361,17 +361,17 @@ class PadDynamicalDecoupling(BasePadding):
theta, phi, lam, phase = OneQubitEulerDecomposer().angles_and_phase(u_inv)
if isinstance(next_node, DAGOpNode) and isinstance(next_node.op, (UGate, U3Gate)):
# Absorb the inverse into the successor (from left in circuit)
theta_r, phi_r, lam_r = next_node.op.params
next_node.op.params = Optimize1qGates.compose_u3(
theta_r, phi_r, lam_r, theta, phi, lam
)
op = next_node.op
theta_r, phi_r, lam_r = op.params
op.params = Optimize1qGates.compose_u3(theta_r, phi_r, lam_r, theta, phi, lam)
next_node.op = op
sequence_gphase += phase
elif isinstance(prev_node, DAGOpNode) and isinstance(prev_node.op, (UGate, U3Gate)):
# Absorb the inverse into the predecessor (from right in circuit)
theta_l, phi_l, lam_l = prev_node.op.params
prev_node.op.params = Optimize1qGates.compose_u3(
theta, phi, lam, theta_l, phi_l, lam_l
)
op = prev_node.op
theta_l, phi_l, lam_l = op.params
op.params = Optimize1qGates.compose_u3(theta, phi, lam, theta_l, phi_l, lam_l)
prev_node.op = op
sequence_gphase += phase
else:
# Don't do anything if there's no single-qubit gate to absorb the inverse

View File

@ -70,8 +70,9 @@ class BaseScheduler(AnalysisPass):
duration = dag.calibrations[node.op.name][cal_key].duration
# Note that node duration is updated (but this is analysis pass)
node.op = node.op.to_mutable()
node.op.duration = duration
op = node.op.to_mutable()
op.duration = duration
node.op = op
else:
duration = node.op.duration

View File

@ -105,9 +105,10 @@ class TimeUnitConversion(TransformationPass):
)
except TranspilerError:
continue
node.op = node.op.to_mutable()
node.op.duration = duration
node.op.unit = time_unit
op = node.op.to_mutable()
op.duration = duration
op.unit = time_unit
node.op = op
self.property_set["time_unit"] = time_unit
return dag

View File

@ -0,0 +1,79 @@
---
features_circuits:
- |
A native rust representation of Qiskit's standard gate library has been added. When a standard gate
is added to a :class:`~.QuantumCircuit` or :class:`~.DAGCircuit` it is now represented in a more
efficient manner directly in Rust seamlessly. Accessing that gate object from a circuit or dag will
return a new Python object representing the standard gate. This leads to faster and more efficient
transpilation and manipulation of circuits for functionality written in Rust.
features_misc:
- |
Added a new build-time environment variable ``QISKIT_NO_CACHE_GATES`` which
when set to a value of ``1`` (i.e. ``QISKIT_NO_CACHE_GATES=1``) which
decreases the memory overhead of a :class:`.CircuitInstruction` and
:class:`.DAGOpNode` object at the cost of decreased runtime on multiple
accesses to :attr:`.CircuitInstruction.operation` and :attr:`.DAGOpNode.op`.
If this environment variable is set when building the Qiskit python package
from source the caching of the return of these attributes will be disabled.
upgrade_circuits:
- |
The :class:`.Operation` instances of :attr:`.DAGOpNode.op`
being returned will not necessarily share a common reference to the
underlying object anymore. This was never guaranteed to be the case and
mutating the :attr:`~.DAGOpNode.op` directly by reference
was unsound and always likely to corrupt the dag's internal state tracking
Due to the internal refactor of the :class:`.QuantumCircuit` and
:class:`.DAGCircuit` to store standard gates in rust the output object from
:attr:`.DAGOpNode.op` will now likely be a copy instead of a shared instance. If you
need to mutate an element should ensure that you either do::
op = dag_node.op
op.params[0] = 3.14159
dag_node.op = op
or::
op = dag_node.op
op.params[0] = 3.14159
dag.substitute_node(dag_node, op)
instead of doing something like::
dag_node.op.params[0] = 3.14159
which will not work for any standard gates in this release. It would have
likely worked by chance in a previous release but was never an API guarantee.
- |
The :class:`.Operation` instances of :attr:`.CircuitInstruction.operation`
being returned will not necessarily share a common reference to the
underlying object anymore. This was never guaranteed to be the case and
mutating the :attr:`~.CircuitInstruction.operation` directly by reference
was unsound and always likely to corrupt the circuit, especially when
parameters were in use. Due to the internal refactor of the QuantumCircuit
to store standard gates in rust the output object from
:attr:`.CircuitInstruction.operation` will now likely be a copy instead
of a shared instance. If you need to mutate an element in the circuit (which
is strongly **not** recommended as it's inefficient and error prone) you
should ensure that you do::
from qiskit.circuit import QuantumCircuit
qc = QuantumCircuit(1)
qc.p(0)
op = qc.data[0].operation
op.params[0] = 3.14
qc.data[0] = qc.data[0].replace(operation=op)
instead of doing something like::
from qiskit.circuit import QuantumCircuit
qc = QuantumCircuit(1)
qc.p(0)
qc.data[0].operation.params[0] = 3.14
which will not work for any standard gates in this release. It would have
likely worked by chance in a previous release but was never an API guarantee.

View File

@ -19,7 +19,7 @@ seaborn>=0.9.0
# Functionality and accelerators.
qiskit-aer
qiskit-qasm3-import
qiskit-qasm3-import>=0.5.0
python-constraint>=1.4
cvxpy
scikit-learn>=0.20.0

View File

@ -30,6 +30,17 @@ from setuptools_rust import Binding, RustExtension
# it's an editable installation.
rust_debug = True if os.getenv("RUST_DEBUG") == "1" else None
# If QISKIT_NO_CACHE_GATES is set then don't enable any features while building
#
# TODO: before final release we should reverse this by default once the default transpiler pass
# is all in rust (default to no caching and make caching an opt-in feature). This is opt-out
# right now to avoid the runtime overhead until we are leveraging the rust gates infrastructure.
if os.getenv("QISKIT_NO_CACHE_GATES") == "1":
features = []
else:
features = ["cache_pygates"]
setup(
rust_extensions=[
RustExtension(
@ -37,6 +48,7 @@ setup(
"crates/pyext/Cargo.toml",
binding=Binding.PyO3,
debug=rust_debug,
features=features,
)
],
options={"bdist_wheel": {"py_limited_api": "cp38"}},

View File

@ -77,17 +77,17 @@ class TestBlueprintCircuit(QiskitTestCase):
with self.subTest(msg="after building"):
self.assertGreater(len(mock._data), 0)
self.assertEqual(len(mock._parameter_table), 1)
self.assertEqual(mock._data.num_params(), 1)
mock._invalidate()
with self.subTest(msg="after invalidating"):
self.assertFalse(mock._is_built)
self.assertEqual(len(mock._parameter_table), 0)
self.assertEqual(mock._data.num_params(), 0)
mock._build()
with self.subTest(msg="after re-building"):
self.assertGreater(len(mock._data), 0)
self.assertEqual(len(mock._parameter_table), 1)
self.assertEqual(mock._data.num_params(), 1)
def test_calling_attributes_works(self):
"""Test that the circuit is constructed when attributes are called."""

View File

@ -187,12 +187,20 @@ class TestQuantumCircuitData(QiskitTestCase):
def test_map_ops(self):
"""Test all operations are replaced."""
qr = QuantumRegister(5)
# Use a custom gate to ensure we get a gate class returned and not
# a standard gate.
class CustomXGate(XGate):
"""A custom X gate that doesn't have rust native representation."""
_standard_gate = None
data_list = [
CircuitInstruction(XGate(), [qr[0]], []),
CircuitInstruction(XGate(), [qr[1]], []),
CircuitInstruction(XGate(), [qr[2]], []),
CircuitInstruction(XGate(), [qr[3]], []),
CircuitInstruction(XGate(), [qr[4]], []),
CircuitInstruction(CustomXGate(), [qr[0]], []),
CircuitInstruction(CustomXGate(), [qr[1]], []),
CircuitInstruction(CustomXGate(), [qr[2]], []),
CircuitInstruction(CustomXGate(), [qr[3]], []),
CircuitInstruction(CustomXGate(), [qr[4]], []),
]
data = CircuitData(qubits=list(qr), data=data_list)
data.map_ops(lambda op: op.to_mutable())
@ -828,6 +836,9 @@ class TestQuantumCircuitInstructionData(QiskitTestCase):
qc0.append(rx, [0])
qc1.append(rx, [0])
qc0.assign_parameters({a: b}, inplace=True)
qc0_instance = next(iter(qc0._parameter_table[b]))[0]
qc1_instance = next(iter(qc1._parameter_table[a]))[0]
# A fancy way of doing qc0_instance = qc0.data[0] and qc1_instance = qc1.data[0]
# but this at least verifies the parameter table is point from the parameter to
# the correct instruction (which is the only one)
qc0_instance = qc0._data[next(iter(qc0._data._get_param(b.uuid.int)))[0]]
qc1_instance = qc1._data[next(iter(qc1._data._get_param(a.uuid.int)))[0]]
self.assertNotEqual(qc0_instance, qc1_instance)

View File

@ -593,7 +593,7 @@ class TestCircuitOperations(QiskitTestCase):
qc.clear()
self.assertEqual(len(qc.data), 0)
self.assertEqual(len(qc._parameter_table), 0)
self.assertEqual(qc._data.num_params(), 0)
def test_barrier(self):
"""Test multiple argument forms of barrier."""

View File

@ -357,7 +357,8 @@ class TestCircuitCompose(QiskitTestCase):
self.assertIsNot(should_copy.data[-1].operation, parametric.data[-1].operation)
self.assertEqual(should_copy.data[-1].operation, parametric.data[-1].operation)
forbid_copy = base.compose(parametric, qubits=[0], copy=False)
self.assertIs(forbid_copy.data[-1].operation, parametric.data[-1].operation)
# For standard gates a fresh copy is returned from the data list each time
self.assertEqual(forbid_copy.data[-1].operation, parametric.data[-1].operation)
conditional = QuantumCircuit(1, 1)
conditional.x(0).c_if(conditional.clbits[0], True)

View File

@ -577,14 +577,14 @@ class TestInstructions(QiskitTestCase):
instructions.add(instruction, [Qubit()], [])
register = ClassicalRegister(2)
instructions.c_if(register, 0)
self.assertIs(instruction.condition[0], register)
self.assertIs(instructions[0].operation.condition[0], register)
with self.subTest("accepts arbitrary bit"):
instruction = RZGate(0)
instructions = InstructionSet()
instructions.add(instruction, [Qubit()], [])
bit = Clbit()
instructions.c_if(bit, 0)
self.assertIs(instruction.condition[0], bit)
self.assertIs(instructions[0].operation.condition[0], bit)
with self.subTest("rejects index"):
instruction = RZGate(0)
instructions = InstructionSet()
@ -617,7 +617,7 @@ class TestInstructions(QiskitTestCase):
bit = Clbit()
instructions.c_if(bit, 0)
dummy_requester.assert_called_once_with(bit)
self.assertIs(instruction.condition[0], sentinel_bit)
self.assertIs(instructions[0].operation.condition[0], sentinel_bit)
with self.subTest("calls requester with index"):
dummy_requester.reset_mock()
instruction = RZGate(0)
@ -626,7 +626,7 @@ class TestInstructions(QiskitTestCase):
index = 0
instructions.c_if(index, 0)
dummy_requester.assert_called_once_with(index)
self.assertIs(instruction.condition[0], sentinel_bit)
self.assertIs(instructions[0].operation.condition[0], sentinel_bit)
with self.subTest("calls requester with register"):
dummy_requester.reset_mock()
instruction = RZGate(0)
@ -635,7 +635,7 @@ class TestInstructions(QiskitTestCase):
register = ClassicalRegister(2)
instructions.c_if(register, 0)
dummy_requester.assert_called_once_with(register)
self.assertIs(instruction.condition[0], sentinel_register)
self.assertIs(instructions[0].operation.condition[0], sentinel_register)
with self.subTest("calls requester only once when broadcast"):
dummy_requester.reset_mock()
instruction_list = [RZGate(0), RZGate(0), RZGate(0)]
@ -646,7 +646,7 @@ class TestInstructions(QiskitTestCase):
instructions.c_if(register, 0)
dummy_requester.assert_called_once_with(register)
for instruction in instruction_list:
self.assertIs(instruction.condition[0], sentinel_register)
self.assertIs(instructions[0].operation.condition[0], sentinel_register)
def test_label_type_enforcement(self):
"""Test instruction label type enforcement."""

View File

@ -102,7 +102,6 @@ class TestIsometry(QiskitTestCase):
# Simulate the decomposed gate
unitary = Operator(qc).data
iso_from_circuit = unitary[::, 0 : 2**num_q_input]
self.assertTrue(np.allclose(iso_from_circuit, iso))
@data(

View File

@ -26,7 +26,7 @@ import qiskit.circuit.library as circlib
from qiskit.circuit.library.standard_gates.rz import RZGate
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
from qiskit.circuit import Gate, Instruction, Parameter, ParameterExpression, ParameterVector
from qiskit.circuit.parametertable import ParameterReferences, ParameterTable, ParameterView
from qiskit.circuit.parametertable import ParameterView
from qiskit.circuit.exceptions import CircuitError
from qiskit.compiler import assemble, transpile
from qiskit import pulse
@ -45,8 +45,6 @@ def raise_if_parameter_table_invalid(circuit):
CircuitError: if QuantumCircuit and ParameterTable are inconsistent.
"""
table = circuit._parameter_table
# Assert parameters present in circuit match those in table.
circuit_parameters = {
parameter
@ -55,7 +53,7 @@ def raise_if_parameter_table_invalid(circuit):
for parameter in param.parameters
if isinstance(param, ParameterExpression)
}
table_parameters = set(table._table.keys())
table_parameters = set(circuit._data.get_params_unsorted())
if circuit_parameters != table_parameters:
raise CircuitError(
@ -67,8 +65,10 @@ def raise_if_parameter_table_invalid(circuit):
# Assert parameter locations in table are present in circuit.
circuit_instructions = [instr.operation for instr in circuit._data]
for parameter, instr_list in table.items():
for instr, param_index in instr_list:
for parameter in table_parameters:
instr_list = circuit._data._get_param(parameter.uuid.int)
for instr_index, param_index in instr_list:
instr = circuit.data[instr_index].operation
if instr not in circuit_instructions:
raise CircuitError(f"ParameterTable instruction not present in circuit: {instr}.")
@ -88,13 +88,15 @@ def raise_if_parameter_table_invalid(circuit):
)
# Assert circuit has no other parameter locations other than those in table.
for instruction in circuit._data:
for instr_index, instruction in enumerate(circuit._data):
for param_index, param in enumerate(instruction.operation.params):
if isinstance(param, ParameterExpression):
parameters = param.parameters
for parameter in parameters:
if (instruction.operation, param_index) not in table[parameter]:
if (instr_index, param_index) not in circuit._data._get_param(
parameter.uuid.int
):
raise CircuitError(
"Found parameterized instruction not "
"present in table. Instruction: {} "
@ -158,15 +160,19 @@ class TestParameters(QiskitTestCase):
self.assertIsNot(qc.data[-1].operation, gate_param)
self.assertEqual(qc.data[-1].operation, gate_param)
# Standard gates are not stored as Python objects so a fresh object
# is always instantiated on accessing `CircuitInstruction.operation`
qc.append(gate_param, [0], copy=False)
self.assertIs(qc.data[-1].operation, gate_param)
self.assertEqual(qc.data[-1].operation, gate_param)
qc.append(gate_expr, [0], copy=True)
self.assertIsNot(qc.data[-1].operation, gate_expr)
self.assertEqual(qc.data[-1].operation, gate_expr)
# Standard gates are not stored as Python objects so a fresh object
# is always instantiated on accessing `CircuitInstruction.operation`
qc.append(gate_expr, [0], copy=False)
self.assertIs(qc.data[-1].operation, gate_expr)
self.assertEqual(qc.data[-1].operation, gate_expr)
def test_parameters_property(self):
"""Test instantiating gate with variable parameters"""
@ -177,10 +183,9 @@ class TestParameters(QiskitTestCase):
qc = QuantumCircuit(qr)
rxg = RXGate(theta)
qc.append(rxg, [qr[0]], [])
vparams = qc._parameter_table
self.assertEqual(len(vparams), 1)
self.assertIs(theta, next(iter(vparams)))
self.assertEqual(rxg, next(iter(vparams[theta]))[0])
self.assertEqual(qc._data.num_params(), 1)
self.assertIs(theta, next(iter(qc._data.get_params_unsorted())))
self.assertEqual(rxg, qc.data[next(iter(qc._data._get_param(theta.uuid.int)))[0]].operation)
def test_parameters_property_by_index(self):
"""Test getting parameters by index"""
@ -553,12 +558,12 @@ class TestParameters(QiskitTestCase):
qc.rx(theta, 0)
qc.ry(phi, 0)
self.assertEqual(len(qc._parameter_table[theta]), 1)
self.assertEqual(len(qc._parameter_table[phi]), 1)
self.assertEqual(qc._data._get_entry_count(theta), 1)
self.assertEqual(qc._data._get_entry_count(phi), 1)
qc.assign_parameters({theta: -phi}, inplace=True)
self.assertEqual(len(qc._parameter_table[phi]), 2)
self.assertEqual(qc._data._get_entry_count(phi), 2)
def test_expression_partial_binding_zero(self):
"""Verify that binding remains possible even if a previous partial bind
@ -580,7 +585,6 @@ class TestParameters(QiskitTestCase):
fbqc = pqc.assign_parameters({phi: 1})
self.assertEqual(fbqc.parameters, set())
self.assertIsInstance(fbqc.data[0].operation.params[0], int)
self.assertEqual(float(fbqc.data[0].operation.params[0]), 0)
def test_raise_if_assigning_params_not_in_circuit(self):
@ -614,7 +618,7 @@ class TestParameters(QiskitTestCase):
qc.append(gate, [0], [])
qc.append(gate, [0], [])
qc2 = qc.assign_parameters({theta: 1.0})
self.assertEqual(len(qc2._parameter_table), 0)
self.assertEqual(qc2._data.num_params(), 0)
for instruction in qc2.data:
self.assertEqual(float(instruction.operation.params[0]), 1.0)
@ -2170,155 +2174,6 @@ class TestParameterEquality(QiskitTestCase):
self.assertEqual(phi._parameter_symbols, cos_phi._parameter_symbols)
class TestParameterReferences(QiskitTestCase):
"""Test the ParameterReferences class."""
def test_equal_inst_diff_instance(self):
"""Different value equal instructions are treated as distinct."""
theta = Parameter("theta")
gate1 = RZGate(theta)
gate2 = RZGate(theta)
self.assertIsNot(gate1, gate2)
self.assertEqual(gate1, gate2)
refs = ParameterReferences(((gate1, 0), (gate2, 0)))
# test __contains__
self.assertIn((gate1, 0), refs)
self.assertIn((gate2, 0), refs)
gate_ids = {id(gate1), id(gate2)}
self.assertEqual(gate_ids, {id(gate) for gate, _ in refs})
self.assertTrue(all(idx == 0 for _, idx in refs))
def test_pickle_unpickle(self):
"""Membership testing after pickle/unpickle."""
theta = Parameter("theta")
gate1 = RZGate(theta)
gate2 = RZGate(theta)
self.assertIsNot(gate1, gate2)
self.assertEqual(gate1, gate2)
refs = ParameterReferences(((gate1, 0), (gate2, 0)))
to_pickle = (gate1, refs)
pickled = pickle.dumps(to_pickle)
(gate1_new, refs_new) = pickle.loads(pickled)
self.assertEqual(len(refs_new), len(refs))
self.assertNotIn((gate1, 0), refs_new)
self.assertIn((gate1_new, 0), refs_new)
def test_equal_inst_same_instance(self):
"""Referentially equal instructions are treated as same."""
theta = Parameter("theta")
gate = RZGate(theta)
refs = ParameterReferences(((gate, 0), (gate, 0)))
self.assertIn((gate, 0), refs)
self.assertEqual(len(refs), 1)
self.assertIs(next(iter(refs))[0], gate)
self.assertEqual(next(iter(refs))[1], 0)
def test_extend_refs(self):
"""Extending references handles duplicates."""
theta = Parameter("theta")
ref0 = (RZGate(theta), 0)
ref1 = (RZGate(theta), 0)
ref2 = (RZGate(theta), 0)
refs = ParameterReferences((ref0,))
refs |= ParameterReferences((ref0, ref1, ref2, ref1, ref0))
self.assertEqual(refs, ParameterReferences((ref0, ref1, ref2)))
def test_copy_param_refs(self):
"""Copy of parameter references is a shallow copy."""
theta = Parameter("theta")
ref0 = (RZGate(theta), 0)
ref1 = (RZGate(theta), 0)
ref2 = (RZGate(theta), 0)
ref3 = (RZGate(theta), 0)
refs = ParameterReferences((ref0, ref1))
refs_copy = refs.copy()
# Check same gate instances in copy
gate_ids = {id(ref0[0]), id(ref1[0])}
self.assertEqual({id(gate) for gate, _ in refs_copy}, gate_ids)
# add new ref to original and check copy not modified
refs.add(ref2)
self.assertNotIn(ref2, refs_copy)
self.assertEqual(refs_copy, ParameterReferences((ref0, ref1)))
# add new ref to copy and check original not modified
refs_copy.add(ref3)
self.assertNotIn(ref3, refs)
self.assertEqual(refs, ParameterReferences((ref0, ref1, ref2)))
class TestParameterTable(QiskitTestCase):
"""Test the ParameterTable class."""
def test_init_param_table(self):
"""Parameter table init from mapping."""
p1 = Parameter("theta")
p2 = Parameter("theta")
ref0 = (RZGate(p1), 0)
ref1 = (RZGate(p1), 0)
ref2 = (RZGate(p2), 0)
mapping = {p1: ParameterReferences((ref0, ref1)), p2: ParameterReferences((ref2,))}
table = ParameterTable(mapping)
# make sure editing mapping doesn't change `table`
del mapping[p1]
self.assertEqual(table[p1], ParameterReferences((ref0, ref1)))
self.assertEqual(table[p2], ParameterReferences((ref2,)))
def test_set_references(self):
"""References replacement by parameter key."""
p1 = Parameter("theta")
ref0 = (RZGate(p1), 0)
ref1 = (RZGate(p1), 0)
table = ParameterTable()
table[p1] = ParameterReferences((ref0, ref1))
self.assertEqual(table[p1], ParameterReferences((ref0, ref1)))
table[p1] = ParameterReferences((ref1,))
self.assertEqual(table[p1], ParameterReferences((ref1,)))
def test_set_references_from_iterable(self):
"""Parameter table init from iterable."""
p1 = Parameter("theta")
ref0 = (RZGate(p1), 0)
ref1 = (RZGate(p1), 0)
ref2 = (RZGate(p1), 0)
table = ParameterTable({p1: ParameterReferences((ref0, ref1))})
table[p1] = (ref2, ref1, ref0)
self.assertEqual(table[p1], ParameterReferences((ref2, ref1, ref0)))
class TestParameterView(QiskitTestCase):
"""Test the ParameterView object."""

View File

@ -0,0 +1,143 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2024
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""Rust gate definition tests"""
from math import pi
from test import QiskitTestCase
import numpy as np
from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping
SKIP_LIST = {"cy", "ccx", "rx", "ry", "ecr", "sx"}
CUSTOM_MAPPING = {"x", "rz"}
class TestRustGateEquivalence(QiskitTestCase):
"""Tests that compile time rust gate definitions is correct."""
def setUp(self):
super().setUp()
self.standard_gates = get_standard_gate_name_mapping()
# Pre-warm gate mapping cache, this is needed so rust -> py conversion is done
qc = QuantumCircuit(3)
for gate in self.standard_gates.values():
if getattr(gate, "_standard_gate", None):
if gate.params:
gate = gate.base_class(*[pi] * len(gate.params))
qc.append(gate, list(range(gate.num_qubits)))
def test_definitions(self):
"""Test definitions are the same in rust space."""
for name, gate_class in self.standard_gates.items():
standard_gate = getattr(gate_class, "_standard_gate", None)
if name in SKIP_LIST:
# gate does not have a rust definition yet
continue
if standard_gate is None:
# gate is not in rust yet
continue
with self.subTest(name=name):
params = [pi] * standard_gate._num_params()
py_def = gate_class.base_class(*params).definition
rs_def = standard_gate._get_definition(params)
if py_def is None:
self.assertIsNone(rs_def)
else:
rs_def = QuantumCircuit._from_circuit_data(rs_def)
for rs_inst, py_inst in zip(rs_def._data, py_def._data):
# Rust uses U but python still uses U3 and u2
if rs_inst.operation.name == "u":
if py_inst.operation.name == "u3":
self.assertEqual(rs_inst.operation.params, py_inst.operation.params)
elif py_inst.operation.name == "u2":
self.assertEqual(
rs_inst.operation.params,
[
pi / 2,
py_inst.operation.params[0],
py_inst.operation.params[1],
],
)
self.assertEqual(
[py_def.find_bit(x).index for x in py_inst.qubits],
[rs_def.find_bit(x).index for x in rs_inst.qubits],
)
# Rust uses P but python still uses u1
elif rs_inst.operation.name == "p":
self.assertEqual(py_inst.operation.name, "u1")
self.assertEqual(rs_inst.operation.params, py_inst.operation.params)
self.assertEqual(
[py_def.find_bit(x).index for x in py_inst.qubits],
[rs_def.find_bit(x).index for x in rs_inst.qubits],
)
else:
self.assertEqual(py_inst.operation.name, rs_inst.operation.name)
self.assertEqual(rs_inst.operation.params, py_inst.operation.params)
self.assertEqual(
[py_def.find_bit(x).index for x in py_inst.qubits],
[rs_def.find_bit(x).index for x in rs_inst.qubits],
)
def test_matrix(self):
"""Test matrices are the same in rust space."""
for name, gate_class in self.standard_gates.items():
standard_gate = getattr(gate_class, "_standard_gate", None)
if standard_gate is None:
# gate is not in rust yet
continue
with self.subTest(name=name):
params = [pi] * standard_gate._num_params()
py_def = gate_class.base_class(*params).to_matrix()
rs_def = standard_gate._to_matrix(params)
np.testing.assert_allclose(rs_def, py_def)
def test_name(self):
"""Test that the gate name properties match in rust space."""
for name, gate_class in self.standard_gates.items():
standard_gate = getattr(gate_class, "_standard_gate", None)
if standard_gate is None:
# gate is not in rust yet
continue
with self.subTest(name=name):
self.assertEqual(gate_class.name, standard_gate.name)
def test_num_qubits(self):
"""Test the number of qubits are the same in rust space."""
for name, gate_class in self.standard_gates.items():
standard_gate = getattr(gate_class, "_standard_gate", None)
if standard_gate is None:
# gate is not in rust yet
continue
with self.subTest(name=name):
self.assertEqual(gate_class.num_qubits, standard_gate.num_qubits)
def test_num_params(self):
"""Test the number of parameters are the same in rust space."""
for name, gate_class in self.standard_gates.items():
standard_gate = getattr(gate_class, "_standard_gate", None)
if standard_gate is None:
# gate is not in rust yet
continue
with self.subTest(name=name):
self.assertEqual(
len(gate_class.params), standard_gate.num_params, msg=f"{name} not equal"
)

View File

@ -1946,7 +1946,7 @@ class TestCircuitQASM3ExporterTemporaryCasesWithBadParameterisation(QiskitTestCa
def test_basis_gates(self):
"""Teleportation with physical qubits"""
qc = QuantumCircuit(3, 2)
first_h = qc.h(1)[0].operation
qc.h(1)
qc.cx(1, 2)
qc.barrier()
qc.cx(0, 1)
@ -1957,52 +1957,51 @@ class TestCircuitQASM3ExporterTemporaryCasesWithBadParameterisation(QiskitTestCa
first_x = qc.x(2).c_if(qc.clbits[1], 1)[0].operation
qc.z(2).c_if(qc.clbits[0], 1)
u2 = first_h.definition.data[0].operation
u3_1 = u2.definition.data[0].operation
u3_2 = first_x.definition.data[0].operation
expected_qasm = "\n".join(
[
"OPENQASM 3.0;",
f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{",
" U(pi/2, 0, pi) _gate_q_0;",
"}",
f"gate u2_{id(u2)}(_gate_p_0, _gate_p_1) _gate_q_0 {{",
f" u3_{id(u3_1)}(pi/2, 0, pi) _gate_q_0;",
"}",
"gate h _gate_q_0 {",
f" u2_{id(u2)}(0, pi) _gate_q_0;",
"}",
f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{",
" U(pi, 0, pi) _gate_q_0;",
"}",
"gate x _gate_q_0 {",
f" u3_{id(u3_2)}(pi, 0, pi) _gate_q_0;",
"}",
"bit[2] c;",
"qubit[3] q;",
"h q[1];",
"cx q[1], q[2];",
"barrier q[0], q[1], q[2];",
"cx q[0], q[1];",
"h q[0];",
"barrier q[0], q[1], q[2];",
"c[0] = measure q[0];",
"c[1] = measure q[1];",
"barrier q[0], q[1], q[2];",
"if (c[1]) {",
" x q[2];",
"}",
"if (c[0]) {",
" z q[2];",
"}",
"",
]
)
self.assertEqual(
Exporter(includes=[], basis_gates=["cx", "z", "U"]).dumps(qc),
expected_qasm,
)
id_len = len(str(id(first_x)))
expected_qasm = [
"OPENQASM 3.0;",
re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len),
" U(pi/2, 0, pi) _gate_q_0;",
"}",
re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len),
re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len),
"}",
"gate h _gate_q_0 {",
re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len),
"}",
re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len),
" U(pi, 0, pi) _gate_q_0;",
"}",
"gate x _gate_q_0 {",
re.compile(r" u3_\d{%s}\(pi, 0, pi\) _gate_q_0;" % id_len),
"}",
"bit[2] c;",
"qubit[3] q;",
"h q[1];",
"cx q[1], q[2];",
"barrier q[0], q[1], q[2];",
"cx q[0], q[1];",
"h q[0];",
"barrier q[0], q[1], q[2];",
"c[0] = measure q[0];",
"c[1] = measure q[1];",
"barrier q[0], q[1], q[2];",
"if (c[1]) {",
" x q[2];",
"}",
"if (c[0]) {",
" z q[2];",
"}",
"",
]
res = Exporter(includes=[], basis_gates=["cx", "z", "U"]).dumps(qc).splitlines()
for result, expected in zip(res, expected_qasm):
if isinstance(expected, str):
self.assertEqual(result, expected)
else:
self.assertTrue(
expected.search(result), f"Line {result} doesn't match regex: {expected}"
)
def test_teleportation(self):
"""Teleportation with physical qubits"""
@ -2120,62 +2119,58 @@ class TestCircuitQASM3ExporterTemporaryCasesWithBadParameterisation(QiskitTestCa
circuit.sx(0)
circuit.cx(0, 1)
rz = circuit.data[0].operation
u1_1 = rz.definition.data[0].operation
u3_1 = u1_1.definition.data[0].operation
sx = circuit.data[1].operation
sdg = sx.definition.data[0].operation
u1_2 = sdg.definition.data[0].operation
u3_2 = u1_2.definition.data[0].operation
h_ = sx.definition.data[1].operation
u2_1 = h_.definition.data[0].operation
u3_3 = u2_1.definition.data[0].operation
expected_qasm = "\n".join(
[
"OPENQASM 3.0;",
f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{",
" U(0, 0, pi/2) _gate_q_0;",
"}",
f"gate u1_{id(u1_1)}(_gate_p_0) _gate_q_0 {{",
f" u3_{id(u3_1)}(0, 0, pi/2) _gate_q_0;",
"}",
f"gate rz_{id(rz)}(_gate_p_0) _gate_q_0 {{",
f" u1_{id(u1_1)}(pi/2) _gate_q_0;",
"}",
f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{",
" U(0, 0, -pi/2) _gate_q_0;",
"}",
f"gate u1_{id(u1_2)}(_gate_p_0) _gate_q_0 {{",
f" u3_{id(u3_2)}(0, 0, -pi/2) _gate_q_0;",
"}",
"gate sdg _gate_q_0 {",
f" u1_{id(u1_2)}(-pi/2) _gate_q_0;",
"}",
f"gate u3_{id(u3_3)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{",
" U(pi/2, 0, pi) _gate_q_0;",
"}",
f"gate u2_{id(u2_1)}(_gate_p_0, _gate_p_1) _gate_q_0 {{",
f" u3_{id(u3_3)}(pi/2, 0, pi) _gate_q_0;",
"}",
"gate h _gate_q_0 {",
f" u2_{id(u2_1)}(0, pi) _gate_q_0;",
"}",
"gate sx _gate_q_0 {",
" sdg _gate_q_0;",
" h _gate_q_0;",
" sdg _gate_q_0;",
"}",
"gate cx c, t {",
" ctrl @ U(pi, 0, pi) c, t;",
"}",
"qubit[2] q;",
f"rz_{id(rz)}(pi/2) q[0];",
"sx q[0];",
"cx q[0], q[1];",
"",
]
)
self.assertEqual(Exporter(includes=[]).dumps(circuit), expected_qasm)
id_len = len(str(id(circuit.data[0].operation)))
expected_qasm = [
"OPENQASM 3.0;",
re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len),
" U(0, 0, pi/2) _gate_q_0;",
"}",
re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len),
re.compile(r" u3_\d{%s}\(0, 0, pi/2\) _gate_q_0;" % id_len),
"}",
re.compile(r"gate rz_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len),
re.compile(r" u1_\d{%s}\(pi/2\) _gate_q_0;" % id_len),
"}",
re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len),
" U(0, 0, -pi/2) _gate_q_0;",
"}",
re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len),
re.compile(r" u3_\d{%s}\(0, 0, -pi/2\) _gate_q_0;" % id_len),
"}",
"gate sdg _gate_q_0 {",
re.compile(r" u1_\d{%s}\(-pi/2\) _gate_q_0;" % id_len),
"}",
re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len),
" U(pi/2, 0, pi) _gate_q_0;",
"}",
re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len),
re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len),
"}",
"gate h _gate_q_0 {",
re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len),
"}",
"gate sx _gate_q_0 {",
" sdg _gate_q_0;",
" h _gate_q_0;",
" sdg _gate_q_0;",
"}",
"gate cx c, t {",
" ctrl @ U(pi, 0, pi) c, t;",
"}",
"qubit[2] q;",
re.compile(r"rz_\d{%s}\(pi/2\) q\[0\];" % id_len),
"sx q[0];",
"cx q[0], q[1];",
"",
]
res = Exporter(includes=[]).dumps(circuit).splitlines()
for result, expected in zip(res, expected_qasm):
if isinstance(expected, str):
self.assertEqual(result, expected)
else:
self.assertTrue(
expected.search(result), f"Line {result} doesn't match regex: {expected}"
)
def test_unusual_conditions(self):
"""Test that special QASM constructs such as ``measure`` are correctly handled when the