Add pynbody and other support (#92)

* Simplify box units

* Move old scripts

* Add printing

* Update readers

* Disable boundscheck

* Add new ordering

* Clean up imports

* Enforce dtype and add mass to quijote

* Simplify print statements

* Fix little typos

* Fix key bug

* Bug fixing

* Delete boring comments

* Improve ultimate clumps for PHEW

* Delete boring comments

* Add basic reading

* Remove 0th index HID

* Add flipping of X and Z

* Updates to halo catalogues

* Add ordered caching

* Fix flipping

* Add new flags

* Fix PHEW empty clumps

* Stop over-wrriting

* Little improvements to angular neighbours

* Add catalogue masking

* Change if-else statements

* Cache only filtered data

* Add PHEW cats

* Add comments

* Sort imports

* Get Quijote workign

* Docs

* Add HMF calculation

* Move to old

* Fix angular

* Add great circle distance

* Update imports

* Update impotrts

* Update docs

* Remove unused import

* Fix a quick bug

* Update compatibility

* Rename files

* Renaming

* Improve compatiblity

* Rename snapsht

* Fix snapshot bug

* Update interface

* Finish updating interface

* Update all paths

* Add old scripts

* Add basic halo

* Update imports

* Improve snapshot processing

* Update ordering

* Fix how CM positions accessed

* Add merger paths

* Add imports

* Add merger reading

* Add making a merger tree

* Add a basic merger tree reader

* Add imports

* Add main branch walking + comments + debuggin

* Get tree running

* Add working merger tree walking along main branch

* Add units conversion for merger data

* Add hid_to_array_index

* Update merger tree

* Add mergertree mass to PHEWcat

* Edit comments

* Add this to track changes...

* Fix a little bug

* Add mergertree mass

* Add cache clearing

* Improve summing substructure code

* Littbe bug

* Little updates to the merger tree reader

* Update .giignore

* Add box selection

* Add optional deletingf of a group

* add to keep track of changes

* Update changes

* Remove

* Add manual tracker

* Fix bug

* Add m200c_to_r200c

* Add manual halo tracking

* Remove skipped snapshots

* update cosmo params to match csiborg

* remove old comments

* Add SDSSxALFALFA

* Fix bugs

* Rename

* Edit paths

* Updates

* Add comments

* Add comment

* Add hour conversion

* Add imports

* Add new observation class

* Add selection

* Add imports

* Fix small bug

* Add field copying for safety

* Add matching to survey without masking

* Add P(k) calculation

* Add nb

* Edit comment

* Move files

* Remove merger import

* Edit setup.yp

* Fix typo

* Edit import warnigns

* update nb

* Update README

* Update README

* Update README

* Add skeleton

* Add skeleton
This commit is contained in:
Richard Stiskalek 2023-12-07 14:23:32 +00:00 committed by GitHub
parent 5500fbd2b9
commit e972f8e3f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 4627 additions and 1774 deletions

View file

@ -12,12 +12,12 @@
# 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.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from csiborgtools import clustering, field, match, read, summary # noqa
from .utils import (center_of_mass, delta2ncells, number_counts, # noqa
periodic_distance, periodic_distance_two_points, # noqa
binned_statistic, cosine_similarity) # noqa
from csiborgtools import clustering, field, halo, match, read, summary # noqa
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
# Arguments to csiborgtools.read.Paths.
paths_glamdring = {"srcdir": "/mnt/extraspace/hdesmond/",
@ -46,5 +46,34 @@ class SDSS:
(lambda x: cls[x] < 155, ("DIST", ))
]
def __call__(self):
return read.SDSS(h=1, sel_steps=self.steps)
def __call__(self, fpath=None, apply_selection=True):
if fpath is None:
fpath = "/mnt/extraspace/rstiskalek/catalogs/nsa_v1_0_1.fits"
sel_steps = self.steps if apply_selection else None
return read.SDSS(fpath, h=1, sel_steps=sel_steps)
class SDSSxALFALFA:
@staticmethod
def steps(cls):
return [(lambda x: cls[x], ("IN_DR7_LSS",)),
(lambda x: cls[x] < 17.6, ("ELPETRO_APPMAG_r", )),
(lambda x: cls[x] < 155, ("DIST", ))
]
def __call__(self, fpath=None, apply_selection=True):
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)
###############################################################################
# Clusters #
###############################################################################
clusters = {"Virgo": read.ObservedCluster(RA=hms_to_degrees(12, 27),
dec=dms_to_degrees(12, 43),
dist=16.5 * 0.7,
name="Virgo"),
}

View file

@ -15,12 +15,11 @@
from warnings import warn
try:
import MAS_library as MASL # noqa
from .density import (DensityField, PotentialField, # noqa
TidalTensorField, VelocityField)
from .interp import (evaluate_cartesian, evaluate_sky, field2rsp, # noqa
fill_outside, make_sky, observer_vobs)
from .utils import nside2radec, smoothen_field # noqa
import MAS_library as MASL # noqa
from .density import (DensityField, PotentialField, TidalTensorField, # noqa
VelocityField, power_spectrum) # noqa
from .interp import (evaluate_cartesian, evaluate_sky, field2rsp, # noqa
fill_outside, make_sky, observer_vobs) # noqa
from .utils import nside2radec, smoothen_field # noqa
except ImportError:
warn("MAS_library not found, `DensityField` will not be available", UserWarning) # noqa
warn("MAS_library not found, `DensityField` and related Pylians-based routines will not be available") # noqa

View file

