Skip to content
Merged
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
4 changes: 4 additions & 0 deletions scripts/graspa/isotherm_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
cif_dir: ./cifs
outdir: ./batch_results

# mode: single — each adsorbate runs independently (separate sims)
# mode: mixture — all adsorbates run together in one sim (requires MolFraction)
mode: single

adsorbates: [CO2, N2]

temperatures: [283, 293, 313, 333]
Expand Down
53 changes: 46 additions & 7 deletions scripts/graspa/setup_isotherms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Usage:
python setup_isotherms.py <config.yaml>
python setup_isotherms.py # defaults to isotherm_config.yaml

Supports two modes via the 'mode' field in the YAML config:
- single: each adsorbate runs as independent single-component sims
- mixture: all adsorbates run together in one sim (requires MolFraction)
"""

import sys
Expand Down Expand Up @@ -38,30 +42,65 @@ def main():
factor = PRESSURE_TO_PA[pressure_unit]
pressures_pa = [p * factor for p in cfg["pressures"]]

adsorbates = [{"MoleculeName": name} for name in cfg["adsorbates"]]

mode = cfg.get("mode", "single")
print(f"Config: {config_path}")
print(f"Mode: {mode}")
print(f"CIF dir: {cfg['cif_dir']}")
print(f"Adsorbates: {cfg['adsorbates']}")
print(f"Temperatures: {cfg['temperatures']}")
print(
f"Pressures: {len(pressures_pa)} points "
f"({cfg['pressures'][0]}-{cfg['pressures'][-1]} "
f"{cfg.get('pressure_unit', 'Pa')})"
)

manifest = setup_batch(
common_kwargs = dict(
cif_dir=cfg["cif_dir"],
outpath=cfg["outdir"],
adsorbates=adsorbates,
temperatures=cfg["temperatures"],
pressures=pressures_pa,
cutoff=cfg.get("cutoff", 12.8),
n_cycle=cfg.get("cycles", 1000),
max_workers=cfg.get("max_workers"),
)

print(f"\nSet up {len(manifest)} simulations in {cfg['outdir']}")
if mode == "single":
print(f"Adsorbates: {cfg['adsorbates']} (each runs independently)")
total = 0
for ads_name in cfg["adsorbates"]:
ads_outdir = str(Path(cfg["outdir"]) / ads_name)
adsorbates = [{"MoleculeName": ads_name}]
manifest = setup_batch(
outpath=ads_outdir,
adsorbates=adsorbates,
template_dir=cfg.get("template_dir", "template"),
**common_kwargs,
)
print(f" {ads_name}: {len(manifest)} simulations in {ads_outdir}")
total += len(manifest)
print(f"\nTotal: {total} simulations")

elif mode == "mixture":
adsorbates = []
for ad in cfg["adsorbates"]:
entry = {"MoleculeName": ad["name"]}
for k, v in ad.items():
if k != "name":
entry[k] = v
adsorbates.append(entry)
names = [ad["MoleculeName"] for ad in adsorbates]
print(f"Adsorbates: {names} (mixture)")

template_dir = cfg.get("template_dir", "template_mixture_isotherm")
manifest = setup_batch(
outpath=cfg["outdir"],
adsorbates=adsorbates,
template_dir=template_dir,
**common_kwargs,
)
print(f"\nSet up {len(manifest)} simulations in {cfg['outdir']}")

else:
raise ValueError(f"Unknown mode '{mode}'. Use 'single' or 'mixture'.")

print(f"Manifest: {cfg['outdir']}/simulations.jsonl")


Expand Down
20 changes: 18 additions & 2 deletions src/matkit/cli.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import logging

import click
import json


@click.group()
def main():
@click.option(
"-v",
"--verbose",
count=True,
help="Increase verbosity (-v for info, -vv for debug).",
)
def main(verbose):
"""MatKit CLI: A modular toolkit for molecular simulations."""
pass
level = logging.WARNING
if verbose == 1:
level = logging.INFO
elif verbose >= 2:
level = logging.DEBUG
logging.basicConfig(
level=level,
format="%(name)s: %(message)s",
)


# ==========================================
Expand Down
56 changes: 24 additions & 32 deletions src/matkit/graspa/graspa.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from __future__ import annotations

import json
import shutil
from concurrent.futures import ThreadPoolExecutor
from itertools import product
from pathlib import Path
import shutil
from matkit.utils.unitcell_calculator import calculate_cell_size
from matkit.types import GRASPAResult

from ase.io import read as ase_read

from matkit.types import GRASPAResult
from matkit.utils.template import copy_template, render_template
from matkit.utils.unitcell_calculator import calculate_cell_size


def get_output_data(
output_path: str,
Expand Down Expand Up @@ -191,21 +194,15 @@ def setup_simulation(
FileNotFoundError: If the CIF file does not exist.
"""
outdir = Path(outpath)
outdir.mkdir(parents=True, exist_ok=True)

cifpath = Path(cif)
if not cifpath.exists():
raise FileNotFoundError(f"CIF file does not exist: {cif}")
cifname = cifpath.stem
shutil.copy(cifpath, outdir / f"{cifname}.cif")

# Copy template files
template_path = Path(__file__).parent / "files" / template_dir
for item in template_path.iterdir():
if item.is_dir():
shutil.copytree(item, outdir, dirs_exist_ok=True)
else:
shutil.copy2(item, outdir)
copy_template(template_path, outdir)
shutil.copy(cifpath, outdir / f"{cifname}.cif")

