Skip to content

Commit bf5e18a

Browse files
committed
add tests for xarray and adapt array module for np.datetime64
1 parent f4cc106 commit bf5e18a

File tree

5 files changed

+285
-87
lines changed

5 files changed

+285
-87
lines changed

src/earthkit/meteo/solar/array/solar.py

Lines changed: 92 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,57 +15,97 @@
1515
DAYS_PER_YEAR = 365.25
1616

1717

18-
def julian_day(date):
19-
"""Day of year as a fractional value.
18+
def _julian_day_scalar(d: datetime.datetime) -> float:
19+
"""Compute fractional day-of-year for a single datetime.
2020
2121
Parameters
2222
----------
23-
date : array-like
24-
Input date.
23+
d : datetime.datetime
24+
Input datetime.
2525
2626
Returns
2727
-------
28-
array-like
29-
Number of days since January 1st of the same year, including
30-
fractional day component based on the time of day.
28+
float
29+
Day-of-year as a fractional value, where January 1st 00:00 is 0.0.
30+
"""
31+
if d.tzinfo is not None and d.tzinfo.utcoffset(d) is not None:
32+
year_start = datetime.datetime(d.year, 1, 1, tzinfo=d.tzinfo)
33+
else:
34+
year_start = datetime.datetime(d.year, 1, 1)
35+
delta = d - year_start
36+
return delta.days + delta.seconds / 86400.0
37+
38+
39+
def julian_day(date) -> float:
40+
"""Compute fractional day-of-year.
41+
42+
Parameters
43+
----------
44+
date : datetime.datetime, numpy.datetime64, or numpy.ndarray
45+
Input date(s). May be a scalar or array of numpy.datetime64 or
46+
datetime.datetime objects.
47+
48+
Returns
49+
-------
50+
float or numpy.ndarray
51+
Fractional day-of-year value(s), where January 1st 00:00 is 0.0.
3152
3253
Notes
3354
-----
34-
This is not the astronomical Julian Day Number. It is the day-of-year
35-
(DOY) expressed as a floating-point value.
55+
This function supports:
56+
57+
- Python ``datetime.datetime`` scalars
58+
- ``numpy.datetime64`` scalars
59+
- numpy arrays of dtype ``datetime64``
60+
61+
This enables compatibility with xarray, which internally represents
62+
time coordinates using numpy.datetime64.
3663
"""
37-
if date.tzinfo is not None and date.tzinfo.utcoffset(date) is not None:
38-
year_start = datetime.datetime(date.year, 1, 1, tzinfo=date.tzinfo)
39-
else:
40-
year_start = datetime.datetime(date.year, 1, 1)
41-
delta = date - year_start
42-
return delta.days + delta.seconds / 86400.0
64+
# numpy.datetime64 scalar
65+
if isinstance(date, np.datetime64):
66+
d_obj = date.astype("datetime64[us]").astype(object)
67+
return _julian_day_scalar(d_obj)
68+
69+
# numpy datetime64 array
70+
if isinstance(date, np.ndarray) and np.issubdtype(date.dtype, np.datetime64):
71+
obj = date.astype("datetime64[us]").astype(object)
72+
return np.vectorize(_julian_day_scalar, otypes=[float])(obj)
4373

74+
# python datetime.datetime
75+
return _julian_day_scalar(date)
4476

45-
def solar_declination_angle(date):
46-
"""Compute solar declination and time correction.
77+
78+
def solar_declination_angle(date) -> tuple[float, float]:
79+
"""Compute solar declination angle and time correction.
4780
4881
Parameters
4982
----------
50-
date : array-like
51-
Input date.
83+
date : datetime.datetime, numpy.datetime64, or numpy.ndarray
84+
Input date(s). May be scalar or array.
5285
5386
Returns
5487
-------
55-
tuple of float
56-
declination : float
57-
Solar declination angle [degrees].
58-
time_correction : float
59-
Time correction factor [hour-degrees].
88+
tuple of float or numpy.ndarray
89+
Tuple containing:
90+
91+
declination : float or numpy.ndarray
92+
Solar declination angle in degrees.
93+
94+
time_correction : float or numpy.ndarray
95+
Time correction factor in hour-degrees.
6096
6197
Notes
6298
-----
6399
Uses a trigonometric approximation based on the fractional year angle.
100+
101+
This function supports numpy.datetime64 inputs, allowing use with xarray
102+
and other numpy-based datetime containers.
103+
104+
Scalar inputs return floats, array inputs return numpy arrays.
64105
"""
65106
angle = julian_day(date) / DAYS_PER_YEAR * np.pi * 2
66107