@ -18,6 +18,7 @@ Density field and cross-correlation calculations.
from abc import ABC
import MAS_library as MASL
import Pk_library as PKL
import numpy
from numba import jit
from tqdm import trange
@ -33,13 +34,7 @@ class BaseField(ABC):
@property
def box(self):
"""
Simulation box information and transformations.
Returns
-------
:py:class:`csiborgtools.units.CSiBORGBox`
"""
"""Simulation box information and transformations."""
return self._box
@box.setter
@ -52,13 +47,7 @@ class BaseField(ABC):
@property
def MAS(self):
"""
Mass-assignment scheme.
Returns
-------
str
"""
"""Mass-assignment scheme."""
if self._MAS is None:
raise ValueError("`MAS` is not set.")
return self._MAS
@ -103,7 +92,6 @@ class DensityField(BaseField):
Calculate the overdensity field from the density field.
Defined as :math:`\rho/ <\rho> - 1`. Overwrites the input array.
Parameters
----------
delta : 3-dimensional array of shape `(grid, grid, grid)`
@ -117,7 +105,7 @@ class DensityField(BaseField):
delta -= 1
return delta
def __call__(self, parts, grid, flip_xz=True, nbatch=30, verbose=True):
def __call__(self, pos, mass, grid, nbatch=30, verbose=True):
"""
Calculate the density field using a Pylians routine [1, 2].
Iteratively loads the particles into memory, flips their `x` and `z`
@ -126,13 +114,12 @@ class DensityField(BaseField):
Parameters
----------
parts : 2-dimensional array of shape `(n_parts, 7)`
Particle positions, velocities and masses.
Columns are: `x`, `y`, `z`, `vx`, `vy`, `vz`, `M`.
pos : 2-dimensional array of shape `(n_parts, 3)`
Particle positions
mass : 1-dimensional array of shape `(n_parts,)`
Particle masses
grid : int
Grid size.
flip_xz : bool, optional
Whether to flip the `x` and `z` coordinates.
nbatch : int, optional
Number of batches to split the particle loading into.
verbose : bool, optional
@ -150,24 +137,20 @@ class DensityField(BaseField):
"""
rho = numpy.zeros((grid, grid, grid), dtype=numpy.float32)
nparts = parts.shape[0]
nparts = pos.shape[0]
batch_size = nparts // nbatch
start = 0
for __ in trange(nbatch + 1, disable=not verbose,
desc="Loading particles for the density field"):
end = min(start + batch_size, nparts)
pos = parts[start:end]
pos, vel, mass = pos[:, :3], pos[:, 3:6], pos[:, 6]
batch_pos = pos[start:end]
batch_mass = mass[start:end]
pos = force_single_precision(pos)
vel = force_single_precision(vel)
mass = force_single_precision(mass)
if flip_xz:
pos[:, [0, 2]] = pos[:, [2, 0]]
vel[:, [0, 2]] = vel[:, [2, 0]]
batch_pos = force_single_precision(batch_pos)
batch_mass = force_single_precision(batch_mass)
MASL.MA(pos, rho, 1., self.MAS, W=mass, verbose=False)
MASL.MA(batch_pos, rho, 1., self.MAS, W=batch_mass, verbose=False)
if end == nparts:
break
start = end
@ -178,8 +161,105 @@ class DensityField(BaseField):
return rho
# class SPHDensityVelocity(BaseField):
# r"""
# Density field calculation. Based primarily on routines of Pylians [1].
#
# Parameters
# ----------
# box : :py:class:`csiborgtools.read.CSiBORGBox`
# The simulation box information and transformations.
# MAS : str
# Mass assignment scheme. Options are Options are: 'NGP' (nearest grid
# point), 'CIC' (cloud-in-cell), 'TSC' (triangular-shape cloud), 'PCS'
# (piecewise cubic spline).
# paths : :py:class:`csiborgtools.read.Paths`
# The simulation paths.
#
# References
# ----------
# [1] https://pylians3.readthedocs.io/
# """
#
# def __init__(self, box, MAS):
# self.box = box
# self.MAS = MAS
#
# def overdensity_field(self, delta):
# r"""
# Calculate the overdensity field from the density field.
# Defined as :math:`\rho/ <\rho> - 1`. Overwrites the input array.
#
# Parameters
# ----------
# delta : 3-dimensional array of shape `(grid, grid, grid)`
# The density field.
#
# Returns
# -------
# 3-dimensional array of shape `(grid, grid, grid)`.
# """
# delta /= delta.mean()
# delta -= 1
# return delta
#
# def __call__(self, pos, mass, grid, nbatch=30, verbose=True):
# """
# Calculate the density field using a Pylians routine [1, 2].
# Iteratively loads the particles into memory, flips their `x` and `z`
# coordinates. Particles are assumed to be in box units, with positions
# in [0, 1]
#
# Parameters
# ----------
# pos : 2-dimensional array of shape `(n_parts, 3)`
# Particle positions
# mass : 1-dimensional array of shape `(n_parts,)`
# Particle masses
# grid : int
# Grid size.
# nbatch : int, optional
# Number of batches to split the particle loading into.
# verbose : bool, optional
# Verbosity flag.
#
# Returns
# -------
# 3-dimensional array of shape `(grid, grid, grid)`.
#
# References
# ----------
# [1] https://pylians3.readthedocs.io/
# [2] https://github.com/franciscovillaescusa/Pylians3/blob/master
# /library/MAS_library/MAS_library.pyx
# """
# rho = numpy.zeros((grid, grid, grid), dtype=numpy.float32)
#
# nparts = pos.shape[0]
# batch_size = nparts // nbatch
# start = 0
#
# for __ in trange(nbatch + 1, disable=not verbose,
# desc="Loading particles for the density field"):
# end = min(start + batch_size, nparts)
# batch_pos = pos[start:end]
# batch_mass = mass[start:end]
#
# batch_pos = force_single_precision(batch_pos)
# batch_mass = force_single_precision(batch_mass)
#
# MASL.MA(batch_pos, rho, 1., self.MAS, W=batch_mass, verbose=False)
# if end == nparts:
# break
# start = end
#
# # Divide by the cell volume in (kpc / h)^3
# rho /= (self.box.boxsize / grid * 1e3)**3
#
# return rho
###############################################################################
# Density field calculation #
# Velocity field calculation #
###############################################################################
@ -242,7 +322,7 @@ class VelocityField(BaseField):
/ numpy.sqrt(px**2 + py**2 + pz**2))
return radvel
def __call__(self, parts, grid, flip_xz=True, nbatch=30,
def __call__(self, pos, vel, mass, grid, flip_xz=True, nbatch=30,
verbose=True):
"""
Calculate the velocity field using a Pylians routine [1, 2].
@ -251,9 +331,12 @@ class VelocityField(BaseField):
Parameters
----------
parts : 2-dimensional array of shape `(n_parts, 7)`
Particle positions, velocities and masses.
Columns are: `x`, `y`, `z`, `vx`, `vy`, `vz`, `M`.
pos : 2-dimensional array of shape `(n_parts, 3)`
Particle positions.
vel : 2-dimensional array of shape `(n_parts, 3)`
Particle velocities.
mass : 1-dimensional array of shape `(n_parts,)`
Particle masses.
grid : int
Grid size.
flip_xz : bool, optional
@ -273,26 +356,26 @@ class VelocityField(BaseField):
[2] https://github.com/franciscovillaescusa/Pylians3/blob/master
/library/MAS_library/MAS_library.pyx
"""
rho_velx = numpy.zeros((grid, grid, grid), dtype=numpy.float32)
rho_vely = numpy.zeros((grid, grid, grid), dtype=numpy.float32)
rho_velz = numpy.zeros((grid, grid, grid), dtype=numpy.float32)
rho_vel = [rho_velx, rho_vely, rho_velz]
rho_vel = [numpy.zeros((grid, grid, grid), dtype=numpy.float32),
numpy.zeros((grid, grid, grid), dtype=numpy.float32),
numpy.zeros((grid, grid, grid), dtype=numpy.float32),
]
cellcounts = numpy.zeros((grid, grid, grid), dtype=numpy.float32)
nparts = parts.shape[0]
nparts = pos.shape[0]
batch_size = nparts // nbatch
start = 0
for __ in trange(nbatch + 1) if verbose else range(nbatch + 1):
end = min(start + batch_size, nparts)
pos = parts[start:end]
pos, vel, mass = pos[:, :3], pos[:, 3:6], pos[:, 6]
pos = force_single_precision(pos)
vel = force_single_precision(vel)
mass = force_single_precision(mass)
if flip_xz:
pos[:, [0, 2]] = pos[:, [2, 0]]
vel[:, [0, 2]] = vel[:, [2, 0]]
batch_pos = pos[start:end]
batch_vel = vel[start:end]
batch_mass = mass[start:end]
batch_pos = force_single_precision(batch_pos)
batch_vel = force_single_precision(batch_vel)
batch_mass = force_single_precision(batch_mass)
vel *= mass.reshape(-1, 1)
for i in range(3):
@ -308,7 +391,7 @@ class VelocityField(BaseField):
for i in range(3):
divide_nonzero(rho_vel[i], cellcounts)
return numpy.stack([rho_velx, rho_vely, rho_velz])
return numpy.stack(rho_vel)
###############################################################################
@ -505,3 +588,35 @@ def eigenvalues_to_environment(eigvals, th):
else:
env[i, j, k] = 3
return env
###############################################################################
# Power spectrum calculation #
###############################################################################
def power_spectrum(delta, boxsize, MAS, threads=1, verbose=True):
"""
Calculate the monopole power spectrum of the density field.
Parameters
----------
delta : 3-dimensional array of shape `(grid, grid, grid)`
The over-density field.
boxsize : float
The simulation box size in `Mpc / h`.
MAS : str
Mass assignment scheme used to calculate the density field.
threads : int, optional
Number of threads to use.
verbose : bool, optional
Verbosity flag.
Returns
-------
k, Pk : 1-dimensional arrays of shape `(grid,)`
The wavenumbers and the power spectrum.
"""
axis = 2 # Axis along which compute the quadrupole and hexadecapole
Pk = PKL.Pk(delta, boxsize, axis, MAS, threads, verbose)
return Pk.k3D, Pk.Pk[:, 0]

View file

@ -98,9 +98,12 @@ def evaluate_sky(*fields, pos, mpc2box, smooth_scales=None, verbose=False):
-------
(list of) 1-dimensional array of shape `(n_samples, len(smooth_scales))`
"""
pos = force_single_precision(pos)
# Make a copy of the positions to avoid modifying the input.
pos = numpy.copy(pos)
pos = force_single_precision(pos)
pos[:, 0] *= mpc2box
cart_pos = radec_to_cartesian(pos) + 0.5
if smooth_scales is not None:

View file

@ -0,0 +1,16 @@
# Copyright (C) 2023 Richard Stiskalek
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# 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.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from .prop import density_profile # noqa

46
csiborgtools/halo/prop.py Normal file
View file

@ -0,0 +1,46 @@
# Copyright (C) 2023 Richard Stiskalek
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# 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.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import numpy
from scipy.stats import binned_statistic
from ..utils import periodic_distance
def density_profile(pos, mass, center, nbins, boxsize):
"""
Calculate a density profile.
"""
raise NotImplementedError("Not implemented yet..")
rdist = periodic_distance(pos, center, boxsize)
rmin, rmax = numpy.min(rdist), numpy.max(rdist)
bin_edges = numpy.logspace(numpy.log10(rmin), numpy.log10(rmax), nbins)
rho, __, __ = binned_statistic(rdist, mass, statistic='sum',
bins=bin_edges)
rho /= 4. / 3 * numpy.pi * (bin_edges[1:]**3 - bin_edges[:-1]**3)
print(bin_edges)
r = 0.5 * (bin_edges[1:] + bin_edges[:-1])
# r = numpy.sqrt(bin_edges[:1] * bin_edges[:-1])
return r, rho

View file

@ -12,7 +12,5 @@
# 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.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from .match import (ParticleOverlap, RealisationsMatcher, # noqa
calculate_overlap, calculate_overlap_indxs, pos2cell, # noqa
find_neighbour, get_halo_cell_limits, # noqa
matching_max) # noqa
from .match import (ParticleOverlap, RealisationsMatcher, calculate_overlap, # noqa
pos2cell, find_neighbour, matching_max) # noqa

View file

@ -13,7 +13,8 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
Support for matching halos between CSiBORG IC realisations.
Support for matching halos between CSiBORG IC realisations based on their
Lagrangian patch overlap.
"""
from abc import ABC
from datetime import datetime
@ -21,30 +22,19 @@ from functools import lru_cache
from math import ceil
import numpy
from scipy.ndimage import gaussian_filter
from numba import jit
from scipy.ndimage import gaussian_filter
from tqdm import tqdm, trange
from ..read import load_halo_particles
class BaseMatcher(ABC):
"""
Base class for `RealisationsMatcher` and `ParticleOverlap`.
"""
"""Base class for `RealisationsMatcher` and `ParticleOverlap`."""
_box_size = None
_bckg_halfsize = None
@property
def box_size(self):
"""
Number of cells in the box.
Returns
-------
box_size : int
"""
"""Number of cells in the box."""
if self._box_size is None:
raise RuntimeError("`box_size` has not been set.")
return self._box_size
@ -64,10 +54,6 @@ class BaseMatcher(ABC):
grid distance from the center of the box to each side over which to
evaluate the background density field. Must be less than or equal to
half the box size.
Returns
-------
bckg_halfsize : int
"""
if self._bckg_halfsize is None:
raise RuntimeError("`bckg_halfsize` has not been set.")
@ -130,10 +116,6 @@ class RealisationsMatcher(BaseMatcher):
"""
Multiplier of the sum of the initial Lagrangian patch sizes of a halo
pair. Determines the range within which neighbors are returned.
Returns
-------
nmult : float
"""
return self._nmult
@ -148,10 +130,6 @@ class RealisationsMatcher(BaseMatcher):
"""
Tolerance on the absolute logarithmic mass difference of potential
matches.
Returns
-------
float
"""
return self._dlogmass
@ -166,10 +144,6 @@ class RealisationsMatcher(BaseMatcher):
"""
Mass kind whose similarity is to be checked. Must be a valid key in the
halo catalogue.
Returns
-------
str
"""
return self._mass_kind
@ -181,17 +155,10 @@ class RealisationsMatcher(BaseMatcher):
@property
def overlapper(self):
"""
The overlapper object.
Returns
-------
:py:class:`csiborgtools.match.ParticleOverlap`
"""
"""The overlapper object."""
return self._overlapper
def cross(self, cat0, catx, particles0, particlesx, halo_map0, halo_mapx,
delta_bckg, cache_size=10000, verbose=True):
def cross(self, cat0, catx, delta_bckg, cache_size=10000, verbose=True):
r"""
Find all neighbours whose CM separation is less than `nmult` times the
sum of their initial Lagrangian patch sizes and calculate their
@ -204,16 +171,6 @@ class RealisationsMatcher(BaseMatcher):
Halo catalogue of the reference simulation.
catx : instance of :py:class:`csiborgtools.read.BaseCatalogue`
Halo catalogue of the cross simulation.
particles0 : 2-dimensional array
Particles archive file of the reference simulation. The columns
must be `x`, `y`, `z` and `M`.
particlesx : 2-dimensional array
Particles archive file of the cross simulation. The columns must be
`x`, `y`, `z` and `M`.
halo_map0 : 2-dimensional array
Halo map of the reference simulation.
halo_mapx : 2-dimensional array
Halo map of the cross simulation.
delta_bckg : 3-dimensional array
Summed background density field of the reference and cross
simulations calculated with particles assigned to halos at the
@ -250,14 +207,11 @@ class RealisationsMatcher(BaseMatcher):
aratio = numpy.abs(numpy.log10(catx[p][indx] / cat0[p][i]))
match_indxs[i] = match_indxs[i][aratio < self.dlogmass]
hid2map0 = {hid: i for i, hid in enumerate(halo_map0[:, 0])}
hid2mapx = {hid: i for i, hid in enumerate(halo_mapx[:, 0])}
# We will cache the halos from the cross simulation to speed up the I/O
@lru_cache(maxsize=cache_size)
def load_cached_halox(hid):
return load_processed_halo(hid, particlesx, halo_mapx, hid2mapx,
nshift=0, ncells=self.box_size)
return load_processed_halo(hid, catx, nshift=0,
ncells=self.box_size)
iterator = tqdm(
cat0["index"],
@ -273,8 +227,7 @@ class RealisationsMatcher(BaseMatcher):
# Next, we find this halo's particles, total mass, minimum and
# maximum cells and convert positions to cells.
pos0, mass0, totmass0, mins0, maxs0 = load_processed_halo(
k0, particles0, halo_map0, hid2map0, nshift=0,
ncells=self.box_size)
k0, cat0, nshift=0, ncells=self.box_size)
# We now loop over matches of this halo and calculate their
# overlap, storing them in `_cross`.
@ -298,9 +251,8 @@ class RealisationsMatcher(BaseMatcher):
cross = numpy.asanyarray(cross, dtype=object)
return match_indxs, cross
def smoothed_cross(self, cat0, catx, particles0, particlesx, halo_map0,
halo_mapx, delta_bckg, match_indxs, smooth_kwargs,
cache_size=10000, verbose=True):
def smoothed_cross(self, cat0, catx, delta_bckg, match_indxs,
smooth_kwargs, cache_size=10000, verbose=True):
r"""
Calculate the smoothed overlaps for pairs previously identified via
`self.cross(...)` to have a non-zero NGP overlap.
@ -311,16 +263,6 @@ class RealisationsMatcher(BaseMatcher):
Halo catalogue of the reference simulation.
catx : instance of :py:class:`csiborgtools.read.BaseCatalogue`
Halo catalogue of the cross simulation.
particles0 : 2-dimensional array
Particles archive file of the reference simulation. The columns
must be `x`, `y`, `z` and `M`.
particlesx : 2-dimensional array
Particles archive file of the cross simulation. The columns must be
`x`, `y`, `z` and `M`.
halo_map0 : 2-dimensional array
Halo map of the reference simulation.
halo_mapx : 2-dimensional array
Halo map of the cross simulation.
delta_bckg : 3-dimensional array
Smoothed summed background density field of the reference and cross
simulations calculated with particles assigned to halos at the
@ -339,13 +281,11 @@ class RealisationsMatcher(BaseMatcher):
overlaps : 1-dimensional array of arrays
"""
nshift = read_nshift(smooth_kwargs)
hid2map0 = {hid: i for i, hid in enumerate(halo_map0[:, 0])}
hid2mapx = {hid: i for i, hid in enumerate(halo_mapx[:, 0])}
@lru_cache(maxsize=cache_size)
def load_cached_halox(hid):
return load_processed_halo(hid, particlesx, halo_mapx, hid2mapx,
nshift=nshift, ncells=self.box_size)
return load_processed_halo(hid, catx, nshift=nshift,
ncells=self.box_size)
iterator = tqdm(
cat0["index"],
@ -355,8 +295,7 @@ class RealisationsMatcher(BaseMatcher):
cross = [numpy.asanyarray([], dtype=numpy.float32)] * match_indxs.size
for i, k0 in enumerate(iterator):
pos0, mass0, __, mins0, maxs0 = load_processed_halo(
k0, particles0, halo_map0, hid2map0, nshift=nshift,
ncells=self.box_size)
k0, cat0, nshift=nshift, ncells=self.box_size)
# Now loop over the matches and calculate the smoothed overlap.
_cross = numpy.full(match_indxs[i].size, numpy.nan, numpy.float32)
@ -396,8 +335,7 @@ class ParticleOverlap(BaseMatcher):
self.box_size = box_size
self.bckg_halfsize = bckg_halfsize
def make_bckg_delta(self, particles, halo_map, hid2map, halo_cat,
delta=None, verbose=False):
def make_bckg_delta(self, cat, delta=None, verbose=False):
"""
Calculate a NGP density field of particles belonging to halos of a
halo catalogue `halo_cat`. Particles are only counted within the
@ -406,15 +344,8 @@ class ParticleOverlap(BaseMatcher):
Parameters
----------
particles : 2-dimensional array
Particles archive file. The columns must be `x`, `y`, `z` and `M`.
halo_map : 2-dimensional array
Array containing start and end indices in the particle array
corresponding to each halo.
hid2map : dict
Dictionary mapping halo IDs to `halo_map` array positions.
halo_cat : instance of :py:class:`csiborgtools.read.BaseCatalogue`
Halo catalogue.
cat : instance of :py:class:`csiborgtools.read.BaseCatalogue`
Halo catalogue of the reference simulation.
delta : 3-dimensional array, optional
Array to store the density field. If `None` a new array is
created.
@ -436,16 +367,17 @@ class ParticleOverlap(BaseMatcher):
& (delta.dtype == numpy.float32))
iterator = tqdm(
halo_cat["index"],
cat["index"],
desc=f"{datetime.now()} Calculating the background field",
disable=not verbose
)
for hid in iterator:
pos = load_halo_particles(hid, particles, halo_map, hid2map)
pos = cat.halo_particles(hid, "pos", in_initial=True)
if pos is None:
continue
pos, mass = pos[:, :3], pos[:, 3]
mass = cat.halo_particles(hid, "mass", in_initial=True)
pos = pos2cell(pos, self.box_size)
# We mask out particles outside the cubical high-resolution region
@ -874,7 +806,7 @@ def calculate_overlap_indxs(delta1, delta2, cellmins, delta_bckg, nonzero,
return intersect / (mass1 + mass2 - intersect)
def load_processed_halo(hid, particles, halo_map, hid2map, ncells, nshift):
def load_processed_halo(hid, cat, ncells, nshift):
"""
Load a processed halo from the `.h5` file. This is to be wrapped by a
cacher.
@ -883,14 +815,8 @@ def load_processed_halo(hid, particles, halo_map, hid2map, ncells, nshift):
----------
hid : int
Halo ID.
particles : 2-dimensional array
Array of particles in box units. The columns must be `x`, `y`, `z`
and `M`.
halo_map : 2-dimensional array
Array containing start and end indices in the particle array
corresponding to each halo.
hid2map : dict
Dictionary mapping halo IDs to `halo_map` array positions.
cat : instance of :py:class:`csiborgtools.read.BaseCatalogue`
Halo catalogue.
ncells : int
Number of cells in the box density field.
nshift : int
@ -909,8 +835,8 @@ def load_processed_halo(hid, particles, halo_map, hid2map, ncells, nshift):
maxs : len-3 tuple
Maximum cell indices of the halo.
"""
pos = load_halo_particles(hid, particles, halo_map, hid2map)
pos, mass = pos[:, :3], pos[:, 3]
pos = cat.halo_particles(hid, "pos", in_initial=True)
mass = cat.halo_particles(hid, "mass", in_initial=True)
pos = pos2cell(pos, ncells)
mins, maxs = get_halo_cell_limits(pos, ncells=ncells, nshift=nshift)

View file

@ -12,9 +12,12 @@
# 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.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from .box_units import CSiBORGBox, QuijoteBox # noqa
from .halo_cat import CSiBORGHaloCatalogue, QuijoteHaloCatalogue, fiducial_observers # noqa
from .obs import SDSS, MCXCClusters, PlanckClusters, TwoMPPGalaxies, TwoMPPGroups # noqa
from .paths import Paths # noqa
from .readsim import MmainReader, CSiBORGReader, QuijoteReader, halfwidth_mask, load_halo_particles # noqa
from .utils import cols_to_structured, read_h5 # noqa
from .box_units import CSiBORGBox, QuijoteBox # noqa
from .halo_cat import (CSiBORGCatalogue, QuijoteCatalogue, # noqa
CSiBORGPHEWCatalogue, fiducial_observers) # noqa
from .obs import (SDSS, MCXCClusters, PlanckClusters, TwoMPPGalaxies, # noqa
TwoMPPGroups, ObservedCluster, match_array_to_no_masking) # noqa
from .paths import Paths # noqa
from .readsim import (CSiBORGReader, QuijoteReader, load_halo_particles, # noqa
make_halomap_dict) # noqa
from .utils import cols_to_structured, read_h5 # noqa

View file

@ -17,6 +17,7 @@ Simulation box unit transformations.
"""
from abc import ABC, abstractmethod, abstractproperty
import numpy
from astropy import constants, units
from astropy.cosmology import LambdaCDM
@ -28,80 +29,39 @@ from .readsim import CSiBORGReader, QuijoteReader
class BaseBox(ABC):
"""
Base class for box units.
"""
_name = "box_units"
_cosmo = None
@property
def cosmo(self):
"""
The box cosmology.
Returns
-------
cosmo : `astropy.cosmology.LambdaCDM`
"""
if self._cosmo is None:
raise ValueError("Cosmology not set.")
return self._cosmo
@property
def H0(self):
r"""
The Hubble parameter at the time of the snapshot in units of
:math:`\mathrm{km} \mathrm{s}^{-1} \mathrm{Mpc}^{-1}`.
Returns
-------
H0 : float
"""
r"""Present Hubble parameter in :math:`\mathrm{km} \mathrm{s}^{-1}`"""
return self.cosmo.H0.value
@property
def rho_crit0(self):
r"""
Present-day critical density in :math:`M_\odot h^2 / \mathrm{cMpc}^3`.
Returns
-------
rho_crit0 : float
"""
"""Present-day critical density in M_sun h^2 / cMpc^3."""
rho_crit0 = self.cosmo.critical_density0
return rho_crit0.to_value(units.solMass / units.Mpc**3)
@property
def h(self):
r"""
The little 'h' parameter at the time of the snapshot.
Returns
-------
h : float
"""
"""The little 'h' parameter at the time of the snapshot."""
return self._h
@property
def Om0(self):
r"""
The matter density parameter.
Returns
-------
Om0 : float
"""
"""The present time matter density parameter."""
return self.cosmo.Om0
@abstractproperty
def boxsize(self):
"""
Box size in cMpc.
Returns
-------
boxsize : float
"""
"""Box size in cMpc."""
pass
@abstractmethod
@ -116,8 +76,7 @@ class BaseBox(ABC):
Returns
-------
length : float
Length in box units.
float
"""
pass
@ -133,8 +92,7 @@ class BaseBox(ABC):
Returns
-------
length : float
Length in :math:`\mathrm{cMpc} / h`
float
"""
pass
@ -150,8 +108,7 @@ class BaseBox(ABC):
Returns
-------
mass : float
Mass in box units.
float
"""
pass
@ -167,8 +124,23 @@ class BaseBox(ABC):
Returns
-------
mass : float
Mass in :math:`M_\odot / h`.
float
"""
pass
@abstractmethod
def m200c_to_r200c(self, m200c):
"""
Convert M200c to R200c in units of cMpc / h.
Parameters
----------
m200c : float
M200c in units of M_sun / h.
Returns
-------
float
"""
pass
@ -248,6 +220,12 @@ class CSiBORGBox(BaseBox):
def boxsize(self):
return self.box2mpc(1.)
def m200c_to_r200c(self, m200c):
rho_crit = self.cosmo.critical_density(1 / self._aexp - 1)
rho_crit = rho_crit.to_value(units.solMass / units.Mpc**3)
r200c = (3 * m200c / (4 * numpy.pi * 200 * rho_crit))**(1 / 3)
return r200c / self._aexp
###############################################################################
# Quijote fiducial cosmology box #
@ -256,7 +234,7 @@ class CSiBORGBox(BaseBox):
class QuijoteBox(BaseBox):
"""
Quijote fiducial cosmology box.
Quijote cosmology box.
Parameters
----------
@ -289,33 +267,10 @@ class QuijoteBox(BaseBox):
return length / self.boxsize
def solarmass2box(self, mass):
r"""
Convert mass from :math:`M_\odot / h` to box units.
Parameters
----------
mass : float
Mass in :math:`M_\odot`.
Returns
-------
mass : float
Mass in box units.
"""
return mass / self._info["TotMass"]
def box2solarmass(self, mass):
r"""
Convert mass from box units to :math:`M_\odot / h`.
Parameters
----------
mass : float
Mass in box units.
Returns
-------
mass : float
Mass in :math:`M_\odot / h`.
"""
return mass * self._info["TotMass"]
def m200c_to_r200c(self, m200c):
raise ValueError("Not implemented for Quijote boxes.")

File diff suppressed because it is too large Load diff

View file

@ -383,6 +383,9 @@ class FitsSurvey(ABC):
return out
return out[self.selection_mask]
def __len__(self):
return self.size
###############################################################################
# Planck clusters #
@ -560,8 +563,7 @@ class SDSS(FitsSurvey):
Parameters
----------
fpath : str, optional
Path to the FITS file. By default
`/mnt/extraspace/rstiskalek/catalogs/nsa_v1_0_1.fits`.
Path to the FITS file.
h : float, optional
Little h. By default `h = 1`. The catalogue assumes this value.
The routine properties should take care of little h conversion.
@ -581,9 +583,7 @@ class SDSS(FitsSurvey):
"""
name = "SDSS"
def __init__(self, fpath=None, h=1, Om0=0.3175, sel_steps=None):
if fpath is None:
fpath = "/mnt/extraspace/rstiskalek/catalogs/nsa_v1_0_1.fits"
def __init__(self, fpath, h=1, Om0=0.3175, sel_steps=None):
self._file = fits.open(fpath, memmap=False)
self.h = h
@ -719,3 +719,114 @@ class SDSS(FitsSurvey):
Get `IN_DR7_LSS` and turn to a boolean array.
"""
return self.get_fitsitem("IN_DR7_LSS").astype(bool)
###############################################################################
# Individual observations #
###############################################################################
class BaseSingleObservation(ABC):
"""
Base class to hold information about a single object.
"""
def __init__(self):
self._spherical_pos = None
self._name = None
@property
def spherical_pos(self):
"""
Spherical position of the observation in dist/RA/dec in Mpc / h and
degrees, respectively.
Returns
-------
1-dimensional array of shape (3,)
"""
if self._spherical_pos is None:
raise ValueError("`spherical_pos` is not set!")
return self._spherical_pos
@spherical_pos.setter
def spherical_pos(self, pos):
if isinstance(pos, (list, tuple)):
pos = numpy.array(pos)
if not pos.shape == (3,):
raise ValueError("`spherical_pos` must be a of shape (3,).")
self._spherical_pos = pos
@property
def name(self):
"""
Observated object name.
Returns
-------
str
"""
if self._name is None:
raise ValueError("`name` is not set!")
return self._name
@name.setter
def name(self, name):
if not isinstance(name, str):
raise ValueError("`name` must be a string.")
self._name = name
class ObservedCluster(BaseSingleObservation):
"""
Class to hold information about an observed cluster.
Parameters
----------
RA : float
Right ascension in degrees.
dec : float
Declination in degrees.
dist : float
Distance in Mpc / h.
name : str
Cluster name.
"""
def __init__(self, RA, dec, dist, name):
super().__init__()
self.name = name
self.spherical_pos = [dist, RA, dec]
###############################################################################
# Utility functions #
###############################################################################
def match_array_to_no_masking(arr, surv):
"""
Match an array to a survey without masking.
Parameters
----------
arr : n-dimensional array
Array to match.
surv : survey class
Survey class.
Returns
-------
out : n-dimensional array
"""
dtype = arr.dtype
if arr.ndim > 1:
shape = arr.shape
out = numpy.full((surv.selection_mask.size, *shape[1:]), numpy.nan,
dtype=dtype)
else:
out = numpy.full(surv.selection_mask.size, numpy.nan, dtype=dtype)
for i, indx in enumerate(surv["INDEX"]):
out[indx] = arr[i]
return out

View file

@ -13,7 +13,7 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""CSiBORG paths manager."""
from glob import glob
from glob import glob, iglob
from os import makedirs
from os.path import isdir, join
from warnings import warn
@ -61,13 +61,7 @@ class Paths:
@property
def srcdir(self):
"""
Path to the folder where CSiBORG simulations are stored.
Returns
-------
str
"""
"""Path to the folder where CSiBORG simulations are stored."""
if self._srcdir is None:
raise ValueError("`srcdir` is not set!")
return self._srcdir
@ -81,13 +75,7 @@ class Paths:
@property
def borg_dir(self):
"""
Path to the folder where BORG MCMC chains are stored.
Returns
-------
str
"""
"""Path to the folder where BORG MCMC chains are stored."""
if self._borg_dir is None:
raise ValueError("`borg_dir` is not set!")
return self._borg_dir
@ -101,13 +89,7 @@ class Paths:
@property
def quijote_dir(self):
"""
Path to the folder where Quijote simulations are stored.
Returns
-------
str
"""
"""Path to the folder where Quijote simulations are stored."""
if self._quijote_dir is None:
raise ValueError("`quijote_dir` is not set!")
return self._quijote_dir
@ -121,13 +103,7 @@ class Paths:
@property
def postdir(self):
"""
Path to the folder where post-processed files are stored.
Returns
-------
str
"""
"""Path to the folder where post-processed files are stored."""
if self._postdir is None:
raise ValueError("`postdir` is not set!")
return self._postdir
@ -139,19 +115,6 @@ class Paths:
check_directory(path)
self._postdir = path
@property
def temp_dumpdir(self):
"""
Path to a temporary dumping folder.
Returns
-------
str
"""
fpath = join(self.postdir, "temp")
try_create_directory(fpath)
return fpath
@staticmethod
def quijote_fiducial_nsim(nsim, nobs=None):
"""
@ -167,7 +130,7 @@ class Paths:
Returns
-------
id : str
str
"""
if nobs is None:
assert isinstance(nsim, str)
@ -190,36 +153,14 @@ class Paths:
"""
return join(self.borg_dir, "mcmc", f"mcmc_{nsim}.h5")
def fof_membership(self, nsim, simname, sorted=False):
"""
Path to the file containing the FoF particle membership.
Parameters
----------
nsim : int
IC realisation index.
simname : str
Simulation name. Must be one of `csiborg` or `quijote`.
sorted : bool, optional
Whether to return path to the file that is sorted in the same
order as the PHEW output.
"""
assert simname in ["csiborg", "quijote"]
if simname == "quijote":
raise RuntimeError("Quijote FoF membership is in the FoF cats.")
fdir = join(self.postdir, "FoF_membership", )
try_create_directory(fdir)
fout = join(fdir, f"fof_membership_{nsim}.npy")
if sorted:
fout = fout.replace(".npy", "_sorted.npy")
return fout
def fof_cat(self, nsim, simname, from_quijote_backup=False):
def fof_cat(self, nsnap, nsim, simname, from_quijote_backup=False):
r"""
Path to the :math:`z = 0` FoF halo catalogue.
Parameters
----------
nsnap : int
Snapshot index.
nsim : int
IC realisation index.
simname : str
@ -228,15 +169,15 @@ class Paths:
Whether to return the path to the Quijote FoF catalogue from the
backup.
Returns
-------
str
"""
if simname == "csiborg":
fdir = join(self.postdir, "FoF_membership", )
fdir = join(self.postdir, "halo_maker", f"ramses_{nsim}",
f"output_{str(nsnap).zfill(5)}", "FOF")
try_create_directory(fdir)
return join(fdir, f"halo_catalog_{nsim}_FOF.txt")
return join(fdir, "fort.132")
elif simname == "quijote":
if from_quijote_backup:
return join(self.quijote_dir, "halos_backup", str(nsim))
@ -245,57 +186,6 @@ class Paths:
else:
raise ValueError(f"Unknown simulation name `{simname}`.")
def mmain(self, nsnap, nsim):
"""
Path to the `mmain` CSiBORG files of summed substructure.
Parameters
----------
nsnap : int
Snapshot index.
nsim : int
IC realisation index.
Returns
-------
str
"""
fdir = join(self.postdir, "mmain")
try_create_directory(fdir)
return join(
fdir, f"mmain_{str(nsim).zfill(5)}_{str(nsnap).zfill(5)}.npz")
def initmatch(self, nsim, simname, kind):
"""
Path to the `initmatch` files where the halo match between the
initial and final snapshot of a CSiBORG realisaiton is stored.
Parameters
----------
nsim : int
IC realisation index.
simname : str
Simulation name. Must be one of `csiborg` or `quijote`.
kind : str
Type of match. Must be one of `particles` or `fit`.
Returns
-------
str
"""
assert kind in ["particles", "fit"]
ftype = "npy" if kind == "fit" else "h5"
if simname == "csiborg":
fdir = join(self.postdir, "initmatch")
elif simname == "quijote":
fdir = join(self.quijote_dir, "initmatch")
else:
raise ValueError(f"Unknown simulation name `{simname}`.")
try_create_directory(fdir)
return join(fdir, f"{kind}_{str(nsim).zfill(5)}.{ftype}")
def get_ics(self, simname, from_quijote_backup=False):
"""
Get available IC realisation IDs for either the CSiBORG or Quijote
@ -411,7 +301,7 @@ class Paths:
Returns
-------
snapstr
str
"""
simpath = self.snapshots(nsim, simname, tonew=nsnap == 1)
if simname == "csiborg":
@ -422,7 +312,27 @@ class Paths:
nsnap = str(nsnap).zfill(3)
return join(simpath, f"snapdir_{nsnap}", f"snap_{nsnap}")
def particles(self, nsim, simname):
def merger_tree_file(self, nsnap, nsim):
"""
Path to the CSiBORG on-the-fly generated merger tree file.
Parameters
----------
nsnap : int
Snapshot index.
nsim : int
IC realisation index.
Returns
-------
str
"""
nsim = str(nsim)
nsnap = str(nsnap).zfill(5)
return join(self.srcdir, f"ramses_out_{nsim}",
f"output_{nsnap}", f"mergertree_{nsnap}.dat")
def processed_output(self, nsim, simname, halo_finder):
"""
Path to the files containing all particles of a CSiBORG realisation at
:math:`z = 0`.
@ -433,22 +343,80 @@ class Paths:
IC realisation index.
simname : str
Simulation name. Must be one of `csiborg` or `quijote`.
halo_finder : str
Halo finder name.
Returns
-------
str
"""
if simname == "csiborg":
fdir = join(self.postdir, "particles")
fdir = join(self.postdir, "processed_output")
elif simname == "quijote":
fdir = join(self.quijote_dir, "Particles_fiducial")
else:
raise ValueError(f"Unknown simulation name `{simname}`.")
try_create_directory(fdir)
fname = f"parts_{str(nsim).zfill(5)}.h5"
fname = f"parts_{halo_finder}_{str(nsim).zfill(5)}.hdf5"
return join(fdir, fname)
def processed_phew(self, nsim):
"""
Path to the files containing PHEW CSiBORG catalogues.
Parameters
----------
nsim : int
IC realisation index.
Returns
-------
str
"""
fdir = join(self.postdir, "processed_output")
try_create_directory(fdir)
return join(fdir, f"phew_{str(nsim).zfill(5)}.hdf5")
def processed_merger_tree(self, nsim):
"""
Path to the files containing the processed original merger tree files.
Parameters
----------
nsim : int
IC realisation index.
Returns
-------
str
"""
fdir = join(self.postdir, "processed_output")
try_create_directory(fdir)
return join(fdir, f"merger_{str(nsim).zfill(5)}.hdf5")
def halomaker_particle_membership(self, nsnap, nsim, halo_finder):
"""
Path to the HaloMaker particle membership file (CSiBORG only).
Parameters
----------
nsnap : int
Snapshot index.
nsim : int
IC realisation index.
halo_finder : str
Halo finder name.
Returns
-------
str
"""
fdir = join(self.postdir, "halo_maker", f"ramses_{nsim}",
f"output_{str(nsnap).zfill(5)}", halo_finder)
fpath = join(fdir, "*particle_membership*")
return next(iglob(fpath, recursive=True), None)
def ascii_positions(self, nsim, kind):
"""
Path to ASCII files containing the positions of particles or halos.
@ -469,35 +437,6 @@ class Paths:
return join(fdir, fname)
def structfit(self, nsnap, nsim, simname):
"""
Path to the halo catalogue from `fit_halos.py`.
Parameters
----------
nsnap : int
Snapshot index.
nsim : int
IC realisation index.
simname : str
Simulation name. Must be one of `csiborg` or `quijote`.
Returns
-------
str
"""
if simname == "csiborg":
fdir = join(self.postdir, "structfit")
elif simname == "quijote":
fdir = join(self.quijote_dir, "structfit")
else:
raise ValueError(f"Unknown simulation name `{simname}`.")
try_create_directory(fdir)
fname = f"out_{str(nsim).zfill(5)}_{str(nsnap).zfill(5)}.npy"
return join(fdir, fname)
def overlap(self, simname, nsim0, nsimx, min_logmass, smoothed):
"""
Path to the overlap files between two CSiBORG simulations.
@ -688,31 +627,6 @@ class Paths:
fname = f"obs_vp_{MAS}_{str(nsim).zfill(5)}_{grid}.npz"
return join(fdir, fname)
def halo_counts(self, simname, nsim, from_quijote_backup=False):
"""
Path to the files containing the binned halo counts.
Parameters
----------
simname : str
Simulation name. Must be `csiborg`, `quijote` or `quijote_full`.
nsim : int
IC realisation index.
from_quijote_backup : bool, optional
Whether to return the path to the Quijote halo counts from the
backup catalogues.
Returns
-------
str
"""
fdir = join(self.postdir, "HMF")
try_create_directory(fdir)
fname = f"halo_counts_{simname}_{str(nsim).zfill(5)}.npz"
if from_quijote_backup:
fname = fname.replace("halo_counts", "halo_counts_backup")
return join(fdir, fname)
def cross_nearest(self, simname, run, kind, nsim=None, nobs=None):
"""
Path to the files containing distance from a halo in a reference

File diff suppressed because it is too large Load diff

View file

@ -15,6 +15,7 @@
"""Collection of stand-off utility functions used in the scripts."""
import numpy
from numba import jit
from datetime import datetime
###############################################################################
# Positions #
@ -87,7 +88,7 @@ def periodic_distance_two_points(p1, p2, boxsize):
return dist**0.5
@jit(nopython=True)
@jit(nopython=True, boundscheck=False)
def periodic_wrap_grid(pos, boxsize=1):
"""Wrap positions in a periodic box."""
for n in range(pos.shape[0]):
@ -139,17 +140,34 @@ def radec_to_cartesian(X):
"""
dist, ra, dec = X[:, 0], X[:, 1], X[:, 2]
ra *= numpy.pi / 180
dec *= numpy.pi / 180
cdec = numpy.cos(dec)
cdec = numpy.cos(dec * numpy.pi / 180)
return numpy.vstack([
dist * cdec * numpy.cos(ra),
dist * cdec * numpy.sin(ra),
dist * numpy.sin(dec)
dist * cdec * numpy.cos(ra * numpy.pi / 180),
dist * cdec * numpy.sin(ra * numpy.pi / 180),
dist * numpy.sin(dec * numpy.pi / 180)
]).T
@jit(nopython=True, fastmath=True, boundscheck=False)
def great_circle_distance(x1, x2):
"""
Great circle distance between two points on a sphere, defined by RA and
dec, both in degrees.
"""
ra1, dec1 = x1
ra2, dec2 = x2
ra1 *= numpy.pi / 180
dec1 *= numpy.pi / 180
ra2 *= numpy.pi / 180
dec2 *= numpy.pi / 180
return 180 / numpy.pi * numpy.arccos(
numpy.sin(dec1) * numpy.sin(dec2)
+ numpy.cos(dec1) * numpy.cos(dec2) * numpy.cos(ra1 - ra2)
)
def cosine_similarity(x, y):
r"""
Calculate the cosine similarity between two Cartesian vectors. Defined
@ -179,6 +197,36 @@ def cosine_similarity(x, y):
return out[0] if out.size == 1 else out
def hms_to_degrees(hours, minutes=None, seconds=None):
"""
Convert hours, minutes and seconds to degrees.
Parameters
----------
hours, minutes, seconds : float
Returns
-------
float
"""
return hours * 15 + (minutes or 0) / 60 * 15 + (seconds or 0) / 3600 * 15
def dms_to_degrees(degrees, arcminutes=None, arcseconds=None):
"""
Convert degrees, arcminutes and arcseconds to decimal degrees.
Parameters
----------
degrees, arcminutes, arcseconds : float
Returns
-------
float
"""
return degrees + (arcminutes or 0) / 60 + (arcseconds or 0) / 3600
def real2redshift(pos, vel, observer_location, observer_velocity, box,
periodic_wrap=True, make_copy=True):
r"""
@ -262,3 +310,9 @@ def binned_statistic(x, y, left_edges, bin_width, statistic):
if numpy.any(mask):
out[i] = statistic(y[mask])
return out
def fprint(msg, verbose=True):
"""Print and flush a message with a timestamp."""
if verbose:
print(f"{datetime.now()}: {msg}", flush=True)