Coverage for tests/test_spectraction.py: 98%

407 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-07 01:54 -0800

1#!/usr/bin/env python 

2# This file is part of atmospec. 

3# 

4# Developed for the LSST Data Management System. 

5# This product includes software developed by the LSST Project 

6# (https://www.lsst.org). 

7# See the COPYRIGHT file at the top-level directory of this distribution 

8# for details of code ownership. 

9# 

10# This program is free software: you can redistribute it and/or modify 

11# it under the terms of the GNU General Public License as published by 

12# the Free Software Foundation, either version 3 of the License, or 

13# (at your option) any later version. 

14# 

15# This program is distributed in the hope that it will be useful, 

16# but WITHOUT ANY WARRANTY; without even the implied warranty of 

17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

18# GNU General Public License for more details. 

19# 

20# You should have received a copy of the GNU General Public License 

21# along with this program. If not, see <https://www.gnu.org/licenses/>. 

22"""Test cases for the atmospec SpectractorShim.""" 

23 

24import functools 

25import importlib.resources 

26import os 

27import shutil 

28import tempfile 

29import unittest 

30from unittest import mock 

31 

32import astropy.config 

33import numpy as np 

34 

35import lsst.utils 

36import lsst.utils.tests 

37import lsst.afw.coord as afwCoord 

38import lsst.afw.image as afwImage 

39import lsst.geom as geom 

40from lsst.daf.base import DateTime 

41 

42from spectractor import parameters 

43from spectractor.extractor.images import Image 

44import lsst.atmospec.spectraction as spectractionModule 

45from lsst.atmospec.spectraction import SpectractorShim, Spectraction 

46 

47HD1_SIMBAD_TABLE = os.path.join(os.path.dirname(__file__), "data", "HD1.ecsv") 

48 

49 

50def withSimbadCache(func): 

51 """Serve the HD1 Simbad lookup from a committed table, without network. 

52 

53 Spectractor caches Simbad results as ``<label>.ecsv`` files under 

54 ``astropy.config.get_cache_dir()/astroquery/Simbad`` and skips the query 

55 when that file exists. The astropy cache dir is pointed at a temporary 

56 directory seeded with ``tests/data/HD1.ecsv``, so the test neither queries 

57 Simbad nor reads or writes the user's real cache. 

58 """ 

59 @functools.wraps(func) 

60 def wrapper(self, *args, **kwargs): 

61 with tempfile.TemporaryDirectory() as tmpDir, astropy.config.set_temp_cache(tmpDir): 

62 simbadDir = os.path.join(astropy.config.get_cache_dir(), "astroquery", "Simbad") 

63 os.makedirs(simbadDir) 

64 shutil.copy(HD1_SIMBAD_TABLE, simbadDir) 

65 return func(self, *args, **kwargs) 

66 return wrapper 

67 

68 

69def _makeExposure(width=20, height=24, physicalFilter="empty~holo4_003", 

70 weather=None, withMetadata=True): 

71 """Build a small ExposureF with a populated VisitInfo and filter.""" 

72 exp = afwImage.ExposureF(width, height) 

73 exp.image.array[:] = np.random.rand(height, width).astype(np.float32) + 10.0 

74 exp.variance.array[:] = 1.0 

75 

76 if weather is None: 

77 weather = afwCoord.Weather(10.0, 74300.0, 40.0) 

78 

79 vi = afwImage.VisitInfo( 

80 exposureTime=30.0, 

81 date=DateTime("2021-01-01T00:00:00Z", DateTime.UTC), 

82 boresightRaDec=geom.SpherePoint(30.0 * geom.degrees, -30.0 * geom.degrees), 

83 boresightAzAlt=geom.SpherePoint(0.0 * geom.degrees, 80.0 * geom.degrees), 

84 boresightAirmass=1.1, 

85 boresightRotAngle=45.0 * geom.degrees, 

86 weather=weather, 

87 ) 

88 exp.getInfo().setVisitInfo(vi) 

89 exp.setFilter(afwImage.FilterLabel(physical=physicalFilter, band="white")) 

90 if withMetadata: 90 ↛ 95line 90 didn't jump to line 95 because the condition on line 90 was always true

91 md = exp.getMetadata() 

92 md['GRATING'] = 'holo4_003' 

93 md['AIRMASS'] = 1.1 

94 md['DATE'] = '2021-01-01T00:00:00' 

95 return exp 

96 

