#!/usr/bin/env python3 """ Merge NSRDB cloudiness metrics into climate-data.csv. Adds: - cloudinessIndexPct Polygon area-weighted values are used first when available; representative-point values remain the fallback. """ from __future__ import annotations import argparse import csv from pathlib import Path DEFAULT_CLIMATE_DATA = Path("data/climate-data.csv") DEFAULT_POLYGON_CLOUD_SUMMARY = Path("data/nrel/county_polygon_cloud_summary.csv") DEFAULT_REPRESENTATIVE_POINT_CLOUD_SUMMARY = Path("data/nrel/county_representative_point_cloud_summary.csv") METRIC_FIELD = "cloudinessIndexPct" POLYGON_SOURCE_TAG = "nsrdb-polygon-area-weighted-cloudiness-tmy" REPRESENTATIVE_POINT_SOURCE_TAG = "nsrdb-representative-point-cloudiness-tmy" CLOUD_SOURCE_TAGS = { POLYGON_SOURCE_TAG, REPRESENTATIVE_POINT_SOURCE_TAG, } def load_cloud_values(path: Path) -> dict[str, str]: """Read cloudiness values keyed by county FIPS.""" values: dict[str, str] = {} with path.open("r", encoding="utf-8-sig", newline="") as handle: reader = csv.DictReader(handle) fieldnames = reader.fieldnames or [] required_fields = {"county_fips", METRIC_FIELD} missing_fields = sorted(required_fields - set(fieldnames)) if missing_fields: raise ValueError(f"{path} is missing fields: {', '.join(missing_fields)}") for row in reader: county_fips = (row.get("county_fips") or "").strip().zfill(5) value = (row.get(METRIC_FIELD) or "").strip() if county_fips and value: values[county_fips] = value return values def append_source_tag(source: str, tag: str) -> str: """Append a source tag if it is not already present.""" parts = [ part.strip() for part in source.split("+") if part.strip() and part.strip() not in CLOUD_SOURCE_TAGS ] if tag not in parts: parts.append(tag) return " + ".join(parts) def ensure_field_after(fieldnames: list[str], field: str, after_field: str | None = None) -> None: """Insert a field into CSV field order if it is missing.""" if field in fieldnames: return if after_field and after_field in fieldnames: fieldnames.insert(fieldnames.index(after_field) + 1, field) return insert_at = fieldnames.index("source") if "source" in fieldnames else len(fieldnames) fieldnames.insert(insert_at, field) def merge_metric( climate_data: Path, polygon_cloud_summary: Path, representative_point_cloud_summary: Path, ) -> tuple[int, int, int, int]: """Merge cloudiness values into the app climate CSV.""" polygon_cloudiness_by_fips = ( load_cloud_values(polygon_cloud_summary) if polygon_cloud_summary.exists() else {} ) representative_cloudiness_by_fips = load_cloud_values(representative_point_cloud_summary) with climate_data.open("r", encoding="utf-8", newline="") as handle: reader = csv.DictReader(handle) fieldnames = list(reader.fieldnames or []) rows = list(reader) if "countyFips" not in fieldnames: raise ValueError(f"{climate_data} is missing countyFips") ensure_field_after(fieldnames, METRIC_FIELD, "avgSolarGhiKwhM2Day") polygon_count = 0 representative_count = 0 missing_count = 0 for row in rows: county_fips = (row.get("countyFips") or "").strip().zfill(5) polygon_value = polygon_cloudiness_by_fips.get(county_fips, "") representative_value = representative_cloudiness_by_fips.get(county_fips, "") value = polygon_value or representative_value row[METRIC_FIELD] = value if polygon_value: row["source"] = append_source_tag(row.get("source", ""), POLYGON_SOURCE_TAG) polygon_count += 1 elif representative_value: row["source"] = append_source_tag(row.get("source", ""), REPRESENTATIVE_POINT_SOURCE_TAG) representative_count += 1 else: missing_count += 1 with climate_data.open("w", encoding="utf-8", newline="") as handle: writer = csv.DictWriter(handle, fieldnames=fieldnames) writer.writeheader() writer.writerows(rows) return len(rows), polygon_count, representative_count, missing_count def main() -> int: parser = argparse.ArgumentParser(description="Merge NSRDB cloudiness metric into climate-data.csv.") parser.add_argument("--climate-data", type=Path, default=DEFAULT_CLIMATE_DATA) parser.add_argument("--polygon-cloud-summary", type=Path, default=DEFAULT_POLYGON_CLOUD_SUMMARY) parser.add_argument( "--representative-point-cloud-summary", type=Path, default=DEFAULT_REPRESENTATIVE_POINT_CLOUD_SUMMARY, ) args = parser.parse_args() row_count, polygon_count, representative_count, missing_count = merge_metric( args.climate_data, args.polygon_cloud_summary, args.representative_point_cloud_summary, ) print(f"Updated {args.climate_data} with {METRIC_FIELD}") print(f"Rows: {row_count}") print(f"Polygon area-weighted rows: {polygon_count}") print(f"Representative-point fallback rows: {representative_count}") print(f"Missing {METRIC_FIELD}: {missing_count}") return 0 if __name__ == "__main__": raise SystemExit(main())