Coverage for tests/test_tmaUtils.py: 28%
270 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-20 10:07 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-20 10:07 +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 @vcr.use_cassette()
321 def test_noDataBehaviour(self):
322 eventMaker = self.tmaEventMaker
323 noDataDayObs = 19500101 # do not use 19700101 - there is data for that day!
324 with self.assertLogs(level='WARNING') as cm:
325 correctMsg = f"No EFD data found for dayObs={noDataDayObs}"
326 events = eventMaker.getEvents(noDataDayObs)
327 self.assertIsInstance(events, list)
328 self.assertEqual(len(events), 0)
329 msg = cm.output[0]
330 self.assertIn(correctMsg, msg)
332 @vcr.use_cassette()
333 def test_helperFunctions(self):
334 eventMaker = self.tmaEventMaker
335 events = eventMaker.getEvents(self.dayObs)
337 slews = [e for e in events if e.type == TMAState.SLEWING]
338 tracks = [e for e in events if e.type == TMAState.TRACKING]
339 foundSlews = getSlewsFromEventList(events)
340 foundTracks = getTracksFromEventList(events)
341 self.assertEqual(slews, foundSlews)
342 self.assertEqual(tracks, foundTracks)
344 @vcr.use_cassette()
345 def test_getEvent(self):
346 # test the singular event getter, and what happens if the event doesn't
347 # exist for the day
348 eventMaker = self.tmaEventMaker
349 events = eventMaker.getEvents(self.dayObs)
350 nEvents = len(events)
352 event = eventMaker.getEvent(self.dayObs, 0)
353 self.assertIsInstance(event, TMAEvent)
354 self.assertEqual(event, events[0])
355 event = eventMaker.getEvent(self.dayObs, 100)
356 self.assertIsInstance(event, TMAEvent)
357 self.assertEqual(event, events[100])
359 with self.assertLogs(level='WARNING') as cm:
360 correctMsg = f"Event {nEvents+1} not found for {self.dayObs}"
361 event = eventMaker.getEvent(self.dayObs, nEvents+1)
362 msg = cm.output[0]
363 self.assertIn(correctMsg, msg)
365 @vcr.use_cassette()
366 def test_printing(self):
367 eventMaker = self.tmaEventMaker
368 events = eventMaker.getEvents(self.dayObs)
370 # test str(), repr(), and _ipython_display_() for an event
371 print(str(events[0]))
372 print(repr(events[0]))
373 print(events[0]._ipython_display_())
375 # spot-check both a slow and a track to print
376 slews = [e for e in events if e.type == TMAState.SLEWING]
377 tracks = [e for e in events if e.type == TMAState.TRACKING]
378 eventMaker.printEventDetails(slews[0])
379 eventMaker.printEventDetails(tracks[0])
380 eventMaker.printEventDetails(events[-1])
382 # check the full day trick works
383 eventMaker.printFullDayStateEvolution(self.dayObs)
385 tma = TMAStateMachine()
386 _initializeTma(tma) # the uninitialized state contains wrong types for printing
387 eventMaker.printTmaDetailedState(tma)
389 @vcr.use_cassette()
390 def test_getAxisData(self):
391 eventMaker = self.tmaEventMaker
392 events = eventMaker.getEvents(self.dayObs)
394 azData, elData = getAzimuthElevationDataForEvent(self.client, events[0])
395 self.assertIsInstance(azData, pd.DataFrame)
396 self.assertIsInstance(elData, pd.DataFrame)
398 paddedAzData, paddedElData = getAzimuthElevationDataForEvent(self.client,
399 events[0],
400 prePadding=2,
401 postPadding=1)
402 self.assertGreater(len(paddedAzData), len(azData))
403 self.assertGreater(len(paddedElData), len(elData))
405 # just check this doesn't raise when called, and check we can pass the
406 # data in
407 plotEvent(self.client, events[0], azimuthData=azData, elevationData=elData)
409 @vcr.use_cassette()
410 def test_plottingAndCommands(self):
411 eventMaker = self.tmaEventMaker
412 events = eventMaker.getEvents(self.dayObs)
413 event = events[28] # this one has commands, and we'll check that later
415 # check we _can_ plot without a figure, and then stop doing that
416 plotEvent(self.client, event)
418 fig = plt.figure(figsize=(10, 8))
419 # just check this doesn't raise when called
420 plotEvent(self.client, event, fig=fig)
421 plt.close(fig)
423 commandsToPlot = ['raDecTarget', 'moveToTarget', 'startTracking', 'stopTracking']
424 commands = getCommandsDuringEvent(self.client, event, commandsToPlot, doLog=False)
425 self.assertTrue(not all([time is None for time in commands.values()])) # at least one command
427 plotEvent(self.client, event, fig=fig, commands=commands)
429 del fig
431 @vcr.use_cassette()
432 def test_findEvent(self):
433 eventMaker = self.tmaEventMaker
434 events = eventMaker.getEvents(self.dayObs)
435 event = events[28] # this one has a contiguous event before it
437 time = event.begin
438 found = eventMaker.findEvent(time)
439 self.assertEqual(found, event)
441 dt = TimeDelta(0.01, format='sec')
442 # must be just inside to get the same event back, because if a moment
443 # is shared it gives the one which starts with the moment (whilst
444 # logging info messages about it)
445 time = event.end - dt
446 found = eventMaker.findEvent(time)
447 self.assertEqual(found, event)
449 # now check that if we're a hair after, we don't get the same event
450 time = event.end + dt
451 found = eventMaker.findEvent(time)
452 self.assertNotEqual(found, event)
454 # Now check the cases which don't find an event at all. It would be
455 # nice to check the log messages here, but it seems too fragile to be
456 # worth it
457 dt = TimeDelta(1, format='sec')
458 tooEarlyOnDay = getDayObsStartTime(self.dayObs) + dt # 1 second after start of day
459 found = eventMaker.findEvent(tooEarlyOnDay)
460 self.assertIsNone(found)
462 # 1 second before end of day and this day does not end with an open
463 # event
464 tooLateOnDay = getDayObsStartTime(calcNextDay(self.dayObs)) - dt
465 found = eventMaker.findEvent(tooLateOnDay)
466 self.assertIsNone(found)
468 # going just inside the last event of the day should be fine
469 lastEvent = events[-1]
470 found = eventMaker.findEvent(lastEvent.end - dt)
471 self.assertEqual(found, lastEvent)
473 # going at the very end of the last event of the day should actually
474 # find nothing, because the last moment of an event isn't actually in
475 # the event itself, because of how contiguous events are defined to
476 # behave (being half-open intervals)
477 found = eventMaker.findEvent(lastEvent.end)
478 self.assertIsNone(found, lastEvent)
481class TestMemory(lsst.utils.tests.MemoryTestCase):
482 pass
485def setup_module(module):
486 lsst.utils.tests.init()
489if __name__ == "__main__": 489 ↛ 490line 489 didn't jump to line 490, because the condition on line 489 was never true
490 lsst.utils.tests.init()
491 unittest.main()