Skip to content

ToggleSpecialistMethod: blended reference and block legs, symmetric selection, caller-stated toggle cycle - #145

Merged
aclerc merged 18 commits into
v1from
v1-pairing-filter
Oct 9, 2026
Merged

aclerc merged 18 commits into
v1from
v1-pairing-filter

Conversation

@aclerc

@aclerc aclerc commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

ToggleSpecialistMethod becomes one method with two blended legs and a symmetric row selection,
and the caller now states the toggle cycle explicitly: its length (toggle_period) and its grid
(toggle_datum). A selection on a signal the treatment affects (for example, keeping only rows
where a control curve was in effect) must not leave the two toggle states with different wind
mixes. Pairing, cycle-level exclusion and per-stage accounting address that. The per-bin and
power_band options read a power the treatment cannot move.

The estimate

  • Two legs, blended. A reference leg (sum) compares the test turbine with the summed power
    of a reference subset. A block leg (block_mean) compares each row with its own toggle cycle's
    mean test power, and needs no reference at all, so a one-turbine campaign still gets an estimate.
    Headline and per bin are blended by minimum-variance weights that account for the legs'
    correlation, so the blend is never worse than its better leg.
  • Reference subset. The reference leg runs on every non-empty subset of the available references
    and keeps the one with the smallest sigma (ties to the larger subset). More than 8 references
    raises: pass the nearest or best-correlated.
  • Decisions on permuted labels. The subset choice and the blend weights are judged on a
    bootstrap with the on/off labels shuffled within each toggle cycle, so neither can favour the
    alternative whose realised uplift came out low. The reported uplift and sigma are the actual ones.
  • One normal-operation filter is applied to every turbine the estimate uses.

Row selection

  • pairing_max_gap (optional): a used row is kept only if the other segment has a used row
    within the gap, inclusive. It must be a positive whole multiple of the timebase. This replaces
    v0's pairing_filter_timedelta_seconds, whose floor-to-timebase semantics silently dropped every
    row below 600 s on 10-min data.
  • columns.exclude_block (new column role): a flagged row removes its whole toggle cycle from
    both legs, so both states lose the same conditions. It is the symmetric alternative to
    exclude_row.
  • exclude_row now reads the references' flags as well as the test turbine's. A timestamp
    flagged on any turbine a leg uses is dropped from that leg, just as if that reference were down.
    ColumnSchema.exclude_row no longer promises "test turbine only"; it leaves which turbines'
    flags count to each method.
  • power_band (optional, (lo, hi] kW, finite with lo < hi): each leg keeps only rows whose
    bin power lies in the band.
  • MethodOutput.selection_accounting: rows kept per segment after each stage (segment,
    filters, exclude_row, pairing, block, and power_band when set), also written per leg to
    <run>_selection_<ts>.csv.

The toggle cycle, stated by the caller

  • toggle_period (required): one full on + off cycle. It sets the block leg's cycles, the label
    permutation and exclude_block.
  • toggle_datum (required): any boundary of the cycle grid, never taken from the data, so a
    missing first record cannot shift every cycle. A ToggleSchedule input is checked against it.
  • Studies: a toggle StudyConfig needs a toggle_datum, and treatment starts are drawn only on
    cycle boundaries, as a clock-aligned controller toggles. This changes which starts a toggle
    study draws.

Power bins

  • bin_power: the per-row power the bins and power_band read.
    • "baseline" (default): no upgraded row enters it. That is rho_base * ref_total on the
      reference leg, and the mean of the cycle's baseline rows on the block leg.
    • "both_states": the two states' mean, for a campaign in which neither state is a true
      baseline. Renaming which state is "on" then moves no row between bins.

Outputs

  • The data-stats CSV has one set of all/baseline/upgraded rows per leg. The results CSV
    reports each leg's ratios and uplift and the blend weight (weight_sum). Each leg's uplift and
    the headline are re-derivable from the CSVs.
  • The per-bin frame carries each leg's energy sums (sum_actual_<leg>,
    sum_counterfactual_<leg>) and the bin's weight_sum, so each bin's blend is re-derivable.
  • The harness uncertainty_diagnostics frame has one row per (condition, condition_bin), each
    leg's columns suffixed _<leg>.
  • A pairing-gap plot (plots/2_filter/<test>_pairing_gap.png, under save_plots): each used row's
    gap to the nearest used row of the other segment, before and after pairing.

