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

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

## 265 statements 35 run 230 missing 0 excluded

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*

5

6:Author:

7 Marco Landoni & David Young

8

9:Date Created:

10 September 1, 2020

11"""

12################# GLOBAL IMPORTS ####################

13 from builtins import object

14 import sys

15 import os

16 os.environ['TERM'] = 'vt100'

17 from fundamentals import tools

18 from soxspipe.commonutils import keyword_lookup

19 from soxspipe.commonutils import detector_lookup

20 from astropy.stats import sigma_clipped_stats

21 from photutils import datasets

22 from photutils import DAOStarFinder

23 from scipy.optimize import curve_fit

24 from fundamentals.renderer import list_of_dictionaries

25 from os.path import expanduser

26 import matplotlib.pyplot as plt

27 from os.path import expanduser

28 from astropy.stats import sigma_clip, mad_std

29 import numpy as np

30 import math

31 from photutils.utils import NoDetectionsWarning

32 import warnings

33 from astropy.visualization import hist

34 from soxspipe.commonutils.polynomials import chebyshev_order_wavelength_polynomials

35 from soxspipe.commonutils.filenamer import filenamer

36 from soxspipe.commonutils.dispersion_map_to_pixel_arrays import dispersion_map_to_pixel_arrays

37 import pandas as pd

38 from tabulate import tabulate

39

40

41 class create_dispersion_map(object):

42 """

43 *detect arc-lines on a pinhole frame to generate a dispersion solution*

44

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*.

50

51 **Usage:**

52

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 """

63

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

76

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")]

85

86 # DETECTOR PARAMETERS LOOKUP OBJECT

87 self.detectorParams = detector_lookup(

88 log=log,

89 settings=settings

90 ).get(self.arm)

91

92 warnings.simplefilter('ignore', NoDetectionsWarning)

93

94 return None

95

96 def get(self):

97 """

98 *generate the dispersion map*

99

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')

104

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

112

113 # READ PREDICTED LINE POSITIONS FROM FILE - RETURNED AS DATAFRAME

114 lineList = self.get_predicted_line_list()

115

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)

119

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)

124

125 # DROP MISSING VALUES

126 lineList.dropna(axis='index', how='any', subset=[

127 'observed_x'], inplace=True)

128

129 order_deg = self.settings[recipe]["order-deg"]

130 wavelength_deg = self.settings[

131 recipe]["wavelength-deg"]

132

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 )

140

141 # WRITE THE MAP TO FILE

142 mapPath = self.write_map_to_file(

143 popt_x, popt_y, order_deg, wavelength_deg, slit_deg)

144

145 self.log.debug('completed the ``get`` method')

146 return mapPath

147

148 def get_predicted_line_list(

149 self):

150 """*lift the predicted line list from the static calibrations*

151

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')

156

157 kw = self.kw

158 pinholeFrame = self.pinholeFrame

159 dp = self.detectorParams

160

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")

169

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

178

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}"]

184

185 # READ CSV FILE TO PANDAS DATAFRAME

186 df = pd.read_csv(predictedLinesFile)

187

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

195

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]

201

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'])

207

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}

213

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 )

220

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']]

237

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)

244

245 # DROP ROWS WITH MISSING SHIFTS

246 df.dropna(axis='index', how='any', subset=[

247 'shift_x'], inplace=True)

248

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']

253

254 # DROP HELPER COLUMNS

255 df.drop(columns=['observed_x', 'observed_y',

256 'shift_x', 'shift_y'], inplace=True)

257

258 predictedLines = df

259 self.log.debug('completed the ``get_predicted_line_list`` method')

260 return predictedLines

261

262 def detect_pinhole_arc_line(

263 self,

264 predictedLine):

265 """*detect the observed position of an arc-line given the predicted pixel positions*

266

267 **Key Arguments:**

268 - ``predictedLine`` -- single predicted line coordinates from predicted line-list

269

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')

274

275 pinholeFrame = self.pinholeFrame

276 windowHalf = self.windowHalf

277 x = predictedLine['detector_x']

278 y = predictedLine['detector_y']

279

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]

286

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)

292

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()

318

319 predictedLine['observed_x'] = observed_x

320 predictedLine['observed_y'] = observed_y

321

322 self.log.debug('completed the ``detect_pinhole_arc_line`` method')

323 return predictedLine

324

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*

333

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)

340

341 **Return:**

342 - ``disp_map_path`` -- path to the saved file

343 """

344 self.log.debug('starting the ``write_map_to_file`` method')

345

346 arm = self.arm

347

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

360

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

373

374 # DETERMINE WHERE TO WRITE THE FILE

375 home = expanduser("~")

376 outDir = self.settings["intermediate-data-root"].replace("~", home)

377

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)

390

391 self.log.debug('completed the ``write_map_to_file`` method')

392 return filePath

393

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*

403

404 **Key Arguments:**

405

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)

412

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')

420

421 arm = self.arm

422

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

426

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"]

435

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"])

442

443 self.log.debug('completed the ``calculate_residuals`` method')

444 return combined_res_mean, combined_res_std, combined_res_median, lineList

445

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*

453

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)

459

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')

465

466 arm = self.arm

467

468 if self.firstGuessMap:

469 recipe = "soxs-spatial-solution"

470 else:

471 recipe = "soxs-disp-solution"

472

473 clippedCount = 1

474

475 poly = chebyshev_order_wavelength_polynomials(

476 log=self.log, order_deg=order_deg, wavelength_deg=wavelength_deg, slit_deg=slit_deg).poly

477

478 clippingSigma = self.settings[

479 recipe]["poly-fitting-residual-clipping-sigma"]

480 clippingIterationLimit = self.settings[

481 recipe]["clipping-iteration-limit"]

482

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)

495

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)

500

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)

509

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')

523

524 # REMOVE FILTERED ROWS FROM DATA FRAME

525 mask = (lineList['residuals_masked'] == True)

526 lineList.drop(index=lineList[mask].index, inplace=True)

527

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)

534

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:])

540

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)

546

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)

556

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)

569

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)

577

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)

584

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"

592

593 # plt.show()

594 home = expanduser("~")

595 outDir = self.settings["intermediate-data-root"].replace("~", home)

596 filePath = f"{outDir}/{filename}"

597 plt.savefig(filePath)

598

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)')

600

601 self.log.debug('completed the ``fit_polynomials`` method')

602 return xcoeff, ycoeff

603

604 # use the tab-trigger below for new method

605 # xt-class-method