Coverage for tests/test_tmaUtils.py: 28%
273 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-08 12:27 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-08 12:27 +0000
1# This file is part of summit_utils.
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/>.
22"""Test cases for utils."""
24import unittest
25import os
26import lsst.utils.tests
28import pandas as pd
29import numpy as np
30import asyncio
31import matplotlib.pyplot as plt
32from astropy.time import TimeDelta
34from lsst.utils import getPackageDir
35from lsst.summit.utils.enums import PowerState
36from lsst.summit.utils.efdUtils import makeEfdClient, getDayObsStartTime, calcNextDay
37from lsst.summit.utils.tmaUtils import (
38 getSlewsFromEventList,
39 getTracksFromEventList,
40 getAzimuthElevationDataForEvent,
41 plotEvent,
42 getCommandsDuringEvent,
43 TMAStateMachine,
44 TMAEvent,
45 TMAEventMaker,
46 TMAState,
47 AxisMotionState,
48 getAxisAndType,
49 _initializeTma,
50)
51from utils import getVcr
53__all__ = [
54 'writeNewTmaEventTestTruthValues',
55]
57vcr = getVcr()
60def getTmaEventTestTruthValues():
61 """Get the current truth values for the TMA event test cases.
63 Returns
64 -------
65 seqNums : `np.array` of `int`
66 The sequence numbers of the events.
67 startRows : `np.array` of `int`
68 The _startRow numbers of the events.
69 endRows : `np.array` of `int`
70 The _endRow numbers of the events.
71 types : `np.array` of `str`
72 The event types, as a string, i.e. the ``TMAEvent.name`` of the event's
73 ``event.type``.
74 endReasons : `np.array` of `str`
75 The event end reasons, as a string, i.e. the ``TMAEvent.name`` of the
76 event's ``event.endReason``.
77 """
78 packageDir = getPackageDir("summit_utils")
79 dataFilename = os.path.join(packageDir, "tests", "data", "tmaEventData.txt")
81 seqNums, startRows, endRows, types, endReasons = np.genfromtxt(dataFilename,
82 delimiter=',',
83 dtype=None,
84 names=True,
85 encoding='utf-8',
86 unpack=True
87 )
88 return seqNums, startRows, endRows, types, endReasons
91def writeNewTmaEventTestTruthValues():
92 """This function is used to write out the truth values for the test cases.
94 If the internal event creation logic changes, these values can change, and
95 will need to be updated. Run this function, and check the new values into
96 git.
98 Note: if you have cause to update values with this function, make sure to
99 update the version number on the TMAEvent class.
100 """
101 dayObs = 20230531 # obviously must match the day in the test class
103 eventMaker = TMAEventMaker()
104 events = eventMaker.getEvents(dayObs)
106 packageDir = getPackageDir("summit_utils")
107 dataFilename = os.path.join(packageDir, "tests", "data", "tmaEventData.txt")
109 columnHeader = "seqNum,startRow,endRow,type,endReason"
110 with open(dataFilename, 'w') as f:
111 f.write(columnHeader + '\n')
112 for event in events:
113 line = (f"{event.seqNum},{event._startRow},{event._endRow},{event.type.name},"
114 f"{event.endReason.name}")
115 f.write(line + '\n')
118def makeValid(tma):
119 """Helper function to turn a TMA into a valid state.
120 """
121 for name, value in tma._parts.items():
122 if value == tma._UNINITIALIZED_VALUE:
123 tma._parts[name] = 1
126def _turnOn(tma):
127 """Helper function to turn TMA axes on for testing.
129 Do not call directly in normal usage or code, as this just arbitrarily
130 sets values to turn the axes on.
132 Parameters
133 ----------
134 tma : `lsst.summit.utils.tmaUtils.TMAStateMachine`
135 The TMA state machine model to initialize.
136 """
137 tma._parts['azimuthSystemState'] = PowerState.ON
138 tma._parts['elevationSystemState'] = PowerState.ON
141class TmaUtilsTestCase(lsst.utils.tests.TestCase):
143 def test_tmaInit(self):
144 tma = TMAStateMachine()
145 self.assertFalse(tma._isValid)
147 # setting one axis should not make things valid
148 tma._parts['azimuthMotionState'] = 1
149 self.assertFalse(tma._isValid)
151 # setting all the other components should make things valid
152 tma._parts['azimuthInPosition'] = 1
153 tma._parts['azimuthSystemState'] = 1
154 tma._parts['elevationInPosition'] = 1
155 tma._parts['elevationMotionState'] = 1
156 tma._parts['elevationSystemState'] = 1
157 self.assertTrue(tma._isValid)
159 def test_tmaReferences(self):
160 """Check the linkage between the component lists and the _parts dict.
161 """
162 tma = TMAStateMachine()
164 # setting one axis should not make things valid
165 self.assertEqual(tma._parts['azimuthMotionState'], tma._UNINITIALIZED_VALUE)
166 self.assertEqual(tma._parts['elevationMotionState'], tma._UNINITIALIZED_VALUE)
167 tma.motion[0] = AxisMotionState.TRACKING # set azimuth to 0
168 tma.motion[1] = AxisMotionState.TRACKING # set azimuth to 0
169 self.assertEqual(tma._parts['azimuthMotionState'], AxisMotionState.TRACKING)
170 self.assertEqual(tma._parts['elevationMotionState'], AxisMotionState.TRACKING)
172 def test_getAxisAndType(self):
173 # check both the long and short form names work
174 for s in ['azimuthMotionState', 'lsst.sal.MTMount.logevent_azimuthMotionState']:
175 self.assertEqual(getAxisAndType(s), ('azimuth', 'MotionState'))
177 # check in position, and use elevation instead of azimuth to test that
178 for s in ['elevationInPosition', 'lsst.sal.MTMount.logevent_elevationInPosition']:
179 self.assertEqual(getAxisAndType(s), ('elevation', 'InPosition'))
181 for s in ['azimuthSystemState', 'lsst.sal.MTMount.logevent_azimuthSystemState']:
182 self.assertEqual(getAxisAndType(s), ('azimuth', 'SystemState'))
184 def test_initStateLogic(self):
185 tma = TMAStateMachine()
186 self.assertFalse(tma._isValid)
187 self.assertFalse(tma.isMoving)
188 self.assertFalse(tma.canMove)
189 self.assertFalse(tma.isTracking)
190 self.assertFalse(tma.isSlewing)
191 self.assertEqual(tma.state, TMAState.UNINITIALIZED)
193 _initializeTma(tma) # we're valid, but still aren't moving and can't
194 self.assertTrue(tma._isValid)
195 self.assertNotEqual(tma.state, TMAState.UNINITIALIZED)
196 self.assertTrue(tma.canMove)
197 self.assertTrue(tma.isNotMoving)
198 self.assertFalse(tma.isMoving)
199 self.assertFalse(tma.isTracking)
200 self.assertFalse(tma.isSlewing)
202 _turnOn(tma) # can now move, still valid, but not in motion
203 self.assertTrue(tma._isValid)
204 self.assertTrue(tma.canMove)
205 self.assertTrue(tma.isNotMoving)
206 self.assertFalse(tma.isMoving)
207 self.assertFalse(tma.isTracking)
208 self.assertFalse(tma.isSlewing)
210 # consider manipulating the axes by hand here and testing these?
211 # it's likely not worth it, given how much this exercised elsewhere,
212 # but these are the only functions not yet being directly tested
213 # tma._axesInFault()
214 # tma._axesOff()
215 # tma._axesOn()
216 # tma._axesInMotion()
217 # tma._axesTRACKING()
218 # tma._axesInPosition()
221@vcr.use_cassette()
222class TMAEventMakerTestCase(lsst.utils.tests.TestCase):
224 @classmethod
225 @vcr.use_cassette()
226 def setUpClass(cls):
227 try:
228 cls.client = makeEfdClient(testing=True)
229 except RuntimeError:
230 raise unittest.SkipTest("Could not instantiate an EFD client")
232 cls.dayObs = 20230531
233 # get a sample expRecord here to test expRecordToTimespan
234 cls.tmaEventMaker = TMAEventMaker(cls.client)
235 cls.events = cls.tmaEventMaker.getEvents(cls.dayObs) # does the fetch
236 cls.sampleData = cls.tmaEventMaker._data[cls.dayObs] # pull the data from the object and test length
238 @vcr.use_cassette()
239 def tearDown(self):
240 loop = asyncio.get_event_loop()
241 loop.run_until_complete(self.client.influx_client.close())
243 @vcr.use_cassette()
244 def test_events(self):
245 data = self.sampleData
246 self.assertIsInstance(data, pd.DataFrame)
247 self.assertEqual(len(data), 993)
249 @vcr.use_cassette()
250 def test_rowDataForValues(self):
251 rowsFor = set(self.sampleData['rowFor'])
252 self.assertEqual(len(rowsFor), 6)
254 # hard coding these ensures that you can't extend the axes/model
255 # without being explicit about it here.
256 correct = {'azimuthInPosition',
257 'azimuthMotionState',
258 'azimuthSystemState',
259 'elevationInPosition',
260 'elevationMotionState',
261 'elevationSystemState'}
262 self.assertSetEqual(rowsFor, correct)
264 @vcr.use_cassette()
265 def test_monotonicTimeInDataframe(self):
266 # ensure that each row is later than the previous
267 times = self.sampleData['private_efdStamp']
268 self.assertTrue(np.all(np.diff(times) > 0))
270 @vcr.use_cassette()
271 def test_monotonicTimeApplicationOfRows(self):
272 # ensure you can apply rows in the correct order
273 tma = TMAStateMachine()
274 row1 = self.sampleData.iloc[0]
275 row2 = self.sampleData.iloc[1]
277 # just running this check it is OK
278 tma.apply(row1)
279 tma.apply(row2)
281 # and that if you apply them in reverse order then things will raise
282 tma = TMAStateMachine()
283 with self.assertRaises(ValueError):
284 tma.apply(row2)
285 tma.apply(row1)
287 @vcr.use_cassette()
288 def test_fullDaySequence(self):
289 # make sure we can apply all the data from the day without falling
290 # through the logic sieve
291 for engineering in (True, False):
292 tma = TMAStateMachine(engineeringMode=engineering)
294 _initializeTma(tma)
296 for rowNum, row in self.sampleData.iterrows():
297 tma.apply(row)
299 @vcr.use_cassette()
300 def test_endToEnd(self):
301 eventMaker = self.tmaEventMaker
302 events = eventMaker.getEvents(self.dayObs)
303 self.assertIsInstance(events, list)
304 self.assertEqual(len(events), 200)
305 self.assertIsInstance(events[0], TMAEvent)
307 slews = [e for e in events if e.type == TMAState.SLEWING]
308 tracks = [e for e in events if e.type == TMAState.TRACKING]
309 self.assertEqual(len(slews), 157)
310 self.assertEqual(len(tracks), 43)
312 seqNums, startRows, endRows, types, endReasons = getTmaEventTestTruthValues()
313 for eventNum, event in enumerate(events):
314 self.assertEqual(event.seqNum, seqNums[eventNum])
315 self.assertEqual(event._startRow, startRows[eventNum])
316 self.assertEqual(event._endRow, endRows[eventNum])
317 self.assertEqual(event.type.name, types[eventNum])
318 self.assertEqual(event.endReason.name, endReasons[eventNum])
320 eventSet = set(slews) # check we can hash
321 eventSet.update(slews) # check it ignores duplicates
322 self.assertEqual(len(eventSet), len(slews))
324 @vcr.use_cassette()
325 def test_noDataBehaviour(self):
326 eventMaker = self.tmaEventMaker
327 noDataDayObs = 19500101 # do not use 19700101 - there is data for that day!
328 with self.assertLogs(level='WARNING') as cm:
329 correctMsg = f"No EFD data found for dayObs={noDataDayObs}"
330 events = eventMaker.getEvents(noDataDayObs)
331 self.assertIsInstance(events, list)
332 self.assertEqual(len(events), 0)
333 msg = cm.output[0]
334 self.assertIn(correctMsg, msg)
336 @vcr.use_cassette()
337 def test_helperFunctions(self):
338 eventMaker = self.tmaEventMaker
339 events = eventMaker.getEvents(self.dayObs)
341 slews = [e for e in events if e.type == TMAState.SLEWING]
342 tracks = [e for e in events if e.type == TMAState.TRACKING]
343 foundSlews = getSlewsFromEventList(events)
344 foundTracks = getTracksFromEventList(events)
345 self.assertEqual(slews, foundSlews)
346 self.assertEqual(tracks, foundTracks)
348 @vcr.use_cassette()
349 def test_getEvent(self):
350 # test the singular event getter, and what happens if the event doesn't
351 # exist for the day
352 eventMaker = self.tmaEventMaker
353 events = eventMaker.getEvents(self.dayObs)
354 nEvents = len(events)
356 event = eventMaker.getEvent(self.dayObs, 0)
357 self.assertIsInstance(event, TMAEvent)
358 self.assertEqual(event, events[0])
359 event = eventMaker.getEvent(self.dayObs, 100)
360 self.assertIsInstance(event, TMAEvent)
361 self.assertEqual(event, events[100])
363 with self.assertLogs(level='WARNING') as cm:
364 correctMsg = f"Event {nEvents+1} not found for {self.dayObs}"
365 event = eventMaker.getEvent(self.dayObs, nEvents+1)
366 msg = cm.output[0]
367 self.assertIn(correctMsg, msg)
369 @vcr.use_cassette()
370 def test_printing(self):
371 eventMaker = self.tmaEventMaker
372 events = eventMaker.getEvents(self.dayObs)
374 # test str(), repr(), and _ipython_display_() for an event
375 print(str(events[0]))
376 print(repr(events[0]))
377 print(events[0]._ipython_display_())
379 # spot-check both a slow and a track to print
380 slews = [e for e in events if e.type == TMAState.SLEWING]
381 tracks = [e for e in events if e.type == TMAState.TRACKING]
382 eventMaker.printEventDetails(slews[0])
383 eventMaker.printEventDetails(tracks[0])
384 eventMaker.printEventDetails(events[-1])
386 # check the full day trick works
387 eventMaker.printFullDayStateEvolution(self.dayObs)
389 tma = TMAStateMachine()
390 _initializeTma(tma) # the uninitialized state contains wrong types for printing
391 eventMaker.printTmaDetailedState(tma)
393 @vcr.use_cassette()
394 def test_getAxisData(self):
395 eventMaker = self.tmaEventMaker
396 events = eventMaker.getEvents(self.dayObs)
398 azData, elData = getAzimuthElevationDataForEvent(self.client, events[0])
399 self.assertIsInstance(azData, pd.DataFrame)
400 self.assertIsInstance(elData, pd.DataFrame)
402 paddedAzData, paddedElData = getAzimuthElevationDataForEvent(self.client,
403 events[0],
404 prePadding=2,
405 postPadding=1)
406 self.assertGreater(len(paddedAzData), len(azData))
407 self.assertGreater(len(paddedElData), len(elData))
409 # just check this doesn't raise when called, and check we can pass the
410 # data in
411 plotEvent(self.client, events[0], azimuthData=azData, elevationData=elData)
413 @vcr.use_cassette()
414 def test_plottingAndCommands(self):
415 eventMaker = self.tmaEventMaker
416 events = eventMaker.getEvents(self.dayObs)
417 event = events[28] # this one has commands, and we'll check that later
419 # check we _can_ plot without a figure, and then stop doing that
420 plotEvent(self.client, event)
422 fig = plt.figure(figsize=(10, 8))
423 # just check this doesn't raise when called
424 plotEvent(self.client, event, fig=fig)
425 plt.close(fig)
427 commandsToPlot = ['raDecTarget', 'moveToTarget', 'startTracking', 'stopTracking']
428 commands = getCommandsDuringEvent(self.client, event, commandsToPlot, doLog=False)
429 self.assertTrue(not all([time is None for time in commands.values()])) # at least one command
431 plotEvent(self.client, event, fig=fig, commands=commands)
433 del fig
435 @vcr.use_cassette()
436 def test_findEvent(self):
437 eventMaker = self.tmaEventMaker
438 events = eventMaker.getEvents(self.dayObs)
439 event = events[28] # this one has a contiguous event before it
441 time = event.begin
442 found = eventMaker.findEvent(time)
443 self.assertEqual(found, event)
445 dt = TimeDelta(0.01, format='sec')
446 # must be just inside to get the same event back, because if a moment
447 # is shared it gives the one which starts with the moment (whilst
448 # logging info messages about it)
449 time = event.end - dt
450 found = eventMaker.findEvent(time)
451 self.assertEqual(found, event)
453 # now check that if we're a hair after, we don't get the same event
454 time = event.end + dt
455 found = eventMaker.findEvent(time)
456 self.assertNotEqual(found, event)
458 # Now check the cases which don't find an event at all. It would be
459 # nice to check the log messages here, but it seems too fragile to be
460 # worth it
461 dt = TimeDelta(1, format='sec')
462 tooEarlyOnDay = getDayObsStartTime(self.dayObs) + dt # 1 second after start of day
463 found = eventMaker.findEvent(tooEarlyOnDay)
464 self.assertIsNone(found)
466 # 1 second before end of day and this day does not end with an open
467 # event
468 tooLateOnDay = getDayObsStartTime(calcNextDay(self.dayObs)) - dt
469 found = eventMaker.findEvent(tooLateOnDay)
470 self.assertIsNone(found)
472 # going just inside the last event of the day should be fine
473 lastEvent = events[-1]
474 found = eventMaker.findEvent(lastEvent.end - dt)
475 self.assertEqual(found, lastEvent)
477 # going at the very end of the last event of the day should actually
478 # find nothing, because the last moment of an event isn't actually in
479 # the event itself, because of how contiguous events are defined to
480 # behave (being half-open intervals)
481 found = eventMaker.findEvent(lastEvent.end)
482 self.assertIsNone(found, lastEvent)
485class TestMemory(lsst.utils.tests.MemoryTestCase):
486 pass
489def setup_module(module):
490 lsst.utils.tests.init()
493if __name__ == "__main__": 493 ↛ 494line 493 didn't jump to line 494, because the condition on line 493 was never true
494 lsst.utils.tests.init()
495 unittest.main()