mattergraph-sim 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mattergraph_sim/__init__.py +61 -0
- mattergraph_sim/ase_runner.py +125 -0
- mattergraph_sim/job_spec.py +68 -0
- mattergraph_sim/lammps_runner.py +15 -0
- mattergraph_sim/parsers/__init__.py +5 -0
- mattergraph_sim/parsers/envelope.py +26 -0
- mattergraph_sim/py.typed +0 -0
- mattergraph_sim/qe_runner.py +15 -0
- mattergraph_sim-0.1.0.dist-info/METADATA +66 -0
- mattergraph_sim-0.1.0.dist-info/RECORD +11 -0
- mattergraph_sim-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from mattergraph_sim.ase_runner import ase_relax
|
|
8
|
+
from mattergraph_sim.job_spec import AseJobSpec, SimulationJob, SimulationResult
|
|
9
|
+
|
|
10
|
+
_EXPORTS: dict[str, tuple[str, str, str | None]] = {
|
|
11
|
+
"AseJobSpec": (
|
|
12
|
+
"mattergraph_sim.job_spec",
|
|
13
|
+
"AseJobSpec",
|
|
14
|
+
None,
|
|
15
|
+
),
|
|
16
|
+
"SimulationJob": (
|
|
17
|
+
"mattergraph_sim.job_spec",
|
|
18
|
+
"SimulationJob",
|
|
19
|
+
None,
|
|
20
|
+
),
|
|
21
|
+
"SimulationResult": (
|
|
22
|
+
"mattergraph_sim.job_spec",
|
|
23
|
+
"SimulationResult",
|
|
24
|
+
None,
|
|
25
|
+
),
|
|
26
|
+
"ase_relax": (
|
|
27
|
+
"mattergraph_sim.ase_runner",
|
|
28
|
+
"ase_relax",
|
|
29
|
+
(
|
|
30
|
+
"Install the optional `ase` dependency or run "
|
|
31
|
+
"`uv sync --all-packages --group dev` to use ase_relax."
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"AseJobSpec",
|
|
38
|
+
"SimulationJob",
|
|
39
|
+
"SimulationResult",
|
|
40
|
+
"ase_relax",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def __getattr__(name: str) -> Any:
|
|
45
|
+
if name not in _EXPORTS:
|
|
46
|
+
msg = f"module {__name__!r} has no attribute {name!r}"
|
|
47
|
+
raise AttributeError(msg)
|
|
48
|
+
module_name, attr_name, hint = _EXPORTS[name]
|
|
49
|
+
try:
|
|
50
|
+
module = import_module(module_name)
|
|
51
|
+
except ImportError as e:
|
|
52
|
+
if hint is None:
|
|
53
|
+
raise
|
|
54
|
+
raise ImportError(hint) from e
|
|
55
|
+
value = getattr(module, attr_name)
|
|
56
|
+
globals()[name] = value
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def __dir__() -> list[str]:
|
|
61
|
+
return sorted(set(globals()) | set(__all__))
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import io
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from mattergraph.schema.structure import CrystalStructure
|
|
9
|
+
|
|
10
|
+
from mattergraph_sim.job_spec import AseJobSpec, SimulationJob, SimulationResult
|
|
11
|
+
|
|
12
|
+
EMT_SUPPORTED_SPECIES = (
|
|
13
|
+
"Ag",
|
|
14
|
+
"Al",
|
|
15
|
+
"Au",
|
|
16
|
+
"C",
|
|
17
|
+
"Cu",
|
|
18
|
+
"H",
|
|
19
|
+
"N",
|
|
20
|
+
"Ni",
|
|
21
|
+
"O",
|
|
22
|
+
"Pd",
|
|
23
|
+
"Pt",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load_ase() -> tuple[Any, Any, Any]:
|
|
28
|
+
try:
|
|
29
|
+
from ase.calculators.emt import EMT
|
|
30
|
+
from ase.optimize import BFGS
|
|
31
|
+
from pymatgen.io.ase import AseAtomsAdaptor
|
|
32
|
+
except ImportError as e:
|
|
33
|
+
msg = "Install the optional `ase` dependency to run local ASE relaxations."
|
|
34
|
+
raise ImportError(msg) from e
|
|
35
|
+
return EMT, BFGS, AseAtomsAdaptor
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _failure(job: SimulationJob, error: str) -> SimulationJob:
|
|
39
|
+
return job.model_copy(
|
|
40
|
+
update={
|
|
41
|
+
"status": "failed",
|
|
42
|
+
"error": error,
|
|
43
|
+
"log": error,
|
|
44
|
+
"result": None,
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _supported_species_for(calc_name: str) -> tuple[str, ...]:
|
|
50
|
+
if calc_name == "emt":
|
|
51
|
+
return EMT_SUPPORTED_SPECIES
|
|
52
|
+
return ()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _unsupported_species(structure: Any, calc_name: str) -> list[str]:
|
|
56
|
+
supported = set(_supported_species_for(calc_name))
|
|
57
|
+
symbols = set(structure.composition.element_composition.as_dict())
|
|
58
|
+
return sorted(symbols - supported)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ase_relax(job: SimulationJob) -> SimulationJob:
|
|
62
|
+
"""
|
|
63
|
+
Local relaxation using ASE + EMT (MVP). Swap calculators for your own backend as needed.
|
|
64
|
+
"""
|
|
65
|
+
EMT, BFGS, _ = _load_ase()
|
|
66
|
+
running = job.model_copy(update={"status": "running", "error": None})
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
spec: AseJobSpec = running.spec
|
|
70
|
+
if running.kind != "relax":
|
|
71
|
+
return _failure(running, f"ASE runner only supports kind='relax'; got {running.kind!r}")
|
|
72
|
+
|
|
73
|
+
structure = CrystalStructure.model_validate(running.input_structure).to_pymatgen()
|
|
74
|
+
unsupported = _unsupported_species(structure, spec.calc_name)
|
|
75
|
+
if unsupported:
|
|
76
|
+
supported = ", ".join(_supported_species_for(spec.calc_name))
|
|
77
|
+
bad = ", ".join(unsupported)
|
|
78
|
+
return _failure(
|
|
79
|
+
running,
|
|
80
|
+
f"ASE {spec.calc_name} does not support species: {bad}. Supported species: {supported}.",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
_, _, AseAtomsAdaptor = _load_ase()
|
|
84
|
+
atoms = AseAtomsAdaptor.get_atoms(structure) # type: ignore[assignment]
|
|
85
|
+
atoms.calc = EMT()
|
|
86
|
+
|
|
87
|
+
optimizer_log = io.StringIO()
|
|
88
|
+
with contextlib.redirect_stdout(optimizer_log), contextlib.redirect_stderr(optimizer_log):
|
|
89
|
+
opt = BFGS(atoms, logfile=optimizer_log)
|
|
90
|
+
converged = bool(opt.run(fmax=spec.fmax, steps=spec.max_steps))
|
|
91
|
+
|
|
92
|
+
forces = atoms.get_forces() if atoms.calc is not None else None
|
|
93
|
+
energy = float(atoms.get_potential_energy()) if atoms.calc is not None else None
|
|
94
|
+
max_force = None
|
|
95
|
+
if forces is not None and len(forces) > 0:
|
|
96
|
+
max_force = float(np.linalg.norm(forces, axis=1).max())
|
|
97
|
+
|
|
98
|
+
relaxed_structure = CrystalStructure.from_pymatgen(AseAtomsAdaptor.get_structure(atoms))
|
|
99
|
+
steps = int(getattr(opt, "nsteps", 0))
|
|
100
|
+
summary = (
|
|
101
|
+
f"calculator={spec.calc_name} energy={energy!s} max_force={max_force!s} "
|
|
102
|
+
f"steps={steps} converged={converged}"
|
|
103
|
+
)
|
|
104
|
+
raw_log = optimizer_log.getvalue().strip()
|
|
105
|
+
log = summary if not raw_log else f"{summary}\n{raw_log}"
|
|
106
|
+
|
|
107
|
+
return running.model_copy(
|
|
108
|
+
update={
|
|
109
|
+
"status": "completed",
|
|
110
|
+
"log": log,
|
|
111
|
+
"result": SimulationResult(
|
|
112
|
+
engine=spec.engine,
|
|
113
|
+
calculator=spec.calc_name,
|
|
114
|
+
converged=converged,
|
|
115
|
+
steps=steps,
|
|
116
|
+
energy=energy,
|
|
117
|
+
max_force=max_force,
|
|
118
|
+
relaxed_structure=relaxed_structure,
|
|
119
|
+
),
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
except ImportError:
|
|
123
|
+
raise
|
|
124
|
+
except Exception as e: # noqa: BLE001
|
|
125
|
+
return _failure(running, f"{type(e).__name__}: {e}")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from mattergraph.schema.structure import CrystalStructure
|
|
7
|
+
from pydantic import BaseModel, Field, field_validator
|
|
8
|
+
|
|
9
|
+
_SUPPORTED_ASE_CALCULATORS = {"emt"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AseJobSpec(BaseModel):
|
|
13
|
+
engine: Literal["ase"] = "ase"
|
|
14
|
+
calc_name: str = "emt"
|
|
15
|
+
fmax: float = 0.05
|
|
16
|
+
max_steps: int = 200
|
|
17
|
+
n_cores: int = 1
|
|
18
|
+
|
|
19
|
+
@field_validator("calc_name")
|
|
20
|
+
@classmethod
|
|
21
|
+
def _normalize_calc_name(cls, value: str) -> str:
|
|
22
|
+
calc_name = value.strip().lower()
|
|
23
|
+
if calc_name not in _SUPPORTED_ASE_CALCULATORS:
|
|
24
|
+
supported = ", ".join(sorted(_SUPPORTED_ASE_CALCULATORS))
|
|
25
|
+
msg = f"unsupported ASE calculator {calc_name!r}; supported calculators: {supported}"
|
|
26
|
+
raise ValueError(msg)
|
|
27
|
+
return calc_name
|
|
28
|
+
|
|
29
|
+
@field_validator("fmax")
|
|
30
|
+
@classmethod
|
|
31
|
+
def _positive_fmax(cls, value: float) -> float:
|
|
32
|
+
if value <= 0:
|
|
33
|
+
msg = "fmax must be positive"
|
|
34
|
+
raise ValueError(msg)
|
|
35
|
+
return value
|
|
36
|
+
|
|
37
|
+
@field_validator("max_steps", "n_cores")
|
|
38
|
+
@classmethod
|
|
39
|
+
def _positive_ints(cls, value: int, info: Any) -> int:
|
|
40
|
+
if value < 1:
|
|
41
|
+
msg = f"{info.field_name} must be at least 1"
|
|
42
|
+
raise ValueError(msg)
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SimulationResult(BaseModel):
|
|
47
|
+
engine: str
|
|
48
|
+
calculator: str
|
|
49
|
+
converged: bool | None = None
|
|
50
|
+
steps: int | None = None
|
|
51
|
+
energy: float | None = None
|
|
52
|
+
max_force: float | None = None
|
|
53
|
+
relaxed_structure: CrystalStructure | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SimulationJob(BaseModel):
|
|
57
|
+
job_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
58
|
+
spec: AseJobSpec
|
|
59
|
+
input_structure: dict[str, Any] = Field(
|
|
60
|
+
default_factory=dict,
|
|
61
|
+
description="Serialized :class:`CrystalStructure` or pymatgen dict",
|
|
62
|
+
)
|
|
63
|
+
kind: Literal["relax", "sc", "md"] = "relax"
|
|
64
|
+
status: Literal["pending", "running", "completed", "failed"] = "pending"
|
|
65
|
+
result_uri: str | None = None
|
|
66
|
+
result: SimulationResult | None = None
|
|
67
|
+
error: str | None = None
|
|
68
|
+
log: str | None = None
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from mattergraph_sim.job_spec import SimulationJob
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_lammps(_job: SimulationJob) -> SimulationJob:
|
|
7
|
+
"""Stub: LAMMPS integration is environment-specific. Implement where binaries exist."""
|
|
8
|
+
return _job.model_copy(
|
|
9
|
+
update={
|
|
10
|
+
"status": "failed",
|
|
11
|
+
"error": "LAMMPS runner not installed (stub).",
|
|
12
|
+
"log": "LAMMPS runner not installed (stub).",
|
|
13
|
+
"result": None,
|
|
14
|
+
}
|
|
15
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from mattergraph import SimulationResultEnvelope
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_result_envelope(
|
|
11
|
+
payload: str | bytes | Mapping[str, Any],
|
|
12
|
+
) -> SimulationResultEnvelope:
|
|
13
|
+
"""Validate an external JSON result envelope without launching an engine."""
|
|
14
|
+
if isinstance(payload, bytes):
|
|
15
|
+
payload = payload.decode("utf-8")
|
|
16
|
+
if isinstance(payload, str):
|
|
17
|
+
value = json.loads(payload)
|
|
18
|
+
else:
|
|
19
|
+
value = dict(payload)
|
|
20
|
+
if not isinstance(value, dict):
|
|
21
|
+
msg = "simulation result envelope must be a JSON object"
|
|
22
|
+
raise ValueError(msg)
|
|
23
|
+
return SimulationResultEnvelope.model_validate(value)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["parse_result_envelope"]
|
mattergraph_sim/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from mattergraph_sim.job_spec import SimulationJob
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_quantum_espresso(_job: SimulationJob) -> SimulationJob:
|
|
7
|
+
"""Stub: QE paths and pseudos are site-specific. Wire up in your environment."""
|
|
8
|
+
return _job.model_copy(
|
|
9
|
+
update={
|
|
10
|
+
"status": "failed",
|
|
11
|
+
"error": "QE runner not installed (stub).",
|
|
12
|
+
"log": "QE runner not installed (stub).",
|
|
13
|
+
"result": None,
|
|
14
|
+
}
|
|
15
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mattergraph-sim
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ASE/LAMMPS/QE simulation job specs and runners (MVP wrappers).
|
|
5
|
+
Project-URL: Homepage, https://github.com/cyrusmo/MatterGraph
|
|
6
|
+
Project-URL: Repository, https://github.com/cyrusmo/MatterGraph
|
|
7
|
+
Project-URL: Issues, https://github.com/cyrusmo/MatterGraph/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/cyrusmo/MatterGraph/blob/main/CHANGELOG.md
|
|
9
|
+
Author: MatterGraph contributors
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Keywords: ase,lammps,materials-science,quantum-espresso,simulation
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: ase>=3.22
|
|
23
|
+
Requires-Dist: mattergraph-core~=0.1.0
|
|
24
|
+
Requires-Dist: pydantic>=2.5
|
|
25
|
+
Requires-Dist: pymatgen>=2024.1.1
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# mattergraph-sim
|
|
29
|
+
|
|
30
|
+
Simulation job specs and runners for [MatterGraph](https://github.com/cyrusmo/MatterGraph).
|
|
31
|
+
|
|
32
|
+
Structures round-trip through pymatgen, jobs are declared as validated Pydantic specs, and runners return a structured `SimulationJob` with status, log, and result rather than raising — so a failed relaxation is data, not an exception.
|
|
33
|
+
|
|
34
|
+
## Engines
|
|
35
|
+
|
|
36
|
+
| Engine | Status |
|
|
37
|
+
|---|---|
|
|
38
|
+
| ASE | Working. Local relaxation via the EMT empirical potential (11 elements: Ag, Al, Au, C, Cu, H, N, Ni, O, Pd, Pt). |
|
|
39
|
+
| LAMMPS | Stub — environment-specific, returns a structured failure |
|
|
40
|
+
| Quantum ESPRESSO | Stub — site-specific paths and pseudopotentials, returns a structured failure |
|
|
41
|
+
|
|
42
|
+
> **Note on scope.** EMT is a fast empirical potential with narrow element coverage; it is suitable for smoke-testing a workflow end to end, not for producing quantitative results. Universal ML interatomic potentials are the intended path to periodic-table-wide coverage.
|
|
43
|
+
|
|
44
|
+
Externally produced results can be validated with
|
|
45
|
+
`mattergraph_sim.parsers.parse_result_envelope`. This is result interchange only; it does not
|
|
46
|
+
launch, route, or coordinate simulation engines.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install mattergraph-sim
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Example
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from mattergraph_sim import AseJobSpec, SimulationJob, ase_relax
|
|
58
|
+
|
|
59
|
+
job = SimulationJob(spec=AseJobSpec(fmax=0.05, max_steps=200), input_structure=structure.model_dump())
|
|
60
|
+
done = ase_relax(job)
|
|
61
|
+
print(done.status, done.result.energy if done.result else done.error)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
Apache-2.0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
mattergraph_sim/__init__.py,sha256=YLiZiLngYEZk8WvrTNY1kGnaQ4t9NCwS5zkjiC03KlI,1374
|
|
2
|
+
mattergraph_sim/ase_runner.py,sha256=4gqh_xRTV9UpyC2SlU6R2rV3xHhnHC0yUkZMYfS7N6I,3709
|
|
3
|
+
mattergraph_sim/job_spec.py,sha256=lSlDz_Tc-4mUwsD4v7XFJ39aqFTCX1JZyd_01inJ8ro,1957
|
|
4
|
+
mattergraph_sim/lammps_runner.py,sha256=mWS0tSQjK9wgHBtjNSioaatiH8bMxxpAgqd68Foe6jk,436
|
|
5
|
+
mattergraph_sim/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
mattergraph_sim/qe_runner.py,sha256=a0vcuDVjpSRLJb9HZWWTwcBMSlR9S0ihuNISZkGDQqw,431
|
|
7
|
+
mattergraph_sim/parsers/__init__.py,sha256=ExJnEtf6qEOuT5YnbFxACD5JpFMcmJYr7IB-0chwcYo,182
|
|
8
|
+
mattergraph_sim/parsers/envelope.py,sha256=3gv5x6hXMLM3k8jEqBfKw5r11XtrRaYf288XKXARUOA,723
|
|
9
|
+
mattergraph_sim-0.1.0.dist-info/METADATA,sha256=9DexeHxndclLanAWhnuVHPXAGdIeWQBuOYPKhEU9bz0,2711
|
|
10
|
+
mattergraph_sim-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
11
|
+
mattergraph_sim-0.1.0.dist-info/RECORD,,
|