Skip to content

Commit

Permalink
Added tasks for single-side-of-focus.
Browse files Browse the repository at this point in the history
  • Loading branch information
jfcrenshaw committed Sep 4, 2024
1 parent aa75922 commit 458fa19
Show file tree
Hide file tree
Showing 7 changed files with 297 additions and 35 deletions.
21 changes: 1 addition & 20 deletions python/lsst/ts/wep/estimation/tie.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,6 @@ class TieAlgorithm(WfAlgorithm):
To see possibilities, see the docstring for
lsst.ts.wep.imageMapper.ImageMapper.createPupilMasks().
(the default is None)
requiresPairs : bool, optional
Whether to allow Zernike estimation with a single donut. If True,
pairs are required. (the default is True)
modelPupilKernelSize : float, optional
The size of the Gaussian kernel to convolve with the model pupil
when estimating Zernikes with a single donut.
Expand All @@ -102,7 +99,6 @@ def __init__(
centerBinary: bool = True,
convergeTol: float = 1e-9,
maskKwargs: Optional[dict] = None,
requiresPairs: bool = True,
modelPupilKernelSize: float = 2,
) -> None:
self.opticalModel = opticalModel
Expand All @@ -113,27 +109,12 @@ def __init__(
self.centerBinary = centerBinary
self.convergeTol = convergeTol
self.maskKwargs = maskKwargs
self.requiresPairs = requiresPairs
self.modelPupilKernelSize = modelPupilKernelSize

@property
def requiresPairs(self) -> bool:
"""Whether the algorithm requires pairs to estimate Zernikes."""
return self._requiresPairs

@requiresPairs.setter
def requiresPairs(self, value: bool) -> None:
"""Set whether the algorithm requires pairs to estimate Zernikes.
Parameters
----------
value : bool
Whether to allow Zernike estimation with a single donut.
If True, pairs are required.
"""
if not isinstance(value, bool):
raise TypeError("requiresPairs must be a bool.")
self._requiresPairs = value
return False

@property
def opticalModel(self) -> str:
Expand Down
2 changes: 2 additions & 0 deletions python/lsst/ts/wep/task/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from .calcZernikesTask import *
from .calcZernikesUnpairedTask import *
from .combineZernikesBase import *
from .combineZernikesMeanTask import *
from .combineZernikesSigmaClipTask import *
from .cutOutDonutsBase import *
from .cutOutDonutsCwfsTask import *
from .cutOutDonutsScienceSensorTask import *
from .cutOutDonutsUnpairedTask import *
from .donutQuickMeasurementTask import *
from .donutSourceSelectorTask import *
from .donutStamp import *
Expand Down
11 changes: 7 additions & 4 deletions python/lsst/ts/wep/task/calcZernikesTask.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,15 @@ def run(
donutStampsExtra: DonutStamps,
donutStampsIntra: DonutStamps,
) -> pipeBase.Struct:
# Get jmax
jmax = self.estimateZernikes.config.maxNollIndex

# If no donuts are in the donutCatalog for a set of exposures
# then return the Zernike coefficients as nan.
if len(donutStampsExtra) == 0 or len(donutStampsIntra) == 0:
return pipeBase.Struct(
outputZernikesRaw=np.full(19, np.nan),
outputZernikesAvg=np.full(19, np.nan),
outputZernikesRaw=np.full(jmax - 4, np.nan),
outputZernikesAvg=np.full(jmax - 4, np.nan),
donutsExtraQuality=pd.DataFrame([]),
donutsIntraQuality=pd.DataFrame([]),
)
Expand All @@ -167,8 +170,8 @@ def run(
):
self.log.info("No donut stamps were selected.")
return pipeBase.Struct(
outputZernikesRaw=np.full(19, np.nan),
outputZernikesAvg=np.full(19, np.nan),
outputZernikesRaw=np.full(jmax - 4, np.nan),
outputZernikesAvg=np.full(jmax - 4, np.nan),
donutsExtraQuality=pd.DataFrame([]),
donutsIntraQuality=pd.DataFrame([]),
)
Expand Down
146 changes: 146 additions & 0 deletions python/lsst/ts/wep/task/calcZernikesUnpairedTask.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# This file is part of ts_wep.
#
# Developed for the LSST Telescope and Site Systems.
# 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/>.

__all__ = [
"CalcZernikesUnpairedTaskConnections",
"CalcZernikesUnpairedTaskConfig",
"CalcZernikesUnpairedTask",
]

import lsst.pipe.base as pipeBase
import numpy as np
import pandas as pd
from lsst.pipe.base import connectionTypes
from lsst.ts.wep.task.calcZernikesTask import CalcZernikesTaskConfig
from lsst.ts.wep.task.donutStamps import DonutStamps
from lsst.ts.wep.utils import DefocalType
from lsst.utils.timer import timeMethod


class CalcZernikesUnpairedTaskConnections(
pipeBase.PipelineTaskConnections,
dimensions=("visit", "detector", "instrument"),
):
donutStamps = connectionTypes.Input(
doc="Defocused Donut Postage Stamp Images",
dimensions=("visit", "detector", "instrument"),
storageClass="StampsBase",
name="donutStamps",
)
outputZernikesRaw = connectionTypes.Output(
doc="Zernike Coefficients from all donuts",
dimensions=("visit", "detector", "instrument"),
storageClass="NumpyArray",
name="zernikeEstimateRaw",
)
outputZernikesAvg = connectionTypes.Output(
doc="Zernike Coefficients averaged over donuts",
dimensions=("visit", "detector", "instrument"),
storageClass="NumpyArray",
name="zernikeEstimateAvg",
)
donutsQuality = connectionTypes.Output(
doc="Quality information for donuts",
dimensions=("visit", "detector", "instrument"),
storageClass="DataFrame",
name="donutsQuality",
)


class CalcZernikesUnpairedTaskConfig(
CalcZernikesTaskConfig,
pipelineConnections=CalcZernikesUnpairedTaskConnections,
):
pass


class CalcZernikesUnpairedTask(pipeBase.PipelineTask):
"""Calculate Zernikes using unpaired donuts."""

ConfigClass = CalcZernikesUnpairedTaskConfig
_DefaultName = "calcZernikesUnpairedTask"

def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)

