Coverage for python/lsst/pipe/tasks/makeWarp.py: 18%
227 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-27 03:59 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-27 03:59 -0700
1# This file is part of pipe_tasks.
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/>.
22__all__ = ["MakeWarpTask", "MakeWarpConfig"]
24import logging
25import numpy
27import lsst.pex.config as pexConfig
28import lsst.afw.image as afwImage
29import lsst.coadd.utils as coaddUtils
30import lsst.pipe.base as pipeBase
31import lsst.pipe.base.connectionTypes as connectionTypes
32import lsst.utils as utils
33import lsst.geom
34from lsst.daf.butler import DeferredDatasetHandle
35from lsst.meas.base import DetectorVisitIdGeneratorConfig
36from lsst.meas.algorithms import CoaddPsf, CoaddPsfConfig, GaussianPsfFactory
37from lsst.skymap import BaseSkyMap
38from lsst.utils.timer import timeMethod
39from .coaddBase import CoaddBaseTask, makeSkyInfo, reorderAndPadList
40from .warpAndPsfMatch import WarpAndPsfMatchTask
41from collections.abc import Iterable
43log = logging.getLogger(__name__)
46class MakeWarpConnections(pipeBase.PipelineTaskConnections,
47 dimensions=("tract", "patch", "skymap", "instrument", "visit"),
48 defaultTemplates={"coaddName": "deep",
49 "calexpType": ""}):
50 calExpList = connectionTypes.Input(
51 doc="Input exposures to be resampled and optionally PSF-matched onto a SkyMap projection/patch",
52 name="{calexpType}calexp",
53 storageClass="ExposureF",
54 dimensions=("instrument", "visit", "detector"),
55 multiple=True,
56 deferLoad=True,
57 )
58 backgroundList = connectionTypes.Input(
59 doc="Input backgrounds to be added back into the calexp if bgSubtracted=False",
60 name="calexpBackground",
61 storageClass="Background",
62 dimensions=("instrument", "visit", "detector"),
63 multiple=True,
64 )
65 skyCorrList = connectionTypes.Input(
66 doc="Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True",
67 name="skyCorr",
68 storageClass="Background",
69 dimensions=("instrument", "visit", "detector"),
70 multiple=True,
71 )
72 skyMap = connectionTypes.Input(
73 doc="Input definition of geometry/bbox and projection/wcs for warped exposures",
74 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
75 storageClass="SkyMap",
76 dimensions=("skymap",),
77 )
78 direct = connectionTypes.Output(
79 doc=("Output direct warped exposure (previously called CoaddTempExp), produced by resampling ",
80 "calexps onto the skyMap patch geometry."),
81 name="{coaddName}Coadd_directWarp",
82 storageClass="ExposureF",
83 dimensions=("tract", "patch", "skymap", "visit", "instrument"),
84 )
85 psfMatched = connectionTypes.Output(
86 doc=("Output PSF-Matched warped exposure (previously called CoaddTempExp), produced by resampling ",
87 "calexps onto the skyMap patch geometry and PSF-matching to a model PSF."),
88 name="{coaddName}Coadd_psfMatchedWarp",
89 storageClass="ExposureF",
90 dimensions=("tract", "patch", "skymap", "visit", "instrument"),
91 )
92 visitSummary = connectionTypes.Input(
93 doc="Input visit-summary catalog with updated calibration objects.",
94 name="finalVisitSummary",
95 storageClass="ExposureCatalog",
96 dimensions=("instrument", "visit",),
97 )
99 def __init__(self, *, config=None):
100 if config.bgSubtracted:
101 del self.backgroundList
102 if not config.doApplySkyCorr:
103 del self.skyCorrList
104 if not config.makeDirect:
105 del self.direct
106 if not config.makePsfMatched:
107 del self.psfMatched
110class MakeWarpConfig(pipeBase.PipelineTaskConfig, CoaddBaseTask.ConfigClass,
111 pipelineConnections=MakeWarpConnections):
112 """Config for MakeWarpTask."""
114 warpAndPsfMatch = pexConfig.ConfigurableField(
115 target=WarpAndPsfMatchTask,
116 doc="Task to warp and PSF-match calexp",
117 )
118 doWrite = pexConfig.Field(
119 doc="persist <coaddName>Coadd_<warpType>Warp",
120 dtype=bool,
121 default=True,
122 )
123 bgSubtracted = pexConfig.Field(
124 doc="Work with a background subtracted calexp?",
125 dtype=bool,
126 default=True,
127 )
128 coaddPsf = pexConfig.ConfigField(
129 doc="Configuration for CoaddPsf",
130 dtype=CoaddPsfConfig,
131 )
132 makeDirect = pexConfig.Field(
133 doc="Make direct Warp/Coadds",
134 dtype=bool,
135 default=True,
136 )
137 makePsfMatched = pexConfig.Field(
138 doc="Make Psf-Matched Warp/Coadd?",
139 dtype=bool,
140 default=False,
141 )
142 modelPsf = GaussianPsfFactory.makeField(doc="Model Psf factory")
143 useVisitSummaryPsf = pexConfig.Field(
144 doc=(
145 "If True, use the PSF model and aperture corrections from the 'visitSummary' connection. "
146 "If False, use the PSF model and aperture corrections from the 'exposure' connection. "
147 ),
148 dtype=bool,
149 default=True,
150 )
151 doWriteEmptyWarps = pexConfig.Field(
152 dtype=bool,
153 default=False,
154 doc="Write out warps even if they are empty"
155 )
156 hasFakes = pexConfig.Field(
157 doc="Should be set to True if fake sources have been inserted into the input data.",
158 dtype=bool,
159 default=False,
160 )
161 doApplySkyCorr = pexConfig.Field(
162 dtype=bool,
163 default=False,
164 doc="Apply sky correction?",
165 )
166 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
168 def validate(self):
169 CoaddBaseTask.ConfigClass.validate(self)
171 if not self.makePsfMatched and not self.makeDirect:
172 raise ValueError("At least one of config.makePsfMatched and config.makeDirect must be True")
173 if self.warpAndPsfMatch.warp.cacheSize != self.coaddPsf.cacheSize:
174 # This is an incomplete check: usually the CoaddPsf cache size
175 # configured here in MakeWarpTask is superseded by the one in
176 # AssembleCoaddTask. A pipeline contract in the drp_pipe is
177 # present to check that.
178 raise ValueError("Image warping cache size and CoaddPSf warping cache size do not agree.")
180 def setDefaults(self):
181 CoaddBaseTask.ConfigClass.setDefaults(self)
182 self.warpAndPsfMatch.warp.cacheSize = 0
183 self.coaddPsf.cacheSize = 0
184 self.warpAndPsfMatch.psfMatch.kernel.active.kernelSize = self.matchingKernelSize
187class MakeWarpTask(CoaddBaseTask):
188 """Warp and optionally PSF-Match calexps onto an a common projection.
190 Warp and optionally PSF-Match calexps onto a common projection, by
191 performing the following operations:
192 - Group calexps by visit/run
193 - For each visit, generate a Warp by calling method @ref run.
194 `run` loops over the visit's calexps calling
195 `~lsst.pipe.tasks.warpAndPsfMatch.WarpAndPsfMatchTask` on each visit
197 """
198 ConfigClass = MakeWarpConfig
199 _DefaultName = "makeWarp"
201 def __init__(self, **kwargs):
202 CoaddBaseTask.__init__(self, **kwargs)
203 self.makeSubtask("warpAndPsfMatch")
204 if self.config.hasFakes:
205 self.calexpType = "fakes_calexp"
206 else:
207 self.calexpType = "calexp"
209 @utils.inheritDoc(pipeBase.PipelineTask)
210 def runQuantum(self, butlerQC, inputRefs, outputRefs):
211 # Docstring to be augmented with info from PipelineTask.runQuantum
212 """Notes
213 -----
214 Obtain the list of input detectors from calExpList. Sort them by
215 detector order (to ensure reproducibility). Then ensure all input
216 lists are in the same sorted detector order.
217 """
218 detectorOrder = [ref.datasetRef.dataId['detector'] for ref in inputRefs.calExpList]
219 detectorOrder.sort()
220 inputRefs = reorderRefs(inputRefs, detectorOrder, dataIdKey='detector')
222 # Read in all inputs.
223 inputs = butlerQC.get(inputRefs)
225 # Construct skyInfo expected by `run`. We remove the SkyMap itself
226 # from the dictionary so we can pass it as kwargs later.
227 skyMap = inputs.pop("skyMap")
228 quantumDataId = butlerQC.quantum.dataId
229 skyInfo = makeSkyInfo(skyMap, tractId=quantumDataId['tract'], patchId=quantumDataId['patch'])
231 # Construct list of input DataIds expected by `run`.
232 dataIdList = [ref.datasetRef.dataId for ref in inputRefs.calExpList]
233 # Construct list of packed integer IDs expected by `run`.
234 ccdIdList = [
235 self.config.idGenerator.apply(dataId).catalog_id
236 for dataId in dataIdList
237 ]
239 # Check early that the visitSummary contains everything we need.
240 visitSummary = inputs["visitSummary"]
241 bboxList = []
242 wcsList = []
243 for dataId in dataIdList:
244 row = visitSummary.find(dataId["detector"])
245 if row is None:
246 raise RuntimeError(
247 f"Unexpectedly incomplete visitSummary provided to makeWarp: {dataId} is missing."
248 )
249 bboxList.append(row.getBBox())
250 wcsList.append(row.getWcs())
251 inputs["bboxList"] = bboxList
252 inputs["wcsList"] = wcsList
254 # Do an initial selection on inputs with complete wcs/photoCalib info.
255 # Qualifying calexps will be read in the following call.
256 completeIndices = self._prepareCalibratedExposures(**inputs)
257 inputs = self.filterInputs(indices=completeIndices, inputs=inputs)
259 # Do another selection based on the configured selection task
260 # (using updated WCSs to determine patch overlap if an external
261 # calibration was applied).
262 cornerPosList = lsst.geom.Box2D(skyInfo.bbox).getCorners()
263 coordList = [skyInfo.wcs.pixelToSky(pos) for pos in cornerPosList]
264 goodIndices = self.select.run(**inputs, coordList=coordList, dataIds=dataIdList)
265 inputs = self.filterInputs(indices=goodIndices, inputs=inputs)
267 # Extract integer visitId requested by `run`.
268 visitId = dataIdList[0]["visit"]
270 results = self.run(**inputs,
271 visitId=visitId,
272 ccdIdList=[ccdIdList[i] for i in goodIndices],
273 dataIdList=[dataIdList[i] for i in goodIndices],
274 skyInfo=skyInfo)
275 if self.config.makeDirect and results.exposures["direct"] is not None:
276 butlerQC.put(results.exposures["direct"], outputRefs.direct)
277 if self.config.makePsfMatched and results.exposures["psfMatched"] is not None:
278 butlerQC.put(results.exposures["psfMatched"], outputRefs.psfMatched)
280 @timeMethod
281 def run(self, calExpList, ccdIdList, skyInfo, visitId=0, dataIdList=None, **kwargs):
282 """Create a Warp from inputs.
284 We iterate over the multiple calexps in a single exposure to construct
285 the warp (previously called a coaddTempExp) of that exposure to the
286 supplied tract/patch.
288 Pixels that receive no pixels are set to NAN; this is not correct
289 (violates LSST algorithms group policy), but will be fixed up by
290 interpolating after the coaddition.
292 calexpRefList : `list`
293 List of data references for calexps that (may)
294 overlap the patch of interest.
295 skyInfo : `lsst.pipe.base.Struct`
296 Struct from `~lsst.pipe.base.coaddBase.makeSkyInfo()` with
297 geometric information about the patch.
298 visitId : `int`
299 Integer identifier for visit, for the table that will
300 produce the CoaddPsf.
302 Returns
303 -------
304 result : `lsst.pipe.base.Struct`
305 Results as a struct with attributes:
307 ``exposures``
308 A dictionary containing the warps requested:
309 "direct": direct warp if ``config.makeDirect``
310 "psfMatched": PSF-matched warp if ``config.makePsfMatched``
311 (`dict`).
312 """
313 warpTypeList = self.getWarpTypeList()
315 totGoodPix = {warpType: 0 for warpType in warpTypeList}
316 didSetMetadata = {warpType: False for warpType in warpTypeList}
317 warps = {warpType: self._prepareEmptyExposure(skyInfo) for warpType in warpTypeList}
318 inputRecorder = {warpType: self.inputRecorder.makeCoaddTempExpRecorder(visitId, len(calExpList))
319 for warpType in warpTypeList}
321 modelPsf = self.config.modelPsf.apply() if self.config.makePsfMatched else None
322 if dataIdList is None:
323 dataIdList = ccdIdList
325 for calExpInd, (calExp, ccdId, dataId) in enumerate(zip(calExpList, ccdIdList, dataIdList)):
326 self.log.info("Processing calexp %d of %d for this Warp: id=%s",
327 calExpInd+1, len(calExpList), dataId)
328 try:
329 warpedAndMatched = self.warpAndPsfMatch.run(calExp, modelPsf=modelPsf,
330 wcs=skyInfo.wcs, maxBBox=skyInfo.bbox,
331 makeDirect=self.config.makeDirect,
332 makePsfMatched=self.config.makePsfMatched)
333 except Exception as e:
334 self.log.warning("WarpAndPsfMatch failed for calexp %s; skipping it: %s", dataId, e)
335 continue
336 try:
337 numGoodPix = {warpType: 0 for warpType in warpTypeList}
338 for warpType in warpTypeList:
339 exposure = warpedAndMatched.getDict()[warpType]
340 if exposure is None:
341 continue
342 warp = warps[warpType]
343 if didSetMetadata[warpType]:
344 mimg = exposure.getMaskedImage()
345 mimg *= (warp.getPhotoCalib().getInstFluxAtZeroMagnitude()
346 / exposure.getPhotoCalib().getInstFluxAtZeroMagnitude())
347 del mimg
348 numGoodPix[warpType] = coaddUtils.copyGoodPixels(
349 warp.getMaskedImage(), exposure.getMaskedImage(), self.getBadPixelMask())
350 totGoodPix[warpType] += numGoodPix[warpType]
351 self.log.debug("Calexp %s has %d good pixels in this patch (%.1f%%) for %s",
352 dataId, numGoodPix[warpType],
353 100.0*numGoodPix[warpType]/skyInfo.bbox.getArea(), warpType)
354 if numGoodPix[warpType] > 0 and not didSetMetadata[warpType]:
355 warp.info.id = exposure.info.id
356 warp.setPhotoCalib(exposure.getPhotoCalib())
357 warp.setFilter(exposure.getFilter())
358 warp.getInfo().setVisitInfo(exposure.getInfo().getVisitInfo())
359 # PSF replaced with CoaddPsf after loop if and only if
360 # creating direct warp.
361 warp.setPsf(exposure.getPsf())
362 didSetMetadata[warpType] = True
364 # Need inputRecorder for CoaddApCorrMap for both direct and
365 # PSF-matched.
366 inputRecorder[warpType].addCalExp(calExp, ccdId, numGoodPix[warpType])
368 except Exception as e:
369 self.log.warning("Error processing calexp %s; skipping it: %s", dataId, e)
370 continue
372 for warpType in warpTypeList:
373 self.log.info("%sWarp has %d good pixels (%.1f%%)",
374 warpType, totGoodPix[warpType], 100.0*totGoodPix[warpType]/skyInfo.bbox.getArea())
376 if totGoodPix[warpType] > 0 and didSetMetadata[warpType]:
377 inputRecorder[warpType].finish(warps[warpType], totGoodPix[warpType])
378 if warpType == "direct":
379 warps[warpType].setPsf(
380 CoaddPsf(inputRecorder[warpType].coaddInputs.ccds, skyInfo.wcs,
381 self.config.coaddPsf.makeControl()))
382 else:
383 if not self.config.doWriteEmptyWarps:
384 # No good pixels. Exposure still empty.
385 warps[warpType] = None
386 # NoWorkFound is unnecessary as the downstream tasks will
387 # adjust the quantum accordingly.
389 result = pipeBase.Struct(exposures=warps)
390 return result
392 def filterInputs(self, indices, inputs):
393 """Filter task inputs by their indices.
395 Parameters
396 ----------
397 indices : `list` [`int`]
398 inputs : `dict` [`list`]
399 A dictionary of input connections to be passed to run.
401 Returns
402 -------
403 inputs : `dict` [`list`]
404 Task inputs with their lists filtered by indices.
405 """
406 for key in inputs.keys():
407 # Only down-select on list inputs
408 if isinstance(inputs[key], list):
409 inputs[key] = [inputs[key][ind] for ind in indices]
410 return inputs
412 def _prepareCalibratedExposures(self, *, visitSummary, calExpList=[], wcsList=None,
413 backgroundList=None, skyCorrList=None, **kwargs):
414 """Calibrate and add backgrounds to input calExpList in place.
416 Parameters
417 ----------
418 visitSummary : `lsst.afw.table.ExposureCatalog`
419 Exposure catalog with potentially all calibrations. Attributes set
420 to `None` are ignored.
421 calExpList : `list` [`lsst.afw.image.Exposure` or
422 `lsst.daf.butler.DeferredDatasetHandle`]
423 Sequence of calexps to be modified in place.
424 wcsList : `list` [`lsst.afw.geom.SkyWcs`]
425 The WCSs of the calexps in ``calExpList``. These will be used to
426 determine if the calexp should be used in the warp. The list is
427 dynamically updated with the WCSs from the visitSummary.
428 backgroundList : `list` [`lsst.afw.math.backgroundList`], optional
429 Sequence of backgrounds to be added back in if bgSubtracted=False.
430 skyCorrList : `list` [`lsst.afw.math.backgroundList`], optional
431 Sequence of background corrections to be subtracted if
432 doApplySkyCorr=True.
433 **kwargs
434 Additional keyword arguments.
436 Returns
437 -------
438 indices : `list` [`int`]
439 Indices of ``calExpList`` and friends that have valid
440 photoCalib/skyWcs.
441 """
442 wcsList = len(calExpList)*[None] if wcsList is None else wcsList
443 backgroundList = len(calExpList)*[None] if backgroundList is None else backgroundList
444 skyCorrList = len(calExpList)*[None] if skyCorrList is None else skyCorrList
446 includeCalibVar = self.config.includeCalibVar
448 indices = []
449 for index, (calexp, background, skyCorr) in enumerate(zip(calExpList,
450 backgroundList,
451 skyCorrList)):
452 if isinstance(calexp, DeferredDatasetHandle):
453 calexp = calexp.get()
455 if not self.config.bgSubtracted:
456 calexp.maskedImage += background.getImage()
458 detectorId = calexp.info.getDetector().getId()
460 # Load all calibrations from visitSummary.
461 row = visitSummary.find(detectorId)
462 if row is None:
463 raise RuntimeError(
464 f"Unexpectedly incomplete visitSummary: detector={detectorId} is missing."
465 )
466 if (photoCalib := row.getPhotoCalib()) is not None:
467 calexp.setPhotoCalib(photoCalib)
468 else:
469 self.log.warning(
470 "Detector id %d for visit %d has None for photoCalib in the visitSummary and will "
471 "not be used in the warp", detectorId, row["visit"],
472 )
473 continue
474 if (skyWcs := row.getWcs()) is not None:
475 calexp.setWcs(skyWcs)
476 wcsList[index] = skyWcs
477 else:
478 self.log.warning(
479 "Detector id %d for visit %d has None for wcs in the visitSummary and will "
480 "not be used in the warp", detectorId, row["visit"],
481 )
482 continue
483 if self.config.useVisitSummaryPsf:
484 if (psf := row.getPsf()) is not None:
485 calexp.setPsf(psf)
486 else:
487 self.log.warning(
488 "Detector id %d for visit %d has None for psf in the visitSummary and will "
489 "not be used in the warp", detectorId, row["visit"],
490 )
491 continue
492 if (apCorrMap := row.getApCorrMap()) is not None:
493 calexp.info.setApCorrMap(apCorrMap)
494 else:
495 self.log.warning(
496 "Detector id %d for visit %d has None for apCorrMap in the visitSummary and will "
497 "not be used in the warp", detectorId, row["visit"],
498 )
499 continue
500 else:
501 if calexp.getPsf() is None:
502 self.log.warning(
503 "Detector id %d for visit %d has None for psf for the calexp and will "
504 "not be used in the warp", detectorId, row["visit"],
505 )
506 continue
507 if calexp.info.getApCorrMap() is None:
508 self.log.warning(
509 "Detector id %d for visit %d has None for apCorrMap in the calexp and will "
510 "not be used in the warp", detectorId, row["visit"],
511 )
512 continue
514 # Calibrate the image.
515 calexp.maskedImage = photoCalib.calibrateImage(calexp.maskedImage,
516 includeScaleUncertainty=includeCalibVar)
517 calexp.maskedImage /= photoCalib.getCalibrationMean()
518 # TODO: The images will have a calibration of 1.0 everywhere once
519 # RFC-545 is implemented.
520 # exposure.setCalib(afwImage.Calib(1.0))
522 # Apply skycorr
523 if self.config.doApplySkyCorr:
524 calexp.maskedImage -= skyCorr.getImage()
526 indices.append(index)
527 calExpList[index] = calexp
529 return indices
531 @staticmethod
532 def _prepareEmptyExposure(skyInfo):
533 """Produce an empty exposure for a given patch.
535 Parameters
536 ----------
537 skyInfo : `lsst.pipe.base.Struct`
538 Struct from `~lsst.pipe.base.coaddBase.makeSkyInfo()` with
539 geometric information about the patch.
541 Returns
542 -------
543 exp : `lsst.afw.image.exposure.ExposureF`
544 An empty exposure for a given patch.
545 """
546 exp = afwImage.ExposureF(skyInfo.bbox, skyInfo.wcs)
547 exp.getMaskedImage().set(numpy.nan, afwImage.Mask
548 .getPlaneBitMask("NO_DATA"), numpy.inf)
549 return exp
551 def getWarpTypeList(self):
552 """Return list of requested warp types per the config.
553 """
554 warpTypeList = []
555 if self.config.makeDirect:
556 warpTypeList.append("direct")
557 if self.config.makePsfMatched:
558 warpTypeList.append("psfMatched")
559 return warpTypeList
562def reorderRefs(inputRefs, outputSortKeyOrder, dataIdKey):
563 """Reorder inputRefs per outputSortKeyOrder.
565 Any inputRefs which are lists will be resorted per specified key e.g.,
566 'detector.' Only iterables will be reordered, and values can be of type
567 `lsst.pipe.base.connections.DeferredDatasetRef` or
568 `lsst.daf.butler.core.datasets.ref.DatasetRef`.
570 Returned lists of refs have the same length as the outputSortKeyOrder.
571 If an outputSortKey not in the inputRef, then it will be padded with None.
572 If an inputRef contains an inputSortKey that is not in the
573 outputSortKeyOrder it will be removed.
575 Parameters
576 ----------
577 inputRefs : `lsst.pipe.base.connections.QuantizedConnection`
578 Input references to be reordered and padded.
579 outputSortKeyOrder : `iterable`
580 Iterable of values to be compared with inputRef's dataId[dataIdKey].
581 dataIdKey : `str`
582 The data ID key in the dataRefs to compare with the outputSortKeyOrder.
584 Returns
585 -------
586 inputRefs : `lsst.pipe.base.connections.QuantizedConnection`
587 Quantized Connection with sorted DatasetRef values sorted if iterable.
588 """
589 for connectionName, refs in inputRefs:
590 if isinstance(refs, Iterable):
591 if hasattr(refs[0], "dataId"):
592 inputSortKeyOrder = [ref.dataId[dataIdKey] for ref in refs]
593 else:
594 inputSortKeyOrder = [ref.datasetRef.dataId[dataIdKey] for ref in refs]
595 if inputSortKeyOrder != outputSortKeyOrder:
596 setattr(inputRefs, connectionName,
597 reorderAndPadList(refs, inputSortKeyOrder, outputSortKeyOrder))
598 return inputRefs