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
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
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)
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." )
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]
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
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