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

Add parameter_names and parameter_symbols protocols. #3273

Merged
merged 9 commits into from
Sep 11, 2020

Conversation

maffoo
Copy link
Contributor

@maffoo maffoo commented Aug 27, 2020

For various internal applications, we'd like to be able to determine not just whether an object is parametrized, but rather what are the actual parameters it has that must be resolved. This adds two new protocol methods in the SupportsParametrization protocol to get the parameters as strings (parameter_names) or sympy.Symbols (parameter_symbols). Parameterized classes can implement one or both of the magic methods _parameter_names_ and _parameter_symbols_; only one is needed since we can easily convert names to symbols and vice-versa.

For now I've just implemented the methods and I'd like to get feedback on the API before I fix up tests, docs, etc.

@googlebot googlebot added the cla: yes Makes googlebot stop complaining. label Aug 27, 2020
@maffoo maffoo requested a review from a team August 27, 2020 17:58
if names is not NotImplemented:
return {sympy.Symbol(name) for name in names}

return set()
Copy link
Collaborator

Choose a reason for hiding this comment

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

The code of these two functions is nearly identical. Do we really need both? At minimum I'd implement one of these in terms of the other, but maybe it's even better to leave the conversion up to the user. I imagine any client code can easily be adapted to work with either.

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'd lean toward implementing parameter_symbols in terms of parameter_names, since strings are built-in. But that would incur extra overhead in some cases, e.g. if getting symbols from a sympy expression we would convert to strings and then back to symbols. Not a big deal, but something to consider.

We have a somewhat unfortunate dichotomy between strings and symbols throughout the parametrization code where strings and symbols are in 1:1 correspondence but one or the other is more useful in different cases. I'd rather not force users who want to work with symbols to always have to convert from strings ({sympy.Symbol(n) for n in cirq.parameter_names(val)}) and vice-versa, which is why I thought to add two functions.

"""Returns a collection of sympy Symbols of parameters that require
resolution. The collection is empty iff _is_parameterized_ is False.
"""

Copy link
Collaborator

@dabacon dabacon Aug 27, 2020

Choose a reason for hiding this comment

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

