Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions maple/function/calculator/chgnet/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._chgnet_calculator import CHGNetCalc

__all__ = ["CHGNetCalc"]
72 changes: 72 additions & 0 deletions maple/function/calculator/chgnet/_chgnet_calculator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
CHGNet calculator wrapper. Uses pretrained CHGNet via chgnet package; converts eV -> Hartree.
"""
from typing import Literal

from ase.calculators.calculator import all_changes
from ..calculator_base import CalcABC

EV2HARTREE = 1.0 / 27.211386245988


def _get_chgnet_calculator(device):
"""Build CHGNet ASE calculator (pretrained model, no local file)."""
try:
from chgnet.model import CHGNet
from chgnet.model import CHGNetCalculator
except ImportError:
raise ImportError(
"CHGNet is required. Install with: pip install chgnet"
) from None
chgnet = CHGNet.load()
return CHGNetCalculator(potential=chgnet)
Comment on lines +21 to +22
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The device parameter is accepted but never used when creating the CHGNet calculator. CHGNet.load() and CHGNetCalculator() should be configured with the device parameter to ensure the model runs on the correct device (CPU vs GPU). The device should be passed to CHGNet.load() using the use_device parameter.

Suggested change
chgnet = CHGNet.load()
return CHGNetCalculator(potential=chgnet)
chgnet = CHGNet.load(use_device=device)
return CHGNetCalculator(potential=chgnet, use_device=device)

Copilot uses AI. Check for mistakes.


class CHGNetCalc(CalcABC):
"""ASE calculator wrapping CHGNet (charge-informed graph neural network potential)."""

implemented_properties = ["energy", "forces", "free_energy", "stress"]

def __init__(
self,
device,
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The device parameter is missing a type hint. For consistency with other calculators in the codebase (e.g., ANICalculator, MACEModelCalculator), consider adding the type hint: device: torch.device.

Copilot uses AI. Check for mistakes.
model: str = "chgnet",
overwrite: bool = False,
implicit: Literal["gbsa", "none"] = "gbsa",
solvent: str = "none",
):
super().__init__()
self._chg = _get_chgnet_calculator(device)
self.device = device
self.overwrite = overwrite
self.implicit_solv_init(implicit=implicit, solvent=solvent)

def calculate(
self, atoms=None, properties=("energy", "forces"), system_changes=all_changes
):
super().calculate(atoms, properties, system_changes)

self._chg.calculate(atoms, properties=properties, system_changes=system_changes)

energy_eV = self._chg.results.get("energy", 0.0)
energy = energy_eV * EV2HARTREE

if self.solvent_correction:
energy = energy + self.implicit_solv_energy(atoms).item()

self.results["energy"] = energy if isinstance(energy, float) else energy.item()
self.results["free_energy"] = self.results["energy"]

if "forces" in properties:
forces = self._chg.results.get("forces")
if forces is not None:
forces = forces * EV2HARTREE # eV/Å -> Hartree/Å
if self.solvent_correction:
_, solvent_force = self.implicit_solv_energy_and_force(atoms)
forces = forces + solvent_force.cpu().numpy()
self.results["forces"] = forces

if "stress" in properties:
stress = self._chg.results.get("stress")
if stress is not None:
self.results["stress"] = stress * EV2HARTREE # eV/ų -> Hartree/ų
3 changes: 3 additions & 0 deletions maple/function/calculator/deepmd/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._dpa3_calculator import DPA3Calculator

__all__ = ["DPA3Calculator"]
86 changes: 86 additions & 0 deletions maple/function/calculator/deepmd/_dpa3_calculator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
DPA3 / DeepMD-Kit calculator (PyTorch backend).
Uses the unified DP ASE calculator from deepmd.calculator; converts eV -> Hartree.
"""
import os
from typing import Literal

from ase.calculators.calculator import all_changes
from ..calculator_base import CalcABC

EV2HARTREE = 1.0 / 27.211386245988


def _get_dp_calculator():
"""Import DeepMD-Kit DP calculator (works with .pt/.pth DPA3 models)."""
try:
from deepmd.calculator import DP
return DP
except ImportError:
try:
from deepmd.pt.utils.ase_calc import DPCalculator as DP
return DP
except ImportError:
raise ImportError(
"DeepMD-Kit is required for DPA3. Install with: pip install deepmd-kit"
) from None


class DPA3Calculator(CalcABC):
"""ASE calculator wrapping DeepMD-Kit DP (DPA3 and other Deep Potential models)."""

implemented_properties = ["energy", "forces", "free_energy"]

