Skip to content

Telemetry

Structured telemetry on polars and hive-partitioned parquet: logs, run and artifact indexes, and sweep progress.

femforge.telemetry

Structured telemetry on polars and hive-partitioned parquet: logs, run and artifact indexes, sweep progress.

Modules:

  • artifacts –

    The artifact index: store sidecars read into parquet by directory mtime, with a fluent query over them.

  • cli –

    The telemetry half of the femforge command: build the indexes, list runs and sweeps, query the logs, compact the store.

  • context –

    The sweep point a thread or task is working on, carried on a contextvar and bound onto every log record it emits.

  • convergence –

    One run's convergence log read into a frame, with the sif-authored targets that give its progress a meaning.

  • hive –

    Append-only hive-partitioned parquet: one shard per writer per partition, never rewritten under a reader.

  • journal –

    The write-ahead half of the store: one JSONL journal per process, flushed per record and checkpointed into parquet.

  • paths –

    Where telemetry lives: the root resolution rule and the typed hive stores under it.

  • runs –

    The run index: bundles read into parquet by manifest mtime, a fluent query over them, and the live overlay from the run locks.

  • schemas –

    The polars schemas of every telemetry table, with column names single-sourced as StrEnums.

  • sink –

    The loguru sink that lands every record in parquet, plus the query surface over what it wrote.

  • sweep –

    Sweep progress: one row per launch, one journalled event per point transition, and the query that reads them back.

  • tail –

    Incremental reads of a growing text file, so a viewer re-reads only the bytes it has not seen.

  • tap –

    An in-process live view of the log stream: a bounded ring buffer plus asyncio fan-out to the workbench.

Classes:

  • ArtifactIndex –

    The parquet index of one local artifact store: an incremental update over its committed directories, plus query.

  • ArtifactQuery –

    A fluent filter over the artifact index; each method returns a new query and nothing runs until frame.

  • SweepPoint –

    Which sweep, and which point of it, the current context belongs to.

  • ConvergenceTargetsFile –

    The iteration caps and tolerances one run's sif authored, written beside its convergence log so the log reads in context.

  • LoopTarget –

    One solver loop's authored iteration cap and tolerance, in the form the bundle persists them.

  • HiveStore –

    One hive-partitioned parquet table: a typed scan, an atomic append of one shard per partition, and a settling compact.

  • Journal –

    A process's own append-only JSONL journal: every record is written and flushed immediately, then folded into parquet by checkpoint.

  • Telemetry –

    A telemetry root and the typed hive store for each table under it.

  • TelemetryDir –

    The subdirectories of a telemetry root: one per table, plus the journal directory the live tables are tailed from.

  • TelemetryRootMissing –

    Neither an explicit path nor FEMFORGE_TELEMETRY names a telemetry root.

  • RunIndex –

    The parquet index of one runs directory: an incremental update over its bundles, the record a driver appends per run, and query.

  • RunQuery –

    A fluent filter over the run index; each method returns a new query and nothing runs until frame.

  • ArtifactColumn –

    The artifact index's columns: one row per stored artifact, keyed by key.

  • ConvergenceColumn –

    The keys elmer.runtime.solve writes into convergence.jsonl, one sample per solver iteration.

  • ConvergenceLoop –

    Which Elmer iteration loop a convergence sample belongs to: nonlinear or steady state.

  • LiveRunColumn –

    The columns of the in-flight run table built from the run lock owners; never persisted.

  • LogColumn –

    The log table's columns; the sink emits these names and every query selects by them.

  • RunColumn –

    The run index's columns: one row per run bundle, keyed by run_id.

  • RunSource –

    How a run index row was produced: a filesystem scan of the bundles, or the Driver reporting live.

  • SweepColumn –

    The sweep table's columns: one row per sweep launch.

  • SweepEventColumn –

    The sweep event table's columns: one row per point state transition.

  • SweepState –

    The lifecycle states a sweep point moves through; each transition is one sweep event row.

  • LogQuery –

    A fluent filter over the log frame; each method returns a new query and nothing runs until page.

  • ParquetLogSink –

    Loguru sink writing every record to a JSONL journal at once and to hive-partitioned parquet in batches.

  • SweepJournal –

    One sweep's writer: the launch and closing rows in the sweeps table, every point transition through this process's own journal.

  • SweepQuery –

    One sweep read back: its launch row, every point transition (settled shards plus live journals), and the progress summary.

  • SweepSummary –

    Where a sweep stands: how many points sit in each state, how fast they are landing, and how long the rest should take.

  • TextTail –

    A byte-offset cursor over one growing text file; a truncation rewinds it and an unterminated last line is held back until it ends.

  • LiveLogTap –

    Loguru sink keeping the last records in memory and pushing each new one onto every asyncio subscriber queue.

Functions:

  • current_sweep_point –

    The sweep point in scope, or None outside any sweep_point block.

  • sweep_point –

    Mark this context as one sweep point, binding both ids onto every log record emitted inside it.

  • scan_convergence –

    A run's convergence.jsonl as a typed frame, carrying each loop's first relative change and how far it has come toward its tolerance.

  • coerce –

    Cast a loosely-typed frame (JSON text, a partial row set) onto schema, parsing date/time strings and filling missing columns with nulls.

  • journal_glob –

    The glob matching every process's journal for the table name.

  • recover_journals –

    Checkpoint and delete the journals of processes that are gone, so a crash costs no records; returns the journals recovered.

  • scan_journals –

    A frame over every matching journal's records, coerced onto schema; lines that are not JSON records are skipped.

  • require_root –

    The telemetry root a reader needs, or TelemetryRootMissing naming both ways to provide one.

  • resolve_root –

    The telemetry root: explicit if given, else FEMFORGE_TELEMETRY, else None because telemetry is off.

  • live_runs –

    The runs claimed right now under runs_dir, read from each <run_id>.lock/owner.json with the age of its heartbeat.

  • scan_logs –

    Every log record: the settled parquet shards unioned with what is still only in the live journals.

  • sweep_runs –

    Every sweep the root knows, newest first and one row each (a finished sweep's closing row wins).

Attributes:

  • TELEMETRY_ENV –

    The env var naming the telemetry root; unset and unpassed means telemetry is off and polars is never imported.

  • ARTIFACT_INDEX_SCHEMA –

    One row per artifact.json sidecar; the tool-version columns mirror ToolVersions.

  • CONVERGENCE_SCHEMA –

    Read straight from a bundle's convergence.jsonl; per-run and never centralised, so it has no partitions.

  • LIVE_RUN_SCHEMA –

    Built per call from the owner.json of each runs/<run_id>.lock, so it is always current rather than stored.

  • LOG_SCHEMA –

    One row per log record; extra is the JSON of everything bound that has no column of its own.

  • RUN_INDEX_SCHEMA –

    One row per run bundle; the index re-reads a bundle only when manifest_mtime moves, and dedupes on run_id.

  • SWEEP_EVENT_SCHEMA –

    Append-only point transitions; the latest state per point_key is the sweep's live progress.

  • SWEEP_RUN_SCHEMA –

    One row per sweep; a row is rewritten (and deduped on sweep_id) when the sweep finishes.

TELEMETRY_ENV module-attribute

TELEMETRY_ENV = 'FEMFORGE_TELEMETRY'

The env var naming the telemetry root; unset and unpassed means telemetry is off and polars is never imported.

ARTIFACT_INDEX_SCHEMA module-attribute

ARTIFACT_INDEX_SCHEMA = pl.Schema({ArtifactColumn.KIND: pl.String, ArtifactColumn.KEY: pl.String, ArtifactColumn.NAME: pl.String, ArtifactColumn.STORE_ROOT: pl.String, ArtifactColumn.CREATED_AT: TIMESTAMP, ArtifactColumn.SPEC_TYPE: pl.String, ArtifactColumn.PRODUCER_VERSION: pl.Int32, ArtifactColumn.PRODUCER_HOST: pl.String, ArtifactColumn.PRODUCER_PID: pl.Int32, ArtifactColumn.FILE_COUNT: pl.Int32, ArtifactColumn.TOTAL_BYTES: pl.Int64, ArtifactColumn.FILES: pl.List(pl.String), ArtifactColumn.SPEC: pl.String, ArtifactColumn.FEMFORGE: pl.String, ArtifactColumn.CODE_SHA: pl.String, ArtifactColumn.GMSH: pl.String, ArtifactColumn.ELMER: pl.String, ArtifactColumn.DIR_MTIME: TIMESTAMP, ArtifactColumn.INDEXED_AT: TIMESTAMP})

One row per artifact.json sidecar; the tool-version columns mirror ToolVersions.

CONVERGENCE_SCHEMA module-attribute

CONVERGENCE_SCHEMA = pl.Schema({ConvergenceColumn.WALL_TIME: pl.Float64, ConvergenceColumn.SOLVER: pl.String, ConvergenceColumn.LOOP: pl.Enum(categories(ConvergenceLoop)), ConvergenceColumn.ITER: pl.Int32, ConvergenceColumn.NRM: pl.Float64, ConvergenceColumn.RELC: pl.Float64})

Read straight from a bundle's convergence.jsonl; per-run and never centralised, so it has no partitions.

LIVE_RUN_SCHEMA module-attribute

LIVE_RUN_SCHEMA = pl.Schema({LiveRunColumn.RUN_ID: pl.String, LiveRunColumn.HOST: pl.String, LiveRunColumn.PID: pl.Int32, LiveRunColumn.STARTED_AT: TIMESTAMP, LiveRunColumn.HEARTBEAT_AT: TIMESTAMP, LiveRunColumn.HEARTBEAT_AGE_S: pl.Float64, LiveRunColumn.STALE: pl.Boolean})

Built per call from the owner.json of each runs/<run_id>.lock, so it is always current rather than stored.

LOG_SCHEMA module-attribute

LOG_SCHEMA = pl.Schema({LogColumn.DATE: pl.Date, LogColumn.PROCESS: pl.String, LogColumn.TIME: TIMESTAMP, LogColumn.LEVEL: pl.Enum(categories(LogLevel)), LogColumn.LEVEL_NO: pl.Int16, LogColumn.NAME: pl.String, LogColumn.FUNCTION: pl.String, LogColumn.LINE: pl.Int32, LogColumn.MESSAGE: pl.String, LogColumn.RUN_ID: pl.String, LogColumn.SWEEP_ID: pl.String, LogColumn.POINT_KEY: pl.String, LogColumn.HOST: pl.String, LogColumn.PID: pl.Int32, LogColumn.THREAD: pl.String, LogColumn.EXCEPTION: pl.String, LogColumn.EXTRA: pl.String})

One row per log record; extra is the JSON of everything bound that has no column of its own.

RUN_INDEX_SCHEMA module-attribute

RUN_INDEX_SCHEMA = pl.Schema({RunColumn.MONTH: pl.Date, RunColumn.RUN_ID: pl.String, RunColumn.CACHE_KEY: pl.String, RunColumn.RUNS_DIR: pl.String, RunColumn.STATUS: pl.Enum(literal_categories(ManifestStatus)), RunColumn.CREATED_AT: TIMESTAMP, RunColumn.FINISHED_AT: TIMESTAMP, RunColumn.CURRENT_STAGE: pl.String, RunColumn.STAGES_DONE: pl.Int32, RunColumn.GIT_SHA: pl.String, RunColumn.ELMER_VERSION: pl.String, RunColumn.PHYSICS: pl.String, RunColumn.ELEMENT_COUNT: pl.Int64, RunColumn.MIN_ELEMENT_SIZE: pl.Float64, RunColumn.MAX_ELEMENT_SIZE: pl.Float64, RunColumn.DESCRIPTION: pl.String, RunColumn.TAGS: pl.String, RunColumn.NOTES: pl.String, RunColumn.MESH_NAME: pl.String, RunColumn.MESH_KEY: pl.String, RunColumn.PUBLISHED: pl.Boolean, RunColumn.HAS_RESULT: pl.Boolean, RunColumn.HAS_VTU: pl.Boolean, RunColumn.CLEANUPS: pl.Int32, RunColumn.BUNDLE_BYTES: pl.Int64, RunColumn.SWEEP_ID: pl.String, RunColumn.POINT_KEY: pl.String, RunColumn.MANIFEST_MTIME: TIMESTAMP, RunColumn.INDEXED_AT: TIMESTAMP, RunColumn.SOURCE: pl.Enum(categories(RunSource)), RunColumn.MODEL_DIM: pl.Int8, RunColumn.ARTIFACT_KEYS: pl.List(pl.String), RunColumn.FORCE_Y: pl.Float64, RunColumn.TEMPERATURE_MAX: pl.Float64, RunColumn.WALL_TIME: pl.Float64})

One row per run bundle; the index re-reads a bundle only when manifest_mtime moves, and dedupes on run_id.

SWEEP_EVENT_SCHEMA module-attribute

SWEEP_EVENT_SCHEMA = pl.Schema({SweepEventColumn.SWEEP_ID: pl.String, SweepEventColumn.POINT_KEY: pl.String, SweepEventColumn.POINT_NAME: pl.String, SweepEventColumn.STATE: pl.Enum(categories(SweepState)), SweepEventColumn.AT: TIMESTAMP, SweepEventColumn.HOST: pl.String, SweepEventColumn.PID: pl.Int32, SweepEventColumn.ORIGIN: pl.String, SweepEventColumn.ERROR: pl.String, SweepEventColumn.DURATION_S: pl.Float64})

Append-only point transitions; the latest state per point_key is the sweep's live progress.

SWEEP_RUN_SCHEMA module-attribute

SWEEP_RUN_SCHEMA = pl.Schema({SweepColumn.MONTH: pl.Date, SweepColumn.SWEEP_ID: pl.String, SweepColumn.DESCRIPTION: pl.String, SweepColumn.STARTED_AT: TIMESTAMP, SweepColumn.FINISHED_AT: TIMESTAMP, SweepColumn.TOTAL: pl.Int32, SweepColumn.SKIPPED: pl.Int32, SweepColumn.MAX_WORKERS: pl.Int32, SweepColumn.HOST: pl.String, SweepColumn.PID: pl.Int32, SweepColumn.PROCESS: pl.String, SweepColumn.GIT_SHA: pl.String, SweepColumn.INDEXED_AT: TIMESTAMP})

One row per sweep; a row is rewritten (and deduped on sweep_id) when the sweep finishes.

ArtifactIndex

ArtifactIndex(telemetry: Telemetry, store: LocalArtifactStore)

The parquet index of one local artifact store: an incremental update over its committed directories, plus query.

Methods:

  • update –

    Index every artifact directory whose mtime moved since the last pass — all of them when rebuild — and return the rows appended.

  • query –

    A fluent query over the index, reading no sidecar until records asks for one.

Source code in src/femforge/telemetry/artifacts.py
def __init__(self, telemetry: Telemetry, store: LocalArtifactStore) -> None:
    """Track the committed artifacts of ``store`` in the artifact table of ``telemetry``."""
    self.telemetry: Telemetry = telemetry
    self.store: LocalArtifactStore = store
    self.hive = telemetry.artifacts

update

update(*, rebuild: bool = False) -> int

Index every artifact directory whose mtime moved since the last pass — all of them when rebuild — and return the rows appended.

Source code in src/femforge/telemetry/artifacts.py
def update(self, *, rebuild: bool = False) -> int:
    """Index every artifact directory whose mtime moved since the last pass — all of them when ``rebuild`` — and return the rows appended."""
    candidates: list[tuple[str, Path, datetime]] = self._candidates()
    if not candidates:
        return 0

    pending = candidates if rebuild else self._changed(candidates)

    rows: list[dict[str, object]] = []
    for _, directory, mtime in pending:
        artifact: StoredArtifact | None = self.store.read(directory)
        if artifact is None:
            continue

        rows.append(artifact_row(artifact, store_root=self.store.root, dir_mtime=mtime))

    if not rows:
        return 0

    _ = self.hive.append(pl.DataFrame(rows, schema=ARTIFACT_INDEX_SCHEMA))

    return len(rows)

query

query() -> ArtifactQuery

A fluent query over the index, reading no sidecar until records asks for one.

Source code in src/femforge/telemetry/artifacts.py
def query(self) -> ArtifactQuery:
    """A fluent query over the index, reading no sidecar until ``records`` asks for one."""
    return ArtifactQuery(self.hive.scan(), self.store)

ArtifactQuery dataclass

ArtifactQuery(lazy: LazyFrame, store: LocalArtifactStore, limit: int | None = None)

A fluent filter over the artifact index; each method returns a new query and nothing runs until frame.

Methods:

  • where –

    A new query narrowed by predicate; the other filters are named shorthands for this.

  • kind –

    Artifacts of any of kinds.

  • spec_type –

    Artifacts whose spec model's module:QualName ends with name, so the bare class name is enough.

  • since –

    Artifacts published at or after when.

  • key_prefix –

    Artifacts whose key starts with prefix.

  • newest –

    Keep only the count most recently published artifacts, applied after the duplicate rows collapse.

  • frame –

    The matching rows, newest first, one per stored artifact (the latest row wins).

  • records –

    The matching artifacts read back from the store, skipping any whose directory is gone.

Attributes:

  • lazy (LazyFrame) –

    The artifact index rows the query filters.

  • store (LocalArtifactStore) –

    The store records reads the matching artifacts back from.

  • limit (int | None) –

    How many of the newest artifacts to keep; None keeps them all.

lazy instance-attribute

lazy: LazyFrame

The artifact index rows the query filters.

store instance-attribute

The store records reads the matching artifacts back from.

limit class-attribute instance-attribute

limit: int | None = None

How many of the newest artifacts to keep; None keeps them all.

where

where(predicate: Expr) -> ArtifactQuery

A new query narrowed by predicate; the other filters are named shorthands for this.

Source code in src/femforge/telemetry/artifacts.py
def where(self, predicate: pl.Expr) -> ArtifactQuery:
    """A new query narrowed by ``predicate``; the other filters are named shorthands for this."""
    return replace(self, lazy=self.lazy.filter(predicate))

kind

kind(*kinds: str) -> ArtifactQuery

Artifacts of any of kinds.

Source code in src/femforge/telemetry/artifacts.py
def kind(self, *kinds: str) -> ArtifactQuery:
    """Artifacts of any of ``kinds``."""
    return self.where(pl.col(ArtifactColumn.KIND).is_in(list(kinds)))

spec_type

spec_type(name: str) -> ArtifactQuery

Artifacts whose spec model's module:QualName ends with name, so the bare class name is enough.

Source code in src/femforge/telemetry/artifacts.py
def spec_type(self, name: str) -> ArtifactQuery:
    """Artifacts whose spec model's ``module:QualName`` ends with ``name``, so the bare class name is enough."""
    return self.where(pl.col(ArtifactColumn.SPEC_TYPE).str.ends_with(name))

since

since(when: datetime) -> ArtifactQuery

Artifacts published at or after when.

Source code in src/femforge/telemetry/artifacts.py
def since(self, when: datetime) -> ArtifactQuery:
    """Artifacts published at or after ``when``."""
    return self.where(pl.col(ArtifactColumn.CREATED_AT) >= when)

key_prefix

key_prefix(prefix: str) -> ArtifactQuery

Artifacts whose key starts with prefix.

Source code in src/femforge/telemetry/artifacts.py
def key_prefix(self, prefix: str) -> ArtifactQuery:
    """Artifacts whose key starts with ``prefix``."""
    return self.where(pl.col(ArtifactColumn.KEY).str.starts_with(prefix))

newest

newest(count: int) -> ArtifactQuery

Keep only the count most recently published artifacts, applied after the duplicate rows collapse.

Source code in src/femforge/telemetry/artifacts.py
def newest(self, count: int) -> ArtifactQuery:
    """Keep only the ``count`` most recently published artifacts, applied after the duplicate rows collapse."""
    return replace(self, limit=count)

frame

frame() -> DataFrame

The matching rows, newest first, one per stored artifact (the latest row wins).

Source code in src/femforge/telemetry/artifacts.py
def frame(self) -> pl.DataFrame:
    """The matching rows, newest first, one per stored artifact (the latest row wins)."""
    deduped = self.lazy.sort(ArtifactColumn.INDEXED_AT, maintain_order=True).unique(subset=[ArtifactColumn.KIND, ArtifactColumn.KEY], keep="last", maintain_order=True)
    ordered = deduped.sort(ArtifactColumn.CREATED_AT, descending=True, nulls_last=True, maintain_order=True)

    return (ordered.head(self.limit) if self.limit is not None else ordered).collect()

records

records() -> list[StoredArtifact]

The matching artifacts read back from the store, skipping any whose directory is gone.

Source code in src/femforge/telemetry/artifacts.py
def records(self) -> list[StoredArtifact]:
    """The matching artifacts read back from the store, skipping any whose directory is gone."""
    rows: pl.DataFrame = self.frame()
    roots: list[str] = cast("list[str]", rows[ArtifactColumn.STORE_ROOT].to_list())
    kinds: list[str] = cast("list[str]", rows[ArtifactColumn.KIND].to_list())
    names: list[str] = cast("list[str]", rows[ArtifactColumn.NAME].to_list())

    found: list[StoredArtifact] = []
    for root, kind, name in zip(roots, kinds, names, strict=True):
        artifact: StoredArtifact | None = self.store.read(Path(root) / kind / name)
        if artifact is None:
            continue

        found.append(artifact)

    return found

SweepPoint

Bases: NamedTuple

Which sweep, and which point of it, the current context belongs to.

Attributes:

sweep_id instance-attribute

sweep_id: str

The sweep's id.

point_key instance-attribute

point_key: str

The point's key within the sweep.

ConvergenceTargetsFile pydantic-model

Bases: BaseModel

The iteration caps and tolerances one run's sif authored, written beside its convergence log so the log reads in context.

Config:

  • use_attribute_docstrings: True

Fields:

loops pydantic-field

loops: list[LoopTarget]

One entry per (solver, loop) the solved stack authored targets for.

of classmethod

of(targets: ConvergenceTargets) -> Self

The persistable form of the mapping convergence_targets builds from a solver stack.

Source code in src/femforge/telemetry/convergence.py
@classmethod
def of(cls, targets: ConvergenceTargets) -> Self:
    """The persistable form of the mapping ``convergence_targets`` builds from a solver stack."""
    loops: list[LoopTarget] = []
    for (solver, loop), target in targets.items():
        loops.append(
            LoopTarget(
                solver=solver,
                loop=ConvergenceLoop(loop),
                max_iterations=target.max_iterations,
                tolerance=target.tolerance,
            )
        )

    return cls(loops=loops)

load classmethod

load(path: str | Path) -> Self

Load a bundle's convergence_targets.json (pass the bundle directory or the file itself); a bundle without one reads as no targets.

Source code in src/femforge/telemetry/convergence.py
@classmethod
def load(cls, path: str | Path) -> Self:
    """Load a bundle's ``convergence_targets.json`` (pass the bundle directory or the file itself); a bundle without one reads as no targets."""
    file: Path = Path(path)
    if file.is_dir():
        file = file / BundleFile.CONVERGENCE_TARGETS

    if not file.is_file():
        return cls()

    return cls.model_validate_json(file.read_text(encoding="utf-8"))

write

write(sim_dir: str | Path) -> Path

Write the targets into sim_dir as convergence_targets.json and return the path.

Source code in src/femforge/telemetry/convergence.py
def write(self, sim_dir: str | Path) -> Path:
    """Write the targets into ``sim_dir`` as ``convergence_targets.json`` and return the path."""
    path: Path = Path(sim_dir) / BundleFile.CONVERGENCE_TARGETS
    write_atomic(path, self.model_dump_json(indent=2))

    return path

targets

targets() -> ConvergenceTargets

The renderer's mapping again, keyed the way Elmer tags ComputeChange lines.

Source code in src/femforge/telemetry/convergence.py
def targets(self) -> ConvergenceTargets:
    """The renderer's mapping again, keyed the way Elmer tags ComputeChange lines."""
    mapping: dict[tuple[str, Literal["NS", "SS"]], LoopTargets] = {}
    for entry in self.loops:
        loop: Literal["NS", "SS"] = "NS" if entry.loop is ConvergenceLoop.NS else "SS"
        mapping[(normalize_solver_key(entry.solver), loop)] = LoopTargets(max_iterations=entry.max_iterations, tolerance=entry.tolerance)

    return mapping

LoopTarget pydantic-model

Bases: BaseModel

One solver loop's authored iteration cap and tolerance, in the form the bundle persists them.

Config:

  • use_attribute_docstrings: True

Fields:

solver pydantic-field

solver: str

The sif Equation name Elmer tags this loop's ComputeChange lines with, normalized.

loop pydantic-field

Which loop the targets apply to: the nonlinear inner loop or the steady-state outer one.

max_iterations pydantic-field

max_iterations: int | None = None

The loop's iteration cap; None when the sif left Elmer's default.

tolerance pydantic-field

tolerance: float | None = None

Δ_tol [-]: The relative-change tolerance the loop converges to; None when the sif left Elmer's default.

HiveStore

HiveStore(root: Path, schema: Schema, partition_keys: Sequence[str])

One hive-partitioned parquet table: a typed scan, an atomic append of one shard per partition, and a settling compact.

Parameters:

  • root (Path) –

    The table's directory, holding one directory level per partition key.

  • schema (Schema) –

    Every column of the table, partition columns included.

  • partition_keys (Sequence[str]) –

    The columns whose values name the partition directories, outermost first.

Methods:

  • shards –

    Every shard file under the root, in sorted order.

  • partitions –

    The directories holding shards, one per distinct partition-key combination.

  • scan –

    A lazy frame over every shard, each typed onto the schema, or a typed empty frame when nothing has been written.

  • append –

    Write frame as one new shard per partition and return the shard paths; existing shards are untouched.

  • compact –

    Fold each partition's settled shards into one merged shard and delete the originals; returns the merged shards.

Attributes:

  • hive_schema (Schema) –

    The partition columns, whose values live in the directory names rather than the file bodies.

  • body_schema (Schema) –

    The columns actually stored inside each shard: everything but the partition keys.

Source code in src/femforge/telemetry/hive.py
def __init__(self, root: Path, schema: pl.Schema, partition_keys: Sequence[str]) -> None:
    """Address the table stored under ``root``; nothing touches disk until a read or write.

    Args:
        root: The table's directory, holding one directory level per partition key.
        schema: Every column of the table, partition columns included.
        partition_keys: The columns whose values name the partition directories, outermost first.
    """
    self.root: Path = root
    self.schema: pl.Schema = schema
    self.partition_keys: tuple[str, ...] = tuple(str(key) for key in partition_keys)

hive_schema property

hive_schema: Schema

The partition columns, whose values live in the directory names rather than the file bodies.

body_schema property

body_schema: Schema

The columns actually stored inside each shard: everything but the partition keys.

shards

shards() -> list[Path]

Every shard file under the root, in sorted order.

Source code in src/femforge/telemetry/hive.py
def shards(self) -> list[Path]:
    """Every shard file under the root, in sorted order."""
    if not self.root.is_dir():
        return []

    return sorted(self.root.rglob("*" + SHARD_SUFFIX))

partitions

partitions() -> list[Path]

The directories holding shards, one per distinct partition-key combination.

Source code in src/femforge/telemetry/hive.py
def partitions(self) -> list[Path]:
    """The directories holding shards, one per distinct partition-key combination."""
    directories = {shard.parent for shard in self.shards()}

    return sorted(directories)

scan

scan() -> LazyFrame

A lazy frame over every shard, each typed onto the schema, or a typed empty frame when nothing has been written.

Source code in src/femforge/telemetry/hive.py
def scan(self) -> pl.LazyFrame:
    """A lazy frame over every shard, each typed onto the schema, or a typed empty frame when nothing has been written."""
    shards = self.shards()
    if not shards:
        return pl.LazyFrame(schema=self.schema)

    # polars refuses to unify Enum columns whose categories differ, so only shards of one schema share a scan, and each run is typed before they meet.
    typed = [coerce(pl.scan_parquet(paths, hive_partitioning=True, hive_schema=self.hive_schema), self.schema) for paths in schema_runs(shards)]

    return pl.concat(typed, how="vertical")

append

append(frame: DataFrame) -> list[Path]

Write frame as one new shard per partition and return the shard paths; existing shards are untouched.

Source code in src/femforge/telemetry/hive.py
def append(self, frame: pl.DataFrame) -> list[Path]:
    """Write ``frame`` as one new shard per partition and return the shard paths; existing shards are untouched."""
    if frame.is_empty():
        return []

    typed = frame.cast(self.schema).select(self.schema.names())
    groups = cast("dict[tuple[object, ...], pl.DataFrame]", typed.partition_by(self.partition_keys, as_dict=True, include_key=False))

    written: list[Path] = []
    for values, body in groups.items():
        segments = (f"{key}={value}" for key, value in zip(self.partition_keys, values, strict=True))
        written.append(write_shard(self.root.joinpath(*segments), body))

    return written

compact

compact(settle_s: float = SETTLE_S) -> list[Path]

Fold each partition's settled shards into one merged shard and delete the originals; returns the merged shards.

Source code in src/femforge/telemetry/hive.py
def compact(self, settle_s: float = SETTLE_S) -> list[Path]:
    """Fold each partition's settled shards into one merged shard and delete the originals; returns the merged shards."""
    cutoff = time.time() - settle_s

    merged: list[Path] = []
    for directory in self.partitions():
        shards = sorted(directory.glob("*" + SHARD_SUFFIX))
        settled = [shard for shard in shards if shard.stat().st_mtime <= cutoff]
        if len(settled) < 2:
            continue

        typed = [coerce(pl.scan_parquet(paths), self.body_schema) for paths in schema_runs(settled)]
        body = pl.concat(typed, how="vertical").collect()
        merged.append(write_shard(directory, body))

        for shard in settled:
            shard.unlink()

    return merged

Journal

Journal(live_dir: Path, name: str)

A process's own append-only JSONL journal: every record is written and flushed immediately, then folded into parquet by checkpoint.

Methods:

  • write –

    Append one record and flush it, so a reader tailing the journal sees it before the next parquet flush.

  • checkpoint –

    Append the journal's records to store and truncate it; returns the shards written.

  • close –

    Close the journal handle and delete the (now empty) file; idempotent.

Source code in src/femforge/telemetry/journal.py
def __init__(self, live_dir: Path, name: str) -> None:
    """Open this process's journal for the table ``name`` under ``live_dir``, creating the directory when needed."""
    live_dir.mkdir(parents=True, exist_ok=True)

    self.path: Path = live_dir / f"{name}-{HOST}-{os.getpid()}{JOURNAL_SUFFIX}"
    self._lock: threading.Lock = threading.Lock()
    self._handle = self.path.open("a", encoding="utf-8", newline="\n")

write

write(row: Mapping[str, object]) -> None

Append one record and flush it, so a reader tailing the journal sees it before the next parquet flush.

Source code in src/femforge/telemetry/journal.py
def write(self, row: Mapping[str, object]) -> None:
    """Append one record and flush it, so a reader tailing the journal sees it before the next parquet flush."""
    line = json.dumps(row, default=json_default)

    with self._lock:
        if self._handle.closed:
            return

        _ = self._handle.write(line + "\n")
        self._handle.flush()

checkpoint

checkpoint(store: HiveStore) -> list[Path]

Append the journal's records to store and truncate it; returns the shards written.

Source code in src/femforge/telemetry/journal.py
def checkpoint(self, store: HiveStore) -> list[Path]:
    """Append the journal's records to ``store`` and truncate it; returns the shards written."""
    with self._lock:
        if self._handle.closed or self.path.stat().st_size == 0:
            return []

        rows = scan_journals(self.path.parent, self.path.name, store.schema).collect()
        written = store.append(rows)
        _ = self._handle.truncate(0)
        _ = self._handle.seek(0)

        return written

close

close() -> None

Close the journal handle and delete the (now empty) file; idempotent.

Source code in src/femforge/telemetry/journal.py
def close(self) -> None:
    """Close the journal handle and delete the (now empty) file; idempotent."""
    with self._lock:
        if self._handle.closed:
            return

        self._handle.close()

    self.path.unlink(missing_ok=True)

Telemetry

Telemetry(root: str | Path)

A telemetry root and the typed hive store for each table under it.

Methods:

  • stores –

    Every table under this root, so a maintenance pass reaches them all without naming them one by one.

Attributes:

  • live (Path) –

    The directory holding each process's write-ahead JSONL journal.

  • logs (HiveStore) –

    Every log record, partitioned by date and process.

  • runs (HiveStore) –

    The run index, partitioned by the month a run was created in.

  • artifacts (HiveStore) –

    The artifact index, partitioned by artifact kind.

  • sweeps (HiveStore) –

    A launch row per sweep plus a closing row once it finishes, partitioned by the month it started in.

  • sweep_events (HiveStore) –

    Per-point state transitions, partitioned by sweep so one sweep's progress reads one directory.

Source code in src/femforge/telemetry/paths.py
def __init__(self, root: str | Path) -> None:
    """Address the tables under the telemetry root ``root``."""
    self.root: Path = Path(root)

live property

live: Path

The directory holding each process's write-ahead JSONL journal.

logs cached property

logs: HiveStore

Every log record, partitioned by date and process.

runs cached property

runs: HiveStore

The run index, partitioned by the month a run was created in.

artifacts cached property

artifacts: HiveStore

The artifact index, partitioned by artifact kind.

sweeps cached property

sweeps: HiveStore

A launch row per sweep plus a closing row once it finishes, partitioned by the month it started in.

sweep_events cached property

sweep_events: HiveStore

Per-point state transitions, partitioned by sweep so one sweep's progress reads one directory.

stores

stores() -> list[HiveStore]

Every table under this root, so a maintenance pass reaches them all without naming them one by one.

Source code in src/femforge/telemetry/paths.py
def stores(self) -> list[HiveStore]:
    """Every table under this root, so a maintenance pass reaches them all without naming them one by one."""
    return [self.logs, self.runs, self.artifacts, self.sweeps, self.sweep_events]

TelemetryDir

Bases: StrEnum

The subdirectories of a telemetry root: one per table, plus the journal directory the live tables are tailed from.

Attributes:

  • LOGS –

    The log table.

  • RUNS –

    The run index.

  • ARTIFACTS –

    The artifact index.

  • SWEEPS –

    The sweep table: a launch row per sweep, plus a closing row once it finishes.

  • SWEEP_EVENTS –

    The sweep event table: one row per point transition.

  • LIVE –

    The write-ahead journals the live tables are tailed from.

LOGS class-attribute instance-attribute

LOGS = 'logs'

The log table.

RUNS class-attribute instance-attribute

RUNS = 'runs'

The run index.

ARTIFACTS class-attribute instance-attribute

ARTIFACTS = 'artifacts'

The artifact index.

SWEEPS class-attribute instance-attribute

SWEEPS = 'sweeps'

The sweep table: a launch row per sweep, plus a closing row once it finishes.

SWEEP_EVENTS class-attribute instance-attribute

SWEEP_EVENTS = 'sweep_events'

The sweep event table: one row per point transition.

LIVE class-attribute instance-attribute

LIVE = 'live'

The write-ahead journals the live tables are tailed from.

TelemetryRootMissing

Bases: ValueError

Neither an explicit path nor FEMFORGE_TELEMETRY names a telemetry root.

RunIndex

RunIndex(telemetry: Telemetry, runs_dir: str | Path)

The parquet index of one runs directory: an incremental update over its bundles, the record a driver appends per run, and query.

Methods:

  • update –

    Index every bundle whose manifest.json moved since the last pass — all of them when rebuild — and return the rows appended.

  • record –

    Append one row for a run the driver just wrote, so the index is current without waiting for a scan.

  • query –

    A fluent query over the whole index — every runs directory the root knows, not only this one — with the live claims one with_live away.

Source code in src/femforge/telemetry/runs.py
def __init__(self, telemetry: Telemetry, runs_dir: str | Path) -> None:
    """Track the bundles under ``runs_dir`` in the run table of ``telemetry``."""
    self.telemetry: Telemetry = telemetry
    self.runs_dir: Path = Path(runs_dir)
    self.store = telemetry.runs

update

update(*, rebuild: bool = False) -> int

Index every bundle whose manifest.json moved since the last pass — all of them when rebuild — and return the rows appended.

Source code in src/femforge/telemetry/runs.py
def update(self, *, rebuild: bool = False) -> int:
    """Index every bundle whose ``manifest.json`` moved since the last pass — all of them when ``rebuild`` — and return the rows appended."""
    candidates: list[tuple[Path, datetime]] = self._candidates()
    if not candidates:
        return 0

    pending = candidates if rebuild else self._changed(candidates)

    rows: list[dict[str, object]] = []
    for sim_dir, mtime in pending:
        rows.append(
            run_row(
                _shallow(sim_dir / BundleFile.MANIFEST),
                sim_dir,
                source=RunSource.SCAN,
                manifest_mtime=mtime,
            )
        )

    if not rows:
        return 0

    _ = self.store.append(self._inherit_sweep(pl.DataFrame(rows, schema=RUN_INDEX_SCHEMA)))

    return len(rows)

record

record(manifest: RunManifest, sim_dir: Path, source: RunSource = DRIVER) -> None

Append one row for a run the driver just wrote, so the index is current without waiting for a scan.

Parameters:

  • manifest (RunManifest) –

    The run's manifest.

  • sim_dir (Path) –

    The bundle directory.

  • source (RunSource, default: DRIVER ) –

    How the row was produced.

Source code in src/femforge/telemetry/runs.py
def record(self, manifest: RunManifest, sim_dir: Path, source: RunSource = RunSource.DRIVER) -> None:
    """Append one row for a run the driver just wrote, so the index is current without waiting for a scan.

    Args:
        manifest: The run's manifest.
        sim_dir: The bundle directory.
        source: How the row was produced.
    """
    try:
        mtime: float = (sim_dir / BundleFile.MANIFEST).stat().st_mtime
        written = datetime.fromtimestamp(mtime, tz=timezone.utc)
    except OSError:
        written = datetime.now(timezone.utc)

    row = run_row(
        cast("Mapping[str, object]", manifest.model_dump(mode="json")),
        sim_dir,
        source=source,
        manifest_mtime=written,
    )

    _ = self.store.append(pl.DataFrame([row], schema=RUN_INDEX_SCHEMA))

query

query() -> RunQuery

A fluent query over the whole index — every runs directory the root knows, not only this one — with the live claims one with_live away.

Source code in src/femforge/telemetry/runs.py
def query(self) -> RunQuery:
    """A fluent query over the whole index — every runs directory the root knows, not only this one — with the live claims one ``with_live`` away."""
    return RunQuery(self.store.scan())

RunQuery dataclass

RunQuery(lazy: LazyFrame, predicates: tuple[Expr, ...] = (), limit: int | None = None, live: bool = False)

A fluent filter over the run index; each method returns a new query and nothing runs until frame.

Methods:

  • where –

    A new query narrowed by predicate, judged on each run's latest row; the other filters are named shorthands for this.

  • status –

    Runs in any of statuses.

  • tags –

    Runs whose manifest metadata carries every one of these tag pairs.

  • text –

    Runs whose id, description or notes contain needle, case-insensitively.

  • since –

    Runs created at or after when.

  • sweep –

    Runs solved under the sweep sweep_id.

  • newest –

    Keep only the count most recently created runs, applied after the duplicate rows collapse.

  • with_live –

    Also carry the lock-owner columns, so a listing can mark which runs are in flight.

  • frame –

    The matching rows, newest first, one per run (the latest row wins), joined with the live claims when asked.

  • run_ids –

    The ids of the matching runs, newest first.

  • runs –

    The matching bundles as Run facades, skipping the ones retention has since removed.

Attributes:

  • lazy (LazyFrame) –

    The run index rows the query filters.

  • predicates (tuple[Expr, ...]) –

    The filters each run's latest row must pass.

  • limit (int | None) –

    How many of the newest runs to keep; None keeps them all.

  • live (bool) –

    Whether frame joins the lock-owner columns.

lazy instance-attribute

lazy: LazyFrame

The run index rows the query filters.

predicates class-attribute instance-attribute

predicates: tuple[Expr, ...] = ()

The filters each run's latest row must pass.

limit class-attribute instance-attribute

limit: int | None = None

How many of the newest runs to keep; None keeps them all.

live class-attribute instance-attribute

live: bool = False

Whether frame joins the lock-owner columns.

where

where(predicate: Expr) -> RunQuery

A new query narrowed by predicate, judged on each run's latest row; the other filters are named shorthands for this.

Source code in src/femforge/telemetry/runs.py
def where(self, predicate: pl.Expr) -> RunQuery:
    """A new query narrowed by ``predicate``, judged on each run's latest row; the other filters are named shorthands for this."""
    return replace(self, predicates=(*self.predicates, predicate))

status

status(*statuses: str) -> RunQuery

Runs in any of statuses.

Source code in src/femforge/telemetry/runs.py
def status(self, *statuses: str) -> RunQuery:
    """Runs in any of ``statuses``."""
    return self.where(pl.col(RunColumn.STATUS).is_in(list(statuses)))

tags

tags(**pairs: str) -> RunQuery

Runs whose manifest metadata carries every one of these tag pairs.

Source code in src/femforge/telemetry/runs.py
def tags(self, **pairs: str) -> RunQuery:
    """Runs whose manifest metadata carries every one of these tag pairs."""
    query = self
    for key, value in pairs.items():
        query = query.where(pl.col(RunColumn.TAGS).str.json_path_match(f"$.{key}") == value)

    return query

text

text(needle: str) -> RunQuery

Runs whose id, description or notes contain needle, case-insensitively.

Source code in src/femforge/telemetry/runs.py
def text(self, needle: str) -> RunQuery:
    """Runs whose id, description or notes contain ``needle``, case-insensitively."""
    haystack = pl.concat_str(
        [
            pl.col(RunColumn.RUN_ID),
            pl.col(RunColumn.DESCRIPTION).fill_null(""),
            pl.col(RunColumn.NOTES).fill_null(""),
        ],
        separator=" ",
    )

    return self.where(haystack.str.to_lowercase().str.contains(needle.casefold(), literal=True))

since

since(when: datetime) -> RunQuery

Runs created at or after when.

Source code in src/femforge/telemetry/runs.py
def since(self, when: datetime) -> RunQuery:
    """Runs created at or after ``when``."""
    return self.where(pl.col(RunColumn.CREATED_AT) >= when)

sweep

sweep(sweep_id: str) -> RunQuery

Runs solved under the sweep sweep_id.

Source code in src/femforge/telemetry/runs.py
def sweep(self, sweep_id: str) -> RunQuery:
    """Runs solved under the sweep ``sweep_id``."""
    return self.where(pl.col(RunColumn.SWEEP_ID) == sweep_id)

newest

newest(count: int) -> RunQuery

Keep only the count most recently created runs, applied after the duplicate rows collapse.

Source code in src/femforge/telemetry/runs.py
def newest(self, count: int) -> RunQuery:
    """Keep only the ``count`` most recently created runs, applied after the duplicate rows collapse."""
    return replace(self, limit=count)

with_live

with_live() -> RunQuery

Also carry the lock-owner columns, so a listing can mark which runs are in flight.

Source code in src/femforge/telemetry/runs.py
def with_live(self) -> RunQuery:
    """Also carry the lock-owner columns, so a listing can mark which runs are in flight."""
    return replace(self, live=True)

frame

frame() -> DataFrame

The matching rows, newest first, one per run (the latest row wins), joined with the live claims when asked.

Source code in src/femforge/telemetry/runs.py
def frame(self) -> pl.DataFrame:
    """The matching rows, newest first, one per run (the latest row wins), joined with the live claims when asked."""
    deduped = self.lazy.sort(RunColumn.INDEXED_AT, maintain_order=True).unique(subset=RunColumn.RUN_ID, keep="last", maintain_order=True)

    narrowed = deduped
    for predicate in self.predicates:
        narrowed = narrowed.filter(predicate)

    ordered = narrowed.sort(RunColumn.CREATED_AT, descending=True, nulls_last=True, maintain_order=True)
    rows: pl.DataFrame = (ordered.head(self.limit) if self.limit is not None else ordered).collect()

    if not self.live:
        return rows

    directories: list[str] = cast("list[str]", rows[RunColumn.RUNS_DIR].unique().to_list())
    claims = pl.concat([live_runs(directory) for directory in directories]) if directories else pl.DataFrame(schema=LIVE_RUN_SCHEMA)

    return rows.join(claims, on=RunColumn.RUN_ID, how="left", maintain_order="left")

run_ids

run_ids() -> list[str]

The ids of the matching runs, newest first.

Source code in src/femforge/telemetry/runs.py
def run_ids(self) -> list[str]:
    """The ids of the matching runs, newest first."""
    return cast("list[str]", self.frame()[RunColumn.RUN_ID].to_list())

runs

runs() -> list[Run]

The matching bundles as Run facades, skipping the ones retention has since removed.

Source code in src/femforge/telemetry/runs.py
def runs(self) -> list[Run]:
    """The matching bundles as ``Run`` facades, skipping the ones retention has since removed."""
    from femforge.run.inspect import Run

    rows: pl.DataFrame = self.frame()
    directories: list[str] = cast("list[str]", rows[RunColumn.RUNS_DIR].to_list())
    names: list[str] = cast("list[str]", rows[RunColumn.RUN_ID].to_list())

    found: list[Run] = []
    for directory, name in zip(directories, names, strict=True):
        path = Path(directory) / name
        if not path.is_dir():
            continue

        found.append(Run(path))

    return found

ArtifactColumn

Bases: StrEnum

The artifact index's columns: one row per stored artifact, keyed by key.

Attributes:

  • KIND –

    The artifact kind name; the partition key.

  • KEY –

    The content key the artifact is stored under.

  • NAME –

    The <adjective>-<noun>-<hash8> store directory name.

  • STORE_ROOT –

    The root of the store holding the artifact.

  • CREATED_AT –

    When the artifact was published.

  • SPEC_TYPE –

    module:QualName of the spec model.

  • PRODUCER_VERSION –

    The kind's producer version at publish time.

  • PRODUCER_HOST –

    The machine that produced the artifact.

  • PRODUCER_PID –

    The id of the process that produced the artifact.

  • FILE_COUNT –

    How many files the artifact holds.

  • TOTAL_BYTES –

    The total size of the artifact's files in bytes.

  • FILES –

    The artifact-relative names of its files.

  • SPEC –

    The hashed spec payload, as JSON.

  • FEMFORGE –

    The femforge release that produced the artifact.

  • CODE_SHA –

    The producing checkout's commit.

  • GMSH –

    The gmsh version, when meshing was involved.

  • ELMER –

    The ElmerSolver version, when a solve was involved.

  • DIR_MTIME –

    The artifact directory's modification time when the row was read.

  • INDEXED_AT –

    When the row was written; the latest row per artifact wins.

KIND class-attribute instance-attribute

KIND = 'kind'

The artifact kind name; the partition key.

KEY class-attribute instance-attribute

KEY = 'key'

The content key the artifact is stored under.

NAME class-attribute instance-attribute

NAME = 'name'

The <adjective>-<noun>-<hash8> store directory name.

STORE_ROOT class-attribute instance-attribute

STORE_ROOT = 'store_root'

The root of the store holding the artifact.

CREATED_AT class-attribute instance-attribute

CREATED_AT = 'created_at'

When the artifact was published.

SPEC_TYPE class-attribute instance-attribute

SPEC_TYPE = 'spec_type'

module:QualName of the spec model.

PRODUCER_VERSION class-attribute instance-attribute

PRODUCER_VERSION = 'producer_version'

The kind's producer version at publish time.

PRODUCER_HOST class-attribute instance-attribute

PRODUCER_HOST = 'producer_host'

The machine that produced the artifact.

PRODUCER_PID class-attribute instance-attribute

PRODUCER_PID = 'producer_pid'

The id of the process that produced the artifact.

FILE_COUNT class-attribute instance-attribute

FILE_COUNT = 'file_count'

How many files the artifact holds.

TOTAL_BYTES class-attribute instance-attribute

TOTAL_BYTES = 'total_bytes'

The total size of the artifact's files in bytes.

FILES class-attribute instance-attribute

FILES = 'files'

The artifact-relative names of its files.

SPEC class-attribute instance-attribute

SPEC = 'spec'

The hashed spec payload, as JSON.

FEMFORGE class-attribute instance-attribute

FEMFORGE = 'femforge'

The femforge release that produced the artifact.

CODE_SHA class-attribute instance-attribute

CODE_SHA = 'code_sha'

The producing checkout's commit.

GMSH class-attribute instance-attribute

GMSH = 'gmsh'

The gmsh version, when meshing was involved.

ELMER class-attribute instance-attribute

ELMER = 'elmer'

The ElmerSolver version, when a solve was involved.

DIR_MTIME class-attribute instance-attribute

DIR_MTIME = 'dir_mtime'

The artifact directory's modification time when the row was read.

INDEXED_AT class-attribute instance-attribute

INDEXED_AT = 'indexed_at'

When the row was written; the latest row per artifact wins.

ConvergenceColumn

Bases: StrEnum

The keys elmer.runtime.solve writes into convergence.jsonl, one sample per solver iteration.

Attributes:

  • WALL_TIME –

    t_wall [s]: Time since the parser started reading the solver output.

  • SOLVER –

    The sif Equation name Elmer tags the sample with, lowercased.

  • LOOP –

    Which loop the sample belongs to.

  • ITER –

    One-based iteration count within the loop.

  • NRM –

    ‖u‖₂ [?]: The solution's L₂ norm at this iteration.

  • RELC –

    Δ_rel [-]: Relative change of the norm from the previous iteration.

WALL_TIME class-attribute instance-attribute

WALL_TIME = 'wall_time'

t_wall [s]: Time since the parser started reading the solver output.

SOLVER class-attribute instance-attribute

SOLVER = 'solver'

The sif Equation name Elmer tags the sample with, lowercased.

LOOP class-attribute instance-attribute

LOOP = 'loop'

Which loop the sample belongs to.

ITER class-attribute instance-attribute

ITER = 'iter'

One-based iteration count within the loop.

NRM class-attribute instance-attribute

NRM = 'nrm'

‖u‖₂ [?]: The solution's L₂ norm at this iteration.

RELC class-attribute instance-attribute

RELC = 'relc'

Δ_rel [-]: Relative change of the norm from the previous iteration.

ConvergenceLoop

Bases: StrEnum

Which Elmer iteration loop a convergence sample belongs to: nonlinear or steady state.

Attributes:

  • NS –

    The nonlinear inner loop.

  • SS –

    The steady-state outer loop.

NS class-attribute instance-attribute

NS = 'NS'

The nonlinear inner loop.

SS class-attribute instance-attribute

SS = 'SS'

The steady-state outer loop.

LiveRunColumn

Bases: StrEnum

The columns of the in-flight run table built from the run lock owners; never persisted.

Attributes:

  • RUN_ID –

    The run the claim is held on.

  • HOST –

    The machine holding the claim.

  • PID –

    The id of the process holding the claim.

  • STARTED_AT –

    When the claim was taken.

  • HEARTBEAT_AT –

    When the holder last refreshed the claim.

  • HEARTBEAT_AGE_S –

    t_beat [s]: Time since the holder last refreshed the claim.

  • STALE –

    Whether the claim has gone without a heartbeat long enough for the next claimant to break it.

RUN_ID class-attribute instance-attribute

RUN_ID = 'run_id'

The run the claim is held on.

HOST class-attribute instance-attribute

HOST = 'host'

The machine holding the claim.

PID class-attribute instance-attribute

PID = 'pid'

The id of the process holding the claim.

STARTED_AT class-attribute instance-attribute

STARTED_AT = 'started_at'

When the claim was taken.

HEARTBEAT_AT class-attribute instance-attribute

HEARTBEAT_AT = 'heartbeat_at'

When the holder last refreshed the claim.

HEARTBEAT_AGE_S class-attribute instance-attribute

HEARTBEAT_AGE_S = 'heartbeat_age_s'

t_beat [s]: Time since the holder last refreshed the claim.

STALE class-attribute instance-attribute

STALE = 'stale'

Whether the claim has gone without a heartbeat long enough for the next claimant to break it.

LogColumn

Bases: StrEnum

The log table's columns; the sink emits these names and every query selects by them.

Attributes:

  • DATE –

    The UTC day the record was logged on; a partition key.

  • PROCESS –

    The sanitized name of the process that wrote the record; a partition key.

  • TIME –

    When the record was logged, in UTC.

  • LEVEL –

    The record's level name, such as INFO.

  • LEVEL_NO –

    The record's numeric severity.

  • NAME –

    The module that logged the record.

  • FUNCTION –

    The function that logged the record.

  • LINE –

    The source line that logged the record.

  • MESSAGE –

    The formatted message.

  • RUN_ID –

    The run the record was bound to; null outside a run.

  • SWEEP_ID –

    The sweep the record was bound to; null outside a sweep.

  • POINT_KEY –

    The sweep point the record was bound to; null outside a sweep.

  • HOST –

    The machine that wrote the record.

  • PID –

    The id of the process that wrote the record.

  • THREAD –

    The name of the thread that logged the record.

  • EXCEPTION –

    The formatted traceback of the exception the record carried; null when it carried none.

  • EXTRA –

    Every other bound key as JSON; null when there are none.

DATE class-attribute instance-attribute

DATE = 'date'

The UTC day the record was logged on; a partition key.

PROCESS class-attribute instance-attribute

PROCESS = 'process'

The sanitized name of the process that wrote the record; a partition key.

TIME class-attribute instance-attribute

TIME = 'time'

When the record was logged, in UTC.

LEVEL class-attribute instance-attribute

LEVEL = 'level'

The record's level name, such as INFO.

LEVEL_NO class-attribute instance-attribute

LEVEL_NO = 'level_no'

The record's numeric severity.

NAME class-attribute instance-attribute

NAME = 'name'

The module that logged the record.

FUNCTION class-attribute instance-attribute

FUNCTION = 'function'

The function that logged the record.

LINE class-attribute instance-attribute

LINE = 'line'

The source line that logged the record.

MESSAGE class-attribute instance-attribute

MESSAGE = 'message'

The formatted message.

RUN_ID class-attribute instance-attribute

RUN_ID = 'run_id'

The run the record was bound to; null outside a run.

SWEEP_ID class-attribute instance-attribute

SWEEP_ID = 'sweep_id'

The sweep the record was bound to; null outside a sweep.

POINT_KEY class-attribute instance-attribute

POINT_KEY = 'point_key'

The sweep point the record was bound to; null outside a sweep.

HOST class-attribute instance-attribute

HOST = 'host'

The machine that wrote the record.

PID class-attribute instance-attribute

PID = 'pid'

The id of the process that wrote the record.

THREAD class-attribute instance-attribute

THREAD = 'thread'

The name of the thread that logged the record.

EXCEPTION class-attribute instance-attribute

EXCEPTION = 'exception'

The formatted traceback of the exception the record carried; null when it carried none.

EXTRA class-attribute instance-attribute

EXTRA = 'extra'

Every other bound key as JSON; null when there are none.

RunColumn

Bases: StrEnum

The run index's columns: one row per run bundle, keyed by run_id.

Attributes:

  • MONTH –

    The first day of the month the run was created in; the partition key.

  • RUN_ID –

    The run's bundle directory name.

  • CACHE_KEY –

    The full content hash of the run's result-affecting inputs.

  • RUNS_DIR –

    The runs directory holding the bundle.

  • STATUS –

    The manifest's lifecycle status.

  • CREATED_AT –

    When the run was assembled.

  • FINISHED_AT –

    When the run's latest stage finished; null before any has.

  • CURRENT_STAGE –

    The stage running when the row was read; null when none was.

  • STAGES_DONE –

    How many stages had finished when the row was read.

  • GIT_SHA –

    The commit of the checkout the run was made in, with -dirty when it had uncommitted changes.

  • ELMER_VERSION –

    The ElmerSolver version parsed from the solver log.

  • PHYSICS –

    The physics preset the run solved.

  • ELEMENT_COUNT –

    Total number of volume elements in the solved mesh.

  • MIN_ELEMENT_SIZE –

    h_min [m]: Smallest element edge length.

  • MAX_ELEMENT_SIZE –

    h_max [m]: Largest element edge length.

  • DESCRIPTION –

    The description in the run's metadata.

  • TAGS –

    The tags in the run's metadata, as a JSON object.

  • NOTES –

    The manifest's free-form provenance note.

  • MESH_NAME –

    The mesh artifact's store directory name.

  • MESH_KEY –

    The mesh artifact's content key.

  • PUBLISHED –

    Whether the bundle was published to the artifact store.

  • HAS_RESULT –

    Whether the bundle holds a readable result.json.

  • HAS_VTU –

    Whether the bundle holds a result VTU file.

  • CLEANUPS –

    How many retention passes the manifest records.

  • BUNDLE_BYTES –

    The bundle directory's total size in bytes.

  • SWEEP_ID –

    The sweep the run was solved under; null outside a sweep.

  • POINT_KEY –

    The sweep point the run was solved for; null outside a sweep.

  • MANIFEST_MTIME –

    The manifest's modification time when the row was read.

  • INDEXED_AT –

    When the row was written; the latest row per run wins.

  • SOURCE –

    How the row was produced.

  • MODEL_DIM –

    The mesh dimension the run's frame solves on: 2 or 3.

  • ARTIFACT_KEYS –

    The content keys of the store artifacts the run consumed.

  • FORCE_Y –

    F_y [N]: Y component of the net force on the moving array.

  • TEMPERATURE_MAX –

    T_max [K]: Peak temperature in the domain.

  • WALL_TIME –

    t_wall [s]: Solver wall-clock time.

MONTH class-attribute instance-attribute

MONTH = 'month'

The first day of the month the run was created in; the partition key.

RUN_ID class-attribute instance-attribute

RUN_ID = 'run_id'

The run's bundle directory name.

CACHE_KEY class-attribute instance-attribute

CACHE_KEY = 'cache_key'

The full content hash of the run's result-affecting inputs.

RUNS_DIR class-attribute instance-attribute

RUNS_DIR = 'runs_dir'

The runs directory holding the bundle.

STATUS class-attribute instance-attribute

STATUS = 'status'

The manifest's lifecycle status.

CREATED_AT class-attribute instance-attribute

CREATED_AT = 'created_at'

When the run was assembled.

FINISHED_AT class-attribute instance-attribute

FINISHED_AT = 'finished_at'

When the run's latest stage finished; null before any has.

CURRENT_STAGE class-attribute instance-attribute

CURRENT_STAGE = 'current_stage'

The stage running when the row was read; null when none was.

STAGES_DONE class-attribute instance-attribute

STAGES_DONE = 'stages_done'

How many stages had finished when the row was read.

GIT_SHA class-attribute instance-attribute

GIT_SHA = 'git_sha'

The commit of the checkout the run was made in, with -dirty when it had uncommitted changes.

ELMER_VERSION class-attribute instance-attribute

ELMER_VERSION = 'elmer_version'

The ElmerSolver version parsed from the solver log.

PHYSICS class-attribute instance-attribute

PHYSICS = 'physics'

The physics preset the run solved.

ELEMENT_COUNT class-attribute instance-attribute

ELEMENT_COUNT = 'element_count'

Total number of volume elements in the solved mesh.

MIN_ELEMENT_SIZE class-attribute instance-attribute

MIN_ELEMENT_SIZE = 'min_element_size'

h_min [m]: Smallest element edge length.

MAX_ELEMENT_SIZE class-attribute instance-attribute

MAX_ELEMENT_SIZE = 'max_element_size'

h_max [m]: Largest element edge length.

DESCRIPTION class-attribute instance-attribute

DESCRIPTION = 'description'

The description in the run's metadata.

TAGS class-attribute instance-attribute

TAGS = 'tags'

The tags in the run's metadata, as a JSON object.

NOTES class-attribute instance-attribute

NOTES = 'notes'

The manifest's free-form provenance note.

MESH_NAME class-attribute instance-attribute

MESH_NAME = 'mesh_name'

The mesh artifact's store directory name.

MESH_KEY class-attribute instance-attribute

MESH_KEY = 'mesh_key'

The mesh artifact's content key.

PUBLISHED class-attribute instance-attribute

PUBLISHED = 'published'

Whether the bundle was published to the artifact store.

HAS_RESULT class-attribute instance-attribute

HAS_RESULT = 'has_result'

Whether the bundle holds a readable result.json.

HAS_VTU class-attribute instance-attribute

HAS_VTU = 'has_vtu'

Whether the bundle holds a result VTU file.

CLEANUPS class-attribute instance-attribute

CLEANUPS = 'cleanups'

How many retention passes the manifest records.

BUNDLE_BYTES class-attribute instance-attribute

BUNDLE_BYTES = 'bundle_bytes'

The bundle directory's total size in bytes.

SWEEP_ID class-attribute instance-attribute

SWEEP_ID = 'sweep_id'

The sweep the run was solved under; null outside a sweep.

POINT_KEY class-attribute instance-attribute

POINT_KEY = 'point_key'

The sweep point the run was solved for; null outside a sweep.

MANIFEST_MTIME class-attribute instance-attribute

MANIFEST_MTIME = 'manifest_mtime'

The manifest's modification time when the row was read.

INDEXED_AT class-attribute instance-attribute

INDEXED_AT = 'indexed_at'

When the row was written; the latest row per run wins.

SOURCE class-attribute instance-attribute

SOURCE = 'source'

How the row was produced.

MODEL_DIM class-attribute instance-attribute

MODEL_DIM = 'model_dim'

The mesh dimension the run's frame solves on: 2 or 3.

ARTIFACT_KEYS class-attribute instance-attribute

ARTIFACT_KEYS = 'artifact_keys'

The content keys of the store artifacts the run consumed.

FORCE_Y class-attribute instance-attribute

FORCE_Y = 'force_y'

F_y [N]: Y component of the net force on the moving array.

TEMPERATURE_MAX class-attribute instance-attribute

TEMPERATURE_MAX = 'temperature_max'

T_max [K]: Peak temperature in the domain.

WALL_TIME class-attribute instance-attribute

WALL_TIME = 'wall_time'

t_wall [s]: Solver wall-clock time.

RunSource

Bases: StrEnum

How a run index row was produced: a filesystem scan of the bundles, or the Driver reporting live.

Attributes:

  • SCAN –

    Read from the bundle on disk by an index pass.

  • DRIVER –

    Recorded by the Driver as it wrote the bundle.

SCAN class-attribute instance-attribute

SCAN = 'scan'

Read from the bundle on disk by an index pass.

DRIVER class-attribute instance-attribute

DRIVER = 'driver'

Recorded by the Driver as it wrote the bundle.

SweepColumn

Bases: StrEnum

The sweep table's columns: one row per sweep launch.

Attributes:

  • MONTH –

    The first day of the month the sweep started in; the partition key.

  • SWEEP_ID –

    The sweep's id.

  • DESCRIPTION –

    What the sweep runs, as its launcher described it.

  • STARTED_AT –

    When the sweep started.

  • FINISHED_AT –

    When the sweep finished; null on its launch row.

  • TOTAL –

    How many points the sweep holds, skipped ones included.

  • SKIPPED –

    How many points were already stored and did not run.

  • MAX_WORKERS –

    How many worker processes run the points.

  • HOST –

    The machine that launched the sweep.

  • PID –

    The id of the process that launched the sweep.

  • PROCESS –

    The sanitized name of the process that launched the sweep.

  • GIT_SHA –

    The commit of the checkout the sweep ran in.

  • INDEXED_AT –

    When the row was written; the closing row supersedes the launch row.

MONTH class-attribute instance-attribute

MONTH = 'month'

The first day of the month the sweep started in; the partition key.

SWEEP_ID class-attribute instance-attribute

SWEEP_ID = 'sweep_id'

The sweep's id.

DESCRIPTION class-attribute instance-attribute

DESCRIPTION = 'description'

What the sweep runs, as its launcher described it.

STARTED_AT class-attribute instance-attribute

STARTED_AT = 'started_at'

When the sweep started.

FINISHED_AT class-attribute instance-attribute

FINISHED_AT = 'finished_at'

When the sweep finished; null on its launch row.

TOTAL class-attribute instance-attribute

TOTAL = 'total'

How many points the sweep holds, skipped ones included.

SKIPPED class-attribute instance-attribute

SKIPPED = 'skipped'

How many points were already stored and did not run.

MAX_WORKERS class-attribute instance-attribute

MAX_WORKERS = 'max_workers'

How many worker processes run the points.

HOST class-attribute instance-attribute

HOST = 'host'

The machine that launched the sweep.

PID class-attribute instance-attribute

PID = 'pid'

The id of the process that launched the sweep.

PROCESS class-attribute instance-attribute

PROCESS = 'process'

The sanitized name of the process that launched the sweep.

GIT_SHA class-attribute instance-attribute

GIT_SHA = 'git_sha'

The commit of the checkout the sweep ran in.

INDEXED_AT class-attribute instance-attribute

INDEXED_AT = 'indexed_at'

When the row was written; the closing row supersedes the launch row.

SweepEventColumn

Bases: StrEnum

The sweep event table's columns: one row per point state transition.

Attributes:

  • SWEEP_ID –

    The sweep the point belongs to; the partition key.

  • POINT_KEY –

    The point's key within the sweep.

  • POINT_NAME –

    A readable label for the point.

  • STATE –

    The state the point entered.

  • AT –

    When the point entered the state.

  • HOST –

    The machine that recorded the transition.

  • PID –

    The id of the process that recorded the transition.

  • ORIGIN –

    Where a finished point's result came from, such as an artifact origin.

  • ERROR –

    The traceback a failed point recorded.

  • DURATION_S –

    t_point [s]: How long the point took up to this transition.

SWEEP_ID class-attribute instance-attribute

SWEEP_ID = 'sweep_id'

The sweep the point belongs to; the partition key.

POINT_KEY class-attribute instance-attribute

POINT_KEY = 'point_key'

The point's key within the sweep.

POINT_NAME class-attribute instance-attribute

POINT_NAME = 'point_name'

A readable label for the point.

STATE class-attribute instance-attribute

STATE = 'state'

The state the point entered.

AT class-attribute instance-attribute

AT = 'at'

When the point entered the state.

HOST class-attribute instance-attribute

HOST = 'host'

The machine that recorded the transition.

PID class-attribute instance-attribute

PID = 'pid'

The id of the process that recorded the transition.

ORIGIN class-attribute instance-attribute

ORIGIN = 'origin'

Where a finished point's result came from, such as an artifact origin.

ERROR class-attribute instance-attribute

ERROR = 'error'

The traceback a failed point recorded.

DURATION_S class-attribute instance-attribute

DURATION_S = 'duration_s'

t_point [s]: How long the point took up to this transition.

SweepState

Bases: StrEnum

The lifecycle states a sweep point moves through; each transition is one sweep event row.

Attributes:

  • QUEUED –

    Waiting for a worker.

  • RUNNING –

    Being solved by a worker.

  • DONE –

    Finished successfully.

  • FAILED –

    Finished with an error; its event carries the traceback.

  • SKIPPED –

    Already stored when the sweep started, so never queued.

QUEUED class-attribute instance-attribute

QUEUED = 'queued'

Waiting for a worker.

RUNNING class-attribute instance-attribute

RUNNING = 'running'

Being solved by a worker.

DONE class-attribute instance-attribute

DONE = 'done'

Finished successfully.

FAILED class-attribute instance-attribute

FAILED = 'failed'

Finished with an error; its event carries the traceback.

SKIPPED class-attribute instance-attribute

SKIPPED = 'skipped'

Already stored when the sweep started, so never queued.

LogQuery

LogQuery(frame: LazyFrame)

A fluent filter over the log frame; each method returns a new query and nothing runs until page.

Methods:

  • over –

    A query spanning one telemetry root's parquet shards and live journals.

  • where –

    A new query narrowed by predicate; the other methods are named shorthands for this.

  • levels –

    Records at any of levels.

  • processes –

    Records written by any of the processes names.

  • run –

    Records bound to the run run_id.

  • sweep –

    Records bound to the sweep sweep_id.

  • since –

    Records logged at or after when.

  • until –

    Records logged at or before when.

  • text –

    Records whose message contains needle, case-sensitively.

  • page –

    Collect one slice of the result, oldest record first.

  • tail –

    Collect the newest limit records, oldest of them first.

Source code in src/femforge/telemetry/sink.py
def __init__(self, frame: pl.LazyFrame) -> None:
    """Query ``frame``, a lazy frame on the log schema."""
    self.frame: pl.LazyFrame = frame

over classmethod

over(telemetry: Telemetry) -> Self

A query spanning one telemetry root's parquet shards and live journals.

Source code in src/femforge/telemetry/sink.py
@classmethod
def over(cls, telemetry: Telemetry) -> Self:
    """A query spanning one telemetry root's parquet shards and live journals."""
    return cls(scan_logs(telemetry))

where

where(predicate: Expr) -> Self

A new query narrowed by predicate; the other methods are named shorthands for this.

Source code in src/femforge/telemetry/sink.py
def where(self, predicate: pl.Expr) -> Self:
    """A new query narrowed by ``predicate``; the other methods are named shorthands for this."""
    return type(self)(self.frame.filter(predicate))

levels

levels(*levels: LogLevel | str) -> Self

Records at any of levels.

Source code in src/femforge/telemetry/sink.py
def levels(self, *levels: LogLevel | str) -> Self:
    """Records at any of ``levels``."""
    return self.where(pl.col(LogColumn.LEVEL).is_in([str(level) for level in levels]))

processes

processes(*names: str) -> Self

Records written by any of the processes names.

Source code in src/femforge/telemetry/sink.py
def processes(self, *names: str) -> Self:
    """Records written by any of the processes ``names``."""
    return self.where(pl.col(LogColumn.PROCESS).is_in(list(names)))

run

run(run_id: str) -> Self

Records bound to the run run_id.

Source code in src/femforge/telemetry/sink.py
def run(self, run_id: str) -> Self:
    """Records bound to the run ``run_id``."""
    return self.where(pl.col(LogColumn.RUN_ID) == run_id)

sweep

sweep(sweep_id: str) -> Self

Records bound to the sweep sweep_id.

Source code in src/femforge/telemetry/sink.py
def sweep(self, sweep_id: str) -> Self:
    """Records bound to the sweep ``sweep_id``."""
    return self.where(pl.col(LogColumn.SWEEP_ID) == sweep_id)

since

since(when: datetime) -> Self

Records logged at or after when.

Source code in src/femforge/telemetry/sink.py
def since(self, when: datetime) -> Self:
    """Records logged at or after ``when``."""
    return self.where(pl.col(LogColumn.TIME) >= when)

until

until(when: datetime) -> Self

Records logged at or before when.

Source code in src/femforge/telemetry/sink.py
def until(self, when: datetime) -> Self:
    """Records logged at or before ``when``."""
    return self.where(pl.col(LogColumn.TIME) <= when)

text

text(needle: str) -> Self

Records whose message contains needle, case-sensitively.

Source code in src/femforge/telemetry/sink.py
def text(self, needle: str) -> Self:
    """Records whose message contains ``needle``, case-sensitively."""
    return self.where(pl.col(LogColumn.MESSAGE).str.contains(needle, literal=True))

page

page(offset: int = 0, limit: int = 200) -> DataFrame

Collect one slice of the result, oldest record first.

Source code in src/femforge/telemetry/sink.py
def page(self, offset: int = 0, limit: int = 200) -> pl.DataFrame:
    """Collect one slice of the result, oldest record first."""
    return self.frame.sort(LogColumn.TIME, maintain_order=True).slice(offset, limit).collect()

tail

tail(limit: int = 200) -> DataFrame

Collect the newest limit records, oldest of them first.

Source code in src/femforge/telemetry/sink.py
def tail(self, limit: int = 200) -> pl.DataFrame:
    """Collect the newest ``limit`` records, oldest of them first."""
    return self.frame.sort(LogColumn.TIME, maintain_order=True).tail(limit).collect()

ParquetLogSink

ParquetLogSink(telemetry: Telemetry, *, process: str | None = None, flush_every: int = FLUSH_EVERY, flush_interval_s: float = FLUSH_INTERVAL_S)

Loguru sink writing every record to a JSONL journal at once and to hive-partitioned parquet in batches.

Parameters:

  • telemetry (Telemetry) –

    The telemetry root the records land under.

  • process (str | None, default: None ) –

    The process name the records are partitioned under; None uses the script's name.

  • flush_every (int, default: FLUSH_EVERY ) –

    How many records trigger a checkpoint into parquet.

  • flush_interval_s (float, default: FLUSH_INTERVAL_S ) –

    Seconds between timed checkpoints.

Methods:

  • __call__ –

    The loguru sink protocol; add this callable with format="{message}" at DEBUG.

  • flush –

    Checkpoint the journal's records into the parquet store; a no-op when nothing is pending.

  • close –

    Stop the timer, checkpoint whatever is left and drop the journal; idempotent.

Source code in src/femforge/telemetry/sink.py
def __init__(self, telemetry: Telemetry, *, process: str | None = None, flush_every: int = FLUSH_EVERY, flush_interval_s: float = FLUSH_INTERVAL_S) -> None:
    """Open this process's journal, recover the ones dead processes left behind, and start the flush timer.

    Args:
        telemetry: The telemetry root the records land under.
        process: The process name the records are partitioned under; None uses the script's name.
        flush_every: How many records trigger a checkpoint into parquet.
        flush_interval_s: Seconds between timed checkpoints.
    """
    self.telemetry: Telemetry = telemetry
    self.process: str = sanitize_process(process) if process is not None else default_process()
    self.flush_every: int = flush_every
    self.flush_interval_s: float = flush_interval_s

    self.store = telemetry.logs
    self.journal: Journal = Journal(telemetry.live, TelemetryDir.LOGS)

    self._console: Console = Console(file=StringIO(), width=EXTRA_WIDTH)
    self._guard: FlushGuard = FlushGuard()
    self._lock: threading.RLock = threading.RLock()
    self._pending: int = 0
    self._closed: bool = False

    _ = recover_journals(telemetry.live, self.store, journal_glob(TelemetryDir.LOGS))

    self._stop: threading.Event = threading.Event()
    self._timer: threading.Thread = threading.Thread(target=self._tick, name="femforge-telemetry-flush", daemon=True)
    self._timer.start()
    _ = atexit.register(self.close)

__call__

__call__(message: Message) -> None

The loguru sink protocol; add this callable with format="{message}" at DEBUG.

Source code in src/femforge/telemetry/sink.py
def __call__(self, message: Message) -> None:
    """The loguru sink protocol; add this callable with ``format="{message}"`` at DEBUG."""
    if self._guard.busy:
        return

    row = log_row(message.record, process=self.process, console=self._console)

    with self._lock:
        if self._closed:
            return

        self.journal.write(row)
        self._pending += 1
        due = self._pending >= self.flush_every

    if due:
        self.flush()

flush

flush() -> None

Checkpoint the journal's records into the parquet store; a no-op when nothing is pending.

Source code in src/femforge/telemetry/sink.py
def flush(self) -> None:
    """Checkpoint the journal's records into the parquet store; a no-op when nothing is pending."""
    with self._lock:
        if self._pending == 0:
            return

        self._guard.busy = True
        try:
            _ = self.journal.checkpoint(self.store)
            self._pending = 0
        finally:
            self._guard.busy = False

close

close() -> None

Stop the timer, checkpoint whatever is left and drop the journal; idempotent.

Source code in src/femforge/telemetry/sink.py
def close(self) -> None:
    """Stop the timer, checkpoint whatever is left and drop the journal; idempotent."""
    with self._lock:
        if self._closed:
            return

        self._closed = True

    self._stop.set()
    self._timer.join(timeout=self.flush_interval_s)
    _ = self.journal.checkpoint(self.store)
    self.journal.close()
    _ = atexit.unregister(self.close)

SweepJournal

SweepJournal(telemetry: Telemetry, *, sweep_id: str, process: str | None = None)

One sweep's writer: the launch and closing rows in the sweeps table, every point transition through this process's own journal.

Parameters:

  • telemetry (Telemetry) –

    The telemetry root the sweep is recorded under.

  • sweep_id (str) –

    The sweep this writer records.

  • process (str | None, default: None ) –

    The process name stamped on the sweep row; None uses the script's name.

Methods:

  • start –

    Open the sweep: stamp its launch row so a reader sees it the moment the first point starts.

  • event –

    Journal one point transition; the parent and each worker write through a journal of their own.

  • checkpoint –

    Fold this process's journalled events into parquet without closing the sweep.

  • finish –

    Close the sweep: the row gains a finish time, every journal lands in parquet, and the shards fold into one per sweep.

Source code in src/femforge/telemetry/sweep.py
def __init__(self, telemetry: Telemetry, *, sweep_id: str, process: str | None = None) -> None:
    """Recover the event journals that dead workers left behind and open this process's own.

    Args:
        telemetry: The telemetry root the sweep is recorded under.
        sweep_id: The sweep this writer records.
        process: The process name stamped on the sweep row; None uses the script's name.
    """
    self.telemetry: Telemetry = telemetry
    self.sweep_id: str = sweep_id
    self.process: str = sanitize_process(process) if process is not None else default_process()

    self.started_at: datetime = datetime.now(timezone.utc)
    self.description: str = ""
    self.total: int = 0
    self.skipped: int = 0
    self.max_workers: int = 1

    _ = self._recover()
    self.journal: Journal = Journal(telemetry.live, TelemetryDir.SWEEP_EVENTS)

start

start(description: str, total: int, skipped: int = 0, max_workers: int = 1) -> None

Open the sweep: stamp its launch row so a reader sees it the moment the first point starts.

Parameters:

  • description (str) –

    What the sweep runs.

  • total (int) –

    How many points the sweep holds, skipped ones included.

  • skipped (int, default: 0 ) –

    How many points were already stored and will not run.

  • max_workers (int, default: 1 ) –

    How many worker processes run the points.

Source code in src/femforge/telemetry/sweep.py
def start(self, description: str, total: int, skipped: int = 0, max_workers: int = 1) -> None:
    """Open the sweep: stamp its launch row so a reader sees it the moment the first point starts.

    Args:
        description: What the sweep runs.
        total: How many points the sweep holds, skipped ones included.
        skipped: How many points were already stored and will not run.
        max_workers: How many worker processes run the points.
    """
    self.started_at = datetime.now(timezone.utc)
    self.description = description
    self.total = total
    self.skipped = skipped
    self.max_workers = max_workers

    self._write_sweep(finished_at=None)

event

event(point_key: str, state: SweepState, *, point_name: str | None = None, origin: str | None = None, error: str | None = None, duration_s: float | None = None) -> None

Journal one point transition; the parent and each worker write through a journal of their own.

Parameters:

  • point_key (str) –

    The point's key within the sweep.

  • state (SweepState) –

    The state the point enters.

  • point_name (str | None, default: None ) –

    A readable label for the point.

  • origin (str | None, default: None ) –

    Where a finished point's result came from, such as an artifact origin.

  • error (str | None, default: None ) –

    The traceback of a failed point.

  • duration_s (float | None, default: None ) –

    Seconds the point took up to this transition.

Source code in src/femforge/telemetry/sweep.py
def event(self, point_key: str, state: SweepState, *, point_name: str | None = None, origin: str | None = None, error: str | None = None, duration_s: float | None = None) -> None:
    """Journal one point transition; the parent and each worker write through a journal of their own.

    Args:
        point_key: The point's key within the sweep.
        state: The state the point enters.
        point_name: A readable label for the point.
        origin: Where a finished point's result came from, such as an artifact origin.
        error: The traceback of a failed point.
        duration_s: Seconds the point took up to this transition.
    """
    self.journal.write(
        {
            SweepEventColumn.SWEEP_ID: self.sweep_id,
            SweepEventColumn.POINT_KEY: point_key,
            SweepEventColumn.POINT_NAME: point_name,
            SweepEventColumn.STATE: state.value,
            SweepEventColumn.AT: datetime.now(timezone.utc),
            SweepEventColumn.HOST: HOST,
            SweepEventColumn.PID: os.getpid(),
            SweepEventColumn.ORIGIN: origin,
            SweepEventColumn.ERROR: error,
            SweepEventColumn.DURATION_S: duration_s,
        }
    )

checkpoint

checkpoint() -> None

Fold this process's journalled events into parquet without closing the sweep.

Source code in src/femforge/telemetry/sweep.py
def checkpoint(self) -> None:
    """Fold this process's journalled events into parquet without closing the sweep."""
    _ = self.journal.checkpoint(self.telemetry.sweep_events)

finish

finish() -> None

Close the sweep: the row gains a finish time, every journal lands in parquet, and the shards fold into one per sweep.

Source code in src/femforge/telemetry/sweep.py
def finish(self) -> None:
    """Close the sweep: the row gains a finish time, every journal lands in parquet, and the shards fold into one per sweep."""
    self._write_sweep(finished_at=datetime.now(timezone.utc))

    self.checkpoint()
    self.journal.close()
    _ = self._recover()
    _ = self.telemetry.sweep_events.compact(settle_s=0.0)

SweepQuery

SweepQuery(telemetry: Telemetry, sweep_id: str)

One sweep read back: its launch row, every point transition (settled shards plus live journals), and the progress summary.

Methods:

  • launch –

    The sweep's own row: its closing one when it has finished, else the one its launch wrote; empty when unknown.

  • events –

    Every transition of this sweep's points, oldest first, unioning what is still only in the live journals.

  • latest –

    The state each point is in now: its last transition, one row per point_key.

  • failures –

    The points whose last transition failed, each carrying the traceback the worker recorded.

  • summary –

    Where the sweep stands at now: the per-state counts, the trailing-window rate, and the ETA that rate implies.

Source code in src/femforge/telemetry/sweep.py
def __init__(self, telemetry: Telemetry, sweep_id: str) -> None:
    """Query the sweep ``sweep_id`` in the tables of ``telemetry``."""
    self.telemetry: Telemetry = telemetry
    self.sweep_id: str = sweep_id

launch

launch() -> DataFrame

The sweep's own row: its closing one when it has finished, else the one its launch wrote; empty when unknown.

Source code in src/femforge/telemetry/sweep.py
def launch(self) -> pl.DataFrame:
    """The sweep's own row: its closing one when it has finished, else the one its launch wrote; empty when unknown."""
    rows = self.telemetry.sweeps.scan().filter(pl.col(SweepColumn.SWEEP_ID) == self.sweep_id)

    return rows.sort(SweepColumn.INDEXED_AT, maintain_order=True).unique(subset=SweepColumn.SWEEP_ID, keep="last", maintain_order=True).collect()

events

events() -> DataFrame

Every transition of this sweep's points, oldest first, unioning what is still only in the live journals.

Source code in src/femforge/telemetry/sweep.py
def events(self) -> pl.DataFrame:
    """Every transition of this sweep's points, oldest first, unioning what is still only in the live journals."""
    live = scan_journals(self.telemetry.live, journal_glob(TelemetryDir.SWEEP_EVENTS), SWEEP_EVENT_SCHEMA)
    union = pl.concat([self.telemetry.sweep_events.scan(), live])

    return union.filter(pl.col(SweepEventColumn.SWEEP_ID) == self.sweep_id).sort(SweepEventColumn.AT, maintain_order=True).collect()

latest

latest() -> DataFrame

The state each point is in now: its last transition, one row per point_key.

Source code in src/femforge/telemetry/sweep.py
def latest(self) -> pl.DataFrame:
    """The state each point is in now: its last transition, one row per ``point_key``."""
    return self.events().unique(subset=SweepEventColumn.POINT_KEY, keep="last", maintain_order=True)

failures

failures() -> DataFrame

The points whose last transition failed, each carrying the traceback the worker recorded.

Source code in src/femforge/telemetry/sweep.py
def failures(self) -> pl.DataFrame:
    """The points whose last transition failed, each carrying the traceback the worker recorded."""
    return self.latest().filter(pl.col(SweepEventColumn.STATE) == SweepState.FAILED)

summary

summary(now: datetime | None = None) -> SweepSummary

Where the sweep stands at now: the per-state counts, the trailing-window rate, and the ETA that rate implies.

Source code in src/femforge/telemetry/sweep.py
def summary(self, now: datetime | None = None) -> SweepSummary:
    """Where the sweep stands at ``now``: the per-state counts, the trailing-window rate, and the ETA that rate implies."""
    moment: datetime = now if now is not None else datetime.now(timezone.utc)
    latest: pl.DataFrame = self.latest()
    states: list[str] = cast("list[str]", latest[SweepEventColumn.STATE].to_list())

    def count(state: SweepState) -> int:
        return sum(1 for value in states if value == state)

    launch: pl.DataFrame = self.launch()
    started: datetime = cast("datetime", launch[SweepColumn.STARTED_AT][0]) if launch.height else moment
    finished = cast("datetime | None", launch[SweepColumn.FINISHED_AT][0]) if launch.height else None
    elapsed_s: float = ((finished if finished is not None else moment) - started).total_seconds()

    settled = latest.filter(pl.col(SweepEventColumn.STATE).is_in([SweepState.DONE, SweepState.FAILED]))
    recent: int = settled.filter(pl.col(SweepEventColumn.AT) >= moment - RATE_WINDOW).height

    window_s: float = min(RATE_WINDOW.total_seconds(), elapsed_s)
    rate_per_h: float = recent / (window_s / 3600.0) if window_s > 0 else 0.0
    outstanding: int = count(SweepState.QUEUED) + count(SweepState.RUNNING)

    return SweepSummary(
        queued=count(SweepState.QUEUED),
        running=count(SweepState.RUNNING),
        done=count(SweepState.DONE),
        failed=count(SweepState.FAILED),
        skipped=count(SweepState.SKIPPED),
        rate_per_h=rate_per_h,
        eta_s=outstanding / rate_per_h * 3600.0 if rate_per_h > 0 else None,
        elapsed_s=elapsed_s,
    )

SweepSummary dataclass

SweepSummary(queued: int, running: int, done: int, failed: int, skipped: int, rate_per_h: float, eta_s: float | None, elapsed_s: float)

Where a sweep stands: how many points sit in each state, how fast they are landing, and how long the rest should take.

Attributes:

  • queued (int) –

    Points waiting for a worker.

  • running (int) –

    Points a worker is solving.

  • done (int) –

    Points that finished successfully.

  • failed (int) –

    Points that finished with an error.

  • skipped (int) –

    Points already stored when the sweep started.

  • rate_per_h (float) –

    ṅ [1/h]: Points settled per hour over the trailing window.

  • eta_s (float | None) –

    t_eta [s]: Seconds the outstanding points should take at that rate; None while nothing has settled.

  • elapsed_s (float) –

    t_elapsed [s]: Seconds since the sweep started, up to its finish when it has one.

queued instance-attribute

queued: int

Points waiting for a worker.

running instance-attribute

running: int

Points a worker is solving.

done instance-attribute

done: int

Points that finished successfully.

failed instance-attribute

failed: int

Points that finished with an error.

skipped instance-attribute

skipped: int

Points already stored when the sweep started.

rate_per_h instance-attribute

rate_per_h: float

ṅ [1/h]: Points settled per hour over the trailing window.

eta_s instance-attribute

eta_s: float | None

t_eta [s]: Seconds the outstanding points should take at that rate; None while nothing has settled.

elapsed_s instance-attribute

elapsed_s: float

t_elapsed [s]: Seconds since the sweep started, up to its finish when it has one.

TextTail

TextTail(path: Path)

A byte-offset cursor over one growing text file; a truncation rewinds it and an unterminated last line is held back until it ends.

Methods:

  • reset –

    Forget the cursor so the next read starts from the beginning of the file.

  • read_new –

    Every complete line written since the last call, newline stripped; empty when the file is absent or unchanged.

Source code in src/femforge/telemetry/tail.py
def __init__(self, path: Path) -> None:
    """Start a cursor at the beginning of ``path``."""
    self.path: Path = path
    self.offset: int = 0
    self._partial: str = ""

reset

reset() -> None

Forget the cursor so the next read starts from the beginning of the file.

Source code in src/femforge/telemetry/tail.py
def reset(self) -> None:
    """Forget the cursor so the next read starts from the beginning of the file."""
    self.offset = 0
    self._partial = ""

read_new

read_new() -> list[str]

Every complete line written since the last call, newline stripped; empty when the file is absent or unchanged.

Source code in src/femforge/telemetry/tail.py
def read_new(self) -> list[str]:
    """Every complete line written since the last call, newline stripped; empty when the file is absent or unchanged."""
    if not self.path.is_file():
        return []

    size = self.path.stat().st_size
    if size < self.offset:
        self.reset()

    with self.path.open("rb") as stream:
        _ = stream.seek(self.offset)
        chunk = stream.read()
        self.offset = stream.tell()

    if not chunk:
        return []

    text = self._partial + chunk.decode("utf-8", errors="replace")
    lines = text.split("\n")
    self._partial = lines.pop()

    return [line.removesuffix("\r") for line in lines]

LiveLogTap

LiveLogTap(*, process: str | None = None, maxlen: int = BUFFER_SIZE)

Loguru sink keeping the last records in memory and pushing each new one onto every asyncio subscriber queue.

Methods:

  • __call__ –

    The loguru sink protocol; buffer the record, then hand it to each subscriber's loop.

  • snapshot –

    The buffered records, oldest first — what a subscriber renders before its first live record arrives.

  • subscribe –

    Register a queue fed on loop and return it; the caller passes it back to unsubscribe.

  • unsubscribe –

    Stop feeding queue; a queue that was never registered is ignored.

Source code in src/femforge/telemetry/tap.py
def __init__(self, *, process: str | None = None, maxlen: int = BUFFER_SIZE) -> None:
    """Keep the last ``maxlen`` records, each tagged with ``process`` or, when None, the script's name."""
    self.process: str = sanitize_process(process) if process is not None else default_process()

    self._buffer: deque[LogRow] = deque(maxlen=maxlen)
    self._console: Console = Console(file=StringIO(), width=EXTRA_WIDTH)
    self._lock: threading.Lock = threading.Lock()
    self._subscribers: list[tuple[AbstractEventLoop, Queue[LogRow]]] = []

__call__

__call__(message: Message) -> None

The loguru sink protocol; buffer the record, then hand it to each subscriber's loop.

Source code in src/femforge/telemetry/tap.py
def __call__(self, message: Message) -> None:
    """The loguru sink protocol; buffer the record, then hand it to each subscriber's loop."""

    def deliver(queue: Queue[LogRow], row: LogRow) -> None:
        try:
            queue.put_nowait(row)
        except asyncio.QueueFull:
            pass

    row = log_row(message.record, process=self.process, console=self._console)

    with self._lock:
        self._buffer.append(row)
        targets = list(self._subscribers)

    for loop, queue in targets:
        try:
            loop.call_soon_threadsafe(deliver, queue, row)
        except RuntimeError:
            self.unsubscribe(queue)

snapshot

snapshot() -> list[LogRow]

The buffered records, oldest first — what a subscriber renders before its first live record arrives.

Source code in src/femforge/telemetry/tap.py
def snapshot(self) -> list[LogRow]:
    """The buffered records, oldest first — what a subscriber renders before its first live record arrives."""
    with self._lock:
        return list(self._buffer)

subscribe

subscribe(loop: AbstractEventLoop, *, maxsize: int = QUEUE_SIZE) -> Queue[LogRow]

Register a queue fed on loop and return it; the caller passes it back to unsubscribe.

Source code in src/femforge/telemetry/tap.py
def subscribe(self, loop: AbstractEventLoop, *, maxsize: int = QUEUE_SIZE) -> Queue[LogRow]:
    """Register a queue fed on ``loop`` and return it; the caller passes it back to ``unsubscribe``."""
    queue: Queue[LogRow] = asyncio.Queue(maxsize=maxsize)

    with self._lock:
        self._subscribers.append((loop, queue))

    return queue

unsubscribe

unsubscribe(queue: Queue[LogRow]) -> None

Stop feeding queue; a queue that was never registered is ignored.

Source code in src/femforge/telemetry/tap.py
def unsubscribe(self, queue: Queue[LogRow]) -> None:
    """Stop feeding ``queue``; a queue that was never registered is ignored."""
    with self._lock:
        self._subscribers = [entry for entry in self._subscribers if entry[1] is not queue]

current_sweep_point

current_sweep_point() -> SweepPoint | None

The sweep point in scope, or None outside any sweep_point block.

Source code in src/femforge/telemetry/context.py
def current_sweep_point() -> SweepPoint | None:
    """The sweep point in scope, or None outside any ``sweep_point`` block."""
    return _current.get()

sweep_point

sweep_point(sweep_id: str, point_key: str) -> Iterator[SweepPoint]

Mark this context as one sweep point, binding both ids onto every log record emitted inside it.

Source code in src/femforge/telemetry/context.py
@contextmanager
def sweep_point(sweep_id: str, point_key: str) -> Iterator[SweepPoint]:
    """Mark this context as one sweep point, binding both ids onto every log record emitted inside it."""
    point = SweepPoint(sweep_id, point_key)
    token = _current.set(point)

    bound = {LogColumn.SWEEP_ID: sweep_id, LogColumn.POINT_KEY: point_key}
    try:
        with log.contextualize(**bound):
            yield point
    finally:
        _current.reset(token)

scan_convergence

scan_convergence(sim_dir: str | Path, *, with_progress: bool = True) -> DataFrame

A run's convergence.jsonl as a typed frame, carrying each loop's first relative change and how far it has come toward its tolerance.

Source code in src/femforge/telemetry/convergence.py
def scan_convergence(sim_dir: str | Path, *, with_progress: bool = True) -> pl.DataFrame:
    """A run's ``convergence.jsonl`` as a typed frame, carrying each loop's first relative change and how far it has come toward its tolerance."""
    bundle: Path = Path(sim_dir)
    path: Path = bundle / BundleFile.CONVERGENCE
    if not path.is_file() or path.stat().st_size == 0:
        return pl.DataFrame(schema=frame_schema(with_progress=with_progress))

    samples: pl.DataFrame = coerce(pl.scan_ndjson(path.as_posix(), ignore_errors=True), CONVERGENCE_SCHEMA).collect()
    if not with_progress:
        return samples

    # Iteration 1 (re)sets the baseline, so a nonlinear loop restarting inside a steady-state sweep is measured from its own start.
    baseline = pl.when(pl.col(ConvergenceColumn.ITER) == 1).then(pl.col(ConvergenceColumn.RELC)).otherwise(None)
    scored: pl.DataFrame = samples.with_columns(baseline.forward_fill().over([ConvergenceColumn.SOLVER, ConvergenceColumn.LOOP]).alias(DerivedColumn.FIRST_RELC))

    lookup: ConvergenceTargets = ConvergenceTargetsFile.load(bundle).targets()
    solvers: list[str | None] = cast("list[str | None]", scored[ConvergenceColumn.SOLVER].to_list())
    loops: list[str | None] = cast("list[str | None]", scored[ConvergenceColumn.LOOP].to_list())
    firsts: list[float | None] = cast("list[float | None]", scored[DerivedColumn.FIRST_RELC].to_list())
    changes: list[float | None] = cast("list[float | None]", scored[ConvergenceColumn.RELC].to_list())

    def tolerance(solver: str | None, loop: str | None) -> float | None:
        """What this sample's loop was authored to converge to, or None when the sif left Elmer's default."""
        target: LoopTargets | None = lookup.get((normalize_solver_key(solver or ""), cast("Literal['NS', 'SS']", loop)))

        return target.tolerance if target is not None else None

    covered: list[float | None] = []
    for solver, loop, first, relc in zip(solvers, loops, firsts, changes, strict=True):
        covered.append(convergence_progress(first, relc, tolerance(solver, loop)) if relc is not None else None)

    return scored.with_columns(pl.Series(DerivedColumn.PROGRESS, covered, dtype=pl.Float64))

coerce

coerce(frame: LazyFrame, schema: Schema) -> LazyFrame

Cast a loosely-typed frame (JSON text, a partial row set) onto schema, parsing date/time strings and filling missing columns with nulls.

Source code in src/femforge/telemetry/hive.py
def coerce(frame: pl.LazyFrame, schema: pl.Schema) -> pl.LazyFrame:
    """Cast a loosely-typed frame (JSON text, a partial row set) onto ``schema``, parsing date/time strings and filling missing columns with nulls."""
    present = frame.collect_schema()

    columns: list[pl.Expr] = []
    for name, dtype in schema.items():
        source = present.get(name)
        if source is None:
            columns.append(pl.lit(None, dtype).alias(name))
            continue

        column = pl.col(name)
        if isinstance(dtype, pl.Enum) and source != dtype:
            # An unrecognised category nulls rather than raising: a stray value must not kill the flush that carries it.
            columns.append(column.cast(dtype, strict=False).alias(name))
            continue

        if source == pl.String and isinstance(dtype, pl.Datetime):
            columns.append(column.str.to_datetime(time_unit=dtype.time_unit, time_zone=dtype.time_zone).alias(name))
            continue

        if source == pl.String and dtype == pl.Date:
            columns.append(column.str.to_date().alias(name))
            continue

        columns.append(column.cast(dtype).alias(name))

    return frame.select(columns)

journal_glob

journal_glob(name: str) -> str

The glob matching every process's journal for the table name.

Source code in src/femforge/telemetry/journal.py
def journal_glob(name: str) -> str:
    """The glob matching every process's journal for the table ``name``."""
    return f"{name}-*{JOURNAL_SUFFIX}"

recover_journals

recover_journals(live_dir: Path, store: HiveStore, pattern: str, *, alive: Callable[[int], bool] = pid_exists) -> list[Path]

Checkpoint and delete the journals of processes that are gone, so a crash costs no records; returns the journals recovered.

Parameters:

  • live_dir (Path) –

    The directory holding the journals.

  • store (HiveStore) –

    The table the recovered records are appended to.

  • pattern (str) –

    The glob selecting that table's journals.

  • alive (Callable[[int], bool], default: pid_exists ) –

    Whether a process id is still running; the journal of a live writer is left alone.

Source code in src/femforge/telemetry/journal.py
def recover_journals(live_dir: Path, store: HiveStore, pattern: str, *, alive: Callable[[int], bool] = psutil.pid_exists) -> list[Path]:
    """Checkpoint and delete the journals of processes that are gone, so a crash costs no records; returns the journals recovered.

    Args:
        live_dir: The directory holding the journals.
        store: The table the recovered records are appended to.
        pattern: The glob selecting that table's journals.
        alive: Whether a process id is still running; the journal of a live writer is left alone.
    """
    if not live_dir.is_dir():
        return []

    def journal_pid(path: Path) -> int | None:
        _, _, tail = path.stem.rpartition("-")
        if not tail.isdigit():
            return None

        return int(tail)

    recovered: list[Path] = []
    for path in sorted(live_dir.glob(pattern)):
        pid = journal_pid(path)
        if pid is None or alive(pid):
            continue

        records, skipped = journal_lines(path)

        if skipped:
            log.bind(journal=path.name, skipped=skipped).warning("Recovered journal had lines that were not JSON records.")

        rows = records_frame(records, store.schema).collect()
        _ = store.append(rows)
        path.unlink()
        recovered.append(path)

    return recovered

scan_journals

scan_journals(live_dir: Path, pattern: str, schema: Schema) -> LazyFrame

A frame over every matching journal's records, coerced onto schema; lines that are not JSON records are skipped.

Parameters:

  • live_dir (Path) –

    The directory holding the journals.

  • pattern (str) –

    The glob selecting the journals to read.

  • schema (Schema) –

    The table schema the records are coerced onto.

Source code in src/femforge/telemetry/journal.py
def scan_journals(live_dir: Path, pattern: str, schema: pl.Schema) -> pl.LazyFrame:
    """A frame over every matching journal's records, coerced onto ``schema``; lines that are not JSON records are skipped.

    Args:
        live_dir: The directory holding the journals.
        pattern: The glob selecting the journals to read.
        schema: The table schema the records are coerced onto.
    """
    if not live_dir.is_dir():
        return pl.LazyFrame(schema=schema)

    records: list[str] = [line for path in sorted(live_dir.glob(pattern)) for line in journal_lines(path)[0]]
    return records_frame(records, schema)

require_root

require_root(explicit: str | Path | None = None) -> Path

The telemetry root a reader needs, or TelemetryRootMissing naming both ways to provide one.

Source code in src/femforge/telemetry/paths.py
def require_root(explicit: str | Path | None = None) -> Path:
    """The telemetry root a reader needs, or [`TelemetryRootMissing`][] naming both ways to provide one."""
    root: Path | None = resolve_root(explicit)
    if root is None:
        raise TelemetryRootMissing(f"no telemetry root: pass --telemetry or set {TELEMETRY_ENV}")

    return root

resolve_root

resolve_root(explicit: str | Path | None = None) -> Path | None

The telemetry root: explicit if given, else FEMFORGE_TELEMETRY, else None because telemetry is off.

Source code in src/femforge/telemetry/paths.py
def resolve_root(explicit: str | Path | None = None) -> Path | None:
    """The telemetry root: ``explicit`` if given, else ``FEMFORGE_TELEMETRY``, else None because telemetry is off."""
    if explicit is not None:
        return Path(explicit)

    from_env = os.environ.get(TELEMETRY_ENV)
    if not from_env:
        return None

    return Path(from_env)

live_runs

live_runs(runs_dir: str | Path) -> DataFrame

The runs claimed right now under runs_dir, read from each <run_id>.lock/owner.json with the age of its heartbeat.

Source code in src/femforge/telemetry/runs.py
def live_runs(runs_dir: str | Path) -> pl.DataFrame:
    """The runs claimed right now under ``runs_dir``, read from each ``<run_id>.lock/owner.json`` with the age of its heartbeat."""
    directory = Path(runs_dir)
    if not directory.is_dir():
        return pl.DataFrame(schema=LIVE_RUN_SCHEMA)

    now = datetime.now(timezone.utc)

    rows: list[dict[str, object]] = []
    for lock in sorted(directory.glob("*" + LOCK_SUFFIX)):
        owner = DirLock(lock).owner()
        if owner is None:
            continue

        age: float = (now - owner.heartbeat_at).total_seconds()
        rows.append(
            {
                LiveRunColumn.RUN_ID: lock.name.removesuffix(LOCK_SUFFIX),
                LiveRunColumn.HOST: owner.host,
                LiveRunColumn.PID: owner.pid,
                LiveRunColumn.STARTED_AT: owner.started_at,
                LiveRunColumn.HEARTBEAT_AT: owner.heartbeat_at,
                LiveRunColumn.HEARTBEAT_AGE_S: age,
                LiveRunColumn.STALE: age > STALE_AFTER_S,
            }
        )

    return pl.DataFrame(rows, schema=LIVE_RUN_SCHEMA)

scan_logs

scan_logs(telemetry: Telemetry) -> LazyFrame

Every log record: the settled parquet shards unioned with what is still only in the live journals.

Source code in src/femforge/telemetry/sink.py
def scan_logs(telemetry: Telemetry) -> pl.LazyFrame:
    """Every log record: the settled parquet shards unioned with what is still only in the live journals."""
    live = scan_journals(telemetry.live, journal_glob(TelemetryDir.LOGS), LOG_SCHEMA)

    return pl.concat([telemetry.logs.scan(), live])

sweep_runs

sweep_runs(telemetry: Telemetry) -> DataFrame

Every sweep the root knows, newest first and one row each (a finished sweep's closing row wins).

Source code in src/femforge/telemetry/sweep.py
def sweep_runs(telemetry: Telemetry) -> pl.DataFrame:
    """Every sweep the root knows, newest first and one row each (a finished sweep's closing row wins)."""
    deduped = telemetry.sweeps.scan().sort(SweepColumn.INDEXED_AT, maintain_order=True).unique(subset=SweepColumn.SWEEP_ID, keep="last", maintain_order=True)

    return deduped.sort(SweepColumn.STARTED_AT, descending=True, nulls_last=True, maintain_order=True).collect()