diff --git a/stellarphot/core.py b/stellarphot/core.py index 6f0fa07f..6586a380 100644 --- a/stellarphot/core.py +++ b/stellarphot/core.py @@ -4,16 +4,14 @@ import pandas as pd from astropy import units as u from astropy.coordinates import EarthLocation, SkyCoord -from astropy.io.misc.yaml import AstropyDumper, AstropyLoader from astropy.table import Column, QTable, Table, TableAttribute from astropy.time import Time -from astropy.units import IrreducibleUnit, Quantity, Unit from astropy.wcs import WCS from astroquery.vizier import Vizier -from pydantic import BaseModel, Field, root_validator, validator + +from .settings import Camera __all__ = [ - "Camera", "BaseEnhancedTable", "PhotometryData", "CatalogData", @@ -23,289 +21,6 @@ ] -# Approach to validation of units was inspired by the GammaPy project -# which did it before we did: -# https://docs.gammapy.org/dev/_modules/gammapy/analysis/config.html - - -class UnitType(Unit): - # Validator for Unit type - @classmethod - def __get_validators__(cls): - yield cls.validate - - @classmethod - def validate(cls, v): - return Unit(v) - - @classmethod - def __modify_schema__(cls, field_schema, field): - # Set default values for the schema in case the field doesn't provide them - name = "Unit" - description = "An astropy unit" - - name = field.name or name - description = field.field_info.description or description - examples = field.field_info.extra.get("examples", []) - - field_schema.update( - { - "title": name, - "description": description, - "examples": examples, - "type": "string", - } - ) - - -class QuantityType(Quantity): - # Validator for Quantity type - @classmethod - def __get_validators__(cls): - yield cls.validate - - @classmethod - def validate(cls, v): - try: - v = Quantity(v) - except TypeError as err: - raise ValueError(f"Invalid value for Quantity: {v}") from err - else: - if not v.unit.bases: - raise ValueError("Must provided a unit") - return v - - @classmethod - def __modify_schema__(cls, field_schema, field): - # Set default values for the schema in case the field doesn't provide them - name = "Quantity" - description = "An astropy Quantity with units" - - name = field.name or name - description = field.field_info.description or description - examples = field.field_info.extra.get("examples", []) - - field_schema.update( - { - "title": name, - "description": description, - "examples": examples, - "type": "string", - } - ) - - -class PixelScaleType(Quantity): - # Validator for pixel scale type - @classmethod - def __get_validators__(cls): - yield cls.validate - - @classmethod - def validate(cls, v): - try: - v = Quantity(v) - except TypeError as err: - raise ValueError(f"Invalid value for Quantity: {v}") from err - if ( - len(v.unit.bases) != 2 - or v.unit.bases[0].physical_type != "angle" - or v.unit.bases[1].name != "pix" - ): - raise ValueError(f"Invalid unit for pixel scale: {v.unit!r}") - return v - - @classmethod - def __modify_schema__(cls, field_schema): - field_schema.update( - { - "title": "PixelScale", - "description": "An astropy Quantity with units of angle per pixel", - "examples": ["0.563 arcsec / pix"], - "type": "string", - } - ) - - -class Camera(BaseModel): - """ - A class to represent a CCD-based camera. - - Parameters - ---------- - - data_unit : `astropy.units.Unit` - The unit of the data. - - gain : `astropy.units.Quantity` - 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`. - - read_noise : `astropy.units.Quantity` - The read noise of the camera with units. - - dark_current : `astropy.units.Quantity` - The dark current of the camera in units such that, when multiplied by - exposure time, the unit matches the units of the `read_noise`. - - pixel_scale : `astropy.units.Quantity` - The pixel scale of the camera in units of arcseconds per pixel. - - max_data_value : `astropy.units.Quantity` - The maximum pixel value to allow while performing photometry. Pixel values - above this will be set to ``NaN``. The unit must be ``data_unit``. - - Attributes - ---------- - - data_unit : `astropy.units.Unit` - The unit of the data. - - gain : `astropy.units.Quantity` - 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`. - - read_noise : `astropy.units.Quantity` - The read noise of the camera with units. - - dark_current : `astropy.units.Quantity` - The dark current of the camera in units such that, when multiplied by - exposure time, the unit matches the units of the `read_noise`. - - pixel_scale : `astropy.units.Quantity` - The pixel scale of the camera in units of arcseconds per pixel. - - max_data_value : `astropy.units.Quantity` - The maximum pixel value to allow while performing photometry. Pixel values - above this will be set to ``NaN``. The unit must be ``data_unit``. - - Notes - ----- - The gain, read noise, and dark current are all assumed to be constant - across the entire CCD. - - Examples - -------- - >>> from astropy import units as u - >>> from stellarphot import Camera - >>> camera = Camera(data_unit="adu", - ... gain=1.0 * u.electron / u.adu, - ... read_noise=1.0 * u.electron, - ... dark_current=0.01 * u.electron / u.second, - ... pixel_scale=0.563 * u.arcsec / u.pixel, - ... max_data_value=50000 * u.adu) - >>> camera.data_unit - Unit("adu") - >>> camera.gain - - >>> camera.read_noise - - >>> camera.dark_current - - >>> camera.pixel_scale - - >>> camera.max_data_value - - """ - - data_unit: UnitType = Field( - description="units of the data", examples=["adu", "counts", "DN", "electrons"] - ) - gain: QuantityType = Field( - description="unit should be consistent with data and read noise", - examples=["1.0 electron / adu"], - ) - read_noise: QuantityType = Field( - description="unit should be consistent with dark current", - examples=["10.0 electron"], - ) - dark_current: QuantityType = Field( - description="unit consistent with read noise, per unit time", - examples=["0.01 electron / second"], - ) - pixel_scale: PixelScaleType = Field( - description="units of angle per pixel", examples=["0.6 arcsec / pix"] - ) - max_data_value: QuantityType = Field( - description="maximum data value while performing photometry", - examples=["50000 adu"], - ) - - class Config: - validate_all = True - validate_assignment = True - extra = "forbid" - json_encoders = { - Quantity: lambda v: f"{v.value} {v.unit}", - QuantityType: lambda v: f"{v.value} {v.unit}", - Unit: lambda v: f"{v}", - IrreducibleUnit: lambda v: f"{v}", - PixelScaleType: lambda v: f"{v.value} {v.unit}", - } - - # When the switch to pydantic v2 happens, this root_validator will need - # to be replaced by a model_validator decorator. - @root_validator(skip_on_failure=True) - @classmethod - def validate_gain(cls, values): - # Get read noise units - rn_unit = Quantity(values["read_noise"]).unit - - # Check that gain and read noise have compatible units, that is that - # gain is read noise per data unit. - gain = values["gain"] - if ( - len(gain.unit.bases) != 2 - or gain.unit.bases[0] != rn_unit - or gain.unit.bases[1] != values["data_unit"] - ): - raise ValueError( - f"Gain units {gain.unit} are not compatible with " - f"read noise units {rn_unit}." - ) - - # Check that dark current and read noise have compatible units, that is - # that dark current is read noise per second. - dark_current = values["dark_current"] - if ( - len(dark_current.unit.bases) != 2 - or dark_current.unit.bases[0] != rn_unit - or dark_current.unit.bases[1] != u.s - ): - raise ValueError( - f"Dark current units {dark_current.unit} are not " - f"compatible with read noise units {rn_unit}." - ) - - # Check that maximum data value is consistent with data units - if values["max_data_value"].unit != values["data_unit"]: - raise ValueError( - f"Maximum data value units {values['max_data_value'].unit} " - f"are not consistent with data units {values['data_unit']}." - ) - return values - - @validator("max_data_value") - @classmethod - def validate_max_data_value(cls, v): - if v.value <= 0: - raise ValueError("max_data_value must be positive") - return v - - -# Add YAML round-tripping for Camera -def camera_representer(dumper, cam): - return dumper.represent_mapping("!Camera", cam.dict()) - - -def camera_constructor(loader, node): - return Camera(**loader.construct_mapping(node)) - - -AstropyDumper.add_representer(Camera, camera_representer) -AstropyLoader.add_constructor("!Camera", camera_constructor) - - class BaseEnhancedTable(QTable): """ A class to validate an `astropy.table.QTable` table of astronomical data during diff --git a/stellarphot/photometry/photometry.py b/stellarphot/photometry/photometry.py index 6a3fe1e6..260e17d6 100644 --- a/stellarphot/photometry/photometry.py +++ b/stellarphot/photometry/photometry.py @@ -16,7 +16,8 @@ from photutils.centroids import centroid_sources from scipy.spatial.distance import cdist -from stellarphot import Camera, PhotometryData, SourceListData +from stellarphot import PhotometryData, SourceListData +from stellarphot.settings import Camera from .source_detection import compute_fwhm diff --git a/stellarphot/photometry/tests/test_photometry.py b/stellarphot/photometry/tests/test_photometry.py index c9b49353..d02fb8ec 100644 --- a/stellarphot/photometry/tests/test_photometry.py +++ b/stellarphot/photometry/tests/test_photometry.py @@ -11,7 +11,7 @@ from astropy.utils.metadata.exceptions import MergeConflictWarning from fake_image import FakeCCDImage, shift_FakeCCDImage -from stellarphot.core import Camera, SourceListData +from stellarphot.core import SourceListData from stellarphot.photometry import ( calculate_noise, find_too_close, @@ -19,7 +19,7 @@ single_image_photometry, source_detection, ) -from stellarphot.settings import ApertureSettings +from stellarphot.settings import ApertureSettings, Camera # Constants for the tests diff --git a/stellarphot/photometry/tests/test_profiles.py b/stellarphot/photometry/tests/test_profiles.py index eec2d5e0..8e50ebf8 100644 --- a/stellarphot/photometry/tests/test_profiles.py +++ b/stellarphot/photometry/tests/test_profiles.py @@ -3,9 +3,9 @@ from astropy.table import Table from photutils.datasets import make_gaussian_sources_image, make_noise_image -from stellarphot import Camera from stellarphot.photometry import CenterAndProfile, find_center -from stellarphot.tests.test_core import TEST_CAMERA_VALUES +from stellarphot.settings import Camera +from stellarphot.settings.tests.test_models import TEST_CAMERA_VALUES # Make a few round stars STARS = Table( diff --git a/stellarphot/settings/astropy_pydantic.py b/stellarphot/settings/astropy_pydantic.py new file mode 100644 index 00000000..452a3923 --- /dev/null +++ b/stellarphot/settings/astropy_pydantic.py @@ -0,0 +1,106 @@ +from astropy.units import Quantity, Unit + +__all__ = ["UnitType", "QuantityType", "PixelScaleType"] + +# Approach to validation of units was inspired by the GammaPy project +# which did it before we did: +# https://docs.gammapy.org/dev/_modules/gammapy/analysis/config.html + + +class UnitType(Unit): + # Validator for Unit type + @classmethod + def __get_validators__(cls): + yield cls.validate + + @classmethod + def validate(cls, v): + return Unit(v) + + @classmethod + def __modify_schema__(cls, field_schema, field): + # Set default values for the schema in case the field doesn't provide them + name = "Unit" + description = "An astropy unit" + + name = field.name or name + description = field.field_info.description or description + examples = field.field_info.extra.get("examples", []) + + field_schema.update( + { + "title": name, + "description": description, + "examples": examples, + "type": "string", + } + ) + + +class QuantityType(Quantity): + # Validator for Quantity type + @classmethod + def __get_validators__(cls): + yield cls.validate + + @classmethod + def validate(cls, v): + try: + v = Quantity(v) + except TypeError as err: + raise ValueError(f"Invalid value for Quantity: {v}") from err + else: + if not v.unit.bases: + raise ValueError("Must provided a unit") + return v + + @classmethod + def __modify_schema__(cls, field_schema, field): + # Set default values for the schema in case the field doesn't provide them + name = "Quantity" + description = "An astropy Quantity with units" + + name = field.name or name + description = field.field_info.description or description + examples = field.field_info.extra.get("examples", []) + + field_schema.update( + { + "title": name, + "description": description, + "examples": examples, + "type": "string", + } + ) + + +class PixelScaleType(Quantity): + # Validator for pixel scale type + @classmethod + def __get_validators__(cls): + yield cls.validate + + @classmethod + def validate(cls, v): + try: + v = Quantity(v) + except TypeError as err: + raise ValueError(f"Invalid value for Quantity: {v}") from err + if ( + len(v.unit.bases) != 2 + or v.unit.bases[0].physical_type != "angle" + or v.unit.bases[1].name != "pix" + ): + raise ValueError(f"Invalid unit for pixel scale: {v.unit!r}") + return v + + @classmethod + def __modify_schema__(cls, field_schema): + field_schema.update( + { + "title": "PixelScale", + "description": "An astropy Quantity with units of angle per pixel", + "examples": ["0.563 arcsec / pix"], + "type": "string", + } + ) diff --git a/stellarphot/settings/models.py b/stellarphot/settings/models.py index bffd25eb..cd2d8a49 100644 --- a/stellarphot/settings/models.py +++ b/stellarphot/settings/models.py @@ -4,14 +4,194 @@ import astropy.units as u from astropy.coordinates import SkyCoord +from astropy.io.misc.yaml import AstropyDumper, AstropyLoader from astropy.time import Time -from astropy.units import Quantity -from pydantic import BaseModel, Field, conint, validator +from astropy.units import IrreducibleUnit, Quantity, Unit +from pydantic import BaseModel, Field, conint, root_validator, validator -from ..core import QuantityType +from .astropy_pydantic import PixelScaleType, QuantityType, UnitType from .autowidgets import CustomBoundedIntTex -__all__ = ["ApertureSettings", "PhotometryFileSettings", "Exoplanet"] +__all__ = ["Camera", "ApertureSettings", "PhotometryFileSettings", "Exoplanet"] + + +class Camera(BaseModel): + """ + A class to represent a CCD-based camera. + + Parameters + ---------- + + data_unit : `astropy.units.Unit` + The unit of the data. + + gain : `astropy.units.Quantity` + 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`. + + read_noise : `astropy.units.Quantity` + The read noise of the camera with units. + + dark_current : `astropy.units.Quantity` + The dark current of the camera in units such that, when multiplied by + exposure time, the unit matches the units of the `read_noise`. + + pixel_scale : `astropy.units.Quantity` + The pixel scale of the camera in units of arcseconds per pixel. + + max_data_value : `astropy.units.Quantity` + The maximum pixel value to allow while performing photometry. Pixel values + above this will be set to ``NaN``. The unit must be ``data_unit``. + + Attributes + ---------- + + data_unit : `astropy.units.Unit` + The unit of the data. + + gain : `astropy.units.Quantity` + 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`. + + read_noise : `astropy.units.Quantity` + The read noise of the camera with units. + + dark_current : `astropy.units.Quantity` + The dark current of the camera in units such that, when multiplied by + exposure time, the unit matches the units of the `read_noise`. + + pixel_scale : `astropy.units.Quantity` + The pixel scale of the camera in units of arcseconds per pixel. + + max_data_value : `astropy.units.Quantity` + The maximum pixel value to allow while performing photometry. Pixel values + above this will be set to ``NaN``. The unit must be ``data_unit``. + + Notes + ----- + The gain, read noise, and dark current are all assumed to be constant + across the entire CCD. + + Examples + -------- + >>> from astropy import units as u + >>> from stellarphot.settings import Camera + >>> camera = Camera(data_unit="adu", + ... gain=1.0 * u.electron / u.adu, + ... read_noise=1.0 * u.electron, + ... dark_current=0.01 * u.electron / u.second, + ... pixel_scale=0.563 * u.arcsec / u.pixel, + ... max_data_value=50000 * u.adu) + >>> camera.data_unit + Unit("adu") + >>> camera.gain + + >>> camera.read_noise + + >>> camera.dark_current + + >>> camera.pixel_scale + + >>> camera.max_data_value + + """ + + data_unit: UnitType = Field( + description="units of the data", examples=["adu", "counts", "DN", "electrons"] + ) + gain: QuantityType = Field( + description="unit should be consistent with data and read noise", + examples=["1.0 electron / adu"], + ) + read_noise: QuantityType = Field( + description="unit should be consistent with dark current", + examples=["10.0 electron"], + ) + dark_current: QuantityType = Field( + description="unit consistent with read noise, per unit time", + examples=["0.01 electron / second"], + ) + pixel_scale: PixelScaleType = Field( + description="units of angle per pixel", examples=["0.6 arcsec / pix"] + ) + max_data_value: QuantityType = Field( + description="maximum data value while performing photometry", + examples=["50000 adu"], + ) + + class Config: + validate_all = True + validate_assignment = True + extra = "forbid" + json_encoders = { + Quantity: lambda v: f"{v.value} {v.unit}", + QuantityType: lambda v: f"{v.value} {v.unit}", + Unit: lambda v: f"{v}", + IrreducibleUnit: lambda v: f"{v}", + PixelScaleType: lambda v: f"{v.value} {v.unit}", + } + + # When the switch to pydantic v2 happens, this root_validator will need + # to be replaced by a model_validator decorator. + @root_validator(skip_on_failure=True) + @classmethod + def validate_gain(cls, values): + # Get read noise units + rn_unit = Quantity(values["read_noise"]).unit + + # Check that gain and read noise have compatible units, that is that + # gain is read noise per data unit. + gain = values["gain"] + if ( + len(gain.unit.bases) != 2 + or gain.unit.bases[0] != rn_unit + or gain.unit.bases[1] != values["data_unit"] + ): + raise ValueError( + f"Gain units {gain.unit} are not compatible with " + f"read noise units {rn_unit}." + ) + + # Check that dark current and read noise have compatible units, that is + # that dark current is read noise per second. + dark_current = values["dark_current"] + if ( + len(dark_current.unit.bases) != 2 + or dark_current.unit.bases[0] != rn_unit + or dark_current.unit.bases[1] != u.s + ): + raise ValueError( + f"Dark current units {dark_current.unit} are not " + f"compatible with read noise units {rn_unit}." + ) + + # Check that maximum data value is consistent with data units + if values["max_data_value"].unit != values["data_unit"]: + raise ValueError( + f"Maximum data value units {values['max_data_value'].unit} " + f"are not consistent with data units {values['data_unit']}." + ) + return values + + @validator("max_data_value") + @classmethod + def validate_max_data_value(cls, v): + if v.value <= 0: + raise ValueError("max_data_value must be positive") + return v + + +# Add YAML round-tripping for Camera +def camera_representer(dumper, cam): + return dumper.represent_mapping("!Camera", cam.dict()) + + +def camera_constructor(loader, node): + return Camera(**loader.construct_mapping(node)) + + +AstropyDumper.add_representer(Camera, camera_representer) +AstropyLoader.add_constructor("!Camera", camera_constructor) class ApertureSettings(BaseModel): diff --git a/stellarphot/settings/tests/test_models.py b/stellarphot/settings/tests/test_models.py index 8165e795..f0ee0ae9 100644 --- a/stellarphot/settings/tests/test_models.py +++ b/stellarphot/settings/tests/test_models.py @@ -4,10 +4,210 @@ from astropy.time import Time from pydantic import ValidationError -from stellarphot.settings.models import ApertureSettings, Exoplanet +from stellarphot.settings.models import ApertureSettings, Camera, Exoplanet DEFAULT_APERTURE_SETTINGS = dict(radius=5, gap=10, annulus_width=15) +TEST_CAMERA_VALUES = dict( + 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_data_value=50000 * u.adu, +) + + +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, + ) + 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 + + +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 + + # 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, + ) + + +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 + + # 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, + ) + + +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 + + # 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, + ) + + +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 + + # 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, + ) + + +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, + ) + c2 = c.copy() + assert c2 == c + + +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 + + 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, + ) + 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 + + +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, + ) + schema = c.schema() + assert len(schema["properties"]) == 6 + + +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, + ) + + c2 = Camera.parse_raw(c.json()) + assert c2 == c + def test_create_aperture_settings_correctly(): ap_set = ApertureSettings(**DEFAULT_APERTURE_SETTINGS) diff --git a/stellarphot/tests/test_core.py b/stellarphot/tests/test_core.py index bdc468c6..112fa602 100644 --- a/stellarphot/tests/test_core.py +++ b/stellarphot/tests/test_core.py @@ -11,218 +11,16 @@ from astropy.utils.data import get_pkg_data_filename from astropy.wcs import WCS from astropy.wcs.wcs import FITSFixedWarning -from pydantic import ValidationError from stellarphot.core import ( BaseEnhancedTable, - Camera, CatalogData, PhotometryData, SourceListData, apass_dr9, vsx_vizier, ) - -TEST_CAMERA_VALUES = dict( - 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_data_value=50000 * u.adu, -) - - -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, - ) - 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 - - -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 - - # 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, - ) - - -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 - - # 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, - ) - - -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 - - # 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, - ) - - -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 - - # 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, - ) - - -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, - ) - c2 = c.copy() - assert c2 == c - - -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 - - 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, - ) - 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 - - -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, - ) - schema = c.schema() - assert len(schema["properties"]) == 6 - - -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, - ) - - c2 = Camera.parse_raw(c.json()) - assert c2 == c - +from stellarphot.settings import Camera # Create several test descriptions for use in base_enhanced_table tests. test_descript = {