Skip to content

Commit 0a5a571

Browse files
committed
Changed variable names to use junction, updated test
2 parents 214f1f4 + f2b08fc commit 0a5a571

File tree

5 files changed

+116
-32
lines changed

5 files changed

+116
-32
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ build/
55
wntr.egg-info/
66
dist/
77
docker/
8+
/.vscode
89

910
*.pyd
1011
*.pyc

documentation/morph.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ This initial set of operations can generate new branch pipes, pipes in series, a
8282
This cycle repeats until the network can no longer be reduced.
8383
The user can specify if branch trimming, series pipe merge, and/or parallel pipe merge should be included in the skeletonization operations.
8484
The user can also specify a maximum number of cycles to include in the process.
85+
The user can also specify a list of junctions and pipes which should be excluded from skeletonization operations.
8586

8687
.. only:: latex
8788

wntr/epanet/io.py

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -236,39 +236,30 @@ def __init__(self):
236236
self.top_comments = []
237237
self.curves = OrderedDict()
238238

239-
def read(self, inp_files, wn=None):
239+
@staticmethod
240+
def read_sections(inp_files:list) -> tuple:
240241
"""
241-
Read an EPANET INP file and load data into a water network model object.
242-
Both EPANET 2.0 and EPANET 2.2 INP file options are recognized and handled.
242+
Read an EPANET INP file and return a dictionary of sections.
243243
244244
Parameters
245245
----------
246246
inp_files : str or list
247247
An EPANET INP input file or list of INP files to be combined
248-
wn : WaterNetworkModel, optional
249-
An optional network model to append onto; by default a new model is created.
250248
251249
Returns
252250
-------
253-
WaterNetworkModel
254-
A water network model object
251+
tuple
252+
A tuple of two elements: a dictionary (OrderedDict) of sections, and a list of top comments
255253
256254
"""
257-
if wn is None:
258-
wn = WaterNetworkModel()
259-
self.wn = wn
260255
if not isinstance(inp_files, list):
261256
inp_files = [inp_files]
262-
wn.name = inp_files[0]
263257

264-
self.curves = OrderedDict()
265-
self.top_comments = []
266-
self.sections = OrderedDict()
258+
sections = OrderedDict()
267259
for sec in _INP_SECTIONS:
268-
self.sections[sec] = []
269-
self.mass_units = None
270-
self.flow_units = None
260+
sections[sec] = []
271261

262+
top_comments = []
272263
for filename in inp_files:
273264
section = None
274265
lnum = 0
@@ -306,13 +297,55 @@ def read(self, inp_files, wn=None):
306297
else:
307298
raise ENSyntaxError(201, line_num=lnum, line = line)
308299
elif section is None and line.startswith(';'):
309-
self.top_comments.append(line[1:])
300+
top_comments.append(line[1:])
310301
continue
311302
elif section is None:
312303
logger.debug('Found confusing line: %s', repr(line))
313304
raise ENSyntaxError(201, line_num=lnum, line=line)
314305
# We have text, and we are in a section
315-
self.sections[section].append((lnum, line))
306+
sections[section].append((lnum, line))
307+
return sections, top_comments
308+
309+
def read(self, inp_files, wn=None, sections=None, top_comments=None):
310+
"""
311+
Read an EPANET INP file and load data into a water network model object.
312+
Both EPANET 2.0 and EPANET 2.2 INP file options are recognized and handled.
313+
314+
Parameters
315+
----------
316+
inp_files : str or list
317+
An EPANET INP input file or list of INP files to be combined
318+
wn : WaterNetworkModel, optional
319+
An optional network model to append onto; by default a new model is created.
320+
321+
Returns
322+
-------
323+
:class:`~wntr.network.model.WaterNetworkModel`
324+
A water network model object
325+
326+
"""
327+
if wn is None:
328+
wn = WaterNetworkModel()
329+
self.wn = wn
330+
if not isinstance(inp_files, list):
331+
inp_files = [inp_files]
332+
wn.name = inp_files[0]
333+
334+
self.curves = OrderedDict()
335+
336+
self.mass_units = None
337+
self.flow_units = None
338+
339+
if sections is None:
340+
self.sections, self.top_comments = self.read_sections(inp_files)
341+
else:
342+
self.sections = sections
343+
self.top_comments = top_comments
344+
if top_comments is None:
345+
self.top_comments = []
346+
347+
348+
assert len([key for key in self.sections.keys()]) == len(_INP_SECTIONS), 'Missing sections in INP file'
316349

317350
# Parse each of the sections
318351
# The order of operations is important as certain things require prior knowledge

wntr/morph/skel.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
logger = logging.getLogger(__name__)
1515

