Coverage for python/lsst/faro/utils/stellar_locus.py : 14%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import numpy as np
2import scipy.stats as scipyStats
3from lsst.pipe.base import Struct
5__all__ = (
6 "stellarLocusResid",
7 "calcP1P2",
8 "getCoeffs",
9 "p1CoeffsFromP2x0y0",
10 "p2p1CoeffsFromLinearFit",
11 "calcQuartileClippedStats",
12)
15def stellarLocusResid(gmags, rmags, imags, **filterargs):
17 gr = gmags - rmags
18 ri = rmags - imags
20 # Also trim large values of r-i, since those will skew the linear regression
21 okfitcolors = (
22 (gr < 1.1)
23 & (gr > 0.3)
24 & (np.abs(ri) < 1.0)
25 & np.isfinite(gmags)
26 & np.isfinite(rmags)
27 & np.isfinite(imags)
28 )
29 # Eventually switch to using orthogonal regression instead of linear (as in pipe-analysis)?
31 slope, intercept, r_value, p_value, std_err = scipyStats.linregress(
32 gr[okfitcolors], ri[okfitcolors]
33 )
34 p2p1coeffs = p2p1CoeffsFromLinearFit(slope, intercept, 0.3, slope * 0.3 + intercept)
35 p1coeffs = p2p1coeffs.p1Coeffs.copy()
36 # hack to put the zeros in for u, z coeffs
37 p1coeffs.insert(0, 0.0)
38 p1coeffs.insert(4, 0.0)
39 p2coeffs = list(p2p1coeffs.p2Coeffs.copy())
40 p2coeffs.insert(0, 0.0)
41 p2coeffs.insert(4, 0.0)
42 umags = np.zeros(len(gmags))
43 zmags = np.zeros(len(gmags))
44 p1_fit = calcP1P2([umags, gmags, rmags, imags, zmags], p1coeffs)
45 p2_fit = calcP1P2([umags, gmags, rmags, imags, zmags], p2coeffs)
46 okp1_fit = (p1_fit < 0.6) & (p1_fit > -0.2)
48 # Do a second iteration, removing large (>3 sigma) outliers in p2:
49 clippedStats = calcQuartileClippedStats(p2_fit[okp1_fit], 3.0)
50 keep = np.abs(p2_fit) < clippedStats.clipValue
52 slope, intercept, r_value, p_value, std_err = scipyStats.linregress(
53 gr[okfitcolors & keep], ri[okfitcolors & keep]
54 )
55 p2p1coeffs = p2p1CoeffsFromLinearFit(slope, intercept, 0.3, slope * 0.3 + intercept)
56 p1coeffs = p2p1coeffs.p1Coeffs.copy()
57 # hack to put the zeros in for u, z coeffs
58 p1coeffs.insert(0, 0.0)
59 p1coeffs.insert(4, 0.0)
60 p2coeffs = list(p2p1coeffs.p2Coeffs.copy())
61 p2coeffs.insert(0, 0.0)
62 p2coeffs.insert(4, 0.0)
63 p1_fit = calcP1P2([umags, gmags, rmags, imags, zmags], p1coeffs)
64 p2_fit = calcP1P2([umags, gmags, rmags, imags, zmags], p2coeffs)
65 okp1_fit = (p1_fit < 0.6) & (p1_fit > -0.2)
67 return p1_fit[okp1_fit], p2_fit[okp1_fit], p1coeffs, p2coeffs
70def calcP1P2(mags, coeffs):
71 # P1 =A′u+B′g+C′r+D′i+E′z+F′
72 # P2′=Au+Bg+Cr+Di+Ez+F
73 p1p2 = (
74 float(coeffs[0]) * mags[0]
75 + float(coeffs[1]) * mags[1]
76 + float(coeffs[2]) * mags[2]
77 + float(coeffs[3]) * mags[3]
78 + float(coeffs[4]) * mags[4]
79 + float(coeffs[5])
80 )
81 return p1p2
84def getCoeffs():
85 # Coefficients from the Ivezic+2004 paper. Warning - if possible, the Coefficients
86 # should be derived from a fit to the stellar locus rather than these "fallback" values.
87 ivezicCoeffs = {
88 "P1s": [0.91, -0.495, -0.415, 0.0, 0.0, -1.28],
89 "P1w": [0.0, 0.928, -0.556, -0.372, 0.0, -0.425],
90 "P2s": [-0.249, 0.794, -0.555, 0.0, 0.0, 0.234],
91 "P2w": [0.0, -0.227, 0.792, -0.567, 0.0, 0.050],
92 }
93 ivezicCoeffsHSC = {
94 "P1s": [0.91, -0.495, -0.415, 0.0, 0.0, -1.28],
95 "P1w": [0.0, 0.888, -0.427, -0.461, 0.0, -0.478],
96 "P2s": [-0.249, 0.794, -0.555, 0.0, 0.0, 0.234],
97 "P2w": [0.0, -0.274, 0.803, -0.529, 0.0, 0.041],
98 }
99 return ivezicCoeffs, ivezicCoeffsHSC
102# Everything below this is copied directly from pipe_analysis/utils.py.
103# Should we move all those functions here once pipe_analysis is rewritten?
106def p1CoeffsFromP2x0y0(p2Coeffs, x0, y0):
107 """Compute Ivezic P1 coefficients using the P2 coeffs and origin (x0, y0)
108 Reference: Ivezic et al. 2004 (2004AN....325..583I)
109 theta = arctan(mP1), where mP1 is the slope of the equivalent straight
110 line (the P1 line) from the P2 coeffs in the (x, y)
111 coordinate system and x = c1 - c2, y = c2 - c3
112 P1 = cos(theta)*c1 + ((sin(theta) - cos(theta))*c2 - sin(theta)*c3 + deltaP1
113 P1 = 0 at x0, y0 ==> deltaP1 = -cos(theta)*x0 - sin(theta)*y0
114 Parameters
115 ----------
116 p2Coeffs : `list` of `float`
117 List of the four P2 coefficients from which, along with the origin point
118 (``x0``, ``y0``), to compute/derive the associated P1 coefficients.
119 x0, y0 : `float`
120 Coordinates at which to set P1 = 0 (i.e. the P1/P2 axis origin).
121 Returns
122 -------
123 p1Coeffs: `list` of `float`
124 The four P1 coefficients.
125 """
126 mP1 = p2Coeffs[0] / p2Coeffs[2]
127 cosTheta = np.cos(np.arctan(mP1))
128 sinTheta = np.sin(np.arctan(mP1))
129 deltaP1 = -cosTheta * x0 - sinTheta * y0
130 p1Coeffs = [cosTheta, sinTheta - cosTheta, -sinTheta, deltaP1]
132 return p1Coeffs
135def p2p1CoeffsFromLinearFit(m, b, x0, y0):
136 """Derive the Ivezic et al. 2004 P2 and P1 equations based on linear fit
137 Where the linear fit is to the given region in color-color space.
138 Reference: Ivezic et al. 2004 (2004AN....325..583I)
139 For y = m*x + b fit, where x = c1 - c2 and y = c2 - c3,
140 P2 = (-m*c1 + (m + 1)*c2 - c3 - b)/sqrt(m**2 + 1)
141 P2norm = P2/sqrt[(m**2 + (m + 1)**2 + 1**2)/(m**2 + 1)]
142 P1 = cos(theta)*x + sin(theta)*y + deltaP1, theta = arctan(m)
143 P1 = cos(theta)*(c1 - c2) + sin(theta)*(c2 - c3) + deltaP1
144 P1 = cos(theta)*c1 + ((sin(theta) - cos(theta))*c2 - sin(theta)*c3 + deltaP1
145 P1 = 0 at x0, y0 ==> deltaP1 = -cos(theta)*x0 - sin(theta)*y0
146 Parameters
147 ----------
148 m : `float`
149 Slope of line to convert.
150 b : `float`
151 Intercept of line to convert.
152 x0, y0 : `float`
153 Coordinates at which to set P1 = 0.
154 Returns
155 -------
156 result : `lsst.pipe.base.Struct`
157 Result struct with components:
158 - ``p2Coeffs`` : four P2 equation coefficents (`list` of `float`).
159 - ``p1Coeffs`` : four P1 equation coefficents (`list` of `float`).
160 """
161 # Compute Ivezic P2 coefficients using the linear fit slope and intercept
162 scaleFact = np.sqrt(m ** 2 + 1.0)
163 p2Coeffs = [-m / scaleFact, (m + 1.0) / scaleFact, -1.0 / scaleFact, -b / scaleFact]
164 p2Norm = 0.0
165 for coeff in p2Coeffs[:-1]: # Omit the constant normalization term
166 p2Norm += coeff ** 2
167 p2Norm = np.sqrt(p2Norm)
168 p2Coeffs /= p2Norm
170 # Compute Ivezic P1 coefficients equation using the linear fit slope and
171 # point (x0, y0) as the origin
172 p1Coeffs = p1CoeffsFromP2x0y0(p2Coeffs, x0, y0)
174 return Struct(p2Coeffs=p2Coeffs, p1Coeffs=p1Coeffs,)
177def calcQuartileClippedStats(dataArray, nSigmaToClip=3.0):
178 """Calculate the quartile-based clipped statistics of a data array.
179 The difference between quartiles[2] and quartiles[0] is the interquartile
180 distance. 0.74*interquartileDistance is an estimate of standard deviation
181 so, in the case that ``dataArray`` has an approximately Gaussian
182 distribution, this is equivalent to nSigma clipping.
183 Parameters
184 ----------
185 dataArray : `list` or `numpy.ndarray` of `float`
186 List or array containing the values for which the quartile-based
187 clipped statistics are to be calculated.
188 nSigmaToClip : `float`, optional
189 Number of \"sigma\" outside of which to clip data when computing the
190 statistics.
191 Returns
192 -------
193 result : `lsst.pipe.base.Struct`
194 The quartile-based clipped statistics with ``nSigmaToClip`` clipping.
195 Atributes are:
196 ``median``
197 The median of the full ``dataArray`` (`float`).
198 ``mean``
199 The quartile-based clipped mean (`float`).
200 ``stdDev``
201 The quartile-based clipped standard deviation (`float`).
202 ``rms``
203 The quartile-based clipped root-mean-squared (`float`).
204 ``clipValue``
205 The value outside of which to clip the data before computing the
206 statistics (`float`).
207 ``goodArray``
208 A boolean array indicating which data points in ``dataArray`` were
209 used in the calculation of the statistics, where `False` indicates
210 a clipped datapoint (`numpy.ndarray` of `bool`).
211 """
212 quartiles = np.percentile(dataArray, [25, 50, 75])
213 assert len(quartiles) == 3
214 median = quartiles[1]
215 interQuartileDistance = quartiles[2] - quartiles[0]
216 clipValue = nSigmaToClip * 0.74 * interQuartileDistance
217 good = np.logical_not(np.abs(dataArray - median) > clipValue)
218 quartileClippedMean = dataArray[good].mean()
219 quartileClippedStdDev = dataArray[good].std()
220 quartileClippedRms = np.sqrt(np.mean(dataArray[good] ** 2))
222 return Struct(
223 median=median,
224 mean=quartileClippedMean,
225 stdDev=quartileClippedStdDev,
226 rms=quartileClippedRms,
227 clipValue=clipValue,
228 goodArray=good,
229 )