Phase 21-23: unmasked foreign pooling for held-out splits + Docker PATH fix
Browse files- submit.py: pool the packaged train table UNMASKED on held-out splits
(66,890 closed-window pairs, 82 dead devices from origin 0); the target
split's own fleet stays under the T-42d mask; byte-inert when the target
IS train (all-48 Docker reproduces 1851.0186805555559 exactly).
Holdout-validated (fleet A=packaged, B=target): warm-up 1108.6 vs floor's
1091.0 (tie) vs starved 1478.9; mid/late spots 1088.4 vs 1141.9. The
cold-start floor gate now never opens on held-out splits (by design);
it remains live for local warm-up regimes.
- Dockerfile: pin /app/env/bin onto PATH - newer base-image entrypoints
shadow the competition env with miniconda at runtime, breaking
`python3 script.py` with ModuleNotFoundError.
- models_cv.py: phase-21 comment (evaluator emergency queue truly opens at
end-of-week, day 48; exact pricing tested and rejected) + inert nn_order
visit-ordering machinery (phase 22, rejected: panel tie 1679.4 vs 1679.2).
- EXPERIMENTS.md: phases 21 (emergency-timing pricing rejected), 22
(sd<7 and NN visit order rejected; ops channel closed), 23 (pooling
change adopted with holdout evidence).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Dockerfile +5 -0
- EXPERIMENTS.md +132 -0
- batteryswap_example/models_cv.py +61 -8
- batteryswap_example/submit.py +31 -9
|
@@ -19,5 +19,10 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|
| 19 |
COPY batteryswap_example/ ./batteryswap_example
|
| 20 |
COPY script.py ./
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
# Default to making submissions
|
| 23 |
CMD python3 script.py
|
|
|
|
| 19 |
COPY batteryswap_example/ ./batteryswap_example
|
| 20 |
COPY script.py ./
|
| 21 |
|
| 22 |
+
# The competition env (with the pinned packages) lives at /app/env; newer
|
| 23 |
+
# base-image entrypoints put miniconda first on PATH at runtime, which shadows
|
| 24 |
+
# it and breaks `python3 script.py` with ModuleNotFoundError. Pin it back.
|
| 25 |
+
ENV PATH=/app/env/bin:${PATH}
|
| 26 |
+
|
| 27 |
# Default to making submissions
|
| 28 |
CMD python3 script.py
|
|
@@ -715,6 +715,138 @@ Champion: logbag / tsroute a0.8 + top-up (m0.8/pm25) + sd7 + censor-aware
|
|
| 715 |
pricing + gated cold-start floor (k=2.5, gate 20). Panel 1679.2 (unchanged),
|
| 716 |
warm-up 2380, near-end 604.8, all-48 Docker 1851.0, public 1717.8.
|
| 717 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
## Test-look ledger
|
| 719 |
|
| 720 |
8 looks total (now historical - see protocol change above); look 9 = the
|
|
|
|
| 715 |
pricing + gated cold-start floor (k=2.5, gate 20). Panel 1679.2 (unchanged),
|
| 716 |
warm-up 2380, near-end 604.8, all-48 Docker 1851.0, public 1717.8.
|
| 717 |
|
| 718 |
+
## Phase 21 - forced-emergency timing: exact pricing tested, rejected
|
| 719 |
+
|
| 720 |
+
Evaluator fact (established by reading evaluate_plan's emergency loop): every
|
| 721 |
+
recorded in-window EOL the plan never swaps gets a FORCED emergency visit -
|
| 722 |
+
late penalty + full unoptimized ops - and the queue does not start at day 42.
|
| 723 |
+
It starts at the **end of the calendar week containing end_time**
|
| 724 |
+
(`end_day = end_time + (6 - weekday)`), then processes one battery per day in
|
| 725 |
+
alphabetical order. All 48 train scenarios start on Mondays, so the first
|
| 726 |
+
emergency is always **day 48**, and the k-th miss lands at day 48+(k-1). Our
|
| 727 |
+
cost model prices parking against `EMERGENCY_DAY = 45` - a systematic ~30
|
| 728 |
+
underprice per expected death, plus an unmodeled queue-position term.
|
| 729 |
+
|
| 730 |
+
This supersedes the phase-10 disclosure that `emergency_day` is "structurally
|
| 731 |
+
inert": the phase-11 top-up and optimize_days' add/drop passes both consult
|
| 732 |
+
`park`, so the knob is live now.
|
| 733 |
+
|
| 734 |
+
| candidate | verdict | evidence |
|
| 735 |
+
|---|---|---|
|
| 736 |
+
| emergency_day=48 (exact) | rejected | panel **1682.8 vs 1679.2** (+3.6); 31/34 plans byte-identical - the repricing only flips decisions at near-data-end origins (s_39 -74, s_40 +67, s_41 +130); near-end block s_42-47 **631.9 vs 604.8** (+27), 0 wins / 4 losses / 2 ties |
|
| 737 |
+
| emergency_day=50 / 52 (exact + queue pad) | rejected | +9.4 / +13.1, monotonically worse |
|
| 738 |
+
|
| 739 |
+
Why the exact price loses: the deaths we park are mostly voltage-silent
|
| 740 |
+
(p42 ~ 0.05), so +30 x p42 adds ~1.5 to their park price - never enough to
|
| 741 |
+
include them. What the pricier park DOES flip are marginal p42 ~ 0.3-0.5
|
| 742 |
+
devices that mostly survive: late improves -3.5 while early pays +6.6
|
| 743 |
+
(panel decomposition). The champion's underpriced park is a de-facto
|
| 744 |
+
regularizer against over-inclusion where FP prices collapse. Same conclusion
|
| 745 |
+
as the phase-14 park_mobility rejection from the other side: emergency-cost
|
| 746 |
+
underestimation is not the binding error - p42 on silent deaths is.
|
| 747 |
+
|
| 748 |
+
## Phase 22 - optimizer-constraint challenges: sd<7 and NN visit order (both rejected)
|
| 749 |
+
|
| 750 |
+
The evaluator's "constraints" are soft prices (weekly limit flat 100/blown
|
| 751 |
+
week, daily flat 100, overtime 2x/h past 8h; ONE worker hard-wired in the
|
| 752 |
+
state machine - no worker field exists in the plan schema) and mobility_cost
|
| 753 |
+
replicates its cost machine exactly, so the ~152/origin the champion pays in
|
| 754 |
+
penalties is a purchased trade (phase 18 already adversarially confirmed).
|
| 755 |
+
The two remaining SELF-imposed constraints were challenged:
|
| 756 |
+
|
| 757 |
+
| candidate | verdict | evidence |
|
| 758 |
+
|---|---|---|
|
| 759 |
+
| split_days=3 / 1 (finer group granularity) | rejected | panel **1685.0 / 1691.1 vs 1679.2**; ops rises 216.6 -> 223.7 -> 228.2 - finer groups fragment the local search's argmin start and it converges to worse optima with more travel; more freedom != better local search |
|
| 760 |
+
| nn=True (visit a day's buildings in NN route order from base instead of name order, priced in the sim AND emitted identically) | rejected | panel **1679.4 vs 1679.2** - tie; 8 wins / 13 losses / 13 ties; per-origin moves up to +-40 are search-trajectory perturbations, not route savings (with a symmetric travel matrix 2-building days are order-invariant; 3+ building days are rare and route deltas are <1 each) |
|
| 761 |
+
|
| 762 |
+
Machinery kept inert: optimize_days.nn_order (default False, cfg key 'nn'),
|
| 763 |
+
assemble(sort_key=). Ops pot accounting: 217/origin total = 152 penalties
|
| 764 |
+
(correctly purchased) + 54 travel/changes + 11 swap/room time; the recoverable
|
| 765 |
+
slice was bounded by ~5-10/origin and the tests found ~0. The ops channel is
|
| 766 |
+
CLOSED - remaining cost concentrates where it always was: late_swap (p42 on
|
| 767 |
+
silent deaths).
|
| 768 |
+
|
| 769 |
+
Provenance note: the first nn panel run was invalidated by a parallel-session
|
| 770 |
+
file race (a peer harness's frozen-file guard checked models_cv.py out to HEAD
|
| 771 |
+
mid-experiment; nn configs silently ran as no-ops - detected because knob
|
| 772 |
+
configs came back byte-identical to baseline). Long background runs now print
|
| 773 |
+
the imported module's md5 and assert the knob under test exists.
|
| 774 |
+
|
| 775 |
+
## Phase 23 - worker-limit saturation campaign (all rejected; non-adaptation is optimal)
|
| 776 |
+
|
| 777 |
+
The one regime local CV never samples: held-out fleets are ~4.3x train
|
| 778 |
+
(phase 16), where fixed worker limits bind and the public leader pays
|
| 779 |
+
~108/scenario in daily-limit penalties. Harness (`research/run_sat.py`,
|
| 780 |
+
frozen): divide each scenario's worker limits (weekly/daily/overtime_start)
|
| 781 |
+
by 4 on real panel origins - planner and evaluator share the transformed
|
| 782 |
+
settings (the phase-11 property). Champion baseline on the block: **2115.1**
|
| 783 |
+
(vs 1679.2 native) = late 1022 / early 446 / OT 112 / daily 262 / weekly 212 /
|
| 784 |
+
travel 41 - so ~585/scenario of limit-driven penalties, with late/early
|
| 785 |
+
unchanged from native: the champion does not adapt its plan to congestion.
|
| 786 |
+
|
| 787 |
+
Autoresearch protocol (mutable surface = opt-knob JSON only): s0 = 6-origin
|
| 788 |
+
saturated screen; s1 = all 34 saturated origins + native no-regression leg.
|
| 789 |
+
10 s0 experiments + 1 s1, all rejected:
|
| 790 |
+
|
| 791 |
+
| knob family | best variant | s0 delta | verdict |
|
| 792 |
+
|---|---|---|---|
|
| 793 |
+
| constructive start (phase 18) | on | +16.2 | worse basin under saturation too |
|
| 794 |
+
| search budget | r6/i120 | -13.4 | one -80 escape in six origins; 5 ties |
|
| 795 |
+
| split_days | 12 / 4 | -4.1 / +19.4 | both directions wash; limits untouched |
|
| 796 |
+
| day_q | 0.2 | 0.0 | identical plans - dead knob under saturation |
|
| 797 |
+
| top-up gate | pm12 | +116.3 | starving the top-up raises late AND limits |
|
| 798 |
+
| alpha | 0.75 / 0.7 | **-59.8** (4/6) / -10.5 | promoted at 0.75+sd12 combo -> s1 |
|
| 799 |
+
|
| 800 |
+
s1 (alpha 0.75 + sd12): sat **+20.0** (18/34 wins but 4 blow-ups sum +1835:
|
| 801 |
+
s_17 +628, s_8 +459, s_31 +391, s_23 +357 vs 18 wins summing -1317), native
|
| 802 |
+
**+42.0**. Decomposition: late +43.5 / early -27.0 / daily +5.9 / weekly -2.9 -
|
| 803 |
+
inclusion cuts buy ZERO congestion relief; they are a mortality bet with
|
| 804 |
+
fat-tailed downside, made worse by saturation (emergencies consume the same
|
| 805 |
+
scarce worker-hours). Screen lesson: 6-origin s0 is high-variance for
|
| 806 |
+
inclusion knobs (-59.8 flipped to +20.0 at s1); the s1 mean gate caught it.
|
| 807 |
+
|
| 808 |
+
**Conclusion**: the ~585/scenario limit spend is the shadow price of
|
| 809 |
+
scheduling MASS, not of bad choices - the day-assignment search is converged
|
| 810 |
+
(extra search, day quantiles, both sd directions, constructive start: all
|
| 811 |
+
flat or worse), and paying the flat fines beats the timing cost of leaving
|
| 812 |
+
peak mortality weeks. The champion's non-adaptation to congestion is optimal
|
| 813 |
+
within its plan space; the leader's limit penalties on the LB are likewise a
|
| 814 |
+
purchased trade, not an exploitable gap. The saturated-regime channel is
|
| 815 |
+
CLOSED; ec_margin was left untested as the same condemned inclusion family.
|
| 816 |
+
|
| 817 |
+
## Phase 23 - unmasked foreign pooling in deployment (ADOPTED; user change, holdout-validated)
|
| 818 |
+
|
| 819 |
+
User change to submit.py: the packaged train table is pooled UNMASKED on
|
| 820 |
+
held-out splits (all 66,890 closed-window pairs, 82 dead devices, explicit
|
| 821 |
+
window_closed filter), while the target split's own pairs - and any train-table
|
| 822 |
+
rows for devices in the target fleet - stay under the T-42d mask. The old
|
| 823 |
+
uniform mask was CV hygiene misapplied to deployment: it starved a held-out
|
| 824 |
+
split's early origins to 3-20 unique training deaths (the cold-start
|
| 825 |
+
mechanism). Byte-inert when the target split IS train (own-fleet guard), so
|
| 826 |
+
panel / warm-up / all-48 are unchanged by construction.
|
| 827 |
+
|
| 828 |
+
Validation (pooling-holdout harness /tmp/pool_holdout.py: fleet A = packaged
|
| 829 |
+
table, fleet B = target, B-only scenarios scored):
|
| 830 |
+
|
| 831 |
+
| regime | warm-up s_0-7 | spots s_16/24/32/40 |
|
| 832 |
+
|---|---|---|
|
| 833 |
+
| old masked pool, no floor | 1478.9 (late 1274) | 1141.9 |
|
| 834 |
+
| old + gated k=2.5 floor (shipped before) | **1091.0** | 1141.9 (gate closed) |
|
| 835 |
+
| **new unmasked pool (ships now)** | 1108.6 | **1088.4** |
|
| 836 |
+
|
| 837 |
+
- Count calibration transfers: held-out s_0 sum(p42) 0.5 -> 3.4 vs 4 true
|
| 838 |
+
deaths. The starvation the phase-19 floor patched is gone at its source.
|
| 839 |
+
- Warm-up vs the floor: statistical tie (5W/3L; s_4 -827 for the floor - a
|
| 840 |
+
local prevalence burst that own-fleet recent-death signals catch and
|
| 841 |
+
foreign calibration lags). Profiles differ: floor buys insurance early
|
| 842 |
+
(480 early), new stays lean (177 early / 786 late).
|
| 843 |
+
- The undersold gain is MID/LATE held-out origins: -53.5 (3W/1L, s_16 -438) -
|
| 844 |
+
the unmasked pool improves every held-out origin's training set, and a real
|
| 845 |
+
split is mostly mid-history origins.
|
| 846 |
+
- Deployment consequence: on held-out splits the floor gate NEVER opens now
|
| 847 |
+
(uniq >= 82 from origin 0); the floor remains live only for genuinely
|
| 848 |
+
history-poor local regimes (train warm-up block, all-48 metric).
|
| 849 |
+
|
| 850 |
## Test-look ledger
|
| 851 |
|
| 852 |
8 looks total (now historical - see protocol change above); look 9 = the
|
|
@@ -996,7 +996,7 @@ def resolve_model(name):
|
|
| 996 |
|
| 997 |
# ------------------------------------------------------------------------- planners
|
| 998 |
|
| 999 |
-
def assemble(selected_days, locations, start_time):
|
| 1000 |
parked_day = start_time + pd.Timedelta(days=WINDOW_DAYS + 1)
|
| 1001 |
scheduled = set(selected_days.index)
|
| 1002 |
parked = [b for b in locations['battery'] if b not in scheduled]
|
|
@@ -1005,10 +1005,15 @@ def assemble(selected_days, locations, start_time):
|
|
| 1005 |
pd.DataFrame({'day': parked_day, 'battery': parked}),
|
| 1006 |
], ignore_index=True)
|
| 1007 |
# within a day the evaluator visits in row order: group by building then room,
|
| 1008 |
-
# otherwise interleaved buildings pay repeated building-changes and travel
|
|
|
|
|
|
|
|
|
|
| 1009 |
loc = locations.set_index('battery')
|
| 1010 |
plan['building'] = plan['battery'].map(loc['building'])
|
| 1011 |
plan['room'] = plan['battery'].map(loc['room'])
|
|
|
|
|
|
|
| 1012 |
plan = (plan.sort_values(['day', 'building', 'room'], kind='stable')
|
| 1013 |
.drop(columns=['building', 'room']).reset_index(drop=True))
|
| 1014 |
check_plan_valid(plan, locations, start_time=start_time)
|
|
@@ -1066,7 +1071,12 @@ def _batch_days(offs, locations, start_time, max_groups_per_day=2):
|
|
| 1066 |
|
| 1067 |
|
| 1068 |
EARLY, LATE = 0.5, 10.0
|
| 1069 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1070 |
CENSOR_AWARE = True # price unobservable (post-data-end) deaths via imputation
|
| 1071 |
PARK_MOBILITY = 0.0 # extra cost per expected emergency (travel/overtime)
|
| 1072 |
|
|
@@ -1258,6 +1268,19 @@ def mobility_cost(day_batteries, locations, travel_hours, settings, start_time):
|
|
| 1258 |
|
| 1259 |
|
| 1260 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1261 |
def optimize_days(groups, curves, locations, travel_hours, settings, start_time,
|
| 1262 |
restarts=None, iters=None):
|
| 1263 |
restarts = restarts if restarts is not None else optimize_days.restarts
|
|
@@ -1267,13 +1290,26 @@ def optimize_days(groups, curves, locations, travel_hours, settings, start_time,
|
|
| 1267 |
group onto another group's day), and deterministic random restarts.
|
| 1268 |
Objective = expected timing cost (model curves) + exact mobility cost.
|
| 1269 |
"""
|
|
|
|
|
|
|
|
|
|
| 1270 |
def timing(day_of):
|
| 1271 |
return sum(curves.at[b, day_of[g]] for g, bs in groups.items() for b in bs)
|
| 1272 |
|
| 1273 |
def mobility(day_of):
|
| 1274 |
-
|
| 1275 |
for g in sorted(day_of, key=lambda g: (day_of[g], g)):
|
| 1276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1277 |
return mobility_cost(day_batteries, locations, travel_hours,
|
| 1278 |
settings, start_time)
|
| 1279 |
|
|
@@ -1332,6 +1368,7 @@ def optimize_days(groups, curves, locations, travel_hours, settings, start_time,
|
|
| 1332 |
optimize_days.restarts = 2
|
| 1333 |
optimize_days.iters = 60
|
| 1334 |
optimize_days.constructive = False
|
|
|
|
| 1335 |
|
| 1336 |
|
| 1337 |
def plan_ecroute(pred, included, locations, start_time, day_q,
|
|
@@ -1448,7 +1485,19 @@ def plan_tsroute(pred, included, locations, start_time, day_q,
|
|
| 1448 |
|
| 1449 |
offs = key.map(day_of).astype(float)
|
| 1450 |
days = start_time + pd.to_timedelta(offs.values, unit='D')
|
| 1451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1452 |
|
| 1453 |
|
| 1454 |
plan_tsroute.alpha = 0.8
|
|
@@ -1739,6 +1788,7 @@ def walk_forward(table, scenario_ctx, origins, configs):
|
|
| 1739 |
optimize_days.restarts = int(cfg.get('restarts', 2))
|
| 1740 |
optimize_days.iters = int(cfg.get('iters', 60))
|
| 1741 |
optimize_days.constructive = bool(cfg.get('constructive', False))
|
|
|
|
| 1742 |
included = pred.index[pred['p42'] >= cfg['p_star']]
|
| 1743 |
kwargs = {}
|
| 1744 |
if cfg['planner'] in ('ecroute', 'tsroute', 'tspig', 'tspig2', 'tscut'):
|
|
@@ -1777,7 +1827,8 @@ def summarize(df, label):
|
|
| 1777 |
('alpha2', 'a2'), ('emergency_day', 'e'), ('restarts', 'r'),
|
| 1778 |
('iters', 'i'), ('ec_margin', 'ec'), ('ec_pmax', 'pm'),
|
| 1779 |
('split_days', 'sd'), ('park_mobility', 'pk'), ('floor', 'fl'),
|
| 1780 |
-
('floor_gate', 'fg'), ('floor_zone', 'fz')
|
|
|
|
| 1781 |
if key in df.columns and df[key].nunique(dropna=False) > 1:
|
| 1782 |
df['name'] += '/' + tag + df[key].fillna(0).astype(str)
|
| 1783 |
mean = df.groupby('name')['total_cost'].agg(['mean', 'std']).sort_values('mean')
|
|
@@ -1820,7 +1871,9 @@ def selection_stability(df, n_random=2000, n_blocks=6, seed=0):
|
|
| 1820 |
for key, tag in [('blend', 'b'), ('p_be', 'be'), ('floor_day', 'f'), ('p_conf', 'c'),
|
| 1821 |
('alpha2', 'a2'), ('emergency_day', 'e'), ('restarts', 'r'),
|
| 1822 |
('iters', 'i'), ('ec_margin', 'ec'), ('ec_pmax', 'pm'),
|
| 1823 |
-
('split_days', 'sd'), ('park_mobility', 'pk'), ('floor', 'fl')
|
|
|
|
|
|
|
| 1824 |
if key in df.columns and df[key].nunique(dropna=False) > 1:
|
| 1825 |
df['name'] += '/' + tag + df[key].fillna(0).astype(str)
|
| 1826 |
pv = df.pivot_table(index='origin', columns='name', values='total_cost')
|
|
|
|
| 996 |
|
| 997 |
# ------------------------------------------------------------------------- planners
|
| 998 |
|
| 999 |
+
def assemble(selected_days, locations, start_time, sort_key=None):
|
| 1000 |
parked_day = start_time + pd.Timedelta(days=WINDOW_DAYS + 1)
|
| 1001 |
scheduled = set(selected_days.index)
|
| 1002 |
parked = [b for b in locations['battery'] if b not in scheduled]
|
|
|
|
| 1005 |
pd.DataFrame({'day': parked_day, 'battery': parked}),
|
| 1006 |
], ignore_index=True)
|
| 1007 |
# within a day the evaluator visits in row order: group by building then room,
|
| 1008 |
+
# otherwise interleaved buildings pay repeated building-changes and travel.
|
| 1009 |
+
# sort_key overrides the building order (per-battery rank, e.g. NN route);
|
| 1010 |
+
# parked rows fall past end_time and are cut by the evaluator, so their
|
| 1011 |
+
# order is irrelevant.
|
| 1012 |
loc = locations.set_index('battery')
|
| 1013 |
plan['building'] = plan['battery'].map(loc['building'])
|
| 1014 |
plan['room'] = plan['battery'].map(loc['room'])
|
| 1015 |
+
if sort_key is not None:
|
| 1016 |
+
plan['building'] = plan['battery'].map(sort_key).fillna(10 ** 9)
|
| 1017 |
plan = (plan.sort_values(['day', 'building', 'room'], kind='stable')
|
| 1018 |
.drop(columns=['building', 'room']).reset_index(drop=True))
|
| 1019 |
check_plan_valid(plan, locations, start_time=start_time)
|
|
|
|
| 1071 |
|
| 1072 |
|
| 1073 |
EARLY, LATE = 0.5, 10.0
|
| 1074 |
+
# The evaluator's true emergency queue opens at the END OF THE WEEK containing
|
| 1075 |
+
# end_time (day 48 for the all-Monday train scenarios), +1 day per queued miss.
|
| 1076 |
+
# Exact pricing (48/50/52) was tested and REJECTED (phase 21): it only flips
|
| 1077 |
+
# decisions at near-data-end origins and loses there - the underprice acts as
|
| 1078 |
+
# a regularizer against over-including marginal survivors. Keep 45.
|
| 1079 |
+
EMERGENCY_DAY = WINDOW_DAYS + 3
|
| 1080 |
CENSOR_AWARE = True # price unobservable (post-data-end) deaths via imputation
|
| 1081 |
PARK_MOBILITY = 0.0 # extra cost per expected emergency (travel/overtime)
|
| 1082 |
|
|
|
|
| 1268 |
|
| 1269 |
|
| 1270 |
|
| 1271 |
+
def _nn_building_order(bldgs, travel_hours, base):
|
| 1272 |
+
"""Nearest-neighbor route over a day's buildings starting from base:
|
| 1273 |
+
{building: visit_rank}. Deterministic (ties broken by name)."""
|
| 1274 |
+
left = sorted(set(bldgs))
|
| 1275 |
+
order, cur = {}, base
|
| 1276 |
+
for i in range(len(left)):
|
| 1277 |
+
nxt = min(left, key=lambda b: (float(travel_hours.at[cur, b]), b))
|
| 1278 |
+
order[nxt] = i
|
| 1279 |
+
left.remove(nxt)
|
| 1280 |
+
cur = nxt
|
| 1281 |
+
return order
|
| 1282 |
+
|
| 1283 |
+
|
| 1284 |
def optimize_days(groups, curves, locations, travel_hours, settings, start_time,
|
| 1285 |
restarts=None, iters=None):
|
| 1286 |
restarts = restarts if restarts is not None else optimize_days.restarts
|
|
|
|
| 1290 |
group onto another group's day), and deterministic random restarts.
|
| 1291 |
Objective = expected timing cost (model curves) + exact mobility cost.
|
| 1292 |
"""
|
| 1293 |
+
loc_b = locations.set_index('battery')['building']
|
| 1294 |
+
gbldg = {g: loc_b.at[bs[0]] for g, bs in groups.items()}
|
| 1295 |
+
|
| 1296 |
def timing(day_of):
|
| 1297 |
return sum(curves.at[b, day_of[g]] for g, bs in groups.items() for b in bs)
|
| 1298 |
|
| 1299 |
def mobility(day_of):
|
| 1300 |
+
day_groups = {}
|
| 1301 |
for g in sorted(day_of, key=lambda g: (day_of[g], g)):
|
| 1302 |
+
day_groups.setdefault(day_of[g], []).append(g)
|
| 1303 |
+
if optimize_days.nn_order:
|
| 1304 |
+
# visit a day's buildings in nearest-neighbor route order instead
|
| 1305 |
+
# of name order (matters only on 3+ building days: with a
|
| 1306 |
+
# symmetric travel matrix a 2-building day is order-invariant)
|
| 1307 |
+
for gs in day_groups.values():
|
| 1308 |
+
rank = _nn_building_order([gbldg[g] for g in gs], travel_hours,
|
| 1309 |
+
settings.base_location)
|
| 1310 |
+
gs.sort(key=lambda g: (rank[gbldg[g]], g))
|
| 1311 |
+
day_batteries = {d: [b for g in gs for b in groups[g]]
|
| 1312 |
+
for d, gs in day_groups.items()}
|
| 1313 |
return mobility_cost(day_batteries, locations, travel_hours,
|
| 1314 |
settings, start_time)
|
| 1315 |
|
|
|
|
| 1368 |
optimize_days.restarts = 2
|
| 1369 |
optimize_days.iters = 60
|
| 1370 |
optimize_days.constructive = False
|
| 1371 |
+
optimize_days.nn_order = False # visit a day's buildings in NN route order
|
| 1372 |
|
| 1373 |
|
| 1374 |
def plan_ecroute(pred, included, locations, start_time, day_q,
|
|
|
|
| 1485 |
|
| 1486 |
offs = key.map(day_of).astype(float)
|
| 1487 |
days = start_time + pd.to_timedelta(offs.values, unit='D')
|
| 1488 |
+
sort_key = None
|
| 1489 |
+
if optimize_days.nn_order:
|
| 1490 |
+
# emit rows in the same NN building order the mobility sim priced
|
| 1491 |
+
base_b = settings.base_location
|
| 1492 |
+
gb = {g: bldg.loc[bs[0]] for g, bs in groups.items()}
|
| 1493 |
+
ranks = {}
|
| 1494 |
+
for d in set(day_of.values()):
|
| 1495 |
+
gs = [g for g in day_of if day_of[g] == d]
|
| 1496 |
+
ranks[d] = _nn_building_order([gb[g] for g in gs], travel_hours, base_b)
|
| 1497 |
+
sort_key = pd.Series({b: ranks[day_of[g]][gb[g]]
|
| 1498 |
+
for g, bs in groups.items() for b in bs})
|
| 1499 |
+
return assemble(pd.Series(days, index=offs.index), locations, start_time,
|
| 1500 |
+
sort_key=sort_key)
|
| 1501 |
|
| 1502 |
|
| 1503 |
plan_tsroute.alpha = 0.8
|
|
|
|
| 1788 |
optimize_days.restarts = int(cfg.get('restarts', 2))
|
| 1789 |
optimize_days.iters = int(cfg.get('iters', 60))
|
| 1790 |
optimize_days.constructive = bool(cfg.get('constructive', False))
|
| 1791 |
+
optimize_days.nn_order = bool(cfg.get('nn', False))
|
| 1792 |
included = pred.index[pred['p42'] >= cfg['p_star']]
|
| 1793 |
kwargs = {}
|
| 1794 |
if cfg['planner'] in ('ecroute', 'tsroute', 'tspig', 'tspig2', 'tscut'):
|
|
|
|
| 1827 |
('alpha2', 'a2'), ('emergency_day', 'e'), ('restarts', 'r'),
|
| 1828 |
('iters', 'i'), ('ec_margin', 'ec'), ('ec_pmax', 'pm'),
|
| 1829 |
('split_days', 'sd'), ('park_mobility', 'pk'), ('floor', 'fl'),
|
| 1830 |
+
('floor_gate', 'fg'), ('floor_zone', 'fz'), ('constructive', 'cn'),
|
| 1831 |
+
('nn', 'nn')]:
|
| 1832 |
if key in df.columns and df[key].nunique(dropna=False) > 1:
|
| 1833 |
df['name'] += '/' + tag + df[key].fillna(0).astype(str)
|
| 1834 |
mean = df.groupby('name')['total_cost'].agg(['mean', 'std']).sort_values('mean')
|
|
|
|
| 1871 |
for key, tag in [('blend', 'b'), ('p_be', 'be'), ('floor_day', 'f'), ('p_conf', 'c'),
|
| 1872 |
('alpha2', 'a2'), ('emergency_day', 'e'), ('restarts', 'r'),
|
| 1873 |
('iters', 'i'), ('ec_margin', 'ec'), ('ec_pmax', 'pm'),
|
| 1874 |
+
('split_days', 'sd'), ('park_mobility', 'pk'), ('floor', 'fl'),
|
| 1875 |
+
('floor_gate', 'fg'), ('floor_zone', 'fz'), ('constructive', 'cn'),
|
| 1876 |
+
('nn', 'nn')]:
|
| 1877 |
if key in df.columns and df[key].nunique(dropna=False) > 1:
|
| 1878 |
df['name'] += '/' + tag + df[key].fillna(0).astype(str)
|
| 1879 |
pv = df.pivot_table(index='origin', columns='name', values='total_cost')
|
|
@@ -8,10 +8,14 @@ static train-split model cannot avoid.
|
|
| 8 |
|
| 9 |
Label integrity: the runtime dataset ships each split's eol_times.csv, which
|
| 10 |
contains ground truth beyond the scenario origins. This module NEVER uses it
|
| 11 |
-
directly for planning.
|
| 12 |
-
window CLOSED before the scenario origin, so every label used
|
| 13 |
-
planning time; an assertion enforces it. (This mirrors
|
| 14 |
-
legitimate use of eol_times to drop already-dead devices.)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
Falls back to the static pickled planner per split if anything goes wrong.
|
| 17 |
"""
|
|
@@ -105,12 +109,24 @@ def adaptive_split_plans(split, split_path, log=print, deadline=None):
|
|
| 105 |
table, daily, dall, betas = build_split_table(
|
| 106 |
locations, timeseries, eol_times, scen_starts, log=log)
|
| 107 |
|
|
|
|
| 108 |
pooled = [table]
|
| 109 |
if TRAIN_TABLE.exists():
|
| 110 |
base = pd.read_parquet(TRAIN_TABLE)
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
pool = pd.concat(pooled, ignore_index=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
plans = []
|
| 116 |
fitted = {}
|
|
@@ -124,9 +140,15 @@ def adaptive_split_plans(split, split_path, log=print, deadline=None):
|
|
| 124 |
raise TimeoutError('split deadline exceeded mid-adaptive')
|
| 125 |
|
| 126 |
if T not in fitted:
|
| 127 |
-
# LABEL INTEGRITY:
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
pos_dev = train.loc[train['dies42'] == 1, 'device_id']
|
| 131 |
uniq = int(pos_dev.nunique())
|
| 132 |
# foreign-fleet fraction: share of training positives that belong
|
|
|
|
| 8 |
|
| 9 |
Label integrity: the runtime dataset ships each split's eol_times.csv, which
|
| 10 |
contains ground truth beyond the scenario origins. This module NEVER uses it
|
| 11 |
+
directly for planning. The split's OWN training pairs are restricted to origins
|
| 12 |
+
whose full 42-day window CLOSED before the scenario origin, so every label used
|
| 13 |
+
was observable at planning time; an assertion enforces it. (This mirrors
|
| 14 |
+
iterate_scenarios' own legitimate use of eol_times to drop already-dead devices.)
|
| 15 |
+
Pairs from the shipped TRAIN table are a different fleet whose labels were all
|
| 16 |
+
known when the submission was built - those are pooled UNMASKED (closed windows
|
| 17 |
+
only), except for devices that also belong to the target split's fleet, which
|
| 18 |
+
stay under the time mask so a train-split run cannot read its own future.
|
| 19 |
|
| 20 |
Falls back to the static pickled planner per split if anything goes wrong.
|
| 21 |
"""
|
|
|
|
| 109 |
table, daily, dall, betas = build_split_table(
|
| 110 |
locations, timeseries, eol_times, scen_starts, log=log)
|
| 111 |
|
| 112 |
+
table['foreign'] = False
|
| 113 |
pooled = [table]
|
| 114 |
if TRAIN_TABLE.exists():
|
| 115 |
base = pd.read_parquet(TRAIN_TABLE)
|
| 116 |
+
# open-window rows (origins within 42d of the train data end) carry
|
| 117 |
+
# unverifiable "survives" labels; the old T-mask excluded them only by
|
| 118 |
+
# coincidence, so filter explicitly - and BEFORE the column intersect,
|
| 119 |
+
# which drops window_closed (build_split_table does not produce it)
|
| 120 |
+
base = base[base['window_closed']]
|
| 121 |
+
base = base[np.intersect1d(base.columns, table.columns)].copy()
|
| 122 |
+
base['foreign'] = True
|
| 123 |
+
pooled.append(base)
|
| 124 |
+
log(f' pooled with train table: +{len(base)} closed-window pairs')
|
| 125 |
pool = pd.concat(pooled, ignore_index=True)
|
| 126 |
+
# a train-table row for a device in THIS split's fleet is not foreign -
|
| 127 |
+
# same fleet means same answer key - so it stays under the time mask.
|
| 128 |
+
# Makes the unmasked pooling inert when the target split IS train.
|
| 129 |
+
pool['foreign'] &= ~pool['device_id'].isin(set(locations['battery']))
|
| 130 |
|
| 131 |
plans = []
|
| 132 |
fitted = {}
|
|
|
|
| 140 |
raise TimeoutError('split deadline exceeded mid-adaptive')
|
| 141 |
|
| 142 |
if T not in fitted:
|
| 143 |
+
# LABEL INTEGRITY: own pairs only where the window closed before T
|
| 144 |
+
# (the split's eol_times is its answer key). Foreign train-fleet
|
| 145 |
+
# pairs are used in full: masking them was CV hygiene misapplied
|
| 146 |
+
# to deployment, starving early origins to 3-20 unique training
|
| 147 |
+
# deaths out of the 82 available (the cold-start mechanism).
|
| 148 |
+
closed = pool['origin'] <= T - pd.Timedelta(days=WINDOW_DAYS)
|
| 149 |
+
train = pool[closed | pool['foreign']]
|
| 150 |
+
own_pairs = train[~train['foreign']]
|
| 151 |
+
assert (own_pairs['origin'] + pd.Timedelta(days=WINDOW_DAYS) <= T).all()
|
| 152 |
pos_dev = train.loc[train['dies42'] == 1, 'device_id']
|
| 153 |
uniq = int(pos_dev.nunique())
|
| 154 |
# foreign-fleet fraction: share of training positives that belong
|