Skip to content

Commit 0c9f797

Browse files
Merge pull request #65 from Demonstrandum/cursor/profile-wrapper-object-e684
Profile wrapper object
2 parents a2ecee0 + 3cbe554 commit 0c9f797

6 files changed

Lines changed: 1263 additions & 242 deletions

File tree

AGENTS_DEV.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,15 +202,19 @@ Plugins are registered via entry points in `pyproject.toml` or discovered by the
202202

203203
Python API for training scripts to configure default dashboard profiles. Writes `<logdir>/.tensorboard/default_profile.json`.
204204

205-
Key functions:
206-
207-
- `create_profile(...)` — builds a profile dict
208-
- `write_profile(logdir, profile)` — writes to disk
205+
The core type is the `Profile` class, which wraps the camelCase JSON the frontend expects and exposes snake_case properties with getters and setters:
206+
207+
- `create_profile(...)` — returns a `Profile` object
208+
- `Profile.write(logdir)` — serialises and writes to disk
209+
- `Profile.serialize()` — returns the `SerializedProfile` dict
210+
- `Profile.load(logdir)` / `Profile.from_serialized(dict)` — construct from existing data
211+
- `Profile.update(other)` / `a | b` — merge two profiles (dicts merged, lists extended, scalars replaced)
212+
- `write_profile(logdir, profile)` — writes a `Profile` or `SerializedProfile` to disk
209213
- `set_default_profile(logdir, ...)` — convenience: create + write in one call
210214
- `pin_scalar(tag)`, `pin_histogram(tag, run_id)`, `pin_image(tag, run_id, sample)` — helpers for pinned card entries
211215
- `create_superimposed_card(title, tags, run_id)` — helper for superimposed card entries
212216

213-
Key parameters of `create_profile` / `set_default_profile` include `pinned_cards`, `run_colors`, `superimposed_cards`, `run_selection`, `selected_runs`, `metric_descriptions`, `tag_filter`, `smoothing`, `y_axis_scale`, `x_axis_scale`, `tag_axis_scales`, `symlog_linear_threshold`, `tag_symlog_linear_thresholds`, `expanded_tag_groups`, and `group_by`. See the docstrings for full details.
217+
`Profile` properties include `name`, `pinned_cards`, `run_colors` (dict, not list), `group_colors` (dict), `superimposed_cards`, `run_selection`, `metric_descriptions`, `tag_filter`, `smoothing`, `y_axis_scale`, `x_axis_scale`, `tag_axis_scales`, `symlog_linear_threshold`, `tag_symlog_linear_thresholds`, `expanded_tag_groups`, and `group_by`. Convenience methods: `pin_scalar()`, `pin_histogram()`, `pin_image()`, `add_superimposed_card()`, `select_runs()`. See the docstrings for full details.
214218

215219
Profile data schema version is tracked via `PROFILE_VERSION = 1`.
216220

@@ -366,7 +370,11 @@ Key CI details:
366370
2. Update `createEmptyProfile()` to include the new field's default value
367371
3. Update `isValidProfile()` to validate the new field
368372
4. If the change is breaking: bump `PROFILE_VERSION` and add migration logic in `migrateProfile()`
369-
5. Update `profile_writer.py` (`create_profile` function) to accept the new field
373+
5. Update `profile_writer.py`:
374+
- Add the field to the `ProfileData` TypedDict (and `_ProfileDataRequired` if required)
375+
- Add a property (getter + setter) on the `Profile` class
376+
- Add the field to `Profile.__init__`, `serialize()`, `from_serialized()`, `update()`, and `__or__`
377+
- Add the field to the `create_profile()` / `set_default_profile()` signatures
370378
6. Update `profile_effects.ts` to apply the new field when activating a profile
371379

372380
### Adding a New Superimposed Card Feature

AGENTS_DOC.md

Lines changed: 97 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@ To take advantage of the new features, you can optionally configure a dashboard
8484
```python
8585
from tensorbored.plugins.core import profile_writer
8686

87-
profile_writer.set_default_profile(
88-
logdir='./logs',
89-
name='My Experiment',
87+
p = profile_writer.create_profile(
88+
'My Experiment',
9089
pinned_cards=[
9190
profile_writer.pin_scalar('train/loss'),
9291
profile_writer.pin_scalar('eval/accuracy'),
9392
],
9493
smoothing=0.8,
9594
)
95+
p.write('./logs')
9696
```
9797

