Working with the Public ICCUB Catalog and Waveform Integration

This tutorial demonstrates how to work with waveforms from the public ICCUB (Institute of Cosmos Sciences) numerical relativity catalog, hosted at egrav.icc.ub.edu, and how to perform waveform integration from Ψ₄ (Weyl scalar) to strain h.

ICCUB simulations provide Ψ₄ data, which needs to be integrated twice to obtain the gravitational wave strain. PyART provides tools for this integration, and also stores a pre-computed strain in the downloaded file that we can use as a reference.

Setup

%matplotlib inline
%config InlineBackend.figure_format = 'retina'

import matplotlib.pyplot as plt
import numpy as np
from PyART.catalogs.icc_public import Waveform_ICC
from PyART.analysis.integrate_wave import IntegrateMultipole

Load ICCUB Waveform

Waveform_ICC downloads the simulation from the public catalog the first time it’s requested (download=True), and reuses the cached files under path afterwards. ellmax is capped at the highest multipole the public release provides (l<=4).

# Public ICCUB catalog entry to use
path = './local_data/icc/'
ID = 1

icc = Waveform_ICC(path=path, ID=ID, download=True, ellmax=4)
icc.load_psi4lm()  # psi4 modes are not loaded automatically

print("Waveform loaded successfully!")
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[2], line 5
      1 # Public ICCUB catalog entry to use
      2 path = './local_data/icc/'
      3 ID = 1
      4 
----> 5 icc = Waveform_ICC(path=path, ID=ID, download=True, ellmax=4)
      6 icc.load_psi4lm()  # psi4 modes are not loaded automatically
      7 
      8 print("Waveform loaded successfully!")

File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/PyART/catalogs/icc_public.py:78, in Waveform_ICC.__init__(self, path, ID, download, load, ellmax, nu_rescale, extraction)
     75 json_catalog = "egrav_data.json"
     77 if not os.path.exists(json_catalog):
---> 78     fetch_and_save_egrav_json()
     80 self.ID = ID
     81 self.path = path

File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/PyART/catalogs/icc_public.py:26, in fetch_and_save_egrav_json(output_path)
     14 """
     15 Download the JSON catalog of public ICC waveforms and save it locally.
     16 Parameters
   (...)     23     List of entries in the catalog.
     24 """
     25 url = "https://egrav.icc.ub.edu/site/list-grid"
---> 26 response = requests.get(url, verify=False)
     27 response.raise_for_status()
     29 data = response.json()

File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/requests/api.py:87, in get(url, params, **kwargs)
     74 def get(
     75     url: _t.UriType, params: _t.ParamsType = None, **kwargs: Unpack[_t.GetKwargs]
     76 ) -> Response:
     77     r"""Sends a GET request.
     78 
     79     :param url: URL for the new :class:`Request` object.
   (...)     84     :rtype: requests.Response
     85     """
---> 87     return request("get", url, params=params, **kwargs)

File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/requests/api.py:71, in request(method, url, **kwargs)
     67 # By using the 'with' statement we are sure the session is closed, thus we
     68 # avoid leaving sockets open which can trigger a ResourceWarning in some
     69 # cases, and look like a memory leak in others.
     70 with sessions.Session() as session:
---> 71     return session.request(method=method, url=url, **kwargs)

File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/requests/sessions.py:651, in Session.request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
    646 send_kwargs = {
    647     "timeout": timeout,
    648     "allow_redirects": allow_redirects,
    649 }
    650 send_kwargs.update(settings)
--> 651 resp = self.send(prep, **send_kwargs)
    653 return resp

File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/requests/sessions.py:784, in Session.send(self, request, **kwargs)
    781 start = preferred_clock()
    783 # Send the request
--> 784 r = adapter.send(request, **kwargs)
    786 # Total elapsed time of the request (approximately)
    787 elapsed = preferred_clock() - start

File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/requests/adapters.py:696, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    693     resolved_timeout = TimeoutSauce(connect=timeout, read=timeout)
    695 try:
