Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:

test:

name: 💻 ${{ matrix.os }}, 🐍 ${{ matrix.python-version }}, 👀 ${{ matrix.openeye }}, pymbar ${{ matrix.pymbar-version }}, Pydantic ${{ matrix.pydantic-version }}, OpenMM ${{ matrix.openmm-version }}
name: 💻 ${{ matrix.os }}, 🐍 ${{ matrix.python-version }}, 👀 ${{ matrix.openeye }}, pymbar ${{ matrix.pymbar-version }}, OpenMM ${{ matrix.openmm-version }}
runs-on: ${{ matrix.os }}

env:
Expand All @@ -35,8 +35,6 @@ jobs:
- "3.12"
pymbar-version:
- "3.1"
pydantic-version:
- "2"
openmm-version:
- "8"
openeye:
Expand All @@ -53,7 +51,6 @@ jobs:
create-args: >-
python=${{ matrix.python-version }}
pymbar=${{ matrix.pymbar-version }}
pydantic=${{ matrix.pydantic-version }}
openmm=${{ matrix.openmm-version }}

- name: Install OpenEye
Expand Down Expand Up @@ -86,7 +83,7 @@ jobs:
run: python -m pip install . utilities/test_plugins/

- name: Run tests
run: python -m pytest -v --cov=openff openff/evaluator/_tests/ --cov-report=xml --color=yes
run: python -m pytest --cov=openff openff/evaluator/_tests/ --cov-report=xml --color=yes

- name: Run (non-GPU) tutorials
if: ${{ matrix.pymbar-version == 3.1 }}
Expand Down
2 changes: 1 addition & 1 deletion devtools/conda-envs/test_env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ dependencies:
- pyyaml
- requests
- python-dateutil
- pydantic >=1.10.17,<3
- pydantic =2
- taproom
- dataclasses
- pandas =2
14 changes: 0 additions & 14 deletions openff/evaluator/_pydantic.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import pandas
import pytest
from openff.units import unit
from pydantic import ValidationError

from openff.evaluator._pydantic import ValidationError
from openff.evaluator.datasets import (
MeasurementSource,
PhysicalPropertyDataSet,
Expand Down
2 changes: 1 addition & 1 deletion openff/evaluator/backends/dask_kubernetes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from dask.utils import funcname
from openff.units import unit
from openff.utilities.utilities import requires_package
from pydantic import BaseModel, Field

from openff.evaluator._pydantic import BaseModel, Field
from openff.evaluator.backends.backends import PodResources
from openff.evaluator.backends.dask import BaseDaskBackend, BaseDaskJobQueueBackend
from openff.evaluator.workflow.schemas import ProtocolSchema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from typing import overload

import pandas
from pydantic import BaseModel

from openff.evaluator._pydantic import BaseModel
from openff.evaluator.datasets import PhysicalPropertyDataSet

logger = logging.getLogger(__name__)
Expand Down
12 changes: 2 additions & 10 deletions openff/evaluator/datasets/curation/components/conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,17 @@

import functools
import logging
from typing import TYPE_CHECKING, Union
from typing import Union

import pandas
from pydantic import Field, conint
from typing_extensions import Literal

from openff.evaluator._pydantic import Field
from openff.evaluator.datasets.curation.components import (
CurationComponent,
CurationComponentSchema,
)

if TYPE_CHECKING:
conint = int
PositiveInt = int
PositiveFloat = float

else:
from openff.evaluator._pydantic import conint

logger = logging.getLogger(__name__)


Expand Down
111 changes: 50 additions & 61 deletions openff/evaluator/datasets/curation/components/filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,24 @@
import itertools
import logging
from collections import defaultdict
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
from typing import Dict, List, Optional, Tuple, Union

import numpy
import pandas
from openff.units import unit
from pydantic import (
Field,
PositiveFloat,
PositiveInt,
confloat,
conint,
constr,
model_validator,
validator,
)
from scipy.optimize import linear_sum_assignment
from typing_extensions import Literal

from openff.evaluator._pydantic import Field, root_validator, validator
from openff.evaluator.datasets.curation.components import (
CurationComponent,
CurationComponentSchema,
Expand All @@ -24,21 +33,6 @@
analyse_functional_groups,
)

if TYPE_CHECKING:
conint = int
confloat = float
PositiveInt = int
PositiveFloat = float

else:
from openff.evaluator._pydantic import (
PositiveFloat,
PositiveInt,
confloat,
conint,
constr,
)

logger = logging.getLogger(__name__)

ComponentEnvironments = List[List[ChemicalEnvironment]]
Expand Down Expand Up @@ -157,15 +151,15 @@ class FilterByTemperatureSchema(CurationComponentSchema):
description="Retain data points measured for temperatures below this value (K)",
)

