-
Notifications
You must be signed in to change notification settings - Fork 33
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
Refactor handling of gate matrices and inverses #752
base: main
Are you sure you want to change the base?
Refactor handling of gate matrices and inverses #752
Conversation
0c22ab7
to
3d824d7
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #752 +/- ##
======================================
Coverage 92.1% 92.1%
======================================
Files 125 125
Lines 13790 13665 -125
Branches 2164 2172 +8
======================================
- Hits 12702 12596 -106
+ Misses 1088 1069 -19
*This pull request uses carry forward flags. Click here to find out more.
|
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.
Hey 👋
Many thanks for starting to work on this! Great to see someone taking this on!
I really like the solution with the flags for indicating the inverse type of a gate and indicating whether a gate is diagonal! Very elegant.
I added some inline comments that are mostlly minor except one bigger one that might trigger some further refactorings. However, I believe these might be worth it.
One thing on top of all of that: It would be great if you could address the clang-tidy complaints.
Typically these are posted as PR comments, but that feature does not work for PRs from forks. You can still see the warnings either in the check summary here: https://github.com/cda-tum/mqt-core/pull/752/checks or when looking through the "Files Changed" tab on GitHub.
Thanks again and let me know if anything is unclear!
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.
Oops, clicked the wrong button 🙃
4632650
to
165f26b
Compare
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.
Many thanks for the iteration here. This is looking great already and I like where this is heading.
I went through everything in detail again and added my feedback inline. Hope that makes sense. Please let me know if it doesn't!
refractor functions to receive Operation as const referece instead of pointer refractor getStandardOperationDD to receive targets as vector, remove overload that takes two targets and inline helper function as it now only has a single use
165f26b
to
337ba29
Compare
I tried to implement your thoughts as good as possible. One change necessary to transition the test to retrieve the DD via the I hope this is a further improvement to the previous draft. |
I am also trying to understand why one CI build fails, but I don't see how my changes produced this error. Would you happen to have an idea, or would you be able to help me with that? |
That failure is unrelated to your changes and either due to an update in pandas or an update in |
Turns out it's something else entirely. It's due to |
Okay, thanks. Then, I will rebase onto the new main so that the test passes again. Do you have any further change requests regarding my pull request? |
Sounds good. |
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.
Many thanks again for your work on this!
I really like the way this is going and how it simplifies certain aspects in the library.
I feel we are really close to the finish line here. There are a couple of errors that snuck their way into the last couple of commits and some further suggestions for getting the implementation even cleaner and more performant.
I am fairly optimistic that this will be the last round of iteration.
// some operations need parameter changes even in the non-inverse case | ||
if (type == qc::U) { | ||
assert(params.size() == 3U); | ||
// shuffle [a, b, c] to [c, b, a] | ||
std::reverse(params.begin(), params.end()); | ||
} else if (type == qc::U2) { | ||
params[0U] = params[1U]; | ||
} |
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.
Wouldn't it be simpler to eliminate these special cases by refactoring the respective u2
and u
matrix methods so that they take their parameters in the opposite order? Sounds like less special handling to me.
Furthermore, the U2
condition isn't quite right as it does not swap the parameters.
auto type = op.getType(); | ||
std::vector<fp> params = op.getParameter(); | ||
std::vector<qc::Qubit> targetQubits = targets; |
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.
Sorry for being so picky here, but I believe this could be improved further by a bit.
At the moment, this incurs an extra copy for these variables in the inverse=false
case that should not really be necessary.
Given that these copies are small, I would be fine with leaving it, but I wanted to bring it up in case you see an elegant solution that avoids that.
One solution that I could think of is to create a separate GetStandardOperationInverseDD
method that essentially forwards to the getStandardOperationDD
method after performing the necessary adaptations.
That way, copies should only be required where they are really necessary.
Instead of break
statements in the respective case
statements of the new inverse method, it could directly return the respective value from getStandardOperationDD
, i.e., the self-inverse gates would return without any modifications, and the others would just construct a new StandardOperation with the right parameters and pass it over.
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.
Yes, good point. I think we can solve this without a separate function and just have it inline like this:
if (not inverse) {
// return gate directly
}
// construct inverse gate
std::vector<qc::Qubit> targetQubits = targets;
switch {
// ...
}
// return inverse gate
This would have a duplicate call to gate creations, but as this is only two lines, I think this is ok.
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.
That works for me 👍🏼 sounds like a nice compromise.
params[0U] = -params[1U] + PI; | ||
params[1U] = -params[1U] - PI; |
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 not quite right as it does not preserve the old semantics
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 will take a closer look at this
@@ -186,17 +141,17 @@ getStandardOperationDD(const qc::StandardOperation* op, Package<Config>& dd, | |||
// An empty permutation marks the identity permutation. | |||
|
|||
template <class Config> | |||
qc::MatrixDD getDD(const qc::Operation* op, Package<Config>& dd, | |||
qc::MatrixDD getDD(const qc::Operation& op, Package<Config>& dd, |
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.
Just a comment: I was a bit hesitant about this change at first, as I thought it would cause object slicing, but I convinced myself that this is not the case and this still works as intended. I agree with your assessment that this even makes the intent clearer because op == nullptr
was never even handled in the first place.
@@ -186,17 +141,17 @@ getStandardOperationDD(const qc::StandardOperation* op, Package<Config>& dd, | |||
// An empty permutation marks the identity permutation. | |||
|
|||
template <class Config> | |||
qc::MatrixDD getDD(const qc::Operation* op, Package<Config>& dd, | |||
qc::MatrixDD getDD(const qc::Operation& op, Package<Config>& dd, |
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.
Given the comment above, I am also thinking whether it wouldn't be cleaner to separate the inverse
function from the non-inverse
function here.
I think the duplication would be at a minimum and the code would be easier to read overall.
What do you think?
if (qc::isTwoQubitGate(type)) { | ||
assert(targets.size() == 2); |
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 distinction on whether it is a one or two target gate is superfluous based on your changes in previous commits.
assert(false && "Invalid single-qubit gate type"); | ||
return {}; // unreachable |
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.
If you really want to mark this as unreachable, then you can use qc::unreachable()
.
assert(false && "Invalid single-qubit gate type"); | |
return {}; // unreachable | |
assert(false && "Invalid single-qubit gate type"); | |
qc::unreachable(); |
Although that part of the code is technically reachable as the method is public and can easily be passed a wrong OpType
.
I'd still be fine with adding the unreachable there (and down below).
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.
If this code can get called by a library user, would it be better to raise an exception?
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.
Most likely, yes.
Could be an std::invalid_argument
exception.
That way, such errors get a proper error message even in Release mode.
auto swapGate = dd->makeTwoQubitGateDD(dd::opToTwoQubitGateMatrix(qc::SWAP), | ||
qc::Controls{}, 0, 2); |
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.
You should be able to also replace this with a corresponding StandardOperation call to stay consistent. (same goes for a couple of occurences down below)
TEST(DDPackageTest, TwoQubitControlledGateDDConstruction) { | ||
const auto nrQubits = 5U; | ||
const auto dd = std::make_unique<dd::Package<>>(nrQubits); | ||
|
||
const auto gateMatrices = std::vector{std::pair{dd::X_MAT, dd::CX_MAT}, | ||
std::pair{dd::Z_MAT, dd::CZ_MAT}}; | ||
|
||
// For every combination of control and target, test that the DD created by | ||
// makeTwoQubitGateDD is equal to the DD created by makeGateDD. This should | ||
// cover every scenario of the makeTwoQubitGateDD function. | ||
for (const auto& [gateMatrix, controlledGateMatrix] : gateMatrices) { | ||
for (dd::Qubit control = 0; control < nrQubits; ++control) { | ||
for (dd::Qubit target = 0; target < nrQubits; ++target) { | ||
if (control == target) { | ||
continue; | ||
} | ||
const auto controlledGateDD = | ||
dd->makeTwoQubitGateDD(controlledGateMatrix, control, target); | ||
const auto gateDD = dd->makeGateDD( | ||
gateMatrix, qc::Control{static_cast<qc::Qubit>(control)}, target); | ||
EXPECT_EQ(controlledGateDD, gateDD); | ||
} | ||
} | ||
} | ||
} | ||
|
||
TEST(DDPackageTest, TwoQubitControlledGateDDConstructionNegativeControls) { | ||
const auto nrQubits = 5U; | ||
const auto dd = std::make_unique<dd::Package<>>(nrQubits); | ||
|
||
const auto gateMatrices = std::vector{std::pair{dd::X_MAT, dd::CX_MAT}, | ||
std::pair{dd::Z_MAT, dd::CZ_MAT}}; | ||
|
||
// For every combination of controls, control type, and target, test that the | ||
// DD created by makeTwoQubitGateDD is equal to the DD created by makeGateDD. | ||
// This should cover every scenario of the makeTwoQubitGateDD function. | ||
for (const auto& [gateMatrix, controlledGateMatrix] : gateMatrices) { | ||
for (dd::Qubit control0 = 0; control0 < nrQubits; ++control0) { | ||
for (dd::Qubit control1 = 0; control1 < nrQubits; ++control1) { | ||
if (control0 == control1) { | ||
continue; | ||
} | ||
for (dd::Qubit target = 0; target < nrQubits; ++target) { | ||
if (control0 == target || control1 == target) { | ||
continue; | ||
} | ||
for (const auto controlType : | ||
{qc::Control::Type::Pos, qc::Control::Type::Neg}) { | ||
const auto controlledGateDD = dd->makeTwoQubitGateDD( | ||
controlledGateMatrix, qc::Controls{{control0, controlType}}, | ||
control1, target); | ||
const auto gateDD = dd->makeGateDD( | ||
gateMatrix, qc::Controls{{control0, controlType}, control1}, | ||
target); | ||
EXPECT_EQ(controlledGateDD, gateDD); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
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'd be in favor of leaving these tests in, but just locally defining the CX and CZ matrices. These are good sanity checks for the DD generation routines.
Description
These commits change how matrices are created from an OpType that reduces the number of switch statements necessary.
The downside of this approach is that some functions can be called with arguments that do not affect the returned value. As the user never calls these functions, I see this as acceptable.
I also removed the CX_MAT and CZ_MAT, as they are only used in tests and do not occur in any other repository.
Fixes #484
Checklist:
I have not squashed the commits to make them easier to review, but I can squash them if desired.