Skip to content

Commit 036b6b8

Browse files
Merge branch 'enrich/candidate-pass-1'
2 parents 754d68d + 5457eeb commit 036b6b8

14 files changed

Lines changed: 7121 additions & 50 deletions

CLAUDE.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ consumes megawatts — and megawatts leave a public paper trail.
4545

4646
`data/sites_seed.csv` columns: site_id, site_name, tier, operator_or_venue,
4747
firms_linked, lat, lon, coord_precision, city, country, capacity_note,
48-
evidence_type, evidence_note, confidence, verify_url_hint (to be replaced by
49-
`evidence_url` with real URLs).
48+
power_mw, status, evidence_type, evidence_note, evidence_url, confidence,
49+
verify_url_hint (legacy; being replaced by `evidence_url` with real URLs).
5050

51-
Planned second file: `data/paths.csv` for Tier-2 arc pairs
52-
(origin_site_id, dest_site_id, operator, medium, evidence_url) — feeds a
53-
kepler arc layer.
51+
Second file: `data/paths.csv` for Tier-2 arc pairs (path_id, origin_site_id,
52+
dest_site_id, operator, medium, status, confidence, evidence_note,
53+
evidence_url) — feeds the kepler arc layer in build_map.py.
5454

5555
## Evidence pipeline (strength order)
5656

ENRICHMENT.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# ENRICHMENT.md — candidate records for verification
2+
3+
Feed this to Claude Code. Workflow per CLAUDE.md: research each candidate,
4+
attach a real evidence_url, geocode the address, then promote into
5+
data/sites_seed.csv with the appropriate confidence grade. Do NOT bulk-add
6+
without sources. Facts below are from working memory — treat all as
7+
candidate-grade until verified.
8+
9+
## Tier 1 — execution (target: global venue coverage)
10+
11+
| candidate | why it matters | evidence trail |
12+
|---|---|---|
13+
| Equinix CH1/CH2, 350 E Cermak, Chicago | Historic heart of Chicago market data/connectivity; pre-Aurora CME home | Equinix docs, DCD archives |
14+
| TMX Markham (Toronto) | Canadian equities matching | TMX colocation docs |
15+
| B3 São Paulo (colo at Equinix SP) | LatAm anchor; proves Tier 1 exists where Tier 3 doesn't | B3 co-location docs |
16+
| BMV Mexico City (KIO) | Second LatAm point | BMV tech docs |
17+
| NSE Mumbai (BKC) | THE colocation-scandal case study (SEBI orders 2019+, tick-data access) | SEBI orders (public), press |
18+
| BSE Mumbai | Completes India | BSE colo docs |
19+
| KRX Busan | Korean derivatives matching; unusual non-capital location | KRX docs |
20+
| SGX (Tier 1 DC, Singapore) | SE Asia hub; also FX ecosystem | SGX co-location docs |
21+
| ASX Gordon (Sydney) | Australia; ALC data centre | ASX docs |
22+
| TWSE Taipei | Adds Taiwan (semis resonance) | TWSE docs |
23+
| JSE Johannesburg colo | Africa's only major colo ecosystem | JSE colocation docs |
24+
| Tel Aviv TASE | Small but documented | TASE docs |
25+
| FX layer: EBS (NY4/LD4/TY3), LSEG Matching (former Refinitiv) | FX matching is tri-site by design — great "follow the sun" visual | vendor docs |
26+
| Moscow Exchange M1 (historical flag only) | Pre-2022 HFT destination; now sanctions case study | archive press |
27+
28+
## Tier 2 — network (target: arcs, not just points)
29+
30+
| candidate | why it matters | evidence trail |
31+
|---|---|---|
32+
| Full Chicago–NJ chain (McKay, New Line, Vigilant relays) | The signature arc set | FCC ULS Part 101 bulk data |
33+
| Anova laser/mm-wave links: Mahwah–Carteret–Secaucus triangle | Metro-scale layer inside NJ | FCC + Anova marketing |
34+
| Swingate/Dover + Dunkirk/Calais crossing sites | Completes Channel narrative with Richborough/Houtem | Ofcom, ANFR Cartoradio, Laumonier |
35+
| London–Frankfurt relay chain (French + Belgian hops) | ANFR open data = French-sourced original work | ANFR bulk data (data.gouv.fr) |
36+
| Tokyo–Osaka microwave corridor | Asia has a Tier 2 too; TSE-OSE arb | academic papers + press |
37+
| Shortwave sites (Elgin/West Chicago area + counterparts) | The exotic frontier; Van Valzah's investigation | FCC experimental licenses |
38+
| Hibernia Express/GTT transatlantic cable (landing points) | The one cable worth mapping: built for latency | press, submarine cable map |
39+
| Quincy Data / DRW Aurora-area head-ends | Completes Aurora story | FCC ULS |
40+
41+
## Tier 3 — research (target: the global compute story)
42+
43+
| candidate | why it matters | evidence trail |
44+
|---|---|---|
45+
| High-Flyer / Fire-Flyer clusters (Hangzhou area) | ~10k A100s pre-export-controls; incubated DeepSeek. The finance-AI merger in one pin | Chinese + Western press, High-Flyer publications |
46+
| Ubiquant + other large Chinese quant GPU clusters | Pattern, not anecdote | press, job ads |
47+
| Jane Street GPU footprint | Major buyer; likely colo — job-ad triangulation | job postings, talks |
48+
| Hudson River Trading research compute | Public engineering blog hints | HRT blog, job ads |
49+
| Two Sigma DC history (NJ colo + cloud) | Long-documented compute culture | press, engineering blog |
50+
| G-Research (UK) compute | Large UK ML fund; recruitment materials discuss cluster | job ads, press |
51+
| Jump Trading GPU/HPC | Also crypto infra angle | press, job ads |
52+
| XTX Kajaani building 2 | Expansion + Finnish electricity-tax review = policy hook | YIT announcement (collected), DCD |
53+
| Verne Global Iceland campus (XTX + others) | The "compute follows power" origin story | Verne marketing, FT |
54+
| Negative-space entries: LatAm=0, Africa=0 documented research compute | The absence IS the finding — consider explicit map annotation | (documented absence) |
55+
56+
## New schema needs
57+
58+
- `paths.csv` for Tier 2 arcs: origin/dest site_ids, operator, medium
59+
(microwave/mm-wave/laser/shortwave/fiber), evidence_url
60+
- Consider `status` column: active / historical / under_construction
61+
(Basildon=historical, Kajaani bldg 2=under_construction, Moscow=historical)
62+
- Consider `power_mw` numeric column for Tier 3 where public

