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/Link.py CHANGED
@@ -73,6 +73,16 @@ class Conduit(Link):
73
73
 
74
74
 
75
75
  class ConduitCircle(Conduit):
76
+ """
77
+ Circular conduit type.
78
+
79
+ Represents a conduit with circular cross-section.
80
+
81
+ Attributes:
82
+ Inherits all attributes from Conduit class
83
+ barrels_number (int): Number of identical barrels (pipes)
84
+ height (float): Diameter of the circular conduit
85
+ """
76
86
  def __init__(self):
77
87
  Conduit.__init__(self)
78
88
  self.barrels_number = 1
@@ -80,6 +90,17 @@ class ConduitCircle(Conduit):
80
90
 
81
91
 
82
92
  class ConduitFilledCircle(Conduit):
93
+ """
94
+ Partially filled circular conduit type.
95
+
96
+ Represents a circular conduit with sediment or partial filling.
97
+
98
+ Attributes:
99
+ Inherits all attributes from Conduit class
100
+ barrels_number (int): Number of identical barrels (pipes)
101
+ height (float): Diameter of the circular conduit
102
+ filled (float): Height of filling/sediment from bottom
103
+ """
83
104
  def __init__(self):
84
105
  Conduit.__init__(self)
85
106
  self.barrels_number = 1
@@ -88,6 +109,17 @@ class ConduitFilledCircle(Conduit):
88
109
 
89
110
 
90
111
  class ConduitRectangleOpen(Conduit):
112
+ """
113
+ Open rectangular conduit type.
114
+
115
+ Represents an open channel with rectangular cross-section.
116
+
117
+ Attributes:
118
+ Inherits all attributes from Conduit class
119
+ barrels_number (int): Number of identical barrels (channels)
120
+ height (float): Height of the rectangular channel
121
+ width (float): Width of the rectangular channel
122
+ """
91
123
  def __init__(self):
92
124
  Conduit.__init__(self)
93
125
  self.barrels_number = 1
@@ -96,6 +128,17 @@ class ConduitRectangleOpen(Conduit):
96
128
 
97
129
 
98
130
  class ConduitCustom(Conduit):
131
+ """
132
+ Custom conduit type.
133
+
134
+ Represents a conduit with custom-defined cross-section based on a curve.
135
+
136
+ Attributes:
137
+ Inherits all attributes from Conduit class
138
+ barrels_number (int): Number of identical barrels
139
+ height (float): Maximum height of the custom cross-section
140
+ curve (str): Name of the curve defining the custom cross-section
141
+ """
99
142
  def __init__(self):
100
143
  Conduit.__init__(self)
101
144
  self.barrels_number = 1
@@ -104,16 +147,50 @@ class ConduitCustom(Conduit):
104
147
 
105
148
 
106
149
  class LinkList:
150
+ """
151
+ A collection class for managing links in a drainage network.
152
+
153
+ This class provides storage and management for various types of links (conduits),
154
+ with methods for adding, accessing, and processing links.
155
+
156
+ Attributes:
157
+ data (list): List containing all link objects
158
+ """
107
159
  def __init__(self):
108
160
  self.data = []
109
161
 
110
162
  def __repr__(self):
163
+ """
164
+ Returns a string representation of the LinkList.
165
+
166
+ Returns:
167
+ str: A string showing the count of links in the list
168
+ """
111
169
  return f'{len(self.data)} Links'
112
170
 
113
171
  def __len__(self):
172
+ """
173
+ Returns the number of links in the list.
174
+
175
+ Returns:
176
+ int: Number of links in the list
177
+ """
114
178
  return len(self.data)
115
179
 
116
180
  def __getitem__(self, key):
181
+ """
182
+ Gets a link by index or name.
183
+
184
+ Args:
185
+ key (int|str): Index or name of link to retrieve
186
+
187
+ Returns:
188
+ Link: The requested link
189
+
190
+ Raises:
191
+ KeyError: If link name not found
192
+ TypeError: If key is not int or str
193
+ """
117
194
  if isinstance(key, int):
118
195
  return self.data[key]
119
196
  elif isinstance(key, str):
