Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scopesim/effects/electronic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
from .electrons import LinearityCurve, Quantization
from .noise import (Bias, PoorMansHxRGReadoutNoise, BasicReadoutNoise,
ShotNoise, DarkCurrent)
from .exposure import AutoExposure, SummedExposure
from .exposure import AutoExposure, SummedExposure, ExposureOutput
from .pixels import ReferencePixelBorder, BinnedImage, UnequalBinnedImage
from .dmps import DetectorModePropertiesSetter
39 changes: 39 additions & 0 deletions scopesim/effects/electronic/exposure.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,45 @@ def apply_to(self, obj, **kwargs):
return obj


class ExposureOutput(Effect):
"""Return average or sum of ``ndit`` subexposures."""

required_keys = {"dit", "ndit"}
z_order: ClassVar[tuple[int, ...]] = (861,)
_current_str = "current_mode"

def __init__(self, mode="average", **kwargs):
super().__init__(**kwargs)
self.meta.update(kwargs)
self.modes = ("average", "sum")
if mode not in self.modes:
raise ValueError("mode must be one of", self.modes)
self.current_mode = mode
self.meta["current_mode"] = self.current_mode
check_keys(self.meta, self.required_keys, action="error")

def apply_to(self, obj, **kwargs):
if not isinstance(obj, DetectorBase):
return obj

dit = from_currsys(self.meta["dit"], self.cmds)
ndit = from_currsys(self.meta["ndit"], self.cmds)
logger.debug("Exposure: DIT = %s s, NDIT = %s", dit, ndit)

if self.current_mode == "average":
obj._hdu.data /= ndit

return obj

def set_mode(self, new_mode):
"""Set new mode for ExposureOutput (average or sum)"""
if new_mode in self.modes:
self.current_mode = new_mode
self.meta["current_mode"] = self.current_mode
else:
logger.warning("Trying to set to unknown mode.")


class SummedExposure(Effect):
"""Simulates a summed stack of ``ndit`` exposures."""

Expand Down
84 changes: 84 additions & 0 deletions scopesim/tests/tests_effects/test_exposure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Tests for Effect ExposureOutput"""

import pytest

import numpy as np

from scopesim import UserCommands
from scopesim.optics.image_plane import ImagePlane
from scopesim.detector import Detector
from scopesim.effects.electronic import ExposureOutput

from scopesim.tests.mocks.py_objects.imagehdu_objects import _image_hdu_square

# pylint: disable=missing-class-docstring
# pylint: disable=missing-function-docstring

def _patched_cmds(exptime=1, dit=None, ndit=None):
return UserCommands(properties={"!OBS.exptime": exptime,
"!OBS.dit": dit,
"!OBS.ndit": ndit})

@pytest.fixture(name="imageplane", scope="class")
def fixture_imageplane():
"""Instantiate an ImagePlane object"""
implane = ImagePlane(_image_hdu_square().header)
implane.hdu.data += 1.e5
return implane

@pytest.fixture(name="exposureoutput", scope="function")
def fixture_exposureoutput():
"""Instantiate an ExposureOutput object"""
return ExposureOutput(mode="average", dit=1, ndit=4)

@pytest.fixture(name="detector", scope="function")
def fixture_detector():
det = Detector(_image_hdu_square().header)
det._hdu.data[:] = 1.e5
return det

class TestExposureOutput:
def test_initialises_correctly(self, exposureoutput):
assert isinstance(exposureoutput, ExposureOutput)

def test_fails_with_unknown_mode(self):
with pytest.raises(ValueError):
ExposureOutput(mode="something", dit=1, ndit=4)

def test_fails_without_dit_and_ndit(self):
with pytest.raises(ValueError):
ExposureOutput(mode="sum")

def test_works_only_on_detector_base(self, exposureoutput, imageplane):
assert exposureoutput.apply_to(imageplane) is imageplane

def test_can_set_to_new_mode(self, exposureoutput):
assert exposureoutput.current_mode == "average"
exposureoutput.set_mode("sum")
assert exposureoutput.current_mode == "sum"
assert exposureoutput.meta["current_mode"] == "sum"

def test_cannot_set_to_unknown_mode(self, exposureoutput):
old_mode = exposureoutput.current_mode
exposureoutput.set_mode("something")
assert exposureoutput.current_mode == old_mode

@pytest.mark.parametrize("dit, ndit",
[(1., 1),
(2., 5),
(3, 36)])
def test_applies_average(self, dit, ndit, detector):
det_mean = detector._hdu.data.mean()
exposureoutput = ExposureOutput("average", dit=dit, ndit=ndit)
result = exposureoutput.apply_to(detector)
assert np.isclose(result._hdu.data.mean(), det_mean / ndit)

@pytest.mark.parametrize("dit, ndit",
[(1., 1),
(2., 5),
(3, 36)])
def test_applies_sum(self, dit, ndit, detector):
det_mean = detector._hdu.data.mean()
exposureoutput = ExposureOutput("sum", dit=dit, ndit=ndit)
result = exposureoutput.apply_to(detector)
assert np.isclose(result._hdu.data.mean(), det_mean)