easysewer 0.0.1__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 ADDED
@@ -0,0 +1,294 @@
1
+ """
2
+ Link Management Module
3
+
4
+ This module implements various types of conduits and channels that connect nodes
5
+ in the drainage network. Supports different cross-section types and hydraulic
6
+ characteristics.
7
+ """
8
+ from .utils import *
9
+
10
+
11
+ class Vertices:
12
+ """
13
+ Container for link vertex coordinates.
14
+
15
+ Stores the geometry of a link's path between its endpoints.
16
+
17
+ Attributes:
18
+ link_name (str): Name of the associated link
19
+ x (list): X-coordinates of vertices
20
+ y (list): Y-coordinates of vertices
21
+ """
22
+ def __init__(self):
23
+ self.link_name = None
24
+ self.x = []
25
+ self.y = []
26
+
27
+
28
+ class Link:
29
+ """
30
+ Base class for all hydraulic links.
31
+
32
+ Represents a connection between two nodes in the drainage network.
33
+
34
+ Attributes:
35
+ name (str): Unique identifier for the link
36
+ vertices (Vertices): Geometric path of the link
37
+ """
38
+ def __init__(self):
39
+ self.name = ''
40
+ self.vertices = Vertices()
41
+
42
+ def __repr__(self):
43
+ return f'Link<{self.name}>'
44
+
45
+
46
+ class Conduit(Link):
47
+ """
48
+ Base class for conduit-type links.
49
+
50
+ Represents closed conduits or open channels with specific hydraulic properties.
51
+
52
+ Attributes:
53
+ upstream_node (str): Name of upstream node
54
+ downstream_node (str): Name of downstream node
55
+ length (float): Conduit length
56
+ roughness (float): Manning's roughness coefficient
57
+ upstream_offset (float): Offset at upstream end
58
+ downstream_offset (float): Offset at downstream end
59
+ initial_flow (float): Initial flow rate
60
+ maximum_flow (float): Maximum allowed flow rate
61
+ """
62
+ def __init__(self):
63
+ Link.__init__(self)
64
+ self.upstream_node = ''
65
+ self.downstream_node = ''
66
+ self.length = 0.0
67
+ self.roughness = 0.0
68
+ self.upstream_offset = 0.0
69
+ self.downstream_offset = 0.0
70
+ # optional variable
71
+ self.initial_flow = 0
72
+ self.maximum_flow = 0 # means no limit
73
+
74
+
75
+ class ConduitCircle(Conduit):
76
+ def __init__(self):
77
+ Conduit.__init__(self)
78
+ self.barrels_number = 1
79
+ self.height = 0.0
80
+
81
+
82
+ class ConduitFilledCircle(Conduit):
83
+ def __init__(self):
84
+ Conduit.__init__(self)
85
+ self.barrels_number = 1
86
+ self.height = 0.0
87
+ self.filled = 0.0
88
+
89
+
90
+ class ConduitRectangleOpen(Conduit):
91
+ def __init__(self):
92
+ Conduit.__init__(self)
93
+ self.barrels_number = 1
94
+ self.height = 0.0
95
+ self.width = 0.0
96
+
97
+
98
+ class ConduitCustom(Conduit):
99
+ def __init__(self):
100
+ Conduit.__init__(self)
101
+ self.barrels_number = 1
102
+ self.height = 0.0
103
+ self.curve = ''
104
+
105
+
106
+ class LinkList:
107
+ def __init__(self):
108
+ self.data = []
109
+
110
+ def __repr__(self):
111
+ return f'{len(self.data)} Links'
112
+
113
+ def __len__(self):
114
+ return len(self.data)
115
+
116
+ def __getitem__(self, key):
117
+ if isinstance(key, int):
118
+ return self.data[key]
119
+ elif isinstance(key, str):
120
+ for item in self.data:
121
+ if item.name == key:
122
+ return item
123
+ raise KeyError(f"No item found with name '{key}'")
124
+ else:
125
+ raise TypeError("Key must be an integer or a string")
126
+
127
+ def __iter__(self):
128
+ return iter(self.data)
129
+
130
+ def __contains__(self, item):
131
+ return item in self.data
132
+
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']}")
196
+
197
+ def read_from_swmm_inp(self, filename):
198
+ conduit_contents = get_swmm_inp_content(filename, '[CONDUITS]')
199
+ # fill in default values
200
+ 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]')
246
+ 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
254
+
255
+ 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:
261
+ 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):
273
+ 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):
291
+ for index, item in enumerate(self.data):
292
+ if item.name == link_name:
293
+ return index
294
+ raise ValueError(f"No item found with name '{link_name}'")
easysewer/Node.py ADDED
@@ -0,0 +1,346 @@
1
+ """
2
+ Node Management Module
3
+
4
+ This module implements various types of nodes used in urban drainage networks including:
5
+ - Basic nodes (junctions)
6
+ - Outfall nodes with different boundary conditions
7
+ - Support for node properties like elevation, coordinates, and flow characteristics
8
+ """
9
+ from .utils import *
10
+
11
+
12
+ class Node:
13
+ """
14
+ Base class for all node types in the drainage network.
15
+
16
+ Represents a point element in the drainage network with basic properties
17
+ like location and elevation.
18
+
19
+ Attributes:
20
+ name (str): Unique identifier for the node
21
+ coordinate (list): [x, y] coordinates of the node
22
+ elevation (float): Node invert elevation
23
+ """
24
+ def __init__(self):
25
+ self.name = ''
26
+ self.coordinate = [0.0, 0.0]
27
+ self.elevation = 0
28
+
29
+ def __repr__(self):
30
+ return f'Node<{self.name}>'
31
+
32
+
33
+ class Junction(Node):
34
+ """
35
+ Junction node type for connecting conduits.
36
+
37
+ Represents intersection points in the drainage network where flows combine
38
+ or split. Includes properties for depth and ponding characteristics.
39
+
40
+ Attributes:
41
+ maximum_depth (float): Maximum water depth at junction
42
+ initial_depth (float): Initial water depth at start of simulation
43
+ overload_depth (float): Depth above which overflows occur
44
+ surface_ponding_area (float): Area available for surface ponding
45
+ dwf_base_value (float): Base dry weather flow value
46
+ dwf_patterns (list): Time patterns for dry weather flow
47
+ inflow (dict): Inflow characteristics and time series
48
+ """
49
+ def __init__(self):
50
+ Node.__init__(self)
51
+ self.maximum_depth = 0
52
+ self.initial_depth = 0
53
+ self.overload_depth = 0
54
+ self.surface_ponding_area = 0
55
+ #
56
+ # dry weather flow
57
+ self.dwf_base_value = 0
58
+ self.dwf_patterns = []
59
+ #
60
+ # inflow
61
+ self.inflow = None
62
+
63
+
64
+ class Outfall(Node):
65
+ """
66
+ Base class for outfall nodes.
67
+
68
+ Represents points where water leaves the drainage system. Supports various
69
+ boundary condition types through derived classes.
70
+
71
+ Attributes:
72
+ flap_gate (bool): Whether backflow prevention is present
73
+ route_to (str): Routing destination for diverted flow
74
+ """
75
+ def __init__(self):
76
+ Node.__init__(self)
77
+ self.flap_gate = False
78
+ self.route_to = ''
79
+
80
+
81
+ class OutfallFree(Outfall):
82
+ def __init__(self):
83
+ Outfall.__init__(self)
84
+
85
+
86
+ class OutfallNormal(Outfall):
87
+ def __init__(self):
88
+ Outfall.__init__(self)
89
+
90
+
91
+ class OutfallFixed(Outfall):
92
+ def __init__(self):
93
+ Outfall.__init__(self)
94
+ self.stage = 0.0
95
+
96
+
97
+ class OutfallTidal(Outfall):
98
+ def __init__(self):
99
+ Outfall.__init__(self)
100
+ self.tidal = ''
101
+
102
+
103
+ class OutfallTimeseries(Outfall):
104
+ def __init__(self):
105
+ Outfall.__init__(self)
106
+ self.time_series = ''
107
+
108
+
109
+ class NodeList:
110
+ def __init__(self):
111
+ self.data = []
112
+
113
+ def __repr__(self):
114
+ return f'{len(self.data)} Nodes'
115
+
116
+ def __len__(self):
117
+ return len(self.data)
118
+
119
+ def __getitem__(self, key):
120
+ if isinstance(key, int):
121
+ return self.data[key]
122
+ elif isinstance(key, str):
123
+ for item in self.data:
124
+ if item.name == key:
125
+ return item
126
+ raise KeyError(f"No item found with name '{key}'")
127
+ else:
128
+ raise TypeError("Key must be an integer or a string")
129
+
130
+ def __iter__(self):
131
+ return iter(self.data)
132
+
133
+ def __contains__(self, item):
134
+ return item in self.data
135
+
136
+ def add_node(self, node_type, node_information):
137
+ def execute(func1, func2):
138
+ def inner():
139
+ # new an object according to node_type
140
+ new_node = func1()
141
+ # add essential information
142
+ if 'name' in node_information:
143
+ new_node.name = node_information['name']
144
+ else: # if it can not find name, raise error
145
+ # print('Unknown Node: Can not recognize node name')
146
+ return -1
147
+ if 'coordinate' in node_information:
148
+ new_node.coordinate = node_information['coordinate']
149
+ if 'elevation' in node_information:
150
+ new_node.elevation = node_information['elevation']
151
+ # for Outfalls
152
+ if 'flap_gate' in node_information:
153
+ new_node.flap_gate = True if node_information['flap_gate'] == 'YES' else False
154
+ if 'route_to' in node_information:
155
+ new_node.route_to = node_information['route_to']
156
+ # add node_type related information
157
+ func2(new_node)
158
+ # update node_list
159
+ self.data.append(new_node)
160
+ return 0
161
+
162
+ return inner
163
+
164
+ match node_type:
165
+ case 'junction' | 'Junction':
166
+ def junction_type(new_node):
167
+ if 'maximum_depth' in node_information:
168
+ new_node.maximum_depth = node_information['maximum_depth']
169
+ if 'initial_depth' in node_information:
170
+ new_node.initial_depth = node_information['initial_depth']
171
+ if 'overload_depth' in node_information:
172
+ new_node.overload_depth = node_information['overload_depth']
173
+ if 'surface_ponding_area' in node_information:
174
+ new_node.surface_ponding_area = node_information['surface_ponding_area']
175
+ if 'dwf_base_value' in node_information:
176
+ new_node.dwf_base_value = node_information['dwf_base_value']
177
+ if 'dwf_patterns' in node_information:
178
+ new_node.dwf_patterns = node_information['dwf_patterns']
179
+
180
+ return execute(Junction, junction_type)()
181
+
182
+ case 'outfall_free' | 'OutfallFree':
183
+ def outfall_free_type(_):
184
+ pass
185
+
186
+ return execute(OutfallFree, outfall_free_type)()
187
+
188
+ case 'outfall_normal' | 'OutfallNormal':
189
+ def outfall_normal_type(_):
190
+ pass
191
+
192
+ return execute(OutfallNormal, outfall_normal_type)()
193
+
194
+ case 'outfall_fixed' | 'OutfallFixed':
195
+ def outfall_fixed_type(new_node):
196
+ if 'stage' in node_information:
197
+ new_node.stage = node_information['stage']
198
+
199
+ return execute(OutfallFixed, outfall_fixed_type)()
200
+
201
+ case 'outfall_tidal' | 'OutfallTidal':
202
+ def outfall_tidal_type(new_node):
203
+ if 'tidal' in node_information:
204
+ new_node.tidal = node_information['tidal']
205
+
206
+ return execute(OutfallTidal, outfall_tidal_type)()
207
+
208
+ case 'outfall_time_series' | 'OutfallTimeseries':
209
+ def outfall_time_series_type(new_node):
210
+ if 'time_series' in node_information:
211
+ new_node.time_series = node_information['time_series']
212
+
213
+ return execute(OutfallTimeseries, outfall_time_series_type)()
214
+
215
+ case _:
216
+ raise TypeError(f"Unknown node type, failed to add {node_information['name']}")
217
+
218
+ def read_from_swmm_inp(self, filename):
219
+ junction_contents = get_swmm_inp_content(filename, '[JUNCTIONS]')
220
+ coordinates = get_swmm_inp_content(filename, '[COORDINATES]')
221
+ outfall_contents = get_swmm_inp_content(filename, '[OUTFALLS]')
222
+ dwf_contents = get_swmm_inp_content(filename, '[DWF]')
223
+ inflow_contents = get_swmm_inp_content(filename, '[INFLOWS]')
224
+
225
+ # coordinate list
226
+ coordinates_dic = {}
227
+ for line in coordinates:
228
+ keys = line.split()
229
+ coordinates_dic[keys[0]] = [float(keys[1]), float(keys[2])]
230
+ # process junctions
231
+ for line in junction_contents:
232
+ pair = line.split()
233
+ dic = {'name': pair[0], 'coordinate': [0.0, 0.0], 'elevation': float(pair[1]),
234
+ 'maximum_depth': float(pair[2]), 'initial_depth': float(pair[3]),
235
+ 'overload_depth': float(pair[4]), 'surface_ponding_area': float(pair[5])}
236
+ dic['coordinate'] = coordinates_dic[dic['name']]
237
+ self.add_node('junction', dic)
238
+ # process outfalls
239
+ for line in outfall_contents:
240
+ pair = line.split()
241
+ dic = {'name': pair[0], 'coordinate': [0.0, 0.0], 'elevation': float(pair[1])}
242
+ dic['coordinate'] = coordinates_dic[dic['name']]
243
+ #
244
+ if pair[-1] == 'YES':
245
+ dic['flap_gate'] = 'YES'
246
+ elif pair[-1] == 'NO':
247
+ dic['flap_gate'] = 'NO'
248
+ else:
249
+ dic['flap_gate'] = pair[-2]
250
+ dic['route_to'] = pair[-1]
251
+ #
252
+ match pair[2]:
253
+ case 'FREE':
254
+ self.add_node('outfall_free', dic)
255
+ case 'NORMAL':
256
+ self.add_node('outfall_normal', dic)
257
+ case 'FIXED':
258
+ dic['stage'] = float(pair[2])
259
+ self.add_node('outfall_fixed', dic)
260
+ case 'TIDAL':
261
+ dic['tidal'] = float(pair[2])
262
+ self.add_node('outfall_tidal', dic)
263
+ case 'TIMESERIES':
264
+ dic['time_series'] = float(pair[2])
265
+ self.add_node('outfall_time_series', dic)
266
+ case _:
267
+ pass
268
+ # process DWF
269
+ for line in dwf_contents:
270
+ pair = line.split()
271
+ for node in self.data:
272
+ if node.name == pair[0]:
273
+ node.dwf_base_value = pair[2]
274
+ for pattern in pair[3::]:
275
+ node.dwf_patterns.append(pattern)
276
+ # process inflow
277
+ for line in inflow_contents:
278
+ pair = line.split()
279
+ if pair[1] != 'FLOW':
280
+ raise Exception('Unsupported inflow type, only FLOW is accepted.')
281
+ for node in self.data:
282
+ if node.name == pair[0]:
283
+ result = {'time_series': pair[2], 'type': pair[3], 'm_factor': float(pair[4]),
284
+ 's_factor': float(pair[5]), 'baseline': float(pair[6]), 'pattern': pair[7]}
285
+ node.inflow = result
286
+ return 0
287
+
288
+ def write_to_swmm_inp(self, filename):
289
+ with open(filename, 'a', encoding='utf-8') as f:
290
+ f.write('\n\n[JUNCTIONS]\n')
291
+ f.write(';;Name Elevation MaxDepth InitDepth SurDepth Ponding\n')
292
+ for node in self.data:
293
+ if isinstance(node, Junction):
294
+ f.write(
295
+ f'{node.name:8} {node.elevation:8.3f} {node.maximum_depth:8.3f} {node.initial_depth:8.3f} {node.overload_depth:8.3f} {node.surface_ponding_area:8.3f}\n')
296
+ #
297
+ f.write('\n\n[OUTFALLS]\n')
298
+ f.write(';;Name Elevation Type // Gated RouteTo\n')
299
+ for node in self.data:
300
+ if isinstance(node, OutfallFree):
301
+ msg = 'YES' if node.flap_gate else 'NO'
302
+ f.write(f'{node.name:8} {node.elevation:8.3f} FREE {msg:8} {node.route_to}\n')
303
+ if isinstance(node, OutfallNormal):
304
+ msg = 'YES' if node.flap_gate else 'NO'
305
+ f.write(f'{node.name:8} {node.elevation:8.3f} NORMAL {msg:8} {node.route_to}\n')
306
+ if isinstance(node, OutfallFixed):
307
+ msg = 'YES' if node.flap_gate else 'NO'
308
+ f.write(
309
+ f'{node.name:8} {node.elevation:8.3f} FIXED {node.stage:8} {msg} {node.route_to}\n')
310
+ if isinstance(node, OutfallTidal):
311
+ msg = 'YES' if node.flap_gate else 'NO'
312
+ f.write(
313
+ f'{node.name:8} {node.elevation:8.3f} TIDAL {node.tidal:8} {msg} {node.route_to}\n')
314
+ if isinstance(node, OutfallTimeseries):
315
+ msg = 'YES' if node.flap_gate else 'NO'
316
+ f.write(
317
+ f'{node.name:8} {node.elevation:8.3f} TIMESERIES {node.time_series:8} {msg} {node.route_to}\n')
318
+ #
319
+ f.write('\n\n[COORDINATES]\n')
320
+ f.write(';;Name X-Coord Y-Coord\n')
321
+ for node in self.data:
322
+ f.write(f'{node.name:8} {node.coordinate[0]:8.2f} {node.coordinate[1]:8.2f}\n')
323
+ #
324
+ f.write('\n\n[DWF]\n')
325
+ f.write(';;Node Constituent Baseline Patterns \n')
326
+ for node in self.data:
327
+ if isinstance(node, Junction):
328
+ if node.dwf_base_value != 0:
329
+ string = ' '.join(node.dwf_patterns)
330
+ f.write(f'{node.name} FLOW {node.dwf_base_value} {string}\n')
331
+ #
332
+ f.write('\n\n[INFLOWS]\n')
333
+ f.write(';;Node Constituent Time Series Type Mfactor Sfactor Baseline Pattern\n')
334
+ for node in self.data:
335
+ if isinstance(node, Junction):
336
+ if node.inflow is not None:
337
+ res = [str(i) for i in list(node.inflow.values())]
338
+ res = ' '.join(res)
339
+ f.write(f'{node.name} FLOW {res} \n')
340
+ return 0
341
+
342
+ def index_of(self, node_name):
343
+ for index, item in enumerate(self.data):
344
+ if item.name == node_name:
345
+ return index
346
+ raise ValueError(f"No item found with name '{node_name}'")