Coverage for tests/test_scatterPlot.py: 89%
102 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-14 21:00 -0700
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-14 21:00 -0700
1# This file is part of analysis_drp.
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/>.
23import os
24import shutil
25import tempfile
26import unittest
28import matplotlib
29import matplotlib.pyplot as plt
30import numpy as np
31import pandas as pd
33import lsst.utils.tests
34from lsst.analysis.tools.actions.plot.plotUtils import get_and_remove_figure_text
35from lsst.analysis.tools.actions.plot.scatterplotWithTwoHists import (
36 ScatterPlotStatsAction,
37 ScatterPlotWithTwoHists,
38)
39from lsst.analysis.tools.actions.vector.mathActions import ConstantValue, DivideVector, SubtractVector
40from lsst.analysis.tools.actions.vector.selectors import (
41 GalaxySelector,
42 SnSelector,
43 StarSelector,
44 VectorSelector,
45)
46from lsst.analysis.tools.actions.vector.vectorActions import ConvertFluxToMag, DownselectVector, LoadVector
47from lsst.analysis.tools.interfaces import AnalysisTool
48from lsst.analysis.tools.math import sqrt
50matplotlib.use("Agg")
52ROOT = os.path.abspath(os.path.dirname(__file__))
53filename_texts_ref = os.path.join(ROOT, "data", "test_scatterPlot_texts.txt")
54path_lines_ref = os.path.join(ROOT, "data", "test_scatterPlot_lines")
57class ScatterPlotWithTwoHistsTaskTestCase(lsst.utils.tests.TestCase):
58 """ScatterPlotWithTwoHistsTask test case."""
60 def setUp(self):
61 self.testDir = tempfile.mkdtemp(dir=ROOT, prefix="test_output")
63 # Set up a quasi-plausible measurement catalog
64 mag = 12.5 + 2.5 * np.log10(np.arange(10, 100000))
65 flux = 10 ** (-0.4 * (mag - (mag[-1] + 1)))
66 rng = np.random.default_rng(0)
67 extendedness = 0.0 + (rng.uniform(size=len(mag)) < 0.99 * (mag - mag[0]) / (mag[-1] - mag[0]))
68 flux_meas = flux + rng.normal(scale=np.sqrt(flux * (1 + extendedness)))
69 flux_err = sqrt(flux_meas * (1 + extendedness))
70 good = (flux_meas / sqrt(flux * (1 + extendedness))) > 3
71 extendedness = extendedness[good]
72 flux = flux[good]
73 flux_meas = flux_meas[good]
74 flux_err = flux_err[good]
76 suffix_x, suffix_y, suffix_stat = "_x", "_y", "_stat"
78 # Configure the plot to show observed vs true mags
79 action = ScatterPlotWithTwoHists(
80 xAxisLabel="mag",
81 yAxisLabel="mag meas - ref",
82 magLabel="mag",
83 plotTypes=[
84 "galaxies",
85 "stars",
86 ],
87 xLims=(20, 30),
88 yLims=(-1000, 1000),
89 addSummaryPlot=False,
90 # Make sure adding a suffix works to produce multiple plots
91 suffix_x=suffix_x,
92 suffix_y=suffix_y,
93 suffix_stat=suffix_stat,
94 )
95 plot = AnalysisTool()
96 plot.produce.plot = action
98 # Load the relevant columns
99 key_flux = "meas_Flux"
100 plot.process.buildActions.fluxes_meas = LoadVector(vectorKey=key_flux)
101 plot.process.buildActions.fluxes_err = LoadVector(vectorKey=f"{key_flux}Err")
102 plot.process.buildActions.fluxes_ref = LoadVector(vectorKey="ref_Flux")
103 plot.process.buildActions.mags_ref = ConvertFluxToMag(
104 vectorKey=plot.process.buildActions.fluxes_ref.vectorKey
105 )
107 # Compute the y-axis quantity
108 plot.process.buildActions.diff = SubtractVector(
109 actionA=ConvertFluxToMag(
110 vectorKey=plot.process.buildActions.fluxes_meas.vectorKey, returnMillimags=True
111 ),
112 actionB=DivideVector(
113 actionA=plot.process.buildActions.mags_ref,
114 actionB=ConstantValue(value=1e-3),
115 ),
116 )
118 # Filter stars/galaxies, storing quantities separately
119 plot.process.buildActions.galaxySelector = GalaxySelector(vectorKey="refExtendedness")
120 plot.process.buildActions.starSelector = StarSelector(vectorKey="refExtendedness")
121 for singular, plural in (("galaxy", "Galaxies"), ("star", "Stars")):
122 setattr(
123 plot.process.filterActions,
124 f"x{plural}{suffix_x}",
125 DownselectVector(
126 vectorKey="mags_ref", selector=VectorSelector(vectorKey=f"{singular}Selector")
127 ),
128 )
129 setattr(
130 plot.process.filterActions,
131 f"y{plural}{suffix_y}",
132 DownselectVector(vectorKey="diff", selector=VectorSelector(vectorKey=f"{singular}Selector")),
133 )
134 setattr(
135 plot.process.filterActions,
136 f"flux{plural}",
137 DownselectVector(
138 vectorKey="fluxes_meas", selector=VectorSelector(vectorKey=f"{singular}Selector")
139 ),
140 )
141 setattr(
142 plot.process.filterActions,
143 f"fluxErr{plural}",
144 DownselectVector(
145 vectorKey="fluxes_err", selector=VectorSelector(vectorKey=f"{singular}Selector")
146 ),
147 )
149 # Compute low/high SN summary stats
150 statAction = ScatterPlotStatsAction(
151 vectorKey=f"y{plural}{suffix_y}",
152 fluxType=f"flux{plural}",
153 highSNSelector=SnSelector(fluxType=f"flux{plural}", threshold=50),
154 lowSNSelector=SnSelector(fluxType=f"flux{plural}", threshold=20),
155 suffix=suffix_stat,
156 )
157 setattr(plot.process.calculateActions, plural.lower(), statAction)
159 data = {
160 "ref_Flux": flux,
161 key_flux: flux_meas,
162 f"{key_flux}Err": flux_err,
163 "refExtendedness": extendedness,
164 }
166 self.data = pd.DataFrame(data)
167 print(self.data.columns)
168 self.plot = plot
169 self.plot.finalize()
170 plotInfo = {key: "test" for key in ("plotName", "run", "tableName")}
171 plotInfo["bands"] = []
172 self.plotInfo = plotInfo
174 def tearDown(self):
175 if os.path.exists(self.testDir): 175 ↛ 177line 175 didn't jump to line 177 because the condition on line 175 was always true
176 shutil.rmtree(self.testDir, True)
177 del self.data
178 del self.plot
179 del self.plotInfo
180 del self.testDir
182 def test_ScatterPlotWithTwoHistsTask(self):
183 plt.rcParams.update(plt.rcParamsDefault)
184 result = self.plot(
185 data=self.data,
186 skymap=None,
187 plotInfo=self.plotInfo,
188 )
189 # unpack the result from the dictionary
190 result = result[type(self.plot.produce.plot).__name__]
191 self.assertTrue(isinstance(result, plt.Figure))
193 # Set to true to save plots as PNGs
194 # Use matplotlib.testing.compare.compare_images if needed
195 save_images = False
196 if save_images: 196 ↛ 197line 196 didn't jump to line 197 because the condition on line 196 was never true
197 result.savefig(os.path.join(ROOT, "data", "test_scatterPlot.png"))
199 texts, lines = get_and_remove_figure_text(result)
200 if save_images: 200 ↛ 201line 200 didn't jump to line 201 because the condition on line 200 was never true
201 result.savefig(os.path.join(ROOT, "data", "test_scatterPlot_unlabeled.png"))
203 # Set to true to re-generate reference data
204 resave = False
206 # Compare line values
207 for idx, line in enumerate(lines):
208 filename = os.path.join(path_lines_ref, f"line_{idx}.txt")
209 if resave: 209 ↛ 210line 209 didn't jump to line 210 because the condition on line 209 was never true
210 np.savetxt(filename, line)
211 arr = np.loadtxt(filename)
212 # Differences of order 1e-12 possible between MacOS and Linux
213 # Plots are generally not expected to be that precise
214 # Differences to 1e-3 should not be visible with this test data
215 self.assertFloatsAlmostEqual(arr, line, atol=1e-3, rtol=1e-4)
217 # Ensure that newlines within labels are replaced by a sentinel
218 newline = "\n"
219 newline_replace = "[newline]"
220 # Compare text labels
221 if resave: 221 ↛ 222line 221 didn't jump to line 222 because the condition on line 221 was never true
222 with open(filename_texts_ref, "w") as f:
223 f.writelines(f"{text.strip().replace(newline, newline_replace)}\n" for text in texts)
225 with open(filename_texts_ref) as f:
226 texts_ref = set(x.strip() for x in f.readlines())
227 texts_set = set(x.strip().replace(newline, newline_replace) for x in texts)
229 self.assertEqual(texts_ref, texts_set)
232class MemoryTester(lsst.utils.tests.MemoryTestCase):
233 pass
236def setup_module(module):
237 lsst.utils.tests.init()
240if __name__ == "__main__": 240 ↛ 241line 240 didn't jump to line 241 because the condition on line 240 was never true
241 lsst.utils.tests.init()
242 unittest.main()