Skip to content

Commit

Permalink
Merge branch 'main' into update-changelog-for-v040
Browse files Browse the repository at this point in the history
  • Loading branch information
lilyminium authored Jul 22, 2024
2 parents 1ece162 + fe5d09b commit 0bdf7eb
Show file tree
Hide file tree
Showing 17 changed files with 501 additions and 31 deletions.
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: 2
updates:

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
9 changes: 2 additions & 7 deletions .github/workflows/dev-ci.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Dev version CI
name: Upstream nightly version CI
on:
schedule:
# weekly tests, Sundays at midnight
Expand All @@ -24,14 +24,9 @@ jobs:
matrix:
os: [macOS-12, ubuntu-latest]
python-version: ["3.10", "3.11", "3.12"]
exclude:
# mdtraj currently doesn't support MacOS py3.12
- os: "macOS-latest"
python-version: "3.12"


steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Build information
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/examples-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:


steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Build information
run: |
Expand Down
19 changes: 8 additions & 11 deletions .github/workflows/gh-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@ jobs:
exclude:
- include-rdkit: false
include-openeye: false
# Can't support 3.12 on Mac yet
- python-version: "3.12"
os: "macOS-latest"
# no openeye for 3.12 yet
- include-openeye: true
python-version: "3.12"
Expand All @@ -51,7 +48,7 @@ jobs:


steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Build information
run: |
Expand Down Expand Up @@ -111,7 +108,7 @@ jobs:
python -m pytest -n 4 -v --cov=openff/nagl --cov-config=setup.cfg --cov-append --cov-report=xml --color=yes openff/nagl/
- name: codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
Expand All @@ -123,10 +120,10 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: "3.10"

Expand All @@ -147,10 +144,10 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: "3.10"

Expand All @@ -177,10 +174,10 @@ jobs:
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Install conda
uses: conda-incubator/setup-miniconda@v2
uses: conda-incubator/setup-miniconda@v3
with:
python-version: ${{ matrix.python-version }}
add-pip-as-python-dependency: true
Expand Down
2 changes: 1 addition & 1 deletion devtools/conda-envs/docs_env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies:
- openff-toolkit-base >=0.11.1
- openff-units
- pydantic <2.0
- rdkit !=2024.03.4
- rdkit
- openeye-toolkits

# database
Expand Down
2 changes: 1 addition & 1 deletion devtools/conda-envs/examples_env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ dependencies:
- openff-units
- openff-recharge
- pydantic <3
- rdkit !=2024.03.4
- rdkit

# database
- pyarrow
Expand Down
2 changes: 1 addition & 1 deletion devtools/conda-envs/nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ dependencies:

# chemistry
- pydantic <3
- rdkit !=2024.03.4
- rdkit
- scipy

# database
Expand Down
2 changes: 1 addition & 1 deletion devtools/conda-envs/test_cuda_env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies:
- openff-toolkit-base >=0.11.1
- openff-units
- pydantic <3
- rdkit !=2024.03.4
- rdkit
- openeye-toolkits

# database
Expand Down
3 changes: 1 addition & 2 deletions devtools/conda-envs/test_env_dgl_false.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies:
- openff-toolkit-base >=0.11.1
- openff-units
- pydantic <3
- rdkit !=2024.03.4
- rdkit
- scipy
- ambertools

Expand All @@ -37,4 +37,3 @@ dependencies:
- pytest-cov
- pytest-xdist
- codecov

2 changes: 1 addition & 1 deletion devtools/conda-envs/test_env_dgl_true.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ dependencies:
- openff-toolkit-base >=0.11.1
- openff-units
- pydantic <3
- rdkit !=2024.03.4
- rdkit
- scipy
- ambertools

Expand Down
4 changes: 2 additions & 2 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ This version adds support for lookup tables.
- [@j-wags]

