Added files to scrape data, and process

This commit is contained in:
Alex Gebben Work 2026-01-22 16:47:37 -07:00
parent 65d3ffbe70
commit ee2cf3e8da
5 changed files with 8617 additions and 0 deletions

View File

@ -0,0 +1,75 @@
Facility,Total_Assemblies,Total_Tons,Op_Date_Min,Op_Date_Max,Close_Date_Min,Close_Date_Max
Arkansas Nuclear One,5612,2500.5,1974-12-19,1980-03-26,2034-05-20,2038-07-17
Beaver Valley,4945,2283.4,1976-10-01,1987-11-17,2036-01-29,2047-05-27
Big Rock Point,526,69.4,1964-05-01,1964-05-01,1997-08-29,1997-08-29
Braidwood,7181,3008.0,1988-07-29,1988-10-17,2046-10-17,2047-12-18
Browns Ferry,22048,3957.0,1974-08-01,1977-03-01,2033-12-20,2036-07-02
Brunswick,13364,2401.3,1975-11-03,1977-03-18,2034-12-27,2036-09-08
Byron,7294,3058.9,1985-09-16,1987-08-02,2044-10-31,2046-11-06
Callaway,3670,1544.2,1984-12-19,1984-12-19,2044-10-18,2044-10-18
Calvert Cliffs,5830,2316.6,1975-05-08,1977-04-01,2034-07-31,2036-08-13
Catawba,6266,2828.3,1985-06-29,1986-08-19,2043-12-05,2043-12-05
Clinton,7256,1314.2,1987-11-24,1987-11-24,2047-04-17,2047-04-17
Columbia,8084,1433.6,1984-12-13,1984-12-13,2043-12-20,2043-12-20
Comanche Peak,7523,3171.9,1990-08-13,1993-08-03,2050-02-08,2053-02-02
Cook,6346,2771.6,1975-08-28,1978-07-01,2034-10-25,2037-12-23
Cooper,5832,1058.4,1974-07-01,1974-07-01,2034-01-18,2034-01-18
Crystal River,1389,650.0,1977-03-13,1977-03-13,2013-02-20,2013-02-20
Davis-Besse,2220,1065.9,1978-07-31,1978-07-31,2037-04-22,2037-04-22
Diablo Canyon,5264,2249.4,1985-05-07,1986-03-13,2029-10-31,2030-10-31
Dresden,15511,2664.2,1960-07-04,1971-11-16,2029-12-22,2031-01-12
Duane Arnold,3648,659.5,1975-02-01,1975-02-01,2020-08-10,2020-08-10
Farley,5503,2380.3,1977-12-01,1981-07-30,2037-06-25,2041-03-31
Fermi,7710,1374.9,1988-01-23,1988-01-23,2045-03-20,2045-03-20
Fitzpatrick,6180,1123.0,1975-07-28,1975-07-28,2034-10-17,2034-10-17
Fort Calhoun,1264,466.0,1973-09-26,1973-09-26,2016-10-24,2016-10-24
Ginna,1894,713.2,1970-07-01,1970-07-01,2029-09-18,2029-09-18
Grand Gulf,10788,1927.8,1985-07-01,1985-07-01,2044-11-01,2044-11-01
Haddam Neck,1102,448.4,1974-12-27,1974-12-27,1996-12-09,1996-12-09
Harris,2699,1227.2,1987-05-02,1987-05-02,2046-10-24,2046-10-24
Hatch,14400,2589.9,1975-12-31,1979-09-05,2034-08-06,2038-06-13
Hope Creek,9413,1691.8,1986-12-20,1986-12-20,2046-04-11,2046-04-11
Humboldt Bay,390,28.9,1963-08-01,1963-08-01,1976-07-02,1976-07-02
Indian Point,3997,1774.6,1962-10-01,1976-08-30,2020-04-20,2021-04-30
Kewaunee,1335,518.7,1974-06-16,1974-06-16,2013-05-07,2013-05-07
La Salle,17587,3143.9,1984-01-01,1984-10-19,2042-04-17,2043-12-16
LaCrosse,334,38.1,1967-07-03,1967-07-03,1987-04-30,1987-04-30
Limerick,17925,3209.7,1986-02-01,1990-01-08,2044-10-26,2049-06-22
Maine Yankee,1434,542.3,1973-06-29,1973-06-29,1996-12-06,1996-12-06
McGuire,6171,2790.2,1981-12-01,1984-03-01,2041-06-12,2043-03-03
Millstone,8817,3071.2,1970-12-28,1986-04-23,2035-07-31,2045-11-25
Monticello,5026,888.9,1971-06-30,1971-06-30,2030-09-08,2030-09-08
Nine Mile Point,14224,2522.9,1969-12-01,1988-03-11,2029-08-22,2046-10-31
North Anna,5324,2472.1,1978-06-06,1980-12-14,2038-04-01,2040-08-21
Oconee,7304,3429.9,1973-07-15,1974-12-16,2033-02-06,2034-07-19
Oyster Creek,4504,796.6,1969-12-01,1969-12-01,2018-09-17,2018-09-17
Palisades,2097,869.7,1971-12-31,1971-12-31,2022-05-31,2022-05-31
Palo Verde,12633,5481.9,1986-01-28,1988-01-08,2045-06-01,2047-11-25
Peach Bottom,23078,4155.1,1974-07-05,1974-12-23,2053-08-08,2054-07-02
Perry,9026,1620.9,1987-11-18,1987-11-18,2046-11-07,2046-11-07
Pilgrim,4113,731.0,1972-12-01,1972-12-01,2019-05-31,2019-05-31
Point Beach,3646,1408.5,1970-12-21,1972-10-01,2030-10-05,2033-03-08
Prairie Island,3754,1386.8,1973-12-16,1974-12-21,2033-08-09,2034-10-29
Quad Cities,14918,2624.5,1973-02-18,1973-03-10,2032-12-14,2032-12-14
Rancho Seco,493,228.4,1974-08-16,1974-08-16,1989-06-07,1989-06-07
River Bend,7701,1374.7,1986-06-16,1986-06-16,2045-08-29,2045-08-29
Robinson,2228,966.8,1971-03-07,1971-03-07,2030-07-31,2030-07-31
Saint Lucie,6731,2647.0,1976-12-21,1983-08-08,2036-03-01,2043-04-06
Salem,5722,2619.3,1977-06-30,1981-10-13,2036-08-13,2040-04-18
San Onofre,4125,1707.8,1967-03-27,1984-04-01,1992-11-30,2013-06-12
Seabrook,3323,1518.2,1990-08-19,1990-08-19,2050-03-15,2050-03-15
Sequoyah,6212,2833.2,1981-07-01,1982-06-01,2040-09-17,2041-09-15
South Texas,6141,3279.1,1988-08-25,1989-06-19,2047-08-20,2048-12-15
Summer,2735,1157.1,1984-01-01,1984-01-01,2042-08-06,2042-08-06
Surry,6583,3017.5,1972-12-22,1973-05-01,2052-05-25,2053-01-29
Susquehanna,19552,3460.6,1983-06-08,1985-02-12,2042-07-17,2044-03-23
Three Mile Island,1663,786.5,1974-09-02,1974-09-02,2019-09-20,2019-09-20
Trojan,790,359.3,1975-11-21,1975-11-21,1992-11-09,1992-11-09
Turkey Point,6468,2930.6,1972-12-14,1973-09-07,2052-07-19,2053-04-10
Vermont Yankee,3879,705.9,1972-11-30,1972-11-30,2014-12-29,2014-12-29
Vogtle,12553,5899.1,1987-06-01,1989-05-20,2047-01-16,2084-03-30
Waterford,3957,1683.3,1985-09-24,1985-09-24,2044-12-18,2044-12-18
Watts Bar,6489,2995.8,1996-05-27,2016-10-19,2055-11-09,2075-10-22
Wolf Creek,3351,1534.1,1985-09-03,1985-09-03,2045-03-11,2045-03-11
Yankee Rowe,533,127.1,1963-12-24,1963-12-24,1991-10-01,1991-10-01
Zion,2226,1019.4,1973-12-31,1974-09-17,1996-09-19,1997-02-21
1 Facility Total_Assemblies Total_Tons Op_Date_Min Op_Date_Max Close_Date_Min Close_Date_Max
2 Arkansas Nuclear One 5612 2500.5 1974-12-19 1980-03-26 2034-05-20 2038-07-17
3 Beaver Valley 4945 2283.4 1976-10-01 1987-11-17 2036-01-29 2047-05-27
4 Big Rock Point 526 69.4 1964-05-01 1964-05-01 1997-08-29 1997-08-29
5 Braidwood 7181 3008.0 1988-07-29 1988-10-17 2046-10-17 2047-12-18
6 Browns Ferry 22048 3957.0 1974-08-01 1977-03-01 2033-12-20 2036-07-02
7 Brunswick 13364 2401.3 1975-11-03 1977-03-18 2034-12-27 2036-09-08
8 Byron 7294 3058.9 1985-09-16 1987-08-02 2044-10-31 2046-11-06
9 Callaway 3670 1544.2 1984-12-19 1984-12-19 2044-10-18 2044-10-18
10 Calvert Cliffs 5830 2316.6 1975-05-08 1977-04-01 2034-07-31 2036-08-13
11 Catawba 6266 2828.3 1985-06-29 1986-08-19 2043-12-05 2043-12-05
12 Clinton 7256 1314.2 1987-11-24 1987-11-24 2047-04-17 2047-04-17
13 Columbia 8084 1433.6 1984-12-13 1984-12-13 2043-12-20 2043-12-20
14 Comanche Peak 7523 3171.9 1990-08-13 1993-08-03 2050-02-08 2053-02-02
15 Cook 6346 2771.6 1975-08-28 1978-07-01 2034-10-25 2037-12-23
16 Cooper 5832 1058.4 1974-07-01 1974-07-01 2034-01-18 2034-01-18
17 Crystal River 1389 650.0 1977-03-13 1977-03-13 2013-02-20 2013-02-20
18 Davis-Besse 2220 1065.9 1978-07-31 1978-07-31 2037-04-22 2037-04-22
19 Diablo Canyon 5264 2249.4 1985-05-07 1986-03-13 2029-10-31 2030-10-31
20 Dresden 15511 2664.2 1960-07-04 1971-11-16 2029-12-22 2031-01-12
21 Duane Arnold 3648 659.5 1975-02-01 1975-02-01 2020-08-10 2020-08-10
22 Farley 5503 2380.3 1977-12-01 1981-07-30 2037-06-25 2041-03-31
23 Fermi 7710 1374.9 1988-01-23 1988-01-23 2045-03-20 2045-03-20
24 Fitzpatrick 6180 1123.0 1975-07-28 1975-07-28 2034-10-17 2034-10-17
25 Fort Calhoun 1264 466.0 1973-09-26 1973-09-26 2016-10-24 2016-10-24
26 Ginna 1894 713.2 1970-07-01 1970-07-01 2029-09-18 2029-09-18
27 Grand Gulf 10788 1927.8 1985-07-01 1985-07-01 2044-11-01 2044-11-01
28 Haddam Neck 1102 448.4 1974-12-27 1974-12-27 1996-12-09 1996-12-09
29 Harris 2699 1227.2 1987-05-02 1987-05-02 2046-10-24 2046-10-24
30 Hatch 14400 2589.9 1975-12-31 1979-09-05 2034-08-06 2038-06-13
31 Hope Creek 9413 1691.8 1986-12-20 1986-12-20 2046-04-11 2046-04-11
32 Humboldt Bay 390 28.9 1963-08-01 1963-08-01 1976-07-02 1976-07-02
33 Indian Point 3997 1774.6 1962-10-01 1976-08-30 2020-04-20 2021-04-30
34 Kewaunee 1335 518.7 1974-06-16 1974-06-16 2013-05-07 2013-05-07
35 La Salle 17587 3143.9 1984-01-01 1984-10-19 2042-04-17 2043-12-16
36 LaCrosse 334 38.1 1967-07-03 1967-07-03 1987-04-30 1987-04-30
37 Limerick 17925 3209.7 1986-02-01 1990-01-08 2044-10-26 2049-06-22
38 Maine Yankee 1434 542.3 1973-06-29 1973-06-29 1996-12-06 1996-12-06
39 McGuire 6171 2790.2 1981-12-01 1984-03-01 2041-06-12 2043-03-03
40 Millstone 8817 3071.2 1970-12-28 1986-04-23 2035-07-31 2045-11-25
41 Monticello 5026 888.9 1971-06-30 1971-06-30 2030-09-08 2030-09-08
42 Nine Mile Point 14224 2522.9 1969-12-01 1988-03-11 2029-08-22 2046-10-31
43 North Anna 5324 2472.1 1978-06-06 1980-12-14 2038-04-01 2040-08-21
44 Oconee 7304 3429.9 1973-07-15 1974-12-16 2033-02-06 2034-07-19
45 Oyster Creek 4504 796.6 1969-12-01 1969-12-01 2018-09-17 2018-09-17
46 Palisades 2097 869.7 1971-12-31 1971-12-31 2022-05-31 2022-05-31
47 Palo Verde 12633 5481.9 1986-01-28 1988-01-08 2045-06-01 2047-11-25
48 Peach Bottom 23078 4155.1 1974-07-05 1974-12-23 2053-08-08 2054-07-02
49 Perry 9026 1620.9 1987-11-18 1987-11-18 2046-11-07 2046-11-07
50 Pilgrim 4113 731.0 1972-12-01 1972-12-01 2019-05-31 2019-05-31
51 Point Beach 3646 1408.5 1970-12-21 1972-10-01 2030-10-05 2033-03-08
52 Prairie Island 3754 1386.8 1973-12-16 1974-12-21 2033-08-09 2034-10-29
53 Quad Cities 14918 2624.5 1973-02-18 1973-03-10 2032-12-14 2032-12-14
54 Rancho Seco 493 228.4 1974-08-16 1974-08-16 1989-06-07 1989-06-07
55 River Bend 7701 1374.7 1986-06-16 1986-06-16 2045-08-29 2045-08-29
56 Robinson 2228 966.8 1971-03-07 1971-03-07 2030-07-31 2030-07-31
57 Saint Lucie 6731 2647.0 1976-12-21 1983-08-08 2036-03-01 2043-04-06
58 Salem 5722 2619.3 1977-06-30 1981-10-13 2036-08-13 2040-04-18
59 San Onofre 4125 1707.8 1967-03-27 1984-04-01 1992-11-30 2013-06-12
60 Seabrook 3323 1518.2 1990-08-19 1990-08-19 2050-03-15 2050-03-15
61 Sequoyah 6212 2833.2 1981-07-01 1982-06-01 2040-09-17 2041-09-15
62 South Texas 6141 3279.1 1988-08-25 1989-06-19 2047-08-20 2048-12-15
63 Summer 2735 1157.1 1984-01-01 1984-01-01 2042-08-06 2042-08-06
64 Surry 6583 3017.5 1972-12-22 1973-05-01 2052-05-25 2053-01-29
65 Susquehanna 19552 3460.6 1983-06-08 1985-02-12 2042-07-17 2044-03-23
66 Three Mile Island 1663 786.5 1974-09-02 1974-09-02 2019-09-20 2019-09-20
67 Trojan 790 359.3 1975-11-21 1975-11-21 1992-11-09 1992-11-09
68 Turkey Point 6468 2930.6 1972-12-14 1973-09-07 2052-07-19 2053-04-10
69 Vermont Yankee 3879 705.9 1972-11-30 1972-11-30 2014-12-29 2014-12-29
70 Vogtle 12553 5899.1 1987-06-01 1989-05-20 2047-01-16 2084-03-30
71 Waterford 3957 1683.3 1985-09-24 1985-09-24 2044-12-18 2044-12-18
72 Watts Bar 6489 2995.8 1996-05-27 2016-10-19 2055-11-09 2075-10-22
73 Wolf Creek 3351 1534.1 1985-09-03 1985-09-03 2045-03-11 2045-03-11
74 Yankee Rowe 533 127.1 1963-12-24 1963-12-24 1991-10-01 1991-10-01
75 Zion 2226 1019.4 1973-12-31 1974-09-17 1996-09-19 1997-02-21

