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