Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c7c4687
Implement minor fixes to dummy regressor to enable reuse in comittee …
smcolby Feb 26, 2026
611b644
Refactor tests such that they are "unit" rather than "integration"
smcolby Feb 26, 2026
d5a9e8f
Add additional tests for active learning modules
smcolby Feb 26, 2026
28f083b
Add pytest-mock dependency
smcolby Feb 26, 2026
518aa1e
Add instructions to avoid common testing pitfalls
smcolby Feb 26, 2026
6a06a2a
Fix data alignment bug in feature concatenation
smcolby Feb 26, 2026
0dacf83
Refactor test suite from integration-style to lightweight unit tests.
smcolby Feb 26, 2026
1df313a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 26, 2026
45ef511
Add documentation to unit tests
smcolby Feb 26, 2026
d9732b0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 26, 2026
f517107
Add testing for different splits and ensemble
smcolby Feb 26, 2026
b86f1fe
Remove parent_spec from workflow base and add grouped runtime kwargs
smcolby Feb 27, 2026
deecfdb
Replace parent_spec access with model/ensemble/feat kwargs and keep r…
smcolby Feb 27, 2026
b0f8df2
Move provenance YAML export to AnvilSpecification.run and align tag/o…
smcolby Feb 27, 2026
a3e1c06
Invoke specification.run for anvil orchestration
smcolby Feb 27, 2026
da12154
Update workflow/spec tests for kwargs wiring, provenance ownership, a…
smcolby Feb 27, 2026
9cf406a
Update anvil CLI assertion path to spec.run and normalize output_dir …
smcolby Feb 27, 2026
7670ab9
Add code-first tests for workflows
smcolby Feb 27, 2026
e832f07
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 27, 2026
b49063c
Redo anvil unit tests
smcolby Feb 27, 2026
4b0b310
Move from excessive dummy implmentations to surgical mocks
smcolby Feb 27, 2026
dc1a0eb
Refactor workflow base unit tests to use dynamic autospecs
smcolby Feb 27, 2026
5a3e471
Add validator for serial and param path(s)
smcolby Feb 28, 2026
d4f4f06
Refactor unit tests
smcolby Feb 28, 2026
ef96db2
Remove legacy tests
smcolby Feb 28, 2026
10dbfdc
Fix formatting
smcolby Feb 28, 2026
2138632
Update docstring
smcolby Feb 28, 2026
feaa77c
Replace mocked inference components with real objects
smcolby Feb 28, 2026
97441ef
Merge branch 'main' into remove-parent-spec
smcolby Feb 28, 2026
5b5f181
Replace mocker.create_autospec with real components in test_workflow_…
smcolby Feb 28, 2026
ec9a226
Reference `self._n_tasks` in both places
smcolby Feb 28, 2026
9ba404c
Remove use of MagicMock
smcolby Feb 28, 2026
91e3526
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 28, 2026
c76d6f8
Handle `use_bagging` class-external field properly
smcolby Feb 28, 2026
62ab44c
Harden random seed usage for bootstrap and model initialization
smcolby Feb 28, 2026
9890e49
Add defaults to `FeatureSpec` for class-external fields in case unspe…
smcolby Feb 28, 2026
878e887
Set default calibration method to `None`
smcolby Feb 28, 2026
9418a14
Reduce ensemble members to 2 to speed up tests
smcolby Feb 28, 2026
2ea103e
Remove kwargs get fallback for calibration method (handled in spec)
smcolby Feb 28, 2026
755ddf2
Guard against zero stdev with small epsilon value
smcolby Feb 28, 2026
8192916
[pre-commit.ci] pre-commit autoupdate (#501)
pre-commit-ci[bot] Mar 2, 2026
4f6f60b
Extract ensure_2d helper and replace duplicate shape coercion blocks
dwwest Mar 6, 2026
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
10 changes: 10 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,13 @@ The CLI entry point is `openadmet` (`openadmet/models/cli/cli.py`), with subcomm
- Ruff + Black formatting; isort with Black-compatible profile
- Sentence case in comments and print statements; acronyms (MPNN, MVE, ADMET, FFN) stay capitalized
- Do not number steps in comments; do not end comments with a period

## Unit Testing & Refactoring Rules

When writing or refactoring tests, you must strictly adhere to the following guidelines to ensure tests are mathematically sound, robust, and non-tautological:

* **Avoid Tautological Mocks:** Do not mock the system under test. Mock heavy I/O, external dependencies, or heavy data loading, but ensure the core logic of the target function is actually executed. Use lightweight synthetic datasets (e.g., small tensors or pandas DataFrames) instead of bypassing the execution entirely.
* **Standard Mocking:** Never write custom nested dummy classes or custom mock fixtures. Always use the standard `pytest-mock` library (the `mocker` fixture) to patch objects and verify calls.
* **No Lazy Assertions:** Never use `assert True`. Assert actual state changes, specific dictionary keys, object types (e.g., `isinstance(obj, matplotlib.figure.Figure)`), or verify file creation via the `tmp_path` fixture.
* **Robust ML Data Testing:** When testing data splitters or clustering algorithms, you must explicitly assert that the resulting train/validation/test sets are mutually exclusive (e.g., checking that set intersections of indices or arrays are empty). Ensure synthetic testing data has enough variance (e.g., diverse SMILES scaffolds) to meaningfully test the algorithm.
* **Safe Floating-Point Math:** Never use strict equality (`==`) to compare floating-point numbers. Always use `pytest.approx()` or `numpy.testing.assert_almost_equal()` to prevent cross-platform precision failures. Assert the actual math (e.g., UQ or metric calculations), not just the existence of the output.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ repos:
- id: black
files: ^openadmet_models
- repo: https://github.com/PyCQA/isort
rev: 8.0.0
rev: 8.0.1
hooks:
- id: isort
files: ^openadmet_models
Expand All @@ -37,7 +37,7 @@ repos:
- --py39-plus
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.2
rev: v0.15.4
hooks:
# Run the linter.
- id: ruff-check
Expand Down
1 change: 1 addition & 0 deletions devtools/conda-envs/openadmet-models-gpu.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies:
- pytorch_scatter
- pytorch_sparse
- pytest
- pytest-mock
- pytest-cov
- pytest-xdist
- rdkit
Expand Down
1 change: 1 addition & 0 deletions devtools/conda-envs/openadmet-models.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies:
- pytorch_scatter
- pytorch_sparse
- pytest
- pytest-mock
- pytest-cov
- pytest-xdist
- rdkit
Expand Down
4 changes: 2 additions & 2 deletions openadmet/models/active_learning/committee.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,8 +382,8 @@ def _predict(self, X, return_std=False, **kwargs):
if return_std is False:
return mean

# Compute standard deviation
std = np.std(preds, axis=-1)
# Compute standard deviation, guard against zero std
std = np.maximum(np.std(preds, axis=-1), 1e-8)

# Calibrate std if calibration model is available
if self.calibrated:
Expand Down
73 changes: 68 additions & 5 deletions openadmet/models/anvil/specification.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
from openadmet.models.active_learning.ensemble_base import (
get_ensemble_class,
)
from openadmet.models.drivers import DriverType
from openadmet.models.architecture.model_base import get_mod_class
from openadmet.models.drivers import DriverType
from openadmet.models.eval.eval_base import get_eval_class
from openadmet.models.features.feature_base import get_featurizer_class
from openadmet.models.registries import * # noqa: F401, F403
Expand Down Expand Up @@ -449,9 +449,23 @@ class SplitSpec(AnvilSection):


class FeatureSpec(AnvilSection):
"""Featurization specification."""
"""
Featurization specification.

Attributes
----------
section_name : ClassVar[str]
The name of the section.
type : Optional[str]
The type of featurizer to use.
params : dict
The parameters for the featurizer.

"""

section_name: ClassVar[str] = "feat"
type: str | None = None
params: dict = Field(default_factory=dict)


class ModelSpec(AnvilSection):
Expand Down Expand Up @@ -546,8 +560,8 @@ class EnsembleSpec(AnvilSection):

section_name: ClassVar[str] = "ensemble"
n_models: int
calibration_method: str | None = "isotonic-regression"
use_bagging: bool = True
calibration_method: str | None = None
use_bagging: bool = False
param_paths: list[str] | None = None
serial_paths: list[str] | None = None

Expand Down Expand Up @@ -729,6 +743,26 @@ def to_workflow(self):
# Pull driver from associated trainer to choose the correct workflow
trainer_class = self.procedure.train.to_class()
driver = _DRIVER_TO_CLASS[trainer_class._driver_type]
model_kwargs = {
"param_path": self.procedure.model.param_path,
"serial_path": self.procedure.model.serial_path,
"freeze_weights": self.procedure.model.freeze_weights,
}
ensemble_kwargs = (
{
"n_models": self.procedure.ensemble.n_models,
"calibration_method": self.procedure.ensemble.calibration_method,
"param_paths": self.procedure.ensemble.param_paths,
"serial_paths": self.procedure.ensemble.serial_paths,
"use_bagging": self.procedure.ensemble.use_bagging,
}
if self.procedure.ensemble
else {}
)
feat_kwargs = {
"type": self.procedure.feat.type,
"params": self.procedure.feat.params,
}

return driver(
metadata=self.metadata,
Expand All @@ -744,5 +778,34 @@ def to_workflow(self):
feat=self.procedure.feat.to_class(),
trainer=self.procedure.train.to_class(),
evals=[eval.to_class() for eval in self.report.eval],
parent_spec=self,
model_kwargs=model_kwargs,
ensemble_kwargs=ensemble_kwargs,
feat_kwargs=feat_kwargs,
)

def run(
self,
output_dir: PathLike = "anvil_training",
debug: bool = False,
tag: str = None,
):
"""Run the Anvil workflow from this specification."""
workflow = self.to_workflow()
result = workflow.run(output_dir=output_dir, debug=debug, tag=tag)

resolved_output_dir = workflow.resolved_output_dir or Path(output_dir)
resolved_output_dir.mkdir(parents=True, exist_ok=True)
provenance_spec = self.model_copy(deep=True)
if tag is not None:
provenance_spec.metadata.tag = tag

provenance_spec.to_recipe(resolved_output_dir / "anvil_recipe.yaml")
recipe_components = resolved_output_dir / "recipe_components"
recipe_components.mkdir(parents=True, exist_ok=True)
provenance_spec.to_multi_yaml(
metadata_yaml=recipe_components / "metadata.yaml",
procedure_yaml=recipe_components / "procedure.yaml",
data_yaml=recipe_components / "data.yaml",
report_yaml=recipe_components / "eval.yaml",
)
return result
Loading