22 from __future__
import annotations
24 __all__ = (
"Translator",
"TranslatorFactory",
"KeyHandler",
"CopyKeyHandler",
"ConstantKeyHandler",
25 "CalibKeyHandler",
"AbstractToPhysicalFilterKeyHandler",
"PhysicalToAbstractFilterKeyHandler",
26 "makeCalibrationLabel")
29 from typing
import Optional, Any, Dict, Tuple, FrozenSet, Iterable, List
30 from abc
import ABCMeta, abstractmethod
37 filter: Optional[str] =
None) -> str:
38 """Make a Gen3 calibration_label string corresponding to a Gen2 data ID.
42 datasetTypeName : `str`
43 Name of the dataset type this calibration label identifies.
45 Date string used in the Gen2 template.
47 Detector ID used in the Gen2 template.
48 filter : `str`, optional
49 Filter used in the Gen2 template.
54 Calibration label string.
58 elements = [datasetTypeName, calibDate]
60 elements.append(f
"{ccd:03d}")
61 if filter
is not None:
62 elements.append(filter)
63 return "gen2/{}".
format(
"_".join(elements))
67 """Base class for Translator helpers that each handle just one Gen3 Data
73 Name of the Gen3 dimension (data ID key) populated by
74 this handler (e.g. "visit" or "abstract_filter").
79 __slots__ = (
"dimension",)
82 return f
"{type(self).__name__}({self.dimension}, ...)"
85 skyMap: Optional[BaseSkyMap], skyMapName: Optional[str],
86 datasetTypeName: str):
87 """Update a Gen3 data ID dict with a single key-value pair from a Gen2
90 This method is implemented by the base class and is not expected to
91 be re-implemented by subclasses.
96 Gen2 data ID from which to draw key-value pairs from.
98 Gen3 data ID to update in-place.
99 skyMap: `BaseSkyMap`, optional
100 SkyMap that defines the tracts and patches used in the Gen2 data
103 Name of the Gen3 skymap dimension that defines the tracts and
104 patches used in the Gen3 data ID.
105 datasetTypeName: `str`
106 Name of the dataset type.
108 gen3id[self.
dimension] = self.
extract(gen2id, skyMap=skyMap, skyMapName=skyMapName,
109 datasetTypeName=datasetTypeName)
112 def extract(self, gen2id: dict, skyMap: Optional[BaseSkyMap], skyMapName: Optional[str],
113 datasetTypeName: str) -> Any:
114 """Extract a Gen3 data ID value from a Gen2 data ID.
119 Gen2 data ID from which to draw key-value pairs from.
120 skyMap: `BaseSkyMap`, optional
121 SkyMap that defines the tracts and patches used in the Gen2 data
124 Name of the Gen3 skymap dimension that defines the tracts and
125 patches used in the Gen3 data ID.
126 datasetTypeName: `str`
127 Name of the dataset type.
129 raise NotImplementedError()
133 """A KeyHandler that adds a constant key-value pair to the Gen3 data ID.
138 Name of the Gen3 dimension (data ID key) populated by
139 this handler (e.g. "visit" or "abstract_filter").
147 __slots__ = (
"value",)
149 def extract(self, gen2id: dict, skyMap: Optional[BaseSkyMap], skyMapName: Optional[str],
150 datasetTypeName: str) -> Any:
156 """A KeyHandler that simply copies a value from a Gen3 data ID.
161 Name of the Gen3 dimension produced by this handler.
162 dtype : `type`, optional
163 If not `None`, the type that values for this key must be an
166 def __init__(self, dimension: str, gen2key: Optional[str] =
None,
167 dtype: Optional[type] =
None):
169 self.
gen2key = gen2key
if gen2key
is not None else dimension
172 __slots__ = (
"gen2key",
"dtype")
175 return f
"{type(self).__name__}({self.gen2key}, {self.dtype})"
177 def extract(self, gen2id: dict, skyMap: Optional[BaseSkyMap], skyMapName: Optional[str],
178 datasetTypeName: str) -> Any:
181 if self.
dtype is not None:
184 except ValueError
as err:
186 f
"'{r}' is not a valid value for {self.dimension}; "
187 f
"expected {self.dtype.__name__}, got {type(r).__name__}."
193 """A KeyHandler for skymap patches.
200 def extract(self, gen2id: dict, skyMap: Optional[BaseSkyMap], skyMapName: Optional[str],
201 datasetTypeName: str) -> Any:
203 tract = gen2id[
"tract"]
204 tractInfo = skyMap[tract]
205 x, y = gen2id[
"patch"].split(
",")
206 patchInfo = tractInfo[int(x), int(y)]
207 return tractInfo.getSequentialPatchIndex(patchInfo)
211 """A KeyHandler for skymaps."""
217 def extract(self, gen2id: dict, skyMap: Optional[BaseSkyMap], skyMapName: Optional[str],
218 datasetTypeName: str) -> Any:
224 """A KeyHandler for master calibration datasets.
226 __slots__ = (
"ccdKey",)
230 super().
__init__(
"calibration_label")
232 def extract(self, gen2id: dict, skyMap: Optional[BaseSkyMap], skyMapName: Optional[str],
233 datasetTypeName: str) -> Any:
236 ccd=gen2id.get(self.
ccdKey), filter=gen2id.get(
"filter"))
240 """KeyHandler for gen2 ``filter`` keys that match ``physical_filter``
241 keys in gen3 but should be mapped to ``abstract_filter``.
243 Note that multiple physical filter can potentially map to one abstract
244 filter, so be careful to only use this translator on obs packages where
245 there is a one-to-one mapping.
248 __slots__ = (
"_map",)
252 self.
_map = {d.physical_filter: d.abstract_filter
for d
in filterDefinitions
253 if d.physical_filter
is not None}
256 physical = gen2id[
"filter"]
257 return self.
_map.get(physical, physical)
261 """KeyHandler for gen2 ``filter`` keys that match ``abstract_filter``
262 keys in gen3 but should be mapped to ``physical_filter``.
264 Note that one abstract filter can potentially map to multiple physical
265 filters, so be careful to only use this translator on obs packages where
266 there is a one-to-one mapping.
269 __slots__ = (
"_map",)
273 self.
_map = {d.abstract_filter: d.physical_filter
for d
in filterDefinitions
274 if d.abstract_filter
is not None}
277 abstract = gen2id[
"filter"]
278 return self.
_map.get(abstract, abstract)
282 """A class that manages a system of rules for translating Gen2 data IDs
283 to Gen3 data IDs, and uses these to construct translators for particular
288 log : `lsst.log.Log`, optional
289 A logger for diagnostic messages.
298 List[Tuple[FrozenSet[str], KeyHandler, bool]]
307 log = Log.getLogger(
"obs.base.gen2to3.TranslatorFactory")
312 for instrumentName, nested
in self._rules.
items():
313 if instrumentName
is None:
314 instrumentName =
"[any instrument]"
315 for datasetTypeName, rules
in nested.items():
316 if datasetTypeName
is None:
317 datasetTypeName =
"[any dataset type]"
318 lines.append(f
"{instrumentName} + {datasetTypeName}:")
319 for gen2keys, handler, consume
in rules:
320 consumed =
" (consumed)" if consume
else ""
321 lines.append(f
" {gen2keys}{consumed}: {handler}")
322 return "\n".join(lines)
324 def addRule(self, handler: KeyHandler, instrument: Optional[str] =
None,
325 datasetTypeName: Optional[str] =
None, gen2keys: Iterable[str] = (),
326 consume: bool =
True):
327 """Add a KeyHandler and an associated matching rule.
331 handler : `KeyHandler`
332 A KeyHandler instance to add to a Translator when this rule
335 Gen3 instrument name the Gen2 repository must be associated with
336 for this rule to match, or None to match any instrument.
337 datasetTypeName : `str`
338 Name of the DatasetType this rule matches, or None to match any
341 Sequence of Gen2 data ID keys that must all be present for this
343 consume : `bool` or `tuple`
344 If True (default), remove all entries in gen2keys from the set of
345 keys being matched to in order to prevent less-specific handlers
347 May also be a `tuple` listing only the keys to consume.
352 consume = frozenset(gen2keys)
354 consume = frozenset(consume)
356 consume = frozenset()
360 rulesForInstrument = self._rules.setdefault(instrument, {
None: []})
361 rulesForInstrumentAndDatasetType = rulesForInstrument.setdefault(datasetTypeName, [])
362 rulesForInstrumentAndDatasetType.append((frozenset(gen2keys), handler, consume))
364 def _addDefaultRules(self):
365 """Add translator rules that should always be present, and don't depend
366 at all on the instrument whose datasets are being converted.
368 This is called by `TranslatorFactory` construction.
374 for coaddName
in (
"deep",
"goodSeeing",
"psfMatched",
"dcr"):
389 gen2keys=(
"filter",
"tract"),
397 for datasetTypeName
in (
"transmission_sensor",
"transmission_optics",
"transmission_filter"):
399 datasetTypeName=datasetTypeName)
412 calibFilterType: str =
"physical_filter",
413 detectorKey: str =
"ccd",
414 exposureKey: str =
"visit"):
415 """Add translation rules that depend on some properties of the
416 instrument but are otherwise generic.
421 The short (dimension) name of the instrument that conversion is
423 calibFilterType : `str`, optional
424 One of ``physical_filter`` or ``abstract_filter``, indicating which
425 of those the gen2 calibRegistry uses as the ``filter`` key.
426 detectorKey : `str`, optional
427 The gen2 key used to identify what in gen3 is `detector`.
428 exposureKey : `str`, optional
429 The gen2 key used to identify what in gen3 is `exposure`.
435 instrument=instrumentName, gen2keys=(exposureKey,), consume=
False)
437 instrument=instrumentName, gen2keys=(detectorKey,), consume=
False)
439 instrument=instrumentName, gen2keys=(
"calibDate",), consume=
False)
444 instrument=instrumentName, datasetTypeName=
"raw", gen2keys=(exposureKey,),
445 consume=(exposureKey,
"filter"))
449 consume=(
"visit",
"filter"))
453 instrument=instrumentName,
454 gen2keys=(detectorKey,))
459 instrument=instrumentName, datasetTypeName=
"transmission_optics")
461 instrument=instrumentName, datasetTypeName=
"transmission_atmosphere")
463 instrument=instrumentName, datasetTypeName=
"transmission_filter")
465 instrument=instrumentName, datasetTypeName=
"transmission_filter")
468 for calibType
in (
'flat',
'sky',
'fringe'):
470 instrument=instrumentName, datasetTypeName=calibType)
475 def makeMatching(self, datasetTypeName: str, gen2keys: Dict[str, type], instrument: Optional[str] =
None,
476 skyMap: Optional[BaseSkyMap] =
None, skyMapName: Optional[str] =
None):
477 """Construct a Translator appropriate for instances of the given
482 datasetTypeName : `str`
483 Name of the dataset type.
485 Keys of a Gen2 data ID for this dataset.
486 instrument: `str`, optional
487 Name of the Gen3 instrument dimension for translated data IDs.
488 skyMap: `~lsst.skymap.BaseSkyMap`, optional
489 The skymap instance that defines any tract/patch data IDs.
490 `~lsst.skymap.BaseSkyMap` instances.
491 skyMapName : `str`, optional
492 Gen3 SkyMap Dimension name to be associated with any tract or patch
497 translator : `Translator`
498 A translator whose translate() method can be used to transform Gen2
499 data IDs to Gen3 dataIds.
501 if instrument
is not None:
502 rulesForInstrument = self._rules.get(instrument, {
None: []})
504 rulesForInstrument = {
None: []}
505 rulesForAnyInstrument = self._rules[
None]
506 candidateRules = itertools.chain(
507 rulesForInstrument.get(datasetTypeName, []),
508 rulesForInstrument[
None],
509 rulesForAnyInstrument.get(datasetTypeName, []),
510 rulesForAnyInstrument[
None],
513 targetKeys =
set(gen2keys)
514 self.
log.
debug(
"Constructing data ID translator for %s with Gen2 keys %s...",
515 datasetTypeName, gen2keys)
516 for ruleKeys, ruleHandlers, consume
in candidateRules:
517 if ruleKeys.issubset(targetKeys):
518 matchedHandlers.append(ruleHandlers)
519 targetKeys -= consume
520 self.
log.
debug(
"...matched %d handlers: %s, with %s unmatched.",
521 len(matchedHandlers), matchedHandlers, targetKeys)
522 return Translator(matchedHandlers, skyMap=skyMap, skyMapName=skyMapName,
523 datasetTypeName=datasetTypeName, log=self.
log)
527 """Callable object that translates Gen2 Data IDs to Gen3 Data IDs for a
528 particular DatasetType.
530 Translators should usually be constructed via
531 `TranslatorFactory.makeMatching`.
536 A list of KeyHandlers this Translator should use.
537 skyMap : `BaseSkyMap`, optional
538 SkyMap instance used to define any tract or patch Dimensions.
540 Gen3 SkyMap Dimension name to be associated with any tract or patch
542 datasetTypeName : `str`
543 Name of the dataset type whose data IDs this translator handles.
545 def __init__(self, handlers: List[KeyHandler], skyMap: Optional[BaseSkyMap], skyMapName: Optional[str],
546 datasetTypeName: str, log: Log):
553 __slots__ = (
"handlers",
"skyMap",
"skyMapName",
"datasetTypeName",
"log")
556 hstr =
",".join(str(h)
for h
in self.
handlers)
557 return f
"{type(self).__name__}(dtype={self.datasetTypeName}, handlers=[{hstr}])"
559 def __call__(self, gen2id: Dict[str, Any], *, partial: bool =
False):
560 """Return a Gen3 data ID that corresponds to the given Gen2 data ID.
565 handler.translate(gen2id, gen3id, skyMap=self.
skyMap, skyMapName=self.
skyMapName,
569 self.
log.
debug(
"Failed to translate %s from %s (this may not be an error).",
570 handler.dimension, gen2id)
578 """The names of the dimensions populated by this Translator
581 return frozenset(h.dimension
for h
in self.
handlers)