#!/usr/bin/env python3 """ Rebuild county representative-point GHI summaries from cached NSRDB raw CSV responses. This does not call the NSRDB API. It reads data/nrel/representative_point_csv/*.csv, computes: avgSolarGhiKwhM2Day = sum(hourly GHI) / 1000 / 365 and writes a county-keyed CSV compatible with build_county_climate_data.py. """ from __future__ import annotations import argparse import csv from pathlib import Path DEFAULT_POINTS_CSV = Path("data/nrel/county_representative_points.csv") DEFAULT_REPRESENTATIVE_POINT_CSV_DIR = Path("data/nrel/representative_point_csv") DEFAULT_OUTPUT_CSV = Path("data/nrel/county_representative_point_ghi_summary.csv") def read_county_points(points_csv: Path) -> list[dict[str, str]]: """Read county representative points keyed by county FIPS.""" with points_csv.open(newline="", encoding="utf-8") as handle: return list(csv.DictReader(handle)) def extract_ghi_values(csv_path: Path) -> list[float]: """Extract numeric hourly GHI values from a cached NSRDB CSV response.""" with csv_path.open(newline="", encoding="utf-8-sig") as handle: reader = csv.reader(handle) ghi_index: int | None = None values: list[float] = [] for row in reader: if ghi_index is None: if {"Year", "Month", "Day", "Hour", "Minute", "GHI"}.issubset(set(row)): ghi_index = row.index("GHI") continue if len(row) <= ghi_index or not row[ghi_index].strip(): continue values.append(float(row[ghi_index])) if ghi_index is None: raise ValueError(f"Could not find NSRDB data header with a GHI column in {csv_path}.") if not values: raise ValueError(f"No GHI values were found in {csv_path}.") return values def select_raw_csv(representative_point_csv_dir: Path, county_fips: str) -> Path | None: """Return the cached raw CSV for a county, preferring polar data when both exist.""" matches = sorted(representative_point_csv_dir.glob(f"{county_fips}_*_ghi.csv")) if not matches: return None polar_matches = [path for path in matches if "_polar-tmy_" in path.name] return polar_matches[0] if polar_matches else matches[0] def source_label(raw_csv: Path) -> str: """Return a readable source label from the cached filename.""" if "_polar-tmy_" in raw_csv.name: return "NSRDB Polar TMY PSM v4 representative point" if "_goes-tmy_" in raw_csv.name: return "NSRDB GOES TMY PSM v4 representative point" return "NSRDB TMY representative point" def summarize_county(point: dict[str, str], raw_csv: Path) -> dict[str, str]: """Summarize one county raw CSV into the app solar metric.""" ghi_values = extract_ghi_values(raw_csv) avg_daily_ghi = sum(ghi_values) / 1000 / 365 return { "county_fips": point["county_fips"], "county_name": point["county_name"], "state_fips": point["state_fips"], "state_abbr": point["state_abbr"], "lat": point["lat"], "lon": point["lon"], "avgSolarGhiKwhM2Day": f"{avg_daily_ghi:.3f}", "ghi_rows": str(len(ghi_values)), "ghi_min": f"{min(ghi_values):.1f}", "ghi_max": f"{max(ghi_values):.1f}", "source": source_label(raw_csv), "raw_csv": str(raw_csv), } def write_summary_csv(rows: list[dict[str, str]], output_csv: Path) -> None: """Write rebuilt county solar summaries.""" 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", "avgSolarGhiKwhM2Day", "ghi_rows", "ghi_min", "ghi_max", "source", "raw_csv", ], ) writer.writeheader() writer.writerows(rows) def parse_args() -> argparse.Namespace: """Parse command-line arguments.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--points-csv", type=Path, default=DEFAULT_POINTS_CSV) parser.add_argument( "--representative-point-csv-dir", dest="representative_point_csv_dir", type=Path, default=DEFAULT_REPRESENTATIVE_POINT_CSV_DIR, ) parser.add_argument("--output-csv", type=Path, default=DEFAULT_OUTPUT_CSV) return parser.parse_args() def main() -> None: """Run the cached raw CSV summarizer.""" args = parse_args() rows: list[dict[str, str]] = [] missing: list[str] = [] failed: list[str] = [] for point in read_county_points(args.points_csv): county_fips = point["county_fips"] raw_csv = select_raw_csv(args.representative_point_csv_dir, county_fips) if raw_csv is None: missing.append(county_fips) continue try: rows.append(summarize_county(point, raw_csv)) except (OSError, ValueError) as exc: failed.append(f"{county_fips}: {exc}") write_summary_csv(rows, args.output_csv) print(f"Wrote {len(rows)} county representative-point solar summaries to {args.output_csv}") if missing: print(f"Missing raw CSVs for {len(missing)} counties: {', '.join(missing[:20])}") if failed: print(f"Failed to summarize {len(failed)} counties:") for message in failed[:20]: print(f" {message}") if __name__ == "__main__": main()