PLAN.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
## Phase 1 — Foundation hygiene
44
- [ ] Geocode-verify all seed coordinates against documented addresses; set
5-
coord_precision=exact where verified
5+
coord_precision=exact where verified (pipeline/geocode.py exists;
6+
T1-004/T2-003 done via enrichment pass 1; original seed rows pending)
67
- [ ] Replace verify_url_hint with real evidence_url for all 19 seed records
7-
- [ ] Add pytest data-validation suite (schema, coordinate bounds, confidence
8-
enum, URL presence for confirmed records) — run in CI
8+
(done for T1-003/T1-005/T1-010/T2-001..004/T3-002 via enrichment pass 1)
9+
- [x] Add pytest data-validation suite (schema, coordinate bounds, confidence
10+
enum, URL presence for post-enrichment records) — CI wiring pending
911

1012
## Phase 2 — Tier 2 extraction (the towers)
1113
- [ ] pipeline/fcc_uls.py: pull FCC ULS Part 101 licenses for known
@@ -14,16 +16,22 @@
1416
- [ ] FCC experimental license pull (shortwave sites)
1517
- [ ] Ofcom fixed-links register extraction (London–Slough–coast paths)
1618
- [ ] ANFR Cartoradio: French relay sites on the London–Frankfurt corridor
17-
- [ ] Add kepler arc layer for paths.csv
19+
(Tour de Reuze pinned from ANFR-derived data; bulk extraction pending;
20+
German-side relays entirely unmapped)
21+
- [x] Add kepler arc layer for paths.csv (39 arcs shipped in enrichment pass 1)
1822