97 

98def _makeSpectractorImage(shape=(10, 12)): 

99 """Make a real (blank) Spectractor Image usable by helper methods.""" 

100 image = Image(file_name='', target_label='', disperser_label='', filter_label='') 

101 image.data = np.ones(shape) 

102 return image 

103 

104 

105class SpectractionStaticTestCase(lsst.utils.tests.TestCase): 

106 """Tests for the pure/static helper methods.""" 

107 

108 def testImport(self): 

109 import lsst.atmospec.spectraction as s # noqa: F401 

110 

111 def test_flipImageLeftRight(self): 

112 image = mock.MagicMock() 

113 data = np.arange(12).reshape(3, 4) 

114 image.data = data.copy() 

115 outImage, xpos, ypos = SpectractorShim.flipImageLeftRight(image, 1.0, 2.0) 

116 # data flipped along axis 1 

117 np.testing.assert_array_equal(outImage.data, np.flip(data, 1)) 

118 self.assertEqual(xpos, data.shape[1] - 1.0) 

119 self.assertEqual(ypos, 2.0) 

120 

121 def test_transposeCentroid(self): 

122 image = mock.MagicMock() 

123 image.data = np.ones((5, 7)) # xSize, ySize = 5, 7 

124 newX, newY = SpectractorShim.transposeCentroid(2.0, 3.0, image) 

125 self.assertEqual(newX, 3.0) # dmYpos 

126 self.assertEqual(newY, 5 - 2.0) # xSize - dmXpos 

127 

128 def test_dumpParameters(self): 

129 # Should simply print without error. 

130 SpectractorShim.dumpParameters() 

131 

132 

133class SpectractionParameterTestCase(lsst.utils.tests.TestCase): 

134 """Tests for the parameter (over/supplement/reset) machinery.""" 

135 

136 def setUp(self): 

137 self.shim = SpectractorShim() 

138 

139 def test_overrideParametersValid(self): 

140 original = parameters.VERBOSE 

141 try: 

142 self.shim.overrideParameters({'VERBOSE': True}) 

143 self.assertTrue(parameters.VERBOSE) 

144 finally: 

145 parameters.VERBOSE = original 

146 

147 def test_overrideParametersInvalidRaises(self): 

148 with self.assertRaises(RuntimeError): 

149 self.shim.overrideParameters({'THIS_PARAM_DOES_NOT_EXIST_XYZ': 1}) 

150 

151 def test_supplementParametersNew(self): 

152 name = 'MY_SUPPLEMENTARY_PARAM_XYZ' 

153 self.assertNotIn(name, dir(parameters)) 

154 try: 

155 self.shim.supplementParameters({name: 42}) 

156 self.assertEqual(getattr(parameters, name), 42) 

157 finally: 

158 if hasattr(parameters, name): 158 ↛ exitline 158 didn't return from function 'test_supplementParametersNew' because the condition on line 158 was always true

159 delattr(parameters, name) 

160 

161 def test_supplementParametersExistingWarns(self): 

162 # Existing key should NOT be modified, only a warning emitted. 

163 original = parameters.VERBOSE 

164 try: 

165 self.shim.supplementParameters({'VERBOSE': not original}) 

166 self.assertEqual(parameters.VERBOSE, original) 

167 finally: 

168 parameters.VERBOSE = original 

169 

170 def test_resetParameters(self): 

171 name = 'MY_RESET_PARAM_XYZ' 

172 try: 

173 self.shim.resetParameters({name: 7}) 

174 self.assertEqual(getattr(parameters, name), 7) 

175 # reset also works for existing attributes 

176 original = parameters.VERBOSE 

177 self.shim.resetParameters({'VERBOSE': not original}) 

178 self.assertEqual(parameters.VERBOSE, not original) 

179 parameters.VERBOSE = original 

180 finally: 

181 if hasattr(parameters, name): 181 ↛ exitline 181 didn't return from function 'test_resetParameters' because the condition on line 181 was always true

182 delattr(parameters, name) 

183 

184 

185class SpectractionInitTestCase(lsst.utils.tests.TestCase): 

186 """Tests for the SpectractorShim constructor.""" 

187 

188 @classmethod 

189 def setUpClass(cls): 

190 with importlib.resources.path("lsst.atmospec", "resources/config/auxtel.ini") as cfg: 

191 cls.configFile = str(cfg) 

192 

193 def test_initDefault(self): 

