Skip to content

Commit ca2f8ff

Browse files
authored
Merge pull request #244 from UW-Hydro/develop
Update for 2.4
2 parents 62b1209 + ac27663 commit ca2f8ff

File tree

8 files changed

+159
-9
lines changed

8 files changed

+159
-9
lines changed

docs/configuration.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ the ``forcing`` entry. Can be one of the following: ``ascii``, ``binary``,
3838

3939
**Optional Variables**
4040

41+
``method ::str``: The method to use for estimation of meteorological quantities.
42+
This can be either ``mtclim`` to estimate missing variables or ``passthrough`` if
43+
some of the meteorological variables have already been estimated (for example, by
44+
DayMet, PRISM, or GridMET). Defaults to ``mtclim``.
45+
4146
``out_prefix :: str``: The output file base name. Defaults to ``forcing``.
4247

4348
``out_precision :: str``: Precision to use when writing output. Defaults to

docs/whats-new.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
What's New
44
==========
55

6+
.. _whats-new.2.4.0:
7+
8+
v2.4.0
9+
------
10+
Enchancements
11+
~~~~~~~~~~~~~
12+
- Allow for passing already estimated met variables
13+
(such as shortwave and/or longwave radiation)
14+
through to the disaggregation routines. This
15+
functionality can be accessed by setting the
16+
``method`` to ``passthrough`` in the configuration
17+
618
.. _whats-new.2.3.0:
719

820
v2.3.3

metsim/data/passthrough.nc

249 KB
Binary file not shown.

metsim/disaggregate.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,13 @@ def disaggregate(df_daily: pd.DataFrame, params: dict,
105105

106106
df_disagg['tskc'] = tskc(df_daily['tskc'].values, ts, params)
107107

108+
if 'longwave' in df_daily:
109+
daily_lw = df_daily['longwave']
110+
else:
111+
daily_lw = None
108112
df_disagg['longwave'] = longwave(
109113
df_disagg['temp'].values, df_disagg['vapor_pressure'].values,
110-
df_disagg['tskc'].values, params)
114+
df_disagg['tskc'].values, params, daily_lw)
111115
df_disagg['prec'] = prec(df_daily['prec'], df_daily['t_min'], ts, params,
112116
df_daily.get('t_pk'), df_daily.get('dur'))
113117

@@ -478,7 +482,7 @@ def vapor_pressure(vp_daily: np.array, temp: np.array, t_t_min: np.array,
478482

479483

480484
def longwave(air_temp: np.array, vapor_pressure: np.array,
481-
tskc: np.array, params: dict) -> np.array:
485+
tskc: np.array, params: dict, daily_lw=None) -> np.array:
482486
"""
483487
Calculate longwave. This calculation can be performed
484488
using a variety of parameterizations for both the
@@ -569,6 +573,13 @@ def longwave(air_temp: np.array, vapor_pressure: np.array,
569573
emiss_func = cloud_calc[params['lw_cloud'].upper()]
570574
emissivity = emiss_func(emissivity_clear, tskc)
571575
lwrad = emissivity * cnst.STEFAN_B * np.power(air_temp, 4)
576+
if daily_lw is not None:
577+
ts = int(params['time_step'])
578+
ts_per_day = int(cnst.HOURS_PER_DAY * cnst.MIN_PER_HOUR / ts)
579+
factor = np.mean(lwrad.reshape(-1, ts_per_day), axis=1).flatten()
580+
factor = daily_lw.values / factor
581+
factor = np.repeat(factor, ts_per_day)
582+
lwrad *= factor
572583
return lwrad
573584

574585

@@ -623,14 +634,17 @@ def shortwave(sw_rad: np.array, daylength: np.array, day_of_year: np.array,
623634
"""
624635
ts = int(params['time_step'])
625636
ts_hourly = float(ts) / cnst.MIN_PER_HOUR
626-
tmp_rad = (sw_rad * daylength) / (cnst.SEC_PER_HOUR * ts_hourly)
637+
if params['method'] == 'mtclim' or params.get('sw_averaging', '') == 'daylight':
638+
tmp_rad = (sw_rad * daylength) / (cnst.SEC_PER_HOUR * ts_hourly)
639+
else:
640+
tmp_rad = sw_rad * 24
627641
n_days = len(tmp_rad)
628642
ts_per_day = int(cnst.HOURS_PER_DAY * cnst.MIN_PER_HOUR / ts)
629643
disaggrad = np.zeros(int(n_days * ts_per_day))
630644
rad_fract_per_day = int(cnst.SEC_PER_DAY / cnst.SW_RAD_DT)
631645
tmp_rad = np.repeat(tmp_rad, rad_fract_per_day)
632646
if params['utc_offset']:
633-
utc_offset = int((params['lon'] / cnst.DEG_PER_REV) * rad_fract_per_day)
647+
utc_offset = int((params['lon'] / cnst.DEG_PER_REV) * rad_fract_per_day)
634648
tiny_rad_fract = np.roll(tiny_rad_fract.flatten(), -utc_offset)
635649
tmp_rad = np.roll(tmp_rad.flatten(), -utc_offset)
636650
tiny_rad_fract = tiny_rad_fract.flatten()

