Coverage for python/lsst/analysis/tools/actions/plot/colorColorFitPlot.py: 11%

237 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-04 03:35 -0700

1# This file is part of analysis_tools. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ("ColorColorFitPlot",) 

25 

26from typing import Mapping, cast 

27 

28import matplotlib.patheffects as pathEffects 

29import matplotlib.pyplot as plt 

30import numpy as np 

31import scipy.stats 

32from lsst.pex.config import Field, ListField, RangeField 

33from matplotlib.figure import Figure 

34from matplotlib.patches import Rectangle 

35from scipy.ndimage import median_filter 

36 

37from ...interfaces import KeyedData, KeyedDataSchema, PlotAction, Scalar, Vector 

38from ...math import nanMean, nanMedian, nanSigmaMad 

39from ..keyedData.stellarLocusFit import perpDistance 

40from .plotUtils import addPlotInfo, mkColormap 

41 

42 

43class ColorColorFitPlot(PlotAction): 

44 """Make a color-color plot and overplot a prefited line to the fit region. 

45 

46 This is mostly used for the stellar locus plots and also includes panels 

47 that illustrate the goodness of the given fit. 

48 """ 

49 

50 xAxisLabel = Field[str](doc="Label to use for the x axis", optional=False) 

51 yAxisLabel = Field[str](doc="Label to use for the y axis", optional=False) 

52 magLabel = Field[str](doc="Label to use for the magnitudes used to color code by", optional=False) 

53 

54 plotTypes = ListField[str]( 

55 doc="Selection of types of objects to plot. Can take any combination of" 

56 " stars, galaxies, unknown, mag, any.", 

57 default=["stars"], 

58 ) 

59 

60 plotName = Field[str](doc="The name for the plot.", optional=False) 

61 minPointsForFit = RangeField[int]( 

62 doc="Minimum number of valid objects to bother attempting a fit.", 

63 default=5, 

64 min=1, 

65 deprecated="This field is no longer used. The value should go as an " 

66 "entry to the paramsDict keyed as minObjectForFit. Will be removed " 

67 "after v27.", 

68 ) 

69 

70 xLims = ListField[float]( 

71 doc="Minimum and maximum x-axis limit to force (provided as a list of [xMin, xMax]). " 

72 "If `None`, limits will be computed and set based on the data.", 

73 dtype=float, 

74 default=None, 

75 optional=True, 

76 ) 

77 

78 yLims = ListField[float]( 

79 doc="Minimum and maximum y-axis limit to force (provided as a list of [yMin, yMax]). " 

80 "If `None`, limits will be computed and set based on the data.", 

81 dtype=float, 

82 default=None, 

83 optional=True, 

84 ) 

85 

86 doPlotRedBlueHists = Field[bool]( 

87 doc="Plot distance from fit histograms separated into blue and red star subsamples?", 

88 default=False, 

89 optional=True, 

90 ) 

91 

92 doPlotDistVsColor = Field[bool]( 

93 doc="Plot distance from fit as a function of color in lower right panel?", 

94 default=True, 

95 optional=True, 

96 ) 

97 

98 def getInputSchema(self, **kwargs) -> KeyedDataSchema: 

99 base: list[tuple[str, type[Vector] | type[Scalar]]] = [] 

100 base.append(("x", Vector)) 

101 base.append(("y", Vector)) 

102 base.append(("mag", Vector)) 

103 base.append(("approxMagDepth", Scalar)) 

104 base.append((f"{self.plotName}_sigmaMAD", Scalar)) 

105 base.append((f"{self.plotName}_median", Scalar)) 

106 base.append(("mODR", Scalar)) 

107 base.append(("bODR", Scalar)) 

108 base.append(("bPerpMin", Scalar)) 

109 base.append(("bPerpMax", Scalar)) 

110 base.append(("mPerp", Scalar)) 

111 

112 return base 

113 

114 def __call__(self, data: KeyedData, **kwargs) -> Mapping[str, Figure] | Figure: 

115 self._validateInput(data, **kwargs) 

116 return self.makePlot(data, **kwargs) 

