Skip to content

Commit d2abc85

Browse files
authored
Merge pull request #2669 from djarecka/fix/wf_graph
FIX: Node __repr__ and detailed graph expansion
2 parents 81fae84 + 670c39e commit d2abc85

File tree

4 files changed

+175
-14
lines changed

4 files changed

+175
-14
lines changed

nipype/pipeline/engine/base.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ def __init__(self, name=None, base_dir=None):
3636
3737
"""
3838
self._hierarchy = None
39-
self._name = None
39+
self.name = name
40+
self._id = self.name # for compatibility with node expansion using iterables
4041

4142
self.base_dir = base_dir
4243
self.config = deepcopy(config._sections)
43-
self.name = name
4444

4545
@property
4646
def name(self):
@@ -66,6 +66,14 @@ def inputs(self):
6666
def outputs(self):
6767
raise NotImplementedError
6868

69+
@property
70+
def itername(self):
71+
"""Name for expanded iterable"""
72+
itername = self._id
73+
if self._hierarchy:
74+
itername = '%s.%s' % (self._hierarchy, self._id)
75+
return itername
76+
6977
def clone(self, name):
7078
"""Clone an EngineBase object
7179
@@ -95,6 +103,9 @@ def _check_inputs(self, parameter):
95103
def __str__(self):
96104
return self.fullname
97105

106+
def __repr__(self):
107+
return self.itername
108+
98109
def save(self, filename=None):
99110
if filename is None:
100111
filename = 'temp.pklz'

nipype/pipeline/engine/nodes.py

