ETo 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {eto-2.1.0 → eto-2.2.0}/PKG-INFO +2 -2
- {eto-2.1.0 → eto-2.2.0}/eto/__init__.py +1 -1
- {eto-2.1.0 → eto-2.2.0}/eto/core.py +9 -5
- {eto-2.1.0 → eto-2.2.0}/eto/methods/ETo.py +15 -9
- {eto-2.1.0 → eto-2.2.0}/eto/methods/hargreaves.py +2 -1
- {eto-2.1.0 → eto-2.2.0}/eto/param_est.py +85 -20
- eto-2.2.0/eto/tests/test_hourly_fao.py +245 -0
- eto-2.2.0/eto/util.py +28 -0
- eto-2.1.0/eto/util.py +0 -4
- {eto-2.1.0 → eto-2.2.0}/.gitignore +0 -0
- {eto-2.1.0 → eto-2.2.0}/LICENSE.txt +0 -0
- {eto-2.1.0 → eto-2.2.0}/README.md +0 -0
- {eto-2.1.0 → eto-2.2.0}/eto/crop_coefficients.py +0 -0
- {eto-2.1.0 → eto-2.2.0}/eto/datasets/__init__.py +0 -0
- {eto-2.1.0 → eto-2.2.0}/eto/datasets/example_daily.csv.zip +0 -0
- {eto-2.1.0 → eto-2.2.0}/eto/datasets/example_daily_results.csv.zip +0 -0
- {eto-2.1.0 → eto-2.2.0}/eto/methods/__init__.py +0 -0
- {eto-2.1.0 → eto-2.2.0}/eto/methods/dual_kc.py +0 -0
- {eto-2.1.0 → eto-2.2.0}/eto/tests/__init__.py +0 -0
- {eto-2.1.0 → eto-2.2.0}/eto/tests/test_eto.py +0 -0
- {eto-2.1.0 → eto-2.2.0}/pyproject.toml +0 -0
{eto-2.1.0 → eto-2.2.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: ETo
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Class and functions to estimate reference ET
|
|
5
5
|
Project-URL: Homepage, https://github.com/mullenkamp/ETo
|
|
6
6
|
Project-URL: Documentation, https://mullenkamp.github.io/ETo/
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Class to estimate reference ET (ETo) from the FAO 56 paper using a minimum of T_min and T_max for daily estimates and T_mean and RH_mean for hourly, but utilizing the maximum number of available met parameters. The function prioritizes the estimation of specific parameters based on the available input data.
|
|
4
4
|
"""
|
|
5
5
|
import numpy as np
|
|
6
|
+
from eto.util import is_hourly
|
|
6
7
|
from eto.param_est import param_est
|
|
7
8
|
from eto.methods.ETo import eto_fao
|
|
8
9
|
from eto.methods.hargreaves import hargreaves
|
|
@@ -23,18 +24,20 @@ class ETo(object):
|
|
|
23
24
|
T_min, T_max, T_mean, T_dew, RH_min, RH_max, RH_mean, n_sun, U_z,
|
|
24
25
|
P, e_a). All arrays must have the same length.
|
|
25
26
|
freq : str
|
|
26
|
-
Time frequency: 'D' for daily, '
|
|
27
|
+
Time frequency: 'D' for daily, 'h' (or 'H') for hourly, 'M' for monthly.
|
|
27
28
|
day_of_year : np.ndarray of int, or None
|
|
28
29
|
Day of year (1-366). Required if dates is not provided.
|
|
29
30
|
hour : np.ndarray of int, or None
|
|
30
31
|
Hour of day (0-23). Required for hourly frequency if dates is not provided.
|
|
31
32
|
dates : np.ndarray of datetime64, or None
|
|
32
33
|
Datetime array. Used to derive day_of_year and hour if they are not provided.
|
|
34
|
+
time_label, rs_rso_init
|
|
35
|
+
Hourly only; see :func:`eto.param_est.param_est`.
|
|
33
36
|
"""
|
|
34
37
|
|
|
35
38
|
def __init__(self, data=None, freq='D', z_msl=None, lat=None, lon=None, TZ_lon=None,
|
|
36
39
|
z_u=2, K_rs=0.16, a_s=0.25, b_s=0.5, alb=0.23,
|
|
37
|
-
day_of_year=None, hour=None, dates=None, validate=True):
|
|
40
|
+
day_of_year=None, hour=None, dates=None, validate=True, time_label='start', rs_rso_init=None):
|
|
38
41
|
|
|
39
42
|
if data is None:
|
|
40
43
|
pass
|
|
@@ -51,7 +54,7 @@ class ETo(object):
|
|
|
51
54
|
if dates is not None:
|
|
52
55
|
dates = np.asarray(dates)
|
|
53
56
|
day_of_year = (dates.astype('datetime64[D]') - dates.astype('datetime64[Y]')).astype(int) + 1
|
|
54
|
-
if
|
|
57
|
+
if is_hourly(freq):
|
|
55
58
|
hour = (dates - dates.astype('datetime64[D]')).astype('timedelta64[h]').astype(int)
|
|
56
59
|
elif day_of_year is not None:
|
|
57
60
|
day_of_year = np.asarray(day_of_year)
|
|
@@ -61,7 +64,7 @@ class ETo(object):
|
|
|
61
64
|
# Validate temporal array lengths
|
|
62
65
|
if len(day_of_year) != n:
|
|
63
66
|
raise ValueError('day_of_year length must match data array length')
|
|
64
|
-
if
|
|
67
|
+
if is_hourly(freq):
|
|
65
68
|
if hour is None:
|
|
66
69
|
raise ValueError('hour array or dates must be provided for hourly frequency')
|
|
67
70
|
hour = np.asarray(hour)
|
|
@@ -69,7 +72,8 @@ class ETo(object):
|
|
|
69
72
|
raise ValueError('hour length must match data array length')
|
|
70
73
|
|
|
71
74
|
self.param_est(data, freq, z_msl, lat, lon, TZ_lon, z_u, K_rs, a_s, b_s, alb,
|
|
72
|
-
day_of_year=day_of_year, hour=hour, validate=validate
|
|
75
|
+
day_of_year=day_of_year, hour=hour, validate=validate,
|
|
76
|
+
time_label=time_label, rs_rso_init=rs_rso_init)
|
|
73
77
|
|
|
74
78
|
|
|
75
79
|
### Add in the ETo methods
|
|
@@ -3,20 +3,24 @@
|
|
|
3
3
|
Function to estimate reference ET (ETo) from the FAO 56 paper using a minimum of T_min and T_max for daily estimates and T_mean and RH_mean for hourly, but utilizing the maximum number of available met parameters.
|
|
4
4
|
"""
|
|
5
5
|
import numpy as np
|
|
6
|
+
from eto.util import is_hourly
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
def eto_fao(self, max_ETo=15, min_ETo=0, ref_crop='short'):
|
|
9
|
+
def eto_fao(self, max_ETo=15, min_ETo=0, ref_crop='short', decimals=2):
|
|
9
10
|
"""
|
|
10
11
|
Function to estimate reference ET (ETo) from the `FAO 56 paper <http://www.fao.org/docrep/X0490E/X0490E00.htm>`_ [1]_ using a minimum of T_min and T_max for daily estimates and T_mean and RH_mean for hourly, but optionally utilising the maximum number of available met parameters.
|
|
11
12
|
|
|
12
13
|
Parameters
|
|
13
14
|
----------
|
|
14
|
-
max_ETo : float or
|
|
15
|
-
The max realistic value of ETo (mm).
|
|
16
|
-
min_ETo : float or
|
|
17
|
-
The min realistic value of ETo (mm).
|
|
15
|
+
max_ETo : float, int or None
|
|
16
|
+
The max realistic value of ETo (mm); higher values become NaN. None disables the check.
|
|
17
|
+
min_ETo : float, int or None
|
|
18
|
+
The min realistic value of ETo (mm); lower values are raised to it. None keeps negative
|
|
19
|
+
values (raw FAO-56, e.g. night-time dew).
|
|
18
20
|
ref_crop : str
|
|
19
21
|
Reference crop type: 'short' (FAO 56 grass) or 'tall' (ASCE alfalfa).
|
|
22
|
+
decimals : int or None
|
|
23
|
+
Decimal places of the result; None returns it unrounded.
|
|
20
24
|
|
|
21
25
|
Returns
|
|
22
26
|
-------
|
|
@@ -31,7 +35,7 @@ def eto_fao(self, max_ETo=15, min_ETo=0, ref_crop='short'):
|
|
|
31
35
|
|
|
32
36
|
######
|
|
33
37
|
## ETo equation — select Cn and Cd based on ref_crop and frequency
|
|
34
|
-
if
|
|
38
|
+
if is_hourly(self.freq):
|
|
35
39
|
if ref_crop == 'tall':
|
|
36
40
|
Cn = 66
|
|
37
41
|
Cd = np.where(self.ts_param['R_n'] > 0, 0.25, 1.7)
|
|
@@ -49,7 +53,9 @@ def eto_fao(self, max_ETo=15, min_ETo=0, ref_crop='short'):
|
|
|
49
53
|
ETo_FAO = (0.408*self.ts_param['delta']*(self.ts_param['R_n'] - self.ts_param['G']) + self.ts_param['gamma']*Cn/(self.ts_param['T_mean'] + 273)*self.ts_param['U_2']*(self.ts_param['e_s'] - self.ts_param['e_a']))/(self.ts_param['delta'] + self.ts_param['gamma']*(1 + Cd*self.ts_param['U_2']))
|
|
50
54
|
|
|
51
55
|
## Clamp negatives to min_ETo, NaN for suspect highs
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
if min_ETo is not None:
|
|
57
|
+
ETo_FAO = np.maximum(ETo_FAO, min_ETo)
|
|
58
|
+
if max_ETo is not None:
|
|
59
|
+
ETo_FAO = np.where(ETo_FAO > max_ETo, np.nan, ETo_FAO)
|
|
54
60
|
|
|
55
|
-
return np.round(ETo_FAO,
|
|
61
|
+
return ETo_FAO if decimals is None else np.round(ETo_FAO, decimals)
|
|
@@ -5,6 +5,7 @@ Created on Sun May 13 09:45:39 2018
|
|
|
5
5
|
@author: MichaelEK
|
|
6
6
|
"""
|
|
7
7
|
import numpy as np
|
|
8
|
+
from eto.util import is_hourly
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def hargreaves(self, max_ETo=15, min_ETo=0):
|
|
@@ -27,7 +28,7 @@ def hargreaves(self, max_ETo=15, min_ETo=0):
|
|
|
27
28
|
######
|
|
28
29
|
## ETo equation
|
|
29
30
|
|
|
30
|
-
if
|
|
31
|
+
if is_hourly(self.freq):
|
|
31
32
|
raise ValueError('Hargreaves should not be calculated at time frequencies of less than a day.')
|
|
32
33
|
|
|
33
34
|
ETo_Har = 0.0023*(self.ts_param['T_mean'] + 17.8)*((self.ts_param['T_max'] - self.ts_param['T_min']) **0.5)*self.ts_param['R_a']*0.408
|
|
@@ -4,9 +4,10 @@ Function for parameter estimation.
|
|
|
4
4
|
"""
|
|
5
5
|
import warnings
|
|
6
6
|
import numpy as np
|
|
7
|
+
from eto.util import is_hourly, TIME_LABELS, RS_RSO_MIN
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
def param_est(self, data, freq='D', z_msl=None, lat=None, lon=None, TZ_lon=None, z_u=2, K_rs=0.16, a_s=0.25, b_s=0.5, alb=0.23, day_of_year=None, hour=None, validate=True):
|
|
10
|
+
def param_est(self, data, freq='D', z_msl=None, lat=None, lon=None, TZ_lon=None, z_u=2, K_rs=0.16, a_s=0.25, b_s=0.5, alb=0.23, day_of_year=None, hour=None, validate=True, time_label='start', rs_rso_init=None):
|
|
10
11
|
"""
|
|
11
12
|
Function to estimate the parameters necessary to calculate reference ET (ETo) from the `FAO 56 paper <http://www.fao.org/docrep/X0490E/X0490E00.htm>`_ using a minimum of T_min and T_max for daily estimates and T_mean and RH_mean for hourly, but optionally utilising the maximum number of available met parameters.
|
|
12
13
|
|
|
@@ -21,9 +22,13 @@ def param_est(self, data, freq='D', z_msl=None, lat=None, lon=None, TZ_lon=None,
|
|
|
21
22
|
lat : float, int, or None
|
|
22
23
|
Latitude (decimal degrees).
|
|
23
24
|
lon : float, int, or None
|
|
24
|
-
Longitude
|
|
25
|
+
Longitude in decimal degrees, **east-positive** (e.g. 172.5 for Canterbury, -16.25 for
|
|
26
|
+
16°15'W). Since 2.2.0; earlier versions applied FAO-56's west-positive formula to this
|
|
27
|
+
argument, so east-positive input put hourly solar time about 2·lon/15 h out (about an hour
|
|
28
|
+
for NZ with ``TZ_lon`` = 0).
|
|
25
29
|
TZ_lon : float, int, or None
|
|
26
|
-
Longitude of the
|
|
30
|
+
Longitude of the centre of the time zone of ``hour``, east-positive (0 for UTC, 180 for
|
|
31
|
+
NZST).
|
|
27
32
|
z_u : float or int
|
|
28
33
|
Height of wind speed measurement (m). Default is 2 m.
|
|
29
34
|
K_rs : float
|
|
@@ -41,11 +46,32 @@ def param_est(self, data, freq='D', z_msl=None, lat=None, lon=None, TZ_lon=None,
|
|
|
41
46
|
validate : bool
|
|
42
47
|
If True (default), warn when input values are outside physically
|
|
43
48
|
reasonable ranges.
|
|
49
|
+
time_label : str
|
|
50
|
+
Hourly only. Whether ``hour`` labels the **start** of its one-hour period ('start', default:
|
|
51
|
+
the period is [hour, hour+1)) or its **end** ('end': (hour-1, hour]).
|
|
52
|
+
rs_rso_init : float or None
|
|
53
|
+
Hourly only. The Rs/Rso ratio carried into the first night of the series, i.e. the value from
|
|
54
|
+
the previous chunk (see ``self.rs_rso_last``). If None and the series starts at night, 1.0 is
|
|
55
|
+
used for the hours before the first reference period, with a warning.
|
|
44
56
|
|
|
45
57
|
Returns
|
|
46
58
|
-------
|
|
47
|
-
None (populates self.ts_param and self.est_val)
|
|
59
|
+
None (populates self.ts_param and self.est_val, and for hourly data self.rs_rso_last)
|
|
60
|
+
|
|
61
|
+
Notes
|
|
62
|
+
-----
|
|
63
|
+
**Night-time cloudiness (hourly).** FAO-56 (Ch. 3, hourly Rnl) sets Rs/Rso at night equal to the
|
|
64
|
+
value for the period 2-3 hours before sunset, where the solar time angle at the period's midpoint
|
|
65
|
+
satisfies ``ws - 0.79 <= w <= ws - 0.52``. That value is carried forward through every hour with
|
|
66
|
+
Rso = 0 until the next such period. It is state: pass the previous chunk's ``rs_rso_last`` as
|
|
67
|
+
``rs_rso_init`` so a series computed in chunks equals one computed whole. (Versions before 2.2.0
|
|
68
|
+
set Rs/Rso = 1 at night, i.e. assumed clear nights.) Hourly Rs/Rso is limited to [0.3, 1] (ASCE-EWRI
|
|
69
|
+
2005; FAO-56 gives only the upper limit; daily keeps FAO-56's [0, 1]), and hourly extraterrestrial radiation integrates only the sunlit
|
|
70
|
+
part of an hour that straddles sunrise or sunset (ASCE-EWRI).
|
|
48
71
|
"""
|
|
72
|
+
hourly = is_hourly(freq)
|
|
73
|
+
if time_label not in TIME_LABELS:
|
|
74
|
+
raise ValueError(f'time_label must be one of {TIME_LABELS}, got {time_label!r}')
|
|
49
75
|
|
|
50
76
|
met_names = ['R_n', 'R_s', 'G', 'T_min', 'T_max', 'T_mean', 'T_dew', 'RH_min', 'RH_max', 'RH_mean', 'n_sun', 'U_z', 'P', 'e_a']
|
|
51
77
|
self.freq = freq
|
|
@@ -85,7 +111,7 @@ def param_est(self, data, freq='D', z_msl=None, lat=None, lon=None, TZ_lon=None,
|
|
|
85
111
|
|
|
86
112
|
####################################
|
|
87
113
|
###### Check to make sure minimum requirements are met
|
|
88
|
-
if
|
|
114
|
+
if hourly:
|
|
89
115
|
T_mean_bool = np.isnan(self.ts_param['T_mean']).any()
|
|
90
116
|
RH_mean_bool = np.isnan(self.ts_param['RH_mean']).any()
|
|
91
117
|
e_a_bool = np.isnan(self.ts_param['e_a']).any()
|
|
@@ -122,7 +148,7 @@ def param_est(self, data, freq='D', z_msl=None, lat=None, lon=None, TZ_lon=None,
|
|
|
122
148
|
self.ts_param['T_mean'][mask] = (self.ts_param['T_max'][mask] + self.ts_param['T_min'][mask])/2
|
|
123
149
|
|
|
124
150
|
## Vapor pressures
|
|
125
|
-
if
|
|
151
|
+
if hourly:
|
|
126
152
|
self.ts_param['e_mean'] = 0.6108*np.exp(17.27*self.ts_param['T_mean']/(self.ts_param['T_mean']+237.3))
|
|
127
153
|
mask = np.isnan(self.ts_param['e_a'])
|
|
128
154
|
self.ts_param['e_a'][mask] = self.ts_param['e_mean'][mask]*self.ts_param['RH_mean'][mask]/100
|
|
@@ -162,7 +188,7 @@ def param_est(self, data, freq='D', z_msl=None, lat=None, lon=None, TZ_lon=None,
|
|
|
162
188
|
self.ts_param['T_dew'][mask] = 237.3 * ln_ratio / (17.27 - ln_ratio)
|
|
163
189
|
|
|
164
190
|
# VPD
|
|
165
|
-
if
|
|
191
|
+
if hourly:
|
|
166
192
|
self.ts_param['VPD'] = self.ts_param['e_mean'] - self.ts_param['e_a']
|
|
167
193
|
else:
|
|
168
194
|
self.ts_param['VPD'] = self.ts_param['e_s'] - self.ts_param['e_a']
|
|
@@ -176,21 +202,37 @@ def param_est(self, data, freq='D', z_msl=None, lat=None, lon=None, TZ_lon=None,
|
|
|
176
202
|
|
|
177
203
|
# R_a
|
|
178
204
|
phi = lat*np.pi/180
|
|
179
|
-
delta = 0.409*np.sin(2*np.pi*Day/365-1.39)
|
|
180
|
-
d_r = 1+0.033*np.cos(2*np.pi*Day/365)
|
|
181
|
-
w_s = np.arccos(-np.tan(phi)*np.tan(delta))
|
|
182
205
|
|
|
183
|
-
if
|
|
184
|
-
|
|
206
|
+
if hourly:
|
|
207
|
+
if lon is None or TZ_lon is None:
|
|
208
|
+
raise ValueError('lon and TZ_lon are required for hourly data')
|
|
209
|
+
# Solar time at the period midpoint (FAO-56 Eq 31-33, east-positive longitudes). Labels may
|
|
210
|
+
# be in any time zone (e.g. UTC), so the solar time can fall on the previous or next day:
|
|
211
|
+
# the declination and sunset angle use that solar day.
|
|
212
|
+
t_mid = np.asarray(hour, dtype=np.float64) + (0.5 if time_label == 'start' else -0.5)
|
|
185
213
|
b = (2*np.pi*(Day - 81))/364
|
|
186
214
|
S_c = 0.1645*np.sin(2*b) - 0.1255*np.cos(b) - 0.025*np.sin(b)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
215
|
+
# lon - TZ_lon wrapped to [-180, 180): the same place given as 183.5 or -176.5 is the same offset.
|
|
216
|
+
dlon = (lon - TZ_lon + 180) % 360 - 180
|
|
217
|
+
t_solar = t_mid + 0.06667*dlon + S_c
|
|
218
|
+
day_shift = np.floor(t_solar/24)
|
|
219
|
+
Day = Day + day_shift
|
|
220
|
+
t_solar = t_solar - 24*day_shift
|
|
221
|
+
w = np.pi/12*(t_solar - 12)
|
|
222
|
+
delta = 0.409*np.sin(2*np.pi*Day/365-1.39)
|
|
223
|
+
d_r = 1+0.033*np.cos(2*np.pi*Day/365)
|
|
224
|
+
w_s = np.arccos(-np.tan(phi)*np.tan(delta))
|
|
225
|
+
# Period start and end angles clipped to sunrise/sunset (ASCE-EWRI 2005), so an hour that
|
|
226
|
+
# straddles the horizon integrates only its sunlit part instead of netting night against day.
|
|
227
|
+
w_1 = np.clip(w - (np.pi*1)/24, -w_s, w_s)
|
|
228
|
+
w_2 = np.clip(w + (np.pi*1)/24, -w_s, w_s)
|
|
190
229
|
|
|
191
230
|
self.ts_param['R_a'] = 12*60/np.pi*0.082*d_r*((w_2 - w_1)*np.sin(phi)*np.sin(delta) + np.cos(phi)*np.cos(delta)*(np.sin(w_2) - np.sin(w_1)))
|
|
192
231
|
self.ts_param['R_a'] = np.maximum(self.ts_param['R_a'], 0)
|
|
193
232
|
else:
|
|
233
|
+
delta = 0.409*np.sin(2*np.pi*Day/365-1.39)
|
|
234
|
+
d_r = 1+0.033*np.cos(2*np.pi*Day/365)
|
|
235
|
+
w_s = np.arccos(-np.tan(phi)*np.tan(delta))
|
|
194
236
|
self.ts_param['R_a'] = 24*60/np.pi*0.082*d_r*(w_s*np.sin(phi)*np.sin(delta) + np.cos(phi)*np.cos(delta)*np.sin(w_s))
|
|
195
237
|
|
|
196
238
|
# Daylight hours
|
|
@@ -215,9 +257,32 @@ def param_est(self, data, freq='D', z_msl=None, lat=None, lon=None, TZ_lon=None,
|
|
|
215
257
|
# R_ns from R_s
|
|
216
258
|
R_ns = (1 - alb)*self.ts_param['R_s']
|
|
217
259
|
|
|
218
|
-
# R_nl —
|
|
219
|
-
|
|
220
|
-
|
|
260
|
+
# R_nl — R_s/R_so capped at 1 (FAO-56); hourly also floored at RS_RSO_MIN (ASCE-EWRI), because a dark
|
|
261
|
+
# hour's ratio is carried through the night. Daily is unchanged (FAO-56 as published). No division
|
|
262
|
+
# where R_so = 0.
|
|
263
|
+
rs_rso_min = RS_RSO_MIN if hourly else 0.0
|
|
264
|
+
Rs_Rso = np.where(R_so > 0, np.clip(self.ts_param['R_s'] / np.where(R_so > 0, R_so, 1.0), rs_rso_min, 1.0), np.nan)
|
|
265
|
+
if hourly:
|
|
266
|
+
# Night (R_so = 0): the ratio from the last period 2-3 h before sunset (FAO-56), carried as state.
|
|
267
|
+
ref = (w >= w_s - 0.79) & (w <= w_s - 0.52) & np.isfinite(Rs_Rso)
|
|
268
|
+
carried = np.where(ref, Rs_Rso, np.nan)
|
|
269
|
+
idx = np.where(np.isfinite(carried), np.arange(n), -1)
|
|
270
|
+
np.maximum.accumulate(idx, out=idx)
|
|
271
|
+
if rs_rso_init is None:
|
|
272
|
+
if ((idx < 0) & ~(R_so > 0)).any():
|
|
273
|
+
warnings.warn('Hourly series starts at night with no prior Rs/Rso (rs_rso_init); using 1.0 '
|
|
274
|
+
'(clear sky) until the first period 2-3 h before sunset')
|
|
275
|
+
init = 1.0
|
|
276
|
+
else:
|
|
277
|
+
init = float(np.clip(rs_rso_init, RS_RSO_MIN, 1.0))
|
|
278
|
+
carried = np.where(idx >= 0, carried[np.maximum(idx, 0)], init)
|
|
279
|
+
Rs_Rso = np.where(R_so > 0, Rs_Rso, carried)
|
|
280
|
+
self.rs_rso_last = float(carried[-1]) if n else init
|
|
281
|
+
else:
|
|
282
|
+
Rs_Rso = np.where(R_so > 0, Rs_Rso, 1.0)
|
|
283
|
+
self.ts_param['R_so'] = R_so
|
|
284
|
+
self.ts_param['Rs_Rso'] = Rs_Rso
|
|
285
|
+
if hourly:
|
|
221
286
|
R_nl = (2.043e-10)*((self.ts_param['T_mean'] + 273.16)**4)*(0.34-0.14*self.ts_param['e_a']**0.5)*(1.35*Rs_Rso - 0.35)
|
|
222
287
|
else:
|
|
223
288
|
R_nl = (4.903e-9)*(((self.ts_param['T_max'] + 273.16)**4 + (self.ts_param['T_min'] + 273.16)**4)/2)*(0.34-0.14*self.ts_param['e_a']**0.5)*(1.35*Rs_Rso - 0.35)
|
|
@@ -230,12 +295,12 @@ def param_est(self, data, freq='D', z_msl=None, lat=None, lon=None, TZ_lon=None,
|
|
|
230
295
|
# G
|
|
231
296
|
mask = np.isnan(self.ts_param['G'])
|
|
232
297
|
self.est_val[mask] += 10
|
|
233
|
-
if
|
|
298
|
+
if hourly:
|
|
234
299
|
day_mask = mask & (self.ts_param['R_n'] > 0)
|
|
235
300
|
night_mask = mask & (self.ts_param['R_n'] <= 0)
|
|
236
301
|
self.ts_param['G'][day_mask] = 0.1 * self.ts_param['R_n'][day_mask]
|
|
237
302
|
self.ts_param['G'][night_mask] = 0.5 * self.ts_param['R_n'][night_mask]
|
|
238
|
-
elif freq.
|
|
303
|
+
elif str(freq).lower() in ('m', '1m'):
|
|
239
304
|
G_monthly = np.zeros(n)
|
|
240
305
|
G_monthly[1:] = 0.14 * (self.ts_param['T_mean'][1:] - self.ts_param['T_mean'][:-1])
|
|
241
306
|
self.ts_param['G'][mask] = G_monthly[mask]
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hourly FAO-56 checks added in 2.2.0: Example 19, solar time with east-positive longitude and
|
|
3
|
+
interval labels, and the night-time Rs/Rso carried as state.
|
|
4
|
+
"""
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from eto import ETo
|
|
11
|
+
|
|
12
|
+
# FAO-56 Example 19: N'Diaye, Senegal, 16°13'N 16°15'W, 8 m, 1 October (J = 274), Lz = 15° W.
|
|
13
|
+
EX19 = dict(z_msl=8, lat=16 + 13 / 60, lon=-16.25, TZ_lon=-15.0)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ex19(hour, T, RH, u2, Rs, **kw):
|
|
17
|
+
data = {'T_mean': np.array([T]), 'RH_mean': np.array([RH]), 'U_z': np.array([u2]), 'R_s': np.array([Rs])}
|
|
18
|
+
return ETo(data, 'h', day_of_year=np.array([274]), hour=np.array([hour]), **EX19, **kw)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_example19_day_period():
|
|
22
|
+
et = ex19(14, 38.0, 52.0, 3.3, 2.450)
|
|
23
|
+
p = et.ts_param
|
|
24
|
+
assert p['R_a'][0] == pytest.approx(3.543, abs=0.002)
|
|
25
|
+
assert p['R_so'][0] == pytest.approx(2.658, abs=0.002)
|
|
26
|
+
assert p['Rs_Rso'][0] == pytest.approx(0.922, abs=0.001)
|
|
27
|
+
assert p['R_n'][0] == pytest.approx(1.749, abs=0.003)
|
|
28
|
+
assert et.eto_fao()[0] == pytest.approx(0.63)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_example19_night_period_uses_the_carried_ratio():
|
|
32
|
+
et = ex19(2, 28.0, 90.0, 1.9, 0.0, rs_rso_init=0.8)
|
|
33
|
+
p = et.ts_param
|
|
34
|
+
assert p['Rs_Rso'][0] == 0.8
|
|
35
|
+
assert p['R_n'][0] == pytest.approx(-0.100, abs=0.002)
|
|
36
|
+
assert et.eto_fao()[0] == pytest.approx(0.0)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def nz_day(time_label='start', lon=171.5, TZ_lon=0.0):
|
|
40
|
+
"""One January day of hourly UTC labels at a Canterbury point."""
|
|
41
|
+
n = 24
|
|
42
|
+
data = {'T_mean': np.full(n, 20.0), 'RH_mean': np.full(n, 60.0), 'U_z': np.full(n, 2.0), 'R_s': np.full(n, 0.5)}
|
|
43
|
+
hours = np.arange(n) + (1 if time_label == 'end' else 0)
|
|
44
|
+
return ETo(data, 'h', z_msl=300, lat=-43.5, lon=lon, TZ_lon=TZ_lon, day_of_year=np.full(n, 15),
|
|
45
|
+
hour=hours, time_label=time_label, rs_rso_init=1.0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_extraterrestrial_radiation_peaks_in_the_hour_holding_solar_noon():
|
|
49
|
+
# Solar noon at 171.5°E on 15 January is about 00:40 UTC (equation of time about -9 min). With
|
|
50
|
+
# west-positive arithmetic on east-positive input the peak moves to the 23 UTC hour.
|
|
51
|
+
et = nz_day('start')
|
|
52
|
+
assert int(np.argmax(et.ts_param['R_a'])) == 0
|
|
53
|
+
# The same instant expressed in NZST labels (TZ 180°E) peaks in the 12 h period.
|
|
54
|
+
data = {'T_mean': np.full(24, 20.0), 'RH_mean': np.full(24, 60.0), 'R_s': np.full(24, 0.5)}
|
|
55
|
+
nzst = ETo(data, 'h', z_msl=300, lat=-43.5, lon=171.5, TZ_lon=180.0, day_of_year=np.full(24, 15),
|
|
56
|
+
hour=np.arange(24), rs_rso_init=1.0)
|
|
57
|
+
assert int(np.argmax(nzst.ts_param['R_a'])) == 12
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_period_ending_labels_shift_by_one_hour():
|
|
61
|
+
start = nz_day('start').ts_param['R_a']
|
|
62
|
+
end = nz_day('end').ts_param['R_a']
|
|
63
|
+
np.testing.assert_allclose(end, start) # labels 1..24 'end' are the same periods as 0..23 'start'
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_time_label_is_validated():
|
|
67
|
+
with pytest.raises(ValueError, match='time_label'):
|
|
68
|
+
nz_day('middle')
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize('freq', ['3h', 'month', '30min', '60min', 'T', 'x'])
|
|
72
|
+
def test_frequency_must_match_exactly(freq):
|
|
73
|
+
data = {'T_mean': np.full(3, 20.0), 'RH_mean': np.full(3, 60.0), 'T_min': np.full(3, 10.0), 'T_max': np.full(3, 25.0)}
|
|
74
|
+
with pytest.raises(ValueError, match='Unsupported frequency'):
|
|
75
|
+
ETo(data, freq, z_msl=10, lat=-43.5, lon=171.5, TZ_lon=0.0, day_of_year=np.array([1, 2, 3]),
|
|
76
|
+
hour=np.array([0, 1, 2]))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def three_days(cloud=(0.5, 0.9, 0.3)):
|
|
80
|
+
"""Hourly UTC series at 171.5°E: R_s = cloud factor of the day times clear-sky Rso."""
|
|
81
|
+
n = 72
|
|
82
|
+
hours = np.arange(n) % 24
|
|
83
|
+
doy = 15 + np.arange(n) // 24
|
|
84
|
+
clear = ETo({'T_mean': np.full(n, 15.0), 'RH_mean': np.full(n, 70.0)}, 'h', z_msl=300, lat=-43.5,
|
|
85
|
+
lon=171.5, TZ_lon=0.0, day_of_year=doy, hour=hours, rs_rso_init=1.0).ts_param['R_so']
|
|
86
|
+
# UTC hours 0-5 of day d are the NZ afternoon of that day (NZST = UTC+12): the day's cloud factor.
|
|
87
|
+
# Sunset is near 08 UTC, so the FAO reference period (2-3 h before sunset) is the 05 UTC hour.
|
|
88
|
+
# The dusk hours 06-07 get 0.7 and the NZ morning (UTC 12-23) 0.95, so a ratio taken from the
|
|
89
|
+
# wrong hour (a dusk hour, the morning) is visible.
|
|
90
|
+
day_cloud = np.array(cloud)[np.arange(n) // 24]
|
|
91
|
+
rs = clear * np.select([hours <= 5, hours <= 11], [day_cloud, 0.7], 0.95)
|
|
92
|
+
data = {'T_mean': np.full(n, 15.0), 'RH_mean': np.full(n, 70.0), 'U_z': np.full(n, 2.0), 'R_s': rs}
|
|
93
|
+
return data, doy, hours
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def run(data, doy, hours, sl, init):
|
|
97
|
+
sub = {k: v[sl] for k, v in data.items()}
|
|
98
|
+
return ETo(sub, 'h', z_msl=300, lat=-43.5, lon=171.5, TZ_lon=0.0, day_of_year=doy[sl], hour=hours[sl],
|
|
99
|
+
rs_rso_init=init)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_night_uses_the_ratio_from_two_to_three_hours_before_sunset():
|
|
103
|
+
data, doy, hours = three_days()
|
|
104
|
+
et = run(data, doy, hours, slice(None), 1.0)
|
|
105
|
+
p = et.ts_param
|
|
106
|
+
night = p['R_so'] <= 0
|
|
107
|
+
# Every night hour after the first evening carries that evening's ratio, never the morning's
|
|
108
|
+
# 0.95 or the clear-sky 1.0.
|
|
109
|
+
after_first_evening = night & (np.arange(72) > 12)
|
|
110
|
+
assert after_first_evening.sum() > 15
|
|
111
|
+
# In January the whole NZ night (about 09:30-18:00 UTC) lies inside one UTC day, whose afternoon
|
|
112
|
+
# is the evening before it.
|
|
113
|
+
expect = np.array([0.5, 0.9, 0.3])[np.arange(72) // 24]
|
|
114
|
+
np.testing.assert_allclose(p['Rs_Rso'][after_first_evening], expect[after_first_evening])
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_chunked_equals_unchunked_with_boundaries_at_night():
|
|
118
|
+
# Boundaries placed in NZ night and low-sun hours (UTC 10-18 is 22:00-06:00 NZST), where the
|
|
119
|
+
# carried state is used. A boundary at UTC 00 (NZ noon) would test nothing.
|
|
120
|
+
data, doy, hours = three_days()
|
|
121
|
+
whole = run(data, doy, hours, slice(None), 1.0)
|
|
122
|
+
# Also every single cut, including those between the reference hour and dark, where the state
|
|
123
|
+
# must be the reference ratio and not the last hour's own ratio.
|
|
124
|
+
for cuts in ([14], [11, 40], [17, 38, 62], [19, 43], *([c] for c in range(1, 72))):
|
|
125
|
+
edges = [0, *cuts, 72]
|
|
126
|
+
init = 1.0
|
|
127
|
+
parts = []
|
|
128
|
+
for a, b in zip(edges[:-1], edges[1:]):
|
|
129
|
+
et = run(data, doy, hours, slice(a, b), init)
|
|
130
|
+
parts.append(et.ts_param['R_n'])
|
|
131
|
+
init = et.rs_rso_last
|
|
132
|
+
np.testing.assert_array_equal(np.concatenate(parts), whole.ts_param['R_n'])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_series_starting_at_night_warns_without_an_initial_ratio_and_not_with_one():
|
|
136
|
+
data, doy, hours = three_days()
|
|
137
|
+
with pytest.warns(UserWarning, match='starts at night'):
|
|
138
|
+
run(data, doy, hours, slice(14, 30), None)
|
|
139
|
+
with warnings.catch_warnings():
|
|
140
|
+
warnings.simplefilter('error')
|
|
141
|
+
run(data, doy, hours, slice(14, 30), 0.7)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_raw_mode_keeps_negative_unrounded_values():
|
|
145
|
+
# A saturated night (RH 100 %, so no vapour deficit) with net longwave loss: FAO-56 gives a small
|
|
146
|
+
# negative value (dew). Example 19's own night value is +0.004 mm, shown as 0.00.
|
|
147
|
+
et = ex19(2, 10.0, 100.0, 1.0, 0.0, rs_rso_init=0.8)
|
|
148
|
+
raw = et.eto_fao(min_ETo=None, max_ETo=None, decimals=None)
|
|
149
|
+
assert raw[0] < 0
|
|
150
|
+
assert raw[0] != np.round(raw[0], 2)
|
|
151
|
+
assert et.eto_fao()[0] == 0.0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_west_longitude_with_utc_labels_finds_the_evening_reference():
|
|
155
|
+
# At 120°W the local evening falls after 00 UTC, so solar time must wrap into the previous day;
|
|
156
|
+
# without the wrap the reference period is never found and nights keep the initial 1.0.
|
|
157
|
+
n = 48
|
|
158
|
+
hours = np.arange(n) % 24
|
|
159
|
+
doy = 180 + np.arange(n) // 24
|
|
160
|
+
base = dict(z_msl=100, lat=40.0, lon=-120.0, TZ_lon=0.0, day_of_year=doy, hour=hours)
|
|
161
|
+
clear = ETo({'T_mean': np.full(n, 20.0), 'RH_mean': np.full(n, 50.0)}, 'h', rs_rso_init=1.0, **base).ts_param['R_so']
|
|
162
|
+
et = ETo({'T_mean': np.full(n, 20.0), 'RH_mean': np.full(n, 50.0), 'R_s': 0.6 * clear}, 'h', rs_rso_init=1.0, **base)
|
|
163
|
+
night = et.ts_param['R_so'] <= 0
|
|
164
|
+
later_nights = night & (np.arange(n) > 24)
|
|
165
|
+
assert later_nights.any()
|
|
166
|
+
np.testing.assert_allclose(et.ts_param['Rs_Rso'][later_nights], 0.6)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# --- review thalweg-step0-code-1 ----------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_reference_is_the_hour_two_to_three_hours_before_sunset():
|
|
174
|
+
# Defect caught: a reference window one hour off (3-4 h before sunset). Each afternoon hour gets its
|
|
175
|
+
# own cloud factor, so the carried night value identifies the hour it came from.
|
|
176
|
+
n = 24
|
|
177
|
+
hours = np.arange(n)
|
|
178
|
+
doy = np.full(n, 15)
|
|
179
|
+
base = dict(z_msl=300, lat=-43.5, lon=171.5, TZ_lon=0.0, day_of_year=doy, hour=hours, rs_rso_init=1.0)
|
|
180
|
+
clear = ETo({'T_mean': np.full(n, 15.0), 'RH_mean': np.full(n, 70.0)}, 'h', **base).ts_param['R_so']
|
|
181
|
+
factor = np.full(n, 0.95)
|
|
182
|
+
factor[:8] = [0.40, 0.45, 0.50, 0.55, 0.60, 0.65, 0.70, 0.75] # UTC 0-7 = NZST 12-19
|
|
183
|
+
et = ETo({'T_mean': np.full(n, 15.0), 'RH_mean': np.full(n, 70.0), 'R_s': factor * clear}, 'h', **base)
|
|
184
|
+
p = et.ts_param
|
|
185
|
+
night = p['R_so'] <= 0
|
|
186
|
+
# Which afternoon hour satisfies FAO-56's window: compute it from the solar angle directly.
|
|
187
|
+
phi = np.deg2rad(-43.5)
|
|
188
|
+
delta = 0.409 * np.sin(2 * np.pi * 15 / 365 - 1.39)
|
|
189
|
+
ws = np.arccos(-np.tan(phi) * np.tan(delta))
|
|
190
|
+
b = 2 * np.pi * (15 - 81) / 364
|
|
191
|
+
sc = 0.1645 * np.sin(2 * b) - 0.1255 * np.cos(b) - 0.025 * np.sin(b)
|
|
192
|
+
w = np.pi / 12 * ((hours + 0.5) + 0.06667 * (171.5 - 0.0) + sc - 24 * ((hours + 0.5 + 0.06667 * 171.5 + sc) >= 24) - 12)
|
|
193
|
+
ref = (w >= ws - 0.79) & (w <= ws - 0.52)
|
|
194
|
+
assert ref.sum() == 1
|
|
195
|
+
k = int(np.flatnonzero(ref)[0])
|
|
196
|
+
np.testing.assert_allclose(p['Rs_Rso'][night & (hours > k)], factor[k])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_carried_ratio_is_the_capped_value():
|
|
200
|
+
# Defect caught: carrying the raw ratio. A reference hour with R_s above clear sky carries 1.0.
|
|
201
|
+
data, doy, hours = three_days(cloud=(1.4, 1.4, 1.4))
|
|
202
|
+
et = run(data, doy, hours, slice(None), 0.5)
|
|
203
|
+
p = et.ts_param
|
|
204
|
+
later_nights = (p['R_so'] <= 0) & (np.arange(72) > 12)
|
|
205
|
+
np.testing.assert_allclose(p['Rs_Rso'][later_nights], 1.0)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_rs_rso_is_floored_at_0_3():
|
|
209
|
+
data, doy, hours = three_days(cloud=(0.05, 0.05, 0.05))
|
|
210
|
+
et = run(data, doy, hours, slice(None), 0.05)
|
|
211
|
+
r = et.ts_param['Rs_Rso']
|
|
212
|
+
assert np.nanmin(r) == pytest.approx(0.3)
|
|
213
|
+
later_nights = (et.ts_param['R_so'] <= 0) & (np.arange(72) > 12)
|
|
214
|
+
np.testing.assert_allclose(r[later_nights], 0.3)
|
|
215
|
+
assert (et.ts_param['R_n'][later_nights] < 0).all() # net longwave stays a loss at night
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_longitude_difference_wraps_across_the_antimeridian():
|
|
219
|
+
a = nz_day('start', lon=183.5).ts_param['R_a']
|
|
220
|
+
b = nz_day('start', lon=-176.5).ts_param['R_a']
|
|
221
|
+
np.testing.assert_allclose(a, b)
|
|
222
|
+
c = nz_day('start', lon=171.5, TZ_lon=180.0).ts_param['R_a']
|
|
223
|
+
d = nz_day('start', lon=171.5, TZ_lon=-180.0).ts_param['R_a']
|
|
224
|
+
np.testing.assert_allclose(c, d)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_sunrise_and_sunset_hours_integrate_only_the_sunlit_part():
|
|
228
|
+
# Defect caught: integrating the whole hour, netting night (negative) against day. Compare each
|
|
229
|
+
# hour's R_a with a fine numerical integral of the instantaneous extraterrestrial radiation.
|
|
230
|
+
et = nz_day('start')
|
|
231
|
+
ra = et.ts_param['R_a']
|
|
232
|
+
phi = np.deg2rad(-43.5)
|
|
233
|
+
for h in range(24):
|
|
234
|
+
# UTC labels at 171.5°E: local solar time = UTC + 11.4 h, so UTC 13-23 fall on the next solar day.
|
|
235
|
+
b0 = 2 * np.pi * (15 - 81) / 364
|
|
236
|
+
sc0 = 0.1645 * np.sin(2 * b0) - 0.1255 * np.cos(b0) - 0.025 * np.sin(b0)
|
|
237
|
+
day = 15 + int(np.floor((h + 0.5 + 0.06667 * 171.5 + sc0) / 24))
|
|
238
|
+
delta = 0.409 * np.sin(2 * np.pi * day / 365 - 1.39)
|
|
239
|
+
dr = 1 + 0.033 * np.cos(2 * np.pi * day / 365)
|
|
240
|
+
# The equation of time (S_c) is taken from the label's day, as the package does; its change
|
|
241
|
+
# across one day is under a minute.
|
|
242
|
+
t = h + (np.arange(600) + 0.5) / 600
|
|
243
|
+
w = np.pi / 12 * (((t + 0.06667 * 171.5 + sc0) % 24) - 12)
|
|
244
|
+
inst = 0.082 * dr * np.maximum(np.sin(phi) * np.sin(delta) + np.cos(phi) * np.cos(delta) * np.cos(w), 0)
|
|
245
|
+
assert ra[h] == pytest.approx(inst.mean() * 60, abs=2e-3), h
|
eto-2.2.0/eto/util.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Utility functions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
HOURLY = ('h', '1h')
|
|
7
|
+
DAILY = ('d', '1d')
|
|
8
|
+
MONTHLY = ('m', '1m')
|
|
9
|
+
TIME_LABELS = ('start', 'end')
|
|
10
|
+
RS_RSO_MIN = 0.3
|
|
11
|
+
"""Lower limit on Rs/Rso (ASCE-EWRI 2005). FAO-56 states only the upper limit of 1; below about 0.26 its
|
|
12
|
+
cloudiness factor (1.35 Rs/Rso - 0.35) turns negative, i.e. net longwave becomes a gain."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_hourly(freq):
|
|
16
|
+
"""
|
|
17
|
+
True for an hourly frequency, False for daily or monthly; anything else raises.
|
|
18
|
+
|
|
19
|
+
Accepted (case-insensitive): 'h' / '1h' (hourly), 'D' / '1D' (daily), 'M' / '1M' (monthly). The
|
|
20
|
+
equations assume exactly these periods, so other strings ('3h', '30min', 'T', 'month') are refused
|
|
21
|
+
rather than silently computed with the wrong period.
|
|
22
|
+
"""
|
|
23
|
+
f = str(freq).lower()
|
|
24
|
+
if f in HOURLY:
|
|
25
|
+
return True
|
|
26
|
+
if f in DAILY or f in MONTHLY:
|
|
27
|
+
return False
|
|
28
|
+
raise ValueError(f'Unsupported frequency {freq!r}: use "h" (hourly), "D" (daily) or "M" (monthly)')
|
eto-2.1.0/eto/util.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|