From 07a00132d06935d7eb77da5697cf301d38f0ead0 Mon Sep 17 00:00:00 2001 From: Matt Craig Date: Thu, 11 Jan 2024 12:56:01 -0600 Subject: [PATCH 1/2] Remove redundancy in Camera tests The goal here was to make it as easy as possible to revise tests if new camera properties are added by removing duplication. --- .../photometry/tests/test_photometry.py | 75 ++++----- stellarphot/settings/tests/test_models.py | 152 ++++-------------- 2 files changed, 61 insertions(+), 166 deletions(-) diff --git a/stellarphot/photometry/tests/test_photometry.py b/stellarphot/photometry/tests/test_photometry.py index d02fb8ec..e68d8d85 100644 --- a/stellarphot/photometry/tests/test_photometry.py +++ b/stellarphot/photometry/tests/test_photometry.py @@ -37,6 +37,16 @@ pixel_scale=1 * u.arcsec / u.pixel, max_data_value=40000 * u.adu, ) + +# Camera with no read noise or dark currnet +ZERO_CAMERA = Camera( + data_unit=u.adu, + gain=1.0 * u.electron / u.adu, + read_noise=0 * u.electron, + dark_current=0.0 * u.electron / u.second, + pixel_scale=1 * u.arcsec / u.pixel, + max_data_value=40000 * u.adu, +) FAKE_OBS = EarthLocation(lat=0 * u.deg, lon=0 * u.deg, height=0 * u.m) COORDS2USE = "pixel" @@ -57,14 +67,8 @@ def test_calc_noise_source_only(gain, aperture_area): expected = np.sqrt(gain * counts) # Create camera instance - camera = Camera( - data_unit=u.adu, - gain=gain * u.electron / u.adu, - read_noise=0 * u.electron, - dark_current=0 * u.electron / u.second, - pixel_scale=1 * u.arcsec / u.pixel, - max_data_value=40000 * u.adu, - ) + camera = ZERO_CAMERA.copy() + camera.gain = gain * camera.gain.unit np.testing.assert_allclose( calculate_noise(camera, counts=counts, aperture_area=aperture_area), expected @@ -80,14 +84,10 @@ def test_calc_noise_dark_only(gain, aperture_area): exposure = 20 # Create camera instance - camera = Camera( - data_unit=u.adu, - gain=gain * u.electron / u.adu, - read_noise=0 * u.electron, - dark_current=dark_current * u.electron / u.second, - pixel_scale=1 * u.arcsec / u.pixel, - max_data_value=40000 * u.adu, - ) + camera = ZERO_CAMERA.copy() + # Set gain and dark current to values for test + camera.dark_current = dark_current * camera.dark_current.unit + camera.gain = gain * camera.gain.unit expected = np.sqrt(dark_current * aperture_area * exposure) @@ -106,14 +106,9 @@ def test_calc_read_noise_only(gain, aperture_area): expected = np.sqrt(aperture_area * read_noise**2) # Create camera instance - camera = Camera( - data_unit=u.adu, - gain=gain * u.electron / u.adu, - read_noise=read_noise * u.electron, - dark_current=0 * u.electron / u.second, - pixel_scale=1 * u.arcsec / u.pixel, - max_data_value=40000 * u.adu, - ) + camera = ZERO_CAMERA.copy() + camera.read_noise = read_noise * camera.read_noise.unit + camera.gain = gain * camera.gain.unit np.testing.assert_allclose( calculate_noise(camera, aperture_area=aperture_area), expected @@ -128,14 +123,8 @@ def test_calc_sky_only(gain, aperture_area): expected = np.sqrt(gain * aperture_area * sky) # Create camera instance - camera = Camera( - data_unit=u.adu, - gain=gain * u.electron / u.adu, - read_noise=0 * u.electron, - dark_current=0 * u.electron / u.second, - pixel_scale=1 * u.arcsec / u.pixel, - max_data_value=40000 * u.adu, - ) + camera = ZERO_CAMERA.copy() + camera.gain = gain * camera.gain.unit np.testing.assert_allclose( calculate_noise(camera, aperture_area=aperture_area, sky_per_pix=sky), expected @@ -153,14 +142,8 @@ def test_annulus_area_term(): expected = np.sqrt(gain * aperture_area * (1 + aperture_area / annulus_area) * sky) # Create camera instance - camera = Camera( - data_unit=u.adu, - gain=gain * u.electron / u.adu, - read_noise=0 * u.electron, - dark_current=0 * u.electron / u.second, - pixel_scale=1 * u.arcsec / u.pixel, - max_data_value=40000 * u.adu, - ) + camera = ZERO_CAMERA.copy() + camera.gain = gain * camera.gain.unit np.testing.assert_allclose( calculate_noise( @@ -189,14 +172,10 @@ def test_calc_noise_messy_case(digit, expected): read_noise = 12 # Create camera instance - camera = Camera( - data_unit=u.adu, - gain=gain * u.electron / u.adu, - read_noise=read_noise * u.electron, - dark_current=dark_current * u.electron / u.second, - pixel_scale=1 * u.arcsec / u.pixel, - max_data_value=40000 * u.adu, - ) + camera = ZERO_CAMERA.copy() + camera.gain = gain * camera.gain.unit + camera.dark_current = dark_current * camera.dark_current.unit + camera.read_noise = read_noise * camera.read_noise.unit np.testing.assert_allclose( calculate_noise( diff --git a/stellarphot/settings/tests/test_models.py b/stellarphot/settings/tests/test_models.py index f0ee0ae9..4fcd9223 100644 --- a/stellarphot/settings/tests/test_models.py +++ b/stellarphot/settings/tests/test_models.py @@ -20,128 +20,72 @@ def test_camera_attributes(): # Check that the attributes are set properly - data_unit = u.adu - gain = 2.0 * u.electron / u.adu - read_noise = 10 * u.electron - dark_current = 0.01 * u.electron / u.second - pixel_scale = 0.563 * u.arcsec / u.pix - max_val = 50000 * u.adu - c = Camera( - data_unit=data_unit, - gain=gain, - read_noise=read_noise, - dark_current=dark_current, - pixel_scale=pixel_scale, - max_data_value=max_val, + **TEST_CAMERA_VALUES, ) - assert c.data_unit == data_unit - assert c.gain == gain - assert c.dark_current == dark_current - assert c.read_noise == read_noise - assert c.pixel_scale == pixel_scale - assert c.max_data_value == max_val + assert c.dict() == TEST_CAMERA_VALUES def test_camera_unitscheck(): # Check that the units are checked properly - gain = 2.0 * u.electron / u.adu - read_noise = 10 * u.electron - dark_current = 0.01 * u.electron / u.second - pixel_scale = 0.563 * u.arcsec / u.pix - max_adu = 50000 * u.adu + # Remove units from all of the Quantity types + camera_dict_no_units = { + k: v.value if hasattr(v, "value") else v for k, v in TEST_CAMERA_VALUES.items() + } # All 5 of the attributes after data_unit will be checked for units # and noted in the ValidationError message. Rather than checking # separately for all 5, we just check for the presence of the # right number of errors with pytest.raises(ValidationError, match="5 validation errors"): Camera( - data_unit=u.adu, - gain=gain.value, - read_noise=read_noise.value, - dark_current=dark_current.value, - pixel_scale=pixel_scale.value, - max_data_value=max_adu.value, + **camera_dict_no_units, ) def test_camera_negative_max_adu(): - # Check that the units are checked properly - data_unit = u.adu - gain = 2.0 * u.electron / u.adu - read_noise = 10 * u.electron - dark_current = 0.01 * u.electron / u.second - pixel_scale = 0.563 * u.arcsec / u.pix - max_val = -50000 * u.adu + # Check that a negative maximum data value raises an error + camera_for_test = TEST_CAMERA_VALUES.copy() + camera_for_test["max_data_value"] = -1 * camera_for_test["max_data_value"] # Make sure that a negative max_adu raises an error with pytest.raises(ValidationError, match="must be positive"): Camera( - data_unit=data_unit, - gain=gain, - read_noise=read_noise, - dark_current=dark_current, - pixel_scale=pixel_scale, - max_data_value=max_val, + **camera_for_test, ) def test_camera_incompatible_gain_units(): - data_unit = u.adu - gain = 2.0 * u.count / u.adu - read_noise = 10 * u.electron - dark_current = 0.01 * u.electron / u.second - pixel_scale = 0.563 * u.arcsec / u.pix - max_val = 50000 * u.adu + camera_for_test = TEST_CAMERA_VALUES.copy() + # Gain unit is incompatible with noise unit (electrons vs. counts) + camera_for_test["gain"] = 2.0 * u.count / u.adu # Make sure that an incompatible gain raises an error with pytest.raises(ValidationError, match="Gain units.*not compatible"): Camera( - data_unit=data_unit, - gain=gain, - read_noise=read_noise, - dark_current=dark_current, - pixel_scale=pixel_scale, - max_data_value=max_val, + **camera_for_test, ) def test_camera_incompatible_max_val_units(): - data_unit = u.adu - gain = 2.0 * u.electron / u.adu - read_noise = 10 * u.electron - dark_current = 0.01 * u.electron / u.second - pixel_scale = 0.563 * u.arcsec / u.pix - max_val = 50000 * u.count + camera_for_test = TEST_CAMERA_VALUES.copy() + # data unit is adu, not count + camera_for_test["max_data_value"] = 50000 * u.count # Make sure that an incompatible gain raises an error with pytest.raises( ValidationError, match="Maximum data value units.*not consistent" ): Camera( - data_unit=data_unit, - gain=gain, - read_noise=read_noise, - dark_current=dark_current, - pixel_scale=pixel_scale, - max_data_value=max_val, + **camera_for_test, ) def test_camera_copy(): # Make sure copy actually copies everything - gain = 2.0 * u.electron / u.adu - read_noise = 10 * u.electron - dark_current = 0.01 * u.electron / u.second - pixel_scale = 0.563 * u.arcsec / u.pix + c = Camera( - data_unit=u.adu, - gain=gain, - read_noise=read_noise, - dark_current=dark_current, - pixel_scale=pixel_scale, - max_data_value=65535 * u.adu, + **TEST_CAMERA_VALUES, ) c2 = c.copy() assert c2 == c @@ -149,61 +93,33 @@ def test_camera_copy(): def test_camera_altunitscheck(): # Check to see that 'count' is allowed instead of 'electron' - data_unit = u.adu - gain = 2.0 * u.count / u.adu - read_noise = 10 * u.count - dark_current = 0.01 * u.count / u.second - pixel_scale = 0.563 * u.arcsec / u.pix - max_val = 50000 * u.adu + camera_for_test = dict( + data_unit=u.adu, + gain=2.0 * u.count / u.adu, + read_noise=10 * u.count, + dark_current=0.01 * u.count / u.second, + pixel_scale=0.563 * u.arcsec / u.pix, + max_data_value=50000 * u.adu, + ) c = Camera( - data_unit=data_unit, - gain=gain, - read_noise=read_noise, - dark_current=dark_current, - pixel_scale=pixel_scale, - max_data_value=max_val, + **camera_for_test, ) - assert c.data_unit == data_unit - assert c.gain == gain - assert c.dark_current == dark_current - assert c.read_noise == read_noise - assert c.pixel_scale == pixel_scale - assert c.max_data_value == max_val + assert c.dict() == camera_for_test def test_camera_schema(): # Check that we can generate a schema for a Camera and that it # has the right number of attributes - c = Camera( - data_unit=u.adu, - gain=5 * u.electron / u.adu, - read_noise=1 * u.electron, - dark_current=0.1 * u.electron / u.second, - pixel_scale=0.6 * u.arcsec / u.pix, - max_data_value=65535 * u.adu, - ) + c = Camera(**TEST_CAMERA_VALUES) schema = c.schema() - assert len(schema["properties"]) == 6 + assert len(schema["properties"]) == len(TEST_CAMERA_VALUES) def test_camera_json_round_trip(): # Check that a camera can be converted to json and back - data_unit = u.adu - gain = 2.0 * u.electron / u.adu - read_noise = 10 * u.electron - dark_current = 0.01 * u.electron / u.second - pixel_scale = 0.563 * u.arcsec / u.pix - max_val = 50000 * u.adu - c = Camera( - data_unit=data_unit, - gain=gain, - read_noise=read_noise, - dark_current=dark_current, - pixel_scale=pixel_scale, - max_data_value=max_val, - ) + c = Camera(**TEST_CAMERA_VALUES) c2 = Camera.parse_raw(c.json()) assert c2 == c From 792fc4daa7240e2f476906794360e7785c3d10e8 Mon Sep 17 00:00:00 2001 From: Matt Craig Date: Thu, 11 Jan 2024 12:20:26 -0600 Subject: [PATCH 2/2] Add name property to Camera --- stellarphot/photometry/tests/test_photometry.py | 4 ++++ stellarphot/settings/models.py | 12 ++++++++++++ stellarphot/settings/tests/test_models.py | 2 ++ stellarphot/tests/test_core.py | 1 + 4 files changed, 19 insertions(+) diff --git a/stellarphot/photometry/tests/test_photometry.py b/stellarphot/photometry/tests/test_photometry.py index e68d8d85..01212e3e 100644 --- a/stellarphot/photometry/tests/test_photometry.py +++ b/stellarphot/photometry/tests/test_photometry.py @@ -29,9 +29,12 @@ SHIFT_TOLERANCE = 6 FWHM_ESTIMATE = 5 + +# A camera with not unreasonable settings FAKE_CAMERA = Camera( data_unit=u.adu, gain=1.0 * u.electron / u.adu, + name="test camera", read_noise=0 * u.electron, dark_current=0.1 * u.electron / u.second, pixel_scale=1 * u.arcsec / u.pixel, @@ -42,6 +45,7 @@ ZERO_CAMERA = Camera( data_unit=u.adu, gain=1.0 * u.electron / u.adu, + name="test camera", read_noise=0 * u.electron, dark_current=0.0 * u.electron / u.second, pixel_scale=1 * u.arcsec / u.pixel, diff --git a/stellarphot/settings/models.py b/stellarphot/settings/models.py index cd2d8a49..8905f872 100644 --- a/stellarphot/settings/models.py +++ b/stellarphot/settings/models.py @@ -29,6 +29,10 @@ class Camera(BaseModel): The gain of the camera in units such the product of `gain` times the image data has units equal to that of the `read_noise`. + name : str + The name of the camera; can be anything that helps the user identify + the camera. + read_noise : `astropy.units.Quantity` The read noise of the camera with units. @@ -53,6 +57,10 @@ class Camera(BaseModel): The gain of the camera in units such the product of `gain` times the image data has units equal to that of the `read_noise`. + name : str + The name of the camera; can be anything that helps the user identify + the camera. + read_noise : `astropy.units.Quantity` The read noise of the camera with units. @@ -78,6 +86,7 @@ class Camera(BaseModel): >>> from stellarphot.settings import Camera >>> camera = Camera(data_unit="adu", ... gain=1.0 * u.electron / u.adu, + ... name="test camera", ... read_noise=1.0 * u.electron, ... dark_current=0.01 * u.electron / u.second, ... pixel_scale=0.563 * u.arcsec / u.pixel, @@ -86,6 +95,8 @@ class Camera(BaseModel): Unit("adu") >>> camera.gain + >>> camera.name + 'test camera' >>> camera.read_noise >>> camera.dark_current @@ -103,6 +114,7 @@ class Camera(BaseModel): description="unit should be consistent with data and read noise", examples=["1.0 electron / adu"], ) + name: str read_noise: QuantityType = Field( description="unit should be consistent with dark current", examples=["10.0 electron"], diff --git a/stellarphot/settings/tests/test_models.py b/stellarphot/settings/tests/test_models.py index 4fcd9223..60d1a41d 100644 --- a/stellarphot/settings/tests/test_models.py +++ b/stellarphot/settings/tests/test_models.py @@ -11,6 +11,7 @@ TEST_CAMERA_VALUES = dict( data_unit=u.adu, gain=2.0 * u.electron / u.adu, + name="test camera", read_noise=10 * u.electron, dark_current=0.01 * u.electron / u.second, pixel_scale=0.563 * u.arcsec / u.pix, @@ -96,6 +97,7 @@ def test_camera_altunitscheck(): camera_for_test = dict( data_unit=u.adu, gain=2.0 * u.count / u.adu, + name="test camera", read_noise=10 * u.count, dark_current=0.01 * u.count / u.second, pixel_scale=0.563 * u.arcsec / u.pix, diff --git a/stellarphot/tests/test_core.py b/stellarphot/tests/test_core.py index 112fa602..805f5625 100644 --- a/stellarphot/tests/test_core.py +++ b/stellarphot/tests/test_core.py @@ -80,6 +80,7 @@ feder_cg_16m = Camera( data_unit=u.adu, gain=1.5 * u.electron / u.adu, + name="Andor Aspen CG16M", read_noise=10.0 * u.electron, dark_current=0.01 * u.electron / u.second, pixel_scale=0.563 * u.arcsec / u.pix,