117 

118 def _validateInput(self, data: KeyedData, **kwargs) -> None: 

119 """NOTE currently can only check that something is not a scalar, not 

120 check that data is consistent with Vector 

121 """ 

122 needed = self.getInputSchema(**kwargs) 

123 if remainder := {key.format(**kwargs) for key, _ in needed} - { 

124 key.format(**kwargs) for key in data.keys() 

125 }: 

126 raise ValueError(f"Task needs keys {remainder} but they were not in input") 

127 for name, typ in needed: 

128 isScalar = issubclass((colType := type(data[name.format(**kwargs)])), Scalar) 

129 if isScalar and typ != Scalar: 

130 raise ValueError(f"Data keyed by {name} has type {colType} but action requires type {typ}") 

131 

132 def makePlot( 

133 self, 

134 data: KeyedData, 

135 plotInfo: Mapping[str, str], 

136 **kwargs, 

137 ) -> Figure: 

138 """Make stellar locus plots using pre fitted values. 

139 

140 Parameters 

141 ---------- 

142 data : `KeyedData` 

143 The data to plot the points from, for more information 

144 please see the notes section. 

145 plotInfo : `dict` 

146 A dictionary of information about the data being plotted 

147 with keys: 

148 

149 * ``"run"`` 

150 The output run for the plots (`str`). 

151 * ``"skymap"`` 

152 The type of skymap used for the data (`str`). 

153 * ``"filter"`` 

154 The filter used for this data (`str`). 

155 * ``"tract"`` 

156 The tract that the data comes from (`str`). 

157 

158 Returns 

159 ------- 

160 fig : `matplotlib.figure.Figure` 

161 The resulting figure. 

162 

163 Notes 

164 ----- 

165 The axis labels are given by `self.xAxisLabel` and `self.yAxisLabel`. 

166 The perpendicular distance of the points to the fit line is given in a 

167 histogram in the second panel. 

168 

169 For the code to work it expects various quantities to be present in 

170 the `data` that it is given. 

171 

172 The quantities that are expected to be present are: 

173 

174 * Statistics that are shown on the plot or used by the plotting code: 

175 * ``approxMagDepth`` 

176 The approximate magnitude corresponding to the SN cut used. 

177 * ``f"{self.plotName}_sigmaMAD"`` 

178 The sigma mad of the distances to the line fit. 

179 * ``f"{self.plotName or ''}_median"`` 

180 The median of the distances to the line fit. 

181 

182 * Parameters from the fitting code that are illustrated on the plot: 

183 * ``"bFixed"`` 

184 The fixed intercept to fall back on. 

185 * ``"mFixed"`` 

186 The fixed gradient to fall back on. 

187 * ``"bODR"`` 

188 The intercept calculated by the final orthogonal distance 

189 regression fitting. 

190 * ``"mODR"`` 

191 The gradient calculated by the final orthogonal distance 

192 regression fitting. 

193 * ``"xMin`"`` 

194 The x minimum of the box used in the fit. 

195 * ``"xMax"`` 

196 The x maximum of the box used in the fit. 

197 * ``"yMin"`` 

198 The y minimum of the box used in the fit. 

199 * ``"yMax"`` 

200 The y maximum of the box used in the fit. 

201 * ``"mPerp"`` 

202 The gradient of the line perpendicular to the line from 

203 the second ODR fit. 

204 * ``"bPerpMin"`` 

205 The intercept of the perpendicular line that goes through 

206 xMin. 

207 * ``"bPerpMax"`` 

208 The intercept of the perpendicular line that goes through 

209 xMax. 

210 * ``"goodPoints"`` 

211 The points that passed the initial set of cuts (typically 

212 in fluxType S/N, extendedness, magnitude, and isfinite). 

213 * ``"fitPoints"`` 

214 The points use in the final fit. 

215 

216 * The main inputs to plot: 

217 x, y, mag 

218 

219 Examples 

220 -------- 

221 An example of the plot produced from this code is here: 

222 

223 .. image:: /_static/analysis_tools/stellarLocusExample.png 

224 

225 For a detailed example of how to make a plot from the command line 

226 please see the 

227 :ref:`getting started guide<analysis-tools-getting-started>`. 

228 """ 

