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.
- lasp_curryer-0.2.0/LICENSE +21 -0
- lasp_curryer-0.2.0/NOTICE +2 -0
- lasp_curryer-0.2.0/PKG-INFO +243 -0
- lasp_curryer-0.2.0/README.md +186 -0
- lasp_curryer-0.2.0/curryer/__init__.py +9 -0
- lasp_curryer-0.2.0/curryer/compute/__init__.py +8 -0
- lasp_curryer-0.2.0/curryer/compute/abstract.py +142 -0
- lasp_curryer-0.2.0/curryer/compute/constants.py +76 -0
- lasp_curryer-0.2.0/curryer/compute/elevation.py +1011 -0
- lasp_curryer-0.2.0/curryer/compute/ephemeris.py +176 -0
- lasp_curryer-0.2.0/curryer/compute/pointing.py +523 -0
- lasp_curryer-0.2.0/curryer/compute/spatial.py +1677 -0
- lasp_curryer-0.2.0/curryer/correction/__init__.py +0 -0
- lasp_curryer-0.2.0/curryer/correction/correction_config.py +131 -0
- lasp_curryer-0.2.0/curryer/correction/data_structures.py +119 -0
- lasp_curryer-0.2.0/curryer/correction/dataio.py +277 -0
- lasp_curryer-0.2.0/curryer/correction/geolocation_error_stats.py +537 -0
- lasp_curryer-0.2.0/curryer/correction/image_match.py +361 -0
- lasp_curryer-0.2.0/curryer/correction/monte_carlo.py +2467 -0
- lasp_curryer-0.2.0/curryer/correction/pairing.py +591 -0
- lasp_curryer-0.2.0/curryer/correction/psf.py +328 -0
- lasp_curryer-0.2.0/curryer/correction/search.py +126 -0
- lasp_curryer-0.2.0/curryer/kernels/__init__.py +8 -0
- lasp_curryer-0.2.0/curryer/kernels/attitude.py +361 -0
- lasp_curryer-0.2.0/curryer/kernels/classes.py +432 -0
- lasp_curryer-0.2.0/curryer/kernels/create.py +275 -0
- lasp_curryer-0.2.0/curryer/kernels/ephemeris.py +424 -0
- lasp_curryer-0.2.0/curryer/kernels/templates/kernel_macros.jinja2 +76 -0
- lasp_curryer-0.2.0/curryer/kernels/templates/mkspk_setup_template.txt +42 -0
- lasp_curryer-0.2.0/curryer/kernels/templates/msopck_setup_template.txt +42 -0
- lasp_curryer-0.2.0/curryer/kernels/writer.py +273 -0
- lasp_curryer-0.2.0/curryer/meta.py +103 -0
- lasp_curryer-0.2.0/curryer/spicetime/__init__.py +55 -0
- lasp_curryer-0.2.0/curryer/spicetime/constants.py +35 -0
- lasp_curryer-0.2.0/curryer/spicetime/convert.py +686 -0
- lasp_curryer-0.2.0/curryer/spicetime/leapsecond.py +273 -0
- lasp_curryer-0.2.0/curryer/spicetime/native.py +143 -0
- lasp_curryer-0.2.0/curryer/spicetime/utils.py +162 -0
- lasp_curryer-0.2.0/curryer/spicierpy/__init__.py +32 -0
- lasp_curryer-0.2.0/curryer/spicierpy/ext.py +511 -0
- lasp_curryer-0.2.0/curryer/spicierpy/obj.py +652 -0
- lasp_curryer-0.2.0/curryer/spicierpy/vectorized.py +170 -0
- lasp_curryer-0.2.0/curryer/tle.py +346 -0
- lasp_curryer-0.2.0/curryer/utils.py +251 -0
- 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,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,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
|