Skip to content

Commit 7dc5747

Browse files
authored
Merge branch 'main' into galaxyq
2 parents 7f70ffd + b2840ce commit 7dc5747

File tree

11 files changed

+304
-15
lines changed

11 files changed

+304
-15
lines changed

.zenodo.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@
4343
"affiliation": "University of Manchester",
4444
"name": "Juan Pablo Cordero"
4545
},
46+
{
47+
"orcid": "0000-0003-1560-7959",
48+
"affiliation": "University of Portsmouth",
49+
"name": "Fox Davidson"
50+
},
4651
{
4752
"orcid": "0000-0002-8218-563X",
4853
"affiliation": "University of Portsmouth",

docs/utils/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ create their own named `speclite.filters.FilterResponse`.
143143
luminosity_in_band
144144
mag_ab
145145
SpectrumTemplates
146+
magnitude_error_rykoff
147+
logistic_completeness_function
146148

147149

148150
Random sampling (`skypy.utils.random`)

skypy/galaxies/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
__all__ = [
77
'schechter_lf',
8+
'schechter_smf',
89
]
910

1011
from . import luminosity # noqa F401,F403

skypy/galaxies/spectrum.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,28 @@ def stellar_mass(self, coefficients, magnitudes, filter):
143143
Mt = self.absolute_magnitudes(coefficients, filter)
144144
return np.power(10, 0.4*(Mt-magnitudes))
145145

146+
def stellar_mass_remain(self, coefficients, magnitudes, filter):
147+
r'''Compute total surviving stellar mass from absolute magnitudes.
148+
149+
Parameters
150+
----------
151+
coefficients : (ng, nt) array_like
152+
Array of template coefficients.
153+
magnitudes : (ng,) array_like
154+
The magnitudes to match in the reference bandpass.
155+
filter : str
156+
A single reference bandpass filter specification for
157+
`~speclite.filters.load_filters`.
158+
159+
Returns
160+
-------
161+
stellar_mass : (ng,) array_like
162+
Total surviving stellar mass of each galaxy in template units.
163+
'''
164+
m = self.stellar_mass(coefficients, magnitudes, filter)
165+
m *= np.dot(coefficients, self.mremain)
166+
return m
167+
146168
def metallicity(self, coefficients):
147169
r'''Galaxy metallicities from kcorrect templates.
148170

skypy/galaxies/stellar_mass.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import numpy as np
55

66
from ..utils.random import schechter
7+
from ..utils import dependent_argument
78

89

910
__all__ = [
@@ -15,6 +16,8 @@
1516
]
1617

1718

19+
@dependent_argument('m_star', 'redshift')
20+
@dependent_argument('alpha', 'redshift')
1821
def schechter_smf_mass(redshift, alpha, m_star, m_min, m_max, size=None,
1922
resolution=1000):
2023
r""" Stellar masses following the Schechter mass function [1]_.
@@ -23,9 +26,10 @@ def schechter_smf_mass(redshift, alpha, m_star, m_min, m_max, size=None,
2326
----------
2427
redshift : array_like
2528
Galaxy redshifts for which to sample magnitudes.
26-
alpha : float
27-
The alpha parameter in the Schechter stellar mass function.
28-
m_star : (nm,) array-like
29+
alpha : float or function
30+
The alpha parameter in the Schechter stellar mass function. If function,
31+
it must return a scalar value.
32+
m_star : (nm,) array-like or function
2933
Characteristic stellar mass m_*.
3034
size: int, optional
3135
Output shape of stellar mass samples. If size is None and m_star

skypy/galaxies/tests/test_spectrum.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,39 @@ def test_kcorrect_stellar_mass():
158158
np.testing.assert_allclose(stellar_mass, truth)
159159

160160

161+
@pytest.mark.skipif(not HAS_SPECLITE, reason='test requires speclite')
162+
def test_kcorrect_stellar_mass_remain():
163+
164+
from astropy import units
165+
from skypy.galaxies.spectrum import kcorrect
166+
from speclite.filters import FilterResponse
167+
168+
# Gaussian bandpass
169+
filt_lam = np.logspace(3, 4, 1000) * units.AA
170+
filt_mean = 5000 * units.AA
171+
filt_width = 100 * units.AA
172+
filt_tx = np.exp(-((filt_lam-filt_mean)/filt_width)**2)
173+
filt_tx[[0, -1]] = 0
174+
FilterResponse(wavelength=filt_lam, response=filt_tx,
175+
meta=dict(group_name='test', band_name='filt'))
176+
177+
# Using the identity matrix for the coefficients yields trivial test cases
178+
coeff = np.eye(5)
179+
Mt = kcorrect.absolute_magnitudes(coeff, 'test-filt')
180+
181+
# Using the absolute magnitudes of the templates as reference magnitudes
182+
# should return one solar mass for each template.
183+
stellar_mass = kcorrect.stellar_mass_remain(coeff, Mt, 'test-filt')
184+
truth = kcorrect.mremain
185+
np.testing.assert_allclose(stellar_mass, truth)
186+
187+
# Solution for given magnitudes without template mixing
188+
Mb = np.array([10, 20, 30, 40, 50])
189+
stellar_mass = kcorrect.stellar_mass_remain(coeff, Mb, 'test-filt')
190+
truth = np.power(10, -0.4*(Mb-Mt)) * kcorrect.mremain
191+
np.testing.assert_allclose(stellar_mass, truth)
192+
193+
161194
def test_kcorrect_metallicity():
162195

163196
from skypy.galaxies.spectrum import kcorrect

skypy/galaxies/tests/test_stellar_mass.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import scipy.integrate
44
from scipy.special import gammaln
55
import pytest
6+
67
from hypothesis import given
78
from hypothesis.strategies import integers
9+
from astropy.modeling.models import Exponential1D
810

911
from skypy.galaxies import stellar_mass
1012
from skypy.utils import special
@@ -27,12 +29,30 @@ def test_stellar_masses():
2729
with pytest.raises(ValueError):
2830
stellar_mass.schechter_smf_mass(0., -1.4, np.array([1e10, 2e10]), 1.e7, 1.e13, size=3)
2931

30-
# Test that an array with the sme shape as m_star is returned if m_star is
32+
# Test that an array with the same shape as m_star is returned if m_star is
3133
# an array and size = None
3234
m_star = np.array([1e10, 2e10])
3335
sample = stellar_mass.schechter_smf_mass(0., -1.4, m_star, 1.e7, 1.e13, size=None)
3436
assert m_star.shape == sample.shape
3537

38+
# Test m_star can be a function that returns an array of values for each redshift
39+
redshift = np.linspace(0, 2, 100)
40+
alpha = 0.357
41+
m_min = 10 ** 7
42+
m_max = 10 ** 14
43+
m_star_function = Exponential1D(10**10.626, np.log(10)/0.095)
44+
sample = stellar_mass.schechter_smf_mass(redshift, alpha, m_star_function, m_min, m_max)
45+
assert sample.shape == redshift.shape
46+
47+
# Test alpha can be a function returning a scalar value.
48+
sample = stellar_mass.schechter_smf_mass(redshift, lambda z: alpha, 10**10.626, m_min, m_max)
49+
assert sample.shape == redshift.shape
50+
51+
# Sampling with an array for alpha is not implemented
52+
alpha_array = np.full_like(redshift, alpha)
53+
with pytest.raises(NotImplementedError, match='only scalar alpha is supported'):
54+
sample = stellar_mass.schechter_smf_mass(redshift, alpha_array, m_star, m_min, m_max)
55+
3656
# Test that sampling corresponds to sampling from the right pdf.
3757
# For this, we sample an array of luminosities for redshift z = 1.0 and we
3858
# compare it to the corresponding cdf.

skypy/utils/photometry.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from abc import ABCMeta, abstractmethod
77
import numpy as np
8+
import scipy.special
89

910

1011
__all__ = [
@@ -13,6 +14,8 @@
1314
'luminosity_in_band',
1415
'mag_ab',
1516
'SpectrumTemplates',
17+
'magnitude_error_rykoff',
18+
'logistic_completeness_function',
1619
]
1720

1821
try:
@@ -304,3 +307,141 @@ def absolute_magnitude_from_luminosity(luminosity, zeropoint=None):
304307
zeropoint = -luminosity_in_band[zeropoint]
305308

306309
return -2.5*np.log10(luminosity) - zeropoint
310+
311+
312+
def magnitude_error_rykoff(magnitude, magnitude_limit, magnitude_zp, a, b, error_limit=np.inf):
313+
r"""Magnitude error according to the model from Rykoff et al. (2015).
314+
315+
Given an apparent magnitude calculate the magnitude error that is introduced
316+
by the survey specifications and follows the model described in Rykoff et al. (2015).
317+
318+
Parameters
319+
----------
320+
magnitude: array_like
321+
Apparent magnitude. This and the other array_like parameters must
322+
be broadcastable to the same shape.
323+
magnitude_limit: array_like
324+
:math:`10\sigma` limiting magnitude of the survey. This and the other
325+
array_like parameters must be broadcastable to the same shape.
326+
magnitude_zp: array_like
327+
Zero-point magnitude of the survey. This and the other array_like parameters must
328+
be broadcastable to the same shape.
329+
a,b: array_like
330+
Model parameters: a is the intercept and
331+
b is the slope of the logarithmic effective time.
332+
These and the other array_like parameters must be broadcastable to the same shape.
333+
error_limit: float, optional
334+
Upper limit of the returned error. If given, all values larger than this value
335+
will be set to error_limit. Default is None.
336+
337+
Returns
338+
-------
339+
error: ndarray
340+
The apparent magnitude error in the Rykoff et al. (2015) model. This is a scalar
341+
if magnitude, magnitude_limit, magnitude_zp, a and b are scalars.
342+
343+
Notes
344+
-----
345+
Rykoff et al. (2015) (see [1]_) describe the error of the apparent magnitude :math:`m` as
346+
347+
.. math::
348+
349+
\sigma_m(m;m_{\mathrm{lim}}, t_{\mathrm{eff}}) &=
350+
\sigma_m(F(m);F_{\mathrm{noise}}(m_{\mathrm{lim}}), t_{\mathrm{eff}}) \\
351+
&= \frac{2.5}{\ln(10)} \left[ \frac{1}{Ft_{\mathrm{eff}}}
352+
\left( 1 + \frac{F_{\mathrm{noise}}}{F} \right) \right]^{1/2} \;,
353+
354+
where
355+
356+
.. math::
357+
358+
F=10^{-0.4(m - m_{\mathrm{ZP}})}
359+
360+
is the source's flux,
361+
362+
.. math::
363+
364+
F_\mathrm{noise} = \frac{F_{\mathrm{lim}}^2 t_{\mathrm{eff}}}{10^2} - F_{\mathrm{lim}}
365+
366+
is the effective noise flux and :math:`t_\mathrm{eff}` is the effective exposure time
367+
(we absorbed the normalisation constant :math:`k` in the definition of
368+
:math:`t_\mathrm{eff}`).
369+
Furthermore, :math:`m_\mathrm{ZP}` is the zero-point magnitude of the survey and
370+
:math:`F_\mathrm{lim}` is the :math:`10\sigma` limiting flux.
371+
Accordingly, :math:`m_\mathrm{lim}` is the :math:`10\sigma` limiting magnitud
372+
associated with :math:`F_\mathrm{lim}`.
373+
374+
The effective exposure time is described by
375+
376+
.. math::
377+
378+
\ln{t_\mathrm{eff}} = a + b(m_\mathrm{lim} - 21)\;,
379+
380+
where :math:`a` and :math:`b` are free parameters.
381+
382+
Further note that the model was originally used for SDSS galaxy photometry.
383+
384+
References
385+
----------
386+
.. [1] Rykoff E. S., Rozo E., Keisler R., 2015, eprint arXiv:1509.00870
387+
388+
"""
389+
390+
flux = luminosity_from_absolute_magnitude(magnitude, -magnitude_zp)
391+
flux_limit = luminosity_from_absolute_magnitude(magnitude_limit, -magnitude_zp)
392+
t_eff = np.exp(a + b * np.subtract(magnitude_limit, 21.0))
393+
flux_noise = np.square(flux_limit / 10) * t_eff - flux_limit
394+
error = 2.5 / np.log(10) * np.sqrt((1 + flux_noise / flux) / (flux * t_eff))
395+
396+
return np.minimum(error, error_limit)
397+
398+
399+
def logistic_completeness_function(magnitude, magnitude_95, magnitude_50):
400+
r'''Logistic completeness function.
401+
402+
This function calculates the logistic completeness function (based on eq. (7) in
403+
López-Sanjuan, C. et al. (2017) [1]_.
404+
405+
.. math::
406+
407+
p(m) = \frac{1}{1 + \exp[\kappa (m - m_{50})]}\;,
408+
409+
which describes the probability :math:`p(m)` that an object of magnitude :math:`m` is detected
410+
in a specific band and with
411+
412+
.. math::
413+
414+
\kappa = \frac{\ln(\frac{1}{19})}{m_{95} - m_{50}}\;.
415+
416+
Here, :math:`m_{95}` and :math:`m_{50}` are the 95% and 50% completeness
417+
magnitudes, respectively.
418+
419+
Parameters
420+
----------
421+
magnitude : array_like
422+
Magnitudes. Can be multidimensional for computing with multiple filter bands.
423+
magnitude_95 : scalar or 1-D array_like
424+
95% completeness magnitude.
425+
If `magnitude_50` is 1-D array it has to be scalar or 1-D array of the same shape.
426+
magnitude_50 : scalar or 1-D array_like
427+
50% completeness magnitude.
428+
If `magnitude_95` is 1-D array it has to be scalar or 1-D array of the same shape.
429+
430+
Returns
431+
-------
432+
probability : scalar or array_like
433+
Probability of detecting an object with magnitude :math:`m`.
434+
Returns array_like of the same shape as magnitude.
435+
Exemption: If magnitude is scalar and `magnitude_95` or `magnitude_50`
436+
is array_like of shape (nb, ) it returns array_like of shape (nb, ).
437+
438+
References
439+
-----------
440+
.. [1] López-Sanjuan, C. et al., `2017A&A…599A..62L`_
441+
.. _2017A&A…599A..62L: https://ui.adsabs.harvard.edu/abs/2017A%26A...599A..62L
442+
443+
'''
444+
445+
kappa = np.log(1. / 19) / np.subtract(magnitude_95, magnitude_50)
446+
arg = kappa * np.subtract(magnitude, magnitude_50)
447+
return scipy.special.expit(-arg)

skypy/utils/tests/test_photometry.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,58 @@ def test_speclite_not_installed():
175175
filter = 'bessell-B'
176176
with pytest.raises(ImportError):
177177
mag_ab(wavelength, spectrum, filter)
178+
179+
180+
def test_magnitude_error_rykoff():
181+
from skypy.utils.photometry import magnitude_error_rykoff
182+
183+
# Test broadcasting to same shape given array for each parameter and
184+
# test for correct result.
185+
magnitude = np.full((2, 1, 1, 1, 1), 21)
186+
magnitude_limit = np.full((3, 1, 1, 1), 21)
187+
magnitude_zp = np.full((5, 1, 1), 21)
188+
a = np.full((7, 1), np.log(200))
189+
b = np.zeros(11)
190+
error = magnitude_error_rykoff(magnitude, magnitude_limit, magnitude_zp, a, b)
191+
# test result
192+
assert np.allclose(error, 0.25 / np.log(10))
193+
# test shape
194+
assert error.shape == (2, 3, 5, 7, 11)
195+
196+
# second test for result
197+
magnitude = 20
198+
magnitude_limit = 22.5
199+
magnitude_zp = 25
200+
b = 2
201+
a = np.log(10) - 1.5 * b
202+
error = magnitude_error_rykoff(magnitude, magnitude_limit, magnitude_zp, a, b)
203+
assert np.isclose(error, 0.25 / np.log(10) / np.sqrt(10))
204+
205+
# test that error limit is returned if error is larger than error_limit
206+
# The following set-up would give a value larger than 10
207+
magnitude = 30
208+
magnitude_limit = 25
209+
magnitude_zp = 30
210+
a = 0.5
211+
b = 1.0
212+
error_limit = 1
213+
error = magnitude_error_rykoff(magnitude, magnitude_limit, magnitude_zp, a, b, error_limit)
214+
assert error == error_limit
215+
216+
217+
def test_logistic_completeness_function():
218+
from skypy.utils.photometry import logistic_completeness_function
219+
220+
# Test that arguments broadcast correctly
221+
m = np.full((2, 1, 1), 21)
222+
m95 = np.full((3, 1), 22)
223+
m50 = np.full(5, 23)
224+
p = logistic_completeness_function(m, m95, m50)
225+
assert p.shape == np.broadcast(m, m95, m50).shape
226+
227+
# Test result of completeness function for different given magnitudes
228+
m95 = 24
229+
m50 = 25
230+
m = [np.finfo(np.float64).min, m95, m50, 2*m50-m95, np.finfo(np.float64).max]
231+
p = logistic_completeness_function(m, m95, m50)
232+
assert np.allclose(p, [1, 0.95, 0.5, 0.05, 0])

0 commit comments

Comments
 (0)