Skip to content

Commit

Permalink
Merge pull request #252 from mwcraig/rearrange-settings
Browse files Browse the repository at this point in the history
  • Loading branch information
mwcraig authored Jan 11, 2024
2 parents 2d02933 + f0c5939 commit 887ad16
Show file tree
Hide file tree
Showing 8 changed files with 500 additions and 500 deletions.
289 changes: 2 additions & 287 deletions stellarphot/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
<Quantity 1. electron / adu>
>>> camera.read_noise
<Quantity 1. electron>
>>> camera.dark_current
<Quantity 0.01 electron / s>
>>> camera.pixel_scale
<Quantity 0.563 arcsec / pix>
>>> camera.max_data_value
<Quantity 50000. adu>
"""

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
Expand Down
3 changes: 2 additions & 1 deletion stellarphot/photometry/photometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions stellarphot/photometry/tests/test_photometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
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,
multi_image_photometry,
single_image_photometry,
source_detection,
)
from stellarphot.settings import ApertureSettings
from stellarphot.settings import ApertureSettings, Camera

# Constants for the tests

Expand Down
4 changes: 2 additions & 2 deletions stellarphot/photometry/tests/test_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 887ad16

Please sign in to comment.