Overlap fixing and more (#107)

* Update README

* Update density field reader

* Update name of SDSSxALFAFA

* Fix quick bug

* Add little fixes

* Update README

* Put back fit_init

* Add paths to initial snapshots

* Add export

* Remove some choices

* Edit README

* Add Jens' comments

* Organize imports

* Rename snapshot

* Add additional print statement

* Add paths to initial snapshots

* Add masses to the initial files

* Add normalization

* Edit README

* Update README

* Fix bug in CSiBORG1 so that does not read fof_00001

* Edit README

* Edit README

* Overwrite comments

* Add paths to init lag

* Fix Quijote path

* Add lagpatch

* Edit submits

* Update README

* Fix numpy int problem

* Update README

* Add a flag to keep the snapshots open when fitting

* Add a flag to keep snapshots open

* Comment out some path issue

* Keep snapshots open

* Access directly snasphot

* Add lagpatch for CSiBORG2

* Add treatment of x-z coordinates flipping

* Add radial velocity field loader

* Update README

* Add lagpatch to Quijote

* Fix typo

* Add setter

* Fix typo

* Update README

* Add output halo cat as ASCII

* Add import

* Add halo plot

* Update README

* Add evaluating field at radial distanfe

* Add field shell evaluation

* Add enclosed mass computation

* Add BORG2 import

* Add BORG boxsize

* Add BORG paths

* Edit run

* Add BORG2 overdensity field

* Add bulk flow clauclation

* Update README

* Add new plots

* Add nbs

* Edit paper

* Update plotting

* Fix overlap paths to contain simname

* Add normalization of positions

* Add default paths to CSiBORG1

* Add overlap path simname

* Fix little things

* Add CSiBORG2 catalogue

* Update README

* Add import

* Add TNG density field constructor

* Add TNG density

* Add draft of calculating BORG ACL

* Fix bug

* Add ACL of enclosed density

* Add nmean acl

* Add galaxy bias calculation

* Add BORG acl notebook

* Add enclosed mass calculation

* Add TNG300-1 dir

* Add TNG300 and BORG1 dir

* Update nb
This commit is contained in:
Richard Stiskalek 2024-01-30 16:14:07 +00:00 committed by GitHub
parent 0984191dc8
commit 9e4b34f579
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 10037 additions and 248 deletions

View file

@ -17,7 +17,8 @@ from csiborgtools import clustering, field, halo, match, read, summary
from .utils import (center_of_mass, delta2ncells, number_counts, # noqa
periodic_distance, periodic_distance_two_points, # noqa
binned_statistic, cosine_similarity, fprint, # noqa
hms_to_degrees, dms_to_degrees, great_circle_distance) # noqa
hms_to_degrees, dms_to_degrees, great_circle_distance, # noqa
radec_to_cartesian) # noqa
from .params import paths_glamdring, simname2boxsize # noqa
@ -52,7 +53,9 @@ class SDSSxALFALFA:
if fpath is None:
fpath = "/mnt/extraspace/rstiskalek/catalogs/5asfullmatch.fits"
sel_steps = self.steps if apply_selection else None
return read.SDSS(fpath, h=1, sel_steps=sel_steps)
survey = read.SDSS(fpath, h=1, sel_steps=sel_steps)
survey.name = "SDSSxALFALFA"
return survey
###############################################################################

View file

@ -17,5 +17,6 @@ from .density import (DensityField, PotentialField, TidalTensorField,
overdensity_field) # noqa
from .interp import (evaluate_cartesian, evaluate_sky, field2rsp, # noqa
fill_outside, make_sky, observer_peculiar_velocity, # noqa
nside2radec, smoothen_field) # noqa
smoothen_field, field_at_distance) # noqa
from .corr import bayesian_bootstrap_correlation # noqa
from .utils import nside2radec # noqa

View file

@ -15,7 +15,6 @@
"""
Tools for interpolating 3D fields at arbitrary positions.
"""
import healpy
import MAS_library as MASL
import numpy
import smoothing_library as SL
@ -23,7 +22,7 @@ from numba import jit
from tqdm import tqdm, trange
from ..utils import periodic_wrap_grid, radec_to_cartesian
from .utils import divide_nonzero, force_single_precision
from .utils import divide_nonzero, force_single_precision, nside2radec
###############################################################################
@ -219,18 +218,47 @@ def make_sky(field, angpos, dist, boxsize, verbose=True):
return out
def nside2radec(nside):
"""
Generate RA [0, 360] deg. and declination [-90, 90] deg. for HEALPix pixel
centres at a given nside.
"""
pixs = numpy.arange(healpy.nside2npix(nside))
theta, phi = healpy.pix2ang(nside, pixs)
###############################################################################
# Average field at a radial distance #
###############################################################################
ra = 180 / numpy.pi * phi
dec = 90 - 180 / numpy.pi * theta
return numpy.vstack([ra, dec]).T
def field_at_distance(field, distance, boxsize, smooth_scales=None, nside=128,
verbose=True):
"""
Evaluate a scalar field at uniformly spaced angular coordinates at a
given distance from the observer
Parameters
----------
field : 3-dimensional array of shape `(grid, grid, grid)`
Field to be interpolated.
distance : float
Distance from the observer in `Mpc / h`.
boxsize : float
Box size in `Mpc / h`.
smooth_scales : (list of) float, optional
Smoothing scales in `Mpc / h`. If `None`, no smoothing is performed.
nside : int, optional
HEALPix nside. Used to generate the uniformly spaced angular
coordinates. Recommended to be >> 1.
verbose : bool, optional
Smoothing verbosity flag.
Returns
-------
vals : n-dimensional array of shape `(npix, len(smooth_scales))`
"""
# Get positions of HEALPix pixels on the sky and then convert those to
# box Cartesian coordinates. We take HEALPix pixels because they are
# uniformly distributed on the sky.
angpos = nside2radec(nside)
X = numpy.hstack([numpy.ones(len(angpos)).reshape(-1, 1) * distance,
angpos])
X = radec_to_cartesian(X) / boxsize + 0.5
return evaluate_cartesian(field, pos=X, smooth_scales=smooth_scales,
verbose=verbose)
###############################################################################

View file