metsim/methods/passthrough.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
Passthrough
3+
"""
4+
# Meteorology Simulator
5+
# Copyright (C) 2017 The Computational Hydrology Group, Department of Civil
6+
# and Environmental Engineering, University of Washington.
7+
8+
# This program is free software: you can redistribute it and/or modify
9+
# it under the terms of the GNU General Public License as published by
10+
# the Free Software Foundation, either version 3 of the License, or
11+
# (at your option) any later version.
12+
13+
# This program is distributed in the hope that it will be useful,
14+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
# GNU General Public License for more details.
17+
18+
# You should have received a copy of the GNU General Public License
19+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
21+
import numpy as np
22+
import pandas as pd
23+
24+
import metsim.constants as cnst
25+
from metsim.physics import atm_pres, calc_pet, svp
26+
from metsim.methods.mtclim import t_day, tfmax, tskc, pet, tdew, vapor_pressure
27+
28+
def run(df, params):
29+
assert 'shortwave' in df
30+
31+
if 't_day' not in df:
32+
df['t_day'] = t_day(df['t_min'].values, df['t_max'].values, params)
33+
if 'tfmax' not in df:
34+
df['tfmax'] = tfmax(df['dtr'].values, df['smoothed_dtr'].values,
35+
df['prec'].values, params)
36+
if 'tskc' not in df:
37+
df['tskc'] = tskc(df['tfmax'].values, params)
38+
if 'pet' not in df:
39+
df['pet'] = pet(df['shortwave'].values, df['t_day'].values,
40+
df['daylength'].values, params)
41+
if 'tdew' not in df:
42+
df['tdew'] = tdew(df['pet'].values, df['t_min'].values,
43+
df['seasonal_prec'].values, df['dtr'].values)
44+
if 'vapor_pressure' not in df:
45+
df['vapor_pressure'] = vapor_pressure(df['tdew'].values)
46+
return df

metsim/metsim.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
from metsim import io
5454
from metsim.datetime import date_range
5555
from metsim.disaggregate import disaggregate
56-
from metsim.methods import mtclim
56+
from metsim.methods import mtclim, passthrough
5757
from metsim.physics import solar_geom
5858
from metsim.units import converters
5959

@@ -104,6 +104,9 @@
104104
'rel_humid': {'units': '%', 'long_name': 'relative humidity',
105105
'standard_name': 'relative_humidity',
106106
'missing_value': np.nan, 'fill_value': np.nan},
107+
'daylength': {'units': 's', 'long_name': 'daylength',
108+
'standard_name': 'length of day',
109+
'missing_value': np.nan, 'fill_value': np.nan},
107110
'spec_humid': {'units': 'g g-1', 'long_name': 'specific humidity',
108111
'standard_name': 'specific_humidity',
109112
'missing_value': np.nan, 'fill_value': np.nan},
@@ -131,7 +134,7 @@ class MetSim(object):
131134
"""
132135