9898
When anyone opens TensorBored pointed at this logdir, they get your pre-configured view automatically.
@@ -151,13 +151,13 @@ This replaces TensorBoard's approach of sharing long URLs.
151151

152152
The most powerful feature: configure the dashboard **before users even open it**.
153153

154+
`create_profile` returns a `Profile` object you can inspect, tweak, merge, and write to disk:
155+
154156
```python
155157
from tensorbored.plugins.core import profile_writer, color_sampler
156158

157-
# This writes <logdir>/.tensorboard/default_profile.json
158-
profile_writer.set_default_profile(
159-
logdir='/path/to/logs',
160-
name='Training Monitor',
159+
p = profile_writer.create_profile(
160+
'Training Monitor',
161161

162162
# Pin your most important metrics at the top
163163
pinned_cards=[
@@ -207,10 +207,34 @@ profile_writer.set_default_profile(
207207
'debug': False,
208208
},
209209
)
210+
211+
# Tweak after creation
212+
p.pin_scalar('gradients/global_norm')
213+
p.run_colors['experiment_v3'] = '#FF9800'
214+
215+
# Write to <logdir>/.tensorboard/default_profile.json
216+
p.write('/path/to/logs')
217+
```
218+
219+
Profiles can also be **merged** with `|` or `update()`:
220+
221+
```python
222+
base = profile_writer.create_profile(
223+
'Base', smoothing=0.8, run_colors={'train': '#2196F3'},
224+
)
225+
overlay = profile_writer.create_profile(
226+
'Overlay',
227+
pinned_cards=[profile_writer.pin_scalar('eval/loss')],
228+
y_axis_scale='log10',
229+
)
230+
combined = base | overlay # dicts merged, lists extended, scalars from overlay
231+
combined.write(logdir)
210232
```
211233

212234
The default profile auto-applies when a user opens TensorBored at this logdir, but only if they don't already have a local profile active. User-created profiles always take priority over the backend default.
213235

236+
The one-shot `set_default_profile(logdir, ...)` helper is still available if you don't need the intermediate `Profile` object.
237+
214238
### Profile Data Schema
215239

216240
A profile contains the following fields:
@@ -258,23 +282,11 @@ Superimposed cards support:
258282
```python
259283
from tensorbored.plugins.core import profile_writer
260284

261-
profile_writer.set_default_profile(
262-
logdir='./logs',
263-
superimposed_cards=[
264-
profile_writer.create_superimposed_card(
265-
title='Train vs Eval Loss',
266-
tags=['loss/train', 'loss/eval'],
267-
),
268-
profile_writer.create_superimposed_card(
269-
title='All Accuracies',
270-
tags=['accuracy/train', 'accuracy/eval', 'accuracy/test'],
271-
),
272-
profile_writer.create_superimposed_card(
273-
title='Loss + Grad Norm',
274-
tags=['loss/train', 'gradients/global_norm'],
275-
),
276-
],
277-
)
285+
p = profile_writer.create_profile()
286+
p.add_superimposed_card('Train vs Eval Loss', ['loss/train', 'loss/eval'])
287+
p.add_superimposed_card('All Accuracies', ['accuracy/train', 'accuracy/eval', 'accuracy/test'])
288+
p.add_superimposed_card('Loss + Grad Norm', ['loss/train', 'gradients/global_norm'])
289+
p.write('./logs')
278290
```
279291

280292
---
@@ -300,8 +312,7 @@ Add long-form descriptions that appear as hover tooltips on metric card headers.
300312
```python
301313
from tensorbored.plugins.core import profile_writer
302314

303-
profile_writer.set_default_profile(
304-
logdir='./logs',
315+
p = profile_writer.create_profile(
305316
metric_descriptions={
306317
'loss/train': 'Cross-entropy training loss used for backpropagation.',
307318
'loss/eval': 'Cross-entropy loss on the held-out validation split, computed every 100 steps.',
@@ -311,6 +322,7 @@ profile_writer.set_default_profile(
311322
'gradients/global_norm': 'Global L2 norm of all model gradients before clipping.',
312323
},
313324
)
325+
p.write('./logs')
314326
```
315327

