mattergraph-api 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,201 @@
1
+ from __future__ import annotations
2
+
3
+ import gc
4
+ import threading
5
+ from collections import OrderedDict, deque
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from mattergraph import DatasetManifest, MaterialStore
10
+
11
+ MAX_DATASETS = 8
12
+ MAX_NORMALIZED_BYTES = 32 * 1024 * 1024
13
+
14
+
15
+ class DatasetRegistryError(RuntimeError):
16
+ pass
17
+
18
+
19
+ class DatasetNotFoundError(DatasetRegistryError):
20
+ def __init__(self, dataset_id: str, *, evicted: bool = False) -> None:
21
+ self.dataset_id = dataset_id
22
+ self.evicted = evicted
23
+ label = "evicted" if evicted else "unknown"
24
+ super().__init__(f"{label} dataset {dataset_id!r}")
25
+
26
+
27
+ class DatasetBusyError(DatasetRegistryError):
28
+ pass
29
+
30
+
31
+ class DatasetCapacityError(DatasetRegistryError):
32
+ pass
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class RegistryEntry:
37
+ manifest: DatasetManifest
38
+ payload: bytes
39
+
40
+
41
+ class DatasetRegistry:
42
+ """Byte-budgeted LRU registry with one lazily materialized store."""
43
+
44
+ def __init__(
45
+ self,
46
+ *,
47
+ max_entries: int = MAX_DATASETS,
48
+ max_bytes: int = MAX_NORMALIZED_BYTES,
49
+ ) -> None:
50
+ self.max_entries = max_entries
51
+ self.max_bytes = max_bytes
52
+ self._entries: OrderedDict[str, RegistryEntry] = OrderedDict()
53
+ self._lock = threading.RLock()
54
+ self._active_dataset_id: str | None = None
55
+ self._active_store: MaterialStore | None = None
56
+ self._evicted_ids: deque[str] = deque(maxlen=64)
57
+
58
+ @property
59
+ def total_bytes(self) -> int:
60
+ with self._lock:
61
+ return sum(len(entry.payload) for entry in self._entries.values())
62
+
63
+ @property
64
+ def active_dataset_id(self) -> str | None:
65
+ with self._lock:
66
+ return self._active_dataset_id
67
+
68
+ def register(self, manifest: DatasetManifest, payload: bytes) -> dict[str, Any]:
69
+ if len(payload) != manifest.normalized_bytes:
70
+ msg = "manifest normalized_bytes does not match the payload"
71
+ raise ValueError(msg)
72
+ if len(payload) > self.max_bytes:
73
+ msg = f"normalized payload exceeds registry byte budget of {self.max_bytes}"
74
+ raise DatasetCapacityError(msg)
75
+ with self._lock:
76
+ prior = self._entries.pop(manifest.dataset_id, None)
77
+ if prior is not None and self._active_dataset_id == manifest.dataset_id:
78
+ self._release_active_locked()
79
+ self._entries[manifest.dataset_id] = RegistryEntry(manifest=manifest, payload=payload)
80
+ evicted = self._evict_to_budget_locked(protected_id=manifest.dataset_id)
81
+ return {
82
+ "manifest": manifest.model_dump(mode="json"),
83
+ "evicted_dataset_ids": evicted,
84
+ "registry": self.stats(),
85
+ }
86
+
87
+ def list(self) -> list[dict[str, Any]]:
88
+ with self._lock:
89
+ return [
90
+ self._entry_status(dataset_id, entry)
91
+ for dataset_id, entry in reversed(self._entries.items())
92
+ ]
93
+
94
+ def get(self, dataset_id: str) -> RegistryEntry:
95
+ with self._lock:
96
+ entry = self._entry_locked(dataset_id)
97
+ self._entries.move_to_end(dataset_id)
98
+ return entry
99
+
100
+ def status(self, dataset_id: str) -> dict[str, Any]:
101
+ with self._lock:
102
+ entry = self._entry_locked(dataset_id)
103
+ self._entries.move_to_end(dataset_id)
104
+ return self._entry_status(dataset_id, entry)
105
+
106
+ def materialize(self, dataset_id: str) -> MaterialStore:
107
+ with self._lock:
108
+ entry = self._entry_locked(dataset_id)
109
+ self._entries.move_to_end(dataset_id)
110
+ if self._active_dataset_id == dataset_id and self._active_store is not None:
111
+ return self._active_store
112
+
113
+ # Enforce the single-store invariant before parsing the next payload.
114
+ self._release_active_locked()
115
+ gc.collect()
116
+ try:
117
+ store = MaterialStore.from_jsonl_text(
118
+ entry.payload.decode("utf-8"),
119
+ max_rows=entry.manifest.record_count,
120
+ )
121
+ except Exception:
122
+ self._active_dataset_id = None
123
+ self._active_store = None
124
+ raise
125
+ self._active_dataset_id = dataset_id
126
+ self._active_store = store
127
+ return store
128
+
129
+ def export(self, dataset_id: str) -> tuple[DatasetManifest, bytes]:
130
+ entry = self.get(dataset_id)
131
+ return entry.manifest, entry.payload
132
+
133
+ def delete(self, dataset_id: str) -> DatasetManifest:
134
+ if not self._lock.acquire(blocking=False):
135
+ msg = f"dataset {dataset_id!r} is currently being replaced or deleted"
136
+ raise DatasetBusyError(msg)
137
+ try:
138
+ entry = self._entry_locked(dataset_id)
139
+ if self._active_dataset_id == dataset_id:
140
+ self._release_active_locked()
141
+ del self._entries[dataset_id]
142
+ return entry.manifest
143
+ finally:
144
+ self._lock.release()
145
+
146
+ def clear(self) -> None:
147
+ with self._lock:
148
+ self._release_active_locked()
149
+ self._entries.clear()
150
+ self._evicted_ids.clear()
151
+
152
+ def stats(self) -> dict[str, int | str | None]:
153
+ with self._lock:
154
+ return {
155
+ "entry_count": len(self._entries),
156
+ "normalized_bytes": sum(len(entry.payload) for entry in self._entries.values()),
157
+ "max_entries": self.max_entries,
158
+ "max_normalized_bytes": self.max_bytes,
159
+ "active_dataset_id": self._active_dataset_id,
160
+ "eviction_policy": "weighted_lru",
161
+ }
162
+
163
+ def _entry_locked(self, dataset_id: str) -> RegistryEntry:
164
+ entry = self._entries.get(dataset_id)
165
+ if entry is None:
166
+ raise DatasetNotFoundError(dataset_id, evicted=dataset_id in self._evicted_ids)
167
+ return entry
168
+
169
+ def _entry_status(self, dataset_id: str, entry: RegistryEntry) -> dict[str, Any]:
170
+ return {
171
+ "manifest": entry.manifest.model_dump(mode="json"),
172
+ "readiness": "ready",
173
+ "normalized_bytes": len(entry.payload),
174
+ "materialized": dataset_id == self._active_dataset_id and self._active_store is not None,
175
+ "eviction": {
176
+ "policy": "least_recently_used",
177
+ "entry_limit": self.max_entries,
178
+ "byte_limit": self.max_bytes,
179
+ },
180
+ }
181
+
182
+ def _evict_to_budget_locked(self, *, protected_id: str) -> list[str]:
183
+ evicted: list[str] = []
184
+ while len(self._entries) > self.max_entries or self.total_bytes > self.max_bytes:
185
+ victim = next((key for key in self._entries if key != protected_id), None)
186
+ if victim is None:
187
+ msg = "registry limits cannot accommodate the normalized dataset"
188
+ raise DatasetCapacityError(msg)
189
+ if victim == self._active_dataset_id:
190
+ self._release_active_locked()
191
+ del self._entries[victim]
192
+ self._evicted_ids.append(victim)
193
+ evicted.append(victim)
194
+ return evicted
195
+
196
+ def _release_active_locked(self) -> None:
197
+ self._active_store = None
198
+ self._active_dataset_id = None
199
+
200
+
201
+ dataset_registry = DatasetRegistry()
@@ -0,0 +1,482 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import json
5
+ from functools import lru_cache
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from mattergraph import Material, MaterialStore, Scorecard
10
+ from mattergraph.datasets import MatterGraphDataset
11
+ from mattergraph.graph import CrystalGraphBuilder
12
+ from mattergraph_connectors import LeMatBulk
13
+ from mattergraph_sim.ase_runner import EMT_SUPPORTED_SPECIES
14
+
15
+ FIXTURE_RELATIVE_PATH = "data/demo/spc_real_snapshot.json"
16
+ CHGNET_REFERENCE_RELATIVE_PATH = "data/demo/chgnet_reference.json"
17
+ SOURCE_DATASET = "LeMaterial/LeMat-Bulk"
18
+ SOURCE_SUBSET = "compatible_pbe"
19
+ SLICE_NAME = "spc_tialn_candidates_v1"
20
+ TARGET = "energy_above_hull"
21
+ WORKFLOW_VERSION = "v1.0"
22
+ RUN_ID = "spc_evidence_first_snapshot_v1"
23
+ FIXTURE_DISCLAIMER = (
24
+ "A checksummed 24-record offline snapshot of real public records. "
25
+ "It demonstrates a reproducible workflow, not the scale of the 5.34M-row source dataset."
26
+ )
27
+ ML_BOUNDARY = (
28
+ "CHGNet relaxation is ML-based proposal support, not a DFT or experimental measurement."
29
+ )
30
+
31
+ DEFAULT_OBJECTIVES: dict[str, dict[str, float | str]] = {
32
+ "density": {"direction": "minimize", "weight": 0.6},
33
+ "energy_above_hull": {"direction": "minimize", "weight": 0.4},
34
+ }
35
+ DEFAULT_CONSTRAINTS: dict[str, dict[str, float]] = {
36
+ "energy_above_hull": {"max": 0.05},
37
+ "max_force": {"max": 0.2},
38
+ }
39
+
40
+
41
+ def repo_root() -> Path:
42
+ return Path(__file__).resolve().parents[4]
43
+
44
+
45
+ def fixture_path() -> Path:
46
+ return repo_root() / FIXTURE_RELATIVE_PATH
47
+
48
+
49
+ def chgnet_reference_path() -> Path:
50
+ return repo_root() / CHGNET_REFERENCE_RELATIVE_PATH
51
+
52
+
53
+ @lru_cache(maxsize=1)
54
+ def get_demo_artifact() -> dict[str, Any]:
55
+ return json.loads(fixture_path().read_text())
56
+
57
+
58
+ def get_demo_manifest() -> dict[str, Any]:
59
+ return dict(get_demo_artifact()["manifest"])
60
+
61
+
62
+ @lru_cache(maxsize=1)
63
+ def get_demo_dataset() -> MatterGraphDataset:
64
+ artifact = get_demo_artifact()
65
+ dataset = LeMatBulk.from_records(
66
+ artifact["records"],
67
+ source_dataset=SOURCE_DATASET,
68
+ subset=SOURCE_SUBSET,
69
+ )
70
+ dataset.metadata["snapshot_manifest"] = artifact["manifest"]
71
+ return dataset
72
+
73
+
74
+ def get_filtered_demo_dataset() -> MatterGraphDataset:
75
+ return (
76
+ get_demo_dataset()
77
+ .filter_elements(include=["Ti", "Al", "N"])
78
+ .filter_complexity(
79
+ max_nsites=16,
80
+ max_nelements=3,
81
+ )
82
+ )
83
+
84
+
85
+ @lru_cache(maxsize=1)
86
+ def get_demo_store() -> MaterialStore:
87
+ return get_demo_dataset().to_material_store()
88
+
89
+
90
+ def get_default_scorecard() -> Scorecard:
91
+ return Scorecard(
92
+ objectives=DEFAULT_OBJECTIVES, # type: ignore[arg-type]
93
+ constraints=DEFAULT_CONSTRAINTS,
94
+ )
95
+
96
+
97
+ def get_default_material_id() -> str:
98
+ ranked = get_default_scorecard().rank(get_demo_store().materials)
99
+ if ranked.empty:
100
+ return get_demo_store().materials[0].material_id
101
+ return str(ranked.iloc[0]["material_id"])
102
+
103
+
104
+ def graph_summary(material: Material, *, max_edges: int = 256) -> dict[str, Any]:
105
+ if material.structure is None:
106
+ msg = "material structure missing; graph export excluded this record"
107
+ raise ValueError(msg)
108
+
109
+ builder = CrystalGraphBuilder(cutoff_radius=5.0, max_neighbors=12)
110
+ graph = builder.build(material.structure)
111
+ edge_count = int(graph.edge_index.shape[1])
112
+ kept_edges = min(edge_count, max(0, min(max_edges, 256)))
113
+ edges = []
114
+ for index in range(kept_edges):
115
+ source = int(graph.edge_index[0, index])
116
+ target = int(graph.edge_index[1, index])
117
+ source_cartesian = graph.cartesian_coordinates[source]
118
+ displacement = graph.displacement_vectors[index]
119
+ target_cartesian = source_cartesian + displacement
120
+ edges.append(
121
+ {
122
+ "source": source,
123
+ "target": target,
124
+ "distance": float(graph.edge_features[index, 0]),
125
+ "image": [int(value) for value in graph.image_offsets[index].tolist()],
126
+ "source_cartesian": [float(value) for value in source_cartesian],
127
+ "target_cartesian": [float(value) for value in target_cartesian],
128
+ "displacement_cartesian": [float(value) for value in displacement],
129
+ }
130
+ )
131
+
132
+ all_edges = [
133
+ (
134
+ int(graph.edge_index[0, index]),
135
+ int(graph.edge_index[1, index]),
136
+ tuple(int(value) for value in graph.image_offsets[index]),
137
+ float(graph.edge_features[index, 0]),
138
+ )
139
+ for index in range(edge_count)
140
+ ]
141
+ edge_keys = {(source, target, image) for source, target, image, _distance in all_edges}
142
+ reciprocal = all(
143
+ (target, source, tuple(-value for value in image)) in edge_keys
144
+ for source, target, image, _distance in all_edges
145
+ )
146
+ zero_distance_count = sum(distance <= 1e-8 for *_edge, distance in all_edges)
147
+ displacement_consistent = all(
148
+ abs(float(graph.edge_features[index, 0]) - float(_norm(graph.displacement_vectors[index])))
149
+ <= 1e-8
150
+ for index in range(edge_count)
151
+ )
152
+ coordination_numbers = _coordination_numbers(all_edges, graph.num_atoms)
153
+ warnings = []
154
+ if not reciprocal:
155
+ warnings.append("graph is not reciprocal")
156
+ if zero_distance_count:
157
+ warnings.append(f"{zero_distance_count} zero-distance edges")
158
+ if graph.info["truncated_sources"]:
159
+ warnings.append("one or more neighbor lists were truncated after a complete distance shell")
160
+ if kept_edges < edge_count:
161
+ warnings.append("rendering geometry is capped at 256 edges")
162
+
163
+ return {
164
+ "material_id": material.material_id,
165
+ "formula": material.formula,
166
+ "nodes": [
167
+ {
168
+ "index": index,
169
+ "species": species,
170
+ "fractional_coordinates": [float(value) for value in coords],
171
+ "cartesian_coordinates": [
172
+ float(value) for value in graph.cartesian_coordinates[index].tolist()
173
+ ],
174
+ }
175
+ for index, (species, coords) in enumerate(
176
+ zip(material.structure.species, material.structure.coords, strict=True)
177
+ )
178
+ ],
179
+ "edges": edges,
180
+ "edge_count": edge_count,
181
+ "edges_truncated": kept_edges < edge_count,
182
+ "lattice_vectors": [
183
+ [float(value) for value in vector] for vector in graph.cell.tolist()
184
+ ] if graph.cell is not None else [],
185
+ "distance_shells": _distance_shells(all_edges),
186
+ "coordination_numbers": coordination_numbers,
187
+ "node_feature_shape": [int(value) for value in graph.node_features.shape],
188
+ "edge_feature_shape": [int(value) for value in graph.edge_features.shape],
189
+ "global_features": graph.global_features,
190
+ "builder": graph.info,
191
+ "validation": {
192
+ "state": (
193
+ "valid"
194
+ if reciprocal and not zero_distance_count and displacement_consistent
195
+ else "invalid"
196
+ ),
197
+ "ordered_structure": True,
198
+ "reciprocal": reciprocal,
199
+ "zero_distance_edges": zero_distance_count,
200
+ "displacement_consistent": displacement_consistent,
201
+ "complete_tied_shells": True,
202
+ "symmetry": {
203
+ "status": "determined" if graph.global_features["spacegroup_number"] else "unknown",
204
+ "spacegroup_number": graph.global_features["spacegroup_number"],
205
+ },
206
+ "truncated": bool(graph.info["truncated_sources"]),
207
+ "warnings": warnings,
208
+ },
209
+ }
210
+
211
+
212
+ def _norm(vector: Any) -> float:
213
+ return sum(float(value) ** 2 for value in vector) ** 0.5
214
+
215
+
216
+ def _coordination_numbers(
217
+ edges: list[tuple[int, int, tuple[int, int, int], float]],
218
+ atom_count: int,
219
+ ) -> list[int]:
220
+ coordination: list[int] = []
221
+ for atom in range(atom_count):
222
+ distances = [distance for source, _target, _image, distance in edges if source == atom]
223
+ if not distances:
224
+ coordination.append(0)
225
+ continue
226
+ first = min(distances)
227
+ coordination.append(sum(distance <= first + 0.1 for distance in distances))
228
+ return coordination
229
+
230
+
231
+ def _distance_shells(
232
+ edges: list[tuple[int, int, tuple[int, int, int], float]],
233
+ ) -> list[dict[str, float | int]]:
234
+ distances = sorted(distance for _source, _target, _image, distance in edges)
235
+ shells: list[list[float]] = []
236
+ for distance in distances:
237
+ if not shells or distance - shells[-1][-1] > 0.1:
238
+ shells.append([distance])
239
+ else:
240
+ shells[-1].append(distance)
241
+ return [
242
+ {
243
+ "index": index + 1,
244
+ "distance": sum(shell) / len(shell),
245
+ "directed_edge_count": len(shell),
246
+ }
247
+ for index, shell in enumerate(shells)
248
+ ]
249
+
250
+
251
+ def simulation_readiness(material: Material) -> dict[str, Any]:
252
+ ase_available = importlib.util.find_spec("ase") is not None
253
+ unsupported = sorted(set(material.elements) - set(EMT_SUPPORTED_SPECIES))
254
+ structure_present = material.structure is not None
255
+ ready = ase_available and structure_present and not unsupported
256
+ if not ase_available:
257
+ reason = "ASE is not installed in this environment."
258
+ elif not structure_present:
259
+ reason = "No crystal structure is available for relaxation."
260
+ elif unsupported:
261
+ reason = f"EMT does not support: {', '.join(unsupported)}."
262
+ else:
263
+ reason = "ASE/EMT supports every species in this structure."
264
+ return {
265
+ "ready": ready,
266
+ "ase_available": ase_available,
267
+ "calculator": "emt",
268
+ "unsupported_species": unsupported,
269
+ "reason": reason,
270
+ }
271
+
272
+
273
+ @lru_cache(maxsize=1)
274
+ def get_chgnet_reference_artifact() -> dict[str, Any] | None:
275
+ path = chgnet_reference_path()
276
+ if not path.is_file():
277
+ return None
278
+ artifact = json.loads(path.read_text())
279
+ artifact["scientific_boundary"] = ML_BOUNDARY
280
+ return artifact
281
+
282
+
283
+ def chgnet_state() -> dict[str, Any]:
284
+ reference = get_chgnet_reference_artifact()
285
+ if reference is None:
286
+ return {
287
+ "state": "unavailable",
288
+ "live_available": False,
289
+ "reference_available": False,
290
+ "detail": "No verified local CHGNet artifact is bundled.",
291
+ "scientific_boundary": ML_BOUNDARY,
292
+ }
293
+ return {
294
+ "state": "cached_only",
295
+ "live_available": False,
296
+ "reference_available": True,
297
+ "reference_material_id": reference["material_id"],
298
+ "model_version": reference["model"]["version"],
299
+ "detail": "A versioned cached reference is available; live execution is not enabled.",
300
+ "scientific_boundary": ML_BOUNDARY,
301
+ }
302
+
303
+
304
+ def capability_catalog() -> list[dict[str, Any]]:
305
+ return [
306
+ _cap(
307
+ "lematerial",
308
+ "LeMaterial adapter",
309
+ "workflow",
310
+ "demo_ready",
311
+ "/workflows/lematerial/demo",
312
+ ),
313
+ _cap(
314
+ "candidate_slices",
315
+ "Candidate slicing + guardrails",
316
+ "workflow",
317
+ "demo_ready",
318
+ "CandidateSlice.report",
319
+ ),
320
+ _cap(
321
+ "crystal_graphs",
322
+ "Crystal graph export",
323
+ "graphs",
324
+ "demo_ready",
325
+ "/materials/{id}/graph-summary",
326
+ ),
327
+ _cap(
328
+ "benchmark_frames",
329
+ "Benchmark-ready frames",
330
+ "benchmarks",
331
+ "demo_ready",
332
+ "MatterGraphDataset.to_benchmark_frame",
333
+ ),
334
+ _cap(
335
+ "scorecard_audit",
336
+ "Transparent scorecard audit",
337
+ "ranking",
338
+ "demo_ready",
339
+ "/scores/rank/audit",
340
+ ),
341
+ _cap(
342
+ "local_workbench",
343
+ "Ephemeral local contributor workbench",
344
+ "workflow",
345
+ "demo_ready",
346
+ "/datasets/inspect + /datasets/import",
347
+ boundary="Local, unauthenticated, in-memory, and limited to small exploratory datasets.",
348
+ ),
349
+ _cap(
350
+ "chgnet_reference",
351
+ "CHGNet reference relaxation",
352
+ "simulation",
353
+ "demo_ready",
354
+ "/simulations/chgnet/reference/{material_id}",
355
+ boundary=ML_BOUNDARY,
356
+ ),
357
+ _cap(
358
+ "ase_relax",
359
+ "ASE/EMT smoke-test runner",
360
+ "simulation",
361
+ "sdk_ready",
362
+ "/simulations/ase/relax",
363
+ optional_dependency="ase",
364
+ boundary="Retained for SDK smoke testing; EMT is not evidence for Ti–Al–N.",
365
+ ),
366
+ _cap(
367
+ "materials_project",
368
+ "Materials Project",
369
+ "connectors",
370
+ "sdk_ready",
371
+ "MaterialsProjectConnector",
372
+ optional_dependency="mp-api",
373
+ ),
374
+ _cap(
375
+ "jarvis",
376
+ "JARVIS-DFT",
377
+ "connectors",
378
+ "sdk_ready",
379
+ "JarvisConnector",
380
+ optional_dependency="jarvis-tools",
381
+ ),
382
+ _cap("nomad", "NOMAD public metadata", "connectors", "sdk_ready", "NOMADConnector"),
383
+ _cap("optimade", "OPTIMADE / OQMD", "connectors", "sdk_ready", "OptimadeConnector"),
384
+ _cap(
385
+ "local_csv",
386
+ "Bounded local CSV / JSONL import",
387
+ "connectors",
388
+ "sdk_ready",
389
+ "inspect_local_content + import_local_content",
390
+ ),
391
+ _cap(
392
+ "connector_http_policy",
393
+ "Connector HTTP resilience policy",
394
+ "connectors",
395
+ "sdk_ready",
396
+ "ConnectorHTTPPolicy",
397
+ ),
398
+ _cap(
399
+ "generated_schemas",
400
+ "Pydantic-generated JSON Schemas",
401
+ "schema",
402
+ "sdk_ready",
403
+ "scripts/generate_schemas.py --check",
404
+ ),
405
+ _cap(
406
+ "simulation_result_envelope",
407
+ "Simulation result interchange",
408
+ "simulation",
409
+ "sdk_ready",
410
+ "SimulationResultEnvelope",
411
+ boundary="Result import and parsing only; no simulator orchestration.",
412
+ ),
413
+ _cap(
414
+ "elastic", "Derived elasticity", "derived", "sdk_ready", "mattergraph.derived.elastic"
415
+ ),
416
+ _cap(
417
+ "benchmark_utilities",
418
+ "Metrics, splits + uncertainty",
419
+ "benchmarks",
420
+ "sdk_ready",
421
+ "mattergraph-benchmarks",
422
+ optional_dependency="scikit-learn / matbench",
423
+ ),
424
+ _cap(
425
+ "oqmd_native",
426
+ "Native OQMD connector",
427
+ "connectors",
428
+ "stub",
429
+ "OQMDStubConnector",
430
+ boundary="Use the working OPTIMADE OQMD provider instead.",
431
+ ),
432
+ _cap("lammps", "LAMMPS runner", "simulation", "stub", "run_lammps"),
433
+ _cap(
434
+ "quantum_espresso",
435
+ "Quantum ESPRESSO runner",
436
+ "simulation",
437
+ "stub",
438
+ "run_quantum_espresso",
439
+ ),
440
+ _cap(
441
+ "persistence",
442
+ "Persistent workflow database",
443
+ "platform",
444
+ "out_of_scope",
445
+ "open-source demo uses an in-memory store",
446
+ ),
447
+ _cap(
448
+ "production_ranking",
449
+ "Production ranking + orchestration",
450
+ "platform",
451
+ "out_of_scope",
452
+ "not part of the public baseline",
453
+ ),
454
+ _cap(
455
+ "active_learning",
456
+ "Active-learning operations",
457
+ "platform",
458
+ "out_of_scope",
459
+ "not part of the public baseline",
460
+ ),
461
+ ]
462
+
463
+
464
+ def _cap(
465
+ capability_id: str,
466
+ label: str,
467
+ category: str,
468
+ status: str,
469
+ evidence: str,
470
+ *,
471
+ optional_dependency: str | None = None,
472
+ boundary: str | None = None,
473
+ ) -> dict[str, Any]:
474
+ return {
475
+ "id": capability_id,
476
+ "label": label,
477
+ "category": category,
478
+ "status": status,
479
+ "evidence": evidence,
480
+ "optional_dependency": optional_dependency,
481
+ "boundary": boundary,
482
+ }