From 7b757ef99999096171552405c6bb2f2be0035353 Mon Sep 17 00:00:00 2001 From: Clare Saunders Date: Wed, 4 Sep 2024 18:30:15 -0700 Subject: [PATCH] Add option to build camera in gbdes task --- python/lsst/drp/tasks/build_camera.py | 123 +++++++++---------- python/lsst/drp/tasks/gbdesAstrometricFit.py | 92 +++++++++++++- tests/test_build_camera.py | 21 ++-- 3 files changed, 160 insertions(+), 76 deletions(-) diff --git a/python/lsst/drp/tasks/build_camera.py b/python/lsst/drp/tasks/build_camera.py index 3edb2ce1..2371bc44 100644 --- a/python/lsst/drp/tasks/build_camera.py +++ b/python/lsst/drp/tasks/build_camera.py @@ -20,37 +20,23 @@ # see . # import re -import time from collections import defaultdict import astshim as ast import lsst.pex.config import numpy as np import scipy.optimize -from lsst.afw.cameraGeom import ( - ACTUAL_PIXELS, - FIELD_ANGLE, - FOCAL_PLANE, - PIXELS, - TAN_PIXELS, - Camera, - CameraSys, - DetectorConfig, -) -from lsst.afw.geom import TransformPoint2ToPoint2, transformConfig +from lsst.afw.cameraGeom import FIELD_ANGLE, FOCAL_PLANE, Camera +from lsst.afw.geom import TransformPoint2ToPoint2 from lsst.geom import degrees from lsst.pipe.base import Task -from .gbdesAstrometricFit import _degreeFromNCoeffs, _nCoeffsFromDegree - __all__ = [ - "BuildCameraConfig", - "BuildCameraTask", + "BuildCameraFromAstrometryConfig", + "BuildCameraFromAstrometryTask", ] -cameraSysList = [FIELD_ANGLE, FOCAL_PLANE, PIXELS, TAN_PIXELS, ACTUAL_PIXELS] -cameraSysMap = dict((sys.getSysName(), sys) for sys in cameraSysList) - +# Set up global variable to use in minimization callback. Nfeval = 0 @@ -149,7 +135,7 @@ def _z_function_dy(params, x, y, order=4): return z -class BuildCameraConfig(lsst.pex.config.Config): +class BuildCameraFromAstrometryConfig(lsst.pex.config.Config): """Configuration for BuildCameraTask.""" tangentPlaneDegree = lsst.pex.config.Field( @@ -184,7 +170,7 @@ class BuildCameraConfig(lsst.pex.config.Config): plateScale = lsst.pex.config.Field( dtype=float, doc=("Scaling between camera coordinates in mm and angle on the sky in" " arcsec."), - default=1.0, + default=20.005867576692737, ) astInversionTolerance = lsst.pex.config.Field( dtype=float, @@ -201,29 +187,20 @@ class BuildCameraConfig(lsst.pex.config.Config): ) -class BuildCameraTask(Task): +class BuildCameraFromAstrometryTask(Task): """Build an `lsst.afw.cameraGeom.Camera` object out of the `gbdes` polynomials mapping from pixels to the tangent plane. Parameters ---------- - camera : `lsst.afw.cameraGeom.Camera` - Camera object from which to take pupil function, name, and other - properties. - detectorList : `list` [`int`] - List of detector ids. - visitList : `list` [`int`] - List of ids for visits that were used to train the input model. + """ - ConfigClass = BuildCameraConfig + ConfigClass = BuildCameraFromAstrometryConfig _DefaultName = "buildCamera" - def __init__(self, camera, detectorList, visitList, **kwargs): + def __init__(self, **kwargs): super().__init__(**kwargs) - self.camera = camera - self.detectorList = detectorList - self.visitList = visitList # The gbdes model normalizes the pixel positions to the range -1 - +1. X = np.arange(-1, 1, 0.1) @@ -232,7 +209,7 @@ def __init__(self, camera, detectorList, visitList, **kwargs): self.x = x.ravel() self.y = y.ravel() - def run(self, mapParams, mapTemplate): + def run(self, mapParams, mapTemplate, detectorList, visitList, inputCamera, rotationAngle): """Convert the model parameters into a Camera object. Parameters @@ -243,6 +220,15 @@ def run(self, mapParams, mapTemplate): mapTemplate : `dict` Dictionary describing the format of the astrometric model, following the convention in `gbdes`. + detectorList : `list` [`int`] + List of detector ids. + visitList : `list` [`int`] + List of ids for visits that were used to train the input model. + camera : `lsst.afw.cameraGeom.Camera` + Camera object from which to take pupil function, name, and other + properties. + rotationAngle : `float` + Value in radians of the average rotation angle of the input visits. Returns ------- @@ -251,27 +237,29 @@ def run(self, mapParams, mapTemplate): """ # Normalize the model. - newParams, newIntX, newIntY = self._normalizeModel(mapParams, mapTemplate) + newParams, newIntX, newIntY = self._normalizeModel(mapParams, mapTemplate, detectorList, visitList) if self.config.modelSplitting == "basic": # Put all of the camera distortion into the pixels->focal plane # part of the distortion model, with the focal plane->tangent plane # part only used for scaling between the focal plane and sky. - pixToFocalPlane, focalPlaneToTangentPlane = self._basicModel(newParams) + pixToFocalPlane, focalPlaneToTangentPlane = self._basicModel(newParams, detectorList) else: # Fit two polynomials, such that the first describes the pixel-> # focal plane part of the model, and the second describes the focal # plane->tangent plane part of the model, with the goal of # generating a more physically-motivated distortion model. - pixToFocalPlane, focalPlaneToTangentPlane = self._splitModel(newIntX, newIntY) + pixToFocalPlane, focalPlaneToTangentPlane = self._splitModel(newIntX, newIntY, detectorList) # Turn the mappings into a Camera object. - camera = self._translateToAfw(pixToFocalPlane, focalPlaneToTangentPlane) + camera = self._translateToAfw( + pixToFocalPlane, focalPlaneToTangentPlane, detectorList, inputCamera, rotationAngle + ) return camera - def _normalizeModel(self, mapParams, mapTemplate): + def _normalizeModel(self, mapParams, mapTemplate, detectorList, visitList): """Normalize the camera mappings, such that they correspond to the average visit. @@ -306,7 +294,7 @@ def _normalizeModel(self, mapParams, mapTemplate): deviceParams = [] visitParams = [] for element in mapTemplate["BAND/DEVICE"]["Elements"]: - for detector in self.detectorList: + for detector in detectorList: detectorTemplate = element.replace("DEVICE", str(detector)) detectorTemplate = detectorTemplate.replace("BAND", ".+") for k, params in mapParams.items(): @@ -315,7 +303,7 @@ def _normalizeModel(self, mapParams, mapTemplate): deviceArray = np.vstack(deviceParams) for element in mapTemplate["EXPOSURE"]["Elements"]: - for visit in self.visitList: + for visit in visitList: visitTemplate = element.replace("EXPOSURE", str(visit)) for k, params in mapParams.items(): if re.fullmatch(visitTemplate, k): @@ -342,7 +330,7 @@ def _normalizeModel(self, mapParams, mapTemplate): newIntY = [] for deviceMap in newDeviceArray: nCoeffsDev = len(deviceMap) // 2 - deviceDegree = _degreeFromNCoeffs(nCoeffsDev) + deviceDegree = int(-1.5 + 0.5 * (1 + 8 * nCoeffsDev) ** 0.5) intX = _z_function(deviceMap[:nCoeffsDev], self.x, self.y, order=deviceDegree) intY = _z_function(deviceMap[nCoeffsDev:], self.x, self.y, order=deviceDegree) @@ -354,7 +342,7 @@ def _normalizeModel(self, mapParams, mapTemplate): return newDeviceArray, newIntX, newIntY - def _basicModel(self, modelParameters): + def _basicModel(self, modelParameters, detectorList): """This will just convert the pix->fp parameters into the right format, and return an identity mapping for the fp->tp part. @@ -376,7 +364,7 @@ def _basicModel(self, modelParameters): nCoeffsFP = modelParameters.shape[1] // 2 pixelsToFocalPlane = defaultdict(dict) - for d, det in enumerate(self.detectorList): + for d, det in enumerate(detectorList): pixelsToFocalPlane[det]["x"] = modelParameters[d][:nCoeffsFP] pixelsToFocalPlane[det]["y"] = modelParameters[d][nCoeffsFP:] @@ -384,7 +372,7 @@ def _basicModel(self, modelParameters): return pixelsToFocalPlane, focalPlaneToTangentPlane - def _splitModel(self, targetX, targetY, pixToFPGuess=None, fpToTpGuess=None): + def _splitModel(self, targetX, targetY, detectorList, pixToFPGuess=None, fpToTpGuess=None): """Fit a two-step model, with one polynomial per detector fitting the pixels to focal plane part, followed by a polynomial fitting the focal plane to tangent plane part. @@ -419,10 +407,10 @@ def _splitModel(self, targetX, targetY, pixToFPGuess=None, fpToTpGuess=None): tpDegree = self.config.tangentPlaneDegree fpDegree = self.config.focalPlaneDegree - nCoeffsFP = _nCoeffsFromDegree(fpDegree) - nCoeffsFP_tot = len(self.detectorList) * nCoeffsFP * 2 + nCoeffsFP = int((fpDegree + 2) * (fpDegree + 1) / 2) + nCoeffsFP_tot = len(detectorList) * nCoeffsFP * 2 - nCoeffsTP = _nCoeffsFromDegree(tpDegree) + nCoeffsTP = int((tpDegree + 2) * (tpDegree + 1) / 2) # The constant and linear terms will be fixed to remove degeneracy with # the pix->fp part of the model nCoeffsFixed = 3 @@ -447,7 +435,7 @@ def two_part_function(params): # The function giving the split model. intX_tot = [] intY_tot = [] - for i in range(len(self.detectorList)): + for i in range(len(detectorList)): intX = _z_function( params[2 * nCoeffsFP * i : (2 * i + 1) * nCoeffsFP], self.x, self.y, order=fpDegree ) @@ -487,7 +475,7 @@ def jac(params): intX_tot = [] intY_tot = [] # Fill in the derivatives for the pix->fp terms. - for i in range(len(self.detectorList)): + for i in range(len(detectorList)): dX_i = dX[nX * i : nX * (i + 1)] dY_i = dY[nX * i : nX * (i + 1)] intX = _z_function( @@ -537,7 +525,7 @@ def hessian(params): # Loop over the detectors to fill in the d(pix->fp)**2 and # d(pix->fp)d(fp->tp) cross terms. - for i in range(len(self.detectorList)): + for i in range(len(detectorList)): intX = _z_function( fp_params[2 * nCoeffsFP * i : (2 * i + 1) * nCoeffsFP], self.x, self.y, order=fpDegree ) @@ -579,7 +567,7 @@ def hessian(params): intX_tot = np.array(intX_tot).ravel() intY_tot = np.array(intY_tot).ravel() - tmpZ_array = np.zeros((nCoeffsFree, nX * len(self.detectorList))) + tmpZ_array = np.zeros((nCoeffsFree, nX * len(detectorList))) # Finally, get the d(fp->tp)**2 terms for j in range(nCoeffsFree): tParams = np.zeros(nCoeffsTP) @@ -604,7 +592,7 @@ def callbackMF(params): initGuess = np.zeros(nCoeffsFP_tot + 2 * nCoeffsFree) if pixToFPGuess: useVar = min(nCoeffsFP, len(pixToFPGuess[0]["x"])) - for i, det in enumerate(self.detectorList): + for i, det in enumerate(detectorList): initGuess[2 * nCoeffsFP * i : 2 * nCoeffsFP * i + useVar] = pixToFPGuess[det]["x"][:useVar] initGuess[(2 * i + 1) * nCoeffsFP : (2 * i + 1) * nCoeffsFP + useVar] = pixToFPGuess[det][ "y" @@ -631,7 +619,7 @@ def callbackMF(params): # Convert parameters to a dictionary. pixToFP = {} - for i, det in enumerate(self.detectorList): + for i, det in enumerate(detectorList): pixToFP[det] = { "x": res.x[2 * nCoeffsFP * i : (2 * i + 1) * nCoeffsFP], "y": res.x[(2 * i + 1) * nCoeffsFP : 2 * nCoeffsFP * (1 + i)], @@ -676,7 +664,9 @@ def makeAstPolyMapCoeffs(self, order, xCoeffs, yCoeffs): return polyArray - def _translateToAfw(self, pixToFocalPlane, focalPlaneToTangentPlane): + def _translateToAfw( + self, pixToFocalPlane, focalPlaneToTangentPlane, detectorList, inputCamera, rotationAngle + ): """Convert the model parameters to a Camera object. Parameters @@ -687,6 +677,8 @@ def _translateToAfw(self, pixToFocalPlane, focalPlaneToTangentPlane): polynomials. focalPlaneToTangentPlane : `dict` [`string`, `float`] Dictionary giving the focal plane to tangent plane mapping. + rotationAngle : `float` + Value in radians of the average rotation angle of the input visits. Returns ------- @@ -695,21 +687,25 @@ def _translateToAfw(self, pixToFocalPlane, focalPlaneToTangentPlane): """ if self.config.modelSplitting == "basic": tpDegree = 1 - fpDegree = _degreeFromNCoeffs(len(pixToFocalPlane[self.detectorList[0]]["x"])) + nCoeffsFP = len(pixToFocalPlane[detectorList[0]]["x"]) + fpDegree = int(-1.5 + 0.5 * (1 + 8 * nCoeffsFP) ** 0.5) else: tpDegree = self.config.tangentPlaneDegree fpDegree = self.config.focalPlaneDegree scaleConvert = (1 * degrees).asArcseconds() / self.config.plateScale - cameraBuilder = Camera.Builder(self.camera.getName()) - cameraBuilder.setPupilFactoryName(self.camera.getPupilFactoryName()) + cameraBuilder = Camera.Builder(inputCamera.getName()) + cameraBuilder.setPupilFactoryName(inputCamera.getPupilFactoryName()) # Convert fp->tp to AST format: forwardCoeffs = self.makeAstPolyMapCoeffs( tpDegree, focalPlaneToTangentPlane["x"], focalPlaneToTangentPlane["y"] ) - rotateAndFlipCoeffs = self.makeAstPolyMapCoeffs(1, [0, 0, -1], [0, -1, 0]) + # Reverse rotation from input visits and flip x-axis. + cosRot = np.cos(rotationAngle) + sinRot = np.sin(rotationAngle) + rotateAndFlipCoeffs = self.makeAstPolyMapCoeffs(1, [0, cosRot, -sinRot], [0, -sinRot, cosRot]) ccdZoom = ast.ZoomMap(2, 1 / scaleConvert) ccdToSky = ast.PolyMap( @@ -731,14 +727,15 @@ def _translateToAfw(self, pixToFocalPlane, focalPlaneToTangentPlane): cameraBuilder.setTransformFromFocalPlaneTo(FIELD_ANGLE, focalPlaneToTPMapping) # Convert pix->fp to AST format: - for detector in self.camera: + for detector in inputCamera: d = detector.getId() if d not in pixToFocalPlane: - # TODO: just grab detector map from the obs_ package. + # This camera will not include detectors that were not used in astrometric fit. continue detectorBuilder = cameraBuilder.add(detector.getName(), detector.getId()) - # TODO: Add all the detector details like pixel size, etc. + detectorBuilder.setBBox(detector.getBBox()) + detectorBuilder.setPixelSize(detector.getPixelSize()) for amp in detector.getAmplifiers(): detectorBuilder.append(amp.rebuild()) diff --git a/python/lsst/drp/tasks/gbdesAstrometricFit.py b/python/lsst/drp/tasks/gbdesAstrometricFit.py index bb4ddaf1..00f96b20 100644 --- a/python/lsst/drp/tasks/gbdesAstrometricFit.py +++ b/python/lsst/drp/tasks/gbdesAstrometricFit.py @@ -43,6 +43,8 @@ from sklearn.cluster import AgglomerativeClustering from smatch.matcher import Matcher +from .build_camera import BuildCameraFromAstrometryTask + __all__ = [ "GbdesAstrometricFitConnections", "GbdesAstrometricFitConfig", @@ -285,6 +287,13 @@ class GbdesAstrometricFitConnections( storageClass="ArrowNumpyDict", dimensions=("instrument", "physical_filter"), ) + inputCamera = pipeBase.connectionTypes.PrerequisiteInput( + doc="Input camera to use when constructing camera from astrometric model.", + name="camera", + storageClass="Camera", + dimensions=("instrument",), + isCalibration=True, + ) outputWcs = pipeBase.connectionTypes.Output( doc=( "Per-tract, per-visit world coordinate systems derived from the fitted model." @@ -324,7 +333,13 @@ class GbdesAstrometricFitConnections( doc="Camera parameters to use for 'device' part of model", name="gbdesAstrometricFit_cameraModel", storageClass="ArrowNumpyDict", - dimensions=("instrument", "physical_filter"), + dimensions=("instrument", "skymap", "tract", "physical_filter"), + ) + camera = pipeBase.connectionTypes.Output( + doc="Camera object constructed using the per-detector part of the astrometric model", + name="gbdesAstrometricFitCamera", + storageClass="Camera", + dimensions=("instrument", "skymap", "tract", "physical_filter"), ) def getSpatialBoundsConnections(self): @@ -339,6 +354,9 @@ def __init__(self, *, config=None): self.prerequisiteInputs.remove("inputCameraModel") if not self.config.saveCameraModel: self.outputs.remove("outputCameraModel") + if not self.config.saveCameraObject: + self.inputs.remove("inputCamera") + self.outputs.remove("camera") class GbdesAstrometricFitConfig( @@ -452,6 +470,14 @@ class GbdesAstrometricFitConfig( doc="Save the 'device' part of the model to be used as input in future runs.", default=False, ) + buildCamera = pexConfig.ConfigurableField( + target=BuildCameraFromAstrometryTask, doc="Subtask to build camera from astrometric model." + ) + saveCameraObject = pexConfig.Field( + dtype=bool, + doc="Build and output an lsst.afw.cameraGeom.Camera object using the fit per-detector model.", + default=False, + ) def setDefaults(self): # Use only stars because aperture fluxes of galaxies are biased and @@ -522,6 +548,8 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self.makeSubtask("sourceSelector") self.makeSubtask("referenceSelector") + if self.config.saveCameraObject: + self.makeSubtask("buildCamera") def runQuantum(self, butlerQC, inputRefs, outputRefs): # We override runQuantum to set up the refObjLoaders @@ -569,6 +597,7 @@ def run( refEpoch=None, refObjectLoader=None, inputCameraModel=None, + inputCamera=None, ): """Run the WCS fit for a given set of visits @@ -588,6 +617,8 @@ def run( Referencef object loader instance. inputCameraModel : `dict` [`str`, `np.ndarray`], optional Parameters to use for the device part of the model. + inputCamera : `lsst.afw.cameraGeom.Camera`, optional + Camera to be used as template when constructing new camera. Returns ------- @@ -606,6 +637,8 @@ def run( ``cameraModelParams`` : `dict` [`str`, `np.ndarray`] Parameters of the device part of the model, in the format needed as input for future runs. + ``camera`` : `lsst.afw.cameraGeom.Camera` + Camera object constructed from the per-detector model. """ self.log.info("Gather instrument, exposure, and field info") @@ -701,12 +734,13 @@ def run( ) self.log.info("WCS fitting done") - outputWcss, cameraParams = self._make_outputs( + outputWcss, cameraParams, camera = self._make_outputs( wcsf, inputVisitSummaries, exposureInfo, mapTemplate, inputCameraModel=(inputCameraModel if self.config.useInputCameraModel else None), + inputCamera=(inputCamera if self.config.buildCamera else None), ) outputCatalog = wcsf.getOutputCatalog() starCatalog = wcsf.getStarCatalog() @@ -719,6 +753,7 @@ def run( starCatalog=starCatalog, modelParams=modelParams, cameraModelParams=cameraParams, + camera=camera, ) def _prep_sky(self, inputVisitSummaries, epoch, fieldName="Field"): @@ -1587,7 +1622,9 @@ def _make_afw_wcs(self, mapDict, centerRA, centerDec, doNormalizePixels=False, x outWCS = afwgeom.SkyWcs(frameDict) return outWCS - def _make_outputs(self, wcsf, visitSummaryTables, exposureInfo, mapTemplate, inputCameraModel=None): + def _make_outputs( + self, wcsf, visitSummaryTables, exposureInfo, mapTemplate, inputCameraModel=None, inputCamera=None + ): """Make a WCS object out of the WCS models. Parameters @@ -1625,6 +1662,8 @@ def _make_outputs(self, wcsf, visitSummaryTables, exposureInfo, mapTemplate, inp cameraParams : `dict` [`str`, `np.ndarray`], optional Parameters for the device part of the model in the format needed when used as input for future runs. + camera : `lsst.afw.cameraGeom.Camera`, optional + Camera object constructed from the per-detector model. """ # Get the parameters of the fit models mapParams = wcsf.mapCollection.getParamDict() @@ -1637,6 +1676,26 @@ def _make_outputs(self, wcsf, visitSummaryTables, exposureInfo, mapTemplate, inp for k, params in mapParams.items(): if re.fullmatch(detectorTemplate, k): cameraParams[k] = params + if self.config.saveCameraObject: + # Get the average rotation angle of the input visits. + rotations = [ + visTable[0].visitInfo.boresightRotAngle.asRadians() for visTable in visitSummaryTables + ] + rotationAngle = np.mean(rotations) + if inputCamera is None: + raise RuntimeError( + "inputCamera must be provided to _make_outputs in order to build output camera." + ) + camera = self.buildCamera.run( + mapParams, + mapTemplate, + exposureInfo.detectors, + exposureInfo.visits, + inputCamera, + rotationAngle, + ) + else: + camera = None if self.config.useInputCameraModel: if inputCameraModel is None: raise RuntimeError( @@ -1725,7 +1784,7 @@ def _make_outputs(self, wcsf, visitSummaryTables, exposureInfo, mapTemplate, inp catalog.sort() catalogs[visit] = catalog - return catalogs, cameraParams + return catalogs, cameraParams, camera def _compute_model_params(self, wcsf): """Get the WCS model parameters and covariance and convert to a @@ -1862,6 +1921,13 @@ class GbdesGlobalAstrometricFitConnections( storageClass="ArrowNumpyDict", dimensions=("instrument", "physical_filter"), ) + inputCamera = pipeBase.connectionTypes.PrerequisiteInput( + doc="Input camera to use when constructing camera from astrometric model.", + name="camera", + storageClass="Camera", + dimensions=("instrument",), + isCalibration=True, + ) outputWcs = pipeBase.connectionTypes.Output( doc=( "Per-visit world coordinate systems derived from the fitted model. These catalogs only contain " @@ -1903,6 +1969,12 @@ class GbdesGlobalAstrometricFitConnections( storageClass="ArrowNumpyDict", dimensions=("instrument", "physical_filter"), ) + camera = pipeBase.connectionTypes.Output( + doc="Camera object constructed using the per-detector part of the astrometric model", + name="gbdesAstrometricFitCamera", + storageClass="Camera", + dimensions=("instrument", "physical_filter"), + ) def getSpatialBoundsConnections(self): return ("inputVisitSummaries",) @@ -1916,6 +1988,9 @@ def __init__(self, *, config=None): self.prerequisiteInputs.remove("inputCameraModel") if not self.config.saveCameraModel: self.outputs.remove("outputCameraModel") + if not self.config.saveCameraObject: + self.inputs.remove("inputCamera") + self.outputs.remove("camera") class GbdesGlobalAstrometricFitConfig( @@ -2006,6 +2081,7 @@ def run( refEpoch=None, refObjectLoader=None, inputCameraModel=None, + inputCamera=None, ): """Run the WCS fit for a given set of visits @@ -2027,6 +2103,8 @@ def run( Reference object loader instance. inputCameraModel : `dict` [`str`, `np.ndarray`], optional Parameters to use for the device part of the model. + inputCamera : `lsst.afw.cameraGeom.Camera`, optional + Camera to be used as template when constructing new camera. Returns ------- @@ -2045,6 +2123,8 @@ def run( ``cameraModelParams`` : `dict` [`str`, `np.ndarray`] Parameters of the device part of the model, in the format needed as input for future runs. + ``camera`` : `lsst.afw.cameraGeom.Camera` + Camera object constructed from the per-detector model. """ self.log.info("Gather instrument, exposure, and field info") @@ -2121,12 +2201,13 @@ def run( ) self.log.info("WCS fitting done") - outputWcss, cameraParams = self._make_outputs( + outputWcss, cameraParams, camera = self._make_outputs( wcsf, inputVisitSummaries, exposureInfo, mapTemplate, inputCameraModel=(inputCameraModel if self.config.useInputCameraModel else None), + inputCamera=(inputCamera if self.config.buildCamera else None), ) outputCatalog = wcsf.getOutputCatalog() starCatalog = wcsf.getStarCatalog() @@ -2139,6 +2220,7 @@ def run( starCatalog=starCatalog, modelParams=modelParams, cameraModelParams=cameraParams, + camera=camera, ) def _prep_sky(self, inputVisitSummaries): diff --git a/tests/test_build_camera.py b/tests/test_build_camera.py index ac1e77fd..41819914 100644 --- a/tests/test_build_camera.py +++ b/tests/test_build_camera.py @@ -30,13 +30,17 @@ import yaml from lsst.afw.cameraGeom.testUtils import FIELD_ANGLE, PIXELS, CameraWrapper from lsst.afw.table import ExposureCatalog -from lsst.drp.tasks.build_camera import BuildCameraConfig, BuildCameraTask, _z_function +from lsst.drp.tasks.build_camera import ( + BuildCameraFromAstrometryConfig, + BuildCameraFromAstrometryTask, + _z_function, +) from scipy.optimize import minimize TESTDIR = os.path.abspath(os.path.dirname(__file__)) -class TestBuildCamera(lsst.utils.tests.TestCase): +class TestBuildCameraFromAstrometry(lsst.utils.tests.TestCase): def setUp(self): @@ -44,7 +48,7 @@ def setUp(self): self.camera = CameraWrapper(isLsstLike=False).camera self.detectorList = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100] self.visitList = np.arange(10) - self.task = BuildCameraTask(self.camera, self.detectorList, self.visitList) + self.task = BuildCameraFromAstrometryTask(self.camera, self.detectorList, self.visitList) datadir = os.path.join(TESTDIR, "data") with open(os.path.join(datadir, "sample_wcs.yaml"), "r") as f: @@ -158,9 +162,9 @@ def jac(params): def test_run_with_basic_model(self): - config = BuildCameraConfig() + config = BuildCameraFromAstrometryConfig() config.modelSplitting = "basic" - task = BuildCameraTask(self.camera, self.detectorList, self.visitList, config=config) + task = BuildCameraFromAstrometryTask(self.camera, self.detectorList, self.visitList, config=config) camera = task.run(self.mapParams, self.mapTemplate) testX, testY = [], [] @@ -180,10 +184,10 @@ def test_run_with_basic_model(self): def test_run_with_splitModel(self): - config = BuildCameraConfig() + config = BuildCameraFromAstrometryConfig() config.modelSplitting = "physical" config.modelSplittingTolerance = 1e-6 - task = BuildCameraTask(self.camera, self.detectorList, self.visitList, config=config) + task = BuildCameraFromAstrometryTask(self.camera, self.detectorList, self.visitList, config=config) camera = task.run(self.mapParams, self.mapTemplate) testX, testY = [], [] @@ -198,7 +202,8 @@ def test_run_with_splitModel(self): testX = np.concatenate(testX) testY = np.concatenate(testY) - # This does not reconstruct the input field angles perfectly. + # The "physical" model splitting is not expected to + # reconstruct the input field angles perfectly. self.assertFloatsAlmostEqual(self.originalFieldAngleX, testX, atol=1e-4) self.assertFloatsAlmostEqual(self.originalFieldAngleY, testY, atol=1e-4)