mirror of
https://github.com/Richard-Sti/csiborgtools.git
synced 2024-12-22 22:08:02 +00:00
JAX and fix conc (#6)
* change to log10 initlogRs * add uncertainty * add check if hessian negative * update TODO * update TODO * output the error too * save e_logRs * add concentration calculation * calcul concentration * missed comma in a list * Update TODO * fix bug * add box units and pretty status * make uncertainty optional * add BIC function * remove BIC again * add new overdensity calculation * change defualt values to max dist and mass * change to r200 * new halo find * speed up the calculation * add commented fucn * update TODO * add check whether too close to outside boundary * Update TODO * extract velocity as well * calculate m200 and m500 * update nb * update TODO
This commit is contained in:
parent
ee84c12a55
commit
7f58b1f78c
6 changed files with 278 additions and 594 deletions
12
README.md
12
README.md
|
@ -1,11 +1,12 @@
|
||||||
# CSiBORG tools
|
# CSiBORG tools
|
||||||
|
|
||||||
## :scroll: Short-term TODO
|
## :scroll: Short-term TODO
|
||||||
- [x] Calculate $M_{\rm vir}, R_{\rm vir}, c$ from $R_s, \rho_0, \ldots$
|
- [ ] Calculate mass functions
|
||||||
- [x] In `NFWPosterior` correct for the radius in which particles are fitted.
|
- [ ] Add functions for converting the output file to box units.
|
||||||
- [x] Calculate $M_{\rm 500c}$ by sphere shrinking
|
- [ ] Calculate catalogues for all realisations.
|
||||||
- [x] Change to log10 of the scale factor
|
- [x] Verify the bulk flow of particles and the clump
|
||||||
- [ ] Calculate uncertainty on $R_{\rm s}$, switch to `JAX` and get gradients.
|
- [x] Check why for some halos $M_{500c} > M_{200c}$
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## :hourglass: Long-term TODO
|
## :hourglass: Long-term TODO
|
||||||
|
@ -14,3 +15,4 @@
|
||||||
|
|
||||||
|
|
||||||
## :bulb: Open questions
|
## :bulb: Open questions
|
||||||
|
- Propagate uncertainty of $\log R_{\rm s}$ to concentration
|
|
@ -18,7 +18,6 @@ Tools for splitting the particles and a clump object.
|
||||||
|
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from scipy.optimize import minimize_scalar
|
|
||||||
from os import remove
|
from os import remove
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
from os.path import join
|
from os.path import join
|
||||||
|
@ -467,7 +466,7 @@ class Clump:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rhoc(self):
|
def rhoc(self):
|
||||||
"""
|
r"""
|
||||||
The critical density :math:`\rho_c` at this snapshot in box units.
|
The critical density :math:`\rho_c` at this snapshot in box units.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
|
@ -510,7 +509,7 @@ class Clump:
|
||||||
"""
|
"""
|
||||||
return numpy.mean(self.pos + self.clump_pos, axis=0)
|
return numpy.mean(self.pos + self.clump_pos, axis=0)
|
||||||
|
|
||||||
def enclosed_spherical_mass(self, rmax, rmin=None):
|
def enclosed_spherical_mass(self, rmax, rmin=0):
|
||||||
"""
|
"""
|
||||||
The enclosed spherical mass between two radii. All quantities remain
|
The enclosed spherical mass between two radii. All quantities remain
|
||||||
in the box units.
|
in the box units.
|
||||||
|
@ -520,152 +519,106 @@ class Clump:
|
||||||
rmax : float
|
rmax : float
|
||||||
The maximum radial distance.
|
The maximum radial distance.
|
||||||
rmin : float, optional
|
rmin : float, optional
|
||||||
The minimum radial distance. By default the radial distance of the
|
The minimum radial distance. By default 0.
|
||||||
closest particle.
|
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
M_enclosed : float
|
M_enclosed : float
|
||||||
The enclosed mass.
|
The enclosed mass.
|
||||||
"""
|
"""
|
||||||
rmin = self.rmin if rmin is None else rmin
|
|
||||||
return numpy.sum(self.m[(self.r >= rmin) & (self.r <= rmax)])
|
return numpy.sum(self.m[(self.r >= rmin) & (self.r <= rmax)])
|
||||||
|
|
||||||
def enclosed_spherical_density(self, rmax, rmin=None):
|
def enclosed_spherical_volume(self, rmax, rmin=0):
|
||||||
"""
|
"""
|
||||||
The enclosed spherical density between two radii. All quantities
|
Enclosed volume within two radii.
|
||||||
remain in box units.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
rmax : float
|
rmax : float
|
||||||
The maximum radial distance.
|
The maximum radial distance.
|
||||||
rmin : float, optional
|
rmin : float, optional
|
||||||
The minimum radial distance. By default the radial distance of the
|
The minimum radial distance. By default 0.
|
||||||
closest particle.
|
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
rho_enclosed : float
|
vol : float
|
||||||
The enclosed density.
|
The enclosed volume.
|
||||||
"""
|
"""
|
||||||
rmin = self.rmin if rmin is None else rmin
|
return 4 * numpy.pi / 3 * (rmax**3 - rmin**3)
|
||||||
M = self.enclosed_spherical_mass(rmax, rmin)
|
|
||||||
V = 4 * numpy.pi / 3 * (rmax**3 - rmin**3)
|
|
||||||
return M / V
|
|
||||||
|
|
||||||
def radius_enclosed_overdensity(self, delta):
|
def spherical_overdensity_mass(self, delta, n_particles_min=10):
|
||||||
r"""
|
r"""
|
||||||
Radius of where the mean enclosed spherical density reaches a multiple
|
Spherical overdensity mass and radius. The mass is defined as the
|
||||||
of the critical radius at a given redshift `self.rho_c`. Returns
|
enclosed mass within a radius of where the mean enclosed spherical
|
||||||
`numpy.nan` if the fit does not converge. Note that `rhoc` must be in
|
density reaches a multiple of the critical radius at a given redshift
|
||||||
box units!
|
`self.rho_c`.
|
||||||
|
|
||||||
|
Starts from the furthest particle, working its way inside the halo
|
||||||
|
through an ordered list of particles. The corresponding values is the
|
||||||
|
radial distance of the first particle whose addition sufficiently
|
||||||
|
increases the mean density.
|
||||||
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
delta : int or float
|
delta : list of int or float
|
||||||
The :math:`\delta_{\rm x}` parameters where :math:`\mathrm{x}` is
|
The :math:`\delta_{\rm x}` parameters where :math:`\mathrm{x}` is
|
||||||
the overdensity multiple.
|
the overdensity multiple.
|
||||||
|
n_particles_min : int
|
||||||
|
The minimum number of enclosed particles for a radius to be
|
||||||
|
considered trustworthy.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
rx : float
|
rx : float
|
||||||
The radius where the enclosed density reaches required value.
|
The radius where the enclosed density reaches required value.
|
||||||
|
mx : float
|
||||||
|
The corresponding spherical enclosed mass.
|
||||||
"""
|
"""
|
||||||
# Loss function to minimise
|
# If single `delta` turn to list
|
||||||
def loss(r):
|
delta = [delta] if isinstance(delta, (float, int)) else delta
|
||||||
return abs(self.enclosed_spherical_density(r, self.rmin)
|
# If given a list or tuple turn to array
|
||||||
- delta * self.rhoc)
|
_istlist = isinstance(delta, (list, tuple))
|
||||||
|
delta = numpy.asarray(delta, dtype=float) if _istlist else delta
|
||||||
|
|
||||||
res = minimize_scalar(loss, bounds=(self.rmin, self.rmax),
|
# Ordering of deltas
|
||||||
method='bounded')
|
order_delta = numpy.argsort(delta)
|
||||||
return res.x if res.success else numpy.nan
|
# Sort the particles
|
||||||
|
order_particles = numpy.argsort(self.r)[::-1]
|
||||||
|
# Density to aim for
|
||||||
|
n_delta = delta.size
|
||||||
|
target_density = delta * self.rhoc
|
||||||
|
|
||||||
@property
|
# The sum of particle masses, starting from the outside
|
||||||
def r200(self):
|
# Adds the furtherst particle ensure that the 0th index is tot mass
|
||||||
r"""
|
cummass_ordered = (self.total_particle_mass
|
||||||
The radius at which the mean spherical density reaches 200 times
|
+ self.m[order_particles][0]
|
||||||
the critical density, :math:`R_{200c}`. Returns `numpy.nan` if the
|
- numpy.cumsum(self.m[order_particles]))
|
||||||
estimate fails.
|
# Enclosed volumes at particle radii
|
||||||
|
volumes = self.enclosed_spherical_volume(self.r[order_particles])
|
||||||
|
densities = cummass_ordered / volumes
|
||||||
|
|
||||||
Returns
|
# Pre-allocate arrays
|
||||||
-------
|
rfound = numpy.full_like(delta, numpy.nan)
|
||||||
r200 : float
|
mfound = numpy.full_like(rfound, numpy.nan)
|
||||||
The R200c radius
|
|
||||||
"""
|
|
||||||
return self.radius_enclosed_overdensity(200)
|
|
||||||
|
|
||||||
@property
|
for n in order_delta:
|
||||||
def r178(self):
|
overdense_mask = densities > target_density[n]
|
||||||
r"""
|
|
||||||
The radius at which the mean spherical density reaches 178 times
|
|
||||||
the critical density, :math:`R_{178c}`. Returns `numpy.nan` if the
|
|
||||||
estimate fails.
|
|
||||||
|
|
||||||
Returns
|
# Enforce that we have at least several particles enclosed
|
||||||
-------
|
if numpy.sum(overdense_mask) < n_particles_min:
|
||||||
r178 : float
|
continue
|
||||||
The R178c radius
|
# The outermost particle radius where the overdensity is achieved
|
||||||
"""
|
k = numpy.where(overdense_mask)[0][0]
|
||||||
return self.radius_enclosed_overdensity(178)
|
rfound[n] = self.r[order_particles][k]
|
||||||
|
mfound[n] = cummass_ordered[k]
|
||||||
|
|
||||||
@property
|
# If only one delta return simply numbers
|
||||||
def r500(self):
|
if n_delta == 1:
|
||||||
r"""
|
rfound = rfound[0]
|
||||||
The radius at which the mean spherical density reaches 500 times
|
mfound = mfound[0]
|
||||||
the critical density, :math:`R_{500c}`. Returns `numpy.nan` if the
|
|
||||||
estimate fails.
|
|
||||||
|
|
||||||
Returns
|
return rfound, mfound
|
||||||
-------
|
|
||||||
r500 : float
|
|
||||||
The R500c radius
|
|
||||||
"""
|
|
||||||
return self.radius_enclosed_overdensity(500)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def m200(self):
|
|
||||||
r"""
|
|
||||||
The mass enclosed within the :math:`R_{200c}` region, obtained from
|
|
||||||
`self.r200`. Returns `numpy.nan` if the radius estimate fails.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
m200 : float
|
|
||||||
The M200 mass
|
|
||||||
"""
|
|
||||||
r200 = self.radius_enclosed_overdensity(200)
|
|
||||||
return self.enclosed_spherical_mass(r200)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def m178(self):
|
|
||||||
r"""
|
|
||||||
The mass enclosed within the :math:`R_{178c}` region, obtained from
|
|
||||||
`self.r178`. This is approximately the virial mass, though this notion
|
|
||||||
depends on the dynamical state of the clump. Returns `numpy.nan` if
|
|
||||||
the radius estimate fails.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
m178 : float
|
|
||||||
The M178 mass
|
|
||||||
"""
|
|
||||||
r178 = self.radius_enclosed_overdensity(178)
|
|
||||||
return self.enclosed_spherical_mass(r178)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def m500(self):
|
|
||||||
r"""
|
|
||||||
The mass enclosed within the :math:`R_{500c}` region, obtained from
|
|
||||||
`self.r500`. Returns `numpy.nan` if the radius estimate fails.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
m500 : float
|
|
||||||
The M500 mass
|
|
||||||
"""
|
|
||||||
r500 = self.radius_enclosed_overdensity(500)
|
|
||||||
return self.enclosed_spherical_mass(r500)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_arrays(cls, particles, clump, rhoc=None):
|
def from_arrays(cls, particles, clump, rhoc=None):
|
||||||
|
|
|
@ -18,11 +18,34 @@ Halo profiles functions and posteriors.
|
||||||
|
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
from jax import numpy as jnumpy
|
||||||
|
from jax import grad
|
||||||
from scipy.optimize import minimize_scalar
|
from scipy.optimize import minimize_scalar
|
||||||
from scipy.stats import uniform
|
from scipy.stats import uniform
|
||||||
from .halo import Clump
|
from .halo import Clump
|
||||||
|
|
||||||
|
|
||||||
|
def jax_switch(f1, f2, use_jax):
|
||||||
|
"""
|
||||||
|
Return function `f1` if `use_jax` else return `f2`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
f1 : function
|
||||||
|
The JAX function.
|
||||||
|
f2 : function
|
||||||
|
The non-JAX function.
|
||||||
|
use_jax : bool
|
||||||
|
Whether to use the JAX function.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
chosen_func : function
|
||||||
|
The chosen function.
|
||||||
|
"""
|
||||||
|
return f1 if use_jax else f2
|
||||||
|
|
||||||
|
|
||||||
class NFWProfile:
|
class NFWProfile:
|
||||||
r"""
|
r"""
|
||||||
The Navarro-Frenk-White (NFW) density profile defined as
|
The Navarro-Frenk-White (NFW) density profile defined as
|
||||||
|
@ -66,7 +89,7 @@ class NFWProfile:
|
||||||
return rho0 / (x * (1 + x)**2)
|
return rho0 / (x * (1 + x)**2)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def logprofile(r, Rs, rho0):
|
def logprofile(r, Rs, rho0, use_jax=False):
|
||||||
r"""
|
r"""
|
||||||
Natural logarithm of the halo profile evaluated at :math:`r`.
|
Natural logarithm of the halo profile evaluated at :math:`r`.
|
||||||
|
|
||||||
|
@ -78,17 +101,20 @@ class NFWProfile:
|
||||||
Scale radius :math:`R_s`.
|
Scale radius :math:`R_s`.
|
||||||
rho0 : float
|
rho0 : float
|
||||||
NFW density parameter :math:`\rho_0`.
|
NFW density parameter :math:`\rho_0`.
|
||||||
|
use_jax : bool, optional
|
||||||
|
Whether to use `JAX` expressions. By default `False`.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
logdensity : float or 1-dimensional array
|
logdensity : float or 1-dimensional array
|
||||||
Logarithmic density of the NFW profile at :math:`r`.
|
Logarithmic density of the NFW profile at :math:`r`.
|
||||||
"""
|
"""
|
||||||
|
log = jax_switch(jnumpy.log, numpy.log, use_jax)
|
||||||
x = r / Rs
|
x = r / Rs
|
||||||
return numpy.log(rho0) - numpy.log(x) - 2 * numpy.log(1 + x)
|
return log(rho0) - log(x) - 2 * log(1 + x)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def enclosed_mass(r, Rs, rho0):
|
def enclosed_mass(r, Rs, rho0, use_jax=False):
|
||||||
r"""
|
r"""
|
||||||
Enclosed mass of a NFW profile in radius :math:`r`.
|
Enclosed mass of a NFW profile in radius :math:`r`.
|
||||||
|
|
||||||
|
@ -100,6 +126,8 @@ class NFWProfile:
|
||||||
Scale radius :math:`R_s`.
|
Scale radius :math:`R_s`.
|
||||||
rho0 : float
|
rho0 : float
|
||||||
NFW density parameter :math:`\rho_0`.
|
NFW density parameter :math:`\rho_0`.
|
||||||
|
use_jax : bool, optional
|
||||||
|
Whether to use `JAX` expressions. By default `False`.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
@ -107,11 +135,11 @@ class NFWProfile:
|
||||||
The enclosed mass.
|
The enclosed mass.
|
||||||
"""
|
"""
|
||||||
x = r / Rs
|
x = r / Rs
|
||||||
out = numpy.log(1 + x) - x / (1 + x)
|
out = jax_switch(jnumpy.log, numpy.log, use_jax)(1 + x) - x / (1 + x)
|
||||||
return 4 * numpy.pi * rho0 * Rs**3 * out
|
return 4 * numpy.pi * rho0 * Rs**3 * out
|
||||||
|
|
||||||
def bounded_enclosed_mass(self, rmin, rmax, Rs, rho0):
|
def bounded_enclosed_mass(self, rmin, rmax, Rs, rho0, use_jax=False):
|
||||||
"""
|
r"""
|
||||||
Calculate the enclosed mass between :math:`r_min <= r <= r_max`.
|
Calculate the enclosed mass between :math:`r_min <= r <= r_max`.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
@ -124,14 +152,16 @@ class NFWProfile:
|
||||||
Scale radius :math:`R_s`.
|
Scale radius :math:`R_s`.
|
||||||
rho0 : float
|
rho0 : float
|
||||||
NFW density parameter :math:`\rho_0`.
|
NFW density parameter :math:`\rho_0`.
|
||||||
|
use_jax : bool, optional
|
||||||
|
Whether to use `JAX` expressions. By default `False`.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
M : float
|
M : float
|
||||||
The enclosed mass within the radial range.
|
The enclosed mass within the radial range.
|
||||||
"""
|
"""
|
||||||
return (self.enclosed_mass(rmax, Rs, rho0)
|
return (self.enclosed_mass(rmax, Rs, rho0, use_jax)
|
||||||
- self.enclosed_mass(rmin, Rs, rho0))
|
- self.enclosed_mass(rmin, Rs, rho0, use_jax))
|
||||||
|
|
||||||
def pdf(self, r, Rs, rmin, rmax):
|
def pdf(self, r, Rs, rmin, rmax):
|
||||||
r"""
|
r"""
|
||||||
|
@ -319,7 +349,7 @@ class NFWPosterior(NFWProfile):
|
||||||
self._clump = clump
|
self._clump = clump
|
||||||
# The minimum separation
|
# The minimum separation
|
||||||
rmin = self.clump.rmin
|
rmin = self.clump.rmin
|
||||||
rmax = self.clump.r200
|
rmax, __ = self.clump.spherical_overdensity_mass(200)
|
||||||
# Set the distances
|
# Set the distances
|
||||||
self._rmin = rmin
|
self._rmin = rmin
|
||||||
self._rmax = rmax
|
self._rmax = rmax
|
||||||
|
@ -340,7 +370,7 @@ class NFWPosterior(NFWProfile):
|
||||||
gamma = 4 * numpy.pi * self.r**2 * self.m * self.Npart
|
gamma = 4 * numpy.pi * self.r**2 * self.m * self.Npart
|
||||||
self._ll0 = numpy.sum(numpy.log(gamma)) - self.Npart * self._logMtot
|
self._ll0 = numpy.sum(numpy.log(gamma)) - self.Npart * self._logMtot
|
||||||
|
|
||||||
def rho0_from_logRs(self, logRs):
|
def rho0_from_Rs(self, Rs):
|
||||||
r"""
|
r"""
|
||||||
Obtain :math:`\rho_0` of the NFW profile from the integral constraint
|
Obtain :math:`\rho_0` of the NFW profile from the integral constraint
|
||||||
on total mass. Calculated as the ratio between the total particle mass
|
on total mass. Calculated as the ratio between the total particle mass
|
||||||
|
@ -357,8 +387,7 @@ class NFWPosterior(NFWProfile):
|
||||||
The NFW density parameter.
|
The NFW density parameter.
|
||||||
"""
|
"""
|
||||||
Mtot = numpy.exp(self._logMtot)
|
Mtot = numpy.exp(self._logMtot)
|
||||||
Mnfw_norm = self.bounded_enclosed_mass(self.rmin, self.rmax,
|
Mnfw_norm = self.bounded_enclosed_mass(self.rmin, self.rmax, Rs, 1)
|
||||||
10**logRs, 1)
|
|
||||||
return Mtot / Mnfw_norm
|
return Mtot / Mnfw_norm
|
||||||
|
|
||||||
def logprior(self, logRs):
|
def logprior(self, logRs):
|
||||||
|
@ -379,7 +408,7 @@ class NFWPosterior(NFWProfile):
|
||||||
return - numpy.infty
|
return - numpy.infty
|
||||||
return - self._logprior_volume
|
return - self._logprior_volume
|
||||||
|
|
||||||
def loglikelihood(self, logRs):
|
def loglikelihood(self, logRs, use_jax=False):
|
||||||
"""
|
"""
|
||||||
Logarithmic likelihood.
|
Logarithmic likelihood.
|
||||||
|
|
||||||
|
@ -387,6 +416,8 @@ class NFWPosterior(NFWProfile):
|
||||||
----------
|
----------
|
||||||
logRs : float
|
logRs : float
|
||||||
Logarithmic scale factor in units matching the coordinates.
|
Logarithmic scale factor in units matching the coordinates.
|
||||||
|
use_jax : bool, optional
|
||||||
|
Whether to use `JAX` expressions. By default `False`.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
@ -394,11 +425,14 @@ class NFWPosterior(NFWProfile):
|
||||||
The logarithmic likelihood.
|
The logarithmic likelihood.
|
||||||
"""
|
"""
|
||||||
Rs = 10**logRs
|
Rs = 10**logRs
|
||||||
|
log = jnumpy.log if use_jax else numpy.log
|
||||||
# Expected enclosed mass from a NFW
|
# Expected enclosed mass from a NFW
|
||||||
Mnfw = self.bounded_enclosed_mass(self.rmin, self.rmax,
|
Mnfw = self.bounded_enclosed_mass(self.rmin, self.rmax,
|
||||||
Rs, 1)
|
Rs, 1, use_jax)
|
||||||
ll = self._ll0 + numpy.sum(self.logprofile(self.r, Rs, 1))
|
ll = jax_switch(jnumpy.sum, numpy.sum, use_jax)(self.logprofile(
|
||||||
return ll - self.Npart * numpy.log(Mnfw)
|
self.r, Rs, 1, use_jax))
|
||||||
|
ll += self._ll0
|
||||||
|
return ll - self.Npart * log(Mnfw)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def initlogRs(self):
|
def initlogRs(self):
|
||||||
|
@ -415,9 +449,9 @@ class NFWPosterior(NFWProfile):
|
||||||
bins = numpy.linspace(self.rmin, self.rmax,
|
bins = numpy.linspace(self.rmin, self.rmax,
|
||||||
self._binsguess)
|
self._binsguess)
|
||||||
counts, edges = numpy.histogram(self.r, bins)
|
counts, edges = numpy.histogram(self.r, bins)
|
||||||
return numpy.log(edges[numpy.argmax(counts)])
|
return numpy.log10(edges[numpy.argmax(counts)])
|
||||||
|
|
||||||
def __call__(self, logRs):
|
def __call__(self, logRs, use_jax=False):
|
||||||
"""
|
"""
|
||||||
Logarithmic posterior. Sum of the logarithmic prior and likelihood.
|
Logarithmic posterior. Sum of the logarithmic prior and likelihood.
|
||||||
|
|
||||||
|
@ -425,6 +459,8 @@ class NFWPosterior(NFWProfile):
|
||||||
----------
|
----------
|
||||||
logRs : float
|
logRs : float
|
||||||
Logarithmic scale factor in units matching the coordinates.
|
Logarithmic scale factor in units matching the coordinates.
|
||||||
|
use_jax : bool, optional
|
||||||
|
Whether to use `JAX` expressions. By default `False`.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
@ -434,21 +470,73 @@ class NFWPosterior(NFWProfile):
|
||||||
lp = self.logprior(logRs)
|
lp = self.logprior(logRs)
|
||||||
if not numpy.isfinite(lp):
|
if not numpy.isfinite(lp):
|
||||||
return - numpy.infty
|
return - numpy.infty
|
||||||
return self.loglikelihood(logRs) + lp
|
return self.loglikelihood(logRs, use_jax) + lp
|
||||||
|
|
||||||
def maxpost_logRs(self):
|
def uncertainty_at_maxpost(self, logRs_max):
|
||||||
r"""
|
r"""
|
||||||
Maximum a-posterio estimate of the scale radius :math:`\log R_{\rm s}`.
|
Calculate Gaussian approximation of the uncertainty at `logRs_max`, the
|
||||||
Returns the scale radius if the fit converged, otherwise `numpy.nan`.
|
maximum a-posteriori esitmate. This is the square root of the negative
|
||||||
|
inverse 2nd derivate of the logarithimic posterior with respect to the
|
||||||
|
logarithm of the scale factor. This is only valid `logRs_max` is the
|
||||||
|
maximum of the posterior!
|
||||||
|
|
||||||
|
This uses `JAX`. The functions should be compiled but unless there is
|
||||||
|
a need for more speed this is fine as it is.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
logRs_max : float
|
||||||
|
Position :math:`\log R_{\rm s}` to evaluate the uncertainty. Must
|
||||||
|
be the maximum.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
res : float
|
uncertainty : float
|
||||||
The scale radius.
|
The uncertainty calculated as outlined above.
|
||||||
|
"""
|
||||||
|
def f(x):
|
||||||
|
return self(x, use_jax=True)
|
||||||
|
|
||||||
|
# Evaluate the second derivative
|
||||||
|
h = grad(grad(f))(logRs_max)
|
||||||
|
h = float(h)
|
||||||
|
if not h < 0:
|
||||||
|
return numpy.nan
|
||||||
|
return (- 1 / h)**0.5
|
||||||
|
|
||||||
|
def maxpost_logRs(self, calc_err=False, eps=1e-4):
|
||||||
|
r"""
|
||||||
|
Maximum a-posterio estimate of the scale radius :math:`\log R_{\rm s}`.
|
||||||
|
Returns the scale radius if the fit converged, otherwise `numpy.nan`.
|
||||||
|
Checks whether :math:`log r_{\rm max} / R_{\rm s} > \epsilon`, where
|
||||||
|
to ensure that the scale radius is not too close to the boundary which
|
||||||
|
occurs if the fit fails.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
calc_err : bool, optional
|
||||||
|
Optional toggle to calculate the uncertainty on the scale radius.
|
||||||
|
By default false.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
logRs: float
|
||||||
|
The log scale radius.
|
||||||
|
uncertainty : float
|
||||||
|
The uncertainty on the scale radius. Calculated following
|
||||||
|
`self.uncertainty_at_maxpost`.
|
||||||
"""
|
"""
|
||||||
# Loss function to optimize
|
# Loss function to optimize
|
||||||
def loss(logRs):
|
def loss(logRs):
|
||||||
return - self(logRs)
|
return - self(logRs)
|
||||||
|
|
||||||
res = minimize_scalar(loss, bounds=(self._logrmin, self._logrmax),
|
res = minimize_scalar(loss, bounds=(self._logrmin, self._logrmax),
|
||||||
method='bounded')
|
method='bounded')
|
||||||
return res.x if res.success else numpy.nan
|
|
||||||
|
if self._logrmax - res.x < eps:
|
||||||
|
res.success = False
|
||||||
|
|
||||||
|
if not res.success:
|
||||||
|
return numpy.nan, numpy.nan
|
||||||
|
e_logRs = self.uncertainty_at_maxpost(res.x) if calc_err else numpy.nan
|
||||||
|
return res.x, e_logRs
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -18,6 +18,7 @@ realisation must have been split in advance by `run_split_halos`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
from datetime import datetime
|
||||||
from os.path import join
|
from os.path import join
|
||||||
from mpi4py import MPI
|
from mpi4py import MPI
|
||||||
try:
|
try:
|
||||||
|
@ -43,52 +44,56 @@ nproc = comm.Get_size()
|
||||||
|
|
||||||
dumpdir = utils.dumpdir
|
dumpdir = utils.dumpdir
|
||||||
loaddir = join(utils.dumpdir, "temp")
|
loaddir = join(utils.dumpdir, "temp")
|
||||||
cols_collect = [("npart", I64), ("totpartmass", F64), ("logRs", F64),
|
cols_collect = [("npart", I64), ("totpartmass", F64), ("Rs", F64),
|
||||||
("rho0", F64), ("rmin", F64), ("rmax", F64),
|
("rho0", F64), ("conc", F64), ("rmin", F64),
|
||||||
("r200", F64), ("r178", F64), ("r500", F64),
|
("rmax", F64), ("r200", F64), ("r500", F64),
|
||||||
("m200", F64), ("m178", F64), ("m500", F64)]
|
("m200", F64), ("m500", F64)]
|
||||||
|
|
||||||
# NOTE later loop over sims too
|
# NOTE later loop over sims too
|
||||||
Nsim = Nsims[0]
|
Nsim = Nsims[0]
|
||||||
|
simpath = csiborgtools.io.get_sim_path(Nsim)
|
||||||
|
box = csiborgtools.units.BoxUnits(Nsnap, simpath)
|
||||||
|
|
||||||
jobs = csiborgtools.fits.split_jobs(utils.Nsplits, nproc)[rank]
|
jobs = csiborgtools.fits.split_jobs(utils.Nsplits, nproc)[rank]
|
||||||
for Nsplit in jobs:
|
for icount, Nsplit in enumerate(jobs):
|
||||||
print("Rank {} working on {}.".format(rank, Nsplit))
|
print("{}: rank {} working {} / {} jobs.".format(datetime.now(), rank,
|
||||||
|
icount + 1, len(jobs)))
|
||||||
parts, part_clumps, clumps = csiborgtools.fits.load_split_particles(
|
parts, part_clumps, clumps = csiborgtools.fits.load_split_particles(
|
||||||
Nsplit, loaddir, Nsim, Nsnap, remove_split=False)
|
Nsplit, loaddir, Nsim, Nsnap, remove_split=False)
|
||||||
|
|
||||||
N = clumps.size
|
N = clumps.size
|
||||||
cols = [("index", I64), ("npart", I64), ("totpartmass", F64),
|
cols = [("index", I64), ("npart", I64), ("totpartmass", F64),
|
||||||
("logRs", F64), ("rho0", F64), ("rmin", F64), ("rmax", F64),
|
("Rs", F64), ("rho0", F64), ("conc", F64),
|
||||||
("r200", F64), ("r178", F64), ("r500", F64),
|
("rmin", F64), ("rmax", F64),
|
||||||
("m200", F64), ("m178", F64), ("m500", F64)]
|
("r200", F64), ("r500", F64), ("m200", F64), ("m500", F64)]
|
||||||
out = csiborgtools.utils.cols_to_structured(N, cols)
|
out = csiborgtools.utils.cols_to_structured(N, cols)
|
||||||
out["index"] = clumps["index"]
|
out["index"] = clumps["index"]
|
||||||
|
|
||||||
for n in range(N):
|
for n in range(N):
|
||||||
# Pick clump and its particles
|
# Pick clump and its particles
|
||||||
xs = csiborgtools.fits.pick_single_clump(n, parts, part_clumps, clumps)
|
xs = csiborgtools.fits.pick_single_clump(n, parts, part_clumps, clumps)
|
||||||
clump = csiborgtools.fits.Clump.from_arrays(*xs)
|
clump = csiborgtools.fits.Clump.from_arrays(*xs, rhoc=box.box_rhoc)
|
||||||
out["npart"][n] = clump.Npart
|
out["npart"][n] = clump.Npart
|
||||||
out["rmin"][n] = clump.rmin
|
out["rmin"][n] = clump.rmin
|
||||||
out["rmax"][n] = clump.rmax
|
out["rmax"][n] = clump.rmax
|
||||||
out["totpartmass"][n] = clump.total_particle_mass
|
out["totpartmass"][n] = clump.total_particle_mass
|
||||||
out["r200"][n] = clump.r200
|
|
||||||
out["r178"][n] = clump.r178
|
# Spherical overdensity radii and masses
|
||||||
out["r500"][n] = clump.r500
|
rs, ms = clump.spherical_overdensity_mass([200, 500])
|
||||||
out["m200"][n] = clump.m200
|
out["r200"][n] = rs[0]
|
||||||
out["m178"][n] = clump.m178
|
out["r500"][n] = rs[1]
|
||||||
out["m500"][n] = clump.m200
|
out["m200"][n] = ms[0]
|
||||||
|
out["m500"][n] = ms[1]
|
||||||
|
|
||||||
# NFW profile fit
|
# NFW profile fit
|
||||||
if clump.Npart > 10 and numpy.isfinite(out["r200"][n]):
|
if clump.Npart > 10 and numpy.isfinite(out["r200"][n]):
|
||||||
# NOTE here it calculates the r200 again, but its fast so does not
|
|
||||||
# matter anyway.
|
|
||||||
nfwpost = csiborgtools.fits.NFWPosterior(clump)
|
nfwpost = csiborgtools.fits.NFWPosterior(clump)
|
||||||
logRs = nfwpost.maxpost_logRs()
|
logRs, __ = nfwpost.maxpost_logRs()
|
||||||
|
Rs = 10**logRs
|
||||||
if not numpy.isnan(logRs):
|
if not numpy.isnan(logRs):
|
||||||
out["logRs"][n] = logRs
|
out["Rs"][n] = Rs
|
||||||
out["rho0"][n] = nfwpost.rho0_from_logRs(logRs)
|
out["rho0"][n] = nfwpost.rho0_from_Rs(Rs)
|
||||||
|
out["conc"][n] = out["r200"][n] / Rs
|
||||||
|
|
||||||
csiborgtools.io.dump_split(out, Nsplit, Nsim, Nsnap, dumpdir)
|
csiborgtools.io.dump_split(out, Nsplit, Nsim, Nsnap, dumpdir)
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ import utils
|
||||||
|
|
||||||
Nsims = [9844]
|
Nsims = [9844]
|
||||||
Nsnap = 1016
|
Nsnap = 1016
|
||||||
partcols = ["x", "y", "z", "M", "level"]
|
partcols = ["x", "y", "z", "vx", "vy", "vz", "M", "level"]
|
||||||
dumpdir = join(utils.dumpdir, "temp")
|
dumpdir = join(utils.dumpdir, "temp")
|
||||||
|
|
||||||
for Nsim in tqdm(Nsims):
|
for Nsim in tqdm(Nsims):
|
||||||
|
|
Loading…
Reference in a new issue