This looks to dissociate parameter names and from the symbos that need to be resolved? Would a better interface be _parameters_ which returns a map from string to symbol (and None if it doesn't require resolution)?

One reason to include parameter names even if they don't currently need resolution is this could be used to automatically populate serializated gate sets names.

Also I think if you did this you could end up using a decorator on classes which could autohandle creating these methods: Those that follow the pattern of having args in init equal to attributes and all are parameters could always to 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.

The two methods are just to get parameters in the format most convenient for the calling code. Names and symbols are in 1:1 correspondence and it's easy to convert between them, e.g. symbol.name <-> sympy.Symbol(name) so I'm not sure that returning a mapping here would help all that much. Maybe if there were bi-directional maps in python since different callers might want to convert in different directions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, wait, I think I misunderstood your comment. You're imagining returning something like {'angle': sympy.Symbol('foo'), 'exponent': None} to indicate that we have "parameters" angle and exponent, and a symbol foo that needs to be resolved? I see some problems with that sort of approach.

  • For purposes of the SupportsParametrization protocol, foo is what we mean by "parameter" while angle and exponent are something else, sort of like slots in which parameters may or may not be put.
  • "Slots" like angle or exponent could actually contain expressions involving many symbols that need to be resolved, so at least we'd need to return a Mapping[str, Set[sympy.Symbol]].
  • If analyzing a composite object like a circuit, the "slots" are defined on sub-objects like gates within the circuit, so we refer to such slots without also specifying which gate at which position in the circuit; there are no slots on the circuit itself. The idea of the protocols I'm proposing is to strip away all this structure and just get a "flattened" set of parameters that need to be resolved anywhere within the composite object.

@maffoo maffoo marked this pull request as ready for review August 27, 2020 22:23
@maffoo
Copy link
Contributor Author

maffoo commented Aug 27, 2020

I changed the parameter_symbols function to delegate to parameter_names, so that there is only one magic method _parameter_names_ to implement. Also added a consistency check for the various parameter resolution protocols and added implementations of _parameter_names_ everywhere we had _is_parameterized_. The consistency check revealed a few places where we did not have a proper _resolve_parameters_ implementation, which I've now added. In addition, implementing this made clear that we sort of used is_parameterized to mean both "this object has free parameters for which values need to be supplied" and "this object has symbolic values that need to be evaluated". We should probably have a separate is_symbolic protocol for the latter, but I'll leave that alone for now.

@maffoo maffoo requested review from dabacon and viathor August 27, 2020 22:28
@maffoo
Copy link
Contributor Author

maffoo commented Sep 2, 2020

@dabacon, @viathor: PTAL

Copy link
Contributor

@balopat balopat left a comment

Choose a reason for hiding this comment

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

This looks good. I would have probably separated out the changes related to resolve_parameters (both in the protocol and in implementing classes) so that they don't end up in the same commit - but it's not the end of the world.

The number check is one bit I'd like to understand better before approving, see comments.

@@ -55,6 +66,8 @@ def is_parameterized(val: Any) -> bool:
"""
if isinstance(val, sympy.Basic):
return True
if isinstance(val, numbers.Number):
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an interesting case which probably should be handled, but I'm not sure if this is the right place. What cases were you trying to capture? I don't see it covered in the tests.

I think this should come first.

>>> isinstance(sp.sympify("1"), numbers.Number)
True
>>> isinstance(sp.sympify("1"), sp.Basic)
True
>>> isinstance(sp.sympify("a + 1"), sp.Basic)
True
>>> isinstance(sp.sympify("a + 1"), numbers.Number)
False

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved this to be the first thing checked before sympy.Basic. I don't recall exactly which failures this fixed, but some of the existing classes that implement the resolve parameters protocol did so incorrectly. This gets covered by some of the tests I've added that check for consistent implementation of the resolve parameters protocol, but I will add some more specific unit tests.

Copy link
Collaborator

Choose a reason for hiding this comment

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

That could be. I think I am the one who added this to parameter resolution so that it avoids sympy.subs which is slow as molasses. The goal was that not too much should fall through to sympy's formula resolution for performance reasons. It's totally possible I goofed and didn't implement sympy's typing correctly or in the right order.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, so putting the Number check first results in some failures because we have code that assumes that if code is not parameterized then it's possible to do floating point operations on it. For example, in TwoQubitDiagonalGate if the gate is not parameterized we try to compute the unitary (https://github.com/quantumlib/Cirq/blob/master/cirq/ops/two_qubit_diagonal_gate.py#L63), but this fails if the gate contains sympy expressions even if there are no free parameters. I guess to model this properly we'd need separate checks for "is symbolic" versus "is parameterized". For now, I'm going to leave the number checks after the sympy checks.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, added #3310 for further work.

"""
if isinstance(val, sympy.Basic):
return {symbol.name for symbol in val.free_symbols}
if isinstance(val, numbers.Number):
Copy link
Contributor

Choose a reason for hiding this comment

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

I would move this up to be first as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

except Exception:
pass
else:
print('names:', names)
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume these print statements were only for debugging, let's remove them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@balopat balopat added the automerge Tells CirqBot to sync and merge this PR. (If it's running.) label Sep 11, 2020
@CirqBot CirqBot added the front_of_queue_automerge CirqBot uses this label to indicate (and remember) what's being merged next. label Sep 11, 2020
@CirqBot CirqBot merged commit 6b1422a into master Sep 11, 2020
@CirqBot CirqBot removed the automerge Tells CirqBot to sync and merge this PR. (If it's running.) label Sep 11, 2020
@CirqBot CirqBot deleted the u/maffoo/get-params branch September 11, 2020 01:26
@CirqBot CirqBot removed the front_of_queue_automerge CirqBot uses this label to indicate (and remember) what's being merged next. label Sep 11, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes Makes googlebot stop complaining.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants