Coverage for python/lsst/skymap/tractBuilder.py: 30%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# LSST Data Management System
2# Copyright 2008, 2009, 2010 LSST Corporation.
3#
4# This product includes software developed by the
5# LSST Project (http://www.lsst.org/).
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the LSST License Statement and
18# the GNU General Public License along with this program. If not,
19# see <http://www.lsstcorp.org/LegalNotices/>.
20#
21__all__ = ["tractBuilderRegistry", "BaseTractBuilder",
22 "LegacyTractBuilder", "CellTractBuilder"]
24import abc
25import numbers
26import struct
27from collections.abc import Iterable
29import lsst.pex.config as pexConfig
30import lsst.geom as geom
31from .patchInfo import PatchInfo
32from .detail import Index2D
35class BaseTractBuilderConfig(pexConfig.Config):
36 """Configuration that is to be shared amongst all tract builders."""
37 pass
40class BaseTractBuilder(metaclass=abc.ABCMeta):
41 """Base class for algorithms that define patches within the tract.
43 Parameters
44 ----------
45 config : `lsst.pexConfig.Config`
46 Input for configuring the algorithm
47 """
48 def __init__(self, config):
49 self.config = config
51 def setupPatches(self, minBBox, wcs):
52 """Set up the patches of a particular size in a tract.
54 We grow the tract bounding box to hold an exact multiple of
55 the desired size (patchInnerDimensions or
56 numCellsPerPatchInner*cellInnerDimensions), while keeping
57 the center roughly the same. We return the final tract
58 bounding box, and the number of patches in each dimension
59 (as an Index2D).
61 Parameters
62 ----------
63 minBBox : `lsst.geom.Box2I`
64 Minimum bounding box for tract
65 wcs : `lsst.afw.geom.SkyWcs`
66 Wcs object
68 Returns
69 -------
70 bbox : `lsst.geom.Box2I
71 final bounding box, number of patches
72 numPatches : `lsst.skymap.Index2D`
73 """
74 bbox = geom.Box2I(minBBox)
75 bboxMin = bbox.getMin()
76 bboxDim = bbox.getDimensions()
77 numPatchesList = [0, 0]
78 for i, innerDim in enumerate(self._patchInnerDimensions):
79 num = (bboxDim[i] + innerDim - 1) // innerDim # round up
80 deltaDim = (innerDim*num) - bboxDim[i]
81 if deltaDim > 0:
82 bboxDim[i] = innerDim * num
83 bboxMin[i] -= deltaDim // 2
84 numPatchesList[i] = num
85 numPatches = Index2D(*numPatchesList)
86 bbox = geom.Box2I(bboxMin, bboxDim)
87 self._numPatches = numPatches
88 # The final tract BBox starts at zero.
89 self._tractBBox = geom.Box2I(geom.Point2I(0, 0), bbox.getDimensions())
90 self._initialized = True
92 return bbox, numPatches
94 def getPatchBorder(self):
95 return self._patchBorder
97 @abc.abstractmethod
98 def getPatchInfo(self, index, tractWcs):
99 """Return information for the specified patch.
101 Parameters
102 ----------
103 index : `lsst.skymap.Index2D` or `Iterable` [`int`, `int`]
104 Index of patch, as Index2D or pair of ints;
105 or a sequential index as returned by getSequentialPatchIndex;
106 negative values are not supported.
107 tractWcs : `lsst.afw.geom.SkyWcs`
108 WCS associated with the tract.
110 Returns
111 -------
112 result : `lsst.skymap.PatchInfo`
113 The patch info for that index.
115 Raises
116 ------
117 IndexError
118 If index is out of range.
119 """
120 raise NotImplementedError("Must be implemented by a subclass")
122 def getPatchInnerDimensions(self):
123 """Get dimensions of inner region of the patches (all are the same)
124 """
125 return self._patchInnerDimensions
127 def getSequentialPatchIndex(self, patchInfo):
128 """Return a single integer that uniquely identifies
129 the given patch within this tract.
131 Parameters
132 ----------
133 patchInfo : `lsst.skymap.PatchInfo`
135 Returns
136 -------
137 sequentialIndex : `int`
138 """
139 index = patchInfo.getIndex()
140 return self.getSequentialPatchIndexFromPair(index)
142 def getSequentialPatchIndexFromPair(self, index):
143 """Return a single integer that uniquely identifies
144 the patch index within the tract.
146 Parameters
147 ----------
148 index : `lsst.skymap.Index2D` or `Iterable` [`int`, `int`]
150 Returns
151 -------
152 sequentialIndex : `int`
153 """
154 if isinstance(index, Index2D):
155 _index = index
156 else:
157 if not isinstance(index, Iterable):
158 raise ValueError("Input index is not an iterable.")
159 if len(index) != 2:
160 raise ValueError("Input index does not have two values.")
161 _index = Index2D(*index)
162 nx, ny = self._numPatches
163 return nx*_index.y + _index.x
165 def getPatchIndexPair(self, sequentialIndex):
166 """Convert sequential index into patch index (x,y) pair.
168 Parameters
169 ----------
170 sequentialIndex : `int`
172 Returns
173 -------
174 x, y : `lsst.skymap.Index2D`
175 """
176 nx, ny = self._numPatches
177 x = sequentialIndex % nx
178 y = sequentialIndex // nx
179 return Index2D(x=x, y=y)
181 @abc.abstractmethod
182 def getPackedConfig(self, config):
183 """Get a packed config suitable for using in a sha1.
185 Parameters
186 ----------
187 config : `lsst.skymap.BaseTractBuilderConfig`
189 Returns
190 -------
191 configPacked : `bytes`
192 """
193 raise NotImplementedError("Must be implemented by a subclass")
196class LegacyTractBuilderConfig(BaseTractBuilderConfig):
197 patchInnerDimensions = pexConfig.ListField(
198 doc="dimensions of inner region of patches (x,y pixels)",
199 dtype=int,
200 length=2,
201 default=(4000, 4000),
202 )
203 patchBorder = pexConfig.Field(
204 doc="border between patch inner and outer bbox (pixels)",
205 dtype=int,
206 default=100,
207 )
210class LegacyTractBuilder(BaseTractBuilder):
211 ConfigClass = LegacyTractBuilderConfig
213 def __init__(self, config):
214 super().__init__(config)
216 self._patchInnerDimensions = geom.Extent2I(*(val
217 for val in config.patchInnerDimensions))
218 self._patchBorder = config.patchBorder
219 self._initialized = False
221 def getPatchInfo(self, index, tractWcs):
222 # This should always be initialized
223 if not self._initialized:
224 raise RuntimeError("Programmer error; this should always be initialized.")
225 if isinstance(index, Index2D):
226 _index = index
227 else:
228 if isinstance(index, numbers.Number):
229 _index = self.getPatchIndexPair(index)
230 else:
231 _index = Index2D(*index)
232 if (not 0 <= _index.x < self._numPatches.x) \
233 or (not 0 <= _index.y < self._numPatches.y):
234 raise IndexError("Patch index %s is not in range [0-%d, 0-%d]" %
235 (_index, self._numPatches.x - 1, self._numPatches.y - 1))
236 innerMin = geom.Point2I(*[_index[i] * self._patchInnerDimensions[i] for i in range(2)])
237 innerBBox = geom.Box2I(innerMin, self._patchInnerDimensions)
238 if not self._tractBBox.contains(innerBBox):
239 raise RuntimeError(
240 "Bug: patch index %s valid but inner bbox=%s not contained in tract bbox=%s" %
241 (_index, innerBBox, self._tractBBox))
242 outerBBox = geom.Box2I(innerBBox)
243 outerBBox.grow(self.getPatchBorder())
244 outerBBox.clip(self._tractBBox)
245 return PatchInfo(
246 index=_index,
247 innerBBox=innerBBox,
248 outerBBox=outerBBox,
249 sequentialIndex=self.getSequentialPatchIndexFromPair(_index),
250 tractWcs=tractWcs
251 )
253 def getPackedConfig(self, config):
254 subConfig = config.tractBuilder[config.tractBuilder.name]
255 configPacked = struct.pack(
256 "<iiidd3sd",
257 subConfig.patchInnerDimensions[0],
258 subConfig.patchInnerDimensions[1],
259 subConfig.patchBorder,
260 config.tractOverlap,
261 config.pixelScale,
262 config.projection.encode('ascii'),
263 config.rotation
264 )
266 return configPacked
269class CellTractBuilderConfig(BaseTractBuilderConfig):
270 cellInnerDimensions = pexConfig.ListField(
271 doc="dimensions of inner region of cells (x,y pixels)",
272 dtype=int,
273 length=2,
274 default=(150, 150),
275 )
276 cellBorder = pexConfig.Field(
277 doc="Border between cell inner and outer bbox (pixels)",
278 dtype=int,
279 default=50,
280 )
281 numCellsPerPatchInner = pexConfig.Field(
282 doc="Number of cells per inner patch.",
283 dtype=int,
284 default=20,
285 )
286 numCellsInPatchBorder = pexConfig.Field(
287 doc="Number of cells in the patch border (outside the inner patch region).",
288 dtype=int,
289 default=1,
290 )
292 def validate(self):
293 if len(self.cellInnerDimensions) != 2:
294 raise ValueError("cellInnerDimensions must be 2 ints.")
296 if self.cellInnerDimensions[0] != self.cellInnerDimensions[1]:
297 raise ValueError("cellInnerDimensions must be equal (for square cells).")
300class CellTractBuilder(BaseTractBuilder):
301 ConfigClass = CellTractBuilderConfig
303 def __init__(self, config):
304 super().__init__(config)
306 self._cellInnerDimensions = geom.Extent2I(*(val
307 for val in config.cellInnerDimensions))
308 self._cellBorder = config.cellBorder
309 self._numCellsPerPatchInner = config.numCellsPerPatchInner
310 self._numCellsInPatchBorder = config.numCellsInPatchBorder
311 self._patchInnerDimensions = geom.Extent2I(*(val*self._numCellsPerPatchInner
312 for val in config.cellInnerDimensions))
313 # The patch border is the number of cells in the border + the cell border
314 self._patchBorder = config.numCellsInPatchBorder*config.cellInnerDimensions[0] + self._cellBorder
315 self._initialized = False
317 def getPatchInfo(self, index, tractWcs):
318 # This should always be initialized
319 if not self._initialized:
320 raise RuntimeError("Programmer error; this should always be initialized.")
321 if isinstance(index, Index2D):
322 _index = index
323 else:
324 if isinstance(index, numbers.Number):
325 _index = self.getPatchIndexPair(index)
326 else:
327 _index = Index2D(*index)
328 if (not 0 <= _index.x < self._numPatches.x) \
329 or (not 0 <= _index.y < self._numPatches.y):
330 raise IndexError("Patch index %s is not in range [0-%d, 0-%d]" %
331 (_index, self._numPatches.x - 1, self._numPatches.y - 1))
332 innerMin = geom.Point2I(*[_index[i]*self._patchInnerDimensions[i] for i in range(2)])
333 innerBBox = geom.Box2I(innerMin, self._patchInnerDimensions)
334 if not self._tractBBox.contains(innerBBox):
335 raise RuntimeError(
336 "Bug: patch index %s valid but inner bbox=%s not contained in tract bbox=%s" %
337 (_index, innerBBox, self._tractBBox))
338 outerBBox = geom.Box2I(innerBBox)
339 outerBBox.grow(self.getPatchBorder())
340 # We do not clip the patch for cell-based tracts.
341 return PatchInfo(
342 index=_index,
343 innerBBox=innerBBox,
344 outerBBox=outerBBox,
345 sequentialIndex=self.getSequentialPatchIndexFromPair(_index),
346 tractWcs=tractWcs,
347 cellInnerDimensions=self._cellInnerDimensions,
348 cellBorder=self._cellBorder,
349 numCellsPerPatchInner=self._numCellsPerPatchInner,
350 numCellsInPatchBorder=self._numCellsInPatchBorder
351 )
353 def getPackedConfig(self, config):
354 subConfig = config.tractBuilder[config.tractBuilder.name]
355 configPacked = struct.pack(
356 "<iiiiidd3sd",
357 subConfig.cellInnerDimensions[0],
358 subConfig.cellInnerDimensions[1],
359 subConfig.cellBorder,
360 subConfig.numCellsPerPatchInner,
361 subConfig.numCellsInPatchBorder,
362 config.tractOverlap,
363 config.pixelScale,
364 config.projection.encode('ascii'),
365 config.rotation
366 )
368 return configPacked
371tractBuilderRegistry = pexConfig.makeRegistry(
372 doc="A registry of Tract Builders (subclasses of BaseTractBuilder)",
373)
375tractBuilderRegistry.register("legacy", LegacyTractBuilder)
376tractBuilderRegistry.register("cells", CellTractBuilder)