easysewer 0.0.1__py3-none-any.whl → 0.0.3__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.
- easysewer/Area.py +617 -159
- easysewer/Link.py +461 -151
- easysewer/ModelAPI.py +194 -0
- easysewer/Node.py +971 -190
- easysewer/Rain.py +108 -19
- easysewer/UDM.py +47 -22
- easysewer/__init__.py +1 -1
- {easysewer-0.0.1.dist-info → easysewer-0.0.3.dist-info}/METADATA +1 -1
- easysewer-0.0.3.dist-info/RECORD +20 -0
- {easysewer-0.0.1.dist-info → easysewer-0.0.3.dist-info}/WHEEL +1 -1
- easysewer-0.0.1.dist-info/RECORD +0 -19
- {easysewer-0.0.1.dist-info → easysewer-0.0.3.dist-info}/top_level.txt +0 -0
easysewer/Node.py
CHANGED
@@ -21,6 +21,7 @@ class Node:
|
|
21
21
|
coordinate (list): [x, y] coordinates of the node
|
22
22
|
elevation (float): Node invert elevation
|
23
23
|
"""
|
24
|
+
|
24
25
|
def __init__(self):
|
25
26
|
self.name = ''
|
26
27
|
self.coordinate = [0.0, 0.0]
|
@@ -46,6 +47,7 @@ class Junction(Node):
|
|
46
47
|
dwf_patterns (list): Time patterns for dry weather flow
|
47
48
|
inflow (dict): Inflow characteristics and time series
|
48
49
|
"""
|
50
|
+
|
49
51
|
def __init__(self):
|
50
52
|
Node.__init__(self)
|
51
53
|
self.maximum_depth = 0
|
@@ -72,6 +74,7 @@ class Outfall(Node):
|
|
72
74
|
flap_gate (bool): Whether backflow prevention is present
|
73
75
|
route_to (str): Routing destination for diverted flow
|
74
76
|
"""
|
77
|
+
|
75
78
|
def __init__(self):
|
76
79
|
Node.__init__(self)
|
77
80
|
self.flap_gate = False
|
@@ -79,44 +82,246 @@ class Outfall(Node):
|
|
79
82
|
|
80
83
|
|
81
84
|
class OutfallFree(Outfall):
|
85
|
+
"""
|
86
|
+
Free outfall node type.
|
87
|
+
|
88
|
+
Represents an outfall with free boundary condition where water can freely exit the system.
|
89
|
+
|
90
|
+
Attributes:
|
91
|
+
Inherits all attributes from Outfall class
|
92
|
+
"""
|
93
|
+
|
82
94
|
def __init__(self):
|
83
95
|
Outfall.__init__(self)
|
84
96
|
|
85
97
|
|
86
98
|
class OutfallNormal(Outfall):
|
99
|
+
"""
|
100
|
+
Normal outfall node type.
|
101
|
+
|
102
|
+
Represents an outfall with normal boundary condition where water exits with normal depth.
|
103
|
+
|
104
|
+
Attributes:
|
105
|
+
Inherits all attributes from Outfall class
|
106
|
+
"""
|
107
|
+
|
87
108
|
def __init__(self):
|
88
109
|
Outfall.__init__(self)
|
89
110
|
|
90
111
|
|
91
112
|
class OutfallFixed(Outfall):
|
113
|
+
"""
|
114
|
+
Fixed outfall node type.
|
115
|
+
|
116
|
+
Represents an outfall with fixed boundary condition where water exits at a fixed stage.
|
117
|
+
|
118
|
+
Attributes:
|
119
|
+
Inherits all attributes from Outfall class
|
120
|
+
stage (float): Fixed water surface elevation at the outfall
|
121
|
+
"""
|
122
|
+
|
92
123
|
def __init__(self):
|
93
124
|
Outfall.__init__(self)
|
94
125
|
self.stage = 0.0
|
95
126
|
|
96
127
|
|
97
128
|
class OutfallTidal(Outfall):
|
129
|
+
"""
|
130
|
+
Tidal outfall node type.
|
131
|
+
|
132
|
+
Represents an outfall with tidal boundary condition where water level varies with tides.
|
133
|
+
|
134
|
+
Attributes:
|
135
|
+
Inherits all attributes from Outfall class
|
136
|
+
tidal (str): Tidal condition identifier or time series name
|
137
|
+
"""
|
138
|
+
|
98
139
|
def __init__(self):
|
99
140
|
Outfall.__init__(self)
|
100
141
|
self.tidal = ''
|
101
142
|
|
102
143
|
|
103
144
|
class OutfallTimeseries(Outfall):
|
145
|
+
"""
|
146
|
+
Timeseries outfall node type.
|
147
|
+
|
148
|
+
Represents an outfall with time-varying boundary condition specified by a time series.
|
149
|
+
|
150
|
+
Attributes:
|
151
|
+
Inherits all attributes from Outfall class
|
152
|
+
time_series (str): Name of time series defining water surface elevation
|
153
|
+
"""
|
154
|
+
|
104
155
|
def __init__(self):
|
105
156
|
Outfall.__init__(self)
|
106
157
|
self.time_series = ''
|
107
158
|
|
108
159
|
|
160
|
+
class Polygon:
|
161
|
+
"""
|
162
|
+
Geometric representation of a node's boundary.
|
163
|
+
|
164
|
+
Stores the polygon vertices that define the boundary of a node.
|
165
|
+
|
166
|
+
Attributes:
|
167
|
+
node_name (str): Name of the associated node
|
168
|
+
x (list): List of x-coordinates of polygon vertices
|
169
|
+
y (list): List of y-coordinates of polygon vertices
|
170
|
+
"""
|
171
|
+
|
172
|
+
def __init__(self):
|
173
|
+
self.node_name = None
|
174
|
+
self.x = []
|
175
|
+
self.y = []
|
176
|
+
|
177
|
+
|
178
|
+
class Storage(Node):
|
179
|
+
"""
|
180
|
+
Base class for storage nodes in the drainage network.
|
181
|
+
|
182
|
+
Represents a storage unit in the drainage network with volume and depth properties.
|
183
|
+
|
184
|
+
Attributes:
|
185
|
+
maximum_depth (float): Maximum water depth in storage unit
|
186
|
+
initial_depth (float): Initial water depth at start of simulation
|
187
|
+
overload_depth (float): Depth above which overflows occur
|
188
|
+
evaporation_factor (float): Factor adjusting evaporation rate
|
189
|
+
seepage_suction_head (float): Soil suction head for seepage calculations
|
190
|
+
seepage_conductivity (float): Soil hydraulic conductivity for seepage
|
191
|
+
seepage_initial_deficit (float): Initial soil moisture deficit
|
192
|
+
inflow (dict): Inflow characteristics and time series
|
193
|
+
polygon (Polygon): Geometric representation of the storage node boundary
|
194
|
+
"""
|
195
|
+
|
196
|
+
def __init__(self):
|
197
|
+
Node.__init__(self)
|
198
|
+
self.maximum_depth = 0
|
199
|
+
self.initial_depth = 0
|
200
|
+
self.overload_depth = 0
|
201
|
+
self.evaporation_factor = 0
|
202
|
+
self.seepage_suction_head = None
|
203
|
+
self.seepage_conductivity = None
|
204
|
+
self.seepage_initial_deficit = None
|
205
|
+
#
|
206
|
+
# inflow
|
207
|
+
self.inflow = None
|
208
|
+
#
|
209
|
+
# polygon boundary
|
210
|
+
self.polygon = Polygon()
|
211
|
+
|
212
|
+
|
213
|
+
class StorageFunctional(Storage):
|
214
|
+
"""
|
215
|
+
Functional storage node type.
|
216
|
+
|
217
|
+
Represents a storage unit with volume defined by a functional relationship.
|
218
|
+
|
219
|
+
Attributes:
|
220
|
+
Inherits all attributes from Storage class
|
221
|
+
constant (float): Constant term in functional relationship
|
222
|
+
coefficient (float): Coefficient in functional relationship
|
223
|
+
exponent (float): Exponent in functional relationship
|
224
|
+
"""
|
225
|
+
|
226
|
+
def __init__(self):
|
227
|
+
Storage.__init__(self)
|
228
|
+
self.constant = 0.0
|
229
|
+
self.coefficient = 0.0
|
230
|
+
self.exponent = 0.0
|
231
|
+
|
232
|
+
|
233
|
+
class StorageCylindrical(Storage):
|
234
|
+
"""
|
235
|
+
Cylindrical storage node type.
|
236
|
+
|
237
|
+
Represents a storage unit with cylindrical shape.
|
238
|
+
|
239
|
+
Attributes:
|
240
|
+
Inherits all attributes from Storage class
|
241
|
+
major_axis_length (float): Length of major axis
|
242
|
+
minor_axis_length (float): Length of minor axis
|
243
|
+
"""
|
244
|
+
|
245
|
+
def __init__(self):
|
246
|
+
Storage.__init__(self)
|
247
|
+
self.major_axis_length = 0.0
|
248
|
+
self.minor_axis_length = 0.0
|
249
|
+
|
250
|
+
|
251
|
+
class StorageTabular(Storage):
|
252
|
+
"""
|
253
|
+
Tabular storage node type.
|
254
|
+
|
255
|
+
Represents a storage unit with volume defined by a tabular curve.
|
256
|
+
|
257
|
+
Attributes:
|
258
|
+
Inherits all attributes from Storage class
|
259
|
+
storage_curve_name (str): Name of curve defining storage volume
|
260
|
+
"""
|
261
|
+
|
262
|
+
def __init__(self):
|
263
|
+
Storage.__init__(self)
|
264
|
+
self.storage_curve_name = ''
|
265
|
+
|
266
|
+
|
109
267
|
class NodeList:
|
268
|
+
"""
|
269
|
+
A collection class for managing nodes in a drainage network.
|
270
|
+
|
271
|
+
This class provides storage and management for various types of nodes (junctions, outfalls),
|
272
|
+
with methods for adding, accessing, and processing nodes. It maintains spatial bounds
|
273
|
+
information for all contained nodes.
|
274
|
+
|
275
|
+
Attributes:
|
276
|
+
data (list): List containing all node objects
|
277
|
+
bounds (dict): Dictionary tracking spatial bounds of all nodes with keys:
|
278
|
+
'min_x' (float): Minimum x-coordinate
|
279
|
+
'min_y' (float): Minimum y-coordinate
|
280
|
+
'max_x' (float): Maximum x-coordinate
|
281
|
+
'max_y' (float): Maximum y-coordinate
|
282
|
+
"""
|
283
|
+
|
110
284
|
def __init__(self):
|
111
285
|
self.data = []
|
286
|
+
self.bounds = {
|
287
|
+
'min_x': float('inf'),
|
288
|
+
'min_y': float('inf'),
|
289
|
+
'max_x': float('-inf'),
|
290
|
+
'max_y': float('-inf')
|
291
|
+
}
|
112
292
|
|
113
293
|
def __repr__(self):
|
294
|
+
"""
|
295
|
+
Returns a string representation of the NodeList.
|
296
|
+
|
297
|
+
Returns:
|
298
|
+
str: A string showing the count of nodes in the list
|
299
|
+
"""
|
114
300
|
return f'{len(self.data)} Nodes'
|
115
301
|
|
116
302
|
def __len__(self):
|
303
|
+
"""
|
304
|
+
Returns the number of nodes in the list.
|
305
|
+
|
306
|
+
Returns:
|
307
|
+
int: Number of nodes in the list
|
308
|
+
"""
|
117
309
|
return len(self.data)
|
118
310
|
|
119
311
|
def __getitem__(self, key):
|
312
|
+
"""
|
313
|
+
Gets a node by index or name.
|
314
|
+
|
315
|
+
Args:
|
316
|
+
key (int|str): Index or name of node to retrieve
|
317
|
+
|
318
|
+
Returns:
|
319
|
+
Node: The requested node
|
320
|
+
|
321
|
+
Raises:
|
322
|
+
KeyError: If node name not found
|
323
|
+
TypeError: If key is not int or str
|
324
|
+
"""
|
120
325
|
if isinstance(key, int):
|
121
326
|
return self.data[key]
|
122
327
|
elif isinstance(key, str):
|
@@ -128,219 +333,795 @@ class NodeList:
|
|
128
333
|
raise TypeError("Key must be an integer or a string")
|
129
334
|
|
130
335
|
def __iter__(self):
|
336
|
+
"""
|
337
|
+
Returns an iterator for the node list.
|
338
|
+
|
339
|
+
Returns:
|
340
|
+
iterator: Iterator for the nodes
|
341
|
+
"""
|
131
342
|
return iter(self.data)
|
132
343
|
|
133
344
|
def __contains__(self, item):
|
345
|
+
"""
|
346
|
+
Checks if a node exists in the list.
|
347
|
+
|
348
|
+
Args:
|
349
|
+
item: Node to check for
|
350
|
+
|
351
|
+
Returns:
|
352
|
+
bool: True if node exists in list
|
353
|
+
"""
|
134
354
|
return item in self.data
|
135
355
|
|
136
|
-
def add_node(self, node_type, node_information):
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
356
|
+
def add_node(self, node_type, node_information=None):
|
357
|
+
"""
|
358
|
+
Add a new node to the data structure based on its type and information.
|
359
|
+
Generates default values for missing essential attributes.
|
360
|
+
|
361
|
+
Args:
|
362
|
+
node_type (str): Type of node to add (e.g., 'junction', 'outfall_free')
|
363
|
+
node_information (dict, optional): Dictionary containing node attributes
|
364
|
+
Defaults to empty dict if None
|
365
|
+
|
366
|
+
Returns:
|
367
|
+
Node: The newly created node object
|
368
|
+
|
369
|
+
Raises:
|
370
|
+
TypeError: If node_type is not recognized
|
371
|
+
ValueError: If a node with the same name already exists
|
372
|
+
or generated default name conflicts
|
373
|
+
|
374
|
+
Example:
|
375
|
+
>>> nodes = NodeList()
|
376
|
+
>>> nodes.add_node('junction', {'name': 'J1', 'elevation': 100.0})
|
377
|
+
<Node J1>
|
378
|
+
"""
|
379
|
+
# Initialize node_information dict if not provided
|
380
|
+
if node_information is None:
|
381
|
+
node_information = {}
|
382
|
+
|
383
|
+
# Normalize node type: lowercase and remove underscores
|
384
|
+
normalized_type = node_type.lower().replace('_', '')
|
385
|
+
|
386
|
+
# Check if a name is provided and if it already exists in the collection
|
387
|
+
if 'name' in node_information:
|
388
|
+
requested_name = node_information['name']
|
389
|
+
if any(node.name == requested_name for node in self.data):
|
390
|
+
raise ValueError(f"Node with name '{requested_name}' already exists")
|
391
|
+
|
392
|
+
# Define attribute hierarchy based on class inheritance
|
393
|
+
# Level 1: Common attributes for all Node types with defaults
|
394
|
+
node_base_attrs = {
|
395
|
+
'name': lambda node_type, info: info.get('name', self._generate_default_name(node_type)),
|
396
|
+
'coordinate': lambda _, info: info.get('coordinate', self._generate_default_coordinate()),
|
397
|
+
'elevation': lambda _, info: info.get('elevation', 0.0)
|
398
|
+
}
|
399
|
+
|
400
|
+
# Level 2: Attributes by node category with defaults
|
401
|
+
junction_attrs = {
|
402
|
+
'maximum_depth': lambda _, info: info.get('maximum_depth', 10.0),
|
403
|
+
'initial_depth': lambda _, info: info.get('initial_depth', 0.0),
|
404
|
+
'overload_depth': lambda _, info: info.get('overload_depth', 0.0),
|
405
|
+
'surface_ponding_area': lambda _, info: info.get('surface_ponding_area', 0.0),
|
406
|
+
'dwf_base_value': lambda _, info: info.get('dwf_base_value', 0.0),
|
407
|
+
'dwf_patterns': lambda _, info: info.get('dwf_patterns', None)
|
408
|
+
}
|
409
|
+
|
410
|
+
outfall_base_attrs = {
|
411
|
+
'flap_gate': lambda _, info: True if info.get('flap_gate') in ('YES', True) else False,
|
412
|
+
'route_to': lambda _, info: info.get('route_to', None)
|
413
|
+
}
|
414
|
+
|
415
|
+
# Level 3: Specific attributes for outfall subtypes with defaults
|
416
|
+
outfall_specific_attrs = {
|
417
|
+
'outfallfixed': {
|
418
|
+
'stage': lambda _, info: info.get('stage', 0.0)
|
419
|
+
},
|
420
|
+
'outfalltidal': {
|
421
|
+
'tidal': lambda _, info: info.get('tidal', None)
|
422
|
+
},
|
423
|
+
'outfalltimeseries': {
|
424
|
+
'time_series': lambda _, info: info.get('time_series', None)
|
425
|
+
},
|
426
|
+
'outfallfree': {},
|
427
|
+
'outfallnormal': {}
|
428
|
+
}
|
429
|
+
|
430
|
+
# Level 2: Attributes for storage nodes with defaults
|
431
|
+
storage_attrs = {
|
432
|
+
'maximum_depth': lambda _, info: info.get('maximum_depth', 10.0),
|
433
|
+
'initial_depth': lambda _, info: info.get('initial_depth', 0.0),
|
434
|
+
'overload_depth': lambda _, info: info.get('overload_depth', 0.0),
|
435
|
+
'evaporation_factor': lambda _, info: info.get('evaporation_factor', 0.0),
|
436
|
+
'seepage_suction_head': lambda _, info: info.get('seepage_suction_head', None),
|
437
|
+
'seepage_conductivity': lambda _, info: info.get('seepage_conductivity', None),
|
438
|
+
'seepage_initial_deficit': lambda _, info: info.get('seepage_initial_deficit', None)
|
439
|
+
}
|
440
|
+
|
441
|
+
# Level 3: Specific attributes for storage subtypes with defaults
|
442
|
+
storage_specific_attrs = {
|
443
|
+
'storagefunctional': {
|
444
|
+
'constant': lambda _, info: info.get('constant', 0.0),
|
445
|
+
'coefficient': lambda _, info: info.get('coefficient', 0.0),
|
446
|
+
'exponent': lambda _, info: info.get('exponent', 0.0)
|
447
|
+
},
|
448
|
+
'storagecylindrical': {
|
449
|
+
'major_axis_length': lambda _, info: info.get('major_axis_length', 0.0),
|
450
|
+
'minor_axis_length': lambda _, info: info.get('minor_axis_length', 0.0)
|
451
|
+
},
|
452
|
+
'storagetabular': {
|
453
|
+
'storage_curve_name': lambda _, info: info.get('storage_curve_name', '')
|
454
|
+
}
|
455
|
+
}
|
456
|
+
|
457
|
+
# Define node type configurations
|
458
|
+
node_types = {
|
459
|
+
'junction': {
|
460
|
+
'class': Junction,
|
461
|
+
'attrs': {**node_base_attrs, **junction_attrs}
|
462
|
+
},
|
463
|
+
'outfallfree': {
|
464
|
+
'class': OutfallFree,
|
465
|
+
'attrs': {**node_base_attrs, **outfall_base_attrs, **outfall_specific_attrs['outfallfree']}
|
466
|
+
},
|
467
|
+
'outfallnormal': {
|
468
|
+
'class': OutfallNormal,
|
469
|
+
'attrs': {**node_base_attrs, **outfall_base_attrs, **outfall_specific_attrs['outfallnormal']}
|
470
|
+
},
|
471
|
+
'outfallfixed': {
|
472
|
+
'class': OutfallFixed,
|
473
|
+
'attrs': {**node_base_attrs, **outfall_base_attrs, **outfall_specific_attrs['outfallfixed']}
|
474
|
+
},
|
475
|
+
'outfalltidal': {
|
476
|
+
'class': OutfallTidal,
|
477
|
+
'attrs': {**node_base_attrs, **outfall_base_attrs, **outfall_specific_attrs['outfalltidal']}
|
478
|
+
},
|
479
|
+
'outfalltimeseries': {
|
480
|
+
'class': OutfallTimeseries,
|
481
|
+
'attrs': {**node_base_attrs, **outfall_base_attrs, **outfall_specific_attrs['outfalltimeseries']}
|
482
|
+
},
|
483
|
+
'storagefunctional': {
|
484
|
+
'class': StorageFunctional,
|
485
|
+
'attrs': {**node_base_attrs, **storage_attrs, **storage_specific_attrs['storagefunctional']}
|
486
|
+
},
|
487
|
+
'storagecylindrical': {
|
488
|
+
'class': StorageCylindrical,
|
489
|
+
'attrs': {**node_base_attrs, **storage_attrs, **storage_specific_attrs['storagecylindrical']}
|
490
|
+
},
|
491
|
+
'storagetabular': {
|
492
|
+
'class': StorageTabular,
|
493
|
+
'attrs': {**node_base_attrs, **storage_attrs, **storage_specific_attrs['storagetabular']}
|
494
|
+
}
|
495
|
+
}
|
496
|
+
|
497
|
+
# Check if normalized node type exists
|
498
|
+
if normalized_type not in node_types:
|
499
|
+
raise TypeError(
|
500
|
+
f"Unknown node type '{node_type}', failed to add {node_information.get('name', 'unnamed node')}"
|
501
|
+
)
|
502
|
+
|
503
|
+
# Get node configuration
|
504
|
+
node_config = node_types[normalized_type]
|
505
|
+
node_class = node_config['class']
|
506
|
+
attrs = node_config['attrs']
|
507
|
+
|
508
|
+
# Create new node
|
509
|
+
new_node = node_class()
|
510
|
+
|
511
|
+
# Set all applicable attributes according to hierarchy, using default generators
|
512
|
+
for attr, default_generator in attrs.items():
|
513
|
+
value = default_generator(normalized_type, node_information)
|
514
|
+
setattr(new_node, attr, value)
|
515
|
+
|
516
|
+
# Check if the generated default name is unique (when name wasn't explicitly provided)
|
517
|
+
if 'name' not in node_information and any(node.name == new_node.name for node in self.data):
|
518
|
+
raise ValueError(f"Generated default name '{new_node.name}' already exists")
|
519
|
+
|
520
|
+
# Update coordinate bounds if coordinate is set
|
521
|
+
if hasattr(new_node, 'coordinate') and new_node.coordinate:
|
522
|
+
self._update_bounds(new_node.coordinate)
|
523
|
+
|
524
|
+
# Add node to data structure
|
525
|
+
self.data.append(new_node)
|
526
|
+
|
527
|
+
return new_node # Return the created node for immediate use if needed
|
528
|
+
|
529
|
+
def _update_bounds(self, coordinate):
|
530
|
+
"""
|
531
|
+
Update the coordinate bounds based on a new node's position.
|
532
|
+
|
533
|
+
Args:
|
534
|
+
coordinate (list): [x, y] coordinates of the node
|
535
|
+
"""
|
536
|
+
if not coordinate:
|
537
|
+
return
|
538
|
+
|
539
|
+
x, y = coordinate
|
540
|
+
self.bounds['min_x'] = min(self.bounds['min_x'], x)
|
541
|
+
self.bounds['min_y'] = min(self.bounds['min_y'], y)
|
542
|
+
self.bounds['max_x'] = max(self.bounds['max_x'], x)
|
543
|
+
self.bounds['max_y'] = max(self.bounds['max_y'], y)
|
544
|
+
|
545
|
+
def _generate_default_name(self, node_type):
|
546
|
+
"""
|
547
|
+
Generate a default name for a node based on its type and existing nodes count.
|
548
|
+
|
549
|
+
Args:
|
550
|
+
node_type (str): Type of node (e.g. 'junction', 'outfall_free')
|
551
|
+
|
552
|
+
Returns:
|
553
|
+
str: Generated name in format 'TYPE##' where TYPE is first 3 letters of node type
|
554
|
+
and ## is sequential number
|
555
|
+
"""
|
556
|
+
# Get count of nodes with the same type prefix
|
557
|
+
prefix = node_type[:3].upper() # First 3 letters of node type
|
558
|
+
existing_count = sum(1 for node in self.data if hasattr(node, 'name') and
|
559
|
+
node.name and node.name.startswith(prefix))
|
560
|
+
|
561
|
+
# Generate name with next number
|
562
|
+
return f"{prefix}{existing_count + 1}"
|
563
|
+
|
564
|
+
def _generate_default_coordinate(self):
|
565
|
+
"""
|
566
|
+
Generate a sensible default coordinate based on existing nodes.
|
567
|
+
|
568
|
+
Returns:
|
569
|
+
tuple: (x, y) coordinates
|
570
|
+
|
571
|
+
Logic:
|
572
|
+
1. If no nodes exist, returns (0, 0)
|
573
|
+
2. If bounds are established, returns center with slight offset
|
574
|
+
3. Otherwise places near last node with offset
|
575
|
+
"""
|
576
|
+
# If no nodes exist yet, start at origin
|
577
|
+
if not self.data:
|
578
|
+
return 0, 0
|
579
|
+
|
580
|
+
# If bounds are established, place in center with slight offset
|
581
|
+
if self.bounds['min_x'] != float('inf'):
|
582
|
+
center_x = (self.bounds['min_x'] + self.bounds['max_x']) / 2
|
583
|
+
center_y = (self.bounds['min_y'] + self.bounds['max_y']) / 2
|
584
|
+
# Add a small offset to avoid perfect overlap
|
585
|
+
offset = len(self.data) * 10
|
586
|
+
return center_x + offset, center_y + offset
|
587
|
+
|
588
|
+
# Fallback - place near the last node
|
589
|
+
last_node = self.data[-1]
|
590
|
+
if hasattr(last_node, 'coordinate') and last_node.coordinate:
|
591
|
+
last_x, last_y = last_node.coordinate
|
592
|
+
return last_x + 50, last_y + 50
|
593
|
+
|
594
|
+
return 0, 0
|
217
595
|
|
218
596
|
def read_from_swmm_inp(self, filename):
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
597
|
+
"""
|
598
|
+
Read node data from a SWMM input file.
|
599
|
+
|
600
|
+
Processes the following sections from SWMM input file:
|
601
|
+
- [JUNCTIONS]
|
602
|
+
- [OUTFALLS]
|
603
|
+
- [STORAGE]
|
604
|
+
- [COORDINATES]
|
605
|
+
- [DWF]
|
606
|
+
- [INFLOWS]
|
607
|
+
- [Polygons] (for Storage nodes)
|
608
|
+
|
609
|
+
Args:
|
610
|
+
filename (str): Path to the SWMM input file
|
611
|
+
|
612
|
+
Returns:
|
613
|
+
int: 0 if successful
|
614
|
+
|
615
|
+
Raises:
|
616
|
+
FileNotFoundError: If the input file doesn't exist
|
617
|
+
ValueError: If required sections are missing
|
618
|
+
Exception: For unsupported inflow types
|
619
|
+
|
620
|
+
Note:
|
621
|
+
Continues processing other nodes if errors occur with individual nodes
|
622
|
+
"""
|
623
|
+
try:
|
624
|
+
# Read all required sections
|
625
|
+
junction_contents = get_swmm_inp_content(filename, '[JUNCTIONS]')
|
626
|
+
coordinates = get_swmm_inp_content(filename, '[COORDINATES]')
|
627
|
+
outfall_contents = get_swmm_inp_content(filename, '[OUTFALLS]')
|
628
|
+
storage_contents = get_swmm_inp_content(filename, '[STORAGE]')
|
629
|
+
dwf_contents = get_swmm_inp_content(filename, '[DWF]')
|
630
|
+
inflow_contents = get_swmm_inp_content(filename, '[INFLOWS]')
|
631
|
+
polygon_contents = get_swmm_inp_content(filename, '[Polygons]')
|
224
632
|
|
225
|
-
|
633
|
+
# Process coordinates (needed by all node types)
|
634
|
+
coordinates_dic = self._process_coordinates(coordinates)
|
635
|
+
|
636
|
+
# Process each node type
|
637
|
+
self._process_junctions(junction_contents, coordinates_dic)
|
638
|
+
self._process_outfalls(outfall_contents, coordinates_dic)
|
639
|
+
self._process_storage(storage_contents, coordinates_dic)
|
640
|
+
self._process_dry_weather_flows(dwf_contents)
|
641
|
+
self._process_inflows(inflow_contents)
|
642
|
+
self._process_polygons(polygon_contents)
|
643
|
+
|
644
|
+
return 0
|
645
|
+
except Exception as e:
|
646
|
+
# Re-raise with more context
|
647
|
+
raise type(e)(f"Error reading SWMM input file: {str(e)}")
|
648
|
+
|
649
|
+
@staticmethod
|
650
|
+
def _process_coordinates(coordinates):
|
651
|
+
"""Process coordinates data from SWMM input file."""
|
226
652
|
coordinates_dic = {}
|
227
653
|
for line in coordinates:
|
228
654
|
keys = line.split()
|
229
|
-
|
230
|
-
|
655
|
+
if len(keys) >= 3: # Ensure we have at least node name, x, y
|
656
|
+
coordinates_dic[keys[0]] = [float(keys[1]), float(keys[2])]
|
657
|
+
return coordinates_dic
|
658
|
+
|
659
|
+
def _process_junctions(self, junction_contents, coordinates_dic):
|
660
|
+
"""Process junction data from SWMM input file."""
|
231
661
|
for line in junction_contents:
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
662
|
+
parts = line.split()
|
663
|
+
if len(parts) < 6: # Skip lines with insufficient data
|
664
|
+
continue
|
665
|
+
|
666
|
+
try:
|
667
|
+
dic = {
|
668
|
+
'name': parts[0],
|
669
|
+
'coordinate': coordinates_dic.get(parts[0], [0.0, 0.0]),
|
670
|
+
'elevation': float(parts[1]),
|
671
|
+
'maximum_depth': float(parts[2]),
|
672
|
+
'initial_depth': float(parts[3]),
|
673
|
+
'overload_depth': float(parts[4]),
|
674
|
+
'surface_ponding_area': float(parts[5])
|
675
|
+
}
|
676
|
+
self.add_node('junction', dic)
|
677
|
+
except (ValueError, KeyError) as e:
|
678
|
+
# Log error but continue processing other junctions
|
679
|
+
print(f"Warning: Error processing junction '{parts[0]}': {str(e)}")
|
680
|
+
|
681
|
+
def _process_outfalls(self, outfall_contents, coordinates_dic):
|
682
|
+
"""Process outfall data from SWMM input file."""
|
239
683
|
for line in outfall_contents:
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
684
|
+
parts = line.split()
|
685
|
+
if len(parts) < 3: # Skip lines with insufficient data
|
686
|
+
continue
|
687
|
+
|
688
|
+
try:
|
689
|
+
# Set up common attributes
|
690
|
+
dic = {
|
691
|
+
'name': parts[0],
|
692
|
+
'coordinate': coordinates_dic.get(parts[0], [0.0, 0.0]),
|
693
|
+
'elevation': float(parts[1])
|
694
|
+
}
|
695
|
+
|
696
|
+
# Process flap gate and route to parameters (last elements)
|
697
|
+
if parts[-1] == 'YES':
|
698
|
+
dic['flap_gate'] = 'YES'
|
699
|
+
elif parts[-1] == 'NO':
|
700
|
+
dic['flap_gate'] = 'NO'
|
701
|
+
else:
|
702
|
+
dic['flap_gate'] = parts[-2]
|
703
|
+
dic['route_to'] = parts[-1]
|
704
|
+
|
705
|
+
# Process outfall type
|
706
|
+
outfall_type = parts[2]
|
707
|
+
if outfall_type == 'FREE':
|
254
708
|
self.add_node('outfall_free', dic)
|
255
|
-
|
709
|
+
elif outfall_type == 'NORMAL':
|
256
710
|
self.add_node('outfall_normal', dic)
|
257
|
-
|
258
|
-
dic['stage'] = float(
|
711
|
+
elif outfall_type == 'FIXED':
|
712
|
+
dic['stage'] = float(parts[3])
|
259
713
|
self.add_node('outfall_fixed', dic)
|
260
|
-
|
261
|
-
dic['tidal'] =
|
714
|
+
elif outfall_type == 'TIDAL':
|
715
|
+
dic['tidal'] = parts[3]
|
262
716
|
self.add_node('outfall_tidal', dic)
|
263
|
-
|
264
|
-
dic['time_series'] =
|
717
|
+
elif outfall_type == 'TIMESERIES':
|
718
|
+
dic['time_series'] = parts[3]
|
265
719
|
self.add_node('outfall_time_series', dic)
|
266
|
-
|
267
|
-
|
268
|
-
|
720
|
+
except (ValueError, KeyError) as e:
|
721
|
+
# Log error but continue processing other outfalls
|
722
|
+
print(f"Warning: Error processing outfall '{parts[0]}': {str(e)}")
|
723
|
+
|
724
|
+
def _process_storage(self, storage_contents, coordinates_dic):
|
725
|
+
"""Process storage data from SWMM input file."""
|
726
|
+
for line in storage_contents:
|
727
|
+
parts = line.split()
|
728
|
+
if len(parts) < 5: # Skip lines with insufficient data
|
729
|
+
continue
|
730
|
+
|
731
|
+
try:
|
732
|
+
# Set up common attributes
|
733
|
+
dic = {
|
734
|
+
'name': parts[0],
|
735
|
+
'coordinate': coordinates_dic.get(parts[0], [0.0, 0.0]),
|
736
|
+
'elevation': float(parts[1]),
|
737
|
+
'maximum_depth': float(parts[2]),
|
738
|
+
'initial_depth': float(parts[3])
|
739
|
+
}
|
740
|
+
|
741
|
+
# Process storage type - check the 5th element (index 4)
|
742
|
+
storage_type = parts[4]
|
743
|
+
|
744
|
+
# Process specific storage type parameters based on storage type
|
745
|
+
if storage_type == 'FUNCTIONAL':
|
746
|
+
# For FUNCTIONAL: coefficient exponent constant
|
747
|
+
if len(parts) > 7: # Ensure we have at least the required parameters
|
748
|
+
dic['coefficient'] = float(parts[5])
|
749
|
+
dic['exponent'] = float(parts[6])
|
750
|
+
dic['constant'] = float(parts[7])
|
751
|
+
|
752
|
+
# Process required parameters: overload_depth and evaporation_factor
|
753
|
+
idx = 8 # Start index for additional parameters
|
754
|
+
|
755
|
+
# overload_depth and evaporation_factor are required
|
756
|
+
if len(parts) > idx + 1: # Need both overload_depth and evaporation_factor
|
757
|
+
dic['overload_depth'] = float(parts[idx])
|
758
|
+
dic['evaporation_factor'] = float(parts[idx + 1])
|
759
|
+
idx += 2
|
760
|
+
|
761
|
+
# Check for seepage parameters - all three must be present or none
|
762
|
+
if len(parts) > idx + 2: # Need all three seepage parameters
|
763
|
+
dic['seepage_suction_head'] = float(parts[idx])
|
764
|
+
dic['seepage_conductivity'] = float(parts[idx + 1])
|
765
|
+
dic['seepage_initial_deficit'] = float(parts[idx + 2])
|
766
|
+
else:
|
767
|
+
# Default values for required parameters if not provided
|
768
|
+
dic['overload_depth'] = 0.0
|
769
|
+
dic['evaporation_factor'] = 0.0
|
770
|
+
|
771
|
+
self.add_node('storage_functional', dic)
|
772
|
+
|
773
|
+
elif storage_type == 'CYLINDRICAL':
|
774
|
+
# For CYLINDRICAL: major_axis_length minor_axis_length 0
|
775
|
+
if len(parts) > 7: # Ensure we have at least the required parameters
|
776
|
+
dic['major_axis_length'] = float(parts[5])
|
777
|
+
dic['minor_axis_length'] = float(parts[6])
|
778
|
+
# Skip the placeholder value '0' at index 7
|
779
|
+
|
780
|
+
# Process required parameters: overload_depth and evaporation_factor
|
781
|
+
idx = 8 # Start index for additional parameters
|
782
|
+
|
783
|
+
# overload_depth and evaporation_factor are required
|
784
|
+
if len(parts) > idx + 1: # Need both overload_depth and evaporation_factor
|
785
|
+
dic['overload_depth'] = float(parts[idx])
|
786
|
+
dic['evaporation_factor'] = float(parts[idx + 1])
|
787
|
+
idx += 2
|
788
|
+
|
789
|
+
# Check for seepage parameters - all three must be present or none
|
790
|
+
if len(parts) > idx + 2: # Need all three seepage parameters
|
791
|
+
dic['seepage_suction_head'] = float(parts[idx])
|
792
|
+
dic['seepage_conductivity'] = float(parts[idx + 1])
|
793
|
+
dic['seepage_initial_deficit'] = float(parts[idx + 2])
|
794
|
+
else:
|
795
|
+
# Default values for required parameters if not provided
|
796
|
+
dic['overload_depth'] = 0.0
|
797
|
+
dic['evaporation_factor'] = 0.0
|
798
|
+
|
799
|
+
self.add_node('storage_cylindrical', dic)
|
800
|
+
|
801
|
+
elif storage_type == 'TABULAR':
|
802
|
+
# For TABULAR: storage_curve_name
|
803
|
+
if len(parts) > 5: # Ensure we have at least the required parameters
|
804
|
+
dic['storage_curve_name'] = parts[5]
|
805
|
+
|
806
|
+
# Process required parameters: overload_depth and evaporation_factor
|
807
|
+
idx = 6 # Start index for additional parameters
|
808
|
+
|
809
|
+
# overload_depth and evaporation_factor are required
|
810
|
+
if len(parts) > idx + 1: # Need both overload_depth and evaporation_factor
|
811
|
+
dic['overload_depth'] = float(parts[idx])
|
812
|
+
dic['evaporation_factor'] = float(parts[idx + 1])
|
813
|
+
idx += 2
|
814
|
+
|
815
|
+
# Check for seepage parameters - all three must be present or none
|
816
|
+
if len(parts) > idx + 2: # Need all three seepage parameters
|
817
|
+
dic['seepage_suction_head'] = float(parts[idx])
|
818
|
+
dic['seepage_conductivity'] = float(parts[idx + 1])
|
819
|
+
dic['seepage_initial_deficit'] = float(parts[idx + 2])
|
820
|
+
else:
|
821
|
+
# Default values for required parameters if not provided
|
822
|
+
dic['overload_depth'] = 0.0
|
823
|
+
dic['evaporation_factor'] = 0.0
|
824
|
+
|
825
|
+
self.add_node('storage_tabular', dic)
|
826
|
+
|
827
|
+
except (ValueError, KeyError) as e:
|
828
|
+
# Log error but continue processing other storage nodes
|
829
|
+
print(f"Warning: Error processing storage '{parts[0]}': {str(e)}")
|
830
|
+
|
831
|
+
def _process_dry_weather_flows(self, dwf_contents):
|
832
|
+
"""Process dry weather flow data from SWMM input file."""
|
269
833
|
for line in dwf_contents:
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
834
|
+
parts = line.split()
|
835
|
+
if len(parts) < 3: # Skip lines with insufficient data
|
836
|
+
continue
|
837
|
+
|
838
|
+
node_name = parts[0]
|
839
|
+
try:
|
840
|
+
node = self.index_of(node_name, return_node=True)
|
841
|
+
except ValueError:
|
842
|
+
node = None
|
843
|
+
|
844
|
+
if node and hasattr(node, 'dwf_base_value'):
|
845
|
+
node.dwf_base_value = parts[2]
|
846
|
+
node.dwf_patterns = parts[3:] if len(parts) > 3 else []
|
847
|
+
|
848
|
+
def _process_inflows(self, inflow_contents):
|
849
|
+
"""Process inflow data from SWMM input file."""
|
277
850
|
for line in inflow_contents:
|
278
|
-
|
279
|
-
if
|
851
|
+
parts = line.split()
|
852
|
+
if len(parts) < 8: # Skip lines with insufficient data
|
853
|
+
continue
|
854
|
+
|
855
|
+
if parts[1] != 'FLOW':
|
280
856
|
raise Exception('Unsupported inflow type, only FLOW is accepted.')
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
857
|
+
|
858
|
+
node_name = parts[0]
|
859
|
+
try:
|
860
|
+
node = self.index_of(node_name, return_node=True)
|
861
|
+
except ValueError:
|
862
|
+
node = None
|
863
|
+
|
864
|
+
if node:
|
865
|
+
inflow_data = {
|
866
|
+
'time_series': parts[2],
|
867
|
+
'type': parts[3],
|
868
|
+
'm_factor': float(parts[4]),
|
869
|
+
's_factor': float(parts[5]),
|
870
|
+
'baseline': float(parts[6]),
|
871
|
+
'pattern': parts[7]
|
872
|
+
}
|
873
|
+
if hasattr(node, 'inflow'):
|
874
|
+
node.inflow = inflow_data
|
875
|
+
|
876
|
+
def _process_polygons(self, polygon_contents):
|
877
|
+
"""Process polygon data from SWMM input file for Storage nodes."""
|
878
|
+
for line in polygon_contents:
|
879
|
+
try:
|
880
|
+
parts = line.split()
|
881
|
+
node_name = parts[0]
|
882
|
+
x_coord = float(parts[1])
|
883
|
+
y_coord = float(parts[2])
|
884
|
+
|
885
|
+
for node in self.data:
|
886
|
+
if isinstance(node, Storage) and node.name == node_name:
|
887
|
+
node.polygon.x.append(x_coord)
|
888
|
+
node.polygon.y.append(y_coord)
|
889
|
+
node.polygon.node_name = node_name
|
890
|
+
except (IndexError, ValueError) as e:
|
891
|
+
# Log warning but continue processing other polygons
|
892
|
+
print(f"Warning: Error processing polygon data: {str(e)}")
|
287
893
|
|
288
894
|
def write_to_swmm_inp(self, filename):
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
895
|
+
"""
|
896
|
+
Write node data to a SWMM input file.
|
897
|
+
|
898
|
+
Writes the following sections to SWMM input file:
|
899
|
+
- [JUNCTIONS]
|
900
|
+
- [OUTFALLS]
|
901
|
+
- [STORAGE]
|
902
|
+
- [COORDINATES]
|
903
|
+
- [DWF]
|
904
|
+
- [INFLOWS]
|
905
|
+
- [Polygons] (for Storage nodes)
|
906
|
+
|
907
|
+
Args:
|
908
|
+
filename (str): Path to the SWMM input file
|
909
|
+
|
910
|
+
Returns:
|
911
|
+
int: 0 if successful
|
912
|
+
|
913
|
+
Raises:
|
914
|
+
IOError: If there's an error writing to the file
|
915
|
+
|
916
|
+
Note:
|
917
|
+
Appends to existing file content
|
918
|
+
"""
|
919
|
+
try:
|
920
|
+
with open(filename, 'a', encoding='utf-8') as f:
|
921
|
+
# Write junctions section
|
922
|
+
self._write_junctions_section(f)
|
923
|
+
|
924
|
+
# Write outfalls section
|
925
|
+
self._write_outfalls_section(f)
|
926
|
+
|
927
|
+
# Write storage section
|
928
|
+
self._write_storage_section(f)
|
929
|
+
|
930
|
+
# Write coordinates section
|
931
|
+
self._write_coordinates_section(f)
|
932
|
+
|
933
|
+
# Write DWF section
|
934
|
+
self._write_dwf_section(f)
|
935
|
+
|
936
|
+
# Write inflows section
|
937
|
+
self._write_inflows_section(f)
|
938
|
+
|
939
|
+
# Write polygons section for Storage nodes
|
940
|
+
self._write_polygons_section(f)
|
941
|
+
|
942
|
+
return 0
|
943
|
+
except IOError as e:
|
944
|
+
raise IOError(f"Error writing to SWMM input file: {str(e)}")
|
945
|
+
|
946
|
+
def _write_junctions_section(self, file):
|
947
|
+
"""Write junctions section to the SWMM input file."""
|
948
|
+
file.write('\n\n[JUNCTIONS]\n')
|
949
|
+
file.write(';;Name Elevation MaxDepth InitDepth SurDepth Ponding\n')
|
950
|
+
|
951
|
+
for node in self.data:
|
952
|
+
if isinstance(node, Junction):
|
953
|
+
file.write(
|
954
|
+
f'{node.name:8} {node.elevation:8.3f} {node.maximum_depth:8.3f} '
|
955
|
+
f'{node.initial_depth:8.3f} {node.overload_depth:8.3f} {node.surface_ponding_area:8.3f}\n')
|
956
|
+
|
957
|
+
def _write_outfalls_section(self, file):
|
958
|
+
"""Write outfalls section to the SWMM input file."""
|
959
|
+
file.write('\n\n[OUTFALLS]\n')
|
960
|
+
file.write(';;Name Elevation Type // Gated RouteTo\n')
|
961
|
+
|
962
|
+
outfall_types = {
|
963
|
+
OutfallFree: ('FREE', None),
|
964
|
+
OutfallNormal: ('NORMAL', None),
|
965
|
+
OutfallFixed: ('FIXED', 'stage'),
|
966
|
+
OutfallTidal: ('TIDAL', 'tidal'),
|
967
|
+
OutfallTimeseries: ('TIMESERIES', 'time_series')
|
968
|
+
}
|
969
|
+
|
970
|
+
for node in self.data:
|
971
|
+
for outfall_class, (type_name, extra_attr) in outfall_types.items():
|
972
|
+
if isinstance(node, outfall_class):
|
973
|
+
route_to = node.route_to if hasattr(node, 'route_to') and node.route_to else ''
|
974
|
+
gate_flag = 'YES' if node.flap_gate else 'NO'
|
975
|
+
|
976
|
+
if extra_attr:
|
977
|
+
extra_value = getattr(node, extra_attr, '')
|
978
|
+
file.write(
|
979
|
+
f'{node.name:8} {node.elevation:8.3f} {type_name} '
|
980
|
+
f'{extra_value:8} {gate_flag} {route_to}\n')
|
981
|
+
else:
|
982
|
+
file.write(
|
983
|
+
f'{node.name:8} {node.elevation:8.3f} {type_name} '
|
984
|
+
f'{gate_flag:8} {route_to}\n')
|
985
|
+
|
986
|
+
def _write_coordinates_section(self, file):
|
987
|
+
"""Write coordinates section to the SWMM input file."""
|
988
|
+
file.write('\n\n[COORDINATES]\n')
|
989
|
+
file.write(';;Name X-Coord Y-Coord\n')
|
990
|
+
|
991
|
+
for node in self.data:
|
992
|
+
if hasattr(node, 'coordinate') and len(node.coordinate) >= 2:
|
993
|
+
file.write(f'{node.name:8} {node.coordinate[0]:8.2f} {node.coordinate[1]:8.2f}\n')
|
994
|
+
|
995
|
+
def _write_dwf_section(self, file):
|
996
|
+
"""Write dry weather flow section to the SWMM input file."""
|
997
|
+
file.write('\n\n[DWF]\n')
|
998
|
+
file.write(';;Node Constituent Baseline Patterns \n')
|
999
|
+
|
1000
|
+
for node in self.data:
|
1001
|
+
if isinstance(node, Junction) and hasattr(node, 'dwf_base_value') and node.dwf_base_value != 0:
|
1002
|
+
patterns = ' '.join(node.dwf_patterns if hasattr(node, 'dwf_patterns') and node.dwf_patterns else [])
|
1003
|
+
file.write(f'{node.name} FLOW {node.dwf_base_value} {patterns}\n')
|
1004
|
+
|
1005
|
+
def _write_storage_section(self, file):
|
1006
|
+
"""Write storage section to the SWMM input file."""
|
1007
|
+
file.write('\n\n[STORAGE]\n')
|
1008
|
+
file.write(
|
1009
|
+
';;Name Elev. MaxDepth InitDepth Shape Curve/Params SurDepth Fevap Seepage\n')
|
1010
|
+
|
1011
|
+
for node in self.data:
|
1012
|
+
if isinstance(node, Storage):
|
1013
|
+
# Common parameters for all storage types
|
1014
|
+
base_params = f'{node.name:14} {node.elevation:8.3f} {node.maximum_depth:8.3f} {node.initial_depth:8.3f} '
|
1015
|
+
|
1016
|
+
# Storage type specific parameters
|
1017
|
+
if isinstance(node, StorageFunctional):
|
1018
|
+
# For FUNCTIONAL: coefficient exponent constant
|
1019
|
+
shape_params = f'FUNCTIONAL {node.coefficient:8.3f} {node.exponent:8.3f} {node.constant:8.3f} '
|
1020
|
+
elif isinstance(node, StorageCylindrical):
|
1021
|
+
# For CYLINDRICAL: major_axis_length minor_axis_length 0
|
1022
|
+
shape_params = f'CYLINDRICAL {node.major_axis_length:8.3f} {node.minor_axis_length:8.3f} 0 '
|
1023
|
+
elif isinstance(node, StorageTabular):
|
1024
|
+
# For TABULAR: storage_curve_name
|
1025
|
+
shape_params = f'TABULAR {node.storage_curve_name:8} '
|
1026
|
+
else:
|
1027
|
+
# Skip unknown storage types
|
1028
|
+
continue
|
1029
|
+
|
1030
|
+
# Required parameters (overload_depth and evaporation_factor are always included)
|
1031
|
+
required_params = f'{node.overload_depth:8.3f} {node.evaporation_factor:8.3f} '
|
1032
|
+
|
1033
|
+
# Seepage parameters (if present)
|
1034
|
+
if (node.seepage_suction_head is not None and
|
1035
|
+
node.seepage_conductivity is not None and
|
1036
|
+
node.seepage_initial_deficit is not None):
|
1037
|
+
seepage_params = f'{node.seepage_suction_head:8.3f} {node.seepage_conductivity:8.3f} {node.seepage_initial_deficit:8.3f}'
|
1038
|
+
else:
|
1039
|
+
seepage_params = ''
|
1040
|
+
|
1041
|
+
# Write the complete line
|
1042
|
+
file.write(f'{base_params}{shape_params}{required_params}{seepage_params}\n')
|
1043
|
+
|
1044
|
+
def _write_inflows_section(self, file):
|
1045
|
+
"""Write inflows section to the SWMM input file."""
|
1046
|
+
file.write('\n\n[INFLOWS]\n')
|
1047
|
+
file.write(';;Node Constituent Time Series Type Mfactor Sfactor Baseline Pattern\n')
|
1048
|
+
|
1049
|
+
for node in self.data:
|
1050
|
+
if (isinstance(node, Junction) or isinstance(node, Storage)) and hasattr(node,'inflow') and node.inflow is not None:
|
1051
|
+
values = list(node.inflow.values())
|
1052
|
+
formatted_values = ' '.join(str(value) for value in values)
|
1053
|
+
file.write(f'{node.name} FLOW {formatted_values} \n')
|
1054
|
+
|
1055
|
+
def _write_polygons_section(self, file):
|
1056
|
+
"""Write polygons section to the SWMM input file for Storage nodes."""
|
1057
|
+
# Check if any Storage nodes have polygon data
|
1058
|
+
has_polygons = any(isinstance(node, Storage) and
|
1059
|
+
hasattr(node, 'polygon') and
|
1060
|
+
node.polygon.node_name is not None and
|
1061
|
+
len(node.polygon.x) > 0 for node in self.data)
|
1062
|
+
|
1063
|
+
if has_polygons:
|
1064
|
+
# Read file line by line to find sections
|
1065
|
+
with open(file.name, 'r') as read_file:
|
1066
|
+
lines = read_file.readlines()
|
1067
|
+
# Find [Polygons] section
|
1068
|
+
polygons_line = -1
|
1069
|
+
next_section_line = -1
|
1070
|
+
for i, line in enumerate(lines):
|
1071
|
+
if line.strip() == '[Polygons]':
|
1072
|
+
polygons_line = i
|
1073
|
+
elif polygons_line != -1 and line.strip().startswith('['):
|
1074
|
+
next_section_line = i
|
1075
|
+
break
|
1076
|
+
if polygons_line == -1:
|
1077
|
+
# No existing section, create new one at current position
|
1078
|
+
file.write('\n\n[Polygons]\n')
|
1079
|
+
file.write(';;Name X-Coord Y-Coord\n')
|
1080
|
+
|
1081
|
+
# Write polygon data
|
1082
|
+
for node in self.data:
|
1083
|
+
if (isinstance(node, Storage) and
|
1084
|
+
hasattr(node, 'polygon') and
|
1085
|
+
node.polygon.node_name is not None):
|
1086
|
+
for xi, yi in zip(node.polygon.x, node.polygon.y):
|
1087
|
+
file.write(f'{node.polygon.node_name} {xi} {yi}\n')
|
1088
|
+
else:
|
1089
|
+
# Section exists, we need to modify file content
|
1090
|
+
# Insert our polygon data just after the header line
|
1091
|
+
insert_position = polygons_line + 2 # +1 for the header, +1 for the column labels
|
1092
|
+
|
1093
|
+
# Prepare polygon data lines
|
1094
|
+
new_lines = []
|
1095
|
+
for node in self.data:
|
1096
|
+
if (isinstance(node, Storage) and
|
1097
|
+
hasattr(node, 'polygon') and
|
1098
|
+
node.polygon.node_name is not None):
|
1099
|
+
for xi, yi in zip(node.polygon.x, node.polygon.y):
|
1100
|
+
new_lines.append(f'{node.polygon.node_name} {xi} {yi}\n')
|
1101
|
+
|
1102
|
+
# Insert the new lines at the appropriate position
|
1103
|
+
lines[insert_position:insert_position] = new_lines
|
1104
|
+
|
1105
|
+
# Rewrite the entire file
|
1106
|
+
file.seek(0)
|
1107
|
+
file.writelines(lines)
|
1108
|
+
file.truncate()
|
1109
|
+
|
1110
|
+
def index_of(self, node_name, return_node=False):
|
1111
|
+
"""
|
1112
|
+
Find a node's index by name.
|
1113
|
+
|
1114
|
+
Args:
|
1115
|
+
node_name (str): The name of the node to find
|
1116
|
+
return_node (bool): If True, returns the node object instead of index
|
1117
|
+
|
1118
|
+
Returns:
|
1119
|
+
int or object: The index of the node or the node object if return_node is True
|
1120
|
+
|
1121
|
+
Raises:
|
1122
|
+
ValueError: If no node with the given name is found
|
1123
|
+
"""
|
343
1124
|
for index, item in enumerate(self.data):
|
344
1125
|
if item.name == node_name:
|
345
|
-
return index
|
1126
|
+
return item if return_node else index
|
346
1127
|
raise ValueError(f"No item found with name '{node_name}'")
|