def __init__(
self,
device,
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The device parameter is missing a type hint. For consistency with other calculators in the codebase (e.g., ANICalculator, MACEModelCalculator), consider adding the type hint: device: torch.device.

Copilot uses AI. Check for mistakes.
model: str = "dpa3",
overwrite: bool = False,
implicit: Literal["gbsa", "none"] = "gbsa",
solvent: str = "none",
):
super().__init__()
DP = _get_dp_calculator()

model_dir = os.path.dirname(os.path.realpath(__file__))
model_dir = os.path.dirname(model_dir)
model_dir = os.path.dirname(model_dir)
default_path = os.path.join(model_dir, "model", f"{model}.pt")
if os.path.isfile(default_path):
model_path = default_path
elif os.path.isdir(model):
model_path = model
elif os.path.isfile(model):
model_path = model
else:
model_path = default_path
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The model path resolution logic has a potential issue. If the default_path does not exist and the model parameter is neither a file nor a directory (e.g., model="dpa3"), the code will still use default_path which doesn't exist, leading to a failure when instantiating the DP calculator. Consider raising a clear error message if no valid model path is found, or ensure the model file exists before attempting to load it.

Suggested change
model_path = default_path
raise FileNotFoundError(
f"Could not find DeepMD/DPA3 model. "
f"Neither default model file '{default_path}' nor provided "
f"model path '{model}' exists."
)

Copilot uses AI. Check for mistakes.

self._dp = DP(model=model_path)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The device parameter is stored but never passed to the DP calculator during instantiation. DeepMD-Kit calculators typically need to be configured with the target device to ensure the model runs on the correct device (CPU vs GPU). Check if the DP calculator accepts a device parameter and pass it during instantiation, or verify that the model file already contains device information.

Suggested change
self._dp = DP(model=model_path)
self._dp = DP(model=model_path, device=device)

Copilot uses AI. Check for mistakes.
self.device = device
self.overwrite = overwrite
self.implicit_solv_init(implicit=implicit, solvent=solvent)

def calculate(
self, atoms=None, properties=("energy", "forces"), system_changes=all_changes
):
super().calculate(atoms, properties, system_changes)

self._dp.calculate(atoms, properties=properties, system_changes=system_changes)

energy_eV = self._dp.results.get("energy", 0.0)
energy = energy_eV * EV2HARTREE

if self.solvent_correction:
energy = energy + self.implicit_solv_energy(atoms).item()

self.results["energy"] = energy if isinstance(energy, float) else energy.item()
self.results["free_energy"] = self.results["energy"]

if "forces" in properties:
forces = self._dp.results.get("forces")
if forces is not None:
forces = forces * EV2HARTREE # eV/Å -> Hartree/Å
if self.solvent_correction:
_, solvent_force = self.implicit_solv_energy_and_force(atoms)
forces = forces + solvent_force.cpu().numpy()
self.results["forces"] = forces
10 changes: 10 additions & 0 deletions maple/function/calculator/set_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
'uma',
'maceomol',
'aimnet2nse',
'dpa3',
'chgnet',
]

# Model name to filename mapping for HuggingFace download
Expand Down Expand Up @@ -145,6 +147,14 @@ def set_calculator(self) -> ase.calculators.calculator.Calculator:
from .mace._mace_general_calculator import MACEModelCalculator
calculator = MACEModelCalculator(model=self.model, device=self.device, implicit = self.implicit, solvent = self.solvent)
return calculator
elif self.model == 'dpa3':
from .deepmd._dpa3_calculator import DPA3Calculator
calculator = DPA3Calculator(model=self.model, device=self.device, implicit=self.implicit, solvent=self.solvent)
return calculator
elif self.model == 'chgnet':
from .chgnet._chgnet_calculator import CHGNetCalc
calculator = CHGNetCalc(model=self.model, device=self.device, implicit=self.implicit, solvent=self.solvent)
return calculator
else:
raise ValueError(f"Model '{self.model}' is not implemented yet.")

Expand Down
3 changes: 2 additions & 1 deletion maple/function/read/command_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class CommandControl:
SUPPORTED_MODELS = {
"ani2x", "ani1x", "ani1ccx", "ani1xnr",
"maceoff23s", "maceoff23m", "maceoff23l",
"egret", "aimnet2", "uma", "maceomol", "aimnet2nse"
"egret", "aimnet2", "uma", "maceomol", "aimnet2nse",
"dpa3", "chgnet",
}

SUPPORTED_TASKS = {"sp", "opt", "ts", "scan", "freq", "irc"}
Expand Down
Loading