evapotranspiration 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 988048fa8b6ccee0e7bd6fcdaca2797cfcdc7a1a
4
+ data.tar.gz: 41a4bd21d9595e51d7b716218e03f81f7574c98f
5
+ SHA512:
6
+ metadata.gz: 82a628bf0d71de55a8297da685f3472ea151db031a03846467a2bc8fd731cb3d42c9b74fb2ee49cc1ff22fc8cbf4df9e2e833cd3892f9479453f9ca4a2ce7173
7
+ data.tar.gz: 221e966a85a752b9160fbb6ba7616f44c8400e791a9c0ac9614e97f5b0b6db48eca476535a130ee6bdca1667dd398799b6fd4f2cb93bee8c374a10c69d6381d7
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /.idea
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.12.4
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in evapotranspiration.gemspec
4
+ gemspec
@@ -0,0 +1,29 @@
1
+ Copyright (c) 2015, Mark Richards
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice,
9
+ this list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from this
17
+ software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
23
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
+ POSSIBILITY OF SUCH DAMAGE.
data/LICENSE.txt ADDED
@@ -0,0 +1,29 @@
1
+ Copyright (c) 2016, Bryce Johnston
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice,
9
+ this list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from this
17
+ software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
23
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
+ POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Evapotranspiration
2
+
3
+ [![Gem Version](http://img.shields.io/gem/v/evapotranspiration.svg)][gem]
4
+ [![Build Status](http://img.shields.io/travis/AgRuby/evapotranspiration.svg)][travis]
5
+
6
+ [gem]: https://rubygems.org/gems/evapotranspiration
7
+ [travis]: http://travis-ci.org/AgRuby/evapotranspiration
8
+
9
+ Ruby library for calculating reference crop evapotranspiration (ETo), also referred to as potential evapotranspiration (PET), using the FAO-56 Penman-Monteith method. This is a Ruby port of [Mark Richard's PyETo Python package](https://github.com/woodcrafty/PyETo). The library provides numerous functions for estimating missing meteorological data.
10
+
11
+ Three methods for estimating ETo/PET are implemented:
12
+
13
+ - FAO-56 Penman-Monteith (Allen et al, 1998)
14
+ - Hargreaves (Hargreaves and Samani, 1982; 1985)
15
+ - Thornthwaite (Thornthwaite, 1948)
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem 'evapotranspiration'
23
+ ```
24
+
25
+ And then execute:
26
+
27
+ $ bundle
28
+
29
+ Or install it yourself as:
30
+
31
+ $ gem install evapotranspiration
32
+
33
+ ## Usage
34
+
35
+ Coming soon...
36
+
37
+ ## Development
38
+
39
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`.
40
+
41
+ ## Contributing
42
+
43
+ Bug reports and pull requests are welcome on GitHub at https://github.com/AgRuby/evapotranspiration.
44
+
45
+ ## Acknowledgments
46
+
47
+ [PyETo](https://github.com/woodcrafty/PyETo) was created by [Mark Richard](https://github.com/woodcrafty) - ported into Ruby and maintained by [Bryce Johnston](https://github.com/brycejohnston)
48
+
49
+ ## License
50
+
51
+ The gem is available as open source under the terms of the BSD 3-Clause License (see [LICENSE.txt](https://github.com/AgRuby/evapotranspiration/blob/master/LICENSE.txt)).
52
+
53
+ The original PyETo Python package it is based on was released under the BSD 3-Clause License (see [LICENSE-ORIGINAL.txt](https://github.com/AgRuby/evapotranspiration/blob/master/LICENSE-ORIGINAL.txt)).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "evapotranspiration"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'evapotranspiration/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "evapotranspiration"
8
+ spec.version = Evapotranspiration::VERSION
9
+ spec.authors = ["Bryce Johnston"]
10
+ spec.email = ["johnstonbrc@gmail.com"]
11
+ spec.summary = %q{Ruby library for calculating reference crop evapotranspiration (ETo)}
12
+ spec.description = %q{Ruby library for calculating reference crop evapotranspiration (ETo), also referred to as potential evapotranspiration (PET), using the FAO-56 Penman-Monteith method. This is a Ruby port of Mark Richard's PyETo Python package.}
13
+ spec.homepage = "https://github.com/AgRuby/evapotranspiration"
14
+ spec.license = "BSD 3-Clause"
15
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
+ spec.bindir = "exe"
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.12"
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+ spec.add_development_dependency "rspec", "~> 3.0"
23
+ end
@@ -0,0 +1,5 @@
1
+ require 'evapotranspiration/version'
2
+ require 'evapotranspiration/conversion'
3
+ require 'evapotranspiration/fao'
4
+ require 'evapotranspiration/thornthwaite'
5
+ require 'evapotranspiration/validation'
@@ -0,0 +1,37 @@
1
+ module Evapotranspiration
2
+ module Conversion
3
+
4
+ # Convert temperature in degrees Celsius to degrees Kelvin
5
+ #
6
+ # @param celsius [Float] Degrees Celsius
7
+ # @return [Float] Degrees Kelvin
8
+ def self.celsius_to_kelvin(celsius)
9
+ celsius + 273.15
10
+ end
11
+
12
+ # Convert temperature in degrees Kelvin to degrees Celsius
13
+ #
14
+ # @param kelvin [Float] Degrees Kelvin
15
+ # @return [Float] Degrees Celsius
16
+ def self.kelvin_to_celsius(kelvin)
17
+ kelvin - 273.15
18
+ end
19
+
20
+ # Convert angular degrees to radians
21
+ #
22
+ # @param degrees [Float] Value in degrees to be converted
23
+ # @return [Float] Value in radians
24
+ def self.deg_to_rad(degrees)
25
+ degrees * (Math::PI / 180.0)
26
+ end
27
+
28
+ # Convert radians to angular degrees
29
+ #
30
+ # @param radians [Float] Value in radians to be converted
31
+ # @return [Float] Value in angular degrees
32
+ def self.rad_to_deg(radians)
33
+ radians * (180.0 / Math::PI)
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,627 @@
1
+ require 'evapotranspiration/validation'
2
+
3
+ module Evapotranspiration
4
+
5
+ # Methods for estimating reference evapotransporation (ETo) for
6
+ # a grass reference crop using the FAO-56 Penman-Monteith and Hargreaves
7
+ # equations. The library includes numerous functions for estimating missing
8
+ # meteorological data.
9
+ module FAO
10
+ include Enumerable
11
+
12
+ # Solar constant [ MJ m-2 min-1]
13
+ SOLAR_CONSTANT = 0.0820
14
+
15
+ # Stefan Boltzmann constant [MJ K-4 m-2 day-1]
16
+ STEFAN_BOLTZMANN_CONSTANT = 0.000000004903
17
+
18
+ # Estimate atmospheric pressure from altitude.
19
+ #
20
+ # Calculated using a simplification of the ideal gas law, assuming 20 degrees
21
+ # Celsius for a standard atmosphere. Based on equation 7, page 62 in Allen
22
+ # et al (1998).
23
+ #
24
+ # @param altitude [Float] Elevation/altitude above sea level (m)
25
+ # @return [Float] atmospheric pressure (kPa)
26
+ def self.atm_pressure(altitude)
27
+ tmp = (293.0 - (0.0065 * altitude)) / 293.0
28
+ return (tmp ** 5.26) * 101.3
29
+ end
30
+
31
+ # Estimate actual vapour pressure (*ea*) from minimum temperature.
32
+ #
33
+ # This method is to be used where humidity data are lacking or are of
34
+ # questionable quality. The method assumes that the dewpoint temperature
35
+ # is approximately equal to the minimum temperature (*tmin*), i.e. the
36
+ # air is saturated with water vapour at *tmin*.
37
+ #
38
+ # **Note**: This assumption may not hold in arid/semi-arid areas.
39
+ # In these areas it may be better to subtract 2 deg C from the
40
+ # minimum temperature (see Annex 6 in FAO paper).
41
+ #
42
+ # Based on equation 48 in Allen et al (1998).
43
+ #
44
+ # @param tmin [Float] Daily minimum temperature (deg C)
45
+ # @return [Float] Actual vapour pressure (kPa)
46
+ def self.avp_from_tmin(tmin)
47
+ return 0.611 * Math.exp((17.27 * tmin) / (tmin + 237.3))
48
+ end
49
+
50
+ # Estimate actual vapour pressure (*ea*) from saturation vapour pressure and
51
+ # relative humidity.
52
+ #
53
+ # Based on FAO equation 17 in Allen et al (1998).
54
+ #
55
+ # @param svp_tmin [Float] Saturation vapour pressure at daily minimum
56
+ # temperature (kPa). Can be estimated using svp_from_t()
57
+ # @param svp_tmax [Float] Saturation vapour pressure at daily maximum
58
+ # temperature (kPa). Can be estimated using svp_from_t()
59
+ # @param rh_min [Float] Minimum relative humidity (%)
60
+ # @param rh_max [Float] Maximum relative humidity (%)
61
+ # @return [Float] Actual vapour pressure (kPa)
62
+ def self.avp_from_rhmin_rhmax(svp_tmin, svp_tmax, rh_min, rh_max)
63
+ tmp1 = svp_tmin * (rh_max / 100.0)
64
+ tmp2 = svp_tmax * (rh_min / 100.0)
65
+ return (tmp1 + tmp2) / 2.0
66
+ end
67
+
68
+ # Estimate actual vapour pressure (*ea*) from saturation vapour pressure at
69
+ # daily minimum and maximum temperature, and mean relative humidity.
70
+ #
71
+ # Based on FAO equation 19 in Allen et al (1998).
72
+ #
73
+ # @param svp_tmin [Float] Saturation vapour pressure at daily minimum
74
+ # temperature (kPa). Can be estimated using svp_from_t()
75
+ # @param rh_max [Float] Maximum relative humidity (%)
76
+ # @return [Float] Actual vapour pressure (kPa)
77
+ def self.avp_from_rhmax(svp_tmin, rh_max)
78
+ return svp_tmin * (rh_max / 100.0)
79
+ end
80
+
81
+ # Estimate actual vapour pressure (*e*a) from saturation vapour pressure at
82
+ # daily minimum temperature and maximum relative humidity.
83
+ #
84
+ # Based on FAO equation 18 in Allen et al (1998).
85
+ #
86
+ # @param svp_tmin [Float] Saturation vapour pressure at daily minimum
87
+ # temperature (kPa). Can be estimated using svp_from_t()
88
+ # @param svp_tmax [Float] Saturation vapour pressure at daily maximum
89
+ # temperature (kPa). Can be estimated using svp_from_t()
90
+ # @return [Float] Actual vapour pressure (kPa)
91
+ def self.avp_from_rhmean(svp_tmin, svp_tmax, rh_mean)
92
+ return (rh_mean / 100.0) * ((svp_tmax + svp_tmin) / 2.0)
93
+ end
94
+
95
+ # Estimate actual vapour pressure (*ea*) from dewpoint temperature.
96
+ #
97
+ # Based on equation 14 in Allen et al (1998). As the dewpoint temperature is
98
+ # the temperature to which air needs to be cooled to make it saturated, the
99
+ # actual vapour pressure is the saturation vapour pressure at the dewpoint
100
+ # temperature.
101
+ #
102
+ # This method is preferable to calculating vapour pressure from
103
+ # minimum temperature.
104
+ #
105
+ # @param tdew [Float] Dewpoint temperature (deg C)
106
+ # @return [Float] Actual vapour pressure (kPa)
107
+ def self.avp_from_tdew(tdew)
108
+ return 0.6108 * Math.exp((17.27 * tdew) / (tdew + 237.3))
109
+ end
110
+
111
+ # Estimate actual vapour pressure (*ea*) from wet and dry bulb temperature.
112
+ #
113
+ # Based on equation 15 in Allen et al (1998). As the dewpoint temperature
114
+ # is the temperature to which air needs to be cooled to make it saturated, the
115
+ # actual vapour pressure is the saturation vapour pressure at the dewpoint
116
+ # temperature.
117
+ #
118
+ # This method is preferable to calculating vapour pressure from
119
+ # minimum temperature.
120
+ #
121
+ # Values for the psychrometric constant of the psychrometer (*psy_const*)
122
+ # can be calculated using psyc_const_of_psychrometer().
123
+ #
124
+ # @param twet [Float] Wet bulb temperature (deg C)
125
+ # @param tdry [Float] Dry bulb temperature (deg C)
126
+ # @param svp_twet [Float] Saturated vapour pressure at the wet bulb
127
+ # temperature (kPa). Can be estimated using svp_from_t()
128
+ # @param psy_const [Float] Psychrometric constant of the pyschrometer
129
+ # (kPa deg C-1). Can be estimated using psy_const() or
130
+ # psy_const_of_psychrometer()
131
+ # @return [Float] Actual vapour pressure (kPa)
132
+ def self.avp_from_twet_tdry(twet, tdry, svp_twet, psy_const)
133
+ return svp_twet - (psy_const * (tdry - twet))
134
+ end
135
+
136
+ # Estimate clear sky radiation from altitude and extraterrestrial radiation.
137
+ #
138
+ # Based on equation 37 in Allen et al (1998) which is recommended when
139
+ # calibrated Angstrom values are not available.
140
+ #
141
+ # @param altitude [Float] Elevation above sea level (m)
142
+ # @param et_rad [Float] Extraterrestrial radiation (MJ m-2 day-1). Can be
143
+ # estimated using et_rad()
144
+ # @return [Float] Clear sky radiation (MJ m-2 day-1)
145
+ def self.cs_rad(altitude, et_rad)
146
+ return (0.00002 * altitude + 0.75) * et_rad
147
+ end
148
+
149
+ # Estimate mean daily temperature from the daily minimum and maximum
150
+ # temperatures.
151
+ #
152
+ # @param tmin [Float] Minimum daily temperature (deg C)
153
+ # @param tmax [Float] Maximum daily temperature (deg C)
154
+ # @return [Float] Mean daily temperature (deg C)
155
+ def self.daily_mean_t(tmin, tmax)
156
+ return (tmax + tmin) / 2.0
157
+ end
158
+
159
+ # Calculate daylight hours from sunset hour angle.
160
+ #
161
+ # Based on FAO equation 34 in Allen et al (1998).
162
+ #
163
+ # @param sha [Float] Sunset hour angle (rad). Can be calculated using
164
+ # sunset_hour_angle()
165
+ # @return [Float] Daylight hours
166
+ def self.daylight_hours(sha)
167
+ Validation.check_sunset_hour_angle_rad(sha)
168
+ return (24.0 / Math::PI) * sha
169
+ end
170
+
171
+ # Estimate the slope of the saturation vapour pressure curve at a given
172
+ # temperature.
173
+ #
174
+ # Based on equation 13 in Allen et al (1998). If using in the Penman-Monteith
175
+ # *t* should be the mean air temperature.
176
+ #
177
+ # @param t [Float] Air temperature (deg C). Use mean air temperature for
178
+ # use in Penman-Monteith
179
+ # @return [Float] Saturation vapour pressure (kPa degC-1)
180
+ def self.delta_svp(t)
181
+ tmp = 4098 * (0.6108 * Math.exp((17.27 * t) / (t + 237.3)))
182
+ return tmp / ((t + 237.3) ** 2)
183
+ end
184
+
185
+ # Convert energy (e.g. radiation energy) in MJ m-2 day-1 to the equivalent
186
+ # evaporation, assuming a grass reference crop.
187
+ #
188
+ # Energy is converted to equivalent evaporation using a conversion
189
+ # factor equal to the inverse of the latent heat of vapourisation
190
+ # (1 / lambda = 0.408).
191
+ #
192
+ # Based on FAO equation 20 in Allen et al (1998).
193
+ #
194
+ # @param energy [Float] Energy e.g. radiation or heat flux (MJ m-2 day-1)
195
+ # @return [Float] Equivalent evaporation (mm day-1)
196
+ def self.energy2evap(energy)
197
+ return 0.408 * energy
198
+ end
199
+
200
+ # Estimate daily extraterrestrial radiation (*Ra*, 'top of the atmosphere
201
+ # radiation').
202
+ #
203
+ # Based on equation 21 in Allen et al (1998). If monthly mean radiation is
204
+ # required make sure *sol_dec*. *sha* and *irl* have been calculated using
205
+ # the day of the year that corresponds to the middle of the month.
206
+ #
207
+ # **Note**: From Allen et al (1998): "For the winter months in latitudes
208
+ # greater than 55 degrees (N or S), the equations have limited validity.
209
+ # Reference should be made to the Smithsonian Tables to assess possible
210
+ # deviations."
211
+ #
212
+ # @param latitude [Float] Latitude (radians)
213
+ # @param sol_dec [Float] Solar declination (radians). Can be calculated
214
+ # using sol_dec()
215
+ # @param sha [Float] Sunset hour angle (radians). Can be calculated using
216
+ # sunset_hour_angle()
217
+ # @param ird [Float] Inverse relative distance earth-sun (dimensionless).
218
+ # Can be calculated using inv_rel_dist_earth_sun()
219
+ # @return [Float] Daily extraterrestrial radiation (MJ m-2 day-1)
220
+ def self.et_rad(latitude, sol_dec, sha, ird)
221
+ Validation.check_latitude_rad(latitude)
222
+ Validation.check_sol_dec_rad(sol_dec)
223
+ Validation.check_sunset_hour_angle_rad(sha)
224
+
225
+ tmp1 = (24.0 * 60.0) / Math::PI
226
+ tmp2 = sha * Math.sin(latitude) * Math.sin(sol_dec)
227
+ tmp3 = Math.cos(latitude) * Math.cos(sol_dec) * Math.sin(sha)
228
+ return tmp1 * SOLAR_CONSTANT * ird * (tmp2 + tmp3)
229
+ end
230
+
231
+ # Estimate reference evapotranspiration (ETo) from a hypothetical
232
+ # short grass reference surface using the FAO-56 Penman-Monteith equation.
233
+ #
234
+ # Based on equation 6 in Allen et al (1998).
235
+ #
236
+ # @param net_rad [Float] Net radiation at crop surface (MJ m-2 day-1). If
237
+ # necessary this can be estimated using net_rad()
238
+ # @param t [Float] Air temperature at 2 m height (deg Kelvin)
239
+ # @param ws [Float] Wind speed at 2 m height (m s-1). If not measured at 2m,
240
+ # convert using wind_speed_at_2m()
241
+ # @param svp [Float] Saturation vapour pressure (kPa). Can be estimated
242
+ # using svp_from_t()
243
+ # @param avp [Float] Actual vapour pressure (kPa). Can be estimated using a
244
+ # range of functions with names beginning with 'avp_from'
245
+ # @param delta_svp [Float] Slope of saturation vapour pressure curve
246
+ # (kPa degC-1). Can be estimated using delta_svp()
247
+ # @param psy [Float] Psychrometric constant (kPa deg C). Can be estimatred
248
+ # using psy_const_of_psychrometer() or psy_const()
249
+ # @param shf [Float] Soil heat flux (G) (MJ m-2 day-1) (default is 0.0,
250
+ # which is reasonable for a daily or 10-day time steps). For monthly time
251
+ # steps *shf* can be estimated using monthly_soil_heat_flux() or
252
+ # monthly_soil_heat_flux2()
253
+ # @return [Float] Reference evapotranspiration (ETo) from a hypothetical
254
+ # grass reference surface (mm day-1)
255
+ def self.fao56_penman_monteith(net_rad, t, ws, svp, avp, delta_svp, psy, shf=0.0)
256
+ a1 = (0.408 * (net_rad - shf) * delta_svp / delta_svp + (psy * (1 + 0.34 * ws)))
257
+ a2 = (900 * ws / t * (svp - avp) * psy / (delta_svp + (psy * (1 + 0.34 * ws))))
258
+ return a1 + a2
259
+ end
260
+
261
+ # Estimate reference evapotranspiration over grass (ETo) using the Hargreaves
262
+ # equation.
263
+ #
264
+ # Generally, when solar radiation data, relative humidity data
265
+ # and/or wind speed data are missing, it is better to estimate them using
266
+ # the functions available in this module, and then calculate ETo
267
+ # the FAO Penman-Monteith equation. However, as an alternative, ETo can be
268
+ # estimated using the Hargreaves ETo equation.
269
+ #
270
+ # Based on equation 52 in Allen et al (1998).
271
+ #
272
+ # @param tmin [Float] Minimum daily temperature (deg C)
273
+ # @param tmax [Float] Maximum daily temperature (deg C)
274
+ # @param tmean [Float] Mean daily temperature (deg C). If measurements not
275
+ # available it can be estimated as (*tmin* + *tmax*) / 2
276
+ # @param et_rad [Float] Extraterrestrial radiation (Ra) (MJ m-2 day-1).
277
+ # Can be estimated using et_rad()
278
+ # @return [Float] Reference evapotranspiration over grass (ETo) (mm day-1)
279
+ def self.hargreaves(tmin, tmax, tmean, et_rad)
280
+ # Note, multiplied by 0.408 to convert extraterrestrial radiation could
281
+ # be given in MJ m-2 day-1 rather than as equivalent evaporation in
282
+ # mm day-1
283
+ return 0.0023 * (tmean + 17.8) * (tmax - tmin) ** 0.5 * 0.408 * et_rad
284
+ end
285
+
286
+ # Calculate the inverse relative distance between earth and sun from
287
+ # day of the year.
288
+ #
289
+ # Based on FAO equation 23 in Allen et al (1998).
290
+ #
291
+ # @param day_of_year [Integer] Day of the year (1 to 366)
292
+ # @return [Float] Inverse relative distance between earth and the sun
293
+ def self.inv_rel_dist_earth_sun(day_of_year)
294
+ Validation.check_doy(day_of_year)
295
+ return 1 + (0.033 * Math.cos((2.0 * Math::PI / 365.0) * day_of_year))
296
+ end
297
+
298
+ # Estimate mean saturation vapour pressure, *es* [kPa] from minimum and
299
+ # maximum temperature.
300
+ #
301
+ # Based on equations 11 and 12 in Allen et al (1998).
302
+ #
303
+ # Mean saturation vapour pressure is calculated as the mean of the
304
+ # saturation vapour pressure at tmax (maximum temperature) and tmin
305
+ # (minimum temperature).
306
+ #
307
+ # @param tmin [Float] Minimum temperature (deg C)
308
+ # @param tmax [Float] Maximum temperature (deg C)
309
+ # @return [Float] Mean saturation vapour pressure (*es*) (kPa)
310
+ def self.mean_svp(tmin, tmax)
311
+ return (svp_from_t(tmin) + svp_from_t(tmax)) / 2.0
312
+ end
313
+
314
+ # Estimate monthly soil heat flux (Gmonth) [MJ m-2 day-1] from the mean
315
+ # air temperature of the previous and current month, assuming a grass crop.
316
+ #
317
+ # Based on equation 44 in Allen et al (1998). If the air temperature of the
318
+ # next month is available, use monthly_soil_heat_flux() instead. The
319
+ # resulting heat flux can be converted to equivalent evaporation (mm day-1)
320
+ # using energy2evap().
321
+ #
322
+ # @param t_month_prev [Float] Mean air temperature of the previous month
323
+ # (deg Celsius)
324
+ # @param t_month_next [Float] Mean air temperature of the next month
325
+ # (deg Celsius)
326
+ # @return [Float] Monthly soil heat flux (Gmonth) (MJ m-2 day-1)
327
+ def self.monthly_soil_heat_flux(t_month_prev, t_month_next)
328
+ return 0.07 * (t_month_next - t_month_prev)
329
+ end
330
+
331
+ # Estimate monthly soil heat flux (Gmonth) from the mean air temperature of
332
+ # the previous and next month, assuming a grass crop.
333
+ #
334
+ # Based on equation 44 in Allen et al (1998). If the air temperature of the
335
+ # next month is available, use monthly_soil_heat_flux() instead. The
336
+ # resulting heat flux can be converted to equivalent evaporation [mm day-1]
337
+ # using ``energy2evap().
338
+ #
339
+ # @param t_month_prev [Float] Mean air temperature of the previous month
340
+ # (deg Celsius)
341
+ # @param t_month_next [Float] Mean air temperature of the current month
342
+ # (deg Celsius)
343
+ # @return [Float] Monthly soil heat flux (Gmonth) (MJ m-2 day-1)
344
+ def self.monthly_soil_heat_flux2(t_month_prev, t_month_cur)
345
+ return 0.14 * (t_month_cur - t_month_prev)
346
+ end
347
+
348
+ # Calculate net incoming solar (or shortwave) radiation from gross
349
+ # incoming solar radiation, assuming a grass reference crop.
350
+ #
351
+ # Net incoming solar radiation is the net shortwave radiation resulting
352
+ # from the balance between incoming and reflected solar radiation. The
353
+ # output can be converted to equivalent evaporation [mm day-1] using
354
+ # energy2evap().
355
+ #
356
+ # Based on FAO equation 38 in Allen et al (1998).
357
+ #
358
+ # @param sol_rad [Float] Gross incoming solar radiation (MJ m-2 day-1).
359
+ # If necessary this can be estimated using functions whose name begins
360
+ # with 'sol_rad_from'
361
+ # @param albedo [Float] Albedo of the crop as the proportion of gross
362
+ # incoming solar radiation that is reflected by the surface. Default value
363
+ # is 0.23, which is the value used by the FAO for a short grass reference
364
+ # crop. Albedo can be as high as 0.95 for freshly fallen snow and as low
365
+ # as 0.05 for wet bare soil. A green vegetation over has an albedo of
366
+ # about 0.20-0.25 (Allen et al, 1998)
367
+ # @return [Float] Net incoming solar (or shortwave) radiation (MJ m-2 day-1)
368
+ def self.net_in_sol_rad(sol_rad, albedo=0.23)
369
+ return (1 - albedo) * sol_rad
370
+ end
371
+
372
+ # Estimate net outgoing longwave radiation.
373
+ #
374
+ # This is the net longwave energy (net energy flux) leaving the
375
+ # earth's surface. It is proportional to the absolute temperature of
376
+ # the surface raised to the fourth power according to the Stefan-Boltzmann
377
+ # law. However, water vapour, clouds, carbon dioxide and dust are absorbers
378
+ # and emitters of longwave radiation. This function corrects the Stefan-
379
+ # Boltzmann law for humidity (using actual vapor pressure) and cloudiness
380
+ # (using solar radiation and clear sky radiation). The concentrations of all
381
+ # other absorbers are assumed to be constant.
382
+ #
383
+ # The output can be converted to equivalent evaporation [mm day-1] using energy2evap().
384
+ #
385
+ # Based on FAO equation 39 in Allen et al (1998).
386
+ #
387
+ # @param tmin [Float] Absolute daily minimum temperature (degrees Kelvin)
388
+ # @param albedo [Float] Absolute daily maximum temperature (degrees Kelvin)
389
+ # @param sol_rad [Float] Solar radiation (MJ m-2 day-1). If necessary this
390
+ # can be estimated using sol+rad()
391
+ # @param cs_rad [Float] Clear sky radiation (MJ m-2 day-1). Can be estimated
392
+ # using cs_rad()
393
+ # @param avp [Float] Actual vapour pressure (kPa). Can be estimated using
394
+ # functions with names beginning with 'avp_from'
395
+ # @return [Float] Net outgoing longwave radiation (MJ m-2 day-1)
396
+ def self.net_out_lw_rad(tmin, tmax, sol_rad, cs_rad, avp)
397
+ tmp1 = (STEFAN_BOLTZMANN_CONSTANT * (((tmax ** 4) + (tmin ** 4)) / 2))
398
+ tmp2 = (0.34 - (0.14 * Math.sqrt(avp)))
399
+ tmp3 = 1.35 * (sol_rad / cs_rad) - 0.35
400
+ return tmp1 * tmp2 * tmp3
401
+ end
402
+
403
+ # Calculate daily net radiation at the crop surface, assuming a grass
404
+ # reference crop.
405
+ #
406
+ # Net radiation is the difference between the incoming net shortwave (or
407
+ # solar) radiation and the outgoing net longwave radiation. Output can be
408
+ # converted to equivalent evaporation [mm day-1] using energy2evap().
409
+ #
410
+ # Based on equation 40 in Allen et al (1998).
411
+ #
412
+ # @param ni_sw_rad [Float] Net incoming shortwave radiation (MJ m-2 day-1).
413
+ # Can be estimated using net_in_sol_rad()
414
+ # @param no_lw_rad [Float] Net outgoing longwave radiation (MJ m-2 day-1).
415
+ # Can be estimated using net_out_lw_rad()
416
+ # @return [Float] Daily net radiation (MJ m-2 day-1)
417
+ def self.net_rad(ni_sw_rad, no_lw_rad)
418
+ return ni_sw_rad - no_lw_rad
419
+ end
420
+
421
+ # Calculate the psychrometric constant.
422
+ #
423
+ # This method assumes that the air is saturated with water vapour at the
424
+ # minimum daily temperature. This assumption may not hold in arid areas.
425
+ #
426
+ # Based on equation 8, page 95 in Allen et al (1998).
427
+ #
428
+ # @param atmos_pres [Float] Atmospheric pressure (kPa). Can be estimated
429
+ # using atm_pressure()
430
+ # @return [Float] Psychrometric constant (kPa degC-1)
431
+ def self.psy_const(atmos_pres)
432
+ return 0.000665 * atmos_pres
433
+ end
434
+
435
+ # Calculate the psychrometric constant for different types of
436
+ # psychrometer at a given atmospheric pressure.
437
+ #
438
+ # Based on FAO equation 16 in Allen et al (1998).
439
+ #
440
+ # psychrometer types:
441
+ # 1. ventilated (Asmann or aspirated type) psychrometer with an air movement of approximately 5 m/s
442
+ # 2. natural ventilated psychrometer with an air movement of approximately 1 m/s
443
+ # 3. non ventilated psychrometer installed indoors
444
+ #
445
+ # @param psychrometer [Float] Integer between 1 and 3 which denotes type of
446
+ # psychrometer
447
+ # @param atmos_pres [Float] Atmospheric pressure [kPa]. Can be estimated
448
+ # using atm_pressure()
449
+ # @return [Float] Psychrometric constant (kPa degC-1)
450
+ def self.psy_const_of_psychrometer(psychrometer, atmos_pres)
451
+ # Select coefficient based on type of ventilation of the wet bulb
452
+ case psychrometer
453
+ when 1
454
+ psy_coeff = 0.000662
455
+ when 2
456
+ psy_coeff = 0.000800
457
+ when 3
458
+ psy_coeff = 0.001200
459
+ else
460
+ raise ArgumentError.new("psychrometer should be in range 1 to 3: #{psychrometer}")
461
+ end
462
+
463
+ return psy_coeff * atmos_pres
464
+ end
465
+
466
+ # Calculate relative humidity as the ratio of actual vapour pressure
467
+ # to saturation vapour pressure at the same temperature.
468
+ #
469
+ # See Allen et al (1998), page 67 for details.
470
+ #
471
+ # @param avp [Float] Actual vapour pressure (units do not matter so long as
472
+ # they are the same as for *svp*). Can be estimated using functions whose
473
+ # name begins with 'avp_from'
474
+ # @param svp [Float] Saturated vapour pressure (units do not matter so long
475
+ # as they are the same as for *avp*). Can be estimated using svp_from_t()
476
+ # @return [Float] Relative humidity (%)
477
+ def self.rh_from_avp_svp(avp, svp)
478
+ return 100.0 * avp / svp
479
+ end
480
+
481
+ # Calculate solar declination from day of the year.
482
+ #
483
+ # Based on FAO equation 24 in Allen et al (1998).
484
+ #
485
+ # @param day_of_year [Integer] Day of year integer between 1 and 365 or 366
486
+ # @return [Float] solar declination (radians)
487
+ def self.sol_dec(day_of_year)
488
+ Validation.check_doy(day_of_year)
489
+ return 0.409 * Math.sin(((2.0 * Math::PI / 365.0) * day_of_year - 1.39))
490
+ end
491
+
492
+ # Calculate incoming solar (or shortwave) radiation, *Rs* (radiation hitting
493
+ # a horizontal plane after scattering by the atmosphere) from relative
494
+ # sunshine duration.
495
+ #
496
+ # If measured radiation data are not available this method is preferable
497
+ # to calculating solar radiation from temperature. If a monthly mean is
498
+ # required then divide the monthly number of sunshine hours by number of
499
+ # days in the month and ensure that *et_rad* and *daylight_hours* was
500
+ # calculated using the day of the year that corresponds to the middle of
501
+ # the month.
502
+ #
503
+ # Based on equations 34 and 35 in Allen et al (1998).
504
+ #
505
+ # @param dl_hours [Integer] Number of daylight hours (hours). Can be
506
+ # calculated using daylight_hours()
507
+ # @param sunshine_hours [Integer] Sunshine duration (hours). Can be
508
+ # calculated using sunshine_hours()
509
+ # @param et_rad [Float] Extraterrestrial radiation (MJ m-2 day-1). Can be
510
+ # estimated using et_rad()
511
+ # @return [Float] Incoming solar (or shortwave) radiation (MJ m-2 day-1)
512
+ def self.sol_rad_from_sun_hours(daylight_hours, sunshine_hours, et_rad)
513
+ Validation.check_day_hours(sunshine_hours, 'sun_hours')
514
+ Validation.check_day_hours(daylight_hours, 'daylight_hours')
515
+
516
+ # 0.5 and 0.25 are default values of regression constants (Angstrom values)
517
+ # recommended by FAO when calibrated values are unavailable.
518
+ return (0.5 * sunshine_hours / daylight_hours + 0.25) * et_rad
519
+ end
520
+
521
+ # Estimate incoming solar (or shortwave) radiation, *Rs*, (radiation hitting
522
+ # a horizontal plane after scattering by the atmosphere) from min and max
523
+ # temperature together with an empirical adjustment coefficient for
524
+ # 'interior' and 'coastal' regions.
525
+ #
526
+ # The formula is based on equation 50 in Allen et al (1998) which is the
527
+ # Hargreaves radiation formula (Hargreaves and Samani, 1982, 1985). This
528
+ # method should be used only when solar radiation or sunshine hours data are
529
+ # not available. It is only recommended for locations where it is not
530
+ # possible to use radiation data from a regional station (either because
531
+ # climate conditions are heterogeneous or data are lacking).
532
+ #
533
+ # **NOTE**: this method is not suitable for island locations due to the
534
+ # moderating effects of the surrounding water.
535
+ #
536
+ # @param et_rad [Float] Extraterrestrial radiation (MJ m-2 day-1). Can be
537
+ # estimated using et_rad()
538
+ # @param cs_rad [Float] Clear sky radiation (MJ m-2 day-1). Can be estimated
539
+ # using cs_rad()
540
+ # @param tmin [Float] Daily minimum temperature (deg C)
541
+ # @param tmax [Float] Daily maximum temperature (deg C)
542
+ # @param coastal [Boolean] True if site is a coastal location, situated on
543
+ # or adjacent to coast of a large land mass and where air masses are
544
+ # influenced by a nearby water body, False if interior location where land
545
+ # mass dominates and air masses are not strongly influenced by a large
546
+ # water body.
547
+ # @return [Float] Incoming solar (or shortwave) radiation (Rs) (MJ m-2 day-1)
548
+ def self.sol_rad_from_t(et_rad, cs_rad, tmin, tmax, coastal)
549
+ # Determine value of adjustment coefficient [deg C-0.5] for
550
+ # coastal/interior locations
551
+ adj = coastal ? 0.19 : 0.16
552
+
553
+ sol_rad = adj * Math.sqrt(tmax - tmin) * et_rad
554
+
555
+ # The solar radiation value is constrained by the clear sky radiation
556
+ return [sol_rad, cs_rad].min
557
+ end
558
+
559
+ # Estimate incoming solar (or shortwave) radiation, *Rs* (radiation hitting
560
+ # a horizontal plane after scattering by the atmosphere) for an island
561
+ # location.
562
+ #
563
+ # An island is defined as a land mass with width perpendicular to the
564
+ # coastline <= 20 km. Use this method only if radiation data from
565
+ # elsewhere on the island is not available.
566
+ #
567
+ # **NOTE**: This method is only applicable for low altitudes (0-100 m)
568
+ # and monthly calculations.
569
+ #
570
+ # Based on FAO equation 51 in Allen et al (1998).
571
+ #
572
+ # @param et_rad [Float] Extraterrestrial radiation (MJ m-2 day-1). Can be
573
+ # estimated using et_rad()
574
+ # @return [Float] Incoming solar (or shortwave) radiation (MJ m-2 day-1)
575
+ def self.sol_rad_island(et_rad)
576
+ return (0.7 * et_rad) - 4.0
577
+ end
578
+
579
+ # Calculate sunset hour angle (*Ws*) from latitude and solar
580
+ # declination.
581
+ #
582
+ # Based on FAO equation 25 in Allen et al (1998).
583
+ #
584
+ # @param latitude [Float] Latitude (radians). Note: *latitude* should be
585
+ # negative if it in the southern hemisphere, positive if in the northern
586
+ # hemisphere
587
+ # @param sol_dec [Float] Solar declination (radians). Can be calculated
588
+ # using sol_dec()
589
+ # @return [Float] Sunset hour angle (radians)
590
+ def self.sunset_hour_angle(latitude, sol_dec)
591
+ Validation.check_latitude_rad(latitude)
592
+ Validation.check_sol_dec_rad(sol_dec)
593
+
594
+ cos_sha = -Math.tan(latitude) * Math.tan(sol_dec)
595
+ # If tmp is >= 1 there is no sunset, i.e. 24 hours of daylight
596
+ # If tmp is <= 1 there is no sunrise, i.e. 24 hours of darkness
597
+ # See http://www.itacanet.org/the-sun-as-a-source-of-energy/
598
+ # part-3-calculating-solar-angles/
599
+ # Domain of acos is -1 <= x <= 1 radians (this is not mentioned in FAO-56!)
600
+ return Math.acos([[cos_sha, -1.0].max, 1.0].min)
601
+ end
602
+
603
+ # Estimate saturation vapour pressure (*es*) from air temperature.
604
+ #
605
+ # Based on equations 11 and 12 in Allen et al (1998).
606
+ #
607
+ # @param t [Float] Temperature (deg C)
608
+ # @return [Float] Saturation vapour pressure (kPa)
609
+ def self.svp_from_t(t)
610
+ return 0.6108 * Math.exp((17.27 * t) / (t + 237.3))
611
+ end
612
+
613
+ # Convert wind speed measured at different heights above the soil
614
+ # surface to wind speed at 2 m above the surface, assuming a short grass
615
+ # surface.
616
+ #
617
+ # Based on FAO equation 47 in Allen et al (1998).
618
+ #
619
+ # @param ws [Float] Measured wind speed (m s-1)
620
+ # @param z [Float] Height of wind measurement above ground surface (m)
621
+ # @return [Float] Wind speed at 2 m above the surface (m s-1)
622
+ def self.wind_speed_2m(ws, z)
623
+ return ws * (4.87 / Math.log((67.8 * z) - 5.42))
624
+ end
625
+
626
+ end
627
+ end
@@ -0,0 +1,122 @@
1
+ require 'evapotranspiration/validation'
2
+ require 'evapotranspiration/fao'
3
+
4
+ module Evapotranspiration
5
+
6
+ # Calculate potential evapotranspiration using the Thornthwaite (1948 method)
7
+ #
8
+ # References
9
+ # ----------
10
+ # Thornthwaite CW (1948) An approach toward a rational classification of
11
+ # climate. Geographical Review, 38, 55-94.
12
+ module Thornthwaite
13
+
14
+ MONTHDAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
15
+ LEAP_MONTHDAYS = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
16
+
17
+ # Estimate monthly potential evapotranspiration (PET) using the
18
+ # Thornthwaite (1948) method.
19
+ #
20
+ # Thornthwaite equation:
21
+ #
22
+ # *PET* = 1.6 (*l*/12) (*n*/30) (10*ta* / *heat_index*)***a*
23
+ #
24
+ # where:
25
+ #
26
+ # * *ta* is the mean daily air temperature [deg C, if negative use 0] of the
27
+ # month being calculated
28
+ # * *n* is the number of days in the month being calculated
29
+ # * *l* is the mean day length [hours] of the month being calculated
30
+ # * *a* = (6.75 x 10-7)*heat_index***3 - (7.71 x 10-5)*heat_index***2 + (1.792 x 10-2)*heat_index* + 0.49239
31
+ # * *heat_index* is a heat index which depends on the 12 monthly mean temperatures and
32
+ # is calculated as the sum of (*tai* / 5)**1.514 for each month, where
33
+ # * *tai* is the air temperature for each month in the year
34
+ #
35
+ # @param monthly_t [Array<Float>] Iterable containing mean daily air
36
+ # temperature for each month of the year (deg C)
37
+ # @param monthly_mean_dlh [Array<Float>] Iterable containing mean daily
38
+ # daylight hours for each month of the year (hours). These can be
39
+ # calculated using monthly_mean_daylight_hours()
40
+ # @param year [Integer] Year for which PET is required. The only effect of
41
+ # year is to change the number of days in February to 29 if it is a leap
42
+ # year. If it is left as the default (None), then the year is assumed not
43
+ # to be a leap year.
44
+ # @return [Array<Float>] Estimated monthly potential evaporation of each month of
45
+ # the year (mm/month)
46
+ def thornthwaite(monthly_t, monthly_mean_dlh, year=nil)
47
+ if monthly_t.size != 12
48
+ raise ArgumentError.new("monthly_t should be length 12 but is length #{monthly_t.size}.")
49
+ end
50
+ if monthly_mean_dlh.size != 12
51
+ raise ArgumentError.new("monthly_mean_dlh should be length 12 but is length #{monthly_mean_dlh.size}.")
52
+ end
53
+
54
+ if year.nil? || !year.leap?
55
+ month_days = MONTHDAYS
56
+ else
57
+ month_days = LEAP_MONTHDAYS
58
+ end
59
+
60
+ # Negative temperatures should be set to zero
61
+ adj_monthly_t = []
62
+ monthly_t.each do |t|
63
+ adj_monthly_t << (t >= 0 ? t : 0)
64
+ end
65
+
66
+ # Calculate the heat index (heat_index)
67
+ heat_index = 0.0
68
+ adj_monthly_t.each do |tai|
69
+ if (tai / 5.0) > 0.0
70
+ heat_index += (tai / 5.0) ** 1.514
71
+ end
72
+ end
73
+
74
+ a = (6.75e-07 * heat_index ** 3) - (7.71e-05 * heat_index ** 2) + (1.792e-02 * heat_index) + 0.49239
75
+
76
+ pet = []
77
+ adj_monthly_t.zip(monthly_mean_dlh, month_days).each do |ta, l, n|
78
+ # Multiply by 10 to convert cm/month --> mm/month
79
+ pet << 1.6 * (l / 12.0) * (n / 30.0) * ((10.0 * ta / heat_index) ** a) * 10.0
80
+ end
81
+ return pet
82
+ end
83
+
84
+ # Calculate mean daylight hours for each month of the year for a given
85
+ # latitude.
86
+ #
87
+ # @param latitude [Float] Latitude (radians)
88
+ # @param year [Integer] Year for the daylight hours are required. The only effect of
89
+ # *year* is to change the number of days in Feb to 29 if it is a leap
90
+ # year. If left as the default, None, then a normal (non-leap) year is
91
+ # assumed.
92
+ # @return [Array<Float>] Mean daily daylight hours of each month of a year
93
+ # (hours)
94
+ def monthly_mean_daylight_hours(latitude, year=nil)
95
+ Validation.check_latitude_rad(latitude)
96
+
97
+ if year.nil? || !year.leap?
98
+ month_days = MONTHDAYS
99
+ else
100
+ month_days = LEAP_MONTHDAYS
101
+ end
102
+ monthly_mean_dlh = []
103
+ doy = 1 # Day of the year
104
+
105
+ month_days.each do |mdays|
106
+ dlh = 0.0 # Cumulative daylight hours for the month
107
+ [1..(mdays + 1)].each do |daynum|
108
+ sd = FAO.sol_dec(doy)
109
+ sha = FAO.sunset_hour_angle(latitude, sd)
110
+ dlh += FAO.daylight_hours(sha)
111
+ doy += 1
112
+ end
113
+ # Calc mean daylight hours of the month
114
+ monthly_mean_dlh << (dlh / mdays)
115
+ end
116
+
117
+ return monthly_mean_dlh
118
+ end
119
+
120
+ end
121
+
122
+ end
@@ -0,0 +1,55 @@
1
+ require 'evapotranspiration/conversion'
2
+
3
+ module Evapotranspiration
4
+ module Validation
5
+
6
+ # Latitude
7
+ MINLAT_RADIANS = Conversion.deg_to_rad(-90.0)
8
+ MAXLAT_RADIANS = Conversion.deg_to_rad(90.0)
9
+
10
+ # Solar declination
11
+ MINSOLDEC_RADIANS = Conversion.deg_to_rad(-23.5)
12
+ MAXSOLDEC_RADIANS = Conversion.deg_to_rad(23.5)
13
+
14
+ # Sunset hour angle
15
+ MINSHA_RADIANS = 0.0
16
+ MAXSHA_RADIANS = Conversion.deg_to_rad(180)
17
+
18
+ # Check that *hours* is in the range 1 to 24
19
+ def self.check_day_hours(hours, arg_name)
20
+ unless hours.between?(0,24)
21
+ raise ArgumentError.new("#{arg_name} should be in the range 0-24: #{hours}")
22
+ end
23
+ end
24
+
25
+ # Check day of the year is valid
26
+ def self.check_doy(doy)
27
+ unless doy.between?(1,366)
28
+ raise ArgumentError.new("day of the year (doy) must be in range 1-366: #{doy}")
29
+ end
30
+ end
31
+
32
+ def self.check_latitude_rad(latitude)
33
+ unless latitude.between?(MINLAT_RADIANS,MAXLAT_RADIANS)
34
+ raise ArgumentError.new("latitude outside valid range #{MINLAT_RADIANS} to #{MAXLAT_RADIANS} rad: #{latitude}")
35
+ end
36
+ end
37
+
38
+ # Solar declination can vary between -23.5 and +23.5 degrees.
39
+ # See http://mypages.iit.edu/~maslanka/SolarGeo.pdf
40
+ def self.check_sol_dec_rad(sd)
41
+ unless sd.between?(MINSOLDEC_RADIANS,MAXSOLDEC_RADIANS)
42
+ raise ArgumentError.new("solar declination outside valid range #{MINSOLDEC_RADIANS} to #{MAXSOLDEC_RADIANS} rad: #{sd}")
43
+ end
44
+ end
45
+
46
+ # Sunset hour angle has the range 0 to 180 degrees.
47
+ # See http://mypages.iit.edu/~maslanka/SolarGeo.pdf
48
+ def self.check_sunset_hour_angle_rad(sha)
49
+ unless sha.between?(MINSHA_RADIANS,MAXSHA_RADIANS)
50
+ raise ArgumentError.new("sunset hour angle outside valid range #{MINSHA_RADIANS} to #{MAXSHA_RADIANS} rad: #{sha}")
51
+ end
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,3 @@
1
+ module Evapotranspiration
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: evapotranspiration
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Bryce Johnston
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-06-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Ruby library for calculating reference crop evapotranspiration (ETo),
56
+ also referred to as potential evapotranspiration (PET), using the FAO-56 Penman-Monteith
57
+ method. This is a Ruby port of Mark Richard's PyETo Python package.
58
+ email:
59
+ - johnstonbrc@gmail.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".gitignore"
65
+ - ".rspec"
66
+ - ".travis.yml"
67
+ - Gemfile
68
+ - LICENSE-ORIGINAL.txt
69
+ - LICENSE.txt
70
+ - README.md
71
+ - Rakefile
72
+ - bin/console
73
+ - bin/setup
74
+ - evapotranspiration.gemspec
75
+ - lib/evapotranspiration.rb
76
+ - lib/evapotranspiration/conversion.rb
77
+ - lib/evapotranspiration/fao.rb
78
+ - lib/evapotranspiration/thornthwaite.rb
79
+ - lib/evapotranspiration/validation.rb
80
+ - lib/evapotranspiration/version.rb
81
+ homepage: https://github.com/AgRuby/evapotranspiration
82
+ licenses:
83
+ - BSD 3-Clause
84
+ metadata: {}
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 2.6.4
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: Ruby library for calculating reference crop evapotranspiration (ETo)
105
+ test_files: []
106
+ has_rdoc: