Rig finding F-1. cheaproll._Charge shared the peak room by rating; control shares it by room left to full (packshare.weights()). With unequal levels one battery's drawn block ended too early.
The roll now uses packshare.weights() over the simulated levels (re-read each 5-min step, each battery capped at its rating, a full one drops out). No control change.
test_ux77.py: two unequal-level walks (rig checks 2 and 3), a by-rating mutant that goes red, and the walks now model control's split by room left (the old by-rating model was the same mistake). Part 2 uses its own level (B at 94 %) since B no longer finishes early in the shared walk.
Rig finding F-1. `cheaproll._Charge` shared the peak room by rating; control shares it by room left to full (`packshare.weights()`). With unequal levels one battery's drawn block ended too early.
The roll now uses `packshare.weights()` over the simulated levels (re-read each 5-min step, each battery capped at its rating, a full one drops out). No control change.
`test_ux77.py`: two unequal-level walks (rig checks 2 and 3), a by-rating mutant that goes red, and the walks now model control's split by room left (the old by-rating model was the same mistake). Part 2 uses its own level (B at 94 %) since B no longer finishes early in the shared walk.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
The cheap-run roll (cheaproll._Charge) shared the peak room by rating, control
shares it by room left to full (packshare.weights). With unequal levels one
battery's drawn block ended too early (rig F-1). The roll now uses
packshare.weights over the simulated levels, each battery capped at its rating,
a full one dropping out. test_ux77 gains two unequal-level walks and a
by-rating mutant; its walks model control's split by room too.
Co-Authored-By: Claude <noreply@anthropic.com>
Verdict: right. The roll now shares the room the same way control's commands do, and it changes no control code.
1. Share matches control
I drove the real control cycle (read_inputs() + cycle(), arbiter, allocator with pack_weights, peak guard on P1) with two plants at 5000 W / 15.36 kWh and 4600 W / 20.48 kWh. Control's charge per battery was read from its setpoint commands. Home was read from homeview.build() at every quarter + 2 min.
Night
Control's split (A / B)
Room left (A : B)
Misses, head / base
Equal 30/30, LS+LS, 0.6 kW
1229 / 1639 W
10.75 : 14.34
4 / 16
Unequal 30/20, LS+LS, 0.6 kW
1136 / 1731 W
10.75 : 16.38
3 / 23
Near full 40/88, LS+CH, 2.85 kW
475 / 127 W
9.22 : 2.46
0 / 55
B at 90, LS+LS, 0.6 kW
A only
-
0 / 0
Control splits by room left to full, to the watt. A by-rating split would give 1493 / 1373 W.
_shares() matches this. No battery goes above its rating, and a full battery drops out because chargers() is re-read every 5 minutes.
Every miss on base, and on the by-rating mutant (the same counts), is a block that ends early.
Every miss left on head is in the run's last hour (N-1 below).
2. Test model
The old walk split by rating. Real control does not, so the old model was wrong and its green on the old walks was false.
The new split_by_room() gives the same stops as real control: 02:49 / 03:12 on the check 2 walk.
It is still a copy of the rule, not derived from control's commands (N-3).
Part 2 at B 94 %: B charges 22:00-00:54 and is at its level at 02:30, so the assertion still tests "charged, now done". At 90 %, real control charges B to 07:00, so the old fixture only held under the wrong model.
3. No control change
The diff is cheaproll.py, test_ux77.py and DECISIONS. cheaproll is imported only by homeview and dayview.
Digests, base vs head: identical command count, sha, net EUR and end SoC on 5 cases (modes_off, ownls_state_epex, ownls_hourly_real, own_state_epex, own_hourly_real).
The probe's control commands are also identical on base and head.
4. Mutant
I made a purged copy with _shares by rating. Both new walks went red, plus the 3 old walks: 5 failed.
The head's test_ux77 against the base tree is also red, with 5 failed.
5. Suite and CI
Local full suite: 96 files, 0 failed (1423 s).
CI run 1540 (job 2691) on 6d2b9b5 is a real run: 96 files, 0 failed, and test_ux77 passed.
Not blocking (follow-ups)
N-1: in the run's last hour, the block runs past the battery's stop, by up to one price slot.
Seen: equal night at 02:02, block to 03:00 while control stops 02:27. Unequal night at 03:02, B's block to 04:00 while control stops 03:12.
Cause: for_today() returns None when the run under way is not rolled, so Home draws the plain hourly run.
This is round 1's design and within part 1's one-slot tolerance. The "within one quarter" rule holds only at the walk's 5 sample times.
Fix idea: clip each battery's block to its _Charge stop even when there is no roll.
N-2: when a capacity is unknown, the roll splits by rating but control weighs every battery 1.0 (equal split). This is about 4 % off, and only with an unknown capacity.
N-3: the walk's split is a copy of control's rule. To derive it from control, drive read_inputs() + cycle() and read the per-battery set_number commands (my probe does this). Alternatively, take the allocation from c.allocation.
Info: a 2-minute tail with no block (A, 02:47) is MIN_KWH hiding under 0.05 kWh, by design.
Info: at N≥3 with a Solar first battery on, pack_weights()'s mode layers could split differently from packshare.weights(). I did not test this.
## Review (round 2): approve and merge
Head reviewed: `6d2b9b5`, on `release/1.0` `47c89a1`.
**Verdict: right.** The roll now shares the room the same way control's commands do, and it changes no control code.
### 1. Share matches control
I drove the real control cycle (`read_inputs()` + `cycle()`, arbiter, allocator with `pack_weights`, peak guard on P1) with two plants at 5000 W / 15.36 kWh and 4600 W / 20.48 kWh. Control's charge per battery was read from its setpoint commands. Home was read from `homeview.build()` at every quarter + 2 min.
| Night | Control's split (A / B) | Room left (A : B) | Misses, head / base |
|---|---|---|---|
| Equal 30/30, LS+LS, 0.6 kW | 1229 / 1639 W | 10.75 : 14.34 | 4 / 16 |
| Unequal 30/20, LS+LS, 0.6 kW | 1136 / 1731 W | 10.75 : 16.38 | 3 / 23 |
| Near full 40/88, LS+CH, 2.85 kW | 475 / 127 W | 9.22 : 2.46 | 0 / 55 |
| B at 90, LS+LS, 0.6 kW | A only | - | 0 / 0 |
- Control splits by room left to full, to the watt. A by-rating split would give 1493 / 1373 W.
- `_shares()` matches this. No battery goes above its rating, and a full battery drops out because `chargers()` is re-read every 5 minutes.
- Every miss on base, and on the by-rating mutant (the same counts), is a block that ends early.
- Every miss left on head is in the run's last hour (N-1 below).
### 2. Test model
The old walk split by rating. Real control does not, so the old model was wrong and its green on the old walks was false.
The new `split_by_room()` gives the same stops as real control: 02:49 / 03:12 on the check 2 walk.
It is still a copy of the rule, not derived from control's commands (N-3).
Part 2 at B 94 %: B charges 22:00-00:54 and is at its level at 02:30, so the assertion still tests "charged, now done". At 90 %, real control charges B to 07:00, so the old fixture only held under the wrong model.
### 3. No control change
- The diff is `cheaproll.py`, `test_ux77.py` and DECISIONS. `cheaproll` is imported only by `homeview` and `dayview`.
- Digests, base vs head: identical command count, sha, net EUR and end SoC on 5 cases (modes_off, ownls_state_epex, ownls_hourly_real, own_state_epex, own_hourly_real).
- The probe's control commands are also identical on base and head.
### 4. Mutant
I made a purged copy with `_shares` by rating. Both new walks went red, plus the 3 old walks: 5 failed.
The head's `test_ux77` against the base tree is also red, with 5 failed.
### 5. Suite and CI
- Local full suite: 96 files, 0 failed (1423 s).
- CI run 1540 (job 2691) on 6d2b9b5 is a real run: 96 files, 0 failed, and `test_ux77` passed.
### Not blocking (follow-ups)
- **N-1:** in the run's last hour, the block runs past the battery's stop, by up to one price slot.
- Seen: equal night at 02:02, block to 03:00 while control stops 02:27. Unequal night at 03:02, B's block to 04:00 while control stops 03:12.
- Cause: `for_today()` returns None when the run under way is not rolled, so Home draws the plain hourly run.
- This is round 1's design and within part 1's one-slot tolerance. The "within one quarter" rule holds only at the walk's 5 sample times.
- Fix idea: clip each battery's block to its `_Charge` stop even when there is no roll.
- **N-2:** when a capacity is unknown, the roll splits by rating but control weighs every battery 1.0 (equal split). This is about 4 % off, and only with an unknown capacity.
- **N-3:** the walk's split is a copy of control's rule. To derive it from control, drive `read_inputs()` + `cycle()` and read the per-battery `set_number` commands (my probe does this). Alternatively, take the allocation from `c.allocation`.
- Info: a 2-minute tail with no block (A, 02:47) is `MIN_KWH` hiding under 0.05 kWh, by design.
- Info: at N≥3 with a Solar first battery on, `pack_weights()`'s mode layers could split differently from `packshare.weights()`. I did not test this.
Probe: `scratch/UX-77/review2/probe/probe.py`. Logs: `scratch/UX-77/review2/logs/`.
admin
merged commit c66aa7f69a into release/1.02026-11-06 11:19:21 +01:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Rig finding F-1.
cheaproll._Chargeshared the peak room by rating; control shares it by room left to full (packshare.weights()). With unequal levels one battery's drawn block ended too early.The roll now uses
packshare.weights()over the simulated levels (re-read each 5-min step, each battery capped at its rating, a full one drops out). No control change.test_ux77.py: two unequal-level walks (rig checks 2 and 3), a by-rating mutant that goes red, and the walks now model control's split by room left (the old by-rating model was the same mistake). Part 2 uses its own level (B at 94 %) since B no longer finishes early in the shared walk.🤖 Generated with Claude Code
https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
Review (round 2): approve and merge
Head reviewed:
6d2b9b5, onrelease/1.047c89a1.Verdict: right. The roll now shares the room the same way control's commands do, and it changes no control code.
1. Share matches control
I drove the real control cycle (
read_inputs()+cycle(), arbiter, allocator withpack_weights, peak guard on P1) with two plants at 5000 W / 15.36 kWh and 4600 W / 20.48 kWh. Control's charge per battery was read from its setpoint commands. Home was read fromhomeview.build()at every quarter + 2 min._shares()matches this. No battery goes above its rating, and a full battery drops out becausechargers()is re-read every 5 minutes.2. Test model
The old walk split by rating. Real control does not, so the old model was wrong and its green on the old walks was false.
The new
split_by_room()gives the same stops as real control: 02:49 / 03:12 on the check 2 walk.It is still a copy of the rule, not derived from control's commands (N-3).
Part 2 at B 94 %: B charges 22:00-00:54 and is at its level at 02:30, so the assertion still tests "charged, now done". At 90 %, real control charges B to 07:00, so the old fixture only held under the wrong model.
3. No control change
cheaproll.py,test_ux77.pyand DECISIONS.cheaprollis imported only byhomeviewanddayview.4. Mutant
I made a purged copy with
_sharesby rating. Both new walks went red, plus the 3 old walks: 5 failed.The head's
test_ux77against the base tree is also red, with 5 failed.5. Suite and CI
6d2b9b5is a real run: 96 files, 0 failed, andtest_ux77passed.Not blocking (follow-ups)
for_today()returns None when the run under way is not rolled, so Home draws the plain hourly run._Chargestop even when there is no roll.read_inputs()+cycle()and read the per-batteryset_numbercommands (my probe does this). Alternatively, take the allocation fromc.allocation.MIN_KWHhiding under 0.05 kWh, by design.pack_weights()'s mode layers could split differently frompackshare.weights(). I did not test this.Probe:
scratch/UX-77/review2/probe/probe.py. Logs:scratch/UX-77/review2/logs/.