Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support to apply unitary and non-unitary gates directly to StateVector from Python #121

Merged
merged 76 commits into from
Aug 9, 2021

Conversation

mlxd
Copy link
Member

@mlxd mlxd commented Jul 30, 2021

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]

@github-actions
Copy link
Contributor

Hello. You may have forgotten to update the changelog!
Please edit .github/CHANGELOG.md with:

  • A one-to-two sentence description of the change. You may include a small working example for new features.
  • A link back to this PR.
  • Your name (or GitHub username) in the contributors section.

@github-actions
Copy link
Contributor

github-actions bot commented Jul 30, 2021

Test Report (C++) on Ubuntu

    1 files  ±0      1 suites  ±0   0s ⏱️ ±0s
164 tests ±0  164 ✔️ ±0  0 💤 ±0  0 ❌ ±0 
694 runs  ±0  694 ✔️ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 800eaf3. ± Comparison against base commit 800eaf3.

♻️ This comment has been updated with latest results.

@codecov
Copy link

codecov bot commented Jul 30, 2021

Codecov Report

Merging #121 (f77db2e) into master (e309d44) will not change coverage.
The diff coverage is n/a.

❗ Current head f77db2e differs from pull request most recent head 132eb49. Consider uploading reports for the commit 132eb49 to get more accurate results
Impacted file tree graph

@@            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.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e309d44...132eb49. Read the comment docs.

@mlxd mlxd requested review from trbromley and trevor-vincent and removed request for trbromley July 30, 2021 10:42
@mlxd mlxd requested a review from trbromley July 30, 2021 11:07
Copy link
Contributor

@trbromley trbromley left a 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.

Comment on lines -49 to -54
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()

Copy link
Contributor

Choose a reason for hiding this comment

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

Why this change?

Copy link
Member Author

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> &params = {}) {
Copy link
Contributor

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.

Copy link
Member Author

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.

Comment on lines +302 to +307
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];
}
Copy link
Contributor

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?

Copy link
Member Author

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.

pennylane_lightning/src/Util.hpp Show resolved Hide resolved
pennylane_lightning/src/tests/TestHelpers.hpp Show resolved Hide resolved
* @param matrix
* @param wires
*/
void applyMatrixWires(const std::vector<std::complex<fp_t>> &matrix,
Copy link
Contributor

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?

Copy link
Member Author

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?

Comment on lines +74 to +87
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) {
Copy link
Contributor

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?

Copy link
Member Author

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.

Comment on lines +63 to +66
void apply(const vector<string> &ops, const vector<vector<size_t>> &wires,
const vector<bool> &inverse) {
this->applyOperations(ops, wires, inverse);
}
Copy link
Contributor

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 []?

Copy link
Member Author

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.

Comment on lines +107 to +112
.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> &,
Copy link
Contributor

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

I 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.

Copy link
Contributor

@trevor-vincent trevor-vincent left a 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.

@mlxd mlxd merged commit 800eaf3 into master Aug 9, 2021
@mlxd mlxd deleted the 7527_unitary_gate branch August 9, 2021 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants