Repository navigation
Expose Rust Custom Gates to Python - #15723
Raynel Sanchez (raynelfss) wants to merge 8 commits into
Conversation
61dad1e to
b35d1c7
Compare
2ad0b09 to
7815ae9
Compare
|
One or more of the following people are relevant to this code:
|
7815ae9 to
df35411
Compare
10e91ac to
affdb50
Compare
- Add errors any time any python specific methods are invoked on any `CustomOperation` instances (to be resolved by Qiskit#15723).
affdb50 to
0d9aa3d
Compare
Coverage Report for CI Build 27134596691Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage decreased (-0.04%) to 87.407%Details
Uncovered Changes
Coverage Regressions450 previously-covered lines in 16 files lost coverage.
Coverage Stats
💛 - Coveralls |
48112b4 to
9bc20fa
Compare
Jake Lishman (jakelishman)
left a comment
There was a problem hiding this comment.
The ownership and testing stories here are my biggest questions. I think we really should get some set up where we can drive/test things from Python space: that'd give us a much clearer view on how well this hangs together.
| pub struct PyCustomOperation { | ||
| pub inner: Box<dyn CustomOperation>, | ||
| pub parameters: Option<SmallVec<[Param; 3]>>, | ||
| } |
There was a problem hiding this comment.
A few questions here:
- how convinced are we of this ownership model of cloning the
Box<dyn CustomOperation>in order to expose it to Python space? At the very least, it feels like we might want the clone to be lazy in some form or another. - do we need to derive
from_py_object? This is a potentially heavy class, and we probably shouldn't be cloning it in Rust without that being very explicit.
There was a problem hiding this comment.
- Hmm... For this I'm not sure how we should proceed. I could see a how we could use
Arcfor this but it would make operations immutable from Python, which to be honest I'm not sure if it should be the way to go. Parameters should be mutable at least but I don't see how we'd change the operation at a base level. - I'll remove it. Honestly I don't see how we'd explicitly need this during extraction as we'd just clone the parameters and the
Box<dyn CustomOperation>(which I'm starting to think maybe we need to make it into something different - maybe useArc<Box<T>>instead?)
| /// Creates a Python view from a [`CustomOperation`] instance. | ||
| /// | ||
| /// # Arguments: | ||
| /// | ||
| /// * `op` - The operation instance within a box. | ||
| /// * `parameters` - The parameters for this operation, if needed. | ||
| /// | ||
| /// # Returns: | ||
| /// | ||
| /// The python view of the [`CustomOperation`] dyn-object. | ||
| pub fn new(op: Box<dyn CustomOperation>, parameters: Option<SmallVec<[Param; 3]>>) -> Self { | ||
| Self { | ||
| inner: op, | ||
| parameters, | ||
| } | ||
| } |
There was a problem hiding this comment.
If the inner fields are pub, this method isn't really needed - the struct-constructor form PyCustomOperation { inner: ..., parameters: ... } is clearer at the callsite than a list of unnamed options.
There was a problem hiding this comment.
I added a couple lazy private fields which make this method required now.
| /// Creates a more complete Python view of the operation with the associated types. | ||
| /// | ||
| /// This version of the function will use clone the original instance. If you posess | ||
| /// ownership of the object and want to consume it, use [`PyCustomOperation::into_py_op.`] | ||
| pub fn create_py_op<'py>(&'py self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> { | ||
| let cloned = self.clone(); | ||
|
|
||
| Self::into_py_op(cloned, py) | ||
| } | ||
|
|
||
| /// Creates a more complete Python view of the operation with the associated types. | ||
| pub fn into_py_op<'py>(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> { | ||
| let py_type = match self.inner.is_unitary() { | ||
| true => CUSTOM_GATE.get_bound(py), | ||
| false => CUSTOM_INSTRUCTION.get_bound(py), | ||
| }; | ||
| py_type.call1((self,)) | ||
| } |
There was a problem hiding this comment.
create_py_op is an unusual choice here - I'm not sure why hiding a clone is particularly useful. ob.clone().into_py_op(py) is a common and fine pattern.
| // Try Python: | ||
| Python::attach(|py| -> PyResult<()> { | ||
| for (idx, inst) in circuit.data().iter().enumerate() { | ||
| let unpacked_operation = circuit.unpack_py_op(py, inst)?.into_bound(py); | ||
|
|
||
| // Compare repr | ||
| assert_eq!( | ||
| format!( | ||
| "Instruction(name='{}', num_qubits={}, num_clbits={}, params={:?})", | ||
| gates[idx].name(), | ||
| gates[idx].num_qubits(), | ||
| gates[idx].num_clbits(), | ||
| inst.params_view() | ||
| .iter() | ||
| .filter_map(|params| params.try_float()) | ||
| .collect::<Vec<_>>(), | ||
| ), | ||
| unpacked_operation.repr()?.to_string() | ||
| ); | ||
|
|
||
| // Compare python matrix | ||
| let orig_matrix = gates[idx] | ||
| .matrix(inst.params_view()) | ||
| .expect("The provided parameters should generate the matrix"); | ||
| let retrieved_array = unpacked_operation | ||
| .call_method0("to_matrix")? | ||
| .cast_into::<PyArray2<Complex64>>()?; | ||
| let retrieved_matrix = retrieved_array.to_owned_array(); | ||
| assert!(approx::abs_diff_eq!( | ||
| orig_matrix, | ||
| retrieved_matrix, | ||
| epsilon = 1e-12 | ||
| )); | ||
|
|
||
| // Compare params | ||
| assert!( | ||
| unpacked_operation | ||
| .getattr("params")? | ||
| .eq(inst.params_view().into_pyobject(py)?)? | ||
| ); | ||
|
|
||
| // Try roundtrip | ||
| let roundtrip = unpacked_operation.extract::<OperationFromPython<CircuitData>>()?; | ||
| let OperationRef::CustomOperation(roundtrip_native) = roundtrip.operation.view() | ||
| else { | ||
| panic!( | ||
| "Retreived operation is not an instance of CustomOperation: '{:?}'", | ||
| roundtrip.operation.view() | ||
| ); | ||
| }; | ||
|
|
||
| assert_eq!(gates[idx], roundtrip_native); | ||
| assert_eq!(gates[idx].is_unitary(), roundtrip_native.is_unitary()); | ||
| assert_eq!( | ||
| gates[idx].label(), | ||
| roundtrip.label.as_deref().map(|x| x.as_str()) | ||
| ); | ||
| } | ||
| Ok(()) | ||
| }) | ||
| .expect("Something went wrong on the Python side."); | ||
| } |
There was a problem hiding this comment.
It's not a great idea to drive this from Rust space - it implicitly accesses two different compilations of the Rust circuit crate (one direct dependency, one via implicit imports of Python-space Qiskit). It'd quite possibly be better to set up some dummy test-only objects into _accelerate and then drive the tests of them from Python space.
There was a problem hiding this comment.
Hmm... This might be an issue for testing, unless we create a dummy function that can only be tested from Python. Sort of how we did for #13995.
* Initial: Add Custom Gates/Instructions in Rust - Add `CustomOperation` trait that allows users to implement methods available in both `Instruction`s and `Gate`s. - Add the packed representation of `CustomOperation` within a `PackedOperation` called `NativeOperation`. - Use example within `BasisTranslator`'s `compose transforms function where a dummy gate called `CustomDummy` is added to each `DAGCircuit` instance natively. - Adapt codebase to new additions. * Fix: Add case for `CustomInstruction` in QPY - Add missing case for `CustomInstruction` views in QPY. - Add docstring to `CustomOperation` trait. - Add miri flag to `try_add_to_circuit`. * Fix: Add missing methods in `PackedInstruction`. - Fix formatting and clean up tests. * Add: `downcast_ref` for `NativeOperation`. - Add missing docstrings. * Remove: Gate and Instruction views. - The following commit removes different views for `Instruction` vs `Gate` in favor of a consolidated view. Users should be able to use `CustomOperation::kind()` to see the type of operation enclosed. * Add: Method to push custom operation to a Circuit - Add `CircuitData::push_custom_operation` method to directly push `CustomOperation` instances into a `CircuitData` instance. - Add `PackedInstruction::from_custom_operation` method to create a full packed instruction directly from a `CustomOperation` implementor. * Fix: Implement `Deref` and `DerefMut` for `CustomOp`. * Fix: `Native` -> `Custom` * Refactor: Hide `CustomOp` to be internal to the `Circuit` crate The `CustomOp` struct acts as a container to the `Box<dyn CustomOperation>` and its presence outisde of this crate seems to be a little redundant. The following commit reduces the visibility of this struct to only the `circuit` crate, which makes it easier to process a custom operation without the added layer of wrapping it within this struct. * Docs: Fix `CustomOperation`s docstring * Revert: Changes to caused by clippy. * Fix: Address minor review comments - Rename `CustomOp` to `BoxedCustomOperation`. - Remove `from_custom_operation` function. - Rename `from_boxed_custom_operation` to `from_custom_operation`. - Add comment to `extras_keys` to explain use of default value. * Address review comments. * FIx: Use trait upcasing to implement `downcast_ref`. * Fix: Old review comments - Expand docstring for `BoxedCustomOperation`. - Rename unwrapping of `CustomOperation` as `operation` not `gate`. * Remove: `push_custom_operation` * Remove: TODO comment in `circuit_writer` * Fix: Remove Python methods. - Add errors any time any python specific methods are invoked on any `CustomOperation` instances (to be resolved by Qiskit#15723). * Remove: `CustomOperationKind` - Refactor `CustomOperation::kind` to `CustomOperation::is_unitary` which now returns a `bool` value instead of an enumeration to define whether the operation is unitary. Co-authored-by: Julien Gacon <gaconju@gmail.com> * Docs: Remove outdated upcasting docstring * Add: Ability to compare opaque gates Added trait `ComparableOp` as a way of implementing `Eq` for custom operations. This trait is automatically implemented if the operation implements `PartialEq`, otherwise the user is responsible for implementing the trait. The trait contains the function `rich_eq` which promptly receives another `CustomOperation` as the `other` argument and then tries to downcast this instance down to original operation's type and then call `PartialEq::eq`. * FIx: Update docstring. * Fix: Use `PartialEq` implictly. - Witht the addition of `Comparableop` in the previous commit, this approach makes the implementation hidden by turning the trait into a sealed trait and adding an error message whenever it is not implemented. - This enables `dyn CustomOperation` to be able to implement `PartialEq` without breaking dyn-compatibility rules. - An explicit error message will be displayed whenever this trait is not implemented, asking the user to implement `PartialEq`. * Fix: Use `Clone` implictly. - Add sealed trait `CopyableOp` to implicitly enforce implementing `Clone` for a dyn operation. - This enables `dyn CustomOperation` to be able to implement `ToOwned` and `Clone` (only for Box<>)` without breaking dyn-compatibility rules. - Remove requirement to implement `clone_dyn` manually. - An explicit error message will be displayed whenever this trait is not implemented, asking the user to implement `PartialEq`. * Test: Extend the test-suite The following tests cases were added: - Custom gate with label. - Gate equality - Dynamic cloning of a gate with internal data. - Downcasting into correct and incorrect types. - Parameterized gate with a matrix. - Gate with self inverse. - Multiple custom ops in Circuit. - OperationRef keeping `Operation` methods. Previous tests also added extra equality checks. * Fix: Lint (PI = 3.14) * Fix: Use `abs_diff_eq!` to compare matrices * Fix: Epsilon value in matrix comp - Changed tests from a match to `assert_eq`, * Docs: Update docstring to remove `clone_dyn` * Simplify test gate definitions * Remove partial support of `CustomOperation` This support was currently only partial, and untested on read. Since `CustomOperation` is currently not exposed to userspace, it shouldn't appear. We can take the time to consider this more completely in the future; all QPY versions up to the present v17 fairly strongly assume that custom gates are defined in Python space, and this change to the data model may motivate a change in format. * Remove duplicated assertion messages * Correct out of date comment --------- Co-authored-by: Julien Gacon <gaconju@gmail.com> Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
- The following commits expose `CustomOperation` structs to Python via a wrapper of type `PyCustomOp`. - Introduce `CustomGate` and `CustomInstruction` python side structs to represent custom rust operations with the correct class hierarchy.
- Fix formatting.
- Unify the names of the inner types within the python `CustomGate` and `CustomInstruction` to `_native_operation`. - Enable ability to extract operation via `OperationFromPython`. - Introduce `CustomOperation::create_py_op_view()` to turn any `CustomOperation` into a python view.
- Removed most print statements in favor of python comparisons.
9bc20fa to
547b733
Compare
- This implementation uses `OnceLock` to make the initialization of the python counterparts of `::matrix`, `::definition`, and `::params` to be lazily initialized once per call. - The implementation of `::__array__` includes casting to a different `dtype` which has to use the `cast_array_type` function from the `quantum_info` module. This commit slighhly modifies the function to accept different dimensions.
Summary
Blocked by and built on top of #15684
CustomOperationstructs to Python via a wrapper of typePyCustomOp.CustomGateandCustomInstructionPython side structs to represent custom Rust operations with the correct class hierarchy.Details and comments