Ejemplo n.º 1
0
class Displacement(object):
    """ Encapsulates a displacement from a base molecule.

    :Attributes:

    base_molecule : `Molecule`
        The Molecule object that the displacement is relative to.
    representation : `Representation`
        The Representation object that the displacements are values in.
    disp_vect : `Vector`
        The displacement amounts in the given representation. (e.g. `disp_vect[i]` corresponds to the
        amount of displacement of the `i`th `Coordinate` in `representation`)

    """

    ####################
    # Class Attributes #
    ####################

    tolerance = 1e-12
    max_iterations = 35

    ##############
    # Attributes #
    ##############

    base_molecule = ReadOnlyAttribute('base_molecule',
        doc="""
        The Molecule object that the displacement is relative to.
        """)
    representation = None
    disp_vect = None

    ######################
    # Private Attributes #
    ######################

    _displaced_molecule = None
    _displaced_representation = None
    _increments = None
    _deltas = None

    ##################
    # Initialization #
    ##################

    @typechecked(
        representation='Representation',
        disps=IterableOf(Number),
        increments=(None, IterableOf(Number)),
        deltas=(None, IterableOf(float))
    )
    def __init__(self, representation, disps, increments=None, deltas=None):
        base = representation.molecule
        self._base_molecule = base
        self.representation = representation

        self.disp_vect = []
        for d in disps:
            self.disp_vect.append(d)
            # TODO Decide if I should handle units here or in displaced_by (probably there is better)
            #if hasunits(d):
            #    if isinstance(representation, InternalRepresentation):
            #        units = representation.units[d.units.genre]
            #    elif isinstance(representation, CartesianRepresentation):
            #        units = representation.units
            #    else:
            #        raise NotImplementedError
            #    self.disp_vect.append(strip_units(d, ) if hasunits(d) else d)
            #else:
            #    self.disp_vect.append(strip_units(d))
        self.disp_vect = Vector(self.disp_vect)
        self._increments = increments
        self._deltas = []
        # TODO Decide if I should handle units here or in displaced_by (probably there is better)
        #if deltas is not None:
        #    for d in deltas:
        #        self._deltas.append(strip_units(d, representation.units[d.units.genre]) if hasunits(d) else d)

    ##############
    # Properties #
    ##############

    @property
    def displaced_molecule(self):
        """ The displaced molecule object resulting from applying `self` to `Molecule`
        """
        if self._displaced_molecule is None:
            self._compute_displacement()
        return self._displaced_molecule

    @property
    def displaced_representation(self):
        """ The displaced molecule object resulting from applying `self` to `Molecule`
        """
        if self._displaced_representation is None:
            self._compute_displacement()
        return self._displaced_representation

    @CachedProperty
    def desired_values(self):
        return self.representation.values + self.disp_vect

    #################
    # Class Methods #
    #################

    @classmethod
    def get_default_deltas(cls, rep):
        if type_checking_enabled:
            if not isinstance(rep, Representation):
                raise TypeError
        del_list = []
        for coord in rep:
            #if coord.default_delta.units.genre is AngularUnits:
            #    del_list.append(coord.default_delta.in_units(Radians))
            #else:
            del_list.append(coord.default_delta)
        return del_list

    @classmethod
    def from_increments(cls, increments, rep, deltas=None):
        if deltas is None:
            deltas = cls.get_default_deltas(rep)
        if sanity_checking_enabled:
            if isinstance(increments, np.ndarray) and len(increments.shape) != 1:
                raise ValueError("'increments' should be 1-dimensional")
            if isinstance(deltas, np.ndarray) and len(deltas.shape) != 1:
                raise ValueError("'deltas' should be 1-dimensional")
        if type_checking_enabled:
            if not isinstance(increments, (np.ndarray, list, tuple)):
                raise TypeError
            if not isinstance(deltas, (np.ndarray, list, tuple)):
                raise TypeError
        if sanity_checking_enabled:
            if len(increments) != len(rep):
                raise ValueError("length of increments must match length of representation, but {0} != {1}".format(len(increments), len(rep)))
            if len(increments) != len(deltas):
                raise ValueError("length of increments must match deltas, but {0} != {1}".format(len(increments), len(deltas)))
        disps = []
        for inc, delta, coord in zip(increments, deltas, rep):
            if hasunits(delta):
                disps.append(inc * delta.in_units(coord.units))
            else:
                # Assume the default units for the genre
                disps.append(inc * (delta * coord.units.genre.default).in_units(coord.units))
        return Displacement(rep, disps, increments=increments, deltas=deltas)

    ###########
    # Methods #
    ###########

    def increments(self, deltas=None):
        if self._increments is not None:
            return self._increments
        if deltas is None:
            if self._deltas is not None:
                deltas = self._deltas
            else:
                deltas = Displacement.get_default_deltas(self.representation)
        increments = []
        for i in self.disp_vect/deltas:
            if abs(i - int(i)) > 1e-8:
                raise RuntimeError("the deltas given are not the original deltas used.")
            increments.append(int(i))
        self._increments = tuple(increments)
        return self._increments


    ###################
    # Private Methods #
    ###################

    def _compute_displacement(self, tol=None, maxiter=None):
        self._displaced_molecule, self._displaced_representation = self.representation.displaced_by(self, tol, maxiter)
        self._displaced_molecule.displacement = self
        return
