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

feat: Updated repr for ParametrizedHamiltonian #4176

Merged
merged 13 commits into from Jun 2, 2023

Conversation

anushkrishnav
Copy link
Contributor

@anushkrishnav anushkrishnav commented May 26, 2023

Before submitting

Please complete the following checklist when submitting a PR:

  • All new features must include a unit test.
    If you've fixed a bug or added code that should be tested, add a test to the
    test directory!

  • All new functions and code must be clearly commented and documented.
    If you do make documentation changes, make sure that the docs build and
    render correctly by running make docs.

  • Ensure that the test suite passes, by running make test.

  • Add a new entry to the doc/releases/changelog-dev.md file, summarizing the
    change, and including a link back to the PR.

  • The PennyLane source code conforms to
    PEP8 standards.
    We check all of our code against Pylint.
    To lint modified files, simply pip install pylint, and then
    run pylint pennylane/path/to/file.py.

When all the above are checked, delete everything above the dashed
line and fill in the pull request template.


Context:
The context of this change is to provide an informative string representation for the ParametrizedHamiltonian class in PennyLane.

Description of the Change:
The change involves updating the repr method of the ParametrizedHamiltonian class to generate a string representation that closely follows the example provided. This includes incorporating the parametrized functions and their corresponding indices in the expression, using the appropriate brackets.

Benefits:

The updated string representation will provide users with a more informative view of the ParametrizedHamiltonian object. It will clearly show the terms of the Hamiltonian, including any parametrized functions, their corresponding parameter indices, and the time variable.

Possible Drawbacks:
One potential drawback is that the string representation may become more complex for Hamiltonians with a large number of terms or deeply nested expressions. This could make it harder to read and understand for very complex Hamiltonians.

Related GitHub Issues:
#4088

@Qottmann Qottmann self-requested a review May 26, 2023 12:40
Copy link
Contributor

@Qottmann Qottmann left a comment

Choose a reason for hiding this comment

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

Hello @anushkrishnav thank you for your contribution!
This already looks like it is on a good way :-)

There are two issues with the current code:

  1. the index i is not displaying the correct number
  2. callable class objects raise an error because they dont have a __name__ attribute

Additionally, it seems there is a failing test in tests/pulse/test_parametrized_hamiltonian.py::TestInitialization::test__repr__
You can update this test, and I would suggest adding another test to check the new feature behaves as you intend it to. In order to check the test is passing, you can run python -m pytest tests/pulse/test_parametrized_hamiltonian.py (or any other relevant file) in the terminal before pushing.

