Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add abstract base class for channels #98

Merged
merged 38 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ac7b5f9
Add empty abstractions subpackage
namurphy Feb 13, 2023
f4b4607
Add AbstractChannel ABC
namurphy Feb 13, 2023
5fc22c1
Re-organize and add gain abstract method
namurphy Feb 13, 2023
4f57a1c
Add imports to __init__.py
namurphy Feb 13, 2023
75131c8
Expand docstrings
namurphy Feb 13, 2023
b52dfb0
Add changelog entry
namurphy Feb 13, 2023
64a60fb
Add nearly empty glossary
namurphy Feb 13, 2023
9fe1bef
Revert "Add changelog entry"
namurphy Feb 13, 2023
160a9cf
first implementation of channel ABC
wtbarnes Oct 3, 2023
5f4b730
clarify property names; add unit annotations
wtbarnes Dec 23, 2023
59e7485
add a test
wtbarnes Dec 23, 2023
919ee62
add some docstrings
wtbarnes Dec 23, 2023
a63a30a
test repr
wtbarnes Dec 23, 2023
8366568
changelog
wtbarnes Dec 23, 2023
b5e5957
fix pre-commit
wtbarnes Dec 23, 2023
4d7f5ba
more docstrings
wtbarnes Dec 23, 2023
8f58e56
add api docs
wtbarnes Sep 3, 2024
7fe5238
small api change to be consistent with topic guide
wtbarnes Sep 3, 2024
a415930
precommit
wtbarnes Sep 3, 2024
50c9776
add xarray dependency to pyproject
wtbarnes Sep 17, 2024
4c7c5b7
rename subpackage to response; temp response stub
wtbarnes Sep 18, 2024
21609b3
move temperature response method to sourcespectra
wtbarnes Sep 18, 2024
fa4dd2e
fix changelog refs
wtbarnes Sep 19, 2024
bcfe9ab
fix api docs for new subpackage name
wtbarnes Sep 19, 2024
9c02948
fix doc build
wtbarnes Sep 24, 2024
c36036a
pin min xarray version
wtbarnes Sep 24, 2024
c329174
use xarray for all operations
wtbarnes Sep 24, 2024
1591b8f
bump min xarray version
wtbarnes Sep 24, 2024
be5f3b6
try filtering np warnings from xarray
wtbarnes Sep 24, 2024
b08d214
Update sunkit_instruments/response/abstractions.py
wtbarnes Sep 25, 2024
5eb3f47
Update sunkit_instruments/response/thermal.py
wtbarnes Sep 25, 2024
ec03cab
Update sunkit_instruments/response/thermal.py
wtbarnes Sep 25, 2024
f4696fa
Update sunkit_instruments/response/thermal.py
wtbarnes Sep 25, 2024
c32a371
Update sunkit_instruments/response/thermal.py
wtbarnes Sep 25, 2024
6ccd9ef
Update sunkit_instruments/response/thermal.py
wtbarnes Sep 25, 2024
c73030f
code review feedback
wtbarnes Sep 25, 2024
e7b3bc3
Merge branch 'main' into add-abstract-channel
wtbarnes Oct 28, 2024
8733ba5
Merge branch 'main' into add-abstract-channel
nabobalis Nov 13, 2024
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: 2 additions & 0 deletions changelog/98.feature.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added `sunkit_instruments.response.abstractions.AbstractChannel` to standardize an interface
for computing wavelength and temperature response functions.
3 changes: 3 additions & 0 deletions changelog/98.feature.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added `sunkit_instruments.response.SourceSpectra` to provide a container for
spectra as a function of temperature and wavelength needed for computing temperature
response functions.
1 change: 1 addition & 0 deletions docs/code_ref/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ API Reference
lyra
rhessi
suvi
response
7 changes: 7 additions & 0 deletions docs/code_ref/response.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
========
Response
========

.. automodapi:: sunkit_instruments.response

.. automodapi:: sunkit_instruments.response.abstractions
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ authors = [
{ name = "The SunPy Community", email = "[email protected]" },
]
dependencies = [
"sunpy[map,net,timeseries,visualization]>=6.0.0"
"sunpy[map,net,timeseries,visualization]>=6.0.0",
"xarray>=2023.12.0",
]
dynamic = ["version"]

Expand Down
5 changes: 5 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ filterwarnings =
error
# Do not fail on pytest config issues (i.e. missing plugins) but do show them
always::pytest.PytestConfigWarning
# A list of warnings to ignore follows. If you add to this list, you MUST
# add a comment or ideally a link to an issue that explains why the warning
# is being ignored
# Do not need to worry about numpy warnings raised by xarray internally
ignore:numpy.core.multiarray is deprecated:DeprecationWarning
# Zeep relies on deprecated cgi in Python 3.11
# Needs a release of zeep 4.2.2 or higher
# https://github.com/mvantellingen/python-zeep/pull/1364
Expand Down
5 changes: 5 additions & 0 deletions sunkit_instruments/response/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
A subpackage for computing instrument responses
"""

from sunkit_instruments.response.thermal import SourceSpectra, get_temperature_response
113 changes: 113 additions & 0 deletions sunkit_instruments/response/abstractions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""This module defines abstractions for computing instrument response."""
import abc

import astropy.units as u

__all__ = ["AbstractChannel"]


class AbstractChannel(abc.ABC):
"""
An abstract base class for defining instrument channels.

For all methods and properties defined here, see the
topic guide on instrument response for more information.
"""

