From f304a4b4f8add43defafeabbbbd32baefa30aa7c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 13 Jun 2024 06:48:40 -0400 Subject: [PATCH] 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 a6e69ba4c99cdd2fa50ed10399cada322bc88903 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` 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>` 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 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 2f81bde8bf32b06b4165048896eabdb36470814d. 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 * 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 Co-authored-by: John Lapeyre Co-authored-by: Kevin Hartman --- .github/workflows/tests.yml | 9 + CONTRIBUTING.md | 12 + Cargo.lock | 4 + Cargo.toml | 5 + crates/accelerate/Cargo.toml | 8 +- crates/accelerate/src/isometry.rs | 2 +- crates/accelerate/src/two_qubit_decompose.rs | 68 +- crates/circuit/Cargo.toml | 15 +- crates/circuit/README.md | 63 ++ crates/circuit/src/bit_data.rs | 13 +- crates/circuit/src/circuit_data.rs | 911 +++++++++++++++++- crates/circuit/src/circuit_instruction.rs | 698 +++++++++++++- crates/circuit/src/dag_node.rs | 86 +- crates/circuit/src/gate_matrix.rs | 224 +++++ crates/circuit/src/imports.rs | 168 ++++ crates/circuit/src/interner.rs | 12 +- crates/circuit/src/lib.rs | 13 +- crates/circuit/src/operations.rs | 786 +++++++++++++++ crates/circuit/src/packed_instruction.rs | 25 - crates/circuit/src/parameter_table.rs | 173 ++++ crates/pyext/Cargo.toml | 1 + qiskit/circuit/controlflow/builder.py | 8 +- qiskit/circuit/instruction.py | 1 + qiskit/circuit/instructionset.py | 9 +- qiskit/circuit/library/blueprintcircuit.py | 8 +- qiskit/circuit/library/standard_gates/ecr.py | 3 + .../library/standard_gates/global_phase.py | 3 + qiskit/circuit/library/standard_gates/h.py | 3 + qiskit/circuit/library/standard_gates/i.py | 3 + qiskit/circuit/library/standard_gates/p.py | 3 + qiskit/circuit/library/standard_gates/rx.py | 3 + qiskit/circuit/library/standard_gates/ry.py | 3 + qiskit/circuit/library/standard_gates/rz.py | 3 + qiskit/circuit/library/standard_gates/swap.py | 3 + qiskit/circuit/library/standard_gates/sx.py | 3 + qiskit/circuit/library/standard_gates/u.py | 3 + qiskit/circuit/library/standard_gates/x.py | 7 + qiskit/circuit/library/standard_gates/y.py | 5 + qiskit/circuit/library/standard_gates/z.py | 5 + qiskit/circuit/parametertable.py | 191 +--- qiskit/circuit/quantumcircuit.py | 419 ++++---- qiskit/circuit/quantumcircuitdata.py | 4 +- qiskit/converters/circuit_to_instruction.py | 12 +- qiskit/qasm3/exporter.py | 10 +- .../operators/dihedral/dihedral.py | 3 +- .../padding/dynamical_decoupling.py | 16 +- .../scheduling/scheduling/base_scheduler.py | 5 +- .../passes/scheduling/time_unit_conversion.py | 7 +- .../circuit-gates-rust-5c6ab6c58f7fd2c9.yaml | 79 ++ requirements-optional.txt | 2 +- setup.py | 12 + .../circuit/library/test_blueprintcircuit.py | 6 +- test/python/circuit/test_circuit_data.py | 25 +- .../python/circuit/test_circuit_operations.py | 2 +- test/python/circuit/test_compose.py | 3 +- test/python/circuit/test_instructions.py | 12 +- test/python/circuit/test_isometry.py | 1 - test/python/circuit/test_parameters.py | 191 +--- test/python/circuit/test_rust_equivalence.py | 143 +++ test/python/qasm3/test_export.py | 201 ++-- 60 files changed, 3780 insertions(+), 936 deletions(-) create mode 100644 crates/circuit/src/gate_matrix.rs create mode 100644 crates/circuit/src/imports.rs create mode 100644 crates/circuit/src/operations.rs delete mode 100644 crates/circuit/src/packed_instruction.rs create mode 100644 crates/circuit/src/parameter_table.rs create mode 100644 releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml create mode 100644 test/python/circuit/test_rust_equivalence.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 08530adfd4f..20e40dec982 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c75b5175001..4641c7878fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index cf8e4f365df..aefa3c932a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1196,7 +1196,11 @@ name = "qiskit-circuit" version = "1.2.0" dependencies = [ "hashbrown 0.14.5", + "ndarray", + "num-complex", + "numpy", "pyo3", + "smallvec", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2827b2206f4..13f43cfabcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 63be9ad90b4..d9865d54543 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -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] diff --git a/crates/accelerate/src/isometry.rs b/crates/accelerate/src/isometry.rs index a4e83358a7d..a3a8be38dae 100644 --- a/crates/accelerate/src/isometry.rs +++ b/crates/accelerate/src/isometry.rs @@ -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 diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 5e833bd86fd..f93eb2a8d99 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -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 { ] } -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 { 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); diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 6ec38392cc3..dd7e878537d 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -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 = [] diff --git a/crates/circuit/README.md b/crates/circuit/README.md index b9375c9f99d..bbb4e54651a 100644 --- a/crates/circuit/README.md +++ b/crates/circuit/README.md @@ -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`. diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 7964ec186e0..40540f9df5a 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -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 { pub(crate) struct BitNotFoundError<'py>(pub(crate) Bound<'py, PyAny>); +impl<'py> From> for PyErr { + fn from(error: BitNotFoundError) -> Self { + PyKeyError::new_err(format!( + "Bit {:?} has not been added to this circuit.", + error.0 + )) + } +} + impl BitData where T: From + 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> + ExactSizeIterator { + pub fn map_indices(&self, bits: &[T]) -> impl ExactSizeIterator> { let v: Vec<_> = bits.iter().map(|i| self.get(*i).unwrap()).collect(); v.into_iter() } diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index fbb7c06fc89..da35787e320 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -10,17 +10,24 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use crate::bit_data::{BitData, BitNotFoundError}; -use crate::circuit_instruction::CircuitInstruction; -use crate::interner::{CacheFullError, IndexedInterner, Interner, InternerKey}; -use crate::packed_instruction::PackedInstruction; +use crate::bit_data::BitData; +use crate::circuit_instruction::{ + convert_py_to_operation_type, operation_type_and_data_to_py, CircuitInstruction, + ExtraInstructionAttributes, OperationInput, PackedInstruction, +}; +use crate::imports::{BUILTIN_LIST, QUBIT}; +use crate::interner::{IndexedInterner, Interner, InternerKey}; +use crate::operations::{Operation, OperationType, Param, StandardGate}; +use crate::parameter_table::{ParamEntry, ParamTable, GLOBAL_PHASE_INDEX}; use crate::{Clbit, Qubit, SliceOrInt}; -use pyo3::exceptions::{PyIndexError, PyKeyError, PyRuntimeError, PyValueError}; +use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; use pyo3::types::{PyList, PySet, PySlice, PyTuple, PyType}; -use pyo3::{PyObject, PyResult, PyTraverseError, PyVisit}; -use std::mem; +use pyo3::{intern, PyTraverseError, PyVisit}; + +use hashbrown::{HashMap, HashSet}; +use smallvec::SmallVec; /// A container for :class:`.QuantumCircuit` instruction listings that stores /// :class:`.CircuitInstruction` instances in a packed form by interning @@ -85,33 +92,250 @@ pub struct CircuitData { qubits: BitData, /// Clbits registered in the circuit. clbits: BitData, + param_table: ParamTable, + #[pyo3(get)] + global_phase: Param, } -impl<'py> From> for PyErr { - fn from(error: BitNotFoundError) -> Self { - PyKeyError::new_err(format!( - "Bit {:?} has not been added to this circuit.", - error.0 - )) +impl CircuitData { + /// An alternate constructor to build a new `CircuitData` from an iterator + /// of standard gates. This can be used to build a circuit from a sequence + /// of standard gates, such as for a `StandardGate` definition or circuit + /// synthesis without needing to involve Python. + /// + /// This can be connected with the Python space + /// QuantumCircuit.from_circuit_data() constructor to build a full + /// QuantumCircuit from Rust. + /// + /// # Arguments + /// + /// * py: A GIL handle this is needed to instantiate Qubits in Python space + /// * num_qubits: The number of qubits in the circuit. These will be created + /// in Python as loose bits without a register. + /// * instructions: An iterator of the standard gate params and qubits to + /// add to the circuit + /// * global_phase: The global phase to use for the circuit + pub fn from_standard_gates( + py: Python, + num_qubits: u32, + instructions: I, + global_phase: Param, + ) -> PyResult + where + I: IntoIterator, SmallVec<[Qubit; 2]>)>, + { + let instruction_iter = instructions.into_iter(); + let mut res = CircuitData { + data: Vec::with_capacity(instruction_iter.size_hint().0), + qargs_interner: IndexedInterner::new(), + cargs_interner: IndexedInterner::new(), + qubits: BitData::new(py, "qubits".to_string()), + clbits: BitData::new(py, "clbits".to_string()), + param_table: ParamTable::new(), + global_phase, + }; + if num_qubits > 0 { + let qubit_cls = QUBIT.get_bound(py); + for _i in 0..num_qubits { + let bit = qubit_cls.call0()?; + res.add_qubit(py, &bit, true)?; + } + } + for (operation, params, qargs) in instruction_iter { + let qubits = PyTuple::new_bound(py, res.qubits.map_indices(&qargs)).unbind(); + let clbits = PyTuple::empty_bound(py).unbind(); + let inst = res.pack_owned( + py, + &CircuitInstruction { + operation: OperationType::Standard(operation), + qubits, + clbits, + params, + extra_attrs: None, + #[cfg(feature = "cache_pygates")] + py_op: None, + }, + )?; + res.data.push(inst); + } + Ok(res) } -} -impl From for PyErr { - fn from(_: CacheFullError) -> Self { - PyRuntimeError::new_err("The bit operands cache is full!") + fn handle_manual_params( + &mut self, + py: Python, + inst_index: usize, + params: &[(usize, Vec)], + ) -> PyResult { + let mut new_param = false; + let mut atomic_parameters: HashMap = HashMap::new(); + for (param_index, raw_param_objs) in params { + raw_param_objs.iter().for_each(|x| { + atomic_parameters.insert( + x.getattr(py, intern!(py, "_uuid")) + .expect("Not a parameter") + .getattr(py, intern!(py, "int")) + .expect("Not a uuid") + .extract::(py) + .unwrap(), + x.clone_ref(py), + ); + }); + for (param_uuid, param_obj) in atomic_parameters.iter() { + match self.param_table.table.get_mut(param_uuid) { + Some(entry) => entry.add(inst_index, *param_index), + None => { + new_param = true; + let new_entry = ParamEntry::new(inst_index, *param_index); + self.param_table + .insert(py, param_obj.clone_ref(py), new_entry)?; + } + }; + } + atomic_parameters.clear() + } + Ok(new_param) + } + + /// Add an instruction's entries to the parameter table + fn update_param_table( + &mut self, + py: Python, + inst_index: usize, + params: Option)>>, + ) -> PyResult { + if let Some(params) = params { + return self.handle_manual_params(py, inst_index, ¶ms); + } + // Update the parameter table + let mut new_param = false; + let inst_params = &self.data[inst_index].params; + if !inst_params.is_empty() { + let params: Vec<(usize, PyObject)> = inst_params + .iter() + .enumerate() + .filter_map(|(idx, x)| match x { + Param::ParameterExpression(param_obj) => Some((idx, param_obj.clone_ref(py))), + _ => None, + }) + .collect(); + if !params.is_empty() { + let list_builtin = BUILTIN_LIST.get_bound(py); + let mut atomic_parameters: HashMap = HashMap::new(); + for (param_index, param) in ¶ms { + let temp: PyObject = param.getattr(py, intern!(py, "parameters"))?; + let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; + raw_param_objs.iter().for_each(|x| { + atomic_parameters.insert( + x.getattr(py, intern!(py, "_uuid")) + .expect("Not a parameter") + .getattr(py, intern!(py, "int")) + .expect("Not a uuid") + .extract(py) + .unwrap(), + x.clone_ref(py), + ); + }); + for (param_uuid, param_obj) in &atomic_parameters { + match self.param_table.table.get_mut(param_uuid) { + Some(entry) => entry.add(inst_index, *param_index), + None => { + new_param = true; + let new_entry = ParamEntry::new(inst_index, *param_index); + self.param_table + .insert(py, param_obj.clone_ref(py), new_entry)?; + } + }; + } + atomic_parameters.clear(); + } + } + } + Ok(new_param) + } + + /// Remove an index's entries from the parameter table. + fn remove_from_parameter_table(&mut self, py: Python, inst_index: usize) -> PyResult<()> { + let list_builtin = BUILTIN_LIST.get_bound(py); + if inst_index == GLOBAL_PHASE_INDEX { + if let Param::ParameterExpression(global_phase) = &self.global_phase { + let temp: PyObject = global_phase.getattr(py, intern!(py, "parameters"))?; + let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; + for (param_index, param_obj) in raw_param_objs.iter().enumerate() { + let uuid: u128 = param_obj + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + let name: String = param_obj.getattr(py, intern!(py, "name"))?.extract(py)?; + self.param_table + .discard_references(uuid, inst_index, param_index, name); + } + } + } else if !self.data[inst_index].params.is_empty() { + let params: Vec<(usize, PyObject)> = self.data[inst_index] + .params + .iter() + .enumerate() + .filter_map(|(idx, x)| match x { + Param::ParameterExpression(param_obj) => Some((idx, param_obj.clone_ref(py))), + _ => None, + }) + .collect(); + if !params.is_empty() { + for (param_index, param) in ¶ms { + let temp: PyObject = param.getattr(py, intern!(py, "parameters"))?; + let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; + let mut atomic_parameters: HashSet<(u128, String)> = + HashSet::with_capacity(params.len()); + for x in raw_param_objs { + let uuid = x + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + let name = x.getattr(py, intern!(py, "name"))?.extract(py)?; + atomic_parameters.insert((uuid, name)); + } + for (uuid, name) in atomic_parameters { + self.param_table + .discard_references(uuid, inst_index, *param_index, name); + } + } + } + } + Ok(()) + } + + fn reindex_parameter_table(&mut self, py: Python) -> PyResult<()> { + self.param_table.clear(); + + for inst_index in 0..self.data.len() { + self.update_param_table(py, inst_index, None)?; + } + // Technically we could keep the global phase entry directly if it exists, but we're + // the incremental cost is minimal after reindexing everything. + self.global_phase(py, self.global_phase.clone())?; + Ok(()) + } + + pub fn append_inner(&mut self, py: Python, value: PyRef) -> PyResult { + let packed = self.pack(py, value)?; + let new_index = self.data.len(); + self.data.push(packed); + self.update_param_table(py, new_index, None) } } #[pymethods] impl CircuitData { #[new] - #[pyo3(signature = (qubits=None, clbits=None, data=None, reserve=0))] + #[pyo3(signature = (qubits=None, clbits=None, data=None, reserve=0, global_phase=Param::Float(0.0)))] pub fn new( py: Python<'_>, qubits: Option<&Bound>, clbits: Option<&Bound>, data: Option<&Bound>, reserve: usize, + global_phase: Param, ) -> PyResult { let mut self_ = CircuitData { data: Vec::new(), @@ -119,7 +343,10 @@ impl CircuitData { cargs_interner: IndexedInterner::new(), qubits: BitData::new(py, "qubits".to_string()), clbits: BitData::new(py, "clbits".to_string()), + param_table: ParamTable::new(), + global_phase: Param::Float(0.), }; + self_.global_phase(py, global_phase)?; if let Some(qubits) = qubits { for bit in qubits.iter()? { self_.add_qubit(py, &bit?, true)?; @@ -241,17 +468,89 @@ impl CircuitData { /// /// Returns: /// CircuitData: The shallow copy. - pub fn copy(&self, py: Python<'_>) -> PyResult { + #[pyo3(signature = (copy_instructions=true, deepcopy=false))] + pub fn copy(&self, py: Python<'_>, copy_instructions: bool, deepcopy: bool) -> PyResult { let mut res = CircuitData::new( py, Some(self.qubits.cached().bind(py)), Some(self.clbits.cached().bind(py)), None, 0, + self.global_phase.clone(), )?; res.qargs_interner = self.qargs_interner.clone(); res.cargs_interner = self.cargs_interner.clone(); res.data.clone_from(&self.data); + res.param_table.clone_from(&self.param_table); + + if deepcopy { + let deepcopy = py + .import_bound(intern!(py, "copy"))? + .getattr(intern!(py, "deepcopy"))?; + for inst in &mut res.data { + match &mut inst.op { + OperationType::Standard(_) => { + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Gate(ref mut op) => { + op.gate = deepcopy.call1((&op.gate,))?.unbind(); + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Instruction(ref mut op) => { + op.instruction = deepcopy.call1((&op.instruction,))?.unbind(); + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Operation(ref mut op) => { + op.operation = deepcopy.call1((&op.operation,))?.unbind(); + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + }; + } + } else if copy_instructions { + for inst in &mut res.data { + match &mut inst.op { + OperationType::Standard(_) => { + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Gate(ref mut op) => { + op.gate = op.gate.call_method0(py, intern!(py, "copy"))?; + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Instruction(ref mut op) => { + op.instruction = op.instruction.call_method0(py, intern!(py, "copy"))?; + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Operation(ref mut op) => { + op.operation = op.operation.call_method0(py, intern!(py, "copy"))?; + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + }; + } + } Ok(res) } @@ -290,10 +589,87 @@ impl CircuitData { /// Args: /// func (Callable[[:class:`~.Operation`], None]): /// The callable to invoke. + #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn foreach_op(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter() { - func.call1((inst.op.bind(py),))?; + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + + let op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + func.call1((op,))?; + } + Ok(()) + } + + /// Invokes callable ``func`` with each instruction's operation. + /// + /// Args: + /// func (Callable[[:class:`~.Operation`], None]): + /// The callable to invoke. + #[cfg(feature = "cache_pygates")] + #[pyo3(signature = (func))] + pub fn foreach_op(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { + for inst in self.data.iter_mut() { + let op = match &inst.py_op { + Some(op) => op.clone_ref(py), + None => { + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + let new_op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + inst.py_op = Some(new_op.clone_ref(py)); + new_op + } + }; + func.call1((op,))?; } Ok(()) } @@ -304,10 +680,88 @@ impl CircuitData { /// Args: /// func (Callable[[int, :class:`~.Operation`], None]): /// The callable to invoke. + #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn foreach_op_indexed(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for (index, inst) in self.data.iter().enumerate() { - func.call1((index, inst.op.bind(py)))?; + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + + let op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + func.call1((index, op))?; + } + Ok(()) + } + + /// Invokes callable ``func`` with the positional index and operation + /// of each instruction. + /// + /// Args: + /// func (Callable[[int, :class:`~.Operation`], None]): + /// The callable to invoke. + #[cfg(feature = "cache_pygates")] + #[pyo3(signature = (func))] + pub fn foreach_op_indexed(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { + for (index, inst) in self.data.iter_mut().enumerate() { + let op = match &inst.py_op { + Some(op) => op.clone_ref(py), + None => { + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + let new_op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + inst.py_op = Some(new_op.clone_ref(py)); + new_op + } + }; + func.call1((index, op))?; } Ok(()) } @@ -315,14 +769,187 @@ impl CircuitData { /// Invokes callable ``func`` with each instruction's operation, /// replacing the operation with the result. /// + /// .. note:: + /// + /// This is only to be used by map_vars() in quantumcircuit.py it + /// assumes that a full Python instruction will only be returned from + /// standard gates iff a condition is set. + /// /// Args: /// func (Callable[[:class:`~.Operation`], :class:`~.Operation`]): /// A callable used to map original operation to their /// replacements. + #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn map_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter_mut() { - inst.op = func.call1((inst.op.bind(py),))?.into_py(py); + let old_op = match &inst.op { + OperationType::Standard(op) => { + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + if condition.is_some() { + operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )? + } else { + op.into_py(py) + } + } + OperationType::Gate(op) => op.gate.clone_ref(py), + OperationType::Instruction(op) => op.instruction.clone_ref(py), + OperationType::Operation(op) => op.operation.clone_ref(py), + }; + let result: OperationInput = func.call1((old_op,))?.extract()?; + match result { + OperationInput::Standard(op) => { + inst.op = OperationType::Standard(op); + } + OperationInput::Gate(op) => { + inst.op = OperationType::Gate(op); + } + OperationInput::Instruction(op) => { + inst.op = OperationType::Instruction(op); + } + OperationInput::Operation(op) => { + inst.op = OperationType::Operation(op); + } + OperationInput::Object(new_op) => { + let new_inst_details = convert_py_to_operation_type(py, new_op)?; + inst.op = new_inst_details.operation; + inst.params = new_inst_details.params; + if new_inst_details.label.is_some() + || new_inst_details.duration.is_some() + || new_inst_details.unit.is_some() + || new_inst_details.condition.is_some() + { + inst.extra_attrs = Some(Box::new(ExtraInstructionAttributes { + label: new_inst_details.label, + duration: new_inst_details.duration, + unit: new_inst_details.unit, + condition: new_inst_details.condition, + })) + } + } + } + } + Ok(()) + } + + /// Invokes callable ``func`` with each instruction's operation, + /// replacing the operation with the result. + /// + /// .. note:: + /// + /// This is only to be used by map_vars() in quantumcircuit.py it + /// assumes that a full Python instruction will only be returned from + /// standard gates iff a condition is set. + /// + /// Args: + /// func (Callable[[:class:`~.Operation`], :class:`~.Operation`]): + /// A callable used to map original operation to their + /// replacements. + #[cfg(feature = "cache_pygates")] + #[pyo3(signature = (func))] + pub fn map_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { + for inst in self.data.iter_mut() { + let old_op = match &inst.py_op { + Some(op) => op.clone_ref(py), + None => match &inst.op { + OperationType::Standard(op) => { + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + if condition.is_some() { + let new_op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + inst.py_op = Some(new_op.clone_ref(py)); + new_op + } else { + op.into_py(py) + } + } + OperationType::Gate(op) => op.gate.clone_ref(py), + OperationType::Instruction(op) => op.instruction.clone_ref(py), + OperationType::Operation(op) => op.operation.clone_ref(py), + }, + }; + let result: OperationInput = func.call1((old_op,))?.extract()?; + match result { + OperationInput::Standard(op) => { + inst.op = OperationType::Standard(op); + } + OperationInput::Gate(op) => { + inst.op = OperationType::Gate(op); + } + OperationInput::Instruction(op) => { + inst.op = OperationType::Instruction(op); + } + OperationInput::Operation(op) => { + inst.op = OperationType::Operation(op); + } + OperationInput::Object(new_op) => { + let new_inst_details = convert_py_to_operation_type(py, new_op.clone_ref(py))?; + inst.op = new_inst_details.operation; + inst.params = new_inst_details.params; + if new_inst_details.label.is_some() + || new_inst_details.duration.is_some() + || new_inst_details.unit.is_some() + || new_inst_details.condition.is_some() + { + inst.extra_attrs = Some(Box::new(ExtraInstructionAttributes { + label: new_inst_details.label, + duration: new_inst_details.duration, + unit: new_inst_details.unit, + condition: new_inst_details.condition, + })) + } + inst.py_op = Some(new_op); + } + } } Ok(()) } @@ -385,7 +1012,7 @@ impl CircuitData { qubits: Option<&Bound>, clbits: Option<&Bound>, ) -> PyResult<()> { - let mut temp = CircuitData::new(py, qubits, clbits, None, 0)?; + let mut temp = CircuitData::new(py, qubits, clbits, None, 0, self.global_phase.clone())?; if qubits.is_some() { if temp.num_qubits() < self.num_qubits() { return Err(PyValueError::new_err(format!( @@ -394,7 +1021,7 @@ impl CircuitData { self.num_qubits(), ))); } - mem::swap(&mut temp.qubits, &mut self.qubits); + std::mem::swap(&mut temp.qubits, &mut self.qubits); } if clbits.is_some() { if temp.num_clbits() < self.num_clbits() { @@ -404,7 +1031,7 @@ impl CircuitData { self.num_clbits(), ))); } - mem::swap(&mut temp.clbits, &mut self.clbits); + std::mem::swap(&mut temp.clbits, &mut self.clbits); } Ok(()) } @@ -430,9 +1057,11 @@ impl CircuitData { py, CircuitInstruction::new( py, - inst.op.clone_ref(py), + inst.op.clone(), self_.qubits.map_indices(qubits.value), self_.clbits.map_indices(clbits.value), + inst.params.clone(), + inst.extra_attrs.clone(), ), ) } else { @@ -455,7 +1084,7 @@ impl CircuitData { } } - pub fn __delitem__(&mut self, index: SliceOrInt) -> PyResult<()> { + pub fn __delitem__(&mut self, py: Python, index: SliceOrInt) -> PyResult<()> { match index { SliceOrInt::Slice(slice) => { let slice = { @@ -468,14 +1097,24 @@ impl CircuitData { s }; for i in slice.into_iter() { - self.__delitem__(SliceOrInt::Int(i))?; + self.__delitem__(py, SliceOrInt::Int(i))?; } + self.reindex_parameter_table(py)?; Ok(()) } SliceOrInt::Int(index) => { let index = self.convert_py_index(index)?; if self.data.get(index).is_some() { - self.data.remove(index); + if index == self.data.len() { + // For individual removal from param table before + // deletion + self.remove_from_parameter_table(py, index)?; + self.data.remove(index); + } else { + // For delete in the middle delete before reindexing + self.data.remove(index); + self.reindex_parameter_table(py)?; + } Ok(()) } else { Err(PyIndexError::new_err(format!( @@ -487,6 +1126,19 @@ impl CircuitData { } } + pub fn setitem_no_param_table_update( + &mut self, + py: Python<'_>, + index: isize, + value: &Bound, + ) -> PyResult<()> { + let index = self.convert_py_index(index)?; + let value: PyRef = value.downcast()?.borrow(); + let mut packed = self.pack(py, value)?; + std::mem::swap(&mut packed, &mut self.data[index]); + Ok(()) + } + pub fn __setitem__( &mut self, py: Python<'_>, @@ -520,7 +1172,7 @@ impl CircuitData { indices.stop, 1isize, ); - self.__delitem__(SliceOrInt::Slice(slice))?; + self.__delitem__(py, SliceOrInt::Slice(slice))?; } else { // Insert any extra values. for v in values.iter().skip(slice.len()).rev() { @@ -535,7 +1187,9 @@ impl CircuitData { let index = self.convert_py_index(index)?; let value: PyRef = value.extract()?; let mut packed = self.pack(py, value)?; - mem::swap(&mut packed, &mut self.data[index]); + self.remove_from_parameter_table(py, index)?; + std::mem::swap(&mut packed, &mut self.data[index]); + self.update_param_table(py, index, None)?; Ok(()) } } @@ -548,8 +1202,14 @@ impl CircuitData { value: PyRef, ) -> PyResult<()> { let index = self.convert_py_index_clamped(index); + let old_len = self.data.len(); let packed = self.pack(py, value)?; self.data.insert(index, packed); + if index == old_len { + self.update_param_table(py, old_len, None)?; + } else { + self.reindex_parameter_table(py)?; + } Ok(()) } @@ -557,14 +1217,21 @@ impl CircuitData { let index = index.unwrap_or_else(|| std::cmp::max(0, self.data.len() as isize - 1).into_py(py)); let item = self.__getitem__(py, index.bind(py))?; - self.__delitem__(index.bind(py).extract()?)?; + + self.__delitem__(py, index.bind(py).extract()?)?; Ok(item) } - pub fn append(&mut self, py: Python<'_>, value: PyRef) -> PyResult<()> { - let packed = self.pack(py, value)?; + pub fn append( + &mut self, + py: Python<'_>, + value: &Bound, + params: Option)>>, + ) -> PyResult { + let packed = self.pack(py, value.try_borrow()?)?; + let new_index = self.data.len(); self.data.push(packed); - Ok(()) + self.update_param_table(py, new_index, params) } pub fn extend(&mut self, py: Python<'_>, itr: &Bound) -> PyResult<()> { @@ -597,28 +1264,33 @@ impl CircuitData { .unwrap()) }) .collect::>>()?; - + let new_index = self.data.len(); let qubits_id = Interner::intern(&mut self.qargs_interner, InternerKey::Value(qubits))?; let clbits_id = Interner::intern(&mut self.cargs_interner, InternerKey::Value(clbits))?; self.data.push(PackedInstruction { - op: inst.op.clone_ref(py), + op: inst.op.clone(), qubits_id: qubits_id.index, clbits_id: clbits_id.index, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), }); + self.update_param_table(py, new_index, None)?; } return Ok(()); } - for v in itr.iter()? { - self.append(py, v?.extract()?)?; + self.append_inner(py, v?.extract()?)?; } Ok(()) } pub fn clear(&mut self, _py: Python<'_>) -> PyResult<()> { std::mem::take(&mut self.data); + self.param_table.clear(); Ok(()) } @@ -656,9 +1328,6 @@ impl CircuitData { } fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - for packed in self.data.iter() { - visit.call(&packed.op)?; - } for bit in self.qubits.bits().iter().chain(self.clbits.bits().iter()) { visit.call(bit)?; } @@ -678,6 +1347,128 @@ impl CircuitData { self.qubits.dispose(); self.clbits.dispose(); } + + #[setter] + pub fn global_phase(&mut self, py: Python, angle: Param) -> PyResult<()> { + let list_builtin = BUILTIN_LIST.get_bound(py); + self.remove_from_parameter_table(py, GLOBAL_PHASE_INDEX)?; + match angle { + Param::Float(angle) => { + self.global_phase = Param::Float(angle.rem_euclid(2. * std::f64::consts::PI)); + } + Param::ParameterExpression(angle) => { + let temp: PyObject = angle.getattr(py, intern!(py, "parameters"))?; + let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; + + for (param_index, param_obj) in raw_param_objs.into_iter().enumerate() { + let param_uuid: u128 = param_obj + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + match self.param_table.table.get_mut(¶m_uuid) { + Some(entry) => entry.add(GLOBAL_PHASE_INDEX, param_index), + None => { + let new_entry = ParamEntry::new(GLOBAL_PHASE_INDEX, param_index); + self.param_table.insert(py, param_obj, new_entry)?; + } + }; + } + self.global_phase = Param::ParameterExpression(angle); + } + Param::Obj(_) => return Err(PyValueError::new_err("Invalid type for global phase")), + }; + Ok(()) + } + + /// Get the global_phase sentinel value + #[classattr] + pub const fn global_phase_param_index() -> usize { + GLOBAL_PHASE_INDEX + } + + // Below are functions to interact with the parameter table. These methods + // are done to avoid needing to deal with shared references and provide + // an entry point via python through an owned CircuitData object. + pub fn num_params(&self) -> usize { + self.param_table.table.len() + } + + pub fn get_param_from_name(&self, py: Python, name: String) -> Option { + self.param_table.get_param_from_name(py, name) + } + + pub fn get_params_unsorted(&self, py: Python) -> PyResult> { + Ok(PySet::new_bound(py, self.param_table.uuid_map.values())?.unbind()) + } + + pub fn pop_param( + &mut self, + py: Python, + uuid: u128, + name: String, + default: PyObject, + ) -> PyObject { + match self.param_table.pop(uuid, name) { + Some(res) => res.into_py(py), + None => default.clone_ref(py), + } + } + + pub fn _get_param(&self, py: Python, uuid: u128) -> PyObject { + self.param_table.table[&uuid].clone().into_py(py) + } + + pub fn contains_param(&self, uuid: u128) -> bool { + self.param_table.table.contains_key(&uuid) + } + + pub fn add_new_parameter( + &mut self, + py: Python, + param: PyObject, + inst_index: usize, + param_index: usize, + ) -> PyResult<()> { + self.param_table.insert( + py, + param.clone_ref(py), + ParamEntry::new(inst_index, param_index), + )?; + Ok(()) + } + + pub fn update_parameter_entry( + &mut self, + uuid: u128, + inst_index: usize, + param_index: usize, + ) -> PyResult<()> { + match self.param_table.table.get_mut(&uuid) { + Some(entry) => { + entry.add(inst_index, param_index); + Ok(()) + } + None => Err(PyIndexError::new_err(format!( + "Invalid parameter uuid: {:?}", + uuid + ))), + } + } + + pub fn _get_entry_count(&self, py: Python, param_obj: PyObject) -> PyResult { + let uuid: u128 = param_obj + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + Ok(self.param_table.table[&uuid].index_ids.len()) + } + + pub fn num_nonlocal_gates(&self) -> usize { + self.data + .iter() + .filter(|inst| inst.op.num_qubits() > 1 && !inst.op.directive()) + .count() + } } impl CircuitData { @@ -730,23 +1521,43 @@ impl CircuitData { Ok(index as usize) } - fn pack( - &mut self, - py: Python, - value: PyRef, - ) -> PyResult { + fn pack(&mut self, py: Python, inst: PyRef) -> PyResult { + let qubits = Interner::intern( + &mut self.qargs_interner, + InternerKey::Value(self.qubits.map_bits(inst.qubits.bind(py))?.collect()), + )?; + let clbits = Interner::intern( + &mut self.cargs_interner, + InternerKey::Value(self.clbits.map_bits(inst.clbits.bind(py))?.collect()), + )?; + Ok(PackedInstruction { + op: inst.operation.clone(), + qubits_id: qubits.index, + clbits_id: clbits.index, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), + }) + } + + fn pack_owned(&mut self, py: Python, inst: &CircuitInstruction) -> PyResult { let qubits = Interner::intern( &mut self.qargs_interner, - InternerKey::Value(self.qubits.map_bits(value.qubits.bind(py))?.collect()), + InternerKey::Value(self.qubits.map_bits(inst.qubits.bind(py))?.collect()), )?; let clbits = Interner::intern( &mut self.cargs_interner, - InternerKey::Value(self.clbits.map_bits(value.clbits.bind(py))?.collect()), + InternerKey::Value(self.clbits.map_bits(inst.clbits.bind(py))?.collect()), )?; Ok(PackedInstruction { - op: value.operation.clone_ref(py), + op: inst.operation.clone(), qubits_id: qubits.index, clbits_id: clbits.index, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), }) } } diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index ac61ae81a61..2bb90367082 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -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, + pub duration: Option, + pub unit: Option, + pub condition: Option, +} + +/// 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>, + #[cfg(feature = "cache_pygates")] + pub py_op: Option, +} /// 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, /// A sequence of the classical bits that this operation reads from or writes to. + #[pyo3(get)] pub clbits: Py, + pub params: SmallVec<[Param; 3]>, + pub extra_attrs: Option>, + #[cfg(feature = "cache_pygates")] + pub py_op: Option, +} + +/// 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( py: Python, - operation: PyObject, + operation: OperationType, qubits: impl IntoIterator, clbits: impl IntoIterator, + params: SmallVec<[Param; 3]>, + extra_attrs: Option>, ) -> 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 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>, clbits: Option<&Bound>, - ) -> PyResult> { + params: SmallVec<[Param; 3]>, + label: Option, + duration: Option, + unit: Option, + condition: Option, + ) -> PyResult { fn as_tuple(py: Python<'_>, seq: Option<&Bound>) -> PyResult> { 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 { + operation_type_to_py(py, self) + } + + #[cfg(feature = "cache_pygates")] + #[getter] + pub fn operation(&mut self, py: Python) -> PyResult { + 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, + operation: Option, qubits: Option<&Bound>, clbits: Option<&Bound>, - ) -> PyResult> { + params: Option>, + label: Option, + duration: Option, + unit: Option, + condition: Option, + ) -> PyResult { + 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 = match unit { + Some(unit) => Some(unit), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.unit.clone(), + None => None, + }, + }; + + let condition: Option = 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 { + fn __getstate__(&self, py: Python<'_>) -> PyResult { 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) -> 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 { + 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> { + 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> { + 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) -> PyResult { - 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) -> PyResult { + 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 { - 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 { + 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> = 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::() { - 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 { + 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, + duration: &Option, + unit: &Option, + condition: &Option, +) -> PyResult { + 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, + pub duration: Option, + pub unit: Option, + pub condition: Option, +} + +/// 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 { + 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 = match binding { + Some(base_class) => base_class.downcast()?.clone().unbind(), + None => op_obj.unbind(), + }; + let op_type: Bound = raw_op_type.into_bound(py); + let mut standard: Option = 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::(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::(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::(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))) +} diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index c766461bb51..c8b6a4c8b08 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -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, py: Python) -> PyObject { + fn __reduce__(slf: PyRef, py: Python) -> PyResult { let state = (slf.as_ref()._node_id, &slf.sort_key); - ( + Ok(( py.get_type_bound::(), ( - &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, state: &Bound) -> 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 { + 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 { - 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 { 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()? )) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs new file mode 100644 index 00000000000..72e1087637c --- /dev/null +++ b/crates/circuit/src/gate_matrix.rs @@ -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], + ] +} diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs new file mode 100644 index 00000000000..050f7f2e053 --- /dev/null +++ b/crates/circuit/src/imports.rs @@ -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>, +} + +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 { + 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; 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; 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 { + 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) +} diff --git a/crates/circuit/src/interner.rs b/crates/circuit/src/interner.rs index 42667570205..f22bb80ae05 100644 --- a/crates/circuit/src/interner.rs +++ b/crates/circuit/src/interner.rs @@ -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 for Index { pub struct CacheFullError; +impl From 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)] diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 90f2b7c7f07..d7f28591175 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -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 for Qubit { fn from(value: BitType) -> Self { @@ -69,5 +72,9 @@ pub fn circuit(m: Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs new file mode 100644 index 00000000000..ead1b8ee1eb --- /dev/null +++ b/crates/circuit/src/operations.rs @@ -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> { + 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 { + 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 { + 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>; + fn definition(&self, params: &[Param]) -> Option; + fn standard_gate(&self) -> Option; + 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 { + 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::() { + Param::Float(val) + } else { + Param::Obj(b.clone().unbind()) + }, + ) + } +} + +impl IntoPy 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) -> Option { + self.matrix(¶ms) + .map(|x| x.into_pyarray_bound(py).into()) + } + + pub fn _num_params(&self) -> u32 { + self.num_params() + } + + pub fn _get_definition(&self, params: Vec) -> Option { + self.definition(¶ms) + } + + #[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> { + 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 { + match self { + Self::ZGate => Python::with_gil(|py| -> Option { + 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 { + 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 { + 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 { + 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 { + match ¶ms[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 { + 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 { + 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 { + 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 { + 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 { + 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> { + None + } + fn definition(&self, _params: &[Param]) -> Option { + Python::with_gil(|py| -> Option { + match self.instruction.getattr(py, intern!(py, "definition")) { + Ok(definition) => { + let res: Option = 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 { + 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> { + Python::with_gil(|py| -> Option> { + match self.gate.getattr(py, intern!(py, "to_matrix")) { + Ok(to_matrix) => { + let res: Option = to_matrix.call0(py).ok()?.extract(py).ok(); + match res { + Some(x) => { + let array: PyReadonlyArray2 = x.extract(py).ok()?; + Some(array.as_array().to_owned()) + } + None => None, + } + } + Err(_) => None, + } + }) + } + fn definition(&self, _params: &[Param]) -> Option { + Python::with_gil(|py| -> Option { + match self.gate.getattr(py, intern!(py, "definition")) { + Ok(definition) => { + let res: Option = 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 { + Python::with_gil(|py| -> Option { + 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> { + None + } + fn definition(&self, _params: &[Param]) -> Option { + None + } + fn standard_gate(&self) -> Option { + 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, + } + }) + } +} diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs deleted file mode 100644 index 0c793f2b640..00000000000 --- a/crates/circuit/src/packed_instruction.rs +++ /dev/null @@ -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, -} diff --git a/crates/circuit/src/parameter_table.rs b/crates/circuit/src/parameter_table.rs new file mode 100644 index 00000000000..48c779eed3a --- /dev/null +++ b/crates/circuit/src/parameter_table.rs @@ -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) -> Py { + slf.into() + } + + fn __next__(mut slf: PyRefMut) -> 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, + /// Mapping of parameter name to uuid as an int + pub names: HashMap, + /// Mapping of uuid to a parameter object + pub uuid_map: HashMap, +} + +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 { + 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 { + self.names + .get(&name) + .map(|x| self.uuid_map.get(x).map(|y| y.clone_ref(py)))? + } +} diff --git a/crates/pyext/Cargo.toml b/crates/pyext/Cargo.toml index daaf19e1f6a..413165e84b1 100644 --- a/crates/pyext/Cargo.toml +++ b/crates/pyext/Cargo.toml @@ -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 diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index c6c95d27f92..bb0a30ea6af 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -57,7 +57,9 @@ def instructions(self) -> Sequence[CircuitInstruction]: """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 @@ def _raise_on_jump(operation): " 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: diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index e339cb8d94b..44155783d40 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -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. diff --git a/qiskit/circuit/instructionset.py b/qiskit/circuit/instructionset.py index ac3d9fabd64..576d5dee826 100644 --- a/qiskit/circuit/instructionset.py +++ b/qiskit/circuit/instructionset.py @@ -140,13 +140,12 @@ def c_if(self, classical: Clbit | ClassicalRegister | int, val: int) -> "Instruc ) 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] diff --git a/qiskit/circuit/library/blueprintcircuit.py b/qiskit/circuit/library/blueprintcircuit.py index 2bbd5ca5650..16cc0e3dbaf 100644 --- a/qiskit/circuit/library/blueprintcircuit.py +++ b/qiskit/circuit/library/blueprintcircuit.py @@ -17,7 +17,7 @@ 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 @@ def _build(self) -> None: 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 @@ def qregs(self, qregs): 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 @@ def parameters(self) -> ParameterView: 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, diff --git a/qiskit/circuit/library/standard_gates/ecr.py b/qiskit/circuit/library/standard_gates/ecr.py index 73bb1bb0389..f00c02df538 100644 --- a/qiskit/circuit/library/standard_gates/ecr.py +++ b/qiskit/circuit/library/standard_gates/ecr.py @@ -17,6 +17,7 @@ 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) diff --git a/qiskit/circuit/library/standard_gates/global_phase.py b/qiskit/circuit/library/standard_gates/global_phase.py index ccd758e4724..59d6b56373d 100644 --- a/qiskit/circuit/library/standard_gates/global_phase.py +++ b/qiskit/circuit/library/standard_gates/global_phase.py @@ -20,6 +20,7 @@ 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" ): diff --git a/qiskit/circuit/library/standard_gates/h.py b/qiskit/circuit/library/standard_gates/h.py index cc06a071a3f..2d273eed74d 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -17,6 +17,7 @@ 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) diff --git a/qiskit/circuit/library/standard_gates/i.py b/qiskit/circuit/library/standard_gates/i.py index 93523215d6f..13a98ce0df8 100644 --- a/qiskit/circuit/library/standard_gates/i.py +++ b/qiskit/circuit/library/standard_gates/i.py @@ -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) diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index 6de0307dc79..1a792649fea 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -19,6 +19,7 @@ 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 `_ """ + _standard_gate = StandardGate.PhaseGate + def __init__( self, theta: ParameterValueType, label: str | None = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index eaa73cf87c9..5579f9d3707 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -21,6 +21,7 @@ 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" ): diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index 633a518bca7..e27398cc296 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -20,6 +20,7 @@ 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" ): diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index 3040f956834..e8ee0f97603 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -17,6 +17,7 @@ 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 `_ """ + _standard_gate = StandardGate.RZGate + def __init__( self, phi: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/swap.py b/qiskit/circuit/library/standard_gates/swap.py index 0e49783308c..243a84701ef 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -17,6 +17,7 @@ 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) diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index 0c003748a66..93ca85da019 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -17,6 +17,7 @@ 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) diff --git a/qiskit/circuit/library/standard_gates/u.py b/qiskit/circuit/library/standard_gates/u.py index 3d631898850..3495bc180f0 100644 --- a/qiskit/circuit/library/standard_gates/u.py +++ b/qiskit/circuit/library/standard_gates/u.py @@ -21,6 +21,7 @@ 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, diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index 7195df90dc9..6e959b3e62c 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -19,6 +19,7 @@ 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, diff --git a/qiskit/circuit/library/standard_gates/y.py b/qiskit/circuit/library/standard_gates/y.py index e69e1e2b794..d62586aa2b9 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -19,6 +19,7 @@ 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, diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index 2b69595936d..19e4382cd84 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -20,6 +20,7 @@ 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, diff --git a/qiskit/circuit/parametertable.py b/qiskit/circuit/parametertable.py index 6803126ec10..e5a41b1971c 100644 --- a/qiskit/circuit/parametertable.py +++ b/qiskit/circuit/parametertable.py @@ -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 = _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): diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 606d0e04373..238a2682522 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -37,6 +37,7 @@ ) 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 .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 @@ def __init__( 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 @@ def __init__( 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 @@ def data(self, data_input: Iterable): 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 @@ def __deepcopy__(self, memo=None): # 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 @@ def replace_var(var: expr.Var, cache: Mapping[expr.Var, expr.Var]) -> expr.Var: 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 @@ def map_vars(op): ) 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 @@ def cbit_argument_conversion(self, clbit_representation: ClbitSpecifier) -> list 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 @@ def append( 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 @@ def _append( 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 @@ def _append(self, instruction, qargs=(), cargs=()): :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 @@ def get_parameter(self, name: str, default: typing.Any = ...) -> Parameter: 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 @@ def num_nonlocal_gates(self) -> int: 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 @@ def copy(self, name: str | None = None) -> typing.Self: """ 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 @@ def copy_empty_like( 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 @@ def clear(self) -> None: 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 @@ def remove_final_measurements(self, inplace: bool = True) -> Optional["QuantumCi 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 @@ def global_phase(self) -> ParameterValueType: """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 @@ def global_phase(self, angle: ParameterValueType): """ # 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 @@ def parameters(self) -> ParameterView: @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 @@ def _unsorted_parameters(self) -> set[Parameter]: """ # 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 @@ def assign_parameters( # pylint: disable=missing-raises-doc 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 @@ def assign_parameters( # pylint: disable=missing-raises-doc 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 @@ def assign_parameters( # pylint: disable=missing-raises-doc 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 @@ def map_calibration(qubits, parameters, schedule): for gate, calibrations in target._calibrations.items() ), ) + target._parameters = None return None if inplace else target def _unroll_param_dict( @@ -4450,9 +4489,7 @@ def h(self, qubit: QubitSpecifier) -> InstructionSet: 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 @@ def id(self, qubit: QubitSpecifier) -> InstructionSet: # pylint: disable=invali 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 @@ def p(self, theta: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: 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 @@ def rx( 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 @@ def ry( 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 @@ def rz(self, phi: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: 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 @@ def ecr(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: 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 @@ def swap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet 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 @@ def sx(self, qubit: QubitSpecifier) -> InstructionSet: 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 @@ def u( 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 @@ def x(self, qubit: QubitSpecifier, label: str | None = None) -> InstructionSet: 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 @@ def cx( 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 @@ def ccx( 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 @@ def y(self, qubit: QubitSpecifier) -> InstructionSet: 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 @@ def cy( 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 @@ def z(self, qubit: QubitSpecifier) -> InstructionSet: 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 @@ def cz( 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 @@ def _pop_previous_instruction_in_scope(self) -> CircuitInstruction: 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 @@ def __init__(self, circuit: QuantumCircuit): 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 diff --git a/qiskit/circuit/quantumcircuitdata.py b/qiskit/circuit/quantumcircuitdata.py index 3e29f36c6be..9ecc8e6a6ca 100644 --- a/qiskit/circuit/quantumcircuitdata.py +++ b/qiskit/circuit/quantumcircuitdata.py @@ -45,8 +45,6 @@ def __setitem__(self, key, value): 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 @@ def _resolve_legacy_value(self, operation, qargs, cargs) -> CircuitInstruction: 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: diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index 2bdcbfef358..1a5907c3ec8 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -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 fix_condition(op): 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 diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 16bc2529ca7..7f737af76eb 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -252,6 +252,9 @@ def __init__(self, includelist, basis_gates=()): 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 @@ def __getitem__(self, key): 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 is_loop_variable(circuit, parameter): # _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: diff --git a/qiskit/quantum_info/operators/dihedral/dihedral.py b/qiskit/quantum_info/operators/dihedral/dihedral.py index 4f49879063e..75b455410f4 100644 --- a/qiskit/quantum_info/operators/dihedral/dihedral.py +++ b/qiskit/quantum_info/operators/dihedral/dihedral.py @@ -452,8 +452,7 @@ def conjugate(self): 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) diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 7cb309dd9aa..806e001f2bd 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -361,17 +361,17 @@ def _pad( 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 diff --git a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index 3792a149fd7..69bea32acca 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -70,8 +70,9 @@ def _get_node_duration( 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 diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index 25672c137f3..08ac932d8ae 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -105,9 +105,10 @@ def run(self, dag: DAGCircuit): ) 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 diff --git a/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml b/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml new file mode 100644 index 00000000000..d826bc15e48 --- /dev/null +++ b/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml @@ -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. diff --git a/requirements-optional.txt b/requirements-optional.txt index 36985cdd7cd..3dfc2031d02 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -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 diff --git a/setup.py b/setup.py index 9bb5b04ae6e..38af5286e81 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,17 @@ # 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 @@ "crates/pyext/Cargo.toml", binding=Binding.PyO3, debug=rust_debug, + features=features, ) ], options={"bdist_wheel": {"py_limited_api": "cp38"}}, diff --git a/test/python/circuit/library/test_blueprintcircuit.py b/test/python/circuit/library/test_blueprintcircuit.py index 2a5070e8ac7..5f0a2814872 100644 --- a/test/python/circuit/library/test_blueprintcircuit.py +++ b/test/python/circuit/library/test_blueprintcircuit.py @@ -77,17 +77,17 @@ def test_invalidate_rebuild(self): 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.""" diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index 73398e4316b..6fc6e8e72bd 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -187,12 +187,20 @@ def test_foreach_op_indexed(self): 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 @@ def test_param_gate_instance(self): 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) diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 6caf194d37d..e9a7416f78c 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -593,7 +593,7 @@ def test_clear_circuit(self): 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.""" diff --git a/test/python/circuit/test_compose.py b/test/python/circuit/test_compose.py index db6280b8823..7bb36a1401f 100644 --- a/test/python/circuit/test_compose.py +++ b/test/python/circuit/test_compose.py @@ -357,7 +357,8 @@ def test_compose_copy(self): 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) diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index edd01c5cc1c..4ac69278fd4 100644 --- a/test/python/circuit/test_instructions.py +++ b/test/python/circuit/test_instructions.py @@ -577,14 +577,14 @@ def test_instructionset_c_if_with_no_requester(self): 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 @@ def dummy_requester(specifier): 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 @@ def dummy_requester(specifier): 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 @@ def dummy_requester(specifier): 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 @@ def dummy_requester(specifier): 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.""" diff --git a/test/python/circuit/test_isometry.py b/test/python/circuit/test_isometry.py index a09ff331e02..35ff639cedd 100644 --- a/test/python/circuit/test_isometry.py +++ b/test/python/circuit/test_isometry.py @@ -102,7 +102,6 @@ def test_isometry_tolerance(self, iso): # 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( diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index c86deee4287..fd63057cff8 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -26,7 +26,7 @@ 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 @@ def test_append_copies_parametric(self): 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 @@ def test_parameters_property(self): 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 @@ def test_two_parameter_expression_binding(self): 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 @@ def test_expression_partial_binding_zero(self): 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 @@ def test_gate_multiplicity_binding(self): 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 @@ def test_parameter_symbol_equal_after_ufunc(self): 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.""" diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py new file mode 100644 index 00000000000..06d4ed86a60 --- /dev/null +++ b/test/python/circuit/test_rust_equivalence.py @@ -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" + ) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 598405beaae..135e874be48 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -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 @@ def test_basis_gates(self): 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 @@ def test_no_include(self): 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