229 paramDict = data.pop("paramDict") 

230 # Points to use for the fit. 

231 fitPoints = data.pop("fitPoints") 

232 # Points with finite values for x, y, and mag. 

233 goodPoints = data.pop("goodPoints") 

234 

235 # TODO: Make a no data fig function and use here. 

236 if sum(fitPoints) < paramDict["minObjectForFit"]: 

237 fig = plt.figure(dpi=120) 

238 ax = fig.add_axes([0.12, 0.25, 0.43, 0.62]) 

239 ax.tick_params(labelsize=7) 

240 noDataText = ( 

241 "Number of objects after cuts ({})\nis less than the minimum required\nby " 

242 "paramDict[minObjectForFit] ({})".format(sum(fitPoints), int(paramDict["minObjectForFit"])) 

243 ) 

244 plt.text(0.5, 0.5, noDataText, ha="center", va="center", fontsize=8) 

245 fig = addPlotInfo(plt.gcf(), plotInfo) 

246 return fig 

247 

248 # Define new colormaps. 

249 newBlues = mkColormap(["darkblue", "paleturquoise"]) 

250 newGrays = mkColormap(["lightslategray", "white"]) 

251 

252 # Make a figure with three panels. 

253 fig = plt.figure(dpi=300) 

254 ax = fig.add_axes([0.12, 0.25, 0.43, 0.62]) 

255 if self.doPlotDistVsColor: 

256 axLowerRight = fig.add_axes([0.65, 0.11, 0.26, 0.34]) 

257 else: 

258 axLowerRight = fig.add_axes([0.65, 0.11, 0.3, 0.34]) 

259 axHist = fig.add_axes([0.65, 0.55, 0.3, 0.32]) 

260 

261 xs = cast(Vector, data["x"]) 

262 ys = cast(Vector, data["y"]) 

263 mags = cast(Vector, data["mag"]) 

264 

265 # Plot the initial fit box. 

266 (initialBox,) = ax.plot( 

267 [paramDict["xMin"], paramDict["xMax"], paramDict["xMax"], paramDict["xMin"], paramDict["xMin"]], 

268 [paramDict["yMin"], paramDict["yMin"], paramDict["yMax"], paramDict["yMax"], paramDict["yMin"]], 

269 "k", 

270 alpha=0.3, 

271 label="Initial selection", 

272 ) 

273 

274 # Add some useful information to the plot. 

275 bbox = dict(alpha=0.9, facecolor="white", edgecolor="none") 

276 infoText = "N Total: {}\nN Used: {}".format(sum(goodPoints), sum(fitPoints)) 

277 ax.text(0.04, 0.97, infoText, color="k", transform=ax.transAxes, fontsize=7, bbox=bbox, va="top") 

278 

279 # Calculate the point density for the Used and NotUsed subsamples. 

280 xyUsed = np.vstack([xs[fitPoints], ys[fitPoints]]) 

281 xyNotUsed = np.vstack([xs[~fitPoints & goodPoints], ys[~fitPoints & goodPoints]]) 

282 zUsed = scipy.stats.gaussian_kde(xyUsed)(xyUsed) 

283 zNotUsed = scipy.stats.gaussian_kde(xyNotUsed)(xyNotUsed) 

284 

285 notUsedScatter = ax.scatter( 

286 xs[~fitPoints & goodPoints], ys[~fitPoints & goodPoints], c=zNotUsed, cmap=newGrays, s=0.3 

287 ) 

288 fitScatter = ax.scatter( 

289 xs[fitPoints], ys[fitPoints], c=zUsed, cmap=newBlues, s=0.3, label="Used for Fit" 

290 ) 

291 

292 # Add colorbars. 

293 cbAx = fig.add_axes([0.12, 0.07, 0.43, 0.04]) 

294 plt.colorbar(fitScatter, cax=cbAx, orientation="horizontal") 

