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 49 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

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

I was not familiar with this new functionality but it looks very promising :)
A couple of comments so far

pennylane/noise/conditionals.py Outdated Show resolved Hide resolved
pennylane/noise/conditionals.py Outdated Show resolved Hide resolved
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 @obliviateandsurrender! Will look again at the docs as this PR evolves.

pennylane/noise/__init__.py Outdated 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

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

Comment on lines +23 to +34
Conditional Constructors
^^^^^^^^^^^^^^^^^^^^^^^^

.. currentmodule:: pennylane.noise.conditionals

.. autosummary::
:toctree: api

~op_eq
~op_in
~wires_eq
~wires_in
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be worth mentioning BooleanFn here, both because it is the base of all of these but more importantly because it can also be used for custom conditionals.

Comment on lines 104 to 107
@property
def bitwise(self):
"""Determine whether wrapped callable is for a bitwise operation or not."""
return bool(getattr(self, "operands", tuple()))
Copy link
Contributor

Choose a reason for hiding this comment

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

So this property is for And, Or, etc.?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep! This will help us know if the given instance is a result of a bitwise operation.

Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 any reason why not to use instance checks for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could do instance checks as well. I find this to be more generalized as it makes this property robust to any bitwise class that is added next or is user-made.

Comment on lines 109 to 112
@property
def conditional(self):
"""Determine whether wrapped callable is for a conditional or not."""
return bool(getattr(self, "condition", None))
Copy link
Contributor

Choose a reason for hiding this comment

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

And this property is for the OpEq, etc., noise-specific conditionals?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, this is for the conditionals we provide in the noise module.

Comment on lines 104 to 107
@property
def bitwise(self):
"""Determine whether wrapped callable is for a bitwise operation or not."""
return bool(getattr(self, "operands", tuple()))
Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 any reason why not to use instance checks for this?

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

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


def _get_wires(val):
Copy link
Contributor

Choose a reason for hiding this comment

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

We could alternatively use the Wires class:

  • check if val is an Operation, if so call (op.wires).to_set()
  • else call w = qml.wires.Wires(val) and then return w.to_set()

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, I did think of leveraging Wires initially. Tried to simplify it a bit by using it.

"""A ``Conditional`` for evaluating if a given wire exist in a specified set of wires

Args:
wires (Union[list[int, str], Wires]): sequence of wires for building the wire set.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should add validation for the wires argument for all of these classes? Since this is the condition, it doesn't get validated in _get_wires like everything else.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The only invalid wires in PL would be the objects which are unhashable, which will be automatically caught by the set(wires) we do during instantiation.

pennylane/noise/conditionals.py Outdated Show resolved Hide resolved
Comment on lines +356 to +360
>>> cond_func = qml.noise.op_eq(qml.RX(1.0, "dino"))
>>> cond_func(qml.RX(1.23, wires=["eve"]))
True
>>> cond_func(qml.RY(1.23, wires=["dino"]))
False
Copy link
Contributor

Choose a reason for hiding this comment

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

This feels unintuitive to me, if I as a user provide an operation qml.RX(1.0, "dino"), I would expect that first if conditional to fail. I would want the conditional to not only match the operator, but also its arguments.

We could probably support this by leveraging qml.equal since it checks for programmatic equality.

Comment on lines 172 to 190
x = [x] if not isinstance(x, (list, tuple, set)) else x

try:
return all(
(
_x in self._cops
if isclass(_x)
else (
isinstance(_x, self._cops)
if not getattr(_x, "arithmetic_depth", 0)
else any(
_check_with_lc_op(op, _x)
for op in self._cond
if not isclass(op) and getattr(op, "arithmetic_depth", 0)
)
)
)
for _x in x
)
Copy link
Contributor

Choose a reason for hiding this comment

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

This logic isn't very "readable", I don't think the few lines we save from using tuple comprehension is worth it in this case. Consider re-writing with a loop and conditionals.

if len(self._cops) == 1
else len(x) == len(self._cond)
and all(
_check_with_lc_op(_op, _x)
Copy link
Contributor

Choose a reason for hiding this comment

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

Two comments:

  • I think the only arithmetic operators we can queue right now, are Prods, and Exp. Does it make sense to be checking using the linear combinations class if those operators don't have an LC representation, while the operators that do won't ever be queued as unitaries to begin with?

  • Does this assume a certain ordering? The comparison happening here is pairwise, I worry if the operator being queued is the same operation but with the summands in a slightly different order, this check would not catch that case.

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 (fdca352) to head (d3c5fc6).
Report is 10 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5674      +/-   ##
==========================================
- Coverage   99.68%   99.67%   -0.01%     
==========================================
  Files         416      418       +2     
  Lines       39053    38538     -515     
==========================================
- Hits        38929    38413     -516     
- Misses        124      125       +1     

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

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