Added
-
Stable benchmark series identity:
plot_sweep(series_id=...)(plan 15, #1012).
Names the over_time trend a benchmark appends to, independently of what identifies its
configuration:tagpartitions storage,series_idnames the trend. Deliberately not
part ofhash_persistent, so declaring one re-keys nothing — it exists so a trend
survives a worker rename or a move between modules. -
Inspectable, pinnable benchmark identity:
bn.sweep_identity(),bn.identity_of(),
bn.diff_identities()(plan 16, #1010). Returns the exact cache/history keys a
declaration resolves to without running it, takingplot_sweep's declarative arguments
with the same spellings; diff two identities to see precisely which field moved a key.
Pairs withSweepSpec:bn.sweep_identity(**spec.bind(W), worker=W). -
bn.ComposableContainerRerun— compose rerun recordings (#1007). Combines complete
.rrdrecordings right/down/sequence/overlay into one recording plus a generated
Blueprint, from insidebenchmark(); the composition itself renders to a.rrd, so
nesting is plain recursion. -
rerun_summary/rerun_grid— merge a whole sweep's rerun recordings into one
viewer (#1017). Sweeping aResultRerunpreviously embedded one wasm web viewer per
sample (blank past ~16 due to browser WebGL context limits, nothing comparable across
iframes). These walk the result dataset and merge every sample's cached.rrdinto a
single recording —rerun_summaryon one shared timeline,rerun_gridlaid out in
space withcompose_method_list=control. Named-only (opt-in) likevideo_summary.
The dimension-ordering policy is extracted tocompose_method_list_for_dims()so the
video and rerun summary renderers share it. -
Single-point sweep ranges (plan 17, #1001).
with_boundsandbn.sweepaccept
low == high, yielding exactly one sample (previously: a raise, or N linspace copies /
an empty arange if the guard had simply been relaxed).samples > 1over a zero-width
range raises at declaration, andbn.sweepvalidates an inverted range at construction
instead of mid-run.bounds=(x, x)andvalues=[x]keep deliberately distinct
identities. -
Unnamed parameters are rejected at resolution time (plan 19, #1003). A variable
declared on a plain (non-Parameterized) mixin registers withparam.name=Noneand
used to fail far from the declaration (aKeyErrorin result storage, or a bare
ValueErrorfrom param, depending on param version). Resolution now raises a
TypeErrornaming the metaclass cause and the remedy; parameter objects built by
bn.box()/bn.sweep()are unaffected. -
bn.SweepSpec— a sweep declaration as a value (plan 18, phase 1). A frozen record
ofplot_sweep's seven declarative arguments (title, descriptions, input/result/const
vars, tag) that can be built once, composed (with_,plus_input_vars,
plus_result_vars,merge), compared (bn.diff_specs), pickled, and bound to any
worker:bench.plot_sweep(**spec.bind(Worker)).bind(worker)checks every by-name
variable up front and runs the duplicate-variable validation at bind time, where the
composition that caused a duplicate is in view. Run configuration (repeats,run_cfg,
plot_callbacks, …) is deliberately absent — a spec states what is measured, not how the
run is driven — and a callable in any field is rejected at construction. Purely additive:
every existingplot_sweepcall is unchanged. -
Per-sample fault tolerance on the sweep path:
catch=andfail_on_sample_error.
Bench.optimize(catch=...)has had this knob since #962, so the same benchmark was
fault-tolerant when driven by Optuna and all-or-nothing when swept.catchlives on
BenchRunCfg(sobn.run(catch=...)works, and it reachesplot_sweepviarun_cfg—
its one home); a bare exception type is accepted and wrapped. A caught sample leaves the missing-value sentinel
at its coordinate —setup_datasetalready fills every result variable, so the dataset
shape is unchanged and reductions skip it — is logged at WARNING with its inputs, and
writes nothing to the sample cache, so a transient flake cannot become a permanent
cached failure. Default()is fail-fast, exactly as before, and tolerating a failure
moves no cache key.fail_on_sample_error: bool | floatis the other half, and the two are a pair rather than
independent knobs:catchalone turns real breakage into a green run over an all-sentinel
dataset.Truefails the run if any sample was caught; a float in (0, 1] fails when the
failed fraction reaches it, measured over samples the run actually executed — a cache
hit never reached the worker, so counting it would make one threshold mean different things
on a cold and a warm cache. The raise happens after the dataset and report are assembled,
so the partial results survive it, and only for a run that actually sampled: on a
benchmark-result cache hit the loaded result carries a previous run's counts, which are
not this run's errors. Both knobs are validated before sampling starts, so a typo'd
threshold costs milliseconds rather than a whole sweep.Inspect failures via
BenchResult.failed_samples(aSampleFailureper caught sample:
job id, inputs, exception repr, traceback),n_failed, andfailed_fraction.
Removed
-
ResultVolumeis gone. It was declarable but not usable: exported from
bencher/__init__.pyand listed inALL_RESULT_TYPES/RESULT_KIND_ORDER, but absent
from every registry that decides how a sample is stored. Putting one inresult_vars
raisedKeyError: No variable named '<name>'inprecompute_result_arrays— the type
got no data variable, yet the collector indexed one anyway — and had it survived that,
the store loop'selseraisedTypeError: Unsupported result type. Since no working
code could have used it, removal is only nominally an API break. Note that
VolumeResult(bencher/results/volume_result.py), the 3-float-input volume plot, is
unrelated and unaffected — it rendersResultFloat.A new
TestEveryResultTypeIsStorablepins the invariant that made this a trap:
membership inALL_RESULT_TYPESnow implies the collector has a branch to store the
type, and thatresult_kindclassifies it as something other than"unknown".
ResultHmapis the one exemption, collected out-of-band viaresult_hmaps.
Changed
- A
ResultReferencecontainer is now called with the object alone. It was called as
container(obj, **kwargs), so the render kwargs every path adds (override,
agg_over_dims,pane_layout, and thewidth/heightthatset_plot_sizeinjects from
bench_cfg.plot_size) leaked into a callback that
only ever wanted the object, and a single-argument renderer raisedTypeError. It now
matches theResultDataSetcontract, so one renderer works for both. Breaking only for a
callback that relied on receiving those keywords; nothing in bencher did. The
container=a renderer passes toto_panesis a separate contract — a panel pane
constructor, still called withstyles=and the layout keywords — and is unchanged. - Intra-sample chart gallery examples now declare their container rather than appending
a report-level plot.res.to(XYCurveResult, ...)and friends add a plot below whatever
plot_sweepalready rendered, which for aResultDataSetwith no declared container is
the raw table — so the example showed the rows and the chart. Declaring
container=bn.xy_curve(...)puts the chart in the result's own position instead, which is
whatexample_plot_xy_scatteralready did. Affectsexample_plot_xy_curve,
example_plot_xy_histogramandexample_plot_xy_hexbin; the chart-type route is
unchanged and still covered by tests. DataSetResultnow rendersResultDataSetresults only. It previously claimed every
pane-type result, which madebench.add(bn.DataSetResult)/plot_list=["dataset"]a
second name for thepanesview on a sweep with no stored payload. Now that
ResultDataSetis a generic payload store, acontainer=written for a payload must not
be handed an unrelated result's value, so the view returnsNonefor such a sweep instead
of falling back. Use thepanesview (res.to_panes(), or the default report) for
image/video/string/reference results — it is unchanged, and both views share one render
path (BenchResultBase.map_sample_panes).- Logging now goes through per-module loggers. Every module logs via
logging.getLogger(__name__)instead of callinglogging.info()and friends on the
root logger, so bencher's output can be configured and filtered per module (e.g.
logging.getLogger("bencher.job").setLevel(logging.DEBUG)) without touching root.
Records now carry their originatingbencher.*logger name; anything that filtered
bencher output by root-handler side effects should key off the logger name instead. - Dev toolchain: ruff 0.16. Bumped the ruff pin to
<=0.16.0(and the ruff-format
prek hook tov0.16.0), which expands ruff's default rule set fromE4/E7/E9/Fto
roughly 413 rules. Lint debt surfaced by the wider defaults is fixed;DTZ001/DTZ005
(naive datetimes) andRUF023(unsorted__slots__) are ignored inruff.tomlwith
rationale — theover_timeaxis is a timezone-naivedatetime64coordinate, and
__slots__order feedshash_persistent(). The formatter is scoped away from markdown,
which 0.16 newly formats. Example generation now runsruff check --fix-onlyalongside
ruff format, so regenerated examples stay lint-clean. No runtime behavior change.
Fixed
-
A container declared on a
ResultRerunnow applies withover_timehistory too.
With history off, a rerun result renders throughds_to_container, where a declared
container wins over the type's built-in viewer. With history on and more than one time
point, rendering is routed to_pane_over_time_gridinstead — a separate path that
hardcodedrrd_file_to_pane, so the same benchmark honoured its declared renderer on
the first run and silently fell back to the rerun viewer on every run after. The grid
now resolves the declared container the same way, and only imports the rerun viewer
stack when it is the renderer actually being used.ResultVideo/ResultImagetake the
matching_pane_over_time_sliderpath but have no container slot to honour, so they are
unaffected. -
A variable declared twice in one sweep is now rejected or normalised, instead of quietly
moving the cache and history key.⚠️ This moves the key for affected benchmarks — see
below.plot_sweepconverted each list positionally with no uniqueness check, and the
three kinds of variable each misbehaved differently:result_vars=["y", "y"]was the damaging one. It produced a dataset byte-identical to
result_vars=["y"]under a different key:ResultCollector.setup_datasetbuilds
data_varsas a dict keyed by name andbencher.historykeys its per-column metadata the
same way, so the repeat collapsed in the data, whileBenchCfg.hash_persistentfolded the
per-variable digests as a sorted tuple and so hashed it twice. The benchmark ran,
reported correct numbers, and appended to a different trend line than the one it appeared
to belong to — with nothing in the run saying so. The duplicate is now dropped (first
occurrence kept) with aUserWarningnaming the variable and both positions.input_vars=["x", "x"]was accepted and then either collapsed to a single dimension or
died inside xarray withbroadcasting cannot handle duplicate dimensions, depending on
the sweep's shape — after the whole sweep had run. It now raises aValueErrorat
declaration time, naming the variable and every position.const_varsbehaved differently depending on spelling: the dict form collapsed duplicates
before bencher saw them, while the list-of-pairs form accepted a repeat with two
different values and let iteration order pick the winner. Repeats with equal values are
now deduped silently; conflicting values raise, naming both.Validation happens once, in
validate_declared_vars, called fromplot_sweepafter
conversion so comparison is on resolved names and every declaration form — string, spec
dict, param object, and mixtures — is covered by the same code.Separately,
hash_persistentnow folds result and const digests as sets rather than
sorted tuples. Its docstring already promised they contribute as an "unordered set", but a
sorted tuple delivered only the ordering half of that; uniqueness was missing, which is
the mechanism behind the bug above. This keeps identity correct on paths that never reach
the validator, such as aBenchCfgbuilt or deserialized directly.Migration: only benchmarks that currently declare an overlapping result variable are
affected, and they are exactly the ones now emitting the new warning. Their key moves onto
the key it would have had if declared correctly, so in the common case the benchmark
rejoins the trend it should have been on all along; in the worst case it starts a fresh
series, andon_history_resetcontrols how loudly that surfaces. Configurations without a
duplicate are bit-identical: every golden hash intest/test_hash_persistent.pyis
unchanged, sincesorted(set(xs)) == sorted(xs)whenxsis already unique. -
hover=Falseon an intra-sample chart now actually disables hover.set_default_opts
registerstools=["hover"]as a global holoviews default for most element types, so a
spec that merely omitted the key got hover back and the option was a silent no-op.
toolsis now set in both directions ([]when off). Passingtools=through**opts
still wins, as it is applied last. -
A
ResultDataSetrenders on every run, not only the first. Withover_timeand
more than one event in the history, a stored payload reachedds_to_containerwith
over_timestill a live dimension:_to_panes_dadropsover_timefrom the pane
recursion so hvplot can use groupby, and the branch that rebuilds it for pane-type
results only coveredResultVideo/ResultImage/ResultRerun. The render then died in
zero_dim_da_to_valwithValueError: Dimension over_time already exists(no input
vars) or, once another dimension was consumed first, in thedataset_listlookup with
TypeError: only integer scalar arrays can be converted to a scalar index— one cause,
two signatures, and viato_autothe traceback was swallowed and the plot silently
vanished from the report. AResultDataSetnow renders the current event: its cells
hold indices intodataset_list, which is rebuilt from the samples of the run doing the
rendering, so the indices merged in from history address this run's list and a slider
across the events would show the current payload under every past run's label. Scalar
results keep their fullover_timeseries. Two supporting hardenings: a length-1
dimension on a point now collapses to a value instead of a one-element array, and
ds_to_containernames the result variable and the dimension that was not reduced
rather than indexing a list with an array several frames later.
Added
-
container=extended to the remaining renderable result types.ResultString,
ResultPathandResultContainer(and soResultRerun, which subclasses it) now take
the same declared-renderer slotResultDataSetandResultReferencehave, and a
declared container beats the type's built-into_container(). AResultPathcan
therefore render a file's contents — a CSV as a chart, a JSON as a tree — where before
it was always a download widget, and aResultStringcan render as Markdown or
highlighted code instead of plain text.ResultReferencealso honours a container
declared on the class, not just one attached to a sample; previously a class-level one
was silently ignored because only the stored sample was consulted. Precedence is uniform:
renderer-supplied, then the sample's, then the class's, then the type's default. Every new
slot is in_hash_exclude, so declaring a renderer leaves cache keys andover_time
history series byte-identical. The resolution itself is now one helper,
BenchResultBase.declared_container, rather than being open-coded per type.Not extended to
ResultVolume: it has no render path at all (nothing dispatches on it —
VolumeResultplotsResultFloatover three float inputs, andResultVolumeis absent
fromPANEL_TYPES), so a container slot there would be an option that never fires. -
xy_histogramchart type — bins one or more measured columns of aResultDataSet,
showing the distribution a single sample measured. Distinct from the existing
histogram, which bins aResultFloatacross the sweep and so shows the spread of the
repeats.column=takes a list to overlay several distributions, binned over a shared
range so they are actually comparable, and drawn translucent so the one underneath stays
readable; it defaults to every numeric column.density=Truenormalises instead of
counting, and the y axis is labelled accordingly. An empty or all-NaN column produces
empty bins rather than raising — numpy cannot pick a range for an empty array, and NaN is
how bencher marks a sample missing, so NaN rows are dropped rather than poisoning the
range. Gallery example:example_plot_xy_histogram. -
xy_hexbinchart type — the density counterpart toxy_scatter, taking the same
axes and binning them into hexagonal tiles. For a cloud of tens of thousands of points
the markers saturate and where the mass actually is becomes the thing a scatter cannot
show.gridsize=sets the resolution,min_count=1drops empty tiles, and the colourbar
is on by default since a density plot without one shows shape but no magnitude. Gallery
example:example_plot_xy_hexbin. -
hv.HexTilesnow carries the shared default figure size. It was absent from
HoloviewResult.DEFAULT_SIZED_ELEMENTS, so a hexbin would have fallen back to the
holoviews default rather than bencher's 600x600 — the same gap Histogram/Area/ErrorBars
were fixed for previously. -
xy_curvechart type — draws one or more measured columns of aResultDataSet
against an x column, for a benchmark that collects a whole series as one sample. The
gap it fills:curveandlineplot across the sweep, with one value per sample, so
they cannot show a series that lives inside one. Available the same two ways as
xy_scatter:bn.xy_curve(x="time", y="signal")returns a picklable spec usable as a
ResultDataSet(container=...), andXYCurveResultis registered as a named-only
chart type.y=takes a list to overlay several series with a legend,markers=True
adds a marker per row for a sparse series, andsort=Falsekeeps the frame's row order
so a trajectory that doubles back in x is drawn as travelled rather than sorted into a
function of x. Gallery example:example_plot_xy_curve. -
A named DataFrame index is now plottable as a column.
Dataset.to_pandas()— the
idiomatic way to build aResultDataSetfrom xarray — leaves the dimension coordinate
in the index and only the data variables in the columns, so the x axis was unreachable
by any chart and inference saw a single-column frame.to_dataframenow promotes named
index levels to columns, at the front so inference finds the x axis first. An unnamed
RangeIndexis row position rather than data and is left alone; a level whose name is
already a column keeps its values and loses its name, since pandas rejects a label that
is both an index level and a column as ambiguous. TheResultDataSetgallery examples
(example_result_dataset_1d/_2d) andBenchableDataSetResultnow declare an
xy_curvecontainer, so they show the collected series as a curve rather than as a
table of raw rows. -
Generic per-sample data rendering, with tabular handling kept at the edge.
BenchResultBase.map_sample_panesis the single operation that retrieves each stored
sample and optionally maps a renderer over it; it neither checks nor converts the payload
type, and takes the result types it claims as a parameter. Both per-sample views now go
through it —PaneResult.to_panes(every pane type, and the path the default report
takes) andrender_data_samples(ResultDataSetonly) — so a fix to the render path
reaches the report and the chart types alike.XYScatterResultcomposes
render_data_samplesinstead of introducing a parallel result hierarchy. The opt-in
holoview_results/tabular_spec.pymodule only contains renderer-side concerns:
TabularSpec, a frozen (therefore picklable) dataclass whose__call__coerces
supported table-like data to a DataFrame, shared HoloViews options, and column helpers
(to_dataframe,check_column,resolve_axes,plot_frame,value_columns).
TabularSpecis exported asbn.TabularSpec, since writing a new chart type is what it
is for. Column-validation errors name the chart that rejected the column, for every
column a chart plots — including the ones a chart option implies (color=), which
value_columnsvalidates rather than letting them reach pandas as a bareKeyError.
Chart types keep
naming their options explicitly rather than accepting**kwargs:**kwargsbelongs
tomap_plot_panes, and a signature-based split could not tell a pane-sizingwidth
from a HoloViews stylewidth. -
xy_scatterchart type — scatters two measured columns of aResultDataSet
against each other, for results whose rows are the measurement (landing points, hit
locations, a phase-space cloud). Distinct from the existingscatter, which puts an
input variable on x and a result variable on y; here both axes come from inside one
sample and the sweep dimensions separate one plot from the next. Available two ways
from one implementation:bn.xy_scatter(x=..., y=..., color=..., data_aspect=1)
returns a picklable spec usable as aResultDataSet(container=...), so the cloud
renders inresult_varsorder with the other results; andXYScatterResultis
registered as a named-only chart type (bench.add(bn.XYScatterResult, x=..., y=...)
orto_auto(plot_list=["xy_scatter"], ...)), so no existing report gains a plot it
did not ask for. Columns are validated against the frame — a typo names the available
columns instead of rendering nothing — x/y are inferred from the numeric columns when
omitted,data_aspect=1gives the equal-aspect scaling a position cloud needs, and
DataFrame / xarray /hv.Datasetobjects are all accepted. The
example_plot_xy_scattergallery declares this renderer on its result, so the default
report contains the scatter in place of the raw table rather than appending both. -
ResultDataSet(container=...)—ResultDataSetis a generic per-sample store for
any picklable Python payload, with no DataFrame/xarray requirement. It can declare how
the payload renders, the wayResultReferencealready could. The callback takes the
stored object and returns anything Panel can display, so domain data shows up as the
view it means, inresult_varsorder with the rest of the results.
Precedence: a container passed to a renderer wins, then one attached to the stored
sample (ResultDataSet(data, container=...)insidebenchmark()), then the one declared
on the class. The callback is invoked with the object alone (no plot kwargs), so
single-argument callables are safe. An explicit
bench.add(bn.DataSetResult, container=...)view still appends to the end of the report
and cannot sit among the other result variables.containeris in
_hash_exclude, so declaring one does not change any cache key. A declared container
rides inBenchCfginto the result cache and the collect/render split, so it must be
picklable — a module-level function or a callable object, not a lambda. -
Plot-selection signature enrichment (A2 Phase S1):
PltCntCfggains additive, cheaply-computed facts alongside the existing counts —has_time/time_steps(temporal axis presence and length),result_kinds(result-variable name → coarse serializable kind, via the newresult_kind/RESULT_KIND_ORDERinbencher.variables.results),cat_levels(levels per categorical input), andsamples_per_point(min repeat count actually present at the latest time step, missing-sentinel-aware per result type, vs the configuredrepeats).generate_plt_cnt_cfgtakes an optional dataset for the data-derived facts,PltCntCfg.__str__includes the new fields, and the aggregation plotting path carries them through. No selection behavior changes.