Coverage for python/lsst/summit/extras/monitoring.py: 16%

95 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-11 05:39 -0700

1# This file is part of summit_extras. 

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 

22from time import sleep 

23from typing import Any 

24 

25import numpy as np 

26 

27import lsst.afw.cameraGeom.utils as cgUtils 

28import lsst.afw.display as afwDisplay 

29import lsst.afw.image as afwImage 

30import lsst.geom as geom 

31from lsst.pex.exceptions import NotFoundError 

32from lsst.summit.utils.bestEffort import BestEffortIsr 

33from lsst.summit.utils.butlerUtils import ( 

34 getExpIdFromDayObsSeqNum, 

35 getExpRecordFromDataId, 

36 getMostRecentDataId, 

37 makeDefaultLatissButler, 

38) 

39 

40# TODO: maybe add option to create display and return URL? 

41 

42 

43class Monitor: 

44 """Create a monitor for AuxTel. 

45 

46 Scans the butler repo for new images and sends each one, after running 

47 bestEffortIsr, to the display. 

48 

49 Now largely superceded by RubinTV. 

50 

51 Parameters 

52 ------- 

53 fireflyDisplay : `lsst.afw.display.Display` 

54 A Firefly display instance. 

55 """ 

56 

57 cadence = 1 # in seconds 

58 runIsr = True 

59 

60 def __init__(self, fireflyDisplay: afwDisplay, **kwargs: Any): 

61 """""" 

62 self.butler = makeDefaultLatissButler() 

63 self.display = fireflyDisplay 

64 self.bestEffort = BestEffortIsr(**kwargs) 

65 self.writeQuickLookImages = None 

66 self.overlayAmps = False # do the overlay? 

67 self.measureFromChipCenter = False 

68 

69 def _getLatestImageDataIdAndExpId(self) -> tuple: 

70 """Get the dataId and expId for the most recent image in the repo.""" 

71 dataId = getMostRecentDataId(self.butler) 

72 expId = getExpIdFromDayObsSeqNum(self.butler, dataId)["exposure"] 

73 return dataId, expId 

74 

75 def _calcImageStats(self, exp: afwImage.Exposure) -> list[str]: 

76 elements = [] 

77 median = np.median(exp.image.array) 

78 elements.append(f"Median={median:.2f}") 

79 mean = np.mean(exp.image.array) 

80 # elements.append(f"Median={median:.2f}") 

81 elements.append(f"Mean={mean:.2f}") 

82 

83 return elements 

84 

85 def _makeImageInfoText(self, dataId: dict, exp: afwImage.Exposure, asList: bool = False) -> list | str: 

86 # TODO: add the following to the display: 

87 # az, el, zenith angle 

88 # main source centroid 

89 # PSF 

90 # num saturated pixels (or maybe just an isSaturated bool) 

91 # main star max ADU (post isr) 

92 

93 elements = [] 

94 

95 expRecord = getExpRecordFromDataId(self.butler, dataId) 

96 imageType = expRecord.observation_type 

97 obj = None 

98 if imageType.upper() not in ["BIAS", "DARK", "FLAT"]: 

99 try: 

100 obj = expRecord.target_name 

101 obj = obj.replace(" ", "") 

102 except Exception: 

103 pass 

104 

105 for k, v in dataId.items(): # dataId done per line for vertical display 

106 elements.append(f"{k}:{v}") 

107 

108 if obj: 

109 elements.append(f"{obj}") 

110 else: 

111 elements.append(f"{imageType}") 

112 

113 expTime = exp.getInfo().getVisitInfo().getExposureTime() 

114 filt = exp.filter.physicalLabel 

115 

116 elements.append(f"{expTime}s exp") 

117 elements.append(f"{filt}") 

118 

119 elements.extend(self._calcImageStats(exp)) 

120 

121 if asList: 

122 return elements 

123 return " ".join([e for e in elements]) 

124 

125 def _printImageInfo(self, elements: list) -> None: 

126 size = 3 

127 top = 3850 # just under title for size=3 

128 xnom = -600 # 0 is the left edge of the image 

129 vSpacing = 100 # about right for size=3, can make f(size) if needed 

130 

131 # TODO: add a with buffering and a .flush() 

132 # Also maybe a sleep as it seems buggy 

133 for i, item in enumerate(elements): 

134 y = top - (i * vSpacing) 

135 x = xnom + (size * 18.5 * len(item) // 2) 

136 self.display.dot(str(item), x, y, size, ctype="red", fontFamily="courier") 

137 

138 def run(self, durationInSeconds: int = -1) -> None: 

139 """Run the monitor, displaying new images as they are taken. 

140 

141 Parameters 

142 ---------- 

143 durationInSeconds : `int`, optional 

144 How long to run for. Use -1 for infinite. 

145 """ 

146 

147 if durationInSeconds == -1: 

148 nLoops = int(1e9) 

149 else: 

150 nLoops = int(durationInSeconds // self.cadence) 

151 

152 lastDisplayed = -1 

153 for i in range(nLoops): 

154 try: 

155 dataId, expId = self._getLatestImageDataIdAndExpId() 

156 

157 if lastDisplayed == expId: 

158 sleep(self.cadence) 

159 continue 

160 

161 if self.runIsr: 

162 exp = self.bestEffort.getExposure(dataId) 

163 else: 

164 exp = self.butler.get("raw", dataId=dataId) 

165 

166 # TODO: add logic to deal with amp overlay and chip center 

167 # being mutually exclusive 

168 if self.measureFromChipCenter: # after writing only! 

169 exp.setXY0(geom.PointI(-2036, -2000)) 

170 

171 print(f"Displaying {dataId}...") 

172 imageInfoText = self._makeImageInfoText(dataId, exp, asList=True) 

173 # too long of a title breaks Java FITS i/o 

174 fireflyTitle = " ".join([s for s in imageInfoText])[:67] 

175 try: 

176 self.display.scale("asinh", "zscale") 

177 self.display.mtv(exp, title=fireflyTitle) 

178 except Exception as e: # includes JSONDecodeError, HTTPError, anything else 

179 print(f"Caught error {e}, skipping this image") # TODO: try again maybe? 

180 

181 if self.overlayAmps: 

182 cgUtils.overlayCcdBoxes(exp.getDetector(), display=self.display, isTrimmed=True) 

183 

184 self._printImageInfo(imageInfoText) 

185 lastDisplayed = expId 

186 

187 except NotFoundError as e: # NotFoundError when filters aren't defined 

188 print(f"Skipped displaying {dataId} due to {e}") 

189 return