Ejemplo n.º 2
0
class LegacyXMLResultGetter(ResultGetter):

    ##############
    # Attributes #
    ##############

    files = ReadOnlyAttribute('files')
    properties = None
    properties_for_molecules = None

    ##################
    # Initialization #
    ##################

    def __init__(self, comparison_representation, *files):
        self.started = True
        self._files = listify_args(*files)
        self.properties = MoleculeDict(comparison_representation,
                                       default=lambda: [])
        self.properties_for_molecules = defaultdict(lambda: [])
        for file in files:
            self._parse_file(file)

    ###################
    # Private Methods #
    ###################

    def _parse_file(self, file):
        def _get_at_least_one(parent, tag, dispnum):
            ret_val = parent.findall(tag)
            if sanity_checking_enabled:
                if len(ret_val) == 0:
                    raise InvalidLegacyXMLFileError(
                        "missing {} section "
                        "for displacement number {}".format(tag, dispnum))
            return ret_val

        def _get_exactly_one(parent, tag, dispnum):
            ret_val = _get_at_least_one(parent, tag, dispnum)
            if sanity_checking_enabled:
                if len(ret_val) > 1:
                    raise InvalidLegacyXMLFileError(
                        "multiple {} sections "
                        "for displacement number {}".format(tag, dispnum))
            return ret_val[0]

        #========================================#
        etr = ElementTree.parse(file)
        for disp in etr.iter('displacement'):
            disp_number = disp.get('number', '<unnumbered>')
            # Get the molecule part
            mol_sect = _get_exactly_one(disp, 'molecule', disp_number)
            # Get the XYZ section
            xyz_sect = _get_exactly_one(mol_sect, 'xyz', disp_number)
            if 'units' in xyz_sect.keys():
                unitstr = xyz_sect.get('units')
                units = eval(unitstr.title(), globals())
            else:
                units = DistanceUnit.default
            energy_el = _get_at_least_one(disp, 'energy', disp_number)
            # for now just use the "molecular" energy
            energy_el = [
                e for e in energy_el if e.get('type', '') == 'molecular'
            ]
            if len(energy_el) == 0:
                raise InvalidLegacyXMLFileError(
                    "missing energy with type='molecular' "
                    "for displacement number {}".format(disp_number))
            elif len(energy_el) > 1:
                raise InvalidLegacyXMLFileError(
                    "multiple energy elements with type='molecular' "
                    "for displacement number {}".format(disp_number))
            energy_el = energy_el[0]
            if 'units' in energy_el.keys():
                unitstr = energy_el.get('units')
                energy_units = eval(unitstr.title(), globals())
            else:
                energy_units = Hartrees
            energy_val = float(energy_el.get('value')) * energy_units
            mol_stub = MoleculeStub(xyz_sect.text, units=units)
            energy = Energy(mol_stub,
                            units=energy_units,
                            details=ComputationDetails(type='molecular'))
            energy.value = energy_val
            self.properties[mol_stub].append(energy)

    def can_get_property_for_molecule(self, molecule, property, details=None):
        return self.has_property_for_molecule(molecule, property, details)

    def has_property_for_molecule(self,
                                  molecule,
                                  property,
                                  details=None,
                                  verbose=True):
        if molecule in self.properties:
            props = self.properties[molecule]
            for p in props:
                pcopy = copy(p)
                pcopy.molecule = molecule
                self.properties_for_molecules[molecule].append(pcopy)
                if MolecularProperty.is_same_property(property, p):
                    if ComputationDetails.is_compatible_details(
                            details, p.details):
                        return True
        return False

    def get_property_for_molecule(self, molecule, property, details=None):
        for p in self.properties_for_molecules[molecule]:
            if MolecularProperty.is_same_property(property, p):
                if ComputationDetails.is_compatible_details(
                        details, p.details):
                    return p
        props = self.properties[molecule]
        for p in props:
            pcopy = copy(p)
            pcopy.molecule = molecule
            self.properties_for_molecules[molecule].append(pcopy)
            if MolecularProperty.is_same_property(property, p):
                if ComputationDetails.is_compatible_details(
                        details, p.details):
                    return pcopy
        raise PropertyUnavailableError
Ejemplo n.º 3
0
class Representation(Freezable):
    """
    Superclass of all the representations types.

    :Attributes:

        molecule : `Molecule`
            The `Molecule` object represented by `self`.
        coords : list of `Coordinate`
            The coordinates that make up the representation

    """

    __metaclass__ = ABCMeta

    ##############
    # Attributes #
    ##############

    molecule = FreezableAttribute(name="molecule",
                                  doc="""
        The `Molecule` object represented by `self`.
        """)

    coords = FreezableListAttribute(name="coords",
                                    doc="""
        The coordinates that make up the representation
        """)

    units = ReadOnlyAttribute(name='units',
                              doc="""
        The units to use for the coordinates created, or if the created coordinates vary in units, a `dict` of
        `UnitGenre`, `Unit` pairs.  Must be passed into constructor; defaults to `genre.default`, where `genre` is
        the `UnitGenre` subclass of the applicable units.
        """)

    ###################
    # Special Methods #
    ###################

    def __getitem__(self, item):
        return self.coords[item]

    def __len__(self):
        return len(self.coords)

    def __iter__(self):
        for coord in self.coords:
            yield coord

    ##############
    # Properties #
    ##############

    @property
    def values(self):
        vals = [c.value for c in self.coords]
        return Vector(vals)

    value = values

    ####################
    # Abstract Methods #
    ####################

    @abstractmethod
    def add_coordinate_copy(self, coordinate):
        return NotImplemented

    @abstractmethod
    def copy_with_molecule(self, molecule):
        """ Make a copy of `self` that is the same in every way except for the `molecule` attribute.
        New Coordinate objects are created using the `Coordinate.copy_for_representation()` method for
        each element of `self.coords`.
        This is an abstract method that *must* be implemented by all Representation subclasses.
        """
        return NotImplemented

    @abstractmethod
    def displaced_by(self, disp, tol=None, maxiter=None):
        """ Apply the `Displacement` instance `disp` to the molecule and current representation,
        generating a new molecule and a new representation (which start as a `deepcopy` and the return value of
        `Representation.copy_with_molecule`, respectively) with the displacement applied.
        This is an abstract method that *must* be implemented by all Representation subclasses.
        """
        return NotImplemented

    ###########
    # Methods #
    ###########

    def values_for_molecule(self, mol):
        return Vector([c.value_for_molecule(mol) for c in self.coords])

    value_for_molecule = function_alias('value_for_molecule',
                                        values_for_molecule)

    def values_for_matrix(self, mat):
        return Vector([c.value_for_molecule_matrix(mat) for c in self.coords])

    value_for_matrix = function_alias('value_for_matrix', values_for_matrix)
