mirror of
https://github.com/Richard-Sti/csiborgtools.git
synced 2024-12-22 16:28:02 +00:00
Periodic neighbours (#84)
* Edit the HMF plot * Add periodic dist 2 points * Add boxsize to RVSSphere * Add periodic distance * Adding periodic distance * Add imports * Change arguments * Update bounds * Lower min number of particles * Change kwargs * Add paths overlap quijote * Add some comments
This commit is contained in:
parent
c7e447df01
commit
c7b600d0ad
14 changed files with 196 additions and 61 deletions
|
@ -13,7 +13,9 @@
|
||||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
from csiborgtools import clustering, field, match, read # noqa
|
from csiborgtools import clustering, field, match, read # noqa
|
||||||
from .utils import (center_of_mass, delta2ncells, periodic_distance, number_counts) # noqa
|
|
||||||
|
from .utils import (center_of_mass, delta2ncells, number_counts,
|
||||||
|
periodic_distance, periodic_distance_two_points) # noqa
|
||||||
|
|
||||||
# Arguments to csiborgtools.read.Paths.
|
# Arguments to csiborgtools.read.Paths.
|
||||||
paths_glamdring = {"srcdir": "/mnt/extraspace/hdesmond/",
|
paths_glamdring = {"srcdir": "/mnt/extraspace/hdesmond/",
|
||||||
|
|
|
@ -51,16 +51,20 @@ class BaseRVS(ABC):
|
||||||
class RVSinsphere(BaseRVS):
|
class RVSinsphere(BaseRVS):
|
||||||
"""
|
"""
|
||||||
Generator of uniform RVS in a sphere of radius `R` in Cartesian
|
Generator of uniform RVS in a sphere of radius `R` in Cartesian
|
||||||
coordinates centered at the origin.
|
coordinates centered at the centre of the box.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
R : float
|
R : float
|
||||||
Radius of the sphere.
|
Radius of the sphere.
|
||||||
|
boxsize : float
|
||||||
|
Box size
|
||||||
"""
|
"""
|
||||||
def __init__(self, R):
|
def __init__(self, R, boxsize):
|
||||||
assert R > 0, "Radius must be positive."
|
assert R > 0, "Radius must be positive."
|
||||||
|
assert boxsize > 0, "Box size must be positive."
|
||||||
self.R = R
|
self.R = R
|
||||||
|
self.boxsize = boxsize
|
||||||
BaseRVS.__init__(self)
|
BaseRVS.__init__(self)
|
||||||
|
|
||||||
def __call__(self, nsamples, random_state=42, dtype=numpy.float32):
|
def __call__(self, nsamples, random_state=42, dtype=numpy.float32):
|
||||||
|
@ -73,7 +77,7 @@ class RVSinsphere(BaseRVS):
|
||||||
x = r * numpy.sin(theta) * numpy.cos(phi)
|
x = r * numpy.sin(theta) * numpy.cos(phi)
|
||||||
y = r * numpy.sin(theta) * numpy.sin(phi)
|
y = r * numpy.sin(theta) * numpy.sin(phi)
|
||||||
z = r * numpy.cos(theta)
|
z = r * numpy.cos(theta)
|
||||||
return numpy.vstack([x, y, z]).T
|
return numpy.vstack([x, y, z]).T + self.boxsize / 2
|
||||||
|
|
||||||
|
|
||||||
class RVSinbox(BaseRVS):
|
class RVSinbox(BaseRVS):
|
||||||
|
|
|
@ -239,7 +239,8 @@ class RealisationsMatcher(BaseMatcher):
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"{datetime.now()}: querying the KNN.", flush=True)
|
print(f"{datetime.now()}: querying the KNN.", flush=True)
|
||||||
match_indxs = radius_neighbours(
|
match_indxs = radius_neighbours(
|
||||||
catx.knn(in_initial=True), cat0.position(in_initial=True),
|
catx.knn(in_initial=True, subtract_observer=False, periodic=True),
|
||||||
|
cat0.position(in_initial=True),
|
||||||
radiusX=cat0["lagpatch_size"], radiusKNN=catx["lagpatch_size"],
|
radiusX=cat0["lagpatch_size"], radiusKNN=catx["lagpatch_size"],
|
||||||
nmult=self.nmult, enforce_int32=True, verbose=verbose)
|
nmult=self.nmult, enforce_int32=True, verbose=verbose)
|
||||||
|
|
||||||
|
@ -515,7 +516,7 @@ class ParticleOverlap(BaseMatcher):
|
||||||
Minimun and maximum cell numbers along each dimension of `halo1`.
|
Minimun and maximum cell numbers along each dimension of `halo1`.
|
||||||
Optional.
|
Optional.
|
||||||
mins2, maxs2 : 1-dimensional arrays of shape `(3,)`
|
mins2, maxs2 : 1-dimensional arrays of shape `(3,)`
|
||||||
Minimun and maximum cell numbers along each dimension of `halo2`.
|
Minimum and maximum cell numbers along each dimension of `halo2`.
|
||||||
Optional.
|
Optional.
|
||||||
smooth_kwargs : kwargs, optional
|
smooth_kwargs : kwargs, optional
|
||||||
Kwargs to be passed to :py:func:`scipy.ndimage.gaussian_filter`.
|
Kwargs to be passed to :py:func:`scipy.ndimage.gaussian_filter`.
|
||||||
|
@ -1014,7 +1015,8 @@ def radius_neighbours(knn, X, radiusX, radiusKNN, nmult=1.0,
|
||||||
def find_neighbour(nsim0, cats):
|
def find_neighbour(nsim0, cats):
|
||||||
"""
|
"""
|
||||||
Find the nearest neighbour of halos from a reference catalogue indexed
|
Find the nearest neighbour of halos from a reference catalogue indexed
|
||||||
`nsim0` in the remaining simulations.
|
`nsim0` in the remaining simulations. Note that this must be the same
|
||||||
|
simulation suite.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
@ -1030,8 +1032,10 @@ def find_neighbour(nsim0, cats):
|
||||||
cross_hindxs : 2-dimensional array of shape `(nhalos, len(cats) - 1)`
|
cross_hindxs : 2-dimensional array of shape `(nhalos, len(cats) - 1)`
|
||||||
Halo indices of the nearest neighbour.
|
Halo indices of the nearest neighbour.
|
||||||
"""
|
"""
|
||||||
|
assert all(isinstance(cat, type(cats[nsim0])) for cat in cats.values())
|
||||||
|
|
||||||
cat0 = cats[nsim0]
|
cat0 = cats[nsim0]
|
||||||
X = cat0.position(in_initial=False, subtract_observer=True)
|
X = cat0.position(in_initial=False)
|
||||||
|
|
||||||
nhalos = X.shape[0]
|
nhalos = X.shape[0]
|
||||||
num_cats = len(cats) - 1
|
num_cats = len(cats) - 1
|
||||||
|
|
|
@ -33,6 +33,7 @@ from .paths import Paths
|
||||||
from .readsim import CSiBORGReader
|
from .readsim import CSiBORGReader
|
||||||
from .utils import (add_columns, cartesian_to_radec, cols_to_structured,
|
from .utils import (add_columns, cartesian_to_radec, cols_to_structured,
|
||||||
flip_cols, radec_to_cartesian, real2redshift)
|
flip_cols, radec_to_cartesian, real2redshift)
|
||||||
|
from ..utils import periodic_distance_two_points
|
||||||
|
|
||||||
|
|
||||||
class BaseCatalogue(ABC):
|
class BaseCatalogue(ABC):
|
||||||
|
@ -73,6 +74,17 @@ class BaseCatalogue(ABC):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractproperty
|
||||||
|
def simname(self):
|
||||||
|
"""
|
||||||
|
Simulation name.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
simname : str
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def paths(self):
|
def paths(self):
|
||||||
"""
|
"""
|
||||||
|
@ -327,7 +339,7 @@ class BaseCatalogue(ABC):
|
||||||
return numpy.vstack([self["L{}".format(p)] for p in ("x", "y", "z")]).T
|
return numpy.vstack([self["L{}".format(p)] for p in ("x", "y", "z")]).T
|
||||||
|
|
||||||
@lru_cache(maxsize=2)
|
@lru_cache(maxsize=2)
|
||||||
def knn(self, in_initial):
|
def knn(self, in_initial, subtract_observer, periodic):
|
||||||
r"""
|
r"""
|
||||||
kNN object for catalogue objects with caching. Positions are centered
|
kNN object for catalogue objects with caching. Positions are centered
|
||||||
on the observer.
|
on the observer.
|
||||||
|
@ -336,14 +348,32 @@ class BaseCatalogue(ABC):
|
||||||
----------
|
----------
|
||||||
in_initial : bool
|
in_initial : bool
|
||||||
Whether to define the kNN on the initial or final snapshot.
|
Whether to define the kNN on the initial or final snapshot.
|
||||||
|
subtract_observer : bool
|
||||||
|
Whether to subtract the observer's location from the positions.
|
||||||
|
periodic : bool
|
||||||
|
Whether to use periodic boundary conditions.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
knn : :py:class:`sklearn.neighbors.NearestNeighbors`
|
knn : :py:class:`sklearn.neighbors.NearestNeighbors`
|
||||||
kNN object fitted with object positions.
|
kNN object fitted with object positions.
|
||||||
"""
|
"""
|
||||||
pos = self.position(in_initial=in_initial)
|
if subtract_observer and periodic:
|
||||||
return NearestNeighbors().fit(pos)
|
raise ValueError("Subtracting observer is not supported for "
|
||||||
|
"periodic boundary conditions.")
|
||||||
|
|
||||||
|
pos = self.position(in_initial=in_initial,
|
||||||
|
subtract_observer=subtract_observer)
|
||||||
|
|
||||||
|
if periodic:
|
||||||
|
L = self.box.boxsize
|
||||||
|
knn = NearestNeighbors(
|
||||||
|
metric=lambda a, b: periodic_distance_two_points(a, b, L))
|
||||||
|
else:
|
||||||
|
knn = NearestNeighbors()
|
||||||
|
|
||||||
|
knn.fit(pos)
|
||||||
|
return knn
|
||||||
|
|
||||||
def nearest_neighbours(self, X, radius, in_initial, knearest=False,
|
def nearest_neighbours(self, X, radius, in_initial, knearest=False,
|
||||||
return_mass=False, mass_key=None):
|
return_mass=False, mass_key=None):
|
||||||
|
@ -382,7 +412,8 @@ class BaseCatalogue(ABC):
|
||||||
if return_mass and not mass_key:
|
if return_mass and not mass_key:
|
||||||
raise ValueError("`mass_key` must be provided if `return_mass`.")
|
raise ValueError("`mass_key` must be provided if `return_mass`.")
|
||||||
|
|
||||||
knn = self.knn(in_initial)
|
knn = self.knn(in_initial, subtract_observer=False, periodic=True)
|
||||||
|
|
||||||
if knearest:
|
if knearest:
|
||||||
dist, indxs = knn.kneighbors(X, radius)
|
dist, indxs = knn.kneighbors(X, radius)
|
||||||
else:
|
else:
|
||||||
|
@ -564,6 +595,10 @@ class CSiBORGHaloCatalogue(BaseCatalogue):
|
||||||
"""
|
"""
|
||||||
return CSiBORGBox(self.nsnap, self.nsim, self.paths)
|
return CSiBORGBox(self.nsnap, self.nsim, self.paths)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def simname(self):
|
||||||
|
return "csiborg"
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Quijote halo catalogue #
|
# Quijote halo catalogue #
|
||||||
|
@ -661,6 +696,10 @@ class QuijoteHaloCatalogue(BaseCatalogue):
|
||||||
assert nsnap in [0, 1, 2, 3, 4]
|
assert nsnap in [0, 1, 2, 3, 4]
|
||||||
self._nsnap = nsnap
|
self._nsnap = nsnap
|
||||||
|
|
||||||
|
@property
|
||||||
|
def simname(self):
|
||||||
|
return "quijote"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def redshift(self):
|
def redshift(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -47,6 +47,10 @@ class PairOverlap:
|
||||||
_data = None
|
_data = None
|
||||||
|
|
||||||
def __init__(self, cat0, catx, paths, maxdist=None):
|
def __init__(self, cat0, catx, paths, maxdist=None):
|
||||||
|
if cat0.simname != catx.simname:
|
||||||
|
raise ValueError("The two catalogues must be from the same "
|
||||||
|
"simulation.")
|
||||||
|
|
||||||
self._cat0 = cat0
|
self._cat0 = cat0
|
||||||
self._catx = catx
|
self._catx = catx
|
||||||
self.load(cat0, catx, paths, maxdist)
|
self.load(cat0, catx, paths, maxdist)
|
||||||
|
@ -77,8 +81,8 @@ class PairOverlap:
|
||||||
|
|
||||||
# We first load in the output files. We need to find the right
|
# We first load in the output files. We need to find the right
|
||||||
# combination of the reference and cross simulation.
|
# combination of the reference and cross simulation.
|
||||||
fname = paths.overlap(nsim0, nsimx, smoothed=False)
|
fname = paths.overlap(cat0.simname, nsim0, nsimx, smoothed=False)
|
||||||
fname_inv = paths.overlap(nsimx, nsim0, smoothed=False)
|
fname_inv = paths.overlap(cat0.simname, nsimx, nsim0, smoothed=False)
|
||||||
if isfile(fname):
|
if isfile(fname):
|
||||||
data_ngp = numpy.load(fname, allow_pickle=True)
|
data_ngp = numpy.load(fname, allow_pickle=True)
|
||||||
to_invert = False
|
to_invert = False
|
||||||
|
@ -89,7 +93,8 @@ class PairOverlap:
|
||||||
else:
|
else:
|
||||||
raise FileNotFoundError(f"No file found for {nsim0} and {nsimx}.")
|
raise FileNotFoundError(f"No file found for {nsim0} and {nsimx}.")
|
||||||
|
|
||||||
fname_smooth = paths.overlap(cat0.nsim, catx.nsim, smoothed=True)
|
fname_smooth = paths.overlap(cat0.simname, cat0.nsim, catx.nsim,
|
||||||
|
smoothed=True)
|
||||||
data_smooth = numpy.load(fname_smooth, allow_pickle=True)
|
data_smooth = numpy.load(fname_smooth, allow_pickle=True)
|
||||||
|
|
||||||
# Create mapping from halo indices to array positions in the catalogue.
|
# Create mapping from halo indices to array positions in the catalogue.
|
||||||
|
@ -764,13 +769,15 @@ class NPairsOverlap:
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
def get_cross_sims(nsim0, paths, smoothed):
|
def get_cross_sims(simname, nsim0, paths, smoothed):
|
||||||
"""
|
"""
|
||||||
Get the list of cross simulations for a given reference simulation for
|
Get the list of cross simulations for a given reference simulation for
|
||||||
which the overlap has been calculated.
|
which the overlap has been calculated.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
simname : str
|
||||||
|
Simulation name.
|
||||||
nsim0 : int
|
nsim0 : int
|
||||||
Reference simulation number.
|
Reference simulation number.
|
||||||
paths : :py:class:`csiborgtools.paths.Paths`
|
paths : :py:class:`csiborgtools.paths.Paths`
|
||||||
|
@ -782,8 +789,8 @@ def get_cross_sims(nsim0, paths, smoothed):
|
||||||
for nsimx in paths.get_ics("csiborg"):
|
for nsimx in paths.get_ics("csiborg"):
|
||||||
if nsimx == nsim0:
|
if nsimx == nsim0:
|
||||||
continue
|
continue
|
||||||
f1 = paths.overlap(nsim0, nsimx, smoothed)
|
f1 = paths.overlap(simname, nsim0, nsimx, smoothed)
|
||||||
f2 = paths.overlap(nsimx, nsim0, smoothed)
|
f2 = paths.overlap(simname, nsimx, nsim0, smoothed)
|
||||||
if isfile(f1) or isfile(f2):
|
if isfile(f1) or isfile(f2):
|
||||||
nsimxs.append(nsimx)
|
nsimxs.append(nsimx)
|
||||||
return nsimxs
|
return nsimxs
|
||||||
|
|
|
@ -462,12 +462,14 @@ class Paths:
|
||||||
fname = f"out_{str(nsim).zfill(5)}_{str(nsnap).zfill(5)}.npy"
|
fname = f"out_{str(nsim).zfill(5)}_{str(nsnap).zfill(5)}.npy"
|
||||||
return join(fdir, fname)
|
return join(fdir, fname)
|
||||||
|
|
||||||
def overlap(self, nsim0, nsimx, smoothed):
|
def overlap(self, simname, nsim0, nsimx, smoothed):
|
||||||
"""
|
"""
|
||||||
Path to the overlap files between two CSiBORG simulations.
|
Path to the overlap files between two CSiBORG simulations.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
simname : str
|
||||||
|
Simulation name. Must be one of `csiborg` or `quijote`.
|
||||||
nsim0 : int
|
nsim0 : int
|
||||||
IC realisation index of the first simulation.
|
IC realisation index of the first simulation.
|
||||||
nsimx : int
|
nsimx : int
|
||||||
|
@ -479,7 +481,12 @@ class Paths:
|
||||||
-------
|
-------
|
||||||
path : str
|
path : str
|
||||||
"""
|
"""
|
||||||
|
if simname == "csiborg":
|
||||||
fdir = join(self.postdir, "overlap")
|
fdir = join(self.postdir, "overlap")
|
||||||
|
elif simname == "quijote":
|
||||||
|
fdir = join(self.quijote_dir, "overlap")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown simulation name `{simname}`.")
|
||||||
|
|
||||||
try_create_directory(fdir)
|
try_create_directory(fdir)
|
||||||
|
|
||||||
|
|
|
@ -92,6 +92,39 @@ def periodic_distance(points, reference, boxsize):
|
||||||
return dist
|
return dist
|
||||||
|
|
||||||
|
|
||||||
|
@jit(nopython=True, fastmath=True, boundscheck=False)
|
||||||
|
def periodic_distance_two_points(p1, p2, boxsize):
|
||||||
|
"""
|
||||||
|
Compute the 3D distance between two points using periodic boundary
|
||||||
|
conditions. This is an optimized JIT implementation.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
p1 : 1-dimensional array of shape `(3, )`
|
||||||
|
First point.
|
||||||
|
p2 : 1-dimensional array of shape `(3, )`
|
||||||
|
Second point.
|
||||||
|
boxsize : float
|
||||||
|
Box size.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dist : 1-dimensional array of shape `(n_points, )`
|
||||||
|
"""
|
||||||
|
half_box = boxsize / 2
|
||||||
|
|
||||||
|
dist = 0
|
||||||
|
for i in range(3):
|
||||||
|
dist_1d = abs(p1[i] - p2[i])
|
||||||
|
|
||||||
|
if dist_1d > (half_box):
|
||||||
|
dist_1d = boxsize - dist_1d
|
||||||
|
|
||||||
|
dist += dist_1d**2
|
||||||
|
|
||||||
|
return dist**0.5
|
||||||
|
|
||||||
|
|
||||||
@jit(nopython=True, fastmath=True, boundscheck=False)
|
@jit(nopython=True, fastmath=True, boundscheck=False)
|
||||||
def delta2ncells(delta):
|
def delta2ncells(delta):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -59,10 +59,10 @@ def do_auto(args, config, cats, nsim, paths):
|
||||||
-------
|
-------
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
rvs_gen = csiborgtools.clustering.RVSinsphere(args.Rmax)
|
|
||||||
knncdf = csiborgtools.clustering.kNN_1DCDF()
|
|
||||||
cat = cats[nsim]
|
cat = cats[nsim]
|
||||||
knn = cat.knn(in_initial=False)
|
rvs_gen = csiborgtools.clustering.RVSinsphere(args.Rmax, cat.boxsize)
|
||||||
|
knncdf = csiborgtools.clustering.kNN_1DCDF()
|
||||||
|
knn = cat.knn(in_initial=False, subtract_observer=False, periodic=True)
|
||||||
rs, cdf = knncdf(
|
rs, cdf = knncdf(
|
||||||
knn, rvs_gen=rvs_gen, nneighbours=config["nneighbours"],
|
knn, rvs_gen=rvs_gen, nneighbours=config["nneighbours"],
|
||||||
rmin=config["rmin"], rmax=config["rmax"],
|
rmin=config["rmin"], rmax=config["rmax"],
|
||||||
|
@ -97,9 +97,9 @@ def do_cross_rand(args, config, cats, nsim, paths):
|
||||||
-------
|
-------
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
rvs_gen = csiborgtools.clustering.RVSinsphere(args.Rmax)
|
|
||||||
cat = cats[nsim]
|
cat = cats[nsim]
|
||||||
knn1 = cat.knn(in_initial=False)
|
rvs_gen = csiborgtools.clustering.RVSinsphere(args.Rmax, cat.boxsize)
|
||||||
|
knn1 = cat.knn(in_initial=False, subtract_observer=False, periodic=True)
|
||||||
|
|
||||||
knn2 = NearestNeighbors()
|
knn2 = NearestNeighbors()
|
||||||
pos2 = rvs_gen(len(cat).shape[0])
|
pos2 = rvs_gen(len(cat).shape[0])
|
||||||
|
@ -126,7 +126,7 @@ if __name__ == "__main__":
|
||||||
help="Simulation name")
|
help="Simulation name")
|
||||||
parser.add_argument("--nsims", type=int, nargs="+", default=None,
|
parser.add_argument("--nsims", type=int, nargs="+", default=None,
|
||||||
help="Indices of simulations to cross. If `-1` processes all simulations.") # noqa
|
help="Indices of simulations to cross. If `-1` processes all simulations.") # noqa
|
||||||
parser.add_argument("--Rmax", type=float, default=155/0.705,
|
parser.add_argument("--Rmax", type=float, default=155,
|
||||||
help="High-resolution region radius") # noqa
|
help="High-resolution region radius") # noqa
|
||||||
parser.add_argument("--verbose", type=lambda x: bool(strtobool(x)),
|
parser.add_argument("--verbose", type=lambda x: bool(strtobool(x)),
|
||||||
default=False)
|
default=False)
|
||||||
|
|
|
@ -37,12 +37,12 @@ except ModuleNotFoundError:
|
||||||
|
|
||||||
|
|
||||||
def do_auto(args, config, cats, nsim, paths):
|
def do_auto(args, config, cats, nsim, paths):
|
||||||
|
cat = cats[nsim]
|
||||||
tpcf = csiborgtools.clustering.Mock2PCF()
|
tpcf = csiborgtools.clustering.Mock2PCF()
|
||||||
rvs_gen = csiborgtools.clustering.RVSinsphere(args.Rmax)
|
rvs_gen = csiborgtools.clustering.RVSinsphere(args.Rmax, cat.boxsize)
|
||||||
bins = numpy.logspace(
|
bins = numpy.logspace(
|
||||||
numpy.log10(config["rpmin"]), numpy.log10(config["rpmax"]),
|
numpy.log10(config["rpmin"]), numpy.log10(config["rpmax"]),
|
||||||
config["nrpbins"] + 1,)
|
config["nrpbins"] + 1,)
|
||||||
cat = cats[nsim]
|
|
||||||
|
|
||||||
pos = cat.position(in_initial=False, cartesian=True)
|
pos = cat.position(in_initial=False, cartesian=True)
|
||||||
nrandom = int(config["randmult"] * pos.shape[0])
|
nrandom = int(config["randmult"] * pos.shape[0])
|
||||||
|
@ -59,7 +59,7 @@ if __name__ == "__main__":
|
||||||
help="Simulation name")
|
help="Simulation name")
|
||||||
parser.add_argument("--nsims", type=int, nargs="+", default=None,
|
parser.add_argument("--nsims", type=int, nargs="+", default=None,
|
||||||
help="Indices of simulations to cross. If `-1` processes all simulations.") # noqa
|
help="Indices of simulations to cross. If `-1` processes all simulations.") # noqa
|
||||||
parser.add_argument("--Rmax", type=float, default=155/0.705,
|
parser.add_argument("--Rmax", type=float, default=155,
|
||||||
help="High-resolution region radius.")
|
help="High-resolution region radius.")
|
||||||
parser.add_argument("--verbose", type=lambda x: bool(strtobool(x)),
|
parser.add_argument("--verbose", type=lambda x: bool(strtobool(x)),
|
||||||
default=False, help="Verbosity flag.")
|
default=False, help="Verbosity flag.")
|
||||||
|
|
|
@ -14,8 +14,9 @@
|
||||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
"""
|
"""
|
||||||
Script to calculate the particle centre of mass, Lagrangian patch size in the
|
Script to calculate the particle centre of mass, Lagrangian patch size in the
|
||||||
initial snapshot. The initial snapshot particles are read from the sorted
|
initial snapshot.
|
||||||
files.
|
|
||||||
|
The initial snapshot particles are read from the sorted files.
|
||||||
"""
|
"""
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -74,7 +75,7 @@ def _main(nsim, simname, verbose):
|
||||||
|
|
||||||
# Initialise the overlapper.
|
# Initialise the overlapper.
|
||||||
if simname == "csiborg":
|
if simname == "csiborg":
|
||||||
kwargs = {"box_size": 2048, "bckg_halfsize": 475}
|
kwargs = {"box_size": 2048, "bckg_halfsize": 512}
|
||||||
else:
|
else:
|
||||||
kwargs = {"box_size": 512, "bckg_halfsize": 256}
|
kwargs = {"box_size": 512, "bckg_halfsize": 256}
|
||||||
overlapper = csiborgtools.match.ParticleOverlap(**kwargs)
|
overlapper = csiborgtools.match.ParticleOverlap(**kwargs)
|
||||||
|
@ -86,7 +87,7 @@ def _main(nsim, simname, verbose):
|
||||||
hid2map)
|
hid2map)
|
||||||
|
|
||||||
# Skip if the halo has no particles or is too small.
|
# Skip if the halo has no particles or is too small.
|
||||||
if part is None or part.size < 100:
|
if part is None or part.size < 40:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
pos, mass = part[:, :3], part[:, 3]
|
pos, mass = part[:, :3], part[:, 3]
|
||||||
|
|
|
@ -57,12 +57,13 @@ def pair_match(nsim0, nsimx, simname, sigma, verbose):
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
|
paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
|
||||||
smooth_kwargs = {"sigma": sigma, "mode": "wrap"}
|
smooth_kwargs = {"sigma": sigma, "mode": "constant", "cval": 0}
|
||||||
|
|
||||||
if simname == "csiborg":
|
if simname == "csiborg":
|
||||||
overlapper_kwargs = {"box_size": 2048, "bckg_halfsize": 475}
|
overlapper_kwargs = {"box_size": 2048, "bckg_halfsize": 512}
|
||||||
mass_kind = "fof_totpartmass"
|
mass_kind = "fof_totpartmass"
|
||||||
bounds = {mass_kind: (1e13, None)}
|
bounds = {"dist": (0, 155), mass_kind: (10**13.25, None)}
|
||||||
|
|
||||||
cat0 = csiborgtools.read.CSiBORGHaloCatalogue(
|
cat0 = csiborgtools.read.CSiBORGHaloCatalogue(
|
||||||
nsim0, paths, bounds=bounds, load_fitted=False,
|
nsim0, paths, bounds=bounds, load_fitted=False,
|
||||||
with_lagpatch=True)
|
with_lagpatch=True)
|
||||||
|
@ -72,11 +73,14 @@ def pair_match(nsim0, nsimx, simname, sigma, verbose):
|
||||||
elif simname == "quijote":
|
elif simname == "quijote":
|
||||||
overlapper_kwargs = {"box_size": 512, "bckg_halfsize": 256}
|
overlapper_kwargs = {"box_size": 512, "bckg_halfsize": 256}
|
||||||
mass_kind = "group_mass"
|
mass_kind = "group_mass"
|
||||||
bounds = {mass_kind: (1e14, None)}
|
bounds = {mass_kind: (10**13.25, None)}
|
||||||
|
|
||||||
cat0 = csiborgtools.read.QuijoteHaloCatalogue(
|
cat0 = csiborgtools.read.QuijoteHaloCatalogue(
|
||||||
nsim0, paths, 4, load_fitted=False, with_lagpatch=True)
|
nsim0, paths, 4, bounds=bounds, load_fitted=False,
|
||||||
|
with_lagpatch=True)
|
||||||
catx = csiborgtools.read.QuijoteHaloCatalogue(
|
catx = csiborgtools.read.QuijoteHaloCatalogue(
|
||||||
nsimx, paths, 4, load_fitted=False, with_lagpatch=True)
|
nsimx, paths, 4, bounds=bounds, load_fitted=False,
|
||||||
|
with_lagpatch=True)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown simulation name: `{simname}`.")
|
raise ValueError(f"Unknown simulation name: `{simname}`.")
|
||||||
|
|
||||||
|
@ -116,7 +120,7 @@ def pair_match(nsim0, nsimx, simname, sigma, verbose):
|
||||||
for j, match in enumerate(matches):
|
for j, match in enumerate(matches):
|
||||||
match_hids[i][j] = catx["index"][match]
|
match_hids[i][j] = catx["index"][match]
|
||||||
|
|
||||||
fout = paths.overlap(nsim0, nsimx, smoothed=False)
|
fout = paths.overlap(simname, nsim0, nsimx, smoothed=False)
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"{datetime.now()}: saving to ... `{fout}`.", flush=True)
|
print(f"{datetime.now()}: saving to ... `{fout}`.", flush=True)
|
||||||
numpy.savez(fout, ref_hids=cat0["index"], match_hids=match_hids,
|
numpy.savez(fout, ref_hids=cat0["index"], match_hids=match_hids,
|
||||||
|
@ -135,7 +139,7 @@ def pair_match(nsim0, nsimx, simname, sigma, verbose):
|
||||||
match_indxs, smooth_kwargs,
|
match_indxs, smooth_kwargs,
|
||||||
verbose=verbose)
|
verbose=verbose)
|
||||||
|
|
||||||
fout = paths.overlap(nsim0, nsimx, smoothed=True)
|
fout = paths.overlap(simname, nsim0, nsimx, smoothed=True)
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"{datetime.now()}: saving to ... `{fout}`.", flush=True)
|
print(f"{datetime.now()}: saving to ... `{fout}`.", flush=True)
|
||||||
numpy.savez(fout, smoothed_overlap=smoothed_overlap, sigma=sigma)
|
numpy.savez(fout, smoothed_overlap=smoothed_overlap, sigma=sigma)
|
||||||
|
|
|
@ -178,6 +178,7 @@ def plot_hmf(pdf=False):
|
||||||
csiborg5511[x > 3e15] = numpy.nan
|
csiborg5511[x > 3e15] = numpy.nan
|
||||||
|
|
||||||
with plt.style.context(plt_utils.mplstyle):
|
with plt.style.context(plt_utils.mplstyle):
|
||||||
|
cols = plt.rcParams["axes.prop_cycle"].by_key()["color"]
|
||||||
fig, ax = plt.subplots(nrows=2, sharex=True,
|
fig, ax = plt.subplots(nrows=2, sharex=True,
|
||||||
figsize=(3.5, 2.625 * 1.25),
|
figsize=(3.5, 2.625 * 1.25),
|
||||||
gridspec_kw={"height_ratios": [1, 0.45]})
|
gridspec_kw={"height_ratios": [1, 0.45]})
|
||||||
|
@ -186,15 +187,15 @@ def plot_hmf(pdf=False):
|
||||||
# Upper panel data
|
# Upper panel data
|
||||||
mean_csiborg = numpy.mean(csiborg_counts, axis=0)
|
mean_csiborg = numpy.mean(csiborg_counts, axis=0)
|
||||||
std_csiborg = numpy.std(csiborg_counts, axis=0)
|
std_csiborg = numpy.std(csiborg_counts, axis=0)
|
||||||
ax[0].plot(x, mean_csiborg, label="CSiBORG")
|
ax[0].plot(x, mean_csiborg, label="CSiBORG", c=cols[0])
|
||||||
ax[0].fill_between(x, mean_csiborg - std_csiborg,
|
ax[0].fill_between(x, mean_csiborg - std_csiborg,
|
||||||
mean_csiborg + std_csiborg, alpha=0.5)
|
mean_csiborg + std_csiborg, alpha=0.5, color=cols[0])
|
||||||
|
|
||||||
mean_quijote = numpy.mean(quijote_counts, axis=0)
|
mean_quijote = numpy.mean(quijote_counts, axis=0)
|
||||||
std_quijote = numpy.std(quijote_counts, axis=0)
|
std_quijote = numpy.std(quijote_counts, axis=0)
|
||||||
ax[0].plot(x, mean_quijote, label="Quijote")
|
ax[0].plot(x, mean_quijote, label="Quijote", c=cols[1])
|
||||||
ax[0].fill_between(x, mean_quijote - std_quijote,
|
ax[0].fill_between(x, mean_quijote - std_quijote,
|
||||||
mean_quijote + std_quijote, alpha=0.5)
|
mean_quijote + std_quijote, alpha=0.5, color=cols[1])
|
||||||
|
|
||||||
ax[0].plot(x, csiborg5511, label="CSiBORG 5511", c="k", ls="--")
|
ax[0].plot(x, csiborg5511, label="CSiBORG 5511", c="k", ls="--")
|
||||||
std5511 = numpy.sqrt(csiborg5511)
|
std5511 = numpy.sqrt(csiborg5511)
|
||||||
|
@ -204,9 +205,11 @@ def plot_hmf(pdf=False):
|
||||||
log_y = numpy.log10(mean_csiborg / mean_quijote)
|
log_y = numpy.log10(mean_csiborg / mean_quijote)
|
||||||
err = numpy.sqrt((std_csiborg / mean_csiborg / numpy.log(10))**2
|
err = numpy.sqrt((std_csiborg / mean_csiborg / numpy.log(10))**2
|
||||||
+ (std_quijote / mean_quijote / numpy.log(10))**2)
|
+ (std_quijote / mean_quijote / numpy.log(10))**2)
|
||||||
ax[1].plot(x, 10**log_y, c="gray")
|
ax[1].plot(x, 10**log_y, c=cols[0])
|
||||||
ax[1].fill_between(x, 10**(log_y - err), 10**(log_y + err), alpha=0.5,
|
ax[1].fill_between(x, 10**(log_y - err), 10**(log_y + err), alpha=0.5,
|
||||||
color="gray")
|
color=col[0])
|
||||||
|
|
||||||
|
ax[1].plot(x, csiborg5511 / mean_quijote, c="k", ls="--")
|
||||||
|
|
||||||
# Labels and accesories
|
# Labels and accesories
|
||||||
ax[1].axhline(1, color="k", ls="--",
|
ax[1].axhline(1, color="k", ls="--",
|
||||||
|
|
|
@ -143,13 +143,15 @@ def plot_mass_vs_maxpairoverlap(nsim0, nsimx):
|
||||||
|
|
||||||
|
|
||||||
@cache_to_disk(7)
|
@cache_to_disk(7)
|
||||||
def get_overlap(nsim0):
|
def get_overlap(simname, nsim0):
|
||||||
"""
|
"""
|
||||||
Calculate the summed overlap and probability of no match for a single
|
Calculate the summed overlap and probability of no match for a single
|
||||||
reference simulation.
|
reference simulation.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
simname : str
|
||||||
|
Simulation name.
|
||||||
nsim0 : int
|
nsim0 : int
|
||||||
Simulation index.
|
Simulation index.
|
||||||
|
|
||||||
|
@ -167,7 +169,8 @@ def get_overlap(nsim0):
|
||||||
Probability of no match for each halo in the reference simulation.
|
Probability of no match for each halo in the reference simulation.
|
||||||
"""
|
"""
|
||||||
paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
|
paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
|
||||||
nsimxs = csiborgtools.read.get_cross_sims(nsim0, paths, smoothed=True)
|
nsimxs = csiborgtools.read.get_cross_sims(simname, nsim0, paths,
|
||||||
|
smoothed=True)
|
||||||
cat0 = open_cat(nsim0)
|
cat0 = open_cat(nsim0)
|
||||||
|
|
||||||
catxs = []
|
catxs = []
|
||||||
|
@ -195,7 +198,7 @@ def plot_mass_vsmedmaxoverlap(nsim0):
|
||||||
nsim0 : int
|
nsim0 : int
|
||||||
Reference simulation index.
|
Reference simulation index.
|
||||||
"""
|
"""
|
||||||
x, __, max_overlap, __, __ = get_overlap(nsim0)
|
x, __, max_overlap, __, __ = get_overlap("csiborg", nsim0)
|
||||||
|
|
||||||
for i in trange(max_overlap.shape[0]):
|
for i in trange(max_overlap.shape[0]):
|
||||||
if numpy.sum(numpy.isnan(max_overlap[i, :])) > 0:
|
if numpy.sum(numpy.isnan(max_overlap[i, :])) > 0:
|
||||||
|
@ -255,7 +258,7 @@ def plot_summed_overlap_vs_mass(nsim0):
|
||||||
-------
|
-------
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
x, __, __, summed_overlap, prob_nomatch = get_overlap(nsim0)
|
x, __, __, summed_overlap, prob_nomatch = get_overlap("csiborg", nsim0)
|
||||||
del __
|
del __
|
||||||
collect()
|
collect()
|
||||||
|
|
||||||
|
@ -389,12 +392,14 @@ def plot_mass_vs_separation(nsim0, nsimx, plot_std=False, min_overlap=0.0):
|
||||||
|
|
||||||
|
|
||||||
@cache_to_disk(7)
|
@cache_to_disk(7)
|
||||||
def get_max_key(nsim0, key):
|
def get_max_key(simname, nsim0, key):
|
||||||
"""
|
"""
|
||||||
Get the value of a maximum overlap halo's property.
|
Get the value of a maximum overlap halo's property.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
simname : str
|
||||||
|
Simulation name.
|
||||||
nsim0 : int
|
nsim0 : int
|
||||||
Reference simulation index.
|
Reference simulation index.
|
||||||
key : str
|
key : str
|
||||||
|
@ -412,7 +417,8 @@ def get_max_key(nsim0, key):
|
||||||
Value of the property of the maximum overlap halo.
|
Value of the property of the maximum overlap halo.
|
||||||
"""
|
"""
|
||||||
paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
|
paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
|
||||||
nsimxs = csiborgtools.read.get_cross_sims(nsim0, paths, smoothed=True)
|
nsimxs = csiborgtools.read.get_cross_sims(simname, nsim0, paths,
|
||||||
|
smoothed=True)
|
||||||
nsimxs = nsimxs
|
nsimxs = nsimxs
|
||||||
cat0 = open_cat(nsim0)
|
cat0 = open_cat(nsim0)
|
||||||
|
|
||||||
|
@ -440,7 +446,7 @@ def plot_maxoverlap_mass(nsim0):
|
||||||
nsim0 : int
|
nsim0 : int
|
||||||
Reference simulation index.
|
Reference simulation index.
|
||||||
"""
|
"""
|
||||||
mass0, __, __, stat = get_max_key(nsim0, "totpartmass")
|
mass0, __, __, stat = get_max_key("csiborg", nsim0, "totpartmass")
|
||||||
|
|
||||||
mu = numpy.mean(stat, axis=1)
|
mu = numpy.mean(stat, axis=1)
|
||||||
std = numpy.std(numpy.log10(stat), axis=1)
|
std = numpy.std(numpy.log10(stat), axis=1)
|
||||||
|
@ -463,8 +469,10 @@ def plot_maxoverlap_mass(nsim0):
|
||||||
|
|
||||||
axs[0].set_xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
|
axs[0].set_xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
|
||||||
axs[1].set_xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
|
axs[1].set_xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
|
||||||
axs[0].set_ylabel(r"Max. overlap mean of $\log M_{\rm tot} ~ [M_\odot / h]$")
|
axs[0].set_ylabel(
|
||||||
axs[1].set_ylabel(r"Max. overlap std. of $\log M_{\rm tot} ~ [M_\odot / h]$")
|
r"Max. overlap mean of $\log M_{\rm tot} ~ [M_\odot / h]$")
|
||||||
|
axs[1].set_ylabel(
|
||||||
|
r"Max. overlap std. of $\log M_{\rm tot} ~ [M_\odot / h]$")
|
||||||
|
|
||||||
ims = [im0, im1]
|
ims = [im0, im1]
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
|
@ -498,7 +506,7 @@ def plot_maxoverlapstat(nsim0, key):
|
||||||
Property to get.
|
Property to get.
|
||||||
"""
|
"""
|
||||||
assert key != "totpartmass"
|
assert key != "totpartmass"
|
||||||
mass0, key_val, __, stat = get_max_key(nsim0, key)
|
mass0, key_val, __, stat = get_max_key("csiborg", nsim0, key)
|
||||||
|
|
||||||
xlabels = {"lambda200c": r"\log \lambda_{\rm 200c}"}
|
xlabels = {"lambda200c": r"\log \lambda_{\rm 200c}"}
|
||||||
key_label = xlabels.get(key, key)
|
key_label = xlabels.get(key, key)
|
||||||
|
@ -546,13 +554,15 @@ def plot_maxoverlapstat(nsim0, key):
|
||||||
|
|
||||||
|
|
||||||
@cache_to_disk(7)
|
@cache_to_disk(7)
|
||||||
def get_expected_mass(nsim0, min_overlap):
|
def get_expected_mass(simname, nsim0, min_overlap):
|
||||||
"""
|
"""
|
||||||
Get the expected mass of a reference halo given its overlap with halos
|
Get the expected mass of a reference halo given its overlap with halos
|
||||||
from other simulations.
|
from other simulations.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
simname : str
|
||||||
|
Simulation name.
|
||||||
nsim0 : int
|
nsim0 : int
|
||||||
Reference simulation index.
|
Reference simulation index.
|
||||||
min_overlap : float
|
min_overlap : float
|
||||||
|
@ -570,7 +580,8 @@ def get_expected_mass(nsim0, min_overlap):
|
||||||
Probability of not matching the reference halo.
|
Probability of not matching the reference halo.
|
||||||
"""
|
"""
|
||||||
paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
|
paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
|
||||||
nsimxs = csiborgtools.read.get_cross_sims(nsim0, paths, smoothed=True)
|
nsimxs = csiborgtools.read.get_cross_sims(simname, nsim0, paths,
|
||||||
|
smoothed=True)
|
||||||
nsimxs = nsimxs
|
nsimxs = nsimxs
|
||||||
cat0 = open_cat(nsim0)
|
cat0 = open_cat(nsim0)
|
||||||
|
|
||||||
|
@ -602,7 +613,8 @@ def plot_mass_vs_expected_mass(nsim0, min_overlap=0, max_prob_nomatch=1):
|
||||||
max_prob_nomatch : float, optional
|
max_prob_nomatch : float, optional
|
||||||
Maximum probability of no match to consider.
|
Maximum probability of no match to consider.
|
||||||
"""
|
"""
|
||||||
mass, mu, std, prob_nomatch = get_expected_mass(nsim0, min_overlap)
|
mass, mu, std, prob_nomatch = get_expected_mass("csiborg", nsim0,
|
||||||
|
min_overlap)
|
||||||
|
|
||||||
std = std / mu / numpy.log(10)
|
std = std / mu / numpy.log(10)
|
||||||
mass = numpy.log10(mass)
|
mass = numpy.log10(mass)
|
||||||
|
@ -1343,7 +1355,7 @@ def plot_kl_vs_overlap(runs, nsim, kwargs, runs_to_mass, plot_std=True,
|
||||||
for run in runs:
|
for run in runs:
|
||||||
nn_data = nn_reader.read_single("csiborg", run, nsim, nobs=None)
|
nn_data = nn_reader.read_single("csiborg", run, nsim, nobs=None)
|
||||||
nn_hindxs = nn_data["ref_hindxs"]
|
nn_hindxs = nn_data["ref_hindxs"]
|
||||||
mass, overlap_hindxs, __, summed_overlap, prob_nomatch = get_overlap(nsim) # noqa
|
mass, overlap_hindxs, __, summed_overlap, prob_nomatch = get_overlap("csiborg", nsim) # noqa
|
||||||
|
|
||||||
# We need to match the hindxs between the two.
|
# We need to match the hindxs between the two.
|
||||||
hind2overlap_array = {hind: i for i, hind in enumerate(overlap_hindxs)}
|
hind2overlap_array = {hind: i for i, hind in enumerate(overlap_hindxs)}
|
||||||
|
|
19
setup.py
19
setup.py
|
@ -1,5 +1,24 @@
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
# List of dependencies:
|
||||||
|
# - Corrfunc -> To be moved to a separate package.
|
||||||
|
# - NumPy
|
||||||
|
# - SciPy
|
||||||
|
# - Numba
|
||||||
|
# - Pylians
|
||||||
|
# - tqdm
|
||||||
|
# - healpy
|
||||||
|
# - astropy
|
||||||
|
# - scikit-learn
|
||||||
|
# - joblib
|
||||||
|
# - h5py
|
||||||
|
# - MPI
|
||||||
|
# - pyyaml
|
||||||
|
# - taskmaster
|
||||||
|
# - matplotlib
|
||||||
|
# - scienceplots
|
||||||
|
# - cache_to_disk
|
||||||
|
|
||||||
|
|
||||||
BUILD_REQ = ["numpy", "scipy"]
|
BUILD_REQ = ["numpy", "scipy"]
|
||||||
INSTALL_REQ = BUILD_REQ
|
INSTALL_REQ = BUILD_REQ
|
||||||
|
|
Loading…
Reference in a new issue