Coverage for python/lsst/summit/extras/focusAnalysis.py: 15%

284 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-15 04:10 -0800

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 

22import lsst.geom as geom 

23 

24import matplotlib.pyplot as plt 

25from matplotlib.colors import LogNorm 

26from matplotlib.patches import Arrow, Rectangle, Circle 

27import matplotlib.cm as cm 

28from matplotlib import gridspec 

29 

30from dataclasses import dataclass 

31 

32import numpy as np 

33from scipy.optimize import curve_fit 

34from scipy.linalg import norm 

35 

36# TODO: change these back to local .imports 

37from lsst.summit.utils.bestEffort import BestEffortIsr 

38from lsst.summit.utils import ImageExaminer 

39from lsst.summit.utils.utils import FWHMTOSIGMA, SIGMATOFWHM 

40from lsst.atmospec.utils import isDispersedExp 

41 

42from lsst.pipe.tasks.quickFrameMeasurement import QuickFrameMeasurementTask, QuickFrameMeasurementTaskConfig 

43from lsst.summit.utils.butlerUtils import (makeDefaultLatissButler, getExpRecordFromDataId) 

44 

45__all__ = ["SpectralFocusAnalyzer", "NonSpectralFocusAnalyzer"] 

46 

47 

48@dataclass 

49class FitResult: 

50 amp: float 

51 mean: float 

52 sigma: float 

53 

54 

55class SpectralFocusAnalyzer(): 

56 """Analyze a focus sweep taken for spectral data. 

57 

58 Take slices across the spectrum for each image, fitting a Gaussian to each 

59 slice, and perform a parabolic fit to these widths. The number of slices 

60 and their distances can be customized by calling setSpectrumBoxOffsets(). 

61 

62 Nominal usage is something like: 

63 

64 %matplotlib inline 

65 dayObs = 20210101 

66 seqNums = [100, 101, 102, 103, 104] 

67 focusAnalyzer = SpectralFocusAnalyzer() 

68 focusAnalyzer.setSpectrumBoxOffsets([500, 750, 1000, 1250]) 

69 focusAnalyzer.getFocusData(dayObs, seqNums, doDisplay=True) 

70 focusAnalyzer.fitDataAndPlot() 

71 

72 focusAnalyzer.run() can be used instead of the last two lines separately. 

73 """ 

74 

75 def __init__(self, **kwargs): 

76 self.butler = makeDefaultLatissButler() 

77 self._bestEffort = BestEffortIsr(**kwargs) 

78 qfmTaskConfig = QuickFrameMeasurementTaskConfig() 

79 self._quickMeasure = QuickFrameMeasurementTask(config=qfmTaskConfig) 

80 

81 self.spectrumHalfWidth = 100 

82 self.spectrumBoxLength = 20 

83 self._spectrumBoxOffsets = [882, 1170, 1467] 

84 self._setColors(len(self._spectrumBoxOffsets)) 

85 

86 def setSpectrumBoxOffsets(self, offsets): 

87 """Set the current spectrum slice offsets. 

88 

89 Parameters 

90 ---------- 

91 offsets : `list` of `float` 

92 The distance at which to slice the spectrum, measured in pixels 

93 from the main star's location. 

94 """ 

95 self._spectrumBoxOffsets = offsets 

96 self._setColors(len(offsets)) 

97 

98 def getSpectrumBoxOffsets(self): 

99 """Get the current spectrum slice offsets. 

100 

101 Returns 

102 ------- 

103 offsets : `list` of `float` 

104 The distance at which to slice the spectrum, measured in pixels 

105 from the main star's location. 

106 """ 

107 return self._spectrumBoxOffsets 

108 

109 def _setColors(self, nPoints): 

110 self.COLORS = cm.rainbow(np.linspace(0, 1, nPoints)) 

111 

112 @staticmethod 

113 def _getFocusFromHeader(exp): 

