bgamazay commited on
Commit
8023c2d
·
verified ·
1 Parent(s): 54f7f53

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +228 -320
app.py CHANGED
@@ -1,354 +1,262 @@
1
  import streamlit as st
2
  import pandas as pd
3
- import numpy as np
4
- import plotly.graph_objects as go
5
 
6
- # --- Page Configuration ---
7
- st.set_page_config(
8
- page_title="US AI Emissions in 2030",
9
- page_icon="⚡",
10
- layout="wide"
11
- )
12
 
13
- # --- Custom CSS ---
14
  st.markdown("""
15
  <style>
16
- /* --- 1. Global Font Reset --- */
17
- /* Applies a clean, modern system font to the whole app */
18
- html, body, [class*="css"] {
19
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
20
- }
21
-
22
- /* --- 2. Sidebar Layout & Width --- */
23
- [data-testid="stSidebar"] {
24
- width: 450px !important;
25
- min-width: 450px !important;
26
- }
27
-
28
- /* --- 3. Sidebar Typography --- */
29
-
30
- /* Target the text inside the sidebar's markdown container */
31
- [data-testid="stSidebarContent"] .stMarkdown {
32
- color: white !important;
33
- }
34
-
35
- /* Optional: Target specific headers or paragraphs in the sidebar for clarity */
36
- [data-testid="stSidebarContent"] p,
37
- [data-testid="stSidebarContent"] h1,
38
- [data-testid="stSidebarContent"] h2,
39
- [data-testid="stSidebarContent"] h3 {
40
- color: white !important;
41
  }
42
-
43
- /* The Custom Question Headers */
44
- .sidebar-question {
45
- font-size: 1.4rem;
46
- font-weight: 700;
47
- /* Use 'inherit' so Streamlit handles the Light/Dark color shift automatically */
48
- color: inherit;
49
- margin-bottom: 8px;
50
- line-height: 1.3;
51
  }
52
-
53
- /* Standard Streamlit Labels (e.g., above sliders if visible) */
54
- [data-testid="stSidebar"] label {
55
- font-size: 1.1rem !important;
56
- font-weight: 600 !important;
57
  }
58
-
59
- /* Markdown Paragraphs in Sidebar (e.g., in Expanders) */
60
- [data-testid="stSidebar"] .stMarkdown p {
61
- font-size: 1.05rem !important;
62
- line-height: 1.5;
63
- color: #4b5563;
64
  }
 
 
65
 
66
- /* --- 4. Sidebar Input Styling --- */
 
 
 
 
 
67
 
68
- /* Number Input: The actual number text */
69
- [data-testid="stSidebar"] [data-testid="stNumberInput"] input {
70
- font-size: 1.4rem !important;
71
- font-weight: 700 !important;
72
- color: #ff4b4b; /* Matches your theme color */
73
- padding-top: 5px;
74
- padding-bottom: 5px;
75
- }
76
-
77
- /* Slider: Min/Max/Current Values */
78
- [data-testid="stSidebar"] [data-testid="stTickBarMin"],
79
- [data-testid="stSidebar"] [data-testid="stTickBarMax"] {
80
- font-size: 1rem !important;
81
- font-weight: 600 !important;
82
- }
83
 
84
- /* Slider: The Hover/Drag Value Popup */
85
- [data-testid="stSidebar"] .stSlider [data-testid="stThumbValue"] {
86
- font-size: 1.2rem !important;
87
- font-weight: bold !important;
88
- }
89
-
90
- /* --- 5. Main Dashboard Components --- */
91
 
92
- .metric-card {
93
- background-color: #ffffff;
94
- padding: 20px;
95
- border-radius: 12px;
96
- text-align: center;
97
- margin-bottom: 15px;
98
- border: 1px solid #e5e7eb;
99
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
100
- }
 
 
 
101
 
102
- .big-number {
103
- font-size: 3.5rem;
104
- font-weight: 800;
105
- color: #ff4b4b;
106
- margin: 10px 0;
107
- line-height: 1;
108
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
- .sub-text {
111
- font-size: 1.6rem;
112
- font-weight: 500;
113
- color: #36454F;
114
- }
115
 
116
- /* Custom Spacer for Sidebar */
117
- .spacer {
118
- margin-top: 1.5rem;
119
- margin-bottom: 1.5rem;
120
- height: 1px;
121
- background-color: #e5e7eb; /* Subtle divider line */
122
- }
123
- /* --- 6. Layout Tweaks --- */
124
- /* Removes the large gap at the top of the page */
125
- .block-container {
126
- padding-top: 5rem !important;
127
- padding-bottom: 0rem !important;
128
- }
 
129
 
130
- </style>
131
- """, unsafe_allow_html=True)
 
