From 24d5fc6ab42a7ab7090047553989f5c8338cf7ac Mon Sep 17 00:00:00 2001 From: Daniel Himmelstein Date: Sat, 4 Jan 2025 21:18:20 -0500 Subject: [PATCH] irradiation vs irradiance Use solar irradiance when discussing the rate of energy at a specific instant. Use solar irradiation when discussing the cumulative energy over time. --- openskistats/analyze.py | 12 +++++----- openskistats/display.py | 6 ++--- openskistats/models.py | 30 +++++++++++------------ openskistats/sunlight.py | 52 ++++++++++++++++++++-------------------- 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/openskistats/analyze.py b/openskistats/analyze.py index dc2936d8fd..f4079126ac 100644 --- a/openskistats/analyze.py +++ b/openskistats/analyze.py @@ -34,7 +34,7 @@ subplot_orientations, ) from openskistats.sunlight import ( - add_solar_irradiance_columns, + add_solar_irradiation_columns, ) from openskistats.utils import ( get_data_directory, @@ -71,12 +71,12 @@ def get_lifts_parquet_path(testing: bool = False) -> Path: } _aggregate_run_segment_sunlight_exprs = { - "solar_irradiance_season": pl_weighted_mean( - value_col="solar_irradiance_season", + "solar_irradiation_season": pl_weighted_mean( + value_col="solar_irradiation_season", weight_col="distance_vertical_drop", ), - "solar_irradiance_solstice": pl_weighted_mean( - value_col="solar_irradiance_solstice", + "solar_irradiation_solstice": pl_weighted_mean( + value_col="solar_irradiation_solstice", weight_col="distance_vertical_drop", ), } @@ -135,7 +135,7 @@ def process_and_export_runs() -> None: .filter(pl.col("index").is_not_null()) .pipe(add_spatial_metric_columns, partition_by="run_id") .collect() - .pipe(add_solar_irradiance_columns) + .pipe(add_solar_irradiation_columns) .lazy() .select( "run_id", diff --git a/openskistats/display.py b/openskistats/display.py index f11c6ff33e..877f55aca4 100644 --- a/openskistats/display.py +++ b/openskistats/display.py @@ -90,7 +90,7 @@ def get_ski_area_frontend_table(story: bool = False) -> pl.DataFrame: "min_elevation", "max_elevation", "vertical_drop", - "solar_irradiance_season", + "solar_irradiation_season", "bearing_mean", "bearing_alignment", "poleward_affinity", @@ -420,7 +420,7 @@ def _ski_area_metric_style(ci: reactable.CellInfo) -> dict[str, Any] | None: footer=reactable.JS("footerMaxMeters"), ), reactable.Column( - id="solar_irradiance_season", + id="solar_irradiation_season", name="Sunlight", show=not story, format=reactable.ColFormat( @@ -535,7 +535,7 @@ def _ski_area_metric_style(ci: reactable.CellInfo) -> dict[str, Any] | None: "min_elevation", "max_elevation", "vertical_drop", - "solar_irradiance_season", + "solar_irradiation_season", ], ), reactable.ColGroup( diff --git a/openskistats/models.py b/openskistats/models.py index 97c64a6486..a1bdff7690 100644 --- a/openskistats/models.py +++ b/openskistats/models.py @@ -47,8 +47,8 @@ class SkiRunUsage(StrEnum): snow_park = "snow_park" -_solar_irradiance_description = ( - "This value measures solar irradiance by treating each run segment as a plane according to its latitude, longitude, elevation, bearing, and slope. " +_solar_irradiation_description = ( + "This value measures solar irradiation by treating each run segment as a plane according to its latitude, longitude, elevation, bearing, and slope. " "Solar irradiance is computed using clear sky estimates for diffuse normal irradiance, global horizontal irradiance, and direct horizontal irradiance according to the Ineichen and Perez model with Linke turbidity.", ) @@ -124,27 +124,27 @@ class RunSegmentModel(Model): # type: ignore [misc] description="Slope of the segment from the previous coordinate to the current coordinate in degrees." ), ] - solar_irradiance_season: Annotated[ + solar_irradiation_season: Annotated[ float | None, Field( ge=0, lt=15, # practical limit - description=f"Average daily solar irradiance received by the segment over the course of a typical 115 ski season in kilowatt-hours per square meter (kW/m²/day). {_solar_irradiance_description}", + description=f"Average daily solar irradiation received by the segment over the course of a typical 115 ski season in kilowatt-hours per square meter (kW/m²/day). {_solar_irradiation_description}", ), ] - solar_irradiance_solstice: Annotated[ + solar_irradiation_solstice: Annotated[ float | None, Field( ge=0, lt=15, # practical limit - description="Solar irradiance received by the segment on the winter solstice in kilowatt-hours per square meter (kW/m²/day).", + description="Solar irradiation received by the segment on the winter solstice in kilowatt-hours per square meter (kW/m²/day).", ), ] - solar_irradiance_cache_version: Annotated[ + solar_cache_version: Annotated[ int | None, Field( - description="Version of the solar irradiance calculation and parameters used to compute the solar irradiance values. " - "This version can be incremented in the source code to invalidate cached solar irradiance values.", + description="Version of the solar irradiation calculation and parameters used to compute the solar irradiation values. " + "This version can be incremented in the source code to invalidate cached solar irradiation values.", ), ] @@ -431,20 +431,20 @@ class SkiAreaModel(Model): # type: ignore [misc] description="Peak elevation of the ski area in meters computed as the highest elevation along all runs.", ), ] - solar_irradiance_season: Annotated[ + solar_irradiation_season: Annotated[ float | None, Field( - description="Average daily solar irradiance received by run segments over the course of a typical 120 ski season in kilowatt-hours per square meter (kW/m²/day). " + description="Average daily solar irradiation received by run segments over the course of a typical 120 ski season in kilowatt-hours per square meter (kW/m²/day). " "The average is weighted by the vertical drop of each segment. " - f"{_solar_irradiance_description}", + f"{_solar_irradiation_description}", ), ] - solar_irradiance_solstice: Annotated[ + solar_irradiation_solstice: Annotated[ float | None, Field( - description="Average daily solar irradiance received by run segments on the winter solstice in kilowatt-hours per square meter (kW/m²/day). " + description="Average daily solar irradiation received by run segments on the winter solstice in kilowatt-hours per square meter (kW/m²/day). " "The average is weighted by the vertical drop of each segment. " - f"{_solar_irradiance_description}", + f"{_solar_irradiation_description}", ), ] for field_name in BearingStatsModel.model_fields: diff --git a/openskistats/sunlight.py b/openskistats/sunlight.py index 8e3f38f373..664ce1f883 100644 --- a/openskistats/sunlight.py +++ b/openskistats/sunlight.py @@ -19,7 +19,7 @@ running_in_test, ) -SOLAR_IRRADIANCE_CACHE_VERSION = 1 +SOLAR_CACHE_VERSION = 1 """ Increment this version number to invalidate the solar irradiance cache. """ @@ -78,12 +78,14 @@ def compute_solar_irradiance( def collapse_solar_irradiance( irrad_df: pl.DataFrame, ) -> dict[str, float]: - def get_mean_irradiance(df: pl.DataFrame) -> float: + """Aggregate solar irradiance to solar irradiation.""" + + def get_irradiation(df: pl.DataFrame) -> float: return 24 * float(df["poa_global"].mean()) / 1_000 return { - "solar_irradiance_season": get_mean_irradiance(irrad_df), - "solar_irradiance_solstice": get_mean_irradiance( + "solar_irradiation_season": get_irradiation(irrad_df), + "solar_irradiation_solstice": get_irradiation( irrad_df.filter(pl.col("is_solstice")) ), } @@ -200,7 +202,7 @@ def write_dartmouth_skiway_solar_irradiance() -> pl.DataFrame: return skiway_df -def add_solar_irradiance_columns( +def add_solar_irradiation_columns( run_segments: pl.DataFrame, skip_cache: bool = False, max_items: int | None = int( @@ -210,13 +212,13 @@ def add_solar_irradiance_columns( """ Adds three columns to a run coordinate/segment DataFrame: - - solar_irradiance_cache_version - - solar_irradiance_season - - solar_irradiance_solstice + - solar_cache_version + - solar_irradiation_season + - solar_irradiation_solstice Unless clear_cache is True, a lookup of prior results is attempted because the computation is quite slow. """ - segments_cached = load_solar_irradiance_cache_pl(skip_cache=skip_cache) + segments_cached = load_solar_irradiation_cache_pl(skip_cache=skip_cache) n_segments = run_segments["segment_hash"].drop_nulls().n_unique() segments_to_compute = ( run_segments @@ -234,12 +236,12 @@ def add_solar_irradiance_columns( .to_dicts() ) logging.info( - f"Solar irradiance requested for {n_segments:,} segments, {len(segments_to_compute):,} segments not in cache." + f"Solar irradiation requested for {n_segments:,} segments, {len(segments_to_compute):,} segments not in cache." ) if max_items is not None: segments_to_compute = segments_to_compute[:max_items] logging.info( - f"Computing solar irradiance for {len(segments_to_compute):,} segments after limiting to {max_items=}." + f"Computing solar irradiation for {len(segments_to_compute):,} segments after limiting to {max_items=}." ) def _process_segment(segment: dict[str, Any]) -> dict[str, float]: @@ -247,7 +249,7 @@ def _process_segment(segment: dict[str, Any]) -> dict[str, float]: result = compute_solar_irradiance(**segment).pipe(collapse_solar_irradiance) assert isinstance(result, dict) result["segment_hash"] = segment_hash - result["solar_irradiance_cache_version"] = SOLAR_IRRADIANCE_CACHE_VERSION + result["solar_cache_version"] = SOLAR_CACHE_VERSION return result start_time = perf_counter() @@ -255,11 +257,11 @@ def _process_segment(segment: dict[str, Any]) -> dict[str, float]: results = list(executor.map(_process_segment, segments_to_compute)) total_time = perf_counter() - start_time logging.info( - f"Computed solar irradiance for {len(segments_to_compute):,} segments in {total_time / 60:.1f} minutes: " + f"Computed solar irradiation for {len(segments_to_compute):,} segments in {total_time / 60:.1f} minutes: " f"{total_time / len(segments_to_compute):.4f} seconds per segment." ) segments_computed = pl.DataFrame( - data=results, schema=_get_solar_irradiance_cache_schema() + data=results, schema=_get_solar_irradiation_cache_schema() ) return run_segments.join( pl.concat([segments_cached, segments_computed]), @@ -268,12 +270,12 @@ def _process_segment(segment: dict[str, Any]) -> dict[str, float]: ) -def _get_solar_irradiance_cache_schema() -> dict[str, pl.DataType]: +def _get_solar_irradiation_cache_schema() -> dict[str, pl.DataType]: return { "segment_hash": pl.UInt64, - "solar_irradiance_cache_version": pl.UInt8, - "solar_irradiance_season": pl.Float32, - "solar_irradiance_solstice": pl.Float32, + "solar_cache_version": pl.UInt8, + "solar_irradiation_season": pl.Float32, + "solar_irradiation_solstice": pl.Float32, } @@ -290,12 +292,12 @@ def _get_runs_cache_path(skip_cache: bool = False) -> str | None | Path: return local_path -def load_solar_irradiance_cache_pl(skip_cache: bool = False) -> pl.DataFrame: +def load_solar_irradiation_cache_pl(skip_cache: bool = False) -> pl.DataFrame: path = _get_runs_cache_path(skip_cache=skip_cache) if not path: return pl.DataFrame( data=[], - schema=_get_solar_irradiance_cache_schema(), + schema=_get_solar_irradiation_cache_schema(), ) return ( pl.scan_parquet(source=path) @@ -303,14 +305,12 @@ def load_solar_irradiance_cache_pl(skip_cache: bool = False) -> pl.DataFrame: .explode("run_coordinates_clean") .unnest("run_coordinates_clean") .filter(pl.col("segment_hash").is_not_null()) - .filter( - pl.col("solar_irradiance_cache_version") == SOLAR_IRRADIANCE_CACHE_VERSION - ) + .filter(pl.col("solar_cache_version") == SOLAR_CACHE_VERSION) .select( "segment_hash", - "solar_irradiance_cache_version", - "solar_irradiance_season", - "solar_irradiance_solstice", + "solar_cache_version", + "solar_irradiation_season", + "solar_irradiation_solstice", ) .unique(subset=["segment_hash"]) .collect()