1923
## Phase 3 — Tier 3 expansion (the compute story)
20-
- [ ] XTX: pin Kajaani precisely (Sokajärventie site), add second building,
21-
Iceland (Verne Global) with sources
22-
- [ ] Job-ad triangulation pass: Jane Street, HRT, Two Sigma, DE Shaw, Jump —
23-
infrastructure roles with locations
24+
- [x] XTX: add second building (T3-017, YIT-sourced), Iceland Verne campus
25+
enriched with operator sources (T3-002); precise Kajaani geocode pending
26+
- [ ] Job-ad triangulation pass: Jane Street, HRT, Two Sigma, Jump, G-Research
27+
done in enrichment pass 1; DE Shaw still outstanding
2428
- [ ] Renaissance: Brookhaven planning/permit search; document the null result
2529
if null
26-
- [ ] Power-capacity column normalization (MW where public)
30+
- [x] Power-capacity column normalization (power_mw column; source-stated
31+
values only: B3 10, KIO SF2 1.137, MOEX 9.5, Verne 140, XTX 22.5, HF ~3)
32+
- [ ] Negative-space annotation (LatAm/Africa research-compute absence):
33+
decided NOT to add unsourced symbolic pins — implement as a map/essay
34+
annotation instead (rule 1 forbids pins without sources)
2735

2836
## Phase 4 — Publication build
2937
- [ ] Decide renderer: kepler standalone vs MapLibre page (embed weight, link

README.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ documented quant/HFT infrastructure, with an evidence grade on every record.
55

66
## Structure
77

8-
- `data/sites_seed.csv` — the inventory (19 seed records; target ~80-120)
9-
- `build_map.py` — CSV → standalone kepler.gl HTML
8+
- `data/sites_seed.csv` — the site inventory (target ~80-120 records)
9+
- `data/paths.csv` — Tier-2 arcs (origin/dest site_ids, operator, medium),
10+
rendered as a kepler arc layer
11+
- `build_map.py` — CSVs → standalone kepler.gl HTML
1012
(`python build_map.py --out docs/index.html`)
13+
- `pipeline/geocode.py` — Nominatim geocoder (rate-limited, cached) for
14+
verifying coordinates against documented addresses
1115
- `docs/index.html` — output (tracked; servable via GitHub Pages); embeddable
1216
in Ghost via iframe or direct upload
17+
- `ENRICHMENT.md` — candidate records awaiting research/verification
1318

1419
## Schema
1520

@@ -18,8 +23,15 @@ documented quant/HFT infrastructure, with an evidence grade on every record.
1823
| `tier` | `execution` (exchange colo) / `network` (microwave, shortwave) / `research` (ML compute) |
1924
| `coord_precision` | `exact` (verified address/geocode) / `approximate` (needs verification) / `city_level` / `symbolic` (no single site exists, e.g. cloud usage) |
2025
| `confidence` | `confirmed` (primary source: license, filing, corporate announcement) / `reported` (credible press) / `inferred` (triangulated from job ads, LinkedIn, network data) |
21-
| `evidence_type` | `spectrum_license` / `planning_docs` / `exchange_docs` / `corporate_announcement` / `operator_docs` / `press` |
22-
| `verify_url_hint` | where to find the primary source (replace with real URLs before publication) |
26+
| `evidence_type` | `spectrum_license` / `planning_docs` / `exchange_docs` / `corporate_announcement` / `operator_docs` / `press` / `regulatory_docs` / `academic` / `job_posting` |
27+
| `status` | `active` / `historical` (e.g. Basildon, Moscow) / `under_construction` |
28+
| `power_mw` | numeric IT/campus power where a public source states it; blank otherwise |
29+
| `evidence_url` | working public URL for the strongest source backing the record |
30+
| `verify_url_hint` | legacy pointer for pre-enrichment records (being replaced by `evidence_url`) |
31+
32+
`data/paths.csv` (Tier-2 arcs): `path_id`, `origin_site_id`, `dest_site_id`,
33+
`operator`, `medium` (`microwave`/`mm-wave`/`laser`/`shortwave`/`fiber`),
34+
`status`, `confidence`, `evidence_note`, `evidence_url`.
2335

2436
**Publication rule: every pin must carry a public primary or credible secondary
2537
source. Nothing gets mapped that isn't already in the public record.**

build_map.py

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
"""
22
Quant/HFT infrastructure map — build script.
33
4-
Reads data/sites_seed.csv, enriches it (Google Maps deep links, tier labels),
5-
and exports a standalone kepler.gl HTML map suitable for embedding in a blog post.
4+
Reads data/sites_seed.csv (point pins) and data/paths.csv (Tier-2 arcs,
5+
joined to site coordinates via origin/dest site_ids), enriches them
6+
(Google Maps deep links, tier labels), and exports a standalone kepler.gl
7+
HTML map suitable for embedding in a blog post.
68
79
Usage:
8-
python build_map.py [--csv data/sites_seed.csv] [--out quant_dc_map.html]
10+
python build_map.py [--csv data/sites_seed.csv] [--paths data/paths.csv]
11+
[--out quant_dc_map.html]
912
"""
1013

1114
from __future__ import annotations
@@ -58,6 +61,9 @@
5861
"research": [70, 130, 240], # blue — the compute layer
5962
}
6063

