Coverage for python/lsst/analysis/tools/actions/vector/vectorActions.py: 50%
115 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-04 03:18 -0700
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-04 03:18 -0700
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 cast
26import numpy as np
27from astropy import units as u
28from lsst.pex.config import DictField, Field
29from lsst.pipe.tasks.configurableActions import ConfigurableActionField
31from ...interfaces import KeyedData, KeyedDataSchema, Vector, VectorAction
32from .selectors import VectorSelector
34_LOG = logging.getLogger(__name__)
37class DownselectVector(VectorAction):
38 """Get a vector from KeyedData, apply specified selector, return the
39 shorter Vector.
40 """
42 vectorKey = Field[str](doc="column key to load from KeyedData")
44 selector = ConfigurableActionField(doc="Action which returns a selection mask", default=VectorSelector)
46 def getInputSchema(self) -> KeyedDataSchema:
47 yield (self.vectorKey, Vector)
48 yield from cast(VectorAction, self.selector).getInputSchema()
50 def __call__(self, data: KeyedData, **kwargs) -> Vector:
51 mask = cast(VectorAction, self.selector)(data, **kwargs)
52 return cast(Vector, data[self.vectorKey.format(**kwargs)])[mask]
55class MagColumnNanoJansky(VectorAction):
56 vectorKey = Field[str](doc="column key to use for this transformation")
58 def getInputSchema(self) -> KeyedDataSchema:
59 return ((self.vectorKey, Vector),)
61 def __call__(self, data: KeyedData, **kwargs) -> Vector:
62 with np.warnings.catch_warnings(): # type: ignore
63 np.warnings.filterwarnings("ignore", r"invalid value encountered") # type: ignore
64 np.warnings.filterwarnings("ignore", r"divide by zero") # type: ignore
65 vec = cast(Vector, data[self.vectorKey.format(**kwargs)])
66 return np.array(-2.5 * np.log10((vec * 1e-9) / 3631.0)) # type: ignore
69class FractionalDifference(VectorAction):
70 """Calculate (A-B)/B"""
72 actionA = ConfigurableActionField(doc="Action which supplies vector A", dtype=VectorAction)
73 actionB = ConfigurableActionField(doc="Action which supplies vector B", dtype=VectorAction)
75 def getInputSchema(self) -> KeyedDataSchema:
76 yield from self.actionA.getInputSchema() # type: ignore
77 yield from self.actionB.getInputSchema() # type: ignore
79 def __call__(self, data: KeyedData, **kwargs) -> Vector:
80 vecA = self.actionA(data, **kwargs) # type: ignore
81 vecB = self.actionB(data, **kwargs) # type: ignore
82 return (vecA - vecB) / vecB
85class LoadVector(VectorAction):
86 """Load and return a Vector from KeyedData"""
88 vectorKey = Field[str](doc="Key of vector which should be loaded")
90 def getInputSchema(self) -> KeyedDataSchema:
91 return ((self.vectorKey, Vector),)
93 def __call__(self, data: KeyedData, **kwargs) -> Vector:
94 return np.array(cast(Vector, data[self.vectorKey.format(**kwargs)]))
97class MagDiff(VectorAction):
98 """Calculate the difference between two magnitudes;
99 each magnitude is derived from a flux column.
100 Parameters
101 ----------
102 TO DO:
103 Returns
104 -------
105 The magnitude difference in milli mags.
106 Notes
107 -----
108 The flux columns need to be in units (specifiable in
109 the fluxUnits1 and 2 config options) that can be converted
110 to janskies. This action doesn't have any calibration
111 information and assumes that the fluxes are already
112 calibrated.
113 """
115 col1 = Field[str](doc="Column to subtract from")
116 fluxUnits1 = Field[str](doc="Units for col1", default="nanojansky")
117 col2 = Field[str](doc="Column to subtract")
118 fluxUnits2 = Field[str](doc="Units for col2", default="nanojansky")
119 returnMillimags = Field[bool](doc="Use millimags or not?", default=True)
121 def getInputSchema(self) -> KeyedDataSchema:
122 return ((self.col1, Vector), (self.col2, Vector))
124 def __call__(self, data: KeyedData, **kwargs) -> Vector:
125 flux1 = np.array(data[self.col1.format(**kwargs)]) * u.Unit(self.fluxUnits1)
126 mag1 = flux1.to(u.ABmag)
128 flux2 = np.array(data[self.col2.format(**kwargs)]) * u.Unit(self.fluxUnits2)
129 mag2 = flux2.to(u.ABmag)
131 magDiff = mag1 - mag2
133 if self.returnMillimags:
134 magDiff = magDiff.to(u.mmag)
136 return np.array(magDiff.value)
139class SNCalculator(VectorAction):
140 """Calculate the signal-to-noise."""
142 fluxType = Field[str](doc="Flux type to calculate the S/N.", default="{band}_psfFlux")
143 uncertaintySuffix = Field[str](
144 doc="Suffix to add to fluxType to specify the uncertainty column", default="Err"
145 )
147 def getInputSchema(self) -> KeyedDataSchema:
148 yield self.fluxType, Vector
149 yield f"{self.fluxType}{self.uncertaintySuffix}", Vector
151 def __call__(self, data: KeyedData, **kwargs) -> Vector:
152 signal = np.array(data[self.fluxType.format(**kwargs)])
153 noise = np.array(data[f"{self.fluxType}{self.uncertaintySuffix}".format(**kwargs)])
154 sn = signal / noise
156 return np.array(sn)
159class ExtinctionCorrectedMagDiff(VectorAction):
160 """Compute the difference between two magnitudes and correct for extinction
161 By default bands are derived from the <band>_ prefix on flux columns,
162 per the naming convention in the Object Table:
163 e.g. the band of 'g_psfFlux' is 'g'. If column names follow another
164 convention, bands can alternatively be supplied via the band1 or band2
165 config parameters.
166 If band1 and band2 are supplied, the flux column names are ignored.
167 """
169 magDiff = ConfigurableActionField(
170 doc="Action that returns a difference in magnitudes", default=MagDiff, dtype=VectorAction
171 )
172 ebvCol = Field[str](doc="E(B-V) Column Name", default="ebv")
173 band1 = Field[str](
174 doc="Optional band for magDiff.col1. Supercedes column name prefix",
175 optional=True,
176 default=None,
177 )
178 band2 = Field[str](
179 doc="Optional band for magDiff.col2. Supercedes column name prefix",
180 optional=True,
181 default=None,
182 )
183 extinctionCoeffs = DictField[str, float](
184 doc="Dictionary of extinction coefficients for conversion from E(B-V) to extinction, A_band."
185 "Key must be the band",
186 optional=True,
187 default=None,
188 )
190 def getInputSchema(self) -> KeyedDataSchema:
191 return self.magDiff.getInputSchema() + ((self.ebvCol, Vector),)
193 def __call__(self, data: KeyedData, **kwargs) -> Vector:
194 diff = self.magDiff(data, **kwargs)
195 if not self.extinctionCoeffs:
196 _LOG.warning("No extinction Coefficients. Not applying extinction correction")
197 return diff
199 col1Band = self.band1 if self.band1 else self.magDiff.col1.split("_")[0]
200 col2Band = self.band2 if self.band2 else self.magDiff.col2.split("_")[0]
202 # Return plain MagDiff with warning if either coeff not found
203 for band in (col1Band, col2Band):
204 if band not in self.extinctionCoeffs:
205 _LOG.warning(
206 "%s band not found in coefficients dictionary: %s" " Not applying extinction correction",
207 band,
208 self.extinctionCoeffs,
209 )
210 return diff
212 av1: float = self.extinctionCoeffs[col1Band]
213 av2: float = self.extinctionCoeffs[col2Band]
215 ebv = data[self.ebvCol]
216 # Ignore type until a more complete Vector protocol
217 correction = np.array((av1 - av2) * ebv) * u.mag # type: ignore
219 if self.magDiff.returnMillimags:
220 correction = correction.to(u.mmag)
222 return np.array(diff - correction.value)
225class AstromDiff(VectorAction):
226 """Calculate the difference between two columns, assuming their units
227 are degrees, and convert the difference to arcseconds.
228 Parameters
229 ----------
230 df : `pandas.core.frame.DataFrame`
231 The catalog to calculate the position difference from.
232 Returns
233 -------
234 angleDiffValue : `np.ndarray`
235 The difference between two columns, either in the input units or in
236 milliarcseconds.
237 Notes
238 -----
239 The columns need to be in units (specifiable in the radecUnits1 and 2
240 config options) that can be converted to arcseconds. This action doesn't
241 have any calibration information and assumes that the positions are already
242 calibrated.
243 """
245 col1 = Field[str](doc="Column to subtract from", dtype=str)
246 radecUnits1 = Field[str](doc="Units for col1", dtype=str, default="degree")
247 col2 = Field[str](doc="Column to subtract", dtype=str)
248 radecUnits2 = Field[str](doc="Units for col2", dtype=str, default="degree")
249 returnMilliArcsecs = Field[bool](doc="Use marcseconds or not?", dtype=bool, default=True)
251 def getInputSchema(self) -> KeyedDataSchema:
252 return ((self.col1, Vector), (self.col2, Vector))
254 def __call__(self, data: KeyedData, **kwargs) -> Vector:
255 angle1 = np.array(data[self.col1.format(**kwargs)]) * u.Unit(self.radecUnits1)
257 angle2 = np.array(data[self.col2.format(**kwargs)]) * u.Unit(self.radecUnits2)
259 angleDiff = angle1 - angle2
261 if self.returnMilliArcsecs:
262 angleDiffValue = angleDiff.to(u.arcsec).value * 1000
263 else:
264 angleDiffValue = angleDiff.value
265 return angleDiffValue