Skip to content

Commit

Permalink
Merge branch 'master' into tickets/DM-29242
Browse files Browse the repository at this point in the history
  • Loading branch information
squisty committed May 21, 2021
2 parents d3c39e0 + 8988d11 commit 38c4b2a
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 216 deletions.
Expand Up @@ -38,4 +38,4 @@ doPause
bool; Pause to inspect the residuals plot? If False, there will be a 4 second delay to allow for inspection of the plot before closing it and moving on.

display
bool; if True display debug information
bool; if True display debug information
23 changes: 19 additions & 4 deletions python/lsst/meas/algorithms/brightStarStamps.py
Expand Up @@ -28,6 +28,7 @@
from operator import ior
from functools import reduce
from typing import Optional
import numpy as np

from lsst.afw.image import MaskedImageF
from lsst.afw import geom as afwGeom
Expand Down Expand Up @@ -124,6 +125,8 @@ def measureAndNormalize(self, annulus, statsControl=afwMath.StatisticsControl(),
# compute annularFlux
annulusStat = afwMath.makeStatistics(annulusImage, statsFlag, statsControl)
self.annularFlux = annulusStat.getValue()
if np.isnan(self.annularFlux):
raise RuntimeError("Annular flux computation failed, likely because no pixels were valid.")
# normalize stamps
self.stamp_im.image.array /= self.annularFlux
return None
Expand Down Expand Up @@ -186,7 +189,7 @@ def __init__(self, starStamps, innerRadius=None, outerRadius=None,
@classmethod
def initAndNormalize(cls, starStamps, innerRadius, outerRadius,
metadata=None, use_mask=True, use_variance=False,
imCenter=None,
imCenter=None, discardNanFluxObjects=True,
statsControl=afwMath.StatisticsControl(),
statsFlag=afwMath.stringToStatisticsProperty("MEAN"),
badMaskPlanes=('BAD', 'SAT', 'NO_DATA')):
Expand Down Expand Up @@ -220,6 +223,9 @@ def initAndNormalize(cls, starStamps, innerRadius, outerRadius,
imCenter : `collections.abc.Sequence`, optional
Center of the object, in pixels. If not provided, the center of the
first stamp's pixel grid will be used.
discardNanFluxObjects : `bool`
Whether objects with NaN annular flux should be discarded.
If False, these objects will not be normalized.
statsControl : `lsst.afw.math.statistics.StatisticsControl`, optional
StatisticsControl to be used when computing flux over all pixels
within the annulus.
Expand Down Expand Up @@ -254,9 +260,18 @@ def initAndNormalize(cls, starStamps, innerRadius, outerRadius,
bss._checkNormalization(True, innerRadius, outerRadius)
bss._innerRadius, bss._outerRadius = innerRadius, outerRadius
# Apply normalization
for stamp in bss._stamps:
stamp.measureAndNormalize(annulus, statsControl=statsControl, statsFlag=statsFlag,
badMaskPlanes=badMaskPlanes)
for j, stamp in enumerate(bss._stamps):
try:
stamp.measureAndNormalize(annulus, statsControl=statsControl, statsFlag=statsFlag,
badMaskPlanes=badMaskPlanes)
except ValueError:
# Optionally keep NaN flux objects, for bookkeeping purposes,
# and to avoid having to re-find and redo the preprocessing
# steps needed before bright stars can be subtracted.
if discardNanFluxObjects:
bss._stamps.pop(j)
else:
stamp.annularFlux = np.nan
bss.normalized = True
return bss

Expand Down
421 changes: 222 additions & 199 deletions python/lsst/meas/algorithms/loadReferenceObjects.py

Large diffs are not rendered by default.

43 changes: 35 additions & 8 deletions python/lsst/meas/algorithms/stamps.py
Expand Up @@ -28,14 +28,17 @@
import abc
from dataclasses import dataclass
import numpy
from typing import Optional

import lsst.afw.image as afwImage
import lsst.afw.fits as afwFits
from lsst.geom import Box2I, Point2I, Extent2I, Angle, degrees, SpherePoint
from lsst.daf.base import PropertyList
from lsst.daf.butler.core.utils import getFullTypeName
from lsst.utils import doImport


def writeFits(filename, stamp_ims, metadata, write_mask, write_variance):
def writeFits(filename, stamp_ims, metadata, type_name, write_mask, write_variance):
"""Write a single FITS file containing all stamps.
Parameters
Expand All @@ -47,6 +50,8 @@ def writeFits(filename, stamp_ims, metadata, write_mask, write_variance):
metadata : `PropertyList`
A collection of key, value metadata pairs to be
written to the primary header
type_name : `str`
Python type name of the StampsBase subclass to use
write_mask : `bool`
Write the mask data to the output file?
write_variance : `bool`
Expand All @@ -55,6 +60,9 @@ def writeFits(filename, stamp_ims, metadata, write_mask, write_variance):
metadata['HAS_MASK'] = write_mask
metadata['HAS_VARIANCE'] = write_variance
metadata['N_STAMPS'] = len(stamp_ims)
metadata['STAMPCLS'] = type_name
# Record version number in case of future code changes
metadata['VERSION'] = 1
# create primary HDU with global metadata
fitsPrimary = afwFits.Fits(filename, "w")
fitsPrimary.createEmpty()
Expand Down Expand Up @@ -196,7 +204,7 @@ class Stamp(AbstractStamp):
must keep track of the coordinate system
"""
stamp_im: afwImage.maskedImage.MaskedImageF
position: SpherePoint
position: Optional[SpherePoint] = SpherePoint(Angle(numpy.nan), Angle(numpy.nan))

@classmethod
def factory(cls, stamp_im, metadata, index):
Expand Down Expand Up @@ -268,7 +276,6 @@ def __init__(self, stamps, metadata=None, use_mask=True, use_variance=True):
self.use_variance = use_variance

@classmethod
@abc.abstractmethod
def readFits(cls, filename):
"""Build an instance of this class from a file.
Expand All @@ -277,10 +284,10 @@ def readFits(cls, filename):
filename : `str`
Name of the file to read
"""
raise NotImplementedError

return cls.readFitsWithOptions(filename, None)

@classmethod
@abc.abstractmethod
def readFitsWithOptions(cls, filename, options):
"""Build an instance of this class with options.
Expand All @@ -291,7 +298,26 @@ def readFitsWithOptions(cls, filename, options):
options : `PropertyList`
Collection of metadata parameters
"""
raise NotImplementedError
# To avoid problems since this is no longer an abstract method
if cls is not StampsBase:
raise NotImplementedError(
f"Please implement specific FITS reader for class {cls}"
)

# Load metadata to get class
metadata = afwFits.readMetadata(filename, hdu=0)
type_name = metadata.get("STAMPCLS")
if type_name is None:
raise RuntimeError(
f"No class name in file {filename}. Unable to instantiate correct"
" stamps subclass. Is this an old version format Stamps file?"
)

# Import class and override `cls`
stamp_type = doImport(type_name)
cls = stamp_type

return cls.readFitsWithOptions(filename, options)

@abc.abstractmethod
def _refresh_metadata(self):
Expand All @@ -310,7 +336,8 @@ def writeFits(self, filename):
"""
self._refresh_metadata()
stamps_ims = self.getMaskedImages()
writeFits(filename, stamps_ims, self._metadata, self.use_mask, self.use_variance)
type_name = getFullTypeName(self)
writeFits(filename, stamps_ims, self._metadata, type_name, self.use_mask, self.use_variance)

def __len__(self):
return len(self._stamps)
Expand Down Expand Up @@ -354,7 +381,7 @@ def append(self, item):
Stamp object to append.
"""
if not isinstance(item, Stamp):
raise ValueError("Ojbects added must be a Stamp object.")
raise ValueError("Objects added must be a Stamp object.")
self._stamps.append(item)
return None

Expand Down
2 changes: 1 addition & 1 deletion src/CR.cc
Expand Up @@ -73,7 +73,7 @@ class IdSpan {
/**
* comparison functor; sort by ID, then by row (y), then by column range start (x0)
*/
struct IdSpanCompar : public std::binary_function<const IdSpan::ConstPtr, const IdSpan::ConstPtr, bool> {
struct IdSpanCompar {
bool operator()(const IdSpan::ConstPtr a, const IdSpan::ConstPtr b) {
if (a->id < b->id) {
return true;
Expand Down
2 changes: 1 addition & 1 deletion src/Interp.cc
Expand Up @@ -2033,7 +2033,7 @@ static void do_defects(std::vector<Defect::Ptr> const &badList, // list of bad

namespace {
template <typename T>
struct Sort_ByX0 : public std::binary_function<std::shared_ptr<T> const, std::shared_ptr<T> const, bool> {
struct Sort_ByX0 {
bool operator()(std::shared_ptr<T> const a, std::shared_ptr<T> const b) const {
return a->getX0() < b->getX0();
}
Expand Down
2 changes: 1 addition & 1 deletion tests/test_htmIndex.py
Expand Up @@ -280,7 +280,7 @@ def testLoadPixelBox(self):
# at assessing the correctness of the change. This is left to the
# explicit testProperMotion() test below.
resultWithEpoch = loader.loadPixelBox(bbox=bbox, wcs=wcs, filterName="a",
epoch=astropy.time.Time(20000, format='mjd', scale="tai"))
epoch=astropy.time.Time(30000, format='mjd', scale="tai"))
self.assertFloatsNotEqual(result.refCat["coord_ra"], resultWithEpoch.refCat["coord_ra"],
rtol=1.0e-4)
self.assertFloatsNotEqual(result.refCat["coord_dec"], resultWithEpoch.refCat["coord_dec"],
Expand Down
46 changes: 46 additions & 0 deletions tests/test_stamps.py
Expand Up @@ -57,6 +57,52 @@ def make_stamps(n_stamps=3):
return stamps.Stamps(stamp_list, metadata=metadata)


class StampsBaseTestCase(lsst.utils.tests.TestCase):
"""Test StampsBase.
"""
def testReadFitsWithOptionsNotImplementedErrorRaised(self):
"""
Test that subclasses have their own version
of this implemented or an error is raised.
"""
class FakeStampsBase(stamps.StampsBase):
def __init__(self):
return

with self.assertRaises(NotImplementedError):
FakeStampsBase.readFitsWithOptions('noFile', {})

def testReadFitsWithOptionsMetadataError(self):
"""Test that error is raised when STAMPCLS returns None
"""
with tempfile.NamedTemporaryFile() as f:
ss = make_stamps()
emptyMetadata = PropertyList()
stamps.writeFits(
f.name, [ss[0].stamp_im], emptyMetadata, None, True, True
)
with self.assertRaises(RuntimeError):
stamps.StampsBase.readFits(f.name)

def testReadFitsReturnsNewClass(self):
"""Test that readFits will return subclass
"""
class FakeStampsBase(stamps.StampsBase):
def __init__(self):
self._metadata = {}
return

@classmethod
def readFitsWithOptions(cls, filename, options):
return cls()

def _refresh_metadata(self):
self._metadata = {}

fakeStamps = FakeStampsBase.readFitsWithOptions('noFile', {})
self.assertEqual(type(fakeStamps), FakeStampsBase)


class StampsTestCase(lsst.utils.tests.TestCase):
"""Test Stamps.
"""
Expand Down
2 changes: 1 addition & 1 deletion ups/meas_algorithms.table
Expand Up @@ -17,4 +17,4 @@ envPrepend(DYLD_LIBRARY_PATH, ${PRODUCT_DIR}/lib)
envPrepend(LSST_LIBRARY_PATH, ${PRODUCT_DIR}/lib)

envPrepend(PYTHONPATH, ${PRODUCT_DIR}/python)
envAppend(PATH, ${PRODUCT_DIR}/bin)
envPrepend(PATH, ${PRODUCT_DIR}/bin)

0 comments on commit 38c4b2a

Please sign in to comment.