Skip to content

Commit 62f1e4b

Browse files
committed
Make trailedAssociatorTask
Make trailedAssociatorTask which filters out trails whose lengths are above 0.416 arcseconds/second in length.
1 parent e97dcde commit 62f1e4b

File tree

6 files changed

+215
-16
lines changed

6 files changed

+215
-16
lines changed

python/lsst/ap/association/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# along with this program. If not, see <https://www.gnu.org/licenses/>.
2121

2222
from .version import *
23+
from .trailedSourceFilter import *
2324
from .association import *
2425
from .diaForcedSource import *
2526
from .loadDiaCatalogs import *

python/lsst/ap/association/association.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import lsst.pex.config as pexConfig
3333
import lsst.pipe.base as pipeBase
3434
from lsst.utils.timer import timeMethod
35+
from .trailedSourceFilter import TrailedSourceFilterTask
3536

3637
# Enforce an error for unsafe column/array value setting in pandas.
3738
pd.options.mode.chained_assignment = 'raise'
@@ -47,10 +48,22 @@ class AssociationConfig(pexConfig.Config):
4748
default=1.0,
4849
)
4950

51+
trailedSourceFilter = pexConfig.ConfigurableField(
52+
target=TrailedSourceFilterTask,
53+
doc="Docs here"
54+
55+
)
56+
57+
doLongTrailFilter = pexConfig.Field(
58+
doc="Filter artifact candidates based on morphological criteria, i.g. those that appear to "
59+
"be streaks.",
60+
dtype=bool,
61+
default=True
62+
)
63+
5064