295 cbKwargs = { 

296 "color": "k", 

297 "rotation": "horizontal", 

298 "ha": "center", 

299 "va": "center", 

300 "fontsize": 7, 

301 } 

302 cbText = cbAx.text( 

303 0.5, 

304 0.5, 

305 "Number Density (used in fit)", 

306 transform=cbAx.transAxes, 

307 **cbKwargs, 

308 ) 

309 cbText.set_path_effects([pathEffects.Stroke(linewidth=1.5, foreground="w"), pathEffects.Normal()]) 

310 cbAx.set_xticks([np.min(zUsed), np.max(zUsed)], labels=["Less", "More"], fontsize=7) 

311 cbAxNotUsed = fig.add_axes([0.12, 0.11, 0.43, 0.04]) 

312 plt.colorbar(notUsedScatter, cax=cbAxNotUsed, orientation="horizontal") 

313 cbText = cbAxNotUsed.text( 

314 0.5, 

315 0.5, 

316 "Number Density (not used in fit)", 

317 transform=cbAxNotUsed.transAxes, 

318 **cbKwargs, 

319 ) 

320 cbText.set_path_effects([pathEffects.Stroke(linewidth=1.5, foreground="w"), pathEffects.Normal()]) 

321 cbAxNotUsed.set_xticks([]) 

322 

323 ax.set_xlabel(self.xAxisLabel, fontsize=8) 

324 ax.set_ylabel(self.yAxisLabel, fontsize=8) 

325 ax.tick_params(labelsize=7) 

326 

327 # Set axis limits from configs if set, otherwise based on the data. 

328 if self.xLims is not None: 

329 ax.set_xlim(self.xLims[0], self.xLims[1]) 

330 else: 

331 percsX = np.nanpercentile(xs[goodPoints], [0.5, 99.5]) 

332 x5 = (percsX[1] - percsX[0]) / 5 

333 ax.set_xlim(percsX[0] - x5, percsX[1] + x5) 

334 if self.yLims is not None: 

335 ax.set_ylim(self.yLims[0], self.yLims[1]) 

336 else: 

337 percsY = np.nanpercentile(ys[goodPoints], [0.5, 99.5]) 

338 y5 = (percsY[1] - percsY[0]) / 5 

339 ax.set_ylim(percsY[0] - y5, percsY[1] + y5) 

340 

341 # Plot the fit lines. 

342 if np.fabs(paramDict["mFixed"]) > 1: 

343 ysFitLineFixed = np.array([paramDict["yMin"], paramDict["yMax"]]) 

344 xsFitLineFixed = (ysFitLineFixed - paramDict["bFixed"]) / paramDict["mFixed"] 

345 ysFitLine = np.array([paramDict["yMin"], paramDict["yMax"]]) 

346 xsFitLine = (ysFitLine - data["bODR"]) / data["mODR"] 

347 

348 else: 

349 xsFitLineFixed = np.array([paramDict["xMin"], paramDict["xMax"]]) 

350 ysFitLineFixed = paramDict["mFixed"] * xsFitLineFixed + paramDict["bFixed"] 

351 xsFitLine = np.array([paramDict["xMin"], paramDict["xMax"]]) 

352 ysFitLine = np.array( 

353 [ 

354 data["mODR"] * xsFitLine[0] + data["bODR"], 

355 data["mODR"] * xsFitLine[1] + data["bODR"], 

356 ] 

357 ) 

358 

359 ax.plot(xsFitLineFixed, ysFitLineFixed, "w", lw=1.5) 

360 (lineFixed,) = ax.plot(xsFitLineFixed, ysFitLineFixed, "tab:green", lw=1, ls="--", label="Fixed") 

361 ax.plot(xsFitLine, ysFitLine, "w", lw=1.5) 

362 (lineOdrFit,) = ax.plot(xsFitLine, ysFitLine, "k", lw=1, ls="--", label="ODR Fit") 

363 ax.legend( 

364 handles=[initialBox, lineFixed, lineOdrFit], handlelength=1.5, fontsize=6, loc="lower right" 

365 ) 

366 

367 # Calculate the distances (in mmag) to the line for the data used in 

