Source code for qpdk.models.datasets.metadata
"""Metadata describing the physical meaning of a FEM extraction dataset.
The metadata travels inside the results files, so a dataset is nothing but its
Parquet files (or Delta table): each Parquet part stores it as JSON under the
key-value metadata key :data:`METADATA_KEY`, and a Delta table stores it as
metadata of the ``value`` column. Parameter grids are not repeated here; they
are read from the data.
The metadata holds what a row of the table cannot carry on its own: the units
of the axes, the meaning of each quantity, the terminal order and reference
ground of matrix quantities, and free-form provenance (recipe, geometry, layer
stack, solver, accuracy).
"""
from enum import StrEnum
from typing import Any, Self
from pydantic import BaseModel, ConfigDict, Field, model_validator
SCHEMA_VERSION = 1
"""Version of the metadata and results-table schema implemented here."""
METADATA_KEY = "qpdk.dataset"
"""Parquet key-value (and Delta column) metadata key holding the JSON metadata."""
RESERVED_COLUMNS = (
"run_id",
"status",
"quantity",
"row",
"col",
"value",
"value_imag",
"unit",
)
"""Results-table columns that are not sweep parameters."""
class _Frozen(BaseModel):
"""Immutable model that rejects unknown keys."""
model_config = ConfigDict(frozen=True, extra="forbid")
[docs]
class QuantityKind(StrEnum):
"""Physical meaning of an extracted quantity.
The Maxwell capacitance matrix, the mutual (SPICE-style) capacitance matrix,
and reduced circuit parameters are different quantities and are never mixed.
"""
MAXWELL_CAPACITANCE = "maxwell_capacitance"
"""Maxwell matrix: positive diagonal, non-positive off-diagonal, w.r.t. the reference ground."""
MUTUAL_CAPACITANCE = "mutual_capacitance"
"""Mutual matrix: non-negative branch capacitances, diagonal entries to the reference ground."""
INDUCTANCE = "inductance"
"""Inductance matrix between terminal loops."""
S_PARAMETERS = "s_parameters"
"""Complex scattering matrix at the reference planes recorded in the provenance."""
CIRCUIT_PARAMETER = "circuit_parameter"
"""Scalar circuit parameter or numerical diagnostic, e.g. coupling capacitance or a solver residual."""
_CANONICAL_UNITS = {
QuantityKind.MAXWELL_CAPACITANCE: "F",
QuantityKind.MUTUAL_CAPACITANCE: "F",
QuantityKind.INDUCTANCE: "H",
QuantityKind.S_PARAMETERS: "1",
}
"""SI unit each physical kind is stored in; lookups apply no unit conversion."""
[docs]
class Quantity(_Frozen):
"""A quantity stored in the results table.
Every kind except :attr:`QuantityKind.CIRCUIT_PARAMETER` is a matrix over the
dataset terminals.
"""
name: str
kind: QuantityKind
unit: str = Field(description="SI unit, e.g. 'F', 'H', or '1' for dimensionless.")
complex: bool = False
@model_validator(mode="after")
def _check_unit(self) -> Self:
"""Require the canonical SI unit of the kind."""
if (unit := _CANONICAL_UNITS.get(self.kind)) and self.unit != unit:
msg = f"Quantity {self.name!r} of kind {self.kind.value!r} must be stored in {unit!r}, not {self.unit!r}."
raise ValueError(msg)
return self
@property
def matrix(self) -> bool:
"""Whether rows carry ``row``/``col`` terminals."""
return self.kind != QuantityKind.CIRCUIT_PARAMETER
[docs]
class Axis(_Frozen):
"""A continuous parameter axis; its grid values come from the data."""
name: str
unit: str
validated: tuple[float, float] | None = Field(
default=None,
description="Validated domain; defaults to the span of the gridded values.",
)
@model_validator(mode="after")
def _check_validated(self) -> Self:
"""Require an ordered validated domain."""
if self.validated is not None and not self.validated[0] <= self.validated[1]:
msg = (
f"Axis {self.name!r} validated domain {self.validated} is not ordered."
)
raise ValueError(msg)
return self
[docs]
class DatasetMetadata(_Frozen):
"""Metadata stored inside the results files of a dataset.
Args:
name: Dataset name.
description: One-line description.
synthetic: True when the values are not real solver output; such data
must not be used for design.
axes: Continuous parameters, in tensor order.
variants: Discrete parameters (string columns), selected and never
interpolated.
quantities: Stored quantities.
terminals: Ordered terminal labels; defines matrix row and column order.
reference_ground: Conductor all potentials are referenced to; not a
matrix terminal.
provenance: Free-form recipe, geometry, layer stack, solver, and accuracy
information.
schema_version: Schema version; must equal :data:`SCHEMA_VERSION`.
"""
name: str
description: str = ""
synthetic: bool = False
axes: tuple[Axis, ...] = Field(min_length=1)
variants: tuple[str, ...] = ()
quantities: tuple[Quantity, ...] = Field(min_length=1)
terminals: tuple[str, ...] = ()
reference_ground: str = "ground"
provenance: dict[str, Any] = Field(default_factory=dict)
schema_version: int = SCHEMA_VERSION
@model_validator(mode="after")
def _check(self) -> Self:
"""Check the schema version, unique names, and terminals."""
if self.schema_version != SCHEMA_VERSION:
msg = f"Unsupported schema_version {self.schema_version}; this qpdk reads version {SCHEMA_VERSION}."
raise ValueError(msg)
names = [a.name for a in self.axes] + list(self.variants)
if len(set(names)) != len(names):
msg = f"Axis and variant names must be unique, got {names}."
raise ValueError(msg)
if reserved := set(names) & set(RESERVED_COLUMNS):
msg = f"Axis or variant names collide with reserved columns: {sorted(reserved)}."
raise ValueError(msg)
quantities = [q.name for q in self.quantities]
if len(set(quantities)) != len(quantities):
msg = f"Quantity names must be unique, got {quantities}."
raise ValueError(msg)
if len(set(self.terminals)) != len(self.terminals):
msg = f"Duplicate terminal labels in {self.terminals}."
raise ValueError(msg)
if self.reference_ground in self.terminals:
msg = "The reference ground must not also be a matrix terminal."
raise ValueError(msg)
if any(q.matrix for q in self.quantities) and not self.terminals:
msg = "Matrix quantities need terminals."
raise ValueError(msg)
return self
[docs]
def quantity(self, name: str) -> Quantity:
"""Return the quantity called ``name``.
Raises:
KeyError: if there is no such quantity.
"""
for quantity in self.quantities:
if quantity.name == name:
return quantity
msg = f"Dataset {self.name!r} has no quantity {name!r}; available: {[q.name for q in self.quantities]}."
raise KeyError(msg)
[docs]
def to_json(self) -> str:
"""Serialize for the file metadata.
Returns:
Compact JSON.
"""
return self.model_dump_json()
[docs]
@classmethod
def from_json(cls, text: str) -> Self:
"""Parse and validate metadata read from a file.
Returns:
The validated metadata.
"""
return cls.model_validate_json(text)