316328
Descriptions are set via the `metric_descriptions` parameter in the profile writer. They support Markdown formatting. The descriptions are served by the backend through the `/data/tags` endpoint and displayed as tooltips on scalar, histogram, and image cards.
@@ -527,9 +539,66 @@ The `SummaryWriter` API is identical — only the import changes. You no longer
527539

528540
**Module:** `tensorbored.plugins.core.profile_writer`
529541

542+
#### `Profile` class
543+
544+
The core type. Wraps the camelCase JSON the frontend expects behind snake_case properties. Created via `create_profile()`, `Profile(...)`, `Profile.load(logdir)`, or `Profile.from_serialized(dict)`.
545+
546+
**Properties** (all have getters and setters):
547+
548+
| Property | Type | Default | Description |
549+
|----------|------|---------|-------------|
550+
| `name` | `str` | `"Default Profile"` | Display name |
551+
| `pinned_cards` | `list[dict]` | `[]` | Cards to pin (use `pin_scalar` etc.) |
552+
| `run_colors` | `dict[str, str]` | `{}` | Run name/ID → hex color |
553+
| `group_colors` | `dict[str, int]` | `{}` | Group key → color palette index |
554+
| `superimposed_cards` | `list[dict]` | `[]` | Multi-tag chart definitions |
555+
| `run_selection` | `list[dict]` | `[]` | Run visibility entries |
556+
| `metric_descriptions` | `dict[str, str]` | `{}` | Tag → description |
557+
| `tag_filter` | `str` | `""` | Tag filter regex |
558+
| `run_filter` | `str` | `""` | Run filter regex |
559+
| `smoothing` | `float` | `0.6` | Scalar smoothing (0.0–0.999) |
560+
| `symlog_linear_threshold` | `float \| None` | `None` | Linear threshold for symlog |
561+
| `group_by` | `dict \| None` | `None` | Run grouping config |
562+
| `y_axis_scale` | `str \| None` | `None` | `"linear"`, `"log10"`, or `"symlog10"` |
563+
| `x_axis_scale` | `str \| None` | `None` | Same (STEP/RELATIVE only) |
564+
| `tag_axis_scales` | `dict[str, dict]` | `{}` | Per-tag axis overrides |
565+
| `tag_symlog_linear_thresholds` | `dict[str, float]` | `{}` | Per-tag symlog thresholds |
566+
| `expanded_tag_groups` | `dict[str, bool]` | `{}` | Section expand/collapse state |
567+
568+
**Convenience methods:**
569+
570+
| Method | Description |
571+
|--------|-------------|
572+
| `pin_scalar(tag)` | Append a scalar pin |
573+
| `pin_histogram(tag, run_id)` | Append a histogram pin |
574+
| `pin_image(tag, run_id, sample=0)` | Append an image pin |
575+
| `add_superimposed_card(title, tags, run_id=None)` | Append a superimposed card |
576+
| `select_runs(run_names)` | Set visible runs by name |
577+
578+
**Serialization & I/O:**
579+
580+
| Method | Description |
581+
|--------|-------------|
582+
| `serialize() -> dict` | Return `SerializedProfile` dict |
583+
| `write(logdir) -> str` | Serialize + write to disk, returns path |
584+
| `Profile.load(logdir) -> Profile \| None` | Load from logdir |
585+
| `Profile.from_serialized(dict) -> Profile` | Construct from dict |
586+
587+
**Merging:**
588+
589+
| Operation | Description |
590+
|-----------|-------------|
591+
| `a.update(b)` | Merge `b` into `a` in place (dicts merged, lists extended, scalars replaced) |
592+
| `a \| b` | Return a new merged profile |
593+
| `a \|= b` | In-place merge (same as `update`) |
594+
595+
#### `create_profile(**kwargs) -> Profile`
596+
597+
Create a `Profile` object. Same parameters as `set_default_profile` minus `logdir`. This is the primary way to build profiles.
598+
530599
#### `set_default_profile(logdir, **kwargs) -> str`
531600

532-
Create and write a default profile in one call. Returns the path to the written profile file.
601+
Create and write a default profile in one call. Returns the path to the written file. Parameters:
533602