114 return float(exp.getMetadata()["FOCUSZ"]) 

115 

116 def _getBboxes(self, centroid): 

117 x, y = centroid 

118 bboxes = [] 

119 

120 for offset in self._spectrumBoxOffsets: 

121 bbox = geom.Box2I(geom.Point2I(x-self.spectrumHalfWidth, y+offset), 

122 geom.Point2I(x+self.spectrumHalfWidth, y+offset+self.spectrumBoxLength)) 

123 bboxes.append(bbox) 

124 return bboxes 

125 

126 def _bboxToMplRectangle(self, bbox, colorNum): 

127 xmin = bbox.getBeginX() 

128 ymin = bbox.getBeginY() 

129 xsize = bbox.getWidth() 

130 ysize = bbox.getHeight() 

131 rectangle = Rectangle((xmin, ymin), xsize, ysize, alpha=1, facecolor='none', lw=2, 

132 edgecolor=self.COLORS[colorNum]) 

133 return rectangle 

134 

135 @staticmethod 

136 def gauss(x, *pars): 

137 amp, mean, sigma = pars 

138 return amp*np.exp(-(x-mean)**2/(2.*sigma**2)) 

139 

140 def run(self, dayObs, seqNums, doDisplay=False, hideFit=False, hexapodZeroPoint=0): 

141 """Perform a focus sweep analysis for spectral data. 

142 

143 For each seqNum for the specified dayObs, take a slice through the 

144 spectrum at y-offsets as specified by the offsets 

145 (see get/setSpectrumBoxOffsets() for setting these) and fit a Gaussian 

146 to the spectrum slice to measure its width. 

147 

148 For each offset distance, fit a parabola to the fitted spectral widths 

149 and return the hexapod position at which the best focus was achieved 

150 for each. 

151 

152 Parameters 

153 ---------- 

154 dayObs : `int` 

155 The dayObs to use. 

156 seqNums : `list` of `int` 

157 The seqNums for the focus sweep to analyze. 

158 doDisplay : `bool` 

159 Show the plots? Designed to be used in a notebook with 

160 %matplotlib inline. 

161 hideFit : `bool`, optional 

162 Hide the fit and just return the result? 

163 hexapodZeroPoint : `float`, optional 

164 Add a zeropoint offset to the hexapod axis? 

165 

166 Returns 

167 ------- 

168 bestFits : `list` of `float` 

169 A list of the best fit focuses, one for each spectral slice. 

170 """ 

171 self.getFocusData(dayObs, seqNums, doDisplay=doDisplay) 

172 bestFits = self.fitDataAndPlot(hideFit=hideFit, hexapodZeroPoint=hexapodZeroPoint) 

173 return bestFits 

174 

175 def getFocusData(self, dayObs, seqNums, doDisplay=False): 

176 """Perform a focus sweep analysis for spectral data. 

177 

178 For each seqNum for the specified dayObs, take a slice through the 

179 spectrum at y-offsets as specified by the offsets 

180 (see get/setSpectrumBoxOffsets() for setting these) and fit a Gaussian 

181 to the spectrum slice to measure its width. 

182 

183 Parameters 

184 ---------- 

185 dayObs : `int` 

186 The dayObs to use. 

187 seqNums : `list` of `int` 

188 The seqNums for the focus sweep to analyze. 

189 doDisplay : `bool` 

190 Show the plots? Designed to be used in a notebook with 

191 %matplotlib inline. 

192 

193 Notes 

194 ----- 

195 Performs the focus analysis per-image, holding the data in the class. 

196 Call fitDataAndPlot() after running this to perform the parabolic fit 

197 to the focus data itself. 

198 """ 

199 fitData = {} 

200 filters = set() 

201 objects = set() 

202 

203 for seqNum in seqNums: 

204 fitData[seqNum] = {} 

205 dataId = {'day_obs': dayObs, 'seq_num': seqNum, 'detector': 0} 