194 shim = SpectractorShim() 

195 self.assertIsNotNone(shim.log) 

196 

197 def test_initWithConfigFile(self): 

198 shim = SpectractorShim(configFile=self.configFile) 

199 self.assertIsNotNone(shim.log) 

200 

201 def test_initWithParamDicts(self): 

202 supName = 'INIT_SUPP_PARAM_XYZ' 

203 resName = 'INIT_RESET_PARAM_XYZ' 

204 original = parameters.VERBOSE 

205 try: 

206 shim = SpectractorShim( 

207 paramOverrides={'VERBOSE': True}, 

208 supplementaryParameters={supName: 1}, 

209 resetParameters={resName: 2}, 

210 ) 

211 self.assertIsNotNone(shim.log) 

212 self.assertTrue(parameters.VERBOSE) 

213 self.assertEqual(getattr(parameters, supName), 1) 

214 self.assertEqual(getattr(parameters, resName), 2) 

215 finally: 

216 parameters.VERBOSE = original 

217 for name in (supName, resName): 

218 if hasattr(parameters, name): 218 ↛ 217line 218 didn't jump to line 217 because the condition on line 218 was always true

219 delattr(parameters, name) 

220 

221 def test_initWithDebug(self): 

222 original = parameters.DEBUG 

223 try: 

224 parameters.DEBUG = True 

225 shim = SpectractorShim() 

226 self.assertIsNotNone(shim.log) 

227 finally: 

228 parameters.DEBUG = original 

229 

230 

231class SpectractionHelperTestCase(lsst.utils.tests.TestCase): 

232 """Tests for the small IO / image helper methods.""" 

233 

234 def setUp(self): 

235 self.shim = SpectractorShim() 

236 

237 def test_makePathPlotting(self): 

238 with tempfile.TemporaryDirectory() as tmp: 

239 target = os.path.join(tmp, 'sub') 

240 self.shim._makePath(target, plotting=True) 

241 self.assertTrue(os.path.exists(os.path.join(target, 'plots'))) 

242 

243 def test_makePathNoPlotting(self): 

244 with tempfile.TemporaryDirectory() as tmp: 

245 target = os.path.join(tmp, 'sub2') 

246 self.shim._makePath(target, plotting=False) 

247 self.assertTrue(os.path.exists(target)) 

248 # Calling again when it already exists should be a no-op. 

249 self.shim._makePath(target, plotting=False) 

250 self.assertTrue(os.path.exists(target)) 

251 

252 def test_ensureFitsHeaderAdds(self): 

253 obj = _makeSpectractorImage() 

254 if 'SIMPLE' in obj.header: 254 ↛ 255line 254 didn't jump to line 255 because the condition on line 254 was never true

255 del obj.header['SIMPLE'] 

256 self.shim._ensureFitsHeader(obj) 

257 self.assertIn('SIMPLE', obj.header) 

258 

259 def test_ensureFitsHeaderAlreadyPresent(self): 

260 obj = _makeSpectractorImage() 

261 obj.header['SIMPLE'] = True 

262 self.shim._ensureFitsHeader(obj) 

263 self.assertIn('SIMPLE', obj.header) 

264 

265 def test_getImageData(self): 

266 exp = _makeExposure() 

267 data = self.shim._getImageData(exp) 

268 # transposed relative to exp.image.array 

269 self.assertEqual(data.shape, exp.image.array.T.shape) 

270 

271 def test_getImageDataTrimToSquare(self): 

272 exp = _makeExposure() 

273 data = self.shim._getImageData(exp, trimToSquare=True) 

274 # image is smaller than 4000, so trimming leaves it unchanged (then .T) 

275 self.assertEqual(data.shape, exp.image.array.T.shape) 

276 

277 def test_setReadNoiseFromExpConst(self): 

278 image = _makeSpectractorImage() 

279 exp = _makeExposure() 

280 self.shim._setReadNoiseFromExp(image, exp, constValue=3.0) 

281 self.assertEqual(image.read_out_noise.shape, image.data.shape) 

282 self.assertTrue(np.all(image.read_out_noise == 3.0)) 

283 

284 def test_setReadNoiseFromExpNoneRaises(self): 

285 image = _makeSpectractorImage() 

286 exp = _makeExposure() 

287 with self.assertRaises(NotImplementedError): 

288 self.shim._setReadNoiseFromExp(image, exp, constValue=None) 

289 

