Skip to content

Commit 2a87370

Browse files
authored
Implement average exposure effect (#603)
2 parents 5e38d9d + cc68b3f commit 2a87370

File tree

3 files changed

+124
-1
lines changed

3 files changed

+124
-1
lines changed

scopesim/effects/electronic/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@
2323
from .electrons import LinearityCurve, Quantization
2424
from .noise import (Bias, PoorMansHxRGReadoutNoise, BasicReadoutNoise,
2525
ShotNoise, DarkCurrent)
26-
from .exposure import AutoExposure, SummedExposure
26+
from .exposure import AutoExposure, SummedExposure, ExposureOutput
2727
from .pixels import ReferencePixelBorder, BinnedImage, UnequalBinnedImage
2828
from .dmps import DetectorModePropertiesSetter

scopesim/effects/electronic/exposure.py

+39
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,45 @@ def apply_to(self, obj, **kwargs):
184184
return obj
185185

186186

187+
class ExposureOutput(Effect):
188+
"""Return average or sum of ``ndit`` subexposures."""
189+
190+
required_keys = {"dit", "ndit"}
191+
z_order: ClassVar[tuple[int, ...]] = (861,)
192+
_current_str = "current_mode"
193+
194+
def __init__(self, mode="average", **kwargs):
195+
super().__init__(**kwargs)
196+
self.meta.update(kwargs)
197+
self.modes = ("average", "sum")
198+
if mode not in self.modes:
199+
raise ValueError("mode must be one of", self.modes)
200+
self.current_mode = mode
201+
self.meta["current_mode"] = self.current_mode
202+
check_keys(self.meta, self.required_keys, action="error")
203+
204+
def apply_to(self, obj, **kwargs):
205+
if not isinstance(obj, DetectorBase):
206+
return obj
207+
208+
dit = from_currsys(self.meta["dit"], self.cmds)
209+
ndit = from_currsys(self.meta["ndit"], self.cmds)
210+
logger.debug("Exposure: DIT = %s s, NDIT = %s", dit, ndit)
211+
212+
if self.current_mode == "average":
213+
obj._hdu.data /= ndit
214+
215+
return obj
216+
217+
def set_mode(self, new_mode):
218+
"""Set new mode for ExposureOutput (average or sum)"""
219+
if new_mode in self.modes:
220+
self.current_mode = new_mode
221+
self.meta["current_mode"] = self.current_mode
222+
else:
223+
logger.warning("Trying to set to unknown mode.")
224+
225+
187226
class SummedExposure(Effect):
188227
"""Simulates a summed stack of ``ndit`` exposures."""
189228

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Tests for Effect ExposureOutput"""
2+
3+
import pytest
4+
5+
import numpy as np
6+
7+
from scopesim import UserCommands
8+
from scopesim.optics.image_plane import ImagePlane
9+
from scopesim.detector import Detector
10+
from scopesim.effects.electronic import ExposureOutput
11+
12+
from scopesim.tests.mocks.py_objects.imagehdu_objects import _image_hdu_square
13+
14+
# pylint: disable=missing-class-docstring
15+
# pylint: disable=missing-function-docstring
16+
17+
def _patched_cmds(exptime=1, dit=None, ndit=None):
18+
return UserCommands(properties={"!OBS.exptime": exptime,
19+
"!OBS.dit": dit,
20+
"!OBS.ndit": ndit})
21+
22+
@pytest.fixture(name="imageplane", scope="class")
23+
def fixture_imageplane():
24+
"""Instantiate an ImagePlane object"""
25+
implane = ImagePlane(_image_hdu_square().header)
26+
implane.hdu.data += 1.e5
27+
return implane
28+
29+
@pytest.fixture(name="exposureoutput", scope="function")
30+
def fixture_exposureoutput():
31+
"""Instantiate an ExposureOutput object"""
32+
return ExposureOutput(mode="average", dit=1, ndit=4)
33+
34+
@pytest.fixture(name="detector", scope="function")
35+
def fixture_detector():
36+
det = Detector(_image_hdu_square().header)
37+
det._hdu.data[:] = 1.e5
38+
return det
39+
40+
class TestExposureOutput:
41+
def test_initialises_correctly(self, exposureoutput):
42+
assert isinstance(exposureoutput, ExposureOutput)
43+
44+
def test_fails_with_unknown_mode(self):
45+
with pytest.raises(ValueError):
46+
ExposureOutput(mode="something", dit=1, ndit=4)
47+
48+
def test_fails_without_dit_and_ndit(self):
49+
with pytest.raises(ValueError):
50+
ExposureOutput(mode="sum")
51+
52+
def test_works_only_on_detector_base(self, exposureoutput, imageplane):
53+
assert exposureoutput.apply_to(imageplane) is imageplane
54+
55+
def test_can_set_to_new_mode(self, exposureoutput):
56+
assert exposureoutput.current_mode == "average"
57+
exposureoutput.set_mode("sum")
58+
assert exposureoutput.current_mode == "sum"
59+
assert exposureoutput.meta["current_mode"] == "sum"
60+
61+
def test_cannot_set_to_unknown_mode(self, exposureoutput):
62+
old_mode = exposureoutput.current_mode
63+
exposureoutput.set_mode("something")
64+
assert exposureoutput.current_mode == old_mode
65+
66+
@pytest.mark.parametrize("dit, ndit",
67+
[(1., 1),
68+
(2., 5),
69+
(3, 36)])
70+
def test_applies_average(self, dit, ndit, detector):
71+
det_mean = detector._hdu.data.mean()
72+
exposureoutput = ExposureOutput("average", dit=dit, ndit=ndit)
73+
result = exposureoutput.apply_to(detector)
74+
assert np.isclose(result._hdu.data.mean(), det_mean / ndit)
75+
76+
@pytest.mark.parametrize("dit, ndit",
77+
[(1., 1),
78+
(2., 5),
79+
(3, 36)])
80+
def test_applies_sum(self, dit, ndit, detector):
81+
det_mean = detector._hdu.data.mean()
82+
exposureoutput = ExposureOutput("sum", dit=dit, ndit=ndit)
83+
result = exposureoutput.apply_to(detector)
84+
assert np.isclose(result._hdu.data.mean(), det_mean)

0 commit comments

Comments
 (0)