206 exp = self._bestEffort.getExposure(dataId) 

207 

208 # sanity checking 

209 filt = exp.filter.physicalLabel 

210 expRecord = getExpRecordFromDataId(self.butler, dataId) 

211 obj = expRecord.target_name 

212 objects.add(obj) 

213 filters.add(filt) 

214 assert isDispersedExp(exp), f"Image is not dispersed! (filter = {filt})" 

215 assert len(filters) == 1, "You accidentally mixed filters!" 

216 assert len(objects) == 1, "You accidentally mixed objects!" 

217 

218 quickMeasResult = self._quickMeasure.run(exp) 

219 centroid = quickMeasResult.brightestObjCentroid 

220 spectrumSliceBboxes = self._getBboxes(centroid) # inside the loop due to centroid shifts 

221 

222 if doDisplay: 

223 fig, axes = plt.subplots(1, 2, figsize=(18, 9)) 

224 exp.image.array[exp.image.array <= 0] = 0.001 

225 axes[0].imshow(exp.image.array, norm=LogNorm(), origin='lower', cmap='gray_r') 

226 plt.tight_layout() 

227 arrowy, arrowx = centroid[0] - 400, centroid[1] # numpy is backwards 

228 dx, dy = 0, 300 

229 arrow = Arrow(arrowy, arrowx, dy, dx, width=200., color='red') 

230 circle = Circle(centroid, radius=25, facecolor='none', color='red') 

231 axes[0].add_patch(arrow) 

232 axes[0].add_patch(circle) 

233 for i, bbox in enumerate(spectrumSliceBboxes): 

234 rect = self._bboxToMplRectangle(bbox, i) 

235 axes[0].add_patch(rect) 

236 

237 for i, bbox in enumerate(spectrumSliceBboxes): 

238 data1d = np.mean(exp[bbox].image.array, axis=0) # flatten 

239 data1d -= np.median(data1d) 

240 xs = np.arange(len(data1d)) 

241 

242 # get rough estimates for fit 

243 # can't use sigma from quickMeasResult due to SDSS shape 

244 # failing on saturated starts, and fp.getShape() is weird 

245 amp = np.max(data1d) 

246 mean = np.argmax(data1d) 

247 sigma = 20 

248 p0 = amp, mean, sigma 

249 

250 try: 

251 coeffs, var_matrix = curve_fit(self.gauss, xs, data1d, p0=p0) 

252 except RuntimeError: 

253 coeffs = (np.nan, np.nan, np.nan) 

254 

255 fitData[seqNum][i] = FitResult(amp=abs(coeffs[0]), mean=coeffs[1], sigma=abs(coeffs[2])) 

256 if doDisplay: 

257 axes[1].plot(xs, data1d, 'x', c=self.COLORS[i]) 

258 highResX = np.linspace(0, len(data1d), 1000) 

259 if coeffs[0] is not np.nan: 

260 axes[1].plot(highResX, self.gauss(highResX, *coeffs), 'k-') 

261 

262 if doDisplay: # show all color boxes together 

263 plt.title(f'Fits to seqNum {seqNum}') 

264 plt.show() 

265 

266 focuserPosition = self._getFocusFromHeader(exp) 

267 fitData[seqNum]['focus'] = focuserPosition 

268 

269 self.fitData = fitData 

270 self.filter = filters.pop() 

271 self.object = objects.pop() 

272 

273 return 

274 

275 def fitDataAndPlot(self, hideFit=False, hexapodZeroPoint=0): 

276 """Fit a parabola to each series of slices and return the best focus. 

277 

278 For each offset distance, fit a parabola to the fitted spectral widths 

279 and return the hexapod position at which the best focus was achieved 

280 for each. 

281 

282 Parameters 

283 ---------- 

284 hideFit : `bool`, optional 

285 Hide the fit and just return the result? 

286 hexapodZeroPoint : `float`, optional 

287 Add a zeropoint offset to the hexapod axis? 

288 

289 Returns 

290 ------- 

291 bestFits : `list` of `float` 

292 A list of the best fit focuses, one for each spectral slice. 

293 """ 

