Skip to content

Commit 08fd7c7

Browse files
authored
Add Content Blocks (#7)
1 parent cb5df08 commit 08fd7c7

File tree

8 files changed

+266
-8
lines changed

8 files changed

+266
-8
lines changed

ablate/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from . import queries, sources
1+
from . import blocks, queries, sources
22

33

4-
__all__ = ["queries", "sources"]
4+
__all__ = ["blocks", "queries", "sources"]
55

66
__version__ = "0.1.0"

ablate/blocks/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from .abstract_block import AbstractBlock
2+
from .figure_blocks import AbstractFigureBlock, MetricPlot
3+
from .table_blocks import AbstractTableBlock, Table
4+
from .text_blocks import H1, H2, H3, H4, H5, H6, AbstractTextBlock, Text
5+
6+
7+
__all__ = [
8+
"AbstractBlock",
9+
"AbstractFigureBlock",
10+
"AbstractTableBlock",
11+
"AbstractTextBlock",
12+
"H1",
13+
"H2",
14+
"H3",
15+
"H4",
16+
"H5",
17+
"H6",
18+
"MetricPlot",
19+
"Table",
20+
"Text",
21+
]

ablate/blocks/abstract_block.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Any, List
3+
4+
from ablate.core.types import Run
5+
6+
7+
class AbstractBlock(ABC):
8+
def __init__(self, runs: List[Run] | None = None) -> None:
9+
"""Abstract content block for a report.
10+
11+
Args:
12+
runs: Optional list of runs to be used for the block instead of the default
13+
runs from the report. Defaults to None.
14+
"""
15+
self.runs = runs
16+
17+
@abstractmethod
18+
def build(self, runs: List[Run]) -> Any:
19+
"""Build the intermediate representation of the block, ready for rendering.
20+
21+
Args:
22+
runs: List of runs to be used for the block.
23+
24+
Returns:
25+
The intermediate representation of the block.
26+
"""

ablate/blocks/figure_blocks.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from __future__ import annotations
2+
3+
from abc import ABC, abstractmethod
4+
from typing import TYPE_CHECKING, List
5+
6+
import pandas as pd
7+
8+
from ablate.queries import AbstractMetric, Id, Param
9+
10+
from .abstract_block import AbstractBlock
11+
12+
13+
if TYPE_CHECKING: # pragma: no cover
14+
from ablate.core.types import Run
15+
16+
17+
class AbstractFigureBlock(AbstractBlock, ABC):
18+
@abstractmethod
19+
def build(self, runs: List[Run]) -> pd.DataFrame: ...
20+
21+
22+
class MetricPlot(AbstractBlock):
23+
def __init__(
24+
self,
25+
metric: AbstractMetric | List[AbstractMetric],
26+
identifier: Param | None = None,
27+
runs: List[Run] | None = None,
28+
) -> None:
29+
"""Block for plotting metrics over time.
30+
31+
Args:
32+
metric: Metric or list of metrics to be plotted over time.
33+
identifier: Optional identifier for the runs. If None, the run ID is used.
34+
Defaults to None.
35+
runs: Optional list of runs to be used for the block instead of the default
36+
runs from the report. Defaults to None.
37+
"""
38+
super().__init__(runs)
39+
self.metrics = metric if isinstance(metric, list) else [metric]
40+
self.identifier = identifier or Id()
41+
42+
def build(self, runs: List[Run]) -> pd.DataFrame:
43+
data = []
44+
for run in runs:
45+
for metric in self.metrics:
46+
series = run.temporal.get(metric.name, [])
47+
for step, value in series:
48+
data.append(
49+
{
50+
"step": step,
51+
"value": value,
52+
"metric": metric.label,
53+
"run": self.identifier(run),
54+
"run_id": run.id,
55+
}
56+
)
57+
return pd.DataFrame(data)

ablate/blocks/table_blocks.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from abc import ABC, abstractmethod
2+
from typing import List
3+
4+
import pandas as pd
5+
6+
from ablate.core.types import Run
7+
from ablate.queries import AbstractSelector
8+
9+
from .abstract_block import AbstractBlock
10+
11+
12+
class AbstractTableBlock(AbstractBlock, ABC):
13+
def __init__(
14+
self,
15+
columns: List[AbstractSelector],
16+
runs: List[Run] | None = None,
17+
) -> None:
18+
"""Table block for a report.
19+
20+
Args:
21+
columns: Columns to be included in the table. Each column is defined by a
22+
selector that extracts the data from the runs.
23+
runs: Optional list of runs to be used for the block instead of the default
24+
runs from the report. Defaults to None.
25+
"""
26+
super().__init__(runs)
27+
self.columns = columns
28+
29+
@abstractmethod
30+
def build(self, runs: List[Run]) -> pd.DataFrame: ...
31+
32+
33+
class Table(AbstractTableBlock):
34+
def build(self, runs: List[Run]) -> pd.DataFrame:
35+
rows = []
36+
for run in runs:
37+
row = {}
38+
for column in self.columns:
39+
row[column.label] = column(run)
40+
rows.append(row)
41+
return pd.DataFrame(rows, columns=[column.label for column in self.columns])

ablate/blocks/text_blocks.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from abc import ABC
2+
from typing import List
3+
4+
from ablate.core.types import Run
5+
6+
from .abstract_block import AbstractBlock
7+
8+
9+
class AbstractTextBlock(AbstractBlock, ABC):
10+
def __init__(self, text: str, runs: List[Run] | None = None) -> None:
11+
"""Block containing styled text for a report.
12+
13+
Args:
14+
text: The text content of the block.
15+
runs: Optional list of runs to be used for the block instead of the default
16+
runs from the report. Defaults to None.
17+
"""
18+
super().__init__(runs)
19+
self.text = text
20+
21+
def build(self, runs: List[Run]) -> str:
22+
return self.text.strip()
23+
24+
25+
class H1(AbstractTextBlock): ...
26+
27+
28+
class H2(AbstractTextBlock): ...
29+
30+
31+
class H3(AbstractTextBlock): ...
32+
33+
34+
class H4(AbstractTextBlock): ...
35+
36+
37+
class H5(AbstractTextBlock): ...
38+
39+
40+
class H6(AbstractTextBlock): ...
41+
42+
43+
class Text(AbstractTextBlock): ...

ablate/queries/selectors.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66

77

88
class AbstractSelector(ABC):
9-
def __init__(self, name: str) -> None:
9+
def __init__(self, name: str, label: str | None = None) -> None:
1010
"""Abstract class for selecting runs based on a specific attribute.
1111
1212
Args:
1313
name: Name of the attribute to select on.
14+
label: Optional label for displaying purposes. If None, defaults to `name`.
15+
Defaults to None.
1416
"""
1517
self.name = name
18+
self.label = label or name
1619

1720
@abstractmethod
1821
def __call__(self, run: Run) -> Any: ...
@@ -43,9 +46,14 @@ class AbstractParam(AbstractSelector, ABC): ...
4346

4447

4548
class Id(AbstractParam):
46-
def __init__(self) -> None:
47-
"""Selector for the ID of the run."""
48-
super().__init__("id")
49+
def __init__(self, label: str | None = None) -> None:
50+
"""Selector for the ID of the run.
51+
52+
Args:
53+
label: Optional label for displaying purposes. If None, defaults to `name`.
54+
Defaults to None.
55+
"""
56+
super().__init__("id", label)
4957

5058
def __call__(self, run: Run) -> str:
5159
return run.id
@@ -63,8 +71,9 @@ def __init__(
6371
self,
6472
name: str,
6573
direction: Literal["min", "max"],
74+
label: str | None = None,
6675
) -> None:
67-
super().__init__(name)
76+
super().__init__(name, label)
6877
if direction not in ("min", "max"):
6978
raise ValueError(
7079
f"Invalid direction: '{direction}'. Must be 'min' or 'max'."
@@ -79,6 +88,8 @@ class Metric(AbstractMetric):
7988
name: Name of the metric to select on.
8089
direction: Direction of the metric. "min" for minimization, "max" for
8190
maximization.
91+
label: Optional label for displaying purposes. If None, defaults to `name`.
92+
Defaults to None.
8293
"""
8394

8495
def __call__(self, run: Run) -> float:
@@ -94,6 +105,7 @@ def __init__(
94105
name: str,
95106
direction: Literal["min", "max"],
96107
reduction: Literal["min", "max", "first", "last"] | None = None,
108+
label: str | None = None,
97109
) -> None:
98110
"""Selector for a specific temporal metric of the run.
99111
@@ -105,8 +117,10 @@ def __init__(
105117
minimum, "max" for maximum, "first" for the first value, and "last"
106118
for the last value. If None, the direction is used as the reduction.
107119
Defaults to None.
120+
label: Optional label for displaying purposes. If None, defaults to `name`.
121+
Defaults to None.
108122
"""
109-
super().__init__(name, direction)
123+
super().__init__(name, direction, label)
110124
if reduction is not None and reduction not in ("min", "max", "first", "last"):
111125
raise ValueError(
112126
f"Invalid reduction method: '{reduction}'. Must be 'min', 'max', "

tests/blocks/test_blocks.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from typing import List
2+
3+
import pandas as pd
4+
5+
from ablate.blocks import H1, MetricPlot, Table, Text
6+
from ablate.core.types import Run
7+
from ablate.queries.selectors import Metric, Param
8+
9+
10+
def make_runs() -> List[Run]:
11+
return [
12+
Run(
13+
id="a",
14+
params={"model": "resnet", "seed": 1},
15+
metrics={"accuracy": 0.7},
16+
temporal={"accuracy": [(0, 0.6), (1, 0.7)]},
17+
),
18+
Run(
19+
id="b",
20+
params={"model": "resnet", "seed": 2},
21+
metrics={"accuracy": 0.8},
22+
temporal={"accuracy": [(0, 0.7), (1, 0.8)]},
23+
),
24+
]
25+
26+
27+
def test_text_blocks() -> None:
28+
assert Text(" simple ").build(make_runs()) == "simple"
29+
assert H1("# Title").build(make_runs()) == "# Title"
30+
31+
32+
def test_table_block() -> None:
33+
table = Table(columns=[Param("model"), Param("seed")])
34+
df = table.build(make_runs())
35+
assert isinstance(df, pd.DataFrame)
36+
assert list(df.columns) == ["model", "seed"]
37+
assert df.iloc[0]["model"] == "resnet"
38+
39+
40+
def test_metric_plot_single() -> None:
41+
plot = MetricPlot(
42+
metric=Metric("accuracy", direction="max"), identifier=Param("seed")
43+
)
44+
df = plot.build(make_runs())
45+
assert isinstance(df, pd.DataFrame)
46+
assert set(df.columns) >= {"step", "value", "metric", "run", "run_id"}
47+
assert df["metric"].unique().tolist() == ["accuracy"]
48+
49+
50+
def test_metric_plot_multi() -> None:
51+
plot = MetricPlot(
52+
metric=[Metric("accuracy", direction="max")], identifier=Param("seed")
53+
)
54+
df = plot.build(make_runs())
55+
assert isinstance(df, pd.DataFrame)
56+
assert all(k in df.columns for k in ["step", "value", "metric", "run"])

0 commit comments

Comments
 (0)