Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
20c2cbe
Restore timestep=dt in run_ve_vep_oscillatory_plot.py
lmoresi Apr 24, 2026
c09f628
Fix infinite recursion in MathematicalMixin.__getattr__ for sym
lmoresi Apr 24, 2026
ee66737
Add divergence_retries kwarg to all SNES solvers (Picard-style rescue)
lmoresi Apr 25, 2026
5f53582
Add scheduled-dt VEP benchmark (negative result on BDF-1 restart hypo…
lmoresi Apr 25, 2026
f37c400
Add diagnostic probes that locate VEP variable-dt projection drift bug
lmoresi Apr 25, 2026
5b3548c
Fix VEP yield-surface drift under variable dt (snapshot projection)
lmoresi Apr 25, 2026
beb4f2e
Refactor VEP projection fix: move snapshot machinery into SemiLagrang…
lmoresi Apr 25, 2026
6e0832f
psi_snapshot: copy via .data (skip unit conv), note transient-var fol…
lmoresi Apr 25, 2026
377511a
Retire bdf_blend (BDF order blending) machinery from VEP and TI-VEP
lmoresi Apr 25, 2026
a209096
Add VEP stability regression tests for yield-lock and dt-change drift
lmoresi Apr 25, 2026
b5a5d82
Clean up bdf_blend references after retirement
lmoresi Apr 26, 2026
59fc5b2
Add TI-VEP yield-lock regression test (snapshot fix inheritance)
lmoresi Apr 26, 2026
6e740f3
Replace VE/VEP benchmarks with proper run/log/plot suite
lmoresi Apr 26, 2026
eb8efa5
Benchmarks: save BDF-1 + BDF-2 traces + add convergence sweep
lmoresi Apr 26, 2026
ddf0329
Benchmark docs: explain BDF-1 vs BDF-2 results + reference convergenc…
lmoresi Apr 26, 2026
d12c124
Add check_saved_data.py — verify benchmark npz files are self-contained
lmoresi Apr 26, 2026
e25dc20
Benchmark index: document the run/log/replot workflow + convergence s…
lmoresi Apr 26, 2026
730fccb
Harmonic benchmark: switch V_top to endpoint sampling, BDF-2 now slope-2
lmoresi Apr 26, 2026
e3045b2
Harmonic benchmark: peak-start initial condition (no startup transient)
lmoresi Apr 26, 2026
d37da66
Fix harmonic plot: use cos analytical to match peak-start bench
lmoresi Apr 26, 2026
d65e8fc
Harmonic peak-start: actually get the benefit (proper ψ* + skip BDF r…
lmoresi Apr 26, 2026
d38128d
Variable-dt square-wave benchmarks (10× reduced dt around BC flips)
lmoresi Apr 26, 2026
28771a3
VEP variable-dt benchmark with smooth yield mode
lmoresi Apr 26, 2026
ed10036
Variable-dt VEP softmin benchmark + fine-dt windows shaded on plots
lmoresi Apr 26, 2026
5e6cd0c
Retire smooth_yield mode — under-clipped by ~50%, no compensating ben…
lmoresi Apr 26, 2026
1cd9ed0
DDt.set_initial_history: public API for BDF restart / analytical IC
lmoresi Apr 26, 2026
aed517f
CHANGELOG: DDt.set_initial_history entry
lmoresi Apr 26, 2026
b0fd7bb
vardt-square doc: embed softmin and smooth (retired) figures
lmoresi Apr 26, 2026
ca2442a
SaddlePt: optional F1_jacobian_source for inexact Newton
lmoresi Apr 26, 2026
3eb320f
Revive bdf_blend on TI-VEP path; default cp linesearch on F1_jac over…
lmoresi Apr 26, 2026
a66d47f
Fix TI-VEP BDF-2 instability: bdf_blend default 0.10, allow α=0
lmoresi Apr 27, 2026
c5c69c4
TI-VEP: revert bdf_blend default to 1.0 — order=1 is the right answer
lmoresi Apr 27, 2026
983a9a3
TI-VEP harmonic plot + γ̇_0 BC factor fix
lmoresi Apr 27, 2026
4772ad1
Phase A: design doc + 1D Maxwell exponential-integrator validator
lmoresi Apr 27, 2026
26855ef
Phase B numerical eval + DDt generalisation sketch
lmoresi Apr 27, 2026
88b87e9
Phase B: square-wave VE/VEP eval (corrected analytical)
lmoresi Apr 27, 2026
6a36415
Phase B UW3 jury-rig: hit propagation issue, design doc updated
lmoresi Apr 27, 2026
d26a36e
Phase B: variable-Δt square-wave eval (1D)
lmoresi Apr 27, 2026
725f609
Phase B vardt: fix fine-zone entry clamp
lmoresi Apr 27, 2026
0fe31cb
Finalise exponential-integrator implementation plan
lmoresi Apr 27, 2026
b6b0e7a
Phase B: extend SemiLagrangian DDt with ETD-2 exponential integrator
lmoresi Apr 28, 2026
06d1777
Phase B: add MaxwellExponentialFlowModel + uniform history dispatch
lmoresi Apr 28, 2026
d6b2a8e
Phase B: validation scripts for MaxwellExponentialFlowModel
lmoresi Apr 28, 2026
06e8157
Phase B: add TransverseIsotropicMaxwellExponentialFlowModel + killer …
lmoresi Apr 28, 2026
bb04e5a
Phase B: revert exp model to yield-in-residual + audit unit handling
lmoresi Apr 28, 2026
3ebce78
Phase B: lagged-τ from actual stress in exp models + per-node yield gate
lmoresi Apr 28, 2026
2b599ad
Phase B: revert lagged-τ experiment, document parity with BDF-1
lmoresi Apr 28, 2026
c067c7b
Phase B Task 6: collapse Maxwell-exp siblings into integrator='bdf'|'…
lmoresi Apr 28, 2026
d589cb0
EXPONENTIAL_VE_INTEGRATOR.md: fill in Phase B results + lessons learned
lmoresi Apr 28, 2026
13d44de
Promote VE_Stokes deprecation to runtime warning + migrate production…
lmoresi Apr 28, 2026
156c9cd
Migrate remaining VE_Stokes callsites in scratch/dev scripts
lmoresi Apr 28, 2026
1a79c6f
Phase B: visualisation scripts + SOLVER_UNIFICATION_DESIGN status update
lmoresi Apr 28, 2026
b7c4b18
Phase B PyVista demo: replace velocity-magnitude heatmap with u_y
lmoresi Apr 28, 2026
a47ef2e
EXPONENTIAL_VE_INTEGRATOR.md: document τ_y/A_∞ ≥ ~0.5 range of validity
lmoresi Apr 28, 2026
1cd69f5
EXPONENTIAL_VE_INTEGRATOR.md: BDF-1 is fine at τ_y=0.05; runaway is E…
lmoresi Apr 28, 2026
417710b
Phase B: BDF-1 vs ETD-2 trajectory comparison at τ_y=0.05
lmoresi Apr 28, 2026
c99d411
Phase D 1D cleanroom bench: per-component ETD-2 vs lumped variants
lmoresi Apr 28, 2026
3531736
Phase D: TransverseIsotropicVEPSplitFlowModel — per-component ETD-2
lmoresi Apr 28, 2026
5b1f01e
Phase D: lag η_∥_eff via forcing_star (fixes per-quad over-damping)
lmoresi Apr 29, 2026
2931a5c
Phase D: explicit-parallel split-ETD respects yield surface like BDF-1
lmoresi Apr 29, 2026
eca4f0b
Phase D: u_y trajectory diagnosis — split-ETD slips at boundary rate
lmoresi Apr 29, 2026
fd55fc4
Phase D: τ_∥ soft-cap (recommendation #4) — partial trade
lmoresi Apr 29, 2026
ec4d6b5
EXPONENTIAL_VE_INTEGRATOR.md: fold in Phase D findings
lmoresi Apr 29, 2026
a3805e5
Phase E: hybrid BDF/ETD with spatial fault weight — also drifts
lmoresi Apr 29, 2026
0f3b6ab
EXPONENTIAL_VE_INTEGRATOR.md: investigation closed
lmoresi Apr 29, 2026
c7f1fef
Phase D and E: mark experimental, point to investigation doc
lmoresi Apr 29, 2026
425611c
Phase B: BDF-2 trajectory at τ_y=0.05 — original blow-up captured
lmoresi Apr 29, 2026
d513a2f
Track the BDF-2 per-step trace (rename .log → .trace.txt)
lmoresi Apr 29, 2026
d5d4edb
ETD-1 (first-order) integrator — matches BDF-1 on the killer test
lmoresi Apr 29, 2026
a1afc0c
EXPONENTIAL_VE_INTEGRATOR.md: ETD-1 ships as recommended default
lmoresi Apr 29, 2026
341bcd6
ETD API: unify ETD-1/ETD-2 behind integrator='etd' + order parameter
lmoresi Apr 29, 2026
e31993a
Phase F: predictor-corrector with radial return — first cut
lmoresi Apr 29, 2026
663255e
Add VEP two-Stokes operator-split investigation plan
lmoresi Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
306 changes: 306 additions & 0 deletions docs/advanced/benchmarks/_bench_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
"""Shared helpers for the VE/VEP analytical benchmark suite.

Three benchmark cases share this module:

* ``bench_ve_harmonic.py`` — Maxwell shear under :math:`V_{top}(t) = V_0 \\sin(\\omega t)`
* ``bench_ve_square.py`` — Maxwell shear under square-wave :math:`V_{top}`
* ``bench_vep_square.py`` — same square-wave forcing with Min-mode plasticity

Common setup
------------
* Mesh: ``StructuredQuadBox`` 16×8 over ``(±1, ±0.5)``.
* Velocity at top/bottom: ``±V_top(t)``, free at left/right.
* Pure shear with strain rate ``γ̇ = 2·V_top/H = V_top``.
* Centre-point stress sample.
* Scaling: ``η = μ = 1``, so Maxwell relaxation time ``t_r = 1`` and the
steady-state VE stress under sustained shear is ``η·γ̇``.

Logging
-------
Each run writes a self-contained ``.npz`` to ``output/benchmarks/<name>.npz``
holding the simulation trace, the analytical reference, the parameter
dict, and metadata. Plotting is decoupled — see ``plot_benchmarks.py``.
"""

import os
import time
import numpy as np
import sympy
import underworld3 as uw
from underworld3.function import expression


_HERE = os.path.dirname(os.path.abspath(__file__))
_REPO_ROOT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
OUTPUT_DIR = os.path.join(_REPO_ROOT, "output", "benchmarks")
FIG_DIR = os.path.join(_REPO_ROOT, "docs", "advanced", "figures")


# ---------------------------------------------------------------------------
# Common parameters
# ---------------------------------------------------------------------------

DEFAULT_PARAMS = dict(
eta=1.0, # shear viscosity
mu=1.0, # shear modulus
H=1.0, # box height (top–bottom)
W=2.0, # box width (left–right)
elementRes=(16, 8),
velocity_degree=2,
pressure_degree=1,
bdf_order=2,
)


def t_relax(params):
return params["eta"] / params["mu"]


# ---------------------------------------------------------------------------
# Analytical solutions
# ---------------------------------------------------------------------------

def maxwell_oscillatory(t, eta, mu, gamma_dot_0, omega):
r"""Closed-form Maxwell shear stress under sinusoidal forcing
:math:`\dot\gamma(t) = \dot\gamma_0 \sin(\omega t)`.

Solving :math:`\dot\sigma + \sigma/t_r = \mu\dot\gamma` with
:math:`\sigma(0) = 0` gives

.. math::
\sigma(t) = \frac{\eta\dot\gamma_0}{1+\mathrm{De}^2}
\left[\sin(\omega t) - \mathrm{De}\cos(\omega t) + \mathrm{De}\,e^{-t/t_r}\right]

where :math:`\mathrm{De} = \omega t_r` is the Deborah number. After
transient decay (:math:`t \gg t_r`) the steady response has amplitude
:math:`\eta\dot\gamma_0/\sqrt{1+\mathrm{De}^2}` and phase lag
:math:`\varphi = \arctan(\mathrm{De})`.
"""
t_r = eta / mu
De = omega * t_r
pre = eta * gamma_dot_0 / (1.0 + De**2)
return pre * (np.sin(omega * t) - De * np.cos(omega * t) + De * np.exp(-t / t_r))


def maxwell_square_wave(t, eta, mu, gamma_dot_0, half_period):
r"""Closed-form Maxwell shear stress under square-wave forcing.

Within each half-period the stress relaxes exponentially toward the
steady-state value :math:`\pm\eta\dot\gamma_0` from the value at the
period boundary:

.. math::
\sigma(t) = s_n\sigma_{\mathrm{ss}} + (\sigma_{0,n} - s_n\sigma_{\mathrm{ss}})\,
e^{-(t - t_n)/t_r}

where :math:`s_n = (-1)^n` is the sign in half-period :math:`n` and
:math:`\sigma_{0,n}` is the stress at the start of that half-period.
"""
t_r = eta / mu
sigma_ss = eta * gamma_dot_0
out = np.zeros_like(np.asarray(t, dtype=float))
sigma_start = 0.0
for i, ti in enumerate(np.asarray(t, dtype=float)):
n = int(ti / half_period)
t_local = ti - n * half_period
# Replay periods 0..n-1 to find sigma at start of period n
sigma_n = 0.0
for j in range(n):
sign = 1.0 if j % 2 == 0 else -1.0
target = sign * sigma_ss
sigma_n = target + (sigma_n - target) * np.exp(-half_period / t_r)
sign = 1.0 if n % 2 == 0 else -1.0
target = sign * sigma_ss
out[i] = target + (sigma_n - target) * np.exp(-t_local / t_r)
return out


def vep_square_wave(t, eta, mu, gamma_dot_0, tau_y, half_period):
r"""Closed-form VEP shear stress under square-wave forcing with
Min-mode plasticity.

Within each half-period, the stress evolves under Maxwell:

.. math::
\sigma(t) = s_n\sigma_{\mathrm{ss}} + (\sigma_{0,n} - s_n\sigma_{\mathrm{ss}})\,
e^{-(t - t_n)/t_r}

until :math:`|\sigma| = \tau_y`, after which the plastic flow holds
:math:`\sigma = \pm\tau_y`. The next half-period starts from the
*clipped* value (``±τ_y`` if the previous period yielded; otherwise
the unclipped end value).

When :math:`\eta\dot\gamma_0 \le \tau_y` the solution coincides with
the unclipped Maxwell square-wave.
"""
t_arr = np.asarray(t, dtype=float)
t_r = eta / mu
sigma_ss = eta * gamma_dot_0
out = np.zeros_like(t_arr)

# Pre-compute σ at the start of each half-period (including clipping)
n_half_max = int(np.ceil(t_arr[-1] / half_period)) + 2
sigma_at_start = [0.0]
for n in range(n_half_max):
sign = 1.0 if n % 2 == 0 else -1.0
target = sign * sigma_ss
sigma_0 = sigma_at_start[-1]
sigma_end = target + (sigma_0 - target) * np.exp(-half_period / t_r)
# Clip to ±τ_y at the period boundary if the unclipped value would
# have exceeded the yield surface
sigma_end_clipped = np.clip(sigma_end, -tau_y, tau_y)
sigma_at_start.append(float(sigma_end_clipped))

# Evaluate at each requested t
for i, ti in enumerate(t_arr):
n = int(ti / half_period)
t_local = ti - n * half_period
sign = 1.0 if n % 2 == 0 else -1.0
target = sign * sigma_ss
sigma_0 = sigma_at_start[n]
sigma_unclipped = target + (sigma_0 - target) * np.exp(-t_local / t_r)
out[i] = np.clip(sigma_unclipped, -tau_y, tau_y)
return out


# ---------------------------------------------------------------------------
# Stokes problem builder
# ---------------------------------------------------------------------------

def build_stokes(label, params, yield_stress=None, yield_mode="min"):
"""Construct a VE_Stokes problem with the standard mesh / BCs.

Parameters
----------
label : str
Used to namespace the mesh variable names so multiple problems can
coexist in one Python session.
params : dict
Material parameters (see DEFAULT_PARAMS).
yield_stress : float or None
If ``None``, pure VE (yield_stress is set to a large finite value).
Otherwise enables VEP with the given yield stress.
yield_mode : str
Passed to ``constitutive_model._yield_mode``. ``"min"`` for
Min-mode plasticity (sharp yield), other options are smooth
approximations.

Returns
-------
mesh, stokes, V_top, params
``V_top`` is the user-facing UWexpression for the top BC velocity.
"""
p = dict(params)
mesh = uw.meshing.StructuredQuadBox(
elementRes=p["elementRes"],
minCoords=(-p["W"] / 2.0, -p["H"] / 2.0),
maxCoords=(p["W"] / 2.0, p["H"] / 2.0),
)
v = uw.discretisation.MeshVariable(f"U_{label}", mesh, mesh.dim, degree=p["velocity_degree"])
pp = uw.discretisation.MeshVariable(f"P_{label}", mesh, 1, degree=p["pressure_degree"])
stokes = uw.systems.Stokes(mesh, velocityField=v, pressureField=pp)
stokes.constitutive_model = uw.constitutive_models.ViscoElasticPlasticFlowModel(
stokes.Unknowns, order=p["bdf_order"],
)
stokes.constitutive_model.Parameters.shear_viscosity_0 = p["eta"]
stokes.constitutive_model.Parameters.shear_modulus = p["mu"]
stokes.constitutive_model.Parameters.yield_stress = (
yield_stress if yield_stress is not None else 1.0e6
)
stokes.constitutive_model.Parameters.strainrate_inv_II_min = 1.0e-6
stokes.constitutive_model._yield_mode = yield_mode

V_top = expression(rf"V_{{top,{label}}}", sympy.Float(0.0), "Top V")
stokes.add_dirichlet_bc((V_top, 0.0), "Top")
stokes.add_dirichlet_bc((-V_top, 0.0), "Bottom")
stokes.add_dirichlet_bc((sympy.oo, 0.0), "Left")
stokes.add_dirichlet_bc((sympy.oo, 0.0), "Right")
stokes.tolerance = 1.0e-6
stokes.petsc_options["snes_force_iteration"] = True

return mesh, stokes, V_top, p


# ---------------------------------------------------------------------------
# Per-step probe
# ---------------------------------------------------------------------------

def probe_centre(stokes, c=np.array([[0.0, 0.0]])):
return float(uw.function.evaluate(stokes.tau.sym[0, 1], c).flatten()[0])


# ---------------------------------------------------------------------------
# Self-contained npz logger
# ---------------------------------------------------------------------------

def save_run(name, *, params, params_extra=None, **arrays):
"""Save a benchmark run to ``output/benchmarks/<name>.npz``.

Parameters
----------
name : str
Output filename stem (no extension).
params : dict
Material/numerical parameters used for the run. Stored as a
single ``params`` field for re-creation/replotting.
params_extra : dict or None
Per-benchmark scalar metadata (omega, half_period, tau_y, …).
**arrays
Per-step arrays: times, sigma, sigma_ana, dt, gamma_dot, etc.
"""
os.makedirs(OUTPUT_DIR, exist_ok=True)
path = f"{OUTPUT_DIR}/{name}.npz"
payload = {f"arr_{k}": np.asarray(v) for k, v in arrays.items()}
payload["__params__"] = np.asarray(repr(dict(params)), dtype=object)
payload["__params_extra__"] = np.asarray(repr(dict(params_extra or {})), dtype=object)
payload["__keys__"] = np.asarray(list(arrays.keys()), dtype=object)
payload["__name__"] = np.asarray(name, dtype=object)
np.savez(path, **payload)
return path


def load_run(name):
"""Reverse of :func:`save_run`. Returns ``(arrays, params, extra)``."""
path = f"{OUTPUT_DIR}/{name}.npz"
with np.load(path, allow_pickle=True) as f:
keys = list(f["__keys__"])
arrays = {k: f[f"arr_{k}"] for k in keys}
params = eval(str(f["__params__"]))
extra = eval(str(f["__params_extra__"]))
return arrays, params, extra


# ---------------------------------------------------------------------------
# Error metrics
# ---------------------------------------------------------------------------

def error_metrics(sigma, sigma_ana):
"""Standard error report: max and rms absolute error."""
diff = sigma - sigma_ana
return dict(
max_abs=float(np.max(np.abs(diff))),
rms=float(np.sqrt(np.mean(diff**2))),
rel_max=float(np.max(np.abs(diff)) / (np.max(np.abs(sigma_ana)) + 1e-30)),
)


def fit_amp_phase(t, sigma, omega):
"""Least-squares fit of ``A·sin(ωt − φ)`` to ``sigma``.

Returns ``(A, phi)``. Drops the first ``2*t_r`` to skip the
transient (assumes ``t_r = 1`` and that the array is long enough).
"""
mask = t > 4.0 # skip ~4 t_r of transient
if mask.sum() < 8:
mask = np.ones_like(t, dtype=bool)
ts = t[mask]
ss = sigma[mask]
# σ ≈ a·sin(ωt) + b·cos(ωt) — fit (a, b) by linear least squares
M = np.column_stack([np.sin(omega * ts), np.cos(omega * ts)])
coeffs, *_ = np.linalg.lstsq(M, ss, rcond=None)
a, b = float(coeffs[0]), float(coeffs[1])
A = np.sqrt(a**2 + b**2)
# σ = A·sin(ωt − φ) → A·(cos(φ)sin(ωt) − sin(φ)cos(ωt)) = a·sin + b·cos
# so a = A cos(φ), b = −A sin(φ). Hence φ = atan2(−b, a).
phi = float(np.arctan2(-b, a))
return A, phi
Loading
Loading