From 2ca9d02a60e57c8149b70480eb6be5b90f45354b Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 12:37:34 +0800 Subject: [PATCH 01/50] explicitly test 3.13 in test workflow --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28a19a3cca7..02ff54fb187 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,11 +35,11 @@ jobs: resolution: highest extras: ci,optional - os: windows-latest - python: "3.10" + python: "3.12" resolution: highest extras: ci,optional,numpy-v1 # Test NP1 on Windows (quite buggy ATM) - os: ubuntu-latest - python: ">3.10" + python: "3.13" resolution: lowest-direct extras: ci,optional - os: macos-latest From 47b4cde2ba2338ea4c1e48112c105d1eb0c4c5c2 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 12:38:30 +0800 Subject: [PATCH 02/50] add 3.13 classifier --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index e136b4b52e6..81677621571 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Chemistry", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Scientific/Engineering :: Physics", From bb4554d36eccc21fdc68f773b529ae06abe28217 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 12:39:51 +0800 Subject: [PATCH 03/50] drop install for openff-toolkit for now --- .github/workflows/release.yml | 4 ++-- .github/workflows/test.yml | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 271d06eff91..539c5a11165 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: - uses: actions/setup-python@v5 name: Install Python with: - python-version: "3.12" + python-version: "3.13" - name: Build sdist run: | @@ -72,7 +72,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.13" - name: Get build artifacts uses: actions/download-artifact@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 02ff54fb187..9ed06248f68 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: # Maximize CI coverage of different platforms and python versions while minimizing the # total number of jobs. We run all pytest splits with the oldest supported python # version (currently 3.10) on windows (seems most likely to surface errors) and with - # newest version (currently 3.12) on ubuntu (to get complete coverage on unix). + # newest version (currently 3.13) on ubuntu (to get complete coverage on unix). config: - os: windows-latest python: "3.10" @@ -71,7 +71,8 @@ jobs: - name: Install ubuntu-only conda dependencies if: matrix.config.os == 'ubuntu-latest' run: | - micromamba install -n pmg -c conda-forge bader enumlib openff-toolkit packmol pygraphviz tblite --yes + micromamba install -n pmg -c conda-forge bader enumlib packmol pygraphviz tblite --yes + # TODO: openff-toolkit doesn't support Python 3.13 yet - name: Install pymatgen and dependencies via uv run: | From 36613595c940f7b0a1d148c2c7be58bb95f290bc Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 12:49:03 +0800 Subject: [PATCH 04/50] bump chgnet to support numpy 2 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 81677621571..18023830daa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ ase = ["ase>=3.23.0"] tblite = ["tblite[ase]>=0.3.0; python_version<'3.12'"] vis = ["vtk>=6.0.0"] abinit = ["netcdf4>=1.7.1"] -mlp = ["chgnet>=0.3.8", "matgl>=1.1.3"] +mlp = ["chgnet>=0.4.0", "matgl>=1.1.3"] electronic_structure = ["fdint>=2.0.2"] ci = ["pytest-cov>=4", "pytest-split>=0.8", "pytest>=8"] docs = ["invoke", "sphinx", "sphinx_markdown_builder", "sphinx_rtd_theme"] @@ -104,7 +104,7 @@ optional = [ # BoltzTraP2 build fails on Windows GitHub runners "BoltzTraP2>=24.9.4 ; platform_system != 'Windows'", "chemview>=0.6", - "chgnet>=0.3.8", + "chgnet>=0.4.0", "f90nml>=1.1.2", "galore>=0.6.1", "h5py>=3.11.0", From 7a8d6ffdea82f19335a98f8b9915f8b545cfbe9a Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 12:49:13 +0800 Subject: [PATCH 05/50] loose torch pin --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9ed06248f68..97b9f387236 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -83,7 +83,7 @@ jobs: # TODO1 (use uv over pip) uv install torch is flaky, track #3826 # TODO2 (pin torch version): DGL library (matgl) doesn't support torch > 2.2.1, # see: https://discuss.dgl.ai/t/filenotfounderror-cannot-find-dgl-c-graphbolt-library/4302 - pip install torch==2.2.1 + pip install torch<=2.2.1 uv pip install --editable '.[${{ matrix.config.extras }}]' --resolution=${{ matrix.config.resolution }} From f3176b48cb687a0fa69006df3a96dd454bda3a8d Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 13:34:08 +0800 Subject: [PATCH 06/50] try to remove manual install of torch --- .github/workflows/test.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 97b9f387236..505538d68e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,11 +80,6 @@ jobs: pip install uv - # TODO1 (use uv over pip) uv install torch is flaky, track #3826 - # TODO2 (pin torch version): DGL library (matgl) doesn't support torch > 2.2.1, - # see: https://discuss.dgl.ai/t/filenotfounderror-cannot-find-dgl-c-graphbolt-library/4302 - pip install torch<=2.2.1 - uv pip install --editable '.[${{ matrix.config.extras }}]' --resolution=${{ matrix.config.resolution }} - name: Install optional Ubuntu dependencies From 02fbe6aadf14f1da3e861f45483bfb92a7a3eb4e Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 13:36:47 +0800 Subject: [PATCH 07/50] bump github action version --- .github/workflows/jekyll-gh-pages.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/jekyll-gh-pages.yml b/.github/workflows/jekyll-gh-pages.yml index 91f17b3ea39..b21ea67fa6c 100644 --- a/.github/workflows/jekyll-gh-pages.yml +++ b/.github/workflows/jekyll-gh-pages.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Pages - uses: actions/configure-pages@v3 + uses: actions/configure-pages@v5 - name: Build with Jekyll uses: actions/jekyll-build-pages@v1 @@ -36,7 +36,7 @@ jobs: destination: ./_site - name: Upload artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 deploy: environment: @@ -47,4 +47,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 From 249821b1d8d6972b007e435131eba327543c5e1f Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 13:38:35 +0800 Subject: [PATCH 08/50] revert bump of chgnet version --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 18023830daa..81677621571 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ ase = ["ase>=3.23.0"] tblite = ["tblite[ase]>=0.3.0; python_version<'3.12'"] vis = ["vtk>=6.0.0"] abinit = ["netcdf4>=1.7.1"] -mlp = ["chgnet>=0.4.0", "matgl>=1.1.3"] +mlp = ["chgnet>=0.3.8", "matgl>=1.1.3"] electronic_structure = ["fdint>=2.0.2"] ci = ["pytest-cov>=4", "pytest-split>=0.8", "pytest>=8"] docs = ["invoke", "sphinx", "sphinx_markdown_builder", "sphinx_rtd_theme"] @@ -104,7 +104,7 @@ optional = [ # BoltzTraP2 build fails on Windows GitHub runners "BoltzTraP2>=24.9.4 ; platform_system != 'Windows'", "chemview>=0.6", - "chgnet>=0.4.0", + "chgnet>=0.3.8", "f90nml>=1.1.2", "galore>=0.6.1", "h5py>=3.11.0", From 2673ce15a1d44fc7e802d432e0b610fe3e68cbb9 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 13:53:19 +0800 Subject: [PATCH 09/50] bump chgnet to 0.4.0 to support np2 --- pyproject.toml | 4 ++-- tests/core/test_structure.py | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 81677621571..18023830daa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ ase = ["ase>=3.23.0"] tblite = ["tblite[ase]>=0.3.0; python_version<'3.12'"] vis = ["vtk>=6.0.0"] abinit = ["netcdf4>=1.7.1"] -mlp = ["chgnet>=0.3.8", "matgl>=1.1.3"] +mlp = ["chgnet>=0.4.0", "matgl>=1.1.3"] electronic_structure = ["fdint>=2.0.2"] ci = ["pytest-cov>=4", "pytest-split>=0.8", "pytest>=8"] docs = ["invoke", "sphinx", "sphinx_markdown_builder", "sphinx_rtd_theme"] @@ -104,7 +104,7 @@ optional = [ # BoltzTraP2 build fails on Windows GitHub runners "BoltzTraP2>=24.9.4 ; platform_system != 'Windows'", "chemview>=0.6", - "chgnet>=0.3.8", + "chgnet>=0.4.0", "f90nml>=1.1.2", "galore>=0.6.1", "h5py>=3.11.0", diff --git a/tests/core/test_structure.py b/tests/core/test_structure.py index 05ad8e9df50..cf1d7caadf8 100644 --- a/tests/core/test_structure.py +++ b/tests/core/test_structure.py @@ -1711,8 +1711,6 @@ def test_calculate_ase(self): assert not hasattr(calculator, "dynamics") assert self.cu_structure == struct_copy, "original structure was modified" - @pytest.mark.skip(reason="chgnet is failing with Numpy 1, see #3992") - @pytest.mark.skipif(int(np.__version__[0]) >= 2, reason="chgnet is not built against NumPy 2.0") def test_relax_chgnet(self): pytest.importorskip("chgnet") struct_copy = self.cu_structure.copy() @@ -1736,8 +1734,6 @@ def test_relax_chgnet(self): assert custom_relaxed.calc.results.get("energy") == approx(-6.0151076, abs=1e-4) assert custom_relaxed.volume == approx(40.044794644, abs=1e-4) - @pytest.mark.skip(reason="chgnet is failing with Numpy 1, see #3992") - @pytest.mark.skipif(int(np.__version__[0]) >= 2, reason="chgnet is not built against NumPy 2.0") def test_calculate_chgnet(self): pytest.importorskip("chgnet") struct = self.get_structure("Si") From dc4725341713cb6f45316623e3ebade9f5b856bf Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 13:54:40 +0800 Subject: [PATCH 10/50] migrate deprecated config --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a8967994f5..1bfad798af4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: rev: v2.3.0 hooks: - id: codespell - stages: [commit, commit-msg] + stages: [pre-commit, commit-msg] exclude_types: [html] additional_dependencies: [tomli] # needed to read pyproject.toml below py3.11 exclude: src/pymatgen/analysis/aflow_prototypes.json From 300f4e414b7de422f85157c484f4bfcb76070595 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 13:56:13 +0800 Subject: [PATCH 11/50] pin python < 3.13 for chgnet and matgl --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 18023830daa..8bee412ed27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ ase = ["ase>=3.23.0"] tblite = ["tblite[ase]>=0.3.0; python_version<'3.12'"] vis = ["vtk>=6.0.0"] abinit = ["netcdf4>=1.7.1"] -mlp = ["chgnet>=0.4.0", "matgl>=1.1.3"] +mlp = ["chgnet>=0.4.0; python_version<'3.13'", "matgl>=1.1.3; python_version<'3.13'"] electronic_structure = ["fdint>=2.0.2"] ci = ["pytest-cov>=4", "pytest-split>=0.8", "pytest>=8"] docs = ["invoke", "sphinx", "sphinx_markdown_builder", "sphinx_rtd_theme"] @@ -104,12 +104,12 @@ optional = [ # BoltzTraP2 build fails on Windows GitHub runners "BoltzTraP2>=24.9.4 ; platform_system != 'Windows'", "chemview>=0.6", - "chgnet>=0.4.0", + "chgnet>=0.4.0; python_version<'3.13'", "f90nml>=1.1.2", "galore>=0.6.1", "h5py>=3.11.0", "jarvis-tools>=2020.7.14", - "matgl>=1.1.3", + "matgl>=1.1.3; python_version<'3.13'", "matplotlib>=3.8", "netCDF4>=1.6.5", "phonopy>=2.23", From 464defe6931276efb3315715b03d39aad0f13f8d Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 14:06:18 +0800 Subject: [PATCH 12/50] try to exclude some python313 incompatible packages --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8bee412ed27..3da28da3203 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ ase = ["ase>=3.23.0"] # https://github.com/tblite/tblite/issues/175 tblite = ["tblite[ase]>=0.3.0; python_version<'3.12'"] vis = ["vtk>=6.0.0"] -abinit = ["netcdf4>=1.7.1"] +abinit = ["netcdf4>=1.7.1; python_version<'3.13'"] mlp = ["chgnet>=0.4.0; python_version<'3.13'", "matgl>=1.1.3; python_version<'3.13'"] electronic_structure = ["fdint>=2.0.2"] ci = ["pytest-cov>=4", "pytest-split>=0.8", "pytest>=8"] @@ -111,7 +111,7 @@ optional = [ "jarvis-tools>=2020.7.14", "matgl>=1.1.3; python_version<'3.13'", "matplotlib>=3.8", - "netCDF4>=1.6.5", + "netcdf4>=1.6.5; python_version<'3.13'", "phonopy>=2.23", "seekpath>=2.0.1", # tblite only support Python 3.12+ through conda-forge @@ -120,7 +120,7 @@ optional = [ "openbabel-wheel>=3.1.1.20", "tblite[ase]>=0.3.0; platform_system=='Linux' and python_version<'3.12'", ] -numba = ["numba>=0.55"] +numba = ["numba>=0.55; python_version<'3.13'"] numpy-v1 = ["numpy>=1.25.0,<2"] # Test NP1 on Windows (quite buggy ATM) [project.scripts] From d01bf59fb344c72e27e19faba733209f83e1953b Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 14:28:04 +0800 Subject: [PATCH 13/50] should work for python 3.13 now --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3da28da3203..b9828e00639 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,8 @@ optional = [ "ase>=3.23.0", "beautifulsoup4", # BoltzTraP2 build fails on Windows GitHub runners - "BoltzTraP2>=24.9.4 ; platform_system != 'Windows'", + # TODO: BoltzTraP2 depends on netcdf4 which doesn't support Python 3.13 yet + "BoltzTraP2>=24.9.4 ; platform_system != 'Windows' and python_version<'3.13'", "chemview>=0.6", "chgnet>=0.4.0; python_version<'3.13'", "f90nml>=1.1.2", @@ -116,7 +117,7 @@ optional = [ "seekpath>=2.0.1", # tblite only support Python 3.12+ through conda-forge # https://github.com/tblite/tblite/issues/175 - "hiphive>=1.3.1", + "hiphive>=1.3.1; python_version<'3.13'", "openbabel-wheel>=3.1.1.20", "tblite[ase]>=0.3.0; platform_system=='Linux' and python_version<'3.12'", ] From 16caa145d2231496079f4ac4f3446dd96a7aad8a Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 14:30:46 +0800 Subject: [PATCH 14/50] revert migrated changes --- .github/workflows/jekyll-gh-pages.yml | 6 +++--- .pre-commit-config.yaml | 2 +- tests/core/test_structure.py | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/jekyll-gh-pages.yml b/.github/workflows/jekyll-gh-pages.yml index b21ea67fa6c..91f17b3ea39 100644 --- a/.github/workflows/jekyll-gh-pages.yml +++ b/.github/workflows/jekyll-gh-pages.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@v3 - name: Build with Jekyll uses: actions/jekyll-build-pages@v1 @@ -36,7 +36,7 @@ jobs: destination: ./_site - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v2 deploy: environment: @@ -47,4 +47,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bfad798af4..2a8967994f5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: rev: v2.3.0 hooks: - id: codespell - stages: [pre-commit, commit-msg] + stages: [commit, commit-msg] exclude_types: [html] additional_dependencies: [tomli] # needed to read pyproject.toml below py3.11 exclude: src/pymatgen/analysis/aflow_prototypes.json diff --git a/tests/core/test_structure.py b/tests/core/test_structure.py index cf1d7caadf8..05ad8e9df50 100644 --- a/tests/core/test_structure.py +++ b/tests/core/test_structure.py @@ -1711,6 +1711,8 @@ def test_calculate_ase(self): assert not hasattr(calculator, "dynamics") assert self.cu_structure == struct_copy, "original structure was modified" + @pytest.mark.skip(reason="chgnet is failing with Numpy 1, see #3992") + @pytest.mark.skipif(int(np.__version__[0]) >= 2, reason="chgnet is not built against NumPy 2.0") def test_relax_chgnet(self): pytest.importorskip("chgnet") struct_copy = self.cu_structure.copy() @@ -1734,6 +1736,8 @@ def test_relax_chgnet(self): assert custom_relaxed.calc.results.get("energy") == approx(-6.0151076, abs=1e-4) assert custom_relaxed.volume == approx(40.044794644, abs=1e-4) + @pytest.mark.skip(reason="chgnet is failing with Numpy 1, see #3992") + @pytest.mark.skipif(int(np.__version__[0]) >= 2, reason="chgnet is not built against NumPy 2.0") def test_calculate_chgnet(self): pytest.importorskip("chgnet") struct = self.get_structure("Si") From 5fb01a7abbd109f30d0394873f4618c4d411eb30 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 16:23:41 +0800 Subject: [PATCH 15/50] NEED CONFIRM: bump numpy to 1.26.2 to support python 3.13 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b9828e00639..be7d59cef2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ dependencies = [ "uncertainties>=3.1.4", # NumPy documentation suggests pinning the current major version as the C API is used # https://numpy.org/devdocs/dev/depending_on_numpy.html#runtime-dependency-version-ranges - "numpy>=1.25.0,<3", + "numpy>=1.26.2,<3", ] version = "2024.10.3" From 960a44b28559b7784234aff0d09a5b3428dca7d6 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 16:27:49 +0800 Subject: [PATCH 16/50] bump h5py to support python 3.13 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index be7d59cef2a..628e00e3592 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,7 +108,7 @@ optional = [ "chgnet>=0.4.0; python_version<'3.13'", "f90nml>=1.1.2", "galore>=0.6.1", - "h5py>=3.11.0", + "h5py>=3.12.1", "jarvis-tools>=2020.7.14", "matgl>=1.1.3; python_version<'3.13'", "matplotlib>=3.8", From a8a765a22ce4eb33f03ed192e9b937563224893a Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Oct 2024 16:31:17 +0800 Subject: [PATCH 17/50] bump scipy to support python 3.13 --- pyproject.toml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 628e00e3592..1e007d4da91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,10 +65,7 @@ dependencies = [ "pybtex>=0.24.0", "requests>=2.32", "ruamel.yaml>=0.17.0", - "scipy>=1.13.0", - # scipy<1.14.1 is incompatible with NumPy 2.0 on Windows - # https://github.com/scipy/scipy/issues/21052 - "scipy>=1.14.1; platform_system == 'Windows'", + "scipy>=1.14.1", "spglib>=2.5.0", "sympy>=1.2", "tabulate>=0.9", From 0f02418f6a30316e973a0b911cb455b8461f8dad Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Tue, 19 Nov 2024 21:37:39 +0800 Subject: [PATCH 18/50] remove duplicate from merging --- pyproject.toml | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4a8ba373ec7..9848cdcd9fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ Python Materials Genomics is a robust materials analysis code that defines core and molecules with support for many electronic structure codes. It is currently the core analysis code powering the Materials Project (https://materialsproject.org).""" readme = "README.md" -requires-python = ">=3.10,<3.13" +requires-python = ">=3.10,<3.14" keywords = [ "ABINIT", "VASP", @@ -88,18 +88,11 @@ Pypi = "https://pypi.org/project/pymatgen" # PR4128: netcdf4 1.7.[0/1] yanked, 1.7.1.post[1/2]/1.7.2 cause CI error abinit = ["netcdf4>=1.6.5,!=1.7.1.post1,!=1.7.1.post2,!=1.7.2"] ase = ["ase>=3.23.0"] -# tblite only support Python 3.12+ through conda-forge -# https://github.com/tblite/tblite/issues/175 -tblite = ["tblite[ase]>=0.3.0; python_version<'3.12'"] -vis = ["vtk>=6.0.0"] -abinit = ["netcdf4>=1.7.1; python_version<'3.13'"] -mlp = ["chgnet>=0.4.0; python_version<'3.13'", "matgl>=1.1.3; python_version<'3.13'"] -electronic_structure = ["fdint>=2.0.2"] ci = ["pytest-cov>=4", "pytest-split>=0.8", "pytest>=8"] docs = ["invoke", "sphinx", "sphinx_markdown_builder", "sphinx_rtd_theme"] electronic_structure = ["fdint>=2.0.2"] -mlp = ["chgnet>=0.3.8", "matgl>=1.1.3"] -numba = ["numba>=0.55"] +mlp = ["chgnet>=0.4.0; python_version<'3.13'", "matgl>=1.1.3; python_version<'3.13'"] +numba = ["numba>=0.55; python_version<'3.13'"] numpy-v1 = ["numpy>=1.25.0,<2"] # Test NP1 on Windows (quite buggy ATM) optional = [ "pymatgen[abinit,ase,mlp,tblite]", @@ -108,24 +101,20 @@ optional = [ # TODO: BoltzTraP2 depends on netcdf4 which doesn't support Python 3.13 yet "BoltzTraP2>=24.9.4 ; platform_system != 'Windows' and python_version<'3.13'", "chemview>=0.6", - "chgnet>=0.4.0; python_version<'3.13'", "f90nml>=1.1.2", "galore>=0.6.1", "h5py>=3.12.1", + "hiphive>=1.3.1; python_version<'3.13'", "jarvis-tools>=2020.7.14", - "matgl>=1.1.3; python_version<'3.13'", "matplotlib>=3.8", - "netcdf4>=1.6.5; python_version<'3.13'", + "openbabel-wheel>=3.1.1.20", "phonopy>=2.23", "seekpath>=2.0.1", - # tblite only support Python 3.12+ through conda-forge - # https://github.com/tblite/tblite/issues/175 - "hiphive>=1.3.1; python_version<'3.13'", - "openbabel-wheel>=3.1.1.20", - "tblite[ase]>=0.3.0; platform_system=='Linux' and python_version<'3.12'", ] -numba = ["numba>=0.55; python_version<'3.13'"] -numpy-v1 = ["numpy>=1.25.0,<2"] # Test NP1 on Windows (quite buggy ATM) +# tblite only support Python 3.12+ through conda-forge +# https://github.com/tblite/tblite/issues/175 +tblite = [ "tblite[ase]>=0.3.0; platform_system=='Linux' and python_version<'3.12'"] +vis = ["vtk>=6.0.0"] [project.scripts] pmg = "pymatgen.cli.pmg:main" From 687effa8da1135c6f1ceac8f7a13af962d6a991f Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Wed, 20 Nov 2024 22:18:53 +0800 Subject: [PATCH 19/50] clean up comments --- pyproject.toml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9848cdcd9fc..4f9a9631125 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,10 @@ dependencies = [ "pybtex>=0.24.0", "requests>=2.32", "ruamel.yaml>=0.17.0", - "scipy>=1.14.1", + # scipy<1.14.1 is incompatible with NumPy 2.0 on Windows + # https://github.com/scipy/scipy/issues/21052 + "scipy>=1.13.0", + "scipy>=1.14.1; platform_system == 'Windows'", "spglib>=2.5.0", "sympy>=1.3", # PR #4116 "tabulate>=0.9", @@ -73,7 +76,7 @@ dependencies = [ "uncertainties>=3.1.4", # NumPy documentation suggests pinning the current major version as the C API is used # https://numpy.org/devdocs/dev/depending_on_numpy.html#runtime-dependency-version-ranges - "numpy>=1.26.2,<3", + "numpy>=1.25.0,<3", # TODO: only numpy>=1.26.2 supports Python 3.13 ] version = "2024.11.13" @@ -103,8 +106,8 @@ optional = [ "chemview>=0.6", "f90nml>=1.1.2", "galore>=0.6.1", - "h5py>=3.12.1", - "hiphive>=1.3.1; python_version<'3.13'", + "h5py>=3.11.0", # h5py>=3.12.1 supports Python 3.13 + "hiphive>=1.3.1; python_version<'3.13'", # TODO: no Python 3.13 support "jarvis-tools>=2020.7.14", "matplotlib>=3.8", "openbabel-wheel>=3.1.1.20", From cb0adc9b5153d27e930c145f3a77853fb2a682db Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Wed, 20 Nov 2024 22:26:30 +0800 Subject: [PATCH 20/50] skip netcdf4 for python3.13 as 1.6.5 cannot be installed --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4f9a9631125..3544f15c7eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,8 +88,8 @@ Issues = "https://github.com/materialsproject/pymatgen/issues" Pypi = "https://pypi.org/project/pymatgen" [project.optional-dependencies] -# PR4128: netcdf4 1.7.[0/1] yanked, 1.7.1.post[1/2]/1.7.2 cause CI error -abinit = ["netcdf4>=1.6.5,!=1.7.1.post1,!=1.7.1.post2,!=1.7.2"] +# PR4128: netcdf4 1.7.1.post[1/2]/1.7.2 cause CI error +abinit = ["netcdf4>=1.6.5,!=1.7.1.post1,!=1.7.1.post2,!=1.7.2; python_version<'3.13'"] ase = ["ase>=3.23.0"] ci = ["pytest-cov>=4", "pytest-split>=0.8", "pytest>=8"] docs = ["invoke", "sphinx", "sphinx_markdown_builder", "sphinx_rtd_theme"] From ccda40d65e05eb53e4e841284f0c1dd7daacf4f3 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Wed, 20 Nov 2024 22:31:01 +0800 Subject: [PATCH 21/50] make wheel build quiet --- .github/workflows/test.yml | 2 +- pyproject.toml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bfc44b37917..4208356e266 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -82,7 +82,7 @@ jobs: # Install from wheels to test the content uv pip install build - python -m build --wheel + python -m build --wheel -C--quiet uv pip install dist/*.whl uv pip install pymatgen[${{ matrix.config.extras }}] --resolution=${{ matrix.config.resolution }} diff --git a/pyproject.toml b/pyproject.toml index 3544f15c7eb..7ab3287a861 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ Pypi = "https://pypi.org/project/pymatgen" [project.optional-dependencies] # PR4128: netcdf4 1.7.1.post[1/2]/1.7.2 cause CI error +# TODO: netcdf4==1.6.5 (current only version left) cannot be installed for Python 3.13 abinit = ["netcdf4>=1.6.5,!=1.7.1.post1,!=1.7.1.post2,!=1.7.2; python_version<'3.13'"] ase = ["ase>=3.23.0"] ci = ["pytest-cov>=4", "pytest-split>=0.8", "pytest>=8"] @@ -96,7 +97,7 @@ docs = ["invoke", "sphinx", "sphinx_markdown_builder", "sphinx_rtd_theme"] electronic_structure = ["fdint>=2.0.2"] mlp = ["chgnet>=0.4.0; python_version<'3.13'", "matgl>=1.1.3; python_version<'3.13'"] numba = ["numba>=0.55; python_version<'3.13'"] -numpy-v1 = ["numpy>=1.25.0,<2"] # Test NP1 on Windows (quite buggy ATM) +numpy-v1 = ["numpy>=1.25.0,<2"] # Test NP1 on Windows (buggy at this moment) optional = [ "pymatgen[abinit,ase,mlp,tblite]", "beautifulsoup4", @@ -116,7 +117,7 @@ optional = [ ] # tblite only support Python 3.12+ through conda-forge # https://github.com/tblite/tblite/issues/175 -tblite = [ "tblite[ase]>=0.3.0; platform_system=='Linux' and python_version<'3.12'"] +tblite = ["tblite[ase]>=0.3.0; platform_system=='Linux' and python_version<'3.12'"] vis = ["vtk>=6.0.0"] [project.scripts] From 0f9d2a1fd9063dbb0cfafabfa693b66a8bff8126 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 21 Nov 2024 10:31:46 +0800 Subject: [PATCH 22/50] test openff-toolkit --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4208356e266..0ba5d9b3d12 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,7 +71,8 @@ jobs: - name: Install ubuntu-only conda dependencies if: matrix.config.os == 'ubuntu-latest' run: | - micromamba install -n pmg -c conda-forge bader enumlib packmol pygraphviz tblite --yes + micromamba install -n pmg -c conda-forge --yes \ + bader enumlib packmol pygraphviz tblite openff-toolkit # TODO: openff-toolkit doesn't support Python 3.13 yet - name: Install pymatgen and dependencies via uv From 18175dbf59ae4eda0eb20400df666e1df4362704 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 21 Nov 2024 10:36:30 +0800 Subject: [PATCH 23/50] still cannot install openff-toolkit --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0ba5d9b3d12..66a84784ee7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,7 +72,8 @@ jobs: if: matrix.config.os == 'ubuntu-latest' run: | micromamba install -n pmg -c conda-forge --yes \ - bader enumlib packmol pygraphviz tblite openff-toolkit + bader enumlib packmol pygraphviz tblite + # openff-toolkit # TODO: openff-toolkit doesn't support Python 3.13 yet - name: Install pymatgen and dependencies via uv From e3f69e58594dc6ec16cffb6917502d7162f808a9 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 21 Nov 2024 10:41:47 +0800 Subject: [PATCH 24/50] clean up comment --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7ab3287a861..bf4a287636d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ Python Materials Genomics is a robust materials analysis code that defines core and molecules with support for many electronic structure codes. It is currently the core analysis code powering the Materials Project (https://materialsproject.org).""" readme = "README.md" -requires-python = ">=3.10,<3.14" +requires-python = ">=3.10,<=3.13" keywords = [ "ABINIT", "VASP", @@ -76,7 +76,7 @@ dependencies = [ "uncertainties>=3.1.4", # NumPy documentation suggests pinning the current major version as the C API is used # https://numpy.org/devdocs/dev/depending_on_numpy.html#runtime-dependency-version-ranges - "numpy>=1.25.0,<3", # TODO: only numpy>=1.26.2 supports Python 3.13 + "numpy>=1.25.0,<3", ] version = "2024.11.13" @@ -107,8 +107,8 @@ optional = [ "chemview>=0.6", "f90nml>=1.1.2", "galore>=0.6.1", - "h5py>=3.11.0", # h5py>=3.12.1 supports Python 3.13 - "hiphive>=1.3.1; python_version<'3.13'", # TODO: no Python 3.13 support + "h5py>=3.11.0", + "hiphive>=1.3.1; python_version<'3.13'", "jarvis-tools>=2020.7.14", "matplotlib>=3.8", "openbabel-wheel>=3.1.1.20", From cf318dee6b63525911fa1610ec2c23cdcd949246 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 12 Dec 2024 12:42:22 +0800 Subject: [PATCH 25/50] remove manual torch install --- .github/workflows/test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6d1b1ac4062..08f0ec66db1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -82,10 +82,6 @@ jobs: - name: Install pymatgen and dependencies via uv run: | micromamba activate pmg - # TODO1 (use uv over pip) uv install torch is flaky, track #3826 - # TODO2 (pin torch version): DGL library (matgl) doesn't support torch > 2.2.1, - # see: https://discuss.dgl.ai/t/filenotfounderror-cannot-find-dgl-c-graphbolt-library/4302 - pip install torch==2.2.1 # Install from wheels to test the content uv build --wheel --no-build-logs From 54bbc6bcf8cadc6010a2f0b582ea21a0d6001279 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 20 Dec 2024 10:51:10 +0800 Subject: [PATCH 26/50] fix python version pin --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3b5d1fa3313..f8d4b18e021 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ Python Materials Genomics is a robust materials analysis code that defines core and molecules with support for many electronic structure codes. It is currently the core analysis code powering the Materials Project (https://materialsproject.org).""" readme = "README.md" -requires-python = ">=3.10,<=3.13" +requires-python = ">=3.10,<3.14" keywords = [ "ABINIT", "VASP", From 50f46228ccb5f5689f1ce28e3d06008dfd2dfe6c Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 24 Jan 2025 17:58:53 +0100 Subject: [PATCH 27/50] un-skip netcdf4 and BoltzTraP2 --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7a8caa9b9d4..7cce8a3c1c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,8 +100,7 @@ optional = [ "pymatgen[abinit,ase,mlp,tblite]", "beautifulsoup4", # BoltzTraP2 build fails on Windows GitHub runners - # TODO: BoltzTraP2 depends on netcdf4 which doesn't support Python 3.13 yet - "BoltzTraP2>=24.9.4 ; platform_system != 'Windows' and python_version<'3.13'", + "BoltzTraP2>=24.9.4 ; platform_system != 'Windows'", "chemview>=0.6", "f90nml>=1.1.2", "galore>=0.6.1", From 56108b650b9c5819e7415dd514d1cf6b47ff9fdd Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 24 Jan 2025 18:00:48 +0100 Subject: [PATCH 28/50] unskip hiphive --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7cce8a3c1c6..f727dd76d68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,7 @@ optional = [ "f90nml>=1.1.2", "galore>=0.6.1", "h5py>=3.11.0", - "hiphive>=1.3.1; python_version<'3.13'", + "hiphive>=1.3.1", "jarvis-tools>=2020.7.14", "matplotlib>=3.8", "openbabel-wheel>=3.1.1.20", From dcd65e194120cf00e521116324a84b5273d69a31 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 24 Jan 2025 18:03:54 +0100 Subject: [PATCH 29/50] unskip numba --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f727dd76d68..52829f399a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ ci = ["pytest-cov>=4", "pytest-split>=0.8", "pytest>=8"] docs = ["invoke", "sphinx", "sphinx_markdown_builder", "sphinx_rtd_theme"] electronic_structure = ["fdint>=2.0.2"] mlp = ["chgnet>=0.4.0; python_version<'3.13'", "matgl>=1.1.3; python_version<'3.13'"] -numba = ["numba>=0.55; python_version<'3.13'"] +numba = ["numba>=0.55"] numpy-v1 = ["numpy>=1.25.0,<2"] # Test NP1 on Windows (buggy at this moment) optional = [ "pymatgen[abinit,ase,mlp,tblite]", From b7051d749f69511b0814afb2da937e5ccc5d7d1e Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 24 Jan 2025 18:32:00 +0100 Subject: [PATCH 30/50] try to un-skip openff-toolkit --- .github/workflows/test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf747d734c6..e3dd62bed43 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,9 +71,7 @@ jobs: if: matrix.config.os == 'ubuntu-latest' run: | micromamba install -n pmg -c conda-forge --yes \ - bader enumlib packmol pygraphviz tblite - # openff-toolkit - # TODO: openff-toolkit doesn't support Python 3.13 yet + bader enumlib packmol pygraphviz tblite openff-toolkit - name: Install uv uses: astral-sh/setup-uv@v4 From 4fbf58ff7a00a9a2914502918780d02db044ecac Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 24 Jan 2025 18:34:08 +0100 Subject: [PATCH 31/50] Revert "try to un-skip openff-toolkit" This reverts commit b7051d749f69511b0814afb2da937e5ccc5d7d1e. --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3dd62bed43..bf747d734c6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,7 +71,9 @@ jobs: if: matrix.config.os == 'ubuntu-latest' run: | micromamba install -n pmg -c conda-forge --yes \ - bader enumlib packmol pygraphviz tblite openff-toolkit + bader enumlib packmol pygraphviz tblite + # openff-toolkit + # TODO: openff-toolkit doesn't support Python 3.13 yet - name: Install uv uses: astral-sh/setup-uv@v4 From 5ea26e3be9cb2f496fd7ef2fe96f95cba1cadb2d Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Sat, 8 Feb 2025 22:50:36 +0100 Subject: [PATCH 32/50] fix mypy error --- src/pymatgen/io/vasp/outputs.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pymatgen/io/vasp/outputs.py b/src/pymatgen/io/vasp/outputs.py index 3e7207564a8..c88330f0315 100644 --- a/src/pymatgen/io/vasp/outputs.py +++ b/src/pymatgen/io/vasp/outputs.py @@ -4719,9 +4719,9 @@ def concatenate( preamble.append(line) elif line == "" or "Direct configuration=" in line: poscar = Poscar.from_str("\n".join([*preamble, "Direct", *coords_str])) - if ( - ionicstep_end is None and ionicstep_cnt >= ionicstep_start - ) or ionicstep_start <= ionicstep_cnt < ionicstep_end: + if (ionicstep_end is None and ionicstep_cnt >= ionicstep_start) or ( + ionicstep_end is not None and ionicstep_start <= ionicstep_cnt < ionicstep_end + ): structures.append(poscar.structure) ionicstep_cnt += 1 coords_str = [] @@ -4732,9 +4732,8 @@ def concatenate( raise ValueError("preamble is None") poscar = Poscar.from_str("\n".join([*preamble, "Direct", *coords_str])) - if ( - (ionicstep_end is None and ionicstep_cnt >= ionicstep_start) - or ionicstep_start <= ionicstep_cnt < ionicstep_end # type: ignore[operator] + if (ionicstep_end is None and ionicstep_cnt >= ionicstep_start) or ( + ionicstep_end is not None and ionicstep_start <= ionicstep_cnt < ionicstep_end ): structures.append(poscar.structure) self.structures = structures From 26007e9663f9dd81e8da759cb23937470615c1af Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Sat, 8 Feb 2025 23:14:42 +0100 Subject: [PATCH 33/50] add types for analysis.eos --- src/pymatgen/analysis/eos.py | 97 +++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/src/pymatgen/analysis/eos.py b/src/pymatgen/analysis/eos.py index f25844eaec7..67f171eba45 100644 --- a/src/pymatgen/analysis/eos.py +++ b/src/pymatgen/analysis/eos.py @@ -24,7 +24,8 @@ from pymatgen.util.plotting import add_fig_kwargs, get_ax_fig, pretty_plot if TYPE_CHECKING: - from typing import ClassVar + from collections.abc import Sequence + from typing import Any, ClassVar import matplotlib.pyplot as plt @@ -40,7 +41,11 @@ class EOSBase(ABC): implementations. """ - def __init__(self, volumes, energies): + def __init__( + self, + volumes: Sequence[float], + energies: Sequence[float], + ) -> None: """ Args: volumes (Sequence[float]): in Ang^3. @@ -50,18 +55,28 @@ def __init__(self, volumes, energies): self.energies = np.array(energies) # minimum energy(e0), buk modulus(b0), # derivative of bulk modulus w.r.t. pressure(b1), minimum volume(v0) - self._params = None + self._params: Sequence | None = None # the eos function parameters. It is the same as _params except for # equation of states that uses polynomial fits(delta_factor and # numerical_eos) - self.eos_params = None + self.eos_params: Sequence | None = None - def _initial_guess(self): + def __call__(self, volume: float) -> float: + """ + Args: + volume (float | list[float]): volume(s) in Ang^3. + + Returns: + Compute EOS with this volume. + """ + return self.func(volume) + + def _initial_guess(self) -> tuple[float, float, float, float]: """ Quadratic fit to get an initial guess for the parameters. Returns: - tuple: 4 floats for (e0, b0, b1, v0) + tuple[float, float, float, float]: e0, b0, b1, v0 """ a, b, c = np.polyfit(self.volumes, self.energies, 2) self.eos_params = [a, b, c] @@ -78,7 +93,7 @@ def _initial_guess(self): return e0, b0, b1, v0 - def fit(self): + def fit(self) -> None: """ Do the fitting. Does least square fitting. If you want to use custom fitting, must override this. @@ -120,24 +135,20 @@ def func(self, volume): """ return self._func(np.array(volume), self.eos_params) - def __call__(self, volume: float) -> float: - """ - Args: - volume (float | list[float]): volume(s) in Ang^3. - - Returns: - Compute EOS with this volume. - """ - return self.func(volume) - @property def e0(self) -> float: """The min energy.""" + if self._params is None: + raise RuntimeError("params have not be initialized.") + return self._params[0] @property def b0(self) -> float: """The bulk modulus in units of energy/unit of volume^3.""" + if self._params is None: + raise RuntimeError("params have not be initialized.") + return self._params[1] @property @@ -156,11 +167,18 @@ def v0(self): return self._params[3] @property - def results(self): + def results(self) -> dict[str, Any]: """A summary dict.""" return {"e0": self.e0, "b0": self.b0, "b1": self.b1, "v0": self.v0} - def plot(self, width=8, height=None, ax: plt.Axes = None, dpi=None, **kwargs): + def plot( + self, + width: float = 8, + height: float | None = None, + ax: plt.Axes = None, + dpi: float | None = None, + **kwargs, + ) -> plt.Axes: """ Plot the equation of state. @@ -170,7 +188,7 @@ def plot(self, width=8, height=None, ax: plt.Axes = None, dpi=None, **kwargs): golden ratio. ax (plt.Axes): If supplied, changes will be made to the existing Axes. Otherwise, new Axes will be created. - dpi: + dpi (float): DPI. kwargs (dict): additional args fed to pyplot.plot. supported keys: style, color, text, label @@ -211,16 +229,18 @@ def plot(self, width=8, height=None, ax: plt.Axes = None, dpi=None, **kwargs): return ax @add_fig_kwargs - def plot_ax(self, ax: plt.Axes = None, fontsize=12, **kwargs): + def plot_ax( + self, + ax: plt.Axes | None = None, + fontsize: float = 12, + **kwargs, + ) -> plt.Figure: """ Plot the equation of state on axis `ax`. Args: ax: matplotlib Axes or None if a new figure should be created. fontsize: Legend fontsize. - color (str): plot color. - label (str): Plot label - text (str): Legend text (options) Returns: plt.Figure: matplotlib figure. @@ -270,7 +290,7 @@ def plot_ax(self, ax: plt.Axes = None, fontsize=12, **kwargs): class Murnaghan(EOSBase): """Murnaghan EOS.""" - def _func(self, volume, params): + def _func(self, volume, params: tuple[float, float, float, float]): """From PRB 28,5480 (1983).""" e0, b0, b1, v0 = tuple(params) return e0 + b0 * volume / b1 * (((v0 / volume) ** b1) / (b1 - 1.0) + 1.0) - v0 * b0 / (b1 - 1.0) @@ -279,7 +299,7 @@ def _func(self, volume, params): class Birch(EOSBase): """Birch EOS.""" - def _func(self, volume, params): + def _func(self, volume, params: tuple[float, float, float, float]): """From Intermetallic compounds: Principles and Practice, Vol. I: Principles Chapter 9 pages 195-210 by M. Mehl. B. Klein, D. Papaconstantopoulos. @@ -296,7 +316,7 @@ def _func(self, volume, params): class BirchMurnaghan(EOSBase): """BirchMurnaghan EOS.""" - def _func(self, volume, params): + def _func(self, volume, params: tuple[float, float, float, float]): """BirchMurnaghan equation from PRB 70, 224107.""" e0, b0, b1, v0 = tuple(params) eta = (v0 / volume) ** (1 / 3) @@ -306,7 +326,7 @@ def _func(self, volume, params): class PourierTarantola(EOSBase): """Pourier-Tarantola EOS.""" - def _func(self, volume, params): + def _func(self, volume, params: tuple[float, float, float, float]): """Pourier-Tarantola equation from PRB 70, 224107.""" e0, b0, b1, v0 = tuple(params) eta = (volume / v0) ** (1 / 3) @@ -317,7 +337,7 @@ def _func(self, volume, params): class Vinet(EOSBase): """Vinet EOS.""" - def _func(self, volume, params): + def _func(self, volume, params: tuple[float, float, float, float]): """Vinet equation from PRB 70, 224107.""" e0, b0, b1, v0 = tuple(params) eta = (volume / v0) ** (1 / 3) @@ -335,7 +355,7 @@ class PolynomialEOS(EOSBase): def _func(self, volume, params): return np.poly1d(list(params))(volume) - def fit(self, order): + def fit(self, order: int) -> None: """ Do polynomial fitting and set the parameters. Uses numpy polyfit. @@ -345,7 +365,7 @@ def fit(self, order): self.eos_params = np.polyfit(self.volumes, self.energies, order) self._set_params() - def _set_params(self): + def _set_params(self) -> None: """ Use the fit polynomial to compute the parameter e0, b0, b1 and v0 and set to the _params attribute. @@ -372,7 +392,7 @@ def _func(self, volume, params): x = volume ** (-2 / 3.0) return np.poly1d(list(params))(x) - def fit(self, order=3): + def fit(self, order: int = 3) -> None: """Overridden since this eos works with volume**(2/3) instead of volume.""" x = self.volumes ** (-2 / 3.0) self.eos_params = np.polyfit(x, self.energies, order) @@ -407,7 +427,12 @@ def _set_params(self): class NumericalEOS(PolynomialEOS): """A numerical EOS.""" - def fit(self, min_ndata_factor=3, max_poly_order_factor=5, min_poly_order=2): + def fit( + self, + min_ndata_factor: int = 3, + max_poly_order_factor: int = 5, + min_poly_order: int = 2, + ) -> None: """Fit the input data to the 'numerical eos', the equation of state employed in the quasiharmonic Debye model described in the paper: 10.1103/PhysRevB.90.174107. @@ -539,7 +564,7 @@ class EOS: eos_fit.plot() """ - MODELS: ClassVar = { + MODELS: ClassVar[dict[str, Any]] = { "murnaghan": Murnaghan, "birch": Birch, "birch_murnaghan": BirchMurnaghan, @@ -549,7 +574,7 @@ class EOS: "numerical_eos": NumericalEOS, } - def __init__(self, eos_name="murnaghan"): + def __init__(self, eos_name: str = "murnaghan") -> None: """ Args: eos_name (str): Type of EOS to fit. @@ -562,7 +587,7 @@ def __init__(self, eos_name="murnaghan"): self._eos_name = eos_name self.model = self.MODELS[eos_name] - def fit(self, volumes, energies): + def fit(self, volumes: Sequence[float], energies: Sequence[float]) -> EOSBase: """Fit energies as function of volumes. Args: From c9cc898301a7198488de660829a63ea62fafef3a Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Sun, 9 Feb 2025 09:57:01 +0100 Subject: [PATCH 34/50] TODO: to be reverted, test 3.13 across all platforms --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf747d734c6..ff75d66a6ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,11 +31,11 @@ jobs: # newest version (currently 3.13) on ubuntu (to get complete coverage on unix). config: - os: windows-latest - python: "3.10" + python: "3.13" resolution: highest extras: ci,optional - os: windows-latest - python: "3.12" + python: "3.13" resolution: highest extras: ci,optional,numpy-v1 # Test NP1 on Windows (quite buggy ATM) - os: ubuntu-latest @@ -43,7 +43,7 @@ jobs: resolution: lowest-direct extras: ci,optional - os: macos-latest - python: "3.11" + python: "3.13" resolution: lowest-direct extras: ci # test with only required dependencies installed From 3e55250903546022b9c3e0fe7d086184de8f59ea Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Sun, 9 Feb 2025 10:37:11 +0100 Subject: [PATCH 35/50] add requires decorator to is_valid_bibtex --- src/pymatgen/util/provenance.py | 204 +++++++++++++++++--------------- 1 file changed, 107 insertions(+), 97 deletions(-) diff --git a/src/pymatgen/util/provenance.py b/src/pymatgen/util/provenance.py index 69220bd52c6..2d4e86c1c02 100644 --- a/src/pymatgen/util/provenance.py +++ b/src/pymatgen/util/provenance.py @@ -9,6 +9,7 @@ from io import StringIO from typing import TYPE_CHECKING, NamedTuple +from monty.dev import requires from monty.json import MontyDecoder, MontyEncoder from pymatgen.core.structure import Molecule, Structure @@ -21,6 +22,7 @@ if TYPE_CHECKING: from collections.abc import Sequence + from typing import Any from typing_extensions import Self @@ -29,23 +31,24 @@ __credits__ = "Dan Gunter" -MAX_HNODE_SIZE = 64_000 # maximum size (bytes) of SNL HistoryNode -MAX_DATA_SIZE = 256_000 # maximum size (bytes) of SNL data field -MAX_HNODES = 100 # maximum number of HistoryNodes in SNL file -MAX_BIBTEX_CHARS = 20_000 # maximum number of characters for BibTeX reference +MAX_HNODE_SIZE: int = 64_000 # maximum size (bytes) of SNL HistoryNode +MAX_DATA_SIZE: int = 256_000 # maximum size (bytes) of SNL data field +MAX_HNODES: int = 100 # maximum number of HistoryNodes in SNL file +MAX_BIBTEX_CHARS: int = 20_000 # maximum number of characters for BibTeX reference +@requires(bibtex is not None, "pybtex is not available") def is_valid_bibtex(reference: str) -> bool: """Use pybtex to validate that a reference is in proper BibTeX format. Args: - reference: A String reference in BibTeX format. + reference (str): Reference in BibTeX format. Returns: - bool: True if reference is valid bibtex. + bool: True if reference is valid BibTeX. """ - # str is necessary since pybtex seems to have an issue with unicode. The - # filter expression removes all non-ASCII characters. + # str is necessary since pybtex seems to have an issue with unicode. + # The filter expression removes all non-ASCII characters. str_io = StringIO(reference.encode("ascii", "ignore").decode("ascii")) parser = bibtex.Parser() errors.set_strict_mode(enable=False) @@ -62,8 +65,6 @@ class HistoryNode(NamedTuple): a custom description of how that code was applied (e.g. a site removal Transformation was applied). - A HistoryNode contains three fields: - Attributes: name (str): The name of a code or resource that this Structure encountered in its history. url (str): The URL of that code/resource. @@ -115,16 +116,16 @@ class Author(NamedTuple): name: str email: str - def __str__(self): + def __str__(self) -> str: """String representation of an Author.""" return f"{self.name} <{self.email}>" - def as_dict(self): + def as_dict(self) -> dict[str, str]: """Get MSONable dict.""" return {"name": self.name, "email": self.email} @classmethod - def from_dict(cls, dct: dict) -> Self: + def from_dict(cls, dct: dict[str, str]) -> Self: """ Args: dct (dict): Dict representation. @@ -135,12 +136,17 @@ def from_dict(cls, dct: dict) -> Self: return cls(dct["name"], dct["email"]) @classmethod - def parse_author(cls, author) -> Self: + def parse_author( + cls, + author: str | dict[str, str] | tuple[str, str], + ) -> Self: """Parse an Author object from either a String, dict, or tuple. Args: - author: A String formatted as "NAME ", - (name, email) tuple, or a dict with name and email keys. + author: in one of the three accepted formats: + - A string formatted as "NAME ". + - A (name, email) tuple. + - A dict with name and email as keys. Returns: An Author object. @@ -152,15 +158,18 @@ def parse_author(cls, author) -> Self: if not match or match.start() != 0 or match.end() != len(author): raise ValueError(f"Invalid author format! {author}") return cls(match.groups()[0], match.groups()[1]) + if isinstance(author, dict): return cls.from_dict(author) + if len(author) != 2: raise ValueError(f"Invalid author, should be String or (name, email) tuple: {author}") + return cls(author[0], author[1]) class StructureNL: - """The Structure Notation Language (SNL, pronounced 'snail') is a container for a pymatgen + """The Structure Notation Language (SNL, pronounced "snail") is a container for a pymatgen Structure/Molecule object with some additional fields for enhanced provenance. It is meant to be imported/exported in a JSON file format with the following structure: @@ -178,41 +187,42 @@ class StructureNL: def __init__( self, - struct_or_mol, - authors, - projects=None, - references="", - remarks=None, - data=None, - history=None, - created_at=None, - ): + struct_or_mol: Structure | Molecule, + authors: str | list[dict] | list[str], + projects: list[str] | str | None = None, + references: str = "", + remarks: list[str] | str | None = None, + data: dict | None = None, + history: list[dict] | None = None, + created_at: datetime | None = None, + ) -> None: """ Args: struct_or_mol: A pymatgen Structure/Molecule object - authors: *List* of {"name":'', "email":''} dicts, - *list* of Strings as 'John Doe ', - or a single String with commas separating authors - projects: List of Strings ['Project A', 'Project B'] - references: A String in BibTeX format - remarks: List of Strings ['Remark A', 'Remark B'] - data: A free form dict. Namespaced at the root level with an + authors (list | str): + - List of {"name":"", "email":""} dicts. + - List of strings as "John Doe ". + - A single String with commas separating authors. + projects (list[str] | str): e.g. ["Project A", "Project B"] + references (str): A string in BibTeX format + remarks (list[str]): e.g. ["Remark A", "Remark B"] + data (dict): A free form dict. Namespaced at the root level with an underscore, e.g. {"_materialsproject": } - history: List of dicts - [{'name':'', 'url':'', 'description':{}}] - created_at: A datetime object. + history (list[dict]): e.g. [{"name":"", "url":"", "description":{}}] + created_at (datetime): Creation date. """ - # initialize root-level structure keys + # Initialize root-level structure keys self.structure = struct_or_mol - # turn authors into list of Author objects + # Turn `authors` into list of `Author` objects authors = authors.split(",") if isinstance(authors, str) else authors self.authors = [Author.parse_author(a) for a in authors] - # turn projects into list of Strings + # Turn `projects` into list of strings projects = projects or [] - self.projects = [projects] if isinstance(projects, str) else projects + self.projects: list[str] = [projects] if isinstance(projects, str) else projects - # check that references are valid BibTeX + # Check that references are valid BibTeX if not isinstance(references, str): raise TypeError("Invalid format for SNL reference! Should be empty string or BibTeX string.") if references and not is_valid_bibtex(references): @@ -224,16 +234,16 @@ def __init__( self.references = references - # turn remarks into list of Strings + # Turn `remarks` into list of strings remarks = remarks or [] - self.remarks = [remarks] if isinstance(remarks, str) else remarks + self.remarks: list[str] = [remarks] if isinstance(remarks, str) else remarks - # check remarks limit + # Check `remarks` length limit for remark in self.remarks: if len(remark) > 140: raise ValueError(f"The remark exceeds the maximum size of 140 characters: {len(remark)}") - # check data limit + # Check data length limit self.data = data or {} if not sys.getsizeof(self.data) < MAX_DATA_SIZE: raise ValueError( @@ -248,7 +258,7 @@ def __init__( f"{key=} does not start with an underscore." ) - # check for valid history nodes + # Check for valid history nodes history = history or [] # initialize null fields if len(history) > MAX_HNODES: raise ValueError(f"A maximum of {MAX_HNODES} History nodes are supported, you have {len(history)}!") @@ -258,7 +268,42 @@ def __init__( self.created_at = created_at or f"{datetime.now(tz=timezone.utc):%Y-%m-%d %H:%M:%S.%f%z}" - def as_dict(self): + def __str__(self) -> str: + return "\n".join( + [ + f"{key}\n{getattr(self, key)}" + for key in ( + "structure", + "authors", + "projects", + "references", + "remarks", + "data", + "history", + "created_at", + ) + ] + ) + + def __eq__(self, other: object) -> bool: + """Check for equality between two StructureNL objects.""" + needed_attrs = ( + "structure", + "authors", + "projects", + "references", + "remarks", + "data", + "history", + "created_at", + ) + + if not all(hasattr(other, attr) for attr in needed_attrs): + return NotImplemented + + return all(getattr(self, attr) == getattr(other, attr) for attr in needed_attrs) + + def as_dict(self) -> dict[str, Any]: """Get MSONable dict.""" dct = self.structure.as_dict() dct["@module"] = type(self).__module__ @@ -275,7 +320,7 @@ def as_dict(self): return dct @classmethod - def from_dict(cls, dct: dict) -> Self: + def from_dict(cls, dct: dict[str, Any]) -> Self: """ Args: dct (dict): Dict representation. @@ -305,36 +350,36 @@ def from_dict(cls, dct: dict) -> Self: def from_structures( cls, structures: Sequence[Structure], - authors: Sequence[dict[str, str]], - projects=None, - references="", - remarks=None, - data=None, - histories=None, - created_at=None, + authors, + projects: list[str] | None = None, + references: str = "", + remarks: list[str] | None = None, + data: list[dict] | None = None, + histories: list[list[dict]] | None = None, + created_at: datetime | None = None, ) -> list[Self]: """A convenience method for getting a list of StructureNL objects by specifying structures and metadata separately. Some of the metadata is applied to all of the structures for ease of use. Args: - structures: A list of Structure objects + structures (Sequence[Structure]): Structure objects authors: *List* of {"name":'', "email":''} dicts, *list* of Strings as 'John Doe ', or a single String with commas separating authors - projects: List of Strings ['Project A', 'Project B']. This + projects (list[str]): e.g. ["Project A", "Project B"]. This applies to all structures. - references: A String in BibTeX format. Again, this applies to all + references (str): A String in BibTeX format. This applies to all structures. - remarks: List of Strings ['Remark A', 'Remark B'] - data: A list of free form dict. Namespaced at the root level + remarks (list[str]): e.g. ["Remark A", "Remark B"]. + data (list[dict]): Free form dicts. Namespaced at the root level with an underscore, e.g. {"_materialsproject":} . The length of data should be the same as the list of structures if not None. - histories: List of list of dicts - [[{'name':'', 'url':'', - 'description':{}}], ...] The length of histories should be the + histories(list[list[dict]]): e.g. [[{"name":"", "url":"", + "description":{}}], ...] The length of histories should be the same as the list of structures if not None. - created_at: A datetime object + created_at (datetime): Creation date. """ data = [{}] * len(structures) if data is None else data histories = [[]] * len(structures) if histories is None else histories @@ -354,38 +399,3 @@ def from_structures( snl_list.append(snl) return snl_list - - def __str__(self): - return "\n".join( - [ - f"{key}\n{getattr(self, key)}" - for key in ( - "structure", - "authors", - "projects", - "references", - "remarks", - "data", - "history", - "created_at", - ) - ] - ) - - def __eq__(self, other: object) -> bool: - """Check for equality between two StructureNL objects.""" - needed_attrs = ( - "structure", - "authors", - "projects", - "references", - "remarks", - "data", - "history", - "created_at", - ) - - if not all(hasattr(other, attr) for attr in needed_attrs): - return NotImplemented - - return all(getattr(self, attr) == getattr(other, attr) for attr in needed_attrs) From 5fd64ababaad6e2d644e7d149f2fb5797c199317 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Sun, 9 Feb 2025 10:59:08 +0100 Subject: [PATCH 36/50] skip prototype test if pybtex is not available --- src/pymatgen/analysis/prototypes.py | 39 ++++++++++++++++++----------- src/pymatgen/util/provenance.py | 2 ++ tests/analysis/test_prototypes.py | 8 +++++- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/pymatgen/analysis/prototypes.py b/src/pymatgen/analysis/prototypes.py index e08e6adaca3..5cf9df533d0 100644 --- a/src/pymatgen/analysis/prototypes.py +++ b/src/pymatgen/analysis/prototypes.py @@ -48,22 +48,27 @@ class AflowPrototypeMatcher: https://doi.org/10.1016/j.commatsci.2017.01.017 """ - def __init__(self, initial_ltol=0.2, initial_stol=0.3, initial_angle_tol=5): + def __init__( + self, + initial_ltol: float = 0.2, + initial_stol: float = 0.3, + initial_angle_tol: float = 5, + ) -> None: """ Tolerances as defined in StructureMatcher. Tolerances will be gradually decreased until only a single match is found (if possible). Args: - initial_ltol: fractional length tolerance - initial_stol: site tolerance - initial_angle_tol: angle tolerance + initial_ltol (float): fractional length tolerance. + initial_stol (float): site tolerance. + initial_angle_tol (float): angle tolerance. """ self.initial_ltol = initial_ltol self.initial_stol = initial_stol self.initial_angle_tol = initial_angle_tol # Preprocess AFLOW prototypes - self._aflow_prototype_library = [] + self._aflow_prototype_library: list[tuple[Structure, dict]] = [] for dct in AFLOW_PROTOTYPE_LIBRARY: structure: Structure = dct["snl"].structure reduced_structure = self._preprocess_structure(structure) @@ -73,7 +78,11 @@ def __init__(self, initial_ltol=0.2, initial_stol=0.3, initial_angle_tol=5): def _preprocess_structure(structure: Structure) -> Structure: return structure.get_reduced_structure(reduction_algo="niggli").get_primitive_structure() - def _match_prototype(self, structure_matcher: StructureMatcher, reduced_structure: Structure): + def _match_prototype( + self, + structure_matcher: StructureMatcher, + reduced_structure: Structure, + ) -> list[dict]: tags = [] for aflow_reduced_structure, dct in self._aflow_prototype_library: # Since both structures are already reduced, we can skip the structure reduction step @@ -84,7 +93,7 @@ def _match_prototype(self, structure_matcher: StructureMatcher, reduced_structur tags.append(dct) return tags - def _match_single_prototype(self, structure: Structure): + def _match_single_prototype(self, structure: Structure) -> list[dict]: sm = StructureMatcher( ltol=self.initial_ltol, stol=self.initial_stol, @@ -102,23 +111,23 @@ def _match_single_prototype(self, structure: Structure): break return tags - def get_prototypes(self, structure: Structure) -> list | None: + def get_prototypes(self, structure: Structure) -> list[dict] | None: """Get prototype(s) structures for a given input structure. If you use this method in your work, please cite the appropriate AFLOW publication: - Mehl, M. J., Hicks, D., Toher, C., Levy, O., Hanson, R. M., Hart, G., & Curtarolo, - S. (2017). The AFLOW library of crystallographic prototypes: part 1. Computational - Materials Science, 136, S1-S828. https://doi.org/10.1016/j.commatsci.2017.01.017 + Mehl, M. J., Hicks, D., Toher, C., Levy, O., Hanson, R. M., Hart, G., & Curtarolo, + S. (2017). The AFLOW library of crystallographic prototypes: part 1. Computational + Materials Science, 136, S1-S828. https://doi.org/10.1016/j.commatsci.2017.01.017 Args: - structure: structure to match + structure (Structure): structure to match Returns: - list | None: A list of dicts with keys 'snl' for the matched prototype and - 'tags', a dict of tags ('mineral', 'strukturbericht' and 'aflow') of that + list[dict] | None: A list of dicts with keys "snl" for the matched prototype and + "tags", a dict of tags ("mineral", "strukturbericht" and "aflow") of that prototype. This should be a list containing just a single entry, but it is possible a material can match multiple prototypes. """ - tags = self._match_single_prototype(structure) + tags: list[dict] = self._match_single_prototype(structure) return tags or None diff --git a/src/pymatgen/util/provenance.py b/src/pymatgen/util/provenance.py index 2d4e86c1c02..cf61a6d400e 100644 --- a/src/pymatgen/util/provenance.py +++ b/src/pymatgen/util/provenance.py @@ -225,8 +225,10 @@ def __init__( # Check that references are valid BibTeX if not isinstance(references, str): raise TypeError("Invalid format for SNL reference! Should be empty string or BibTeX string.") + if references and not is_valid_bibtex(references): raise ValueError("Invalid format for SNL reference! Should be BibTeX string.") + if len(references) > MAX_BIBTEX_CHARS: raise ValueError( f"The BibTeX string must be fewer than {MAX_BIBTEX_CHARS} chars, you have {len(references)}" diff --git a/tests/analysis/test_prototypes.py b/tests/analysis/test_prototypes.py index d2a16258d03..40738171ec8 100644 --- a/tests/analysis/test_prototypes.py +++ b/tests/analysis/test_prototypes.py @@ -1,6 +1,12 @@ from __future__ import annotations -from pymatgen.analysis.prototypes import AflowPrototypeMatcher +import pytest + +try: + from pymatgen.analysis.prototypes import AflowPrototypeMatcher +except RuntimeError: + pytest.skip("pybtex is not available", allow_module_level=True) + from pymatgen.util.testing import PymatgenTest From b04042cffaa503efb04e54cc5bfe22f648406f3d Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Sun, 9 Feb 2025 11:03:29 +0100 Subject: [PATCH 37/50] more readable error msg from the start --- src/pymatgen/analysis/prototypes.py | 5 ++++- tests/analysis/test_prototypes.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pymatgen/analysis/prototypes.py b/src/pymatgen/analysis/prototypes.py index 5cf9df533d0..2baf2476628 100644 --- a/src/pymatgen/analysis/prototypes.py +++ b/src/pymatgen/analysis/prototypes.py @@ -25,7 +25,10 @@ from pymatgen.core.structure import Structure MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) -AFLOW_PROTOTYPE_LIBRARY = loadfn(f"{MODULE_DIR}/aflow_prototypes.json") +try: + AFLOW_PROTOTYPE_LIBRARY = loadfn(f"{MODULE_DIR}/aflow_prototypes.json") +except RuntimeError as exc: + raise ImportError("pybtex is needed to load AFLOW library") from exc @due.dcite( diff --git a/tests/analysis/test_prototypes.py b/tests/analysis/test_prototypes.py index 40738171ec8..06a84456007 100644 --- a/tests/analysis/test_prototypes.py +++ b/tests/analysis/test_prototypes.py @@ -4,7 +4,7 @@ try: from pymatgen.analysis.prototypes import AflowPrototypeMatcher -except RuntimeError: +except ImportError: pytest.skip("pybtex is not available", allow_module_level=True) from pymatgen.util.testing import PymatgenTest From 9f2dda1e5c760e65704d47ec469c4c6c6e08636c Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sun, 9 Feb 2025 14:36:39 +0100 Subject: [PATCH 38/50] make pybtex optional --- pyproject.toml | 2 +- src/pymatgen/io/cif.py | 4 ++-- src/pymatgen/util/provenance.py | 14 ++++++-------- tests/analysis/test_cost.py | 8 ++++++++ tests/io/test_cif.py | 2 ++ tests/util/test_provenance.py | 7 +++++++ 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 65913d6f660..f8958275596 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,6 @@ dependencies = [ "palettable>=3.3.3", "pandas>=2", "plotly>=4.5.0,<6.0.0", - "pybtex>=0.24.0", "requests>=2.32", "ruamel.yaml>=0.17.0", # scipy<1.14.1 is incompatible with NumPy 2.0 on Windows @@ -110,6 +109,7 @@ optional = [ "matplotlib>=3.8", "openbabel-wheel>=3.1.1.20", "phonopy>=2.33.3", + "pybtex>=0.24.0", # TODO: don't work on MacOS Python 3.13 "seekpath>=2.0.1", ] # moyopy[interface] includes ase diff --git a/src/pymatgen/io/cif.py b/src/pymatgen/io/cif.py index 0bb6413402e..0d1cfbf7bcf 100644 --- a/src/pymatgen/io/cif.py +++ b/src/pymatgen/io/cif.py @@ -1357,8 +1357,8 @@ def get_bibtex_string(self) -> str: """ try: from pybtex.database import BibliographyData, Entry - except ImportError: - raise RuntimeError("Bibliographic data extraction requires pybtex.") + except ImportError as exc: + raise RuntimeError("Bibliographic data extraction requires pybtex.") from exc bibtex_keys: dict[str, tuple[str, ...]] = { "author": ("_publ_author_name", "_citation_author_name"), diff --git a/src/pymatgen/util/provenance.py b/src/pymatgen/util/provenance.py index cf61a6d400e..682e2206c19 100644 --- a/src/pymatgen/util/provenance.py +++ b/src/pymatgen/util/provenance.py @@ -9,17 +9,10 @@ from io import StringIO from typing import TYPE_CHECKING, NamedTuple -from monty.dev import requires from monty.json import MontyDecoder, MontyEncoder from pymatgen.core.structure import Molecule, Structure -try: - from pybtex import errors - from pybtex.database.input import bibtex -except ImportError: - pybtex = bibtex = errors = None - if TYPE_CHECKING: from collections.abc import Sequence from typing import Any @@ -37,7 +30,6 @@ MAX_BIBTEX_CHARS: int = 20_000 # maximum number of characters for BibTeX reference -@requires(bibtex is not None, "pybtex is not available") def is_valid_bibtex(reference: str) -> bool: """Use pybtex to validate that a reference is in proper BibTeX format. @@ -47,6 +39,12 @@ def is_valid_bibtex(reference: str) -> bool: Returns: bool: True if reference is valid BibTeX. """ + try: + from pybtex import errors + from pybtex.database.input import bibtex + except ImportError as exc: + raise ImportError("pybtex is not available") from exc + # str is necessary since pybtex seems to have an issue with unicode. # The filter expression removes all non-ASCII characters. str_io = StringIO(reference.encode("ascii", "ignore").decode("ascii")) diff --git a/tests/analysis/test_cost.py b/tests/analysis/test_cost.py index c20bed2fd54..dda0eda1a5b 100644 --- a/tests/analysis/test_cost.py +++ b/tests/analysis/test_cost.py @@ -2,11 +2,19 @@ from unittest import TestCase +import pytest from pytest import approx from pymatgen.analysis.cost import CostAnalyzer, CostDBCSV, CostDBElements from pymatgen.util.testing import TEST_FILES_DIR +try: + # Not using find_spec because it would error out during import + import pybtex.database # noqa: F401 + +except ImportError: + pytest.skip("pybtex is not available", allow_module_level=True) + TEST_DIR = f"{TEST_FILES_DIR}/analysis/cost" diff --git a/tests/io/test_cif.py b/tests/io/test_cif.py index 040ff45ee96..2dcc1a0c312 100644 --- a/tests/io/test_cif.py +++ b/tests/io/test_cif.py @@ -13,6 +13,7 @@ try: import pybtex + import pybtex.database except ImportError: pybtex = None @@ -165,6 +166,7 @@ def test_long_loop(self): class TestCifIO(PymatgenTest): + @pytest.mark.skipif(pybtex is None, reason="pybtex not present") def test_cif_parser(self): parser = CifParser(f"{TEST_FILES_DIR}/cif/LiFePO4.cif") for struct in parser.parse_structures(): diff --git a/tests/util/test_provenance.py b/tests/util/test_provenance.py index c44817d6982..299d5dfe25b 100644 --- a/tests/util/test_provenance.py +++ b/tests/util/test_provenance.py @@ -11,6 +11,13 @@ from pymatgen.core.structure import Molecule, Structure from pymatgen.util.provenance import Author, HistoryNode, StructureNL +try: + # Not using find_spec because it would error out during import + import pybtex.database # noqa: F401 + +except ImportError: + pytest.skip("pybtex is not available", allow_module_level=True) + __author__ = "Anubhav Jain" __credits__ = "Shyue Ping Ong" __copyright__ = "Copyright 2012, The Materials Project" From 4825b12724c48868dbbabbc8d81c6a6b4ed1afc0 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 14 Feb 2025 15:43:37 +0100 Subject: [PATCH 39/50] Revert "add types for analysis.eos" This reverts commit 26007e9663f9dd81e8da759cb23937470615c1af. --- src/pymatgen/analysis/eos.py | 97 +++++++++++++----------------------- 1 file changed, 36 insertions(+), 61 deletions(-) diff --git a/src/pymatgen/analysis/eos.py b/src/pymatgen/analysis/eos.py index 67f171eba45..f25844eaec7 100644 --- a/src/pymatgen/analysis/eos.py +++ b/src/pymatgen/analysis/eos.py @@ -24,8 +24,7 @@ from pymatgen.util.plotting import add_fig_kwargs, get_ax_fig, pretty_plot if TYPE_CHECKING: - from collections.abc import Sequence - from typing import Any, ClassVar + from typing import ClassVar import matplotlib.pyplot as plt @@ -41,11 +40,7 @@ class EOSBase(ABC): implementations. """ - def __init__( - self, - volumes: Sequence[float], - energies: Sequence[float], - ) -> None: + def __init__(self, volumes, energies): """ Args: volumes (Sequence[float]): in Ang^3. @@ -55,28 +50,18 @@ def __init__( self.energies = np.array(energies) # minimum energy(e0), buk modulus(b0), # derivative of bulk modulus w.r.t. pressure(b1), minimum volume(v0) - self._params: Sequence | None = None + self._params = None # the eos function parameters. It is the same as _params except for # equation of states that uses polynomial fits(delta_factor and # numerical_eos) - self.eos_params: Sequence | None = None + self.eos_params = None - def __call__(self, volume: float) -> float: - """ - Args: - volume (float | list[float]): volume(s) in Ang^3. - - Returns: - Compute EOS with this volume. - """ - return self.func(volume) - - def _initial_guess(self) -> tuple[float, float, float, float]: + def _initial_guess(self): """ Quadratic fit to get an initial guess for the parameters. Returns: - tuple[float, float, float, float]: e0, b0, b1, v0 + tuple: 4 floats for (e0, b0, b1, v0) """ a, b, c = np.polyfit(self.volumes, self.energies, 2) self.eos_params = [a, b, c] @@ -93,7 +78,7 @@ def _initial_guess(self) -> tuple[float, float, float, float]: return e0, b0, b1, v0 - def fit(self) -> None: + def fit(self): """ Do the fitting. Does least square fitting. If you want to use custom fitting, must override this. @@ -135,20 +120,24 @@ def func(self, volume): """ return self._func(np.array(volume), self.eos_params) + def __call__(self, volume: float) -> float: + """ + Args: + volume (float | list[float]): volume(s) in Ang^3. + + Returns: + Compute EOS with this volume. + """ + return self.func(volume) + @property def e0(self) -> float: """The min energy.""" - if self._params is None: - raise RuntimeError("params have not be initialized.") - return self._params[0] @property def b0(self) -> float: """The bulk modulus in units of energy/unit of volume^3.""" - if self._params is None: - raise RuntimeError("params have not be initialized.") - return self._params[1] @property @@ -167,18 +156,11 @@ def v0(self): return self._params[3] @property - def results(self) -> dict[str, Any]: + def results(self): """A summary dict.""" return {"e0": self.e0, "b0": self.b0, "b1": self.b1, "v0": self.v0} - def plot( - self, - width: float = 8, - height: float | None = None, - ax: plt.Axes = None, - dpi: float | None = None, - **kwargs, - ) -> plt.Axes: + def plot(self, width=8, height=None, ax: plt.Axes = None, dpi=None, **kwargs): """ Plot the equation of state. @@ -188,7 +170,7 @@ def plot( golden ratio. ax (plt.Axes): If supplied, changes will be made to the existing Axes. Otherwise, new Axes will be created. - dpi (float): DPI. + dpi: kwargs (dict): additional args fed to pyplot.plot. supported keys: style, color, text, label @@ -229,18 +211,16 @@ def plot( return ax @add_fig_kwargs - def plot_ax( - self, - ax: plt.Axes | None = None, - fontsize: float = 12, - **kwargs, - ) -> plt.Figure: + def plot_ax(self, ax: plt.Axes = None, fontsize=12, **kwargs): """ Plot the equation of state on axis `ax`. Args: ax: matplotlib Axes or None if a new figure should be created. fontsize: Legend fontsize. + color (str): plot color. + label (str): Plot label + text (str): Legend text (options) Returns: plt.Figure: matplotlib figure. @@ -290,7 +270,7 @@ def plot_ax( class Murnaghan(EOSBase): """Murnaghan EOS.""" - def _func(self, volume, params: tuple[float, float, float, float]): + def _func(self, volume, params): """From PRB 28,5480 (1983).""" e0, b0, b1, v0 = tuple(params) return e0 + b0 * volume / b1 * (((v0 / volume) ** b1) / (b1 - 1.0) + 1.0) - v0 * b0 / (b1 - 1.0) @@ -299,7 +279,7 @@ def _func(self, volume, params: tuple[float, float, float, float]): class Birch(EOSBase): """Birch EOS.""" - def _func(self, volume, params: tuple[float, float, float, float]): + def _func(self, volume, params): """From Intermetallic compounds: Principles and Practice, Vol. I: Principles Chapter 9 pages 195-210 by M. Mehl. B. Klein, D. Papaconstantopoulos. @@ -316,7 +296,7 @@ def _func(self, volume, params: tuple[float, float, float, float]): class BirchMurnaghan(EOSBase): """BirchMurnaghan EOS.""" - def _func(self, volume, params: tuple[float, float, float, float]): + def _func(self, volume, params): """BirchMurnaghan equation from PRB 70, 224107.""" e0, b0, b1, v0 = tuple(params) eta = (v0 / volume) ** (1 / 3) @@ -326,7 +306,7 @@ def _func(self, volume, params: tuple[float, float, float, float]): class PourierTarantola(EOSBase): """Pourier-Tarantola EOS.""" - def _func(self, volume, params: tuple[float, float, float, float]): + def _func(self, volume, params): """Pourier-Tarantola equation from PRB 70, 224107.""" e0, b0, b1, v0 = tuple(params) eta = (volume / v0) ** (1 / 3) @@ -337,7 +317,7 @@ def _func(self, volume, params: tuple[float, float, float, float]): class Vinet(EOSBase): """Vinet EOS.""" - def _func(self, volume, params: tuple[float, float, float, float]): + def _func(self, volume, params): """Vinet equation from PRB 70, 224107.""" e0, b0, b1, v0 = tuple(params) eta = (volume / v0) ** (1 / 3) @@ -355,7 +335,7 @@ class PolynomialEOS(EOSBase): def _func(self, volume, params): return np.poly1d(list(params))(volume) - def fit(self, order: int) -> None: + def fit(self, order): """ Do polynomial fitting and set the parameters. Uses numpy polyfit. @@ -365,7 +345,7 @@ def fit(self, order: int) -> None: self.eos_params = np.polyfit(self.volumes, self.energies, order) self._set_params() - def _set_params(self) -> None: + def _set_params(self): """ Use the fit polynomial to compute the parameter e0, b0, b1 and v0 and set to the _params attribute. @@ -392,7 +372,7 @@ def _func(self, volume, params): x = volume ** (-2 / 3.0) return np.poly1d(list(params))(x) - def fit(self, order: int = 3) -> None: + def fit(self, order=3): """Overridden since this eos works with volume**(2/3) instead of volume.""" x = self.volumes ** (-2 / 3.0) self.eos_params = np.polyfit(x, self.energies, order) @@ -427,12 +407,7 @@ def _set_params(self): class NumericalEOS(PolynomialEOS): """A numerical EOS.""" - def fit( - self, - min_ndata_factor: int = 3, - max_poly_order_factor: int = 5, - min_poly_order: int = 2, - ) -> None: + def fit(self, min_ndata_factor=3, max_poly_order_factor=5, min_poly_order=2): """Fit the input data to the 'numerical eos', the equation of state employed in the quasiharmonic Debye model described in the paper: 10.1103/PhysRevB.90.174107. @@ -564,7 +539,7 @@ class EOS: eos_fit.plot() """ - MODELS: ClassVar[dict[str, Any]] = { + MODELS: ClassVar = { "murnaghan": Murnaghan, "birch": Birch, "birch_murnaghan": BirchMurnaghan, @@ -574,7 +549,7 @@ class EOS: "numerical_eos": NumericalEOS, } - def __init__(self, eos_name: str = "murnaghan") -> None: + def __init__(self, eos_name="murnaghan"): """ Args: eos_name (str): Type of EOS to fit. @@ -587,7 +562,7 @@ def __init__(self, eos_name: str = "murnaghan") -> None: self._eos_name = eos_name self.model = self.MODELS[eos_name] - def fit(self, volumes: Sequence[float], energies: Sequence[float]) -> EOSBase: + def fit(self, volumes, energies): """Fit energies as function of volumes. Args: From 2692696b7b033c2d605c53255fa245eb66c8e6f8 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 14 Feb 2025 15:47:43 +0100 Subject: [PATCH 40/50] revert: make pybtex optional --- src/pymatgen/util/provenance.py | 214 +++++++++++++++----------------- 1 file changed, 103 insertions(+), 111 deletions(-) diff --git a/src/pymatgen/util/provenance.py b/src/pymatgen/util/provenance.py index 682e2206c19..7057477deac 100644 --- a/src/pymatgen/util/provenance.py +++ b/src/pymatgen/util/provenance.py @@ -13,9 +13,14 @@ from pymatgen.core.structure import Molecule, Structure +try: + from pybtex import errors + from pybtex.database.input import bibtex +except ImportError: + pybtex = bibtex = errors = None + if TYPE_CHECKING: from collections.abc import Sequence - from typing import Any from typing_extensions import Self @@ -24,29 +29,23 @@ __credits__ = "Dan Gunter" -MAX_HNODE_SIZE: int = 64_000 # maximum size (bytes) of SNL HistoryNode -MAX_DATA_SIZE: int = 256_000 # maximum size (bytes) of SNL data field -MAX_HNODES: int = 100 # maximum number of HistoryNodes in SNL file -MAX_BIBTEX_CHARS: int = 20_000 # maximum number of characters for BibTeX reference +MAX_HNODE_SIZE = 64_000 # maximum size (bytes) of SNL HistoryNode +MAX_DATA_SIZE = 256_000 # maximum size (bytes) of SNL data field +MAX_HNODES = 100 # maximum number of HistoryNodes in SNL file +MAX_BIBTEX_CHARS = 20_000 # maximum number of characters for BibTeX reference def is_valid_bibtex(reference: str) -> bool: """Use pybtex to validate that a reference is in proper BibTeX format. Args: - reference (str): Reference in BibTeX format. + reference: A String reference in BibTeX format. Returns: - bool: True if reference is valid BibTeX. + bool: True if reference is valid bibtex. """ - try: - from pybtex import errors - from pybtex.database.input import bibtex - except ImportError as exc: - raise ImportError("pybtex is not available") from exc - - # str is necessary since pybtex seems to have an issue with unicode. - # The filter expression removes all non-ASCII characters. + # str is necessary since pybtex seems to have an issue with unicode. The + # filter expression removes all non-ASCII characters. str_io = StringIO(reference.encode("ascii", "ignore").decode("ascii")) parser = bibtex.Parser() errors.set_strict_mode(enable=False) @@ -63,6 +62,8 @@ class HistoryNode(NamedTuple): a custom description of how that code was applied (e.g. a site removal Transformation was applied). + A HistoryNode contains three fields: + Attributes: name (str): The name of a code or resource that this Structure encountered in its history. url (str): The URL of that code/resource. @@ -114,16 +115,16 @@ class Author(NamedTuple): name: str email: str - def __str__(self) -> str: + def __str__(self): """String representation of an Author.""" return f"{self.name} <{self.email}>" - def as_dict(self) -> dict[str, str]: + def as_dict(self): """Get MSONable dict.""" return {"name": self.name, "email": self.email} @classmethod - def from_dict(cls, dct: dict[str, str]) -> Self: + def from_dict(cls, dct: dict) -> Self: """ Args: dct (dict): Dict representation. @@ -134,17 +135,12 @@ def from_dict(cls, dct: dict[str, str]) -> Self: return cls(dct["name"], dct["email"]) @classmethod - def parse_author( - cls, - author: str | dict[str, str] | tuple[str, str], - ) -> Self: + def parse_author(cls, author) -> Self: """Parse an Author object from either a String, dict, or tuple. Args: - author: in one of the three accepted formats: - - A string formatted as "NAME ". - - A (name, email) tuple. - - A dict with name and email as keys. + author: A String formatted as "NAME ", + (name, email) tuple, or a dict with name and email keys. Returns: An Author object. @@ -156,18 +152,15 @@ def parse_author( if not match or match.start() != 0 or match.end() != len(author): raise ValueError(f"Invalid author format! {author}") return cls(match.groups()[0], match.groups()[1]) - if isinstance(author, dict): return cls.from_dict(author) - if len(author) != 2: raise ValueError(f"Invalid author, should be String or (name, email) tuple: {author}") - return cls(author[0], author[1]) class StructureNL: - """The Structure Notation Language (SNL, pronounced "snail") is a container for a pymatgen + """The Structure Notation Language (SNL, pronounced 'snail') is a container for a pymatgen Structure/Molecule object with some additional fields for enhanced provenance. It is meant to be imported/exported in a JSON file format with the following structure: @@ -185,42 +178,41 @@ class StructureNL: def __init__( self, - struct_or_mol: Structure | Molecule, - authors: str | list[dict] | list[str], - projects: list[str] | str | None = None, - references: str = "", - remarks: list[str] | str | None = None, - data: dict | None = None, - history: list[dict] | None = None, - created_at: datetime | None = None, - ) -> None: + struct_or_mol, + authors, + projects=None, + references="", + remarks=None, + data=None, + history=None, + created_at=None, + ): """ Args: struct_or_mol: A pymatgen Structure/Molecule object - authors (list | str): - - List of {"name":"", "email":""} dicts. - - List of strings as "John Doe ". - - A single String with commas separating authors. - projects (list[str] | str): e.g. ["Project A", "Project B"] - references (str): A string in BibTeX format - remarks (list[str]): e.g. ["Remark A", "Remark B"] - data (dict): A free form dict. Namespaced at the root level with an + authors: *List* of {"name":'', "email":''} dicts, + *list* of Strings as 'John Doe ', + or a single String with commas separating authors + projects: List of Strings ['Project A', 'Project B'] + references: A String in BibTeX format + remarks: List of Strings ['Remark A', 'Remark B'] + data: A free form dict. Namespaced at the root level with an underscore, e.g. {"_materialsproject": } - history (list[dict]): e.g. [{"name":"", "url":"", "description":{}}] - created_at (datetime): Creation date. + history: List of dicts - [{'name':'', 'url':'', 'description':{}}] + created_at: A datetime object. """ - # Initialize root-level structure keys + # initialize root-level structure keys self.structure = struct_or_mol - # Turn `authors` into list of `Author` objects + # turn authors into list of Author objects authors = authors.split(",") if isinstance(authors, str) else authors self.authors = [Author.parse_author(a) for a in authors] - # Turn `projects` into list of strings + # turn projects into list of Strings projects = projects or [] - self.projects: list[str] = [projects] if isinstance(projects, str) else projects + self.projects = [projects] if isinstance(projects, str) else projects - # Check that references are valid BibTeX + # check that references are valid BibTeX if not isinstance(references, str): raise TypeError("Invalid format for SNL reference! Should be empty string or BibTeX string.") @@ -234,16 +226,16 @@ def __init__( self.references = references - # Turn `remarks` into list of strings + # turn remarks into list of Strings remarks = remarks or [] - self.remarks: list[str] = [remarks] if isinstance(remarks, str) else remarks + self.remarks = [remarks] if isinstance(remarks, str) else remarks - # Check `remarks` length limit + # check remarks limit for remark in self.remarks: if len(remark) > 140: raise ValueError(f"The remark exceeds the maximum size of 140 characters: {len(remark)}") - # Check data length limit + # check data limit self.data = data or {} if not sys.getsizeof(self.data) < MAX_DATA_SIZE: raise ValueError( @@ -258,7 +250,7 @@ def __init__( f"{key=} does not start with an underscore." ) - # Check for valid history nodes + # check for valid history nodes history = history or [] # initialize null fields if len(history) > MAX_HNODES: raise ValueError(f"A maximum of {MAX_HNODES} History nodes are supported, you have {len(history)}!") @@ -268,42 +260,7 @@ def __init__( self.created_at = created_at or f"{datetime.now(tz=timezone.utc):%Y-%m-%d %H:%M:%S.%f%z}" - def __str__(self) -> str: - return "\n".join( - [ - f"{key}\n{getattr(self, key)}" - for key in ( - "structure", - "authors", - "projects", - "references", - "remarks", - "data", - "history", - "created_at", - ) - ] - ) - - def __eq__(self, other: object) -> bool: - """Check for equality between two StructureNL objects.""" - needed_attrs = ( - "structure", - "authors", - "projects", - "references", - "remarks", - "data", - "history", - "created_at", - ) - - if not all(hasattr(other, attr) for attr in needed_attrs): - return NotImplemented - - return all(getattr(self, attr) == getattr(other, attr) for attr in needed_attrs) - - def as_dict(self) -> dict[str, Any]: + def as_dict(self): """Get MSONable dict.""" dct = self.structure.as_dict() dct["@module"] = type(self).__module__ @@ -320,7 +277,7 @@ def as_dict(self) -> dict[str, Any]: return dct @classmethod - def from_dict(cls, dct: dict[str, Any]) -> Self: + def from_dict(cls, dct: dict) -> Self: """ Args: dct (dict): Dict representation. @@ -350,36 +307,36 @@ def from_dict(cls, dct: dict[str, Any]) -> Self: def from_structures( cls, structures: Sequence[Structure], - authors, - projects: list[str] | None = None, - references: str = "", - remarks: list[str] | None = None, - data: list[dict] | None = None, - histories: list[list[dict]] | None = None, - created_at: datetime | None = None, + authors: Sequence[dict[str, str]], + projects=None, + references="", + remarks=None, + data=None, + histories=None, + created_at=None, ) -> list[Self]: """A convenience method for getting a list of StructureNL objects by specifying structures and metadata separately. Some of the metadata is applied to all of the structures for ease of use. Args: - structures (Sequence[Structure]): Structure objects + structures: A list of Structure objects authors: *List* of {"name":'', "email":''} dicts, *list* of Strings as 'John Doe ', or a single String with commas separating authors - projects (list[str]): e.g. ["Project A", "Project B"]. This + projects: List of Strings ['Project A', 'Project B']. This applies to all structures. - references (str): A String in BibTeX format. This applies to all + references: A String in BibTeX format. Again, this applies to all structures. - remarks (list[str]): e.g. ["Remark A", "Remark B"]. - data (list[dict]): Free form dicts. Namespaced at the root level + remarks: List of Strings ['Remark A', 'Remark B'] + data: A list of free form dict. Namespaced at the root level with an underscore, e.g. {"_materialsproject":} . The length of data should be the same as the list of structures if not None. - histories(list[list[dict]]): e.g. [[{"name":"", "url":"", - "description":{}}], ...] The length of histories should be the + histories: List of list of dicts - [[{'name':'', 'url':'', + 'description':{}}], ...] The length of histories should be the same as the list of structures if not None. - created_at (datetime): Creation date. + created_at: A datetime object """ data = [{}] * len(structures) if data is None else data histories = [[]] * len(structures) if histories is None else histories @@ -399,3 +356,38 @@ def from_structures( snl_list.append(snl) return snl_list + + def __str__(self): + return "\n".join( + [ + f"{key}\n{getattr(self, key)}" + for key in ( + "structure", + "authors", + "projects", + "references", + "remarks", + "data", + "history", + "created_at", + ) + ] + ) + + def __eq__(self, other: object) -> bool: + """Check for equality between two StructureNL objects.""" + needed_attrs = ( + "structure", + "authors", + "projects", + "references", + "remarks", + "data", + "history", + "created_at", + ) + + if not all(hasattr(other, attr) for attr in needed_attrs): + return NotImplemented + + return all(getattr(self, attr) == getattr(other, attr) for attr in needed_attrs) From 769b2fbbdbe46f8599a1a0b270fb58df3668f4f3 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 14 Feb 2025 16:03:48 +0100 Subject: [PATCH 41/50] Revert "skip prototype test if pybtex is not available" This reverts commit 5fd64ababaad6e2d644e7d149f2fb5797c199317. --- src/pymatgen/analysis/prototypes.py | 39 +++++++++++------------------ src/pymatgen/util/provenance.py | 2 -- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/pymatgen/analysis/prototypes.py b/src/pymatgen/analysis/prototypes.py index 2baf2476628..3781249c0d2 100644 --- a/src/pymatgen/analysis/prototypes.py +++ b/src/pymatgen/analysis/prototypes.py @@ -51,27 +51,22 @@ class AflowPrototypeMatcher: https://doi.org/10.1016/j.commatsci.2017.01.017 """ - def __init__( - self, - initial_ltol: float = 0.2, - initial_stol: float = 0.3, - initial_angle_tol: float = 5, - ) -> None: + def __init__(self, initial_ltol=0.2, initial_stol=0.3, initial_angle_tol=5): """ Tolerances as defined in StructureMatcher. Tolerances will be gradually decreased until only a single match is found (if possible). Args: - initial_ltol (float): fractional length tolerance. - initial_stol (float): site tolerance. - initial_angle_tol (float): angle tolerance. + initial_ltol: fractional length tolerance + initial_stol: site tolerance + initial_angle_tol: angle tolerance """ self.initial_ltol = initial_ltol self.initial_stol = initial_stol self.initial_angle_tol = initial_angle_tol # Preprocess AFLOW prototypes - self._aflow_prototype_library: list[tuple[Structure, dict]] = [] + self._aflow_prototype_library = [] for dct in AFLOW_PROTOTYPE_LIBRARY: structure: Structure = dct["snl"].structure reduced_structure = self._preprocess_structure(structure) @@ -81,11 +76,7 @@ def __init__( def _preprocess_structure(structure: Structure) -> Structure: return structure.get_reduced_structure(reduction_algo="niggli").get_primitive_structure() - def _match_prototype( - self, - structure_matcher: StructureMatcher, - reduced_structure: Structure, - ) -> list[dict]: + def _match_prototype(self, structure_matcher: StructureMatcher, reduced_structure: Structure): tags = [] for aflow_reduced_structure, dct in self._aflow_prototype_library: # Since both structures are already reduced, we can skip the structure reduction step @@ -96,7 +87,7 @@ def _match_prototype( tags.append(dct) return tags - def _match_single_prototype(self, structure: Structure) -> list[dict]: + def _match_single_prototype(self, structure: Structure): sm = StructureMatcher( ltol=self.initial_ltol, stol=self.initial_stol, @@ -114,23 +105,23 @@ def _match_single_prototype(self, structure: Structure) -> list[dict]: break return tags - def get_prototypes(self, structure: Structure) -> list[dict] | None: + def get_prototypes(self, structure: Structure) -> list | None: """Get prototype(s) structures for a given input structure. If you use this method in your work, please cite the appropriate AFLOW publication: - Mehl, M. J., Hicks, D., Toher, C., Levy, O., Hanson, R. M., Hart, G., & Curtarolo, - S. (2017). The AFLOW library of crystallographic prototypes: part 1. Computational - Materials Science, 136, S1-S828. https://doi.org/10.1016/j.commatsci.2017.01.017 + Mehl, M. J., Hicks, D., Toher, C., Levy, O., Hanson, R. M., Hart, G., & Curtarolo, + S. (2017). The AFLOW library of crystallographic prototypes: part 1. Computational + Materials Science, 136, S1-S828. https://doi.org/10.1016/j.commatsci.2017.01.017 Args: - structure (Structure): structure to match + structure: structure to match Returns: - list[dict] | None: A list of dicts with keys "snl" for the matched prototype and - "tags", a dict of tags ("mineral", "strukturbericht" and "aflow") of that + list | None: A list of dicts with keys 'snl' for the matched prototype and + 'tags', a dict of tags ('mineral', 'strukturbericht' and 'aflow') of that prototype. This should be a list containing just a single entry, but it is possible a material can match multiple prototypes. """ - tags: list[dict] = self._match_single_prototype(structure) + tags = self._match_single_prototype(structure) return tags or None diff --git a/src/pymatgen/util/provenance.py b/src/pymatgen/util/provenance.py index 7057477deac..69220bd52c6 100644 --- a/src/pymatgen/util/provenance.py +++ b/src/pymatgen/util/provenance.py @@ -215,10 +215,8 @@ def __init__( # check that references are valid BibTeX if not isinstance(references, str): raise TypeError("Invalid format for SNL reference! Should be empty string or BibTeX string.") - if references and not is_valid_bibtex(references): raise ValueError("Invalid format for SNL reference! Should be BibTeX string.") - if len(references) > MAX_BIBTEX_CHARS: raise ValueError( f"The BibTeX string must be fewer than {MAX_BIBTEX_CHARS} chars, you have {len(references)}" From ebfd91799cab975d4c63392dd41b17c0c508fa4f Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 14 Feb 2025 16:10:36 +0100 Subject: [PATCH 42/50] reapply requires to is_valid_bibtex --- src/pymatgen/util/provenance.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pymatgen/util/provenance.py b/src/pymatgen/util/provenance.py index 69220bd52c6..b2c8cf8c804 100644 --- a/src/pymatgen/util/provenance.py +++ b/src/pymatgen/util/provenance.py @@ -9,6 +9,7 @@ from io import StringIO from typing import TYPE_CHECKING, NamedTuple +from monty.dev import requires from monty.json import MontyDecoder, MontyEncoder from pymatgen.core.structure import Molecule, Structure @@ -35,6 +36,7 @@ MAX_BIBTEX_CHARS = 20_000 # maximum number of characters for BibTeX reference +@requires(bibtex is not None, "pybtex is not available") def is_valid_bibtex(reference: str) -> bool: """Use pybtex to validate that a reference is in proper BibTeX format. From af559fa24643050523939701ce5df5d9b3ce6a29 Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Fri, 14 Feb 2025 18:24:03 +0100 Subject: [PATCH 43/50] insert debug tags --- src/pymatgen/analysis/eos.py | 7 +++++++ tests/analysis/test_eos.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/src/pymatgen/analysis/eos.py b/src/pymatgen/analysis/eos.py index f25844eaec7..8a9ef7e8e87 100644 --- a/src/pymatgen/analysis/eos.py +++ b/src/pymatgen/analysis/eos.py @@ -143,6 +143,13 @@ def b0(self) -> float: @property def b0_GPa(self) -> FloatWithUnit: """The bulk modulus in GPa. This assumes the energy and volumes are in eV and Ang^3.""" + print(f"{self.b0=}") + print("unit conversion:", FloatWithUnit(1, "eV ang^-3").to("GPa")) # DEBUG: scipy const + import scipy + + print(f"scipy version: {scipy.__version__}") + + print(f"BLAS implementation: {np.show_config()}") return FloatWithUnit(self.b0, "eV ang^-3").to("GPa") @property diff --git a/tests/analysis/test_eos.py b/tests/analysis/test_eos.py index 9a839f8279b..c730e93502f 100644 --- a/tests/analysis/test_eos.py +++ b/tests/analysis/test_eos.py @@ -425,6 +425,9 @@ def test_numerical_eoswrapper(self): def test_numerical_eos_values(self): assert_allclose(self.num_eos_fit.e0, -10.84749, atol=1e-3) assert_allclose(self.num_eos_fit.v0, 40.857201, atol=1e-1) + print(f"{self.num_eos_fit.b0=}") + print(f"{self.num_eos_fit.b0_GPa=}") + assert_allclose(self.num_eos_fit.b0, 0.55, atol=1e-2) assert_allclose(self.num_eos_fit.b0_GPa, 89.0370727, atol=1e-1) assert_allclose(self.num_eos_fit.b1, 4.344039, atol=1e-2) From 4cf635a347f45a85a4aa435d0eeaa0f1bdad4e6c Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Fri, 14 Feb 2025 18:46:39 +0100 Subject: [PATCH 44/50] Revert "insert debug tags" This reverts commit af559fa24643050523939701ce5df5d9b3ce6a29. --- src/pymatgen/analysis/eos.py | 7 ------- tests/analysis/test_eos.py | 3 --- 2 files changed, 10 deletions(-) diff --git a/src/pymatgen/analysis/eos.py b/src/pymatgen/analysis/eos.py index 8a9ef7e8e87..f25844eaec7 100644 --- a/src/pymatgen/analysis/eos.py +++ b/src/pymatgen/analysis/eos.py @@ -143,13 +143,6 @@ def b0(self) -> float: @property def b0_GPa(self) -> FloatWithUnit: """The bulk modulus in GPa. This assumes the energy and volumes are in eV and Ang^3.""" - print(f"{self.b0=}") - print("unit conversion:", FloatWithUnit(1, "eV ang^-3").to("GPa")) # DEBUG: scipy const - import scipy - - print(f"scipy version: {scipy.__version__}") - - print(f"BLAS implementation: {np.show_config()}") return FloatWithUnit(self.b0, "eV ang^-3").to("GPa") @property diff --git a/tests/analysis/test_eos.py b/tests/analysis/test_eos.py index c730e93502f..9a839f8279b 100644 --- a/tests/analysis/test_eos.py +++ b/tests/analysis/test_eos.py @@ -425,9 +425,6 @@ def test_numerical_eoswrapper(self): def test_numerical_eos_values(self): assert_allclose(self.num_eos_fit.e0, -10.84749, atol=1e-3) assert_allclose(self.num_eos_fit.v0, 40.857201, atol=1e-1) - print(f"{self.num_eos_fit.b0=}") - print(f"{self.num_eos_fit.b0_GPa=}") - assert_allclose(self.num_eos_fit.b0, 0.55, atol=1e-2) assert_allclose(self.num_eos_fit.b0_GPa, 89.0370727, atol=1e-1) assert_allclose(self.num_eos_fit.b1, 4.344039, atol=1e-2) From f11974fc19c8c59665ab427797fa5581c653063b Mon Sep 17 00:00:00 2001 From: Haoyu Yang Date: Fri, 14 Feb 2025 18:46:57 +0100 Subject: [PATCH 45/50] add PR tag --- tests/analysis/test_eos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/analysis/test_eos.py b/tests/analysis/test_eos.py index 9a839f8279b..6a855e32fe6 100644 --- a/tests/analysis/test_eos.py +++ b/tests/analysis/test_eos.py @@ -426,7 +426,7 @@ def test_numerical_eos_values(self): assert_allclose(self.num_eos_fit.e0, -10.84749, atol=1e-3) assert_allclose(self.num_eos_fit.v0, 40.857201, atol=1e-1) assert_allclose(self.num_eos_fit.b0, 0.55, atol=1e-2) - assert_allclose(self.num_eos_fit.b0_GPa, 89.0370727, atol=1e-1) + assert_allclose(self.num_eos_fit.b0_GPa, 89.0370727, atol=0.3) # PR 4100 assert_allclose(self.num_eos_fit.b1, 4.344039, atol=1e-2) def test_eos_func(self): From eac06e46e8d2c95a71a147f781de1d12d463863c Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Apr 2025 22:28:07 +0200 Subject: [PATCH 46/50] skip matgl in py 3.13 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a7ae21d3965..56b2dabb9d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,7 @@ Pypi = "https://pypi.org/project/pymatgen" abinit = ["netcdf4>=1.7.2"] ase = ["ase>=3.23.0"] electronic_structure = ["fdint>=2.0.2"] -mlp = ["matgl>=1.2.5"] +mlp = ["matgl>=1.2.5 ; python_version<'3.13'"] numba = ["numba>=0.55"] numpy-v1 = ["numpy>=1.25.0,<2"] # Test NP1 on Windows (buggy at this moment) zeopp = ["pyzeo"] # Note: requires voro++ and zeo++ to be installed as binaries From 6bb58187e563d48851dcbbd61344ff00deba0d4a Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Apr 2025 22:53:43 +0200 Subject: [PATCH 47/50] skip matgl on python 3.13 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f2a73fc0a31..34024c18317 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -110,7 +110,7 @@ jobs: uv pip install $WHEEL_FILE[${{matrix.config.extras}}] --resolution=${{matrix.config.resolution}} - name: Install DGL and MatGL specially on ubuntu. - if: matrix.config.os == 'ubuntu-latest' + if: matrix.config.os == 'ubuntu-latest' && matrix.config.python < '3.13' run: | micromamba activate pmg pip install --upgrade dgl -f https://data.dgl.ai/wheels/torch-2.4/repo.html From d5379cb26a8789a848c78c279581cd0f5bfa0682 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Apr 2025 22:58:21 +0200 Subject: [PATCH 48/50] also catch runtimeerror from requires --- tests/analysis/test_prototypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/analysis/test_prototypes.py b/tests/analysis/test_prototypes.py index fb3fd3f5345..29bf0e35458 100644 --- a/tests/analysis/test_prototypes.py +++ b/tests/analysis/test_prototypes.py @@ -33,7 +33,7 @@ try: from pymatgen.analysis.prototypes import AflowPrototypeMatcher -except ImportError: +except (ImportError, RuntimeError): pytest.skip("pybtex is not available", allow_module_level=True) try: From 003742e648a2eba66a3f2c464cb14985a03b5b18 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 10 Apr 2025 23:03:43 +0200 Subject: [PATCH 49/50] fix excpetion catcher --- tests/analysis/test_prototypes.py | 43 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/tests/analysis/test_prototypes.py b/tests/analysis/test_prototypes.py index 29bf0e35458..d94d7afb968 100644 --- a/tests/analysis/test_prototypes.py +++ b/tests/analysis/test_prototypes.py @@ -7,33 +7,32 @@ import pytest -from pymatgen.analysis.prototypes import ( - WYCKOFF_POSITION_RELAB_DICT, - AflowPrototypeMatcher, - _find_translations, - count_crystal_dof, - count_crystal_sites, - count_distinct_wyckoff_letters, - count_wyckoff_positions, - get_anonymous_formula_from_prototype_formula, - get_formula_from_protostructure_label, - get_protostructure_label, - get_protostructure_label_from_aflow, - get_protostructure_label_from_moyopy, - get_protostructure_label_from_spg_analyzer, - get_protostructure_label_from_spglib, - get_protostructures_from_aflow_label_and_composition, - get_prototype_formula_from_composition, - get_prototype_from_protostructure, - get_random_structure_for_protostructure, -) from pymatgen.core.structure import Composition, Lattice, Structure from pymatgen.symmetry.analyzer import SpacegroupAnalyzer from pymatgen.util.testing import TEST_FILES_DIR, PymatgenTest try: - from pymatgen.analysis.prototypes import AflowPrototypeMatcher -except (ImportError, RuntimeError): + from pymatgen.analysis.prototypes import ( + WYCKOFF_POSITION_RELAB_DICT, + AflowPrototypeMatcher, + _find_translations, + count_crystal_dof, + count_crystal_sites, + count_distinct_wyckoff_letters, + count_wyckoff_positions, + get_anonymous_formula_from_prototype_formula, + get_formula_from_protostructure_label, + get_protostructure_label, + get_protostructure_label_from_aflow, + get_protostructure_label_from_moyopy, + get_protostructure_label_from_spg_analyzer, + get_protostructure_label_from_spglib, + get_protostructures_from_aflow_label_and_composition, + get_prototype_formula_from_composition, + get_prototype_from_protostructure, + get_random_structure_for_protostructure, + ) +except RuntimeError: pytest.skip("pybtex is not available", allow_module_level=True) try: From 38d8b049808bbc41eb69b92501fa9023be3a8752 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 17 Apr 2025 21:53:04 +0200 Subject: [PATCH 50/50] drop python upper pin --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 56b2dabb9d5..8c924a31c8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ Python Materials Genomics is a robust materials analysis code that defines core and molecules with support for many electronic structure codes. It is currently the core analysis code powering the Materials Project (https://materialsproject.org).""" readme = "README.md" -requires-python = ">=3.10,<3.14" +requires-python = ">=3.10" keywords = [ "ABINIT", "VASP",