-9
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ def __init__(self,
159159
self._got_inputs = False
160160
self._originputs = None
161161
self._output_dir = None
162-
self._id = self.name # for compatibility with node expansion using iterables
163162

164163
self.iterables = iterables
165164
self.synchronize = synchronize
@@ -249,14 +248,6 @@ def n_procs(self, value):
249248
if hasattr(self._interface.inputs, 'num_threads'):
250249
self._interface.inputs.num_threads = self._n_procs
251250

252-
@property
253-
def itername(self):
254-
"""Name for expanded iterable"""
255-
itername = self._id
256-
if self._hierarchy:
257-
itername = '%s.%s' % (self._hierarchy, self._id)
258-
return itername
259-
260251
def output_dir(self):
261252
"""Return the location of the output directory for the node"""
262253
# Output dir is cached

nipype/pipeline/engine/tests/test_engine.py

+159
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ def test_write_graph_runs(tmpdir):
441441

442442
assert os.path.exists('graph.dot') or os.path.exists(
443443
'graph_detailed.dot')
444+
444445
try:
445446
os.remove('graph.dot')
446447
except OSError:
@@ -484,6 +485,164 @@ def test_deep_nested_write_graph_runs(tmpdir):
484485
pass
485486

486487

488+
# examples of dot files used in the following test
489+
dotfile_orig = ['strict digraph {\n',
490+
'"mod1 (engine)";\n',
491+
'"mod2 (engine)";\n',
492+
'"mod1 (engine)" -> "mod2 (engine)";\n',
493+
'}\n']
494+
495+
dotfile_detailed_orig = ['digraph structs {\n',
496+
'node [shape=record];\n',
497+
'pipemod1 [label="{IN}|{ mod1 | engine | }|{OUT|<outoutput1> output1}"];\n',
498+
'pipemod2 [label="{IN|<ininput1> input1}|{ mod2 | engine | }|{OUT}"];\n',
499+
'pipemod1:outoutput1:e -> pipemod2:ininput1:w;\n',
500+
'}']
501+
502+
503+
dotfile_hierarchical = ['digraph pipe{\n',
504+
' label="pipe";\n',
505+
' pipe_mod1[label="mod1 (engine)"];\n',
506+
' pipe_mod2[label="mod2 (engine)"];\n',
507+
' pipe_mod1 -> pipe_mod2;\n',
508+
'}']
509+
510+
dotfile_colored = ['digraph pipe{\n',
511+
' label="pipe";\n',
512+
' pipe_mod1[label="mod1 (engine)", style=filled, fillcolor="#FFFFC8"];\n',
513+
' pipe_mod2[label="mod2 (engine)", style=filled, fillcolor="#FFFFC8"];\n',
514+
' pipe_mod1 -> pipe_mod2;\n',
515+
'}']
516+
517+
dotfiles = {
518+
"orig": dotfile_orig,
519+
"flat": dotfile_orig,
520+
"exec": dotfile_orig,
521+
"hierarchical": dotfile_hierarchical,
522+
"colored": dotfile_colored
523+
}
524+
525+
@pytest.mark.parametrize("simple", [True, False])
526+
@pytest.mark.parametrize("graph_type", ['orig', 'flat', 'exec', 'hierarchical', 'colored'])
527+
def test_write_graph_dotfile(tmpdir, graph_type, simple):
528+
""" checking dot files for a workflow without iterables"""
529+
tmpdir.chdir()
530+
531+
pipe = pe.Workflow(name='pipe')
532+
mod1 = pe.Node(interface=EngineTestInterface(), name='mod1')
533+
mod2 = pe.Node(interface=EngineTestInterface(), name='mod2')
534+
pipe.connect([(mod1, mod2, [('output1', 'input1')])])
535+
pipe.write_graph(
536+
graph2use=graph_type, simple_form=simple, format='dot')
537+
538+
with open("graph.dot") as f:
539+
graph_str = f.read()
540+
541+
if simple:
542+
for line in dotfiles[graph_type]:
543+
assert line in graph_str
544+
else:
545+
# if simple=False graph.dot uses longer names
546+
for line in dotfiles[graph_type]:
547+
if graph_type in ["hierarchical", "colored"]:
548+
assert line.replace("mod1 (engine)", "mod1.EngineTestInterface.engine").replace(
549+
"mod2 (engine)", "mod2.EngineTestInterface.engine") in graph_str
550+
else:
551+
assert line.replace(
552+
"mod1 (engine)", "pipe.mod1.EngineTestInterface.engine").replace(
553+
"mod2 (engine)", "pipe.mod2.EngineTestInterface.engine") in graph_str
554+
555+
# graph_detailed is the same for orig, flat, exec (if no iterables)
556+
# graph_detailed is not created for hierachical or colored
557+
if graph_type not in ["hierarchical", "colored"]:
558+
with open("graph_detailed.dot") as f:
559+
graph_str = f.read()
560+
for line in dotfile_detailed_orig:
561+
assert line in graph_str
562+
563+
564+
# examples of dot files used in the following test
565+
dotfile_detailed_iter_exec = [
566+
'digraph structs {\n',
567+
'node [shape=record];\n',
568+
'pipemod1aIa1 [label="{IN}|{ a1 | engine | mod1.aI }|{OUT|<outoutput1> output1}"];\n',
569+
'pipemod2a1 [label="{IN|<ininput1> input1}|{ a1 | engine | mod2 }|{OUT}"];\n',
570+
'pipemod1aIa0 [label="{IN}|{ a0 | engine | mod1.aI }|{OUT|<outoutput1> output1}"];\n',
571+
'pipemod2a0 [label="{IN|<ininput1> input1}|{ a0 | engine | mod2 }|{OUT}"];\n',
572+
'pipemod1aIa0:outoutput1:e -> pipemod2a0:ininput1:w;\n',
573+
'pipemod1aIa1:outoutput1:e -> pipemod2a1:ininput1:w;\n',
574+
'}']
575+
576+
dotfile_iter_hierarchical = [
577+
'digraph pipe{\n',
578+
' label="pipe";\n',
579+
' pipe_mod1[label="mod1 (engine)", shape=box3d,style=filled, color=black, colorscheme=greys7 fillcolor=2];\n',
580+
' pipe_mod2[label="mod2 (engine)"];\n',
581+
' pipe_mod1 -> pipe_mod2;\n',
582+
'}']
583+
584+
dotfile_iter_colored = [
585+
'digraph pipe{\n',
586+
' label="pipe";\n',
587+
' pipe_mod1[label="mod1 (engine)", shape=box3d,style=filled, color=black, colorscheme=greys7 fillcolor=2];\n',
588+
' pipe_mod2[label="mod2 (engine)", style=filled, fillcolor="#FFFFC8"];\n',
589+
' pipe_mod1 -> pipe_mod2;\n',
590+
'}']
591+
592+
dotfiles_iter = {
593+
"orig": dotfile_orig,
594+
"flat": dotfile_orig,
595+
"exec": dotfile_orig,
596+
"hierarchical": dotfile_iter_hierarchical,
597+
"colored": dotfile_iter_colored
598+
}
599+
600+
dotfiles_detailed_iter = {
601+
"orig": dotfile_detailed_orig,
602+
"flat": dotfile_detailed_orig,
603+
"exec": dotfile_detailed_iter_exec
604+
}
605+
606+
@pytest.mark.parametrize("simple", [True, False])
607+
@pytest.mark.parametrize("graph_type", ['orig', 'flat', 'exec', 'hierarchical', 'colored'])
608+
def test_write_graph_dotfile_iterables(tmpdir, graph_type, simple):
609+
""" checking dot files for a workflow with iterables"""
610+
tmpdir.chdir()
611+
612+
pipe = pe.Workflow(name='pipe')
613+
mod1 = pe.Node(interface=EngineTestInterface(), name='mod1')
614+
mod1.iterables = ('input1', [1, 2])
615+
mod2 = pe.Node(interface=EngineTestInterface(), name='mod2')
616+
pipe.connect([(mod1, mod2, [('output1', 'input1')])])
617+
pipe.write_graph(
618+
graph2use=graph_type, simple_form=simple, format='dot')
619+
620+
with open("graph.dot") as f:
621+
graph_str = f.read()
622+
623+
if simple:
624+
for line in dotfiles_iter[graph_type]:
625+
assert line in graph_str
626+
else:
627+
# if simple=False graph.dot uses longer names
628+
for line in dotfiles_iter[graph_type]:
629+
if graph_type in ["hierarchical", "colored"]:
630+
assert line.replace("mod1 (engine)", "mod1.EngineTestInterface.engine").replace(
631+
"mod2 (engine)", "mod2.EngineTestInterface.engine") in graph_str
632+
else:
633+
assert line.replace(
634+
"mod1 (engine)", "pipe.mod1.EngineTestInterface.engine").replace(
635+
"mod2 (engine)", "pipe.mod2.EngineTestInterface.engine") in graph_str
636+
637+
# graph_detailed is not created for hierachical or colored
638+
if graph_type not in ["hierarchical", "colored"]:
639+
with open("graph_detailed.dot") as f:
640+
graph_str = f.read()
641+
for line in dotfiles_detailed_iter[graph_type]:
642+
assert line in graph_str
643+
644+
645+
487646
def test_io_subclass():
488647
"""Ensure any io subclass allows dynamic traits"""
489648
from nipype.interfaces.io import IOBase

nipype/pipeline/engine/utils.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ def _write_detailed_dot(graph, dotfilename):
570570
# write nodes
571571
edges = []
572572
for n in nx.topological_sort(graph):
573-
nodename = str(n)
573+
nodename = n.itername
574574
inports = []
575575
for u, v, d in graph.in_edges(nbunch=n, data=True):
576576
for cd in d['connect']:
@@ -582,8 +582,8 @@ def _write_detailed_dot(graph, dotfilename):
582582
ipstrip = 'in%s' % _replacefunk(inport)
583583
opstrip = 'out%s' % _replacefunk(outport)
584584
edges.append(
585-
'%s:%s:e -> %s:%s:w;' % (str(u).replace('.', ''), opstrip,
586-
str(v).replace('.', ''), ipstrip))
585+
'%s:%s:e -> %s:%s:w;' % (u.itername.replace('.', ''), opstrip,
586+
v.itername.replace('.', ''), ipstrip))
587587
if inport not in inports:
588588
inports.append(inport)
589589
inputstr = ['{IN'] + [

0 commit comments

Comments
 (0)