@u.quantity_input
def wavelength_response(
self, obstime=None
) -> u.cm**2 * u.DN * u.steradian / (u.photon * u.pixel):
"""
Instrument response as a function of wavelength

The wavelength response is the effective area with
the conversion factors from photons to DN and steradians
to pixels.

Parameters
----------
obstime: any format parsed by `~sunpy.time.parse_time`, optional
If specified, this is used to compute the time-dependent
instrument degradation.
"""
area_eff = self.effective_area(obstime=obstime)
return (
area_eff
* self.energy_per_photon
* self.pixel_solid_angle
* self.camera_gain
/ self.energy_per_electron
)

@u.quantity_input
def effective_area(self, obstime=None) -> u.cm**2:
"""
Effective area as a function of wavelength.

The effective area is the geometrical collecting area
weighted by the mirror reflectance, filter transmittance,
quantum efficiency, and instrument degradation.

Parameters
----------
obstime: any format parsed by `sunpy.time.parse_time`, optional
If specified, this is used to compute the time-dependent
instrument degradation.
"""
return (
self.geometrical_area
* self.mirror_reflectance
* self.filter_transmittance
* self.effective_quantum_efficiency
* self.degradation(obstime=obstime)
)

@property
@u.quantity_input
def energy_per_photon(self) -> u.eV / u.photon:
return self.wavelength.to("eV", equivalencies=u.spectral()) / u.photon

@abc.abstractmethod
@u.quantity_input
def degradation(self, obstime=None) -> u.dimensionless_unscaled: ...

@property
@abc.abstractmethod
@u.quantity_input
def geometrical_area(self) -> u.cm**2: ...

@property
@abc.abstractmethod
@u.quantity_input
def mirror_reflectance(self) -> u.dimensionless_unscaled: ...

@property
@abc.abstractmethod
@u.quantity_input
def filter_transmittance(self) -> u.dimensionless_unscaled: ...

@property
@abc.abstractmethod
@u.quantity_input
def effective_quantum_efficiency(self) -> u.dimensionless_unscaled: ...

@property
@abc.abstractmethod
@u.quantity_input
def camera_gain(self) -> u.DN / u.electron: ...

@property
@abc.abstractmethod
@u.quantity_input
def energy_per_electron(self) -> u.eV / u.electron: ...

@property
@abc.abstractmethod
@u.quantity_input
def pixel_solid_angle(self) -> u.steradian / u.pixel: ...

@property
@abc.abstractmethod
@u.quantity_input
def wavelength(self) -> u.Angstrom: ...
92 changes: 92 additions & 0 deletions sunkit_instruments/response/tests/test_channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import numpy as np
import pytest

import astropy.units as u

from sunkit_instruments.response import SourceSpectra
from sunkit_instruments.response.abstractions import AbstractChannel


class TestChannel(AbstractChannel):
@property
@u.quantity_input
def wavelength(self) -> u.Angstrom:
return np.linspace(100, 200, 100) * u.AA

@u.quantity_input
def degradation(self, obstime=None) -> u.dimensionless_unscaled:
return 1.0

@property
@u.quantity_input
def geometrical_area(self) -> u.cm**2:
return 10 * u.cm**2

@property
@u.quantity_input
def mirror_reflectance(self) -> u.dimensionless_unscaled:
return np.exp(
-((self.wavelength - self.wavelength[0]) / self.wavelength[0]).decompose()
)

@property
@u.quantity_input
def filter_transmittance(self) -> u.dimensionless_unscaled:
return np.exp(
-((self.wavelength - 150 * u.AA) ** 2 / (1 * u.AA) ** 2).decompose()
)

@property
@u.quantity_input
def effective_quantum_efficiency(self) -> u.dimensionless_unscaled:
return np.ones(self.wavelength.shape)

@property
@u.quantity_input
def camera_gain(self) -> u.DN / u.electron:
return 2 * u.DN / u.electron

@property
@u.quantity_input
def energy_per_electron(self) -> u.eV / u.electron:
return 3.65 * u.eV / u.electron

@property
@u.quantity_input
def pixel_solid_angle(self) -> u.steradian / u.pixel:
return (1 * u.arcsec) ** 2 / u.pixel


@pytest.fixture
def fake_channel():
return TestChannel()


def test_effective_area(fake_channel):
assert isinstance(fake_channel.effective_area(), u.Quantity)


def test_wavelength_response(fake_channel):
assert isinstance(fake_channel.wavelength_response(), u.Quantity)


@pytest.fixture
def fake_spectra():
temperature = np.logspace(5, 8, 100) * u.K
density = 1e15 * u.cm ** (-3) * u.K / temperature
wavelength = np.linspace(50, 250, 1000) * u.AA
data = np.random.rand(*temperature.shape + wavelength.shape) * u.Unit(
"photon cm3 s-1 sr-1 Angstrom-1"
)
return SourceSpectra(temperature, wavelength, data, density=density)


def test_spectra_repr(fake_spectra):
assert isinstance(fake_spectra.__repr__(), str)


@pytest.mark.parametrize('obstime', [None, '2020-01-01'])
def test_temperature_response(fake_channel, fake_spectra, obstime):
temp_response = fake_spectra.temperature_response(fake_channel, obstime=obstime)
assert isinstance(temp_response, u.Quantity)
assert temp_response.shape == fake_spectra.temperature.shape
Loading