Coverage for tests/test_quantum_clustering_funcs.py: 27%
103 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-07 02:51 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-07 02:51 -0800
1# This file is part of ctrl_bps.
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"""Unit tests for the clustering methods.
22"""
24# Turn off "doesn't conform to snake_case naming style" because matching
25# the unittest casing.
26# pylint: disable=invalid-name
28import os
29import unittest
31from cqg_test_utils import check_cqg
32from lsst.ctrl.bps import BpsConfig
33from lsst.ctrl.bps.quantum_clustering_funcs import dimension_clustering, single_quantum_clustering
34from qg_test_utils import make_test_quantum_graph
36TESTDIR = os.path.abspath(os.path.dirname(__file__))
39class TestSingleQuantumClustering(unittest.TestCase):
40 """Tests for single_quantum_clustering method."""
42 def setUp(self):
43 self.qgraph = make_test_quantum_graph()
45 def tearDown(self):
46 pass
48 def testClustering(self):
49 """Test valid single quantum clustering."""
51 # Note: the cluster config should be ignored.
52 config = BpsConfig(
53 {
54 "templateDataId": "{D1}_{D2}_{D3}_{D4}",
55 "cluster": {"cl1": {"pipetasks": "T1, T2, T3, T4", "dimensions": "D1, D2"}},
56 }
57 )
59 cqg = single_quantum_clustering(config, self.qgraph, "single")
60 self.assertIsNotNone(cqg)
61 self.assertIn(cqg.name, "single")
62 self.assertEqual(len(cqg), len(self.qgraph))
64 def testClusteringNoTemplate(self):
65 """Test valid single quantum clustering wihtout a template for the
66 cluster names."""
67 # Note: the cluster config should be ignored.
68 config = BpsConfig({"cluster": {"cl1": {"pipetasks": "T1, T2, T3, T4", "dimensions": "D1, D2"}}})
70 cqg = single_quantum_clustering(config, self.qgraph, "single-no-template")
71 self.assertIsNotNone(cqg)
72 self.assertIn(cqg.name, "single-no-template")
73 self.assertEqual(len(cqg), len(self.qgraph))
76class TestDimensionClustering(unittest.TestCase):
77 """Tests for dimension_clustering method."""
79 def setUp(self):
80 self.qgraph = make_test_quantum_graph()
82 def tearDown(self):
83 pass
85 def testClusterAllInOne(self):
86 """All tasks in one cluster."""
87 name = "all-in-one"
88 config = BpsConfig(
89 {
90 "templateDataId": "{D1}_{D2}_{D3}_{D4}",
91 "cluster": {"cl1": {"pipetasks": "T1, T2, T3, T4", "dimensions": "D1, D2"}},
92 }
93 )
94 answer = {
95 "name": name,
96 "nodes": {
97 "cl1_1_2": {
98 "label": "cl1",
99 "dims": {"D1": 1, "D2": 2},
100 "counts": {"T1": 1, "T2": 1, "T3": 1, "T4": 1},
101 },
102 "cl1_3_4": {
103 "label": "cl1",
104 "dims": {"D1": 3, "D2": 4},
105 "counts": {"T1": 1, "T2": 1, "T3": 1, "T4": 1},
106 },
107 },
108 "edges": [],
109 }
111 cqg = dimension_clustering(config, self.qgraph, name)
112 check_cqg(cqg, answer)
114 def testClusterTemplate(self):
115 """Test uses clusterTemplate value to name clusters."""
116 name = "cluster-template"
117 config = BpsConfig(
118 {
119 "templateDataId": "tdid_{D1}_{D2}_{D3}_{D4}",
120 "cluster": {
121 "cl1": {
122 "clusterTemplate": "ct_{D1}_{D2}_{D3}_{D4}",
123 "pipetasks": "T1, T2, T3, T4",
124 "dimensions": "D1, D2",
125 }
126 },
127 }
128 )
129 # Note: clusterTemplate can produce trailing underscore
130 answer = {
131 "name": name,
132 "nodes": {
133 "ct_1_2_": {
134 "label": "cl1",
135 "dims": {"D1": 1, "D2": 2},
136 "counts": {"T1": 1, "T2": 1, "T3": 1, "T4": 1},
137 },
138 "ct_3_4_": {
139 "label": "cl1",
140 "dims": {"D1": 3, "D2": 4},
141 "counts": {"T1": 1, "T2": 1, "T3": 1, "T4": 1},
142 },
143 },
144 "edges": [],
145 }
147 cqg = dimension_clustering(config, self.qgraph, name)
148 check_cqg(cqg, answer)
150 def testClusterNoDims(self):
151 """Test if clusters have no dimensions."""
152 name = "cluster-no-dims"
153 config = BpsConfig(
154 {
155 "templateDataId": "tdid_{D1}_{D2}_{D3}_{D4}",
156 "cluster": {
157 "cl1": {
158 "pipetasks": "T1, T2",
159 },
160 "cl2": {
161 "pipetasks": "T3, T4",
162 },
163 },
164 }
165 )
166 answer = {
167 "name": name,
168 "nodes": {
169 "cl1": {"label": "cl1", "dims": {}, "counts": {"T1": 2, "T2": 2}},
170 "cl2": {"label": "cl2", "dims": {}, "counts": {"T3": 2, "T4": 2}},
171 },
172 "edges": [("cl1", "cl2")],
173 }
175 cqg = dimension_clustering(config, self.qgraph, name)
176 check_cqg(cqg, answer)
178 def testClusterTaskRepeat(self):
179 """Can't have PipelineTask in more than one cluster."""
180 name = "task-repeat"
181 config = BpsConfig(
182 {
183 "templateDataId": "tdid_{D1}_{D2}_{D3}_{D4}",
184 "cluster": {
185 "cl1": {
186 "pipetasks": "T1, T2",
187 },
188 "cl2": {
189 "pipetasks": "T2, T3, T4",
190 },
191 },
192 }
193 )
195 with self.assertRaises(RuntimeError):
196 _ = dimension_clustering(config, self.qgraph, name)
198 def testClusterMissingDimValue(self):
199 """Quantum can't be missing a value for a clustering dimension."""
200 config = BpsConfig(
201 {
202 "templateDataId": "{D1}_{D2}_{D3}_{D4}",
203 "cluster": {"cl1": {"pipetasks": "T1, T2, T3, T4", "dimensions": "D1, NotThere"}},
204 }
205 )
207 with self.assertRaises(RuntimeError):
208 _ = dimension_clustering(config, self.qgraph, "missing-dim-value")
210 def testClusterEqualDim1(self):
211 """Test equalDimensions using right half."""
212 name = "equal-dim"
213 config = BpsConfig(
214 {
215 "templateDataId": "{D1}_{D2}_{D3}_{D4}",
216 "cluster": {
217 "cl1": {
218 "pipetasks": "T1, T2, T3, T4",
219 "dimensions": "D1, NotThere",
220 "equalDimensions": "NotThere:D2",
221 }
222 },
223 }
224 )
225 answer = {
226 "name": name,
227 "nodes": {
228 "cl1_1_2": {
229 "label": "cl1",
230 "dims": {"D1": 1, "NotThere": 2},
231 "counts": {"T1": 1, "T2": 1, "T3": 1, "T4": 1},
232 },
233 "cl1_3_4": {
234 "label": "cl1",
235 "dims": {"D1": 3, "NotThere": 4},
236 "counts": {"T1": 1, "T2": 1, "T3": 1, "T4": 1},
237 },
238 },
239 "edges": [],
240 }
242 cqg = dimension_clustering(config, self.qgraph, name)
243 check_cqg(cqg, answer)
245 def testClusterEqualDim2(self):
246 """Test equalDimensions using left half."""
247 name = "equal-dim-2"
248 config = BpsConfig(
249 {
250 "templateDataId": "{D1}_{D2}_{D3}_{D4}",
251 "cluster": {
252 "cl1": {
253 "pipetasks": "T1, T2, T3, T4",
254 "dimensions": "D1, NotThere",
255 "equalDimensions": "D2:NotThere",
256 }
257 },
258 }
259 )
260 answer = {
261 "name": name,
262 "nodes": {
263 "cl1_1_2": {
264 "label": "cl1",
265 "dims": {"D1": 1, "NotThere": 2},
266 "counts": {"T1": 1, "T2": 1, "T3": 1, "T4": 1},
267 },
268 "cl1_3_4": {
269 "label": "cl1",
270 "dims": {"D1": 3, "NotThere": 4},
271 "counts": {"T1": 1, "T2": 1, "T3": 1, "T4": 1},
272 },
273 },
274 "edges": [],
275 }
277 cqg = dimension_clustering(config, self.qgraph, name)
278 check_cqg(cqg, answer)
280 def testClusterMult(self):
281 """Test multiple tasks in multiple clusters."""
282 name = "cluster-mult"
283 config = BpsConfig(
284 {
285 "templateDataId": "{D1}_{D2}_{D3}_{D4}",
286 "cluster": {
287 "cl1": {"pipetasks": "T1, T2", "dimensions": "D1, D2"},
288 "cl2": {"pipetasks": "T3, T4", "dimensions": "D1, D2"},
289 },
290 }
291 )
292 answer = {
293 "name": name,
294 "nodes": {
295 "cl1_1_2": {"label": "cl1", "dims": {"D1": 1, "D2": 2}, "counts": {"T1": 1, "T2": 1}},
296 "cl1_3_4": {"label": "cl1", "dims": {"D1": 3, "D2": 4}, "counts": {"T1": 1, "T2": 1}},
297 "cl2_1_2": {"label": "cl2", "dims": {"D1": 1, "D2": 2}, "counts": {"T3": 1, "T4": 1}},
298 "cl2_3_4": {"label": "cl2", "dims": {"D1": 3, "D2": 4}, "counts": {"T3": 1, "T4": 1}},
299 },
300 "edges": [("cl1_3_4", "cl2_3_4"), ("cl1_1_2", "cl2_1_2")],
301 }
303 cqg = dimension_clustering(config, self.qgraph, name)
304 check_cqg(cqg, answer)
306 def testClusterPart(self):
307 """Test will use templateDataId if no clusterTemplate."""
308 name = "cluster-part"
309 config = BpsConfig(
310 {
311 "templateDataId": "{D1}_{D2}_{D3}_{D4}",
312 "cluster": {
313 "cl1": {"pipetasks": "T1, T2", "dimensions": "D1, D2"},
314 },
315 }
316 )
317 answer = {
318 "name": name,
319 "nodes": {
320 "cl1_1_2": {"label": "cl1", "dims": {"D1": 1, "D2": 2}, "counts": {"T1": 1, "T2": 1}},
321 "cl1_3_4": {"label": "cl1", "dims": {"D1": 3, "D2": 4}, "counts": {"T1": 1, "T2": 1}},
322 "NODENAME_T3_1_2_": {"label": "T3", "dims": {"D1": 1, "D2": 2}, "counts": {"T3": 1}},
323 "NODENAME_T3_3_4_": {"label": "T3", "dims": {"D1": 3, "D2": 4}, "counts": {"T3": 1}},
324 "NODENAME_T4_1_2_": {"label": "T4", "dims": {"D1": 1, "D2": 2}, "counts": {"T4": 1}},
325 "NODENAME_T4_3_4_": {"label": "T4", "dims": {"D1": 3, "D2": 4}, "counts": {"T4": 1}},
326 },
327 "edges": [("cl1_1_2", "NODENAME_T3_1_2_"), ("cl1_3_4", "NODENAME_T3_3_4_")],
328 }
330 cqg = dimension_clustering(config, self.qgraph, name)
331 check_cqg(cqg, answer)
333 def testClusterPartNoTemplate(self):
334 """No templateDataId nor clusterTemplate (use cluster label)."""
335 name = "cluster-part-no-template"
336 config = BpsConfig(
337 {
338 "cluster": {
339 "cl1": {"pipetasks": "T1, T2"},
340 }
341 }
342 )
343 answer = {
344 "name": name,
345 "nodes": {
346 "cl1": {"label": "cl1", "dims": {}, "counts": {"T1": 2, "T2": 2}},
347 "NODEONLY_T3_{'D1': 1, 'D2': 2}": {
348 "label": "T3",
349 "dims": {"D1": 1, "D2": 2},
350 "counts": {"T3": 1},
351 },
352 "NODEONLY_T3_{'D1': 3, 'D2': 4}": {
353 "label": "T3",
354 "dims": {"D1": 3, "D2": 4},
355 "counts": {"T3": 1},
356 },
357 "NODEONLY_T4_{'D1': 1, 'D2': 2}": {
358 "label": "T4",
359 "dims": {"D1": 1, "D2": 2},
360 "counts": {"T4": 1},
361 },
362 "NODEONLY_T4_{'D1': 3, 'D2': 4}": {
363 "label": "T4",
364 "dims": {"D1": 3, "D2": 4},
365 "counts": {"T4": 1},
366 },
367 },
368 "edges": [("cl1", "NODEONLY_T3_{'D1': 1, 'D2': 2}"), ("cl1", "NODEONLY_T3_{'D1': 3, 'D2': 4}")],
369 }
371 cqg = dimension_clustering(config, self.qgraph, name)
372 check_cqg(cqg, answer)
374 def testClusterExtra(self):
375 """Clustering includes labels of pipetasks that aren't in QGraph.
376 They should just be ignored."""
377 name = "extra"
378 config = BpsConfig(
379 {
380 "templateDataId": "{D1}_{D2}_{D3}_{D4}",
381 "cluster": {
382 "cl1": {"pipetasks": "T1, Extra1, T2, Extra2, T3, T4", "dimensions": "D1, D2"},
383 },
384 }
385 )
386 answer = {
387 "name": name,
388 "nodes": {
389 "cl1_1_2": {
390 "label": "cl1",
391 "dims": {"D1": 1, "D2": 2},
392 "counts": {"T1": 1, "T2": 1, "T3": 1, "T4": 1},
393 },
394 "cl1_3_4": {
395 "label": "cl1",
396 "dims": {"D1": 3, "D2": 4},
397 "counts": {"T1": 1, "T2": 1, "T3": 1, "T4": 1},
398 },
399 },
400 "edges": [],
401 }
403 cqg = dimension_clustering(config, self.qgraph, name)
404 check_cqg(cqg, answer)
406 def testClusterRepeat(self):
407 """A PipelineTask appears in more than one cluster definition."""
408 config = BpsConfig(
409 {
410 "templateDataId": "{D1}_{D2}_{D3}_{D4}",
411 "cluster": {
412 "cl1": {"pipetasks": "T1, T2, T3, T4", "dimensions": "D1, D2"},
413 "cl2": {"pipetasks": "T2", "dimensions": "D1, D2"},
414 },
415 }
416 )
418 with self.assertRaises(RuntimeError):
419 _ = dimension_clustering(config, self.qgraph, "repeat-task")
421 def testClusterDepends(self):
422 """Part of a chain of PipelineTask appears in different cluster."""
423 config = BpsConfig(
424 {
425 "templateDataId": "{D1}_{D2}_{D3}_{D4}",
426 "cluster": {
427 "cl1": {"pipetasks": "T1, T3, T4", "dimensions": "D1, D2"},
428 "cl2": {"pipetasks": "T2", "dimensions": "D1, D2"},
429 },
430 }
431 )
433 with self.assertRaises(RuntimeError):
434 _ = dimension_clustering(config, self.qgraph, "task-depends")
437if __name__ == "__main__": 437 ↛ 438line 437 didn't jump to line 438, because the condition on line 437 was never true
438 unittest.main()