-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathflow_diagram.py
180 lines (154 loc) · 5.35 KB
/
flow_diagram.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
import os
from os import path as op
title = "mne-python flow diagram"
font_face = "Arial"
node_size = 12
node_small_size = 9
edge_size = 9
sensor_color = "#7bbeca"
source_color = "#ff6347"
legend = f"""
<<FONT POINT-SIZE="{edge_size}">
<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="4" CELLPADDING="4">
<TR><TD BGCOLOR="{sensor_color}"> </TD><TD ALIGN="left">
Sensor (M/EEG) space</TD></TR>
<TR><TD BGCOLOR="{source_color}"> </TD><TD ALIGN="left">
Source (brain) space</TD></TR>
</TABLE></FONT>>"""
legend = "".join(legend.split("\n"))
nodes = dict(
T1="T1",
flashes="Flash5/30",
trans="Head-MRI trans",
recon="Freesurfer surfaces",
bem="BEM",
src="Source space\nmne.SourceSpaces",
cov="Noise covariance\nmne.Covariance",
fwd="Forward solution\nmne.forward.Forward",
inv="Inverse operator\nmne.minimum_norm.InverseOperator",
stc="Source estimate\nmne.SourceEstimate",
raw="Raw data\nmne.io.Raw",
epo="Epoched data\nmne.Epochs",
evo="Averaged data\nmne.Evoked",
pre="Preprocessed data\nmne.io.Raw",
legend=legend,
)
sensor_space = ("raw", "pre", "epo", "evo", "cov")
source_space = ("src", "stc", "bem", "flashes", "recon", "T1")
edges = (
("T1", "recon"),
("flashes", "bem"),
("recon", "bem"),
("recon", "src", "mne.setup_source_space"),
("src", "fwd"),
("bem", "fwd"),
("trans", "fwd", "mne.make_forward_solution"),
("fwd", "inv"),
("cov", "inv", "mne.make_inverse_operator"),
("inv", "stc"),
("evo", "stc", "mne.minimum_norm.apply_inverse"),
(
"raw",
"pre",
"raw.filter\n"
"mne.preprocessing.ICA\n"
"mne.preprocessing.compute_proj_eog\n"
"mne.preprocessing.compute_proj_ecg\n"
"...",
),
("pre", "epo", "mne.Epochs"),
("epo", "evo", "epochs.average"),
("epo", "cov", "mne.compute_covariance"),
)
subgraphs = (
[
("T1", "flashes", "recon", "bem", "src"),
(
f'<Structural information<BR/><FONT POINT-SIZE="{node_small_size}"><I>'
"Freesurfer / MNE-C</I></FONT>>"
),
],
)
def setup(app):
app.connect("builder-inited", generate_flow_diagram)
app.add_config_value("make_flow_diagram", True, "html")
def setup_module():
# HACK: Stop nosetests running setup() above
pass
def generate_flow_diagram(app):
out_dir = op.join(app.builder.outdir, "_static")
if not op.isdir(out_dir):
os.makedirs(out_dir)
out_fname = op.join(out_dir, "mne-python_flow.svg")
make_flow_diagram = app is None or bool(app.builder.config.make_flow_diagram)
if not make_flow_diagram:
print("Skipping flow diagram, webpage will have a missing image")
return
import pygraphviz as pgv
g = pgv.AGraph(name=title, directed=True)
for key, label in nodes.items():
label = label.split("\n")
if len(label) > 1:
label[0] = f'<<FONT POINT-SIZE="{node_size}">' + label[0] + "</FONT>"
for li in range(1, len(label)):
label[li] = (
f'<FONT POINT-SIZE="{node_small_size}"><I>'
+ label[li]
+ "</I></FONT>"
)
label[-1] = label[-1] + ">"
label = "<BR/>".join(label)
else:
label = label[0]
g.add_node(key, shape="plaintext", label=label)
# Create and customize nodes and edges
for edge in edges:
g.add_edge(*edge[:2])
e = g.get_edge(*edge[:2])
if len(edge) > 2:
e.attr["label"] = (
"<<I>"
+ '<BR ALIGN="LEFT"/>'.join(edge[2].split("\n"))
+ '<BR ALIGN="LEFT"/></I>>'
)
e.attr["fontsize"] = edge_size
# Change colors
for these_nodes, color in zip(
(sensor_space, source_space), (sensor_color, source_color)
):
for node in these_nodes:
g.get_node(node).attr["fillcolor"] = color
g.get_node(node).attr["style"] = "filled"
# Create subgraphs
for si, subgraph in enumerate(subgraphs):
g.add_subgraph(subgraph[0], f"cluster{si}", label=subgraph[1], color="black")
# Format (sub)graphs
for gr in g.subgraphs() + [g]:
for x in [gr.node_attr, gr.edge_attr]:
x["fontname"] = font_face
g.node_attr["shape"] = "box"
# A couple of special ones
for ni, node in enumerate(("fwd", "inv", "trans")):
node = g.get_node(node)
node.attr["gradientangle"] = 270
colors = (source_color, sensor_color)
colors = colors if ni == 0 else colors[::-1]
node.attr["fillcolor"] = ":".join(colors)
node.attr["style"] = "filled"
del node
g.get_node("legend").attr.update(shape="plaintext", margin=0, rank="sink")
# put legend in same rank/level as inverse
leg = g.add_subgraph(["legend", "inv"], name="legendy")
leg.graph_attr["rank"] = "same"
g.layout("dot")
g.draw(out_fname, format="svg")
return g
# This is useful for testing/iterating to see what the result looks like
if __name__ == "__main__":
from mne.io.constants import Bunch
out_dir = op.abspath(op.join(op.dirname(__file__), "..", "_build", "html"))
app = Bunch(builder=Bunch(outdir=out_dir, config=Bunch(make_flow_diagram=True)))
g = generate_flow_diagram(app)