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.
@@ -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())
@@ -0,0 +1,2 @@
1
+ from swmm.pandas.output.output import Output
2
+ from swmm.pandas.output.structure import Structure