implemented COLA with P3M force evaluation and custom time stepping for P3M

This commit is contained in:
Tristan Hoellinger 2025-06-16 15:39:16 +02:00
parent 7cb65744f3
commit 34594a2bf6
29 changed files with 69704 additions and 106986 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -436,7 +436,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"display_name": "p3m",
"language": "python",
"name": "python3"
},

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,437 @@
#!/usr/bin/env python3
# ----------------------------------------------------------------------
# Copyright (C) 2025 Tristan Hoellinger
# Distributed under the GNU General Public License v3.0 (GPLv3).
# See the LICENSE file in the root directory for details.
# SPDX-License-Identifier: GPL-3.0-or-later
# ----------------------------------------------------------------------
__author__ = "Tristan Hoellinger"
__version__ = "0.1.0"
__date__ = "2025"
__license__ = "GPLv3"
"""
Compute time step limiters from baseline (linear / logarithmic) time
step distribution.
"""
# pyright: reportWildcardImportFromLibrary=false
from os.path import isfile
from pathlib import Path
import numpy as np
from pysbmy.power import PowerSpectrum
from pysbmy.fft import FourierGrid
from pysbmy.timestepping import StandardTimeStepping
from wip3m import *
from wip3m.tools import get_k_max, generate_sim_params, generate_white_noise_Field, run_simulation
from wip3m.params import (
params_planck_kmax_missing,
cosmo_small_to_full_dict,
z2a,
BASELINE_SEEDPHASE,
)
from wip3m.plot_utils import *
from wip3m.logger import getCustomLogger, INDENT, UNINDENT
logger = getCustomLogger(__name__)
workdir = ROOT_PATH + "results/"
output_path = OUTPUT_PATH
from argparse import ArgumentParser
parser = ArgumentParser(description="Run convergence tests towards implementing P3M gravity.")
parser.add_argument("--run_id", type=str, help="Simulation run identifier.")
parser.add_argument("--L", type=float, default=128.0, help="Box size in Mpc/h.")
parser.add_argument("--N", type=int, default=256, help="Density grid size per dimension.")
parser.add_argument("--Np", type=int, default=128, help="Number of particles per dimension.")
parser.add_argument("--Npm", type=int, default=256, help="PM mesh resolution per dimension.")
parser.add_argument(
"--n_Tiles", type=int, default=32, help="Number of tiles per dimension for short-range PP."
)
parser.add_argument("--z_i", type=float, default=199.0, help="Initial redshift.")
parser.add_argument("--z_f", type=float, default=0.0, help="Final redshift.")
# Timestep settings per method
for scheme in ["pmref", "pm1", "pm2", "p3m1", "p3m2", "p3m3"]:
parser.add_argument(
f"--nsteps_{scheme}",
type=int,
default={
"pmref": 300,
"pm1": 200,
"pm2": 100,
"p3m1": 500,
"p3m2": 400,
"p3m3": 300,
}[scheme],
help=f"Number of timesteps for {scheme.upper()}.",
)
parser.add_argument(
f"--tsd_{scheme}",
type=int,
default=1,
help=f"TimeStepDistribution ID for {scheme.upper()}.",
)
parser.add_argument(
"--nsteps_p3m4",
type=int,
help="Number of timesteps for P3M4.",
)
parser.add_argument(
"--tsd_p3m4",
type=int,
default=1,
help="TimeStepDistribution ID for P3M4.",
)
parser.add_argument(
"--nsteps_p3m5",
type=int,
help="Number of timesteps for P3M5.",
)
parser.add_argument(
"--tsd_p3m5",
type=int,
default=1,
help="TimeStepDistribution ID for P3M5.",
)
args = parser.parse_args()
if __name__ == "__main__":
try:
go_beyond_Nyquist_ss = True # for the summary statistics
force = False
force_hard = False
run_id = args.run_id
L = args.L
N = args.N
Np = args.Np
Npm = args.Npm
n_Tiles = args.n_Tiles
nsteps_pmref = args.nsteps_pmref
nsteps_pm1 = args.nsteps_pm1
nsteps_pm2 = args.nsteps_pm2
nsteps_p3m1 = args.nsteps_p3m1
nsteps_p3m2 = args.nsteps_p3m2
nsteps_p3m3 = args.nsteps_p3m3
tsd_pmref = args.tsd_pmref
tsd_pm1 = args.tsd_pm1
tsd_pm2 = args.tsd_pm2
tsd_p3m1 = args.tsd_p3m1
tsd_p3m2 = args.tsd_p3m2
tsd_p3m3 = args.tsd_p3m3
if args.nsteps_p3m4 is None:
p3m4 = False
else:
p3m4 = True
nsteps_p3m4 = args.nsteps_p3m4
tsd_p3m4 = args.tsd_p3m4
if args.nsteps_p3m5 is None:
p3m5 = False
else:
p3m5 = True
nsteps_p3m5 = args.nsteps_p3m5
tsd_p3m5 = args.tsd_p3m5
RedshiftLPT = args.z_i
RedshiftFCs = args.z_f
logger.info("Running convergence tests...")
logger.info(f"Run ID: {run_id}")
logger.info(f"Box size: {L} Mpc/h")
logger.info(f"Density grid size: {N}^3")
logger.info(f"Particles per dimension: {Np}^3")
logger.info(f"PM grid size: {Npm}^3")
logger.info(f"Number of tiles: {n_Tiles}^3")
logger.info(f"Number of timesteps for PMREF: {nsteps_pmref}")
logger.info(f"Number of timesteps for PM1: {nsteps_pm1}")
logger.info(f"Number of timesteps for PM2: {nsteps_pm2}")
logger.info(f"Number of timesteps for P3M1: {nsteps_p3m1}")
logger.info(f"Number of timesteps for P3M2: {nsteps_p3m2}")
logger.info(f"Number of timesteps for P3M3: {nsteps_p3m3}")
if p3m4:
logger.info(f"Number of timesteps for P3M4: {nsteps_p3m4}")
if p3m5:
logger.info(f"Number of timesteps for P3M5: {nsteps_p3m5}")
INDENT()
corner = -L / 2.0
ai = z2a(RedshiftLPT)
af = z2a(RedshiftFCs)
k_max = get_k_max(L, N) # k_max in h/Mpc
cosmo = params_planck_kmax_missing.copy()
cosmo["k_max"] = k_max
wd = workdir + run_id + "/"
simdir = output_path + run_id + "/"
logdir = simdir + "logs/"
if force_hard:
import shutil
if Path(simdir).exists():
shutil.rmtree(simdir)
if Path(wd).exists():
shutil.rmtree(wd)
Path(wd).mkdir(parents=True, exist_ok=True)
Path(logdir).mkdir(parents=True, exist_ok=True)
input_white_noise_file = simdir + "input_white_noise.h5"
input_seed_phase_file = simdir + "seed"
ICs_path = simdir + "initial_density.h5"
simpath = simdir
# Path to the input matter power spectrum (generated later)
input_power_file = simdir + "input_power.h5"
sim_params = {
"L": L,
"N": N,
"Np": Np,
"Npm": Npm,
"n_Tiles": n_Tiles,
"RedshiftLPT": RedshiftLPT,
"RedshiftFCs": RedshiftFCs,
"nsteps_pmref": nsteps_pmref,
"nsteps_pm1": nsteps_pm1,
"nsteps_pm2": nsteps_pm2,
"nsteps_p3m1": nsteps_p3m1,
"nsteps_p3m2": nsteps_p3m2,
"nsteps_p3m3": nsteps_p3m3,
"ai": ai,
"af": af,
"cosmo": cosmo,
"tsd_pmref": tsd_pmref,
"tsd_pm1": tsd_pm1,
"tsd_pm2": tsd_pm2,
"tsd_p3m1": tsd_p3m1,
"tsd_p3m2": tsd_p3m2,
"tsd_p3m3": tsd_p3m3,
}
if p3m4:
sim_params["nsteps_p3m4"] = nsteps_p3m4
sim_params["tsd_p3m4"] = tsd_p3m4
if p3m5:
sim_params["nsteps_p3m5"] = nsteps_p3m5
sim_params["tsd_p3m5"] = tsd_p3m5
with open(wd + "sim_params.txt", "w") as f:
f.write(f"{sim_params}\n")
logger.info("Setting up simulation parameters...")
common_params = {
"Np": Np,
"N": N,
"L": L,
"corner0": corner,
"corner1": corner,
"corner2": corner,
"h": cosmo["h"],
"Omega_m": cosmo["Omega_m"],
"Omega_b": cosmo["Omega_b"],
"n_s": cosmo["n_s"],
"sigma8": cosmo["sigma8"],
}
lpt_params = common_params.copy()
lpt_params["method"] = "lpt"
lpt_params["InputPowerSpectrum"] = input_power_file
lpt_params["ICsMode"] = 1
lpt_params["InputWhiteNoise"] = input_white_noise_file
common_params_num = common_params.copy()
common_params_num["ai"] = ai
common_params_num["af"] = af
common_params_num["RedshiftLPT"] = RedshiftLPT
common_params_num["RedshiftFCs"] = RedshiftFCs
common_params_num["Npm"] = Npm
common_params_num["RunForceDiagnostic"] = False
common_params_num["nBinsForceDiagnostic"] = 20
common_params_num["nPairsForceDiagnostic"] = 3
common_params_num["maxTrialsForceDiagnostic"] = int(1e8)
common_params_num["OutputForceDiagnostic"] = simdir + "force_diagnostic.txt"
pmref_params = common_params_num.copy()
pmref_params["method"] = "pm"
pmref_params["TimeStepDistribution"] = tsd_pmref
pmref_params["nsteps"] = nsteps_pmref
pm1_params = common_params_num.copy()
pm1_params["method"] = "pm"
pm1_params["TimeStepDistribution"] = tsd_pm1
pm1_params["nsteps"] = nsteps_pm1
pm2_params = common_params_num.copy()
pm2_params["method"] = "pm"
pm2_params["TimeStepDistribution"] = tsd_pm2
pm2_params["nsteps"] = nsteps_pm2
p3m1_params = common_params_num.copy()
p3m1_params["method"] = "p3m"
p3m1_params["EvolutionMode"] = 4
p3m1_params["TimeStepDistribution"] = tsd_p3m1
p3m1_params["nsteps"] = nsteps_p3m1
p3m1_params["n_Tiles"] = n_Tiles
p3m1_params["PrintOutputTimestepsLog"] = True
p3m1_params["OutputTimestepsLog"] = simdir + "p3m1_timestep_log.txt"
p3m2_params = common_params_num.copy()
p3m2_params["method"] = "p3m"
p3m2_params["EvolutionMode"] = 4
p3m2_params["TimeStepDistribution"] = tsd_p3m2
p3m2_params["nsteps"] = nsteps_p3m2
p3m2_params["n_Tiles"] = n_Tiles
p3m2_params["PrintOutputTimestepsLog"] = True
p3m2_params["OutputTimestepsLog"] = simdir + "p3m2_timestep_log.txt"
p3m3_params = common_params_num.copy()
p3m3_params["method"] = "p3m"
p3m3_params["EvolutionMode"] = 4
p3m3_params["TimeStepDistribution"] = tsd_p3m3
p3m3_params["nsteps"] = nsteps_p3m3
p3m3_params["n_Tiles"] = n_Tiles
p3m3_params["PrintOutputTimestepsLog"] = True
p3m3_params["OutputTimestepsLog"] = simdir + "p3m3_timestep_log.txt"
if p3m4:
p3m4_params = common_params_num.copy()
p3m4_params["method"] = "p3m"
p3m4_params["EvolutionMode"] = 4
p3m4_params["TimeStepDistribution"] = tsd_p3m4
p3m4_params["nsteps"] = nsteps_p3m4
p3m4_params["n_Tiles"] = n_Tiles
p3m4_params["PrintOutputTimestepsLog"] = True
p3m4_params["OutputTimestepsLog"] = simdir + "p3m4_timestep_log.txt"
if p3m5:
p3m5_params = common_params_num.copy()
p3m5_params["method"] = "p3m"
p3m5_params["EvolutionMode"] = 4
p3m5_params["TimeStepDistribution"] = tsd_p3m5
p3m5_params["nsteps"] = nsteps_p3m5
p3m5_params["n_Tiles"] = n_Tiles
p3m5_params["PrintOutputTimestepsLog"] = True
p3m5_params["OutputTimestepsLog"] = simdir + "p3m5_timestep_log.txt"
logger.info("Setting up simulation parameters done.")
logger.info("Generating simulation parameters...")
INDENT()
reset_plotting() # Default style for Simbelmynë
generate_sim_params(lpt_params, ICs_path, wd, simdir, None, force)
all_sim_params = [
("pmref", pmref_params),
("pm1", pm1_params),
("pm2", pm2_params),
("p3m1", p3m1_params),
("p3m2", p3m2_params),
("p3m3", p3m3_params),
]
if p3m4:
all_sim_params.append(("p3m4", p3m4_params))
if p3m5:
all_sim_params.append(("p3m5", p3m5_params))
for name, parameters in all_sim_params:
logger.info(
f"Generating parameters for {name.upper()} with nsteps = {parameters['nsteps']}..."
)
file_ext = f"{name}_nsteps{parameters['nsteps']}"
generate_sim_params(parameters, ICs_path, wd, simdir, file_ext, force)
logger.info(f"Generating parameters for {name.upper()} done.")
UNINDENT()
logger.info("Generating simulation parameters done.")
setup_plotting() # Reset plotting style for this project
logger.info("Generating white noise field...")
generate_white_noise_Field(
L=L,
size=N,
corner=corner,
seedphase=BASELINE_SEEDPHASE,
fname_whitenoise=input_white_noise_file,
seedname_whitenoise=input_seed_phase_file,
force_phase=force,
)
logger.info("Generating white noise field done.")
# If cosmo["WhichSpectrum"] == "class", then classy is required.
if not isfile(input_power_file) or force:
logger.info("Generating input power spectrum...")
Pk = PowerSpectrum(L, L, L, N, N, N, cosmo_small_to_full_dict(cosmo))
Pk.write(input_power_file)
logger.info("Generating input power spectrum done.")
logger.info("Generating Fourier grid...")
# k grid used to compute the final overdensity power spectrum
Pinit = 100
trim_threshold = 100 # Merge bins until this minimum number of modes per bin is reached
log_kmin = np.log10(2 * np.pi / (np.sqrt(3) * L)) # Minimum non-zero k in h/Mpc
if go_beyond_Nyquist_ss:
k_max_ss = get_k_max(L, N)
else:
k_max_ss = get_k_max(L, N) / np.sqrt(3) # 1D Nyquist frequency
Pbins_left_bnds = np.logspace(log_kmin, np.log10(k_max_ss), Pinit + 1, dtype=np.float32)
Pbins_left_bnds = Pbins_left_bnds[:-1]
input_ss_file = simdir + "input_ss_k_grid.h5"
Gk = FourierGrid(
L,
L,
L,
N,
N,
N,
k_modes=Pbins_left_bnds,
kmax=k_max_ss,
trim_bins=True,
trim_threshold=trim_threshold,
)
Gk.write(input_ss_file)
logger.info("Generating Fourier grid done.")
logger.info("Running simulations...")
INDENT()
logger.info("Running LPT simulation...")
run_simulation("lpt", lpt_params, wd, logdir)
logger.info("Running LPT simulation done.")
for name, parameters in all_sim_params:
logger.info(f"Running {name.upper()} simulation...")
run_simulation(name, parameters, wd, logdir)
logger.info(f"Running {name.upper()} simulation done.")
UNINDENT()
logger.info("All simulations done.")
logger.info("Plotting time step diagnostics...")
INDENT()
for name, parameters in all_sim_params:
if name.startswith("p3m") or name.startswith("spm"):
file_ext = f"{name}_nsteps{parameters['nsteps']}"
method = parameters["method"]
aiDrift = StandardTimeStepping.read(wd + file_ext + f"_ts_{method}.h5").aiDrift
plot_timestepping_diagnostics(
log_path=parameters["OutputTimestepsLog"],
aiDrift=aiDrift,
TimeStepDistribution=parameters["TimeStepDistribution"],
nsteps=parameters["nsteps"],
save_path=wd+"time_step_diagnostics.pdf"
)
UNINDENT()
logger.info("Plotting time step diagnostics done.")
except OSError as e:
logger.error("Directory or file access error: %s", str(e))
raise
except Exception as e:
logger.critical("An unexpected error occurred: %s", str(e))
raise RuntimeError("Failed.") from e
finally:
UNINDENT()
logger.info("Running convergence tests done.")

View file

@ -22,6 +22,7 @@ import numpy as np
from pysbmy.power import PowerSpectrum
from pysbmy.fft import FourierGrid
from pysbmy.timestepping import StandardTimeStepping
from wip3m import *
from wip3m.tools import get_k_max, generate_sim_params, generate_white_noise_Field, run_simulation
@ -52,6 +53,8 @@ parser.add_argument("--Npm", type=int, default=64, help="PM mesh resolution per
parser.add_argument(
"--n_Tiles", type=int, default=8, help="Number of tiles per dimension for short-range PP."
)
parser.add_argument("--z_i", type=float, default=19.0, help="Initial redshift.")
parser.add_argument("--z_f", type=float, default=0.0, help="Final redshift.")
# Timestep settings per method
for scheme in ["pmref", "pm1", "pm2", "cola", "spm", "p3m1", "p3m2", "p3m3"]:
@ -107,6 +110,8 @@ if __name__ == "__main__":
tsd_p3m1 = args.tsd_p3m1
tsd_p3m2 = args.tsd_p3m2
tsd_p3m3 = args.tsd_p3m3
RedshiftLPT = args.z_i
RedshiftFCs = args.z_f
logger.info("Running convergence tests...")
logger.info(f"Run ID: {run_id}")
logger.info(f"Box size: {L} Mpc/h")
@ -125,8 +130,6 @@ if __name__ == "__main__":
INDENT()
corner = -L / 2.0
RedshiftLPT = 19.0
RedshiftFCs = 0.0
ai = z2a(RedshiftLPT)
af = z2a(RedshiftFCs)
k_max = get_k_max(L, N) # k_max in h/Mpc
@ -242,6 +245,8 @@ if __name__ == "__main__":
spm_params["TimeStepDistribution"] = tsd_spm
spm_params["nsteps"] = nsteps_spm
spm_params["n_Tiles"] = n_Tiles
spm_params["PrintOutputTimestepsLog"] = True
spm_params["OutputTimestepsLog"] = simdir + "spm_timestep_log.txt"
p3m1_params = common_params_num.copy()
p3m1_params["method"] = "p3m"
@ -249,6 +254,8 @@ if __name__ == "__main__":
p3m1_params["TimeStepDistribution"] = tsd_p3m1
p3m1_params["nsteps"] = nsteps_p3m1
p3m1_params["n_Tiles"] = n_Tiles
p3m1_params["PrintOutputTimestepsLog"] = True
p3m1_params["OutputTimestepsLog"] = simdir + "p3m1_timestep_log.txt"
p3m2_params = common_params_num.copy()
p3m2_params["method"] = "p3m"
@ -256,6 +263,8 @@ if __name__ == "__main__":
p3m2_params["TimeStepDistribution"] = tsd_p3m2
p3m2_params["nsteps"] = nsteps_p3m2
p3m2_params["n_Tiles"] = n_Tiles
p3m2_params["PrintOutputTimestepsLog"] = True
p3m2_params["OutputTimestepsLog"] = simdir + "p3m2_timestep_log.txt"
p3m3_params = common_params_num.copy()
p3m3_params["method"] = "p3m"
@ -263,6 +272,8 @@ if __name__ == "__main__":
p3m3_params["TimeStepDistribution"] = tsd_p3m3
p3m3_params["nsteps"] = nsteps_p3m3
p3m3_params["n_Tiles"] = n_Tiles
p3m3_params["PrintOutputTimestepsLog"] = True
p3m3_params["OutputTimestepsLog"] = simdir + "p3m3_timestep_log.txt"
logger.info("Setting up simulation parameters done.")
@ -281,12 +292,12 @@ if __name__ == "__main__":
("p3m2", p3m2_params),
("p3m3", p3m3_params),
]
for name, params in all_sim_params:
for name, parameters in all_sim_params:
logger.info(
f"Generating parameters for {name.upper()} with nsteps = {params['nsteps']}..."
f"Generating parameters for {name.upper()} with nsteps = {parameters['nsteps']}..."
)
file_ext = f"{name}_nsteps{params['nsteps']}"
generate_sim_params(params, ICs_path, wd, simdir, file_ext, force)
file_ext = f"{name}_nsteps{parameters['nsteps']}"
generate_sim_params(parameters, ICs_path, wd, simdir, file_ext, force)
logger.info(f"Generating parameters for {name.upper()} done.")
UNINDENT()
@ -353,6 +364,24 @@ if __name__ == "__main__":
UNINDENT()
logger.info("All simulations done.")
logger.info("Plotting time step diagnostics...")
INDENT()
for name, parameters in all_sim_params:
if name.startswith("p3m") or name.startswith("spm"):
file_ext = f"{name}_nsteps{parameters['nsteps']}"
method = parameters["method"]
aiDrift = StandardTimeStepping.read(wd + file_ext + f"_ts_{method}.h5").aiDrift
plot_timestepping_diagnostics(
log_path=parameters["OutputTimestepsLog"],
aiDrift=aiDrift,
TimeStepDistribution=parameters["TimeStepDistribution"],
nsteps=parameters["nsteps"],
save_path=wd+"time_step_diagnostics.pdf"
)
UNINDENT()
logger.info("Plotting time step diagnostics done.")
except OSError as e:
logger.error("Directory or file access error: %s", str(e))
raise

View file