290 def test_setReadNoiseToNone(self): 

291 image = _makeSpectractorImage() 

292 self.shim._setReadNoiseToNone(image) 

293 self.assertIsNone(image.read_out_noise) 

294 

295 def test_setGainFromExpConst(self): 

296 image = _makeSpectractorImage() 

297 exp = _makeExposure() 

298 gain = self.shim._setGainFromExp(image, exp, constValue=0.85) 

299 self.assertEqual(gain.shape, image.data.shape) 

300 self.assertTrue(np.all(gain == 0.85)) 

301 

302 def test_setGainFromExpDefault(self): 

303 image = _makeSpectractorImage() 

304 exp = _makeExposure() 

305 gain = self.shim._setGainFromExp(image, exp, constValue=None) 

306 self.assertTrue(np.all(gain == 1.0)) 

307 

308 def test_setStatErrorInImageComputed(self): 

309 image = _makeSpectractorImage() 

310 exp = _makeExposure() 

311 image.read_out_noise = np.ones_like(image.data) 

312 image.gain = np.ones_like(image.data) 

313 self.shim._setStatErrorInImage(image, exp, useExpVariance=False) 

314 self.assertIsNotNone(image.err) 

315 

316 def test_setStatErrorInImageFromVariance(self): 

317 # useExpVariance=True must write to Image.err (renamed from the old 

318 # Image.stat_errors, which no longer exists in Spectractor). 

319 image = _makeSpectractorImage(shape=(24, 20)) 

320 exp = _makeExposure() 

321 self.assertFalse(hasattr(Image, "stat_errors")) 

322 self.shim._setStatErrorInImage(image, exp, useExpVariance=True) 

323 np.testing.assert_array_equal(image.err, exp.maskedImage.variance.array) 

324 

325 def test_debugPrintTargetCentroidValue(self): 

326 image = mock.MagicMock() 

327 image.target_guess = (2.0, 3.0) 

328 image.data = np.ones((10, 10)) 

329 # Should run without error. 

330 self.shim.debugPrintTargetCentroidValue(image) 

331 

332 def test_setImageAndHeaderInfoVisitInfo(self): 

333 image = _makeSpectractorImage() 

334 exp = _makeExposure() 

335 self.shim._setImageAndHeaderInfo(image, exp, useVisitInfo=True) 

336 self.assertEqual(image.expo, 30.0) 

337 self.assertEqual(image.header.filter, 'empty') 

338 

339 def test_setImageAndHeaderInfoMetadata(self): 

340 image = _makeSpectractorImage() 

341 exp = _makeExposure() 

342 self.shim._setImageAndHeaderInfo(image, exp, useVisitInfo=False) 

343 self.assertEqual(image.airmass, 1.1) 

344 self.assertEqual(image.date_obs, '2021-01-01T00:00:00') 

345 

346 def test_setImageAndHeaderInfoException(self): 

347 # Missing AIRMASS in the metadata triggers the exception branch, 

348 # falling back to a default airmass of 1. 

349 image = _makeSpectractorImage() 

350 exp = _makeExposure() 

351 exp.getMetadata().remove('AIRMASS') 

352 self.shim._setImageAndHeaderInfo(image, exp, useVisitInfo=False) 

353 self.assertEqual(image.header.airmass, 1.) 

354 

355 def test_displayImage(self): 

356 image = mock.MagicMock() 

357 image.data = np.ones((5, 6)) 

358 with mock.patch("lsst.afw.display.Display") as mockDisp: 

359 self.shim.displayImage(image, centroid=(1, 2)) 

360 mockDisp.assert_called_once() 

361 self.assertTrue(mockDisp.return_value.mtv.called) 

362 self.assertTrue(mockDisp.return_value.dot.called) 

363 

364 def test_displayImageNoCentroid(self): 

365 image = mock.MagicMock() 

366 image.data = np.ones((5, 6)) 

367 with mock.patch("lsst.afw.display.Display") as mockDisp: 

368 self.shim.displayImage(image, centroid=None) 

369 self.assertFalse(mockDisp.return_value.dot.called) 

370 

371 

372class SpectractionAdrTestCase(lsst.utils.tests.TestCase): 

373 """Tests for setAdrParameters.""" 

374 

375 def setUp(self): 

376 self.shim = SpectractorShim() 

377 

378 def test_setAdrParametersValidWeather(self): 