@@ -125,170 +202,403 @@ class LinkList:
125
202
  raise TypeError("Key must be an integer or a string")
126
203
 
127
204
  def __iter__(self):
205
+ """
206
+ Returns an iterator for the link list.
207
+
208
+ Returns:
209
+ iterator: Iterator for the links
210
+ """
128
211
  return iter(self.data)
129
212
 
130
213
  def __contains__(self, item):
214
+ """
215
+ Checks if a link exists in the list.
216
+
217
+ Args:
218
+ item: Link to check for
219
+
220
+ Returns:
221
+ bool: True if link exists in list
222
+ """
131
223
  return item in self.data
132
224
 
133
- def add_link(self, link_type, link_information):
134
- def execute(func1, func2):
135
- def inner():
136
- new_link = func1()
137
- # basic information of conduit
138
- new_link.name = link_information['name']
139
- new_link.upstream_node = link_information['upstream_node']
140
- new_link.downstream_node = link_information['downstream_node']
141
- new_link.length = link_information['length']
142
- new_link.roughness = link_information['roughness']
143
- new_link.upstream_offset = link_information['upstream_offset']
144
- new_link.downstream_offset = link_information['downstream_offset']
145
- if 'initial_flow' in link_information:
146
- new_link.initial_flow = link_information['initial_flow']
147
- if 'maximum_flow' in link_information:
148
- new_link.maximum_flow = link_information['maximum_flow']
149
- # specific information of different conduit type
150
- func2(new_link)
151
- # add new link to link list
152
- self.data.append(new_link)
153
- return 0
154
-
155
- return inner
156
-
157
- match link_type:
158
- case 'conduit_circle' | 'ConduitCircle':
159
- def conduit_circle(new_link):
160
- new_link.height = link_information['height']
161
- if 'barrels_number' in link_information:
162
- new_link.barrels_number = link_information['barrels_number']
163
-
164
- return execute(ConduitCircle, conduit_circle)()
165
-
166
- case 'conduit_filled_circle' | 'ConduitFilledCircle':
167
-
168
- def conduit_filled_circle(new_link):
169
- new_link.height = link_information['height']
170
- new_link.filled = link_information['filled']
171
- if 'barrels_number' in link_information:
172
- new_link.barrels_number = link_information['barrels_number']
173
-
174
- return execute(ConduitFilledCircle, conduit_filled_circle)()
175
-
176
- case 'conduit_rectangle_open' | 'ConduitRectangleOpen':
177
- def conduit_rectangle_open(new_link):
178
- new_link.height = link_information['height']
179
- new_link.width = link_information['width']
180
- if 'barrels_number' in link_information:
181
- new_link.barrels_number = link_information['barrels_number']
182
-
183
- return execute(ConduitRectangleOpen, conduit_rectangle_open)()
184
-
185
- case 'conduit_custom' | 'ConduitCustom':
186
- def conduit_custom(new_link):
187
- new_link.height = link_information['height']
188
- new_link.curve = link_information['curve']
189
- if 'barrels_number' in link_information:
190
- new_link.barrels_number = link_information['barrels_number']
191
-
192
- return execute(ConduitCustom, conduit_custom)()
193
-
194
- case _:
195
- raise TypeError(f"Unknown link type, failed to add {link_information['name']}")
225
+ def add_link(self, link_type, link_information=None):
226
+ """
227
+ Add a new link to the data structure based on its type and information.
228
+ Generates default values for missing essential attributes.
229
+
230
+ Args:
231
+ link_type (str): Type of link to add (e.g., 'conduit_circle', 'conduit_filled_circle')
232
+ link_information (dict, optional): Dictionary containing link attributes
233
+ Defaults to empty dict if None
234
+
235
+ Returns:
236
+ Link: The newly created link object
237
+
238
+ Raises:
239
+ TypeError: If link_type is not recognized
240
+ ValueError: If a link with the same name already exists
241
+ or if required attributes are missing
242
+
243
+ Example:
244
+ >>> links = LinkList()
245
+ >>> links.add_link('conduit_circle', {'name': 'C1', 'upstream_node': 'J1',
246
+ ... 'downstream_node': 'J2', 'height': 0.5})
247
+ <Link C1>
248
+ """
249
+ # Initialize link_information dict if not provided
250
+ if link_information is None:
251
+ link_information = {}
252
+
253
+ # Normalize link type: lowercase and handle both formats
254
+ normalized_type = link_type.lower().replace('_', '')
255
+ if normalized_type.startswith('conduit'):
256
+ normalized_type = normalized_type
257
+ else:
258
+ normalized_type = 'conduit' + normalized_type
259
+
260
+ # Check if a name is provided and if it already exists in the collection
261
+ if 'name' in link_information:
262
+ requested_name = link_information['name']
263
+ if any(link.name == requested_name for link in self.data):
264
+ raise ValueError(f"Link with name '{requested_name}' already exists")
265
+
266
+ # Define attribute hierarchy based on class inheritance
267
+ # Level 1: Common attributes for all Link types with defaults
268
+ link_base_attrs = {
269
+ 'name': lambda link_type, info: info.get('name', self._generate_default_name(link_type))
270
+ }
271
+
272
+ # Level 2: Attributes for all Conduit types with defaults
273
+ conduit_base_attrs = {
274
+ 'upstream_node': lambda _, info: info.get('upstream_node', ''),
275
+ 'downstream_node': lambda _, info: info.get('downstream_node', ''),
276
+ 'length': lambda _, info: info.get('length', 4.29),
277
+ 'roughness': lambda _, info: info.get('roughness', 0.013), # Default Manning's n
278
+ 'upstream_offset': lambda _, info: info.get('upstream_offset', 0.0),
279
+ 'downstream_offset': lambda _, info: info.get('downstream_offset', 0.0),
280
+ 'initial_flow': lambda _, info: info.get('initial_flow', 0.0),
281
+ 'maximum_flow': lambda _, info: info.get('maximum_flow', 0.0)
282
+ }
283
+
284
+ # Level 3: Specific attributes for conduit subtypes with defaults
285
+ conduit_specific_attrs = {
286
+ 'conduitcircle': {
287
+ 'barrels_number': lambda _, info: info.get('barrels_number', 1),
288
+ 'height': lambda _, info: info.get('height', 0.3)
289
+ },
290
+ 'conduitfilledcircle': {
291
+ 'barrels_number': lambda _, info: info.get('barrels_number', 1),
292
+ 'height': lambda _, info: info.get('height', 0.3),
293
+ 'filled': lambda _, info: info.get('filled', 0.0)
294
+ },
295
+ 'conduitrectangleopen': {
296
+ 'barrels_number': lambda _, info: info.get('barrels_number', 1),
297
+ 'height': lambda _, info: info.get('height', 2.0),
298
+ 'width': lambda _, info: info.get('width', 1.0)
299
+ },
300
+ 'conduitcustom': {
301
+ 'barrels_number': lambda _, info: info.get('barrels_number', 1),
302
+ 'height': lambda _, info: info.get('height', 1.0),
303
+ 'curve': lambda _, info: info.get('curve', '')
304
+ }
305
+ }
306
+
307
+ # Define link type configurations
308
+ link_types = {
309
+ 'conduitcircle': {
310
+ 'class': ConduitCircle,
311
+ 'attrs': {**link_base_attrs, **conduit_base_attrs, **conduit_specific_attrs['conduitcircle']}
312
+ },
313
+ 'conduitfilledcircle': {
314
+ 'class': ConduitFilledCircle,
315
+ 'attrs': {**link_base_attrs, **conduit_base_attrs, **conduit_specific_attrs['conduitfilledcircle']}
316
+ },
317
+ 'conduitrectangleopen': {
318
+ 'class': ConduitRectangleOpen,
319
+ 'attrs': {**link_base_attrs, **conduit_base_attrs, **conduit_specific_attrs['conduitrectangleopen']}
320
+ },
321
+ 'conduitcustom': {
322
+ 'class': ConduitCustom,
323
+ 'attrs': {**link_base_attrs, **conduit_base_attrs, **conduit_specific_attrs['conduitcustom']}
324
+ }
325
+ }
326
+
327
+ # Check if normalized link type exists
328
+ if normalized_type not in link_types:
329
+ raise TypeError(
330
+ f"Unknown link type '{link_type}', failed to add {link_information.get('name', 'unnamed link')}"
331
+ )
332
+
333
+ # Get link configuration
334
+ link_config = link_types[normalized_type]
335
+ link_class = link_config['class']
336
+ attrs = link_config['attrs']
337
+
338
+ # Create new link
339
+ new_link = link_class()
340
+
341
+ # Set all applicable attributes according to hierarchy, using default generators
342
+ for attr, default_generator in attrs.items():
343
+ value = default_generator(normalized_type, link_information)
344
+ setattr(new_link, attr, value)
345
+
346
+ # Check if the generated default name is unique (when name wasn't explicitly provided)
347
+ if 'name' not in link_information and any(link.name == new_link.name for link in self.data):
348
+ raise ValueError(f"Generated default name '{new_link.name}' already exists")
349
+
350
+ # Add link to data structure
351
+ self.data.append(new_link)
352
+
353
+ return new_link # Return the created link for immediate use if needed
354
+
355
+ def _generate_default_name(self, link_type):
356
+ """
357
+ Generate a default name for a link based on its type and existing links count.
358
+
359
+ Args:
360
+ link_type (str): Type of link (e.g. 'conduit_circle', 'conduit_custom')
361
+
362
+ Returns:
363
+ str: Generated name in format 'TYPE##' where TYPE is first 3 letters of link type
364
+ and ## is sequential number
365
+ """
366
+ # Extract the specific type after 'conduit_'
367
+ if '_' in link_type:
368
+ specific_type = link_type.split('_')[1]
369
+ else:
370
+ specific_type = link_type.replace('conduit', '')
371
+
372
+ # Get first 3 letters of the specific type
373
+ prefix = specific_type[:3].upper() # First 3 letters of link type
374
+
375
+ # Get count of links with the same type prefix
376
+ existing_count = sum(1 for link in self.data if hasattr(link, 'name') and
377
+ link.name and link.name.startswith(prefix))
378
+
379
+ # Generate name with next number
380
+ return f"{prefix}{existing_count + 1}"
196
381
 
