ArthurY's picture
update source
c3d0544
# ignore_header_test
# climt/LICENSE
# @mcgibbon
# BSD License
# Copyright (c) 2016, Rodrigo Caballero
# All rights reserved.
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
import datetime
from typing import TypeVar, Union
import numpy as np
# can replace this import with zoneinfo from the standard library in python3.9+.
import pytz
# helper type
dtype = np.float32
T = TypeVar("T", np.ndarray, float)
TIMESTAMP_2000 = datetime.datetime(2000, 1, 1, 12, 0, tzinfo=pytz.utc).timestamp()
def cos_zenith_angle(
time: Union[T, datetime.datetime],
lon: T,
lat: T,
) -> T: # pragma: no cover
"""
Cosine of sun-zenith angle for lon, lat at time (UTC).
If DataArrays are provided for the lat and lon arguments, their units will
be assumed to be in degrees, unless they have a units attribute that
contains "rad"; in that case they will automatically be converted to having
units of degrees.
Parameters
----------
time: time in UTC
lon: float or np.ndarray in degrees (E/W)
lat: float or np.ndarray in degrees (N/S)
Returns
--------
float, np.ndarray
Example:
--------
>>> model_time = datetime.datetime(2002, 1, 1, 12, 0, 0)
>>> angle = cos_zenith_angle(model_time, lat=360, lon=120)
>>> abs(angle - -0.447817277) < 1e-6
True
"""
lon_rad = np.deg2rad(lon, dtype=dtype)
lat_rad = np.deg2rad(lat, dtype=dtype)
julian_centuries = _datetime_to_julian_century(time)
return _star_cos_zenith(julian_centuries, lon_rad, lat_rad)
def cos_zenith_angle_from_timestamp(
timestamp: T,
lon: T,
lat: T,
) -> T:
"""
Compute cosine of zenith angle using UNIX timestamp
Since the UNIX timestamp is a floating point or integer this routine can be
compiled with jax.
Parameters
----------
timestamp: timestamp in seconds from UNIX epoch
lon: longitude in degrees E
lat: latitude in degrees N
Example:
--------
>>> model_time = datetime.datetime(2002, 1, 1, 12, 0, 0)
>>> angle = cos_zenith_angle_from_timestamp(model_time.timestamp(), lat=360, lon=120)
>>> abs(angle - -0.447817277) < 1e-6
True
"""
lon_rad = np.deg2rad(lon, dtype=dtype)
lat_rad = np.deg2rad(lat, dtype=dtype)
julian_centuries = _timestamp_to_julian_century(timestamp)
return _star_cos_zenith(julian_centuries, lon_rad, lat_rad)
def irradiance(
t,
S0=1361,
e=0.0167,
perihelion_longitude=282.895,
mean_tropical_year=365.2422,
newton_iterations: int = 3,
):
"""The flux of solar energy in W/m2 towards Earth
The default orbital parameters are set to 2000 values.
Over the period of 1900-2100 this will result in an error of at most 0.02%,
so can be neglected for many applications.
Parameters
----------
t: linux timestamp
S0: the solar constant in W/m2. This is the mean irradiance received by
earth over a year.
e: the eccentricity of earths elliptical orbit
perihelion_longitude: spatial angle from moving vernal equinox to perihelion with Sun as angle vertex.
Perihelion is moment when earth is closest to sun. vernal equinox is
the longitude when the Earth crosses the equator from South to North.
newton_iterations: number of iterations for newton solver for elliptic anomaly
Notes
-----
TISR can be computed from Berger's formulas:
Berger, A. (1978). Long-Term Variations of Daily Insolation and
Quaternary Climatic Changes. Journal of the Atmospheric Sciences,
35(12), 2362–2367.
https://doi.org/10.1175/1520-0469(1978)035<2362:LTVODI>2.0.CO;2
NASA Example computing the orbital parameters: https://data.giss.nasa.gov/modelE/ar5plots/srorbpar.html. From 1900-2100 these are the ranges::
Orbital Parmameters
Long. of
Year Eccentri Obliquity Perihel.
(A.D.) city (degrees) (degrees)
------ -------- --------- --------
1900 0.016744 23.4528 281.183
2000 0.016704 23.4398 282.895
2100 0.016663 23.4268 284.609
"""
seconds_per_solar_day = 86400
mean_tropical_year = mean_tropical_year * seconds_per_solar_day
year_2000_equinox = datetime.datetime(2000, 3, 20, 7, 35, tzinfo=pytz.utc)
# from appendix of Berger 1978
M = (t - year_2000_equinox.timestamp()) % mean_tropical_year
M = M / mean_tropical_year * 2 * np.pi
M -= np.deg2rad(perihelion_longitude)
# to get the elliptic anomaly E from the "mean anomaly" M
# use eq. 6.37
# https://link.springer.com/book/10.1007/978-3-662-53045-0)
# r / a = (1 - e cos E )
# E - e sin(E) = M
def f(E):
return E - e * np.sin(E) - M
def fp(E):
return 1 - e * np.cos(E)
# newton iterations
# initial guess
E = M
for _ in range(newton_iterations):
E = E - f(E) / fp(E)
rho = 1 - e * np.cos(E)
return S0 / rho**2
def toa_incident_solar_radiation_accumulated(
t,
lat,
lon,
interval=3600,
S0=1361,
e=0.0167,
perihelion_longitude=282.895,
mean_tropical_year=365.2422,
):
"""Approximate ECMWF TISR with analytical formulas
According to the ECWMF docs, the TISR variable is integrated over the
preceeding hour. Error is about 0.1% different from the ECMWF TISR
variable.
Parameters
----------
t: linux timestamp
lat, lon: latitude and longitude in degrees
interval: the integral length in seconds over which the irradiance is integrated
S0: the solar constant in W/m2. This is the mean irradiance received by
earth over a year.
e: the eccentricity of earths elliptical orbit
perihelion_longitude: spatial angle from moving vernal equinox to perihelion with Sun as angle vertex.
Perihelion is moment when earth is closest to sun. vernal equinox is
the longitude when the Earth crosses the equator from South to North.
Returns
-------
TOA incident solar radiation accumulated from [t-inteval, t] in J/m2
Notes
-----
We make some approximations:
The default orbital parameters are set to 2000 values.
Over the period of 1900-2100 this will result in an error of at most 0.02%,
so can be neglected for many applications.
The irradiance is constant over the ``interval``.
From ECWMF [docs](https://confluence.ecmwf.int/display/CKB/ERA5%3A+data+documentation#ERA5:datadocumentation-Meanrates/fluxesandaccumulations)
Such parameters, which are only available from forecasts, have undergone particular types of statistical processing (temporal mean or accumulation, respectively) over a period of time called the processing period. In addition, these parameters may, or may not, have been averaged in time, to produce monthly means.
The accumulations (over the accumulation/processing period) in the short forecasts (from 06 and 18 UTC) of ERA5 are treated differently compared with those in ERA-Interim and operational data (where the accumulations are from the beginning of the forecast to the validity date/time). In the short forecasts of ERA5, the accumulations are since the previous post processing (archiving), so for:
reanalysis: accumulations are over the hour (the accumulation/processing period) ending at the validity date/time
ensemble: accumulations are over the 3 hours (the accumulation/processing period) ending at the validity date/time
Monthly means (of daily means, stream=moda/edmo): accumulations have been scaled to have an "effective" processing period of one day, see section Monthly means
Mean rate/flux parameters in ERA5 (e.g. Table 4 for surface and single levels) provide similar information to accumulations (e.g. Table 3 for surface and single levels), except they are expressed as temporal means, over the same processing periods, and so have units of "per second".
Mean rate/flux parameters are easier to deal with than accumulations because the units do not vary with the processing period.
The mean rate hydrological parameters (e.g. the "Mean total precipitation rate") have units of "kg m-2 s-1", which are equivalent to "mm s-1". They can be multiplied by 86400 seconds (24 hours) to convert to kg m-2 day-1 or mm day-1.
Note that:
For the CDS time, or validity time, of 00 UTC, the mean rates/fluxes and accumulations are over the hour (3 hours for the EDA) ending at 00 UTC i.e. the mean or accumulation is during part of the previous day.
Mean rates/fluxes and accumulations are not available from the analyses.
Mean rates/fluxes and accumulations at step=0 have values of zero because the length of the processing period is zero.
""" # noqa
lat = np.deg2rad(lat)
lon = np.deg2rad(lon)
century = _timestamp_to_julian_century(t)
ra, dec = _right_ascension_declination(century)
interval_radians = interval / 86400 * 2 * np.pi
# 0 <= h1 < 2 pi
h1 = _local_hour_angle(century, lon, ra)
h0 = h1 - interval_radians
A = np.sin(lat) * np.sin(dec)
B = np.cos(lat) * np.cos(dec)
# assume irradiance is constant over the interval
S = irradiance(t, S0, e, perihelion_longitude, mean_tropical_year)
sec_per_rad = 86400 / (2 * np.pi)
return S * _integrate_abs_cosz(A, B, h0, h1) * sec_per_rad
def _integrate_abs_cosz(A, B, h0, h1):
"""Analytically integrate max(A + B cos(h), 0) from h=h0 to h1"""
hc = np.arccos(-A / B)
def integrate_cosz(left, right):
return A * (right - left) + B * (np.sin(right) - np.sin(left))
def integrate_abs_cosz_from_zero_to(a):
root1 = -hc + 2 * np.pi
T = np.pi * 2
# how many periods
n = a // T
# if there is a root
a = a % T
C = integrate_cosz(0, np.where(a < hc, a, hc))
D = np.where(root1 < a, integrate_cosz(root1, a), 0)
total = integrate_cosz(0, hc) + integrate_cosz(root1, 2 * np.pi)
return C + D + total * n
return np.where(
np.isnan(hc),
np.maximum(integrate_cosz(h0, h1), 0),
integrate_abs_cosz_from_zero_to(h1) - integrate_abs_cosz_from_zero_to(h0),
)
def _datetime_to_julian_century(time: datetime.datetime) -> float:
return _days_from_2000(time) / 36525.0
def _days_from_2000(model_time):
"""Get the days since year 2000.
Example:
--------
>>> model_time = datetime.datetime(2002, 1, 1, 12, 0, 0)
>>> _days_from_2000(model_time)
731.0
"""
if isinstance(model_time, datetime.datetime):
model_time = model_time.replace(tzinfo=pytz.utc)
date_type = type(np.asarray(model_time).ravel()[0])
if date_type not in [datetime.datetime]:
raise ValueError(
f"model_time has an invalid date type. It must be "
f"datetime.datetime. Got {date_type}."
)
return _total_days(model_time - date_type(2000, 1, 1, 12, 0, 0, tzinfo=pytz.utc))
def _total_days(time_diff):
"""
Total time in units of days
"""
return np.asarray(time_diff).astype("timedelta64[us]") / np.timedelta64(1, "D")
def _timestamp_to_julian_century(timestamp):
seconds_in_day = 86400
days_in_julian_century = 36525.0
return (timestamp - TIMESTAMP_2000) / days_in_julian_century / seconds_in_day
def _greenwich_mean_sidereal_time(jul_centuries):
"""
Greenwich mean sidereal time, in radians.
Reference:
The AIAA 2006 implementation:
http://www.celestrak.com/publications/AIAA/2006-6753/
Example:
--------
>>> model_time = datetime.datetime(2002, 1, 1, 12, 0, 0)
>>> c = _timestamp_to_julian_century(model_time.timestamp())
>>> g_time = _greenwich_mean_sidereal_time(c)
>>> abs(g_time - 4.903831411) < 1e-8
True
"""
theta = 67310.54841 + jul_centuries * (
876600 * 3600
+ 8640184.812866
+ jul_centuries * (0.093104 - jul_centuries * 6.2 * 10e-6)
)
theta_radians = np.deg2rad(theta / 240.0) % (2 * np.pi)
return theta_radians
def _local_mean_sidereal_time(julian_centuries, longitude):
"""
Local mean sidereal time. requires longitude in radians.
Ref:
http://www.setileague.org/askdr/lmst.htm
Example:
--------
>>> model_time = datetime.datetime(2002, 1, 1, 12, 0, 0)
>>> c = _timestamp_to_julian_century(model_time.timestamp())
>>> l_time = _local_mean_sidereal_time(c, np.deg2rad(90))
>>> abs(l_time - 6.474627737) < 1e-8
True
"""
return _greenwich_mean_sidereal_time(julian_centuries) + longitude
def _sun_ecliptic_longitude(julian_centuries):
"""
Ecliptic longitude of the sun.
Reference:
http://www.geoastro.de/elevaz/basics/meeus.htm
Example:
--------
>>> model_time = datetime.datetime(2002, 1, 1, 12, 0, 0)
>>> c = _timestamp_to_julian_century(model_time.timestamp())
>>> lon = _sun_ecliptic_longitude(c)
>>> abs(lon - 17.469114444) < 1e-8
True
"""
# mean anomaly calculation
mean_anomaly = np.deg2rad(
357.52910
+ 35999.05030 * julian_centuries
- 0.0001559 * julian_centuries * julian_centuries
- 0.00000048 * julian_centuries * julian_centuries * julian_centuries
)
# mean longitude
mean_longitude = np.deg2rad(
280.46645 + 36000.76983 * julian_centuries + 0.0003032 * (julian_centuries**2)
)
d_l = np.deg2rad(
(1.914600 - 0.004817 * julian_centuries - 0.000014 * (julian_centuries**2))
* np.sin(mean_anomaly)
+ (0.019993 - 0.000101 * julian_centuries) * np.sin(2 * mean_anomaly)
+ 0.000290 * np.sin(3 * mean_anomaly)
)
# true longitude
return mean_longitude + d_l
def _obliquity_star(julian_centuries):
"""
return obliquity of the sun
Use 5th order equation from
https://en.wikipedia.org/wiki/Ecliptic#Obliquity_of_the_ecliptic
Example:
--------
>>> model_time = datetime.datetime(2002, 1, 1, 12, 0, 0)
>>> julian_centuries = _days_from_2000(model_time) / 36525.0
>>> obl = _obliquity_star(julian_centuries)
>>> abs(obl - 0.409088056) < 1e-8
True
"""
return np.deg2rad(
23.0
+ 26.0 / 60
+ 21.406 / 3600.0
- (
46.836769 * julian_centuries
- 0.0001831 * (julian_centuries**2)
+ 0.00200340 * (julian_centuries**3)
- 0.576e-6 * (julian_centuries**4)
- 4.34e-8 * (julian_centuries**5)
)
/ 3600.0
)
def _right_ascension_declination(julian_centuries):
"""
Right ascension and declination of the sun.
Ref:
http://www.geoastro.de/elevaz/basics/meeus.htm
Example:
--------
>>> model_time = datetime.datetime(2002, 1, 1, 12, 0, 0)
>>> c = _timestamp_to_julian_century(model_time.timestamp())
>>> out1, out2 = _right_ascension_declination(c)
>>> abs(out1 - -1.363787213) < 1e-8
True
>>> abs(out2 - -0.401270126) < 1e-8
True
"""
eps = _obliquity_star(julian_centuries)
eclon = _sun_ecliptic_longitude(julian_centuries)
x = np.cos(eclon)
y = np.cos(eps) * np.sin(eclon)
z = np.sin(eps) * np.sin(eclon)
r = np.sqrt(1.0 - z * z)
# sun declination
declination = np.arctan2(z, r)
# right ascension
right_ascension = 2 * np.arctan2(y, (x + r))
return right_ascension, declination
def _local_hour_angle(julian_centuries, longitude, right_ascension):
"""
Hour angle at model_time for the given longitude and right_ascension
longitude in radians
Ref:
https://en.wikipedia.org/wiki/Hour_angle#Relation_with_the_right_ascension
"""
return _local_mean_sidereal_time(julian_centuries, longitude) - right_ascension
def _star_cos_zenith(julian_centuries, lon, lat):
"""
Return cosine of star zenith angle
lon,lat in radians
Ref:
Azimuth:
https://en.wikipedia.org/wiki/Solar_azimuth_angle#Formulas
Zenith:
https://en.wikipedia.org/wiki/Solar_zenith_angle
"""
ra, dec = _right_ascension_declination(julian_centuries)
h_angle = _local_hour_angle(julian_centuries, lon, ra)
cosine_zenith = np.sin(lat) * np.sin(dec) + np.cos(lat) * np.cos(dec) * np.cos(
h_angle
)
return cosine_zenith