294 data = self.fitData 

295 filt = self.filter 

296 obj = self.object 

297 

298 bestFits = [] 

299 

300 titleFontSize = 18 

301 legendFontSize = 12 

302 labelFontSize = 14 

303 

304 arcminToPixel = 10 

305 sigmaToFwhm = 2.355 

306 

307 f, axes = plt.subplots(2, 1, figsize=[10, 12]) 

308 focusPositions = [data[k]['focus']-hexapodZeroPoint for k in sorted(data.keys())] 

309 fineXs = np.linspace(np.min(focusPositions), np.max(focusPositions), 101) 

310 seqNums = sorted(data.keys()) 

311 

312 nSpectrumSlices = len(data[list(data.keys())[0]])-1 

313 pointsForLegend = [0.0 for offset in range(nSpectrumSlices)] 

314 for spectrumSlice in range(nSpectrumSlices): # the blue/green/red slices through the spectrum 

315 # for scatter plots, the color needs to be a single-row 2d array 

316 thisColor = np.array([self.COLORS[spectrumSlice]]) 

317 

318 amps = [data[seqNum][spectrumSlice].amp for seqNum in seqNums] 

319 widths = [data[seqNum][spectrumSlice].sigma / arcminToPixel * sigmaToFwhm for seqNum in seqNums] 

320 

321 pointsForLegend[spectrumSlice] = axes[0].scatter(focusPositions, amps, c=thisColor) 

322 axes[0].set_xlabel('Focus position (mm)', fontsize=labelFontSize) 

323 axes[0].set_ylabel('Height (ADU)', fontsize=labelFontSize) 

324 

325 axes[1].scatter(focusPositions, widths, c=thisColor) 

326 axes[1].set_xlabel('Focus position (mm)', fontsize=labelFontSize) 

327 axes[1].set_ylabel('FWHM (arcsec)', fontsize=labelFontSize) 

328 

329 quadFitPars = np.polyfit(focusPositions, widths, 2) 

330 if not hideFit: 

331 axes[1].plot(fineXs, np.poly1d(quadFitPars)(fineXs), c=self.COLORS[spectrumSlice]) 

332 fitMin = -quadFitPars[1] / (2.0*quadFitPars[0]) 

333 bestFits.append(fitMin) 

334 axes[1].axvline(fitMin, color=self.COLORS[spectrumSlice]) 

335 msg = f"Best focus offset = {np.round(fitMin, 2)}" 

336 axes[1].text(fitMin, np.mean(widths), msg, horizontalalignment='right', 

337 verticalalignment='center', rotation=90, color=self.COLORS[spectrumSlice], 

338 fontsize=legendFontSize) 

339 

340 titleText = f"Focus curve for {obj} w/ {filt}" 

341 plt.suptitle(titleText, fontsize=titleFontSize) 

342 legendText = self._generateLegendText(nSpectrumSlices) 

343 axes[0].legend(pointsForLegend, legendText, fontsize=legendFontSize) 

344 axes[1].legend(pointsForLegend, legendText, fontsize=legendFontSize) 

345 f.tight_layout(rect=[0, 0.03, 1, 0.95]) 

346 plt.show() 

347 

348 for i, bestFit in enumerate(bestFits): 

349 print(f"Best fit for spectrum slice {i} = {bestFit:.4f}mm") 

350 return bestFits 

351 

352 def _generateLegendText(self, nSpectrumSlices): 

353 if nSpectrumSlices == 1: 

354 return ['m=+1 spectrum slice'] 

355 if nSpectrumSlices == 2: 

356 return ['m=+1 blue end', 'm=+1 red end'] 

357 

358 legendText = [] 