132
 
133
- # --- Title ---
134
- #st.title("🌍 The Climate Cost of the AI Race ⛽️")
135
 
136
- # --- Sidebar Inputs ---
137
- st.sidebar.markdown("""
138
- **What will the US emissions of AI be in 2030?** Model the 3 variables below to see.
139
- """)
140
- st.sidebar.markdown('<div class="spacer"></div>', unsafe_allow_html=True)
141
 
142
- # 1. AI Power Demand
143
- st.sidebar.markdown('<p class="sidebar-question">1. How much power will AI require in 2030? (GW)</p>', unsafe_allow_html=True)
144
- ai_demand_gw = st.sidebar.number_input(
145
- "Demand (GW)",
146
- value=100,
147
- step=10,
148
- format="%d",
149
- label_visibility="collapsed"
150
- )
151
- with st.sidebar.expander("More on AI Demand Forecasts"):
152
- st.markdown("""
153
- Current forecasts vary wildly, suggesting US AI data centers will need anywhere from **50 GW to over 250 GW** in the coming decade.$^{1,2,3}$
154
-
155
- * Epoch AI analysts currently project that **100 GW** is the best forecast for AI power demand by 2030, representing roughly **10% of America's total peak power capacity**, requiring growth rates not seen since the 1980s.$^{1}$
156
- * Anthropic projects the U.S. AI sector needs at least **50 GW** by 2028 to maintain global leadership, which is roughly double the peak electricity demand of New York City.$^{2}$
157
- * OpenAI leadership has indicated a desire for up to **250 GW** of power by 2033 to support future model scaling.$^{3}$
158
-
159
- *Sources:*
160
- 1. [Epoch AI: America's AI Power Problem (Dec 2025)](https://epoch.ai/gradient-updates/is-almost-everyone-wrong-about-americas-ai-power-problem)
161
- 2. [Anthropic: Build AI in America (July 2025)](https://www.anthropic.com/news/build-ai-in-america)
162
- 3. [The Information: Sam Altman Wants 250 GW (2025)](https://www.theinformation.com/articles/sam-altman-wants-250-gigawatts-power-possible)
163
- """)
164
-
165
- st.sidebar.markdown('<div class="spacer"></div>', unsafe_allow_html=True)
166
 
167
- # 2. Gas Share
168
- st.sidebar.markdown('<p class="sidebar-question">2. What proportion of this power will be supplied by natural gas?</p>', unsafe_allow_html=True)
169
- gas_share = st.sidebar.slider(
170
- "Gas Share",
171
- min_value=0, max_value=100, value=90, step=5,
172
- format="%d%%",
173
- label_visibility="collapsed"
174
  )
175
 
