From f9e835c612bb4445b5312f9451b4bdf659b640ab Mon Sep 17 00:00:00 2001 From: Runze Liu Date: Tue, 9 Dec 2025 17:19:51 -0800 Subject: [PATCH 01/23] Unify model naming convention --- src/matcalc/utils.py | 51 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/src/matcalc/utils.py b/src/matcalc/utils.py index 57570330..94f2c890 100644 --- a/src/matcalc/utils.py +++ b/src/matcalc/utils.py @@ -32,7 +32,6 @@ "SevenNet", "TensorNet", "GRACE", - "TensorPotential", "ORB", "PBE", "r2SCAN", @@ -53,13 +52,50 @@ except Exception: # noqa: BLE001 warnings.warn("Unable to get pre-trained MatGL universal calculators.", stacklevel=1) -# Provide simple aliases for some common models. The key in MODEL_ALIASES must be lower case. +# Different backend libraries (MatGL, MACE, GRACE, etc.) use +# inconsistent or non-canonical model names (e.g., "small-omat-0", +# "TensorNet-MatPES-PBE-v2025.1-PES", "GRACE-1L-OMAT-medium-base"). + +# Users and developers are encouraged to use the model naming convention below. +# MatCalc Model Naming Convention (Unified model ID format): +# [Model]-(#Layers)-[Dataset]-(Functional)-(Version)-(Size)-(Postprocess) +# Not all fields must appear. + +# Examples: +# TensorNet-MatPES-PBE-v2025.1-PES +# MACE-MP-0-medium +# GRACE-1L-OMAT-medium-base +# GRACE-2L-OMAT-large-ft-AM + +# This table maps such identifiers into the backend model names that +# each library expects. + +# This tabel will be gradually expanded as new models are released. + +# Keys must be lowercase and represent canonical identifiers +# Values are the actual model names passed to the backend libraries. + MODEL_ALIASES = { + # Using abbreviations will load the most advanced model. "tensornet": "TensorNet-MatPES-PBE-v2025.1-PES", "m3gnet": "M3GNet-MatPES-PBE-v2025.1-PES", "chgnet": "CHGNet-MatPES-PBE-2025.2.10-2.7M-PES", + "mace": "medium-mpa-0", + "grace": "GRACE-2L-OAM", "pbe": "TensorNet-MatPES-PBE-v2025.1-PES", "r2scan": "TensorNet-MatPES-r2SCAN-v2025.1-PES", + "mace-mp-0-small": "small", + "mace-mp-0-medium": "medium", + "mace-mp-0-large": "large", + "mace-mp-0b-small": "small-0b", + "mace-mp-0b-medium": "medium-0b", + "mace-mp-0b2-small": "small-0b2", + "mace-mp-0b2-medium": "medium-0b2", + "mace-mp-0b2-large": "large-0b2", + "mace-mp-0b3-medium": "medium-0b3", + "mace-mpa-0-medium": "medium-mpa-0", + "mace-omat-0-small": "small-omat-0", + "mace-omat-0-medium": "medium-omat-0", } @@ -373,21 +409,22 @@ def load_universal(name: str | Calculator, **kwargs: Any) -> Calculator: # noqa name = MODEL_ALIASES.get(name.lower(), name) result = PESCalculator.load_matgl(name, **kwargs) - elif name.lower() == "mace": + elif name.lower().startswith("mace"): + name = MODEL_ALIASES.get(name.lower(), name) from mace.calculators import mace_mp - result = mace_mp(**kwargs) + result = mace_mp(model=name, **kwargs) elif name.lower() == "sevennet": from sevenn.calculator import SevenNetCalculator result = SevenNetCalculator(**kwargs) - elif name.lower() == "grace" or name.lower() == "tensorpotential": + elif name.lower().startswith("grace"): + name = MODEL_ALIASES.get(name.lower(), name) from tensorpotential.calculator.foundation_models import grace_fm - kwargs.setdefault("model", "GRACE-2L-OAM") - result = grace_fm(**kwargs) + result = grace_fm(model=name, **kwargs) elif name.lower() == "orb": from orb_models.forcefield.calculator import ORBCalculator From 9ce8db021a57faa8a5694b05d4b24821db1d7817 Mon Sep 17 00:00:00 2001 From: Runze Liu Date: Tue, 9 Dec 2025 20:45:58 -0800 Subject: [PATCH 02/23] Fix the pytest --- src/matcalc/utils.py | 2 +- tests/test_utils.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/matcalc/utils.py b/src/matcalc/utils.py index 94f2c890..bb5df197 100644 --- a/src/matcalc/utils.py +++ b/src/matcalc/utils.py @@ -523,4 +523,4 @@ def to_pmg_molecule(structure: Atoms | Structure | Molecule | IMolecule) -> IMol if isinstance(structure, Atoms): structure = AseAtomsAdaptor.get_molecule(structure) - return Molecule.from_sites(structure) # type: ignore[return-value] + return Molecule.from_sites(structure) # type: ignore[return-value] \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py index 2470a7e8..24aebf31 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -179,6 +179,60 @@ def test_pescalculator_calculate() -> None: def test_aliases() -> None: # Ensures that model aliases always point to valid models. names = [u.name for u in UNIVERSAL_CALCULATORS] + if find_spec("mace"): + from mace.calculators.foundations_models import mace_mp_names + + mace_models = set(mace_mp_names) + else: + # This set is from https://github.com/ACEsuit/mace/blob/main/mace/calculators/foundations_models.py#L37 + mace_models = { + "small", + "medium", + "large", + "small-0b", + "medium-0b", + "small-0b2", + "medium-0b2", + "large-0b2", + "medium-0b3", + "medium-mpa-0", + "small-omat-0", + "medium-omat-0", + "mace-matpes-pbe-0", + "mace-matpes-r2scan-0", + "mh-0", + "mh-1", + } + if find_spec("tensorpotential"): + from tensorpotential.calculator.foundation_models import MODELS_METADATA + + grace_models = set(MODELS_METADATA.keys()) + else: + # This set is from https://github.com/ICAMS/grace-tensorpotential/blob/master/tensorpotential/calculator/foundation_models.py#L46 + grace_models = { + "GRACE-1L-MP-r6", + "GRACE-2L-MP-r5", + "GRACE-2L-MP-r6", + "GRACE-FS-OAM", + "GRACE-1L-OAM", + "GRACE-2L-OAM", + "GRACE-FS-OMAT", + "GRACE-1L-OMAT", + "GRACE-2L-OMAT", + "GRACE-1L-OMAT-medium-base", + "GRACE-1L-OMAT-medium-ft-E", + "GRACE-1L-OMAT-medium-ft-AM", + "GRACE-1L-OMAT-large-base", + "GRACE-1L-OMAT-large-ft-E", + "GRACE-1L-OMAT-large-ft-AM", + "GRACE-2L-OMAT-medium-base", + "GRACE-2L-OMAT-medium-ft-E", + "GRACE-2L-OMAT-medium-ft-AM", + "GRACE-2L-OMAT-large-base", + "GRACE-2L-OMAT-large-ft-E", + "GRACE-2L-OMAT-large-ft-AM", + } + names = names | mace_models | grace_models for v in MODEL_ALIASES.values(): # We are not testing DGL based models. if "M3GNet" not in v and "CHGNet" not in v: From 84db0127886fb0366b065371a01805820449ebdb Mon Sep 17 00:00:00 2001 From: Runze Liu Date: Tue, 9 Dec 2025 20:51:03 -0800 Subject: [PATCH 03/23] Fix the pytest --- src/matcalc/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matcalc/utils.py b/src/matcalc/utils.py index bb5df197..96b6ea04 100644 --- a/src/matcalc/utils.py +++ b/src/matcalc/utils.py @@ -58,12 +58,13 @@ # Users and developers are encouraged to use the model naming convention below. # MatCalc Model Naming Convention (Unified model ID format): -# [Model]-(#Layers)-[Dataset]-(Functional)-(Version)-(Size)-(Postprocess) +# [Model]-(#Layers)-[Dataset]-(Functional)-(Cutoff)-(Version)-(Size)-(Postprocess) # Not all fields must appear. # Examples: # TensorNet-MatPES-PBE-v2025.1-PES # MACE-MP-0-medium +# GRACE-1L-MP-r6 # GRACE-1L-OMAT-medium-base # GRACE-2L-OMAT-large-ft-AM From dc095a9974328fae843654f68b924771ff550e9a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 04:54:20 +0000 Subject: [PATCH 04/23] pre-commit auto-fixes --- src/matcalc/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matcalc/utils.py b/src/matcalc/utils.py index 96b6ea04..fa013127 100644 --- a/src/matcalc/utils.py +++ b/src/matcalc/utils.py @@ -524,4 +524,4 @@ def to_pmg_molecule(structure: Atoms | Structure | Molecule | IMolecule) -> IMol if isinstance(structure, Atoms): structure = AseAtomsAdaptor.get_molecule(structure) - return Molecule.from_sites(structure) # type: ignore[return-value] \ No newline at end of file + return Molecule.from_sites(structure) # type: ignore[return-value] From 35deadeceeb1a86ab7546cd7a3751eea5848a119 Mon Sep 17 00:00:00 2001 From: Runze Liu <146490083+rul048@users.noreply.github.com> Date: Tue, 9 Dec 2025 21:43:04 -0800 Subject: [PATCH 05/23] Fix the pytest Change names from list comprehension to set comprehension. Signed-off-by: Runze Liu <146490083+rul048@users.noreply.github.com> --- tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 24aebf31..bb29b162 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -178,7 +178,7 @@ def test_pescalculator_calculate() -> None: def test_aliases() -> None: # Ensures that model aliases always point to valid models. - names = [u.name for u in UNIVERSAL_CALCULATORS] + names = {u.name for u in UNIVERSAL_CALCULATORS} if find_spec("mace"): from mace.calculators.foundations_models import mace_mp_names From 774241578f72de53beb329a11172ce07de9a68f3 Mon Sep 17 00:00:00 2001 From: Runze Liu Date: Fri, 19 Dec 2025 16:46:31 -0800 Subject: [PATCH 06/23] Resolve ID and alias --- src/matcalc/utils.py | 168 +++++++++++++++++++++++++++++-------------- tests/test_utils.py | 47 +++++++++--- 2 files changed, 153 insertions(+), 62 deletions(-) diff --git a/src/matcalc/utils.py b/src/matcalc/utils.py index fa013127..ab34fa82 100644 --- a/src/matcalc/utils.py +++ b/src/matcalc/utils.py @@ -54,51 +54,70 @@ # Different backend libraries (MatGL, MACE, GRACE, etc.) use # inconsistent or non-canonical model names (e.g., "small-omat-0", -# "TensorNet-MatPES-PBE-v2025.1-PES", "GRACE-1L-OMAT-medium-base"). +# "TensorNet-MatPES-PBE-v2025.1-PES", "GRACE-2L-OMAT-medium-base"). # Users and developers are encouraged to use the model naming convention below. # MatCalc Model Naming Convention (Unified model ID format): -# [Model]-(#Layers)-[Dataset]-(Functional)-(Cutoff)-(Version)-(Size)-(Postprocess) -# Not all fields must appear. +# ---- # Examples: -# TensorNet-MatPES-PBE-v2025.1-PES -# MACE-MP-0-medium -# GRACE-1L-MP-r6 -# GRACE-1L-OMAT-medium-base -# GRACE-2L-OMAT-large-ft-AM +# TensorNet-MatPES-r2SCAN-v2025.1-S +# MACE-MP-PBE-0-M +# GRACE-OMAT-PBE-0-L # This table maps such identifiers into the backend model names that -# each library expects. - -# This tabel will be gradually expanded as new models are released. +# each library expects and will be gradually expanded as new models +# are released. # Keys must be lowercase and represent canonical identifiers # Values are the actual model names passed to the backend libraries. - -MODEL_ALIASES = { - # Using abbreviations will load the most advanced model. - "tensornet": "TensorNet-MatPES-PBE-v2025.1-PES", - "m3gnet": "M3GNet-MatPES-PBE-v2025.1-PES", - "chgnet": "CHGNet-MatPES-PBE-2025.2.10-2.7M-PES", - "mace": "medium-mpa-0", - "grace": "GRACE-2L-OAM", - "pbe": "TensorNet-MatPES-PBE-v2025.1-PES", - "r2scan": "TensorNet-MatPES-r2SCAN-v2025.1-PES", - "mace-mp-0-small": "small", - "mace-mp-0-medium": "medium", - "mace-mp-0-large": "large", - "mace-mp-0b-small": "small-0b", - "mace-mp-0b-medium": "medium-0b", - "mace-mp-0b2-small": "small-0b2", - "mace-mp-0b2-medium": "medium-0b2", - "mace-mp-0b2-large": "large-0b2", - "mace-mp-0b3-medium": "medium-0b3", - "mace-mpa-0-medium": "medium-mpa-0", - "mace-omat-0-small": "small-omat-0", - "mace-omat-0-medium": "medium-omat-0", +ID_TO_NAME = { + "tensornet-matpes-pbe-v2025.1-s": "TensorNet-MatPES-PBE-v2025.1-PES", + "tensornet-matpes-r2scan-v2025.1-s": "TensorNet-MatPES-r2SCAN-v2025.1-PES", + "m3gnet-matpes-pbe-v2025.1-s": "M3GNet-MatPES-PBE-v2025.1-PES", + "m3gnet-matpes-r2scan-v2025.1-s": "M3GNet-MatPES-r2SCAN-v2025.1-PES", + "chgnet-matpes-pbe-v2025.2-m": "CHGNet-MatPES-PBE-2025.2.10-2.7M-PES", + "chgnet-matpes-r2scan-v2025.2-m": "CHGNet-MatPES-r2SCAN-2025.2.10-2.7M-PES", + "mace-mp-pbe-0-s": "small", + "mace-mp-pbe-0-m": "medium", + "mace-mp-pbe-0-l": "large", + "mace-mp-pbe-0b-s": "small-0b", + "mace-mp-pbe-0b-m": "medium-0b", + "mace-mp-pbe-0b2-s": "small-0b2", + "mace-mp-pbe-0b2-m": "medium-0b2", + "mace-mp-pbe-0b2-l": "large-0b2", + "mace-mp-pbe-0b3-m": "medium-0b3", + "mace-mpa-pbe-0-m": "medium-mpa-0", + "mace-omt-pbe-0-s": "small-omat-0", + "mace-omat-pbe-0-m": "medium-omat-0", + "mace-matpes-pbe-0-m": "mace-matpes-pbe-0", + "mace-matpes-r2scan-0-m": "mace-matpes-r2scan-0", + "grace-mp-pbe-0-l": "GRACE-2L-MP-r6", + "grace-oam-pbe-0-s": "GRACE-2L-OAM", + "grace-oam-pbe-0-m": "GRACE-2L-OMAT-medium-ft-AM", + "grace-oam-pbe-0-l": "GRACE-2L-OMAT-large-ft-AM", + "grace-omat-pbe-0-s": "GRACE-2L-OMAT", + "grace-omat-pbe-0-m": "GRACE-2L-OMAT-medium-ft-E", + "grace-omat-pbe-0-l": "GRACE-2L-OMAT-large-ft-E", } +# Common aliases and abbreviations will load the most advanced or widely used model. +ID_TO_ALIAS = { + "tensornet-matpes-pbe-v2025.1-s": ["tensornet", "tensornet-pbe", "pbe"], + "tensornet-matpes-r2scan-v2025.1-s": ["tensornet-r2scan", "r2scan"], + "m3gnet-matpes-pbe-v2025.1-s": ["m3gnet", "m3gnet-pbe"], + "m3gnet-matpes-r2scan-v2025.1-s": ["m3gnet-r2scan"], + "chgnet-matpes-pbe-v2025.2-m": ["chgnet", "chgnet-pbe"], + "chgnet-matpes-r2scan-v2025.2-m": ["chgnet-r2scan"], + "mace-mp-pbe-0-m": ["mace-mp-0", "mace-mp-0-m", "mace-mp-pbe-0"], + "mace-mpa-pbe-0-m": ["mace", "mace-mpa-0", "mace-mpa-0-m", "mace-mpa-pbe-0"], + "mace-omat-pbe-0-m": ["mace-omat-0", "mace-omat-0-m", "mace-omat-pbe-0"], + "mace-matpes-pbe-0-m": ["mace-matpes-pbe", "mace-matpes-pbe-0"], + "mace-matpes-r2scan-0-m": ["mace-matpes-r2scan", "mace-matpes-r2scan-0"], + "grace-mp-pbe-0-l": ["grace-mp"], + "grace-oam-pbe-0-l": ["grace", "grace-oam"], + "grace-omat-pbe-0-l": ["grace-omat"], +} UNIVERSAL_CALCULATORS = Enum("UNIVERSAL_CALCULATORS", {k: k for k in _universal_calculators}) # type: ignore[misc] @@ -403,46 +422,46 @@ def load_universal(name: str | Calculator, **kwargs: Any) -> Calculator: # noqa """ result: Calculator - if not isinstance(name, str): # e.g. already an ase Calculator instance - result = name + if not isinstance(name, str): + return name - elif any(name.lower().startswith(m) for m in ("m3gnet", "chgnet", "tensornet", "pbe", "r2scan")): - name = MODEL_ALIASES.get(name.lower(), name) - result = PESCalculator.load_matgl(name, **kwargs) + backend_name, route_tag = _resolve_model(name) + backend = _infer_backend(route_tag) - elif name.lower().startswith("mace"): - name = MODEL_ALIASES.get(name.lower(), name) - from mace.calculators import mace_mp + if backend == "matgl": - result = mace_mp(model=name, **kwargs) + result = PESCalculator.load_matgl(backend_name, **kwargs) - elif name.lower() == "sevennet": - from sevenn.calculator import SevenNetCalculator + elif backend == "mace": + from mace.calculators import mace_mp - result = SevenNetCalculator(**kwargs) + result = mace_mp(model=backend_name, **kwargs) - elif name.lower().startswith("grace"): - name = MODEL_ALIASES.get(name.lower(), name) + elif backend == "grace": from tensorpotential.calculator.foundation_models import grace_fm - result = grace_fm(model=name, **kwargs) + result = grace_fm(model=backend_name, **kwargs) - elif name.lower() == "orb": + elif backend == "sevennet": + from sevenn.calculator import SevenNetCalculator + + result = SevenNetCalculator(**kwargs) + + elif backend == "orb": from orb_models.forcefield.calculator import ORBCalculator from orb_models.forcefield.pretrained import ORB_PRETRAINED_MODELS model = kwargs.pop("model", "orb-v2") device = kwargs.get("device", "cpu") - orbff = ORB_PRETRAINED_MODELS[model](device=device) result = ORBCalculator(orbff, **kwargs) - elif name.lower() == "mattersim": # pragma: no cover + elif backend == "mattersim": # pragma: no cover from mattersim.forcefield import MatterSimCalculator result = MatterSimCalculator(**kwargs) - elif name.lower() == "fairchem": # pragma: no cover + elif backend == "fairchem": # pragma: no cover from fairchem.core import FAIRChemCalculator, pretrained_mlip device = kwargs.pop("device", "cpu") @@ -451,7 +470,7 @@ def load_universal(name: str | Calculator, **kwargs: Any) -> Calculator: # noqa predictor = pretrained_mlip.get_predict_unit(model, device=device) result = FAIRChemCalculator(predictor, task_name=task_name, **kwargs) - elif name.lower() == "petmad": # pragma: no cover + elif backend == "petmad": # pragma: no cover from pet_mad.calculator import PETMADCalculator result = PETMADCalculator(**kwargs) @@ -525,3 +544,46 @@ def to_pmg_molecule(structure: Atoms | Structure | Molecule | IMolecule) -> IMol structure = AseAtomsAdaptor.get_molecule(structure) return Molecule.from_sites(structure) # type: ignore[return-value] + + +def _resolve_model(name: str) -> tuple[str, str]: + key = name.lower() + model_id: str | None = None + + if key in ID_TO_NAME: + model_id = key + else: + for mid, aliases in ID_TO_ALIAS.items(): + if key in aliases: + model_id = mid + break + + if model_id is not None: + return ID_TO_NAME[model_id], model_id + + return name, key + + +def _infer_backend(route_tag: str) -> str: + backend = "unknown" + + if route_tag.startswith(("tensornet", "m3gnet", "chgnet")): + backend = "matgl" + elif route_tag.startswith("mace"): + backend = "mace" + elif route_tag.startswith("grace"): + backend = "grace" + elif route_tag == "sevennet": + backend = "sevennet" + elif route_tag == "orb": + backend = "orb" + elif route_tag == "mattersim": + backend = "mattersim" + elif route_tag == "fairchem": + backend = "fairchem" + elif route_tag == "petmad": + backend = "petmad" + elif route_tag.startswith("deepmd"): + backend = "deepmd" + + return backend diff --git a/tests/test_utils.py b/tests/test_utils.py index bb29b162..705d9458 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -11,9 +11,11 @@ from matcalc import RelaxCalc from matcalc.utils import ( - MODEL_ALIASES, + ID_TO_ALIAS, + ID_TO_NAME, UNIVERSAL_CALCULATORS, PESCalculator, + _resolve_model, ) DIR = Path(__file__).parent.absolute() @@ -66,7 +68,7 @@ def _map_calculators_to_packages(calculators: UNIVERSAL_CALCULATORS) -> dict[str ("GPa", 1.0), ], ) -@pytest.mark.skipif(not find_spec("maml"), reason="maml is not installed") +# @pytest.mark.skipif(not find_spec("maml"), reason="maml is not installed") def test_pescalculator_load_mtp(expected_unit: str, expected_weight: float) -> None: calc = PESCalculator.load_mtp( filename=DIR / "pes" / "MTP-Cu-2020.1-PES" / "fitted.mtp", @@ -176,9 +178,12 @@ def test_pescalculator_calculate() -> None: assert list(stresses.shape) == [6] -def test_aliases() -> None: - # Ensures that model aliases always point to valid models. +def _known_backend_models() -> set[str]: + """Return all backend model names that IDs or aliases are allowed to map to.""" + # Built-in universal calculators names = {u.name for u in UNIVERSAL_CALCULATORS} + + # MACE backend models if find_spec("mace"): from mace.calculators.foundations_models import mace_mp_names @@ -203,6 +208,8 @@ def test_aliases() -> None: "mh-0", "mh-1", } + + # GRACE backend models if find_spec("tensorpotential"): from tensorpotential.calculator.foundation_models import MODELS_METADATA @@ -232,8 +239,30 @@ def test_aliases() -> None: "GRACE-2L-OMAT-large-ft-E", "GRACE-2L-OMAT-large-ft-AM", } - names = names | mace_models | grace_models - for v in MODEL_ALIASES.values(): - # We are not testing DGL based models. - if "M3GNet" not in v and "CHGNet" not in v: - assert v in names + + return names | mace_models | grace_models + + +def test_id_to_name() -> None: + """Verify that all ID_TO_NAME values map to known backend model names.""" + known = _known_backend_models() + + for model_name in ID_TO_NAME.values(): + # Skip matgl models since they depend on installed pretrained models. + if any(key in model_name for key in ("M3GNet", "CHGNet", "TensorNet")): + continue + assert model_name in known, f"Unknown backend model mapped: {model_name!r}" + + +def test_allias_to_id() -> None: + """Ensure aliases resolve to canonical IDs and correct backend names.""" + for model_id, aliases in ID_TO_ALIAS.items(): + assert model_id in ID_TO_NAME # Canonical ID must exist + expected_backend_name = ID_TO_NAME[model_id] + + for alias in aliases: + backend_name, route_tag = _resolve_model(alias) + assert backend_name == expected_backend_name, ( + f"Alias {alias!r} resolved to {backend_name!r}, " f"expected {expected_backend_name!r}" + ) + assert route_tag == model_id, f"Alias {alias!r} produced route_tag {route_tag!r}, " f"expected {model_id!r}" From e1be217061d61b85ae3a25954f91d8d8e5348214 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 20 Dec 2025 00:47:01 +0000 Subject: [PATCH 07/23] pre-commit auto-fixes --- src/matcalc/utils.py | 1 - tests/test_utils.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/matcalc/utils.py b/src/matcalc/utils.py index ab34fa82..4d08192d 100644 --- a/src/matcalc/utils.py +++ b/src/matcalc/utils.py @@ -429,7 +429,6 @@ def load_universal(name: str | Calculator, **kwargs: Any) -> Calculator: # noqa backend = _infer_backend(route_tag) if backend == "matgl": - result = PESCalculator.load_matgl(backend_name, **kwargs) elif backend == "mace": diff --git a/tests/test_utils.py b/tests/test_utils.py index 705d9458..770721fd 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -263,6 +263,6 @@ def test_allias_to_id() -> None: for alias in aliases: backend_name, route_tag = _resolve_model(alias) assert backend_name == expected_backend_name, ( - f"Alias {alias!r} resolved to {backend_name!r}, " f"expected {expected_backend_name!r}" + f"Alias {alias!r} resolved to {backend_name!r}, expected {expected_backend_name!r}" ) - assert route_tag == model_id, f"Alias {alias!r} produced route_tag {route_tag!r}, " f"expected {model_id!r}" + assert route_tag == model_id, f"Alias {alias!r} produced route_tag {route_tag!r}, expected {model_id!r}" From 3d21d370b4eb826f9abb0cf38e913905240db31d Mon Sep 17 00:00:00 2001 From: Runze Liu Date: Tue, 23 Dec 2025 16:30:51 -0800 Subject: [PATCH 08/23] Fix --- src/matcalc/utils.py | 172 +++++++++++++++---------------------------- 1 file changed, 61 insertions(+), 111 deletions(-) diff --git a/src/matcalc/utils.py b/src/matcalc/utils.py index 4d08192d..ec6a29ea 100644 --- a/src/matcalc/utils.py +++ b/src/matcalc/utils.py @@ -69,56 +69,62 @@ # each library expects and will be gradually expanded as new models # are released. -# Keys must be lowercase and represent canonical identifiers +# Keys use proper canonical capitalization. # Values are the actual model names passed to the backend libraries. ID_TO_NAME = { - "tensornet-matpes-pbe-v2025.1-s": "TensorNet-MatPES-PBE-v2025.1-PES", - "tensornet-matpes-r2scan-v2025.1-s": "TensorNet-MatPES-r2SCAN-v2025.1-PES", - "m3gnet-matpes-pbe-v2025.1-s": "M3GNet-MatPES-PBE-v2025.1-PES", - "m3gnet-matpes-r2scan-v2025.1-s": "M3GNet-MatPES-r2SCAN-v2025.1-PES", - "chgnet-matpes-pbe-v2025.2-m": "CHGNet-MatPES-PBE-2025.2.10-2.7M-PES", - "chgnet-matpes-r2scan-v2025.2-m": "CHGNet-MatPES-r2SCAN-2025.2.10-2.7M-PES", - "mace-mp-pbe-0-s": "small", - "mace-mp-pbe-0-m": "medium", - "mace-mp-pbe-0-l": "large", - "mace-mp-pbe-0b-s": "small-0b", - "mace-mp-pbe-0b-m": "medium-0b", - "mace-mp-pbe-0b2-s": "small-0b2", - "mace-mp-pbe-0b2-m": "medium-0b2", - "mace-mp-pbe-0b2-l": "large-0b2", - "mace-mp-pbe-0b3-m": "medium-0b3", - "mace-mpa-pbe-0-m": "medium-mpa-0", - "mace-omt-pbe-0-s": "small-omat-0", - "mace-omat-pbe-0-m": "medium-omat-0", - "mace-matpes-pbe-0-m": "mace-matpes-pbe-0", - "mace-matpes-r2scan-0-m": "mace-matpes-r2scan-0", - "grace-mp-pbe-0-l": "GRACE-2L-MP-r6", - "grace-oam-pbe-0-s": "GRACE-2L-OAM", - "grace-oam-pbe-0-m": "GRACE-2L-OMAT-medium-ft-AM", - "grace-oam-pbe-0-l": "GRACE-2L-OMAT-large-ft-AM", - "grace-omat-pbe-0-s": "GRACE-2L-OMAT", - "grace-omat-pbe-0-m": "GRACE-2L-OMAT-medium-ft-E", - "grace-omat-pbe-0-l": "GRACE-2L-OMAT-large-ft-E", + "TensorNet-MatPES-PBE-v2025.1-S": "TensorNet-MatPES-PBE-v2025.1-PES", + "TensorNet-MatPES-r2SCAN-v2025.1-S": "TensorNet-MatPES-r2SCAN-v2025.1-PES", + "M3GNet-MatPES-PBE-v2025.1-S": "M3GNet-MatPES-PBE-v2025.1-PES", + "M3GNet-MatPES-r2SCAN-v2025.1-S": "M3GNet-MatPES-r2SCAN-v2025.1-PES", + "CHGNet-MatPES-PBE-v2025.2-M": "CHGNet-MatPES-PBE-2025.2.10-2.7M-PES", + "CHGNet-MatPES-r2SCAN-v2025.2-M": "CHGNet-MatPES-r2SCAN-2025.2.10-2.7M-PES", + "MACE-MP-PBE-0-S": "small", + "MACE-MP-PBE-0-M": "medium", + "MACE-MP-PBE-0-L": "large", + "MACE-MP-PBE-0b-S": "small-0b", + "MACE-MP-PBE-0b-M": "medium-0b", + "MACE-MP-PBE-0b2-S": "small-0b2", + "MACE-MP-PBE-0b2-M": "medium-0b2", + "MACE-MP-PBE-0b2-L": "large-0b2", + "MACE-MP-PBE-0b3-M": "medium-0b3", + "MACE-MPA-PBE-0-M": "medium-mpa-0", + "MACE-OMT-PBE-0-S": "small-omat-0", + "MACE-OMAT-PBE-0-M": "medium-omat-0", + "MACE-MatPES-PBE-0-M": "mace-matpes-pbe-0", + "MACE-MatPES-r2SCAN-0-M": "mace-matpes-r2scan-0", + "GRACE-MP-PBE-0-L": "GRACE-2L-MP-r6", + "GRACE-OAM-PBE-0-S": "GRACE-2L-OAM", + "GRACE-OAM-PBE-0-M": "GRACE-2L-OMAT-medium-ft-AM", + "GRACE-OAM-PBE-0-L": "GRACE-2L-OMAT-large-ft-AM", + "GRACE-OMAT-PBE-0-S": "GRACE-2L-OMAT", + "GRACE-OMAT-PBE-0-M": "GRACE-2L-OMAT-medium-ft-E", + "GRACE-OMAT-PBE-0-L": "GRACE-2L-OMAT-large-ft-E", } +_ID_LOOKUP = {cid.lower(): cid for cid in ID_TO_NAME} + + # Common aliases and abbreviations will load the most advanced or widely used model. ID_TO_ALIAS = { - "tensornet-matpes-pbe-v2025.1-s": ["tensornet", "tensornet-pbe", "pbe"], - "tensornet-matpes-r2scan-v2025.1-s": ["tensornet-r2scan", "r2scan"], - "m3gnet-matpes-pbe-v2025.1-s": ["m3gnet", "m3gnet-pbe"], - "m3gnet-matpes-r2scan-v2025.1-s": ["m3gnet-r2scan"], - "chgnet-matpes-pbe-v2025.2-m": ["chgnet", "chgnet-pbe"], - "chgnet-matpes-r2scan-v2025.2-m": ["chgnet-r2scan"], - "mace-mp-pbe-0-m": ["mace-mp-0", "mace-mp-0-m", "mace-mp-pbe-0"], - "mace-mpa-pbe-0-m": ["mace", "mace-mpa-0", "mace-mpa-0-m", "mace-mpa-pbe-0"], - "mace-omat-pbe-0-m": ["mace-omat-0", "mace-omat-0-m", "mace-omat-pbe-0"], - "mace-matpes-pbe-0-m": ["mace-matpes-pbe", "mace-matpes-pbe-0"], - "mace-matpes-r2scan-0-m": ["mace-matpes-r2scan", "mace-matpes-r2scan-0"], - "grace-mp-pbe-0-l": ["grace-mp"], - "grace-oam-pbe-0-l": ["grace", "grace-oam"], - "grace-omat-pbe-0-l": ["grace-omat"], + "TensorNet-MatPES-PBE-v2025.1-S": ["tensornet", "tensornet-pbe", "pbe"], + "TensorNet-MatPES-r2SCAN-v2025.1-S": ["tensornet-r2scan", "r2scan"], + "M3GNet-MatPES-PBE-v2025.1-S": ["m3gnet", "m3gnet-pbe"], + "M3GNet-MatPES-r2SCAN-v2025.1-S": ["m3gnet-r2scan"], + "CHGNet-MatPES-PBE-v2025.2-M": ["chgnet", "chgnet-pbe"], + "CHGNet-MatPES-r2SCAN-v2025.2-M": ["chgnet-r2scan"], + "MACE-MP-PBE-0-M": ["mace-mp-0", "mace-mp-0-m", "mace-mp-pbe-0"], + "MACE-MPA-PBE-0-M": ["mace", "mace-mpa-0", "mace-mpa-0-m", "mace-mpa-pbe-0"], + "MACE-OMAT-PBE-0-M": ["mace-omat-0", "mace-omat-0-m", "mace-omat-pbe-0"], + "MACE-MatPES-PBE-0-M": ["mace-matpes-pbe", "mace-matpes-pbe-0"], + "MACE-MatPES-r2SCAN-0-M": ["mace-matpes-r2scan", "mace-matpes-r2scan-0"], + "GRACE-MP-PBE-0-L": ["grace-mp"], + "GRACE-OAM-PBE-0-L": ["grace", "grace-oam"], + "GRACE-OMAT-PBE-0-L": ["grace-omat"], } +ALIAS_TO_ID = {alias.lower(): cid for cid, aliases in ID_TO_ALIAS.items() for alias in aliases} + + UNIVERSAL_CALCULATORS = Enum("UNIVERSAL_CALCULATORS", {k: k for k in _universal_calculators}) # type: ignore[misc] @@ -149,16 +155,7 @@ def __init__( stress_weight: float = 1.0, **kwargs: Any, ) -> None: - """ - Initialize PESCalculator with a potential from maml. - - Args: - potential (LMPStaticCalculator): maml.apps.pes._lammps.LMPStaticCalculator - stress_unit (str): The unit of stress. Default to "GPa" - stress_weight (float): The conversion factor from GPa to eV/A^3, if it is set to 1.0, the unit is in GPa. - Default to 1.0. - **kwargs: Additional keyword arguments passed to super().__init__(). - """ + """Initialize PESCalculator with a potential from maml.""" super().__init__(**kwargs) self.potential = potential @@ -178,19 +175,9 @@ def calculate( properties: list | None = None, system_changes: list | None = None, ) -> None: - """ - Perform calculation for an input Atoms. - - Args: - atoms (ase.Atoms): ase Atoms object - properties (list): The list of properties to calculate - system_changes (list): monitor which properties of atoms were - changed for new calculation. If not, the previous calculation - results will be loaded. - """ + """Perform calculation for an input Atoms.""" from ase.calculators.calculator import all_changes, all_properties from maml.apps.pes import EnergyForceStress - from pymatgen.io.ase import AseAtomsAdaptor properties = properties or all_properties system_changes = system_changes or all_changes @@ -492,53 +479,17 @@ def load_universal(name: str | Calculator, **kwargs: Any) -> Calculator: # noqa def to_ase_atoms(structure: Atoms | Structure | Molecule) -> Atoms: - """ - Converts a given structure into an ASE Atoms object. This function checks - if the input structure is already an ASE Atoms object. If not, it converts - a pymatgen Structure object to an ASE Atoms object using the AseAtomsAdaptor. - - :param structure: The input structure, which can be either an ASE Atoms object - or a pymatgen Structure object. - :type structure: Atoms | Structure - :return: An ASE Atoms object representing the given structure. - :rtype: Atoms - """ + """Converts a given structure into an ASE Atoms object.""" return structure if isinstance(structure, Atoms) else AseAtomsAdaptor.get_atoms(structure) def to_pmg_structure(structure: Atoms | Structure) -> Structure: - """ - Converts a given structure of type Atoms or Structure into a Structure - object. If the input structure is already of type Structure, it is - returned unchanged. If the input structure is of type Atoms, it is - converted to a Structure using the AseAtomsAdaptor. - - :param structure: The input structure to be converted. This can be of - type Atoms or Structure. - :type structure: Atoms | Structure - :return: A Structure object corresponding to the input structure. If the - input is already a Structure, it is returned as-is. Otherwise, it is - converted. - :rtype: Structure - """ + """Converts a given structure into a pymatgen Structure.""" return structure if isinstance(structure, Structure) else AseAtomsAdaptor.get_structure(structure) # type: ignore[return-value] def to_pmg_molecule(structure: Atoms | Structure | Molecule | IMolecule) -> IMolecule: - """ - Converts a given structure of type Atoms or Structure into a Molecule - object. If the input structure is already of type Molecule, it is - returned unchanged. If the input structure is of type Atoms, it is - converted to a Molecule using the AseAtomsAdaptor. - - :param structure: The input structure to be converted. This can be of - type Atoms or Structure or Molecule. - :type structure: Atoms | Structure | Molecule - :return: A Molecule object corresponding to the input structure. If the - input is already a Molecule, it is returned as-is. Otherwise, it is - converted. - :rtype: Molecule - """ + """Converts a given structure into a pymatgen Molecule.""" if isinstance(structure, Atoms): structure = AseAtomsAdaptor.get_molecule(structure) @@ -546,16 +497,15 @@ def to_pmg_molecule(structure: Atoms | Structure | Molecule | IMolecule) -> IMol def _resolve_model(name: str) -> tuple[str, str]: + """Resolve user input to (backend_name, route_tag). + + - Canonical IDs (keys of ID_TO_NAME) are matched case-insensitively via _ID_LOOKUP. + - Aliases are matched case-insensitively via ALIAS_TO_ID (O(1) lookup). + - If neither matches, passthrough is used. + """ key = name.lower() - model_id: str | None = None - - if key in ID_TO_NAME: - model_id = key - else: - for mid, aliases in ID_TO_ALIAS.items(): - if key in aliases: - model_id = mid - break + + model_id = _ID_LOOKUP.get(key) or ALIAS_TO_ID.get(key) if model_id is not None: return ID_TO_NAME[model_id], model_id From 620e984549ee8ac0c5805d55e6badede744ab66a Mon Sep 17 00:00:00 2001 From: Runze Liu Date: Tue, 23 Dec 2025 17:36:48 -0800 Subject: [PATCH 09/23] Fix --- src/matcalc/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matcalc/utils.py b/src/matcalc/utils.py index ec6a29ea..8825ef8d 100644 --- a/src/matcalc/utils.py +++ b/src/matcalc/utils.py @@ -514,6 +514,7 @@ def _resolve_model(name: str) -> tuple[str, str]: def _infer_backend(route_tag: str) -> str: + route_tag = route_tag.lower() backend = "unknown" if route_tag.startswith(("tensornet", "m3gnet", "chgnet")): From cc684fabe05f8252fb7b44bb218ceaa8e565eedb Mon Sep 17 00:00:00 2001 From: Runze Liu Date: Tue, 23 Dec 2025 18:40:19 -0800 Subject: [PATCH 10/23] Fix --- src/matcalc/utils.py | 42 ++++++++++++++++++++++-------------------- tests/test_utils.py | 9 ++++++--- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/matcalc/utils.py b/src/matcalc/utils.py index 8825ef8d..b4958c1b 100644 --- a/src/matcalc/utils.py +++ b/src/matcalc/utils.py @@ -88,7 +88,7 @@ "MACE-MP-PBE-0b2-L": "large-0b2", "MACE-MP-PBE-0b3-M": "medium-0b3", "MACE-MPA-PBE-0-M": "medium-mpa-0", - "MACE-OMT-PBE-0-S": "small-omat-0", + "MACE-OMAT-PBE-0-S": "small-omat-0", "MACE-OMAT-PBE-0-M": "medium-omat-0", "MACE-MatPES-PBE-0-M": "mace-matpes-pbe-0", "MACE-MatPES-r2SCAN-0-M": "mace-matpes-r2scan-0", @@ -105,24 +105,26 @@ # Common aliases and abbreviations will load the most advanced or widely used model. -ID_TO_ALIAS = { - "TensorNet-MatPES-PBE-v2025.1-S": ["tensornet", "tensornet-pbe", "pbe"], - "TensorNet-MatPES-r2SCAN-v2025.1-S": ["tensornet-r2scan", "r2scan"], - "M3GNet-MatPES-PBE-v2025.1-S": ["m3gnet", "m3gnet-pbe"], - "M3GNet-MatPES-r2SCAN-v2025.1-S": ["m3gnet-r2scan"], - "CHGNet-MatPES-PBE-v2025.2-M": ["chgnet", "chgnet-pbe"], - "CHGNet-MatPES-r2SCAN-v2025.2-M": ["chgnet-r2scan"], - "MACE-MP-PBE-0-M": ["mace-mp-0", "mace-mp-0-m", "mace-mp-pbe-0"], - "MACE-MPA-PBE-0-M": ["mace", "mace-mpa-0", "mace-mpa-0-m", "mace-mpa-pbe-0"], - "MACE-OMAT-PBE-0-M": ["mace-omat-0", "mace-omat-0-m", "mace-omat-pbe-0"], - "MACE-MatPES-PBE-0-M": ["mace-matpes-pbe", "mace-matpes-pbe-0"], - "MACE-MatPES-r2SCAN-0-M": ["mace-matpes-r2scan", "mace-matpes-r2scan-0"], - "GRACE-MP-PBE-0-L": ["grace-mp"], - "GRACE-OAM-PBE-0-L": ["grace", "grace-oam"], - "GRACE-OMAT-PBE-0-L": ["grace-omat"], +ALIAS_TO_ID = { + ("tensornet", "tensornet-pbe", "pbe"): "TensorNet-MatPES-PBE-v2025.1-S", + ("tensornet-r2scan", "r2scan"): "TensorNet-MatPES-r2SCAN-v2025.1-S", + ("m3gnet", "m3gnet-pbe"): "M3GNet-MatPES-PBE-v2025.1-S", + ("m3gnet-r2scan"): "M3GNet-MatPES-r2SCAN-v2025.1-S", + ("chgnet", "chgnet-pbe"): "CHGNet-MatPES-PBE-v2025.2-M", + ("chgnet-r2scan",): "CHGNet-MatPES-r2SCAN-v2025.2-M", + ("mace-mp-0", "mace-mp-0-m", "mace-mp-pbe-0"): "MACE-MP-PBE-0-M", + ("mace", "mace-mpa-0", "mace-mpa-0-m", "mace-mpa-pbe-0"): "MACE-MPA-PBE-0-M", + ("mace-omat-0", "mace-omat-0-m", "mace-omat-pbe-0"): "MACE-OMAT-PBE-0-M", + ("mace-matpes-pbe", "mace-matpes-pbe-0"): "MACE-MatPES-PBE-0-M", + ("mace-matpes-r2scan", "mace-matpes-r2scan-0"): "MACE-MatPES-r2SCAN-0-M", + ("grace-mp",): "GRACE-MP-PBE-0-L", + ("grace", "grace-oam"): "GRACE-OAM-PBE-0-L", + ("grace-omat",): "GRACE-OMAT-PBE-0-L", } -ALIAS_TO_ID = {alias.lower(): cid for cid, aliases in ID_TO_ALIAS.items() for alias in aliases} +ALIAS_HASH_TABLE: dict[str, str] = { + alias.lower(): canonical for aliases, canonical in ALIAS_TO_ID.items() for alias in aliases +} UNIVERSAL_CALCULATORS = Enum("UNIVERSAL_CALCULATORS", {k: k for k in _universal_calculators}) # type: ignore[misc] @@ -461,7 +463,7 @@ def load_universal(name: str | Calculator, **kwargs: Any) -> Calculator: # noqa result = PETMADCalculator(**kwargs) - elif name.lower().startswith("deepmd"): # pragma: no cover + elif backend == "deepmd": # pragma: no cover from pathlib import Path from deepmd.calculator import DP @@ -500,12 +502,12 @@ def _resolve_model(name: str) -> tuple[str, str]: """Resolve user input to (backend_name, route_tag). - Canonical IDs (keys of ID_TO_NAME) are matched case-insensitively via _ID_LOOKUP. - - Aliases are matched case-insensitively via ALIAS_TO_ID (O(1) lookup). + - Aliases are matched case-insensitively via ALIAS_HASH_TABLE. - If neither matches, passthrough is used. """ key = name.lower() - model_id = _ID_LOOKUP.get(key) or ALIAS_TO_ID.get(key) + model_id = _ID_LOOKUP.get(key) or ALIAS_HASH_TABLE.get(key) if model_id is not None: return ID_TO_NAME[model_id], model_id diff --git a/tests/test_utils.py b/tests/test_utils.py index 770721fd..e2083953 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -11,7 +11,7 @@ from matcalc import RelaxCalc from matcalc.utils import ( - ID_TO_ALIAS, + ALIAS_TO_ID, ID_TO_NAME, UNIVERSAL_CALCULATORS, PESCalculator, @@ -256,7 +256,7 @@ def test_id_to_name() -> None: def test_allias_to_id() -> None: """Ensure aliases resolve to canonical IDs and correct backend names.""" - for model_id, aliases in ID_TO_ALIAS.items(): + for aliases, model_id in ALIAS_TO_ID.items(): assert model_id in ID_TO_NAME # Canonical ID must exist expected_backend_name = ID_TO_NAME[model_id] @@ -265,4 +265,7 @@ def test_allias_to_id() -> None: assert backend_name == expected_backend_name, ( f"Alias {alias!r} resolved to {backend_name!r}, expected {expected_backend_name!r}" ) - assert route_tag == model_id, f"Alias {alias!r} produced route_tag {route_tag!r}, expected {model_id!r}" + assert route_tag == model_id, ( + f"Alias {alias!r} produced route_tag {route_tag!r}, expected {model_id!r}" + ) + From 0fdf82922e0e7323c2a4f01d5377202f2ea3f6b1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 02:40:44 +0000 Subject: [PATCH 11/23] pre-commit auto-fixes --- tests/test_utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index e2083953..2aa8675a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -265,7 +265,4 @@ def test_allias_to_id() -> None: assert backend_name == expected_backend_name, ( f"Alias {alias!r} resolved to {backend_name!r}, expected {expected_backend_name!r}" ) - assert route_tag == model_id, ( - f"Alias {alias!r} produced route_tag {route_tag!r}, expected {model_id!r}" - ) - + assert route_tag == model_id, f"Alias {alias!r} produced route_tag {route_tag!r}, expected {model_id!r}" From 260035a26365e7733b59586a875c63180959154d Mon Sep 17 00:00:00 2001 From: Runze Liu Date: Tue, 23 Dec 2025 19:16:59 -0800 Subject: [PATCH 12/23] Fix --- src/matcalc/utils.py | 64 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/src/matcalc/utils.py b/src/matcalc/utils.py index b4958c1b..04e92671 100644 --- a/src/matcalc/utils.py +++ b/src/matcalc/utils.py @@ -157,7 +157,16 @@ def __init__( stress_weight: float = 1.0, **kwargs: Any, ) -> None: - """Initialize PESCalculator with a potential from maml.""" + """ + Initialize PESCalculator with a potential from maml. + + Args: + potential (LMPStaticCalculator): maml.apps.pes._lammps.LMPStaticCalculator + stress_unit (str): The unit of stress. Default to "GPa" + stress_weight (float): The conversion factor from GPa to eV/A^3, if it is set to 1.0, the unit is in GPa. + Default to 1.0. + **kwargs: Additional keyword arguments passed to super().__init__(). + """ super().__init__(**kwargs) self.potential = potential @@ -177,7 +186,16 @@ def calculate( properties: list | None = None, system_changes: list | None = None, ) -> None: - """Perform calculation for an input Atoms.""" + """ + Perform calculation for an input Atoms. + + Args: + atoms (ase.Atoms): ase Atoms object + properties (list): The list of properties to calculate + system_changes (list): monitor which properties of atoms were + changed for new calculation. If not, the previous calculation + results will be loaded. + """ from ase.calculators.calculator import all_changes, all_properties from maml.apps.pes import EnergyForceStress @@ -481,17 +499,53 @@ def load_universal(name: str | Calculator, **kwargs: Any) -> Calculator: # noqa def to_ase_atoms(structure: Atoms | Structure | Molecule) -> Atoms: - """Converts a given structure into an ASE Atoms object.""" + """ + Converts a given structure into an ASE Atoms object. This function checks + if the input structure is already an ASE Atoms object. If not, it converts + a pymatgen Structure object to an ASE Atoms object using the AseAtomsAdaptor. + + :param structure: The input structure, which can be either an ASE Atoms object + or a pymatgen Structure object. + :type structure: Atoms | Structure + :return: An ASE Atoms object representing the given structure. + :rtype: Atoms + """ return structure if isinstance(structure, Atoms) else AseAtomsAdaptor.get_atoms(structure) def to_pmg_structure(structure: Atoms | Structure) -> Structure: - """Converts a given structure into a pymatgen Structure.""" + """ + Converts a given structure of type Atoms or Structure into a Structure + object. If the input structure is already of type Structure, it is + returned unchanged. If the input structure is of type Atoms, it is + converted to a Structure using the AseAtomsAdaptor. + + :param structure: The input structure to be converted. This can be of + type Atoms or Structure. + :type structure: Atoms | Structure + :return: A Structure object corresponding to the input structure. If the + input is already a Structure, it is returned as-is. Otherwise, it is + converted. + :rtype: Structure + """ return structure if isinstance(structure, Structure) else AseAtomsAdaptor.get_structure(structure) # type: ignore[return-value] def to_pmg_molecule(structure: Atoms | Structure | Molecule | IMolecule) -> IMolecule: - """Converts a given structure into a pymatgen Molecule.""" + """ + Converts a given structure of type Atoms or Structure into a Molecule + object. If the input structure is already of type Molecule, it is + returned unchanged. If the input structure is of type Atoms, it is + converted to a Molecule using the AseAtomsAdaptor. + + :param structure: The input structure to be converted. This can be of + type Atoms or Structure or Molecule. + :type structure: Atoms | Structure | Molecule + :return: A Molecule object corresponding to the input structure. If the + input is already a Molecule, it is returned as-is. Otherwise, it is + converted. + :rtype: Molecule + """ if isinstance(structure, Atoms): structure = AseAtomsAdaptor.get_molecule(structure) From 192a2c2bdab808f13838d84efe36a12518dc5728 Mon Sep 17 00:00:00 2001 From: Runze Liu Date: Tue, 23 Dec 2025 19:19:13 -0800 Subject: [PATCH 13/23] Fix --- tests/test_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 2aa8675a..1dace400 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -68,7 +68,9 @@ def _map_calculators_to_packages(calculators: UNIVERSAL_CALCULATORS) -> dict[str ("GPa", 1.0), ], ) -# @pytest.mark.skipif(not find_spec("maml"), reason="maml is not installed") + + +@pytest.mark.skipif(not find_spec("maml"), reason="maml is not installed") def test_pescalculator_load_mtp(expected_unit: str, expected_weight: float) -> None: calc = PESCalculator.load_mtp( filename=DIR / "pes" / "MTP-Cu-2020.1-PES" / "fitted.mtp", From 95a957879484f39c5bcc89523bda6fae0d12be32 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 03:19:58 +0000 Subject: [PATCH 14/23] pre-commit auto-fixes --- tests/test_utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 1dace400..583e0559 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -68,8 +68,6 @@ def _map_calculators_to_packages(calculators: UNIVERSAL_CALCULATORS) -> dict[str ("GPa", 1.0), ], ) - - @pytest.mark.skipif(not find_spec("maml"), reason="maml is not installed") def test_pescalculator_load_mtp(expected_unit: str, expected_weight: float) -> None: calc = PESCalculator.load_mtp( From 3df256f6d6ad9bba6f263aef39de450634bfe7d8 Mon Sep 17 00:00:00 2001 From: Runze Liu Date: Mon, 5 Jan 2026 15:37:04 -0800 Subject: [PATCH 15/23] Fix --- src/matcalc/utils.py | 162 +++++++++++++++++++++---------------------- tests/test_utils.py | 133 ++++++++++++----------------------- 2 files changed, 127 insertions(+), 168 deletions(-) diff --git a/src/matcalc/utils.py b/src/matcalc/utils.py index 04e92671..01e0fe61 100644 --- a/src/matcalc/utils.py +++ b/src/matcalc/utils.py @@ -52,64 +52,13 @@ except Exception: # noqa: BLE001 warnings.warn("Unable to get pre-trained MatGL universal calculators.", stacklevel=1) -# Different backend libraries (MatGL, MACE, GRACE, etc.) use -# inconsistent or non-canonical model names (e.g., "small-omat-0", -# "TensorNet-MatPES-PBE-v2025.1-PES", "GRACE-2L-OMAT-medium-base"). - -# Users and developers are encouraged to use the model naming convention below. -# MatCalc Model Naming Convention (Unified model ID format): -# ---- - -# Examples: -# TensorNet-MatPES-r2SCAN-v2025.1-S -# MACE-MP-PBE-0-M -# GRACE-OMAT-PBE-0-L - -# This table maps such identifiers into the backend model names that -# each library expects and will be gradually expanded as new models -# are released. - -# Keys use proper canonical capitalization. -# Values are the actual model names passed to the backend libraries. -ID_TO_NAME = { - "TensorNet-MatPES-PBE-v2025.1-S": "TensorNet-MatPES-PBE-v2025.1-PES", - "TensorNet-MatPES-r2SCAN-v2025.1-S": "TensorNet-MatPES-r2SCAN-v2025.1-PES", - "M3GNet-MatPES-PBE-v2025.1-S": "M3GNet-MatPES-PBE-v2025.1-PES", - "M3GNet-MatPES-r2SCAN-v2025.1-S": "M3GNet-MatPES-r2SCAN-v2025.1-PES", - "CHGNet-MatPES-PBE-v2025.2-M": "CHGNet-MatPES-PBE-2025.2.10-2.7M-PES", - "CHGNet-MatPES-r2SCAN-v2025.2-M": "CHGNet-MatPES-r2SCAN-2025.2.10-2.7M-PES", - "MACE-MP-PBE-0-S": "small", - "MACE-MP-PBE-0-M": "medium", - "MACE-MP-PBE-0-L": "large", - "MACE-MP-PBE-0b-S": "small-0b", - "MACE-MP-PBE-0b-M": "medium-0b", - "MACE-MP-PBE-0b2-S": "small-0b2", - "MACE-MP-PBE-0b2-M": "medium-0b2", - "MACE-MP-PBE-0b2-L": "large-0b2", - "MACE-MP-PBE-0b3-M": "medium-0b3", - "MACE-MPA-PBE-0-M": "medium-mpa-0", - "MACE-OMAT-PBE-0-S": "small-omat-0", - "MACE-OMAT-PBE-0-M": "medium-omat-0", - "MACE-MatPES-PBE-0-M": "mace-matpes-pbe-0", - "MACE-MatPES-r2SCAN-0-M": "mace-matpes-r2scan-0", - "GRACE-MP-PBE-0-L": "GRACE-2L-MP-r6", - "GRACE-OAM-PBE-0-S": "GRACE-2L-OAM", - "GRACE-OAM-PBE-0-M": "GRACE-2L-OMAT-medium-ft-AM", - "GRACE-OAM-PBE-0-L": "GRACE-2L-OMAT-large-ft-AM", - "GRACE-OMAT-PBE-0-S": "GRACE-2L-OMAT", - "GRACE-OMAT-PBE-0-M": "GRACE-2L-OMAT-medium-ft-E", - "GRACE-OMAT-PBE-0-L": "GRACE-2L-OMAT-large-ft-E", -} - -_ID_LOOKUP = {cid.lower(): cid for cid in ID_TO_NAME} - # Common aliases and abbreviations will load the most advanced or widely used model. ALIAS_TO_ID = { ("tensornet", "tensornet-pbe", "pbe"): "TensorNet-MatPES-PBE-v2025.1-S", ("tensornet-r2scan", "r2scan"): "TensorNet-MatPES-r2SCAN-v2025.1-S", ("m3gnet", "m3gnet-pbe"): "M3GNet-MatPES-PBE-v2025.1-S", - ("m3gnet-r2scan"): "M3GNet-MatPES-r2SCAN-v2025.1-S", + ("m3gnet-r2scan",): "M3GNet-MatPES-r2SCAN-v2025.1-S", ("chgnet", "chgnet-pbe"): "CHGNet-MatPES-PBE-v2025.2-M", ("chgnet-r2scan",): "CHGNet-MatPES-r2SCAN-v2025.2-M", ("mace-mp-0", "mace-mp-0-m", "mace-mp-pbe-0"): "MACE-MP-PBE-0-M", @@ -127,6 +76,9 @@ } +MODEL_ID_PARTS = 5 # Architecture-Dataset-Functional-Version-Size + + UNIVERSAL_CALCULATORS = Enum("UNIVERSAL_CALCULATORS", {k: k for k in _universal_calculators}) # type: ignore[misc] @@ -432,21 +384,24 @@ def load_universal(name: str | Calculator, **kwargs: Any) -> Calculator: # noqa if not isinstance(name, str): return name - backend_name, route_tag = _resolve_model(name) - backend = _infer_backend(route_tag) + model_id = _resolve_model(name) + backend = _infer_backend(model_id) if backend == "matgl": - result = PESCalculator.load_matgl(backend_name, **kwargs) + model_name = _parse_matgl_model_id(model_id) + result = PESCalculator.load_matgl(model_name, **kwargs) elif backend == "mace": from mace.calculators import mace_mp - result = mace_mp(model=backend_name, **kwargs) + model_name = _parse_mace_model_id(model_id) + result = mace_mp(model=model_name, **kwargs) elif backend == "grace": from tensorpotential.calculator.foundation_models import grace_fm - result = grace_fm(model=backend_name, **kwargs) + model_name = _parse_grace_model_id(model_id) + result = grace_fm(model=model_name, **kwargs) elif backend == "sevennet": from sevenn.calculator import SevenNetCalculator @@ -552,44 +507,89 @@ def to_pmg_molecule(structure: Atoms | Structure | Molecule | IMolecule) -> IMol return Molecule.from_sites(structure) # type: ignore[return-value] -def _resolve_model(name: str) -> tuple[str, str]: - """Resolve user input to (backend_name, route_tag). - - - Canonical IDs (keys of ID_TO_NAME) are matched case-insensitively via _ID_LOOKUP. - - Aliases are matched case-insensitively via ALIAS_HASH_TABLE. - - If neither matches, passthrough is used. - """ +def _resolve_model(name: str) -> str: key = name.lower() - model_id = _ID_LOOKUP.get(key) or ALIAS_HASH_TABLE.get(key) - - if model_id is not None: - return ID_TO_NAME[model_id], model_id - - return name, key + return ALIAS_HASH_TABLE.get(key, name) -def _infer_backend(route_tag: str) -> str: - route_tag = route_tag.lower() +def _infer_backend(model_id: str) -> str: + model_id = model_id.lower() backend = "unknown" - if route_tag.startswith(("tensornet", "m3gnet", "chgnet")): + if model_id.startswith(("tensornet", "m3gnet", "chgnet")): backend = "matgl" - elif route_tag.startswith("mace"): + elif model_id.startswith("mace"): backend = "mace" - elif route_tag.startswith("grace"): + elif model_id.startswith("grace"): backend = "grace" - elif route_tag == "sevennet": + elif model_id == "sevennet": backend = "sevennet" - elif route_tag == "orb": + elif model_id == "orb": backend = "orb" - elif route_tag == "mattersim": + elif model_id == "mattersim": backend = "mattersim" - elif route_tag == "fairchem": + elif model_id == "fairchem": backend = "fairchem" - elif route_tag == "petmad": + elif model_id == "petmad": backend = "petmad" - elif route_tag.startswith("deepmd"): + elif model_id.startswith("deepmd"): backend = "deepmd" return backend + + +def _parse_mace_model_id(model_id: str) -> str: + parts = model_id.split("-") + result = model_id + + if len(parts) == MODEL_ID_PARTS and parts[0] == "MACE": + _, dataset, functional, version, size = parts + + size_map = {"S": "small", "M": "medium", "L": "large"} + mapped_size = size_map.get(size.upper()) + + if mapped_size is not None: + version = version.lower() + functional = functional.lower() + + if dataset == "MP": + result = mapped_size if version == "0" else f"{mapped_size}-{version}" + elif dataset == "MPA": + result = f"{mapped_size}-mpa-{version}" + elif dataset == "OMAT": + result = f"{mapped_size}-omat-{version}" + elif dataset == "MatPES": + result = f"mace-matpes-{functional}-{version}" + + return result + + +def _parse_matgl_model_id(model_id: str) -> str: + parts = model_id.split("-") + if len(parts) == MODEL_ID_PARTS and parts[0] in {"TensorNet", "M3GNet", "CHGNet"} and parts[1] == "MatPES": + return "-".join(parts[:-1] + ["PES"]) + + return model_id + + +def _parse_grace_model_id(model_id: str) -> str: + parts = model_id.split("-") + result = model_id + + if len(parts) == MODEL_ID_PARTS and parts[0] == "GRACE": + _, dataset, functional, version, size = parts + size = size.upper() + + if functional == "PBE" and version == "0": + if dataset == "MP": + if size == "L": + result = "GRACE-2L-MP-r6" + elif dataset in {"OMAT", "OAM"}: + if size == "S": + result = "GRACE-2L-OMAT" if dataset == "OMAT" else "GRACE-2L-OAM" + elif size in {"M", "L"}: + suffix = "E" if dataset == "OMAT" else "AM" + result = f"GRACE-2L-OMAT-medium-ft-{suffix}" if size == "M" else f"GRACE-2L-OMAT-large-ft-{suffix}" + + return result diff --git a/tests/test_utils.py b/tests/test_utils.py index 583e0559..0a02d8ad 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -12,10 +12,12 @@ from matcalc import RelaxCalc from matcalc.utils import ( ALIAS_TO_ID, - ID_TO_NAME, UNIVERSAL_CALCULATORS, PESCalculator, _resolve_model, + _parse_mace_model_id, + _parse_matgl_model_id, + _parse_grace_model_id, ) DIR = Path(__file__).parent.absolute() @@ -178,91 +180,48 @@ def test_pescalculator_calculate() -> None: assert list(stresses.shape) == [6] -def _known_backend_models() -> set[str]: - """Return all backend model names that IDs or aliases are allowed to map to.""" - # Built-in universal calculators - names = {u.name for u in UNIVERSAL_CALCULATORS} - - # MACE backend models - if find_spec("mace"): - from mace.calculators.foundations_models import mace_mp_names - - mace_models = set(mace_mp_names) - else: - # This set is from https://github.com/ACEsuit/mace/blob/main/mace/calculators/foundations_models.py#L37 - mace_models = { - "small", - "medium", - "large", - "small-0b", - "medium-0b", - "small-0b2", - "medium-0b2", - "large-0b2", - "medium-0b3", - "medium-mpa-0", - "small-omat-0", - "medium-omat-0", - "mace-matpes-pbe-0", - "mace-matpes-r2scan-0", - "mh-0", - "mh-1", - } - - # GRACE backend models - if find_spec("tensorpotential"): - from tensorpotential.calculator.foundation_models import MODELS_METADATA - - grace_models = set(MODELS_METADATA.keys()) - else: - # This set is from https://github.com/ICAMS/grace-tensorpotential/blob/master/tensorpotential/calculator/foundation_models.py#L46 - grace_models = { - "GRACE-1L-MP-r6", - "GRACE-2L-MP-r5", - "GRACE-2L-MP-r6", - "GRACE-FS-OAM", - "GRACE-1L-OAM", - "GRACE-2L-OAM", - "GRACE-FS-OMAT", - "GRACE-1L-OMAT", - "GRACE-2L-OMAT", - "GRACE-1L-OMAT-medium-base", - "GRACE-1L-OMAT-medium-ft-E", - "GRACE-1L-OMAT-medium-ft-AM", - "GRACE-1L-OMAT-large-base", - "GRACE-1L-OMAT-large-ft-E", - "GRACE-1L-OMAT-large-ft-AM", - "GRACE-2L-OMAT-medium-base", - "GRACE-2L-OMAT-medium-ft-E", - "GRACE-2L-OMAT-medium-ft-AM", - "GRACE-2L-OMAT-large-base", - "GRACE-2L-OMAT-large-ft-E", - "GRACE-2L-OMAT-large-ft-AM", - } - - return names | mace_models | grace_models - - -def test_id_to_name() -> None: - """Verify that all ID_TO_NAME values map to known backend model names.""" - known = _known_backend_models() - - for model_name in ID_TO_NAME.values(): - # Skip matgl models since they depend on installed pretrained models. - if any(key in model_name for key in ("M3GNet", "CHGNet", "TensorNet")): - continue - assert model_name in known, f"Unknown backend model mapped: {model_name!r}" - - -def test_allias_to_id() -> None: - """Ensure aliases resolve to canonical IDs and correct backend names.""" +def test_allias_to_id(): for aliases, model_id in ALIAS_TO_ID.items(): - assert model_id in ID_TO_NAME # Canonical ID must exist - expected_backend_name = ID_TO_NAME[model_id] - for alias in aliases: - backend_name, route_tag = _resolve_model(alias) - assert backend_name == expected_backend_name, ( - f"Alias {alias!r} resolved to {backend_name!r}, expected {expected_backend_name!r}" - ) - assert route_tag == model_id, f"Alias {alias!r} produced route_tag {route_tag!r}, expected {model_id!r}" + assert _resolve_model(alias) == model_id + +ID_TO_NAME = { + "TensorNet-MatPES-PBE-v2025.1-S": "TensorNet-MatPES-PBE-v2025.1-PES", + "TensorNet-MatPES-r2SCAN-v2025.1-S": "TensorNet-MatPES-r2SCAN-v2025.1-PES", + "M3GNet-MatPES-PBE-v2025.1-S": "M3GNet-MatPES-PBE-v2025.1-PES", + "M3GNet-MatPES-r2SCAN-v2025.1-S": "M3GNet-MatPES-r2SCAN-v2025.1-PES", + "MACE-MP-PBE-0-S": "small", + "MACE-MP-PBE-0-M": "medium", + "MACE-MP-PBE-0-L": "large", + "MACE-MP-PBE-0b-S": "small-0b", + "MACE-MP-PBE-0b-M": "medium-0b", + "MACE-MP-PBE-0b2-S": "small-0b2", + "MACE-MP-PBE-0b2-M": "medium-0b2", + "MACE-MP-PBE-0b2-L": "large-0b2", + "MACE-MP-PBE-0b3-M": "medium-0b3", + "MACE-MPA-PBE-0-M": "medium-mpa-0", + "MACE-OMAT-PBE-0-S": "small-omat-0", + "MACE-OMAT-PBE-0-M": "medium-omat-0", + "MACE-MatPES-PBE-0-M": "mace-matpes-pbe-0", + "MACE-MatPES-r2SCAN-0-M": "mace-matpes-r2scan-0", + "GRACE-MP-PBE-0-L": "GRACE-2L-MP-r6", + "GRACE-OAM-PBE-0-S": "GRACE-2L-OAM", + "GRACE-OAM-PBE-0-M": "GRACE-2L-OMAT-medium-ft-AM", + "GRACE-OAM-PBE-0-L": "GRACE-2L-OMAT-large-ft-AM", + "GRACE-OMAT-PBE-0-S": "GRACE-2L-OMAT", + "GRACE-OMAT-PBE-0-M": "GRACE-2L-OMAT-medium-ft-E", + "GRACE-OMAT-PBE-0-L": "GRACE-2L-OMAT-large-ft-E", +} + +@pytest.mark.parametrize("model_id, expected", ID_TO_NAME.items()) +def test_id_to_name(model_id: str, expected: str) -> None: + if model_id.startswith("MACE"): + parsed = _parse_mace_model_id(model_id) + elif model_id.startswith(("TensorNet", "M3GNet", "CHGNet")): + parsed = _parse_matgl_model_id(model_id) + elif model_id.startswith("GRACE"): + parsed = _parse_grace_model_id(model_id) + else: + pytest.skip(f"No parser for {model_id}") + + assert parsed == expected From f1379fb9aa239b5a473eb1b5e65d5967c3f19b71 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 23:38:42 +0000 Subject: [PATCH 16/23] pre-commit auto-fixes --- tests/test_utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 0a02d8ad..02e15ead 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -14,10 +14,10 @@ ALIAS_TO_ID, UNIVERSAL_CALCULATORS, PESCalculator, - _resolve_model, + _parse_grace_model_id, _parse_mace_model_id, _parse_matgl_model_id, - _parse_grace_model_id, + _resolve_model, ) DIR = Path(__file__).parent.absolute() @@ -185,6 +185,7 @@ def test_allias_to_id(): for alias in aliases: assert _resolve_model(alias) == model_id + ID_TO_NAME = { "TensorNet-MatPES-PBE-v2025.1-S": "TensorNet-MatPES-PBE-v2025.1-PES", "TensorNet-MatPES-r2SCAN-v2025.1-S": "TensorNet-MatPES-r2SCAN-v2025.1-PES", @@ -213,6 +214,7 @@ def test_allias_to_id(): "GRACE-OMAT-PBE-0-L": "GRACE-2L-OMAT-large-ft-E", } + @pytest.mark.parametrize("model_id, expected", ID_TO_NAME.items()) def test_id_to_name(model_id: str, expected: str) -> None: if model_id.startswith("MACE"): From 655f91d9067485ac98eb237f2137e47bfa24b0a8 Mon Sep 17 00:00:00 2001 From: Runze Liu Date: Mon, 5 Jan 2026 16:13:57 -0800 Subject: [PATCH 17/23] Fix --- src/matcalc/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matcalc/utils.py b/src/matcalc/utils.py index 01e0fe61..7ea6f681 100644 --- a/src/matcalc/utils.py +++ b/src/matcalc/utils.py @@ -568,7 +568,7 @@ def _parse_mace_model_id(model_id: str) -> str: def _parse_matgl_model_id(model_id: str) -> str: parts = model_id.split("-") if len(parts) == MODEL_ID_PARTS and parts[0] in {"TensorNet", "M3GNet", "CHGNet"} and parts[1] == "MatPES": - return "-".join(parts[:-1] + ["PES"]) + return "-".join([*parts[:-1], "PES"]) return model_id From 7dc737d429bc80c1126ff70d160f9c8816778a9c Mon Sep 17 00:00:00 2001 From: Runze Liu Date: Mon, 5 Jan 2026 16:35:20 -0800 Subject: [PATCH 18/23] Fix --- src/matcalc/_neb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matcalc/_neb.py b/src/matcalc/_neb.py index f3980636..0ecbc65c 100644 --- a/src/matcalc/_neb.py +++ b/src/matcalc/_neb.py @@ -12,7 +12,7 @@ try: from ase.mep import NEB -except ImportError: +except ImportError: # pragma: no cover from ase.neb import NEB from ase.utils.forcecurve import fit_images from pymatgen.core import Lattice, Structure From 4b365a6301df9a3acbdd116a2b0c0b8944e625a8 Mon Sep 17 00:00:00 2001 From: Runze Liu <146490083+rul048@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:12:28 -0800 Subject: [PATCH 19/23] Refactor NEB import handling in _neb.py Signed-off-by: Runze Liu <146490083+rul048@users.noreply.github.com> --- src/matcalc/_neb.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/matcalc/_neb.py b/src/matcalc/_neb.py index 0ecbc65c..49ce9ad3 100644 --- a/src/matcalc/_neb.py +++ b/src/matcalc/_neb.py @@ -12,8 +12,9 @@ try: from ase.mep import NEB -except ImportError: # pragma: no cover - from ase.neb import NEB +except ImportError: + from ase.neb import NEB as _NEB + NEB = _NEB from ase.utils.forcecurve import fit_images from pymatgen.core import Lattice, Structure from pymatgen.core.periodic_table import Species From f5c4a4bdac9ab63b68f3ec28a89cccbf59d75943 Mon Sep 17 00:00:00 2001 From: Runze Liu <146490083+rul048@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:18:51 -0800 Subject: [PATCH 20/23] Fix indentation for NEB assignment Signed-off-by: Runze Liu <146490083+rul048@users.noreply.github.com> --- src/matcalc/_neb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matcalc/_neb.py b/src/matcalc/_neb.py index 49ce9ad3..de87996d 100644 --- a/src/matcalc/_neb.py +++ b/src/matcalc/_neb.py @@ -14,7 +14,7 @@ from ase.mep import NEB except ImportError: from ase.neb import NEB as _NEB - NEB = _NEB + NEB = _NEB from ase.utils.forcecurve import fit_images from pymatgen.core import Lattice, Structure from pymatgen.core.periodic_table import Species From b10bb632bc1f2365552d87424ee1d04beafa65a9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 01:20:29 +0000 Subject: [PATCH 21/23] pre-commit auto-fixes --- src/matcalc/_neb.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matcalc/_neb.py b/src/matcalc/_neb.py index de87996d..874c3971 100644 --- a/src/matcalc/_neb.py +++ b/src/matcalc/_neb.py @@ -14,6 +14,7 @@ from ase.mep import NEB except ImportError: from ase.neb import NEB as _NEB + NEB = _NEB from ase.utils.forcecurve import fit_images from pymatgen.core import Lattice, Structure From 0aa163397b324e22dbc0e2caf85c02c71a3ecdde Mon Sep 17 00:00:00 2001 From: Runze Liu <146490083+rul048@users.noreply.github.com> Date: Tue, 6 Jan 2026 13:58:04 -0800 Subject: [PATCH 22/23] Simplify NEB import in _neb.py Signed-off-by: Runze Liu <146490083+rul048@users.noreply.github.com> --- src/matcalc/_neb.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/matcalc/_neb.py b/src/matcalc/_neb.py index 874c3971..d8b01086 100644 --- a/src/matcalc/_neb.py +++ b/src/matcalc/_neb.py @@ -10,12 +10,7 @@ from ase.io import Trajectory from ase.mep import NEBTools -try: - from ase.mep import NEB -except ImportError: - from ase.neb import NEB as _NEB - - NEB = _NEB +from ase.mep import NEB from ase.utils.forcecurve import fit_images from pymatgen.core import Lattice, Structure from pymatgen.core.periodic_table import Species From d00089e46be5a76130d5a9168ca6a94128d113be Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 21:58:58 +0000 Subject: [PATCH 23/23] pre-commit auto-fixes --- src/matcalc/_neb.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/matcalc/_neb.py b/src/matcalc/_neb.py index d8b01086..9219e87b 100644 --- a/src/matcalc/_neb.py +++ b/src/matcalc/_neb.py @@ -8,9 +8,7 @@ import numpy as np from ase.io import Trajectory -from ase.mep import NEBTools - -from ase.mep import NEB +from ase.mep import NEB, NEBTools from ase.utils.forcecurve import fit_images from pymatgen.core import Lattice, Structure from pymatgen.core.periodic_table import Species