Skip to content

pelinker.selected_hyperparameters

Machine-readable handoff from the search stages to pelinker-fit.

Model selection picks a (model, layer) and a pooled min_cluster_size; dimension selection picks (pca_components, umap_dim). Both wrote their winners only into human-readable reports, so the values reached the fit by being retyped into Hydra overrides. That is how a real run could choose pca=22, umap_dim=3 while pelinker-fit went on defaulting to 100 and 8.

This module defines the small typed record both searches emit and the fit consumes, so the transfer is checked rather than remembered. The realized row count travels with it — min_cluster_size is an absolute count and means nothing without the N it was chosen at (see :mod:pelinker.scaling).

SelectedHyperparameters dataclass

What a search stage chose, and the conditions it chose them under.

Source code in pelinker/selected_hyperparameters.py
@dataclass(frozen=True)
class SelectedHyperparameters:
    """What a search stage chose, and the conditions it chose them under."""

    source: str
    """Which stage wrote this (``model_selection`` / ``dim_selection``)."""
    model: str
    layer: str
    pca_components: int
    umap_dim: int
    min_cluster_size: int
    manifold_kind: str
    """Coordinates the search ran on. ``pelinker-fit`` refuses a fit whose
    ``predict_mode`` implies a different manifold, rather than silently transferring
    ``min_cluster_size`` across a coordinate-system change."""
    n_rows_realized: int | None = None
    """Mention rows the winning configuration was actually scored on."""
    umap_n_neighbors: int | None = None
    run_fingerprint: str | None = None
    outer_score: float | None = None

    def __post_init__(self) -> None:
        if not self.model:
            raise ValueError("model must be a non-empty string")
        if self.pca_components < 1:
            raise ValueError("pca_components must be >= 1")
        if self.umap_dim < 2:
            raise ValueError("umap_dim must be >= 2")
        if self.min_cluster_size < 2:
            raise ValueError("min_cluster_size must be >= 2")
        if self.manifold_kind not in ("umap", "parametric"):
            raise ValueError(
                f"manifold_kind must be 'umap' or 'parametric', got {self.manifold_kind!r}"
            )

    def to_jsonable(self) -> dict[str, Any]:
        return {
            "schema": SELECTED_HYPERPARAMETERS_SCHEMA,
            "source": self.source,
            "model": self.model,
            "layer": self.layer,
            "pca_components": int(self.pca_components),
            "umap_dim": int(self.umap_dim),
            "min_cluster_size": int(self.min_cluster_size),
            "manifold_kind": self.manifold_kind,
            "n_rows_realized": (
                None if self.n_rows_realized is None else int(self.n_rows_realized)
            ),
            "umap_n_neighbors": (
                None if self.umap_n_neighbors is None else int(self.umap_n_neighbors)
            ),
            "run_fingerprint": self.run_fingerprint,
            "outer_score": (
                None if self.outer_score is None else float(self.outer_score)
            ),
        }

    @staticmethod
    def from_jsonable(data: dict[str, Any]) -> SelectedHyperparameters:
        schema = data.get("schema")
        if schema != SELECTED_HYPERPARAMETERS_SCHEMA:
            raise ValueError(
                f"expected schema {SELECTED_HYPERPARAMETERS_SCHEMA!r}, got {schema!r}"
            )
        return SelectedHyperparameters(
            source=str(data.get("source", "")),
            model=str(data["model"]),
            layer=str(data["layer"]),
            pca_components=int(data["pca_components"]),
            umap_dim=int(data["umap_dim"]),
            min_cluster_size=int(data["min_cluster_size"]),
            manifold_kind=str(data["manifold_kind"]),
            n_rows_realized=(
                None
                if data.get("n_rows_realized") is None
                else int(data["n_rows_realized"])
            ),
            umap_n_neighbors=(
                None
                if data.get("umap_n_neighbors") is None
                else int(data["umap_n_neighbors"])
            ),
            run_fingerprint=data.get("run_fingerprint"),
            outer_score=(
                None if data.get("outer_score") is None else float(data["outer_score"])
            ),
        )

manifold_kind instance-attribute

Coordinates the search ran on. pelinker-fit refuses a fit whose predict_mode implies a different manifold, rather than silently transferring min_cluster_size across a coordinate-system change.

n_rows_realized = None class-attribute instance-attribute

Mention rows the winning configuration was actually scored on.

source instance-attribute

Which stage wrote this (model_selection / dim_selection).

load_selected_hyperparameters(path)

Read a handoff file, or the file of that name inside a report directory.

Source code in pelinker/selected_hyperparameters.py
def load_selected_hyperparameters(
    path: pathlib.Path | str,
) -> SelectedHyperparameters:
    """Read a handoff file, or the file of that name inside a report directory."""
    p = pathlib.Path(path).expanduser()
    if p.is_dir():
        p = p / SELECTED_HYPERPARAMETERS_BASENAME
    return SelectedHyperparameters.from_jsonable(
        json.loads(p.read_text(encoding="utf-8"))
    )

write_selected_hyperparameters(selected, report_path)

Write selected_hyperparameters.json under report_path (atomic).

Source code in pelinker/selected_hyperparameters.py
def write_selected_hyperparameters(
    selected: SelectedHyperparameters, report_path: pathlib.Path
) -> pathlib.Path:
    """Write ``selected_hyperparameters.json`` under ``report_path`` (atomic)."""
    report_path = pathlib.Path(report_path).expanduser()
    report_path.mkdir(parents=True, exist_ok=True)
    out = report_path / SELECTED_HYPERPARAMETERS_BASENAME
    tmp = out.with_suffix(out.suffix + ".tmp")
    tmp.write_text(
        json.dumps(selected.to_jsonable(), indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )
    tmp.replace(out)
    return out