176
- with st.sidebar.expander("More on Energy Mix"):
177
- st.markdown("""
178
- **Why Gas?**
179
- The electric grid in major hubs like Texas is effectively "sold out," with wait times for connection approaching 5 years. To bypass this, AI labs are adopting "Bring Your Own Generation" (BYOG) strategies, primarily using natural gas which can be deployed in months rather than years. In fact, current projections suggest that **nearly a third of all new data center development will deploy behind-the-meter (BTM) gas generation** to circumvent these bottlenecks.$^{1,2}$
180
-
181
- Even when projects connected to the grid, natural gas is the backbone of the US power system, accounting for approximately **43% of total utility-scale electricity generation** in 2023.$^{3,4}$
182
-
183
- "Demand response" (or data center flexibility) could theoretically pull gigawatts "out of thin air" by matching AI training jobs to times when the grid has spare capacity.$^{5}$ However, many experts remain skeptical of the true magnitude of this solution, as large-scale implementation faces significant technical hurdles and pushback from major grid operators like PJM.$^{5,6}$
184
-
185
- **What about Solar?**
186
- While solar prices have dropped ~88% since 2009, it faces physical limits. 2 GW of solar requires a land area roughly the size of Manhattan (approx. 60 km²). Solar requires massive battery storage for 24/7 reliability, adding complexity for off-grid "island" data centers that cannot draw on spare grid capacity at night.$^{5}$
187
-
188
- *Sources:*
189
- 1. [Latitude Media (Jan 2026)](https://www.latitudemedia.com/news/what-the-michigan-stargate-site-says-about-todays-ai-market)
190
- 2. [JLL 2026 Global Data Center Outlook](https://www.jll.com/en-us/insights/market-outlook/data-center-outlook)
191
- 3. [U.S. Energy Information Administration (EIA)](https://www.eia.gov/energyexplained/electricity/electricity-in-the-us.php)
192
- 4. [Ember Energy - U.S. Country Profile](https://ember-energy.org/countries-and-regions/united-states-of-america/)
193
- 5. [Epoch AI: America's AI Power Problem (Dec 2025)](https://epoch.ai/gradient-updates/is-almost-everyone-wrong-about-americas-ai-power-problem)
194
- 6. [Woodway Energy: Bridging the 5-Year Gap](https://www.woodwayenergy.com/off-grid-btm-gas-power-generation-data-centers/)
195
- """)
196
-
197
- st.sidebar.markdown('<div class="spacer"></div>', unsafe_allow_html=True)
198
-
199
- # 3. Turbine Efficiency
200
- st.sidebar.markdown('<p class="sidebar-question">3. What will the efficiency of the gas turbines be?</p>', unsafe_allow_html=True)
201
- turbine_eff_percent = st.sidebar.slider(
202
- "Efficiency",
203
- min_value=35, max_value=60, value=45, step=1,
204
- format="%d%%",
205
- label_visibility="collapsed"
206
  )
207
 
208
- with st.sidebar.expander("More on Turbine Tech"):
209
- st.markdown("""
210
- **Trade-offs: Efficiency vs. Speed**: The efficiency of gas turbines, which determines their carbon emissions, varies from 35-60%. The most efficient, are, generally, the slowest to build. In frontier data centers, AI chips cost roughly 10x more than the power infrastructure, driving companies to prioritize deployment speed over energy efficiency.
211
-
212
- * **Aeroderivative (35-40%):** Modified jet engines (e.g., GE LM2500). They are less efficient but can be deployed in weeks via truck. xAI used these to bypass multi-year grid delays.
213
- * **Reciprocating Engines (40-50%):** Modular internal combustion engines (e.g., Wärtsilä). They maintain high efficiency even at partial loads, making them ideal for flexible "demand response" strategies.
214
- * **Combined Cycle (50-60%):** The gold standard, using waste heat to drive a steam turbine. However, lead times of **36-60 months** make them too slow for the current AI race. These are representative of utility-scale gas turbines.
215
- * **Solid Oxide Fuel Cells (50-55%):** (e.g., Bloom Energy). These offer high efficiency and fast deployment (no combustion permitting), but come with a high capital cost.
216
-
217
- [Source](https://open.substack.com/pub/semianalysis/p/how-ai-labs-are-solving-the-power)
218
- """)
219
-
220
- # Constants
221
- CAPACITY_FACTOR = 0.90
222
- US_BASELINE_MMT = 4900
223
- FUEL_CARBON_CONSTANT = 486 * 0.60
224
-
225
- # --- Calculations ---
226
-
227
- # 1. Calculate Emissions Factor
228
- if turbine_eff_percent > 0:
229
- calculated_ef = FUEL_CARBON_CONSTANT / (turbine_eff_percent / 100)
230
- else:
231
- calculated_ef = 0
 
