Skip to content
Open
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
2 changes: 0 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
# ase needed to get FrechetCellFilter used by ML force fields
pip install git+https://gitlab.com/ase/ase
pip install .[strict,docs]

- name: Build
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
# ase needed to get FrechetCellFilter used by ML force fields
pip install git+https://gitlab.com/ase/ase
pip install .[strict,docs]

- name: Build
Expand Down
24 changes: 19 additions & 5 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ jobs:

- uses: pre-commit/action@v3.0.0

test:
test-non-ase:
# prevent this action from running on forks
if: github.repository == 'materialsproject/atomate2'

services:
local_mongodb:
image: mongo:4.0
Expand All @@ -37,6 +40,7 @@ jobs:
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
split: [1, 2, 3]

steps:
- name: Check out repo
Expand Down Expand Up @@ -72,20 +76,30 @@ jobs:
micromamba activate a2
uv pip install --upgrade 'git+https://github.com/materialsproject/pymatgen@${{ github.event.client_payload.pymatgen_ref }}'

- name: Test
- name: Test split ${{ matrix.split }}
env:
MP_API_KEY: ${{ secrets.MP_API_KEY }}

# regenerate durations file with `pytest --store-durations --durations-path tests/.pytest-split-durations`
# Note the use of `--splitting-algorithm least_duration`.
# This helps prevent a test split having no tests to run, and then the GH action failing, see:
# https://github.com/jerry-git/pytest-split/issues/95
# However this `splitting-algorithm` means that tests cannot depend sensitively on the order they're executed in.
run: |
micromamba activate a2
pytest --ignore=tests/ase --cov=atomate2 --cov-report=xml
pytest --splits 3 --group ${{ matrix.split }} --durations-path tests/.pytest-split-durations --splitting-algorithm least_duration --ignore=tests/ase --cov=atomate2 --cov-report=xml

- uses: codecov/codecov-action@v1
if: matrix.python-version == '3.10' && github.repository == 'materialsproject/atomate2'
with:
token: ${{ secrets.CODECOV_TOKEN }}
name: coverage${{ matrix.split }}
file: ./coverage.xml

test-notebooks-and-ase:
# prevent this action from running on forks
if: github.repository == 'materialsproject/atomate2'

# It seems like anything torch-dependent and tblite can't be installed in the same environment
# without the tblite tests failing in CI, see, e.g.:
# https://github.com/tblite/tblite/issues/116
Expand Down Expand Up @@ -143,7 +157,7 @@ jobs:
MP_API_KEY: ${{ secrets.MP_API_KEY }}
run: |
micromamba activate a2
pytest --cov=atomate2 --cov-report=xml tests/ase
pytest --splits 1 --group 1 --cov=atomate2 --cov-report=xml tests/ase

- uses: codecov/codecov-action@v1
if: matrix.python-version == '3.10' && github.repository == 'materialsproject/atomate2'
Expand Down Expand Up @@ -172,7 +186,7 @@ jobs:
run: sphinx-build docs docs_build

automerge:
needs: [lint, test, docs]
needs: [lint, test-non-ase, test-notebooks-and-ase, docs]
runs-on: ubuntu-latest

permissions:
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Change log

## v0.0.17

### Bug Fixes 🐛

* Fix `prev_dir` behavior in input set generator of `MPGGAStaticMaker` by @Andrew-S-Rosen in https://github.com/materialsproject/atomate2/pull/996

### Documentation 📖

* Bump min supported Python to 3.10 by @janosh in https://github.com/materialsproject/atomate2/pull/992

### House-Keeping 🧹

* Excise openff dependency from OpenMM testing by @orionarcher in https://github.com/materialsproject/atomate2/pull/993
* Use `pytest-split` to parallelize across 3 runners and speedup CI by @esoteric-ephemera in https://github.com/materialsproject/atomate2/pull/985

**Full Changelog**: https://github.com/materialsproject/atomate2/compare/v0.0.16...v0.0.17

## v0.0.16