379 exp = _makeExposure(weather=afwCoord.Weather(10.0, 74300.0, 40.0)) 

380 spectrum = mock.MagicMock() 

381 self.shim.setAdrParameters(spectrum, exp) 

382 # pressure converted Pa -> hPa 

383 self.assertAlmostEqual(spectrum.pressure, 743.0) 

384 self.assertEqual(spectrum.temperature, 10.0) 

385 self.assertEqual(spectrum.humidity, 40.0) 

386 self.assertEqual(len(spectrum.adr_params), 6) 

387 

388 def test_setAdrParametersNanWeather(self): 

389 nan = float('nan') 

390 exp = _makeExposure(weather=afwCoord.Weather(nan, nan, nan)) 

391 spectrum = mock.MagicMock() 

392 self.shim.setAdrParameters(spectrum, exp) 

393 # falls back to nominal values 

394 self.assertEqual(spectrum.temperature, 10) 

395 self.assertEqual(spectrum.pressure, 743) 

396 self.assertIsNone(spectrum.humidity) 

397 

398 def test_setAdrParametersLowPressure(self): 

399 # pressure already in hPa (< 10000) should not be divided. 

400 exp = _makeExposure(weather=afwCoord.Weather(10.0, 743.0, 40.0)) 

401 spectrum = mock.MagicMock() 

402 self.shim.setAdrParameters(spectrum, exp) 

403 self.assertAlmostEqual(spectrum.pressure, 743.0) 

404 

405 

406class SpectractionImageBuildTestCase(lsst.utils.tests.TestCase): 

407 """Tests for spectractorImageFromLsstExposure.""" 

408 

409 @classmethod 

410 def setUpClass(cls): 

411 with importlib.resources.path("lsst.atmospec", "resources/config/auxtel.ini") as cfg: 

412 cls.shim = SpectractorShim(configFile=str(cfg)) 

413 

414 @withSimbadCache 

415 def test_spectractorImageFromLsstExposure(self): 

416 exp = _makeExposure() 

417 with mock.patch.object(spectractionModule, "Hologram"), \ 

418 mock.patch.object(Image, "compute_parallactic_angle"): 

419 image = self.shim.spectractorImageFromLsstExposure( 

420 exp, 5.0, 6.0, target_label='HD1', 

421 disperser_label='holo4_003', filter_label='empty') 

422 self.assertEqual(image.units, "ADU/s") 

423 self.assertEqual(image.expo, 30.0) 

424 # target_guess is the (y, x) translation of the input centroid 

425 self.assertEqual(tuple(image.target_guess), (6.0, 5.0)) 

426 

427 @withSimbadCache 

428 def test_hologramConstructedWithoutDeadKwargs(self): 

429 # Spectractor's Disperser/Hologram no longer accept ``D`` or 

430 # ``verbose``; the shim must call Hologram with only the supported 

431 # kwargs (currently just data_dir). 

432 exp = _makeExposure() 

433 with mock.patch.object(spectractionModule, "Hologram") as mockHologram, \ 

434 mock.patch.object(Image, "compute_parallactic_angle"): 

435 self.shim.spectractorImageFromLsstExposure( 

436 exp, 5.0, 6.0, target_label='HD1', 

437 disperser_label='holo4_003', filter_label='empty') 

438 mockHologram.assert_called_once() 

439 _, kwargs = mockHologram.call_args 

440 self.assertNotIn('D', kwargs) 

441 self.assertNotIn('verbose', kwargs) 

442 

443 def test_hologramSignatureRejectsDeadKwargs(self): 

444 # Guard against the upstream signature regressing: the real Hologram 

445 # must not accept ``D`` or ``verbose``. 

446 import inspect 

447 from spectractor.extractor.dispersers import Disperser, Hologram 

448 params = set(inspect.signature(Hologram.__init__).parameters) 

449 params |= set(inspect.signature(Disperser.__init__).parameters) 

450 self.assertNotIn('D', params) 

451 self.assertNotIn('verbose', params) 

452 

453 @withSimbadCache 

454 def test_spectractorImageFromLsstExposureDebug(self): 

455 exp = _makeExposure() 

456 original = parameters.DEBUG 

457 try: 

458 parameters.DEBUG = True 

459 with mock.patch.object(spectractionModule, "Hologram"), \ 

460 mock.patch.object(Image, "compute_parallactic_angle"): 

