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