Breaking changes

  • toggle_period and toggle_datum are required.
  • Per-bin sum_actual / sum_counterfactual and the results CSV's ratio_baseline /
    ratio_upgraded are replaced by per-leg columns.
  • Default bins read baseline power: on the reference leg the bin scale moves by about
    1 / (1 + uplift/2) against v1's two-state mean (bin_power="both_states" keeps v1's).
  • exclude_row now honours reference flags.
  • A toggle StudyConfig needs toggle_datum, and its draws change.

Also

  • Fixes to the toggle placebo end-to-end tests (10-min fixture, availability, a zero-spread
    resample in _paired_correlation).
  • Pairing gaps are read at nanosecond resolution whatever the index unit.
  • Windows path-separator fix in test_uplift_plots.py.

Test plan

  • pair_within boundaries; parity with v0's pairing filter at 10/30/60 min
  • Each leg recovers a known uplift; the block leg with no references; a reference outage does not cost the block leg rows
  • exclude_block removes the whole cycle in both legs; no flags change nothing
  • The subset choice reads the permuted bootstrap; the blend is never worse than its better leg
  • toggle_datum: required; the cycle grid survives a missing first record; an off-grid schedule and a tz mismatch are refused
  • bin_power: baseline bins do not move with the upgraded rows, both_states bins do; with both_states, swapping the state labels moves no row between bins
  • power_band: every used row lies in the band; a constant uplift reads true inside it; bad bands refused
  • Headline and every bin re-derivable from the per-leg sums and weights
  • Study draws land on cycle boundaries
  • tests/benchmarking (not slow): 1363 passed; ruff and mypy clean

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

Only a non-blocking documentation nit remains; no approval-blocking issues were identified.

Review effort: Lite
Findings: None

What changed in this PR

Adds pairing-aware selection, per-stage accounting, imbalance warnings, and pairing-gap diagnostics to ToggleSpecialistMethod.

Changes:

  • Adds validated pairing_max_gap filtering.
  • Exports segment-level selection accounting.
  • Adds pairing-gap plots and Windows-safe path handling.
  • Review note: update the module docstring to document the fourth diagnostic plot.