Ejemplo n.º 4
0
class Atom(object):
    """
    Encapsulates an atom.  Molecules are made up of Atoms.

    :Attributes:

    position : `Vector`
        The atom's Cartesian coordinates [x y z] as a :py:class:`~grendel.math.vector.Vector` object.
    parent_molecule : `Molecule`
        Reference back to the parent molecule of the atom.  (Not set in initializer, but set when the atom gets
        included in a `Molecule`).  Note that for purposes of pickling and copying this may become a weakref in the
        future.  Do not implement behavior that depends on it being a strong reference.
    zmat_label : `str`
        Used in the construction of a z-matrix to differentiate atoms of the same Element.  Currently not used anywhere
        else, and not set when any other Atom construction method is used.



    """

    ####################
    # Class Attributes #
    ####################

    same_coordinate_tolerance = 1e-6  # Affects the __lt__ and __eq__ functions

    ################
    #  Attributes  #
    ################

    position = None
    parent_molecule = None
    zmat_label = None

    base_atom = None
    """
    If the `Atom` instance is a displaced copy of another `Atom`, the original is held here.
    """

    cartesian_units = ReadOnlyAttribute('cartesian_units')

    ########################
    #  Private Attributes  #
    ########################

    _element = None
    _isotope = None
    _orphaned_cart_coords = None

    ##################
    # Initialization #
    ##################

    @overloaded
    @with_flexible_arguments(optional=[('units', 'cartesian_units')],
                             what_to_call_it='Atom constructor')
    def __init__(self, *args, **kwargs):
        """
        Atom(*args, **kwargs)

        **Signatures**
            * ``Atom(symbol, position)``
            * ``Atom(symbol, x, y, z)``


        :Parameters:

        symbol : str
            The atomic symbol for the atom object
        position : Vector
            The atom's cartesian position
        x : float
            The atom's x posistion
        y : float
            The atom's y posistion
        z : float
            The atom's z posistion



        :Examples:

        >>> Atom('H', [0,0,0])
        Atom('H', [  0.00000000,  0.00000000,  0.00000000 ] )
        >>> Atom(position=(1.5, 0., 0.), symbol='H')
        Atom('H', [  1.50000000,  0.00000000,  0.00000000 ] )
        >>> Atom([1.5, 1.5, 0.], symbol='H')
        Atom('H', [  1.50000000,  1.50000000,  0.00000000 ] )

        """
        raise OverloadedFunctionCallError

    @__init__.overload_with(symbol=basestring, position=IterableOf(Real))
    def __init__(self, symbol, position, **kwargs):
        self._init_common(**kwargs)
        self.symbol = symbol
        if hasunits(position):
            if 'units' not in kwargs:
                self._cartesian_units = position.units
            elif self._cartesian_units != kwargs['units']:
                position = position.in_units(self._cartesian_units)
        self.position = Vector(position, copy=True, units=self.cartesian_units)

    @__init__.overload_with(symbol=basestring, x=Real, y=Real, z=Real)
    def __init__(self, symbol, x, y, z, **kwargs):
        self._init_common(**kwargs)
        self.symbol = symbol
        self.position = Vector([x, y, z], units=self.cartesian_units)

    @__init__.overload_with(element=Element, isotope=Isotope, position=Vector)
    def __init__(self, element, isotope, position, **kwargs):
        self._init_common(**kwargs)
        self._element = element
        self.isotope = isotope
        self.position = position

    def _init_common(self, **kwargs):
        self.position = None
        self.parent_molecule = None
        self._cartesian_units = kwargs.pop('units', DistanceUnit.default)
        if type_checking_enabled:
            if not isunit(self.cartesian_units
                          ) or self.cartesian_units.genre is not DistanceUnit:
                raise ValueError(
                    "Invalid units for Atom constructor: {}".format(
                        self.cartesian_units))

    ###################
    # Special Methods #
    ###################

    def __copy__(self):
        """ Copy the atom.  Calls `deepcopy` (i.e. there's only one way to copy an Atom instance).
        """
        return deepcopy(self)

    def __deepcopy__(self, memo):
        """ Override deepcopy to prevent copying of the element object
        """
        # -> *Don't* copy the parent_molecule attribute.  Rationale: this will most often be called by the
        #       Molecule class's __deepcopy__, and thus the atom will be assigned to a new molecule
        dup = Atom(self.symbol,
                   deepcopy(self.position, memo),
                   units=self.cartesian_units)
        dup._isotope = self._isotope
        dup.zmat_label = self.zmat_label
        return dup

    def __eq__(self, other):
        if (self.symbol, self.isotope) != (other.symbol, other.isotope):
            return False
        elif abs(self.x - other.x) > self.same_coordinate_tolerance:
            return False
        elif abs(self.y - other.y) > self.same_coordinate_tolerance:
            return False
        elif abs(self.z - other.z) > self.same_coordinate_tolerance:
            return False
        return True

    def __lt__(self, other):
        """
        a < b as Atoms if a.symbol < b.symbol or (if a.symbol == b.symbol), a.isotope < b.isotope, or
        (if a.isotope == b.isotope) if a.x < b.x, or (if a.x == a.x), if a.y < b.y, or (if a.y == a.y),
        if a.z < b.z.
        This establishes a well-ordering of atoms in a molecule to make comparison of molecule objects easier.
        """
        if (self.symbol, self.isotope) < (other.symbol, other.isotope):
            return True
        elif (self.symbol, self.isotope) > (other.symbol, other.isotope):
            return False
        elif abs(self.x - other.x) > self.same_coordinate_tolerance:
            return self.x < other.x
        elif abs(self.y - other.y) > self.same_coordinate_tolerance:
            return self.y < other.y
        elif abs(self.z - other.z) > self.same_coordinate_tolerance:
            return self.z < other.z
        else:
            return False

    #------------------------#
    # Output Representations #
    #------------------------#

    def __repr__(self):
        return "Atom('" + self.symbol + "', [" + ','.join(
            ["%12.8f" % num for num in self.position]) + " ] )"

    def __str__(self):
        symb_iso = self.symbol
        if not self.isotope.is_principal():
            symb_iso = self.isotope.symbol
        if self.zmat_label:
            ret_val = "Atom '{}'".format(self.zmat_label)
        elif not self.is_orphaned():

            ret_val = "Atom #{0}: '{1}'".format(self.index + 1, self.symbol)
        else:
            ret_val = "orphaned Atom '{0}'".format(self.symbol)
        ret_val += ' with position {0}'.format(self.pos)

        return ret_val

    ################
    #  Properties  #
    ################

    @property
    def symbol(self):
        """ The atomic symbol from the periodic table (as a :py:class:`~__builtin__.str` object)
        , with correct capitalization.  Updating this property transparently updates the
        :py:class:`~grendel.util.element.Element` object associated with ``self``.
        """
        return self.element.symbol

    @symbol.setter
    def symbol(self, symb):
        old = self._element
        try:
            self._element = Elements[symb]
        except KeyError:
            raise StandardError(
                "Unknown element symbol " + repr(symb) +
                ".  Please add element information to grendel/util/ElementData.py"
            )

        if old is None or not old == self._element:
            self.isotope = self._element.principal_isotope

    @CachedProperty
    def index(self):
        """ The index of the atom in its parent molecule.
        If the atom is orphaned but has a `base_atom`, that `Atom`'s `index` will be returned (and,
        of course, if `base_atom` is orphaned, proceed recursively until either the atom has no
        `base_atom` or a non-orphaned `Atom` is found).

        .. note::
           This is a cached property.  Be sure and reset it (by setting atom._index to None) if you
           reorder the atoms in the parent molecule (which is a disasterous thing to do for many other
           parts of the program as well.  If you really need to reorder atoms in a `Molecule`, you
           should create a new `Molecule` instance and copy the atoms to the new instance.)

        """
        if self.is_orphaned():
            if self.base_atom is not None:
                return self.base_atom.index
            else:
                # None will not be cached...
                return None
        else:
            return self.parent_molecule.atoms.index(self)

    @property
    def element(self):
        """ The :py:class:`~grendel.util.element.Element` object corresponding to `self`.
        Updated transparently using the :py:obj:`~grendel.atom.Atom.symbol` property
        """
        return self._element

    @property
    def mass(self):
        """ The atom's mass as a `float` (in AMU).  Automatically retrieved from the `element` and `isotope` attributes.
        """
        if self.isotope is None: return None
        return self.isotope.mass

    @property
    def isotope(self):
        """ The isotope of the element that `self` is composed of.  Defaults to `element.principal_isotope`
        """
        return self._isotope

    @isotope.setter
    def isotope(self, value):
        if self._element is None:
            raise StandardError(
                "self.element is not set.  Programmer needs more coffee...")
        if value not in self._element.isotopes:
            raise StandardError("Unknown isotope " + str(self._isotope) +
                                " of element " + str(self._element))
        self._isotope = value

    @CachedProperty
    def full_symbol(self):
        if self.isotope.is_principal():
            return self.symbol
        else:
            return self.symbol + str(int(round(self.isotope.mass)))

    @property
    def x(self):
        return self.position[0]

    @x.setter
    def x(self, val):
        self.position[0] = val

    @property
    def y(self):
        return self.position[1]

    @y.setter
    def y(self, val):
        self.position[1] = val

    @property
    def z(self):
        return self.position[2]

    @z.setter
    def z(self, val):
        self.position[2] = val

    @property
    def xyz(self):
        """ Alias for `self.position`.  `pos` is also an alias for `self.position`
        """
        return self.position

    @xyz.setter
    def xyz(self, val):
        self.position = val

    pos = xyz

    @property
    def cart_coords(self):
        return [coord for coord in self.iter_cart_coords()]

    @property
    def cart_indices(self):
        return [idx for coord, idx in self.iter_cart_coords(True)]

    @property
    def parent(self):
        """
        Alias for the `parent_molecule` attribute.
        """
        return self.parent_molecule

    @parent.setter
    def parent(self, new_val):
        self.parent_molecule = new_val

    ###########
    # Methods #
    ###########

    #-------------------------------------#
    # Inquiry methods (which return bool) #
    #-------------------------------------#

    def is_orphaned(self):
        return self.parent_molecule is None

    #---------------#
    # Other methods #
    #---------------#

    @typechecked(vect=LightVector)
    def displace(self, vect):
        if sanity_checking_enabled:
            if len(vect) != 3:
                raise ValueError(
                    "displacement vector must have three dimensions")
        self.position += vect

    def displaced(self, disp_vect):
        """
        Creates an orphaned copy of `atom` and displaces it by disp_vect
        """
        ret_val = deepcopy(self)
        ret_val.displace(disp_vect)
        ret_val.base_atom = self
        return ret_val

    def convert_units(self, new_units):
        self.position = self.position * self._cartesian_units.to(new_units)
        self._cartesian_units = new_units

    def in_units(self, new_units):
        ret_val = deepcopy(self)
        ret_val.convert_units(new_units)
        return ret_val

    def iter_cart_coords(self, with_index=False):
        if not self.is_orphaned():
            cart_rep = self.parent_molecule.cartesian_representation
            idx = self.index
            for x in [0, 1, 2]:
                if with_index:
                    yield cart_rep[3 * idx + x], 3 * idx + x
                else:
                    yield cart_rep[3 * idx + x]
        else:
            raise ValueError(
                "cannot iterate over cartesian coordinates of an orphaned atom."
            )
