Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions python/lsst/analysis/tools/actions/scalar/scalarActions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"FracThreshold",
"MaxAction",
"MinAction",
"FullRangeAction",
"FracInRange",
"FracNan",
"SumAction",
Expand Down Expand Up @@ -284,6 +285,16 @@ def __call__(self, data: KeyedData, **kwargs) -> Scalar:
return nanMin(_dataToArray(data[self.vectorKey.format(**kwargs)])[mask])


class FullRangeAction(ScalarFromVectorAction):
"""Returns the full range (i.e., max-min) of the given data."""

def __call__(self, data: KeyedData, **kwargs) -> Scalar:
mask = self.getMask(**kwargs)
return nanMax(_dataToArray(data[self.vectorKey.format(**kwargs)][mask])) - nanMin(
_dataToArray(data[self.vectorKey.format(**kwargs)][mask])
)


class FracInRange(ScalarFromVectorAction):
"""Compute the fraction of a distribution that is between specified
minimum and maximum values, and is not NaN.
Expand Down
1 change: 1 addition & 0 deletions python/lsst/analysis/tools/atools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .amplifierCorrelation import *
from .astrometricRepeatability import *
from .astrometryMetrics import *
from .calexpMetrics import *
from .calibQuantityProfile import *
from .calibration import *
Expand Down
89 changes: 89 additions & 0 deletions python/lsst/analysis/tools/atools/astrometryMetrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# This file is part of analysis_tools.
#
# Developed for the LSST Data Management System.
# This product includes software developed by the LSST Project
# (https://www.lsst.org).
# See the COPYRIGHT file at the top-level directory of this distribution
# for details of code ownership.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from ..actions.scalar import (
FullRangeAction,
MaxAction,
MeanAction,
MedianAction,
MinAction,
SigmaMadAction,
StdevAction,
)
from ..actions.vector import AngularSeparation, DivideVector
from ..interfaces import AnalysisTool

__all__ = ("AstrometryStatistics",)


class AstrometryStatistics(AnalysisTool):
"""Calculate astrometry metrics from the visit_summary table."""

def setDefaults(self):
super().setDefaults()

self.process.buildActions.cornersep = AngularSeparation(
raKey_A="raCorners_0",
decKey_A="decCorners_0",
raKey_B="raCorners_2",
decKey_B="decCorners_2",
outputUnit="arcminute",
)
self.process.buildActions.ratio = DivideVector()
self.process.buildActions.ratio.actionA = AngularSeparation(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use the already calculated version of this from above?

Copy link
Contributor Author

@jrmullaney jrmullaney Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - I'll sort that out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh - on second thoughts, I don't think I can without introducing a filterActions step, which doesn't seem right for this calculation (it's not a filter).
We can't guarantee what order the buildActions will be done in, so we can't know for certain that cornersep will be built before the DivideVector is attempted.
So I think it's better to leave as-is, but let me know otherwise.

raKey_A="raCorners_0",
decKey_A="decCorners_0",
raKey_B="raCorners_2",
decKey_B="decCorners_2",
outputUnit="arcminute",
)
self.process.buildActions.ratio.actionB = AngularSeparation(
raKey_A="raCorners_1",
decKey_A="decCorners_1",
raKey_B="raCorners_3",
decKey_B="decCorners_3",
outputUnit="arcminute",
)

self.process.calculateActions.minCornerSeparation = MinAction(vectorKey="cornersep")
self.process.calculateActions.maxCornerSeparation = MaxAction(vectorKey="cornersep")
self.process.calculateActions.minCornerSeparationRatio = MinAction(vectorKey="ratio")
self.process.calculateActions.maxCornerSeparationRatio = MaxAction(vectorKey="ratio")
self.process.calculateActions.minPixelScale = MinAction(vectorKey="pixelScale")
self.process.calculateActions.maxPixelScale = MaxAction(vectorKey="pixelScale")
self.process.calculateActions.fullRangePixelScale = FullRangeAction(vectorKey="pixelScale")
self.process.calculateActions.medianPixelScale = MedianAction(vectorKey="pixelScale")
self.process.calculateActions.sigmaMADPixelScale = SigmaMadAction(vectorKey="pixelScale")
self.process.calculateActions.meanPixelScale = MeanAction(vectorKey="pixelScale")
self.process.calculateActions.stdevPixelScale = StdevAction(vectorKey="pixelScale")

self.produce.metric.units = {
"minCornerSeparation": "arcmin",
"maxCornerSeparation": "arcmin",
"minCornerSeparationRatio": "",
"maxCornerSeparationRatio": "",
"minPixelScale": "arcsec",
"maxPixelScale": "arcsec",
"medianPixelScale": "arcsec",
"sigmaMADPixelScale": "arcsec",
"meanPixelScale": "arcsec",
"stdevPixelScale": "arcsec",
}
1 change: 1 addition & 0 deletions python/lsst/analysis/tools/tasks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .diffimTaskDetectorVisitMetricsAnalysis import *
from .diffimTaskDetectorVisitSpatiallySampledAnalysis import *
from .diffMatchedAnalysis import *
from .exposureCatalogAnalysis import *
from .gatherResourceUsage import *
from .injectedObjectAnalysis import *
from .makeMetricTable import *
Expand Down
120 changes: 120 additions & 0 deletions python/lsst/analysis/tools/tasks/exposureCatalogAnalysis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# This file is part of analysis_tools.
#
# Developed for the LSST Data Management System.
# This product includes software developed by the LSST Project
# (https://www.lsst.org).
# See the COPYRIGHT file at the top-level directory of this distribution
# for details of code ownership.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations

__all__ = ("ExposureCatalogAnalysisConfig", "ExposureCatalogAnalysisTask")

import numpy as np
from astropy.table import Column

from lsst.pex.config import Field, ListField
from lsst.pipe.base import connectionTypes as cT
from lsst.skymap import BaseSkyMap

from ..interfaces import AnalysisBaseConfig, AnalysisBaseConnections, AnalysisPipelineTask


class ExposureCatalogAnalysisConnections(
AnalysisBaseConnections,
dimensions=(),
defaultTemplates={"inputName": ""},
):
data = cT.Input(
doc="Exposure Catalog to analyze.",
name="{inputName}",
storageClass="ExposureCatalog",
dimensions=(),
deferLoad=True,
)

skymap = cT.Input(
doc="The skymap that covers the tract that the data is from.",
name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
storageClass="SkyMap",
dimensions=("skymap",),
)

camera = cT.PrerequisiteInput(
doc="Input camera to use for focal plane geometry.",
name="camera",
storageClass="Camera",
dimensions=("instrument",),
isCalibration=True,
)

def __init__(self, *, config=None):
super().__init__(config=config)

self.data = cT.Input(
doc=self.data.doc,
name=self.data.name,
storageClass=self.data.storageClass,
deferLoad=self.data.deferLoad,
dimensions=frozenset(sorted(config.inputTableDimensions)),
multiple=self.data.multiple,
)

self.dimensions.update(frozenset(sorted(config.taskDimensions)))

if not config.loadSkymap:
del self.skymap
if not config.loadCamera:
del self.camera


class ExposureCatalogAnalysisConfig(
AnalysisBaseConfig, pipelineConnections=ExposureCatalogAnalysisConnections
):

inputTableDimensions = ListField[str](doc="Dimensions of the input table.", optional=False)

taskDimensions = ListField[str](doc="Task and output dimensions.", optional=False)

loadSkymap = Field[bool](doc="Whether to load the skymap.", default=False)

loadCamera = Field[bool](doc="Whether to load the camera.", default=False)


class ExposureCatalogAnalysisTask(AnalysisPipelineTask):
"""An ``AnalysisPipelineTask`` that loads an ExposureCatalog passes it
to the atools for analysis.

It will also pass the ``camera`` and ``skyMap`` inputs to the parent run
methods if these are requested by the config parameters.
"""

ConfigClass = ExposureCatalogAnalysisConfig
_DefaultName = "exposureCatalogAnalysisTask"

def loadData(self, handle):
# The parent class loadData does not work for ExposureCatalog.

data = handle.get().asAstropy()
for colname in list(data.columns.keys()):
# An ExposureCatalog may contain elements that are themselves
# arrays. In such cases, the arrays are expanded into multiple
# columns named: <colname>_0, <colname>_1, etc.
if isinstance(data[colname][0], np.ndarray):
for index in np.arange(len(data[colname][0])):
values = [row[index] for row in data[colname]]
data.add_column(Column(name=f"{colname}_{index}", data=values))

return data
Loading