461 image = self.shim.spectractorImageFromLsstExposure( 

462 exp, 5.0, 6.0, target_label='HD1', 

463 disperser_label='holo4_003', filter_label='empty') 

464 self.assertIsNotNone(image) 

465 finally: 

466 parameters.DEBUG = original 

467 

468 

469class SpectractionRunTestCase(lsst.utils.tests.TestCase): 

470 """Tests for the run() pipeline, with heavy mocking of Spectractor.""" 

471 

472 @classmethod 

473 def setUpClass(cls): 

474 with importlib.resources.path("lsst.atmospec", "resources/config/auxtel.ini") as cfg: 

475 cls.shim = SpectractorShim(configFile=str(cfg)) 

476 

477 def setUp(self): 

478 self._saved = {name: getattr(parameters, name) for name in ( 

479 'DEBUG', 'VERBOSE', 'CCD_REBIN', 'SPECTRACTOR_DECONVOLUTION_PSF2D', 

480 'SPECTRACTOR_DECONVOLUTION_FFM', 'OBS_OBJECT_TYPE', 'DISPLAY')} 

481 

482 def tearDown(self): 

483 for name, value in self._saved.items(): 

484 setattr(parameters, name, value) 

485 

486 def _runWithMocks(self, **runKwargs): 

487 exp = _makeExposure() 

488 spectrum = mock.MagicMock() 

489 spectrum.order = 1 

490 spectrum.lambdas = np.zeros(10) 

491 image = mock.MagicMock() 

492 image.data = np.ones((20, 24)) 

493 image.target_guess = (5.0, 6.0) 

494 

495 patches = { 

496 "spectractorImageFromLsstExposure": mock.patch.object( 

497 SpectractorShim, "spectractorImageFromLsstExposure", return_value=image), 

498 "setAdrParameters": mock.patch.object(SpectractorShim, "setAdrParameters"), 

499 "find_target": mock.patch.object(spectractionModule, "find_target"), 

500 "turn_image": mock.patch.object(spectractionModule, "turn_image"), 

501 "Spectrum": mock.patch.object(spectractionModule, "Spectrum", return_value=spectrum), 

502 "extract": mock.patch.object( 

503 spectractionModule, "extract_spectrum_from_image", 

504 return_value=(mock.MagicMock(), mock.MagicMock())), 

505 "psf2d": mock.patch.object(spectractionModule, "run_spectrogram_deconvolution_psf2d"), 

506 "calibrate": mock.patch.object(spectractionModule, "calibrate_spectrum"), 

507 "ffmWs": mock.patch.object(spectractionModule, "FullForwardModelFitWorkspace"), 

508 "ffmRun": mock.patch.object( 

509 spectractionModule, "run_ffm_minimisation", return_value=spectrum), 

510 "specWs": mock.patch.object(spectractionModule, "SpectrumFitWorkspace"), 

511 "specRun": mock.patch.object(spectractionModule, "run_spectrum_minimisation"), 

512 "sgramWs": mock.patch.object(spectractionModule, "SpectrogramFitWorkspace"), 

513 "sgramRun": mock.patch.object(spectractionModule, "run_spectrogram_minimisation"), 

514 "rebin": mock.patch.object(spectractionModule, "apply_rebinning_to_parameters"), 

515 } 

516 started = {k: p.start() for k, p in patches.items()} 

517 self.addCleanup(mock.patch.stopall) 

518 result = self.shim.run(exp, 5.0, 6.0, "HD1", **runKwargs) 

519 return result, spectrum, image, started 

520 

521 def test_runAllBranchesOn(self): 

522 parameters.CCD_REBIN = 1 

523 parameters.VERBOSE = False 

524 parameters.DEBUG = False 

525 parameters.SPECTRACTOR_DECONVOLUTION_PSF2D = True 

526 parameters.SPECTRACTOR_DECONVOLUTION_FFM = True 

527 parameters.OBS_OBJECT_TYPE = "STAR" 

528 with tempfile.TemporaryDirectory() as tmp: 

529 result, spectrum, image, started = self._runWithMocks( 

530 doFitAtmosphere=True, doFitAtmosphereOnSpectrogram=True, 

531 outputRoot=tmp, plotting=True) 

532 self.assertIsInstance(result, Spectraction) 

533 self.assertIs(result.spectrum, spectrum) 

534 self.assertIs(result.image, image) 

535 self.assertIsNotNone(result.spectrumForwardModelFitParameters) 

