87 """T(r, t0) - T_sat on the cell centres, for the three initialisations of `--init`.
89 'similarity' : Scriven's similarity profile sampled AT the cell centre. This is what the
90 driver has always done and what Sato & Niceno (JCP 2013) and Malan et al.
91 (2021) do; it is the shipped default and this branch is byte-identical to the
93 'cellavg' : the same profile, CELL-AVERAGED by sub^3 subsampling (a finite-volume initial
94 state rather than a point sample -- the O(h^2 T'') half of the same question).
95 'uniform' : uniform superheat T_inf in the liquid, T_sat inside the bubble, i.e. NO thermal
96 boundary layer at t0. The classic trap, kept as the control.
98 ax = (np.arange(n) + 0.5) - ctr
99 rad = np.sqrt(ax[:,
None,
None] ** 2 + ax[
None, :,
None] ** 2 + ax[
None,
None, :] ** 2)
100 if mode ==
"similarity":
101 return np.asfortranarray(np.vectorize(
lambda r:
scriven_T(r, R, beta, rr, dT))(rad))
102 if mode ==
"uniform":
103 return np.asfortranarray(np.where(rad > R, dT, 0.0))
104 if mode !=
"cellavg":
105 raise ValueError(f
"unknown --init mode {mode!r}")
108 rmax = math.sqrt(3.0) * n
109 rt = np.linspace(0.0, rmax, 200001)
110 tt = np.array([
scriven_T(r, R, beta, rr, dT)
for r
in rt])
111 off = (np.arange(sub) + 0.5) / sub
112 qs = (np.arange(n)[:,
None] + off[
None, :]).ravel() - ctr
113 out = np.empty((n, n, n))
115 xi = qs[i * sub:(i + 1) * sub]
116 d2 = xi[:,
None,
None] ** 2 + qs[
None, :,
None] ** 2 + qs[
None,
None, :] ** 2
117 t = np.interp(np.sqrt(d2), rt, tt)
118 out[i] = t.reshape(sub, n, sub, n, sub).
mean(axis=(0, 2, 4))
119 return np.asfortranarray(out)
148 """Liquid fraction (C = 1 liquid, 0 vapour) of a sphere of radius R, by sub^3 subsampling."""
149 off = (np.arange(sub) + 0.5) / sub
151 c = np.ones((n, n, n), order=
"F")
152 lo = [max(0, int(math.floor(q - R - 2)))
for q
in (cx, cy, cz)]
153 hi = [min(n, int(math.ceil(q + R + 2)))
for q
in (cx, cy, cz)]
154 xs = (ax[lo[0]:hi[0],
None] + off[
None, :]).ravel()
155 ys = (ax[lo[1]:hi[1],
None] + off[
None, :]).ravel()
156 zs = (ax[lo[2]:hi[2],
None] + off[
None, :]).ravel()
157 d2 = ((xs - cx) ** 2)[:,
None,
None] + ((ys - cy) ** 2)[
None, :,
None] + \
158 ((zs - cz) ** 2)[
None,
None, :]
159 inside = (d2 < R * R).reshape(hi[0] - lo[0], sub, hi[1] - lo[1], sub, hi[2] - lo[2], sub)
160 frac = inside.mean(axis=(1, 3, 5))
161 c[lo[0]:hi[0], lo[1]:hi[1], lo[2]:hi[2]] = 1.0 - frac
162 return np.asfortranarray(c)
188def run(n, ja, ratio, r0, r1, cfl=0.2, alpha_l=1.0, sweeps=200, plane=True, consistent=True,
189 quad=True, muscl=False, init="similarity", sub=4, area_mode=None, verbose=True,
190 budget=0, carry=False, fitkap=False, p3g=None):
193 t0 = (r0 / (2 * beta)) ** 2 / alpha_l
194 te = (r1 / (2 * beta)) ** 2 / alpha_l
195 rho_l, rho_v = 1.0, rr
197 k_l = alpha_l * rho_l * cpl
198 rcp_l, rcp_v = rho_l * cpl, rho_v * cpl
201 h_lv = rho_l * cpl * dT / (rho_v * ja)
204 s = pf.Solver(n, n, n)
207 for f
in (
"-x",
"+x",
"-y",
"+y",
"-z",
"+z"):
208 s.set_domain_bc(f,
"outflow")
209 s.set_pressure_geometry(np.full((n, n, n), 1.0, order=
"F"))
216 r0meas = (3.0 * (float(n) ** 3 - c0.sum()) / (4.0 * math.pi)) ** (1.0 / 3.0)
217 s.set_property_model(
"rho",
"linear",
"C", [rho_v, rho_l - rho_v])
218 s.set_pressure_fcg(
True, 600, 1e-10)
221 s.add_scalar(
"T", k_l / rcp_l,
"koren", sweeps)
222 for f
in (
"-x",
"+x",
"-y",
"+y",
"-z",
"+z"):
223 s.set_scalar_bc(
"T", f,
"dirichlet", dT)
224 s.set_field(
"T", np.asfortranarray(tprof))
225 s.enable_phase_change(rho_v, rho_l, h_lv)
230 if os.environ.get(
"PECLET_P23_WISP"):
231 s.diagnostics.set_vof_wisp_eps(float(os.environ[
"PECLET_P23_WISP"]))
232 if os.environ.get(
"PECLET_P23_NO_EXACTRES"):
233 s.diagnostics.set_pressure_exact_residual(
False)
234 if os.environ.get(
"PECLET_P23_NO_OUTFLOWRHO"):
235 s.diagnostics.set_outflow_rho_correction(
False)
236 s.set_phase_change_thermal(
True,
"T", 0.0, k_v, k_l, 0.0)
237 s.diagnostics.set_phase_change_plane_dirichlet(plane)
239 s.set_phase_change_energy(
True, rcp_v, rcp_l)
240 s.diagnostics.set_phase_change_energy_muscl(muscl)
241 s.diagnostics.set_phase_change_quadratic_fit(quad)
242 if area_mode
is not None:
243 s.diagnostics.set_phase_change_area(AREA_MODES[area_mode])
245 s.diagnostics.set_phase_change_fit_curvature(-2.0 / r0)
248 s.diagnostics.set_phase_change_carry_conserve(
True)
251 budget = budget
or 10
255 s.diagnostics.set_phase_change_budget(
True)
268 tcur, nst, itmax, capped = t0, 0, 0, 0
269 Rd = beta * math.sqrt(alpha_l / tcur)
270 dt0 = 0.4 * cfl / max(Rd * (1.0 - rr), 1e-30)
277 dt = min(dt, te - tcur)
281 s.diagnostics.set_phase_change_fit_curvature(-2.0 / rows[-1][1])
287 except RuntimeError
as ex:
288 if "Weymouth-Yue boundedness cap" not in str(ex):
293 f
"vof_scriven: dt COLLAPSED below {dtmin:.3e} (initial {dt0:.3e}) at step "
294 f
"{nst}, t = {tcur:.4f} of {te:.4f}, last CFL {s.diagnostics.vof_last_courant():.4g} — "
295 f
"the interface velocity is running away, not the time step being small")
299 if verbose
and nst % 200 == 0:
300 print(f
" [step {nst}: t {tcur:.4f}/{te:.4f}, dt {dt:.4e}, "
301 f
"cfl {s.diagnostics.vof_last_courant():.4f}]", flush=
True)
302 c_ = s.diagnostics.vof_last_courant()
303 dt *= min(1.3, max(0.5, cfl / max(c_, 1e-30)))
304 it = s.diagnostics.last_pressure_iterations()
305 itmax = max(itmax, it)
306 capped += 1
if it >= 600
else 0
307 vol = float(n) ** 3 - s.get_vof().sum()
308 rnum = (3.0 * vol / (4.0 * math.pi)) ** (1.0 / 3.0)
309 rex = 2 * beta * math.sqrt(alpha_l * tcur)
310 dg = s.diagnostics.phase_change_diagnostics()
312 mex = rho_v * beta * math.sqrt(alpha_l / tcur)
318 area = dg[
'interface_area']
325 area_end = s.vof_interface_area()
326 meff = rho_l * dg[
'removed_volume'] / (dt_used * area)
if area > 0
else float(
"nan")
327 arel = area / (4.0 * math.pi * rnum * rnum) - 1.0
328 arel_end = area_end / (4.0 * math.pi * rnum * rnum) - 1.0
333 delta = dg[
'removed_volume'] / area
if area > 0
else float(
'nan')
334 r_a = math.sqrt(area / (4.0 * math.pi))
340 e_lat_n = rho_l * dg[
'removed_volume'] * h_lv / dt_used
341 if p3g
is not None and budget:
342 b_ = s.diagnostics.phase_change_budget()
346 worst_id = max(worst_id, abs(e_lat_n - dg[
'q_operator']) / abs(e_lat_n))
347 if qprev
is not None and dg[
'q_operator'] != 0.0:
350 worst_lag = max(worst_lag, abs(dg[
'q_operator'] + qprev) / abs(dg[
'q_operator']))
351 qprev = b_[
'q_gfm'] + b_[
'q_behind']
352 if budget
and (nst % budget == 0
or nst == 1)
and verbose:
353 b = s.diagnostics.phase_change_budget()
356 e_lat = rho_l * dg[
'removed_volume'] * h_lv / dt_used
357 q = b[
'q_gfm'] + b[
'q_behind']
358 dcls = (b[
'e_enter'] - b[
'e_leave']) / dt_used
359 dovw = b[
'd_overwrite'] / dt_used
360 dovn = b[
'd_overwrite_new'] / dt_used
361 dH = (b[
'h_open_new'] - b[
'h_open']) / dt_used
362 print(f
" [BUDGET step {nst:4d} t {tcur:9.4f} dt {dt_used:.3e}] "
363 f
"E_lat {e_lat:12.5e} W q_gfm {q:12.5e} W (-q_gfm/E_lat "
364 f
"{-q/e_lat if e_lat else float('nan'):8.5f})")
365 print(f
" H_liq {b['h_liquid']:12.5e} H_open {b['h_open']:12.5e} "
366 f
"dH_open/dt {dH:12.5e} W | class change: enter {b['e_enter']:11.4e} J "
367 f
"leave {b['e_leave']:11.4e} J net {dcls:12.5e} W ({100*dcls/e_lat if e_lat else float('nan'):+7.3f} % of E_lat)")
368 led = s.diagnostics.phase_change_carry_ledger()
371 idn = (led[
'deposited'] + led[
'lost'] + b[
'd_overwrite'])
372 print(f
" carry ledger: deposited {led['deposited']:12.5e} J "
373 f
"lost {led['lost']:11.4e} J identity residual {idn:.3e} "
374 f
"(rel {idn/b['d_overwrite'] if b['d_overwrite'] else 0.0:.2e})")
375 print(f
" overwrite {dovw:12.5e} W (new cells {dovn:12.5e} W, "
376 f
"{100*dovn/e_lat if e_lat else float('nan'):+7.3f} % of E_lat) | "
377 f
"L->I {b['n_enter_liquid']:5d} G->I {b['n_enter_gas']:5d} "
378 f
"I->L {b['n_leave_liquid']:5d} I->G {b['n_leave_gas']:5d} masked {b['n_masked']:6d}")
386 gas_now = float(n) ** 3 - s.get_vof().sum()
387 if vol_prev
is not None:
388 booked = dg[
'removed_volume'] * rho_l / rho_v
390 vol_ratio.append((gas_now - vol_prev) / booked)
392 rows.append((tcur, rnum, rex, it, dg[
'mdot_mean'], mex, meff, arel,
393 dg[
'interface_cells'], delta, r_a, dg[
'deficit_cells'],
394 dg[
'redistributed'], arel_end, area_end))
395 d = s.diagnostics.phase_change_diagnostics()
397 lo_, hi_ = r0, 200.0 * r0
399 mid_ = 0.5 * (lo_ + hi_)
400 if scriven_T(mid_, r0, beta, rr, dT) > 0.99 * dT:
405 half = rows[len(rows) // 2:]
406 errs = [abs(r[1] - r[2]) / r[2]
for r
in half]
412 _t = np.array([r[0]
for r
in half])
413 _R2 = np.array([r[1]
for r
in half]) ** 2
414 _sl, _ic = np.polyfit(_t, _R2, 1)
415 beta_eff = math.sqrt(max(_sl, 0.0) / (4.0 * alpha_l))
416 t_off = -_ic / _sl
if _sl > 0
else float(
"nan")
417 R_off = rows[-1][1] - 2 * beta_eff * math.sqrt(alpha_l * rows[-1][0])
419 print(f
" N = {n}^3 Ja = {ja:g} ratio {ratio:g} beta = {beta:.6f} "
420 f
"R {r0:g} -> {r1:g} cells steps {nst} init {init} sub {sub}^3"
421 + (f
" area mode {area_mode}" if area_mode
is not None else ""))
422 print(f
" init: thermal BL (99 % of dT) {dt99:.3f} cells, "
423 f
"R0/(2 beta^2) = {r0/(2*beta*beta):.3f} cells; R0 from the colour field "
424 f
"{r0meas:.5f} vs exact {r0:.5f} ({100*(r0meas-r0)/r0:+.4f} %)")
425 for k
in list(range(0, len(rows), max(1, len(rows) // 8))) + [len(rows) - 1]:
426 t_, a, b_, it, mn_, mx_, me_, ar_, nc_, de_, ra_, dc_, rd_, ae_, aa_ = rows[k]
427 print(f
" t {t_:10.4f} R_num {a:8.4f} R_exact {b_:8.4f} "
428 f
"rel {100*(a-b_)/b_:+7.3f} % mdot {mn_:.5e} vs {mx_:.5e} "
429 f
"({100*(mn_-mx_)/mx_:+7.3f} %) mdot_area {100*(me_-mx_)/mx_:+7.3f} % "
430 f
"A/4piR^2 {100*ar_:+6.2f} % cells {nc_:6d} ({nc_/(4*math.pi*a*a):.3f}/h^2)"
432 print(f
" REGRESSION delta {de_:.5e} cells/step (delta/R "
433 f
"{de_/a:.3e}), R_area {ra_:8.4f} ({100*(ra_-b_)/b_:+7.3f} % vs exact), "
434 f
"clipped {dc_}, residue moved {rd_:.3e}; A_end/4piR^2 {100*ae_:+6.2f} % "
435 f
"(R from A_end {math.sqrt(aa_/(4*math.pi)):8.4f}, {100*(math.sqrt(aa_/(4*math.pi))-b_)/b_:+7.3f} % vs exact)")
436 early =
", ".join(f
"{100*(r[4]-r[5])/r[5]:+.2f}" for r
in rows[:6])
437 print(f
" EARLY mdot rel error, steps 1..6: {early} % "
438 f
"(last {100*(rows[-1][4]-rows[-1][5])/rows[-1][5]:+.2f} %)")
440 print(f
" AREA-AVERAGED mdot rel error, last half: mean "
441 f
"{100*np.mean([(r[6]-r[5])/r[5] for r in _h]):+.3f} %, first {100*(_h[0][6]-_h[0][5])/_h[0][5]:+.3f} %, "
442 f
"last {100*(_h[-1][6]-_h[-1][5])/_h[-1][5]:+.3f} %; A/(4 pi R^2) - 1 mean "
443 f
"{100*np.mean([r[7] for r in _h]):+.3f} % [STALE, WO-P3e]; "
444 f
"A_end/(4 pi R^2) - 1 mean {100*np.mean([r[13] for r in _h]):+.3f} %")
445 print(f
" R rel error at step 1 {100*(rows[0][1]-rows[0][2])/rows[0][2]:+.4f} %, "
446 f
"at the half point {100*(half[0][1]-half[0][2])/half[0][2]:+.4f} %, "
447 f
"at the end {100*(rows[-1][1]-rows[-1][2])/rows[-1][2]:+.4f} %")
448 print(f
" GROWTH over the last half: beta_eff {beta_eff:.6f} vs beta {beta:.6f} "
449 f
"({100*(beta_eff-beta)/beta:+.3f} %), implied time offset {t_off:+.5f} of t0 "
450 f
"{t0:.5f} ({100*t_off/t0:+.2f} %), radius offset at the end {R_off:+.4f} cells")
451 print(f
" max |dR|/R over the LAST HALF = {100*max(errs):.3f} % [gate 1 %]")
453 _v = np.array(vol_ratio[len(vol_ratio) // 2:])
454 print(f
" VOLUME AUDIT d(gas)/d(gas booked by the regression), last half: mean "
455 f
"{_v.mean():.6f}, min {_v.min():.6f}, max {_v.max():.6f} "
456 f
"[1 = the colour field delivers exactly what mdot booked]")
457 if p3g
is not None and budget:
458 print(f
" WO-P3g gate (c): worst |E_lat - q_op| / E_lat = {worst_id:.3e} "
459 f
"[gate 1e-10], worst |q_op(n) + q_solve(n-1)| / q_op = {worst_lag:.3e}; "
460 f
"q_orphan/q_op last step "
461 f
"{dg['q_orphan']/dg['q_operator'] if dg['q_operator'] else 0.0:.3e}, "
462 f
"mdot_fit/mdot_op last step "
463 f
"{dg['mdot_fit']/(rho_l*dg['removed_volume']/(dt_used*area)) if area > 0 and dg['removed_volume'] else float('nan'):.6f}")
464 print(f
" pressure iters max {itmax}/600 (capped {capped}), band_div "
465 f
"{d['band_div']:.3e}, C in [{d['min_C']:.2e}, {d['max_C']:.6f}], "
466 f
"T in [{d['T_min']:.3e}, {d['T_max']:.6f}], fallback {d['fallback_cells']}, "
467 f
"unresolved {d['unresolved']:.1e}")
469 print(
" *** CAPPED pressure solve: rule 3b, this run is INVALID")
470 return max(errs), capped
560def area_advect(n=128, R=16.0, sub=16, steps=100, cfl=0.2, amp=0.0, drift=(0.5, 0.25, 0.125),
561 modes=(0, 3, 4, 6, 7)):
562 """WO-P3d gate (b): is the JOINED area BOUNDED under Weymouth-Yue advection?
564 An a-priori probe measures a construction on an EXACT colour field. A running one never sees
565 one: WY leaves round-off residue in every cell its sweeps touch (down to 1e-300 -- the residue
566 the V4 curvature needed `interfaceEps` for and the W0 block container needed `bubbleEps` for),
567 and the interfacial-cell density climbs from the initialisation's value to ~1.5/h^2 within ten
568 steps. A per-cell area sums over those cells, so the question "does the area drift with the
569 WISP population" is a real one; a joined sheet is built from the C = 1/2 crossing and should be
570 blind to them. Both are measured here, on the SAME field, step by step.
572 `amp = 0` is the pure TRANSLATION control, where the exact answer stays `4 pi R^2` for ever.
575 s = pf.Solver(n, n, n)
578 s.set_pressure_geometry(np.full((n, n, n), 1.0, order=
"F"))
582 vol0 = float(n) ** 3 - c0.sum()
583 ref = 4.0 * math.pi * (3.0 * vol0 / (4.0 * math.pi)) ** (2.0 / 3.0)
588 div = s.max_open_divergence_projected()
589 dt = cfl / max(s.vof_max_courant(), 1e-30)
590 print(f
" ADVECTION area gate: {n}^3, sphere R = {R:g} (sub {sub}^3), {steps} WY steps at "
591 f
"cfl {cfl:g} (dt {dt:.4g}), amp {amp:g}, drift {drift}")
592 print(f
" max|div(open u)| of the prescribed field {div:.3e} (advect_vof gate 1e-10); "
593 f
"reference 4 pi R^2 = {ref:.4f}")
598 s.diagnostics.set_phase_change_area(AREA_MODES[m])
599 row[m] = s.vof_interface_area()
601 mixed = int(np.count_nonzero((cc > 0.0) & (cc < 1.0)))
602 wisp = int(np.count_nonzero(((cc > 0.0) & (cc < 1e-8)) | ((cc < 1.0) & (cc > 1 - 1e-8))))
603 vol = float(n) ** 3 - cc.sum()
604 print(f
" {tag:>6} mixed {mixed:7d} wisps {wisp:7d} dV/V {(vol-vol0)/vol0:+.3e} "
605 +
" ".join(f
"m{m} {100*(row[m]/ref-1):+8.3f} %" for m
in modes))
608 hist = {m: []
for m
in modes}
612 for i
in range(1, steps + 1):
614 if i % 10 == 0
or i == steps:
618 print(
" DRIFT over the run (max-min)/ref, and the last value:")
620 h = np.array(hist[m])
621 print(f
" mode {m}: span {100*(h.max()-h.min())/ref:7.3f} % "
622 f
"last {100*(h[-1]/ref-1):+8.3f} % first {100*(h[0]/ref-1):+8.3f} %")
636def area_probe(n, radii, ratio=100.0, sub=4, mode=None, shape="sphere"):
637 """A-PRIORI probe (WO-P3b): the summed PLIC interface area of an EXACT sphere, against 4 pi R^2.
639 No time stepping, no energy solve, no velocity — the exact sphere fractions are set, one
640 `apply_phase_change(0.0)` builds the interface, and `phase_change_diagnostics()['interface_area']`
641 is read. This isolates the geometry of `plicArea` + the MYC normals from every other part of
642 the rung, and it is the quantity the R(t) gate integrates: the bubble grows as int mdot dA.
644 rho_l, rho_v = 1.0, 1.0 / ratio
645 print(f
" a-priori INTERFACE AREA probe, {n}^3, exact sphere fractions (sub = {sub}^3), "
646 f
"area mode {0 if mode is None else mode}")
650 s = pf.Solver(n, n, n)
653 s.set_pressure_geometry(np.full((n, n, n), 1.0, order=
"F"))
655 if R > 0
and shape ==
"cylinder":
658 rm = math.sqrt((float(n) ** 3 - c0.sum()) / (math.pi * float(n)))
659 ref, lbl = 2.0 * math.pi * rm * float(n),
"2 pi R n_z"
662 ref, lbl = 4.0 * math.pi * ((3.0 * (float(n) ** 3 - c0.sum()) / (4.0 * math.pi))
663 ** (1.0 / 3.0)) ** 2,
"4 pi R^2 "
665 nrm = {-5: (0, 0, 1), -6: (1, 1, 0), -7: (1, 1, 1), -8: (1, 2, 3)}[int(R)]
667 ref = 2.0 * math.sqrt(float(nrm[0] ** 2 + nrm[1] ** 2 + nrm[2] ** 2)) * float(n) ** 2
668 lbl = f
"periodic {nrm}"
672 nrm = {-1: (0, 0, 1), -2: (1, 1, 0), -3: (1, 1, 1), -4: (1, 2, 3)}[int(R)]
674 ref, lbl = float(
"nan"), f
"plane {nrm}"
676 s.set_property_model(
"rho",
"linear",
"C", [rho_v, rho_l - rho_v])
677 s.enable_phase_change(rho_v, rho_l, 1.0)
678 s.set_mass_flux_uniform(0.0)
680 s.diagnostics.set_phase_change_area(AREA_MODES[mode])
681 s.apply_phase_change(0.0)
682 d = s.diagnostics.phase_change_diagnostics()
683 mc =
_mc_area(c0)
if R > -5
else float(
"nan")
686 rel = d[
'interface_area'] / ref - 1.0
688 if prev
is not None and R > 0
and prev[0] > 0:
689 ordr = f
" order vs R = {prev[0]:g}: {math.log(abs(prev[1]) / abs(rel)) / math.log(R / prev[0]):.3f}"
696 tot = d[
'interface_area'] + d[
'area_orphan']
697 orph = (f
" orphan {d['area_no_cascade_cells']} cells / {d['area_orphan']:.4f} h^2"
698 f
" -> SHEET {tot:.4f} ({100*(tot/ref-1):+7.3f} %)")
699 print(f
" R {R:6.2f} {lbl} ({d['interface_cells']:6d} cells"
700 + (f
", HF {d['area_hf_cells']:6d} PV {d['area_pv_cells']:5d} "
701 f
"none {d['area_no_cascade_cells']}" if mode
else "") + orph +
") "
702 f
"A_sum {d['interface_area']:12.4f} ref {ref:12.4f} "
703 f
"marching cubes {mc:12.4f} ({100*(mc/ref-1):+6.3f} %) "
704 f
"A_sum rel {100*rel:+7.3f} %{ordr}")
710 """Real spherical-harmonic moments of a signed cell weight `w` (a removal density), returned
711 as the per-l RMS coefficient normalised by |sum w|.
713 The regression shifts each cell's plane along that cell's OWN normal, so a systematic
714 under-shift on (say) the octant diagonals would leave the bubble faceted. A cubic lattice can
715 only produce CUBIC-harmonic anisotropy, whose leading term is l = 4 (l = 2 vanishes by the
716 three mirror symmetries), so the l = 2 row is the control and the l = 4 row is the signal.
719 X = (ax - ctr[0])[:,
None,
None] * np.ones((1, len(ax), len(ax)))
720 Y = (ax - ctr[1])[
None, :,
None] * np.ones((len(ax), 1, len(ax)))
721 Z = (ax - ctr[2])[
None,
None, :] * np.ones((len(ax), len(ax), 1))
722 r = np.sqrt(X * X + Y * Y + Z * Z)
723 r = np.where(r > 0, r, 1.0)
724 u, v, t = X / r, Y / r, Z / r
728 q = [u * v, v * t, t * u, u * u - v * v, (3.0 * t * t - 1.0) / math.sqrt(3.0)]
729 out[2] = math.sqrt(sum(float((w * c).sum()) ** 2
for c
in q) / 5.0)
734 u2, v2, t2 = u * u, v * v, t * t
735 k = [u2 * u2 + v2 * v2 + t2 * t2 - 3.0 / 5.0,
736 u * v * (u2 - v2), v * t * (v2 - t2), t * u * (t2 - u2),
737 u * v * (7.0 * t2 - 1.0), v * t * (7.0 * u2 - 1.0), t * u * (7.0 * v2 - 1.0),
738 (u2 - v2) * (7.0 * t2 - 1.0), (35.0 * t2 * t2 - 30.0 * t2 + 3.0)]
739 out[4] = math.sqrt(sum(float((w * c).sum()) ** 2
for c
in k) / 9.0)
760def regress_probe(n=128, radii=(16.0,), deltas=(0.05, 0.1, 0.2), sub=16, modes=(0, 6),
761 ratio=100.0, advect_steps=0, drift=(0.5, 0.25, 0.125), cfl=0.2, nbin=4):
762 """WO-P3e — the a-priori INTERFACE REGRESSION probe. No time stepping, no energy solve, no
763 velocity: an exact sphere goes in, ONE `apply_phase_change(dt)` with a uniform prescribed mdot
764 runs the plane shift + clip-and-redistribute, and the colour field that comes out is compared
765 against the analytically known answer.
767 With `rho_l = 1` and `mdot = 1` the step's normal displacement is exactly `delta = dt`, so the
768 exact liquid volume removed from a sphere of radius R is the shell
770 dV = 4/3 pi ((R+delta)^3 - R^3) = 4 pi R^2 delta (1 + delta/R + delta^2/(3 R^2))
772 (the bubble is the GAS, so evaporation GROWS it: R -> R + delta), the new radius from the
773 volume deficit is R + delta exactly, and the new interfacial area is 4 pi (R+delta)^2. Those
774 three are the gate. The fourth read-out is ISOTROPY: the shift acts along each cell's own
775 normal with the cell's own area, so an under-shift on the octant diagonals would leave the
776 bubble faceted -- measured both as the ratio of the removed volume to the exact removed volume
777 in bins of the cubic invariant `u_x^4 + u_y^4 + u_z^4`, and as the l = 2 / l = 4 spherical
778 harmonic moments of the removal density.
780 `--regress-advect N` runs the sphere through N Weymouth-Yue steps of the PURE TRANSLATION field
781 of gate (b) first, so the fractions are the ones a running solver actually carries (WY re-creates
782 the sliver/wisp population within ten steps) while the exact reference stays a sphere.
785 rho_l, rho_v = 1.0, 1.0 / ratio
786 print(f
" a-priori REGRESSION probe, {n}^3, exact sphere fractions (sub = {sub}^3), "
787 f
"ratio {ratio:g}, {advect_steps} WY pre-steps")
790 cstart, ctr = c0, (ctr0, ctr0, ctr0)
792 s = pf.Solver(n, n, n)
795 s.set_pressure_geometry(np.full((n, n, n), 1.0, order=
"F"))
802 dta = cfl / max(s.vof_max_courant(), 1e-30)
803 for _
in range(advect_steps):
806 ctr = tuple(ctr0 + d * dta * advect_steps
for d
in drift)
808 vg0 = float(n) ** 3 - cstart.sum()
809 R0 = (3.0 * vg0 / (4.0 * math.pi)) ** (1.0 / 3.0)
810 mixed0 = int(np.count_nonzero((cstart > 0.0) & (cstart < 1.0)))
813 cex0 = (
sphere_colour(n, ctr[0], ctr[1], ctr[2], R, sub=sub)
if advect_steps
else c0)
814 l1 = float(np.abs(cstart - cex0).sum())
815 print(f
" R {R:g}: R0 from the volume {R0:.5f} ({100*(R0-R)/R:+.4f} %), mixed cells "
816 f
"{mixed0}, |C - C_exact|_1 {l1:.4e}")
820 s = pf.Solver(n, n, n)
823 s.set_pressure_geometry(np.full((n, n, n), 1.0, order=
"F"))
826 s.set_property_model(
"rho",
"linear",
"C", [rho_v, rho_l - rho_v])
827 s.enable_phase_change(rho_v, rho_l, 1.0)
828 s.diagnostics.set_phase_change_area(AREA_MODES[mode])
829 s.set_mass_flux_uniform(1.0)
830 a_bef = s.vof_interface_area()
831 s.apply_phase_change(delta)
832 d = s.diagnostics.phase_change_diagnostics()
834 a_aft = s.vof_interface_area()
836 vg1 = float(n) ** 3 - c1.sum()
837 R1 = (3.0 * vg1 / (4.0 * math.pi)) ** (1.0 / 3.0)
839 removed = float(rem.sum())
840 rex = 4.0 * math.pi * R0 * R0 * delta * (1.0 + delta / R0
841 + delta * delta / (3.0 * R0 * R0))
843 print(f
" mode {mode} delta {delta:6.3f} "
844 f
"dV {removed:12.6f} vs exact {rex:12.6f} ({100*(removed/rex-1):+8.4f} %) "
845 f
"R1 {R1:9.5f} vs {R1x:9.5f} ({100*(R1-R1x)/R1x:+8.5f} %)")
846 print(f
" A_before {a_bef:11.4f} "
847 f
"({100*(a_bef/(4*math.pi*R0*R0)-1):+7.3f} %) "
848 f
"A_after {a_aft:11.4f} ({100*(a_aft/(4*math.pi*R1x*R1x)-1):+7.3f} %) "
849 f
"A_regr_used {d['interface_area']:11.4f} removed_vol(diag) "
850 f
"{d['removed_volume']:.6f}")
851 pure_l = int(np.count_nonzero((cstart >= 1.0) & (c1 < 1.0)))
852 pure_g = int(np.count_nonzero((cstart <= 0.0) & (c1 > 0.0)))
853 emptied = int(np.count_nonzero((cstart > 0.0) & (c1 <= 0.0)))
854 print(f
" CENSUS clipped at 0 {d['deficit_cells']:6d}, "
855 f
"at 1 {d['excess_cells']:5d}, |residue| moved {d['redistributed']:.6e}, "
856 f
"unresolved {d['unresolved']:.2e}; cells emptied {emptied}, pure LIQUID "
857 f
"cells touched {pure_l}, pure GAS cells touched {pure_g}; "
858 f
"C in [{d['min_C']:.3e}, {d['max_C']:.6f}]")
862 else sphere_colour(n, ctr[0], ctr[1], ctr[2], R + delta, sub=sub))
865 for b
in range(nbin):
867 se, sa = float(ex[m].sum()), float(rem[m].sum())
868 rat.append(sa / se
if se != 0
else float(
"nan"))
869 lbl =
" ".join(f
"s<{edges[b+1]:.3f} {100*(rat[b]-1):+7.3f} %" for b
in range(nbin))
870 print(f
" ISOTROPY (removed/exact by cubic bin, "
871 f
"axis=1 body-diag=1/3): {lbl}")
872 ax = np.arange(n) + 0.5
876 print(f
" MOMENTS l2/l0: actual {m_a[2]/abs(t_a):.3e} "
877 f
"exact {m_e[2]/abs(t_e):.3e}; l4/l0: actual {m_a[4]/abs(t_a):.3e} exact "
878 f
"{m_e[4]/abs(t_e):.3e}; residual l0 {t_d/t_e:+.3e} l2 "
879 f
"{m_d[2]/abs(t_e):.3e} l4 {m_d[4]/abs(t_e):.3e}")
882def _mdot_scene(n, R, ja, ratio, sub, alpha_l, area_mode, quad, plane, geom, dt, prof, kap=False,
884 """One a-priori mdot evaluation: `geom` x `prof`, the 2x2 that separates the two curvatures.
886 `geom` = 'sphere' (the exact sphere, sub^3 fractions) or 'plane' (a flat interface normal to x
887 at a sub-cell offset, the same sub^3 fractions). `prof` = 'scriven' (Scriven's similarity
888 profile of a bubble of radius R at the matching time, as a function of the signed distance to
889 the interface -- so the sphere and plane rows carry IDENTICAL T', T'' at the interface) or
890 'linear' (T = T'(R) d exactly, zero profile curvature).
892 ONE `apply_phase_change(dt)` evaluates the shipped estimator: no energy solve, no velocity, no
893 time stepping. The only things measured are the one-sided gradient fit and the area on that
894 field. `plane x linear` is the exactness control (the fit's model is the exact state);
895 `sphere x linear` isolates the INTERFACE's curvature; `plane x scriven` isolates the PROFILE's
896 curvature and its resolution; `sphere x scriven` is the run's own configuration.
900 t = (R / (2 * beta)) ** 2 / alpha_l
901 rho_l, rho_v = 1.0, rr
903 k_l = alpha_l * rho_l * cpl
904 rcp_l, rcp_v = rho_l * cpl, rho_v * cpl
907 h_lv = rho_l * cpl * dT / (rho_v * ja)
909 Rdot = beta * math.sqrt(alpha_l / t)
910 mdot_ex = rho_v * Rdot
911 gradT = mdot_ex * h_lv / k_l
913 ax = (np.arange(n) + 0.5) - ctr
917 rad = np.sqrt(ax[:,
None,
None] ** 2 + ax[
None, :,
None] ** 2 + ax[
None,
None, :] ** 2)
918 Rmeas = (3.0 * (float(n) ** 3 - c0.sum()) / (4.0 * math.pi)) ** (1.0 / 3.0)
919 ref_area = 4.0 * math.pi * Rmeas * Rmeas
923 off = (np.arange(sub) + 0.5) / sub
924 q = (np.arange(n)[:,
None] + off[
None, :]).ravel() - ctr - shift
925 frac = (q > 0.0).reshape(n, sub).
mean(axis=1)
926 c0 = np.asfortranarray(np.repeat(frac, n * n).reshape(n, n, n))
927 d = (ax - shift)[:,
None,
None] * np.ones((1, n, n))
929 ref_area = float(n) ** 2
931 tprof = np.asfortranarray(np.where(d > 0.0, gradT * d, 0.0))
933 tprof = np.asfortranarray(np.vectorize(
lambda x:
scriven_T(R + x, R, beta, rr, dT))(
936 s = pf.Solver(n, n, n)
939 s.set_pressure_geometry(np.full((n, n, n), 1.0, order=
"F"))
942 s.set_property_model(
"rho",
"linear",
"C", [rho_v, rho_l - rho_v])
943 s.add_scalar(
"T", k_l / rcp_l,
"koren", 1)
944 for f
in (
"-x",
"+x",
"-y",
"+y",
"-z",
"+z"):
945 s.set_scalar_bc(
"T", f,
"neumann", 0.0)
946 s.set_field(
"T", tprof)
947 s.enable_phase_change(rho_v, rho_l, h_lv)
948 s.set_phase_change_thermal(
True,
"T", 0.0, k_v, k_l, 0.0)
949 s.diagnostics.set_phase_change_plane_dirichlet(plane)
950 s.set_phase_change_energy(
True, rcp_v, rcp_l)
951 s.diagnostics.set_phase_change_quadratic_fit(quad)
952 if area_mode
is not None:
953 s.diagnostics.set_phase_change_area(AREA_MODES[area_mode])
957 s.diagnostics.set_phase_change_fit_curvature(-2.0 / R
if geom ==
"sphere" else 0.0)
958 s.diagnostics.set_phase_change_budget(
True)
961 s.apply_phase_change(dt)
962 dg = s.diagnostics.phase_change_diagnostics()
963 bud = s.diagnostics.phase_change_budget()
964 md = np.asarray(s.get_field(
"mdot"))
966 iface = (cc > 1e-12) & (cc < 1.0 - 1e-12)
967 A = dg[
'interface_area']
968 m_area = rho_l * dg[
'removed_volume'] / (dt * A)
if A > 0
else float(
'nan')
972 e_lat = rho_l * dg[
'removed_volume'] * h_lv / dt
973 q_ex = mdot_ex * h_lv * ref_area
974 return dict(beta=beta, t=t, Rdot=Rdot, mdot_ex=mdot_ex, area=A, ref_area=ref_area,
975 m_area=m_area, cells=int(dg[
'interface_cells']), m_cell=md[iface],
976 Rmeas=Rmeas, gradT=gradT, bl=dT / gradT, e_lat=e_lat,
977 q_gfm=bud[
'q_gfm'] + bud[
'q_behind'], q_ex=q_ex, mdot_fit=dg[
'mdot_fit'],
978 q_op=dg[
'q_operator'], q_orphan=dg[
'q_orphan'],
979 a_orphan=dg[
'area_orphan'], n_noc=dg[
'area_no_cascade_cells'],
980 delta=dg[
'removed_volume'] / A
if A > 0
else float(
'nan'),
981 clipped=int(dg[
'deficit_cells']))
1037def carry_probe(n, steps, ratio=100.0, carry=False, sub=16, grad=0.05, mdot=2.0e-3, p3g=None):
1038 """WO-P3f, the a-priori gate of `--carry`: a PLANAR interface swept through cells at a
1039 PRESCRIBED mass flux, with a linear superheat profile in the liquid.
1041 Everything about the scene is chosen so the ONLY enthalpy book-keeping under test is the
1042 interfacial cells' Dirichlet overwrite: `mdot` is prescribed (no gradient fit), the phase
1043 conductivities are ~0 (no diffusion), the interface is flat (no curvature) and the profile is
1044 linear (no profile curvature). Per step the instrument prints the identity the option must
1045 satisfy exactly -- what the deposit hands back plus what it could not place equals what the
1046 overwrite destroys -- and the trajectory of the total enthalpy `sum rho c_p(C) (T - T_sat)`
1047 over the inner cells, which is the quantity the option exists to stop leaking.
1049 rho_l, rho_v = 1.0, 1.0 / ratio
1050 rcp_l, rcp_v = 1.0, rho_v
1051 k_l, k_v = 1.0, 1.0 / ratio
1052 h_lv = k_l * grad / mdot
1054 shift = round(0.37 * sub) / sub
1055 off = (np.arange(sub) + 0.5) / sub
1056 q = (np.arange(n)[:,
None] + off[
None, :]).ravel() - ctr - shift
1057 frac = (q > 0.0).reshape(n, sub).
mean(axis=1)
1058 c0 = np.asfortranarray(np.repeat(frac, n * n).reshape(n, n, n))
1059 ax = (np.arange(n) + 0.5) - ctr - shift
1060 d = ax[:,
None,
None] * np.ones((1, n, n))
1061 tprof = np.asfortranarray(np.where(d > 0.0, grad * d, 0.0))
1063 s = pf.Solver(n, n, n)
1066 for f
in (
"-x",
"+x",
"-y",
"+y",
"-z",
"+z"):
1067 s.set_domain_bc(f,
"outflow")
1068 s.set_pressure_geometry(np.full((n, n, n), 1.0, order=
"F"))
1071 s.set_property_model(
"rho",
"linear",
"C", [rho_v, rho_l - rho_v])
1072 s.set_pressure_fcg(
True, 600, 1e-10)
1073 s.add_scalar(
"T", k_l / rcp_l,
"koren", 50)
1074 for f
in (
"-x",
"+x",
"-y",
"+y",
"-z",
"+z"):
1075 s.set_scalar_bc(
"T", f,
"neumann", 0.0)
1076 s.set_field(
"T", tprof)
1077 s.enable_phase_change(rho_v, rho_l, h_lv)
1083 s.set_phase_change_thermal(
True,
"T", 0.0, k_v, k_l, 0.0)
1084 s.set_phase_change_energy(
True, rcp_v, rcp_l)
1085 s.diagnostics.set_phase_change_area(
"sheet-plic-centroid")
1086 s.diagnostics.set_phase_change_budget(
True)
1088 s.diagnostics.set_phase_change_carry_conserve(
True)
1091 print(f
" WO-P3f CARRY probe, {n}^3, planar interface, prescribed mdot {mdot:g}, linear "
1092 f
"superheat grad {grad:g}/cell, k_l = {k_l:g}, h_lv = {h_lv:g}, carry {carry}")
1095 dt = 0.1 / (mdot * (1.0 / rho_v - 1.0 / rho_l))
1098 for k
in range(steps):
1101 b = s.diagnostics.phase_change_budget()
1102 led = s.diagnostics.phase_change_carry_ledger()
1103 idn = led[
'deposited'] + led[
'lost'] + b[
'd_overwrite']
1104 T = np.asarray(s.get_field(
"T"))
1105 C = np.asarray(s.get_vof())
1106 H = float(np.sum((rcp_v * (1.0 - C) + rcp_l * C) * T))
1107 dH = float(
"nan")
if hprev
is None else H - hprev
1109 dgp = s.diagnostics.phase_change_diagnostics()
1110 e_lat_p = rho_l * dgp[
'removed_volume'] * h_lv / dt
1111 qop = b[
'q_gfm'] + b[
'q_behind']
1112 print(f
" step {k+1:3d} E_lat {e_lat_p:12.5e} W q_op(build) "
1113 f
"{dgp['q_operator']:12.5e} W IDENTITY (E_lat - q_op)/E_lat "
1114 f
"{(e_lat_p - dgp['q_operator'])/e_lat_p if e_lat_p else float('nan'):+.3e} | "
1115 f
"solve q_gfm+q_behind {qop:12.5e} W, vs the build's flux "
1116 f
"{(dgp['q_operator'] + qpr)/dgp['q_operator'] if (qpr is not None and dgp['q_operator']) else float('nan'):+.3e}")
1118 print(f
" step {k+1:3d} overwrite {b['d_overwrite']:12.5e} J deposited "
1119 f
"{led['deposited']:12.5e} lost {led['lost']:11.4e} IDENTITY {idn:.3e} "
1120 f
"(rel {idn/b['d_overwrite'] if b['d_overwrite'] else 0.0:.2e}) "
1121 f
"H {H:.10e} dH {dH:+.5e} q_gfm {b['q_gfm']:.3e}")
1125 ap = argparse.ArgumentParser()
1126 ap.add_argument(
"--n", type=int, default=128)
1127 ap.add_argument(
"--ja", type=str, default=
"0.5,2,10")
1128 ap.add_argument(
"--ratio", type=float, default=10.0)
1129 ap.add_argument(
"--r0", type=float, default=6.0)
1130 ap.add_argument(
"--r1", type=float, default=20.0)
1131 ap.add_argument(
"--sweeps", type=int, default=200)
1132 ap.add_argument(
"--cfl", type=float, default=0.2,
1133 help=
"target interface-local Courant number of the adaptive dt (WO-P3b uses "
1134 "it as the temporal-refinement ablation of the growth-rate deficit)")
1135 ap.add_argument(
"--no-plane", action=
"store_true")
1136 ap.add_argument(
"--no-consistent", action=
"store_true")
1137 ap.add_argument(
"--no-quad", action=
"store_true")
1138 ap.add_argument(
"--muscl", action=
"store_true")
1139 ap.add_argument(
"--init", choices=(
"similarity",
"cellavg",
"uniform"), default=
"similarity",
1140 help=
"initial T(r, t0): Scriven's similarity profile at the cell centre "
1141 "(default, = the shipped behaviour), the same profile cell-averaged, or "
1142 "a uniform superheat with a sharp bubble (the classic trap; control)")
1143 ap.add_argument(
"--sub", type=int, default=4,
1144 help=
"sub^3 subsampling of the RUN's initial sphere colour field (WO-P3c: "
1145 "sub = 4 drops a quarter of the interfacial cells and 6 %% of the area)")
1146 ap.add_argument(
"--area-mode", type=int, default=
None,
1147 help=
"0 = PLIC/MYC area (rung P0/P1), 1 = cascade metric, 2 = cascade normal, "
1148 "3 = cascade footprint; WO-P3d 4..7 = the JOINED marching-tetrahedra "
1149 "sheet (4 C=1/2 centroid, 5 C=1/2 split, 6 PLIC-distance centroid, "
1150 "7 PLIC-distance split)")
1151 ap.add_argument(
"--area-shape", choices=(
"sphere",
"cylinder"), default=
"sphere",
1152 help=
"what the area probe puts in the box (the cylinder has one zero "
1153 "principal curvature and an exact z direction)")
1154 ap.add_argument(
"--area-sub", type=int, default=4,
1155 help=
"sub^3 subsampling of the area probe's EXACT sphere fractions (WO-P3c: "
1156 "the sliver cells a coarse sub drops are what WO-P3b measured)")
1157 ap.add_argument(
"--area-probe", type=str, default=
"",
1158 help=
"comma-separated radii in cells: run the a-priori PLIC-area probe on an "
1159 "exact sphere instead of the growth run, and exit")
1160 ap.add_argument(
"--area-advect", action=
"store_true",
1161 help=
"WO-P3d gate (b): is the area BOUNDED under Weymouth-Yue advection? "
1162 "Runs a sphere through a solenoidal field (or a pure translation with "
1163 "--advect-amp 0) and prints every area mode on the SAME field beside the "
1164 "mixed-cell and wisp census")
1165 ap.add_argument(
"--advect-amp", type=float, default=0.0)
1166 ap.add_argument(
"--advect-steps", type=int, default=100)
1167 ap.add_argument(
"--advect-r", type=float, default=16.0)
1168 ap.add_argument(
"--regress-probe", type=str, default=
"",
1169 help=
"WO-P3e: comma-separated radii in cells — run the a-priori INTERFACE "
1170 "REGRESSION probe (one apply_phase_change at a uniform mdot on an exact "
1171 "sphere, against the analytic shell volume / radius / area / isotropy) "
1172 "instead of the growth run, and exit")
1173 ap.add_argument(
"--regress-delta", type=str, default=
"0.05,0.1,0.2",
1174 help=
"WO-P3e: comma-separated normal displacements per step, in cells")
1175 ap.add_argument(
"--regress-modes", type=str, default=
"0,6",
1176 help=
"WO-P3e: comma-separated set_phase_change_area modes to run the probe on")
1177 ap.add_argument(
"--regress-advect", type=int, default=0,
1178 help=
"WO-P3e: Weymouth-Yue pre-steps (pure translation) before the regression "
1179 "step, so the fractions are advection-realistic")
1180 ap.add_argument(
"--fit-curvature", action=
"store_true",
1181 help=
"WO-P3f: correct the one-sided fits' sample distances for the interface "
1182 "curvature, with kappa = -2/R PRESCRIBED from the known geometry")
1183 ap.add_argument(
"--carry", action=
"store_true",
1184 help=
"WO-P3f: turn on set_phase_change_carry_conserve (the enthalpy the "
1185 "interfacial cells' Dirichlet overwrite destroys is returned to the "
1186 "phase it came from)")
1187 ap.add_argument(
"--carry-probe", type=int, default=0,
1188 help=
"WO-P3f a-priori gate for --carry: N steps of a PLANAR interface swept "
1189 "at a prescribed mdot through a linear superheat profile, checking the "
1190 "conservation identity per step; then exit")
1191 ap.add_argument(
"--budget", type=int, default=0,
1192 help=
"WO-P3f instrument (a): print the ENERGY BUDGET of the energy solve every "
1193 "N steps (0 = off, the shipped behaviour)")
1194 ap.add_argument(
"--mdot-probe", type=str, default=
"",
1195 help=
"WO-P3f gate (b): comma-separated radii in cells - run the a-priori MDOT "
1196 "probe (Scriven's similarity profile on an EXACT sphere, one "
1197 "apply_phase_change, no energy solve) against the analytic mdot, with a "
1198 "flat-interface control carrying the same profile; then exit")
1199 ap.add_argument(
"--mdot-geom", type=str, default=
"sphere,plane",
1200 help=
"WO-P3f: which scenes the mdot probe runs (sphere and/or plane)")
1201 ap.add_argument(
"--mdot-prof", type=str, default=
"scriven",
1202 help=
"WO-P3f: which temperature profiles the mdot probe runs "
1203 "('scriven' and/or 'linear'; linear x plane is the exactness control)")
1204 ap.add_argument(
"--mdot-dt", type=float, default=0.0,
1205 help=
"WO-P3f: fixed regression dt for the mdot probe (0 = pick it so the "
1206 "normal displacement is the run's own 1e-3 cells)")
1207 ap.add_argument(
"--energy-order", type=int, default=1,
1208 help=
"WO-P3g: 1 = the shipped interfacial energy operator; 2 = the "
1209 "second-order package (operator-flux mdot + Gibou-Fedkiw row + "
1210 "curvature-consistent distances + the conserving Dirichlet overwrite)")
1211 ap.add_argument(
"--op-mdot", type=int, default=
None,
1212 help=
"WO-P3g item 1 override (0/1): mdot from the energy operator's own flux")
1213 ap.add_argument(
"--gfm-order", type=int, default=
None,
1214 help=
"WO-P3g item 2 override (1/2): the ghost-fluid row's order")
1215 ap.add_argument(
"--curv-dist", type=int, default=
None,
1216 help=
"WO-P3g item 3 override (0/1): curvature-consistent distances")
1217 ap.add_argument(
"--carry-opt", type=int, default=
None,
1218 help=
"WO-P3g item 4 override (0/1): the conserving Dirichlet overwrite")
1219 ap.add_argument(
"--deposit-fallback", type=int, default=
None,
1220 help=
"WO-P3f open item 6 (0/1): give an interfacial cell whose two "
1221 "along-the-normal deposit candidates are both interfacial a target from "
1222 "the 5^3 box, instead of leaving the divergence source in place")
1225 if a.energy_order != 1
or a.op_mdot
is not None or a.gfm_order
is not None or \
1226 a.curv_dist
is not None or a.carry_opt
is not None or a.deposit_fallback
is not None:
1227 p3g = dict(order=a.energy_order, op_mdot=a.op_mdot, gfm_order=a.gfm_order,
1228 curv_dist=a.curv_dist, carry=a.carry_opt, deposit=a.deposit_fallback)
1230 area_advect(a.n, R=a.advect_r, sub=a.area_sub, steps=a.advect_steps, cfl=a.cfl,
1234 regress_probe(a.n, [float(x)
for x
in a.regress_probe.split(
",")],
1235 deltas=[float(x)
for x
in a.regress_delta.split(
",")],
1236 sub=a.area_sub, modes=[int(x)
for x
in a.regress_modes.split(
",")],
1237 ratio=a.ratio, advect_steps=a.regress_advect, cfl=a.cfl)
1240 carry_probe(a.n, a.carry_probe, ratio=a.ratio, carry=a.carry, p3g=p3g)
1243 mdot_probe(a.n, [float(x)
for x
in a.mdot_probe.split(
",")],
1244 ja=float(a.ja.split(
",")[0]), ratio=a.ratio, sub=a.area_sub,
1245 area_mode=a.area_mode, quad=
not a.no_quad, plane=
not a.no_plane,
1246 dt=(a.mdot_dt
if a.mdot_dt > 0
else None),
1247 geoms=tuple(a.mdot_geom.split(
",")),
1248 profs=tuple(a.mdot_prof.split(
",")), kap=a.fit_curvature, p3g=p3g)
1251 area_probe(a.n, [float(x)
for x
in a.area_probe.split(
",")], ratio=a.ratio,
1252 sub=a.area_sub, mode=a.area_mode, shape=a.area_shape)
1254 print(f
"P3 Scriven bubble growth, {a.n}^3, cfl {a.cfl:g}, sweeps {a.sweeps}, "
1255 f
"plane Dirichlet {not a.no_plane}, "
1256 f
"consistent energy {not a.no_consistent}, quadratic fit {not a.no_quad}, "
1257 f
"energy MUSCL {a.muscl}, init {a.init}, sub {a.sub}^3, area mode {a.area_mode}, "
1259 for ja
in [float(x)
for x
in a.ja.split(
",")]:
1260 run(a.n, ja, a.ratio, a.r0, a.r1, cfl=a.cfl, sweeps=a.sweeps, plane=
not a.no_plane,
1261 consistent=
not a.no_consistent, quad=
not a.no_quad, muscl=a.muscl, init=a.init,
1262 sub=a.sub, area_mode=a.area_mode, budget=a.budget, carry=a.carry,
1263 fitkap=a.fit_curvature, p3g=p3g)
run(n, ja, ratio, r0, r1, cfl=0.2, alpha_l=1.0, sweeps=200, plane=True, consistent=True, quad=True, muscl=False, init="similarity", sub=4, area_mode=None, verbose=True, budget=0, carry=False, fitkap=False, p3g=None)