232
 
233
- # 2. Calculate Total Energy
234
- gas_gw_capacity = ai_demand_gw * (gas_share/100)
235
- total_gwh = gas_gw_capacity * CAPACITY_FACTOR * 8760
236
 
237
- # 3. Calculate Emissions (MMT CO2e)
238
- ai_emissions_mmt = (total_gwh * calculated_ef) / 1_000_000
239
- percent_increase = (ai_emissions_mmt / US_BASELINE_MMT) * 100
 
 
 
 
 
240
 
241
- # --- Dashboard Layout ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
- # 1. The Big Number
244
- c1, c2, c3 = st.columns([1, 2, 1])
245
- with c2:
246
- st.markdown(f"""
247
- <div class="metric-card">
248
- <div class="sub-text">2030 US Emissions Increase due to AI</div>
249
- <div class="big-number">+{percent_increase:.2f}%</div>
250
- <div class="sub-text">{ai_emissions_mmt:.1f} Million tCO<sub>2</sub>e</div>
251
- <div style="font-size: 1em; color: #888; margin-top: 10px;">
 
 
 
 
 
 
 
252
  </div>
253
- </div>
254
- """, unsafe_allow_html=True)
255
-
256
- # 2. Charts
257
- years = [2020, 2021, 2022, 2023, 2024]
258
- emissions_hist = [4690, 5020, 5060, 4920, 4900]
259
- target_2030 = 3065
260
- bau_2030 = 4900
261
-
262
- fig1 = go.Figure()
263
-
264
- # History Line
265
- fig1.add_trace(go.Scatter(
266
- x=years, y=emissions_hist,
267
- mode='lines+markers',
268
- name='Historical CO₂',
269
- line=dict(color='lightgray', width=4),
270
- marker=dict(size=12)
271
- ))
272
-
273
- fig1.add_trace(go.Scatter(
274
- x=[2030], y=[target_2030],
275
- mode='markers',
276
- name='2030 Climate Goal',
277
- marker=dict(color='green', size=15, symbol='circle')
278
- ))
279
-
280
- fig1.add_trace(go.Bar(
281
- x=[2030], y=[bau_2030],
282
- name='2030 Baseline Estimate',
283
- marker_color='lightgray'
284
- ))
285
- fig1.add_trace(go.Bar(
286
- x=[2030], y=[ai_emissions_mmt],
287
- name='Additional AI Gas Emissions',
288
- base=bau_2030,
289
- marker_color='#ff4b4b'
290
  ))
291
 
292
- fig1.update_layout(
293
- height=650,
294
- font=dict(size=18),
295
- title={
296
- 'text': "US Emissions Trajectory vs. AI Impact",
297
- 'font': {'size': 22},
298
- 'x': 0.5,
299
- 'xanchor': 'center',
300
- 'y': 0.9, # Keeps title low and close to the chart
301
- 'yanchor': 'top'
302
- },
303
- yaxis={
304
- 'title': {
305
- 'text': "Emissions (Million tCO<sub>2</sub>)",
306
- 'font': {'size': 20}
307
- },
308
- 'tickfont': {'size': 16}
309
- },
310
- xaxis={
311
- 'tickfont': {'size': 16}
312
- },
313
- barmode='stack',
314
- legend={
315
- 'orientation': "h",
316
- 'y': -0.15,
317
- 'x': 0.5,
318
- 'xanchor': 'center',
319
- 'font': {'size': 18}
320
- },
321
- margin=dict(l=50, r=50, t=80, b=100),
322
- hovermode="x unified"
323
- )
324
-
325
- st.plotly_chart(fig1, use_container_width=True)
326
-
327
  st.markdown("---")
