Coverage for python/lsst/summit/extras/focusAnalysis.py: 15%
280 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-21 04:25 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-21 04:25 -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/>.
22from dataclasses import dataclass
24import matplotlib.cm as cm
25import matplotlib.pyplot as plt
26import numpy as np
27from matplotlib import gridspec
28from matplotlib.colors import LogNorm
29from matplotlib.patches import Arrow, Circle, Rectangle
30from scipy.linalg import norm
31from scipy.optimize import curve_fit
33import lsst.geom as geom
34from lsst.atmospec.utils import isDispersedExp
35from lsst.pipe.tasks.quickFrameMeasurement import QuickFrameMeasurementTask, QuickFrameMeasurementTaskConfig
36from lsst.summit.utils import ImageExaminer
38# TODO: change these back to local .imports
39from lsst.summit.utils.bestEffort import BestEffortIsr
40from lsst.summit.utils.butlerUtils import getExpRecordFromDataId, makeDefaultLatissButler
41from lsst.summit.utils.utils import FWHMTOSIGMA, SIGMATOFWHM
43__all__ = ["SpectralFocusAnalyzer", "NonSpectralFocusAnalyzer"]
46@dataclass
47class FitResult:
48 amp: float
49 mean: float
50 sigma: float
53def getFocusFromExposure(exp):
54 """Get the focus value from an exposure.
56 This was previously accessed via raw metadata but now lives inside the
57 visitInfo.
59 Parameters
60 ----------
61 exp : `lsst.afw.image.Exposure`
62 The exposure.
64 Returns
65 -------
66 focus : `float`
67 The focus value.
69 """
70 return float(exp.visitInfo.focusZ)
73class SpectralFocusAnalyzer:
74 """Analyze a focus sweep taken for spectral data.
76 Take slices across the spectrum for each image, fitting a Gaussian to each
77 slice, and perform a parabolic fit to these widths. The number of slices
78 and their distances can be customized by calling setSpectrumBoxOffsets().
80 Nominal usage is something like:
82 %matplotlib inline
83 dayObs = 20210101
84 seqNums = [100, 101, 102, 103, 104]
85 focusAnalyzer = SpectralFocusAnalyzer()
86 focusAnalyzer.setSpectrumBoxOffsets([500, 750, 1000, 1250])
87 focusAnalyzer.getFocusData(dayObs, seqNums, doDisplay=True)
88 focusAnalyzer.fitDataAndPlot()
90 focusAnalyzer.run() can be used instead of the last two lines separately.
91 """
93 def __init__(self, embargo=False):
94 self.butler = makeDefaultLatissButler(embargo=embargo)
95 self._bestEffort = BestEffortIsr(embargo=embargo)
96 qfmTaskConfig = QuickFrameMeasurementTaskConfig()
97 self._quickMeasure = QuickFrameMeasurementTask(config=qfmTaskConfig)
99 self.spectrumHalfWidth = 100
100 self.spectrumBoxLength = 20
101 self._spectrumBoxOffsets = [882, 1170, 1467]
102 self._setColors(len(self._spectrumBoxOffsets))
104 def setSpectrumBoxOffsets(self, offsets):
105 """Set the current spectrum slice offsets.
107 Parameters
108 ----------
109 offsets : `list` of `float`
110 The distance at which to slice the spectrum, measured in pixels
111 from the main star's location.
112 """
113 self._spectrumBoxOffsets = offsets
114 self._setColors(len(offsets))
116 def getSpectrumBoxOffsets(self):
117 """Get the current spectrum slice offsets.
119 Returns
120 -------
121 offsets : `list` of `float`
122 The distance at which to slice the spectrum, measured in pixels
123 from the main star's location.
124 """
125 return self._spectrumBoxOffsets
127 def _setColors(self, nPoints):
128 self.COLORS = cm.rainbow(np.linspace(0, 1, nPoints))
130 def _getBboxes(self, centroid):
131 x, y = centroid
132 bboxes = []
134 for offset in self._spectrumBoxOffsets:
135 bbox = geom.Box2I(
136 geom.Point2I(x - self.spectrumHalfWidth, y + offset),
137 geom.Point2I(x + self.spectrumHalfWidth, y + offset + self.spectrumBoxLength),
138 )
139 bboxes.append(bbox)
140 return bboxes
142 def _bboxToMplRectangle(self, bbox, colorNum):
143 xmin = bbox.getBeginX()
144 ymin = bbox.getBeginY()
145 xsize = bbox.getWidth()
146 ysize = bbox.getHeight()
147 rectangle = Rectangle(
148 (xmin, ymin), xsize, ysize, alpha=1, facecolor="none", lw=2, edgecolor=self.COLORS[colorNum]
149 )
150 return rectangle
152 @staticmethod
153 def gauss(x, *pars):
154 amp, mean, sigma = pars
155 return amp * np.exp(-((x - mean) ** 2) / (2.0 * sigma**2))
157 def run(self, dayObs, seqNums, doDisplay=False, hideFit=False, hexapodZeroPoint=0):
158 """Perform a focus sweep analysis for spectral data.
160 For each seqNum for the specified dayObs, take a slice through the
161 spectrum at y-offsets as specified by the offsets
162 (see get/setSpectrumBoxOffsets() for setting these) and fit a Gaussian
163 to the spectrum slice to measure its width.
165 For each offset distance, fit a parabola to the fitted spectral widths
166 and return the hexapod position at which the best focus was achieved
167 for each.
169 Parameters
170 ----------
171 dayObs : `int`
172 The dayObs to use.
173 seqNums : `list` of `int`
174 The seqNums for the focus sweep to analyze.
175 doDisplay : `bool`
176 Show the plots? Designed to be used in a notebook with
177 %matplotlib inline.
178 hideFit : `bool`, optional
179 Hide the fit and just return the result?
180 hexapodZeroPoint : `float`, optional
181 Add a zeropoint offset to the hexapod axis?
183 Returns
184 -------
185 bestFits : `list` of `float`
186 A list of the best fit focuses, one for each spectral slice.
187 """
188 self.getFocusData(dayObs, seqNums, doDisplay=doDisplay)
189 bestFits = self.fitDataAndPlot(hideFit=hideFit, hexapodZeroPoint=hexapodZeroPoint)
190 return bestFits
192 def getFocusData(self, dayObs, seqNums, doDisplay=False):
193 """Perform a focus sweep analysis for spectral data.
195 For each seqNum for the specified dayObs, take a slice through the
196 spectrum at y-offsets as specified by the offsets
197 (see get/setSpectrumBoxOffsets() for setting these) and fit a Gaussian
198 to the spectrum slice to measure its width.
200 Parameters
201 ----------
202 dayObs : `int`
203 The dayObs to use.
204 seqNums : `list` of `int`
205 The seqNums for the focus sweep to analyze.
206 doDisplay : `bool`
207 Show the plots? Designed to be used in a notebook with
208 %matplotlib inline.
210 Notes
211 -----
212 Performs the focus analysis per-image, holding the data in the class.
213 Call fitDataAndPlot() after running this to perform the parabolic fit
214 to the focus data itself.
215 """
216 fitData = {}
217 filters = set()
218 objects = set()
220 for seqNum in seqNums:
221 fitData[seqNum] = {}
222 dataId = {"day_obs": dayObs, "seq_num": seqNum, "detector": 0}
223 exp = self._bestEffort.getExposure(dataId)
225 # sanity checking
226 filt = exp.filter.physicalLabel
227 expRecord = getExpRecordFromDataId(self.butler, dataId)
228 obj = expRecord.target_name
229 objects.add(obj)
230 filters.add(filt)
231 assert isDispersedExp(exp), f"Image is not dispersed! (filter = {filt})"
232 assert len(filters) == 1, "You accidentally mixed filters!"
233 assert len(objects) == 1, "You accidentally mixed objects!"
235 quickMeasResult = self._quickMeasure.run(exp)
236 centroid = quickMeasResult.brightestObjCentroid
237 spectrumSliceBboxes = self._getBboxes(centroid) # inside the loop due to centroid shifts
239 if doDisplay:
240 fig, axes = plt.subplots(1, 2, figsize=(18, 9))
241 exp.image.array[exp.image.array <= 0] = 0.001
242 axes[0].imshow(exp.image.array, norm=LogNorm(), origin="lower", cmap="gray_r")
243 plt.tight_layout()
244 arrowy, arrowx = centroid[0] - 400, centroid[1] # numpy is backwards
245 dx, dy = 0, 300
246 arrow = Arrow(arrowy, arrowx, dy, dx, width=200.0, color="red")
247 circle = Circle(centroid, radius=25, facecolor="none", color="red")
248 axes[0].add_patch(arrow)
249 axes[0].add_patch(circle)
250 for i, bbox in enumerate(spectrumSliceBboxes):
251 rect = self._bboxToMplRectangle(bbox, i)
252 axes[0].add_patch(rect)
254 for i, bbox in enumerate(spectrumSliceBboxes):
255 data1d = np.mean(exp[bbox].image.array, axis=0) # flatten
256 data1d -= np.median(data1d)
257 xs = np.arange(len(data1d))
259 # get rough estimates for fit
260 # can't use sigma from quickMeasResult due to SDSS shape
261 # failing on saturated starts, and fp.getShape() is weird
262 amp = np.max(data1d)
263 mean = np.argmax(data1d)
264 sigma = 20
265 p0 = amp, mean, sigma
267 try:
268 coeffs, var_matrix = curve_fit(self.gauss, xs, data1d, p0=p0)
269 except RuntimeError:
270 coeffs = (np.nan, np.nan, np.nan)
272 fitData[seqNum][i] = FitResult(amp=abs(coeffs[0]), mean=coeffs[1], sigma=abs(coeffs[2]))
273 if doDisplay:
274 axes[1].plot(xs, data1d, "x", c=self.COLORS[i])
275 highResX = np.linspace(0, len(data1d), 1000)
276 if coeffs[0] is not np.nan:
277 axes[1].plot(highResX, self.gauss(highResX, *coeffs), "k-")
279 if doDisplay: # show all color boxes together
280 plt.title(f"Fits to seqNum {seqNum}")
281 plt.show()
283 focuserPosition = getFocusFromExposure(exp)
284 fitData[seqNum]["focus"] = focuserPosition
286 self.fitData = fitData
287 self.filter = filters.pop()
288 self.object = objects.pop()
290 return
292 def fitDataAndPlot(self, hideFit=False, hexapodZeroPoint=0):
293 """Fit a parabola to each series of slices and return the best focus.
295 For each offset distance, fit a parabola to the fitted spectral widths
296 and return the hexapod position at which the best focus was achieved
297 for each.
299 Parameters
300 ----------
301 hideFit : `bool`, optional
302 Hide the fit and just return the result?
303 hexapodZeroPoint : `float`, optional
304 Add a zeropoint offset to the hexapod axis?
306 Returns
307 -------
308 bestFits : `list` of `float`
309 A list of the best fit focuses, one for each spectral slice.
310 """
311 data = self.fitData
312 filt = self.filter
313 obj = self.object
315 bestFits = []
317 titleFontSize = 18
318 legendFontSize = 12
319 labelFontSize = 14
321 arcminToPixel = 10
322 sigmaToFwhm = 2.355
324 f, axes = plt.subplots(2, 1, figsize=[10, 12])
325 focusPositions = [data[k]["focus"] - hexapodZeroPoint for k in sorted(data.keys())]
326 fineXs = np.linspace(np.min(focusPositions), np.max(focusPositions), 101)
327 seqNums = sorted(data.keys())
329 nSpectrumSlices = len(data[list(data.keys())[0]]) - 1
330 pointsForLegend = [0.0 for offset in range(nSpectrumSlices)]
331 for spectrumSlice in range(nSpectrumSlices): # the blue/green/red slices through the spectrum
332 # for scatter plots, the color needs to be a single-row 2d array
333 thisColor = np.array([self.COLORS[spectrumSlice]])
335 amps = [data[seqNum][spectrumSlice].amp for seqNum in seqNums]
336 widths = [data[seqNum][spectrumSlice].sigma / arcminToPixel * sigmaToFwhm for seqNum in seqNums]
338 pointsForLegend[spectrumSlice] = axes[0].scatter(focusPositions, amps, c=thisColor)
339 axes[0].set_xlabel("Focus position (mm)", fontsize=labelFontSize)
340 axes[0].set_ylabel("Height (ADU)", fontsize=labelFontSize)
342 axes[1].scatter(focusPositions, widths, c=thisColor)
343 axes[1].set_xlabel("Focus position (mm)", fontsize=labelFontSize)
344 axes[1].set_ylabel("FWHM (arcsec)", fontsize=labelFontSize)
346 quadFitPars = np.polyfit(focusPositions, widths, 2)
347 if not hideFit:
348 axes[1].plot(fineXs, np.poly1d(quadFitPars)(fineXs), c=self.COLORS[spectrumSlice])
349 fitMin = -quadFitPars[1] / (2.0 * quadFitPars[0])
350 bestFits.append(fitMin)
351 axes[1].axvline(fitMin, color=self.COLORS[spectrumSlice])
352 msg = f"Best focus offset = {np.round(fitMin, 2)}"
353 axes[1].text(
354 fitMin,
355 np.mean(widths),
356 msg,
357 horizontalalignment="right",
358 verticalalignment="center",
359 rotation=90,
360 color=self.COLORS[spectrumSlice],
361 fontsize=legendFontSize,
362 )
364 titleText = f"Focus curve for {obj} w/ {filt}"
365 plt.suptitle(titleText, fontsize=titleFontSize)
366 legendText = self._generateLegendText(nSpectrumSlices)
367 axes[0].legend(pointsForLegend, legendText, fontsize=legendFontSize)
368 axes[1].legend(pointsForLegend, legendText, fontsize=legendFontSize)
369 f.tight_layout(rect=[0, 0.03, 1, 0.95])
370 plt.show()
372 for i, bestFit in enumerate(bestFits):
373 print(f"Best fit for spectrum slice {i} = {bestFit:.4f}mm")
374 return bestFits
376 def _generateLegendText(self, nSpectrumSlices):
377 if nSpectrumSlices == 1:
378 return ["m=+1 spectrum slice"]
379 if nSpectrumSlices == 2:
380 return ["m=+1 blue end", "m=+1 red end"]
382 legendText = []
383 legendText.append("m=+1 blue end")
384 for i in range(nSpectrumSlices - 2):
385 legendText.append("m=+1 redder...")
386 legendText.append("m=+1 red end")
387 return legendText
390class NonSpectralFocusAnalyzer:
391 """Analyze a focus sweep taken for direct imaging data.
393 For each image, measure the FWHM of the main star and the 50/80/90%
394 encircled energy radii, and fit a parabola to get the position of best
395 focus.
397 Nominal usage is something like:
399 %matplotlib inline
400 dayObs = 20210101
401 seqNums = [100, 101, 102, 103, 104]
402 focusAnalyzer = NonSpectralFocusAnalyzer()
403 focusAnalyzer.getFocusData(dayObs, seqNums, doDisplay=True)
404 focusAnalyzer.fitDataAndPlot()
406 focusAnalyzer.run() can be used instead of the last two lines separately.
407 """
409 def __init__(self, embargo=False):
410 self.butler = makeDefaultLatissButler(embargo=embargo)
411 self._bestEffort = BestEffortIsr(embargo=embargo)
413 @staticmethod
414 def gauss(x, *pars):
415 amp, mean, sigma = pars
416 return amp * np.exp(-((x - mean) ** 2) / (2.0 * sigma**2))
418 def run(
419 self,
420 dayObs,
421 seqNums,
422 *,
423 manualCentroid=None,
424 doCheckDispersed=True,
425 doDisplay=False,
426 doForceCoM=False,
427 ):
428 """Perform a focus sweep analysis for direct imaging data.
430 For each seqNum for the specified dayObs, run the image through imExam
431 and collect the widths from the Gaussian fit and the 50/80/90%
432 encircled energy metrics, saving the data in the class for fitting.
434 For each of the [Gaussian fit, 50%, 80%, 90% encircled energy] metrics,
435 fit a parabola and return the focus value at which the minimum is
436 found.
438 Parameters
439 ----------
440 dayObs : `int`
441 The dayObs to use.
442 seqNums : `list` of `int`
443 The seqNums for the focus sweep to analyze.
444 manualCentroid : `tuple` of `float`, optional
445 Use this as the centroid position instead of fitting each image.
446 doCheckDispersed : `bool`, optional
447 Check if any of the seqNums actually refer to dispersed images?
448 doDisplay : `bool`, optional
449 Show the plots? Designed to be used in a notebook with
450 %matplotlib inline.
451 doForceCoM : `bool`, optional
452 Force using centre-of-mass for centroiding?
454 Returns
455 -------
456 result : `dict` of `float`
457 A dict of the fit minima keyed by the metric it is the minimum for.
458 """
459 self.getFocusData(
460 dayObs,
461 seqNums,
462 manualCentroid=manualCentroid,
463 doCheckDispersed=doCheckDispersed,
464 doDisplay=doDisplay,
465 doForceCoM=doForceCoM,
466 )
467 bestFit = self.fitDataAndPlot()
468 return bestFit
470 def getFocusData(
471 self,
472 dayObs,
473 seqNums,
474 *,
475 manualCentroid=None,
476 doCheckDispersed=True,
477 doDisplay=False,
478 doForceCoM=False,
479 ):
480 """Perform a focus sweep analysis for direct imaging data.
482 For each seqNum for the specified dayObs, run the image through imExam
483 and collect the widths from the Gaussian fit and the 50/80/90%
484 encircled energy metrics, saving the data in the class for fitting.
486 Parameters
487 ----------
488 dayObs : `int`
489 The dayObs to use.
490 seqNums : `list` of `int`
491 The seqNums for the focus sweep to analyze.
492 manualCentroid : `tuple` of `float`, optional
493 Use this as the centroid position instead of fitting each image.
494 doCheckDispersed : `bool`, optional
495 Check if any of the seqNums actually refer to dispersed images?
496 doDisplay : `bool`, optional
497 Show the plots? Designed to be used in a notebook with
498 %matplotlib inline.
499 doForceCoM : `bool`, optional
500 Force using centre-of-mass for centroiding?
502 Notes
503 -----
504 Performs the focus analysis per-image, holding the data in the class.
505 Call fitDataAndPlot() after running this to perform the parabolic fit
506 to the focus data itself.
507 """
508 fitData = {}
509 filters = set()
510 objects = set()
512 maxDistance = 200
513 firstCentroid = None
515 for seqNum in seqNums:
516 fitData[seqNum] = {}
517 dataId = {"day_obs": dayObs, "seq_num": seqNum, "detector": 0}
518 exp = self._bestEffort.getExposure(dataId)
520 # sanity/consistency checking
521 filt = exp.filter.physicalLabel
522 expRecord = getExpRecordFromDataId(self.butler, dataId)
523 obj = expRecord.target_name
524 objects.add(obj)
525 filters.add(filt)
526 if doCheckDispersed:
527 assert not isDispersedExp(exp), f"Image is dispersed! (filter = {filt})"
528 assert len(filters) == 1, "You accidentally mixed filters!"
529 assert len(objects) == 1, "You accidentally mixed objects!"
531 imExam = ImageExaminer(
532 exp, centroid=manualCentroid, doTweakCentroid=True, boxHalfSize=105, doForceCoM=doForceCoM
533 )
534 if doDisplay:
535 imExam.plot()
537 fwhm = imExam.imStats.fitFwhm
538 amp = imExam.imStats.fitAmp
539 gausMean = imExam.imStats.fitGausMean
540 centroid = imExam.centroid
542 if seqNum == seqNums[0]:
543 firstCentroid = centroid
545 dist = norm(np.array(centroid) - np.array(firstCentroid))
546 if dist > maxDistance:
547 print(f"Skipping {seqNum} because distance {dist}> maxDistance {maxDistance}")
549 fitData[seqNum]["fitResult"] = FitResult(amp=amp, mean=gausMean, sigma=fwhm * FWHMTOSIGMA)
550 fitData[seqNum]["eeRadius50"] = imExam.imStats.eeRadius50
551 fitData[seqNum]["eeRadius80"] = imExam.imStats.eeRadius80
552 fitData[seqNum]["eeRadius90"] = imExam.imStats.eeRadius90
554 focuserPosition = getFocusFromExposure(exp)
555 fitData[seqNum]["focus"] = focuserPosition
557 self.fitData = fitData
558 self.filter = filters.pop()
559 self.object = objects.pop()
561 return
563 def fitDataAndPlot(self):
564 """Fit a parabola to each width metric, returning their best focuses.
566 For each of the [Gaussian fit, 50%, 80%, 90% encircled energy] metrics,
567 fit a parabola and return the focus value at which the minimum is
568 found.
570 Returns
571 -------
572 result : `dict` of `float`
573 A dict of the fit minima keyed by the metric it is the minimum for.
574 """
575 fitData = self.fitData
577 labelFontSize = 14
579 arcminToPixel = 10
581 fig = plt.figure(figsize=(10, 10)) # noqa
582 gs = gridspec.GridSpec(2, 1, height_ratios=[1, 1])
584 seqNums = sorted(fitData.keys())
585 widths = [fitData[seqNum]["fitResult"].sigma * SIGMATOFWHM / arcminToPixel for seqNum in seqNums]
586 focusPositions = [fitData[seqNum]["focus"] for seqNum in seqNums]
587 fineXs = np.linspace(np.min(focusPositions), np.max(focusPositions), 101)
589 fwhmFitPars = np.polyfit(focusPositions, widths, 2)
590 fwhmFitMin = -fwhmFitPars[1] / (2.0 * fwhmFitPars[0])
592 ax0 = plt.subplot(gs[0])
593 ax0.scatter(focusPositions, widths, c="k")
594 ax0.set_ylabel("FWHM (arcsec)", fontsize=labelFontSize)
595 ax0.plot(fineXs, np.poly1d(fwhmFitPars)(fineXs), "b-")
596 ax0.axvline(fwhmFitMin, c="r", ls="--")
598 ee90s = [fitData[seqNum]["eeRadius90"] for seqNum in seqNums]
599 ee80s = [fitData[seqNum]["eeRadius80"] for seqNum in seqNums]
600 ee50s = [fitData[seqNum]["eeRadius50"] for seqNum in seqNums]
601 ax1 = plt.subplot(gs[1], sharex=ax0)
602 ax1.scatter(focusPositions, ee90s, c="r", label="Encircled energy 90%")
603 ax1.scatter(focusPositions, ee80s, c="g", label="Encircled energy 80%")
604 ax1.scatter(focusPositions, ee50s, c="b", label="Encircled energy 50%")
606 ee90FitPars = np.polyfit(focusPositions, ee90s, 2)
607 ee90FitMin = -ee90FitPars[1] / (2.0 * ee90FitPars[0])
608 ee80FitPars = np.polyfit(focusPositions, ee80s, 2)
609 ee80FitMin = -ee80FitPars[1] / (2.0 * ee80FitPars[0])
610 ee50FitPars = np.polyfit(focusPositions, ee50s, 2)
611 ee50FitMin = -ee50FitPars[1] / (2.0 * ee50FitPars[0])
613 ax1.plot(fineXs, np.poly1d(ee90FitPars)(fineXs), "r-")
614 ax1.plot(fineXs, np.poly1d(ee80FitPars)(fineXs), "g-")
615 ax1.plot(fineXs, np.poly1d(ee50FitPars)(fineXs), "b-")
617 ax1.axvline(ee90FitMin, c="r", ls="--")
618 ax1.axvline(ee80FitMin, c="g", ls="--")
619 ax1.axvline(ee50FitMin, c="b", ls="--")
621 ax1.set_xlabel("User-applied focus offset (mm)", fontsize=labelFontSize)
622 ax1.set_ylabel("Radius (pixels)", fontsize=labelFontSize)
624 ax1.legend()
626 plt.subplots_adjust(hspace=0.0)
627 plt.show()
629 results = {
630 "fwhmFitMin": fwhmFitMin,
631 "ee90FitMin": ee90FitMin,
632 "ee80FitMin": ee80FitMin,
633 "ee50FitMin": ee50FitMin,
634 }
636 return results
639if __name__ == "__main__": 639 ↛ 641line 639 didn't jump to line 641, because the condition on line 639 was never true
640 # TODO: DM-34239 Move this to be a butler-driven test
641 analyzer = SpectralFocusAnalyzer()
642 # dataId = {'dayObs': '2020-02-20', 'seqNum': 485} # direct image
643 dataId = {"day_obs": 20200312}
644 seqNums = [121, 122]
645 analyzer.getFocusData(dataId["day_obs"], seqNums, doDisplay=True)
646 analyzer.fitDataAndPlot()