Page MenuHomec4science

depend.py
No OneTemporary

File Metadata

Created
Fri, Jul 26, 16:23

depend.py

"""Contains the classes that are used to define the dependency network.
Copyright (C) 2013, Joshua More and Michele Ceriotti
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http.//www.gnu.org/licenses/>.
The classes defined in this module overload the standard __get__ and __set__
routines of the numpy ndarray class and standard library object class so that
they automatically keep track of whether anything they depend on has been
altered, and so only recalculate their value when necessary.
Basic quantities that depend on nothing else can be manually altered in the
usual way, all other quantities are updated automatically and cannot be changed
directly.
The exceptions to this are synchronized properties, which are in effect
multiple basic quantities all related to each other, for example the bead and
normal mode representations of the positions and momenta. In this case any of
the representations can be set manually, and all the other representations
must keep in step.
For a more detailed discussion, see the reference manual.
Classes:
depend_base: Base depend class with the generic methods and attributes.
depend_value: Depend class for scalar objects.
depend_array: Depend class for arrays.
synchronizer: Class that holds the different objects that are related to each
other and keeps track of which property has been set manually.
dobject: An extension of the standard library object that overloads
__getattribute__ and __setattribute__, so that we can use the
standard syntax for setting and getting the depend object,
i.e. foo = value, not foo.set(value).
Functions:
dget: Gets the dependencies of a depend object.
dset: Sets the dependencies of a depend object.
depstrip: Used on a depend_array object, to access its value without
needing the depend machinery, and so much more quickly. Must not be used
if the value of the array is to be changed.
depcopy: Copies the dependencies from one object to another
deppipe: Used to make two objects be synchronized to the same value.
"""
__all__ = ['depend_base', 'depend_value', 'depend_array', 'synchronizer',
'dobject', 'dget', 'dset', 'depstrip', 'depcopy', 'deppipe']
import numpy as np
from ipi.utils.messages import verbosity, warning
class synchronizer(object):
"""Class to implement synched objects.
Holds the objects used to keep two or more objects in step with each other.
This is shared between all the synched objects.
Attributes:
synched: A dictionary containing all the synched objects, of the form
{"name": depend object}.
manual: A string containing the name of the object being manually changed.
"""
def __init__(self, deps=None):
"""Initialises synchronizer.
Args:
deps: Optional dictionary giving the synched objects of the form
{"name": depend object}.
"""
if deps is None:
self.synced = dict()
else:
self.synced = deps
self.manual = None
#TODO put some error checks in the init to make sure that the object is initialized from consistent synchro and func states
class depend_base(object):
"""Base class for dependency handling.
Builds the majority of the machinery required for the different depend
objects. Contains functions to add and remove dependencies, the tainting
mechanism by which information about which objects have been updated is
passed around the dependency network, and the manual and automatic update
functions to check that depend objects with functions are not manually
updated and that synchronized objects are kept in step with the one manually
changed.
Attributes:
_tainted: An array containing one boolean, which is True if one of the
dependencies has been changed since the last time the value was
cached.
_func: A function name giving the method of calculating the value,
if required. None otherwise.
_name: The name of the depend base object.
_synchro: A synchronizer object to deal with synched objects, if
required. None otherwise.
_dependants: A list containing all objects dependent on the self.
"""
def __init__(self, name, synchro=None, func=None, dependants=None, dependencies=None, tainted=None):
"""Initialises depend_base.
An unusual initialisation routine, as it has to be able to deal with the
depend array mechanism for returning slices as new depend arrays.
This is the reason for the penultimate if statement; it automatically
taints objects created from scratch but does nothing to slices which are
not tainted.
Also, the last if statement makes sure that if a synchronized property is
sliced, this initialization routine does not automatically set it to the
manually updated property.
Args:
name: A string giving the name of self.
tainted: An optional array containing one boolean which is True if one
of the dependencies has been changed.
func: An optional argument that can be specified either by a function
name, or for synchronized values a dictionary of the form
{"name": function name}; where "name" is one of the other
synched objects and function name is the name of a function to
get the object "name" from self.
synchro: An optional synchronizer object.
dependants: An optional list containing objects that depend on self.
dependencies: An optional list containing objects that self
depends upon.
"""
self._dependants = []
if tainted is None:
tainted = np.array([True],bool)
if dependants is None:
dependants = []
if dependencies is None:
dependencies = []
self._tainted = tainted
self._func = func
self._name = name
self.add_synchro(synchro)
for item in dependencies:
item.add_dependant(self, tainted)
self._dependants = dependants
# Don't taint self if the object is a primitive one. However, do propagate tainting to dependants if required.
if (tainted):
if self._func is None:
self.taint(taintme=False)
else:
self.taint(taintme=tainted)
def add_synchro(self, synchro=None):
""" Links depend object to a synchronizer. """
self._synchro = synchro
if not self._synchro is None and not self._name in self._synchro.synced:
self._synchro.synced[self._name] = self
self._synchro.manual = self._name
def add_dependant(self, newdep, tainted=True):
"""Adds a dependant property.
Args:
newdep: The depend object to be added to the dependency list.
tainted: A boolean that decides whether newdep should be tainted.
True by default.
"""
self._dependants.append(newdep)
if tainted:
newdep.taint(taintme=True)
def add_dependency(self, newdep, tainted=True):
"""Adds a dependency.
Args:
newdep: The depend object self now depends upon.
tainted: A boolean that decides whether self should
be tainted. True by default.
"""
newdep._dependants.append(self)
if tainted:
self.taint(taintme=True)
def taint(self,taintme=True):
"""Recursively sets tainted flag on dependent objects.
The main function dealing with the dependencies. Taints all objects
further down the dependency tree until either all objects have been
tainted, or it reaches only objects that have already been tainted. Note
that in the case of a dependency loop the initial setting of _tainted to
True prevents an infinite loop occuring.
Also, in the case of a synchro object, the manually set quantity is not
tainted, as it is assumed that synchro objects only depend on each other.
Args:
taintme: A boolean giving whether self should be tainted at the end.
True by default.
"""
self._tainted[:] = True
for item in self._dependants:
if (not item._tainted[0]):
item.taint()
if not self._synchro is None:
for v in self._synchro.synced.values():
if (not v._tainted[0]) and (not v is self):
v.taint(taintme=True)
self._tainted[:] = (taintme and (not self._name == self._synchro.manual))
else:
self._tainted[:] = taintme
def tainted(self):
"""Returns tainted flag."""
return self._tainted[0]
def update_auto(self):
"""Automatic update routine.
Updates the value when get has been called and self has been tainted.
"""
if not self._synchro is None:
if (not self._name == self._synchro.manual):
self.set(self._func[self._synchro.manual](), manual=False)
else:
warning(self._name + " probably shouldn't be tainted (synchro)", verbosity.low)
elif not self._func is None:
self.set(self._func(), manual=False)
else:
warning(self._name + " probably shouldn't be tainted (value)", verbosity.low)
def update_man(self):
"""Manual update routine.
Updates the value when the value has been manually set. Also raises an
exception if a calculated quantity has been manually set. Also starts the
tainting routine.
Raises:
NameError: If a calculated quantity has been manually set.
"""
if not self._synchro is None:
self._synchro.manual = self._name
for v in self._synchro.synced.values():
v.taint(taintme=True)
self._tainted[:] = False
elif not self._func is None:
raise NameError("Cannot set manually the value of the automatically-computed property <" + self._name + ">")
else:
self.taint(taintme=False)
def set(self, value, manual=False):
"""Dummy setting routine."""
pass
def get(self):
"""Dummy getting routine."""
pass
class depend_value(depend_base):
"""Scalar class for dependency handling.
Attributes:
_value: The value associated with self.
"""
def __init__(self, name, value=None, synchro=None, func=None, dependants=None, dependencies=None, tainted=None):
"""Initialises depend_value.
Args:
name: A string giving the name of self.
value: The value of the object. Optional.
tainted: An optional array giving the tainted flag. Default is [True].
func: An optional argument that can be specified either by a function
name, or for synchronized values a dictionary of the form
{"name": function name}; where "name" is one of the other
synched objects and function name is the name of a function to
get the object "name" from self.
synchro: An optional synchronizer object.
dependants: An optional list containing objects that depend on self.
dependencies: An optional list containing objects that self
depends upon.
"""
self._value = value
super(depend_value,self).__init__(name, synchro, func, dependants, dependencies, tainted)
def get(self):
"""Returns value, after recalculating if necessary.
Overwrites the standard method of getting value, so that value
is recalculated if tainted.
"""
if self._tainted[0]:
self.update_auto()
self.taint(taintme=False)
return self._value
def __get__(self, instance, owner):
"""Overwrites standard get function."""
return self.get()
def set(self, value, manual=True):
"""Alters value and taints dependencies.
Overwrites the standard method of setting value, so that dependent
quantities are tainted, and so we check that computed quantities are not
manually updated.
"""
self._value = value
self.taint(taintme=False)
if (manual):
self.update_man()
def __set__(self, instance, value):
"""Overwrites standard set function."""
self.set(value)
class depend_array(np.ndarray, depend_base):
"""Array class for dependency handling.
Differs from depend_value as arrays handle getting items in a different
way to scalar quantities, and as there needs to be support for slicing an
array. Initialisation is also done in a different way for ndarrays.
Attributes:
_bval: The base deparray storage space. Equal to depstrip(self) unless
self is a slice.
"""
def __new__(cls, value, name, synchro=None, func=None, dependants=None, dependencies=None, tainted=None, base=None):
"""Creates a new array from a template.
Called whenever a new instance of depend_array is created. Casts the
array base into an appropriate form before passing it to
__array_finalize__().
Args:
See __init__().
"""
obj = np.asarray(value).view(cls)
return obj
def __init__(self, value, name, synchro=None, func=None, dependants=None, dependencies=None, tainted=None, base=None):
"""Initialises depend_array.
Note that this is only called when a new array is created by an
explicit constructor.
Args:
name: A string giving the name of self.
value: The (numpy) array to serve as the memory base.
tainted: An optional array giving the tainted flag. Default is [True].
func: An optional argument that can be specified either by a function
name, or for synchronized values a dictionary of the form
{"name": function name}; where "name" is one of the other
synched objects and function name is the name of a function to
get the object "name" from self.
synchro: An optional synchronizer object.
dependants: An optional list containing objects that depend on self.
dependencies: An optional list containing objects that self
depends upon.
"""
super(depend_array,self).__init__(name, synchro, func, dependants, dependencies, tainted)
if base is None:
self._bval = value
else:
self._bval = base
def copy(self, order='C', maskna=None):
"""Wrapper for numpy copy mechanism."""
# Sets a flag and hands control to the numpy copy
self._fcopy = True
return super(depend_array,self).copy(order)
def __array_finalize__(self, obj):
"""Deals with properly creating some arrays.
In the case where a function acting on a depend array returns a ndarray,
this casts it into the correct form and gives it the
depend machinery for other methods to be able to act upon it. New
depend_arrays will next be passed to __init__ ()to be properly
initialized, but some ways of creating arrays do not call __new__() or
__init__(), so need to be initialized.
"""
depend_base.__init__(self, name="")
if type(obj) is depend_array:
# We are in a view cast or in new from template. Unfortunately
# there is no sure way to tell (or so it seems). Hence we need to
# handle special cases, and hope we are in a view cast otherwise.
if hasattr(obj,"_fcopy"):
del(obj._fcopy) # removes the "copy flag"
self._bval = depstrip(self)
else:
# Assumes we are in view cast, so copy over the attributes from the
# parent object. Typical case: when transpose is performed as a
# view.
super(depend_array,self).__init__(obj._name, obj._synchro, obj._func, obj._dependants, None, obj._tainted)
self._bval = obj._bval
else:
# Most likely we came here on the way to init.
# Just sets a defaults for safety
self._bval = depstrip(self)
def __array_prepare__(self, arr, context=None):
"""Prepare output array for ufunc.
Depending on the context we try to understand if we are doing an
in-place operation (in which case we want to keep the return value a
deparray) or we are generating a new array as a result of the ufunc.
In this case there is no way to know if dependencies should be copied,
so we strip and return a ndarray.
"""
if context is None or len(context) < 2 or not type(context[0]) is np.ufunc:
# It is not clear what we should do. If in doubt, strip dependencies.
return np.ndarray.__array_prepare__(self.view(np.ndarray),arr.view(np.ndarray),context)
elif len(context[1]) > context[0].nin and context[0].nout > 0:
# We are being called by a ufunc with a output argument, which is being
# actually used. Most likely, something like an increment,
# so we pass on a deparray
return super(depend_array,self).__array_prepare__(arr,context)
else:
# Apparently we are generating a new array.
# We have no way of knowing its
# dependencies, so we'd better return a ndarray view!
return np.ndarray.__array_prepare__(self.view(np.ndarray),arr.view(np.ndarray),context)
def __array_wrap__(self, arr, context=None):
""" Wraps up output array from ufunc.
See docstring of __array_prepare__().
"""
if context is None or len(context) < 2 or not type(context[0]) is np.ufunc:
return np.ndarray.__array_wrap__(self.view(np.ndarray),arr.view(np.ndarray),context)
elif len(context[1]) > context[0].nin and context[0].nout > 0:
return super(depend_array,self).__array_wrap__(arr,context)
else:
return np.ndarray.__array_wrap__(self.view(np.ndarray),arr.view(np.ndarray),context)
# whenever possible in compound operations just return a regular ndarray
__array_priority__ = -1.0
def reshape(self, newshape):
"""Changes the shape of the base array.
Args:
newshape: A tuple giving the desired shape of the new array.
Returns:
A depend_array with the dimensions given by newshape.
"""
return depend_array(depstrip(self).reshape(newshape), name=self._name, synchro=self._synchro, func=self._func, dependants=self._dependants, tainted=self._tainted, base=self._bval)
def flatten(self):
"""Makes the base array one dimensional.
Returns:
A flattened array.
"""
return self.reshape(self.size)
@staticmethod
def __scalarindex(index, depth=1):
"""Checks if an index points at a scalar value.
Used so that looking up one item in an array returns a scalar, whereas
looking up a slice of the array returns a new array with the same
dependencies as the original, so that changing the slice also taints
the global array.
Arguments:
index: the index to be checked.
depth: the rank of the array which is being accessed. Default value
is 1.
Returns:
A logical stating whether a __get__ instruction based
on index would return a scalar.
"""
if (np.isscalar(index) and depth <= 1):
return True
elif (isinstance(index, tuple) and len(index)==depth):
#if the index is a tuple check it does not contain slices
for i in index:
if not np.isscalar(i): return False
return True
return False
def __getitem__(self,index):
"""Returns value[index], after recalculating if necessary.
Overwrites the standard method of getting value, so that value
is recalculated if tainted. Scalar slices are returned as an ndarray,
so without depend machinery. If you need a "scalar depend" which
behaves as a slice, just create a 1x1 matrix, e.g b=a(7,1:2)
Args:
index: A slice variable giving the appropriate slice to be read.
"""
if self._tainted[0]:
self.update_auto()
self.taint(taintme=False)
if (self.__scalarindex(index, self.ndim)):
return depstrip(self)[index]
else:
return depend_array(depstrip(self)[index], name=self._name, synchro=self._synchro, func=self._func, dependants=self._dependants, tainted=self._tainted, base=self._bval)
def __getslice__(self,i,j):
"""Overwrites standard get function."""
return self.__getitem__(slice(i,j,None))
def get(self):
"""Alternative to standard get function."""
return self.__get__(slice(None,None,None))
def __get__(self, instance, owner):
"""Overwrites standard get function."""
# It is worth duplicating this code that is also used in __getitem__ as this
# is called most of the time, and we avoid creating a load of copies pointing to the same depend_array
if self._tainted[0]:
self.update_auto()
self.taint(taintme=False)
return self
def __setitem__(self,index,value,manual=True):
"""Alters value[index] and taints dependencies.
Overwrites the standard method of setting value, so that dependent
quantities are tainted, and so we check that computed quantities are not
manually updated.
Args:
index: A slice variable giving the appropriate slice to be read.
value: The new value of the slice.
manual: Optional boolean giving whether the value has been changed
manually. True by default.
"""
self.taint(taintme=False)
if manual:
depstrip(self)[index] = value
self.update_man()
elif index == slice(None,None,None):
self._bval[index] = value
else:
raise IndexError("Automatically computed arrays should span the whole parent")
def __setslice__(self,i,j,value):
"""Overwrites standard set function."""
return self.__setitem__(slice(i,j),value)
def set(self, value, manual=True):
"""Alterative to standard set function.
Args:
See __setitem__().
"""
self.__setitem__(slice(None,None),value=value,manual=manual)
def __set__(self, instance, value):
"""Overwrites standard set function."""
self.__setitem__(slice(None,None),value=value)
# np.dot and other numpy.linalg functions have the nasty habit to
# view cast to generate the output. Since we don't want to pass on
# dependencies to the result of these functions, and we can't use
# the ufunc mechanism to demote the class type to ndarray, we must
# overwrite np.dot and other similar functions.
# BEGINS NUMPY FUNCTIONS OVERRIDE
# ** np.dot
__dp_dot = np.dot
def dep_dot(da, db):
a=depstrip(da)
b=depstrip(db)
return __dp_dot(da,db)
np.dot = dep_dot
# ENDS NUMPY FUNCTIONS OVERRIDE
def dget(obj,member):
"""Takes an object and retrieves one of its attributes.
Note that this is necessary as calling it in the standard way calls the
__get__() function of member.
Args:
obj: A user defined class.
member: A string giving the name of an attribute of obj.
Exceptions:
KeyError: If member is not an attribute of obj.
Returns:
obj.member.
"""
return obj.__dict__[member]
def dset(obj,member,value,name=None):
"""Takes an object and sets one of its attributes.
Necessary for editing any depend object, and should be used for
initialising them as well, as often initialization occurs more than once,
with the second time effectively being an edit.
Args:
obj: A user defined class.
member: A string giving the name of an attribute of obj.
value: The new value of member.
name: New name of member.
Exceptions:
KeyError: If member is not an attribute of obj.
"""
obj.__dict__[member] = value
if not name is None:
obj.__dict__[member]._name = name
def depstrip(da):
"""Removes dependencies from a depend_array.
Takes a depend_array and returns its value as a ndarray, effectively
stripping the dependencies from the ndarray. This speeds up a lot of
calculations involving these arrays. Must only be used if the value of the
array is not going to be changed.
Args:
deparray: A depend_array.
Returns:
A ndarray with the same value as deparray.
"""
if isinstance(da, depend_array): # only bother to strip dependencies if the array actually IS a depend_array
#if da._tainted[0]:
# print "!!! WARNING depstrip called on tainted array WARNING !!!!!" # I think we can safely assume that when we call depstrip the array has been cleared already but I am not 100% sure so better check - and in case raise the update
return da.view(np.ndarray)
else:
return da
def deppipe(objfrom,memberfrom,objto,memberto):
"""Synchronizes two depend objects.
Takes two depend objects, and makes one of them depend on the other in such
a way that both keep the same value. Used for attributes such as temperature
that are used in many different modules, and so need different depend objects
in each, but which should all have the same value.
Args:
objfrom: An object containing memberfrom.
memberfrom: The base depend object.
objto: An object containing memberto.
memberto: The depend object that should be equal to memberfrom.
"""
dfrom = dget(objfrom,memberfrom)
dto = dget(objto,memberto)
dto._func = lambda : dfrom.get()
dto.add_dependency(dfrom)
def depcopy(objfrom,memberfrom,objto,memberto):
"""Copies the dependencies of one depend object to another.
Args:
See deppipe.
"""
dfrom = dget(objfrom,memberfrom)
dto = dget(objto,memberto)
dto._dependants = dfrom._dependants
dto._synchro = dfrom._synchro
dto.add_synchro(dfrom._synchro)
dto._tainted = dfrom._tainted
dto._func = dfrom._func
if hasattr(dfrom,"_bval"):
dto._bval = dfrom._bval
class dobject(object):
"""Class that allows standard notation to be used for depend objects."""
def __getattribute__(self, name):
"""Overwrites standard __getattribute__().
This changes the standard __getattribute__() function of any class that
subclasses dobject such that depend objects are called with their own
__get__() function rather than the standard one.
"""
value = object.__getattribute__(self, name)
if hasattr(value, '__get__'):
value = value.__get__(self, self.__class__)
return value
def __setattr__(self, name, value):
"""Overwrites standard __setattribute__().
This changes the standard __setattribute__() function of any class that
subclasses dobject such that depend objects are called with their own
__set__() function rather than the standard one.
"""
try:
obj = object.__getattribute__(self, name)
except AttributeError:
pass
else:
if hasattr(obj, '__set__'):
return obj.__set__(self, value)
return object.__setattr__(self, name, value)

Event Timeline