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,403 @@
|
|
|
1
|
+
# %%
|
|
2
|
+
# swmm-pandas input
|
|
3
|
+
# scope:
|
|
4
|
+
# - high level api for loading, inspecting, changing, and
|
|
5
|
+
# altering a SWMM input file using pandas dataframes
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from swmm.pandas.input._section_classes import SectionBase, SectionDf, _sections
|
|
9
|
+
from swmm.pandas.input.input import InputFile
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
import swmm.pandas.input._section_classes as sc
|
|
13
|
+
import pathlib
|
|
14
|
+
import re
|
|
15
|
+
from typing import Optional, Callable, Any, TypeVar
|
|
16
|
+
import warnings
|
|
17
|
+
import copy
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def object_hasattr(obj: Any, name: str):
|
|
24
|
+
try:
|
|
25
|
+
object.__getattribute__(obj, name)
|
|
26
|
+
return True
|
|
27
|
+
except AttributeError:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def object_getattr(obj: Any, name: str):
|
|
32
|
+
return object.__getattribute__(obj, name)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NoAssignmentError(Exception):
|
|
36
|
+
def __init__(self, prop_name):
|
|
37
|
+
self.prop_name = prop_name
|
|
38
|
+
|
|
39
|
+
def __str__(self) -> str:
|
|
40
|
+
return f"Cannot assign '{self.prop_name}' property, only mutation is allowed."
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class NoAccessError(Exception):
|
|
44
|
+
def __init__(self, prop_name):
|
|
45
|
+
self.prop_name = prop_name
|
|
46
|
+
|
|
47
|
+
def __str__(self) -> str:
|
|
48
|
+
return (
|
|
49
|
+
f"Cannot directly edit '{self.prop_name}' property in the Input object.\n"
|
|
50
|
+
f"Use the associated node/link table or use the InputFile object for lower level control. "
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def no_setter_property(func: Callable[[Any], T]) -> property:
|
|
55
|
+
|
|
56
|
+
def readonly_setter(self: Any, obj: Any) -> None:
|
|
57
|
+
raise NoAssignmentError(func.__name__)
|
|
58
|
+
|
|
59
|
+
return property(fget=func, fset=readonly_setter, doc=func.__doc__)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Input:
|
|
63
|
+
|
|
64
|
+
def __init__(self, inpfile: Optional[str | Input] = None):
|
|
65
|
+
if isinstance(inpfile, InputFile):
|
|
66
|
+
self.inp = inpfile
|
|
67
|
+
elif isinstance(inpfile, str | pathlib.Path):
|
|
68
|
+
self.inp = InputFile(inpfile)
|
|
69
|
+
|
|
70
|
+
# def __getattribute__(self, name: str) -> Any:
|
|
71
|
+
# _self_no_access = [
|
|
72
|
+
# "tags",
|
|
73
|
+
# "dwf",
|
|
74
|
+
# "inflow",
|
|
75
|
+
# "rdii",
|
|
76
|
+
# "losses",
|
|
77
|
+
# "xsections",
|
|
78
|
+
# ]
|
|
79
|
+
|
|
80
|
+
# if name in _self_no_access:
|
|
81
|
+
# raise NoAccessError(name)
|
|
82
|
+
|
|
83
|
+
# elif object_hasattr(self, name):
|
|
84
|
+
# return object_getattr(self, name)
|
|
85
|
+
|
|
86
|
+
# elif object_hasattr(InputFile, name):
|
|
87
|
+
# return object_getattr(object_getattr(self, "inp"), name)
|
|
88
|
+
# else:
|
|
89
|
+
# raise AttributeError(f"'Input' object has no attribute '{name}'")
|
|
90
|
+
|
|
91
|
+
##########################################################
|
|
92
|
+
# region General df constructors and destructors #########
|
|
93
|
+
##########################################################
|
|
94
|
+
|
|
95
|
+
# destructors
|
|
96
|
+
def _general_destructor(
|
|
97
|
+
self, inp_frame: pd.DataFrame, output_frames: list[SectionDf]
|
|
98
|
+
) -> None:
|
|
99
|
+
for output_frame in output_frames:
|
|
100
|
+
output_frame_name = output_frame.__class__.__name__.lower()
|
|
101
|
+
cols = output_frame._data_cols(desc=False)
|
|
102
|
+
inp_df = inp_frame.loc[:, cols]
|
|
103
|
+
out_df = copy.deepcopy(output_frame)
|
|
104
|
+
|
|
105
|
+
out_df = out_df.reindex(
|
|
106
|
+
out_df.index.union(inp_df.index).rename(out_df.index.name)
|
|
107
|
+
)
|
|
108
|
+
out_df.loc[inp_df.index, cols] = inp_df[list(cols)]
|
|
109
|
+
out_df = out_df.dropna(how="all")
|
|
110
|
+
setattr(self.inp, output_frame_name, out_df)
|
|
111
|
+
|
|
112
|
+
def _destruct_tags(
|
|
113
|
+
self,
|
|
114
|
+
input_frame: pd.DataFrame,
|
|
115
|
+
element_type: str,
|
|
116
|
+
) -> None:
|
|
117
|
+
tag_df = self._extract_table_and_restore_multi_index(
|
|
118
|
+
input_frame=input_frame,
|
|
119
|
+
input_index_name="Name",
|
|
120
|
+
output_frame=self.inp.tags,
|
|
121
|
+
prepend=[("Element", element_type)],
|
|
122
|
+
)
|
|
123
|
+
self.inp.tags = tag_df
|
|
124
|
+
|
|
125
|
+
def _extract_table_and_restore_multi_index(
|
|
126
|
+
self,
|
|
127
|
+
input_frame: pd.DataFrame,
|
|
128
|
+
input_index_name: str,
|
|
129
|
+
output_frame: pd.DataFrame,
|
|
130
|
+
prepend: list[tuple[str, str]] = [],
|
|
131
|
+
append: list[tuple[str, str]] = [],
|
|
132
|
+
) -> pd.DataFrame:
|
|
133
|
+
cols = output_frame._data_cols(desc=False)
|
|
134
|
+
inp_df = input_frame.loc[:, cols]
|
|
135
|
+
out_df = copy.deepcopy(output_frame)
|
|
136
|
+
levels = [pd.Index([val], name=nom) for nom, val in prepend]
|
|
137
|
+
levels += [inp_df.index.rename(input_index_name)]
|
|
138
|
+
levels += [pd.Index([val], name=nom) for nom, val in append]
|
|
139
|
+
|
|
140
|
+
new_idx = pd.MultiIndex.from_product(levels)
|
|
141
|
+
inp_df.index = new_idx
|
|
142
|
+
|
|
143
|
+
out_df = out_df.reindex(out_df.index.union(inp_df.index))
|
|
144
|
+
out_df.loc[inp_df.index, cols] = inp_df[cols]
|
|
145
|
+
out_df = out_df.dropna(how="all")
|
|
146
|
+
return out_df
|
|
147
|
+
|
|
148
|
+
# constructors
|
|
149
|
+
def _general_constructor(self, inp_frames: list[SectionDf]) -> pd.DataFrame:
|
|
150
|
+
left = inp_frames.pop(0).drop("desc", axis=1)
|
|
151
|
+
for right in inp_frames:
|
|
152
|
+
left = pd.merge(
|
|
153
|
+
left,
|
|
154
|
+
right.drop("desc", axis=1),
|
|
155
|
+
left_index=True,
|
|
156
|
+
right_index=True,
|
|
157
|
+
how="left",
|
|
158
|
+
)
|
|
159
|
+
return left
|
|
160
|
+
|
|
161
|
+
# endregion General df constructors and destructors ######
|
|
162
|
+
|
|
163
|
+
# %% ###########################
|
|
164
|
+
# region Generalized NODES #####
|
|
165
|
+
################################
|
|
166
|
+
|
|
167
|
+
def _node_constructor(self, inp_df: SectionDf) -> pd.DataFrame:
|
|
168
|
+
return self._general_constructor(
|
|
169
|
+
[
|
|
170
|
+
inp_df,
|
|
171
|
+
self.inp.dwf.loc[(slice(None), slice("FLOW", "FLOW")), :].droplevel(
|
|
172
|
+
"Constituent"
|
|
173
|
+
),
|
|
174
|
+
self.inp.inflow.loc[(slice(None), slice("FLOW", "FLOW")), :].droplevel(
|
|
175
|
+
"Constituent"
|
|
176
|
+
),
|
|
177
|
+
self.inp.rdii,
|
|
178
|
+
self.inp.tags.loc[slice("Node", "Node"), slice(None)].droplevel(
|
|
179
|
+
"Element"
|
|
180
|
+
),
|
|
181
|
+
self.inp.coordinates,
|
|
182
|
+
]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def _node_destructor(self, inp_df: pd.DataFrame, out_df: SectionDf) -> None:
|
|
186
|
+
self._general_destructor(
|
|
187
|
+
inp_df,
|
|
188
|
+
[
|
|
189
|
+
out_df,
|
|
190
|
+
self.inp.rdii,
|
|
191
|
+
self.inp.coordinates,
|
|
192
|
+
],
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
self._destruct_tags(inp_df, "Node")
|
|
196
|
+
|
|
197
|
+
self.inp.dwf = self._extract_table_and_restore_multi_index(
|
|
198
|
+
input_frame=inp_df,
|
|
199
|
+
input_index_name="Node",
|
|
200
|
+
output_frame=self.inp.dwf,
|
|
201
|
+
append=[("Constituent", "FLOW")],
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
self.inp.inflow = self._extract_table_and_restore_multi_index(
|
|
205
|
+
input_frame=inp_df,
|
|
206
|
+
input_index_name="Node",
|
|
207
|
+
output_frame=self.inp.inflow,
|
|
208
|
+
append=[("Constituent", "FLOW")],
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# endregion NODES and LINKS ######
|
|
212
|
+
|
|
213
|
+
# %% ###########################
|
|
214
|
+
# region MAIN TABLES ###########
|
|
215
|
+
################################
|
|
216
|
+
|
|
217
|
+
######### JUNCTIONS #########
|
|
218
|
+
@no_setter_property
|
|
219
|
+
def junc(self) -> pd.DataFrame:
|
|
220
|
+
if not hasattr(self, "_junc_full"):
|
|
221
|
+
self._junc_full = self._node_constructor(self.inp.junc)
|
|
222
|
+
|
|
223
|
+
return self._junc_full
|
|
224
|
+
|
|
225
|
+
def _junction_destructor(self) -> None:
|
|
226
|
+
if hasattr(self, "_junc_full"):
|
|
227
|
+
self._node_destructor(self.junc, self.inp.junc)
|
|
228
|
+
|
|
229
|
+
######## OUTFALLS #########
|
|
230
|
+
@no_setter_property
|
|
231
|
+
def outfall(self) -> pd.DataFrame:
|
|
232
|
+
if not hasattr(self, "_outfall_full"):
|
|
233
|
+
self._outfall_full = self._node_constructor(self.inp.outfall)
|
|
234
|
+
|
|
235
|
+
return self._outfall_full
|
|
236
|
+
|
|
237
|
+
def _outfall_destructor(self) -> None:
|
|
238
|
+
if hasattr(self, "_outfall_full"):
|
|
239
|
+
self._node_destructor(self.outfall, self.inp.outfall)
|
|
240
|
+
|
|
241
|
+
######## STORAGE #########
|
|
242
|
+
@no_setter_property
|
|
243
|
+
def storage(self):
|
|
244
|
+
if not hasattr(self, "_storage_full"):
|
|
245
|
+
self._storage_full = self._node_constructor(self.inp.storage)
|
|
246
|
+
|
|
247
|
+
return self._storage_full
|
|
248
|
+
|
|
249
|
+
def _storage_destructor(self) -> None:
|
|
250
|
+
if hasattr(self, "_storage_full"):
|
|
251
|
+
self._node_destructor(self.storage, self.inp.storage)
|
|
252
|
+
|
|
253
|
+
######## DIVIDER #########
|
|
254
|
+
@no_setter_property
|
|
255
|
+
def divider(self):
|
|
256
|
+
if not hasattr(self, "_divider_full"):
|
|
257
|
+
self._divider_full = self._node_constructor(self.inp.divider)
|
|
258
|
+
|
|
259
|
+
return self._storage_full
|
|
260
|
+
|
|
261
|
+
def _storage_destructor(self) -> None:
|
|
262
|
+
if hasattr(self, "_divider_full"):
|
|
263
|
+
self._node_destructor(self.divider, self.inp.divider)
|
|
264
|
+
|
|
265
|
+
######### CONDUITS #########
|
|
266
|
+
@no_setter_property
|
|
267
|
+
def conduit(self) -> pd.DataFrame:
|
|
268
|
+
if not hasattr(self, "_conduit_full"):
|
|
269
|
+
self._conduit_full = self._general_constructor(
|
|
270
|
+
[
|
|
271
|
+
self.inp.conduit,
|
|
272
|
+
self.inp.losses,
|
|
273
|
+
self.inp.xsections,
|
|
274
|
+
self.inp.tags.loc[slice("Link", "Link"), slice(None)].droplevel(0),
|
|
275
|
+
]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return self._conduit_full
|
|
279
|
+
|
|
280
|
+
def _conduit_destructor(self) -> None:
|
|
281
|
+
if hasattr(self, "_conduit_full"):
|
|
282
|
+
self._general_destructor(
|
|
283
|
+
self.conduit,
|
|
284
|
+
[
|
|
285
|
+
self.inp.conduit,
|
|
286
|
+
self.inp.losses,
|
|
287
|
+
self.inp.xsections,
|
|
288
|
+
],
|
|
289
|
+
)
|
|
290
|
+
self._destruct_tags(self.conduit, "Link")
|
|
291
|
+
|
|
292
|
+
######## PUMPS #########
|
|
293
|
+
@no_setter_property
|
|
294
|
+
def pump(self) -> pd.DataFrame:
|
|
295
|
+
if not hasattr(self, "_pump_full"):
|
|
296
|
+
self._pump_full = self._general_constructor(
|
|
297
|
+
[
|
|
298
|
+
self.inp.pump,
|
|
299
|
+
self.inp.tags.loc[slice("Link", "Link"), slice(None)].droplevel(0),
|
|
300
|
+
]
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
return self._pump_full
|
|
304
|
+
|
|
305
|
+
def _pump_destructor(self) -> None:
|
|
306
|
+
if hasattr(self, "_pump_full"):
|
|
307
|
+
self._general_destructor(
|
|
308
|
+
self.pump,
|
|
309
|
+
[
|
|
310
|
+
self.inp.pump,
|
|
311
|
+
],
|
|
312
|
+
)
|
|
313
|
+
self._destruct_tags(self.pump, "Link")
|
|
314
|
+
|
|
315
|
+
######## WEIRS #########
|
|
316
|
+
@no_setter_property
|
|
317
|
+
def weir(self) -> pd.DataFrame:
|
|
318
|
+
if not hasattr(self, "_weir_full"):
|
|
319
|
+
self._weir_full = self._general_constructor(
|
|
320
|
+
[
|
|
321
|
+
self.inp.weir,
|
|
322
|
+
self.inp.tags.loc[slice("Link", "Link"), slice(None)].droplevel(0),
|
|
323
|
+
]
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
return self._weir_full
|
|
327
|
+
|
|
328
|
+
def _weir_destructor(self) -> None:
|
|
329
|
+
if hasattr(self, "_weir_full"):
|
|
330
|
+
self._general_destructor(
|
|
331
|
+
self.weir,
|
|
332
|
+
[
|
|
333
|
+
self.inp.weir,
|
|
334
|
+
],
|
|
335
|
+
)
|
|
336
|
+
self._destruct_tags(self.weir, "Link")
|
|
337
|
+
|
|
338
|
+
######## ORIFICES #########
|
|
339
|
+
@no_setter_property
|
|
340
|
+
def orifice(self) -> pd.DataFrame:
|
|
341
|
+
if not hasattr(self, "_orifice_full"):
|
|
342
|
+
self._orifice_full = self._general_constructor(
|
|
343
|
+
[
|
|
344
|
+
self.inp.orifice,
|
|
345
|
+
self.inp.tags.loc[slice("Link", "Link"), slice(None)].droplevel(0),
|
|
346
|
+
]
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return self._orifice_full
|
|
350
|
+
|
|
351
|
+
def _orifice_destructor(self) -> None:
|
|
352
|
+
if hasattr(self, "_orifice_full"):
|
|
353
|
+
self._general_destructor(
|
|
354
|
+
self.orifice,
|
|
355
|
+
[
|
|
356
|
+
self.inp.orifice,
|
|
357
|
+
],
|
|
358
|
+
)
|
|
359
|
+
self._destruct_tags(self.orifice, "Link")
|
|
360
|
+
|
|
361
|
+
######## OULETS #########
|
|
362
|
+
@no_setter_property
|
|
363
|
+
def outlet(self) -> pd.DataFrame:
|
|
364
|
+
if not hasattr(self, "_outlet_full"):
|
|
365
|
+
self._outlet_full = self._general_constructor(
|
|
366
|
+
[
|
|
367
|
+
self.outlet,
|
|
368
|
+
self.inp.tags.loc[slice("Link", "Link"), slice(None)].droplevel(0),
|
|
369
|
+
]
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
return self._outlet_full
|
|
373
|
+
|
|
374
|
+
def _outlet_destructor(self) -> None:
|
|
375
|
+
if hasattr(self, "_outlet_full"):
|
|
376
|
+
self._general_destructor(
|
|
377
|
+
self.outlet,
|
|
378
|
+
[
|
|
379
|
+
self.inp.outlet,
|
|
380
|
+
],
|
|
381
|
+
)
|
|
382
|
+
self._destruct_tags(self.outlet, "Link")
|
|
383
|
+
|
|
384
|
+
####### SUBCATCHMENTS
|
|
385
|
+
# endregion MAIN TABLES ######
|
|
386
|
+
|
|
387
|
+
def _sync(self):
|
|
388
|
+
# nodes
|
|
389
|
+
self._junction_destructor()
|
|
390
|
+
self._outfall_destructor()
|
|
391
|
+
self._storage_destructor()
|
|
392
|
+
|
|
393
|
+
# links
|
|
394
|
+
self._conduit_destructor()
|
|
395
|
+
self._pump_destructor()
|
|
396
|
+
self._orifice_destructor()
|
|
397
|
+
self._weir_destructor()
|
|
398
|
+
self._outlet_destructor()
|
|
399
|
+
|
|
400
|
+
def to_file(self, path: str | pathlib.Path):
|
|
401
|
+
self._sync()
|
|
402
|
+
with open(path, "w") as f:
|
|
403
|
+
f.write(self.inp.to_string())
|