pyPRMS 0.9.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyprms-0.9.7/LICENSE.md +21 -0
- pyprms-0.9.7/PKG-INFO +67 -0
- pyprms-0.9.7/README.md +30 -0
- pyprms-0.9.7/pyPRMS/Exceptions_custom.py +31 -0
- pyprms-0.9.7/pyPRMS/__init__.py +52 -0
- pyprms-0.9.7/pyPRMS/cbh/Cbh.py +431 -0
- pyprms-0.9.7/pyPRMS/cbh/CbhAscii.py +458 -0
- pyprms-0.9.7/pyPRMS/cbh/CbhNetcdf.py +199 -0
- pyprms-0.9.7/pyPRMS/cbh/__init__.py +3 -0
- pyprms-0.9.7/pyPRMS/constants.py +131 -0
- pyprms-0.9.7/pyPRMS/control/Control.py +362 -0
- pyprms-0.9.7/pyPRMS/control/ControlFile.py +161 -0
- pyprms-0.9.7/pyPRMS/control/ControlVariable.py +208 -0
- pyprms-0.9.7/pyPRMS/control/__init__.py +3 -0
- pyprms-0.9.7/pyPRMS/dimensions/Dimension.py +154 -0
- pyprms-0.9.7/pyPRMS/dimensions/Dimensions.py +256 -0
- pyprms-0.9.7/pyPRMS/dimensions/__init__.py +2 -0
- pyprms-0.9.7/pyPRMS/input/DataFile.py +354 -0
- pyprms-0.9.7/pyPRMS/input/InputVariable.py +61 -0
- pyprms-0.9.7/pyPRMS/input/__init__.py +0 -0
- pyprms-0.9.7/pyPRMS/metadata/__init__.py +1 -0
- pyprms-0.9.7/pyPRMS/metadata/metadata.py +430 -0
- pyprms-0.9.7/pyPRMS/parameters/ParamDb.py +73 -0
- pyprms-0.9.7/pyPRMS/parameters/Parameter.py +624 -0
- pyprms-0.9.7/pyPRMS/parameters/ParameterFile.py +190 -0
- pyprms-0.9.7/pyPRMS/parameters/ParameterNetCDF.py +74 -0
- pyprms-0.9.7/pyPRMS/parameters/ParameterSet.py +96 -0
- pyprms-0.9.7/pyPRMS/parameters/Parameters.py +1506 -0
- pyprms-0.9.7/pyPRMS/parameters/__init__.py +5 -0
- pyprms-0.9.7/pyPRMS/plot_helpers.py +305 -0
- pyprms-0.9.7/pyPRMS/prms_helpers.py +235 -0
- pyprms-0.9.7/pyPRMS/py.typed +0 -0
- pyprms-0.9.7/pyPRMS/summary/OutputCSV.py +64 -0
- pyprms-0.9.7/pyPRMS/summary/OutputVariable.py +163 -0
- pyprms-0.9.7/pyPRMS/summary/OutputVariables.py +227 -0
- pyprms-0.9.7/pyPRMS/summary/__init__.py +2 -0
- pyprms-0.9.7/pyPRMS/utilities/__init__.py +0 -0
- pyprms-0.9.7/pyPRMS/utilities/convert_cbh.py +107 -0
- pyprms-0.9.7/pyPRMS/utilities/convert_model_output.py +91 -0
- pyprms-0.9.7/pyPRMS/utilities/convert_params.py +60 -0
- pyprms-0.9.7/pyPRMS/version.py +13 -0
- pyprms-0.9.7/pyPRMS/xml/cbh.xml +163 -0
- pyprms-0.9.7/pyPRMS/xml/control.xml +1447 -0
- pyprms-0.9.7/pyPRMS/xml/dimensions.xml +311 -0
- pyprms-0.9.7/pyPRMS/xml/modules.xml +251 -0
- pyprms-0.9.7/pyPRMS/xml/parameters.xml +6932 -0
- pyprms-0.9.7/pyPRMS/xml/time_series_input.xml +198 -0
- pyprms-0.9.7/pyPRMS/xml/variables.xml +8173 -0
- pyprms-0.9.7/pyPRMS.egg-info/PKG-INFO +67 -0
- pyprms-0.9.7/pyPRMS.egg-info/SOURCES.txt +55 -0
- pyprms-0.9.7/pyPRMS.egg-info/dependency_links.txt +1 -0
- pyprms-0.9.7/pyPRMS.egg-info/entry_points.txt +3 -0
- pyprms-0.9.7/pyPRMS.egg-info/not-zip-safe +1 -0
- pyprms-0.9.7/pyPRMS.egg-info/requires.txt +24 -0
- pyprms-0.9.7/pyPRMS.egg-info/top_level.txt +1 -0
- pyprms-0.9.7/pyproject.toml +61 -0
- pyprms-0.9.7/setup.cfg +4 -0
pyprms-0.9.7/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Parker Norton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pyprms-0.9.7/PKG-INFO
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: pyPRMS
|
|
3
|
+
Version: 0.9.7
|
|
4
|
+
Summary: Python library for Precipitation-Runoff Modeling System
|
|
5
|
+
Author-email: Parker Norton <pnorton@usgs.gov>
|
|
6
|
+
Maintainer-email: Parker Norton <pnorton@usgs.gov>
|
|
7
|
+
Project-URL: Repository, https://github.com/EC-USGS/pyPRMS.git
|
|
8
|
+
Keywords: PRMS,NHM,streamflow,hydrology,surface
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: <3.13,>=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE.md
|
|
14
|
+
Requires-Dist: cartopy
|
|
15
|
+
Requires-Dist: dask
|
|
16
|
+
Requires-Dist: fsspec
|
|
17
|
+
Requires-Dist: geopandas
|
|
18
|
+
Requires-Dist: matplotlib
|
|
19
|
+
Requires-Dist: netCDF4
|
|
20
|
+
Requires-Dist: networkx>=2
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: pandas
|
|
23
|
+
Requires-Dist: pyproj
|
|
24
|
+
Requires-Dist: rich
|
|
25
|
+
Requires-Dist: shapely
|
|
26
|
+
Requires-Dist: sphinx
|
|
27
|
+
Requires-Dist: sphinx-autodoc-typehints
|
|
28
|
+
Requires-Dist: sphinx-book-theme
|
|
29
|
+
Requires-Dist: xarray
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: mypy; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy_extensions; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-xdist; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-datafiles; extra == "dev"
|
|
37
|
+
|
|
38
|
+
pyPRMS
|
|
39
|
+
======
|
|
40
|
+
[](https://github.com/EC-USGS/pyPRMS/actions?query=workflows%3CI)
|
|
41
|
+
|
|
42
|
+
A Python library for working with the Precipitation-Runoff Modeling System (PRMS)
|
|
43
|
+
|
|
44
|
+
----
|
|
45
|
+
|
|
46
|
+
The pyPRMS library provides a set of python interfaces to read, modify, and write various files used by and for PRMS.
|
|
47
|
+
|
|
48
|
+
pip install
|
|
49
|
+
-----------
|
|
50
|
+
The library can be installed using pip. The development branch contains the most
|
|
51
|
+
up-to-date version of the software.
|
|
52
|
+
|
|
53
|
+
To install from development branch use the following command::
|
|
54
|
+
|
|
55
|
+
pip install git+https://github.com/EC-USGS/pyPRMS.git
|
|
56
|
+
|
|
57
|
+
To update from the development branch type::
|
|
58
|
+
|
|
59
|
+
pip install git+https://github.com/EC-USGS/pyPRMS.git --upgrade
|
|
60
|
+
|
|
61
|
+
To uninstall pyPRMS type::
|
|
62
|
+
|
|
63
|
+
pip uninstall pyPRMS
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
pyprms-0.9.7/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
pyPRMS
|
|
2
|
+
======
|
|
3
|
+
[](https://github.com/EC-USGS/pyPRMS/actions?query=workflows%3CI)
|
|
4
|
+
|
|
5
|
+
A Python library for working with the Precipitation-Runoff Modeling System (PRMS)
|
|
6
|
+
|
|
7
|
+
----
|
|
8
|
+
|
|
9
|
+
The pyPRMS library provides a set of python interfaces to read, modify, and write various files used by and for PRMS.
|
|
10
|
+
|
|
11
|
+
pip install
|
|
12
|
+
-----------
|
|
13
|
+
The library can be installed using pip. The development branch contains the most
|
|
14
|
+
up-to-date version of the software.
|
|
15
|
+
|
|
16
|
+
To install from development branch use the following command::
|
|
17
|
+
|
|
18
|
+
pip install git+https://github.com/EC-USGS/pyPRMS.git
|
|
19
|
+
|
|
20
|
+
To update from the development branch type::
|
|
21
|
+
|
|
22
|
+
pip install git+https://github.com/EC-USGS/pyPRMS.git --upgrade
|
|
23
|
+
|
|
24
|
+
To uninstall pyPRMS type::
|
|
25
|
+
|
|
26
|
+
pip uninstall pyPRMS
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
class ConcatError(Exception):
|
|
3
|
+
"""Concatenation error"""
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ParameterError(Exception):
|
|
8
|
+
def __init__(self, err_args):
|
|
9
|
+
Exception.__init__(self, err_args)
|
|
10
|
+
self.errArgs = err_args
|
|
11
|
+
|
|
12
|
+
class ParameterExistsError(Exception):
|
|
13
|
+
def __init__(self, err_args):
|
|
14
|
+
Exception.__init__(self, err_args)
|
|
15
|
+
self.errArgs = err_args
|
|
16
|
+
|
|
17
|
+
class ParameterNotValidError(Exception):
|
|
18
|
+
def __init__(self, err_args):
|
|
19
|
+
Exception.__init__(self, err_args)
|
|
20
|
+
self.errArgs = err_args
|
|
21
|
+
|
|
22
|
+
class FixedDimensionError(Exception):
|
|
23
|
+
"""Raised when attempting to modify a fixed dimension"""
|
|
24
|
+
def __init__(self, err_args):
|
|
25
|
+
Exception.__init__(self, err_args)
|
|
26
|
+
self.errArgs = err_args
|
|
27
|
+
|
|
28
|
+
class ControlError(Exception):
|
|
29
|
+
def __init__(self, err_args):
|
|
30
|
+
Exception.__init__(self, err_args)
|
|
31
|
+
self.errArgs = err_args
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
|
|
2
|
+
from . import constants
|
|
3
|
+
from . import Exceptions_custom
|
|
4
|
+
from . import plot_helpers
|
|
5
|
+
from . import prms_helpers
|
|
6
|
+
from .control.Control import Control
|
|
7
|
+
from .control.ControlVariable import ControlVariable
|
|
8
|
+
from .control.ControlFile import ControlFile
|
|
9
|
+
from .dimensions.Dimension import Dimension
|
|
10
|
+
from .dimensions.Dimensions import Dimensions, ParamDimensions
|
|
11
|
+
from .metadata.metadata import MetaData
|
|
12
|
+
from .summary.OutputVariables import OutputVariables
|
|
13
|
+
from .summary.OutputVariable import OutputVariable
|
|
14
|
+
from .parameters.Parameter import Parameter
|
|
15
|
+
from .parameters.Parameters import Parameters
|
|
16
|
+
from .parameters.ParameterFile import ParameterFile
|
|
17
|
+
from .parameters.ParamDb import ParamDb
|
|
18
|
+
from .parameters.ParameterNetCDF import ParameterNetCDF
|
|
19
|
+
from .cbh.Cbh import Cbh
|
|
20
|
+
from .cbh.CbhAscii import CbhAscii
|
|
21
|
+
from .cbh.CbhNetcdf import CbhNetcdf
|
|
22
|
+
from pyPRMS.input.DataFile import DataFile
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
from .version import __author__, __author_email__, __version__
|
|
26
|
+
|
|
27
|
+
__all__ = ['constants',
|
|
28
|
+
'Exceptions_custom',
|
|
29
|
+
'plot_helpers',
|
|
30
|
+
'prms_helpers',
|
|
31
|
+
'control',
|
|
32
|
+
'dimensions',
|
|
33
|
+
'parameters',
|
|
34
|
+
'cbh',
|
|
35
|
+
'Cbh',
|
|
36
|
+
'CbhAscii',
|
|
37
|
+
'CbhNetcdf',
|
|
38
|
+
'Control',
|
|
39
|
+
'ControlFile',
|
|
40
|
+
'ControlVariable',
|
|
41
|
+
'Dimension',
|
|
42
|
+
'Dimensions',
|
|
43
|
+
'MetaData',
|
|
44
|
+
'OutputVariables',
|
|
45
|
+
'OutputVariable',
|
|
46
|
+
'ParamDimensions',
|
|
47
|
+
'Parameter',
|
|
48
|
+
'Parameters',
|
|
49
|
+
'ParameterFile',
|
|
50
|
+
'ParamDb',
|
|
51
|
+
'ParameterNetCDF',
|
|
52
|
+
'DataFile']
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import fsspec # type: ignore
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd # type: ignore
|
|
4
|
+
import netCDF4 as nc # type: ignore
|
|
5
|
+
import xarray as xr # type: ignore
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Optional, Sequence, Union
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich import pretty
|
|
12
|
+
|
|
13
|
+
from ..control.Control import Control
|
|
14
|
+
from ..constants import MetaDataType, NEW_PTYPE_TO_DTYPE
|
|
15
|
+
|
|
16
|
+
pretty.install()
|
|
17
|
+
con = Console(force_jupyter=False)
|
|
18
|
+
|
|
19
|
+
__author__ = 'Parker Norton (pnorton@usgs.gov)'
|
|
20
|
+
|
|
21
|
+
CBH_VARNAMES = ['prcp', 'tmin', 'tmax']
|
|
22
|
+
CBH_INDEX_COLS = [0, 1, 2, 3, 4, 5]
|
|
23
|
+
TS_FORMAT = '%Y %m %d %H %M %S' # 1915 1 13 0 0 0
|
|
24
|
+
NA_VALS_DEFAULT = ('-99.0', '-999.0', 'NaN', 'inf')
|
|
25
|
+
DATA_SEP = '####'
|
|
26
|
+
|
|
27
|
+
var_crosswalk: Dict[str, str] = dict(tmax='tmax_hru',
|
|
28
|
+
T2MAX='tmax_hru',
|
|
29
|
+
tmin='tmin_hru',
|
|
30
|
+
T2MIN='tmin_hru',
|
|
31
|
+
precip='hru_ppt',
|
|
32
|
+
prcp='hru_ppt',
|
|
33
|
+
RAIN='hru_ppt',
|
|
34
|
+
rhavg='humidity_hru')
|
|
35
|
+
temp_units = {0: 'degree_fahrenheit', 1: 'degree_celsius'}
|
|
36
|
+
precip_units = {0: 'inch', 1: 'mm'}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Cbh(object):
|
|
40
|
+
"""Climate-By-HRU (CBH) files for PRMS."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, src_path: Union[str, Path, List[Union[str, Path]]],
|
|
43
|
+
metadata: MetaDataType,
|
|
44
|
+
engine: Optional[str] = 'ascii',
|
|
45
|
+
control: Optional[Control] = None):
|
|
46
|
+
"""
|
|
47
|
+
:param src_path: List of paths to CBH files
|
|
48
|
+
:param metadata: Metadata dictionary for Climate-by-HRU variables
|
|
49
|
+
:param engine: Engine to use for reading CBH files (one of netcdf, zarr, or ascii)
|
|
50
|
+
:param control: Control object for PRMS model containing configuration information
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
self.has_nhm_id = False
|
|
54
|
+
self.metadata = metadata['cbh']
|
|
55
|
+
self.__var_map = {}
|
|
56
|
+
self.__var_src: Dict[str, str] = {}
|
|
57
|
+
|
|
58
|
+
if isinstance(src_path, str):
|
|
59
|
+
src_path = Path(src_path)
|
|
60
|
+
|
|
61
|
+
if isinstance(src_path, list):
|
|
62
|
+
self.__src_path = [Path(ff).resolve() for ff in src_path]
|
|
63
|
+
else:
|
|
64
|
+
if '*' in src_path.name:
|
|
65
|
+
# wildcard character in filename so glob the path
|
|
66
|
+
self.__src_path = list(src_path.parent.glob(src_path.name))
|
|
67
|
+
else:
|
|
68
|
+
self.__src_path = [src_path.resolve()]
|
|
69
|
+
|
|
70
|
+
con.print(f'CBH files: {self.__src_path}')
|
|
71
|
+
|
|
72
|
+
assert self.__src_path is not None
|
|
73
|
+
|
|
74
|
+
match engine:
|
|
75
|
+
case 'netcdf':
|
|
76
|
+
ds = xr.open_mfdataset(self.__src_path, chunks={}, combine='by_coords',
|
|
77
|
+
data_vars='minimal', decode_cf=True, engine='netcdf4',
|
|
78
|
+
parallel=True)
|
|
79
|
+
case 'zarr':
|
|
80
|
+
if len(self.__src_path) > 1:
|
|
81
|
+
con.print('[red]ERROR[/]: Zarr engine does not support reading multiple files')
|
|
82
|
+
elif not self.__src_path[0].is_dir():
|
|
83
|
+
con.print('[red]ERROR[/]: Zarr engine requires a directory of files')
|
|
84
|
+
else:
|
|
85
|
+
ds = xr.open_zarr(self.__src_path[0], consolidated=True)
|
|
86
|
+
case 'ascii':
|
|
87
|
+
if control is None:
|
|
88
|
+
ds = self._cbh_to_xarray(self.__src_path) # type: ignore
|
|
89
|
+
else:
|
|
90
|
+
# When a control object is specified, the src_path indicates
|
|
91
|
+
# the model directory and the *_day variables are read to get
|
|
92
|
+
# candidate CBH files.
|
|
93
|
+
cbh_file_vars = dict(albedo_day='albedo_hru',
|
|
94
|
+
cloud_cover_day='cloud_cover_cbh',
|
|
95
|
+
humidity_day='humidity_hru',
|
|
96
|
+
potet_day='potet',
|
|
97
|
+
precip_day='hru_ppt',
|
|
98
|
+
swrad_day='swrad',
|
|
99
|
+
tmax_day='tmax_hru',
|
|
100
|
+
tmin_day='tmin_hru',
|
|
101
|
+
transp_day='transp_on',
|
|
102
|
+
windspeed_day='windspeed_hru')
|
|
103
|
+
|
|
104
|
+
cbh_files = []
|
|
105
|
+
# cbh_vars = []
|
|
106
|
+
for ctl_var, prms_var in cbh_file_vars.items():
|
|
107
|
+
cfile = control.get(ctl_var).values
|
|
108
|
+
assert type(cfile) is str
|
|
109
|
+
|
|
110
|
+
if (self.__src_path[0] / cfile).exists():
|
|
111
|
+
con.print(f'[green]INFO[/]: Found {cfile}')
|
|
112
|
+
cbh_files.append(self.__src_path[0] / cfile)
|
|
113
|
+
# cbh_vars.append(prms_var)
|
|
114
|
+
|
|
115
|
+
ds = self._cbh_to_xarray(cbh_files) # type: ignore # , variables=cbh_vars)
|
|
116
|
+
|
|
117
|
+
if 'nhm_id' in ds.data_vars:
|
|
118
|
+
# dataset has nhm_id variable so use it as the nhru dimension
|
|
119
|
+
ds = ds.assign_coords(nhru=ds.nhm_id)
|
|
120
|
+
self.has_nhm_id = True
|
|
121
|
+
|
|
122
|
+
for cvar in ds.data_vars:
|
|
123
|
+
self.__var_map[str(cvar)] = var_crosswalk.get(str(cvar), str(cvar))
|
|
124
|
+
|
|
125
|
+
self.__dataset = ds
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def data(self) -> xr.Dataset:
|
|
129
|
+
"""Returns the CBH dataset.
|
|
130
|
+
|
|
131
|
+
:returns: xarray dataset
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
return self.__dataset
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def var_map(self) -> Dict[str, str]:
|
|
138
|
+
"""Return variable-to-prms_variable mapping."""
|
|
139
|
+
|
|
140
|
+
return self.__var_map
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def var_src(self) -> Dict[str, str]:
|
|
144
|
+
"""Return variable to source-file mapping."""
|
|
145
|
+
|
|
146
|
+
return self.__var_src
|
|
147
|
+
|
|
148
|
+
def set_nhm_id(self, nhm_ids: np.ndarray):
|
|
149
|
+
"""Add the model nhm_id parameter as a coordinate variable.
|
|
150
|
+
|
|
151
|
+
:param nhm_ids: array of nhm_id values
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
# Returns ValueError if the nhm_ids size does not match the dataset dimension size
|
|
155
|
+
if not self.has_nhm_id:
|
|
156
|
+
# Only add nhm_id if it doesn't already exist
|
|
157
|
+
self.__dataset['nhm_id'] = ('nhru', nhm_ids)
|
|
158
|
+
self.__dataset['nhm_id'].attrs['long_name'] = 'Global model Hydrologic Response Unit ID (HRU)'
|
|
159
|
+
self.__dataset = self.__dataset.assign_coords(nhru=self.__dataset.nhm_id)
|
|
160
|
+
self.has_nhm_id = True
|
|
161
|
+
|
|
162
|
+
def write_ascii(self, filename: Union[str, Path],
|
|
163
|
+
variable: str,
|
|
164
|
+
time_slice: Optional[Union[list, slice]] = None,
|
|
165
|
+
hru_ids: Optional[Union[list, np.ndarray]] = None):
|
|
166
|
+
"""Write CBH data for selected variable to PRMS ASCII-formatted file.
|
|
167
|
+
|
|
168
|
+
:param filename: Climate-by-HRU filename
|
|
169
|
+
:param variable: CBH variable to write
|
|
170
|
+
:param time_slice: time slice to write
|
|
171
|
+
:param hru_ids: list or array of HRU IDs (local IDs if has_nhm_id is false) to write
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
if hru_ids is None:
|
|
175
|
+
# Return all HRUs if hru_ids is not provided
|
|
176
|
+
hru_ids = self.__dataset['nhru'].values
|
|
177
|
+
|
|
178
|
+
if time_slice is None:
|
|
179
|
+
# Return all time steps if time_slice is not provided
|
|
180
|
+
time_slice = slice(self.__dataset['time'][0].values,
|
|
181
|
+
self.__dataset['time'][-1].values)
|
|
182
|
+
|
|
183
|
+
if isinstance(time_slice, list):
|
|
184
|
+
time_slice = slice(time_slice[0], time_slice[-1])
|
|
185
|
+
|
|
186
|
+
# For out_order the first six columns contain the time information and
|
|
187
|
+
# are always output for the cbh files
|
|
188
|
+
# out_order: List[Union[int, str]] = [kk for kk in self.__nhm_hrus]
|
|
189
|
+
out_order = [kk for kk in hru_ids]
|
|
190
|
+
for cc in ['second', 'minute', 'hour', 'day', 'month', 'year']:
|
|
191
|
+
out_order.insert(0, cc)
|
|
192
|
+
|
|
193
|
+
# variable = var_crosswalk.get(variable, variable)
|
|
194
|
+
|
|
195
|
+
if variable in self.__dataset.data_vars:
|
|
196
|
+
ds = self.__dataset[variable].sel(nhru=hru_ids, time=time_slice).to_pandas()
|
|
197
|
+
|
|
198
|
+
# Add time information as columns
|
|
199
|
+
ds['year'] = ds.index.year
|
|
200
|
+
ds['month'] = ds.index.month
|
|
201
|
+
ds['day'] = ds.index.day
|
|
202
|
+
ds['hour'] = 0
|
|
203
|
+
ds['minute'] = 0
|
|
204
|
+
ds['second'] = 0
|
|
205
|
+
|
|
206
|
+
out_cbh = open(filename, 'w')
|
|
207
|
+
out_cbh.write('Written by Bandit\n')
|
|
208
|
+
out_cbh.write(f'{var_crosswalk.get(variable, variable)} {len(hru_ids)}\n')
|
|
209
|
+
out_cbh.write('########################################\n')
|
|
210
|
+
ds.to_csv(out_cbh, columns=out_order, na_rep='-999', float_format='%0.2f',
|
|
211
|
+
sep=' ', index=False, header=False, lineterminator='\n', encoding=None,
|
|
212
|
+
chunksize=10)
|
|
213
|
+
out_cbh.close()
|
|
214
|
+
else:
|
|
215
|
+
print(f'WARNING: {variable} does not exist in source CBH files..skipping')
|
|
216
|
+
|
|
217
|
+
def write_netcdf(self, filename: Union[str, Path],
|
|
218
|
+
variables: Optional[List[str]] = None,
|
|
219
|
+
global_attrs: Optional[Dict] = None,
|
|
220
|
+
time_slice: Optional[Union[list, slice]] = None,
|
|
221
|
+
hru_ids: Optional[Union[list, np.ndarray]] = None):
|
|
222
|
+
"""Write CBH variables to netCDF file.
|
|
223
|
+
|
|
224
|
+
:param filename: name of netCDF output file
|
|
225
|
+
:param variables: list of CBH variables to write
|
|
226
|
+
:param global_attrs: optional dictionary of global attributes to include in netcdf file
|
|
227
|
+
:param time_slice: time slice to write
|
|
228
|
+
:param hru_ids: list or array of HRU IDs (local IDs if has_nhm_id is false) to write
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
if hru_ids is None:
|
|
232
|
+
# Return all HRUs if hru_ids is not provided
|
|
233
|
+
hru_ids = self.__dataset['nhru'].values
|
|
234
|
+
|
|
235
|
+
if time_slice is None:
|
|
236
|
+
# Return all time steps if time_slice is not provided
|
|
237
|
+
time_slice = slice(self.__dataset['time'][0].values,
|
|
238
|
+
self.__dataset['time'][-1].values)
|
|
239
|
+
|
|
240
|
+
if isinstance(time_slice, list):
|
|
241
|
+
time_slice = slice(time_slice[0], time_slice[-1])
|
|
242
|
+
|
|
243
|
+
ds = self.__dataset.sel(nhru=hru_ids, time=time_slice)
|
|
244
|
+
# ds = ds.sel(time=slice(self.__stdate, self.__endate), nhru=self.__nhm_hrus)
|
|
245
|
+
|
|
246
|
+
if variables is None:
|
|
247
|
+
pass
|
|
248
|
+
elif isinstance(variables, list):
|
|
249
|
+
ds = ds[variables]
|
|
250
|
+
|
|
251
|
+
# Remove _FillValue from coordinate variables
|
|
252
|
+
for vv in list(ds.coords):
|
|
253
|
+
ds[vv].encoding.update({'_FillValue': None})
|
|
254
|
+
|
|
255
|
+
if 'crs' in ds.variables:
|
|
256
|
+
# ds['crs'] = self.__dataset['crs']
|
|
257
|
+
ds['crs'].encoding.update({'_FillValue': None,
|
|
258
|
+
'contiguous': True})
|
|
259
|
+
|
|
260
|
+
ds['time'].attrs['standard_name'] = 'time'
|
|
261
|
+
ds['time'].attrs['long_name'] = 'time'
|
|
262
|
+
|
|
263
|
+
# Add nhm_id variable which will be the global NHM IDs
|
|
264
|
+
ds['nhm_id'] = ds['nhru']
|
|
265
|
+
ds['nhm_id'].attrs['long_name'] = 'Global model Hydrologic Response Unit ID (HRU)'
|
|
266
|
+
|
|
267
|
+
if not self.has_nhm_id:
|
|
268
|
+
ds['nhm_id'].attrs['note'] = 'Locally generated ID; not the same as NHM global ID'
|
|
269
|
+
|
|
270
|
+
# Change the nhru coordinate variable values to reflect the local model HRU IDs
|
|
271
|
+
ds = ds.assign_coords(nhru=np.arange(1, ds.nhru.values.size+1, dtype=ds.nhru.dtype))
|
|
272
|
+
ds['nhru'].attrs['long_name'] = 'Local model Hydrologic Response Unit ID (HRU)'
|
|
273
|
+
ds['nhru'].attrs['cf_role'] = 'timeseries_id'
|
|
274
|
+
|
|
275
|
+
# Add/update global attributes
|
|
276
|
+
ds.attrs['Description'] = 'Climate-by-HRU'
|
|
277
|
+
|
|
278
|
+
if global_attrs is not None:
|
|
279
|
+
for kk, vv in global_attrs.items():
|
|
280
|
+
ds.attrs[kk] = vv
|
|
281
|
+
|
|
282
|
+
encoding = {}
|
|
283
|
+
|
|
284
|
+
for cvar in ds.variables:
|
|
285
|
+
if ds[cvar].ndim > 1:
|
|
286
|
+
encoding[cvar] = dict(_FillValue=ds[cvar].encoding['_FillValue'],
|
|
287
|
+
compression='zlib',
|
|
288
|
+
complevel=2,
|
|
289
|
+
fletcher32=True)
|
|
290
|
+
else:
|
|
291
|
+
encoding[cvar] = dict(_FillValue=None,
|
|
292
|
+
contiguous=True)
|
|
293
|
+
|
|
294
|
+
ds.load().to_netcdf(filename, engine='netcdf4', format='NETCDF4', encoding=encoding)
|
|
295
|
+
|
|
296
|
+
def _cbh_to_xarray(self, filename: Union[str, Path, List[Union[str, Path]]]) -> xr.Dataset:
|
|
297
|
+
# variables: Optional[List[str]] = None) -> xr.Dataset:
|
|
298
|
+
"""Convert ASCII CBH file(s) to xarray
|
|
299
|
+
|
|
300
|
+
:param filename: list of CBH filepaths or a single CBH filename
|
|
301
|
+
:returns: xarray dataset of CBH data
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
var_meta = dict(time=dict(standard_name='time', long_name='time'),
|
|
305
|
+
nhru=dict(standard_name='nhru', long_name='Local model Hydrologic Response Unit ID (HRU)'))
|
|
306
|
+
|
|
307
|
+
var_enc = {'float32': dict(_FillValue=nc.default_fillvals['f4']),
|
|
308
|
+
'float64': dict(_FillValue=nc.default_fillvals['f8']),
|
|
309
|
+
'int32': dict(_FillValue=nc.default_fillvals['i4']),
|
|
310
|
+
'int64': dict(_FillValue=nc.default_fillvals['i8'])}
|
|
311
|
+
|
|
312
|
+
if isinstance(filename, str):
|
|
313
|
+
filename = Path(filename)
|
|
314
|
+
|
|
315
|
+
if not isinstance(filename, list):
|
|
316
|
+
filename = [filename]
|
|
317
|
+
|
|
318
|
+
df = []
|
|
319
|
+
read_ok = True
|
|
320
|
+
|
|
321
|
+
for idx, cfile in enumerate(filename):
|
|
322
|
+
assert isinstance(cfile, Path)
|
|
323
|
+
|
|
324
|
+
# First get the header info which has the variable name and number of HRUs
|
|
325
|
+
with open(cfile, 'r') as fhdl:
|
|
326
|
+
# First line is a descriptive header
|
|
327
|
+
header = fhdl.readline().rstrip()
|
|
328
|
+
|
|
329
|
+
# Next line has the variable name (not checked by PRMS) and number of dimensions
|
|
330
|
+
var_name, ndims = fhdl.readline().rstrip().split()
|
|
331
|
+
ndims = int(ndims) # type: ignore
|
|
332
|
+
|
|
333
|
+
self.__var_src[var_name] = cfile.name
|
|
334
|
+
# if variables is not None:
|
|
335
|
+
# # Override the variable name when a list of variables has been provided
|
|
336
|
+
# var_name = variables[idx]
|
|
337
|
+
# else:
|
|
338
|
+
# var_name = var_crosswalk.get(var_name, var_name)
|
|
339
|
+
|
|
340
|
+
line = fhdl.readline().rstrip()
|
|
341
|
+
|
|
342
|
+
if line[0:len(DATA_SEP)] != DATA_SEP:
|
|
343
|
+
if line.split()[0] == 'orad':
|
|
344
|
+
# This happens when orad_flag == 1
|
|
345
|
+
con.print(f'[red]ERROR[/]: Two variables in CBH file ({var_name}, orad). Data will not be read.')
|
|
346
|
+
else:
|
|
347
|
+
con.print(f'[red]ERROR[/]: Unknown extra line: {line}.\n Data will not be read')
|
|
348
|
+
|
|
349
|
+
read_ok = False
|
|
350
|
+
|
|
351
|
+
if read_ok:
|
|
352
|
+
df.append(pd.DataFrame(self._read_ascii_file(cfile).stack()))
|
|
353
|
+
df[-1].index.rename(['time', 'nhru'], inplace=True)
|
|
354
|
+
df[-1].rename(columns={0: var_name}, inplace=True)
|
|
355
|
+
|
|
356
|
+
ds = xr.merge([cdf.to_xarray() for cdf in df])
|
|
357
|
+
|
|
358
|
+
# Apply metadata to variables
|
|
359
|
+
for cvar in ds.variables:
|
|
360
|
+
cvar_x = var_crosswalk.get(str(cvar), str(cvar))
|
|
361
|
+
|
|
362
|
+
if cvar_x in self.metadata:
|
|
363
|
+
# if cvar in self.metadata or cvar in self.__var_map:
|
|
364
|
+
# cattrs = self.metadata[cvar]
|
|
365
|
+
cattrs = self.metadata[cvar_x]
|
|
366
|
+
# con.print(f' cattrs: {cattrs}')
|
|
367
|
+
|
|
368
|
+
ds[cvar] = ds[cvar].astype(NEW_PTYPE_TO_DTYPE[cattrs['datatype']])
|
|
369
|
+
|
|
370
|
+
ds[cvar].attrs['long_name'] = cattrs['description']
|
|
371
|
+
|
|
372
|
+
if cattrs['units'] == 'temp_units':
|
|
373
|
+
# For now just default to degrees_fahrenheit
|
|
374
|
+
ds[cvar].attrs['units'] = temp_units[0]
|
|
375
|
+
elif cattrs['units'] == 'precip_units':
|
|
376
|
+
# For now just default to inches
|
|
377
|
+
ds[cvar].attrs['units'] = precip_units[0]
|
|
378
|
+
else:
|
|
379
|
+
ds[cvar].attrs['units'] = cattrs['units']
|
|
380
|
+
|
|
381
|
+
# Set the fill value
|
|
382
|
+
ds[cvar].encoding.update(var_enc[cattrs['datatype']])
|
|
383
|
+
|
|
384
|
+
if cvar in var_meta:
|
|
385
|
+
for cattr, cval in var_meta[cvar].items(): # type: ignore
|
|
386
|
+
ds[cvar].attrs[cattr] = cval
|
|
387
|
+
|
|
388
|
+
# if cvar in var_enc:
|
|
389
|
+
# ds[cvar].encoding.update(var_enc[cvar]) # type: ignore
|
|
390
|
+
|
|
391
|
+
return ds
|
|
392
|
+
|
|
393
|
+
@staticmethod
|
|
394
|
+
def _read_ascii_file(filename: Union[str, Path],
|
|
395
|
+
columns: Optional[List] = None) -> pd.DataFrame:
|
|
396
|
+
"""Reads a single ASCII CBH file.
|
|
397
|
+
|
|
398
|
+
:param filename: name of the CBH file
|
|
399
|
+
:param columns: columns to read
|
|
400
|
+
:returns: dataframe of CBH variable
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
if isinstance(filename, str):
|
|
404
|
+
filename = Path(filename)
|
|
405
|
+
|
|
406
|
+
# Written by Bandit
|
|
407
|
+
# prcp 14
|
|
408
|
+
# ########################################
|
|
409
|
+
# 1980 1 1 0
|
|
410
|
+
|
|
411
|
+
# Columns 0-5 always represent date/time information
|
|
412
|
+
time_col_names = {0: 'year', 1: 'month', 2: 'day', 3: 'hour', 4: 'minute', 5: 'second'}
|
|
413
|
+
|
|
414
|
+
df = pd.read_csv(filename, sep=' ', skipinitialspace=True,
|
|
415
|
+
skiprows=3, engine='python',
|
|
416
|
+
# skiprows=3, engine='c', memory_map=True,
|
|
417
|
+
header=None, na_values=NA_VALS_DEFAULT,
|
|
418
|
+
usecols=columns)
|
|
419
|
+
|
|
420
|
+
# Rename columns with time information
|
|
421
|
+
df.rename(columns=time_col_names, inplace=True)
|
|
422
|
+
|
|
423
|
+
# Rename columns with local model indices
|
|
424
|
+
ren_dict = {k + 6: k + 1 for k in range(len(df.columns))}
|
|
425
|
+
df.rename(columns=ren_dict, inplace=True)
|
|
426
|
+
|
|
427
|
+
df['time'] = pd.to_datetime(df[time_col_names.values()], yearfirst=True)
|
|
428
|
+
df.drop(columns=time_col_names.values(), inplace=True)
|
|
429
|
+
df.set_index('time', inplace=True)
|
|
430
|
+
|
|
431
|
+
return df
|