368 # the fit. Two points are needed to characterize the lines we want 

369 # to get the distances to. 

370 p1 = np.array([xsFitLine[0], ysFitLine[0]]) 

371 p2 = np.array([xsFitLine[1], ysFitLine[1]]) 

372 

373 p1Fixed = np.array([xsFitLineFixed[0], ysFitLineFixed[0]]) 

374 p2Fixed = np.array([xsFitLineFixed[1], ysFitLineFixed[1]]) 

375 

376 # Convert to millimags. 

377 statsUnitStr = "mmag" 

378 distsFixed = np.array(perpDistance(p1Fixed, p2Fixed, zip(xs[fitPoints], ys[fitPoints]))) * 1000 

379 dists = np.array(perpDistance(p1, p2, zip(xs[fitPoints], ys[fitPoints]))) * 1000 

380 maxDist = np.abs(np.nanmax(dists)) / 1000 # These will be used to set the fit boundary line limits. 

381 minDist = np.abs(np.nanmin(dists)) / 1000 

382 # Now we have the information for the perpendicular line we can use it 

383 # to calculate the points at the ends of the perpendicular lines that 

384 # intersect at the box edges. 

385 if np.fabs(paramDict["mFixed"]) > 1: 

386 xMid = (paramDict["yMin"] - data["bODR"]) / data["mODR"] 

387 xsFit = np.array([xMid - max(0.2, maxDist), xMid, xMid + max(0.2, minDist)]) 

388 ysFit = data["mPerp"] * xsFit + data["bPerpMin"] 

389 else: 

390 xsFit = np.array( 

391 [ 

392 paramDict["xMin"] - max(0.2, np.fabs(paramDict["mFixed"]) * maxDist), 

393 paramDict["xMin"], 

394 paramDict["xMin"] + max(0.2, np.fabs(paramDict["mFixed"]) * minDist), 

395 ] 

396 ) 

397 ysFit = xsFit * data["mPerp"] + data["bPerpMin"] 

398 ax.plot(xsFit, ysFit, "k--", alpha=0.7, lw=1) 

399 

400 if np.fabs(paramDict["mFixed"]) > 1: 

401 xMid = (paramDict["yMax"] - data["bODR"]) / data["mODR"] 

402 xsFit = np.array([xMid - max(0.2, maxDist), xMid, xMid + max(0.2, minDist)]) 

403 ysFit = data["mPerp"] * xsFit + data["bPerpMax"] 

404 else: 

405 xsFit = np.array( 

406 [ 

407 paramDict["xMax"] - max(0.2, np.fabs(paramDict["mFixed"]) * maxDist), 

408 paramDict["xMax"], 

409 paramDict["xMax"] + max(0.2, np.fabs(paramDict["mFixed"]) * minDist), 

410 ] 

411 ) 

412 ysFit = xsFit * data["mPerp"] + data["bPerpMax"] 

413 ax.plot(xsFit, ysFit, "k--", alpha=0.7, lw=1) 

414 

415 # Compute statistics for fit. 

416 medDists = nanMedian(dists) 

417 madDists = nanSigmaMad(dists) 

418 meanDists = nanMean(dists) 

419 rmsDists = np.sqrt(np.mean(np.array(dists) ** 2)) 

420 

421 xMid = paramDict["xMin"] + 0.5 * (paramDict["xMax"] - paramDict["xMin"]) 

422 if self.doPlotRedBlueHists: 

423 blueStars = (xs[fitPoints] < xMid) & (xs[fitPoints] >= paramDict["xMin"]) 

424 blueDists = dists[blueStars] 

425 blueMedDists = nanMedian(blueDists) 

426 redStars = (xs[fitPoints] >= xMid) & (xs[fitPoints] <= paramDict["xMax"]) 

427 redDists = dists[redStars] 

428 redMedDists = nanMedian(redDists) 

429 

430 if self.doPlotRedBlueHists: 

431 blueStars = (xs[fitPoints] < xMid) & (xs[fitPoints] >= paramDict["xMin"]) 

