Thibaut Forest Claude Opus 5 (1M context) commited on
Commit
3c7b883
·
1 Parent(s): bd56acd

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 CHANGED
@@ -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
EXPERIMENTS.md CHANGED
@@ -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
batteryswap_example/models_cv.py CHANGED
@@ -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
- EMERGENCY_DAY = WINDOW_DAYS + 3 # emergencies run just after the plan ends
 
 
 
 
 
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
- day_batteries = {}
1275
  for g in sorted(day_of, key=lambda g: (day_of[g], g)):
1276
- day_batteries.setdefault(day_of[g], []).extend(groups[g])
 
 
 
 
 
 
 
 
 
 
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
- return assemble(pd.Series(days, index=offs.index), locations, start_time)
 
 
 
 
 
 
 
 
 
 
 
 
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')
batteryswap_example/submit.py CHANGED
@@ -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. Training pairs are restricted to origins whose full 42-day
12
- window CLOSED before the scenario origin, so every label used was observable at
13
- planning time; an assertion enforces it. (This mirrors iterate_scenarios' own
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
- pooled.append(base[np.intersect1d(base.columns, table.columns)])
112
- log(f' pooled with train table: +{len(base)} pairs')
 
 
 
 
 
 
 
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: only pairs whose window closed before T
128
- train = pool[pool['origin'] <= T - pd.Timedelta(days=WINDOW_DAYS)]
129
- assert (train['origin'] + pd.Timedelta(days=WINDOW_DAYS) <= T).all()
 
 
 
 
 
 
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