Skip to content

V1 w1a composed wind-up method - #142

Open
aclerc wants to merge 39 commits into
v1from
v1-w1a
Open

V1 w1a composed wind-up method#142
aclerc wants to merge 39 commits into
v1from
v1-w1a

Conversation

@aclerc

@aclerc aclerc commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

See issue W1a in issues_campaigns.md
First run of an agent using wind-up to report on a synthetic campaign (placebo campaign, pass)

aclerc and others added 16 commits September 10, 2026 10:58
Extract estimate_campaign() into benchmarking/campaigns/run.py: a campaign
run that reads no ground truth, so real data with no original_df beside it
can be run at all. CampaignRunner becomes a thin truth-adding layer over it,
with its public behaviour unchanged.

- run.py: visible_mask/visible_scada re-cut from CampaignRunner._visible_dataset
  to operate on frames, so the core can clip and north and the runner can clip
  the ground-truth original separately. estimate_campaign() returns a
  CampaignReport carrying per_turbine, farm, farm_uplifts, reference_stability
  and conditional -- none of them with a truth column.
- report.py splits: write_report() is the analyst-facing report, adding a
  reference_stability table (each candidate reference estimated as if it were a
  test turbine) and estimate-only conditional output. write_campaign_report()
  is the benchmark overlay on top, unchanged in what it writes.
- scoring.py splits score_one into the estimate plus score_output(), so a
  campaign can estimate once on the truth-free path and score that same output.
- plots.py: plot_conditional_uplift draws estimates alone when the frame has no
  mean_truth column; conditional_estimates() shapes an output without truth.

test_runner_pin.py pins the runner's numbers on a campaign with an injected
upgrade before the refactor moves anything -- the existing runner tests are
placebo tests where every number is zero. test_runner.py and
test_placebo_end_to_end.py pass untouched.

poe all-fast green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LMXdmmtp1kyqhHsCnQA2w
Two corrections to the W1a issue, both describing code that has now landed.

The scope told the next reader to build wind-up in benchmarking/baselines
"like every v1 method". The spec's decision 1 rejects that: at method level
wind-up would be a no-op relabel of power_model, since the R3 screen is
already inside PowerModelMethod and R1 northing is already applied farm-wide.
Northing runs once for the whole farm and cannot sit inside a per-turbine
method without misrepresenting its scope, so wind-up is campaign-level.

The scope also did not mention the truth-free core, which is the bulk of the
work and the reason W3's isolation is structural rather than promised.

Still deferred to the phases that make them true: the declaration decisions,
the C8 boundary, the deferred distance-bounded reference selection, the new
done-when clause, and the W3 isolation mechanics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LMXdmmtp1kyqhHsCnQA2w
An analyst describes a campaign in one YAML file of campaign facts and runs
it, without writing Python.

- loader.py: YAML plus a turbines sidecar into a CampaignSpec. Named column
  schemas (hill_of_towie), the northing tri-state, and a tagged timing block
  so a third mode is additive. Timestamps are coerced here rather than by
  PyYAML: tz-aware is converted to UTC, naive is read as UTC, and the resolved
  values are echoed into the run output so a mis-declared timezone is visible.
  Reanalysis is not declared -- it is self-served from the turbines file's
  centroid over a window rounded out to whole calendar years, so campaigns on
  one site share a cache entry.
- composed.py: wind_up_method() is the accepted power_model defaults under one
  name, and run_declaration() runs a declaration end to end onto the truth-free
  report. The module name is a placeholder; its docstring says so.
- __main__.py: python -m benchmarking.campaigns run campaign.yaml --out DIR

test_composed.py compares wind-up's configuration field by field against the
power_model carried_forward_methods builds, so a default drifting on either
side is caught. test_declaration_end_to_end.py runs the whole path in both
modes on a synthetic farm and asserts the report carries no truth column.

Verified on real Hill of Towie SCADA through the CLI in both modes: reanalysis
self-served from the centroid, four northing changepoints discovered across
nine turbines, the R3 screen ruling out T10 on the prepost arm, and no truth
column anywhere in the output tree.

