Coverage for tests/test_tmaUtils.py: 28%

273 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-01 15:39 +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 eventSet = set(slews) # check we can hash 

321 eventSet.update(slews) # check it ignores duplicates 

322 self.assertEqual(len(eventSet), len(slews)) 

323 

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) 

335 

336 @vcr.use_cassette() 

337 def test_helperFunctions(self): 

338 eventMaker = self.tmaEventMaker 

339 events = eventMaker.getEvents(self.dayObs) 

340 

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) 

347 

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) 

355 

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

362 

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) 

368 

369 @vcr.use_cassette() 

370 def test_printing(self): 

371 eventMaker = self.tmaEventMaker 

372 events = eventMaker.getEvents(self.dayObs) 

373 

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

378 

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

385 

386 # check the full day trick works 

387 eventMaker.printFullDayStateEvolution(self.dayObs) 

388 

389 tma = TMAStateMachine() 

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

391 eventMaker.printTmaDetailedState(tma) 

392 

393 @vcr.use_cassette() 

394 def test_getAxisData(self): 

395 eventMaker = self.tmaEventMaker 

396 events = eventMaker.getEvents(self.dayObs) 

397 

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

399 self.assertIsInstance(azData, pd.DataFrame) 

400 self.assertIsInstance(elData, pd.DataFrame) 

401 

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

408 

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) 

412 

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 

418 

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

420 plotEvent(self.client, event) 

421 

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) 

426 

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 

430 

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

432 

433 del fig 

434 

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 

440 

441 time = event.begin 

442 found = eventMaker.findEvent(time) 

443 self.assertEqual(found, event) 

444 

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) 

452 

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) 

457 

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) 

465 

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) 

471 

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) 

476 

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) 

483 

484 

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

486 pass 

487 

488 

489def setup_module(module): 

490 lsst.utils.tests.init() 

491 

492 

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