lasp-curryer 0.2.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.
Files changed (45) hide show
  1. lasp_curryer-0.2.0/LICENSE +21 -0
  2. lasp_curryer-0.2.0/NOTICE +2 -0
  3. lasp_curryer-0.2.0/PKG-INFO +243 -0
  4. lasp_curryer-0.2.0/README.md +186 -0
  5. lasp_curryer-0.2.0/curryer/__init__.py +9 -0
  6. lasp_curryer-0.2.0/curryer/compute/__init__.py +8 -0
  7. lasp_curryer-0.2.0/curryer/compute/abstract.py +142 -0
  8. lasp_curryer-0.2.0/curryer/compute/constants.py +76 -0
  9. lasp_curryer-0.2.0/curryer/compute/elevation.py +1011 -0
  10. lasp_curryer-0.2.0/curryer/compute/ephemeris.py +176 -0
  11. lasp_curryer-0.2.0/curryer/compute/pointing.py +523 -0
  12. lasp_curryer-0.2.0/curryer/compute/spatial.py +1677 -0
  13. lasp_curryer-0.2.0/curryer/correction/__init__.py +0 -0
  14. lasp_curryer-0.2.0/curryer/correction/correction_config.py +131 -0
  15. lasp_curryer-0.2.0/curryer/correction/data_structures.py +119 -0
  16. lasp_curryer-0.2.0/curryer/correction/dataio.py +277 -0
  17. lasp_curryer-0.2.0/curryer/correction/geolocation_error_stats.py +537 -0
  18. lasp_curryer-0.2.0/curryer/correction/image_match.py +361 -0
  19. lasp_curryer-0.2.0/curryer/correction/monte_carlo.py +2467 -0
  20. lasp_curryer-0.2.0/curryer/correction/pairing.py +591 -0
  21. lasp_curryer-0.2.0/curryer/correction/psf.py +328 -0
  22. lasp_curryer-0.2.0/curryer/correction/search.py +126 -0
  23. lasp_curryer-0.2.0/curryer/kernels/__init__.py +8 -0
  24. lasp_curryer-0.2.0/curryer/kernels/attitude.py +361 -0
  25. lasp_curryer-0.2.0/curryer/kernels/classes.py +432 -0
  26. lasp_curryer-0.2.0/curryer/kernels/create.py +275 -0
  27. lasp_curryer-0.2.0/curryer/kernels/ephemeris.py +424 -0
  28. lasp_curryer-0.2.0/curryer/kernels/templates/kernel_macros.jinja2 +76 -0
  29. lasp_curryer-0.2.0/curryer/kernels/templates/mkspk_setup_template.txt +42 -0
  30. lasp_curryer-0.2.0/curryer/kernels/templates/msopck_setup_template.txt +42 -0
  31. lasp_curryer-0.2.0/curryer/kernels/writer.py +273 -0
  32. lasp_curryer-0.2.0/curryer/meta.py +103 -0
  33. lasp_curryer-0.2.0/curryer/spicetime/__init__.py +55 -0
  34. lasp_curryer-0.2.0/curryer/spicetime/constants.py +35 -0
  35. lasp_curryer-0.2.0/curryer/spicetime/convert.py +686 -0
  36. lasp_curryer-0.2.0/curryer/spicetime/leapsecond.py +273 -0
  37. lasp_curryer-0.2.0/curryer/spicetime/native.py +143 -0
  38. lasp_curryer-0.2.0/curryer/spicetime/utils.py +162 -0
  39. lasp_curryer-0.2.0/curryer/spicierpy/__init__.py +32 -0
  40. lasp_curryer-0.2.0/curryer/spicierpy/ext.py +511 -0
  41. lasp_curryer-0.2.0/curryer/spicierpy/obj.py +652 -0
  42. lasp_curryer-0.2.0/curryer/spicierpy/vectorized.py +170 -0
  43. lasp_curryer-0.2.0/curryer/tle.py +346 -0
  44. lasp_curryer-0.2.0/curryer/utils.py +251 -0
  45. lasp_curryer-0.2.0/pyproject.toml +122 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 The Regents of the University of Colorado.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ Curryer