328
- with st.expander("Methodology"):
329
- st.markdown(f"""
330
- **Baseline & Context**
331
- * Annual CO₂ emissions data from [Our World in Data](https://ourworldindata.org/profile/co2/united-states) (fossil fuels and industry) is used to visualize the 2020-2024 trajectory.
332
- * The 2030 percentage increase is calculated against the 2024 annual CO₂ emissions (approx. {US_BASELINE_MMT} Million TCO₂e).
333
- * 2030 Climate Goal is based on the Paris Agreement commitment to halve 2005 emissions (6130 TCO₂e).
334
-
335
-
336
- **The Physics of Efficiency:**
337
- The critical driver in this model is **Thermal Efficiency** ($\eta$).
338
- * Lower efficiency turbines (like simple-cycle Aeroderivatives) must burn *more* fuel to generate the same amount of electricity. Therefore, emissions scale inversely with efficiency.
339
- * We use [NREL Life Cycle Assessment](https://docs.nrel.gov/docs/fy21osti/80580.pdf) data for a modern Combined Cycle Gas Turbine (CCGT) as the anchor: **486 gCO₂e/kWh @ 60% efficiency**.
340
- * This implies a fuel carbon content constant ($C_{{fuel}}$) of approx. **291.6**.
341
- $$ EF_{{scenario}} = \\frac{{291.6}}{{\\eta_{{selected}}}} $$
342
- *Example:* A 35% efficient turbine results in an emissions factor of ~833 gCO₂e/kWh ($291.6 / 0.35$).
343
-
344
- **Total Emissions Formula:**
345
- The final annual emissions ($E_{{total}}$) are calculated as:
346
- $$ E_{{total}} = P_{{GW}} \\times 8,760 \\times CF \\times S_{{gas}} \\times EF_{{scenario}} $$
347
-
348
- * $P_{{GW}}$: AI Power Demand (selected in sidebar).
349
- * $8,760$: Total hours in a year.
350
- * $CF$: **Capacity Factor (90%)**. Fixed to represent the "always-on" baseload nature of AI data centers, which run significantly harder than typical residential usage.
351
- * $S_{{gas}}$: The % of that power demand supplied by Natural Gas (vs. zero-carbon sources or flexibility), selected in sidebar.
352
-
353
- **Assumptions:** Assumes only natural gas emissions, and that all other power infrastructure is non-emitting. In reality, even clean energy technologies have emissions, and coal could be used. Assumes only direct emissions, ignores indirect emissions or reductions.
354
- """)
 
1
  import streamlit as st
2
  import pandas as pd
3
+ import pydeck as pdk
4
+ import math
5
 
6
+ # --- PAGE CONFIGURATION ---
7
+ st.set_page_config(layout="wide", page_title="Frontier AI Emissions Map")
 
 
 
 
8
 
9
+ # --- CUSTOM CSS FOR METRICS & STYLE ---
10
  st.markdown("""
11
  <style>
12
+ .metric-card {
13
+ background-color: #1E1E1E;
14
+ border: 1px solid #333;
15
+ border-radius: 8px;
16
+ padding: 15px;
17
+ margin-bottom: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  }
19
+ .metric-value {
20
+ font-size: 24px;
21
+ font-weight: bold;
22
+ color: #FFFFFF;
 
 
 
 
 
23
  }
24
+ .metric-label {
25
+ font-size: 14px;
26
+ color: #AAAAAA;
 
 
27
  }
28
+ /* Tooltip styling logic happens in PyDeck, but general text style */
29
+ body {
30
+ color: #E0E0E0;
31
+ background-color: #0E1117;
 
 
32
  }
33
+ </style>
34
+ """, unsafe_allow_html=True)
35
 
36
+ # --- 1. DATA LOADING & CLEANING ---
37
+ @st.cache_data
38
+ def load_data():
39
+ # Load the uploaded dataset
40
+ # Note: In a real HF Space, ensure the filename matches exactly or use a relative path
41
+ df = pd.read_csv("Frontier AI DC Emissions - Frontier Timeline.csv")
42
 
