22"""Tasks for making and manipulating HIPS images."""
25 "HighResolutionHipsTask",
26 "HighResolutionHipsConfig",
27 "HighResolutionHipsConnections",
28 "HighResolutionHipsQuantumGraphBuilder",
31 "GenerateColorHipsTask",
32 "GenerateColorHipsConfig",
35from collections
import defaultdict
44from datetime
import datetime
46import healsparse
as hsp
47from astropy.io
import fits
50 from astropy.visualization.lupton_rgb
import AsinhMapping
52 from ._fallback_asinhmapping
import AsinhMapping
56from lsst.utils.timer
import timeMethod
57from lsst.daf.butler
import Butler
58from lsst.daf.butler.queries.expression_factory
import ExpressionFactory
61from lsst.pipe.base.quantum_graph_builder
import QuantumGraphBuilder
62from lsst.pipe.base.quantum_graph_skeleton
import QuantumGraphSkeleton
65import lsst.afw.image
as afwImage
68from lsst.resources
import ResourcePath
70from .healSparseMapping
import _is_power_of_two
71from .prettyPictureMaker
import PrettyPictureTask, PrettyPictureConfig
75 pipeBase.PipelineTaskConnections, dimensions=(
"healpix9",
"band"), defaultTemplates={
"coaddName":
"deep"}
77 coadd_exposure_handles = pipeBase.connectionTypes.Input(
78 doc=
"Coadded exposures to convert to HIPS format.",
79 name=
"{coaddName}Coadd_calexp",
80 storageClass=
"ExposureF",
81 dimensions=(
"tract",
"patch",
"skymap",
"band"),
85 hips_exposures = pipeBase.connectionTypes.Output(
86 doc=
"HiPS-compatible HPX image.",
87 name=
"{coaddName}Coadd_hpx",
88 storageClass=
"ExposureF",
89 dimensions=(
"healpix11",
"band"),
93 def __init__(self, *, config=None):
94 super().__init__(config=config)
97 for dim
in self.dimensions:
99 if quantum_order
is not None:
100 raise ValueError(
"Must not specify more than one quantum healpix dimension.")
101 quantum_order = int(dim.split(
"healpix")[1])
102 if quantum_order
is None:
103 raise ValueError(
"Must specify a healpix dimension in quantum dimensions.")
105 if quantum_order > config.hips_order:
106 raise ValueError(
"Quantum healpix dimension order must not be greater than hips_order")
109 for dim
in self.hips_exposures.dimensions:
111 if order
is not None:
112 raise ValueError(
"Must not specify more than one healpix dimension.")
113 order = int(dim.split(
"healpix")[1])
115 raise ValueError(
"Must specify a healpix dimension in hips_exposure dimensions.")
117 if order != config.hips_order:
118 raise ValueError(
"healpix dimension order must match config.hips_order.")
121class HighResolutionHipsConfig(
122 pipeBase.PipelineTaskConfig, pipelineConnections=HighResolutionHipsConnections
124 """Configuration parameters for HighResolutionHipsTask.
128 A HiPS image covers one HEALPix cell, with the HEALPix nside equal to
129 2**hips_order. Each cell is 'shift_order' orders deeper than the HEALPix
130 cell, with 2**shift_order x 2**shift_order sub-pixels on a side, which
131 defines the target resolution of the HiPS image. The IVOA recommends
132 shift_order=9, for 2**9=512 pixels on a side.
135 https://www.ivoa.net/documents/HiPS/20170519/REC-HIPS-1.0-20170519.pdf
136 shows the relationship between hips_order, number of tiles (full
137 sky coverage), cell size, and sub-pixel size/image resolution (with
138 the default shift_order=9):
139 +------------+-----------------+--------------+------------------+
140 | hips_order | Number of Tiles | Cell Size | Image Resolution |
141 +============+=================+==============+==================+
142 | 0 | 12 | 58.63 deg | 6.871 arcmin |
143 | 1 | 48 | 29.32 deg | 3.435 arcmin |
144 | 2 | 192 | 14.66 deg | 1.718 arcmin |
145 | 3 | 768 | 7.329 deg | 51.53 arcsec |
146 | 4 | 3072 | 3.665 deg | 25.77 arcsec |
147 | 5 | 12288 | 1.832 deg | 12.88 arcsec |
148 | 6 | 49152 | 54.97 arcmin | 6.442 arcsec |
149 | 7 | 196608 | 27.48 arcmin | 3.221 arcsec |
150 | 8 | 786432 | 13.74 arcmin | 1.61 arcsec |
151 | 9 | 3145728 | 6.871 arcmin | 805.2mas |
152 | 10 | 12582912 | 3.435 arcmin | 402.6mas |
153 | 11 | 50331648 | 1.718 arcmin | 201.3mas |
154 | 12 | 201326592 | 51.53 arcsec | 100.6mas |
155 | 13 | 805306368 | 25.77 arcsec | 50.32mas |
156 +------------+-----------------+--------------+------------------+
159 hips_order = pexConfig.Field(
160 doc=
"HIPS image order.",
164 shift_order = pexConfig.Field(
165 doc=
"HIPS shift order (such that each tile is 2**shift_order pixels on a side)",
169 warp = pexConfig.ConfigField(
170 dtype=afwMath.Warper.ConfigClass,
171 doc=
"Warper configuration",
174 def setDefaults(self):
175 self.warp.warpingKernelName =
"lanczos5"
178class HipsTaskNameDescriptor:
179 """Descriptor used create a DefaultName that matches the order of
180 the defined dimensions in the connections class.
185 The prefix of the Default name, to which the order will be
189 def __init__(self, prefix):
191 self._defaultName = f
"{prefix}{{}}"
194 def __get__(self, obj, klass=None):
196 raise RuntimeError(
"HipsTaskDescriptor was used in an unexpected context")
197 if self._order
is None:
198 klassDimensions = klass.ConfigClass.ConnectionsClass.dimensions
199 for dim
in klassDimensions:
200 if (match := re.match(
r"^healpix(\d*)$", dim))
is not None:
201 self._order = int(match.group(1))
204 raise RuntimeError(
"Could not find healpix dimension in connections class")
205 return self._defaultName.format(self._order)
208class HighResolutionHipsTask(pipeBase.PipelineTask):
209 """Task for making high resolution HiPS images."""
211 ConfigClass = HighResolutionHipsConfig
212 _DefaultName = HipsTaskNameDescriptor(
"highResolutionHips")
214 def __init__(self, **kwargs):
215 super().__init__(**kwargs)
216 self.warper = afwMath.Warper.fromConfig(self.config.warp)
219 def runQuantum(self, butlerQC, inputRefs, outputRefs):
220 inputs = butlerQC.get(inputRefs)
222 healpix_dim = f
"healpix{self.config.hips_order}"
224 pixels = [hips_exposure.dataId[healpix_dim]
for hips_exposure
in outputRefs.hips_exposures]
226 outputs = self.run(pixels=pixels, coadd_exposure_handles=inputs[
"coadd_exposure_handles"])
228 hips_exposure_ref_dict = {
229 hips_exposure_ref.dataId[healpix_dim]: hips_exposure_ref
230 for hips_exposure_ref
in outputRefs.hips_exposures
232 for pixel, hips_exposure
in outputs.hips_exposures.items():
233 butlerQC.put(hips_exposure, hips_exposure_ref_dict[pixel])
235 def run(self, pixels, coadd_exposure_handles):
236 """Run the HighResolutionHipsTask.
240 pixels : `Iterable` [ `int` ]
241 Iterable of healpix pixels (nest ordering) to warp to.
242 coadd_exposure_handles : `list` [`lsst.daf.butler.DeferredDatasetHandle`]
243 Handles for the coadd exposures.
247 outputs : `lsst.pipe.base.Struct`
248 ``hips_exposures`` is a dict with pixel (key) and hips_exposure (value)
250 self.log.info(
"Generating HPX images for %d pixels at order %d", len(pixels), self.config.hips_order)
252 npix = 2**self.config.shift_order
261 wcs_hpx = afwGeom.makeHpxWcs(self.config.hips_order, pixel, shift_order=self.config.shift_order)
262 exp_hpx = afwImage.ExposureF(bbox_hpx, wcs_hpx)
263 exp_hpx_dict[pixel] = exp_hpx
264 warp_dict[pixel] = []
269 for handle
in coadd_exposure_handles:
270 coadd_exp = handle.get()
274 warped = self.warper.warpExposure(exp_hpx_dict[pixel].getWcs(), coadd_exp, maxBBox=bbox_hpx)
276 exp = afwImage.ExposureF(exp_hpx_dict[pixel].getBBox(), exp_hpx_dict[pixel].getWcs())
277 exp.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask(
"NO_DATA"), np.nan)
282 exp_hpx_dict[pixel].mask.conformMaskPlanes(coadd_exp.mask.getMaskPlaneDict())
283 exp_hpx_dict[pixel].setFilter(coadd_exp.getFilter())
284 exp_hpx_dict[pixel].setPhotoCalib(coadd_exp.getPhotoCalib())
286 if warped.getBBox().getArea() == 0
or not np.any(np.isfinite(warped.image.array)):
289 "No overlap between output HPX %d and input exposure %s", pixel, handle.dataId
293 exp.maskedImage.assign(warped.maskedImage, warped.getBBox())
294 warp_dict[pixel].append(exp.maskedImage)
300 stats_ctrl.setNanSafe(
True)
301 stats_ctrl.setWeighted(
True)
302 stats_ctrl.setCalcErrorFromInputVariance(
True)
308 exp_hpx_dict[pixel].maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask(
"NO_DATA"), np.nan)
310 if not warp_dict[pixel]:
312 self.log.debug(
"No data in HPX pixel %d", pixel)
315 exp_hpx_dict.pop(pixel)
322 [1.0] * len(warp_dict[pixel]),
327 return pipeBase.Struct(hips_exposures=exp_hpx_dict)
330 def build_quantum_graph_cli(cls, argv):
331 """A command-line interface entry point to `build_quantum_graph`.
332 This method provides the implementation for the
333 ``build-high-resolution-hips-qg`` script.
337 argv : `Sequence` [ `str` ]
338 Command-line arguments (e.g. ``sys.argv[1:]``).
340 parser = cls._make_cli_parser()
342 args = parser.parse_args(argv)
344 if args.subparser_name
is None:
348 pipeline = pipeBase.Pipeline.from_uri(args.pipeline)
349 pipeline_graph = pipeline.to_graph()
351 if len(pipeline_graph.tasks) != 1:
352 raise RuntimeError(f
"Pipeline file {args.pipeline} may only contain one task.")
354 (task_node,) = pipeline_graph.tasks.values()
356 butler = Butler(args.butler_config, collections=args.input)
358 if args.subparser_name ==
"segment":
361 dataset = task_node.inputs[
"coadd_exposure_handles"].dataset_type_name
362 with butler.query()
as q:
363 data_ids = list(q.join_dataset_search(dataset).data_ids(
"tract").with_dimension_records())
365 for data_id
in data_ids:
366 region = data_id.region
367 pixel_range = hpix_pixelization.envelope(region)
368 for r
in pixel_range.ranges():
369 region_pixels.extend(range(r[0], r[1]))
370 indices = np.unique(region_pixels)
372 print(f
"Pixels to run at HEALPix order --hpix_build_order {args.hpix_build_order}:")
373 for pixel
in indices:
376 elif args.subparser_name ==
"build":
380 if args.output_run
is None:
381 if args.output
is None:
382 raise ValueError(
"At least one of --output or --output-run options is required.")
383 args.output_run =
"{}/{}".format(args.output, pipeBase.Instrument.makeCollectionTimestamp())
385 build_ranges =
RangeSet(sorted(args.pixels))
390 "butler_argument": args.butler_config,
391 "output": args.output,
392 "output_run": args.output_run,
393 "data_query": args.where,
394 "time": f
"{datetime.now()}",
397 builder = HighResolutionHipsQuantumGraphBuilder(
400 input_collections=args.input,
401 output_run=args.output_run,
402 constraint_order=args.hpix_build_order,
403 constraint_ranges=build_ranges,
406 qg = builder.build(metadata, attach_datastore_records=
True)
407 qg.saveUri(args.save_qgraph)
410 def _make_cli_parser(cls):
411 """Make the command-line parser.
415 parser : `argparse.ArgumentParser`
417 parser = argparse.ArgumentParser(
418 description=(
"Build a QuantumGraph that runs HighResolutionHipsTask on existing coadd datasets."),
420 subparsers = parser.add_subparsers(help=
"sub-command help", dest=
"subparser_name")
422 parser_segment = subparsers.add_parser(
"segment", help=
"Determine survey segments for workflow.")
423 parser_build = subparsers.add_parser(
"build", help=
"Build quantum graph for HighResolutionHipsTask")
425 for sub
in [parser_segment, parser_build]:
431 help=
"Path to data repository or butler configuration.",
438 help=
"Pipeline file, limited to one task.",
446 help=
"Input collection(s) to search for coadd exposures.",
451 "--hpix_build_order",
454 help=
"HEALPix order to segment sky for building quantum graph files.",
461 help=
"Data ID expression used when querying for input coadd datasets.",
464 parser_build.add_argument(
468 "Name of the output CHAINED collection. If this options is specified and "
469 "--output-run is not, then a new RUN collection will be created by appending "
470 "a timestamp to the value of this option."
475 parser_build.add_argument(
479 "Output RUN collection to write resulting images. If not provided "
480 "then --output must be provided and a new RUN collection will be created "
481 "by appending a timestamp to the value passed with --output."
486 parser_build.add_argument(
490 help=
"Output filename for QuantumGraph.",
493 parser_build.add_argument(
498 help=
"Pixels at --hpix_build_order to generate quantum graph.",
505class HighResolutionHipsQuantumGraphBuilder(QuantumGraphBuilder):
506 """A custom a `lsst.pipe.base.QuantumGraphBuilder` for running
507 `HighResolutionHipsTask` only.
509 This is a workaround for incomplete butler query support for HEALPix
514 pipeline_graph : `lsst.pipe.base.PipelineGraph`
515 Pipeline graph with exactly one task, which must be a configuration
516 of `HighResolutionHipsTask`.
517 butler : `lsst.daf.butler.Butler`
518 Client for the butler data repository. May be read-only.
519 input_collections : `str` or `Iterable` [ `str` ], optional
520 Collection or collections to search for input datasets, in order.
521 If not provided, ``butler.collections`` will be searched.
522 output_run : `str`, optional
523 Name of the output collection. If not provided, ``butler.run`` will
525 constraint_order : `int`
526 HEALPix order used to constrain which quanta are generated, via
527 ``constraint_indices``. This should be a coarser grid (smaller
528 order) than the order used for the task's quantum and output data
529 IDs, and ideally something between the spatial scale of a patch or
530 the data repository's "common skypix" system (usually ``htm7``).
531 constraint_ranges : `lsst.sphgeom.RangeSet`
532 RangeSet that describes constraint pixels (HEALPix NEST, with order
533 ``constraint_order``) to constrain generated quanta.
534 where : `str`, optional
535 A boolean `str` expression of the form accepted by
536 `lsst.daf.butler.Butler` to constrain input datasets. This may
537 contain a constraint on tracts, patches, or bands, but not HEALPix
538 indices. Constraints on tracts and patches should usually be
539 unnecessary, however - existing coadds that overlap the given
540 HEALpix indices will be selected without such a constraint, and
541 providing one may reject some that should normally be included.
549 input_collections=None,
555 super().__init__(pipeline_graph, butler, input_collections=input_collections, output_run=output_run)
556 self.constraint_order = constraint_order
557 self.constraint_ranges = constraint_ranges
560 def process_subgraph(self, subgraph):
562 (task_node,) = subgraph.tasks.values()
566 (input_dataset_type_node,) = subgraph.inputs_of(task_node.label).values()
567 assert input_dataset_type_node
is not None,
"PipelineGraph should be resolved by base class."
568 (output_edge,) = task_node.outputs.values()
569 output_dataset_type_node = subgraph.dataset_types[output_edge.parent_dataset_type_name]
570 (hpx_output_dimension,) = (
571 self.butler.dimensions.skypix_dimensions[d]
for d
in output_dataset_type_node.dimensions.skypix
573 constraint_hpx_pixelization = self.butler.dimensions.skypix_dimensions[
574 f
"healpix{self.constraint_order}"
576 common_skypix_pixelization = self.butler.dimensions.commonSkyPix.pixelization
582 self.butler.dimensions.skypix_dimensions[d]
for d
in task_node.dimensions.names
if d !=
"band"
584 hpx_pixelization = hpx_dimension.pixelization
585 if hpx_pixelization.level < self.constraint_order:
586 raise ValueError(f
"Quantum order {hpx_pixelization.level} must be < {self.constraint_order}")
587 hpx_ranges = self.constraint_ranges.scaled(4 ** (hpx_pixelization.level - self.constraint_order))
592 for begin, end
in self.constraint_ranges:
593 for hpx_index
in range(begin, end):
594 constraint_hpx_region = constraint_hpx_pixelization.pixel(hpx_index)
595 common_skypix_ranges |= common_skypix_pixelization.envelope(constraint_hpx_region)
599 for simp
in range(1, 10):
600 if len(common_skypix_ranges) < 100:
602 common_skypix_ranges.simplify(simp)
609 xf = ExpressionFactory(self.butler.dimensions)
610 common_skypix_proxy = getattr(xf, self.butler.dimensions.commonSkyPix.name)
612 for begin, end
in common_skypix_ranges:
614 where_terms.append(common_skypix_proxy == begin)
616 where_terms.append(common_skypix_proxy.in_range(begin, end))
617 where = xf.any(*where_terms)
621 with self.butler.query()
as query:
623 query.datasets(input_dataset_type_node.dataset_type, collections=self.input_collections)
628 .with_dimension_records()
630 inputs_by_patch = defaultdict(set)
631 patch_dimensions = self.butler.dimensions.conform([
"patch"])
632 skeleton = QuantumGraphSkeleton([task_node.label])
633 for input_ref
in input_refs:
634 dataset_key = skeleton.add_dataset_node(input_ref.datasetType.name, input_ref.dataId)
635 skeleton.set_dataset_ref(input_ref, dataset_key)
636 inputs_by_patch[input_ref.dataId.subset(patch_dimensions)].add(dataset_key)
637 if not inputs_by_patch:
638 message_body =
"\n".join(input_refs.explain_no_results())
639 raise RuntimeError(f
"No inputs found:\n{message_body}")
644 inputs_by_hpx = defaultdict(set)
645 for patch_data_id, input_keys_for_patch
in inputs_by_patch.items():
646 patch_hpx_ranges = hpx_pixelization.envelope(patch_data_id.region)
647 for begin, end
in patch_hpx_ranges & hpx_ranges:
648 for hpx_index
in range(begin, end):
649 inputs_by_hpx[hpx_index].update(input_keys_for_patch)
652 for hpx_index, input_keys_for_hpx_index
in inputs_by_hpx.items():
654 input_keys_by_band = defaultdict(list)
655 for input_key
in input_keys_for_hpx_index:
656 input_ref = skeleton.get_dataset_ref(input_key)
657 assert input_ref
is not None,
"Code above adds the same nodes to the graph with refs."
658 input_keys_by_band[input_ref.dataId[
"band"]].append(input_key)
660 for band, input_keys_for_band
in input_keys_by_band.items():
661 data_id = self.butler.registry.expandDataId({hpx_dimension.name: hpx_index,
"band": band})
662 quantum_key = skeleton.add_quantum_node(task_node.label, data_id)
664 skeleton.add_input_edges(quantum_key, input_keys_for_band)
666 hpx_pixel_ranges =
RangeSet(hpx_index)
667 hpx_output_ranges = hpx_pixel_ranges.scaled(
668 4 ** (task_node.config.hips_order - hpx_pixelization.level)
670 for begin, end
in hpx_output_ranges:
671 for hpx_output_index
in range(begin, end):
672 dataset_key = skeleton.add_dataset_node(
673 output_dataset_type_node.name,
674 self.butler.registry.expandDataId(
675 {hpx_output_dimension: hpx_output_index,
"band": band}
678 skeleton.add_output_edge(quantum_key, dataset_key)
680 for write_edge
in task_node.iter_all_outputs():
681 if write_edge.connection_name == output_edge.connection_name:
683 dataset_key = skeleton.add_dataset_node(write_edge.parent_dataset_type_name, data_id)
684 skeleton.add_output_edge(quantum_key, dataset_key)
688class HipsPropertiesSpectralTerm(pexConfig.Config):
689 lambda_min = pexConfig.Field(
690 doc=
"Minimum wavelength (nm)",
693 lambda_max = pexConfig.Field(
694 doc=
"Maximum wavelength (nm)",
699class HipsPropertiesConfig(pexConfig.Config):
700 """Configuration parameters for writing a HiPS properties file."""
702 creator_did_template = pexConfig.Field(
703 doc=(
"Unique identifier of the HiPS - Format: IVOID. Use ``{band}`` to substitute the band name."),
707 obs_collection = pexConfig.Field(
708 doc=
"Short name of original data set - Format: one word",
712 obs_description_template = pexConfig.Field(
714 "Data set description - Format: free text, longer free text "
715 "description of the dataset. Use ``{band}`` to substitute "
720 prov_progenitor = pexConfig.ListField(
721 doc=
"Provenance of the original data - Format: free text",
725 obs_title_template = pexConfig.Field(
727 "Data set title format: free text, but should be short. "
728 "Use ``{band}`` to substitute the band name."
733 spectral_ranges = pexConfig.ConfigDictField(
734 doc=(
"Mapping from band to lambda_min, lamba_max (nm). May be approximate."),
736 itemtype=HipsPropertiesSpectralTerm,
739 initial_ra = pexConfig.Field(
740 doc=
"Initial RA (deg) (default for HiPS viewer). If not set will use a point in MOC.",
744 initial_dec = pexConfig.Field(
745 doc=
"Initial Declination (deg) (default for HiPS viewer). If not set will use a point in MOC.",
749 initial_fov = pexConfig.Field(
750 doc=
"Initial field-of-view (deg). If not set will use ~1 healpix tile.",
754 obs_ack = pexConfig.Field(
755 doc=
"Observation acknowledgements (free text).",
759 t_min = pexConfig.Field(
760 doc=
"Time (MJD) of earliest observation included in HiPS",
764 t_max = pexConfig.Field(
765 doc=
"Time (MJD) of latest observation included in HiPS",
773 if self.obs_collection
is not None:
774 if re.search(
r"\s", self.obs_collection):
775 raise ValueError(
"obs_collection cannot contain any space characters.")
777 def setDefaults(self):
780 u_term = HipsPropertiesSpectralTerm()
781 u_term.lambda_min = 330.0
782 u_term.lambda_max = 400.0
783 self.spectral_ranges[
"u"] = u_term
784 g_term = HipsPropertiesSpectralTerm()
785 g_term.lambda_min = 402.0
786 g_term.lambda_max = 552.0
787 self.spectral_ranges[
"g"] = g_term
788 r_term = HipsPropertiesSpectralTerm()
789 r_term.lambda_min = 552.0
790 r_term.lambda_max = 691.0
791 self.spectral_ranges[
"r"] = r_term
792 i_term = HipsPropertiesSpectralTerm()
793 i_term.lambda_min = 691.0
794 i_term.lambda_max = 818.0
795 self.spectral_ranges[
"i"] = i_term
796 z_term = HipsPropertiesSpectralTerm()
797 z_term.lambda_min = 818.0
798 z_term.lambda_max = 922.0
799 self.spectral_ranges[
"z"] = z_term
800 y_term = HipsPropertiesSpectralTerm()
801 y_term.lambda_min = 970.0
802 y_term.lambda_max = 1060.0
803 self.spectral_ranges[
"y"] = y_term
806class GenerateHipsConnections(
807 pipeBase.PipelineTaskConnections,
808 dimensions=(
"instrument",
"band"),
809 defaultTemplates={
"coaddName":
"deep"},
811 hips_exposure_handles = pipeBase.connectionTypes.Input(
812 doc=
"HiPS-compatible HPX images.",
813 name=
"{coaddName}Coadd_hpx",
814 storageClass=
"ExposureF",
815 dimensions=(
"healpix11",
"band"),
820 def __init__(self, *, config):
821 super().__init__(config=config)
822 if config.parallel_highest_order:
823 healpix_dimensions = self.hips_exposure_handles.dimensions
824 for dim
in healpix_dimensions:
828 current_dimensions = self.dimensions
829 self.dimensions = set((*current_dimensions, hdim))
832class GenerateHipsConfig(pipeBase.PipelineTaskConfig, pipelineConnections=GenerateHipsConnections):
833 """Configuration parameters for GenerateHipsTask."""
839 hips_base_uri = pexConfig.Field(
840 doc=
"URI to HiPS base for output.",
844 min_order = pexConfig.Field(
845 doc=
"Minimum healpix order for HiPS tree.",
849 properties = pexConfig.ConfigField(
850 dtype=HipsPropertiesConfig,
851 doc=
"Configuration for properties file.",
853 allsky_tilesize = pexConfig.Field(
855 doc=
"Allsky tile size; must be power of 2. HiPS standard recommends 64x64 tiles.",
857 check=_is_power_of_two,
859 png_gray_asinh_minimum = pexConfig.Field(
860 doc=
"AsinhMapping intensity to be mapped to black for grayscale png scaling.",
864 png_gray_asinh_stretch = pexConfig.Field(
865 doc=
"AsinhMapping linear stretch for grayscale png scaling.",
869 png_gray_asinh_softening = pexConfig.Field(
870 doc=
"AsinhMapping softening parameter (Q) for grayscale png scaling.",
874 parallel_highest_order = pexConfig.Field[bool](
876 "If this is set to True, each of the highest order hips pixels will"
877 " be put into their own quanta, and can be run in parallel. The "
878 "trade off is the highest order must be run alone by setting "
879 "min_order to be the the same as the healpix dimension. This will "
880 "skip writing all sky info, which must be done in another "
881 "invocation of this task."
885 skip_highest_image = pexConfig.Field[bool](
887 "This option should be used if in another task instance "
888 "parallel_highest_order was set to True. This option will skip "
889 "making and writing png for the highest order."
893 file_extension = pexConfig.ChoiceField[str](
894 doc=
"Extension for the presisted image, must be png or webp",
895 allowed={
"png":
"Use the png image extension",
"webp":
"Use the webp image extension"},
900 if self.parallel_highest_order:
901 dimensions = self.connections.ConnectionsClass.hips_exposure_handles.dimensions
903 for dim
in dimensions:
905 order = int(dim.replace(
"healpix",
""))
907 raise RuntimeError(
"Could not determine the healpix dim order")
908 if self.min_order != order:
910 "min_order must be the same as healpix order if parallel_highest_order is True"
912 if self.skip_highest_image:
913 raise ValueError(
"Skip_highest_image should be False when parallel_highest_order is True")
916class GenerateHipsTask(pipeBase.PipelineTask):
917 """Task for making a HiPS tree with FITS and grayscale PNGs."""
919 ConfigClass = GenerateHipsConfig
920 _DefaultName =
"generateHips"
926 def runQuantum(self, butlerQC, inputRefs, outputRefs):
927 inputs = butlerQC.get(inputRefs)
929 dims = inputRefs.hips_exposure_handles[0].dataId.dimensions.names
933 order = int(dim.split(
"healpix")[1])
937 raise RuntimeError(
"Could not determine healpix order for input exposures.")
939 hips_exposure_handle_dict = {
941 hips_exposure_handle.dataId[healpix_dim],
942 hips_exposure_handle.dataId[
"band"],
943 ): hips_exposure_handle
944 for hips_exposure_handle
in inputs[
"hips_exposure_handles"]
948 hips_exposure_handle.dataId[
"band"]
for hips_exposure_handle
in inputs[
"hips_exposure_handles"]
950 bands = self._check_data_bands(data_bands)
955 hips_exposure_handle_dict=hips_exposure_handle_dict,
956 do_color=self.color_task,
959 def _check_data_bands(self, data_bands):
960 """Check that the data has only a single band.
964 data_bands : `set` [`str`]
965 Bands from the input data.
969 bands : `list` [`str`]
970 List of single band to process.
974 RuntimeError if there is not exactly one band.
976 if len(data_bands) != 1:
977 raise RuntimeError(
"GenerateHipsTask can only use data from a single band.")
979 return list(data_bands)
982 def run(self, bands, max_order, hips_exposure_handle_dict, do_color=False):
983 """Run the GenerateHipsTask.
987 bands : `list [ `str` ]
988 List of bands to be processed (or single band).
990 HEALPix order of the maximum (native) HPX exposures.
991 hips_exposure_handle_dict : `dict` {`int`: `lsst.daf.butler.DeferredDatasetHandle`}
992 Dict of handles for the HiPS high-resolution exposures.
993 Key is (pixel number, ``band``).
994 do_color : `bool`, optional
995 Do color pngs instead of per-band grayscale.
997 min_order = self.config.min_order
1001 self.config.png_gray_asinh_minimum,
1002 self.config.png_gray_asinh_stretch,
1003 Q=self.config.png_gray_asinh_softening,
1006 match self.config.rgbStyle:
1009 self.config.png_color_asinh_minimum,
1010 self.config.png_color_asinh_stretch,
1011 Q=self.config.png_color_asinh_softening,
1014 bcb = self.config.blue_channel_band
1015 gcb = self.config.green_channel_band
1016 rcb = self.config.red_channel_band
1017 colorstr = f
"{bcb}{gcb}{rcb}"
1019 colorstr =
"".join(bands)
1022 hips_base_path = ResourcePath(self.config.hips_base_uri, forceDirectory=
True)
1026 pixels = np.unique(np.array([pixel
for pixel, _
in hips_exposure_handle_dict.keys()]))
1029 pixels = np.append(pixels, [0])
1033 pixels_shifted[max_order] = pixels
1034 for order
in range(max_order - 1, min_order - 1, -1):
1035 pixels_shifted[order] = np.right_shift(pixels_shifted[order + 1], 2)
1038 for order
in range(min_order, max_order + 1):
1039 pixels_shifted[order][-1] = -1
1042 exp0 = list(hips_exposure_handle_dict.values())[0].get()
1043 bbox = exp0.getBBox()
1044 npix = bbox.getWidth()
1045 shift_order = int(np.round(np.log2(npix)))
1052 for order
in range(min_order, max_order + 1):
1053 exp = exp0.Factory(bbox=bbox)
1054 exp.image.array[:, :] = np.nan
1055 exposures[(band, order)] = exp
1058 for pixel_counter, pixel
in enumerate(pixels[:-1]):
1059 self.log.debug(
"Working on high resolution pixel %d", pixel)
1065 if (pixel, band)
in hips_exposure_handle_dict:
1066 exposures[(band, max_order)] = hips_exposure_handle_dict[(pixel, band)].get()
1072 for order
in range(max_order, min_order - 1, -1):
1073 if pixels_shifted[order][pixel_counter + 1] == pixels_shifted[order][pixel_counter]:
1079 if self.config.skip_highest_image
and order == max_order:
1080 do_write_image =
False
1082 do_write_image =
True
1087 self._write_hips_image(
1088 hips_base_path.join(f
"band_{band}", forceDirectory=
True),
1090 pixels_shifted[order][pixel_counter],
1091 exposures[(band, order)].image,
1092 png_grayscale_mapping,
1093 shift_order=shift_order,
1099 match self.config.rgbStyle:
1101 lupton_args[
"image_red"] = exposures[
1102 (self.config.red_channel_band, order)
1104 lupton_args[
"image_green"] = exposures[
1105 (self.config.green_channel_band, order)
1108 lupton_args[
"image_blue"] = exposures[
1109 (self.config.blue_channel_band, order)
1112 lupton_args[
"png_mapping"] = png_color_mapping
1117 if (value := exposures.get(key))
is not None:
1118 band_mapping[band] = value
1119 lsstRGB_args[
"band_mapping"] = band_mapping
1121 self._write_hips_color_png(
1122 hips_base_path.join(f
"color_{colorstr}", forceDirectory=
True),
1124 pixels_shifted[order][pixel_counter],
1129 log_level = self.log.INFO
if order == (max_order - 3)
else self.log.DEBUG
1132 "Completed HiPS generation for %s, order %d, pixel %d (%d/%d)",
1135 pixels_shifted[order][pixel_counter],
1141 if order == min_order:
1143 exposures[(band, order)].image.array[:, :] = np.nan
1148 arr = exposures[(band, order)].image.array.reshape(npix // 2, 2, npix // 2, 2)
1149 with warnings.catch_warnings():
1150 warnings.simplefilter(
"ignore")
1151 binned_image_arr = np.nanmean(arr, axis=(1, 3))
1155 sub_index = pixels_shifted[order][pixel_counter] - np.left_shift(
1156 pixels_shifted[order - 1][pixel_counter], 2
1160 exp = exposures[(band, order - 1)]
1164 exp.image.array[npix // 2 :, 0 : npix // 2] = binned_image_arr
1165 elif sub_index == 1:
1166 exp.image.array[0 : npix // 2, 0 : npix // 2] = binned_image_arr
1167 elif sub_index == 2:
1168 exp.image.array[npix // 2 :, npix // 2 :] = binned_image_arr
1169 elif sub_index == 3:
1170 exp.image.array[0 : npix // 2, npix // 2 :] = binned_image_arr
1173 raise ValueError(
"Illegal pixel sub index")
1176 if order < max_order:
1177 exposures[(band, order)].image.array[:, :] = np.nan
1179 if not self.config.parallel_highest_order:
1183 band_pixels = np.array(
1184 [pixel
for pixel, band_
in hips_exposure_handle_dict.keys()
if band_ == band]
1186 band_pixels = np.sort(band_pixels)
1188 self._write_properties_and_moc(
1189 hips_base_path.join(f
"band_{band}", forceDirectory=
True),
1197 self._write_allsky_file(
1198 hips_base_path.join(f
"band_{band}", forceDirectory=
True),
1202 self._write_properties_and_moc(
1203 hips_base_path.join(f
"color_{colorstr}", forceDirectory=
True),
1211 self._write_allsky_file(
1212 hips_base_path.join(f
"color_{colorstr}", forceDirectory=
True),
1216 def _write_hips_image(self, hips_base_path, order, pixel, image, png_mapping, shift_order=9):
1217 """Write a HiPS image.
1221 hips_base_path : `lsst.resources.ResourcePath`
1222 Resource path to the base of the HiPS directory tree.
1224 HEALPix order of the HiPS image to write.
1226 HEALPix pixel of the HiPS image.
1227 image : `lsst.afw.image.Image`
1229 png_mapping : `astropy.visualization.lupton_rgb.AsinhMapping`
1230 Mapping to convert image to scaled png.
1231 shift_order : `int`, optional
1239 dir_number = self._get_dir_number(pixel)
1240 hips_dir = hips_base_path.join(f
"Norder{order}", forceDirectory=
True).join(
1241 f
"Dir{dir_number}", forceDirectory=
True
1244 wcs = makeHpxWcs(order, pixel, shift_order=shift_order)
1246 uri = hips_dir.join(f
"Npix{pixel}.fits")
1248 with ResourcePath.temporary_uri(suffix=uri.getExtension())
as temporary_uri:
1249 image.writeFits(temporary_uri.ospath, metadata=wcs.getFitsMetadata())
1251 uri.transfer_from(temporary_uri, transfer=
"copy", overwrite=
True)
1255 with np.errstate(invalid=
"ignore"):
1256 vals = 255 - png_mapping.map_intensity_to_uint8(image.array).astype(np.uint8)
1258 vals[~np.isfinite(image.array) | (image.array < 0)] = 0
1259 im = Image.fromarray(vals[::-1, :],
"L")
1261 uri = hips_dir.join(f
"Npix{pixel}.{self.config.file_extension}")
1263 with ResourcePath.temporary_uri(suffix=uri.getExtension())
as temporary_uri:
1264 im.save(temporary_uri.ospath)
1266 uri.transfer_from(temporary_uri, transfer=
"copy", overwrite=
True)
1268 def _write_hips_color_png(self, hips_base_path, order, pixel, lupton_args, lsstRGB_args):
1269 """Write a color png HiPS image.
1273 hips_base_path : `lsst.resources.ResourcePath`
1274 Resource path to the base of the HiPS directory tree.
1276 HEALPix order of the HiPS image to write.
1278 HEALPix pixel of the HiPS image.
1279 lupton_args : `dict`
1280 A mapping of parameters used when building lupton color images.
1281 lsstRGB_args : `dict`
1282 A mapping of parameters used when building lsstRGB color images.
1289 dir_number = self._get_dir_number(pixel)
1290 hips_dir = hips_base_path.join(f
"Norder{order}", forceDirectory=
True).join(
1291 f
"Dir{dir_number}", forceDirectory=
True
1293 match self.config.rgbStyle:
1296 png_mapping = lupton_args[
"png_mapping"]
1297 arr_red = lupton_args[
"image_red"].array.copy()
1298 arr_red[np.isnan(arr_red)] = png_mapping.minimum[0]
1299 arr_green = lupton_args[
"image_green"].array.copy()
1300 arr_green[np.isnan(arr_green)] = png_mapping.minimum[1]
1301 arr_blue = lupton_args[
"image_blue"].array.copy()
1302 arr_blue[np.isnan(arr_blue)] = png_mapping.minimum[2]
1304 image_array = png_mapping.make_rgb_image(arr_red, arr_green, arr_blue)
1306 image_array = self.rgbGenerator.run(lsstRGB_args[
"band_mapping"]).outputRGB
1308 im = Image.fromarray(image_array[::-1, :, :], mode=
"RGB")
1310 uri = hips_dir.join(f
"Npix{pixel}.{self.config.file_extension}")
1313 if self.config.file_extension ==
"webp":
1314 extra_args[
"lossless"] =
True
1315 extra_args[
"quality"] = 80
1317 with ResourcePath.temporary_uri(suffix=uri.getExtension())
as temporary_uri:
1318 im.save(temporary_uri.ospath, **extra_args)
1320 uri.transfer_from(temporary_uri, transfer=
"copy", overwrite=
True)
1322 def _write_properties_and_moc(
1323 self, hips_base_path, max_order, pixels, exposure, shift_order, band, multiband
1325 """Write HiPS properties file and MOC.
1329 hips_base_path : : `lsst.resources.ResourcePath`
1330 Resource path to the base of the HiPS directory tree.
1332 Maximum HEALPix order.
1333 pixels : `np.ndarray` (N,)
1334 Array of pixels used.
1335 exposure : `lsst.afw.image.Exposure`
1336 Sample HPX exposure used for generating HiPS tiles.
1342 Is band multiband / color?
1344 area = hpg.nside_to_pixel_area(2**max_order, degrees=
True) * len(pixels)
1346 initial_ra = self.config.properties.initial_ra
1347 initial_dec = self.config.properties.initial_dec
1348 initial_fov = self.config.properties.initial_fov
1350 if initial_ra
is None or initial_dec
is None or initial_fov
is None:
1353 temp_pixels = pixels.copy()
1354 if temp_pixels.size % 2 == 0:
1355 temp_pixels = np.append(temp_pixels, [temp_pixels[0]])
1356 medpix = int(np.median(temp_pixels))
1357 _initial_ra, _initial_dec = hpg.pixel_to_angle(2**max_order, medpix)
1358 _initial_fov = hpg.nside_to_resolution(2**max_order, units=
"arcminutes") / 60.0
1360 if initial_ra
is None or initial_dec
is None:
1361 initial_ra = _initial_ra
1362 initial_dec = _initial_dec
1363 if initial_fov
is None:
1364 initial_fov = _initial_fov
1366 self._write_hips_properties_file(
1368 self.config.properties,
1381 self._write_hips_moc_file(
1387 def _write_hips_properties_file(
1401 """Write HiPS properties file.
1405 hips_base_path : `lsst.resources.ResourcePath`
1406 ResourcePath at top of HiPS tree. File will be written
1407 to this path as ``properties``.
1408 properties_config : `lsst.pipe.tasks.hips.HipsPropertiesConfig`
1409 Configuration for properties values.
1411 Name of band(s) for HiPS tree.
1413 Is multiband / color?
1414 exposure : `lsst.afw.image.Exposure`
1415 Sample HPX exposure used for generating HiPS tiles.
1417 Maximum HEALPix order.
1421 Coverage area in square degrees.
1422 initial_ra : `float`
1423 Initial HiPS RA position (degrees).
1424 initial_dec : `float`
1425 Initial HiPS Dec position (degrees).
1426 initial_fov : `float`
1427 Initial HiPS display size (degrees).
1434 def _write_property(fh, name, value):
1435 """Write a property name/value to a file handle.
1439 fh : file handle (blah)
1448 if re.search(
r"\s", name):
1449 raise ValueError(f
"``{name}`` cannot contain any space characters.")
1451 raise ValueError(f
"``{name}`` cannot contain an ``=``")
1453 fh.write(f
"{name:25}= {value}\n")
1455 if exposure.image.array.dtype == np.dtype(
"float32"):
1457 elif exposure.image.array.dtype == np.dtype(
"float64"):
1459 elif exposure.image.array.dtype == np.dtype(
"int32"):
1462 date_iso8601 = datetime.utcnow().isoformat(timespec=
"seconds") +
"Z"
1463 pixel_scale = hpg.nside_to_resolution(2 ** (max_order + shift_order), units=
"degrees")
1465 uri = hips_base_path.join(
"properties")
1466 with ResourcePath.temporary_uri(suffix=uri.getExtension())
as temporary_uri:
1467 with open(temporary_uri.ospath,
"w")
as fh:
1471 properties_config.creator_did_template.format(band=band),
1473 if properties_config.obs_collection
is not None:
1474 _write_property(fh,
"obs_collection", properties_config.obs_collection)
1478 properties_config.obs_title_template.format(band=band),
1480 if properties_config.obs_description_template
is not None:
1484 properties_config.obs_description_template.format(band=band),
1486 if len(properties_config.prov_progenitor) > 0:
1487 for prov_progenitor
in properties_config.prov_progenitor:
1488 _write_property(fh,
"prov_progenitor", prov_progenitor)
1489 if properties_config.obs_ack
is not None:
1490 _write_property(fh,
"obs_ack", properties_config.obs_ack)
1491 _write_property(fh,
"obs_regime",
"Optical")
1492 _write_property(fh,
"data_pixel_bitpix", str(bitpix))
1493 _write_property(fh,
"dataproduct_type",
"image")
1494 _write_property(fh,
"moc_sky_fraction", str(area / 41253.0))
1495 _write_property(fh,
"data_ucd",
"phot.flux")
1496 _write_property(fh,
"hips_creation_date", date_iso8601)
1497 _write_property(fh,
"hips_builder",
"lsst.pipe.tasks.hips.GenerateHipsTask")
1498 _write_property(fh,
"hips_creator",
"Vera C. Rubin Observatory")
1499 _write_property(fh,
"hips_version",
"1.4")
1500 _write_property(fh,
"hips_release_date", date_iso8601)
1501 _write_property(fh,
"hips_frame",
"equatorial")
1502 _write_property(fh,
"hips_order", str(max_order))
1503 _write_property(fh,
"hips_tile_width", str(exposure.getBBox().getWidth()))
1504 _write_property(fh,
"hips_status",
"private master clonableOnce")
1506 _write_property(fh,
"hips_tile_format", self.config.file_extension)
1507 _write_property(fh,
"dataproduct_subtype",
"color")
1509 _write_property(fh,
"hips_tile_format", f
"{self.config.file_extension} fits")
1510 _write_property(fh,
"hips_pixel_bitpix", str(bitpix))
1511 _write_property(fh,
"hips_pixel_scale", str(pixel_scale))
1512 _write_property(fh,
"hips_initial_ra", str(initial_ra))
1513 _write_property(fh,
"hips_initial_dec", str(initial_dec))
1514 _write_property(fh,
"hips_initial_fov", str(initial_fov))
1516 if self.config.blue_channel_band
in properties_config.spectral_ranges:
1518 properties_config.spectral_ranges[self.config.blue_channel_band].lambda_min / 1e9
1521 self.log.warning(
"blue band %s not in self.config.spectral_ranges.", band)
1523 if self.config.red_channel_band
in properties_config.spectral_ranges:
1525 properties_config.spectral_ranges[self.config.red_channel_band].lambda_max / 1e9
1528 self.log.warning(
"red band %s not in self.config.spectral_ranges.", band)
1531 if band
in properties_config.spectral_ranges:
1532 em_min = properties_config.spectral_ranges[band].lambda_min / 1e9
1533 em_max = properties_config.spectral_ranges[band].lambda_max / 1e9
1535 self.log.warning(
"band %s not in self.config.spectral_ranges.", band)
1538 _write_property(fh,
"em_min", str(em_min))
1539 _write_property(fh,
"em_max", str(em_max))
1540 if properties_config.t_min
is not None:
1541 _write_property(fh,
"t_min", properties_config.t_min)
1542 if properties_config.t_max
is not None:
1543 _write_property(fh,
"t_max", properties_config.t_max)
1545 uri.transfer_from(temporary_uri, transfer=
"copy", overwrite=
True)
1547 def _write_hips_moc_file(self, hips_base_path, max_order, pixels, min_uniq_order=1):
1548 """Write HiPS MOC file.
1552 hips_base_path : `lsst.resources.ResourcePath`
1553 ResourcePath to top of HiPS tree. File will be written as
1554 to this path as ``Moc.fits``.
1556 Maximum HEALPix order.
1557 pixels : `np.ndarray`
1558 Array of pixels covered.
1559 min_uniq_order : `int`, optional
1560 Minimum HEALPix order for looking for fully covered pixels.
1568 uniq = 4 * (4**max_order) + pixels
1571 hspmap = hsp.HealSparseMap.make_empty(2**min_uniq_order, 2**max_order, dtype=np.float32)
1572 hspmap[pixels] = 1.0
1575 for uniq_order
in range(max_order - 1, min_uniq_order - 1, -1):
1576 hspmap = hspmap.degrade(2**uniq_order, reduction=
"sum")
1577 pix_shift = np.right_shift(pixels, 2 * (max_order - uniq_order))
1579 (covered,) = np.isclose(hspmap[pix_shift], 4 ** (max_order - uniq_order)).nonzero()
1580 if covered.size == 0:
1584 uniq[covered] = 4 * (4**uniq_order) + pix_shift[covered]
1587 uniq = np.unique(uniq)
1590 tbl = np.zeros(uniq.size, dtype=[(
"UNIQ",
"i8")])
1593 order = np.log2(tbl[
"UNIQ"] // 4).astype(np.int32) // 2
1594 moc_order = np.max(order)
1596 hdu = fits.BinTableHDU(tbl)
1597 hdu.header[
"PIXTYPE"] =
"HEALPIX"
1598 hdu.header[
"ORDERING"] =
"NUNIQ"
1599 hdu.header[
"COORDSYS"] =
"C"
1600 hdu.header[
"MOCORDER"] = moc_order
1601 hdu.header[
"MOCTOOL"] =
"lsst.pipe.tasks.hips.GenerateHipsTask"
1603 uri = hips_base_path.join(
"Moc.fits")
1605 with ResourcePath.temporary_uri(suffix=uri.getExtension())
as temporary_uri:
1606 hdu.writeto(temporary_uri.ospath)
1608 uri.transfer_from(temporary_uri, transfer=
"copy", overwrite=
True)
1610 def _write_allsky_file(self, hips_base_path, allsky_order):
1611 """Write an Allsky.png file.
1615 hips_base_path : `lsst.resources.ResourcePath`
1616 Resource path to the base of the HiPS directory tree.
1617 allsky_order : `int`
1618 HEALPix order of the minimum order to make allsky file.
1620 tile_size = self.config.allsky_tilesize
1632 n_tiles = hpg.nside_to_npixel(hpg.order_to_nside(allsky_order))
1633 n_tiles_wide = int(np.floor(np.sqrt(n_tiles)))
1634 n_tiles_high = int(np.ceil(n_tiles / n_tiles_wide))
1638 allsky_order_uri = hips_base_path.join(f
"Norder{allsky_order}", forceDirectory=
True)
1639 if self.config.file_extension ==
"png":
1640 pixel_regex = re.compile(
r"Npix([0-9]+)\.png$")
1641 elif self.config.file_extension ==
"webp":
1642 pixel_regex = re.compile(
r"Npix([0-9]+)\.webp$")
1644 raise RuntimeError(
"Unknown file extension")
1647 ResourcePath.findFileResources(
1648 candidates=[allsky_order_uri],
1649 file_filter=pixel_regex,
1653 for png_uri
in png_uris:
1654 matches = re.match(pixel_regex, png_uri.basename())
1655 pix_num = int(matches.group(1))
1656 tile_image = Image.open(io.BytesIO(png_uri.read()))
1657 row = math.floor(pix_num // n_tiles_wide)
1658 column = pix_num % n_tiles_wide
1659 box = (column * tile_size, row * tile_size, (column + 1) * tile_size, (row + 1) * tile_size)
1660 tile_image_shrunk = tile_image.resize((tile_size, tile_size))
1662 if allsky_image
is None:
1663 allsky_image = Image.new(
1665 (n_tiles_wide * tile_size, n_tiles_high * tile_size),
1667 allsky_image.paste(tile_image_shrunk, box)
1669 uri = allsky_order_uri.join(f
"Allsky.{self.config.file_extension}")
1671 with ResourcePath.temporary_uri(suffix=uri.getExtension())
as temporary_uri:
1672 allsky_image.save(temporary_uri.ospath)
1674 uri.transfer_from(temporary_uri, transfer=
"copy", overwrite=
True)
1676 def _get_dir_number(self, pixel):
1677 """Compute the directory number from a pixel.
1682 HEALPix pixel number.
1687 HiPS directory number.
1689 return (pixel // 10000) * 10000
1692class GenerateColorHipsConnections(
1693 pipeBase.PipelineTaskConnections, dimensions=(
"instrument",), defaultTemplates={
"coaddName":
"deep"}
1695 hips_exposure_handles = pipeBase.connectionTypes.Input(
1696 doc=
"HiPS-compatible HPX images.",
1697 name=
"{coaddName}Coadd_hpx",
1698 storageClass=
"ExposureF",
1699 dimensions=(
"healpix11",
"band"),
1704 def __init__(self, *, config):
1705 super().__init__(config=config)
1706 if config.parallel_highest_order:
1707 healpix_dimensions = self.hips_exposure_handles.dimensions
1708 for dim
in healpix_dimensions:
1709 if "healpix" in dim:
1712 current_dimensions = self.dimensions
1713 self.dimensions = set((*current_dimensions, hdim))
1716class GenerateColorHipsConfig(GenerateHipsConfig, pipelineConnections=GenerateColorHipsConnections):
1717 """Configuration parameters for GenerateColorHipsTask."""
1719 blue_channel_band = pexConfig.Field(
1720 doc=
"Band to use for blue channel of color pngs in lupton color mapping.",
1724 green_channel_band = pexConfig.Field(
1725 doc=
"Band to use for green channel of color pngs in lupton color mapping.",
1729 red_channel_band = pexConfig.Field(
1730 doc=
"Band to use for red channel of color pngs in lupton color mapping.",
1734 png_color_asinh_minimum = pexConfig.Field(
1735 doc=
"AsinhMapping intensity to be mapped to black for color png scaling in lupton color mapping.",
1739 png_color_asinh_stretch = pexConfig.Field(
1740 doc=
"AsinhMapping linear stretch for color png scaling in lupton color mapping.",
1744 png_color_asinh_softening = pexConfig.Field(
1745 doc=
"AsinhMapping softening parameter (Q) for color png scaling in lupton color mapping.",
1749 rgbGenerator = pexConfig.ConfigurableField[PrettyPictureTask](
1750 doc=
"The task to use to generate an RGB image", target=PrettyPictureTask
1752 rgbStyle = pexConfig.ChoiceField[str](
1753 doc=
"The rgb mapping style, must be one of lsstRGB or lupton",
1755 "lupton":
"Use the lupton algorithm for RGB images",
1756 "lsstRGB":
"Use new style lsstRGB color algorithm for RGB images",
1761 def setDefaults(self):
1762 super().setDefaults()
1763 self.rgbGenerator: PrettyPictureConfig
1764 self.rgbGenerator.imageRemappingConfig.absMax = 550
1765 self.rgbGenerator.luminanceConfig.Q = 0.7
1766 self.rgbGenerator.doPSFDeconcovlve =
False
1767 self.rgbGenerator.exposureBrackets =
None
1768 self.rgbGenerator.localContrastConfig.doLocalContrast =
False
1769 self.rgbGenerator.luminanceConfig.stretch = 250
1770 self.rgbGenerator.luminanceConfig.max = 100
1771 self.rgbGenerator.luminanceConfig.highlight = 0.905882
1772 self.rgbGenerator.luminanceConfig.shadow = 0.12
1773 self.rgbGenerator.luminanceConfig.midtone = 0.25
1774 self.rgbGenerator.colorConfig.maxChroma = 80
1775 self.rgbGenerator.colorConfig.saturation = 0.6
1776 self.rgbGenerator.cieWhitePoint = (0.28, 0.28)
1781class GenerateColorHipsTask(GenerateHipsTask):
1782 """Task for making a HiPS tree with color pngs."""
1784 ConfigClass = GenerateColorHipsConfig
1785 _DefaultName =
"generateColorHips"
1788 def __init__(self, *, config=None, log=None, initInputs=None, **kwargs):
1789 super().__init__(config=config, log=log, **kwargs)
1790 self.makeSubtask(
"rgbGenerator")
1792 def _check_data_bands(self, data_bands):
1793 """Check the data for configured bands.
1795 Warn if any color bands are missing data.
1799 data_bands : `set` [`str`]
1800 Bands from the input data.
1804 bands : `list` [`str`]
1805 List of bands in bgr color order.
1807 if len(data_bands) == 0:
1808 raise RuntimeError(
"GenerateColorHipsTask must have data from at least one band.")
1810 match self.config.rgbStyle:
1812 if self.config.blue_channel_band
not in data_bands:
1814 "Color png blue_channel_band %s not in dataset.", self.config.blue_channel_band
1816 if self.config.green_channel_band
not in data_bands:
1818 "Color png green_channel_band %s not in dataset.", self.config.green_channel_band
1820 if self.config.red_channel_band
not in data_bands:
1822 "Color png red_channel_band %s not in dataset.", self.config.red_channel_band
1826 self.config.blue_channel_band,
1827 self.config.green_channel_band,
1828 self.config.red_channel_band,
1837 for band_name, config
in self.config.rgbGenerator.channelConfig.items():
1838 band_names.append(band_name)
1839 band_values.append((config.r, config.g, config.b))
1841 labs = colour.XYZ_to_Oklab(colour.RGB_to_XYZ(band_values, colourspace=
"CIE RGB"))
1842 hues = np.arctan2(labs[:, 2], labs[:, 1])
1844 hues[hues < 0] = 2 * np.pi + hues[hues < 0]
1846 order = np.argsort(hues)[::-1]
1847 config_bands = [band_names[index]
for index
in order]
1849 for band_name
in config_bands:
1850 if band_name
not in data_bands:
1852 f
"Data band {band_name} is specified in the RGB config but there is no input "
1853 "data for that band."
Pass parameters to a Statistics object.
An integer coordinate rectangle.
A RangeSet is a set of unsigned 64 bit integers.
std::shared_ptr< lsst::afw::image::Image< PixelT > > statisticsStack(std::vector< std::shared_ptr< lsst::afw::image::Image< PixelT > > > &images, Property flags, StatisticsControl const &sctrl=StatisticsControl(), std::vector< lsst::afw::image::VariancePixel > const &wvector=std::vector< lsst::afw::image::VariancePixel >(0))
A function to compute some statistics of a stack of Images.
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)