Ко всем библиотекам · scripts

scripts/grchombo/ltb_to_grchombo.py

Late-start initial data for the GRChombo PBHCosmo example from the exact LTB solution (e = 0 test, plan 6.12 stage 1).

320 строк · 16.0 KB · pbhgr @ 9e8e13d · как текст

#!/usr/bin/env python3
"""Late-start initial data for the GRChombo PBHCosmo example from the exact LTB solution (e = 0 test, plan 6.12 stage 1).

At t_0 = f0 t_C(0) the growing-mode LTB solution for zeta = mu exp(-k^2 r^2/6) (pbhgr.ltb, Yoo units k = 1, H_i = 5)
is written in the synchronous comoving slice: dl^2 = R'^2/(1+2E) dr^2 + R^2 dOmega^2, K^r_r = -R'dot/R', K^th_th = -Rdot/R,
rho = m'/(4 pi R^2 R'), u^i = 0.  The slice is mapped to the isotropic radius varrho (conformally flat, psi^2 = R/varrho,
varrho -> a(t_0) r far away, so the grid coordinate is the physical radius at t_0 and chi -> 1 in the FLRW region).
Matter proxy: real scalar field with phi = 0, Pi = sqrt(2 rho) at t_0 (rho_sf = Pi^2/2 exactly, J_i = 0 exactly), so the
LTB geometry satisfies the constraints exactly.  De Broglie map: q = m/H_ent = 1.5 m t_H, sigma_ent = sqrt6/q.

Output (--out DIR): ltb_table.dat (uniform varrho grid: varrho chi K Arr Pi), params.txt for the example, setup.json.
"""
from __future__ import annotations
import argparse, json, os, sys
import numpy as np
from scipy.interpolate import CubicSpline
from scipy.integrate import cumulative_trapezoid

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
from pbhgr.ltb import LTBGaussian