2
+ Copyright (c) 2023 The Regents of the University of Colorado
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: lasp-curryer
3
+ Version: 0.2.0
4
+ Summary: LASP SPICE extentions and geospatial data product generation tools.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ License-File: NOTICE
8
+ Keywords: spice,geolocation,geometry,ephemeris,attitude,kernel,coordinates,lasp,university of colorado,naif
9
+ Author: Brandon Stone
10
+ Author-email: brandon.h.stone@colorado.edu
11
+ Requires-Python: >=3.9,<4
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Natural Language :: English
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Topic :: Software Development
21
+ Classifier: Topic :: Scientific/Engineering
22
+ Classifier: Operating System :: POSIX
23
+ Classifier: Operating System :: Unix
24
+ Classifier: Operating System :: MacOS
25
+ Provides-Extra: dev
26
+ Provides-Extra: docs
27
+ Provides-Extra: test
28
+ Requires-Dist: affine (>=2.3)
29
+ Requires-Dist: boto3
30
+ Requires-Dist: furo ; extra == "docs"
31
+ Requires-Dist: h5py (>=3.0,<3.14.0)
32
+ Requires-Dist: ipython ; extra == "dev"
33
+ Requires-Dist: jinja2 (>=3)
34
+ Requires-Dist: myst-parser (>=0.18) ; extra == "docs"
35
+ Requires-Dist: netcdf4 (>=1.6,<1.7.3)
36
+ Requires-Dist: numpy (>=1.23,<2.0)
37
+ Requires-Dist: openpyxl (>=3)
38
+ Requires-Dist: pandas (>=1.5)
39
+ Requires-Dist: pre-commit ; extra == "dev"
40
+ Requires-Dist: pyproj (>=3.6)
41
+ Requires-Dist: pytest ; extra == "test"
42
+ Requires-Dist: pytest-cov ; extra == "test"
43
+ Requires-Dist: pytest-randomly ; extra == "test"
44
+ Requires-Dist: requests (>=2.28)
45
+ Requires-Dist: responses (>=0.25.8) ; extra == "test"
46
+ Requires-Dist: ruff ; extra == "dev"
47
+ Requires-Dist: scipy (>=1.9)
48
+ Requires-Dist: sphinx (>=5.0) ; extra == "docs"
49
+ Requires-Dist: sphinx-autoapi (>=2.0) ; extra == "docs"
50
+ Requires-Dist: spiceypy (>=6)
51
+ Requires-Dist: tifffile (>=2024.1.1)
52
+ Requires-Dist: xarray (>=2022.11)
53
+ Project-URL: Documentation, https://lasp-curryer.readthedocs.io/
54
+ Project-URL: Repository, https://github.com/lasp/curryer
55
+ Description-Content-Type: text/markdown
56
+
57
+ # Curryer
58
+
59
+ A library for SPICE extensions and geospatial data product generation.
60
+
61
+ - Github: https://github.com/lasp/curryer
62
+ - PyPi: https://pypi.org/project/lasp-curryer/
63
+ - Documentation: https://lasp-curryer.readthedocs.io/
64
+
65
+ ## Core Features
66
+
67
+ - Extensions and wrappers for SPICE routines and common data patterns.
68
+ - Automation of SPICE kernel creation from JSON definition files and modern data
69
+ file formats and third-party data structures.
70
+ - Level-1 geospatial data processing routines (e.g., geolocation).
71
+
72
+ ## Install
73
+
74
+ ```shell
75
+ pip install lasp-curryer
76
+ ```
77
+
78
+ ### Data / Binary Files
79
+
80
+ _NOTE: Data files and precompiled binaries are not currently automated and thus
81
+ require manual downloading. This will be addressed in the next major release._
82
+
83
+ Download from the Curryer repo:
84
+
85
+ - `data/generic` - Generic spice kernels (e.g., leapsecond kernel)
86
+ - Download
87
+ - `data/<mission>` - Mission specific kernels and/or kernel definitions.
88
+
89
+ Define the top-level directory using the environment variable `CURRYER_DATA_DIR`
90
+ or pass the path to routines which require data files.
91
+
92
+ Download Third-party Files:
93
+
94
+ - SPICE Utilities: https://naif.jpl.nasa.gov/naif/utilities.html
95
+ - At minimum: `mkspk`, `msopck`, `brief`, `ckbreif`
96
+ - SPICE Generic Kernels (large):
97
+ - [de430.bsp](https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/de430.bsp),
98
+ place in `data/generic`.
99
+ - PyProj Data:
100
+ - Data directory: `import pyproj; print(pyproj.datadir.get_user_data_dir())`
101
+ - [EGM96 TIFF](https://cdn.proj.org/us_nga_egm96_15.tif)
102
+ - (OPTIONAL, USED FOR TESTING) `data/gmted` - Digital Elevation Model (DEMs) with global coverage at
103
+ 15-arc-second.
104
+ - Alternatively, use the script [download_dem.py](bin/download_dem.py) to
105
+ download different types and/or resolutions from the USGS.
106
+
107
+ ## Examples
108
+
109
+ ### SPICE Extensions
110
+
111
+ Time conversion:
112
+
113
+ ```python
114
+ from curryer import spicetime
115
+
116
+ print(spicetime.adapt(0, from_='ugps', to='iso'))
117
+ # 1980-01-06 00:00:00.000000
118
+
119
+ print(spicetime.adapt('2024-11-13', 'iso'))
120
+ # 1415491218000000
121
+
122
+ print(spicetime.adapt(1415491218000000, to='et'))
123
+ # 784728069.1827033
124
+
125
+ import numpy as np
126
+
127
+ print(repr(spicetime.adapt(np.arange(4) * 60e6 + 1415491218000000, to='dt64')))
128
+ # array(['2024-11-13T00:00:00.000000', '2024-11-13T00:01:00.000000',
129
+ # '2024-11-13T00:02:00.000000', '2024-11-13T00:03:00.000000'],
130
+ # dtype='datetime64[us]')
131
+ ```
132
+
133
+ Abstractions:
134
+
135
+ ```python
136
+ from curryer import spicierpy
137
+
138
+ spicierpy.ext.infer_ids('ISS', 25544, from_norad=True)
139
+ # {'mission': 'ISS',
140
+ # 'spacecraft': -125544,
141
+ # 'clock': -125544,
142
+ # 'ephemeris': -125544,
143
+ # 'attitude': -125544000,
144
+ # 'instruments': {}}
145
+
146
+ earth = spicierpy.obj.Body('Earth')
147
+ print(earth, earth.id)
148
+ # Body(EARTH) 399
149
+
150
+ import curryer
151
+
152
+ mkrn = curryer.meta.MetaKernel.from_json(
153
+ 'data/tsis1/tsis_v01.kernels.tm.json', sds_dir='data/generic', relative=True
154
+ )
155
+ print(mkrn)
156
+ # MetaKernel(Spacecraft(ISS_SC), Body(ISS_ELC3), Body(ISS_EXPA35), Body(TSIS_TADS),
157
+ # Body(TSIS_AZEL), Body(TSIS_TIM), Body(TSIS_TIM_GLINT))
158
+
159
+ with spicierpy.ext.load_kernel([mkrn.sds_kernels, mkrn.mission_kernels]):
160
+ print(spicierpy.ext.instrument_boresight('TSIS_TIM'))
161
+ # [0. 0. 1.]
162
+
163
+ mkrn = curryer.meta.MetaKernel.from_json(
164
+ 'tests/data/clarreo/cprs_v01.kernels.tm.json', sds_dir='data/generic', relative=True
165
+ )
166
+ print(mkrn)
167
+ # MetaKernel(Spacecraft(ISS_SC), Body(CPRS_BASE), Body(CPRS_PEDE),
168
+ # Body(CPRS_AZ), Body(CPRS_YOKE), Body(CPRS_EL), Body(CPRS_HYSICS))
169
+
170
+ with spicierpy.ext.load_kernel([mkrn.sds_kernels, mkrn.mission_kernels]):
171
+ print(curryer.compute.spatial.pixel_vectors('CPRS_HYSICS'))
172
+ # (480,
173
+ # array([[ 0.00173869, -0.08715574, 0.99619318],
174
+ # [ 0.0017315 , -0.08679351, 0.99622482],
175
+ # [ 0.00172431, -0.08643127, 0.99625632],
176
+ # ...,
177
+ # [-0.00171712, 0.08606901, 0.9962877 ],
178
+ # [-0.00172431, 0.08643127, 0.99625632],
179
+ # [-0.0017315 , 0.08679351, 0.99622482]]))
180
+ ```
181
+
182
+ ### SPICE Kernel Creation
183
+
184
+ Create CLARREO Dynamic Kernels:
185
+
186
+ ```python
187
+ import curryer
188
+
189
+ meta_kernel = 'tests/data/clarreo/cprs_v01.kernels.tm.json'
190
+ generic_dir = 'data/generic'
191
+ kernel_configs = [
192
+ 'data/clarreo/iss_sc_v01.ephemeris.spk.json',
193
+ 'data/clarreo/iss_sc_v01.attitude.ck.json',
194
+ 'data/clarreo/cprs_az_v01.attitude.ck.json',
195
+ 'data/clarreo/cprs_el_v01.attitude.ck.json',
196
+ ]
197
+ output_dir = '/tmp'
198
+ input_file_or_obj = 'tests/data/demo/cprs_geolocation_tlm_20230101_20240430.nc'
199
+
200
+ # Load meta kernel details. Includes existing static kernels.
201
+ mkrn = curryer.meta.MetaKernel.from_json(meta_kernel, relative=True, sds_dir=generic_dir)
202
+
203
+ # Create the dynamic kernels from the JSONs alone. Note that they
204
+ # contain the reference to the input_data netcdf4 file to read.
205
+ generated_kernels = []
206
+ creator = curryer.kernels.create.KernelCreator(overwrite=False, append=False)
207
+
208
+ # Generate the kernels from the config and input data (file or object).
209
+ for kernel_config in kernel_configs:
210
+ generated_kernels.append(creator.write_from_json(
211
+ kernel_config, output_kernel=output_dir, input_data=input_file_or_obj,
212
+ ))
213
+
214
+ ```
215
+
216
+ ### Level-1 Geospatial Processing
217
+
218
+ Geolocate CLARREO HYSICS Instrument:
219
+
220
+ ```python
221
+ import pandas as pd
222
+ import curryer
223
+
224
+ meta_kernel = 'tests/data/clarreo/cprs_v01.kernels.tm.json'
225
+ generic_dir = 'data/generic'
226
+
227
+ time_range = ('2023-01-01', '2023-01-01T00:05:00')
228
+ ugps_times = curryer.spicetime.adapt(pd.date_range(*time_range, freq='67ms', inclusive='left'), 'iso')
229
+
230
+ # Load meta kernel details. Includes existing static kernels.
231
+ mkrn = curryer.meta.MetaKernel.from_json(meta_kernel, relative=True, sds_dir=generic_dir)
232
+
233
+ # Geolocate all the individual pixels and create the L1A data product!
234
+ with curryer.spicierpy.ext.load_kernel([mkrn.sds_kernels, mkrn.mission_kernels]):
235
+ geoloc_inst = curryer.compute.spatial.Geolocate('CPRS_HYSICS')
236
+ l1a_dataset = geoloc_inst(ugps_times)
237
+ l1a_dataset.to_netcdf('cprs_geolocation_l1a_20230101.nc')
238
+
239
+ ```
240
+
241
+ _Assumes dynamic kernels have been created and their file names defined within
242
+ the metakernel JSON file._
243
+
@@ -0,0 +1,186 @@
1
+ # Curryer
2
+
3
+ A library for SPICE extensions and geospatial data product generation.
4
+
5
+ - Github: https://github.com/lasp/curryer
6
+ - PyPi: https://pypi.org/project/lasp-curryer/
7
+ - Documentation: https://lasp-curryer.readthedocs.io/
8
+
9
+ ## Core Features
10
+
11
+ - Extensions and wrappers for SPICE routines and common data patterns.
12
+ - Automation of SPICE kernel creation from JSON definition files and modern data
13
+ file formats and third-party data structures.
14
+ - Level-1 geospatial data processing routines (e.g., geolocation).
15
+
16
+ ## Install
17
+
18
+ ```shell
19
+ pip install lasp-curryer
20
+ ```
21
+
22
+ ### Data / Binary Files
23
+
24
+ _NOTE: Data files and precompiled binaries are not currently automated and thus
25
+ require manual downloading. This will be addressed in the next major release._
26
+
27
+ Download from the Curryer repo:
28
+
29
+ - `data/generic` - Generic spice kernels (e.g., leapsecond kernel)
30
+ - Download
31
+ - `data/<mission>` - Mission specific kernels and/or kernel definitions.
32
+
33
+ Define the top-level directory using the environment variable `CURRYER_DATA_DIR`
34
+ or pass the path to routines which require data files.
35
+
36
+ Download Third-party Files:
37
+
38
+ - SPICE Utilities: https://naif.jpl.nasa.gov/naif/utilities.html
39
+ - At minimum: `mkspk`, `msopck`, `brief`, `ckbreif`
40
+ - SPICE Generic Kernels (large):
41
+ - [de430.bsp](https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/de430.bsp),
42
+ place in `data/generic`.
43
+ - PyProj Data:
44
+ - Data directory: `import pyproj; print(pyproj.datadir.get_user_data_dir())`
45
+ - [EGM96 TIFF](https://cdn.proj.org/us_nga_egm96_15.tif)
46
+ - (OPTIONAL, USED FOR TESTING) `data/gmted` - Digital Elevation Model (DEMs) with global coverage at
47
+ 15-arc-second.
48
+ - Alternatively, use the script [download_dem.py](bin/download_dem.py) to
49
+ download different types and/or resolutions from the USGS.
50
+
51
+ ## Examples
52
+
53
+ ### SPICE Extensions
54
+
55
+ Time conversion:
56
+
57
+ ```python
58
+ from curryer import spicetime
59
+
60
+ print(spicetime.adapt(0, from_='ugps', to='iso'))
61
+ # 1980-01-06 00:00:00.000000
62
+
63
+ print(spicetime.adapt('2024-11-13', 'iso'))
64
+ # 1415491218000000
65
+
66
+ print(spicetime.adapt(1415491218000000, to='et'))
67
+ # 784728069.1827033
68
+
69
+ import numpy as np
70
+
71
+ print(repr(spicetime.adapt(np.arange(4) * 60e6 + 1415491218000000, to='dt64')))
72
+ # array(['2024-11-13T00:00:00.000000', '2024-11-13T00:01:00.000000',
73
+ # '2024-11-13T00:02:00.000000', '2024-11-13T00:03:00.000000'],
74
+ # dtype='datetime64[us]')
75
+ ```
76
+
77
+ Abstractions:
78
+
79
+ ```python
80
+ from curryer import spicierpy
81
+
82
+ spicierpy.ext.infer_ids('ISS', 25544, from_norad=True)
83
+ # {'mission': 'ISS',
84
+ # 'spacecraft': -125544,
85
+ # 'clock': -125544,
86
+ # 'ephemeris': -125544,
87
+ # 'attitude': -125544000,
88
+ # 'instruments': {}}
89
+
90
+ earth = spicierpy.obj.Body('Earth')
91
+ print(earth, earth.id)
92
+ # Body(EARTH) 399
93
+
94
+ import curryer
95
+
96
+ mkrn = curryer.meta.MetaKernel.from_json(
97
+ 'data/tsis1/tsis_v01.kernels.tm.json', sds_dir='data/generic', relative=True
98
+ )
99
+ print(mkrn)
100
+ # MetaKernel(Spacecraft(ISS_SC), Body(ISS_ELC3), Body(ISS_EXPA35), Body(TSIS_TADS),
101
+ # Body(TSIS_AZEL), Body(TSIS_TIM), Body(TSIS_TIM_GLINT))
102
+
103
+ with spicierpy.ext.load_kernel([mkrn.sds_kernels, mkrn.mission_kernels]):
104
+ print(spicierpy.ext.instrument_boresight('TSIS_TIM'))
105
+ # [0. 0. 1.]
106
+
107
+ mkrn = curryer.meta.MetaKernel.from_json(
108
+ 'tests/data/clarreo/cprs_v01.kernels.tm.json', sds_dir='data/generic', relative=True
109
+ )
110
+ print(mkrn)
111
+ # MetaKernel(Spacecraft(ISS_SC), Body(CPRS_BASE), Body(CPRS_PEDE),
112
+ # Body(CPRS_AZ), Body(CPRS_YOKE), Body(CPRS_EL), Body(CPRS_HYSICS))
113
+
114
+ with spicierpy.ext.load_kernel([mkrn.sds_kernels, mkrn.mission_kernels]):
115
+ print(curryer.compute.spatial.pixel_vectors('CPRS_HYSICS'))
116
+ # (480,
117
+ # array([[ 0.00173869, -0.08715574, 0.99619318],
118
+ # [ 0.0017315 , -0.08679351, 0.99622482],
119
+ # [ 0.00172431, -0.08643127, 0.99625632],
120
+ # ...,
121
+ # [-0.00171712, 0.08606901, 0.9962877 ],
122
+ # [-0.00172431, 0.08643127, 0.99625632],
123
+ # [-0.0017315 , 0.08679351, 0.99622482]]))
124
+ ```
125
+
126
+ ### SPICE Kernel Creation
127
+
128
+ Create CLARREO Dynamic Kernels:
129
+
130
+ ```python
131
+ import curryer
132
+
133
+ meta_kernel = 'tests/data/clarreo/cprs_v01.kernels.tm.json'
134
+ generic_dir = 'data/generic'
135
+ kernel_configs = [
136
+ 'data/clarreo/iss_sc_v01.ephemeris.spk.json',
137
+ 'data/clarreo/iss_sc_v01.attitude.ck.json',
138
+ 'data/clarreo/cprs_az_v01.attitude.ck.json',
139
+ 'data/clarreo/cprs_el_v01.attitude.ck.json',
140
+ ]
141
+ output_dir = '/tmp'
142
+ input_file_or_obj = 'tests/data/demo/cprs_geolocation_tlm_20230101_20240430.nc'
143
+
144
+ # Load meta kernel details. Includes existing static kernels.
145
+ mkrn = curryer.meta.MetaKernel.from_json(meta_kernel, relative=True, sds_dir=generic_dir)
146
+
147
+ # Create the dynamic kernels from the JSONs alone. Note that they
148
+ # contain the reference to the input_data netcdf4 file to read.
149
+ generated_kernels = []
150
+ creator = curryer.kernels.create.KernelCreator(overwrite=False, append=False)
151
+
152
+ # Generate the kernels from the config and input data (file or object).
153
+ for kernel_config in kernel_configs:
154
+ generated_kernels.append(creator.write_from_json(
155
+ kernel_config, output_kernel=output_dir, input_data=input_file_or_obj,
156
+ ))
157
+
158
+ ```
159
+
160
+ ### Level-1 Geospatial Processing
161
+
162
+ Geolocate CLARREO HYSICS Instrument:
163
+
164
+ ```python
165
+ import pandas as pd
166
+ import curryer
167
+
168
+ meta_kernel = 'tests/data/clarreo/cprs_v01.kernels.tm.json'
169
+ generic_dir = 'data/generic'
170
+
171
+ time_range = ('2023-01-01', '2023-01-01T00:05:00')
172
+ ugps_times = curryer.spicetime.adapt(pd.date_range(*time_range, freq='67ms', inclusive='left'), 'iso')
173
+
174
+ # Load meta kernel details. Includes existing static kernels.
175
+ mkrn = curryer.meta.MetaKernel.from_json(meta_kernel, relative=True, sds_dir=generic_dir)
176
+
177
+ # Geolocate all the individual pixels and create the L1A data product!
178
+ with curryer.spicierpy.ext.load_kernel([mkrn.sds_kernels, mkrn.mission_kernels]):
179
+ geoloc_inst = curryer.compute.spatial.Geolocate('CPRS_HYSICS')
180
+ l1a_dataset = geoloc_inst(ugps_times)
181
+ l1a_dataset.to_netcdf('cprs_geolocation_l1a_20230101.nc')
182
+
183
+ ```
184
+
185
+ _Assumes dynamic kernels have been created and their file names defined within
186
+ the metakernel JSON file._
@@ -0,0 +1,9 @@
1
+ """Helper library for using high-level SPICE functions, creating SPICE kernels,
2
+ and calculating spatial datasets.
3
+
4
+ @author: Brandon Stone
5
+ """
6
+
7
+ from . import compute, kernels, meta, spicetime, spicierpy, tle, utils
8
+
9
+ __all__ = ["compute", "kernels", "meta", "spicetime", "spicierpy", "tle", "utils"]
@@ -0,0 +1,8 @@
1
+ """Methods for computing spatial and pointing datasets.
2
+
3
+ @author: Brandon Stone
4
+ """
5
+
6
+ from . import constants, elevation, ephemeris, pointing, spatial
7
+
8
+ __all__ = ["constants", "elevation", "ephemeris", "pointing", "spatial"]
@@ -0,0 +1,142 @@
1
+ """Abstract mission data class.
2
+
3
+ @author: Brandon Stone
4
+ """
5
+
6
+ import functools
7
+ import logging
8
+ from abc import ABCMeta, abstractmethod
9
+ from io import StringIO
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def log_return(max_rows=5):
18
+ """Log any output tables/arrays from a function."""
19
+
20
+ def log_return_wrapper(func):
21
+ """Store the function to log output from."""
22
+ _logger = logging.getLogger(getattr(func, "__module__", __name__))
23
+
24
+ @functools.wraps(func)
25
+ def log_return_call(*args, **kwargs):
26
+ """Call the wrapper function and log the output."""
27
+ output = func(*args, **kwargs)
28
+
29
+ if _logger.isEnabledFor(logging.DEBUG):
30
+ output_name = output.__class__.__name__
31
+
32
+ # Table.
33
+ if isinstance(output, pd.DataFrame):
34
+ if pd.get_option("display.width") == 80:
35
+ pd.set_option("display.width", 140)
36
+ info_strm = StringIO()
37
+ _logger.debug("%s%s:\n %s", output_name, output.shape, output.to_string(max_rows=max_rows))
38
+ output.info(buf=info_strm)
39
+ _logger.debug("%s details:\n%s", output_name, info_strm.getvalue())
40
+
41
+ # Numpy array.
42
+ elif isinstance(output, np.ndarray):
43
+ if np.get_printoptions()["threshold"] == 1000:
44
+ np.set_printoptions(threshold=100)
45
+ _logger.debug("%s%s:\n %s", output_name, output.shape, output) # Auto trims.
46
+
47
+ return output
48
+
49
+ return log_return_call
50
+
51
+ return log_return_wrapper
52
+
53
+
54
+ class AbstractMissionData(metaclass=ABCMeta):
55
+ """Abstract class to get/write mission data."""
56
+
57
+ @abstractmethod
58
+ def __init__(self, microsecond_cadence, *args, **kwargs):
59
+ self.microsecond_cadence = microsecond_cadence
60
+ self._loaded_kernels = None
61
+ self.allow_nans = True
62
+
63
+ def __del__(self):
64
+ if not self._loaded_kernels:
65
+ return
66
+ try:
67
+ self._loaded_kernels.unload(clear=True)
68
+ except: # noqa E722
69
+ logging.exception("Error while unloading kernels:\n")
70
+
71
+ @log_return()
72
+ def get_times(self, ugps_range, cadence=None):
73
+ """Array of evenly spaced values.
74
+
75
+ Parameters
76
+ ----------
77
+ ugps_range : (int, int)
78
+ Time range [start, end).
79
+ cadence : int, optional
80
+ Number of microseconds to step by. Default=`self.DEFAULT_CADENCE`
81
+
82
+ Returns
83
+ -------
84
+ numpy.ndarray
85
+ Array of time values (int64).
86
+
87
+ """
88
+ if cadence is None:
89
+ cadence = self.microsecond_cadence
90
+ if len(ugps_range) != 2:
91
+ raise ValueError("Must specify two uGPS times: inclusive start, exclusive end.")
92
+
93
+ logger.debug("Determining times in range [%s] with cadence [%s]", ugps_range, cadence)
94
+ return np.arange(ugps_range[0], ugps_range[1], cadence)
95
+
96
+
97
+ def write_to_database(table, session, dbtable, auto_commit):
98
+ """Insert table values using the SQLAlchemy bulk insert method.
99
+
100
+ Parameters
101
+ ----------
102
+ table : DataFrame
103
+ session : Session
104
+ dbtable : Table
105
+ auto_commit : bool
106
+
107
+ Returns
108
+ -------
109
+ int
110
+
111
+ """
112
+ logger.info("Inserting [%s] rows into [%s] @ [%s]", table.shape[0], dbtable.__table__.fullname, session)
113
+
114
+ if table.shape[0] == 0:
115
+ logger.warning("Skipping insert for empty dataset [%s]!", dbtable.__table__.fullname)
116
+ return 0
117
+
118
+ query = session.query(
119
+ dbtable,
120
+ ).filter(
121
+ dbtable.ugps >= int(table.index.min()),
122
+ dbtable.ugps <= int(table.index.max()),
123
+ *[getattr(dbtable, col) == table[col].iloc[0] for col in table.columns if col != "ugps"],
124
+ # dbtable.instrumentmodeid == int(table['instrumentmodeid'].iloc[0]),
125
+ # dbtable.version == int(table['version'].iloc[0])
126
+ )
127
+ if query.count() != 0:
128
+ raise ValueError("Data already exists!")
129
+
130
+ # Bulk insert a list of dicts (rows).
131
+ # Convert DataFrame to a Numpy records array to maintain
132
+ # the individual column data types. Convert to a list to change
133
+ # from Numpy scalar types to Python scalar types (e.g., int).
134
+ # TODO: Add an iter to flush every (e.g., 1440 rows / day's worth)?
135
+ col_names = (table.index.name, *table.columns)
136
+ session.bulk_insert_mappings(dbtable, (dict(zip(col_names, row)) for row in table.to_records().tolist()))
137
+
138
+ if query.count() != table.shape[0]:
139
+ raise ValueError("Database is missing some data.")
140
+ if auto_commit:
141
+ session.commit()
142
+ return table.shape[0]
@@ -0,0 +1,76 @@
1
+ """SPICE related constants for TIM.
2
+
3
+ @author: Brandon Stone
4
+ """
5
+
6
+ from enum import IntFlag, unique
7
+
8
+ from spiceypy.utils.exceptions import SpiceyError
9
+
10
+ # Time-interval between output samples (TIM/SIM missions only).
11
+ # Units: Microseconds
12
+ EPHEMERIS_TIMESTEP_USEC = 60000000
13
+ ATTITUDE_TIMESTEP_USEC = 10000000
14
+
15
+ # Astronomical unit (2012 definition)
16
+ # Units: KM / 1 AU
17
+ # Source: http://www.iau.org/static/resolutions/IAU2012_English.pdf
18
+ # Note: Java uses ###.66 instead of ###.700
19
+ KM_PER_ASTRONOMICAL_UNIT = 149597870.700
20
+
21
+ # Speed of light in a vacuum
22
+ # Units: KM / SEC
23
+ SPEED_OF_LIGHT_KM_PER_S = 299792.458
24
+
25
+ # WGS84 Ellipsoid reference
26
+ # Units: KM
27
+ # Source: https://epsg.org/ellipsoid_7030/WGS-84.html
28
+ WGS84_SEMI_MAJOR_AXIS_KM = 6378.137
29
+ WGS84_SEMI_MINOR_AXIS_KM = 6356.752314245
30
+ WGS84_INVERSE_FLATTENING = 1 / 298.257223563
31
+ WGS84_ECCENTRICITY2 = 2 * WGS84_INVERSE_FLATTENING - WGS84_INVERSE_FLATTENING**2
32
+
33
+
34
+ @unique
35
+ class SpatialQualityFlags(IntFlag):
36
+ GOOD = 0x0
37
+
38
+ SPICE_ERR_NO_RESULTS_FOUND = 0x1 # E.g. no surface intersect.
39
+ SPICE_ERR_MISSING_EPHEMERIS = 0x2
40
+ SPICE_ERR_MISSING_ATTITUDE = 0x4
41
+ SPICE_ERR_INVALID_LOCATION = 0x8 # E.g. inside planet.
42
+ SPICE_ERR_UNKNOWN_NONSPECIFIC = 0x10
43
+
44
+ CALC_UNEXPECTED_NOT_FINITE = 0x20
45
+
46
+ CALC_KERNELS_UNDESIRED_TLM = 0x40
47
+ CALC_KERNELS_FAILED = 0x80
48
+
49
+ CALC_ELLIPS_INSUFF_DATA = 0x100
50
+ CALC_ELLIPS_NO_INTERSECT = 0x200
51
+
52
+ # TODO: CALC_TERRAIN_INSUFF_DATA ?
53
+ CALC_TERRAIN_EXTREME_ZENITH = 0x400
54
+ CALC_TERRAIN_MAX_ITER = 0x800
55
+ CALC_TERRAIN_NOT_FINITE = 0x1000
56
+
57
+ CALC_ANCIL_INSUFF_DATA = 0x2000
58
+ CALC_ANCIL_NOT_FINITE = 0x4000
59
+
60
+ @classmethod
61
+ def from_spice_error(cls, error: SpiceyError):
62
+ if error is None:
63
+ return cls.GOOD
64
+ if not isinstance(error, SpiceyError):
65
+ raise TypeError(f"Method requires a SpiceyError, not: {type(error)}")
66
+ # if isinstance(error, NotFoundError):
67
+ # return cls.SPICE_ERR_NO_LOOKUP_FOUND
68
+ if "returns not found" in error.message:
69
+ return cls.SPICE_ERR_NO_RESULTS_FOUND
70
+ if "SPICE(SPKINSUFFDATA)" in error.short:
71
+ return cls.SPICE_ERR_MISSING_EPHEMERIS
72
+ if "SPICE(NOFRAMECONNECT)" in error.short:
73
+ return cls.SPICE_ERR_MISSING_ATTITUDE
74
+ if "SPICE(NOTDISJOINT)" in error.short:
75
+ return cls.SPICE_ERR_INVALID_LOCATION
76
+ return cls.SPICE_ERR_UNKNOWN_NONSPECIFIC