432 blueDists = dists[blueStars] 

433 blueMedDists = nanMedian(blueDists) 

434 redStars = (xs[fitPoints] >= xMid) & (xs[fitPoints] <= paramDict["xMax"]) 

435 redDists = dists[redStars] 

436 redMedDists = nanMedian(redDists) 

437 

438 # Add a histogram. 

439 axHist.set_ylabel("Number", fontsize=7) 

440 axHist.set_xlabel("Distance to Line Fit ({})".format(statsUnitStr), fontsize=7) 

441 axHist.tick_params(labelsize=7) 

442 nSigToPlot = 3.5 

443 axHist.set_xlim(meanDists - nSigToPlot * madDists, meanDists + nSigToPlot * madDists) 

444 lineMedian = axHist.axvline( 

445 medDists, color="k", lw=1, alpha=0.5, label="Median: {:0.2f} {}".format(medDists, statsUnitStr) 

446 ) 

447 lineMad = axHist.axvline( 

448 medDists + madDists, 

449 color="k", 

450 ls="--", 

451 lw=1, 

452 alpha=0.5, 

453 label=r"$\sigma_{MAD}$" + ": {:0.2f} {}".format(madDists, statsUnitStr), 

454 ) 

455 axHist.axvline(medDists - madDists, color="k", ls="--", lw=1, alpha=0.5) 

456 lineRms = axHist.axvline( 

457 meanDists + rmsDists, 

458 color="k", 

459 ls=":", 

460 lw=1, 

461 alpha=0.3, 

462 label="RMS: {:0.2f} {}".format(rmsDists, statsUnitStr), 

463 ) 

464 axHist.axvline(meanDists - rmsDists, color="k", ls=":", lw=1, alpha=0.3) 

465 if self.doPlotRedBlueHists: 

466 lineBlueMedian = axHist.axvline( 

467 blueMedDists, 

468 color="blue", 

469 ls=":", 

470 lw=1.0, 

471 alpha=0.5, 

472 label="blueMed: {:0.2f}".format(blueMedDists), 

473 ) 

474 lineRedMedian = axHist.axvline( 

475 redMedDists, 

476 color="red", 

477 ls=":", 

478 lw=1.0, 

479 alpha=0.5, 

480 label="redMed: {:0.2f}".format(redMedDists), 

481 ) 

482 linesForLegend = [lineMedian, lineMad, lineRms, lineBlueMedian, lineRedMedian] 

483 else: 

484 linesForLegend = [lineMedian, lineMad, lineRms] 

485 fig.legend( 

486 handles=linesForLegend, 

487 handlelength=1.0, 

488 fontsize=6, 

489 loc="lower right", 

490 bbox_to_anchor=(0.955, 0.89), 

491 bbox_transform=fig.transFigure, 

492 ncol=2, 

493 ) 

494 

495 axHist.hist(dists, bins=100, histtype="stepfilled", label="ODR Fit", color="k", ec="k", alpha=0.3) 

496 axHist.hist(distsFixed, bins=100, histtype="step", label="Fixed", color="tab:green", alpha=1.0) 

497 if self.doPlotRedBlueHists: 

498 axHist.hist(blueDists, bins=100, histtype="stepfilled", color="blue", ec="blue", alpha=0.3) 

499 axHist.hist(redDists, bins=100, histtype="stepfilled", color="red", ec="red", alpha=0.3) 

500 

501 handles = [Rectangle((0, 0), 1, 1, color="k", alpha=0.4)] 

502 handles.append(Rectangle((0, 0), 1, 1, color="none", ec="tab:green", alpha=1.0)) 

503 labels = ["ODR Fit", "Fixed"] 

504 if self.doPlotRedBlueHists: 

505 handles.append(Rectangle((0, 0), 1, 1, color="blue", alpha=0.3)) 

506 handles.append(Rectangle((0, 0), 1, 1, color="red", alpha=0.3)) 

507 labels = ["ODR Fit", "Blue Stars", "Red Stars", "Fixed"] 

508 axHist.legend(handles, labels, fontsize=5, loc="upper right") 

509 

