-
Notifications
You must be signed in to change notification settings - Fork 35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support to apply unitary and non-unitary gates directly to StateVector from Python #121
Conversation
Hello. You may have forgotten to update the changelog!
|
Codecov Report
@@ Coverage Diff @@
## master #121 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 3 3
Lines 54 54
=========================================
Hits 54 54 Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @mlxd! Looks great and I'm really excited to get this feature finally in 🎉
I've left some comments but will defer to @trevor-vincent's feedback for a deeper dive on the technical side.
if ("$ENV{USE_LAPACK}" OR "${USE_LAPACK}") | ||
message(STATUS "Use LAPACKE") | ||
target_link_libraries(external_dependency INTERFACE lapacke) | ||
target_compile_options(external_dependency INTERFACE "-DLAPACKE=1") | ||
endif() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We make no use of LAPACK functions or methods in the library right now, so we don't require this. If you think keeping this to a separate PR is better I'm fine with that.
*/ | ||
void applyOperation(const std::vector<CFP_t> &matrix, | ||
const vector<size_t> &wires, bool inverse = false, | ||
[[maybe_unused]] const vector<fp_t> ¶ms = {}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I can't see why we'd use it here if we have the matrix directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, the params here is to allow simplified dispatch functionality. We define std::function
with a given signature elsewhere, and attempt to ensure similarity throughout. Keeping the params in there was just a cheap way of ensuring this with no additional overheads, since [[maybe_unused]]
is now fully supported by the standard.
if (inverse == true) { | ||
for (size_t j = 0; j < indices.size(); j++) { | ||
const size_t baseIndex = j * indices.size(); | ||
shiftedState[index] += | ||
conj(matrix[baseIndex + i]) * v[j]; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great if the matrix is a unitary, but it will not be the inverse of an arbitrary matrix 🤔 I guess we'll assume unitarity is checked Python-side and inverse can never be set to true if non-unitary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, that was something I was worried about --- I think the safety checks happening Python side make sense for now. If non unitary always ensure inverse is false sounds good for now.
* @param matrix | ||
* @param wires | ||
*/ | ||
void applyMatrixWires(const std::vector<std::complex<fp_t>> &matrix, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree that for this PR it is ok to go for a copy of the matrix rather than passing by reference, but do you have an idea of what changes would be needed to support passing by reference? Would it be a major overhaul?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't the above signature passing by reference already?
void applyMatrixWires(const std::vector<std::complex<fp_t>> &matrix, | ||
const vector<size_t> &wires, bool inverse = false) { | ||
this->applyOperation(matrix, wires, inverse); | ||
} | ||
/** | ||
* @brief Directly apply a given matrix to the specified wires. Data in 1/2D | ||
* numpy complex array format. | ||
* | ||
* @param matrix | ||
* @param wires | ||
* @param inverse | ||
*/ | ||
void applyMatrixWires(const py::array_t<complex<fp_t>> &matrix, | ||
const vector<size_t> &wires, bool inverse = false) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the two cases of applyMatrixWires
, will the second one be the most commonly used (at least initially)? I.e., with directly passing the numpy array. Is there an advantage of the first approach and do we want to steer towards that down the line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, that is the plan. The second approach (the py::array_t
version) works with the way we have everything currently implemented, and allows for as easy as possible a conversion from the numpy array to the C++ layer by casting the pointer to the data buffer, an following through to the underlying implementation accepting pointers over references. The first approach though will be easier to maintain, and adapt. We have both now as both will allow development and comparison to define our needs as the CPP implementations are fleshed out more.
For the moment I think this is the best way for us to handle this, but long-term, I think we may opt to have a seamless CPP backed matrix class that we pass instead. This would alleviate the need for the multiple overloads, but may make interop with other python interfaces a little trickier.
I think this will likely evolve in time once we eventually move away from the fully Python defined implementations.
void apply(const vector<string> &ops, const vector<vector<size_t>> &wires, | ||
const vector<bool> &inverse) { | ||
this->applyOperations(ops, wires, inverse); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this for the case where we want to pass multiple operations that do not have parameters? E.g., CNOTs, Toffolis, etc?
What is the advantage of this approach vs just passing params as []
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, you are right about the nonparam gates. I added this to simply user-experience if choosing to apply defined gates, as I don't think we should require the user to apply empty params if there are no parametric gates needed. Having an overload for this seemed like the best call, though I can remove this and refer to the empty param args if you think it best.
.def("apply", | ||
py::overload_cast< | ||
const vector<string> &, const vector<vector<size_t>> &, | ||
const vector<bool> &, const vector<vector<PrecisionT>> &>( | ||
&StateVecBinder<PrecisionT>::apply)) | ||
.def("apply", py::overload_cast<const vector<string> &, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the long run, is the plan to expose applyOperation
and have the Python side iterate through the operations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is an open question. Whether we aim to have Python iterate through the supplied operations and directly call the newly defined class methods, or simply offload everything to the CPP layer is something I think we eventually should check. Given some conversions are required to pass between the interfaces, my gut says to let Python handle it and make direct calls there, but we should leave this to figure out at a later date once up and running.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work Lee! I didn't see any issues beyond what Tom posted.
Context: Previously the implementation allowed application of a given set of gates. This PR allows a user-defined gate to be directly applied to the statevector from Python.
Description of the Change: Bound the C++ method to directly apply matrix in row-major format. Any Python numeric datatype (list of floats, list of complex, numpy array) that match the given index size can be applied directly. Given the size of the gate is minor compared to the size of the statevector, a copy from Python to CPP is applied first. Further architectural changes may be required later to enable reference/direct access without copies, but are beyond this PR scope.
Benefits: Any arbitrary gate can be now called on lightning from Python.
Possible Drawbacks:
Related GitHub Issues: [ch7527]