diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1aec2e5..e39eecd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.7 + rev: v0.1.9 hooks: - id: ruff args: [--fix] @@ -21,7 +21,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 + rev: v1.8.0 hooks: - id: mypy diff --git a/README.md b/README.md index cd07120..78b0b63 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@

- MatCalc logo + MatCalc logo
MatCalc

+

+ [![GitHub license](https://img.shields.io/github/license/materialsvirtuallab/matcalc)](https://github.com/materialsvirtuallab/matcalc/blob/main/LICENSE) [![Linting](https://github.com/materialsvirtuallab/matcalc/workflows/Linting/badge.svg)](https://github.com/materialsvirtuallab/matcalc/workflows/Linting/badge.svg) [![Testing](https://github.com/materialsvirtuallab/matcalc/workflows/Testing/badge.svg)](https://github.com/materialsvirtuallab/matcalc/workflows/Testing/badge.svg) @@ -10,23 +12,25 @@ [![Requires Python 3.8+](https://img.shields.io/badge/Python-3.8+-blue.svg?logo=python&logoColor=white)](https://python.org/downloads) [![PyPI](https://img.shields.io/pypi/v/matcalc?logo=pypi&logoColor=white)](https://pypi.org/project/matcalc?logo=pypi&logoColor=white) +

+ ## Docs [materialsvirtuallab.github.io/matcalc](https://materialsvirtuallab.github.io/matcalc) ## Introduction -MatCalc is a Python library for calculating materials properties from the potential energy surface (PES). The -PES can be from DFT or, more commonly, from machine learning interatomic potentials (MLIPs). +MatCalc is a Python library for calculating material properties from the potential energy surface (PES). The +PES can come from DFT or, more commonly, from machine learning interatomic potentials (MLIPs). -Calculating various materials properties can require relatively involved setup of various simulation codes. The +Calculating material properties often requires involved setups of various simulation codes. The goal of MatCalc is to provide a simplified, consistent interface to access these properties with any parameterization of the PES. ## Outline The main base class in MatCalc is `PropCalc` (property calculator). [All `PropCalc` subclasses](https://github.com/search?q=repo%3Amaterialsvirtuallab%2Fmatcalc%20%22(PropCalc)%22) should implement a -`calc(pymatgen.Structure) -> dict` method that returns a dict of properties. +`calc(pymatgen.Structure) -> dict` method that returns a dictionary of properties. In general, `PropCalc` should be initialized with an ML model or ASE calculator, which is then used by either ASE, LAMMPS or some other simulation code to perform calculations of properties. diff --git a/matcalc/base.py b/matcalc/base.py index 623951c..49e0a5e 100644 --- a/matcalc/base.py +++ b/matcalc/base.py @@ -2,7 +2,7 @@ from __future__ import annotations import abc -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from joblib import Parallel, delayed @@ -29,7 +29,7 @@ def calc(self, structure: Structure) -> dict: """ def calc_many( - self, structures: Sequence[Structure], n_jobs: None | int = None, **kwargs + self, structures: Sequence[Structure], n_jobs: None | int = None, **kwargs: Any ) -> Generator[dict, None, None]: """ Performs calc on many structures. The return type is a generator given that the calc method can potentially be diff --git a/matcalc/elasticity.py b/matcalc/elasticity.py index cd07ff2..5d1c60e 100644 --- a/matcalc/elasticity.py +++ b/matcalc/elasticity.py @@ -15,6 +15,7 @@ from collections.abc import Sequence from ase.calculators.calculator import Calculator + from numpy.typing import ArrayLike from pymatgen.core import Structure @@ -48,11 +49,11 @@ def __init__( self.norm_strains = tuple(np.array([1]) * np.asarray(norm_strains)) self.shear_strains = tuple(np.array([1]) * np.asarray(shear_strains)) if len(self.norm_strains) == 0: - raise ValueError("norm_strains must be nonempty") + raise ValueError("norm_strains is empty") if len(self.shear_strains) == 0: - raise ValueError("shear_strains must be nonempty") + raise ValueError("shear_strains is empty") if 0 in self.norm_strains or 0 in self.shear_strains: - raise ValueError("Strains must be nonzero") + raise ValueError("strains must be non-zero") self.relax_structure = relax_structure self.fmax = fmax if len(self.norm_strains) > 1 and len(self.shear_strains) > 1: @@ -112,11 +113,11 @@ def calc(self, structure: Structure) -> dict[str, Any]: def _elastic_tensor_from_strains( self, - strains, - stresses, - eq_stress=None, + strains: ArrayLike, + stresses: ArrayLike, + eq_stress: ArrayLike = None, tol: float = 1e-7, - ): + ) -> tuple[ElasticTensor, float]: """ Slightly modified version of Pymatgen function pymatgen.analysis.elasticity.elastic.ElasticTensor.from_independent_strains; diff --git a/matcalc/neb.py b/matcalc/neb.py index 3681658..e069166 100644 --- a/matcalc/neb.py +++ b/matcalc/neb.py @@ -2,7 +2,7 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from ase.io import Trajectory from ase.neb import NEB, NEBTools @@ -27,8 +27,8 @@ def __init__( traj_folder: str | None = None, interval: int = 1, climb: bool = True, - **kwargs, - ): + **kwargs: Any, + ) -> None: """ Args: images(list): A list of pymatgen structures as NEB image structures. @@ -65,8 +65,8 @@ def from_end_images( n_images: int = 7, interpolate_lattices: bool = False, autosort_tol: float = 0.5, - **kwargs, - ): + **kwargs: Any, + ) -> NEBCalc: """ Initialize a NEBCalc from end images. @@ -94,7 +94,7 @@ def from_end_images( def calc( # type: ignore[override] self, fmax: float = 0.1, max_steps: int = 1000 - ) -> float: + ) -> tuple[float, float]: """ Perform NEB calculation. diff --git a/matcalc/utils.py b/matcalc/utils.py index 726de2d..4822c46 100644 --- a/matcalc/utils.py +++ b/matcalc/utils.py @@ -4,7 +4,7 @@ import functools from inspect import isclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import ase.optimize from ase.optimize.optimize import Optimizer @@ -23,7 +23,7 @@ @functools.lru_cache -def get_universal_calculator(name: str | Calculator, **kwargs) -> Calculator: +def get_universal_calculator(name: str | Calculator, **kwargs: Any) -> Calculator: """ Helper method to get some well-known **universal** calculators. Imports should be inside if statements to ensure that all models are optional dependencies. diff --git a/pyproject.toml b/pyproject.toml index b11b9cc..87f3a8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,51 +51,34 @@ packages = ["matcalc"] [tool.ruff] target-version = "py39" line-length = 120 -select = [ - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "D", # pydocstyle - "E", # pycodestyle error - "EXE", # flake8-executable - "F", # pyflakes - "I", # isort - "ICN", # flake8-import-conventions - "ISC", # flake8-implicit-str-concat - "PD", # pandas-vet - "PERF", # perflint - "PIE", # flake8-pie - "PL", # pylint - "PT", # flake8-pytest-style - "PYI", # flakes8-pyi - "Q", # flake8-quotes - "RET", # flake8-return - "RSE", # flake8-raise - "RUF", # Ruff-specific rules - "SIM", # flake8-simplify - "SLOT", # flake8-slots - "TCH", # flake8-type-checking - "TID", # tidy imports - "TID", # flake8-tidy-imports - "UP", # pyupgrade - "W", # pycodestyle warning - "YTT", # flake8-2020 -] +select = ["ALL"] ignore = [ + "ANN101", + "ANN102", + "ANN401", "B019", # functools.lru_cache on methods can lead to memory leaks + "COM812", # trailing comma missing "D105", # Missing docstring in magic method "D205", # 1 blank line required between summary line and description "D212", # Multi-line docstring summary should start at the first line + "EM101", + "EM102", + "FBT001", + "FBT002", "PLR", # pylint refactor "PLW0603", # Using the global statement to update variables is discouraged + "PTH", # prefer Path to os.path "SIM105", # Use contextlib.suppress(OSError) instead of try-except-pass + "TRY003", ] +exclude = ["docs/conf.py"] pydocstyle.convention = "google" isort.required-imports = ["from __future__ import annotations"] [tool.ruff.per-file-ignores] "__init__.py" = ["F401"] -"tasks.py" = ["D"] -"tests/*" = ["D"] +"tasks.py" = ["ANN", "D", "T203"] +"tests/*" = ["D", "INP001", "N802", "N803", "S101"] [tool.pytest.ini_options] addopts = "--durations=30 --quiet -rXs --color=yes -p no:warnings" diff --git a/tasks.py b/tasks.py index 9f308be..57bd4bc 100644 --- a/tasks.py +++ b/tasks.py @@ -67,8 +67,6 @@ def make_docs(ctx): ctx.run("cp ../README.md index.md", warn=True) ctx.run("rm matcalc.*.rst", warn=True) ctx.run("sphinx-apidoc -P -M -d 6 -o . -f ../matcalc") - # ctx.run("rm matcalc*.html", warn=True) - # ctx.run("sphinx-build -b html . ../docs") # HTML building. ctx.run("cp modules.rst index.rst") ctx.run("sphinx-build -M markdown . .") ctx.run("rm *.rst", warn=True) @@ -110,7 +108,7 @@ def publish(ctx): @task -def release_github(ctx): +def release_github(ctx): # noqa: ARG001 desc = get_changelog() payload = { "tag_name": "v" + NEW_VER, @@ -124,16 +122,16 @@ def release_github(ctx): "https://api.github.com/repos/materialsvirtuallab/matcalc/releases", data=json.dumps(payload), headers={"Authorization": "token " + os.environ["GITHUB_RELEASES_TOKEN"]}, + timeout=10, ) pprint(response.json()) @task -def release(ctx, notest=False): +def release(ctx, notest: bool = False) -> None: ctx.run("rm -r dist build matcalc.egg-info", warn=True) if not notest: ctx.run("pytest tests") - # publish(ctx) release_github(ctx) @@ -145,6 +143,6 @@ def get_changelog(): @task -def view_docs(ctx): +def view_docs(ctx) -> None: with cd("docs"): ctx.run("bundle exec jekyll serve") diff --git a/tests/conftest.py b/tests/conftest.py index 9164dba..0e9b7c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,16 +10,21 @@ """ from __future__ import annotations +from typing import TYPE_CHECKING + import matgl import pytest from matgl.ext.ase import M3GNetCalculator from pymatgen.util.testing import PymatgenTest +if TYPE_CHECKING: + from pymatgen.core import Structure + matgl.clear_cache(confirm=False) @pytest.fixture(scope="session") -def LiFePO4(): +def LiFePO4() -> Structure: """LiFePO4 structure as session-scoped fixture (don't modify in-place, will affect other tests). """ @@ -27,13 +32,13 @@ def LiFePO4(): @pytest.fixture(scope="session") -def Li2O(): +def Li2O() -> Structure: """Li2O structure as session-scoped fixture.""" return PymatgenTest.get_structure("Li2O") @pytest.fixture(scope="session") -def M3GNetCalc(): +def M3GNetCalc() -> M3GNetCalculator: """M3GNet calculator as session-scoped fixture.""" potential = matgl.load_model("M3GNet-MP-2021.2.8-PES") return M3GNetCalculator(potential=potential, stress_weight=0.01) diff --git a/tests/test_elasticity.py b/tests/test_elasticity.py index 087d87b..f27420b 100644 --- a/tests/test_elasticity.py +++ b/tests/test_elasticity.py @@ -59,12 +59,13 @@ def test_elastic_calc(Li2O: Structure, M3GNetCalc: M3GNetCalculator) -> None: assert results["bulk_modulus_vrh"] == pytest.approx(0.6631894154825593, rel=1e-3) -def test_elastic_calc_invalid_states(M3GNetCalc: M3GNetCalculator): - with pytest.raises(ValueError, match="shear_strains must be nonempty"): +def test_elastic_calc_invalid_states(M3GNetCalc: M3GNetCalculator) -> None: + with pytest.raises(ValueError, match="shear_strains is empty"): ElasticityCalc(M3GNetCalc, shear_strains=[]) - with pytest.raises(ValueError, match="norm_strains must be nonempty"): + with pytest.raises(ValueError, match="norm_strains is empty"): ElasticityCalc(M3GNetCalc, norm_strains=[]) - with pytest.raises(ValueError, match="Strains must be nonzero"): + + with pytest.raises(ValueError, match="strains must be non-zero"): ElasticityCalc(M3GNetCalc, norm_strains=[0.0, 0.1]) - with pytest.raises(ValueError, match="Strains must be nonzero"): + with pytest.raises(ValueError, match="strains must be non-zero"): ElasticityCalc(M3GNetCalc, shear_strains=[0.0, 0.1]) diff --git a/tests/test_neb.py b/tests/test_neb.py index 3a400a2..96cde00 100644 --- a/tests/test_neb.py +++ b/tests/test_neb.py @@ -19,12 +19,11 @@ def test_neb_calc(LiFePO4: Structure, M3GNetCalc: M3GNetCalculator, tmp_path: Pa image_start.remove_sites([2]) image_end = LiFePO4.copy() image_end.remove_sites([3]) - NEBcalc = NEBCalc.from_end_images(image_start, image_end, M3GNetCalc, n_images=5, traj_folder=tmp_path) - barriers = NEBcalc.calc(fmax=0.5) - print(barriers) - assert len(NEBcalc.neb.images) == 7 + neb_calc = NEBCalc.from_end_images(image_start, image_end, M3GNetCalc, n_images=5, traj_folder=tmp_path) + barriers = neb_calc.calc(fmax=0.5) + assert len(neb_calc.neb.images) == 7 assert barriers[0] == pytest.approx(0.0184783935546875, rel=0.002) assert barriers[1] == pytest.approx(0.0018920898, rel=0.002) with pytest.raises(ValueError, match="Unknown optimizer='invalid', must be one of "): - NEBcalc.from_end_images(image_start, image_end, M3GNetCalc, optimizer="invalid") + neb_calc.from_end_images(image_start, image_end, M3GNetCalc, optimizer="invalid")