2loop_burgers_fraction.py
3Extracts and plots the fraction of ½⟨111⟩ dislocation loops vs temperature
4and irradiation dose from Ferritics_Irradiated_Microstructure_Data.xlsx.
6Two data sources are used:
7 1. The `Dominant Burgers Vector` column of the
8 `N – Dislocation Loops` and `Ion – Dislocation Loops` sheets.
9 Free-text descriptions are parsed into numeric f₁₁₁ ∈ [0, 1].
10 2. The `111 Fraction` sheet, which contains size-binned histograms.
11 For conditions where both ⟨100⟩ and ½⟨111⟩ histograms are present
12 (e.g. 350°C / 16 dpa), an integrated bulk fraction is computed.
14Programmatic use (Jupyter):
15 from loop_burgers_fraction import plot_loop_111_fraction
16 figs, df = plot_loop_111_fraction(
17 xlsx="../../input/Ferritics_Irradiated_Microstructure_Data.xlsx",
18 outdir="../../output/microstructure",
19 font_size=16, save=True, show=True,
29import matplotlib.pyplot
as plt
31warnings.filterwarnings(
"ignore")
44 """Axes (or panel) title, drawn only when SHOW_TITLES is on."""
46 ax.set_title(text, **kw)
50 return any(n
in s
for n
in needles)
53 if bv_str
is None or (isinstance(bv_str, float)
and np.isnan(bv_str)):
55 s = str(bv_str).strip()
59 m = re.search(
r"~?(\d{1,3})\s*%\s*(?:⟨|<|a)?\s*1?00", s_low)
61 f100 = float(m.group(1)) / 100.0
62 return max(0.0, min(1.0, 1.0 - f100))
63 m = re.search(
r"~?(\d{1,3})\s*%\s*(?:⟨|<|a)?\s*111", s_low)
65 return max(0.0, min(1.0, float(m.group(1)) / 100.0))
69 if "→" in s
and "111" in s
and "100" in s:
72 t_lo_match = re.search(
r"≤\s*(\d{2,4})\s*°?c", s_low)
73 t_hi_match = re.search(
r"≥\s*(\d{2,4})\s*°?c", s_low)
74 t_lo = float(t_lo_match.group(1))
if t_lo_match
else 300.0
75 t_hi = float(t_hi_match.group(1))
if t_hi_match
else 350.0
76 if temp_c <= t_lo:
return 1.0
77 if temp_c >= t_hi:
return 0.0
78 return float((t_hi - temp_c) / max(1.0, (t_hi - t_lo)))
82 if (
"dpa" in s_low)
and (
"<" in s_low
or ">" in s_low)
and "111" in s_low
and "100" in s_low:
85 d_match = re.search(
r"<\s*(\d+(?:\.\d+)?)\s*dpa", s_low)
86 d_thr = float(d_match.group(1))
if d_match
else 4.0
87 return 1.0
if dose_dpa <= d_thr
else 0.15
89 has_111 =
"111" in s_low
90 has_100 =
"100" in s_low
93 if has_111
and not has_100:
95 if has_100
and not has_111:
99 if has_111
and has_100:
102 m100_dom = bool(re.search(
r"100[^\.;]*?\bdom", s_low))
103 m111_dom = bool(re.search(
r"111[^\.;]*?\bdom", s_low))
104 if m100_dom
and not m111_dom:
return 0.15
105 if m111_dom
and not m100_dom:
return 0.85
112def extract_f111_table(xlsx):
115 for sheet, irr_kind
in ((
"N – Dislocation Loops",
"Neutron"),
116 (
"Ion – Dislocation Loops",
"Ion")):
117 df = pd.read_excel(xlsx, sheet_name=sheet, header=[1, 2])
118 df.columns = [str(c2).replace(
"\n",
" ").strip()
for (_, c2)
in df.columns]
119 for _, r
in df.iterrows():
120 mat = r.get(
"Material")
123 T = pd.to_numeric(str(r.get(
"Temp. (°C)",
"")).split(
"–")[0].strip(), errors=
"coerce")
124 D = pd.to_numeric(str(r.get(
"Dose (dpa)",
"")).split(
"–")[0].strip(), errors=
"coerce")
126 temp_c=
None if pd.isna(T)
else float(T),
127 dose_dpa=
None if pd.isna(D)
else float(D))
128 if f
is None or pd.isna(T):
130 rows.append(dict(material=str(mat).strip(),
131 irradiation=irr_kind,
132 dose_dpa=float(D)
if not pd.isna(D)
else np.nan,
133 temperature_C=float(T),
136 raw_bv=str(r.get(
"Dominant Burgers Vector"))))
141 raw = pd.read_excel(xlsx, sheet_name=
"111 Fraction", header=
None)
143 labels = [str(raw.iat[0, j])
if pd.notna(raw.iat[0, j])
else "" for j
in range(raw.shape[1])]
146 for j, lab
in enumerate(labels):
147 if lab
and lab.lower() !=
"nan":
148 blocks[lab] = (j, j + 1)
151 for lab, (jd, jf)
in blocks.items():
152 vals = pd.to_numeric(raw.iloc[2:, jf], errors=
"coerce").dropna().values
153 block_sum[lab] = vals.sum()
155 if "350C_16dpa_100" in block_sum
and "350C_16dpa_111" in block_sum:
156 s100 = block_sum[
"350C_16dpa_100"]
157 s111 = block_sum[
"350C_16dpa_111"]
158 f = s111 / (s100 + s111)
if (s100 + s111) > 0
else np.nan
159 rows.append(dict(material=
"EUROFER97 (111 sheet)",
160 irradiation=
"Neutron",
164 source=
"111 Fraction (integrated)",
165 raw_bv=
"histogram-derived"))
169 out = pd.DataFrame(rows)
174def _apply_style(font_size, font_family, font_serif, axes_linewidth, dpi, save_dpi):
175 matplotlib.rcParams.update({
176 "font.family": font_family,
177 "font.serif": list(font_serif),
178 "font.size": font_size,
179 "axes.labelsize": font_size,
180 "axes.titlesize": font_size,
181 "legend.fontsize": font_size,
182 "xtick.labelsize": font_size,
183 "ytick.labelsize": font_size,
184 "axes.linewidth": axes_linewidth,
185 "axes.spines.top":
False,
186 "axes.spines.right":
False,
188 "savefig.dpi": save_dpi,
189 "savefig.bbox":
"tight",
194 """Coefficient of determination for a least-squares linear fit."""
195 x, y = np.asarray(x, float), np.asarray(y, float)
196 ok = ~(np.isnan(x) | np.isnan(y))
198 if x.size < 3
or np.std(x) == 0:
200 a, b = np.polyfit(x, y, 1)
202 ss_res = np.sum((y - yhat) ** 2)
203 ss_tot = np.sum((y - np.mean(y)) ** 2)
204 return 0.0
if ss_tot == 0
else 1.0 - ss_res / ss_tot
208 """|Spearman ρ| — captures monotonic (incl. nonlinear) dependence; better
209 than R² when the underlying relationship is sigmoidal or step-like."""
210 x, y = np.asarray(x, float), np.asarray(y, float)
211 ok = ~(np.isnan(x) | np.isnan(y))
215 rx = pd.Series(x).rank().values
216 ry = pd.Series(y).rank().values
217 if np.std(rx) == 0
or np.std(ry) == 0:
219 return float(abs(np.corrcoef(rx, ry)[0, 1]))
222def _scatter(ax, df, xkey, xlabel, color_by="material", marker_by="irradiation"):
223 materials = sorted(df[
"material"].unique())
224 cmap = plt.get_cmap(
"tab10")
225 color_map = {m: cmap(i % 10)
for i, m
in enumerate(materials)}
226 marker_map = {
"Neutron":
"o",
"Ion":
"^"}
228 for mat
in materials:
229 sub = df[df[
"material"] == mat]
230 for irr, mk
in marker_map.items():
231 ssub = sub[sub[
"irradiation"] == irr]
234 ax.scatter(ssub[xkey], 100.0 * ssub[
"f_111"],
235 color=color_map[mat], marker=mk,
236 s=110, edgecolors=
"black", linewidths=0.6,
237 alpha=0.85, label=f
"{mat} ({irr})")
238 ax.set_xlabel(xlabel)
239 ax.set_ylabel(
"Fraction of ½⟨111⟩ loops (%)")
241 ax.grid(
True, ls=
":", alpha=0.4)
245 xlsx="Ferritics_Irradiated_Microstructure_Data.xlsx",
249 formats=(
"pdf",
"png"),
254 font_serif=(
"Times New Roman",
"DejaVu Serif"),
261 Build figures of f_111 vs (T, dose).
264 - Compute |Spearman ρ| for f_111 vs T and vs dose (rank-based;
265 captures monotonic non-linear trends — e.g. the sigmoidal
266 ½⟨111⟩→⟨100⟩ transition with T).
267 - If both > weak_threshold, plot both.
268 - If only one is, plot only that one (the weak axis is omitted,
269 per the user's spec).
270 - If neither is, plot both anyway as a diagnostic.
271 - `force` overrides the auto choice.
273 Returns (figs_dict, dataframe).
275 _apply_style(font_size, font_family, font_serif, axes_linewidth, dpi, save_dpi)
277 os.makedirs(outdir, exist_ok=
True)
279 df = extract_f111_table(xlsx)
281 print(
"No parseable f_111 data found.")
286 r2_T =
_r2(df[
"temperature_C"], df[
"f_111"])
287 r2_dose =
_r2(df[
"dose_dpa"], df[
"f_111"])
288 print(f
"Parsed {len(df)} data points")
289 print(f
" |Spearman ρ|(T) = {rho_T:.3f} (linear R² = {r2_T:.3f})")
290 print(f
" |Spearman ρ|(dose) = {rho_dose:.3f} (linear R² = {r2_dose:.3f})")
291 print(f
" weak threshold = {weak_threshold}")
295 elif force ==
"dose":
297 elif force ==
"both":
298 which = (
"T",
"dose")
300 T_strong = rho_T >= weak_threshold
301 dose_strong = rho_dose >= weak_threshold
302 if T_strong
and dose_strong:
303 which = (
"T",
"dose")
309 print(
" → both dependences look weak; plotting both as a diagnostic.")
310 which = (
"T",
"dose")
315 fig, ax = plt.subplots(figsize=figsize)
316 _scatter(ax, df,
"temperature_C",
"Irradiation temperature (°C)")
317 _set_title(ax, f
"Fraction of ½⟨111⟩ loops vs Temperature "
318 f
"(|ρ| = {rho_T:.2f})", fontweight=
"bold")
319 ax.legend(frameon=
False, loc=
"center left", bbox_to_anchor=(1.01, 0.5),
320 fontsize=max(8, font_size - 4))
322 out[
"loop_111_fraction_vs_temperature"] = fig
325 fig, ax = plt.subplots(figsize=figsize)
326 _scatter(ax, df,
"dose_dpa",
"Dose (dpa)")
328 _set_title(ax, f
"Fraction of ½⟨111⟩ loops vs Dose "
329 f
"(|ρ| = {rho_dose:.2f})", fontweight=
"bold")
330 ax.legend(frameon=
False, loc=
"center left", bbox_to_anchor=(1.01, 0.5),
331 fontsize=max(8, font_size - 4))
333 out[
"loop_111_fraction_vs_dose"] = fig
336 for stem, fig
in out.items():
338 fig.savefig(os.path.join(outdir, f
"{stem}.{ext}"))
339 print(f
"Saved {len(out)} figure(s) to {os.path.abspath(outdir)}/")
342 for fig
in out.values():
348if __name__ ==
"__main__":
350 p = argparse.ArgumentParser()
351 p.add_argument(
"--xlsx", default=
"Ferritics_Irradiated_Microstructure_Data.xlsx")
352 p.add_argument(
"--outdir", default=
"figures")
353 p.add_argument(
"--font-size", type=int, default=16)
356 font_size=a.font_size, save=
True, show=
False)
plot_loop_111_fraction(xlsx="Ferritics_Irradiated_Microstructure_Data.xlsx", outdir="figures", save=True, show=False, formats=("pdf", "png"), weak_threshold=0.20, force=None, font_size=16, font_family="serif", font_serif=("Times New Roman", "DejaVu Serif"), axes_linewidth=1.2, dpi=150, save_dpi=300, figsize=(10, 6))
_apply_style(font_size, font_family, font_serif, axes_linewidth, dpi, save_dpi)
_scatter(ax, df, xkey, xlabel, color_by="material", marker_by="irradiation")
_set_title(ax, text, **kw)
parse_burgers_fraction(bv_str, temp_c=None, dose_dpa=None)