poe all-fast green: 1425 passed, 98 deselected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LMXdmmtp1kyqhHsCnQA2w
A turbine that is not a valid reference is currently dropped from the
estimate's frame entirely, so its wake is invisible. power_model already has
the mechanism -- a power_free reference keeps direction and a waking boolean
but contributes no power -- but it is reachable only through R3's screen, and
only for turbines already in the candidate pool.

Two gates: CampaignContext.select() keeps test_wtg, candidate_references and
also only, and PowerModelMethod never passes also; _checked_power_free raises
on a name that is not already in refs.

It matters most where the campaign is largest: all 21 Hill of Towie turbines
were upgraded by AeroUp, so each estimate would keep only the hand-declared
references and none of the wake neighbours.

Ordered with or just after C8, whose per-turbine timeline makes the role
time-ranged rather than whole-turbine, and before W1b/W2, since it adds a
field to the CampaignContext seam W2 publishes. Carries an open design-note
§3 question to settle on C5's fixture: the waking boolean is thresholded on
power, which is very likely treatment-invariant for a Cp change and is not
for wake steering.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LMXdmmtp1kyqhHsCnQA2w
The machinery for a W3 dry run: build a campaign directory an analyst can be
given, holding everything they see and nothing else.

- handover.py: writes <root>/analyst/ (brief, a blank campaign.yaml template,
  scada.parquet, turbines.csv, docs/) and <root>/key/ground_truth.json. Only
  synthetic_df reaches analyst/; original_df and run_metadata carry the answer
  and stay behind. Two tests carry that guarantee: the answer key is outside
  analyst/, and analyst/ holds no original data and no run metadata.
- placebo.py: placebo_instance() draws the upgraded turbines and the treatment
  start from a seed, so a handover does not identify its own campaign by
  matching the checked-in defaults. Its window stays before the site's real
  blade-upgrade installs and it never upgrades the site's known bad turbine,
  so the truth stays 0 by construction. placebo_campaign() and
  placebo_analysis_period() take a campaign_start to make that possible.
- docs/running-a-campaign.md: the analyst-facing documentation, and the thing
  W3 actually measures. Covers the declaration, the run, and how to read the
  outputs -- leading on reference_stability.csv, since that is the campaign
  checking its own references. Its "known limits" section names the flat
  timing model (C8) and dropped wake neighbours (C9).

The campaign.yaml template is deliberately blank: a populated one would answer
the brief before the analyst starts.

