Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
1036de2
Implement MACEEMLEJoint backend
kzinovjev Sep 13, 2025
35c030e
Fix vacuum energy not printed with MACE backends
kzinovjev Sep 13, 2025
5c7b834
Use static atomic dipoles in maceemle backend
kzinovjev Sep 25, 2025
16e01d1
Include s, q_core and q into qm.xyz (maceemle backend)
kzinovjev Sep 25, 2025
bc65db6
Include mu into qm.xyz (maceemle backend)
kzinovjev Sep 25, 2025
9e8ff94
Write total QM charge to qm.xyz
kzinovjev Sep 25, 2025
0ab5a34
Merge branch 'emle-analyze-without-orca' into emle-mace
kzinovjev Sep 26, 2025
42d2595
Add maceemle support to emle-analyze
kzinovjev Sep 26, 2025
fc42131
Include static atomic dipoles data in emle-analyze output
kzinovjev Sep 26, 2025
5cbbd84
Fix total charge not passes to MACEEMLEJoint
kzinovjev Oct 9, 2025
174b177
Added methods to parse energy and forces from ORCA output
Oct 12, 2025
bf808ab
Added Eh/a0 to eV/A conversion factor and import units in parser
Oct 12, 2025
da9cdab
Added tarball-to-extxyz conversion tool
Oct 12, 2025
92a7a1d
Merge branch 'main' into emle-mace
kzinovjev Dec 19, 2025
5e40730
Typo
kzinovjev Dec 19, 2025
53c3bda
Add MACE gradients to emle-analyze output
kzinovjev Dec 19, 2025
8dc7917
Merge pull request #2 from lkantin/emle-mace
kzinovjev Feb 18, 2026
505fdb2
Add optional cell argument to EMLECalculator._sire_callback
kzinovjev Feb 19, 2026
473b652
Merge branch 'callback-fix' into emle-mace
kzinovjev Feb 19, 2026
d9ab8e2
Merge remote-tracking branch 'origin/main' into emle-mace
kzinovjev Feb 20, 2026
a2152c6
Merge remote-tracking branch 'origin/emle-mace' into emle-mace
kzinovjev Feb 20, 2026
ea42914
Add missing cell argument to MACEEMLEJoint.forward
kzinovjev Feb 26, 2026
1af64ed
Fix plot_data in EMLETrainer broken due to Thole loss returning tuples
kzinovjev Feb 28, 2026
e70dacc
Fix plot_data in EMLETrainer broken due to Thole loss returning tuples
kzinovjev Feb 28, 2026
27e24fc
Fix custom species not stored on EMLEBase instance
kzinovjev Mar 2, 2026
bdb5a92
Merge branch 'devel' into emle-mace
kzinovjev Mar 2, 2026
b66239e
Merge pull request #70 from kzinovjev/devel
lohedges Mar 3, 2026
b0ad29e
Blacken. [ci skip]
lohedges Mar 3, 2026
db97ca0
Fix bugs in calculator.py
lohedges Mar 11, 2026
8a73e7d
Cache MACE models to avoid download rate limit errors.
lohedges Mar 11, 2026
7fff638
Merge pull request #72 from chemle/fix_calculator
lohedges Mar 11, 2026
f205eaf
Pass total QM charge to _calculate_energy_and_gradients inside sire c…
kzinovjev Mar 14, 2026
3286cde
Merge branch 'devel' into emle-mace
kzinovjev Mar 14, 2026
0c25b14
Merge branch 'devel' into devel
kzinovjev Mar 14, 2026
6bc845a
Merge remote-tracking branch 'origin/devel' into emle-mace
kzinovjev Mar 14, 2026
c40d021
Merge pull request #74 from kzinovjev/devel
lohedges Mar 24, 2026
20bc1ed
Merge branch 'devel' into emle-mace
kzinovjev Apr 11, 2026
9ba159d
Support EnergyEMLEMACE models in MACEEMLE and MACEEMLEJoint
kzinovjev Apr 12, 2026
b026c93
Inject EnergyEMLEMACE into mace.modules.models for torch.load()
kzinovjev Apr 12, 2026
82ffa7e
Fix torch.load weights_only in _load_mace_model for PyTorch 2.6+
kzinovjev Apr 12, 2026
26c9382
Fix PyTorch 2.6 / e3nn import compatibility in EnergyEMLEMACE injecti…
kzinovjev Apr 12, 2026
0d378cd
Support compiled (TorchScript) EnergyEMLEMACE models in MACEEMLE and …
kzinovjev Apr 12, 2026
942c1dc
rename: maceemle backend → emle-mace
kzinovjev Apr 13, 2026
24a9a53
Fix misplaced MACE model help string in emle-analyze
kzinovjev Apr 13, 2026
04a46cf
Fix shapes of arguments passed to self._emle in MACEEMLEJoint
kzinovjev Apr 18, 2026
0e7c4c9
Add test for emle-mace backend
kzinovjev Apr 18, 2026
913ffc6
Extract model type check to _is_energy_emle_mace helper function
kzinovjev Apr 24, 2026
0806751
Doc strings and style cleanup
kzinovjev Apr 24, 2026
b340afa
Move EV_TO_HARTREE outside of batches loop in mace models
kzinovjev Apr 24, 2026
e0446e0
Keep batch dimension of self._emle call arguments in MACEEMLE.forward()
kzinovjev Apr 24, 2026
91e4a23
Always use alpha_mode="species" in MACEEMLEJoint
kzinovjev Apr 24, 2026
59d4755
Define k_Z and a_Thole outside of batch loop in MACEEMLEJoint.forward
kzinovjev Apr 24, 2026
cb14a30
Style cleanup
kzinovjev Apr 24, 2026
792c984
Store q_val in self.emle_values in MACEEMLEJoint (to be written to qm…
kzinovjev Apr 24, 2026
6d51128
Rely on q_val being present in qm.xyz in EMLEAnalyzer
kzinovjev Apr 24, 2026
06a8645
Add doc string for external_params argument in EMLE.forward()
kzinovjev Apr 24, 2026
119e4e7
Rename self.use_dipoles to self._use_dipoles in EMLECalculator
kzinovjev Apr 24, 2026
4f31263
Assert that EMLECalculator is called with a single structure, not a b…
kzinovjev Apr 24, 2026
ae73413
Cleanup
kzinovjev Apr 24, 2026
4d2af58
Doc strings and formatting cleanup in ORCAParser
kzinovjev Apr 24, 2026
35d2aee
Doc strings and formatting cleanup in orca_to_extxyz
kzinovjev Apr 24, 2026
db0a634
Return Hartree/Bohr instead of eV/A from ORCAParser.get_forces()
kzinovjev Apr 24, 2026
64fa2b3
Blacken
kzinovjev Apr 24, 2026
9e0760d
Cleanup
kzinovjev Apr 27, 2026
89aa423
Merge pull request #75 from kzinovjev/mace-emle-joint
lohedges Apr 28, 2026
87aa03a
Merge branch 'main' into sync_devel
lohedges May 5, 2026
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
29 changes: 23 additions & 6 deletions bin/emle-analyze
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ parser.add_argument(
parser.add_argument(
"--backend",
type=str,
choices=["torchani", "mace", "deepmd"],
choices=["torchani", "mace", "emle-mace", "deepmd"],
help="Gas phase ML backend",
)
parser.add_argument(
Expand All @@ -48,7 +48,7 @@ parser.add_argument(
parser.add_argument(
"--mace-model",
type=str,
help="MACE model file (for backend='mace')",
help="MACE model file (for 'mace' and 'emle-mace' backends)",
)
parser.add_argument(
"--qm-xyz", type=str, metavar="name.xyz", required=True, help="QM xyz file"
Expand All @@ -73,8 +73,14 @@ parser.add_argument(
parser.add_argument(
"--q-total",
type=int,
default=0,
help="Total charge of the QM region",
)
parser.add_argument(
"--use-dipoles",
action="store_true",
help="Whether to include static atomic dipoles",
)
parser.add_argument(
"--start",
type=int,
Expand All @@ -97,8 +103,8 @@ from emle._orca_parser import ORCAParser

if args.backend == "deepmd" and not args.deepmd_model:
parser.error("--deepmd-model is required when backend='deepmd'")
if args.backend == "mace" and not args.mace_model:
parser.error("--mace-model is required when backend='mace'")
if args.backend in ("mace", "emle-mace") and not args.mace_model:
parser.error("--mace-model is required when backend is 'mace' or 'emle-mace'")

backend = None
if args.backend == "torchani":
Expand All @@ -110,10 +116,14 @@ elif args.backend == "mace":

backend = MACEEMLE(emle_model=args.emle_model, mace_model=args.mace_model)

elif args.backend == "maceemle":
elif args.backend == "emle-mace":
from emle.models import MACEEMLEJoint

backend = MACEEMLEJoint(emle_model=args.emle_model, mace_model=args.mace_model)
backend = MACEEMLEJoint(
emle_model=args.emle_model,
mace_model=args.mace_model,
qm_charge=args.q_total,
)

elif args.backend == "deepmd":
from emle._backends import DeePMD
Expand All @@ -133,6 +143,7 @@ analyzer = EMLEAnalyzer(
backend,
orca_parser,
args.q_total,
args.use_dipoles,
start=args.start,
end=args.end,
)
Expand All @@ -148,6 +159,9 @@ result = {
"atomic_alpha_emle": analyzer.atomic_alpha,
"alpha_emle": analyzer.alpha,
}
if args.use_dipoles:
result["mu_emle"] = analyzer.mu
result["E_static_mu_emle"] = analyzer.e_static_mu

if orca_parser:
result.update(
Expand All @@ -163,9 +177,12 @@ if orca_parser:
"E_static_mbis": analyzer.e_static_mbis,
}
)
if args.use_dipoles:
result["mu_qm"] = orca_parser.mbis["mu"]

if args.backend:
result["E_vac_emle"] = analyzer.e_backend
result["grad_vac_emle"] = analyzer.grad_backend
if args.alpha:
result["alpha_qm"] = orca_parser.alpha

Expand Down
5 changes: 5 additions & 0 deletions bin/emle-server
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ except:
model = os.getenv("EMLE_MODEL")
method = os.getenv("EMLE_METHOD")
alpha_mode = os.getenv("EMLE_ALPHA_MODE")
try:
use_dipoles = strtobool(os.getenv("EMLE_USE_DIPOLES"))
except:
use_dipoles = False
atomic_numbers = os.getenv("EMLE_ATOMIC_NUMBERS")
mm_charges = os.getenv("EMLE_MM_CHARGES")
try:
Expand Down Expand Up @@ -144,6 +148,7 @@ env = {
"model": model,
"method": method,
"alpha_mode": alpha_mode,
"use_dipoles": use_dipoles,
"atomic_numbers": atomic_numbers,
"mm_charges": mm_charges,
"num_clients": num_clients,
Expand Down
6 changes: 6 additions & 0 deletions bin/tarball-to-extxyz
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env python

from emle._tarball_to_extXYZ import main

if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions emle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
_supported_backends = [
"torchani",
"mace",
"emle-mace",
"ace",
"deepmd",
"orca",
Expand Down
46 changes: 39 additions & 7 deletions emle/_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

from ._units import _HARTREE_TO_KCAL_MOL, _ANGSTROM_TO_BOHR
from ._utils import pad_to_max as _pad_to_max
from .models import MACEEMLEJoint as _MACEEMLEJoint


class EMLEAnalyzer:
Expand All @@ -48,6 +49,7 @@ def __init__(
backend=None,
parser=None,
q_total=None,
use_dipoles=False,
start=None,
end=None,
):
Expand Down Expand Up @@ -145,13 +147,18 @@ def __init__(
if isinstance(backend, _torch.nn.Module):
backend = backend.to(device).to(dtype)
atomic_numbers = _torch.tensor(atomic_numbers, device=device)
qm_xyz = _torch.tensor(qm_xyz, dtype=dtype, device=device)
qm_xyz = _torch.tensor(
qm_xyz, dtype=dtype, device=device, requires_grad=True
)
charges_mm = _torch.empty((len(qm_xyz), 0), dtype=dtype, device=device)
mm_xyz = _torch.empty((len(qm_xyz), 0, 3), dtype=dtype, device=device)
self.e_backend = (
backend(atomic_numbers, charges_mm, qm_xyz, mm_xyz).T
backend(atomic_numbers, charges_mm, qm_xyz, mm_xyz, qm_charge=q_total).T
* _HARTREE_TO_KCAL_MOL
)
self.grad_backend = _torch.autograd.grad(self.e_backend.T[0].sum(), qm_xyz)[
0
]

self.atomic_numbers = _torch.tensor(
atomic_numbers, dtype=_torch.int, device=device
Expand All @@ -163,11 +170,25 @@ def __init__(
qm_xyz_bohr = self.qm_xyz * _ANGSTROM_TO_BOHR
pc_xyz_bohr = self.pc_xyz * _ANGSTROM_TO_BOHR

self.s, self.q_core, self.q_val, self.A_thole = emle_base(
self.atomic_numbers,
self.qm_xyz,
self.q_total,
)
if isinstance(backend, _MACEEMLEJoint):
self.s = _torch.stack(backend.emle_values["s"])
self.q_core = _torch.stack(backend.emle_values["q_core"])
self.q_val = _torch.stack(backend.emle_values["q_val"])
self.mu = _torch.stack(backend.emle_values["mu"])

a_Thole = backend._mace.a_Thole
species_id = emle_base._species_map[self.atomic_numbers]
k = backend._mace.elements_alpha_v_ratios[species_id]
r_data = emle_base._get_r_data(qm_xyz_bohr, atomic_numbers > 0)
self.A_thole = emle_base._get_A_thole(
r_data, self.s, self.q_val, k, a_Thole
)
else:
self.s, self.q_core, self.q_val, self.A_thole = emle_base(
self.atomic_numbers,
self.qm_xyz,
self.q_total,
)
self.atomic_alpha = 1.0 / _torch.diagonal(self.A_thole, dim1=1, dim2=2)[:, ::3]
self.alpha = self._get_mol_alpha(self.A_thole, self.atomic_numbers)

Expand All @@ -179,6 +200,13 @@ def __init__(
)
* _HARTREE_TO_KCAL_MOL
)
if use_dipoles:
self.e_static_mu = (
emle_base.get_static_energy(
self.q_core, self.q_val, self.pc_charges, mesh_data, self.mu
)
* _HARTREE_TO_KCAL_MOL
)
self.e_induced = (
emle_base.get_induced_energy(
self.A_thole, self.pc_charges, self.s, mesh_data, mask
Expand All @@ -203,13 +231,17 @@ def __init__(
"s",
"q_core",
"q_val",
"q",
"mu",
"q_total",
"atomic_alpha",
"alpha",
"e_backend",
"e_static",
"e_static_mu",
"e_induced",
"e_static_mbis",
"grad_backend",
):
if attr in self.__dict__:
setattr(self, attr, getattr(self, attr).detach().cpu().numpy())
Expand Down
77 changes: 73 additions & 4 deletions emle/_orca_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@
import tarfile as _tarfile

from ._utils import pad_to_max

_HARTREE_TO_KCALMOL = 627.509
from ._units import _HARTREE_TO_KCAL_MOL


class ORCAParser:
Expand Down Expand Up @@ -132,6 +131,9 @@ def __init__(self, filename, decompose=False, alpha=False):
self.mbis = self._parse_horton()
self.z, self.xyz = self._get_z_xyz()

self.E_vac = self.get_E_vac()
self.forces = self.get_forces()

if decompose:
self.vac_E, self.pc_E = self._get_E()
self.E = self.pc_E - self.vac_E
Expand Down Expand Up @@ -165,13 +167,13 @@ def _get_E(self):
def _get_E_from_out(self, f):
E_prefix = b"FINAL SINGLE POINT ENERGY"
E_line = next(line for line in f if line.startswith(E_prefix))
return float(E_line.split()[-1]) * _HARTREE_TO_KCALMOL
return float(E_line.split()[-1]) * _HARTREE_TO_KCAL_MOL

def _get_E_static(self):
vpot_all = self._get_vpot()
pc_all = self._get_pc()
result = _np.array([(vpot @ pc) for vpot, pc in zip(vpot_all, pc_all)])
return result * _HARTREE_TO_KCALMOL
return result * _HARTREE_TO_KCAL_MOL

def _get_vpot(self):
return [
Expand Down Expand Up @@ -251,3 +253,70 @@ def _parse_horton_out(self, f):

def _get_file(self, name, suffix):
return self._tar.extractfile(f"{name}.{suffix}")

def get_E_vac(self):
"""
Parse the gas-phase ORCA output for each configuration and return the
total energy in Hartree.

Returns
-------

E_tot: numpy.ndarray (N_CONFIGS,)
Total gas-phase energy per configuration, in Hartree.
"""
E_tot = [
self._get_E_vac_from_out(self._get_file(name, "vac.orca"))
for name in self.names
]
return _np.array(E_tot)

def _get_E_vac_from_out(self, f):
"""
Extract the total energy from an ORCA gas-phase output file.
"""
E_prefix = b"Total Energy :"
E_line = [line for line in f if line.startswith(E_prefix)]
return float(E_line[0].split()[-2])

def get_forces(self):
"""
Parse the gas-phase ORCA output for each configuration and return the
atomic forces in Hartree/Bohr.

Returns
-------

forces: List[numpy.ndarray]
Per-configuration atomic forces in Hartree/Bohr, each of shape
(N_ATOMS, 3).
"""
forces = [
self._get_forces_from_out(self._get_file(name, "vac.orca"))
for name in self.names
]
return forces

def _get_forces_from_out(self, f):
"""
Extract the Cartesian forces from an ORCA gas-phase output file.
ORCA reports the gradient in Hartree/Bohr; we negate (forces =
-gradient) and return in the same units.
"""
while next(f) != b"CARTESIAN GRADIENT\n":
pass
next(f)
next(f)
forces = []
try:
while True:
line_elements = next(f).split()
if len(line_elements) == 0:
break
# Extract gradient (last 3 elements) and negate to get forces.
force_values = [-float(x) for x in line_elements[-3:]]
forces.append(force_values)

except ValueError:
pass
return _np.asarray(forces)
Loading
Loading