Coverage for tests/test_tmaUtils.py: 28%

270 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-24 12:17 +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/>. 

21 

22"""Test cases for utils.""" 

23 

24import unittest 

25import os 

26import lsst.utils.tests 

27 

28import pandas as pd 

29import numpy as np 

30import asyncio 

31import matplotlib.pyplot as plt 

32from astropy.time import TimeDelta 

33 

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 

52 

53__all__ = [ 

54 'writeNewTmaEventTestTruthValues', 

55] 

56 

57vcr = getVcr() 

58 

59 

60def getTmaEventTestTruthValues(): 

61 """Get the current truth values for the TMA event test cases. 

62 

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") 

80 

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 

89 

90 

91def writeNewTmaEventTestTruthValues(): 

92 """This function is used to write out the truth values for the test cases. 

93 

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. 

97 

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 

102 

103 eventMaker = TMAEventMaker() 

104 events = eventMaker.getEvents(dayObs) 

105 

106 packageDir = getPackageDir("summit_utils") 

107 dataFilename = os.path.join(packageDir, "tests", "data", "tmaEventData.txt") 

108 

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') 

116 

117 

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 

124 

125 

126def _turnOn(tma): 

127 """Helper function to turn TMA axes on for testing. 

128 

129 Do not call directly in normal usage or code, as this just arbitrarily 

130 sets values to turn the axes on. 

131 

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 

139 

140 

141class TmaUtilsTestCase(lsst.utils.tests.TestCase): 

142 

143 def test_tmaInit(self): 

144 tma = TMAStateMachine() 

145 self.assertFalse(tma._isValid) 

146 

147 # setting one axis should not make things valid 

148 tma._parts['azimuthMotionState'] = 1 

149 self.assertFalse(tma._isValid) 

150 

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) 

158 

159 def test_tmaReferences(self): 

160 """Check the linkage between the component lists and the _parts dict. 

161 """ 

162 tma = TMAStateMachine() 

163 

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) 

171 

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')) 

176 

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')) 

180 

181 for s in ['azimuthSystemState', 'lsst.sal.MTMount.logevent_azimuthSystemState']: 

182 self.assertEqual(getAxisAndType(s), ('azimuth', 'SystemState')) 

183 

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) 

192 

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) 

201 

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) 

209 

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() 

219 

220 

221@vcr.use_cassette() 

222class TMAEventMakerTestCase(lsst.utils.tests.TestCase): 

223 

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") 

231 

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 

237 

238 @vcr.use_cassette() 

239 def tearDown(self): 

240 loop = asyncio.get_event_loop() 

241 loop.run_until_complete(self.client.influx_client.close()) 

242 

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) 

248 

249 @vcr.use_cassette() 

250 def test_rowDataForValues(self): 

251 rowsFor = set(self.sampleData['rowFor']) 

252 self.assertEqual(len(rowsFor), 6) 

253 

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) 

263 

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)) 

269 

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] 

276 

277 # just running this check it is OK 

278 tma.apply(row1) 

279 tma.apply(row2) 

280 

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) 

286 

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) 

293 

294 _initializeTma(tma) 

295 

296 for rowNum, row in self.sampleData.iterrows(): 

297 tma.apply(row) 

298 

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) 

306 

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) 

311 

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]) 

319 

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) 

331 

332 @vcr.use_cassette() 

333 def test_helperFunctions(self): 

334 eventMaker = self.tmaEventMaker 

335 events = eventMaker.getEvents(self.dayObs) 

336 

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) 

343 

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) 

351 

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]) 

358 

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) 

364 

365 @vcr.use_cassette() 

366 def test_printing(self): 

367 eventMaker = self.tmaEventMaker 

368 events = eventMaker.getEvents(self.dayObs) 

369 

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_()) 

374 

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]) 

381 

382 # check the full day trick works 

383 eventMaker.printFullDayStateEvolution(self.dayObs) 

384 

385 tma = TMAStateMachine() 

386 _initializeTma(tma) # the uninitialized state contains wrong types for printing 

387 eventMaker.printTmaDetailedState(tma) 

388 

389 @vcr.use_cassette() 

390 def test_getAxisData(self): 

391 eventMaker = self.tmaEventMaker 

392 events = eventMaker.getEvents(self.dayObs) 

393 

394 azData, elData = getAzimuthElevationDataForEvent(self.client, events[0]) 

395 self.assertIsInstance(azData, pd.DataFrame) 

396 self.assertIsInstance(elData, pd.DataFrame) 

397 

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)) 

404 

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) 

408 

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 

414 

415 # check we _can_ plot without a figure, and then stop doing that 

416 plotEvent(self.client, event) 

417 

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) 

422 

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 

426 

427 plotEvent(self.client, event, fig=fig, commands=commands) 

428 

429 del fig 

430 

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 

436 

437 time = event.begin 

438 found = eventMaker.findEvent(time) 

439 self.assertEqual(found, event) 

440 

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) 

448 

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) 

453 

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) 

461 

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) 

467 

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) 

472 

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) 

479 

480 

481class TestMemory(lsst.utils.tests.MemoryTestCase): 

482 pass 

483 

484 

485def setup_module(module): 

486 lsst.utils.tests.init() 

487 

488 

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()