-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #34 from timbernat/DOP-hotfix
Polymer builder overhaul
- Loading branch information
Showing
34 changed files
with
1,456 additions
and
281 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ | |
__author__ = 'Timotej Bernat' | ||
__email__ = '[email protected]' | ||
|
||
from typing import Callable, ParamSpec, TypeVar | ||
from typing import Callable, Optional, ParamSpec, TypeVar | ||
|
||
Params = ParamSpec('Params') | ||
ReturnType = TypeVar('ReturnType') | ||
|
@@ -14,6 +14,25 @@ | |
from functools import wraps | ||
|
||
|
||
class MissingPrerequisitePackage(Exception): | ||
'''Raised when a package dependency cannot be found and the user should be alerted with install instructions''' | ||
def __init__(self, | ||
importing_package_name : str, | ||
use_case : str, | ||
install_link : str, | ||
dependency_name : str, | ||
dependency_name_formal : Optional[str]=None | ||
): | ||
if dependency_name_formal is None: | ||
dependency_name_formal = dependency_name | ||
|
||
message = f''' | ||
{use_case.capitalize()} require(s) {dependency_name_formal}, which was not found in the current environment | ||
Please install `{dependency_name}` by following the installation instructions at {install_link} | ||
Then try importing from "{importing_package_name}" again''' | ||
|
||
super().__init__(message) | ||
|
||
def module_installed(module_name : str) -> bool: | ||
''' | ||
Check whether a module of the given name is present on the system | ||
|
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
'''For identifying and concatenating substrings of other strings with unique properties''' | ||
|
||
__author__ = 'Timotej Bernat' | ||
__email__ = '[email protected]' | ||
|
||
|
||
def unique_string(string : str, preserve_order : bool=True) -> str: | ||
''' | ||
Accepts a string and returns another string containing | ||
only the UNIQUE characters in the origin string | ||
Can specify whether order is important with the "preserve_order" keyword | ||
Parameters | ||
---------- | ||
string : str | ||
An arbitrary string on wants the unique characters from | ||
preserve_order : bool, default True | ||
Whether or not to keep the unique characters in the order they are found | ||
For example: | ||
unique_string("balaclava", preserve_order=False) -> "bcavl" | ||
unique_string("balaclava", preserve_order=True) -> "balcv" | ||
Returns | ||
------- | ||
uniquified_str : str | ||
Another string containing only the unique characters in "string" | ||
Order depends on the value of the "preserve_order" parameter | ||
''' | ||
if not preserve_order: | ||
unique_chars = set(string) | ||
else: | ||
unique_chars = [] | ||
for char in string: | ||
if char not in unique_chars: | ||
unique_chars.append(char) | ||
|
||
return ''.join(unique_chars) | ||
|
||
def shortest_repeating_substring(string : str) -> str: | ||
'''Return the shortest substring such that the passed string can be written as some number of repeats (including 1) of the substring | ||
Will return the original string if no simpler decomposition exists''' | ||
i = (2*string).find(string, 1, -1) # check if string matches itself in a cycle in non-trivial way (i.e more than just the two repeats) | ||
return string if (i == -1) else string[:i] | ||
|
||
def repeat_string_to_length(string : str, target_length : int, joiner : str='') -> str: | ||
''' | ||
Takes a string and repeats it cyclically to produce another string of a given length | ||
The number of times the original string occurs in the new string may be fractional | ||
for example: | ||
>> repeat_string_to_length("CAT", 6) -> "CATCAT" | ||
>> repeat_string_to_length("BACA", 10) -> "BACABACABA" | ||
Parameters | ||
---------- | ||
string : str | ||
An arbitrary string to repeat | ||
target_length : int | ||
The length of the final desired string | ||
This does NOT have to be an integer multiple of the length of "string" | ||
E.g. repeat_string_to_length("BACA", 10) -> "BACABACABA" | ||
Nor does it have to be greater than the length of "string" | ||
E.g. repeat_string_to_length("BACA", 3) -> "BAC" | ||
Returns | ||
------- | ||
rep_string : str | ||
A new string which has the desired target length and consists of cycles of the initial string | ||
''' | ||
if not string: | ||
raise ValueError(f'Cannot generate nonempty string from any amount of repeats of the empty string') | ||
if not isinstance(target_length, int): | ||
raise TypeError(f'Only integer target string lengths are allowed, not non-integer type "{type(target_length).__name__}"') | ||
if target_length < 0: | ||
raise IndexError(f'Cannot generate a string of negative length (requested length of {target_length} character(s))') | ||
|
||
num_str_reps, num_extra_chars = divmod(target_length, len(string)) | ||
remainder = (string[:num_extra_chars],) if num_extra_chars else () # empty container avoids extra joiner at end when remainder string is empty | ||
|
||
return joiner.join(num_str_reps*(string,) + remainder) # tuples here are ~2 OOM faster than moral equivalent with lists | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,13 +4,14 @@ | |
__email__ = '[email protected]' | ||
|
||
# Subpackage-wide precheck to see if OpenFF is even usable in the first place | ||
from ...genutils.importutils.dependencies import modules_installed | ||
from ...genutils.importutils.dependencies import modules_installed, MissingPrerequisitePackage | ||
if not modules_installed('openff', 'openff.toolkit'): | ||
raise ModuleNotFoundError( | ||
f''' | ||
OpenFF packages which are required to utilitize {__name__} not found in current environment | ||
Please follow installation instructions at https://docs.openforcefield.org/projects/toolkit/en/stable/installation.html, then retry import | ||
''' | ||
raise MissingPrerequisitePackage( | ||
importing_package_name=__spec__.name, | ||
use_case='OpenFF addons', | ||
install_link='https://docs.openforcefield.org/projects/toolkit/en/stable/installation.html', | ||
dependency_name='openff-toolkit', | ||
dependency_name_formal='the OpenFF software stack', | ||
) | ||
|
||
# Import of toplevel OpenFF object registries | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,9 +4,3 @@ | |
__email__ = '[email protected]' | ||
|
||
from .mdobjects import forcefield_flexible, openff_topology_to_openmm | ||
from .unitsys import ( | ||
openmm_to_openff, | ||
openff_to_openmm, | ||
allow_openmm_units, | ||
allow_openff_units, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
'''Utilities for reading from and writing to various molecular file formats''' | ||
|
||
__author__ = 'Timotej Bernat' | ||
__email__ = '[email protected]' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
'''PDB file formatting tools''' | ||
|
||
__author__ = 'Timotej Bernat' | ||
__email__ = '[email protected]' | ||
|
||
from dataclasses import dataclass, field | ||
from collections import Counter | ||
|
||
|
||
@dataclass(frozen=True) | ||
class SerialAtomLabeller: | ||
''' | ||
For assigning unique numbered atom names based on their | ||
order of appearance within a molecule and elemental class | ||
Useful, for example, in generating unique atom names for a PDB file | ||
Parameters | ||
---------- | ||
atom_label_width : int , default 4 | ||
Exact length alloted for any generated atom label | ||
Labels shorter than this are right-padded with spaces, | ||
while labels longer than this are truncated | ||
Default of 4 is the chosen to be compatible with the PDB specification ("Atom name: lines 13-16, left-justified") | ||
https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/tutorials/pdbintro.html | ||
include_elem_idx : bool, default True | ||
Whether to attach a numerical element-index postfix to atom labels | ||
E.g. with atom_label_width=4, the fifth carbon in a topology | ||
will be labelled as "C004" with include_elem_idx=True, | ||
while labelled as "C " with include_elem_idx=False, | ||
default_elem_idx : int, default 0 | ||
Starting index for each element category | ||
By default, is 0-indexed; MUST BE POSITIVE | ||
''' | ||
atom_label_width : int = 4 | ||
include_elem_idx : bool = True | ||
default_elem_idx : int = 0 | ||
|
||
element_counter : Counter = field(init=False, default_factory=Counter) | ||
|
||
def __post_init__(self) -> None: | ||
'''Check ranges on input values''' | ||
if self.atom_label_width < 0: | ||
raise ValueError(f'Must provide a non-negative number of index digits to include (provided {self.atom_label_width})') | ||
|
||
if self.default_elem_idx < 0: | ||
raise ValueError(f'Must provide a non-negative starting index for element indices (provided {self.default_elem_idx})') | ||
|
||
def get_atom_label(self, elem_symbol : str) -> str: | ||
''' | ||
Obtain a numbered atom label for an atom based on its element, | ||
updating the underlying element context in the process | ||
''' | ||
if not isinstance(elem_symbol, str): | ||
raise TypeError(f'Must pass symbol of atom\'s element as str (not type {type(elem_symbol).__name__})') | ||
|
||
if elem_symbol not in self.element_counter: # initialize first occurence to starting value | ||
self.element_counter[elem_symbol] = self.default_elem_idx | ||
|
||
atom_idx_label : str = '' | ||
if self.include_elem_idx: | ||
atom_idx = self.element_counter[elem_symbol] | ||
num_idx_digits = max(self.atom_label_width - len(elem_symbol), 0) # number of symbols left over for an atom index | ||
atom_idx_label = f'{atom_idx:0{num_idx_digits}d}' | ||
|
||
atom_name = f'{elem_symbol}{atom_idx_label}' | ||
atom_name = atom_name.ljust(self.atom_label_width, ' ')[:self.atom_label_width] # pad with spaces if too short, or truncate if too long | ||
assert(len(atom_name) <= self.atom_label_width) # perfunctory check to make sure things are working as expected | ||
|
||
self.element_counter[elem_symbol] += 1 # update tally with addition of new occurence of a particular element | ||
|
||
return atom_name | ||
|
Oops, something went wrong.