534603
| Parameter | Type | Default | Description |
535604
|-----------|------|---------|-------------|
@@ -548,13 +617,9 @@ Create and write a default profile in one call. Returns the path to the written
548617
| `group_by` | `dict` | `None` | Grouping config (`key`: `"RUN"`, `"EXPERIMENT"`, `"REGEX"`, or `"REGEX_BY_EXP"`; optional `regexString`) |
549618
| `expanded_tag_groups` | `dict[str, bool]` | `None` | Which tag group sections to expand/collapse (omit for default behavior) |
550619

551-
#### `create_profile(**kwargs) -> dict`
552-
553-
Create a profile dictionary without writing it. Same parameters as `set_default_profile` minus `logdir`.
554-
555620
#### `write_profile(logdir, profile) -> str`
556621

557-
Write a profile dict to `<logdir>/.tensorboard/default_profile.json`.
622+
Write a `Profile` or `SerializedProfile` dict to `<logdir>/.tensorboard/default_profile.json`.
558623

559624
#### `read_profile(logdir) -> dict | None`
560625

NEW.md

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ Want full control? Set them from your training script:
2929
```python
3030
from tensorbored.plugins.core import profile_writer
3131

32-
profile_writer.set_default_profile(
33-
logdir='./logs',
32+
p = profile_writer.create_profile(
3433
run_colors={
3534
'baseline': '#9E9E9E',
3635
'experiment_v1': '#2196F3',
3736
'experiment_v2': '#4CAF50',
3837
},
3938
)
39+
p.write('./logs')
4040
```
4141

4242
Or generate perceptually uniform palettes automatically:
@@ -56,15 +56,9 @@ TensorBoard gives you one chart per tag. Want to see `train/loss` and `eval/loss
5656
TensorBored lets you superimpose any scalar tags onto a single chart. From the UI: click the menu on any card, "Add to superimposed plot", done. Or pre-configure them:
5757

5858
```python
59-
profile_writer.set_default_profile(
60-
logdir='./logs',
61-
superimposed_cards=[
62-
profile_writer.create_superimposed_card(
63-
title='Train vs Eval Loss',
64-
tags=['loss/train', 'loss/eval'],
65-
),
66-
],
67-
)
59+
p = profile_writer.create_profile()
60+
p.add_superimposed_card('Train vs Eval Loss', ['loss/train', 'loss/eval'])
61+
p.write('./logs')
6862
```
6963

7064
---
@@ -83,16 +77,16 @@ Pin limit went from ~15 (URL length cap) to **1,000**.
8377
Set up the dashboard from Python before anyone even opens a browser:
8478

8579
```python
86-
profile_writer.set_default_profile(
87-
logdir='./logs',
88-
name='Training Dashboard',
80+
p = profile_writer.create_profile(
81+
'Training Dashboard',
8982
pinned_cards=[
9083
profile_writer.pin_scalar('loss/train'),
9184
profile_writer.pin_scalar('accuracy/eval'),
9285
],
9386
tag_filter='loss|accuracy',
9487
smoothing=0.8,
9588
)
89+
p.write('./logs')
9690
```
9791

9892
---
@@ -114,14 +108,14 @@ Drag-and-drop to reorder. Arrow buttons for precise positioning. Order persists
114108
## You have 50 metrics and can't remember what `aux_head_3/nll` means? Add descriptions.
115109

116110
```python
117-
profile_writer.set_default_profile(
118-
logdir='./logs',
111+
p = profile_writer.create_profile(
119112
metric_descriptions={
120113
'loss/train': 'Cross-entropy loss for backprop.',
121114
'aux_head_3/nll': 'NLL from the auxiliary prediction head on layer 3.',
122115
'gradients/global_norm': 'Global L2 norm of all gradients before clipping.',
123116
},
124117
)
118+
p.write('./logs')
125119
```
126120

127121
Hover over any metric card header and the description appears as a tooltip. Set it once in your training harness, every teammate sees it.
@@ -156,7 +150,7 @@ Type a filter, refresh — it's still there. Clear the filter, refresh — it st
156150
| All runs hidden | Blank dashboard | Auto-reset to visible |
157151
| Tag filter on refresh | Resets | Remembered |
158152
| Share config | Long URL | JSON export/import |
159-
| Configure from Python | No | `profile_writer` API |
153+
| Configure from Python | No | `Profile` object + `profile_writer` API |
160154
| Colour palette | Random | OKLCH perceptually uniform |
161155

162156
---

0 commit comments

Comments
 (0)