evapotranspiration 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: