Skip to content

Commit

Permalink
Merge pull request #37 from timbernat/dev
Browse files Browse the repository at this point in the history
Polymerist omnibus improvements
  • Loading branch information
timbernat authored Dec 18, 2024
2 parents 8f924f0 + ce9e052 commit 84a530d
Show file tree
Hide file tree
Showing 251 changed files with 3,878 additions and 782 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ on:
push:
branches:
- "main"
- "dev"
pull_request:
branches:
- "main"
- "dev"
schedule:
# Weekly tests run on main by default:
# Scheduled workflows run on the latest commit on the default or base branch.
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,6 @@ ENV/

# In-tree generated files
*/_version.py

# Espaloma junk output
**/.model.pt
8 changes: 5 additions & 3 deletions devtools/conda-envs/release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ dependencies:
- pip
- jupyterlab

# Testing
# Testing and docs
- pytest
- pytest-cov
- codecov
- sphinx

# Numerical libraries
- numpy
Expand All @@ -24,6 +25,7 @@ dependencies:
- openmm
- lammps
- mdtraj
- pint # for units in case OpenFF is not installed

# Molecule building
- mbuild
Expand All @@ -41,11 +43,11 @@ dependencies:
# OpenFF stack
- openff-toolkit ~=0.16
- openff-interchange >=0.3.28
- openff-nagl
- openff-nagl >= 0.4
- openff-nagl-models >= 0.3

# Chemical database queries
- cirpy
- pubchempy
- chemspipy
- pip:
- espaloma-charge ==0.0.8
11 changes: 7 additions & 4 deletions devtools/conda-envs/test-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ channels:
- conda-forge
- openeye
dependencies:
# Basic Python dependencies
# Basic Python dependencies
- python
- pip
- jupyterlab

# Testing
# Testing and docs
- pytest
- pytest-cov
- codecov
- sphinx

# Numerical libraries
- numpy
Expand All @@ -24,9 +25,11 @@ dependencies:
- openmm
- lammps
- mdtraj
- pint # for units in case OpenFF is not installed

# Molecule building
- mbuild
- openbabel
- rdkit
- openeye-toolkits # TODO: consider making this optional?

Expand All @@ -40,11 +43,11 @@ dependencies:
# OpenFF stack
- openff-toolkit ~=0.16
- openff-interchange >=0.3.28
- openff-nagl
- openff-nagl >= 0.4
- openff-nagl-models >= 0.3

# Chemical database queries
- cirpy
- pubchempy
- chemspipy
- pip:
- espaloma-charge ==0.0.8
6 changes: 5 additions & 1 deletion polymerist/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
"""A unified set of tools for setting up general organic polymer systems for MD within the OpenFF framework"""
"""A unified set of tools for setting up general organic polymer systems for molecular dynamics"""

__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'

# Add imports here
from ._version import __version__
from .genutils import importutils
# from .genutils.importutils import register_submodules, module_by_pkg_str

# _MODULE_SELF = module_by_pkg_str(__package__) # keep reference to own module
Expand Down
5 changes: 4 additions & 1 deletion polymerist/analysis/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
'''Utilities for calculating properties from MD configurations and trajectories'''
'''Utilities for calculating properties from MD configurations and trajectories'''

__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'
3 changes: 3 additions & 0 deletions polymerist/analysis/calculation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'''Direct implementations of property calculations'''

__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'

import numpy as np
from ..maths.linearalg.decomposition import diagonalize

Expand Down
3 changes: 3 additions & 0 deletions polymerist/analysis/mdtrajutils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'''Thin wrappers around mdtraj-implemented property calculations'''

__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'

from typing import Any, Callable, Iterable, Optional, TypeAlias, Union
from dataclasses import dataclass, field

Expand Down
5 changes: 4 additions & 1 deletion polymerist/data/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
'''Additional data shipped along with polymerist source code'''
'''Additional data shipped along with polymerist source code'''

__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'
5 changes: 4 additions & 1 deletion polymerist/genutils/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
'''General-purpose utilities constructed only with Python builtins + numpy'''
'''General-purpose utilities constructed only with Python builtins + numpy'''

__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'
41 changes: 41 additions & 0 deletions polymerist/genutils/attrs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'''For dynamically inspecting and modifying attributes of Python objects'''

