mirror of
https://github.com/Richard-Sti/csiborgtools.git
synced 2024-12-22 21:38:03 +00:00
Fixing overlaps and halo definitions. (#80)
* Add imports * Refactor code * Rename fof velocities * Clean up and add Quijote * Edit docstrings * Update submission script * Fix bug * Start loading fitted properties * Edit docstrings * Update fitting for new `halo` * Update CM definition and R200c * Tune the minimum number of particles * Enforce crossing threshold & tune hypers * Fix periodiity when calculating angmom * Doc strings * Relax checkip * Minor edit * Fix old kwarg bug * Fix CSiBORG bounds * Catch warnings! * Add `mass_kind` and new boundaries
This commit is contained in:
parent
169a5e5bd7
commit
344ff8e091
10 changed files with 543 additions and 388 deletions
|
@ -19,17 +19,13 @@ import numpy
|
||||||
from numba import jit
|
from numba import jit
|
||||||
from scipy.optimize import minimize
|
from scipy.optimize import minimize
|
||||||
|
|
||||||
|
GRAV = 4.300917270069976e-09 # G in (Msun / h)^-1 (Mpc / h) (km / s)^2
|
||||||
GRAV = 6.6743e-11 # m^3 kg^-1 s^-2
|
|
||||||
MSUN = 1.988409870698051e+30 # kg
|
|
||||||
MPC2M = 3.0856775814671916e+22 # 1 Mpc is this many meters
|
|
||||||
|
|
||||||
|
|
||||||
class BaseStructure(ABC):
|
class BaseStructure(ABC):
|
||||||
"""
|
"""
|
||||||
Basic structure object for handling operations on its particles.
|
Basic structure object for handling operations on its particles.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_particles = None
|
_particles = None
|
||||||
_box = None
|
_box = None
|
||||||
|
|
||||||
|
@ -90,94 +86,119 @@ class BaseStructure(ABC):
|
||||||
"""
|
"""
|
||||||
return numpy.vstack([self[p] for p in ("vx", "vy", "vz")]).T
|
return numpy.vstack([self[p] for p in ("vx", "vy", "vz")]).T
|
||||||
|
|
||||||
def spherical_overdensity_mass(self, delta_mult, kind="crit", rtol=1e-8,
|
def center_of_mass(self, npart_min=30, shrink_factor=0.98):
|
||||||
maxiter=100, npart_min=10):
|
|
||||||
r"""
|
r"""
|
||||||
Calculate spherical overdensity mass and radius via the iterative
|
Calculate the center of mass of a halo via the shrinking sphere
|
||||||
shrinking sphere method.
|
procedure. Iteratively reduces initial radius and calculates the CM of
|
||||||
|
enclosed particles while the number of enclosed particles is greater
|
||||||
|
than a set minimum.
|
||||||
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
npart_min : int, optional
|
||||||
|
Minimum number of enclosed particles above which to continue
|
||||||
|
shrinking the sphere.
|
||||||
|
shrink_factor : float, optional
|
||||||
|
Factor by which to shrink the sphere radius at each iteration.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
cm : 1-dimensional array of shape `(3, )`
|
||||||
|
Center of mass in box units.
|
||||||
|
dist : 1-dimensional array of shape `(n_particles, )`
|
||||||
|
Distance of each particle from the center of mass in box units.
|
||||||
|
"""
|
||||||
|
pos, mass = self.pos, self["M"]
|
||||||
|
|
||||||
|
cm = center_of_mass(pos, mass, boxsize=1)
|
||||||
|
rad = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
dist = periodic_distance(pos, cm, boxsize=1)
|
||||||
|
|
||||||
|
if rad is None:
|
||||||
|
rad = numpy.max(dist)
|
||||||
|
|
||||||
|
within_rad = dist <= rad
|
||||||
|
|
||||||
|
cm = center_of_mass(pos[within_rad], mass[within_rad], boxsize=1)
|
||||||
|
|
||||||
|
if numpy.sum(within_rad) < npart_min:
|
||||||
|
return cm, periodic_distance(pos, cm, boxsize=1)
|
||||||
|
|
||||||
|
rad *= shrink_factor
|
||||||
|
|
||||||
|
def spherical_overdensity_mass(self, dist, delta_mult, kind="crit"):
|
||||||
|
r"""
|
||||||
|
Calculate spherical overdensity mass and radius around a CM, defined as
|
||||||
|
the inner-most radius where the density falls below a given threshold.
|
||||||
|
The exact radius is found via linear interpolation between the two
|
||||||
|
particles enclosing the threshold.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dist : 1-dimensional array of shape `(n_particles, )`
|
||||||
|
Distance of each particle from the centre of mass in box units.
|
||||||
delta_mult : int or float
|
delta_mult : int or float
|
||||||
Overdensity multiple.
|
Overdensity multiple.
|
||||||
kind : str, optional
|
kind : str, optional
|
||||||
Either `crit` or `matter`, for critical or matter overdensity
|
Either `crit` or `matter`, for critical or matter overdensity
|
||||||
rtol : float, optional
|
|
||||||
Tolerance for the change in the center of mass or radius.
|
|
||||||
maxiter : int, optional
|
|
||||||
Maximum number of iterations.
|
|
||||||
npart_min : int, optional
|
|
||||||
Minimum number of enclosed particles to reset the iterator.
|
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
mass : float
|
mass : float
|
||||||
The requested spherical overdensity mass in :math:`M_\odot / h`.
|
Overdensity mass in (Msun / h).
|
||||||
rad : float
|
rad : float
|
||||||
The radius of the sphere enclosing the requested overdensity in box
|
Overdensity radius in box units.
|
||||||
units.
|
|
||||||
cm : 1-dimensional array of shape `(3, )`
|
|
||||||
The center of mass of the sphere enclosing the requested
|
|
||||||
overdensity in box units.
|
|
||||||
"""
|
"""
|
||||||
assert kind in ["crit", "matter"]
|
if kind not in ["crit", "matter"]:
|
||||||
|
raise ValueError("kind must be either `crit` or `matter`.")
|
||||||
|
|
||||||
# Calculate density based on the provided kind
|
|
||||||
rho = delta_mult * self.box.rho_crit0
|
rho = delta_mult * self.box.rho_crit0
|
||||||
if kind == "matter":
|
rho *= self.box.Om if kind == "matter" else 1.
|
||||||
rho *= self.box.Om
|
|
||||||
|
|
||||||
pos, mass = self.pos, self["M"]
|
argsort = numpy.argsort(dist)
|
||||||
|
dist = self.box.box2mpc(dist[argsort])
|
||||||
|
|
||||||
# Initial estimates for center of mass and radius
|
norm_density = numpy.cumsum(self['M'][argsort])
|
||||||
init_cm = center_of_mass(pos, mass, boxsize=1)
|
totmass = norm_density[-1]
|
||||||
init_rad = self.box.mpc2box(mass_to_radius(numpy.sum(mass), rho) * 1.5)
|
with numpy.errstate(divide="ignore"):
|
||||||
|
norm_density /= (4. / 3. * numpy.pi * dist**3)
|
||||||
|
norm_density /= rho
|
||||||
|
|
||||||
rad, cm = init_rad, numpy.copy(init_cm)
|
# This ensures that the j - 1 index is also just above 1, therefore the
|
||||||
|
# expression below strictly interpolates.
|
||||||
|
j = find_first_below_threshold(norm_density, 1.)
|
||||||
|
|
||||||
for _ in range(maxiter):
|
if j is None:
|
||||||
dist = periodic_distance(pos, cm, boxsize=1)
|
return numpy.nan, numpy.nan
|
||||||
within_rad = dist <= rad
|
|
||||||
|
|
||||||
# Heuristic reset if too few enclosed particles
|
i = j - 1
|
||||||
if numpy.sum(within_rad) < npart_min:
|
|
||||||
js = numpy.random.choice(len(self), len(self), replace=True)
|
|
||||||
cm = center_of_mass(pos[js], mass[js], boxsize=1)
|
|
||||||
rad = init_rad * (0.75 + numpy.random.rand())
|
|
||||||
dist = periodic_distance(pos, cm, boxsize=1)
|
|
||||||
within_rad = dist <= rad
|
|
||||||
|
|
||||||
# If there are still too few particles, then skip this
|
rad = (dist[j] - dist[i])
|
||||||
# iteration.
|
rad *= (1. - norm_density[i]) / (norm_density[j] - norm_density[i])
|
||||||
if numpy.sum(within_rad) < npart_min:
|
rad += dist[i]
|
||||||
continue
|
|
||||||
|
|
||||||
enclosed_mass = numpy.sum(mass[within_rad])
|
mass = radius_to_mass(rad, rho)
|
||||||
new_rad = self.box.mpc2box(mass_to_radius(enclosed_mass, rho))
|
rad = self.box.mpc2box(rad)
|
||||||
new_cm = center_of_mass(pos[within_rad], mass[within_rad],
|
|
||||||
boxsize=1)
|
|
||||||
|
|
||||||
# Check convergence based on center of mass and radius
|
if mass > totmass:
|
||||||
cm_conv = numpy.linalg.norm(cm - new_cm) < rtol
|
return numpy.nan, numpy.nan
|
||||||
rad_conv = abs(rad - new_rad) < rtol
|
|
||||||
|
|
||||||
if cm_conv or rad_conv:
|
return mass, rad
|
||||||
return enclosed_mass, rad, cm
|
|
||||||
|
|
||||||
cm, rad = new_cm, new_rad
|
def angular_momentum(self, dist, cm, rad, npart_min=10):
|
||||||
|
|
||||||
# Return NaN values if no convergence after max iterations
|
|
||||||
return numpy.nan, numpy.nan, numpy.full(3, numpy.nan, numpy.float32)
|
|
||||||
|
|
||||||
def angular_momentum(self, ref, rad, npart_min=10):
|
|
||||||
r"""
|
r"""
|
||||||
Calculate angular momentum around a reference point using all particles
|
Calculate angular momentum around a centre of mass using all particles
|
||||||
within a radius. Units are
|
within a radius. Accounts for periodicity of the box and units are
|
||||||
:math:`(M_\odot / h) (\mathrm{Mpc} / h) \mathrm{km} / \mathrm{s}`.
|
(Msun / h) * (Mpc / h) * (km / s).
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
ref : 1-dimensional array of shape `(3, )`
|
dist : 1-dimensional array of shape `(n_particles, )`
|
||||||
|
Distance of each particle from center of mass in box units.
|
||||||
|
cm : 1-dimensional array of shape `(3, )`
|
||||||
Reference point in box units.
|
Reference point in box units.
|
||||||
rad : float
|
rad : float
|
||||||
Radius around the reference point in box units.
|
Radius around the reference point in box units.
|
||||||
|
@ -189,31 +210,28 @@ class BaseStructure(ABC):
|
||||||
-------
|
-------
|
||||||
angmom : 1-dimensional array or shape `(3, )`
|
angmom : 1-dimensional array or shape `(3, )`
|
||||||
"""
|
"""
|
||||||
# Calculate the distance of each particle from the reference point.
|
mask = dist < rad
|
||||||
distances = periodic_distance(self.pos, ref, boxsize=1)
|
|
||||||
|
|
||||||
# Filter particles within the provided radius.
|
|
||||||
mask = distances < rad
|
|
||||||
if numpy.sum(mask) < npart_min:
|
if numpy.sum(mask) < npart_min:
|
||||||
return numpy.full(3, numpy.nan, numpy.float32)
|
return numpy.full(3, numpy.nan, numpy.float32)
|
||||||
|
|
||||||
mass, pos, vel = self["M"][mask], self.pos[mask], self.vel[mask]
|
mass, pos, vel = self["M"][mask], self.pos[mask], self.vel[mask]
|
||||||
|
|
||||||
# Convert positions to Mpc / h and center around the reference point.
|
pos = shift_to_center_of_box(pos, cm, 1.0, set_cm_to_zero=True)
|
||||||
pos = self.box.box2mpc(pos) - ref
|
pos = self.box.box2mpc(pos)
|
||||||
# Adjust velocities to be in the CM frame.
|
|
||||||
vel -= numpy.average(vel, axis=0, weights=mass)
|
vel -= numpy.average(vel, axis=0, weights=mass)
|
||||||
# Calculate angular momentum.
|
|
||||||
return numpy.sum(mass[:, numpy.newaxis] * numpy.cross(pos, vel),
|
return numpy.sum(mass[:, numpy.newaxis] * numpy.cross(pos, vel),
|
||||||
axis=0)
|
axis=0)
|
||||||
|
|
||||||
def lambda_bullock(self, ref, rad):
|
def lambda_bullock(self, angmom, mass, rad):
|
||||||
r"""
|
"""
|
||||||
Bullock spin, see Eq. 5 in [1], in a given radius around a reference
|
Calculate the Bullock spin, see Eq. 5 in [1].
|
||||||
point.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
angmom : 1-dimensional array of shape `(3, )`
|
||||||
|
Angular momentum in (Msun / h) * (Mpc / h) * (km / s).
|
||||||
ref : 1-dimensional array of shape `(3, )`
|
ref : 1-dimensional array of shape `(3, )`
|
||||||
Reference point in box units.
|
Reference point in box units.
|
||||||
rad : float
|
rad : float
|
||||||
|
@ -229,28 +247,18 @@ class BaseStructure(ABC):
|
||||||
Bullock, J. S.; Dekel, A.; Kolatt, T. S.; Kravtsov, A. V.;
|
Bullock, J. S.; Dekel, A.; Kolatt, T. S.; Kravtsov, A. V.;
|
||||||
Klypin, A. A.; Porciani, C.; Primack, J. R.
|
Klypin, A. A.; Porciani, C.; Primack, J. R.
|
||||||
"""
|
"""
|
||||||
# Filter particles within the provided radius
|
out = numpy.linalg.norm(angmom)
|
||||||
mask = periodic_distance(self.pos, ref, boxsize=1) < rad
|
return out / numpy.sqrt(2 * GRAV * mass**3 * self.box.box2mpc(rad))
|
||||||
# Calculate the total mass of the enclosed particles
|
|
||||||
enclosed_mass = numpy.sum(self["M"][mask])
|
|
||||||
# Convert the radius from box units to Mpc/h
|
|
||||||
rad_mpc = self.box.box2mpc(rad)
|
|
||||||
# Circular velocity in km/s
|
|
||||||
circvel = (GRAV * enclosed_mass * MSUN / (rad_mpc * MPC2M))**0.5 * 1e-3
|
|
||||||
# Magnitude of the angular momentum
|
|
||||||
l_norm = numpy.linalg.norm(self.angular_momentum(ref, rad))
|
|
||||||
# Compute and return the Bullock spin parameter
|
|
||||||
return l_norm / (numpy.sqrt(2) * enclosed_mass * circvel * rad_mpc)
|
|
||||||
|
|
||||||
def nfw_concentration(self, ref, rad, conc_min=1e-3, npart_min=10):
|
def nfw_concentration(self, dist, rad, conc_min=1e-3, npart_min=10):
|
||||||
"""
|
"""
|
||||||
Calculate the NFW concentration parameter in a given radius around a
|
Calculate the NFW concentration parameter in a given radius around a
|
||||||
reference point.
|
reference point.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
ref : 1-dimensional array of shape `(3, )`
|
dist : 1-dimensional array of shape `(n_particles, )`
|
||||||
Reference point in box units.
|
Distance of each particle from center of mass in box units.
|
||||||
rad : float
|
rad : float
|
||||||
Radius around the reference point in box units.
|
Radius around the reference point in box units.
|
||||||
conc_min : float
|
conc_min : float
|
||||||
|
@ -263,36 +271,25 @@ class BaseStructure(ABC):
|
||||||
-------
|
-------
|
||||||
conc : float
|
conc : float
|
||||||
"""
|
"""
|
||||||
dist = periodic_distance(self.pos, ref, boxsize=1)
|
|
||||||
mask = dist < rad
|
mask = dist < rad
|
||||||
|
|
||||||
if numpy.sum(mask) < npart_min:
|
if numpy.sum(mask) < npart_min:
|
||||||
return numpy.nan
|
return numpy.nan
|
||||||
|
|
||||||
dist, weight = dist[mask], self["M"][mask]
|
dist, weight = dist[mask], self["M"][mask]
|
||||||
weight /= numpy.mean(weight)
|
weight /= weight[0]
|
||||||
|
|
||||||
# Objective function for minimization
|
res = minimize(negll_nfw_concentration, x0=1.,
|
||||||
def negll_nfw_concentration(log_c, xs, w):
|
|
||||||
c = 10**log_c
|
|
||||||
ll = xs / (1 + c * xs)**2 * c**2
|
|
||||||
ll *= (1 + c) / ((1 + c) * numpy.log(1 + c) - c)
|
|
||||||
ll = numpy.sum(numpy.log(w * ll))
|
|
||||||
return -ll
|
|
||||||
|
|
||||||
initial_guess = 1.5
|
|
||||||
res = minimize(negll_nfw_concentration, x0=initial_guess,
|
|
||||||
args=(dist / rad, weight, ), method='Nelder-Mead',
|
args=(dist / rad, weight, ), method='Nelder-Mead',
|
||||||
bounds=((numpy.log10(conc_min), 5),))
|
bounds=((numpy.log10(conc_min), 5),))
|
||||||
|
|
||||||
if not res.success:
|
if not res.success:
|
||||||
return numpy.nan
|
return numpy.nan
|
||||||
|
|
||||||
conc_value = 10**res["x"][0]
|
conc = 10**res["x"][0]
|
||||||
if conc_value < conc_min or numpy.isclose(conc_value, conc_min):
|
if conc < conc_min or numpy.isclose(conc, conc_min):
|
||||||
return numpy.nan
|
return numpy.nan
|
||||||
|
|
||||||
return conc_value
|
return conc
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
key_to_index = {'x': 0, 'y': 1, 'z': 2,
|
key_to_index = {'x': 0, 'y': 1, 'z': 2,
|
||||||
|
@ -329,11 +326,12 @@ class Halo(BaseStructure):
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
@jit(nopython=True, fastmath=True, boundscheck=False)
|
||||||
def center_of_mass(points, mass, boxsize):
|
def center_of_mass(points, mass, boxsize):
|
||||||
"""
|
"""
|
||||||
Calculate the center of mass of a halo, while assuming for periodic
|
Calculate the center of mass of a halo while assuming periodic boundary
|
||||||
boundary conditions of a cubical box. Assuming that particle positions are
|
conditions of a cubical box. Assuming that particle positions are in
|
||||||
in `[0, boxsize)` range.
|
`[0, boxsize)` range. This is a JIT implementation.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
@ -348,20 +346,29 @@ def center_of_mass(points, mass, boxsize):
|
||||||
-------
|
-------
|
||||||
cm : 1-dimensional array of shape `(3, )`
|
cm : 1-dimensional array of shape `(3, )`
|
||||||
"""
|
"""
|
||||||
# Convert positions to unit circle coordinates in the complex plane
|
cm = numpy.zeros(3, dtype=points.dtype)
|
||||||
pos = numpy.exp(2j * numpy.pi * points / boxsize)
|
totmass = sum(mass)
|
||||||
# Compute weighted average of these coordinates, convert it back to
|
|
||||||
# box coordinates and fix any negative positions due to angle calculations.
|
# Convert positions to unit circle coordinates in the complex plane,
|
||||||
cm = numpy.angle(numpy.average(pos, axis=0, weights=mass))
|
# calculate the weighted average and convert it back to box coordinates.
|
||||||
cm *= boxsize / (2 * numpy.pi)
|
for i in range(3):
|
||||||
cm[cm < 0] += boxsize
|
cm_i = sum(mass * numpy.exp(2j * numpy.pi * points[:, i] / boxsize))
|
||||||
|
cm_i /= totmass
|
||||||
|
|
||||||
|
cm_i = numpy.arctan2(cm_i.imag, cm_i.real) * boxsize / (2 * numpy.pi)
|
||||||
|
|
||||||
|
if cm_i < 0:
|
||||||
|
cm_i += boxsize
|
||||||
|
cm[i] = cm_i
|
||||||
|
|
||||||
return cm
|
return cm
|
||||||
|
|
||||||
|
|
||||||
|
@jit(nopython=True)
|
||||||
def periodic_distance(points, reference, boxsize):
|
def periodic_distance(points, reference, boxsize):
|
||||||
"""
|
"""
|
||||||
Compute the periodic distance between multiple points and a reference
|
Compute the 3D distance between multiple points and a reference point using
|
||||||
point.
|
periodic boundary conditions. This is an optimized JIT implementation.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
@ -376,9 +383,22 @@ def periodic_distance(points, reference, boxsize):
|
||||||
-------
|
-------
|
||||||
dist : 1-dimensional array of shape `(n_points, )`
|
dist : 1-dimensional array of shape `(n_points, )`
|
||||||
"""
|
"""
|
||||||
delta = numpy.abs(points - reference)
|
npoints = len(points)
|
||||||
delta = numpy.where(delta > boxsize / 2, boxsize - delta, delta)
|
half_box = boxsize / 2
|
||||||
return numpy.linalg.norm(delta, axis=1)
|
|
||||||
|
dist = numpy.zeros(npoints, dtype=points.dtype)
|
||||||
|
for i in range(npoints):
|
||||||
|
for j in range(3):
|
||||||
|
dist_1d = abs(points[i, j] - reference[j])
|
||||||
|
|
||||||
|
if dist_1d > (half_box):
|
||||||
|
dist_1d = boxsize - dist_1d
|
||||||
|
|
||||||
|
dist[i] += dist_1d**2
|
||||||
|
|
||||||
|
dist[i] = dist[i]**0.5
|
||||||
|
|
||||||
|
return dist
|
||||||
|
|
||||||
|
|
||||||
def shift_to_center_of_box(points, cm, boxsize, set_cm_to_zero=False):
|
def shift_to_center_of_box(points, cm, boxsize, set_cm_to_zero=False):
|
||||||
|
@ -407,26 +427,74 @@ def shift_to_center_of_box(points, cm, boxsize, set_cm_to_zero=False):
|
||||||
return pos
|
return pos
|
||||||
|
|
||||||
|
|
||||||
def mass_to_radius(mass, rho):
|
@jit(nopython=True, fastmath=True, boundscheck=False)
|
||||||
|
def radius_to_mass(radius, rho):
|
||||||
"""
|
"""
|
||||||
Compute the radius of a sphere with a given mass and density.
|
Compute the mass of a sphere with a given radius and density.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
mass : float
|
radius : float
|
||||||
Mass of the sphere.
|
Radius of the sphere.
|
||||||
rho : float
|
rho : float
|
||||||
Density of the sphere.
|
Density of the sphere.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
rad : float
|
mass : float
|
||||||
Radius of the sphere.
|
|
||||||
"""
|
"""
|
||||||
return ((3 * mass) / (4 * numpy.pi * rho))**(1./3)
|
return ((4 * numpy.pi * rho) / 3) * radius**3
|
||||||
|
|
||||||
|
|
||||||
@jit(nopython=True)
|
@jit(nopython=True, fastmath=True, boundscheck=False)
|
||||||
|
def find_first_below_threshold(x, threshold):
|
||||||
|
"""
|
||||||
|
Find index of first element in `x` that is below `threshold`. The index
|
||||||
|
must be greater than 0. If no such element is found, return `None`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
x : 1-dimensional array
|
||||||
|
Array to search in.
|
||||||
|
threshold : float
|
||||||
|
Threshold value.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
index : int or None
|
||||||
|
"""
|
||||||
|
for i in range(1, len(x)):
|
||||||
|
if 1 < x[i - 1] and x[i] < threshold:
|
||||||
|
return i
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@jit(nopython=True, fastmath=True, boundscheck=False)
|
||||||
|
def negll_nfw_concentration(log_c, xs, w):
|
||||||
|
"""
|
||||||
|
Negative log-likelihood of the NFW concentration parameter.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
log_c : float
|
||||||
|
Logarithm of the concentration parameter.
|
||||||
|
xs : 1-dimensional array
|
||||||
|
Normalised radii.
|
||||||
|
w : 1-dimensional array
|
||||||
|
Weights.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
------
|
||||||
|
negll : float
|
||||||
|
"""
|
||||||
|
c = 10**log_c
|
||||||
|
ll = xs / (1 + c * xs)**2 * c**2
|
||||||
|
ll *= (1 + c) / ((1 + c) * numpy.log(1 + c) - c)
|
||||||
|
ll = numpy.sum(numpy.log(w * ll))
|
||||||
|
return -ll
|
||||||
|
|
||||||
|
|
||||||
|
@jit(nopython=True, fastmath=True, boundscheck=False)
|
||||||
def delta2ncells(delta):
|
def delta2ncells(delta):
|
||||||
"""
|
"""
|
||||||
Calculate the number of cells in `delta` that are non-zero.
|
Calculate the number of cells in `delta` that are non-zero.
|
||||||
|
@ -451,7 +519,7 @@ def delta2ncells(delta):
|
||||||
return tot
|
return tot
|
||||||
|
|
||||||
|
|
||||||
@jit(nopython=True)
|
@jit(nopython=True, fastmath=True, boundscheck=False)
|
||||||
def number_counts(x, bin_edges):
|
def number_counts(x, bin_edges):
|
||||||
"""
|
"""
|
||||||
Calculate counts of samples in bins.
|
Calculate counts of samples in bins.
|
||||||
|
|
|
@ -13,5 +13,5 @@
|
||||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
from .match import (ParticleOverlap, RealisationsMatcher, # noqa
|
from .match import (ParticleOverlap, RealisationsMatcher, # noqa
|
||||||
calculate_overlap, calculate_overlap_indxs,
|
calculate_overlap, calculate_overlap_indxs, pos2cell,
|
||||||
cosine_similarity, find_neighbour)
|
cosine_similarity, find_neighbour, get_halo_cell_limits)
|
||||||
|
|
|
@ -21,8 +21,9 @@ from functools import lru_cache
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numba import jit
|
|
||||||
from scipy.ndimage import gaussian_filter
|
from scipy.ndimage import gaussian_filter
|
||||||
|
|
||||||
|
from numba import jit
|
||||||
from tqdm import tqdm, trange
|
from tqdm import tqdm, trange
|
||||||
|
|
||||||
from ..read import load_halo_particles
|
from ..read import load_halo_particles
|
||||||
|
@ -45,34 +46,39 @@ class BaseMatcher(ABC):
|
||||||
box_size : int
|
box_size : int
|
||||||
"""
|
"""
|
||||||
if self._box_size is None:
|
if self._box_size is None:
|
||||||
raise RuntimeError("`box_size` is not set.")
|
raise RuntimeError("`box_size` has not been set.")
|
||||||
return self._box_size
|
return self._box_size
|
||||||
|
|
||||||
@box_size.setter
|
@box_size.setter
|
||||||
def box_size(self, value):
|
def box_size(self, value):
|
||||||
assert isinstance(value, int)
|
if not (isinstance(value, int) and value > 0):
|
||||||
assert value > 0
|
raise ValueError("`box_size` must be a positive integer.")
|
||||||
|
if not value != 0 and (value & (value - 1) == 0):
|
||||||
|
raise ValueError("`box_size` must be a power of 2.")
|
||||||
self._box_size = value
|
self._box_size = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bckg_halfsize(self):
|
def bckg_halfsize(self):
|
||||||
"""
|
"""
|
||||||
Number of to each side of the centre of the box to calculate the
|
Background half-size for density field calculation. This is the
|
||||||
density field. This is because in CSiBORG we are only interested in the
|
grid distance from the center of the box to each side over which to
|
||||||
high-resolution region.
|
evaluate the background density field. Must be less than or equal to
|
||||||
|
half the box size.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
bckg_halfsize : int
|
bckg_halfsize : int
|
||||||
"""
|
"""
|
||||||
if self._bckg_halfsize is None:
|
if self._bckg_halfsize is None:
|
||||||
raise RuntimeError("`bckg_halfsize` is not set.")
|
raise RuntimeError("`bckg_halfsize` has not been set.")
|
||||||
return self._bckg_halfsize
|
return self._bckg_halfsize
|
||||||
|
|
||||||
@bckg_halfsize.setter
|
@bckg_halfsize.setter
|
||||||
def bckg_halfsize(self, value):
|
def bckg_halfsize(self, value):
|
||||||
assert isinstance(value, int)
|
if not (isinstance(value, int) and value > 0):
|
||||||
assert value > 0
|
raise ValueError("`bckg_halfsize` must be a positive integer.")
|
||||||
|
if value > self.box_size // 2:
|
||||||
|
raise ValueError("`bckg_halfsize` must be <= half the box size.")
|
||||||
self._bckg_halfsize = value
|
self._bckg_halfsize = value
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,26 +89,26 @@ class BaseMatcher(ABC):
|
||||||
|
|
||||||
class RealisationsMatcher(BaseMatcher):
|
class RealisationsMatcher(BaseMatcher):
|
||||||
"""
|
"""
|
||||||
A tool to match haloes between IC realisations.
|
Matches haloes between IC realisations.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
box_size : int
|
box_size : int
|
||||||
Number of cells in the box.
|
Number of cells in the box.
|
||||||
bckg_halfsize : int
|
bckg_halfsize : int
|
||||||
Number of to each side of the centre of the box to calculate the
|
Background half-size for density field calculation. This is the
|
||||||
density field. This is because in CSiBORG we are only interested in the
|
grid distance from the center of the box to each side over which to
|
||||||
high-resolution region.
|
evaluate the background density field. Must be less than or equal to
|
||||||
|
half the box size.
|
||||||
nmult : float or int, optional
|
nmult : float or int, optional
|
||||||
Multiple of the sum of pair initial Lagrangian patch sizes
|
Multiplier of the sum of the initial Lagrangian patch sizes of a halo
|
||||||
within which to return neighbours. By default 1.
|
pair. Determines the range within which neighbors are returned.
|
||||||
dlogmass : float, optional
|
dlogmass : float, optional
|
||||||
Tolerance on the absolute logarithmic mass difference of potential
|
Tolerance on the absolute logarithmic mass difference of potential
|
||||||
matches. By default 2.
|
matches.
|
||||||
mass_kind : str, optional
|
mass_kind : str, optional
|
||||||
The mass kind whose similarity is to be checked. Must be a valid
|
Mass kind whose similarity is to be checked. Must be a valid key in the
|
||||||
catalogue key. By default `totpartmass`, i.e. the total particle
|
halo catalogue.
|
||||||
mass associated with a halo.
|
|
||||||
"""
|
"""
|
||||||
_nmult = None
|
_nmult = None
|
||||||
_dlogmass = None
|
_dlogmass = None
|
||||||
|
@ -111,21 +117,19 @@ class RealisationsMatcher(BaseMatcher):
|
||||||
|
|
||||||
def __init__(self, box_size, bckg_halfsize, nmult=1.0, dlogmass=2.0,
|
def __init__(self, box_size, bckg_halfsize, nmult=1.0, dlogmass=2.0,
|
||||||
mass_kind="totpartmass"):
|
mass_kind="totpartmass"):
|
||||||
assert nmult > 0
|
|
||||||
assert dlogmass > 0
|
|
||||||
assert isinstance(mass_kind, str)
|
|
||||||
self.box_size = box_size
|
self.box_size = box_size
|
||||||
self.halfsize = bckg_halfsize
|
self.bckg_halfsize = bckg_halfsize
|
||||||
self._nmult = nmult
|
self.nmult = nmult
|
||||||
self._dlogmass = dlogmass
|
self.dlogmass = dlogmass
|
||||||
self._mass_kind = mass_kind
|
self.mass_kind = mass_kind
|
||||||
|
|
||||||
self._overlapper = ParticleOverlap(box_size, bckg_halfsize)
|
self._overlapper = ParticleOverlap(box_size, bckg_halfsize)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nmult(self):
|
def nmult(self):
|
||||||
"""
|
"""
|
||||||
Multiple of the sum of pair initial Lagrangian patch sizes within which
|
Multiplier of the sum of the initial Lagrangian patch sizes of a halo
|
||||||
to return neighbours.
|
pair. Determines the range within which neighbors are returned.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
@ -133,6 +137,12 @@ class RealisationsMatcher(BaseMatcher):
|
||||||
"""
|
"""
|
||||||
return self._nmult
|
return self._nmult
|
||||||
|
|
||||||
|
@nmult.setter
|
||||||
|
def nmult(self, value):
|
||||||
|
if not (value > 0 and isinstance(value, (int, float))):
|
||||||
|
raise ValueError("`nmult` must be a positive integer or float.")
|
||||||
|
self._nmult = float(value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dlogmass(self):
|
def dlogmass(self):
|
||||||
"""
|
"""
|
||||||
|
@ -145,10 +155,17 @@ class RealisationsMatcher(BaseMatcher):
|
||||||
"""
|
"""
|
||||||
return self._dlogmass
|
return self._dlogmass
|
||||||
|
|
||||||
|
@dlogmass.setter
|
||||||
|
def dlogmass(self, value):
|
||||||
|
if not (value > 0 and isinstance(value, (float, int))):
|
||||||
|
raise ValueError("`dlogmass` must be a positive float.")
|
||||||
|
self._dlogmass = float(value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mass_kind(self):
|
def mass_kind(self):
|
||||||
"""
|
"""
|
||||||
Mass kind whose similarity is to be checked.
|
Mass kind whose similarity is to be checked. Must be a valid key in the
|
||||||
|
halo catalogue.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
@ -156,6 +173,12 @@ class RealisationsMatcher(BaseMatcher):
|
||||||
"""
|
"""
|
||||||
return self._mass_kind
|
return self._mass_kind
|
||||||
|
|
||||||
|
@mass_kind.setter
|
||||||
|
def mass_kind(self, value):
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise ValueError("`mass_kind` must be a string.")
|
||||||
|
self._mass_kind = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def overlapper(self):
|
def overlapper(self):
|
||||||
"""
|
"""
|
||||||
|
@ -172,34 +195,33 @@ class RealisationsMatcher(BaseMatcher):
|
||||||
r"""
|
r"""
|
||||||
Find all neighbours whose CM separation is less than `nmult` times the
|
Find all neighbours whose CM separation is less than `nmult` times the
|
||||||
sum of their initial Lagrangian patch sizes and calculate their
|
sum of their initial Lagrangian patch sizes and calculate their
|
||||||
overlap. Enforces that the neighbours' are similar in mass up to
|
overlap. Enforces that the neighbours are similar in mass up to
|
||||||
`dlogmass` dex.
|
`dlogmass` dex.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
cat0 : :py:class:`csiborgtools.read.CSiBORGHaloCatalogue`
|
cat0 : instance of :py:class:`csiborgtools.read.BaseCatalogue`
|
||||||
Halo catalogue of the reference simulation.
|
Halo catalogue of the reference simulation.
|
||||||
catx : :py:class:`csiborgtools.read.CSiBORGHaloCatalogue`
|
catx : instance of :py:class:`csiborgtools.read.BaseCatalogue`
|
||||||
Halo catalogue of the cross simulation.
|
Halo catalogue of the cross simulation.
|
||||||
particles0 : 2-dimensional array
|
particles0 : 2-dimensional array
|
||||||
Array of particles in box units in the reference simulation.
|
Particles archive file of the reference simulation. The columns
|
||||||
The columns must be `x`, `y`, `z` and `M`.
|
must be `x`, `y`, `z` and `M`.
|
||||||
particlesx : 2-dimensional array
|
particlesx : 2-dimensional array
|
||||||
Array of particles in box units in the cross simulation.
|
Particles archive file of the cross simulation. The columns must be
|
||||||
The columns must be `x`, `y`, `z` and `M`.
|
`x`, `y`, `z` and `M`.
|
||||||
halo_map0 : 2-dimensional array
|
halo_map0 : 2-dimensional array
|
||||||
Halo map of the reference simulation.
|
Halo map of the reference simulation.
|
||||||
halo_mapx : 2-dimensional array
|
halo_mapx : 2-dimensional array
|
||||||
Halo map of the cross simulation.
|
Halo map of the cross simulation.
|
||||||
delta_bckg : 3-dimensional array
|
delta_bckg : 3-dimensional array
|
||||||
Summed background density field of the reference and cross
|
Summed background density field of the reference and cross
|
||||||
simulations calculated with particles assigned to haloes at the
|
simulations calculated with particles assigned to halos at the
|
||||||
final snapshot. Assumed to only be sampled in cells
|
final snapshot. Calculated on a grid determined by `bckg_halfsize`.
|
||||||
:math:`[512, 1536)^3`.
|
|
||||||
cache_size : int, optional
|
cache_size : int, optional
|
||||||
Caching size for loading the cross simulation halos.
|
Caching size for loading the cross simulation halos.
|
||||||
verbose : bool, optional
|
verbose : bool, optional
|
||||||
iterator verbosity flag. by default `true`.
|
Iterator verbosity flag. By default `true`.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
@ -279,21 +301,21 @@ class RealisationsMatcher(BaseMatcher):
|
||||||
halo_mapx, delta_bckg, match_indxs, smooth_kwargs,
|
halo_mapx, delta_bckg, match_indxs, smooth_kwargs,
|
||||||
cache_size=10000, verbose=True):
|
cache_size=10000, verbose=True):
|
||||||
r"""
|
r"""
|
||||||
Calculate the smoothed overlaps for pair previously identified via
|
Calculate the smoothed overlaps for pairs previously identified via
|
||||||
`self.cross(...)` to have a non-zero overlap.
|
`self.cross(...)` to have a non-zero NGP overlap.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
cat0 : :py:class:`csiborgtools.read.CSiBORGHaloCatalogue`
|
cat0 : instance of :py:class:`csiborgtools.read.BaseCatalogue`
|
||||||
Halo catalogue of the reference simulation.
|
Halo catalogue of the reference simulation.
|
||||||
catx : :py:class:`csiborgtools.read.CSiBORGHaloCatalogue`
|
catx : instance of :py:class:`csiborgtools.read.BaseCatalogue`
|
||||||
Halo catalogue of the cross simulation.
|
Halo catalogue of the cross simulation.
|
||||||
particles0 : 2-dimensional array
|
particles0 : 2-dimensional array
|
||||||
Array of particles in box units in the reference simulation.
|
Particles archive file of the reference simulation. The columns
|
||||||
The columns must be `x`, `y`, `z` and `M`.
|
must be `x`, `y`, `z` and `M`.
|
||||||
particlesx : 2-dimensional array
|
particlesx : 2-dimensional array
|
||||||
Array of particles in box units in the cross simulation.
|
Particles archive file of the cross simulation. The columns must be
|
||||||
The columns must be `x`, `y`, `z` and `M`.
|
`x`, `y`, `z` and `M`.
|
||||||
halo_map0 : 2-dimensional array
|
halo_map0 : 2-dimensional array
|
||||||
Halo map of the reference simulation.
|
Halo map of the reference simulation.
|
||||||
halo_mapx : 2-dimensional array
|
halo_mapx : 2-dimensional array
|
||||||
|
@ -301,8 +323,7 @@ class RealisationsMatcher(BaseMatcher):
|
||||||
delta_bckg : 3-dimensional array
|
delta_bckg : 3-dimensional array
|
||||||
Smoothed summed background density field of the reference and cross
|
Smoothed summed background density field of the reference and cross
|
||||||
simulations calculated with particles assigned to halos at the
|
simulations calculated with particles assigned to halos at the
|
||||||
final snapshot. Assumed to only be sampled in cells
|
final snapshot. Calculated on a grid determined by `bckg_halfsize`.
|
||||||
:math:`[512, 1536)^3`.
|
|
||||||
match_indxs : 1-dimensional array of arrays
|
match_indxs : 1-dimensional array of arrays
|
||||||
Indices of halo counterparts in the cross catalogue.
|
Indices of halo counterparts in the cross catalogue.
|
||||||
smooth_kwargs : kwargs
|
smooth_kwargs : kwargs
|
||||||
|
@ -310,7 +331,7 @@ class RealisationsMatcher(BaseMatcher):
|
||||||
cache_size : int, optional
|
cache_size : int, optional
|
||||||
Caching size for loading the cross simulation halos.
|
Caching size for loading the cross simulation halos.
|
||||||
verbose : bool, optional
|
verbose : bool, optional
|
||||||
Iterator verbosity flag. By default `True`.
|
Iterator verbosity flag. By default `true`.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
@ -328,8 +349,8 @@ class RealisationsMatcher(BaseMatcher):
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"{datetime.now()}: calculating smoothed overlaps.",
|
print(f"{datetime.now()}: calculating smoothed overlaps.",
|
||||||
flush=True)
|
flush=True)
|
||||||
indxs = cat0["index"]
|
|
||||||
cross = [numpy.asanyarray([], dtype=numpy.float32)] * match_indxs.size
|
cross = [numpy.asanyarray([], dtype=numpy.float32)] * match_indxs.size
|
||||||
|
indxs = cat0["index"]
|
||||||
for i, k0 in enumerate(tqdm(indxs) if verbose else indxs):
|
for i, k0 in enumerate(tqdm(indxs) if verbose else indxs):
|
||||||
pos0, mass0, __, mins0, maxs0 = load_processed_halo(
|
pos0, mass0, __, mins0, maxs0 = load_processed_halo(
|
||||||
k0, particles0, halo_map0, hid2map0, nshift=nshift,
|
k0, particles0, halo_map0, hid2map0, nshift=nshift,
|
||||||
|
@ -348,41 +369,10 @@ class RealisationsMatcher(BaseMatcher):
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Matching statistics #
|
# Overlap calculator #
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
def cosine_similarity(x, y):
|
|
||||||
r"""
|
|
||||||
Calculate the cosine similarity between two Cartesian vectors. Defined
|
|
||||||
as :math:`\Sum_{i} x_i y_{i} / (|x| * |y|)`.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
x : 1-dimensional array
|
|
||||||
The first vector.
|
|
||||||
y : 1- or 2-dimensional array
|
|
||||||
The second vector. Can be 2-dimensional of shape `(n_samples, 3)`,
|
|
||||||
in which case the calculation is broadcasted.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
out : float or 1-dimensional array
|
|
||||||
The cosine similarity. If y is 1-dimensinal returns only a float.
|
|
||||||
"""
|
|
||||||
# Quick check of dimensions
|
|
||||||
if x.ndim != 1:
|
|
||||||
raise ValueError("`x` must be a 1-dimensional array.")
|
|
||||||
y = y.reshape(-1, 3) if y.ndim == 1 else y
|
|
||||||
|
|
||||||
out = numpy.sum(x * y, axis=1)
|
|
||||||
out /= numpy.linalg.norm(x) * numpy.linalg.norm(y, axis=1)
|
|
||||||
|
|
||||||
if out.size == 1:
|
|
||||||
return out[0]
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
class ParticleOverlap(BaseMatcher):
|
class ParticleOverlap(BaseMatcher):
|
||||||
r"""
|
r"""
|
||||||
Halo overlaps calculator. The density field calculation is based on the
|
Halo overlaps calculator. The density field calculation is based on the
|
||||||
|
@ -394,9 +384,10 @@ class ParticleOverlap(BaseMatcher):
|
||||||
box_size : int
|
box_size : int
|
||||||
Number of cells in the box.
|
Number of cells in the box.
|
||||||
bckg_halfsize : int
|
bckg_halfsize : int
|
||||||
Number of to each side of the centre of the box to calculate the
|
Background half-size for density field calculation. This is the
|
||||||
density field. This is because in CSiBORG we are only interested in the
|
grid distance from the center of the box to each side over which to
|
||||||
high-resolution region.
|
evaluate the background density field. Must be less than or equal to
|
||||||
|
half the box size.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, box_size, bckg_halfsize):
|
def __init__(self, box_size, bckg_halfsize):
|
||||||
|
@ -414,16 +405,16 @@ class ParticleOverlap(BaseMatcher):
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
particles : 2-dimensional array
|
particles : 2-dimensional array
|
||||||
Array of particles.
|
Particles archive file. The columns must be `x`, `y`, `z` and `M`.
|
||||||
halo_map : 2-dimensional array
|
halo_map : 2-dimensional array
|
||||||
Array containing start and end indices in the particle array
|
Array containing start and end indices in the particle array
|
||||||
corresponding to each halo.
|
corresponding to each halo.
|
||||||
hid2map : dict
|
hid2map : dict
|
||||||
Dictionary mapping halo IDs to `halo_map` array positions.
|
Dictionary mapping halo IDs to `halo_map` array positions.
|
||||||
halo_cat: :py:class:`csiborgtools.read.CSiBORGHaloCatalogue`
|
halo_cat : instance of :py:class:`csiborgtools.read.BaseCatalogue`
|
||||||
Halo catalogue.
|
Halo catalogue.
|
||||||
delta : 3-dimensional array, optional
|
delta : 3-dimensional array, optional
|
||||||
Array to store the density field in. If `None` a new array is
|
Array to store the density field. If `None` a new array is
|
||||||
created.
|
created.
|
||||||
verbose : bool, optional
|
verbose : bool, optional
|
||||||
Verbosity flag for loading the halos' particles.
|
Verbosity flag for loading the halos' particles.
|
||||||
|
@ -449,6 +440,7 @@ class ParticleOverlap(BaseMatcher):
|
||||||
|
|
||||||
pos, mass = pos[:, :3], pos[:, 3]
|
pos, mass = pos[:, :3], pos[:, 3]
|
||||||
pos = pos2cell(pos, self.box_size)
|
pos = pos2cell(pos, self.box_size)
|
||||||
|
|
||||||
# We mask out particles outside the cubical high-resolution region
|
# We mask out particles outside the cubical high-resolution region
|
||||||
mask = numpy.all((cellmin <= pos) & (pos < cellmax), axis=1)
|
mask = numpy.all((cellmin <= pos) & (pos < cellmax), axis=1)
|
||||||
pos = pos[mask]
|
pos = pos[mask]
|
||||||
|
@ -465,14 +457,13 @@ class ParticleOverlap(BaseMatcher):
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
pos : 2-dimensional array
|
pos : 2-dimensional array
|
||||||
Halo particle position array.
|
Halo's particles position array.
|
||||||
mass : 1-dimensional array
|
mass : 1-dimensional array
|
||||||
Halo particle mass array.
|
Halo's particles mass array.
|
||||||
mins, maxs : 1-dimensional arrays of shape `(3,)`
|
mins, maxs : 1-dimensional arrays of shape `(3,)`
|
||||||
Minimun and maximum cell numbers along each dimension.
|
Minimun and maximum cell numbers along each dimension.
|
||||||
subbox : bool, optional
|
subbox : bool, optional
|
||||||
Whether to calculate the density field on a grid strictly enclosing
|
Whether to calculate the field on a grid enclosing the halo.
|
||||||
the halo.
|
|
||||||
smooth_kwargs : kwargs, optional
|
smooth_kwargs : kwargs, optional
|
||||||
Kwargs to be passed to :py:func:`scipy.ndimage.gaussian_filter`.
|
Kwargs to be passed to :py:func:`scipy.ndimage.gaussian_filter`.
|
||||||
If `None` no smoothing is applied.
|
If `None` no smoothing is applied.
|
||||||
|
@ -483,25 +474,25 @@ class ParticleOverlap(BaseMatcher):
|
||||||
"""
|
"""
|
||||||
nshift = read_nshift(smooth_kwargs)
|
nshift = read_nshift(smooth_kwargs)
|
||||||
cells = pos2cell(pos, self.box_size)
|
cells = pos2cell(pos, self.box_size)
|
||||||
# Check that minima and maxima are integers
|
|
||||||
if not (mins is None and maxs is None):
|
if not (mins is None and maxs is None):
|
||||||
assert mins.dtype.char in numpy.typecodes["AllInteger"]
|
assert mins.dtype.char in numpy.typecodes["AllInteger"]
|
||||||
assert maxs.dtype.char in numpy.typecodes["AllInteger"]
|
assert maxs.dtype.char in numpy.typecodes["AllInteger"]
|
||||||
|
|
||||||
if subbox:
|
if subbox:
|
||||||
if mins is None or maxs is None:
|
if mins is None or maxs is None:
|
||||||
mins, maxs = get_halolims(cells, self.box_size, nshift)
|
mins, maxs = get_halo_cell_limits(cells, self.box_size, nshift)
|
||||||
|
ncells = maxs - mins + 1
|
||||||
ncells = maxs - mins + 1 # To get the number of cells
|
|
||||||
else:
|
else:
|
||||||
mins = [0, 0, 0]
|
mins = [0, 0, 0]
|
||||||
ncells = (self.box_size, ) * 3
|
ncells = (self.box_size, ) * 3
|
||||||
|
|
||||||
# Preallocate and fill the array
|
|
||||||
delta = numpy.zeros(ncells, dtype=numpy.float32)
|
delta = numpy.zeros(ncells, dtype=numpy.float32)
|
||||||
fill_delta(delta, cells[:, 0], cells[:, 1], cells[:, 2], *mins, mass)
|
fill_delta(delta, cells[:, 0], cells[:, 1], cells[:, 2], *mins, mass)
|
||||||
|
|
||||||
if smooth_kwargs is not None:
|
if smooth_kwargs is not None:
|
||||||
gaussian_filter(delta, output=delta, **smooth_kwargs)
|
gaussian_filter(delta, output=delta, **smooth_kwargs)
|
||||||
|
|
||||||
return delta
|
return delta
|
||||||
|
|
||||||
def make_deltas(self, pos1, pos2, mass1, mass2, mins1=None, maxs1=None,
|
def make_deltas(self, pos1, pos2, mass1, mass2, mins1=None, maxs1=None,
|
||||||
|
@ -543,6 +534,7 @@ class ParticleOverlap(BaseMatcher):
|
||||||
nshift = read_nshift(smooth_kwargs)
|
nshift = read_nshift(smooth_kwargs)
|
||||||
pos1 = pos2cell(pos1, self.box_size)
|
pos1 = pos2cell(pos1, self.box_size)
|
||||||
pos2 = pos2cell(pos2, self.box_size)
|
pos2 = pos2cell(pos2, self.box_size)
|
||||||
|
|
||||||
xc1, yc1, zc1 = [pos1[:, i] for i in range(3)]
|
xc1, yc1, zc1 = [pos1[:, i] for i in range(3)]
|
||||||
xc2, yc2, zc2 = [pos2[:, i] for i in range(3)]
|
xc2, yc2, zc2 = [pos2[:, i] for i in range(3)]
|
||||||
|
|
||||||
|
@ -551,6 +543,7 @@ class ParticleOverlap(BaseMatcher):
|
||||||
xmin = min(numpy.min(xc1), numpy.min(xc2)) - nshift
|
xmin = min(numpy.min(xc1), numpy.min(xc2)) - nshift
|
||||||
ymin = min(numpy.min(yc1), numpy.min(yc2)) - nshift
|
ymin = min(numpy.min(yc1), numpy.min(yc2)) - nshift
|
||||||
zmin = min(numpy.min(zc1), numpy.min(zc2)) - nshift
|
zmin = min(numpy.min(zc1), numpy.min(zc2)) - nshift
|
||||||
|
|
||||||
# Make sure shifting does not go beyond boundaries
|
# Make sure shifting does not go beyond boundaries
|
||||||
xmin, ymin, zmin = [max(px, 0) for px in (xmin, ymin, zmin)]
|
xmin, ymin, zmin = [max(px, 0) for px in (xmin, ymin, zmin)]
|
||||||
|
|
||||||
|
@ -558,6 +551,7 @@ class ParticleOverlap(BaseMatcher):
|
||||||
xmax = max(numpy.max(xc1), numpy.max(xc2)) + nshift
|
xmax = max(numpy.max(xc1), numpy.max(xc2)) + nshift
|
||||||
ymax = max(numpy.max(yc1), numpy.max(yc2)) + nshift
|
ymax = max(numpy.max(yc1), numpy.max(yc2)) + nshift
|
||||||
zmax = max(numpy.max(zc1), numpy.max(zc2)) + nshift
|
zmax = max(numpy.max(zc1), numpy.max(zc2)) + nshift
|
||||||
|
|
||||||
# Make sure shifting does not go beyond boundaries
|
# Make sure shifting does not go beyond boundaries
|
||||||
xmax, ymax, zmax = [min(px, self.box_size - 1)
|
xmax, ymax, zmax = [min(px, self.box_size - 1)
|
||||||
for px in (xmax, ymax, zmax)]
|
for px in (xmax, ymax, zmax)]
|
||||||
|
@ -565,10 +559,9 @@ class ParticleOverlap(BaseMatcher):
|
||||||
xmin, ymin, zmin = [min(mins1[i], mins2[i]) for i in range(3)]
|
xmin, ymin, zmin = [min(mins1[i], mins2[i]) for i in range(3)]
|
||||||
xmax, ymax, zmax = [max(maxs1[i], maxs2[i]) for i in range(3)]
|
xmax, ymax, zmax = [max(maxs1[i], maxs2[i]) for i in range(3)]
|
||||||
|
|
||||||
cellmins = (xmin, ymin, zmin) # Cell minima
|
cellmins = (xmin, ymin, zmin)
|
||||||
ncells = xmax - xmin + 1, ymax - ymin + 1, zmax - zmin + 1 # Num cells
|
ncells = (xmax - xmin + 1, ymax - ymin + 1, zmax - zmin + 1,)
|
||||||
|
|
||||||
# Preallocate and fill the arrays
|
|
||||||
delta1 = numpy.zeros(ncells, dtype=numpy.float32)
|
delta1 = numpy.zeros(ncells, dtype=numpy.float32)
|
||||||
delta2 = numpy.zeros(ncells, dtype=numpy.float32)
|
delta2 = numpy.zeros(ncells, dtype=numpy.float32)
|
||||||
|
|
||||||
|
@ -590,6 +583,7 @@ class ParticleOverlap(BaseMatcher):
|
||||||
if smooth_kwargs is not None:
|
if smooth_kwargs is not None:
|
||||||
gaussian_filter(delta1, output=delta1, **smooth_kwargs)
|
gaussian_filter(delta1, output=delta1, **smooth_kwargs)
|
||||||
gaussian_filter(delta2, output=delta2, **smooth_kwargs)
|
gaussian_filter(delta2, output=delta2, **smooth_kwargs)
|
||||||
|
|
||||||
return delta1, delta2, cellmins, nonzero
|
return delta1, delta2, cellmins, nonzero
|
||||||
|
|
||||||
def __call__(self, pos1, pos2, mass1, mass2, delta_bckg,
|
def __call__(self, pos1, pos2, mass1, mass2, delta_bckg,
|
||||||
|
@ -644,9 +638,10 @@ class ParticleOverlap(BaseMatcher):
|
||||||
if smooth_kwargs is not None:
|
if smooth_kwargs is not None:
|
||||||
return calculate_overlap(delta1, delta2, cellmins, delta_bckg,
|
return calculate_overlap(delta1, delta2, cellmins, delta_bckg,
|
||||||
self.box_size, self.bckg_halfsize)
|
self.box_size, self.bckg_halfsize)
|
||||||
# Calculate masses not given
|
|
||||||
totmass1 = numpy.sum(mass1) if totmass1 is None else totmass1
|
totmass1 = numpy.sum(mass1) if totmass1 is None else totmass1
|
||||||
totmass2 = numpy.sum(mass2) if totmass2 is None else totmass2
|
totmass2 = numpy.sum(mass2) if totmass2 is None else totmass2
|
||||||
|
|
||||||
return calculate_overlap_indxs(delta1, delta2, cellmins, delta_bckg,
|
return calculate_overlap_indxs(delta1, delta2, cellmins, delta_bckg,
|
||||||
nonzero, totmass1, totmass2,
|
nonzero, totmass1, totmass2,
|
||||||
self.box_size, self.bckg_halfsize)
|
self.box_size, self.bckg_halfsize)
|
||||||
|
@ -681,29 +676,26 @@ def pos2cell(pos, ncells):
|
||||||
|
|
||||||
def read_nshift(smooth_kwargs):
|
def read_nshift(smooth_kwargs):
|
||||||
"""
|
"""
|
||||||
Read off the number of cells to pad the density field if smoothing is
|
Determine the number of cells to pad the density field if smoothing is
|
||||||
applied. Defaults to the ceiling of twice of the smoothing scale.
|
applied. It defaults to the ceiling of three times the smoothing scale.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
smooth_kwargs : kwargs, optional
|
smooth_kwargs : dict or None
|
||||||
Kwargs to be passed to :py:func:`scipy.ndimage.gaussian_filter`.
|
Arguments to be passed to :py:func:`scipy.ndimage.gaussian_filter`.
|
||||||
If `None` no smoothing is applied.
|
If `None`, no smoothing is applied.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
nshift : int
|
nshift : int
|
||||||
"""
|
"""
|
||||||
if smooth_kwargs is None:
|
return 0 if smooth_kwargs is None else ceil(3 * smooth_kwargs["sigma"])
|
||||||
return 0
|
|
||||||
else:
|
|
||||||
return ceil(2 * smooth_kwargs["sigma"])
|
|
||||||
|
|
||||||
|
|
||||||
@jit(nopython=True)
|
@jit(nopython=True)
|
||||||
def fill_delta(delta, xcell, ycell, zcell, xmin, ymin, zmin, weights):
|
def fill_delta(delta, xcell, ycell, zcell, xmin, ymin, zmin, weights):
|
||||||
"""
|
"""
|
||||||
Fill array `delta` at the specified indices with their weights. This is a
|
Fill array `delta` by adding `weights` to the specified cells. This is a
|
||||||
JIT implementation.
|
JIT implementation.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
@ -715,20 +707,23 @@ def fill_delta(delta, xcell, ycell, zcell, xmin, ymin, zmin, weights):
|
||||||
xmin, ymin, zmin : ints
|
xmin, ymin, zmin : ints
|
||||||
Minimum cell IDs of particles.
|
Minimum cell IDs of particles.
|
||||||
weights : 1-dimensional arrays
|
weights : 1-dimensional arrays
|
||||||
Particle mass.
|
Weights
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
for n in range(xcell.size):
|
n_particles = xcell.size
|
||||||
delta[xcell[n] - xmin, ycell[n] - ymin, zcell[n] - zmin] += weights[n]
|
|
||||||
|
for n in range(n_particles):
|
||||||
|
i, j, k = xcell[n] - xmin, ycell[n] - ymin, zcell[n] - zmin
|
||||||
|
delta[i, j, k] += weights[n]
|
||||||
|
|
||||||
|
|
||||||
@jit(nopython=True)
|
@jit(nopython=True)
|
||||||
def fill_delta_indxs(delta, xcell, ycell, zcell, xmin, ymin, zmin, weights):
|
def fill_delta_indxs(delta, xcell, ycell, zcell, xmin, ymin, zmin, weights):
|
||||||
"""
|
"""
|
||||||
Fill array `delta` at the specified indices with their weights and return
|
Fill array `delta` by adding `weights` to the specified cells and return
|
||||||
indices where `delta` was assigned a value. This is a JIT implementation.
|
indices where `delta` was assigned a value. This is a JIT implementation.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
@ -740,36 +735,41 @@ def fill_delta_indxs(delta, xcell, ycell, zcell, xmin, ymin, zmin, weights):
|
||||||
xmin, ymin, zmin : ints
|
xmin, ymin, zmin : ints
|
||||||
Minimum cell IDs of particles.
|
Minimum cell IDs of particles.
|
||||||
weights : 1-dimensional arrays
|
weights : 1-dimensional arrays
|
||||||
Particle mass.
|
Weights.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
cells : 1-dimensional array
|
cells : 1-dimensional array
|
||||||
Indices where `delta` was assigned a value.
|
Indices where `delta` was assigned a value.
|
||||||
"""
|
"""
|
||||||
# Array to count non-zero cells
|
n_particles = xcell.size
|
||||||
cells = numpy.full((xcell.size, 3), numpy.nan, numpy.int32)
|
cells = numpy.full((n_particles, 3), numpy.nan, numpy.int32)
|
||||||
count_nonzero = 0
|
count_nonzero = 0
|
||||||
for n in range(xcell.size):
|
|
||||||
|
for n in range(n_particles):
|
||||||
i, j, k = xcell[n] - xmin, ycell[n] - ymin, zcell[n] - zmin
|
i, j, k = xcell[n] - xmin, ycell[n] - ymin, zcell[n] - zmin
|
||||||
# If a cell is zero add it
|
|
||||||
if delta[i, j, k] == 0:
|
if delta[i, j, k] == 0:
|
||||||
cells[count_nonzero, :] = i, j, k
|
cells[count_nonzero] = i, j, k
|
||||||
count_nonzero += 1
|
count_nonzero += 1
|
||||||
|
|
||||||
delta[i, j, k] += weights[n]
|
delta[i, j, k] += weights[n]
|
||||||
|
|
||||||
return cells[:count_nonzero, :] # Cutoff unassigned places
|
return cells[:count_nonzero]
|
||||||
|
|
||||||
|
|
||||||
def get_halolims(pos, ncells, nshift=None):
|
@jit(nopython=True)
|
||||||
|
def get_halo_cell_limits(pos, ncells, nshift=0):
|
||||||
"""
|
"""
|
||||||
Get the lower and upper limit of a halo's positions or cell numbers.
|
Get the lower and upper limit of a halo's cell numbers. Optionally,
|
||||||
|
floating point positions are also supported. However, in this case `nshift`
|
||||||
|
must be 0. Be careful, no error will be raised.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
pos : 2-dimensional array
|
pos : 2-dimensional array
|
||||||
Halo particle array. Columns must be `x`, `y`, `z`.
|
Halo particle array. The first three columns must be the cell numbers
|
||||||
|
corresponding to `x`, `y`, `z`.
|
||||||
ncells : int
|
ncells : int
|
||||||
Number of grid cells of the box along a single dimension.
|
Number of grid cells of the box along a single dimension.
|
||||||
nshift : int, optional
|
nshift : int, optional
|
||||||
|
@ -778,16 +778,12 @@ def get_halolims(pos, ncells, nshift=None):
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
mins, maxs : 1-dimensional arrays of shape `(3, )`
|
mins, maxs : 1-dimensional arrays of shape `(3, )`
|
||||||
Minimum and maximum along each axis.
|
|
||||||
"""
|
"""
|
||||||
# Check that in case of `nshift` we have integer positions.
|
|
||||||
dtype = pos.dtype
|
dtype = pos.dtype
|
||||||
if nshift is not None and dtype.char not in numpy.typecodes["AllInteger"]:
|
|
||||||
raise TypeError("`nshift` supported only positions are cells.")
|
|
||||||
nshift = 0 if nshift is None else nshift # To simplify code below
|
|
||||||
|
|
||||||
mins = numpy.full(3, numpy.nan, dtype=dtype)
|
mins = numpy.full(3, numpy.nan, dtype=dtype)
|
||||||
maxs = numpy.full(3, numpy.nan, dtype=dtype)
|
maxs = numpy.full(3, numpy.nan, dtype=dtype)
|
||||||
|
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
mins[i] = max(numpy.min(pos[:, i]) - nshift, 0)
|
mins[i] = max(numpy.min(pos[:, i]) - nshift, 0)
|
||||||
maxs[i] = min(numpy.max(pos[:, i]) + nshift, ncells - 1)
|
maxs[i] = min(numpy.max(pos[:, i]) + nshift, ncells - 1)
|
||||||
|
@ -810,27 +806,29 @@ def calculate_overlap(delta1, delta2, cellmins, delta_bckg, box_size,
|
||||||
delta2 : 3-dimensional array
|
delta2 : 3-dimensional array
|
||||||
Density field of the second halo.
|
Density field of the second halo.
|
||||||
cellmins : len-3 tuple
|
cellmins : len-3 tuple
|
||||||
Tuple of left-most cell ID in the full box.
|
Tuple of lower cell ID in the full box.
|
||||||
delta_bckg : 3-dimensional array
|
delta_bckg : 3-dimensional array
|
||||||
Summed background density field of the reference and cross simulations
|
Summed background density field of the reference and cross simulations
|
||||||
calculated with particles assigned to halos at the final snapshot.
|
calculated with particles assigned to halos at the final snapshot.
|
||||||
Assumed to only be sampled in cells :math:`[512, 1536)^3`.
|
Calculated on a grid determined by `bckg_halfsize`.
|
||||||
box_size : int
|
box_size : int
|
||||||
Number of cells in the box.
|
Number of cells in the box.
|
||||||
bckg_halfsize : int
|
bckg_halfsize : int
|
||||||
Number of to each side of the centre of the box to calculate the
|
Background half-size for density field calculation. This is the
|
||||||
density field. This is because in CSiBORG we are only interested in the
|
grid distance from the center of the box to each side over which to
|
||||||
high-resolution region.
|
evaluate the background density field. Must be less than or equal to
|
||||||
|
half the box size.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
overlap : float
|
overlap : float
|
||||||
"""
|
"""
|
||||||
totmass = 0.0 # Total mass of halo 1 and halo 2
|
totmass = 0.0
|
||||||
intersect = 0.0 # Weighted intersecting mass
|
intersect = 0.0
|
||||||
i0, j0, k0 = cellmins # Unpack things
|
|
||||||
bckg_size = 2 * bckg_halfsize
|
bckg_size = 2 * bckg_halfsize
|
||||||
bckg_offset = box_size // 2 - bckg_halfsize
|
bckg_offset = box_size // 2 - bckg_halfsize
|
||||||
|
|
||||||
|
i0, j0, k0 = cellmins
|
||||||
imax, jmax, kmax = delta1.shape
|
imax, jmax, kmax = delta1.shape
|
||||||
|
|
||||||
for i in range(imax):
|
for i in range(imax):
|
||||||
|
@ -868,11 +866,11 @@ def calculate_overlap_indxs(delta1, delta2, cellmins, delta_bckg, nonzero,
|
||||||
delta2 : 3-dimensional array
|
delta2 : 3-dimensional array
|
||||||
Density field of the second halo.
|
Density field of the second halo.
|
||||||
cellmins : len-3 tuple
|
cellmins : len-3 tuple
|
||||||
Tuple of left-most cell ID in the full box.
|
Tuple of lower cell ID in the full box.
|
||||||
delta_bckg : 3-dimensional array
|
delta_bckg : 3-dimensional array
|
||||||
Summed background density field of the reference and cross simulations
|
Summed background density field of the reference and cross simulations
|
||||||
calculated with particles assigned to halos at the final snapshot.
|
calculated with particles assigned to halos at the final snapshot.
|
||||||
Assumed to only be sampled in cells :math:`[512, 1536)^3`.
|
Calculated on a grid determined by `bckg_halfsize`.
|
||||||
nonzero : 2-dimensional array of shape `(n_cells, 3)`
|
nonzero : 2-dimensional array of shape `(n_cells, 3)`
|
||||||
Indices of cells that are non-zero of the lower mass halo. Expected to
|
Indices of cells that are non-zero of the lower mass halo. Expected to
|
||||||
be precomputed from `fill_delta_indxs`.
|
be precomputed from `fill_delta_indxs`.
|
||||||
|
@ -882,19 +880,21 @@ def calculate_overlap_indxs(delta1, delta2, cellmins, delta_bckg, nonzero,
|
||||||
box_size : int
|
box_size : int
|
||||||
Number of cells in the box.
|
Number of cells in the box.
|
||||||
bckg_halfsize : int
|
bckg_halfsize : int
|
||||||
Number of to each side of the centre of the box to calculate the
|
Background half-size for density field calculation. This is the
|
||||||
density field. This is because in CSiBORG we are only interested in the
|
grid distance from the center of the box to each side over which to
|
||||||
high-resolution region.
|
evaluate the background density field. Must be less than or equal to
|
||||||
|
half the box size.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
overlap : float
|
overlap : float
|
||||||
"""
|
"""
|
||||||
intersect = 0.0 # Weighted intersecting mass
|
intersect = 0.0
|
||||||
i0, j0, k0 = cellmins # Unpack cell minimas
|
|
||||||
bckg_size = 2 * bckg_halfsize
|
bckg_size = 2 * bckg_halfsize
|
||||||
bckg_offset = box_size // 2 - bckg_halfsize
|
bckg_offset = box_size // 2 - bckg_halfsize
|
||||||
|
|
||||||
|
i0, j0, k0 = cellmins
|
||||||
|
|
||||||
for n in range(nonzero.shape[0]):
|
for n in range(nonzero.shape[0]):
|
||||||
i, j, k = nonzero[n, :]
|
i, j, k = nonzero[n, :]
|
||||||
m1, m2 = delta1[i, j, k], delta2[i, j, k]
|
m1, m2 = delta1[i, j, k], delta2[i, j, k]
|
||||||
|
@ -905,7 +905,7 @@ def calculate_overlap_indxs(delta1, delta2, cellmins, delta_bckg, nonzero,
|
||||||
jj = j0 + j - bckg_offset # background density field.
|
jj = j0 + j - bckg_offset # background density field.
|
||||||
kk = k0 + k - bckg_offset
|
kk = k0 + k - bckg_offset
|
||||||
|
|
||||||
ishighres = 0 <= ii < bckg_size # Is this cell is in the high
|
ishighres = 0 <= ii < bckg_size # Is this cell is in the high
|
||||||
ishighres &= 0 <= jj < bckg_size # resolution region for which the
|
ishighres &= 0 <= jj < bckg_size # resolution region for which the
|
||||||
ishighres &= 0 <= kk < bckg_size # background field is calculated.
|
ishighres &= 0 <= kk < bckg_size # background field is calculated.
|
||||||
|
|
||||||
|
@ -933,9 +933,9 @@ def load_processed_halo(hid, particles, halo_map, hid2map, ncells, nshift):
|
||||||
hid2map : dict
|
hid2map : dict
|
||||||
Dictionary mapping halo IDs to `halo_map` array positions.
|
Dictionary mapping halo IDs to `halo_map` array positions.
|
||||||
ncells : int
|
ncells : int
|
||||||
Number of cells in the original density field. Typically 2048.
|
Number of cells in the box density field.
|
||||||
nshift : int
|
nshift : int
|
||||||
Number of cells to pad the density field.
|
Cell padding for the density field.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
@ -952,29 +952,28 @@ def load_processed_halo(hid, particles, halo_map, hid2map, ncells, nshift):
|
||||||
"""
|
"""
|
||||||
pos = load_halo_particles(hid, particles, halo_map, hid2map)
|
pos = load_halo_particles(hid, particles, halo_map, hid2map)
|
||||||
pos, mass = pos[:, :3], pos[:, 3]
|
pos, mass = pos[:, :3], pos[:, 3]
|
||||||
|
|
||||||
pos = pos2cell(pos, ncells)
|
pos = pos2cell(pos, ncells)
|
||||||
totmass = numpy.sum(mass)
|
mins, maxs = get_halo_cell_limits(pos, ncells=ncells, nshift=nshift)
|
||||||
mins, maxs = get_halolims(pos, ncells=ncells, nshift=nshift)
|
return pos, mass, numpy.sum(mass), mins, maxs
|
||||||
return pos, mass, totmass, mins, maxs
|
|
||||||
|
|
||||||
|
|
||||||
def radius_neighbours(knn, X, radiusX, radiusKNN, nmult=1.0,
|
def radius_neighbours(knn, X, radiusX, radiusKNN, nmult=1.0,
|
||||||
enforce_int32=False, verbose=True):
|
enforce_int32=False, verbose=True):
|
||||||
"""
|
"""
|
||||||
Find all neigbours of a trained KNN model whose center of mass separation
|
Find all neigbours of a fitted kNN model whose center of mass separation
|
||||||
is less than `nmult` times the sum of their respective radii.
|
is less than `nmult` times the sum of their respective radii.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
knn : :py:class:`sklearn.neighbors.NearestNeighbors`
|
knn : :py:class:`sklearn.neighbors.NearestNeighbors`
|
||||||
Fitted nearest neighbour search.
|
Fitted nearest neighbour search.
|
||||||
X : 2-dimensional array
|
X : 2-dimensional array of shape `(n_samples, 3)`
|
||||||
Array of shape `(n_samples, 3)`, where the latter axis represents
|
Array of halo positions from the cross simulation.
|
||||||
`x`, `y` and `z`.
|
|
||||||
radiusX: 1-dimensional array of shape `(n_samples, )`
|
radiusX: 1-dimensional array of shape `(n_samples, )`
|
||||||
Patch radii corresponding to haloes in `X`.
|
Lagrangian patch radii corresponding to haloes in `X`.
|
||||||
radiusKNN : 1-dimensional array
|
radiusKNN : 1-dimensional array
|
||||||
Patch radii corresponding to haloes used to train `knn`.
|
Lagrangian patch radii corresponding to haloes used to train the kNN.
|
||||||
nmult : float, optional
|
nmult : float, optional
|
||||||
Multiple of the sum of two radii below which to consider a match.
|
Multiple of the sum of two radii below which to consider a match.
|
||||||
enforce_int32 : bool, optional
|
enforce_int32 : bool, optional
|
||||||
|
@ -988,22 +987,24 @@ def radius_neighbours(knn, X, radiusX, radiusKNN, nmult=1.0,
|
||||||
indxs : 1-dimensional array `(n_samples,)` of arrays
|
indxs : 1-dimensional array `(n_samples,)` of arrays
|
||||||
Matches to `X` from `knn`.
|
Matches to `X` from `knn`.
|
||||||
"""
|
"""
|
||||||
assert X.ndim == 2 and X.shape[1] == 3 # shape of X ok?
|
if X.shape != (radiusX.size, 3):
|
||||||
assert X.shape[0] == radiusX.size # patchX matches X?
|
raise ValueError("Mismatch in shape of `X` or `radiusX`")
|
||||||
assert radiusKNN.size == knn.n_samples_fit_ # patchknn matches the knn?
|
if radiusKNN.size != knn.n_samples_fit_:
|
||||||
|
raise ValueError("Mismatch in shape of `radiusKNN` or `knn`")
|
||||||
|
|
||||||
nsamples = X.shape[0]
|
nsamples = len(X)
|
||||||
indxs = [None] * nsamples
|
indxs = [None] * nsamples
|
||||||
patchknn_max = numpy.max(radiusKNN) # Maximum for completeness
|
patchknn_max = numpy.max(radiusKNN)
|
||||||
|
|
||||||
for i in trange(nsamples) if verbose else range(nsamples):
|
for i in trange(nsamples) if verbose else range(nsamples):
|
||||||
dist, indx = knn.radius_neighbors(
|
dist, indx = knn.radius_neighbors(
|
||||||
X[i, :].reshape(-1, 3), radiusX[i] + patchknn_max,
|
X[i].reshape(1, -1), radiusX[i] + patchknn_max,
|
||||||
sort_results=True)
|
sort_results=True)
|
||||||
# Note that `dist` and `indx` are wrapped in 1-element arrays
|
# Note that `dist` and `indx` are wrapped in 1-element arrays
|
||||||
# so we take the first item where appropriate
|
# so we take the first item where appropriate
|
||||||
mask = (dist[0] / (radiusX[i] + radiusKNN[indx[0]])) < nmult
|
mask = (dist[0] / (radiusX[i] + radiusKNN[indx[0]])) < nmult
|
||||||
indxs[i] = indx[0][mask]
|
indxs[i] = indx[0][mask]
|
||||||
|
|
||||||
if enforce_int32:
|
if enforce_int32:
|
||||||
indxs[i] = indxs[i].astype(numpy.int32)
|
indxs[i] = indxs[i].astype(numpy.int32)
|
||||||
|
|
||||||
|
@ -1048,3 +1049,32 @@ def find_neighbour(nsim0, cats):
|
||||||
cross_hindxs[:, i] = catx["index"][numpy.ravel(ind)]
|
cross_hindxs[:, i] = catx["index"][numpy.ravel(ind)]
|
||||||
|
|
||||||
return dists, cross_hindxs
|
return dists, cross_hindxs
|
||||||
|
|
||||||
|
|
||||||
|
def cosine_similarity(x, y):
|
||||||
|
r"""
|
||||||
|
Calculate the cosine similarity between two Cartesian vectors. Defined
|
||||||
|
as :math:`\Sum_{i} x_i y_{i} / (|x| * |y|)`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
x : 1-dimensional array
|
||||||
|
The first vector.
|
||||||
|
y : 1- or 2-dimensional array
|
||||||
|
The second vector. Can be 2-dimensional of shape `(n_samples, 3)`,
|
||||||
|
in which case the calculation is broadcasted.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
out : float or 1-dimensional array
|
||||||
|
"""
|
||||||
|
if x.ndim != 1:
|
||||||
|
raise ValueError("`x` must be a 1-dimensional array.")
|
||||||
|
|
||||||
|
if y.ndim == 1:
|
||||||
|
y = y.reshape(1, -1)
|
||||||
|
|
||||||
|
out = numpy.sum(x * y, axis=1)
|
||||||
|
out /= numpy.linalg.norm(x) * numpy.linalg.norm(y, axis=1)
|
||||||
|
|
||||||
|
return out[0] if out.size == 1 else out
|
||||||
|
|
|
@ -263,8 +263,10 @@ class QuijoteBox(BaseBox):
|
||||||
----------
|
----------
|
||||||
nsnap : int
|
nsnap : int
|
||||||
Snapshot number.
|
Snapshot number.
|
||||||
**kwargs : dict
|
nsim : int
|
||||||
Empty keyword arguments. For backwards compatibility.
|
IC realisation index.
|
||||||
|
paths : py:class`csiborgtools.read.Paths`
|
||||||
|
Paths manager
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, nsnap, nsim, paths):
|
def __init__(self, nsnap, nsim, paths):
|
||||||
|
|
|
@ -58,7 +58,8 @@ class BaseCatalogue(ABC):
|
||||||
|
|
||||||
@nsim.setter
|
@nsim.setter
|
||||||
def nsim(self, nsim):
|
def nsim(self, nsim):
|
||||||
assert isinstance(nsim, int)
|
if not isinstance(nsim, (int, numpy.integer)):
|
||||||
|
raise TypeError("`nsim` must be an integer!")
|
||||||
self._nsim = nsim
|
self._nsim = nsim
|
||||||
|
|
||||||
@abstractproperty
|
@abstractproperty
|
||||||
|
@ -614,9 +615,9 @@ class QuijoteHaloCatalogue(BaseCatalogue):
|
||||||
SFR=False, read_IDs=False)
|
SFR=False, read_IDs=False)
|
||||||
|
|
||||||
cols = [("x", numpy.float32), ("y", numpy.float32),
|
cols = [("x", numpy.float32), ("y", numpy.float32),
|
||||||
("z", numpy.float32), ("vx", numpy.float32),
|
("z", numpy.float32), ("fof_vx", numpy.float32),
|
||||||
("vy", numpy.float32), ("vz", numpy.float32),
|
("fof_vy", numpy.float32), ("fof_vz", numpy.float32),
|
||||||
("group_mass", numpy.float32), ("npart", numpy.int32),
|
("group_mass", numpy.float32), ("fof_npart", numpy.int32),
|
||||||
("index", numpy.int32)]
|
("index", numpy.int32)]
|
||||||
data = cols_to_structured(fof.GroupLen.size, cols)
|
data = cols_to_structured(fof.GroupLen.size, cols)
|
||||||
|
|
||||||
|
@ -624,9 +625,9 @@ class QuijoteHaloCatalogue(BaseCatalogue):
|
||||||
vel = fof.GroupVel * (1 + self.redshift)
|
vel = fof.GroupVel * (1 + self.redshift)
|
||||||
for i, p in enumerate(["x", "y", "z"]):
|
for i, p in enumerate(["x", "y", "z"]):
|
||||||
data[p] = pos[:, i]
|
data[p] = pos[:, i]
|
||||||
data["v" + p] = vel[:, i]
|
data["fof_v" + p] = vel[:, i]
|
||||||
data["group_mass"] = fof.GroupMass * 1e10
|
data["group_mass"] = fof.GroupMass * 1e10
|
||||||
data["npart"] = fof.GroupLen
|
data["fof_npart"] = fof.GroupLen
|
||||||
# We want to start indexing from 1. Index 0 is reserved for
|
# We want to start indexing from 1. Index 0 is reserved for
|
||||||
# particles unassigned to any FoF group.
|
# particles unassigned to any FoF group.
|
||||||
data["index"] = 1 + numpy.arange(data.size, dtype=numpy.int32)
|
data["index"] = 1 + numpy.arange(data.size, dtype=numpy.int32)
|
||||||
|
@ -634,7 +635,7 @@ class QuijoteHaloCatalogue(BaseCatalogue):
|
||||||
if load_initial:
|
if load_initial:
|
||||||
data = self.load_initial(data, paths, "quijote")
|
data = self.load_initial(data, paths, "quijote")
|
||||||
if load_fitted:
|
if load_fitted:
|
||||||
assert nsnap == 4
|
data = self.load_fitted(data, paths, "quijote")
|
||||||
|
|
||||||
if load_initial and with_lagpatch:
|
if load_initial and with_lagpatch:
|
||||||
data = data[numpy.isfinite(data["lagpatch_size"])]
|
data = data[numpy.isfinite(data["lagpatch_size"])]
|
||||||
|
|
|
@ -366,6 +366,7 @@ class Paths:
|
||||||
snapshots : 1-dimensional array
|
snapshots : 1-dimensional array
|
||||||
"""
|
"""
|
||||||
simpath = self.snapshots(nsim, simname, tonew=False)
|
simpath = self.snapshots(nsim, simname, tonew=False)
|
||||||
|
|
||||||
if simname == "csiborg":
|
if simname == "csiborg":
|
||||||
# Get all files in simpath that start with output_
|
# Get all files in simpath that start with output_
|
||||||
snaps = glob(join(simpath, "output_*"))
|
snaps = glob(join(simpath, "output_*"))
|
||||||
|
@ -456,6 +457,8 @@ class Paths:
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown simulation name `{simname}`.")
|
raise ValueError(f"Unknown simulation name `{simname}`.")
|
||||||
|
|
||||||
|
try_create_directory(fdir)
|
||||||
|
|
||||||
fname = f"out_{str(nsim).zfill(5)}_{str(nsnap).zfill(5)}.npy"
|
fname = f"out_{str(nsim).zfill(5)}_{str(nsnap).zfill(5)}.npy"
|
||||||
return join(fdir, fname)
|
return join(fdir, fname)
|
||||||
|
|
||||||
|
@ -477,6 +480,7 @@ class Paths:
|
||||||
path : str
|
path : str
|
||||||
"""
|
"""
|
||||||
fdir = join(self.postdir, "overlap")
|
fdir = join(self.postdir, "overlap")
|
||||||
|
|
||||||
try_create_directory(fdir)
|
try_create_directory(fdir)
|
||||||
|
|
||||||
fname = f"overlap_{str(nsim0).zfill(5)}_{str(nsimx).zfill(5)}.npz"
|
fname = f"overlap_{str(nsim0).zfill(5)}_{str(nsimx).zfill(5)}.npz"
|
||||||
|
@ -508,9 +512,10 @@ class Paths:
|
||||||
-------
|
-------
|
||||||
path : str
|
path : str
|
||||||
"""
|
"""
|
||||||
fdir = join(self.postdir, "environment")
|
|
||||||
assert kind in ["density", "velocity", "potential", "radvel",
|
assert kind in ["density", "velocity", "potential", "radvel",
|
||||||
"environment"]
|
"environment"]
|
||||||
|
fdir = join(self.postdir, "environment")
|
||||||
|
|
||||||
try_create_directory(fdir)
|
try_create_directory(fdir)
|
||||||
|
|
||||||
if in_rsp:
|
if in_rsp:
|
||||||
|
|
|
@ -37,7 +37,8 @@ except ModuleNotFoundError:
|
||||||
|
|
||||||
def fit_halo(particles, box):
|
def fit_halo(particles, box):
|
||||||
"""
|
"""
|
||||||
Fit a single halo from the particle array.
|
Fit a single halo from the particle array. Only halos with more than 100
|
||||||
|
particles are fitted.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
@ -59,12 +60,17 @@ def fit_halo(particles, box):
|
||||||
for i, v in enumerate(["vx", "vy", "vz"]):
|
for i, v in enumerate(["vx", "vy", "vz"]):
|
||||||
out[v] = numpy.average(halo.vel[:, i], weights=halo["M"])
|
out[v] = numpy.average(halo.vel[:, i], weights=halo["M"])
|
||||||
|
|
||||||
m200c, r200c, cm = halo.spherical_overdensity_mass(200, kind="crit",
|
if out["npart"] < 100:
|
||||||
maxiter=100)
|
return out
|
||||||
|
|
||||||
|
cm, dist = halo.center_of_mass()
|
||||||
|
m200c, r200c = halo.spherical_overdensity_mass(dist, 200)
|
||||||
|
angmom = halo.angular_momentum(dist, cm, r200c)
|
||||||
|
|
||||||
out["m200c"] = m200c
|
out["m200c"] = m200c
|
||||||
out["r200c"] = r200c
|
out["r200c"] = r200c
|
||||||
out["lambda200c"] = halo.lambda_bullock(cm, r200c)
|
out["lambda200c"] = halo.lambda_bullock(angmom, m200c, r200c)
|
||||||
out["conc"] = halo.nfw_concentration(cm, r200c)
|
out["conc"] = halo.nfw_concentration(dist, r200c)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,9 +87,6 @@ def _main(nsim, simname, verbose):
|
||||||
verbose : bool
|
verbose : bool
|
||||||
Verbosity flag.
|
Verbosity flag.
|
||||||
"""
|
"""
|
||||||
# if simname == "quijote":
|
|
||||||
# raise NotImplementedError("Quijote not implemented yet.")
|
|
||||||
|
|
||||||
cols = [("index", numpy.int32),
|
cols = [("index", numpy.int32),
|
||||||
("npart", numpy.int32),
|
("npart", numpy.int32),
|
||||||
("totpartmass", numpy.float32),
|
("totpartmass", numpy.float32),
|
||||||
|
@ -116,7 +119,6 @@ def _main(nsim, simname, verbose):
|
||||||
for i in trange(len(cat)) if verbose else range(len(cat)):
|
for i in trange(len(cat)) if verbose else range(len(cat)):
|
||||||
hid = cat["index"][i]
|
hid = cat["index"][i]
|
||||||
out["index"][i] = hid
|
out["index"][i] = hid
|
||||||
# print("i = ", i)
|
|
||||||
part = csiborgtools.read.load_halo_particles(hid, particles, halo_map,
|
part = csiborgtools.read.load_halo_particles(hid, particles, halo_map,
|
||||||
hid2map)
|
hid2map)
|
||||||
# Skip if no particles.
|
# Skip if no particles.
|
||||||
|
@ -125,7 +127,7 @@ def _main(nsim, simname, verbose):
|
||||||
|
|
||||||
_out = fit_halo(part, box)
|
_out = fit_halo(part, box)
|
||||||
for key in _out.keys():
|
for key in _out.keys():
|
||||||
out[key][i] = _out[key]
|
out[key][i] = _out.get(key, numpy.nan)
|
||||||
|
|
||||||
fout = paths.structfit(nsnap, nsim, simname)
|
fout = paths.structfit(nsnap, nsim, simname)
|
||||||
if verbose:
|
if verbose:
|
||||||
|
|
|
@ -66,7 +66,7 @@ def _main(nsim, simname, verbose):
|
||||||
|
|
||||||
if simname == "csiborg":
|
if simname == "csiborg":
|
||||||
cat = csiborgtools.read.CSiBORGHaloCatalogue(
|
cat = csiborgtools.read.CSiBORGHaloCatalogue(
|
||||||
nsim, paths, rawdata=True, load_fitted=False, load_initial=False)
|
nsim, paths, bounds=None, load_fitted=False, load_initial=False)
|
||||||
else:
|
else:
|
||||||
cat = csiborgtools.read.QuijoteHaloCatalogue(
|
cat = csiborgtools.read.QuijoteHaloCatalogue(
|
||||||
nsim, paths, nsnap=4, load_fitted=False, load_initial=False)
|
nsim, paths, nsnap=4, load_fitted=False, load_initial=False)
|
||||||
|
|
|
@ -11,10 +11,7 @@
|
||||||
# You should have received a copy of the GNU General Public License along
|
# You should have received a copy of the GNU General Public License along
|
||||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
"""
|
"""A script to match all IC pairs of a simulation."""
|
||||||
Script to match all pairs of CSiBORG simulations. Mathches main haloes whose
|
|
||||||
mass is above 1e12 solar masses.
|
|
||||||
"""
|
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
from itertools import combinations
|
from itertools import combinations
|
||||||
|
@ -34,10 +31,15 @@ except ModuleNotFoundError:
|
||||||
import csiborgtools
|
import csiborgtools
|
||||||
|
|
||||||
|
|
||||||
def get_combs():
|
def get_combs(simname):
|
||||||
"""
|
"""
|
||||||
Get the list of all pairs of simulations, then permute them with a known
|
Get the list of all pairs of IC indices and permute them with a fixed
|
||||||
seed to minimise loading the same files simultaneously.
|
seed.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
simname : str
|
||||||
|
Simulation name.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
@ -45,38 +47,49 @@ def get_combs():
|
||||||
List of pairs of simulations.
|
List of pairs of simulations.
|
||||||
"""
|
"""
|
||||||
paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
|
paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
|
||||||
ics = paths.get_ics("csiborg")
|
combs = list(combinations(paths.get_ics(simname), 2))
|
||||||
combs = list(combinations(ics, 2))
|
|
||||||
Random(42).shuffle(combs)
|
Random(42).shuffle(combs)
|
||||||
return combs
|
return combs
|
||||||
|
|
||||||
|
|
||||||
def do_work(comb):
|
def main(comb, simname, sigma, verbose):
|
||||||
"""
|
"""
|
||||||
Match a pair of simulations.
|
Match a pair of simulations.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
comb : tuple
|
comb : tuple
|
||||||
Pair of simulations.
|
Pair of simulation IC indices.
|
||||||
|
simname : str
|
||||||
|
Simulation name.
|
||||||
|
sigma : float
|
||||||
|
Smoothing scale in number of grid cells.
|
||||||
|
verbose : bool
|
||||||
|
Verbosity flag.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
nsim0, nsimx = comb
|
nsim0, nsimx = comb
|
||||||
pair_match(nsim0, nsimx, args.sigma, args.smoothen, args.verbose)
|
pair_match(nsim0, nsimx, simname, sigma, verbose)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = ArgumentParser()
|
parser = ArgumentParser()
|
||||||
parser.add_argument("--sigma", type=float, default=None)
|
parser.add_argument("--simname", type=str, help="Simulation name.",
|
||||||
parser.add_argument("--smoothen", type=lambda x: bool(strtobool(x)),
|
choices=["csiborg", "quijote"])
|
||||||
default=None)
|
parser.add_argument("--sigma", type=float, default=0,
|
||||||
|
help="Smoothing scale in number of grid cells.")
|
||||||
parser.add_argument("--verbose", type=lambda x: bool(strtobool(x)),
|
parser.add_argument("--verbose", type=lambda x: bool(strtobool(x)),
|
||||||
default=False)
|
default=False, help="Verbosity flag.")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
comm = MPI.COMM_WORLD
|
|
||||||
|
|
||||||
combs = get_combs()
|
combs = get_combs()
|
||||||
work_delegation(do_work, combs, comm, master_verbose=True)
|
|
||||||
|
def _main(comb):
|
||||||
|
main(comb, args.simname, args.sigma, args.verbose)
|
||||||
|
|
||||||
|
work_delegation(_main, combs, MPI.COMM_WORLD)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,13 @@
|
||||||
# You should have received a copy of the GNU General Public License along
|
# You should have received a copy of the GNU General Public License along
|
||||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
"""A script to calculate overlap between two CSiBORG realisations."""
|
"""
|
||||||
|
A script to calculate overlap between two IC realisations of the same
|
||||||
|
simulation. The matching is performed for haloes whose total particles mass is
|
||||||
|
- CSiBORG: > 1e13 Msun/h,
|
||||||
|
- Quijote: > 1e14 Msun/h,
|
||||||
|
since Quijote has much lower resolution than CSiBORG.
|
||||||
|
"""
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -29,95 +35,123 @@ except ModuleNotFoundError:
|
||||||
import csiborgtools
|
import csiborgtools
|
||||||
|
|
||||||
|
|
||||||
def pair_match(nsim0, nsimx, sigma, smoothen, verbose):
|
def pair_match(nsim0, nsimx, simname, sigma, verbose):
|
||||||
# TODO fix this.
|
"""
|
||||||
simname = "csiborg"
|
Calculate overlaps between two simulations.
|
||||||
overlapper_kwargs = {"box_size": 512, "bckg_halfsize": 475}
|
|
||||||
from csiborgtools.read import CSiBORGHaloCatalogue, read_h5
|
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
nsim0 : int
|
||||||
|
The reference simulation IC index.
|
||||||
|
nsimx : int
|
||||||
|
The cross simulation IC index.
|
||||||
|
simname : str
|
||||||
|
Simulation name.
|
||||||
|
sigma : float
|
||||||
|
Smoothing scale in number of grid cells.
|
||||||
|
verbose : bool
|
||||||
|
Verbosity flag.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
None
|
||||||
|
"""
|
||||||
paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
|
paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
|
||||||
smooth_kwargs = {"sigma": sigma, "mode": "constant", "cval": 0.0}
|
smooth_kwargs = {"sigma": sigma, "mode": "wrap"}
|
||||||
overlapper = csiborgtools.match.ParticleOverlap(**overlapper_kwargs)
|
|
||||||
matcher = csiborgtools.match.RealisationsMatcher(**overlapper_kwargs)
|
|
||||||
|
|
||||||
# Load the raw catalogues (i.e. no selection) including the initial CM
|
if simname == "csiborg":
|
||||||
# positions and the particle archives.
|
overlapper_kwargs = {"box_size": 2048, "bckg_halfsize": 475}
|
||||||
bounds = {"totpartmass": (1e12, None)}
|
mass_kind = "fof_totpartmass"
|
||||||
cat0 = CSiBORGHaloCatalogue(nsim0, paths, load_initial=True, bounds=bounds,
|
bounds = {mass_kind: (1e13, None)}
|
||||||
with_lagpatch=True, load_clumps_cat=True)
|
cat0 = csiborgtools.read.CSiBORGHaloCatalogue(
|
||||||
catx = CSiBORGHaloCatalogue(nsimx, paths, load_initial=True, bounds=bounds,
|
nsim0, paths, bounds=bounds, load_fitted=False,
|
||||||
with_lagpatch=True, load_clumps_cat=True)
|
with_lagpatch=True)
|
||||||
|
catx = csiborgtools.read.CSiBORGHaloCatalogue(
|
||||||
|
nsimx, paths, bounds=bounds, load_fitted=False,
|
||||||
|
with_lagpatch=True)
|
||||||
|
elif simname == "quijote":
|
||||||
|
overlapper_kwargs = {"box_size": 512, "bckg_halfsize": 256}
|
||||||
|
mass_kind = "group_mass"
|
||||||
|
bounds = {mass_kind: (1e14, None)}
|
||||||
|
cat0 = csiborgtools.read.QuijoteHaloCatalogue(
|
||||||
|
nsim0, paths, 4, load_fitted=False, with_lagpatch=True)
|
||||||
|
catx = csiborgtools.read.QuijoteHaloCatalogue(
|
||||||
|
nsimx, paths, 4, load_fitted=False, with_lagpatch=True)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown simulation name: `{simname}`.")
|
||||||
|
|
||||||
clumpmap0 = read_h5(paths.particles(nsim0, simname))["clumpmap"]
|
halomap0 = csiborgtools.read.read_h5(
|
||||||
parts0 = read_h5(paths.initmatch(nsim0, simname, "particles"))["particles"]
|
paths.particles(nsim0, simname))["halomap"]
|
||||||
clid2map0 = {clid: i for i, clid in enumerate(clumpmap0[:, 0])}
|
parts0 = csiborgtools.read.read_h5(
|
||||||
|
paths.initmatch(nsim0, simname, "particles"))["particles"]
|
||||||
|
hid2map0 = {hid: i for i, hid in enumerate(halomap0[:, 0])}
|
||||||
|
|
||||||
clumpmapx = read_h5(paths.particles(nsimx, simname))["clumpmap"]
|
halomapx = csiborgtools.read.read_h5(
|
||||||
partsx = read_h5(paths.initmatch(nsimx, simname, "particles"))["particles"]
|
paths.particles(nsimx, simname))["halomap"]
|
||||||
clid2mapx = {clid: i for i, clid in enumerate(clumpmapx[:, 0])}
|
partsx = csiborgtools.read.read_h5(
|
||||||
|
paths.initmatch(nsimx, simname, "particles"))["particles"]
|
||||||
|
hid2mapx = {hid: i for i, hid in enumerate(halomapx[:, 0])}
|
||||||
|
|
||||||
# We generate the background density fields. Loads halos's particles one by
|
|
||||||
# one from the archive, concatenates them and calculates the NGP density
|
|
||||||
# field.
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"{datetime.now()}: generating the background density fields.",
|
print(f"{datetime.now()}: calculating the background density fields.",
|
||||||
flush=True)
|
flush=True)
|
||||||
delta_bckg = overlapper.make_bckg_delta(parts0, clumpmap0, clid2map0, cat0,
|
overlapper = csiborgtools.match.ParticleOverlap(**overlapper_kwargs)
|
||||||
|
delta_bckg = overlapper.make_bckg_delta(parts0, halomap0, hid2map0, cat0,
|
||||||
verbose=verbose)
|
verbose=verbose)
|
||||||
delta_bckg = overlapper.make_bckg_delta(partsx, clumpmapx, clid2mapx, catx,
|
delta_bckg = overlapper.make_bckg_delta(partsx, halomapx, hid2mapx, catx,
|
||||||
delta=delta_bckg, verbose=verbose)
|
delta=delta_bckg, verbose=verbose)
|
||||||
|
|
||||||
# We calculate the overlap between the NGP fields.
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"{datetime.now()}: crossing the simulations.", flush=True)
|
print(f"{datetime.now()}: NGP crossing the simulations.", flush=True)
|
||||||
|
matcher = csiborgtools.match.RealisationsMatcher(
|
||||||
|
mass_kind=mass_kind, **overlapper_kwargs)
|
||||||
match_indxs, ngp_overlap = matcher.cross(cat0, catx, parts0, partsx,
|
match_indxs, ngp_overlap = matcher.cross(cat0, catx, parts0, partsx,
|
||||||
clumpmap0, clumpmapx, delta_bckg,
|
halomap0, halomapx, delta_bckg,
|
||||||
verbose=verbose)
|
verbose=verbose)
|
||||||
# We wish to store the halo IDs of the matches, not their array positions
|
|
||||||
# in the catalogues
|
# We want to store the halo IDs of the matches, not their array positions
|
||||||
|
# in the catalogues.
|
||||||
match_hids = deepcopy(match_indxs)
|
match_hids = deepcopy(match_indxs)
|
||||||
for i, matches in enumerate(match_indxs):
|
for i, matches in enumerate(match_indxs):
|
||||||
for j, match in enumerate(matches):
|
for j, match in enumerate(matches):
|
||||||
match_hids[i][j] = catx["index"][match]
|
match_hids[i][j] = catx["index"][match]
|
||||||
|
|
||||||
fout = paths.overlap(nsim0, nsimx, smoothed=False)
|
fout = paths.overlap(nsim0, nsimx, smoothed=False)
|
||||||
|
if verbose:
|
||||||
|
print(f"{datetime.now()}: saving to ... `{fout}`.", flush=True)
|
||||||
numpy.savez(fout, ref_hids=cat0["index"], match_hids=match_hids,
|
numpy.savez(fout, ref_hids=cat0["index"], match_hids=match_hids,
|
||||||
ngp_overlap=ngp_overlap)
|
ngp_overlap=ngp_overlap)
|
||||||
if verbose:
|
|
||||||
print(f"{datetime.now()}: calculated NGP overlap, saved to {fout}.",
|
|
||||||
flush=True)
|
|
||||||
|
|
||||||
if not smoothen:
|
if not sigma > 0:
|
||||||
quit()
|
return
|
||||||
|
|
||||||
# We now smoothen up the background density field for the smoothed overlap
|
|
||||||
# calculation.
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"{datetime.now()}: smoothing the background field.", flush=True)
|
print(f"{datetime.now()}: smoothing the background field.", flush=True)
|
||||||
gaussian_filter(delta_bckg, output=delta_bckg, **smooth_kwargs)
|
gaussian_filter(delta_bckg, output=delta_bckg, **smooth_kwargs)
|
||||||
|
|
||||||
# We calculate the smoothed overlap for the pairs whose NGP overlap is > 0.
|
# We calculate the smoothed overlap for the pairs whose NGP overlap is > 0.
|
||||||
smoothed_overlap = matcher.smoothed_cross(cat0, catx, parts0, partsx,
|
smoothed_overlap = matcher.smoothed_cross(cat0, catx, parts0, partsx,
|
||||||
clumpmap0, clumpmapx, delta_bckg,
|
halomap0, halomapx, delta_bckg,
|
||||||
match_indxs, smooth_kwargs,
|
match_indxs, smooth_kwargs,
|
||||||
verbose=verbose)
|
verbose=verbose)
|
||||||
|
|
||||||
fout = paths.overlap(nsim0, nsimx, smoothed=True)
|
fout = paths.overlap(nsim0, nsimx, smoothed=True)
|
||||||
numpy.savez(fout, smoothed_overlap=smoothed_overlap, sigma=sigma)
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"{datetime.now()}: calculated smoothing, saved to {fout}.",
|
print(f"{datetime.now()}: saving to ... `{fout}`.", flush=True)
|
||||||
flush=True)
|
numpy.savez(fout, smoothed_overlap=smoothed_overlap, sigma=sigma)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = ArgumentParser()
|
parser = ArgumentParser()
|
||||||
parser.add_argument("--nsim0", type=int)
|
parser.add_argument("--nsim0", type=int,
|
||||||
parser.add_argument("--nsimx", type=int)
|
help="Reference simulation IC index.")
|
||||||
parser.add_argument("--sigma", type=float, default=None)
|
parser.add_argument("--nsimx", type=int,
|
||||||
parser.add_argument("--smoothen", type=lambda x: bool(strtobool(x)),
|
help="Cross simulation IC index.")
|
||||||
default=None)
|
parser.add_argument("--simname", type=str, help="Simulation name.")
|
||||||
|
parser.add_argument("--sigma", type=float, default=0,
|
||||||
|
help="Smoothing scale in number of grid cells.")
|
||||||
parser.add_argument("--verbose", type=lambda x: bool(strtobool(x)),
|
parser.add_argument("--verbose", type=lambda x: bool(strtobool(x)),
|
||||||
default=False)
|
default=False, help="Verbosity flag.")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
pair_match(args.nsim0, args.nsimx, args.sigma, args.smoothen, args.verbose)
|
pair_match(args.nsim0, args.nsimx, args.simname, args.sigma, args.verbose)
|
||||||
|
|
Loading…
Reference in a new issue