LSSTApplications  11.0-13-gbb96280,12.1+18,12.1+7,12.1-1-g14f38d3+72,12.1-1-g16c0db7+5,12.1-1-g5961e7a+84,12.1-1-ge22e12b+23,12.1-11-g06625e2+4,12.1-11-g0d7f63b+4,12.1-19-gd507bfc,12.1-2-g7dda0ab+38,12.1-2-gc0bc6ab+81,12.1-21-g6ffe579+2,12.1-21-gbdb6c2a+4,12.1-24-g941c398+5,12.1-3-g57f6835+7,12.1-3-gf0736f3,12.1-37-g3ddd237,12.1-4-gf46015e+5,12.1-5-g06c326c+20,12.1-5-g648ee80+3,12.1-5-gc2189d7+4,12.1-6-ga608fc0+1,12.1-7-g3349e2a+5,12.1-7-gfd75620+9,12.1-9-g577b946+5,12.1-9-gc4df26a+10
LSSTDataManagementBasePackage
cameraMapper.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 
23 from builtins import str
24 from past.builtins import long
25 import copy
26 import errno
27 import glob
28 import os
29 import pyfits # required by _makeDefectsDict until defects are written as AFW tables
30 import re
31 import shutil
32 import weakref
33 import lsst.daf.persistence as dafPersist
34 from . import ImageMapping, ExposureMapping, CalibrationMapping, DatasetMapping
35 import lsst.daf.base as dafBase
36 import lsst.afw.geom as afwGeom
37 import lsst.afw.image as afwImage
38 import lsst.afw.table as afwTable
39 import lsst.afw.cameraGeom as afwCameraGeom
40 import lsst.log as lsstLog
41 import lsst.pex.policy as pexPolicy
42 from .exposureIdInfo import ExposureIdInfo
43 from .makeRawVisitInfo import MakeRawVisitInfo
44 from lsst.utils import getPackageDir
45 
46 """This module defines the CameraMapper base class."""
47 
48 
49 class CameraMapper(dafPersist.Mapper):
50 
51  """CameraMapper is a base class for mappers that handle images from a
52  camera and products derived from them. This provides an abstraction layer
53  between the data on disk and the code.
54 
55  Public methods: keys, queryMetadata, getDatasetTypes, map,
56  canStandardize, standardize
57 
58  Mappers for specific data sources (e.g., CFHT Megacam, LSST
59  simulations, etc.) should inherit this class.
60 
61  The CameraMapper manages datasets within a "root" directory. It can also
62  be given an "outputRoot". If so, the input root is linked into the
63  outputRoot directory using a symlink named "_parent"; writes go into the
64  outputRoot while reads can come from either the root or outputRoot. As
65  outputRoots are used as inputs for further processing, the chain of
66  _parent links allows any dataset to be retrieved. Note that writing to a
67  dataset present in the input root will hide the existing dataset but not
68  overwrite it. See #2160 for design discussion.
69 
70  A camera is assumed to consist of one or more rafts, each composed of
71  multiple CCDs. Each CCD is in turn composed of one or more amplifiers
72  (amps). A camera is also assumed to have a camera geometry description
73  (CameraGeom object) as a policy file, a filter description (Filter class
74  static configuration) as another policy file, and an optional defects
75  description directory.
76 
77  Information from the camera geometry and defects are inserted into all
78  Exposure objects returned.
79 
80  The mapper uses one or two registries to retrieve metadata about the
81  images. The first is a registry of all raw exposures. This must contain
82  the time of the observation. One or more tables (or the equivalent)
83  within the registry are used to look up data identifier components that
84  are not specified by the user (e.g. filter) and to return results for
85  metadata queries. The second is an optional registry of all calibration
86  data. This should contain validity start and end entries for each
87  calibration dataset in the same timescale as the observation time.
88 
89  Subclasses will typically set MakeRawVisitInfoClass:
90 
91  MakeRawVisitInfoClass: a class variable that points to a subclass of
92  MakeRawVisitInfo, a functor that creates an
93  lsst.afw.image.VisitInfo from the FITS metadata of a raw image.
94 
95  Subclasses must provide the following methods:
96 
97  _extractDetectorName(self, dataId): returns the detector name for a CCD
98  (e.g., "CFHT 21", "R:1,2 S:3,4") as used in the AFW CameraGeom class given
99  a dataset identifier referring to that CCD or a subcomponent of it.
100 
101  _computeCcdExposureId(self, dataId): see below
102 
103  _computeCoaddExposureId(self, dataId, singleFilter): see below
104 
105  Subclasses may also need to override the following methods:
106 
107  _transformId(self, dataId): transformation of a data identifier
108  from colloquial usage (e.g., "ccdname") to proper/actual usage (e.g., "ccd"),
109  including making suitable for path expansion (e.g. removing commas).
110  The default implementation does nothing. Note that this
111  method should not modify its input parameter.
112 
113  getShortCcdName(self, ccdName): a static method that returns a shortened name
114  suitable for use as a filename. The default version converts spaces to underscores.
115 
116  _getCcdKeyVal(self, dataId): return a CCD key and value
117  by which to look up defects in the defects registry.
118  The default value returns ("ccd", detector name)
119 
120  _mapActualToPath(self, template, actualId): convert a template path to an
121  actual path, using the actual dataset identifier.
122 
123  The mapper's behaviors are largely specified by the policy file.
124  See the MapperDictionary.paf for descriptions of the available items.
125 
126  The 'exposures', 'calibrations', and 'datasets' subpolicies configure
127  mappings (see Mappings class).
128 
129  Common default mappings for all subclasses can be specified in the
130  "policy/{images,exposures,calibrations,datasets}.yaml" files. This provides
131  a simple way to add a product to all camera mappers.
132 
133  Functions to map (provide a path to the data given a dataset
134  identifier dictionary) and standardize (convert data into some standard
135  format or type) may be provided in the subclass as "map_{dataset type}"
136  and "std_{dataset type}", respectively.
137 
138  If non-Exposure datasets cannot be retrieved using standard
139  daf_persistence methods alone, a "bypass_{dataset type}" function may be
140  provided in the subclass to return the dataset instead of using the
141  "datasets" subpolicy.
142 
143  Implementations of map_camera and bypass_camera that should typically be
144  sufficient are provided in this base class.
145 
146  @todo
147  * Handle defects the same was as all other calibration products, using the calibration registry
148  * Instead of auto-loading the camera at construction time, load it from the calibration registry
149  * Rewrite defects as AFW tables so we don't need pyfits to unpersist them; then remove all mention
150  of pyfits from this package.
151  """
152  packageName = None
153 
154  # a class or subclass of MakeRawVisitInfo, a functor that makes an
155  # lsst.afw.image.VisitInfo from the FITS metadata of a raw image
156  MakeRawVisitInfoClass = MakeRawVisitInfo
157 
158  def __init__(self, policy, repositoryDir,
159  root=None, registry=None, calibRoot=None, calibRegistry=None,
160  provided=None, outputRoot=None):
161  """Initialize the CameraMapper.
162  @param policy (daf_persistence.Policy, or pexPolicy.Policy (only for backward compatibility))
163  Policy with per-camera defaults already merged
164  @param repositoryDir (string) Policy repository for the subclassing
165  module (obtained with getRepositoryPath() on the
166  per-camera default dictionary)
167  @param root (string) Root directory for data
168  @param registry (string) Path to registry with data's metadata
169  @param calibRoot (string) Root directory for calibrations
170  @param calibRegistry (string) Path to registry with calibrations'
171  metadata
172  @param provided (list of strings) Keys provided by the mapper
173  @param outputRoot (string) Root directory for output data
174  """
175 
176  dafPersist.Mapper.__init__(self)
177 
178  self.log = lsstLog.Log.getLogger("CameraMapper")
179 
180  self.root = root
181  if isinstance(policy, pexPolicy.Policy):
182  policy = dafPersist.Policy(policy)
183 
184  repoPolicy = CameraMapper.getRepoPolicy(self.root, self.root)
185  if repoPolicy is not None:
186  policy.update(repoPolicy)
187 
188  defaultPolicyFile = dafPersist.Policy.defaultPolicyFile("obs_base",
189  "MapperDictionary.paf",
190  "policy")
191  dictPolicy = dafPersist.Policy(defaultPolicyFile)
192  policy.merge(dictPolicy)
193 
194  # Levels
195  self.levels = dict()
196  if 'levels' in policy:
197  levelsPolicy = policy['levels']
198  for key in levelsPolicy.names(True):
199  self.levels[key] = set(levelsPolicy.asArray(key))
200  self.defaultLevel = policy['defaultLevel']
201  self.defaultSubLevels = dict()
202  if 'defaultSubLevels' in policy:
203  self.defaultSubLevels = policy['defaultSubLevels']
204 
205  # Root directories
206  if root is None:
207  root = "."
208  root = dafPersist.LogicalLocation(root).locString()
209 
210  if outputRoot is not None and os.path.abspath(outputRoot) != os.path.abspath(root):
211  # Path manipulations are subject to race condition
212  if not os.path.exists(outputRoot):
213  try:
214  os.makedirs(outputRoot)
215  except OSError as e:
216  if not e.errno == errno.EEXIST:
217  raise
218  if not os.path.exists(outputRoot):
219  raise RuntimeError("Unable to create output repository '%s'" % (outputRoot,))
220  if os.path.exists(root):
221  # Symlink existing input root to "_parent" in outputRoot.
222  src = os.path.abspath(root)
223  dst = os.path.join(outputRoot, "_parent")
224  if not os.path.exists(dst):
225  try:
226  os.symlink(src, dst)
227  except OSError:
228  pass
229  if os.path.exists(dst):
230  if os.path.realpath(dst) != os.path.realpath(src):
231  raise RuntimeError("Output repository path "
232  "'%s' already exists and differs from "
233  "input repository path '%s'" % (dst, src))
234  else:
235  raise RuntimeError("Unable to symlink from input "
236  "repository path '%s' to output repository "
237  "path '%s'" % (src, dst))
238  # We now use the outputRoot as the main root with access to the
239  # input via "_parent".
240  root = outputRoot
241 
242  if calibRoot is None:
243  if 'calibRoot' in policy:
244  calibRoot = policy['calibRoot']
245  calibRoot = dafPersist.LogicalLocation(calibRoot).locString()
246  else:
247  calibRoot = root
248 
249  if not os.path.exists(root):
250  self.log.warn("Root directory not found: %s", root)
251  if not os.path.exists(calibRoot):
252  self.log.warn("Calibration root directory not found: %s", calibRoot)
253 
254  self.root = root
255 
256  # Registries
257  self.registry = self._setupRegistry("registry", registry, policy, "registryPath", root)
258  if 'needCalibRegistry' in policy and policy['needCalibRegistry']:
259  calibRegistry = self._setupRegistry("calibRegistry", calibRegistry, policy,
260  "calibRegistryPath", calibRoot)
261  else:
262  calibRegistry = None
263 
264  # Dict of valid keys and their value types
265  self.keyDict = dict()
266 
267  self._initMappings(policy, root, calibRoot, calibRegistry, provided=None)
268 
269  # Camera geometry
270  self.cameraDataLocation = None # path to camera geometry config file
271  self.camera = self._makeCamera(policy=policy, repositoryDir=repositoryDir)
272 
273  # Defect registry and root
274  self.defectRegistry = None
275  if 'defects' in policy:
276  self.defectPath = os.path.join(repositoryDir, policy['defects'])
277  defectRegistryLocation = os.path.join(self.defectPath, "defectRegistry.sqlite3")
278  self.defectRegistry = dafPersist.Registry.create(defectRegistryLocation)
279 
280  # Filter translation table
281  self.filters = None
282 
283  # Skytile policy
284  self.skypolicy = policy['skytiles']
285 
286  # verify that the class variable packageName is set before attempting
287  # to instantiate an instance
288  if self.packageName is None:
289  raise ValueError('class variable packageName must not be None')
290 
292 
293  def _initMappings(self, policy, root=None, calibRoot=None, calibRegistry=None, provided=None):
294  """Initialize mappings
295 
296  For each of the dataset types that we want to be able to read, there are
297  methods that can be created to support them:
298  * map_<dataset> : determine the path for dataset
299  * std_<dataset> : standardize the retrieved dataset
300  * bypass_<dataset> : retrieve the dataset (bypassing the usual retrieval machinery)
301  * query_<dataset> : query the registry
302 
303  Besides the dataset types explicitly listed in the policy, we create
304  additional, derived datasets for additional conveniences, e.g., reading
305  the header of an image, retrieving only the size of a catalog.
306 
307  @param policy (Policy) Policy with per-camera defaults already merged
308  @param root (string) Root directory for data
309  @param calibRoot (string) Root directory for calibrations
310  @param calibRegistry (string) Path to registry with calibrations' metadata
311  @param provided (list of strings) Keys provided by the mapper
312  """
313  # Sub-dictionaries (for exposure/calibration/dataset types)
314  imgMappingPolicy = dafPersist.Policy(dafPersist.Policy.defaultPolicyFile(
315  "obs_base", "ImageMappingDictionary.paf", "policy"))
316  expMappingPolicy = dafPersist.Policy(dafPersist.Policy.defaultPolicyFile(
317  "obs_base", "ExposureMappingDictionary.paf", "policy"))
318  calMappingPolicy = dafPersist.Policy(dafPersist.Policy.defaultPolicyFile(
319  "obs_base", "CalibrationMappingDictionary.paf", "policy"))
320  dsMappingPolicy = dafPersist.Policy(dafPersist.Policy.defaultPolicyFile(
321  "obs_base", "DatasetMappingDictionary.paf", "policy"))
322 
323  # Mappings
324  mappingList = (
325  ("images", imgMappingPolicy, ImageMapping),
326  ("exposures", expMappingPolicy, ExposureMapping),
327  ("calibrations", calMappingPolicy, CalibrationMapping),
328  ("datasets", dsMappingPolicy, DatasetMapping)
329  )
330  self.mappings = dict()
331  for name, defPolicy, cls in mappingList:
332  if name in policy:
333  datasets = policy[name]
334 
335  # Centrally-defined datasets
336  defaultsPath = os.path.join(getPackageDir("obs_base"), "policy", name + ".yaml")
337  if os.path.exists(defaultsPath):
338  datasets.merge(dafPersist.Policy(defaultsPath))
339 
340  mappings = dict()
341  setattr(self, name, mappings)
342  for datasetType in datasets.names(True):
343  subPolicy = datasets[datasetType]
344  subPolicy.merge(defPolicy)
345 
346  if not hasattr(self, "map_" + datasetType) and 'composite' in subPolicy:
347  def compositeClosure(dataId, write=False, mapper=None, mapping=None, subPolicy=subPolicy):
348  components = subPolicy.get('composite')
349  assembler = subPolicy['assembler'] if 'assembler' in subPolicy else None
350  disassembler = subPolicy['disassembler'] if 'disassembler' in subPolicy else None
351  python = subPolicy['python']
352  butlerComposite = dafPersist.ButlerComposite(assembler=assembler,
353  disassembler=disassembler,
354  python=python,
355  dataId=dataId,
356  mapper=self)
357  for name, component in components.items():
358  butlerComposite.add(id=name,
359  datasetType=component.get('datasetType'),
360  setter=component.get('setter', None),
361  getter=component.get('getter', None),
362  subset=component.get('subset', False),
363  inputOnly=component.get('inputOnly', False))
364  return butlerComposite
365  setattr(self, "map_" + datasetType, compositeClosure)
366  # for now at least, don't set up any other handling for this dataset type.
367  continue
368 
369  if name == "calibrations":
370  mapping = cls(datasetType, subPolicy, self.registry, calibRegistry, calibRoot,
371  provided=provided)
372  else:
373  mapping = cls(datasetType, subPolicy, self.registry, root, provided=provided)
374  self.keyDict.update(mapping.keys())
375  mappings[datasetType] = mapping
376  self.mappings[datasetType] = mapping
377  if not hasattr(self, "map_" + datasetType):
378  def mapClosure(dataId, write=False, mapper=weakref.proxy(self), mapping=mapping):
379  return mapping.map(mapper, dataId, write)
380  setattr(self, "map_" + datasetType, mapClosure)
381  if not hasattr(self, "query_" + datasetType):
382  def queryClosure(format, dataId, mapping=mapping):
383  return mapping.lookup(format, dataId)
384  setattr(self, "query_" + datasetType, queryClosure)
385  if hasattr(mapping, "standardize") and not hasattr(self, "std_" + datasetType):
386  def stdClosure(item, dataId, mapper=weakref.proxy(self), mapping=mapping):
387  return mapping.standardize(mapper, item, dataId)
388  setattr(self, "std_" + datasetType, stdClosure)
389 
390  def setMethods(suffix, mapImpl=None, bypassImpl=None, queryImpl=None):
391  """Set convenience methods on CameraMapper"""
392  mapName = "map_" + datasetType + "_" + suffix
393  bypassName = "bypass_" + datasetType + "_" + suffix
394  queryName = "query_" + datasetType + "_" + suffix
395  if not hasattr(self, mapName):
396  setattr(self, mapName, mapImpl or getattr(self, "map_" + datasetType))
397  if not hasattr(self, bypassName):
398  if bypassImpl is None and hasattr(self, "bypass_" + datasetType):
399  bypassImpl = getattr(self, "bypass_" + datasetType)
400  if bypassImpl is not None:
401  setattr(self, bypassName, bypassImpl)
402  if not hasattr(self, queryName):
403  setattr(self, queryName, queryImpl or getattr(self, "query_" + datasetType))
404 
405  # Filename of dataset
406  setMethods("filename", bypassImpl=lambda datasetType, pythonType, location, dataId:
407  location.getLocations())
408 
409  # Metadata from FITS file
410  if subPolicy["storage"] == "FitsStorage": # a FITS image
411  setMethods("md", bypassImpl=lambda datasetType, pythonType, location, dataId:
412  afwImage.readMetadata(location.getLocations()[0]))
413  if name == "exposures":
414  setMethods("wcs", bypassImpl=lambda datasetType, pythonType, location, dataId:
415  afwImage.makeWcs(afwImage.readMetadata(location.getLocations()[0])))
416  setMethods("calib", bypassImpl=lambda datasetType, pythonType, location, dataId:
417  afwImage.Calib(afwImage.readMetadata(location.getLocations()[0])))
418  setMethods("visitInfo",
419  bypassImpl=lambda datasetType, pythonType, location, dataId:
420  afwImage.VisitInfo(afwImage.readMetadata(location.getLocations()[0])))
421  if subPolicy["storage"] == "FitsCatalogStorage": # a FITS catalog
422  setMethods("md", bypassImpl=lambda datasetType, pythonType, location, dataId:
423  afwImage.readMetadata(location.getLocations()[0], 2))
424 
425  # Sub-images
426  if subPolicy["storage"] == "FitsStorage":
427  def mapSubClosure(dataId, write=False, mapper=weakref.proxy(self), mapping=mapping):
428  subId = dataId.copy()
429  del subId['bbox']
430  loc = mapping.map(mapper, subId, write)
431  bbox = dataId['bbox']
432  llcX = bbox.getMinX()
433  llcY = bbox.getMinY()
434  width = bbox.getWidth()
435  height = bbox.getHeight()
436  loc.additionalData.set('llcX', llcX)
437  loc.additionalData.set('llcY', llcY)
438  loc.additionalData.set('width', width)
439  loc.additionalData.set('height', height)
440  if 'imageOrigin' in dataId:
441  loc.additionalData.set('imageOrigin',
442  dataId['imageOrigin'])
443  return loc
444  def querySubClosure(key, format, dataId, mapping=mapping):
445  subId = dataId.copy()
446  del subId['bbox']
447  return mapping.lookup(format, subId)
448  setMethods("sub", mapImpl=mapSubClosure, queryImpl=querySubClosure)
449 
450  if subPolicy["storage"] == "FitsCatalogStorage":
451  # Length of catalog
452  setMethods("len", bypassImpl=lambda datasetType, pythonType, location, dataId:
453  afwImage.readMetadata(location.getLocations()[0], 2).get("NAXIS2"))
454 
455  # Schema of catalog
456  if not datasetType.endswith("_schema") and datasetType + "_schema" not in datasets:
457  setMethods("schema", bypassImpl=lambda datasetType, pythonType, location, dataId:
458  afwTable.Schema.readFits(location.getLocations()[0]))
459 
460 
461  def _computeCcdExposureId(self, dataId):
462  """Compute the 64-bit (long) identifier for a CCD exposure.
463 
464  Subclasses must override
465 
466  @param dataId (dict) Data identifier with visit, ccd
467  """
468  raise NotImplementedError()
469 
470  def _computeCoaddExposureId(self, dataId, singleFilter):
471  """Compute the 64-bit (long) identifier for a coadd.
472 
473  Subclasses must override
474 
475  @param dataId (dict) Data identifier with tract and patch.
476  @param singleFilter (bool) True means the desired ID is for a single-
477  filter coadd, in which case dataId
478  must contain filter.
479  """
480  raise NotImplementedError()
481 
482  @staticmethod
483  def getRepoPolicy(root, repos):
484  """Get the policy stored in a repo (specified by 'root'), if there is one.
485 
486  @param root (string) path to the root location of the repository
487  @param repos (string) path from the root of the repo to the folder containing a file named
488  _policy.paf or _policy.yaml
489  @return (lsst.daf.persistence.Policy or None) A Policy instantiated with the policy found according to
490  input variables, or None if a policy file was not found.
491  """
492  policy = None
493  if root is not None:
494  paths = CameraMapper.parentSearch(root, os.path.join(repos, '_policy.*'))
495  if paths is not None:
496  for postfix in ('.yaml', '.paf'):
497  matches = [path for path in paths if (os.path.splitext(path))[1] == postfix]
498  if len(matches) > 1:
499  raise RuntimeError("More than 1 policy possibility for root:%s" % root)
500  elif len(matches) == 1:
501  policy = dafPersist.Policy(matches[0])
502  break
503  return policy
504 
505  def _parentSearch(self, path):
506  return CameraMapper.parentSearch(self.root, path)
507 
508  @staticmethod
509  def parentSearch(root, path):
510  """Look for the given path in the current root or any of its parents
511  by following "_parent" symlinks; return None if it can't be found. A
512  little tricky because the path may be in an alias of the root (e.g.
513  ".") and because the "_parent" links go between the root and the rest
514  of the path.
515  If the path contains an HDU indicator (a number in brackets before the
516  dot, e.g. 'foo.fits[1]', this will be stripped when searching and so
517  will match filenames without the HDU indicator, e.g. 'foo.fits'. The
518  path returned WILL contain the indicator though, e.g. ['foo.fits[1]'].
519  """
520  # Separate path into a root-equivalent prefix (in dir) and the rest
521  # (left in path)
522 
523  rootDir = root
524  # First remove trailing slashes (#2527)
525  while len(rootDir) > 1 and rootDir[-1] == '/':
526  rootDir = rootDir[:-1]
527 
528  if path.startswith(rootDir + "/"):
529  # Common case; we have the same root prefix string
530  path = path[len(rootDir)+1:]
531  dir = rootDir
532  elif rootDir == "/" and path.startswith("/"):
533  path = path[1:]
534  dir = rootDir
535  else:
536  # Search for prefix that is the same as root
537  pathPrefix = os.path.dirname(path)
538  while pathPrefix != "" and pathPrefix != "/":
539  if os.path.realpath(pathPrefix) == os.path.realpath(root):
540  break
541  pathPrefix = os.path.dirname(pathPrefix)
542  if os.path.realpath(pathPrefix) != os.path.realpath(root):
543  # No prefix matching root, don't search for parents
544  paths = glob.glob(path)
545 
546  # The contract states that `None` will be returned
547  # if no matches are found.
548  # Thus we explicitly set up this if/else to return `None`
549  # if `not paths` instead of `[]`.
550  # An argument could be made that the contract should be changed
551  if paths:
552  return paths
553  else:
554  return None
555  if pathPrefix == "/":
556  path = path[1:]
557  elif pathPrefix != "":
558  path = path[len(pathPrefix)+1:]
559  # If pathPrefix == "", then the current directory is the root
560  dir = pathPrefix
561 
562  # Now search for the path in the root or its parents
563  # Strip off any cfitsio bracketed extension if present
564  strippedPath = path
565  pathStripped = None
566  firstBracket = path.find("[")
567  if firstBracket != -1:
568  strippedPath = path[:firstBracket]
569  pathStripped = path[firstBracket:]
570 
571  while True:
572  paths = glob.glob(os.path.join(dir, strippedPath))
573  if len(paths) > 0:
574  if pathStripped is not None:
575  paths = [p + pathStripped for p in paths]
576  return paths
577  dir = os.path.join(dir, "_parent")
578  if not os.path.exists(dir):
579  return None
580 
581  def backup(self, datasetType, dataId):
582  """Rename any existing object with the given type and dataId.
583 
584  The CameraMapper implementation saves objects in a sequence of e.g.:
585  foo.fits
586  foo.fits~1
587  foo.fits~2
588  All of the backups will be placed in the output repo, however, and will
589  not be removed if they are found elsewhere in the _parent chain. This
590  means that the same file will be stored twice if the previous version was
591  found in an input repo.
592  """
593  def firstElement(list):
594  """Get the first element in the list, or None if that can't be done.
595  """
596  return list[0] if list is not None and len(list) else None
597 
598  n = 0
599  newLocation = self.map(datasetType, dataId, write=True)
600  newPath = newLocation.getLocations()[0]
601  path = self._parentSearch(newPath)
602  path = firstElement(path)
603  oldPaths = []
604  while path is not None:
605  n += 1
606  oldPaths.append((n, path))
607  path = self._parentSearch("%s~%d" % (newPath, n))
608  path = firstElement(path)
609  for n, oldPath in reversed(oldPaths):
610  newDir, newFile = os.path.split(newPath)
611  if not os.path.exists(newDir):
612  os.makedirs(newDir)
613  shutil.copy(oldPath, "%s~%d" % (newPath, n))
614 
615  def keys(self):
616  """Return supported keys.
617  @return (iterable) List of keys usable in a dataset identifier"""
618  return iter(self.keyDict.keys())
619 
620  def getKeys(self, datasetType, level):
621  """Return supported keys and their value types for a given dataset
622  type at a given level of the key hierarchy.
623 
624  @param datasetType (str) dataset type or None for all keys
625  @param level (str) level or None for all levels
626  @return (iterable) Set of keys usable in a dataset identifier"""
627 
628  # not sure if this is how we want to do this. what if None was intended?
629  if level == '':
630  level = self.getDefaultLevel()
631 
632  if datasetType is None:
633  keyDict = copy.copy(self.keyDict)
634  else:
635  keyDict = self.mappings[datasetType].keys()
636  if level is not None and level in self.levels:
637  keyDict = copy.copy(keyDict)
638  for l in self.levels[level]:
639  if l in keyDict:
640  del keyDict[l]
641  return keyDict
642 
643  def getDefaultLevel(self):
644  return self.defaultLevel
645 
646  def getDefaultSubLevel(self, level):
647  if level in self.defaultSubLevels:
648  return self.defaultSubLevels[level]
649  return None
650 
651  @classmethod
652  def getCameraName(cls):
653  """Return the name of the camera that this CameraMapper is for."""
654  className = str(cls)
655  className = className[className.find('.'):-1]
656  m = re.search(r'(\w+)Mapper', className)
657  if m is None:
658  m = re.search(r"class '[\w.]*?(\w+)'", className)
659  name = m.group(1)
660  return name[:1].lower() + name[1:] if name else ''
661 
662  @classmethod
663  def getPackageName(cls):
664  """Return the name of the package containing this CameraMapper."""
665  if cls.packageName is None:
666  raise ValueError('class variable packageName must not be None')
667  return cls.packageName
668 
669  def map_camera(self, dataId, write=False):
670  """Map a camera dataset."""
671  if self.camera is None:
672  raise RuntimeError("No camera dataset available.")
673  actualId = self._transformId(dataId)
674  return dafPersist.ButlerLocation(
675  pythonType="lsst.afw.cameraGeom.CameraConfig",
676  cppType="Config",
677  storageName="ConfigStorage",
678  locationList=self.cameraDataLocation or "ignored",
679  dataId=actualId,
680  mapper=self
681  )
682 
683  def bypass_camera(self, datasetType, pythonType, butlerLocation, dataId):
684  """Return the (preloaded) camera object.
685  """
686  if self.camera is None:
687  raise RuntimeError("No camera dataset available.")
688  return self.camera
689 
690  def map_defects(self, dataId, write=False):
691  """Map defects dataset.
692 
693  @return a very minimal ButlerLocation containing just the locationList field
694  (just enough information that bypass_defects can use it).
695  """
696  defectFitsPath = self._defectLookup(dataId=dataId)
697  if defectFitsPath is None:
698  raise RuntimeError("No defects available for dataId=%s" % (dataId,))
699 
700  return dafPersist.ButlerLocation(None, None, None, defectFitsPath, dataId, self)
701 
702  def bypass_defects(self, datasetType, pythonType, butlerLocation, dataId):
703  """Return a defect based on the butler location returned by map_defects
704 
705  @param[in] butlerLocation: a ButlerLocation with locationList = path to defects FITS file
706  @param[in] dataId: the usual data ID; "ccd" must be set
707 
708  Note: the name "bypass_XXX" means the butler makes no attempt to convert the ButlerLocation
709  into an object, which is what we want for now, since that conversion is a bit tricky.
710  """
711  detectorName = self._extractDetectorName(dataId)
712  defectsFitsPath = butlerLocation.locationList[0]
713  with pyfits.open(defectsFitsPath) as hduList:
714  for hdu in hduList[1:]:
715  if hdu.header["name"] != detectorName:
716  continue
717 
718  defectList = []
719  for data in hdu.data:
720  bbox = afwGeom.Box2I(
721  afwGeom.Point2I(int(data['x0']), int(data['y0'])),
722  afwGeom.Extent2I(int(data['width']), int(data['height'])),
723  )
724  defectList.append(afwImage.DefectBase(bbox))
725  return defectList
726 
727  raise RuntimeError("No defects for ccd %s in %s" % (detectorName, defectsFitsPath))
728 
729  def map_expIdInfo(self, dataId, write=False):
730  return dafPersist.ButlerLocation(
731  pythonType="lsst.obs.base.ExposureIdInfo",
732  cppType=None,
733  storageName="Internal",
734  locationList="ignored",
735  dataId=dataId,
736  mapper=self,
737  )
738 
739  def bypass_expIdInfo(self, datasetType, pythonType, location, dataId):
740  """Hook to retrieve an lsst.obs.base.ExposureIdInfo for an exposure"""
741  expId = self.bypass_ccdExposureId(datasetType, pythonType, location, dataId)
742  expBits = self.bypass_ccdExposureId_bits(datasetType, pythonType, location, dataId)
743  return ExposureIdInfo(expId=expId, expBits=expBits)
744 
745  def std_bfKernel(self, item, dataId):
746  """Disable standardization for bfKernel
747 
748  bfKernel is a calibration product that is numpy array,
749  unlike other calibration products that are all images;
750  all calibration images are sent through _standardizeExposure
751  due to CalibrationMapping, but we don't want that to happen to bfKernel
752  """
753  return item
754 
755  def std_raw(self, item, dataId):
756  """Standardize a raw dataset by converting it to an Exposure instead of an Image"""
757  exposure = exposureFromImage(item)
758  exposureId = self._computeCcdExposureId(dataId)
759  md = exposure.getMetadata()
760  visitInfo = self.makeRawVisitInfo(md=md, exposureId=exposureId)
761  exposure.getInfo().setVisitInfo(visitInfo)
762  return self._standardizeExposure(self.exposures['raw'], exposure, dataId,
763  trimmed=False)
764 
765  def map_skypolicy(self, dataId):
766  """Map a sky policy."""
767  return dafPersist.ButlerLocation("lsst.pex.policy.Policy", "Policy",
768  "Internal", None, None, self)
769 
770  def std_skypolicy(self, item, dataId):
771  """Standardize a sky policy by returning the one we use."""
772  return self.skypolicy
773 
774 ###############################################################################
775 #
776 # Utility functions
777 #
778 ###############################################################################
779 
780  def _getCcdKeyVal(self, dataId):
781  """Return CCD key and value used to look a defect in the defect registry
782 
783  The default implementation simply returns ("ccd", full detector name)
784  """
785  return ("ccd", self._extractDetectorName(dataId))
786 
787  def _setupRegistry(self, name, path, policy, policyKey, root):
788  """Set up a registry (usually SQLite3), trying a number of possible
789  paths.
790  @param name (string) Name of registry
791  @param path (string) Path for registry
792  @param policyKey (string) Key in policy for registry path
793  @param root (string) Root directory to look in
794  @return (lsst.daf.persistence.Registry) Registry object"""
795 
796  if path is None and policyKey in policy:
797  path = dafPersist.LogicalLocation(policy[policyKey]).locString()
798  if not os.path.exists(path):
799  if not os.path.isabs(path) and root is not None:
800  newPath = self._parentSearch(os.path.join(root, path))
801  newPath = newPath[0] if newPath is not None and len(newPath) else None
802  if newPath is None:
803  self.log.warn("Unable to locate registry at policy path (also looked in root): %s",
804  path)
805  path = newPath
806  else:
807  self.log.warn("Unable to locate registry at policy path: %s", path)
808  path = None
809 
810  # determine if there is an sqlite registry and if not, try the posix registry.
811  registry = None
812 
813  if path is None and root is not None:
814  path = os.path.join(root, "%s.sqlite3" % name)
815  newPath = self._parentSearch(path)
816  newPath = newPath[0] if newPath is not None and len(newPath) else None
817  if newPath is None:
818  self.log.info("Unable to locate %s registry in root: %s", name, path)
819  path = newPath
820  if path is None:
821  path = os.path.join(".", "%s.sqlite3" % name)
822  newPath = self._parentSearch(path)
823  newPath = newPath[0] if newPath is not None and len(newPath) else None
824  if newPath is None:
825  self.log.info("Unable to locate %s registry in current dir: %s", name, path)
826  path = newPath
827  if path is not None:
828  if not os.path.exists(path):
829  newPath = self._parentSearch(path)
830  newPath = newPath[0] if newPath is not None and len(newPath) else None
831  if newPath is not None:
832  path = newPath
833  self.log.debug("Loading %s registry from %s", name, path)
834  registry = dafPersist.Registry.create(path)
835  elif not registry and os.path.exists(root):
836  self.log.info("Loading Posix registry from %s", root)
837  registry = dafPersist.PosixRegistry(root)
838 
839  return registry
840 
841  def _transformId(self, dataId):
842  """Generate a standard ID dict from a camera-specific ID dict.
843 
844  Canonical keys include:
845  - amp: amplifier name
846  - ccd: CCD name (in LSST this is a combination of raft and sensor)
847  The default implementation returns a copy of its input.
848 
849  @param dataId[in] (dict) Dataset identifier; this must not be modified
850  @return (dict) Transformed dataset identifier"""
851 
852  return dataId.copy()
853 
854  def _mapActualToPath(self, template, actualId):
855  """Convert a template path to an actual path, using the actual data
856  identifier. This implementation is usually sufficient but can be
857  overridden by the subclass.
858  @param template (string) Template path
859  @param actualId (dict) Dataset identifier
860  @return (string) Pathname"""
861 
862  try:
863  transformedId = self._transformId(actualId)
864  return template % transformedId
865  except Exception as e:
866  raise RuntimeError("Failed to format %r with data %r: %s" % (template, transformedId, e))
867 
868  @staticmethod
869  def getShortCcdName(ccdName):
870  """Convert a CCD name to a form useful as a filename
871 
872  The default implementation converts spaces to underscores.
873  """
874  return ccdName.replace(" ", "_")
875 
876  def _extractDetectorName(self, dataId):
877  """Extract the detector (CCD) name from the dataset identifier.
878 
879  The name in question is the detector name used by lsst.afw.cameraGeom.
880 
881  @param dataId (dict) Dataset identifier
882  @return (string) Detector name
883  """
884  raise NotImplementedError("No _extractDetectorName() function specified")
885 
886  def _extractAmpId(self, dataId):
887  """Extract the amplifier identifer from a dataset identifier.
888 
889  @warning this is deprecated; DO NOT USE IT
890 
891  amplifier identifier has two parts: the detector name for the CCD
892  containing the amplifier and index of the amplifier in the detector.
893  @param dataId (dict) Dataset identifer
894  @return (tuple) Amplifier identifier"""
895 
896  trDataId = self._transformId(dataId)
897  return (trDataId["ccd"], int(trDataId['amp']))
898 
899  def _setAmpDetector(self, item, dataId, trimmed=True):
900  """Set the detector object in an Exposure for an amplifier.
901  Defects are also added to the Exposure based on the detector object.
902  @param[in,out] item (lsst.afw.image.Exposure)
903  @param dataId (dict) Dataset identifier
904  @param trimmed (bool) Should detector be marked as trimmed? (ignored)"""
905 
906  return self._setCcdDetector(item=item, dataId=dataId, trimmed=trimmed)
907 
908  def _setCcdDetector(self, item, dataId, trimmed=True):
909  """Set the detector object in an Exposure for a CCD.
910  @param[in,out] item (lsst.afw.image.Exposure)
911  @param dataId (dict) Dataset identifier
912  @param trimmed (bool) Should detector be marked as trimmed? (ignored)"""
913 
914  detectorName = self._extractDetectorName(dataId)
915  detector = self.camera[detectorName]
916  item.setDetector(detector)
917 
918  def _setFilter(self, mapping, item, dataId):
919  """Set the filter object in an Exposure. If the Exposure had a FILTER
920  keyword, this was already processed during load. But if it didn't,
921  use the filter from the registry.
922  @param mapping (lsst.obs.base.Mapping)
923  @param[in,out] item (lsst.afw.image.Exposure)
924  @param dataId (dict) Dataset identifier"""
925 
926  if not (isinstance(item, afwImage.ExposureU) or isinstance(item, afwImage.ExposureI) or
927  isinstance(item, afwImage.ExposureF) or isinstance(item, afwImage.ExposureD)):
928  return
929 
930  actualId = mapping.need(['filter'], dataId)
931  filterName = actualId['filter']
932  if self.filters is not None and filterName in self.filters:
933  filterName = self.filters[filterName]
934  item.setFilter(afwImage.Filter(filterName))
935 
936  # Default standardization function for exposures
937  def _standardizeExposure(self, mapping, item, dataId, filter=True,
938  trimmed=True):
939  """Default standardization function for images.
940 
941  This sets the Detector from the camera geometry
942  and optionally set the Fiter. In both cases this saves
943  having to persist some data in each exposure (or image).
944 
945  @param mapping (lsst.obs.base.Mapping)
946  @param[in,out] item image-like object; any of lsst.afw.image.Exposure,
947  lsst.afw.image.DecoratedImage, lsst.afw.image.Image
948  or lsst.afw.image.MaskedImage
949  @param dataId (dict) Dataset identifier
950  @param filter (bool) Set filter? Ignored if item is already an exposure
951  @param trimmed (bool) Should detector be marked as trimmed?
952  @return (lsst.afw.image.Exposure) the standardized Exposure"""
953  if not hasattr(item, "getMaskedImage"):
954  try:
955  item = exposureFromImage(item)
956  except Exception as e:
957  self.log.error("Could not turn item=%r into an exposure: %s" % (repr(item), e))
958  raise
959 
960  if mapping.level.lower() == "amp":
961  self._setAmpDetector(item, dataId, trimmed)
962  elif mapping.level.lower() == "ccd":
963  self._setCcdDetector(item, dataId, trimmed)
964 
965  if filter:
966  self._setFilter(mapping, item, dataId)
967 
968  return item
969 
970  def _defectLookup(self, dataId):
971  """Find the defects for a given CCD.
972  @param dataId (dict) Dataset identifier
973  @return (string) path to the defects file or None if not available"""
974  if self.defectRegistry is None:
975  return None
976  if self.registry is None:
977  raise RuntimeError("No registry for defect lookup")
978 
979  ccdKey, ccdVal = self._getCcdKeyVal(dataId)
980 
981  dataIdForLookup = {'visit': dataId['visit']}
982  # .lookup will fail in a posix registry because there is no template to provide.
983  rows = self.registry.lookup(('taiObs'), ('raw_visit'), dataIdForLookup)
984  if len(rows) == 0:
985  return None
986  assert len(rows) == 1
987  taiObs = rows[0][0]
988 
989  # Lookup the defects for this CCD serial number that are valid at the exposure midpoint.
990  rows = self.defectRegistry.executeQuery(("path",), ("defect",),
991  [(ccdKey, "?")],
992  ("DATETIME(?)", "DATETIME(validStart)", "DATETIME(validEnd)"),
993  (ccdVal, taiObs))
994  if not rows or len(rows) == 0:
995  return None
996  if len(rows) == 1:
997  return os.path.join(self.defectPath, rows[0][0])
998  else:
999  raise RuntimeError("Querying for defects (%s, %s) returns %d files: %s" %
1000  (ccdVal, taiObs, len(rows), ", ".join([_[0] for _ in rows])))
1001 
1002  def _makeCamera(self, policy, repositoryDir):
1003  """Make a camera (instance of lsst.afw.cameraGeom.Camera) describing the camera geometry
1004 
1005  Also set self.cameraDataLocation, if relevant (else it can be left None).
1006 
1007  This implementation assumes that policy contains an entry "camera" that points to the
1008  subdirectory in this package of camera data; specifically, that subdirectory must contain:
1009  - a file named `camera.py` that contains persisted camera config
1010  - ampInfo table FITS files, as required by lsst.afw.cameraGeom.makeCameraFromPath
1011 
1012  @param policy (daf_persistence.Policy, or pexPolicy.Policy (only for backward compatibility))
1013  Policy with per-camera defaults already merged
1014  @param repositoryDir (string) Policy repository for the subclassing
1015  module (obtained with getRepositoryPath() on the
1016  per-camera default dictionary)
1017  """
1018  if isinstance(policy, pexPolicy.Policy):
1019  policy = dafPersist.Policy(pexPolicy=policy)
1020  if 'camera' not in policy:
1021  raise RuntimeError("Cannot find 'camera' in policy; cannot construct a camera")
1022  cameraDataSubdir = policy['camera']
1023  self.cameraDataLocation = os.path.normpath(
1024  os.path.join(repositoryDir, cameraDataSubdir, "camera.py"))
1025  cameraConfig = afwCameraGeom.CameraConfig()
1026  cameraConfig.load(self.cameraDataLocation)
1027  ampInfoPath = os.path.dirname(self.cameraDataLocation)
1028  return afwCameraGeom.makeCameraFromPath(
1029  cameraConfig=cameraConfig,
1030  ampInfoPath=ampInfoPath,
1031  shortNameFunc=self.getShortCcdName
1032  )
1033 
1034 
1036  """Generate an Exposure from an image-like object
1037 
1038  If the image is a DecoratedImage then also set its WCS and metadata
1039  (Image and MaskedImage are missing the necessary metadata
1040  and Exposure already has those set)
1041 
1042  @param[in] image Image-like object (lsst.afw.image.DecoratedImage, Image, MaskedImage or Exposure)
1043  @return (lsst.afw.image.Exposure) Exposure containing input image
1044  """
1045  if hasattr(image, "getVariance"):
1046  # MaskedImage
1047  exposure = afwImage.makeExposure(image)
1048  elif hasattr(image, "getImage"):
1049  # DecoratedImage
1050  exposure = afwImage.makeExposure(afwImage.makeMaskedImage(image.getImage()))
1051  metadata = image.getMetadata()
1052  wcs = afwImage.makeWcs(metadata, True)
1053  exposure.setWcs(wcs)
1054  exposure.setMetadata(metadata)
1055  elif hasattr(image, "getMaskedImage"):
1056  # Exposure
1057  exposure = image
1058  else:
1059  # Image
1061 
1062  return exposure
int iter
MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > * makeMaskedImage(typename Image< ImagePixelT >::Ptr image, typename Mask< MaskPixelT >::Ptr mask=typename Mask< MaskPixelT >::Ptr(), typename Image< VariancePixelT >::Ptr variance=typename Image< VariancePixelT >::Ptr())
A function to return a MaskedImage of the correct type (cf.
Definition: MaskedImage.h:1073
Encapsulate information about a bad portion of a detector.
Definition: Defect.h:41
Class for logical location of a persisted Persistable instance.
a container for holding hierarchical configuration data in memory.
Definition: Policy.h:169
Information about a single exposure of an imaging camera.
Definition: VisitInfo.h:63
Describe an exposure&#39;s calibration.
Definition: Calib.h:82
An integer coordinate rectangle.
Definition: Box.h:53
std::string getPackageDir(std::string const &packageName)
return the root directory of a setup package
Definition: Utils.cc:34
boost::shared_ptr< Wcs > makeWcs(coord::Coord const &crval, geom::Point2D const &crpix, double CD11, double CD12, double CD21, double CD22)
Create a Wcs object from crval, crpix, CD, using CD elements (useful from python) ...
Definition: makeWcs.cc:138
Holds an integer identifier for an LSST filter.
Definition: Filter.h:108
boost::shared_ptr< daf::base::PropertyList > readMetadata(std::string const &fileName, int hdu=0, bool strip=false)
Return the metadata (header entries) from a FITS file.
Definition: Utils.h:62
Exposure< ImagePixelT, MaskPixelT, VariancePixelT >::Ptr makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, boost::shared_ptr< Wcs const > wcs=boost::shared_ptr< Wcs const >())
A function to return an Exposure of the correct type (cf.
Definition: Exposure.h:314
def _getCcdKeyVal
Utility functions.