spacecoords 0.1.0__py3-none-any.whl

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.

Potentially problematic release.


This version of spacecoords might be problematic. Click here for more details.

@@ -0,0 +1,44 @@
1
+ """General purpose convenience functions for different coordinate systems and linear algebra
2
+ functions
3
+ """
4
+
5
+ from types import ModuleType
6
+ import importlib.util
7
+ from .version import __version__
8
+
9
+ from . import linalg
10
+ from . import spherical
11
+ from . import constants
12
+
13
+
14
+ def _make_missing_module(name: str, dep: str):
15
+ class _MissingModule(ModuleType):
16
+ def __getattr__(self, key):
17
+ raise ImportError(
18
+ f"The optional dependency `{dep}` for is missing.\n"
19
+ f"Install it with `pip install spacecoords[all]` or `pip install {dep}`."
20
+ )
21
+
22
+ return _MissingModule(name)
23
+
24
+
25
+ # Optional modules
26
+ if importlib.util.find_spec("astropy") is not None:
27
+ from . import celestial
28
+ else:
29
+ astropy = _make_missing_module("celestial", "astropy")
30
+
31
+ if importlib.util.find_spec("jplephem") is not None:
32
+ from . import spk_basic
33
+ else:
34
+ naif_ephemeris = _make_missing_module("spk_basic", "jplephem")
35
+
36
+ if importlib.util.find_spec("spice") is not None:
37
+ from . import spice
38
+ else:
39
+ naif_spice = _make_missing_module("spice", "spiceypy")
40
+
41
+ if importlib.util.find_spec("requests") is not None:
42
+ from . import download
43
+ else:
44
+ download = _make_missing_module("download", "requests")
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env python
2
+
3
+ """Coordinate frame transformations and related functions.
4
+ Main usage is the `convert` function that wraps Astropy frame transformations.
5
+ """
6
+ from typing import Type, Any
7
+ from pathlib import Path
8
+ import numpy as np
9
+ from astropy.time import Time
10
+ import astropy.coordinates as coord
11
+ import astropy.units as units
12
+ import astropy.config as config
13
+
14
+ from .types import (
15
+ NDArray_N,
16
+ NDArray_3,
17
+ NDArray_6,
18
+ NDArray_3xN,
19
+ NDArray_6xN,
20
+ T,
21
+ )
22
+
23
+ """List of astropy frames
24
+ """
25
+ ASTROPY_FRAMES = {
26
+ "TEME": "TEME",
27
+ "ITRS": "ITRS",
28
+ "ITRF": "ITRS",
29
+ "ICRS": "ICRS",
30
+ "ICRF": "ICRS",
31
+ "GCRS": "GCRS",
32
+ "GCRF": "GCRS",
33
+ "HCRS": "HCRS",
34
+ "HCRF": "HCRS",
35
+ "HeliocentricMeanEcliptic".upper(): "HeliocentricMeanEcliptic",
36
+ "GeocentricMeanEcliptic".upper(): "GeocentricMeanEcliptic",
37
+ "HeliocentricTrueEcliptic".upper(): "HeliocentricTrueEcliptic",
38
+ "GeocentricTrueEcliptic".upper(): "GeocentricTrueEcliptic",
39
+ "BarycentricMeanEcliptic".upper(): "BarycentricMeanEcliptic",
40
+ "BarycentricTrueEcliptic".upper(): "BarycentricTrueEcliptic",
41
+ "SPICEJ2000": "ICRS",
42
+ }
43
+
44
+ """List of frames that are not time-dependant
45
+ """
46
+ ASTROPY_NOT_OBSTIME = [
47
+ "ICRS",
48
+ "BarycentricMeanEcliptic",
49
+ "BarycentricTrueEcliptic",
50
+ ]
51
+
52
+
53
+ def get_solarsystem_body_state(
54
+ body: str,
55
+ time: Time,
56
+ kernel_dir: Path,
57
+ ephemeris: str = "jpl",
58
+ ) -> NDArray_6xN | NDArray_6:
59
+ """
60
+
61
+ This is to not have to remember how to do this astropy config stuff
62
+ # https://docs.astropy.org/en/stable/api/astropy.coordinates.solar_system_ephemeris.html
63
+ """
64
+ with config.set_temp_cache(path=str(kernel_dir), delete=False):
65
+ pos, vel = coord.get_body_barycentric_posvel(body, time, ephemeris=ephemeris)
66
+
67
+ size = len(time)
68
+ shape: tuple[int, ...] = (6, size) if size > 0 else (6,)
69
+ state = np.empty(shape, dtype=np.float64)
70
+ state[:3, ...] = pos.xyz.to(units.m).value
71
+ state[3:, ...] = vel.d_xyz.to(units.m / units.s).value
72
+ return state
73
+
74
+
75
+ def not_geocentric(frame: str) -> bool:
76
+ """Check if the given frame name is one of the non-geocentric frames."""
77
+ frame = frame.upper()
78
+ return frame in ["ICRS", "ICRF", "HCRS", "HCRF"] or frame.startswith("Heliocentric".upper())
79
+
80
+
81
+ def is_geocentric(frame: str) -> bool:
82
+ """Check if the frame name is a supported geocentric frame"""
83
+ return not not_geocentric(frame)
84
+
85
+
86
+ def convert(
87
+ t: NDArray_N,
88
+ states: NDArray_6xN,
89
+ in_frame: str,
90
+ out_frame: str,
91
+ frame_kwargs: dict[str, Any],
92
+ ) -> NDArray_6xN:
93
+ """Perform predefined coordinate transformations using Astropy.
94
+ Always returns a copy of the array.
95
+
96
+ Parameters
97
+ ----------
98
+ t
99
+ Absolute time corresponding to the input states.
100
+ states
101
+ Size `(6,n)` matrix of states in SI units where rows 1-3
102
+ are position and 4-6 are velocity.
103
+ in_frame
104
+ Name of the frame the input states are currently in.
105
+ out_frame
106
+ Name of the state to transform to.
107
+ frame_kwargs
108
+ Any arguments needed for the specific transform detailed by `astropy`
109
+ in their documentation
110
+
111
+ Returns
112
+ -------
113
+ Size `(6,n)` matrix of states in SI units where rows
114
+ 1-3 are position and 4-6 are velocity.
115
+
116
+ """
117
+
118
+ in_frame = in_frame.upper()
119
+ out_frame = out_frame.upper()
120
+
121
+ if in_frame == out_frame:
122
+ return states.copy()
123
+
124
+ if in_frame in ASTROPY_FRAMES:
125
+ in_frame_ = ASTROPY_FRAMES[in_frame]
126
+ in_frame_cls = getattr(coord, in_frame_)
127
+ else:
128
+ err_str = [
129
+ f"In frame '{in_frame}' not recognized, ",
130
+ "please check spelling or perform manual transformation",
131
+ ]
132
+ raise ValueError("".join(err_str))
133
+
134
+ kw = {}
135
+ kw.update(frame_kwargs)
136
+ if in_frame_ not in ASTROPY_NOT_OBSTIME:
137
+ kw["obstime"] = t
138
+
139
+ astropy_states = _convert_to_astropy(states, in_frame_cls, kw)
140
+
141
+ if out_frame in ASTROPY_FRAMES:
142
+ out_frame_ = ASTROPY_FRAMES[out_frame]
143
+ out_frame_cls = getattr(coord, out_frame_)
144
+ else:
145
+ err_str = [
146
+ f"Out frame '{out_frame}' not recognized, ",
147
+ "please check spelling or perform manual transformation",
148
+ ]
149
+ raise ValueError("".join(err_str))
150
+
151
+ kw = {}
152
+ kw.update(frame_kwargs)
153
+ if out_frame_ not in ASTROPY_NOT_OBSTIME:
154
+ kw["obstime"] = t
155
+
156
+ out_states = astropy_states.transform_to(out_frame_cls(**kw))
157
+
158
+ rets = states.copy()
159
+ rets[:3, ...] = out_states.cartesian.xyz.to(units.m).value
160
+ rets[3:, ...] = out_states.velocity.d_xyz.to(units.m / units.s).value
161
+
162
+ return rets
163
+
164
+
165
+ def _convert_to_astropy(
166
+ states: NDArray_6xN | NDArray_6,
167
+ frame: Type[T],
168
+ frame_kwargs: dict[str, Any],
169
+ ) -> T:
170
+ state_p = coord.CartesianRepresentation(states[:3, ...] * units.m)
171
+ state_v = coord.CartesianDifferential(states[3:, ...] * units.m / units.s)
172
+ astropy_states = frame(state_p.with_differentials(state_v), **frame_kwargs) # type: ignore
173
+ return astropy_states
174
+
175
+
176
+ def geodetic_to_ITRS(
177
+ lat: NDArray_N | float,
178
+ lon: NDArray_N | float,
179
+ alt: NDArray_N | float,
180
+ degrees: bool = True,
181
+ ) -> NDArray_3xN | NDArray_3:
182
+ """Use `astropy.coordinates.WGS84GeodeticRepresentation` to transform from WGS84 to ITRS."""
183
+ ang_unit = units.deg if degrees else units.rad
184
+
185
+ wgs_cord = coord.WGS84GeodeticRepresentation(
186
+ lon=lon * ang_unit,
187
+ lat=lat * ang_unit,
188
+ height=alt * units.m,
189
+ )
190
+ itrs_cord = coord.ITRS(wgs_cord)
191
+
192
+ if isinstance(lat, np.ndarray):
193
+ size = lat.size
194
+ else:
195
+ size = 0
196
+
197
+ shape: tuple[int, ...] = (6, size) if size > 0 else (6,)
198
+ state = np.empty(shape, dtype=np.float64)
199
+ state[:3, ...] = itrs_cord.cartesian.xyz.to(units.m).value
200
+ state[3:, ...] = itrs_cord.velocity.d_xyz.to(units.m / units.s).value
201
+
202
+ return state
203
+
204
+
205
+ def ITRS_to_geodetic(
206
+ state: NDArray_3xN | NDArray_N,
207
+ degrees: bool = True,
208
+ ):
209
+ """Use `astropy.coordinates.WGS84GeodeticRepresentation` to transform from ITRS to WGS84."""
210
+ raise NotImplementedError()
211
+ # ang_unit = units.deg if degrees else units.rad
212
+ # astropy_states = _convert_to_astropy(state, coord.ITRS)
213
+ # wgs_cord = coord.WGS84GeodeticRepresentation(astropy_states)
214
+ #
215
+ # if isinstance(lat, np.ndarray):
216
+ # size = lat.size
217
+ # else:
218
+ # size = 0
219
+ #
220
+ # shape: tuple[int, ...] = (6, size) if size > 0 else (6,)
221
+ # state = np.empty(shape, dtype=np.float64)
222
+ # state[:3, ...] = itrs_cord.cartesian.xyz.to(units.m).value
223
+ # state[3:, ...] = itrs_cord.velocity.d_xyz.to(units.m / units.s).value
224
+ #
225
+ # return state
226
+ pass
spacecoords/cli.py ADDED
@@ -0,0 +1,22 @@
1
+ import argparse
2
+
3
+
4
+ def main():
5
+ import spacecoords as sc
6
+
7
+ parser = argparse.ArgumentParser(description="Download files")
8
+ subparsers = parser.add_subparsers(help="Available download interfaces", dest="command")
9
+
10
+ naif_parser = subparsers.add_parser("naif_kernel", help="Download NAIF Kernels")
11
+ naif_parser.add_argument(
12
+ "kernel_type",
13
+ choices=list(sc.download.KERNEL_PATHS.keys()),
14
+ help="Type of kernel (determines location on server)",
15
+ )
16
+ naif_parser.add_argument("kernel_name", help="Kernel filename")
17
+ naif_parser.add_argument("output_file", help="Path to output file")
18
+
19
+ args = parser.parse_args()
20
+
21
+ if args.command == "naif_kernel":
22
+ sc.download.naif_kernel_main(args)
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python
2
+
3
+ """Constants useful for different coordinate systems"""
4
+
5
+
6
+ G: float = 6.67430e-11
7
+ """Newtonian constant of gravitation (m^3 kg^-1 s^-2), CODATA Recommended Values of the Fundamental
8
+ Physical Constants 2022.
9
+ """
10
+
11
+
12
+ class WGS84:
13
+ """World Geodetic System 1984 constants."""
14
+
15
+ a: float = 6378.137 * 1e3
16
+ """float: semi-major axis parameter in meters of the World Geodetic System 1984 (WGS84)
17
+ """
18
+
19
+ b: float = 6356.7523142 * 1e3
20
+ """float: semi-minor axis parameter in meters of the World Geodetic System 1984 (WGS84)
21
+ """
22
+
23
+ esq: float = 6.69437999014 * 0.001
24
+ """float: `esq` parameter in meters of the World Geodetic System 1984 (WGS84)
25
+ """
26
+
27
+ e1sq: float = 6.73949674228 * 0.001
28
+ """float: `e1sq` parameter in meters of the World Geodetic System 1984 (WGS84)
29
+ """
30
+
31
+ f: float = 1 / 298.257223563
32
+ """float: `f` parameter of the World Geodetic System 1984 (WGS84)
33
+ """
34
+
35
+
36
+ R_earth: float = 6371.0088e3
37
+ """float: Radius of the Earth using the International
38
+ Union of Geodesy and Geophysics (IUGG) definition
39
+ """
40
+
41
+
42
+ class WGS72:
43
+ """World Geodetic System 1972 constants."""
44
+
45
+ MU_earth: float = 398600.8 * 1e9
46
+ """float: Standard gravitational parameter of the Earth using the WGS72 convention.
47
+ """
48
+
49
+ M_earth: float = MU_earth / G
50
+ """float: Mass of the Earth using the WGS72 convention.
51
+ """
@@ -0,0 +1,59 @@
1
+ import sys
2
+ import os
3
+ from pathlib import Path
4
+ import requests
5
+
6
+ NAIF_URL = "https://naif.jpl.nasa.gov/pub/naif/"
7
+
8
+ KERNEL_PATHS = {
9
+ "planetary": "generic_kernels/spk/planets/",
10
+ }
11
+
12
+
13
+ def naif_kernel(
14
+ kernel_path: str,
15
+ output_file: Path,
16
+ progress: bool = True,
17
+ chunk_size: int = 8192,
18
+ ):
19
+ url = NAIF_URL + kernel_path
20
+ with requests.get(url, stream=True) as r:
21
+ r.raise_for_status()
22
+ total_length = r.headers.get("content-length")
23
+ downloaded = 0
24
+ if total_length is None:
25
+ with open(output_file, "wb") as fh:
26
+ for chunk in r.iter_content(chunk_size=chunk_size):
27
+ fh.write(chunk)
28
+ if progress:
29
+ downloaded += len(chunk)
30
+ sys.stdout.write(f"{downloaded / 1024:.1f} KB")
31
+ sys.stdout.flush()
32
+ if progress:
33
+ print()
34
+ else:
35
+ total_length = int(total_length)
36
+ prog_width = min(os.get_terminal_size()[0] - 10, 100)
37
+ with open(output_file, "wb") as fh:
38
+ for chunk in r.iter_content(chunk_size=chunk_size):
39
+ if not chunk:
40
+ continue
41
+ fh.write(chunk)
42
+ if progress:
43
+ downloaded += len(chunk)
44
+ done = int(prog_width * downloaded / total_length)
45
+ sys.stdout.write(
46
+ f"\r[{'=' * done}{' ' * (prog_width - done)}] "
47
+ f"{downloaded / 1024:.1f} KB / {total_length / 1024:.1f} KB"
48
+ )
49
+ sys.stdout.flush()
50
+ if progress:
51
+ print()
52
+
53
+
54
+ def naif_kernel_main(args):
55
+ naif_kernel(
56
+ kernel_path=KERNEL_PATHS[args.kernel_type] + args.kernel_name,
57
+ output_file=args.output_file,
58
+ progress=True,
59
+ )
spacecoords/frames.py ADDED
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env python
2
+
3
+ """Transforms between common coordinate frames without any additional dependencies"""
4
+
5
+ import numpy as np
6
+ from .spherical import sph_to_cart
7
+ from .types import (
8
+ NDArray_N,
9
+ NDArray_3,
10
+ NDArray_3xN,
11
+ )
12
+
13
+
14
+ def ned_to_ecef(
15
+ lat: float,
16
+ lon: float,
17
+ ned: NDArray_3xN | NDArray_3,
18
+ degrees: bool = False,
19
+ ) -> NDArray_3xN | NDArray_3:
20
+ """NED (north/east/down) using geocentric zenith to ECEF coordinate system
21
+ conversion, not including translation.
22
+
23
+ Parameters
24
+ ----------
25
+ lat
26
+ Latitude of the origin in geocentric spherical coordinates
27
+ lon
28
+ Longitude of the origin in geocentric spherical coordinates
29
+ ned
30
+ (3,n) input matrix of positions in the NED-convention.
31
+ degrees
32
+ If `True`, use degrees. Else all angles are given in radians.
33
+
34
+ Returns
35
+ -------
36
+ (3,) or (3,n) array x,y and z coordinates in ECEF.
37
+ """
38
+ enu = np.empty(ned.size, dtype=ned.dtype)
39
+ enu[0, ...] = ned[1, ...]
40
+ enu[1, ...] = ned[0, ...]
41
+ enu[2, ...] = -ned[2, ...]
42
+ return enu_to_ecef(lat, lon, enu, degrees=degrees)
43
+
44
+
45
+ def azel_to_ecef(
46
+ lat: float,
47
+ lon: float,
48
+ az: NDArray_N | float,
49
+ el: NDArray_N | float,
50
+ degrees: bool = False,
51
+ ) -> NDArray_3xN | NDArray_3:
52
+ """Radar pointing (az,el) using geocentric zenith to unit vector in
53
+ ECEF, not including translation.
54
+
55
+ Parameters
56
+ ----------
57
+ lat
58
+ Latitude of the origin in geocentric spherical coordinates
59
+ lon
60
+ Longitude of the origin in geocentric spherical coordinates
61
+ az
62
+ Azimuth of the pointing direction
63
+ el
64
+ Elevation of the pointing direction
65
+ degrees
66
+ If `True`, use degrees. Else all angles are given in radians.
67
+
68
+ Returns
69
+ -------
70
+ (3,) or (3,n) array x,y and z coordinates in ECEF.
71
+ """
72
+ shape: tuple[int, ...] = (3,)
73
+
74
+ if isinstance(az, np.ndarray):
75
+ if len(az.shape) == 0:
76
+ az = float(az)
77
+ elif len(az) > 1:
78
+ shape = (3, len(az))
79
+ az = az.flatten()
80
+ else:
81
+ az = az[0]
82
+
83
+ if isinstance(el, np.ndarray):
84
+ if len(el.shape) == 0:
85
+ el = float(el)
86
+ elif len(el) > 1:
87
+ shape = (3, len(el))
88
+ el = el.flatten()
89
+ else:
90
+ el = el[0]
91
+
92
+ sph = np.empty(shape, dtype=np.float64)
93
+ sph[0, ...] = az
94
+ sph[1, ...] = el
95
+ sph[2, ...] = 1.0
96
+ enu = sph_to_cart(sph, degrees=degrees)
97
+ return enu_to_ecef(lat, lon, enu, degrees=degrees)
98
+
99
+
100
+ def enu_to_ecef(
101
+ lat: float,
102
+ lon: float,
103
+ enu: NDArray_3 | NDArray_3xN,
104
+ degrees: bool = False,
105
+ ) -> NDArray_3xN | NDArray_3:
106
+ """Rotate ENU (east/north/up) using geocentric zenith to ECEF coordinate system,
107
+ not including translation.
108
+
109
+ Parameters
110
+ ----------
111
+ lat
112
+ Latitude of the origin in geocentric spherical coordinates
113
+ lon
114
+ Longitude of the origin in geocentric spherical coordinates
115
+ enu
116
+ (3,n) input matrix of positions in the ENU-convention.
117
+ degrees
118
+ If `True`, use degrees. Else all angles are given in radians.
119
+
120
+ Returns
121
+ -------
122
+ (3,) or (3,n) array x,y and z coordinates in ECEF.
123
+ """
124
+ if degrees:
125
+ lat, lon = np.radians(lat), np.radians(lon)
126
+
127
+ mx = np.array(
128
+ [
129
+ [-np.sin(lon), -np.sin(lat) * np.cos(lon), np.cos(lat) * np.cos(lon)],
130
+ [np.cos(lon), -np.sin(lat) * np.sin(lon), np.cos(lat) * np.sin(lon)],
131
+ [0, np.cos(lat), np.sin(lat)],
132
+ ]
133
+ )
134
+
135
+ ecef = np.dot(mx, enu)
136
+ return ecef
137
+
138
+
139
+ def ecef_to_enu(
140
+ lat: float,
141
+ lon: float,
142
+ ecef: NDArray_3 | NDArray_3xN,
143
+ degrees: bool = False,
144
+ ) -> NDArray_3xN | NDArray_3:
145
+ """Rotate ECEF coordinate system to local ENU (east,north,up) using geocentric
146
+ zenith, not including translation.
147
+
148
+ Parameters
149
+ ----------
150
+ lat
151
+ Latitude of the origin in geocentric spherical coordinates
152
+ lon
153
+ Longitude of the origin in geocentric spherical coordinates
154
+ ecef
155
+ (3,) or (3,n) array x,y and z coordinates in ECEF.
156
+ degrees
157
+ If `True`, use degrees. Else all angles are given in radians.
158
+
159
+ Returns
160
+ -------
161
+ (3,) or (3,n) array x, y and z coordinates in ENU.
162
+ """
163
+ if degrees:
164
+ lat, lon = np.radians(lat), np.radians(lon)
165
+
166
+ mx = np.array(
167
+ [
168
+ [-np.sin(lon), -np.sin(lat) * np.cos(lon), np.cos(lat) * np.cos(lon)],
169
+ [np.cos(lon), -np.sin(lat) * np.sin(lon), np.cos(lat) * np.sin(lon)],
170
+ [0, np.cos(lat), np.sin(lat)],
171
+ ]
172
+ )
173
+ enu = np.dot(np.linalg.inv(mx), ecef)
174
+ return enu
spacecoords/linalg.py ADDED
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env python
2
+
3
+ """Useful utility functions related to linear algebra"""
4
+
5
+ import numpy as np
6
+ import numpy.typing as npt
7
+ from .types import (
8
+ NDArray_3,
9
+ NDArray_3xN,
10
+ NDArray_N,
11
+ NDArray_3x3,
12
+ NDArray_3x3xN,
13
+ NDArray_2x2,
14
+ NDArray_NxN,
15
+ )
16
+
17
+
18
+ def vector_angle(
19
+ a: NDArray_3 | NDArray_3xN, b: NDArray_3 | NDArray_3xN, degrees: bool = False
20
+ ) -> NDArray_N | float:
21
+ """Angle between two vectors.
22
+
23
+ Parameters
24
+ ----------
25
+ a
26
+ (3, N) or (3,) vector of Cartesian coordinates.
27
+ This argument is vectorized in the second array dimension.
28
+ b
29
+ (3, N) or (3,) vector of Cartesian coordinates.
30
+ This argument is vectorized in the second array dimension.
31
+ degrees
32
+ If `True`, use degrees. Else all angles are given in radians.
33
+
34
+ Returns
35
+ -------
36
+ (N, ) or float vector of angles between input vectors.
37
+
38
+ Notes
39
+ -----
40
+ Definition
41
+ $$
42
+ \\theta = \\cos^{-1}\\frac{
43
+ \\langle\\mathbf{a},\\mathbf{b}\\rangle
44
+ }{
45
+ |\\mathbf{a}||\\mathbf{b}|
46
+ }
47
+ $$
48
+ where $\\langle\\mathbf{a},\\mathbf{b}\\rangle$ is the dot
49
+ product and $|\\mathbf{a}|$ denotes the norm.
50
+
51
+ """
52
+ a_norm = np.linalg.norm(a, axis=0)
53
+ b_norm = np.linalg.norm(b, axis=0)
54
+
55
+ if len(a.shape) == 1:
56
+ proj = np.dot(a, b) / (a_norm * b_norm)
57
+ elif len(b.shape) == 1:
58
+ proj = np.dot(b, a) / (a_norm * b_norm)
59
+ else:
60
+ assert a.shape == b.shape, "Input shapes do not match"
61
+ proj = np.sum(a * b, axis=0) / (a_norm * b_norm)
62
+
63
+ if len(a.shape) == 1 and len(b.shape) == 1:
64
+ if proj > 1.0:
65
+ proj = 1.0
66
+ elif proj < -1.0:
67
+ proj = -1.0
68
+ else:
69
+ proj[proj > 1.0] = 1.0
70
+ proj[proj < -1.0] = -1.0
71
+
72
+ theta = np.arccos(proj)
73
+ if degrees:
74
+ theta = np.degrees(theta)
75
+
76
+ return theta
77
+
78
+
79
+ def rot_mat_x(
80
+ theta: NDArray_N | float, dtype: npt.DTypeLike = np.float64, degrees: bool = False
81
+ ) -> NDArray_3x3xN | NDArray_3x3:
82
+ """Compute matrix for rotation of R3 vector through angle theta
83
+ around the X-axis. For frame rotation, use the transpose.
84
+
85
+ Parameters
86
+ ----------
87
+ theta
88
+ Angle to rotate.
89
+ dtype
90
+ Numpy datatype of the rotation matrix.
91
+ degrees
92
+ If `True`, use degrees. Else all angles are given in radians.
93
+
94
+ Returns
95
+ -------
96
+ (3, 3) Rotation matrix, or (3, 3, n) tensor if theta is vector input.
97
+
98
+ """
99
+ if degrees:
100
+ theta = np.radians(theta)
101
+ size: tuple[int, ...]
102
+ if isinstance(theta, np.ndarray) and theta.ndim > 0:
103
+ size = (3, 3, len(theta))
104
+ else:
105
+ size = (3, 3)
106
+
107
+ ca, sa = np.cos(theta), np.sin(theta)
108
+ rot = np.zeros(size, dtype=dtype)
109
+ rot[0, 0, ...] = 1
110
+ rot[1, 1, ...] = ca
111
+ rot[1, 2, ...] = -sa
112
+ rot[2, 1, ...] = sa
113
+ rot[2, 2, ...] = ca
114
+ return rot
115
+
116
+
117
+ def rot_mat_y(
118
+ theta: NDArray_N | float, dtype: npt.DTypeLike = np.float64, degrees: bool = False
119
+ ) -> NDArray_3x3xN | NDArray_3x3:
120
+ """Compute matrix for rotation of R3 vector through angle theta
121
+ around the Y-axis. For frame rotation, use the transpose.
122
+
123
+ Parameters
124
+ ----------
125
+ theta
126
+ Angle to rotate.
127
+ dtype
128
+ Numpy datatype of the rotation matrix.
129
+ degrees
130
+ If `True`, use degrees. Else all angles are given in radians.
131
+
132
+ Returns
133
+ -------
134
+ (3, 3) Rotation matrix, or (3, 3, n) tensor if theta is vector input.
135
+
136
+ """
137
+ if degrees:
138
+ theta = np.radians(theta)
139
+ size: tuple[int, ...]
140
+ if isinstance(theta, np.ndarray) and theta.ndim > 0:
141
+ size = (3, 3, len(theta))
142
+ else:
143
+ size = (3, 3)
144
+
145
+ ca, sa = np.cos(theta), np.sin(theta)
146
+ rot = np.zeros(size, dtype=dtype)
147
+ rot[0, 0, ...] = ca
148
+ rot[0, 2, ...] = sa
149
+ rot[1, 1, ...] = 1
150
+ rot[2, 0, ...] = -sa
151
+ rot[2, 2, ...] = ca
152
+ return rot
153
+
154
+
155
+ def rot_mat_z(
156
+ theta: NDArray_N | float, dtype: npt.DTypeLike = np.float64, degrees: bool = False
157
+ ) -> NDArray_3x3xN | NDArray_3x3:
158
+ """Compute matrix for rotation of R3 vector through angle theta
159
+ around the Z-axis. For frame rotation, use the transpose.
160
+
161
+ Parameters
162
+ ----------
163
+ theta
164
+ Angle to rotate.
165
+ dtype
166
+ Numpy datatype of the rotation matrix.
167
+ degrees
168
+ If `True`, use degrees. Else all angles are given in radians.
169
+
170
+ Returns
171
+ -------
172
+ (3, 3) Rotation matrix, or (3, 3, n) tensor if theta is vector input.
173
+
174
+ """
175
+ if degrees:
176
+ theta = np.radians(theta)
177
+ size: tuple[int, ...]
178
+ if isinstance(theta, np.ndarray) and theta.ndim > 0:
179
+ size = (3, 3, len(theta))
180
+ else:
181
+ size = (3, 3)
182
+
183
+ ca, sa = np.cos(theta), np.sin(theta)
184
+ rot = np.zeros(size, dtype=dtype)
185
+ rot[0, 0, ...] = ca
186
+ rot[0, 1, ...] = -sa
187
+ rot[1, 0, ...] = sa
188
+ rot[1, 1, ...] = ca
189
+ rot[2, 2, ...] = 1
190
+ return rot
191
+
192
+
193
+ def rot_mat_2d(
194
+ theta: float,
195
+ dtype: npt.DTypeLike = np.float64,
196
+ degrees: bool = False,
197
+ ) -> NDArray_2x2:
198
+ """Matrix for rotation of R2 vector in the plane through angle theta
199
+ For frame rotation, use the transpose.
200
+
201
+ Parameters
202
+ ----------
203
+ theta : float
204
+ Angle to rotate.
205
+ dtype : numpy.dtype
206
+ Numpy datatype of the rotation matrix.
207
+ degrees : bool
208
+ If :code:`True`, use degrees. Else all angles are given in radians.
209
+
210
+ Returns
211
+ -------
212
+ numpy.ndarray
213
+ (2, 2) Rotation matrix.
214
+
215
+ """
216
+ if degrees:
217
+ theta = np.radians(theta)
218
+
219
+ ca, sa = np.cos(theta), np.sin(theta)
220
+ return np.array([[ca, -sa], [sa, ca]], dtype=dtype)
221
+
222
+
223
+ def scale_mat_2d(
224
+ x: float,
225
+ y: float,
226
+ dtype: npt.DTypeLike = np.float64,
227
+ ) -> NDArray_2x2:
228
+ """Matrix for 2d scaling.
229
+
230
+ Parameters
231
+ ----------
232
+ x
233
+ Scaling coefficient for first coordinate axis.
234
+ y
235
+ Scaling coefficient for second coordinate axis.
236
+
237
+ Returns
238
+ -------
239
+ (2, 2) Scaling matrix.
240
+ """
241
+ M_scale = np.zeros((2, 2), dtype=dtype)
242
+ M_scale[0, 0] = x
243
+ M_scale[1, 1] = y
244
+ return M_scale
245
+
246
+
247
+ def vec_to_vec(vec_in: NDArray_N, vec_out: NDArray_N) -> NDArray_NxN:
248
+ """Get the rotation matrix that rotates `vec_in` to `vec_out` along the
249
+ plane containing both. Uses quaternion calculations.
250
+ """
251
+ N = len(vec_in)
252
+ if N != len(vec_out):
253
+ raise ValueError("Input and output vectors must be same dimensionality.")
254
+ assert N == 3, "Only implemented for 3d vectors"
255
+
256
+ a = vec_in / np.linalg.norm(vec_in)
257
+ b = vec_out / np.linalg.norm(vec_out)
258
+
259
+ adotb = np.dot(a, b)
260
+ axb = np.cross(a, b)
261
+ axb_norm = np.linalg.norm(axb)
262
+
263
+ # rotation in the plane frame of `vec_in` and `vec_out`
264
+ G = np.zeros((N, N), dtype=vec_in.dtype)
265
+ G[0, 0] = adotb
266
+ G[0, 1] = -axb_norm
267
+ G[1, 0] = axb_norm
268
+ G[1, 1] = adotb
269
+ G[2, 2] = 1
270
+
271
+ # inverse of change of basis from standard orthonormal to `vec_in` and `vec_out` plane
272
+ F = np.zeros((N, N), dtype=vec_in.dtype)
273
+ F[:, 0] = a
274
+ F[:, 1] = (b - adotb * a) / np.linalg.norm(b - adotb * a)
275
+ F[:, 2] = axb
276
+
277
+ # go to frame, rotation in plane, leave frame
278
+ R = F @ G @ np.linalg.inv(F)
279
+
280
+ return R
spacecoords/py.typed ADDED
File without changes
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env python
2
+
3
+ """Functions related to spherical coordinate systems"""
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+ from .types import NDArray_3, NDArray_3xN, NDArray_N
8
+
9
+ from . import linalg
10
+
11
+ CLOSE_TO_POLE_LIMIT = 1e-9**2
12
+ CLOSE_TO_POLE_LIMIT_rad = np.arctan(1 / np.sqrt(CLOSE_TO_POLE_LIMIT))
13
+
14
+
15
+ def arctime_to_degrees(minutes: NDArray | float, seconds: NDArray | float) -> NDArray | float:
16
+ return (minutes + seconds / 60.0) / 60.0
17
+
18
+
19
+ def cart_to_sph(vec: NDArray_3 | NDArray_3xN, degrees: bool = False) -> NDArray_3 | NDArray_3xN:
20
+ """Convert from Cartesian coordinates (east, north, up) to Spherical
21
+ coordinates (azimuth, elevation, range) in a angle east of north and
22
+ elevation fashion. Returns azimuth between [-pi, pi] and elevation between
23
+ [-pi/2, pi/2].
24
+
25
+ Parameters
26
+ ----------
27
+ vec
28
+ (3, N) or (3,) vector of Cartesian coordinates (east, north, up).
29
+ This argument is vectorized in the second array dimension.
30
+ degrees
31
+ If `True`, use degrees. Else all angles are given in radians.
32
+
33
+ Returns
34
+ -------
35
+ (3, N) or (3, ) vector of Spherical coordinates
36
+ (azimuth, elevation, range).
37
+
38
+ Notes
39
+ -----
40
+ Azimuth close to pole convention
41
+ Uses a :code:`CLOSE_TO_POLE_LIMIT` constant when transforming determine
42
+ if the point is close to the pole and sets the azimuth by definition
43
+ to 0 "at" the poles for consistency.
44
+
45
+ """
46
+
47
+ r2_ = vec[0, ...] ** 2 + vec[1, ...] ** 2
48
+
49
+ sph = np.empty(vec.shape, dtype=vec.dtype)
50
+
51
+ if len(vec.shape) == 1:
52
+ if r2_ < CLOSE_TO_POLE_LIMIT:
53
+ sph[0] = 0.0
54
+ sph[1] = np.sign(vec[2]) * np.pi * 0.5
55
+ else:
56
+ sph[0] = np.arctan2(vec[0], vec[1])
57
+ sph[1] = np.arctan(vec[2] / np.sqrt(r2_))
58
+ else:
59
+ inds_ = r2_ < CLOSE_TO_POLE_LIMIT
60
+ not_inds_ = np.logical_not(inds_)
61
+
62
+ sph[0, inds_] = 0.0
63
+ sph[1, inds_] = np.sign(vec[2, inds_]) * np.pi * 0.5
64
+ sph[0, not_inds_] = np.arctan2(vec[0, not_inds_], vec[1, not_inds_])
65
+ sph[1, not_inds_] = np.arctan(vec[2, not_inds_] / np.sqrt(r2_[not_inds_]))
66
+
67
+ sph[2, ...] = np.sqrt(r2_ + vec[2, ...] ** 2)
68
+ if degrees:
69
+ sph[:2, ...] = np.degrees(sph[:2, ...])
70
+
71
+ return sph
72
+
73
+
74
+ def sph_to_cart(vec: NDArray_3 | NDArray_3xN, degrees: bool = False) -> NDArray_3 | NDArray_3xN:
75
+ """Convert from spherical coordinates (azimuth, elevation, range) to
76
+ Cartesian (east, north, up) in a angle east of north and elevation fashion.
77
+
78
+
79
+ Parameters
80
+ ----------
81
+ vec
82
+ (3, N) or (3,) vector of Cartesian Spherical
83
+ (azimuth, elevation, range).
84
+ This argument is vectorized in the second array dimension.
85
+ degrees
86
+ If :code:`True`, use degrees. Else all angles are given in radians.
87
+
88
+ Returns
89
+ -------
90
+ (3, N) or (3, ) vector of Cartesian coordinates (east, north, up).
91
+
92
+ """
93
+
94
+ _az = vec[0, ...]
95
+ _el = vec[1, ...]
96
+ if degrees:
97
+ _az, _el = np.radians(_az), np.radians(_el)
98
+ cart = np.empty(vec.shape, dtype=vec.dtype)
99
+
100
+ cart[0, ...] = vec[2, ...] * np.sin(_az) * np.cos(_el)
101
+ cart[1, ...] = vec[2, ...] * np.cos(_az) * np.cos(_el)
102
+ cart[2, ...] = vec[2, ...] * np.sin(_el)
103
+
104
+ return cart
105
+
106
+
107
+ def az_el_to_sph(
108
+ azimuth: NDArray_N | float,
109
+ elevation: NDArray_N | float,
110
+ ) -> NDArray_3xN | NDArray_3:
111
+ """Convert input azimuth and elevation to spherical coordinates states,
112
+ i.e a `shape=(3,n)` numpy array.
113
+ """
114
+
115
+ az_len = azimuth.size if isinstance(azimuth, np.ndarray) else None
116
+ el_len = elevation.size if isinstance(elevation, np.ndarray) else None
117
+
118
+ if el_len is not None and az_len is not None:
119
+ assert el_len == az_len, f"azimuth {az_len} and elevation {el_len} sizes must agree"
120
+
121
+ shape: tuple[int] | tuple[int, int]
122
+ if az_len is not None:
123
+ shape = (3, az_len)
124
+ elif el_len is not None:
125
+ shape = (3, el_len)
126
+ else:
127
+ shape = (3,)
128
+
129
+ sph = np.empty(shape, dtype=np.float64)
130
+ sph[0, ...] = azimuth
131
+ sph[1, ...] = elevation
132
+ sph[2, ...] = 1.0
133
+
134
+ return sph
135
+
136
+
137
+ def az_el_point(
138
+ self, azimuth: NDArray_N | float, elevation: NDArray_N | float, degrees: bool = False
139
+ ) -> NDArray_3xN | NDArray_3:
140
+ """Point beam towards azimuth and elevation coordinate.
141
+
142
+ Parameters
143
+ ----------
144
+ azimuth : float
145
+ Azimuth east of north of pointing direction.
146
+ elevation : float
147
+ Elevation from horizon of pointing direction.
148
+ degrees : bool
149
+ If :code:`True` all input/output angles are in degrees,
150
+ else they are in radians. Defaults to instance
151
+ settings :code:`self.radians`.
152
+
153
+ """
154
+ sph = az_el_to_sph(azimuth, elevation)
155
+ return sph_to_cart(sph, degrees=degrees)
156
+
157
+
158
+ def az_el_vs_cart_angle(
159
+ self,
160
+ azimuth: NDArray_N | float,
161
+ elevation: NDArray_N | float,
162
+ cart: NDArray_3xN | NDArray_3,
163
+ degrees: bool = False,
164
+ ) -> NDArray_N | float:
165
+ """Get angle between azimuth and elevation and pointing direction.
166
+
167
+ Parameters
168
+ ----------
169
+ azimuth : float or NDArray
170
+ Azimuth east of north of pointing direction.
171
+ elevation : float or NDArray
172
+ Elevation from horizon of pointing direction.
173
+ degrees : bool
174
+ If :code:`True` all input/output angles are in degrees,
175
+ else they are in radians.
176
+
177
+ Returns
178
+ -------
179
+ float or NDArray
180
+ Angle between pointing and given direction.
181
+
182
+ """
183
+ sph = az_el_to_sph(azimuth, elevation)
184
+ k = sph_to_cart(sph, degrees=degrees)
185
+ return linalg.vector_angle(cart, k, degrees=degrees)
spacecoords/spice.py ADDED
@@ -0,0 +1,8 @@
1
+ """This does advanced spice things that jplephem can not do
2
+
3
+ #TODO: impalement what is needed here
4
+ """
5
+
6
+ import spiceypy as spice
7
+
8
+ # spice.furnsh("./cassMetaK.txt")
@@ -0,0 +1,71 @@
1
+ from collections import OrderedDict
2
+ import numpy as np
3
+ from jplephem.spk import SPK
4
+
5
+ """Mapping from body name to integer id's used by the kernels.
6
+
7
+ #todo: these can be expanded to get any body probably? and do more than astropy implementation does
8
+ """
9
+ BODY_NAME_TO_KERNEL_SPEC = OrderedDict(
10
+ [
11
+ ("sun", [(0, 10)]),
12
+ ("mercury", [(0, 1), (1, 199)]),
13
+ ("venus", [(0, 2), (2, 299)]),
14
+ ("earth-moon-barycenter", [(0, 3)]),
15
+ ("earth", [(0, 3), (3, 399)]),
16
+ ("moon", [(0, 3), (3, 301)]),
17
+ ("mars", [(0, 4)]),
18
+ ("jupiter", [(0, 5)]),
19
+ ("saturn", [(0, 6)]),
20
+ ("uranus", [(0, 7)]),
21
+ ("neptune", [(0, 8)]),
22
+ ("pluto", [(0, 9)]),
23
+ ]
24
+ )
25
+
26
+
27
+ def get_solarsystem_body_states(bodies, epoch, kernel, units=None):
28
+ """Open a kernel file and get the statates of the given bodies at epoch in ICRS.
29
+
30
+ Note: All outputs from kernel computations are in the Barycentric (ICRS) "eternal" frame.
31
+ """
32
+ assert SPK is not None, "jplephem package needed to directly interact with kernels"
33
+ states = {}
34
+
35
+ kernel = SPK.open(kernel)
36
+
37
+ epoch_ = epoch.tdb # jplephem uses Barycentric Dynamical Time (TDB)
38
+ jd1, jd2 = epoch_.jd1, epoch_.jd2
39
+
40
+ for body in bodies:
41
+ body_ = body.lower().strip()
42
+
43
+ if body_ not in BODY_NAME_TO_KERNEL_SPEC:
44
+ raise ValueError(f'Body name "{body}" not recognized')
45
+
46
+ states[body] = np.zeros((6,), dtype=np.float64)
47
+
48
+ # if there are multiple steps to go from states to
49
+ # ICRS barycentric, iterate trough and combine
50
+ for pair in BODY_NAME_TO_KERNEL_SPEC[body_]:
51
+ spk = kernel[pair]
52
+ if spk.data_type == 3:
53
+ # Type 3 kernels contain both position and velocity.
54
+ posvel = spk.compute(jd1, jd2).flatten()
55
+ else:
56
+ pos_, vel_ = spk.compute_and_differentiate(jd1, jd2)
57
+ posvel = np.zeros((6,), dtype=np.float64)
58
+ posvel[:3] = pos_
59
+ posvel[3:] = vel_
60
+
61
+ states[body] += posvel
62
+
63
+ # units from kernels are usually in km and km/day
64
+ if units is None:
65
+ states[body] *= 1e3
66
+ states[body][3:] /= 86400.0
67
+ else:
68
+ states[body] *= units[0]
69
+ states[body][3:] /= units[1]
70
+
71
+ return states
spacecoords/types.py ADDED
@@ -0,0 +1,35 @@
1
+ """
2
+ This module contains convenient type information so that typing can be precise
3
+ but not too verbose in the code itself.
4
+ """
5
+ from typing import TypeVar
6
+ import numpy.typing as npt
7
+
8
+ T = TypeVar("T")
9
+
10
+ NDArray_N = npt.NDArray
11
+ "(n,) shaped ndarray"
12
+
13
+ NDArray_3 = npt.NDArray
14
+ "(3,) shaped ndarray"
15
+
16
+ NDArray_6 = npt.NDArray
17
+ "(6,) shaped ndarray"
18
+
19
+ NDArray_3xN = npt.NDArray
20
+ "(3,n) shaped ndarray"
21
+
22
+ NDArray_6xN = npt.NDArray
23
+ "(6,n) shaped ndarray"
24
+
25
+ NDArray_NxN = npt.NDArray
26
+ "(n,n) shaped ndarray"
27
+
28
+ NDArray_3x3 = npt.NDArray
29
+ "(3,3) shaped ndarray"
30
+
31
+ NDArray_2x2 = npt.NDArray
32
+ "(2,2) shaped ndarray"
33
+
34
+ NDArray_3x3xN = npt.NDArray
35
+ "(3,3,n) shaped ndarray"
spacecoords/version.py ADDED
@@ -0,0 +1,3 @@
1
+ import importlib.metadata
2
+
3
+ __version__ = importlib.metadata.version("spacecoords")
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: spacecoords
3
+ Version: 0.1.0
4
+ Summary: A collection of coordinate transforms for convenient use
5
+ Author-email: Daniel Kastinen <daniel.kastinen@irf.se>
6
+ Maintainer-email: Daniel Kastinen <daniel.kastinen@irf.se>
7
+ License: MIT License
8
+
9
+ Copyright (c) [2025] [Daniel Kastinen]
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Project-URL: Documentation, https://danielk.developer.irf.se/spacecoords/
30
+ Project-URL: Repository, https://github.com/danielk333/spacecoords
31
+ Classifier: Intended Audience :: Science/Research
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: Topic :: Scientific/Engineering :: Physics
34
+ Classifier: Operating System :: OS Independent
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Requires-Python: >=3.10
37
+ Description-Content-Type: text/markdown
38
+ License-File: LICENSE
39
+ Requires-Dist: numpy>=2
40
+ Provides-Extra: all
41
+ Requires-Dist: astropy>=6; extra == "all"
42
+ Requires-Dist: spiceypy>=8.0.0; extra == "all"
43
+ Requires-Dist: jplephem>=2.23; extra == "all"
44
+ Requires-Dist: requests>=2.20; extra == "all"
45
+ Provides-Extra: develop
46
+ Requires-Dist: pytest; extra == "develop"
47
+ Requires-Dist: pytest-cov; extra == "develop"
48
+ Requires-Dist: flake8; extra == "develop"
49
+ Requires-Dist: wheel; extra == "develop"
50
+ Requires-Dist: build; extra == "develop"
51
+ Requires-Dist: twine; extra == "develop"
52
+ Requires-Dist: auditwheel; extra == "develop"
53
+ Requires-Dist: numpydoc; extra == "develop"
54
+ Requires-Dist: black; extra == "develop"
55
+ Requires-Dist: matplotlib; extra == "develop"
56
+ Requires-Dist: ipykernel; extra == "develop"
57
+ Requires-Dist: mkdocs-material; extra == "develop"
58
+ Requires-Dist: mkdocstrings[python]; extra == "develop"
59
+ Requires-Dist: mkdocs-jupyter; extra == "develop"
60
+ Requires-Dist: mkdocs-gen-files; extra == "develop"
61
+ Requires-Dist: mkdocs-literate-nav; extra == "develop"
62
+ Requires-Dist: mkdocs-section-index; extra == "develop"
63
+ Provides-Extra: tests
64
+ Requires-Dist: pytest; extra == "tests"
65
+ Requires-Dist: pytest-cov; extra == "tests"
66
+ Dynamic: license-file
67
+
68
+ # spacecoords
69
+
70
+ A collection of coordinate transforms for convenient use across our applications to avoid
71
+ duplication
@@ -0,0 +1,19 @@
1
+ spacecoords/__init__.py,sha256=Pnio_jaYCpNxzWRde23PFCnk-t_fvq5zavoWT-ObRN0,1230
2
+ spacecoords/celestial.py,sha256=_WEQXv4YGRpa6_9kyWVNMJjcx-pJQG5rvYF6IA25V-E,6657
3
+ spacecoords/cli.py,sha256=Esbbi3-u-xfvXSG5P8QqpT0odElwUreza_J1wmdS5yU,737
4
+ spacecoords/constants.py,sha256=3BTHyhUFMXNzFrzuE-2FaRkEVTFW9khQQdG2IOoyFC0,1375
5
+ spacecoords/download.py,sha256=h1e46ekYJC-TZeP4PpKoEyPSVPNWItWE1z-5PXWyauw,1931
6
+ spacecoords/frames.py,sha256=u5og846ZXVGXyyWPMrRSX3Y82-09dVPd3Jh9uVH1w7Q,4646
7
+ spacecoords/linalg.py,sha256=YT4F0y76_OLk-I0az89i6J3fL4PjIE4Q2I4wByd0Sb0,7189
8
+ spacecoords/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ spacecoords/spherical.py,sha256=x_6pFAyEI-XySui70B9RWJSdKhYponCGX-jFpgh_njk,5589
10
+ spacecoords/spice.py,sha256=CibzR8EopwAwLvtd0BVGkRPjrScILENlLQUU_YcpWMU,164
11
+ spacecoords/spk_basic.py,sha256=CA4cZe6Y8Mw3UfL8X2ZaVFmvxWPnrcWAF6dgn2BeCQk,2335
12
+ spacecoords/types.py,sha256=bDO9YJWNUb1M6IAIgNaNlEvJwJbV3T9Pc0QO_DWTOhQ,644
13
+ spacecoords/version.py,sha256=56qNmktYm-a9EiinIbbIJG-6XTFmNReaWyZTcLJ15SI,83
14
+ spacecoords-0.1.0.dist-info/licenses/LICENSE,sha256=HwPwsjPm8Dw0iFn-o4B6E-X4irUfOlYFHAI3jB2bTWM,1076
15
+ spacecoords-0.1.0.dist-info/METADATA,sha256=RvssUD5fWfrTwtE1pL0OEpohTY0pgp22b1fnDvggKaE,3257
16
+ spacecoords-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ spacecoords-0.1.0.dist-info/entry_points.txt,sha256=lDDxb87uHwAnvKkEVGvBxBbGOfTjYmWXp6BHBYpBjq4,53
18
+ spacecoords-0.1.0.dist-info/top_level.txt,sha256=jknXkDXmkZUa9yRuVhcgbl5Da8LVxQsLofpQiLO0n70,12
19
+ spacecoords-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ spacecoords = spacecoords.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2025] [Daniel Kastinen]
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 @@
1
+ spacecoords