67-
# declination in [degrees]
68-
declination = float(
108+
declination = (
69109
0.396372
70110
- 22.91327 * np.cos(angle)
71111
+ 4.025430 * np.sin(angle)
@@ -74,62 +114,68 @@ def solar_declination_angle(date):
74114
- 0.154527 * np.cos(3 * angle)
75115
+ 0.084798 * np.sin(3 * angle)
76116
)
77-
# time correction in [ h.degrees ]
78-
time_correction = float(
117+
118+
time_correction = (
79119
0.004297
80120
+ 0.107029 * np.cos(angle)
81121
- 1.837877 * np.sin(angle)
82122
- 0.837378 * np.cos(2 * angle)
83123
- 2.340475 * np.sin(2 * angle)
84124
)
125+
126+
if np.ndim(declination) == 0:
127+
return float(declination), float(time_correction)
128+
85129
return declination, time_correction
86130

87131

88-
def cos_solar_zenith_angle(date, latitudes, longitudes):
89-
"""Cosine of solar zenith angle.
132+
def cos_solar_zenith_angle(
133+
date: datetime.datetime,
134+
latitudes,
135+
longitudes,
136+
):
137+
"""Compute cosine of the solar zenith angle.
90138
91139
Parameters
92140
----------
93-
date: datetime.datetime
94-
Date
95-
latitudes: array-like
96-
Latitude [degrees]
97-
longitudes: array-like
98-
Longitude [degrees]
141+
date : datetime.datetime
142+
Date and time of the observation.
143+
latitudes : array-like
144+
Latitude values in degrees.
145+
longitudes : array-like
146+
Longitude values in degrees.
99147
100148
Returns
101149
-------
102-
float array
103-
Cosine of the solar zenith angle (all values, including negatives)
104-
[Hogan_and_Hirahara2015]_. See also:
105-
http://answers.google.com/answers/threadview/id/782886.html
150+
array-like
151+
Cosine of the solar zenith angle. Negative values are clipped to 0.
152+
153+
Notes
154+
-----
155+
Supports any array type compatible with the Python Array API standard,
156+
including numpy, cupy, and torch tensors.
106157
158+
The result is clipped to ensure physically meaningful values.
107159
"""
108160
xp = array_namespace(latitudes, longitudes)
109161
latitudes = xp.asarray(latitudes)
110162
longitudes = xp.asarray(longitudes)
111163
device = xp.device(latitudes)
112164

113-
# declination angle + time correction for solar angle
114165
declination, time_correction = solar_declination_angle(date)
115166

116167
declination = xp.asarray(declination, device=device)
117168
time_correction = xp.asarray(time_correction, device=device)
118169

119-
# solar_declination_angle returns degrees
120-
# TODO: deg2rad() is not part of the array API standard
121170
declination = xp.deg2rad(declination)
122171
latitudes = xp.deg2rad(latitudes)
123172

124173
sindec_sinlat = xp.sin(declination) * xp.sin(latitudes)
125174
cosdec_coslat = xp.cos(declination) * xp.cos(latitudes)
126175

127-
# solar hour angle [h.deg]
128-
# TODO: deg2rad() is not part of the array API standard
129176
solar_angle = xp.deg2rad((date.hour - 12) * 15 + longitudes + time_correction)
130177
zenith_angle = sindec_sinlat + cosdec_coslat * xp.cos(solar_angle)
131178

132-
# Clip negative values
133179
return xp.clip(zenith_angle, 0.0, None)
134180

135181

0 commit comments

Comments
 (0)