#!/usr/bin/env python3 """ Build representative county points for NSRDB point-based solar sampling. The output CSV is intended as input to fetch_nsrdb_representative_point_ghi.py. """ from __future__ import annotations import argparse import csv from pathlib import Path import geopandas as gpd DEFAULT_COUNTIES_GEOJSON = Path("data/geojson-counties-fips.json") DEFAULT_OUTPUT_CSV = Path("data/nrel/county_representative_points.csv") STATE_FIPS_TO_ABBR = { "01": "AL", "02": "AK", "04": "AZ", "05": "AR", "06": "CA", "08": "CO", "09": "CT", "10": "DE", "11": "DC", "12": "FL", "13": "GA", "15": "HI", "16": "ID", "17": "IL", "18": "IN", "19": "IA", "20": "KS", "21": "KY", "22": "LA", "23": "ME", "24": "MD", "25": "MA", "26": "MI", "27": "MN", "28": "MS", "29": "MO", "30": "MT", "31": "NE", "32": "NV", "33": "NH", "34": "NJ", "35": "NM", "36": "NY", "37": "NC", "38": "ND", "39": "OH", "40": "OK", "41": "OR", "42": "PA", "44": "RI", "45": "SC", "46": "SD", "47": "TN", "48": "TX", "49": "UT", "50": "VT", "51": "VA", "53": "WA", "54": "WV", "55": "WI", "56": "WY", "60": "AS", "66": "GU", "69": "MP", "72": "PR", "78": "VI", } def normalize_fips(value: object, width: int) -> str: """Return a zero-padded FIPS string from a mixed text or numeric value.""" digits = "".join(character for character in str(value).strip() if character.isdigit()) return digits.zfill(width)[-width:] if digits else "" def build_county_points(input_geojson: Path, include_puerto_rico: bool) -> list[dict[str, str]]: """Load county polygons and return one interior representative point per county.""" counties = gpd.read_file(input_geojson) rows: list[dict[str, str]] = [] for _, county in counties.iterrows(): state_fips = normalize_fips(county.get("STATE"), 2) county_code = normalize_fips(county.get("COUNTY"), 3) county_fips = normalize_fips(county.get("id") or f"{state_fips}{county_code}", 5) if not county_fips or (state_fips == "72" and not include_puerto_rico): continue point = county.geometry.representative_point() county_name = str(county.get("NAME") or f"County {county_fips}").strip() rows.append( { "county_fips": county_fips, "county_name": county_name, "state_fips": state_fips, "state_abbr": STATE_FIPS_TO_ABBR.get(state_fips, state_fips), "lat": f"{point.y:.6f}", "lon": f"{point.x:.6f}", } ) return sorted(rows, key=lambda row: row["county_fips"]) def write_points_csv(rows: list[dict[str, str]], output_csv: Path) -> None: """Write county representative points to CSV.""" output_csv.parent.mkdir(parents=True, exist_ok=True) with output_csv.open("w", newline="", encoding="utf-8") as handle: writer = csv.DictWriter( handle, fieldnames=["county_fips", "county_name", "state_fips", "state_abbr", "lat", "lon"], ) writer.writeheader() writer.writerows(rows) def parse_args() -> argparse.Namespace: """Parse command-line arguments.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--input-geojson", type=Path, default=DEFAULT_COUNTIES_GEOJSON) parser.add_argument("--output-csv", type=Path, default=DEFAULT_OUTPUT_CSV) parser.add_argument( "--include-puerto-rico", action="store_true", help="Include Puerto Rico counties. The current web app excludes Puerto Rico polygons.", ) return parser.parse_args() def main() -> None: """Build and write the county representative-point CSV.""" args = parse_args() rows = build_county_points(args.input_geojson, args.include_puerto_rico) write_points_csv(rows, args.output_csv) print(f"Wrote {len(rows)} county representative points to {args.output_csv}") if __name__ == "__main__": main()