536 self.assertIsNotNone(result.spectrumLibradtranFitParameters) 

537 self.assertIsNotNone(result.spectrogramLibradtranFitParameters) 

538 self.assertTrue(started["psf2d"].called) 

539 self.assertTrue(started["specRun"].called) 

540 self.assertTrue(started["sgramRun"].called) 

541 

542 def test_runAllBranchesOff(self): 

543 parameters.CCD_REBIN = 1 

544 parameters.VERBOSE = False 

545 parameters.DEBUG = False 

546 parameters.SPECTRACTOR_DECONVOLUTION_PSF2D = False 

547 parameters.SPECTRACTOR_DECONVOLUTION_FFM = False 

548 parameters.OBS_OBJECT_TYPE = "HG-AR" # triggers with_adr = False 

549 result, spectrum, image, started = self._runWithMocks( 

550 doFitAtmosphere=False, doFitAtmosphereOnSpectrogram=False, 

551 outputRoot=None, plotting=True) 

552 self.assertIsInstance(result, Spectraction) 

553 self.assertIsNone(result.spectrumForwardModelFitParameters) 

554 self.assertIsNone(result.spectrumLibradtranFitParameters) 

555 self.assertIsNone(result.spectrogramLibradtranFitParameters) 

556 self.assertFalse(started["psf2d"].called) 

557 self.assertFalse(started["ffmRun"].called) 

558 self.assertFalse(started["specRun"].called) 

559 self.assertFalse(started["sgramRun"].called) 

560 

561 def test_extractSpectrumCalledWithoutRightEdge(self): 

562 # Spectractor's extract_spectrum_from_image no longer accepts 

563 # ``right_edge`` (it computes it internally from data_rotated), so the 

564 # shim must not pass it. 

565 parameters.CCD_REBIN = 1 

566 parameters.VERBOSE = False 

567 parameters.DEBUG = False 

568 parameters.SPECTRACTOR_DECONVOLUTION_PSF2D = False 

569 parameters.SPECTRACTOR_DECONVOLUTION_FFM = False 

570 parameters.OBS_OBJECT_TYPE = "STAR" 

571 result, spectrum, image, started = self._runWithMocks( 

572 doFitAtmosphere=False, doFitAtmosphereOnSpectrogram=False, 

573 outputRoot=None, plotting=False) 

574 self.assertTrue(started["extract"].called) 

575 _, kwargs = started["extract"].call_args 

576 self.assertNotIn('right_edge', kwargs) 

577 

578 def test_extractSpectrumSignatureRejectsRightEdge(self): 

579 # Guard against the upstream signature regressing: the real 

580 # extract_spectrum_from_image must not accept ``right_edge``. 

581 import inspect 

582 from spectractor.extractor.extractor import extract_spectrum_from_image 

583 params = inspect.signature(extract_spectrum_from_image).parameters 

584 self.assertNotIn('right_edge', params) 

585 

586 def test_runDebugAndRebin(self): 

587 parameters.CCD_REBIN = 2 

588 parameters.VERBOSE = True 

589 parameters.DEBUG = True 

590 parameters.SPECTRACTOR_DECONVOLUTION_PSF2D = True 

591 parameters.SPECTRACTOR_DECONVOLUTION_FFM = True 

592 parameters.OBS_OBJECT_TYPE = "STAR" 

593 result, spectrum, image, started = self._runWithMocks( 

594 doFitAtmosphere=True, doFitAtmosphereOnSpectrogram=True, 

595 outputRoot=None, plotting=True) 

596 self.assertIsInstance(result, Spectraction) 

597 # rebinning path was exercised 

598 self.assertTrue(started["rebin"].called) 

599 self.assertTrue(image.rebin.called) 

600 

601 

602class SpectractionContainerTestCase(lsst.utils.tests.TestCase): 

603 """Tests for the Spectraction data container.""" 

604 

605 def test_instantiate(self): 

606 result = Spectraction() 

607 result.spectrum = "spectrum" 

608 result.image = "image" 

609 self.assertEqual(result.spectrum, "spectrum") 

610 self.assertEqual(result.image, "image") 

611 

612 

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

614 pass 

615 

616 

617def setup_module(module): 

618 lsst.utils.tests.init() 

619 

620 

621if __name__ == "__main__": 621 ↛ 622line 621 didn't jump to line 622 because the condition on line 621 was never true

622 lsst.utils.tests.init() 

623 unittest.main()