|
| 1 | +"""Run the mesh-refinement convergence study for every 3D MMS case. |
| 2 | +
|
| 3 | +Loops cases × nu × nx and writes per-(case, nu) text tables and convergence |
| 4 | +plots into the shared `results/` directory. Mirrors the 2D driver minus the |
| 5 | +plane-stress / plane-strain `dim` axis (3D has a single constitutive branch). |
| 6 | +""" |
| 7 | + |
| 8 | +import os |
| 9 | +import numpy as np |
| 10 | +import matplotlib.pyplot as plt |
| 11 | + |
| 12 | +from sinus_neumann import mms as sinus_neumann_mms |
| 13 | + |
| 14 | +from solid import ( |
| 15 | + RESULTS_DIR, |
| 16 | + load_params, |
| 17 | + solve_solid, |
| 18 | + element_hex, |
| 19 | +) |
| 20 | +from plot_utils import annotate_convergence_rates |
| 21 | + |
| 22 | + |
| 23 | +def convergence_study(elem_specs, mms, L, E, nu, nx_values): |
| 24 | + """ |
| 25 | + Run convergence study for each element type in elem_specs, write a |
| 26 | + per-(element) text table, and one shared plot with L²/H¹ for every |
| 27 | + element on the same axes. |
| 28 | +
|
| 29 | + elem_specs : list of dicts with keys 'elem', 'label', 'l2_style', 'h1_style' |
| 30 | + """ |
| 31 | + print(f"\n PoissonRatio = {nu}", flush=True) |
| 32 | + hdr = (f"{'nx':>5} | {'h':>10} | {'L2':>14} | {'rate_L2':>7} " |
| 33 | + f"| {'H1':>14} | {'rate_H1':>7}") |
| 34 | + |
| 35 | + plot_series, hs_ref = [], None |
| 36 | + for spec in elem_specs: |
| 37 | + elem, label = spec["elem"], spec["label"] |
| 38 | + tag = label.replace(" ", "_") |
| 39 | + stem = f"convergence_{mms.name}_{tag}_nu{nu}" |
| 40 | + |
| 41 | + print(f"\n── {label} {mms.name} nu={nu} ──\n{hdr}", flush=True) |
| 42 | + |
| 43 | + rows, hs, l2s, h1s = [], [], [], [] |
| 44 | + for k, nx in enumerate(nx_values): |
| 45 | + ny = nz = nx |
| 46 | + h = L / (nx - 1) |
| 47 | + sol = solve_solid(elem, mms, L, E, nu, nx, ny, nz) |
| 48 | + l2 = elem.compute_l2(sol, mms, L) |
| 49 | + h1 = elem.compute_h1(sol, mms, L) |
| 50 | + |
| 51 | + rate_l2 = (f"{np.log(l2 / l2s[-1]) / np.log(h / hs[-1]):.2f}" |
| 52 | + if k > 0 else "") |
| 53 | + rate_h1 = (f"{np.log(h1 / h1s[-1]) / np.log(h / hs[-1]):.2f}" |
| 54 | + if k > 0 else "") |
| 55 | + print(f"{nx:5d} | {h:10.4f} | {l2:14.6e} | {rate_l2:>7} " |
| 56 | + f"| {h1:14.6e} | {rate_h1:>7}", flush=True) |
| 57 | + rows.append({"nx": nx, "h": h, |
| 58 | + "L2": l2, "rate_L2": rate_l2, |
| 59 | + "H1": h1, "rate_H1": rate_h1}) |
| 60 | + hs.append(h); l2s.append(l2); h1s.append(h1) |
| 61 | + |
| 62 | + write_convergence_table(stem, rows) |
| 63 | + plot_series.append({"label": f"{label} L²", |
| 64 | + "errors": l2s, "style": spec["l2_style"]}) |
| 65 | + plot_series.append({"label": f"{label} H¹", |
| 66 | + "errors": h1s, "style": spec["h1_style"]}) |
| 67 | + hs_ref = hs |
| 68 | + |
| 69 | + title = f"Convergence — {mms.name} nu={nu}" |
| 70 | + plot_convergence(f"convergence_{mms.name}_nu{nu}", |
| 71 | + hs_ref, plot_series, title=title) |
| 72 | + |
| 73 | + |
| 74 | +def write_convergence_table(stem, rows): |
| 75 | + """ |
| 76 | + Write convergence table to results/<stem>.txt. |
| 77 | +
|
| 78 | + rows : list of dicts with keys 'nx', 'h', and one key per error column. |
| 79 | + Rate columns are strings (empty for the first row). |
| 80 | + """ |
| 81 | + os.makedirs(RESULTS_DIR, exist_ok=True) |
| 82 | + path = os.path.join(RESULTS_DIR, f"{stem}.txt") |
| 83 | + err_keys = [k for k in rows[0] if k not in ("nx", "h")] |
| 84 | + header = f"{'nx':>6} | {'h':>10}" + "".join(f" | {k:>16}" for k in err_keys) |
| 85 | + with open(path, "w") as f: |
| 86 | + f.write(header + "\n") |
| 87 | + f.write("-" * len(header) + "\n") |
| 88 | + for row in rows: |
| 89 | + line = f"{row['nx']:6d} | {row['h']:10.4f}" |
| 90 | + for k in err_keys: |
| 91 | + v = row[k] |
| 92 | + line += f" | {v:16.6e}" if isinstance(v, float) else f" | {v:>16}" |
| 93 | + f.write(line + "\n") |
| 94 | + |
| 95 | + |
| 96 | +def plot_convergence(stem, hs, series, title, ylabel="Error"): |
| 97 | + """ |
| 98 | + Save log-log convergence plot to results/<stem>.png. |
| 99 | +
|
| 100 | + series : list of {"label", "errors", "style"?} dicts |
| 101 | + Per-segment convergence rates are annotated above each line segment. |
| 102 | + """ |
| 103 | + os.makedirs(RESULTS_DIR, exist_ok=True) |
| 104 | + h_arr = np.array(hs) |
| 105 | + default = ["bo-", "rs--", "g^:", "m^-"] |
| 106 | + fig, ax = plt.subplots(figsize=(8, 5)) |
| 107 | + for i, s in enumerate(series): |
| 108 | + style = s.get("style", default[i % len(default)]) |
| 109 | + e_arr = np.array(s["errors"]) |
| 110 | + ax.loglog(h_arr, e_arr, style, label=s["label"], linewidth=2, markersize=7) |
| 111 | + annotate_convergence_rates(ax, h_arr, e_arr) |
| 112 | + ax.set_xlabel("h") |
| 113 | + ax.set_ylabel(ylabel) |
| 114 | + ax.set_title(title) |
| 115 | + ax.legend() |
| 116 | + ax.grid(True, alpha=0.3, which="both") |
| 117 | + fig.tight_layout() |
| 118 | + fig.savefig(os.path.join(RESULTS_DIR, f"{stem}.png"), dpi=150) |
| 119 | + plt.close(fig) |
| 120 | + |
| 121 | + |
| 122 | +if __name__ == "__main__": |
| 123 | + cfg = load_params() |
| 124 | + L = cfg["length"] |
| 125 | + E = cfg["youngModulus"] |
| 126 | + conv = cfg["convergence"] |
| 127 | + |
| 128 | + specs = [ |
| 129 | + {"elem": element_hex, "label": "Q1 hex", |
| 130 | + "l2_style": "bo-", "h1_style": "rs--"}, |
| 131 | + ] |
| 132 | + |
| 133 | + for mms in (sinus_neumann_mms,): |
| 134 | + nx_vals = conv["nx_values"][mms.name] |
| 135 | + print(f"\n══ {mms.name} ══") |
| 136 | + for nu in conv["nu_values"]: |
| 137 | + convergence_study(specs, mms, L, E, nu, nx_vals) |
0 commit comments