File diff suppressed because it is too large Load Diff

34
Data_Proc.r Normal file
View File

@ -0,0 +1,34 @@
library(tidyverse)
library(parallel)
TS <- read_csv("Data/Curie_Spent_Fuel_Timeline.csv")
TOTAL <- read_csv("Data/Curie_Spent_Fuel_Site_Totals.csv") %>% mutate(OP_YEAR=year(Op_Date_Min),CLOSE_YEAR=year(Close_Date_Max))%>% select(Facility,Total_Assemblies,Total_Tons,OP_YEAR,CLOSE_YEAR)
###########Series of functions to calculate the gross consumer surplus from a CIFS for each facility, in each year from 1960 to 2082.
#Function to find the net present revenue of a facility,given a discount rate, and the current year, and year the facility will close. This is the sum of discounted costs that WOULD have taken place if the facility was not built
VALUE_ADD <- function(r,CURRENT_YEAR,CLOSE_YEAR){
Years_Until_Close <- max(CLOSE_YEAR-CURRENT_YEAR+1,0)
VALUES <- (1+r)^-(1:10^6)
if(Years_Until_Close==0){return(sum(VALUES))} else{return(sum(VALUES[-1:-Years_Until_Close]))}
}
#A function to extend the revenues calculations to the closure date of all of the facilities.
VALUE_ADD_SINGLE_YEAR <- function(r,CURRENT_YEAR,CLOSE_YEARS){return(sapply(CLOSE_YEARS,function(x){VALUE_ADD(r,CURRENT_YEAR,x)}))}
#A function to extend the calculation of the net present revenues of each facility to all years of interest. That is what is the NPV of building the facility in each year, for each facility.
NPV_CALC <- function(r,DATA=TOTAL,YEARS=1960:2083){
Facility <- pull(DATA,Facility)
RES <- cbind(Facility,do.call(cbind,lapply(YEARS,function(x){VALUE_ADD_SINGLE_YEAR(r,x,DATA$CLOSE_YEAR)})))
colnames(RES) <- c("Facility",YEARS)
RES <- as_tibble(RES) %>% pivot_longer(cols=-Facility,names_to="Year",values_to="Revenue") %>% arrange(Year)
return(RES)
}
#A function which returns a list, of net present revenue calculation tables (facility by year) for a range of possible discount rates. This allows for the results to be quickly looked up, when we want to adjust the time value of money. These results will need to be combined with costs to calculate NPV
MULTI_DISCOUNT_RATE_NPV <- function(INCREMENT=0.005,DATA=TOTAL,YEARS=1960:2083){
NCORES <- detectCores()-1
RES <- mclapply(seq(0,1,by=INCREMENT),NPV_CALC,mc.cores = NCORES)
names(RES) <- 100*seq(0,1,by=INCREMENT) #Name with the discount rate of the given table
return(RES)
}
TOTAL_VALUE_METRICS <- MULTI_DISCOUNT_RATE_NPV(INCREMENT=0.0025)
dir.create("./Results",showWarnings=FALSE)
saveRDS(TOTAL_VALUE_METRICS,"./Results/Storage_Values_by_Facility_and_Variable_Discounts.Rds")

