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.
@@ -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`.
@@ -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)