__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'

from typing import Any, Optional, Union
import re


def compile_argfree_getable_attrs(obj : Any, getter_re : Union[str, re.Pattern]='.*', repl_str : Optional[str]=None) -> dict[str, Any]:
'''Compile the values of all methods of an object which require no arguments other than perhaps the object itself (this EXCLUDES properties)
Returns a dict whose keys are the names of the methods called and whose values are the return values of those object methods
Can optionally filter the names of returned method using a regular expression, passed to "getter_re"
Can also optionally replace the chosen regex with an arbitrary string (including the empty string), passed to "repl_str"
Parameters
----------
obj : Any
Any object instance
getter_re : str or re.Pattern (optional), default ".*"
Optional regular expression to use for filtering down returned methods
Only methods whose names match the target regex are returns
repl_str : str (optional)
If provided, will replace the
for example, repl_str="" can be used to delete the regex from returned method names
Returns
-------
getable_dict : dict[str, Any]
dict whose keys are the selected method names and whose values are the corresponding method returns
'''
getable_dict = {}
for attr_name in dir(obj):
if re.search(getter_re, attr_name):
try:
attr_key = attr_name if (repl_str is None) else re.sub(getter_re, repl_str, attr_name)
getable_dict[attr_key] = getattr(obj, attr_name)()
except (TypeError, Exception): # TODO : find way to selectively intercept the Boost C++ wrapper ArgumentError
pass
return getable_dict
3 changes: 3 additions & 0 deletions polymerist/genutils/bits.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'''For bitwise operations and conversions to/from bitstrings'''

__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'

from typing import Union


Expand Down
3 changes: 3 additions & 0 deletions polymerist/genutils/containers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'''Custom data containers with useful properties'''

__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'

from typing import Any, Iterable, TypeVar
T = TypeVar('T') # generic type variable

Expand Down
5 changes: 4 additions & 1 deletion polymerist/genutils/decorators/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
'''Decorators for modifying functions and classes. Supply useful behaviors and/or eliminate boilerplate'''
'''Decorators for modifying functions and classes. Supply useful behaviors and/or eliminate boilerplate'''

__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'
35 changes: 32 additions & 3 deletions polymerist/genutils/decorators/classmod.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
'''Decorators for modifying classes'''

from typing import Callable, Iterable, Optional, TypeVar
__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'

from typing import Callable, Iterable, Optional, TypeVar, Union
C = TypeVar('C')


def generate_repr(cls : Optional[C]=None, disp_attrs : Optional[Iterable[str]]=None, lookup_attr : Optional[str]=None):
def generate_repr(cls : Optional[C]=None, disp_attrs : Optional[Iterable[str]]=None, lookup_attr : Optional[str]=None) -> Union[C, Callable[[C], C]]:
'''
Class decorator for auto-generating __repr__ methods
Expand Down Expand Up @@ -33,7 +36,7 @@ def _repr_generic(self) -> str:
return class_decorator
return class_decorator(cls) # return literal class decorator call

def register_subclasses(cls : Optional[C]=None, key_attr : str='__name__', reg_attr : str='subclass_registry') -> Callable[[C], C]:
def register_subclasses(cls : Optional[C]=None, key_attr : str='__name__', reg_attr : str='subclass_registry') -> Union[C, Callable[[C], C]]:
'''
Parametric class decorator for automatically generating a registry of subclasses of a target class
Binds registry to the "registry" class property in the target class
Expand All @@ -57,3 +60,29 @@ def _registry(cls : C) -> dict[str, C]:
if cls is None: # null case (i.e. call without parens), return factory call
return class_decorator
return class_decorator(cls) # return literal class decorator call

# NOTE: "klass" is needed to distinguish between the class modified by this decorator and the classmethod arg when calling super()
# "klass" here is the parent, while "cls" is the child
def register_abstract_class_attrs(*attr_names : list[str]) -> Callable[[C], C]: # TODO: add mechanism for typehinting
'''Register a list of string attribute names as abstract class attributes,
which MUST be implemented by child classes of the wrapped class'''
def class_decorator(klass : C) -> C:
'''The actual (argument-free) class decorator'''
def __wrapped_init_subclass__(cls : C, **kwargs) -> None:
'''Wrapper for subclass definition which actually enforces that all named attributes are set'''
for attr_name in attr_names:
passed_attr_value = kwargs.pop(attr_name, NotImplemented) # want this removed from kwargs before passing to super, regardless of whether already set in child
attr_val_on_child = getattr(cls, attr_name, NotImplemented) # check if this has been set in the child in code