The following is optional:
There is a special subclass of ParametrizedHamiltonian, HardwareHamiltonian. Let me tell you what the situation is and how it is handled in HardwareHamiltonian.
For transmon or rydberg systems, the driving term of the Hamiltonian typically has the form
$H_\text{drive} = \Omega(p, t) (\cos(\nu t) X - \sin(\nu t) Y)$.
When implementing this in pennylane right now, this yields two separate terms $\Omega(p, t) \cos(\nu t) X$ and $-\Omega(p, t) \sin(\nu t) Y$, sharing the same parameters $(p, \nu)$. In order to avoid having to pass the parameters twice to the function, there is a _reorder_parameters function that reorders the parameters that are passed (see https://github.com/PennyLaneAI/pennylane/blob/master/pennylane/pulse/hardware_hamiltonian.py#L237 for rydberg systems and https://github.com/PennyLaneAI/pennylane/blob/master/pennylane/pulse/transmon.py#L502 for transmons.
That means, the string representation of HardwareHamiltonian (which inhertis from ParametrizedHamiltonian) would have to be overwritten with a slightly modified function that takes that reordering into account.

It is slightly more tricky, so optional. Let me know if you are interested and I can provide you with more info and help.

pennylane/pulse/parametrized_hamiltonian.py Outdated Show resolved Hide resolved
pennylane/pulse/parametrized_hamiltonian.py Outdated Show resolved Hide resolved
@anushkrishnav
Copy link
Contributor Author

Hey thanks for the detailed review @Qottmann I will start working on the requested changes, Will update the tests as well.

@anushkrishnav
Copy link
Contributor Author

I have made the requested updates @Qottmann
Kindly take a look,
Thank you.

@anushkrishnav
Copy link
Contributor Author

anushkrishnav commented May 28, 2023

Yeh, the optional one sounds interesting do share more info I will work on it as an individual PR, I am currently looking at the files you share to get a better grasp of what's actually happening and the reordering ,
The overwriting is pretty easy, I just wish to know how the reordering takes place and whats the expected output string

@codecov
Copy link

codecov bot commented May 28, 2023

Codecov Report

Merging #4176 (e7840df) into master (a7ae25b) will increase coverage by 0.00%.
The diff coverage is 100.00%.

@@           Coverage Diff           @@
##           master    #4176   +/-   ##
=======================================
  Coverage   99.77%   99.77%           
=======================================
  Files         342      342           
  Lines       30709    30721   +12     
=======================================
+ Hits        30639    30651   +12     
  Misses         70       70           
Impacted Files Coverage Δ
pennylane/ops/functions/dot.py 100.00% <ø> (ø)
pennylane/pulse/__init__.py 100.00% <ø> (ø)
pennylane/pulse/rydberg.py 100.00% <ø> (ø)
pennylane/pulse/transmon.py 100.00% <ø> (ø)
pennylane/pulse/hardware_hamiltonian.py 100.00% <100.00%> (ø)
pennylane/pulse/parametrized_hamiltonian.py 100.00% <100.00%> (ø)

Copy link
Contributor

@Qottmann Qottmann left a comment

Choose a reason for hiding this comment

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

Getting there, great work @anushkrishnav 💪

Note that lines 253 and 255 are currently not covered by the tests (this is what codecov checks and complains about in the test suite below). In order to fix that, you need to add tests that cover these cases. I.e. test the string representation with a ParametrizedHamiltonian that contains a callable class object with a __name__, and another test with a callable class object with no __name__ and assert that it outputs what you expect.

Some optional refinement (not needed for unitary hack, but I think it would make the PR overall nicer): Currently everything is returned in one line. It would be great to output terms in separate lines like is done in qml.Hamiltonian, see ops/qubit/hamiltonian.py#LL490 for inspiration. See example here:
image

Since we are changing the string representation, we also need to make sure the outputs that were hardcoded in the docs are updated. This concerns, as far as I can see, the doc strings in:

If you want to do the case of HardwareHamiltonian in a separate PR, already this PR should overwrite the __repr__ to do the default because it currently outputs the wrong string. I will write a separate message to provide more details to continue there :)

@Qottmann
Copy link
Contributor

Qottmann commented May 29, 2023

Re HardwareHamiltonian:
This is a special class to look at Hamiltonians that can be executed on a real device. We are currently supporting transmon (superconducting qubits) and rydberg (neutral atom) systems. Let us look at the case for transmons (rydberg is very similar). The Hamiltonian has the form

$H = H_\text{int} + \sum_i \Omega_i(t) (\exp(\phi_i + \nu_i t) a_i + \exp(\phi_i + \nu_i t) a^\dagger_i)$

where $H_\text{int}$ is some fixed interaction Hamiltonian. There are two complications:
a) All three of $\phi_i$, $\Omega_i(t)$ and $\nu_i$ can be callable, so we have two functions, each expecting a parameter to be passed to it (this is a problem because ParametrizedHamiltonian always expects the scalar callables to have the signature f(p, t))
b) Two different terms, $\Omega_i(t) \exp(\phi_i + \nu_i t) X_i$, and $\Omega_i(t) \exp(\phi_i + \nu_i t) Y_i$ share the parameters in \Omega_i and \nu_i.
If you were to implement this Hamiltonian by hand in PennyLane, you need some workarounds. I did this in this demo:
image

To allow users to be flexible in whether they want $\phi_i$, $\nu_i$ or $\Omega_i$ to be callable or not, and not have to pass the parameters twice, we introduced the reorder_fn under the hood in HardwareHamiltonian. Internally, it uses a special callable class object AmplitudeAndPhaseAndFreq that checks which are callable and which one is not, and accordingly uses the right function as its call. On the other hand, the reorder function checks which of the three are callable, and passes twice, depending on how many are callable, the next 1, 2 or 3 parameters.

