Thibaut Forest Claude Fable 5 commited on
Commit ·
97af9ac
1
Parent(s): 34c30a6
Phase 62 postscript: scaling verdict is geography-insensitive (cluster-local and map-faithful travel variants <2% shift, arm ranking unchanged)
Browse files
agent_reports_bgkf/artifacts/travel_cluster.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cluster-realistic travel for the synthetic fleets: a clone building is a
|
| 2 |
+
NEW building in its base's cluster - one nearest-neighbour-scale hop from
|
| 3 |
+
its base and its sibling clones (jittered, capped at local scale for the
|
| 4 |
+
remote site), and its base's real distance (small jitter) to everyone else.
|
| 5 |
+
Rewrites the travel frame inside fleet_k{K}.pkl -> fleetc_k{K}.pkl."""
|
| 6 |
+
import sys, pickle
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import numpy as np, pandas as pd
|
| 9 |
+
|
| 10 |
+
sys.path.insert(0, '/Users/THIF/repos/BatterySwapAI2026-Example')
|
| 11 |
+
from batteryswap_public.utils import load_dataset
|
| 12 |
+
|
| 13 |
+
K = int(sys.argv[1])
|
| 14 |
+
rng = np.random.default_rng(42 + K)
|
| 15 |
+
*_, sc = load_dataset(Path('/Users/THIF/repos/BatterySwapAI2026-Example/dataset/train'))
|
| 16 |
+
tc = sc[0]['travel_costs']
|
| 17 |
+
base_h = tc.set_index(['from', 'to'])['hours']
|
| 18 |
+
off = tc[tc['from'] != tc['to']]
|
| 19 |
+
nn = off.pivot(index='from', columns='to', values='hours').min(axis=1)
|
| 20 |
+
local = nn.clip(upper=2 * nn.median()) # remote-site guard
|
| 21 |
+
|
| 22 |
+
with open(f'/tmp/c5/fleet_k{K}.pkl', 'rb') as f:
|
| 23 |
+
FL = pickle.load(f)
|
| 24 |
+
locs = FL['locations']
|
| 25 |
+
base_of = locs.drop_duplicates('building').set_index('building')['base_building']
|
| 26 |
+
blds = sorted(locs['building'].unique())
|
| 27 |
+
|
| 28 |
+
rows, seen = [], {}
|
| 29 |
+
for i in blds:
|
| 30 |
+
for j in blds:
|
| 31 |
+
if i == j:
|
| 32 |
+
h = 0.0
|
| 33 |
+
else:
|
| 34 |
+
key = tuple(sorted((i, j)))
|
| 35 |
+
if key not in seen:
|
| 36 |
+
bi, bj = base_of[i], base_of[j]
|
| 37 |
+
if bi == bj:
|
| 38 |
+
h = float(local[bi]) * rng.uniform(0.6, 1.4)
|
| 39 |
+
else:
|
| 40 |
+
h = float(base_h.get((bi, bj), off['hours'].median()))
|
| 41 |
+
if i != bi or j != bj: # at least one clone: jitter
|
| 42 |
+
h *= rng.uniform(0.9, 1.1)
|
| 43 |
+
seen[key] = max(h, 0.02)
|
| 44 |
+
h = seen[key]
|
| 45 |
+
rows.append({'from': i, 'to': j, 'hours': h})
|
| 46 |
+
FL['travel'] = pd.DataFrame(rows)
|
| 47 |
+
with open(f'/tmp/c5/fleetc_k{K}.pkl', 'wb') as f:
|
| 48 |
+
pickle.dump(FL, f)
|
| 49 |
+
print(f'k={K}: cluster travel written ({len(blds)} buildings)')
|
agent_reports_bgkf/artifacts/travel_map.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Map-faithful travel for synthetic fleets (per the competition map:
|
| 2 |
+
one dominant metro cluster + small satellite clumps, hours apart).
|
| 3 |
+
|
| 4 |
+
Clusters inferred from the real travel matrix (average-linkage, 1h cut:
|
| 5 |
+
sizes [18,2,1,1,1,1]). Each CLONE building is assigned to a cluster with
|
| 6 |
+
probability proportional to real cluster size; its distances are sampled
|
| 7 |
+
from the empirical intra-cluster hop distribution (satellite clumps: the
|
| 8 |
+
map shows tight multi-dot sites -> local hops ~0.05-0.15h) and the real
|
| 9 |
+
between-cluster block distances. Fleet/telemetry unchanged.
|
| 10 |
+
fleet_k{K}.pkl -> fleetm_k{K}.pkl
|
| 11 |
+
"""
|
| 12 |
+
import sys, pickle
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
import numpy as np, pandas as pd
|
| 15 |
+
from scipy.cluster.hierarchy import linkage, fcluster
|
| 16 |
+
from scipy.spatial.distance import squareform
|
| 17 |
+
|
| 18 |
+
sys.path.insert(0, '/Users/THIF/repos/BatterySwapAI2026-Example')
|
| 19 |
+
from batteryswap_public.utils import load_dataset
|
| 20 |
+
|
| 21 |
+
K = int(sys.argv[1])
|
| 22 |
+
rng = np.random.default_rng(1000 + K)
|
| 23 |
+
*_, sc = load_dataset(Path('/Users/THIF/repos/BatterySwapAI2026-Example/dataset/train'))
|
| 24 |
+
tc = sc[0]['travel_costs']
|
| 25 |
+
piv = tc.pivot(index='from', columns='to', values='hours')
|
| 26 |
+
order = sorted(piv.index)
|
| 27 |
+
piv = piv.reindex(index=order, columns=order)
|
| 28 |
+
D = (piv.values + piv.values.T) / 2
|
| 29 |
+
np.fill_diagonal(D, 0)
|
| 30 |
+
lab = fcluster(linkage(squareform(D, checks=False), method='average'),
|
| 31 |
+
t=1.0, criterion='distance')
|
| 32 |
+
clus = pd.Series(lab, index=order)
|
| 33 |
+
sizes = clus.value_counts()
|
| 34 |
+
|
| 35 |
+
def intra_pool(c):
|
| 36 |
+
m = clus[clus == c].index
|
| 37 |
+
if len(m) < 2:
|
| 38 |
+
return np.array([0.08]) # tight clump scale (map dots)
|
| 39 |
+
ix = [order.index(b) for b in m]
|
| 40 |
+
v = D[np.ix_(ix, ix)][np.triu_indices(len(ix), 1)]
|
| 41 |
+
return v[v > 0] if (v > 0).any() else np.array([0.08])
|
| 42 |
+
|
| 43 |
+
def between_pool(c1, c2):
|
| 44 |
+
m1 = [order.index(b) for b in clus[clus == c1].index]
|
| 45 |
+
m2 = [order.index(b) for b in clus[clus == c2].index]
|
| 46 |
+
return D[np.ix_(m1, m2)].ravel()
|
| 47 |
+
|
| 48 |
+
INTRA = {c: intra_pool(c) for c in sizes.index}
|
| 49 |
+
BETW = {(a, b): between_pool(a, b) for a in sizes.index for b in sizes.index
|
| 50 |
+
if a != b}
|
| 51 |
+
|
| 52 |
+
with open(f'/tmp/c5/fleet_k{K}.pkl', 'rb') as f:
|
| 53 |
+
FL = pickle.load(f)
|
| 54 |
+
locs = FL['locations']
|
| 55 |
+
base_of = locs.drop_duplicates('building').set_index('building')['base_building']
|
| 56 |
+
blds = sorted(locs['building'].unique())
|
| 57 |
+
|
| 58 |
+
# cluster of every synthetic building: originals keep theirs; clones drawn
|
| 59 |
+
# proportional to real cluster sizes (metro gets ~75% of new buildings)
|
| 60 |
+
p = (sizes / sizes.sum()).sort_index()
|
| 61 |
+
assign = {}
|
| 62 |
+
for b in blds:
|
| 63 |
+
if b in clus.index:
|
| 64 |
+
assign[b] = int(clus[b])
|
| 65 |
+
else:
|
| 66 |
+
assign[b] = int(rng.choice(p.index.to_numpy(), p=p.to_numpy()))
|
| 67 |
+
|
| 68 |
+
rows, seen = [], {}
|
| 69 |
+
for i in blds:
|
| 70 |
+
for j in blds:
|
| 71 |
+
if i == j:
|
| 72 |
+
h = 0.0
|
| 73 |
+
else:
|
| 74 |
+
key = tuple(sorted((i, j)))
|
| 75 |
+
if key not in seen:
|
| 76 |
+
ci, cj = assign[i], assign[j]
|
| 77 |
+
both_real = i in clus.index and j in clus.index
|
| 78 |
+
if both_real:
|
| 79 |
+
h = float(D[order.index(i), order.index(j)])
|
| 80 |
+
elif ci == cj:
|
| 81 |
+
h = float(rng.choice(INTRA[ci])) * rng.uniform(0.7, 1.3)
|
| 82 |
+
else:
|
| 83 |
+
h = float(rng.choice(BETW[(ci, cj)])) * rng.uniform(0.9, 1.1)
|
| 84 |
+
seen[key] = max(h, 0.02)
|
| 85 |
+
h = seen[key]
|
| 86 |
+
rows.append({'from': i, 'to': j, 'hours': h})
|
| 87 |
+
FL['travel'] = pd.DataFrame(rows)
|
| 88 |
+
with open(f'/tmp/c5/fleetm_k{K}.pkl', 'wb') as f:
|
| 89 |
+
pickle.dump(FL, f)
|
| 90 |
+
n_new = sum(1 for b in blds if b not in clus.index)
|
| 91 |
+
metro = sizes.idxmax()
|
| 92 |
+
share = sum(1 for b in blds if assign[b] == metro) / len(blds)
|
| 93 |
+
print(f'k={K}: {len(blds)} buildings ({n_new} new), metro share {share:.2f}')
|
agent_reports_bgkf/c3_findings.md
CHANGED
|
@@ -208,3 +208,20 @@ FINDINGS:
|
|
| 208 |
|
| 209 |
Artifacts: /tmp/c5/ (generator synth.py, runner scale_run.py, fleets,
|
| 210 |
per-arm CSVs, /tmp/cv_bgkfcand_ft2fix.csv).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
Artifacts: /tmp/c5/ (generator synth.py, runner scale_run.py, fleets,
|
| 210 |
per-arm CSVs, /tmp/cv_bgkfcand_ft2fix.csv).
|
| 211 |
+
|
| 212 |
+
## Phase 62 postscript: geography robustness (user's map)
|
| 213 |
+
|
| 214 |
+
The real travel matrix clusters exactly like the competition map (average
|
| 215 |
+
linkage, 1h cut): one 18-building metro + one pair + four satellite
|
| 216 |
+
singletons; intra-metro hops median 0.25h, nearest-neighbour hops ~0.08h,
|
| 217 |
+
cross-cluster up to 10h. Two additional synthetic geographies were run at
|
| 218 |
+
k=2/4 (fleets unchanged, travel rebuilt): CLUSTER-LOCAL (clones one
|
| 219 |
+
nearest-neighbour-scale hop from their base) and MAP-FAITHFUL (clones
|
| 220 |
+
assigned to clusters proportional to real cluster sizes - metro share
|
| 221 |
+
.75/.78 - with intra/inter distances sampled from the empirical
|
| 222 |
+
distributions). Per-device costs moved by <2% in every cell and the arm
|
| 223 |
+
ranking never changed (champ ~= ft2 at 2x, mild ft2 inversion at 4x,
|
| 224 |
+
alpha 1.0 best everywhere). Cost is dominated by late/early, not travel:
|
| 225 |
+
the scaling verdict is geography-insensitive. Stopped early per user (two
|
| 226 |
+
k=4 alpha arms unfinished - pattern already unambiguous). Artifacts:
|
| 227 |
+
/tmp/c5/travel_map.py, travel_cluster.py, scalem_*/scalec_* CSVs.
|