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.
- mattergraph_api-0.1.0/.gitignore +48 -0
- mattergraph_api-0.1.0/Dockerfile +14 -0
- mattergraph_api-0.1.0/PKG-INFO +60 -0
- mattergraph_api-0.1.0/README.md +28 -0
- mattergraph_api-0.1.0/mattergraph_api/__init__.py +1 -0
- mattergraph_api-0.1.0/mattergraph_api/config.py +15 -0
- mattergraph_api-0.1.0/mattergraph_api/db/migrations/.gitkeep +1 -0
- mattergraph_api-0.1.0/mattergraph_api/db/models.py +6 -0
- mattergraph_api-0.1.0/mattergraph_api/db/session.py +4 -0
- mattergraph_api-0.1.0/mattergraph_api/main.py +77 -0
- mattergraph_api-0.1.0/mattergraph_api/py.typed +0 -0
- mattergraph_api-0.1.0/mattergraph_api/routes/__init__.py +1 -0
- mattergraph_api-0.1.0/mattergraph_api/routes/datasets.py +184 -0
- mattergraph_api-0.1.0/mattergraph_api/routes/demo.py +116 -0
- mattergraph_api-0.1.0/mattergraph_api/routes/materials.py +38 -0
- mattergraph_api-0.1.0/mattergraph_api/routes/scores.py +80 -0
- mattergraph_api-0.1.0/mattergraph_api/routes/search.py +26 -0
- mattergraph_api-0.1.0/mattergraph_api/routes/simulations.py +46 -0
- mattergraph_api-0.1.0/mattergraph_api/routes/workflows.py +18 -0
- mattergraph_api-0.1.0/mattergraph_api/services/__init__.py +1 -0
- mattergraph_api-0.1.0/mattergraph_api/services/dataset_registry.py +201 -0
- mattergraph_api-0.1.0/mattergraph_api/services/demo_service.py +482 -0
- mattergraph_api-0.1.0/mattergraph_api/services/store_service.py +38 -0
- mattergraph_api-0.1.0/mattergraph_api/services/workflow_service.py +195 -0
- mattergraph_api-0.1.0/pyproject.toml +53 -0
- mattergraph_api-0.1.0/tests/conftest.py +9 -0
- mattergraph_api-0.1.0/tests/registry_memory_probe.py +59 -0
- mattergraph_api-0.1.0/tests/test_api.py +235 -0
- mattergraph_api-0.1.0/tests/test_dataset_api.py +180 -0
- mattergraph_api-0.1.0/tests/test_registry_memory_budget.py +55 -0
- 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 @@
|
|
|
1
|
+
|
|
@@ -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
|