if attr_val_on_child is NotImplemented: # if the value has not been set in code...
if passed_attr_value is not NotImplemented: # ...fall back to value passed into class definition, if it exists...
setattr(cls, attr_name, passed_attr_value)
else: # otherwise, fail and raise Exception
raise TypeError(f"Can't instantiate abstract class {cls.__name__} with abstract class property '{attr_name}' undefined")

super(klass, cls).__init_subclass__(**kwargs) # this should fail if extraneous named args are passed

klass.__init_subclass__ = classmethod(__wrapped_init_subclass__)
return klass

return class_decorator # no need for application check here, since the parameterized decorator doesn't take a class to be modified
26 changes: 15 additions & 11 deletions polymerist/genutils/decorators/functional.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
'''Decorators for modifying functions'''

from typing import Callable, Iterable, Optional, Type, Union
__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'

from typing import Callable, Concatenate, Iterable, Iterator, Optional, ParamSpec, TypeVar, Union

T = TypeVar('T')
Params = ParamSpec('Params')

from inspect import signature, Parameter
from functools import wraps, partial
Expand All @@ -10,20 +16,18 @@

from .meta import extend_to_methods
from . import signatures
from ..typetools.parametric import T, Args, KWArgs
from ..typetools.categorical import ListLike
from ..fileutils.pathutils import aspath, asstrpath


@extend_to_methods
def optional_in_place(funct : Callable[[object, Args, KWArgs], None]) -> Callable[[object, Args, bool, KWArgs], Optional[object]]:
def optional_in_place(funct : Callable[[Concatenate[object, Params]], None]) -> Callable[[Concatenate[object, Params]], Optional[object]]:
'''Decorator function for allowing in-place (writeable) functions which modify object attributes
to be not performed in-place (i.e. read-only), specified by a boolean flag'''
# TODO : add assertion that the wrapped function has at least one arg AND that the first arg is of the desired (limited) type
old_sig = signature(funct)

