Coverage for tests/test_tmaUtils.py: 29%

252 statements  

« prev     ^ index     » next       coverage.py v7.3.0, created at 2023-08-15 10:03 +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 

27import vcr 

28 

29import pandas as pd 

30import numpy as np 

31import asyncio 

32import matplotlib.pyplot as plt 

33from astropy.time import TimeDelta 

34 

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) 

52 

53__all__ = [ 

54 'writeNewTmaEventTestTruthValues', 

55] 

56 

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) 

70 

71 

72def getTmaEventTestTruthValues(): 

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

74 

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

92 

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 

101 

102 

103def writeNewTmaEventTestTruthValues(): 

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

105 

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. 

109 

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 

114 

115 eventMaker = TMAEventMaker() 

116 events = eventMaker.getEvents(dayObs) 

117 

118 packageDir = getPackageDir("summit_utils") 

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

120 

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

128 

129 

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 

136 

137 

138def _turnOn(tma): 

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

140 

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

142 sets values to turn the axes on. 

143 

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 

151 

152 

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

154 

155 def test_tmaInit(self): 

156 tma = TMAStateMachine() 

157 self.assertFalse(tma._isValid) 

158 

159 # setting one axis should not make things valid 

160 tma._parts['azimuthMotionState'] = 1 

161 self.assertFalse(tma._isValid) 

162 

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) 

170 

171 def test_tmaReferences(self): 

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

173 """ 

174 tma = TMAStateMachine() 

175 

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) 

183 

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

188 

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

192 

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

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

195 

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) 

204 

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) 

213 

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) 

221 

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

231 

232 

233@safe_vcr.use_cassette() 

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

235 

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

243 

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 

249 

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

254 

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) 

260 

261 @safe_vcr.use_cassette() 

262 def test_rowDataForValues(self): 

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

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

265 

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) 

275 

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

281 

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] 

288 

289 # just running this check it is OK 

290 tma.apply(row1) 

291 tma.apply(row2) 

292 

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) 

298 

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) 

305 

306 _initializeTma(tma) 

307 

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

309 tma.apply(row) 

310 

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) 

318 

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) 

323 

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

331 

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) 

340 

341 @safe_vcr.use_cassette() 

342 def test_helperFunctions(self): 

343 eventMaker = self.tmaEventMaker 

344 events = eventMaker.getEvents(self.dayObs) 

345 

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) 

352 

353 @safe_vcr.use_cassette() 

354 def test_printing(self): 

355 eventMaker = self.tmaEventMaker 

356 events = eventMaker.getEvents(self.dayObs) 

357 

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

362 

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

369 

370 # check the full day trick works 

371 eventMaker.printFullDayStateEvolution(self.dayObs) 

372 

373 tma = TMAStateMachine() 

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

375 eventMaker.printTmaDetailedState(tma) 

376 

377 @safe_vcr.use_cassette() 

378 def test_getAxisData(self): 

379 eventMaker = self.tmaEventMaker 

380 events = eventMaker.getEvents(self.dayObs) 

381 

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

383 self.assertIsInstance(azData, pd.DataFrame) 

384 self.assertIsInstance(elData, pd.DataFrame) 

385 

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

392 

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) 

396 

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 

402 

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

404 plotEvent(self.client, event) 

405 

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) 

410 

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 

414 

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

416 

417 del fig 

418 

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 

424 

425 time = event.begin 

426 found = eventMaker.findEvent(time) 

427 self.assertEqual(found, event) 

428 

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) 

436 

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) 

441 

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) 

449 

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) 

455 

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) 

460 

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) 

467 

468 

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

470 pass 

471 

472 

473def setup_module(module): 

474 lsst.utils.tests.init() 

475 

476 

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