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*find and fit the continuum in a pinhole flat frame with low-order polynomials. These polynominals are the central loctions of the orders* 

5 

6:Author: 

7 David Young & Marco Landoni 

8 

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 

37 

38 

39class _base_detect(object): 

40 

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* 

48 

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 

54 

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

60 

61 arm = self.arm 

62 

63 clippedCount = 1 

64 

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

71 

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] 

77 

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 

94 

95 res, res_mean, res_std, res_median, xfit = self.calculate_residuals( 

96 orderPixelTable=pixelListFiltered, 

97 coeff=coeff, 

98 xCol=xCol, 

99 yCol=yCol) 

100 

101 pixelList.loc[mask, "x_fit_res"] = res 

102 pixelList.loc[mask, "x_fit"] = xfit 

103 

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 

108 

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) 

114 

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

117 

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

119 return coeff, pixelList 

120 

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* 

128 

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 

134 

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

143 

144 arm = self.arm 

145 

146 poly = chebyshev_xy_polynomial( 

147 log=self.log, deg=self.polyDeg).poly 

148 

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 

154 

155 # CALCULATE COMBINED RESIDUALS AND STATS 

156 res_mean = np.mean(res) 

157 res_std = np.std(res) 

158 res_median = np.median(res) 

159 

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

161 return res, res_mean, res_std, res_median, xfit 

162 

163 def write_order_table_to_file( 

164 self, 

165 frame, 

166 orderPolyTable): 

167 """*write out the fitted polynomial solution coefficients to file* 

168 

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) 

172 

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

177 

178 arm = self.arm 

179 

180 # DETERMINE WHERE TO WRITE THE FILE 

181 home = expanduser("~") 

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

183 

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" 

191 

192 order_table_path = f"{outDir}/{filename}" 

193 orderPolyTable.to_csv(order_table_path, index=False) 

194 

195 self.log.debug('completed the ``write_order_table_to_file`` method') 

196 return order_table_path 

197 

198 

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* 

202 

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 

211 

212 **Usage:** 

213 

214 To use the ``detect_continuum`` object, use the following: 

215 

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

228 

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 

250 

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

259 

260 # DETECTOR PARAMETERS LOOKUP OBJECT 

261 self.detectorParams = detector_lookup( 

262 log=log, 

263 settings=settings 

264 ).get(self.arm) 

265 

266 # DEG OF THE POLYNOMIALS TO FIT THE ORDER CENTRE LOCATIONS 

267 self.polyDeg = self.recipeSettings["poly-deg"] 

268 

269 return None 

270 

271 def get(self): 

272 """ 

273 *return the order centre table filepath* 

274 

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

279 

280 arm = self.arm 

281 

282 # READ THE SPECTRAL FORMAT TABLE TO DETERMINE THE LIMITS OF THE TRACES 

283 orderNums, waveLengthMin, waveLengthMax = self.read_spectral_format() 

284 

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

294 

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 

298 

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

310 

311 # GET UNIQUE VALUES IN COLUMN 

312 uniqueOrders = orderPixelTable['order'].unique() 

313 

314 orderLocations = {} 

315 orderPixelTable['x_fit'] = np.nan 

316 orderPixelTable['x_fit_res'] = np.nan 

317 

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 

327 

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 

340 

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) 

346 

347 # HERE IS THE LINE LIST IF NEEDED FOR QC 

348 orderPixelTable.drop(columns=['mask'], inplace=True) 

349 

350 plotPath = self.plot_results( 

351 orderPixelTable=orderPixelTable, 

352 orderPolyTable=orderPolyTable 

353 ) 

354 

355 mean_res = np.mean(np.abs(orderPixelTable['x_fit_res'].values)) 

356 std_res = np.std(np.abs(orderPixelTable['x_fit_res'].values)) 

357 

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

359 

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) 

363 

364 print(f'\nFind results of the order centre fitting here: {plotPath}') 

365 

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

