RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
size_distributions.py
Go to the documentation of this file.
1"""
2size_distributions.py
3Reads Size_Distribution_Database_FM_Steels.xlsx and produces
4publication-quality bar-graph figures of dislocation loop and
5void/cavity size distributions — one figure per condition.
6
7Programmatic use (Jupyter):
8 from size_distributions import plot_size_distributions
9 figs = plot_size_distributions(
10 xlsx="../../input/Size_Distribution_Database_FM_Steels.xlsx",
11 outdir="../../output/size_distributions",
12 font_size=16, save=True, show=True,
13 )
14
15CLI use:
16 python size_distributions.py --xlsx ... --outdir ...
17"""
18
19import argparse
20import os
21import re
22import warnings
23import numpy as np
24import pandas as pd
25import matplotlib
26import matplotlib.pyplot as plt
27import matplotlib.patches as mpatches
28
29warnings.filterwarnings("ignore")
30
31# ── Per-condition catalog ────────────────────────────────────────────────────
32# Each entry produces ONE single-panel figure. Empty data → skipped.
33# fields: kind, material, dose, temp, irr_type (optional filter), color,
34# mean (optional vline), citation, xlim, title_prefix, bimodal (void only)
35LOOP_CASES = [
36 dict(kind="loop", material="EUROFER97", dose=15, temp=330, color="#1B4F8A",
37 mean=3.4, citation="[CIT-01] Klimenkov 2012", xlim=(0, 16),
38 title="EUROFER97 — 15 dpa / 330 °C (BOR-60, neutron)"),
39 dict(kind="loop", material="EUROFER97", dose=32, temp=335, color="#E67E22",
40 mean=4.8, citation="[CIT-01] Klimenkov 2012", xlim=(0, 16),
41 title="EUROFER97 — 32 dpa / 335 °C (BOR-60, neutron)"),
42 dict(kind="loop", material="EUROFER97", dose=16.3, temp=250, color="#154360",
43 mean=2.5, citation="[CIT-03] Gaganidze 2011", xlim=(0, 14),
44 title="EUROFER97 — 16.3 dpa / 250 °C (BOR-60, neutron)"),
45 dict(kind="loop", material="EUROFER97", dose=16.3, temp=300, color="#1A5276",
46 mean=4.0, citation="[CIT-03] Gaganidze 2011", xlim=(0, 14),
47 title="EUROFER97 — 16.3 dpa / 300 °C (BOR-60, neutron)"),
48 dict(kind="loop", material="EUROFER97", dose=16.3, temp=350, color="#2980B9",
49 mean=10.0, citation="[CIT-03] Gaganidze 2011", xlim=(0, 28),
50 title="EUROFER97 — 16.3 dpa / 350 °C (BOR-60, neutron)"),
51 dict(kind="loop", material="EUROFER97", dose=16.3, temp=415, color="#AED6F1",
52 mean=12.5, citation="[CIT-03] Gaganidze 2011", xlim=(0, 28),
53 title="EUROFER97 — 16.3 dpa / 415 °C (BOR-60, neutron)"),
54 dict(kind="loop", material="T91", dose=15.4, temp=376, color="#117A65",
55 mean=8.0, citation="[CIT-05] Gao 2018", xlim=(0, 50),
56 title="T91 — 15.4 dpa / 376 °C (BOR-60, neutron)"),
57 dict(kind="loop", material="T91", dose=35.1, temp=376, color="#1E8449",
58 mean=14.0, citation="[CIT-05] Gao 2018", xlim=(0, 50),
59 title="T91 — 35.1 dpa / 376 °C (BOR-60, neutron)"),
60 dict(kind="loop", material="HT9", dose=17.1, temp=377, color="#6E2F5E",
61 mean=10.0, citation="[CIT-06] Gao 2019", xlim=(0, 50),
62 title="HT9 — 17.1 dpa / 377 °C (BOR-60, neutron)"),
63 dict(kind="loop", material="HT9", dose=35.1, temp=377, color="#922B21",
64 mean=16.0, citation="[CIT-06] Gao 2019", xlim=(0, 50),
65 title="HT9 — 35.1 dpa / 377 °C (BOR-60, neutron)"),
66 dict(kind="loop", material="Pure Fe", irr_type="Ion", color="#E67E22",
67 mean=3.5, citation="[CIT-24] Tanaka 2018", xlim=(0, 10),
68 title="Pure Fe — Ion (6.4 MeV Fe³⁺, 30 dpa, 300 °C)"),
69 dict(kind="loop", material="Pure Fe", irr_type="Neutron", color="#1B4F8A",
70 mean=9.5, citation="[CIT-08] Lambrecht 2009", xlim=(0, 26),
71 title="Pure Fe — Neutron (0.19 dpa, 300 °C)"),
72]
73
74VOID_CASES = [
75 dict(kind="void", material="EUROFER97", dose=15, temp=330, color="#1B4F8A",
76 citation="[CIT-01] Klimenkov 2012", xlim=(0, 5),
77 title="EUROFER97 — 15 dpa / 330 °C (BOR-60, neutron)"),
78 dict(kind="void", material="EUROFER97", dose=32, temp=335, color="#2E86C1",
79 citation="[CIT-01] Klimenkov 2012", xlim=(0, 5),
80 title="EUROFER97 — 32 dpa / 335 °C (BOR-60, neutron)"),
81 dict(kind="void", material="T91", dose=15.4, temp=376, color="#117A65",
82 citation="[CIT-05] Gao 2018", xlim=(0, 5),
83 title="T91 — 15.4 dpa / 376 °C (BOR-60, neutron)"),
84 dict(kind="void", material="HT9", dose=17.1, temp=377, color="#1E8449",
85 citation="[CIT-06] Gao 2019", xlim=(0, 5),
86 title="HT9 — 17.1 dpa / 377 °C (BOR-60, neutron)"),
87 dict(kind="void", material="T91", dose=35.1, temp=376, color="#922B21",
88 citation="[CIT-05] Gao 2018", xlim=(0, 18), bimodal=True,
89 title="T91 — 35.1 dpa / 376 °C (BOR-60, neutron) — bimodal"),
90 dict(kind="void", material="HT9", dose=35.1, temp=377, color="#C0392B",
91 citation="[CIT-06] Gao 2019", xlim=(0, 18), bimodal=True,
92 title="HT9 — 35.1 dpa / 377 °C (BOR-60, neutron) — bimodal"),
93]
94
95
96# Rendered titles are suppressed to match `py_utils/visualization.py`: these
97# figures are captioned by the notebook or document that embeds them. Set
98# SHOW_TITLES = True to draw them again.
99SHOW_TITLES = False
100
101
102def _set_title(ax, text, **kw):
103 """Axes (or panel) title, drawn only when SHOW_TITLES is on."""
104 if SHOW_TITLES:
105 ax.set_title(text, **kw)
106
107
108def _slug(text):
109 s = re.sub(r"[^A-Za-z0-9]+", "_", text).strip("_")
110 return s.lower()
111
112
113def _apply_style(font_family, font_serif, font_size, axes_linewidth, dpi, save_dpi):
114 matplotlib.rcParams.update({
115 "font.family": font_family,
116 "font.serif": list(font_serif),
117 "font.size": font_size,
118 "axes.labelsize": font_size,
119 "axes.titlesize": font_size,
120 "legend.fontsize": font_size,
121 "xtick.labelsize": font_size,
122 "ytick.labelsize": font_size,
123 "axes.linewidth": axes_linewidth,
124 "axes.spines.top": False,
125 "axes.spines.right": False,
126 "figure.dpi": dpi,
127 "savefig.dpi": save_dpi,
128 "savefig.bbox": "tight",
129 })
130
131
132def _load_bins(xlsx_path, sheet):
133 df = pd.read_excel(xlsx_path, sheet_name=sheet, header=2)
134 df.columns = [str(c).strip() for c in df.columns]
135 return df.dropna(how="all")
136
137
138def _annotate_cite(ax, text, fontsize):
139 ax.text(0.97, 0.96, text, transform=ax.transAxes, fontsize=fontsize,
140 ha="right", va="top", color="#555555",
141 bbox=dict(facecolor="white", edgecolor="none", alpha=0.6, pad=1))
142
143
144def _save(fig, outdir, stem, formats):
145 for ext in formats:
146 fig.savefig(os.path.join(outdir, f"{stem}.{ext}"))
147
148
149def _filter(df, mat_c, type_c, dose_c, temp_c, bin_c, freq_c,
150 material=None, irr_type=None, dose=None, temp=None):
151 m = df.copy()
152 if material:
153 m = m[m[mat_c].astype(str).str.contains(material, case=False)]
154 if irr_type:
155 m = m[m[type_c].astype(str).str.contains(irr_type, case=False)]
156 if dose is not None:
157 m = m[pd.to_numeric(m[dose_c], errors="coerce") == dose]
158 if temp is not None:
159 m = m[pd.to_numeric(m[temp_c], errors="coerce") == temp]
160 return m.dropna(subset=[bin_c, freq_c])
161
162
163def _xywh(df, bin_c, bw_c, freq_c):
164 x = pd.to_numeric(df[bin_c], errors="coerce").values
165 w = pd.to_numeric(df[bw_c], errors="coerce").values
166 y = pd.to_numeric(df[freq_c], errors="coerce").values
167 ok = ~(np.isnan(x) | np.isnan(y))
168 return x[ok], w[ok], y[ok]
169
170
172 xlsx="Ferritics_Irradiated_Microstructure_Data.xlsx",
173 outdir="figures",
174 save=True,
175 show=False,
176 formats=("pdf", "png"),
177 include_overview=True,
178 # graphics controls — single font_size drives all text
179 font_size=16,
180 font_family="serif",
181 font_serif=("Times New Roman", "DejaVu Serif"),
182 axes_linewidth=1.2,
183 dpi=150,
184 save_dpi=300,
185 cite_fontsize=None, # defaults to font_size - 4
186 figsize=(8, 5),
187):
188 """
189 Build one figure per condition (loop and void size distributions).
190
191 Empty datasets are skipped. Returns dict{stem -> Figure} of figures
192 actually produced.
193
194 Set `include_overview=False` to skip the mean-loop-size-vs-T scatter.
195 """
196 if cite_fontsize is None:
197 cite_fontsize = max(7, font_size - 4)
198
199 _apply_style(font_family, font_serif, font_size, axes_linewidth, dpi, save_dpi)
200
201 if save:
202 os.makedirs(outdir, exist_ok=True)
203
204 loops = _load_bins(xlsx, "Loop Bins")
205 voids = _load_bins(xlsx, "Void Bins")
206
207 def col(df, partial):
208 matches = [c for c in df.columns if partial.lower() in c.lower()]
209 return matches[0] if matches else None
210
211 l_mat, l_type, l_dose, l_temp = (col(loops, k) for k in
212 ("Material", "Irrad. Type", "Dose", "Temp"))
213 l_bin, l_bw, l_freq = (col(loops, k) for k in
214 ("Bin Centre", "Bin Width", "Rel. Frequency"))
215
216 v_mat, v_type, v_dose, v_temp = (col(voids, k) for k in
217 ("Material", "Irrad. Type", "Dose", "Temp"))
218 v_bin, v_bw, v_freq = (col(voids, k) for k in
219 ("Bin Centre", "Bin Width", "Rel. Frequency"))
220
221 out = {}
222 skipped = []
223
224 # ── Loop distributions (one figure per condition) ───────────────────────
225 for case in LOOP_CASES:
226 d = _filter(loops, l_mat, l_type, l_dose, l_temp, l_bin, l_freq,
227 material=case["material"], irr_type=case.get("irr_type"),
228 dose=case.get("dose"), temp=case.get("temp"))
229 x, w, y = _xywh(d, l_bin, l_bw, l_freq)
230 if x.size == 0:
231 skipped.append(("loop", case["title"]))
232 continue
233
234 stem = "loop_" + _slug(case["title"])
235 fig, ax = plt.subplots(figsize=figsize)
236 ax.bar(x, y, width=w * 0.85, color=case["color"],
237 alpha=0.85, edgecolor="white", linewidth=0.6)
238 if case.get("mean") is not None:
239 ax.axvline(case["mean"], color=case["color"], lw=1.8, ls="--",
240 alpha=0.9, label=f"Mean = {case['mean']:.1f} nm")
241 ax.legend(frameon=False)
242 ax.set_xlabel("Loop diameter (nm)")
243 ax.set_ylabel("Relative frequency (%)")
244 _set_title(ax, case["title"], fontweight="bold")
245 ax.set_xlim(*case["xlim"])
246 _annotate_cite(ax, case["citation"], cite_fontsize)
247 fig.tight_layout()
248 out[stem] = fig
249
250 # ── Void distributions (one figure per condition) ───────────────────────
251 for case in VOID_CASES:
252 d = _filter(voids, v_mat, v_type, v_dose, v_temp, v_bin, v_freq,
253 material=case["material"], irr_type=case.get("irr_type"),
254 dose=case.get("dose"), temp=case.get("temp"))
255 x, w, y = _xywh(d, v_bin, v_bw, v_freq)
256 if x.size == 0:
257 skipped.append(("void", case["title"]))
258 continue
259
260 stem = "void_" + _slug(case["title"])
261 fig, ax = plt.subplots(figsize=figsize)
262 if case.get("bimodal"):
263 small = x < 2.2
264 large = ~small
265 ax.bar(x[small], y[small], width=w[small] * 0.85,
266 color="#2E86C1", alpha=0.85, edgecolor="white", lw=0.6,
267 label="He-bubbles ($r < r_c$)")
268 ax.bar(x[large], y[large], width=w[large] * 0.85,
269 color=case["color"], alpha=0.85, edgecolor="white", lw=0.6,
270 label="Growing voids ($r > r_c$)")
271 ax.axvline(2.0, color="gray", lw=1.2, ls=":", alpha=0.8,
272 label="Approx. critical radius")
273 ax.legend(frameon=False)
274 else:
275 ax.bar(x, y, width=w * 0.85, color=case["color"],
276 alpha=0.85, edgecolor="white", linewidth=0.6)
277 ax.set_xlabel("Cavity diameter (nm)")
278 ax.set_ylabel("Relative frequency (%)")
279 _set_title(ax, case["title"], fontweight="bold")
280 ax.set_xlim(*case["xlim"])
281 _annotate_cite(ax, case["citation"], cite_fontsize)
282 fig.tight_layout()
283 out[stem] = fig
284
285 # ── Overview scatter (kept as one figure) ───────────────────────────────
286 if include_overview:
287 try:
288 summary_df = pd.read_excel(xlsx, sheet_name="Loop Size Distributions", header=3)
289 summary_df.columns = [str(c).strip() for c in summary_df.columns]
290 summary_df = summary_df.dropna(how="all")
291
292 mean_col = [c for c in summary_df.columns if "Mean Diam" in c][0]
293 temp_col2 = [c for c in summary_df.columns if "Temp" in c][0]
294 mat_col2 = [c for c in summary_df.columns if "Material" in c][0]
295 type_col2 = [c for c in summary_df.columns if "Irrad. Type" in c][0]
296
297 fig, ax = plt.subplots(figsize=(figsize[0] + 2, figsize[1] + 0.5))
298 materials = ["EUROFER97", "T91", "HT9", "Pure Fe", "Fe-9Cr"]
299 markers = ["o", "s", "^", "D", "v"]
300 cols_m = ["#1B4F8A", "#117A65", "#922B21", "#D4AC0D", "#6C3483"]
301
302 plotted_any = False
303 for mat, mk, c in zip(materials, markers, cols_m):
304 sub = summary_df[summary_df[mat_col2].astype(str).str.contains(mat, case=False)]
305 for _, row in sub.iterrows():
306 try:
307 t = float(str(row[temp_col2]).replace("~", ""))
308 m_raw = str(row[mean_col]).replace("~", "").replace("+", "").split()[0]
309 m = float(m_raw)
310 is_ion = "ion" in str(row[type_col2]).lower()
311 fc = "none" if is_ion else c
312 ax.scatter(t, m, marker=mk, color=c, facecolors=fc,
313 s=80, zorder=5, linewidths=1.5)
314 plotted_any = True
315 except Exception:
316 pass
317
318 if plotted_any:
319 legend_patches = [plt.scatter([], [], marker=mk, color=c, s=70, label=mat)
320 for mat, mk, c in zip(materials, markers, cols_m)]
321 legend_patches += [
322 mpatches.Patch(facecolor="gray", edgecolor="gray", alpha=0.8,
323 label="Filled = neutron"),
324 mpatches.Patch(facecolor="none", edgecolor="gray", linewidth=1.5,
325 label="Open = ion"),
326 ]
327 ax.legend(handles=legend_patches, frameon=False, ncol=2, loc="upper left")
328 ax.set_xlabel("Irradiation temperature (°C)")
329 ax.set_ylabel("Mean loop diameter (nm)")
330 _set_title(ax, "Mean Dislocation Loop Diameter vs Irradiation Temperature",
331 fontweight="bold")
332 ax.set_xlim(50, 650); ax.set_ylim(0, 35)
333 ax.grid(True, ls=":", alpha=0.4)
334 fig.tight_layout()
335 out["overview_mean_loop_size_vs_temperature"] = fig
336 else:
337 plt.close(fig)
338 skipped.append(("overview", "mean loop size vs T (no parseable rows)"))
339 except Exception as e:
340 skipped.append(("overview", f"failed: {e}"))
341
342 if save:
343 for stem, fig in out.items():
344 _save(fig, outdir, stem, formats=formats)
345 print(f"Saved {len(out)} figure(s) to {os.path.abspath(outdir)}/")
346
347 if skipped:
348 print(f"Skipped {len(skipped)} figure(s) (no data):")
349 for kind, title in skipped:
350 print(f" [{kind}] {title}")
351
352 if not show:
353 for fig in out.values():
354 plt.close(fig)
355
356 return out
357
358
359def main():
360 parser = argparse.ArgumentParser()
361 parser.add_argument("--xlsx", default="Ferritics_Irradiated_Microstructure_Data.xlsx")
362 parser.add_argument("--outdir", default="figures")
363 parser.add_argument("--font-size", type=int, default=16)
364 args = parser.parse_args()
365 plot_size_distributions(xlsx=args.xlsx, outdir=args.outdir,
366 font_size=args.font_size, save=True, show=False)
367
368
369if __name__ == "__main__":
370 main()
_filter(df, mat_c, type_c, dose_c, temp_c, bin_c, freq_c, material=None, irr_type=None, dose=None, temp=None)
_apply_style(font_family, font_serif, font_size, axes_linewidth, dpi, save_dpi)
_xywh(df, bin_c, bw_c, freq_c)
plot_size_distributions(xlsx="Ferritics_Irradiated_Microstructure_Data.xlsx", outdir="figures", save=True, show=False, formats=("pdf", "png"), include_overview=True, font_size=16, font_family="serif", font_serif=("Times New Roman", "DejaVu Serif"), axes_linewidth=1.2, dpi=150, save_dpi=300, cite_fontsize=None, figsize=(8, 5))
_annotate_cite(ax, text, fontsize)
_save(fig, outdir, stem, formats)