"""Polars results table of a FEM extraction dataset.
The table is long-format: one row per parameter point, quantity, and matrix entry.
=============== =========== ===========================================================
Column Type Meaning
=============== =========== ===========================================================
``run_id`` String Identity of the solver run that produced the row.
``status`` String ``ok``, ``failed``, or ``not_converged`` (see :class:`RunStatus`).
<axis> Float64 One column per metadata axis, in the axis' unit.
<variant> String One column per metadata variant.
``quantity`` String Name of a metadata quantity.
``row`` String Row terminal label; null for scalar quantities.
``col`` String Column terminal label; null for scalar quantities.
``value`` Float64 Value (real part for complex quantities); null unless ``ok``.
``value_imag`` Float64 Imaginary part; null for real quantities.
``unit`` String SI unit; must equal the metadata unit of the quantity.
=============== =========== ===========================================================
The :class:`~qpdk.models.datasets.metadata.DatasetMetadata` is stored inside the files
(see :mod:`qpdk.models.datasets.store`), so a dataset is self-describing and nothing
but its Parquet parts or Delta table.
.. only:: html
.. mermaid::
flowchart LR
A["Python generator: metadata + grid"] --> B["Palace solves"]
B --> C["Validated Parquet parts or Delta table"]
C --> D["Lazy scan: filter + select"]
D --> E["Dense grid for one quantity and variant"]
E --> F["JAX interpolation"]
F --> G["SAX circuit model"]
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import StrEnum
from functools import cached_property
from itertools import product
from pathlib import Path
from typing import TYPE_CHECKING
import numpy as np
from qpdk.models.datasets.metadata import Axis, DatasetMetadata, Quantity
from qpdk.models.datasets.store import DeltaStore, ParquetParts
if TYPE_CHECKING:
import jax
import polars as pl
DATASETS_PATH = Path(__file__).parent / "data"
"""Curated datasets shipped with qpdk."""
[docs]
class RunStatus(StrEnum):
"""Outcome of a solver run."""
OK = "ok"
FAILED = "failed"
NOT_CONVERGED = "not_converged"
[docs]
class DatasetError(ValueError):
"""The dataset is inconsistent with its metadata or cannot be gridded."""
def __init__(self, dataset: str, problems: list[str]) -> None:
"""Collect every problem into one message."""
self.problems = problems
super().__init__(
f"Dataset {dataset!r} failed validation:\n"
+ "\n".join(f" - {p}" for p in problems)
)
[docs]
@dataclass(frozen=True)
class Grid:
"""A quantity on a complete rectilinear grid, ready for interpolation.
``values`` has shape ``(*axis sizes, *component shape)`` where the component
shape is ``(n_terminals, n_terminals)`` for matrix quantities and ``()`` for
scalars. Axes follow metadata order with ascending ``coords``, terminals
follow :attr:`~qpdk.models.datasets.metadata.DatasetMetadata.terminals`. Missing or
failed points are NaN, never zero.
"""
dataset: str
quantity: Quantity
axes: tuple[Axis, ...]
coords: tuple[np.ndarray | jax.Array, ...]
terminals: tuple[str, ...]
variant: dict[str, str]
values: np.ndarray | jax.Array
@property
def axis_names(self) -> tuple[str, ...]:
"""Axis names in tensor order."""
return tuple(axis.name for axis in self.axes)
@property
def domain(self) -> dict[str, tuple[float, float]]:
"""Validated domain of each axis; defaults to the span of its grid values."""
return {
axis.name: axis.validated or (float(c[0]), float(c[-1]))
for axis, c in zip(self.axes, self.coords, strict=True)
}
[docs]
def schema(metadata: DatasetMetadata) -> pl.Schema:
"""Column schema of the results table for ``metadata``.
Returns:
The Polars schema.
"""
import polars as pl # ruff: ignore[import-outside-top-level]
return pl.Schema({
"run_id": pl.String,
"status": pl.String,
**{axis.name: pl.Float64 for axis in metadata.axes},
**dict.fromkeys(metadata.variants, pl.String),
"quantity": pl.String,
"row": pl.String,
"col": pl.String,
"value": pl.Float64,
"value_imag": pl.Float64,
"unit": pl.String,
})
[docs]
class Dataset:
"""A FEM extraction dataset: Parquet parts or a Delta table with its metadata.
Args:
location: Directory of Parquet parts, the name of a dataset shipped in
:data:`DATASETS_PATH`, or with ``delta=True`` a Delta table path or
object-store URI such as ``gs://bucket/dataset``.
metadata: Metadata for a new dataset. For an existing one it is read
from the files and, if given, must match.
delta: Store results as a Delta Lake table (``delta`` extra).
version: Delta table version to read; a pinned dataset is read-only.
storage_options: Object-store options (credentials, endpoints) for a
Delta URI. Never written to the dataset.
Raises:
FileNotFoundError: if nothing is stored yet and no ``metadata`` is given.
DatasetError: if ``metadata`` differs from the stored metadata.
"""
def __init__(
self,
location: Path | str,
metadata: DatasetMetadata | None = None,
*,
delta: bool = False,
version: int | None = None,
storage_options: dict[str, str] | None = None,
) -> None:
"""Open the dataset and read its metadata."""
if delta:
self.store: ParquetParts | DeltaStore = DeltaStore(
location, storage_options=storage_options, version=version
)
else:
if version is not None or storage_options is not None:
msg = "version and storage_options apply to Delta tables only; pass delta=True."
raise ValueError(msg)
path = Path(location)
if not path.is_dir() and (DATASETS_PATH / path).is_dir():
path = DATASETS_PATH / path
self.store = ParquetParts(path)
stored = self.store.metadata()
if stored is not None and metadata is not None and stored != metadata:
raise DatasetError(
stored.name,
["The given metadata differs from the metadata stored in the files."],
)
if (resolved := stored or metadata) is None:
msg = f"No dataset at {self.store!r}; pass metadata to create one."
raise FileNotFoundError(msg)
self.metadata: DatasetMetadata = resolved
def __repr__(self) -> str:
"""Show the dataset name and store."""
return f"Dataset({self.metadata.name!r}, store={self.store!r})"
[docs]
def scan(self) -> pl.LazyFrame:
"""Lazily scan the results with the metadata schema.
Returns:
Lazy frame over all results; not validated.
"""
return self.store.scan(schema(self.metadata))
@cached_property
def table(self) -> pl.DataFrame:
"""The full results table, validated against the metadata.
Raises:
DatasetError: if the stored results do not match the schema.
"""
import polars as pl # ruff: ignore[import-outside-top-level]
try:
frame = self.scan().collect()
except (pl.exceptions.SchemaError, pl.exceptions.ColumnNotFoundError) as error:
raise DatasetError(
self.metadata.name, [f"Results do not match the schema: {error}"]
) from error
validate(frame, self.metadata)
return frame
[docs]
def grid(
self, quantity: str, *, allow_missing: bool = False, **variant: str
) -> Grid:
"""Arrange ``quantity`` on its grid; see :func:`to_grid`.
Returns:
The dense grid of ``quantity``.
Raises:
DatasetError: If the selected rows are invalid or variants are ambiguous.
"""
self.metadata.quantity(quantity)
if set(variant) != set(self.metadata.variants):
raise DatasetError(
self.metadata.name,
[
f"Select one value of each variant {list(self.metadata.variants)}, got {variant}."
],
)
import polars as pl # ruff: ignore[import-outside-top-level]
selected = (
self
.scan()
.filter(
pl.col("quantity") == quantity,
*(pl.col(name) == value for name, value in variant.items()),
)
.collect()
)
validate(selected, self.metadata)
return to_grid(
selected, self.metadata, quantity, allow_missing=allow_missing, **variant
)
[docs]
def append(self, frame: pl.DataFrame, *, part: str | None = None) -> str:
"""Validate ``frame`` together with the stored results, then store it.
Stored rows are never rewritten. Duplicates against earlier results are
rejected, so re-running a sweep cannot silently double a point. Parquet
parts take one writer at a time; a Delta append is one atomic commit and
the first append creates the table.
Returns:
What was written: a part path, or a Delta URI and version.
Raises:
DatasetError: If rows duplicate existing entries or mix run identities.
"""
import polars as pl # ruff: ignore[import-outside-top-level]
stored = self.store.metadata()
if stored is not None and stored != self.metadata:
raise DatasetError(
self.metadata.name,
["The given metadata differs from the metadata stored in the files."],
)
frame = frame.select(schema(self.metadata).names()).cast(schema(self.metadata))
validate(frame, self.metadata)
point_columns = [*self.metadata.variants, *(a.name for a in self.metadata.axes)]
try:
existing = self.scan()
except FileNotFoundError:
existing = None
if existing is not None:
keys = _key_columns(self.metadata)
overlaps = (
existing
.filter(
*(
pl.col(name).is_in(frame[name].unique().implode())
for name in point_columns
),
)
.select(keys)
.join(frame.lazy().select(keys), on=keys, how="semi", nulls_equal=True)
.limit(5)
.collect()
)
if overlaps.height:
raise DatasetError(
self.metadata.name, [f"duplicate entries, e.g. {overlaps.rows()}."]
)
run_columns = ["run_id", "status", *point_columns]
runs = (
pl
.concat([
existing.filter(
pl.col("run_id").is_in(frame["run_id"].unique().implode())
).select(run_columns),
frame.lazy().select(run_columns),
])
.group_by("run_id")
.agg(pl.struct(["status", *point_columns]).n_unique().alias("points"))
.filter(pl.col("points") > 1)
.limit(5)
.collect()
)
if runs.height:
raise DatasetError(
self.metadata.name,
[
f"Runs mix parameter points or statuses: {runs['run_id'].to_list()}."
],
)
written = self.store.write(
frame.sort(_key_columns(self.metadata), nulls_last=True),
self.metadata,
part,
)
self.__dict__.pop("table", None)
return written
def _key_columns(metadata: DatasetMetadata) -> list[str]:
"""Columns that identify one value: variants, axes, quantity, row, col.
Returns:
Column names.
"""
return [
*metadata.variants,
*(a.name for a in metadata.axes),
"quantity",
"row",
"col",
]
[docs]
def validate(frame: pl.DataFrame, metadata: DatasetMetadata) -> None:
"""Check a results table against its metadata.
Detects wrong columns, unknown quantities or statuses, unit mismatches,
terminals outside the declared ones, non-finite values, inconsistent runs,
duplicate points, and missing or spurious values. Grid completeness is
checked per selection by :func:`to_grid`.
Raises:
DatasetError: listing every problem found.
"""
import polars as pl # ruff: ignore[import-outside-top-level]
problems: list[str] = []
expected = schema(metadata)
if dict(frame.schema) != dict(expected):
missing = sorted(set(expected) - set(frame.columns))
extra = sorted(set(frame.columns) - set(expected))
wrong = sorted(
c
for c in set(expected) & set(frame.columns)
if frame.schema[c] != expected[c]
)
problems.append(
f"Columns do not match the schema (missing {missing}, unexpected {extra}, wrong dtype {wrong})."
)
raise DatasetError(metadata.name, problems)
def report(mask: pl.Expr, what: str, columns: list[str]) -> None:
bad = frame.filter(mask)
if bad.height:
sample = bad.select(columns).unique(maintain_order=True).head(5).rows()
problems.append(f"{bad.height} rows {what}, e.g. {sample}.")
axes = [a.name for a in metadata.axes]
for column in ["run_id", "status", "quantity", "unit", *metadata.variants, *axes]:
report(pl.col(column).is_null(), f"have a null {column!r}", ["run_id"])
for column in axes:
report(~pl.col(column).is_finite(), f"have a non-finite {column!r}", [column])
report(
~pl.col("value").is_finite() | ~pl.col("value_imag").is_finite(),
"have a non-finite value; record a failed solve with its status and a null value",
["run_id", "status"],
)
quantities = {q.name: q for q in metadata.quantities}
report(
~pl.col("quantity").is_in(list(quantities)),
"have an unknown quantity",
["quantity"],
)
report(
~pl.col("status").is_in([s.value for s in RunStatus]),
"have an unknown status",
["status"],
)
terminals = list(metadata.terminals)
for name, quantity in quantities.items():
is_q = pl.col("quantity") == name
report(
is_q & (pl.col("unit") != quantity.unit),
f"of {name!r} are not in {quantity.unit!r}",
["unit"],
)
if quantity.matrix:
report(
is_q
& ~(
pl.col("row").is_in(terminals).fill_null(False)
& pl.col("col").is_in(terminals).fill_null(False)
),
f"of {name!r} have null terminals or terminals outside {terminals}",
["row", "col"],
)
else:
report(
is_q & (pl.col("row").is_not_null() | pl.col("col").is_not_null()),
f"of scalar {name!r} have terminals",
["row", "col"],
)
imag = pl.col("value_imag").is_not_null()
report(
is_q
& (~imag if quantity.complex else imag)
& (pl.col("status") == RunStatus.OK),
f"of {name!r} have an inconsistent imaginary part",
["run_id"],
)
ok = pl.col("status") == RunStatus.OK
report(ok & pl.col("value").is_null(), "are 'ok' but have no value", ["run_id"])
report(
~ok & (pl.col("value").is_not_null() | pl.col("value_imag").is_not_null()),
"are not 'ok' but carry a value",
["run_id", "status"],
)
point = [*metadata.variants, *axes, "status"]
runs = (
frame
.group_by("run_id")
.agg(pl.struct(point).n_unique().alias("n"))
.filter(pl.col("n") > 1)
)
if runs.height:
problems.append(
f"Runs mix parameter points or statuses: {runs['run_id'].head(5).to_list()}."
)
dupes = frame.group_by(_key_columns(metadata)).len().filter(pl.col("len") > 1)
if dupes.height:
problems.append(
f"{dupes.height} duplicate entries, e.g. {dupes.drop('len').head(3).rows()}."
)
if problems:
raise DatasetError(metadata.name, problems)
[docs]
def to_grid(
frame: pl.DataFrame,
metadata: DatasetMetadata,
quantity: str,
*,
allow_missing: bool = False,
**variant: str,
) -> Grid:
"""Arrange one quantity of one variant on its complete rectilinear grid.
The grid of each axis is the sorted set of values in the selected rows, and
every combination of them must be present. The layout is deterministic:
axes in metadata order with ascending values, then row and column terminals
in metadata order.
Args:
frame: Validated results table.
metadata: Dataset metadata.
quantity: Quantity name.
allow_missing: Return NaN for missing or failed points instead of raising.
**variant: Value for every metadata variant; discrete variants are
selected explicitly and never interpolated.
Returns:
The dense grid of ``quantity``.
Raises:
DatasetError: if the selection is ambiguous or empty, a validated domain
leaves the grid, or the grid is incomplete.
"""
import polars as pl # ruff: ignore[import-outside-top-level]
q = metadata.quantity(quantity)
if set(variant) != set(metadata.variants):
raise DatasetError(
metadata.name,
[
f"Select one value of each variant {list(metadata.variants)}, got {variant}."
],
)
selected = frame.filter(
pl.col("quantity") == quantity,
*(pl.col(k) == v for k, v in variant.items()),
)
if not selected.height:
raise DatasetError(
metadata.name, [f"No rows of {quantity!r} for variant {variant}."]
)
axis_names = [a.name for a in metadata.axes]
coords = tuple(selected[name].unique().sort().to_numpy() for name in axis_names)
problems = [
f"Axis {axis.name!r} validated domain {axis.validated} leaves the grid [{c[0]}, {c[-1]}]."
for axis, c in zip(metadata.axes, coords, strict=True)
if axis.validated
and not c[0] <= axis.validated[0] <= axis.validated[1] <= c[-1]
]
if problems:
raise DatasetError(metadata.name, problems)
terminals = metadata.terminals if q.matrix else ()
full = pl.DataFrame(
list(product(*coords)),
schema=dict.fromkeys(axis_names, pl.Float64),
orient="row",
)
if q.matrix:
pairs = pl.DataFrame(
list(product(terminals, terminals)),
schema={"row": pl.String, "col": pl.String},
orient="row",
)
full = full.join(pairs, how="cross")
keys = [*axis_names, "row", "col"]
else:
keys = axis_names
joined = full.join(
selected, on=keys, how="left", nulls_equal=True, maintain_order="left"
)
bad = joined.filter(pl.col("value").is_null())
if bad.height and not allow_missing:
sample = bad.select([*axis_names, "status"]).unique(maintain_order=True)
raise DatasetError(
metadata.name,
[
(
f"{sample.height} grid points of {quantity!r} are missing or failed, e.g. "
f"{sample.head(5).rows(named=True)}. Pass allow_missing=True to keep them as NaN."
)
],
)
real = joined["value"].fill_null(np.nan).to_numpy()
values = (
real + 1j * joined["value_imag"].fill_null(0.0).to_numpy()
if q.complex
else real
)
shape = (*(len(c) for c in coords), *(len(terminals),) * (2 if q.matrix else 0))
return Grid(
dataset=metadata.name,
quantity=q,
axes=metadata.axes,
coords=coords,
terminals=tuple(terminals),
variant=dict(variant),
values=values.reshape(shape),
)