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