197
382
  def read_from_swmm_inp(self, filename):
198
- conduit_contents = get_swmm_inp_content(filename, '[CONDUITS]')
199
- # fill in default values
383
+ """
384
+ Read link data from a SWMM input file.
385
+
386
+ Processes the following sections from SWMM input file:
387
+ - [CONDUITS]
388
+ - [XSECTIONS]
389
+ - [VERTICES]
390
+
391
+ Args:
392
+ filename (str): Path to the SWMM input file
393
+
394
+ Returns:
395
+ int: 0 if successful
396
+
397
+ Raises:
398
+ FileNotFoundError: If the input file doesn't exist
399
+ ValueError: If required sections are missing or data is malformed
400
+ Exception: For other processing errors
401
+ """
402
+ try:
403
+ # Read all required sections
404
+ conduit_contents = get_swmm_inp_content(filename, '[CONDUITS]')
405
+ x_section_contents = get_swmm_inp_content(filename, '[XSECTIONS]')
406
+ vertices_contents = get_swmm_inp_content(filename, '[VERTICES]')
407
+
408
+ # Process conduits and cross-sections
409
+ self._process_conduits_and_xsections(conduit_contents, x_section_contents)
410
+
411
+ # Process vertices
412
+ self._process_vertices(vertices_contents)
413
+
414
+ return 0
415
+ except Exception as e:
416
+ # Re-raise with more context
417
+ raise type(e)(f"Error reading SWMM input file: {str(e)}")
418
+
419
+ def _process_conduits_and_xsections(self, conduit_contents, x_section_contents):
420
+ """
421
+ Process conduit and cross-section data from SWMM input file.
422
+
423
+ Args:
424
+ conduit_contents (list): Lines from the [CONDUITS] section
425
+ x_section_contents (list): Lines from the [XSECTIONS] section
426
+ """
427
+ # Fill in default values for conduit contents
200
428
  for index, line in enumerate(conduit_contents):