414
Scrape_Curie_Map_Totals.js Normal file
View File

@ -0,0 +1,414 @@
///////////////////////////// // Get totals in the last year of the map (2082) as a way to check total capacity. This can be combined with the yearly data.
(async () => {
/**********************
* Utility functions *
**********************/
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const visible = (el) => !!el && !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
const norm = (s) => (s || "").replace(/\u00A0/g, " ").replace(/\s+/g, " ").trim();
const labelKey = (s) => norm(s).replace(/:$/, "").toLowerCase();
// CSV helpers
const csvEscape = (val) => {
const s = (val === null || val === undefined) ? "" : String(val);
if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
return s;
};
const saveCSV = (rows, filename = "Curie_Spent_Fuel_Site_Totals.csv") => {
const csv = rows.map(r => r.map(csvEscape).join(",")).join("\n");
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 1000);
};
/********************************************
* ISO-only date extraction & min/max *
********************************************/
const isValidISO = (yyyy, mm, dd) => {
const y = Number(yyyy), m = Number(mm), d = Number(dd);
if (!(y >= 1800 && y <= 2200)) return false;
if (!(m >= 1 && m <= 12)) return false;
if (!(d >= 1 && d <= 31)) return false; // light bounds
return true;
};
// Extract ISO date strings YYYY-MM-DD only, validated
const extractISODates = (str) => {
const s = norm(str);
const re = /\b(\d{4})-(\d{2})-(\d{2})\b/g;
const out = [];
let m;
while ((m = re.exec(s)) !== null) {
if (isValidISO(m[1], m[2], m[3])) out.push(`${m[1]}-${m[2]}-${m[3]}`);
}
return Array.from(new Set(out)); // dedupe
};
// Compute min/max via lexicographic compare (correct for ISO)
const isoMinMax = (valOrArr) => {
const arr = Array.isArray(valOrArr) ? valOrArr : [valOrArr];
const all = [];
for (const v of arr) {
if (!v) continue;
all.push(...extractISODates(v));
}
if (!all.length) return { min: "", max: "" };
all.sort();
return { min: all[0], max: all[all.length - 1] };
};
/****************************************
* Locate the "Locate Site" UI control *
****************************************/
const findLocateSiteControl = () => {
// 1) Prefer labeled <select>
const selects = Array.from(document.querySelectorAll("select"));
for (const sel of selects) {
const id = sel.id;
let hasLabel = false;
if (id) {
const lab = document.querySelector(`label[for="${CSS.escape(id)}"]`);
if (lab && /locate site/i.test(norm(lab.innerText))) hasLabel = true;
}
if (!hasLabel && sel.getAttribute("aria-label") && /locate site/i.test(sel.getAttribute("aria-label"))) {
hasLabel = true;
}
if (!hasLabel) {
const parentText = norm((sel.parentElement && sel.parentElement.innerText) || "");
if (/locate site/i.test(parentText)) hasLabel = true;
}
if (hasLabel) return { type: "select", el: sel };
}
// 2) ARIA combobox/button with text "Locate Site"
const candidates = Array.from(document.querySelectorAll('[role="combobox"],[aria-haspopup="listbox"],button,div'))
.filter(el => /locate site/i.test(norm(el.innerText)) || /locate site/i.test(norm(el.getAttribute("aria-label") || "")));
if (candidates.length) {
const combobox = candidates.find(visible) || candidates[0];
return { type: "combobox", el: combobox };
}
// 3) Fallback
const any = Array.from(document.querySelectorAll("*")).find(el => /locate site/i.test(norm(el.innerText)));
if (any) return { type: "unknown", el: any };
return null;
};
/*******************************************************
* Read all facility names from the control *
*******************************************************/
const collectAllFacilityNames = async (control) => {
const EXCLUDE = (t) => {
const n = norm(t).toLowerCase();
return !n || /^select|^choose|^locate site/.test(n) || n.includes("pick to zoom");
};
if (control.type === "select") {
const optEls = Array.from(control.el.querySelectorAll("option"));
return optEls.map(o => norm(o.textContent)).filter(t => !EXCLUDE(t));
}
// For combobox/custom dropdowns
const openDropdown = () => control.el.click();
const getListbox = () =>
document.querySelector('[role="listbox"], [role="menu"], ul[role], div[role="listbox"], .dropdown-menu, .menu, ul');
const closeDropdown = () => document.body.click();
openDropdown();
await sleep(300);
const container = getListbox();
if (!container) {
console.warn("Could not locate the listbox menu for the combobox.");
closeDropdown();
return [];
}
const seen = new Set();
const readVisibleItems = () => {
const items = Array.from(container.querySelectorAll('[role="option"], li, [data-value], .option, .menu-item'));
for (const it of items) {
const txt = norm(it.textContent);
if (!EXCLUDE(txt)) seen.add(txt);
}
};
let lastSize = -1;
let stagnant = 0;
while (stagnant < 3) {
readVisibleItems();
if (container.scrollHeight > container.clientHeight) {
container.scrollTop = container.scrollHeight;
await sleep(150);
readVisibleItems();
container.scrollTop = 0;
await sleep(120);
}
if (seen.size === lastSize) stagnant++;
else { lastSize = seen.size; stagnant = 0; }
}
closeDropdown();
return Array.from(seen);
};
/***************************************************************
* Select a facility by name in the control *
***************************************************************/
const selectFacility = async (control, name) => {
if (control.type === "select") {
const sel = control.el;
const option = Array.from(sel.options).find(o => norm(o.textContent) === norm(name));
if (!option) throw new Error(`Option not found: ${name}`);
sel.value = option.value;
sel.dispatchEvent(new Event("input", { bubbles: true }));
sel.dispatchEvent(new Event("change", { bubbles: true }));
return;
}
control.el.click();
await sleep(180);
const items = Array.from(document.querySelectorAll('[role="option"], li, [data-value], .option, .menu-item'));
let target = items.find(it => norm(it.textContent) === norm(name));
if (!target) target = items.find(it => norm(it.textContent).toLowerCase().includes(norm(name).toLowerCase()));
if (!target) throw new Error(`Could not find menu item for: ${name}`);
target.scrollIntoView({ block: "nearest" });
target.click();
await sleep(220);
};
/*******************************************************
* Wait for popup/details and extract the needed data *
* (scope to most recently visible container) *
*******************************************************/
const waitForDetailLis = async (timeoutMs = 15000) => {
const selectorContainers = '.leaflet-popup-content, .leaflet-popup, .popup, .modal, .panel, [role="dialog"], [role="region"]';
const start = performance.now();
while (performance.now() - start < timeoutMs) {
const containers = Array.from(document.querySelectorAll(selectorContainers)).filter(visible);
// Prefer the last (most recently added) container that contains our labels
for (let i = containers.length - 1; i >= 0; i--) {
const lis = Array.from(containers[i].querySelectorAll("li"));
const hasKey = lis.some(li =>
/^(?:Number of Assemblies:|Operating Date:|Last Projected Discharge:|Projected License Expiration Year:|Metric Tons of Heavy Metal \(MTHM\):)/i
.test(norm(li.textContent))
);
if (hasKey && lis.length) return lis;
}
// Fallback: any visible <li> that matches
const allLis = Array.from(document.querySelectorAll("li")).filter(visible);
const hasKey = allLis.some(li =>
/Number of Assemblies:|Operating Date:|Last Projected Discharge:|Projected License Expiration Year:|Metric Tons of Heavy Metal \(MTHM\):/i
.test(norm(li.textContent))
);
if (hasKey) return allLis;
await sleep(150);
}
return [];
};
// Parse list items to a Map of normalized label -> [values...], strict labels only
const parseDetailListToMap = (lis) => {
const map = new Map();
const pushVal = (key, value) => {
const k = labelKey(key);
const v = norm(value);
if (!map.has(k)) map.set(k, []);
if (v) map.get(k).push(v);
};
for (const li of lis) {
const text = norm(li.textContent);
if (!text) continue;
let label = "";
let value = "";
// Prefer <strong>/<b> at start
const boldish = li.querySelector("strong, b");
if (boldish) {
const boldText = norm(boldish.textContent);
const restText = norm(text.replace(boldText, ""));
if (/:$/.test(boldText)) {
label = boldText;
value = restText.replace(/^:\s*/, "");
} else {
const idx = text.indexOf(":");
if (idx !== -1) { label = text.slice(0, idx + 1); value = text.slice(idx + 1); }
}
} else {
const idx = text.indexOf(":");
if (idx !== -1) { label = text.slice(0, idx + 1); value = text.slice(idx + 1); }
}
label = labelKey(label);
value = norm(value);
if ([
"number of assemblies",
"operating date",
"last projected discharge",
"projected license expiration year",
"metric tons of heavy metal (mthm)"
].includes(label)) {
pushVal(label, value);
}
}
return map;
};
const getDetailsForCurrentSelection = async () => {
const lis = await waitForDetailLis(15000);
if (!lis.length) {
return {
numberOfAssemblies: "",
mthm: "",
opMin: "",
opMax: "",
lpdMin: "",
lpdMax: ""
};
}
const map = parseDetailListToMap(lis);
// Total_Assemblies
let numberOfAssemblies = "";
const noaVals = map.get("number of assemblies") || [];
if (noaVals.length) {
const m = noaVals.join(" ").match(/(\d[\d,]*)/);
numberOfAssemblies = m ? m[1].replace(/,/g, "") : norm(noaVals.join(" "));
}
// Total_Tons (MTHM)
let mthm = "";
const mthmVals = map.get("metric tons of heavy metal (mthm)") || [];
if (mthmVals.length) {
const mm = mthmVals.join(" ").match(/(\d[\d,]*(?:\.\d+)?)/);
if (mm) mthm = mm[1].replace(/,/g, "");
else mthm = norm(mthmVals.join(" "));
}
// Operating Date (ISO only)
let opMin = "", opMax = "";
const opVals = map.get("operating date") || [];
if (opVals.length) {
const { min, max } = isoMinMax(opVals);
opMin = min; opMax = max;
}
// Close date from LPD (ISO-only). Fallback to PLEY (ISO-first, then year -> YYYY-12-31)
let lpdMin = "", lpdMax = "";
const lpdVals = map.get("last projected discharge") || [];
let usedLPD = false;
if (lpdVals.length) {
const { min, max } = isoMinMax(lpdVals);
if (min && max) {
lpdMin = min; lpdMax = max;
usedLPD = true;
}
}
if (!usedLPD) {
const pleyVals = map.get("projected license expiration year") || [];
if (pleyVals.length) {
// ISO inside PLEY first
const isoFromPley = isoMinMax(pleyVals);
if (isoFromPley.min && isoFromPley.max) {
lpdMin = isoFromPley.min;
lpdMax = isoFromPley.max;
} else {
// Year-only fallback -> end-of-year
const years = [];
for (const s of pleyVals) {
const matches = norm(s).match(/\b(\d{4})\b/g) || [];
years.push(...matches.map(Number).filter(y => y >= 1800 && y <= 2200));
}
if (years.length) {
years.sort((a, b) => a - b);
lpdMin = `${years[0]}-12-31`;
lpdMax = `${years[years.length - 1]}-12-31`;
}
}
}
}
return {
numberOfAssemblies,
mthm,
opMin, opMax,
lpdMin, lpdMax
};
};
/*******************************
* Main routine starts here *
*******************************/
console.log("Locating the 'Locate Site' control...");
const control = findLocateSiteControl();
if (!control) {
console.error("Could not find the 'Locate Site' control on this page. Scroll the page to ensure it is visible, then try again.");
return;
}
console.log("Control type:", control.type);
console.log("Collecting facility names (excluding 'Pick to zoom')...");
const facilityNames = await collectAllFacilityNames(control);
if (!facilityNames.length) {
console.error("No facility names discovered in the 'Locate Site' menu.");
return;
}
console.log(`Discovered ${facilityNames.length} facilities.`);
const results = [];
// Simplified headers for R/Python use
results.push([
"Facility",
"Total_Assemblies",
"Total_Tons",
"Op_Date_Min",
"Op_Date_Max",
"Close_Date_Min",
"Close_Date_Max"
]);
let i = 0;
for (const name of facilityNames) {
i++;
console.log(`[${i}/${facilityNames.length}] Selecting: ${name}`);
try {
await selectFacility(control, name);
await sleep(350); // allow UI to render
const details = await getDetailsForCurrentSelection();
results.push([
name,
details.numberOfAssemblies || "",
details.mthm || "",
details.opMin || "",
details.opMax || "",
details.lpdMin || "",
details.lpdMax || ""
]);
} catch (err) {
console.warn(`Failed to collect for "${name}":`, err?.message || err);
results.push([name, "", "", "", "", "", ""]);
}
await sleep(220);
}
console.log("Saving CSV...");
saveCSV(results, "Curie_Spent_Fuel_Site_Totals.csv");
console.log("Done. Check your downloads for Curie_Spent_Fuel_Site_Totals.csv");
})();

