Source code for strawberryfields.parameters
# Copyright 2019 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
The classes in this module represent parameters passed to the
quantum operations represented by :class:`~.Operation` subclasses.
Parameter types
---------------
There are three basic types of parameters:
1. **Numerical parameters** (bound and fixed): An immediate, immutable numerical object
(float, complex, int, numerical array).
Implemented as-is, not encapsulated in a class.
2. **Measured parameters** (bound but not fixed): Certain quantum circuits/protocols require that
Operations can be conditioned on measurement results obtained during the execution of the
circuit. In this case the parameter value is not known/fixed until the measurement is made
(or simulated). Represented by :class:`MeasuredParameter` instances.
Constructed from the :class:`.RegRef` instance storing the measurement
result using the :meth:`.RegRef.par` method.
3. **Free parameters** (not bound nor fixed): A *parametrized circuit template* is a circuit that
depends on a number of unbound (free) parameters. These parameters need to be bound to fixed
numerical values before the circuit can be executed on a hardware quantum device or a numeric
simulator. Represented by :class:`FreeParameter` instances.
Simulators with symbolic capability can accept a parametrized circuit as input (and should
return symbolic expressions representing the measurement results, with the same free parameters,
as output).
Free parameters belong to a single :class:`.Program` instance, are constructed using the
:meth:`.Program.params` method, and are bound using :meth:`.Program.bind_params`.
:class:`.Operation` subclass constructors accept parameters that are functions or algebraic
combinations of any number of these basic parameter types. This is made possible by
:class:`MeasuredParameter` and :class:`FreeParameter` inheriting from :class:`sympy.Symbol`.
.. note:: Binary arithmetic operations between sympy symbols and numpy arrays produces numpy object arrays containing sympy symbols.
Operation lifecycle
-------------------
The normal lifecycle of an Operation object and its associated parameters is as follows:
* An Operation instance is constructed, and given some input arguments.
In :meth:`.Operation.__init__`,
the RegRef dependencies of measured parameters are added to :attr:`.Operation._measurement_deps`.
* The Operation instance is applied using its :meth:`~ops.Operation.__or__`
method inside a :class:`.Program` context.
This creates a :class:`.Command` instance that wraps
the Operation and the RegRefs it acts on, which is appended to :attr:`.Program.circuit`.
* Before the Program is run, it is compiled and optimized for a specific backend. This involves
checking that the Program only contains valid Operations, decomposing non-elementary Operations
using :meth:`~ops.Operation.decompose`, and finally merging and commuting Commands inside
the graph representing the quantum circuit.
The circuit graph is built using the knowledge of which subsystems the Commands act and depend on.
* Decompositions, merges, and commutations often involve the creation of new Operations with algebraically
transformed parameters.
For example, merging two :class:`.Gate` instances of the same subclass involves
adding their first parameters after equality-comparing the others. This is easily done if
all the parameters have an immediate numerical value.
Measured and free parameters are handled symbolically by Sympy.
* The compiled Program is run by a :class:`.BaseEngine` instance, which calls the
:meth:`~ops.Operation.apply` method of each Operation in turn.
* :meth:`~ops.Operation.apply` then calls :meth:`~ops.Operation._apply` which is redefined by each Operation subclass.
It evaluates the value of the parameters using :func:`par_evaluate`, and
may perform additional numeric transformations on them.
The parameter values are finally passed to the appropriate backend API method.
It is up to the backend to either accept NumPy arrays and TensorFlow objects as parameters, or not.
What we cannot do at the moment:
* Use anything except integers and RegRefs (or Sequences thereof) as the subsystem argument
for the :meth:`~ops.Operation.__or__` method.
Technically we could allow any parameters that evaluate into an integer.
"""
# pylint: disable=too-many-ancestors,unused-argument,protected-access,import-outside-toplevel
import collections.abc
import functools
import types
import linecache
import blackbird
import numpy as np
import sympy
import sympy.functions as sf
[docs]def wrap_mathfunc(func):
"""Applies the wrapped sympy function elementwise to NumPy arrays.
Required because the sympy math functions cannot deal with NumPy arrays.
We implement no broadcasting; if the first argument is a NumPy array, we assume
all the arguments are arrays of the same shape.
"""
@functools.wraps(func)
def wrapper(*args):
temp = [isinstance(k, np.ndarray) for k in args]
if any(temp):
if not all(temp):
raise ValueError(
"Parameter functions with array arguments: all the arguments must be arrays of the same shape."
)
for k in args[1:]:
if len(k) != len(args[0]):
raise ValueError(
"Parameter functions with array arguments: all the arguments must be arrays of the same shape."
)
# apply func elementwise, recursively, on the args
return np.array([wrapper(*k) for k in zip(*args)])
return func(*args)
return wrapper
par_funcs = types.SimpleNamespace(
**{name: wrap_mathfunc(getattr(sf, name)) for name in dir(sf) if name[0] != "_"}
)
"""SimpleNamespace: Namespace of mathematical functions for manipulating Parameters.
Consists of all :mod:`sympy.functions` public members, which we wrap with :func:`wrap_mathfunc`.
"""
[docs]class ParameterError(RuntimeError):
"""Exception raised when the Parameter classes encounter an illegal operation.
E.g., trying to use a measurement result before it is available.
"""
[docs]def is_object_array(p):
"""Returns True iff p is an object array.
Args:
p (Any): object to be checked
Returns:
bool: True iff p is a NumPy object array
"""
return isinstance(p, np.ndarray) and p.dtype == object
[docs]def par_evaluate(params, dtype=None):
"""Evaluate an Operation parameter sequence.
Any parameters descending from :class:`sympy.Basic` are evaluated, others are returned as-is.
Evaluation means that free and measured parameters are replaced by their numeric values.
NumPy object arrays are evaluated elementwise.
Alternatively, evaluates a single parameter and returns its value.
Args:
params (Sequence[Any]): parameters to evaluate
dtype (None, np.dtype, tf.dtype): NumPy or TensorFlow datatype to optionally cast atomic symbols
to *before* evaluating the parameter expression. Note that if the atom
is a TensorFlow tensor, a NumPy datatype can still be passed; ``tensorflow.dtype.as_dtype()``
is used to determine the corresponding TensorFlow dtype internally.
Returns:
list[Any]: evaluated parameters
"""
scalar = False
if not isinstance(params, collections.abc.Sequence):
scalar = True
params = [params]
def do_evaluate(p):
"""Evaluates a single parameter."""
if is_object_array(p):
return np.array([do_evaluate(k) for k in p])
if not par_is_symbolic(p):
return p
# using lambdify we can also substitute np.ndarrays and tf.Tensors for the atoms
atoms = list(p.atoms(MeasuredParameter, FreeParameter))
if not atoms:
# If there are not atomic values, we just convert to elementary
# Python types
return float(p) if p.is_real else complex(p)
# evaluate the atoms of the expression
vals = [k._eval_evalf(None) for k in atoms]
# use the tensorflow printer if any of the symbolic parameter values are TF objects
# (we do it like this to avoid importing tensorflow if it's not needed)
is_tf = (type(v).__module__.startswith("tensorflow") for v in vals)
printer = "tensorflow" if any(is_tf) else "numpy"
func = sympy.lambdify(atoms, p, printer)
# sympy.lambdify caches data using linecache, if called many times this
# can make up for a lot of memory used. We clear the cache here to
# avoid that.
linecache.clearcache()
if dtype is not None:
# cast the input values
if printer == "tensorflow":
import tensorflow as tf
tfdtype = tf.as_dtype(dtype)
vals = [tf.cast(v, dtype=tfdtype) for v in vals]
else:
vals = [dtype(v) for v in vals]
return func(*vals)
ret = list(map(do_evaluate, params))
if scalar:
return ret[0]
return ret
[docs]def par_is_symbolic(p):
"""Returns True iff p is a symbolic Operation parameter instance.
If a parameter inherits :class:`sympy.Basic` it is symbolic.
A NumPy object array is symbolic if any of its elements are.
All other objects are considered not symbolic parameters.
Note that :data:`strawberryfields.math` functions applied to numerical (non-symbolic) parameters return
symbolic parameters.
"""
if is_object_array(p):
return any(par_is_symbolic(k) for k in p)
return isinstance(p, sympy.Basic)
[docs]def par_convert(args, prog):
"""Convert Blackbird symbolic Operation arguments into their SF counterparts.
Args:
args (Iterable[Any]): Operation arguments
prog (Program): program containing the Operations.
Returns:
list[Any]: converted arguments
"""
def do_convert(a):
if isinstance(a, blackbird.RegRefTransform):
a = a.expr
if isinstance(a, sympy.Basic):
# substitute SF symbolic parameter objects for Blackbird ones
s = {}
for k in a.atoms(sympy.Symbol):
if k.name[0] == "q":
s[k] = MeasuredParameter(prog.register[int(k.name[1:])])
else:
s[k] = prog.params(k.name) # free parameter
return a.subs(s)
return a # return non-symbols as-is
return [do_convert(a) for a in args]
[docs]def par_regref_deps(p):
"""RegRef dependencies of an Operation parameter.
Returns the RegRefs that the parameter depends on through the :class:`MeasuredParameter`
atoms it contains.
Args:
p (Any): Operation parameter
Returns:
set[RegRef]: RegRefs the parameter depends on
"""
ret = set()
if is_object_array(p):
# p is an object array, possibly containing symbols
for k in p:
ret.update(par_regref_deps(k))
elif isinstance(p, sympy.Basic):
# p is a Sympy expression, possibly containing measured parameters
for k in p.atoms(MeasuredParameter):
ret.add(k.regref)
return ret
[docs]def par_str(p):
"""String representation of the Operation parameter.
Args:
p (Any): Operation parameter
Returns:
str: string representation
"""
if isinstance(p, np.ndarray):
np.set_printoptions(precision=4)
return str(p)
if par_is_symbolic(p):
return str(p)
return "{:.4g}".format(p) # scalar parameters
[docs]class MeasuredParameter(sympy.Symbol):
"""Single measurement result used as an Operation parameter.
A MeasuredParameter instance, given as a parameter to a
:class:`~strawberryfields.ops.Operation` constructor, represents
a dependence of the Operation on classical information obtained by
measuring a subsystem of the register.
Used for deferred measurements, i.e., using a measurement's value
symbolically in defining a gate before the numeric value of that
measurement is available.
Former RegRefTransform (SF <= 0.11) functionality is provided by the sympy.Symbol base class.
Args:
regref (RegRef): register reference responsible for storing the measurement result
"""
def __new__(cls, regref):
# sympy.Basic.__new__ wants a name, other arguments must not end up in self._args
return super().__new__(cls, "q" + str(regref.ind))
def __init__(self, regref):
if not regref.active:
raise ValueError("Trying to use an inactive RegRef.")
#: RegRef: the value of the parameter depends on this RegRef, and can only be evaluated after the corresponding subsystem has been measured
self.regref = regref
def _sympystr(self, printer):
"""Blackbird notation.
The Sympy printing system uses this method instead of __str__.
"""
return "q{}".format(self.regref.ind)
def _eval_evalf(self, prec):
"""Returns the numeric result of the measurement if it is available.
Returns:
Any: measurement result
Raises:
ParameterError: iff the parameter has not been measured yet
"""
res = self.regref.val
if res is None:
raise ParameterError(
"{}: trying to use a nonexistent measurement result (e.g., before it has been measured).".format(
self
)
)
# remove unnecessary dims when returning measurement
try:
res = np.squeeze(res).item()
except ValueError:
res = np.squeeze(res)
return res
[docs]class FreeParameter(sympy.Symbol):
"""Named symbolic Operation parameter.
Args:
name (str): name of the free parameter
"""
def __init__(self, name):
#: str: name of the free parameter
self.name = name
#: Any: value of the parameter, None means unbound
self.val = None
#: Any: default value of the parameter, used if unbound
self.default = None
def _sympystr(self, printer):
"""Blackbird notation.
The Sympy printing system uses this method instead of __str__.
"""
return "{{{}}}".format(self.name)
def _eval_evalf(self, prec):
"""Returns the value of the parameter if it has been bound, or the default value if not.
Returns:
Any: bound value, or the default value if not bound
Raises:
ParameterError: iff the parameter has not been bound, and has no default value
"""
if self.val is None:
if self.default is None:
raise ParameterError("{}: unbound parameter with no default value.".format(self))
return self.default
return self.val
_modules/strawberryfields/parameters
Download Python script
Download Notebook
View on GitHub