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
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import numpy as np
|
|
3
|
+
import numpy.typing as npt
|
|
4
|
+
import pandas as pd # type: ignore
|
|
5
|
+
from typing import Any, cast, Dict, List, NamedTuple, Optional, Union
|
|
6
|
+
import xml.etree.ElementTree as xmlET
|
|
7
|
+
|
|
8
|
+
from ..constants import NEW_PTYPE_TO_DTYPE
|
|
9
|
+
from ..dimensions.Dimensions import ParamDimensions
|
|
10
|
+
from ..Exceptions_custom import FixedDimensionError
|
|
11
|
+
|
|
12
|
+
ParamDataRawType = Union[npt.NDArray, np.int32, np.float32, np.float64, np.str_]
|
|
13
|
+
ParamDataType = Union[npt.NDArray, np.int32, np.float32, np.float64, np.str_, int, float, str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Outliers(NamedTuple):
|
|
17
|
+
name: str
|
|
18
|
+
under: int
|
|
19
|
+
over: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Stats(NamedTuple):
|
|
23
|
+
name: str
|
|
24
|
+
min: Optional[npt.DTypeLike]
|
|
25
|
+
max: Optional[npt.DTypeLike]
|
|
26
|
+
mean: Optional[npt.DTypeLike]
|
|
27
|
+
median: Optional[npt.DTypeLike]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Parameter(object):
|
|
31
|
+
"""Container for a single Parameter object.
|
|
32
|
+
|
|
33
|
+
A parameter has a name, datatype, optional units, one or more dimensions, and
|
|
34
|
+
associated data.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# Container for a single parameter
|
|
38
|
+
def __init__(self, name: str,
|
|
39
|
+
meta: Optional[Dict] = None,
|
|
40
|
+
global_dims=None,
|
|
41
|
+
strict: Optional[bool] = True):
|
|
42
|
+
"""
|
|
43
|
+
Initialize a parameter object.
|
|
44
|
+
|
|
45
|
+
:param name: A valid PRMS parameter name
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
# Set the parameter name
|
|
49
|
+
self.__name = name
|
|
50
|
+
self.__dimensions = ParamDimensions(strict=False)
|
|
51
|
+
|
|
52
|
+
if meta is None:
|
|
53
|
+
if strict:
|
|
54
|
+
raise ValueError(f'Strict is true but no metadata was supplied')
|
|
55
|
+
else:
|
|
56
|
+
# NOTE: Having no metadata creates a parameter with no dimensions
|
|
57
|
+
self.meta = {}
|
|
58
|
+
else:
|
|
59
|
+
if strict:
|
|
60
|
+
if name in meta:
|
|
61
|
+
self.meta = meta[name]
|
|
62
|
+
|
|
63
|
+
# Add the dimensions for this parameter
|
|
64
|
+
for cname in self.meta['dimensions']:
|
|
65
|
+
self.__dimensions.add(cname)
|
|
66
|
+
|
|
67
|
+
if global_dims is not None:
|
|
68
|
+
self.__dimensions[cname].size = global_dims.get(cname).size
|
|
69
|
+
self.__dimensions[cname].meta = global_dims[cname].meta
|
|
70
|
+
else:
|
|
71
|
+
raise ValueError(f'`{self.name}` does not exist in metadata')
|
|
72
|
+
else:
|
|
73
|
+
# The meta must be supplied as an adhoc dictionary
|
|
74
|
+
self.meta = meta
|
|
75
|
+
|
|
76
|
+
self.__data: Optional[ParamDataRawType] = None
|
|
77
|
+
self.__modified = False
|
|
78
|
+
|
|
79
|
+
def __str__(self) -> str:
|
|
80
|
+
"""Pretty-print string representation of the parameter information.
|
|
81
|
+
|
|
82
|
+
:return: Pretty-print string of arameter information
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
outstr = f'----- Parameter -----\n'
|
|
86
|
+
outstr += f'name: {self.name}\n'
|
|
87
|
+
|
|
88
|
+
for kk, vv in self.meta.items():
|
|
89
|
+
outstr += f'{kk}: {vv}\n'
|
|
90
|
+
|
|
91
|
+
return outstr
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def as_dataframe(self) -> pd.DataFrame:
|
|
95
|
+
"""Returns the parameter data as a pandas DataFrame with local model indices as the index.
|
|
96
|
+
|
|
97
|
+
:returns: dataframe of parameter data
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
if len(self.data_raw.shape) == 2:
|
|
101
|
+
df = pd.DataFrame(self.data_raw)
|
|
102
|
+
df.rename(columns=lambda xx: '{}_{}'.format(self.name,
|
|
103
|
+
df.columns.get_loc(xx) + 1), inplace=True) # type: ignore
|
|
104
|
+
else:
|
|
105
|
+
# Assuming 1D array
|
|
106
|
+
df = pd.DataFrame(self.data_raw, columns=[self.name]) # type: ignore
|
|
107
|
+
|
|
108
|
+
df.rename(index={k: k + 1 for k in df.index}, inplace=True)
|
|
109
|
+
|
|
110
|
+
if self.is_hru_param():
|
|
111
|
+
idx_name = 'model_hru_idx'
|
|
112
|
+
elif self.is_seg_param():
|
|
113
|
+
idx_name = 'model_seg_idx'
|
|
114
|
+
elif self.is_poi_param():
|
|
115
|
+
idx_name = 'model_poi_idx'
|
|
116
|
+
else:
|
|
117
|
+
idx_name = 'idx'
|
|
118
|
+
|
|
119
|
+
df.index.name = idx_name
|
|
120
|
+
return df
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def data(self) -> ParamDataType:
|
|
124
|
+
"""Returns the data associated with the parameter.
|
|
125
|
+
|
|
126
|
+
:returns: parameter data
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
# TODO: Best way to prevent modification of data elements?
|
|
130
|
+
if self.__data is not None:
|
|
131
|
+
if self.is_scalar:
|
|
132
|
+
return self.__data.item()
|
|
133
|
+
return self.__data
|
|
134
|
+
raise TypeError(f'Parameter, {self.__name}, has no data')
|
|
135
|
+
|
|
136
|
+
@data.setter
|
|
137
|
+
def data(self, data_in: ParamDataRawType):
|
|
138
|
+
"""Sets the data for the parameter.
|
|
139
|
+
|
|
140
|
+
:param data_in: A list containing the parameter data
|
|
141
|
+
:raises TypeError: if the datatype for the parameter is invalid
|
|
142
|
+
:raises ValueError: if the number of dimensions for the parameter is greater than 2
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
# Metadata required: datatype, dimensions
|
|
146
|
+
if self.is_scalar:
|
|
147
|
+
if isinstance(data_in, np.ndarray):
|
|
148
|
+
if data_in.size > 1:
|
|
149
|
+
raise IndexError(f'{self.__name}: parameter expects a scalar but '
|
|
150
|
+
f'incoming data has size={data_in.size}')
|
|
151
|
+
|
|
152
|
+
if data_in.dtype == NEW_PTYPE_TO_DTYPE[self.meta['datatype']]:
|
|
153
|
+
self.__data = data_in
|
|
154
|
+
else:
|
|
155
|
+
# Attempt to convert to correct datatype
|
|
156
|
+
self.__data = np.array(data_in, dtype=NEW_PTYPE_TO_DTYPE[self.meta['datatype']])
|
|
157
|
+
else:
|
|
158
|
+
self.__data = np.array([data_in], dtype=NEW_PTYPE_TO_DTYPE[self.meta['datatype']])
|
|
159
|
+
|
|
160
|
+
if self.__dimensions.get('one').size == 0:
|
|
161
|
+
self.__dimensions.get('one').size = 1
|
|
162
|
+
elif isinstance(data_in, np.ndarray) or isinstance(data_in, np.generic):
|
|
163
|
+
expected_shape = tuple(ss.size for ss in self.dimensions.values())
|
|
164
|
+
expected_size = functools.reduce(lambda x, y: x * y, expected_shape)
|
|
165
|
+
|
|
166
|
+
if expected_size > 0 and data_in.shape != expected_shape:
|
|
167
|
+
# If this is a parameter that was collapsed to a scalar we
|
|
168
|
+
# can broadcast it to the correct shape
|
|
169
|
+
if data_in.size == 1:
|
|
170
|
+
data_in = np.repeat(data_in, expected_size)
|
|
171
|
+
|
|
172
|
+
# if data_in.size == 12 and expected_shape[1] == 12:
|
|
173
|
+
if data_in.size == 12 and 'nmonths' in self.__dimensions.keys():
|
|
174
|
+
# Expand nmonths to nhru, nmonth
|
|
175
|
+
data_in = np.resize(data_in, expected_shape)
|
|
176
|
+
else:
|
|
177
|
+
# Try to reshape the data to match the dimensionality
|
|
178
|
+
try:
|
|
179
|
+
data_in = data_in.reshape(expected_shape, order='F')
|
|
180
|
+
except ValueError:
|
|
181
|
+
raise IndexError(f'{self.__name}: Shape of incoming data, {data_in.shape}, '
|
|
182
|
+
f'does not match the expected shape, {expected_shape} '
|
|
183
|
+
'and cannot be reshaped to expected shape.')
|
|
184
|
+
|
|
185
|
+
if data_in.ndim != len(self.meta['dimensions']):
|
|
186
|
+
raise IndexError(f'{self.__name}: Number of dimensions do not match '
|
|
187
|
+
f'({data_in.ndim} != {len(self.meta["dimensions"])})')
|
|
188
|
+
|
|
189
|
+
if data_in.dtype == NEW_PTYPE_TO_DTYPE[self.meta['datatype']]:
|
|
190
|
+
self.__data = data_in
|
|
191
|
+
else:
|
|
192
|
+
# Attempt to convert to correct datatype
|
|
193
|
+
self.__data = np.array(data_in, dtype=NEW_PTYPE_TO_DTYPE[self.meta['datatype']])
|
|
194
|
+
|
|
195
|
+
if expected_size == 0:
|
|
196
|
+
# Set the dimension size(s) if existing dimension sizes are zero
|
|
197
|
+
for cname, cdim in zip(self.meta['dimensions'], self.__data.shape):
|
|
198
|
+
self.__dimensions.get(cname).size = cdim
|
|
199
|
+
else:
|
|
200
|
+
# TODO: 2023-11-13 PAN - This should raise an error
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def data_raw(self) -> ParamDataRawType:
|
|
205
|
+
"""Returns the raw data associated with the parameter.
|
|
206
|
+
|
|
207
|
+
:returns: parameter data
|
|
208
|
+
"""
|
|
209
|
+
if self.__data is not None:
|
|
210
|
+
return self.__data
|
|
211
|
+
raise TypeError(f'Parameter, {self.__name}, has no data')
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def dimensions(self) -> ParamDimensions:
|
|
215
|
+
"""Returns the Dimensions object associated with the parameter.
|
|
216
|
+
|
|
217
|
+
:returns: Dimensions object for the parameter"""
|
|
218
|
+
|
|
219
|
+
return self.__dimensions
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def index_map(self) -> Union[Dict[Any, int], None]:
|
|
223
|
+
"""Returns an ordered dictionary which maps data values of a 1D array
|
|
224
|
+
to index positions.
|
|
225
|
+
|
|
226
|
+
:returns: dictionary mapping data values to index position
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
# FIXME: 20230706 PAN - this is flawed; duplicated values overwrite
|
|
230
|
+
# index positions.
|
|
231
|
+
if self.data_raw.ndim == 1:
|
|
232
|
+
return dict((val.item(), idx[0]) for idx, val in np.ndenumerate(self.data_raw))
|
|
233
|
+
else:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def is_scalar(self):
|
|
238
|
+
try:
|
|
239
|
+
return 'one' in self.meta['dimensions']
|
|
240
|
+
except KeyError:
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def modified(self) -> bool:
|
|
245
|
+
"""Logical denoting whether elements in the parameter data have been modified.
|
|
246
|
+
|
|
247
|
+
:returns: True is parameter data was modified
|
|
248
|
+
"""
|
|
249
|
+
return self.__modified
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def modules(self) -> List[str]:
|
|
253
|
+
"""Returns the names of the PRMS modules that require the parameter.
|
|
254
|
+
|
|
255
|
+
:returns: names of PRMS modules that require the parameter
|
|
256
|
+
"""
|
|
257
|
+
return self.meta.get('modules', [])
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def name(self) -> str:
|
|
261
|
+
"""Returns the parameter name.
|
|
262
|
+
|
|
263
|
+
:returns: parameter name
|
|
264
|
+
"""
|
|
265
|
+
return self.__name
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def ndim(self) -> int:
|
|
269
|
+
"""Returns the number of dimensions that are defined for the parameter.
|
|
270
|
+
|
|
271
|
+
:returns: numbers of parameter dimensions
|
|
272
|
+
"""
|
|
273
|
+
if self.is_scalar:
|
|
274
|
+
return 0
|
|
275
|
+
else:
|
|
276
|
+
return self.__dimensions.ndim
|
|
277
|
+
|
|
278
|
+
# @property
|
|
279
|
+
# def size(self) -> int:
|
|
280
|
+
# """Return the total size of the parameter for the defined dimensions.
|
|
281
|
+
#
|
|
282
|
+
# :returns total size of parameter dimensions"""
|
|
283
|
+
# arr_shp = [dd.size for dd in self.dimensions.dimensions.values()]
|
|
284
|
+
#
|
|
285
|
+
# # Compute the total size of the parameter
|
|
286
|
+
# return functools.reduce(lambda x, y: x * y, arr_shp)
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def xml(self) -> xmlET.Element:
|
|
290
|
+
"""Return the xml metadata for the parameter as an xml Element.
|
|
291
|
+
|
|
292
|
+
:returns: xml element of parameter metadata
|
|
293
|
+
"""
|
|
294
|
+
param_root = xmlET.Element('parameter')
|
|
295
|
+
param_root.set('name', cast(str, self.name))
|
|
296
|
+
param_root.set('version', 'ver')
|
|
297
|
+
param_root.append(self.dimensions.xml)
|
|
298
|
+
return param_root
|
|
299
|
+
|
|
300
|
+
def all_equal(self) -> bool:
|
|
301
|
+
"""Check if all values for parameter are equal.
|
|
302
|
+
|
|
303
|
+
:returns true if all values are equal
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
if self.data_raw.size > 1:
|
|
307
|
+
return (self.data_raw == self.data_raw[0]).all() # type: ignore
|
|
308
|
+
|
|
309
|
+
return True # scalar
|
|
310
|
+
|
|
311
|
+
def check(self) -> str:
|
|
312
|
+
"""Verifies the total size of the data for the parameter matches the total declared dimension(s) size
|
|
313
|
+
and returns a message.
|
|
314
|
+
|
|
315
|
+
:returns: OK for valid data size, BAD for invalid data size
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
# TODO: check that values are between min and max values
|
|
319
|
+
# Check a variable to see if the number of values it has is
|
|
320
|
+
# consistent with the given dimensions
|
|
321
|
+
if self.has_correct_size():
|
|
322
|
+
# The number of values for the defined dimensions match
|
|
323
|
+
return f'{self.name}: OK'
|
|
324
|
+
else:
|
|
325
|
+
return f'{self.name}: BAD'
|
|
326
|
+
|
|
327
|
+
def check_values(self) -> bool:
|
|
328
|
+
"""Returns true if all data values are within the min/max values for the parameter.
|
|
329
|
+
|
|
330
|
+
:returns: true when all values are within the valid min/max range for the parameter
|
|
331
|
+
"""
|
|
332
|
+
# if self.__data is not None:
|
|
333
|
+
minval = self.meta.get('minimum', None)
|
|
334
|
+
maxval = self.meta.get('maximum', None)
|
|
335
|
+
|
|
336
|
+
if minval is not None and maxval is not None:
|
|
337
|
+
# Check both ends of the range
|
|
338
|
+
if not (isinstance(minval, str) or isinstance(maxval, str)):
|
|
339
|
+
return (self.data_raw >= minval).all() and (self.data_raw <= maxval).all().item()
|
|
340
|
+
elif minval == 'bounded':
|
|
341
|
+
return (self.data_raw >= self.meta.get('default')).all().item() # type: ignore
|
|
342
|
+
|
|
343
|
+
return True
|
|
344
|
+
|
|
345
|
+
def has_correct_size(self) -> bool:
|
|
346
|
+
"""Verifies the total size of the data for the parameter matches the total declared dimension(s) sizes.
|
|
347
|
+
|
|
348
|
+
:returns: true if size of parameter data matches declared size of dimensions
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
# Get the defined size for each dimension used by the variable
|
|
352
|
+
total_size = 1
|
|
353
|
+
for dd in self.dimensions.keys():
|
|
354
|
+
total_size *= self.dimensions.get(dd).size
|
|
355
|
+
|
|
356
|
+
return self.data_raw.size == total_size
|
|
357
|
+
|
|
358
|
+
def is_hru_param(self) -> bool:
|
|
359
|
+
"""Test if parameter is dimensioned by HRU.
|
|
360
|
+
|
|
361
|
+
:returns: true if parameter is dimensioned by nhru, ngw, or nssr
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
return not set(self.meta.get('dimensions', [])).isdisjoint({'nhru', 'ngw', 'nssr'})
|
|
365
|
+
|
|
366
|
+
def is_poi_param(self) -> bool:
|
|
367
|
+
"""Test if parameter is dimensioned by nsegment
|
|
368
|
+
|
|
369
|
+
:returns: true if parameter is dimensioned by npoigages
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
return not set(self.meta.get('dimensions', [])).isdisjoint({'npoigages'})
|
|
373
|
+
|
|
374
|
+
def is_seg_param(self) -> bool:
|
|
375
|
+
"""Test if parameter is dimensioned by nsegment.
|
|
376
|
+
|
|
377
|
+
:returns: true if parameter is dimensioned by nsegment"""
|
|
378
|
+
|
|
379
|
+
return not set(self.meta.get('dimensions', [])).isdisjoint({'nsegment'})
|
|
380
|
+
|
|
381
|
+
def outliers(self) -> Outliers:
|
|
382
|
+
"""Returns the number of values less than or greater than the valid range
|
|
383
|
+
|
|
384
|
+
:returns: NamedTuple containing count of values less than and values greater than valid range
|
|
385
|
+
"""
|
|
386
|
+
# Outliers = namedtuple('Outliers', ['name', 'under', 'over'])
|
|
387
|
+
|
|
388
|
+
values_under = 0
|
|
389
|
+
values_over = 0
|
|
390
|
+
|
|
391
|
+
if self.meta.get('minimum', None) is not None:
|
|
392
|
+
values_under = np.count_nonzero(self.data_raw < self.meta.get('minimum')) # type: ignore
|
|
393
|
+
|
|
394
|
+
if self.meta.get('maximum', None) is not None:
|
|
395
|
+
values_over = np.count_nonzero(self.data_raw > self.meta.get('maximum')) # type: ignore
|
|
396
|
+
|
|
397
|
+
return Outliers(self.__name, values_under, values_over)
|
|
398
|
+
|
|
399
|
+
def remove_by_index(self, dim_name: str, indices: List[int]):
|
|
400
|
+
"""Remove columns (nhru or nsegment) from data array given a list of indices.
|
|
401
|
+
|
|
402
|
+
:param dim_name: Name of dimension to reduce
|
|
403
|
+
:param indices: List of indices to remove"""
|
|
404
|
+
|
|
405
|
+
if isinstance(indices, type(dict().values())):
|
|
406
|
+
indices = list(indices)
|
|
407
|
+
|
|
408
|
+
if self.__data is not None:
|
|
409
|
+
if len(indices) > self.__data.size:
|
|
410
|
+
raise IndexError(f'{self.name}: Cannot remove more values than exist')
|
|
411
|
+
|
|
412
|
+
self.__data = np.delete(self.__data, indices, axis=self.dimensions.get_position(dim_name))
|
|
413
|
+
assert self.__data is not None # Needed so mypy doesn't fail on next line
|
|
414
|
+
self.dimensions[dim_name].size = self.__data.shape[self.dimensions.get_position(dim_name)]
|
|
415
|
+
else:
|
|
416
|
+
raise TypeError('Parameter data is not initialized')
|
|
417
|
+
|
|
418
|
+
# def reshape(self, new_dims: Dict):
|
|
419
|
+
# """Reshape a parameter, broadcasting existing values as necessary.
|
|
420
|
+
#
|
|
421
|
+
# :param new_dims: Dimension names and sizes that will be used to reshape the parameter data
|
|
422
|
+
# """
|
|
423
|
+
#
|
|
424
|
+
# if self.__data is None:
|
|
425
|
+
# # Reshape has no meaning if there is no data to reshape
|
|
426
|
+
# return
|
|
427
|
+
#
|
|
428
|
+
# if self.dimensions.ndim == 1:
|
|
429
|
+
# if 'one' in self.dimensions.keys():
|
|
430
|
+
# # Reshaping from a scalar to a 1D or 2D array
|
|
431
|
+
# # print('Scalar to 1D or 2D')
|
|
432
|
+
# new_sizes = [vv.size for vv in new_dims.values()]
|
|
433
|
+
# tmp_data = np.broadcast_to(self.__data, new_sizes)
|
|
434
|
+
#
|
|
435
|
+
# # Remove the original dimension
|
|
436
|
+
# self.dimensions.remove('one')
|
|
437
|
+
#
|
|
438
|
+
# # Add the new ones
|
|
439
|
+
# for kk, vv in new_dims.items():
|
|
440
|
+
# self.dimensions.add(kk, vv.size)
|
|
441
|
+
#
|
|
442
|
+
# self.__data = tmp_data
|
|
443
|
+
# elif set(self.dimensions.keys()).issubset(set(new_dims.keys())):
|
|
444
|
+
# # Reschaping a 1D to a 2D
|
|
445
|
+
# if len(new_dims) == 1:
|
|
446
|
+
# print('ERROR: Cannot reshape from 1D array to 1D array')
|
|
447
|
+
# else:
|
|
448
|
+
# # print('1D array to 2D array')
|
|
449
|
+
# new_sizes = [vv.size for vv in new_dims.values()]
|
|
450
|
+
# try:
|
|
451
|
+
# tmp_data = np.broadcast_to(self.__data, new_sizes)
|
|
452
|
+
# except ValueError:
|
|
453
|
+
# # operands could not be broadcast together with remapped shapes
|
|
454
|
+
# tmp_data = np.broadcast_to(self.__data, new_sizes[::-1]).T
|
|
455
|
+
#
|
|
456
|
+
# old_dim = list(self.dimensions.keys())[0]
|
|
457
|
+
# self.dimensions.remove(old_dim)
|
|
458
|
+
#
|
|
459
|
+
# for kk, vv in new_dims.items():
|
|
460
|
+
# self.dimensions.add(kk, vv.size)
|
|
461
|
+
#
|
|
462
|
+
# self.__data = tmp_data
|
|
463
|
+
|
|
464
|
+
def stats(self) -> Stats:
|
|
465
|
+
"""Returns basic statistics on parameter values.
|
|
466
|
+
|
|
467
|
+
:returns: None (for strings or no data) or NamedTuple containing min, max, mean, and median of parameter values
|
|
468
|
+
"""
|
|
469
|
+
# Stats = namedtuple('Stats', ['name', 'min', 'max', 'mean', 'median'])
|
|
470
|
+
|
|
471
|
+
try:
|
|
472
|
+
return Stats(self.__name, np.min(self.data_raw), np.max(self.data_raw),
|
|
473
|
+
np.mean(self.data_raw), np.median(self.data_raw)) # type: ignore
|
|
474
|
+
except TypeError:
|
|
475
|
+
# This happens with string data
|
|
476
|
+
return Stats(self.__name, None, None, None, None)
|
|
477
|
+
|
|
478
|
+
def subset_by_index(self, dim_name: str, indices):
|
|
479
|
+
"""Reduce array by axis (nhru or nsegment) a list of local indices.
|
|
480
|
+
|
|
481
|
+
:param dim_name: name of dimension
|
|
482
|
+
:param indices: local indices of HRUs or segments to extract"""
|
|
483
|
+
|
|
484
|
+
if isinstance(indices, type(dict().values())):
|
|
485
|
+
indices = list(indices)
|
|
486
|
+
|
|
487
|
+
if self.dimensions[dim_name].is_fixed:
|
|
488
|
+
raise FixedDimensionError(f'{self.name}: Cannot reduce array on a fixed dimension {dim_name}')
|
|
489
|
+
|
|
490
|
+
# First get the index position of the given dimension name so data
|
|
491
|
+
# won't be changed if the dimension name does not exist
|
|
492
|
+
dim_idx = self.dimensions.get_position(dim_name)
|
|
493
|
+
|
|
494
|
+
# We can't use the data setter when modifying the shape of parameter data
|
|
495
|
+
self.__data = np.take(self.data_raw, indices, axis=dim_idx)
|
|
496
|
+
assert self.data_raw is not None # Needed so mypy doesn't fail on next line
|
|
497
|
+
self.dimensions[dim_name].size = self.data_raw.shape[dim_idx]
|
|
498
|
+
|
|
499
|
+
def tolist(self) -> List[Union[int, float, str]]:
|
|
500
|
+
"""Returns the parameter data as a list.
|
|
501
|
+
|
|
502
|
+
:returns: Parameter data
|
|
503
|
+
"""
|
|
504
|
+
|
|
505
|
+
# TODO: is this correct for snarea_curve?
|
|
506
|
+
# Return a list of the data
|
|
507
|
+
return self.data_raw.ravel(order='F').tolist()
|
|
508
|
+
|
|
509
|
+
def toparamdb(self) -> str:
|
|
510
|
+
"""Outputs parameter data in the paramDb csv format.
|
|
511
|
+
|
|
512
|
+
:returns: parameter data in the paramDb CSV format
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
outstr = '$id,{}\n'.format(self.name)
|
|
516
|
+
|
|
517
|
+
ii = 0
|
|
518
|
+
# Do not use self.tolist() here because it causes minor changes
|
|
519
|
+
# to the values for floats.
|
|
520
|
+
for dd in self.data_raw.ravel(order='F'):
|
|
521
|
+
if self.meta.get('datatype', 'null') in ['float32', 'float64']:
|
|
522
|
+
# Float and double types have to be formatted specially so
|
|
523
|
+
# they aren't written in exponential notation or with
|
|
524
|
+
# extraneous zeroes
|
|
525
|
+
tmp = f'{dd:<20.7f}'.rstrip('0 ')
|
|
526
|
+
if tmp[-1] == '.':
|
|
527
|
+
tmp += '0'
|
|
528
|
+
outstr += f'{ii+1},{tmp}\n'
|
|
529
|
+
else:
|
|
530
|
+
outstr += f'{ii+1},{dd}\n'
|
|
531
|
+
ii += 1
|
|
532
|
+
return outstr
|
|
533
|
+
|
|
534
|
+
def tostructure(self) -> dict:
|
|
535
|
+
"""Returns a dictionary structure of the parameter.
|
|
536
|
+
|
|
537
|
+
This is typically used for serializing parameters.
|
|
538
|
+
|
|
539
|
+
:returns: dictionary structure of the parameter
|
|
540
|
+
"""
|
|
541
|
+
|
|
542
|
+
# Return all information about this parameter in the following form
|
|
543
|
+
param = {'name': self.name,
|
|
544
|
+
'datatype': self.meta.get('datatype', 'null'),
|
|
545
|
+
'dimensions': self.dimensions.tostructure(),
|
|
546
|
+
'data': self.tolist()}
|
|
547
|
+
return param
|
|
548
|
+
|
|
549
|
+
def unique(self) -> Optional[npt.NDArray]:
|
|
550
|
+
"""Create array of unique values from the parameter data.
|
|
551
|
+
|
|
552
|
+
:returns: Array of unique values
|
|
553
|
+
"""
|
|
554
|
+
return np.unique(self.data_raw)
|
|
555
|
+
|
|
556
|
+
def update_element(self, index: int, value: Union[int, float, List[int], List[float]]):
|
|
557
|
+
"""Update single value or row of values (e.g. nhru by nmonths) for a
|
|
558
|
+
given local zero-based index in the parameter data array.
|
|
559
|
+
|
|
560
|
+
:param index: scalar, zero-based array index
|
|
561
|
+
:param value: updated value(s)
|
|
562
|
+
"""
|
|
563
|
+
|
|
564
|
+
# NOTE: index is zero-based
|
|
565
|
+
# Update a single element or single row (e.g. nhru x nmonth) in the
|
|
566
|
+
# parameter data array.
|
|
567
|
+
if self.is_scalar:
|
|
568
|
+
if isinstance(value, list):
|
|
569
|
+
if len(value) > 1:
|
|
570
|
+
raise TypeError(f'{self.name}: Cannot update scalar with list containing multiple values')
|
|
571
|
+
value = value[0]
|
|
572
|
+
elif isinstance(value, np.ndarray):
|
|
573
|
+
if value.size > 1:
|
|
574
|
+
raise TypeError(f'{self.name}: Cannot update scalar with array containing multiple values')
|
|
575
|
+
value = value.item()
|
|
576
|
+
|
|
577
|
+
if self.data != value:
|
|
578
|
+
# We use the data setter to make sure the new scalar is cast to a numpy array internally
|
|
579
|
+
self.data = value # type: ignore
|
|
580
|
+
self.__modified = True
|
|
581
|
+
else:
|
|
582
|
+
if self.data_raw.ndim == 1:
|
|
583
|
+
if isinstance(value, list):
|
|
584
|
+
if len(value) > 1:
|
|
585
|
+
raise TypeError(f'{self.name}: Cannot update single element with list '
|
|
586
|
+
f'containing multiple values')
|
|
587
|
+
value = value[0]
|
|
588
|
+
elif isinstance(value, np.ndarray):
|
|
589
|
+
if value.size > 1:
|
|
590
|
+
raise TypeError(f'{self.name}: Cannot update single element with array '
|
|
591
|
+
f'containing multiple values')
|
|
592
|
+
value = value.item()
|
|
593
|
+
elif self.data_raw.ndim == 2:
|
|
594
|
+
if isinstance(value, list):
|
|
595
|
+
if len(value) == 1:
|
|
596
|
+
value = value[0]
|
|
597
|
+
elif len(value) != self.data_raw.shape[1]:
|
|
598
|
+
raise TypeError(f'{self.name}: Cannot update row with list of incorrect size')
|
|
599
|
+
elif isinstance(value, np.ndarray):
|
|
600
|
+
if value.size == 1:
|
|
601
|
+
value = value.item()
|
|
602
|
+
elif value.size != self.data_raw.shape[1]:
|
|
603
|
+
raise TypeError(f'{self.name}: Cannot update row with array of incorrect size')
|
|
604
|
+
|
|
605
|
+
if not np.array_equal(self.__data[index], value): # type: ignore
|
|
606
|
+
# Change the element only if the incoming value is different
|
|
607
|
+
# from the existing value
|
|
608
|
+
self.__data[index] = value # type: ignore
|
|
609
|
+
self.__modified = True
|
|
610
|
+
|
|
611
|
+
def _value_index_1d(self, value: Union[int, float, str]) -> npt.NDArray:
|
|
612
|
+
"""Given a scalar value return the indices where there is a match.
|
|
613
|
+
|
|
614
|
+
:param value: The value to find in the parameter data array
|
|
615
|
+
|
|
616
|
+
:returns: Array of zero-based indices matching the given value
|
|
617
|
+
"""
|
|
618
|
+
|
|
619
|
+
if self.ndim == 1:
|
|
620
|
+
# Returns a list of indices where the data elements match value
|
|
621
|
+
return np.argwhere(self.data_raw == value)[:, 0] # .tolist()
|
|
622
|
+
# return np.where(self.data_raw == value)[0]
|
|
623
|
+
else:
|
|
624
|
+
raise TypeError(f'{self.name}: Cannot search for value in multi-dimensional array')
|