Coverage for tests/test_producePlots.py: 97%

88 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-15 10:18 +0000

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 <http://www.gnu.org/licenses/>. 

21 

22from unittest import TestCase, main 

23 

24import matplotlib.pyplot as plt 

25 

26import lsst.utils.tests 

27from lsst.analysis.tools.actions.vector import LoadVector 

28from lsst.analysis.tools.atools import SkyObjectHistPlot 

29from lsst.analysis.tools.interfaces import AnalysisTool, JointAction, PlotAction 

30 

31# BaseMetricAction is used to produce a real Measurement without needing to 

32# hand-roll a MetricAction subclass. 

33from lsst.analysis.tools.interfaces._stages import BaseMetricAction 

34from lsst.analysis.tools.tasks.objectTableTractAnalysis import ObjectTableTractAnalysisConfig 

35from lsst.pipe.base.connections import iterConnections 

36from lsst.verify import Measurement 

37 

38 

39class NullPlot(PlotAction): 

40 def __call__(self, data, **kwargs): 

41 return plt.figure() 

42 

43 

44class ProducePlotsToolTestCase(TestCase): 

45 """Test the `AnalysisTool.doProducePlots` switch.""" 

46 

47 def _makeJointTool(self) -> AnalysisTool: 

48 tool = AnalysisTool() 

49 tool.prep.keysToLoad = ["value"] 

50 tool.process.buildActions.value = LoadVector(vectorKey="value") 

51 joint = JointAction(metric=BaseMetricAction(), plot=NullPlot()) 

52 joint.metric.units = {"value": "ct"} 

53 tool.produce = joint 

54 return tool 

55 

56 def testJointActionSkipsPlotOnly(self): 

57 """With doProducePlots=False, the metric is still produced but the 

58 plot is not. 

59 """ 

60 tool = self._makeJointTool() 

61 data = {"value": 5} 

62 

63 resultsWithPlots = tool(data) 

64 self.assertTrue(any(isinstance(v, Measurement) for v in resultsWithPlots.values())) 

65 self.assertTrue(any(hasattr(v, "savefig") for v in resultsWithPlots.values())) 

66 

67 tool.doProducePlots = False 

68 resultsNoPlots = tool(data) 

69 self.assertTrue(any(isinstance(v, Measurement) for v in resultsNoPlots.values())) 

70 self.assertFalse(any(hasattr(v, "savefig") for v in resultsNoPlots.values())) 

71 

72 def testPurePlotActionProducesNothing(self): 

73 """An AnalysisTool whose produce action is only a PlotAction (no 

74 metric) produces no results at all when doProducePlots is False. 

75 """ 

76 tool = AnalysisTool() 

77 tool.produce = NullPlot() 

78 

79 self.assertTrue(tool({})) 

80 

81 tool.doProducePlots = False 

82 self.assertEqual(tool({}), {}) 

83 

84 def testGetOutputNamesEmptyWhenDisabled(self): 

85 tool = self._makeJointTool() 

86 self.assertNotEqual(tuple(tool.getOutputNames()), tuple()) 

87 

88 tool.doProducePlots = False 

89 self.assertEqual(tuple(tool.getOutputNames()), tuple()) 

90 

91 

92class ProducePlotsTaskTestCase(TestCase): 

93 """Test that `AnalysisBaseConfig.doProducePlots` propagates to atools on 

94 freeze, without ever re-enabling a tool that has disabled its own plots. 

95 """ 

96 

97 def _makeConfig(self, taskDoProducePlots: bool, toolDoProducePlots: bool | None = None): 

98 config = ObjectTableTractAnalysisConfig() 

99 config.connections.outputName = "test" 

100 config.atools.skyObjectHistPlot = SkyObjectHistPlot() 

101 if toolDoProducePlots is not None: 

102 config.atools.skyObjectHistPlot.doProducePlots = toolDoProducePlots 

103 config.doProducePlots = taskDoProducePlots 

104 config.validate() 

105 config.freeze() 

106 return config 

107 

108 def testTaskLevelForcesToolsOff(self): 

109 config = self._makeConfig(taskDoProducePlots=False) 

110 self.assertFalse(config.atools.skyObjectHistPlot.doProducePlots) 

111 

112 def testTaskLevelDoesNotForceToolsOn(self): 

113 config = self._makeConfig(taskDoProducePlots=True) 

114 self.assertTrue(config.atools.skyObjectHistPlot.doProducePlots) 

115 

116 def testTaskLevelDoesNotReenableDisabledTool(self): 

117 # There may be occasions when one wants to off the plots for one 

118 # analysis tool within a suite of analysis tools. 

119 config = self._makeConfig(taskDoProducePlots=True, toolDoProducePlots=False) 

120 self.assertFalse(config.atools.skyObjectHistPlot.doProducePlots) 

121 

122 

123class ProducePlotsConnectionsTestCase(TestCase): 

124 """Test that plot output connections are not created for atools with 

125 plots disabled. 

126 """ 

127 

128 def _plotConnectionNames(self, config) -> set[str]: 

129 connections = config.connections.ConnectionsClass(config=config) 

130 return {c.name for c in iterConnections(connections, "outputs") if c.storageClass == "Plot"} 

131 

132 def _makeConfig(self, doProducePlots: bool): 

133 config = ObjectTableTractAnalysisConfig() 

134 config.connections.outputName = "test" 

135 config.atools.skyObjectHistPlot = SkyObjectHistPlot() 

136 config.doProducePlots = doProducePlots 

137 config.validate() 

138 config.freeze() 

139 return config 

140 

141 def testPlotConnectionsPresentWhenEnabled(self): 

142 config = self._makeConfig(doProducePlots=True) 

143 self.assertTrue(self._plotConnectionNames(config)) 

144 

145 def testPlotConnectionsAbsentWhenDisabled(self): 

146 config = self._makeConfig(doProducePlots=False) 

147 self.assertEqual(self._plotConnectionNames(config), set()) 

148 

149 

150class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase): 

151 pass 

152 

153 

154def setup_module(module): 

155 lsst.utils.tests.init() 

156 

157 

158if __name__ == "__main__": 158 ↛ 159line 158 didn't jump to line 159 because the condition on line 158 was never true

159 lsst.utils.tests.init() 

160 main()