1616
def skeletonize(wn, pipe_diameter_threshold, branch_trim=True, series_pipe_merge=True,
17-
parallel_pipe_merge=True, max_cycles=None, use_epanet=True,
17+
parallel_pipe_merge=True, max_cycles=None, use_epanet=True,
18+
pipes_to_exclude:list=[], junctions_to_exclude:list=[],
1819
return_map=False, return_copy=True):
1920
"""
2021
Perform network skeletonization using branch trimming, series pipe merge,
@@ -43,6 +44,10 @@ def skeletonize(wn, pipe_diameter_threshold, branch_trim=True, series_pipe_merge
4344
use_epanet: bool, optional
4445
If True, use the EpanetSimulator to compute headloss in pipes.
4546
If False, use the WNTRSimulator to compute headloss in pipes.
47+
pipes_to_exclude: list, optional
48+
List of pipe names to exclude from skeletonization
49+
junctions_to_exclude: list, optional
50+
List of junction names to exclude from skeletonization
4651
return_map: bool, optional
4752
If True, return a skeletonization map. The map is a dictionary
4853
that includes original nodes as keys and a list of skeletonized nodes
@@ -60,7 +65,12 @@ def skeletonize(wn, pipe_diameter_threshold, branch_trim=True, series_pipe_merge
6065
nodes as keys and a list of skeletonized nodes that were merged into
6166
each original node as values.
6267
"""
63-
skel = _Skeletonize(wn, use_epanet, return_copy)
68+
if len(pipes_to_exclude) > 0:
69+
assert len(set(pipes_to_exclude) - set(wn.pipe_name_list)) == 0
70+
if len(junctions_to_exclude) > 0:
71+
assert len(set(junctions_to_exclude) - set(wn.junction_name_list)) == 0
72+
73+
skel = _Skeletonize(wn, use_epanet, return_copy, pipes_to_exclude, junctions_to_exclude)
6474