359 legendText.append('m=+1 blue end') 

360 for i in range(nSpectrumSlices-2): 

361 legendText.append('m=+1 redder...') 

362 legendText.append('m=+1 red end') 

363 return legendText 

364 

365 

366class NonSpectralFocusAnalyzer(): 

367 """Analyze a focus sweep taken for direct imaging data. 

368 

369 For each image, measure the FWHM of the main star and the 50/80/90% 

370 encircled energy radii, and fit a parabola to get the position of best 

371 focus. 

372 

373 Nominal usage is something like: 

374 

375 %matplotlib inline 

376 dayObs = 20210101 

377 seqNums = [100, 101, 102, 103, 104] 

378 focusAnalyzer = NonSpectralFocusAnalyzer() 

379 focusAnalyzer.getFocusData(dayObs, seqNums, doDisplay=True) 

380 focusAnalyzer.fitDataAndPlot() 

381 

382 focusAnalyzer.run() can be used instead of the last two lines separately. 

383 """ 

384 

385 def __init__(self, **kwargs): 

386 self.butler = makeDefaultLatissButler() 

387 self._bestEffort = BestEffortIsr(**kwargs) 

388 

389 @staticmethod 

390 def _getFocusFromHeader(exp): 

391 return float(exp.getMetadata()["FOCUSZ"]) 

392 

393 @staticmethod 

394 def gauss(x, *pars): 

395 amp, mean, sigma = pars 

396 return amp*np.exp(-(x-mean)**2/(2.*sigma**2)) 

397 

398 def run(self, dayObs, seqNums, *, manualCentroid=None, doCheckDispersed=True, doDisplay=False, 

399 doForceCoM=False): 

400 """Perform a focus sweep analysis for direct imaging data. 

401 

402 For each seqNum for the specified dayObs, run the image through imExam 

403 and collect the widths from the Gaussian fit and the 50/80/90% 

404 encircled energy metrics, saving the data in the class for fitting. 

405 

406 For each of the [Gaussian fit, 50%, 80%, 90% encircled energy] metrics, 

407 fit a parabola and return the focus value at which the minimum is 

408 found. 

409 

410 Parameters 

411 ---------- 

412 dayObs : `int` 

413 The dayObs to use. 

414 seqNums : `list` of `int` 

415 The seqNums for the focus sweep to analyze. 

416 manualCentroid : `tuple` of `float`, optional 

417 Use this as the centroid position instead of fitting each image. 

418 doCheckDispersed : `bool`, optional 

419 Check if any of the seqNums actually refer to dispersed images? 

420 doDisplay : `bool`, optional 

421 Show the plots? Designed to be used in a notebook with 

422 %matplotlib inline. 

423 doForceCoM : `bool`, optional 

424 Force using centre-of-mass for centroiding? 

425 

426 Returns 

427 ------- 

428 result : `dict` of `float` 

429 A dict of the fit minima keyed by the metric it is the minimum for. 

430 """ 

431 self.getFocusData(dayObs, seqNums, manualCentroid=manualCentroid, doCheckDispersed=doCheckDispersed, 

432 doDisplay=doDisplay, doForceCoM=doForceCoM) 

433 bestFit = self.fitDataAndPlot() 

434 return bestFit 

435 

436 def getFocusData(self, dayObs, seqNums, *, manualCentroid=None, doCheckDispersed=True, 

437 doDisplay=False, doForceCoM=False): 