--> 696     resp = conn.urlopen(
    697         method=request.method,
    698         url=url,
    699         body=request.body,  # type: ignore[arg-type]  # urllib3 stubs don't accept Iterable[bytes | str]
    700         headers=request.headers,  # type: ignore[arg-type]  # urllib3#3072
    701         redirect=False,
    702         assert_same_host=False,
    703         preload_content=False,
    704         decode_content=False,
    705         retries=self.max_retries,
    706         timeout=resolved_timeout,
    707         chunked=chunked,
    708     )
    710 except (ProtocolError, OSError) as err:
    711     raise ConnectionError(err, request=request)

File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/urllib3/connectionpool.py:793, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
    790 response_conn = conn if not release_conn else None
    792 # Make the request on the HTTPConnection object
--> 793 response = self._make_request(
    794     conn,
    795     method,
    796     url,
    797     timeout=timeout_obj,
    798     body=body,
    799     headers=headers,
    800     chunked=chunked,
    801     retries=retries,
    802     response_conn=response_conn,
    803     preload_content=preload_content,
    804     decode_content=decode_content,
    805     **response_kw,
    806 )
    808 # Everything went great!
    809 clean_exit = True

File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/urllib3/connectionpool.py:470, in HTTPConnectionPool._make_request(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)
    467 try:
    468     # Trigger any extra validation we need to do.
    469     try:
--> 470         self._validate_conn(conn)
    471     except (SocketTimeout, BaseSSLError) as e:
    472         self._raise_timeout(err=e, url=url, timeout_value=conn.timeout)

File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/urllib3/connectionpool.py:1125, in HTTPSConnectionPool._validate_conn(self, conn)
   1123 # Force connect early to allow us to validate the connection.
   1124 if conn.is_closed:
-> 1125     conn.connect()
   1127 # TODO revise this, see https://github.com/urllib3/urllib3/issues/2791
   1128 if not conn.is_verified and not conn.proxy_is_verified:

File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/urllib3/connection.py:827, in HTTPSConnection.connect(self)
    825 try:
    826     sock: socket.socket | ssl.SSLSocket
--> 827     self.sock = sock = self._new_conn()
    828     server_hostname: str = self.host
    829     tls_in_tls = False

File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/urllib3/connection.py:239, in HTTPConnection._new_conn(self)
    234 """Establish a socket connection and set nodelay settings on it.
    235 
    236 :return: New socket connection.
    237 """
    238 try:
--> 239     sock = connection.create_connection(
    240         (self._dns_host, self.port),
    241         self.timeout,
    242         source_address=self.source_address,
    243         socket_options=self.socket_options,
    244     )
    245 except socket.gaierror as e:
    246     raise NameResolutionError(self.host, self, e) from e

File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/site-packages/urllib3/util/connection.py:73, in create_connection(address, timeout, source_address, socket_options)
     71 if source_address:
     72     sock.bind(source_address)
---> 73 sock.connect(sa)
     74 # Break explicitly a reference cycle
     75 err = None

KeyboardInterrupt: 

Display Metadata

Let’s examine the simulation parameters:

print("Simulation Metadata:")
print('=' * 60)
for k, v in icc.metadata.items():
    print(f'{k:20s} : {v}')
print('=' * 60)

Visualize the Waveform

Plot the (2,2) strain and Ψ₄ modes stored in the catalog:

# Merger time from the stored strain
tmrg, _, _, _ = icc.find_max(kind='global')

fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True)

axes[0].plot(icc.u - tmrg, icc.hlm[(2, 2)]['real'], label='Re', linewidth=1.5)
axes[0].plot(icc.u - tmrg, icc.hlm[(2, 2)]['A'], label='Amplitude', linewidth=1.5, c='k', ls='--')
axes[0].set_ylabel(r'$h_{22}$', fontsize=14)
axes[0].legend(fontsize=11)
axes[0].grid(True, alpha=0.3)

axes[1].plot(icc.u - tmrg, icc.psi4lm[(2, 2)]['real'], label='Re', linewidth=1.5, c='C1')
axes[1].plot(icc.u - tmrg, icc.psi4lm[(2, 2)]['A'], label='Amplitude', linewidth=1.5, c='k', ls='--')
axes[1].set_ylabel(r'$\psi_4^{22}$', fontsize=14)
axes[1].set_xlabel(r'$t-t_{\rm mrg}$ (M)', fontsize=14)
axes[1].legend(fontsize=11)
axes[1].grid(True, alpha=0.3)

