From 3b0948301d64dafa6f355e8df8991efec668813f Mon Sep 17 00:00:00 2001 From: Simone Albanesi Date: Wed, 8 Jul 2026 17:11:01 +0200 Subject: [PATCH 1/5] Redirected stdout to logger instead of null before sxsmod.load. Removed stderr redirection to null. Added tols in D1-uniform-check --- PyART/catalogs/sxs.py | 10 +++++----- PyART/utils/utils.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/PyART/catalogs/sxs.py b/PyART/catalogs/sxs.py index 64e9c7e3..0add4d12 100644 --- a/PyART/catalogs/sxs.py +++ b/PyART/catalogs/sxs.py @@ -6,6 +6,7 @@ import json from ..waveform import Waveform from ..utils import cat_utils as cat_ut +from ..utils.utils import LoggerWriter class Waveform_SXS(Waveform): @@ -284,9 +285,9 @@ def download_simulation( # based on the logging level, redirect stdout to null # This is because the sxs module prints a lot of information to stdout original_stdout = sys.stdout - original_stderr = sys.stderr - sys.stdout = open(os.devnull, "w") - sys.stderr = open(os.devnull, "w") + sys.stdout = LoggerWriter(logging.getLogger(__name__)) + # sys.stdout = open(os.devnull, "w") + sxs_sim = sxsmod.load( name_level, extrapolation_order=extrapolation_order, @@ -408,9 +409,8 @@ def download_simulation( if ":" in fld: shutil.rmtree(os.path.join(os.environ["SXSCACHEDIR"], fld)) - # Restore stdout/stderr + # Restore stdout sys.stdout = original_stdout - sys.stderr = original_stderr pass diff --git a/PyART/utils/utils.py b/PyART/utils/utils.py index f61578a9..bb39236a 100644 --- a/PyART/utils/utils.py +++ b/PyART/utils/utils.py @@ -791,7 +791,7 @@ def D02(xp, yp, pad=True): return dyp -def D1(f, x, order=4, uniform_check=True): +def D1(f, x, order=4, uniform_check=True, uc_atol=1e-4, uc_rtol=1e-4): """ Computes the first derivative of function f(x) @@ -804,9 +804,12 @@ def D1(f, x, order=4, uniform_check=True): order : int, optional finite differencing order (default is 4) uniform_check: bool, optional - check that the arrayr has uniform spacing + check that the array has uniform spacing (default is true) - + uc_atol : float, optional + atol to use in np.allclose if uniform_check is True + uc_rtol : float, optional + rtol to use in np.allclose if uniform_check is True Returns ------- df : list (or numpy array) @@ -815,7 +818,7 @@ def D1(f, x, order=4, uniform_check=True): if uniform_check: dx = np.diff(x) - is_constant = np.allclose(dx, dx[0]) + is_constant = np.allclose(dx, dx[0], rtol=uc_rtol, atol=uc_atol) if not is_constant: raise RuntimeError("Array not uniformly spaced") @@ -1322,3 +1325,23 @@ def refine_local(idx, find_max=True): tap = refine_local(idxap, find_max=True) tpe = refine_local(idxpe, find_max=False) return tap, tpe + + +class LoggerWriter: + """ + Small class to redirect stdout to logging like: + original_stdout = sys.stdout + sys.stdout = LoggerWriter(logger) + """ + + def __init__(self, logger, level=logging.INFO): + self.logger = logger + self.level = level + + def write(self, message): + message = message.rstrip() + if message: + self.logger.log(self.level, message) + + def flush(self): + pass # Needed because sys.stdout expects it From cc7f4ee319da72d289e65542b114cb37eadb5649 Mon Sep 17 00:00:00 2001 From: Simone Albanesi Date: Wed, 8 Jul 2026 18:44:50 +0200 Subject: [PATCH 2/5] Added possibility to updated downloaded SXS data by adding missing N-level --- PyART/catalogs/sxs.py | 65 ++++++++++++++++++++++++++++--------------- requirements.txt | 2 +- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/PyART/catalogs/sxs.py b/PyART/catalogs/sxs.py index 0add4d12..3ad7d17a 100644 --- a/PyART/catalogs/sxs.py +++ b/PyART/catalogs/sxs.py @@ -111,7 +111,7 @@ def __init__( raise ValueError("basename is None, but unknown src!") self.basename = basename - if self.level is not None and isinstance(self.level, int): + if isinstance(self.level, int): levpath = f"{self.sxs_data_path}/Lev{self.level}" else: levpath = self.sxs_data_path @@ -125,12 +125,29 @@ def __init__( levpath = None else: levpath = None - self.check_cut_consistency() - if levpath is None or not os.path.exists(levpath): + + needs_download = levpath is None or not os.path.exists(levpath) + if not needs_download: + # if files are already downloaded, additionally check that N-order + # is there as well. Note: if we enter here, levpath is not None + # and the path exists, and thus lev_dirs not empty + order_group = f"Extrapolated_N{self.order}.dir" + if self.level is None: + level = int(lev_dirs[-1].replace("Lev", "")) + fname = self.get_lev_fname(basename=self.basename, level=level) + with h5py.File(fname, "r") as f: + needs_download = order_group not in f + if needs_download: + logging.info( + f"{levpath} found, but not the requested N={self.order} order. Download needed." + ) + + if needs_download: if download: logging.info( - f"The path {self.sxs_data_path} does not exist or contains no 'Lev*' directory." + f"The path {self.sxs_data_path} does not exist, contains no 'Lev*'" + + "directory, or does not contain the requested order." ) logging.info("Downloading the simulation from the SXS catalog.") self.download_simulation( @@ -186,6 +203,9 @@ def __init__( self.load_horizon() if "psi4lm" in load: self.load_psi4lm(load_m0=load_m0) + + if self.nr is not None: + self.nr.close() pass def check_cut_consistency(self): @@ -283,10 +303,8 @@ def download_simulation( name_level = name # based on the logging level, redirect stdout to null - # This is because the sxs module prints a lot of information to stdout original_stdout = sys.stdout sys.stdout = LoggerWriter(logging.getLogger(__name__)) - # sys.stdout = open(os.devnull, "w") sxs_sim = sxsmod.load( name_level, @@ -301,17 +319,8 @@ def download_simulation( self.level = self.level or int( sxs_sim.Lev.replace("Lev", "") ) # Guarantees int(self.level) - lev = f"Lev{self.level}" - - # Create the output directory - sxs_dir = f"SXS_{self.src}_{ID}" - # Only add sxs_dir if it's not already the last part of the path - if not path.endswith(sxs_dir): - full_path = os.path.join(path, sxs_dir) - else: - full_path = path - out_dir = os.path.join(full_path, lev) + out_dir = self.get_lev_fname(level=self.level, basename="") os.makedirs(out_dir, exist_ok=True) # Save hlm data if requested @@ -339,10 +348,16 @@ def download_simulation( ) continue # create the h5 file - h5file = h5py.File( - os.path.join(out_dir, f"rhOverM_Asymptotic_GeometricUnits_CoM.h5"), "w" + # h5file = h5py.File(os.path.join(out_dir, f"rhOverM_Asymptotic_GeometricUnits_CoM.h5"), "w") + # save_dict_to_h5(h5file, to_h5file) + filename = os.path.join( + out_dir, f"rhOverM_Asymptotic_GeometricUnits_CoM.h5" ) - save_dict_to_h5(h5file, to_h5file) + with h5py.File(filename, "a") as h5file: + if extp in h5file: + logging.info(f"{extp} already present, skipping.") + else: + save_dict_to_h5(h5file, {extp: to_h5file[extp]}) h5file.close() logging.info("Saved hlm data.") @@ -363,10 +378,14 @@ def download_simulation( to_h5file[extp][mode_string] = wav[mode_string] # create the h5 file - h5file = h5py.File( - os.path.join(out_dir, f"rMPsi4_Asymptotic_GeometricUnits_CoM.h5"), "w" - ) - save_dict_to_h5(h5file, to_h5file) + # h5file = h5py.File(os.path.join(out_dir, f"rMPsi4_Asymptotic_GeometricUnits_CoM.h5"), "w") + # save_dict_to_h5(h5file, to_h5file) + filename = os.path.join(out_dir, f"rMPsi4_Asymptotic_GeometricUnits_CoM.h5") + with h5py.File(filename, "a") as h5file: + if extp in h5file: + logging.info(f"{extp} already present, skipping.") + else: + save_dict_to_h5(h5file, {extp: to_h5file[extp]}) h5file.close() logging.info("Saved psi4lm data.") diff --git a/requirements.txt b/requirements.txt index 141c70ca..161a5c70 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ pycbc>=2.4.0 # gw-eccentricity>=1.0.4 # mayawaves>=2024.4.25 lalsuite>=7.22 -phenomxpy @ git+https://gitlab.com/imrphenom-dev/phenomxpy.git +#phenomxpy @ git+https://gitlab.com/imrphenom-dev/phenomxpy.git # SXS catalog support (optional; needed for downloading SXS simulations) sxs>=2025.0 From 5cb8ddba8680df2778abaf3ea3c4aca69b6f47b4 Mon Sep 17 00:00:00 2001 From: Simone Albanesi Date: Wed, 8 Jul 2026 19:33:40 +0200 Subject: [PATCH 3/5] removed commented lines --- PyART/catalogs/sxs.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/PyART/catalogs/sxs.py b/PyART/catalogs/sxs.py index 3ad7d17a..9e734cf7 100644 --- a/PyART/catalogs/sxs.py +++ b/PyART/catalogs/sxs.py @@ -347,9 +347,7 @@ def download_simulation( f"Mode Y_l{ell}_m{m} not found in the waveform data! Skipping." ) continue - # create the h5 file - # h5file = h5py.File(os.path.join(out_dir, f"rhOverM_Asymptotic_GeometricUnits_CoM.h5"), "w") - # save_dict_to_h5(h5file, to_h5file) + # create/udpate the h5 file filename = os.path.join( out_dir, f"rhOverM_Asymptotic_GeometricUnits_CoM.h5" ) @@ -377,9 +375,7 @@ def download_simulation( if mode_string in wav: to_h5file[extp][mode_string] = wav[mode_string] - # create the h5 file - # h5file = h5py.File(os.path.join(out_dir, f"rMPsi4_Asymptotic_GeometricUnits_CoM.h5"), "w") - # save_dict_to_h5(h5file, to_h5file) + # create/udpdate the h5 file filename = os.path.join(out_dir, f"rMPsi4_Asymptotic_GeometricUnits_CoM.h5") with h5py.File(filename, "a") as h5file: if extp in h5file: From 3f945b0cf8d6764bdaa13e1bfc8cd167fb082182 Mon Sep 17 00:00:00 2001 From: Simone Albanesi Date: Wed, 8 Jul 2026 19:53:36 +0200 Subject: [PATCH 4/5] uncommented phenom in requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 161a5c70..141c70ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ pycbc>=2.4.0 # gw-eccentricity>=1.0.4 # mayawaves>=2024.4.25 lalsuite>=7.22 -#phenomxpy @ git+https://gitlab.com/imrphenom-dev/phenomxpy.git +phenomxpy @ git+https://gitlab.com/imrphenom-dev/phenomxpy.git # SXS catalog support (optional; needed for downloading SXS simulations) sxs>=2025.0 From 56e1c8fee19e52b7bce44f54c8384f314ad94053 Mon Sep 17 00:00:00 2001 From: Simone Albanesi Date: Wed, 8 Jul 2026 20:12:41 +0200 Subject: [PATCH 5/5] fixed minor bug if level was specified. Updated sxs test --- PyART/catalogs/sxs.py | 8 +++++--- tests/test_sxs.py | 28 +++++++++++++++++----------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/PyART/catalogs/sxs.py b/PyART/catalogs/sxs.py index 9e734cf7..3aea9ed3 100644 --- a/PyART/catalogs/sxs.py +++ b/PyART/catalogs/sxs.py @@ -135,9 +135,11 @@ def __init__( order_group = f"Extrapolated_N{self.order}.dir" if self.level is None: level = int(lev_dirs[-1].replace("Lev", "")) - fname = self.get_lev_fname(basename=self.basename, level=level) - with h5py.File(fname, "r") as f: - needs_download = order_group not in f + else: + level = self.level + fname = self.get_lev_fname(basename=self.basename, level=level) + with h5py.File(fname, "r") as f: + needs_download = order_group not in f if needs_download: logging.info( f"{levpath} found, but not the requested N={self.order} order. Download needed." diff --git a/tests/test_sxs.py b/tests/test_sxs.py index 16482598..a8c58715 100644 --- a/tests/test_sxs.py +++ b/tests/test_sxs.py @@ -12,17 +12,19 @@ def test_sxs(): """ Test the SXS download function. """ - wf = sxs.Waveform_SXS( - ID="0180", - path="./", - download=True, - downloads=["hlm", "metadata", "horizons"], - load=["hlm", "metadata", "horizons"], - ignore_deprecation=True, - level=4, - order=2, - nu_rescale=False, - ) + opts = { + "ID": "0180", + "path": "./", + "download": True, + "downloads": ["hlm", "metadata", "horizons"], + "load": ["hlm", "metadata", "horizons"], + "level": 4, + "order": 2, + "nu_rescale": False, + "ignore_deprecation": True, + } + wf = sxs.Waveform_SXS(**opts) + # check attributes assert wf.ID == "0180" assert wf.level == 4 @@ -54,5 +56,9 @@ def test_sxs(): # check length assert len(wf.hlm[mode]["A"]) == len(wf.u) + # get also order=3 + opts["order"] = 3 + wf = sxs.Waveform_SXS(**opts) + # check that conversion to LVKNR works wf.to_lvk(modes=[(2, 2)])