438 """Perform a focus sweep analysis for direct imaging data. 

439 

440 For each seqNum for the specified dayObs, run the image through imExam 

441 and collect the widths from the Gaussian fit and the 50/80/90% 

442 encircled energy metrics, saving the data in the class for fitting. 

443 

444 Parameters 

445 ---------- 

446 dayObs : `int` 

447 The dayObs to use. 

448 seqNums : `list` of `int` 

449 The seqNums for the focus sweep to analyze. 

450 manualCentroid : `tuple` of `float`, optional 

451 Use this as the centroid position instead of fitting each image. 

452 doCheckDispersed : `bool`, optional 

453 Check if any of the seqNums actually refer to dispersed images? 

454 doDisplay : `bool`, optional 

455 Show the plots? Designed to be used in a notebook with 

456 %matplotlib inline. 

457 doForceCoM : `bool`, optional 

458 Force using centre-of-mass for centroiding? 

459 

460 Notes 

461 ----- 

462 Performs the focus analysis per-image, holding the data in the class. 

463 Call fitDataAndPlot() after running this to perform the parabolic fit 

464 to the focus data itself. 

465 """ 

466 fitData = {} 

467 filters = set() 

468 objects = set() 

469 

470 maxDistance = 200 

471 firstCentroid = None 

472 

473 for seqNum in seqNums: 

474 fitData[seqNum] = {} 

475 dataId = {'day_obs': dayObs, 'seq_num': seqNum, 'detector': 0} 

476 exp = self._bestEffort.getExposure(dataId) 

477 

478 # sanity/consistency checking 

479 filt = exp.filter.physicalLabel 

480 expRecord = getExpRecordFromDataId(self.butler, dataId) 

481 obj = expRecord.target_name 

482 objects.add(obj) 

483 filters.add(filt) 

484 if doCheckDispersed: 

485 assert not isDispersedExp(exp), f"Image is dispersed! (filter = {filt})" 

486 assert len(filters) == 1, "You accidentally mixed filters!" 

487 assert len(objects) == 1, "You accidentally mixed objects!" 

488 

489 imExam = ImageExaminer(exp, centroid=manualCentroid, doTweakCentroid=True, boxHalfSize=105, 

490 doForceCoM=doForceCoM) 

491 if doDisplay: 

492 imExam.plot() 

493 

494 fwhm = imExam.imStats.fitFwhm 

495 amp = imExam.imStats.fitAmp 

496 gausMean = imExam.imStats.fitGausMean 

497 centroid = imExam.centroid 

498 

499 if seqNum == seqNums[0]: 

500 firstCentroid = centroid 

501 

502 dist = norm(np.array(centroid) - np.array(firstCentroid)) 

503 if dist > maxDistance: 

504 print(f"Skipping {seqNum} because distance {dist}> maxDistance {maxDistance}") 

505 

506 fitData[seqNum]['fitResult'] = FitResult(amp=amp, mean=gausMean, sigma=fwhm*FWHMTOSIGMA) 

507 fitData[seqNum]['eeRadius50'] = imExam.imStats.eeRadius50 

508 fitData[seqNum]['eeRadius80'] = imExam.imStats.eeRadius80 

509 fitData[seqNum]['eeRadius90'] = imExam.imStats.eeRadius90 

510 

511 focuserPosition = self._getFocusFromHeader(exp) 

512 fitData[seqNum]['focus'] = focuserPosition 

513 

514 self.fitData = fitData 

515 self.filter = filters.pop() 

516 self.object = objects.pop() 

517 

518 return 

519 

520 def fitDataAndPlot(self): 

521 """Fit a parabola to each width metric, returning their best focuses. 

522 

523 For each of the [Gaussian fit, 50%, 80%, 90% encircled energy] metrics, 

524 fit a parabola and return the focus value at which the minimum is 

525 found. 

526 

527 Returns 

528 ------- 

529 result : `dict` of `float` 

530 A dict of the fit minima keyed by the metric it is the minimum for. 

531 """ 

532 fitData = self.fitData 

533 

534 labelFontSize = 14 

535 

536 arcminToPixel = 10 

537 

538 fig = plt.figure(figsize=(10, 10)) # noqa 

539 gs = gridspec.GridSpec(2, 1, height_ratios=[1, 1]) 

540 

541 seqNums = sorted(fitData.keys()) 