@ -18,6 +18,7 @@ imports.
"""
from numba import jit
import numpy
import healpy
def force_single_precision(x):
@ -42,3 +43,26 @@ def divide_nonzero(field0, field1):
for k in range(kmax):
if field1[i, j, k] != 0:
field0[i, j, k] /= field1[i, j, k]
def nside2radec(nside):
"""
Generate RA [0, 360] deg. and declination [-90, 90] deg for HEALPix pixel
centres at a given nside.
Parameters
----------
nside : int
HEALPix nside.
Returns
-------
angpos : 2-dimensional array of shape (npix, 2)
"""
pixs = numpy.arange(healpy.nside2npix(nside))
theta, phi = healpy.pix2ang(nside, pixs)
ra = 180 / numpy.pi * phi
dec = 90 - 180 / numpy.pi * theta
return numpy.vstack([ra, dec]).T

View file

@ -202,10 +202,9 @@ class RealisationsMatcher(BaseMatcher):
# in the reference simulation from the cross simulation in the initial
# snapshot.
match_indxs = radius_neighbours(
catx.knn(in_initial=True, subtract_observer=False, periodic=True),
cat0["lagpatch_coordinates"], radiusX=cat0["lagpatch_radius"],
radiusKNN=catx["lagpatch_radius"], nmult=self.nmult,
enforce_int32=True, verbose=verbose)
catx.knn(in_initial=True), cat0["lagpatch_coordinates"],
radiusX=cat0["lagpatch_radius"], radiusKNN=catx["lagpatch_radius"],
nmult=self.nmult, enforce_int32=True, verbose=verbose)
# We next remove neighbours whose mass is too large/small.
if self.dlogmass is not None:
@ -367,6 +366,7 @@ class ParticleOverlap(BaseMatcher):
cellmin = self.box_size // 2 - self.bckg_halfsize
cellmax = self.box_size // 2 + self.bckg_halfsize
ncells = cellmax - cellmin
boxsize_mpc = cat.boxsize
# We then pre-allocate the density field/check it is of the right shape
if delta is None:
delta = numpy.zeros((ncells,) * 3, dtype=numpy.float32)
@ -382,6 +382,7 @@ class ParticleOverlap(BaseMatcher):
for hid in iterator:
try:
pos = cat.snapshot.halo_coordinates(hid, is_group=True)
pos /= boxsize_mpc
except ValueError as e:
# If not particles found for this halo, just skip it.
if str(e).startswith("Halo "):
@ -852,6 +853,8 @@ def load_processed_halo(hid, cat, ncells, nshift):
pos = cat.snapshot.halo_coordinates(hid, is_group=True)
mass = cat.snapshot.halo_masses(hid, is_group=True)
pos /= cat.boxsize
pos = pos2cell(pos, ncells)
mins, maxs = get_halo_cell_limits(pos, ncells=ncells, nshift=nshift)
return pos, mass, numpy.sum(mass), mins, maxs

View file

@ -34,6 +34,8 @@ def simname2boxsize(simname):
"csiborg2_main": 676.6,
"csiborg2_varysmall": 676.6,
"csiborg2_random": 676.6,
"borg1": 677.7,
"borg2": 676.6,
"quijote": 1000.
}
@ -52,6 +54,8 @@ paths_glamdring = {
"csiborg2_random_srcdir": "/mnt/extraspace/rstiskalek/csiborg2_random", # noqa
"postdir": "/mnt/extraspace/rstiskalek/csiborg_postprocessing/",
"quijote_dir": "/mnt/extraspace/rstiskalek/quijote",
"borg2_dir": "/mnt/extraspace/rstiskalek/BORG_STOPYRA_2023",
"tng300_1_dir": "/mnt/extraspace/rstiskalek/TNG300-1/",
}

View file

@ -14,8 +14,10 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from .catalogue import (CSiBORG1Catalogue, CSiBORG2Catalogue, # noqa
CSiBORG2MergerTreeReader, QuijoteCatalogue) # noqa
from .snapshot import (CSIBORG1Snapshot, CSIBORG2Snapshot, QuijoteSnapshot, # noqa
CSiBORG1Field, CSiBORG2Field, QuijoteField) # noqa
from .snapshot import (CSiBORG1Snapshot, CSiBORG2Snapshot, QuijoteSnapshot, # noqa
CSiBORG1Field, CSiBORG2Field, QuijoteField, BORG2Field, # noqa
BORG1Field) # noqa
from .obs import (SDSS, MCXCClusters, PlanckClusters, TwoMPPGalaxies, # noqa
TwoMPPGroups, ObservedCluster, match_array_to_no_masking) # noqa
TwoMPPGroups, ObservedCluster, match_array_to_no_masking, # noqa
cols_to_structured) # noqa
from .paths import Paths # noqa

View file

@ -69,6 +69,7 @@ class BaseCatalogue(ABC):
self._observer_location = None
self._observer_velocity = None
self._flip_xz = False
self._boxsize = None
self._cache = OrderedDict()
@ -81,7 +82,7 @@ class BaseCatalogue(ABC):
def init_with_snapshot(self, simname, nsim, nsnap, paths, snapshot,
bounds, boxsize, observer_location,
observer_velocity, cache_maxsize=64):
observer_velocity, flip_xz, cache_maxsize=64):
self.simname = simname
self.nsim = nsim
self.nsnap = nsnap
@ -89,6 +90,7 @@ class BaseCatalogue(ABC):
self.boxsize = boxsize
self.observer_location = observer_location
self.observer_velocity = observer_velocity
self.flip_xz = flip_xz
self.cache_maxsize = cache_maxsize
@ -211,6 +213,24 @@ class BaseCatalogue(ABC):
raise TypeError("`boxsize` must be an integer or float.")
self._boxsize = float(boxsize)
@property
def flip_xz(self):
"""
Whether to flip the x- and z-coordinates to undo the MUSIC bug to match
observations.
Returns
-------
bool
"""
return self._flip_xz
@flip_xz.setter
def flip_xz(self, flip_xz):
if not isinstance(flip_xz, bool):
raise TypeError("`flip_xz` must be a boolean.")
self._flip_xz = flip_xz
@property
def cache_maxsize(self):
"""
@ -592,6 +612,10 @@ class BaseCatalogue(ABC):
elif key == "redshift_dist":
out = self["__cartesian_redshift_pos"]
out = numpy.linalg.norm(out - self.observer_location, axis=1)
elif key == "lagpatch_radius":
out = self.lagpatch_radius
elif key == "lagpatch_coordinates":
out = self.lagpatch_coordinates
elif key == "npart":
out = self.npart
elif key == "totmass":
@ -650,16 +674,23 @@ class CSiBORG1Catalogue(BaseCatalogue):
a boolean.
observer_velocity : 1-dimensional array, optional
Observer's velocity in :math:`\mathrm{km} / \mathrm{s}`.
flip_xz : bool, optional
Whether to flip the x- and z-coordinates to undo the MUSIC bug to match
observations.
cache_maxsize : int, optional
Maximum number of cached arrays.
"""
def __init__(self, nsim, paths=None, snapshot=None, bounds=None,
observer_velocity=None, cache_maxsize=64):
observer_velocity=None, flip_xz=True, cache_maxsize=64):
super().__init__()
if paths is None:
paths = Paths(**paths_glamdring)
super().init_with_snapshot(
"csiborg1", nsim, max(paths.get_snapshots(nsim, "csiborg1")),
paths, snapshot, bounds, 677.7, [338.85, 338.85, 338.85],
observer_velocity, cache_maxsize)
observer_velocity, flip_xz, cache_maxsize)
self._custom_keys = []
@ -675,9 +706,12 @@ class CSiBORG1Catalogue(BaseCatalogue):
@property
def coordinates(self):
# NOTE: We flip x and z to undo MUSIC bug.
z, y, x = [self._read_fof_catalogue(key) for key in ["x", "y", "z"]]
return numpy.vstack([x, y, z]).T
x, y, z = [self._read_fof_catalogue(key) for key in ["x", "y", "z"]]
if self.flip_xz:
return numpy.vstack([z, y, x]).T
else:
return numpy.vstack([x, y, z]).T
@property
def velocities(self):
@ -698,11 +732,18 @@ class CSiBORG1Catalogue(BaseCatalogue):
@property
def lagpatch_coordinates(self):
raise RuntimeError("Lagrangian patch coordinates are not available.")
fpath = self.paths.initial_lagpatch(self.nsim, self.simname)
data = numpy.load(fpath)
if self.flip_xz:
return numpy.vstack([data["z"], data["y"], data["x"]]).T
else:
return numpy.vstack([data["x"], data["y"], data["z"]]).T
@property
def lagpatch_radius(self):
raise RuntimeError("Lagrangian patch radius is not available.")
fpath = self.paths.initial_lagpatch(self.nsim, self.simname)
return numpy.load(fpath)["lagpatch_size"]
###############################################################################
@ -730,15 +771,20 @@ class CSiBORG2Catalogue(BaseCatalogue):
a boolean.
observer_velocity : 1-dimensional array, optional
Observer's velocity in :math:`\mathrm{km} / \mathrm{s}`.
flip_xz : bool, optional
Whether to flip the x- and z-coordinates to undo the MUSIC bug to match
observations.
cache_maxsize : int, optional
Maximum number of cached arrays.
"""
def __init__(self, nsim, nsnap, kind, paths=None, snapshot=None,
bounds=None, observer_velocity=None, cache_maxsize=64):
bounds=None, observer_velocity=None, flip_xz=True,
cache_maxsize=64):
super().__init__()
super().init_with_snapshot(
f"csiborg2_{kind}", nsim, nsnap, paths, snapshot, bounds,
676.6, [338.3, 338.3, 338.3], observer_velocity, cache_maxsize)
676.6, [338.3, 338.3, 338.3], observer_velocity, flip_xz,
cache_maxsize)
self._custom_keys = ["GroupFirstSub", "GroupContamination",
"GroupNsubs", "Group_M_Crit200"]
@ -767,16 +813,16 @@ class CSiBORG2Catalogue(BaseCatalogue):
@property
def coordinates(self):
# Loading directly the Gadget4 output, flip x and z to undo MUSIC bug.
out = self._read_fof_catalogue("GroupPos")
out[:, [0, 2]] = out[:, [2, 0]]
if self.flip_xz:
out[:, [0, 2]] = out[:, [2, 0]]
return out
@property
def velocities(self):
# Loading directly the Gadget4 output, flip x and z to undo MUSIC bug.
out = self._read_fof_catalogue("GroupVel")
out[:, [0, 2]] = out[:, [2, 0]]
if self.flip_xz:
out[:, [0, 2]] = out[:, [2, 0]]
return out
@property
@ -795,11 +841,28 @@ class CSiBORG2Catalogue(BaseCatalogue):
@property
def lagpatch_coordinates(self):
raise RuntimeError("Lagrangian patch coordinates are not available.")
if self.nsnap != 99:
raise RuntimeError("Lagrangian patch information is only "
"available for haloes defined at the final "
f"snapshot (indexed 99). Chosen {self.nsnap}.")
fpath = self.paths.initial_lagpatch(self.nsim, self.simname)
data = numpy.load(fpath)
if self.flip_xz:
return numpy.vstack([data["z"], data["y"], data["x"]]).T
else:
return numpy.vstack([data["x"], data["y"], data["z"]]).T
@property
def lagpatch_radius(self):
raise RuntimeError("Lagrangian patch radius is not available.")
if self.nsnap != 99:
raise RuntimeError("Lagrangian patch information is only "
"available for haloes defined at the final "
f"snapshot (indexed 99). Chosen {self.nsnap}.")
fpath = self.paths.initial_lagpatch(self.nsim, self.simname)
return numpy.load(fpath)["lagpatch_size"]
@property
def GroupFirstSub(self):
@ -1086,12 +1149,11 @@ class QuijoteCatalogue(BaseCatalogue):
Maximum number of cached arrays.
"""
def __init__(self, nsim, paths=None, snapshot=None, bounds=None,
observer_velocity=None,
cache_maxsize=64):
observer_velocity=None, cache_maxsize=64):
super().__init__()
super().init_with_snapshot(
"quijote", nsim, 4, paths, snapshot, bounds, 1000,
[500., 500., 500.,], observer_velocity, cache_maxsize)
[500., 500., 500.,], observer_velocity, False, cache_maxsize)
self._custom_keys = []
self._bounds = bounds
@ -1131,11 +1193,14 @@ class QuijoteCatalogue(BaseCatalogue):
@property
def lagpatch_coordinates(self):
raise RuntimeError("Lagrangian patch coordinates are not available.")
fpath = self.paths.initial_lagpatch(self.nsim, self.simname)
data = numpy.load(fpath)
return numpy.vstack([data["x"], data["y"], data["z"]]).T
@property
def lagpatch_radius(self):
raise RuntimeError("Lagrangian patch radius is not available.")
fpath = self.paths.initial_lagpatch(self.nsim, self.simname)
return numpy.load(fpath)["lagpatch_size"]
def pick_fiducial_observer(self, n, rmax):
r"""

