mattergraph-api 0.1.0__tar.gz

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.
Files changed (31) hide show
  1. mattergraph_api-0.1.0/.gitignore +48 -0
  2. mattergraph_api-0.1.0/Dockerfile +14 -0
  3. mattergraph_api-0.1.0/PKG-INFO +60 -0
  4. mattergraph_api-0.1.0/README.md +28 -0
  5. mattergraph_api-0.1.0/mattergraph_api/__init__.py +1 -0
  6. mattergraph_api-0.1.0/mattergraph_api/config.py +15 -0
  7. mattergraph_api-0.1.0/mattergraph_api/db/migrations/.gitkeep +1 -0
  8. mattergraph_api-0.1.0/mattergraph_api/db/models.py +6 -0
  9. mattergraph_api-0.1.0/mattergraph_api/db/session.py +4 -0
  10. mattergraph_api-0.1.0/mattergraph_api/main.py +77 -0
  11. mattergraph_api-0.1.0/mattergraph_api/py.typed +0 -0
  12. mattergraph_api-0.1.0/mattergraph_api/routes/__init__.py +1 -0
  13. mattergraph_api-0.1.0/mattergraph_api/routes/datasets.py +184 -0
  14. mattergraph_api-0.1.0/mattergraph_api/routes/demo.py +116 -0
  15. mattergraph_api-0.1.0/mattergraph_api/routes/materials.py +38 -0
  16. mattergraph_api-0.1.0/mattergraph_api/routes/scores.py +80 -0
  17. mattergraph_api-0.1.0/mattergraph_api/routes/search.py +26 -0
  18. mattergraph_api-0.1.0/mattergraph_api/routes/simulations.py +46 -0
  19. mattergraph_api-0.1.0/mattergraph_api/routes/workflows.py +18 -0
  20. mattergraph_api-0.1.0/mattergraph_api/services/__init__.py +1 -0
  21. mattergraph_api-0.1.0/mattergraph_api/services/dataset_registry.py +201 -0
  22. mattergraph_api-0.1.0/mattergraph_api/services/demo_service.py +482 -0
  23. mattergraph_api-0.1.0/mattergraph_api/services/store_service.py +38 -0
  24. mattergraph_api-0.1.0/mattergraph_api/services/workflow_service.py +195 -0
  25. mattergraph_api-0.1.0/pyproject.toml +53 -0
  26. mattergraph_api-0.1.0/tests/conftest.py +9 -0
  27. mattergraph_api-0.1.0/tests/registry_memory_probe.py +59 -0
  28. mattergraph_api-0.1.0/tests/test_api.py +235 -0
  29. mattergraph_api-0.1.0/tests/test_dataset_api.py +180 -0
  30. mattergraph_api-0.1.0/tests/test_registry_memory_budget.py +55 -0
  31. mattergraph_api-0.1.0/tests/test_spc_snapshot.py +101 -0
