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.
@@ -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,5 @@
1
+ """Validation helpers for externally produced simulation-result evidence."""
2
+
3
+ from mattergraph_sim.parsers.envelope import parse_result_envelope
4
+
5
+ __all__ = ["parse_result_envelope"]
@@ -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"]
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any