Coverage for python/lsst/analysis/tools/actions/vector/vectorActions.py: 50%
177 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-27 09:54 +0000
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-27 09:54 +0000
1# This file is part of analysis_tools.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21from __future__ import annotations
23import logging
24from typing import Optional, cast
26import numpy as np
27import pandas as pd
28from astropy import units as u
29from lsst.pex.config import DictField, Field
30from lsst.pipe.tasks.configurableActions import ConfigurableActionField, ConfigurableActionStructField
32from ...interfaces import KeyedData, KeyedDataSchema, Vector, VectorAction
33from .selectors import VectorSelector
35_LOG = logging.getLogger(__name__)
38class DownselectVector(VectorAction):
39 """Get a vector from KeyedData, apply specified selector, return the
40 shorter Vector.
41 """
43 vectorKey = Field[str](doc="column key to load from KeyedData")
45 selector = ConfigurableActionField(doc="Action which returns a selection mask", default=VectorSelector)
47 def getInputSchema(self) -> KeyedDataSchema:
48 yield (self.vectorKey, Vector)
49 yield from cast(VectorAction, self.selector).getInputSchema()
51 def __call__(self, data: KeyedData, **kwargs) -> Vector:
52 mask = cast(VectorAction, self.selector)(data, **kwargs)
53 return cast(Vector, data[self.vectorKey.format(**kwargs)])[mask]
56class MultiCriteriaDownselectVector(VectorAction):
57 """Get a vector from KeyedData, apply specified set of selectors with AND
58 logic, and return the shorter Vector.
59 """
61 vectorKey = Field[str](doc="column key to load from KeyedData")
63 selectors = ConfigurableActionStructField[VectorAction](
64 doc="Selectors for selecting rows, will be AND together",
65 )
67 def getInputSchema(self) -> KeyedDataSchema:
68 yield (self.vectorKey, Vector)
69 for action in self.selectors:
70 yield from cast(VectorAction, action).getInputSchema()
72 def __call__(self, data: KeyedData, **kwargs) -> Vector:
73 mask: Optional[Vector] = None
74 for selector in self.selectors:
75 subMask = selector(data, **kwargs)
76 if mask is None:
77 mask = subMask
78 else:
79 mask *= subMask # type: ignore
80 return cast(Vector, data[self.vectorKey.format(**kwargs)])[mask]
83class MagColumnNanoJansky(VectorAction):
84 vectorKey = Field[str](doc="column key to use for this transformation")
85 returnMillimags = Field[bool](doc="Use millimags or not?", default=False)
87 def getInputSchema(self) -> KeyedDataSchema:
88 return ((self.vectorKey, Vector),)
90 def __call__(self, data: KeyedData, **kwargs) -> Vector:
91 with np.warnings.catch_warnings(): # type: ignore
92 np.warnings.filterwarnings("ignore", r"invalid value encountered") # type: ignore
93 np.warnings.filterwarnings("ignore", r"divide by zero") # type: ignore
94 vec = cast(Vector, data[self.vectorKey.format(**kwargs)])
95 mag = np.array(-2.5 * np.log10((vec * 1e-9) / 3630.780547701003)) # type: ignore
96 if self.returnMillimags:
97 return mag * u.mag.to(u.mmag)
98 else:
99 return mag
102class FractionalDifference(VectorAction):
103 """Calculate (A-B)/B"""
105 actionA = ConfigurableActionField(doc="Action which supplies vector A", dtype=VectorAction)
106 actionB = ConfigurableActionField(doc="Action which supplies vector B", dtype=VectorAction)
108 def getInputSchema(self) -> KeyedDataSchema:
109 yield from self.actionA.getInputSchema() # type: ignore
110 yield from self.actionB.getInputSchema() # type: ignore
112 def __call__(self, data: KeyedData, **kwargs) -> Vector:
113 vecA = self.actionA(data, **kwargs) # type: ignore
114 vecB = self.actionB(data, **kwargs) # type: ignore
115 return (vecA - vecB) / vecB
118class Sn(VectorAction):
119 """Compute signal-to-noise in the given flux type"""
121 fluxType = Field[str](doc="Flux type to calculate the S/N in.", default="{band}_psfFlux")
122 uncertaintySuffix = Field[str](
123 doc="Suffix to add to fluxType to specify uncertainty column", default="Err"
124 )
125 band = Field[str](doc="Band to calculate the S/N in.", default="i")
127 def getInputSchema(self) -> KeyedDataSchema:
128 yield (fluxCol := self.fluxType), Vector
129 yield f"{fluxCol}{self.uncertaintySuffix}", Vector
131 def __call__(self, data: KeyedData, **kwargs) -> Vector:
132 """Computes S/N in self.fluxType
133 Parameters
134 ----------
135 df : `Tabular`
136 Returns
137 -------
138 result : `Vector`
139 Computed signal-to-noise ratio.
140 """
141 fluxCol = self.fluxType.format(**(kwargs | dict(band=self.band)))
142 errCol = f"{fluxCol}{self.uncertaintySuffix.format(**kwargs)}"
143 result = cast(Vector, data[fluxCol]) / data[errCol] # type: ignore
145 return np.array(cast(Vector, result))
148class SubtractVector(VectorAction):
149 """Calculate (A-B)"""
151 actionA = ConfigurableActionField(doc="Action which supplies vector A", dtype=VectorAction)
152 actionB = ConfigurableActionField(doc="Action which supplies vector B", dtype=VectorAction)
154 def getInputSchema(self) -> KeyedDataSchema:
155 yield from self.actionA.getInputSchema() # type: ignore
156 yield from self.actionB.getInputSchema() # type: ignore
158 def __call__(self, data: KeyedData, **kwargs) -> Vector:
159 vecA = self.actionA(data, **kwargs) # type: ignore
160 vecB = self.actionB(data, **kwargs) # type: ignore
161 return vecA - vecB
164class DivideVector(VectorAction):
165 """Calculate (A-B)"""
167 actionA = ConfigurableActionField(doc="Action which supplies vector A", dtype=VectorAction)
168 actionB = ConfigurableActionField(doc="Action which supplies vector B", dtype=VectorAction)
170 def getInputSchema(self) -> KeyedDataSchema:
171 yield from self.actionA.getInputSchema() # type: ignore
172 yield from self.actionB.getInputSchema() # type: ignore
174 def __call__(self, data: KeyedData, **kwargs) -> Vector:
175 vecA = self.actionA(data, **kwargs) # type: ignore
176 vecB = self.actionB(data, **kwargs) # type: ignore
177 return vecA / vecB
180class LoadVector(VectorAction):
181 """Load and return a Vector from KeyedData"""
183 vectorKey = Field[str](doc="Key of vector which should be loaded")
185 def getInputSchema(self) -> KeyedDataSchema:
186 return ((self.vectorKey, Vector),)
188 def __call__(self, data: KeyedData, **kwargs) -> Vector:
189 return np.array(cast(Vector, data[self.vectorKey.format(**kwargs)]))
192class MagDiff(VectorAction):
193 """Calculate the difference between two magnitudes;
194 each magnitude is derived from a flux column.
195 Parameters
196 ----------
197 TO DO:
198 Returns
199 -------
200 The magnitude difference in milli mags.
201 Notes
202 -----
203 The flux columns need to be in units (specifiable in
204 the fluxUnits1 and 2 config options) that can be converted
205 to janskies. This action doesn't have any calibration
206 information and assumes that the fluxes are already
207 calibrated.
208 """
210 col1 = Field[str](doc="Column to subtract from")
211 fluxUnits1 = Field[str](doc="Units for col1", default="nanojansky")
212 col2 = Field[str](doc="Column to subtract")
213 fluxUnits2 = Field[str](doc="Units for col2", default="nanojansky")
214 returnMillimags = Field[bool](doc="Use millimags or not?", default=True)
216 def getInputSchema(self) -> KeyedDataSchema:
217 return ((self.col1, Vector), (self.col2, Vector))
219 def __call__(self, data: KeyedData, **kwargs) -> Vector:
220 flux1 = np.array(data[self.col1.format(**kwargs)]) * u.Unit(self.fluxUnits1)
221 mag1 = flux1.to(u.ABmag)
223 flux2 = np.array(data[self.col2.format(**kwargs)]) * u.Unit(self.fluxUnits2)
224 mag2 = flux2.to(u.ABmag)
226 magDiff = mag1 - mag2
228 if self.returnMillimags:
229 magDiff = magDiff.to(u.mmag)
231 return np.array(magDiff.value)
234class SNCalculator(VectorAction):
235 """Calculate the signal-to-noise."""
237 fluxType = Field[str](doc="Flux type to calculate the S/N.", default="{band}_psfFlux")
238 uncertaintySuffix = Field[str](
239 doc="Suffix to add to fluxType to specify the uncertainty column", default="Err"
240 )
242 def getInputSchema(self) -> KeyedDataSchema:
243 yield self.fluxType, Vector
244 yield f"{self.fluxType}{self.uncertaintySuffix}", Vector
246 def __call__(self, data: KeyedData, **kwargs) -> Vector:
247 signal = np.array(data[self.fluxType.format(**kwargs)])
248 noise = np.array(data[f"{self.fluxType}{self.uncertaintySuffix}".format(**kwargs)])
249 sn = signal / noise
251 return np.array(sn)
254class ExtinctionCorrectedMagDiff(VectorAction):
255 """Compute the difference between two magnitudes and correct for extinction
256 By default bands are derived from the <band>_ prefix on flux columns,
257 per the naming convention in the Object Table:
258 e.g. the band of 'g_psfFlux' is 'g'. If column names follow another
259 convention, bands can alternatively be supplied via the band1 or band2
260 config parameters.
261 If band1 and band2 are supplied, the flux column names are ignored.
262 """
264 magDiff = ConfigurableActionField(
265 doc="Action that returns a difference in magnitudes", default=MagDiff, dtype=VectorAction
266 )
267 ebvCol = Field[str](doc="E(B-V) Column Name", default="ebv")
268 band1 = Field[str](
269 doc="Optional band for magDiff.col1. Supercedes column name prefix",
270 optional=True,
271 default=None,
272 )
273 band2 = Field[str](
274 doc="Optional band for magDiff.col2. Supercedes column name prefix",
275 optional=True,
276 default=None,
277 )
278 extinctionCoeffs = DictField[str, float](
279 doc="Dictionary of extinction coefficients for conversion from E(B-V) to extinction, A_band."
280 "Key must be the band",
281 optional=True,
282 default=None,
283 )
285 def getInputSchema(self) -> KeyedDataSchema:
286 return self.magDiff.getInputSchema() + ((self.ebvCol, Vector),)
288 def __call__(self, data: KeyedData, **kwargs) -> Vector:
289 diff = self.magDiff(data, **kwargs)
290 if not self.extinctionCoeffs:
291 _LOG.warning("No extinction Coefficients. Not applying extinction correction")
292 return diff
294 col1Band = self.band1 if self.band1 else self.magDiff.col1.split("_")[0]
295 col2Band = self.band2 if self.band2 else self.magDiff.col2.split("_")[0]
297 # Return plain MagDiff with warning if either coeff not found
298 for band in (col1Band, col2Band):
299 if band not in self.extinctionCoeffs:
300 _LOG.warning(
301 "%s band not found in coefficients dictionary: %s" " Not applying extinction correction",
302 band,
303 self.extinctionCoeffs,
304 )
305 return diff
307 av1: float = self.extinctionCoeffs[col1Band]
308 av2: float = self.extinctionCoeffs[col2Band]
310 ebv = data[self.ebvCol]
311 # Ignore type until a more complete Vector protocol
312 correction = np.array((av1 - av2) * ebv) * u.mag # type: ignore
314 if self.magDiff.returnMillimags:
315 correction = correction.to(u.mmag)
317 return np.array(diff - correction.value)
320class AstromDiff(VectorAction):
321 """Calculate the difference between two columns, assuming their units
322 are degrees, and convert the difference to arcseconds.
323 Parameters
324 ----------
325 df : `pandas.core.frame.DataFrame`
326 The catalog to calculate the position difference from.
327 Returns
328 -------
329 angleDiffValue : `np.ndarray`
330 The difference between two columns, either in the input units or in
331 milliarcseconds.
332 Notes
333 -----
334 The columns need to be in units (specifiable in the radecUnits1 and 2
335 config options) that can be converted to arcseconds. This action doesn't
336 have any calibration information and assumes that the positions are already
337 calibrated.
338 """
340 col1 = Field[str](doc="Column to subtract from", dtype=str)
341 radecUnits1 = Field[str](doc="Units for col1", dtype=str, default="degree")
342 col2 = Field[str](doc="Column to subtract", dtype=str)
343 radecUnits2 = Field[str](doc="Units for col2", dtype=str, default="degree")
344 returnMilliArcsecs = Field[bool](doc="Use marcseconds or not?", dtype=bool, default=True)
346 def getInputSchema(self) -> KeyedDataSchema:
347 return ((self.col1, Vector), (self.col2, Vector))
349 def __call__(self, data: KeyedData, **kwargs) -> Vector:
350 angle1 = np.array(data[self.col1.format(**kwargs)]) * u.Unit(self.radecUnits1)
352 angle2 = np.array(data[self.col2.format(**kwargs)]) * u.Unit(self.radecUnits2)
354 angleDiff = angle1 - angle2
356 if self.returnMilliArcsecs:
357 angleDiffValue = angleDiff.to(u.arcsec).value * 1000
358 else:
359 angleDiffValue = angleDiff.value
360 return angleDiffValue
363class PerGroupStatistic(VectorAction):
364 """Compute per-group statistic values and return result as a vector with
365 one element per group. The computed statistic can be any function accepted
366 by pandas DataFrameGroupBy.aggregate passed in as a string function name.
367 """
369 groupKey = Field[str](doc="Column key to use for forming groups", default="obj_index")
370 buildAction = ConfigurableActionField(doc="Action to build vector", default=LoadVector)
371 func = Field[str](doc="Name of function to be applied per group")
373 def getInputSchema(self) -> KeyedDataSchema:
374 return tuple(self.buildAction.getInputSchema()) + ((self.groupKey, Vector),)
376 def __call__(self, data: KeyedData, **kwargs) -> Vector:
377 df = pd.DataFrame({"groupKey": data[self.groupKey], "value": self.buildAction(data, **kwargs)})
378 result = df.groupby("groupKey")["value"].aggregate(self.func)
379 return np.array(result)