Coverage for tests/test_tmaUtils.py: 18%
236 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-06 04:34 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-06 04:34 +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)
52__all__ = [
53 'writeNewTmaEventTestTruthValues',
54]
57def getTmaEventTestTruthValues():
58 """Get the current truth values for the TMA event test cases.
60 Returns
61 -------
62 seqNums : `np.array` of `int`
63 The sequence numbers of the events.
64 startRows : `np.array` of `int`
65 The _startRow numbers of the events.
66 endRows : `np.array` of `int`
67 The _endRow numbers of the events.
68 types : `np.array` of `str`
69 The event types, as a string, i.e. the ``TMAEvent.name`` of the event's
70 ``event.type``.
71 endReasons : `np.array` of `str`
72 The event end reasons, as a string, i.e. the ``TMAEvent.name`` of the
73 event's ``event.endReason``.
74 """
75 packageDir = getPackageDir("summit_utils")
76 dataFilename = os.path.join(packageDir, "tests", "data", "tmaEventData.txt")
78 seqNums, startRows, endRows, types, endReasons = np.genfromtxt(dataFilename,
79 delimiter=',',
80 dtype=None,
81 names=True,
82 encoding='utf-8',
83 unpack=True
84 )
85 return seqNums, startRows, endRows, types, endReasons
88def writeNewTmaEventTestTruthValues():
89 """This function is used to write out the truth values for the test cases.
91 If the internal event creation logic changes, these values can change, and
92 will need to be updated. Run this function, and check the new values into
93 git.
95 Note: if you have cause to update values with this function, make sure to
96 update the version number on the TMAEvent class.
97 """
98 dayObs = 20230531 # obviously must match the day in the test class
100 eventMaker = TMAEventMaker()
101 events = eventMaker.getEvents(dayObs)
103 packageDir = getPackageDir("summit_utils")
104 dataFilename = os.path.join(packageDir, "tests", "data", "tmaEventData.txt")
106 columnHeader = "seqNum,startRow,endRow,type,endReason"
107 with open(dataFilename, 'w') as f:
108 f.write(columnHeader + '\n')
109 for event in events:
110 line = (f"{event.seqNum},{event._startRow},{event._endRow},{event.type.name},"
111 f"{event.endReason.name}")
112 f.write(line + '\n')
115def makeValid(tma):
116 """Helper function to turn a TMA into a valid state.
117 """
118 for name, value in tma._parts.items():
119 if value == tma._UNINITIALIZED_VALUE:
120 tma._parts[name] = 1
123def _turnOn(tma):
124 """Helper function to turn TMA axes on for testing.
126 Do not call directly in normal usage or code, as this just arbitrarily
127 sets values to turn the axes on.
129 Parameters
130 ----------
131 tma : `lsst.summit.utils.tmaUtils.TMAStateMachine`
132 The TMA state machine model to initialize.
133 """
134 tma._parts['azimuthSystemState'] = PowerState.ON
135 tma._parts['elevationSystemState'] = PowerState.ON
138@unittest.skip("Skipping until DM-40101 is resolved.")
139class TmaUtilsTestCase(lsst.utils.tests.TestCase):
141 def test_tmaInit(self):
142 tma = TMAStateMachine()
143 self.assertFalse(tma._isValid)
145 # setting one axis should not make things valid
146 tma._parts['azimuthMotionState'] = 1
147 self.assertFalse(tma._isValid)
149 # setting all the other components should make things valid
150 tma._parts['azimuthInPosition'] = 1
151 tma._parts['azimuthSystemState'] = 1
152 tma._parts['elevationInPosition'] = 1
153 tma._parts['elevationMotionState'] = 1
154 tma._parts['elevationSystemState'] = 1
155 self.assertTrue(tma._isValid)
157 def test_tmaReferences(self):
158 """Check the linkage between the component lists and the _parts dict.
159 """
160 tma = TMAStateMachine()
162 # setting one axis should not make things valid
163 self.assertEqual(tma._parts['azimuthMotionState'], tma._UNINITIALIZED_VALUE)
164 self.assertEqual(tma._parts['elevationMotionState'], tma._UNINITIALIZED_VALUE)
165 tma.motion[0] = AxisMotionState.TRACKING # set azimuth to 0
166 tma.motion[1] = AxisMotionState.TRACKING # set azimuth to 0
167 self.assertEqual(tma._parts['azimuthMotionState'], AxisMotionState.TRACKING)
168 self.assertEqual(tma._parts['elevationMotionState'], AxisMotionState.TRACKING)
170 def test_getAxisAndType(self):
171 # check both the long and short form names work
172 for s in ['azimuthMotionState', 'lsst.sal.MTMount.logevent_azimuthMotionState']:
173 self.assertEqual(getAxisAndType(s), ('azimuth', 'MotionState'))
175 # check in position, and use elevation instead of azimuth to test that
176 for s in ['elevationInPosition', 'lsst.sal.MTMount.logevent_elevationInPosition']:
177 self.assertEqual(getAxisAndType(s), ('elevation', 'InPosition'))
179 for s in ['azimuthSystemState', 'lsst.sal.MTMount.logevent_azimuthSystemState']:
180 self.assertEqual(getAxisAndType(s), ('azimuth', 'SystemState'))
182 def test_initStateLogic(self):
183 tma = TMAStateMachine()
184 self.assertFalse(tma._isValid)
185 self.assertFalse(tma.isMoving)
186 self.assertFalse(tma.canMove)
187 self.assertFalse(tma.isTracking)
188 self.assertFalse(tma.isSlewing)
189 self.assertEqual(tma.state, TMAState.UNINITIALIZED)
191 _initializeTma(tma) # we're valid, but still aren't moving and can't
192 self.assertTrue(tma._isValid)
193 self.assertNotEqual(tma.state, TMAState.UNINITIALIZED)
194 self.assertTrue(tma.canMove)
195 self.assertTrue(tma.isNotMoving)
196 self.assertFalse(tma.isMoving)
197 self.assertFalse(tma.isTracking)
198 self.assertFalse(tma.isSlewing)
200 _turnOn(tma) # can now move, still valid, but not in motion
201 self.assertTrue(tma._isValid)
202 self.assertTrue(tma.canMove)
203 self.assertTrue(tma.isNotMoving)
204 self.assertFalse(tma.isMoving)
205 self.assertFalse(tma.isTracking)
206 self.assertFalse(tma.isSlewing)
208 # consider manipulating the axes by hand here and testing these?
209 # it's likely not worth it, given how much this exercised elsewhere,
210 # but these are the only functions not yet being directly tested
211 # tma._axesInFault()
212 # tma._axesOff()
213 # tma._axesOn()
214 # tma._axesInMotion()
215 # tma._axesTRACKING()
216 # tma._axesInPosition()
219@unittest.skip("Skipping until DM-40101 is resolved.")
220class TMAEventMakerTestCase(lsst.utils.tests.TestCase):
221 @classmethod
222 def setUpClass(cls):
223 try:
224 cls.client = makeEfdClient()
225 except RuntimeError:
226 raise unittest.SkipTest("Could not instantiate an EFD client")
228 cls.dayObs = 20230531
229 # get a sample expRecord here to test expRecordToTimespan
230 cls.tmaEventMaker = TMAEventMaker(cls.client)
231 cls.events = cls.tmaEventMaker.getEvents(cls.dayObs) # does the fetch
232 cls.sampleData = cls.tmaEventMaker._data[cls.dayObs] # pull the data from the object and test length
234 def tearDown(self):
235 loop = asyncio.get_event_loop()
236 loop.run_until_complete(self.client.influx_client.close())
238 def test_events(self):
239 data = self.sampleData
240 self.assertIsInstance(data, pd.DataFrame)
241 self.assertEqual(len(data), 993)
243 def test_rowDataForValues(self):
244 rowsFor = set(self.sampleData['rowFor'])
245 self.assertEqual(len(rowsFor), 6)
247 # hard coding these ensures that you can't extend the axes/model
248 # without being explicit about it here.
249 correct = {'azimuthInPosition',
250 'azimuthMotionState',
251 'azimuthSystemState',
252 'elevationInPosition',
253 'elevationMotionState',
254 'elevationSystemState'}
255 self.assertSetEqual(rowsFor, correct)
257 def test_monotonicTimeInDataframe(self):
258 # ensure that each row is later than the previous
259 times = self.sampleData['private_efdStamp']
260 self.assertTrue(np.all(np.diff(times) > 0))
262 def test_monotonicTimeApplicationOfRows(self):
263 # ensure you can apply rows in the correct order
264 tma = TMAStateMachine()
265 row1 = self.sampleData.iloc[0]
266 row2 = self.sampleData.iloc[1]
268 # just running this check it is OK
269 tma.apply(row1)
270 tma.apply(row2)
272 # and that if you apply them in reverse order then things will raise
273 tma = TMAStateMachine()
274 with self.assertRaises(ValueError):
275 tma.apply(row2)
276 tma.apply(row1)
278 def test_fullDaySequence(self):
279 # make sure we can apply all the data from the day without falling
280 # through the logic sieve
281 for engineering in (True, False):
282 tma = TMAStateMachine(engineeringMode=engineering)
284 _initializeTma(tma)
286 for rowNum, row in self.sampleData.iterrows():
287 tma.apply(row)
289 def test_endToEnd(self):
290 eventMaker = self.tmaEventMaker
291 events = eventMaker.getEvents(self.dayObs)
292 self.assertIsInstance(events, list)
293 self.assertEqual(len(events), 200)
294 self.assertIsInstance(events[0], TMAEvent)
296 slews = [e for e in events if e.type == TMAState.SLEWING]
297 tracks = [e for e in events if e.type == TMAState.TRACKING]
298 self.assertEqual(len(slews), 157)
299 self.assertEqual(len(tracks), 43)
301 seqNums, startRows, endRows, types, endReasons = getTmaEventTestTruthValues()
302 for eventNum, event in enumerate(events):
303 self.assertEqual(event.seqNum, seqNums[eventNum])
304 self.assertEqual(event._startRow, startRows[eventNum])
305 self.assertEqual(event._endRow, endRows[eventNum])
306 self.assertEqual(event.type.name, types[eventNum])
307 self.assertEqual(event.endReason.name, endReasons[eventNum])
309 def test_noDataBehaviour(self):
310 eventMaker = self.tmaEventMaker
311 noDataDayObs = 19500101 # do not use 19700101 - there is data for that day!
312 with self.assertWarns(Warning, msg=f"No EFD data found for dayObs={noDataDayObs}"):
313 events = eventMaker.getEvents(noDataDayObs)
314 self.assertIsInstance(events, list)
315 self.assertEqual(len(events), 0)
317 def test_helperFunctions(self):
318 eventMaker = self.tmaEventMaker
319 events = eventMaker.getEvents(self.dayObs)
321 slews = [e for e in events if e.type == TMAState.SLEWING]
322 tracks = [e for e in events if e.type == TMAState.TRACKING]
323 foundSlews = getSlewsFromEventList(events)
324 foundTracks = getTracksFromEventList(events)
325 self.assertEqual(slews, foundSlews)
326 self.assertEqual(tracks, foundTracks)
328 def test_printing(self):
329 eventMaker = self.tmaEventMaker
330 events = eventMaker.getEvents(self.dayObs)
332 # test str(), repr(), and _ipython_display_() for an event
333 print(str(events[0]))
334 print(repr(events[0]))
335 print(events[0]._ipython_display_())
337 # spot-check both a slow and a track to print
338 slews = [e for e in events if e.type == TMAState.SLEWING]
339 tracks = [e for e in events if e.type == TMAState.TRACKING]
340 eventMaker.printEventDetails(slews[0])
341 eventMaker.printEventDetails(tracks[0])
342 eventMaker.printEventDetails(events[-1])
344 # check the full day trick works
345 eventMaker.printFullDayStateEvolution(self.dayObs)
347 tma = TMAStateMachine()
348 _initializeTma(tma) # the uninitialized state contains wrong types for printing
349 eventMaker.printTmaDetailedState(tma)
351 def test_getAxisData(self):
352 eventMaker = self.tmaEventMaker
353 events = eventMaker.getEvents(self.dayObs)
355 azData, elData = getAzimuthElevationDataForEvent(self.client, events[0])
356 self.assertIsInstance(azData, pd.DataFrame)
357 self.assertIsInstance(elData, pd.DataFrame)
359 paddedAzData, paddedElData = getAzimuthElevationDataForEvent(self.client,
360 events[0],
361 prePadding=2,
362 postPadding=1)
363 self.assertGreater(len(paddedAzData), len(azData))
364 self.assertGreater(len(paddedElData), len(elData))
366 # just check this doesn't raise when called, and check we can pass the
367 # data in
368 plotEvent(self.client, events[0], azimuthData=azData, elevationData=elData)
370 def test_plottingAndCommands(self):
371 eventMaker = self.tmaEventMaker
372 events = eventMaker.getEvents(self.dayObs)
373 event = events[28] # this one has commands, and we'll check that later
375 # check we _can_ plot without a figure, and then stop doing that
376 plotEvent(self.client, event)
378 fig = plt.figure(figsize=(10, 8))
379 # just check this doesn't raise when called
380 plotEvent(self.client, event, fig=fig)
381 plt.close(fig)
383 commandsToPlot = ['raDecTarget', 'moveToTarget', 'startTracking', 'stopTracking']
384 commands = getCommandsDuringEvent(self.client, event, commandsToPlot, doLog=False)
385 self.assertTrue(not all([time is None for time in commands.values()])) # at least one command
387 plotEvent(self.client, event, fig=fig, commands=commands)
389 del fig
391 def test_findEvent(self):
392 eventMaker = self.tmaEventMaker
393 events = eventMaker.getEvents(self.dayObs)
394 event = events[28] # this one has a contiguous event before it
396 time = event.begin
397 found = eventMaker.findEvent(time)
398 self.assertEqual(found, event)
400 dt = TimeDelta(0.01, format='sec')
401 # must be just inside to get the same event back, because if a moment
402 # is shared it gives the one which starts with the moment (whilst
403 # logging info messages about it)
404 time = event.end - dt
405 found = eventMaker.findEvent(time)
406 self.assertEqual(found, event)
408 # now check that if we're a hair after, we don't get the same event
409 time = event.end + dt
410 found = eventMaker.findEvent(time)
411 self.assertNotEqual(found, event)
413 # Now check the cases which don't find an event at all. It would be
414 # nice to check the log messages here, but it seems too fragile to be
415 # worth it
416 dt = TimeDelta(1, format='sec')
417 tooEarlyOnDay = getDayObsStartTime(self.dayObs) + dt # 1 second after start of day
418 found = eventMaker.findEvent(tooEarlyOnDay)
419 self.assertIsNone(found)
421 # 1 second before end of day and this day does not end with an open
422 # event
423 tooLateOnDay = getDayObsStartTime(calcNextDay(self.dayObs)) - dt
424 found = eventMaker.findEvent(tooLateOnDay)
425 self.assertIsNone(found)
427 # going just inside the last event of the day should be fine
428 lastEvent = events[-1]
429 found = eventMaker.findEvent(lastEvent.end - dt)
430 self.assertEqual(found, lastEvent)
432 # going at the very end of the last event of the day should actually
433 # find nothing, because the last moment of an event isn't actually in
434 # the event itself, because of how contiguous events are defined to
435 # behave (being half-open intervals)
436 found = eventMaker.findEvent(lastEvent.end)
437 self.assertIsNone(found, lastEvent)
440class TestMemory(lsst.utils.tests.MemoryTestCase):
441 pass
444def setup_module(module):
445 lsst.utils.tests.init()
448if __name__ == "__main__": 448 ↛ 449line 448 didn't jump to line 449, because the condition on line 448 was never true
449 lsst.utils.tests.init()
450 unittest.main()