Coverage for soxspipe/commonutils/create_dispersion_map.py : 13%

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*detect arc-lines on a pinhole frame to generate a dispersion solution*
6:Author:
7 Marco Landoni & David Young
9:Date Created:
10 September 1, 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 astropy.stats import sigma_clipped_stats
21from photutils import datasets
22from photutils import DAOStarFinder
23from scipy.optimize import curve_fit
24from fundamentals.renderer import list_of_dictionaries
25from os.path import expanduser
26import matplotlib.pyplot as plt
27from os.path import expanduser
28from astropy.stats import sigma_clip, mad_std
29import numpy as np
30import math
31from photutils.utils import NoDetectionsWarning
32import warnings
33from astropy.visualization import hist
34from soxspipe.commonutils.polynomials import chebyshev_order_wavelength_polynomials
35from soxspipe.commonutils.filenamer import filenamer
36from soxspipe.commonutils.dispersion_map_to_pixel_arrays import dispersion_map_to_pixel_arrays
37import pandas as pd
38from tabulate import tabulate
41class create_dispersion_map(object):
42 """
43 *detect arc-lines on a pinhole frame to generate a dispersion solution*
45 **Key Arguments:**
46 - ``log`` -- logger
47 - ``settings`` -- the settings dictionary
48 - ``pinholeFrame`` -- the calibrated pinhole frame (single or multi)
49 - ``firstGuessMap`` -- the first guess dispersion map from the `soxs_disp_solution` recipe (needed in `soxs_spat_solution` recipe). Default *False*.
51 **Usage:**
53 ```python
54 from soxspipe.commonutils import create_dispersion_map
55 mapPath = create_dispersion_map(
56 log=log,
57 settings=settings,
58 pinholeFrame=frame,
59 firstGuessMap=False
60 ).get()
61 ```
62 """
64 def __init__(
65 self,
66 log,
67 settings,
68 pinholeFrame,
69 firstGuessMap=False
70 ):
71 self.log = log
72 log.debug("instantiating a new 'create_dispersion_map' object")
73 self.settings = settings
74 self.pinholeFrame = pinholeFrame
75 self.firstGuessMap = firstGuessMap
77 # KEYWORD LOOKUP OBJECT - LOOKUP KEYWORD FROM DICTIONARY IN RESOURCES
78 # FOLDER
79 kw = keyword_lookup(
80 log=self.log,
81 settings=self.settings
82 ).get
83 self.kw = kw
84 self.arm = pinholeFrame.header[kw("SEQ_ARM")]
86 # DETECTOR PARAMETERS LOOKUP OBJECT
87 self.detectorParams = detector_lookup(
88 log=log,
89 settings=settings
90 ).get(self.arm)
92 warnings.simplefilter('ignore', NoDetectionsWarning)
94 return None
96 def get(self):
97 """
98 *generate the dispersion map*
100 **Return:**
101 - ``mapPath`` -- path to the file containing the coefficients of the x,y polynomials of the global dispersion map fit
102 """
103 self.log.debug('starting the ``get`` method')
105 # WHICH RECIPE ARE WE WORKING WITH?
106 if self.firstGuessMap:
107 recipe = "soxs-spatial-solution"
108 slit_deg = self.settings[recipe]["slit-deg"]
109 else:
110 recipe = "soxs-disp-solution"
111 slit_deg = 0
113 # READ PREDICTED LINE POSITIONS FROM FILE - RETURNED AS DATAFRAME
114 lineList = self.get_predicted_line_list()
116 # GET THE WINDOW SIZE FOR ATTEMPTING TO DETECT LINES ON FRAME
117 windowSize = self.settings[recipe]["pixel-window-size"]
118 self.windowHalf = int(windowSize / 2)
120 # DETECT THE LINES ON THE PINHILE FRAME AND
121 # ADD OBSERVED LINES TO DATAFRAME
122 lineList = lineList.apply(
123 self.detect_pinhole_arc_line, axis=1)
125 # DROP MISSING VALUES
126 lineList.dropna(axis='index', how='any', subset=[
127 'observed_x'], inplace=True)
129 order_deg = self.settings[recipe]["order-deg"]
130 wavelength_deg = self.settings[
131 recipe]["wavelength-deg"]
133 # ITERATIVELY FIT THE POLYNOMIAL SOLUTIONS TO THE DATA
134 popt_x, popt_y = self.fit_polynomials(
135 lineList=lineList,
136 wavelength_deg=wavelength_deg,
137 order_deg=order_deg,
138 slit_deg=slit_deg,
139 )
141 # WRITE THE MAP TO FILE
142 mapPath = self.write_map_to_file(
143 popt_x, popt_y, order_deg, wavelength_deg, slit_deg)
145 self.log.debug('completed the ``get`` method')
146 return mapPath
148 def get_predicted_line_list(
149 self):
150 """*lift the predicted line list from the static calibrations*
152 **Return:**
153 - ``predictedLines`` -- a dictionary of lists detailing Wavelength,Order,slit_index,slit_position,detector_x,detector_y
154 """
155 self.log.debug('starting the ``get_predicted_line_list`` method')
157 kw = self.kw
158 pinholeFrame = self.pinholeFrame
159 dp = self.detectorParams
161 # WHICH TYPE OF PINHOLE FRAME DO WE HAVE - SINGLE OR MULTI
162 if self.pinholeFrame.header[kw("DPR_TECH")] == "ECHELLE,PINHOLE":
163 frameTech = "single"
164 elif self.pinholeFrame.header[kw("DPR_TECH")] == "ECHELLE,MULTI-PINHOLE":
165 frameTech = "multi"
166 else:
167 raise TypeError(
168 "The input frame needs to be a calibrated single- or multi-pinhole arc lamp frame")
170 # FIND THE APPROPRIATE PREDICTED LINE-LIST
171 arm = self.arm
172 if kw('WIN_BINX') in pinholeFrame.header:
173 binx = int(self.pinholeFrame.header[kw('WIN_BINX')])
174 biny = int(self.pinholeFrame.header[kw('WIN_BINY')])
175 else:
176 binx = 1
177 biny = 1
179 # READ THE FILE
180 home = expanduser("~")
181 calibrationRootPath = self.settings[
182 "calibration-data-root"].replace("~", home)
183 predictedLinesFile = calibrationRootPath + "/" + dp["predicted pinhole lines"][frameTech][f"{binx}x{biny}"]
185 # READ CSV FILE TO PANDAS DATAFRAME
186 df = pd.read_csv(predictedLinesFile)
188 # WANT TO DETERMINE SYSTEMATIC SHIFT IF FIRST GUESS SOLUTION PRESENT
189 if self.firstGuessMap:
190 # ADD SOME EXTRA COLUMNS TO DATAFRAME
191 df['observed_x'] = np.nan
192 df['observed_y'] = np.nan
193 df['shift_x'] = np.nan
194 df['shift_y'] = np.nan
196 # FILTER THE PREDICTED LINES TO ONLY SLIT POSITION INCLUDED IN
197 # SINGLE PINHOLE FRAMES
198 slitIndex = int(dp["mid_slit_index"])
199 mask = (df['slit_index'] == slitIndex)
200 filteredDf = df.loc[mask]
202 # GROUP RESULTS BY ORDER
203 # GET UNIQUE ORDER AND SLIT INDEXES
204 uniqueOrders = filteredDf['Order'].unique()
205 uniqueSlits = df['slit_index'].unique()
206 dfGroups = filteredDf.groupby(['Order'])
208 # CREATE orderWavelengthDict FOR dispersion_map_to_pixel_arrays
209 # FUNCTION
210 orderWavelengthDict = {}
211 orderWavelengthDict = {o: dfGroups.get_group(
212 o)['Wavelength'].values for o in uniqueOrders}
214 # GET THE OBSERVED PIXELS VALUES
215 pixelArrays = dispersion_map_to_pixel_arrays(
216 log=self.log,
217 dispersionMapPath=self.firstGuessMap,
218 orderWavelengthDict=orderWavelengthDict
219 )
221 # ITERATE OVER EACH ORDER
222 for o in uniqueOrders:
223 thisGroup = dfGroups.get_group(o).copy()
224 # DETERMINE THE SHIFT IN SINGLE PINHOLE PREDICTED TO OBSERVED
225 # PIXELS
226 thisGroup.loc[:, ('observed_x')], thisGroup.loc[:, ('observed_y')] = zip(
227 *[(p[0], p[1]) for p in pixelArrays[o]])
228 mask = (df['slit_index'] == slitIndex) & (df['Order'] == o)
229 df.loc[mask, ('observed_x')], df.loc[mask, ('observed_y')] = zip(
230 *[(p[0], p[1]) for p in pixelArrays[o]])
231 thisGroup.loc[:, 'shift_xx'] = thisGroup[
232 'detector_x'].values - thisGroup['observed_x'].values
233 thisGroup.loc[:, 'shift_yy'] = thisGroup[
234 'detector_y'].values - thisGroup['observed_y'].values
235 thisGroup = thisGroup.loc[
236 :, ['Wavelength', 'Order', 'shift_xx', 'shift_yy']]
238 # MERGING SHIFTS INTO MAIN DATAFRAME
239 df = df.merge(thisGroup, on=[
240 'Wavelength', 'Order'], how='outer')
241 df.loc[df['shift_xx'].notnull(), ['shift_x', 'shift_y']] = df.loc[
242 df['shift_xx'].notnull(), ['shift_xx', 'shift_yy']].values
243 df.drop(columns=['shift_xx', 'shift_yy'], inplace=True)
245 # DROP ROWS WITH MISSING SHIFTS
246 df.dropna(axis='index', how='any', subset=[
247 'shift_x'], inplace=True)
249 # SHIFT DETECTOR LINE PIXEL POSITIONS BY SHIFTS
250 # UPDATE FILTERED VALUES
251 df.loc[:, 'detector_x'] -= df.loc[:, 'shift_x']
252 df.loc[:, 'detector_y'] -= df.loc[:, 'shift_y']
254 # DROP HELPER COLUMNS
255 df.drop(columns=['observed_x', 'observed_y',
256 'shift_x', 'shift_y'], inplace=True)
258 predictedLines = df
259 self.log.debug('completed the ``get_predicted_line_list`` method')
260 return predictedLines
262 def detect_pinhole_arc_line(
263 self,
264 predictedLine):
265 """*detect the observed position of an arc-line given the predicted pixel positions*
267 **Key Arguments:**
268 - ``predictedLine`` -- single predicted line coordinates from predicted line-list
270 **Return:**
271 - ``predictedLine`` -- the line with the observed pixel coordinates appended (if detected, otherwise nan)
272 """
273 self.log.debug('starting the ``detect_pinhole_arc_line`` method')
275 pinholeFrame = self.pinholeFrame
276 windowHalf = self.windowHalf
277 x = predictedLine['detector_x']
278 y = predictedLine['detector_y']
280 # CLIP A STAMP FROM IMAGE AROUNDS PREDICTED POSITION
281 xlow = int(np.max([x - windowHalf, 0]))
282 xup = int(np.min([x + windowHalf, pinholeFrame.shape[1]]))
283 ylow = int(np.max([y - windowHalf, 0]))
284 yup = int(np.min([y + windowHalf, pinholeFrame.shape[0]]))
285 stamp = pinholeFrame[ylow:yup, xlow:xup]
287 # USE DAOStarFinder TO FIND LINES WITH 2D GUASSIAN FITTING
288 mean, median, std = sigma_clipped_stats(stamp, sigma=3.0)
289 daofind = DAOStarFinder(
290 fwhm=2.0, threshold=5. * std, roundlo=-3.0, roundhi=3.0, sharplo=-3.0, sharphi=3.0)
291 sources = daofind(stamp - median)
293 # plt.clf()
294 # plt.imshow(stamp)
295 old_resid = windowHalf * 4
296 if sources:
297 # FIND SOURCE CLOSEST TO CENTRE
298 if len(sources) > 1:
299 for source in sources:
300 tmp_x = source['xcentroid']
301 tmp_y = source['ycentroid']
302 new_resid = ((windowHalf - tmp_x)**2 +
303 (windowHalf - tmp_y)**2)**0.5
304 if new_resid < old_resid:
305 observed_x = tmp_x + xlow
306 observed_y = tmp_y + ylow
307 old_resid = new_resid
308 else:
309 observed_x = sources[0]['xcentroid'] + xlow
310 observed_y = sources[0]['ycentroid'] + ylow
311 # plt.scatter(observed_x - xlow, observed_y -
312 # ylow, marker='x', s=30)
313 # plt.show()
314 else:
315 observed_x = np.nan
316 observed_y = np.nan
317 # plt.show()
319 predictedLine['observed_x'] = observed_x
320 predictedLine['observed_y'] = observed_y
322 self.log.debug('completed the ``detect_pinhole_arc_line`` method')
323 return predictedLine
325 def write_map_to_file(
326 self,
327 xcoeff,
328 ycoeff,
329 order_deg,
330 wavelength_deg,
331 slit_deg):
332 """*write out the fitted polynomial solution coefficients to file*
334 **Key Arguments:**
335 - ``xcoeff`` -- the x-coefficients
336 - ``ycoeff`` -- the y-coefficients
337 - ``order_deg`` -- degree of the order fitting
338 - ``wavelength_deg`` -- degree of wavelength fitting
339 - ``slit_deg`` -- degree of the slit fitting (False for single pinhole)
341 **Return:**
342 - ``disp_map_path`` -- path to the saved file
343 """
344 self.log.debug('starting the ``write_map_to_file`` method')
346 arm = self.arm
348 # SORT X COEFFICIENT OUTPUT TO WRITE TO FILE
349 coeff_dict_x = {}
350 coeff_dict_x["axis"] = "x"
351 coeff_dict_x["order-deg"] = order_deg
352 coeff_dict_x["wavelength-deg"] = wavelength_deg
353 coeff_dict_x["slit-deg"] = slit_deg
354 n_coeff = 0
355 for i in range(0, order_deg + 1):
356 for j in range(0, wavelength_deg + 1):
357 for k in range(0, slit_deg + 1):
358 coeff_dict_x[f'c{i}{j}{k}'] = xcoeff[n_coeff]
359 n_coeff += 1
361 # SORT Y COEFFICIENT OUTPUT TO WRITE TO FILE
362 coeff_dict_y = {}
363 coeff_dict_y["axis"] = "y"
364 coeff_dict_y["order-deg"] = order_deg
365 coeff_dict_y["wavelength-deg"] = wavelength_deg
366 coeff_dict_y["slit-deg"] = slit_deg
367 n_coeff = 0
368 for i in range(0, order_deg + 1):
369 for j in range(0, wavelength_deg + 1):
370 for k in range(0, slit_deg + 1):
371 coeff_dict_y[f'c{i}{j}{k}'] = ycoeff[n_coeff]
372 n_coeff += 1
374 # DETERMINE WHERE TO WRITE THE FILE
375 home = expanduser("~")
376 outDir = self.settings["intermediate-data-root"].replace("~", home)
378 filename = filenamer(
379 log=self.log,
380 frame=self.pinholeFrame,
381 settings=self.settings
382 )
383 filename = filename.split("ARC")[0] + "DISP_MAP.csv"
384 filePath = f"{outDir}/{filename}"
385 dataSet = list_of_dictionaries(
386 log=self.log,
387 listOfDictionaries=[coeff_dict_x, coeff_dict_y]
388 )
389 csvData = dataSet.csv(filepath=filePath)
391 self.log.debug('completed the ``write_map_to_file`` method')
392 return filePath
394 def calculate_residuals(
395 self,
396 lineList,
397 xcoeff,
398 ycoeff,
399 order_deg,
400 wavelength_deg,
401 slit_deg):
402 """*calculate residuals of the polynomial fits against the observed line positions*
404 **Key Arguments:**
406 - ``lineList`` -- the predicted line list as a data frame
407 - ``xcoeff`` -- the x-coefficients
408 - ``ycoeff`` -- the y-coefficients
409 - ``order_deg`` -- degree of the order fitting
410 - ``wavelength_deg`` -- degree of wavelength fitting
411 - ``slit_deg`` -- degree of the slit fitting (False for single pinhole)
413 **Return:**
414 - ``residuals`` -- combined x-y residuals
415 - ``mean`` -- the mean of the combine residuals
416 - ``std`` -- the stdev of the combine residuals
417 - ``median`` -- the median of the combine residuals
418 """
419 self.log.debug('starting the ``calculate_residuals`` method')
421 arm = self.arm
423 # POLY FUNCTION NEEDS A DATAFRAME AS INPUT
424 poly = chebyshev_order_wavelength_polynomials(
425 log=self.log, order_deg=order_deg, wavelength_deg=wavelength_deg, slit_deg=slit_deg).poly
427 # CALCULATE X & Y RESIDUALS BETWEEN OBSERVED LINE POSITIONS AND POLY
428 # FITTED POSITIONS
429 lineList["fit_x"] = poly(lineList, *xcoeff)
430 lineList["fit_y"] = poly(lineList, *ycoeff)
431 lineList["residuals_x"] = lineList[
432 "fit_x"] - lineList["observed_x"]
433 lineList["residuals_y"] = lineList[
434 "fit_y"] - lineList["observed_y"]
436 # CALCULATE COMBINED RESIDUALS AND STATS
437 lineList["residuals_xy"] = np.sqrt(np.square(
438 lineList["residuals_x"]) + np.square(lineList["residuals_y"]))
439 combined_res_mean = np.mean(lineList["residuals_xy"])
440 combined_res_std = np.std(lineList["residuals_xy"])
441 combined_res_median = np.median(lineList["residuals_xy"])
443 self.log.debug('completed the ``calculate_residuals`` method')
444 return combined_res_mean, combined_res_std, combined_res_median, lineList
446 def fit_polynomials(
447 self,
448 lineList,
449 wavelength_deg,
450 order_deg,
451 slit_deg):
452 """*iteratively fit the dispersion map polynomials to the data, clipping residuals with each iteration*
454 **Key Arguments:**
455 - ``lineList`` -- data frame containing order, wavelengths, slit positions and observed pixel positions
456 - ``wavelength_deg`` -- degree of wavelength fitting
457 - ``order_deg`` -- degree of the order fitting
458 - ``slit_deg`` -- degree of the slit fitting (0 for single pinhole)
460 **Return:**
461 - ``xcoeff`` -- the x-coefficients post clipping
462 - ``ycoeff`` -- the y-coefficients post clipping
463 """
464 self.log.debug('starting the ``fit_polynomials`` method')
466 arm = self.arm
468 if self.firstGuessMap:
469 recipe = "soxs-spatial-solution"
470 else:
471 recipe = "soxs-disp-solution"
473 clippedCount = 1
475 poly = chebyshev_order_wavelength_polynomials(
476 log=self.log, order_deg=order_deg, wavelength_deg=wavelength_deg, slit_deg=slit_deg).poly
478 clippingSigma = self.settings[
479 recipe]["poly-fitting-residual-clipping-sigma"]
480 clippingIterationLimit = self.settings[
481 recipe]["clipping-iteration-limit"]
483 iteration = 0
484 while clippedCount > 0 and iteration < clippingIterationLimit:
485 iteration += 1
486 observed_x = lineList["observed_x"].to_numpy()
487 observed_y = lineList["observed_y"].to_numpy()
488 # USE LEAST-SQUARED CURVE FIT TO FIT CHEBY POLYS
489 # FIRST X
490 coeff = np.ones((order_deg + 1) *
491 (wavelength_deg + 1) * (slit_deg + 1))
492 self.log.info("""curvefit x""" % locals())
493 xcoeff, pcov_x = curve_fit(
494 poly, xdata=lineList, ydata=observed_x, p0=coeff)
496 # NOW Y
497 self.log.info("""curvefit y""" % locals())
498 ycoeff, pcov_y = curve_fit(
499 poly, xdata=lineList, ydata=observed_y, p0=coeff)
501 self.log.info("""calculate_residuals""" % locals())
502 mean_res, std_res, median_res, lineList = self.calculate_residuals(
503 lineList=lineList,
504 xcoeff=xcoeff,
505 ycoeff=ycoeff,
506 order_deg=order_deg,
507 wavelength_deg=wavelength_deg,
508 slit_deg=slit_deg)
510 # SIGMA-CLIP THE DATA
511 self.log.info("""sigma_clip""" % locals())
512 masked_residuals = sigma_clip(
513 lineList["residuals_xy"], sigma_lower=clippingSigma, sigma_upper=clippingSigma, maxiters=1, cenfunc='median', stdfunc=mad_std)
514 lineList["residuals_masked"] = masked_residuals.mask
515 # RETURN BREAKDOWN OF COLUMN VALUE COUNT
516 valCounts = lineList[
517 'residuals_masked'].value_counts(normalize=False)
518 if True in valCounts:
519 clippedCount = valCounts[True]
520 else:
521 clippedCount = 0
522 print(f'{clippedCount} arc lines where clipped in this iteration of fitting a global dispersion map')
524 # REMOVE FILTERED ROWS FROM DATA FRAME
525 mask = (lineList['residuals_masked'] == True)
526 lineList.drop(index=lineList[mask].index, inplace=True)
528 # a = plt.figure(figsize=(40, 15))
529 if arm == "UVB":
530 fig = plt.figure(figsize=(6, 13.5), constrained_layout=True)
531 else:
532 fig = plt.figure(figsize=(6, 11), constrained_layout=True)
533 gs = fig.add_gridspec(6, 4)
535 # CREATE THE GID OF AXES
536 toprow = fig.add_subplot(gs[0:2, :])
537 midrow = fig.add_subplot(gs[2:4, :])
538 bottomleft = fig.add_subplot(gs[4:, 0:2])
539 bottomright = fig.add_subplot(gs[4:, 2:])
541 # ROTATE THE IMAGE FOR BETTER LAYOUT
542 rotatedImg = np.rot90(self.pinholeFrame.data, 1)
543 toprow.imshow(rotatedImg, vmin=10, vmax=50, cmap='gray', alpha=0.5)
544 toprow.set_title(
545 "observed arc-line positions (post-clipping)", fontsize=10)
547 x = np.ones(lineList.shape[0]) * \
548 self.pinholeFrame.data.shape[1] - lineList["observed_x"]
549 toprow.scatter(lineList["observed_y"],
550 x, marker='x', c='red', s=4)
551 # toprow.set_yticklabels([])
552 # toprow.set_xticklabels([])
553 toprow.set_ylabel("x-axis", fontsize=8)
554 toprow.set_xlabel("y-axis", fontsize=8)
555 toprow.tick_params(axis='both', which='major', labelsize=9)
557 midrow.imshow(rotatedImg, vmin=10, vmax=50, cmap='gray', alpha=0.5)
558 midrow.set_title(
559 "global dispersion solution", fontsize=10)
560 xfit = np.ones(lineList.shape[0]) * \
561 self.pinholeFrame.data.shape[1] - lineList["fit_x"]
562 midrow.scatter(lineList["fit_y"],
563 xfit, marker='x', c='blue', s=4)
564 # midrow.set_yticklabels([])
565 # midrow.set_xticklabels([])
566 midrow.set_ylabel("x-axis", fontsize=8)
567 midrow.set_xlabel("y-axis", fontsize=8)
568 midrow.tick_params(axis='both', which='major', labelsize=9)
570 # PLOT THE FINAL RESULTS:
571 plt.subplots_adjust(top=0.92)
572 bottomleft.scatter(lineList["residuals_x"], lineList[
573 "residuals_y"], alpha=0.4)
574 bottomleft.set_xlabel('x residual')
575 bottomleft.set_ylabel('y residual')
576 bottomleft.tick_params(axis='both', which='major', labelsize=9)
578 hist(lineList["residuals_xy"], bins='scott', ax=bottomright, histtype='stepfilled',
579 alpha=0.7, density=True)
580 bottomright.set_xlabel('xy residual')
581 bottomright.tick_params(axis='both', which='major', labelsize=9)
582 subtitle = f"mean res: {mean_res:2.2f} pix, res stdev: {std_res:2.2f}"
583 fig.suptitle(f"residuals of global dispersion solution fitting - single pinhole\n{subtitle}", fontsize=12)
585 # GET FILENAME FOR THE RESIDUAL PLOT
586 filename = filenamer(
587 log=self.log,
588 frame=self.pinholeFrame,
589 settings=self.settings
590 )
591 filename = filename.split("ARC")[0] + "DISP_MAP_RESIDUALS.pdf"
593 # plt.show()
594 home = expanduser("~")
595 outDir = self.settings["intermediate-data-root"].replace("~", home)
596 filePath = f"{outDir}/{filename}"
597 plt.savefig(filePath)
599 print(f'\nThe dispersion maps fitted against the observed arc-line positions with a mean residual of {mean_res:2.2f} pixels (stdev = {std_res:2.2f} pixles)')
601 self.log.debug('completed the ``fit_polynomials`` method')
602 return xcoeff, ycoeff
604 # use the tab-trigger below for new method
605 # xt-class-method