from .utility import getLinearlyIndependentCoeffs
from parampy import Parameters
import numpy as np
import scipy as sp
import scipy.linalg as spla
import sympy
import types
[docs]class Operator(object):
'''
Operator is the base class used by QuBricks to facilitate dynamic generation
of matrices (with partial support for n-dimensional operations when `exact` is False).
Operator objects wrap around a dictionary of "components" which are indexed
by a function of parameters. When evaluated, Operator objects evaluate the
parameters using a Parameters instance, and then add the various components
together. Operator objects support arithmetic (addition, subtraction
and multiplication); basis transformations; restriction to a subspace of the basis;
inversion (where appropriate); and the tensor (Kronecker) product.
:param components: Specification of the operator form
:type components: dict or numpy.ndarray or sympy.Matrix
:param parameters: Parameters instance
:type parameters: parampy.Parameters
:param basis: The basis in which Operator is represented
:type basis: Basis or None
:param exact: :code:`True` if Operator is to maintain an exact representation of numbers.
:type exact: bool
Operator Specifications:
The first and simplest way to construct an Operator object is to wrap an
Operator around a pre-existing numpy array or sympy Matrix.
>>> p = Parameters()
>>> a = numpy.array([[1,2,3],[4,5,6]])
>>> op = Operator(a, parameters=p)
>>> x,y,z = sympy.var('x,y,z')
>>> b = sympy.Matrix([[x,y**2,z+x],[y+z,x**2,x*y*z]])
>>> op2 = Operator(b, parameters=p)
The first example above demonstrates wrapping a static numeric matrix
into an Operator object; while the second demonstrates a conversion of an
already symbolic operator into an Operator object.
The other way to define specify the form of an Operator object is to
create a dictionary with keys of (functions of) parameters and values corresponding to
the representation of those parameters in the matrix/array. For example:
>>> d = {'x':[[1,0,0],[0,0,0]], 'sin(y)':[[0,0,0],[0,0,3]], None: [[1,1,1],[2,2,2]]}
>>> op = Operator(d, parameters=p)
The above code snippet represents the below matrix:
::
[x+1, 1 , 1 ]
[ 2 , 2 , 2 + sin(y) ]
Evaluating an Operator:
Operator objects can be evaluated to numeric matricies (whereby parameters)
are substituted in from the Parameters instance, or symbolic sympy.Matrix
objects.
Numeric evaluation looks like calling the operator:
>>> d = {'x':[[1,0,0],[0,0,0]], 'sin(y)':[[0,0,0],[0,0,3]], None: [[1,1,1],[2,2,2]]}
>>> op = Operator(d, parameters=p)
>>> op(x=2, y=0)
array([[ 3.+0.j, 1.+0.j, 1.+0.j],
[ 2.+0.j, 2.+0.j, 2.+0.j]])
.. note:: Providing parameters as shown above makes use of functionality
in the Parameters instance, where they are called parameter overrides.
Consequently, you can also supply functions of other parameters here.
You can also supply united quantities. See the Parameters documentation
for more.
Symbolic evaluation looks like:
>>> op.symbolic()
Matrix([
[x + 1, 1, 1],
[ 2, 2, 3*sin(y) + 2]])
Parameters can also be specified during symbolic evaluation:
>>> op.symbolic(y=0)
Matrix([
[x + 1, 1, 1],
[ 2, 2, 2]])
.. note:: Parameter substitution during symbolic evaluation makes use of
the `subs` function of sympy objects. It too supports substitution
with functions of parameters. See the sympy documentation for more.
It is also possible to *apply* an Operator to a vector without ever
explicitly evaluating the Operator. This may lead to faster runtimes.
See the documentation for the *apply* method.
Operator arithmetic:
Operator objects can be added, subtracted and multiplied like any
other Pythonic numeric object.
Supported operations are:
- Addition: :code:`op1+op2`
- Subtraction: :code:`op1-op2`
- Multiplication: :code:`op1*op2`
- Scaling: :code:`k*op1`, with *k* a scalar constant.
Operator indexing:
Operator objects use `numpy.ndarray` objects to internally represent the
array; and thus inherits sophisticated indexing. You can index an `Operator`
object using any indexing method supported by numpy. For example:
>>> op[1:3,2] # Will return a new `Operator` object sliced with the 2nd and third rows, with the third column
The rest of the functionality of the Operator object is described in the
method documentation below.
'''
__array_priority__ = 1000 # Cause pre-multiplication by numpy array to call __rmul__ appropriately.
def __init__(self, components, parameters=None, basis=None, exact=False):
self.__p = parameters
self.__optimised = {}
self.__shape = None
self.components = {}
self.__exact = exact
self.basis = basis
self.__add_components(components)
@property
def exact(self):
"A boolean value indicating whether the Operator should maintain exact representations of numbers."
return self.__exact
@exact.setter
def exact(self, value):
self.__exact = value
def __add_components(self, components):
'''
Import the specified components, verifying that each component has the same shape.
'''
if type(components) == dict:
for pam, component in components.items():
self.__add_component(pam, component)
else:
self.__add_component(None, components)
def __add_component(self, pam, component): # Assume components of type numpy.ndarray or sympy.Matrix
if not isinstance(component, np.ndarray):
component = np.array(component)
if component.dtype == np.dtype(object):
try:
S = np.vectorize(lambda x: complex(x))
S(component)
except:
return self.__add_components(self.__extract_components(component, pam=pam))
if self.exact:
S = np.vectorize(lambda x: sympy.S(x))
else:
S = np.vectorize(lambda x: complex(x))
component = S(component)
if self.__shape is None:
self.__shape = component.shape
elif component.shape != self.__shape:
raise ValueError("Different components of the same Operator object report different shape.")
if pam in self.components:
self.components[pam] += component
else:
self.components[pam] = component
def __extract_components(self, array, pam=None):
components = {}
if isinstance(array, np.ndarray) and array.dtype == np.dtype(object):
# Check for symbolic nested components
it = np.nditer(array, flags=['multi_index', 'refs_ok'])
while not it.finished:
e = it[0][()]
if isinstance(e, sympy.Expr):
if e.is_number:
if None not in components:
components[None] = self.__zero(array.shape)
components[None][it.multi_index] += e
else:
for coefficient, symbol in getLinearlyIndependentCoeffs(e):
if symbol is sympy.S.One:
key = None
else:
key = str(symbol)
if key not in components:
components[key] = self.__zero(array.shape)
components[key][it.multi_index] += coefficient
it.iternext()
else:
components[None] = np.array(array)
return components
def __call__(self, **params):
'''
Calling an Operator instance returns the numpy.array numeric representation
of the operator in the current basis evaluated with the parameters specified.
If a particular parameter is not specified in `params`, the current value in
in the Parameters instance is used in its stead. Parameter values can be
specified in any format that Parameters supports.
e.g. operator(pam1=2,pam3=('2','mV')), etc.
'''
return self.__assemble(params=params) # Hamiltonian evaluated with parameters as above
[docs] def symbolic(self, **params):
'''
This method returns a symbolic representation of the Operator object as
a numpy array with a dtype of `object`, as documented in the general
class documentation.
:param params: A dictionary of parameter overrides.
:type params: dict
For example:
>>> op.symbolic(x=2,y='sin(x)')
'''
return self.__assemble(symbolic=True, params=params)
[docs] def matrix(self, **params):
'''
This method returns a symbolic representation of the Operator object as
a sympy.Matrix object, as documented in the general class documentation.
:param params: A dictionary of parameter overrides.
:type params: dict
For example:
>>> op.matrix(x=2,y='sin(x)')
'''
return sympy.Matrix(self.__assemble(symbolic=True, params=params))
def __assemble(self, symbolic=False, params={}, apply_state=None, left=True):
'''
This utility function does the grunt work of compiling the various components into
the required form.
'''
R = None
S = None
if not symbolic and self.exact:
S = np.vectorize(lambda x: complex(x))
elif symbolic and not self.exact:
S = np.vectorize(lambda x: sympy.S(x))
if symbolic:
pam_eval = lambda pam: sympy.S(pam).subs(params)
else:
pam_eval = lambda pam: self.p(self.__optimise(pam), **params)
apply_op = None
if apply_state is not None:
apply_op = lambda x: x.dot(self.__np(apply_state)) if left else self.__np(apply_state).dot(x)
for pam, component in self.components.items():
if S is not None:
component = S(component)
if apply_op is not None:
component = apply_op(component)
if R is None:
R = self.__zero(exact=symbolic, shape=component.shape)
if pam is None:
R += component
else:
R += pam_eval(pam) * component
return R
[docs] def apply(self, state, symbolic=False, left=True, params={}):
'''
This method returns the object resulting from multiplication by this Operator
without ever fully constructing the Operator; making it potentially a little
faster. When using `apply` on symbolic arrays, be sure to set `symbolic` to `True`.
:param state: An array with suitable dimensions for being pre- (or post-, if left is False) multipled by the Operator represented by this object.
:type state: numpy.array or object
:param symbolic: :code:`True` if multiplication should be done symbolically using sympy, and :code:`False` otherwise (uses numpy).
:type symbolic: bool
:param left: :code:`True` if the state should be multiplied from the left; and :code:`False` otherwise.
:type left: bool
:param params: A dictionary of parameter overrides.
:type params: dict
For example:
>>> op = Operator([[1,2],[3,4]])
>>> op.apply([1,2])
array([ 5.+0.j, 11.+0.j])
'''
return self.__assemble(symbolic=symbolic, apply_state=state, left=left, params=params)
def __repr__(self):
return "<Operator with shape %s>" % (str(self.shape))
@property
def shape(self):
"A tuple representing the dimensions of the Operator."
if self.__shape is None:
return ()
return self.__shape
############# Parameters helper methods ################################################
@property
def p(self):
'''
A reference to the internal Parameter object.
'''
if self.__p is None:
raise ValueError("Operator requires use of Parameters object; but none specified. You can add one using: Operator.p = parametersInstance.")
if type(self.__p) in (types.FunctionType, types.MethodType):
return self.__p()
return self.__p
@p.setter
def p(self, parameters):
if not (isinstance(parameters, Parameters) or isinstance(parameters, (types.FunctionType, types.MethodType)) and (isinstance(parameters(), Parameters) or parameters() is None)):
raise ValueError("You must provide a Parameters instance or a function with no arguments that returns a Parameters instance.")
self.__p = parameters
return self
def __optimise(self, pam):
'''
This helper generates a local cache of the optimised function returned by the Parameters
instance.
'''
if pam not in self.__optimised:
self.__optimised[pam] = self.p.optimise(pam)
return self.__optimised[pam]
############# Basis helper methods #######################################################
@property
def basis(self):
"A reference to the current Basis of the Operator; or None if it has no specified Basis."
return self.__basis
@basis.setter
def basis(self, basis):
'''
Sets the current basis of the Operator object. Note that this does NOT transform the
Operator into this basis; but simply identifies the current form the Operator as being
written in the specified basis. To transform basis, use Operator.change_basis .
'''
self.__basis = basis
[docs] def change_basis(self, basis=None, threshold=False, params={}):
'''
This method returns a copy of this Operator object expressed in the new basis. The threshold
parameter allows elements in the resulting Operator which differ from zero
only by numerical noise to be set to zero. For more information, refer
to the documentation for Basis.transform.
:param basis: The basis in which to represent this Operator.
:type basis: Basis or None
:param threshold: A boolean indicating whether to threshold to minimise numerical error, or a float indicating the threshold level.
:type threshold: bool or float
:param params: Parameter overrides to use during the basis transformation.
:type params: dict
:returns: A reference to the transformed Operator.
'''
if basis == self.__basis:
return self
elif basis is None and self.__basis is not None:
O = self.__basis.transform_to(self, basis=None, threshold=threshold, params=params)
elif basis is not None and self.__basis is None:
O = basis.transform_from(self, basis=None, threshold=threshold, params=params)
else:
O = basis.transform_from(self, basis=self.__basis, threshold=threshold, params=params)
O.basis = basis
return O
[docs] def connected(self, *indices, **params):
'''
This method returns a set of indices that represents the rows/columns that mix
with the specified indices if this operator were to be multiplied by itself. This method
requires that the Operator object be square.
:param indices: A sequence of zero-indexed indices to test for connectedness.
:type indices: tuple
:param params: A parameter override context.
:type params: dict
For example:
>>> op.connected(1,2,3,x=12,y=(3,'ms'))
{1,2,3}
The above output would suggest that in the context of x and y set as indicated,
the specified subspace is closed under repeated self-multiplication of the Operator.
'''
if len(self.shape) != 2 or self.shape[0] != self.shape[1]:
raise ValueError("Operator not square. Connectedness only works when Operators are square. %s" % self.components)
new = set(indices)
for key, component in self.components.items():
if key is None or not (not self.exact and self.p.is_resolvable(key, **params) and self.p(key, **params) == 0):
for index in indices:
new.update(np.where(np.logical_or(component[:, index] != 0, component[index, :] != 0))[0].tolist())
if len(new.difference(indices)) != 0:
new.update(self.connected(*new, **params))
return new
[docs] def restrict(self, *indices):
'''
This method returns a copy of the Operator restricted to the basis elements specified
as row/column indices. This method requires that the shape of the Operator is square.
:param indices: A sequence of zero-indexed indices which correspond to the rows/columns to keep.
:type indices: tuple
For example:
>>> op = Operator([[1,2,3],[4,5,6],[7,8,9]]).restrict(1,2)
>>> op()
array([[ 5.+0.j, 6.+0.j],
[ 8.+0.j, 9.+0.j]])
'''
if len(self.shape) != 2 or self.shape[0] != self.shape[1]:
raise ValueError("Operator not square. Restriction only works when Operators are square.")
indices = np.array(indices)
components = {}
for pam, component in self.components.items():
components[pam] = component[indices[:,None], indices]
return self._new(components)
############### Other utility methods #############################################
def __np(self, array, exact=None):
'''
A utility function to convert any component to a numpy array.
'''
exact = exact if exact is not None else self.exact
dtype = object if exact else complex
if type(array) == np.ndarray:
return array.astype(dtype)
if isinstance(array, sympy.MatrixBase):
return np.array(array.tolist(), dtype=dtype)
if isinstance(array, (tuple, list)):
return np.array(array, dtype=dtype)
raise ValueError("Unknown conversion from %s to numpy array." % type(array))
[docs] def clean(self, threshold):
'''
This method zeroes out all elements of the components which
are different from zero only by a magnitude less than `threshold`. One must
use this function with caution, as it does not take into account the value
of the parameter multiplying the matrix form.
:param threshold: A threshold value.
:param type: float
'''
for key in self.components:
ind = np.where(np.abs(self.components[key]) < threshold)
for k in xrange(len(ind[0])):
self.components[key][(ind[0][k], ind[1][k])] = 0
return self
########## Define Basic Arithmetic ################################################
def _new(self, components={}):
return Operator(components, parameters=self.__p, basis=self.__basis, exact=self.__exact)
def _copy(self):
return Operator(self.components, parameters=self.__p, basis=self.__basis, exact=self.__exact)
def __get_other_operator(self, other):
if self.basis == other.basis or other.basis == "*" or self.basis == '*':
return other
else:
return other.change_basis(self.basis)
def __zero(self, shape=None, exact=None):
exact = exact if exact is not None else self.exact
shape = shape if shape is not None else self.shape
return np.zeros(shape, dtype=object) if exact else np.zeros(shape, dtype=complex)
def __add__(self, other):
O = self._copy()
if not isinstance(other, Operator):
other = self._new(other)
for pam, component in self.__get_other_operator(other).components.items():
O.components[pam] = O.components.get(pam, self.__zero()) + component
return O
def __radd__(self, other):
return self.__add__(other)
def __sub__(self, other):
O = self._copy()
if not isinstance(other, Operator):
other = self._new(other)
for pam, component in self.__get_other_operator(other).components.items():
O.components[pam] = O.components.get(pam, self.__zero()) - component
return O
def __rsub__(self, other):
other = self._new(other)
return other - self
def __dot(self, one, two):
if type(one) == type(two) and type(one) == np.ndarray:
return np.dot(one, two)
return one * two
def __mul__(self, other):
components = {}
if isinstance(other, (np.ndarray, sympy.MatrixBase)):
other = self._new(other)
if isinstance(other, Operator):
for pam, component in self.components.items():
for pam_other, component_other in self.__get_other_operator(other).components.items():
pam = pam if pam is not None else 1
pam_other = pam_other if pam_other is not None else 1
mpam = str(sympy.S(pam) * sympy.S(pam_other))
r = self.__dot(component, component_other)
if mpam not in components:
components[mpam] = self.__zero(shape=r.shape, exact=self.exact or other.exact)
components[mpam] += r
elif isinstance(other, (str, sympy.expr.Expr)):
for pam, component in self.components.items():
components[str(sympy.S(other) * sympy.S(pam))] = component
elif isinstance(other, (int, float, complex, long)):
for pam, component in self.components.items():
components[pam] = other * component
else:
raise ValueError("Operator cannot be multiplied by type: %s" % type(other))
return self._new(components)
def __rmul__(self, other):
components = {}
if isinstance(other, (np.ndarray, sympy.MatrixBase)):
other = self._new(other)
return other * self
else:
return self * other
return self._new(components)
def __div__(self, other):
components = {}
if isinstance(other, (str, sympy.expr.Expr)):
for pam, component in self.components.items():
components[str(sympy.S(pam)/sympy.S(other))] = component
elif isinstance(other, (int, float, complex, long)):
for pam, component in self.components.items():
components[pam] = component/other
return self._new(components)
# TODO: Implement tensor products
# def tensor(self, other):
# '''
# This method returns a new Operator object that is the component-wise tensor
# (or Kronecker) product of this Operator with *other*.
#
# :param other: Another Operator with which to perform the tensor product
# :type other: Operator
# '''
# NotImplementedError("Tensor product calculations have yet to be implemented.")
[docs] def block_diag(self, other):
'''
This method returns a new Operator object with the `other` Operator appended
as a diagonal block below this Operator.
:param other: Another Operator to add as a diagonal block.
:type other: Operator
'''
components = {}
if not isinstance(other, Operator):
other = self._new(other)
for pam, component in self.components.items():
components[pam] = spla.block_diag(component, self.__zero(other.shape))
for pam, component in self.__get_other_operator(other).components.items():
if pam not in components:
components[pam] = self.__zero(np.array(self.shape) + np.array(component.shape))
components[pam] += spla.block_diag(sp.zeros(self.shape), component)
return self._new(components)
[docs] def inverse(self):
'''
This method computes and returns the pseudo-inverse of the Operator object. This may be very slow. If you do not need a
symbolic inversion, then simply numerically evaluate the Operator object and take a numerical inverse using
numpy.
.. note:: The pseudo-inverse will equal the normal inverse if it exists.
'''
if len(self.shape) != 2:
raise ValueError("Cannot perform inverse on Operators with dimension other than 2.")
return self._new(self.matrix().pinv())
[docs] def collapse(self, *wrt, **params):
'''
This method returns a new Operator object that is a simplification of this one.
It collapses and simplifies this Operator object by assuming certain parameters are going
to be fixed and non-varying. As many parameters as possible are collapsed into the constant component
of the operator. All other entries are analytically simplified as much as possible.
If no parameters are specified, then only the simplification is performed. A full collapse
to a numerical matrix should be achieved by evaluating it numerically, as described in the class description.
.. note:: An optimised cache of parameters (and parameter expressions) is also made
using `Parameters.optimise`. These optimised parameter extraction methods also
assume that the parameters will no longer change. If the do end up changing, you
might witness some odd results.
:param wrt: A sequence of parameter names.
:type wrt: tuple
:param params: Parameter overrides.
:type params: dict
For example:
>>> op.collapse('x',y=1)
This will lead to a new Operator being formed which is numeric except for terms involving
`x`, when `y` is set to `1`.
'''
components = {}
def add_comp(key, contrib):
if key not in components:
components[key] = self.__zero()
components[key] += contrib
for pam, component in self.components.items():
if pam is None:
add_comp(None, component)
else:
new_pam = 0
for (coeff, indet) in getLinearlyIndependentCoeffs(sympy.S(pam)):
if len(wrt) > 0 and self.p.is_constant(str(indet), *wrt, **params):
add_comp(None, coeff * self.p(indet, **params) * component)
else:
subs = params.copy()
if len(wrt) > 0:
for s in indet.free_symbols:
if self.p.is_constant(str(s), *wrt, **params):
subs[s] = self.p(str(s), **params)
coeff2, indet2 = getLinearlyIndependentCoeffs(indet.subs(subs))[0]
if isinstance(indet2, sympy.Number):
add_comp(None, coeff * coeff2 * component)
else:
new_pam += coeff * coeff2 * indet2
if new_pam != 0:
add_comp(str(new_pam.simplify()), component)
new_op = self._new(components)
# Construct a cache which respects the parameter constancy indicated when calling this method
for pam in components.keys():
new_op._Operator__optimised[pam] = self.p.optimise(pam, *wrt, **params)
return new_op
# Support Indexing
def __getitem__(self, index):
components = {}
for arg, form in self.components.items():
components[arg] = form[index]
return self._new(components)
[docs]class OrthogonalOperator(Operator):
'''
OrthogonalOperator is a subclass of Operator for representing Orthogonal matrices.
The only difference is that the inversion process is simplified,
using the result that the inverse of Q is simply its transpose.
Apart from the inversion operation, all other operations will result in a
normal Operator object being returned.
'''
[docs] def inverse(self): # Orthogonal Operators are orthogonal matrices. Thus Q^-1 = Q^T
'''
:returns: An OrthogonalOperator object which is the inverse of this one.
This method computes the inverse of the Operator object. This may be very slow. If you do not need a
symbolic inversion, then simply call the Operator object and take a numerical inverse using
numpy.
'''
components = {}
for key, c in self.components.items():
components[key] = c.transpose()
return OrthogonalOperator(components, parameters=self.p, basis=self.basis, exact=self.exact)
[docs]class OperatorSet(object):
'''
OperatorSet objects a container for multiple Operator objects, such that one
can construct different combinations of the elements of the OperatorSet at
runtime. This is useful, for example, if you have a Hamiltonian with various
different couplings, and you want to consider various combinations of them.
:param components: A dictionary of Operator objects, indexed by a string representing
the name of the corresponding Operator object.
:type components: dict of Operator instances or None
:param defaults: A list of component names which should be compiled into an operator when
a custom list is not supplied. If defaults is None, then all components are
used.
:type: list of str or None
Creating an OperatorSet instance:
>>> ops = OperatorSet ( components = { 'op1': Operator(...),
'op2': Operator(...) } )
Extracting Operator instances:
Usually, one wants to call OperatorSet objects, with a list of keys to be compiled
into a single Operator object. e.g. operatorset('name1','name2',...) . For
example:
>>> ops('op1') # This is just op1 on its own
>>> ops('op1','op2') # This is the sum of op1 and op2
>>> ops() # Same as above, since no defaults provided.
Individual components can also be accessed using: :code:`operatorset['name']`.
Subclassing OperatorSet:
To subclass `OperatorSet` simply override the `init` method to construct whichever
`Operators` you desire. You should initialise `OperatorSet.components` to be a dictionary;
but otherwise, there are no constraints. Note that you can add elements to an OperatorSet
without subclassing it.
'''
def __init__(self, components=None, defaults=None, **kwargs):
self.components = components
self.defaults = defaults
if self.components is None:
self.components = {}
self.init(**kwargs)
[docs] def init(self):
'''
Subclasses may use this hook to initialise themselves. It is called after
`OperatorSet.components` and `OperatorSet.defaults` are set to their
passed values, with `Operator.components` guaranteed to be a dictionary.
'''
pass
def __call__(self, *components):
'''
Calling the OperatorSet object returns an Operator object which is the sum of
all Operator objects referred to in the `components` list. If no components are
supplied, the OperatorSet.defaults property is used instead.
e.g. operatorset('object_name_1','object_name_2')
'''
return self.__assemble(components)
def __assemble(self, components):
'''
This internal method does sums the relevant components into an Operator object,
which it then returns.
'''
if len(components) == 0:
if self.defaults is not None:
components = self.defaults
else:
components = self.components.keys()
cs = []
if len(components) == 0:
raise ValueError("Attempted to construct an empty Operator.")
for component in components:
cs.append(self[component])
return self.__sum(cs)
[docs] def apply(self, state, symbolic=False, left=True, params=None, components=None):
'''
This method applies the Operator.apply method from each of the stored Operators, and
sums up their results. The only difference from the Operator.apply method
is the `components` keyword, which allows you to specify which operators are used. If
`components` is not specified, then the default components are used.
:param state: An array with suitable dimensions for being pre- (or post-, if left is False) multipled by the Operator represented by this object.
:type state: numpy.array or object
:param symbolic: :code:`True` if multiplication should be done symbolically using sympy, and :code:`False` otherwise (uses numpy).
:type symbolic: bool
:param left: :code:`True` if the state should be multiplied from the left; and :code:`False` otherwise.
:type left: bool
:param params: A dictionary of parameter overrides.
:type params: dict
:param components: A list of components to use.
:type components: iterable
'''
if components is None or len(components) == 0:
if self.defaults is not None:
components = self.defaults
else:
components = self.components.keys()
rs = []
if len(components) == 0:
raise ValueError("Attempted to apply an empty Operator.")
for component in components:
rs.append(self[component].apply(state, symbolic=symbolic, left=left, params=params))
return self.__sum(rs)
def __sum(self, operators):
'''
A utility function to sum operators.
'''
s = None
for operator in operators:
if s is None:
s = operator
else:
s += operator
return s
def __getitem__(self, key):
try:
return self.components[key]
except:
raise ValueError("Invalid operator component: '%s'" % key)
def __setitem__(self, key, value):
if not isinstance(value, Operator):
try:
value = Operator(value)
except:
raise ValueError("Set value is not an Operator instance, nor can it be transformed into one. Received: %s" % value)
self.components[key] = value