6575
skel.run(pipe_diameter_threshold, branch_trim, series_pipe_merge,
6676
parallel_pipe_merge, max_cycles)
@@ -73,7 +83,7 @@ def skeletonize(wn, pipe_diameter_threshold, branch_trim=True, series_pipe_merge
7383

7484
class _Skeletonize(object):
7585

76-
def __init__(self, wn, use_epanet, return_copy):
86+
def __init__(self, wn, use_epanet, return_copy, pipes_to_exclude, junctions_to_exclude):
7787

7888
if return_copy:
7989
# Get a copy of the WaterNetworkModel
@@ -93,6 +103,7 @@ def __init__(self, wn, use_epanet, return_copy):
93103
self.skeleton_map = skel_map
94104

95105
# Get a list of junction and pipe names that are associated with controls
106+
# Add them to junctions and pipes to exclude
96107
junc_with_controls = []
97108
pipe_with_controls = []
98109
for name, control in self.wn.controls():
@@ -101,8 +112,10 @@ def __init__(self, wn, use_epanet, return_copy):
101112
junc_with_controls.append(req.name)
102113
elif isinstance(req, Pipe):
103114
pipe_with_controls.append(req.name)
104-
self.junc_with_controls = list(set(junc_with_controls))
105-
self.pipe_with_controls = list(set(pipe_with_controls))
115+
self.junc_to_exclude = list(set(junc_with_controls))
116+
self.junc_to_exclude.extend(junctions_to_exclude)
117+
self.pipe_to_exclude = list(set(pipe_with_controls))
118+
self.pipe_to_exclude.extend(pipes_to_exclude)
106119

107120
# Calculate pipe headloss using a single period EPANET simulation
108121
duration = self.wn.options.time.duration
@@ -163,7 +176,7 @@ def branch_trim(self, pipe_threshold):
163176
patterns) to the neighboring junction.
164177
"""
165178
for junc_name in self.wn.junction_name_list:
166-
if junc_name in self.junc_with_controls:
179+
if junc_name in self.junc_to_exclude:
167180
continue
168181
neighbors = list(nx.neighbors(self.G,junc_name))
169182
if len(neighbors) > 1:
@@ -181,7 +194,7 @@ def branch_trim(self, pipe_threshold):
181194
pipe = self.wn.get_link(pipe_name)
182195
if not ((isinstance(pipe, Pipe)) and \
183196
(pipe.diameter <= pipe_threshold) and \
184-
pipe_name not in self.pipe_with_controls):
197+
pipe_name not in self.pipe_to_exclude):
185198
continue
186199

187200
logger.info('Branch trim: '+ str(junc_name) + str(neighbors))
@@ -215,7 +228,7 @@ def series_pipe_merge(self, pipe_threshold):
215228
to the nearest junction.
216229
"""
217230
for junc_name in self.wn.junction_name_list:
218-
if junc_name in self.junc_with_controls:
231+
if junc_name in self.junc_to_exclude:
219232
continue
220233
neighbors = list(nx.neighbors(self.G,junc_name))
221234
if not (len(neighbors) == 2):
@@ -239,8 +252,8 @@ def series_pipe_merge(self, pipe_threshold):
239252
(isinstance(pipe1, Pipe)) and \
240253
((pipe0.diameter <= pipe_threshold) and \
241254
(pipe1.diameter <= pipe_threshold)) and \
242-
pipe_name0 not in self.pipe_with_controls and \
243-
pipe_name1 not in self.pipe_with_controls):
255+
pipe_name0 not in self.pipe_to_exclude and \
256+
pipe_name1 not in self.pipe_to_exclude):
244257
continue
245258
# Find closest neighbor junction
246259
if (isinstance(neigh_junc0, Junction)) and \
@@ -305,7 +318,7 @@ def parallel_pipe_merge(self, pipe_threshold):
305318
"""
306319

307320
for junc_name in self.wn.junction_name_list:
308-
if junc_name in self.junc_with_controls:
321+
if junc_name in self.junc_to_exclude:
309322
continue
310323
neighbors = nx.neighbors(self.G,junc_name)
311324
for neighbor in [n for n in neighbors]:
@@ -322,8 +335,8 @@ def parallel_pipe_merge(self, pipe_threshold):
322335
(isinstance(pipe1, Pipe)) and \
323336
((pipe0.diameter <= pipe_threshold) and \
324337
(pipe1.diameter <= pipe_threshold)) and \
325-
pipe_name0 not in self.pipe_with_controls and \
326-
pipe_name1 not in self.pipe_with_controls):
338+
pipe_name0 not in self.pipe_to_exclude and \
339+
pipe_name1 not in self.pipe_to_exclude):
327340
continue
328341

329342
logger.info('Parallel pipe merge: '+ str(junc_name) + str((pipe_name0, pipe_name1)))

wntr/tests/test_morph.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,42 @@ def test_skeletonize_with_controls(self):
328328
self.assertEqual(skel_wn.num_nodes, wn.num_nodes - 17)
329329
self.assertEqual(skel_wn.num_links, wn.num_links - 22)
330330

331+
def test_skeletonize_with_excluding_nodes_and_pipes(self):
332+
333+
inp_file = join(datadir, "skeletonize.inp")
334+
wn = wntr.network.WaterNetworkModel(inp_file)
335+
336+
# Run skeletonization without excluding junctions or pipes
337+
skel_wn = wntr.morph.skeletonize(wn, 12.0 * 0.0254, use_epanet=False)
338+
# Junction 13 and Pipe 60 are not in the skeletonized model
339+
assert "13" not in skel_wn.junction_name_list
340+
assert "60" not in skel_wn.pipe_name_list
341+
342+
# Run skeletonization excluding Junction 13 and Pipe 60
343+
skel_wn = wntr.morph.skeletonize(wn, 12.0 * 0.0254, use_epanet=False,
344+
junctions_to_exclude=["13"],
345+
pipes_to_exclude=["60"])
346+
# Junction 13 and Pipe 60 are in the skeletonized model
347+
assert "13" in skel_wn.junction_name_list
348+
assert "60" in skel_wn.pipe_name_list
349+
self.assertEqual(skel_wn.num_nodes, wn.num_nodes - 17)
350+
self.assertEqual(skel_wn.num_links, wn.num_links - 22)
351+
352+
# Change diameter of link 60 one link connected to Junction 13 to be
353+
# greater than 12, should get some results as above
354+
# Note, link 11 is connected to Junction 13
355+
link = wn.get_link("60")
356+
link.diameter = 16 * 0.0254
357+
link_connected_to_13 = wn.get_links_for_node('13')[0]
358+
link = wn.get_link(link_connected_to_13)
359+
link.diameter = 16 * 0.0254
360+
361+
skel_wn = wntr.morph.skeletonize(wn, 12.0 * 0.0254, use_epanet=False)
362+
assert "13" in skel_wn.junction_name_list
363+
assert "60" in skel_wn.pipe_name_list
364+
self.assertEqual(skel_wn.num_nodes, wn.num_nodes - 17)
365+
self.assertEqual(skel_wn.num_links, wn.num_links - 22)
366+
331367
def test_series_merge_properties(self):
332368

333369
wn = wntr.network.WaterNetworkModel()

0 commit comments

Comments
 (0)