64+
PATHS_LABEL = "Tier 2 — Paths (arcs)"
65+
PATHS_COLOR = [250, 190, 60] # amber, matching the network tier
66+
6167

6268
def load_sites(csv_path: Path) -> pd.DataFrame:
6369
df = pd.read_csv(csv_path)
@@ -84,8 +90,33 @@ def load_sites(csv_path: Path) -> pd.DataFrame:
8490
return df
8591

8692

87-
def kepler_config(df: pd.DataFrame) -> dict:
88-
"""Minimal kepler config: one point layer per tier, tooltip with evidence fields."""
93+
def load_paths(paths_csv: Path, sites: pd.DataFrame) -> pd.DataFrame | None:
94+
"""Load Tier-2 arcs and join origin/dest coordinates from the sites table.
95+
96+
Returns None when the paths file is absent or has no rows."""
97+
if not paths_csv.exists():
98+
return None
99+
paths = pd.read_csv(paths_csv)
100+
if paths.empty:
101+
return None
102+
103+
coords = sites.set_index("site_id")[["lat", "lon", "site_name"]]
104+
unknown = (set(paths["origin_site_id"]) | set(paths["dest_site_id"])) - set(coords.index)
105+
if unknown:
106+
raise ValueError(f"paths.csv references unknown site_ids: {sorted(unknown)}")
107+
108+
for end, col in (("origin", "origin_site_id"), ("dest", "dest_site_id")):
109+
joined = coords.loc[paths[col]].reset_index(drop=True)
110+
paths[f"{end}_lat"] = joined["lat"]
111+
paths[f"{end}_lon"] = joined["lon"]
112+
paths[f"{end}_name"] = joined["site_name"]
113+
paths["route"] = paths["origin_name"] + " → " + paths["dest_name"]
114+
return paths
115+
116+
117+
def kepler_config(df: pd.DataFrame, with_paths: bool = False) -> dict:
118+
"""Minimal kepler config: one point layer per tier (+ an arc layer for
119+
Tier-2 paths), tooltip with evidence fields."""
89120
center_lat = float(df["lat"].mean())
90121
center_lon = float(df["lon"].mean())
91122