@root_validator
def _min_max(cls, values):
minimum_temperature = values.get("minimum_temperature")
maximum_temperature = values.get("maximum_temperature")

if minimum_temperature is not None and maximum_temperature is not None:
assert maximum_temperature > minimum_temperature
@model_validator(mode="after")
def _min_max(self):
if (
self.minimum_temperature is not None
and self.maximum_temperature is not None
):
assert self.maximum_temperature > self.minimum_temperature

return values
return self


class FilterByTemperature(CurationComponent):
Expand Down Expand Up @@ -207,15 +201,12 @@ class FilterByPressureSchema(CurationComponentSchema):
description="Retain data points measured for pressures below this value (kPa)",
)

@root_validator
def _min_max(cls, values):
minimum_pressure = values.get("minimum_pressure")
maximum_pressure = values.get("maximum_pressure")
@model_validator(mode="after")
def _min_max(self):
if self.minimum_pressure is not None and self.maximum_pressure is not None:
assert self.maximum_pressure > self.minimum_pressure

if minimum_pressure is not None and maximum_pressure is not None:
assert maximum_pressure > minimum_pressure

return values
return self


class FilterByPressure(CurationComponent):
Expand Down Expand Up @@ -381,15 +372,12 @@ class FilterByElementsSchema(CurationComponentSchema):
"`allowed_elements`",
)

@root_validator
def _validate_mutually_exclusive(cls, values):
allowed_elements = values.get("allowed_elements")
forbidden_elements = values.get("forbidden_elements")

assert allowed_elements is not None or forbidden_elements is not None
assert allowed_elements is None or forbidden_elements is None
@model_validator(mode="after")
def _validate_mutually_exclusive(self):
assert self.allowed_elements is not None or self.forbidden_elements is not None
assert self.allowed_elements is None or self.forbidden_elements is None

return values
return self


class FilterByElements(CurationComponent):
Expand Down Expand Up @@ -447,14 +435,14 @@ class FilterByPropertyTypesSchema(CurationComponentSchema):
"required to have been measured at the same state.",
)

@root_validator
def _validate_n_components(cls, values):
property_types = values.get("property_types")
n_components = values.get("n_components")
@model_validator(mode="after")
def _validate_n_components(self):
property_types = self.property_types
n_components = self.n_components

assert all(x in property_types for x in n_components)

return values
return self


class FilterByPropertyTypes(CurationComponent):
Expand Down Expand Up @@ -726,15 +714,13 @@ class FilterBySmilesSchema(CurationComponentSchema):
"This option only applies when `smiles_to_include` is set.",
)

@root_validator
def _validate_mutually_exclusive(cls, values):
smiles_to_include = values.get("smiles_to_include")
smiles_to_exclude = values.get("smiles_to_exclude")

assert smiles_to_include is not None or smiles_to_exclude is not None
assert smiles_to_include is None or smiles_to_exclude is None
@model_validator(mode="after")
@classmethod
def _validate_mutually_exclusive(cls, data):
assert data.smiles_to_include is not None or data.smiles_to_exclude is not None
assert data.smiles_to_include is None or data.smiles_to_exclude is None

return values
return cls


class FilterBySmiles(CurationComponent):
Expand Down Expand Up @@ -806,7 +792,8 @@ class FilterBySmirksSchema(CurationComponentSchema):
"when `smirks_to_include` is set.",
)

@root_validator
@model_validator(mode="before")
Copy link
Contributor

@lilyminium lilyminium Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, this breaks filtering using the CurationWorkflow as seen in https://github.com/openforcefield/openff-sage/blob/a6a061ba8ebc144e3fda46f5f7a9e7a352b9cd2f/data-set-curation/physical-property/optimizations/curate-training-set.py#L103-L173 . Any reason it was set to "before"? "after" works for me.

I think it's because validation is run before the type attribute (Literal) gets a chance to distinguish different classes -- I wind up with errors like the below:

