Coverage for soxspipe/commonutils/detect_continuum.py : 90%

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#!/usr/bin/env python
2# encoding: utf-8
3"""
4*find and fit the continuum in a pinhole flat frame with low-order polynomials. These polynominals are the central loctions of the orders*
6:Author:
7 David Young & Marco Landoni
9:Date Created:
10 September 10, 2020
11"""
12################# GLOBAL IMPORTS ####################
13from builtins import object
14import sys
15import os
16os.environ['TERM'] = 'vt100'
17from fundamentals import tools
18from soxspipe.commonutils import keyword_lookup
19from soxspipe.commonutils import detector_lookup
20from os.path import expanduser
21import numpy as np
22from soxspipe.commonutils.polynomials import chebyshev_xy_polynomial
23from soxspipe.commonutils.filenamer import filenamer
24import matplotlib.pyplot as plt
25from astropy.stats import mad_std
26from astropy.modeling import models, fitting
27from scipy.signal import find_peaks
28from random import random
29from scipy.optimize import curve_fit
30from astropy.stats import sigma_clip, mad_std
31from astropy.visualization import hist
32import collections
33from fundamentals.renderer import list_of_dictionaries
34from soxspipe.commonutils.dispersion_map_to_pixel_arrays import dispersion_map_to_pixel_arrays
35import pandas as pd
36from soxspipe.commonutils.toolkit import cut_image_slice
39class _base_detect(object):
41 def fit_polynomial(
42 self,
43 pixelList,
44 order,
45 xCol,
46 yCol):
47 """*iteratively fit the dispersion map polynomials to the data, clipping residuals with each iteration*
49 **Key Arguments:**
50 - ``pixelList`` -- data-frame group containing x,y pixel array
51 - ``order`` -- the order to fit
52 - ``xCol`` -- name of x-pixel column
53 - ``yCol`` -- name of y-pixel column
55 **Return:**
56 - ``coeffs`` -- the coefficients of the polynomial fit
57 - ``pixelList`` -- the pixel list but now with fits and residuals included
58 """
59 self.log.debug('starting the ``fit_polynomial`` method')
61 arm = self.arm
63 clippedCount = 1
65 poly = chebyshev_xy_polynomial(
66 log=self.log, deg=self.polyDeg).poly
67 clippingSigma = self.recipeSettings[
68 "poly-fitting-residual-clipping-sigma"]
69 clippingIterationLimit = self.recipeSettings[
70 "clipping-iteration-limit"]
72 iteration = 0
73 mask = (pixelList['order'] == order)
74 pixelListFiltered = pixelList.loc[mask]
75 while clippedCount > 0 and iteration < clippingIterationLimit:
76 pixelListFiltered = pixelList.loc[mask]
78 startCount = len(pixelListFiltered.index)
79 iteration += 1
80 # USE LEAST-SQUARED CURVE FIT TO FIT CHEBY POLY
81 coeff = np.ones((self.polyDeg + 1))
82 # NOTE X AND Y COLUMN ARE CORRECLY IN xdata AND ydata - WANT TO
83 # FIND X (UNKNOWN) WRT Y (KNOWNN)
84 try:
85 coeff, pcov_x = curve_fit(
86 poly, xdata=pixelListFiltered[yCol].values, ydata=pixelListFiltered[xCol].values, p0=coeff)
87 except TypeError as e:
88 # REMOVE THIS ORDER FROM PIXEL LIST
89 pixelList.drop(index=pixelList[mask].index, inplace=True)
90 coeff = None
91 return coeff, pixelList
92 except Exception as e:
93 raise e
95 res, res_mean, res_std, res_median, xfit = self.calculate_residuals(
96 orderPixelTable=pixelListFiltered,
97 coeff=coeff,
98 xCol=xCol,
99 yCol=yCol)
101 pixelList.loc[mask, "x_fit_res"] = res
102 pixelList.loc[mask, "x_fit"] = xfit
104 # SIGMA-CLIP THE DATA
105 masked_residuals = sigma_clip(
106 res, sigma_lower=clippingSigma, sigma_upper=clippingSigma, maxiters=1, cenfunc='median', stdfunc=mad_std)
107 pixelList.loc[mask, "mask"] = masked_residuals.mask
109 # REMOVE FILTERED ROWS FROM DATA FRAME
110 removeMask = (pixelList["mask"] == True)
111 pixelList.drop(index=pixelList[removeMask].index, inplace=True)
112 pixelListFiltered = pixelList.loc[mask]
113 clippedCount = startCount - len(pixelListFiltered.index)
115 sys.stdout.write("\x1b[1A\x1b[2K")
116 print(f'\t\tORDER {order:0.0f}: {clippedCount} pixel positions where clipped in iteration {iteration} of fitting the polynomial')
118 self.log.debug('completed the ``fit_polynomials`` method')
119 return coeff, pixelList
121 def calculate_residuals(
122 self,
123 orderPixelTable,
124 coeff,
125 xCol,
126 yCol):
127 """*calculate residuals of the polynomial fits against the observed line postions*
129 **Key Arguments:**
130 - ``orderPixelTable`` -- data-frame containing pixel list for given order
131 - ``coeff`` -- the coefficients of the fitted polynomial
132 - ``xCol`` -- name of x-pixel column
133 - ``yCol`` -- name of y-pixel column
135 **Return:**
136 - ``res`` -- x residuals
137 - ``mean`` -- the mean of the residuals
138 - ``std`` -- the stdev of the residuals
139 - ``median`` -- the median of the residuals
140 - ``xfit`` -- fitted x values
141 """
142 self.log.debug('starting the ``calculate_residuals`` method')
144 arm = self.arm
146 poly = chebyshev_xy_polynomial(
147 log=self.log, deg=self.polyDeg).poly
149 # CALCULATE RESIDUALS BETWEEN GAUSSIAN PEAK LINE POSITIONS AND POLY
150 # FITTED POSITIONS
151 xfit = poly(
152 orderPixelTable[yCol].values, *coeff)
153 res = xfit - orderPixelTable[xCol].values
155 # CALCULATE COMBINED RESIDUALS AND STATS
156 res_mean = np.mean(res)
157 res_std = np.std(res)
158 res_median = np.median(res)
160 self.log.debug('completed the ``calculate_residuals`` method')
161 return res, res_mean, res_std, res_median, xfit
163 def write_order_table_to_file(
164 self,
165 frame,
166 orderPolyTable):
167 """*write out the fitted polynomial solution coefficients to file*
169 **Key Arguments:**
170 - ``frame`` -- the calibration frame used to generate order location data
171 - ``orderPolyTable`` -- data-frames containing centre location coefficients (and possibly also order edge coeffs)
173 **Return:**
174 - ``order_table_path`` -- path to the order table file
175 """
176 self.log.debug('starting the ``write_order_table_to_file`` method')
178 arm = self.arm
180 # DETERMINE WHERE TO WRITE THE FILE
181 home = expanduser("~")
182 outDir = self.settings["intermediate-data-root"].replace("~", home)
184 filename = filenamer(
185 log=self.log,
186 frame=frame,
187 settings=self.settings
188 )
189 filename = filename.replace("MFLAT", "FLAT")
190 filename = filename.split("FLAT")[0] + "ORDER_LOCATIONS.csv"
192 order_table_path = f"{outDir}/{filename}"
193 orderPolyTable.to_csv(order_table_path, index=False)
195 self.log.debug('completed the ``write_order_table_to_file`` method')
196 return order_table_path
199class detect_continuum(_base_detect):
200 """
201 *find and fit the continuum in a pinhole flat frame with low-order polynomials. These polynominals are the central loctions of the orders*
203 **Key Arguments:**
204 - ``log`` -- logger
205 - ``pinholeFlat`` -- calibrationed pinhole flat frame (CCDObject)
206 - ``dispersion_map`` -- path to dispersion map csv file containing polynomial fits of the dispersion solution for the frame
207 - ``settings`` -- the recipe settings dictionary
208 - ``recipeName`` -- the recipe name as given in the settings dictionary
209 - ``qcTable`` -- the data frame to collect measured QC metrics
210 - ``productsTable`` -- the data frame to collect output products
212 **Usage:**
214 To use the ``detect_continuum`` object, use the following:
216 ```python
217 from soxspipe.commonutils import detect_continuum
218 detector = detect_continuum(
219 log=log,
220 pinholeFlat=pinholeFlat,
221 dispersion_map=dispersion_map,
222 settings=settings,
223 recipeName="soxs-order-centre"
224 )
225 order_table_path = detector.get()
226 ```
227 """
229 def __init__(
230 self,
231 log,
232 pinholeFlat,
233 dispersion_map,
234 settings=False,
235 recipeName=False,
236 qcTable=False,
237 productsTable=False
238 ):
239 self.log = log
240 log.debug("instansiating a new 'detect_continuum' object")
241 self.settings = settings
242 if recipeName:
243 self.recipeSettings = settings[recipeName]
244 else:
245 self.recipeSettings = False
246 self.pinholeFlat = pinholeFlat
247 self.dispersion_map = dispersion_map
248 self.qc = qcTable
249 self.products = productsTable
251 # KEYWORD LOOKUP OBJECT - LOOKUP KEYWORD FROM DICTIONARY IN RESOURCES
252 # FOLDER
253 self.kw = keyword_lookup(
254 log=self.log,
255 settings=self.settings
256 ).get
257 self.arm = pinholeFlat.header[self.kw("SEQ_ARM")]
258 self.dateObs = pinholeFlat.header[self.kw("DATE_OBS")]
260 # DETECTOR PARAMETERS LOOKUP OBJECT
261 self.detectorParams = detector_lookup(
262 log=log,
263 settings=settings
264 ).get(self.arm)
266 # DEG OF THE POLYNOMIALS TO FIT THE ORDER CENTRE LOCATIONS
267 self.polyDeg = self.recipeSettings["poly-deg"]
269 return None
271 def get(self):
272 """
273 *return the order centre table filepath*
275 **Return:**
276 - ``order_table_path`` -- file path to the order centre table giving polynomial coeffs to each order fit
277 """
278 self.log.debug('starting the ``get`` method')
280 arm = self.arm
282 # READ THE SPECTRAL FORMAT TABLE TO DETERMINE THE LIMITS OF THE TRACES
283 orderNums, waveLengthMin, waveLengthMax = self.read_spectral_format()
285 # CONVERT WAVELENGTH TO PIXEL POSTIONS AND RETURN ARRAY OF POSITIONS TO
286 # SAMPLE THE TRACES
287 orderPixelTable = self.create_pixel_arrays(
288 orderNums,
289 waveLengthMin,
290 waveLengthMax)
291 # SLICE LENGTH TO SAMPLE TRACES IN THE CROSS-DISPERSION DIRECTION
292 self.sliceLength = self.recipeSettings["slice-length"]
293 self.peakSigmaLimit = self.recipeSettings["peak-sigma-limit"]
295 # PREP LISTS WITH NAN VALUE IN CONT_X AND CONT_Y BEFORE FITTING
296 orderPixelTable['cont_x'] = np.nan
297 orderPixelTable['cont_y'] = np.nan
299 # FOR EACH ORDER, FOR EACH PIXEL POSITION SAMPLE, FIT A 1D GAUSSIAN IN
300 # CROSS-DISPERSION DIRECTTION. RETURN PEAK POSTIONS
301 orderPixelTable = orderPixelTable.apply(
302 self.fit_1d_gaussian_to_slice, axis=1)
303 allLines = len(orderPixelTable.index)
304 # DROP ROWS WITH NAN VALUES
305 orderPixelTable.dropna(axis='index', how='any',
306 subset=['cont_x'], inplace=True)
307 foundLines = len(orderPixelTable.index)
308 percent = 100 * foundLines / allLines
309 print(f"{foundLines} out of {allLines} found ({percent:3.0f}%)")
311 # GET UNIQUE VALUES IN COLUMN
312 uniqueOrders = orderPixelTable['order'].unique()
314 orderLocations = {}
315 orderPixelTable['x_fit'] = np.nan
316 orderPixelTable['x_fit_res'] = np.nan
318 for o in uniqueOrders:
319 # ITERATIVELY FIT THE POLYNOMIAL SOLUTIONS TO THE DATA
320 coeff, orderPixelTable = self.fit_polynomial(
321 pixelList=orderPixelTable,
322 order=o,
323 xCol="cont_x",
324 yCol="cont_y"
325 )
326 orderLocations[o] = coeff
328 # SORT CENTRE TRACE COEFFICIENT OUTPUT TO PANDAS DATAFRAME
329 columnsNames = ["order", "degy_cent", "ymin", "ymax"]
330 coeffColumns = [f'cent_c{i}' for i in range(0, self.polyDeg + 1)]
331 columnsNames.extend(coeffColumns)
332 myDict = {k: [] for k in columnsNames}
333 for k, v in orderLocations.items():
334 myDict["order"].append(k)
335 myDict["degy_cent"].append(self.polyDeg)
336 n_coeff = 0
337 for i in range(0, self.polyDeg + 1):
338 myDict[f'cent_c{i}'].append(v[n_coeff])
339 n_coeff += 1
341 myDict["ymin"].append(
342 np.min(orderPixelTable.loc[(orderPixelTable['order'] == k)]["fit_y"].values))
343 myDict["ymax"].append(
344 np.max(orderPixelTable.loc[(orderPixelTable['order'] == k)]["fit_y"].values))
345 orderPolyTable = pd.DataFrame(myDict)
347 # HERE IS THE LINE LIST IF NEEDED FOR QC
348 orderPixelTable.drop(columns=['mask'], inplace=True)
350 plotPath = self.plot_results(
351 orderPixelTable=orderPixelTable,
352 orderPolyTable=orderPolyTable
353 )
355 mean_res = np.mean(np.abs(orderPixelTable['x_fit_res'].values))
356 std_res = np.std(np.abs(orderPixelTable['x_fit_res'].values))
358 print(f'\nThe order centre polynomial fitted against the observed 1D gaussian peak positions with a mean residual of {mean_res:2.2f} pixels (stdev = {std_res:2.2f} pixels)')
360 # WRITE OUT THE FITS TO THE ORDER CENTRE TABLE
361 order_table_path = self.write_order_table_to_file(
362 frame=self.pinholeFlat, orderPolyTable=orderPolyTable)
364 print(f'\nFind results of the order centre fitting here: {plotPath}')
366 self.log.debug('completed the ``get`` method')
367 return order_table_path
369 def read_spectral_format(
370 self):
371 """*read the spectral format table to get some key parameters*
373 **Return:**
374 - ``orderNums`` -- a list of the order numbers
375 - ``waveLengthMin`` -- a list of the maximum wavelengths reached by each order
376 - ``waveLengthMax`` -- a list of the minimum wavelengths reached by each order
377 """
378 self.log.debug('starting the ``read_spectral_format`` method')
380 kw = self.kw
381 pinholeFlat = self.pinholeFlat
382 dp = self.detectorParams
384 # READ THE SPECTRAL FORMAT TABLE FILE
385 home = expanduser("~")
386 calibrationRootPath = self.settings[
387 "calibration-data-root"].replace("~", home)
388 spectralFormatFile = calibrationRootPath + \
389 "/" + dp["spectral format table"]
391 # READ CSV FILE TO PANDAS DATAFRAME
392 specFormatTable = pd.read_csv(
393 spectralFormatFile, index_col=False, na_values=['NA', 'MISSING'])
394 # # DATAFRAME INFO
396 # EXTRACT REQUIRED PARAMETERS
397 orderNums = specFormatTable["ORDER"].values
398 waveLengthMin = specFormatTable["WLMINFUL"].values
399 waveLengthMax = specFormatTable["WLMAXFUL"].values
401 self.log.debug('completed the ``read_spectral_format`` method')
402 return orderNums, waveLengthMin, waveLengthMax
404 def create_pixel_arrays(
405 self,
406 orderNums,
407 waveLengthMin,
408 waveLengthMax):
409 """*create a pixel array for the approximate centre of each order*
411 **Key Arguments:**
412 - ``orderNums`` -- a list of the order numbers
413 - ``waveLengthMin`` -- a list of the maximum wavelengths reached by each order
414 - ``waveLengthMax`` -- a list of the minimum wavelengths reached by each order
416 **Return:**
417 - ``orderPixelTable`` -- a data-frame containing lines and associated pixel locations
418 """
419 self.log.debug('starting the ``create_pixel_arrays`` method')
421 # READ ORDER SAMPLING RESOLUTION FROM SETTINGS
422 sampleCount = self.settings[
423 "soxs-order-centre"]["order-sample-count"]
425 # CREATE THE WAVELENGTH/ORDER ARRAYS TO BE CONVERTED TO PIXELS
426 myDict = {
427 "order": np.asarray([]),
428 "wavelength": np.asarray([]),
429 "slit_position": np.asarray([])
430 }
431 for o, wmin, wmax in zip(orderNums, waveLengthMin, waveLengthMax):
433 wlArray = np.arange(
434 wmin, wmax, (wmax - wmin) / sampleCount)
435 myDict["wavelength"] = np.append(myDict["wavelength"], wlArray)
436 myDict["order"] = np.append(
437 myDict["order"], np.ones(len(wlArray)) * o)
438 myDict["slit_position"] = np.append(
439 myDict["slit_position"], np.zeros(len(wlArray)))
441 orderPixelTable = pd.DataFrame(myDict)
442 orderPixelTable = dispersion_map_to_pixel_arrays(
443 log=self.log,
444 dispersionMapPath=self.dispersion_map,
445 orderPixelTable=orderPixelTable
446 )
448 self.log.debug('completed the ``create_pixel_arrays`` method')
449 return orderPixelTable
451 def fit_1d_gaussian_to_slice(
452 self,
453 pixelPostion):
454 """*cut a slice from the pinhole flat along the cross-dispersion direction centred on pixel position, fit 1D gaussian and return the peak pixel position*
456 **Key Arguments:**
457 - ``pixelPostion`` -- the x,y pixel coordinate from orderPixelTable data-frame (series)
459 **Return:**
460 - ``pixelPostion`` -- now including gaussian fit peak xy position
461 """
462 self.log.debug('starting the ``fit_1d_gaussian_to_slice`` method')
464 # CLIP OUT A SLICE TO INSPECT CENTRED AT POSITION
465 halfSlice = self.sliceLength / 2
467 slice = cut_image_slice(log=self.log, frame=self.pinholeFlat,
468 width=1, length=self.sliceLength, x=pixelPostion["fit_x"], y=pixelPostion["fit_y"], median=True, plot=False)
470 if slice is None:
471 pixelPostion["cont_x"] = np.nan
472 pixelPostion["cont_y"] = np.nan
473 return pixelPostion
475 # CHECK THE SLICE POINTS IF NEEDED
476 if 1 == 0:
477 x = np.arange(0, len(slice))
478 plt.figure(figsize=(8, 5))
479 plt.plot(x, slice, 'ko')
480 plt.xlabel('Position')
481 plt.ylabel('Flux')
482 plt.show()
484 # EVALUATING THE MEAN AND STD-DEV FOR PEAK FINDING - REMOVES SLICE
485 # CONTAINING JUST NOISE
486 median_r = np.median(slice)
487 std_r = mad_std(slice)
489 if not median_r:
490 pixelPostion["cont_x"] = np.nan
491 pixelPostion["cont_y"] = np.nan
492 return pixelPostion
494 peaks, _ = find_peaks(slice, height=median_r +
495 self.peakSigmaLimit * std_r, width=1)
497 # CHECK PEAK HAS BEEN FOUND
498 if peaks is None or len(peaks) <= 0:
499 # CHECK THE SLICE POINTS IF NEEDED
500 if 1 == 0:
501 x = np.arange(0, len(slice))
502 plt.figure(figsize=(8, 5))
503 plt.plot(x, slice, 'ko')
504 plt.xlabel('Position')
505 plt.ylabel('Flux')
506 plt.show()
507 pixelPostion["cont_x"] = np.nan
508 pixelPostion["cont_y"] = np.nan
509 return pixelPostion
511 # FIT THE DATA USING A 1D GAUSSIAN - USING astropy.modeling
512 # CENTRE THE GAUSSIAN ON THE PEAK
513 g_init = models.Gaussian1D(
514 amplitude=1000., mean=peaks[0], stddev=1.)
515 # print(f"g_init: {g_init}")
516 fit_g = fitting.LevMarLSQFitter()
518 # NOW FIT
519 g = fit_g(g_init, np.arange(0, len(slice)), slice)
520 pixelPostion["cont_x"] = g.mean + \
521 max(0, int(pixelPostion["fit_x"] - halfSlice))
522 pixelPostion["cont_y"] = pixelPostion["fit_y"]
524 # PRINT A FEW PLOTS IF NEEDED - GUASSIAN FIT OVERLAYED
525 if 1 == 0 and random() < 0.02:
526 x = np.arange(0, len(slice))
527 plt.figure(figsize=(8, 5))
528 plt.plot(x, slice, 'ko')
529 plt.xlabel('Position')
530 plt.ylabel('Flux')
531 guassx = np.arange(0, max(x), 0.05)
532 plt.plot(guassx, g(guassx), label='Gaussian')
533 plt.show()
535 self.log.debug('completed the ``fit_1d_gaussian_to_slice`` method')
536 return pixelPostion
538 def plot_results(
539 self,
540 orderPixelTable,
541 orderPolyTable):
542 """*generate a plot of the polynomial fits and residuals*
544 **Key Arguments:**
545 - ``orderPixelTable`` -- the pixel table with residuals of fits
546 - ``orderPolyTable`` -- data-frame of order-location polynomial coeff
548 **Return:**
549 - ``filePath`` -- path to the plot pdf
550 """
551 self.log.debug('starting the ``plot_results`` method')
553 arm = self.arm
555 # a = plt.figure(figsize=(40, 15))
556 if arm == "UVB":
557 fig = plt.figure(figsize=(6, 13.5), constrained_layout=True)
558 else:
559 fig = plt.figure(figsize=(6, 11), constrained_layout=True)
560 gs = fig.add_gridspec(6, 4)
562 # CREATE THE GID OF AXES
563 toprow = fig.add_subplot(gs[0:2, :])
564 midrow = fig.add_subplot(gs[2:4, :])
565 bottomleft = fig.add_subplot(gs[4:, 0:2])
566 bottomright = fig.add_subplot(gs[4:, 2:])
568 # ROTATE THE IMAGE FOR BETTER LAYOUT
569 rotatedImg = np.rot90(self.pinholeFlat.data, 1)
570 toprow.imshow(rotatedImg, vmin=10, vmax=50, cmap='gray', alpha=0.5)
571 toprow.set_title(
572 "1D guassian peak positions (post-clipping)", fontsize=10)
573 x = np.ones(len(orderPixelTable.index)) * \
574 self.pinholeFlat.data.shape[1] - orderPixelTable['cont_x'].values
575 toprow.scatter(orderPixelTable[
576 'cont_y'].values, x, marker='x', c='red', s=4)
577 # toprow.set_yticklabels([])
578 # toprow.set_xticklabels([])
579 toprow.set_ylabel("x-axis", fontsize=8)
580 toprow.set_xlabel("y-axis", fontsize=8)
581 toprow.tick_params(axis='both', which='major', labelsize=9)
583 midrow.imshow(rotatedImg, vmin=10, vmax=50, cmap='gray', alpha=0.5)
584 midrow.set_title(
585 "order-location fit solutions", fontsize=10)
586 ylinelist = np.arange(0, self.pinholeFlat.data.shape[0], 3)
587 poly = chebyshev_xy_polynomial(
588 log=self.log, deg=self.polyDeg).poly
590 # ONLY DO THIS FOR SMALL DATAFRAMES - THIS IS AN ANTIPATTERN
591 for index, row in orderPolyTable.iterrows():
592 o = row["order"]
593 coeff = [float(v) for k, v in row.items() if "cent_" in k]
594 xfit = poly(ylinelist, *coeff)
595 xfit = np.ones(len(xfit)) * \
596 self.pinholeFlat.data.shape[1] - xfit
597 xfit, ylinelist = zip(
598 *[(x, y) for x, y in zip(xfit, ylinelist) if x > 0 and x < (self.pinholeFlat.data.shape[1]) - 10])
599 midrow.plot(ylinelist, xfit)
601 # xfit = np.ones(len(xfit)) * \
602 # self.pinholeFrame.data.shape[1] - xfit
603 # midrow.scatter(yfit, xfit, marker='x', c='blue', s=4)
604 # midrow.set_yticklabels([])
605 # midrow.set_xticklabels([])
606 midrow.set_ylabel("x-axis", fontsize=8)
607 midrow.set_xlabel("y-axis", fontsize=8)
608 midrow.tick_params(axis='both', which='major', labelsize=9)
610 # PLOT THE FINAL RESULTS:
611 plt.subplots_adjust(top=0.92)
612 bottomleft.scatter(orderPixelTable['cont_x'].values, orderPixelTable[
613 'x_fit_res'].values, alpha=0.2, s=1)
614 bottomleft.set_xlabel('x pixel position')
615 bottomleft.set_ylabel('x residual')
616 bottomleft.tick_params(axis='both', which='major', labelsize=9)
618 # PLOT THE FINAL RESULTS:
619 plt.subplots_adjust(top=0.92)
620 bottomright.scatter(orderPixelTable['cont_y'].values, orderPixelTable[
621 'x_fit_res'].values, alpha=0.2, s=1)
622 bottomright.set_xlabel('y pixel position')
623 bottomright.tick_params(axis='both', which='major', labelsize=9)
624 # bottomright.set_ylabel('x residual')
625 bottomright.set_yticklabels([])
627 mean_res = np.mean(np.abs(orderPixelTable['x_fit_res'].values))
628 std_res = np.std(np.abs(orderPixelTable['x_fit_res'].values))
630 subtitle = f"mean res: {mean_res:2.2f} pix, res stdev: {std_res:2.2f}"
631 fig.suptitle(f"traces of order-centre locations - pinhole flat-frame\n{subtitle}", fontsize=12)
633 # plt.show()
634 filename = filenamer(
635 log=self.log,
636 frame=self.pinholeFlat,
637 settings=self.settings
638 )
639 filename = filename.split("FLAT")[0] + "ORDER_CENTRES_residuals.pdf"
641 home = expanduser("~")
642 outDir = self.settings["intermediate-data-root"].replace("~", home)
643 filePath = f"{outDir}/{filename}"
644 plt.savefig(filePath)
646 self.log.debug('completed the ``plot_results`` method')
647 return filePath
649 # use the tab-trigger below for new method
650 # xt-class-method