Ejemplo n.º 5
0
class DisplacementManager(FiniteDifferenceFunction):
    """ A function (in the `finite_difference.FiniteDifferenceFunction` sense) that can get values for displacements of
    some base molecule.
    """

    ##############
    # Attributes #
    ##############

    representation = ReadOnlyAttribute('representation',
        doc=""" The representation in which to carry out the displacements."""
    )
    differentiable = ReadOnlyAttribute('differentiable',
        doc=""" The subclass of Differentiable that `value_for_displacements` should return an instance of."""
    )

    variables = ReadOnlyAttribute('variables',
        doc="""The list of `FiniteDifferenceVariable` instances on which the function depends."""
    )

    output_type = ReadOnlyAttribute('output_type',
        doc="""The subclass of `Differentiable` that the function generates."""
    )

    deltas = None

    displacements = None

    details = None

    ##################
    # Initialization #
    ##################

    @typechecked(
        base_molecule_or_representation=('Molecule', 'Representation'),
        differentiable=type,
        deltas=(SequenceOf(float), None),
        details=(ComputationDetails, None)
    )
    def __init__(self, base_molecule_or_representation, differentiable, deltas=None, details=None):
        if type_checking_enabled:
            if not isinstance(base_molecule_or_representation, (Molecule, Representation)):
                raise TypeError
            if not isinstance(differentiable, type) and issubclass(differentiable, Differentiable):
                raise TypeError
        if sanity_checking_enabled:
            if not issubclass(differentiable, MolecularProperty):
                # TODO write this error
                raise ValueError
        self._differentiable = differentiable
        if isinstance(base_molecule_or_representation, Molecule):
            # Assume the first InternalRepresentation is the one we want
            self._representation = base_molecule_or_representation.internal_representation
        else:
            self._representation = base_molecule_or_representation
        self.displacements = {}
        self.deltas = deltas
        self.details = details
        if deltas is not None:
            if type_checking_enabled and not isinstance(deltas, Iterable):
                raise TypeError
            if sanity_checking_enabled and len(deltas) != len(self.representation):
                raise ValueError("dimension mismatch.  'deltas' list (length {}) must be the same " \
                                  "as the number of coordinates ({})".format(len(deltas), len(self.representation)))
            self.deltas = []
            for delta, coord in zip(deltas, self.representation):
                if hasunits(delta):
                    if sanity_checking_enabled and not compatible_units(delta.units, coord.units):
                        raise IncompatibleUnitsError("{} is not in valid units for a displacement of a {}".format(
                            delta, coord.__class__.__name__
                        ))
                else:
                    delta = delta * coord.units
                self.deltas.append(delta)
        else:
            self.deltas = Displacement.get_default_deltas(self.representation)

    ##############
    # Properties #
    ##############

    @property
    def base_molecule(self):
        return self.representation.molecule

    @property
    def displaced_molecules(self):
        return [d.displaced_molecule for d in self.displacements.values]

    ###################
    # Special Methods #
    ###################

    def __contains__(self, item):
        if isinstance(item, tuple):
            return item in self.displacements
        elif isinstance(item, Displacement):
            return item.increments(self.deltas) in self.displacements
        else:
            raise TypeError

    ###########
    # Methods #
    ###########

    def displacement_for(self, increments):
        if increments not in self.displacements:
            self.displacements[increments] = Displacement.from_increments(
                increments,
                self.representation,
                self.deltas)
        return self.displacements[increments]

    #----------------------------------------------#
    # Methods abstract in FiniteDifferenceFunction #
    #----------------------------------------------#

    def value_for_displacements(self, pairs):
        # Type checking and sanity checking
        if type_checking_enabled:
            if not all(len(pair) == 2 and isinstance(pair[0], FiniteDifferenceVariable) and isinstance(pair[1], int) for pair in pairs):
                raise TypeError
        if sanity_checking_enabled:
            for pair in pairs:
                if not isinstance(pair[0], Coordinate):
                    raise ValueError(
                        "FiniteDifferenceVariable instances passed to"
                        " DisplacementManager.value_for_displacements"
                        " must be instances of Coordinate subclasses."
                        "  (Got at least one that was a {0})".format(
                            type(pair[0]).__name__
                        ))
        #--------------------------------------------------------------------------------#
        # prepare the increments
        increments = [0]*len(self.representation)
        for coord, ndeltas in pairs:
            i = self.representation.coords.index(coord)
            increments[i] = ndeltas
        increments = tuple(increments)
        #--------------------------------------------------------------------------------#
        # get the displacement, and ask the displaced molecule for the property
        try:
            displacement = self.displacement_for(increments)
            rv = displacement.displaced_molecule.get_property(
                self.differentiable,
                details=self.details
            )
            if rv is None:
                raise ValueError("couldn't get displacement for increments {}".format(increments))
            return rv
        #--------------------------------------------------------------------------------#
        # Old debugging code.  Only applies to LegacyXMLResultGetter
        except RuntimeError as e:
            if all(isinstance(rg, LegacyXMLResultGetter)
                    for rg in self.displacements[increments].displaced_molecule.result_getters):
                legacy_getters = [rg
                                     for rg in self.base_molecule.result_getters
                                         if isinstance(rg, LegacyXMLResultGetter)]
                molecule = self.displacements[increments].displaced_molecule
                smallest_dist = float('inf') * molecule.cartesian_units
                smallest_dist_stub = None
                best_rg = None
                mol = copy(molecule); mol.convert_units(DistanceUnit.default)
                for rg in legacy_getters:
                    for stub in rg.properties.keys():
                        st = copy(stub); st.convert_units(DistanceUnit.default)
                        ldiff = rg.properties.key_representation.value_for_molecule(stub)
                        ldiff -= rg.properties.key_representation.value_for_molecule(mol)
                        ldiff = abs(ldiff)
                        if max(ldiff) < smallest_dist:
                            best_rg = rg
                            smallest_dist = max(ldiff)
                            smallest_dist_stub = st
                if smallest_dist_stub is not None:
                    # Print debugging information for legacy xml parser
                    print("Failed to find match.  Smallest " \
                          "maximum geometric difference was {}".format(smallest_dist),
                        file=sys.stderr)
                    print(indented(
                            "Desired molecule representation " \
                            "was:\n{}".format(indented(str(molecule.internal_representation.copy_with_molecule(mol))))
                        ), file=sys.stderr)
                    print(indented(
                        '-'*40 +
                        "\nClosest stub in the same representation:\n{}".format(
                            indented(str(molecule.internal_representation.copy_with_molecule(smallest_dist_stub))))
                    ), file=sys.stderr)
                    print("Tree leaf: \n{}".format(
                        best_rg.properties._get_possibilities(mol)
                    ), file=sys.stderr)
                    print("Chains: \nMolecule:\n{}\nStub:\n{}".format(
                        indented(str(best_rg.properties._key_chain_str(mol))),
                        indented(str(best_rg.properties._key_chain_str(smallest_dist_stub))),
                    ), file=sys.stderr)
                    sys.stderr.flush()
                raise RuntimeError("couldn't get {} for displacements {}".format(
                    self.differentiable.__name__,
                    increments
                ))
            else:
                raise

    def deltas_for_variables(self, vars):
        return [self.deltas[i] for i in range(len(self.representation)) if self.representation[i] in vars]