This release brings lots of new workflows and support for all ASE calculators.
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ tests = [
"nbmake==1.5.4",
"pytest-cov==5.0.0",
"pytest-mock==3.14.0",
"pytest-split==0.9.0",
"pytest==8.3.3",
]
strict = [
Expand Down
10 changes: 7 additions & 3 deletions src/atomate2/common/jobs/mpmorph.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,11 @@ def get_average_volume_from_icsd(
def get_entry_from_dict(chem_env: str) -> dict | None:
data = icsd_avg_vols[icsd_avg_vols["chem_env"] == chem_env]
data = data[
data["with_oxi"]
if (not ignore_oxi_states and len(data[data["with_oxi"]]) > 0)
else ~data["with_oxi"]
(
data["with_oxi"]
if (not ignore_oxi_states and len(data[data["with_oxi"]]) > 0)
else ~data["with_oxi"]
)
]
if len(data) > 0:
return {k: data[k].squeeze() for k in ("avg_vol", "count")}
Expand Down Expand Up @@ -315,6 +317,8 @@ def get_random_packed_structure(
)
if isinstance(composition, str):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would probably just change these following lines to:

if isinstance(composition, (str,dict)):
    composition = Composition(composition)

Copy link
Copy Markdown
Author

@orionarcher orionarcher Oct 1, 2024

Choose a reason for hiding this comment

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

Agreed, that's much cleaner.

composition = Composition(composition)
if isinstance(composition, dict):
composition = Composition.from_dict(composition)

struct_db = (
vol_per_atom_source.lower() if isinstance(vol_per_atom_source, str) else None
Expand Down
4 changes: 2 additions & 2 deletions src/atomate2/openmm/flows/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import TYPE_CHECKING

from emmet.core.openmm import Calculation, OpenMMInterchange, OpenMMTaskDocument
from jobflow import Flow, Job, Response
from jobflow import Flow, Job, Maker, Response
from monty.json import MontyDecoder, MontyEncoder

from atomate2.openmm.jobs.base import openmm_job
Expand Down Expand Up @@ -68,7 +68,7 @@ def collect_outputs(


@dataclass
class OpenMMFlowMaker:
class OpenMMFlowMaker(Maker):
"""Run a production simulation.

This flexible flow links together any flows of OpenMM jobs in
Expand Down
18 changes: 13 additions & 5 deletions src/atomate2/openmm/jobs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ def make(

# Run the simulation
start = time.time()
self.run_openmm(sim)
self.run_openmm(sim, dir_name)
elapsed_time = time.time() - start

self._update_interchange(interchange, sim, prev_task)
Expand Down Expand Up @@ -303,6 +303,7 @@ def _add_reporters(
traj_file_name = self._resolve_attr("traj_file_name", prev_task)
traj_file_type = self._resolve_attr("traj_file_type", prev_task)
report_velocities = self._resolve_attr("report_velocities", prev_task)
wrap_traj = self._resolve_attr("wrap_traj", prev_task)

if has_steps & (traj_interval > 0):
writer_kwargs = {}
Expand All @@ -327,7 +328,7 @@ def _add_reporters(
kwargs = dict(
file=str(dir_name / f"{self.traj_file_name}.{traj_file_type}"),
reportInterval=traj_interval,
enforcePeriodicBox=self._resolve_attr("wrap_traj", prev_task),
enforcePeriodicBox=wrap_traj,
)
if report_velocities:
# assert package version
Expand Down Expand Up @@ -364,7 +365,7 @@ def _add_reporters(
)
sim.reporters.append(state_reporter)

def run_openmm(self, simulation: Simulation) -> NoReturn:
def run_openmm(self, sim: Simulation, dir_name: Path) -> NoReturn:
"""Abstract method for running the OpenMM simulation.

This method should be implemented by subclasses to
Expand Down Expand Up @@ -524,6 +525,11 @@ def _update_interchange(
interchange.box = state.getPeriodicBoxVectors(asNumpy=True)
elif isinstance(interchange, OpenMMInterchange):
interchange.state = XmlSerializer.serialize(state)
else:
raise TypeError(
f"Interchange must be an Interchange or "
f"OpenMMInterchange object, got {type(interchange).__name__}"
)

def _create_structure(
self, sim: Simulation, prev_task: OpenMMTaskDocument | None = None
Expand Down Expand Up @@ -607,8 +613,10 @@ def _create_task_doc(

prev_task = prev_task or OpenMMTaskDocument()

interchange_json = interchange.json()
# interchange_bytes = interchange_json.encode("utf-8")
if isinstance(interchange, Interchange):
interchange_json = interchange.json()
else:
interchange_json = interchange.model_dump_json()

return OpenMMTaskDocument(
tags=tags,
Expand Down
33 changes: 29 additions & 4 deletions src/atomate2/openmm/jobs/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@

import numpy as np
from openmm import Integrator, LangevinMiddleIntegrator, MonteCarloBarostat
from openmm.app import StateDataReporter
from openmm.unit import atmosphere, kelvin, kilojoules_per_mole, nanometer, picoseconds

from atomate2.openmm.jobs.base import BaseOpenMMMaker
from atomate2.openmm.utils import create_list_summing_to

if TYPE_CHECKING:
from pathlib import Path

from emmet.core.openmm import OpenMMTaskDocument
from openmm.app import Simulation

Expand Down Expand Up @@ -41,7 +44,7 @@ class EnergyMinimizationMaker(BaseOpenMMMaker):
tolerance: float = 10
max_iterations: int = 0

def run_openmm(self, sim: Simulation) -> None:
def run_openmm(self, sim: Simulation, dir_name: Path) -> None:
"""Run the energy minimization with OpenMM.

This method performs energy minimization on the molecular system using
Expand All @@ -62,6 +65,28 @@ def run_openmm(self, sim: Simulation) -> None:
maxIterations=self.max_iterations,
)

if self.state_interval > 0:
state = sim.context.getState(
getPositions=True,
getVelocities=True,
getForces=True,
getEnergy=True,
enforcePeriodicBox=self.wrap_traj,
)

state_reporter = StateDataReporter(
file=f"{dir_name / self.state_file_name}.csv",
reportInterval=0,
step=True,
potentialEnergy=True,
kineticEnergy=True,
totalEnergy=True,
temperature=True,
volume=True,
density=True,
)
state_reporter.report(sim, state)


@dataclass
class NPTMaker(BaseOpenMMMaker):
Expand All @@ -87,7 +112,7 @@ class NPTMaker(BaseOpenMMMaker):
pressure: float = 1
pressure_update_frequency: int = 10

def run_openmm(self, sim: Simulation) -> None:
def run_openmm(self, sim: Simulation, dir_name: Path) -> None:
"""Evolve the simulation for self.n_steps in the NPT ensemble.

This adds a Monte Carlo barostat to the system to put it into NPT, runs the
Expand Down Expand Up @@ -138,7 +163,7 @@ class NVTMaker(BaseOpenMMMaker):
name: str = "nvt simulation"
n_steps: int = 1_000_000

def run_openmm(self, sim: Simulation) -> None:
def run_openmm(self, sim: Simulation, dir_name: Path) -> None:
"""Evolve the simulation with OpenMM for self.n_steps.

Parameters
Expand Down Expand Up @@ -177,7 +202,7 @@ class TempChangeMaker(BaseOpenMMMaker):
temp_steps: int | None = None
starting_temperature: float | None = None

def run_openmm(self, sim: Simulation) -> None:
def run_openmm(self, sim: Simulation, dir_name: Path) -> None:
"""Evolve the simulation while gradually changing the temperature.

self.temperature is the final temperature. self.temp_steps
Expand Down
39 changes: 39 additions & 0 deletions src/atomate2/openmm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@

from __future__ import annotations

import io
import re
import tempfile
import time
import warnings
from pathlib import Path
from typing import TYPE_CHECKING

import numpy as np
import openmm.unit as omm_unit
from emmet.core.openmm import OpenMMInterchange
from openmm import LangevinMiddleIntegrator, XmlSerializer
from openmm.app import PDBFile

if TYPE_CHECKING:
from emmet.core.openmm import OpenMMTaskDocument
from openff.interchange import Interchange


def download_opls_xml(
Expand Down Expand Up @@ -132,3 +140,34 @@ def task_reports(task: OpenMMTaskDocument, traj_or_state: str = "traj") -> bool:
else:
raise ValueError("traj_or_state must be 'traj' or 'state'")
return calc_input.n_steps >= report_freq


def openff_to_openmm_interchange(
openff_interchange: Interchange,
) -> OpenMMInterchange:
"""Convert an OpenFF Interchange object to an OpenMM Interchange object."""
integrator = LangevinMiddleIntegrator(
300 * omm_unit.kelvin,
10.0 / omm_unit.picoseconds,
1.0 * omm_unit.femtoseconds,
)
sim = openff_interchange.to_openmm_simulation(integrator)
state = sim.context.getState(
getPositions=True,
getVelocities=True,
enforcePeriodicBox=True,
)
with io.StringIO() as buffer:
PDBFile.writeFile(
sim.topology,
np.zeros(shape=(sim.topology.getNumAtoms(), 3)),
file=buffer,
)
buffer.seek(0)
pdb = buffer.read()

return OpenMMInterchange(
system=XmlSerializer.serialize(sim.system),
state=XmlSerializer.serialize(state),
topology=pdb,
)
10 changes: 8 additions & 2 deletions src/atomate2/vasp/jobs/mp.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ class MPGGARelaxMaker(BaseVaspMaker):

name: str = "MP GGA relax"
input_set_generator: VaspInputGenerator = field(
default_factory=lambda: MPRelaxSet(force_gamma=True, auto_metal_kpoints=True)
default_factory=lambda: MPRelaxSet(
force_gamma=True, auto_metal_kpoints=True, inherit_incar=False
)
)


Expand Down Expand Up @@ -90,7 +92,9 @@ class MPGGAStaticMaker(BaseVaspMaker):

name: str = "MP GGA static"
input_set_generator: VaspInputGenerator = field(
default_factory=lambda: MPStaticSet(force_gamma=True, auto_metal_kpoints=True)
default_factory=lambda: MPStaticSet(
force_gamma=True, auto_metal_kpoints=True, inherit_incar=False
)
)


Expand Down Expand Up @@ -127,6 +131,7 @@ class MPPreRelaxMaker(BaseVaspMaker):
input_set_generator: VaspInputGenerator = field(
default_factory=lambda: MPScanRelaxSet(
auto_ismear=False,
inherit_incar=False,
user_incar_settings={
"EDIFFG": -0.05,
"GGA": "PS",
Expand Down Expand Up @@ -172,6 +177,7 @@ class MPMetaGGARelaxMaker(BaseVaspMaker):
input_set_generator: VaspInputGenerator = field(
default_factory=lambda: MPScanRelaxSet(
auto_ismear=False,
inherit_incar=False,
user_incar_settings={
"GGA": None, # unset GGA, shouldn't be set anyway but best be sure
"LCHARG": True,
Expand Down
Loading