@@ -110,6 +141,26 @@ def kepler_config(df: pd.DataFrame) -> dict:
110141
},
111142
}
112143
)
144+
if with_paths:
145+
layers.append(
146+
{
147+
"id": "layer_paths",
148+
"type": "arc",
149+
"config": {
150+
"dataId": "paths",
151+
"label": PATHS_LABEL,
152+
"columns": {
153+
"lat0": "origin_lat",
154+
"lng0": "origin_lon",
155+
"lat1": "dest_lat",
156+
"lng1": "dest_lon",
157+
},
158+
"isVisible": True,
159+
"color": PATHS_COLOR,
160+
"visConfig": {"opacity": 0.5, "thickness": 1.5},
161+
},
162+
}
163+
)
113164

114165
tooltip_fields = [
115166
{"name": f}
@@ -118,11 +169,22 @@ def kepler_config(df: pd.DataFrame) -> dict:
118169
"operator_or_venue",
119170
"firms_linked",
120171
"capacity_note",
172+
"power_mw",
173+
"status",
121174
"confidence",
122175
"evidence_note",
176+
"evidence_url",
123177
"gmaps_url",
124178
]
125179
]
180+
paths_tooltip = [
181+
{"name": f}
182+
for f in ["route", "operator", "medium", "status", "confidence",
183+
"evidence_note", "evidence_url"]
184+
]
185+
fields_to_show = {t: tooltip_fields for t in TIER_COLORS}
186+
if with_paths:
187+
fields_to_show["paths"] = paths_tooltip
126188

127189
return {
128190
"version": "v1",
@@ -131,15 +193,15 @@ def kepler_config(df: pd.DataFrame) -> dict:
131193
"layers": layers,
132194
"interactionConfig": {
133195
"tooltip": {
134-
"fieldsToShow": {t: tooltip_fields for t in TIER_COLORS},
196+
"fieldsToShow": fields_to_show,
135197
"enabled": True,
136198
}
137199
},
138200
},
139201
"mapState": {
140202
"latitude": center_lat,
141203
"longitude": center_lon,
142-
"zoom": 2.4,
204+
"zoom": 1.7,
143205
},
144206
"mapStyle": {
145207
"styleType": CARTO_DARK["id"],
@@ -149,15 +211,18 @@ def kepler_config(df: pd.DataFrame) -> dict:
149211
}
150212

151213

152-
def build(csv_path: Path, out_path: Path) -> None:
214+
def build(csv_path: Path, out_path: Path, paths_csv: Path | None = None) -> None:
153215
df = load_sites(csv_path)
216+
paths = load_paths(paths_csv, df) if paths_csv else None
154217

155-
m = KeplerGl(config=kepler_config(df))
218+
m = KeplerGl(config=kepler_config(df, with_paths=paths is not None))
156219
# One dataset per tier so each gets its own layer + legend entry + toggle.
157220
for tier in TIER_COLORS:
158221
subset = df[df["tier"] == tier].reset_index(drop=True)
159222
if not subset.empty:
160223
m.add_data(data=subset, name=tier)
224+
if paths is not None:
225+
m.add_data(data=paths, name="paths")
161226

162227
m.save_to_html(file_name=str(out_path), read_only=False)
163228

@@ -169,14 +234,16 @@ def build(csv_path: Path, out_path: Path) -> None:
169234
html = head + FULLSCREEN_SNIPPET + "</body>" + tail
170235
out_path.write_text(html, encoding="utf-8")
171236

237+
n_paths = 0 if paths is None else len(paths)
172238
print(f"Wrote {out_path}{len(df)} sites "
173-
f"({df['tier'].value_counts().to_dict()}); "
239+
f"({df['tier'].value_counts().to_dict()}), {n_paths} paths; "
174240
f"stripped {n_stripped} bundled Mapbox token(s)")
175241

176242

177243
if __name__ == "__main__":
178244
parser = argparse.ArgumentParser()
179245
parser.add_argument("--csv", default="data/sites_seed.csv", type=Path)
246+
parser.add_argument("--paths", default="data/paths.csv", type=Path)
180247
parser.add_argument("--out", default="quant_dc_map.html", type=Path)
181248
args = parser.parse_args()
182-
build(args.csv, args.out)
249+
build(args.csv, args.out, paths_csv=args.paths)

0 commit comments

Comments
 (0)