Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# 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 them to build an extended 

23PSF model. 

24""" 

25 

26from dataclasses import dataclass 

27from typing import List 

28 

29from lsst.afw import image as afwImage 

30from lsst.afw import fits as afwFits 

31from lsst.daf.base import PropertyList 

32 

33 

34@dataclass 

35class FocalPlaneRegionExtendedPsf: 

36 """Single extended PSF over a focal plane region. 

37 

38 The focal plane region is defined through a list 

39 of detectors. 

40 

41 Parameters 

42 ---------- 

43 extended_psf_image : `lsst.afw.image.MaskedImageF` 

44 Image of the extended PSF model. 

45 detector_list : `list` [`int`] 

46 List of detector IDs that define the focal plane region over which this 

47 extended PSF model has been built (and can be used). 

48 """ 

49 extended_psf_image: afwImage.MaskedImageF 

50 detector_list: List[int] 

51 

52 

53class ExtendedPsf: 

54 """Extended PSF model. 

55 

56 Each instance may contain a default extended PSF, a set of extended PSFs 

57 that correspond to different focal plane regions, or both. At this time, 

58 focal plane regions are always defined as a subset of detectors. 

59 

60 Parameters 

61 ---------- 

62 default_extended_psf : `lsst.afw.image.MaskedImageF` 

63 Extended PSF model to be used as default (or only) extended PSF model. 

64 """ 

65 def __init__(self, default_extended_psf=None): 

66 self.default_extended_psf = default_extended_psf 

67 self.focal_plane_regions = {} 

68 self.detectors_focal_plane_regions = {} 

69 

70 def add_regional_extended_psf(self, extended_psf_image, region_name, detector_list): 

71 """Add a new focal plane region, along wit hits extended PSF, to the 

72 ExtendedPsf instance. 

73 

74 Parameters 

75 ---------- 

76 extended_psf_image : `lsst.afw.image.MaskedImageF` 

77 Extended PSF model for the region. 

78 region_name : `str` 

79 Name of the focal plane region. Will be converted to all-uppercase. 

80 detector_list : `list` [`int`] 

81 List of IDs for the detectors that define the focal plane region. 

82 """ 

83 region_name = region_name.upper() 

84 if region_name in self.focal_plane_regions: 

85 raise ValueError(f"Region name {region_name} is already used by this ExtendedPsf instance.") 

86 self.focal_plane_regions[region_name] = FocalPlaneRegionExtendedPsf( 

87 extended_psf_image=extended_psf_image, detector_list=detector_list) 

88 for det in detector_list: 

89 self.detectors_focal_plane_regions[det] = region_name 

90 

91 def __call__(self, detector=None): 

92 """Return the appropriate extended PSF. 

93 

94 If the instance contains no extended PSF defined over focal plane 

95 regions, the default extended PSF will be returned regardless of 

96 whether a detector ID was passed as argument. 

97 

98 Parameters 

99 ---------- 

100 detector : `int`, optional 

101 Detector ID. If focal plane region PSFs are defined, is used to 

102 determine which model to return. 

103 

104 Returns 

105 ------- 

106 extendedPsfImage : `lsst.afw.image.MaskedImageF` 

107 The extended PSF model. If this instance contains extended PSFs 

108 defined over focal plane regions, the extended PSF model for the 

109 region that contains ``detector`` is returned. If not, the default 

110 extended PSF is returned. 

111 """ 

112 if detector is None: 

113 if self.default_extended_psf is None: 

114 raise ValueError("No default extended PSF available; please provide detector number.") 

115 return self.default_extended_psf 

116 elif not self.focal_plane_regions: 

117 return self.default_extended_psf 

118 return self.get_regional_extended_psf(detector=detector) 

119 

120 def __len__(self): 

121 """Returns the number of extended PSF models present in the instance. 

122 

123 Note that if the instance contains both a default model and a set of 

124 focal plane region models, the length of the instance will be the 

125 number of regional models, plus one (the default). This is true even 

126 in the case where the default model is one of the focal plane 

127 region-specific models. 

128 """ 

129 n_regions = len(self.focal_plane_regions) 

130 if self.default_extended_psf is not None: 

131 n_regions += 1 

132 return n_regions 

133 

134 def get_regional_extended_psf(self, region_name=None, detector=None): 

135 """Returns the extended PSF for a focal plane region. 

136 

137 The region can be identified either by name, or through a detector ID. 

138 

139 Parameters 

140 ---------- 

141 region_name : `str` or `None`, optional 

142 Name of the region for which the extended PSF should be retrieved. 

143 Ignored if ``detector`` is provided. Must be provided if 

144 ``detector`` is None. 

145 detector : `int` or `None`, optional 

146 If provided, returns the extended PSF for the focal plane region 

147 that includes this detector. 

148 

149 Raises 

150 ------ 

151 ValueError 

152 Raised if neither ``detector`` nor ``regionName`` is provided. 

153 """ 

154 if detector is None: 

155 if region_name is None: 

156 raise ValueError("One of either a regionName or a detector number must be provided.") 

157 return self.focal_plane_regions[region_name].extended_psf_image 

158 return self.focal_plane_regions[self.detectors_focal_plane_regions[detector]].extended_psf_image 

159 

160 def write_fits(self, filename): 

161 """Write this object to a file. 

