Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
164f3fe
experiment client API + /experiments/log endpoint
Manik-Khajuria-5 Jul 13, 2026
ee4f18b
Merge branch 'dev' into Experiments-api
Manik-Khajuria-5 Jul 14, 2026
fbd8c45
experiment query parser + heavy tests
Manik-Khajuria-5 Jul 14, 2026
dd23e31
type build_record with a Protocol, single-pass metrics
Manik-Khajuria-5 Jul 14, 2026
90bfcb5
Merge branch 'dev' into query-parser
Manik-Khajuria-5 Jul 15, 2026
31a6838
experiment search endpoint + vis.search_experiments
Manik-Khajuria-5 Jul 15, 2026
eb998dc
drop explanatory comments from search layer
Manik-Khajuria-5 Jul 15, 2026
4b2cc8a
experiment compare endpoint + vis.compare_experiments
Manik-Khajuria-5 Jul 15, 2026
5599953
drop the inline annotations from the compare docstring
Manik-Khajuria-5 Jul 15, 2026
6f28f6e
type the search and compare client APIs in __init__.pyi
Manik-Khajuria-5 Jul 15, 2026
eb933d3
drop query selection from compare, leave it to search
Manik-Khajuria-5 Jul 15, 2026
29baa49
group the runs that agree, per field
Manik-Khajuria-5 Jul 15, 2026
7a6e022
experiment suggest endpoint stub + vis.suggest_experiment
Manik-Khajuria-5 Jul 16, 2026
7c9b1f1
document the experiments API in the README
Manik-Khajuria-5 Jul 16, 2026
5b5c8fb
docs: document the data_model storage abstraction layer
Manik-Khajuria-5 Jul 16, 2026
f9495ea
Merge branch 'dev' into PR-6-searchLayer
Manik-Khajuria-5 Jul 16, 2026
75f3601
vis.hparams() opens a hyper-parameter pane
Manik-Khajuria-5 Jul 17, 2026
a0e8219
hparams mode select, tags in flatten, test cleanup
Manik-Khajuria-5 Jul 17, 2026
05aefbb
fetch hparams runs by id when no query is used
Manik-Khajuria-5 Jul 17, 2026
0e995d3
strict per-mode selection for hparams
Manik-Khajuria-5 Jul 17, 2026
a2f3358
Add /experiments/hparams endpoint that builds the pane; hparams calls it
Manik-Khajuria-5 Jul 17, 2026
0d9eb16
Merge branch 'dev' into Hyper-Param-PR-1
Manik-Khajuria-5 Jul 18, 2026
c770fc3
feat(hparams): render the hyper-parameter pane on the frontend
Manik-Khajuria-5 Jul 18, 2026
7bde223
feat(hparams): add sortable, filterable run table with color spine
Manik-Khajuria-5 Jul 19, 2026
0c6e425
feat(hparams): centered title, group dividers, and layout fixes
Manik-Khajuria-5 Jul 19, 2026
f3a7312
feat(hparams): add scatter matrix (SPLOM) view with dimension picker …
Manik-Khajuria-5 Jul 20, 2026
66958dd
Merge branch 'dev' into Layer3-PR5
Manik-Khajuria-5 Jul 21, 2026
7134e02
feat(hparams): add parallel coordinates view
Manik-Khajuria-5 Jul 21, 2026
eb3f6a1
refactor(hparams): share column, tree, and plot helpers across the th…
Manik-Khajuria-5 Jul 22, 2026
696c4a2
fix(hparams): stop clipping parallel-coordinates axis labels
Manik-Khajuria-5 Jul 22, 2026
51e0f8a
fix(hparams): persist each view's selection across view switches
Manik-Khajuria-5 Jul 22, 2026
ea1ec85
fix(hparams): give parallel-coordinates more bottom margin
Manik-Khajuria-5 Jul 22, 2026
9a14a07
fix(hparams): span parallel-coordinates axes to the exact data range
Manik-Khajuria-5 Jul 22, 2026
aee589a
fix(hparams): drop null-holed runs from parallel coordinates
Manik-Khajuria-5 Jul 22, 2026
65ae35d
fix(hparams): explain empty parallel coordinates instead of blanking
Manik-Khajuria-5 Jul 22, 2026
b6d19cf
feat(hparams): add a run identity axis to parallel coordinates
Manik-Khajuria-5 Jul 22, 2026
28ed13c
feat(hparams): filter runs across every view from a shared sidebar
Manik-Khajuria-5 Jul 23, 2026
30406e0
fix(hparams): show a single run search shared by every view
Manik-Khajuria-5 Jul 23, 2026
02da57a
fix(hparams): show the include-missing toggle on every filter
Manik-Khajuria-5 Jul 23, 2026
ca76ae5
feat(hparams): open the plots on one param against one metric
Manik-Khajuria-5 Jul 23, 2026
2fa9ac8
fix(hparams): leave the include-missing toggle usable on every column
Manik-Khajuria-5 Jul 23, 2026
3f12ec9
refactor(hparams): share the axis logic the two plot views had each c…
Manik-Khajuria-5 Jul 23, 2026
d46a6d1
feat(hparams): compare view, linked selection, and per-cell scatter h…
Manik-Khajuria-5 Jul 24, 2026
dd930d2
refactor(hparams): share the data-cell class builder across table and…
Manik-Khajuria-5 Jul 24, 2026
b7e1ad7
perf(hparams): stop the pane and its views re-rendering on unrelated …
Manik-Khajuria-5 Jul 24, 2026
640620d
feat(hparams): plot the metric history of the selected runs
Manik-Khajuria-5 Jul 25, 2026
dda79ef
feat(hparams): export the runs on screen and finish the keyboard pass
Manik-Khajuria-5 Jul 25, 2026
7e79f83
refactor(hparams): share the pieces the five views had each copied
Manik-Khajuria-5 Jul 25, 2026
3487c62
Merge branch 'dev' into Layer3-PR9
Manik-Khajuria-5 Jul 26, 2026
a41d1e5
fix(hparams): stop the title rule from swallowing the bar's buttons
Manik-Khajuria-5 Jul 26, 2026
c73ba26
fix(experiments): write env files from live state, not a stale snapshot
Manik-Khajuria-5 Jul 28, 2026
d56bd21
feat(hparams): dedicated update endpoint that syncs state and disk
Manik-Khajuria-5 Jul 28, 2026
ecf1b33
feat(client): vis.update_hparams to change or refresh a pane
Manik-Khajuria-5 Jul 28, 2026
b47455a
docs(hparams): cover the hparams endpoints in openapi, README and stubs
Manik-Khajuria-5 Jul 28, 2026
a044633
Merge branch 'dev' into Layer3-PR10
Manik-Khajuria-5 Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/skills/env-persistence/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Use this skill for persistence format changes and env reload/view layout behavio
## Documentation