@ -0,0 +1,918 @@
#!/usr/bin/env python3
# ----------------------------------------------------------------------
# Copyright (C) 2025 Tristan Hoellinger
# Distributed under the GNU General Public License v3.0 (GPLv3).
# See the LICENSE file in the root directory for details.
# SPDX-License-Identifier: GPL-3.0-or-later
# ----------------------------------------------------------------------
__author__ = "Tristan Hoellinger"
__version__ = "0.1.0"
__date__ = "2025"
__license__ = "GPLv3"
"""
Perform time step convergence tests with custom time step distributions,
using COLA with different force evaluations (PM, sPM, P3M).
"""
# pyright: reportWildcardImportFromLibrary=false
from os.path import isfile
from pathlib import Path
import numpy as np
from pysbmy.power import PowerSpectrum
from pysbmy.fft import FourierGrid
from pysbmy.timestepping import StandardTimeStepping, P3MTimeStepping
from wip3m import *
from wip3m.tools import none_bool_str, get_k_max, generate_sim_params, generate_white_noise_Field, run_simulation
from wip3m.params import (
params_planck_kmax_missing,
cosmo_small_to_full_dict,
z2a,
BASELINE_SEEDPHASE,
)
from wip3m.plot_utils import *
from wip3m.logger import getCustomLogger, INDENT, UNINDENT
logger = getCustomLogger(__name__)
workdir = ROOT_PATH + "results/"
output_path = OUTPUT_PATH
from argparse import ArgumentParser
parser = ArgumentParser(description="Run convergence tests towards implementing P3M gravity.")
parser.add_argument("--run_id", type=str, help="Simulation run identifier.")
parser.add_argument("--L", type=float, default=32.0, help="Box size in Mpc/h.")
parser.add_argument("--N", type=int, default=128, help="Density grid size per dimension.")
parser.add_argument("--Np", type=int, default=32, help="Number of particles per dimension.")
parser.add_argument("--Npm", type=int, default=64, help="PM mesh resolution per dimension.")
parser.add_argument(
"--n_Tiles", type=int, default=8, help="Number of tiles per dimension for short-range PP."
)
parser.add_argument("--z_i", type=float, default=199.0, help="Initial redshift.")
parser.add_argument("--z_f", type=float, default=19.0, help="Final redshift.")
parser.add_argument(
"--plot_fields",
type=none_bool_str,
default=False,
help="Whether to plot the fields or not.",
)
parser.add_argument(
"--lpt",
type=none_bool_str,
default=False,
help="Whether to compute and plot the LPT power spectrum.",
)
parser.add_argument(
"--scale_limiter",
type=none_bool_str,
default=False,
help="Which limiter should be scaled by the scaling arguments? ",
)
parser.add_argument(
"--only_plot",
type=none_bool_str,
default=False,
help="Do not re-run simulations. Warning: it is up to the user to ensure that the required files are present.",
)
# Timestep settings per method
for scheme in ["pmref", "pm1", "pm2", "spm", "p3m1", "p3m2", "p3m3"]:
parser.add_argument(
f"--scaling_{scheme}",
type=float,
default={
"pmref": 1.,
"pm1": 0.95,
"pm2": 0.9,
"spm": 1.,
"p3m1": 1.,
"p3m2": 0.95,
"p3m3": 0.9,
}[scheme],
help=f"Relax the step limiter by this factor for {scheme.upper()}.",
)
args = parser.parse_args()
if __name__ == "__main__":
try:
go_beyond_Nyquist_ss = True # for the summary statistics
force = False
force_hard = False
only_plot = args.only_plot
run_id = args.run_id
L = args.L
N = args.N
Np = args.Np
Npm = args.Npm
n_Tiles = args.n_Tiles
scaling_pmref = args.scaling_pmref
scaling_pm1 = args.scaling_pm1
scaling_pm2 = args.scaling_pm2
scaling_spm = args.scaling_spm
scaling_p3m1 = args.scaling_p3m1
scaling_p3m2 = args.scaling_p3m2
scaling_p3m3 = args.scaling_p3m3
name_scaled = args.scale_limiter
if name_scaled is not False and name_scaled not in [
"fac_dyn_custom",
"da_max_early_custom",
"fac_H_custom",
"fac_bend",
"sub_bend1",
"sub_bend2",
"da_max_late",
]:
raise ValueError(
f"Invalid name_scaled: {name_scaled}. Must be one of the limiter names or False."
)
RedshiftLPT = args.z_i
RedshiftFCs = args.z_f
logger.info("Running convergence tests...")
logger.info(f"Run ID: {run_id}")
logger.info(f"Box size: {L} Mpc/h")
logger.info(f"Density grid size: {N}^3")
logger.info(f"Particles per dimension: {Np}^3")
logger.info(f"PM grid size: {Npm}^3")
logger.info(f"Number of tiles: {n_Tiles}^3")
logger.info(f"Limiter to scale: {name_scaled if name_scaled else 'None'}")
if name_scaled:
logger.info(f"Limiter rescaling for PMREF: {scaling_pmref}")
logger.info(f"Limiter rescaling for PM1: {scaling_pm1}")
logger.info(f"Limiter rescaling for PM2: {scaling_pm2}")
logger.info(f"Limiter rescaling for sPM: {scaling_spm}")
logger.info(f"Limiter rescaling for P3M1: {scaling_p3m1}")
logger.info(f"Limiter rescaling for P3M2: {scaling_p3m2}")
logger.info(f"Limiter rescaling for P3M3: {scaling_p3m3}")
INDENT()
lpt = args.lpt
plot_fields = args.plot_fields
corner = -L / 2.0
ai = z2a(RedshiftLPT)
af = z2a(RedshiftFCs)
k_max = get_k_max(L, N) # k_max in h/Mpc
cosmo = params_planck_kmax_missing.copy()
cosmo["k_max"] = k_max
wd = workdir + run_id + "/"
simdir = output_path + run_id + "/"
logdir = simdir + "logs/"
outdir = simdir + "plots/"
if force_hard:
import shutil
if Path(simdir).exists():
shutil.rmtree(simdir)
if Path(wd).exists():
shutil.rmtree(wd)
Path(wd).mkdir(parents=True, exist_ok=True)
Path(logdir).mkdir(parents=True, exist_ok=True)
Path(outdir).mkdir(parents=True, exist_ok=True)
input_white_noise_file = simdir + "input_white_noise.h5"
input_seed_phase_file = simdir + "seed"
ICs_path = simdir + "initial_density.h5"
simpath = simdir
# Path to the input matter power spectrum (generated later)
input_power_file = simdir + "input_power.h5"
sim_params = {
"L": L,
"N": N,
"Np": Np,
"Npm": Npm,
"n_Tiles": n_Tiles,
"RedshiftLPT": RedshiftLPT,
"RedshiftFCs": RedshiftFCs,
"name_scaled": name_scaled,
"scaling_pmref": scaling_pmref,
"scaling_pm1": scaling_pm1,
"scaling_pm2": scaling_pm2,
"scaling_spm": scaling_spm,
"scaling_p3m1": scaling_p3m1,
"scaling_p3m2": scaling_p3m2,
"scaling_p3m3": scaling_p3m3,
}
with open(wd + "sim_params.txt", "w") as f:
f.write(f"{sim_params}\n")
logger.info("Setting up simulation parameters...")
common_params = {
"Np": Np,
"N": N,
"L": L,
"corner0": corner,
"corner1": corner,
"corner2": corner,
"h": cosmo["h"],
"Omega_m": cosmo["Omega_m"],
"Omega_b": cosmo["Omega_b"],
"n_s": cosmo["n_s"],
"sigma8": cosmo["sigma8"],
}
lpt_params = common_params.copy()
lpt_params["method"] = "lpt"
lpt_params["InputPowerSpectrum"] = input_power_file
lpt_params["ICsMode"] = 1
lpt_params["InputWhiteNoise"] = input_white_noise_file
fac_dyn_custom = DEFAULT_FAC_DYN_CUSTOM
da_early = DEFAULT_DA_MAX_EARLY_CUSTOM
fac_H_custom = DEFAULT_FAC_H_CUSTOM
fac_bend = DEFAULT_FAC_BEND
sub_bend1 = DEFAULT_SUB_BEND1_COLA
sub_bend2 = DEFAULT_SUB_BEND2_COLA
da_late = DEFAULT_DA_MAX_LATE_CUSTOM
limiters = {
"fac_dyn_custom": fac_dyn_custom,
"da_max_early_custom": da_early,
"fac_H_custom": fac_H_custom,
"fac_bend": fac_bend,
"sub_bend1": sub_bend1,
"sub_bend2": sub_bend2,
"da_max_late": da_late,
}
common_params_num = common_params.copy()
common_params_num["ai"] = ai
common_params_num["af"] = af
common_params_num["RedshiftLPT"] = RedshiftLPT
common_params_num["RedshiftFCs"] = RedshiftFCs
common_params_num["Npm"] = Npm
common_params_num["RunForceDiagnostic"] = False
common_params_num["nBinsForceDiagnostic"] = 20
common_params_num["nPairsForceDiagnostic"] = 3
common_params_num["maxTrialsForceDiagnostic"] = int(1e8)
common_params_num["OutputForceDiagnostic"] = simdir + "force_diagnostic.txt"
common_params_num["TimeStepDistribution"] = 3 # Custom time step distribution
common_params_num["cosmo_dict"] = cosmo
common_params_num["fac_dyn_custom"] = limiters["fac_dyn_custom"]
common_params_num["da_max_early_custom"] = limiters["da_max_early_custom"]
common_params_num["fac_H_custom"] = limiters["fac_H_custom"]
common_params_num["fac_bend"] = limiters["fac_bend"]
common_params_num["sub_bend1"] = limiters["sub_bend1"]
common_params_num["sub_bend2"] = limiters["sub_bend2"]
common_params_num["da_max_late_custom"] = limiters["da_max_late"]
pmref_params = common_params_num.copy()
pmref_params["method"] = "cola" # COLA+PM
pmref_params["relax"] = scaling_pmref # plot unscaled values during the simulation
if name_scaled:
pmref_params[name_scaled] /= scaling_pmref
pm1_params = common_params_num.copy()
pm1_params["method"] = "cola" # COLA+PM
pm1_params["relax"] = scaling_pm1
if name_scaled:
pm1_params[name_scaled] /= scaling_pm1
pm2_params = common_params_num.copy()
pm2_params["method"] = "cola" # COLA+PM
pm2_params["relax"] = scaling_pm2
if name_scaled:
pm2_params[name_scaled] /= scaling_pm2
spm_params = common_params_num.copy()
spm_params["method"] = "spm"
spm_params["EvolutionMode"] = 6 # COLA+sPM
spm_params["n_Tiles"] = n_Tiles
spm_params["PrintOutputTimestepsLog"] = True
spm_params["OutputTimestepsLog"] = simdir + "colaspm_timestep_log.txt"
spm_params["relax"] = scaling_spm
if name_scaled:
spm_params[name_scaled] /= scaling_spm
p3m1_params = common_params_num.copy()
p3m1_params["method"] = "p3m"
p3m1_params["EvolutionMode"] = 7 # COLA+P3M
p3m1_params["n_Tiles"] = n_Tiles
p3m1_params["PrintOutputTimestepsLog"] = True
p3m1_params["OutputTimestepsLog"] = simdir + "colap3m1_timestep_log.txt"
p3m1_params["relax"] = scaling_p3m1
if name_scaled:
p3m1_params[name_scaled] /= scaling_p3m1
p3m2_params = common_params_num.copy()
p3m2_params["method"] = "p3m"
p3m2_params["EvolutionMode"] = 7 # COLA+P3M
p3m2_params["n_Tiles"] = n_Tiles
p3m2_params["PrintOutputTimestepsLog"] = True
p3m2_params["OutputTimestepsLog"] = simdir + "colap3m2_timestep_log.txt"
p3m2_params["relax"] = scaling_p3m2
if name_scaled:
p3m2_params[name_scaled] /= scaling_p3m2
p3m3_params = common_params_num.copy()
p3m3_params["method"] = "p3m"
p3m3_params["EvolutionMode"] = 7 # COLA+P3M
p3m3_params["n_Tiles"] = n_Tiles
p3m3_params["PrintOutputTimestepsLog"] = True
p3m3_params["OutputTimestepsLog"] = simdir + "colap3m3_timestep_log.txt"
p3m3_params["relax"] = scaling_p3m3
if name_scaled:
p3m3_params[name_scaled] /= scaling_p3m3
logger.info("Setting up simulation parameters done.")
if not only_plot:
logger.info("Generating simulation parameters...")
INDENT()
reset_plotting() # Default style for Simbelmynë
generate_sim_params(lpt_params, ICs_path, wd, simdir, None, force)
all_sim_params = [
("pmref", pmref_params),
("pm1", pm1_params),
("pm2", pm2_params),
("spm", spm_params),
("p3m1", p3m1_params),
("p3m2", p3m2_params),
("p3m3", p3m3_params),
]
for name, parameters in all_sim_params:
logger.info(
f"Generating parameters for {name.upper()} with relax = {parameters['relax']}..."
)
file_ext = f"{name}_relax{str(parameters['relax']).replace('.', '_')}"
generate_sim_params(parameters, ICs_path, wd, simdir, file_ext, force)
logger.info(f"Generating parameters for {name.upper()} done.")
UNINDENT()
logger.info("Generating simulation parameters done.")
setup_plotting() # Reset plotting style for this project
logger.info("Generating white noise field...")
generate_white_noise_Field(
L=L,
size=N,
corner=corner,
seedphase=BASELINE_SEEDPHASE,
fname_whitenoise=input_white_noise_file,
seedname_whitenoise=input_seed_phase_file,
force_phase=force,
)
logger.info("Generating white noise field done.")
# If cosmo["WhichSpectrum"] == "class", then classy is required.
if not isfile(input_power_file) or force:
logger.info("Generating input power spectrum...")
Pk = PowerSpectrum(L, L, L, N, N, N, cosmo_small_to_full_dict(cosmo))
Pk.write(input_power_file)
logger.info("Generating input power spectrum done.")
logger.info("Generating Fourier grid...")
# k grid used to compute the final overdensity power spectrum
Pinit = 100
trim_threshold = 100 # Merge bins until this minimum number of modes per bin is reached
log_kmin = np.log10(2 * np.pi / (np.sqrt(3) * L)) # Minimum non-zero k in h/Mpc
if go_beyond_Nyquist_ss:
k_max_ss = get_k_max(L, N)
else:
k_max_ss = get_k_max(L, N) / np.sqrt(3) # 1D Nyquist frequency
Pbins_left_bnds = np.logspace(log_kmin, np.log10(k_max_ss), Pinit + 1, dtype=np.float32)
Pbins_left_bnds = Pbins_left_bnds[:-1]
input_ss_file = simdir + "input_ss_k_grid.h5"
Gk = FourierGrid(
L,
L,
L,
N,
N,
N,
k_modes=Pbins_left_bnds,
kmax=k_max_ss,
trim_bins=True,
trim_threshold=trim_threshold,
)
Gk.write(input_ss_file)
logger.info("Generating Fourier grid done.")
logger.info("Running simulations...")
INDENT()
logger.info("Running LPT simulation...")
run_simulation("lpt", lpt_params, wd, logdir)
logger.info("Running LPT simulation done.")
for name, parameters in all_sim_params:
logger.info(f"Running {name.upper()} simulation...")
run_simulation(name, parameters, wd, logdir)
logger.info(f"Running {name.upper()} simulation done.")
UNINDENT()
logger.info("All simulations done.")
logger.info("Plotting time step diagnostics...")
INDENT()
nsteps_dict = {}
for name, parameters in all_sim_params:
TimeStepDistribution = parameters["TimeStepDistribution"]
file_ext = f"{name}_relax{str(parameters['relax']).replace('.', '_')}"
method = parameters["method"]
TSpath = wd + file_ext + f"_ts_{method}.h5" if file_ext else wd + f"ts_{method}.h5"
if TimeStepDistribution in [0, 1, 2]:
TS = StandardTimeStepping.read(TSpath)
aiDrift = TS.aiDrift
nsteps = TS.nsteps
elif TimeStepDistribution == 3:
TS = P3MTimeStepping.read(TSpath)
aiDrift = TS.aiDrift
nsteps = TS.nsteps
else:
raise ValueError(f"Invalid TimeStepDistribution: {TimeStepDistribution}")
nsteps_dict[name] = nsteps
if name.startswith("p3m") or name.startswith("spm"):
plot_timestepping_diagnostics(
log_path=parameters["OutputTimestepsLog"],
aiDrift=aiDrift,
TimeStepDistribution=TimeStepDistribution,
nsteps=nsteps,
ymin=2e-3,
exact=False,
save_path=outdir + f"time_step_diagnostics_{name}.pdf",
)
plot_custom_timestepping_diagnostics(
log_path=parameters["OutputTimestepsLog"],
aiDrift=aiDrift,
TimeStepDistribution=TimeStepDistribution,
nsteps=nsteps,
ymin=2e-3,
fac_hubble=fac_H_custom,
fac_bend=fac_bend,
da_max_early=da_early,
da_max_late=DEFAULT_DA_MAX_LATE_CUSTOM,
save_path=outdir + f"time_step_diagnostics_custom_{name}.pdf",
)
UNINDENT()
logger.info("Plotting time step diagnostics done.")
logger.info("Computing and plotting power spectra...")
INDENT()
from pysbmy.fft import read_FourierGrid
from pysbmy.field import read_field
from pysbmy.correlations import get_autocorrelation
logger.info("Loading Fourier grid...")
G = read_FourierGrid(input_ss_file)
logger.info("Loading Fourier grid done.")
k = G.k_modes
AliasingCorr = False
slice_ijk = (N // 2, slice(None), slice(None)) # for plot_fields
logger.info("Computing power spectra...")
DELTA = read_field(simdir + "initial_density.h5")
Pk_INI, _ = get_autocorrelation(DELTA, G, AliasingCorr)
if lpt:
DELTA = read_field(simdir + "lpt_density.h5")
Pk_LPT, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"pmref_relax{str(pmref_params['relax']).replace('.', '_')}_final_density_cola.h5")
if plot_fields:
delta_pmref = DELTA.data[slice_ijk]
Pk_PMref, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"pm1_relax{str(pm1_params['relax']).replace('.', '_')}_final_density_cola.h5")
if plot_fields:
delta_pm1 = DELTA.data[slice_ijk]
Pk_PM1, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"pm2_relax{str(pm2_params['relax']).replace('.', '_')}_final_density_cola.h5")
if plot_fields:
delta_pm2 = DELTA.data[slice_ijk]
Pk_PM2, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"spm_relax{str(spm_params['relax']).replace('.', '_')}_final_density_spm.h5")
if plot_fields:
delta_spm = DELTA.data[slice_ijk]
Pk_sPM, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"p3m1_relax{str(p3m1_params['relax']).replace('.', '_')}_final_density_p3m.h5")
if plot_fields:
delta_p3m1 = DELTA.data[slice_ijk]
Pk_P3M1, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"p3m2_relax{str(p3m2_params['relax']).replace('.', '_')}_final_density_p3m.h5")
if plot_fields:
delta_p3m2 = DELTA.data[slice_ijk]
Pk_P3M2, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"p3m3_relax{str(p3m3_params['relax']).replace('.', '_')}_final_density_p3m.h5")
if plot_fields:
delta_p3m3 = DELTA.data[slice_ijk]
Pk_P3M3, _ = get_autocorrelation(DELTA, G, AliasingCorr)
logger.info("Computing power spectra done.")
ref = "P3M1"
scaling_reference = scaling_p3m1
Pk_ref = Pk_P3M1
label_ref = f"P3M, scaling={scaling_reference}, ns={nsteps_dict['p3m1']}"
logger.info(f"Plotting power spectra...")
INDENT()
fig, ax = plt.subplots(figsize=(7, 4))
ax.set_xscale("log")
k = G.k_modes
kmin, kmax = k.min(), k.max()
logger.info(f"kmin = {kmin:.2e}, kmax = {kmax:.2e}")
log_pad = 0.02
log_k_min = np.log10(kmin)
log_k_max = np.log10(kmax)
log_range = log_k_max - log_k_min
xlim_min = 10 ** (log_k_min - log_pad * log_range)
xlim_max = 10 ** (log_k_max + log_pad * log_range)
plt.xlim(xlim_min, xlim_max)
# ax.set_ylim([0.2, 1.8])
dark_grey_bnd = 0.01
medium_grey_bnd = 0.05
light_grey_bnd = 0.1
ax.plot([kmin, kmax], [1, 1], color="black", linestyle="-", label=label_ref)
if lpt:
ax.plot(k, Pk_LPT / Pk_ref, label="2LPT", linestyle="--")
fields_to_plot = [
("PMref", Pk_PMref),
("PM1", Pk_PM1),
("PM2", Pk_PM2),
("sPM", Pk_sPM),
("P3M1", Pk_P3M1),
("P3M2", Pk_P3M2),
("P3M3", Pk_P3M3),
]
# Remove the field corresponding to the reference:
fields_to_plot = [
(field_name, Pk) for field_name, Pk in fields_to_plot if field_name != ref
]
for field_name, Pk in fields_to_plot:
label = f"{field_name}, scaling={eval(f'scaling_{field_name.lower()}')}, ns={nsteps_dict[field_name.lower()]}"
if field_name in ["PMref", "P3M1"]:
linestyle = "-"
zorder = 1
else:
linestyle = "--"
zorder = 2
ax.plot(k, Pk / Pk_ref, label=label, linestyle=linestyle)
# ax.axhspan(1 - dark_grey_bnd, 1 + dark_grey_bnd, color="grey", alpha=0.3)
# ax.axhspan(1 - medium_grey_bnd, 1 + medium_grey_bnd, color="grey", alpha=0.2)
# ax.axhspan(1 - light_grey_bnd, 1 + light_grey_bnd, color="grey", alpha=0.1)
ax.axhspan(1 - dark_grey_bnd, 1 + dark_grey_bnd, color="grey", alpha=0.3)
ax.axhline(1 - medium_grey_bnd, color="grey", linestyle="-", linewidth=1)
ax.axhline(1 + medium_grey_bnd, color="grey", linestyle="-", linewidth=1)
for i in range(1, len(k)):
ax.axvline(k[i], color="black", linestyle=":", linewidth=1, alpha=0.1, zorder=0)
ax.yaxis.set_major_locator(plt.MaxNLocator(6))
ax.yaxis.get_major_ticks()[0].label1.set_visible(False)
ax.set_xlabel("$k$ [$h/\\mathrm{Mpc}$]", fontsize=fs)
ax.set_ylabel("$P(k)/P_\\mathrm{ref}(k)$", fontsize=fs)
ax.tick_params(which="both", direction="in")
ax.tick_params(axis="both", which="major", labelsize=fs)
ax.tick_params(axis="both", which="minor", labelsize=fs)
# Characteristic vertical reference scales
nyquist = np.pi * N / L
nyquist_PM = np.pi * Npm / L
epsilon = 0.03 * L / Np
particle_length = 2 * epsilon
xs = 1.25 * L / Npm
xr = 4.5 * xs
particle_wavenumber = 2 * np.pi / particle_length # Too large to be shown
xs_inv = 2 * np.pi / xs
xr_inv = 2 * np.pi / xr
if nyquist <= xlim_max:
line1 = ax.axvline(
x=nyquist, color="black", linestyle="--", lw=2, label="Nyquist (density grid)", zorder=0
)
if nyquist_PM <= xlim_max:
line2 = ax.axvline(
x=nyquist_PM, color="black", linestyle="-", lw=1, label="Nyquist (PM grid)", zorder=0
)
if xs_inv <= xlim_max:
line3 = ax.axvline(
x=xs_inv, color="gray", linestyle="-.", lw=2, label=r"Split wavenumber $x_s$", zorder=0
)
if xr_inv <= xlim_max:
line4 = ax.axvline(
x=xr_inv, color="gray", linestyle=":", lw=2, label=r"Short-range reach $x_r$", zorder=0
)
logger.info(f"Nyquist (density grid): {nyquist:.2f} h/Mpc")
logger.info(f"Nyquist (PM grid): {nyquist_PM:.2f} h/Mpc")
logger.info(f"Particle wavenumber: {particle_wavenumber:.2f} h/Mpc")
logger.info(f"Split wavenumber: {xs_inv:.2f} h/Mpc")
logger.info(f"Short-range reach: {xr_inv:.2f} h/Mpc")
handles, labels = plt.gca().get_legend_handles_labels()
# empty_patch = mpatches.Patch(color="none", label="")
# handles = [empty_patch, *handles]
# labels = ["", *labels]
plt.legend(
handles,
labels,
loc="upper center",
ncol=2,
bbox_to_anchor=(0.5, -0.2),
fontsize=fs,
frameon=False,
)
fig.savefig(
outdir + "power_spectra.png",
bbox_inches="tight",
dpi=300,
transparent=True,
)
fig.savefig(
outdir + "power_spectra.pdf",
bbox_inches="tight",
dpi=300,
)
plt.close(fig)
UNINDENT()
logger.info(
f"Power spectra plotted and saved to {outdir}power_spectra.png and power_spectra.pdf"
)
UNINDENT()
logger.info("Plotting power spectra done.")
logger.info(f"Plotting P3M power spectra...")
INDENT()
fig, ax = plt.subplots(figsize=(7, 4))
ax.set_xscale("log")
k = G.k_modes
kmin, kmax = k.min(), k.max()
logger.info(f"kmin = {kmin:.2e}, kmax = {kmax:.2e}")
log_pad = 0.02
log_k_min = np.log10(kmin)
log_k_max = np.log10(kmax)
log_range = log_k_max - log_k_min
xlim_min = 10 ** (log_k_min - log_pad * log_range)
xlim_max = 10 ** (log_k_max + log_pad * log_range)
plt.xlim(xlim_min, xlim_max)
# ax.set_ylim([0.2, 1.8])
dark_grey_bnd = 0.01
medium_grey_bnd = 0.05
light_grey_bnd = 0.1
ax.plot([kmin, kmax], [1, 1], color="black", linestyle="-", label=label_ref)
fields_to_plot = [
("P3M1", Pk_P3M1),
("P3M2", Pk_P3M2),
("P3M3", Pk_P3M3),
]
# Remove the field corresponding to the reference:
fields_to_plot = [
(field_name, Pk) for field_name, Pk in fields_to_plot if field_name != ref
]
for field_name, Pk in fields_to_plot:
label = f"{field_name}, scaling={eval(f'scaling_{field_name.lower()}')}, ns={nsteps_dict[field_name.lower()]}"
if field_name in ["PMref", "P3M1"]:
linestyle = "-"
zorder = 1
else:
linestyle = "--"
zorder = 2
ax.plot(k, Pk / Pk_ref, label=label, linestyle=linestyle)
ax.axhspan(1 - dark_grey_bnd, 1 + dark_grey_bnd, color="grey", alpha=0.3)
ax.axhline(1 - medium_grey_bnd, color="grey", linestyle="-", linewidth=1)
ax.axhline(1 + medium_grey_bnd, color="grey", linestyle="-", linewidth=1)
for i in range(1, len(k)):
ax.axvline(k[i], color="black", linestyle=":", linewidth=1, alpha=0.1, zorder=0)
ax.yaxis.set_major_locator(plt.MaxNLocator(6))
ax.yaxis.get_major_ticks()[0].label1.set_visible(False)
ax.set_xlabel("$k$ [$h/\\mathrm{Mpc}$]", fontsize=fs)
ax.set_ylabel("$P(k)/P_\\mathrm{ref}(k)$", fontsize=fs)
ax.tick_params(which="both", direction="in")
ax.tick_params(axis="both", which="major", labelsize=fs)
ax.tick_params(axis="both", which="minor", labelsize=fs)
# Characteristic vertical reference scales
nyquist = np.pi * N / L
nyquist_PM = np.pi * Npm / L
epsilon = 0.03 * L / Np
particle_length = 2 * epsilon
xs = 1.25 * L / Npm
xr = 4.5 * xs
particle_wavenumber = 2 * np.pi / particle_length # Too large to be shown
xs_inv = 2 * np.pi / xs
xr_inv = 2 * np.pi / xr
if nyquist <= xlim_max:
line1 = ax.axvline(
x=nyquist, color="black", linestyle="--", lw=2, label="Nyquist (density grid)", zorder=0
)
if nyquist_PM <= xlim_max:
line2 = ax.axvline(
x=nyquist_PM, color="black", linestyle="-", lw=1, label="Nyquist (PM grid)", zorder=0
)
if xs_inv <= xlim_max:
line3 = ax.axvline(
x=xs_inv, color="gray", linestyle="-.", lw=2, label=r"Split wavenumber $x_s$", zorder=0
)
if xr_inv <= xlim_max:
line4 = ax.axvline(
x=xr_inv, color="gray", linestyle=":", lw=2, label=r"Short-range reach $x_r$", zorder=0
)
logger.info(f"Nyquist (density grid): {nyquist:.2f} h/Mpc")
logger.info(f"Nyquist (PM grid): {nyquist_PM:.2f} h/Mpc")
logger.info(f"Particle wavenumber: {particle_wavenumber:.2f} h/Mpc")
logger.info(f"Split wavenumber: {xs_inv:.2f} h/Mpc")
logger.info(f"Short-range reach: {xr_inv:.2f} h/Mpc")
handles, labels = plt.gca().get_legend_handles_labels()
plt.legend(
handles,
labels,
loc="upper center",
ncol=2,
bbox_to_anchor=(0.5, -0.2),
fontsize=fs,
frameon=False,
)
fig.savefig(
outdir + "p3m_power_spectra.png",
bbox_inches="tight",
dpi=300,
transparent=True,
)
fig.savefig(
outdir + "p3m_power_spectra.pdf",
bbox_inches="tight",
dpi=300,
)
plt.close(fig)
UNINDENT()
logger.info(
f"P3M power spectra plotted and saved to {outdir}p3m_power_spectra.png and power_spectra.pdf"
)
logger.info("Pplotting P3M power spectra done.")
UNINDENT()
logger.info("Computing and plotting power spectra done.")
if plot_fields:
logger.info("Reading lpt field...")
delta_lpt = read_field(simdir + "lpt_density.h5").data[slice_ijk]
logger.info("Reading lpt field done.")
diff_pm1_pmref = delta_pm1 - delta_pmref
diff_pm2_pmref = delta_pm2 - delta_pmref
diff_p3m1_pmref = delta_p3m1 - delta_pmref
diff_p3m2_p3m1 = delta_p3m2 - delta_p3m1
diff_p3m3_p3m1 = delta_p3m3 - delta_p3m1
diff_p3m1_spm = delta_p3m1 - delta_spm
fields = [
"pmref",
"pm1",
"pm2",
"p3m1",
"p3m2",
"p3m3",
"diff_p3m1_spm",
"diff_pm1_pmref",
"diff_pm2_pmref",
"diff_p3m1_pmref",
"diff_p3m2_p3m1",
"diff_p3m3_p3m1",
] # fields to plot
ncols = 6 # Max panels per row
figname = "_".join(fields)
slices_dict = {
"lpt": delta_lpt,
"pmref": delta_pmref,
"pm1": delta_pm1,
"pm2": delta_pm2,
"spm": delta_spm,
"p3m1": delta_p3m1,
"p3m2": delta_p3m2,
"p3m3": delta_p3m3,
"diff_pm1_pmref": diff_pm1_pmref,
"diff_pm2_pmref": diff_pm2_pmref,
"diff_p3m1_pmref": diff_p3m1_pmref,
"diff_p3m2_p3m1": diff_p3m2_p3m1,
"diff_p3m3_p3m1": diff_p3m3_p3m1,
"diff_p3m1_spm": diff_p3m1_spm,
}
titles_dict = {
"lpt": "LPT",
"pmref": f"PMref relax={scaling_pmref}",
"pm1": f"PM1 relax={scaling_pm1}",
"pm2": f"PM2 relax={scaling_pm2}",
"spm": f"sPM relax={scaling_spm}",
"p3m1": f"P3M1 relax={scaling_p3m1}",
"p3m2": f"P3M2 relax={scaling_p3m2}",
"p3m3": f"P3M3 relax={scaling_p3m3}",
"diff_pm1_pmref": r"$\delta_{\rm PM1}-\delta_{\rm PMref}$",
"diff_pm2_pmref": r"$\delta_{\rm PM2}-\delta_{\rm PMref}$",
"diff_p3m1_pmref": r"$\delta_{\rm P3M1}-\delta_{\rm PMref}$",
"diff_p3m2_p3m1": r"$\delta_{\rm P3M2}-\delta_{\rm P3M1}$",
"diff_p3m3_p3m1": r"$\delta_{\rm P3M3}-\delta_{\rm P3M1}$",
"diff_p3m1_spm": r"$\delta_{\rm P3M1}-\delta_{\rm sPM}$",
}
logger.info(f"Plotting fields: {fields}...")
npanels = len(fields)
nrows = np.ceil(npanels / ncols).astype(int)
fig, axs = plt.subplots(
nrows=nrows, ncols=ncols, figsize=(3 * ncols, 4.6 * nrows), sharey=True
)
axs = axs.flatten()
ims = []
for i, key in enumerate(fields):
ax = axs[i]
data = slices_dict[key]
title = titles_dict[key]
if key.startswith("diff"):
vmin = -np.log(1 + np.abs(np.min(data)))
vmax = np.log10(1 + np.abs(np.max(data)))
if vmax > 0 > vmin:
norm = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
im = ax.imshow(
np.sign(data) * np.log(1 + np.abs(data)), cmap="RdBu_r", norm=norm
)
else:
im = ax.imshow(np.log10(1 + np.abs(data)), cmap="RdBu_r")
else:
im = ax.imshow(np.log10(2 + data), cmap=cmap)
ims.append((im, key))
ax.set_title(title, fontsize=fs_titles)
for spine in ax.spines.values():
spine.set_visible(False)
# Hide unused axes
for i in range(npanels, len(axs)):
axs[i].axis("off")
# Shared axes labels
for i, ax in enumerate(axs[:npanels]):
if i % ncols == 0:
ax.set_yticks([0, N // 2, N])
ax.set_yticklabels([f"{-L/2:.0f}", "0", f"{L/2:.0f}"], fontsize=fs)
ax.set_ylabel(r"Mpc/$h$", size=GLOBAL_FS_SMALL)
ax.set_xticks([0, N // 2, N])
ax.set_xticklabels([f"{-L/2:.0f}", "0", f"{L/2:.0f}"], fontsize=fs)
ax.set_xlabel(r"Mpc/$h$", size=GLOBAL_FS_SMALL)
# Colorbars
for ax, (im, key) in zip(axs[:npanels], ims):
divider = make_axes_locatable(ax)
cax = divider.append_axes("bottom", size="5%", pad=0.6)
cb = fig.colorbar(im, cax=cax, orientation="horizontal")
if key.startswith("diff"):
cb.set_label(
r"$\textrm{sgn}\left(\Delta\delta\right)\log_{10}(1 + |\Delta\delta|)$",
fontsize=fs,
)
else:
cb.set_label(r"$\log_{10}(2 + \delta)$", fontsize=fs)
cb.ax.tick_params(labelsize=fs)
cax.xaxis.set_ticks_position("bottom")
cax.xaxis.set_label_position("bottom")
fig.savefig(outdir + f"{figname}.png", bbox_inches="tight", dpi=300, transparent=True)
fig.savefig(outdir + f"{figname}.pdf", bbox_inches="tight", dpi=300)
plt.close(fig)
logger.info(f"Plotting fields done. Saved to {outdir}{figname}.png and {figname}.pdf")
logger.info("All plots completed successfully.")
except OSError as e:
logger.error("Directory or file access error: %s", str(e))
raise
except Exception as e:
logger.critical("An unexpected error occurred: %s", str(e))
raise RuntimeError("Failed.") from e
finally:
UNINDENT()
logger.info("Running convergence tests done.")

View file

@ -0,0 +1,718 @@
#!/usr/bin/env python3
# ----------------------------------------------------------------------
# Copyright (C) 2025 Tristan Hoellinger
# Distributed under the GNU General Public License v3.0 (GPLv3).
# See the LICENSE file in the root directory for details.
# SPDX-License-Identifier: GPL-3.0-or-later
# ----------------------------------------------------------------------
__author__ = "Tristan Hoellinger"
__version__ = "0.1.0"
__date__ = "2025"
__license__ = "GPLv3"
"""
Perform time step convergence tests with custom time step distributions,
using COLA with different force evaluations (PM, sPM, P3M).
"""
# pyright: reportWildcardImportFromLibrary=false
from os.path import isfile
from pathlib import Path
import numpy as np
from pysbmy.power import PowerSpectrum
from pysbmy.fft import FourierGrid
from pysbmy.timestepping import StandardTimeStepping, P3MTimeStepping
from wip3m import *
from wip3m.tools import none_bool_str, get_k_max, generate_sim_params, generate_white_noise_Field, run_simulation
from wip3m.params import (
params_planck_kmax_missing,
cosmo_small_to_full_dict,
z2a,
BASELINE_SEEDPHASE,
)
from wip3m.plot_utils import *
from wip3m.logger import getCustomLogger, INDENT, UNINDENT
logger = getCustomLogger(__name__)
workdir = ROOT_PATH + "results/"
output_path = OUTPUT_PATH
from argparse import ArgumentParser
parser = ArgumentParser(description="Run convergence tests towards implementing P3M gravity.")
parser.add_argument("--run_id", type=str, help="Simulation run identifier.")
parser.add_argument("--L", type=float, default=32.0, help="Box size in Mpc/h.")
parser.add_argument("--N", type=int, default=128, help="Density grid size per dimension.")
parser.add_argument("--Np", type=int, default=32, help="Number of particles per dimension.")
parser.add_argument("--Npm", type=int, default=64, help="PM mesh resolution per dimension.")
parser.add_argument(
"--n_Tiles", type=int, default=8, help="Number of tiles per dimension for short-range PP."
)
parser.add_argument("--z_i", type=float, default=199.0, help="Initial redshift.")
parser.add_argument("--z_f", type=float, default=19.0, help="Final redshift.")
parser.add_argument(
"--plot_fields",
type=none_bool_str,
default=False,
help="Whether to plot the fields or not.",
)
parser.add_argument(
"--lpt",
type=none_bool_str,
default=False,
help="Whether to compute and plot the LPT power spectrum.",
)
parser.add_argument(
"--scale_limiter",
type=none_bool_str,
default=False,
help="Which limiter should be scaled by the scaling arguments? ",
)
parser.add_argument(
"--use_p3m_fit",
type=none_bool_str,
default=False,
help="Whether to use the fitted P3M limiter or the bend limiter.",
)
parser.add_argument(
"--only_plot",
type=none_bool_str,
default=False,
help="Do not re-run simulations. Warning: it is up to the user to ensure that the required files are present.",
)
# Timestep settings per method
for scheme in ["p3m1", "p3m2", "p3m3"]:
parser.add_argument(
f"--scaling_{scheme}",
type=float,
default={
"p3m1": 1.,
"p3m2": 0.95,
"p3m3": 0.9,
}[scheme],
help=f"Relax the step limiter by this factor for {scheme.upper()}.",
)
args = parser.parse_args()
if __name__ == "__main__":
try:
go_beyond_Nyquist_ss = True # for the summary statistics
force = False
force_hard = False
only_plot = args.only_plot
run_id = args.run_id
L = args.L
N = args.N
Np = args.Np
Npm = args.Npm
n_Tiles = args.n_Tiles
scaling_p3m1 = args.scaling_p3m1
scaling_p3m2 = args.scaling_p3m2
scaling_p3m3 = args.scaling_p3m3
name_scaled = args.scale_limiter
use_p3m_fit = args.use_p3m_fit
if name_scaled is not False and name_scaled not in [
"fac_dyn_custom",
"da_max_early_custom",
"fac_H_custom",
"fac_bend",
"sub_bend1",
"sub_bend2",
"fac_p3m_fit",
"da_max_late",
]:
raise ValueError(
f"Invalid name_scaled: {name_scaled}. Must be one of the limiter names or False."
)
RedshiftLPT = args.z_i
RedshiftFCs = args.z_f
logger.info("Running convergence tests...")
logger.info(f"Run ID: {run_id}")
logger.info(f"Box size: {L} Mpc/h")
logger.info(f"Density grid size: {N}^3")
logger.info(f"Particles per dimension: {Np}^3")
logger.info(f"PM grid size: {Npm}^3")
logger.info(f"Number of tiles: {n_Tiles}^3")
logger.info(f"Limiter to scale: {name_scaled if name_scaled else 'None'}")
if name_scaled:
logger.info(f"Limiter rescaling for P3M1: {scaling_p3m1}")
logger.info(f"Limiter rescaling for P3M2: {scaling_p3m2}")
logger.info(f"Limiter rescaling for P3M3: {scaling_p3m3}")
INDENT()
lpt = args.lpt
plot_fields = args.plot_fields
corner = -L / 2.0
ai = z2a(RedshiftLPT)
af = z2a(RedshiftFCs)
k_max = get_k_max(L, N) # k_max in h/Mpc
cosmo = params_planck_kmax_missing.copy()
cosmo["k_max"] = k_max
wd = workdir + run_id + "/"
simdir = output_path + run_id + "/"
logdir = simdir + "logs/"
outdir = simdir + "plots/"
if force_hard:
import shutil
if Path(simdir).exists():
shutil.rmtree(simdir)
if Path(wd).exists():
shutil.rmtree(wd)
Path(wd).mkdir(parents=True, exist_ok=True)
Path(logdir).mkdir(parents=True, exist_ok=True)
Path(outdir).mkdir(parents=True, exist_ok=True)
input_white_noise_file = simdir + "input_white_noise.h5"
input_seed_phase_file = simdir + "seed"
ICs_path = simdir + "initial_density.h5"
simpath = simdir
# Path to the input matter power spectrum (generated later)
input_power_file = simdir + "input_power.h5"
input_ss_file = simdir + "input_ss_k_grid.h5"
sim_params = {
"L": L,
"N": N,
"Np": Np,
"Npm": Npm,
"n_Tiles": n_Tiles,
"RedshiftLPT": RedshiftLPT,
"RedshiftFCs": RedshiftFCs,
"name_scaled": name_scaled,
"scaling_p3m1": scaling_p3m1,
"scaling_p3m2": scaling_p3m2,
"scaling_p3m3": scaling_p3m3,
}
with open(wd + "sim_params.txt", "w") as f:
f.write(f"{sim_params}\n")
logger.info("Setting up simulation parameters...")
common_params = {
"Np": Np,
"N": N,
"L": L,
"corner0": corner,
"corner1": corner,
"corner2": corner,
"h": cosmo["h"],
"Omega_m": cosmo["Omega_m"],
"Omega_b": cosmo["Omega_b"],
"n_s": cosmo["n_s"],
"sigma8": cosmo["sigma8"],
}
lpt_params = common_params.copy()
lpt_params["method"] = "lpt"
lpt_params["InputPowerSpectrum"] = input_power_file
lpt_params["ICsMode"] = 1
lpt_params["InputWhiteNoise"] = input_white_noise_file
fac_dyn_custom = DEFAULT_FAC_DYN_CUSTOM
da_early = DEFAULT_DA_MAX_EARLY_CUSTOM
fac_H_custom = DEFAULT_FAC_H_CUSTOM
fac_bend = DEFAULT_FAC_BEND
sub_bend1 = DEFAULT_SUB_BEND1_COLA
sub_bend2 = DEFAULT_SUB_BEND2_COLA
fac_p3m_fit = DEFAULT_FAC_P3M_FIT
da_late = DEFAULT_DA_MAX_LATE_CUSTOM
limiters = {
"fac_dyn_custom": fac_dyn_custom,
"da_max_early_custom": da_early,
"fac_H_custom": fac_H_custom,
"fac_bend": fac_bend,
"sub_bend1": sub_bend1,
"sub_bend2": sub_bend2,
"fac_p3m_fit": fac_p3m_fit,
"da_max_late": da_late,
}
if use_p3m_fit:
if L == 2 * Np:
p3m_fit_coeffs = P3M_FIT_COEFFS_DEFAULT_2NP
elif L == Np:
p3m_fit_coeffs = P3M_FIT_COEFFS_DEFAULT_NP
else:
raise ValueError(
f"Invalid box size L={L}. Must be either 2*Np={2*Np} or Np={Np} when using the fitted P3M limiter."
)
else:
p3m_fit_coeffs = None
common_params_num = common_params.copy()
common_params_num["ai"] = ai
common_params_num["af"] = af
common_params_num["RedshiftLPT"] = RedshiftLPT
common_params_num["RedshiftFCs"] = RedshiftFCs
common_params_num["Npm"] = Npm
common_params_num["RunForceDiagnostic"] = False
common_params_num["nBinsForceDiagnostic"] = 20
common_params_num["nPairsForceDiagnostic"] = 3
common_params_num["maxTrialsForceDiagnostic"] = int(1e8)
common_params_num["OutputForceDiagnostic"] = simdir + "force_diagnostic.txt"
common_params_num["TimeStepDistribution"] = 3 # Custom time step distribution
common_params_num["cosmo_dict"] = cosmo
common_params_num["fac_dyn_custom"] = limiters["fac_dyn_custom"]
common_params_num["da_max_early_custom"] = limiters["da_max_early_custom"]
common_params_num["fac_H_custom"] = limiters["fac_H_custom"]
common_params_num["fac_bend"] = limiters["fac_bend"]
common_params_num["sub_bend1"] = limiters["sub_bend1"]
common_params_num["sub_bend2"] = limiters["sub_bend2"]
common_params_num["fac_p3m_fit"] = limiters["fac_p3m_fit"]
common_params_num["da_max_late_custom"] = limiters["da_max_late"]
common_params_num["p3m_fit_coeffs"] = p3m_fit_coeffs
common_params_num["use_p3m_fit"] = use_p3m_fit
p3m1_params = common_params_num.copy()
p3m1_params["method"] = "p3m"
p3m1_params["EvolutionMode"] = 7 # COLA+P3M
p3m1_params["n_Tiles"] = n_Tiles
p3m1_params["PrintOutputTimestepsLog"] = True
p3m1_params["OutputTimestepsLog"] = simdir + "colap3m1_timestep_log.txt"
p3m1_params["relax"] = scaling_p3m1
if name_scaled:
p3m1_params[name_scaled] /= scaling_p3m1
p3m2_params = common_params_num.copy()
p3m2_params["method"] = "p3m"
p3m2_params["EvolutionMode"] = 7 # COLA+P3M
p3m2_params["n_Tiles"] = n_Tiles
p3m2_params["PrintOutputTimestepsLog"] = True
p3m2_params["OutputTimestepsLog"] = simdir + "colap3m2_timestep_log.txt"
p3m2_params["relax"] = scaling_p3m2
if name_scaled:
p3m2_params[name_scaled] /= scaling_p3m2
p3m3_params = common_params_num.copy()
p3m3_params["method"] = "p3m"
p3m3_params["EvolutionMode"] = 7 # COLA+P3M
p3m3_params["n_Tiles"] = n_Tiles
p3m3_params["PrintOutputTimestepsLog"] = True
p3m3_params["OutputTimestepsLog"] = simdir + "colap3m3_timestep_log.txt"
p3m3_params["relax"] = scaling_p3m3
if name_scaled:
p3m3_params[name_scaled] /= scaling_p3m3
all_sim_params = [
("p3m1", p3m1_params),
("p3m2", p3m2_params),
("p3m3", p3m3_params),
]
logger.info("Setting up simulation parameters done.")
if not only_plot:
logger.info("Generating simulation parameters...")
INDENT()
reset_plotting() # Default style for Simbelmynë
generate_sim_params(lpt_params, ICs_path, wd, simdir, None, force)
for name, parameters in all_sim_params:
logger.info(
f"Generating parameters for {name.upper()} with relax = {parameters['relax']}..."
)
file_ext = f"{name}_relax{str(parameters['relax']).replace('.', '_')}"
generate_sim_params(parameters, ICs_path, wd, simdir, file_ext, force)
logger.info(f"Generating parameters for {name.upper()} done.")
UNINDENT()
logger.info("Generating simulation parameters done.")
setup_plotting() # Reset plotting style for this project
logger.info("Generating white noise field...")
generate_white_noise_Field(
L=L,
size=N,
corner=corner,
seedphase=BASELINE_SEEDPHASE,
fname_whitenoise=input_white_noise_file,
seedname_whitenoise=input_seed_phase_file,
force_phase=force,
)
logger.info("Generating white noise field done.")
# If cosmo["WhichSpectrum"] == "class", then classy is required.
if not isfile(input_power_file) or force:
logger.info("Generating input power spectrum...")
Pk = PowerSpectrum(L, L, L, N, N, N, cosmo_small_to_full_dict(cosmo))
Pk.write(input_power_file)
logger.info("Generating input power spectrum done.")
logger.info("Generating Fourier grid...")
# k grid used to compute the final overdensity power spectrum
Pinit = 100
trim_threshold = 100 # Merge bins until this minimum number of modes per bin is reached
log_kmin = np.log10(2 * np.pi / (np.sqrt(3) * L)) # Minimum non-zero k in h/Mpc
if go_beyond_Nyquist_ss:
k_max_ss = get_k_max(L, N)
else:
k_max_ss = get_k_max(L, N) / np.sqrt(3) # 1D Nyquist frequency
Pbins_left_bnds = np.logspace(log_kmin, np.log10(k_max_ss), Pinit + 1, dtype=np.float32)
Pbins_left_bnds = Pbins_left_bnds[:-1]
Gk = FourierGrid(
L,
L,
L,
N,
N,
N,
k_modes=Pbins_left_bnds,
kmax=k_max_ss,
trim_bins=True,
trim_threshold=trim_threshold,
)
Gk.write(input_ss_file)
logger.info("Generating Fourier grid done.")
logger.info("Running simulations...")
INDENT()
logger.info("Running LPT simulation...")
run_simulation("lpt", lpt_params, wd, logdir)
logger.info("Running LPT simulation done.")
for name, parameters in all_sim_params:
logger.info(f"Running {name.upper()} simulation...")
run_simulation(name, parameters, wd, logdir)
logger.info(f"Running {name.upper()} simulation done.")
UNINDENT()
logger.info("All simulations done.")
logger.info("Plotting time step diagnostics...")
INDENT()
nsteps_dict = {}
for name, parameters in all_sim_params:
TimeStepDistribution = parameters["TimeStepDistribution"]
file_ext = f"{name}_relax{str(parameters['relax']).replace('.', '_')}"
method = parameters["method"]
TSpath = wd + file_ext + f"_ts_{method}.h5" if file_ext else wd + f"ts_{method}.h5"
if TimeStepDistribution in [0, 1, 2]:
TS = StandardTimeStepping.read(TSpath)
aiDrift = TS.aiDrift
nsteps = TS.nsteps
elif TimeStepDistribution == 3:
TS = P3MTimeStepping.read(TSpath)
aiDrift = TS.aiDrift
nsteps = TS.nsteps
else:
raise ValueError(f"Invalid TimeStepDistribution: {TimeStepDistribution}")
nsteps_dict[name] = nsteps
if name.startswith("p3m") or name.startswith("spm"):
plot_timestepping_diagnostics(
log_path=parameters["OutputTimestepsLog"],
aiDrift=aiDrift,
TimeStepDistribution=TimeStepDistribution,
nsteps=nsteps,
ymin=3e-3,
exact=False,
save_path=outdir + f"time_step_diagnostics_{name}.pdf",
)
plot_custom_timestepping_diagnostics(
log_path=parameters["OutputTimestepsLog"],
aiDrift=aiDrift,
TimeStepDistribution=TimeStepDistribution,
nsteps=nsteps,
ymin=3e-3,
fac_hubble=fac_H_custom,
fac_bend=fac_bend,
plot_bend=not use_p3m_fit,
da_max_early=da_early,
da_max_late=DEFAULT_DA_MAX_LATE_CUSTOM,
save_path=outdir + f"time_step_diagnostics_custom_{name}.pdf",
)
UNINDENT()
logger.info("Plotting time step diagnostics done.")
logger.info("Computing and plotting power spectra...")
INDENT()
from pysbmy.fft import read_FourierGrid
from pysbmy.field import read_field
from pysbmy.correlations import get_autocorrelation
logger.info("Loading Fourier grid...")
G = read_FourierGrid(input_ss_file)
logger.info("Loading Fourier grid done.")
k = G.k_modes
AliasingCorr = False
slice_ijk = (N // 2, slice(None), slice(None)) # for plot_fields
logger.info("Computing power spectra...")
DELTA = read_field(simdir + "initial_density.h5")
Pk_INI, _ = get_autocorrelation(DELTA, G, AliasingCorr)
if lpt:
DELTA = read_field(simdir + "lpt_density.h5")
Pk_LPT, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"p3m1_relax{str(p3m1_params['relax']).replace('.', '_')}_final_density_p3m.h5")
if plot_fields:
delta_p3m1 = DELTA.data[slice_ijk]
Pk_P3M1, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"p3m2_relax{str(p3m2_params['relax']).replace('.', '_')}_final_density_p3m.h5")
if plot_fields:
delta_p3m2 = DELTA.data[slice_ijk]
Pk_P3M2, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"p3m3_relax{str(p3m3_params['relax']).replace('.', '_')}_final_density_p3m.h5")
if plot_fields:
delta_p3m3 = DELTA.data[slice_ijk]
Pk_P3M3, _ = get_autocorrelation(DELTA, G, AliasingCorr)
logger.info("Computing power spectra done.")
ref = "P3M1"
scaling_reference = scaling_p3m1
Pk_ref = Pk_P3M1
label_ref = f"P3M, scaling={scaling_reference}, ns={nsteps_dict['p3m1']}"
logger.info(f"Plotting power spectra...")
INDENT()
fig, ax = plt.subplots(figsize=(7, 4))
ax.set_xscale("log")
k = G.k_modes
kmin, kmax = k.min(), k.max()
logger.info(f"kmin = {kmin:.2e}, kmax = {kmax:.2e}")
log_pad = 0.02
log_k_min = np.log10(kmin)
log_k_max = np.log10(kmax)
log_range = log_k_max - log_k_min
xlim_min = 10 ** (log_k_min - log_pad * log_range)
xlim_max = 10 ** (log_k_max + log_pad * log_range)
plt.xlim(xlim_min, xlim_max)
# ax.set_ylim([0.2, 1.8])
dark_grey_bnd = 0.01
medium_grey_bnd = 0.05
light_grey_bnd = 0.1
ax.plot([kmin, kmax], [1, 1], color="black", linestyle="-", label=label_ref)
fields_to_plot = [
("P3M1", Pk_P3M1),
("P3M2", Pk_P3M2),
("P3M3", Pk_P3M3),
]
# Remove the field corresponding to the reference:
fields_to_plot = [
(field_name, Pk) for field_name, Pk in fields_to_plot if field_name != ref
]
for field_name, Pk in fields_to_plot:
label = f"{field_name}, scaling={eval(f'scaling_{field_name.lower()}')}, ns={nsteps_dict[field_name.lower()]}"
linestyle = "--"
zorder = 2
ax.plot(k, Pk / Pk_ref, label=label, linestyle=linestyle)
ax.axhspan(1 - dark_grey_bnd, 1 + dark_grey_bnd, color="grey", alpha=0.3)
ax.axhline(1 - medium_grey_bnd, color="grey", linestyle="-", linewidth=1)
ax.axhline(1 + medium_grey_bnd, color="grey", linestyle="-", linewidth=1)
for i in range(1, len(k)):
ax.axvline(k[i], color="black", linestyle=":", linewidth=1, alpha=0.1, zorder=0)
ax.yaxis.set_major_locator(plt.MaxNLocator(6))
ax.yaxis.get_major_ticks()[0].label1.set_visible(False)
ax.set_xlabel("$k$ [$h/\\mathrm{Mpc}$]", fontsize=fs)
ax.set_ylabel("$P(k)/P_\\mathrm{ref}(k)$", fontsize=fs)
ax.tick_params(which="both", direction="in")
ax.tick_params(axis="both", which="major", labelsize=fs)
ax.tick_params(axis="both", which="minor", labelsize=fs)
# Characteristic vertical reference scales
nyquist = np.pi * N / L
nyquist_PM = np.pi * Npm / L
epsilon = 0.03 * L / Np
particle_length = 2 * epsilon
xs = 1.25 * L / Npm
xr = 4.5 * xs
particle_wavenumber = 2 * np.pi / particle_length # Too large to be shown
xs_inv = 2 * np.pi / xs
xr_inv = 2 * np.pi / xr
if nyquist <= xlim_max:
line1 = ax.axvline(
x=nyquist, color="black", linestyle="--", lw=2, label="Nyquist (density grid)", zorder=0
)
if nyquist_PM <= xlim_max:
line2 = ax.axvline(
x=nyquist_PM, color="black", linestyle="-", lw=1, label="Nyquist (PM grid)", zorder=0
)
if xs_inv <= xlim_max:
line3 = ax.axvline(
x=xs_inv, color="gray", linestyle="-.", lw=2, label=r"Split wavenumber $x_s$", zorder=0
)
if xr_inv <= xlim_max:
line4 = ax.axvline(
x=xr_inv, color="gray", linestyle=":", lw=2, label=r"Short-range reach $x_r$", zorder=0
)
logger.info(f"Nyquist (density grid): {nyquist:.2f} h/Mpc")
logger.info(f"Nyquist (PM grid): {nyquist_PM:.2f} h/Mpc")
logger.info(f"Particle wavenumber: {particle_wavenumber:.2f} h/Mpc")
logger.info(f"Split wavenumber: {xs_inv:.2f} h/Mpc")
logger.info(f"Short-range reach: {xr_inv:.2f} h/Mpc")
handles, labels = plt.gca().get_legend_handles_labels()
plt.legend(
handles,
labels,
loc="upper center",
ncol=2,
bbox_to_anchor=(0.5, -0.2),
fontsize=fs,
frameon=False,
)
fig.savefig(
outdir + "p3m_power_spectra.png",
bbox_inches="tight",
dpi=300,
transparent=True,
)
fig.savefig(
outdir + "p3m_power_spectra.pdf",
bbox_inches="tight",
dpi=300,
)
plt.close(fig)
UNINDENT()
logger.info(
f"P3M power spectra plotted and saved to {outdir}p3m_power_spectra.png and power_spectra.pdf"
)
logger.info("Pplotting P3M power spectra done.")
UNINDENT()
logger.info("Computing and plotting power spectra done.")
if plot_fields:
logger.info("Reading lpt field...")
delta_lpt = read_field(simdir + "lpt_density.h5").data[slice_ijk]
logger.info("Reading lpt field done.")
diff_p3m2_p3m1 = delta_p3m2 - delta_p3m1
diff_p3m3_p3m1 = delta_p3m3 - delta_p3m1
fields = [
"p3m1",
"p3m2",
"p3m3",
"diff_p3m2_p3m1",
"diff_p3m3_p3m1",
] # fields to plot
ncols = 6 # Max panels per row
figname = "_".join(fields)
slices_dict = {
"lpt": delta_lpt,
"p3m1": delta_p3m1,
"p3m2": delta_p3m2,
"p3m3": delta_p3m3,
"diff_p3m2_p3m1": diff_p3m2_p3m1,
"diff_p3m3_p3m1": diff_p3m3_p3m1,
}
titles_dict = {
"lpt": "LPT",
"p3m1": f"P3M1 relax={scaling_p3m1}",
"p3m2": f"P3M2 relax={scaling_p3m2}",
"p3m3": f"P3M3 relax={scaling_p3m3}",
"diff_p3m2_p3m1": r"$\delta_{\rm P3M2}-\delta_{\rm P3M1}$",
"diff_p3m3_p3m1": r"$\delta_{\rm P3M3}-\delta_{\rm P3M1}$",
"diff_p3m1_spm": r"$\delta_{\rm P3M1}-\delta_{\rm sPM}$",
}
logger.info(f"Plotting fields: {fields}...")
npanels = len(fields)
nrows = np.ceil(npanels / ncols).astype(int)
fig, axs = plt.subplots(
nrows=nrows, ncols=ncols, figsize=(3 * ncols, 4.6 * nrows), sharey=True
)
axs = axs.flatten()
ims = []
for i, key in enumerate(fields):
ax = axs[i]
data = slices_dict[key]
title = titles_dict[key]
if key.startswith("diff"):
vmin = -np.log(1 + np.abs(np.min(data)))
vmax = np.log10(1 + np.abs(np.max(data)))
if vmax > 0 > vmin:
norm = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
im = ax.imshow(
np.sign(data) * np.log(1 + np.abs(data)), cmap="RdBu_r", norm=norm
)
else:
im = ax.imshow(np.log10(1 + np.abs(data)), cmap="RdBu_r")
else:
im = ax.imshow(np.log10(2 + data), cmap=cmap)
ims.append((im, key))
ax.set_title(title, fontsize=fs_titles)
for spine in ax.spines.values():
spine.set_visible(False)
# Hide unused axes
for i in range(npanels, len(axs)):
axs[i].axis("off")
# Shared axes labels
for i, ax in enumerate(axs[:npanels]):
if i % ncols == 0:
ax.set_yticks([0, N // 2, N])
ax.set_yticklabels([f"{-L/2:.0f}", "0", f"{L/2:.0f}"], fontsize=fs)
ax.set_ylabel(r"Mpc/$h$", size=GLOBAL_FS_SMALL)
ax.set_xticks([0, N // 2, N])
ax.set_xticklabels([f"{-L/2:.0f}", "0", f"{L/2:.0f}"], fontsize=fs)
ax.set_xlabel(r"Mpc/$h$", size=GLOBAL_FS_SMALL)
# Colorbars
for ax, (im, key) in zip(axs[:npanels], ims):
divider = make_axes_locatable(ax)
cax = divider.append_axes("bottom", size="5%", pad=0.6)
cb = fig.colorbar(im, cax=cax, orientation="horizontal")
if key.startswith("diff"):
cb.set_label(
r"$\textrm{sgn}\left(\Delta\delta\right)\log_{10}(1 + |\Delta\delta|)$",
fontsize=fs,
)
else:
cb.set_label(r"$\log_{10}(2 + \delta)$", fontsize=fs)
cb.ax.tick_params(labelsize=fs)
cax.xaxis.set_ticks_position("bottom")
cax.xaxis.set_label_position("bottom")
fig.savefig(outdir + f"{figname}.png", bbox_inches="tight", dpi=300, transparent=True)
fig.savefig(outdir + f"{figname}.pdf", bbox_inches="tight", dpi=300)
plt.close(fig)
logger.info(f"Plotting fields done. Saved to {outdir}{figname}.png and {figname}.pdf")
logger.info("All plots completed successfully.")
except OSError as e:
logger.error("Directory or file access error: %s", str(e))
raise
except Exception as e:
logger.critical("An unexpected error occurred: %s", str(e))
raise RuntimeError("Failed.") from e
finally:
UNINDENT()
logger.info("Running convergence tests done.")

View file

@ -0,0 +1,907 @@
#!/usr/bin/env python3
# ----------------------------------------------------------------------
# Copyright (C) 2025 Tristan Hoellinger
# Distributed under the GNU General Public License v3.0 (GPLv3).
# See the LICENSE file in the root directory for details.
# SPDX-License-Identifier: GPL-3.0-or-later
# ----------------------------------------------------------------------
__author__ = "Tristan Hoellinger"
__version__ = "0.1.0"
__date__ = "2025"
__license__ = "GPLv3"
"""
Perform time step convergence tests with custom time step distributions
"""
# pyright: reportWildcardImportFromLibrary=false
from os.path import isfile
from pathlib import Path
import numpy as np
from pysbmy.power import PowerSpectrum
from pysbmy.fft import FourierGrid
from pysbmy.timestepping import StandardTimeStepping, P3MTimeStepping
from wip3m import *
from wip3m.tools import none_bool_str, get_k_max, generate_sim_params, generate_white_noise_Field, run_simulation
from wip3m.params import (
params_planck_kmax_missing,
cosmo_small_to_full_dict,
z2a,
BASELINE_SEEDPHASE,
)
from wip3m.plot_utils import *
from wip3m.logger import getCustomLogger, INDENT, UNINDENT
logger = getCustomLogger(__name__)
workdir = ROOT_PATH + "results/"
output_path = OUTPUT_PATH
from argparse import ArgumentParser
parser = ArgumentParser(description="Run convergence tests towards implementing P3M gravity.")
parser.add_argument("--run_id", type=str, help="Simulation run identifier.")
parser.add_argument("--L", type=float, default=32.0, help="Box size in Mpc/h.")
parser.add_argument("--N", type=int, default=128, help="Density grid size per dimension.")
parser.add_argument("--Np", type=int, default=32, help="Number of particles per dimension.")
parser.add_argument("--Npm", type=int, default=64, help="PM mesh resolution per dimension.")
parser.add_argument(
"--n_Tiles", type=int, default=8, help="Number of tiles per dimension for short-range PP."
)
parser.add_argument("--z_i", type=float, default=199.0, help="Initial redshift.")
parser.add_argument("--z_f", type=float, default=19.0, help="Final redshift.")
parser.add_argument(
"--plot_fields",
type=none_bool_str,
default=False,
help="Whether to plot the fields or not.",
)
parser.add_argument(
"--lpt",
type=none_bool_str,
default=False,
help="Whether to compute and plot the LPT power spectrum.",
)
parser.add_argument(
"--scale_limiter",
type=none_bool_str,
default=False,
help="Which limiter should be scaled by the scaling arguments? ",
)
# Timestep settings per method
for scheme in ["pmref", "pm1", "pm2", "spm", "p3m1", "p3m2", "p3m3"]:
parser.add_argument(
f"--scaling_{scheme}",
type=float,
default={
"pmref": 1.,
"pm1": 0.95,
"pm2": 0.9,
"spm": 1.,
"p3m1": 1.,
"p3m2": 0.95,
"p3m3": 0.9,
}[scheme],
help=f"Relax the step limiter by this factor for {scheme.upper()}.",
)
args = parser.parse_args()
if __name__ == "__main__":
try:
go_beyond_Nyquist_ss = True # for the summary statistics
force = False
force_hard = False
run_id = args.run_id
L = args.L
N = args.N
Np = args.Np
Npm = args.Npm
n_Tiles = args.n_Tiles
scaling_pmref = args.scaling_pmref
scaling_pm1 = args.scaling_pm1
scaling_pm2 = args.scaling_pm2
scaling_spm = args.scaling_spm
scaling_p3m1 = args.scaling_p3m1
scaling_p3m2 = args.scaling_p3m2
scaling_p3m3 = args.scaling_p3m3
name_scaled = args.scale_limiter
if name_scaled is not False and name_scaled not in [
"fac_dyn_custom",
"da_max_early_custom",
"fac_H_custom",
"fac_bend",
"sub_bend1",
"sub_bend2",
"da_max_late",
]:
raise ValueError(
f"Invalid name_scaled: {name_scaled}. Must be one of the limiter names or False."
)
RedshiftLPT = args.z_i
RedshiftFCs = args.z_f
logger.info("Running convergence tests...")
logger.info(f"Run ID: {run_id}")
logger.info(f"Box size: {L} Mpc/h")
logger.info(f"Density grid size: {N}^3")
logger.info(f"Particles per dimension: {Np}^3")
logger.info(f"PM grid size: {Npm}^3")
logger.info(f"Number of tiles: {n_Tiles}^3")
logger.info(f"Limiter to scale: {name_scaled if name_scaled else 'None'}")
if name_scaled:
logger.info(f"Limiter rescaling for PMREF: {scaling_pmref}")
logger.info(f"Limiter rescaling for PM1: {scaling_pm1}")
logger.info(f"Limiter rescaling for PM2: {scaling_pm2}")
logger.info(f"Limiter rescaling for sPM: {scaling_spm}")
logger.info(f"Limiter rescaling for P3M1: {scaling_p3m1}")
logger.info(f"Limiter rescaling for P3M2: {scaling_p3m2}")
logger.info(f"Limiter rescaling for P3M3: {scaling_p3m3}")
INDENT()
lpt = args.lpt
plot_fields = args.plot_fields
corner = -L / 2.0
ai = z2a(RedshiftLPT)
af = z2a(RedshiftFCs)
k_max = get_k_max(L, N) # k_max in h/Mpc
cosmo = params_planck_kmax_missing.copy()
cosmo["k_max"] = k_max
wd = workdir + run_id + "/"
simdir = output_path + run_id + "/"
logdir = simdir + "logs/"
outdir = simdir + "plots/"
if force_hard:
import shutil
if Path(simdir).exists():
shutil.rmtree(simdir)
if Path(wd).exists():
shutil.rmtree(wd)
Path(wd).mkdir(parents=True, exist_ok=True)
Path(logdir).mkdir(parents=True, exist_ok=True)
Path(outdir).mkdir(parents=True, exist_ok=True)
input_white_noise_file = simdir + "input_white_noise.h5"
input_seed_phase_file = simdir + "seed"
ICs_path = simdir + "initial_density.h5"
simpath = simdir
# Path to the input matter power spectrum (generated later)
input_power_file = simdir + "input_power.h5"
sim_params = {
"L": L,
"N": N,
"Np": Np,
"Npm": Npm,
"n_Tiles": n_Tiles,
"RedshiftLPT": RedshiftLPT,
"RedshiftFCs": RedshiftFCs,
"name_scaled": name_scaled,
"scaling_pmref": scaling_pmref,
"scaling_pm1": scaling_pm1,
"scaling_pm2": scaling_pm2,
"scaling_spm": scaling_spm,
"scaling_p3m1": scaling_p3m1,
"scaling_p3m2": scaling_p3m2,
"scaling_p3m3": scaling_p3m3,
}
with open(wd + "sim_params.txt", "w") as f:
f.write(f"{sim_params}\n")
logger.info("Setting up simulation parameters...")
common_params = {
"Np": Np,
"N": N,
"L": L,
"corner0": corner,
"corner1": corner,
"corner2": corner,
"h": cosmo["h"],
"Omega_m": cosmo["Omega_m"],
"Omega_b": cosmo["Omega_b"],
"n_s": cosmo["n_s"],
"sigma8": cosmo["sigma8"],
}
lpt_params = common_params.copy()
lpt_params["method"] = "lpt"
lpt_params["InputPowerSpectrum"] = input_power_file
lpt_params["ICsMode"] = 1
lpt_params["InputWhiteNoise"] = input_white_noise_file
fac_dyn_custom = DEFAULT_FAC_DYN_CUSTOM
da_early = DEFAULT_DA_MAX_EARLY_CUSTOM
fac_H_custom = DEFAULT_FAC_H_CUSTOM
fac_bend = DEFAULT_FAC_BEND
sub_bend1 = DEFAULT_SUB_BEND1
sub_bend2 = DEFAULT_SUB_BEND2
da_late = DEFAULT_DA_MAX_LATE_CUSTOM
limiters = {
"fac_dyn_custom": fac_dyn_custom,
"da_max_early_custom": da_early,
"fac_H_custom": fac_H_custom,
"fac_bend": fac_bend,
"sub_bend1": sub_bend1,
"sub_bend2": sub_bend2,
"da_max_late": da_late,
}
common_params_num = common_params.copy()
common_params_num["ai"] = ai
common_params_num["af"] = af
common_params_num["RedshiftLPT"] = RedshiftLPT
common_params_num["RedshiftFCs"] = RedshiftFCs
common_params_num["Npm"] = Npm
common_params_num["RunForceDiagnostic"] = False
common_params_num["nBinsForceDiagnostic"] = 20
common_params_num["nPairsForceDiagnostic"] = 3
common_params_num["maxTrialsForceDiagnostic"] = int(1e8)
common_params_num["OutputForceDiagnostic"] = simdir + "force_diagnostic.txt"
common_params_num["TimeStepDistribution"] = 3 # Custom time step distribution
common_params_num["cosmo_dict"] = cosmo
common_params_num["fac_dyn_custom"] = limiters["fac_dyn_custom"]
common_params_num["da_max_early_custom"] = limiters["da_max_early_custom"]
common_params_num["fac_H_custom"] = limiters["fac_H_custom"]
common_params_num["fac_bend"] = limiters["fac_bend"]
common_params_num["sub_bend1"] = limiters["sub_bend1"]
common_params_num["sub_bend2"] = limiters["sub_bend2"]
common_params_num["da_max_late_custom"] = limiters["da_max_late"]
pmref_params = common_params_num.copy()
pmref_params["method"] = "pm"
pmref_params["relax"] = scaling_pmref # plot unscaled values during the simulation
if name_scaled:
pmref_params[name_scaled] /= scaling_pmref
pm1_params = common_params_num.copy()
pm1_params["method"] = "pm"
pm1_params["relax"] = scaling_pm1
if name_scaled:
pm1_params[name_scaled] /= scaling_pm1
pm2_params = common_params_num.copy()
pm2_params["method"] = "pm"
pm2_params["relax"] = scaling_pm2
if name_scaled:
pm2_params[name_scaled] /= scaling_pm2
spm_params = common_params_num.copy()
spm_params["method"] = "spm"
spm_params["EvolutionMode"] = 5
spm_params["n_Tiles"] = n_Tiles
spm_params["PrintOutputTimestepsLog"] = True
spm_params["OutputTimestepsLog"] = simdir + "spm_timestep_log.txt"
spm_params["relax"] = scaling_spm
if name_scaled:
spm_params[name_scaled] /= scaling_spm
p3m1_params = common_params_num.copy()
p3m1_params["method"] = "p3m"
p3m1_params["EvolutionMode"] = 4
p3m1_params["n_Tiles"] = n_Tiles
p3m1_params["PrintOutputTimestepsLog"] = True
p3m1_params["OutputTimestepsLog"] = simdir + "p3m1_timestep_log.txt"
p3m1_params["relax"] = scaling_p3m1
if name_scaled:
p3m1_params[name_scaled] /= scaling_p3m1
p3m2_params = common_params_num.copy()
p3m2_params["method"] = "p3m"
p3m2_params["EvolutionMode"] = 4
p3m2_params["n_Tiles"] = n_Tiles
p3m2_params["PrintOutputTimestepsLog"] = True
p3m2_params["OutputTimestepsLog"] = simdir + "p3m2_timestep_log.txt"
p3m2_params["relax"] = scaling_p3m2
if name_scaled:
p3m2_params[name_scaled] /= scaling_p3m2
p3m3_params = common_params_num.copy()
p3m3_params["method"] = "p3m"
p3m3_params["EvolutionMode"] = 4
p3m3_params["n_Tiles"] = n_Tiles
p3m3_params["PrintOutputTimestepsLog"] = True
p3m3_params["OutputTimestepsLog"] = simdir + "p3m3_timestep_log.txt"
p3m3_params["relax"] = scaling_p3m3
if name_scaled:
p3m3_params[name_scaled] /= scaling_p3m3
logger.info("Setting up simulation parameters done.")
logger.info("Generating simulation parameters...")
INDENT()
reset_plotting() # Default style for Simbelmynë
generate_sim_params(lpt_params, ICs_path, wd, simdir, None, force)
all_sim_params = [
("pmref", pmref_params),
("pm1", pm1_params),
("pm2", pm2_params),
("spm", spm_params),
("p3m1", p3m1_params),
("p3m2", p3m2_params),
("p3m3", p3m3_params),
]
for name, parameters in all_sim_params:
logger.info(
f"Generating parameters for {name.upper()} with relax = {parameters['relax']}..."
)
file_ext = f"{name}_relax{str(parameters['relax']).replace('.', '_')}"
generate_sim_params(parameters, ICs_path, wd, simdir, file_ext, force)
logger.info(f"Generating parameters for {name.upper()} done.")
UNINDENT()
logger.info("Generating simulation parameters done.")
setup_plotting() # Reset plotting style for this project
logger.info("Generating white noise field...")
generate_white_noise_Field(
L=L,
size=N,
corner=corner,
seedphase=BASELINE_SEEDPHASE,
fname_whitenoise=input_white_noise_file,
seedname_whitenoise=input_seed_phase_file,
force_phase=force,
)
logger.info("Generating white noise field done.")
# If cosmo["WhichSpectrum"] == "class", then classy is required.
if not isfile(input_power_file) or force:
logger.info("Generating input power spectrum...")
Pk = PowerSpectrum(L, L, L, N, N, N, cosmo_small_to_full_dict(cosmo))
Pk.write(input_power_file)
logger.info("Generating input power spectrum done.")
logger.info("Generating Fourier grid...")
# k grid used to compute the final overdensity power spectrum
Pinit = 100
trim_threshold = 100 # Merge bins until this minimum number of modes per bin is reached
log_kmin = np.log10(2 * np.pi / (np.sqrt(3) * L)) # Minimum non-zero k in h/Mpc
if go_beyond_Nyquist_ss:
k_max_ss = get_k_max(L, N)
else:
k_max_ss = get_k_max(L, N) / np.sqrt(3) # 1D Nyquist frequency
Pbins_left_bnds = np.logspace(log_kmin, np.log10(k_max_ss), Pinit + 1, dtype=np.float32)
Pbins_left_bnds = Pbins_left_bnds[:-1]
input_ss_file = simdir + "input_ss_k_grid.h5"
Gk = FourierGrid(
L,
L,
L,
N,
N,
N,
k_modes=Pbins_left_bnds,
kmax=k_max_ss,
trim_bins=True,
trim_threshold=trim_threshold,
)
Gk.write(input_ss_file)
logger.info("Generating Fourier grid done.")
logger.info("Running simulations...")
INDENT()
logger.info("Running LPT simulation...")
run_simulation("lpt", lpt_params, wd, logdir)
logger.info("Running LPT simulation done.")
for name, parameters in all_sim_params:
logger.info(f"Running {name.upper()} simulation...")
run_simulation(name, parameters, wd, logdir)
logger.info(f"Running {name.upper()} simulation done.")
UNINDENT()
logger.info("All simulations done.")
logger.info("Plotting time step diagnostics...")
INDENT()
nsteps_dict = {}
for name, parameters in all_sim_params:
TimeStepDistribution = parameters["TimeStepDistribution"]
file_ext = f"{name}_relax{str(parameters['relax']).replace('.', '_')}"
method = parameters["method"]
TSpath = wd + file_ext + f"_ts_{method}.h5" if file_ext else wd + f"ts_{method}.h5"
if TimeStepDistribution in [0, 1, 2]:
TS = StandardTimeStepping.read(TSpath)
aiDrift = TS.aiDrift
nsteps = TS.nsteps
elif TimeStepDistribution == 3:
TS = P3MTimeStepping.read(TSpath)
aiDrift = TS.aiDrift
nsteps = TS.nsteps
else:
raise ValueError(f"Invalid TimeStepDistribution: {TimeStepDistribution}")
nsteps_dict[name] = nsteps
if name.startswith("p3m") or name.startswith("spm"):
plot_timestepping_diagnostics(
log_path=parameters["OutputTimestepsLog"],
aiDrift=aiDrift,
TimeStepDistribution=TimeStepDistribution,
nsteps=nsteps,
exact=False,
save_path=outdir + f"time_step_diagnostics_{name}.pdf",
)
plot_custom_timestepping_diagnostics(
log_path=parameters["OutputTimestepsLog"],
aiDrift=aiDrift,
TimeStepDistribution=TimeStepDistribution,
nsteps=nsteps,
ymin=8e-3,
fac_hubble=fac_H_custom,
fac_bend=fac_bend,
da_max_early=da_early,
save_path=outdir + f"time_step_diagnostics_custom_{name}.pdf",
)
UNINDENT()
logger.info("Plotting time step diagnostics done.")
logger.info("Computing and plotting power spectra...")
INDENT()
from pysbmy.fft import read_FourierGrid
from pysbmy.field import read_field
from pysbmy.correlations import get_autocorrelation
logger.info("Loading Fourier grid...")
G = read_FourierGrid(input_ss_file)
logger.info("Loading Fourier grid done.")
k = G.k_modes
AliasingCorr = False
slice_ijk = (N // 2, slice(None), slice(None)) # for plot_fields
logger.info("Computing power spectra...")
DELTA = read_field(simdir + "initial_density.h5")
Pk_INI, _ = get_autocorrelation(DELTA, G, AliasingCorr)
if lpt:
DELTA = read_field(simdir + "lpt_density.h5")
Pk_LPT, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"pmref_relax{str(pmref_params['relax']).replace('.', '_')}_final_density_pm.h5")
if plot_fields:
delta_pmref = DELTA.data[slice_ijk]
Pk_PMref, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"pm1_relax{str(pm1_params['relax']).replace('.', '_')}_final_density_pm.h5")
if plot_fields:
delta_pm1 = DELTA.data[slice_ijk]
Pk_PM1, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"pm2_relax{str(pm2_params['relax']).replace('.', '_')}_final_density_pm.h5")
if plot_fields:
delta_pm2 = DELTA.data[slice_ijk]
Pk_PM2, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"spm_relax{str(spm_params['relax']).replace('.', '_')}_final_density_spm.h5")
if plot_fields:
delta_spm = DELTA.data[slice_ijk]
Pk_sPM, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"p3m1_relax{str(p3m1_params['relax']).replace('.', '_')}_final_density_p3m.h5")
if plot_fields:
delta_p3m1 = DELTA.data[slice_ijk]
Pk_P3M1, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"p3m2_relax{str(p3m2_params['relax']).replace('.', '_')}_final_density_p3m.h5")
if plot_fields:
delta_p3m2 = DELTA.data[slice_ijk]
Pk_P3M2, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"p3m3_relax{str(p3m3_params['relax']).replace('.', '_')}_final_density_p3m.h5")
if plot_fields:
delta_p3m3 = DELTA.data[slice_ijk]
Pk_P3M3, _ = get_autocorrelation(DELTA, G, AliasingCorr)
logger.info("Computing power spectra done.")
ref = "P3M1"
scaling_reference = scaling_p3m1
Pk_ref = Pk_P3M1
label_ref = f"P3M, scaling={scaling_reference}, ns={nsteps_dict['p3m1']}"
logger.info(f"Plotting power spectra...")
INDENT()
fig, ax = plt.subplots(figsize=(7, 4))
ax.set_xscale("log")
k = G.k_modes
kmin, kmax = k.min(), k.max()
logger.info(f"kmin = {kmin:.2e}, kmax = {kmax:.2e}")
log_pad = 0.02
log_k_min = np.log10(kmin)
log_k_max = np.log10(kmax)
log_range = log_k_max - log_k_min
xlim_min = 10 ** (log_k_min - log_pad * log_range)
xlim_max = 10 ** (log_k_max + log_pad * log_range)
plt.xlim(xlim_min, xlim_max)
# ax.set_ylim([0.2, 1.8])
dark_grey_bnd = 0.01
medium_grey_bnd = 0.05
light_grey_bnd = 0.1
ax.plot([kmin, kmax], [1, 1], color="black", linestyle="-", label=label_ref)
if lpt:
ax.plot(k, Pk_LPT / Pk_ref, label="2LPT", linestyle="--")
fields_to_plot = [
("PMref", Pk_PMref),
("PM1", Pk_PM1),
("PM2", Pk_PM2),
("sPM", Pk_sPM),
("P3M1", Pk_P3M1),
("P3M2", Pk_P3M2),
("P3M3", Pk_P3M3),
]
# Remove the field corresponding to the reference:
fields_to_plot = [
(field_name, Pk) for field_name, Pk in fields_to_plot if field_name != ref
]
for field_name, Pk in fields_to_plot:
label = f"{field_name}, scaling={eval(f'scaling_{field_name.lower()}')}, ns={nsteps_dict[field_name.lower()]}"
if field_name in ["PMref", "P3M1"]:
linestyle = "-"
zorder = 1
else:
linestyle = "--"
zorder = 2
ax.plot(k, Pk / Pk_ref, label=label, linestyle=linestyle)
# ax.axhspan(1 - dark_grey_bnd, 1 + dark_grey_bnd, color="grey", alpha=0.3)
# ax.axhspan(1 - medium_grey_bnd, 1 + medium_grey_bnd, color="grey", alpha=0.2)
# ax.axhspan(1 - light_grey_bnd, 1 + light_grey_bnd, color="grey", alpha=0.1)
ax.axhspan(1 - dark_grey_bnd, 1 + dark_grey_bnd, color="grey", alpha=0.3)
ax.axhline(1 - medium_grey_bnd, color="grey", linestyle="-", linewidth=1)
ax.axhline(1 + medium_grey_bnd, color="grey", linestyle="-", linewidth=1)
for i in range(1, len(k)):
ax.axvline(k[i], color="black", linestyle=":", linewidth=1, alpha=0.1, zorder=0)
ax.yaxis.set_major_locator(plt.MaxNLocator(6))
ax.yaxis.get_major_ticks()[0].label1.set_visible(False)
ax.set_xlabel("$k$ [$h/\\mathrm{Mpc}$]", fontsize=fs)
ax.set_ylabel("$P(k)/P_\\mathrm{ref}(k)$", fontsize=fs)
ax.tick_params(which="both", direction="in")
ax.tick_params(axis="both", which="major", labelsize=fs)
ax.tick_params(axis="both", which="minor", labelsize=fs)
# Characteristic vertical reference scales
nyquist = np.pi * N / L
nyquist_PM = np.pi * Npm / L
epsilon = 0.03 * L / Np
particle_length = 2 * epsilon
xs = 1.25 * L / Npm
xr = 4.5 * xs
particle_wavenumber = 2 * np.pi / particle_length # Too large to be shown
xs_inv = 2 * np.pi / xs
xr_inv = 2 * np.pi / xr
if nyquist <= xlim_max:
line1 = ax.axvline(
x=nyquist, color="black", linestyle="--", lw=2, label="Nyquist (density grid)", zorder=0
)
if nyquist_PM <= xlim_max:
line2 = ax.axvline(
x=nyquist_PM, color="black", linestyle="-", lw=1, label="Nyquist (PM grid)", zorder=0
)
if xs_inv <= xlim_max:
line3 = ax.axvline(
x=xs_inv, color="gray", linestyle="-.", lw=2, label=r"Split wavenumber $x_s$", zorder=0
)
if xr_inv <= xlim_max:
line4 = ax.axvline(
x=xr_inv, color="gray", linestyle=":", lw=2, label=r"Short-range reach $x_r$", zorder=0
)
logger.info(f"Nyquist (density grid): {nyquist:.2f} h/Mpc")
logger.info(f"Nyquist (PM grid): {nyquist_PM:.2f} h/Mpc")
logger.info(f"Particle wavenumber: {particle_wavenumber:.2f} h/Mpc")
logger.info(f"Split wavenumber: {xs_inv:.2f} h/Mpc")
logger.info(f"Short-range reach: {xr_inv:.2f} h/Mpc")
handles, labels = plt.gca().get_legend_handles_labels()
# empty_patch = mpatches.Patch(color="none", label="")
# handles = [empty_patch, *handles]
# labels = ["", *labels]
plt.legend(
handles,
labels,
loc="upper center",
ncol=2,
bbox_to_anchor=(0.5, -0.2),
fontsize=fs,
frameon=False,
)
fig.savefig(
outdir + "power_spectra.png",
bbox_inches="tight",
dpi=300,
transparent=True,
)
fig.savefig(
outdir + "power_spectra.pdf",
bbox_inches="tight",
dpi=300,
)
plt.close(fig)
UNINDENT()
logger.info(
f"Power spectra plotted and saved to {outdir}power_spectra.png and power_spectra.pdf"
)
UNINDENT()
logger.info("Plotting power spectra done.")
logger.info(f"Plotting P3M power spectra...")
INDENT()
fig, ax = plt.subplots(figsize=(7, 4))
ax.set_xscale("log")
k = G.k_modes
kmin, kmax = k.min(), k.max()
logger.info(f"kmin = {kmin:.2e}, kmax = {kmax:.2e}")
log_pad = 0.02
log_k_min = np.log10(kmin)
log_k_max = np.log10(kmax)
log_range = log_k_max - log_k_min
xlim_min = 10 ** (log_k_min - log_pad * log_range)
xlim_max = 10 ** (log_k_max + log_pad * log_range)
plt.xlim(xlim_min, xlim_max)
# ax.set_ylim([0.2, 1.8])
dark_grey_bnd = 0.01
medium_grey_bnd = 0.05
light_grey_bnd = 0.1
ax.plot([kmin, kmax], [1, 1], color="black", linestyle="-", label=label_ref)
fields_to_plot = [
("P3M1", Pk_P3M1),
("P3M2", Pk_P3M2),
("P3M3", Pk_P3M3),
]
# Remove the field corresponding to the reference:
fields_to_plot = [
(field_name, Pk) for field_name, Pk in fields_to_plot if field_name != ref
]
for field_name, Pk in fields_to_plot:
label = f"{field_name}, scaling={eval(f'scaling_{field_name.lower()}')}, ns={nsteps_dict[field_name.lower()]}"
if field_name in ["PMref", "P3M1"]:
linestyle = "-"
zorder = 1
else:
linestyle = "--"
zorder = 2
ax.plot(k, Pk / Pk_ref, label=label, linestyle=linestyle)
ax.axhspan(1 - dark_grey_bnd, 1 + dark_grey_bnd, color="grey", alpha=0.3)
ax.axhline(1 - medium_grey_bnd, color="grey", linestyle="-", linewidth=1)
ax.axhline(1 + medium_grey_bnd, color="grey", linestyle="-", linewidth=1)
for i in range(1, len(k)):
ax.axvline(k[i], color="black", linestyle=":", linewidth=1, alpha=0.1, zorder=0)
ax.yaxis.set_major_locator(plt.MaxNLocator(6))
ax.yaxis.get_major_ticks()[0].label1.set_visible(False)
ax.set_xlabel("$k$ [$h/\\mathrm{Mpc}$]", fontsize=fs)
ax.set_ylabel("$P(k)/P_\\mathrm{ref}(k)$", fontsize=fs)
ax.tick_params(which="both", direction="in")
ax.tick_params(axis="both", which="major", labelsize=fs)
ax.tick_params(axis="both", which="minor", labelsize=fs)
# Characteristic vertical reference scales
nyquist = np.pi * N / L
nyquist_PM = np.pi * Npm / L
epsilon = 0.03 * L / Np
particle_length = 2 * epsilon
xs = 1.25 * L / Npm
xr = 4.5 * xs
particle_wavenumber = 2 * np.pi / particle_length # Too large to be shown
xs_inv = 2 * np.pi / xs
xr_inv = 2 * np.pi / xr
if nyquist <= xlim_max:
line1 = ax.axvline(
x=nyquist, color="black", linestyle="--", lw=2, label="Nyquist (density grid)", zorder=0
)
if nyquist_PM <= xlim_max:
line2 = ax.axvline(
x=nyquist_PM, color="black", linestyle="-", lw=1, label="Nyquist (PM grid)", zorder=0
)
if xs_inv <= xlim_max:
line3 = ax.axvline(
x=xs_inv, color="gray", linestyle="-.", lw=2, label=r"Split wavenumber $x_s$", zorder=0
)
if xr_inv <= xlim_max:
line4 = ax.axvline(
x=xr_inv, color="gray", linestyle=":", lw=2, label=r"Short-range reach $x_r$", zorder=0
)
logger.info(f"Nyquist (density grid): {nyquist:.2f} h/Mpc")
logger.info(f"Nyquist (PM grid): {nyquist_PM:.2f} h/Mpc")
logger.info(f"Particle wavenumber: {particle_wavenumber:.2f} h/Mpc")
logger.info(f"Split wavenumber: {xs_inv:.2f} h/Mpc")
logger.info(f"Short-range reach: {xr_inv:.2f} h/Mpc")
handles, labels = plt.gca().get_legend_handles_labels()
plt.legend(
handles,
labels,
loc="upper center",
ncol=2,
bbox_to_anchor=(0.5, -0.2),
fontsize=fs,
frameon=False,
)
fig.savefig(
outdir + "p3m_power_spectra.png",
bbox_inches="tight",
dpi=300,
transparent=True,
)
fig.savefig(
outdir + "p3m_power_spectra.pdf",
bbox_inches="tight",
dpi=300,
)
plt.close(fig)
UNINDENT()
logger.info(
f"P3M power spectra plotted and saved to {outdir}p3m_power_spectra.png and power_spectra.pdf"
)
logger.info("Pplotting P3M power spectra done.")
UNINDENT()
logger.info("Computing and plotting power spectra done.")
if plot_fields:
logger.info("Reading lpt field...")
delta_lpt = read_field(simdir + "lpt_density.h5").data[slice_ijk]
logger.info("Reading lpt field done.")
diff_pm1_pmref = delta_pm1 - delta_pmref
diff_pm2_pmref = delta_pm2 - delta_pmref
diff_p3m1_pmref = delta_p3m1 - delta_pmref
diff_p3m2_p3m1 = delta_p3m2 - delta_p3m1
diff_p3m3_p3m1 = delta_p3m3 - delta_p3m1
diff_p3m1_spm = delta_p3m1 - delta_spm
fields = [
"pmref",
"pm1",
"pm2",
"p3m1",
"p3m2",
"p3m3",
"diff_p3m1_spm",
"diff_pm1_pmref",
"diff_pm2_pmref",
"diff_p3m1_pmref",
"diff_p3m2_p3m1",
"diff_p3m3_p3m1",
] # fields to plot
ncols = 6 # Max panels per row
figname = "_".join(fields)
slices_dict = {
"lpt": delta_lpt,
"pmref": delta_pmref,
"pm1": delta_pm1,
"pm2": delta_pm2,
"spm": delta_spm,
"p3m1": delta_p3m1,
"p3m2": delta_p3m2,
"p3m3": delta_p3m3,
"diff_pm1_pmref": diff_pm1_pmref,
"diff_pm2_pmref": diff_pm2_pmref,
"diff_p3m1_pmref": diff_p3m1_pmref,
"diff_p3m2_p3m1": diff_p3m2_p3m1,
"diff_p3m3_p3m1": diff_p3m3_p3m1,
"diff_p3m1_spm": diff_p3m1_spm,
}
titles_dict = {
"lpt": "LPT",
"pmref": f"PMref relax={scaling_pmref}",
"pm1": f"PM1 relax={scaling_pm1}",
"pm2": f"PM2 relax={scaling_pm2}",
"spm": f"sPM relax={scaling_spm}",
"p3m1": f"P3M1 relax={scaling_p3m1}",
"p3m2": f"P3M2 relax={scaling_p3m2}",
"p3m3": f"P3M3 relax={scaling_p3m3}",
"diff_pm1_pmref": r"$\delta_{\rm PM1}-\delta_{\rm PMref}$",
"diff_pm2_pmref": r"$\delta_{\rm PM2}-\delta_{\rm PMref}$",
"diff_p3m1_pmref": r"$\delta_{\rm P3M1}-\delta_{\rm PMref}$",
"diff_p3m2_p3m1": r"$\delta_{\rm P3M2}-\delta_{\rm P3M1}$",
"diff_p3m3_p3m1": r"$\delta_{\rm P3M3}-\delta_{\rm P3M1}$",
"diff_p3m1_spm": r"$\delta_{\rm P3M1}-\delta_{\rm sPM}$",
}
logger.info(f"Plotting fields: {fields}...")
npanels = len(fields)
nrows = np.ceil(npanels / ncols).astype(int)
fig, axs = plt.subplots(
nrows=nrows, ncols=ncols, figsize=(3 * ncols, 4.6 * nrows), sharey=True
)
axs = axs.flatten()
ims = []
for i, key in enumerate(fields):
ax = axs[i]
data = slices_dict[key]
title = titles_dict[key]
if key.startswith("diff"):
vmin = -np.log(1 + np.abs(np.min(data)))
vmax = np.log10(1 + np.abs(np.max(data)))
if vmax > 0 > vmin:
norm = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
im = ax.imshow(
np.sign(data) * np.log(1 + np.abs(data)), cmap="RdBu_r", norm=norm
)
else:
im = ax.imshow(np.log10(1 + np.abs(data)), cmap="RdBu_r")
else:
im = ax.imshow(np.log10(2 + data), cmap=cmap)
ims.append((im, key))
ax.set_title(title, fontsize=fs_titles)
for spine in ax.spines.values():
spine.set_visible(False)
# Hide unused axes
for i in range(npanels, len(axs)):
axs[i].axis("off")
# Shared axes labels
for i, ax in enumerate(axs[:npanels]):
if i % ncols == 0:
ax.set_yticks([0, N // 2, N])
ax.set_yticklabels([f"{-L/2:.0f}", "0", f"{L/2:.0f}"], fontsize=fs)
ax.set_ylabel(r"Mpc/$h$", size=GLOBAL_FS_SMALL)
ax.set_xticks([0, N // 2, N])
ax.set_xticklabels([f"{-L/2:.0f}", "0", f"{L/2:.0f}"], fontsize=fs)
ax.set_xlabel(r"Mpc/$h$", size=GLOBAL_FS_SMALL)
# Colorbars
for ax, (im, key) in zip(axs[:npanels], ims):
divider = make_axes_locatable(ax)
cax = divider.append_axes("bottom", size="5%", pad=0.6)
cb = fig.colorbar(im, cax=cax, orientation="horizontal")
if key.startswith("diff"):
cb.set_label(
r"$\textrm{sgn}\left(\Delta\delta\right)\log_{10}(1 + |\Delta\delta|)$",
fontsize=fs,
)
else:
cb.set_label(r"$\log_{10}(2 + \delta)$", fontsize=fs)
cb.ax.tick_params(labelsize=fs)
cax.xaxis.set_ticks_position("bottom")
cax.xaxis.set_label_position("bottom")
fig.savefig(outdir + f"{figname}.png", bbox_inches="tight", dpi=300, transparent=True)
fig.savefig(outdir + f"{figname}.pdf", bbox_inches="tight", dpi=300)
plt.close(fig)
logger.info(f"Plotting fields done. Saved to {outdir}{figname}.png and {figname}.pdf")
logger.info("All plots completed successfully.")
except OSError as e:
logger.error("Directory or file access error: %s", str(e))
raise
except Exception as e:
logger.critical("An unexpected error occurred: %s", str(e))
raise RuntimeError("Failed.") from e
finally:
UNINDENT()
logger.info("Running convergence tests done.")

View file

@ -72,6 +72,25 @@ params_planck_kmax_missing = {
"WhichSpectrum": WHICH_SPECTRUM,
}
Omega_b_CONCEPT = 0.049
Omega_cdm_CONCEPT = 0.27
Omega_m_CONCEPT = Omega_b_CONCEPT + Omega_cdm_CONCEPT
params_CONCEPT_kmax_missing = {
"h": 0.67,
"Omega_r": 0.0,
"Omega_q": 1.0 - Omega_m_CONCEPT,
"Omega_b": Omega_b_CONCEPT,
"Omega_m": Omega_m_CONCEPT,
"m_ncdm": 0.0,
"Omega_k": 0.0,
"tau_reio": TAU_REIO,
"n_s": 0.96,
"sigma8": 0.8102,
"w0_fld": -1.0,
"wa_fld": 0.0,
"WhichSpectrum": WHICH_SPECTRUM,
}
def z2a(z):
return 1.0 / (1 + z)
@ -109,5 +128,27 @@ def cosmo_small_to_full_dict(cosmo_min):
}
return cosmo_full
# Default parameters time step limiters
# Mostly for development and testing purposes.
# For real P3M+COLA runs starting at redshift z=19, only
# DEFAULT_FAC_DYN_CUSTOM_COLA, DEFAULT_FAC_H_CUSTOM_COLA,
# DEFAULT_FAC_P3M_FIT, DEFAULT_DA_MAX_LATE_CUSTOM and
# P3M_FIT_COEFFS_DEFAULT_{1,2}NP are relevant.
DEFAULT_FAC_DYN_CUSTOM = 0.0154
DEFAULT_FAC_DYN_CUSTOM_COLA = 0.03
DEFAULT_DA_MAX_EARLY_CUSTOM = 0.0013
DEFAULT_FAC_H_CUSTOM = 0.03
DEFAULT_FAC_H_CUSTOM_COLA = 0.06
DEFAULT_FAC_BEND = 0.04375
DEFAULT_SUB_BEND1 = 0.012
DEFAULT_SUB_BEND2 = 0.007
DEFAULT_SUB_BEND1_COLA = 0.012
DEFAULT_SUB_BEND2_COLA = 0.014
DEFAULT_FAC_P3M_FIT = 1.0
DEFAULT_DA_MAX_LATE_CUSTOM = 0.02
P3M_FIT_COEFFS_DEFAULT_2NP = [-4.41928363, 0.24380666, 1.92034984, 1.21626721, 0.81694801, 0.20590599] # fitted P3M limiter for L=2Np
P3M_FIT_COEFFS_DEFAULT_NP = [-5.14695921, 0.35234941, 2.08397701, 1.2447759, 0.61695249, 0.11401126] # fitted P3M limiter for L=Np
# TODO: add polynomial coefficients for more L/Np values
# pyright: reportUnsupportedDunderAll=false
__all__ = [k for k in globals() if not k.startswith("_")]

View file

@ -0,0 +1,531 @@
#!/usr/bin/env python3
# ----------------------------------------------------------------------
# Copyright (C) 2025 Tristan Hoellinger
# Distributed under the GNU General Public License v3.0 (GPLv3).
# See the LICENSE file in the root directory for details.
# SPDX-License-Identifier: GPL-3.0-or-later
# ----------------------------------------------------------------------
__author__ = "Tristan Hoellinger"
__version__ = "0.1.0"
__date__ = "2025"
__license__ = "GPLv3"
"""Plot results of time step convergence tests."""
# pyright: reportWildcardImportFromLibrary=false
from pathlib import Path
import numpy as np
from pysbmy.fft import read_FourierGrid
from pysbmy.field import read_field
from pysbmy.correlations import get_autocorrelation
from pysbmy.timestepping import StandardTimeStepping
from wip3m import *
from wip3m.tools import none_bool_str
from wip3m.plot_utils import *
from wip3m.logger import getCustomLogger, INDENT, UNINDENT
logger = getCustomLogger(__name__)
workdir = ROOT_PATH + "results/"
output_path = OUTPUT_PATH
from argparse import ArgumentParser
parser = ArgumentParser(description="Plot convergence results for WIP3M.")
parser.add_argument(
"--run_id",
type=str,
help="Run ID to use for plotting.",
)
parser.add_argument(
"--outdir_suffix",
type=str,
default="plots",
help="Suffix to append to the output directory.",
)
parser.add_argument(
"--ref",
type=str,
default="P3M1",
choices=["P3M1", "PMref"],
help="Reference field for power spectrum comparison.",
)
parser.add_argument(
"--lpt",
type=none_bool_str,
default=False,
help="Whether to compute and plot the LPT power spectrum.",
)
parser.add_argument(
"--plot_fields",
type=none_bool_str,
default=False,
help="Whether to plot the fields or not.",
)
args = parser.parse_args()
run_id = args.run_id
outdir_suffix = args.outdir_suffix
ref = args.ref
lpt = args.lpt
plot_fields = args.plot_fields
if not run_id:
raise ValueError("Please provide a run ID using the --run_id argument.")
simdir = output_path + run_id + "/"
outdir = simdir + outdir_suffix + "/"
input_ss_file = simdir + "input_ss_k_grid.h5"
wd = workdir + run_id + "/"
Path(outdir).mkdir(parents=True, exist_ok=True)
if __name__ == "__main__":
logger.info(f"Plotting convergence results for run ID: {run_id}")
logger.info(f"Output directory: {outdir}")
logger.info(f"Reading simulation parameters from {wd}sim_params.txt...")
with open(wd + "sim_params.txt", "r") as f:
sim_params = eval(f.read())
L = sim_params["L"] # Box size in Mpc/h
N = sim_params["N"] # Density grid size
Np = sim_params["Np"] # Number of dark matter particles per spatial dimension
Npm = sim_params["Npm"] # PM grid size
nsteps_pmref = sim_params["nsteps_pmref"]
nsteps_pm1 = sim_params["nsteps_pm1"]
nsteps_pm2 = sim_params["nsteps_pm2"]
nsteps_p3m1 = sim_params["nsteps_p3m1"]
nsteps_p3m2 = sim_params["nsteps_p3m2"]
nsteps_p3m3 = sim_params["nsteps_p3m3"]
tsd_p3m1 = sim_params["tsd_p3m1"]
tsd_p3m2 = sim_params["tsd_p3m2"]
tsd_p3m3 = sim_params["tsd_p3m3"]
p3m4 = "nsteps_p3m4" in sim_params
p3m5 = "nsteps_p3m5" in sim_params
if p3m4:
nsteps_p3m4 = sim_params["nsteps_p3m4"]
tsd_p3m4 = sim_params["tsd_p3m4"]
if p3m5:
nsteps_p3m5 = sim_params["nsteps_p3m5"]
tsd_p3m5 = sim_params["tsd_p3m5"]
logger.info(f"Reading simulation parameters from {wd}sim_params.txt done.")
if plot_fields:
logger.info(f"Loading fields from {simdir}...")
INDENT()
slice_ijk = (N // 2, slice(None), slice(None))
DELTA_LPT = read_field(simdir + "lpt_density.h5").data[slice_ijk]
DELTA_PMREF = read_field(simdir + f"pmref_nsteps{nsteps_pmref}_final_density_pm.h5").data[
slice_ijk
]
DELTA_PM1 = read_field(simdir + f"pm1_nsteps{nsteps_pm1}_final_density_pm.h5").data[
slice_ijk
]
DELTA_PM2 = read_field(simdir + f"pm2_nsteps{nsteps_pm2}_final_density_pm.h5").data[
slice_ijk
]
DELTA_P3M1 = read_field(simdir + f"p3m1_nsteps{nsteps_p3m1}_final_density_p3m.h5").data[
slice_ijk
]
DELTA_P3M2 = read_field(simdir + f"p3m2_nsteps{nsteps_p3m2}_final_density_p3m.h5").data[
slice_ijk
]
DELTA_P3M3 = read_field(simdir + f"p3m3_nsteps{nsteps_p3m3}_final_density_p3m.h5").data[
slice_ijk
]
if p3m4:
DELTA_P3M4 = read_field(simdir + f"p3m4_nsteps{nsteps_p3m4}_final_density_p3m.h5").data[
slice_ijk
]
if p3m5:
DELTA_P3M5 = read_field(simdir + f"p3m5_nsteps{nsteps_p3m5}_final_density_p3m.h5").data[
slice_ijk
]
UNINDENT()
logger.info(f"Loading fields from {simdir} done.")
diff_pm1_pmref = DELTA_PM1 - DELTA_PMREF
diff_pm2_pmref = DELTA_PM2 - DELTA_PMREF
diff_p3m1_pmref = DELTA_P3M1 - DELTA_PMREF
diff_p3m2_pmref = DELTA_P3M2 - DELTA_PMREF
diff_p3m3_pmref = DELTA_P3M3 - DELTA_PMREF
if p3m4:
diff_p3m4_pmref = DELTA_P3M4 - DELTA_PMREF
if p3m5:
diff_p3m5_pmref = DELTA_P3M5 - DELTA_PMREF
fields = [
"pmref",
"pm1",
"pm2",
"p3m1",
"p3m2",
"p3m3",
"diff_pm1_pmref",
"diff_pm2_pmref",
"diff_p3m1_pmref",
"diff_p3m2_pmref",
"diff_p3m3_pmref",
] # fields to plot
if p3m4:
fields.append("p3m4")
fields.append("diff_p3m4_pmref")
if p3m5:
fields.append("p3m5")
fields.append("diff_p3m5_pmref")
ncols = 6 # Max panels per row
figname = "_".join(fields)
slices_dict = {
"lpt": DELTA_LPT,
"pmref": DELTA_PMREF,
"pm1": DELTA_PM1,
"pm2": DELTA_PM2,
"p3m1": DELTA_P3M1,
"p3m2": DELTA_P3M2,
"p3m3": DELTA_P3M3,
"diff_pm1_pmref": diff_pm1_pmref,
"diff_pm2_pmref": diff_pm2_pmref,
"diff_p3m1_pmref": diff_p3m1_pmref,
"diff_p3m2_pmref": diff_p3m2_pmref,
"diff_p3m3_pmref": diff_p3m3_pmref,
}
if p3m4:
slices_dict["p3m4"] = DELTA_P3M4
slices_dict["diff_p3m4_pmref"] = diff_p3m4_pmref
if p3m5:
slices_dict["p3m5"] = DELTA_P3M5
slices_dict["diff_p3m5_pmref"] = diff_p3m5_pmref
titles_dict = {
"lpt": "LPT",
"pmref": f"PMref $n_\\mathrm{{steps}}={nsteps_pmref}$",
"pm1": f"PM1 $n_\\mathrm{{steps}}={nsteps_pm1}$",
"pm2": f"PM2 $n_\\mathrm{{steps}}={nsteps_pm2}$",
"p3m1": f"P3M1 $n_\\mathrm{{steps}}={nsteps_p3m1}$",
"p3m2": f"P3M2 $n_\\mathrm{{steps}}={nsteps_p3m2}$",
"p3m3": f"P3M3 $n_\\mathrm{{steps}}={nsteps_p3m3}$",
"diff_pm1_pmref": r"$\delta_{\rm PM1}-\delta_{\rm PMref}$",
"diff_pm2_pmref": r"$\delta_{\rm PM2}-\delta_{\rm PMref}$",
"diff_p3m1_pmref": r"$\delta_{\rm P3M1}-\delta_{\rm PMref}$",
"diff_p3m2_pmref": r"$\delta_{\rm P3M2}-\delta_{\rm PMref}$",
"diff_p3m3_pmref": r"$\delta_{\rm P3M3}-\delta_{\rm PMref}$",
}
if p3m4:
slices_dict["p3m4"] = DELTA_P3M4
titles_dict["p3m4"] = f"P3M4 $n_\\mathrm{{steps}}={nsteps_p3m4}$"
slices_dict["diff_p3m4_pmref"] = diff_p3m4_pmref
titles_dict["diff_p3m4_pmref"] = r"$\delta_{\rm P3M4}-\delta_{\rm PMref}$"
if p3m5:
slices_dict["p3m5"] = DELTA_P3M5
titles_dict["p3m5"] = f"P3M5 $n_\\mathrm{{steps}}={nsteps_p3m5}$"
slices_dict["diff_p3m5_pmref"] = diff_p3m5_pmref
titles_dict["diff_p3m5_pmref"] = r"$\delta_{\rm P3M5}-\delta_{\rm PMref}$"
logger.info(f"Plotting fields: {fields}...")
npanels = len(fields)
nrows = np.ceil(npanels / ncols).astype(int)
fig, axs = plt.subplots(
nrows=nrows, ncols=ncols, figsize=(3 * ncols, 4.6 * nrows), sharey=True
)
axs = axs.flatten()
ims = []
for i, key in enumerate(fields):
ax = axs[i]
data = slices_dict[key]
title = titles_dict[key]
if key.startswith("diff"):
norm = TwoSlopeNorm(
vmin=-np.log(1 + np.abs(np.min(data))),
vcenter=0,
vmax=np.log10(1 + np.abs(np.max(data))),
)
im = ax.imshow(np.sign(data) * np.log(1 + np.abs(data)), cmap="RdBu_r", norm=norm)
else:
im = ax.imshow(np.log10(2 + data), cmap=cmap)
ims.append((im, key))
ax.set_title(title, fontsize=fs_titles)
for spine in ax.spines.values():
spine.set_visible(False)
# Hide unused axes
for i in range(npanels, len(axs)):
axs[i].axis("off")
# Shared axes labels
for i, ax in enumerate(axs[:npanels]):
if i % ncols == 0:
ax.set_yticks([0, N // 2, N])
ax.set_yticklabels([f"{-L/2:.0f}", "0", f"{L/2:.0f}"], fontsize=fs)
ax.set_ylabel(r"Mpc/$h$", size=GLOBAL_FS_SMALL)
ax.set_xticks([0, N // 2, N])
ax.set_xticklabels([f"{-L/2:.0f}", "0", f"{L/2:.0f}"], fontsize=fs)
ax.set_xlabel(r"Mpc/$h$", size=GLOBAL_FS_SMALL)
# Colorbars
for ax, (im, key) in zip(axs[:npanels], ims):
divider = make_axes_locatable(ax)
cax = divider.append_axes("bottom", size="5%", pad=0.6)
cb = fig.colorbar(im, cax=cax, orientation="horizontal")
if key.startswith("diff"):
cb.set_label(
r"$\textrm{sgn}\left(\Delta\delta\right)\log_{10}(1 + |\Delta\delta|)$",
fontsize=fs,
)
else:
cb.set_label(r"$\log_{10}(2 + \delta)$", fontsize=fs)
cb.ax.tick_params(labelsize=fs)
cax.xaxis.set_ticks_position("bottom")
cax.xaxis.set_label_position("bottom")
fig.savefig(outdir + f"{figname}.png", bbox_inches="tight", dpi=300, transparent=True)
fig.savefig(outdir + f"{figname}.pdf", bbox_inches="tight", dpi=300)
plt.close(fig)
logger.info(f"Plotting fields done. Saved to {outdir}{figname}.png and {figname}.pdf")
logger.info("Computing and plotting power spectra...")
INDENT()
logger.info("Loading Fourier grid...")
G = read_FourierGrid(input_ss_file)
logger.info("Loading Fourier grid done.")
k = G.k_modes
AliasingCorr = False
logger.info("Computing power spectra...")
DELTA = read_field(simdir + "initial_density.h5")
Pk_INI, _ = get_autocorrelation(DELTA, G, AliasingCorr)
if lpt:
DELTA = read_field(simdir + "lpt_density.h5")
Pk_LPT, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"pmref_nsteps{nsteps_pmref}_final_density_pm.h5")
Pk_PMref, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"pm1_nsteps{nsteps_pm1}_final_density_pm.h5")
Pk_PM1, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"pm2_nsteps{nsteps_pm2}_final_density_pm.h5")
Pk_PM2, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"p3m1_nsteps{nsteps_p3m1}_final_density_p3m.h5")
Pk_P3M1, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"p3m2_nsteps{nsteps_p3m2}_final_density_p3m.h5")
Pk_P3M2, _ = get_autocorrelation(DELTA, G, AliasingCorr)
DELTA = read_field(simdir + f"p3m3_nsteps{nsteps_p3m3}_final_density_p3m.h5")
Pk_P3M3, _ = get_autocorrelation(DELTA, G, AliasingCorr)
if p3m4:
DELTA = read_field(simdir + f"p3m4_nsteps{nsteps_p3m4}_final_density_p3m.h5")
Pk_P3M4, _ = get_autocorrelation(DELTA, G, AliasingCorr)
if p3m5:
DELTA = read_field(simdir + f"p3m5_nsteps{nsteps_p3m5}_final_density_p3m.h5")
Pk_P3M5, _ = get_autocorrelation(DELTA, G, AliasingCorr)
logger.info("Computing power spectra done.")
if ref == "P3M1":
nsteps_reference = nsteps_p3m1
Pk_ref = Pk_P3M1
label_ref = f"Ref (P3M, $n_\\mathrm{{steps}}={nsteps_reference}$)"
elif ref == "PMref":
nsteps_reference = nsteps_pm1
Pk_ref = Pk_PMref
label_ref = f"Ref (PM, $n_\\mathrm{{steps}}={nsteps_reference}$)"
else:
raise ValueError("Invalid reference field. Choose 'P3M1' or 'PMref'.")
logger.info("Plotting time step diagnostics...")
INDENT()
file_ext = f"p3m1_nsteps{nsteps_p3m1}"
aiDrift = StandardTimeStepping.read(wd + file_ext + f"_ts_p3m.h5").aiDrift
plot_timestepping_diagnostics(
log_path=simdir + "p3m1_timestep_log.txt",
aiDrift=aiDrift,
TimeStepDistribution=tsd_p3m1,
nsteps=nsteps_p3m1,
save_path=outdir + "time_step_diagnostics_p3m1.pdf",
)
file_ext = f"p3m2_nsteps{nsteps_p3m2}"
aiDrift = StandardTimeStepping.read(wd + file_ext + f"_ts_p3m.h5").aiDrift
plot_timestepping_diagnostics(
log_path=simdir + "p3m2_timestep_log.txt",
aiDrift=aiDrift,
TimeStepDistribution=tsd_p3m2,
nsteps=nsteps_p3m2,
save_path=outdir + "time_step_diagnostics_p3m2.pdf",
)
file_ext = f"p3m3_nsteps{nsteps_p3m3}"
aiDrift = StandardTimeStepping.read(wd + file_ext + f"_ts_p3m.h5").aiDrift
plot_timestepping_diagnostics(
log_path=simdir + "p3m3_timestep_log.txt",
aiDrift=aiDrift,
TimeStepDistribution=tsd_p3m3,
nsteps=nsteps_p3m3,
save_path=outdir + "time_step_diagnostics_p3m3.pdf",
)
if p3m4:
file_ext = f"p3m4_nsteps{nsteps_p3m4}"
aiDrift = StandardTimeStepping.read(wd + file_ext + f"_ts_p3m.h5").aiDrift
plot_timestepping_diagnostics(
log_path=simdir + "p3m4_timestep_log.txt",
aiDrift=aiDrift,
TimeStepDistribution=tsd_p3m4,
nsteps=nsteps_p3m4,
save_path=outdir + "time_step_diagnostics_p3m4.pdf",
)
if p3m5:
file_ext = f"p3m5_nsteps{nsteps_p3m5}"
aiDrift = StandardTimeStepping.read(wd + file_ext + f"_ts_p3m.h5").aiDrift
plot_timestepping_diagnostics(
log_path=simdir + "p3m5_timestep_log.txt",
aiDrift=aiDrift,
TimeStepDistribution=tsd_p3m5,
nsteps=nsteps_p3m5,
save_path=outdir + "time_step_diagnostics_p3m5.pdf",
)
logger.info(f"Plotting power spectra...")
INDENT()
fig, ax = plt.subplots(figsize=(7, 4))
ax.set_xscale("log")
k = G.k_modes
kmin, kmax = k.min(), k.max()
logger.info(f"kmin = {kmin:.2e}, kmax = {kmax:.2e}")
log_pad = 0.02
log_k_min = np.log10(kmin)
log_k_max = np.log10(kmax)
log_range = log_k_max - log_k_min
xlim_min = 10 ** (log_k_min - log_pad * log_range)
xlim_max = 10 ** (log_k_max + log_pad * log_range)
plt.xlim(xlim_min, xlim_max)
# ax.set_ylim([0.2, 1.8])
dark_grey_bnd = 0.01
medium_grey_bnd = 0.05
light_grey_bnd = 0.1
ax.plot([kmin, kmax], [1, 1], color="black", linestyle="-", label=label_ref)
if lpt:
ax.plot(k, Pk_LPT / Pk_ref, label="2LPT", linestyle="--")
fields_to_plot = [
("PMref", Pk_PMref),
("PM1", Pk_PM1),
("PM2", Pk_PM2),
("P3M1", Pk_P3M1),
("P3M2", Pk_P3M2),
("P3M3", Pk_P3M3),
]
if p3m4:
fields_to_plot.append(("P3M4", Pk_P3M4))
if p3m5:
fields_to_plot.append(("P3M5", Pk_P3M5))
# Remove the field corresponding to the reference:
fields_to_plot = [(field_name, Pk) for field_name, Pk in fields_to_plot if field_name != ref]
for field_name, Pk in fields_to_plot:
label = f"{field_name}, $n_\\mathrm{{steps}}={eval(f'nsteps_{field_name.lower()}')}$"
if field_name in ["PMref", "P3M1"]:
linestyle = "-"
zorder = 1
else:
linestyle = "--"
zorder = 2
ax.plot(k, Pk / Pk_ref, label=label, linestyle=linestyle)
ax.axhspan(1 - dark_grey_bnd, 1 + dark_grey_bnd, color="grey", alpha=0.3)
ax.axhspan(1 - medium_grey_bnd, 1 + medium_grey_bnd, color="grey", alpha=0.2)
ax.axhspan(1 - light_grey_bnd, 1 + light_grey_bnd, color="grey", alpha=0.1)
for i in range(1, len(k)):
ax.axvline(k[i], color="black", linestyle=":", linewidth=1, alpha=0.1, zorder=0)
ax.yaxis.set_major_locator(plt.MaxNLocator(6))
ax.yaxis.get_major_ticks()[0].label1.set_visible(False)
ax.set_xlabel("$k$ [$h/\\mathrm{Mpc}$]", fontsize=fs)
ax.set_ylabel("$P(k)/P_\\mathrm{ref}(k)$", fontsize=fs)
ax.tick_params(which="both", direction="in")
ax.tick_params(axis="both", which="major", labelsize=fs)
ax.tick_params(axis="both", which="minor", labelsize=fs)
# Characteristic vertical reference scales
nyquist = np.pi * N / L
nyquist_PM = np.pi * Npm / L
epsilon = 0.03 * L / Np
particle_length = 2 * epsilon
xs = 1.25 * L / Npm
xr = 4.5 * xs
particle_wavenumber = 2 * np.pi / particle_length # Too large to be shown
xs_inv = 2 * np.pi / xs
xr_inv = 2 * np.pi / xr
if nyquist <= xlim_max:
line1 = ax.axvline(
x=nyquist, color="black", linestyle="--", lw=2, label="Nyquist (density grid)", zorder=0
)
if nyquist_PM <= xlim_max:
line2 = ax.axvline(
x=nyquist_PM, color="black", linestyle="-", lw=1, label="Nyquist (PM grid)", zorder=0
)
if xs_inv <= xlim_max:
line3 = ax.axvline(
x=xs_inv, color="gray", linestyle="-.", lw=2, label=r"Split wavenumber $x_s$", zorder=0
)
if xr_inv <= xlim_max:
line4 = ax.axvline(
x=xr_inv, color="gray", linestyle=":", lw=2, label=r"Short-range reach $x_r$", zorder=0
)
logger.info(f"Nyquist (density grid): {nyquist:.2f} h/Mpc")
logger.info(f"Nyquist (PM grid): {nyquist_PM:.2f} h/Mpc")
logger.info(f"Particle wavenumber: {particle_wavenumber:.2f} h/Mpc")
logger.info(f"Split wavenumber: {xs_inv:.2f} h/Mpc")
logger.info(f"Short-range reach: {xr_inv:.2f} h/Mpc")
handles, labels = plt.gca().get_legend_handles_labels()
# empty_patch = mpatches.Patch(color="none", label="")
# handles = [empty_patch, *handles]
# labels = ["", *labels]
plt.legend(
handles,
labels,
loc="upper center",
ncol=2,
bbox_to_anchor=(0.5, -0.2),
fontsize=fs,
frameon=False,
)
fig.savefig(
outdir + "power_spectra.png",
bbox_inches="tight",
dpi=300,
transparent=True,
)
fig.savefig(
outdir + "power_spectra.pdf",
bbox_inches="tight",
dpi=300,
)
plt.close(fig)
UNINDENT()
logger.info(
f"Power spectra plotted and saved to {outdir}power_spectra.png and power_spectra.pdf"
)
UNINDENT()
logger.info("Computing and plotting power spectra done.")
logger.info("All plots completed successfully.")

View file

@ -17,12 +17,17 @@ import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.colors import TwoSlopeNorm
from matplotlib.gridspec import GridSpec
from mpl_toolkits.axes_grid1 import make_axes_locatable
import cmocean.cm as cm
import plotly.graph_objects as go
from wip3m.plot_params import * # type: ignore
from wip3m.logger import getCustomLogger, INDENT, UNINDENT
logger = getCustomLogger(__name__)
# Configure global plotting settings
setup_plotting()
@ -263,7 +268,7 @@ def plot_force_distance(r, fmag, f_max=1e-1, a=None, title=None, save_path=None)
r = np.linspace(np.min(rs), np.max(rs), 300)
f_th = inverse_square(r, a)
ax.plot(r, f_th, "r-", label=r"$\propto 1/r^2$")
ax.set_xscale("log")
ax.set_yscale("log")
ax.set_xlabel(r"Distance $r$ [Mpc/$h$]")
@ -275,7 +280,7 @@ def plot_force_distance(r, fmag, f_max=1e-1, a=None, title=None, save_path=None)
plt.tight_layout()
if save_path:
plt.savefig(save_path)
plt.savefig(save_path, dpi=300)
print(f"Figure saved to: {save_path}")
plt.show()
@ -369,4 +374,295 @@ def plot_force_distance_comparison(rr, ff, ll, L, Np, Npm, ss=None, a=None, titl
if save_path:
plt.savefig(save_path)
print(f"Figure saved to: {save_path}")
plt.show()
plt.show()
def plot_timestepping_diagnostics(log_path, aiDrift=None, TimeStepDistribution=None, nsteps=None, ymin=2e-3, save_path=None, exact=True, show=True):
"""
Plot and compare timestep limiters from log file, as given by the
`compute_time_step` function in p3m.c.
Parameters
----------
log_path : str
Path to the timestep log file.
aiDrift : array_like or None
Reference array of scale factors.
TimeStepDistribution : int or None
Baseline timestep distribution mode (0, 1, or 2).
nsteps : int or None
Total number of baseline steps (used for labelling).
save_path : str or None
If provided, path to save the figure.
show : bool, optional
Whether to display the plot (default: True).
"""
logger.info(f"Plotting timestep limiters from {log_path}...")
INDENT()
a, da, da_max, da_H_approx, da_H_exact, da_dyn_approx, da_dyn_exact, da_p3m, da_late = np.loadtxt(log_path, delimiter=",", unpack=True, skiprows=0)
if aiDrift is not None:
assert np.allclose(a, aiDrift), "aiDrift does not match the time steps in the log file."
fig, ax = plt.subplots(figsize=(5.5, 4))
ax.set_ylim(ymin, 0.35)
ax.set_xlim(np.min(a), np.max(a))
# ax.loglog(a, np.ones_like(a)*1/50 / a, label=r"$\Delta a=1/50$")
ax.loglog(a, da_H_approx / a, marker="o", markersize=4, linestyle="-", label=r"$\Delta a/a\approx 0.031$ (Hubble, approx)")
ax.loglog(a, da_dyn_approx / a, marker="o", markersize=4, linestyle="-", label=r"$\Delta a\propto aH(a)\left(G\rho\right)^{-1/2}$ (Dynamical, approx)")
ax.loglog(a, da_p3m / a, marker="o", markersize=4, linestyle="-", label=r"$\Delta t\propto x_s/\sqrt{\langle v^2\rangle}$ (P3M, untuned)")
if exact:
ax.loglog(a, da_dyn_exact/a, marker="o", markersize=4, linestyle="-", label=r"$\Delta t=\eta_\textrm{d}\left(G\rho\right)^{-1/2}$ (Dynamical, exact)")
ax.loglog(a, da_H_exact / a, marker="x", markersize=4, linestyle="-", label=r"$\Delta t=0.031/H(t)$ (Hubble, exact)")
ax.loglog(a, da_late / a, marker="o", markersize=4, linestyle="-", label=r"$\Delta a_\textrm{max}=0.05$ (late)")
ls = "dotted" if TimeStepDistribution in [1, 3] else ""
ax.loglog(a, da_max / a, color="black", marker="o", markersize=4, linestyle=ls, label="CONCEPT global step")
ax.loglog(a, da / a, marker="x", markersize=4, linestyle="", label=(
rf"$\Delta a=1/{nsteps}$ (baseline)" if TimeStepDistribution == 0 else
f"Baseline ({nsteps} log-spaced steps)" if TimeStepDistribution == 1 else
f"Actual {nsteps} steps"))
ax.set_xlabel(r"Scale factor $a$", fontsize=12)
ax.set_ylabel(r"$\Delta a/a$", fontsize=12)
ax.xaxis.grid(True, which="both", linestyle=":", linewidth=0.6)
ax.tick_params(labelsize=10)
fig.subplots_adjust(bottom=0.37)
fig.legend(fontsize=10, loc='lower center', ncol=2, frameon=False)
if save_path:
plt.savefig(save_path)
logger.info(f"Figure saved to: {save_path}")
UNINDENT()
logger.info(f"Plotting timestep limiters from {log_path} done.")
if show:
plt.show()
# plt.close(fig)
def plot_custom_timestepping_diagnostics(
log_path,
aiDrift=None,
TimeStepDistribution=None,
nsteps=None,
fac_hubble=None,
fac_bend=None,
fac_p3m_fit=None,
da_max_early=None,
da_max_late=None,
plot_bend=True,
ymin=2e-3,
ymax=0.35,
save_path=None,
show=True,
):
"""
Plot and compare timestep limiters from log file, as given by the
`compute_time_step` function in p3m.c.
Parameters
----------
log_path : str
Path to the timestep log file.
aiDrift : array_like or None
Reference array of scale factors.
TimeStepDistribution : int or None
Baseline timestep distribution mode (0, 1, or 2).
nsteps : int or None
Total number of baseline steps (used for labelling).
fac_hubble : float, optional
Factor for the Hubble timestep limiter (default: None).
fac_bend : float, optional
Factor for the bend timestep limiter (default: None).
fac_p3m_fit : float, optional
Factor for the P3M fit timestep limiter (default: None).
da_max_early : float, optional
Maximum timestep allowed for early stages (default: None).
da_max_late : float, optional
Maximum timestep allowed for late stages (default: None).
plot_bend : bool, optional
Whether to plot the bend limiter (default: True).
ymin : float, optional
Minimum y-axis limit for the plot (default: 5e-3).
save_path : str or None
If provided, path to save the figure.
show : bool, optional
Whether to display the plot (default: True).
"""
log_path_custom = log_path[:-4] + "_custom.txt"
logger.info(f"Plotting timestep limiters from {log_path} and {log_path_custom}...")
INDENT()
a, da, da_max, da_dyn, da_bend, da_p3m, da_p3m_fit, da_hubble, da_hubble_lenient, da_late = np.loadtxt(log_path_custom, delimiter=",", unpack=True, skiprows=0)
if aiDrift is not None:
assert np.allclose(a, aiDrift), "aiDrift does not match the time steps in the log file."
fig, ax = plt.subplots(figsize=(5.5, 4))
ax.set_ylim(ymin, ymax)
ax.set_xlim(np.min(a), np.max(a))
bottom_pad = 0.37
if fac_hubble is not None:
lab = rf"$\Delta a/a= {fac_hubble:.3f}$ (Hubble)"
else:
lab = r"$\Delta a/a$ constant (Hubble)"
ax.loglog(a, da_hubble / a, marker="o", markersize=4, linestyle="-", label=lab)
if da_max_early is not None:
lab = rf"Hubble, lenient (early $\Delta a_\textrm{{max}}={da_max_early:.3f}$)"
else:
lab = "Hubble, lenient"
ax.loglog(a, da_hubble_lenient / a, marker="o", markersize=4, linestyle="-", label=lab)
ax.loglog(a, da_dyn / a, marker="o", markersize=4, linestyle="-", label=r"$\Delta a\propto aH(a)\left(G\rho\right)^{-1/2}$ (Dynamical)")
if plot_bend:
if fac_bend is not None:
lab = rf"Bend-approx. P3M ($\eta_\textrm{{P}}={fac_bend:.5f}$)"
else:
lab = "Bend-approx. P3M"
ax.loglog(a, da_bend / a, marker="o", markersize=4, linestyle="-", label=lab)
if fac_p3m_fit is not None:
lab = rf"P3M fit ($\eta_\textrm{{Pf}}={fac_p3m_fit:.3f}$)"
ax.loglog(a, da_p3m_fit / a, marker="o", markersize=4, linestyle="-", label=lab)
bottom_pad += 0.06
# _, _, _, _, _, _, _, da_p3m_concept, _ = np.loadtxt(log_path, delimiter=",", unpack=True, skiprows=0)
# ax.loglog(a, da_p3m_concept / a, marker="o", markersize=4, linestyle="-", label=r"$\Delta t\propto x_s/\sqrt{\langle v^2\rangle}$ (P3M, untuned)")
ax.loglog(a, da_p3m / a, marker="o", markersize=4, linestyle="-", label=r"$\Delta t\propto x_s/\sqrt{\langle v^2\rangle}$ (P3M)")
if da_max_late is not None:
lab = rf"$\Delta a_\textrm{{max}}={da_max_late:.3f}$ (late)"
else:
lab = r"$\Delta a_\textrm{max}$ (late)"
ax.loglog(a, da_late / a, marker="o", markersize=4, linestyle="-", label=lab)
ls = "dotted" if TimeStepDistribution in [1, 3] else ""
ax.loglog(a, da_max / a, color="black", marker="o", markersize=4, linestyle=ls, label="Max time step allowed (custom)")
ax.loglog(a, da / a, marker=".", markersize=4, linestyle="", label=(
rf"$\Delta a=1/{nsteps}$ (baseline)" if TimeStepDistribution == 0 else
f"Baseline ({nsteps} log-spaced steps)" if TimeStepDistribution == 1 else
f"Actual {nsteps} steps"))
ax.set_xlabel(r"Scale factor $a$", fontsize=12)
ax.set_ylabel(r"$\Delta a/a$", fontsize=12)
ax.xaxis.grid(True, which="both", linestyle=":", linewidth=0.6)
ax.tick_params(labelsize=10)
fig.subplots_adjust(bottom=bottom_pad)
fig.legend(fontsize=10, loc='lower center', ncol=2, frameon=False)
if save_path:
plt.savefig(save_path, dpi=300)
logger.info(f"Figure saved to: {save_path}")
UNINDENT()
logger.info(f"Plotting timestep limiters from {log_path} and {log_path_custom} done.")
if show:
plt.show()
# plt.close(fig)
def log_poly_model(log_a, *coeffs):
"""Polynomial model. log(y) = c0 + c1*log(a) + c2*log(a)^2 + ..."""
return sum(c * log_a**i for i, c in enumerate(coeffs))
def fit_p3m(a, da_p3m, degree=3, amin=1e-1):
"""
Fit log(da_p3m / a) = poly(log(a)) for a > 1e-1.
Parameters
----------
a : ndarray
Scale factor.
da_p3m : ndarray
P3M limiter values.
degree : int
Polynomial degree.
Returns
-------
coeffs : ndarray
Polynomial coefficients [c0, ..., c_degree].
"""
from scipy.optimize import curve_fit
mask = a > amin
if not np.any(mask):
raise ValueError(f"No values of 'a' greater than {amin} found.")
log_a = np.log(a[mask])
log_y = np.log(da_p3m[mask] / a[mask])
if degree == 3:
initial_guess = [-3.0, 0.2, 2.5, 0.6]
elif degree == 5:
initial_guess = [-3.5, -0.3, -0.5, -4.0, -3.0, -0.5]
else:
initial_guess = np.ones(degree + 1)
coeffs, _ = curve_fit(log_poly_model, log_a, log_y, p0=initial_guess)
return coeffs
def evaluate_polynomial_log(coeffs, a):
log_a = np.log(a)
return np.exp(log_poly_model(log_a, *coeffs))
def plot_p3m(a, da_p3m, coeffs, amin_fit=1e-1, amin_plot=5e-2):
"""
Plot da_p3m/a, fit, and residuals.
Parameters
----------
a : ndarray
Scale factor values.
da_p3m : ndarray
P3M limiter values.
coeffs : ndarray
Polynomial coefficients.
amin_fit : float
Minimum a value used in the fit (for legend).
amin_plot: float
Minimum a value to include in plot.
"""
mask = a > amin_plot # Only plot for a > amin_plot
a = a[mask]
da_p3m = da_p3m[mask]
ratio = da_p3m / a
a_dense = np.logspace(np.log10(a.min()), np.log10(a.max()), 500)
fit_vals = evaluate_polynomial_log(coeffs, a_dense)
fit_interp = evaluate_polynomial_log(coeffs, a)
# ratio = da_p3m/a
# ratio_fit = exp(fit(log(da_p3m)/log(a))) and we're interested in
# the residuals, res, of the da_p3m fit, that is,
# res = log(da_p3m) - log_da_p3m_fit
# = log(da_p3m) - log(exp(log_da_p3m_fit))
# = log(da_p3m) - log(a*fit_interp)
# = log(da_p3m / (a*fit_interp))
# = log(da_p3m/a / fit_interp)
# = log(ratio / fit_interp)
# res_exp = ratio / fit_interp
residuals = ratio / fit_interp
fig = plt.figure(figsize=(6, 3.5))
gs = GridSpec(2, 1, height_ratios=[3, 1], hspace=0.05)
ax1 = fig.add_subplot(gs[0])
ax2 = fig.add_subplot(gs[1], sharex=ax1)
# P3M limiter and fit
ax1.plot(a, ratio, ".", label=r"$\Delta t \propto x_s/\sqrt{\langle v^2 \rangle}$", alpha=0.7)
ax1.plot(a_dense, fit_vals, "-", label=rf"Fit for $a > {amin_fit}$", lw=1.8)
ax1.set_yscale("log")
ax1.set_xscale("log")
ax1.set_ylabel(r"$\Delta a/a$")
ax1.legend(loc="upper right", frameon=False)
ax1.tick_params(which="both", direction="in", top=True, right=True)
ax1.grid(True, which="major", ls=":", lw=0.5)
# Residuals
ax2.plot(a, residuals, "o", color="grey", alpha=0.6, markersize=3)
ax2.axhline(1.0, color="black", lw=1, ls="--")
ax2.set_ylabel("res", labelpad=2)
ax2.set_xlabel(r"Scale factor $a$")
ax2.set_xscale("log")
ax2.set_yscale("log")
ax2.tick_params(which="both", direction="in", top=True, right=True)
ax2.grid(True, which="major", ls=":", lw=0.5)
ax1.tick_params(labelbottom=False)
ax2.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda y, _: f"{y:g}"))
plt.tight_layout()
plt.show()

View file

@ -20,6 +20,7 @@ import numpy as np
from pysbmy.fft import read_FourierGrid
from pysbmy.field import read_field
from pysbmy.correlations import get_autocorrelation
from pysbmy.timestepping import StandardTimeStepping
from wip3m import *
from wip3m.tools import none_bool_str
@ -109,6 +110,10 @@ if __name__ == "__main__":
nsteps_p3m1 = sim_params["nsteps_p3m1"]
nsteps_p3m2 = sim_params["nsteps_p3m2"]
nsteps_p3m3 = sim_params["nsteps_p3m3"]
tsd_spm = sim_params["tsd_spm"]
tsd_p3m1 = sim_params["tsd_p3m1"]
tsd_p3m2 = sim_params["tsd_p3m2"]
tsd_p3m3 = sim_params["tsd_p3m3"]
logger.info(f"Reading simulation parameters from {wd}sim_params.txt done.")
if plot_fields:
@ -275,9 +280,6 @@ if __name__ == "__main__":
G = read_FourierGrid(input_ss_file)
logger.info("Loading Fourier grid done.")
# Exit here with "0" for debugging purposes:
exit(0)
k = G.k_modes
AliasingCorr = False
@ -326,6 +328,49 @@ if __name__ == "__main__":
else:
raise ValueError("Invalid reference field. Choose 'P3M1' or 'PMref'.")
logger.info("Plotting time step diagnostics...")
INDENT()
file_ext = f"spm_nsteps{nsteps_spm}"
aiDrift = StandardTimeStepping.read(wd + file_ext + f"_ts_spm.h5").aiDrift
plot_timestepping_diagnostics(
log_path=simdir + "spm_timestep_log.txt",
aiDrift=aiDrift,
TimeStepDistribution=tsd_spm,
nsteps=nsteps_spm,
save_path=outdir + "time_step_diagnostics_spm.pdf",
)
file_ext = f"p3m1_nsteps{nsteps_p3m1}"
aiDrift = StandardTimeStepping.read(wd + file_ext + f"_ts_p3m.h5").aiDrift
plot_timestepping_diagnostics(
log_path=simdir + "p3m1_timestep_log.txt",
aiDrift=aiDrift,
TimeStepDistribution=tsd_p3m1,
nsteps=nsteps_p3m1,
save_path=outdir + "time_step_diagnostics_p3m1.pdf",
)
file_ext = f"p3m2_nsteps{nsteps_p3m2}"
aiDrift = StandardTimeStepping.read(wd + file_ext + f"_ts_p3m.h5").aiDrift
plot_timestepping_diagnostics(
log_path=simdir + "p3m2_timestep_log.txt",
aiDrift=aiDrift,
TimeStepDistribution=tsd_p3m2,
nsteps=nsteps_p3m2,
save_path=outdir + "time_step_diagnostics_p3m2.pdf",
)
file_ext = f"p3m3_nsteps{nsteps_p3m3}"
aiDrift = StandardTimeStepping.read(wd + file_ext + f"_ts_p3m.h5").aiDrift
plot_timestepping_diagnostics(
log_path=simdir + "p3m3_timestep_log.txt",
aiDrift=aiDrift,
TimeStepDistribution=tsd_p3m3,
nsteps=nsteps_p3m3,
save_path=outdir + "time_step_diagnostics_p3m3.pdf",
)
logger.info(f"Plotting power spectra...")
INDENT()
fig, ax = plt.subplots(figsize=(7, 4))
@ -397,18 +442,22 @@ if __name__ == "__main__":
particle_wavenumber = 2 * np.pi / particle_length # Too large to be shown
xs_inv = 2 * np.pi / xs
xr_inv = 2 * np.pi / xr
line1 = ax.axvline(
x=nyquist, color="black", linestyle="--", lw=2, label="Nyquist (density grid)", zorder=0
)
line1 = ax.axvline(
x=nyquist_PM, color="black", linestyle="-", lw=1, label="Nyquist (PM grid)", zorder=0
)
line3 = ax.axvline(
x=xs_inv, color="gray", linestyle="-.", lw=2, label=r"Split wavenumber $x_s$", zorder=0
)
line4 = ax.axvline(
x=xr_inv, color="gray", linestyle=":", lw=2, label=r"Short-range reach $x_r$", zorder=0
)
if nyquist <= xlim_max:
line1 = ax.axvline(
x=nyquist, color="black", linestyle="--", lw=2, label="Nyquist (density grid)", zorder=0
)
if nyquist_PM <= xlim_max:
line2 = ax.axvline(
x=nyquist_PM, color="black", linestyle="-", lw=1, label="Nyquist (PM grid)", zorder=0
)
if xs_inv <= xlim_max:
line3 = ax.axvline(
x=xs_inv, color="gray", linestyle="-.", lw=2, label=r"Split wavenumber $x_s$", zorder=0
)
if xr_inv <= xlim_max:
line4 = ax.axvline(
x=xr_inv, color="gray", linestyle=":", lw=2, label=r"Short-range reach $x_r$", zorder=0
)
logger.info(f"Nyquist (density grid): {nyquist:.2f} h/Mpc")
logger.info(f"Nyquist (PM grid): {nyquist_PM:.2f} h/Mpc")

View file

@ -19,6 +19,7 @@ import os
import gc
import numpy as np
from wip3m import *
from wip3m.logger import getCustomLogger
logger = getCustomLogger(__name__)
@ -98,6 +99,8 @@ def generate_sim_params(params_dict, ICs, workdir, outdir, file_ext=None, force=
Directory where to store the simulation outputs.
file_ext : str, optional
Prefix for the output files.
force : bool, optional
If True, force regeneration of the parameter file.
Returns
-------
@ -107,7 +110,7 @@ def generate_sim_params(params_dict, ICs, workdir, outdir, file_ext=None, force=
"""
from os.path import isfile
from pysbmy import param_file
from pysbmy.timestepping import StandardTimeStepping
from pysbmy.timestepping import StandardTimeStepping, P3MTimeStepping
method = params_dict["method"]
path = workdir + file_ext + "_" if file_ext else workdir
@ -129,16 +132,51 @@ def generate_sim_params(params_dict, ICs, workdir, outdir, file_ext=None, force=
# Generate the time-stepping distribution
if method in ["pm", "cola", "spm", "p3m"]:
TimeStepDistribution = params_dict["TimeStepDistribution"]
fac_dyn_custom = params_dict.get("fac_dyn_custom", DEFAULT_FAC_DYN_CUSTOM)
da_max_early_custom = params_dict.get("da_max_early_custom", DEFAULT_DA_MAX_EARLY_CUSTOM)
fac_H_custom = params_dict.get("fac_H_custom", DEFAULT_FAC_H_CUSTOM)
fac_bend = params_dict.get("fac_bend", DEFAULT_FAC_BEND)
sub_bend1 = params_dict.get("sub_bend1", DEFAULT_SUB_BEND1)
sub_bend2 = params_dict.get("sub_bend2", DEFAULT_SUB_BEND2)
da_max_late_custom = params_dict.get("da_max_late_custom", DEFAULT_DA_MAX_LATE_CUSTOM)
ts_filename = path + "ts_" + method + ".h5"
logger.info("Time-stepping distribution file: %s", ts_filename)
if not isfile(ts_filename) or force:
TimeStepDistribution = params_dict["TimeStepDistribution"]
ai = params_dict["ai"]
af = params_dict["af"]
nsteps = params_dict["nsteps"]
snapshots = np.full((nsteps), False)
TS = StandardTimeStepping(ai, af, snapshots, TimeStepDistribution)
if TimeStepDistribution in [0, 1, 2]:
ai = params_dict["ai"]
af = params_dict["af"]
nsteps = params_dict["nsteps"]
snapshots = np.full((nsteps), False)
TS = StandardTimeStepping(ai, af, snapshots, TimeStepDistribution)
elif TimeStepDistribution == 3:
ai = params_dict["ai"]
af = params_dict["af"]
p3m_fit_coeffs = params_dict.get("p3m_fit_coeffs", None)
use_p3m_fit = params_dict.get("use_p3m_fit", False)
fac_p3m_fit = params_dict.get("fac_p3m_fit", DEFAULT_FAC_P3M_FIT)
TS = P3MTimeStepping(
ai=params_dict["ai"],
af=params_dict["af"],
cosmo=params_dict["cosmo_dict"],
fac_dynamical=fac_dyn_custom,
delta_a_max_early=da_max_early_custom,
fac_hubble=fac_H_custom,
fac_bend=fac_bend,
sub_bend1=sub_bend1,
sub_bend2=sub_bend2,
delta_a_max_late=da_max_late_custom,
p3m_fit_coeffs=p3m_fit_coeffs,
use_p3m_fit=use_p3m_fit,
fac_p3m_fit=fac_p3m_fit,
forces_every=1,
)
else:
raise ValueError("Invalid TimeStepDistribution: {}".format(TimeStepDistribution))
TS.write(ts_filename)
logger.info("TS.ai = %f, TS.af = %f, TS.nsteps = %d", TS.ai, TS.af, TS.nsteps)
else:
logger.info("Time-stepping distribution file already exists: %s", ts_filename)
StandardTimeStepping.read(ts_filename).plot(savepath=path + "ts_" + method + ".png")
@ -277,6 +315,13 @@ def generate_sim_params(params_dict, ICs, workdir, outdir, file_ext=None, force=
wa_fld=0.0,
)
elif params_dict["method"] == "p3m" or params_dict["method"] == "spm":
if params_dict["EvolutionMode"] == 6 or params_dict["EvolutionMode"] == 7:
## Special case for COLA ##
sub_bend1_diagnostic = params_dict.get("sub_bend1", DEFAULT_SUB_BEND1_COLA)
sub_bend2_diagnostic = params_dict.get("sub_bend2", DEFAULT_SUB_BEND2_COLA)
else:
sub_bend1_diagnostic = params_dict.get("sub_bend1", DEFAULT_SUB_BEND1)
sub_bend2_diagnostic = params_dict.get("sub_bend2", DEFAULT_SUB_BEND2)
S = param_file(
# Basic setup:
Particles=Particles,
@ -306,11 +351,22 @@ def generate_sim_params(params_dict, ICs, workdir, outdir, file_ext=None, force=
WriteFinalDensity=1,
OutputFinalDensity=simpath + "final_density_{}.h5".format(method),
n_Tiles=params_dict["n_Tiles"],
RunForceDiagnostic=params_dict["RunForceDiagnostic"],
nPairsForceDiagnostic=params_dict["nPairsForceDiagnostic"],
nBinsForceDiagnostic=params_dict["nBinsForceDiagnostic"],
maxTrialsForceDiagnostic=params_dict["maxTrialsForceDiagnostic"],
OutputForceDiagnostic=params_dict["OutputForceDiagnostic"],
RunForceDiagnostic=params_dict.get("RunForceDiagnostic", False),
nPairsForceDiagnostic=params_dict.get("nPairsForceDiagnostic", 0),
nBinsForceDiagnostic=params_dict.get("nBinsForceDiagnostic", 0),
maxTrialsForceDiagnostic=params_dict.get("maxTrialsForceDiagnostic", 0),
OutputForceDiagnostic=params_dict.get("OutputForceDiagnostic", ""),
PrintOutputTimestepsLog=params_dict.get("PrintOutputTimestepsLog", False),
OutputTimestepsLog=params_dict.get("OutputTimestepsLog", ""),
fac_dyn_custom=params_dict.get("fac_dyn_custom", DEFAULT_FAC_DYN_CUSTOM),
fac_H_custom=params_dict.get("fac_H_custom", DEFAULT_FAC_H_CUSTOM),
fac_bend=params_dict.get("fac_bend", DEFAULT_FAC_BEND),
sub_bend1=params_dict.get("sub_bend1", sub_bend1_diagnostic),
sub_bend2=params_dict.get("sub_bend2", sub_bend2_diagnostic),
fac_p3m_fit=params_dict.get("fac_p3m_fit", DEFAULT_FAC_P3M_FIT),
use_p3m_fit=params_dict.get("use_p3m_fit", False),
da_max_early_custom=params_dict.get("da_max_early_custom", DEFAULT_DA_MAX_EARLY_CUSTOM),
da_max_late_custom=params_dict.get("da_max_late_custom", DEFAULT_DA_MAX_LATE_CUSTOM),
#############################
## Cosmological parameters ##
#############################
@ -428,12 +484,19 @@ def run_simulation(name, params, wd, logdir):
from wip3m.low_level import stderr_redirector
from pysbmy import pySbmy
file_ext = f"{name}_nsteps{params['nsteps']}" if params.get("nsteps") is not None else None
if params.get("nsteps") is not None:
file_ext = f"{name}_nsteps{params['nsteps']}"
elif params.get("relax") is not None:
file_ext = f"{name}_relax{str(params['relax']).replace('.', '_')}"
elif params.get("file_ext") is not None:
file_ext = params["file_ext"]
else:
file_ext = None
method = params["method"]
path = wd + file_ext + "_" if file_ext else wd
sbmy_path = joinstrs([path, "example_", method, ".sbmy"])
log_path = joinstrs([logdir, file_ext, method, ".txt"])
f = BytesIO()
with stderr_redirector(f):
pySbmy(sbmy_path, log_path)

View file

@ -0,0 +1,86 @@
#!/bin/bash
#SBATCH --job-name=cola_2_1_05_L64_N64_Np64_v4
#SBATCH --output=/data70/hoellinger/WIP3M/cola_2_1_05_L64_N64_Np64_v4/log.log
#SBATCH --error=/data70/hoellinger/WIP3M/cola_2_1_05_L64_N64_Np64_v4/err.err
#SBATCH --nodes=1 # Number of nodes (value or min-max)
#SBATCH --ntasks=64 # The number of tasks (i.e. cores) per node
#SBATCH --partition=comp,pscomp # Partition name
#SBATCH --time=12:00:00
##SBATCH --exclusive
##SBATCH --nodelist=i26 # Node name
##SBATCH --mem=64G # Memory pool for all cores (see also --mem-per-cpu)
##SBATCH --array=0-10 # Size of the array
##SBATCH --constraint=? # Constraint e.g. specific node type
conda activate p3m
export OMP_NUM_THREADS=16
python $WIP3M_ROOT_PATH"src/wip3m/convergence_cola_p3m_only_expl.py" \
--run_id cola_2_1_05_L64_N64_Np64_v4 \
--L 64 \
--N 64 \
--Np 64 \
--Npm 128 \
--n_Tiles 16 \
--z_i 19.0 \
--z_f 0.0 \
--plot_fields True \
--scale_limiter "fac_p3m_fit" \
--use_p3m_fit True \
--scaling_p3m1 2.0 \
--scaling_p3m2 1.0 \
--scaling_p3m3 0.5
# export OMP_NUM_THREADS=32
# python $WIP3M_ROOT_PATH"src/wip3m/convergence_cola_p3m_only_expl.py" \
# --run_id cola_2_1_05_L128_N128_Np128 \
# --L 128 \
# --N 128 \
# --Np 128 \
# --Npm 256 \
# --n_Tiles 32 \
# --z_i 19.0 \
# --z_f 0.0 \
# --plot_fields True \
# --scale_limiter "fac_p3m_fit" \
# --use_p3m_fit True \
# --scaling_p3m1 2.0 \
# --scaling_p3m2 1.0 \
# --scaling_p3m3 0.5
# export OMP_NUM_THREADS=64
# python $WIP3M_ROOT_PATH"src/wip3m/convergence_cola_p3m_only_expl.py" \
# --run_id cola_432_L1024_N512_Np512 \
# --L 1024 \
# --N 512 \
# --Np 512 \
# --Npm 1024 \
# --n_Tiles 128 \
# --z_i 19.0 \
# --z_f 0.0 \
# --plot_fields True \
# --scale_limiter "fac_p3m_fit" \
# --use_p3m_fit True \
# --scaling_p3m1 4.0 \
# --scaling_p3m2 3.0 \
# --scaling_p3m3 2.0
# export OMP_NUM_THREADS=64
# python $WIP3M_ROOT_PATH"src/wip3m/convergence_cola_p3m_only_expl.py" \
# --run_id cola_432_L128_N256_Np128 \
# --L 128 \
# --N 256 \
# --Np 128 \
# --Npm 256 \
# --n_Tiles 32 \
# --z_i 19.0 \
# --z_f 0.0 \
# --plot_fields True \
# --scale_limiter "fac_p3m_fit" \
# --use_p3m_fit True \
# --scaling_p3m1 4.0 \
# --scaling_p3m2 3.0 \
# --scaling_p3m3 2.0
exit 0

38
submit/colalim.sh Normal file
View file

@ -0,0 +1,38 @@
#!/bin/bash
#SBATCH --job-name=cola_zi19_zf0L512_N512_Np512
#SBATCH --output=/data70/hoellinger/WIP3M/cola_zi19_zf0L512_N512_Np512/log.log
#SBATCH --error=/data70/hoellinger/WIP3M/cola_zi19_zf0L512_N512_Np512/err.err
#SBATCH --nodes=1 # Number of nodes (value or min-max)
#SBATCH --ntasks=128 # The number of tasks (i.e. cores) per node
#SBATCH --partition=comp,pscomp # Partition name
#SBATCH --time=48:00:00
##SBATCH --exclusive
##SBATCH --nodelist=i26 # Node name
##SBATCH --mem=64G # Memory pool for all cores (see also --mem-per-cpu)
##SBATCH --array=0-10 # Size of the array
##SBATCH --constraint=? # Constraint e.g. specific node type
conda activate p3m
export OMP_NUM_THREADS=64
python $WIP3M_ROOT_PATH"src/wip3m/convergence_cola_expl_parser.py" \
--run_id cola_zi19_zf0L512_N512_Np512 \
--L 512 \
--N 512 \
--Np 512 \
--Npm 1024 \
--n_Tiles 128 \
--z_i 19.0 \
--z_f 0.0 \
--plot_fields True \
--scale_limiter "fac_bend" \
--scaling_pmref 1.2 \
--scaling_pm1 1.0 \
--scaling_pm2 0.8 \
--scaling_spm 1.2 \
--scaling_p3m1 1.2 \
--scaling_p3m2 1.0 \
--scaling_p3m3 0.8
exit 0

View file

@ -1,139 +1,45 @@
#!/bin/bash
#SBATCH --job-name=cappel_L1024_Np512
#SBATCH -o /data70/hoellinger/WIP3M/cappel_L1024_Np512/log.log
#SBATCH -e /data70/hoellinger/WIP3M/cappel_L1024_Np512/err.err
#SBATCH --exclusive
#SBATCH -N 1 # Number of nodes (value or min-max)
#SBATCH -n 128 # The number of tasks (i.e. cores) per node
#SBATCH --job-name=obz99_zf19_ts0_L256_N1024_Np512
#SBATCH --output=/data70/hoellinger/WIP3M/obz99_zf19_ts0_L256_N1024_Np512/log.log
#SBATCH --error=/data70/hoellinger/WIP3M/obz99_zf19_ts0_L256_N1024_Np512/err.err
#SBATCH --nodes=1 # Number of nodes (value or min-max)
#SBATCH --ntasks=128 # The number of tasks (i.e. cores) per node
#SBATCH --partition=comp,pscomp # Partition name
#SBATCH --time=24:00:00
##SBATCH --exclusive
##SBATCH --nodelist=i26 # Node name
##SBATCH --mem=64G # Memory pool for all cores (see also --mem-per-cpu)
##SBATCH --array=0-10 # Size of the array
##SBATCH --constraint=i1 # Constraint e.g. specific node
##SBATCH --constraint=? # Constraint e.g. specific node type
conda activate p3m
export OMP_NUM_THREADS=128
python $WIP3M_ROOT_PATH"src/wip3m/convergence_baseline_ts_parser.py" \
--run_id cappel_L1024_Np512 \
--L 1024 \
export OMP_NUM_THREADS=64
python $WIP3M_ROOT_PATH"src/wip3m/compute_limiters_from_baseline_ts.py" \
--run_id obz99_zf19_ts0_L256_N1024_Np512\
--L 256 \
--N 1024 \
--Np 512 \
--Npm 1024 \
--n_Tiles 128 \
--nsteps_pmref 200 \
--nsteps_pm1 100 \
--nsteps_pm2 50 \
--nsteps_cola 20 \
--nsteps_spm 400 \
--nsteps_p3m1 400 \
--nsteps_p3m2 300 \
--nsteps_p3m3 200
# python $WIP3M_ROOT_PATH"src/wip3m/convergence_baseline_ts_parser.py" \
# --run_id cappel_Np512 \
# --L 512 \
# --N 1024 \
# --Np 512 \
# --Npm 1024 \
# --n_Tiles 128 \
# --nsteps_pmref 200 \
# --nsteps_pm1 100 \
# --nsteps_pm2 50 \
# --nsteps_cola 20 \
# --nsteps_spm 400 \
# --nsteps_p3m1 400 \
# --nsteps_p3m2 300 \
# --nsteps_p3m3 200
# python $WIP3M_ROOT_PATH"src/wip3m/convergence_baseline_ts_parser.py" \
# --run_id cappel_loga_Np512 \
# --L 512 \
# --N 1024 \
# --Np 512 \
# --Npm 1024 \
# --n_Tiles 128 \
# --nsteps_pmref 200 \
# --nsteps_pm1 100 \
# --nsteps_pm2 50 \
# --nsteps_cola 20 \
# --nsteps_spm 400 \
# --nsteps_p3m1 400 \
# --nsteps_p3m2 300 \
# --nsteps_p3m3 200 \
# --tsd_pmref 1 \
# --tsd_pm1 1 \
# --tsd_pm2 1 \
# --tsd_cola 1 \
# --tsd_spm 1 \
# --tsd_p3m1 1 \
# --tsd_p3m2 1 \
# --tsd_p3m3 1
# python $WIP3M_ROOT_PATH"src/wip3m/convergence_baseline_ts_parser.py" \
# --run_id brabois_Np512_L1024 \
# --L 1024 \
# --N 1024 \
# --Np 512 \
# --Npm 1024 \
# --n_Tiles 128 \
# --nsteps_pmref 100 \
# --nsteps_pm1 50 \
# --nsteps_pm2 20 \
# --nsteps_cola 20 \
# --nsteps_spm 200 \
# --nsteps_p3m1 200 \
# --nsteps_p3m2 100 \
# --nsteps_p3m3 50
# python $WIP3M_ROOT_PATH"src/wip3m/convergence_baseline_ts_parser.py" \
# --run_id brabois_Np512 \
# --L 512 \
# --N 1024 \
# --Np 512 \
# --Npm 1024 \
# --n_Tiles 128 \
# --nsteps_pmref 100 \
# --nsteps_pm1 50 \
# --nsteps_pm2 20 \
# --nsteps_cola 20 \
# --nsteps_spm 200 \
# --nsteps_p3m1 200 \
# --nsteps_p3m2 100 \
# --nsteps_p3m3 50
# python $WIP3M_ROOT_PATH"src/wip3m/convergence_baseline_ts_parser.py" \
# --run_id brabois_Np256 \
# --L 512 \
# --N 512 \
# --Np 256 \
# --Npm 512 \
# --n_Tiles 64 \
# --nsteps_pmref 200 \
# --nsteps_pm1 100 \
# --nsteps_pm2 50 \
# --nsteps_cola 20 \
# --nsteps_spm 400 \
# --nsteps_p3m1 400 \
# --nsteps_p3m2 300 \
# --nsteps_p3m3 200
# python $WIP3M_ROOT_PATH"src/wip3m/convergence_baseline_ts_parser.py" \
# --run_id brabois_Np256_fine \
# --L 512 \
# --N 1024 \
# --Np 256 \
# --Npm 512 \
# --n_Tiles 64 \
# --nsteps_pmref 200 \
# --nsteps_pm1 100 \
# --nsteps_pm2 50 \
# --nsteps_cola 20 \
# --nsteps_spm 400 \
# --nsteps_p3m1 400 \
# --nsteps_p3m2 300 \
# --nsteps_p3m3 200
--z_i 99.0 \
--z_f 19.0 \
--nsteps_pmref 50 \
--nsteps_pm1 10 \
--nsteps_pm2 5 \
--nsteps_p3m1 50 \
--nsteps_p3m2 20 \
--nsteps_p3m3 10 \
--nsteps_p3m4 5 \
--nsteps_p3m5 3 \
--tsd_pmref 0 \
--tsd_pm1 0 \
--tsd_pm2 0 \
--tsd_p3m1 0 \
--tsd_p3m2 0 \
--tsd_p3m3 0 \
--tsd_p3m4 0 \
--tsd_p3m5 0
exit 0

View file

@ -0,0 +1,38 @@
#!/bin/bash
#SBATCH --job-name=s2_tiny_zi199_zf0_L64_N256_Np128
#SBATCH --output=/data70/hoellinger/WIP3M/s2_tiny_zi199_zf0_L64_N256_Np128/log.log
#SBATCH --error=/data70/hoellinger/WIP3M/s2_tiny_zi199_zf0_L64_N256_Np128/err.err
#SBATCH --nodes=1 # Number of nodes (value or min-max)
#SBATCH --ntasks=128 # The number of tasks (i.e. cores) per node
#SBATCH --partition=comp,pscomp # Partition name
#SBATCH --time=24:00:00
##SBATCH --exclusive
##SBATCH --nodelist=i26 # Node name
##SBATCH --mem=64G # Memory pool for all cores (see also --mem-per-cpu)
##SBATCH --array=0-10 # Size of the array
##SBATCH --constraint=? # Constraint e.g. specific node type
conda activate p3m
export OMP_NUM_THREADS=64
python $WIP3M_ROOT_PATH"src/wip3m/convergence_custom_ts_expl_parser.py" \
--run_id s2_tiny_zi199_zf0_L64_N256_Np128 \
--L 64 \
--N 256 \
--Np 128 \
--Npm 256 \
--n_Tiles 32 \
--z_i 199.0 \
--z_f 0.0 \
--plot_fields True \
--scale_limiter "fac_H_custom" \
--scaling_pmref 2.0 \
--scaling_pm1 1.5 \
--scaling_pm2 1.2 \
--scaling_spm 2.0 \
--scaling_p3m1 2.0 \
--scaling_p3m2 1.5 \
--scaling_p3m3 1.2
exit 0

38
submit/limiters.sh Normal file
View file

@ -0,0 +1,38 @@
#!/bin/bash
#SBATCH --job-name=s4_tiny_zi13_5_zf0_L128_N256_Np128
#SBATCH --output=/data70/hoellinger/WIP3M/s4_tiny_zi13_5_zf0_L128_N256_Np128/log.log
#SBATCH --error=/data70/hoellinger/WIP3M/s4_tiny_zi13_5_zf0_L128_N256_Np128/err.err
#SBATCH --nodes=1 # Number of nodes (value or min-max)
#SBATCH --ntasks=64 # The number of tasks (i.e. cores) per node
#SBATCH --partition=comp,pscomp # Partition name
#SBATCH --time=24:00:00
##SBATCH --exclusive
##SBATCH --nodelist=i26 # Node name
##SBATCH --mem=64G # Memory pool for all cores (see also --mem-per-cpu)
##SBATCH --array=0-10 # Size of the array
##SBATCH --constraint=? # Constraint e.g. specific node type
conda activate p3m
export OMP_NUM_THREADS=64
python $WIP3M_ROOT_PATH"src/wip3m/convergence_custom_ts_expl_parser.py" \
--run_id s4_tiny_zi13_5_zf0_L128_N256_Np128 \
--L 128 \
--N 256 \
--Np 128 \
--Npm 256 \
--n_Tiles 32 \
--z_i 13.5 \
--z_f 0.0 \
--plot_fields True \
--scale_limiter "fac_bend" \
--scaling_pmref 4.0 \
--scaling_pm1 3.0 \
--scaling_pm2 2.0 \
--scaling_spm 4.0 \
--scaling_p3m1 4.0 \
--scaling_p3m2 3.0 \
--scaling_p3m3 2.0
exit 0

50
submit/only_cola_p3m.sh Normal file
View file

@ -0,0 +1,50 @@
#!/bin/bash
#SBATCH --job-name=colazf08_2_08_07_L512_N1024_Np512
#SBATCH --output=/data70/hoellinger/WIP3M/colazf08_2_08_07_L512_N1024_Np512/log.log
#SBATCH --error=/data70/hoellinger/WIP3M/colazf08_2_08_07_L512_N1024_Np512/err.err
#SBATCH --nodes=1 # Number of nodes (value or min-max)
#SBATCH --ntasks=128 # The number of tasks (i.e. cores) per node
#SBATCH --partition=comp,pscomp # Partition name
#SBATCH --time=24:00:00
##SBATCH --exclusive
##SBATCH --nodelist=i26 # Node name
##SBATCH --mem=64G # Memory pool for all cores (see also --mem-per-cpu)
##SBATCH --array=0-10 # Size of the array
##SBATCH --constraint=? # Constraint e.g. specific node type
conda activate p3m
# export OMP_NUM_THREADS=64
# python $WIP3M_ROOT_PATH"src/wip3m/convergence_cola_p3m_only_expl.py" \
# --run_id cola_H_L128_N128_Np128 \
# --L 128 \
# --N 128 \
# --Np 128 \
# --Npm 256 \
# --n_Tiles 32 \
# --z_i 19.0 \
# --z_f 0.0 \
# --plot_fields True \
# --scale_limiter "fac_H_custom" \
# --scaling_p3m1 1.0 \
# --scaling_p3m2 0.6 \
# --scaling_p3m3 0.3
export OMP_NUM_THREADS=64
python $WIP3M_ROOT_PATH"src/wip3m/convergence_cola_p3m_only_expl.py" \
--run_id colazf08_2_08_07_L512_N1024_Np512 \
--L 512 \
--N 1024 \
--Np 512 \
--Npm 1024 \
--n_Tiles 128 \
--z_i 19.0 \
--z_f 0.8 \
--plot_fields True \
--scale_limiter "fac_bend" \
--scaling_p3m1 2.0 \
--scaling_p3m2 0.8 \
--scaling_p3m3 0.7
exit 0

View file

@ -0,0 +1,23 @@
#!/bin/bash
#SBATCH --job-name=obz99_zf19_L128_N512_Np256
#SBATCH -o /data70/hoellinger/WIP3M/obz99_zf19_L128_N512_Np256/plots.log
#SBATCH -N 1 # Number of nodes (value or min-max)
#SBATCH -n 128 # The number of tasks (i.e. cores) per node
#SBATCH --partition=comp,pscomp # Partition name
#SBATCH --time=12:00:00
##SBATCH --exclusive
##SBATCH --mem=64G # Memory pool for all cores (see also --mem-per-cpu)
##SBATCH --array=0-10 # Size of the array
##SBATCH --constraint=? # Constraint e.g. specific node type
conda activate p3m
export OMP_NUM_THREADS=64
python $WIP3M_ROOT_PATH"src/wip3m/plot_limiters_from_baseline_ts.py" \
--run_id obz99_zf19_L128_N512_Np256 \
--outdir_suffix plots \
--lpt False
exit 0

View file

@ -1,24 +1,24 @@
#!/bin/bash
#SBATCH --job-name=cappel_L1024_Np512
#SBATCH -o /data70/hoellinger/WIP3M/cappel_L1024_Np512/plots.log
#SBATCH --exclusive
#SBATCH --job-name=ob_tsd1_L512_N1024_Np512
#SBATCH -o /data70/hoellinger/WIP3M/ob_tsd1_L512_N1024_Np512/plots.log
#SBATCH -N 1 # Number of nodes (value or min-max)
#SBATCH -n 128 # The number of tasks (i.e. cores) per node
#SBATCH --time=2:00:00
#SBATCH --partition=comp,pscomp # Partition name
#SBATCH --time=12:00:00
##SBATCH --exclusive
##SBATCH --mem=64G # Memory pool for all cores (see also --mem-per-cpu)
##SBATCH --array=0-10 # Size of the array
##SBATCH --constraint=i1 # Constraint e.g. specific node
##SBATCH --constraint=? # Constraint e.g. specific node type
conda activate p3m
export OMP_NUM_THREADS=64
python $WIP3M_ROOT_PATH"src/wip3m/plots_convergence_baseline_ts_parser.py" \
--run_id cappel_L1024_Np512 \
--run_id ob_tsd1_L512_N1024_Np512 \
--outdir_suffix plots \
--lpt True \
--cola False
--lpt False \
--cola True
exit 0