pandorafits 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pandorafits-0.1.0/PKG-INFO +55 -0
- pandorafits-0.1.0/README.md +34 -0
- pandorafits-0.1.0/pyproject.toml +28 -0
- pandorafits-0.1.0/src/pandorafits/__init__.py +20 -0
- pandorafits-0.1.0/src/pandorafits/fileformats/README.md +43 -0
- pandorafits-0.1.0/src/pandorafits/fileformats/nirda-extension-types-level0.xlsx +0 -0
- pandorafits-0.1.0/src/pandorafits/fileformats/nirda-extension-types-level2.xlsx +0 -0
- pandorafits-0.1.0/src/pandorafits/fileformats/nirda-headers-level0.xlsx +0 -0
- pandorafits-0.1.0/src/pandorafits/fileformats/nirda-headers-level2.xlsx +0 -0
- pandorafits-0.1.0/src/pandorafits/fileformats/visda-extension-types-level0.xlsx +0 -0
- pandorafits-0.1.0/src/pandorafits/fileformats/visda-extension-types-level2.xlsx +0 -0
- pandorafits-0.1.0/src/pandorafits/fileformats/visda-headers-level0.xlsx +0 -0
- pandorafits-0.1.0/src/pandorafits/fileformats/visda-headers-level2.xlsx +0 -0
- pandorafits-0.1.0/src/pandorafits/fits.py +218 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pandorafits
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tools to work with fits files for Pandora
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Christina Hedges
|
|
7
|
+
Author-email: christina.l.hedges@nasa.gov
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Dist: astropy (>=5)
|
|
16
|
+
Requires-Dist: lxml (>=5.3.0,<6.0.0)
|
|
17
|
+
Requires-Dist: openpyxl (>=3.1.5,<4.0.0)
|
|
18
|
+
Requires-Dist: pandas (>=2.2.2,<3.0.0)
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# `pandora-fits`
|
|
22
|
+
|
|
23
|
+
Tools to work with fits files from Pandora.
|
|
24
|
+
|
|
25
|
+
`pandora-fits` wraps `astropy.io.fits.HDUList` classes to ensure that files conform to Pandora FITS standards.
|
|
26
|
+
|
|
27
|
+
The standards are defined using excel files in the `src/pandorasat/fileformats/` folder. Changing these files will change the standards that this tool checks against.
|
|
28
|
+
|
|
29
|
+
## Pandora Detectors
|
|
30
|
+
|
|
31
|
+
Pandora has two detectors, VISDA and NIRDA. You can read more about each of these in [pandora-sat](https://github.com/PandoraMission/pandora-sat/tree/main).
|
|
32
|
+
|
|
33
|
+
## Pandora File Levels
|
|
34
|
+
|
|
35
|
+
Pandora will have the following levels of files for each detector
|
|
36
|
+
|
|
37
|
+
| Level | Description |
|
|
38
|
+
|-------|--------------------------------------------------------------|
|
|
39
|
+
| 0 | Raw data from spacecraft |
|
|
40
|
+
| 1 | Reorganized raw data, with potential for additional keywords |
|
|
41
|
+
| 2 | Calibrated image data products |
|
|
42
|
+
| 3 | Spectral time-series data, ready for science. |
|
|
43
|
+
|
|
44
|
+
## Exceptions
|
|
45
|
+
|
|
46
|
+
`pandora-fits` will throw exceptions if files are not in the correct format. This includes
|
|
47
|
+
|
|
48
|
+
- Files do not have the right number of extensions
|
|
49
|
+
- Extensions are not the correct type
|
|
50
|
+
- Header keywords have the wrong values when compared with the template
|
|
51
|
+
|
|
52
|
+
## Warnings
|
|
53
|
+
|
|
54
|
+
`pandora-fits` will log warnings if files are missing keyword headers, but those headers aren't valued in the excel spreadsheet.
|
|
55
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# `pandora-fits`
|
|
2
|
+
|
|
3
|
+
Tools to work with fits files from Pandora.
|
|
4
|
+
|
|
5
|
+
`pandora-fits` wraps `astropy.io.fits.HDUList` classes to ensure that files conform to Pandora FITS standards.
|
|
6
|
+
|
|
7
|
+
The standards are defined using excel files in the `src/pandorasat/fileformats/` folder. Changing these files will change the standards that this tool checks against.
|
|
8
|
+
|
|
9
|
+
## Pandora Detectors
|
|
10
|
+
|
|
11
|
+
Pandora has two detectors, VISDA and NIRDA. You can read more about each of these in [pandora-sat](https://github.com/PandoraMission/pandora-sat/tree/main).
|
|
12
|
+
|
|
13
|
+
## Pandora File Levels
|
|
14
|
+
|
|
15
|
+
Pandora will have the following levels of files for each detector
|
|
16
|
+
|
|
17
|
+
| Level | Description |
|
|
18
|
+
|-------|--------------------------------------------------------------|
|
|
19
|
+
| 0 | Raw data from spacecraft |
|
|
20
|
+
| 1 | Reorganized raw data, with potential for additional keywords |
|
|
21
|
+
| 2 | Calibrated image data products |
|
|
22
|
+
| 3 | Spectral time-series data, ready for science. |
|
|
23
|
+
|
|
24
|
+
## Exceptions
|
|
25
|
+
|
|
26
|
+
`pandora-fits` will throw exceptions if files are not in the correct format. This includes
|
|
27
|
+
|
|
28
|
+
- Files do not have the right number of extensions
|
|
29
|
+
- Extensions are not the correct type
|
|
30
|
+
- Header keywords have the wrong values when compared with the template
|
|
31
|
+
|
|
32
|
+
## Warnings
|
|
33
|
+
|
|
34
|
+
`pandora-fits` will log warnings if files are missing keyword headers, but those headers aren't valued in the excel spreadsheet.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "pandorafits"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Tools to work with fits files for Pandora"
|
|
5
|
+
authors = ["Christina Hedges <christina.l.hedges@nasa.gov>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.9"
|
|
11
|
+
pandas = "^2.2.2"
|
|
12
|
+
lxml = "^5.3.0"
|
|
13
|
+
astropy = ">=5"
|
|
14
|
+
openpyxl = "^3.1.5"
|
|
15
|
+
|
|
16
|
+
[tool.poetry.group.dev]
|
|
17
|
+
optional = true
|
|
18
|
+
|
|
19
|
+
[tool.poetry.group.dev.dependencies]
|
|
20
|
+
pytest = "^5.2"
|
|
21
|
+
flake8 = "^4.0.1"
|
|
22
|
+
black = "^22.12.0"
|
|
23
|
+
isort = "^5.10.1"
|
|
24
|
+
jupyterlab = "^4.2.5"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["poetry-core"]
|
|
28
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
2
|
+
import logging # noqa: E402
|
|
3
|
+
import os # noqa
|
|
4
|
+
import pandas as pd # noqa
|
|
5
|
+
import numpy as np # noqa
|
|
6
|
+
|
|
7
|
+
PACKAGEDIR = os.path.abspath(os.path.dirname(__file__))
|
|
8
|
+
FORMATSDIR = f"{PACKAGEDIR}/fileformats/"
|
|
9
|
+
logger = logging.getLogger("pandorafits")
|
|
10
|
+
|
|
11
|
+
BITPIX_DICT = {
|
|
12
|
+
8: np.uint8,
|
|
13
|
+
16: np.int16,
|
|
14
|
+
32: np.float32,
|
|
15
|
+
-32: np.float32,
|
|
16
|
+
-64: np.float64,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
from .fits import NIRDALevel0HDUList, NIRDALevel2HDUList, VISDALevel0HDUList, VISDALevel2HDUList # noqa
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# File Formats
|
|
2
|
+
|
|
3
|
+
File formats for Pandora FITS files are defined in these excel files. Excel files are used because they are highly human readable, and each sheet can be used to define each extension.
|
|
4
|
+
|
|
5
|
+
There are two files with definitions:
|
|
6
|
+
|
|
7
|
+
## Extension Types Files
|
|
8
|
+
|
|
9
|
+
For each detector and each level of data there is a file defining the extension types, for example
|
|
10
|
+
|
|
11
|
+
| Extension | Type |
|
|
12
|
+
|-----------|------------|
|
|
13
|
+
| 0 | PrimaryHDU |
|
|
14
|
+
| 1 | ImageHDU |
|
|
15
|
+
| 2 | ImageHDU |
|
|
16
|
+
| 3 | ImageHDU |
|
|
17
|
+
| 4 | ImageHDU |
|
|
18
|
+
| 5 | TableHDU |
|
|
19
|
+
|
|
20
|
+
This is used to check that input files have the right types of FITS extensions.
|
|
21
|
+
|
|
22
|
+
## Header Formats
|
|
23
|
+
|
|
24
|
+
For each detector and level the headers are defined in excel files. Each excel file has one sheet per extension. Headers are defined with the keyword name, the value expected (if any) and a comment.
|
|
25
|
+
|
|
26
|
+
| Name | Value | Comment |
|
|
27
|
+
|----------|--------------|-------------------------------|
|
|
28
|
+
| SIMPLE | TRUE | conforms to FITS standard |
|
|
29
|
+
| BITPIX | 8 | array data type |
|
|
30
|
+
| NAXIS | 0 | number of array dimensions |
|
|
31
|
+
| EXTEND | | |
|
|
32
|
+
| EXTNAME | PRIMARY | name of extension |
|
|
33
|
+
| NEXTEND | 5 | number of standard extensions |
|
|
34
|
+
| SIMDATA | | simulated data |
|
|
35
|
+
| SCIDATA | | science data |
|
|
36
|
+
| TELESCOP | NASA Pandora | telescope |
|
|
37
|
+
| INSTRMNT | NIRDA | instrument |
|
|
38
|
+
|
|
39
|
+
This tool will check to ensure that all the correct header keys exist in the files, and if they are valued in the excel sheet the tool will check that they have the expected value.
|
|
40
|
+
|
|
41
|
+
## Dummy Files
|
|
42
|
+
|
|
43
|
+
`pandora-fits` can make dummy files with the formats described in this repository which have data of the correct `dtype` in them, but the data is all values of `1`.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Class to handle Pandora fits files"""
|
|
2
|
+
|
|
3
|
+
from . import logger
|
|
4
|
+
from . import BITPIX_DICT, FORMATSDIR
|
|
5
|
+
import numpy as np
|
|
6
|
+
from astropy.io import fits
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FITSTemplateException(Exception):
|
|
11
|
+
"""Custom exception for fits files not having the right shape."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message):
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FITSValueException(Exception):
|
|
18
|
+
"""Custom exception for fits files not having the right values."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, message):
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FITSHandlerMixins(object):
|
|
25
|
+
"""Mixins to verify fits objects have the expected formats"""
|
|
26
|
+
|
|
27
|
+
def _get_expected_cards(self, index):
|
|
28
|
+
return [
|
|
29
|
+
fits.Card(*d.fillna("").values)
|
|
30
|
+
for _, d in self.header_formats[index].iterrows()
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
def _get_dummy_hdus(self):
|
|
34
|
+
hdulist = []
|
|
35
|
+
for idx, d in self.extension_types.iterrows():
|
|
36
|
+
cards = self._get_expected_cards(idx)
|
|
37
|
+
hdr = fits.Header(cards)
|
|
38
|
+
data = None
|
|
39
|
+
if d.Type == "PrimaryHDU":
|
|
40
|
+
hdu = fits.PrimaryHDU(header=hdr)
|
|
41
|
+
elif d.Type == "ImageHDU":
|
|
42
|
+
shape = tuple(
|
|
43
|
+
[
|
|
44
|
+
hdr[f"NAXIS{naxis}"]
|
|
45
|
+
for naxis in np.arange(1, hdr["NAXIS"] + 1)[::-1]
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
data = np.ones(shape, dtype=BITPIX_DICT[hdr["BITPIX"]])
|
|
49
|
+
hdu = fits.ImageHDU(header=hdr, data=data)
|
|
50
|
+
elif d.Type == "TableHDU":
|
|
51
|
+
ncolumns = len(
|
|
52
|
+
[c.keyword for c in cards if c.keyword.startswith("TTYPE")]
|
|
53
|
+
)
|
|
54
|
+
coldefs = fits.ColDefs(
|
|
55
|
+
[
|
|
56
|
+
fits.Column(
|
|
57
|
+
name=hdr[f"TTYPE{idx}"],
|
|
58
|
+
format=hdr[f"TFORM{idx}"],
|
|
59
|
+
unit=f"TUNIT{idx}",
|
|
60
|
+
)
|
|
61
|
+
for idx in np.arange(1, ncolumns + 1)
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
hdu = fits.TableHDU.from_columns(coldefs, header=hdr)
|
|
65
|
+
hdulist.append(hdu)
|
|
66
|
+
return hdulist
|
|
67
|
+
|
|
68
|
+
def _validate_ext_types(self):
|
|
69
|
+
"""Validate that the extensions have the correct types, e.g. ImageHDU, TableHDU, etc"""
|
|
70
|
+
if not len(self) == self.nextension:
|
|
71
|
+
raise FITSTemplateException(
|
|
72
|
+
f"Expected {self.nextension} extensions, got {len(self)}."
|
|
73
|
+
)
|
|
74
|
+
for hdu, expected_type in zip(self, self.extension_types.Type.values):
|
|
75
|
+
if not isinstance(hdu, getattr(fits, expected_type)):
|
|
76
|
+
raise FITSTemplateException(
|
|
77
|
+
f"Expected extension type {expected_type}, got {hdu}."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def _validate_headers(self):
|
|
81
|
+
"""Validate the extensions have the right header keywords"""
|
|
82
|
+
for idx, hdu in enumerate(self):
|
|
83
|
+
hdr = hdu.header
|
|
84
|
+
expected_header = fits.Header(self._get_expected_cards(idx))
|
|
85
|
+
for key in expected_header:
|
|
86
|
+
if not (key in hdr):
|
|
87
|
+
logger.warning(f"Key {key} expected, but not found.")
|
|
88
|
+
continue
|
|
89
|
+
if expected_header[key] in ["", None, np.nan]:
|
|
90
|
+
logger.warning(f"{key} header key missing from ext {idx}.")
|
|
91
|
+
else:
|
|
92
|
+
if hdr[key] != expected_header[key]:
|
|
93
|
+
if key not in [
|
|
94
|
+
"NAXIS1",
|
|
95
|
+
"NAXIS2",
|
|
96
|
+
"NAXIS3",
|
|
97
|
+
"NAXIS4",
|
|
98
|
+
"NAXIS5",
|
|
99
|
+
]:
|
|
100
|
+
FITSValueException(
|
|
101
|
+
f"{key} expected to have value of {expected_header[key]}, but has value {hdr[key]}."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _validate_data(self):
|
|
105
|
+
"""Check the data in the fits file is all the right dtype, given expected `bitpix`"""
|
|
106
|
+
for idx, hdu in enumerate(self):
|
|
107
|
+
if hdu.header["EXTNAME"] == "PRIMARY":
|
|
108
|
+
continue
|
|
109
|
+
if isinstance(hdu, fits.ImageHDU):
|
|
110
|
+
expected_header = fits.Header(self._get_expected_cards(idx))
|
|
111
|
+
expected_type = BITPIX_DICT[expected_header["bitpix"]]
|
|
112
|
+
if not hdu.data.dtype == expected_type:
|
|
113
|
+
raise FITSTemplateException(
|
|
114
|
+
f"Expected data of type {expected_type}, got {hdu.data.dtype}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def validate(self):
|
|
118
|
+
"""Validate all aspects of the file"""
|
|
119
|
+
self._validate_ext_types()
|
|
120
|
+
self._validate_headers()
|
|
121
|
+
self._validate_data()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class PandoraHDUList(fits.HDUList, FITSHandlerMixins):
|
|
125
|
+
"""Base class, not designed to be used. Adds mixins to the fits.HDUList object"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, file=None):
|
|
128
|
+
if file is None:
|
|
129
|
+
super().__init__(self._get_dummy_hdus())
|
|
130
|
+
elif isinstance(file, str):
|
|
131
|
+
super().__init__(fits.open(file))
|
|
132
|
+
elif isinstance(file, fits.HDUList):
|
|
133
|
+
super().__init__(file)
|
|
134
|
+
self.nextension = len(self.header_formats)
|
|
135
|
+
self.validate()
|
|
136
|
+
|
|
137
|
+
def writeto(self, *args, **kwargs):
|
|
138
|
+
"""Write to file
|
|
139
|
+
|
|
140
|
+
Here we will add in some functionality to add in keywords on write that express the history somehow, and check the file names?
|
|
141
|
+
"""
|
|
142
|
+
fits.HDUList(self).writeto(*args, **kwargs)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class NIRDALevel0HDUList(PandoraHDUList):
|
|
146
|
+
"""NIRDA Level 0 File Type"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, file=None):
|
|
149
|
+
"""
|
|
150
|
+
This class will read a file passed to it using astropy fits.
|
|
151
|
+
After loading it will validate that the file is compliant with
|
|
152
|
+
the NIRDA Level 0 file standards specified in the `fileformats`
|
|
153
|
+
folder. This object subclasses the fits.HDUList object, and
|
|
154
|
+
maintains all its class methods.
|
|
155
|
+
|
|
156
|
+
Parameters:
|
|
157
|
+
-----------
|
|
158
|
+
file: None, str, fits.HDUList
|
|
159
|
+
The file to load
|
|
160
|
+
"""
|
|
161
|
+
self.header_formats = [
|
|
162
|
+
pd.read_excel(FORMATSDIR + "nirda-headers-level0.xlsx", idx)
|
|
163
|
+
for idx in range(2)
|
|
164
|
+
]
|
|
165
|
+
self.extension_types = pd.read_excel(
|
|
166
|
+
FORMATSDIR + "nirda-extension-types-level0.xlsx"
|
|
167
|
+
)
|
|
168
|
+
super().__init__(file=file)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class NIRDALevel2HDUList(PandoraHDUList):
|
|
172
|
+
def __init__(self, file=None):
|
|
173
|
+
self.header_formats = [
|
|
174
|
+
pd.read_excel(FORMATSDIR + "nirda-headers-level2.xlsx", idx)
|
|
175
|
+
for idx in range(6)
|
|
176
|
+
]
|
|
177
|
+
self.extension_types = pd.read_excel(
|
|
178
|
+
FORMATSDIR + "nirda-extension-types-level2.xlsx"
|
|
179
|
+
)
|
|
180
|
+
super().__init__(file=file)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class VISDALevel0HDUList(PandoraHDUList):
|
|
184
|
+
def __init__(self, file=None):
|
|
185
|
+
self.header_formats = [
|
|
186
|
+
pd.read_excel(FORMATSDIR + "visda-headers-level0.xlsx", idx)
|
|
187
|
+
for idx in range(3)
|
|
188
|
+
]
|
|
189
|
+
self.extension_types = pd.read_excel(
|
|
190
|
+
FORMATSDIR + "visda-extension-types-level0.xlsx"
|
|
191
|
+
)
|
|
192
|
+
super().__init__(file=file)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class VISDALevel2HDUList(PandoraHDUList):
|
|
196
|
+
def __init__(self, file=None, nROIs: int = 9):
|
|
197
|
+
self.header_formats = [
|
|
198
|
+
pd.read_excel(FORMATSDIR + "visda-headers-level2.xlsx", 0),
|
|
199
|
+
*[
|
|
200
|
+
pd.read_excel(FORMATSDIR + "visda-headers-level2.xlsx", 1)
|
|
201
|
+
for _ in range(1, nROIs + 1)
|
|
202
|
+
],
|
|
203
|
+
*[
|
|
204
|
+
pd.read_excel(FORMATSDIR + "visda-headers-level2.xlsx", idx)
|
|
205
|
+
for idx in range(2, 5)
|
|
206
|
+
],
|
|
207
|
+
]
|
|
208
|
+
self.extension_types = pd.read_excel(
|
|
209
|
+
FORMATSDIR + "visda-extension-types-level2.xlsx"
|
|
210
|
+
)
|
|
211
|
+
self.header_formats[1].loc[
|
|
212
|
+
self.header_formats[1].Name == "EXTNAME", "Value"
|
|
213
|
+
] = "TARGET"
|
|
214
|
+
for idx in np.arange(2, nROIs + 2):
|
|
215
|
+
self.header_formats[idx].loc[
|
|
216
|
+
self.header_formats[1].Name == "EXTNAME", "Value"
|
|
217
|
+
] = f"STAR{idx - 1:03}"
|
|
218
|
+
super().__init__(file=file)
|