133136
# Class variables
134-
methods = {'mtclim': mtclim}
137+
methods = {'mtclim': mtclim, 'passthrough': passthrough}
135138
params = {
136139
"period_ending": False,
137140
"is_worker": False,
@@ -474,7 +477,6 @@ def run_slice(self):
474477
locs = {d: i for d, i in zip(self.domain['mask'].dims, index)}
475478
else:
476479
continue
477-
478480
df, state = wrap_run_cell(self.method.run, params,
479481
self.met_data.isel(**locs),
480482
self.state.isel(**locs),
@@ -654,7 +656,7 @@ def _validate_setup(self):
654656

655657
# Check output variables are valid
656658
daily_out_vars = ['t_min', 't_max', 't_day', 'prec', 'vapor_pressure',
657-
'shortwave', 'tskc', 'pet', 'wind']
659+
'shortwave', 'tskc', 'pet', 'wind', 'daylength']
658660
out_var_check = ['temp', 'prec', 'shortwave', 'vapor_pressure',
659661
'air_pressure', 'rel_humid', 'spec_humid',
660662
'longwave', 'tskc', 'wind']
@@ -778,7 +780,13 @@ def wrap_run_cell(func: callable, params: dict,
778780
# If we're outputting daily values, we dont' need to
779781
# change the output dates - see inside of `if` condition
780782
# above for more explanation
781-
new_times = out_times
783+
start = out_times.values[0]
784+
stop = (out_times.values[-1] + pd.Timedelta('1 days') -
785+
pd.Timedelta("{} minutes".format(params['time_step'])))
786+
new_times = date_range(
787+
start, stop, freq='{}T'.format(params['time_step']),
788+
calendar=params['calendar'])
789+
df_base.index = new_times
782790
df_complete = df_base
783791

784792
# Cut the returned data down to the correct time index

metsim/tests/test_metsim.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,68 @@ def test_variable_rename():
277277
assert 'SWRadAtm' in ds.variables
278278

279279

280+
def test_passthrough():
281+
"""Test to make sure passing through previously estimated
282+
variables doesn't alter the values from disaggregation"""
283+
loc = data_locations['binary']
284+
data_files = [os.path.join(loc, f) for f in os.listdir(loc)]
285+
out_dir = '.'
286+
params = {'start': dates['binary'][0],
287+
'stop': dates['binary'][1],
288+
'forcing_fmt': 'binary',
289+
'domain_fmt': 'netcdf',
290+
'state_fmt': 'netcdf',
291+
'domain': './metsim/data/stehekin.nc',
292+
'state': './metsim/data/state_vic.nc',
293+
'forcing': data_files,
294+
'method': 'mtclim',
295+
'scheduler': 'threading',
296+
'time_step': "60",
297+
'out_dir': out_dir,
298+
'out_state': os.path.join(out_dir, 'state.nc'),
299+
'out_vars': {'prec': {'out_name': 'prec'},
300+
'temp': {'out_name': 'temp'},
301+
'longwave': {'out_name': 'longwave'},
302+
'vapor_pressure': {'out_name': 'vapor_pressure'},
303+
'shortwave': {'out_name': 'shortwave'}},
304+
'forcing_vars': in_vars_section['binary'],
305+
'domain_vars': domain_section['binary']}
306+
307+
# Second run will be to use mtclim and hourly disagg
308+
params1 = dict()
309+
params1.update(params)
310+
params1['out_prefix'] = 'mtclim'
311+
ms1 = MetSim(params1)
312+
ms1.run()
313+
with ms1.open_output() as ds:
314+
mtclim_ds = ds.load()
315+
316+
# Third run will be to use passthrough and hourly disagg
317+
# with input data from teh first run
318+
params2 = dict()
319+
params2.update(params)
320+
params2['method'] = 'passthrough'
321+
params2['out_prefix'] = 'passthrough'
322+
params2['forcing_vars'] = OrderedDict(
323+
prec='prec', t_max='t_max', t_min='t_min',
324+
wind='wind', shortwave='shortwave', vapor_pressure='vapor_pressure')
325+
params2['forcing_fmt'] = 'netcdf'
326+
params2['forcing'] = './metsim/data/passthrough.nc'
327+
ms2 = MetSim(params2)
328+
ms2.run()
329+
with ms2.open_output() as ds:
330+
passthrough_ds = ds.load()
331+
332+
tol = 1e-4
333+
assert np.allclose(passthrough_ds['shortwave'].mean(),
334+
mtclim_ds['shortwave'].mean(), atol=tol)
335+
assert np.allclose(passthrough_ds['vapor_pressure'].mean(),
336+
mtclim_ds['vapor_pressure'].mean(), atol=tol)
337+
assert np.allclose(passthrough_ds['longwave'].mean(),
338+
mtclim_ds['longwave'].mean(), atol=tol)
339+
340+
341+
280342
def test_unit_conversion():
281343
"""Tests to make sure that variable renaming works"""
282344
loc = data_locations['binary']

metsim/units.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,8 @@
5252
},
5353
'wind': {
5454
'm s-1': lambda x, ts: x,
55+
},
56+
'daylength': {
57+
's': lambda x, ts: x,
5558
}
5659
}

0 commit comments

Comments
 (0)