Skip to content

Commit 2af1389

Browse files
authored
Merge pull request #7 from tdpham2/dev/add-pacmof2-module
Add PACMOF2 module for charge prediction
2 parents 68631b0 + bec8fd6 commit 2af1389

7 files changed

Lines changed: 233 additions & 9 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ dependencies = [
1919
rdkit = ["rdkit"]
2020
mlip = ["mace-torch"]
2121
plot = ["matplotlib>=3.5"]
22-
all = ["rdkit", "mace-torch", "matplotlib>=3.5"]
22+
pacmof2 = ["pacmof2"]
23+
all = ["rdkit", "mace-torch", "matplotlib>=3.5", "pacmof2"]
2324
dev = ["pytest>=7.0", "ruff>=0.4"]
2425

2526
[project.scripts]

src/matkit/__init__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
11
"""MatKit: A modular Python toolkit for molecular simulations."""
22

33
__version__ = "0.1.0"
4+
5+
_SUBMODULES = {
6+
"graspa",
7+
"graspa_sycl",
8+
"raspa2",
9+
"raspa3",
10+
"zeopp",
11+
"mlip",
12+
"utils",
13+
"io",
14+
"plot",
15+
"tobacco",
16+
"orca",
17+
"pacmof2",
18+
}
19+
20+
21+
def __getattr__(name):
22+
if name in _SUBMODULES:
23+
import importlib
24+
25+
return importlib.import_module(f"matkit.{name}")
26+
raise AttributeError(f"module 'matkit' has no attribute {name!r}")

src/matkit/cli.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,116 @@ def uma_opt_batch_cmd(
10531053
click.echo(f"Error: {e}", err=True)
10541054

10551055

1056+
# ==========================================
1057+
# PACMOF2 COMMANDS
1058+
# ==========================================
1059+
@main.group("pacmof2")
1060+
def pacmof2_cli():
1061+
"""Commands for PACMOF2 charge prediction."""
1062+
pass
1063+
1064+
1065+
@pacmof2_cli.command("predict")
1066+
@click.option(
1067+
"--cif",
1068+
default=None,
1069+
type=click.Path(exists=True),
1070+
help="Path to a single CIF file.",
1071+
)
1072+
@click.option(
1073+
"--cif-dir",
1074+
default=None,
1075+
type=click.Path(exists=True, file_okay=False),
1076+
help="Directory containing CIF files.",
1077+
)
1078+
@click.option(
1079+
"--outdir",
1080+
required=True,
1081+
type=click.Path(),
1082+
help="Output directory for CIFs with predicted charges.",
1083+
)
1084+
@click.option(
1085+
"--identifier",
1086+
default="_pacmof",
1087+
help="Suffix for output filenames (default: _pacmof).",
1088+
)
1089+
@click.option(
1090+
"--net-charge",
1091+
default="0",
1092+
help="Net charge: integer for single MOF, or path to "
1093+
"JSON file mapping filenames to charges for batch.",
1094+
)
1095+
@click.option(
1096+
"--adjust-method",
1097+
default="mean",
1098+
type=click.Choice(["mean", "magnitude"]),
1099+
help="Charge adjustment method.",
1100+
)
1101+
def pacmof2_predict(
1102+
cif,
1103+
cif_dir,
1104+
outdir,
1105+
identifier,
1106+
net_charge,
1107+
adjust_method,
1108+
):
1109+
"""Predict partial atomic charges for CIF structures.
1110+
1111+
Provide either --cif for a single file or --cif-dir for
1112+
a directory of CIF files.
1113+
1114+
\b
1115+
Examples:
1116+
matkit pacmof2 predict --cif-dir cifs/ --outdir charged/
1117+
matkit pacmof2 predict --cif structure.cif --outdir charged/
1118+
matkit pacmof2 predict --cif-dir cifs/ --outdir charged/ \\
1119+
--net-charge charges.json
1120+
"""
1121+
if cif and cif_dir:
1122+
click.echo(
1123+
"Error: specify --cif or --cif-dir, not both.",
1124+
err=True,
1125+
)
1126+
return
1127+
if not cif and not cif_dir:
1128+
click.echo("Error: provide --cif or --cif-dir.", err=True)
1129+
return
1130+
1131+
cif_path = cif_dir if cif_dir else cif
1132+
1133+
# Parse net_charge: try int/float first, then JSON path
1134+
try:
1135+
nc = int(net_charge)
1136+
except ValueError:
1137+
try:
1138+
nc = float(net_charge)
1139+
except ValueError:
1140+
nc = net_charge # treat as JSON file path
1141+
1142+
try:
1143+
from matkit.pacmof2 import run_charge_prediction
1144+
1145+
result = run_charge_prediction(
1146+
cif_path=cif_path,
1147+
output_dir=outdir,
1148+
identifier=identifier,
1149+
net_charge=nc,
1150+
adjust_charge_method=adjust_method,
1151+
)
1152+
click.echo(
1153+
f"Predicted charges for {result['num_structures']} "
1154+
f"structure(s). Output: {result['output_dir']}"
1155+
)
1156+
except ImportError:
1157+
click.echo(
1158+
"Error: pacmof2 is required. "
1159+
"Install with: pip install matkit[pacmof2]",
1160+
err=True,
1161+
)
1162+
except Exception as e:
1163+
click.echo(f"Error: {e}", err=True)
1164+
1165+
10561166
# ==========================================
10571167
# ZEOPP COMMANDS
10581168
# ==========================================

src/matkit/graspa/files/template/simulation.input

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,4 @@ CutOffVDW CUTOFF
2727
CutOffCoulomb CUTOFF
2828
EwaldPrecision 1e-6
2929

30-
Component 0 MoleculeName ADSORBATE
31-
IdealGasRosenbluthWeight 1.0
32-
FugacityCoefficient PR-EOS
33-
TranslationProbability 1.0
34-
RotationProbability 1.0
35-
ReinsertionProbability 1.0
36-
SwapProbability 2.0
37-
CreateNumberOfMolecules 0
30+
__COMPONENTS__

src/matkit/pacmof2/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
__all__ = []
2+
3+
try:
4+
from matkit.pacmof2.pacmof2 import run_charge_prediction
5+
6+
__all__ += ["run_charge_prediction"]
7+
except ImportError:
8+
pass

src/matkit/pacmof2/pacmof2.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""PACMOF2 charge prediction wrapper for matkit."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
from pathlib import Path
7+
from typing import Union
8+
9+
from pacmof2 import get_charges
10+
11+
from matkit.types import PACMOF2Result
12+
13+
14+
def run_charge_prediction(
15+
cif_path: str,
16+
output_dir: str,
17+
identifier: str = "_pacmof",
18+
net_charge: Union[int, float, dict] = 0,
19+
adjust_charge_method: str = "mean",
20+
) -> PACMOF2Result:
21+
"""Run PACMOF2 charge prediction on CIF file(s).
22+
23+
Args:
24+
cif_path: Path to a single CIF file or a directory
25+
containing CIF files.
26+
output_dir: Directory where output CIF files with
27+
predicted charges will be written.
28+
identifier: Suffix appended to output filenames
29+
(default: "_pacmof").
30+
net_charge: Net charge for ionic MOFs. Use 0 for
31+
neutral MOFs (default), an int/float for a single
32+
structure, or a dict mapping CIF filenames to
33+
charges for batch ionic processing.
34+
adjust_charge_method: Method to enforce net charge
35+
constraint. Either "mean" (default) or
36+
"magnitude".
37+
38+
Returns:
39+
PACMOF2Result dict with keys: success, output_dir,
40+
num_structures, error.
41+
"""
42+
cif_path = Path(cif_path)
43+
output_dir = Path(output_dir)
44+
45+
if not cif_path.exists():
46+
raise FileNotFoundError(f"CIF path not found: {cif_path}")
47+
48+
multiple_cifs = cif_path.is_dir()
49+
50+
if multiple_cifs:
51+
n = len(list(cif_path.glob("*.cif")))
52+
if n == 0:
53+
raise FileNotFoundError(f"No .cif files found in {cif_path}")
54+
else:
55+
n = 1
56+
57+
# If net_charge is a string path, load the JSON file
58+
if isinstance(net_charge, str):
59+
nc_path = Path(net_charge)
60+
if nc_path.is_file():
61+
with open(nc_path) as f:
62+
net_charge = json.load(f)
63+
64+
output_dir.mkdir(parents=True, exist_ok=True)
65+
66+
get_charges(
67+
path_to_cif=str(cif_path),
68+
output_path=str(output_dir),
69+
identifier=identifier,
70+
multiple_cifs=multiple_cifs,
71+
adjust_charge_method=adjust_charge_method,
72+
net_charge=net_charge,
73+
)
74+
75+
return PACMOF2Result(
76+
success=True,
77+
output_dir=str(output_dir),
78+
num_structures=n,
79+
error=None,
80+
)

src/matkit/types.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ class ZeoppResult(TypedDict):
5151
error: Optional[str]
5252

5353

54+
class PACMOF2Result(TypedDict):
55+
"""Return type for ``matkit.pacmof2.run_charge_prediction``."""
56+
57+
success: bool
58+
output_dir: str
59+
num_structures: int
60+
error: Optional[str]
61+
62+
5463
class UMABatchResult(TypedDict):
5564
"""Record type for ``matkit.mlip.run_opt_uma_batch`` results."""
5665

0 commit comments

Comments
 (0)