LSST Applications g180d380827+0f66a164bb,g2079a07aa2+86d27d4dc4,g2305ad1205+7d304bc7a0,g29320951ab+500695df56,g2bbee38e9b+0e5473021a,g337abbeb29+0e5473021a,g33d1c0ed96+0e5473021a,g3a166c0a6a+0e5473021a,g3ddfee87b4+e42ea45bea,g48712c4677+36a86eeaa5,g487adcacf7+2dd8f347ac,g50ff169b8f+96c6868917,g52b1c1532d+585e252eca,g591dd9f2cf+c70619cc9d,g5a732f18d5+53520f316c,g5ea96fc03c+341ea1ce94,g64a986408d+f7cd9c7162,g858d7b2824+f7cd9c7162,g8a8a8dda67+585e252eca,g99cad8db69+469ab8c039,g9ddcbc5298+9a081db1e4,ga1e77700b3+15fc3df1f7,gb0e22166c9+60f28cb32d,gba4ed39666+c2a2e4ac27,gbb8dafda3b+c92fc63c7e,gbd866b1f37+f7cd9c7162,gc120e1dc64+02c66aa596,gc28159a63d+0e5473021a,gc3e9b769f7+b0068a2d9f,gcf0d15dbbd+e42ea45bea,gdaeeff99f8+f9a426f77a,ge6526c86ff+84383d05b3,ge79ae78c31+0e5473021a,gee10cc3b42+585e252eca,gff1a9f87cc+f7cd9c7162,w.2024.17
LSST Data Management Base Package
Loading...
Searching...
No Matches
extended_psf.py
Go to the documentation of this file.
1# This file is part of pipe_tasks.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22"""Read preprocessed bright stars and stack to build an extended PSF model."""
23
24__all__ = [
25 "FocalPlaneRegionExtendedPsf",
26 "ExtendedPsf",
27 "StackBrightStarsConfig",
28 "StackBrightStarsTask",
29 "MeasureExtendedPsfConfig",
30 "MeasureExtendedPsfTask",
31 "DetectorsInRegion",
32]
33
34from dataclasses import dataclass
35
36from lsst.afw.fits import Fits, readMetadata
37from lsst.afw.image import ImageF, MaskedImageF, MaskX
38from lsst.afw.math import StatisticsControl, statisticsStack, stringToStatisticsProperty
39from lsst.daf.base import PropertyList
40from lsst.geom import Extent2I
41from lsst.pex.config import ChoiceField, Config, ConfigDictField, ConfigurableField, Field, ListField
42from lsst.pipe.base import PipelineTaskConfig, PipelineTaskConnections, Struct, Task
43from lsst.pipe.base.connectionTypes import Input, Output
44from lsst.pipe.tasks.coaddBase import subBBoxIter
45
46
47def find_region_for_detector(detector_id, detectors_focal_plane_regions):
48 """Find the focal plane region that contains a given detector.
49
50 Parameters
51 ----------
52 detector_id : `int`
53 The detector ID.
54
55 detectors_focal_plane_regions :
56 `dict` [`str`, `lsst.pipe.tasks.extended_psf.DetectorsInRegion`]
57 A dictionary containing focal plane region names as keys, and the
58 corresponding detector IDs encoded within the values.
59
60 Returns
61 -------
62 key: `str`
63 The name of the region to which the given detector belongs.
64
65 Raises
66 ------
67 KeyError
68 Raised if the given detector is not included in any focal plane region.
69 """
70 for region_id, detectors_in_region in detectors_focal_plane_regions.items():
71 if detector_id in detectors_in_region.detectors:
72 return region_id
73 raise KeyError(
74 "Detector %d is not included in any focal plane region.",
75 detector_id,
76 )
77
78
80 """Provides a list of detectors that define a region."""
81
82 detectors = ListField[int](
83 doc="A list containing the detectors IDs.",
84 default=[],
85 )
86
87
88@dataclass
90 """Single extended PSF over a focal plane region.
91
92 The focal plane region is defined through a list of detectors.
93
94 Parameters
95 ----------
96 extended_psf_image : `lsst.afw.image.MaskedImageF`
97 Image of the extended PSF model.
98 region_detectors : `lsst.pipe.tasks.extended_psf.DetectorsInRegion`
99 List of detector IDs that define the focal plane region over which this
100 extended PSF model has been built (and can be used).
101 """
102
103 extended_psf_image: MaskedImageF
104 region_detectors: DetectorsInRegion
105
106
108 """Extended PSF model.
109
110 Each instance may contain a default extended PSF, a set of extended PSFs
111 that correspond to different focal plane regions, or both. At this time,
112 focal plane regions are always defined as a subset of detectors.
113
114 Parameters
115 ----------
116 default_extended_psf : `lsst.afw.image.MaskedImageF`
117 Extended PSF model to be used as default (or only) extended PSF model.
118 """
119
120 def __init__(self, default_extended_psf=None):
121 self.default_extended_psf = default_extended_psf
124
125 def add_regional_extended_psf(self, extended_psf_image, region_name, region_detectors):
126 """Add a new focal plane region, along with its extended PSF, to the
127 ExtendedPsf instance.
128
129 Parameters
130 ----------
131 extended_psf_image : `lsst.afw.image.MaskedImageF`
132 Extended PSF model for the region.
133 region_name : `str`
134 Name of the focal plane region. Will be converted to all-uppercase.
135 region_detectors : `lsst.pipe.tasks.extended_psf.DetectorsInRegion`
136 List of detector IDs for the detectors that define a region on the
137 focal plane.
138 """
139 region_name = region_name.upper()
140 if region_name in self.focal_plane_regions:
141 raise ValueError(f"Region name {region_name} is already used by this ExtendedPsf instance.")
143 extended_psf_image=extended_psf_image, region_detectors=region_detectors
144 )
145 self.detectors_focal_plane_regions[region_name] = region_detectors
146
147 def __call__(self, detector=None):
148 """Return the appropriate extended PSF.
149
150 If the instance contains no extended PSF defined over focal plane
151 regions, the default extended PSF will be returned regardless of
152 whether a detector ID was passed as argument.
153
154 Parameters
155 ----------
156 detector : `int`, optional
157 Detector ID. If focal plane region PSFs are defined, is used to
158 determine which model to return.
159
160 Returns
161 -------
162 extendedPsfImage : `lsst.afw.image.MaskedImageF`
163 The extended PSF model. If this instance contains extended PSFs
164 defined over focal plane regions, the extended PSF model for the
165 region that contains ``detector`` is returned. If not, the default
166 extended PSF is returned.
167 """
168 if detector is None:
169 if self.default_extended_psf is None:
170 raise ValueError("No default extended PSF available; please provide detector number.")
171 return self.default_extended_psf
172 elif not self.focal_plane_regions:
173 return self.default_extended_psf
174 return self.get_extended_psf(region_name=detector)
175
176 def __len__(self):
177 """Returns the number of extended PSF models present in the instance.
178
179 Note that if the instance contains both a default model and a set of
180 focal plane region models, the length of the instance will be the
181 number of regional models, plus one (the default). This is true even
182 in the case where the default model is one of the focal plane
183 region-specific models.
184 """
185 n_regions = len(self.focal_plane_regions)
186 if self.default_extended_psf is not None:
187 n_regions += 1
188 return n_regions
189
190 def get_extended_psf(self, region_name):
191 """Returns the extended PSF for a focal plane region or detector.
192
193 This method takes either a region name or a detector ID as input. If
194 the input is a `str` type, it is assumed to be the region name and if
195 the input is a `int` type it is assumed to be the detector ID.
196
197 Parameters
198 ----------
199 region_name : `str` or `int`
200 Name of the region (str) or detector (int) for which the extended
201 PSF should be retrieved.
202
203 Returns
204 -------
205 extended_psf_image: `lsst.afw.image.MaskedImageF`
206 The extended PSF model for the requested region or detector.
207
208 Raises
209 ------
210 ValueError
211 Raised if the input is not in the correct type.
212 """
213 if isinstance(region_name, str):
214 return self.focal_plane_regions[region_name].extended_psf_image
215 elif isinstance(region_name, int):
216 region_name = find_region_for_detector(region_name, self.detectors_focal_plane_regions)
217 return self.focal_plane_regions[region_name].extended_psf_image
218 else:
219 raise ValueError("A region name with `str` type or detector number with `int` must be provided")
220
221 def write_fits(self, filename):
222 """Write this object to a file.
223
224 Parameters
225 ----------
226 filename : `str`
227 Name of file to write.
228 """
229 # Create primary HDU with global metadata.
230 metadata = PropertyList()
231 metadata["HAS_DEFAULT"] = self.default_extended_psf is not None
232 if self.focal_plane_regions:
233 metadata["HAS_REGIONS"] = True
234 metadata["REGION_NAMES"] = list(self.focal_plane_regions.keys())
235 for region, e_psf_region in self.focal_plane_regions.items():
236 metadata[region] = e_psf_region.region_detectors.detectors
237 else:
238 metadata["HAS_REGIONS"] = False
239 fits_primary = Fits(filename, "w")
240 fits_primary.createEmpty()
241 fits_primary.writeMetadata(metadata)
242 fits_primary.closeFile()
243 # Write default extended PSF.
244 if self.default_extended_psf is not None:
245 default_hdu_metadata = PropertyList()
246 default_hdu_metadata.update({"REGION": "DEFAULT", "EXTNAME": "IMAGE"})
247 self.default_extended_psf.image.writeFits(filename, metadata=default_hdu_metadata, mode="a")
248 default_hdu_metadata.update({"REGION": "DEFAULT", "EXTNAME": "MASK"})
249 self.default_extended_psf.mask.writeFits(filename, metadata=default_hdu_metadata, mode="a")
250 # Write extended PSF for each focal plane region.
251 for j, (region, e_psf_region) in enumerate(self.focal_plane_regions.items()):
252 metadata = PropertyList()
253 metadata.update({"REGION": region, "EXTNAME": "IMAGE"})
254 e_psf_region.extended_psf_image.image.writeFits(filename, metadata=metadata, mode="a")
255 metadata.update({"REGION": region, "EXTNAME": "MASK"})
256 e_psf_region.extended_psf_image.mask.writeFits(filename, metadata=metadata, mode="a")
257
258 def writeFits(self, filename):
259 """Alias for ``write_fits``; for compatibility with the Butler."""
260 self.write_fits(filename)
261
262 @classmethod
263 def read_fits(cls, filename):
264 """Build an instance of this class from a file.
265
266 Parameters
267 ----------
268 filename : `str`
269 Name of the file to read.
270 """
271 # Extract info from metadata.
272 global_metadata = readMetadata(filename, hdu=0)
273 has_default = global_metadata.getBool("HAS_DEFAULT")
274 if global_metadata.getBool("HAS_REGIONS"):
275 focal_plane_region_names = global_metadata.getArray("REGION_NAMES")
276 else:
277 focal_plane_region_names = []
278 f = Fits(filename, "r")
279 n_extensions = f.countHdus()
280 extended_psf_parts = {}
281 for j in range(1, n_extensions):
282 md = readMetadata(filename, hdu=j)
283 if has_default and md["REGION"] == "DEFAULT":
284 if md["EXTNAME"] == "IMAGE":
285 default_image = ImageF(filename, hdu=j)
286 elif md["EXTNAME"] == "MASK":
287 default_mask = MaskX(filename, hdu=j)
288 continue
289 if md["EXTNAME"] == "IMAGE":
290 extended_psf_part = ImageF(filename, hdu=j)
291 elif md["EXTNAME"] == "MASK":
292 extended_psf_part = MaskX(filename, hdu=j)
293 extended_psf_parts.setdefault(md["REGION"], {})[md["EXTNAME"].lower()] = extended_psf_part
294 # Handle default if present.
295 if has_default:
296 extended_psf = cls(MaskedImageF(default_image, default_mask))
297 else:
298 extended_psf = cls()
299 # Ensure we recovered an extended PSF for all focal plane regions.
300 if len(extended_psf_parts) != len(focal_plane_region_names):
301 raise ValueError(
302 f"Number of per-region extended PSFs read ({len(extended_psf_parts)}) does not "
303 "match with the number of regions recorded in the metadata "
304 f"({len(focal_plane_region_names)})."
305 )
306 # Generate extended PSF regions mappings.
307 for r_name in focal_plane_region_names:
308 extended_psf_image = MaskedImageF(**extended_psf_parts[r_name])
309 region_detectors = DetectorsInRegion()
310 region_detectors.detectors = global_metadata.getArray(r_name)
311 extended_psf.add_regional_extended_psf(extended_psf_image, r_name, region_detectors)
312 # Instantiate ExtendedPsf.
313 return extended_psf
314
315 @classmethod
316 def readFits(cls, filename):
317 """Alias for ``readFits``; exists for compatibility with the Butler."""
318 return cls.read_fits(filename)
319
320
322 """Configuration parameters for StackBrightStarsTask."""
323
324 subregion_size = ListField[int](
325 doc="Size, in pixels, of the subregions over which the stacking will be " "iteratively performed.",
326 default=(100, 100),
327 )
328 stacking_statistic = ChoiceField[str](
329 doc="Type of statistic to use for stacking.",
330 default="MEANCLIP",
331 allowed={
332 "MEAN": "mean",
333 "MEDIAN": "median",
334 "MEANCLIP": "clipped mean",
335 },
336 )
337 num_sigma_clip = Field[float](
338 doc="Sigma for outlier rejection; ignored if stacking_statistic != 'MEANCLIP'.",
339 default=4,
340 )
341 num_iter = Field[int](
342 doc="Number of iterations of outlier rejection; ignored if stackingStatistic != 'MEANCLIP'.",
343 default=3,
344 )
345 bad_mask_planes = ListField[str](
346 doc="Mask planes that define pixels to be excluded from the stacking of the bright star stamps.",
347 default=("BAD", "CR", "CROSSTALK", "EDGE", "NO_DATA", "SAT", "SUSPECT", "UNMASKEDNAN"),
348 )
349 do_mag_cut = Field[bool](
350 doc="Apply magnitude cut before stacking?",
351 default=False,
352 )
353 mag_limit = Field[float](
354 doc="Magnitude limit, in Gaia G; all stars brighter than this value will be stacked",
355 default=18,
356 )
357 minValidAnnulusFraction = Field[float](
358 doc="Minimum number of valid pixels that must fall within the annulus for the bright star to be "
359 "included in the generation of a PSF.",
360 default=0.0,
361 )
362
363
365 """Stack bright stars together to build an extended PSF model."""
366
367 ConfigClass = StackBrightStarsConfig
368 _DefaultName = "stack_bright_stars"
369
370 def _set_up_stacking(self, example_stamp):
371 """Configure stacking statistic and control from config fields."""
372 stats_control = StatisticsControl(
373 numSigmaClip=self.config.num_sigma_clip,
374 numIter=self.config.num_iter,
375 )
376 if bad_masks := self.config.bad_mask_planes:
377 and_mask = example_stamp.mask.getPlaneBitMask(bad_masks[0])
378 for bm in bad_masks[1:]:
379 and_mask = and_mask | example_stamp.mask.getPlaneBitMask(bm)
380 stats_control.setAndMask(and_mask)
381 stats_flags = stringToStatisticsProperty(self.config.stacking_statistic)
382 return stats_control, stats_flags
383
384 def removeInvalidStamps(self, read_stars):
385 """Remove stamps that do not have enough valid pixels in the annulus.
386
387 Parameters
388 ----------
389 read_stars : `list` of `lsst.pipe.tasks.processBrightStars.BrightStarStamp`
390 List of bright star stamps to be stacked.
391 """
392 # Find stamps that do not have enough valid pixels in the annulus
393 invalidStamps = []
394 for stamp in read_stars:
395 if stamp.validAnnulusFraction < self.config.minValidAnnulusFraction:
396 invalidStamps.append(stamp)
397 # Remove stamps that do not have enough valid pixels in the annulus
398 if len(invalidStamps):
399 for invalidStamp in invalidStamps:
400 read_stars._stamps.remove(invalidStamp)
401
402 def run(self, bss_ref_list, stars_dict, region_name=None):
403 """Read input bright star stamps and stack them together.
404
405 The stacking is done iteratively over smaller areas of the final model
406 image to allow for a great number of bright star stamps to be used.
407
408 Parameters
409 ----------
410 bss_ref_list : `list` of
411 `lsst.daf.butler._deferredDatasetHandle.DeferredDatasetHandle`
412 List of available bright star stamps data references.
413 stars_dict: `dict`
414 Dictionary to store the number of stars used to generate the PSF.
415 region_name : `str`, optional
416 Name of the focal plane region, if applicable. Only used for
417 logging purposes, when running over multiple such regions
418 (typically from `MeasureExtendedPsfTask`)
419 """
420 if region_name:
421 region_message = f" for region '{region_name}'."
422 else:
423 region_message = "."
424 if region_name is not None:
425 stars_dict_key = region_name
426 else:
427 stars_dict_key = "all"
428 self.log.info(
429 "Building extended PSF from stamps extracted from %d detector images%s",
430 len(bss_ref_list),
431 region_message,
432 )
433 # read in example set of full stamps
434 example_bss = bss_ref_list[0].get()
435 example_stamp = example_bss[0].stamp_im
436 # create model image
437 ext_psf = MaskedImageF(example_stamp.getBBox())
438 # divide model image into smaller subregions
439 subregion_size = Extent2I(*self.config.subregion_size)
440 sub_bboxes = subBBoxIter(ext_psf.getBBox(), subregion_size)
441 # compute approximate number of subregions
442 n_subregions = ((ext_psf.getDimensions()[0]) // (subregion_size[0] + 1)) * (
443 (ext_psf.getDimensions()[1]) // (subregion_size[1] + 1)
444 )
445 self.log.info(
446 "Stacking performed iteratively over approximately %d smaller areas of the final model image.",
447 n_subregions,
448 )
449 # set up stacking statistic
450 stats_control, stats_flags = self._set_up_stacking(example_stamp)
451 # perform stacking
452 for jbbox, bbox in enumerate(sub_bboxes):
453 all_stars = None
454 for bss_ref in bss_ref_list:
455 read_stars = bss_ref.get(parameters={"bbox": bbox})
456 self.removeInvalidStamps(read_stars)
457 if jbbox == 0:
458 # Store the number of stars used to generate the PSF model
459 # in the metadata dictionary.
460 stars_dict[stars_dict_key] += len(read_stars)
461 if self.config.do_mag_cut:
462 read_stars = read_stars.selectByMag(magMax=self.config.mag_limit)
463 if all_stars:
464 all_stars.extend(read_stars)
465 else:
466 all_stars = read_stars
467 # TODO: DM-27371 add weights to bright stars for stacking
468 coadd_sub_bbox = statisticsStack(all_stars.getMaskedImages(), stats_flags, stats_control)
469 ext_psf.assign(coadd_sub_bbox, bbox)
470 return ext_psf
471
472
473class MeasureExtendedPsfConnections(PipelineTaskConnections, dimensions=("band", "instrument")):
474 input_brightStarStamps = Input(
475 doc="Input list of bright star collections to be stacked.",
476 name="brightStarStamps",
477 storageClass="BrightStarStamps",
478 dimensions=("visit", "detector"),
479 deferLoad=True,
480 multiple=True,
481 )
482 extended_psf = Output(
483 doc="Extended PSF model built by stacking bright stars.",
484 name="extended_psf",
485 storageClass="ExtendedPsf",
486 dimensions=("band",),
487 )
488
489
490class MeasureExtendedPsfConfig(PipelineTaskConfig, pipelineConnections=MeasureExtendedPsfConnections):
491 """Configuration parameters for MeasureExtendedPsfTask."""
492
493 stack_bright_stars = ConfigurableField(
494 target=StackBrightStarsTask,
495 doc="Stack selected bright stars",
496 )
497 detectors_focal_plane_regions = ConfigDictField(
498 keytype=str,
499 itemtype=DetectorsInRegion,
500 doc=(
501 "Mapping from focal plane region names to detector IDs. "
502 "If empty, a constant extended PSF model is built from all selected bright stars. "
503 "It's possible for a single detector to be included in multiple regions if so desired."
504 ),
505 default={},
506 )
507
508
510 """Build and save extended PSF model.
511
512 The model is built by stacking bright star stamps, extracted and
513 preprocessed by
514 `lsst.pipe.tasks.processBrightStars.ProcessBrightStarsTask`.
515
516 If a mapping from detector IDs to focal plane regions is provided, a
517 different extended PSF model will be built for each focal plane region. If
518 not, a single constant extended PSF model is built with all available data.
519 """
520
521 ConfigClass = MeasureExtendedPsfConfig
522 _DefaultName = "measureExtendedPsf"
523
524 def __init__(self, initInputs=None, *args, **kwargs):
525 super().__init__(*args, **kwargs)
526 self.makeSubtask("stack_bright_stars")
527 self.detectors_focal_plane_regions = self.config.detectors_focal_plane_regions
529
530 def select_detector_refs(self, ref_list):
531 """Split available sets of bright star stamps according to focal plane
532 regions.
533
534 Parameters
535 ----------
536 ref_list : `list` of
537 `lsst.daf.butler._deferredDatasetHandle.DeferredDatasetHandle`
538 List of available bright star stamps data references.
539 """
540 region_ref_list = {region: [] for region in self.detectors_focal_plane_regions.keys()}
541 for dataset_handle in ref_list:
542 detector_id = dataset_handle.ref.dataId["detector"]
543 if detector_id in self.regionless_dets:
544 continue
545 try:
546 region_name = find_region_for_detector(detector_id, self.detectors_focal_plane_regions)
547 except KeyError:
548 self.log.warning(
549 "Bright stars were available for detector %d, but it was missing from the %s config "
550 "field, so they will not be used to build any of the extended PSF models.",
551 detector_id,
552 "'detectors_focal_plane_regions'",
553 )
554 self.regionless_dets.append(detector_id)
555 continue
556 region_ref_list[region_name].append(dataset_handle)
557 return region_ref_list
558
559 def runQuantum(self, butlerQC, inputRefs, outputRefs):
560 input_data = butlerQC.get(inputRefs)
561 bss_ref_list = input_data["input_brightStarStamps"]
562 # Creating the metadata dictionary to store the number of stars used to
563 # generate the PSF model(s).
564 self.metadata["psfStarCount"] = {}
565 if not self.config.detectors_focal_plane_regions:
566 self.log.info(
567 "No detector groups were provided to MeasureExtendedPsfTask; computing a single, "
568 "constant extended PSF model over all available observations."
569 )
570 # Creating the sub-dictionary to store the number of stars when one
571 # PSF modle is generated for focal plane.
572 self.metadata["psfStarCount"]["all"] = 0
573 output_e_psf = ExtendedPsf(
574 self.stack_bright_stars.run(bss_ref_list, self.metadata["psfStarCount"])
575 )
576 else:
577 output_e_psf = ExtendedPsf()
578 region_ref_list = self.select_detector_refs(bss_ref_list)
579 for region_name, ref_list in region_ref_list.items():
580 if not ref_list:
581 # no valid references found
582 self.log.warning(
583 "No valid brightStarStamps reference found for region '%s'; skipping it.",
584 region_name,
585 )
586 continue
587 # Creating the sub-dictionary to store the number of stars
588 # that are used to generate the PSF model for the current
589 # region.
590 self.metadata["psfStarCount"][region_name] = 0
591 ext_psf = self.stack_bright_stars.run(ref_list, self.metadata["psfStarCount"], region_name)
592 output_e_psf.add_regional_extended_psf(
593 ext_psf, region_name, self.detectors_focal_plane_regions[region_name]
594 )
595 output = Struct(extended_psf=output_e_psf)
596 butlerQC.put(output, outputRefs)
std::vector< SchemaItem< Flag > > * items
A simple struct that combines the two arguments that must be passed to most cfitsio routines and cont...
Definition fits.h:308
Pass parameters to a Statistics object.
Definition Statistics.h:83
Class for storing ordered metadata with comments.
__init__(self, default_extended_psf=None)
add_regional_extended_psf(self, extended_psf_image, region_name, region_detectors)
__init__(self, initInputs=None, *args, **kwargs)
runQuantum(self, butlerQC, inputRefs, outputRefs)
run(self, bss_ref_list, stars_dict, region_name=None)
find_region_for_detector(detector_id, detectors_focal_plane_regions)