View file

@ -53,6 +53,12 @@ class Paths:
Path to the CSiBORG post-processing directory.
quijote_dir : str
Path to the Quijote simulation directory.
borg1_dir : str
Path to the BORG1 simulation directory.
borg2_dir : str
Path to the BORG2 simulation directory.
tng300_1_dir : str
Path to the TNG300-1 simulation directory.
"""
def __init__(self,
csiborg1_srcdir,
@ -61,13 +67,18 @@ class Paths:
csiborg2_varysmall_srcdir,
postdir,
quijote_dir,
borg1_dir,
borg2_dir,
tng300_1_dir
):
self.csiborg1_srcdir = csiborg1_srcdir
self.csiborg2_main_srcdir = csiborg2_main_srcdir
self.csiborg2_random_srcdir = csiborg2_random_srcdir
self.csiborg2_varysmall_srcdir = csiborg2_varysmall_srcdir
self.quijote_dir = quijote_dir
self.borg1_dir = borg1_dir
self.borg2_dir = borg2_dir
self.tng300_1_dir = tng300_1_dir
self.postdir = postdir
def get_ics(self, simname):
@ -83,10 +94,10 @@ class Paths:
-------
ids : 1-dimensional array
"""
if simname == "csiborg1":
if simname == "csiborg1" or simname == "borg1":
files = glob(join(self.csiborg1_srcdir, "chain_*"))
files = [int(search(r'chain_(\d+)', f).group(1)) for f in files]
elif simname == "csiborg2_main":
elif simname == "csiborg2_main" or simname == "borg2":
files = glob(join(self.csiborg2_main_srcdir, "chain_*"))
files = [int(search(r'chain_(\d+)', f).group(1)) for f in files]
elif simname == "csiborg2_random":
@ -175,25 +186,27 @@ class Paths:
str
"""
if simname == "csiborg1":
return join(self.csiborg1_srcdir, f"chain_{nsim}",
f"snapshot_{str(nsnap).zfill(5)}.hdf5")
fpath = join(self.csiborg1_srcdir, f"chain_{nsim}",
f"snapshot_{str(nsnap).zfill(5)}.hdf5")
elif simname == "csiborg2_main":
return join(self.csiborg2_main_srcdir, f"chain_{nsim}", "output",
f"snapshot_{str(nsnap).zfill(3)}.hdf5")
fpath = join(self.csiborg2_main_srcdir, f"chain_{nsim}", "output",
f"snapshot_{str(nsnap).zfill(3)}.hdf5")
elif simname == "csiborg2_random":
return join(self.csiborg2_random_srcdir, f"chain_{nsim}", "output",
f"snapshot_{str(nsnap).zfill(3)}.hdf5")
fpath = join(self.csiborg2_random_srcdir, f"chain_{nsim}",
"output", f"snapshot_{str(nsnap).zfill(3)}.hdf5")
elif simname == "csiborg2_varysmall":
return join(self.csiborg2_varysmall_srcdir,
f"chain_16417_{str(nsim).zfill(3)}", "output",
f"snapshot_{str(nsnap).zfill(3)}.hdf5")
fpath = join(self.csiborg2_varysmall_srcdir,
f"chain_16417_{str(nsim).zfill(3)}", "output",
f"snapshot_{str(nsnap).zfill(3)}.hdf5")
elif simname == "quijote":
return join(self.quijote_dir, "fiducial_processed",
f"chain_{nsim}",
f"snapshot_{str(nsnap).zfill(3)}.hdf5")
fpath = join(self.quijote_dir, "fiducial_processed",
f"chain_{nsim}",
f"snapshot_{str(nsnap).zfill(3)}.hdf5")
else:
raise ValueError(f"Unknown simulation name `{simname}`.")
return fpath
def snapshot_catalogue(self, nsnap, nsim, simname):
"""
Path to the halo catalogue of a simulation snapshot.
@ -218,7 +231,7 @@ class Paths:
return join(self.csiborg2_main_srcdir, f"chain_{nsim}", "output",
f"fof_subhalo_tab_{str(nsnap).zfill(3)}.hdf5")
elif simname == "csiborg2_random":
return join(self.csiborg2_ranodm_srcdir, f"chain_{nsim}", "output",
return join(self.csiborg2_random_srcdir, f"chain_{nsim}", "output",
f"fof_subhalo_tab_{str(nsnap).zfill(3)}.hdf5")
elif simname == "csiborg2_varysmall":
return join(self.csiborg2_varysmall_srcdir,
@ -231,6 +244,40 @@ class Paths:
else:
raise ValueError(f"Unknown simulation name `{simname}`.")
def initial_lagpatch(self, nsim, simname):
"""
Path to the Lagrangain patch information of a simulation for halos
defined at z = 0.
Parameters
----------
nsim : int
IC realisation index.
simname : str
Simulation name.
Returns
-------
str
"""
if simname == "csiborg1":
return join(self.csiborg1_srcdir, f"chain_{nsim}",
"initial_lagpatch.npy")
elif simname == "csiborg2_main":
return join(self.csiborg2_main_srcdir, "catalogues",
f"initial_lagpatch_{nsim}.npy")
elif simname == "csiborg2_random":
return join(self.csiborg2_random_srcdir, "catalogues",
f"initial_lagpatch_{nsim}.npy")
elif simname == "csiborg2_varysmall":
return join(self.csiborg2_varysmall_srcdir, "catalogues",
f"initial_lagpatch_{nsim}.npy")
elif simname == "quijote":
return join(self.quijote_dir, "fiducial_processed",
f"chain_{nsim}", "initial_lagpatch.npy")
else:
raise ValueError(f"Unknown simulation name `{simname}`.")
def trees(self, nsim, simname):
"""
Path to the halo trees of a simulation snapshot.
@ -284,7 +331,7 @@ class Paths:
-------
str
"""
if simname == "csiborg":
if "csiborg" in simname:
fdir = join(self.postdir, "overlap")
elif simname == "quijote":
fdir = join(self.quijote_dir, "overlap")
@ -297,7 +344,7 @@ class Paths:
nsimx = str(nsimx).zfill(5)
min_logmass = float('%.4g' % min_logmass)
fname = f"overlap_{nsim0}_{nsimx}_{min_logmass}.npz"
fname = f"overlap_{simname}_{nsim0}_{nsimx}_{min_logmass}.npz"
if smoothed:
fname = fname.replace("overlap", "overlap_smoothed")
return join(fdir, fname)
@ -367,6 +414,13 @@ class Paths:
-------
str
"""
if simname == "borg2":
return join(self.borg2_dir, f"mcmc_{nsim}.h5")
if simname == "borg1":
#
return f"/mnt/zfsusers/hdesmond/BORG_final/mcmc_{nsim}.h5"
if MAS == "SPH" and kind in ["density", "velocity"]:
if simname == "csiborg1":
raise ValueError("SPH field not available for CSiBORG1.")
@ -581,3 +635,13 @@ class Paths:
files = glob(join(fdir, f"{simname}_tpcf*"))
run = "__" + run
return [f for f in files if run in f]
def tng300_1(self):
"""
Path to the TNG300-1 simulation directory.
Returns
-------
str
"""
return self.tng300_1_dir

View file

@ -18,6 +18,7 @@ should be implemented things such as flipping x- and z-axes, to make sure that
observed RA-dec can be mapped into the simulation box.
"""
from abc import ABC, abstractmethod, abstractproperty
from os.path import join
import numpy
from h5py import File
@ -35,14 +36,26 @@ class BaseSnapshot(ABC):
"""
Base class for reading snapshots.
"""
def __init__(self, nsim, nsnap, paths):
if not isinstance(nsim, int):
raise TypeError("`nsim` must be an integer")
self._nsim = nsim
def __init__(self, nsim, nsnap, paths, keep_snapshot_open=False,
flip_xz=False):
self._keep_snapshot_open = None
if not isinstance(nsnap, int):
if not isinstance(nsim, (int, numpy.integer)):
raise TypeError("`nsim` must be an integer")
self._nsim = int(nsim)
if not isinstance(nsnap, (int, numpy.integer)):
raise TypeError("`nsnap` must be an integer")
self._nsnap = nsnap
self._nsnap = int(nsnap)
if not isinstance(keep_snapshot_open, bool):
raise TypeError("`keep_snapshot_open` must be a boolean.")
self._keep_snapshot_open = keep_snapshot_open
self._snapshot_file = None
if not isinstance(flip_xz, bool):
raise TypeError("`flip_xz` must be a boolean.")
self._flip_xz = flip_xz
self._paths = paths
self._hid2offset = None
@ -106,6 +119,30 @@ class BaseSnapshot(ABC):
self._paths = Paths(**paths_glamdring)
return self._paths
@property
def keep_snapshot_open(self):
"""
Whether to keep the snapshot file open when reading halo particles.
This is useful for repeated access to the snapshot.
Returns
-------
bool
"""
return self._keep_snapshot_open
@property
def flip_xz(self):
"""
Whether to flip the x- and z-axes to undo the MUSIC bug so that the
coordinates are consistent with observations.
Returns
-------
bool
"""
return self._flip_xz
@abstractproperty
def coordinates(self):
"""
@ -221,6 +258,43 @@ class BaseSnapshot(ABC):
"""
pass
def open_snapshot(self):
"""
Open the snapshot file, particularly used in the context of loading in
particles of individual haloes.
Returns
-------
h5py.File
"""
if not self.keep_snapshot_open:
# Check if the snapshot path is set
if not hasattr(self, "_snapshot_path"):
raise RuntimeError("Snapshot path not set.")
return File(self._snapshot_path, "r")
# Here if we want to keep the snapshot open
if self._snapshot_file is None:
self._snapshot_file = File(self._snapshot_path, "r")
return self._snapshot_file
def close_snapshot(self):
"""
Close the snapshot file opened with `open_snapshot`.
Returns
-------
None
"""
if not self.keep_snapshot_open:
return
if self._snapshot_file is not None:
self._snapshot_file.close()
self._snapshot_file = None
def select_box(self, center, boxwidth):
"""
Find particle coordinates of particles within a box of size `boxwidth`
@ -248,10 +322,11 @@ class BaseSnapshot(ABC):
###############################################################################
class CSIBORG1Snapshot(BaseSnapshot):
class CSiBORG1Snapshot(BaseSnapshot):
"""
CSiBORG1 snapshot class with the FoF halo finder particle assignment.
CSiBORG1 was run with RAMSES.
CSiBORG1 was run with RAMSES. Note that the haloes are defined at z = 0 and
index from 1.
Parameters
----------
@ -261,9 +336,16 @@ class CSIBORG1Snapshot(BaseSnapshot):
Snapshot index.
paths : Paths, optional
Paths object.
keep_snapshot_open : bool, optional
Whether to keep the snapshot file open when reading halo particles.
This is useful for repeated access to the snapshot.
flip_xz : bool, optional
Whether to flip the x- and z-axes to undo the MUSIC bug so that the
coordinates are consistent with observations.
"""
def __init__(self, nsim, nsnap, paths=None):
super().__init__(nsim, nsnap, paths)
def __init__(self, nsim, nsnap, paths=None, keep_snapshot_open=False,
flip_xz=False):
super().__init__(nsim, nsnap, paths, keep_snapshot_open, flip_xz)
self._snapshot_path = self.paths.snapshot(
self.nsnap, self.nsim, "csiborg1")
self._simname = "csiborg1"
@ -272,6 +354,9 @@ class CSIBORG1Snapshot(BaseSnapshot):
with File(self._snapshot_path, "r") as f:
x = f[kind][...]
if self.flip_xz and kind in ["Coordinates", "Velocities"]:
x[:, [0, 2]] = x[:, [2, 0]]
return x
def coordinates(self):
@ -293,13 +378,18 @@ class CSIBORG1Snapshot(BaseSnapshot):
if not is_group:
raise ValueError("There is no subhalo catalogue for CSiBORG1.")
with File(self._snapshot_path, "r") as f:
i, j = self.hid2offset.get(halo_id, (None, None))
f = self.open_snapshot()
i, j = self.hid2offset.get(halo_id, (None, None))
if i is None:
raise ValueError(f"Halo `{halo_id}` not found.")
if i is None:
raise ValueError(f"Halo `{halo_id}` not found.")
x = f[kind][i:j + 1]
x = f[kind][i:j + 1]
if not self.keep_snapshot_open:
self.close_snapshot()
if self.flip_xz and kind in ["Coordinates", "Velocities"]:
x[:, [0, 2]] = x[:, [2, 0]]
return x
@ -313,8 +403,9 @@ class CSIBORG1Snapshot(BaseSnapshot):
return self._get_halo_particles(halo_id, "Masses", is_group)
def _make_hid2offset(self):
nsnap = max(self.paths.get_snapshots(self.nsim, "csiborg1"))
catalogue_path = self.paths.snapshot_catalogue(
self.nsnap, self.nsim, "csiborg1")
nsnap, self.nsim, "csiborg1")
with File(catalogue_path, "r") as f:
offset = f["GroupOffset"][:]
@ -326,7 +417,7 @@ class CSIBORG1Snapshot(BaseSnapshot):
# CSiBORG2 snapshot class #
###############################################################################
class CSIBORG2Snapshot(BaseSnapshot):
class CSiBORG2Snapshot(BaseSnapshot):
"""
CSiBORG2 snapshot class with the FoF halo finder particle assignment and
SUBFIND subhalo finder. The simulations were run with Gadget4.
@ -341,9 +432,16 @@ class CSIBORG2Snapshot(BaseSnapshot):
CSiBORG2 run kind. One of `main`, `random`, or `varysmall`.
paths : Paths, optional
Paths object.
keep_snapshot_open : bool, optional
Whether to keep the snapshot file open when reading halo particles.
This is useful for repeated access to the snapshot.
flip_xz : bool, optional
Whether to flip the x- and z-axes to undo the MUSIC bug so that the
coordinates are consistent with observations.
"""
def __init__(self, nsim, nsnap, kind, paths=None):
super().__init__(nsim, nsnap, paths)
def __init__(self, nsim, nsnap, kind, paths=None,
keep_snapshot_open=False, flip_xz=False):
super().__init__(nsim, nsnap, paths, keep_snapshot_open, flip_xz)
self.kind = kind
fpath = self.paths.snapshot(self.nsnap, self.nsim,
@ -390,6 +488,9 @@ class CSIBORG2Snapshot(BaseSnapshot):
else:
x = numpy.vstack([x, f[f"PartType5/{kind}"][...]])
if self.flip_xz and kind in ["Coordinates", "Velocities"]:
x[:, [0, 2]] = x[:, [2, 0]]
return x
def coordinates(self):
@ -408,26 +509,39 @@ class CSIBORG2Snapshot(BaseSnapshot):
if not is_group:
raise RuntimeError("While the CSiBORG2 subhalo catalogue exists, it is not currently implemented.") # noqa
with File(self._snapshot_path, "r") as f:
i1, j1 = self.hid2offset["type1"].get(halo_id, (None, None))
i5, j5 = self.hid2offset["type5"].get(halo_id, (None, None))
f = self.open_snapshot()
i1, j1 = self.hid2offset["type1"].get(halo_id, (None, None))
i5, j5 = self.hid2offset["type5"].get(halo_id, (None, None))
# Check if this is a valid halo
if i1 is None and i5 is None:
raise ValueError(f"Halo `{halo_id}` not found.")
if j1 - i1 == 0 and j5 - i5 == 0:
raise ValueError(f"Halo `{halo_id}` has no particles.")
# Check if this is a valid halo
if i1 is None and i5 is None:
raise ValueError(f"Halo `{halo_id}` not found.")
if j1 - i1 == 0 and j5 - i5 == 0:
raise ValueError(f"Halo `{halo_id}` has no particles.")
if i1 is not None and j1 - i1 > 0:
if kind == "Masses":
x1 = numpy.ones(j1 - i1, dtype=numpy.float32)
x1 *= f["Header"].attrs["MassTable"][1]
else:
x1 = f[f"PartType1/{kind}"][i1:j1]
if i1 is not None and j1 - i1 > 0:
if kind == "Masses":
x1 = numpy.ones(j1 - i1, dtype=numpy.float32)
x1 *= f["Header"].attrs["MassTable"][1]
else:
x1 = f[f"PartType1/{kind}"][i1:j1]
if i5 is not None and j5 - i5 > 0:
x5 = f[f"PartType5/{kind}"][i5:j5]
# Flipping of x- and z-axes
if self.flip_xz:
x1[:, [0, 2]] = x1[:, [2, 0]]
if i5 is not None and j5 - i5 > 0:
x5 = f[f"PartType5/{kind}"][i5:j5]
# Flipping of x- and z-axes
if self.flip_xz and kind in ["Coordinates", "Velocities"]:
x5[:, [0, 2]] = x5[:, [2, 0]]
# Close the snapshot file if we don't want to keep it open
if not self.keep_snapshot_open:
self.close_snapshot()
# Are we stacking high-resolution and low-resolution particles?
if i5 is None or j5 - i5 == 0:
return x1
@ -475,7 +589,7 @@ class CSIBORG2Snapshot(BaseSnapshot):
###############################################################################
class QuijoteSnapshot(CSIBORG1Snapshot):
class QuijoteSnapshot(CSiBORG1Snapshot):
"""
Quijote snapshot class with the FoF halo finder particle assignment.
Because of similarities with how the snapshot is processed with CSiBORG1,
@ -489,9 +603,12 @@ class QuijoteSnapshot(CSIBORG1Snapshot):
Snapshot index.
paths : Paths, optional
Paths object.
keep_snapshot_open : bool, optional
Whether to keep the snapshot file open when reading halo particles.
This is useful for repeated access to the snapshot.
"""
def __init__(self, nsim, nsnap, paths=None):
super().__init__(nsim, nsnap, paths)
def __init__(self, nsim, nsnap, paths=None, keep_snapshot_open=False):
super().__init__(nsim, nsnap, paths, keep_snapshot_open, flip_xz=False)
self._snapshot_path = self.paths.snapshot(self.nsnap, self.nsim,
"quijote")
self._simname = "quijote"
@ -515,13 +632,17 @@ class BaseField(ABC):
"""
Base class for reading fields such as density or velocity fields.
"""
def __init__(self, nsim, paths):
def __init__(self, nsim, paths, flip_xz=False):
if isinstance(nsim, numpy.integer):
nsim = int(nsim)
if not isinstance(nsim, int):
raise TypeError(f"`nsim` must be an integer. Received `{type(nsim)}`.") # noqa
self._nsim = nsim
if not isinstance(flip_xz, bool):
raise TypeError("`flip_xz` must be a boolean.")
self._flip_xz = flip_xz
self._paths = paths
@property
@ -548,6 +669,18 @@ class BaseField(ABC):
self._paths = Paths(**paths_glamdring)
return self._paths
@property
def flip_xz(self):
"""
Whether to flip the x- and z-axes to undo the MUSIC bug so that the
coordinates are consistent with observations.
Returns
-------
bool
"""
return self._flip_xz
@abstractmethod
def density_field(self, MAS, grid):
"""
@ -584,6 +717,24 @@ class BaseField(ABC):
"""
pass
@abstractmethod
def radial_velocity_field(self, MAS, grid):
"""
Return the pre-computed radial velocity field.
Parameters
----------
MAS : str
Mass assignment scheme.
grid : int
Grid size.
Returns
-------
field : 3-dimensional array
"""
pass
###############################################################################
# CSiBORG1 field class #
@ -600,9 +751,12 @@ class CSiBORG1Field(BaseField):
Simulation index.
paths : Paths, optional
Paths object. By default, the paths are set to the `glamdring` paths.
flip_xz : bool, optional
Whether to flip the x- and z-axes to undo the MUSIC bug so that the
coordinates are consistent with observations.
"""
def __init__(self, nsim, paths=None):
super().__init__(nsim, paths)
def __init__(self, nsim, paths=None, flip_xz=True):
super().__init__(nsim, paths, flip_xz)
self._simname = "csiborg1"
def density_field(self, MAS, grid):
@ -615,8 +769,7 @@ class CSiBORG1Field(BaseField):
else:
field = numpy.load(fpath)
# Flip x- and z-axes
if self._simname == "csiborg1":
if self.flip_xz:
field = field.T
return field
@ -634,8 +787,7 @@ class CSiBORG1Field(BaseField):
else:
field = numpy.load(fpath)
# Flip x- and z-axes
if self._simname == "csiborg1":
if self.flip_xz:
field[0, ...] = field[0, ...].T
field[1, ...] = field[1, ...].T
field[2, ...] = field[2, ...].T
@ -643,6 +795,14 @@ class CSiBORG1Field(BaseField):
return field
def radial_velocity_field(self, MAS, grid):
if not self.flip_xz and self._simname == "csiborg1":
raise ValueError("The radial velocity field is only implemented "
"for the flipped x- and z-axes.")
fpath = self.paths.field("radvel", MAS, grid, self.nsim, "csiborg1")
return numpy.load(fpath)
###############################################################################
# CSiBORG2 field class #
@ -661,10 +821,12 @@ class CSiBORG2Field(BaseField):
CSiBORG2 run kind. One of `main`, `random`, or `varysmall`.
paths : Paths, optional
Paths object. By default, the paths are set to the `glamdring` paths.
flip_xz : bool, optional
Whether to flip the x- and z-axes to undo the MUSIC bug so that the
coordinates are consistent with observations.
"""
def __init__(self, nsim, kind, paths=None):
super().__init__(nsim, paths)
def __init__(self, nsim, kind, paths=None, flip_xz=True):
super().__init__(nsim, paths, flip_xz)
self.kind = kind
@property
@ -696,7 +858,9 @@ class CSiBORG2Field(BaseField):
else:
field = numpy.load(fpath)
field = field.T # Flip x- and z-axes
if self.flip_xz:
field = field.T
return field
def velocity_field(self, MAS, grid):
@ -713,14 +877,142 @@ class CSiBORG2Field(BaseField):
else:
field = numpy.load(fpath)
# Flip x- and z-axes
field[0, ...] = field[0, ...].T
field[1, ...] = field[1, ...].T
field[2, ...] = field[2, ...].T
field[[0, 2], ...] = field[[2, 0], ...]
if self.flip_xz:
field[0, ...] = field[0, ...].T
field[1, ...] = field[1, ...].T
field[2, ...] = field[2, ...].T
field[[0, 2], ...] = field[[2, 0], ...]
return field
def radial_velocity_field(self, MAS, grid):
if not self.flip_xz:
raise ValueError("The radial velocity field is only implemented "
"for the flipped x- and z-axes.")
fpath = self.paths.field("radvel", MAS, grid, self.nsim,
f"csiborg2_{self.kind}")
return numpy.load(fpath)
###############################################################################
# BORG1 field class #
###############################################################################
class BORG1Field(BaseField):
"""
BORG2 `z = 0` field class.
Parameters
----------
nsim : int
Simulation index.
paths : Paths, optional
Paths object. By default, the paths are set to the `glamdring` paths.
"""
def __init__(self, nsim, paths=None):
super().__init__(nsim, paths, False)
def overdensity_field(self):
fpath = self.paths.field(None, None, None, self.nsim, "borg1")
with File(fpath, "r") as f:
field = f["scalars/BORG_final_density"][:].astype(numpy.float32)
return field
def density_field(self):
field = self.overdensity_field()
omega0 = 0.307
rho_mean = omega0 * 277.53662724583074 # Msun / kpc^3
field += 1
field *= rho_mean
return field
def velocity_field(self, MAS, grid):
raise RuntimeError("The velocity field is not available.")
def radial_velocity_field(self, MAS, grid):
raise RuntimeError("The radial velocity field is not available.")
###############################################################################
# BORG2 field class #
###############################################################################
class BORG2Field(BaseField):
"""
BORG2 `z = 0` field class.
Parameters
----------
nsim : int
Simulation index.
paths : Paths, optional
Paths object. By default, the paths are set to the `glamdring` paths.
"""
def __init__(self, nsim, paths=None):
super().__init__(nsim, paths, False)
def overdensity_field(self):
fpath = self.paths.field(None, None, None, self.nsim, "borg2")
with File(fpath, "r") as f:
field = f["scalars/BORG_final_density"][:].astype(numpy.float32)
return field
def density_field(self):
field = self.overdensity_field()
omega0 = 0.3111
rho_mean = omega0 * 277.53662724583074 # h^2 Msun / kpc^3
field += 1
field *= rho_mean
# return field
def velocity_field(self, MAS, grid):
raise RuntimeError("The velocity field is not available.")
def radial_velocity_field(self, MAS, grid):
raise RuntimeError("The radial velocity field is not available.")
###############################################################################
# TNG300-1 field #
###############################################################################
class TNG300_1Field(BaseField):
"""
TNG300-1 dark matter-only `z = 0` field class.
Parameters
----------
paths : Paths, optional
Paths object. By default, the paths are set to the `glamdring` paths.
"""
def __init__(self, paths=None):
super().__init__(0, paths, False)
def overdensity_field(self, MAS, grid):
density = self.density_field(MAS, grid)
omega_dm = 0.3089 - 0.0486
rho_mean = omega_dm * 277.53662724583074 # h^2 Msun / kpc^3
density /= rho_mean
density -= 1
return density
def density_field(self, MAS, grid):
fpath = join(self.paths.tng300_1, "postprocessing", "density_field",
f"rho_dm_099_{grid}_{MAS}.npy")
return numpy.load(fpath)
def velocity_field(self, MAS, grid):
raise RuntimeError("The velocity field is not available.")
def radial_velocity_field(self, MAS, grid):
raise RuntimeError("The radial velocity field is not available.")
###############################################################################
# Quijote field class #
@ -739,7 +1031,7 @@ class QuijoteField(CSiBORG1Field):
Paths object.
"""
def __init__(self, nsim, paths):
super().__init__(nsim, paths)
super().__init__(nsim, paths, flip_xz=False)
self._simname = "quijote"

View file

@ -21,61 +21,53 @@ from tqdm import tqdm
###############################################################################
def read_interpolated_field(survey_name, kind, galaxy_index, paths, MAS, grid,
in_rsp, rand_data=False, verbose=True):
def read_interpolated_field(survey, simname, kind, MAS, grid, paths,
verbose=True):
"""
Read in the interpolated field at the galaxy positions, and reorder the
data to match the galaxy index.
Parameters
----------
survey_name : str
Survey name.
survey : Survey
Survey object.
simname : str
Simulation name.
kind : str
Field kind.
galaxy_index : 1-dimensional array
Galaxy indices to read in.
paths : py:class:`csiborgtools.read.Paths`
Paths manager.
MAS : str
Mass assignment scheme.
grid : int
Grid size.
in_rsp : bool
Whether to read in the field in redshift space.
rand_data : bool, optional
Whether to read in the random field data instead of the galaxy field.
paths : py:class:`csiborgtools.read.Paths`
Paths manager.
verbose : bool, optional
Verbosity flag.
Returns
-------
3-dimensional array of shape (nsims, len(galaxy_index), nsmooth)
val : 3-dimensional array of shape (nsims, num_gal, nsmooth)
Scalar field values at the galaxy positions.
smooth_scales : 1-dimensional array
Smoothing scales.
"""
nsims = paths.get_ics("csiborg")
nsims = paths.get_ics(simname)
for i, nsim in enumerate(tqdm(nsims,
desc="Reading fields",
disable=not verbose)):
fpath = paths.field_interpolated(
survey_name, kind, MAS, grid, nsim, in_rsp=in_rsp)
fpath = paths.field_interpolated(survey.name, simname, nsim, kind, MAS,
grid)
data = numpy.load(fpath)
out_ = data["val"] if not rand_data else data["rand_val"]
out_ = data["val"]
if i == 0:
out = numpy.empty((len(nsims), *out_.shape), dtype=out_.dtype)
indxs = data["indxs"]
smooth_scales = data["smooth_scales"]
out[i] = out_
# Reorder the data to match the survey index.
ind2pos = {v: k for k, v in enumerate(indxs)}
ks = numpy.empty(len(galaxy_index), dtype=numpy.int64)
if survey.selection_mask is not None:
out = out[:, survey.selection_mask, :]
for i, k in enumerate(galaxy_index):
j = ind2pos.get(k, None)
if j is None:
raise ValueError(f"There is no galaxy with index {k} in the "
"interpolated field.")
ks[i] = j
return out[:, ks, :]
return out, smooth_scales

View file

@ -32,7 +32,8 @@ def find_peak(x, weights, shrink=0.95, min_obs=5):
"""
Find the peak of a 1D distribution using a shrinking window.
"""
assert shrink <= 1.
if not shrink < 1:
raise ValueError("`shrink` must be less than 1.")
xmin, xmax = numpy.min(x), numpy.max(x)
xpos = (xmax + xmin) / 2
@ -58,9 +59,9 @@ class PairOverlap:
Parameters
----------
cat0 : :py:class:`csiborgtools.read.CSiBORGHaloCatalogue`
cat0 : instance of :py:class:`csiborgtools.read.BaseCatalogue`
Halo catalogue corresponding to the reference simulation.
catx : :py:class:`csiborgtools.read.CSiBORGHaloCatalogue`
catx : instance of :py:class:`csiborgtools.read.BaseCatalogue`
Halo catalogue corresponding to the cross simulation.
min_logmass : float
Minimum halo mass in :math:`\log_{10} M_\odot / h` to consider.
@ -305,17 +306,21 @@ class PairOverlap:
"""
assert (norm_kind is None or norm_kind in ("r200c", "ref_patch", "sum_patch")) # noqa
# Get positions either in the initial or final snapshot
pos0 = self.cat0().position(in_initial=in_initial)
posx = self.catx().position(in_initial=in_initial)
if in_initial:
pos0 = self.cat0("lagpatch_coordinates")
posx = self.catx("lagpatch_coordinates")
else:
pos0 = self.cat0("cartesian_pos")
posx = self.catx("cartesian_pos")
# Get the normalisation array if applicable
if norm_kind == "r200c":
norm = self.cat0("r200c")
if norm_kind == "ref_patch":
norm = self.cat0("lagpatch_size")
norm = self.cat0("lagpatch_radius")
if norm_kind == "sum_patch":
patch0 = self.cat0("lagpatch_size")
patchx = self.catx("lagpatch_size")
patch0 = self.cat0("lagpatch_radius")
patchx = self.catx("lagpatch_radius")
norm = [None] * len(self)
for i, ind in enumerate(self["match_indxs"]):
norm[i] = patch0[i] + patchx[ind]
@ -330,7 +335,7 @@ class PairOverlap:
dist[i] /= norm[i]
return numpy.array(dist, dtype=object)
def mass_ratio(self, mass_kind="totpartmass", in_log=True, in_abs=True):
def mass_ratio(self, in_log=True, in_abs=True):
"""
Pair mass ratio of matched halos between the reference and cross
simulations.
@ -350,7 +355,7 @@ class PairOverlap:
-------
ratio : array of 1-dimensional arrays of shape `(nhalos, )`
"""
mass0, massx = self.cat0(mass_kind), self.catx(mass_kind)
mass0, massx = self.cat0("totmass"), self.catx("totmass")
ratio = [None] * len(self)
for i, ind in enumerate(self["match_indxs"]):