Skip to content

Commit 82d3b58

Browse files
committed
Refactor 3D just like 1D/2D
- manufactured_solution.py - solid_solution.py - solid.py - sinus_neumann.py - run_convergence.py - params.json
1 parent 5f83ab7 commit 82d3b58

8 files changed

Lines changed: 810 additions & 738 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Abstract base class for 3D MMS cases (Cartesian domain [0,L]^3)."""
2+
3+
from abc import ABC, abstractmethod
4+
import numpy as np
5+
6+
7+
def lame(E, nu):
8+
"""Lamé parameters (lambda, mu) for 3D linear elasticity (full Hooke)."""
9+
lam = E * nu / ((1.0 + nu) * (1.0 - 2.0 * nu))
10+
mu = E / (2.0 * (1.0 + nu))
11+
return lam, mu
12+
13+
14+
class MMSCase3D(ABC):
15+
name = None # case identifier (must match the params.json key)
16+
plot_label = None # LaTeX label for the exact solution
17+
18+
# Body-force quadrature rule — must be set by each concrete case.
19+
# No framework fallback; assembly raises if unset.
20+
source_quadrature_hex = None # element rule for Q1 hexes (e.g. hex_q1_rule(2))
21+
22+
@abstractmethod
23+
def u_ex(self, x, y, z, L):
24+
"""Exact solution: returns (ux, uy, uz)."""
25+
26+
@abstractmethod
27+
def grad_u_ex(self, x, y, z, L):
28+
"""Exact 3x3 displacement gradient
29+
[[dux/dx, dux/dy, dux/dz],
30+
[duy/dx, duy/dy, duy/dz],
31+
[duz/dx, duz/dy, duz/dz]]."""
32+
33+
@abstractmethod
34+
def source(self, x, y, z, E, nu, L):
35+
"""Body force: returns (fx, fy, fz)."""
36+
37+
@abstractmethod
38+
def apply_bcs(self, Solid, nodes_3d, L):
39+
"""Install Dirichlet constraints on the SOFA `Solid` node.
40+
41+
The MMS author chooses the BC pattern matching the manufactured
42+
solution. Neumann tractions on the six faces are already assembled
43+
into the consistent nodal force by the framework (via `self.traction`);
44+
this method only needs to add SOFA constraint objects.
45+
"""
46+
47+
def traction(self, x, y, z, nx, ny, nz, E, nu, L):
48+
"""Traction on a face with outward unit normal (nx, ny, nz):
49+
returns (Tx, Ty, Tz).
50+
51+
Derived from `grad_u_ex` via sigma = lambda tr(eps) I + 2 mu eps and
52+
T = sigma . n.
53+
"""
54+
lam, mu = lame(E, nu)
55+
G = self.grad_u_ex(x, y, z, L)
56+
eps = 0.5 * (G + G.T)
57+
tr = eps[0, 0] + eps[1, 1] + eps[2, 2]
58+
S = lam * tr * np.eye(3) + 2.0 * mu * eps
59+
T = S @ np.array([nx, ny, nz])
60+
return T[0], T[1], T[2]
Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
{
22
"length": 1.0,
33
"youngModulus": 1e6,
4-
"RatioPoisson": 0.3,
5-
"nx": 5,
6-
"ny": 5,
7-
"nz": 5,
8-
"nxConvergence": {
9-
"linear_neumann": [5, 15, 26, 36, 46, 56, 66, 76, 86],
10-
"sinus_neumann": [5, 15, 26, 36, 46, 56, 66, 76, 86],
11-
"sinus_dirichlet": [5, 15, 26, 36, 46, 56, 66, 76, 86]
4+
"reference": {
5+
"nx": 6,
6+
"nu": 0.3
7+
},
8+
"convergence": {
9+
"nu_values": [0.0, 0.3, 0.49],
10+
"nx_values": {
11+
"sinus_neumann": [4, 6, 8, 12, 16]
12+
}
1213
}
1314
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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)

examples/Freefem/MMS/3D/sin.py

Lines changed: 0 additions & 71 deletions
This file was deleted.

0 commit comments

Comments
 (0)