# Create subtasks
self.estimateZernikes = self.config.estimateZernikes
self.makeSubtask("estimateZernikes")

self.combineZernikes = self.config.combineZernikes
self.makeSubtask("combineZernikes")

self.donutStampSelector = self.config.donutStampSelector
self.makeSubtask("donutStampSelector")

self.doDonutStampSelector = self.config.doDonutStampSelector

@timeMethod
def run(self, donutStamps: DonutStamps) -> pipeBase.Struct:
# Get jmax
jmax = self.estimateZernikes.config.maxNollIndex

# If no donuts are in the donutCatalog for a set of exposures
# then return the Zernike coefficients as nan.
if len(donutStamps) == 0:
return pipeBase.Struct(
outputZernikesRaw=np.full(jmax - 4, np.nan),
outputZernikesAvg=np.full(jmax - 4, np.nan),
donutsQuality=pd.DataFrame([]),
)

# Run donut selection
if self.doDonutStampSelector:
self.log.info("Running Donut Stamp Selector")
selection = self.donutStampSelector.run(donutStamps)

# If no donuts get selected, return NaNs
if len(selection.donutStampsSelect) == 0:
self.log.info("No donut stamps were selected.")
return pipeBase.Struct(
outputZernikesRaw=np.full(jmax - 4, np.nan),
outputZernikesAvg=np.full(jmax - 4, np.nan),
donutsQuality=pd.DataFrame([]),
)

# Save selection and quality
selectedDonuts = selection.donutStampsSelect
donutsQuality = selection.donutsQuality
else:
selectedDonuts = donutStamps
donutsQuality = pd.DataFrame([])

