|
| 1 | +# Type hinting for flexible input types |
| 2 | +from typing import Union |
| 3 | +import numpy as np |
| 4 | +from rasters import Raster, SpatialGeometry |
| 5 | +from datetime import datetime |
| 6 | +from dateutil import parser |
| 7 | +import pandas as pd |
| 8 | + |
| 9 | +from solar_apparent_time import solar_day_of_year_for_longitude |
| 10 | + |
| 11 | +from .SHA_deg_from_DOY_lat import SHA_deg_from_DOY_lat |
| 12 | +from .daylight_from_SHA import daylight_from_SHA |
| 13 | + |
| 14 | +def calculate_daylight( |
| 15 | + time_UTC: Union[datetime, str, list, np.ndarray] = None, |
| 16 | + geometry: SpatialGeometry = None, |
| 17 | + day_of_year: Union[Raster, np.ndarray, int] = None, |
| 18 | + lat: Union[Raster, np.ndarray, float] = None, |
| 19 | + lon: Union[Raster, np.ndarray, float] = None, |
| 20 | + SHA_deg: Union[Raster, np.ndarray, float] = None |
| 21 | +) -> Union[Raster, np.ndarray]: |
| 22 | + """ |
| 23 | + Calculate the number of daylight hours for a given day and location. |
| 24 | +
|
| 25 | + This function flexibly computes daylight hours based on the information provided: |
| 26 | +
|
| 27 | + - If `SHA_deg` (sunrise hour angle in degrees) is provided, it is used directly to compute daylight hours. |
| 28 | + - If `SHA_deg` is not provided, it is calculated from `day_of_year` and `lat` (latitude in degrees). |
| 29 | + - If `lat` is not provided but a `geometry` object is given, latitude is extracted from `geometry.lat`. |
| 30 | + - If `day_of_year` is not provided but `time_UTC` is given, the function converts the UTC time(s) to day of year, accounting for longitude if available. |
| 31 | +
|
| 32 | + Parameters |
| 33 | + ---------- |
| 34 | + time_UTC : Union[datetime, str, list, np.ndarray], optional |
| 35 | + Datetime(s) in UTC. Used to determine `day_of_year` if not provided. Accepts a single datetime, a string, or an array/list of datetimes or strings. |
| 36 | + Example: `time_UTC=['2025-06-21', '2025-12-21']` or `time_UTC=datetime(2025, 6, 21)` |
| 37 | + geometry : SpatialGeometry, optional |
| 38 | + Geometry object containing latitude (and optionally longitude) information. Used if `lat` or `lon` are not provided. |
| 39 | + day_of_year : Union[Raster, np.ndarray, int], optional |
| 40 | + Day of year (1-366). Can be a Raster, numpy array, or integer. If not provided, will be inferred from `time_UTC` if available. |
| 41 | + lat : Union[Raster, np.ndarray, float], optional |
| 42 | + Latitude in degrees. Can be a Raster, numpy array, or float. If not provided, will be inferred from `geometry` if available. |
| 43 | + lon : Union[Raster, np.ndarray, float], optional |
| 44 | + Longitude in degrees. Can be a Raster, numpy array, or float. If not provided, will be inferred from `geometry` if available. |
| 45 | + SHA_deg : Union[Raster, np.ndarray, float], optional |
| 46 | + Sunrise hour angle in degrees. If provided, it is used directly and other parameters are ignored. |
| 47 | +
|
| 48 | + Returns |
| 49 | + ------- |
| 50 | + Union[Raster, np.ndarray] |
| 51 | + Daylight hours for the given inputs. The output type matches the input type (e.g., float, array, or Raster). |
| 52 | +
|
| 53 | + Examples |
| 54 | + -------- |
| 55 | + 1. Provide SHA directly: |
| 56 | + daylight = calculate_daylight(SHA_deg=100) |
| 57 | + 2. Provide day of year and latitude: |
| 58 | + daylight = calculate_daylight(day_of_year=172, lat=34.0) |
| 59 | + 3. Provide UTC time and latitude: |
| 60 | + daylight = calculate_daylight(time_UTC='2025-06-21', lat=34.0) |
| 61 | + 4. Provide geometry and UTC time: |
| 62 | + daylight = calculate_daylight(time_UTC='2025-06-21', geometry=my_geometry) |
| 63 | + """ |
| 64 | + |
| 65 | + # If SHA_deg is not provided, calculate it from DOY and latitude |
| 66 | + if SHA_deg is None: |
| 67 | + # If latitude is not provided, try to extract from geometry |
| 68 | + if lat is None and geometry is not None: |
| 69 | + lat = geometry.lat |
| 70 | + |
| 71 | + if lon is None and geometry is not None: |
| 72 | + lon = geometry.lon |
| 73 | + |
| 74 | + # Handle day_of_year input: convert lists to np.ndarray |
| 75 | + if day_of_year is not None: |
| 76 | + if isinstance(day_of_year, list): |
| 77 | + day_of_year = np.array(day_of_year) |
| 78 | + |
| 79 | + # Handle lat input: convert lists to np.ndarray |
| 80 | + if lat is not None: |
| 81 | + if isinstance(lat, list): |
| 82 | + lat = np.array(lat) |
| 83 | + |
| 84 | + # If day_of_year is not provided, try to infer from time_UTC |
| 85 | + if day_of_year is None: |
| 86 | + # Handle string or list of strings for time_UTC |
| 87 | + if isinstance(time_UTC, str): |
| 88 | + time_UTC = parser.parse(time_UTC) |
| 89 | + elif isinstance(time_UTC, list): |
| 90 | + time_UTC = [parser.parse(t) if isinstance(t, str) else t for t in time_UTC] |
| 91 | + elif isinstance(time_UTC, np.ndarray) and time_UTC.dtype.type is np.str_: |
| 92 | + time_UTC = np.array([parser.parse(t) for t in time_UTC]) |
| 93 | + |
| 94 | + # If lon is None, raise a clear error |
| 95 | + if lon is None: |
| 96 | + raise ValueError("Longitude (lon) must be provided when using time_UTC to infer day_of_year.") |
| 97 | + |
| 98 | + day_of_year = solar_day_of_year_for_longitude( |
| 99 | + time_UTC=time_UTC, |
| 100 | + lon=lon |
| 101 | + ) |
| 102 | + |
| 103 | + SHA_deg = SHA_deg_from_DOY_lat(day_of_year, lat) |
| 104 | + |
| 105 | + # Compute daylight hours from SHA_deg |
| 106 | + daylight_hours = daylight_from_SHA(SHA_deg) |
| 107 | + |
| 108 | + return daylight_hours |
0 commit comments