43
+ # Clean numeric columns (remove commas, handle non-numeric)
44
+ def clean_numeric(val):
45
+ if isinstance(val, str):
46
+ val = val.replace(',', '').strip()
47
+ return pd.to_numeric(val, errors='coerce')
48
+
49
+ df['Power (MW)'] = df['Power (MW)'].apply(clean_numeric)
50
+ df['Carbon Intensity'] = df['Carbon Intensity'].apply(clean_numeric)
51
+ df['Annual Million tCO2'] = df['Annual Million tCO2'].apply(clean_numeric)
52
+
53
+ # --- UNIT CORRECTION LOGIC ---
54
+ # The CSV likely has emissions in Kilotonnes (e.g., 13093) or is just raw.
55
+ # Logic: If value > 100 (which is physically impossible for MtCO2/yr for one plant),
56
+ # assume it is Kilotonnes and divide by 1000 to get Million Tonnes (Mt).
57
+ # Recalculate to verify: MW * 8760 * (Intensity/1000) / 1,000,000 = Mt
58
 
59
+ # We will create a 'Calculated_MtCO2' for verification, but prefer the user's manual column if it exists
60
+ # normalizing it to Million Tonnes.
61
+ df['Emissions_Mt'] = df['Annual Million tCO2'].apply(lambda x: x / 1000 if x > 100 else x)
 
 
 
 
62
 
63
+ # Handle missing coordinates manually based on research
64
+ # Locations:
65
+ # Fermi America "HyperGrid" -> Amarillo/Panhandle, TX
66
+ df.loc[df['Project'].str.contains('Fermi', case=False, na=False), ['Latitude', 'Longitude']] = [35.344, -101.373]
67
+ # Crane Clean Energy Center -> Three Mile Island, PA
68
+ df.loc[df['Project'].str.contains('Crane', case=False, na=False), ['Latitude', 'Longitude']] = [40.154, -76.725]
69
+ # CleanArc Hyperscale -> Caroline County, VA
70
+ df.loc[df['Project'].str.contains('CleanArc', case=False, na=False), ['Latitude', 'Longitude']] = [38.005, -77.478]
71
+ # Vantage Data Centers -> Fredericksburg, VA
72
+ df.loc[df['Project'].str.contains('Vantage', case=False, na=False), ['Latitude', 'Longitude']] = [38.381, -77.495]
73
+ # Stargate Michigan -> Saline Township, MI
74
+ df.loc[df['Project'].str.contains('Stargate Michigan', case=False, na=False), ['Latitude', 'Longitude']] = [42.167, -83.850]
75
 
76
+ # Clean Lat/Long to numeric
77
+ def clean_coord(val):
78
+ if isinstance(val, str):
79
+ # Remove symbols like " ° ' N W E "
80
+ val = val.replace('°', '').replace("'", '').replace('"', '').replace('N','').replace('W','').replace('E','')
81
+ # Handle DMS to Decimal if necessary, but most look like decimals or simple strings
82
+ # For this dataset, simple cleanup might suffice if formats are consistent
83
+ pass
84
+ return pd.to_numeric(val, errors='coerce')
85
+
86
+ # The dataset has DMS strings (e.g., 42°40'28"N). We need a DMS parser.
87
+ def dms_to_dd(dms_str):
88
+ if pd.isna(dms_str) or isinstance(dms_str, (int, float)):
89
+ return dms_str
90
+ dms_str = str(dms_str).strip()
91
+ if not dms_str: return None
92
+
93
+ # Simple parser for format: 42°40'28"N
94
+ try:
95
+ parts = dms_str.replace('°', ' ').replace("'", ' ').replace('"', ' ').split()
96
+ degrees = float(parts[0])
97
+ minutes = float(parts[1]) if len(parts) > 1 else 0
98
+ seconds = float(parts[2]) if len(parts) > 2 else 0
99
+ direction = parts[-1] if parts[-1] in ['N','S','E','W'] else 'N' # Default N/E if missing
100
+
101
+ dd = degrees + minutes/60 + seconds/3600
102
+ if direction in ['S', 'W']:
103
+ dd *= -1
104
+ return dd
105
+ except:
106
+ return None # Fallback or keep original if it was already decimal
107
+
108
+ # Apply DMS conversion only where it looks like a string with degrees
109
+ # Note: The manual overrides above provided decimal, so we skip those rows
110
+ for col in ['Latitude', 'Longitude']:
111
+ df[col] = df[col].apply(lambda x: dms_to_dd(x) if isinstance(x, str) and '°' in x else x)
112
+ df[col] = pd.to_numeric(df[col], errors='coerce')
113
+
114
+ # Drop rows without coordinates
115
+ df = df.dropna(subset=['Latitude', 'Longitude'])
116
 