- [Skill reference](references/REFERENCE.md)
- `py/visdom/data_model/README.md` — the storage abstraction layer (`DataStore`/`JSONStore`); persistence now funnels through it
- `py/visdom/data_model/base.py`, `py/visdom/data_model/json_store.py`
- `py/visdom/utils/server_utils.py`
- `py/visdom/server/handlers/web_handlers.py`
- `py/visdom/server/app.py`
Expand Down
116 changes: 116 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,17 @@ vis._send({'data': [trace], 'layout': layout, 'win': 'mywin'})
- [`vis.check_connection`](#vischeck_connection): check if the server is connected
- [`vis.replay_log`](#visreplay_log): replay the actions from the provided log file

### Experiments
Track experiment metadata (hyper-parameters, metrics, tags) alongside your plots, then search and compare runs across your server:
- [`vis.experiment`](#visexperiment) : create or update experiment metadata for an env
- [`vis.log_metrics`](#vislog_metrics) : append metric observations to an env's experiment
- [`vis.finish_experiment`](#visfinish_experiment) : mark an experiment terminal (finished/failed)
- [`vis.search_experiments`](#vissearch_experiments) : search experiments across envs with a query
- [`vis.compare_experiments`](#viscompare_experiments) : diff experiments field by field
- [`vis.suggest_experiment`](#vissuggest_experiment) : suggest parameters for the next run (reserved)
- [`vis.hparams`](#vishparams) : open a hyper-parameter pane over selected runs
- [`vis.update_hparams`](#visupdate_hparams) : change or refresh an existing hyper-parameter pane


## Loggers

Expand Down Expand Up @@ -1106,6 +1117,111 @@ This function takes the contents of a visdom log and replays them to the current
Arguments:
- `log_filename`: log file to replay the contents of.

### Experiments

Attach experiment metadata — hyper-parameters, metric observations and tags — to an environment, then search and compare runs across your server. Metadata is stored under the environment's `experiment` key and persisted through the server's data store, so a server started without a persistence path (`env_path=None`) has nothing to store, search or compare. Each function returns the server's reply decoded as a dict.

#### vis.experiment

This function creates or updates the experiment metadata for an environment. Calling it again for the same env merges in new params/tags and overwrites the name/description, so it is safe to call at the start of and again during a run.

Arguments:
- `name`: Display name for the experiment. Defaults to the env id.
- `params`: Hyper-parameters as a dict of `{name: value}`.
- `tags`: Free-form tags as a dict of `{name: value}`.
- `description`: Free-form description.
- `env`: Environment to attach the experiment to. Defaults to the client's env.

#### vis.log_metrics

This function appends one or more metric observations to an env's experiment, creating the experiment automatically if it does not exist yet.

Arguments:
- `metrics`: A non-empty dict of `{name: value}` observations.
- `step`: Optional training step the observations were recorded at.
- `env`: Environment whose experiment to log to. Defaults to the client's env.

> **Note**: once an experiment is finished (see `finish_experiment`), further `experiment`/`log_metrics` writes are rejected so a completed run's recorded data cannot change after the fact.

#### vis.finish_experiment

This function marks an env's experiment terminal so its recorded data is frozen.

Arguments:
- `status`: Terminal status, either `finished` (default) or `failed`.
- `env`: Environment whose experiment to finish. Defaults to the client's env.

#### vis.search_experiments

This function searches the experiments logged on the server, across all environments. The `query` uses a small readable syntax — comparisons (`<`, `<=`, `>`, `>=`, `=`, `!=`, `contains`) over param, metric and tag names, combined with `AND`/`OR` and parentheses. A name is matched bare (`acc`) or namespaced (`metric.acc`, `param.lr`, `tag.owner`) when ambiguous; metrics compare on their latest value. Queries are evaluated in Python, never `eval`'d, so a hostile query is a parse error rather than code execution.

```python
vis.search_experiments("lr < 0.01 AND acc > 0.9")
vis.search_experiments("status = finished AND (dataset contains mnist)")
```

Arguments:
- `query`: The filter string. `None` (default) returns everything.
- `limit`: Maximum results to return (default `100`). `None` returns all matches.
- `offset`: Number of results to skip, for paging (default `0`).
- `sort_by`: Field to sort by (any of the same names). Defaults to newest-created first.
- `descending`: Sort direction (default `True`).

Returns a dict of `experiments` (one page's worth), the unpaged `total` matching the query, and the `limit`/`offset`/`query` used.

#### vis.compare_experiments

This function compares the named experiments field by field to see what differs. Finding the runs is `search_experiments`' job — it answers "which runs match?"; this answers "how do these runs differ?". To compare a query's matches, search first and pass the resulting ids on.

Arguments:
- `env_ids`: A list (or tuple) of environment ids to compare, in the order given. Each must have an experiment.

Returns a dict with the compared runs (`env_ids`, full `experiments`) and a `params`, `metrics` and `tags` section. Each section lists the union of `fields`, the `shared` ones every run agrees on, the `differing` rest, the per-run `values`, and `groups` clustering the runs that agree per field.

#### vis.suggest_experiment

> **Reserved**: this endpoint is a stub. Choosing the next set of hyper-parameters to try (an Optuna-backed search strategy) is planned for a later layer, so the server currently replies `501 Not Implemented` and this function returns a stub dict `{"status": "not_implemented", "suggestion": None, ...}` rather than a real suggestion. The method, its arguments and the endpoint are in place so callers and docs are ready for when the strategy is wired in.

Arguments:
- `params`: The search space to suggest over, as a dict of `{name: spec}`. Currently ignored by the stub.
- `env`: Environment (study) to suggest against. Defaults to the client's env.

#### vis.hparams

This function opens a hyper-parameter pane over the experiments logged on the server: the selected runs are flattened into a table of hyper-parameters against their latest metric values (and tags) and registered as a dedicated `hparams` window. The environment is saved as soon as the pane exists, so it survives a server restart without an explicit save.

```python
vis.hparams("lr < 0.01 AND acc > 0.9") # query
vis.hparams(env_ids=["run-a", "run-b"]) # env_ids
vis.hparams("acc > 0.9", ["run-a", "run-b"]) # both
```

Arguments:
- `query`: Selection query, in the syntax of `search_experiments`.
- `env_ids`: Explicit list of environment ids, kept in the order given.
- `mode`: `"query"`, `"env_ids"` or `"both"`; inferred from which of `query`/`env_ids` were given when omitted. There is no "show everything" call — with neither argument the server rejects the request.
- `win` / `env` / `opts`: As for the other plotting methods.

Returns the created window id. The resolved selection is stored on the window, so `update_hparams` can re-run it later.

#### vis.update_hparams

This function changes or refreshes an existing hyper-parameter pane — the dedicated write path for `hparams` windows, since the generic update route only understands plot windows. Called with a `query`/`env_ids`/`mode` selection the pane's selection is replaced, under the rules of `hparams`; called with only `win` the selection stored on the window is re-run, a manual refresh that picks up runs logged (or newly matching) since the pane was built. The pane keeps its id and position, is rebuilt in place, and the environment is saved so the update reaches disk immediately.

```python
win = vis.hparams("lr < 0.01")
vis.update_hparams(win, "lr < 0.1 AND acc > 0.9") # replace the selection
vis.update_hparams(win) # refresh as-is
```

Arguments:
- `win`: Id of the pane to update (required). Unknown windows are a 404; non-hparams windows a 400.
- `query` / `env_ids` / `mode`: The replacement selection, validated exactly as on `hparams`.
- `env`: Environment holding the window. Defaults to the client's env.
- `opts`: Overrides the pane's title/size; when omitted the current ones are kept.

Returns the window id.

## Customizing Visdom
The user config directory for visdom is
- `~/.config/visdom` for Linux
Expand Down
16 changes: 2 additions & 14 deletions js/api/ApiProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { showToast } from '../toasts/toastEvents';
import ApiContext from './ApiContext';
import Poller from './Legacy';
import serverPath from './serverPath';

const ApiProvider = ({ children }) => {
const [connected, setConnected] = useState(false);
Expand All @@ -15,20 +16,7 @@ const ApiProvider = ({ children }) => {
// helper functions //
// ---------------- //

// Normalize window.location by removing specific path segments
// and ensuring the pathname ends with a '/'
const correctPathname = () => {
var pathname = window.location.pathname;
if (pathname.indexOf('/env/') > -1) {
pathname = pathname.split('/env/')[0];
} else if (pathname.indexOf('/compare/') > -1) {
pathname = pathname.split('/compare/')[0];
}
if (pathname.slice(-1) != '/') {
pathname = pathname + '/';
}
return pathname;
};
const correctPathname = serverPath;

// ------------------- //
// basic communication //
Expand Down
29 changes: 29 additions & 0 deletions js/api/experimentsApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import serverPath from './serverPath';

export function fetchExperimentComparison(envIds, signal) {
const ids = (envIds || []).filter((id) => typeof id === 'string');
if (ids.length === 0) {
return Promise.reject(new Error('No runs to load.'));
}
return window
.fetch(serverPath() + 'experiments/compare', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ env_ids: ids }),
signal,
})
.catch((err) => {
if (err && err.name === 'AbortError') throw err;
throw new Error('Could not reach the server.');
})
.then((res) => {
if (!res.ok) {
const reason = res.statusText || 'request failed';
throw new Error('Could not load metric history (' + reason + ').');
}
return res.json();
});
}

export default fetchExperimentComparison;
12 changes: 12 additions & 0 deletions js/api/serverPath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function serverPath() {
var pathname = window.location.pathname;
if (pathname.indexOf('/env/') > -1) {
pathname = pathname.split('/env/')[0];
} else if (pathname.indexOf('/compare/') > -1) {
pathname = pathname.split('/compare/')[0];
}
if (pathname.slice(-1) != '/') {
pathname = pathname + '/';
}
return pathname;
}
1 change: 1 addition & 0 deletions js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
'use strict';

import 'fetch';
import 'rc-slider/assets/index.css';
import 'rc-tree-select/assets/index.less';

import React, { useContext, useEffect, useRef, useState } from 'react';
Expand Down
Loading
Loading