RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
eurofer_network.py
Go to the documentation of this file.
1import matplotlib.pyplot as plt
2import matplotlib.patches as mpatches
3from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
4import matplotlib.patheffects as pe
5import numpy as np
6
7# ── Figure setup ─────────────────────────────────────────────────────────────
8fig, ax = plt.subplots(figsize=(28, 21))
9ax.set_xlim(0, 28)
10ax.set_ylim(0, 21)
11ax.axis('off')
12fig.patch.set_facecolor('white')
13
14# ── Font sizes ────────────────────────────────────────────────────────────────
15FS_TITLE = 30
16FS_SUBT = 24
17FS_NODE = 20
18FS_BOX = 20
19FS_LABEL = 20
20FS_EQ = 20
21FS_KEY_HDR = 20
22FS_KEY = 20
23
24# ── Colours ───────────────────────────────────────────────────────────────────
25C_SIA = '#C62828'
26C_CAV = '#1565C0'
27C_HE = '#1B5E20'
28C_SRC = '#004D40'
29C_SINK = '#37474F'
30C_P3 = '#C62828'
31C_P5 = '#E65100'
32C_P1 = '#6A1B9A'
33C_P2V = '#1565C0'
34C_P2H = '#2E7D32'
35C_P2I = '#00695C'
36C_P7 = '#558B2F'
37C_P8 = '#546E7A'
38C_P4 = '#37474F'
39C_SRC_ARROW = '#90A4AE' # source-box connector arrows (was white, invisible on white bg)
40
41
42# ─────────────────────────────────────────────────────────────────────────────
43# helpers
44# ─────────────────────────────────────────────────────────────────────────────
45def circle(ax, cx, cy, r, color, zorder=3):
46 c = plt.Circle((cx, cy), r, color=color, zorder=zorder, linewidth=0)
47 ax.add_patch(c)
48
49def node_text(ax, cx, cy, txt, fs=FS_NODE, color='white', zorder=4):
50 ax.text(cx, cy, txt, ha='center', va='center', fontsize=fs,
51 color=color, fontweight='bold', zorder=zorder)
52
53def fancy_box(ax, x, y, w, h, fc, ec='white', lw=2.0, radius=0.15, zorder=2):
54 p = FancyBboxPatch((x, y), w, h,
55 boxstyle=f"round,pad={radius}",
56 facecolor=fc, edgecolor=ec, linewidth=lw, zorder=zorder)
57 ax.add_patch(p)
58
59def arr(ax, x1, y1, x2, y2, color, lw=2.2, ls='-', rad=0.0, zorder=2):
60 cs = f'arc3,rad={rad}' if rad != 0 else 'arc3,rad=0'
61 ax.annotate('', xy=(x2, y2), xytext=(x1, y1),
62 arrowprops=dict(arrowstyle='->', color=color, lw=lw,
63 linestyle=ls,
64 connectionstyle=cs),
65 zorder=zorder)
66
67def labeled_box(ax, x, y, w, h, title, sub, fc=C_SRC):
68 fancy_box(ax, x, y, w, h, fc=fc, ec='white', lw=2.0)
69 ax.text(x+w/2, y+h*0.65, title, ha='center', va='center',
70 fontsize=FS_BOX, color='white', fontweight='bold')
71 ax.text(x+w/2, y+h*0.25, sub, ha='center', va='center',
72 fontsize=FS_LABEL, color='white')
73
74
75# ─────────────────────────────────────────────────────────────────────────────
76# TITLE
77# ─────────────────────────────────────────────────────────────────────────────
78ax.text(14, 20.5,
79 'EUROFER97 Reaction Network — Graph-Walker Representation',
80 ha='center', va='center', fontsize=FS_TITLE, fontweight='bold')
81ax.text(14, 19.85,
82 r'$c_n$: SIA clusters, $c_{m,\ell}$: cavities, $c_h$: free He',
83 ha='center', va='center', fontsize=FS_SUBT)
84
85
86# ─────────────────────────────────────────────────────────────────────────────
87# SOURCE BOXES
88# ─────────────────────────────────────────────────────────────────────────────
89# Displacement cascade (left)
90labeled_box(ax, 0.4, 18.0, 6.0, 1.5,
91 'Displacement cascade', r'$G_n,\ G_{m,\ell}$')
92
93# He production (right)
94labeled_box(ax, 21.5, 18.0, 6.0, 1.5,
95 'He production / transmutation', r'$G_{\rm He}$')
96
97# Graph-Walker equation (centre)
98fancy_box(ax, 8.8, 17.8, 10.3, 1.7, fc='#FFF9C4', ec='#F57F17', lw=2.5)
99ax.text(13.95, 19.1,
100 'Graph Walker (abstract master equation)',
101 ha='center', va='center', fontsize=FS_LABEL, fontweight='bold',
102 color='#E65100')
103ax.text(13.95, 18.3,
104 r'$\dfrac{dc_a}{dt} = G_a + \sum_{r\in\{P1\ldots P8\}}'
105 r'S_{ad}\,r_r(\mathbf{c}) - D_a c_a$',
106 ha='center', va='center', fontsize=FS_EQ)
107
108
109# ─────────────────────────────────────────────────────────────────────────────
110# SIA CLUSTER CHAIN c_1 c_2 c_3 ... c_n
111# ─────────────────────────────────────────────────────────────────────────────
112SIA_Y = 14.5
113NR = 0.60 # node radius
114SIA_X = [1.4, 3.2, 5.0, 6.8]
115SIA_L = [r'$c_1$', r'$c_2$', r'$c_3$', r'$c_n$']
116
117ax.text(0.3, 16.2, r'$c_I\;(\sigma=+1)$',
118 ha='left', va='center', fontsize=FS_LABEL+1,
119 color=C_SIA, fontweight='bold')
120
121for x, lbl in zip(SIA_X, SIA_L):
122 circle(ax, x, SIA_Y, NR, C_SIA)
123 node_text(ax, x, SIA_Y, lbl)
124
125# dots between c_3 and c_n
126ax.text(5.9, SIA_Y, r'$\cdots$', ha='center', va='center',
127 fontsize=22, color=C_SIA, zorder=3)
128
129# P3 growth arrows (forward, top half of node gap)
130for i in range(len(SIA_X)-1):
131 x1 = SIA_X[i] + NR + 0.05
132 x2 = SIA_X[i+1] - NR - 0.05
133 if i == 2:
134 x1 += 0.25; x2 -= 0.1
135 arr(ax, x1, SIA_Y+0.18, x2, SIA_Y+0.18, C_P3, lw=2.5)
136
137ax.text(3.2, SIA_Y+1.15,
138 r'$P3:\ \kappa_{\ell\ell}^{loop}c_n c_1$',
139 ha='center', va='center', fontsize=FS_LABEL,
140 color=C_P3, fontweight='bold',
141 bbox=dict(boxstyle='round,pad=0.25', fc='white', ec=C_P3, lw=1.8))
142
143# P5i emission arrows (backward, bottom half)
144for i in range(1, len(SIA_X)):
145 x1 = SIA_X[i] - NR - 0.05
146 x2 = SIA_X[i-1] + NR + 0.05
147 if i == 3:
148 x1 -= 0.1; x2 += 0.25
149 arr(ax, x1, SIA_Y-0.20, x2, SIA_Y-0.20, C_P5, lw=2.0, ls='dashed')
150
151ax.text(4.0, SIA_Y-1.1,
152 r'$P5i:\ \varepsilon_I(q)c_n$',
153 ha='center', va='center', fontsize=FS_LABEL,
154 color=C_P5, fontweight='bold',
155 bbox=dict(boxstyle='round,pad=0.25', fc='white', ec=C_P5, lw=1.8))
156
157
158# ─────────────────────────────────────────────────────────────────────────────
159# FREE He NODE
160# ─────────────────────────────────────────────────────────────────────────────
161HE_X, HE_Y = 10.5, 14.5
162fancy_box(ax, HE_X-1.2, HE_Y-0.82, 2.4, 1.65,
163 fc=C_HE, ec='white', lw=2.5, zorder=3)
164ax.text(HE_X, HE_Y+0.22, r'$c_h$',
165 ha='center', va='center', fontsize=FS_NODE+4,
166 color='white', fontweight='bold', zorder=5)
167ax.text(HE_X, HE_Y-0.40, '(free He)',
168 ha='center', va='center', fontsize=FS_LABEL-1,
169 color='white', zorder=5)
170
171
172# ─────────────────────────────────────────────────────────────────────────────
173# CAVITY GRID c_{m,l}
174# ─────────────────────────────────────────────────────────────────────────────
175CR = 0.58 # cavity node radius
176CAV_MX = [13.5, 17.0, 22.5] # m=1, m=2, m columns
177CAV_LY = [14.5, 12.0, 9.5] # l=0, l=1, l=2 rows
178M_LABELS = ['m=1', 'm=2', 'm']
179L_LABELS = [r'$\ell=0$', r'$\ell=1$', r'$\ell=2$']
180
181ax.text(12.5, 16.3, r'$c_V\;(\sigma=-1)$',
182 ha='left', va='center', fontsize=FS_LABEL+1,
183 color=C_CAV, fontweight='bold')
184
185for j, (mx, ml) in enumerate(zip(CAV_MX, M_LABELS)):
186 ax.text(mx, 15.55, ml, ha='center', va='center',
187 fontsize=FS_LABEL, color=C_CAV, fontweight='bold')
188
189for i, (ly, ll) in enumerate(zip(CAV_LY, L_LABELS)):
190 ax.text(12.0, ly, ll, ha='center', va='center',
191 fontsize=FS_LABEL, color=C_CAV)
192
193# draw cavity nodes
194for i, ly in enumerate(CAV_LY):
195 for j, mx in enumerate(CAV_MX):
196 m_str = str(j+1) if j < 2 else 'm'
197 lbl = rf'$c_{{{m_str},{i}}}$'
198 circle(ax, mx, ly, CR, C_CAV)
199 node_text(ax, mx, ly, lbl, fs=FS_NODE-1)
200
201# dots between col m=2 and col m
202for ly in CAV_LY:
203 ax.text(19.8, ly, r'$\cdots$', ha='center', va='center',
204 fontsize=22, color=C_CAV, zorder=3)
205# dotted extension line on l=0 row
206ax.plot([CAV_MX[2]+CR, CAV_MX[2]+1.4], [CAV_LY[0], CAV_LY[0]],
207 ':', color=C_CAV, lw=2.2)
208
209# P2v cavity growth arrows (horizontal, l=0 row)
210arr(ax, CAV_MX[0]+CR+0.05, CAV_LY[0]+0.18,
211 CAV_MX[1]-CR-0.05, CAV_LY[0]+0.18, C_P2V, lw=2.5)
212arr(ax, CAV_MX[1]+CR+0.05, CAV_LY[0]+0.18,
213 CAV_MX[2]-CR-0.9, CAV_LY[0]+0.18, C_P2V, lw=2.5)
214
215ax.text(19.5, 16.6,
216 r'$P2v:\ \kappa_{V,m}^{cav}c_{1,0}\,\tilde{c}_m$',
217 ha='center', va='center', fontsize=FS_LABEL, color=C_P2V,
218 fontweight='bold',
219 bbox=dict(boxstyle='round,pad=0.25', fc='white', ec=C_P2V, lw=1.8))
220
221# P2h He-trapping arrows (vertical, downward, left side of node)
222for mx in CAV_MX:
223 for i in range(len(CAV_LY)-1):
224 y1 = CAV_LY[i] - CR - 0.05
225 y2 = CAV_LY[i+1] + CR + 0.05
226 arr(ax, mx-0.12, y1, mx-0.12, y2, C_P2H, lw=2.5)
227
228ax.text(24.5, 13.2,
229 r'$P2h:\ \kappa_{h,m}^{cav}c_h\,c_{m,\ell}$',
230 ha='left', va='center', fontsize=FS_LABEL, color=C_P2H,
231 bbox=dict(boxstyle='round,pad=0.25', fc='white', ec=C_P2H, lw=1.8))
232ax.text(24.5, 12.55,
233 'P2h: He trap\nP5h+P8: emit/resolute',
234 ha='left', va='center', fontsize=FS_LABEL-1, color=C_P2H)
235
236# P5v emission arrows (vertical, upward, right side of node)
237for mx in CAV_MX:
238 for i in range(1, len(CAV_LY)):
239 y1 = CAV_LY[i] + CR + 0.05
240 y2 = CAV_LY[i-1] - CR - 0.05
241 arr(ax, mx+0.12, y1, mx+0.12, y2, C_P5, lw=2.0, ls='dashed')
242
243ax.text(15.25, 13.25,
244 r'$P5v:\ \varepsilon_v(m,\ell)c_{m,\ell}$',
245 ha='center', va='center', fontsize=FS_LABEL, color=C_P5,
246 bbox=dict(boxstyle='round,pad=0.25', fc='white', ec=C_P5, lw=1.8))
247
248# P2i/P6 SIA absorption by cavity (He → cavity l=0)
249arr(ax, HE_X+1.2, HE_Y+0.25, CAV_MX[0]-CR-0.05, CAV_LY[0]+0.20,
250 C_P2I, lw=2.5)
251ax.text(10.5, 13.15,
252 r'$P2i/P6:\ \kappa_{i,m}^{cav}c_n\,c_m$',
253 ha='center', va='center', fontsize=FS_LABEL, color=C_P2I,
254 fontweight='bold',
255 bbox=dict(boxstyle='round,pad=0.25', fc='#E8F5E9', ec=C_P2I, lw=1.8))
256
257# P1 V-I recombination (c_1 → c_{1,0})
258arr(ax, SIA_X[0]+NR*0.7, SIA_Y-NR*0.7,
259 CAV_MX[0]-CR*0.7, CAV_LY[0]-CR*0.7,
260 C_P1, lw=2.5, rad=0.15)
261ax.text(7.8, 12.1,
262 r'$P1:\ \mathcal{K}_{iV}c_1 c_{1,0}$',
263 ha='center', va='center', fontsize=FS_LABEL, color=C_P1,
264 bbox=dict(boxstyle='round,pad=0.25', fc='white', ec=C_P1, lw=1.8))
265
266# P7 trap mutation V_{m,l} → V_{m+1,l} + I_1
267# arrow from cavity to SIA side (escaping interstitial)
268ax.annotate('', xy=(SIA_X[0]-NR*0.5, SIA_Y-NR*0.85),
269 xytext=(CAV_MX[0]+CR*0.5, CAV_LY[1]-CR*0.5),
270 arrowprops=dict(arrowstyle='->', color=C_P7, lw=2.2,
271 connectionstyle='arc3,rad=-0.35'),
272 zorder=2)
273ax.text(8.5, 11.0,
274 r'$P7:\ \Gamma_{TM}c_{m,\ell}\!\to\!I_1$',
275 ha='center', va='center', fontsize=FS_LABEL, color=C_P7,
276 bbox=dict(boxstyle='round,pad=0.25', fc='white', ec=C_P7, lw=1.8))
277
278
279# ─────────────────────────────────────────────────────────────────────────────
280# SOURCE → NETWORK arrows
281# ─────────────────────────────────────────────────────────────────────────────
282# Displacement cascade → c_2 (SIA)
283ax.annotate('', xy=(SIA_X[1], SIA_Y+NR),
284 xytext=(3.2, 18.0),
285 arrowprops=dict(arrowstyle='->', color=C_SRC_ARROW, lw=1.8,
286 linestyle='dashed',
287 connectionstyle='arc3,rad=0.0'), zorder=5)
288# Displacement cascade → c_{1,0}
289ax.annotate('', xy=(CAV_MX[0], CAV_LY[0]+CR),
290 xytext=(6.0, 18.0),
291 arrowprops=dict(arrowstyle='->', color=C_SRC_ARROW, lw=1.8,
292 linestyle='dashed',
293 connectionstyle='arc3,rad=-0.15'), zorder=5)
294# He source → c_h
295ax.annotate('', xy=(HE_X, HE_Y+0.83),
296 xytext=(22.0, 18.0),
297 arrowprops=dict(arrowstyle='->', color=C_SRC_ARROW, lw=1.8,
298 linestyle='dashed',
299 connectionstyle='arc3,rad=0.2'), zorder=5)
300
301
302# ─────────────────────────────────────────────────────────────────────────────
303# SINK BOXES — placed lower to avoid overlap with process key
304# ─────────────────────────────────────────────────────────────────────────────
305SINK_Y = 5.6 # top of sink boxes
306SINK_H = 1.4
307SINK_W = 5.8
308
309labeled_box(ax, 0.8, SINK_Y-SINK_H, SINK_W, SINK_H,
310 'Dislocation network', r'$D_\alpha^d$', fc=C_SINK)
311labeled_box(ax, 10.9, SINK_Y-SINK_H, SINK_W, SINK_H,
312 'Grain boundaries', r'$D_\alpha^{gb}$', fc=C_SINK)
313labeled_box(ax, 21.2, SINK_Y-SINK_H, SINK_W, SINK_H,
314 'Precipitates', r'$D_\alpha^p$', fc=C_SINK)
315
316# P4 label
317ax.text(14.0, 7.2,
318 r'$P4:\ D_\alpha c_\alpha$',
319 ha='center', va='center', fontsize=FS_LABEL+1, color=C_P4,
320 fontweight='bold')
321
322# Dashed P4 arrows from all cluster nodes down to sinks
323sink_centres = [3.7, 13.8, 24.1]
324source_nodes = (
325 [(x, SIA_Y-NR) for x in SIA_X] +
326 [(HE_X, HE_Y-0.83)] +
327 [(mx, CAV_LY[-1]-CR) for mx in CAV_MX]
328)
329sink_targets = (
330 [(3.7, SINK_Y-SINK_H)] * 4 +
331 [(13.8, SINK_Y-SINK_H)] +
332 [(24.1, SINK_Y-SINK_H)] * 3
333)
334for (x1,y1),(x2,y2) in zip(source_nodes, sink_targets):
335 arr(ax, x1, y1, x2, y2, C_P4, lw=1.5, ls='dashed', zorder=1)
336
337
338# ─────────────────────────────────────────────────────────────────────────────
339# PROCESS KEY — placed BELOW all sink boxes
340# ─────────────────────────────────────────────────────────────────────────────
341KY = 3.9 # top of key area (well below SINK_Y-SINK_H = 4.2)
342
343# Three columns of three keys, each aligned under its sink box
344key_columns = [
345 # under Dislocation network
346 [
347 (C_P3, '-', r'P3: loop growth $I_{n-1}+I_1\to I_n$'),
348 (C_P5, '--', r'P5: SIA emission (inverse of P3)'),
349 (C_P1, '-', r'P1: V-I recombination $I_1+V_1\to\emptyset$'),
350 ],
351 # under Grain boundaries
352 [
353 (C_P2V, '-', r'P2v: cavity growth $v+V_m\to V_{m+1}$'),
354 (C_P2H, '-', r'P2h: He trapping $c_h+V_{m,\ell}\to V_{m,\ell+1}$'),
355 (C_P2I, '-', r'P2i/P6: SIA absorption by cavity'),
356 ],
357 # under Precipitates
358 [
359 (C_P7, '-', r'P7: trap mutation $V_{m,\ell}\to V_{m+1,\ell}+I_1$'),
360 (C_P8, '--', r'P8: radiation re-solution'),
361 (C_P4, '--', r'P4: absorption at fixed sinks $D_\alpha c_\alpha$'),
362 ],
363]
364col_x = [0.5, 10.6, 20.9] # left edges, aligned with sink box columns
365
366ax.text(col_x[0], KY, 'Process key',
367 fontsize=FS_KEY_HDR, fontweight='bold', va='top')
368
369ROW_H = 0.45
370for col_idx, entries in enumerate(key_columns):
371 KX = col_x[col_idx]
372 for k, (color, ls, label) in enumerate(entries):
373 y_row = KY - 0.55 - k * ROW_H
374 lx = KX + 0.15
375 if ls == '--':
376 ax.plot([lx, lx+0.9], [y_row, y_row],
377 color=color, lw=2.2, linestyle='dashed')
378 else:
379 ax.annotate('', xy=(lx+0.9, y_row), xytext=(lx, y_row),
380 arrowprops=dict(arrowstyle='->', color=color, lw=2.2))
381 ax.text(lx+1.15, y_row, label,
382 va='center', fontsize=FS_KEY, color='black')
383
384
385# ─────────────────────────────────────────────────────────────────────────────
386# SAVE
387# ─────────────────────────────────────────────────────────────────────────────
388import os, shutil
389
390module_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
391out_dir = os.path.join(module_root, 'output', 'schematics')
392os.makedirs(out_dir, exist_ok=True)
393
394out_path = os.path.join(out_dir, 'eurofer_reaction_network.png')
395old_path = os.path.join(out_dir, 'eurofer_reaction_network_old.png')
396
397if os.path.exists(out_path):
398 shutil.move(out_path, old_path)
399
400plt.savefig(out_path, dpi=200, bbox_inches='tight',
401 facecolor='white', edgecolor='none')
402plt.close()
403print("PNG saved to", out_path)
arr(ax, x1, y1, x2, y2, color, lw=2.2, ls='-', rad=0.0, zorder=2)
node_text(ax, cx, cy, txt, fs=FS_NODE, color='white', zorder=4)
fancy_box(ax, x, y, w, h, fc, ec='white', lw=2.0, radius=0.15, zorder=2)
labeled_box(ax, x, y, w, h, title, sub, fc=C_SRC)
circle(ax, cx, cy, r, color, zorder=3)