# Use pre-computed cell size or read CIF
if cell_size is not None:
Expand All @@ -214,31 +211,26 @@ def setup_simulation(
atoms = ase_read(cifpath)
uc_x, uc_y, uc_z = calculate_cell_size(atoms)

# Read template and replace placeholders
input_path = outdir / "simulation.input"
with input_path.open("r") as f:
template = f.read()

subs = {
"NCYCLE": str(n_cycle),
"TEMPERATURE": str(temperature),
"PRESSURE": str(pressure),
"CUTOFF": str(cutoff),
"CIFFILE": cifname,
"UC_X": str(uc_x),
"UC_Y": str(uc_y),
"UC_Z": str(uc_z),
}

for key, val in subs.items():
template = template.replace(key, val)
render_template(
input_path,
{
"NCYCLE": str(n_cycle),
"TEMPERATURE": str(temperature),
"PRESSURE": str(pressure),
"CUTOFF": str(cutoff),
"CIFFILE": cifname,
"UC_X": str(uc_x),
"UC_Y": str(uc_y),
"UC_Z": str(uc_z),
},
)

# Replace component block placeholder
component_block = generate_component_blocks(adsorbates)
template = template.replace("__COMPONENTS__", component_block)
# Write final input
with input_path.open("w") as f:
f.write(template)
content = input_path.read_text()
content = content.replace("__COMPONENTS__", component_block)
input_path.write_text(content)

return True

Expand Down
54 changes: 21 additions & 33 deletions src/matkit/graspa_sycl/graspa_sycl.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from pathlib import Path
import shutil
from matkit.utils.unitcell_calculator import calculate_cell_size
from pathlib import Path

from ase.io import read as ase_read

from matkit.utils.template import copy_template, render_template
from matkit.utils.unitcell_calculator import calculate_cell_size

_file_dir = Path(__file__).parent / "files" / "template"


Expand Down Expand Up @@ -33,45 +36,30 @@ def setup_simulation(
FileNotFoundError: If the CIF file does not exist.
"""
outdir = Path(outpath)
outdir.mkdir(parents=True, exist_ok=True)

cifpath = Path(cif)
if not cifpath.exists():
raise FileNotFoundError(f"CIF file does not exist: {cif}")

cifname = cifpath.stem
for item in _file_dir.iterdir():
if item.is_dir():
shutil.copytree(item, outdir, dirs_exist_ok=True)
else:
shutil.copy2(item, outdir)
copy_template(_file_dir, outdir)
shutil.copy(cif, outdir)
# Editing input file.

atoms = ase_read(cif)
[uc_x, uc_y, uc_z] = calculate_cell_size(atoms)

with (
open(f"{outdir}/simulation.input", "r") as f_in,
open(f"{outdir}/simulation.input.tmp", "w") as f_out,
):
for line in f_in:
if "NCYCLE" in line:
line = line.replace("NCYCLE", str(n_cycle))
if "ADSORBATE" in line:
line = line.replace("ADSORBATE", adsorbate)
if "TEMPERATURE" in line:
line = line.replace("TEMPERATURE", str(temperature))
if "PRESSURE" in line:
line = line.replace("PRESSURE", str(pressure))
if "UC_X UC_Y UC_Z" in line:
line = line.replace("UC_X UC_Y UC_Z", f"{uc_x} {uc_y} {uc_z}")
if "CUTOFF" in line:
line = line.replace("CUTOFF", str(cutoff))
if "CIFFILE" in line:
line = line.replace("CIFFILE", cifname)
f_out.write(line)

shutil.move(f"{outdir}/simulation.input.tmp", f"{outdir}/simulation.input")
uc_x, uc_y, uc_z = calculate_cell_size(atoms)

render_template(
outdir / "simulation.input",
{
"NCYCLE": str(n_cycle),
"ADSORBATE": adsorbate,
"TEMPERATURE": str(temperature),
"PRESSURE": str(pressure),
"UC_X UC_Y UC_Z": f"{uc_x} {uc_y} {uc_z}",
"CUTOFF": str(cutoff),
"CIFFILE": cifname,
},
)

return True

Expand Down
17 changes: 10 additions & 7 deletions src/matkit/mlip/mace_opt.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import logging
from pathlib import Path

from mace.calculators import mace_mp
from ase.constraints import ExpCellFilter
from ase.io import read as ase_read
from ase.io import write as ase_write
from ase.optimize import BFGS
from ase.constraints import ExpCellFilter
from mace.calculators import mace_mp

logger = logging.getLogger(__name__)


def run_opt_mace(
Expand Down Expand Up @@ -78,13 +81,13 @@ def run_opt_mace(
dyn.run(fmax=fmax, steps=steps)

else:
print(
(f"run_type {run_type} is not supported."),
("Options are 'geo_opt', cell_opt' and 'geo_opt_cell_opt'"),
raise ValueError(
f"run_type '{run_type}' is not supported. "
"Options are 'geo_opt', 'cell_opt', and 'geo_opt_cell_opt'."
)
return "Error"

ase_write(output_fname, atoms)

except Exception as e:
print(e)
logger.error("MACE optimization failed: %s", e)
raise
Loading
Loading