Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-27388: Implement metric system for fakes in AP #65

Merged
merged 6 commits into from
Dec 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
141 changes: 141 additions & 0 deletions python/lsst/ap/pipe/metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# This file is part of ap_pipe.
#
# Developed for the LSST Data Management System.
# This product includes software developed by the LSST Project
# (http://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 <http://www.gnu.org/licenses/>.
#

"""Metrics for ap_pipe tasks.
"""

__all__ = [
"ApFakesCompletenessMetricTask", "ApFakesCompletenessMetricConfig",
]

import astropy.units as u
import numpy as np
import traceback

import lsst.pex.config as pexConfig
from lsst.pipe.base import Struct
import lsst.pipe.base.connectionTypes as connTypes
from lsst.pipe.tasks.insertFakes import InsertFakesConfig
from lsst.verify import Measurement
from lsst.verify.tasks import MetricTask, MetricComputationError


class ApFakesCompletenessMetricConnections(
MetricTask.ConfigClass.ConnectionsClass,
dimensions={"instrument", "visit", "detector", "band"},
Copy link
Member

Choose a reason for hiding this comment

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

I think the band is redundant here (it's implied by visit). Do you need to declare it to get the data ID extraction in runQuantum to work?

defaultTemplates={"coaddName": "deep",
"fakesType": "",
"package": "ap_pipe",
"metric": "apFakesCompleteness"}):
Comment on lines +47 to +48
Copy link
Member

Choose a reason for hiding this comment

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

Re: names, there should be a corresponding metric defined in the verify_metrics package (they cannot have periods, because they're namespaced as ap_pipe.apFakesCompleteness). I think creating a Measurement for a nonexistent metric is supposed to fail, but that's always been flaky.

Copy link
Member

@kfindeisen kfindeisen Dec 8, 2020

Choose a reason for hiding this comment

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

Oh, now I see what you're trying to do. The current system really doesn't support parametrized metrics, unless you have a standardized set of values (e.g., apFakesCompleteness20t25, apFakesCompleteness25t30). Support for tweaking numbers was sort-of discussed on #dm-squash in a different context, but then nothing got acted on, and I'm not sure the proposed solution would work in Gen 3.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not quite getting where to set these names (This is in addition to your comment later about connections). Should I just remove the automated naming in this code and just rely on doing everything in the pipeline config? If so, what all do I have to set?

Copy link
Member

Choose a reason for hiding this comment

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

I meant here that you'd have to add the metrics (the final names, with magnitudes) to https://github.com/lsst/verify_metrics/blob/master/metrics/ap_pipe.yaml if you want them to work everywhere (I think SQuaSH only uploads metrics defined in those files). You can use << notation (see pipe_tasks.yaml for examples) to cut down on the boilerplate.

Doing everything in the pipeline config would work; I think you'd just have to set connections.metric plus the magnitude values for each case.

Copy link
Member

@kfindeisen kfindeisen Dec 9, 2020

Choose a reason for hiding this comment

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

So for the metric definitions in verify_metrics, something like:

apFakesCompleteness20t25: &apFakesCompleteness
    description: ...
    unit: ""
    reference:
        url: https://confluence.lsstcorp.org/pages/viewpage.action?spaceKey=~ebellm&title=Alert+Production+Metrics
    tags:
        - ap_verify
        - assoc
        - CCD

apFakesCompleteness25t30:
    <<: *apFakesCompleteness
    description: ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I basically have another ticket for this set of work: DM-26593. This one is mostly just to make the metric and get it tested so I'll add adding this to that ticket.

"""ApFakesCompleteness connections.
"""
matchedFakes = connTypes.Input(
doc="Fakes matched to their detections in the difference image.",
name="{fakesType}{coaddName}Diff_matchDiaSrc",
storageClass="DataFrame",
dimensions=("instrument", "visit", "detector"),
)


# Inherits from InsertFakesConfig to preserve column names in the fakes
# catalog.
class ApFakesCompletenessMetricConfig(
MetricTask.ConfigClass,
InsertFakesConfig,
Copy link
Member

Choose a reason for hiding this comment

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

Might be worth a comment why you need to inherit from InsertFakesConfig (I assume it's to grab column names?).

pipelineConnections=ApFakesCompletenessMetricConnections):
"""ApFakesCompleteness config.
"""
magMin = pexConfig.RangeField(
doc="Minimum of cut on magnitude range used to compute completeness "
"in.",
dtype=float,
default=20,
min=1,
max=40,
)
magMax = pexConfig.RangeField(
doc="Maximum of cut on magnitude range used to compute completeness "
"in.",
dtype=int,
default=30,
min=1,
max=40,
)


class ApFakesCompletenessMetricTask(MetricTask):
kfindeisen marked this conversation as resolved.
Show resolved Hide resolved
"""Metric task for summarizing the completeness of fakes inserted into the
AP pipeline.
"""
_DefaultName = "apFakesCompleteness"
ConfigClass = ApFakesCompletenessMetricConfig

def runQuantum(self, butlerQC, inputRefs, outputRefs):
try:
inputs = butlerQC.get(inputRefs)
inputs["band"] = butlerQC.quantum.dataId["band"]
outputs = self.run(**inputs)
if outputs.measurement is not None:
butlerQC.put(outputs, outputRefs)
else:
self.log.debugf("Skipping measurement of {!r} on {} "
"as not applicable.", self, inputRefs)
except MetricComputationError:
# Apparently lsst.log doesn't have built-in exception support?
self.log.errorf(
"Measurement of {!r} failed on {}->{}\n{}",
self, inputRefs, outputRefs, traceback.format_exc())

def run(self, matchedFakes, band):
"""Compute the completeness of recovered fakes within a magnitude
range.

Parameters
----------
matchedFakes : `lsst.afw.table.SourceCatalog` or `None`
Catalog of fakes that were inserted into the ccdExposure matched
to their detected counterparts.

Returns
-------
result : `lsst.pipe.base.Struct`
A `~lsst.pipe.base.Struct` containing the following component:
``measurement``
the ratio (`lsst.verify.Measurement` or `None`)
"""
if matchedFakes is not None:
magnitudes = matchedFakes[f"{self.config.magVar}" % band]
magCutFakes = matchedFakes[np.logical_and(magnitudes > self.config.magMin,
magnitudes < self.config.magMax)]
if len(magCutFakes) <= 0.0:
raise MetricComputationError(
"No matched fakes catalog sources found; Completeness is "
"ill defined.")
else:
meas = Measurement(
self.config.metricName,
((magCutFakes["diaSourceId"] > 0).sum() / len(magCutFakes))
* u.dimensionless_unscaled)
else:
self.log.info("Nothing to do: no matched catalog found.")
meas = None
return Struct(measurement=meas)
136 changes: 136 additions & 0 deletions tests/test_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# This file is part of ap_pipe.
#
# Developed for the LSST Data Management System.
# This product includes software developed by the LSST Project
# (http://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 <http://www.gnu.org/licenses/>.
#

import astropy.units as u
import numpy as np
import unittest

from lsst.pipe.base import testUtils
import lsst.skymap as skyMap
import lsst.utils.tests
from lsst.verify import Name
from lsst.verify.tasks.testUtils import MetricTaskTestCase

from lsst.ap.pipe.createApFakes import CreateRandomApFakesTask, CreateRandomApFakesConfig
from lsst.ap.pipe.metrics import ApFakesCompletenessMetricTask, ApFakesCompletenessMetricConfig


class TestApCompletenessTask(MetricTaskTestCase):

@classmethod
def makeTask(cls, magMin=20, magMax=30):
"""Make the task and allow for modification of the config min and max.

Parameters
----------
magMin : min magnitude, `float`
Minimum magnitude
magMax : min magnitude, `float`
Maximum magnitude
"""
config = ApFakesCompletenessMetricConfig()
config.magMin = magMin
config.magMax = magMax

return ApFakesCompletenessMetricTask(config=config)

def setUp(self):
super().setUp()

simpleMapConfig = skyMap.discreteSkyMap.DiscreteSkyMapConfig()
simpleMapConfig.raList = [45]
simpleMapConfig.decList = [45]
simpleMapConfig.radiusList = [0.1]

self.simpleMap = skyMap.DiscreteSkyMap(simpleMapConfig)
self.tractId = 0
bCircle = self.simpleMap.generateTract(self.tractId).getInnerSkyPolygon().getBoundingCircle()
self.targetSources = 1000
self.sourceDensity = (self.targetSources
/ (bCircle.getArea() * (180 / np.pi) ** 2))

fakesConfig = CreateRandomApFakesConfig()
fakesConfig.fraction = 0.0
fakesConfig.fakeDensity = self.sourceDensity
fakesTask = CreateRandomApFakesTask(config=fakesConfig)
fakeCat = fakesTask.run(self.tractId, self.simpleMap).fakeCat

self.band = 'g'
self.magCut = 25
magMask = (fakeCat[fakesConfig.magVar % self.band] < self.magCut)
self.expectedAllMatched = magMask.sum()
ids = np.where(magMask, np.arange(1, len(fakeCat) + 1, dtype=int), 0)
# Add columns to mimic the matched fakes result without running the
# full pipeline.
self.fakeCat = fakeCat.assign(diaObjectId=ids,
filterName=["g"] * len(fakeCat),
diaSourceId=ids)
Comment on lines +84 to +86
Copy link
Member

Choose a reason for hiding this comment

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

This is a bit surprising. Shouldn't the catalog returned by fakesTask already have a filter column?

Copy link
Contributor Author

@morriscb morriscb Dec 9, 2020

Choose a reason for hiding this comment

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

The columns here are to mimic the results of matchFakes, not create fakes. The fakes have all magnitudes in all bands created. This is faking columns after processing. Added a comment.


def testValid(self):
"""Test the run method.
"""
result = self.task.run(self.fakeCat, self.band)
testUtils.assertValidOutput(self.task, result)

meas = result.measurement
self.assertEqual(meas.metric_name, Name(metric="ap_pipe.apFakesCompleteness"))
# Work around for Mac failing this test.
self.assertAlmostEqual(
meas.quantity.value,
((self.expectedAllMatched / self.targetSources) * u.dimensionless_unscaled).value,
places=2)

def testMissingData(self):
"""Test the run method with no data.
"""
result = self.task.run(None, None)
testUtils.assertValidOutput(self.task, result)
meas = result.measurement
self.assertIsNone(meas)

def testValidEmpty(self):
"""Test the run method with a valid but zero result.
"""
metricComplete = self.makeTask(self.magCut, self.magCut + 5)
result = metricComplete.run(self.fakeCat, self.band)
testUtils.assertValidOutput(metricComplete, result)

meas = result.measurement
self.assertEqual(meas.metric_name, Name(metric="ap_pipe.apFakesCompleteness"))
self.assertEqual(meas.quantity, 0 * u.dimensionless_unscaled)


# Hack around unittest's hacky test setup system
del MetricTaskTestCase


class MemoryTester(lsst.utils.tests.MemoryTestCase):
pass


def setup_module(module):
lsst.utils.tests.init()


if __name__ == "__main__":
lsst.utils.tests.init()
unittest.main()