easysewer 0.0.2__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 +46 -17
- easysewer/UDM.py +47 -22
- easysewer/__init__.py +1 -1
- {easysewer-0.0.2.dist-info → easysewer-0.0.3.dist-info}/METADATA +1 -1
- easysewer-0.0.3.dist-info/RECORD +20 -0
- {easysewer-0.0.2.dist-info → easysewer-0.0.3.dist-info}/WHEEL +1 -1
- easysewer-0.0.2.dist-info/RECORD +0 -19
- {easysewer-0.0.2.dist-info → easysewer-0.0.3.dist-info}/top_level.txt +0 -0
easysewer/Area.py
CHANGED
@@ -4,33 +4,99 @@ Subcatchment Area Management Module
|
|
4
4
|
This module handles subcatchment areas in the drainage network, including their
|
5
5
|
physical characteristics, infiltration parameters, and routing behavior.
|
6
6
|
"""
|
7
|
+
from warnings import warn
|
7
8
|
from .utils import *
|
8
9
|
|
9
10
|
|
10
11
|
class InfiltrationHorton:
|
12
|
+
"""
|
13
|
+
Horton infiltration model parameters.
|
14
|
+
|
15
|
+
Represents the Horton infiltration method which models infiltration rate
|
16
|
+
decay from a maximum to minimum value over time.
|
17
|
+
|
18
|
+
Attributes:
|
19
|
+
maximum_rate (float): Maximum infiltration rate (mm/h)
|
20
|
+
minimum_rate (float): Minimum infiltration rate (mm/h)
|
21
|
+
decay_rate (float): Rate of decay from max to min (1/h)
|
22
|
+
dry_time (float): Time needed for soil to fully dry (days)
|
23
|
+
maximum_infiltration_volume (float): Maximum infiltration volume (mm), 0 if not applicable
|
24
|
+
"""
|
25
|
+
# Default parameters for Horton model
|
26
|
+
MAX_RATE_DEFAULT = 50 # mm/h
|
27
|
+
MIN_RATE_DEFAULT = 5 # mm/h
|
28
|
+
DECAY_RATE_DEFAULT = 5 # 1/h
|
29
|
+
DRY_TIME_DEFAULT = 7 # days
|
30
|
+
MAX_INFIL_VOLUME_DEFAULT = 0 # mm
|
31
|
+
|
11
32
|
def __init__(self):
|
12
|
-
self.maximum_rate =
|
13
|
-
self.minimum_rate =
|
14
|
-
self.decay_rate =
|
15
|
-
self.dry_time =
|
16
|
-
self.maximum_infiltration_volume =
|
33
|
+
self.maximum_rate = self.MAX_RATE_DEFAULT
|
34
|
+
self.minimum_rate = self.MIN_RATE_DEFAULT
|
35
|
+
self.decay_rate = self.DECAY_RATE_DEFAULT
|
36
|
+
self.dry_time = self.DRY_TIME_DEFAULT
|
37
|
+
self.maximum_infiltration_volume = self.MAX_INFIL_VOLUME_DEFAULT
|
17
38
|
|
18
39
|
|
19
40
|
class InfiltrationGreenAmpt:
|
41
|
+
"""
|
42
|
+
Green-Ampt infiltration model parameters.
|
43
|
+
|
44
|
+
Represents the Green-Ampt infiltration method which models infiltration based on
|
45
|
+
soil physics and hydraulic principles.
|
46
|
+
|
47
|
+
Attributes:
|
48
|
+
soil_capillary_suction (float): Soil capillary suction head (mm)
|
49
|
+
soil_saturated_hydraulic_conductivity (float): Saturated hydraulic conductivity (mm/h)
|
50
|
+
initial_soil_moisture_deficit (float): Initial soil moisture deficit (fraction)
|
51
|
+
"""
|
52
|
+
# Default parameters for sandy loam soil
|
53
|
+
CAPILLARY_SUCTION_DEFAULT = 110.0 # mm
|
54
|
+
HYDRAULIC_CONDUCTIVITY_DEFAULT = 10.0 # mm/h
|
55
|
+
MOISTURE_DEFICIT_DEFAULT = 0.3 # fraction
|
56
|
+
|
20
57
|
def __init__(self):
|
21
|
-
|
22
|
-
self.
|
23
|
-
self.
|
58
|
+
# Default values for sandy loam soil
|
59
|
+
self.soil_capillary_suction = self.CAPILLARY_SUCTION_DEFAULT
|
60
|
+
self.soil_saturated_hydraulic_conductivity = self.HYDRAULIC_CONDUCTIVITY_DEFAULT
|
61
|
+
self.initial_soil_moisture_deficit = self.MOISTURE_DEFICIT_DEFAULT
|
24
62
|
|
25
63
|
|
26
64
|
class InfiltrationCurveNumber:
|
65
|
+
"""
|
66
|
+
SCS Curve Number infiltration model parameters.
|
67
|
+
|
68
|
+
Represents the SCS Curve Number method which models infiltration based on
|
69
|
+
soil classification and land use characteristics.
|
70
|
+
|
71
|
+
Attributes:
|
72
|
+
curve_number (float): SCS curve number
|
73
|
+
dry_time (float): Time for fully saturated soil to completely dry (days)
|
74
|
+
soil_saturated_hydraulic_conductivity (float): Saturated hydraulic conductivity (mm/h)
|
75
|
+
"""
|
76
|
+
# Default parameters for HSG-B soil
|
77
|
+
CURVE_NUMBER_DEFAULT = 79
|
78
|
+
CN_DRY_TIME_DEFAULT = 7 # days
|
79
|
+
CN_HYDRAULIC_CONDUCTIVITY_DEFAULT = 8.0 # mm/h
|
80
|
+
|
27
81
|
def __init__(self):
|
28
|
-
|
29
|
-
self.
|
30
|
-
self.
|
82
|
+
# Default values for open space (poor condition)
|
83
|
+
self.curve_number = self.CURVE_NUMBER_DEFAULT
|
84
|
+
self.dry_time = self.CN_DRY_TIME_DEFAULT
|
85
|
+
self.soil_saturated_hydraulic_conductivity = self.CN_HYDRAULIC_CONDUCTIVITY_DEFAULT
|
31
86
|
|
32
87
|
|
33
88
|
class Infiltration:
|
89
|
+
"""
|
90
|
+
Container for different infiltration model parameters.
|
91
|
+
|
92
|
+
Holds instances of all supported infiltration models, allowing the appropriate
|
93
|
+
model to be selected and used based on simulation requirements.
|
94
|
+
|
95
|
+
Attributes:
|
96
|
+
horton (InfiltrationHorton): Horton infiltration model parameters
|
97
|
+
green_ampt (InfiltrationGreenAmpt): Green-Ampt infiltration model parameters
|
98
|
+
curve_number (InfiltrationCurveNumber): SCS Curve Number infiltration model parameters
|
99
|
+
"""
|
34
100
|
def __init__(self):
|
35
101
|
self.horton = InfiltrationHorton()
|
36
102
|
self.green_ampt = InfiltrationGreenAmpt()
|
@@ -38,6 +104,16 @@ class Infiltration:
|
|
38
104
|
|
39
105
|
|
40
106
|
class Polygon:
|
107
|
+
"""
|
108
|
+
Geometric representation of a subcatchment area.
|
109
|
+
|
110
|
+
Stores the polygon vertices that define the boundary of a subcatchment area.
|
111
|
+
|
112
|
+
Attributes:
|
113
|
+
area_name (str): Name of the associated subcatchment area
|
114
|
+
x (list): List of x-coordinates of polygon vertices
|
115
|
+
y (list): List of y-coordinates of polygon vertices
|
116
|
+
"""
|
41
117
|
def __init__(self):
|
42
118
|
self.area_name = None
|
43
119
|
self.x = []
|
@@ -101,16 +177,51 @@ class Area:
|
|
101
177
|
|
102
178
|
|
103
179
|
class AreaList:
|
180
|
+
"""
|
181
|
+
A collection class for managing subcatchment areas in a drainage network.
|
182
|
+
|
183
|
+
This class provides storage and management for subcatchment areas,
|
184
|
+
with methods for adding, accessing, and processing areas. It also handles
|
185
|
+
reading from and writing to SWMM input files.
|
186
|
+
|
187
|
+
Attributes:
|
188
|
+
data (list): List containing all Area objects
|
189
|
+
"""
|
104
190
|
def __init__(self):
|
105
191
|
self.data = []
|
106
192
|
|
107
193
|
def __repr__(self):
|
194
|
+
"""
|
195
|
+
Returns a string representation of the AreaList.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
str: A string showing the count of areas in the list
|
199
|
+
"""
|
108
200
|
return f'{len(self.data)} Areas'
|
109
201
|
|
110
202
|
def __len__(self):
|
203
|
+
"""
|
204
|
+
Returns the number of areas in the list.
|
205
|
+
|
206
|
+
Returns:
|
207
|
+
int: Number of areas in the list
|
208
|
+
"""
|
111
209
|
return len(self.data)
|
112
210
|
|
113
211
|
def __getitem__(self, key):
|
212
|
+
"""
|
213
|
+
Gets an area by index or name.
|
214
|
+
|
215
|
+
Args:
|
216
|
+
key (int|str): Index or name of area to retrieve
|
217
|
+
|
218
|
+
Returns:
|
219
|
+
Area: The requested area
|
220
|
+
|
221
|
+
Raises:
|
222
|
+
KeyError: If area name not found
|
223
|
+
TypeError: If key is not int or str
|
224
|
+
"""
|
114
225
|
if isinstance(key, int):
|
115
226
|
return self.data[key]
|
116
227
|
elif isinstance(key, str):
|
@@ -122,173 +233,520 @@ class AreaList:
|
|
122
233
|
raise TypeError("Key must be an integer or a string")
|
123
234
|
|
124
235
|
def __iter__(self):
|
236
|
+
"""
|
237
|
+
Returns an iterator over the areas in the list.
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
iterator: Iterator over the areas
|
241
|
+
"""
|
125
242
|
return iter(self.data)
|
126
243
|
|
127
244
|
def __contains__(self, item):
|
245
|
+
"""
|
246
|
+
Checks if an area is in the list.
|
247
|
+
|
248
|
+
Args:
|
249
|
+
item (Area): Area to check for
|
250
|
+
|
251
|
+
Returns:
|
252
|
+
bool: True if the area is in the list, False otherwise
|
253
|
+
"""
|
128
254
|
return item in self.data
|
129
255
|
|
256
|
+
def _generate_default_name(self):
|
257
|
+
"""
|
258
|
+
Generate a default name for an area based on existing areas count.
|
259
|
+
|
260
|
+
Returns:
|
261
|
+
str: Generated name in format 'Area##' where ## is sequential number
|
262
|
+
"""
|
263
|
+
# Use 'Area' as the prefix for all generated area names
|
264
|
+
prefix = 'Area'
|
265
|
+
|
266
|
+
# Get count of areas with the same prefix
|
267
|
+
existing_count = sum(1 for area in self.data if hasattr(area, 'name') and
|
268
|
+
area.name and area.name.startswith(prefix))
|
269
|
+
|
270
|
+
# Generate name with next number
|
271
|
+
return f"{prefix}{existing_count + 1}"
|
272
|
+
|
130
273
|
def add_area(self, area_information):
|
131
|
-
|
274
|
+
"""
|
275
|
+
Creates and adds a new Area object to the list based on provided information.
|
276
|
+
|
277
|
+
Args:
|
278
|
+
area_information (dict): Dictionary containing area properties
|
279
|
+
|
280
|
+
Returns:
|
281
|
+
Area: The newly created and added area object
|
282
|
+
|
283
|
+
Raises:
|
284
|
+
ValueError: If an area with the same name already exists
|
285
|
+
"""
|
286
|
+
# Check if a name is provided and if it already exists in the collection
|
132
287
|
if 'name' in area_information:
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
if
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
288
|
+
requested_name = area_information['name']
|
289
|
+
if any(area.name == requested_name for area in self.data):
|
290
|
+
raise ValueError(f"Area with name '{requested_name}' already exists")
|
291
|
+
|
292
|
+
new_area = Area()
|
293
|
+
|
294
|
+
# Set identification attributes
|
295
|
+
new_area.name = area_information.get('name', self._generate_default_name())
|
296
|
+
new_area.rain_gage = area_information.get('rain_gage', '*')
|
297
|
+
new_area.outlet = area_information.get('outlet', '*')
|
298
|
+
|
299
|
+
# Set physical attributes
|
300
|
+
# Validate and set physical attributes
|
301
|
+
area_value = area_information.get('area', 0.0)
|
302
|
+
if area_value <= 0:
|
303
|
+
warn(f"Area must be a positive number, got {area_value}")
|
304
|
+
new_area.area = area_value
|
305
|
+
|
306
|
+
impervious_ratio_value = area_information.get('impervious_ratio', 0)
|
307
|
+
if not (0 <= impervious_ratio_value <= 100):
|
308
|
+
warn(f"Impervious ratio must be between 0 and 100, got {impervious_ratio_value}")
|
309
|
+
new_area.impervious_ratio = impervious_ratio_value
|
310
|
+
|
311
|
+
width_value = area_information.get('width', 0)
|
312
|
+
if width_value <= 0:
|
313
|
+
warn(f"Width must be positive number, got {width_value}")
|
314
|
+
new_area.width = width_value
|
315
|
+
|
316
|
+
slope_value = area_information.get('slope', 0)
|
317
|
+
if slope_value <= 0:
|
318
|
+
warn(f"Slope must be positive number, got {slope_value}")
|
319
|
+
new_area.slope = slope_value
|
320
|
+
|
321
|
+
# Set surface attributes
|
322
|
+
new_area.curb_length = area_information.get('curb_length', 0)
|
323
|
+
new_area.snow_pack = area_information.get('snow_pack', '')
|
324
|
+
|
325
|
+
# Set hydraulic attributes
|
326
|
+
# Validate hydraulic parameters
|
327
|
+
manning_impervious = area_information.get('manning_impervious', 0)
|
328
|
+
if not (0.01 <= manning_impervious <= 0.5):
|
329
|
+
warn(f"Manning's n for impervious area must be between 0.01-0.5, got {manning_impervious}")
|
330
|
+
new_area.manning_impervious = manning_impervious
|
331
|
+
|
332
|
+
manning_pervious = area_information.get('manning_pervious', 0)
|
333
|
+
if not (0.01 <= manning_pervious <= 0.8):
|
334
|
+
warn(f"Manning's n for pervious area must be between 0.01-0.8, got {manning_pervious}")
|
335
|
+
new_area.manning_pervious = manning_pervious
|
336
|
+
|
337
|
+
depression_impervious = area_information.get('depression_impervious', 0)
|
338
|
+
if depression_impervious < 0:
|
339
|
+
warn(f"Depression storage for impervious area cannot be negative, got {depression_impervious}")
|
340
|
+
new_area.depression_impervious = depression_impervious
|
341
|
+
|
342
|
+
depression_pervious = area_information.get('depression_pervious', 0)
|
343
|
+
if depression_pervious < 0:
|
344
|
+
warn(f"Depression storage for pervious area cannot be negative, got {depression_pervious}")
|
345
|
+
new_area.depression_pervious = depression_pervious
|
346
|
+
|
347
|
+
impervious_without_depression = area_information.get('impervious_without_depression', 0)
|
348
|
+
if not (0 <= impervious_without_depression <= 100):
|
349
|
+
warn(f"Impervious without depression must be 0-100%, got {impervious_without_depression}")
|
350
|
+
new_area.impervious_without_depression = impervious_without_depression
|
351
|
+
|
352
|
+
# Set routing attributes
|
353
|
+
new_area.route_type = area_information.get('route_type', 'OUTLET')
|
354
|
+
new_area.route_type_ratio = area_information.get('route_type_ratio', 100)
|
355
|
+
|
356
|
+
# Handle infiltration parameters
|
169
357
|
if 'infiltration' in area_information:
|
170
358
|
new_area.infiltration = area_information['infiltration']
|
171
|
-
|
172
|
-
|
359
|
+
else:
|
360
|
+
# Set values for Horton infiltration
|
361
|
+
if 'horton_maximum_rate' in area_information:
|
362
|
+
new_area.infiltration.horton.maximum_rate = area_information.get('horton_maximum_rate')
|
363
|
+
if 'horton_minimum_rate' in area_information:
|
364
|
+
new_area.infiltration.horton.minimum_rate = area_information.get('horton_minimum_rate')
|
365
|
+
if 'horton_decay_rate' in area_information:
|
366
|
+
new_area.infiltration.horton.decay_rate = area_information.get('horton_decay_rate')
|
367
|
+
if 'horton_dry_time' in area_information:
|
368
|
+
new_area.infiltration.horton.dry_time = area_information.get('horton_dry_time')
|
369
|
+
if 'horton_maximum_infiltration_volume' in area_information:
|
370
|
+
new_area.infiltration.horton.maximum_infiltration_volume = area_information.get('horton_maximum_infiltration_volume')
|
371
|
+
|
372
|
+
# Set values for Green-Ampt infiltration
|
373
|
+
if 'green_ampt_soil_capillary_suction' in area_information:
|
374
|
+
new_area.infiltration.green_ampt.soil_capillary_suction = area_information.get('green_ampt_soil_capillary_suction')
|
375
|
+
if 'green_ampt_soil_saturated_hydraulic_conductivity' in area_information:
|
376
|
+
new_area.infiltration.green_ampt.soil_saturated_hydraulic_conductivity = area_information.get('green_ampt_soil_saturated_hydraulic_conductivity')
|
377
|
+
if 'green_ampt_initial_soil_moisture_deficit' in area_information:
|
378
|
+
new_area.infiltration.green_ampt.initial_soil_moisture_deficit = area_information.get('green_ampt_initial_soil_moisture_deficit')
|
379
|
+
|
380
|
+
# Set values for Curve Number infiltration
|
381
|
+
if 'curve_number' in area_information:
|
382
|
+
new_area.infiltration.curve_number.curve_number = area_information.get('curve_number')
|
383
|
+
if 'curve_number_dry_time' in area_information:
|
384
|
+
new_area.infiltration.curve_number.dry_time = area_information.get('curve_number_dry_time')
|
385
|
+
if 'curve_number_soil_saturated_hydraulic_conductivity' in area_information:
|
386
|
+
new_area.infiltration.curve_number.soil_saturated_hydraulic_conductivity = area_information.get('curve_number_soil_saturated_hydraulic_conductivity')
|
387
|
+
|
388
|
+
# Check if the generated default name is unique (when name wasn't explicitly provided)
|
389
|
+
if 'name' not in area_information and any(area.name == new_area.name for area in self.data):
|
390
|
+
raise ValueError(f"Generated default name '{new_area.name}' already exists")
|
391
|
+
|
392
|
+
# Add the new area to the collection
|
173
393
|
self.data.append(new_area)
|
394
|
+
return new_area
|
174
395
|
|
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
|
-
if pair[8] != 'VOID':
|
213
|
-
dic['snow_pack'] = pair[8]
|
214
|
-
if pair[15] != '100':
|
215
|
-
dic['route_type_ratio'] = float(pair[15])
|
216
|
-
#
|
217
|
-
new_infiltration = Infiltration()
|
396
|
+
@staticmethod
|
397
|
+
def _prepare_section_contents(filename):
|
398
|
+
"""
|
399
|
+
Prepares and combines section contents from SWMM input file.
|
400
|
+
|
401
|
+
Args:
|
402
|
+
filename (str): Path to the SWMM input file
|
403
|
+
|
404
|
+
Returns:
|
405
|
+
tuple: Combined content, polygon content
|
406
|
+
"""
|
407
|
+
try:
|
408
|
+
# Get subcatchments section and normalize
|
409
|
+
sub_contents = get_swmm_inp_content(filename, '[SUBCATCHMENTS]')
|
410
|
+
for index, line in enumerate(sub_contents):
|
411
|
+
if len(line.split()) == 8:
|
412
|
+
sub_contents[index] += ' VOID'
|
413
|
+
|
414
|
+
# Get subareas section and normalize
|
415
|
+
subarea_contents = get_swmm_inp_content(filename, '[SUBAREAS]')
|
416
|
+
for index, line in enumerate(subarea_contents):
|
417
|
+
if len(line.split()) == 7:
|
418
|
+
subarea_contents[index] += ' 100'
|
419
|
+
|
420
|
+
# Combine contents
|
421
|
+
content = combine_swmm_inp_contents(sub_contents, subarea_contents)
|
422
|
+
|
423
|
+
# Add infiltration data
|
424
|
+
infiltration_contents = get_swmm_inp_content(filename, '[INFILTRATION]')
|
425
|
+
content = combine_swmm_inp_contents(content, infiltration_contents)
|
426
|
+
|
427
|
+
# Get polygon data separately
|
428
|
+
polygon_contents = get_swmm_inp_content(filename, '[Polygons]')
|
429
|
+
|
430
|
+
return content, polygon_contents
|
431
|
+
except Exception as e:
|
432
|
+
raise IOError(f"Error preparing SWMM input sections: {str(e)}")
|
218
433
|
|
434
|
+
@staticmethod
|
435
|
+
def _parse_infiltration_data(pair, infiltration_type):
|
436
|
+
"""
|
437
|
+
Parses infiltration data based on the specified infiltration type.
|
438
|
+
|
439
|
+
Args:
|
440
|
+
pair (list): Split line from the input file
|
441
|
+
infiltration_type (str): Type of infiltration model
|
442
|
+
|
443
|
+
Returns:
|
444
|
+
dict: Dictionary containing infiltration parameters
|
445
|
+
"""
|
446
|
+
try:
|
447
|
+
infiltration_data = {}
|
219
448
|
match infiltration_type:
|
220
449
|
case 'Horton':
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
450
|
+
infiltration_data['horton_maximum_rate'] = float(pair[16])
|
451
|
+
infiltration_data['horton_minimum_rate'] = float(pair[17])
|
452
|
+
infiltration_data['horton_decay_rate'] = float(pair[18])
|
453
|
+
infiltration_data['horton_dry_time'] = float(pair[19])
|
454
|
+
infiltration_data['horton_maximum_infiltration_volume'] = float(pair[20])
|
226
455
|
case 'GreenAmpt':
|
227
|
-
|
228
|
-
|
229
|
-
|
456
|
+
infiltration_data['green_ampt_soil_capillary_suction'] = float(pair[16])
|
457
|
+
infiltration_data['green_ampt_soil_saturated_hydraulic_conductivity'] = float(pair[17])
|
458
|
+
infiltration_data['green_ampt_initial_soil_moisture_deficit'] = float(pair[18])
|
230
459
|
case 'CurveNumber':
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
460
|
+
infiltration_data['curve_number'] = float(pair[16])
|
461
|
+
infiltration_data['curve_number_soil_saturated_hydraulic_conductivity'] = float(pair[17])
|
462
|
+
infiltration_data['curve_number_dry_time'] = float(pair[18])
|
463
|
+
case _:
|
464
|
+
raise ValueError(f"Unsupported infiltration type: {infiltration_type}")
|
465
|
+
return infiltration_data
|
466
|
+
except (IndexError, ValueError) as e:
|
467
|
+
raise ValueError(f"Error parsing infiltration data: {str(e)}")
|
468
|
+
|
469
|
+
def _process_polygon_data(self, polygon_contents):
|
470
|
+
"""
|
471
|
+
Processes polygon data and assigns to corresponding areas.
|
472
|
+
|
473
|
+
Args:
|
474
|
+
polygon_contents (list): Lines from the Polygons section
|
475
|
+
"""
|
241
476
|
for line in polygon_contents:
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
477
|
+
try:
|
478
|
+
pair = line.split()
|
479
|
+
area_name = pair[0]
|
480
|
+
x_coord = float(pair[1])
|
481
|
+
y_coord = float(pair[2])
|
482
|
+
|
483
|
+
for area in self.data:
|
484
|
+
if area.name == area_name:
|
485
|
+
area.polygon.x.append(x_coord)
|
486
|
+
area.polygon.y.append(y_coord)
|
487
|
+
area.polygon.area_name = area_name
|
488
|
+
except (IndexError, ValueError) as e:
|
489
|
+
# Log warning but continue processing other polygons
|
490
|
+
print(f"Warning: Error processing polygon data: {str(e)}")
|
491
|
+
|
492
|
+
def read_from_swmm_inp(self, filename):
|
493
|
+
"""
|
494
|
+
Reads subcatchment area data from a SWMM .inp file and populates the area list.
|
495
|
+
|
496
|
+
Args:
|
497
|
+
filename (str): Path to the input .inp file
|
498
|
+
|
499
|
+
Returns:
|
500
|
+
int: 0 on success, raises exceptions on failure
|
501
|
+
|
502
|
+
Raises:
|
503
|
+
IOError: If file operations fail
|
504
|
+
ValueError: If data parsing fails
|
505
|
+
"""
|
506
|
+
try:
|
507
|
+
# First, get the infiltration method from the OPTIONS section
|
508
|
+
options_contents = get_swmm_inp_content(filename, '[OPTIONS]')
|
509
|
+
infiltration_type = 'Horton' # Default fallback
|
510
|
+
|
511
|
+
# Extract the infiltration method from OPTIONS section
|
512
|
+
for line in options_contents:
|
513
|
+
pair = line.split()
|
514
|
+
if len(pair) >= 2 and pair[0] == 'INFILTRATION':
|
515
|
+
# Convert SWMM infiltration method to our internal format
|
516
|
+
if pair[1].upper() in ['HORTON', 'MODIFIED_HORTON']:
|
517
|
+
infiltration_type = 'Horton'
|
518
|
+
elif pair[1].upper() in ['GREEN_AMPT', 'MODIFIED_GREEN_AMPT']:
|
519
|
+
infiltration_type = 'GreenAmpt'
|
520
|
+
elif pair[1].upper() == 'CURVE_NUMBER':
|
521
|
+
infiltration_type = 'CurveNumber'
|
522
|
+
break
|
523
|
+
|
524
|
+
# Prepare and get section contents
|
525
|
+
content, polygon_contents = self._prepare_section_contents(filename)
|
526
|
+
|
527
|
+
# Process each line of combined content
|
528
|
+
for line in content:
|
529
|
+
pair = line.split()
|
530
|
+
|
531
|
+
# Create basic area information dictionary
|
532
|
+
dic = {
|
533
|
+
'name': pair[0],
|
534
|
+
'rain_gage': pair[1],
|
535
|
+
'outlet': pair[2],
|
536
|
+
'area': float(pair[3]),
|
537
|
+
'impervious_ratio': float(pair[4]),
|
538
|
+
'width': float(pair[5]),
|
539
|
+
'slope': float(pair[6]),
|
540
|
+
'curb_length': float(pair[7]),
|
541
|
+
'manning_impervious': float(pair[9]),
|
542
|
+
'manning_pervious': float(pair[10]),
|
543
|
+
'depression_impervious': float(pair[11]),
|
544
|
+
'depression_pervious': float(pair[12]),
|
545
|
+
'impervious_without_depression': float(pair[13]),
|
546
|
+
'route_type': pair[14]
|
547
|
+
}
|
548
|
+
|
549
|
+
# Parse and add infiltration data
|
550
|
+
infiltration_data = self._parse_infiltration_data(pair, infiltration_type)
|
551
|
+
dic.update(infiltration_data)
|
552
|
+
|
553
|
+
# Handle special cases
|
554
|
+
if dic['curb_length'] < 10e-5:
|
555
|
+
dic['curb_length'] = int(0)
|
556
|
+
|
557
|
+
if pair[8] != 'VOID':
|
558
|
+
dic['snow_pack'] = pair[8]
|
559
|
+
|
560
|
+
if pair[15] != '100':
|
561
|
+
dic['route_type_ratio'] = float(pair[15])
|
562
|
+
|
563
|
+
# Add the area to the collection
|
564
|
+
self.add_area(dic)
|
565
|
+
|
566
|
+
# Process polygon data
|
567
|
+
self._process_polygon_data(polygon_contents)
|
568
|
+
|
569
|
+
return 0
|
570
|
+
|
571
|
+
except Exception as e:
|
572
|
+
raise IOError(f"Error reading SWMM input file: {str(e)}")
|
573
|
+
|
574
|
+
def _write_subcatchments_section(self, f):
|
575
|
+
"""
|
576
|
+
Writes the SUBCATCHMENTS section to the file.
|
577
|
+
|
578
|
+
Args:
|
579
|
+
f (file): Open file handle to write to
|
580
|
+
"""
|
581
|
+
f.write('\n\n[SUBCATCHMENTS]\n')
|
582
|
+
f.write(';;Name RainGage Outlet Area %Imperv Width %Slope CurbLen (SnowPack)\n')
|
583
|
+
for area in self.data:
|
253
584
|
f.write(
|
254
|
-
'
|
255
|
-
|
256
|
-
f.
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
585
|
+
f'{area.name} {area.rain_gage} {area.outlet} {area.area:8.3f} '
|
586
|
+
f'{area.impervious_ratio:8.2f} {area.width:8.3f} {area.slope:8.2f} '
|
587
|
+
f'{area.curb_length:8} {area.snow_pack}\n')
|
588
|
+
|
589
|
+
def _write_subareas_section(self, f):
|
590
|
+
"""
|
591
|
+
Writes the SUBAREAS section to the file.
|
592
|
+
|
593
|
+
Args:
|
594
|
+
f (file): Open file handle to write to
|
595
|
+
"""
|
596
|
+
f.write('\n\n[SUBAREAS]\n')
|
597
|
+
f.write(';;Subcatchment N-Imperv N-Perv S-Imperv S-Perv PctZero RouteTo (PctRouted)\n')
|
598
|
+
for area in self.data:
|
599
|
+
base_str = (f'{area.name} {area.manning_impervious:8.3f} {area.manning_pervious:8.2f} '
|
600
|
+
f'{area.depression_impervious:8.2f} {area.depression_pervious:8.2f} '
|
601
|
+
f'{area.impervious_without_depression:8.2f} {area.route_type:8}')
|
602
|
+
|
603
|
+
if area.route_type_ratio != 100:
|
604
|
+
f.write(f'{base_str} {area.route_type_ratio:8}\n')
|
605
|
+
else:
|
606
|
+
f.write(f'{base_str}\n')
|
607
|
+
|
608
|
+
def _write_infiltration_section(self, f, infiltration_type):
|
609
|
+
"""
|
610
|
+
Writes the INFILTRATION section to the file based on infiltration type.
|
611
|
+
|
612
|
+
Args:
|
613
|
+
f (file): Open file handle to write to
|
614
|
+
infiltration_type (str): Type of infiltration model
|
615
|
+
|
616
|
+
Raises:
|
617
|
+
ValueError: If infiltration type is not supported
|
618
|
+
"""
|
619
|
+
f.write('\n\n[INFILTRATION]\n')
|
620
|
+
|
621
|
+
match infiltration_type:
|
622
|
+
case 'Horton':
|
623
|
+
f.write(';;;;Subcatchment MaxRate MinRate Decay DryTime MaxInfil \n')
|
624
|
+
for area in self.data:
|
625
|
+
horton = area.infiltration.horton
|
263
626
|
f.write(
|
264
|
-
f'{area.name} {
|
265
|
-
|
627
|
+
f'{area.name} {horton.maximum_rate:8.1f} {horton.minimum_rate:8.1f} '
|
628
|
+
f'{horton.decay_rate:8.1f} {horton.dry_time:8.1f} '
|
629
|
+
f'{horton.maximum_infiltration_volume:8.1f}\n')
|
630
|
+
case 'GreenAmpt':
|
631
|
+
f.write(';;;;Subcatchment Suction Conductivity InitialDeficit\n')
|
632
|
+
for area in self.data:
|
633
|
+
ga = area.infiltration.green_ampt
|
266
634
|
f.write(
|
267
|
-
f'{area.name} {
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
635
|
+
f'{area.name} {ga.soil_capillary_suction:8} '
|
636
|
+
f'{ga.soil_saturated_hydraulic_conductivity:8} '
|
637
|
+
f'{ga.initial_soil_moisture_deficit:8}\n')
|
638
|
+
case 'CurveNumber':
|
639
|
+
f.write(';;;;Subcatchment CurveNum Conductivity DryTime\n')
|
640
|
+
for area in self.data:
|
641
|
+
cn = area.infiltration.curve_number
|
642
|
+
f.write(
|
643
|
+
f'{area.name} {cn.curve_number:8} '
|
644
|
+
f'{cn.soil_saturated_hydraulic_conductivity:8} '
|
645
|
+
f'{cn.dry_time:8}\n')
|
646
|
+
case _:
|
647
|
+
raise ValueError(f"Unsupported infiltration type: {infiltration_type}")
|
648
|
+
|
649
|
+
def _write_polygons_section(self, file):
|
650
|
+
"""
|
651
|
+
Writes the Polygons section to the file.
|
652
|
+
|
653
|
+
Args:
|
654
|
+
file (file): Open file handle to write to
|
655
|
+
"""
|
656
|
+
# Check if any areas have polygon data
|
657
|
+
has_polygons = any(area.polygon.area_name is not None for area in self.data)
|
658
|
+
|
659
|
+
if has_polygons:
|
660
|
+
# Read file line by line to find sections
|
661
|
+
with open(file.name, 'r') as read_file:
|
662
|
+
lines = read_file.readlines()
|
663
|
+
# Find [Polygons] section
|
664
|
+
polygons_line = -1
|
665
|
+
next_section_line = -1
|
666
|
+
for i, line in enumerate(lines):
|
667
|
+
if line.strip() == '[Polygons]':
|
668
|
+
polygons_line = i
|
669
|
+
elif polygons_line != -1 and line.strip().startswith('['):
|
670
|
+
next_section_line = i
|
671
|
+
break
|
672
|
+
if polygons_line == -1:
|
673
|
+
# No existing section, create new one at current position
|
674
|
+
file.write('\n\n[Polygons]\n')
|
675
|
+
file.write(';;Name X-Coord Y-Coord\n')
|
676
|
+
|
677
|
+
# Write polygon data
|
678
|
+
for area in self.data:
|
679
|
+
if area.polygon.area_name is not None:
|
680
|
+
for xi, yi in zip(area.polygon.x, area.polygon.y):
|
681
|
+
file.write(f'{area.polygon.area_name} {xi} {yi}\n')
|
682
|
+
else:
|
683
|
+
# Section exists, we need to modify file content
|
684
|
+
# Insert our polygon data just after the header line
|
685
|
+
insert_position = polygons_line + 2 # +1 for the header, +1 for the column labels
|
686
|
+
|
687
|
+
# Prepare polygon data lines
|
688
|
+
new_lines = []
|
689
|
+
for area in self.data:
|
690
|
+
if area.polygon.area_name is not None:
|
691
|
+
for xi, yi in zip(area.polygon.x, area.polygon.y):
|
692
|
+
new_lines.append(f'{area.polygon.area_name} {xi} {yi}\n')
|
693
|
+
|
694
|
+
# Insert the new lines at the appropriate position
|
695
|
+
lines[insert_position:insert_position] = new_lines
|
696
|
+
|
697
|
+
# Rewrite the entire file
|
698
|
+
file.seek(0)
|
699
|
+
file.writelines(lines)
|
700
|
+
file.truncate()
|
701
|
+
|
702
|
+
def write_to_swmm_inp(self, filename):
|
703
|
+
"""
|
704
|
+
Writes subcatchment area data to a SWMM .inp file.
|
705
|
+
|
706
|
+
Args:
|
707
|
+
filename (str): Path to the output .inp file
|
708
|
+
|
709
|
+
Returns:
|
710
|
+
int: 0 on success, raises exceptions on failure
|
711
|
+
|
712
|
+
Raises:
|
713
|
+
IOError: If file operations fail
|
714
|
+
ValueError: If infiltration type is not supported
|
715
|
+
"""
|
716
|
+
try:
|
717
|
+
# Determine infiltration type to use
|
718
|
+
infiltration_type = 'Horton' # Default fallback
|
719
|
+
|
720
|
+
# Check if file exists to read infiltration type from it
|
721
|
+
import os
|
722
|
+
if os.path.exists(filename):
|
723
|
+
try:
|
724
|
+
# Get infiltration method from the OPTIONS section if file exists
|
725
|
+
options_contents = get_swmm_inp_content(filename, '[OPTIONS]')
|
726
|
+
|
727
|
+
# Extract the infiltration method from OPTIONS section
|
728
|
+
for line in options_contents:
|
729
|
+
pair = line.split()
|
730
|
+
if len(pair) >= 2 and pair[0] == 'INFILTRATION':
|
731
|
+
# Convert SWMM infiltration method to our internal format
|
732
|
+
if pair[1].upper() in ['HORTON', 'MODIFIED_HORTON']:
|
733
|
+
infiltration_type = 'Horton'
|
734
|
+
elif pair[1].upper() in ['GREEN_AMPT', 'MODIFIED_GREEN_AMPT']:
|
735
|
+
infiltration_type = 'GreenAmpt'
|
736
|
+
elif pair[1].upper() == 'CURVE_NUMBER':
|
737
|
+
infiltration_type = 'CurveNumber'
|
738
|
+
break
|
739
|
+
except Exception as e:
|
740
|
+
# If there's an error reading the file, use default infiltration type
|
741
|
+
print(f"Warning: Could not read infiltration type from file: {str(e)}")
|
742
|
+
print(f"Using default infiltration type: {infiltration_type}")
|
743
|
+
|
744
|
+
with open(filename, 'a', encoding='utf-8') as f:
|
745
|
+
self._write_subcatchments_section(f)
|
746
|
+
self._write_subareas_section(f)
|
747
|
+
self._write_infiltration_section(f, infiltration_type)
|
748
|
+
self._write_polygons_section(f)
|
293
749
|
return 0
|
750
|
+
except Exception as e:
|
751
|
+
raise IOError(f"Error writing to SWMM input file: {str(e)}")
|
294
752
|
|