542 widths = [fitData[seqNum]['fitResult'].sigma * SIGMATOFWHM / arcminToPixel for seqNum in seqNums] 

543 focusPositions = [fitData[seqNum]['focus'] for seqNum in seqNums] 

544 fineXs = np.linspace(np.min(focusPositions), np.max(focusPositions), 101) 

545 

546 fwhmFitPars = np.polyfit(focusPositions, widths, 2) 

547 fwhmFitMin = -fwhmFitPars[1] / (2.0*fwhmFitPars[0]) 

548 

549 ax0 = plt.subplot(gs[0]) 

550 ax0.scatter(focusPositions, widths, c='k') 

551 ax0.set_ylabel('FWHM (arcsec)', fontsize=labelFontSize) 

552 ax0.plot(fineXs, np.poly1d(fwhmFitPars)(fineXs), 'b-') 

553 ax0.axvline(fwhmFitMin, c='r', ls='--') 

554 

555 ee90s = [fitData[seqNum]['eeRadius90'] for seqNum in seqNums] 

556 ee80s = [fitData[seqNum]['eeRadius80'] for seqNum in seqNums] 

557 ee50s = [fitData[seqNum]['eeRadius50'] for seqNum in seqNums] 

558 ax1 = plt.subplot(gs[1], sharex=ax0) 

559 ax1.scatter(focusPositions, ee90s, c='r', label='Encircled energy 90%') 

560 ax1.scatter(focusPositions, ee80s, c='g', label='Encircled energy 80%') 

561 ax1.scatter(focusPositions, ee50s, c='b', label='Encircled energy 50%') 

562 

563 ee90FitPars = np.polyfit(focusPositions, ee90s, 2) 

564 ee90FitMin = -ee90FitPars[1] / (2.0*ee90FitPars[0]) 

565 ee80FitPars = np.polyfit(focusPositions, ee80s, 2) 

566 ee80FitMin = -ee80FitPars[1] / (2.0*ee80FitPars[0]) 

567 ee50FitPars = np.polyfit(focusPositions, ee50s, 2) 

568 ee50FitMin = -ee50FitPars[1] / (2.0*ee50FitPars[0]) 

569 

570 ax1.plot(fineXs, np.poly1d(ee90FitPars)(fineXs), 'r-') 

571 ax1.plot(fineXs, np.poly1d(ee80FitPars)(fineXs), 'g-') 

572 ax1.plot(fineXs, np.poly1d(ee50FitPars)(fineXs), 'b-') 

573 

574 ax1.axvline(ee90FitMin, c='r', ls='--') 

575 ax1.axvline(ee80FitMin, c='g', ls='--') 

576 ax1.axvline(ee50FitMin, c='b', ls='--') 

577 

578 ax1.set_xlabel('User-applied focus offset (mm)', fontsize=labelFontSize) 

579 ax1.set_ylabel('Radius (pixels)', fontsize=labelFontSize) 

580 

581 ax1.legend() 

582 

583 plt.subplots_adjust(hspace=.0) 

584 plt.show() 

585 

586 results = {"fwhmFitMin": fwhmFitMin, 

587 "ee90FitMin": ee90FitMin, 

588 "ee80FitMin": ee80FitMin, 

589 "ee50FitMin": ee50FitMin} 

590 

591 return results 

592 

593 

594if __name__ == '__main__': 594 ↛ 596line 594 didn't jump to line 596, because the condition on line 594 was never true

595 # TODO: DM-34239 Move this to be a butler-driven test 

596 analyzer = SpectralFocusAnalyzer() 

597 # dataId = {'dayObs': '2020-02-20', 'seqNum': 485} # direct image 

598 dataId = {'day_obs': 20200312} 

599 seqNums = [121, 122] 

600 analyzer.getFocusData(dataId['day_obs'], seqNums, doDisplay=True) 

601 analyzer.fitDataAndPlot()