PARAMS = """# PBHCosmo (GRChombo) — e = 0 late start from LTB, generated by ltb_to_grchombo.py
# mu = {mu}, t_0 = {f0} t_C(0) = {t0_tH:.4f} t_H, a(t_0) = {a0:.3f}, m t_H = {mtH}, q = {q:.1f}, sigma_ent = {sigma:.4f}
# Yoo units k = 1: t_H = {tH:.4f}, t_C(0) = {tC0:.4f}, M_H(t_k) = 0.75 t_H = {MH:.3f}; physical r_m at t_0 = {rm_phys:.3f}
# GRChombo time t = physical time - t_0; expected first AH (1D CMC map, cold): t = {t_hor_exp:.2f} (1.1203 t_C(0) - t_0)
verbosity = 0
chk_prefix = PBH_
plot_prefix = PBHp_
checkpoint_interval = {chk_int}
plot_interval = {plot_int}
num_plot_vars = 7
plot_vars = chi lapse K rho Pi Ham Mom
hdf5_subpath = "hdf5"
pout_subpath = "pout"
data_subpath = "data"
print_progress_only_to_rank_0 = 1

G_Newton = 1.
scalar_mass = {m:.8e}
ltb_table = {table}
ltb_rho_far = {rho_far:.10e}
lineout_num_points = {lineout}
tagging_center = {c:.6f} {c:.6f} {c:.6f}
tagging_radii = {tag_radii}
tagging_rho_thr = {tag_rho}
diag_interval = {diag_int}
gauge_K_ref = {gauge_K_ref}

N_full = {N}
L_full = {L:.6f}
max_level = {max_level}
regrid_interval = {regrid}
regrid_threshold = {regrid_thr}
max_box_size = {box}
min_box_size = {box}
tag_buffer_size = 3

isPeriodic = 1 1 1
vars_parity            = 0 0 4 6 0 5 0    0 0 4 6 0 5 0    0 1 2 3    0 1 2 3 1 2 3    0 0 0
vars_parity_diagnostic = 0 1 2 3 0 0 0 0 0
num_nonzero_asymptotic_vars = 5
nonzero_asymptotic_vars = chi h11 h22 h33 lapse
nonzero_asymptotic_values = 1.0 1.0 1.0 1.0 1.0

dt_multiplier = {dtm:.12e}
use_subcycling = {subcyc}
fixed_dt = {fixed_dt:.12e}
stop_time = {stop:.4f}
max_steps = {max_steps}
max_spatial_derivative_order = 4
nan_check = 1

lapse_advec_coeff = 1.0
lapse_coeff = {lapse_coeff}
lapse_power = 1.0
shift_advec_coeff = 0.0
shift_Gamma_coeff = 0.75
eta = {eta:.6e}
formulation = 1
kappa1 = 0.
kappa2 = 0.
kappa3 = 0.
covariantZ4 = 1
sigma = 0.3
min_chi = 1.e-50
min_lapse = 1.e-50

AH_activate = {ah}
AH_num_ranks = 4
AH_num_points_u = 30
AH_num_points_v = 50
AH_solve_interval = {ah_int}
AH_print_interval = 1
AH_track_center = false
AH_predict_origin = false
AH_level_to_run = 0
AH_start_time = {ah_start:.4f}
AH_give_up_time = -1.
AH_allow_re_attempt = 1
AH_max_fails_after_lost = -1
AH_verbose = 1
AH_initial_guess = {ah_guess:.4f}
"""


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--mu", type=float, default=0.10)
    ap.add_argument("--f0", type=float, default=0.5, help="t_0 / t_C(0)")
    ap.add_argument("--mtH", type=float, default=30.0, help="scalar mass in units 1/t_H")
    ap.add_argument("--box", type=float, default=6.0, help="box side in units of the physical r_m at t_0")
    ap.add_argument("--N", type=int, default=64)
    ap.add_argument("--max_level", type=int, default=3)
    ap.add_argument("--tag_radii", type=str, default="2.0 0.8 0.35 0.18 0.09",
                    help="nested refinement spheres, level l+1 inside tag_radii[l] (units of the physical r_m at t_0)")
    ap.add_argument("--gauge_K_ref", type=int, default=1, help="1: K at the box corner (FLRW), 0: <K>")
    ap.add_argument("--subcycling", type=int, default=1, help="0: same fixed dt on all levels (larger constraint violation!)")
    ap.add_argument("--rho_thr", type=str, default="0 0 50 300",
                    help="density gate per level in units of rho_far(t_0): level l+1 only where rho > rho_thr[l] rho_far")
    ap.add_argument("--diag_interval", type=int, default=8, help="coarse steps between data_out/lineout outputs")
    ap.add_argument("--teclyn_levels", type=int, default=0, help="GRTeclyn params: amr.max_level (nested halves)")
    ap.add_argument("--teclyn_gauge", type=int, default=0, help="GRTeclyn params: 1 = weak 1+log + Gamma driver, 0 = geodesic")
    ap.add_argument("--teclyn_ah_interval", type=int, default=0, help="GRTeclyn params: AH search every N coarse steps (0 = off)")
    ap.add_argument("--lapse_coeff", type=float, default=0.3,
                    help="1+log coefficient: d_t alpha = -coeff alpha (K - fref K_far); 2 = standard (freezes the core)")
    ap.add_argument("--boxsize", type=int, default=16)
    ap.add_argument("--stop", type=float, default=0.9, help="stop at t_0 + stop * t_C(0)")
    ap.add_argument("--max_steps", type=int, default=10**7)
    ap.add_argument("--osc", type=float, default=0.2, help="m dt (0.2 = GRChombo's warning threshold)")
    ap.add_argument("--cfl", type=float, default=0.25)
    ap.add_argument("--n_table", type=int, default=16384)
    ap.add_argument("--n_r", type=int, default=20000, help="comoving grid points for the LTB slice")
    ap.add_argument("--r_max", type=float, default=8.0, help="table extent in r_m (comoving)")
    ap.add_argument("--ah", type=int, default=1)
    ap.add_argument("--ah_start", type=float, default=0.5, help="AH search from t_0 + ah_start * t_C(0)")
    ap.add_argument("--plot_every", type=float, default=0.05, help="plot interval in t_C(0)")
    ap.add_argument("--sigma_inj", type=float, default=0.0,
                    help="warm particles: 1D velocity dispersion (units of c) injected at t_inj as in the 1D code")
    ap.add_argument("--t_inj", type=float, default=1.0, help="injection time in t_H")
    ap.add_argument("--xdisp", type=float, default=2.0, help="dispersion only for shells with R/a < xdisp r_m at t_inj")
    ap.add_argument("--out", required=True)
    a = ap.parse_args()

    ltb = LTBGaussian(a.mu)
    tH, rm, ti = ltb.t_H, ltb.r_m, ltb.t_i
    tC0 = float(ltb.tC(1e-6))
    t0 = a.f0 * tC0
    a0 = (t0 / ti) ** (2.0 / 3.0)

    # --- LTB slice at t_0 on a fine comoving grid (dense near the centre, r in units of k^-1)
    r = np.linspace(1e-6, a.r_max * rm, a.n_r)          # uniform: spline second derivatives stay smooth
    R = np.array([ltb.R(t0, ri) for ri in r]); Rd = np.array([ltb.Rdot(t0, ri) for ri in r])
    E = ltb.E(r); mp = ltb.mp(r)
    sR, sRd = CubicSpline(r, R), CubicSpline(r, Rd)
    Rp, Rdp = sR(r, 1), sRd(r, 1)
    if np.any(Rp <= 0):
        raise SystemExit(f"shell crossing before t_0: min R' = {Rp.min():.3e}")
    rho = mp / (4 * np.pi * R**2 * Rp)
    Krr = -Rdp / Rp; Kth = -Rd / R
    K = Krr + 2 * Kth; Arr = (2.0 / 3.0) * (Krr - Kth)
    # --- warm particles: the 1D code injects an isotropic Maxwellian of width sigma_inj at t_inj (shell rest frame).
    # Collisionless evolution to t_0 conserves R w_t and (R'/sqrt(1+2E)) w_r for small sigma, so the local dispersions
    # at t_0 are sigma_t = sigma_inj R(t_inj)/R(t_0) and sigma_r = sigma_inj R'(t_inj)/R'(t_0); the table carries the ratios.
    sig_r = np.zeros_like(r); sig_t = np.zeros_like(r)
    if a.sigma_inj > 0:
        t_inj = a.t_inj * tH
        if not t_inj < t0:
            raise SystemExit("t_inj must be earlier than t_0")
        a_inj = (t_inj / ti) ** (2.0 / 3.0)
        Rj = np.array([ltb.R(t_inj, ri) for ri in r]); Rjp = CubicSpline(r, Rj)(r, 1)
        inside = Rj / a_inj < a.xdisp * rm
        sig_t = np.where(inside, Rj / R, 0.0); sig_r = np.where(inside, Rjp / Rp, 0.0)
    # --- isotropic radius: ln(varrho/(a0 r)) = -int_r^inf [R'/(R sqrt(1+2E)) - 1/r'] dr'
    f = Rp / (R * np.sqrt(1 + 2 * E)) - 1.0 / r
    tail = cumulative_trapezoid(f[::-1], r[::-1], initial=0.0)[::-1]      # = -int_r^{rmax} f  (rmax -> inf)
    varrho = a0 * r * np.exp(tail)
    psi2 = R / varrho; chi = 1.0 / psi2**2
    Pi = np.sqrt(2 * rho)
    # --- Hamiltonian constraint check in the isotropic form: -8 psi^-5 Lap psi + K^2 - K_ij K^ij = 16 pi rho
    psi = np.sqrt(psi2); sp = CubicSpline(varrho, psi)
    lap = sp(varrho, 2) + 2 * sp(varrho, 1) / varrho
    ham = -8 * lap / psi**5 + K**2 - (Krr**2 + 2 * Kth**2) - 16 * np.pi * rho
    ham_rel = np.abs(ham) / (16 * np.pi * rho)
    sel = varrho > 0.01 * a0 * rm
    # --- uniform table in varrho (cubic interpolation of the profiles; last row = FLRW far field)
    vr = np.linspace(0.0, varrho[-1], a.n_table)
    cols = [CubicSpline(varrho, q, extrapolate=True)(vr) for q in (chi, K, Arr, Pi)]
    chi_t, K_t, Arr_t, Pi_t = cols
    chi_t[0], Arr_t[0] = CubicSpline(varrho, chi)(0.0), 0.0
    os.makedirs(a.out, exist_ok=True)
    table = os.path.abspath(os.path.join(a.out, "ltb_table.dat"))
    hdr = (f"LTB slice mu={a.mu} t0={t0:.8e} (= {a.f0} tC0, tC0={tC0:.8e}, tH={tH:.8e}) a0={a0:.8e} k=1 H_i=5\n"
           f"columns: varrho(isotropic physical radius at t_0)  chi  K  A^rho_rho  Pi ; uniform spacing, last row = FLRW")
    np.savetxt(table, np.column_stack([vr, chi_t, K_t, Arr_t, Pi_t]), header=hdr, fmt="%.12e")
    # Vlasov (GRTeclyn PBHVlasov) table: rho (normal-observer dust density), u_r = 0 in the synchronous slice
    rho_t = 0.5 * Pi_t ** 2
    vcols = [vr, chi_t, K_t, Arr_t, rho_t, np.zeros_like(vr)]
    vhdr = hdr.replace("Pi", "rho  u_r")
    if a.sigma_inj > 0:
        vcols += [np.interp(vr, varrho, sig_r), np.interp(vr, varrho, sig_t)]   # linear: the cut at xdisp is sharp
        vhdr = vhdr.replace("rho  u_r", "rho  u_r  sigma_r/sigma_inj  sigma_t/sigma_inj")
    np.savetxt(os.path.join(a.out, "ltb_vlasov_table.dat"), np.column_stack(vcols), header=vhdr, fmt="%.12e")

    # --- grid and time-step choices
    rm_phys = float(np.interp(rm, r, varrho))
    L = a.box * rm_phys
    dx = L / a.N
    m = a.mtH / tH
    dt_osc = a.osc / m
    dt_cfl = a.cfl * dx
    dt = min(dt_osc, dt_cfl)
    dtm = dt / dx
    # oscillation-limited dt: advance all levels with the same dt (no subcycling) if the finest level's CFL allows
    dx_fine = dx / 2**a.max_level
    subcyc = 0 if (a.subcycling == 0 and dt <= a.cfl * dx_fine) else 1
    fixed_dt = dt if subcyc == 0 else -1.0
    if a.subcycling == 0 and subcyc == 1:
        print(f"WARNING: dt = {dt:.3g} exceeds the finest-level CFL step {a.cfl * dx_fine:.3g}; subcycling kept", file=sys.stderr)
    stop = a.stop * tC0
    q = 1.5 * a.mtH; sigma = np.sqrt(6.0) / q
    rho_far = 0.5 * Pi_t[-1] ** 2
    MH = 0.75 * tH
    t_hor_exp = 1.1203 * tC0 - t0
    plot_int = max(1, int(round(a.plot_every * tC0 / dt)))
    t0_tH_str = f"{t0 / tH:.4f}"
    plot_v = max(1, int(round(a.plot_every * tC0 / (a.cfl * dx / 2 ** a.max_level))))
    params = PARAMS.format(mu=a.mu, f0=a.f0, t0_tH=t0 / tH, a0=a0, mtH=a.mtH, q=q, sigma=sigma, tH=tH, tC0=tC0, MH=MH,
                           rm_phys=rm_phys, t_hor_exp=t_hor_exp, chk_int=10 * plot_int, plot_int=plot_int, m=m,
                           table=os.path.basename(table), rho_far=rho_far, lineout=3 * a.N, c=L / 2, N=a.N, L=L,
                           tag_radii=" ".join(f"{float(x) * rm_phys:.3f}" for x in a.tag_radii.split()[:max(1, a.max_level)]),
                           gauge_K_ref=a.gauge_K_ref, diag_int=a.diag_interval,
                           tag_rho=" ".join(f"{float(x) * rho_far:.6e}" for x in (a.rho_thr.split() + ["0"] * 8)[:max(1, a.max_level)]),
                           max_level=a.max_level, regrid="16 " * max(1, a.max_level), regrid_thr=50.0, box=a.boxsize,
                           dtm=dtm, subcyc=subcyc, fixed_dt=fixed_dt, stop=stop, max_steps=a.max_steps, eta=1.0 / MH, ah=a.ah,
                           ah_int=max(1, plot_int // 4), ah_start=a.ah_start * tC0, ah_guess=0.1 * rm_phys,
                           lapse_coeff=a.lapse_coeff)
    with open(os.path.join(a.out, "params.txt"), "w") as fh:
        fh.write(params)
    dt_v = a.cfl * dx / 2 ** a.max_level  # particle run: CFL-limited only (no oscillation constraint)
    grteclyn = f"""# PBHVlasov (GRTeclyn) — LTB e = 0 late start on particles, mu = {a.mu}, t_0 = {a.f0} t_C(0) = {t0_tH_str} t_H
# t_C(0) = {tC0:.6f}, t_H = {tH:.6f}, M_H = {MH:.4f}; GRTeclyn time t = physical time - t_0
amr.verbose = 1
amr.plot_int = {plot_v}
amr.check_int = {10 * plot_v}
amr.plot_vars = chi lapse K rho_p h11 h22 A11 A22 shift1
amr.n_cell = {a.N} {a.N} {a.N}
geometry.prob_extent = {L:.6f} {L:.6f} {L:.6f}
geometry.is_periodic = 1 1 1
amr.max_level = {a.teclyn_levels}
amr.regrid_int = -1
amr.max_grid_size = {a.boxsize}
amr.blocking_factor = 8
amr.n_error_buf = 0
geometry.center = {L / 2:.6f} {L / 2:.6f} {L / 2:.6f}
evolution.dt_multiplier = {a.cfl}
evolution.stop_time = {stop:.4f}
evolution.max_steps = {a.max_steps}
evolution.sigma = 0.3
# gauge: 0 = geodesic slicing (LTB comparison before the caustic), 1 = weak profile-referenced 1+log + Gamma driver
gauge.lapse_advec_coeff = {1.0 if a.teclyn_gauge else 0.0}
gauge.lapse_coeff = {a.lapse_coeff if a.teclyn_gauge else 0.0}
gauge.lapse_power = 1.0
gauge.shift_advec_coeff = 0.0
gauge.shift_Gamma_coeff = {0.75 if a.teclyn_gauge else 0.0}
gauge.eta = {1.0 / MH:.6e}
ccz4.formulation = 1
pbh_vlasov.init = table
pbh_vlasov.table = ltb_vlasov_table.dat
pbh_vlasov.n_per_dir = 2
pbh_vlasov.lattice_buffer = 4
# apparent-horizon search: every ah_interval coarse steps from t = ah_start (run time), guess radius in coordinates
pbh_vlasov.ah_interval = {a.teclyn_ah_interval}
pbh_vlasov.ah_start = {max(0.0, a.ah_start * tC0 - t0):.4f}
pbh_vlasov.ah_guess = {0.1 * rm_phys:.4f}
pbh_vlasov.ah_num_particles = 2000
pbh_vlasov.ah_max_iter = 3000
# particle proper time starts at the LTB time of the initial slice; ray expansion diagnostic every coarse step
pbh_vlasov.tau0 = {t0:.6f}
pbh_vlasov.theta_rays = 1
pbh_vlasov.theta_ray_n = 256
pbh_vlasov.theta_ray_dr = 0.5
pbh_vlasov.theta_profile_interval = 4
pbh_vlasov.max_disp_cells = 0.8
# warm particles (0 = cold): dispersion at injection; the table columns 7-8 carry sigma_r, sigma_t / sigma at t_0
pbh_vlasov.sigma = {a.sigma_inj}
pbh_vlasov.sigma_seed = 12345
# AMReX TimeIntegrator used by the AH finder flow (required parameters)
integration.type = RungeKutta
integration.rk.type = 4
ah_finder.eta = 3.0
ah_finder.c = 1.0
ah_finder.cfl_factor = 2.0
ah_finder.tolerance = 1e-4
ah_finder.r = 1.15
rho_line_extraction.enabled = true
rho_line_extraction.end = {L * (1 - 1.0 / a.N):.6f} {L / 2:.6f} {L / 2:.6f}
chi_line_extraction.enabled = true
chi_line_extraction.end = {L * (1 - 1.0 / a.N):.6f} {L / 2:.6f} {L / 2:.6f}
K_line_extraction.enabled = true
K_line_extraction.end = {L * (1 - 1.0 / a.N):.6f} {L / 2:.6f} {L / 2:.6f}
lapse_line_extraction.enabled = true
lapse_line_extraction.end = {L * (1 - 1.0 / a.N):.6f} {L / 2:.6f} {L / 2:.6f}
"""
    with open(os.path.join(a.out, "params_vlasov.txt"), "w") as fh:
        fh.write(grteclyn)
    setup = dict(mu=a.mu, f0=a.f0, t0=t0, tC0=tC0, tH=tH, ti=ti, a0=a0, rm=rm, rm_phys=rm_phys, L=L, N=a.N, dx=dx,
                 m=m, mtH=a.mtH, q=q, sigma_ent=sigma, dt=dt, dt_osc=dt_osc, dt_cfl=dt_cfl, dt_multiplier=dtm,
                 use_subcycling=subcyc, fixed_dt=fixed_dt, dx_finest=dx_fine, lapse_coeff=a.lapse_coeff,
                 n_steps_coarse=int(stop / dt), stop=stop, MH=MH, rho_far=rho_far, t_hor_expected_1D=t_hor_exp,
                 chi_centre=float(chi_t[0]), K_centre=float(K_t[0]), K_far=float(K_t[-1]), K_flrw=-2.0 / t0,
                 rho_centre_over_far=float(0.5 * Pi_t[0] ** 2 / rho_far), ham_rel_max=float(ham_rel[sel].max()),
                 table=table)
    with open(os.path.join(a.out, "setup.json"), "w") as fh:
        json.dump(setup, fh, indent=1)
    print(json.dumps(setup, indent=1))


if __name__ == "__main__":
    main()