107 """Cyclic Jacobi eigendecomposition of a symmetric matrix, fixed sweep order (p < q, row-major).
109 Stops when the off-diagonal Frobenius norm is <= tol * ||A||_F, or after max_sweeps sweeps.
110 Returns (lam, V) with A = V diag(lam) V^T (columns of V are the eigenvectors)."""
111 A = np.array(A, dtype=np.float64, copy=
True)
114 normF = math.sqrt(float(np.sum(A * A)))
115 for _
in range(max_sweeps):
116 off = math.sqrt(max(float(np.sum(A * A) - np.sum(np.diag(A) ** 2)), 0.0))
117 if off <= tol * normF:
119 for p
in range(n - 1):
120 for q
in range(p + 1, n):
124 theta = (A[q, q] - A[p, p]) / (2.0 * apq)
125 if abs(theta) > 1e150:
128 t = math.copysign(1.0, theta) / (abs(theta) + math.sqrt(theta * theta + 1.0))
129 c = 1.0 / math.sqrt(t * t + 1.0)
134 A[:, p] = c * ap - s * aq
135 A[:, q] = s * ap + c * aq
138 A[p, :] = c * ap - s * aq
139 A[q, :] = s * ap + c * aq
142 V[:, p] = c * vp - s * vq
143 V[:, q] = s * vp + c * vq
144 return np.diag(A).copy(), V
544def march_to_steady(solver, monitor, *, rtol=1e-4, max_steps=5000, accelerate=True, window=5,
545 check_every=5, num_passes=3, slow_rate=0.997, roundoff=1e-11, callback=None,
546 oracle_kw=None, holder=None, budget=None, rev=1):
547 """§7 driver. rev 0 = the note's revision 0 (certification budget num_passes + 3 blocks);
548 rev 1 = budget 2 (num_passes + 3) blocks plus the early "slow" exit: a failing block whose
549 ratio R lies in [slow_rate^check_every, 1) — a clean tail as slow as the instrument assumes,
550 failing on its remainder bound — resumes acceleration at once."""
551 st = dict(steps=0, acc_steps=0)
555 budget = (num_passes + 3)
if rev == 0
else 2 * (num_passes + 3)
557 cb = callback
or (
lambda steps, phase:
None)
560 def result(converged, reason):
561 return MarchResult(converged=converged, steps=st[
"steps"], accelerated_steps=st[
"acc_steps"],
562 reason=reason, num_restarts=acc.numRestarts
if acc
else 0,
563 monitor=float(monitor()), acc=acc)
565 def certify(step_fn, budget, phase, growth_ref=None, slow_exit=False):
566 """The §3.2 instrument with a fresh block state; returns pass/budget/slow/max/growth/
567 diverged ("slow" only with slow_exit: the driver's policy, not part of the instrument)."""
573 if st[
"steps"] >= max_steps:
577 cb(st[
"steps"], phase)
578 if growth_ref
is not None and (
not math.isfinite(acc.residual)
579 or acc.residual > 10.0 * growth_ref):
583 if local % check_every != check_every - 1:
586 if not math.isfinite(m):
587 if growth_ref
is not None:
593 ok = abs(d) <= roundoff * abs(m)
597 ok = 0.0 < R < 1.0
and \
598 abs(d) / (1.0 - max(R, slow_rate ** check_every)) < rtol * abs(m)
599 passes = passes + 1
if ok
else 0
600 if slow_exit
and not ok
and R
is not None and slow_rate ** check_every <= R < 1.0:
602 if passes >= num_passes:
607 if budget
is not None and blocks >= budget:
610 def plain_tail(phase):
611 out = certify(
lambda:
run_map(solver),
None, phase)
613 return result(
True,
"certified")
614 if out ==
"diverged":
615 return result(
False,
"diverged")
616 return result(
False,
"max_steps")
619 return plain_tail(
"plain")
620 acc = AndersonOracle(solver, window=window, **(oracle_kw
or {}))
621 if holder
is not None:
623 target = (1.0 - slow_rate) * rtol
624 while st[
"steps"] < max_steps:
625 best, since, stagnated = math.inf, 0,
False
626 while st[
"steps"] < max_steps
and acc.status ==
"active" and acc.residual > target:
630 cb(st[
"steps"],
"accelerate")
631 if acc.status ==
"unstable":
632 if acc.ritz_action ==
"fallback":
633 return plain_tail(
"plain")
634 return result(
False,
"unstable")
635 if acc.residual <= 0.5 * best:
636 best, since = acc.residual, 0
639 if since >= 10 * window:
642 if acc.status !=
"active":
643 return plain_tail(
"plain")
644 out = certify(
lambda: acc.step(
False), budget,
"certify", growth_ref=acc.residual,
647 return result(
True,
"certified")
649 return result(
False,
"max_steps")
650 if out ==
"growth" or stagnated:
652 return plain_tail(
"plain")
654 return result(
False,
"max_steps")
672def build_solver(flow, scheme, N, sdf_fn, L, *, rho, mu, F, dt, advection, settings, vel_sweeps=200):
673 Cls = flow.Solver
if scheme ==
"staggered" else flow.SolverColocated
674 s = Cls((N, N, N), extent=(L, L, L))
678 s.set_body_force((F, 0.0, 0.0))
680 s.set_advection(
True)
681 s.set_advection_scheme(advection)
683 s.set_advection(
False)
684 s.diagnostics.set_velocity_solver_params(vel_sweeps)
685 s.set_pressure_multigrid(
True, levels=max(2, int(np.log2(N)) - 1))
686 s.set_pressure_pcg(
True, max_iter=settings[
"pcg"][0], rtol=settings[
"pcg"][1])
687 if settings[
"vel_rtol"]
is not None:
688 s.set_velocity_residual_tolerance(settings[
"vel_rtol"])
689 cx, cy, cz = s.cell_centers()
690 s.set_solid(np.asfortranarray(sdf_fn(cx, cy, cz)), cutcell_pressure=
True)
692 rho_i = rho * us[
"density_to_internal"]
693 mu_i = mu * us[
"viscosity_to_internal"]
694 dt_i = dt * us[
"time_to_internal"]
695 f_i = F * us[
"force_density_to_internal"][0]
696 c_p = 1.0 / (mu_i + rho_i / dt_i)
699 tau = settings[
"vel_rtol"]
if settings[
"vel_rtol"]
else settings[
"pcg"][1]
700 oracle_kw = dict(signature=(dt_i, rho_i, mu_i, f_i, 0.0, 0.0), c_p=c_p, gauged=
True,
701 collocated_advection=(scheme ==
"collocated" and bool(advection)),
702 cells=(N, N, N), inner_tolerance=tau)
707 sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..",
"..",
709 from _bootstrap
import ensure_flow
712 ap = argparse.ArgumentParser()
713 ap.add_argument(
"case", choices=(
"sphere",
"bed"))
714 ap.add_argument(
"--scheme", choices=(
"staggered",
"collocated"), required=
True)
715 ap.add_argument(
"--N", type=int, default=16)
716 ap.add_argument(
"--phi", type=float, default=0.125)
717 ap.add_argument(
"--mu", type=float, default=1.0)
718 ap.add_argument(
"--beta", type=float, default=6.0, help=
"nu dt / h^2 (ignored with --dt)")
719 ap.add_argument(
"--dt", type=float, default=
None)
720 ap.add_argument(
"--advection", default=
None, help=
"advection scheme name (default: Stokes)")
721 ap.add_argument(
"--arrangement", default=
None, help=
"bed: .npz with centres, radii, L")
722 ap.add_argument(
"--settings", choices=(
"production",
"tight"), default=
"production")
723 ap.add_argument(
"--window", type=int, nargs=
"+", default=[0, 3, 5, 8],
724 help=
"0 = plain march (accelerate=False)")
725 ap.add_argument(
"--max-steps", type=int, default=
None)
726 ap.add_argument(
"--json", default=
None)
727 ap.add_argument(
"--label", default=
"")
728 ap.add_argument(
"--trace", action=
"store_true", help=
"print the per-step residual")
729 ap.add_argument(
"--rev", type=int, choices=(0, 1, 2), default=2,
730 help=
"design revision: 0 = W metric, Ritz on every call, floor 1e-10 (WO-1); "
731 "1 = velocity metric, Ritz on mixed windows above 1000 x tau; "
732 "2 = rev 1 without the guard (readings recorded, no consequence)")
733 ap.add_argument(
"--metric", choices=(
"V",
"W"), default=
None, help=
"override the revision's metric")
734 ap.add_argument(
"--ritz-scope", choices=(
"mixed",
"all"), default=
None)
735 ap.add_argument(
"--ritz-action", choices=(
"stop",
"fallback",
"none"), default=
None,
736 help=
"override the guard's consequence (rev 0/1: stop; rev 2: none)")
737 ap.add_argument(
"--vel-sweeps", type=int, default=200,
738 help=
"velocity sweep cap (the study's 200; Revision 2's inexactness probe raised it)")
739 ap.add_argument(
"--budget", type=int, default=
None, help=
"certification budget in blocks "
740 "(default num_passes + 3)")
743 if a.max_steps
is not None:
744 st[
"max_steps"] = a.max_steps
746 if a.case ==
"sphere":
748 R = (3.0 * a.phi / (4.0 * math.pi)) ** (1.0 / 3.0) * L
751 def sdf_fn(cx, cy, cz):
752 X, Y, Z = np.meshgrid(cx, cy, cz, indexing=
"ij")
753 return np.sqrt((X - c) ** 2 + (Y - c) ** 2 + (Z - c) ** 2) - R
756 return F * L ** 3 / (6.0 * math.pi * a.mu * R * u)
758 z = np.load(a.arrangement)
759 cen, rad, L = np.asarray(z[
"centres"], float), np.asarray(z[
"radii"], float), float(z[
"L"])
760 Dm = 2.0 * float(rad.mean())
763 def sdf_fn(cx, cy, cz):
764 from scipy.spatial
import cKDTree
765 X, Y, Z = np.meshgrid(cx, cy, cz, indexing=
"ij")
766 pts = np.stack([X.ravel(order=
"C"), Y.ravel(order=
"C"), Z.ravel(order=
"C")], axis=1)
767 tree = cKDTree(np.mod(cen, L), boxsize=L)
769 d, idx = tree.query(np.mod(pts, L), k=k)
770 return (d - rad[idx]).min(axis=1).reshape(X.shape, order=
"C")
773 return F * L ** 3 / (3.0 * math.pi * a.mu * Dm * u * npart)
775 dt = a.dt
if a.dt
is not None else a.beta * rho * h * h / a.mu
777 s, okw =
build_solver(flow, a.scheme, a.N, sdf_fn, L, rho=rho, mu=a.mu, F=F, dt=dt,
778 advection=a.advection, settings=st, vel_sweeps=a.vel_sweeps)
779 okw[
"metric"] = a.metric
or (
"W" if a.rev == 0
else "V")
780 okw[
"ritz_scope"] = a.ritz_scope
or (
"all" if a.rev == 0
else "mixed")
781 okw[
"ritz_action"] = a.ritz_action
or (
"none" if a.rev == 2
else "stop")
783 okw[
"inner_tolerance"] = 0.0
787 def cb(steps, phase, trace=trace, accref=accref):
788 acc = accref.get(
"acc")
789 trace.append((steps, phase, acc.residual
if acc
is not None else None,
790 acc.ritzRadius
if acc
is not None else None,
791 acc.status
if acc
is not None else None))
792 if a.trace
and acc
is not None:
793 print(f
" {steps:5d} {phase:10s} res={acc.residual:.3e} ritz={acc.ritzRadius:.5f} "
794 f
"mk={acc.mk} st={acc.status} rst={acc.numRestarts}", flush=
True)
797 return float(s.get_u().
mean())
798 t0 = time.perf_counter()
799 res =
march_to_steady(s, monitor, rtol=st[
"rtol"], max_steps=st[
"max_steps"],
800 accelerate=(w > 0), window=max(w, 1), callback=cb, oracle_kw=okw,
801 holder=accref, budget=a.budget, rev=a.rev)
802 wall = time.perf_counter() - t0
805 K = drag_K(um)
if math.isfinite(um)
and um != 0
else math.nan
806 rec = dict(label=a.label, rev=a.rev, metric=okw[
"metric"], ritz_scope=okw[
"ritz_scope"],
807 ritz_action=okw[
"ritz_action"],
808 ritz_would_fire=[list(t)
for t
in acc.ritz_would_fire]
if acc
else None,
809 inner_tolerance=okw[
"inner_tolerance"], budget=a.budget,
810 case=a.case, scheme=a.scheme, N=a.N, phi=a.phi, mu=a.mu, dt=dt,
811 advection=a.advection, settings=a.settings, window=w,
812 converged=res[
"converged"], reason=res[
"reason"], steps=res[
"steps"],
813 accelerated_steps=res[
"accelerated_steps"], num_restarts=res[
"num_restarts"],
814 monitor=um, K=K, wall_s=wall, c_p=okw[
"c_p"],
815 map_seconds=MAP_STATS[
"seconds"], map_calls=MAP_STATS[
"calls"],
816 pressure_iterations=MAP_STATS[
"pressure_iterations"],
817 status=acc.status
if acc
else None, acc_reason=acc.reason
if acc
else None,
818 num_resets=acc.numResets
if acc
else None,
819 ritz_max=acc.ritz_max
if acc
else None,
820 sum_abs_alpha_max=acc.sum_abs_alpha_max
if acc
else None,
821 cond_drops=acc.cond_drops
if acc
else None,
822 zero_diag_drops=acc.zero_diag_drops
if acc
else None,
823 mixed_steps=acc.mixed_steps
if acc
else None,
824 trace=[list(t)
for t
in trace]
if acc
else None)
826 def rmax(ph, trace=trace):
827 v = [t[3]
for t
in trace
if t[1] == ph
and t[3]
is not None and math.isfinite(t[3])]
828 return max(v)
if v
else None
829 rec[
"ritz_max_accelerate"] = rmax(
"accelerate")
830 rec[
"ritz_max_certify"] = rmax(
"certify")
831 rec[
"ritz_over_1p001"] = sum(1
for t
in trace
if t[3]
is not None and math.isfinite(t[3])
832 and t[3] > 1.001
and t[4] ==
"active")
833 first = next((t
for t
in trace
if t[4] ==
"unstable"),
None)
834 rec[
"unstable_step"] = first[0]
if first
else None
835 rec[
"unstable_phase"] = first[1]
if first
else None
836 tag = f
"rev{a.rev}/{okw['metric']}/{okw['ritz_scope']}/{okw['ritz_action']}"
837 print(f
"{a.label:>10s} {tag} {a.case} {a.scheme:10s} N={a.N} {a.settings:10s} m={w} "
838 f
"conv={res['converged']} reason={res['reason']} steps={res['steps']} "
839 f
"acc={res['accelerated_steps']} K={K:.10f} restarts={res['num_restarts']} "
840 f
"ritzA={rec.get('ritz_max_accelerate')} ritzC={rec.get('ritz_max_certify')} "
841 f
"unstable@{rec.get('unstable_step')}/{rec.get('unstable_phase')} "
842 f
"would_fire={[(t[0], f'{t[1]:.2e}', round(t[2], 5)) for t in (rec['ritz_would_fire'] or [])]} "
843 f
"sum|a|max={rec['sum_abs_alpha_max']} "
844 f
"drops={rec['cond_drops']}/{rec['zero_diag_drops']} wall={wall:.2f}s", flush=
True)
846 with open(a.json,
"a")
as fjs:
847 fjs.write(json.dumps(rec) +
"\n")
march_to_steady(solver, monitor, *rtol=1e-4, max_steps=5000, accelerate=True, window=5, check_every=5, num_passes=3, slow_rate=0.997, roundoff=1e-11, callback=None, oracle_kw=None, holder=None, budget=None, rev=1)