367 return order_table_path 

368 

369 def read_spectral_format( 

370 self): 

371 """*read the spectral format table to get some key parameters* 

372 

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

379 

380 kw = self.kw 

381 pinholeFlat = self.pinholeFlat 

382 dp = self.detectorParams 

383 

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

390 

391 # READ CSV FILE TO PANDAS DATAFRAME 

392 specFormatTable = pd.read_csv( 

393 spectralFormatFile, index_col=False, na_values=['NA', 'MISSING']) 

394 # # DATAFRAME INFO 

395 

396 # EXTRACT REQUIRED PARAMETERS 

397 orderNums = specFormatTable["ORDER"].values 

398 waveLengthMin = specFormatTable["WLMINFUL"].values 

399 waveLengthMax = specFormatTable["WLMAXFUL"].values 

400 

401 self.log.debug('completed the ``read_spectral_format`` method') 

402 return orderNums, waveLengthMin, waveLengthMax 

403 

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* 

410 

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 

415 

416 **Return:** 

417 - ``orderPixelTable`` -- a data-frame containing lines and associated pixel locations 

418 """ 

419 self.log.debug('starting the ``create_pixel_arrays`` method') 

420 

421 # READ ORDER SAMPLING RESOLUTION FROM SETTINGS 

422 sampleCount = self.settings[ 

423 "soxs-order-centre"]["order-sample-count"] 

424 

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

432 

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

440 

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 ) 

447 

448 self.log.debug('completed the ``create_pixel_arrays`` method') 

449 return orderPixelTable 

450 

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* 

455 

456 **Key Arguments:** 

457 - ``pixelPostion`` -- the x,y pixel coordinate from orderPixelTable data-frame (series) 

458 

459 **Return:** 

460 - ``pixelPostion`` -- now including gaussian fit peak xy position 

461 """ 

462 self.log.debug('starting the ``fit_1d_gaussian_to_slice`` method') 

463 

464 # CLIP OUT A SLICE TO INSPECT CENTRED AT POSITION 

465 halfSlice = self.sliceLength / 2 

466 

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) 

469 

470 if slice is None: 

471 pixelPostion["cont_x"] = np.nan 

472 pixelPostion["cont_y"] = np.nan 

473 return pixelPostion 

474 

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

483 

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) 

488 

489 if not median_r: 

490 pixelPostion["cont_x"] = np.nan 

491 pixelPostion["cont_y"] = np.nan 

492 return pixelPostion 

493 

494 peaks, _ = find_peaks(slice, height=median_r + 

495 self.peakSigmaLimit * std_r, width=1) 

496 

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 

510 

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

517 

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

523 

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

534 

535 self.log.debug('completed the ``fit_1d_gaussian_to_slice`` method') 

536 return pixelPostion 

537 

538 def plot_results( 

539 self, 

540 orderPixelTable, 

541 orderPolyTable): 

542 """*generate a plot of the polynomial fits and residuals* 

543 

544 **Key Arguments:** 

545 - ``orderPixelTable`` -- the pixel table with residuals of fits 

546 - ``orderPolyTable`` -- data-frame of order-location polynomial coeff 

547 

548 **Return:** 

549 - ``filePath`` -- path to the plot pdf 

550 """ 

551 self.log.debug('starting the ``plot_results`` method') 

552 

553 arm = self.arm 

554 

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) 

561 

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

567 

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) 

582 

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 

589 

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) 

600 

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) 

609 

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) 

617 

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

626 

627 mean_res = np.mean(np.abs(orderPixelTable['x_fit_res'].values)) 

628 std_res = np.std(np.abs(orderPixelTable['x_fit_res'].values)) 

629 

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) 

632 

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" 

640 

641 home = expanduser("~") 

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

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

644 plt.savefig(filePath) 

645 

646 self.log.debug('completed the ``plot_results`` method') 

647 return filePath 

648 

649 # use the tab-trigger below for new method 

650 # xt-class-method