astroz 0.4.1__cp312-cp312-manylinux_2_38_x86_64.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.
astroz/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """astroz - High-performance astrodynamics library."""
2
+
3
+ import atexit
4
+ from .__version__ import __version__
5
+ from ._lib import _lib
6
+ from .tle import Tle
7
+ from .sgp4 import Sgp4
8
+ from . import orbital
9
+ from . import coords
10
+
11
+ # initialize on import
12
+ _lib.astroz_init()
13
+ atexit.register(_lib.astroz_deinit)
14
+
15
+
16
+ def version() -> str:
17
+ """Return astroz version string."""
18
+ v = _lib.astroz_version()
19
+ return f"{(v >> 16) & 0xFF}.{(v >> 8) & 0xFF}.{v & 0xFF}"
20
+
21
+
22
+ __all__ = ["__version__", "version", "Tle", "Sgp4", "orbital", "coords"]
astroz/__version__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.4.1"
astroz/_lib.py ADDED
@@ -0,0 +1,111 @@
1
+ """Load the astroz shared library."""
2
+
3
+ import ctypes
4
+ import os
5
+ import platform
6
+
7
+
8
+ _LIB_NAMES = {
9
+ "Linux": "libastroz_c.so",
10
+ "Darwin": "libastroz_c.dylib",
11
+ "Windows": "astroz_c.dll",
12
+ }
13
+
14
+
15
+ def _find_library():
16
+ """Find libastroz_c shared library.
17
+
18
+ Search order:
19
+ 1. ASTROZ_LIB env var (explicit override)
20
+ 2. Next to this package (installed via pip)
21
+ 3. zig-out/lib/ (development)
22
+ """
23
+ system = platform.system()
24
+ name = _LIB_NAMES.get(system)
25
+ if name is None:
26
+ raise OSError(f"Unsupported platform: {system}")
27
+
28
+ # Explicit override
29
+ env_path = os.environ.get("ASTROZ_LIB")
30
+ if env_path:
31
+ if os.path.exists(env_path):
32
+ return env_path
33
+ raise OSError(f"ASTROZ_LIB set but file not found: {env_path}")
34
+
35
+ # Standard search paths
36
+ pkg_dir = os.path.dirname(__file__)
37
+ search_paths = [
38
+ os.path.join(pkg_dir, "lib", name), # installed package (wheel)
39
+ os.path.join(pkg_dir, name), # legacy location
40
+ os.path.join(pkg_dir, "..", "..", "zig-out", "lib", name), # dev
41
+ ]
42
+
43
+ for path in search_paths:
44
+ if os.path.exists(path):
45
+ return path
46
+
47
+ raise OSError(
48
+ f"Cannot find {name}. Either:\n"
49
+ f" - Run 'zig build c-api' for development\n"
50
+ f" - Set ASTROZ_LIB=/path/to/{name}"
51
+ )
52
+
53
+
54
+ _lib = ctypes.CDLL(_find_library())
55
+
56
+ # aliases
57
+ _ptr = ctypes.c_void_p
58
+ _i32 = ctypes.c_int32
59
+ _u32 = ctypes.c_uint32
60
+ _f32 = ctypes.c_float
61
+ _f64 = ctypes.c_double
62
+ _str = ctypes.c_char_p
63
+ _out_ptr = ctypes.POINTER(ctypes.c_void_p)
64
+ _out_vec3 = ctypes.POINTER(ctypes.c_double * 3)
65
+
66
+
67
+ def _sig(func, args, ret=_i32):
68
+ """Set function signature: argtypes and restype."""
69
+ func.argtypes = args
70
+ func.restype = ret
71
+
72
+
73
+ _sig(_lib.astroz_init, [], None)
74
+ _sig(_lib.astroz_deinit, [], None)
75
+ _sig(_lib.astroz_version, [], _u32)
76
+
77
+ _sig(_lib.tle_parse, [_str, _out_ptr])
78
+ _sig(_lib.tle_free, [_ptr], None)
79
+ _sig(_lib.tle_get_satellite_number, [_ptr], _u32)
80
+ _sig(_lib.tle_get_epoch, [_ptr], _f64)
81
+ _sig(_lib.tle_get_inclination, [_ptr], _f64)
82
+ _sig(_lib.tle_get_eccentricity, [_ptr], _f64)
83
+ _sig(_lib.tle_get_mean_motion, [_ptr], _f64)
84
+
85
+ _sig(_lib.sgp4_init, [_ptr, _i32, _out_ptr])
86
+ _sig(_lib.sgp4_free, [_ptr], None)
87
+ _sig(_lib.sgp4_propagate, [_ptr, _f64, _out_vec3, _out_vec3])
88
+ _sig(
89
+ _lib.sgp4_propagate_batch, [_ptr, ctypes.POINTER(_f64), ctypes.POINTER(_f64), _u32]
90
+ )
91
+
92
+
93
+ class HohmannResult(ctypes.Structure):
94
+ _fields_ = [
95
+ ("semi_major_axis", _f64),
96
+ ("delta_v1", _f64),
97
+ ("delta_v2", _f64),
98
+ ("total_delta_v", _f64),
99
+ ("transfer_time", _f64),
100
+ ("transfer_time_days", _f64),
101
+ ]
102
+
103
+
104
+ _sig(_lib.orbital_hohmann, [_f64, _f64, _f64, ctypes.POINTER(HohmannResult)])
105
+ _sig(_lib.orbital_velocity, [_f64, _f64, _f64], _f64)
106
+ _sig(_lib.orbital_period, [_f64, _f64], _f64)
107
+ _sig(_lib.orbital_escape_velocity, [_f64, _f64], _f64)
108
+
109
+ _sig(_lib.coords_eci_to_ecef, [_out_vec3, _f64, _out_vec3], None)
110
+ _sig(_lib.coords_ecef_to_geodetic, [_out_vec3, _out_vec3], None)
111
+ _sig(_lib.coords_julian_to_gmst, [_f64], _f64)
astroz/coords.py ADDED
@@ -0,0 +1,30 @@
1
+ """Coordinate transformations."""
2
+
3
+ from ctypes import byref, c_double
4
+ from ._lib import _lib
5
+
6
+
7
+ def eci_to_ecef(eci: tuple, gmst: float) -> tuple:
8
+ """ECI to ECEF. Returns (x, y, z) in km."""
9
+ eci_arr = (c_double * 3)(*eci)
10
+ ecef_arr = (c_double * 3)()
11
+ _lib.coords_eci_to_ecef(byref(eci_arr), gmst, byref(ecef_arr))
12
+ return (ecef_arr[0], ecef_arr[1], ecef_arr[2])
13
+
14
+
15
+ def ecef_to_geodetic(ecef: tuple) -> tuple:
16
+ """ECEF to geodetic (WGS84). Returns (lat_deg, lon_deg, alt_km)."""
17
+ ecef_arr = (c_double * 3)(*ecef)
18
+ lla_arr = (c_double * 3)()
19
+ _lib.coords_ecef_to_geodetic(byref(ecef_arr), byref(lla_arr))
20
+ return (lla_arr[0], lla_arr[1], lla_arr[2])
21
+
22
+
23
+ def eci_to_geodetic(eci: tuple, gmst: float) -> tuple:
24
+ """ECI to geodetic (WGS84). Returns (lat_deg, lon_deg, alt_km)."""
25
+ return ecef_to_geodetic(eci_to_ecef(eci, gmst))
26
+
27
+
28
+ def julian_to_gmst(jd: float) -> float:
29
+ """Julian date to GMST (radians)."""
30
+ return _lib.coords_julian_to_gmst(jd)
astroz/exceptions.py ADDED
@@ -0,0 +1,40 @@
1
+ """Exception handling for astroz errors."""
2
+
3
+
4
+ class AstrozError(Exception):
5
+ """Base exception for astroz errors."""
6
+
7
+
8
+ class TleParseError(AstrozError):
9
+ """TLE parsing failed."""
10
+
11
+
12
+ class Sgp4Error(AstrozError):
13
+ """SGP4 propagation error."""
14
+
15
+
16
+ class OrbitalError(AstrozError):
17
+ """Orbital mechanics calculation error."""
18
+
19
+
20
+ _ERROR_MAP = {
21
+ -1: (TleParseError, "Bad TLE length"),
22
+ -2: (TleParseError, "Bad checksum"),
23
+ -10: (Sgp4Error, "Deep space not supported"),
24
+ -11: (Sgp4Error, "Invalid eccentricity"),
25
+ -12: (Sgp4Error, "Satellite decayed"),
26
+ -20: (OrbitalError, "Invalid orbital parameters"),
27
+ -100: (AstrozError, "Allocation failed"),
28
+ -101: (AstrozError, "Null pointer"),
29
+ -102: (AstrozError, "Not initialized"),
30
+ }
31
+
32
+
33
+ def check(code: int) -> None:
34
+ """Raise exception if error code is non-zero."""
35
+ if code == 0:
36
+ return
37
+ err = _ERROR_MAP.get(code)
38
+ if err:
39
+ raise err[0](err[1])
40
+ raise AstrozError(f"Unknown error: {code}")
Binary file
astroz/orbital.py ADDED
@@ -0,0 +1,38 @@
1
+ """Orbital mechanics functions."""
2
+
3
+ from ctypes import byref
4
+ from ._lib import _lib, HohmannResult
5
+ from .exceptions import check
6
+
7
+
8
+ def hohmann(mu: float, r1: float, r2: float) -> dict:
9
+ """Hohmann transfer between two circular orbits.
10
+
11
+ Returns dict with: semi_major_axis, delta_v1, delta_v2,
12
+ total_delta_v, transfer_time, transfer_time_days
13
+ """
14
+ result = HohmannResult()
15
+ check(_lib.orbital_hohmann(mu, r1, r2, byref(result)))
16
+ return {
17
+ "semi_major_axis": result.semi_major_axis,
18
+ "delta_v1": result.delta_v1,
19
+ "delta_v2": result.delta_v2,
20
+ "total_delta_v": result.total_delta_v,
21
+ "transfer_time": result.transfer_time,
22
+ "transfer_time_days": result.transfer_time_days,
23
+ }
24
+
25
+
26
+ def velocity(mu: float, radius: float, sma: float = 0) -> float:
27
+ """Orbital velocity (km/s). Use sma=0 for circular orbit."""
28
+ return _lib.orbital_velocity(mu, radius, sma)
29
+
30
+
31
+ def period(mu: float, sma: float) -> float:
32
+ """Orbital period (seconds)."""
33
+ return _lib.orbital_period(mu, sma)
34
+
35
+
36
+ def escape_velocity(mu: float, radius: float) -> float:
37
+ """Escape velocity (km/s)."""
38
+ return _lib.orbital_escape_velocity(mu, radius)
astroz/sgp4.py ADDED
@@ -0,0 +1,86 @@
1
+ """SGP4 orbit propagator wrapper."""
2
+
3
+ import ctypes
4
+ from ._lib import _lib
5
+ from .exceptions import check
6
+ from .tle import Tle
7
+
8
+
9
+ class Sgp4:
10
+ """SGP4 orbit propagator."""
11
+
12
+ WGS84 = 0
13
+ WGS72 = 1
14
+
15
+ def __init__(self, tle: Tle, gravity_model: int = WGS84):
16
+ """Initialize SGP4 propagator from TLE."""
17
+ self._handle = ctypes.c_void_p()
18
+ check(_lib.sgp4_init(tle._handle, gravity_model, ctypes.byref(self._handle)))
19
+
20
+ def __del__(self):
21
+ if hasattr(self, "_handle") and self._handle:
22
+ _lib.sgp4_free(self._handle)
23
+
24
+ def propagate(self, tsince: float) -> tuple:
25
+ """Propagate to time since TLE epoch.
26
+
27
+ Args:
28
+ tsince: Minutes since TLE epoch
29
+
30
+ Returns:
31
+ (position, velocity) tuples in km and km/s (TEME frame)
32
+ """
33
+ pos = (ctypes.c_double * 3)()
34
+ vel = (ctypes.c_double * 3)()
35
+ check(
36
+ _lib.sgp4_propagate(
37
+ self._handle, tsince, ctypes.byref(pos), ctypes.byref(vel)
38
+ )
39
+ )
40
+ return (pos[0], pos[1], pos[2]), (vel[0], vel[1], vel[2])
41
+
42
+ def propagate_batch(self, times: list) -> list:
43
+ """Batch propagation - much faster for multiple time steps.
44
+
45
+ Args:
46
+ times: List of minutes since TLE epoch
47
+
48
+ Returns:
49
+ List of (position, velocity) tuples
50
+ """
51
+ n = len(times)
52
+ times_arr = (ctypes.c_double * n)(*times)
53
+ results_arr = (ctypes.c_double * (n * 6))()
54
+ check(_lib.sgp4_propagate_batch(self._handle, times_arr, results_arr, n))
55
+ out = []
56
+ for i in range(n):
57
+ base = i * 6
58
+ pos = (results_arr[base], results_arr[base + 1], results_arr[base + 2])
59
+ vel = (results_arr[base + 3], results_arr[base + 4], results_arr[base + 5])
60
+ out.append((pos, vel))
61
+ return out
62
+
63
+ def propagate_batch_np(self, times):
64
+ """Batch propagation returning numpy arrays (fastest).
65
+
66
+ Args:
67
+ times: Array-like of minutes since TLE epoch
68
+
69
+ Returns:
70
+ (positions, velocities) as numpy arrays of shape (n, 3)
71
+ """
72
+ import numpy as np
73
+
74
+ times = np.asarray(times, dtype=np.float64)
75
+ n = len(times)
76
+ results = np.empty(n * 6, dtype=np.float64)
77
+ check(
78
+ _lib.sgp4_propagate_batch(
79
+ self._handle,
80
+ times.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
81
+ results.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
82
+ n,
83
+ )
84
+ )
85
+ results = results.reshape(n, 6)
86
+ return results[:, :3], results[:, 3:]
astroz/tle.py ADDED
@@ -0,0 +1,43 @@
1
+ """TLE (Two-Line Element) wrapper."""
2
+
3
+ import ctypes
4
+ from ._lib import _lib
5
+ from .exceptions import check
6
+
7
+
8
+ class Tle:
9
+ """Two-Line Element set for satellite orbit data."""
10
+
11
+ def __init__(self, tle_string: str):
12
+ """Parse a TLE string (both lines)."""
13
+ self._handle = ctypes.c_void_p()
14
+ check(_lib.tle_parse(tle_string.encode(), ctypes.byref(self._handle)))
15
+
16
+ def __del__(self):
17
+ if hasattr(self, "_handle") and self._handle:
18
+ _lib.tle_free(self._handle)
19
+
20
+ @property
21
+ def satellite_number(self) -> int:
22
+ """NORAD catalog number."""
23
+ return _lib.tle_get_satellite_number(self._handle)
24
+
25
+ @property
26
+ def epoch(self) -> float:
27
+ """Epoch in J2000 seconds."""
28
+ return _lib.tle_get_epoch(self._handle)
29
+
30
+ @property
31
+ def inclination(self) -> float:
32
+ """Orbital inclination in degrees."""
33
+ return _lib.tle_get_inclination(self._handle)
34
+
35
+ @property
36
+ def eccentricity(self) -> float:
37
+ """Orbital eccentricity."""
38
+ return _lib.tle_get_eccentricity(self._handle)
39
+
40
+ @property
41
+ def mean_motion(self) -> float:
42
+ """Mean motion in revolutions per day."""
43
+ return _lib.tle_get_mean_motion(self._handle)
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: astroz
3
+ Version: 0.4.1
4
+ Summary: Python bindings for astroz - high-performance astrodynamics library
5
+ License: MIT
6
+ Project-URL: Repository, https://github.com/ATTron/astroz
7
+ Keywords: astrodynamics,orbital-mechanics,sgp4,tle,satellite
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+
16
+ # astroz Python bindings
17
+
18
+ Python bindings for [astroz](https://github.com/ATTron/astroz), a high-performance astrodynamics library written in Zig.
19
+
20
+ ## Performance
21
+
22
+ Using the batch numpy API, astroz significantly outperforms other Python SGP4 libraries:
23
+
24
+ | Scenario | astroz | python-sgp4 | skyfield | vs sgp4 |
25
+ |----------|--------|-------------|----------|---------|
26
+ | 1 day (minute res) | 0.37 ms | 0.54 ms | 8 ms | **1.5x faster** |
27
+ | 1 week (minute res) | 2.6 ms | 3.7 ms | 50 ms | **1.4x faster** |
28
+ | 2 weeks (minute res) | 4.8 ms | 7.4 ms | 92 ms | **1.5x faster** |
29
+ | 2 weeks (second res) | 280 ms | 460 ms | - | **1.6x faster** |
30
+ | 1 month (minute res) | 9.8 ms | 17 ms | 187 ms | **1.7x faster** |
31
+
32
+ The raw Zig implementation achieves ~5 million propagations/second, outperforming
33
+ the Rust [sgp4 crate](https://github.com/neuromorphicsystems/sgp4) by 1.3-2.2x.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ # Build the shared library first
39
+ cd /path/to/astroz
40
+ zig build c-api
41
+
42
+ # Install Python package
43
+ cd python
44
+ pip install -e .
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```python
50
+ import astroz
51
+ from astroz import Tle, Sgp4, orbital, coords
52
+
53
+ # Parse TLE and propagate with SGP4
54
+ tle = Tle("""1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
55
+ 2 25544 51.6400 208.9163 0006703 40.6542 114.5435 15.49910020999999""")
56
+
57
+ sgp4 = Sgp4(tle)
58
+ pos, vel = sgp4.propagate(30.0) # 30 minutes after epoch
59
+ print(f"Position: {pos} km")
60
+ print(f"Velocity: {vel} km/s")
61
+
62
+ # Convert to ground coordinates
63
+ gmst = coords.julian_to_gmst(2460310.5)
64
+ lat, lon, alt = coords.eci_to_geodetic(pos, gmst)
65
+ print(f"Lat: {lat:.2f}, Lon: {lon:.2f}, Alt: {alt:.1f} km")
66
+
67
+ # Orbital mechanics
68
+ transfer = orbital.hohmann(398600.5, 6778, 42164) # LEO to GEO
69
+ print(f"Delta-V: {transfer['total_delta_v']:.2f} km/s")
70
+ ```
71
+
72
+ ## API
73
+
74
+ ### TLE Parsing
75
+ - `Tle(tle_string)` - Parse two-line element set
76
+ - Properties: `satellite_number`, `epoch`, `inclination`, `eccentricity`, `mean_motion`
77
+
78
+ ### SGP4 Propagation
79
+ - `Sgp4(tle, gravity_model=WGS84)` - Initialize propagator
80
+ - `propagate(tsince)` - Single propagation (returns tuple)
81
+ - `propagate_batch(times)` - Batch propagation (returns list)
82
+ - `propagate_batch_np(times)` - Batch with numpy (fastest, returns arrays)
83
+
84
+ ### Coordinates
85
+ - `coords.eci_to_ecef(eci, gmst)` - ECI to ECEF
86
+ - `coords.ecef_to_geodetic(ecef)` - ECEF to lat/lon/alt
87
+ - `coords.eci_to_geodetic(eci, gmst)` - ECI to lat/lon/alt
88
+ - `coords.julian_to_gmst(jd)` - Julian date to GMST
89
+
90
+ ### Orbital Mechanics
91
+ - `orbital.hohmann(mu, r1, r2)` - Hohmann transfer calculation
92
+ - `orbital.velocity(mu, radius, sma=0)` - Orbital velocity
93
+ - `orbital.period(mu, sma)` - Orbital period
94
+ - `orbital.escape_velocity(mu, radius)` - Escape velocity
@@ -0,0 +1,13 @@
1
+ astroz/__init__.py,sha256=bVQmXRjQQDIjBs8wW8iGwT9LTy7LPNUB8qNhfPnbY_I,527
2
+ astroz/__version__.py,sha256=pMtTmSUht-XtbR_7Doz6bsQqopJJd8rZ8I8zy2HwwoA,22
3
+ astroz/_lib.py,sha256=FGADZSXmW5fRIr53H3RDzSEz-rK5lR1j2BTAwALv5Rg,3065
4
+ astroz/coords.py,sha256=fYODTZ0ZqZ9tf8NjRo-2Dr7H2FOol0frMD0t45DgIlc,984
5
+ astroz/exceptions.py,sha256=7FdJk0FENI3DZ0UJxBeouwmQAWExcWHHPkZsncq05ts,1000
6
+ astroz/orbital.py,sha256=xSfXJPc_vwhkSKCk4GOQ-0R5yDKFDbwNNiR6-Cw_wFg,1191
7
+ astroz/sgp4.py,sha256=efsOBmtYLLS8Z1MdcjyXiMKcV5cwDC9tkQqIw-UYAfs,2658
8
+ astroz/tle.py,sha256=6OLdcLG_wSNJ3u8SL9JSoEvRxOBizKqwZ3MAV-s2b-I,1232
9
+ astroz/lib/libastroz_c.so,sha256=xV8K7x_Jt2l_0r-ejVGuZMtmEaURszrFwZ4Hr15oXU0,276368
10
+ astroz-0.4.1.dist-info/METADATA,sha256=XLsAGlFkB42qIs0F3rVL8oD0WEVp2ODBOMxVC7fQ328,3252
11
+ astroz-0.4.1.dist-info/WHEEL,sha256=w_6alU7T_sBwGm5Vyu6GD3AOYNoPHJEZwBbFBO_bp5Q,113
12
+ astroz-0.4.1.dist-info/top_level.txt,sha256=X8LWxpbPipvBmFB0vnafmfaz1WmkzU2MOyoeJX30Yig,12
13
+ astroz-0.4.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp312-cp312-manylinux_2_38_x86_64
5
+
@@ -0,0 +1,2 @@
1
+ astroz
2
+ dist