pyPRMS 0.9.7__py3-none-any.whl
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/Exceptions_custom.py +31 -0
- pyPRMS/__init__.py +52 -0
- pyPRMS/cbh/Cbh.py +431 -0
- pyPRMS/cbh/CbhAscii.py +458 -0
- pyPRMS/cbh/CbhNetcdf.py +199 -0
- pyPRMS/cbh/__init__.py +3 -0
- pyPRMS/constants.py +131 -0
- pyPRMS/control/Control.py +362 -0
- pyPRMS/control/ControlFile.py +161 -0
- pyPRMS/control/ControlVariable.py +208 -0
- pyPRMS/control/__init__.py +3 -0
- pyPRMS/dimensions/Dimension.py +154 -0
- pyPRMS/dimensions/Dimensions.py +256 -0
- pyPRMS/dimensions/__init__.py +2 -0
- pyPRMS/input/DataFile.py +354 -0
- pyPRMS/input/InputVariable.py +61 -0
- pyPRMS/input/__init__.py +0 -0
- pyPRMS/metadata/__init__.py +1 -0
- pyPRMS/metadata/metadata.py +430 -0
- pyPRMS/parameters/ParamDb.py +73 -0
- pyPRMS/parameters/Parameter.py +624 -0
- pyPRMS/parameters/ParameterFile.py +190 -0
- pyPRMS/parameters/ParameterNetCDF.py +74 -0
- pyPRMS/parameters/ParameterSet.py +96 -0
- pyPRMS/parameters/Parameters.py +1506 -0
- pyPRMS/parameters/__init__.py +5 -0
- pyPRMS/plot_helpers.py +305 -0
- pyPRMS/prms_helpers.py +235 -0
- pyPRMS/py.typed +0 -0
- pyPRMS/summary/OutputCSV.py +64 -0
- pyPRMS/summary/OutputVariable.py +163 -0
- pyPRMS/summary/OutputVariables.py +227 -0
- pyPRMS/summary/__init__.py +2 -0
- pyPRMS/utilities/__init__.py +0 -0
- pyPRMS/utilities/convert_cbh.py +107 -0
- pyPRMS/utilities/convert_model_output.py +91 -0
- pyPRMS/utilities/convert_params.py +60 -0
- pyPRMS/version.py +13 -0
- pyPRMS/xml/cbh.xml +163 -0
- pyPRMS/xml/control.xml +1447 -0
- pyPRMS/xml/dimensions.xml +311 -0
- pyPRMS/xml/modules.xml +251 -0
- pyPRMS/xml/parameters.xml +6932 -0
- pyPRMS/xml/time_series_input.xml +198 -0
- pyPRMS/xml/variables.xml +8173 -0
- pyprms-0.9.7.dist-info/LICENSE.md +21 -0
- pyprms-0.9.7.dist-info/METADATA +67 -0
- pyprms-0.9.7.dist-info/RECORD +51 -0
- pyprms-0.9.7.dist-info/WHEEL +5 -0
- pyprms-0.9.7.dist-info/entry_points.txt +3 -0
- pyprms-0.9.7.dist-info/top_level.txt +1 -0
pyPRMS/constants.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from packaging.version import Version
|
|
4
|
+
from typing import Dict, List, NamedTuple, Union
|
|
5
|
+
|
|
6
|
+
# Define aliases for static typing
|
|
7
|
+
MetaDataType = Dict[str, Dict]
|
|
8
|
+
|
|
9
|
+
# Version = NamedTuple('Version', [('major', Union[int, None]),
|
|
10
|
+
# ('minor', Union[int, None]),
|
|
11
|
+
# ('revision', Union[int, None])])
|
|
12
|
+
|
|
13
|
+
# Default PRMS version to use for metadata
|
|
14
|
+
PRMS_VERSION = Version('5.2.1.1')
|
|
15
|
+
|
|
16
|
+
# PRMS6 experimental development version
|
|
17
|
+
PRMS6_DEV_VERSION = Version('60.0')
|
|
18
|
+
|
|
19
|
+
# Order to write control file parameters for printing and writing a new control file
|
|
20
|
+
ctl_order: List[str] = ['start_time', 'end_time', 'initial_deltat', 'executable_desc', 'executable_model', 'model_mode',
|
|
21
|
+
'param_file', 'data_file', 'model_output_file', 'print_debug',
|
|
22
|
+
'et_module', 'precip_module', 'soilzone_module', 'solrad_module', 'srunoff_module',
|
|
23
|
+
'strmflow_module', 'temp_module', 'transp_module',
|
|
24
|
+
'init_vars_from_file', 'save_vars_to_file', 'var_init_file', 'var_save_file',
|
|
25
|
+
'albedo_day', 'cloud_cover_day', 'humidity_day', 'potet_day', 'precip_day', 'precip_grid',
|
|
26
|
+
'swrad_day', 'tmax_day', 'tmax_grid', 'tmin_day', 'transp_day', 'windspeed_day',
|
|
27
|
+
'tmin_grid',
|
|
28
|
+
'albedo_cbh_flag', 'cascade_flag', 'cascadegw_flag', 'cbh_binary_flag', 'cbh_check_flag',
|
|
29
|
+
'cloud_cover_cbh_flag', 'dprst_flag', 'frozen_flag', 'glacier_flag',
|
|
30
|
+
'gwflow_cbh_flag', 'gwr_swale_flag', 'humidity_cbh_flag', 'mbInit_flag', 'orad_flag',
|
|
31
|
+
'parameter_check_flag', 'snarea_curve_flag', 'snow_cbh_flag', 'snow_cloudcover_flag',
|
|
32
|
+
'soilzone_aet_flag',
|
|
33
|
+
'stream_temp_flag', 'stream_temp_shade_flag', 'strmtemp_humidity_flag',
|
|
34
|
+
'subbasin_flag', 'windspeed_cbh_flag',
|
|
35
|
+
'prms_warmup',
|
|
36
|
+
'csvON_OFF', 'csv_output_file',
|
|
37
|
+
'nhruOutON_OFF', 'nhruOutBaseFileName', 'nhruOutVars', 'nhruOutVar_names', 'nhruOut_freq',
|
|
38
|
+
'nhruOut_format', 'nhruOutNcol', 'outputSelectDatesON_OFF', 'selectDatesFileName',
|
|
39
|
+
'nsegmentOutON_OFF', 'nsegmentOutBaseFileName', 'nsegmentOutVars', 'nsegmentOutVar_names',
|
|
40
|
+
'nsegmentOut_freq', 'nsegmentOut_format',
|
|
41
|
+
'basinOutON_OFF', 'basinOutBaseFileName', 'basinOutVars', 'basinOutVar_names', 'basinOut_freq',
|
|
42
|
+
'nsubOutON_OFF', 'nsubOutBaseFileName', 'nsubOutVars', 'nsubOutVar_names', 'nsubOut_format',
|
|
43
|
+
'nsubOut_freq',
|
|
44
|
+
'statsON_OFF', 'stat_var_file', 'nstatVars', 'statVar_element', 'statVar_names',
|
|
45
|
+
'dyn_covden_flag', 'dyn_covtype_flag', 'dyn_dprst_flag', 'dyn_fallfrost_flag',
|
|
46
|
+
'dyn_imperv_flag',
|
|
47
|
+
'dyn_intcp_flag', 'dyn_potet_flag', 'dyn_radtrncf_flag', 'dyn_snareathresh_flag',
|
|
48
|
+
'dyn_soil_flag',
|
|
49
|
+
'dyn_springfrost_flag', 'dyn_sro2dprst_imperv_flag', 'dyn_sro2dprst_perv_flag',
|
|
50
|
+
'dyn_transp_flag', 'dyn_transp_on_flag',
|
|
51
|
+
'dynamic_param_log_file',
|
|
52
|
+
'covden_sum_dynamic', 'covden_win_dynamic', 'covtype_dynamic', 'dprst_depth_dynamic',
|
|
53
|
+
'dprst_frac_dynamic', 'fallfrost_dynamic', 'imperv_frac_dynamic', 'imperv_stor_dynamic',
|
|
54
|
+
'potetcoef_dynamic', 'radtrncf_dynamic', 'snareathresh_dynamic', 'snow_intcp_dynamic',
|
|
55
|
+
'soilmoist_dynamic', 'soilrechr_dynamic', 'springfrost_dynamic', 'srain_intcp_dynamic',
|
|
56
|
+
'sro2dprst_imperv_dynamic', 'sro2dprst_perv_dynamic',
|
|
57
|
+
'transp_on_dynamic', 'transpbeg_dynamic', 'transpend_dynamic', 'wrain_intcp_dynamic',
|
|
58
|
+
'dprst_transferON_OFF', 'dprst_transfer_file', 'external_transferON_OFF',
|
|
59
|
+
'external_transfer_file', 'gwr_transferON_OFF', 'gwr_transfer_file', 'lake_transferON_OFF',
|
|
60
|
+
'lake_transfer_file', 'segment_transferON_OFF', 'segment_transfer_file', 'segmentOutON_OFF',
|
|
61
|
+
'aniOutON_OFF', 'ani_output_file', 'naniOutVars', 'aniOutVar_names',
|
|
62
|
+
'dispGraphsBuffSize', 'ndispGraphs', 'dispVar_element', 'dispVar_names', 'dispVar_plot',
|
|
63
|
+
'mapOutON_OFF', 'nmapOutVars', 'mapOutVar_names']
|
|
64
|
+
|
|
65
|
+
# ctl_variable_modules: List[str] = ['et_module', 'precip_module', 'soilzone_module', 'solrad_module',
|
|
66
|
+
# 'srunoff_module', 'strmflow_module', 'temp_module', 'transp_module']
|
|
67
|
+
|
|
68
|
+
# ctl_summary_modules: List[str] = ['basin_sum', 'basin_summary', 'map_results',
|
|
69
|
+
# 'nhru_summary', 'nsegment_summary', 'nsub_summary', 'subbasin']
|
|
70
|
+
|
|
71
|
+
ctl_implicit_modules: Dict[str, str] = {'basin_module': 'basin',
|
|
72
|
+
'intcp_module': 'intcp',
|
|
73
|
+
'obs_module': 'obs',
|
|
74
|
+
'snow_module': 'snowcomp',
|
|
75
|
+
'gw_module': 'gwflow',
|
|
76
|
+
'soilzone_module': 'soilzone'}
|
|
77
|
+
|
|
78
|
+
# This is a map between what is in the control file and what is in the parameters.xml file
|
|
79
|
+
internal_module_map = dict(et_module={'climate_hru': 'potet_hru'},
|
|
80
|
+
precip_module={'climate_hru': 'precipitation_hru'},
|
|
81
|
+
solrad_module={'climate_hru': 'solar_radiation_hru'},
|
|
82
|
+
temp_module={'climate_hru': 'temperature_hru'},
|
|
83
|
+
transp_module={'climate_hru': 'transpiration_hru'})
|
|
84
|
+
|
|
85
|
+
# This maps the internal parameters.xml names to the external control file module names
|
|
86
|
+
external_module_map = dict(potet_hru='climate_hru',
|
|
87
|
+
precipitation_hru='climate_hru',
|
|
88
|
+
solar_radiation_hru='climate_hru',
|
|
89
|
+
temperature_hru='climate_hru',
|
|
90
|
+
transpiration_hru='climate_hru')
|
|
91
|
+
|
|
92
|
+
# Constants related to parameter files
|
|
93
|
+
DIMENSIONS_HDR: str = 'Dimensions'
|
|
94
|
+
PARAMETERS_HDR: str = 'Parameters'
|
|
95
|
+
CATEGORY_DELIM: str = '**' # Delimiter for categories of variables
|
|
96
|
+
VAR_DELIM: str = '####' # Used to delimit dimensions and parameters
|
|
97
|
+
# DATA_TYPES: Dict[int, str] = {1: 'integer', 2: 'float', 3: 'double', 4: 'string'}
|
|
98
|
+
|
|
99
|
+
# Valid dimensions names for PRMS
|
|
100
|
+
# DIMENSION_NAMES: List[str] = ['mxnsos', 'ncascade', 'ncascdgw', 'nconsumed', 'ndays', 'ndepl',
|
|
101
|
+
# 'ndeplval', 'ngate', 'ngate2', 'ngate3', 'ngate4',
|
|
102
|
+
# 'nevap', 'nexternal', 'ngw', 'ngwcell', 'nhru', 'nhrucell', 'nhumid',
|
|
103
|
+
# 'nlake', 'nlakeelev', 'nlapse', 'nmonths', 'nobs', 'npoigages', 'nrain',
|
|
104
|
+
# 'nratetbl', 'nsegment', 'nsnow', 'nsol', 'nssr', 'nsub', 'ntemp',
|
|
105
|
+
# 'numlakes', 'nwateruse', 'nwind', 'one',
|
|
106
|
+
# 'nstage', 'nstage2', 'nstage3', 'nstage4']
|
|
107
|
+
|
|
108
|
+
# These dimensions are related and should have same size
|
|
109
|
+
# NOTE: 2025-02-05 PAN - this is currently needed by Bandit
|
|
110
|
+
HRU_DIMS: List[str] = ['nhru', 'ngw', 'nssr']
|
|
111
|
+
|
|
112
|
+
# Constants for NhmParamDb
|
|
113
|
+
REGIONS: List[str] = ['r01', 'r02', 'r03', 'r04', 'r05', 'r06', 'r07', 'r08', 'r09',
|
|
114
|
+
'r10L', 'r10U', 'r11', 'r12', 'r13', 'r14', 'r15', 'r16', 'r17', 'r18']
|
|
115
|
+
PARAMETERS_XML: str = 'parameters.xml'
|
|
116
|
+
DIMENSIONS_XML: str = 'dimensions.xml'
|
|
117
|
+
|
|
118
|
+
# 2025-01-24 PAN - used by Parameters.write_parameter_netcdf()
|
|
119
|
+
NETCDF_DATATYPES: Dict[int, str] = {1: 'i4', 2: 'f4', 3: 'f8', 4: 'S1'}
|
|
120
|
+
|
|
121
|
+
# 2025-01-24 PAN - used by Parameters.xml_global_parameters()
|
|
122
|
+
NHM_DATATYPES: Dict[str, int] = {'I': 1, 'F': 2, 'D': 3, 'S': 4}
|
|
123
|
+
|
|
124
|
+
# 2025-01-24 PAN - used by ControlFile._read() and ParameterFile._read()
|
|
125
|
+
PTYPE_TO_DTYPE = {1: np.int32, 2: np.float32, 3: np.float64, 4: np.str_}
|
|
126
|
+
|
|
127
|
+
NEW_PTYPE_TO_DTYPE = {'int32': np.int32, 'float32': np.float32, 'float64': np.float64, 'string': np.str_, 'datetime': np.datetime64}
|
|
128
|
+
PTYPE_TO_PRMS_TYPE: Dict[str, int] = {'int32': 1, 'float32': 2, 'float64': 3, 'string': 4, 'datetime': 1}
|
|
129
|
+
|
|
130
|
+
# PARNAME_DATATYPES = {'long': 1, 'float': 2, 'double': 3, 'string': 4}
|
|
131
|
+
# DATATYPE_TO_DTYPE = {1: int, 2: np.float32, 3: np.float64, 4: np.str_}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import numpy as np
|
|
5
|
+
import operator
|
|
6
|
+
import pandas as pd # type: ignore
|
|
7
|
+
import pkgutil
|
|
8
|
+
import re
|
|
9
|
+
import xml.etree.ElementTree as xmlET
|
|
10
|
+
|
|
11
|
+
from typing import Dict, List, Optional, Sequence, Union # OrderedDict as OrderedDictType,
|
|
12
|
+
|
|
13
|
+
from networkx.utils.misc import check_create_using
|
|
14
|
+
|
|
15
|
+
from .ControlVariable import ControlVariable
|
|
16
|
+
from ..Exceptions_custom import ControlError
|
|
17
|
+
from ..constants import (ctl_order, ctl_implicit_modules, internal_module_map,
|
|
18
|
+
MetaDataType, VAR_DELIM, PTYPE_TO_PRMS_TYPE)
|
|
19
|
+
|
|
20
|
+
cond_check = {'=': operator.eq,
|
|
21
|
+
'>': operator.gt,
|
|
22
|
+
'<': operator.lt}
|
|
23
|
+
|
|
24
|
+
class Control(object):
|
|
25
|
+
"""
|
|
26
|
+
Class object for a collection of control variables.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# Author: Parker Norton (pnorton@usgs.gov)
|
|
30
|
+
# Create date: 2019-04-18
|
|
31
|
+
|
|
32
|
+
def __init__(self, metadata: MetaDataType, verbose: Optional[bool] = False):
|
|
33
|
+
"""Create Control object.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# Container to hold dicionary of ControlVariables
|
|
37
|
+
# self.__control_vars = OrderedDict()
|
|
38
|
+
self.__control_vars: Dict = {}
|
|
39
|
+
self.__header: Optional[List[str]] = None
|
|
40
|
+
self.__verbose = verbose
|
|
41
|
+
|
|
42
|
+
# Create an entry for each variable in the control section of
|
|
43
|
+
# the metadata dictionary
|
|
44
|
+
# for cvar, cvals in metadata['control'].items():
|
|
45
|
+
# self.add(name=cvar, meta=cvals)
|
|
46
|
+
for cvar in metadata['control'].keys():
|
|
47
|
+
self.add(name=cvar, meta=metadata['control'])
|
|
48
|
+
|
|
49
|
+
if verbose:
|
|
50
|
+
print('Pre-populate control variables done')
|
|
51
|
+
|
|
52
|
+
def __getitem__(self, item: str) -> ControlVariable:
|
|
53
|
+
"""Get ControlVariable object for a variable.
|
|
54
|
+
|
|
55
|
+
:param item: name of control file variable
|
|
56
|
+
:returns: ControlVariable object
|
|
57
|
+
"""
|
|
58
|
+
return self.get(item)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def additional_modules(self) -> List[str]:
|
|
62
|
+
"""Get list of summary modules in PRMS
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
# TODO: module_requirements should be added to metadata?
|
|
66
|
+
# NOTE: 20231109 PAN - we always want basin_sum included since the
|
|
67
|
+
# print_debug could be set to 4 (which uses the basin_sum module)
|
|
68
|
+
module_requirements = {'basin_sum': 'print_debug < 100',
|
|
69
|
+
# 'basin_sum': 'print_debug = 4',
|
|
70
|
+
'basin_summary': 'basinOutON_OFF > 0',
|
|
71
|
+
'map_results': 'mapOutON_OFF > 0',
|
|
72
|
+
'nhru_summary': 'nhruOutON_OFF > 0',
|
|
73
|
+
'nsegment_summary': 'nsegmentOutON_OFF > 0',
|
|
74
|
+
'nsub_summary': 'nsubOutON_OFF > 0',
|
|
75
|
+
'stream_temp': 'stream_temp_flag > 0',
|
|
76
|
+
'subbasin': 'subbasin_flag = 1'}
|
|
77
|
+
|
|
78
|
+
active_modules = []
|
|
79
|
+
|
|
80
|
+
for cmod, cond in module_requirements.items():
|
|
81
|
+
if self._check_condition(cond):
|
|
82
|
+
active_modules.append(cmod)
|
|
83
|
+
|
|
84
|
+
return active_modules
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def cbh_files(self) -> List[str]:
|
|
88
|
+
"""Get list of possible CBH filenames.
|
|
89
|
+
|
|
90
|
+
:returns: list of CBH files
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
# List of control variables that specify possible CBH files
|
|
94
|
+
ctl_cbh_files = ['albebo_day', 'cloud_cover_day', 'humidity_day', 'potet_day', 'precip_day',
|
|
95
|
+
'swrad_day', 'tmax_day', 'tmin_day', 'transp_day', 'windspeed_day']
|
|
96
|
+
cbh_files = []
|
|
97
|
+
|
|
98
|
+
for cvar in ctl_cbh_files:
|
|
99
|
+
if self.exists(cvar):
|
|
100
|
+
cbh_files.append(self.get(cvar).values)
|
|
101
|
+
|
|
102
|
+
return sorted(list(set(cbh_files)))
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def control_variables(self) -> Dict[str, ControlVariable]:
|
|
106
|
+
# def control_variables(self) -> OrderedDictType[str, ControlVariable]:
|
|
107
|
+
"""Get control variable objects.
|
|
108
|
+
|
|
109
|
+
:returns: control variable objects
|
|
110
|
+
"""
|
|
111
|
+
return self.__control_vars
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def dynamic_parameters(self) -> List[str]:
|
|
115
|
+
"""Get list of parameter names for which a dynamic flag set.
|
|
116
|
+
|
|
117
|
+
:returns: list of parameter names
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
dyn_params: List[str] = []
|
|
121
|
+
|
|
122
|
+
for dv in self.__control_vars.keys():
|
|
123
|
+
cvar = self.get(dv)
|
|
124
|
+
|
|
125
|
+
if (cvar.meta.get('valid_value_type', '') == 'parameter' and
|
|
126
|
+
(isinstance(cvar.values, int | np.int32 | np.int64))):
|
|
127
|
+
# Dynamic parameter flags should always be integers
|
|
128
|
+
if cvar.values > 0:
|
|
129
|
+
dyn_params.extend(cvar.dyn_param_meaning)
|
|
130
|
+
# dyn_params.append(self.get(dv).associated_values)
|
|
131
|
+
return dyn_params
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def has_dynamic_parameters(self) -> bool:
|
|
135
|
+
"""Indicates if any dynamic parameters have been requested.
|
|
136
|
+
|
|
137
|
+
:returns: True if dynamic parameters are required
|
|
138
|
+
"""
|
|
139
|
+
return len(self.dynamic_parameters) > 0
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def header(self) -> Optional[Sequence[str]]:
|
|
143
|
+
"""Get header information defined for a control object.
|
|
144
|
+
|
|
145
|
+
This is typically taken from the first two lines of a control file.
|
|
146
|
+
|
|
147
|
+
:returns: Header information from control file
|
|
148
|
+
"""
|
|
149
|
+
return self.__header
|
|
150
|
+
|
|
151
|
+
@header.setter
|
|
152
|
+
def header(self, info: Union[Sequence[str], str, None]):
|
|
153
|
+
"""Set the header information.
|
|
154
|
+
|
|
155
|
+
:param info: list or string of header line(s)
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
if info is None:
|
|
159
|
+
self.__header = None
|
|
160
|
+
elif isinstance(info, list):
|
|
161
|
+
self.__header = info
|
|
162
|
+
elif isinstance(info, str):
|
|
163
|
+
self.__header = [info]
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def modules(self) -> Dict[str, str]:
|
|
167
|
+
"""Get the modules defined in the control file.
|
|
168
|
+
|
|
169
|
+
Note: climate_hru is changed to precipitation_hru, temperature_hru,
|
|
170
|
+
potet_hru, or solar_radiation_hru depending on the module type. This
|
|
171
|
+
makes for easier identification of which process(es) is/are actually using
|
|
172
|
+
climate_hru. Interally the module type is still climate_hru.
|
|
173
|
+
|
|
174
|
+
:returns: dictionary of control variable, module name pairs
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
mod_dict = {}
|
|
178
|
+
|
|
179
|
+
for vv in self.control_variables.values():
|
|
180
|
+
if vv.meta.get('valid_value_type', '') == 'module':
|
|
181
|
+
mname = internal_module_map.get(vv.name, {}).get(vv.values, vv.values)
|
|
182
|
+
|
|
183
|
+
mod_dict[vv.name] = str(mname)
|
|
184
|
+
|
|
185
|
+
# Add the modules that are implicitly included
|
|
186
|
+
for mtype, mname in ctl_implicit_modules.items():
|
|
187
|
+
if mtype not in mod_dict:
|
|
188
|
+
mod_dict[mtype] = mname
|
|
189
|
+
|
|
190
|
+
return mod_dict
|
|
191
|
+
|
|
192
|
+
def add(self, name: str, meta=None):
|
|
193
|
+
"""Add a control variable by name.
|
|
194
|
+
|
|
195
|
+
:param name: Name of the control variable
|
|
196
|
+
:param datatype: The datatype of the control variable
|
|
197
|
+
|
|
198
|
+
:raises ControlError: if control variable already exists
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
if self.exists(name):
|
|
202
|
+
raise ControlError("Control variable already exists")
|
|
203
|
+
self.__control_vars[name] = ControlVariable(name=name, meta=meta)
|
|
204
|
+
# self.__control_vars[name] = ControlVariable(name=name, datatype=datatype, meta=meta)
|
|
205
|
+
|
|
206
|
+
def exists(self, name: str) -> bool:
|
|
207
|
+
"""Checks if control variable exists.
|
|
208
|
+
|
|
209
|
+
:param name: Name of the control variable
|
|
210
|
+
:returns: True if control variable exists otherwise False
|
|
211
|
+
"""
|
|
212
|
+
return name in self.__control_vars.keys()
|
|
213
|
+
|
|
214
|
+
def get(self, name: str) -> ControlVariable:
|
|
215
|
+
"""Returns the given control variable object.
|
|
216
|
+
|
|
217
|
+
:param name: Name of the control variable
|
|
218
|
+
:returns: Control variable object
|
|
219
|
+
|
|
220
|
+
:raises ValueError: if control variable does not exist
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
if self.exists(name):
|
|
224
|
+
return self.__control_vars[name]
|
|
225
|
+
raise ValueError(f'Control variable, {name}, does not exist.')
|
|
226
|
+
|
|
227
|
+
def remove(self, name: str):
|
|
228
|
+
"""Delete a control variable if it exists.
|
|
229
|
+
|
|
230
|
+
:param name: Name of the control variable
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
if self.exists(name):
|
|
234
|
+
del self.__control_vars[name]
|
|
235
|
+
|
|
236
|
+
def to_dict(self):
|
|
237
|
+
"""Dictionary of data for each control variable"""
|
|
238
|
+
|
|
239
|
+
return {kk: vv.values for (kk, vv) in self.__control_vars.items()}
|
|
240
|
+
|
|
241
|
+
def write(self, filename: str):
|
|
242
|
+
"""Write a control file.
|
|
243
|
+
|
|
244
|
+
:param filename: Name of control file to create
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
outfile = open(filename, 'w')
|
|
248
|
+
|
|
249
|
+
if self.__header is not None:
|
|
250
|
+
for hh in self.__header:
|
|
251
|
+
outfile.write(f'{hh}\n')
|
|
252
|
+
|
|
253
|
+
order = ['datatype', 'values']
|
|
254
|
+
|
|
255
|
+
# Get set of variables in ctl_order that are missing from control_vars
|
|
256
|
+
setdiff = set(self.__control_vars.keys()).difference(set(ctl_order))
|
|
257
|
+
|
|
258
|
+
# Add missing control variables (setdiff) in ctl_order to the end of the list
|
|
259
|
+
ctl_order.extend(list(setdiff))
|
|
260
|
+
|
|
261
|
+
for kk in ctl_order:
|
|
262
|
+
if self.exists(kk):
|
|
263
|
+
cvar = self.get(kk)
|
|
264
|
+
|
|
265
|
+
outfile.write(f'{VAR_DELIM}\n')
|
|
266
|
+
outfile.write(f'{kk}\n')
|
|
267
|
+
|
|
268
|
+
for item in order:
|
|
269
|
+
if cvar.meta['datatype'] == 'datetime':
|
|
270
|
+
date_tmp = [int(xx) for xx in re.split(r'[-T:.]+', str(cvar.values))[0:6]]
|
|
271
|
+
|
|
272
|
+
if item == 'datatype':
|
|
273
|
+
outfile.write(f'{len(date_tmp)}\n')
|
|
274
|
+
outfile.write(f'{PTYPE_TO_PRMS_TYPE[cvar.meta["datatype"]]}\n')
|
|
275
|
+
if item == 'values':
|
|
276
|
+
for cval in date_tmp:
|
|
277
|
+
outfile.write(f'{cval}\n')
|
|
278
|
+
else:
|
|
279
|
+
if item == 'datatype':
|
|
280
|
+
outfile.write(f'{cvar.size}\n')
|
|
281
|
+
outfile.write(f'{PTYPE_TO_PRMS_TYPE[cvar.meta["datatype"]]}\n')
|
|
282
|
+
if item == 'values':
|
|
283
|
+
if cvar.meta['context'] == 'scalar':
|
|
284
|
+
# Single-values (e.g. int, float, str)
|
|
285
|
+
# print(type(cvar.values))
|
|
286
|
+
if isinstance(cvar.values, np.bytes_):
|
|
287
|
+
print("BYTES")
|
|
288
|
+
outfile.write(f'{cvar.values.decode()}\n')
|
|
289
|
+
else:
|
|
290
|
+
outfile.write(f'{cvar.values}\n')
|
|
291
|
+
else:
|
|
292
|
+
# Multiple-values
|
|
293
|
+
if isinstance(cvar.values, np.ndarray):
|
|
294
|
+
for cval in cvar.values:
|
|
295
|
+
outfile.write(f'{cval}\n')
|
|
296
|
+
else:
|
|
297
|
+
outfile.write(f'{cvar.values}\n')
|
|
298
|
+
|
|
299
|
+
outfile.close()
|
|
300
|
+
|
|
301
|
+
def write_metadata_csv(self, filename: str, sep: str = '\t'):
|
|
302
|
+
"""Writes the control metadata to a CSV file"""
|
|
303
|
+
|
|
304
|
+
out_list = []
|
|
305
|
+
|
|
306
|
+
# <control_param name="soilrechr_dynamic" version="5.0">
|
|
307
|
+
# <default>dyn_soil_rechr.param</default>
|
|
308
|
+
# <force_default>1</force_default>
|
|
309
|
+
# <type>4</type>
|
|
310
|
+
# <numvals>1</numvals>
|
|
311
|
+
# <desc>Pathname of the time series of pre-processed values for dynamic parameter soil_rechr_max_frac</desc>
|
|
312
|
+
# </control_param>
|
|
313
|
+
|
|
314
|
+
for pk in sorted(list(self.__control_vars.keys())):
|
|
315
|
+
cvar = self.get(pk)
|
|
316
|
+
md = cvar.meta
|
|
317
|
+
assert md is not None
|
|
318
|
+
|
|
319
|
+
meta_default = md.get('default')
|
|
320
|
+
assert meta_default is not None
|
|
321
|
+
|
|
322
|
+
if pk in ['start_time', 'end_time']:
|
|
323
|
+
dt = pd.Timestamp(meta_default)
|
|
324
|
+
out_list.append([cvar.name,
|
|
325
|
+
'int32',
|
|
326
|
+
md.get('description', ''),
|
|
327
|
+
f'{dt.year},{dt.month},{dt.day},{dt.hour},{dt.minute},{dt.second}'])
|
|
328
|
+
# pd.Timestamp(meta_default).strftime('%-Y,%-m,%-d,%-H,%-M,%-S')])
|
|
329
|
+
else:
|
|
330
|
+
out_list.append([cvar.name,
|
|
331
|
+
md.get('datatype', ''),
|
|
332
|
+
md.get('description', ''),
|
|
333
|
+
md.get('default')])
|
|
334
|
+
|
|
335
|
+
col_names = ['variable_name', 'datatype', 'description', 'default']
|
|
336
|
+
|
|
337
|
+
df = pd.DataFrame.from_records(out_list, columns=col_names)
|
|
338
|
+
if sep == ',': # pragma: no cover
|
|
339
|
+
df.to_csv(filename, sep=sep, quotechar='"', index=False)
|
|
340
|
+
else:
|
|
341
|
+
df.to_csv(filename, sep=sep, index=False)
|
|
342
|
+
|
|
343
|
+
def _check_condition(self, cstr: str) -> bool:
|
|
344
|
+
"""Takes a string of the form '<control_var> <op> <value>' and checks
|
|
345
|
+
if the condition is True
|
|
346
|
+
"""
|
|
347
|
+
# if len(cstr) == 0:
|
|
348
|
+
# return True
|
|
349
|
+
value: Union[int, str]
|
|
350
|
+
|
|
351
|
+
var, op, value = cstr.split(' ')
|
|
352
|
+
value = int(value)
|
|
353
|
+
|
|
354
|
+
ctl_val = self.get(var).values
|
|
355
|
+
assert ctl_val is not None
|
|
356
|
+
|
|
357
|
+
return cond_check[op](ctl_val, value)
|
|
358
|
+
|
|
359
|
+
def _read(self):
|
|
360
|
+
"""Abstract function for reading.
|
|
361
|
+
"""
|
|
362
|
+
assert False, 'Control._read() must be defined by child class'
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Union
|
|
7
|
+
from ..constants import PTYPE_TO_DTYPE, VAR_DELIM
|
|
8
|
+
from ..prms_helpers import get_file_iter
|
|
9
|
+
from .Control import Control
|
|
10
|
+
from ..Exceptions_custom import ControlError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ControlFile(Control):
|
|
14
|
+
"""
|
|
15
|
+
Class which handles the processing of PRMS control files.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# Author: Parker Norton (pnorton@usgs.gov)
|
|
19
|
+
# Create date: 2019-04-18
|
|
20
|
+
# Description: Class object to handle reading and writing PRMS control files.
|
|
21
|
+
|
|
22
|
+
def __init__(self, filename: Union[str, Path],
|
|
23
|
+
metadata,
|
|
24
|
+
verbose: Optional[bool] = False):
|
|
25
|
+
super(ControlFile, self).__init__(metadata=metadata, verbose=verbose)
|
|
26
|
+
|
|
27
|
+
self.__verbose = verbose
|
|
28
|
+
self.__isloaded = False
|
|
29
|
+
|
|
30
|
+
if isinstance(filename, str):
|
|
31
|
+
filename = Path(filename)
|
|
32
|
+
self.filename = filename
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def filename(self) -> Union[str, Path]:
|
|
36
|
+
"""Get control filename.
|
|
37
|
+
|
|
38
|
+
:returns: Name of control file
|
|
39
|
+
"""
|
|
40
|
+
return self.__filename
|
|
41
|
+
|
|
42
|
+
@filename.setter
|
|
43
|
+
def filename(self, filename: Union[str, Path]):
|
|
44
|
+
"""Set the name of the control file and read it in.
|
|
45
|
+
|
|
46
|
+
:param filename: Name of the control file
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
self.__isloaded = False
|
|
50
|
+
self.__filename = filename
|
|
51
|
+
self._read()
|
|
52
|
+
|
|
53
|
+
def _read(self):
|
|
54
|
+
"""Read contents of control file.
|
|
55
|
+
|
|
56
|
+
Reads the contents of a control file into the class.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
if self.__verbose:
|
|
60
|
+
chk_vars = []
|
|
61
|
+
|
|
62
|
+
self.__isloaded = False
|
|
63
|
+
|
|
64
|
+
it = get_file_iter(self.filename)
|
|
65
|
+
header_tmp = []
|
|
66
|
+
|
|
67
|
+
for fidx, line in enumerate(it):
|
|
68
|
+
if fidx == 0:
|
|
69
|
+
header_tmp.append(line)
|
|
70
|
+
continue
|
|
71
|
+
elif line == VAR_DELIM:
|
|
72
|
+
continue
|
|
73
|
+
else:
|
|
74
|
+
# We're dealing with a control parameter/variable
|
|
75
|
+
# We're in a parameter section
|
|
76
|
+
varname = line.split(' ')[0]
|
|
77
|
+
|
|
78
|
+
if self.__verbose:
|
|
79
|
+
if varname in chk_vars:
|
|
80
|
+
print(f'WARNING: {varname} already exists')
|
|
81
|
+
chk_vars.append(varname)
|
|
82
|
+
|
|
83
|
+
numval = int(next(it)) # number of values for this variable
|
|
84
|
+
valuetype = int(next(it)) # Variable type (1 - integer, 2 - float, 4 - character)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# if self.get(varname).context == 'scalar' and varname not in ['start_time', 'end_time']:
|
|
88
|
+
if self.get(varname).meta['context'] == 'scalar':
|
|
89
|
+
if numval == 1: # varname not in ['start_time', 'end_time']:
|
|
90
|
+
vals = PTYPE_TO_DTYPE[valuetype](next(it))
|
|
91
|
+
else:
|
|
92
|
+
# Currently only start_time and end_time are scalars with numval > 1
|
|
93
|
+
vals = np.zeros(numval, dtype=PTYPE_TO_DTYPE[valuetype])
|
|
94
|
+
for idx in range(0, numval):
|
|
95
|
+
# NOTE: string-float to int works but float to int does not
|
|
96
|
+
vals[idx] = next(it)
|
|
97
|
+
elif self.get(varname).meta['context'] == 'array':
|
|
98
|
+
if valuetype == 4:
|
|
99
|
+
# Arrays of strings should be objects
|
|
100
|
+
vals = np.zeros(numval, dtype=object)
|
|
101
|
+
else:
|
|
102
|
+
vals = np.zeros(numval, dtype=PTYPE_TO_DTYPE[valuetype])
|
|
103
|
+
|
|
104
|
+
for idx in range(0, numval):
|
|
105
|
+
# NOTE: string-float to int works but float to int does not
|
|
106
|
+
vals[idx] = next(it)
|
|
107
|
+
else:
|
|
108
|
+
print(f'WARNING: {varname} has context={self.get(varname).meta["context"]} which is not supported')
|
|
109
|
+
|
|
110
|
+
# After reading expected values make sure there aren't more values
|
|
111
|
+
# before the next delimiter.
|
|
112
|
+
try:
|
|
113
|
+
cnt = numval
|
|
114
|
+
while next(it) != VAR_DELIM:
|
|
115
|
+
cnt += 1
|
|
116
|
+
|
|
117
|
+
if cnt > numval:
|
|
118
|
+
raise ControlError(f'{varname}: too many values specified')
|
|
119
|
+
# print(f'WARNING: Too many values specified for {varname}')
|
|
120
|
+
# print(f' {numval} expected, {cnt} given')
|
|
121
|
+
# print(f' Keeping first {numval} values')
|
|
122
|
+
except StopIteration:
|
|
123
|
+
# Hit the end of the file
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
self.get(varname).values = vals
|
|
127
|
+
|
|
128
|
+
except ValueError as err:
|
|
129
|
+
print(f'WARNING: {varname} is not a valid control variable')
|
|
130
|
+
print(err)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
while next(it) != VAR_DELIM:
|
|
134
|
+
pass
|
|
135
|
+
except StopIteration:
|
|
136
|
+
# Hit the end of the file
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
self.header = header_tmp
|
|
140
|
+
self.__isloaded = True
|
|
141
|
+
|
|
142
|
+
# def clear_parameter_group(self, group_name):
|
|
143
|
+
# """Clear a parameter group.
|
|
144
|
+
#
|
|
145
|
+
# Given a single parameter group name will clear out values for that parameter
|
|
146
|
+
# and all related parameters.
|
|
147
|
+
#
|
|
148
|
+
# Args:
|
|
149
|
+
# group_name: The name of a group of related parameters.
|
|
150
|
+
# One of 'statVar', 'aniOut', 'mapOut', 'dispVar', 'nhruOut'.
|
|
151
|
+
# """
|
|
152
|
+
#
|
|
153
|
+
# groups = {'aniOut': {'naniOutVars': 0, 'aniOutON_OFF': 0, 'aniOutVar_names': []},
|
|
154
|
+
# 'dispVar': {'ndispGraphs': 0, 'dispVar_element': [], 'dispVar_names': [], 'dispVar_plot': []},
|
|
155
|
+
# 'mapOut': {'nmapOutVars': 0, 'mapOutON_OFF': 0, 'mapOutVar_names': []},
|
|
156
|
+
# 'nhruOut': {'nhruOutVars': 0, 'nhruOutON_OFF': 0, 'nhruOutVar_names': []},
|
|
157
|
+
# 'statVar': {'nstatVars': 0, 'statsON_OFF': 0, 'statVar_element': [], 'statVar_names': []}}
|
|
158
|
+
#
|
|
159
|
+
# for (kk, vv) in iteritems(groups[group_name]):
|
|
160
|
+
# if kk in self.__controldict:
|
|
161
|
+
# self.replace_values(kk, vv)
|