(Ignore the speculation above, I'm tired and not thinking clearly.)

2025-07-31 16:45:18,828 - __main__ - INFO - Starting at Thu Jul 31 16:45:18 2025
2025-07-31 16:45:34,560 - __main__ - INFO - Loading 9216 data
Traceback (most recent call last):
  File "/Users/lily/pydev/ash-sage-rc2/01_download-data/physprop/final/filter-data-training.py", line 264, in <module>
    main()
  File "/Users/lily/micromamba/envs/evaluator-050/lib/python3.11/site-packages/click/core.py", line 1442, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/lily/micromamba/envs/evaluator-050/lib/python3.11/site-packages/click/core.py", line 1363, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/Users/lily/micromamba/envs/evaluator-050/lib/python3.11/site-packages/click/core.py", line 1226, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/lily/micromamba/envs/evaluator-050/lib/python3.11/site-packages/click/core.py", line 794, in invoke
    return callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/lily/pydev/ash-sage-rc2/01_download-data/physprop/final/filter-data-training.py", line 239, in main
    training_set_frame = curate_data_set(
                         ^^^^^^^^^^^^^^^^
  File "/Users/lily/pydev/ash-sage-rc2/01_download-data/physprop/final/filter-data-training.py", line 161, in curate_data_set
    curation_schema = CurationWorkflowSchema(
                      ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/lily/micromamba/envs/evaluator-050/lib/python3.11/site-packages/pydantic/main.py", line 253, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/lily/pydev/openff-evaluator/openff/evaluator/datasets/curation/components/filtering.py", line 798, in _validate_mutually_exclusive
    smirks_to_include = values.get("smirks_to_include")
                        ^^^^^^^^^^
  File "/Users/lily/micromamba/envs/evaluator-050/lib/python3.11/site-packages/pydantic/main.py", line 989, in __getattr__
    raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')
AttributeError: 'FilterByEnvironmentsSchema' object has no attribute 'get'

@classmethod
def _validate_mutually_exclusive(cls, values):
smirks_to_include = values.get("smirks_to_include")
smirks_to_exclude = values.get("smirks_to_exclude")
Expand Down Expand Up @@ -980,7 +967,8 @@ class FilterBySubstancesSchema(CurationComponentSchema):
"This option is mutually exclusive with `substances_to_include`.",
)

@root_validator
@model_validator(mode="before")
@classmethod
def _validate_mutually_exclusive(cls, values):
substances_to_include = values.get("substances_to_include")
substances_to_exclude = values.get("substances_to_exclude")
Expand Down Expand Up @@ -1099,10 +1087,11 @@ def _validate_per_component_environments(cls, value):
assert all(len(y) == x for x, y in value.items())
return value

@root_validator
@model_validator(mode="after")
@classmethod
def _validate_mutually_exclusive(cls, values):
at_least_one_environment = values.get("at_least_one_environment")
strictly_specified_environments = values.get("strictly_specified_environments")
at_least_one_environment = values.at_least_one_environment
strictly_specified_environments = values.strictly_specified_environments

assert (
at_least_one_environment is True or strictly_specified_environments is True
Expand All @@ -1112,8 +1101,8 @@ def _validate_mutually_exclusive(cls, values):
or strictly_specified_environments is False
)

per_component_environments = values.get("per_component_environments")
environments = values.get("environments")
per_component_environments = values.per_component_environments
environments = values.environments

assert per_component_environments is not None or environments is not None
assert per_component_environments is None or environments is None
Expand Down
10 changes: 3 additions & 7 deletions openff/evaluator/datasets/curation/components/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

import numpy
import pandas
from pydantic import BaseModel, Field, PositiveInt, validator
from typing_extensions import Literal

from openff.evaluator._pydantic import BaseModel, Field, conlist, validator
from openff.evaluator.datasets.curation.components import (
CurationComponent,
CurationComponentSchema,
Expand All @@ -28,16 +28,11 @@
PropertyType = Tuple[str, int]

if TYPE_CHECKING:
PositiveInt = int

try:
from openeye.oegraphsim import OEFingerPrint
except ImportError:
OEFingerPrint = None

else:
from openff.evaluator._pydantic import PositiveInt


class State(BaseModel):
temperature: float = Field(..., description="The temperature (K) of interest.")
Expand Down Expand Up @@ -74,10 +69,11 @@ class FingerPrintType(Enum):
class SelectSubstancesSchema(CurationComponentSchema):
type: Literal["SelectSubstances"] = "SelectSubstances"

target_environments: conlist(ChemicalEnvironment, min_items=1) = Field(
target_environments: list[ChemicalEnvironment] = Field(
...,
description="The chemical environments which selected substances should "
"contain.",
min_length=1,
)

n_per_environment: PositiveInt = Field(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

import pandas
import requests
from pydantic import Field, HttpUrl
from typing_extensions import Literal

from openff.evaluator._pydantic import Field, HttpUrl
from openff.evaluator.datasets.curation.components import (
CurationComponent,
CurationComponentSchema,
Expand Down
2 changes: 1 addition & 1 deletion openff/evaluator/datasets/curation/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

import numpy
import pandas
from pydantic import BaseModel, Field

from openff.evaluator._pydantic import BaseModel, Field
from openff.evaluator.datasets import PhysicalPropertyDataSet
from openff.evaluator.datasets.curation.components import CurationComponent
from openff.evaluator.datasets.curation.components.conversion import (
Expand Down
Loading