View File

@ -0,0 +1,423 @@
(async () => {
// === Curie "Locate Site" scraper → CSV: year,Facility,Assemblies,Tons (stops at 2082) ===
// - Facility from dropdown selection text
// - Assemblies from: <li>Number of Assemblies: N</li> (missing -> 0)
// - Tons from: <li>Metric Tons of Heavy Metal (MTHM): N</li> (missing -> 0)
// - Debounced popup read (waits for popup content change to avoid offset)
// - Dedupes per (year|Facility)
// - Steps year-by-year to 2082 and saves Curie_Spent_Fuel_Timeline.csv
const STOP_YEAR = 2082;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const inViewport = el => {
if (!el || !(el instanceof Element)) return false;
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
// --- Year readout ---
const getCurrentYear = () => {
const el = document.querySelector('#slider-current-year.slider-display');
if (!el) return null;
const t = (el.textContent || '').trim();
const m = t.match(/\b(18|19|20)\d{2}\b/);
return m ? parseInt(m[0], 10) : null;
};
// --- Timeline arrows ---
const clickableSel = [
'button', '[role="button"]', 'a',
'.btn', '.button', '.MuiIconButton-root', '.v-btn',
'.mdc-icon-button', '.ant-btn', '.el-button',
'.mat-icon-button', '.mat-button', '.bp3-button',
'[onclick]', '[ng-click]', '[data-action]', '[data-testid]', '[data-cy]', '[data-qa]'
].join(',');
const findRightBtn = () => {
const icon = Array.from(document.querySelectorAll('i.fa.fa-arrow-right, i.fa-arrow-right, i.fas.fa-arrow-right'))
.find(inViewport);
return icon ? (icon.closest(clickableSel) || icon) : null;
};
const findLeftBtn = () => {
const icon = Array.from(document.querySelectorAll('i.fa.fa-arrow-left, i.fa-arrow-left, i.fas.fa-arrow-left'))
.find(inViewport);
return icon ? (icon.closest(clickableSel) || icon) : null;
};
const waitForYearChangeFrom = async (prevYear, timeoutMs = 4000, pollMs = 120) => {
const start = performance.now();
while (performance.now() - start < timeoutMs) {
const y = getCurrentYear();
if (Number.isInteger(y) && y !== prevYear) return y;
await sleep(pollMs);
}
return getCurrentYear();
};
const stepRightOneYear = async (rightBtn, leftBtn, prevYear) => {
if (!rightBtn) return null;
rightBtn.click();
let y = await waitForYearChangeFrom(prevYear);
if (!Number.isInteger(y) || y === prevYear) {
await sleep(250);
rightBtn.click();
y = await waitForYearChangeFrom(prevYear);
}
// Overshoot correction (> +1)
if (Number.isInteger(y) && y > prevYear + 1 && leftBtn) {
let guard = 0;
while (y > prevYear + 1 && guard < 10) {
leftBtn.click();
const prev = y;
y = await waitForYearChangeFrom(prev, 2500, 120);
if (!Number.isInteger(y)) y = prev - 1;
guard++;
}
if (y > prevYear + 1) y = prevYear + 1;
}
if (!Number.isInteger(y) || y === prevYear) return null;
return y;
};
// --- Popup helpers & parsing (DOM-based li extraction) ---
const popupEl = () => document.querySelector('.leaflet-popup-content');
const popupSignature = () => {
const el = popupEl();
return el ? (el.innerHTML || '').trim() : '';
};
const waitForPopup = async (timeoutMs = 3500, pollMs = 90) => {
const start = performance.now();
while (performance.now() - start < timeoutMs) {
const el = popupEl();
if (el && el.offsetParent !== null) return el;
await sleep(pollMs);
}
return null;
};
const waitForPopupContentChange = async (prevSig, timeoutMs = 4000, pollMs = 90) => {
const start = performance.now();
while (performance.now() - start < timeoutMs) {
const sig = popupSignature();
if (sig && sig !== prevSig) return sig;
await sleep(pollMs);
}
return popupSignature(); // may be unchanged
};
const closePopupIfOpen = () => {
const btn = document.querySelector('.leaflet-popup-close-button');
if (btn) btn.click();
};
// Extract Assemblies and Tons from <li> elements in the popup
const extractFromPopup = (popup) => {
let Assemblies = 0;
let Tons = 0;
if (!popup) return { Assemblies, Tons };
const lis = Array.from(popup.querySelectorAll('li'));
for (const li of lis) {
const t = (li.textContent || '').trim();
if (/^number\s+of\s+assemblies\s*:/i.test(t)) {
const after = t.split(':').slice(1).join(':');
const m = (after || '').replace(/,/g, '').match(/\d+/);
Assemblies = m ? parseInt(m[0], 10) : 0;
}
if (/^metric\s+tons\s+of\s+heavy\s+metal\s*\(mthm\)\s*:/i.test(t)) {
const after = t.split(':').slice(1).join(':');
const m = (after || '').replace(/,/g, '').match(/-?\d+(?:\.\d+)?/);
Tons = m ? parseFloat(m[0]) : 0;
}
}
return {
Assemblies: Number.isFinite(Assemblies) ? Assemblies : 0,
Tons: Number.isFinite(Tons) ? Tons : 0
};
};
// --- Locate Site control discovery (native <select> or ARIA combobox) ---
const isLocateLabel = (txt) => /\blocate\s+site\b/i.test(txt || '');
const findLocateSelect = () => {
// Label + select
for (const lab of Array.from(document.querySelectorAll('label'))) {
if (isLocateLabel(lab.textContent)) {
const id = lab.getAttribute('for');
if (id) {
const sel = document.getElementById(id);
if (sel && sel.tagName.toLowerCase() === 'select' && inViewport(sel)) return sel;
}
const sel2 = lab.closest('*')?.querySelector('select');
if (sel2 && inViewport(sel2)) return sel2;
}
}
// aria-label/title
const sel3 = Array.from(document.querySelectorAll('select[aria-label], select[title]'))
.find(s => isLocateLabel(s.getAttribute('aria-label') || s.getAttribute('title') || ''));
if (sel3 && inViewport(sel3)) return sel3;
// any visible select near text
for (const sel of Array.from(document.querySelectorAll('select'))) {
if (!inViewport(sel)) continue;
const parentText = (sel.closest('*')?.innerText || '').trim();
if (isLocateLabel(parentText)) return sel;
}
return null;
};
const findLocateCombobox = () => {
const byAria = Array.from(document.querySelectorAll('[role="combobox"], input[aria-haspopup="listbox"], [aria-controls]'))
.filter(inViewport)
.find(el => isLocateLabel(el.getAttribute('aria-label') || el.getAttribute('title') || el.textContent || ''));
if (byAria) return byAria;
const containers = Array.from(document.querySelectorAll('*')).filter(inViewport)
.filter(el => isLocateLabel(el.textContent || ''));
for (const c of containers) {
const trigger = c.querySelector('[role="combobox"], input, .select, .dropdown, button');
if (trigger && inViewport(trigger)) return trigger;
}
return null;
};
// Build fresh list of option texts each year (unique, in order)
const getSelectOptionTexts = (sel) => {
const out = [];
for (let i = 0; i < sel.options.length; i++) {
const o = sel.options[i];
const t = (o.text || '').trim();
if (o.disabled) continue;
if (!t) continue;
if (/^select\b|^choose\b|locate site/i.test(t)) continue; // placeholders
out.push(t);
}
return out.filter((t, i) => out.indexOf(t) === i);
};
const openComboAndGetOptionTexts = async (combo) => {
combo.scrollIntoView({ block: 'center' });
combo.click();
const start = performance.now();
let panel = null;
while (performance.now() - start < 1500) {
panel = document.querySelector('[role="listbox"], .select-menu, .dropdown-menu, .menu, .listbox');
if (panel && panel.offsetParent !== null) break;
await sleep(60);
}
if (!panel) return { texts: [] };
const items = Array.from(panel.querySelectorAll('[role="option"], li, .option, .menu-item'))
.filter(el => inViewport(el) && (el.textContent || '').trim());
const texts = items.map(el => el.textContent.trim())
.filter(t => !/^select\b|^choose\b|locate site/i.test(t));
// unique
const unique = texts.filter((t, i) => texts.indexOf(t) === i);
// Close panel to keep a clean state
document.body.click();
await sleep(80);
return { texts: unique };
};
// Select option by exact text
const selectByTextInSelect = async (sel, text) => {
let idx = -1;
for (let i = 0; i < sel.options.length; i++) {
if ((sel.options[i].text || '').trim() === text) { idx = i; break; }
}
if (idx < 0) return false;
sel.selectedIndex = idx;
sel.dispatchEvent(new Event('input', { bubbles: true }));
sel.dispatchEvent(new Event('change', { bubbles: true }));
return true;
};
const openComboAndClickText = async (combo, text) => {
combo.scrollIntoView({ block: 'center' });
combo.click();
const start = performance.now();
let panel = null;
while (performance.now() - start < 1500) {
panel = document.querySelector('[role="listbox"], .select-menu, .dropdown-menu, .menu, .listbox');
if (panel && panel.offsetParent !== null) break;
await sleep(60);
}
if (!panel) return false;
const item = Array.from(panel.querySelectorAll('[role="option"], li, .option, .menu-item'))
.find(el => (el.textContent || '').trim() === text);
if (!item) {
document.body.click();
await sleep(50);
return false;
}
item.scrollIntoView({ block: 'nearest' });
item.click();
await sleep(120);
return true;
};
// --- CSV helpers ---
const csvEscape = v => {
if (v == null) return '';
const s = String(v);
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
};
const downloadCSV = (rows) => {
const header = ['year', 'Facility', 'Assemblies', 'Tons'];
const body = rows.map(r => [r.year, r.Facility, r.Assemblies, r.Tons].map(csvEscape).join(',')).join('\n');
const csv = header.join(',') + '\n' + body;
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Curie_Spent_Fuel_Timeline.csv'; // fixed name
document.body.appendChild(a);
a.click();
a.remove();
};
// --- MAIN (declare once; no redeclarations) ---
const rightBtn = findRightBtn();
const leftBtn = findLeftBtn();
window._curieScrapeStop = false;
let year = getCurrentYear();
if (!Number.isInteger(year)) {
console.warn('Could not read the current year; assuming 1968.');
year = 1968;
}
// Resolve the Locate Site control
const nativeSelect = findLocateSelect();
const comboTrigger = nativeSelect ? null : findLocateCombobox();
if (!nativeSelect && !comboTrigger) {
console.error('Could not find the "Locate Site" dropdown/combobox. Make sure it is visible.');
return;
}
const rows = [];
console.log(`Starting capture via "Locate Site" from ${year} to ${STOP_YEAR} (inclusive).`);
// Capture all facilities for a year (debounced popup + precise li parsing)
const captureYear = async (y) => {
let optionTexts = [];
if (nativeSelect) {
optionTexts = getSelectOptionTexts(nativeSelect);
} else {
const { texts } = await openComboAndGetOptionTexts(comboTrigger);
optionTexts = texts;
}
console.log(`Year ${y}: Locate Site options = ${optionTexts.length}`);
const perYearSeen = new Set(); // (year|Facility)
let prevSig = popupSignature();
// Clean start
closePopupIfOpen();
prevSig = popupSignature();
for (let i = 0; i < optionTexts.length; i++) {
if (window._curieScrapeStop) break;
const Facility = optionTexts[i];
const rowKey = `${y}|${Facility}`;
if (perYearSeen.has(rowKey)) continue;
// Close popup so content change is detectable
closePopupIfOpen();
prevSig = popupSignature();
// Select by text
let ok = false;
if (nativeSelect) {
ok = await selectByTextInSelect(nativeSelect, Facility);
} else {
ok = await openComboAndClickText(comboTrigger, Facility);
}
if (!ok) {
console.warn(` [${y}] Could not select "${Facility}".`);
continue;
}
// Wait for popup & its content to CHANGE vs previous signature
const pop = await waitForPopup(4500, 100);
if (!pop) {
console.warn(` [${y}] No popup after selecting "${Facility}". Skipping.`);
continue;
}
const newSig = await waitForPopupContentChange(prevSig, 4500, 100);
if (!newSig || newSig === prevSig) {
// Retry once
if (nativeSelect) {
await selectByTextInSelect(nativeSelect, Facility);
} else {
await openComboAndClickText(comboTrigger, Facility);
}
const pop2 = await waitForPopup(4500, 100);
if (!pop2) {
console.warn(` [${y}] Popup did not open for "${Facility}" after retry.`);
continue;
}
const newSig2 = await waitForPopupContentChange(prevSig, 4500, 100);
if (!newSig2 || newSig2 === prevSig) {
console.warn(` [${y}] Popup content unchanged for "${Facility}". Skipping to avoid offset.`);
continue;
}
prevSig = newSig2;
} else {
prevSig = newSig;
}
// Parse Assemblies and Tons from the popup's <li> elements
const parsed = extractFromPopup(popupEl());
const Assemblies = parsed.Assemblies || 0;
const Tons = parsed.Tons || 0;
// Record row (dedupe per year|Facility)
if (!perYearSeen.has(rowKey)) {
perYearSeen.add(rowKey);
rows.push({ year: y, Facility, Assemblies, Tons });
}
// Close before next selection
closePopupIfOpen();
if ((i + 1) % 10 === 0) {
console.log(` ${i + 1}/${optionTexts.length} facilities captured...`);
}
}
// Per-year quick summary
const yrRows = rows.filter(r => r.year === y);
const sumTons = yrRows.reduce((a, r) => a + (Number.isFinite(r.Tons) ? r.Tons : 0), 0);
const sumAssemblies = yrRows.reduce((a, r) => a + (Number.isFinite(r.Assemblies) ? r.Assemblies : 0), 0);
console.log(`Year ${y} summary → rows=${yrRows.length}, Sum Assemblies=${sumAssemblies}, Sum Tons=${sumTons.toFixed(3)}`);
};
// Capture starting year
await captureYear(year);
// Advance to 2082 (reuses the SAME rightBtn/leftBtn; no redeclaration)
if (rightBtn) {
while (year < STOP_YEAR) {
if (window._curieScrapeStop) break;
const prev = year;
const next = await stepRightOneYear(rightBtn, leftBtn, prev);
if (!Number.isInteger(next) || next === prev) {
console.warn('Year did not advance; saving partial results and exiting.');
break;
}
year = next;
await sleep(350); // let map/widgets update
await captureYear(year);
}
}
// Download CSV with fixed filename
downloadCSV(rows);
console.log(`Done. CSV downloaded with ${rows.length} rows. Last year captured: ${Math.min(year, STOP_YEAR)}`);
})().catch(e => console.error('[Curie via-locate-site scraper error]', e));