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

Custom boolean functions for supporting noise models #5674

Open
wants to merge 60 commits into
base: master
Choose a base branch
from

Conversation

obliviateandsurrender
Copy link
Contributor

@obliviateandsurrender obliviateandsurrender commented May 9, 2024

Context: We aim to have a custom boolean function that could serve as a building block for creating noise models.

Description of the Change:

  1. Generalizes the existing BooleanFn to work with arbitrary args and kwargs.
  2. Adds NoiseConditionals as a class that helps build BooleanFn required for creating noise models.
  3. Adds methods like wire/op_eq/in to enhance the building experience for the above BooleanFn.

Benefits:

Possible Drawbacks:

Related GitHub Issues: [sc-63080]

Copy link
Contributor

github-actions bot commented May 9, 2024

Hello. You may have forgotten to update the changelog!
Please edit doc/releases/changelog-dev.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.

Copy link

codecov bot commented May 9, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 99.67%. Comparing base (b7b4b75) to head (e2e7a31).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5674      +/-   ##
==========================================
- Coverage   99.68%   99.67%   -0.01%     
==========================================
  Files         415      417       +2     
  Lines       38898    38748     -150     
==========================================
- Hits        38774    38623     -151     
- Misses        124      125       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@trbromley
Copy link
Contributor

Thanks @obliviateandsurrender! Would you mind linking the corresponding SC story?

pennylane/boolean_fn.py Outdated Show resolved Hide resolved
pennylane/boolean_fn.py Outdated Show resolved Hide resolved
@obliviateandsurrender obliviateandsurrender marked this pull request as ready for review May 10, 2024 21:35
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.