### Added
- Support for lookup tables and entries (PR #122)
- Pins to DGL and RDKit for compatibility (PRs #117, #123)
- Added lookup table support (`openff.nagl.lookups.AtomPropertiesLookupTable`) (PR #122)
- Pins to DGL for compatibility (PR #117)

## v0.3.8 -- 2024-04-11

Expand Down
210 changes: 210 additions & 0 deletions openff/nagl/lookups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import types
import typing

import torch

from openff.nagl._base.base import ImmutableModel
from openff.nagl.utils._utils import is_iterable, potential_dict_to_list

try:
from pydantic.v1 import Field, validator
except ImportError:
from pydantic import Field, validator

if typing.TYPE_CHECKING:
from openff.toolkit.topology import Molecule


class PropertyProvenance(ImmutableModel):
"""
Class for storing the provenance of a property
"""
description: str = Field(
description="A description of the provenance"
)
versions: dict[str, str] = Field(
default_factory=dict,
description="The versions of the relevant software packages used to compute the property"
)

class BasePropertiesLookupTableEntry(ImmutableModel):
inchi: str = Field(
description="The InChI of the molecule"
)
provenance: PropertyProvenance = Field(
description="The provenance of the property value"
)

class AtomPropertiesLookupTableEntry(BasePropertiesLookupTableEntry):
"""
Class for storing property lookup table entries
"""
property_type: typing.Literal["atom"] = Field(
default="atom",
description="The type of the property"
)

mapped_smiles: str = Field(
description="The mapped SMILES of the molecule"
)

property_value: tuple[float, ...] = Field(
description=(
"The values of the property, ordered according to mapped SMILES"
)
)

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

class BaseLookupTable(ImmutableModel):
"""
Class for storing property lookup tables
"""

property_name: str = Field(
description="The name of the property"
)


class AtomPropertiesLookupTable(BaseLookupTable):
"""
Class for storing property lookup tables for atom properties
"""

property_type: typing.Literal["atom"] = Field(
default="atom",
description="The type of the property"
)

properties: types.MappingProxyType[str, AtomPropertiesLookupTableEntry] = Field(
description="The property lookup table"
)

@validator("properties", pre=True)
def _convert_property_lookup_table(cls, v):
"""
Do two things:
1. Account for an iterable being passed instead of a mapping
2. Ignore the keys of the mapping and re-generate them from inchi
"""
v = potential_dict_to_list(v)
if not is_iterable(v):
raise ValueError("The property lookup table must be an iterable")

if not all(isinstance(entry, AtomPropertiesLookupTableEntry) for entry in v):
raise ValueError("All entries must be AtomPropertiesLookupTableEntry instances")

return types.MappingProxyType({
entry.inchi: entry
for entry in v
})

def __len__(self) -> int:
return len(self.properties)

def __getitem__(self, key: str) -> AtomPropertiesLookupTableEntry:
return self.properties[key]

def __contains__(self, key: str) -> bool:
return key in self.properties

def lookup(self, molecule: "Molecule") -> torch.Tensor:
"""
Look up the property value for a molecule
Parameters
----------
molecule : openff.toolkit.topology.Molecule
The molecule to look up
Returns
-------
torch.Tensor
The property values, in the order of the molecule's atoms
Raises
------
KeyError
If the property value cannot be found for this molecule
"""
from openff.toolkit.topology import Molecule

inchi_key = molecule.to_inchi(fixed_hydrogens=True)
try:
entry = self.properties[inchi_key]
except KeyError:
raise KeyError(f"Could not find property value for molecule with InChI {inchi_key}")

assert len(entry) == molecule.n_atoms

# remap to query order
entry_molecule = Molecule.from_mapped_smiles(
entry.mapped_smiles,
allow_undefined_stereo=True
)

# first try with input bond orders and formal charges
is_isomorphic, query_to_entry_mapping = Molecule.are_isomorphic(
molecule,
entry_molecule,
return_atom_map=True,
)
if not is_isomorphic:
# try again without bond orders and formal charges.
# This should be enough to match the InChI

is_isomorphic, query_to_entry_mapping = Molecule.are_isomorphic(
molecule,
entry_molecule,
return_atom_map=True,
aromatic_matching=True,
formal_charge_matching=False,
bond_order_matching=False,
)

if not is_isomorphic:
# lastly -- skip stereochemistry matching
is_isomorphic, query_to_entry_mapping = Molecule.are_isomorphic(
molecule,
entry_molecule,
return_atom_map=True,
formal_charge_matching=False,
bond_order_matching=False,
# skip stereochemistry because matching inchi should be enough
atom_stereochemistry_matching=False,
bond_stereochemistry_matching=False,
)

assert is_isomorphic

# remap the property values to the query order
property_values = [
entry.property_value[query_to_entry_mapping[atom_index]]
for atom_index in range(molecule.n_atoms)
]
return torch.tensor(property_values, dtype=torch.float32)





LookupTableEntryType = typing.Union[AtomPropertiesLookupTableEntry]
LookupTableType = typing.Union[AtomPropertiesLookupTable]

LOOKUP_TABLE_CLASSES = {
"atom": AtomPropertiesLookupTable,
}


def _as_lookup_table(lookup_table_kwargs: dict) -> LookupTableType:
"""
Convert a dictionary to a lookup table
"""
if not isinstance(lookup_table_kwargs, BaseLookupTable):
lookup_table_type = lookup_table_kwargs["property_type"]
lookup_table_class = LOOKUP_TABLE_CLASSES[lookup_table_type]
return lookup_table_class(**lookup_table_kwargs)
return lookup_table_kwargs
Loading

0 comments on commit 0bdf7eb

Please sign in to comment.