201
- if len(line.split()) == 7:
202
- conduit_contents[index] += ' 0 0'
203
- elif len(line.split()) == 8:
204
- conduit_contents[index] += ' 0'
205
- x_section = get_swmm_inp_content(filename, '[XSECTIONS]')
206
- content = combine_swmm_inp_contents(conduit_contents, x_section)
207
- for line in content:
208
- pair = line.split()
209
- dic = {'name': pair[0], 'upstream_node': pair[1], 'downstream_node': pair[2], 'length': float(pair[3]),
210
- 'roughness': float(pair[4]), 'upstream_offset': float(pair[5]), 'downstream_offset': float(pair[6]),
211
- 'initial_flow': float(pair[7]), 'maximum_flow': float(pair[8])}
212
-
213
- match pair[9]:
214
- case 'CIRCULAR':
215
- dic['height'] = float(pair[10])
216
- # optional variable: Barrels
217
- if len(pair) >= 15:
218
- dic['barrels_number'] = int(pair[14])
219
- self.add_link('conduit_circle', dic)
220
-
221
- case 'FILLED_CIRCULAR':
222
- dic['height'] = float(pair[10])
223
- dic['filled'] = float(pair[11])
224
- # optional variable: Barrels
225
- if len(pair) >= 15:
226
- dic['barrels_number'] = int(pair[14])
227
- self.add_link('conduit_filled_circle', dic)
228
-
229
- case 'RECT_OPEN':
230
- dic['height'] = float(pair[10])
231
- dic['width'] = float(pair[11])
232
- # optional variable: Barrels
233
- if len(pair) >= 15:
234
- dic['barrels_number'] = int(pair[14])
235
- self.add_link('conduit_rectangle_open', dic)
236
-
237
- case 'CUSTOM':
238
- dic['height'] = float(pair[10])
239
- dic['curve'] = pair[11]
240
- # optional variable: Barrels
241
- if len(pair) >= 13:
242
- dic['barrels_number'] = int(pair[-1])
243
- self.add_link('conduit_custom', dic)
244
- #
245
- vertices_contents = get_swmm_inp_content(filename, '[VERTICES]')
429
+ parts = line.split()
430
+ if len(parts) == 7:
431
+ conduit_contents[index] += ' 0 0' # Add default initial_flow and maximum_flow
432
+ elif len(parts) == 8:
433
+ conduit_contents[index] += ' 0' # Add default maximum_flow
434
+
435
+ # Combine conduit and cross-section data
436
+ combined_content = combine_swmm_inp_contents(conduit_contents, x_section_contents)
437
+
438
+ # Process each combined line
439
+ for line in combined_content:
440
+ try:
441
+ parts = line.split()
442
+ if len(parts) < 10: # Need at least basic conduit info + shape
443
+ continue
444
+
445
+ # Create basic conduit dictionary
446
+ conduit_data = {
447
+ 'name': parts[0],
448
+ 'upstream_node': parts[1],
449
+ 'downstream_node': parts[2],
450
+ 'length': float(parts[3]),
451
+ 'roughness': float(parts[4]),
452
+ 'upstream_offset': float(parts[5]),
453
+ 'downstream_offset': float(parts[6]),
454
+ 'initial_flow': float(parts[7]),
455
+ 'maximum_flow': float(parts[8])
456
+ }
457
+
458
+ # Process based on cross-section shape
459
+ shape = parts[9]
460
+ if shape == 'CIRCULAR':
461
+ if len(parts) > 10:
462
+ conduit_data['height'] = float(parts[10])
463
+ if len(parts) >= 15:
464
+ conduit_data['barrels_number'] = int(parts[14])
465
+ self.add_link('conduit_circle', conduit_data)
466
+
467
+ elif shape == 'FILLED_CIRCULAR':
468
+ if len(parts) > 11:
469
+ conduit_data['height'] = float(parts[10])
470
+ conduit_data['filled'] = float(parts[11])
471
+ if len(parts) >= 15:
472
+ conduit_data['barrels_number'] = int(parts[14])
473
+ self.add_link('conduit_filled_circle', conduit_data)
474
+
475
+ elif shape == 'RECT_OPEN':
476
+ if len(parts) > 11:
477
+ conduit_data['height'] = float(parts[10])
478
+ conduit_data['width'] = float(parts[11])
479
+ if len(parts) >= 15:
480
+ conduit_data['barrels_number'] = int(parts[14])
481
+ self.add_link('conduit_rectangle_open', conduit_data)
482
+
483
+ elif shape == 'CUSTOM':
484
+ if len(parts) > 11:
485
+ conduit_data['height'] = float(parts[10])
486
+ conduit_data['curve'] = parts[11]
487
+ if len(parts) >= 13:
488
+ conduit_data['barrels_number'] = int(parts[-1])
489
+ self.add_link('conduit_custom', conduit_data)
490
+ except (ValueError, IndexError) as e:
491
+ # Log error but continue processing other conduits
492
+ print(f"Warning: Error processing conduit in line '{line}': {str(e)}")
493
+
494
+ def _process_vertices(self, vertices_contents):
495
+ """
496
+ Process vertex data from SWMM input file.
497
+
498
+ Args:
499
+ vertices_contents (list): Lines from the [VERTICES] section
500
+ """
246
501
  for line in vertices_contents:
247
- pair = line.split()
248
- for link in self.data:
249
- if link.name == pair[0]:
250
- link.vertices.x.append(float(pair[1]))
251
- link.vertices.y.append(float(pair[2]))
252
- link.vertices.link_name = pair[0]
253
- return 0
502
+ try:
503
+ parts = line.split()
504
+ if len(parts) < 3: # Need at least link name, x, y
505
+ continue
506
+
507
+ link_name = parts[0]
508
+ x_coord = float(parts[1])
509
+ y_coord = float(parts[2])
510
+
511
+ # Find the link and add vertex
512
+ for link in self.data:
513
+ if link.name == link_name:
514
+ link.vertices.x.append(x_coord)
515
+ link.vertices.y.append(y_coord)
516
+ link.vertices.link_name = link_name
517
+ break
518
+ except (ValueError, IndexError) as e:
519
+ # Log error but continue processing other vertices
520
+ print(f"Warning: Error processing vertex in line '{line}': {str(e)}")
254
521
 
255
522
  def write_to_swmm_inp(self, filename):
256
- with open(filename, 'a', encoding='utf-8') as f:
257
- f.write('\n\n[CONDUITS]\n')
258
- f.write(
259
- ';;Name Upstream Downstream Length Roughness Up-offset Down-offset Init_flow Max_flow\n')
260
- for link in self.data:
523
+ """
524
+ Write link data to a SWMM input file.
525
+
526
+ Writes the following sections to the SWMM input file:
527
+ - [CONDUITS]
528
+ - [XSECTIONS]
529
+ - [VERTICES]
530
+
531
+ Args:
532
+ filename (str): Path to the SWMM input file to write to
533
+
534
+ Returns:
535
+ int: 0 if successful
536
+
537
+ Raises:
538
+ IOError: If the file cannot be opened or written to
539
+ """
540
+ try:
541
+ with open(filename, 'a', encoding='utf-8') as f:
542
+ # Write CONDUITS section
543
+ f.write('\n\n[CONDUITS]\n')
261
544
  f.write(
262
- f'{link.name:30} {link.upstream_node:8} {link.downstream_node:8} {link.length:8.2f} {link.roughness:8.3f} {link.upstream_offset:8.3f} {link.downstream_offset:8.3f} {link.initial_flow:8.2f} {link.maximum_flow:8.2f}\n')
263
- #
264
- f.write('\n\n[XSECTIONS]\n')
265
- f.write(
266
- ';;Name Shape Geom1 Geom2 Geom3 Geom4 Barrels (Culvert)\n')
267
- for link in self.data:
268
- zero = 0
269
- if isinstance(link, ConduitCircle):
270
- f.write(
271
- f'{link.name:30} CIRCULAR {link.height:8.2f} {zero:8} {zero:8} {zero:8} {link.barrels_number:8}\n')
272
- if isinstance(link, ConduitFilledCircle):
545
+ ';;Name Upstream Downstream Length Roughness Up-offset Down-offset Init_flow Max_flow\n')
546
+ for link in self.data:
273
547
  f.write(
274
- f'{link.name:30} FILLED_CIRCULAR {link.height:8.2f} {link.filled:8.2f} {zero:8} {zero:8} {link.barrels_number:8}\n')
275
- if isinstance(link, ConduitRectangleOpen):
276
- f.write(
277
- f'{link.name:30} RECT_OPEN {link.height:8.2f} {link.width:8.2f} {zero:8} {zero:8} {link.barrels_number:8}\n')
278
- if isinstance(link, ConduitCustom):
279
- f.write(
280
- f'{link.name:30} CUSTOM {link.height:8.2f} {link.curve:8} 0 0 {link.barrels_number:8}\n')
281
- #
282
- f.write('\n\n[VERTICES]\n')
283
- f.write(';;Link X-Coord Y-Coord\n')
284
- for link in self.data:
285
- if link.vertices.link_name is not None:
286
- for xi, yi in zip(link.vertices.x, link.vertices.y):
287
- f.write(f'{link.vertices.link_name} {xi} {yi}\n')
288
- return 0
289
-
290
- def index_of(self, link_name):
548
+ f'{link.name:30} {link.upstream_node:8} {link.downstream_node:8} {link.length:8.2f} {link.roughness:8.3f} {link.upstream_offset:8.3f} {link.downstream_offset:8.3f} {link.initial_flow:8.2f} {link.maximum_flow:8.2f}\n')
549
+
550
+ # Write XSECTIONS section
551
+ f.write('\n\n[XSECTIONS]\n')
552
+ f.write(
553
+ ';;Name Shape Geom1 Geom2 Geom3 Geom4 Barrels (Culvert)\n')
554
+ for link in self.data:
555
+ zero = 0
556
+ if isinstance(link, ConduitCircle):
557
+ f.write(
558
+ f'{link.name:30} CIRCULAR {link.height:8.2f} {zero:8} {zero:8} {zero:8} {link.barrels_number:8}\n')
559
+ elif isinstance(link, ConduitFilledCircle):
560
+ f.write(
561
+ f'{link.name:30} FILLED_CIRCULAR {link.height:8.2f} {link.filled:8.2f} {zero:8} {zero:8} {link.barrels_number:8}\n')
562
+ elif isinstance(link, ConduitRectangleOpen):
563
+ f.write(
564
+ f'{link.name:30} RECT_OPEN {link.height:8.2f} {link.width:8.2f} {zero:8} {zero:8} {link.barrels_number:8}\n')
565
+ elif isinstance(link, ConduitCustom):
566
+ f.write(
567
+ f'{link.name:30} CUSTOM {link.height:8.2f} {link.curve:8} 0 0 {link.barrels_number:8}\n')
568
+
569
+ # Write VERTICES section
570
+ f.write('\n\n[VERTICES]\n')
571
+ f.write(';;Link X-Coord Y-Coord\n')
572
+ for link in self.data:
573
+ if link.vertices.link_name is not None:
574
+ for xi, yi in zip(link.vertices.x, link.vertices.y):
575
+ f.write(f'{link.vertices.link_name} {xi} {yi}\n')
576
+ return 0
577
+ except IOError as e:
578
+ raise IOError(f"Error writing to SWMM input file: {str(e)}")
579
+
580
+ def index_of(self, link_name, return_link=False):
581
+ """
582
+ Find the index of a link by name.
583
+
584
+ Args:
585
+ link_name (str): Name of the link to find
586
+ return_link (bool, optional): If True, returns the link object instead of index
587
+ Defaults to False
588
+
589
+ Returns:
590
+ int or Link: Index of the link in the data list, or the link object if return_link is True
591
+
592
+ Raises:
593
+ ValueError: If no link with the given name is found
594
+
595
+ Example:
596
+ >>> links = LinkList()
597
+ >>> links.add_link('conduit_circle', {'name': 'C1'})
598
+ >>> links.index_of('C1')
599
+ 0
600
+ """
291
601
  for index, item in enumerate(self.data):
292
602
  if item.name == link_name:
293
- return index
603
+ return item if return_link else index
294
604
  raise ValueError(f"No item found with name '{link_name}'")