diff --git a/README.md b/README.md index 5702f26..b826381 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,11 @@ Recording solar radiation gets us the most accurate ETo: # TODO + * rounding consistency. Currently rounding of decimal points for floating point numbers + follow the [original paper][1]'s usage examples. I did so in order to be able + to test examples used in the paper. However this is far from ideal. This + issue of significant digits must be revisited in future revisions! + * *import_data()* must be supported by *penmon.eto.Station* class to import bulk data into the station. diff --git a/setup.py b/setup.py index 3305aa7..68d3914 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="penmon", - version="0.2.0", + version="0.2.1", description="Implementation of Penman-Monteith equation to calculate ET for a reference crop", # long_description_content_type="text/markdown", # long_description=long_description, @@ -11,7 +11,7 @@ author_email="sherzodr@gmail.com", license="MIT", url="https://github.com/sherzodr/penmon", - download_url="https://github.com/sherzodr/penmon/archive/0.2.0.tar.gz", + download_url="https://github.com/sherzodr/penmon/archive/0.2.1.tar.gz", py_modules=["penmon.eto"], package_dir={'': 'src'}, packages=["penmon"], @@ -23,4 +23,3 @@ "Topic :: Scientific/Engineering :: Atmospheric Science" ] ) - diff --git a/src/penmon/eto.py b/src/penmon/eto.py index 530b5b4..68cd634 100644 --- a/src/penmon/eto.py +++ b/src/penmon/eto.py @@ -15,7 +15,6 @@ CHECK_RADIATION_RANGE = True CHECK_SUNSHINE_HOURS_RANGE = True - def is_number(s): try: float(s) @@ -23,7 +22,6 @@ def is_number(s): except ValueError: return False - class Station: """ Class that implements a weather station at a known latitude and elevation.""" @@ -50,6 +48,12 @@ def __init__(self, latitude, altitude, anemometer_height=2): * climate - set to default **Climate()** instance * ref_crop - instance of **Crop** class, which sets default chracteristics of the reference crop according to the paper. + + Should you wish to change assumes Climate and Crop characteristics + you can do so after the object is innitialized, like so: + + station=Station(41.42, 109) + station.ref_crop = Crop(albedo=0.25, height=0.35) """ if not type(latitude) is float: @@ -81,6 +85,7 @@ def day(self, day_number): def get_day(self, day_number, date_template="%Y-%m-%d", temp_min=None, temp_max=None, + temp_mean=None, wind_speed=None, humidity_mean=None, radiation_s=None, @@ -135,6 +140,7 @@ def get_day(self, day_number, date_template="%Y-%m-%d", self.days[day_number] = day day.temp_min = temp_min day.temp_max = temp_max + day.temp_mean=temp_mean day.humidity_mean = humidity_mean day.wind_speed = wind_speed @@ -206,7 +212,6 @@ def __init__(self, day_number, station): - vapour_pressure - wind_speed - radiation_s - - radiation_a - stephan_boltzman_constant - climate - convenient reference to station.climate - sunshine_hours @@ -224,7 +229,6 @@ def __init__(self, day_number, station): self.humidity_max = None self.wind_speed = None self.radiation_s = None - self.radiation_a = None self.temp_dew = None self.temp_dry = None self.temp_wet = None @@ -254,7 +258,7 @@ def wind_speed_2m(self): # speed at 2m if self.wind_speed and self.station.anemometer_height != 2: return round(self.wind_speed * (4.87 / - math.log(67.8 * self.station.anemometer_height - 5.42)), 1) + math.log(67.8 * self.station.anemometer_height - 5.42)), 1) # if we reach this far no wind information is available to work with. we # consult if station has any climatic data, in which case we try to @@ -551,6 +555,9 @@ def R_nl(self): """ Net longwave radiation. (Eq. 39) """ + + if not ( self.temp_max and self.temp_min ): + raise Exception("Net longwave radiation cannot be calculated without min/max temperature") TmaxK = self.temp_max + 273.16 TminK = self.temp_min + 273.16 @@ -560,7 +567,7 @@ def R_nl(self): sb_constant = self.stephan_boltzmann_constant return round(sb_constant * ((TmaxK ** 4 + TminK ** 4) / 2) * - (0.34 - 0.14 * math.sqrt(ea)) * + (0.34 - 0.14 * math.sqrt(ea)) * (1.35 * (rs / rso) - 0.35), 1) def net_radiation(self): @@ -568,7 +575,11 @@ def net_radiation(self): Net Radiation. (Eq. 40) """ ns = self.R_ns() - nl = self.R_nl() + + try: + nl = self.R_nl() + except Exception as e: + raise(str(e)) if (not ns is None) and (not nl is None): return round(ns - nl, 1) @@ -613,31 +624,32 @@ def eto(self): Eq. 6 """ - Tmax = self.temp_max - Tmin = self.temp_min - # if we cannot get wind speed data we revert to Hargreaves formula. - # Which is not ideal! + # Which is not ideal! This can happen only if user removed default 'climate' + # reference if not self.wind_speed_2m(): return self.eto_hargreaves() - if Tmax and Tmin: - Tmean = (Tmax + Tmin) / 2 - - slope_of_vp = self.slope_of_saturation_vapour_pressure(Tmean) - net_radiation = self.net_radiation() - G = self.soil_heat_flux() - u2m = self.wind_speed_2m() - eto_nominator = (0.408 * slope_of_vp * (net_radiation - G) + - self.psychrometric_constant() * (900 / (Tmean + 273)) * u2m * - self.vapour_pressure_deficit()) + if self.Tmean() == None: + raise Exception( + "Cannot calculate eto(): temp_mean (mean temperature) is missing") - eto_denominator = slope_of_vp + self.psychrometric_constant() * (1 + 0.34 * u2m) - - return round(eto_nominator / eto_denominator, 2) - - return None + try: + net_radiation = self.net_radiation() + except Exception as e: + raise(str(e)) + + Tmean=self.Tmean() + slope_of_vp = self.slope_of_saturation_vapour_pressure(Tmean) + G = self.soil_heat_flux() + u2m = self.wind_speed_2m() + eto_nominator = (0.408 * slope_of_vp * (net_radiation - G) + + self.psychrometric_constant() * (900 / (Tmean + 273)) * u2m * + self.vapour_pressure_deficit()) + + eto_denominator = slope_of_vp + self.psychrometric_constant() * (1 + 0.34 * u2m) + return round(eto_nominator / eto_denominator, 2) class Climate: diff --git a/tests/error_handling.py b/tests/error_handling.py index 2bbe0e3..f21e182 100644 --- a/tests/error_handling.py +++ b/tests/error_handling.py @@ -6,7 +6,6 @@ import unittest import penmon.eto as pm - class Test(unittest.TestCase): def test_smoke(self): @@ -15,7 +14,7 @@ def test_smoke(self): def test_type_error(self): try: - station = pm.Station(latitude=10, altitude=109) + pm.Station(latitude=10, altitude=109) except TypeError: self.assertTrue(True, "Exception was expected and raised") else: @@ -23,7 +22,7 @@ def test_type_error(self): def test_latitude_range_error(self): try: - station = pm.Station(latitude=100.0, altitude=100) + pm.Station(latitude=100.0, altitude=100) except: self.assertTrue(True, "Exception was expected and raised") else: @@ -31,7 +30,7 @@ def test_latitude_range_error(self): def test_altitude_range_error(self): try: - station = pm.Station(latitude=41.42, altitude=-1) + pm.Station(latitude=41.42, altitude=-1) except: self.assertTrue(True, "Exception was expected and raised") else: @@ -40,7 +39,7 @@ def test_altitude_range_error(self): def test_day_number_type(self): station = pm.Station(latitude=41.42, altitude=109) try: - day = station.get_day(365.0) + station.get_day(365.0) except TypeError: self.assertTrue(True, "Exception was expected and raised") else: @@ -50,21 +49,25 @@ def test_day_number_range(self): station = pm.Station(latitude=41.42, altitude=109) try: - day = station.get_day(367) + station.get_day(367) except: self.assertTrue(True, "Exception was expected and raised") else: self.assertTrue(False, "Exception was expected but was NOT raised") - def test_immature_eto(self): station = pm.Station(41.42, 109) - day = station.get_day(238) + day = station.get_day(238, temp_mean=25.00) + self.assertTrue(day.temp_mean != None, "temp_mean was set") + self.assertEqual(day.temp_mean, day.Tmean()) - eto = day.eto() - - self.assertEqual(eto, None) - + # following code should raise an exception: + try: + day.eto() + except Exception as e: + self.assertTrue(True, str(e)) + else: + self.assertTrue(False, "Exception was expected") if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName']