Skip to content

Commit 503dc53

Browse files
authored
Merge pull request #65 from CosmoStat/feat/size-conversions
feat: size module — Gaussian size-conversion web (T, sigma, r50, FWHM)
2 parents ce8d159 + a8771ad commit 503dc53

2 files changed

Lines changed: 305 additions & 0 deletions

File tree

cs_util/size.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
"""SIZE.
2+
3+
:Name: size.py
4+
5+
:Description: Conversions between the Gaussian object-size measures
6+
used across the UNIONS / ShapePipe stack.
7+
8+
All conversions assume a circular Gaussian profile, for which the
9+
measures are related through the scale parameter ``sigma``:
10+
11+
- ``T = 2 sigma^2`` — the ngmix / DES area parameter (arcsec^2),
12+
- ``r50 = sqrt(2 ln 2) sigma = 1.17741 sigma`` — the half-light
13+
radius (the primary size in the UNIONS shape-catalogue papers),
14+
- ``FWHM = 2 sqrt(2 ln 2) sigma = 2.35482 sigma``.
15+
16+
These functions are the single source of truth for the size web;
17+
producers (ShapePipe) and consumers (sp_validation) should import
18+
from here rather than re-deriving the factors locally.
19+
20+
:Author: Cail Daley <cailmdaley@gmail.com>
21+
22+
"""
23+
24+
import numpy as np
25+
26+
# Half-light radius of a circular Gaussian per unit sigma:
27+
# r50 = sqrt(2 ln 2) * sigma
28+
SIGMA_TO_R50 = np.sqrt(2 * np.log(2))
29+
30+
# Full width at half maximum of a Gaussian per unit sigma:
31+
# FWHM = 2 sqrt(2 ln 2) * sigma
32+
SIGMA_TO_FWHM = 2 * np.sqrt(2 * np.log(2))
33+
34+
35+
def T_to_sigma(T):
36+
"""T To Sigma.
37+
38+
Gaussian scale ``sigma = sqrt(T / 2)`` from the ngmix area
39+
parameter ``T = 2 sigma^2``.
40+
41+
Parameters
42+
----------
43+
T : float or numpy.ndarray
44+
ngmix area parameter, ``T = 2 sigma^2``
45+
46+
Returns
47+
-------
48+
float or numpy.ndarray
49+
Gaussian scale ``sigma``, in units of ``sqrt(T)``
50+
51+
"""
52+
return np.sqrt(T / 2)
53+
54+
55+
def sigma_to_T(sigma):
56+
"""Sigma To T.
57+
58+
ngmix area parameter ``T = 2 sigma^2`` from the Gaussian scale.
59+
60+
Parameters
61+
----------
62+
sigma : float or numpy.ndarray
63+
Gaussian scale
64+
65+
Returns
66+
-------
67+
float or numpy.ndarray
68+
ngmix area parameter ``T``, in units of ``sigma^2``
69+
70+
"""
71+
return 2 * sigma ** 2
72+
73+
74+
def sigma_to_r50(sigma):
75+
"""Sigma To R50.
76+
77+
Half-light radius ``r50 = sqrt(2 ln 2) sigma`` of a circular
78+
Gaussian.
79+
80+
Parameters
81+
----------
82+
sigma : float or numpy.ndarray
83+
Gaussian scale
84+
85+
Returns
86+
-------
87+
float or numpy.ndarray
88+
half-light radius, in units of ``sigma``
89+
90+
"""
91+
return SIGMA_TO_R50 * sigma
92+
93+
94+
def r50_to_sigma(r50):
95+
"""R50 To Sigma.
96+
97+
Gaussian scale from the half-light radius.
98+
99+
Parameters
100+
----------
101+
r50 : float or numpy.ndarray
102+
half-light radius
103+
104+
Returns
105+
-------
106+
float or numpy.ndarray
107+
Gaussian scale ``sigma``, in units of ``r50``
108+
109+
"""
110+
return r50 / SIGMA_TO_R50
111+
112+
113+
def sigma_to_fwhm(sigma):
114+
"""Sigma To Fwhm.
115+
116+
Full width at half maximum ``FWHM = 2 sqrt(2 ln 2) sigma`` of a
117+
Gaussian.
118+
119+
Parameters
120+
----------
121+
sigma : float or numpy.ndarray
122+
Gaussian scale
123+
124+
Returns
125+
-------
126+
float or numpy.ndarray
127+
FWHM, in units of ``sigma``
128+
129+
"""
130+
return SIGMA_TO_FWHM * sigma
131+
132+
133+
def fwhm_to_sigma(fwhm):
134+
"""Fwhm To Sigma.
135+
136+
Gaussian scale from the full width at half maximum.
137+
138+
Parameters
139+
----------
140+
fwhm : float or numpy.ndarray
141+
full width at half maximum
142+
143+
Returns
144+
-------
145+
float or numpy.ndarray
146+
Gaussian scale ``sigma``, in units of ``fwhm``
147+
148+
"""
149+
return fwhm / SIGMA_TO_FWHM
150+
151+
152+
def T_to_r50(T):
153+
"""T To R50.
154+
155+
Half-light radius ``r50 = sqrt(2 ln 2) * sqrt(T / 2)
156+
= sqrt(ln 2 * T)`` from the ngmix area parameter.
157+
158+
Parameters
159+
----------
160+
T : float or numpy.ndarray
161+
ngmix area parameter, ``T = 2 sigma^2``
162+
163+
Returns
164+
-------
165+
float or numpy.ndarray
166+
half-light radius, in units of ``sqrt(T)``
167+
168+
"""
169+
return sigma_to_r50(T_to_sigma(T))
170+
171+
172+
def r50_to_T(r50):
173+
"""R50 To T.
174+
175+
ngmix area parameter ``T = 2 (r50 / sqrt(2 ln 2))^2 = r50^2 / ln 2``
176+
from the half-light radius.
177+
178+
Parameters
179+
----------
180+
r50 : float or numpy.ndarray
181+
half-light radius
182+
183+
Returns
184+
-------
185+
float or numpy.ndarray
186+
ngmix area parameter ``T``, in units of ``r50^2``
187+
188+
"""
189+
return sigma_to_T(r50_to_sigma(r50))
190+
191+
192+
def T_to_fwhm(T):
193+
"""T To Fwhm.
194+
195+
Full width at half maximum ``FWHM = 2 sqrt(2 ln 2) * sqrt(T / 2)``
196+
from the ngmix area parameter.
197+
198+
Note that ``T`` is an *area*: the conversion to the length ``FWHM``
199+
involves a square root, not a linear factor.
200+
201+
Parameters
202+
----------
203+
T : float or numpy.ndarray
204+
ngmix area parameter, ``T = 2 sigma^2``
205+
206+
Returns
207+
-------
208+
float or numpy.ndarray
209+
FWHM, in units of ``sqrt(T)``
210+
211+
"""
212+
return sigma_to_fwhm(T_to_sigma(T))

cs_util/tests/test_size.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""UNIT TESTS FOR SIZE SUBPACKAGE.
2+
3+
This module contains unit tests for the size subpackage.
4+
5+
"""
6+
7+
import numpy as np
8+
from numpy import testing as npt
9+
10+
from unittest import TestCase
11+
12+
from cs_util import size
13+
14+
15+
class SizeTestCase(TestCase):
16+
"""Test case for the ``size`` module."""
17+
18+
def setUp(self):
19+
"""Set test parameter values."""
20+
# Unit-sigma Gaussian: T = 2, r50 = 1.17741, FWHM = 2.35482
21+
self._sigma = 1.0
22+
self._T = 2.0
23+
self._r50 = 1.1774100226
24+
self._fwhm = 2.3548200450
25+
26+
def tearDown(self):
27+
"""Unset test parameter values."""
28+
self._sigma = None
29+
self._T = None
30+
self._r50 = None
31+
self._fwhm = None
32+
33+
def test_constants(self):
34+
"""Test the module constants against their closed forms."""
35+
npt.assert_almost_equal(size.SIGMA_TO_R50, np.sqrt(2 * np.log(2)))
36+
npt.assert_almost_equal(size.SIGMA_TO_FWHM, 2 * np.sqrt(2 * np.log(2)))
37+
npt.assert_almost_equal(size.SIGMA_TO_R50, 1.17741, decimal=5)
38+
npt.assert_almost_equal(size.SIGMA_TO_FWHM, 2.35482, decimal=5)
39+
40+
def test_unit_sigma_values(self):
41+
"""Test all conversions on the unit-sigma Gaussian."""
42+
npt.assert_almost_equal(size.T_to_sigma(self._T), self._sigma)
43+
npt.assert_almost_equal(size.sigma_to_T(self._sigma), self._T)
44+
npt.assert_almost_equal(size.sigma_to_r50(self._sigma), self._r50)
45+
npt.assert_almost_equal(size.sigma_to_fwhm(self._sigma), self._fwhm)
46+
npt.assert_almost_equal(size.T_to_r50(self._T), self._r50)
47+
npt.assert_almost_equal(size.T_to_fwhm(self._T), self._fwhm)
48+
49+
def test_T_to_r50_closed_form(self):
50+
"""Test ``T_to_r50(T) == sqrt(ln 2 * T)``."""
51+
T = np.array([0.05, 0.18, 2.0, 10.0])
52+
npt.assert_allclose(size.T_to_r50(T), np.sqrt(np.log(2) * T))
53+
54+
def test_inverse_direct_values(self):
55+
"""Test each inverse converter directly on the unit-sigma case."""
56+
npt.assert_almost_equal(size.r50_to_sigma(self._r50), self._sigma)
57+
npt.assert_almost_equal(size.fwhm_to_sigma(self._fwhm), self._sigma)
58+
npt.assert_almost_equal(size.r50_to_T(self._r50), self._T)
59+
60+
def test_nonpositive_T(self):
61+
"""Test that T = 0 maps to size 0 and negative T to NaN."""
62+
npt.assert_equal(size.T_to_sigma(0.0), 0.0)
63+
npt.assert_equal(size.T_to_r50(0.0), 0.0)
64+
npt.assert_equal(size.T_to_fwhm(0.0), 0.0)
65+
with np.errstate(invalid="ignore"):
66+
self.assertTrue(np.isnan(size.T_to_r50(-1.0)))
67+
68+
def test_round_trips(self):
69+
"""Test that inverse pairs compose to the identity."""
70+
values = np.array([0.01, 0.1, 1.0, 7.5])
71+
npt.assert_allclose(size.r50_to_T(size.T_to_r50(values)), values)
72+
npt.assert_allclose(size.T_to_r50(size.r50_to_T(values)), values)
73+
npt.assert_allclose(
74+
size.sigma_to_T(size.T_to_sigma(values)), values
75+
)
76+
npt.assert_allclose(
77+
size.r50_to_sigma(size.sigma_to_r50(values)), values
78+
)
79+
npt.assert_allclose(
80+
size.fwhm_to_sigma(size.sigma_to_fwhm(values)), values
81+
)
82+
83+
def test_fwhm_is_twice_r50(self):
84+
"""Test ``FWHM = 2 r50`` for a Gaussian, via both paths."""
85+
T = np.array([0.05, 0.18, 2.0])
86+
npt.assert_allclose(size.T_to_fwhm(T), 2 * size.T_to_r50(T))
87+
88+
def test_array_input(self):
89+
"""Test that array input returns arrays of the same shape."""
90+
T = np.linspace(0.01, 5.0, 11)
91+
r50 = size.T_to_r50(T)
92+
self.assertEqual(r50.shape, T.shape)
93+
self.assertTrue(np.all(np.diff(r50) > 0))

0 commit comments

Comments
 (0)