5165
class AssociationTask(pipeBase.Task):
5266
"""Associate DIAOSources into existing DIAObjects.
53-
5467
This task performs the association of detected DIASources in a visit
5568
with the previous DIAObjects detected over time. It also creates new
5669
DIAObjects out of DIASources that cannot be associated with previously
@@ -60,10 +73,16 @@ class AssociationTask(pipeBase.Task):
6073
ConfigClass = AssociationConfig
6174
_DefaultName = "association"
6275

76+
def __init__(self, *args, **kwargs):
77+
super().__init__(*args, **kwargs)
78+
if self.config.doLongTrailFilter:
79+
self.makeSubtask("trailedSourceFilter")
80+
6381
@timeMethod
6482
def run(self,
6583
diaSources,
66-
diaObjects):
84+
diaObjects,
85+
exposure):
6786
"""Associate the new DiaSources with existing DiaObjects.
6887
6988
Parameters
@@ -72,6 +91,9 @@ def run(self,
7291
New DIASources to be associated with existing DIAObjects.
7392
diaObjects : `pandas.DataFrame`
7493
Existing diaObjects from the Apdb.
94+
exposure : `pandas.DataFrame`
95+
Calibrated exposure differenced with a template image during
96+
image differencing.
7597
7698
Returns
7799
-------
@@ -98,7 +120,18 @@ def run(self,
98120
nUpdatedDiaObjects=0,
99121
nUnassociatedDiaObjects=0)
100122

101-
matchResult = self.associate_sources(diaObjects, diaSources)
123+
if self.config.doLongTrailFilter:
124+
diaTrailedResult = self.trailedSourceFilter.run(diaSources, exposure)
125+
126+
if len(diaTrailedResult.trailedDiaSources) > 0:
127+
print("Trailed sources cleaned.")
128+
else:
129+
print("No trailed sources to clean.")
130+
131+
matchResult = self.associate_sources(diaObjects, diaTrailedResult.diaSources)
132+
133+
else:
134+
matchResult = self.associate_sources(diaObjects, diaSources)
102135

103136
mask = matchResult.diaSources["diaObjectId"] != 0
104137

python/lsst/ap/association/diaPipe.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
Currently loads directly from the Apdb rather than pre-loading.
2929
"""
3030

31+
__all__ = ("DiaPipelineConfig",
32+
"DiaPipelineTask",
33+
"DiaPipelineConnections")
34+
3135
import pandas as pd
3236

3337
import lsst.dax.apdb as daxApdb
@@ -41,13 +45,10 @@
4145
AssociationTask,
4246
DiaForcedSourceTask,
4347
LoadDiaCatalogsTask,
44-
PackageAlertsTask)
48+
PackageAlertsTask,
49+
TrailedSourceFilterTask)
4550
from lsst.ap.association.ssoAssociation import SolarSystemAssociationTask
4651

47-
__all__ = ("DiaPipelineConfig",
48-
"DiaPipelineTask",
49-
"DiaPipelineConnections")
50-
5152

5253
class DiaPipelineConnections(
5354
pipeBase.PipelineTaskConnections,
@@ -221,6 +222,10 @@ class DiaPipelineConfig(pipeBase.PipelineTaskConfig,
221222
target=AssociationTask,
222223
doc="Task used to associate DiaSources with DiaObjects.",
223224
)
225+
trailedFilter = pexConfig.ConfigurableField(
226+
target=TrailedSourceFilterTask,
227+
doc="Task used to find trailed DiaSources.",
228+
)
224229
doSolarSystemAssociation = pexConfig.Field(
225230
dtype=bool,
226231
default=False,
@@ -368,7 +373,7 @@ def run(self,
368373

369374
# Associate new DiaSources with existing DiaObjects.
370375
assocResults = self.associator.run(diaSourceTable,
371-
loaderResult.diaObjects)
376+
loaderResult.diaObjects, exposure)
372377
if self.config.doSolarSystemAssociation:
373378
ssoAssocResult = self.solarSystemAssociator.run(
374379
assocResults.unAssocDiaSources,
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# This file is part of ap_association.
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+
"""A simple implementation of source association task for ap_verify.
23+
"""
24+
25+
__all__ = ["TrailedSourceFilterTask", "TrailedSourceFilterConfig"]
26+
27+
import lsst.pex.config as pexConfig
28+
import lsst.pipe.base as pipeBase
29+
from lsst.utils.timer import timeMethod
30+
31+
# import numpy as np
32+
import pandas as pd
33+
34+
# Enforce an error for unsafe column/array value setting in pandas.
35+
pd.options.mode.chained_assignment = 'raise'
36+
37+
38+
class TrailedSourceFilterConfig(pexConfig.Config):
39+
"""Config class for TrailedSourceFilterTask.
40+
"""
41+
maxTrailLength = pexConfig.Field(
42+
dtype=float,
43+
doc='Maximum trail length permitted is less than 10 degrees/day. This is a rate '
44+
'of 0.416 arcseconds per second.As trail length is measured in '
45+
'arcseconds, it is dependant on the length of the exposure.',
46+
default=0.416, # HSC Default. Should Decam and LSST defaults be passed? Does it change possibly?
47+
)
48+
49+
50+
class TrailedSourceFilterTask(pipeBase.Task):
51+
"""Find trailed sources in DIAObjects.
52+
53+
"""
54+
55+
ConfigClass = TrailedSourceFilterConfig
56+
_DefaultName = "trailedAssociation"
57+
58+
@timeMethod
59+
def run(self,
60+
dia_sources, exposure):
61+
"""Find trailed sources which have not been filtered out and will
62+
not be included in the diaSource catalog.
63+
64+
Parameters
65+
----------
66+
dia_sources : `pandas.DataFrame`
67+
New DIASources to be checked for trailed sources.
68+
exposure : `pandas.DataFrame`
69+
Calibrated exposure differenced with a template image during
70+
image differencing.
71+
72+
Returns
73+
-------
74+
result : `lsst.pipe.base.Struct`
75+
Results struct with components.
76+
77+
- ``"dia_sources"`` : DiaSource table that is free from unwanted
78+
trailed sources (`pandas.DataFrame`)
79+
80+
- ``"trailed_dia_sources"`` : DiaSources that have trailed more than
81+
0.416 arcseconds/second*exposure_time(`pandas.DataFrame`)
82+
"""
83+
trail_mask = self.check_dia_source_trail(dia_sources, exposure)
84+
85+
return pipeBase.Struct(
86+
diaSources=dia_sources[~trail_mask].reset_index(drop=True),
87+
trailedDiaSources=dia_sources[trail_mask].reset_index(drop=True))
88+
89+
def check_dia_source_trail(self, dia_sources, exposure):
90+
"""Check that all DiaSources have trails.
91+
92+
Creates a mask for sources with lengths greater than 0.416 arcseconds/second*exposure time.
93+
94+
Parameters
95+
----------
96+
dia_sources : `pandas.DataFrame`
97+
Input DiaSources to check for trail lengths.
98+
exposure : `pandas.DataFrame`
99+
Calibrated exposure differenced with a template image during
100+
image differencing.
101+
102+
Returns
103+
-------
104+
trail_mask : `pandas.DataFrame`
105+
Boolean mask for dia_sources which are greater than the cuttoff length.
106+
"""
107+
exposure_time = exposure.getInfo().getVisitInfo().getExposureTime()
108+
trail_mask = (dia_sources.loc[:, "trailLength"].values[:] >= self.config.maxTrailLength*exposure_time)
109+
110+
return trail_mask

tests/test_association_task.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import numpy as np
2323
import pandas as pd
2424
import unittest
25+
import lsst.afw.image as afwImage
26+
from lsst.afw.coord import Weather
2527

2628
import lsst.geom as geom
2729
import lsst.utils.tests
@@ -46,20 +48,38 @@ def setUp(self):
4648
self.diaSources = pd.DataFrame(data=[
4749
{"ra": 0.04*idx + scatter*rng.uniform(-1, 1),
4850
"dec": 0.04*idx + scatter*rng.uniform(-1, 1),
49-
"diaSourceId": idx + 1 + self.nObjects, "diaObjectId": 0}
51+
"diaSourceId": idx + 1 + self.nObjects, "diaObjectId": 0, "trailLength": 5.5*idx}
5052
for idx in range(self.nSources)])
5153
self.diaSourceZeroScatter = pd.DataFrame(data=[
5254
{"ra": 0.04*idx,
5355
"dec": 0.04*idx,
54-
"diaSourceId": idx + 1 + self.nObjects, "diaObjectId": 0}
56+
"diaSourceId": idx + 1 + self.nObjects, "diaObjectId": 0, "trailLength": 5.5*idx}
5557
for idx in range(self.nSources)])
58+
exposureId = 5
59+
exposureTime = 30
60+
boresightRotAngle = 45.6 * lsst.geom.degrees
61+
weather = Weather(1.1, 2.2, 0.3)
62+
visitInfo = afwImage.VisitInfo(
63+
exposureId=exposureId,
64+
exposureTime=exposureTime,
65+
boresightRotAngle=boresightRotAngle,
66+
weather=weather,
67+
)
68+
exposureInfo = afwImage.ExposureInfo()
69+
exposureInfo.setVisitInfo(visitInfo)
70+
maskedImage = afwImage.MaskedImageF(lsst.geom.Extent2I(64, 64))
71+
self.exposure = afwImage.ExposureF(maskedImage, exposureInfo)
5672

5773
def test_run(self):
5874
"""Test the full task by associating a set of diaSources to
5975
existing diaObjects.
60-
"""
61-
assocTask = AssociationTask()
62-
results = assocTask.run(self.diaSources, self.diaObjects)
76+
# """
77+
78+
config = AssociationTask.ConfigClass()
79+
config.doLongTrailFilter = False
80+
assocTask = AssociationTask(config=config)
81+
82+
results = assocTask.run(self.diaSources, self.diaObjects, self.exposure)
6383

6484
self.assertEqual(results.nUpdatedDiaObjects, len(self.diaObjects) - 1)
6585
self.assertEqual(results.nUnassociatedDiaObjects, 1)
@@ -75,13 +95,40 @@ def test_run(self):
7595
[0]):
7696
self.assertEqual(test_obj_id, expected_obj_id)
7797

