Hannes Gustafsson Hannes Gustafsson

500 Meters from Blackout: Mapping Stockholm's Infrastructure Fragility

Demo: ask-sweden.gssonhannes.workers.dev


Last week there was a fire 500 meters from Solna kraftstation. It was contained.

But what if it hadn’t been?

That fire, if it had spread, would have threatened power to Danderyds sjukhus. The hospital has 6 hours of backup. But Brista fjärrvärme supplies heat to Norrvikens vattenverk, which supplies water to Danderyds. 450 beds. The region’s largest emergency room. A fire in Solna becomes a hospital without water.

~20 hours over a weekend. I built a tool to trace these cascades.

Karolinska Solna and its critical dependencies

Karolinska Solna’s dependency panel: Solna kraftstation (8h backup), Norrvikens vattenverk, Solna telestation (2h backup).


Why This Works in Sweden

Two things made this possible.

First: offentlighetsprincipen. Sweden’s constitutional principle of public access means incident reports, infrastructure locations, and capacity data are accessible to anyone. MSB publishes real-time alerts. Municipalities publish infrastructure maps. Most countries can’t build this—the data is classified or fragmented. Sweden’s transparency infrastructure is underutilized.

Second: the right abstraction. I’d already built data ingestion—MSB, Polisen, SMHI, Trafikverket streaming to a map. I knew simulation tools existed: Simulink, AnyLogic. But raw incidents are just dots. Simulations without real inputs are just hypotheticals.

Then a fire got reported near Solna kraftstation. What if I could take that event—timestamped, geolocated—and ask what it means for the hospital 3km away?

Infrastructure as a dependency graph. Real-world events inject risk into nodes. A hospital depends on power. Power depends on fuel. Each node has a backup time. A cascade is breadth-first traversal where failure propagates with delay.

Hypothesis: Can a citizen build this in a weekend with public data?

Result: Yes, with caveats.


The Simulation Engine

flowchart LR
    subgraph sources["Sources"]
        MSB[MSB]
        POL[Polisen]
        SMHI[SMHI]
        TRF[Trafikverket]
    end

    subgraph core["Core"]
        API[Aggregator + Geocoding]
        GRAPH[(Dependency Graph)]
        SIM["Simulation"]
    end

    subgraph client["Client"]
        MAP[Map]
        PANEL[Risk Panel]
        FLOW[React Flow]
    end

    MSB --> API
    POL --> API
    SMHI --> API
    TRF --> API
    API --> GRAPH
    GRAPH --> SIM
    SIM --> MAP
    SIM --> PANEL
    SIM --> FLOW

React Flow for graph visualization. Discrete time-step simulation running client-side. ~50 lines of TypeScript:

function step(state: SimulationState, edges: Edge[], dt: number): SimulationState {
  const sorted = topologicalSort(state.nodes, edges);
  const newNodes: NodeState[] = [];
  
  for (const node of sorted) {
    const inputs = gatherInputs(node, edges, newNodes);
    const { output } = transferFunctions[node.type](inputs, node);
    newNodes.push({ ...node, output });
  }
  
  return { t: state.t + 1, nodes: newNodes };
}

Each node type has a transfer function: output as f(inputs). Hospital at 100% with power. Power drops → backup. Backup depletes → 30% capacity.


How Risk Propagates

Event → Node Risk

Hypothetical event on map → risk calculation for nearby nodes:

Prisk=min(0.95,  Rbaseed/λFredFsev)P_{risk} = \min(0.95, \; R_{base} \cdot e^{-d/\lambda} \cdot F_{red} \cdot F_{sev})

  • RbaseR_{base}: event-type × node-type matrix (fire near fuel depot = 0.95, fire near hospital = 0.15)
  • dd: Haversine distance in km
  • λ=2.0\lambda = 2.0 km decay constant
  • Fred=0.3F_{red} = 0.3 if backup exists, else 1.01.0
  • FsevF_{sev}: severity multiplier (1.0, 1.2, 1.5)

Exponential decay is key: 500m is dangerous, 5km is noise.

Risk assessment showing affected infrastructure

Fire near Värtaverket. District heating at 54%, elderly care at 47%, telecom at 15%.

Backup Depletion

Linear depletion when primary input fails:

  • O=1.0O = 1.0 if power 0.8\geq 0.8
  • O=0.9O = 0.9 if tbackup<Tmaxt_{backup} < T_{max}
  • O=CcritO = C_{crit} otherwise

Cumulative tracking: lose power, regain briefly, lose again → continues from where it left off.

Output Aggregation

Multiple dependencies aggregate via min:

Ofinal=min(O1,O2,,On)(1Ddirect)O_{final} = \min(O_1, O_2, \ldots, O_n) \cdot (1 - D_{direct})

Node is only as healthy as its weakest dependency. Power but no heating in January = crisis.

The cascade simulation graph

T+18h. Red = critical, orange = degraded.


Where the Model Breaks

What’s public: hospital locations, power station names, district heating plants, telecom towers.

What’s not: actual dependency relationships, backup capacities, grid redundancy, real-time utilization.

Educated guesses from MSB reports, news articles (“Danderyds lost power for 3h in 2019”), reasonable assumptions.

Result: Directionally correct, not operationally accurate. Good for “what if.” Not for actual crisis planning.

Confidence badges in UI: verified, estimated, outdated. Transparency about uncertainty beats false precision.


What the Simulations Showed

Dozens of scenarios. Three patterns.

1. The 6-hour threshold. Before 6h, cascades stay contained. After, secondary failures trigger—heating shuts down, water degrades, telecom dies. MSB guidance: 72h backup. Most facilities: 6-8h. Gap is 10x.

2. District heating is fragile. Power grid has redundancy. District heating doesn’t—fewer plants, longer restart, everything downstream depends on it. January cold snap: heating failure cascades faster than power failure.

3. Elderly care fails first. Expected hospitals. Wrong. Hospitals have robust backups, redundant feeds, trained staff. Elderly care: 2-4h backup, dual dependency on heat and power, residents can’t self-evacuate. Hits critical 2-4h before hospitals.


Try It

ask-sweden.gssonhannes.workers.dev

Place a hypothesis. Watch risk propagate. Step through the cascade.

Code: github.com/hanng00/ask-sweden