Ejemplo n.º 6
0
class FiniteDifferenceDerivative(object):
    """ An arbitrary finite difference derivative with respect to k `FiniteDifferenceVariable` instances (not necessarily distinct)
    of a `FiniteDifferenceFunction` of an arbitrary number of `FiniteDifferenceVariable` instances whose output is a `Differentiable` instance.
    Ideally, the derivative can be calculated to an arbitrary order of robustness in the displacement, though in
    practice not all orders may be implemented yet.
    """

    ####################
    # Class Attributes #
    ####################

    formulas = []
    generated_single_variable_formulas = {}

    ##############
    # Attributes #
    ##############

    function = ReadOnlyAttribute('function',
        doc="""The `FiniteDifferenceFunction` instance to differentiate."""
    )

    variables = ReadOnlyAttribute('variables',
        doc="""The list of `FiniteDifferenceVariable` instances to be displaced for computation of the finite difference derivative."""
    )

    target_robustness = ReadOnlyAttribute('target_robustness',
        doc="""The minimum order of the error in the displacement.  Defaults to 2."""
    )

    formula = ReadOnlyAttribute('formula',
        doc="""The FiniteDifferenceFormula object we need to use, based on the input parameters."""
    )

    orders = ReadOnlyAttribute('orders')

    ######################
    # Private Attributes #
    ######################

    _value = None
    _value_function = None
    _delta_function = None
    _delta = None
    _forward = None


    ##################
    # Initialization #
    ##################

    @with_flexible_arguments(
        optional = [
            ('target_robustness', 'robustness', 'accuracy', 'order', 'correct_to_order')
        ]
    )
    @typechecked(function='FiniteDifferenceFunction')
    def __init__(self, function, *variables, **kwargs):
        """
        """
        if len(FiniteDifferenceDerivative.formulas) == 0:
            # Load the formulas generated "by hand", which (for now, anyway) require fewer
            #   displacements than the automatically generated formulas if we also need to
            #   compute the lower order derivatives as well, as is the case with the computation
            #   of quartic forcefields. (But not, for instance, the B tensor.  So the
            #   FiniteDifferenceDerivative constructor could be optimized to take a parameter
            #   which specifies whether we should choose the formula with the fewest overall
            #   displacements or the fewest "new" displacements not needed for smaller derivatives)
            load_formulas()
        #--------------------------------------------------------------------------------#
        # miscellanea
        self._target_robustness = kwargs.pop('target_robustness', 2)
        self._value_function = kwargs.pop('value_function', None)
        self._delta_function = kwargs.pop('delta_function', None)
        self._delta = kwargs.pop('delta', None)
        self._forward = kwargs.pop('forward', False)
        self._function = function
        #--------------------------------------------------------------------------------#
        # type checking
        if type_checking_enabled:
            if not all(isinstance(v, FiniteDifferenceVariable) for v in variables):
                raise TypeError
            if not isinstance(self.target_robustness, int):
                raise TypeError
        #--------------------------------------------------------------------------------#
        # Get the variables and the orders....
        vars = listify_args(*variables)
        # Determine which formula we need
        vars = sorted(vars, key=id)
        # This is nasty, but it works...The zip(*list_of_lists) effectively "unzips"
        self._orders, self._variables = zip(
            *sorted(
                [(len(list(g)), k) for k, g in groupby(vars)],
                reverse=True)
        )
        #--------------------------------------------------------------------------------#
        # Determine which formula to use
        # This gets reused, so define a quicky function...
        def get_possibilities(formula_list):
            return [f for f in formula_list
                if f.orders == list(self.orders)
                        and f.robustness >= self.target_robustness
                        and (f.is_forward() if self._forward else f.is_central())
            ]
        #----------------------------------------#
        # First, try and get a "hand-generated" formula
        possibilities = get_possibilities(FiniteDifferenceDerivative.formulas)
        if len(possibilities) == 0:
            # We know how to generate single variable formulas to arbitrary order, so let's do it
            n_derivative_vars = len(self.orders)
            derivative_order = sum(self.orders)
            if n_derivative_vars == 1:
                # This long name is unweildy...
                gen_dict = FiniteDifferenceDerivative.generated_single_variable_formulas
                # See if we've already generated it...
                formula = gen_dict.get(
                    (
                        derivative_order,
                        self.target_robustness
                            + (1 if not self._forward and self.target_robustness % 2 == 1 else 0),
                        self._forward
                    ),
                    None)
                if formula is None:
                    # okay, we can generate it.
                    generate_single_variable_formulas(
                        derivative_order,
                        self.target_robustness
                            + (1 if not self._forward and self.target_robustness % 2 == 1 else 0),
                        self._forward)
                    formula = gen_dict[(
                        derivative_order,
                        self.target_robustness
                            + (1 if not self._forward and self.target_robustness % 2 == 1 else 0),
                        self._forward)]
                possibilities.append(formula)
                if sanity_checking_enabled:
                    possibilities = get_possibilities(possibilities)
            else:
                # we don't know how to generate these...yet...but I'm working on it!
                raise RuntimeError("Can't find formula for orders {0} and"
                                   " robustness {1}".format(
                    self.orders, self.target_robustness))
        # Use the minimum robustness for now.  Later we can make it use
        #   the best possible without additional calculations.
        self._formula = sorted(possibilities, key=attrgetter('robustness'))[0]

    ##############
    # Properties #
    ##############

    @property
    def value(self):
        if self._value is None:
            self.compute()
        return self._value

    @property
    def needed_increments(self):
        return self.formula.coefficients.keys()

    #################
    # Class Methods #
    #################

    @classmethod
    def precompute_single_variable(cls, max_derivative, max_order, forward=False):
        """ Save a little bit of time by prepopulating the single variable displacement
        formulas dictionary up to `max_derivative` and `max_order`.  If `forward` is True,
        the forward formulas are precomputed instead of the central ones.
        """
        generate_single_variable_formulas(max_derivative, max_order, forward)



    ###########
    # Methods #
    ###########

    def compute(self):
        #TODO handle units (efficiently!!!)
        if self._value is not None:
            return self._value
        total = None
        for increments, coeff in self.formula.coefficients.items():
            if self._value_function:
                tmp = self._value_function(zip(self.variables, increments))
            else:
                tmp = self.function.value_for_displacements(zip(self.variables, increments))
            if tmp is None:
                raise ValueError("the value_for_displacements method of FiniteDifferenceFunction"
                                 " '{}' returned `None` for increments {}".format(
                    self.function, increments
                ))
            if hasattr(tmp, 'value'):
                val = tmp.value * coeff
            else:
                val = tmp * coeff
            if total is not None:
                total += val
            else:
                total = val
        if self._delta is not None:
            deltas = (self._delta,) * len(set(self.variables))
        elif self._delta_function:
            deltas = self._delta_function(self.variables)
        else:
            deltas = self.function.deltas_for_variables(self.variables)
        if isinstance(total, Fraction):
            # Try and keep it that way
            denom = reduce(mul, [d**exp for d, exp in zip(deltas, self.orders)])
            total /= denom
        else:
            total /= reduce(mul, Tensor(deltas)**Tensor(self.orders))
        self._value = total
