Hide keyboard shortcuts

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

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 

39 

40 

41class 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