File Summary
tests/​benchmarking/​campaigns/​test_uplift_plots.py Makes plot path assertions platform-independent.
tests/​benchmarking/​baselines/​test_toggle_specialist.py Tests pairing, accounting, warnings, and plots.
benchmarking/​harness/​method.py Adds additive selection_accounting output support.
benchmarking/​baselines/​toggle_specialist.py Implements pairing, accounting, diagnostics, and output changes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread benchmarking/baselines/toggle_specialist.py Outdated
aclerc and others added 2 commits September 25, 2026 12:43
- pairing_max_gap: keep a used row only if the other segment has a used row
  within the gap, inclusive. Must be a positive whole multiple of the
  timebase, so a sub-timebase value can no longer silently drop every row
  (v0's floored-seconds semantics). Applied after exclude_row, so the uplift
  and the bootstrap see the same selection. Parity-tested against v0's
  any_within_timedelta.
- MethodOutput.selection_accounting: rows kept per segment after each stage
  (segment, filters, exclude_row, pairing), also written to a selection CSV;
  a warning when a stage keeps the two segments' rows unevenly
  (segment_imbalance_warning).
- Pairing-gap diagnostic plot (2_filter): each used row's gap to the nearest
  used row of the other segment, before and after pairing, with hours kept.
  Drawn without a pairing filter too, to judge whether one is needed.
- Path-separator-agnostic assertion in test_uplift_plots (Windows fix, as on
  northing-improvement).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
index.asi8 is in the index's own unit; real SCADA often arrives as
datetime64[us], so a 10-minute gap read as 0.6 s and the pairing filter
kept every row. Convert to ns first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aclerc and others added 12 commits October 5, 2026 12:02
…listMethod

reference_mode selects what the test power is compared against: "sum" (the
summed references, as before), "block_mean" (the mean test power of the row's
own fixed wall-clock block, so no references are needed at all) or
"block_scaled" (the summed references rescaled per block so each block's
reference total equals its test total). Every used row of a block gets the
same reference factor, so the on/off ratio cancels the block's wind level
exactly; a block whose used rows lack either state is dropped and reported as
a new "block" selection stage. The block length defaults to pairing_max_gap.

The exclude_row column is now honoured on every turbine's rows, so a flagged
reference row removes the timestamp the way a reference outage does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mates

reference_mode="combined" runs the sum and block_mean estimates, each on its
own row selection, and blends them by inverse variance, headline and per
bin. The bootstrap now exposes each cell's resampled uplifts; two bootstraps
with the same campaign span, timebase, block length, resample count and seed
draw the same blocks, so combine_estimates can take the components'
correlation from their draw-paired resamples and report an honest sigma for
the blend. A window with no references at all falls back to block_mean with
a warning. The uncertainty diagnostics gain the reported sigma and a
component column; the selection CSV lists every component's selection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…l back on

A cell with at most three records a side blends to the fallback alone; when the
per-record scatter cannot be measured either, the result is NaN rather than
the ~1e-17 std of resamples that all see the same handful of records. That
residue read as near-certainty, and in the combined reference mode it took
the whole weight.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… draw by draw

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d is never worse than its better leg

The weights were plain inverse variance, with the resample correlation entering only the
reported sigma; a noisier leg correlated with the better one could then widen the blend
beyond the better leg alone. The weights are now wa ∝ max(sb² - r·sa·sb, 0) and
wb ∝ max(sa² - r·sa·sb, 0), which drop a strongly correlated noisy leg to zero.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ialist uses

The test turbine went through NormalOperationFilter while the references got a separate
complete-case plus availability check. With the stuck filter off both tests are the same, so
nothing changes today; one code path means any future filter change reaches the references.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…erence block

The pairing filter removes stretches where one toggle state is absent, never the counterpart
of a single dropped row, so a caller-side selection that thins one state's rows in particular
conditions (a controlled-data residual test, say) leaves the two states with different wind
mixes. A row flagged in the exclude_block column drops every row of its one-cycle block, for
every turbine and in every reference mode, so both states lose the same conditions. Block ids
are now computed whenever a block length is available, sum mode included.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e subset with the smallest sigma

Every non-empty subset of the available references is estimated in full (own row selection,
bootstrap); the subset with the smallest finite headline sigma becomes the sum estimate, alone
or as the blend's reference leg, ties to the larger subset. A reference that tracks badly or is
screened out for long stretches loses to the subsets without it. Every subset's result is
written to a reference_subsets CSV; the results CSV records the choice and the references used.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hts on label-permuted bootstraps

Ranking reference subsets by the realised uplift's bootstrap sigma, and weighting the
combined blend by the legs' realised sigmas, both favour the alternative whose realised
uplift happened to come out low, because the on/off contrast noise is skewed: a winner's
curse that biases the estimate down and under-reports sigma, and does not shrink with
more data. With decision_basis="permuted" each component also bootstraps the same rows
with the on/off labels shuffled within each reference block (block on/off counts kept),
and the min_sigma choice and the blend weights read that sigma instead. The reported
uplift and sigma are always the actual ones. Subsets being ranked skip their actual
bootstrap and the winner is re-run, so the cost is unchanged.

combine_estimates gains a `weighting` argument (another bootstrap's sigmas and resamples
to take the weights from); the blend's variance still uses the actual ones.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, decided on permuted labels

Remove the reference_mode, reference_choice and decision_basis options (and the block_scaled
mode). The method is always the minimum-variance blend of the reference leg (the summed power of
the reference subset with the smallest label-permuted sigma) and the block leg (the test turbine
against its own one-cycle block mean), with the subset choice and the blend weights judged on
label-permuted bootstraps and the reported uplift and sigma always the actual ones. With no
reference turbine the estimate is the block leg alone.

reference_block is now a required constructor argument: one toggle cycle, stated by the caller,
independent of pairing_max_gap and never inferred. The blend's per-bin n_records counts the used
upgraded rows by the merged bin label, so labeled_rows reproduces it; the data-stats CSV counts
rows of the turbines the estimate used only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- The placebo fixture is 10-minute SCADA: its 100-minute toggle cycle cannot tile an hourly grid,
  which the required reference_block now refuses.
- A component with no resample spread takes the default correlation rather than tripping
  np.corrcoef's divide warning (identical-turbine placebo).
- ToggleSpecialist hands the harness one uncertainty_diagnostics row per (condition, condition_bin),
  each leg's columns suffixed _<component>; the per-leg rows clashed with the harness's sigma column
  and its one-row-per-key rule. The uncertainty CSV keeps the per-leg rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ived power lies in a band

power_band=(lo, hi] restricts each leg's used rows to those whose bin power (rho_label * ref_total,
the state-neutral baseline power the bins use) lies in the band, and estimates the uplift over
them. The cut is a "power_band" stage in the selection accounting and is written to the run
config. A caller measuring the uplift only where a control table is followed uses it instead of
cutting on the test turbine's own power, which the state moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread benchmarking/baselines/toggle_specialist.py Outdated
… segment_imbalance_warning