117
+ # --- ENRICHMENT FOR HOVER ---
118
+ # 1 MtCO2 approx 217,000 passenger vehicles/year (EPA is 4.6 metric tons/car/year)
119
+ # 1,000,000 tons / 4.6 = ~217,391 cars.
120
+ # User stat: 13.1 Mt = 2.9M cars -> implies ~4.5 tons/car. We will use 4.6.
121
+ df['Cars_Equivalent_Millions'] = (df['Emissions_Mt'] * 1000000 / 4600 / 1000000).round(2)
122
 
123
+ # Coal Plant Equivalent: Average coal plant is ~3.5 to 4 MtCO2/year
124
+ df['Coal_Plants_Equivalent'] = (df['Emissions_Mt'] / 4.0).round(1)
125
+
126
+ # Color Categories (R, G, B, A)
127
+ def get_color(status):
128
+ s = str(status).lower()
129
+ if 'off-grid' in s or 'gas' in s:
130
+ return [255, 65, 54, 200] # Red (Danger)
131
+ elif 'hybrid' in s or 'nuclear' in s: # Nuclear often grouped here as transition/special
132
+ return [255, 133, 27, 200] # Orange (Transition)
133
+ else:
134
+ return [0, 116, 217, 200] # Blue (Grid)
135
+
136
+ df['color'] = df['Grid Status'].apply(get_color)
137
 
138
+ # Bubble Size (Scaled)
139
+ # Scale factor for visual sizing
140
+ df['radius'] = df['Emissions_Mt'].apply(lambda x: math.sqrt(x) * 15000)
141
 
142
+ return df
 
143
 
144
+ df = load_data()
 
 
 
 
145
 