Ejemplo n.º 7
0
class FiniteDifferenceFormula(object):
    """ A formula for a given finite difference derivative.
    """

    #############
    # Constants #
    #############

    CENTRAL = 0
    FORWARD = 1

    ##############
    # Attributes #
    ##############

    orders = ReadOnlyAttribute('orders')
    robustness = ReadOnlyAttribute('robustness')
    coefficients = ReadOnlyAttribute('coefficients',
        """ displacement => coefficient dictionary, where
        the elements of the displacement tuple correspond
        to the orders tuple
        """
    )
    direction = None

    ##################
    # Initialization #
    ##################

    def __init__(self, orders, robustness, pairs, forward=False):
        self._orders = orders
        self._robustness = robustness
        # pairs is a list of (coefficient, displacement tuple) tuples
        self._coefficients = dict((p[1], p[0]) for p in pairs)
        if forward:
            self.direction = FiniteDifferenceFormula.FORWARD
        else:
            self.direction = FiniteDifferenceFormula.CENTRAL

    ###################
    # Special Methods #
    ###################

    #------------------------#
    # Output Representations #
    #------------------------#

    #def __eq__(self, other):
    #    return self._robustness

    def __str__(self):
        max_width=80
        indent_size = 4
        function_name = 'F'
        disp_name = 'h'
        #----------------------------------------#
        def var(idx):
            vars = 'xyz' + str(reversed(string.ascii_lowercase[:-3]))
            vars = vars.replace(disp_name, '')
            vars = vars.replace(function_name, '')
            try:
                return vars[idx]
            except IndexError:
                return 'x_' + idx
        #----------------------------------------#
        flines = []
        curr_line = '['
        for dispnum, (disp, coeff) in enumerate(sorted(self.coefficients.items())):
            if coeff == 0:
                continue
            if coeff.denominator == 1:
                # Only print non-unit coefficients
                if abs(coeff) != 1:
                    disp_f = str(abs(coeff))
                else:
                    disp_f = ''
            else:
                disp_f = '(' + str(abs(coeff)) + ')'
            disp_f += function_name + '('
            for idx, d in enumerate(disp):
                disp_f += var(idx)
                if d == -1:
                    disp_f += ' - ' + disp_name + '_' + var(idx)
                elif d == 1:
                    disp_f += ' + ' + disp_name + '_' + var(idx)
                elif d < 0:
                    disp_f += ' - ' + str(abs(d)) + disp_name + '_' + var(idx)
                elif d > 0:
                    disp_f += ' + ' + str(d) + disp_name + '_' + var(idx)
                if d is not disp[-1]:
                    disp_f += ', '
            disp_f += ')'
            if dispnum != 0:
                if coeff < 0:
                    disp_f = ' - ' + disp_f
                else:
                    disp_f = ' + ' + disp_f
            elif coeff < 0:  # and dispnum == 0
                disp_f = '-' + disp_f
            if len(curr_line + disp_f) > (max_width if len(flines) == 0 else max_width - indent_size):
                if curr_line != '':
                    flines.append(curr_line)
                    curr_line = disp_f
                else:
                    # one term per line is the best we can do, and we still overflow...yikes...
                    flines.append(disp_f)
            else:
                curr_line += disp_f
        denom = '] / '
        if len(self.orders) > 1:
            denom += '('
        for idx, exp in enumerate(self.orders):
            denom += disp_name + '_' + var(idx)
            if exp > 1:
                denom += '^' + str(exp)
        if len(self.orders) > 1:
            denom += ')'
        if len(curr_line + denom) > (max_width if len(flines) == 0 else max_width - indent_size):
            if curr_line != '':
                flines.append(curr_line)
                curr_line = denom
            else:
                curr_line = denom
        else:
            curr_line += denom
        flines.append(curr_line)

        return '{cent} finite difference formula for df/{dvars},' \
             ' correct to order {order} in displacement ({terms} terms):\n{formula}'.format(
            cent='Central' if self.direction == FiniteDifferenceFormula.CENTRAL else 'Forward',
            order = self.robustness,
            dvars=''.join('d' + var(idx) + ('^'+str(exp) if exp > 1 else '')
                              for idx, exp in enumerate(self.orders)),
            formula=indented(('\n' + ' ' * indent_size).join(flines), indent_size),
            terms=len([c for c in self.coefficients.values() if c != 0])
        )


    def __repr__(self):
        return "FiniteDifferenceFormula({dord}, {rob}, [\n{terms}\n])".format(
            dord=repr(self.orders),
            rob=self.robustness,
            terms=indented(
                ',\n'.join(repr((coeff, deltas)) for deltas, coeff in
                    sorted(self.coefficients.items(), key=lambda x: ' '.join(str(i) for i in x[0]))
                        if coeff != 0)
                )
        )

    ##############
    # Properties #
    ##############

    @property
    def order(self):
        return reduce(add, self.orders)

    @property
    def needed_displacements(self):
        return self.coefficients.items()

    ###########
    # Methods #
    ###########

    #-----------------#
    # Inquiry methods #
    #-----------------#

    def is_forward(self):
        return self.direction == FiniteDifferenceFormula.FORWARD

    def is_central(self):
        return self.direction == FiniteDifferenceFormula.CENTRAL