From 39b3498621038d05364a12741254a2f3654763d6 Mon Sep 17 00:00:00 2001 From: Richard Stiskalek Date: Wed, 19 Apr 2023 16:39:35 +0200 Subject: [PATCH] Correct fitting script for both clumps and halos (#46) * Minor typos * fix minor bugs * pep8 * formatting * pep8 * Fix minor bugs * New path & pep8 * add splitt * Updates * Improve calculation within radius * pep8 * pep8 * get the script working * Add matter overdensity * Add m200m to the script * Fix looping bug * add parents support * add import * Optionally concatenate velocities * Make optional masking * Ignore the error message * Start reading in raw data * Fix cat reading * Additional units conversions * Add clump reading * Fix indexing * Remove old comment * Remove old comment * set npart to 0 instead of overflow from NaN * fix docs * rm boring stuff * Remove old stuff * Remove old stuff * Remove old comment * Update nb --- csiborgtools/fits/__init__.py | 1 + csiborgtools/fits/halo.py | 115 +++++++++----- csiborgtools/fits/haloprofile.py | 29 ++-- csiborgtools/fits/utils.py | 43 +++++ csiborgtools/match/__init__.py | 5 +- csiborgtools/match/utils.py | 26 ++- csiborgtools/read/__init__.py | 3 +- csiborgtools/read/box_units.py | 75 ++++++--- csiborgtools/read/halo_cat.py | 119 +++++++++----- csiborgtools/read/paths.py | 69 ++++---- csiborgtools/read/utils.py | 14 +- notebooks/fitting.ipynb | 140 ++++++++++++++++ scripts/cluster_knn_auto.py | 69 ++++---- scripts/cluster_knn_cross.py | 39 ++--- scripts/cluster_tcpf_auto.py | 38 ++--- scripts/pre_fithalos.py | 264 ++++++++++++++++++------------- scripts/pre_splithalos.py | 17 +- 17 files changed, 709 insertions(+), 357 deletions(-) create mode 100644 csiborgtools/fits/utils.py create mode 100644 notebooks/fitting.ipynb diff --git a/csiborgtools/fits/__init__.py b/csiborgtools/fits/__init__.py index e9e935e..c62bcc1 100644 --- a/csiborgtools/fits/__init__.py +++ b/csiborgtools/fits/__init__.py @@ -14,3 +14,4 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from .halo import Clump, Halo # noqa from .haloprofile import NFWPosterior, NFWProfile # noqa +from .utils import split_jobs # noqa diff --git a/csiborgtools/fits/halo.py b/csiborgtools/fits/halo.py index 3cacf0b..ca04915 100644 --- a/csiborgtools/fits/halo.py +++ b/csiborgtools/fits/halo.py @@ -22,6 +22,7 @@ class BaseStructure(ABC): """ Basic structure object for handling operations on its particles. """ + _particles = None _info = None _box = None @@ -39,7 +40,7 @@ class BaseStructure(ABC): @particles.setter def particles(self, particles): - pars = ['x', 'y', 'z', 'vx', 'vy', 'vz', 'M'] + pars = ["x", "y", "z", "vx", "vy", "vz", "M"] assert all(p in particles.dtype.names for p in pars) self._particles = particles @@ -73,7 +74,7 @@ class BaseStructure(ABC): @box.setter def box(self, box): try: - assert box._name == "box_units" + assert box._name == "box_units" self._box = box except AttributeError as err: raise TypeError from err @@ -87,20 +88,9 @@ class BaseStructure(ABC): ------- pos : 2-dimensional array of shape `(n_particles, 3)`. """ - ps = ('x', 'y', 'z') + ps = ("x", "y", "z") return numpy.vstack([self[p] - self.info[p] for p in ps]).T - @property - def r(self): - """ - Radial separation of the particles from the centre of the object. - - Returns - ------- - r : 1-dimensional array of shape `(n_particles, )`. - """ - return numpy.linalg.norm(self.pos, axis=1) - @property def vel(self): """ @@ -112,34 +102,61 @@ class BaseStructure(ABC): """ return numpy.vstack([self[p] for p in ("vx", "vy", "vz")]).T - @property - def cmass(self): + def r(self): """ - Cartesian position components of the object's centre of mass. Note that - this is already in a frame centered at the clump's potential minimum, - so its distance from origin indicates the separation of the centre of - mass and potential minimum. + Calculate the radial separation of the particles from the centre of the + object. + + Returns + ------- + r : 1-dimensional array of shape `(n_particles, )`. + """ + return numpy.linalg.norm(self.pos, axis=1) + + def cmass(self, rmax, rmin): + """ + Calculate Cartesian position components of the object's centre of mass. + Note that this is already in a frame centered at the clump's potential + minimum, so its distance from origin indicates the separation of the + centre of mass and potential minimum. + + Parameters + ---------- + rmax : float + Maximum radius for particles to be included in the calculation. + rmin : float + Minimum radius for particles to be included in the calculation. Returns ------- cm : 1-dimensional array of shape `(3, )` """ - return numpy.average(self.pos, axis=0, weights=self['M']) + r = self.r() + mask = (r >= rmin) & (r <= rmax) + return numpy.average(self.pos[mask], axis=0, weights=self["M"][mask]) - @property - def angular_momentum(self): + def angular_momentum(self, rmax, rmin=0): """ - Angular momentum in the box coordinates. + Calculate angular momentum in the box coordinates. - NOTE: here also change velocities to the CM and appropriately edit the - docs. + Parameters + ---------- + rmax : float + Maximum radius for particles to be included in the calculation. + rmin : float + Minimum radius for particles to be included in the calculation. Returns ------- J : 1-dimensional array or shape `(3, )` """ - J = numpy.cross(self.pos - self.cmass, self.vel) - return numpy.einsum("i,ij->j", self.m, J) + r = self.r() + mask = (r >= rmin) & (r <= rmax) + pos = self.pos[mask] - self.cmass(rmax, rmin) + # Velocitities in the object CM frame + vel = self.vel[mask] + vel -= numpy.average(self.vel[mask], axis=0, weights=self["M"][mask]) + return numpy.einsum("i,ij->j", self["M"][mask], numpy.cross(pos, vel)) def enclosed_mass(self, rmax, rmin=0): """ @@ -156,8 +173,8 @@ class BaseStructure(ABC): ------- enclosed_mass : float """ - r = self.r - return numpy.sum(self['M'][(r >= rmin) & (r <= rmax)]) + r = self.r() + return numpy.sum(self["M"][(r >= rmin) & (r <= rmax)]) def lambda_bullock(self, radius, npart_min=10): r""" @@ -182,19 +199,21 @@ class BaseStructure(ABC): Bullock, J. S.; Dekel, A.; Kolatt, T. S.; Kravtsov, A. V.; Klypin, A. A.; Porciani, C.; Primack, J. R. """ - mask = self.r <= radius + mask = self.r() <= radius if numpy.sum(mask) < npart_min: return numpy.nan mass = self.enclosed_mass(radius) V = numpy.sqrt(self.box.box_G * mass / radius) - return (numpy.linalg.norm(self.angular_momentum[mask]) - / (numpy.sqrt(2) * mass * V * radius)) + return numpy.linalg.norm(self.angular_momentum(radius)) / ( + numpy.sqrt(2) * mass * V * radius + ) - def spherical_overdensity_mass(self, delta_mult, npart_min=10): + def spherical_overdensity_mass(self, delta_mult, npart_min=10, kind="crit"): r""" Calculate spherical overdensity mass and radius. The mass is defined as the enclosed mass within an outermost radius where the mean enclosed - spherical density reaches a multiple of the critical density `delta`. + spherical density reaches a multiple of the critical density `delta` + (times the matter density if `kind` is `matter`). Parameters ---------- @@ -203,6 +222,8 @@ class BaseStructure(ABC): npart_min : int Minimum number of enclosed particles for a radius to be considered trustworthy. + kind : str + Either `crit` or `matter`, for critical or matter overdensity Returns ------- @@ -211,17 +232,25 @@ class BaseStructure(ABC): mx : float Corresponding spherical enclosed mass. """ + # Quick check of inputs + assert kind in ["crit", "matter"] + # We first sort the particles in an increasing separation - rs = self.r + rs = self.r() order = numpy.argsort(rs) rs = rs[order] - cmass = numpy.cumsum(self['M']) # Cumulative mass + cmass = numpy.cumsum(self["M"][order]) # Cumulative mass # We calculate the enclosed volume and indices where it is above target - vol = 4 * numpy.pi / 3 * (rs**3 - rs[0]**3) - ks = numpy.where([cmass / vol > delta_mult * self.box.rhoc])[0] + vol = 4 * numpy.pi / 3 * (rs**3 - rs[0] ** 3) + + target_density = delta_mult * self.box.box_rhoc + if kind == "matter": + target_density *= self.box.cosmo.Om0 + with numpy.errstate(divide="ignore"): + ks = numpy.where(cmass / vol > target_density)[0] if ks.size == 0: # Never above the threshold? return numpy.nan, numpy.nan - k = numpy.maximum(ks) + k = numpy.max(ks) if k < npart_min: # Too few particles? return numpy.nan, numpy.nan return rs[k], cmass[k] @@ -235,7 +264,7 @@ class BaseStructure(ABC): ------- key : list of str """ - return self.data.dtype.names + return self.particles.dtype.names def __getitem__(self, key): if key not in self.keys: @@ -259,6 +288,7 @@ class Clump(BaseStructure): box : :py:class:`csiborgtools.read.BoxUnits` Box units object. """ + def __init__(self, particles, info, box): self.particles = particles self.info = info @@ -279,7 +309,8 @@ class Halo(BaseStructure): box : :py:class:`csiborgtools.read.BoxUnits` Box units object. """ + def __init__(self, particles, info, box): self.particles = particles self.info = info - self.box = box \ No newline at end of file + self.box = box diff --git a/csiborgtools/fits/haloprofile.py b/csiborgtools/fits/haloprofile.py index b6c901a..d647a99 100644 --- a/csiborgtools/fits/haloprofile.py +++ b/csiborgtools/fits/haloprofile.py @@ -50,7 +50,7 @@ class NFWProfile: density : 1-dimensional array """ x = r / Rs - return rho0 / (x * (1 + x)**2) + return rho0 / (x * (1 + x) ** 2) @staticmethod def _logprofile(r, Rs, rho0): @@ -153,7 +153,7 @@ class NFWProfile: samples : float or 1-dimensional array Samples following the NFW profile. """ - gen = uniform(rmin, rmax-rmin) + gen = uniform(rmin, rmax - rmin) samples = numpy.full(size, numpy.nan) for i in range(size): while True: @@ -187,9 +187,6 @@ class NFWPosterior(NFWProfile): Clump object containing the particles and clump information. """ - def __init__(self): - super().__init__() - @property def clump(self): """ @@ -228,7 +225,7 @@ class NFWPosterior(NFWProfile): ------- rho0: float """ - return mass / self.bounded_enclosed_mass(rmin, rmax, Rs, 1) + return mass / self.bounded_mass(rmin, rmax, Rs, 1) def initlogRs(self, r, rmin, rmax, binsguess=10): r""" @@ -255,7 +252,6 @@ class NFWPosterior(NFWProfile): counts, edges = numpy.histogram(r, bins) return numpy.log10(edges[numpy.argmax(counts)]) - def logprior(self, logRs, rmin, rmax): r""" Logarithmic uniform prior on :math:`\log R_{\rm s}`. Unnormalised but @@ -275,8 +271,8 @@ class NFWPosterior(NFWProfile): lp : float """ if not rmin < 10**logRs < rmax: - return - numpy.infty - return 0. + return -numpy.infty + return 0.0 def loglikelihood(self, logRs, r, rmin, rmax, npart): """ @@ -303,7 +299,6 @@ class NFWPosterior(NFWProfile): mnfw = self.bounded_mass(rmin, rmax, Rs, 1) return numpy.sum(self._logprofile(r, Rs, 1)) - npart * numpy.log(mnfw) - def __call__(self, logRs, r, rmin, rmax, npart): """ Logarithmic posterior. Sum of the logarithmic prior and likelihood. @@ -327,7 +322,7 @@ class NFWPosterior(NFWProfile): """ lp = self.logprior(logRs, rmin, rmax) if not numpy.isfinite(lp): - return - numpy.infty + return -numpy.infty return self.loglikelihood(logRs, r, rmin, rmax, npart) + lp def fit(self, clump, eps=1e-4): @@ -353,18 +348,20 @@ class NFWPosterior(NFWProfile): Best fit NFW central density. """ assert isinstance(clump, Clump) - r = clump.r + r = clump.r() rmin = numpy.min(r) rmax, mtot = clump.spherical_overdensity_mass(200) - npart = numpy.sum((rmin <= r) & (r <= rmax)) + mask = (rmin <= r) & (r <= rmax) + npart = numpy.sum(mask) + r = r[mask] # Loss function to optimize def loss(logRs): - return - self(logRs, r, rmin, rmax, npart) + return -self(logRs, r, rmin, rmax, npart) res = minimize_scalar( - loss, bounds=(numpy.log10(rmin), numpy.log10(rmax)), - method='bounded') + loss, bounds=(numpy.log10(rmin), numpy.log10(rmax)), method="bounded" + ) if numpy.log10(rmax) - res.x < eps: res.success = False diff --git a/csiborgtools/fits/utils.py b/csiborgtools/fits/utils.py new file mode 100644 index 0000000..2e9d2ed --- /dev/null +++ b/csiborgtools/fits/utils.py @@ -0,0 +1,43 @@ +# Copyright (C) 2022 Richard Stiskalek, Deaglan Bartlett +# 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. +"""Fitting utility functions.""" + +import numpy + + +def split_jobs(njobs, ncpu): + """ + Split `njobs` amongst `ncpu`. + + Parameters + ---------- + njobs : int + Number of jobs. + ncpu : int + Number of CPUs. + + Returns + ------- + jobs : list of lists of integers + Outer list of each CPU and inner lists for CPU's jobs. + """ + njobs_per_cpu, njobs_remainder = divmod(njobs, ncpu) + jobs = numpy.arange(njobs_per_cpu * ncpu).reshape((njobs_per_cpu, ncpu)).T + + jobs = jobs.tolist() + for i in range(njobs_remainder): + jobs[i].append(njobs_per_cpu * ncpu + i) + + return jobs diff --git a/csiborgtools/match/__init__.py b/csiborgtools/match/__init__.py index 72eaca6..a6341d6 100644 --- a/csiborgtools/match/__init__.py +++ b/csiborgtools/match/__init__.py @@ -12,7 +12,7 @@ # 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.match.match import ( # noqa +from .match import ( # noqa ParticleOverlap, RealisationsMatcher, calculate_overlap, @@ -24,4 +24,5 @@ from csiborgtools.match.match import ( # noqa fill_delta_indxs, get_clumplims, ) -from csiborgtools.match.num_density import binned_counts, number_density # noqa +from .num_density import binned_counts, number_density # noqa +from .utils import concatenate_clumps # noqa diff --git a/csiborgtools/match/utils.py b/csiborgtools/match/utils.py index 4fda0ac..bd59124 100644 --- a/csiborgtools/match/utils.py +++ b/csiborgtools/match/utils.py @@ -16,7 +16,7 @@ import numpy -def concatenate_clumps(clumps): +def concatenate_clumps(clumps, include_velocities=False): """ Concatenate an array of clumps to a single array containing all particles. @@ -24,6 +24,8 @@ def concatenate_clumps(clumps): ---------- clumps : list of structured arrays List of clumps. Each clump must be a structured array with keys + include_velocities : bool, optional + Whether to include velocities in the output array. Returns ------- @@ -34,22 +36,32 @@ def concatenate_clumps(clumps): for clump, __ in clumps: N += clump.size # Infer dtype of positions - if clumps[0][0]['x'].dtype.char in numpy.typecodes["AllInteger"]: + if clumps[0][0]["x"].dtype.char in numpy.typecodes["AllInteger"]: posdtype = numpy.int32 else: posdtype = numpy.float32 - # Pre-allocate array - dtype = {"names": ['x', 'y', 'z', 'M'], - "formats": [posdtype] * 3 + [numpy.float32]} + # We pre-allocate an empty array. By default, we include just particle positions, + # which may be specified by cell IDs if integers, and masses. Additionally also + # outputs velocities. + if include_velocities: + dtype = { + "names": ["x", "y", "z", "vx", "vy", "vz", "M"], + "formats": [posdtype] * 3 + [numpy.float32] * 4, + } + else: + dtype = { + "names": ["x", "y", "z", "M"], + "formats": [posdtype] * 3 + [numpy.float32], + } particles = numpy.full(N, numpy.nan, dtype) # Fill it one clump by another start = 0 for clump, __ in clumps: end = start + clump.size - for p in ('x', 'y', 'z', 'M'): + for p in dtype["names"]: particles[p][start:end] = clump[p] start = end - return particles \ No newline at end of file + return particles diff --git a/csiborgtools/read/__init__.py b/csiborgtools/read/__init__.py index 026275a..01a3454 100644 --- a/csiborgtools/read/__init__.py +++ b/csiborgtools/read/__init__.py @@ -12,6 +12,7 @@ # 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 BoxUnits # noqa from .halo_cat import ClumpsCatalogue, HaloCatalogue # noqa from .knn_summary import kNNCDFReader # noqa from .obs import ( # noqa @@ -26,4 +27,4 @@ from .paths import CSiBORGPaths # noqa from .pk_summary import PKReader # noqa from .readsim import MmainReader, ParticleReader, halfwidth_select, read_initcm # noqa from .tpcf_summary import TPCFReader # noqa -from .utils import cartesian_to_radec, radec_to_cartesian \ No newline at end of file +from .utils import cartesian_to_radec, cols_to_structured, radec_to_cartesian # noqa diff --git a/csiborgtools/read/box_units.py b/csiborgtools/read/box_units.py index dcd4a94..fe05144 100644 --- a/csiborgtools/read/box_units.py +++ b/csiborgtools/read/box_units.py @@ -24,11 +24,27 @@ from .readsim import ParticleReader # Map of unit conversions CONV_NAME = { - "length": ['x', 'y', 'z', "peak_x", "peak_y", "peak_z", "Rs", "rmin", - "rmax", "r200", "r500", "x0", "y0", "z0", "lagpatch"], - "mass": ["mass_cl", "totpartmass", "m200", "m500", "mass_mmain", 'M'], - "density": ["rho0"] - } + "length": [ + "x", + "y", + "z", + "peak_x", + "peak_y", + "peak_z", + "Rs", + "rmin", + "rmax", + "r200c", + "r500c", + "r200m", + "x0", + "y0", + "z0", + "lagpatch", + ], + "mass": ["mass_cl", "totpartmass", "m200c", "m500c", "mass_mmain", "M", "m200m"], + "density": ["rho0"], +} class BoxUnits: @@ -53,15 +69,29 @@ class BoxUnits: """ partreader = ParticleReader(paths) info = partreader.read_info(nsnap, nsim) - pars = ["boxlen", "time", "aexp", "H0", - "omega_m", "omega_l", "omega_k", "omega_b", - "unit_l", "unit_d", "unit_t"] + pars = [ + "boxlen", + "time", + "aexp", + "H0", + "omega_m", + "omega_l", + "omega_k", + "omega_b", + "unit_l", + "unit_d", + "unit_t", + ] for par in pars: setattr(self, "_" + par, float(info[par])) - self._cosmo = LambdaCDM(H0=self._H0, Om0=self._omega_m, - Ode0=self._omega_l, Tcmb0=2.725 * units.K, - Ob0=self._omega_b) + self._cosmo = LambdaCDM( + H0=self._H0, + Om0=self._omega_m, + Ode0=self._omega_l, + Tcmb0=2.725 * units.K, + Ob0=self._omega_b, + ) self._Msuncgs = constants.M_sun.cgs.value # Solar mass in grams @property @@ -108,7 +138,7 @@ class BoxUnits: ------- G : float """ - return constants.G.cgs.value * (self._unit_d * self._unit_t ** 2) + return constants.G.cgs.value * (self._unit_d * self._unit_t**2) @property def box_H0(self): @@ -142,7 +172,7 @@ class BoxUnits: rhoc : float """ - return 3 * self.box_H0 ** 2 / (8 * numpy.pi * self.box_G) + return 3 * self.box_H0**2 / (8 * numpy.pi * self.box_G) def box2kpc(self, length): r""" @@ -227,7 +257,7 @@ class BoxUnits: cosmo_redshift : foat Cosmological redshift. """ - x = numpy.linspace(0., 1., 5001) + x = numpy.linspace(0.0, 1.0, 5001) y = self.cosmo.comoving_distance(x) return interp1d(y, x)(self.box2mpc(dist)) @@ -254,7 +284,7 @@ class BoxUnits: """ # Peculiar velocity along the radial distance r = numpy.vstack([px - p0x, py - p0y, pz - p0z]).T - norm = numpy.sum(r**2, axis=1)**0.5 + norm = numpy.sum(r**2, axis=1) ** 0.5 v = numpy.vstack([vx, vy, vz]).T vpec = numpy.sum(r * v, axis=1) / norm # Ratio between the peculiar velocity and speed of light @@ -284,7 +314,7 @@ class BoxUnits: Observed redshift. """ r = numpy.vstack([px - p0x, py - p0y, pz - p0z]).T - zcosmo = self.box2cosmoredshift(numpy.sum(r**2, axis=1)**0.5) + zcosmo = self.box2cosmoredshift(numpy.sum(r**2, axis=1) ** 0.5) zpec = self.box2pecredshift(vx, vy, vz, px, py, pz, p0x, p0y, p0z) return (1 + zpec) * (1 + zcosmo) - 1 @@ -337,8 +367,7 @@ class BoxUnits: density : float Density in :math:`M_\odot / \mathrm{pc}^3`. """ - return (density * self._unit_d / self._Msuncgs - * (units.Mpc.to(units.cm))**3) + return density * self._unit_d / self._Msuncgs * (units.Mpc.to(units.cm)) ** 3 def dens2box(self, density): r""" @@ -355,8 +384,7 @@ class BoxUnits: density : float Density in box units. """ - return (density / self._unit_d * self._Msuncgs - / (units.Mpc.to(units.cm))**3) + return density / self._unit_d * self._Msuncgs / (units.Mpc.to(units.cm)) ** 3 def convert_from_boxunits(self, data, names): r""" @@ -389,8 +417,8 @@ class BoxUnits: transforms = { "length": self.box2mpc, "mass": self.box2solarmass, - "density": self.box2dens - } + "density": self.box2dens, + } for name in names: # Check that the name is even in the array @@ -407,7 +435,8 @@ class BoxUnits: # If nothing found if not found: raise NotImplementedError( - "Conversion of `{}` is not defined.".format(name)) + "Conversion of `{}` is not defined.".format(name) + ) # Center at the observer if name in ["peak_x", "peak_y", "peak_z", "x0", "y0", "z0"]: diff --git a/csiborgtools/read/halo_cat.py b/csiborgtools/read/halo_cat.py index 149648c..889aec7 100644 --- a/csiborgtools/read/halo_cat.py +++ b/csiborgtools/read/halo_cat.py @@ -21,13 +21,14 @@ from sklearn.neighbors import NearestNeighbors from .box_units import BoxUnits from .paths import CSiBORGPaths from .readsim import ParticleReader -from .utils import cartesian_to_radec, flip_cols, radec_to_cartesian +from .utils import add_columns, cartesian_to_radec, flip_cols, radec_to_cartesian class BaseCatalogue(ABC): """ Base (sub)halo catalogue. """ + _data = None _paths = None _nsim = None @@ -106,7 +107,7 @@ class BaseCatalogue(ABC): @box.setter def box(self, box): try: - assert box._name == "box_units" + assert box._name == "box_units" self._box = box except AttributeError as err: raise TypeError from err @@ -132,9 +133,9 @@ class BaseCatalogue(ABC): pos : 2-dimensional array of shape `(nobjects, 3)` """ if in_initial: - ps = ['x0', 'y0', 'z0'] + ps = ["x0", "y0", "z0"] else: - ps = ['x', 'y', 'z'] + ps = ["x", "y", "z"] pos = numpy.vstack([self[p] for p in ps]).T if not cartesian: pos = cartesian_to_radec(pos) @@ -248,8 +249,7 @@ class BaseCatalogue(ABC): knn.fit(pos) # Convert angular radius to cosine difference. metric_maxdist = 1 - numpy.cos(numpy.deg2rad(ang_radius)) - dist, ind = knn.radius_neighbors(X, radius=metric_maxdist, - sort_results=True) + dist, ind = knn.radius_neighbors(X, radius=metric_maxdist, sort_results=True) # And the cosine difference to angular distance. for i in range(X.shape[0]): dist[i] = numpy.rad2deg(numpy.arccos(1 - dist[i])) @@ -282,13 +282,9 @@ class ClumpsCatalogue(BaseCatalogue): r""" Clumps catalogue, defined in the final snapshot. - TODO: - Add fitted quantities. - Add threshold on number of particles - Parameters ---------- - nsim : int + sim : int IC realisation index. paths : py:class`csiborgtools.read.CSiBORGPaths` CSiBORG paths object. @@ -299,28 +295,68 @@ class ClumpsCatalogue(BaseCatalogue): minmass : len-2 tuple, optional Minimum mass. The first element is the catalogue key and the second is the value. + load_fitted : bool, optional + Whether to load fitted quantities. + rawdata : bool, optional + Whether to return the raw data. In this case applies no cuts and + transformations. """ - def __init__(self, nsim, paths, maxdist=155.5 / 0.705, - minmass=("mass_cl", 1e12)): + + def __init__( + self, + nsim, + paths, + maxdist=155.5 / 0.705, + minmass=("mass_cl", 1e12), + load_fitted=True, + rawdata=False, + ): self.nsim = nsim self.paths = paths # Read in the clumps from the final snapshot partreader = ParticleReader(self.paths) - cols = ["index", "parent", 'x', 'y', 'z', "mass_cl"] - data = partreader.read_clumps(self.nsnap, self.nsim, cols=cols) + cols = ["index", "parent", "x", "y", "z", "mass_cl"] + self._data = partreader.read_clumps(self.nsnap, self.nsim, cols=cols) # Overwrite the parent with the ultimate parent mmain = numpy.load(self.paths.mmain_path(self.nsnap, self.nsim)) - data["parent"] = mmain["ultimate_parent"] + self._data["parent"] = mmain["ultimate_parent"] - # Flip positions and convert from code units to cMpc. Convert M too - flip_cols(data, "x", "z") - for p in ('x', 'y', 'z'): - data[p] -= 0.5 - data = self.box.convert_from_boxunits(data, ['x', 'y', 'z', "mass_cl"]) + if load_fitted: + fits = numpy.load(paths.structfit_path(self.nsnap, nsim, "clumps")) + cols = [col for col in fits.dtype.names if col != "index"] + X = [fits[col] for col in cols] + self._data = add_columns(self._data, X, cols) - mask = numpy.sqrt(data['x']**2 + data['y']**2 + data['z']**2) < maxdist - mask &= data[minmass[0]] > minmass[1] - self._data = data[mask] + # If the raw data is not required, then start applying transformations + # and cuts. + if not rawdata: + flip_cols(self._data, "x", "z") + for p in ("x", "y", "z"): + self._data[p] -= 0.5 + self._data = self.box.convert_from_boxunits( + self._data, + [ + "x", + "y", + "z", + "mass_cl", + "totpartmass", + "rho0", + "r200c", + "r500c", + "m200c", + "m500c", + "r200m", + "m200m", + ], + ) + if maxdist is not None: + dist = numpy.sqrt( + self._data["x"] ** 2 + self._data["y"] ** 2 + self._data["z"] ** 2 + ) + self._data = self._data[dist < maxdist] + if minmass is not None: + self._data = self._data[self._data[minmass[0]] > minmass[1]] @property def ismain(self): @@ -341,7 +377,6 @@ class HaloCatalogue(BaseCatalogue): TODO: Add the fitted quantities - Add threshold on number of particles Parameters ---------- @@ -356,20 +391,32 @@ class HaloCatalogue(BaseCatalogue): minmass : len-2 tuple Minimum mass. The first element is the catalogue key and the second is the value. + rawdata : bool, optional + Whether to return the raw data. In this case applies no cuts and + transformations. """ - def __init__(self, nsim, paths, maxdist=155.5 / 0.705, - minmass=('M', 1e12)): + + def __init__( + self, nsim, paths, maxdist=155.5 / 0.705, minmass=("M", 1e12), rawdata=False + ): self.nsim = nsim self.paths = paths # Read in the mmain catalogue of summed substructure mmain = numpy.load(self.paths.mmain_path(self.nsnap, self.nsim)) - data = mmain["mmain"] - # Flip positions and convert from code units to cMpc. Convert M too - flip_cols(data, "x", "z") - for p in ('x', 'y', 'z'): - data[p] -= 0.5 - data = self.box.convert_from_boxunits(data, ['x', 'y', 'z', 'M']) + self._data = mmain["mmain"] + if not rawdata: + # Flip positions and convert from code units to cMpc. Convert M too + flip_cols(self._data, "x", "z") + for p in ("x", "y", "z"): + self._data[p] -= 0.5 + self._data = self.box.convert_from_boxunits( + self._data, ["x", "y", "z", "M"] + ) - mask = numpy.sqrt(data['x']**2 + data['y']**2 + data['z']**2) < maxdist - mask &= data[minmass[0]] > minmass[1] - self._data = data[mask] + if maxdist is not None: + dist = numpy.sqrt( + self._data["x"] ** 2 + self._data["y"] ** 2 + self._data["z"] ** 2 + ) + self._data = self._data[dist < maxdist] + if minmass is not None: + self._data = self._data[self._data[minmass[0]] > minmass[1]] diff --git a/csiborgtools/read/paths.py b/csiborgtools/read/paths.py index c347285..6ccc72a 100644 --- a/csiborgtools/read/paths.py +++ b/csiborgtools/read/paths.py @@ -23,15 +23,16 @@ import numpy class CSiBORGPaths: """ - Paths manager for CSiBORG IC realisations. + Paths manager for CSiBORG IC realisations. - Parameters - ---------- - srcdir : str - Path to the folder where the RAMSES outputs are stored. - postdir: str - Path to the folder where post-processed files are stored. + Parameters + ---------- + srcdir : str + Path to the folder where the RAMSES outputs are stored. + postdir: str + Path to the folder where post-processed files are stored. """ + _srcdir = None _postdir = None @@ -96,8 +97,7 @@ class CSiBORGPaths: fpath = join(self.postdir, "temp") if not isdir(fpath): mkdir(fpath) - warn("Created directory `{}`.".format(fpath), UserWarning, - stacklevel=1) + warn("Created directory `{}`.".format(fpath), UserWarning, stacklevel=1) return fpath def mmain_path(self, nsnap, nsim): @@ -118,12 +118,10 @@ class CSiBORGPaths: fdir = join(self.postdir, "mmain") if not isdir(fdir): mkdir(fdir) - warn("Created directory `{}`.".format(fdir), UserWarning, - stacklevel=1) + warn("Created directory `{}`.".format(fdir), UserWarning, stacklevel=1) return join( - fdir, - "mmain_{}_{}.npz".format(str(nsim).zfill(5), str(nsnap).zfill(5)) - ) + fdir, "mmain_{}_{}.npz".format(str(nsim).zfill(5), str(nsnap).zfill(5)) + ) def initmatch_path(self, nsim, kind): """ @@ -145,8 +143,7 @@ class CSiBORGPaths: fdir = join(self.postdir, "initmatch") if not isdir(fdir): mkdir(fdir) - warn("Created directory `{}`.".format(fdir), UserWarning, - stacklevel=1) + warn("Created directory `{}`.".format(fdir), UserWarning, stacklevel=1) return join(fdir, "{}_{}.npy".format(kind, str(nsim).zfill(5))) def split_path(self, nsnap, nsim): @@ -167,10 +164,10 @@ class CSiBORGPaths: fdir = join(self.postdir, "split") if not isdir(fdir): mkdir(fdir) - warn("Created directory `{}`.".format(fdir), UserWarning, - stacklevel=1) - return join(fdir, "clumps_{}_{}.npz" - .format(str(nsim).zfill(5), str(nsnap).zfill(5))) + warn("Created directory `{}`.".format(fdir), UserWarning, stacklevel=1) + return join( + fdir, "clumps_{}_{}.npz".format(str(nsim).zfill(5), str(nsnap).zfill(5)) + ) def get_ics(self, tonew): """ @@ -194,7 +191,7 @@ class CSiBORGPaths: else: files = [f for f in files if "_inv" not in f] # Remove inv. ICs files = [f for f in files if "_new" not in f] # Remove _new - files = [f for f in files if "OLD" not in f] # Remove _old + files = [f for f in files if "OLD" not in f] # Remove _old ids = [int(f.split("_")[-1]) for f in files] try: ids.remove(5511) @@ -239,7 +236,7 @@ class CSiBORGPaths: # Get all files in simpath that start with output_ snaps = glob(join(simpath, "output_*")) # Take just the last _00XXXX from each file and strip zeros - snaps = [int(snap.split('_')[-1].lstrip('0')) for snap in snaps] + snaps = [int(snap.split("_")[-1].lstrip("0")) for snap in snaps] return numpy.sort(snaps) def snapshot_path(self, nsnap, nsim): @@ -261,22 +258,31 @@ class CSiBORGPaths: simpath = self.ic_path(nsim, tonew=tonew) return join(simpath, "output_{}".format(str(nsnap).zfill(5))) - def hcat_path(self, nsim): + def structfit_path(self, nsnap, nsim, kind): """ - Path to the final snapshot halo catalogue from `fit_halos.py`. + Path to the clump or halo catalogue from `fit_halos.py`. Parameters ---------- + nsnap : int + Snapshot index. nsim : int IC realisation index. + kind : str + Type of catalogue. Can be either `clumps` or `halos`. Returns ------- path : str """ - nsnap = str(max(self.get_snapshots(nsim))).zfill(5) - fname = "ramses_out_{}_{}.npy".format(str(self.nsim).zfill(5), nsnap) - return join(self.postdir, fname) + assert kind in ["clumps", "halos"] + fdir = join(self.postdir, "structfit") + if not isdir(fdir): + mkdir(fdir) + warn("Created directory `{}`.".format(fdir), UserWarning, stacklevel=1) + + fname = "{}_out_{}_{}.npy".format(kind, str(nsim).zfill(5), str(nsnap).zfill(5)) + return join(fdir, fname) def knnauto_path(self, run, nsim=None): """ @@ -297,8 +303,7 @@ class CSiBORGPaths: fdir = join(self.postdir, "knn", "auto") if not isdir(fdir): makedirs(fdir) - warn("Created directory `{}`.".format(fdir), UserWarning, - stacklevel=1) + warn("Created directory `{}`.".format(fdir), UserWarning, stacklevel=1) if nsim is not None: return join(fdir, "knncdf_{}_{}.p".format(str(nsim).zfill(5), run)) @@ -325,8 +330,7 @@ class CSiBORGPaths: fdir = join(self.postdir, "knn", "cross") if not isdir(fdir): makedirs(fdir) - warn("Created directory `{}`.".format(fdir), UserWarning, - stacklevel=1) + warn("Created directory `{}`.".format(fdir), UserWarning, stacklevel=1) if nsims is not None: assert isinstance(nsims, (list, tuple)) and len(nsims) == 2 nsim0 = str(nsims[0]).zfill(5) @@ -356,8 +360,7 @@ class CSiBORGPaths: fdir = join(self.postdir, "tpcf", "auto") if not isdir(fdir): makedirs(fdir) - warn("Created directory `{}`.".format(fdir), UserWarning, - stacklevel=1) + warn("Created directory `{}`.".format(fdir), UserWarning, stacklevel=1) if nsim is not None: return join(fdir, "tpcf{}_{}.p".format(str(nsim).zfill(5), run)) diff --git a/csiborgtools/read/utils.py b/csiborgtools/read/utils.py index d64db77..f21bf95 100644 --- a/csiborgtools/read/utils.py +++ b/csiborgtools/read/utils.py @@ -41,7 +41,7 @@ def cartesian_to_radec(X, indeg=True): """ x, y, z = X[:, 0], X[:, 1], X[:, 2] dist = numpy.linalg.norm(X, axis=1) - dec = numpy.arcsin(z/dist) + dec = numpy.arcsin(z / dist) ra = numpy.arctan2(y, x) ra[ra < 0] += 2 * numpy.pi # Wrap RA to [0, 2pi) if indeg: @@ -101,8 +101,7 @@ def cols_to_structured(N, cols): if not isinstance(cols, list) and all(isinstance(c, tuple) for c in cols): raise TypeError("`cols` must be a list of tuples.") - dtype = {"names": [col[0] for col in cols], - "formats": [col[1] for col in cols]} + dtype = {"names": [col[0] for col in cols], "formats": [col[1] for col in cols]} return numpy.full(N, numpy.nan, dtype=dtype) @@ -237,8 +236,9 @@ def array_to_structured(arr, cols): """ cols = [cols] if isinstance(cols, str) else cols if arr.ndim != 2 and arr.shape[1] != len(cols): - raise TypeError("`arr` must be a 2-dimensional array of " - "shape `(n_samples, n_cols)`.") + raise TypeError( + "`arr` must be a 2-dimensional array of " "shape `(n_samples, n_cols)`." + ) dtype = {"names": cols, "formats": [arr.dtype] * len(cols)} out = numpy.full(arr.shape[0], numpy.nan, dtype=dtype) @@ -299,5 +299,7 @@ def extract_from_structured(arr, cols): out[:, i] = arr[col] # Optionally flatten if len(cols) == 1: - return out.reshape(-1,) + return out.reshape( + -1, + ) return out diff --git a/notebooks/fitting.ipynb b/notebooks/fitting.ipynb new file mode 100644 index 0000000..5bc8135 --- /dev/null +++ b/notebooks/fitting.ipynb @@ -0,0 +1,140 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "5a38ed25", + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-12T14:25:46.519408Z", + "start_time": "2023-04-12T14:25:03.003304Z" + }, + "scrolled": true + }, + "outputs": [], + "source": [ + "import sys\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "sys.path.append(\"../\")\n", + "import csiborgtools\n", + "\n", + "%matplotlib widget \n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "06a5b2ff", + "metadata": {}, + "outputs": [], + "source": [ + "paths = csiborgtools.read.CSiBORGPaths(**csiborgtools.paths_glamdring)\n", + "partreader = csiborgtools.read.ParticleReader(paths)\n", + "\n", + "\n", + "nsim = 7444\n", + "nsnap = max(paths.get_snapshots(nsim))\n", + "box = csiborgtools.read.BoxUnits(nsnap, nsim, paths)\n", + "\n", + "particle_archive = np.load(paths.split_path(nsnap, nsim))\n", + "clumpsarr = partreader.read_clumps(nsnap, nsim, cols=[\"index\", 'x', 'y', 'z'])\n", + "clumpid2arrpos = {ind: ii for ii, ind in enumerate(clumpsarr[\"index\"])}" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "382c5341", + "metadata": {}, + "outputs": [], + "source": [ + "nsim = 7492\n", + "cat = csiborgtools.read.ClumpsCatalogue(nsim, paths)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "b1a84840", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5350561d0be1413d98e3227df70d0e18", + "version_major": 2, + "version_minor": 0 + }, + "image/png": "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", + "text/html": [ + "\n", + "
\n", + "
\n", + " Figure\n", + "
\n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ismain = cat.ismain\n", + "\n", + "plt.figure()\n", + "plt.scatter(cat['totpartmass'][ismain], cat['mass_cl'][ismain], s=3, label=\"Ultimate parent halos\")\n", + "plt.scatter(cat['totpartmass'][~ismain], cat['mass_cl'][~ismain], s=3, label=\"Not ultimate parent halos\")\n", + "\n", + "\n", + "t = np.linspace(1e12, 1e15, 1000)\n", + "plt.plot(t, t, c=\"red\", ls=\"--\")\n", + "plt.xlabel(\"Total particle mass\")\n", + "plt.ylabel(\"Clump pass\")\n", + "plt.legend()\n", + "plt.xscale(\"log\")\n", + "plt.yscale(\"log\")\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d9ac81d", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv_galomatch", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/scripts/cluster_knn_auto.py b/scripts/cluster_knn_auto.py index 8f5f2de..f87e567 100644 --- a/scripts/cluster_knn_auto.py +++ b/scripts/cluster_knn_auto.py @@ -29,6 +29,7 @@ try: import csiborgtools except ModuleNotFoundError: import sys + sys.path.append("../") import csiborgtools @@ -43,23 +44,13 @@ nproc = comm.Get_size() parser = ArgumentParser() parser.add_argument("--runs", type=str, nargs="+") args = parser.parse_args() -with open('../scripts/knn_auto.yml', 'r') as file: +with open("../scripts/knn_auto.yml", "r") as file: config = yaml.safe_load(file) Rmax = 155 / 0.705 # Mpc (h = 0.705) high resolution region radius totvol = 4 * numpy.pi * Rmax**3 / 3 -minmass = 1e12 -ics = [7444, 7468, 7492, 7516, 7540, 7564, 7588, 7612, 7636, 7660, 7684, - 7708, 7732, 7756, 7780, 7804, 7828, 7852, 7876, 7900, 7924, 7948, - 7972, 7996, 8020, 8044, 8068, 8092, 8116, 8140, 8164, 8188, 8212, - 8236, 8260, 8284, 8308, 8332, 8356, 8380, 8404, 8428, 8452, 8476, - 8500, 8524, 8548, 8572, 8596, 8620, 8644, 8668, 8692, 8716, 8740, - 8764, 8788, 8812, 8836, 8860, 8884, 8908, 8932, 8956, 8980, 9004, - 9028, 9052, 9076, 9100, 9124, 9148, 9172, 9196, 9220, 9244, 9268, - 9292, 9316, 9340, 9364, 9388, 9412, 9436, 9460, 9484, 9508, 9532, - 9556, 9580, 9604, 9628, 9652, 9676, 9700, 9724, 9748, 9772, 9796, - 9820, 9844] paths = csiborgtools.read.CSiBORGPaths(**csiborgtools.paths_glamdring) +ics = paths.get_ics(False) knncdf = csiborgtools.clustering.kNN_CDF() ############################################################################### @@ -75,9 +66,9 @@ def read_single(selection, cat): psel = selection["primary"] pmin, pmax = psel.get("min", None), psel.get("max", None) if pmin is not None: - mmask &= (cat[psel["name"]] >= pmin) + mmask &= cat[psel["name"]] >= pmin if pmax is not None: - mmask &= (cat[psel["name"]] < pmax) + mmask &= cat[psel["name"]] < pmax pos = pos[mmask, ...] # Secondary selection @@ -92,12 +83,13 @@ def read_single(selection, cat): if ssel.get("marked", True): x = cat[psel["name"]][mmask] prop = csiborgtools.clustering.normalised_marks( - x, prop, nbins=config["nbins_marks"]) + x, prop, nbins=config["nbins_marks"] + ) if smin is not None: - smask &= (prop >= smin) + smask &= prop >= smin if smax is not None: - smask &= (prop < smax) + smask &= prop < smax return pos[smask, ...] @@ -106,8 +98,7 @@ def do_auto(run, cat, ic): """Calculate the kNN-CDF single catalgoue autocorrelation.""" _config = config.get(run, None) if _config is None: - warn("No configuration for run {}.".format(run), UserWarning, - stacklevel=1) + warn("No configuration for run {}.".format(run), UserWarning, stacklevel=1) return rvs_gen = csiborgtools.clustering.RVSinsphere(Rmax) @@ -115,21 +106,28 @@ def do_auto(run, cat, ic): knn = NearestNeighbors() knn.fit(pos) rs, cdf = knncdf( - knn, rvs_gen=rvs_gen, nneighbours=config["nneighbours"], - rmin=config["rmin"], rmax=config["rmax"], - nsamples=int(config["nsamples"]), neval=int(config["neval"]), - batch_size=int(config["batch_size"]), random_state=config["seed"]) + knn, + rvs_gen=rvs_gen, + nneighbours=config["nneighbours"], + rmin=config["rmin"], + rmax=config["rmax"], + nsamples=int(config["nsamples"]), + neval=int(config["neval"]), + batch_size=int(config["batch_size"]), + random_state=config["seed"], + ) - joblib.dump({"rs": rs, "cdf": cdf, "ndensity": pos.shape[0] / totvol}, - paths.knnauto_path(run, ic)) + joblib.dump( + {"rs": rs, "cdf": cdf, "ndensity": pos.shape[0] / totvol}, + paths.knnauto_path(run, ic), + ) def do_cross_rand(run, cat, ic): """Calculate the kNN-CDF cross catalogue random correlation.""" _config = config.get(run, None) if _config is None: - warn("No configuration for run {}.".format(run), UserWarning, - stacklevel=1) + warn("No configuration for run {}.".format(run), UserWarning, stacklevel=1) return rvs_gen = csiborgtools.clustering.RVSinsphere(Rmax) @@ -142,10 +140,17 @@ def do_cross_rand(run, cat, ic): knn2.fit(pos2) rs, cdf0, cdf1, joint_cdf = knncdf.joint( - knn1, knn2, rvs_gen=rvs_gen, nneighbours=int(config["nneighbours"]), - rmin=config["rmin"], rmax=config["rmax"], - nsamples=int(config["nsamples"]), neval=int(config["neval"]), - batch_size=int(config["batch_size"]), random_state=config["seed"]) + knn1, + knn2, + rvs_gen=rvs_gen, + nneighbours=int(config["nneighbours"]), + rmin=config["rmin"], + rmax=config["rmax"], + nsamples=int(config["nsamples"]), + neval=int(config["neval"]), + batch_size=int(config["batch_size"]), + random_state=config["seed"], + ) corr = knncdf.joint_to_corr(cdf0, cdf1, joint_cdf) joblib.dump({"rs": rs, "corr": corr}, paths.knnauto_path(run, ic)) @@ -180,4 +185,4 @@ comm.Barrier() if rank == 0: print("{}: all finished.".format(datetime.now())) -quit() # Force quit the script \ No newline at end of file +quit() # Force quit the script diff --git a/scripts/cluster_knn_cross.py b/scripts/cluster_knn_cross.py index 43b22e3..23ebafe 100644 --- a/scripts/cluster_knn_cross.py +++ b/scripts/cluster_knn_cross.py @@ -16,7 +16,6 @@ from argparse import ArgumentParser from datetime import datetime from itertools import combinations -from os.path import join from warnings import warn import joblib @@ -30,6 +29,7 @@ try: import csiborgtools except ModuleNotFoundError: import sys + sys.path.append("../") import csiborgtools @@ -44,24 +44,12 @@ nproc = comm.Get_size() parser = ArgumentParser() parser.add_argument("--runs", type=str, nargs="+") args = parser.parse_args() -with open('../scripts/knn_cross.yml', 'r') as file: +with open("../scripts/knn_cross.yml", "r") as file: config = yaml.safe_load(file) Rmax = 155 / 0.705 # Mpc (h = 0.705) high resolution region radius -minmass = 1e12 -ics = [7444, 7468, 7492, 7516, 7540, 7564, 7588, 7612, 7636, 7660, 7684, - 7708, 7732, 7756, 7780, 7804, 7828, 7852, 7876, 7900, 7924, 7948, - 7972, 7996, 8020, 8044, 8068, 8092, 8116, 8140, 8164, 8188, 8212, - 8236, 8260, 8284, 8308, 8332, 8356, 8380, 8404, 8428, 8452, 8476, - 8500, 8524, 8548, 8572, 8596, 8620, 8644, 8668, 8692, 8716, 8740, - 8764, 8788, 8812, 8836, 8860, 8884, 8908, 8932, 8956, 8980, 9004, - 9028, 9052, 9076, 9100, 9124, 9148, 9172, 9196, 9220, 9244, 9268, - 9292, 9316, 9340, 9364, 9388, 9412, 9436, 9460, 9484, 9508, 9532, - 9556, 9580, 9604, 9628, 9652, 9676, 9700, 9724, 9748, 9772, 9796, - 9820, 9844] paths = csiborgtools.read.CSiBORGPaths(**csiborgtools.paths_glamdring) -dumpdir = "/mnt/extraspace/rstiskalek/csiborg/knn" -fout = join(dumpdir, "cross", "knncdf_{}_{}_{}.p") +ics = paths.get_ics(False) knncdf = csiborgtools.clustering.kNN_CDF() ############################################################################### @@ -76,9 +64,9 @@ def read_single(selection, cat): psel = selection["primary"] pmin, pmax = psel.get("min", None), psel.get("max", None) if pmin is not None: - mmask &= (cat[psel["name"]] >= pmin) + mmask &= cat[psel["name"]] >= pmin if pmax is not None: - mmask &= (cat[psel["name"]] < pmax) + mmask &= cat[psel["name"]] < pmax return pos[mmask, ...] @@ -99,10 +87,17 @@ def do_cross(run, ics): knn2.fit(pos2) rs, cdf0, cdf1, joint_cdf = knncdf.joint( - knn1, knn2, rvs_gen=rvs_gen, nneighbours=int(config["nneighbours"]), - rmin=config["rmin"], rmax=config["rmax"], - nsamples=int(config["nsamples"]), neval=int(config["neval"]), - batch_size=int(config["batch_size"]), random_state=config["seed"]) + knn1, + knn2, + rvs_gen=rvs_gen, + nneighbours=int(config["nneighbours"]), + rmin=config["rmin"], + rmax=config["rmax"], + nsamples=int(config["nsamples"]), + neval=int(config["neval"]), + batch_size=int(config["batch_size"]), + random_state=config["seed"], + ) corr = knncdf.joint_to_corr(cdf0, cdf1, joint_cdf) joblib.dump({"rs": rs, "corr": corr}, paths.knncross_path(run, ics)) @@ -135,4 +130,4 @@ comm.Barrier() if rank == 0: print("{}: all finished.".format(datetime.now())) -quit() # Force quit the script \ No newline at end of file +quit() # Force quit the script diff --git a/scripts/cluster_tcpf_auto.py b/scripts/cluster_tcpf_auto.py index c4397fa..91d9ad9 100644 --- a/scripts/cluster_tcpf_auto.py +++ b/scripts/cluster_tcpf_auto.py @@ -16,7 +16,6 @@ from argparse import ArgumentParser from copy import deepcopy from datetime import datetime -from os.path import join from warnings import warn import joblib @@ -29,6 +28,7 @@ try: import csiborgtools except ModuleNotFoundError: import sys + sys.path.append("../") import csiborgtools @@ -43,24 +43,12 @@ nproc = comm.Get_size() parser = ArgumentParser() parser.add_argument("--runs", type=str, nargs="+") args = parser.parse_args() -with open('../scripts/tpcf_auto.yml', 'r') as file: +with open("../scripts/tpcf_auto.yml", "r") as file: config = yaml.safe_load(file) Rmax = 155 / 0.705 # Mpc (h = 0.705) high resolution region radius -minmass = 1e12 -ics = [7444, 7468, 7492, 7516, 7540, 7564, 7588, 7612, 7636, 7660, 7684, - 7708, 7732, 7756, 7780, 7804, 7828, 7852, 7876, 7900, 7924, 7948, - 7972, 7996, 8020, 8044, 8068, 8092, 8116, 8140, 8164, 8188, 8212, - 8236, 8260, 8284, 8308, 8332, 8356, 8380, 8404, 8428, 8452, 8476, - 8500, 8524, 8548, 8572, 8596, 8620, 8644, 8668, 8692, 8716, 8740, - 8764, 8788, 8812, 8836, 8860, 8884, 8908, 8932, 8956, 8980, 9004, - 9028, 9052, 9076, 9100, 9124, 9148, 9172, 9196, 9220, 9244, 9268, - 9292, 9316, 9340, 9364, 9388, 9412, 9436, 9460, 9484, 9508, 9532, - 9556, 9580, 9604, 9628, 9652, 9676, 9700, 9724, 9748, 9772, 9796, - 9820, 9844] -dumpdir = "/mnt/extraspace/rstiskalek/csiborg/tpcf" -fout = join(dumpdir, "auto", "tpcf_{}_{}.p") paths = csiborgtools.read.CSiBORGPaths() +ics = paths.get_ics(False) tpcf = csiborgtools.clustering.Mock2PCF() ############################################################################### @@ -76,9 +64,9 @@ def read_single(selection, cat): psel = selection["primary"] pmin, pmax = psel.get("min", None), psel.get("max", None) if pmin is not None: - mmask &= (cat[psel["name"]] >= pmin) + mmask &= cat[psel["name"]] >= pmin if pmax is not None: - mmask &= (cat[psel["name"]] < pmax) + mmask &= cat[psel["name"]] < pmax pos = pos[mmask, ...] # Secondary selection @@ -93,12 +81,13 @@ def read_single(selection, cat): if ssel.get("marked", True): x = cat[psel["name"]][mmask] prop = csiborgtools.clustering.normalised_marks( - x, prop, nbins=config["nbins_marks"]) + x, prop, nbins=config["nbins_marks"] + ) if smin is not None: - smask &= (prop >= smin) + smask &= prop >= smin if smax is not None: - smask &= (prop < smax) + smask &= prop < smax return pos[smask, ...] @@ -110,8 +99,11 @@ def do_auto(run, cat, ic): return rvs_gen = csiborgtools.clustering.RVSinsphere(Rmax) - bins = numpy.logspace(numpy.log10(config["rpmin"]), - numpy.log10(config["rpmax"]), config["nrpbins"] + 1) + bins = numpy.logspace( + numpy.log10(config["rpmin"]), + numpy.log10(config["rpmax"]), + config["nrpbins"] + 1, + ) pos = read_single(_config, cat) nrandom = int(config["randmult"] * pos.shape[0]) rp, wp = tpcf(pos, rvs_gen, nrandom, bins) @@ -146,4 +138,4 @@ comm.Barrier() if rank == 0: print("{}: all finished.".format(datetime.now())) -quit() # Force quit the script \ No newline at end of file +quit() # Force quit the script diff --git a/scripts/pre_fithalos.py b/scripts/pre_fithalos.py index 4247377..33936ff 100644 --- a/scripts/pre_fithalos.py +++ b/scripts/pre_fithalos.py @@ -16,18 +16,26 @@ A script to fit halos (concentration, ...). The particle array of each CSiBORG realisation must have been split in advance by `runsplit_halos`. """ +from argparse import ArgumentParser from datetime import datetime +from os.path import join import numpy from mpi4py import MPI +from tqdm import tqdm try: import csiborgtools except ModuleNotFoundError: import sys + sys.path.append("../") import csiborgtools +parser = ArgumentParser() +parser.add_argument("--kind", type=str, choices=["halos", "clumps"]) +args = parser.parse_args() + # Get MPI things comm = MPI.COMM_WORLD @@ -35,128 +43,170 @@ rank = comm.Get_rank() nproc = comm.Get_size() paths = csiborgtools.read.CSiBORGPaths(**csiborgtools.paths_glamdring) -partreader =csiborgtools.read.ParticleReader(paths) - -cols_collect = [("npart", numpy.int64), ("totpartmass", numpy.float64), - ("Rs", numpy.float64), ("vx", numpy.float64), - ("vy", numpy.float64), ("vz", numpy.float64), - ("Lx", numpy.float64), ("Ly", numpy.float64), - ("Lz", numpy.float64), ("rho0", numpy.float64), - ("conc", numpy.float64), ("rmin", numpy.float64), - ("rmax", numpy.float64), ("r200", numpy.float64), - ("r500", numpy.float64), ("m200", numpy.float64), - ("m500", numpy.float64), ("lambda200c", numpy.float64)] - -def fit_clump(particles, clump, box): +partreader = csiborgtools.read.ParticleReader(paths) +nfwpost = csiborgtools.fits.NFWPosterior() +ftemp = join(paths.temp_dumpdir, "fit_clump_{}_{}_{}.npy") +cols_collect = [ + ("index", numpy.int32), + ("npart", numpy.int32), + ("totpartmass", numpy.float32), + ("vx", numpy.float32), + ("vy", numpy.float32), + ("vz", numpy.float32), + ("conc", numpy.float32), + ("rho0", numpy.float32), + ("r200c", numpy.float32), + ("r500c", numpy.float32), + ("m200c", numpy.float32), + ("m500c", numpy.float32), + ("lambda200c", numpy.float32), + ("r200m", numpy.float32), + ("m200m", numpy.float32), +] +def fit_clump(particles, clump_info, box): + """ + Fit an object. Can be eithe a clump or a parent halo. + """ + obj = csiborgtools.fits.Clump(particles, clump_info, box) + + out = {} + if numpy.isnan(clump_info["index"]): + print("Why am I NaN?", flush=True) + out["index"] = clump_info["index"] + out["npart"] = len(obj) + out["totpartmass"] = numpy.sum(obj["M"]) + for i, v in enumerate(["vx", "vy", "vz"]): + out[v] = numpy.average(obj.vel[:, i], weights=obj["M"]) + # Overdensity masses + out["r200c"], out["m200c"] = obj.spherical_overdensity_mass(200, kind="crit") + out["r500c"], out["m500c"] = obj.spherical_overdensity_mass(500, kind="crit") + out["r200m"], out["m200m"] = obj.spherical_overdensity_mass(200, kind="matter") + # NFW fit + if out["npart"] > 10 and numpy.isfinite(out["r200c"]): + Rs, rho0 = nfwpost.fit(obj) + out["conc"] = Rs / out["r200c"] + out["rho0"] = rho0 + # Spin within R200c + if numpy.isfinite(out["r200c"]): + out["lambda200c"] = obj.lambda_bullock(out["r200c"]) + return out - out["npart"][n] = clump.Npart - out["rmin"][n] = clump.rmin - out["rmax"][n] = clump.rmax - out["totpartmass"][n] = clump.total_particle_mass - out["vx"][n] = numpy.average(clump.vel[:, 0], weights=clump.m) - out["vy"][n] = numpy.average(clump.vel[:, 1], weights=clump.m) - out["vz"][n] = numpy.average(clump.vel[:, 2], weights=clump.m) - out["Lx"][n], out["Ly"][n], out["Lz"][n] = clump.angular_momentum +def load_clump_particles(clumpid, particle_archive): + """ + Load a clump's particles from the particle archive. If it is not there, i.e + clump has no associated particles, return `None`. + """ + try: + part = particle_archive[str(clumpid)] + except KeyError: + part = None + return part +def load_parent_particles(clumpid, particle_archive, clumps_cat): + """ + Load a parent halo's particles. + """ + indxs = clumps_cat["index"][clumps_cat["parent"] == clumpid] + # We first load the particles of each clump belonging to this parent and then + # concatenate them for further analysis. + clumps = [] + for ind in indxs: + parts = load_clump_particles(ind, particle_archive) + if parts is not None: + clumps.append([parts, None]) + if len(clumps) == 0: + return None + return csiborgtools.match.concatenate_clumps(clumps, include_velocities=True) + + +# We now start looping over all simulations for i, nsim in enumerate(paths.get_ics(tonew=False)): if rank == 0: - print("{}: calculating {}th simulation `{}`." - .format(datetime.now(), i, nsim), flush=True) + print( + "{}: calculating {}th simulation `{}`.".format(datetime.now(), i, nsim), + flush=True, + ) nsnap = max(paths.get_snapshots(nsim)) box = csiborgtools.read.BoxUnits(nsnap, nsim, paths) # Archive of clumps, keywords are their clump IDs - particle_archive = paths.split_path(nsnap, nsim) - clumpsarr = partreader.read_clumps(nsnap, nsim, - cols=["index", 'x', 'y', 'z']) - clumpid2arrpos = {ind: ii for ii, ind in enumerate(clumpsarr["index"])} + particle_archive = numpy.load(paths.split_path(nsnap, nsim)) + clumps_cat = csiborgtools.read.ClumpsCatalogue( + nsim, paths, maxdist=None, minmass=None, rawdata=True, load_fitted=False + ) + # We check whether we fit halos or clumps, will be indexing over different + # iterators. + if args.kind == "halos": + ismain = clumps_cat.ismain + else: + ismain = numpy.ones(len(clumps_cat), dtype=bool) + ntasks = len(clumps_cat) + # We split the clumps among the processes. Each CPU calculates a fraction + # of them and dumps the results in a structured array. Even if we are + # calculating parent halo this index runs over all clumps. + jobs = csiborgtools.fits.split_jobs(ntasks, nproc)[rank] + out = csiborgtools.read.cols_to_structured(len(jobs), cols_collect) + for i, j in enumerate(tqdm(jobs)) if nproc == 1 else enumerate(jobs): + # If we are fitting halos and this clump is not a main, then continue. + if args.kind == "halos" and not ismain[j]: + continue + clumpid = clumps_cat["index"][j] + if args.kind == "halos": + part = load_parent_particles(clumpid, particle_archive, clumps_cat) + else: + part = load_clump_particles(clumpid, particle_archive) - nclumps = len(particle_archive.files) - # Fit 5000 clumps at a time, then dump results - batchsize = 5000 + # We fit the particles if there are any. If not we assign the index, + # otherwise it would be NaN converted to integers (-2147483648) and + # yield an error further down. + if part is not None: + _out = fit_clump(part, clumps_cat[j], box) + for key in _out.keys(): + out[key][i] = _out[key] + else: + out["index"][i] = clumpid + out["npart"][i] = 0 - # This rank does these `batchsize` clumps/halos - jobs = csiborgtools.utils.split_jobs(nclumps, nclumps // batchsize)[rank] - for clumpid in jobs: - ... = fit_clump(particle_archive[str(clumpid)], clumpsarr[clumpid2arrpos[clumpid]]) - - - - jobs = csiborgtools.utils.split_jobs(nclumps, nproc)[rank] - for nsplit in jobs: - parts, part_clumps, clumps = csiborgtools.fits.load_split_particles( - nsplit, nsnap, nsim, paths, remove_split=False) - - N = clumps.size - cols = [("index", numpy.int64), ("npart", numpy.int64), - ("totpartmass", numpy.float64), ("Rs", numpy.float64), - ("rho0", numpy.float64), ("conc", numpy.float64), - ("lambda200c", numpy.float64), ("vx", numpy.float64), - ("vy", numpy.float64), ("vz", numpy.float64), - ("Lx", numpy.float64), ("Ly", numpy.float64), - ("Lz", numpy.float64), ("rmin", numpy.float64), - ("rmax", numpy.float64), ("r200", numpy.float64), - ("r500", numpy.float64), ("m200", numpy.float64), - ("m500", numpy.float64)] - out = csiborgtools.utils.cols_to_structured(N, cols) - out["index"] = clumps["index"] - - for n in range(N): - # Pick clump and its particles - xs = csiborgtools.fits.pick_single_clump(n, parts, part_clumps, - clumps) - clump = csiborgtools.fits.Clump.from_arrays( - *xs, rhoc=box.box_rhoc, G=box.box_G) - out["npart"][n] = clump.Npart - out["rmin"][n] = clump.rmin - out["rmax"][n] = clump.rmax - out["totpartmass"][n] = clump.total_particle_mass - out["vx"][n] = numpy.average(clump.vel[:, 0], weights=clump.m) - out["vy"][n] = numpy.average(clump.vel[:, 1], weights=clump.m) - out["vz"][n] = numpy.average(clump.vel[:, 2], weights=clump.m) - out["Lx"][n], out["Ly"][n], out["Lz"][n] = clump.angular_momentum - - # Spherical overdensity radii and masses - rs, ms = clump.spherical_overdensity_mass([200, 500]) - out["r200"][n] = rs[0] - out["r500"][n] = rs[1] - out["m200"][n] = ms[0] - out["m500"][n] = ms[1] - out["lambda200c"][n] = clump.lambda200c - - # NFW profile fit - if clump.Npart > 10 and numpy.isfinite(out["r200"][n]): - nfwpost = csiborgtools.fits.NFWPosterior(clump) - logRs, __ = nfwpost.maxpost_logRs() - Rs = 10**logRs - if not numpy.isnan(logRs): - out["Rs"][n] = Rs - out["rho0"][n] = nfwpost.rho0_from_Rs(Rs) - out["conc"][n] = out["r200"][n] / Rs - - csiborgtools.read.dump_split(out, nsplit, nsnap, nsim, paths) - - # Wait until all jobs finished before moving to another simulation + fout = ftemp.format(str(nsim).zfill(5), str(nsnap).zfill(5), rank) + if nproc == 0: + print( + "{}: rank {} saving to `{}`.".format(datetime.now(), rank, fout), flush=True + ) + numpy.save(fout, out) + # We saved this CPU's results in a temporary file. Wait now for the other + # CPUs and then collect results from the 0th rank and save them. comm.Barrier() -# # Use the rank 0 to combine outputs for this CSiBORG realisation -# if rank == 0: -# print("Collecting results!") -# partreader = csiborgtools.read.ParticleReader(paths) -# out_collected = csiborgtools.read.combine_splits( -# utils.Nsplits, nsnap, nsim, partreader, cols_collect, -# remove_splits=True, verbose=False) -# fname = paths.hcat_path(nsim) -# print("Saving results to `{}`.".format(fname)) -# numpy.save(fname, out_collected) -# -# comm.Barrier() -# -# if rank == 0: -# print("All finished! See ya!") \ No newline at end of file + if rank == 0: + print( + "{}: collecting results for simulation `{}`.".format(datetime.now(), nsim), + flush=True, + ) + # We write to the output array. Load data from each CPU and append to + # the output array. + out = csiborgtools.read.cols_to_structured(ntasks, cols_collect) + clumpid2outpos = {indx: i for i, indx in enumerate(clumps_cat["index"])} + for i in range(nproc): + inp = numpy.load(ftemp.format(str(nsim).zfill(5), str(nsnap).zfill(5), i)) + for j, clumpid in enumerate(inp["index"]): + k = clumpid2outpos[clumpid] + for key in inp.dtype.names: + out[key][k] = inp[key][j] + + # If we were analysing main halos, then remove array indices that do + # not correspond to parent halos. + if args.kind == "halos": + out = out[ismain] + + fout = paths.structfit_path(nsnap, nsim, "clumps") + print("Saving to `{}`.".format(fout), flush=True) + numpy.save(fout, out) + + # We now wait before moving on to another simulation. + comm.Barrier() diff --git a/scripts/pre_splithalos.py b/scripts/pre_splithalos.py index a3cdd3a..09378e4 100644 --- a/scripts/pre_splithalos.py +++ b/scripts/pre_splithalos.py @@ -12,7 +12,10 @@ # 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. -"""Script to split particles to indivudual files according to their clump.""" +""" +Script to split particles to individual files according to their clump. This is +useful for calculating the halo properties directly from the particles. +""" from datetime import datetime from gc import collect from glob import glob @@ -28,6 +31,7 @@ try: import csiborgtools except ModuleNotFoundError: import sys + sys.path.append("../") import csiborgtools @@ -38,7 +42,7 @@ nproc = comm.Get_size() paths = csiborgtools.read.CSiBORGPaths(**csiborgtools.paths_glamdring) verbose = nproc == 1 -partcols = ['x', 'y', 'z', "vx", "vy", "vz", 'M'] +partcols = ["x", "y", "z", "vx", "vy", "vz", "M"] def do_split(nsim): @@ -46,8 +50,8 @@ def do_split(nsim): reader = csiborgtools.read.ParticleReader(paths) ftemp_base = join( paths.temp_dumpdir, - "split_{}_{}".format(str(nsim).zfill(5), str(nsnap).zfill(5)) - ) + "split_{}_{}".format(str(nsim).zfill(5), str(nsnap).zfill(5)), + ) ftemp = ftemp_base + "_{}.npz" # Load the particles and their clump IDs @@ -85,7 +89,7 @@ def do_split(nsim): # Now load back in every temporary file, combine them into a single # dictionary and save as a single .npz file. out = {} - for file in glob(ftemp_base + '*'): + for file in glob(ftemp_base + "*"): inp = numpy.load(file) for key in inp.files: out.update({key: inp[key]}) @@ -107,9 +111,8 @@ if nproc > 1: worker_process(do_split, comm, verbose=False) else: tasks = paths.get_ics(tonew=False) - tasks = [tasks[0]] # REMOVE for task in tasks: print("{}: completing task `{}`.".format(datetime.now(), task)) do_split(task) -comm.Barrier() \ No newline at end of file +comm.Barrier()