@@ -0,0 +1,48 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ dist/
7
+ build/
8
+ .venv/
9
+ venv/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+ .pytest_cache/
13
+ .hypothesis/
14
+ .coverage
15
+ coverage.xml
16
+ htmlcov/
17
+
18
+ # Docs build output
19
+ site/
20
+
21
+ # Env
22
+ .env
23
+ .env.local
24
+ *.local
25
+
26
+ # Node
27
+ node_modules/
28
+ apps/web/dist/
29
+ apps/web/playwright-report/
30
+ apps/web/test-results/
31
+ *.tsbuildinfo
32
+ .next/
33
+ out/
34
+
35
+ # IDE
36
+ .idea/
37
+ .vscode/
38
+ *.swp
39
+
40
+ # OS
41
+ .DS_Store
42
+
43
+ # Data artifacts (keep demo/ tracked)
44
+ data/cache/
45
+ *.sqlite3
46
+
47
+ .claude
48
+ apps/private-platform-ui/
@@ -0,0 +1,14 @@
1
+ FROM python:3.12-slim
2
+ RUN apt-get update && apt-get install -y --no-install-recommends build-essential \
3
+ && rm -rf /var/lib/apt/lists/*
4
+ WORKDIR /app
5
+ COPY pyproject.toml README.md /app/
6
+ COPY _workspace_meta.py /app/
7
+ COPY packages /app/packages
8
+ COPY data /app/data
9
+ RUN pip install --no-cache-dir uv
10
+ RUN cd /app && uv sync --all-packages
11
+ ENV PYTHONPATH=/app/packages/mattergraph-api
12
+ EXPOSE 8000
13
+ WORKDIR /app
14
+ CMD ["uv", "run", "uvicorn", "mattergraph_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: mattergraph-api
3
+ Version: 0.1.0
4
+ Summary: FastAPI service for the MatterGraph demo and integrations.
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: api,fastapi,materials-informatics,materials-science
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
21
+ Classifier: Topic :: Scientific/Engineering :: Physics
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: fastapi>=0.110
24
+ Requires-Dist: mattergraph-connectors~=0.1.0
25
+ Requires-Dist: mattergraph-core~=0.1.0
26
+ Requires-Dist: mattergraph-sim~=0.1.0
27
+ Requires-Dist: pydantic-settings>=2.2
28
+ Requires-Dist: pydantic>=2.5
29
+ Requires-Dist: pymatgen>=2024.1.1
30
+ Requires-Dist: uvicorn[standard]>=0.29.0
31
+ Description-Content-Type: text/markdown
32
+
33
+ # mattergraph-api
34
+
35
+ FastAPI demo service for [MatterGraph](https://github.com/cyrusmo/MatterGraph).
36
+
37
+ This is a **demonstration surface**, not a production service: storage is an in-memory store loaded from a JSONL fixture, and the persistence layer under `mattergraph_api/db/` is a placeholder for installations that need one.
38
+
39
+ ## Routes
40
+
41
+ | Route | Purpose |
42
+ |---|---|
43
+ | `GET /health` | Liveness |
44
+ | `GET /materials`, `GET /materials/{mid}` | Browse normalized records |
45
+ | `GET /search?element=` | Filter by element |
46
+ | `POST /scores/rank` | Rank candidates with objectives, constraints, and weights |
47
+ | `POST /simulations/ase/relax` | Run an ASE relaxation (503 if `ase` is unavailable) |
48
+ | `GET /workflows/lematerial/demo` | End-to-end LeMat-Bulk screening walkthrough |
49
+
50
+ ## Install and run
51
+
52
+ ```bash
53
+ pip install mattergraph-api
54
+ export MATTERGRAPH_DEMO_DATA=data/demo/materials_sample.jsonl
55
+ uvicorn mattergraph_api.main:app --host 0.0.0.0 --port 8000
56
+ ```
57
+
58
+ ## License
59
+
60
+ Apache-2.0
@@ -0,0 +1,28 @@
1
+ # mattergraph-api
2
+
3
+ FastAPI demo service for [MatterGraph](https://github.com/cyrusmo/MatterGraph).
4
+
5
+ This is a **demonstration surface**, not a production service: storage is an in-memory store loaded from a JSONL fixture, and the persistence layer under `mattergraph_api/db/` is a placeholder for installations that need one.
6
+
7
+ ## Routes
8
+
9
+ | Route | Purpose |
10
+ |---|---|
11
+ | `GET /health` | Liveness |
12
+ | `GET /materials`, `GET /materials/{mid}` | Browse normalized records |
13
+ | `GET /search?element=` | Filter by element |
14
+ | `POST /scores/rank` | Rank candidates with objectives, constraints, and weights |
15
+ | `POST /simulations/ase/relax` | Run an ASE relaxation (503 if `ase` is unavailable) |
16
+ | `GET /workflows/lematerial/demo` | End-to-end LeMat-Bulk screening walkthrough |
17
+
18
+ ## Install and run
19
+
20
+ ```bash
21
+ pip install mattergraph-api
22
+ export MATTERGRAPH_DEMO_DATA=data/demo/materials_sample.jsonl
23
+ uvicorn mattergraph_api.main:app --host 0.0.0.0 --port 8000
24
+ ```
25
+
26
+ ## License
27
+
28
+ Apache-2.0
@@ -0,0 +1 @@
1
+ """MatterGraph demo API package."""
@@ -0,0 +1,15 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+
6
+
7
+ class Settings(BaseSettings):
8
+ model_config = SettingsConfigDict(env_prefix="MATTERGRAPH_", extra="ignore")
9
+
10
+ demo_data: Path = Path(
11
+ os.environ.get("MATTERGRAPH_DEMO_DATA", "data/demo/materials_sample.jsonl")
12
+ )
13
+
14
+
15
+ settings = Settings()
@@ -0,0 +1,6 @@
1
+ # Placeholder: SQLAlchemy models for setups that persist material records.
2
+
3
+ from typing import Any
4
+
5
+ # Not used in the open-source demo, which is JSONL-in-memory.
6
+ StubModel: Any = object
@@ -0,0 +1,4 @@
1
+ # Placeholder: database session factory for a user-provided persistence backend.
2
+
3
+ def get_session() -> None:
4
+ return None
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import FastAPI, Request
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from fastapi.responses import JSONResponse
6
+ from mattergraph_connectors.local_import import ImportLimitError, ImportValidationError
7
+
8
+ from mattergraph_api.routes import datasets, demo, materials, scores, search, simulations, workflows
9
+ from mattergraph_api.services.dataset_registry import (
10
+ DatasetBusyError,
11
+ DatasetCapacityError,
12
+ DatasetNotFoundError,
13
+ )
14
+
15
+ app = FastAPI(
16
+ title="MatterGraph API",
17
+ description=(
18
+ "Evidence-first demo API for provenanced materials records, reciprocal graph summaries, "
19
+ "audited ranking, and explicitly labeled simulation evidence."
20
+ ),
21
+ version="0.1.0",
22
+ )
23
+ app.add_middleware(
24
+ CORSMiddleware,
25
+ allow_origins=["*"],
26
+ allow_credentials=True,
27
+ allow_methods=["*"],
28
+ allow_headers=["*"],
29
+ )
30
+
31
+ app.include_router(materials.router, tags=["materials"])
32
+ app.include_router(search.router, tags=["search"])
33
+ app.include_router(scores.router, tags=["scores"])
34
+ app.include_router(simulations.router, tags=["simulations"])
35
+ app.include_router(workflows.router, tags=["workflows"])
36
+ app.include_router(demo.router, tags=["demo"])
37
+ app.include_router(datasets.router, tags=["datasets"])
38
+
39
+
40
+ @app.exception_handler(DatasetNotFoundError)
41
+ def dataset_not_found(_request: Request, error: DatasetNotFoundError) -> JSONResponse:
42
+ return JSONResponse(
43
+ status_code=404,
44
+ content={
45
+ "detail": str(error),
46
+ "code": "dataset_evicted" if error.evicted else "dataset_not_found",
47
+ "dataset_id": error.dataset_id,
48
+ },
49
+ )
50
+
51
+
52
+ @app.exception_handler(DatasetBusyError)
53
+ def dataset_busy(_request: Request, error: DatasetBusyError) -> JSONResponse:
54
+ return JSONResponse(status_code=409, content={"detail": str(error), "code": "dataset_busy"})
55
+
56
+
57
+ @app.exception_handler(ImportLimitError)
58
+ @app.exception_handler(DatasetCapacityError)
59
+ def dataset_limit(_request: Request, error: Exception) -> JSONResponse:
60
+ return JSONResponse(status_code=413, content={"detail": str(error), "code": "dataset_limit"})
61
+
62
+
63
+ @app.exception_handler(ImportValidationError)
64
+ def invalid_import(_request: Request, error: ImportValidationError) -> JSONResponse:
65
+ return JSONResponse(
66
+ status_code=422,
67
+ content={
68
+ "detail": str(error),
69
+ "code": "invalid_import",
70
+ "report": error.report.model_dump(mode="json"),
71
+ },
72
+ )
73
+
74
+
75
+ @app.get("/health")
76
+ def health() -> dict[str, str]:
77
+ return {"status": "ok"}
File without changes
@@ -0,0 +1 @@
1
+ """API routes."""
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from fastapi import APIRouter, Response
6
+ from mattergraph import MaterialStore
7
+ from mattergraph.datasets import DeduplicationBasis, MatterGraphDataset
8
+ from mattergraph_connectors.local_import import (
9
+ DatasetImportMapping,
10
+ ImportReport,
11
+ ImportResult,
12
+ import_local_content,
13
+ inspect_local_content,
14
+ )
15
+ from pydantic import BaseModel, ConfigDict, Field
16
+
17
+ from mattergraph_api.services.dataset_registry import dataset_registry
18
+ from mattergraph_api.services.demo_service import graph_summary
19
+
20
+ router = APIRouter(prefix="/datasets")
21
+
22
+
23
+ class InspectRequest(BaseModel):
24
+ model_config = ConfigDict(extra="forbid")
25
+
26
+ filename: str
27
+ format: Literal["csv", "jsonl"]
28
+ content: str
29
+
30
+
31
+ class ImportRequest(InspectRequest):
32
+ mapping: DatasetImportMapping | None = None
33
+ error_policy: Literal["reject_file", "skip_invalid_rows"] = "reject_file"
34
+
35
+
36
+ class SlicePreviewRequest(BaseModel):
37
+ model_config = ConfigDict(extra="forbid")
38
+
39
+ include_elements: list[str] = Field(default_factory=list)
40
+ exclude_elements: list[str] = Field(default_factory=list)
41
+ max_nsites: int | None = Field(default=None, ge=1)
42
+ max_nelements: int | None = Field(default=None, ge=1)
43
+ target: str | None = None
44
+ deduplication_basis: DeduplicationBasis = "immutable_id"
45
+ allow_mixed_functionals: bool = False
46
+ allow_duplicate_records: bool = False
47
+
48
+
49
+ @router.get("")
50
+ def list_datasets() -> dict[str, Any]:
51
+ return {"datasets": dataset_registry.list(), "registry": dataset_registry.stats()}
52
+
53
+
54
+ @router.post("/inspect", response_model=ImportReport)
55
+ def inspect_dataset(request: InspectRequest) -> ImportReport:
56
+ return inspect_local_content(
57
+ filename=request.filename,
58
+ format=request.format,
59
+ content=request.content,
60
+ )
61
+
62
+
63
+ @router.post("/import", response_model=ImportResult)
64
+ def import_dataset(request: ImportRequest) -> ImportResult:
65
+ imported = import_local_content(
66
+ filename=request.filename,
67
+ format=request.format,
68
+ content=request.content,
69
+ mapping=request.mapping,
70
+ error_policy=request.error_policy,
71
+ )
72
+ dataset_registry.register(
73
+ imported.result.manifest,
74
+ imported.normalized_jsonl,
75
+ )
76
+ return imported.result.model_copy(
77
+ update={
78
+ "manifest": imported.result.manifest.model_copy(
79
+ update={"degraded": imported.result.manifest.degraded}
80
+ )
81
+ }
82
+ )
83
+
84
+
85
+ @router.get("/{dataset_id}")
86
+ def get_dataset(dataset_id: str) -> dict[str, Any]:
87
+ return dataset_registry.status(dataset_id)
88
+
89
+
90
+ @router.delete("/{dataset_id}")
91
+ def delete_dataset(dataset_id: str) -> dict[str, Any]:
92
+ manifest = dataset_registry.delete(dataset_id)
93
+ return {"deleted": True, "dataset_id": manifest.dataset_id}
94
+
95
+
96
+ @router.get("/{dataset_id}/export")
97
+ def export_dataset(dataset_id: str, format: Literal["jsonl"] = "jsonl") -> Response:
98
+ manifest, payload = dataset_registry.export(dataset_id)
99
+ return Response(
100
+ content=payload,
101
+ media_type="application/x-ndjson",
102
+ headers={
103
+ "Content-Disposition": f'attachment; filename="{manifest.dataset_id}.jsonl"',
104
+ "X-MatterGraph-Dataset-Id": manifest.dataset_id,
105
+ "X-MatterGraph-SHA256": manifest.normalized_sha256,
106
+ "X-MatterGraph-Record-Count": str(manifest.record_count),
107
+ },
108
+ )
109
+
110
+
111
+ @router.post("/{dataset_id}/slices/preview")
112
+ def preview_slice(dataset_id: str, request: SlicePreviewRequest) -> dict[str, Any]:
113
+ store = dataset_registry.materialize(dataset_id)
114
+ dataset = _dataset_from_store(dataset_id, store)
115
+ candidate = dataset.candidate_pool(
116
+ include=request.include_elements,
117
+ exclude=request.exclude_elements,
118
+ max_nsites=request.max_nsites,
119
+ max_nelements=request.max_nelements,
120
+ )
121
+ candidate_slice = candidate.create_slice(
122
+ "local_workbench_preview",
123
+ allow_mixed_functionals=request.allow_mixed_functionals,
124
+ allow_duplicate_records=request.allow_duplicate_records,
125
+ deduplication_basis=request.deduplication_basis,
126
+ target=request.target,
127
+ )
128
+ graph_ready = 0
129
+ graph_excluded = 0
130
+ material_ids = [str(value) for value in candidate_slice.frame["material_id"].tolist()]
131
+ materials_by_id = {material.material_id: material for material in store.materials}
132
+ for material_id in material_ids:
133
+ try:
134
+ graph_summary(materials_by_id[material_id], max_edges=0)
135
+ graph_ready += 1
136
+ except ValueError:
137
+ graph_excluded += 1
138
+ preview_columns = ["material_id", "formula", "nsites", "nelements"]
139
+ if request.target:
140
+ preview_columns.append(request.target)
141
+ frame = candidate_slice.frame
142
+ preview_columns = [column for column in preview_columns if column in frame.columns]
143
+ benchmark_preview = frame[preview_columns].head(20).where(frame.notna(), None).to_dict(
144
+ orient="records"
145
+ )
146
+ return {
147
+ "slice": candidate_slice.report(),
148
+ "material_ids": material_ids,
149
+ "graph_readiness": {"included_count": graph_ready, "excluded_count": graph_excluded},
150
+ "benchmark_preview": benchmark_preview,
151
+ }
152
+
153
+
154
+ def _dataset_from_store(dataset_id: str, store: MaterialStore) -> MatterGraphDataset:
155
+ records: list[dict[str, Any]] = []
156
+ property_columns: set[str] = set()
157
+ property_units: dict[str, str] = {}
158
+ for material in store.materials:
159
+ record: dict[str, Any] = {
160
+ "material_id": material.material_id,
161
+ "formula": material.formula,
162
+ "reduced_formula": material.reduced_formula,
163
+ "elements": material.elements,
164
+ "structure": material.structure,
165
+ "immutable_id": material.material_id,
166
+ "provenance": material.provenance,
167
+ }
168
+ for property_value in material.properties:
169
+ record[property_value.name] = property_value.value
170
+ property_columns.add(property_value.name)
171
+ if property_value.unit:
172
+ property_units.setdefault(property_value.name, property_value.unit)
173
+ records.append(record)
174
+ return MatterGraphDataset.from_records(
175
+ records,
176
+ source_dataset=dataset_id,
177
+ source_subset="local_import",
178
+ metadata={
179
+ "default_deduplication_basis": "immutable_id",
180
+ "property_columns": sorted(property_columns),
181
+ "property_units": property_units,
182
+ "provenance_fields": ["immutable_id", "provenance"],
183
+ },
184
+ )
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from fastapi import APIRouter
6
+
7
+ from mattergraph_api.services import store_service
8
+ from mattergraph_api.services.demo_service import (
9
+ DEFAULT_CONSTRAINTS,
10
+ DEFAULT_OBJECTIVES,
11
+ FIXTURE_DISCLAIMER,
12
+ FIXTURE_RELATIVE_PATH,
13
+ capability_catalog,
14
+ chgnet_state,
15
+ get_default_material_id,
16
+ get_default_scorecard,
17
+ get_demo_manifest,
18
+ graph_summary,
19
+ simulation_readiness,
20
+ )
21
+
22
+ router = APIRouter()
23
+
24
+
25
+ @router.get("/capabilities")
26
+ def capabilities() -> dict[str, list[dict[str, Any]]]:
27
+ return {"capabilities": capability_catalog()}
28
+
29
+
30
+ @router.get("/demo/preflight")
31
+ def demo_preflight() -> dict[str, Any]:
32
+ store = store_service.get_store()
33
+ graph_ready = 0
34
+ graph_excluded = 0
35
+ graph_invalid = 0
36
+ for material in store.materials:
37
+ try:
38
+ summary = graph_summary(material, max_edges=0)
39
+ graph_ready += 1
40
+ if summary["validation"]["state"] != "valid":
41
+ graph_invalid += 1
42
+ except ValueError:
43
+ graph_excluded += 1
44
+
45
+ scorecard = get_default_scorecard()
46
+ score_report = scorecard.report(store.materials)
47
+ simulation_targets = {
48
+ material.material_id: simulation_readiness(material) for material in store.materials
49
+ }
50
+ default_material_id = get_default_material_id()
51
+ manifest = get_demo_manifest()
52
+ ml_state = chgnet_state()
53
+ checks = [
54
+ {
55
+ "id": "fixture",
56
+ "status": "pass" if store.materials else "fail",
57
+ "detail": f"{len(store.materials)} normalized records",
58
+ },
59
+ {
60
+ "id": "graphs",
61
+ "status": "pass" if graph_ready and not graph_invalid else "fail",
62
+ "detail": (
63
+ f"{graph_ready} graph-ready; {graph_excluded} excluded; {graph_invalid} invalid"
64
+ ),
65
+ },
66
+ {
67
+ "id": "ranking",
68
+ "status": "pass" if score_report["ranked_count"] >= 3 else "warn",
69
+ "detail": (
70
+ f"{score_report['ranked_count']} rank-eligible; "
71
+ f"{score_report['excluded_by_constraints']} excluded"
72
+ ),
73
+ },
74
+ {
75
+ "id": "ml_reference",
76
+ "status": "pass" if ml_state["reference_available"] else "warn",
77
+ "detail": str(ml_state["detail"]),
78
+ },
79
+ ]
80
+ overall = "ready" if all(check["status"] == "pass" for check in checks) else "degraded"
81
+ return {
82
+ "status": overall,
83
+ "fixture": {
84
+ "path": FIXTURE_RELATIVE_PATH,
85
+ "kind": "checksummed_real_snapshot",
86
+ "disclaimer": FIXTURE_DISCLAIMER,
87
+ "dataset": manifest["dataset"],
88
+ "subset": manifest["subset"],
89
+ "upstream_revision": manifest["upstream_revision"],
90
+ "hull_dataset": manifest["hull_dataset"],
91
+ "hull_revision": manifest["hull_revision"],
92
+ "license": manifest["license"],
93
+ "citation_doi": manifest["citation_doi"],
94
+ "snapshot_sha256": manifest["snapshot_sha256"],
95
+ "source_population": manifest["source_population"],
96
+ "field_sources": manifest["field_sources"],
97
+ },
98
+ "record_count": len(store.materials),
99
+ "graph": {
100
+ "included_count": graph_ready,
101
+ "excluded_count": graph_excluded,
102
+ "invalid_count": graph_invalid,
103
+ "validation_state": "valid" if graph_ready and not graph_invalid else "invalid",
104
+ },
105
+ "ranking": {
106
+ "ranked_count": score_report["ranked_count"],
107
+ "excluded_by_constraints": score_report["excluded_by_constraints"],
108
+ "binary_normalization": score_report["binary_normalization"],
109
+ "objectives": DEFAULT_OBJECTIVES,
110
+ "constraints": DEFAULT_CONSTRAINTS,
111
+ },
112
+ "default_material_id": default_material_id,
113
+ "chgnet": ml_state,
114
+ "simulation_targets": simulation_targets,
115
+ "checks": checks,
116
+ }
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter, HTTPException, Query
4
+ from mattergraph import Material
5
+
6
+ from mattergraph_api.services import store_service
7
+ from mattergraph_api.services.demo_service import graph_summary
8
+
9
+ router = APIRouter()
10
+
11
+
12
+ @router.get("/materials")
13
+ def list_materials(dataset_id: str | None = Query(default=None)) -> list[dict]:
14
+ store = store_service.resolve_store(dataset_id)
15
+ return [m.model_dump() for m in store.materials]
16
+
17
+
18
+ @router.get("/materials/{mid}")
19
+ def get_material(mid: str, dataset_id: str | None = Query(default=None)) -> dict:
20
+ store = store_service.resolve_store(dataset_id)
21
+ m: Material | None = store.get(mid)
22
+ if m is None:
23
+ raise HTTPException(status_code=404, detail="not found")
24
+ return m.model_dump()
25
+
26
+
27
+ @router.get("/materials/{mid}/graph-summary")
28
+ def get_material_graph_summary(
29
+ mid: str, dataset_id: str | None = Query(default=None)
30
+ ) -> dict:
31
+ store = store_service.resolve_store(dataset_id)
32
+ material: Material | None = store.get(mid)
33
+ if material is None:
34
+ raise HTTPException(status_code=404, detail="not found")
35
+ try:
36
+ return graph_summary(material)
37
+ except ValueError as exc:
38
+ raise HTTPException(status_code=422, detail=str(exc)) from exc