98+
def test_run_trailed_sources(self):
99+
"""Test the full task by associating a set of diaSources to
100+
existing diaObjects when trailed sources are filtered
101+
# """
102+
103+
config = AssociationTask.ConfigClass()
104+
config.doLongTrailFilter = True
105+
assocTask = AssociationTask(config=config)
106+
107+
results = assocTask.run(self.diaSources, self.diaObjects, self.exposure)
108+
109+
print(results.nUpdatedDiaObjects, "results nupdated")
110+
print(len(self.diaObjects) - 3, " length of dia objects")
111+
self.assertEqual(results.nUpdatedDiaObjects, len(self.diaObjects) - 3)
112+
self.assertEqual(results.nUnassociatedDiaObjects, 3)
113+
self.assertEqual(len(results.matchedDiaSources),
114+
len(self.diaObjects) - 3)
115+
self.assertEqual(len(results.unAssocDiaSources), 1)
116+
for test_obj_id, expected_obj_id in zip(
117+
results.matchedDiaSources["diaObjectId"].to_numpy(),
118+
[1, 2, 3, 4]):
119+
self.assertEqual(test_obj_id, expected_obj_id)
120+
for test_obj_id, expected_obj_id in zip(
121+
results.unAssocDiaSources["diaObjectId"].to_numpy(),
122+
[0]):
123+
self.assertEqual(test_obj_id, expected_obj_id)
124+
78125
def test_run_no_existing_objects(self):
79126
"""Test the run method with a completely empty database.
80127
"""
81128
assocTask = AssociationTask()
82129
results = assocTask.run(
83130
self.diaSources,
84-
pd.DataFrame(columns=["ra", "dec", "diaObjectId"]))
131+
pd.DataFrame(columns=["ra", "dec", "diaObjectId", "trailLength"]), self.exposure)
85132
self.assertEqual(results.nUpdatedDiaObjects, 0)
86133
self.assertEqual(results.nUnassociatedDiaObjects, 0)
87134
self.assertEqual(len(results.matchedDiaSources), 0)
@@ -100,6 +147,9 @@ def test_associate_sources(self):
100147
[0, 1, 2, 3, 4]):
101148
self.assertEqual(test_obj_id, expected_obj_id)
102149

150+
def test_trailed_source(self):
151+
"""Test the removal of trailed sources"""
152+
103153
def test_score_and_match(self):
104154
"""Test association between a set of sources and an existing
105155
DIAObjectCollection.

tests/test_diaPipe.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def solarSystemAssociator_run(self, unAssocDiaSources, solarSystemObjectTable, d
121121
unAssocDiaSources=MagicMock(spec=pd.DataFrame()))
122122

123123
@lsst.utils.timer.timeMethod
124-
def associator_run(self, table, diaObjects):
124+
def associator_run(self, table, diaObjects, exposure):
125125
return lsst.pipe.base.Struct(nUpdatedDiaObjects=2, nUnassociatedDiaObjects=3,
126126
matchedDiaSources=MagicMock(spec=pd.DataFrame()),
127127
unAssocDiaSources=MagicMock(spec=pd.DataFrame()))

0 commit comments

Comments
 (0)