doc/code/qml_noise.rst Show resolved Hide resolved
pennylane/boolean_fn.py Outdated Show resolved Hide resolved
pennylane/boolean_fn.py Outdated Show resolved Hide resolved
pennylane/boolean_fn.py Outdated Show resolved Hide resolved
.. seealso:: Users are advised to use :func:`~.wires_eq` for a functional construction.
"""

def __init__(self, wires):
Copy link
Contributor

Choose a reason for hiding this comment

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

I might be misremembering, but I thought I saw you show in the meeting the input argument of WiresEq being an operation?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh it's in the wires_in and wires_eq function below 🤔 Why do we need operation input?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is just an additional support to make the constructor functions more flexible and easy to use for the users. Especially in a scenario where they are trying to automate building noise models based on some set of operations they have in their pipeline, then they can still make these conditional without having to extract wires manually.

Comment on lines 295 to 296
ops (str, Operation, Union(list[str, Operation])): string
representation or instance of the operation.
Copy link
Contributor

Choose a reason for hiding this comment

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

Here I was expecting op_in([qml.RX, qml.Hadamard]). Is this approach challenging/not advisable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I just forgot to mention it, but doing that is possible as well! I'll add it to the docs. 😁

Comment on lines +390 to +392
>>> func = qml.noise.partial_wires(qml.RX(1.2, [12]))
>>> func(2)
qml.RX(1.2, wires=[2])
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm confused, so we originally have an instantiated RX on wire 12 but it gets mapped to wire 2?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. We had an operation that was RX(1.2, wires=12). When we wrap it with a partial_wires, we can instantiate that operation with the new input wires, in this case, 2.

>>> func = qml.noise.partial_wires(qml.RX(1.2, [12]))
>>> func(2)
qml.RX(1.2, wires=[2])
>>> func(qml.RY(1.0, ["wires"]))
Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 Do we need to support calling on operations, rather than just wires?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It makes it easier to use this method and is kinda intuitive as well since we want to initialize the wrapped operation based on the wires of an encountered operation.

Copy link
Contributor

@Jaybsoni Jaybsoni left a comment

Choose a reason for hiding this comment

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

Looking good so far. I added a few suggestions to clean up the code, along with some clarification questions. The two big questions I have:

  1. The only use of the bitwise and condition properties so far is to determine what the repr of the BooleanFunction is going to be. If they are just being used as flags, then I can suggest two alternatives that might be cleaner and more space efficient:
  • Update the base repr method to always return BooleanFn(self.fn.name), and in each of the sub classes, just overwrite the repr method directly (as you have done for the str method. You can also drop the name kwarg.

  • Update the base repr method to return BooleanFn(self.fn.name) if name is None else return name. The only time name is provided is when the sub classes are conditionals or bitwise operators. In the case that a user provides a name, we probably want to display that anyways.

  1. Regarding OpEq, it doesn't seem to me that we need it? It's just a special case of OpIn when the list of operations is length 1. Also it seems to be coded to allow passing in multiple operators both as the reference list and as the items being compared, but this doesn't make sense to me because when we use this conditional in a circuit, we are going to iterate over the gates in the circuit one at a time anyways? So the item being compared will always be a single operation. Furthermore, comparing multiple lists on both sides can be easily achieved by using the map function, so it's probably not worth it? Happy to discuss offline if I am missing something here.

Look forward to seeing this get in!

doc/code/qml_noise.rst Outdated Show resolved Hide resolved
pennylane/boolean_fn.py Outdated Show resolved Hide resolved
pennylane/noise/conditionals.py Show resolved Hide resolved
pennylane/noise/conditionals.py Show resolved Hide resolved
pennylane/noise/conditionals.py Outdated Show resolved Hide resolved
pennylane/noise/conditionals.py Show resolved Hide resolved
pennylane/noise/conditionals.py Outdated Show resolved Hide resolved
pennylane/noise/conditionals.py Outdated Show resolved Hide resolved
pennylane/noise/conditionals.py Outdated Show resolved Hide resolved
pennylane/noise/conditionals.py Outdated Show resolved Hide resolved
@codecov-commenter
Copy link

codecov-commenter commented May 24, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 99.67%. Comparing base (5b192e6) to head (b08b836).
Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5674      +/-   ##
==========================================
- Coverage   99.67%   99.67%   -0.01%     
==========================================
  Files         416      418       +2     
  Lines       38686    38543     -143     
==========================================
- Hits        38562    38418     -144     
- Misses        124      125       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@obliviateandsurrender
Copy link
Contributor Author

The only use of the bitwise and condition properties so far is to determine what the repr of the BooleanFunction is going to be.

Not really. I do use them for that, but that is not the intended purpose for them. These properties check the existence of underlying respective attributes that store expressions that BooleanFn uses for evaluation. For the repr, I do have my implementation based on your second suggestion.

Regarding OpEq, it doesn't seem to me that we need it?

Yes, it is a special case of OpIn, and similar to us having similar conditionals for wires - WiresEq and WiresIn. This conditional checks for equivalence based on type and not attributes (like parameters and wires). You may combine it with a wire conditional to include checking wires as well as wires_eq(0) & op_eq(qml.RX). Otherwise, for actually checking attributes, they can rely on usingqml.equal as I note above in one of the comments -

cond_equal = qml.BooleanFn(lambda ops, **kwargs: (lambda op: qml.equal(op, ops, **kwargs)))

Copy link
Contributor

@Jaybsoni Jaybsoni left a comment

Choose a reason for hiding this comment

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

Looks good to go 💯. A left a few comments, also would suggest adding some tests for wires_in and wires_eq comparing with operations (specifically with operations of multiple wires like CNOT or some template)

Good job!

iters = val if isinstance(val, (list, tuple, set, Wires)) else getattr(val, "wires", [val])
try:
wires = set().union(*((getattr(w, "wires", None) or Wires(w)).tolist() for w in iters))
except TypeError:
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you expect a TypeError here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TypeError happens when inputs are classes. Added an additional WireError for unhashable input w to Wires, as well.

pennylane/noise/conditionals.py Show resolved Hide resolved
pennylane/noise/conditionals.py Outdated Show resolved Hide resolved
pennylane/noise/conditionals.py Outdated Show resolved Hide resolved
Copy link
Contributor

@KetpuntoG KetpuntoG left a comment

Choose a reason for hiding this comment

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

Looks great, good job! Here are a few minor comments.
I've been also doing a lot of tricky testing and I see it's bulletproof 😎

Comment on lines +40 to +41
self._cond = set(wires)
self.condition = self._cond
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you do this instead of self.condition = set(wires)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No specific reason, just consistency with the Op classes. I essentially use cond as a private attribute for the conditionals, which I use in the computation later, whereas the condition attribute is a user-facing attribute that I don't access later on.

pennylane/noise/conditionals.py Show resolved Hide resolved
One may give an instance of :class:`Operation <pennylane.operation.Operation>`
for the ``operation`` argument:
>>> func = qml.noise.partial_wires(qml.RX(1.2, [12]))
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably not super important, but it does not work with some templates:

func = qml.noise.partial_wires(qml.Qubitization(qml.X(0), control = [1]))

It would be the same with any template that has not "wires" directly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it will work with operations (and templates) that take in "wires" directly. It will be too much effort to generalize it IMO as each corner case would be different. Moreover, if a template doesn't take wires directly, there shouldn't be a need to use this.

tests/noise/test_conditionals.py Outdated Show resolved Hide resolved
pennylane/noise/conditionals.py Show resolved Hide resolved
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

5 participants