swmm-pandas 0.6.0__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.
- swmm/pandas/__init__.py +7 -0
- swmm/pandas/constants.py +37 -0
- swmm/pandas/input/README.md +61 -0
- swmm/pandas/input/__init__.py +2 -0
- swmm/pandas/input/_section_classes.py +2309 -0
- swmm/pandas/input/input.py +888 -0
- swmm/pandas/input/model.py +403 -0
- swmm/pandas/output/__init__.py +2 -0
- swmm/pandas/output/output.py +2580 -0
- swmm/pandas/output/structure.py +317 -0
- swmm/pandas/output/tools.py +32 -0
- swmm/pandas/py.typed +0 -0
- swmm/pandas/report/__init__.py +1 -0
- swmm/pandas/report/report.py +773 -0
- swmm_pandas-0.6.0.dist-info/METADATA +71 -0
- swmm_pandas-0.6.0.dist-info/RECORD +19 -0
- swmm_pandas-0.6.0.dist-info/WHEEL +4 -0
- swmm_pandas-0.6.0.dist-info/entry_points.txt +4 -0
- swmm_pandas-0.6.0.dist-info/licenses/LICENSE.md +157 -0
|
@@ -0,0 +1,2309 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from numbers import Number
|
|
5
|
+
import warnings
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
from calendar import month_abbr
|
|
10
|
+
import re
|
|
11
|
+
import textwrap
|
|
12
|
+
import copy
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from pandas._libs.missing import NAType
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from typing import Self, TypeGuard
|
|
19
|
+
from collections.abc import Iterable, Iterator
|
|
20
|
+
|
|
21
|
+
TRow = list[str | float | int | pd.Timestamp | pd.Timedelta | NAType]
|
|
22
|
+
|
|
23
|
+
_logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class classproperty:
|
|
27
|
+
def __init__(self, f):
|
|
28
|
+
self.f = f
|
|
29
|
+
|
|
30
|
+
def __get__(self, obj, owner):
|
|
31
|
+
return self.f(owner)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# class ClassPropertyDescriptor(object):
|
|
35
|
+
|
|
36
|
+
# def __init__(self, fget, fset=None):
|
|
37
|
+
# self.fget = fget
|
|
38
|
+
# self.fset = fset
|
|
39
|
+
|
|
40
|
+
# def __get__(self, obj, klass=None):
|
|
41
|
+
# if klass is None:
|
|
42
|
+
# klass = type(obj)
|
|
43
|
+
# return self.fget.__get__(obj, klass)()
|
|
44
|
+
|
|
45
|
+
# def __set__(self, obj, value):
|
|
46
|
+
# if not self.fset:
|
|
47
|
+
# raise AttributeError("can't set attribute")
|
|
48
|
+
# type_ = type(obj)
|
|
49
|
+
# return self.fset.__get__(obj, type_)(value)
|
|
50
|
+
|
|
51
|
+
# def setter(self, func):
|
|
52
|
+
# if not isinstance(func, (classmethod, staticmethod)):
|
|
53
|
+
# func = classmethod(func)
|
|
54
|
+
# self.fset = func
|
|
55
|
+
# return self
|
|
56
|
+
|
|
57
|
+
# def classproperty(func):
|
|
58
|
+
# if not isinstance(func, (classmethod, staticmethod)):
|
|
59
|
+
# func = classmethod(func)
|
|
60
|
+
|
|
61
|
+
# return ClassPropertyDescriptor(func)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _coerce_numeric(data: str) -> str | float | int:
|
|
65
|
+
try:
|
|
66
|
+
number = float(data)
|
|
67
|
+
number = int(number) if number.is_integer() and "." not in data else number
|
|
68
|
+
return number
|
|
69
|
+
# if str(number) == data:
|
|
70
|
+
# return number
|
|
71
|
+
except ValueError:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
return data
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _strip_comment(line: str) -> tuple[str, str]:
|
|
78
|
+
"""
|
|
79
|
+
Splits a line into its data and comments
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
Examples
|
|
83
|
+
--------
|
|
84
|
+
>>> _strip_comment(" JUNC1 1.5 10.25 0 0 5000 ; This is my fav junction ")
|
|
85
|
+
["JUNC1 1.5 10.25 0 0 5000 ", "This is my fav junction"]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
return line[: line.index(";")].strip(), line[line.index(";") + 1 :].strip()
|
|
91
|
+
|
|
92
|
+
except ValueError:
|
|
93
|
+
return line, ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _is_line_comment(line: str) -> bool:
|
|
97
|
+
"""Determines if a line in the inp file is a comment line"""
|
|
98
|
+
try:
|
|
99
|
+
return line.strip()[0] == ";"
|
|
100
|
+
except IndexError:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_data(line: str):
|
|
105
|
+
"""
|
|
106
|
+
Determines if an inp file line has data by checking if the line
|
|
107
|
+
is a table header (starting with `;;`) or a section header (starting with a `[`)
|
|
108
|
+
"""
|
|
109
|
+
if len(line) == 0 or line.strip()[0:2] == ";;" or line.strip()[0] == "[":
|
|
110
|
+
return False
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def comment_formatter(line: str):
|
|
115
|
+
if len(line) > 0:
|
|
116
|
+
line = ";" + line.strip().strip("\n").strip()
|
|
117
|
+
line = line.replace("\n", "\n;") + "\n"
|
|
118
|
+
return line
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class SectionSeries(pd.Series):
|
|
122
|
+
@property
|
|
123
|
+
def _constructor(self):
|
|
124
|
+
return SectionSeries
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def _constructor_expanddim(self):
|
|
128
|
+
return SectionDf
|
|
129
|
+
|
|
130
|
+
# def _constructor_from_mgr(self, mgr, axes) -> Self:
|
|
131
|
+
# # required override for pandas
|
|
132
|
+
# return self.__class__._from_mgr(mgr, axes)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class SectionBase(ABC):
|
|
136
|
+
|
|
137
|
+
_section_name: str
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def from_section_text(cls, text: str, *args, **kwargs) -> Self: ...
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
@abstractmethod
|
|
145
|
+
def _from_section_text(cls, text: str, *args, **kwargs) -> Self: ...
|
|
146
|
+
@classmethod
|
|
147
|
+
@abstractmethod
|
|
148
|
+
def _new_empty(cls) -> Self: ...
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
@abstractmethod
|
|
152
|
+
def _newobj(cls, *args, **kwargs) -> Self: ...
|
|
153
|
+
|
|
154
|
+
@abstractmethod
|
|
155
|
+
def to_swmm_string(self) -> str: ...
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class SectionText(SectionBase, str):
|
|
159
|
+
@classmethod
|
|
160
|
+
def from_section_text(cls, text: str) -> Self:
|
|
161
|
+
"""Construct an instance of the class from the section inp text"""
|
|
162
|
+
return cls._from_section_text(text)
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def _from_section_text(cls, text: str) -> Self:
|
|
166
|
+
return cls(text)
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def _new_empty(cls) -> Self:
|
|
170
|
+
return cls("")
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def _newobj(cls, *args, **kwargs) -> Self:
|
|
174
|
+
return cls(*args, **kwargs)
|
|
175
|
+
|
|
176
|
+
def to_swmm_string(self) -> str:
|
|
177
|
+
return ";;Project Title/Notes\n" + self
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SectionDf(SectionBase, pd.DataFrame):
|
|
181
|
+
_metadata = ["_ncol", "_headings", "headings"]
|
|
182
|
+
_ncol: int = 0
|
|
183
|
+
_headings: list[str] = []
|
|
184
|
+
_index_col: list[str] | str | None = None
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def _data_cols(cls, desc: bool = True) -> list[str]:
|
|
188
|
+
if isinstance(cls._index_col, str):
|
|
189
|
+
idx = [copy.deepcopy(cls._index_col)]
|
|
190
|
+
else:
|
|
191
|
+
idx = copy.deepcopy(cls._index_col)
|
|
192
|
+
|
|
193
|
+
if not desc:
|
|
194
|
+
idx.append("desc")
|
|
195
|
+
|
|
196
|
+
return [col for col in cls.headings if col not in idx]
|
|
197
|
+
|
|
198
|
+
@classproperty
|
|
199
|
+
def headings(cls) -> list[str]:
|
|
200
|
+
return (
|
|
201
|
+
cls._headings
|
|
202
|
+
+ [f"param{i+1}" for i in range(cls._ncol - len(cls._headings))]
|
|
203
|
+
+ ["desc"]
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def __init__(self, *args, **kwargs):
|
|
207
|
+
super().__init__(*args, **kwargs)
|
|
208
|
+
# self._validate_headings()
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def from_section_text(cls, text: str) -> Self:
|
|
212
|
+
"""Construct an instance of the class from the section inp text"""
|
|
213
|
+
raise NotImplementedError
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def _from_section_text(cls, text: str, ncols: int) -> Self:
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
Parse the SWMM section t ext into a dataframe
|
|
220
|
+
|
|
221
|
+
This is a generic parser that assumes the SWMM section is tabular with the each row
|
|
222
|
+
having the same number of tokens (i.e. columns). Comments preceeding a row in the inp file
|
|
223
|
+
are added to the dataframe in a comments column.
|
|
224
|
+
|
|
225
|
+
"""
|
|
226
|
+
rows = text.split("\n")
|
|
227
|
+
data: list[TRow] = []
|
|
228
|
+
line_comment = ""
|
|
229
|
+
for row in rows:
|
|
230
|
+
# check if row contains data
|
|
231
|
+
if not _is_data(row):
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
elif _is_line_comment(row):
|
|
235
|
+
line_comment += _strip_comment(row)[1] + "\n"
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
line, comment = _strip_comment(row)
|
|
239
|
+
if len(comment) > 0:
|
|
240
|
+
line_comment += comment + "\n"
|
|
241
|
+
|
|
242
|
+
# split row into tokens coercing numerics into floats
|
|
243
|
+
split_data = [_coerce_numeric(val) for val in line.split()]
|
|
244
|
+
|
|
245
|
+
# parse tokenzied data into uniform tabular shape so each
|
|
246
|
+
# row has the same number of columns
|
|
247
|
+
table_data = cls._tabulate(split_data)
|
|
248
|
+
|
|
249
|
+
data += cls._get_rows(
|
|
250
|
+
table_data=table_data,
|
|
251
|
+
ncols=ncols,
|
|
252
|
+
line_comment=line_comment,
|
|
253
|
+
)
|
|
254
|
+
line_comment = ""
|
|
255
|
+
|
|
256
|
+
# instantiate DataFrame
|
|
257
|
+
df = cls(data=data, columns=cls.headings, dtype=object)
|
|
258
|
+
return cls(df.set_index(cls._index_col)) if cls._index_col else df
|
|
259
|
+
|
|
260
|
+
# if cls._index_col is not None:
|
|
261
|
+
# df.set_index(cls._index_col)
|
|
262
|
+
# return df
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def _is_nested_list(l: TRow | list[TRow]) -> TypeGuard[list[TRow]]:
|
|
266
|
+
return isinstance(l[0], list)
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def _is_not_nested_list(l: TRow | list[TRow]) -> TypeGuard[TRow]:
|
|
270
|
+
return not isinstance(l[0], list)
|
|
271
|
+
|
|
272
|
+
@classmethod
|
|
273
|
+
def _get_rows(
|
|
274
|
+
cls,
|
|
275
|
+
table_data: TRow | list[TRow],
|
|
276
|
+
ncols: int,
|
|
277
|
+
line_comment: str,
|
|
278
|
+
) -> list[TRow]:
|
|
279
|
+
|
|
280
|
+
_table_data: list[TRow]
|
|
281
|
+
if cls._is_nested_list(table_data):
|
|
282
|
+
_table_data = table_data
|
|
283
|
+
elif cls._is_not_nested_list(table_data):
|
|
284
|
+
_table_data = [table_data]
|
|
285
|
+
else:
|
|
286
|
+
raise Exception(f"Error parsing row {table_data}")
|
|
287
|
+
|
|
288
|
+
rows: list[TRow] = []
|
|
289
|
+
for row in _table_data:
|
|
290
|
+
# create and empty row
|
|
291
|
+
row_data: TRow = [""] * (ncols + 1)
|
|
292
|
+
# assign data to row
|
|
293
|
+
row_data[:ncols] = row
|
|
294
|
+
# add comments to last column
|
|
295
|
+
row_data[-1] = line_comment.strip("\n")
|
|
296
|
+
rows.append(row_data)
|
|
297
|
+
return rows
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def _tabulate(cls, line: list[str | float | int]) -> TRow | list[TRow]:
|
|
301
|
+
"""
|
|
302
|
+
Function to convert tokenized data into a table row with an expected number of columns
|
|
303
|
+
|
|
304
|
+
This function allows the parser to accomodate lines in a SWWM section that might have
|
|
305
|
+
different numbers of tokens.
|
|
306
|
+
|
|
307
|
+
This is the generic version of the method that assumes all tokens in the line
|
|
308
|
+
are assign the front of the table row and any left over spaces in the row are left
|
|
309
|
+
blank. Various sections require custom implementations of thie method.
|
|
310
|
+
|
|
311
|
+
"""
|
|
312
|
+
out: TRow = [""] * cls._ncol
|
|
313
|
+
out[: len(line)] = line
|
|
314
|
+
return out
|
|
315
|
+
|
|
316
|
+
@classmethod
|
|
317
|
+
def _new_empty(cls) -> Self:
|
|
318
|
+
"""Construct and empty instance"""
|
|
319
|
+
df = cls(data=[], columns=cls.headings)
|
|
320
|
+
return df.set_index(cls._index_col) if cls._index_col else df
|
|
321
|
+
|
|
322
|
+
@classmethod
|
|
323
|
+
def _newobj(cls, *args, **kwargs) -> Self:
|
|
324
|
+
df = cls(*args, **kwargs)
|
|
325
|
+
return df
|
|
326
|
+
|
|
327
|
+
def _validate_headings(self) -> None:
|
|
328
|
+
missing = []
|
|
329
|
+
for heading in self.headings:
|
|
330
|
+
if heading not in self.reset_index().columns:
|
|
331
|
+
missing.append(heading)
|
|
332
|
+
if len(missing) > 0:
|
|
333
|
+
# print('cols: ',self.columns)
|
|
334
|
+
raise ValueError(
|
|
335
|
+
f"{self.__class__.__name__} section is missing columns {missing}"
|
|
336
|
+
)
|
|
337
|
+
# self.reindex(self.headings,inplace=True)
|
|
338
|
+
|
|
339
|
+
def add_element(self, **kwargs) -> Self:
|
|
340
|
+
# Create a new row with NaN values for all columns
|
|
341
|
+
headings = self.headings.copy()
|
|
342
|
+
idx_name: str | tuple[str, ...]
|
|
343
|
+
try:
|
|
344
|
+
if isinstance(self._index_col, str):
|
|
345
|
+
idx_name = self._index_col
|
|
346
|
+
idx = kwargs[idx_name]
|
|
347
|
+
headings.remove(idx_name)
|
|
348
|
+
kwargs.pop(idx_name)
|
|
349
|
+
|
|
350
|
+
elif isinstance(self._index_col, (list, tuple)):
|
|
351
|
+
idx_name = tuple(self._index_col)
|
|
352
|
+
idx = []
|
|
353
|
+
for col in idx_name:
|
|
354
|
+
idx.append(kwargs[col])
|
|
355
|
+
headings.remove(col)
|
|
356
|
+
kwargs.pop(col)
|
|
357
|
+
idx = tuple(idx)
|
|
358
|
+
|
|
359
|
+
except KeyError:
|
|
360
|
+
raise KeyError(
|
|
361
|
+
f"Missing index column {self._index_col!r} in provided values. Please provide a value for {self._index_col!r}"
|
|
362
|
+
)
|
|
363
|
+
new_row = pd.Series(index=headings, name=idx, dtype=object)
|
|
364
|
+
|
|
365
|
+
# Update the new row with provided values
|
|
366
|
+
for col, value in kwargs.items():
|
|
367
|
+
if col in headings:
|
|
368
|
+
new_row.loc[col] = value
|
|
369
|
+
else:
|
|
370
|
+
print(
|
|
371
|
+
f"Warning: Column '{col}' not found in the DataFrame. Skipping this value."
|
|
372
|
+
)
|
|
373
|
+
# Append the new row to the DataFrame
|
|
374
|
+
self.loc[idx, :] = new_row
|
|
375
|
+
return self
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def _constructor(self):
|
|
379
|
+
# required override for pandas
|
|
380
|
+
# https://pandas.pydata.org/docs/development/extending.html#override-constructor-properties
|
|
381
|
+
return self.__class__
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def _constructor_sliced(self):
|
|
385
|
+
# required override for pandas
|
|
386
|
+
# https://pandas.pydata.org/docs/development/extending.html#override-constructor-properties
|
|
387
|
+
return SectionSeries
|
|
388
|
+
|
|
389
|
+
def _constructor_from_mgr(self, mgr, axes) -> Self:
|
|
390
|
+
# required override for pandas
|
|
391
|
+
return self.__class__._from_mgr(mgr, axes)
|
|
392
|
+
|
|
393
|
+
def _constructor_sliced_from_mgr(self, mgr, axes) -> SectionSeries:
|
|
394
|
+
# required override for pandas
|
|
395
|
+
return SectionSeries._from_mgr(mgr, axes)
|
|
396
|
+
|
|
397
|
+
def to_swmm_string(self) -> str:
|
|
398
|
+
"""Create a string representation of section"""
|
|
399
|
+
self._validate_headings()
|
|
400
|
+
# reset index
|
|
401
|
+
out_df = (
|
|
402
|
+
self.reset_index(self._index_col)
|
|
403
|
+
.reindex(self.headings, axis=1)
|
|
404
|
+
.infer_objects(copy=False)
|
|
405
|
+
.fillna("")
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# determine the longest variable in each column of the table
|
|
409
|
+
# used to figure out how wide to make the columns
|
|
410
|
+
max_data = (
|
|
411
|
+
out_df.astype(str)
|
|
412
|
+
.map(
|
|
413
|
+
len,
|
|
414
|
+
)
|
|
415
|
+
.max()
|
|
416
|
+
)
|
|
417
|
+
# determine the length of the header names
|
|
418
|
+
max_header = out_df.columns.to_series().apply(len)
|
|
419
|
+
|
|
420
|
+
max_header.iloc[
|
|
421
|
+
0
|
|
422
|
+
] += 2 # add 2 to first header to account for comment formatting
|
|
423
|
+
|
|
424
|
+
# determine the column widths by finding the max legnth out of data
|
|
425
|
+
# and headers
|
|
426
|
+
col_widths = pd.concat([max_header, max_data], axis=1).max(axis=1) + 2
|
|
427
|
+
|
|
428
|
+
# create format strings for header, divider, and data
|
|
429
|
+
header_format = ""
|
|
430
|
+
header_divider = ""
|
|
431
|
+
data_format = ""
|
|
432
|
+
for i, col in enumerate(col_widths.drop("desc")):
|
|
433
|
+
data_format += f"{{:<{col}}}"
|
|
434
|
+
header_format += f";;{{:<{col-2}}}" if i == 0 else f"{{:<{col}}}"
|
|
435
|
+
header_divider += f";;{'-'*(col-4)} " if i == 0 else f"{'-'*(col-2)} "
|
|
436
|
+
data_format += "\n"
|
|
437
|
+
header_format += "\n"
|
|
438
|
+
header_divider += "\n"
|
|
439
|
+
|
|
440
|
+
# loop over data and format each each row of data as a string
|
|
441
|
+
outstr = ""
|
|
442
|
+
for i, row in enumerate(out_df.drop("desc", axis=1).values):
|
|
443
|
+
desc = out_df.loc[i, "desc"]
|
|
444
|
+
if (not pd.isna(desc)) and (len(strdesc := str(desc)) > 0):
|
|
445
|
+
outstr += comment_formatter(strdesc)
|
|
446
|
+
outstr += data_format.format(*row)
|
|
447
|
+
|
|
448
|
+
header = header_format.format(*out_df.drop("desc", axis=1).columns)
|
|
449
|
+
# concatenate the header, divider, and data
|
|
450
|
+
return header + header_divider + outstr
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class Title(SectionText):
|
|
454
|
+
_section_name = "TITLE"
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class Option(SectionDf):
|
|
458
|
+
"""
|
|
459
|
+
Index: Option
|
|
460
|
+
Columns: Value
|
|
461
|
+
"""
|
|
462
|
+
|
|
463
|
+
_section_name = "OPTIONS"
|
|
464
|
+
_ncol = 2
|
|
465
|
+
_headings = ["Option", "Value"]
|
|
466
|
+
_index_col = "Option"
|
|
467
|
+
|
|
468
|
+
@classmethod
|
|
469
|
+
def from_section_text(cls, text: str):
|
|
470
|
+
return super()._from_section_text(text, cls._ncol)
|
|
471
|
+
|
|
472
|
+
def _ipython_key_completions_(self):
|
|
473
|
+
return list(["Value"])
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class Report(SectionBase):
|
|
477
|
+
_section_name = "REPORT"
|
|
478
|
+
|
|
479
|
+
@dataclass
|
|
480
|
+
class LIDReportEntry:
|
|
481
|
+
Name: str
|
|
482
|
+
Subcatch: str
|
|
483
|
+
Fname: str
|
|
484
|
+
|
|
485
|
+
class LIDReport(list[LIDReportEntry]):
|
|
486
|
+
def __init__(self, entries: Iterable[Report.LIDReportEntry]):
|
|
487
|
+
for i in entries:
|
|
488
|
+
if not isinstance(i, Report.LIDReportEntry):
|
|
489
|
+
raise ValueError(
|
|
490
|
+
f"LIDReport is instantiated with a sequence of LIDReportEntries, got {type(i)}",
|
|
491
|
+
)
|
|
492
|
+
super().__init__(entries)
|
|
493
|
+
|
|
494
|
+
def add(self, lid_name: str, subcatch: str, Fname: str) -> None:
|
|
495
|
+
self.append(
|
|
496
|
+
Report.LIDReportEntry(
|
|
497
|
+
Name=lid_name,
|
|
498
|
+
Subcatch=subcatch,
|
|
499
|
+
Fname=Fname,
|
|
500
|
+
),
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
def delete(self, lid_name: str) -> None:
|
|
504
|
+
for i, v in enumerate(self):
|
|
505
|
+
if v.Name == lid_name:
|
|
506
|
+
break
|
|
507
|
+
self.pop(i)
|
|
508
|
+
|
|
509
|
+
def __repr__(self):
|
|
510
|
+
rep = "LIDReportList(\n"
|
|
511
|
+
for lid in self:
|
|
512
|
+
rep += f" {lid}.__repr__()\n"
|
|
513
|
+
return f"{rep})"
|
|
514
|
+
|
|
515
|
+
def __init__(
|
|
516
|
+
self,
|
|
517
|
+
disabled: str | None = None,
|
|
518
|
+
input: str | None = None,
|
|
519
|
+
continuity: str | None = None,
|
|
520
|
+
flowstats: str | None = None,
|
|
521
|
+
controls: str | None = None,
|
|
522
|
+
averages: str | None = None,
|
|
523
|
+
subcatchments: list[str] = [],
|
|
524
|
+
nodes: list[str] = [],
|
|
525
|
+
links: list[str] = [],
|
|
526
|
+
lids: list[dict] = [],
|
|
527
|
+
):
|
|
528
|
+
self.DISABLED = disabled
|
|
529
|
+
self.INPUT = input
|
|
530
|
+
self.CONTINUITY = continuity
|
|
531
|
+
self.FLOWSTATS = flowstats
|
|
532
|
+
self.CONTROLS = controls
|
|
533
|
+
self.AVERAGES = averages
|
|
534
|
+
self.SUBCATCHMENTS = subcatchments
|
|
535
|
+
self.NODES = nodes
|
|
536
|
+
self.LINKS = links
|
|
537
|
+
self.LID = self.LIDReport([])
|
|
538
|
+
|
|
539
|
+
for lid in lids:
|
|
540
|
+
self.LID.add(lid["name"], lid["subcatch"], lid["fname"])
|
|
541
|
+
|
|
542
|
+
@classmethod
|
|
543
|
+
def from_section_text(cls, text: str, *args, **kwargs) -> Self:
|
|
544
|
+
rows = text.split("\n")
|
|
545
|
+
|
|
546
|
+
obj = cls()
|
|
547
|
+
|
|
548
|
+
for row in rows:
|
|
549
|
+
# check if row contains data
|
|
550
|
+
if not _is_data(row):
|
|
551
|
+
continue
|
|
552
|
+
|
|
553
|
+
if ";" in row:
|
|
554
|
+
warnings.warn(
|
|
555
|
+
"swmm.pandas does not currently support comments in the [REPORT] section. Truncating...",
|
|
556
|
+
)
|
|
557
|
+
if _is_line_comment(row):
|
|
558
|
+
continue
|
|
559
|
+
|
|
560
|
+
tokens = row.split()
|
|
561
|
+
report_type = tokens[0].upper()
|
|
562
|
+
if not hasattr(obj, report_type):
|
|
563
|
+
warnings.warn(
|
|
564
|
+
f"{report_type} is not a supported report type, skipping..."
|
|
565
|
+
)
|
|
566
|
+
continue
|
|
567
|
+
elif report_type in ("SUBCATCHMENTS", "NODES", "LINKS"):
|
|
568
|
+
setattr(
|
|
569
|
+
obj,
|
|
570
|
+
report_type,
|
|
571
|
+
getattr(obj, report_type) + tokens[1:],
|
|
572
|
+
)
|
|
573
|
+
elif report_type == "LID":
|
|
574
|
+
obj.LID.add(
|
|
575
|
+
lid_name=tokens[1],
|
|
576
|
+
subcatch=tokens[2],
|
|
577
|
+
Fname=tokens[3],
|
|
578
|
+
)
|
|
579
|
+
else:
|
|
580
|
+
setattr(obj, report_type, tokens[1])
|
|
581
|
+
|
|
582
|
+
return obj
|
|
583
|
+
|
|
584
|
+
@classmethod
|
|
585
|
+
def _from_section_text(cls, text: str, *args, **kwargs) -> Self:
|
|
586
|
+
raise NotImplementedError
|
|
587
|
+
|
|
588
|
+
@classmethod
|
|
589
|
+
def _new_empty(cls) -> Self:
|
|
590
|
+
return cls()
|
|
591
|
+
|
|
592
|
+
@classmethod
|
|
593
|
+
def _newobj(cls, *args, **kwargs) -> Self:
|
|
594
|
+
return cls(*args, **kwargs)
|
|
595
|
+
|
|
596
|
+
def to_swmm_string(self) -> str:
|
|
597
|
+
return ";;Reporting Options\n" + self.__repr__()
|
|
598
|
+
|
|
599
|
+
def __repr__(self) -> str:
|
|
600
|
+
out_str = ""
|
|
601
|
+
for switch in (
|
|
602
|
+
"DISABLED",
|
|
603
|
+
"INPUT",
|
|
604
|
+
"CONTINUITY",
|
|
605
|
+
"FLOWSTATS",
|
|
606
|
+
"CONTROLS",
|
|
607
|
+
"AVERAGES",
|
|
608
|
+
):
|
|
609
|
+
if (value := getattr(self, switch)) is not None:
|
|
610
|
+
out_str += f"{switch} {value}\n"
|
|
611
|
+
|
|
612
|
+
for seq in ("SUBCATCHMENTS", "NODES", "LINKS"):
|
|
613
|
+
if len(items := getattr(self, seq)) > 0:
|
|
614
|
+
i = 0
|
|
615
|
+
while i < len(items):
|
|
616
|
+
out_str += f"{seq} {' '.join(items[i:i+5])}\n"
|
|
617
|
+
i += 5
|
|
618
|
+
if len(self.LID) > 0:
|
|
619
|
+
for lid in self.LID:
|
|
620
|
+
out_str += f"LID {lid.Name} {lid.Subcatch} {lid.Fname}\n"
|
|
621
|
+
|
|
622
|
+
return out_str
|
|
623
|
+
|
|
624
|
+
def __len__(self):
|
|
625
|
+
length = 0
|
|
626
|
+
for switch in ("DISABLED", "INPUT", "CONTINUITY", "FLOWSTATS", "CONTROLS"):
|
|
627
|
+
if getattr(self, switch) is not None:
|
|
628
|
+
length += 1
|
|
629
|
+
|
|
630
|
+
for seq in ("SUBCATCHMENTS", "NODES", "LINKS"):
|
|
631
|
+
length += len(getattr(self, seq))
|
|
632
|
+
|
|
633
|
+
length += len(self.LID)
|
|
634
|
+
return length
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
class Files(SectionText):
|
|
638
|
+
_section_name = "FILES"
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class Event(SectionDf):
|
|
642
|
+
_section_name = "EVENT"
|
|
643
|
+
_ncol = 2
|
|
644
|
+
_headings = ["Start", "End"]
|
|
645
|
+
|
|
646
|
+
@classmethod
|
|
647
|
+
def _tabulate(cls, line: list[str | float | int]) -> TRow | list[TRow]:
|
|
648
|
+
out: TRow = [""] * cls._ncol
|
|
649
|
+
if len(line) != 4:
|
|
650
|
+
raise ValueError(f"Event lines must have 4 values but found {len(line)}")
|
|
651
|
+
|
|
652
|
+
start_time = " ".join(line[:2]) # type: ignore
|
|
653
|
+
end_time = " ".join(line[2:]) # type: ignore
|
|
654
|
+
|
|
655
|
+
try:
|
|
656
|
+
out[0] = pd.to_datetime(start_time)
|
|
657
|
+
out[1] = pd.to_datetime(end_time)
|
|
658
|
+
return out
|
|
659
|
+
except Exception as e:
|
|
660
|
+
print(f"Error parsing event dates: {start_time} or {end_time}")
|
|
661
|
+
raise e
|
|
662
|
+
|
|
663
|
+
@classmethod
|
|
664
|
+
def from_section_text(cls, text: str):
|
|
665
|
+
return super()._from_section_text(text, cls._ncol)
|
|
666
|
+
|
|
667
|
+
def to_swmm_string(self) -> str:
|
|
668
|
+
df = self.copy()
|
|
669
|
+
|
|
670
|
+
df["Start"] = pd.to_datetime(df["Start"]).dt.strftime("%m/%d/%Y %H:%M")
|
|
671
|
+
df["End"] = pd.to_datetime(df["End"]).dt.strftime("%m/%d/%Y %H:%M")
|
|
672
|
+
return super(Event, df).to_swmm_string()
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
class Raingage(SectionDf):
|
|
676
|
+
_section_name = "RAINGAGES"
|
|
677
|
+
_ncol = 8
|
|
678
|
+
_headings = [
|
|
679
|
+
"Name",
|
|
680
|
+
"Format",
|
|
681
|
+
"Interval",
|
|
682
|
+
"SCF",
|
|
683
|
+
"Source_Type",
|
|
684
|
+
"Source",
|
|
685
|
+
"Station",
|
|
686
|
+
"Units",
|
|
687
|
+
]
|
|
688
|
+
_index_col = "Name"
|
|
689
|
+
|
|
690
|
+
@classmethod
|
|
691
|
+
def from_section_text(cls, text: str):
|
|
692
|
+
return super()._from_section_text(text, cls._ncol)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
class Evap(SectionDf):
|
|
696
|
+
_section_name = "EVAPORATION"
|
|
697
|
+
_ncol = 13
|
|
698
|
+
_headings = ["Type"]
|
|
699
|
+
_index_col = "Type"
|
|
700
|
+
|
|
701
|
+
@classmethod
|
|
702
|
+
def from_section_text(cls, text: str):
|
|
703
|
+
return super()._from_section_text(text, cls._ncol)
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
class Temperature(SectionDf):
|
|
707
|
+
_section_name = "TEMPERATURE"
|
|
708
|
+
_ncol = 14
|
|
709
|
+
_headings = ["Option"]
|
|
710
|
+
_index_col = "Option"
|
|
711
|
+
|
|
712
|
+
@classmethod
|
|
713
|
+
def from_section_text(cls, text: str):
|
|
714
|
+
return super()._from_section_text(text, cls._ncol)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
class Subcatchment(SectionDf):
|
|
718
|
+
_section_name = "SUBCATCHMENTS"
|
|
719
|
+
_ncol = 9
|
|
720
|
+
_headings = [
|
|
721
|
+
"Name",
|
|
722
|
+
"RainGage",
|
|
723
|
+
"Outlet",
|
|
724
|
+
"Area",
|
|
725
|
+
"PctImp",
|
|
726
|
+
"Width",
|
|
727
|
+
"Slope",
|
|
728
|
+
"CurbLeng",
|
|
729
|
+
"SnowPack",
|
|
730
|
+
]
|
|
731
|
+
_index_col = "Name"
|
|
732
|
+
|
|
733
|
+
@classmethod
|
|
734
|
+
def from_section_text(cls, text: str):
|
|
735
|
+
return super()._from_section_text(text, cls._ncol)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
class Subarea(SectionDf):
|
|
739
|
+
_section_name = "SUBAREAS"
|
|
740
|
+
_ncol = 8
|
|
741
|
+
_headings = [
|
|
742
|
+
"Subcatchment",
|
|
743
|
+
"Nimp",
|
|
744
|
+
"Nperv",
|
|
745
|
+
"Simp",
|
|
746
|
+
"Sperv",
|
|
747
|
+
"PctZero",
|
|
748
|
+
"RouteTo",
|
|
749
|
+
"PctRouted",
|
|
750
|
+
]
|
|
751
|
+
_index_col = "Subcatchment"
|
|
752
|
+
|
|
753
|
+
@classmethod
|
|
754
|
+
def from_section_text(cls, text: str):
|
|
755
|
+
return super()._from_section_text(text, cls._ncol)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
class Infil(SectionDf):
|
|
759
|
+
_section_name = "INFILTRATION"
|
|
760
|
+
_ncol = 7
|
|
761
|
+
_headings = [
|
|
762
|
+
"Subcatchment",
|
|
763
|
+
"param1",
|
|
764
|
+
"param2",
|
|
765
|
+
"param3",
|
|
766
|
+
"param4",
|
|
767
|
+
"param5",
|
|
768
|
+
"Method",
|
|
769
|
+
]
|
|
770
|
+
_index_col = "Subcatchment"
|
|
771
|
+
_infiltration_methods = (
|
|
772
|
+
"HORTON",
|
|
773
|
+
"MODIFIED_HORTON",
|
|
774
|
+
"GREEN_AMPT",
|
|
775
|
+
"MODIFIED_GREEN_AMPT",
|
|
776
|
+
"CURVE_NUMBER",
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
@classmethod
|
|
780
|
+
def from_section_text(cls, text: str):
|
|
781
|
+
return super()._from_section_text(text, cls._ncol)
|
|
782
|
+
|
|
783
|
+
@classmethod
|
|
784
|
+
def _tabulate(cls, line: list[str | float | int]) -> TRow | list[TRow]:
|
|
785
|
+
out: TRow = [""] * cls._ncol
|
|
786
|
+
|
|
787
|
+
# pop first entry in the line (subcatch name)
|
|
788
|
+
out[0] = line.pop(0)
|
|
789
|
+
|
|
790
|
+
# add catchment specific method if present
|
|
791
|
+
if line[-1] in cls._infiltration_methods:
|
|
792
|
+
out[cls._headings.index("Method")] = line.pop(-1)
|
|
793
|
+
|
|
794
|
+
# add params
|
|
795
|
+
out[1 : 1 + len(line)] = line
|
|
796
|
+
return out
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
class Aquifer(SectionDf):
|
|
800
|
+
_section_name = "AQUIFERS"
|
|
801
|
+
_ncol = 14
|
|
802
|
+
_headings = [
|
|
803
|
+
"Name",
|
|
804
|
+
"Por",
|
|
805
|
+
"WP",
|
|
806
|
+
"FC",
|
|
807
|
+
"Ksat",
|
|
808
|
+
"Kslope",
|
|
809
|
+
"Tslope",
|
|
810
|
+
"ETu",
|
|
811
|
+
"ETs",
|
|
812
|
+
"Seep",
|
|
813
|
+
"Ebot",
|
|
814
|
+
"Egw",
|
|
815
|
+
"Umc",
|
|
816
|
+
"ETupat",
|
|
817
|
+
]
|
|
818
|
+
_index_col = "Name"
|
|
819
|
+
|
|
820
|
+
@classmethod
|
|
821
|
+
def from_section_text(cls, text: str):
|
|
822
|
+
return super()._from_section_text(text, cls._ncol)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
class Groundwater(SectionDf):
|
|
826
|
+
_section_name = "GROUNDWATER"
|
|
827
|
+
_ncol = 14
|
|
828
|
+
_headings = [
|
|
829
|
+
"Subcatchment",
|
|
830
|
+
"Aquifer",
|
|
831
|
+
"Node",
|
|
832
|
+
"Esurf",
|
|
833
|
+
"A1",
|
|
834
|
+
"B1",
|
|
835
|
+
"A2",
|
|
836
|
+
"B2",
|
|
837
|
+
"A3",
|
|
838
|
+
"Dsw",
|
|
839
|
+
"Egwt",
|
|
840
|
+
"Ebot",
|
|
841
|
+
"Wgr",
|
|
842
|
+
"Umc",
|
|
843
|
+
]
|
|
844
|
+
_index_col = "Subcatchment"
|
|
845
|
+
|
|
846
|
+
@classmethod
|
|
847
|
+
def from_section_text(cls, text: str):
|
|
848
|
+
return super()._from_section_text(text, cls._ncol)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
class GWF(SectionDf):
|
|
852
|
+
_section_name = "GWF"
|
|
853
|
+
_ncol = 3
|
|
854
|
+
_headings = [
|
|
855
|
+
"Subcatch",
|
|
856
|
+
"Type",
|
|
857
|
+
"Expr",
|
|
858
|
+
]
|
|
859
|
+
_index_col = ["Subcatch", "Type"]
|
|
860
|
+
|
|
861
|
+
@classmethod
|
|
862
|
+
def from_section_text(cls, text: str):
|
|
863
|
+
return super()._from_section_text(text, cls._ncol)
|
|
864
|
+
|
|
865
|
+
@classmethod
|
|
866
|
+
def _tabulate(cls, line: list[str | float]) -> TRow | list[TRow]:
|
|
867
|
+
out: TRow = [""] * cls._ncol
|
|
868
|
+
out[0] = line.pop(0)
|
|
869
|
+
out[1] = line.pop(0)
|
|
870
|
+
out[2] = "".join([str(s).strip() for s in line])
|
|
871
|
+
return out
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
class Snowpack(SectionDf):
|
|
875
|
+
_section_name = "SNOWPACKS"
|
|
876
|
+
_ncol = 9
|
|
877
|
+
_headings = ["Name", "Surface"]
|
|
878
|
+
_index_col = ["Name", "Surface"]
|
|
879
|
+
|
|
880
|
+
@classmethod
|
|
881
|
+
def from_section_text(cls, text: str):
|
|
882
|
+
return super()._from_section_text(text, cls._ncol)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
class Junc(SectionDf):
|
|
886
|
+
_section_name = "JUNCTIONS"
|
|
887
|
+
_ncol = 6
|
|
888
|
+
_headings = [
|
|
889
|
+
"Name",
|
|
890
|
+
"Elevation",
|
|
891
|
+
"MaxDepth",
|
|
892
|
+
"InitDepth",
|
|
893
|
+
"SurDepth",
|
|
894
|
+
"Aponded",
|
|
895
|
+
]
|
|
896
|
+
_index_col = "Name"
|
|
897
|
+
|
|
898
|
+
@classmethod
|
|
899
|
+
def from_section_text(cls, text: str):
|
|
900
|
+
return super()._from_section_text(text, cls._ncol)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
class Outfall(SectionDf):
|
|
904
|
+
_section_name = "OUTFALLS"
|
|
905
|
+
_ncol = 6
|
|
906
|
+
_headings = ["Name", "Elevation", "Type", "StageData", "Gated", "RouteTo"]
|
|
907
|
+
_index_col = "Name"
|
|
908
|
+
|
|
909
|
+
@classmethod
|
|
910
|
+
def _tabulate(cls, line: list[str | float | int]) -> TRow | list[TRow]:
|
|
911
|
+
out: TRow = [""] * cls._ncol
|
|
912
|
+
|
|
913
|
+
# pop first three entries in the line
|
|
914
|
+
# (required entries for every outfall type)
|
|
915
|
+
out[:3] = line[:3]
|
|
916
|
+
outfall_type = str(out[2]).lower()
|
|
917
|
+
del line[:3]
|
|
918
|
+
try:
|
|
919
|
+
if outfall_type in ("free", "normal"):
|
|
920
|
+
out[4 : 4 + len(line)] = line
|
|
921
|
+
return out
|
|
922
|
+
else:
|
|
923
|
+
out[3 : 3 + len(line)] = line
|
|
924
|
+
return out
|
|
925
|
+
except Exception as e:
|
|
926
|
+
print("Error parsing Outfall line: {line}")
|
|
927
|
+
raise e
|
|
928
|
+
|
|
929
|
+
@classmethod
|
|
930
|
+
def from_section_text(cls, text: str):
|
|
931
|
+
return super()._from_section_text(text, cls._ncol)
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
class Storage(SectionDf):
|
|
935
|
+
_section_name = "STORAGE"
|
|
936
|
+
_ncol = 14
|
|
937
|
+
_headings = [
|
|
938
|
+
"Name",
|
|
939
|
+
"Elev",
|
|
940
|
+
"MaxDepth",
|
|
941
|
+
"InitDepth",
|
|
942
|
+
"Shape",
|
|
943
|
+
"CurveName",
|
|
944
|
+
"A1_L",
|
|
945
|
+
"A2_W",
|
|
946
|
+
"A0_Z",
|
|
947
|
+
"SurDepth",
|
|
948
|
+
"Fevap",
|
|
949
|
+
"Psi",
|
|
950
|
+
"Ksat",
|
|
951
|
+
"IMD",
|
|
952
|
+
]
|
|
953
|
+
_index_col = "Name"
|
|
954
|
+
|
|
955
|
+
@classmethod
|
|
956
|
+
def _tabulate(cls, line: list[str | float | int]) -> TRow | list[TRow]:
|
|
957
|
+
out: TRow = [""] * cls._ncol
|
|
958
|
+
out[: cls._headings.index("CurveName")] = line[:5]
|
|
959
|
+
line = line[5:]
|
|
960
|
+
shape = str(out[cls._headings.index("Shape")]).lower()
|
|
961
|
+
if shape in ("functional", "cylindrical", "conical", "paraboloid", "pyramidal"):
|
|
962
|
+
out[6 : 6 + len(line)] = line
|
|
963
|
+
return out
|
|
964
|
+
elif shape == "tabular":
|
|
965
|
+
out[cls._headings.index("CurveName")] = line.pop(0)
|
|
966
|
+
out[
|
|
967
|
+
cls._headings.index("SurDepth") : cls._headings.index("SurDepth")
|
|
968
|
+
+ len(line)
|
|
969
|
+
] = line
|
|
970
|
+
return out
|
|
971
|
+
else:
|
|
972
|
+
raise ValueError(f"Unexpected line in storage section ({line})")
|
|
973
|
+
|
|
974
|
+
@classmethod
|
|
975
|
+
def from_section_text(cls, text: str):
|
|
976
|
+
return super()._from_section_text(text, cls._ncol)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
class Divider(SectionDf):
|
|
980
|
+
_section_name = "DIVIDERS"
|
|
981
|
+
_ncol = 12
|
|
982
|
+
_headings = [
|
|
983
|
+
"Name",
|
|
984
|
+
"Elevation",
|
|
985
|
+
"DivLink",
|
|
986
|
+
"DivType",
|
|
987
|
+
"DivCurve",
|
|
988
|
+
"Qmin",
|
|
989
|
+
"Height",
|
|
990
|
+
"Cd",
|
|
991
|
+
"Ymax",
|
|
992
|
+
"Y0",
|
|
993
|
+
"Ysur",
|
|
994
|
+
"Apond",
|
|
995
|
+
]
|
|
996
|
+
_index_col = "Name"
|
|
997
|
+
|
|
998
|
+
@classmethod
|
|
999
|
+
def _tabulate(cls, line: list[str | float | int]) -> TRow | list[TRow]:
|
|
1000
|
+
out: TRow = [""] * cls._ncol
|
|
1001
|
+
|
|
1002
|
+
# pop first four entries in the line
|
|
1003
|
+
# (required entries for every Divider type)
|
|
1004
|
+
out[:4] = line[:4]
|
|
1005
|
+
div_type = str(out[3]).lower()
|
|
1006
|
+
del line[:4]
|
|
1007
|
+
try:
|
|
1008
|
+
if div_type == "overflow":
|
|
1009
|
+
out[8 : 8 + len(line)] = line
|
|
1010
|
+
|
|
1011
|
+
elif div_type == "cutoff":
|
|
1012
|
+
out[5] = line.pop(0)
|
|
1013
|
+
out[8 : 8 + len(line)] = line
|
|
1014
|
+
elif div_type == "tabular":
|
|
1015
|
+
out[4] = line.pop(0)
|
|
1016
|
+
out[8 : 8 + len(line)] = line
|
|
1017
|
+
elif div_type == "weir":
|
|
1018
|
+
out[5 : 5 + len(line)] = line
|
|
1019
|
+
else:
|
|
1020
|
+
raise ValueError(f"Unexpected divider type: {div_type!r}")
|
|
1021
|
+
return out
|
|
1022
|
+
|
|
1023
|
+
except Exception as e:
|
|
1024
|
+
print("Error parsing Divider line: {line!r}")
|
|
1025
|
+
raise e
|
|
1026
|
+
|
|
1027
|
+
@classmethod
|
|
1028
|
+
def from_section_text(cls, text: str):
|
|
1029
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
class Conduit(SectionDf):
|
|
1033
|
+
_section_name = "CONDUITS"
|
|
1034
|
+
_ncol = 9
|
|
1035
|
+
_headings = [
|
|
1036
|
+
"Name",
|
|
1037
|
+
"FromNode",
|
|
1038
|
+
"ToNode",
|
|
1039
|
+
"Length",
|
|
1040
|
+
"Roughness",
|
|
1041
|
+
"InOffset",
|
|
1042
|
+
"OutOffset",
|
|
1043
|
+
"InitFlow",
|
|
1044
|
+
"MaxFlow",
|
|
1045
|
+
]
|
|
1046
|
+
_index_col = "Name"
|
|
1047
|
+
|
|
1048
|
+
@classmethod
|
|
1049
|
+
def from_section_text(cls, text: str):
|
|
1050
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
class Pump(SectionDf):
|
|
1054
|
+
_section_name = "PUMPS"
|
|
1055
|
+
_ncol = 7
|
|
1056
|
+
_headings = [
|
|
1057
|
+
"Name",
|
|
1058
|
+
"FromNode",
|
|
1059
|
+
"ToNode",
|
|
1060
|
+
"PumpCurve",
|
|
1061
|
+
"Status",
|
|
1062
|
+
"Startup",
|
|
1063
|
+
"Shutoff",
|
|
1064
|
+
]
|
|
1065
|
+
_index_col = "Name"
|
|
1066
|
+
|
|
1067
|
+
@classmethod
|
|
1068
|
+
def from_section_text(cls, text: str):
|
|
1069
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
class Orifice(SectionDf):
|
|
1073
|
+
_section_name = "ORIFICES"
|
|
1074
|
+
_ncol = 8
|
|
1075
|
+
_headings = [
|
|
1076
|
+
"Name",
|
|
1077
|
+
"FromNode",
|
|
1078
|
+
"ToNode",
|
|
1079
|
+
"Type",
|
|
1080
|
+
"Offset",
|
|
1081
|
+
"Qcoeff",
|
|
1082
|
+
"Gated",
|
|
1083
|
+
"CloseTime",
|
|
1084
|
+
]
|
|
1085
|
+
_index_col = "Name"
|
|
1086
|
+
|
|
1087
|
+
@classmethod
|
|
1088
|
+
def from_section_text(cls, text: str):
|
|
1089
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
class Weir(SectionDf):
|
|
1093
|
+
_section_name = "WEIRS"
|
|
1094
|
+
_ncol = 13
|
|
1095
|
+
_headings = [
|
|
1096
|
+
"Name",
|
|
1097
|
+
"FromNode",
|
|
1098
|
+
"ToNode",
|
|
1099
|
+
"Type",
|
|
1100
|
+
"CrestHt",
|
|
1101
|
+
"Qcoeff",
|
|
1102
|
+
"Gated",
|
|
1103
|
+
"EndCon",
|
|
1104
|
+
"EndCoeff",
|
|
1105
|
+
"Surcharge",
|
|
1106
|
+
"RoadWidth",
|
|
1107
|
+
"RoadSurf",
|
|
1108
|
+
"CoeffCurve",
|
|
1109
|
+
]
|
|
1110
|
+
_index_col = "Name"
|
|
1111
|
+
|
|
1112
|
+
@classmethod
|
|
1113
|
+
def from_section_text(cls, text: str):
|
|
1114
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
class Outlet(SectionDf):
|
|
1118
|
+
_section_name = "OUTLETS"
|
|
1119
|
+
_ncol = 9
|
|
1120
|
+
_headings = [
|
|
1121
|
+
"Name",
|
|
1122
|
+
"FromNode",
|
|
1123
|
+
"ToNode",
|
|
1124
|
+
"Offset",
|
|
1125
|
+
"Type",
|
|
1126
|
+
"CurveName",
|
|
1127
|
+
"Qcoeff",
|
|
1128
|
+
"Qexpon",
|
|
1129
|
+
"Gated",
|
|
1130
|
+
]
|
|
1131
|
+
_index_col = "Name"
|
|
1132
|
+
|
|
1133
|
+
@classmethod
|
|
1134
|
+
def _tabulate(cls, line: list[str | float | int]) -> TRow | list[TRow]:
|
|
1135
|
+
out: TRow = [""] * cls._ncol
|
|
1136
|
+
out[: cls._headings.index("CurveName")] = line[:5]
|
|
1137
|
+
line = line[5:]
|
|
1138
|
+
|
|
1139
|
+
if "functional" in str(out[cls._headings.index("Type")]).lower():
|
|
1140
|
+
out[6 : 6 + len(line)] = line
|
|
1141
|
+
return out
|
|
1142
|
+
elif "tabular" in str(out[cls._headings.index("Type")]).lower():
|
|
1143
|
+
out[cls._headings.index("CurveName")] = line[0]
|
|
1144
|
+
if len(line) > 1:
|
|
1145
|
+
out[cls._headings.index("Gated")] = line[1]
|
|
1146
|
+
return out
|
|
1147
|
+
else:
|
|
1148
|
+
raise ValueError(f"Unexpected line in outlet section ({line})")
|
|
1149
|
+
|
|
1150
|
+
@classmethod
|
|
1151
|
+
def from_section_text(cls, text: str):
|
|
1152
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
class Xsections(SectionDf):
|
|
1156
|
+
_section_name = "XSECTIONS"
|
|
1157
|
+
_shapes = (
|
|
1158
|
+
"CIRCULAR",
|
|
1159
|
+
"FORCE_MAIN",
|
|
1160
|
+
"FILLED_CIRCULAR",
|
|
1161
|
+
"DUMMY",
|
|
1162
|
+
"RECT_CLOSED",
|
|
1163
|
+
"RECT_OPEN",
|
|
1164
|
+
"TRAPEZOIDAL",
|
|
1165
|
+
"TRIANGULAR",
|
|
1166
|
+
"HORIZ_ELLIPSE",
|
|
1167
|
+
"VERT_ELLIPSE",
|
|
1168
|
+
"ARCH",
|
|
1169
|
+
"PARABOLIC",
|
|
1170
|
+
"POWER",
|
|
1171
|
+
"RECT_TRIANGULAR",
|
|
1172
|
+
"RECT_ROUND",
|
|
1173
|
+
"MODBASKETHANDLE",
|
|
1174
|
+
"EGG",
|
|
1175
|
+
"HORSESHOE",
|
|
1176
|
+
"GOTHIC",
|
|
1177
|
+
"CATENARY",
|
|
1178
|
+
"SEMIELLIPTICAL",
|
|
1179
|
+
"BASKETHANDLE",
|
|
1180
|
+
"SEMICIRCULAR",
|
|
1181
|
+
"CUSTOM",
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
_ncol = 9
|
|
1185
|
+
_headings = [
|
|
1186
|
+
"Link",
|
|
1187
|
+
"Shape",
|
|
1188
|
+
"Geom1",
|
|
1189
|
+
"Curve",
|
|
1190
|
+
"Geom2",
|
|
1191
|
+
"Geom3",
|
|
1192
|
+
"Geom4",
|
|
1193
|
+
"Barrels",
|
|
1194
|
+
"Culvert",
|
|
1195
|
+
]
|
|
1196
|
+
_index_col = "Link"
|
|
1197
|
+
|
|
1198
|
+
@classmethod
|
|
1199
|
+
def _tabulate(cls, line: list[str | float | int]) -> TRow | list[TRow]:
|
|
1200
|
+
out: TRow = [""] * cls._ncol
|
|
1201
|
+
out[:2] = line[:2]
|
|
1202
|
+
line = line[2:]
|
|
1203
|
+
|
|
1204
|
+
if str(out[1]).lower() == "custom" and len(line) >= 2:
|
|
1205
|
+
out[cls._headings.index("Curve")], out[cls._headings.index("Geom1")] = (
|
|
1206
|
+
line[1],
|
|
1207
|
+
line[0],
|
|
1208
|
+
)
|
|
1209
|
+
## TODO: Fix this depending on results from https://github.com/USEPA/Stormwater-Management-Model/issues/193
|
|
1210
|
+
# out[cls.headings.index("Barrels")] = line[2] if len(line) > 2 else 1
|
|
1211
|
+
out[
|
|
1212
|
+
cls._headings.index("Geom3") : cls._headings.index("Geom3")
|
|
1213
|
+
+ len(line)
|
|
1214
|
+
- 2
|
|
1215
|
+
] = line[2:]
|
|
1216
|
+
return out
|
|
1217
|
+
elif str(out[1]).lower() == "irregular":
|
|
1218
|
+
out[cls._headings.index("Curve")] = line[0]
|
|
1219
|
+
return out
|
|
1220
|
+
elif str(out[1]).upper() in cls._shapes:
|
|
1221
|
+
out[cls._headings.index("Geom1")] = line.pop(0)
|
|
1222
|
+
out[
|
|
1223
|
+
cls._headings.index("Geom2") : cls._headings.index("Geom2") + len(line)
|
|
1224
|
+
] = line
|
|
1225
|
+
return out
|
|
1226
|
+
else:
|
|
1227
|
+
raise ValueError(f"Unexpected line in xsection section ({line})")
|
|
1228
|
+
|
|
1229
|
+
@classmethod
|
|
1230
|
+
def from_section_text(cls, text: str):
|
|
1231
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1232
|
+
|
|
1233
|
+
def to_swmm_string(self) -> str:
|
|
1234
|
+
df = self.copy(deep=True)
|
|
1235
|
+
|
|
1236
|
+
# fill geoms
|
|
1237
|
+
mask = df["Shape"].isin(self._shapes)
|
|
1238
|
+
geom_cols = [f"Geom{i}" for i in range(1, 5)]
|
|
1239
|
+
df.loc[mask, geom_cols] = (
|
|
1240
|
+
df.loc[mask, geom_cols]
|
|
1241
|
+
.infer_objects(copy=False)
|
|
1242
|
+
.fillna(0)
|
|
1243
|
+
.infer_objects(copy=False)
|
|
1244
|
+
)
|
|
1245
|
+
df.loc[mask, geom_cols] = (
|
|
1246
|
+
df.loc[mask, geom_cols]
|
|
1247
|
+
.infer_objects(copy=False)
|
|
1248
|
+
.replace("", 0)
|
|
1249
|
+
.infer_objects(copy=False)
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
# fix custom shapes, Geom2 needs to be empty since the curve goes there
|
|
1253
|
+
mask = df["Shape"].astype(str).str.upper() == "CUSTOM"
|
|
1254
|
+
df.loc[mask, "Geom2"] = ""
|
|
1255
|
+
|
|
1256
|
+
return super(Xsections, df).to_swmm_string()
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
class Street(SectionDf):
|
|
1260
|
+
_section_name = "STREETS"
|
|
1261
|
+
_ncol = 11
|
|
1262
|
+
_headings = [
|
|
1263
|
+
"Name",
|
|
1264
|
+
"Tcrown",
|
|
1265
|
+
"Hcurb",
|
|
1266
|
+
"Sroad",
|
|
1267
|
+
"nRoad",
|
|
1268
|
+
"Hdep",
|
|
1269
|
+
"Wdep",
|
|
1270
|
+
"Sides",
|
|
1271
|
+
"Wback",
|
|
1272
|
+
"Sback",
|
|
1273
|
+
"nBack",
|
|
1274
|
+
]
|
|
1275
|
+
_index_col = "Name"
|
|
1276
|
+
|
|
1277
|
+
@classmethod
|
|
1278
|
+
def from_section_text(cls, text: str):
|
|
1279
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
class Transects(SectionText):
|
|
1283
|
+
_section_name = "TRANSECTS"
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
class Timeseries(SectionBase):
|
|
1287
|
+
_section_name = "TIMESERIES"
|
|
1288
|
+
|
|
1289
|
+
def __init__(self, ts: dict):
|
|
1290
|
+
self._timeseries = ts
|
|
1291
|
+
|
|
1292
|
+
@dataclass
|
|
1293
|
+
class TimeseriesFile:
|
|
1294
|
+
name: str
|
|
1295
|
+
Fname: str
|
|
1296
|
+
desc: str = ""
|
|
1297
|
+
|
|
1298
|
+
def to_swmm(self):
|
|
1299
|
+
desc = comment_formatter(self.desc)
|
|
1300
|
+
return f"{self.desc}{self.name} FILE {self.Fname}\n\n"
|
|
1301
|
+
|
|
1302
|
+
@staticmethod
|
|
1303
|
+
def _timeseries_to_swmm_dat(df, name):
|
|
1304
|
+
def df_time_formatter(x):
|
|
1305
|
+
if isinstance(x, pd.Timedelta):
|
|
1306
|
+
total_seconds = x.total_seconds()
|
|
1307
|
+
hours = int(total_seconds // 3600) # Get the total hours
|
|
1308
|
+
minutes = int((total_seconds % 3600) // 60) # Get the remaining minutes
|
|
1309
|
+
return f"{hours:02}:{minutes:02}"
|
|
1310
|
+
elif isinstance(x, pd.Timestamp):
|
|
1311
|
+
return x.strftime("%m/%d/%Y %H:%M")
|
|
1312
|
+
elif isinstance(x, (float, int)):
|
|
1313
|
+
return x
|
|
1314
|
+
|
|
1315
|
+
def df_comment_formatter(x):
|
|
1316
|
+
if len(x) > 0:
|
|
1317
|
+
return comment_formatter(x).strip("\n")
|
|
1318
|
+
else:
|
|
1319
|
+
return ""
|
|
1320
|
+
|
|
1321
|
+
df["name"] = name
|
|
1322
|
+
|
|
1323
|
+
if len(comment := df.attrs.get("desc", "")) > 0:
|
|
1324
|
+
comment_line = df_comment_formatter(comment) + "\n"
|
|
1325
|
+
else:
|
|
1326
|
+
comment_line = ""
|
|
1327
|
+
return (
|
|
1328
|
+
comment_line
|
|
1329
|
+
+ df.reset_index(names="time")
|
|
1330
|
+
.reindex(["name", "time", "value", "desc"], axis=1)
|
|
1331
|
+
.fillna("")
|
|
1332
|
+
.to_string(
|
|
1333
|
+
formatters=dict(time=df_time_formatter, desc=df_comment_formatter),
|
|
1334
|
+
index=False,
|
|
1335
|
+
header=False,
|
|
1336
|
+
)
|
|
1337
|
+
+ "\n\n"
|
|
1338
|
+
)
|
|
1339
|
+
|
|
1340
|
+
@classmethod
|
|
1341
|
+
def from_section_text(cls, text: str):
|
|
1342
|
+
def is_valid_time_format(time_string):
|
|
1343
|
+
pattern = r"^\d+:\d+$"
|
|
1344
|
+
return bool(re.match(pattern, time_string))
|
|
1345
|
+
|
|
1346
|
+
def is_valid_date(date_str):
|
|
1347
|
+
# Regex pattern to match mm/dd/yyyy, m/d/yyyy, m/dd/yyyy, or mm/d/yyyy
|
|
1348
|
+
pattern = r"^(0?[1-9]|1[0-2])/([0-2]?[0-9]|3[01])/(\d{4})$"
|
|
1349
|
+
|
|
1350
|
+
# Check if the date string matches the pattern
|
|
1351
|
+
match = re.match(pattern, date_str)
|
|
1352
|
+
|
|
1353
|
+
return bool(match)
|
|
1354
|
+
|
|
1355
|
+
timeseries: dict[str, pd.DataFrame | Timeseries.TimeseriesFile] = {}
|
|
1356
|
+
|
|
1357
|
+
rows = text.split("\n")
|
|
1358
|
+
line_comment = ""
|
|
1359
|
+
ts_comment = ""
|
|
1360
|
+
current_time_series_name = ""
|
|
1361
|
+
current_time_series_data: list[TRow] = []
|
|
1362
|
+
for row in rows:
|
|
1363
|
+
# check if row contains data
|
|
1364
|
+
if not _is_data(row):
|
|
1365
|
+
continue
|
|
1366
|
+
|
|
1367
|
+
elif _is_line_comment(row):
|
|
1368
|
+
line_comment += _strip_comment(row)[1] + "\n"
|
|
1369
|
+
continue
|
|
1370
|
+
|
|
1371
|
+
line, comment = _strip_comment(row)
|
|
1372
|
+
if len(comment) > 0:
|
|
1373
|
+
line_comment += comment + "\n"
|
|
1374
|
+
|
|
1375
|
+
# split row into tokens coercing numerics into floats
|
|
1376
|
+
split_data = [_coerce_numeric(val) for val in line.split()]
|
|
1377
|
+
|
|
1378
|
+
ts_name = str(split_data.pop(0))
|
|
1379
|
+
if ts_name != current_time_series_name:
|
|
1380
|
+
|
|
1381
|
+
if len(current_time_series_data) > 0:
|
|
1382
|
+
df = pd.DataFrame(
|
|
1383
|
+
current_time_series_data, columns=["time", "value", "desc"]
|
|
1384
|
+
).set_index("time")
|
|
1385
|
+
df.attrs["desc"] = ts_comment
|
|
1386
|
+
timeseries[current_time_series_name] = df
|
|
1387
|
+
|
|
1388
|
+
current_time_series_name = ts_name
|
|
1389
|
+
current_time_series_data = []
|
|
1390
|
+
ts_comment = line_comment
|
|
1391
|
+
line_comment = ""
|
|
1392
|
+
|
|
1393
|
+
if str(split_data[0]).upper() == "FILE" and len(split_data) == 2:
|
|
1394
|
+
timeseries[ts_name] = cls.TimeseriesFile(
|
|
1395
|
+
name=ts_name, Fname=str(split_data[1]), desc=line_comment
|
|
1396
|
+
)
|
|
1397
|
+
continue
|
|
1398
|
+
time: pd.Timedelta | pd.Timestamp
|
|
1399
|
+
while len(split_data) > 0:
|
|
1400
|
+
if isinstance(split_data[0], Number):
|
|
1401
|
+
time = pd.Timedelta(hours=float(split_data.pop(0)))
|
|
1402
|
+
value = float(split_data.pop(0))
|
|
1403
|
+
elif is_valid_time_format(split_data[0]):
|
|
1404
|
+
hours, minutes = str(split_data.pop(0)).split(":")
|
|
1405
|
+
time = pd.Timedelta(hours=int(hours), minutes=int(minutes))
|
|
1406
|
+
value = float(split_data.pop(0))
|
|
1407
|
+
elif is_valid_date(split_data[0]):
|
|
1408
|
+
date = pd.to_datetime(split_data.pop(0))
|
|
1409
|
+
if not is_valid_time_format(split_data[0]):
|
|
1410
|
+
raise ValueError(
|
|
1411
|
+
f"Error parsing timeseries {ts_name!r} time: {split_data[0]}"
|
|
1412
|
+
)
|
|
1413
|
+
hours, minutes = str(split_data.pop(0)).split(":")
|
|
1414
|
+
_time = pd.Timedelta(hours=int(hours), minutes=int(minutes))
|
|
1415
|
+
time = date + _time
|
|
1416
|
+
value = float(split_data.pop(0))
|
|
1417
|
+
else:
|
|
1418
|
+
raise ValueError(f"Error parsing Timeseries row {split_data}")
|
|
1419
|
+
|
|
1420
|
+
current_time_series_data.append([time, value, line_comment])
|
|
1421
|
+
|
|
1422
|
+
line_comment = ""
|
|
1423
|
+
|
|
1424
|
+
# instantiate DataFrame
|
|
1425
|
+
return cls(ts=timeseries)
|
|
1426
|
+
|
|
1427
|
+
@classmethod
|
|
1428
|
+
def _from_section_text(cls, text: str, *args, **kwargs) -> Self:
|
|
1429
|
+
raise NotImplementedError
|
|
1430
|
+
|
|
1431
|
+
@classmethod
|
|
1432
|
+
def _new_empty(cls) -> Self:
|
|
1433
|
+
return cls(ts={})
|
|
1434
|
+
|
|
1435
|
+
@classmethod
|
|
1436
|
+
def _newobj(cls, *args, **kwargs) -> Self:
|
|
1437
|
+
return cls(*args, **kwargs)
|
|
1438
|
+
|
|
1439
|
+
def to_swmm_string(self) -> str:
|
|
1440
|
+
out_str = textwrap.dedent(
|
|
1441
|
+
"""\
|
|
1442
|
+
;;Name Date Time Value
|
|
1443
|
+
;;-------------- ---------- ---------- ----------
|
|
1444
|
+
"""
|
|
1445
|
+
)
|
|
1446
|
+
for ts_name, ts_data in self._timeseries.items():
|
|
1447
|
+
if isinstance(ts_data, pd.DataFrame):
|
|
1448
|
+
out_str += self._timeseries_to_swmm_dat(ts_data, ts_name)
|
|
1449
|
+
elif isinstance(ts_data, self.TimeseriesFile):
|
|
1450
|
+
out_str += ts_data.to_swmm()
|
|
1451
|
+
return out_str
|
|
1452
|
+
|
|
1453
|
+
def add_file_timeseries(self, name: str, Fname: str, comment: str = "") -> Self:
|
|
1454
|
+
self._timeseries[name] = self.TimeseriesFile(
|
|
1455
|
+
name=name, Fname=Fname, desc=comment
|
|
1456
|
+
)
|
|
1457
|
+
return self
|
|
1458
|
+
|
|
1459
|
+
def __setitem__(self, key, data) -> None:
|
|
1460
|
+
if isinstance(data, pd.DataFrame):
|
|
1461
|
+
if "value" not in data.columns:
|
|
1462
|
+
raise ValueError(
|
|
1463
|
+
f"Expected 'value' columns in dataframe, got {data.columns!r}"
|
|
1464
|
+
)
|
|
1465
|
+
|
|
1466
|
+
self._timeseries[key] = data.reindex(["value", "comment"], axis=1)
|
|
1467
|
+
else:
|
|
1468
|
+
raise TypeError(
|
|
1469
|
+
f"__setitem__ currently only supports dataframes, got {type(data)}. "
|
|
1470
|
+
"Use the `add_file_timeseries` method to add file-based timeseries"
|
|
1471
|
+
)
|
|
1472
|
+
|
|
1473
|
+
def __getitem__(self, name) -> TimeseriesFile | pd.DataFrame:
|
|
1474
|
+
return self._timeseries[name]
|
|
1475
|
+
|
|
1476
|
+
def __repr__(self) -> str:
|
|
1477
|
+
longest_name = max(map(len, self._timeseries.keys()))
|
|
1478
|
+
width = longest_name + 2
|
|
1479
|
+
reprstr = ""
|
|
1480
|
+
for name, value in self._timeseries.items():
|
|
1481
|
+
if isinstance(value, self.TimeseriesFile):
|
|
1482
|
+
reprstr += f"{name:{width}}| TimeseriesFile(Fname={value.Fname!r}, desc={value.desc!r})\n"
|
|
1483
|
+
elif isinstance(value, pd.DataFrame):
|
|
1484
|
+
reprstr += f"{name:{width}}| DataFrame(start={value.index[0]!r}, end={value.index[-1]!r},len={len(value)})\n"
|
|
1485
|
+
return reprstr
|
|
1486
|
+
|
|
1487
|
+
def __iter__(self) -> Iterator[TimeseriesFile | pd.DataFrame]:
|
|
1488
|
+
return iter(self._timeseries.values())
|
|
1489
|
+
|
|
1490
|
+
def __len__(self) -> int:
|
|
1491
|
+
return len(self._timeseries)
|
|
1492
|
+
|
|
1493
|
+
def _ipython_key_completions_(self) -> list[str]:
|
|
1494
|
+
"""Provide method for the key-autocompletions in IPython.
|
|
1495
|
+
See http://ipython.readthedocs.io/en/stable/config/integrating.html#tab-completion
|
|
1496
|
+
For the details.
|
|
1497
|
+
"""
|
|
1498
|
+
|
|
1499
|
+
return list(self._timeseries.keys())
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
class Patterns(SectionDf):
|
|
1503
|
+
_section_name = "PATTERNS"
|
|
1504
|
+
_ncol = 3
|
|
1505
|
+
_headings = ["Name", "Type", "Multiplier"]
|
|
1506
|
+
_index_col = ["Name"]
|
|
1507
|
+
_valid_types = [
|
|
1508
|
+
"MONTHLY",
|
|
1509
|
+
"DAILY",
|
|
1510
|
+
"HOURLY",
|
|
1511
|
+
"WEEKEND",
|
|
1512
|
+
]
|
|
1513
|
+
|
|
1514
|
+
@classmethod
|
|
1515
|
+
def _tabulate(cls, line: list[str | float]) -> TRow | list[TRow]:
|
|
1516
|
+
out: list[TRow] = []
|
|
1517
|
+
name = line.pop(0)
|
|
1518
|
+
|
|
1519
|
+
pattern_type: str | NAType
|
|
1520
|
+
if str(line[0]).upper() in cls._valid_types:
|
|
1521
|
+
pattern_type = str(line.pop(0)).upper()
|
|
1522
|
+
elif isinstance(line[0], Number):
|
|
1523
|
+
pattern_type = pd.NA
|
|
1524
|
+
else:
|
|
1525
|
+
raise ValueError(f"Error parsing pattern line {[name]+line!r}")
|
|
1526
|
+
|
|
1527
|
+
for value in line:
|
|
1528
|
+
row: TRow = [""] * cls._ncol
|
|
1529
|
+
float_val = float(value)
|
|
1530
|
+
row[0:3] = name, pattern_type, float_val
|
|
1531
|
+
out.append(row)
|
|
1532
|
+
return out
|
|
1533
|
+
|
|
1534
|
+
@classmethod
|
|
1535
|
+
def _validate_pattern_types(cls, df: pd.DataFrame) -> dict[str, str]:
|
|
1536
|
+
unique_patterns = df.reset_index()[["Name", "Type"]].dropna().drop_duplicates()
|
|
1537
|
+
if unique_patterns["Name"].duplicated().any():
|
|
1538
|
+
raise ValueError(
|
|
1539
|
+
"Pattern with duplicate types found in input file. "
|
|
1540
|
+
"Each pattern must only specify a single type to work with swmm.pandas"
|
|
1541
|
+
)
|
|
1542
|
+
if not all(
|
|
1543
|
+
bools := [pattern in cls._valid_types for pattern in unique_patterns.Type]
|
|
1544
|
+
):
|
|
1545
|
+
invalid_patterns = unique_patterns["Type"].loc[~np.array(bools)].to_list()
|
|
1546
|
+
raise ValueError(f"Unknown curves {invalid_patterns!r}")
|
|
1547
|
+
|
|
1548
|
+
return unique_patterns.set_index("Name")["Type"].to_dict()
|
|
1549
|
+
|
|
1550
|
+
@classmethod
|
|
1551
|
+
def from_section_text(cls, text: str) -> Self:
|
|
1552
|
+
df = super()._from_section_text(text, cls._ncol)
|
|
1553
|
+
pattern_types = cls._validate_pattern_types(df)
|
|
1554
|
+
df = df.reset_index().drop("Type", axis=1)
|
|
1555
|
+
df["Pattern_Index"] = df.groupby("Name").cumcount()
|
|
1556
|
+
df = cls(df.set_index(["Name", "Pattern_Index"]))
|
|
1557
|
+
df.attrs = pattern_types # type: ignore
|
|
1558
|
+
return df
|
|
1559
|
+
|
|
1560
|
+
def to_swmm_string(self) -> str:
|
|
1561
|
+
df = self.copy(deep=True)
|
|
1562
|
+
|
|
1563
|
+
# add type back into frame in first row of curve
|
|
1564
|
+
type_idx = pd.MultiIndex.from_frame(
|
|
1565
|
+
df.index.to_frame()
|
|
1566
|
+
.drop("Name", axis=1)
|
|
1567
|
+
.groupby("Name")["Pattern_Index"]
|
|
1568
|
+
.min()
|
|
1569
|
+
.reset_index()
|
|
1570
|
+
)
|
|
1571
|
+
type_values = type_idx.get_level_values(0).map(df.attrs).to_numpy()
|
|
1572
|
+
df.loc[:, "Type"] = ""
|
|
1573
|
+
df.loc[type_idx, "Type"] = type_values
|
|
1574
|
+
|
|
1575
|
+
# sort by name and index then drop the curve index field since swmm doesn't use it
|
|
1576
|
+
df = df.sort_index(ascending=[True, True])
|
|
1577
|
+
df.index = df.index.droplevel("Pattern_Index")
|
|
1578
|
+
return super(Patterns, df).to_swmm_string()
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
class Inlet(SectionDf):
|
|
1582
|
+
_section_name = "INLETS"
|
|
1583
|
+
_ncol = 7
|
|
1584
|
+
_headings = [
|
|
1585
|
+
"Name",
|
|
1586
|
+
"Type",
|
|
1587
|
+
]
|
|
1588
|
+
_index_col = ["Name", "Type"]
|
|
1589
|
+
|
|
1590
|
+
@classmethod
|
|
1591
|
+
def from_section_text(cls, text: str):
|
|
1592
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
class Inlet_Usage(SectionDf):
|
|
1596
|
+
_section_name = "INLET_USAGE"
|
|
1597
|
+
_ncol = 9
|
|
1598
|
+
_headings = [
|
|
1599
|
+
"Conduit",
|
|
1600
|
+
"Inlet",
|
|
1601
|
+
"Node",
|
|
1602
|
+
"Number",
|
|
1603
|
+
"%Clogged",
|
|
1604
|
+
"MaxFlow",
|
|
1605
|
+
"hDStore",
|
|
1606
|
+
"wDStore",
|
|
1607
|
+
"Placement",
|
|
1608
|
+
]
|
|
1609
|
+
_index_col = "Conduit"
|
|
1610
|
+
|
|
1611
|
+
@classmethod
|
|
1612
|
+
def from_section_text(cls, text: str):
|
|
1613
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1614
|
+
|
|
1615
|
+
|
|
1616
|
+
class Losses(SectionDf):
|
|
1617
|
+
_section_name = "LOSSES"
|
|
1618
|
+
_ncol = 6
|
|
1619
|
+
_headings = ["Link", "Kentry", "Kexit", "Kavg", "FlapGate", "Seepage"]
|
|
1620
|
+
_index_col = "Link"
|
|
1621
|
+
|
|
1622
|
+
@classmethod
|
|
1623
|
+
def from_section_text(cls, text: str):
|
|
1624
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1625
|
+
|
|
1626
|
+
def to_swmm_string(self) -> str:
|
|
1627
|
+
df = self.copy(deep=True)
|
|
1628
|
+
|
|
1629
|
+
for col in self._data_cols(desc=False):
|
|
1630
|
+
if col != "FlapGate":
|
|
1631
|
+
df[col] = df[col].infer_objects(copy=False).fillna(0.0)
|
|
1632
|
+
else:
|
|
1633
|
+
df[col] = df[col].infer_objects(copy=False).fillna("NO")
|
|
1634
|
+
|
|
1635
|
+
return super(Losses, df).to_swmm_string()
|
|
1636
|
+
|
|
1637
|
+
|
|
1638
|
+
class Controls(SectionText):
|
|
1639
|
+
_section_name = "CONTROLS"
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
class Pollutants(SectionDf):
|
|
1643
|
+
_section_name = "POLLUTANTS"
|
|
1644
|
+
_ncol = 11
|
|
1645
|
+
_headings = [
|
|
1646
|
+
"Name",
|
|
1647
|
+
"Units",
|
|
1648
|
+
"Crain",
|
|
1649
|
+
"Cgw",
|
|
1650
|
+
"Crdii",
|
|
1651
|
+
"Kdecay",
|
|
1652
|
+
"SnowOnly",
|
|
1653
|
+
"CoPollutant",
|
|
1654
|
+
"CoFrac",
|
|
1655
|
+
"Cdwf",
|
|
1656
|
+
"Cinit",
|
|
1657
|
+
]
|
|
1658
|
+
_index_col = "Name"
|
|
1659
|
+
|
|
1660
|
+
@classmethod
|
|
1661
|
+
def from_section_text(cls, text: str):
|
|
1662
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
class LandUse(SectionDf):
|
|
1666
|
+
_section_name = "LANDUSES"
|
|
1667
|
+
_ncol = 4
|
|
1668
|
+
_headings = ["Name", "SweepInterval", "Availability", "LastSweep"]
|
|
1669
|
+
_index_col = "Name"
|
|
1670
|
+
|
|
1671
|
+
@classmethod
|
|
1672
|
+
def from_section_text(cls, text: str):
|
|
1673
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1674
|
+
|
|
1675
|
+
def to_swmm_string(self) -> str:
|
|
1676
|
+
for col in self.columns:
|
|
1677
|
+
self[col] = self[col].infer_objects(copy=False).fillna(0.0)
|
|
1678
|
+
return super().to_swmm_string()
|
|
1679
|
+
|
|
1680
|
+
|
|
1681
|
+
class Coverage(SectionDf):
|
|
1682
|
+
_section_name = "COVERAGES"
|
|
1683
|
+
_ncol = 3
|
|
1684
|
+
_headings = ["Subcatchment", "landuse", "Percent"]
|
|
1685
|
+
_index_col = ["Subcatchment", "landuse"]
|
|
1686
|
+
|
|
1687
|
+
@classmethod
|
|
1688
|
+
def _tabulate(cls, line: list[str | float | int]) -> TRow | list[TRow]:
|
|
1689
|
+
if len(line) > 3:
|
|
1690
|
+
raise Exception(
|
|
1691
|
+
"swmm.pandas doesn't yet support having multiple land "
|
|
1692
|
+
"uses on a single coverage line. Separate your land use "
|
|
1693
|
+
"coverages onto individual lines first",
|
|
1694
|
+
)
|
|
1695
|
+
return super()._tabulate(line)
|
|
1696
|
+
|
|
1697
|
+
@classmethod
|
|
1698
|
+
def from_section_text(cls, text: str):
|
|
1699
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
class Loading(SectionDf):
|
|
1703
|
+
_section_name = "LOADINGS"
|
|
1704
|
+
_ncol = 3
|
|
1705
|
+
_headings = ["Subcatchment", "Pollutant", "InitBuildup"]
|
|
1706
|
+
_index_col = ["Subcatchment", "Pollutant"]
|
|
1707
|
+
|
|
1708
|
+
@classmethod
|
|
1709
|
+
def _tabulate(cls, line: list[str | float | int]) -> TRow | list[TRow]:
|
|
1710
|
+
if len(line) > 3:
|
|
1711
|
+
raise Exception(
|
|
1712
|
+
"swmm.pandas doesn't yet support having multiple pollutants "
|
|
1713
|
+
"uses on a single loading line. Separate your pollutant "
|
|
1714
|
+
"loadings onto individual lines first",
|
|
1715
|
+
)
|
|
1716
|
+
return super()._tabulate(line)
|
|
1717
|
+
|
|
1718
|
+
@classmethod
|
|
1719
|
+
def from_section_text(cls, text: str):
|
|
1720
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1721
|
+
|
|
1722
|
+
|
|
1723
|
+
class Buildup(SectionDf):
|
|
1724
|
+
_section_name = "BUILDUP"
|
|
1725
|
+
_ncol = 4
|
|
1726
|
+
_headings = ["Landuse", "Pollutant", "FuncType", "C1", "C2", "C3", "PerUnit"]
|
|
1727
|
+
_index_col = ["Landuse", "Pollutant"]
|
|
1728
|
+
|
|
1729
|
+
@classmethod
|
|
1730
|
+
def from_section_text(cls, text: str):
|
|
1731
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
class Washoff(SectionDf):
|
|
1735
|
+
_section_name = "WASHOFF"
|
|
1736
|
+
_ncol = 4
|
|
1737
|
+
_headings = ["Landuse", "Pollutant", "FuncType", "C1", "C2", "SweepRmvl", "BmpRmvl"]
|
|
1738
|
+
_index_col = ["Landuse", "Pollutant"]
|
|
1739
|
+
|
|
1740
|
+
@classmethod
|
|
1741
|
+
def from_section_text(cls, text: str):
|
|
1742
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1743
|
+
|
|
1744
|
+
|
|
1745
|
+
class Treatment(SectionDf):
|
|
1746
|
+
_section_name = "TREATMENT"
|
|
1747
|
+
_ncol = 3
|
|
1748
|
+
_headings = ["Node", "Pollutant", "Func"]
|
|
1749
|
+
_index_col = ["Node", "Pollutant"]
|
|
1750
|
+
|
|
1751
|
+
@classmethod
|
|
1752
|
+
def from_section_text(cls, text: str):
|
|
1753
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1754
|
+
|
|
1755
|
+
@classmethod
|
|
1756
|
+
def _tabulate(cls, line: list[str | float]) -> TRow | list[TRow]:
|
|
1757
|
+
node = str(line.pop(0))
|
|
1758
|
+
poll = str(line.pop(0))
|
|
1759
|
+
eqn = " ".join(str(v) for v in line)
|
|
1760
|
+
out: TRow = [node, poll, eqn]
|
|
1761
|
+
return out
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
class Inflow(SectionDf):
|
|
1765
|
+
_section_name = "INFLOWS"
|
|
1766
|
+
_ncol = 8
|
|
1767
|
+
_headings = [
|
|
1768
|
+
"Node",
|
|
1769
|
+
"Constituent",
|
|
1770
|
+
"TimeSeries",
|
|
1771
|
+
"InflowType",
|
|
1772
|
+
"Mfactor",
|
|
1773
|
+
"Sfactor",
|
|
1774
|
+
"Baseline",
|
|
1775
|
+
"Pattern",
|
|
1776
|
+
]
|
|
1777
|
+
_index_col = ["Node", "Constituent"]
|
|
1778
|
+
|
|
1779
|
+
@classmethod
|
|
1780
|
+
def from_section_text(cls, text: str):
|
|
1781
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1782
|
+
|
|
1783
|
+
@classmethod
|
|
1784
|
+
def _tabulate(cls, line: list[str | float | int]) -> TRow | list[TRow]:
|
|
1785
|
+
return [v.replace('"', "") if isinstance(v, str) else v for v in line]
|
|
1786
|
+
|
|
1787
|
+
def to_swmm_string(self) -> str:
|
|
1788
|
+
df = self.copy(deep=True)
|
|
1789
|
+
df["Mfactor"] = df["Mfactor"].infer_objects(copy=False).fillna(1.0)
|
|
1790
|
+
df["Sfactor"] = df["Sfactor"].infer_objects(copy=False).fillna(1.0)
|
|
1791
|
+
|
|
1792
|
+
# strip out any existing double quotes
|
|
1793
|
+
df["TimeSeries"] = df["TimeSeries"].fillna("").str.replace('"', "")
|
|
1794
|
+
df["TimeSeries"] = '"' + df["TimeSeries"].astype(str) + '"'
|
|
1795
|
+
return super(Inflow, df).to_swmm_string()
|
|
1796
|
+
|
|
1797
|
+
|
|
1798
|
+
class DWF(SectionDf):
|
|
1799
|
+
_section_name = "DWF"
|
|
1800
|
+
_ncol = 7
|
|
1801
|
+
_headings = [
|
|
1802
|
+
"Node",
|
|
1803
|
+
"Constituent",
|
|
1804
|
+
"AvgValue",
|
|
1805
|
+
"Pat1",
|
|
1806
|
+
"Pat2",
|
|
1807
|
+
"Pat3",
|
|
1808
|
+
"Pat4",
|
|
1809
|
+
]
|
|
1810
|
+
_index_col = ["Node", "Constituent"]
|
|
1811
|
+
|
|
1812
|
+
@classmethod
|
|
1813
|
+
def from_section_text(cls, text: str):
|
|
1814
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1815
|
+
|
|
1816
|
+
@classmethod
|
|
1817
|
+
def _tabulate(cls, line: list[str | float | int]) -> TRow | list[TRow]:
|
|
1818
|
+
return [v.replace('"', "") if isinstance(v, str) else v for v in line]
|
|
1819
|
+
|
|
1820
|
+
def to_swmm_string(self) -> str:
|
|
1821
|
+
df = self.copy(deep=True)
|
|
1822
|
+
df["AvgValue"] = df["AvgValue"].infer_objects(copy=False).fillna(0.0)
|
|
1823
|
+
|
|
1824
|
+
for ipat in range(1, 5):
|
|
1825
|
+
col = f"Pat{ipat}"
|
|
1826
|
+
df[col] = df[col].fillna("").str.replace('"', "")
|
|
1827
|
+
df[col] = '"' + df[col].astype(str) + '"'
|
|
1828
|
+
|
|
1829
|
+
return super(DWF, df).to_swmm_string()
|
|
1830
|
+
|
|
1831
|
+
|
|
1832
|
+
class RDII(SectionDf):
|
|
1833
|
+
_section_name = "RDII"
|
|
1834
|
+
_ncol = 3
|
|
1835
|
+
_headings = ["Node", "UHgroup", "SewerArea"]
|
|
1836
|
+
_index_col = "Node"
|
|
1837
|
+
|
|
1838
|
+
@classmethod
|
|
1839
|
+
def from_section_text(cls, text: str):
|
|
1840
|
+
return super()._from_section_text(text, cls._ncol)
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
class Hydrographs(SectionDf):
|
|
1844
|
+
_section_name = "HYDROGRAPHS"
|
|
1845
|
+
_ncol = 9
|
|
1846
|
+
_headings = [
|
|
1847
|
+
"Name",
|
|
1848
|
+
"Month_RG",
|
|
1849
|
+
"Response",
|
|
1850
|
+
"R",
|
|
1851
|
+
"T",
|
|
1852
|
+
"K",
|
|
1853
|
+
"IA_max",
|
|
1854
|
+
"IA_rec",
|
|
1855
|
+
"IA_ini",
|
|
1856
|
+
]
|
|
1857
|
+
_index_col = ["Name", "Month_RG", "Response"]
|
|
1858
|
+
|
|
1859
|
+
@classmethod
|
|
1860
|
+
def from_section_text(cls, text: str) -> Self:
|
|
1861
|
+
|
|
1862
|
+
df = super()._from_section_text(text, cls._ncol).reset_index()
|
|
1863
|
+
rg_rows = cls._find_rain_gauge_rows(df)
|
|
1864
|
+
rgs = df.loc[rg_rows].set_index("Name")["Month_RG"].to_dict()
|
|
1865
|
+
df.drop(rg_rows, inplace=True)
|
|
1866
|
+
df = cls(df.set_index(cls._index_col).sort_index())
|
|
1867
|
+
df.attrs = rgs
|
|
1868
|
+
return df
|
|
1869
|
+
|
|
1870
|
+
@property
|
|
1871
|
+
def rain_gauges(self) -> dict[str, str]:
|
|
1872
|
+
return self.attrs # type: ignore
|
|
1873
|
+
|
|
1874
|
+
@staticmethod
|
|
1875
|
+
def _find_rain_gauge_rows(df) -> pd.Index:
|
|
1876
|
+
# Function to check if a row matches the raingauge criteria
|
|
1877
|
+
def is_raingauge_row(row):
|
|
1878
|
+
return (row != "").sum() == 2
|
|
1879
|
+
|
|
1880
|
+
# Apply the function to each row and get the indices where it's True
|
|
1881
|
+
raingauge_indices = df.loc[df.apply(is_raingauge_row, axis=1)].index
|
|
1882
|
+
|
|
1883
|
+
return raingauge_indices
|
|
1884
|
+
|
|
1885
|
+
def to_swmm_string(self) -> str:
|
|
1886
|
+
|
|
1887
|
+
def month_to_number(month):
|
|
1888
|
+
try:
|
|
1889
|
+
return list(month_abbr).index(month.capitalize())
|
|
1890
|
+
except ValueError:
|
|
1891
|
+
return -1 # This will sort unrecognized months to the top
|
|
1892
|
+
|
|
1893
|
+
def index_mapper(index):
|
|
1894
|
+
if index.name == "Month_RG":
|
|
1895
|
+
return index.map(month_to_number)
|
|
1896
|
+
else:
|
|
1897
|
+
return index
|
|
1898
|
+
|
|
1899
|
+
# add rain gauge rows
|
|
1900
|
+
_temp = self.__class__._new_empty()
|
|
1901
|
+
for name in self.index.get_level_values("Name").unique():
|
|
1902
|
+
try:
|
|
1903
|
+
_temp.add_element(
|
|
1904
|
+
Name=name, Month_RG=self.rain_gauges[name], Response=""
|
|
1905
|
+
)
|
|
1906
|
+
except KeyError:
|
|
1907
|
+
raise KeyError(
|
|
1908
|
+
f"Raingauge for hydrograph {name!r} not found in hydrographs.rain_gauges property. "
|
|
1909
|
+
f"Only found {self.rain_gauges!r}"
|
|
1910
|
+
)
|
|
1911
|
+
|
|
1912
|
+
df = pd.concat([self, _temp])
|
|
1913
|
+
# sort by name, month, and response after adding in raingauges
|
|
1914
|
+
df = Hydrographs(df.sort_index(ascending=[True, True, False], key=index_mapper))
|
|
1915
|
+
return super(Hydrographs, df).to_swmm_string()
|
|
1916
|
+
|
|
1917
|
+
|
|
1918
|
+
class Curves(SectionDf):
|
|
1919
|
+
_section_name = "CURVES"
|
|
1920
|
+
_ncol = 4
|
|
1921
|
+
_headings = ["Name", "Type", "X_Value", "Y_Value"]
|
|
1922
|
+
_index_col = ["Name"]
|
|
1923
|
+
_valid_types = [
|
|
1924
|
+
"STORAGE",
|
|
1925
|
+
"SHAPE",
|
|
1926
|
+
"DIVERSION",
|
|
1927
|
+
"TIDAL",
|
|
1928
|
+
"PUMP1",
|
|
1929
|
+
"PUMP2",
|
|
1930
|
+
"PUMP3",
|
|
1931
|
+
"PUMP4",
|
|
1932
|
+
"PUMP5",
|
|
1933
|
+
"RATING",
|
|
1934
|
+
"CONTROL",
|
|
1935
|
+
"WEIR",
|
|
1936
|
+
]
|
|
1937
|
+
|
|
1938
|
+
@classmethod
|
|
1939
|
+
def _tabulate(cls, line: list[str | float]) -> TRow | list[TRow]:
|
|
1940
|
+
out = []
|
|
1941
|
+
name = line.pop(0)
|
|
1942
|
+
|
|
1943
|
+
curve_type: str | NAType
|
|
1944
|
+
if str(line[0]).upper() in cls._valid_types:
|
|
1945
|
+
curve_type = str(line.pop(0)).upper()
|
|
1946
|
+
elif isinstance(line[0], Number):
|
|
1947
|
+
curve_type = pd.NA
|
|
1948
|
+
else:
|
|
1949
|
+
raise ValueError(f"Error parsing curve line {[name]+line!r}")
|
|
1950
|
+
|
|
1951
|
+
for chunk in range(0, len(line), 2):
|
|
1952
|
+
row: TRow = [""] * cls._ncol
|
|
1953
|
+
x_value, y_value = line[chunk : chunk + 2]
|
|
1954
|
+
row[0:4] = name, curve_type, x_value, y_value
|
|
1955
|
+
out.append(row)
|
|
1956
|
+
return out
|
|
1957
|
+
|
|
1958
|
+
@classmethod
|
|
1959
|
+
def _validate_curve_types(cls, df: pd.DataFrame) -> dict[str, str]:
|
|
1960
|
+
unique_curves = df.reset_index()[["Name", "Type"]].dropna().drop_duplicates()
|
|
1961
|
+
if unique_curves["Name"].duplicated().any():
|
|
1962
|
+
raise ValueError(
|
|
1963
|
+
"Curve with duplicate types found in input file. "
|
|
1964
|
+
"Each curve must only specify a single type to work with swmm.pandas"
|
|
1965
|
+
)
|
|
1966
|
+
if not all(
|
|
1967
|
+
bools := [curve in cls._valid_types for curve in unique_curves.Type]
|
|
1968
|
+
):
|
|
1969
|
+
invalid_curves = unique_curves["Type"].loc[~np.array(bools)].to_list()
|
|
1970
|
+
raise ValueError(f"Unknown curves {invalid_curves!r}")
|
|
1971
|
+
|
|
1972
|
+
return unique_curves.set_index("Name")["Type"].to_dict()
|
|
1973
|
+
|
|
1974
|
+
@classmethod
|
|
1975
|
+
def from_section_text(cls, text: str) -> Self:
|
|
1976
|
+
df = super()._from_section_text(text, cls._ncol)
|
|
1977
|
+
curve_types = cls._validate_curve_types(df)
|
|
1978
|
+
df = df.reset_index().drop("Type", axis=1)
|
|
1979
|
+
df["Curve_Index"] = df.groupby("Name").cumcount()
|
|
1980
|
+
df = cls(df.set_index(["Name", "Curve_Index"]))
|
|
1981
|
+
df.attrs = curve_types # type: ignore
|
|
1982
|
+
return df
|
|
1983
|
+
|
|
1984
|
+
def to_swmm_string(self) -> str:
|
|
1985
|
+
df = self.copy(deep=True)
|
|
1986
|
+
|
|
1987
|
+
# add type back into frame in first row of curve
|
|
1988
|
+
type_idx = pd.MultiIndex.from_frame(
|
|
1989
|
+
df.index.to_frame()
|
|
1990
|
+
.drop("Name", axis=1)
|
|
1991
|
+
.groupby("Name")["Curve_Index"]
|
|
1992
|
+
.min()
|
|
1993
|
+
.reset_index()
|
|
1994
|
+
)
|
|
1995
|
+
type_values = type_idx.get_level_values(0).map(df.attrs).to_numpy()
|
|
1996
|
+
df.loc[:, "Type"] = ""
|
|
1997
|
+
df.loc[type_idx, "Type"] = type_values
|
|
1998
|
+
|
|
1999
|
+
# sort by name and index then drop the curve index field since swmm doesn't use it
|
|
2000
|
+
df = Curves(df.sort_index(ascending=[True, True]))
|
|
2001
|
+
df.index = df.index.droplevel("Curve_Index")
|
|
2002
|
+
return super(Curves, df).to_swmm_string()
|
|
2003
|
+
|
|
2004
|
+
|
|
2005
|
+
class Coordinates(SectionDf):
|
|
2006
|
+
_section_name = "COORDINATES"
|
|
2007
|
+
_ncol = 3
|
|
2008
|
+
_headings = ["Node", "X", "Y"]
|
|
2009
|
+
_index_col = "Node"
|
|
2010
|
+
|
|
2011
|
+
@classmethod
|
|
2012
|
+
def from_section_text(cls, text: str):
|
|
2013
|
+
return super()._from_section_text(text, cls._ncol)
|
|
2014
|
+
|
|
2015
|
+
|
|
2016
|
+
class Vertices(SectionDf):
|
|
2017
|
+
_section_name = "VERTICIES"
|
|
2018
|
+
_ncol = 3
|
|
2019
|
+
_headings = ["Link", "X", "Y"]
|
|
2020
|
+
_index_col = "Link"
|
|
2021
|
+
|
|
2022
|
+
@classmethod
|
|
2023
|
+
def from_section_text(cls, text: str):
|
|
2024
|
+
return super()._from_section_text(text, cls._ncol)
|
|
2025
|
+
|
|
2026
|
+
|
|
2027
|
+
class Polygons(SectionDf):
|
|
2028
|
+
_section_name = "POLYGONS"
|
|
2029
|
+
_ncol = 3
|
|
2030
|
+
_headings = ["Subcatch", "X", "Y"]
|
|
2031
|
+
_index_col = "Subcatch"
|
|
2032
|
+
|
|
2033
|
+
@classmethod
|
|
2034
|
+
def from_section_text(cls, text: str):
|
|
2035
|
+
return super()._from_section_text(text, cls._ncol)
|
|
2036
|
+
|
|
2037
|
+
|
|
2038
|
+
class Symbols(SectionDf):
|
|
2039
|
+
_section_name = "SYMBOLS"
|
|
2040
|
+
_ncol = 3
|
|
2041
|
+
_headings = ["Gage", "X", "Y"]
|
|
2042
|
+
_index_col = "Gage"
|
|
2043
|
+
|
|
2044
|
+
@classmethod
|
|
2045
|
+
def from_section_text(cls, text: str):
|
|
2046
|
+
return super()._from_section_text(text, cls._ncol)
|
|
2047
|
+
|
|
2048
|
+
|
|
2049
|
+
class Labels(SectionDf):
|
|
2050
|
+
_section_name = "LABELS"
|
|
2051
|
+
_ncol = 8
|
|
2052
|
+
_headings = [
|
|
2053
|
+
"Xcoord",
|
|
2054
|
+
"Ycoord",
|
|
2055
|
+
"Label",
|
|
2056
|
+
"Anchor",
|
|
2057
|
+
"Font",
|
|
2058
|
+
"Size",
|
|
2059
|
+
"Bold",
|
|
2060
|
+
"Italic",
|
|
2061
|
+
]
|
|
2062
|
+
|
|
2063
|
+
@classmethod
|
|
2064
|
+
def from_section_text(cls, text: str):
|
|
2065
|
+
return super()._from_section_text(text, cls._ncol)
|
|
2066
|
+
|
|
2067
|
+
|
|
2068
|
+
class Tags(SectionDf):
|
|
2069
|
+
_section_name = "TAGS"
|
|
2070
|
+
_ncol = 3
|
|
2071
|
+
_headings = ["Element", "Name", "Tag"]
|
|
2072
|
+
_index_col = ["Element", "Name"]
|
|
2073
|
+
|
|
2074
|
+
@classmethod
|
|
2075
|
+
def from_section_text(cls, text: str):
|
|
2076
|
+
return super()._from_section_text(text, cls._ncol)
|
|
2077
|
+
|
|
2078
|
+
|
|
2079
|
+
class Profile(SectionText):
|
|
2080
|
+
_section_name = "PROFILE"
|
|
2081
|
+
|
|
2082
|
+
|
|
2083
|
+
class LID_Control(SectionDf):
|
|
2084
|
+
_section_name = "LID_CONTROLS"
|
|
2085
|
+
_ncol = 9
|
|
2086
|
+
_headings = ["Name", "Type"]
|
|
2087
|
+
_index_col = "Name"
|
|
2088
|
+
|
|
2089
|
+
@classmethod
|
|
2090
|
+
def from_section_text(cls, text: str):
|
|
2091
|
+
return super()._from_section_text(text, cls._ncol)
|
|
2092
|
+
|
|
2093
|
+
@classmethod
|
|
2094
|
+
def _tabulate(cls, line: list[str | float]) -> TRow | list[TRow]:
|
|
2095
|
+
lid_type = line[1]
|
|
2096
|
+
if lid_type == "REMOVALS":
|
|
2097
|
+
out: list[TRow] = []
|
|
2098
|
+
name = line.pop(0)
|
|
2099
|
+
lid_type = line.pop(0)
|
|
2100
|
+
for chunk in range(0, len(line), 2):
|
|
2101
|
+
row: TRow = [""] * cls._ncol
|
|
2102
|
+
pollutant, removal = line[chunk : chunk + 2]
|
|
2103
|
+
row[0:4] = name, lid_type, pollutant, removal
|
|
2104
|
+
out.append(row)
|
|
2105
|
+
return out
|
|
2106
|
+
else:
|
|
2107
|
+
return super()._tabulate(line)
|
|
2108
|
+
|
|
2109
|
+
|
|
2110
|
+
class LID_Usage(SectionDf):
|
|
2111
|
+
_section_name = "LID_USAGE"
|
|
2112
|
+
_ncol = 11
|
|
2113
|
+
_headings = [
|
|
2114
|
+
"Subcatchment",
|
|
2115
|
+
"LIDProcess",
|
|
2116
|
+
"Number",
|
|
2117
|
+
"Area",
|
|
2118
|
+
"Width",
|
|
2119
|
+
"InitSat",
|
|
2120
|
+
"FromImp",
|
|
2121
|
+
"ToPerv",
|
|
2122
|
+
"RptFile",
|
|
2123
|
+
"DrainTo",
|
|
2124
|
+
"FromPerv",
|
|
2125
|
+
]
|
|
2126
|
+
|
|
2127
|
+
_index_col = ["Subcatchment", "LIDProcess"]
|
|
2128
|
+
|
|
2129
|
+
@classmethod
|
|
2130
|
+
def from_section_text(cls, text: str):
|
|
2131
|
+
return super()._from_section_text(text, cls._ncol)
|
|
2132
|
+
|
|
2133
|
+
|
|
2134
|
+
class Adjustments(SectionDf):
|
|
2135
|
+
_section_name = "ADJUSTMENTS"
|
|
2136
|
+
_ncol = 15
|
|
2137
|
+
_headings = [
|
|
2138
|
+
"Parameter",
|
|
2139
|
+
"Subcatchment",
|
|
2140
|
+
"Pattern",
|
|
2141
|
+
"Jan",
|
|
2142
|
+
"Feb",
|
|
2143
|
+
"Mar",
|
|
2144
|
+
"Apr",
|
|
2145
|
+
"May",
|
|
2146
|
+
"Jun",
|
|
2147
|
+
"Jul",
|
|
2148
|
+
"Aug",
|
|
2149
|
+
"Sep",
|
|
2150
|
+
"Oct",
|
|
2151
|
+
"Nov",
|
|
2152
|
+
"Dec",
|
|
2153
|
+
]
|
|
2154
|
+
_index_col = "Parameter"
|
|
2155
|
+
|
|
2156
|
+
@classmethod
|
|
2157
|
+
def from_section_text(cls, text: str):
|
|
2158
|
+
return super()._from_section_text(text, cls._ncol)
|
|
2159
|
+
|
|
2160
|
+
@classmethod
|
|
2161
|
+
def _tabulate(cls, line: list[str | float | int]) -> TRow | list[TRow]:
|
|
2162
|
+
out: TRow = [""] * cls._ncol
|
|
2163
|
+
out[0] = line.pop(0)
|
|
2164
|
+
if str(out[0]).lower() in ["n-perv", "dstore"]:
|
|
2165
|
+
out[1 : 1 + len(line)] = line
|
|
2166
|
+
else:
|
|
2167
|
+
start = cls._headings.index("Jan")
|
|
2168
|
+
out[start : start + len(line)] = line
|
|
2169
|
+
return out
|
|
2170
|
+
|
|
2171
|
+
|
|
2172
|
+
class Backdrop(SectionText):
|
|
2173
|
+
_section_name = "BACKDROP"
|
|
2174
|
+
|
|
2175
|
+
|
|
2176
|
+
# TODO: write custom to_string class
|
|
2177
|
+
# class Backdrop:
|
|
2178
|
+
# @classmethod
|
|
2179
|
+
# def __init__(self, text: str):
|
|
2180
|
+
# rows = text.split("\n")
|
|
2181
|
+
# data = []
|
|
2182
|
+
# line_comment = ""
|
|
2183
|
+
# for row in rows:
|
|
2184
|
+
# if not _is_data(row):
|
|
2185
|
+
# continue
|
|
2186
|
+
|
|
2187
|
+
# elif row.strip()[0] == ";":
|
|
2188
|
+
# print(row)
|
|
2189
|
+
# line_comment += row
|
|
2190
|
+
# continue
|
|
2191
|
+
|
|
2192
|
+
# line, comment = _strip_comment(row)
|
|
2193
|
+
# line_comment += comment
|
|
2194
|
+
|
|
2195
|
+
# split_data = [_coerce_numeric(val) for val in row.split()]
|
|
2196
|
+
|
|
2197
|
+
# if split_data[0].upper() == "DIMENSIONS":
|
|
2198
|
+
# self.dimensions = split_data[1:]
|
|
2199
|
+
|
|
2200
|
+
# elif split_data[0].upper() == "FILE":
|
|
2201
|
+
# self.file = split_data[1]
|
|
2202
|
+
|
|
2203
|
+
# def from_section_text(cls, text: str):
|
|
2204
|
+
# return cls(text)
|
|
2205
|
+
|
|
2206
|
+
# def __repr__(self) -> str:
|
|
2207
|
+
# return f"Backdrop(dimensions = {self.dimensions}, file = {self.file})"
|
|
2208
|
+
|
|
2209
|
+
|
|
2210
|
+
class Map(SectionText):
|
|
2211
|
+
_section_name = "MAP"
|
|
2212
|
+
|
|
2213
|
+
|
|
2214
|
+
# TODO: write custom to_string class
|
|
2215
|
+
# class Map:
|
|
2216
|
+
# @classmethod
|
|
2217
|
+
# def __init__(self, text: str):
|
|
2218
|
+
# rows = text.split("\n")
|
|
2219
|
+
# data = []
|
|
2220
|
+
# line_comment = ""
|
|
2221
|
+
# for row in rows:
|
|
2222
|
+
# if not _is_data(row):
|
|
2223
|
+
# continue
|
|
2224
|
+
|
|
2225
|
+
# elif row.strip()[0] == ";":
|
|
2226
|
+
# print(row)
|
|
2227
|
+
# line_comment += row
|
|
2228
|
+
# continue
|
|
2229
|
+
|
|
2230
|
+
# line, comment = _strip_comment(row)
|
|
2231
|
+
# line_comment += comment
|
|
2232
|
+
|
|
2233
|
+
# split_data = [_coerce_numeric(val) for val in row.split()]
|
|
2234
|
+
|
|
2235
|
+
# if split_data[0].upper() == "DIMENSIONS":
|
|
2236
|
+
# self.dimensions = split_data[1:]
|
|
2237
|
+
|
|
2238
|
+
# elif split_data[0].upper() == "UNITS":
|
|
2239
|
+
# self.units = split_data[1]
|
|
2240
|
+
|
|
2241
|
+
# @classmethod
|
|
2242
|
+
# def from_section_text(cls, text: str):
|
|
2243
|
+
# return cls(text)
|
|
2244
|
+
|
|
2245
|
+
# def __repr__(self) -> str:
|
|
2246
|
+
# return f"Map(dimensions = {self.dimensions}, units = {self.units})"
|
|
2247
|
+
|
|
2248
|
+
|
|
2249
|
+
_sections: dict[str, type[SectionBase]] = {
|
|
2250
|
+
"TITLE": Title,
|
|
2251
|
+
"OPTION": Option,
|
|
2252
|
+
"REPORT": Report,
|
|
2253
|
+
"EVENT": Event,
|
|
2254
|
+
"FILE": Files,
|
|
2255
|
+
"RAINGAGE": Raingage,
|
|
2256
|
+
"EVAP": Evap,
|
|
2257
|
+
"TEMPERATURE": Temperature,
|
|
2258
|
+
"ADJUSTMENT": Adjustments,
|
|
2259
|
+
"SUBCATCHMENT": Subcatchment,
|
|
2260
|
+
"SUBAREA": Subarea,
|
|
2261
|
+
"INFIL": Infil,
|
|
2262
|
+
"LID_CONTROL": LID_Control,
|
|
2263
|
+
"LID_USAGE": LID_Usage,
|
|
2264
|
+
"AQUIFER": Aquifer,
|
|
2265
|
+
"GROUNDWATER": Groundwater,
|
|
2266
|
+
"GWF": GWF,
|
|
2267
|
+
"SNOWPACK": Snowpack,
|
|
2268
|
+
"JUNC": Junc,
|
|
2269
|
+
"OUTFALL": Outfall,
|
|
2270
|
+
"DIVIDER": Divider,
|
|
2271
|
+
"STORAGE": Storage,
|
|
2272
|
+
"CONDUIT": Conduit,
|
|
2273
|
+
"PUMP": Pump,
|
|
2274
|
+
"ORIFICE": Orifice,
|
|
2275
|
+
"WEIR": Weir,
|
|
2276
|
+
"OUTLET": Outlet,
|
|
2277
|
+
"XSECT": Xsections,
|
|
2278
|
+
# TODO build parser for this table
|
|
2279
|
+
"TRANSECT": Transects,
|
|
2280
|
+
"STREETS": Street,
|
|
2281
|
+
"INLET_USAGE": Inlet_Usage,
|
|
2282
|
+
"INLET": Inlet,
|
|
2283
|
+
"LOSS": Losses,
|
|
2284
|
+
# TODO build parser for this table
|
|
2285
|
+
"CONTROL": Controls,
|
|
2286
|
+
"POLLUT": Pollutants,
|
|
2287
|
+
"LANDUSE": LandUse,
|
|
2288
|
+
"COVERAGE": Coverage,
|
|
2289
|
+
"LOADING": Loading,
|
|
2290
|
+
"BUILDUP": Buildup,
|
|
2291
|
+
"WASHOFF": Washoff,
|
|
2292
|
+
"TREATMENT": Treatment,
|
|
2293
|
+
"INFLOW": Inflow,
|
|
2294
|
+
"DWF": DWF,
|
|
2295
|
+
"RDII": RDII,
|
|
2296
|
+
"HYDROGRAPH": Hydrographs,
|
|
2297
|
+
"CURVE": Curves,
|
|
2298
|
+
"TIMESERIES": Timeseries,
|
|
2299
|
+
"PATTERN": Patterns,
|
|
2300
|
+
"MAP": Map,
|
|
2301
|
+
"POLYGON": Polygons,
|
|
2302
|
+
"COORDINATE": Coordinates,
|
|
2303
|
+
"VERTICES": Vertices,
|
|
2304
|
+
"LABEL": Labels,
|
|
2305
|
+
"SYMBOL": Symbols,
|
|
2306
|
+
"BACKDROP": Backdrop,
|
|
2307
|
+
"PROFILE": Profile,
|
|
2308
|
+
"TAG": Tags,
|
|
2309
|
+
}
|