@wraps(funct) # for preserving docstring and type annotations / signatures
def in_place_wrapper(obj : object, *args : Args, in_place : bool=False, **kwargs : KWArgs) -> Optional[object]: # read-only by default
def in_place_wrapper(obj : object, *args : Params.args, in_place : bool=False, **kwargs : Params.kwargs) -> Optional[object]: # read-only by default
'''If not in-place, create a clone on which the method is executed''' # NOTE : old_sig.bind screws up arg passing
if in_place:
funct(obj, *args, **kwargs) # default call to writeable method - implicitly returns None
Expand Down Expand Up @@ -51,9 +55,9 @@ def in_place_wrapper(obj : object, *args : Args, in_place : bool=False, **kwargs
return in_place_wrapper

# TODO : implement support for extend_to_methods (current mechanism is broken by additional deocrator parameters)
def flexible_listlike_input(funct : Callable[[ListLike], T]=None, CastType : Type[ListLike]=list, valid_member_types : Union[Type, tuple[Type]]=object) -> Callable[[Iterable], T]:
def flexible_listlike_input(funct : Callable[[Iterator], T]=None, CastType : type[Iterator]=list, valid_member_types : Union[type, tuple[type]]=object) -> Callable[[Iterable], T]:
'''Wrapper which allows a function which expects a single list-initializable, Container-like object to accept any Iterable (or even star-unpacked arguments)'''
if not issubclass(CastType, ListLike):
if not issubclass(CastType, Iterator):
raise TypeError(f'Cannot wrap listlike input with non-listlike type "{CastType.__name__}"')

@wraps(funct)
Expand All @@ -76,13 +80,13 @@ def wrapper(*args) -> T: # wrapper which accepts an arbitrary number of non-keyw
return wrapper

@extend_to_methods
def allow_string_paths(funct : Callable[[Path, Args, KWArgs], T]) -> Callable[[Union[Path, str], Args, KWArgs], T]:
def allow_string_paths(funct : Callable[[Concatenate[Path, Params]], T]) -> Callable[[Concatenate[Union[Path, str], Params]], T]:
'''Modifies a function which expects a Path as its first argument to also accept string-paths'''
# TODO : add assertion that the wrapped function has at least one arg AND that the first arg is of the desired (limited) type
old_sig = signature(funct) # lookup old type signature

@wraps(funct) # for preserving docstring and type annotations / signatures
def str_path_wrapper(flex_path : Union[str, Path], *args : Args, **kwargs : KWArgs) -> T:
def str_path_wrapper(flex_path : Union[str, Path], *args : Params.args, **kwargs : Params.kwargs) -> T:
'''First converts stringy paths into normal Paths, then executes the original function'''
return funct(aspath(flex_path), *args, **kwargs)

Expand All @@ -96,13 +100,13 @@ def str_path_wrapper(flex_path : Union[str, Path], *args : Args, **kwargs : KWAr
return str_path_wrapper

@extend_to_methods
def allow_pathlib_paths(funct : Callable[[str, Args, KWArgs], T]) -> Callable[[Union[Path, str], Args, KWArgs], T]:
def allow_pathlib_paths(funct : Callable[[Concatenate[str, Params]], T]) -> Callable[[Concatenate[Union[Path, str], Params]], T]:
'''Modifies a function which expects a string path as its first argument to also accept canonical pathlib Paths'''
# TODO : add assertion that the wrapped function has at least one arg AND that the first arg is of the desired (limited) type
old_sig = signature(funct) # lookup old type signature

@wraps(funct) # for preserving docstring and type annotations / signatures
def str_path_wrapper(flex_path : Union[str, Path], *args : Args, **kwargs : KWArgs) -> T:
def str_path_wrapper(flex_path : Union[str, Path], *args : Params.args, **kwargs : Params.kwargs) -> T:
'''First converts normal Paths into stringy paths, then executes the original function'''
return funct(asstrpath(flex_path), *args, **kwargs)

Expand Down
14 changes: 9 additions & 5 deletions polymerist/genutils/decorators/meta.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
'''Decorators for modifying other decorators'''

__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'

from typing import Concatenate, Callable, ParamSpec, TypeAlias, TypeVar
from functools import update_wrapper, wraps

from ..typetools.parametric import C, O, P, R, Args, KWArgs
Decorator : TypeAlias = Callable[[Callable[P, R]], Callable[P, R]]
Params = ParamSpec('Params') # can also use to typehint *args and **kwargs
ReturnType = TypeVar('ReturnType')
Decorator : TypeAlias = Callable[[Callable[Params, ReturnType]], Callable[Params, ReturnType]]


# META DECORATORS
Expand All @@ -15,16 +19,16 @@ def extend_to_methods(dec : Decorator) -> Decorator:

@wraps(dec, updated=()) # transfer decorator signature to decorator adapter class, without updating the __dict__ field
class AdaptedDecorator:
def __init__(self, funct : Callable[P, R]) -> None:
def __init__(self, funct : Callable[Params, ReturnType]) -> None:
'''Record function'''
self.funct = funct
update_wrapper(self, funct) # equivalent to functools.wraps, transfers docstring, module, etc. for documentation

def __call__(self, *args : Args, **kwargs : KWArgs) -> ReturnSignature: # TODO : fix this to reflect the decorator's return signature
def __call__(self, *args : Params.args, **kwargs : Params.kwargs) -> ReturnSignature: # TODO : fix this to reflect the decorator's return signature
'''Apply decorator to function, then call decorated function'''
return dec(self.funct)(*args, **kwargs)

def __get__(self, instance : O, owner : C) -> Callable[[Concatenate[O, P]], R]:
def __get__(self, instance : object, owner : type) -> Callable[[Concatenate[object, Params]], ReturnType]:
'''Generate partial application with calling instance as first argument (fills in for "self")'''
method = self.funct.__get__(instance, owner) # look up method belonging to owner class
return dec(method) # return the decorated method
Expand Down
Loading

0 comments on commit 84a530d

Please sign in to comment.