-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add solar intensity based on declination angle (single parameter "latitude") #24
Changes from all commits
69ddd50
722b6f6
68e9f70
7f406e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Additions should go in a new |
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -315,3 +315,100 @@ def get_bearing_summary_stats( | |||||||||||||||
bearing_magnitude_net=round(net_magnitude, 7), | ||||||||||||||||
bearing_magnitude_cum=round(cum_magnitude, 7), | ||||||||||||||||
) | ||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
# def get_solar_intensity( | ||||||||||||||||
# latitudes: list[float] | npt.NDArray[np.float64] | ||||||||||||||||
# ) -> pl.DataFrame: | ||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
HOURS_IN_DAY = 24 | ||||||||||||||||
HOURS_START = 9 # Start time (9 AM) | ||||||||||||||||
HOURS_END = 16 # End time (4 PM) | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you evaluate existing libraries like: For whether they could do these calculations and were suitable dependencies? For calculations like these that are highly technical, I think it's best if we start with existing tooling and only switch to a homegrown implementation if third party libs don't do what we want. Code looks nice and methods appear correct to me, but also I know very little here and there are no references provided for which we can verify the methods. |
||||||||||||||||
|
||||||||||||||||
def solar_declination(day_of_year: int) -> float: | ||||||||||||||||
""" | ||||||||||||||||
Calculate the solar declination for a given day of the year. | ||||||||||||||||
This uses the approximation formula for solar declination. | ||||||||||||||||
""" | ||||||||||||||||
# Calculate the solar declination using the approximation formula | ||||||||||||||||
# δ = 23.44 * sin(360° * (N + 10) / 365) | ||||||||||||||||
# Where N is the day of the year (1 to 365) | ||||||||||||||||
return 23.44 * np.sin(np.radians(360 * (day_of_year + 10) / 365)) | ||||||||||||||||
|
||||||||||||||||
def solar_intensity_at_time(latitude: float, declination: float, hour: int) -> float: | ||||||||||||||||
""" | ||||||||||||||||
Calculate the solar intensity at a given time of day (hour) for a given latitude and solar declination. | ||||||||||||||||
""" | ||||||||||||||||
# Convert latitude and declination to radians for trigonometric calculations | ||||||||||||||||
latitude_rad = np.radians(latitude) | ||||||||||||||||
declination_rad = np.radians(declination) | ||||||||||||||||
|
||||||||||||||||
# Hour angle (ω) for a given time of day (simple model) | ||||||||||||||||
# Hour angle increases by 15 degrees per hour, starting from 12:00 PM. | ||||||||||||||||
hour_angle = np.radians(15 * (hour - 12)) | ||||||||||||||||
|
||||||||||||||||
# Calculate the solar zenith angle using the formula | ||||||||||||||||
# cos(θ) = sin(δ) * sin(φ) + cos(δ) * cos(φ) * cos(ω) | ||||||||||||||||
# Where: | ||||||||||||||||
# δ = solar declination, φ = latitude, ω = hour angle | ||||||||||||||||
solar_zenith = np.arccos( | ||||||||||||||||
np.sin(declination_rad) * np.sin(latitude_rad) + | ||||||||||||||||
np.cos(declination_rad) * np.cos(latitude_rad) * np.cos(hour_angle) | ||||||||||||||||
) | ||||||||||||||||
|
||||||||||||||||
intensity = np.cos(solar_zenith) | ||||||||||||||||
return max(intensity, 0) | ||||||||||||||||
|
||||||||||||||||
def integrate_solar_intensity(latitude: float, day_of_year: int) -> float: | ||||||||||||||||
""" | ||||||||||||||||
Integrate solar intensity from 9 AM to 4 PM. | ||||||||||||||||
The function returns the average solar intensity over this period. | ||||||||||||||||
""" | ||||||||||||||||
declination = solar_declination(day_of_year) | ||||||||||||||||
intensities = [solar_intensity_at_time(latitude, declination, hour) for hour in range(HOURS_START, HOURS_END)] | ||||||||||||||||
|
||||||||||||||||
return np.mean(intensities) | ||||||||||||||||
|
||||||||||||||||
def get_hemisphere(latitude: float) -> Literal["north", "south"] | None: | ||||||||||||||||
if latitude is None: | ||||||||||||||||
return None | ||||||||||||||||
elif latitude >= 0: | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. latitude 0 should return None as per openskistats/openskistats/utils.py Lines 41 to 47 in 3d0e496
and my past research into which hemisphere the equator is in. This will never arise in the data. |
||||||||||||||||
return "north" | ||||||||||||||||
elif latitude < 0: | ||||||||||||||||
return "south" | ||||||||||||||||
Comment on lines
+373
to
+379
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move to utils |
||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
def get_solar_intensity( | ||||||||||||||||
latitude: float | ||||||||||||||||
) -> float: | ||||||||||||||||
""" | ||||||||||||||||
Calculate the average solar intensity from 9 AM to 4 PM for each latitude. | ||||||||||||||||
|
||||||||||||||||
Args: | ||||||||||||||||
latitudes (list or ndarray): A list or numpy array of latitudes. | ||||||||||||||||
hemisphere: lets us know if we are in the northern or southern hemisphere | ||||||||||||||||
|
||||||||||||||||
Returns: | ||||||||||||||||
polars.DataFrame: A DataFrame with latitudes and their corresponding average solar intensity. | ||||||||||||||||
""" | ||||||||||||||||
# For each latitude, calculate the solar intensity (assuming a fixed day of the year, e.g., 172nd day for mid-winter in southern hemisphere) | ||||||||||||||||
# and the 355th day for mid-winter in northern hemisphere | ||||||||||||||||
import logging | ||||||||||||||||
logging.info(f"latitude is {latitude}") | ||||||||||||||||
hemisphere: Literal["north", "south"] | None = None | ||||||||||||||||
hemisphere = get_hemisphere(latitude) | ||||||||||||||||
if hemisphere == "north": | ||||||||||||||||
day_of_year = 355 # Mid-winter day (e.g., December 21) | ||||||||||||||||
elif hemisphere == "south": | ||||||||||||||||
day_of_year = 172 # Mid-winter day (e.g., June 21) | ||||||||||||||||
else: | ||||||||||||||||
logging.info(f"hemisphere has value: {hemisphere}") | ||||||||||||||||
return None | ||||||||||||||||
|
||||||||||||||||
# TODO we could dynamically use the open and close dates of each resort | ||||||||||||||||
avg_intensity = integrate_solar_intensity(latitude, day_of_year) | ||||||||||||||||
return avg_intensity | ||||||||||||||||
|
||||||||||||||||
def get_solar_intensity_batch(latitudes: pl.Series) -> pl.Series: | ||||||||||||||||
return pl.Series([get_solar_intensity(latitude) for latitude in latitudes]) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -100,6 +100,7 @@ def get_ski_area_frontend_table(story: bool = False) -> pl.DataFrame: | |
"country_code", | ||
"region", | ||
"locality", | ||
"solar_intensity", | ||
"latitude", | ||
"run_count", | ||
"lift_count", | ||
|
@@ -204,6 +205,7 @@ def _percent_diverging_style(ci: reactable.CellInfo) -> dict[str, Any] | None: | |
# add custom descriptions including overwriting SkiAreaModel descriptions | ||
"latitude": "Hemisphere where the ski area is located, either ℕ for north or 𝕊 for south, " | ||
"as well as the latitude (φ) in decimal degrees.", | ||
"solar_intensity" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. syntax issue, should be a key: value pair. |
||
"bin_proportion_4_N": "Proportion of vertical-weighted run segments oriented with a northern cardinal direction.", | ||
"bin_proportion_4_E": "Proportion of vertical-weighted run segments oriented with an eastern cardinal direction.", | ||
"bin_proportion_4_S": "Proportion of vertical-weighted run segments oriented with a southern cardinal direction.", | ||
|
@@ -385,6 +387,11 @@ def _ski_area_metric_style(ci: reactable.CellInfo) -> dict[str, Any] | None: | |
filter_method=reactable.JS("filterLatitude"), | ||
min_width=60, | ||
), | ||
reactable.Column( | ||
id="solar_intensity", | ||
name=f"Solar", | ||
**_column_kwargs_location_str, | ||
), | ||
Comment on lines
+390
to
+394
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not add to table yet until we have refined and tested this metric. |
||
reactable.Column( | ||
id="run_count", | ||
name="Runs", | ||
|
@@ -511,7 +518,7 @@ def _ski_area_metric_style(ci: reactable.CellInfo) -> dict[str, Any] | None: | |
cell=reactable.JS("cellRose"), | ||
# max_width=45, | ||
class_="border-left", | ||
), | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the pre-commit formatter would not allow this (and will fix it once enabled) |
||
], | ||
column_groups=[ | ||
reactable.ColGroup( | ||
|
@@ -521,6 +528,7 @@ def _ski_area_metric_style(ci: reactable.CellInfo) -> dict[str, Any] | None: | |
name="Location", | ||
columns=[ | ||
"latitude", | ||
"solar_intensity", | ||
"country_emoji", | ||
"country", | ||
"region", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -252,6 +252,11 @@ class SkiAreaModel(Model): # type: ignore [misc] | |
hemisphere: Literal["north", "south"] | None = Field( | ||
description="Hemisphere of the ski area.", | ||
) | ||
solar_intensity: float | None = Field( | ||
description="Solar intensity of the ski resort calculated by the declination at that latitude at the solstice", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specify which solstice. What does solar intensity mean? Is it insolation scaled in some way? E.g. how does one interpret solar_intensity of 60%? |
||
ge=0.0, | ||
le=1.0, | ||
) | ||
min_elevation: Annotated[ | ||
float | None, | ||
Field( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the type here is wrong?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That looks correct to me, but you may need
returns_scalar=True
. Not entirely sure. Thepolars.Expr.map_batches
docs are kinda confusing.