510 if self.doPlotDistVsColor: 

511 axLowerRight.axhline(0.0, color="k", ls="-", lw=0.8, zorder=-1) 

512 # Compute and plot a running median of dists vs. color. 

513 if np.fabs(data["mODR"]) > 1.0: 

514 xRun = ys[fitPoints].copy() 

515 axLowerRight.set_xlabel(self.yAxisLabel, fontsize=7) 

516 else: 

517 xRun = xs[fitPoints].copy() 

518 axLowerRight.set_xlabel(self.xAxisLabel, fontsize=7) 

519 lowerRightPlot = axLowerRight.scatter(xRun, dists, c=mags[fitPoints], cmap=newBlues, s=0.2) 

520 yRun = dists.copy() 

521 xySorted = zip(xRun, yRun) 

522 xySorted = sorted(xySorted) 

523 xSorted = [x for x, y in xySorted] 

524 ySorted = [y for x, y in xySorted] 

525 nCumulate = int(max(3, len(xRun) // 10)) 

526 yRunMedian = median_filter(ySorted, size=nCumulate) 

527 axLowerRight.plot(xSorted, yRunMedian, "w", lw=1.8) 

528 axLowerRight.plot(xSorted, yRunMedian, c="purple", ls="-", lw=1.1, label="Running Median") 

529 axLowerRight.set_ylim(-2.5 * madDists, 2.5 * madDists) 

530 axLowerRight.set_ylabel("Distance to Line Fit ({})".format(statsUnitStr), fontsize=7) 

531 axLowerRight.legend(fontsize=4, loc="upper right", handlelength=1.0) 

532 # Add colorbars. 

533 cbAx = fig.add_axes([0.915, 0.11, 0.014, 0.34]) 

534 plt.colorbar(lowerRightPlot, cax=cbAx, orientation="vertical") 

535 cbKwargs = { 

536 "color": "k", 

537 "rotation": "vertical", 

538 "ha": "center", 

539 "va": "center", 

540 "fontsize": 4, 

541 } 

542 cbText = cbAx.text( 

543 0.5, 

544 0.5, 

545 self.magLabel, 

546 transform=cbAx.transAxes, 

547 **cbKwargs, 

548 ) 

549 cbText.set_path_effects([pathEffects.Stroke(linewidth=1.2, foreground="w"), pathEffects.Normal()]) 

550 cbAx.tick_params(length=2, labelsize=3.5) 

551 else: 

552 # Add a contour plot showing the magnitude dependance of the 

553 # distance to the fit. 

554 axLowerRight.invert_yaxis() 

555 axLowerRight.axvline(0.0, color="k", ls="--", zorder=-1) 

556 percsDists = np.nanpercentile(dists, [4, 96]) 

557 minXs = -1 * np.min(np.fabs(percsDists)) 

558 maxXs = np.min(np.fabs(percsDists)) 

559 plotPoints = (dists < maxXs) & (dists > minXs) 

560 xsContour = np.array(dists)[plotPoints] 

561 ysContour = cast(Vector, cast(Vector, mags)[cast(Vector, fitPoints)])[cast(Vector, plotPoints)] 

562 H, xEdges, yEdges = np.histogram2d(xsContour, ysContour, bins=(11, 11)) 

563 xBinWidth = xEdges[1] - xEdges[0] 

564 yBinWidth = yEdges[1] - yEdges[0] 

565 axLowerRight.contour( 

566 xEdges[:-1] + xBinWidth / 2, yEdges[:-1] + yBinWidth / 2, H.T, levels=7, cmap=newBlues 

567 ) 

568 axLowerRight.set_xlabel("Distance to Line Fit ({})".format(statsUnitStr), fontsize=8) 

569 axLowerRight.set_ylabel(self.magLabel, fontsize=8) 

570 axLowerRight.set_xlim(meanDists - nSigToPlot * madDists, meanDists + nSigToPlot * madDists) 

571 axLowerRight.tick_params(labelsize=6) 

572 

573 fig = addPlotInfo(plt.gcf(), plotInfo) 

574 

575 return fig