162 

163 Parameters 

164 ---------- 

165 filename : `str` 

166 Name of file to write. 

167 """ 

168 # Create primary HDU with global metadata. 

169 metadata = PropertyList() 

170 metadata["HAS_DEFAULT"] = self.default_extended_psf is not None 

171 if self.focal_plane_regions: 

172 metadata["HAS_REGIONS"] = True 

173 metadata["REGION_NAMES"] = list(self.focal_plane_regions.keys()) 

174 for region, e_psf_region in self.focal_plane_regions.items(): 

175 metadata[region] = e_psf_region.detector_list 

176 else: 

177 metadata["HAS_REGIONS"] = False 

178 fits_primary = afwFits.Fits(filename, "w") 

179 fits_primary.createEmpty() 

180 fits_primary.writeMetadata(metadata) 

181 fits_primary.closeFile() 

182 # Write default extended PSF. 

183 if self.default_extended_psf is not None: 

184 default_hdu_metadata = PropertyList() 

185 default_hdu_metadata.update({"REGION": "DEFAULT", "EXTNAME": "IMAGE"}) 

186 self.default_extended_psf.image.writeFits(filename, metadata=default_hdu_metadata, mode="a") 

187 default_hdu_metadata.update({"REGION": "DEFAULT", "EXTNAME": "MASK"}) 

188 self.default_extended_psf.mask.writeFits(filename, metadata=default_hdu_metadata, mode="a") 

189 # Write extended PSF for each focal plane region. 

190 for j, (region, e_psf_region) in enumerate(self.focal_plane_regions.items()): 

191 metadata = PropertyList() 

192 metadata.update({"REGION": region, "EXTNAME": "IMAGE"}) 

193 e_psf_region.extended_psf_image.image.writeFits(filename, metadata=metadata, mode="a") 

194 metadata.update({"REGION": region, "EXTNAME": "MASK"}) 

195 e_psf_region.extended_psf_image.mask.writeFits(filename, metadata=metadata, mode="a") 

196 

197 def writeFits(self, filename): 

198 """Alias for ``write_fits``; exists for compatibility with the Butler. 

199 """ 

200 self.write_fits(filename) 

201 

202 @classmethod 

203 def read_fits(cls, filename): 

204 """Build an instance of this class from a file. 

205 

206 Parameters 

207 ---------- 

208 filename : `str` 

209 Name of the file to read. 

210 """ 

211 # Extract info from metadata. 

212 global_metadata = afwFits.readMetadata(filename, hdu=0) 

213 has_default = global_metadata.getBool("HAS_DEFAULT") 

214 if global_metadata.getBool("HAS_REGIONS"): 

215 focal_plane_region_names = global_metadata.getArray("REGION_NAMES") 

216 else: 

217 focal_plane_region_names = [] 

218 f = afwFits.Fits(filename, "r") 

219 n_extensions = f.countHdus() 

220 extended_psf_parts = {} 

221 for j in range(1, n_extensions): 

222 md = afwFits.readMetadata(filename, hdu=j) 

223 if has_default and md["REGION"] == "DEFAULT": 

224 if md["EXTNAME"] == "IMAGE": 

225 default_image = afwImage.ImageF(filename, hdu=j) 

226 elif md["EXTNAME"] == "MASK": 

227 default_mask = afwImage.MaskX(filename, hdu=j) 

228 continue 

229 if md["EXTNAME"] == "IMAGE": 

230 extended_psf_part = afwImage.ImageF(filename, hdu=j) 

231 elif md["EXTNAME"] == "MASK": 

232 extended_psf_part = afwImage.MaskX(filename, hdu=j) 

233 extended_psf_parts.setdefault(md["REGION"], {})[md["EXTNAME"].lower()] = extended_psf_part 

234 # Handle default if present. 

235 if has_default: 

236 extended_psf = cls(afwImage.MaskedImageF(default_image, default_mask)) 

237 else: 

238 extended_psf = cls() 

239 # Ensure we recovered an extended PSF for all focal plane regions. 

240 if len(extended_psf_parts) != len(focal_plane_region_names): 

241 raise ValueError(f"Number of per-region extended PSFs read ({len(extended_psf_parts)}) does not " 

242 "match with the number of regions recorded in the metadata " 

243 f"({len(focal_plane_region_names)}).") 

244 # Generate extended PSF regions mappings. 

245 for r_name in focal_plane_region_names: 

246 extended_psf_image = afwImage.MaskedImageF(**extended_psf_parts[r_name]) 

247 detector_list = global_metadata.getArray(r_name) 

248 extended_psf.add_regional_extended_psf(extended_psf_image, r_name, detector_list) 

249 # Instantiate ExtendedPsf. 

250 return extended_psf 

251 

252 @classmethod 

253 def readFits(cls, filename): 

254 """Alias for ``readFits``; exists for compatibility with the Butler. 

255 """ 

256 return cls.read_fits(filename)