-
Notifications
You must be signed in to change notification settings - Fork 188
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
[WIP] - Maximum Independent Set with DQVA ansatz #386
base: master
Are you sure you want to change the base?
Conversation
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.
Amazing work @pangara! ✨ I certainly felt like I understand DQVA better after reading this demo!
I've left some comments and suggestions throughout and admittedly will need to come back to go through the code in more detail
demonstrations/tutorial_dqva_mis.py
Outdated
# infeasible solutions). However, the QAO-Ansatz requires more complex | ||
# quantum circuitry, which in the case of the MIS, manifests itself as | ||
# Multi-Controlled Toffoli gates that require high connectivity between | ||
# qubits. This makes the QAO-Ansatz inadequate for large graphs on current | ||
# near-term quantum computers. |
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.
Great insight! We have found that the large depth of these circuits results in far too much noise on current hardware to get any useful output
demonstrations/tutorial_dqva_mis.py
Outdated
# example, a mixing operator :math:`U_M (\beta)` is decomposed into a | ||
# sequence of partial mixers :math:`U_{M, x}` which may not correspond to | ||
# time evolution under a fixed mixer Hamiltonian :math:`H_M`. |
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.
So here is the idea that the partial mixers correspond to time evolution under components of H_M
but not H_M
itself? Feels like we could add a sentence to clarify how we do model time evolution for the mixer here
demonstrations/tutorial_dqva_mis.py
Outdated
# .. math:: | ||
# | ||
# | ||
# \tilde{b}_{v_j} = \frac{1+Z_{v_j}}{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.
Again, should be Identity
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.
Same here> The way I understood it was that Z__v_j is PauliZ applied to qubit 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.
Awesome work @pangara :) I just finished a pass through the first half of the tutorial and will leave additional comments soon.
@@ -0,0 +1,795 @@ | |||
r""".. _dqva_mis: |
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.
Putting this suggestion on the first line for convenience. This is a bit nitpicky, but the thumbnail for this tutorial does not convey too much information about what to expect. Just stating this in case you have an easy fix; otherwise it's not too important, as other tutorials suffer from this as well.
demonstrations/tutorial_dqva_mis.py
Outdated
# parameters. You can learn more about this in `PennyLane's tutorial on | ||
# QAOA <https://pennylane.ai/qml/demos/tutorial_qaoa_intro.html>`__. | ||
# | ||
# For solving constrained combinatorial optimization problems such as maximal independent set (MIS) |
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 comment does not require a change, just food for thought. Unfortunately, the word "constraint" is a bit ambiguous here, since even Max-Cut has "constraints". These are the constraints whose violation decreases the objective function. This is distinct from "hard constraints" which translate to infeasible solutions in the input space (bitstrings). I think the original QAO-Ansatz paper (Hadfield et al.) has some discussion about this point.
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.
Code is looking really well structured @pangara! Easy to follow and learn from 🎓
I've added some suggestions, mainly tidying up. One of the bigger suggestions would be to add dosctrings for some of the bigger functions. This will help users if they wanted to re-use some of your code (which I'm sure they will!). I've left an example to follow in the code.
There's a couple more functions toward the end of the demo I need to come back to review
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.
Second pass, still more comments to come!
demonstrations/tutorial_dqva_mis.py
Outdated
# .. math:: | ||
# | ||
# | ||
# \tilde{B} = \prod_{j=1}^{l} \tilde{b}_{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.
Kind of weird that the dependence on the node label is is suppressed for this operator but I see that the paper also does this...
# where P is the permutation's function of labels from :math:`1` to | ||
# :math:`N`. |
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 know this is inherited from the paper as well, but this notation doesn't really make sense to me. (I can infer the meaning, but not ideal to have to do that.)
Co-authored-by: Angus Lowe <[email protected]> Co-authored-by: anthayes92 <[email protected]>
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.
Final few suggestion on the code have been added, the main suggestion being to break down some of the code that gets repeated into smaller helper functions that can be re-used.
The logic within the code is looking fantastic, amazing work @pangara 🙌
demonstrations/tutorial_dqva_mis.py
Outdated
# 5. If no new Hamming weight is obtained, the partial mixers are randomized and steps 2 and 3 are repeated to check if | ||
# a better Hamming weight is found. The number of randomizations is controlled via a hyperparameter. |
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.
Are there rendering issues with this line break?
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.
Seems so on my end as well
demonstrations/tutorial_dqva_mis.py
Outdated
|
||
# Select an ordering for the partial mixers | ||
if mixer_order == None: | ||
cur_permutation = np.random.permutation(list(graph.nodes)).tolist() | ||
else: | ||
cur_permutation = mixer_order | ||
|
||
def f(params): | ||
|
||
probs = probability_dqva( | ||
P, params=params, init_state=cur_init_state, mixer_order=cur_permutation | ||
) | ||
avg_cost = 0 | ||
for sample in range(0, len(probs)): | ||
|
||
x = [int(bit) for bit in list(np.binary_repr(sample, len(graph.nodes)))] | ||
# Cost function is Hamming weight | ||
avg_cost += probs[sample] * sum(x) | ||
|
||
return -avg_cost | ||
|
||
# Begin outer optimization loop | ||
best_indset = init_state | ||
best_init_state = init_state | ||
cur_init_state = init_state | ||
best_params = None | ||
best_perm = copy.copy(cur_permutation) | ||
|
||
# Randomly permute the order of mixer unitaries m times | ||
for mixer_round in range(1, m + 1): | ||
|
||
inner_round = 1 | ||
new_hamming_weight = hamming_weight(cur_init_state) | ||
|
||
while True: | ||
print( | ||
"Start round {}.{}, Initial state = {}".format( | ||
mixer_round, inner_round, cur_init_state | ||
) | ||
) | ||
|
||
# Begin inner variational loop | ||
num_params = P * (len(graph.nodes()) + 1) | ||
print("\tNum params =", num_params) | ||
init_params = np.random.uniform(low=0.0, high=2 * np.pi, size=num_params) | ||
print("\tCurrent Mixer Order:", cur_permutation) | ||
|
||
# Optimize parameters | ||
optimizer = qml.GradientDescentOptimizer(stepsize=0.5) | ||
cur_params = init_params.copy() | ||
|
||
for i in range(70): | ||
cur_params, opt_cost = optimizer.step_and_cost(f, cur_params) | ||
|
||
opt_params = cur_params.copy() | ||
|
||
print("\tOptimal cost:", opt_cost) | ||
|
||
probs = probability_dqva( | ||
P, params=opt_params, init_state=cur_init_state, mixer_order=cur_permutation | ||
) | ||
|
||
top_counts = list( | ||
map(lambda x: np.binary_repr(x, len(graph.nodes)), np.argsort(probs)) | ||
)[::-1] | ||
|
||
best_hamming_weight = hamming_weight(best_indset) | ||
better_strs = [] | ||
|
||
for bitstr in top_counts[:1]: | ||
this_hamming = hamming_weight(bitstr) | ||
if is_indset(bitstr, graph) and this_hamming > best_hamming_weight: | ||
better_strs.append((bitstr, this_hamming)) | ||
better_strs = sorted(better_strs, key=lambda t: t[1], reverse=True) | ||
|
||
# If no improvement was made, break and go to next mixer round | ||
if len(better_strs) == 0: | ||
print( | ||
"\tNone of the measured bitstrings had higher Hamming weight than:", best_indset | ||
) | ||
break | ||
|
||
# Otherwise, save the new bitstring and repeat | ||
best_indset, new_hamming_weight = better_strs[0] | ||
best_init_state = cur_init_state | ||
best_params = opt_params | ||
best_perm = copy.copy(cur_permutation) | ||
cur_init_state = best_indset | ||
print( | ||
"\tFound new independent set: {}, Hamming weight = {}".format( | ||
best_indset, new_hamming_weight | ||
) | ||
) | ||
inner_round += 1 | ||
|
||
# Choose a new permutation of the mixer unitaries | ||
cur_permutation = np.random.permutation(list(graph.nodes)).tolist() | ||
|
||
print("\tRETURNING, best hamming weight:", new_hamming_weight) | ||
return best_indset, best_params, best_init_state, best_perm |
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 function uses a lot of the same code that solve_mis_qaoa
uses on line 363
. Again we could think about creating smaller functions to be reused here. The above examples of helper function suggestions can be used as a guide here 😃
This may also have the bonus of breaking up these MIS solver functions into smaller digestible pieces
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 have made two helpers for this - hopefully it's better. This is a long function!
demonstrations/tutorial_dqva_mis.py
Outdated
base_str = "0" * len(graph.nodes) | ||
for i in range(len(graph.nodes)): | ||
init_str = list(base_str) | ||
init_str[i] = "1" | ||
out = solve_mis_qaoa("".join(init_str), P=1, m=4, threshold=1e-5, cutoff=2) | ||
print(f"Init string: {init_str}, Best MIS: {out[0]}") | ||
print() |
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 could be made into a function since it is used again after using DQVA to solve MIS on line 754
demonstrations/tutorial_dqva_mis.py
Outdated
# 5. If no new Hamming weight is obtained, the partial mixers are randomized and steps 2 and 3 are repeated to check if | ||
# a better Hamming weight is found. The number of randomizations is controlled via a hyperparameter. |
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.
Seems so on my end as well
Co-authored-by: anthayes92 <[email protected]>
Co-authored-by: Angus Lowe <[email protected]> Co-authored-by: anthayes92 <[email protected]>
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.
@pangara great to see this come together 🎉 I've just done a quick pass, I didn't go thoroughly through all the math or code examples.
print() | ||
|
||
###################################################################### | ||
# Starting with an all-zero initial string will give us an independent |
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.
Depending on how the randomness works out this may be confusing; in my local build, the all-zero start found an MIS of size 3, but the subsequent code block, not all of them did, and I had to scroll through a lot of output. Maybe for this case it would be better to streamline the output so that it only returns the final result of the loop (like only indicate which string was the starting string and the best MIS it found)
# is the same as the QAO-Ansatz (the Hamming weight operator). | ||
# | ||
# In the DQVA, the way mixers are defined is slightly different from the | ||
# QAO-Ansatz and are allowed to be independent. |
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.
# QAO-Ansatz and are allowed to be independent. | |
# QAO-Ansatz and are allowed to be independent: |
Independent of what?
# | ||
|
||
|
||
def solve_mis_dqva(init_state: Optional[str], P: Optional[int]=1, m: Optional[int]=1, mixer_order: Optional[List]=None, threshold: Optional[float]=1e-5, cutoff: Optional[int]=1) -> Tuple[str, np.ndarray, str, List]: |
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.
As a general comment, I had a hard time following the code blocks and outputs because there was just a lot of "stuff" in them (type hints, full docstrings, print statements, etc.) For a demo, there isn't usually full documentation unless there is something particularly unclear (e.g., parameters that weren't discussed in the text), since in a sense the prose is documenting what is being done.
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.
Ah so I actually advocated for docstrings and typehints but perhaps I was too quick to assume that this is makes things clearer for everyone!
IMO typehints could be dropped but docstrings could be useful for re-usability (though not sure if this is the intention of demos). Happy to leave this to your best judgement.
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.
As @glassnotes notes, we generally avoid docstrings and typehints in demos 🙂 They can be a distraction from the content and code for readers that are not familiar with Python software development, and can in some cases hinder from the flow of the text
# solve MIS, which involves classical partitioning of a large graph into | ||
# sub-graphs, finding a solution using DQVA on a subgraph, and then | ||
# preparing a set of states to be passed as an input, essentially | ||
# stitching together solutions. |
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.
Nice conclusion 👍
Co-authored-by: Olivia Di Matteo <[email protected]>
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.
Approved subject to adding the remaining suggestions ✔️ 🎉
Thanks for the consistent great work here @pangara. This demo come together really nicely!
# | ||
|
||
|
||
def solve_mis_dqva(init_state: Optional[str], P: Optional[int]=1, m: Optional[int]=1, mixer_order: Optional[List]=None, threshold: Optional[float]=1e-5, cutoff: Optional[int]=1) -> Tuple[str, np.ndarray, str, List]: |
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.
Ah so I actually advocated for docstrings and typehints but perhaps I was too quick to assume that this is makes things clearer for everyone!
IMO typehints could be dropped but docstrings could be useful for re-usability (though not sure if this is the intention of demos). Happy to leave this to your best judgement.
Thank you for opening this pull request. You can find the built site at this link. Deployment Info:
Note: It may take several minutes for updates to this pull request to be reflected on the deployed site. |
Before submitting
Please complete the following checklist when submitting a PR:
Ensure that your tutorial executes correctly, and conforms to the
guidelines specified in the README.
Add a thumbnail link to your tutorial in
beginner.rst
, or if aQML implementation, in
implementations.rst
.All QML tutorials conform to
PEP8 standards.
To auto format files, simply
pip install black
, and thenrun
black -l 100 path/to/file.py
.When all the above are checked, delete everything above the dashed
line and fill in the pull request template.
Title: Maximum Independent Set with DQVA ansatz
Summary:
Relevant references: Approaches to Constrained Quantum Approximate Optimization
Possible Drawbacks:
Related GitHub Issues: