Scroll the runs table, pick a range, and choose what a comparison plots against
Docs / docs (push) Successful in 35s
Playwright Tests / test-playwright (1, 2) (push) Failing after 3m14s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m44s
pre-commit / pre-commit (push) Failing after 3m54s
Test Backend / test-backend (push) Successful in 3m12s
Compose Smoke Test / test-compose (push) Successful in 32s
Playwright Tests / merge-reports (push) Failing after 1m28s
Docs / docs (push) Successful in 35s
Playwright Tests / test-playwright (1, 2) (push) Failing after 3m14s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m44s
pre-commit / pre-commit (push) Failing after 3m54s
Test Backend / test-backend (push) Successful in 3m12s
Compose Smoke Test / test-compose (push) Successful in 32s
Playwright Tests / merge-reports (push) Failing after 1m28s
This commit is contained in:
@@ -132,6 +132,9 @@ class MetricSeries(BaseModel):
|
||||
|
||||
class SeriesAnswer(BaseModel):
|
||||
metric: str
|
||||
#: What the x values are: "step", "time" (seconds since this run's first
|
||||
#: reading), or the name of another metric this one was plotted against.
|
||||
x: str = "step"
|
||||
lines: list[MetricSeries] = Field(default_factory=list)
|
||||
|
||||
|
||||
@@ -356,13 +359,38 @@ def read_metrics(
|
||||
return rows
|
||||
|
||||
|
||||
def _points(session: Session, run_id: str, metric: str, x: str) -> list[list[float]]:
|
||||
"""One run's readings of ``metric``, against whichever x was asked for.
|
||||
|
||||
The step is the default because it is what every run has. Time answers
|
||||
"which one got there sooner", and is measured from this run's own first
|
||||
reading so that runs started hours apart still lie on top of each other.
|
||||
Another metric answers "against what the loop was actually counting" — an
|
||||
epoch, or samples seen — and is joined on the step the two share, which is
|
||||
the only thing they have in common.
|
||||
"""
|
||||
rows = _series(session, run_id, metric)
|
||||
if x == "time":
|
||||
if not rows:
|
||||
return []
|
||||
start = min(row.ts for row in rows)
|
||||
return [[row.ts - start, row.value] for row in rows]
|
||||
if x and x != "step":
|
||||
against = {row.step: row.value for row in _series(session, run_id, x)}
|
||||
return [[against[row.step], row.value] for row in rows if row.step in against]
|
||||
return [[float(row.step), row.value] for row in rows]
|
||||
|
||||
|
||||
@router.get("/series/compare", response_model=SeriesAnswer)
|
||||
def compare_metric(session: SessionDep, ids: str, metric: str) -> Any:
|
||||
def compare_metric(session: SessionDep, ids: str, metric: str, x: str = "") -> Any:
|
||||
"""One metric across several runs, as the chart widget's series shape.
|
||||
|
||||
This is the comparison view: it answers in the same shape a flow answers a
|
||||
chart's query with, so putting three training curves beside each other is
|
||||
a widget binding rather than a screen of its own.
|
||||
|
||||
``x`` names what to plot against — nothing or "step", "time", or another
|
||||
metric of the same runs.
|
||||
"""
|
||||
run_ids = [part for part in ids.split(",") if part]
|
||||
if not run_ids:
|
||||
@@ -376,13 +404,10 @@ def compare_metric(session: SessionDep, ids: str, metric: str) -> Any:
|
||||
run = runs.get(run_id)
|
||||
if run is None:
|
||||
continue
|
||||
rows = _series(session, run_id, metric)
|
||||
label = run_id
|
||||
if run.seed is not None:
|
||||
label = f"{run_id} (seed {run.seed})"
|
||||
lines.append(
|
||||
MetricSeries(
|
||||
label=label, points=[[float(row.step), row.value] for row in rows]
|
||||
)
|
||||
MetricSeries(label=label, points=_points(session, run_id, metric, x))
|
||||
)
|
||||
return SeriesAnswer(metric=metric, lines=lines)
|
||||
return SeriesAnswer(metric=metric, x=x or "step", lines=lines)
|
||||
|
||||
@@ -8,7 +8,7 @@ import json
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
from sqlmodel import Session, select
|
||||
from sqlmodel import Session, col, select
|
||||
|
||||
from fluksio.core.config import settings
|
||||
from fluksio.core.db import engine as db_engine
|
||||
@@ -380,3 +380,85 @@ def test_a_curve_whose_run_is_gone_is_empty_rather_than_an_error(
|
||||
)
|
||||
assert answer.status_code == 200
|
||||
assert answer.json() == []
|
||||
|
||||
|
||||
def _metric(run_id: str, name: str, step: int, value: float, ts: float) -> RunMetric:
|
||||
return RunMetric(run_id=run_id, name=name, step=step, value=value, ts=ts)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def plotted():
|
||||
"""A run with a loss curve and an epoch counter beside it."""
|
||||
run_id = new_run_id()
|
||||
with Session(db_engine) as session:
|
||||
session.add(
|
||||
Run(id=run_id, flow="study", status="ok", created_at=datetime.now(UTC))
|
||||
)
|
||||
for step, (loss, epoch) in enumerate([(1.0, 10.0), (0.5, 20.0), (0.25, 30.0)]):
|
||||
session.add(_metric(run_id, "study.loss", step, loss, 100.0 + step * 5))
|
||||
session.add(_metric(run_id, "study.epoch", step, epoch, 100.0 + step * 5))
|
||||
session.commit()
|
||||
yield run_id
|
||||
with Session(db_engine) as session:
|
||||
for row in session.exec(
|
||||
select(RunMetric).where(col(RunMetric.run_id) == run_id)
|
||||
).all():
|
||||
session.delete(row)
|
||||
session.delete(session.get(Run, run_id))
|
||||
session.commit()
|
||||
|
||||
|
||||
def test_a_comparison_is_plotted_against_the_step_by_default(
|
||||
client, superuser_token_headers, plotted
|
||||
):
|
||||
answer = client.get(
|
||||
f"{settings.API_V1_STR}/runs/series/compare",
|
||||
params={"ids": plotted, "metric": "study.loss"},
|
||||
headers=superuser_token_headers,
|
||||
).json()
|
||||
|
||||
assert answer["x"] == "step"
|
||||
assert answer["lines"][0]["points"] == [[0.0, 1.0], [1.0, 0.5], [2.0, 0.25]]
|
||||
|
||||
|
||||
def test_time_is_measured_from_this_runs_own_first_reading(
|
||||
client, superuser_token_headers, plotted
|
||||
):
|
||||
"""Runs started hours apart still lie on top of each other."""
|
||||
answer = client.get(
|
||||
f"{settings.API_V1_STR}/runs/series/compare",
|
||||
params={"ids": plotted, "metric": "study.loss", "x": "time"},
|
||||
headers=superuser_token_headers,
|
||||
).json()
|
||||
|
||||
assert answer["x"] == "time"
|
||||
assert answer["lines"][0]["points"] == [[0.0, 1.0], [5.0, 0.5], [10.0, 0.25]]
|
||||
|
||||
|
||||
def test_one_metric_can_be_plotted_against_another(
|
||||
client, superuser_token_headers, plotted
|
||||
):
|
||||
answer = client.get(
|
||||
f"{settings.API_V1_STR}/runs/series/compare",
|
||||
params={"ids": plotted, "metric": "study.loss", "x": "study.epoch"},
|
||||
headers=superuser_token_headers,
|
||||
).json()
|
||||
|
||||
assert answer["lines"][0]["points"] == [[10.0, 1.0], [20.0, 0.5], [30.0, 0.25]]
|
||||
|
||||
|
||||
def test_a_step_the_x_metric_never_reached_is_left_out(
|
||||
client, superuser_token_headers, plotted
|
||||
):
|
||||
"""The join is on the step, which is the only thing two series share."""
|
||||
with Session(db_engine) as session:
|
||||
session.add(_metric(plotted, "study.loss", 3, 0.1, 120.0))
|
||||
session.commit()
|
||||
|
||||
answer = client.get(
|
||||
f"{settings.API_V1_STR}/runs/series/compare",
|
||||
params={"ids": plotted, "metric": "study.loss", "x": "study.epoch"},
|
||||
headers=superuser_token_headers,
|
||||
).json()
|
||||
|
||||
assert [point[0] for point in answer["lines"][0]["points"]] == [10.0, 20.0, 30.0]
|
||||
|
||||
Reference in New Issue
Block a user