Coverage for python/lsst/analysis/tools/utils.py: 100%

43 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-16 09:31 +0000

1# This file is part of analysis_tools. 

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__all__ = ( 

23 "getTractCorners", 

24 "getPatchCorners", 

25 "http_client", 

26) 

27 

28from collections.abc import Generator 

29from contextlib import contextmanager 

30 

31import numpy as np 

32import requests 

33from requests.adapters import HTTPAdapter 

34from urllib3 import Retry 

35 

36from lsst.geom import Box2D 

37 

38 

39def getTractCorners(skymap, tractId): 

40 """Calculate the corners of a tract, given a skymap. 

41 

42 Parameters 

43 ---------- 

44 skymap : `lsst.skymap` 

45 tractId : `int` 

46 Identification number of the tract whose corner coordinates 

47 are returned. 

48 

49 Returns 

50 ------- 

51 corners : `list` of `tuples` of `float` 

52 

53 Notes 

54 ----- 

55 Corners are returned in degrees and wrapped in ra. 

56 """ 

57 # Get bounding box is needed if the skymap isn't rings 

58 # The discrete skymaps don't return a box from 

59 # getInnerSkyRegion 

60 tractBox = skymap[tractId].getInnerSkyRegion().getBoundingBox() 

61 minRa, maxRa = tractBox.getLon().getA().asDegrees(), tractBox.getLon().getB().asDegrees() 

62 minDec, maxDec = tractBox.getLat().getA().asDegrees(), tractBox.getLat().getB().asDegrees() 

63 

64 tractCorners = [(minRa, minDec), (maxRa, minDec), (maxRa, maxDec), (minRa, minDec)] 

65 corners = _wrapRa(tractCorners) 

66 

67 return corners 

68 

69 

70def getPatchCorners(tractInfo, patchId): 

71 """Calculate the corners of a patch, given tractInfo. 

72 

73 Parameters 

74 ---------- 

75 tractInfo : `lsst.skymap.tractInfo.ExplicitTractInfo` 

76 Tract info object of the tract containing the patch whose 

77 corner coordinates are returned. 

78 patchId : `int` 

79 Identification number of the patch whose corner coordinates 

80 are returned. 

81 

82 Returns 

83 ------- 

84 corners : `list` of `tuples` of `float` 

85 

86 Notes 

87 ----- 

88 Corners are returned in degrees and are wrapped in ra. 

89 """ 

90 patchInfo = tractInfo.getPatchInfo(patchId) 

91 patchCorners = Box2D(patchInfo.getInnerBBox()).getCorners() 

92 

93 tractWcs = tractInfo.getWcs() 

94 patchCorners = tractWcs.pixelToSky(patchCorners) 

95 corners = _wrapRa([(corner.getRa().asDegrees(), corner.getDec().asDegrees()) for corner in patchCorners]) 

96 

97 return corners 

98 

99 

100def _wrapRa(corners): 

101 """Wrap in right ascension if the corners span RA=0 

102 

103 Parameters 

104 ---------- 

105 corners : `list` of `tuples` of `float` 

106 Pairs of coordinates representing tract or patch corners. 

107 

108 Returns 

109 ------- 

110 corners : `list` of `tuples` of `float` 

111 Pairs of coordinates representing tract or patch corners, 

112 wrapped in RA. 

113 """ 

114 

115 minRa = np.min([corner[0] for corner in corners]) 

116 maxRa = np.max([corner[0] for corner in corners]) 

117 # If the tract needs wrapping in ra, wrap it 

118 if maxRa - minRa > 10: 

119 x = maxRa 

120 maxRa = 360 + minRa 

121 minRa = x 

122 minDec = np.min([corner[1] for corner in corners]) 

123 maxDec = np.max([corner[1] for corner in corners]) 

124 corners = [(minRa, minDec), (maxRa, minDec), (maxRa, maxDec), (minRa, maxDec)] 

125 

126 return corners 

127 

128 

129@contextmanager 

130def http_client() -> Generator[requests.Session]: 

131 """Creates a requests session with a custom transport to support 

132 automatic retries with backoff for dealing with transient server-side 

133 issues. 

134 

135 Notes 

136 ----- 

137 The goal of the adapter defined here is to avoid premature client abends 

138 when transient server or infrastructure issues prevent good-faith attempts 

139 at accessing APIs. To the extent that we want to balance "eventually 

140 successful" HTTP requests with the desire to vacate the compute resources 

141 our process is occupying, these retries should not overstay their welcome. 

142 

143 The "POST" HTTP verb is not usually part of the allowed methods for retries 

144 because unlike "PUT", "POST" is not considered idempotent by default. It is 

145 partially for this reason that a custom Retry adapter is needed, because 

146 by default "POST" requests would not be retried for status. 

147 

148 The backoff_factor is an exponential factor used to calculate how long to 

149 sleep between the third and subsequent tries, in seconds. The first retry 

150 is immediate and the total backoff won't exceed backoff_max, which defaults 

151 to 120 seconds. 

152 """ 

153 

154 retriable_statuses = [ 

155 requests.codes.too_many_requests, 

156 requests.codes.server_error, 

157 requests.codes.bad_gateway, 

158 requests.codes.service_unavailable, 

159 requests.codes.gateway_timeout, 

160 ] 

161 session = requests.Session() 

162 retry_strategy = Retry( 

163 total=None, # use specific conditional constraints 

164 connect=3, # network or tcp errors 

165 read=0, # request sent, response is bad 

166 status=5, # retries based on bad response status (see retriable_statuses) 

167 redirect=3, # default value, follow 3 redirects 

168 other=0, # edge cases and weird stuff 

169 backoff_factor=0.1, # sleep == {factor} * 2^(previous tries) 

170 status_forcelist=retriable_statuses, 

171 raise_on_status=True, 

172 allowed_methods={"GET", "HEAD", "POST", "PUT"}, 

173 ) 

174 session.mount("http://", HTTPAdapter(max_retries=retry_strategy)) 

175 session.mount("https://", HTTPAdapter(max_retries=retry_strategy)) 

176 try: 

177 yield session 

178 finally: 

179 session.close()