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.
Files changed (51) hide show
  1. pyPRMS/Exceptions_custom.py +31 -0
  2. pyPRMS/__init__.py +52 -0
  3. pyPRMS/cbh/Cbh.py +431 -0
  4. pyPRMS/cbh/CbhAscii.py +458 -0
  5. pyPRMS/cbh/CbhNetcdf.py +199 -0
  6. pyPRMS/cbh/__init__.py +3 -0
  7. pyPRMS/constants.py +131 -0
  8. pyPRMS/control/Control.py +362 -0
  9. pyPRMS/control/ControlFile.py +161 -0
  10. pyPRMS/control/ControlVariable.py +208 -0
  11. pyPRMS/control/__init__.py +3 -0
  12. pyPRMS/dimensions/Dimension.py +154 -0
  13. pyPRMS/dimensions/Dimensions.py +256 -0
  14. pyPRMS/dimensions/__init__.py +2 -0
  15. pyPRMS/input/DataFile.py +354 -0
  16. pyPRMS/input/InputVariable.py +61 -0
  17. pyPRMS/input/__init__.py +0 -0
  18. pyPRMS/metadata/__init__.py +1 -0
  19. pyPRMS/metadata/metadata.py +430 -0
  20. pyPRMS/parameters/ParamDb.py +73 -0
  21. pyPRMS/parameters/Parameter.py +624 -0
  22. pyPRMS/parameters/ParameterFile.py +190 -0
  23. pyPRMS/parameters/ParameterNetCDF.py +74 -0
  24. pyPRMS/parameters/ParameterSet.py +96 -0
  25. pyPRMS/parameters/Parameters.py +1506 -0
  26. pyPRMS/parameters/__init__.py +5 -0
  27. pyPRMS/plot_helpers.py +305 -0
  28. pyPRMS/prms_helpers.py +235 -0
  29. pyPRMS/py.typed +0 -0
  30. pyPRMS/summary/OutputCSV.py +64 -0
  31. pyPRMS/summary/OutputVariable.py +163 -0
  32. pyPRMS/summary/OutputVariables.py +227 -0
  33. pyPRMS/summary/__init__.py +2 -0
  34. pyPRMS/utilities/__init__.py +0 -0
  35. pyPRMS/utilities/convert_cbh.py +107 -0
  36. pyPRMS/utilities/convert_model_output.py +91 -0
  37. pyPRMS/utilities/convert_params.py +60 -0
  38. pyPRMS/version.py +13 -0
  39. pyPRMS/xml/cbh.xml +163 -0
  40. pyPRMS/xml/control.xml +1447 -0
  41. pyPRMS/xml/dimensions.xml +311 -0
  42. pyPRMS/xml/modules.xml +251 -0
  43. pyPRMS/xml/parameters.xml +6932 -0
  44. pyPRMS/xml/time_series_input.xml +198 -0
  45. pyPRMS/xml/variables.xml +8173 -0
  46. pyprms-0.9.7.dist-info/LICENSE.md +21 -0
  47. pyprms-0.9.7.dist-info/METADATA +67 -0
  48. pyprms-0.9.7.dist-info/RECORD +51 -0
  49. pyprms-0.9.7.dist-info/WHEEL +5 -0
  50. pyprms-0.9.7.dist-info/entry_points.txt +3 -0
  51. pyprms-0.9.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import datetime