fig.suptitle(f'ICCUB Simulation ID: {ID:04d}', fontsize=18)
plt.tight_layout()
plt.show()

Testing the Integration Method

The catalog also provides the raw Ψ₄ multipoles, so we can run PyART’s IntegrateMultipole (fixed-frequency integration) ourselves and inspect its outputs:

# Get Psi4 data
t = icc.t_psi4
psi4 = icc.psi4lm[(2, 2)]['z']
l, m = 2, 2

integr_opts = {
    'f0': 0.002,
    'extrap_psi4': False,  # the catalog data is already extrapolated to r=infinity
    'method': 'FFI',
    'window': [20, -20],
}

# Run the integration explicitly
mode = IntegrateMultipole(
    l, m, t, psi4,
    **integr_opts,
    mass=icc.metadata['M'],
    radius=1.0,
    integrand='psi4'
)

# Plot the integrated quantities
plt.figure(figsize=(14, 10))

plt.subplot(3, 1, 1)
plt.plot(t, mode.h.real, c='b', linewidth=2, label='Integrated')
plt.plot(t, icc.hlm[(2, 2)]['real'], label='Original', linewidth=1.5, color='r', linestyle='--')
plt.ylabel(r'Re[$h_{22}$]', fontsize=14)
plt.grid(True, alpha=0.3)
plt.title('Strain', fontsize=14)
plt.legend()

plt.subplot(3, 1, 2)
plt.plot(t, mode.dh.real, c='b', linewidth=2)
plt.ylabel(r'Re[$\dot{h}_{22}$]', fontsize=14)
plt.grid(True, alpha=0.3)
plt.title('Strain Derivative', fontsize=14)

plt.subplot(3, 1, 3)
plt.plot(t, mode.psi4.real, c='b', linewidth=2, label='Integrated')
plt.plot(t, icc.psi4lm[(2, 2)]['real'], c='r', linewidth=1.5, linestyle='--', label='Original')
plt.xlabel('Time (M)', fontsize=14)
plt.ylabel(r'Re[$\psi_4^{22}$]', fontsize=14)
plt.grid(True, alpha=0.3)
plt.title('Weyl Scalar', fontsize=14)
plt.legend()

plt.tight_layout()
plt.show()

The choice of \(f_0\) in the integration options determines the low-frequency cutoff for the waveform integration. A smaller value of \(f_0\) allows for more accurate integration of the low-frequency components, but may also introduce numerical noise. For instance, the real part of the integrated strain does not exactly taper to zero after merger. Choosing a slightly larger value of \(f_0\) can help mitigate this issue by suppressing the low-frequency noise

# Get Psi4 data
t = icc.t_psi4
psi4 = icc.psi4lm[(2, 2)]['z']
l, m = 2, 2

# Plot the integrated quantities
fig, ax = plt.subplots(figsize=(14, 6))
for i, f0 in enumerate([0.001, 0.002, 0.005, 0.01]):
    integr_opts['f0'] = f0

    # Run the integration explicitly
    mode = IntegrateMultipole(
        l, m, t, psi4,
        **integr_opts,
        mass=icc.metadata['M'],
        radius=1.0,
        integrand='psi4'
    )

    ax.plot(t, mode.h.real, linewidth=2, label=rf'$f_0={f0}$')
ax.set_xlabel('Time (M)')
ax.set_ylabel(r'Re[$h_{22}$]')
ax.grid(True, alpha=0.3)
ax.legend()

plt.show()

Summary

This tutorial demonstrated:

  • Downloading waveforms from the public ICCUB catalog

  • Integrating Ψ₄ (Weyl scalar) to obtain gravitational wave strain

  • Comparing the stored strain with the raw Ψ₄ data

  • Running PyART’s integration tools explicitly

Key Points

  • The public ICCUB release currently provides multipoles up to l=4

  • The Fixed Frequency Integration (FFI) method provides accurate strain from Ψ₄

  • Integration requires careful choices of f₀ (low-frequency cutoff) and time windows

  • PyART’s integration tools handle extrapolation and windowing automatically

Next Steps

  • Explore different integration methods and parameters

  • Compare ICCUB waveforms with other catalogs

  • Perform mismatch calculations with EOB models