# Assign stamps to either intra or extra
if selectedDonuts[0].wep_im.defocalType == DefocalType.Extra:
extraStamps = selectedDonuts
intraStamps = []
else:
extraStamps = []
intraStamps = selectedDonuts

# Estimate Zernikes
zkCoeffRaw = self.estimateZernikes.run(extraStamps, intraStamps)
zkCoeffCombined = self.combineZernikes.run(zkCoeffRaw.zernikes)
return pipeBase.Struct(
outputZernikesAvg=np.array(zkCoeffCombined.combinedZernikes),
outputZernikesRaw=np.array(zkCoeffRaw.zernikes),
donutsQuality=donutsQuality,
)
127 changes: 127 additions & 0 deletions python/lsst/ts/wep/task/cutOutDonutsUnpairedTask.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# This file is part of ts_wep.
#
# Developed for the LSST Telescope and Site Systems.
# 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/>.

__all__ = [
"CutOutDonutsUnpairedTaskConnections",
"CutOutDonutsUnpairedTaskConfig",
"CutOutDonutsUnpairedTask",
]


import typing
from copy import copy

import lsst.afw.cameraGeom
import lsst.afw.image as afwImage
import lsst.pipe.base as pipeBase
import pandas as pd
from lsst.fgcmcal.utilities import lookupStaticCalibrations
from lsst.pipe.base import connectionTypes
from lsst.ts.wep.task.cutOutDonutsBase import (
CutOutDonutsBaseTask,
CutOutDonutsBaseTaskConfig,
)
from lsst.ts.wep.utils import DefocalType
from lsst.utils.timer import timeMethod


class CutOutDonutsUnpairedTaskConnections(
pipeBase.PipelineTaskConnections,
dimensions=("exposure", "instrument"),
):
exposures = connectionTypes.Input(
doc="Input exposure to make measurements on",
dimensions=("exposure", "detector", "instrument"),
storageClass="Exposure",
name="postISRCCD",
multiple=True,
)
donutCatalog = connectionTypes.Input(
doc="Donut Locations",
dimensions=(
"visit",
"detector",
"instrument",
),
storageClass="DataFrame",
name="donutCatalog",
multiple=True,
)
camera = connectionTypes.PrerequisiteInput(
name="camera",
storageClass="Camera",
doc="Input camera to construct complete exposures.",
dimensions=["instrument"],
isCalibration=True,
lookupFunction=lookupStaticCalibrations,
)
donutStamps = connectionTypes.Output(
doc="Defocused Donut Postage Stamp Images",
dimensions=("visit", "detector", "instrument"),
storageClass="StampsBase",
name="donutStamps",
multiple=True,
)


class CutOutDonutsUnpairedTaskConfig(
CutOutDonutsBaseTaskConfig,
pipelineConnections=CutOutDonutsUnpairedTaskConnections,
):
pass


class CutOutDonutsUnpairedTask(CutOutDonutsBaseTask):
"""Cutout donuts without pairing any exposures."""

ConfigClass = CutOutDonutsUnpairedTaskConfig
_DefaultName = "CutOutDonutsUnpairedTask"

def __init__(self, **kwargs):
super().__init__(**kwargs)

@timeMethod
def run(
self,
exposures: typing.List[afwImage.Exposure],
donutCatalog: typing.List[pd.DataFrame],
camera: lsst.afw.cameraGeom.Camera,
) -> pipeBase.Struct:
# Loop over exposures (and corresponding catalogs)
stampList = []
for exposure, catalog in zip(exposures, donutCatalog):
# Determine which side of focus
if exposure.visitInfo.focusZ > 0:
defocalType = DefocalType.Extra
else:
defocalType = DefocalType.Intra

# Cutout the stamps
stampList.append(
self.cutOutStamps(
exposure,
catalog,
defocalType,
camera.getName(),
)
)

return pipeBase.Struct(donutStamps=stampList)
Loading

0 comments on commit 458fa19

Please sign in to comment.