4
+ import numpy as np
5
+ import re
6
+ from typing import Callable, Dict, List, Optional, Sequence, Union
7
+
8
+ from ..constants import NEW_PTYPE_TO_DTYPE
9
+ from pyPRMS.prms_helpers import cond_check, set_date
10
+ from ..Exceptions_custom import ControlError
11
+
12
+ class ControlVariable(object):
13
+ """
14
+ Class object for a single control variable.
15
+ """
16
+
17
+ # Author: Parker Norton (pnorton@usgs.gov)
18
+ # Create date: 2019-04-18
19
+
20
+ def __init__(self, name: str,
21
+ value = None,
22
+ meta: Optional[Dict] = None,
23
+ strict: Optional[bool] = True):
24
+ """Initialize a control variable object.
25
+
26
+ :param name: Name of control variable
27
+ :param meta: Metadata of the control variable
28
+ """
29
+
30
+ self.__name = name
31
+ self.__values: Union[np.ndarray, None] = None
32
+
33
+ if meta is None:
34
+ if strict:
35
+ raise ValueError(f'Strict is true but no metadata was supplied')
36
+ else:
37
+ self.meta = {}
38
+ else:
39
+ if strict:
40
+ if name in meta:
41
+ self.meta = meta[name]
42
+ else:
43
+ raise ValueError(f'`{self.name}` does not exist in metadata')
44
+ else:
45
+ # The meta must be supplied as an adhoc dictionary
46
+ self.meta = meta
47
+
48
+ self.values = value
49
+
50
+ def __str__(self) -> str:
51
+ outstr = f'----- ControlVariable -----\n'
52
+ outstr += f'name: {self.name}\n'
53
+
54
+ for kk, vv in self.meta.items():
55
+ outstr += f'{kk}: {vv}\n'
56
+
57
+ return outstr
58
+
59
+ @property
60
+ def dyn_param_meaning(self):
61
+ """Returns a list of meanings for the value of a parameter flag"""
62
+ meaning = self.meta.get('valid_values', {}).get(str(self.values), None)
63
+
64
+ if meaning is None:
65
+ return list()
66
+ else:
67
+ return meaning.split(',')
68
+
69
+ @property
70
+ def name(self) -> str:
71
+ """Returns the name of the control variable.
72
+
73
+ :returns: Name of control variable
74
+ """
75
+
76
+ return self.__name
77
+
78
+ @property
79
+ def size(self) -> int:
80
+ """Return number of values"""
81
+ if self.__values is None:
82
+ return 0
83
+ elif isinstance(self.__values, np.ndarray):
84
+ return self.__values.size
85
+ else:
86
+ # int, float, str scalars
87
+ return 1
88
+
89
+ @property
90
+ def values(self) -> Union[np.ndarray, int, float, str, None]:
91
+ """Get the values for the control variable.
92
+
93
+ If force_default is True then the default value is returned regardless
94
+ of what the value is set to; otherwise, current value is returned.
95
+
96
+ :returns: Value(s) of control variable
97
+ """
98
+
99
+ force_default = self.meta.get('force_default', False)
100
+
101
+ if force_default or self.__values is None:
102
+ if isinstance(self.meta.get('default', None), np.str_ | np.int32 | np.float32):
103
+ return self.meta.get('default', None).item()
104
+ else:
105
+ return self.meta.get('default', None)
106
+ else:
107
+ if isinstance(self.__values, np.str_ | np.int32 | np.float32):
108
+ return self.__values.item()
109
+ else:
110
+ return self.__values
111
+
112
+ @values.setter
113
+ def values(self, data: Union[Sequence[str], str, int, float, datetime.datetime]):
114
+ """Set the value(s) for the control variable.
115
+
116
+ :param data: list or string of value(s)
117
+ """
118
+
119
+ # if self.meta is not None:
120
+ if data is None:
121
+ self.__values = self.meta.get('default', None)
122
+ else:
123
+ cdtype = NEW_PTYPE_TO_DTYPE[self.meta['datatype']]
124
+
125
+ if self.meta['context'] == 'scalar':
126
+ if isinstance(data, list):
127
+ # Only first value is used for scalars
128
+ self.__values = np.array(data, dtype=cdtype)[0]
129
+ elif isinstance(data, np.ndarray):
130
+ if self.meta['datatype'] == 'datetime':
131
+ self.__values = cdtype(set_date(data))
132
+ elif data.dtype == cdtype:
133
+ self.__values = data[0]
134
+ else:
135
+ raise TypeError(f'{self.name}: {cdtype} expected but got {data[0].dtype}')
136
+ # print(f'WARNING: {self.name} - datatype inconsistency; no values added')
137
+ elif ((cdtype == np.str_ and not isinstance(data, str | cdtype)) or
138
+ (cdtype == np.int32 and not isinstance(data, int | cdtype)) or
139
+ (cdtype == np.float32 and not isinstance(data, float | cdtype))):
140
+ raise TypeError(f'{self.name}: {cdtype} expected but got {type(data)}')
141
+ else:
142
+ self.__values = cdtype(data)
143
+ else:
144
+ if isinstance(data, list):
145
+ self.__values = np.array(data, dtype=cdtype)
146
+ elif isinstance(data, np.ndarray):
147
+ if data.dtype == cdtype:
148
+ # TODO: 20230707 PAN - currently never hit with string arrays
149
+ self.__values = data # pragma: no cover
150
+ else:
151
+ # Attempt to convert to correct datatype
152
+ self.__values = np.array(data, dtype=cdtype)
153
+ else:
154
+ # TODO: 20230707 PAN - currently never hit
155
+ self.__values = np.array([data], dtype=cdtype) # pragma: no cover
156
+
157
+ @property
158
+ def value_meaning(self) -> Union[str, None]:
159
+ """Returns the meaning for a given value if it exists.
160
+
161
+ :returns: Control variable value meaning
162
+ """
163
+
164
+ # This will fail for keys that are conditionals (e.g. ">0")
165
+ meaning = self.meta.get('valid_values', None)
166
+
167
+ if meaning is None:
168
+ # valid_values key does not exist
169
+ return meaning
170
+
171
+ return self._value_meaning_test(self.values, meaning)
172
+ # return meaning.get(self.values, meaning.get(str(self.values), None))
173
+
174
+ def _value_meaning_test(self, key, src_dict):
175
+ try:
176
+ return src_dict[key]
177
+ except KeyError:
178
+ # Maybe the key is a string
179
+ try:
180
+ return src_dict[str(key)]
181
+ except KeyError:
182
+ # Maybe one of the keys is a conditional?
183
+ patterns = ['[><]']
184
+ regex = [re.compile('^' + pat).match for pat in patterns]
185
+
186
+ tt = {kk: vv for kk, vv in src_dict.items()
187
+ if any (reg(kk) for reg in regex)}
188
+
189
+ if len(tt) > 0:
190
+ # So there is a conditional
191
+ for mm in tt:
192
+ # print(mm.split())
193
+ if cond_check[mm[0]](key, int(mm[1:])):
194
+ return src_dict[mm]
195
+ # print(f'{mm}: {src_dict[mm]}')
196
+ raise ValueError('Invalid control value')
197
+
198
+
199
+ # try:
200
+ # if 'valid_values' in self.meta:
201
+ # # We want a KeyError here if the key is missing
202
+ # return self.meta['valid_values'][self.values]
203
+ #
204
+ # return None
205
+ # except KeyError:
206
+ # # Try again but return None if the key is still missing
207
+ # return self.meta['valid_values'].get(str(self.values), None)
208
+ # return None
@@ -0,0 +1,3 @@
1
+ from .Control import Control
2
+ from .ControlVariable import ControlVariable
3
+ from .ControlFile import ControlFile
@@ -0,0 +1,154 @@
1
+ from typing import Dict, Optional, Union
2
+
3
+
4
+ class Dimension(object):
5
+ """Defines a single dimension."""
6
+
7
+ __name: str = ''
8
+ __size: int = 0
9
+
10
+ def __init__(self, name: str,
11
+ meta: Optional[Dict] = None,
12
+ size: Optional[int] = None,
13
+ strict: Optional[bool] = True):
14
+ """Create a new dimension object.
15
+
16
+ A dimension has a name and a size associated with it.
17
+
18
+ :param name: The name of the dimension
19
+ :param size: The size of the dimension
20
+ :param strict: Enforce use of valid dimensions metadata
21
+ """
22
+
23
+ self.__name = name
24
+
25
+ if meta is None:
26
+ if strict:
27
+ raise ValueError(f'Strict is true but no metadata was supplied')
28
+ else:
29
+ self.meta = {}
30
+ else:
31
+ if strict:
32
+ if name in meta:
33
+ self.meta = meta[name]
34
+ else:
35
+ raise ValueError(f'`{self.name}` does not exist in metadata')
36
+ else:
37
+ if isinstance(meta, dict):
38
+ # Assume we have a dictionary of metadata for this dimension
39
+ self.meta = meta
40
+
41
+ if size is None:
42
+ self.size = self.meta.get('default', 0)
43
+ else:
44
+ self.size = size
45
+
46
+ def __iadd__(self, other: int):
47
+ """Add a number to dimension size.
48
+
49
+ :param other: Integer value
50
+
51
+ :returns: Dimension size
52
+
53
+ :raises ValueError: if type of parameter is not an integer
54
+ """
55
+
56
+ # Augment in-place addition so the instance plus a number results
57
+ # in a change to self.__size
58
+ if not isinstance(other, int):
59
+ raise ValueError('Dimension size type must be an integer')
60
+ self.size += other
61
+ return self
62
+
63
+ def __isub__(self, other: int):
64
+ """Subtracts integer from dimension size.
65
+
66
+ :param other: Integer value
67
+
68
+ :returns: Dimension size
69
+
70
+ :raises ValueError: if type of parameter is not an integer
71
+ :raises ValeuError: if parameter is not a positive integer
72
+ """
73
+
74
+ # Augment in-place addition so the instance minus a number results
75
+ # in a change to self.__size
76
+ if not isinstance(other, int):
77
+ raise ValueError('Dimension size type must be an integer')
78
+ self.size -= other
79
+ return self
80
+
81
+ def __repr__(self) -> str:
82
+ """String respresentation of dimension.
83
+
84
+ :returns: string with name and size of dimension
85
+ """
86
+ return f"Dimension(name='{self.name}', meta={self.meta}, size={self.size}, strict=False)"
87
+
88
+ def __str__(self) -> str:
89
+ """Return friendly string representation of dimension
90
+ """
91
+ outstr = f'----- Dimension -----\n'
92
+ outstr += f'name: {self.name}\n'
93
+
94
+ for kk, vv in self.meta.items():
95
+ if kk != 'size':
96
+ outstr += f'{kk}: {vv}\n'
97
+
98
+ outstr += f'size: {self.size}\n'
99
+
100
+ return outstr
101
+
102
+ @property
103
+ def is_fixed(self):
104
+ return self.meta.get('is_fixed', False)
105
+
106
+ @property
107
+ def name(self) -> str:
108
+ """Name of the dimension.
109
+
110
+ :returns: Name of the dimension
111
+ """
112
+
113
+ return self.__name
114
+
115
+ @property
116
+ def size(self) -> int:
117
+ """Size of the dimension.
118
+
119
+ :returns: Size of the dimension
120
+ """
121
+
122
+ return self.__size
123
+
124
+ @size.setter
125
+ def size(self, value: Union[int, str]):
126
+ """Set the size of the dimension.
127
+
128
+ :param value: Size of the dimension
129
+ :raises ValueError: if dimension size is not a positive integer
130
+ """
131
+
132
+ if isinstance(value, float):
133
+ raise ValueError(f'{self.name} size cannot be a float value')
134
+
135
+ value = int(value)
136
+ def_value = self.meta.get('default', 0)
137
+
138
+ if value < 0:
139
+ raise ValueError('Dimension size must be a positive integer')
140
+
141
+ if self.is_fixed:
142
+ if 0 < value != def_value:
143
+ raise ValueError(f'{self.name} is a fixed dimension and cannot be changed')
144
+
145
+ self.__size = def_value
146
+ else:
147
+ # The size of a dimension should never be less than the default
148
+ if value < def_value:
149
+ raise ValueError(f'{self.name} size cannot be less than default value ({def_value})')
150
+
151
+ self.__size = max(value, def_value)
152
+
153
+ # TODO: 2023-06-07 PAN - should the metadata size also get changed?
154
+ self.meta['size'] = self.__size
@@ -0,0 +1,256 @@
1
+
2
+ from typing import Any, Dict, Optional, Union # , OrderedDict as OrderedDictType, Union
3
+
4
+ import xml.etree.ElementTree as xmlET
5
+
6
+ from .Dimension import Dimension
7
+ from ..constants import MetaDataType
8
+
9
+
10
+ class Dimensions(object):
11
+ """Container of Dimension objects."""
12
+ __dimensions: Dict[str, Dimension]
13
+
14
+ def __init__(self, metadata: Optional[MetaDataType] = None,
15
+ verbose: Optional[bool] = False,
16
+ strict: Optional[bool] = True):
17
+ """Create dictionary containing Dimension objects.
18
+
19
+ :param verbose: Output additional debug information
20
+ """
21
+ self.__dimensions: Dict[str, Dimension] = {}
22
+ self.__verbose = verbose
23
+ self.__strict = strict
24
+ self.metadata: Union[Dict, None] = None
25
+
26
+ if strict:
27
+ if metadata is None:
28
+ raise ValueError(f'Metadata is required but was not supplied')
29
+ self.metadata = metadata['dimensions']
30
+ else:
31
+ if metadata is None:
32
+ self.metadata = {}
33
+ else:
34
+ # TODO: 20230707 PAN - is adhoc metadata a useful idea?
35
+ self.metadata = metadata
36
+
37
+ # if metadata is not None:
38
+ # self.metadata = metadata['dimensions']
39
+
40
+ # if self.metadata is not None:
41
+ # for cdim, cvals in self.metadata.items():
42
+ # self.add(name=cdim, meta=self.metadata)
43
+ #
44
+
45
+ def __getattr__(self, name: str) -> Any:
46
+ """Get named dimension.
47
+
48
+ :param name: name of dimensions
49
+ :returns: dimension object
50
+ """
51
+
52
+ # print('ATTR: {}'.format(name))
53
+ # https://nedbatchelder.com/blog/201010/surprising_getattr_recursion.html
54
+ if name == "__setstate__":
55
+ raise AttributeError(name)
56
+ return getattr(self.__dimensions, name)
57
+
58
+ def __getitem__(self, item: str) -> Dimension:
59
+ """Get named dimension.
60
+
61
+ :param item: name of dimension
62
+ :returns: Dimension object
63
+ """
64
+ return self.__dimensions[item]
65
+
66
+ def __str__(self) -> str:
67
+ """Pretty-print dimensions.
68
+
69
+ :returns: Pretty-print string of dimensions
70
+ """
71
+
72
+ outstr = ''
73
+ if len(self.__dimensions) == 0:
74
+ outstr = '<empty>'
75
+ else:
76
+ for kk, vv in self.__dimensions.items():
77
+ outstr += f'{vv}\n'
78
+ return outstr
79
+
80
+ @property
81
+ def dimensions(self) -> Dict[str, Dimension]:
82
+ """Get ordered dictionary of Dimension objects.
83
+
84
+ :returns: OrderedDict of Dimension objects
85
+ """
86
+ return self.__dimensions
87
+
88
+ @property
89
+ def ndim(self) -> int:
90
+ """Get number of dimensions.
91
+
92
+ :returns: Number of dimensions
93
+ """
94
+
95
+ return len(self.__dimensions)
96
+
97
+ @property
98
+ def xml(self) -> xmlET.Element:
99
+ """Get xml element for the dimensions.
100
+
101
+ :returns: XML element for the dimensions
102
+ """
103
+
104
+ dims_xml = xmlET.Element('dimensions')
105
+
106
+ for kk, vv in self.dimensions.items():
107
+ dim_sub = xmlET.SubElement(dims_xml, 'dimension')
108
+ dim_sub.set('name', kk)
109
+ xmlET.SubElement(dim_sub, 'size').text = str(vv.size)
110
+ # dim_sub.set('size', str(vv.size))
111
+ return dims_xml
112
+
113
+ def add(self, name: str, size: Optional[int] = None):
114
+ """Add a new Dimension object.
115
+
116
+ :param name: Name of the dimension
117
+ :param size: Size of the dimension
118
+ """
119
+
120
+ # This method adds a dimension if it doesn't exist
121
+ # Duplicate dimension names are silently ignored
122
+ if name not in self.__dimensions:
123
+ self.__dimensions[name] = Dimension(name=name, meta=self.metadata,
124
+ size=size,
125
+ strict=self.__strict)
126
+ # else:
127
+ # # TODO: Should this raise an error?
128
+ # print('Dimension {} already exists...skipping add name'.format(name))
129
+
130
+ def exists(self, name: str) -> bool:
131
+ """Check if dimension exists.
132
+
133
+ :param name: Name of the dimension
134
+ :returns: True if dimension exists, otherwise False
135
+ """
136
+
137
+ return name in self.dimensions.keys()
138
+
139
+ def get(self, name: str) -> Dimension:
140
+ """Get dimension.
141
+
142
+ :param name: Name of the dimension
143
+
144
+ :returns: Dimension object
145
+
146
+ :raises ValueError: if dimension does not exist
147
+ """
148
+
149
+ if self.exists(name):
150
+ return self.__dimensions[name]
151
+ else:
152
+ raise ValueError(f'Dimension, {name}, does not exist.')
153
+
154
+ def remove(self, name: str):
155
+ """Remove Dimension object.
156
+
157
+ :param name: name of dimension to remove
158
+ """
159
+
160
+ if self.exists(name):
161
+ del self.__dimensions[name]
162
+
163
+ def tostructure(self) -> Dict[str, Dict[str, int]]:
164
+ """Get data structure of Dimensions data for serialization.
165
+
166
+ :returns: dictionary of dimension names and sizes
167
+ """
168
+
169
+ dims = {}
170
+ for kk, vv in self.dimensions.items():
171
+ dims[kk] = {'size': vv.size}
172
+ return dims
173
+
174
+
175
+ class ParamDimensions(Dimensions):
176
+ """Container for parameter dimensions.
177
+
178
+ This object adds tracking of dimension position and restricts the total number
179
+ of individual dimensions to 2.
180
+ """
181
+
182
+ def __init__(self, metadata: Optional[MetaDataType] = None,
183
+ verbose: Optional[bool] = False,
184
+ strict: Optional[bool] = True):
185
+
186
+ super(ParamDimensions, self).__init__(metadata=metadata, verbose=verbose, strict=strict)
187
+
188
+ @property
189
+ def xml(self) -> xmlET.Element:
190
+ """Get xml for the dimensions.
191
+
192
+ :returns: XML element of the dimensions
193
+ """
194
+
195
+ # <dimensions>
196
+ # <dimension name = "nsegment" position = "1" size = "1434" />
197
+ # </ dimensions>
198
+ dims_xml = xmlET.Element('dimensions')
199
+
200
+ for kk, vv in self.dimensions.items():
201
+ dim_sub = xmlET.SubElement(dims_xml, 'dimension')
202
+ dim_sub.set('name', kk)
203
+ xmlET.SubElement(dim_sub, 'position').text = str(self.get_position(kk)+1)
204
+ xmlET.SubElement(dim_sub, 'size').text = str(vv.size)
205
+
206
+ # dim_sub.set('position', str(self.get_position(kk)+1))
207
+ # dim_sub.set('size', str(vv.size))
208
+ return dims_xml
209
+
210
+ def add(self, name: str, size: Optional[int] = None):
211
+ """Add a new Dimension object.
212
+
213
+ :param name: Name of the dimension
214
+ :param size: Size of the dimension
215
+ """
216
+
217
+ if self.ndim == 2:
218
+ raise ValueError('A parameter cannot have more than two dimensions.')
219
+
220
+ # Restrict number of dimensions for parameters
221
+ super().add(name, size)
222
+
223
+ def get_dimsize_by_index(self, index: int) -> int:
224
+ """Return size of dimension at the given index.
225
+
226
+ :param index: The 0-based position of the dimension
227
+ :returns: Size of the dimension
228
+
229
+ :raises ValueError: if index is greater than number dimensions for the parameter
230
+ """
231
+
232
+ if index < len(self.dimensions.items()):
233
+ return list(self.dimensions.items())[index][1].size
234
+ raise IndexError(f'Parameter has no dimension at index {index}')
235
+
236
+ def get_position(self, name: str) -> int:
237
+ """Get 0-based index position of a dimension.
238
+
239
+ :param name: name of the dimension
240
+
241
+ :returns: Zero-based Index position of dimension
242
+ """
243
+
244
+ # TODO: method name should be index() ??
245
+ return list(self.dimensions.keys()).index(name)
246
+
247
+ def tostructure(self) -> Dict[str, Dict[str, int]]:
248
+ """Get dictionary structure of the dimensions.
249
+
250
+ :returns: dictionary of Dimensions names, sizes, and positions
251
+ """
252
+
253
+ ldims = super(ParamDimensions, self).tostructure()
254
+ for kk, vv in ldims.items():
255
+ vv['position'] = self.get_position(kk)
256
+ return ldims
@@ -0,0 +1,2 @@
1
+ from .Dimension import Dimension
2
+ from .Dimensions import Dimensions, ParamDimensions