easysewer 0.0.2__py3-none-any.whl → 0.0.4__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 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 = 50 # mm/h
13
- self.minimum_rate = 5 # mm/h
14
- self.decay_rate = 5 # 1/h
15
- self.dry_time = 7 # day
16
- self.maximum_infiltration_volume = 0 # mm, 0 if not applicable
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
- self.soil_capillary_suction = 0
22
- self.soil_saturated_hydraulic_conductivity = 0
23
- self.initial_soil_moisture_deficit = 0
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
- self.curve_number = 0
29
- self.dry_time = 0
30
- self.soil_saturated_hydraulic_conductivity = 0
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
- new_area = Area()
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
- new_area.name = area_information['name']
134
- if 'rain_gage' in area_information:
135
- new_area.rain_gage = area_information['rain_gage']
136
- if 'outlet' in area_information:
137
- new_area.outlet = area_information['outlet']
138
- #
139
- if 'area' in area_information:
140
- new_area.area = area_information['area']
141
- if 'impervious_ratio' in area_information:
142
- new_area.impervious_ratio = area_information['impervious_ratio']
143
- if 'width' in area_information:
144
- new_area.width = area_information['width']
145
- if 'slope' in area_information:
146
- new_area.slope = area_information['slope']
147
- #
148
- if 'curb_length' in area_information:
149
- new_area.curb_length = area_information['curb_length']
150
- if 'snow_pack' in area_information:
151
- new_area.snow_pack = area_information['snow_pack']
152
- #
153
- if 'manning_impervious' in area_information:
154
- new_area.manning_impervious = area_information['manning_impervious']
155
- if 'manning_pervious' in area_information:
156
- new_area.manning_pervious = area_information['manning_pervious']
157
- if 'depression_impervious' in area_information:
158
- new_area.depression_impervious = area_information['depression_impervious']
159
- if 'depression_pervious' in area_information:
160
- new_area.depression_pervious = area_information['depression_pervious']
161
- if 'impervious_without_depression' in area_information:
162
- new_area.impervious_without_depression = area_information['impervious_without_depression']
163
- #
164
- if 'route_type' in area_information:
165
- new_area.route_type = area_information['route_type']
166
- if 'route_type_ratio' in area_information:
167
- new_area.route_type_ratio = area_information['route_type_ratio']
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
- def read_from_swmm_inp(self, filename, infiltration_type='Horton'):
176
- sub_contents = get_swmm_inp_content(filename, '[SUBCATCHMENTS]')
177
- # fill in default values
178
- for index, line in enumerate(sub_contents):
179
- if len(line.split()) == 8:
180
- sub_contents[index] += ' VOID'
181
- #
182
- subarea_contents = get_swmm_inp_content(filename, '[SUBAREAS]')
183
- # fill in default values
184
- for index, line in enumerate(subarea_contents):
185
- if len(line.split()) == 7:
186
- subarea_contents[index] += ' 100'
187
- content = combine_swmm_inp_contents(sub_contents, subarea_contents)
188
- #
189
- infiltration_contents = get_swmm_inp_content(filename, '[INFILTRATION]')
190
- content = combine_swmm_inp_contents(content, infiltration_contents)
191
-
192
- for line in content:
193
- pair = line.split()
194
- dic = {'name': pair[0],
195
- 'rain_gage': pair[1],
196
- 'outlet': pair[2],
197
- 'area': float(pair[3]),
198
- 'impervious_ratio': float(pair[4]),
199
- 'width': float(pair[5]),
200
- 'slope': float(pair[6]),
201
- 'curb_length': float(pair[7]),
202
- 'manning_impervious': float(pair[9]),
203
- 'manning_pervious': float(pair[10]),
204
- 'depression_impervious': float(pair[11]),
205
- 'depression_pervious': float(pair[12]),
206
- 'impervious_without_depression': float(pair[13]),
207
- 'route_type': pair[14]
208
- }
209
- if dic['curb_length'] < 10e-5:
210
- dic['curb_length'] = int(0)
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
- new_infiltration.horton.maximum_rate = float(pair[16])
222
- new_infiltration.horton.minimum_rate = float(pair[17])
223
- new_infiltration.horton.decay_rate = float(pair[18])
224
- new_infiltration.horton.dry_time = float(pair[19])
225
- new_infiltration.horton.maximum_infiltration_volume = float(pair[20])
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
- new_infiltration.green_ampt.soil_capillary_suction = float(pair[16])
228
- new_infiltration.green_ampt.soil_saturated_hydraulic_conductivity = float(pair[17])
229
- new_infiltration.green_ampt.initial_soil_moisture_deficit = float(pair[18])
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
- new_infiltration.curve_number.curve_number = float(pair[16])
232
- new_infiltration.curve_number.soil_saturated_hydraulic_conductivity = float(pair[17])
233
- new_infiltration.curve_number.dry_time = float(pair[18])
234
-
235
- dic['infiltration'] = new_infiltration
236
- #
237
- self.add_area(dic)
238
-
239
- #
240
- polygon_contents = get_swmm_inp_content(filename, '[Polygons]')
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
- pair = line.split()
243
- for area in self.data:
244
- if area.name == pair[0]:
245
- area.polygon.x.append(float(pair[1]))
246
- area.polygon.y.append(float(pair[2]))
247
- area.polygon.area_name = pair[0]
248
- return 0
249
-
250
- def write_to_swmm_inp(self, filename, infiltration_type='Horton'):
251
- with open(filename, 'a', encoding='utf-8') as f:
252
- f.write('\n\n[SUBCATCHMENTS]\n')
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
- ';;Name RainGage Outlet Area %Imperv Width %Slope CurbLen (SnowPack)\n')
255
- for area in self.data:
256
- f.write(
257
- f'{area.name} {area.rain_gage} {area.outlet} {area.area:8.3f} {area.impervious_ratio:8.2f} {area.width:8.3f} {area.slope:8.2f} {area.curb_length:8} {area.snow_pack}\n')
258
- #
259
- f.write('\n\n[SUBAREAS]\n')
260
- f.write(';;Subcatchment N-Imperv N-Perv S-Imperv S-Perv PctZero RouteTo (PctRouted)\n')
261
- for area in self.data:
262
- if area.route_type_ratio != 100:
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} {area.manning_impervious:8.3f} {area.manning_pervious:8.2f} {area.depression_impervious:8.2f} {area.depression_pervious:8.2f} {area.impervious_without_depression:8.2f} {area.route_type:8} {area.route_type_ratio:8}\n')
265
- else:
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} {area.manning_impervious:8.3f} {area.manning_pervious:8.2f} {area.depression_impervious:8.2f} {area.depression_pervious:8.2f} {area.impervious_without_depression:8.2f} {area.route_type:8}\n')
268
- #
269
- f.write('\n\n[INFILTRATION]\n')
270
- match infiltration_type:
271
- case 'Horton':
272
- f.write(';;;;Subcatchment MaxRate MinRate Decay DryTime MaxInfil \n')
273
- for area in self.data:
274
- f.write(
275
- f'{area.name} {area.infiltration.horton.maximum_rate:8.1f} {area.infiltration.horton.minimum_rate:8.1f} {area.infiltration.horton.decay_rate:8.1f} {area.infiltration.horton.dry_time:8.1f} {area.infiltration.horton.maximum_infiltration_volume:8.1f}\n')
276
- case 'GreenAmpt':
277
- f.write(';;;;Subcatchment \n')
278
- for area in self.data:
279
- f.write(
280
- f'{area.name} {area.infiltration.green_ampt.soil_capillary_suction:8} {area.infiltration.green_ampt.soil_saturated_hydraulic_conductivity:8} {area.infiltration.green_ampt.initial_soil_moisture_deficit:8}\n')
281
- case 'CurveNumber':
282
- f.write(';;;;Subcatchment \n')
283
- for area in self.data:
284
- f.write(
285
- f'{area.name} {area.infiltration.curve_number.curve_number:8} {area.infiltration.curve_number.soil_saturated_hydraulic_conductivity:8} {area.infiltration.curve_number.dry_time:8}\n')
286
- #
287
- f.write('\n\n[Polygons]\n')
288
- f.write(';;Subcatchment X-Coord Y-Coord\n')
289
- for area in self.data:
290
- if area.polygon.area_name is not None:
291
- for xi, yi in zip(area.polygon.x, area.polygon.y):
292
- f.write(f'{area.polygon.area_name} {xi} {yi}\n')
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