146
+ # --- SIDEBAR CONTROLS ---
147
+ st.sidebar.header("🌍 Frontier AI Emissions")
148
+ st.sidebar.markdown("Filter the map to analyze the carbon footprint of planned AI infrastructure.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
150
+ # Filters
151
+ grid_filter = st.sidebar.multiselect(
152
+ "Connection Type",
153
+ options=df['Grid Status'].unique(),
154
+ default=df['Grid Status'].unique()
 
 
155
  )
156
 
157
+ owner_filter = st.sidebar.multiselect(
158
+ "Owner",
159
+ options=df['Owner'].unique(),
160
+ default=df['Owner'].unique()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  )
162
 
163
+ # Apply filters
164
+ filtered_df = df[
165
+ (df['Grid Status'].isin(grid_filter)) &
166
+ (df['Owner'].isin(owner_filter))
167
+ ]
168
+
169
+ # --- SCORECARD METRICS ---
170
+ total_power = filtered_df['Power (MW)'].sum() / 1000 # GW
171
+ total_emissions = filtered_df['Emissions_Mt'].sum()
172
+ total_cars = filtered_df['Cars_Equivalent_Millions'].sum()
173
+ avg_intensity = filtered_df['Carbon Intensity'].mean()
174
+
175
+ st.sidebar.divider()
176
+ st.sidebar.markdown("### 📊 Aggregate Impact")
177
+
178
+ col1, col2 = st.sidebar.columns(2)
179
+ col1.metric("Total Power", f"{total_power:.1f} GW", help="Total capacity of visible projects")
180
+ col2.metric("Annual Emissions", f"{total_emissions:.1f} Mt", help="Million Tonnes CO2/year")
181
+
182
+ st.sidebar.markdown(f"""
183
+ <div class="metric-card">
184
+ <div class="metric-label">🚗 Equivalent Traffic Added</div>
185
+ <div class="metric-value">{total_cars:.1f} Million Cars</div>
186
+ </div>
187
+ """, unsafe_allow_html=True)
188
 
189
+ st.sidebar.markdown(f"**Avg Carbon Intensity:** {avg_intensity:.0f} kg/MWh")
 
 
190
 
191
+ # --- MAIN MAP ---
192
+ st.title("The Carbon Footprint of Frontier AI")
193
+ st.markdown(
194
+ "This map visualizes the annual emissions of major planned AI data centers. "
195
+ "**Bubble size** represents CO₂ emissions. **Color** indicates grid status "
196
+ "(<span style='color:#FF4136'><b>Red = Off-Grid/Gas</b></span>, <span style='color:#0074D9'><b>Blue = Grid</b></span>).",
197
+ unsafe_allow_html=True
198
+ )
199
 
200
+ # PyDeck Layer
201
+ layer = pdk.Layer(
202
+ "ScatterplotLayer",
203
+ filtered_df,
204
+ get_position="[Longitude, Latitude]",
205
+ get_radius="radius",
206
+ get_fill_color="color",
207
+ pickable=True,
208
+ opacity=0.8,
209
+ stroked=True,
210
+ filled=True,
211
+ radius_min_pixels=5,
212
+ radius_max_pixels=100,
213
+ line_width_min_pixels=1,
214
+ get_line_color=[0, 0, 0],
215
+ )
216
 
217
+ # Tooltip
218
+ tooltip = {
219
+ "html": """
220
+ <div style="font-family: sans-serif; padding: 10px; color: white;">
221
+ <h3 style="margin:0;">{Project}</h3>
222
+ <hr style="border-top: 1px solid #555;">
223
+ <b>Owner:</b> {Owner}<br/>
224
+ <b>Location:</b> {Location}<br/>
225
+ <b>Power:</b> {Power (MW)} MW<br/>
226
+ <b>Status:</b> {Grid Status}<br/>
227
+ <br/>
228
+ <b style="font-size: 1.1em; color: #ffcccb;">Annual Emissions: {Emissions_Mt} MtCO₂</b><br/>
229
+ <i style="font-size: 0.9em;">(Intensity: {Carbon Intensity} kg/MWh)</i>
230
+ <hr style="border-top: 1px dashed #555;">
231
+ <b>🚗 Equal to:</b> {Cars_Equivalent_Millions} Million Cars<br/>
232
+ <b>🏭 Equal to:</b> {Coal_Plants_Equivalent} Coal Power Plants
233
  </div>
234
+ """,
235
+ "style": {
236
+ "backgroundColor": "#1E1E1E",
237
+ "border": "1px solid #333",
238
+ "borderRadius": "8px",
239
+ "color": "white"
240
+ }
241
+ }
242
+
243
+ # Render Map
244
+ st.pydeck_chart(pdk.Deck(
245
+ map_style="mapbox://styles/mapbox/dark-v11", # Or use default 'dark' if no token
246
+ initial_view_state=pdk.ViewState(
247
+ latitude=39.8,
248
+ longitude=-98.6,
249
+ zoom=3.5,
250
+ pitch=0,
251
+ ),
252
+ layers=[layer],
253
+ tooltip=tooltip
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  ))
255
 
256
+ # --- FOOTER / SOURCE ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  st.markdown("---")
258
+ st.caption(
259
+ "**Methodology:** Emissions calculated based on publicly stated power capacity (MW) and regional/source-specific carbon intensity. "
260
+ "Car equivalents assume 4.6 metric tonnes CO₂ per passenger vehicle per year (EPA). "
261
+ "Coal plant equivalent assumes ~4.0 MtCO₂/year for a typical plant."
262
+ )