The string representation would basically have to follow the same logic as in https://github.com/PennyLaneAI/pennylane/blob/master/pennylane/pulse/transmon.py#L502 i.e. check whether it is an instance of this special callable class, and if yes count the number of callables and accordingly increase the index that is being displayed.

So ideally the string representation for the following Hamiltonian would look like this

qubit_freq = [1., 1.]
connections = [(0, 1)]
coupling = 0.01831
wires = [0, 1]

def f1(p, t):
    return p * t

def f2(p, t):
    return jnp.sin(p * t)

H = qml.pulse.transmon_interaction(qubit_freq, connections, coupling, wires)
H += qml.pulse.transmon_drive(f1, f1, 0.5, wires=[0])
H += qml.pulse.transmon_drive(f1, 0.2, f2, wires=[1])

>>> print(H)
(6.283185307179586*(((0.5*(PauliX(wires=[0]))) + ((-0-0.5j)*(PauliY(wires=[0])))) @ ((0.5*(PauliX(wires=[0]))) + (0.5j*(PauliY(wires=[0]))))))
+ (6.283185307179586*(((0.5*(PauliX(wires=[1]))) + ((-0-0.5j)*(PauliY(wires=[1])))) @ ((0.5*(PauliX(wires=[1]))) + (0.5j*(PauliY(wires=[1]))))))
+ (0.11504512297445822*((((0.5*(PauliX(wires=[0]))) + ((-0-0.5j)*(PauliY(wires=[0])))) @ ((0.5*(PauliX(wires=[1])))
+ (6.283185307179586*(((0.5*(PauliX(wires=[1]))) + ((-0-0.5j)*(PauliY(wires=[1])))) @ ((0.5*(PauliX(wires=[1]))) + (0.5j*(PauliY(wires=[1])))))) + (0.06283185307179587*((((0.5*(PauliX(wires=[0]))) + ((-0-0.5j)*(PauliY(wires=[0])))) @ ((0.5*(PauliX(wires=[1]))) + (0.5j*(PauliY(wires=[1]))))) + (((0.5*(PauliX(wires=[1]))) + ((-0-0.5j)*(PauliY(wires=[1])))) @ ((0.5*(PauliX(wires=[0]))) + (0.5j*(PauliY(wires=[0])))))))
+ (f1(params_0, t) cos(f1(params_1, t) + 0.5 * t) * (PauliX(wires=[0]))) + (f1(params_0, t) cos(f1(params_1, t) + 0.5 * t) * *(-1*(PauliY(wires=[0]))))
+ (f1(params_2, t) cos(0.2 + f2(params_3, t) * t) * (PauliX(wires=[1]))) + (f1(params_2, t) cos(0.2 + f2(params_3, t) * t) * (-1*(PauliY(wires=[1])))

The part about the annihilation and creation operators being explicitly constructed as $a = 1/2(X + i Y)$ is something that would go beyond the scope of this PR I'm afraid. It is not super pretty yet, but still much more informative than the default behavior.
For rydbergs it should be prettier :-)
Some further refinement could be to write the $6.283185307179586 = 2 \pi$ factors explicitly.

@anushkrishnav
Copy link
Contributor Author

anushkrishnav commented May 30, 2023

Getting there, great work @anushkrishnav muscle

Note that lines 253 and 255 are currently not covered by the tests (this is what codecov checks and complains about in the test suite below). In order to fix that, you need to add tests that cover these cases. I.e. test the string representation with a ParametrizedHamiltonian that contains a callable class object with a __name__, and another test with a callable class object with no __name__ and assert that it outputs what you expect.

Some optional refinement (not needed for unitary hack, but I think it would make the PR overall nicer): Currently everything is returned in one line. It would be great to output terms in separate lines like is done in qml.Hamiltonian, see ops/qubit/hamiltonian.py#LL490 for inspiration. See example here: image

Since we are changing the string representation, we also need to make sure the outputs that were hardcoded in the docs are updated. This concerns, as far as I can see, the doc strings in:

If you want to do the case of HardwareHamiltonian in a separate PR, already this PR should overwrite the __repr__ to do the default because it currently outputs the wrong string. I will write a separate message to provide more details to continue there :)

I don't think we will require the else condition since it deals with only 3 situations from what I understand, The coeff are numerical values or a function with name or an object with no name but can be obtained using class.name
I tried to use lambda as an Anonymous function but we are still able to obtain the info that the function is a lambda function which is what we ideally need. So I am removing the else condifiton and retaining the if and elif and adding test to check both of the statements.

from pennylane.pulse import ParametrizedHamiltonian
coeffs = [2.0, f1, f2, lambda x: x**2]
ops = [qml.PauliX(0), qml.PauliY(0), qml.PauliZ(0), qml.PauliX(0)]
H = ParametrizedHamiltonian(coeffs, ops)
expected = "(2.0*(PauliX(wires=[0]))) + (f1(params_0, t)*(PauliY(wires=[0]))) + (f2(params_1, t)*(PauliZ(wires=[0]))) + (3*(PauliX(wires=[1])))"

print(H)

(2.0*(PauliX(wires=[0]))) + (f1(params_0, t)(PauliY(wires=[0]))) + (f2(params_1, t)(PauliZ(wires=[0]))) + ((params_2, t)*(PauliX(wires=[0])))

@anushkrishnav
Copy link
Contributor Author

anushkrishnav commented May 30, 2023

As for the docs I just need to update the docstring right? I did a search and updated the docstring I can find

Copy link
Contributor

@Qottmann Qottmann left a comment

Choose a reason for hiding this comment

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

Thanks for the update @anushkrishnav 🚀
The line breaks make it very clean, nice!

There are missing outputs in the docstring of ParametrizedHamiltonian

We cant merge the PR as long as the transmon and rydberg Hamiltonians are producing erroneous outputs. Since HardwareHamiltonian inherits from ParametrizedHamiltonian, it is using the same __repr__ method. But in that case, we want the default back because the logic that you added does not apply to those Hamiltonians as described and discussed in the potential follow-up PR.

tests/pulse/test_parametrized_hamiltonian.py Outdated Show resolved Hide resolved
tests/pulse/test_parametrized_hamiltonian.py Show resolved Hide resolved
pennylane/pulse/rydberg.py Outdated Show resolved Hide resolved
@Qottmann
Copy link
Contributor

Note also that currently pylint and black are failing!
See https://docs.pennylane.ai/en/stable/development/guide/pullrequests.html

@anushkrishnav
Copy link
Contributor Author

anushkrishnav commented May 30, 2023

Yeh I saw the pylint my next commit will fix that

Copy link
Contributor

@Qottmann Qottmann left a comment

Choose a reason for hiding this comment

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

Thanks for the update @anushkrishnav 💪

Looks almost good to go!
There are some missing output changes in the docstrings!

In the rydberg_interaction docstring:
image
in rydberg_drive
image
in transmon_interaction
image

We require two approving reviews before merging, another colleague will do a code-review in the following days. There may be things that I have missed and further changes necessary for them, but overall it is looking good to me 👍

@anushkrishnav
Copy link
Contributor Author

We require two approving reviews before merging, another colleague will do a code-review in the following days. There may be things that I have missed and further changes necessary for them, but overall it is looking good to me

I noticed it much later, pushing in the changes sorry about the docsting
No problem, will await the code review. Thank you for your time and guidance!

Copy link
Contributor

@lillian542 lillian542 left a comment

Choose a reason for hiding this comment

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

This is looking great, thanks! 🎉

I only have a couple of very minor comments:

There is an example in pennylane/ops/functions/dot.py that still uses the old representation in the docstring. I also see a few docstring examples in the transmon.py and rydberg.py files that show ParametrizedHamiltonian instead of HardwareHamiltonian.

Don't forget to add a change-log entry and include yourself in the list of contributors!

pennylane/pulse/rydberg.py Outdated Show resolved Hide resolved
Copy link
Contributor

@lillian542 lillian542 left a comment

Choose a reason for hiding this comment

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

Nice! Thanks @anushkrishnav 🎉

@anushkrishnav
Copy link
Contributor Author

Nice! Thanks @anushkrishnav tada

Thank you ! I will look into Hardware soon , I think I will be able to get it done too

@Qottmann Qottmann enabled auto-merge (squash) June 2, 2023 09:21
@Qottmann Qottmann merged commit 6849ff7 into PennyLaneAI:master Jun 2, 2023
43 checks passed
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.

None yet

3 participants