poe lint clean; 326 campaign tests pass, poe test-fast green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LMXdmmtp1kyqhHsCnQA2w
Two handovers on real Hill of Towie SCADA were given to fresh agents holding
only the analyst directory. The placebo instance was read correctly ("nothing
at all -- I cannot distinguish this from zero"); the injected instance
recovered the magnitude (+1 to +2.5% against a true +2.06%) but read a
wind-speed-dependent Cp gain as a flat one.

Fixed, all three found by being used:

- The reanalysis centroid moved with the turbine roles, so dropping a
  reference shifted it, changed the ERA5 cache key and silently changed a
  top-5 model feature -- during the very reference-set sensitivity run both
  analysts relied on. It is now taken over the whole turbines file and rounded
  to 2 dp, so a site keeps one cache entry and the roles cannot perturb it.
- placebo_instance drew 4-6 upgraded turbines regardless of farm size, which
  on a 9-turbine slice left two references after screening. The draw now
  scales with the farm and always leaves a pool the screen can judge.
- running-a-campaign.md said `name` names the output subdirectory, which is
  false when --out is given, and located reanalysis at the centre of the
  campaign's turbines rather than the site's. It also now documents the
  reference-set sensitivity re-run, which is what actually decided both
  assessments and was suggested nowhere.

Recorded rather than fixed: CF15 carries the scores, the isolation holes
(CLAUDE.md is auto-injected into any agent whose cwd is the checkout, and
concurrent runs see each other's processes) and the finding that a placebo on
real SCADA cannot score the fault half. The W3 section carries the remaining
documentation gap list for W2, and W1a's done-when now names the dry run.

poe all-fast green: 1452 passed, 98 deselected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LMXdmmtp1kyqhHsCnQA2w
The campaign.yaml template still said `name` "names the output subdirectory"
after the doc was corrected to say it does not once --out is given. A dry-run
analyst caught the contradiction from reading alone, before running anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LMXdmmtp1kyqhHsCnQA2w
Two corrections from the dry runs.

The documentation told the analyst that re-running with a different reference
set was "the single most useful check you can make". That codifies the manual
re-work v1 exists to remove: one run should answer the question. It now judges
scale from reference_stability.csv, which comes from the same single run and
is the noise yardstick, and says that wanting to re-run is a limitation to
report rather than a procedure to follow.

placebo_instance drew its treatment start from 2018 or 2019, and a 2019 draw
puts the treated period into 2020 on a 2019-only baseline -- the one window
that reads far worse than the rest. It now draws 2018 only, so an instance
lands on a window the method is known to handle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LMXdmmtp1kyqhHsCnQA2w
An unconstrained draw clustered the test turbines: on the 21-turbine instance
it picked T02/T04/T05 as a mutual triangle and T13/T14 as an adjacent pair,
leaving T05 with no reference among its two nearest neighbours at all. A real
campaign is designed so every test turbine has nearby references to compare
against, so the draw now does the same.

Test turbines are picked one at a time, and each pick retires its own two
nearest neighbours plus any turbine holding the pick among *its* two nearest.
The reverse half matters because nearest is not symmetric: T21's two nearest
are T20 and T19, but T19's are T18 and T20, so picking T19 first would
otherwise leave T21 pickable and adjacent to it.

placebo_instance now takes the coordinates the rule reads, and passes them
through to the campaign instead of the placeholder zeros.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LMXdmmtp1kyqhHsCnQA2w
wind_up.geodesy is now the one place distances and bearings come from: one
geographiclib Inverse call per pair (v0 made two), a pairwise matrix for a whole
layout, and local east/north metres measured from the minimum easting and
northing so the maxima give the farm's extent.

The shortcuts it replaces were measurably off on the Hill of Towie layout: the
equirectangular spacing in the placebo draw by up to 0.24% in distance, the
haversine helper in benchmarking/synthetic by 0.35%, its spherical bearing by
0.06 deg. WakeSteering pairs now use the geodesic; no pinned number moved.

v0's calc_distance, calc_bearing and get_distance_and_bearing delegate to it,
with functools.cache in place of the hand-rolled module-global dict, so v0
numbers are unchanged. The cache-timing test clears the cache first, since
calc_bearing now warms it.

The capacity-factor bubble plot projected through UTM, whose grid north is off
true north by the convergence angle: 2.4 m across two Homer test turbines.
It now uses local east/north, keeps its median centring, and utm leaves the
dependencies.

wind_up.layout holds a validated turbine table with its geodesic matrices,
the IEC 61400-12-1 disturbed sector (moved from v0, which re-exports it) and
front-row classification, for the campaign design utility that follows. The
upwind cases of v0's test_get_iec_upwind_turbines are ported to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147zaQzN7s1THjdDCrwdA1P
design_campaign chooses test turbines that comply with step 2 of the uplift
validation methodology ("at least three reference turbines"), as rules a set
of test turbines either meets or not:

- among each test turbine's 4 nearest reference-eligible turbines, within
  20 of its rotor diameters, at least 3 are not test turbines;
- a test turbine's nearest neighbour is never a test turbine;
- the front-row test count is the nearest whole number to the farm's
  front-row share, so neither too many nor too few;
- excluded and reference-only turbines are never tested.

It takes the most test turbines any compliant design allows, then walks the
test priority, keeping each turbine for which a compliant design of that size
still exists (an exact scipy milp look-ahead). Unlisted candidates follow in a
seeded random order. check_design judges any set, however it was chosen, and
write_design writes a per-test-turbine compliance table, every turbine's role
and outcome, a declaration roles block, a summary and two maps.

Brute force over every subset of small random layouts confirms the maximum is
exact and the walk returns the first compliant set in priority order.

placebo_instance now designs with it at the maximum count, replacing the
spacing draw added in f373fe5, and keeps T17 reference-only. On the real Hill
of Towie layout that is 10 of 21 test turbines, 7 of them front row against a
fair share of 6.67. The clustered draw of the first dry runs is reported
non-compliant. test_handover's fixture was missing the coords argument that
commit made required; it now passes them.

The declaration loader skips turbines.csv rows without a name, so a design's
layout file can serve as the sidecar. docs/designing-a-campaign.md explains
the inputs, rules and outputs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147zaQzN7s1THjdDCrwdA1P
… allows

Maximising the count pushes references outward: the furthest reference was
often 17-20 rotor diameters on Hill of Towie subsets. After the listed test
priority is walked, the design now finds the least reference-distance limit
that still allows its size alongside the turbines kept, and walks the unlisted
candidates, in their seeded order, under that limit. summary.yaml reports it
as reference_limit_d.

It sits after the listed priority, not before it: applied first, the tightest
limit usually leaves a single design (a 5x5 grid went from 8 distinct designs
to 1), so neither priority nor seed would matter. A fully listed priority is
therefore never overridden, and placebo_instance now lists every candidate in
a shuffled order so its randomness is the priority's, as intended.

On Hill of Towie clusters the median furthest reference drops by 1-3 D (12
turbines: 11.8 -> 8.6 D). The whole farm is unchanged: both of its maximum
designs need 11.8 D.

The first version lost every test turbine on one sparse five-turbine subset:
candidate limits were distance / diameter but membership was tested as
distance <= limit * diameter, and one ulp dropped the turbine that set the
limit; the search then fell back to an unverified limit. Both now use one
distance-in-diameters matrix, and the search falls back to the site's own
limit, where a design is known to exist. That subset is a regression test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147zaQzN7s1THjdDCrwdA1P
At the maximum count Hill of Towie has exactly two compliant designs, differing
only in T01 against T02, so every instance upgraded the same nine turbines and
only the start date varied. One below the maximum there are 60 compliant
designs (464 two below), and ten seeds now draw at least five different test
sets. A farm that supports a single test turbine still gets one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147zaQzN7s1THjdDCrwdA1P
…ion ends

CF16 records what designing the test turbines found on Hill of Towie: the
reference rule decides the count (7 test turbines under the strict 3-nearest
rule, 10 under the chosen pool rule), the site has only 2 compliant designs at
that maximum against 60 one below it, clusters design down to 4 turbines, and
only a tie-break confined to the unlisted candidates keeps priority meaningful.

The W3 section now says its instances are designed, not drawn, and its gap list
carries the conditional.csv finding, checked against relevel_conditional: the
per-bin levels are scaled by one factor to the headline, so read the class from
the shape across bins. W2 gains the release of the design utility. The running
page points to the new designing page.

The design spec is force-added under docs/superpowers/specs, as W1a's was, with
its departures after implementation listed at the end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147zaQzN7s1THjdDCrwdA1P
…r fix into W1a

write_handover takes the design that chose the campaign's upgraded turbines and
writes it to analyst/design/: both maps, the compliance table, the summary and
the roles block, as a real campaign's design documents would arrive. The
priority columns of its turbines table are withheld, because a placebo's
priority is a random shuffle that would read as expected uplift. A design that
does not match the campaign is refused. placebo_design returns the design behind
placebo_instance for the same seed.

W1a gains an item ahead of the prepost dry run: each test turbine's estimate
drops every other test turbine from its frame, losing their waking state. With
designed instances testing 9 of 21 turbines, each estimate loses 8 wake
neighbours. C9's whole-turbine form is pulled forward to fix it; C9 keeps the
time-ranged form and the wake-steering question.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147zaQzN7s1THjdDCrwdA1P
A turbine wakes its neighbours whatever its role, so no turbine with data is
dropped from an estimate any more. CampaignContext gains a wake_contributors
role: every turbine in the frame that is neither the test turbine nor one of
its candidate references. select() keeps them, and power_model feeds them to
build_reference_features as wake_only, with the same channels as a screened
reference: availability, north-calibrated direction and a waking boolean,
never power.

The same holds when power_model estimates a reference, for the screen or the
reference-uplift table: the test turbine, and any reference left out of that
estimate's pool, stay in as wake contributors.

Excluded is now a role only, never tested and never offered as a reference.
Its data is no longer dropped by usable_mask, and SyntheticCampaign.generate
keeps every turbine it is given, declared or not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147zaQzN7s1THjdDCrwdA1P
@aclerc
aclerc changed the base branch from main to v1 September 11, 2026 13:35
@aclerc
aclerc requested a balanced review from Copilot September 11, 2026 14:24

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.

🟡 Changes recommended

Several correctness and packaging issues can produce misleading compliance output, unreproducible placebo keys, or import failures.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Introduces the first composed, truth-free wind-up campaign workflow, including campaign design, YAML loading, reporting, geodesic calculations, and wake-only contributors.

Changes:

  • Adds campaign declaration, execution, reporting, handover, and CLI workflows.
  • Adds geodesic campaign-design utilities and maps.
  • Preserves non-reference turbines as power-free wake contributors.
File summaries
File Description
uv.lock Removes the UTM dependency.
pyproject.toml Updates dependencies and type-checking modules.
src/wind_up/campaign_design.py Implements campaign selection and compliance checks.
src/wind_up/layout.py Adds layout validation and wake geometry.
src/wind_up/geodesy.py Adds WGS84 distance and coordinate utilities.
src/wind_up/campaign_design_plots.py Adds campaign-design maps.
src/wind_up_v0/waking_state.py Reuses shared geodesic and IEC calculations.
src/wind_up_v0/plots/misc_plots.py Replaces UTM plotting coordinates.
benchmarking/campaigns/__init__.py Exports truth-free campaign APIs.
benchmarking/campaigns/__main__.py Adds the campaign CLI.
benchmarking/campaigns/composed.py Composes the wind-up campaign method.
benchmarking/campaigns/context.py Derives references and wake contributors.
benchmarking/campaigns/declaration.py Updates campaign visibility semantics.
benchmarking/campaigns/handover.py Builds isolated analyst handovers.
benchmarking/campaigns/loader.py Loads YAML campaign declarations.
benchmarking/campaigns/placebo.py Adds randomized compliant placebo instances.
benchmarking/campaigns/report.py Adds truth-free analyst reports.
benchmarking/campaigns/run.py Implements the truth-free execution core.
benchmarking/campaigns/runner.py Layers benchmark scoring over the core.
benchmarking/harness/__init__.py Exports output scoring.
benchmarking/harness/context.py Adds wake-contributor context support.
benchmarking/harness/plots.py Supports estimate-only conditional plots.
benchmarking/harness/scoring.py Scores previously computed outputs.
benchmarking/synthetic/__init__.py Updates synthetic geometry exports.
benchmarking/synthetic/geometry.py Uses ellipsoidal geodesics.
benchmarking/synthetic/sources/hill_of_towie.py Adds rotor diameter metadata.
benchmarking/synthetic/upgrades.py Uses shared wake geometry.
benchmarking/baselines/power_model/features.py Adds wake-only feature channels.
benchmarking/baselines/power_model/method.py Integrates wake contributors into estimates.
docs/designing-a-campaign.md Documents campaign design.
docs/running-a-campaign.md Documents declaration and reporting workflows.
docs/v1/findings_campaigns.md Records campaign-design findings.
docs/v1/issues_campaigns.md Updates W1a/C9 scope and status.
tests/wind_up/layouts.py Adds synthetic layout fixtures.
tests/wind_up/test_campaign_design.py Tests design and compliance behavior.
tests/wind_up/test_campaign_design_outputs.py Tests design files and maps.
tests/wind_up/test_geodesy.py Tests geodesic utilities.
tests/wind_up/test_layout.py Tests layout and wake geometry.
tests/test_waking_state.py Stabilizes cache timing tests.
tests/plots/test_misc_plots.py Tests corrected plot coordinates.
tests/benchmarking/synthetic/test_wake_steering.py Tests geodesic steering bearings.
tests/benchmarking/harness/test_context.py Tests wake contributors in contexts.
tests/benchmarking/harness/test_plots.py Tests truth-free conditional plots.
tests/benchmarking/harness/test_scoring.py Tests output-based scoring.
tests/benchmarking/campaigns/test_composed.py Tests composed method configuration.
tests/benchmarking/campaigns/test_context.py Tests campaign wake roles.
tests/benchmarking/campaigns/test_declaration.py Tests expanded data visibility.
tests/benchmarking/campaigns/test_declaration_end_to_end.py Exercises declared campaigns end-to-end.
tests/benchmarking/campaigns/test_handover.py Tests handover isolation.
tests/benchmarking/campaigns/test_loader.py Tests declaration loading.
tests/benchmarking/campaigns/test_main.py Tests CLI parsing.
tests/benchmarking/campaigns/test_placebo.py Tests randomized placebo design.
tests/benchmarking/campaigns/test_report.py Tests analyst report output.
tests/benchmarking/campaigns/test_run.py Tests the truth-free core.
tests/benchmarking/campaigns/test_runner.py Updates wake-contributor expectations.
tests/benchmarking/campaigns/test_runner_pin.py Pins benchmark behavior.
tests/benchmarking/baselines/test_power_model_features.py Tests wake-only features.
tests/benchmarking/baselines/test_power_model_method.py Tests wake-aware estimation.
Review details
  • Files reviewed: 58/59 changed files
  • Comments generated: 6
  • Review effort level: Balanced

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

Comment thread src/wind_up/campaign_design.py
Comment thread benchmarking/campaigns/loader.py
Comment thread benchmarking/campaigns/placebo.py
Comment thread src/wind_up/campaign_design.py
Comment thread src/wind_up/layout.py Outdated
Comment thread docs/designing-a-campaign.md Outdated
aclerc and others added 3 commits September 11, 2026 16:00
- Require scipy >= 1.9, the first release with scipy.optimize.milp.
- Reject a turbine name given twice in a declaration's turbines file.
- Record a placebo instance's seed on its campaign, so the answer key names it.
- Mark a reference-only test turbine's compliance-table row non-compliant.
- Treat pandas nullable missing values (pd.NA, NaT) as blank layout names.
- Document that rotor_diameter_m needs a value on at least one turbine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Acw68DCtZYU5aEnV5xBugG
numpy 2.2 (the Python 3.10 resolution) infers np.arange(360.0) as
floating[Any], which failed mypy in the 3.10 lint job; an explicit
dtype gives float64 under numpy 2.2 and 2.4 alike.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Acw68DCtZYU5aEnV5xBugG
…urbines by outcome

write_design now also saves front_row_map.png: the farm's turbines in east/north metres, green when
front row and purple otherwise, with other farms' turbines grey. The design maps drop the hollow
reference-only marker and its legend entry; a reference-only turbine is drawn like any other
non-test turbine, blue when in use and grey when not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Acw68DCtZYU5aEnV5xBugG

@aclerc aclerc left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

General comment: the power_model benchmark jsons need updating due to the changes in this PR

Comment thread src/wind_up/campaign_design.py Outdated
Comment thread src/wind_up/campaign_design.py Outdated
Comment thread src/wind_up/layout.py Outdated
Comment thread docs/superpowers/specs/2026-09-11-campaign-design-utility-design.md Outdated
Comment thread docs/v1/issues_campaigns.md Outdated
Comment thread docs/designing-a-campaign.md Outdated
Comment thread docs/designing-a-campaign.md Outdated
Comment thread docs/designing-a-campaign.md Outdated
Comment thread docs/designing-a-campaign.md Outdated
Comment thread docs/designing-a-campaign.md
aclerc and others added 6 commits September 11, 2026 17:13
…spec from the PR

The campaign_design module, ComplianceReport and Layout docstrings no longer
restate the rules or the tables' columns; docs/designing-a-campaign.md carries
the method. The docs take the review's wording for the roles, say the utility
does not suit upgrades with turbine-turbine interactions such as wake steering,
and list the maps without describing their colours. The campaign design spec is
untracked again, as docs/superpowers/ is git-ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Acw68DCtZYU5aEnV5xBugG
The table is built from the constant, and a row whose columns drift from it
raises instead of leaving an empty column. check_design and design_campaign
point to docs/designing-a-campaign.md for the rules and the layout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Acw68DCtZYU5aEnV5xBugG
…ps in the docs

examples/v1/design_hill_of_towie.py designs Hill of Towie's 21 turbines with T17
reference-only, from a turbines CSV taken from the Hill of Towie open dataset (CC BY
4.0). It writes the design under examples/v1/output/ and refreshes the two maps
docs/designing-a-campaign.md now embeds in place of the worked example's prose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Acw68DCtZYU5aEnV5xBugG
Every power_model cell outside 6-month prepost reproduces the ec7dca2 recording
within 0.01 pp, most bit-identical, and toggle is bit-identical throughout;
study_toggle_methods_compare moves at most 0.017 pp on its linux file and not at
all on the portable one.

The exception is prepost at 6 months on cp_minus_10pct and cp_plus_3pct, where
bias rises 0.248 and 0.289 pp because T07's screen now rules out T03 at 2.70 and
2.60 pp against its 2.5 pp floor. Re-running those two profiles from a 4ab8259
snapshot reproduces the old numbers exactly, so the move is the wake-contributor
change: screening estimates now keep the test turbine in the frame. CF17 records
the evidence and why the screen is not being tuned against it yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCpUmwPVSzxJUaYbvKZTwa
aclerc and others added 2 commits September 12, 2026 11:20
poe test-fast drops from 613 s to about 100 s. It runs under pytest-xdist with
work stealing, which suits a suite whose per-test cost is very uneven, and
coverage now comes from pytest-cov so the workers are measured.

Parallel workers alone bought nothing: each LightGBM fit already took every core,
so the suite was saturated and system time exceeded wall time. The toy fits are
single-threaded now, and _screen_method passes _FAST_PARAMS rather than fitting
the 600-tree default on its 200-day frames. The placebo instance helpers are
cached, since the design solve behind them is the expensive part and the tests
read the same seeds repeatedly.

test_screening_leaves_no_temp_directories globbed the shared temp directory, so
it saw other workers' runs and failed; it now has a temp root of its own.

60 s is not reachable by scheduling: the suite is ~13 CPU-minutes, so twelve
cores floor at ~65 s. Deselecting the heaviest tests saves 4 s, so marking tests
slow would cost coverage for nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCpUmwPVSzxJUaYbvKZTwa
Both dry runs clear the one-run bar on the full 21-turbine farm: the first
prepost run and a re-run toggle one each said "cannot distinguish this from zero"
against a truth of zero. CF18 records them, including the near-miss where an
analyst declined a post-hoc reference subgroup its own phase 2 then proved wrong,
and two real anemometer faults on T01 and T16 that no run reports.

What the analysts asked wind-up for now goes to docs/v1/analyst_feedback.md as
AF-numbered entries for triage, rather than staying in a transcript. AF1 to AF3
are the three both analysts raised independently: a reference-relative estimate
with a standard error, reference stability resolved by month, and an anemometer
directory mirroring northing.

CF17 records the benchmark move behind the re-recorded baselines, and why the
reference screen is not being tuned against it: a true bad reference and this
false positive sit 0.1 pp apart, and the benchmark's small turbine groups would
over-fit any change made now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCpUmwPVSzxJUaYbvKZTwa
@aclerc
aclerc marked this pull request as ready for review September 12, 2026 10:21
aclerc and others added 9 commits September 12, 2026 12:35
poe test-fast asked for every core, which made the machine unusable while the
gate ran. It now asks for four workers through PYTEST_XDIST_AUTO_NUM_WORKERS,
which xdist's own auto hook reads, so the count is one environment variable to
change and nothing in pytest config depends on it: plain pytest, pytest
-p no:xdist and single-file runs are all unaffected.

On a 12-core machine the fast suite is 4m46s serial, 2m12s on four workers,
1m46s on six and about 1m35s on ten. Four leaves two thirds of the machine free
for the cost of 26 seconds over six.

poe test is parallel now as well, since CI spends about 30 minutes in it. It
uses loadfile rather than worksteal: the slow tests carry big frames on
module-scoped fixtures, and keeping a file on one worker loads each of those
once rather than once per worker. Locally the full suite takes 15m05s on four
workers, 1704 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCpUmwPVSzxJUaYbvKZTwa
The v1 feature branches PR into v1 rather than main, and the branch filter named
only main, so those PRs ran no checks at all. The one run PR 142 does have came
from it briefly targeting main by accident.

The test step sets PYTEST_XDIST_AUTO_NUM_WORKERS to 4 because the runner is
dedicated to the job, where the local default deliberately is not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCpUmwPVSzxJUaYbvKZTwa
test_wedowind_pitch_angle asserts preprocess_warning_counts == 0 and failed in
CI on 3.10 with 1. result_manager is a process-wide singleton whose
stored_warnings an analysis clears only when it reads them, so a warning left
behind by an earlier test in the same process is counted by the next one. The
assertion therefore depended on what had run before it, which running in
parallel changed.

Reproduced by leaving a single warning in the singleton before the test: it
fails with exactly the CI diff, left 1 against right 0. An autouse fixture now
empties the store before every test, so the counts are the test's own whatever
else ran, in any order and on any worker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCpUmwPVSzxJUaYbvKZTwa
"Run it" gives the command and never says where from, while the declaration's
data: paths resolve against the working directory. The first human analyst hit
it immediately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCpUmwPVSzxJUaYbvKZTwa
The commented `# prepost:` / `# toggle:` lines in the declaration template read
as required keys rather than labels, so the first human analyst nested prepost
and changeover under mode; mode is then a mapping and the error reports its repr
instead of naming the two strings it accepts.

The working-directory entry claimed the data: paths resolve against the working
directory. They resolve against the declaration's own directory (loader.py:114
and 169-175). The gap is real, the reason given for it was not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCpUmwPVSzxJUaYbvKZTwa
Alex's call, 2026-09-12: anemometer faults come out of the brief's failure-mode
list and will not be injected into campaigns, because CF11 measured them at
0.21 pp worst case in prepost and exactly zero in toggle. AF3 keeps only the
weaker half of its argument, that the campaign cannot see a fault that is really
in the data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCpUmwPVSzxJUaYbvKZTwa
The brief an analyst is judged against, and the failure-mode menu it names, live
only in a scratch build script, so a dry run is not reproducible from the repo
and edits to the menu are not reviewable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCpUmwPVSzxJUaYbvKZTwa
The brief a W3 dry run is judged against lived only in a scratch build script,
so a run was not reproducible from the repo and edits to the menus it offers
were not reviewable. handover.campaign_brief renders it from the campaign:
who was treated, whether prepost or toggle and on what cycle, the period the
SCADA covers, and the two menus.

The fault menu no longer names anemometer gain steps or drift, and campaigns
will not inject them: CF11 priced them at 0.21 pp in prepost and exactly zero in
toggle, so a dry run spent looking for them measures nothing. A test pins that,
since it is a decision rather than an oversight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCpUmwPVSzxJUaYbvKZTwa

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.

🟡 Changes recommended

Layout and toggle declaration validation gaps can produce invalid campaign geometry or silently incorrect schedules.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 66/71 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment on lines +225 to +230
if mode == "toggle":
return ToggleSchedule(
period=pd.Timedelta(block["period"]),
start=_timestamp(block["start"]) if block.get("start") is not None else None,
start_on=bool(block.get("start_on", False)),
)
Comment thread src/wind_up/layout.py
Comment on lines +82 to +88
diameters = pd.to_numeric(column(ROTOR_DIAMETER_COL), errors="coerce").astype(float)
if diameters.isna().all():
msg = "the layout has no rotor diameter for any turbine; at least one is needed"
raise ValueError(msg)
unknown = diameters.isna()
filled = tuple(str(n) if n is not None else f"row {i}" for i, n in enumerate(names) if unknown.iloc[i])
diameters = diameters.fillna(diameters.max())
Comment on lines +35 to +37
:param wake_contributors: turbines kept in the frame for their wake alone -- the campaign's
other changed turbines. Never references, so a method may read their operating state
and direction but not their power.
aclerc and others added 3 commits September 12, 2026 15:48
The runner configures logging with basicConfig, so the console is the only copy,
while v0's setup_logger writes analysis.log beside the results. The screen's
thresholds and its early stop live only in that output, and when it stops
reference_stability.csv is header-only, so closing the terminal loses the only
account of how the reference pool was judged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCpUmwPVSzxJUaYbvKZTwa
The runner configured logging with basicConfig alone, so the console was the
only copy and closing the terminal lost it. run_declaration now also sends the
root logger to <out>/run.log, which covers the command line and any caller,
since that is where the output directory is resolved. A second run in the same
process replaces the handler rather than adding one, so it writes to its own
directory and not also to the first one's.

This matters because stdout is load-bearing here: the reference screen's
thresholds, the size of the pool it judged, and whether it stopped early are
reported nowhere else, and when it stops reference_stability.csv is header-only.
The documentation now says the log is there and why to read it, and says which
directory to run from -- both gaps a human analyst hit today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCpUmwPVSzxJUaYbvKZTwa
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