lsst.pipe.tasks gd7e1fb3723+835d19d8ff
Loading...
Searching...
No Matches
functors.py
Go to the documentation of this file.
1# This file is part of pipe_tasks.
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/>.
21
22__all__ = ["init_fromDict", "Functor", "CompositeFunctor", "mag_aware_eval",
23 "CustomFunctor", "Column", "Index", "CoordColumn", "RAColumn",
24 "DecColumn", "SinglePrecisionFloatColumn", "HtmIndex20", "fluxName", "fluxErrName", "Mag",
25 "MagErr", "MagDiff", "Color", "DeconvolvedMoments", "SdssTraceSize",
26 "PsfSdssTraceSizeDiff", "HsmTraceSize", "PsfHsmTraceSizeDiff",
27 "HsmFwhm", "E1", "E2", "RadiusFromQuadrupole", "LocalWcs",
28 "ComputePixelScale", "ConvertPixelToArcseconds",
29 "ConvertPixelSqToArcsecondsSq",
30 "ConvertDetectorAngleToPositionAngle",
31 "ConvertDetectorAngleErrToPositionAngleErr",
32 "ReferenceBand", "Photometry",
33 "NanoJansky", "NanoJanskyErr", "LocalPhotometry", "LocalNanojansky",
34 "LocalNanojanskyErr", "LocalDipoleMeanFlux",
35 "LocalDipoleMeanFluxErr", "LocalDipoleDiffFlux",
36 "LocalDipoleDiffFluxErr", "Ebv",
37 "MomentsIuuSky", "MomentsIvvSky", "MomentsIuvSky",
38 "CorrelationIuuSky", "CorrelationIvvSky", "CorrelationIuvSky",
39 "PositionAngleFromMoments", "PositionAngleFromCorrelation",
40 "SemimajorAxisFromMoments", "SemimajorAxisFromCorrelation",
41 "SemiminorAxisFromMoments", "SemiminorAxisFromCorrelation",
42 ]
43
44import logging
45import os
46import os.path
47import re
48import warnings
49from contextlib import redirect_stdout
50from itertools import product
51
52import astropy.units as u
53import lsst.geom as geom
54import lsst.sphgeom as sphgeom
55import numpy as np
56import pandas as pd
57import yaml
58from astropy.coordinates import SkyCoord
59from lsst.daf.butler import DeferredDatasetHandle
60from lsst.pipe.base import InMemoryDatasetHandle
61from lsst.resources import ResourcePath
62from lsst.utils import doImport
63from lsst.utils.introspection import get_full_type_name
64
65
66def init_fromDict(initDict, basePath='lsst.pipe.tasks.functors',
67 typeKey='functor', name=None):
68 """Initialize an object defined in a dictionary.
69
70 The object needs to be importable as f'{basePath}.{initDict[typeKey]}'.
71 The positional and keyword arguments (if any) are contained in "args" and
72 "kwargs" entries in the dictionary, respectively.
73 This is used in `~lsst.pipe.tasks.functors.CompositeFunctor.from_yaml` to
74 initialize a composite functor from a specification in a YAML file.
75
76 Parameters
77 ----------
78 initDict : dictionary
79 Dictionary describing object's initialization.
80 Must contain an entry keyed by ``typeKey`` that is the name of the
81 object, relative to ``basePath``.
82 basePath : str
83 Path relative to module in which ``initDict[typeKey]`` is defined.
84 typeKey : str
85 Key of ``initDict`` that is the name of the object (relative to
86 ``basePath``).
87 """
88 initDict = initDict.copy()
89 # TO DO: DM-21956 We should be able to define functors outside this module
90 pythonType = doImport(f'{basePath}.{initDict.pop(typeKey)}')
91 args = []
92 if 'args' in initDict:
93 args = initDict.pop('args')
94 if isinstance(args, str):
95 args = [args]
96 try:
97 element = pythonType(*args, **initDict)
98 except Exception as e:
99 message = f'Error in constructing functor "{name}" of type {pythonType.__name__} with args: {args}'
100 raise type(e)(message, e.args)
101 return element
102
103
104class Functor(object):
105 """Define and execute a calculation on a DataFrame or Handle holding a
106 DataFrame.
107
108 The `__call__` method accepts either a `~pandas.DataFrame` object or a
109 `~lsst.daf.butler.DeferredDatasetHandle` or
110 `~lsst.pipe.base.InMemoryDatasetHandle`, and returns the
111 result of the calculation as a single column.
112 Each functor defines what columns are needed for the calculation, and only
113 these columns are read from the dataset handle.
114
115 The action of `__call__` consists of two steps: first, loading the
116 necessary columns from disk into memory as a `~pandas.DataFrame` object;
117 and second, performing the computation on this DataFrame and returning the
118 result.
119
120 To define a new `Functor`, a subclass must define a `_func` method,
121 that takes a `~pandas.DataFrame` and returns result in a `~pandas.Series`.
122 In addition, it must define the following attributes:
123
124 * `_columns`: The columns necessary to perform the calculation
125 * `name`: A name appropriate for a figure axis label
126 * `shortname`: A name appropriate for use as a dictionary key
127
128 On initialization, a `Functor` should declare what band (``filt`` kwarg)
129 and dataset (e.g. ``'ref'``, ``'meas'``, ``'forced_src'``) it is intended
130 to be applied to.
131 This enables the `_get_data` method to extract the proper columns from the
132 underlying data.
133 If not specified, the dataset will fall back on the `_defaultDataset`
134 attribute.
135 If band is not specified and ``dataset`` is anything other than ``'ref'``,
136 then an error will be raised when trying to perform the calculation.
137
138 Originally, `Functor` was set up to expect datasets formatted like the
139 ``deepCoadd_obj`` dataset; that is, a DataFrame with a multi-level column
140 index, with the levels of the column index being ``band``, ``dataset``, and
141 ``column``.
142 It has since been generalized to apply to DataFrames without multi-level
143 indices and multi-level indices with just ``dataset`` and ``column``
144 levels.
145 In addition, the `_get_data` method that reads the columns from the
146 underlying data will return a DataFrame with column index levels defined by
147 the `_dfLevels` attribute; by default, this is ``column``.
148
149 The `_dfLevels` attributes should generally not need to be changed, unless
150 `_func` needs columns from multiple filters or datasets to do the
151 calculation.
152 An example of this is the `~lsst.pipe.tasks.functors.Color` functor, for
153 which `_dfLevels = ('band', 'column')`, and `_func` expects the DataFrame
154 it gets to have those levels in the column index.
155
156 Parameters
157 ----------
158 filt : str
159 Band upon which to do the calculation.
160
161 dataset : str
162 Dataset upon which to do the calculation (e.g., 'ref', 'meas',
163 'forced_src').
164 """
165
166 _defaultDataset = 'ref'
167 _dfLevels = ('column',)
168 _defaultNoDup = False
169
170 def __init__(self, filt=None, dataset=None, noDup=None):
171 self.filt = filt
172 self.dataset = dataset if dataset is not None else self._defaultDataset
173 self._noDup = noDup
174 self.log = logging.getLogger(type(self).__name__)
175
176 @property
177 def noDup(self):
178 """Do not explode by band if used on object table."""
179 if self._noDup is not None:
180 return self._noDup
181 else:
182 return self._defaultNoDup
183
184 @property
185 def columns(self):
186 """Columns required to perform calculation."""
187 if not hasattr(self, '_columns'):
188 raise NotImplementedError('Must define columns property or _columns attribute')
189 return self._columns
190
191 def _get_data_columnLevels(self, data, columnIndex=None):
192 """Gets the names of the column index levels.
193
194 This should only be called in the context of a multilevel table.
195
196 Parameters
197 ----------
198 data : various
199 The data to be read, can be a
200 `~lsst.daf.butler.DeferredDatasetHandle` or
201 `~lsst.pipe.base.InMemoryDatasetHandle`.
202 columnIndex (optional): pandas `~pandas.Index` object
203 If not passed, then it is read from the
204 `~lsst.daf.butler.DeferredDatasetHandle`
205 for `~lsst.pipe.base.InMemoryDatasetHandle`.
206 """
207 if columnIndex is None:
208 columnIndex = data.get(component="columns")
209 return columnIndex.names
210
211 def _get_data_columnLevelNames(self, data, columnIndex=None):
212 """Gets the content of each of the column levels for a multilevel
213 table.
214 """
215 if columnIndex is None:
216 columnIndex = data.get(component="columns")
217
218 columnLevels = columnIndex.names
219 columnLevelNames = {
220 level: list(np.unique(np.array([c for c in columnIndex])[:, i]))
221 for i, level in enumerate(columnLevels)
222 }
223 return columnLevelNames
224
225 def _colsFromDict(self, colDict, columnIndex=None):
226 """Converts dictionary column specficiation to a list of columns."""
227 new_colDict = {}
228 columnLevels = self._get_data_columnLevels(None, columnIndex=columnIndex)
229
230 for i, lev in enumerate(columnLevels):
231 if lev in colDict:
232 if isinstance(colDict[lev], str):
233 new_colDict[lev] = [colDict[lev]]
234 else:
235 new_colDict[lev] = colDict[lev]
236 else:
237 new_colDict[lev] = columnIndex.levels[i]
238
239 levelCols = [new_colDict[lev] for lev in columnLevels]
240 cols = list(product(*levelCols))
241 colsAvailable = [col for col in cols if col in columnIndex]
242 return colsAvailable
243
244 def multilevelColumns(self, data, columnIndex=None, returnTuple=False):
245 """Returns columns needed by functor from multilevel dataset.
246
247 To access tables with multilevel column structure, the
248 `~lsst.daf.butler.DeferredDatasetHandle` or
249 `~lsst.pipe.base.InMemoryDatasetHandle` needs to be passed
250 either a list of tuples or a dictionary.
251
252 Parameters
253 ----------
254 data : various
255 The data as either `~lsst.daf.butler.DeferredDatasetHandle`, or
256 `~lsst.pipe.base.InMemoryDatasetHandle`.
257 columnIndex (optional): pandas `~pandas.Index` object
258 Either passed or read in from
259 `~lsst.daf.butler.DeferredDatasetHandle`.
260 `returnTuple` : `bool`
261 If true, then return a list of tuples rather than the column
262 dictionary specification.
263 This is set to `True` by `CompositeFunctor` in order to be able to
264 combine columns from the various component functors.
265
266 """
267 if not isinstance(data, (DeferredDatasetHandle, InMemoryDatasetHandle)):
268 raise RuntimeError(f"Unexpected data type. Got {get_full_type_name(data)}.")
269
270 if columnIndex is None:
271 columnIndex = data.get(component="columns")
272
273 # Confirm that the dataset has the column levels the functor is
274 # expecting it to have.
275 columnLevels = self._get_data_columnLevels(data, columnIndex)
276
277 columnDict = {'column': self.columns,
278 'dataset': self.dataset}
279 if self.filt is None:
280 columnLevelNames = self._get_data_columnLevelNames(data, columnIndex)
281 if "band" in columnLevels:
282 if self.dataset == "ref":
283 columnDict["band"] = columnLevelNames["band"][0]
284 else:
285 raise ValueError(f"'filt' not set for functor {self.name}"
286 f"(dataset {self.dataset}) "
287 "and DataFrame "
288 "contains multiple filters in column index. "
289 "Set 'filt' or set 'dataset' to 'ref'.")
290 else:
291 columnDict['band'] = self.filt
292
293 if returnTuple:
294 return self._colsFromDict(columnDict, columnIndex=columnIndex)
295 else:
296 return columnDict
297
298 def _func(self, df, dropna=True):
299 raise NotImplementedError('Must define calculation on DataFrame')
300
301 def _get_columnIndex(self, data):
302 """Return columnIndex."""
303
304 if isinstance(data, (DeferredDatasetHandle, InMemoryDatasetHandle)):
305 return data.get(component="columns")
306 else:
307 return None
308
309 def _get_data(self, data):
310 """Retrieve DataFrame necessary for calculation.
311
312 The data argument can be a `~pandas.DataFrame`, a
313 `~lsst.daf.butler.DeferredDatasetHandle`, or
314 an `~lsst.pipe.base.InMemoryDatasetHandle`.
315
316 Returns a DataFrame upon which `self._func` can act.
317 """
318 # We wrap a DataFrame in a handle here to take advantage of the
319 # DataFrame delegate DataFrame column wrangling abilities.
320 if isinstance(data, pd.DataFrame):
321 _data = InMemoryDatasetHandle(data, storageClass="DataFrame")
322 elif isinstance(data, (DeferredDatasetHandle, InMemoryDatasetHandle)):
323 _data = data
324 else:
325 raise RuntimeError(f"Unexpected type provided for data. Got {get_full_type_name(data)}.")
326
327 # First thing to do: check to see if the data source has a multilevel
328 # column index or not.
329 columnIndex = self._get_columnIndex(_data)
330 is_multiLevel = isinstance(columnIndex, pd.MultiIndex)
331
332 # Get proper columns specification for this functor.
333 if is_multiLevel:
334 columns = self.multilevelColumns(_data, columnIndex=columnIndex)
335 else:
336 columns = self.columns
337
338 # Load in-memory DataFrame with appropriate columns the gen3 way.
339 df = _data.get(parameters={"columns": columns})
340
341 # Drop unnecessary column levels.
342 if is_multiLevel:
343 df = self._setLevels(df)
344
345 return df
346
347 def _setLevels(self, df):
348 levelsToDrop = [n for n in df.columns.names if n not in self._dfLevels]
349 df.columns = df.columns.droplevel(levelsToDrop)
350 return df
351
352 def _dropna(self, vals):
353 return vals.dropna()
354
355 def __call__(self, data, dropna=False):
356 df = self._get_data(data)
357 try:
358 vals = self._func(df)
359 except Exception as e:
360 self.log.error("Exception in %s call: %s: %s", self.name, type(e).__name__, e)
361 vals = self.fail(df)
362 if dropna:
363 vals = self._dropna(vals)
364
365 return vals
366
367 def difference(self, data1, data2, **kwargs):
368 """Computes difference between functor called on two different
369 DataFrame/Handle objects.
370 """
371 return self(data1, **kwargs) - self(data2, **kwargs)
372
373 def fail(self, df):
374 return pd.Series(np.full(len(df), np.nan), index=df.index)
375
376 @property
377 def name(self):
378 """Full name of functor (suitable for figure labels)."""
379 return NotImplementedError
380
381 @property
382 def shortname(self):
383 """Short name of functor (suitable for column name/dict key)."""
384 return self.name
385
386
388 """Perform multiple calculations at once on a catalog.
389
390 The role of a `CompositeFunctor` is to group together computations from
391 multiple functors.
392 Instead of returning `~pandas.Series` a `CompositeFunctor` returns a
393 `~pandas.DataFrame`, with the column names being the keys of ``funcDict``.
394
395 The `columns` attribute of a `CompositeFunctor` is the union of all columns
396 in all the component functors.
397
398 A `CompositeFunctor` does not use a `_func` method itself; rather, when a
399 `CompositeFunctor` is called, all its columns are loaded at once, and the
400 resulting DataFrame is passed to the `_func` method of each component
401 functor.
402 This has the advantage of only doing I/O (reading from parquet file) once,
403 and works because each individual `_func` method of each component functor
404 does not care if there are *extra* columns in the DataFrame being passed;
405 only that it must contain *at least* the `columns` it expects.
406
407 An important and useful class method is `from_yaml`, which takes as an
408 argument the path to a YAML file specifying a collection of functors.
409
410 Parameters
411 ----------
412 funcs : `dict` or `list`
413 Dictionary or list of functors.
414 If a list, then it will be converted into a dictonary according to the
415 `.shortname` attribute of each functor.
416 """
417 dataset = None
418 name = "CompositeFunctor"
419
420 def __init__(self, funcs, **kwargs):
421
422 if type(funcs) is dict:
423 self.funcDict = funcs
424 else:
425 self.funcDict = {f.shortname: f for f in funcs}
426
427 self._filt = None
428
429 super().__init__(**kwargs)
430
431 @property
432 def filt(self):
433 return self._filt
434
435 @filt.setter
436 def filt(self, filt):
437 if filt is not None:
438 for _, f in self.funcDict.items():
439 f.filt = filt
440 self._filt = filt
441
442 def update(self, new):
443 """Update the functor with new functors."""
444 if isinstance(new, dict):
445 self.funcDict.update(new)
446 elif isinstance(new, CompositeFunctor):
447 self.funcDict.update(new.funcDict)
448 else:
449 raise TypeError('Can only update with dictionary or CompositeFunctor.')
450
451 # Make sure new functors have the same 'filt' set.
452 if self.filt is not None:
453 self.filt = self.filt
454
455 @property
456 def columns(self):
457 return list(set([x for y in [f.columns for f in self.funcDict.values()] for x in y]))
458
459 def multilevelColumns(self, data, **kwargs):
460 # Get the union of columns for all component functors.
461 # Note the need to have `returnTuple=True` here.
462 return list(
463 set(
464 [
465 x
466 for y in [
467 f.multilevelColumns(data, returnTuple=True, **kwargs) for f in self.funcDict.values()
468 ]
469 for x in y
470 ]
471 )
472 )
473
474 def __call__(self, data, **kwargs):
475 """Apply the functor to the data table.
476
477 Parameters
478 ----------
479 data : various
480 The data represented as `~lsst.daf.butler.DeferredDatasetHandle`,
481 `~lsst.pipe.base.InMemoryDatasetHandle`, or `~pandas.DataFrame`.
482 The table or a pointer to a table on disk from which columns can
483 be accessed.
484 """
485 if isinstance(data, pd.DataFrame):
486 _data = InMemoryDatasetHandle(data, storageClass="DataFrame")
487 elif isinstance(data, (DeferredDatasetHandle, InMemoryDatasetHandle)):
488 _data = data
489 else:
490 raise RuntimeError(f"Unexpected type provided for data. Got {get_full_type_name(data)}.")
491
492 columnIndex = self._get_columnIndex(_data)
493
494 if isinstance(columnIndex, pd.MultiIndex):
495 columns = self.multilevelColumns(_data, columnIndex=columnIndex)
496 df = _data.get(parameters={"columns": columns})
497
498 valDict = {}
499 for k, f in self.funcDict.items():
500 try:
501 subdf = f._setLevels(
502 df[f.multilevelColumns(_data, returnTuple=True, columnIndex=columnIndex)]
503 )
504 valDict[k] = f._func(subdf)
505 except Exception as e:
506 self.log.exception(
507 "Exception in %s (funcs: %s) call: %s",
508 self.name,
509 str(list(self.funcDict.keys())),
510 type(e).__name__,
511 )
512 try:
513 valDict[k] = f.fail(subdf)
514 except NameError:
515 raise e
516
517 else:
518 df = _data.get(parameters={"columns": self.columns})
519
520 valDict = {k: f._func(df) for k, f in self.funcDict.items()}
521
522 # Check that output columns are actually columns.
523 for name, colVal in valDict.items():
524 if len(colVal.shape) != 1:
525 raise RuntimeError("Transformed column '%s' is not the shape of a column. "
526 "It is shaped %s and type %s." % (name, colVal.shape, type(colVal)))
527
528 try:
529 valDf = pd.concat(valDict, axis=1)
530 except TypeError:
531 print([(k, type(v)) for k, v in valDict.items()])
532 raise
533
534 if kwargs.get('dropna', False):
535 valDf = valDf.dropna(how='any')
536
537 return valDf
538
539 @classmethod
540 def renameCol(cls, col, renameRules):
541 if renameRules is None:
542 return col
543 for old, new in renameRules:
544 if col.startswith(old):
545 col = col.replace(old, new)
546 return col
547
548 @classmethod
549 def from_file(cls, filename, **kwargs):
550 # Allow environment variables in the filename.
551 filename = os.path.expandvars(filename)
552 with ResourcePath(filename).open("r") as f:
553 translationDefinition = yaml.safe_load(f)
554
555 return cls.from_yaml(translationDefinition, **kwargs)
556
557 @classmethod
558 def from_yaml(cls, translationDefinition, **kwargs):
559 funcs = {}
560 for func, val in translationDefinition['funcs'].items():
561 funcs[func] = init_fromDict(val, name=func)
562
563 if 'flag_rename_rules' in translationDefinition:
564 renameRules = translationDefinition['flag_rename_rules']
565 else:
566 renameRules = None
567
568 if 'calexpFlags' in translationDefinition:
569 for flag in translationDefinition['calexpFlags']:
570 funcs[cls.renameCol(flag, renameRules)] = Column(flag, dataset='calexp')
571
572 if 'refFlags' in translationDefinition:
573 for flag in translationDefinition['refFlags']:
574 funcs[cls.renameCol(flag, renameRules)] = Column(flag, dataset='ref')
575
576 if 'forcedFlags' in translationDefinition:
577 for flag in translationDefinition['forcedFlags']:
578 funcs[cls.renameCol(flag, renameRules)] = Column(flag, dataset='forced_src')
579
580 if 'flags' in translationDefinition:
581 for flag in translationDefinition['flags']:
582 funcs[cls.renameCol(flag, renameRules)] = Column(flag, dataset='meas')
583
584 return cls(funcs, **kwargs)
585
586
587def mag_aware_eval(df, expr, log):
588 """Evaluate an expression on a DataFrame, knowing what the 'mag' function
589 means.
590
591 Builds on `pandas.DataFrame.eval`, which parses and executes math on
592 DataFrames.
593
594 Parameters
595 ----------
596 df : ~pandas.DataFrame
597 DataFrame on which to evaluate expression.
598
599 expr : str
600 Expression.
601 """
602 try:
603 expr_new = re.sub(r'mag\‍((\w+)\‍)', r'-2.5*log(\g<1>)/log(10)', expr)
604 val = df.eval(expr_new)
605 except Exception as e: # Should check what actually gets raised
606 log.error("Exception in mag_aware_eval: %s: %s", type(e).__name__, e)
607 expr_new = re.sub(r'mag\‍((\w+)\‍)', r'-2.5*log(\g<1>_instFlux)/log(10)', expr)
608 val = df.eval(expr_new)
609 return val
610
611
613 """Arbitrary computation on a catalog.
614
615 Column names (and thus the columns to be loaded from catalog) are found by
616 finding all words and trying to ignore all "math-y" words.
617
618 Parameters
619 ----------
620 expr : str
621 Expression to evaluate, to be parsed and executed by
622 `~lsst.pipe.tasks.functors.mag_aware_eval`.
623 """
624 _ignore_words = ('mag', 'sin', 'cos', 'exp', 'log', 'sqrt')
625
626 def __init__(self, expr, **kwargs):
627 self.expr = expr
628 super().__init__(**kwargs)
629
630 @property
631 def name(self):
632 return self.expr
633
634 @property
635 def columns(self):
636 flux_cols = re.findall(r'mag\‍(\s*(\w+)\s*\‍)', self.expr)
637
638 cols = [c for c in re.findall(r'[a-zA-Z_]+', self.expr) if c not in self._ignore_words]
639 not_a_col = []
640 for c in flux_cols:
641 if not re.search('_instFlux$', c):
642 cols.append(f'{c}_instFlux')
643 not_a_col.append(c)
644 else:
645 cols.append(c)
646
647 return list(set([c for c in cols if c not in not_a_col]))
648
649 def _func(self, df):
650 return mag_aware_eval(df, self.expr, self.log)
651
652
654 """Get column with a specified name."""
655
656 def __init__(self, col, **kwargs):
657 self.col = col
658 super().__init__(**kwargs)
659
660 @property
661 def name(self):
662 return self.col
663
664 @property
665 def columns(self):
666 return [self.col]
667
668 def _func(self, df):
669 return df[self.col]
670
671
673 """Return the value of the index for each object."""
674
675 columns = ['coord_ra'] # Just a dummy; something has to be here.
676 _defaultDataset = 'ref'
677 _defaultNoDup = True
678
679 def _func(self, df):
680 return pd.Series(df.index, index=df.index)
681
682
684 """Base class for coordinate column, in degrees."""
685 _radians = True
686
687 def __init__(self, col, **kwargs):
688 super().__init__(col, **kwargs)
689
690 def _func(self, df):
691 # Must not modify original column in case that column is used by
692 # another functor.
693 output = df[self.col] * 180 / np.pi if self._radians else df[self.col]
694 return output
695
696
698 """Right Ascension, in degrees."""
699 name = 'RA'
700 _defaultNoDup = True
701
702 def __init__(self, **kwargs):
703 super().__init__('coord_ra', **kwargs)
704
705 def __call__(self, catalog, **kwargs):
706 return super().__call__(catalog, **kwargs)
707
708
710 """Declination, in degrees."""
711 name = 'Dec'
712 _defaultNoDup = True
713
714 def __init__(self, **kwargs):
715 super().__init__('coord_dec', **kwargs)
716
717 def __call__(self, catalog, **kwargs):
718 return super().__call__(catalog, **kwargs)
719
720
722 """Angular uncertainty in Right Ascension, in degrees.
723
724 Note that ``coord_raErr`` is the uncertainty on RA·cos(Dec),
725 i.e. an arc-length on the sky in the RA direction.
726 """
727 name = 'RAErr'
728 _defaultNoDup = True
729
730 def __init__(self, **kwargs):
731 super().__init__('coord_raErr', **kwargs)
732
733
735 """Uncertainty in declination, in degrees."""
736 name = 'DecErr'
737 _defaultNoDup = True
738
739 def __init__(self, **kwargs):
740 super().__init__('coord_decErr', **kwargs)
741
742
744 """Tangent-plane angular covariance, in degrees^2.
745
746 As with the RA error, this is the covariance between RA·cos(Dec) and Dec,
747 Cov(xi, eta) = Cov(RA*cos(Dec), Dec).
748 """
749 _radians = True
750 name = 'RADecCov'
751 _defaultNoDup = True
752
753 def __init__(self, **kwargs):
754 super().__init__('coord_ra_dec_Cov', **kwargs)
755
756 def _func(self, df):
757 # Must not modify original column in case that column is used by
758 # another functor.
759 output = df[self.col]*(180/np.pi)**2 if self._radians else df[self.col]
760 return output
761
762
764 """A column with a band in a multiband table."""
765 def __init__(self, col, band_to_check, **kwargs):
766 self._band_to_check = band_to_check
767 super().__init__(col=col, **kwargs)
768
769 @property
770 def band_to_check(self):
771 return self._band_to_check
772
773
775 """A float32 MultibandColumn"""
776 def _func(self, df):
777 return super()._func(df).astype(np.float32)
778
779
781 """Return a column cast to a single-precision float."""
782
783 def _func(self, df):
784 return df[self.col].astype(np.float32)
785
786
788 """Compute the level 20 HtmIndex for the catalog.
789
790 Notes
791 -----
792 This functor was implemented to satisfy requirements of old APDB interface
793 which required the ``pixelId`` column in DiaObject with HTM20 index.
794 The APDB interface had migrated to not need that information, but we keep
795 this class in case it may be useful for something else.
796 """
797 name = "Htm20"
798 htmLevel = 20
799 _radians = True
800
801 def __init__(self, ra, dec, **kwargs):
803 self.ra = ra
804 self.dec = dec
805 self._columns = [self.ra, self.dec]
806 super().__init__(**kwargs)
807
808 def _func(self, df):
809
810 def computePixel(row):
811 if self._radians:
812 sphPoint = geom.SpherePoint(row[self.ra],
813 row[self.dec],
814 geom.radians)
815 else:
816 sphPoint = geom.SpherePoint(row[self.ra],
817 row[self.dec],
818 geom.degrees)
819 return self.pixelator.index(sphPoint.getVector())
820
821 return df.apply(computePixel, axis=1, result_type='reduce').astype('int64')
822
823
824def fluxName(col):
825 """Append _instFlux to the column name if it doesn't have it already."""
826 if not col.endswith('_instFlux'):
827 col += '_instFlux'
828 return col
829
830
831def fluxErrName(col):
832 """Append _instFluxErr to the column name if it doesn't have it already."""
833 if not col.endswith('_instFluxErr'):
834 col += '_instFluxErr'
835 return col
836
837
839 """Compute calibrated magnitude.
840
841 Returns the flux at mag=0.
842 The default ``fluxMag0`` is 63095734448.0194, which is default for HSC.
843 TO DO: This default should be made configurable in DM-21955.
844
845 This calculation hides warnings about invalid values and dividing by zero.
846
847 As with all functors, a ``dataset`` and ``filt`` kwarg should be provided
848 upon initialization.
849 Unlike the default `Functor`, however, the default dataset for a `Mag` is
850 ``'meas'``, rather than ``'ref'``.
851
852 Parameters
853 ----------
854 col : `str`
855 Name of flux column from which to compute magnitude.
856 Can be parseable by the `~lsst.pipe.tasks.functors.fluxName` function;
857 that is, you can pass ``'modelfit_CModel'`` instead of
858 ``'modelfit_CModel_instFlux'``, and it will understand.
859 """
860 _defaultDataset = 'meas'
861
862 def __init__(self, col, **kwargs):
863 self.col = fluxName(col)
864 # TO DO: DM-21955 Replace hard coded photometic calibration values.
865 self.fluxMag0 = 63095734448.0194
866
867 super().__init__(**kwargs)
868
869 @property
870 def columns(self):
871 return [self.col]
872
873 def _func(self, df):
874 with warnings.catch_warnings():
875 warnings.filterwarnings('ignore', r'invalid value encountered')
876 warnings.filterwarnings('ignore', r'divide by zero')
877 return -2.5*np.log10(df[self.col] / self.fluxMag0)
878
879 @property
880 def name(self):
881 return f'mag_{self.col}'
882
883
884class MagErr(Mag):
885 """Compute calibrated magnitude uncertainty.
886
887 Parameters
888 ----------
889 col : `str`
890 Name of the flux column.
891 """
892
893 def __init__(self, *args, **kwargs):
894 super().__init__(*args, **kwargs)
895 # TO DO: DM-21955 Replace hard coded photometic calibration values.
896 self.fluxMag0Err = 0.
897
898 @property
899 def columns(self):
900 return [self.col, self.col + 'Err']
901
902 def _func(self, df):
903 with warnings.catch_warnings():
904 warnings.filterwarnings('ignore', r'invalid value encountered')
905 warnings.filterwarnings('ignore', r'divide by zero')
906 fluxCol, fluxErrCol = self.columns
907 x = df[fluxErrCol] / df[fluxCol]
908 y = self.fluxMag0Err / self.fluxMag0
909 magErr = (2.5 / np.log(10.)) * np.sqrt(x*x + y*y)
910 return magErr
911
912 @property
913 def name(self):
914 return super().name + '_err'
915
916
918 """Functor to calculate magnitude difference."""
919 _defaultDataset = 'meas'
920
921 def __init__(self, col1, col2, **kwargs):
922 self.col1 = fluxName(col1)
923 self.col2 = fluxName(col2)
924 super().__init__(**kwargs)
925
926 @property
927 def columns(self):
928 return [self.col1, self.col2]
929
930 def _func(self, df):
931 with warnings.catch_warnings():
932 warnings.filterwarnings('ignore', r'invalid value encountered')
933 warnings.filterwarnings('ignore', r'divide by zero')
934 return -2.5*np.log10(df[self.col1]/df[self.col2])
935
936 @property
937 def name(self):
938 return f'(mag_{self.col1} - mag_{self.col2})'
939
940 @property
941 def shortname(self):
942 return f'magDiff_{self.col1}_{self.col2}'
943
944
946 """Compute the color between two filters.
947
948 Computes color by initializing two different `Mag` functors based on the
949 ``col`` and filters provided, and then returning the difference.
950
951 This is enabled by the `_func` method expecting a DataFrame with a
952 multilevel column index, with both ``'band'`` and ``'column'``, instead of
953 just ``'column'``, which is the `Functor` default.
954 This is controlled by the `_dfLevels` attribute.
955
956 Also of note, the default dataset for `Color` is ``forced_src'``, whereas
957 for `Mag` it is ``'meas'``.
958
959 Parameters
960 ----------
961 col : str
962 Name of the flux column from which to compute; same as would be passed
963 to `~lsst.pipe.tasks.functors.Mag`.
964
965 filt2, filt1 : str
966 Filters from which to compute magnitude difference.
967 Color computed is ``Mag(filt2) - Mag(filt1)``.
968 """
969 _defaultDataset = 'forced_src'
970 _dfLevels = ('band', 'column')
971 _defaultNoDup = True
972
973 def __init__(self, col, filt2, filt1, **kwargs):
974 self.col = fluxName(col)
975 if filt2 == filt1:
976 raise RuntimeError("Cannot compute Color for %s: %s - %s " % (col, filt2, filt1))
977 self.filt2 = filt2
978 self.filt1 = filt1
979
980 self.mag2 = Mag(col, filt=filt2, **kwargs)
981 self.mag1 = Mag(col, filt=filt1, **kwargs)
982
983 super().__init__(**kwargs)
984
985 @property
986 def filt(self):
987 return None
988
989 @filt.setter
990 def filt(self, filt):
991 pass
992
993 def _func(self, df):
994 mag2 = self.mag2._func(df[self.filt2])
995 mag1 = self.mag1._func(df[self.filt1])
996 return mag2 - mag1
997
998 @property
999 def columns(self):
1000 return [self.mag1.col, self.mag2.col]
1001
1002 def multilevelColumns(self, parq, **kwargs):
1003 return [(self.dataset, self.filt1, self.col), (self.dataset, self.filt2, self.col)]
1004
1005 @property
1006 def name(self):
1007 return f'{self.filt2} - {self.filt1} ({self.col})'
1008
1009 @property
1010 def shortname(self):
1011 return f"{self.col}_{self.filt2.replace('-', '')}m{self.filt1.replace('-', '')}"
1012
1013
1015 """This functor subtracts the trace of the PSF second moments from the
1016 trace of the second moments of the source.
1017
1018 If the HsmShapeAlgorithm measurement is valid, then these will be used for
1019 the sources.
1020 Otherwise, the SdssShapeAlgorithm measurements will be used.
1021 """
1022 name = 'Deconvolved Moments'
1023 shortname = 'deconvolvedMoments'
1024 _columns = ("ext_shapeHSM_HsmSourceMoments_xx",
1025 "ext_shapeHSM_HsmSourceMoments_yy",
1026 "base_SdssShape_xx", "base_SdssShape_yy",
1027 "ext_shapeHSM_HsmPsfMoments_xx",
1028 "ext_shapeHSM_HsmPsfMoments_yy")
1029
1030 def _func(self, df):
1031 """Calculate deconvolved moments."""
1032 if "ext_shapeHSM_HsmSourceMoments_xx" in df.columns: # _xx added by tdm
1033 hsm = df["ext_shapeHSM_HsmSourceMoments_xx"] + df["ext_shapeHSM_HsmSourceMoments_yy"]
1034 else:
1035 hsm = np.ones(len(df))*np.nan
1036 sdss = df["base_SdssShape_xx"] + df["base_SdssShape_yy"]
1037 if "ext_shapeHSM_HsmPsfMoments_xx" in df.columns:
1038 psf = df["ext_shapeHSM_HsmPsfMoments_xx"] + df["ext_shapeHSM_HsmPsfMoments_yy"]
1039 else:
1040 # LSST does not have shape.sdss.psf.
1041 # We could instead add base_PsfShape to the catalog using
1042 # exposure.getPsf().computeShape(s.getCentroid()).getIxx().
1043 raise RuntimeError('No psf shape parameter found in catalog')
1044
1045 return hsm.where(np.isfinite(hsm), sdss) - psf
1046
1047
1049 """Functor to calculate the SDSS trace radius size for sources.
1050
1051 The SDSS trace radius size is a measure of size equal to the square root of
1052 half of the trace of the second moments tensor measured with the
1053 SdssShapeAlgorithm plugin.
1054 This has units of pixels.
1055 """
1056 name = "SDSS Trace Size"
1057 shortname = 'sdssTrace'
1058 _columns = ("base_SdssShape_xx", "base_SdssShape_yy")
1059
1060 def _func(self, df):
1061 srcSize = np.sqrt(0.5*(df["base_SdssShape_xx"] + df["base_SdssShape_yy"]))
1062 return srcSize
1063
1064
1066 """Functor to calculate the SDSS trace radius size difference (%) between
1067 the object and the PSF model.
1068
1069 See Also
1070 --------
1071 SdssTraceSize
1072 """
1073 name = "PSF - SDSS Trace Size"
1074 shortname = 'psf_sdssTrace'
1075 _columns = ("base_SdssShape_xx", "base_SdssShape_yy",
1076 "base_SdssShape_psf_xx", "base_SdssShape_psf_yy")
1077
1078 def _func(self, df):
1079 srcSize = np.sqrt(0.5*(df["base_SdssShape_xx"] + df["base_SdssShape_yy"]))
1080 psfSize = np.sqrt(0.5*(df["base_SdssShape_psf_xx"] + df["base_SdssShape_psf_yy"]))
1081 sizeDiff = 100*(srcSize - psfSize)/(0.5*(srcSize + psfSize))
1082 return sizeDiff
1083
1084
1086 """Functor to calculate the HSM trace radius size for sources.
1087
1088 The HSM trace radius size is a measure of size equal to the square root of
1089 half of the trace of the second moments tensor measured with the
1090 HsmShapeAlgorithm plugin.
1091 This has units of pixels.
1092 """
1093 name = 'HSM Trace Size'
1094 shortname = 'hsmTrace'
1095 _columns = ("ext_shapeHSM_HsmSourceMoments_xx",
1096 "ext_shapeHSM_HsmSourceMoments_yy")
1097
1098 def _func(self, df):
1099 srcSize = np.sqrt(0.5*(df["ext_shapeHSM_HsmSourceMoments_xx"]
1100 + df["ext_shapeHSM_HsmSourceMoments_yy"]))
1101 return srcSize
1102
1103
1105 """Functor to calculate the HSM trace radius size difference (%) between
1106 the object and the PSF model.
1107
1108 See Also
1109 --------
1110 HsmTraceSize
1111 """
1112 name = 'PSF - HSM Trace Size'
1113 shortname = 'psf_HsmTrace'
1114 _columns = ("ext_shapeHSM_HsmSourceMoments_xx",
1115 "ext_shapeHSM_HsmSourceMoments_yy",
1116 "ext_shapeHSM_HsmPsfMoments_xx",
1117 "ext_shapeHSM_HsmPsfMoments_yy")
1118
1119 def _func(self, df):
1120 srcSize = np.sqrt(0.5*(df["ext_shapeHSM_HsmSourceMoments_xx"]
1121 + df["ext_shapeHSM_HsmSourceMoments_yy"]))
1122 psfSize = np.sqrt(0.5*(df["ext_shapeHSM_HsmPsfMoments_xx"]
1123 + df["ext_shapeHSM_HsmPsfMoments_yy"]))
1124 sizeDiff = 100*(srcSize - psfSize)/(0.5*(srcSize + psfSize))
1125 return sizeDiff
1126
1127
1129 """Functor to calculate the PSF FWHM with second moments measured from the
1130 HsmShapeAlgorithm plugin.
1131
1132 This is in units of arcseconds, and assumes the hsc_rings_v1 skymap pixel
1133 scale of 0.168 arcseconds/pixel.
1134
1135 Notes
1136 -----
1137 This conversion assumes the PSF is Gaussian, which is not always the case.
1138 """
1139 name = 'HSM Psf FWHM'
1140 _columns = ('ext_shapeHSM_HsmPsfMoments_xx', 'ext_shapeHSM_HsmPsfMoments_yy')
1141 # TODO: DM-21403 pixel scale should be computed from the CD matrix or transform matrix
1142 pixelScale = 0.168
1143 SIGMA2FWHM = 2*np.sqrt(2*np.log(2))
1144
1145 def _func(self, df):
1146 return (self.pixelScale*self.SIGMA2FWHM*np.sqrt(
1147 0.5*(df['ext_shapeHSM_HsmPsfMoments_xx']
1148 + df['ext_shapeHSM_HsmPsfMoments_yy']))).astype(np.float32)
1149
1150
1152 r"""Calculate :math:`e_1` ellipticity component for sources, defined as:
1153
1154 .. math::
1155 e_1 &= (I_{xx}-I_{yy})/(I_{xx}+I_{yy})
1156
1157 See Also
1158 --------
1159 E2
1160 """
1161 name = "Distortion Ellipticity (e1)"
1162 shortname = "Distortion"
1163
1164 def __init__(self, colXX, colXY, colYY, **kwargs):
1165 self.colXX = colXX
1166 self.colXY = colXY
1167 self.colYY = colYY
1168 self._columns = [self.colXX, self.colXY, self.colYY]
1169 super().__init__(**kwargs)
1170
1171 @property
1172 def columns(self):
1173 return [self.colXX, self.colXY, self.colYY]
1174
1175 def _func(self, df):
1176 return ((df[self.colXX] - df[self.colYY]) / (
1177 df[self.colXX] + df[self.colYY])).astype(np.float32)
1178
1179
1181 r"""Calculate :math:`e_2` ellipticity component for sources, defined as:
1182
1183 .. math::
1184 e_2 &= 2I_{xy}/(I_{xx}+I_{yy})
1185
1186 See Also
1187 --------
1188 E1
1189 """
1190 name = "Ellipticity e2"
1191
1192 def __init__(self, colXX, colXY, colYY, **kwargs):
1193 self.colXX = colXX
1194 self.colXY = colXY
1195 self.colYY = colYY
1196 super().__init__(**kwargs)
1197
1198 @property
1199 def columns(self):
1200 return [self.colXX, self.colXY, self.colYY]
1201
1202 def _func(self, df):
1203 return (2*df[self.colXY] / (df[self.colXX] + df[self.colYY])).astype(np.float32)
1204
1205
1207 """Calculate the radius from the quadrupole moments.
1208
1209 This returns the fourth root of the determinant of the second moments
1210 tensor, which has units of pixels.
1211
1212 See Also
1213 --------
1214 SdssTraceSize
1215 HsmTraceSize
1216 """
1217
1218 def __init__(self, colXX, colXY, colYY, **kwargs):
1219 self.colXX = colXX
1220 self.colXY = colXY
1221 self.colYY = colYY
1222 super().__init__(**kwargs)
1223
1224 @property
1225 def columns(self):
1226 return [self.colXX, self.colXY, self.colYY]
1227
1228 def _func(self, df):
1229 return ((df[self.colXX]*df[self.colYY] - df[self.colXY]**2)**0.25).astype(np.float32)
1230
1231
1233 """Computations using the stored localWcs."""
1234 name = "LocalWcsOperations"
1235
1236 def __init__(self,
1237 colCD_1_1,
1238 colCD_1_2,
1239 colCD_2_1,
1240 colCD_2_2,
1241 **kwargs):
1242 self.colCD_1_1 = colCD_1_1
1243 self.colCD_1_2 = colCD_1_2
1244 self.colCD_2_1 = colCD_2_1
1245 self.colCD_2_2 = colCD_2_2
1246 super().__init__(**kwargs)
1247
1248 def computeDeltaRaDec(self, x, y, cd11, cd12, cd21, cd22):
1249 """Compute the dRA, dDec from dx, dy.
1250
1251 Parameters
1252 ----------
1253 x : `~pandas.Series`
1254 X pixel coordinate.
1255 y : `~pandas.Series`
1256 Y pixel coordinate.
1257 cd11 : `~pandas.Series`
1258 [1, 1] element of the local Wcs affine transform.
1259 cd12 : `~pandas.Series`
1260 [1, 2] element of the local Wcs affine transform.
1261 cd21 : `~pandas.Series`
1262 [2, 1] element of the local Wcs affine transform.
1263 cd22 : `~pandas.Series`
1264 [2, 2] element of the local Wcs affine transform.
1265
1266 Returns
1267 -------
1268 raDecTuple : tuple
1269 RA and Dec conversion of x and y given the local Wcs.
1270 Returned units are in radians.
1271
1272 Notes
1273 -----
1274 If x and y are with respect to the CRVAL1, CRVAL2
1275 then this will return the RA, Dec for that WCS.
1276 """
1277 return (x * cd11 + y * cd12, x * cd21 + y * cd22)
1278
1279 def computeSkySeparation(self, ra1, dec1, ra2, dec2):
1280 """Compute the local pixel scale conversion.
1281
1282 Parameters
1283 ----------
1284 ra1 : `~pandas.Series`
1285 Ra of the first coordinate in radians.
1286 dec1 : `~pandas.Series`
1287 Dec of the first coordinate in radians.
1288 ra2 : `~pandas.Series`
1289 Ra of the second coordinate in radians.
1290 dec2 : `~pandas.Series`
1291 Dec of the second coordinate in radians.
1292
1293 Returns
1294 -------
1295 dist : `~pandas.Series`
1296 Distance on the sphere in radians.
1297 """
1298 deltaDec = dec2 - dec1
1299 deltaRa = ra2 - ra1
1300 return 2 * np.arcsin(
1301 np.sqrt(
1302 np.sin(deltaDec / 2) ** 2
1303 + np.cos(dec2) * np.cos(dec1) * np.sin(deltaRa / 2) ** 2))
1304
1305 def getSkySeparationFromPixel(self, x1, y1, x2, y2, cd11, cd12, cd21, cd22):
1306 """Compute the distance on the sphere from x2, y1 to x1, y1.
1307
1308 Parameters
1309 ----------
1310 x1 : `~pandas.Series`
1311 X pixel coordinate.
1312 y1 : `~pandas.Series`
1313 Y pixel coordinate.
1314 x2 : `~pandas.Series`
1315 X pixel coordinate.
1316 y2 : `~pandas.Series`
1317 Y pixel coordinate.
1318 cd11 : `~pandas.Series`
1319 [1, 1] element of the local Wcs affine transform.
1320 cd12 : `~pandas.Series`
1321 [1, 2] element of the local Wcs affine transform.
1322 cd21 : `~pandas.Series`
1323 [2, 1] element of the local Wcs affine transform.
1324 cd22 : `~pandas.Series`
1325 [2, 2] element of the local Wcs affine transform.
1326
1327 Returns
1328 -------
1329 Distance : `~pandas.Series`
1330 Arcseconds per pixel at the location of the local WC.
1331 """
1332 ra1, dec1 = self.computeDeltaRaDec(x1, y1, cd11, cd12, cd21, cd22)
1333 ra2, dec2 = self.computeDeltaRaDec(x2, y2, cd11, cd12, cd21, cd22)
1334 # Great circle distance for small separations.
1335 return self.computeSkySeparation(ra1, dec1, ra2, dec2)
1336
1337 def computePositionAngle(self, ra1, dec1, ra2, dec2):
1338 """Compute position angle (E of N) from (ra1, dec1) to (ra2, dec2).
1339
1340 Parameters
1341 ----------
1342 ra1 : iterable [`float`]
1343 RA of the first coordinate [radian].
1344 dec1 : iterable [`float`]
1345 Dec of the first coordinate [radian].
1346 ra2 : iterable [`float`]
1347 RA of the second coordinate [radian].
1348 dec2 : iterable [`float`]
1349 Dec of the second coordinate [radian].
1350
1351 Returns
1352 -------
1353 Position Angle: `~pandas.Series`
1354 radians E of N
1355
1356 Notes
1357 -----
1358 (ra1, dec1) -> (ra2, dec2) is interpreted as the shorter way around the sphere
1359
1360 For a separation of 0.0001 rad, the position angle is good to 0.0009 rad
1361 all over the sphere.
1362 """
1363 # lsst.geom.SpherePoint has "bearingTo", which returns angle N of E
1364 # We instead want the astronomy convention of "Position Angle", which is angle E of N
1365 position_angle = np.zeros(len(ra1))
1366 for i, (r1, d1, r2, d2) in enumerate(zip(ra1, dec1, ra2, dec2)):
1367 point1 = geom.SpherePoint(r1, d1, geom.radians)
1368 point2 = geom.SpherePoint(r2, d2, geom.radians)
1369 bearing = point1.bearingTo(point2)
1370 pa_ref_angle = geom.Angle(np.pi/2, geom.radians) # in bearing system
1371 pa = pa_ref_angle - bearing
1372 # Wrap around to get Delta_RA from -pi to +pi
1373 pa = pa.wrapCtr()
1374 position_angle[i] = pa.asRadians()
1375
1376 return pd.Series(position_angle)
1377
1378 def getPositionAngleFromDetectorAngle(self, theta, cd11, cd12, cd21, cd22):
1379 """Compute position angle (E of N) from detector angle (+y of +x).
1380
1381 Parameters
1382 ----------
1383 theta : `float`
1384 detector angle [radian]
1385 cd11 : `float`
1386 [1, 1] element of the local Wcs affine transform.
1387 cd12 : `float`
1388 [1, 2] element of the local Wcs affine transform.
1389 cd21 : `float`
1390 [2, 1] element of the local Wcs affine transform.
1391 cd22 : `float`
1392 [2, 2] element of the local Wcs affine transform.
1393
1394 Returns
1395 -------
1396 Position Angle: `~pandas.Series`
1397 Degrees E of N.
1398 """
1399 # Create a unit vector in (x, y) along da
1400 dx = np.cos(theta)
1401 dy = np.sin(theta)
1402 ra1, dec1 = self.computeDeltaRaDec(0, 0, cd11, cd12, cd21, cd22)
1403 ra2, dec2 = self.computeDeltaRaDec(dx, dy, cd11, cd12, cd21, cd22)
1404 # Position angle of vector from (RA1, Dec1) to (RA2, Dec2)
1405 return np.rad2deg(self.computePositionAngle(ra1, dec1, ra2, dec2))
1406
1407
1409 """Compute the local pixel scale from the stored CDMatrix.
1410 """
1411 name = "PixelScale"
1412
1413 @property
1414 def columns(self):
1415 return [self.colCD_1_1,
1418 self.colCD_2_2]
1419
1420 def pixelScaleArcseconds(self, cd11, cd12, cd21, cd22):
1421 """Compute the local pixel to scale conversion in arcseconds.
1422
1423 Parameters
1424 ----------
1425 cd11 : `~pandas.Series`
1426 [1, 1] element of the local Wcs affine transform in radians.
1427 cd11 : `~pandas.Series`
1428 [1, 1] element of the local Wcs affine transform in radians.
1429 cd12 : `~pandas.Series`
1430 [1, 2] element of the local Wcs affine transform in radians.
1431 cd21 : `~pandas.Series`
1432 [2, 1] element of the local Wcs affine transform in radians.
1433 cd22 : `~pandas.Series`
1434 [2, 2] element of the local Wcs affine transform in radians.
1435
1436 Returns
1437 -------
1438 pixScale : `~pandas.Series`
1439 Arcseconds per pixel at the location of the local WC.
1440 """
1441 return 3600 * np.degrees(np.sqrt(np.fabs(cd11 * cd22 - cd12 * cd21)))
1442
1443 def _func(self, df):
1444 return self.pixelScaleArcseconds(df[self.colCD_1_1],
1445 df[self.colCD_1_2],
1446 df[self.colCD_2_1],
1447 df[self.colCD_2_2])
1448
1449
1451 """Convert a value in units of pixels to units of arcseconds."""
1452
1453 def __init__(self,
1454 col,
1455 colCD_1_1,
1456 colCD_1_2,
1457 colCD_2_1,
1458 colCD_2_2,
1459 **kwargs):
1460 self.col = col
1461 super().__init__(colCD_1_1,
1462 colCD_1_2,
1463 colCD_2_1,
1464 colCD_2_2,
1465 **kwargs)
1466
1467 @property
1468 def name(self):
1469 return f"{self.col}_asArcseconds"
1470
1471 @property
1472 def columns(self):
1473 return [self.col,
1477 self.colCD_2_2]
1478
1479 def _func(self, df):
1480 return df[self.col] * self.pixelScaleArcseconds(df[self.colCD_1_1],
1481 df[self.colCD_1_2],
1482 df[self.colCD_2_1],
1483 df[self.colCD_2_2])
1484
1485
1487 """Convert a value in units of pixels squared to units of arcseconds
1488 squared.
1489 """
1490
1491 def __init__(self,
1492 col,
1493 colCD_1_1,
1494 colCD_1_2,
1495 colCD_2_1,
1496 colCD_2_2,
1497 **kwargs):
1498 self.col = col
1499 super().__init__(colCD_1_1,
1500 colCD_1_2,
1501 colCD_2_1,
1502 colCD_2_2,
1503 **kwargs)
1504
1505 @property
1506 def name(self):
1507 return f"{self.col}_asArcsecondsSq"
1508
1509 @property
1510 def columns(self):
1511 return [self.col,
1515 self.colCD_2_2]
1516
1517 def _func(self, df):
1518 pixScale = self.pixelScaleArcseconds(df[self.colCD_1_1],
1519 df[self.colCD_1_2],
1520 df[self.colCD_2_1],
1521 df[self.colCD_2_2])
1522 return df[self.col] * pixScale * pixScale
1523
1524
1526 """Compute a position angle from a detector angle and the stored CDMatrix.
1527
1528 Returns
1529 -------
1530 position angle : degrees
1531 """
1532
1533 name = "PositionAngle"
1534
1536 self,
1537 theta_col,
1538 colCD_1_1,
1539 colCD_1_2,
1540 colCD_2_1,
1541 colCD_2_2,
1542 **kwargs
1543 ):
1544 self.theta_col = theta_col
1545 super().__init__(colCD_1_1, colCD_1_2, colCD_2_1, colCD_2_2, **kwargs)
1546
1547 @property
1548 def columns(self):
1549 return [
1550 self.theta_col,
1554 self.colCD_2_2
1555 ]
1556
1557 def _func(self, df):
1559 df[self.theta_col],
1560 df[self.colCD_1_1],
1561 df[self.colCD_1_2],
1562 df[self.colCD_2_1],
1563 df[self.colCD_2_2]
1564 )
1565
1566
1568 """Convert a detector angle error to a position angle error.
1569
1570 Returns
1571 -------
1572 position angle error : degrees
1573 """
1574
1575 name = "PositionAngleErr"
1576
1577 def __init__(self, theta_err_col, **kwargs):
1578 self.theta_err_col = theta_err_col
1579 super().__init__(**kwargs)
1580
1581 @property
1582 def columns(self):
1583 return [self.theta_err_col]
1584
1585 def _func(self, df):
1586 return np.rad2deg(df[self.theta_err_col])
1587
1588
1590 """Return the band used to seed multiband forced photometry.
1591
1592 This functor is to be used on Object tables.
1593 It converts the boolean merge_measurements_{band} columns into a single
1594 string representing the first band for which merge_measurements_{band}
1595 is True.
1596
1597 Assumes the default priority order of i, r, z, y, g, u.
1598 """
1599 name = 'Reference Band'
1600 shortname = 'refBand'
1601
1602 band_order = ("i", "r", "z", "y", "g", "u")
1603
1604 @property
1605 def columns(self):
1606 # Build the actual input column list, not hardcoded ugrizy
1607 bands = [band for band in self.band_order if band in self.bands]
1608 # In the unlikely scenario that users attempt to add non-ugrizy bands
1609 bands += [band for band in self.bands if band not in self.band_order]
1610 return [f"merge_measurement_{band}" for band in bands]
1611
1612 def _func(self, df: pd.DataFrame) -> pd.Series:
1613 def getFilterAliasName(row):
1614 # Get column name with the max value (True > False).
1615 colName = row.idxmax()
1616 return colName.replace('merge_measurement_', '')
1617
1618 # Skip columns that are unavailable, because this functor requests the
1619 # superset of bands that could be included in the object table.
1620 columns = [col for col in self.columns if col in df.columns]
1621 # Makes a Series of dtype object if df is empty.
1622 return df[columns].apply(getFilterAliasName, axis=1,
1623 result_type='reduce').astype('object')
1624
1625 def __init__(self, bands: tuple[str] | list[str] | None = None, **kwargs):
1626 super().__init__(**kwargs)
1627 self.bands = self.band_order if bands is None else tuple(bands)
1628
1629
1631 """Base class for Object table calibrated fluxes and magnitudes."""
1632 # AB to NanoJansky (3631 Jansky).
1633 AB_FLUX_SCALE = (0 * u.ABmag).to_value(u.nJy)
1634 LOG_AB_FLUX_SCALE = 12.56
1635 FIVE_OVER_2LOG10 = 1.085736204758129569
1636 # TO DO: DM-21955 Replace hard coded photometic calibration values.
1637 COADD_ZP = 27
1638
1639 def __init__(self, colFlux, colFluxErr=None, **kwargs):
1640 self.vhypot = np.vectorize(self.hypot)
1641 self.col = colFlux
1642 self.colFluxErr = colFluxErr
1643
1644 self.fluxMag0 = 1./np.power(10, -0.4*self.COADD_ZP)
1645 self.fluxMag0Err = 0.
1646
1647 super().__init__(**kwargs)
1648
1649 @property
1650 def columns(self):
1651 return [self.col]
1652
1653 @property
1654 def name(self):
1655 return f'mag_{self.col}'
1656
1657 @classmethod
1658 def hypot(cls, a, b):
1659 """Compute sqrt(a^2 + b^2) without under/overflow."""
1660 if np.abs(a) < np.abs(b):
1661 a, b = b, a
1662 if a == 0.:
1663 return 0.
1664 q = b/a
1665 return np.abs(a) * np.sqrt(1. + q*q)
1666
1667 def dn2flux(self, dn, fluxMag0):
1668 """Convert instrumental flux to nanojanskys."""
1669 return (self.AB_FLUX_SCALE * dn / fluxMag0).astype(np.float32)
1670
1671 def dn2mag(self, dn, fluxMag0):
1672 """Convert instrumental flux to AB magnitude."""
1673 with warnings.catch_warnings():
1674 warnings.filterwarnings('ignore', r'invalid value encountered')
1675 warnings.filterwarnings('ignore', r'divide by zero')
1676 return (-2.5 * np.log10(dn/fluxMag0)).astype(np.float32)
1677
1678 def dn2fluxErr(self, dn, dnErr, fluxMag0, fluxMag0Err):
1679 """Convert instrumental flux error to nanojanskys."""
1680 retVal = self.vhypot(dn * fluxMag0Err, dnErr * fluxMag0)
1681 retVal *= self.AB_FLUX_SCALE / fluxMag0 / fluxMag0
1682 return retVal.astype(np.float32)
1683
1684 def dn2MagErr(self, dn, dnErr, fluxMag0, fluxMag0Err):
1685 """Convert instrumental flux error to AB magnitude error."""
1686 retVal = self.dn2fluxErr(dn, dnErr, fluxMag0, fluxMag0Err) / self.dn2flux(dn, fluxMag0)
1687 return (self.FIVE_OVER_2LOG10 * retVal).astype(np.float32)
1688
1689
1691 """Convert instrumental flux to nanojanskys."""
1692 def _func(self, df):
1693 return self.dn2flux(df[self.col], self.fluxMag0)
1694
1695
1697 """Convert instrumental flux error to nanojanskys."""
1698 @property
1699 def columns(self):
1700 return [self.col, self.colFluxErr]
1701
1702 def _func(self, df):
1703 retArr = self.dn2fluxErr(df[self.col], df[self.colFluxErr], self.fluxMag0, self.fluxMag0Err)
1704 return pd.Series(retArr, index=df.index)
1705
1706
1708 """Base class for calibrating the specified instrument flux column using
1709 the local photometric calibration.
1710
1711 Parameters
1712 ----------
1713 instFluxCol : `str`
1714 Name of the instrument flux column.
1715 instFluxErrCol : `str`
1716 Name of the assocated error columns for ``instFluxCol``.
1717 photoCalibCol : `str`
1718 Name of local calibration column.
1719 photoCalibErrCol : `str`, optional
1720 Error associated with ``photoCalibCol``. Ignored and deprecated; will
1721 be removed after v29.
1722
1723 See Also
1724 --------
1725 LocalNanojansky
1726 LocalNanojanskyErr
1727 """
1728 logNJanskyToAB = (1 * u.nJy).to_value(u.ABmag)
1729
1730 def __init__(self,
1731 instFluxCol,
1732 instFluxErrCol,
1733 photoCalibCol,
1734 photoCalibErrCol=None,
1735 **kwargs):
1736 self.instFluxCol = instFluxCol
1737 self.instFluxErrCol = instFluxErrCol
1738 self.photoCalibCol = photoCalibCol
1739 # TODO[DM-49400]: remove this check and the argument it corresponds to.
1740 if photoCalibErrCol is not None:
1741 warnings.warn("The photoCalibErrCol argument is deprecated and will be removed after v29.",
1742 category=FutureWarning)
1743 super().__init__(**kwargs)
1744
1745 def instFluxToNanojansky(self, instFlux, localCalib):
1746 """Convert instrument flux to nanojanskys.
1747
1748 Parameters
1749 ----------
1750 instFlux : `~numpy.ndarray` or `~pandas.Series`
1751 Array of instrument flux measurements.
1752 localCalib : `~numpy.ndarray` or `~pandas.Series`
1753 Array of local photometric calibration estimates.
1754
1755 Returns
1756 -------
1757 calibFlux : `~numpy.ndarray` or `~pandas.Series`
1758 Array of calibrated flux measurements.
1759 """
1760 return instFlux * localCalib
1761
1762 def instFluxErrToNanojanskyErr(self, instFlux, instFluxErr, localCalib, localCalibErr=None):
1763 """Convert instrument flux to nanojanskys.
1764
1765 Parameters
1766 ----------
1767 instFlux : `~numpy.ndarray` or `~pandas.Series`
1768 Array of instrument flux measurements. Ignored (accepted for
1769 backwards compatibility and consistency with magnitude-error
1770 calculation methods).
1771 instFluxErr : `~numpy.ndarray` or `~pandas.Series`
1772 Errors on associated ``instFlux`` values.
1773 localCalib : `~numpy.ndarray` or `~pandas.Series`
1774 Array of local photometric calibration estimates.
1775 localCalibErr : `~numpy.ndarray` or `~pandas.Series`, optional
1776 Errors on associated ``localCalib`` values. Ignored and deprecated;
1777 will be removed after v29.
1778
1779 Returns
1780 -------
1781 calibFluxErr : `~numpy.ndarray` or `~pandas.Series`
1782 Errors on calibrated flux measurements.
1783 """
1784 # TODO[DM-49400]: remove this check and the argument it corresponds to.
1785 if localCalibErr is not None:
1786 warnings.warn("The localCalibErr argument is deprecated and will be removed after v29.",
1787 category=FutureWarning)
1788 return instFluxErr * localCalib
1789
1790 def instFluxToMagnitude(self, instFlux, localCalib):
1791 """Convert instrument flux to nanojanskys.
1792
1793 Parameters
1794 ----------
1795 instFlux : `~numpy.ndarray` or `~pandas.Series`
1796 Array of instrument flux measurements.
1797 localCalib : `~numpy.ndarray` or `~pandas.Series`
1798 Array of local photometric calibration estimates.
1799
1800 Returns
1801 -------
1802 calibMag : `~numpy.ndarray` or `~pandas.Series`
1803 Array of calibrated AB magnitudes.
1804 """
1805 return -2.5 * np.log10(self.instFluxToNanojansky(instFlux, localCalib)) + self.logNJanskyToAB
1806
1807 def instFluxErrToMagnitudeErr(self, instFlux, instFluxErr, localCalib, localCalibErr=None):
1808 """Convert instrument flux err to nanojanskys.
1809
1810 Parameters
1811 ----------
1812 instFlux : `~numpy.ndarray` or `~pandas.Series`
1813 Array of instrument flux measurements.
1814 instFluxErr : `~numpy.ndarray` or `~pandas.Series`
1815 Errors on associated ``instFlux`` values.
1816 localCalib : `~numpy.ndarray` or `~pandas.Series`
1817 Array of local photometric calibration estimates.
1818 localCalibErr : `~numpy.ndarray` or `~pandas.Series`, optional
1819 Errors on associated ``localCalib`` values. Ignored and deprecated;
1820 will be removed after v29.
1821
1822 Returns
1823 -------
1824 calibMagErr: `~numpy.ndarray` or `~pandas.Series`
1825 Error on calibrated AB magnitudes.
1826 """
1827 # TODO[DM-49400]: remove this check and the argument it corresponds to.
1828 if localCalibErr is not None:
1829 warnings.warn("The localCalibErr argument is deprecated and will be removed after v29.",
1830 category=FutureWarning)
1831 err = self.instFluxErrToNanojanskyErr(instFlux, instFluxErr, localCalib)
1832 return 2.5 / np.log(10) * err / self.instFluxToNanojansky(instFlux, instFluxErr)
1833
1834
1836 """Compute calibrated fluxes using the local calibration value.
1837
1838 This returns units of nanojanskys.
1839 """
1840
1841 @property
1842 def columns(self):
1843 return [self.instFluxCol, self.photoCalibCol]
1844
1845 @property
1846 def name(self):
1847 return f'flux_{self.instFluxCol}'
1848
1849 def _func(self, df):
1850 return self.instFluxToNanojansky(df[self.instFluxCol],
1851 df[self.photoCalibCol]).astype(np.float32)
1852
1853
1855 """Compute calibrated flux errors using the local calibration value.
1856
1857 This returns units of nanojanskys.
1858 """
1859
1860 @property
1861 def columns(self):
1862 return [self.instFluxCol, self.instFluxErrCol, self.photoCalibCol]
1863
1864 @property
1865 def name(self):
1866 return f'fluxErr_{self.instFluxCol}'
1867
1868 def _func(self, df):
1869 return self.instFluxErrToNanojanskyErr(df[self.instFluxCol], df[self.instFluxErrCol],
1870 df[self.photoCalibCol]).astype(np.float32)
1871
1872
1874 """Compute absolute mean of dipole fluxes.
1875
1876 See Also
1877 --------
1878 LocalNanojansky
1879 LocalNanojanskyErr
1880 LocalDipoleMeanFluxErr
1881 LocalDipoleDiffFlux
1882 LocalDipoleDiffFluxErr
1883 """
1884 def __init__(self,
1885 instFluxPosCol,
1886 instFluxNegCol,
1887 instFluxPosErrCol,
1888 instFluxNegErrCol,
1889 photoCalibCol,
1890 # TODO[DM-49400]: remove this option; it's already deprecated (in super).
1891 photoCalibErrCol=None,
1892 **kwargs):
1893 self.instFluxNegCol = instFluxNegCol
1894 self.instFluxPosCol = instFluxPosCol
1895 self.instFluxNegErrCol = instFluxNegErrCol
1896 self.instFluxPosErrCol = instFluxPosErrCol
1897 self.photoCalibCol = photoCalibCol
1898 super().__init__(instFluxNegCol,
1899 instFluxNegErrCol,
1900 photoCalibCol,
1901 photoCalibErrCol,
1902 **kwargs)
1903
1904 @property
1905 def columns(self):
1906 return [self.instFluxPosCol,
1907 self.instFluxNegCol,
1908 self.photoCalibCol]
1909
1910 @property
1911 def name(self):
1912 return f'dipMeanFlux_{self.instFluxPosCol}_{self.instFluxNegCol}'
1913
1914 def _func(self, df):
1915 return 0.5*(np.fabs(self.instFluxToNanojansky(df[self.instFluxNegCol], df[self.photoCalibCol]))
1916 + np.fabs(self.instFluxToNanojansky(df[self.instFluxPosCol], df[self.photoCalibCol])))
1917
1918
1920 """Compute the error on the absolute mean of dipole fluxes.
1921
1922 See Also
1923 --------
1924 LocalNanojansky
1925 LocalNanojanskyErr
1926 LocalDipoleMeanFlux
1927 LocalDipoleDiffFlux
1928 LocalDipoleDiffFluxErr
1929 """
1930
1931 @property
1939 @property
1940 def name(self):
1941 return f'dipMeanFluxErr_{self.instFluxPosCol}_{self.instFluxNegCol}'
1942
1943 def _func(self, df):
1944 return 0.5*np.hypot(df[self.instFluxNegErrCol], df[self.instFluxPosErrCol]) * df[self.photoCalibCol]
1945
1946
1948 """Compute the absolute difference of dipole fluxes.
1949
1950 Calculated value is (abs(pos) - abs(neg)).
1951
1952 See Also
1953 --------
1954 LocalNanojansky
1955 LocalNanojanskyErr
1956 LocalDipoleMeanFlux
1957 LocalDipoleMeanFluxErr
1958 LocalDipoleDiffFluxErr
1959 """
1960
1961 @property
1962 def columns(self):
1963 return [self.instFluxPosCol,
1965 self.photoCalibCol]
1966
1967 @property
1968 def name(self):
1969 return f'dipDiffFlux_{self.instFluxPosCol}_{self.instFluxNegCol}'
1970
1971 def _func(self, df):
1972 return (np.fabs(self.instFluxToNanojansky(df[self.instFluxPosCol], df[self.photoCalibCol]))
1973 - np.fabs(self.instFluxToNanojansky(df[self.instFluxNegCol], df[self.photoCalibCol])))
1974
1975
1977 """Compute the error on the absolute difference of dipole fluxes.
1978
1979 See Also
1980 --------
1981 LocalNanojansky
1982 LocalNanojanskyErr
1983 LocalDipoleMeanFlux
1984 LocalDipoleMeanFluxErr
1985 LocalDipoleDiffFlux
1986 """
1987
1988 @property
1996 @property
1997 def name(self):
1998 return f'dipDiffFluxErr_{self.instFluxPosCol}_{self.instFluxNegCol}'
1999
2000 def _func(self, df):
2001 return np.hypot(df[self.instFluxPosErrCol], df[self.instFluxNegErrCol]) * df[self.photoCalibCol]
2002
2003
2005 """Compute E(B-V) from dustmaps.sfd."""
2006 _defaultDataset = 'ref'
2007 name = "E(B-V)"
2008 shortname = "ebv"
2009
2010 def __init__(self, **kwargs):
2011 # Import is only needed for Ebv.
2012 # Suppress unnecessary .dustmapsrc log message on import.
2013 with open(os.devnull, "w") as devnull:
2014 with redirect_stdout(devnull):
2015 from dustmaps.sfd import SFDQuery
2016 self._columns = ['coord_ra', 'coord_dec']
2017 self.sfd = SFDQuery()
2018 super().__init__(**kwargs)
2019
2020 def _func(self, df):
2021 coords = SkyCoord(df['coord_ra'].values * u.rad, df['coord_dec'].values * u.rad)
2022 ebv = self.sfd(coords)
2023 return pd.Series(ebv, index=df.index).astype('float32')
2024
2025
2027 """Base class for functors that use shape moments and localWCS
2028
2029 Attributes
2030 ----------
2031 is_covariance : bool
2032 Whether the shape columns are terms of a covariance matrix. If False,
2033 they will be assumed to be terms of a correlation matrix instead.
2034 """
2035
2036 is_covariance: bool = True
2037
2038 def __init__(self,
2039 shape_1_1,
2040 shape_2_2,
2041 shape_1_2,
2042 colCD_1_1,
2043 colCD_1_2,
2044 colCD_2_1,
2045 colCD_2_2,
2046 **kwargs):
2047 self.shape_1_1 = shape_1_1
2048 self.shape_2_2 = shape_2_2
2049 self.shape_1_2 = shape_1_2
2050 self.colCD_1_1 = colCD_1_1
2051 self.colCD_1_2 = colCD_1_2
2052 self.colCD_2_1 = colCD_2_1
2053 self.colCD_2_2 = colCD_2_2
2054 super().__init__(**kwargs)
2055
2056 @property
2057 def columns(self):
2058 return [
2059 self.shape_1_1,
2060 self.shape_2_2,
2061 self.shape_1_2,
2062 ] + self.columns_ref
2063
2064 @property
2065 def columns_ref(self):
2066 """Return columns that are needed from the ref table."""
2067 return [
2068 self.colCD_1_1,
2069 self.colCD_1_2,
2070 self.colCD_2_1,
2071 self.colCD_2_2]
2072
2073 def compute_ellipse_terms(self, df, sky: bool = True):
2074 r"""Return terms commonly used for ellipse parameterization conversions.
2075
2076 Parameters
2077 ----------
2078 df
2079 The data frame.
2080 sky
2081 Whether to compute the terms in sky coordinates.
2082 If False, XX, YY and XY moments are used instead of
2083 UU, VV and UV.
2084
2085 Returns
2086 -------
2087 xx_p_yy
2088 The sum of the diagonal terms of the covariance.
2089 xx_m_yy
2090 The difference of the diagonal terms of the covariance.
2091 t2
2092 A term similar to the discriminant of the quadratic formula.
2093 """
2094 xx = self.sky_uu(df) if sky else self.get_xx(df)
2095 yy = self.sky_vv(df) if sky else self.get_yy(df)
2096 xx_m_yy = xx - yy
2097 t2 = xx_m_yy**2 + 4.0*(self.sky_uv(df) if sky else self.get_xy(df))**2
2098 # TODO: Check alternative form that may be more stable for computing
2099 # the minor axis size (see gauss2d/src/ellipse.cc)
2100 # t2 = xx**2 + yy**2 - 2*(xx*yy - 2*xy**2)
2101 return xx + yy, xx_m_yy, t2
2102
2103 def get_xx(self, df):
2104 xx = df[self.shape_1_1]
2105 return xx if self.is_covariance else xx**2
2106
2107 def get_yy(self, df):
2108 yy = df[self.shape_2_2]
2109 return yy if self.is_covariance else yy**2
2110
2111 def get_xy(self, df):
2112 xy = df[self.shape_1_2]
2113 return xy if self.is_covariance else xy*df[self.shape_1_1]*df[self.shape_2_2]
2114
2115 # Each of sky_uu, sky_vv, sky_uv evaluates one element of
2116 # CD_matrix * moments_matrix * CD_matrix.T
2117 def sky_uu(self, df):
2118 """Return the component of the moments tensor aligned with the RA axis, in radians."""
2119 i_xx = self.get_xx(df)
2120 i_yy = self.get_yy(df)
2121 i_xy = self.get_xy(df)
2122 CD_1_1 = df[self.colCD_1_1]
2123 CD_1_2 = df[self.colCD_1_2]
2124 return (CD_1_1*(i_xx*CD_1_1 + i_xy*CD_1_2)
2125 + CD_1_2*(i_xy*CD_1_1 + i_yy*CD_1_2))
2126
2127 def sky_vv(self, df):
2128 """Return the component of the moments tensor aligned with the dec axis, in radians."""
2129 i_xx = self.get_xx(df)
2130 i_yy = self.get_yy(df)
2131 i_xy = self.get_xy(df)
2132 CD_2_1 = df[self.colCD_2_1]
2133 CD_2_2 = df[self.colCD_2_2]
2134 return (CD_2_1*(i_xx*CD_2_1 + i_xy*CD_2_2)
2135 + CD_2_2*(i_xy*CD_2_1 + i_yy*CD_2_2))
2136
2137 def sky_uv(self, df):
2138 """Return the covariance of the moments tensor in ra, dec coordinates, in radians."""
2139 i_xx = self.get_xx(df)
2140 i_yy = self.get_yy(df)
2141 i_xy = self.get_xy(df)
2142 CD_1_1 = df[self.colCD_1_1]
2143 CD_1_2 = df[self.colCD_1_2]
2144 CD_2_1 = df[self.colCD_2_1]
2145 CD_2_2 = df[self.colCD_2_2]
2146 return ((CD_1_1 * i_xx + CD_1_2 * i_xy) * CD_2_1
2147 + (CD_1_1 * i_xy + CD_1_2 * i_yy) * CD_2_2)
2148
2149 def get_g1(self, df):
2150 """
2151 Calculate shear-type ellipticity parameter G1.
2152 """
2153 # TODO: Replace this with functionality from afwGeom, DM-54015
2154 sky_uu = self.sky_uu(df)
2155 sky_vv = self.sky_vv(df)
2156 sky_uv = self.sky_uv(df)
2157 denom = sky_uu + sky_vv + 2 * np.sqrt(sky_uu*sky_vv - sky_uv**2)
2158 return ((sky_uu - sky_vv) / denom).astype(np.float32)
2159
2160 def get_g2(self, df):
2161 """
2162 Calculate shear-type ellipticity parameter G2.
2163
2164 This has the opposite sign as sky_uv in order to maintain consistency with the HSM moments
2165 sign convention.
2166 """
2167 # TODO: Replace this with functionality from afwGeom, DM-54015
2168 sky_uu = self.sky_uu(df)
2169 sky_vv = self.sky_vv(df)
2170 sky_uv = self.sky_uv(df)
2171 denom = sky_uu + sky_vv + 2 * np.sqrt(sky_uu*sky_vv - sky_uv**2)
2172 return (-2*sky_uv / denom).astype(np.float32)
2173
2174 def get_trace(self, df):
2175 sky_uu = self.sky_uu(df)
2176 sky_vv = self.sky_vv(df)
2177 return np.sqrt(0.5*(sky_uu + sky_vv)).astype(np.float32)
2178
2179
2181 """Rotate pixel moments Ixx,Iyy,Ixy into RA/dec frame and G1/G2 reduced
2182 shear parameterization"""
2183 _defaultDataset = 'meas'
2184 name = "moments_g1"
2185 shortname = "moments_g1"
2186
2187 def _func(self, df):
2188 sky_g1 = self.get_g1(df)
2189
2190 return pd.Series(sky_g1.astype(np.float32), index=df.index)
2191
2192
2194 """Rotate pixel moments Ixx,Iyy,Ixy into RA/dec frame and G1/G2 reduced
2195 shear parameterization"""
2196 _defaultDataset = 'meas'
2197 name = "moments_g2"
2198 shortname = "moments_g2"
2199
2200 def _func(self, df):
2201 sky_g2 = self.get_g2(df)
2202
2203 return pd.Series(sky_g2.astype(np.float32), index=df.index)
2204
2205
2207 """Trace radius size in arcseconds from pixel moments Ixx,Iyy,Ixy
2208
2209 The trace radius size is a measure of size equal to the square root of
2210 half of the trace of the second moments tensor.
2211 """
2212 _defaultDataset = 'meas'
2213 name = "moments_trace"
2214 shortname = "moments_trace"
2215
2216 def _func(self, df):
2217 sky_trace_radians = self.get_trace(df)
2218
2219 return pd.Series((sky_trace_radians*(180/np.pi)*3600).astype(np.float32), index=df.index)
2220
2221
2223 """Rotate pixel moments Ixx,Iyy,Ixy into ra,dec frame and arcseconds"""
2224 _defaultDataset = 'meas'
2225 name = "moments_uu"
2226 shortname = "moments_uu"
2227
2228 def _func(self, df):
2229 sky_uu_radians = self.sky_uu(df)
2230
2231 return pd.Series((sky_uu_radians*((180/np.pi)*3600)**2).astype(np.float32), index=df.index)
2232
2233
2235 """MomentsIuuSky but from sigma_x, sigma_y, rho correlation terms."""
2236 is_covariance = False
2237
2238
2240 """Rotate pixel moments Ixx,Iyy,Ixy into ra,dec frame and arcseconds"""
2241 _defaultDataset = 'meas'
2242 name = "moments_vv"
2243 shortname = "moments_vv"
2244
2245 def _func(self, df):
2246 sky_vv_radians = self.sky_vv(df)
2247
2248 return pd.Series((sky_vv_radians*((180/np.pi)*3600)**2).astype(np.float32), index=df.index)
2249
2250
2252 """MomentsIvvSky but from sigma_x, sigma_y, rho correlation terms."""
2253 is_covariance = False
2254
2255
2257 """Rotate pixel moments Ixx,Iyy,Ixy into ra,dec frame and arcseconds"""
2258 _defaultDataset = 'meas'
2259 name = "moments_uv"
2260 shortname = "moments_uv"
2261
2262 def _func(self, df):
2263 sky_uv_radians = self.sky_uv(df)
2264
2265 return pd.Series((sky_uv_radians*((180/np.pi)*3600)**2).astype(np.float32), index=df.index)
2266
2267
2269 """MomentsIuvSky but from sigma_x, sigma_y, rho correlation terms."""
2270 is_covariance = False
2271
2272
2274 """Compute position angle relative to ra,dec frame, in degrees, from Ixx,Iyy,Ixy pixel moments."""
2275 _defaultDataset = 'meas'
2276 name = "moments_theta"
2277 shortname = "moments_theta"
2278
2279 def _func(self, df):
2280 sky_uu = self.sky_uu(df)
2281 sky_vv = self.sky_vv(df)
2282 sky_uv = self.sky_uv(df)
2283 theta = 0.5*np.arctan2(2*sky_uv, sky_uu - sky_vv)
2284
2285 return pd.Series((np.degrees(np.array(theta))).astype(np.float32), index=df.index)
2286
2287
2289 """PositionAngleFromMoments but from sigma_x, sigma_y, rho correlation terms."""
2290 is_covariance = False
2291
2292
2294 """Compute the semimajor axis length in arcseconds, from Ixx,Iyy,Ixy pixel moments."""
2295 _defaultDataset = 'meas'
2296 name = "moments_a"
2297 shortname = "moments_a"
2298
2299 def _func(self, df):
2300 xx_p_yy, _, t2 = self.compute_ellipse_terms(df)
2301 # This copies what is done (unvectorized) in afw.geom.ellipse
2302 a_radians = np.sqrt(0.5 * (xx_p_yy + np.sqrt(t2)))
2303
2304 return pd.Series((np.degrees(a_radians)*3600).astype(np.float32), index=df.index)
2305
2306
2308 """SemimajorAxisFromMoments but from sigma_x, sigma_y, rho correlation terms."""
2309 is_covariance = False
2310
2311
2313 """Compute the semiminor axis length in arcseconds, from Ixx,Iyy,Ixy pixel moments."""
2314 _defaultDataset = 'meas'
2315 name = "moments_b"
2316 shortname = "moments_b"
2317
2318 def _func(self, df):
2319 xx_p_yy, _, t2 = self.compute_ellipse_terms(df)
2320 # This copies what is done (unvectorized) in afw.geom.ellipse
2321 b_radians = np.sqrt(0.5 * (xx_p_yy - np.sqrt(t2)))
2322
2323 return pd.Series((np.degrees(b_radians)*3600).astype(np.float32), index=df.index)
2324
2325
2327 """SemiminorAxisFromMoments but from sigma_x, sigma_y, rho correlation terms."""
2328 is_covariance = False
__init__(self, col, filt2, filt1, **kwargs)
Definition functors.py:973
multilevelColumns(self, parq, **kwargs)
Definition functors.py:1002
__init__(self, col, **kwargs)
Definition functors.py:656
multilevelColumns(self, data, **kwargs)
Definition functors.py:459
from_file(cls, filename, **kwargs)
Definition functors.py:549
from_yaml(cls, translationDefinition, **kwargs)
Definition functors.py:558
pixelScaleArcseconds(self, cd11, cd12, cd21, cd22)
Definition functors.py:1420
__init__(self, theta_col, colCD_1_1, colCD_1_2, colCD_2_1, colCD_2_2, **kwargs)
Definition functors.py:1543
__init__(self, col, colCD_1_1, colCD_1_2, colCD_2_1, colCD_2_2, **kwargs)
Definition functors.py:1497
__init__(self, col, colCD_1_1, colCD_1_2, colCD_2_1, colCD_2_2, **kwargs)
Definition functors.py:1459
__init__(self, col, **kwargs)
Definition functors.py:687
__call__(self, catalog, **kwargs)
Definition functors.py:717
__init__(self, colXX, colXY, colYY, **kwargs)
Definition functors.py:1164
__init__(self, colXX, colXY, colYY, **kwargs)
Definition functors.py:1192
_func(self, df, dropna=True)
Definition functors.py:298
multilevelColumns(self, data, columnIndex=None, returnTuple=False)
Definition functors.py:244
__call__(self, data, dropna=False)
Definition functors.py:355
_get_data_columnLevels(self, data, columnIndex=None)
Definition functors.py:191
_colsFromDict(self, colDict, columnIndex=None)
Definition functors.py:225
difference(self, data1, data2, **kwargs)
Definition functors.py:367
_get_data_columnLevelNames(self, data, columnIndex=None)
Definition functors.py:211
__init__(self, filt=None, dataset=None, noDup=None)
Definition functors.py:170
__init__(self, ra, dec, **kwargs)
Definition functors.py:801
__init__(self, instFluxPosCol, instFluxNegCol, instFluxPosErrCol, instFluxNegErrCol, photoCalibCol, photoCalibErrCol=None, **kwargs)
Definition functors.py:1892
instFluxToNanojansky(self, instFlux, localCalib)
Definition functors.py:1745
instFluxErrToMagnitudeErr(self, instFlux, instFluxErr, localCalib, localCalibErr=None)
Definition functors.py:1807
instFluxToMagnitude(self, instFlux, localCalib)
Definition functors.py:1790
__init__(self, instFluxCol, instFluxErrCol, photoCalibCol, photoCalibErrCol=None, **kwargs)
Definition functors.py:1735
instFluxErrToNanojanskyErr(self, instFlux, instFluxErr, localCalib, localCalibErr=None)
Definition functors.py:1762
computeSkySeparation(self, ra1, dec1, ra2, dec2)
Definition functors.py:1279
__init__(self, colCD_1_1, colCD_1_2, colCD_2_1, colCD_2_2, **kwargs)
Definition functors.py:1241
computeDeltaRaDec(self, x, y, cd11, cd12, cd21, cd22)
Definition functors.py:1248
getSkySeparationFromPixel(self, x1, y1, x2, y2, cd11, cd12, cd21, cd22)
Definition functors.py:1305
computePositionAngle(self, ra1, dec1, ra2, dec2)
Definition functors.py:1337
getPositionAngleFromDetectorAngle(self, theta, cd11, cd12, cd21, cd22)
Definition functors.py:1378
__init__(self, col1, col2, **kwargs)
Definition functors.py:921
__init__(self, *args, **kwargs)
Definition functors.py:893
__init__(self, col, **kwargs)
Definition functors.py:862
__init__(self, shape_1_1, shape_2_2, shape_1_2, colCD_1_1, colCD_1_2, colCD_2_1, colCD_2_2, **kwargs)
Definition functors.py:2046
compute_ellipse_terms(self, df, bool sky=True)
Definition functors.py:2073
__init__(self, col, band_to_check, **kwargs)
Definition functors.py:765
__init__(self, colFlux, colFluxErr=None, **kwargs)
Definition functors.py:1639
dn2MagErr(self, dn, dnErr, fluxMag0, fluxMag0Err)
Definition functors.py:1684
dn2fluxErr(self, dn, dnErr, fluxMag0, fluxMag0Err)
Definition functors.py:1678
__call__(self, catalog, **kwargs)
Definition functors.py:705
__init__(self, colXX, colXY, colYY, **kwargs)
Definition functors.py:1218
pd.Series _func(self, pd.DataFrame df)
Definition functors.py:1612
__init__(self, tuple[str]|list[str]|None bands=None, **kwargs)
Definition functors.py:1625
init_fromDict(initDict, basePath='lsst.pipe.tasks.functors', typeKey='functor', name=None)
Definition functors.py:67
mag_aware_eval(df, expr, log)
Definition functors.py:587