toggle_period says what the caller states (one toggle cycle) and no longer reads like the
bootstrap block or the block leg. The imbalance warning was hard to interpret; the same per-stage
kept fractions stay in selection_accounting and the selection CSV.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread benchmarking/baselines/toggle_specialist.py Outdated
Comment thread benchmarking/baselines/toggle_specialist.py Outdated
Comment thread benchmarking/baselines/toggle_specialist.py
Comment thread benchmarking/baselines/toggle_specialist.py
Comment thread benchmarking/baselines/toggle_specialist.py
Comment thread benchmarking/baselines/toggle_specialist.py
Comment thread benchmarking/baselines/toggle_specialist.py
Comment thread benchmarking/baselines/toggle_specialist.py Outdated
aclerc and others added 2 commits October 7, 2026 16:58
…diagnostics

- bin_power: "baseline" (default) bins and power_band read power no upgraded row enters
  (rho_base * ref_total on the reference leg, the cycle's baseline-row mean on the block leg);
  "both_states" keeps the two-state mean, for campaigns with no true baseline state.
- toggle_datum (required): the toggle-cycle grid is tiled from it, never from the first record,
  so a missing record cannot shift cycle boundaries. Toggle studies state one and draw treatment
  starts only on cycle boundaries.
- Data-stats rows and results-CSV ratios per leg, plus the blend weight, so each leg and the
  headline are re-derivable; plots show one leg's own rows.
- Refuse more than 8 references for the subset choice; validate power_band.
- ColumnSchema.exclude_row: which turbines' flags count is the method's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…blend weight

The blended per-bin frame took sum_actual / sum_counterfactual from one leg while p50_uplift is
the blend. Each leg's frame now carries sum_actual_<leg> / sum_counterfactual_<leg>, and the
blended frame carries both legs' and the bin's weight_sum, so every bin's blend is re-derivable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aclerc aclerc changed the title Pairing filter and selection accounting for ToggleSpecialistMethod ToggleSpecialistMethod: blended reference and block legs, symmetric selection, caller-stated toggle cycle Oct 7, 2026
…ach leg's uplift

A block-bootstrap resample whose on-side or off-side reference energy is below a tenth of the whole
campaign's (a calm spell or outage drawn over and over) now counts as non-finite instead of feeding
the spread: on a near-calm fortnight the reported sigma reached 10^5 %. Every row of
uncertainty_diagnostics carries uplift, per leg and for the blend, overall and per bin, so a
consumer can see which leg an error came from without re-running.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@aclerc
aclerc merged commit 7efaac9 into v1 Oct 9, 2026
4 checks passed
@aclerc
aclerc deleted the v1-pairing-filter branch October 9, 2026 13:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants