Coverage for tests/test_tmaUtils.py: 18%

236 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-27 11:10 +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) 

51 

52__all__ = [ 

53 'writeNewTmaEventTestTruthValues', 

54] 

55 

56 

57def getTmaEventTestTruthValues(): 

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

59 

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

77 

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 

86 

87 

88def writeNewTmaEventTestTruthValues(): 

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

90 

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. 

94 

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 

99 

100 eventMaker = TMAEventMaker() 

101 events = eventMaker.getEvents(dayObs) 

102 

103 packageDir = getPackageDir("summit_utils") 

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

105 

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

113 

114 

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 

121 

122 

123def _turnOn(tma): 

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

125 

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

127 sets values to turn the axes on. 

128 

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 

136 

137 

138@unittest.skip("Skipping until DM-40101 is resolved.") 

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

140 

141 def test_tmaInit(self): 

142 tma = TMAStateMachine() 

143 self.assertFalse(tma._isValid) 

144 

145 # setting one axis should not make things valid 

146 tma._parts['azimuthMotionState'] = 1 

147 self.assertFalse(tma._isValid) 

148 

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) 

156 

157 def test_tmaReferences(self): 

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

159 """ 

160 tma = TMAStateMachine() 

161 

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) 

169 

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

174 

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

178 

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

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

181 

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) 

190 

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) 

199 

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) 

207 

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

217 

218 

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

227 

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 

233 

234 def tearDown(self): 

235 loop = asyncio.get_event_loop() 

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

237 

238 def test_events(self): 

239 data = self.sampleData 

240 self.assertIsInstance(data, pd.DataFrame) 

241 self.assertEqual(len(data), 993) 

242 

243 def test_rowDataForValues(self): 

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

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

246 

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) 

256 

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

261 

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] 

267 

268 # just running this check it is OK 

269 tma.apply(row1) 

270 tma.apply(row2) 

271 

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) 

277 

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) 

283 

284 _initializeTma(tma) 

285 

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

287 tma.apply(row) 

288 

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) 

295 

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) 

300 

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

308 

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) 

316 

317 def test_helperFunctions(self): 

318 eventMaker = self.tmaEventMaker 

319 events = eventMaker.getEvents(self.dayObs) 

320 

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) 

327 

328 def test_printing(self): 

329 eventMaker = self.tmaEventMaker 

330 events = eventMaker.getEvents(self.dayObs) 

331 

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

336 

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

343 

344 # check the full day trick works 

345 eventMaker.printFullDayStateEvolution(self.dayObs) 

346 

347 tma = TMAStateMachine() 

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

349 eventMaker.printTmaDetailedState(tma) 

350 

351 def test_getAxisData(self): 

352 eventMaker = self.tmaEventMaker 

353 events = eventMaker.getEvents(self.dayObs) 

354 

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

356 self.assertIsInstance(azData, pd.DataFrame) 

357 self.assertIsInstance(elData, pd.DataFrame) 

358 

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

365 

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) 

369 

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 

374 

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

376 plotEvent(self.client, event) 

377 

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) 

382 

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 

386 

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

388 

389 del fig 

390 

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 

395 

396 time = event.begin 

397 found = eventMaker.findEvent(time) 

398 self.assertEqual(found, event) 

399 

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) 

407 

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) 

412 

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) 

420 

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) 

426 

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) 

431 

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) 

438 

439 

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

441 pass 

442 

443 

444def setup_module(module): 

445 lsst.utils.tests.init() 

446 

447 

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