Geopack-2008 2.2.1__py3-none-win_arm64.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.
geopack/__init__.py ADDED
@@ -0,0 +1,322 @@
1
+ """Geopack-2008 for Python.
2
+
3
+ A Pythonic wrapper around the NativeAOT-built ``geopack`` shared library
4
+ (magnetic field of the Earth, solar-terrestrial coordinate transforms). No .NET
5
+ runtime is required at runtime.
6
+
7
+ Typical usage::
8
+
9
+ import geopack
10
+
11
+ ctx = geopack.recalc(1997, 12, 16, 21, 0, 0, vx=-304, vy=13.78, vz=4)
12
+ b = ctx.igrf_gsw(1, 1, 1) # (bx, by, bz) nT, GSW
13
+ r = ctx.gsw_to_geo(1, 1, 1) # (x, y, z) Earth radii, GEO
14
+ ctx.close()
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from datetime import datetime
20
+ from typing import NamedTuple, Optional, Sequence, Union
21
+
22
+ from . import _native
23
+ from ._enums import CoordinateSystem, MagnetopausePosition
24
+ from ._native import GeopackError
25
+
26
+ __all__ = [
27
+ "GeopackError",
28
+ "CoordinateSystem",
29
+ "MagnetopausePosition",
30
+ "Vector3",
31
+ "Sun",
32
+ "MagnetopauseResult",
33
+ "Context",
34
+ "DEFAULT_SOLAR_WIND",
35
+ "recalc",
36
+ "sun",
37
+ "shu_mgnp",
38
+ "t96_mgnp",
39
+ "t89",
40
+ "__version__",
41
+ ]
42
+
43
+ __version__ = "2.2.1"
44
+
45
+ #: Default solar wind velocity (GSE, km/s) used when the caller does not provide one.
46
+ DEFAULT_SOLAR_WIND = (-400.0, 0.0, 0.0)
47
+
48
+
49
+ class Vector3(NamedTuple):
50
+ """A Cartesian vector in Earth radii (positions) or nT (fields).
51
+
52
+ Attributes:
53
+ x: X component.
54
+ y: Y component.
55
+ z: Z component.
56
+ """
57
+
58
+ x: float
59
+ y: float
60
+ z: float
61
+
62
+
63
+ class Sun(NamedTuple):
64
+ """Position of the Sun for a given UTC date/time.
65
+
66
+ Attributes:
67
+ gst: Greenwich mean sidereal time in radians.
68
+ slong: Ecliptic longitude of the Sun in radians.
69
+ srasn: Right ascension of the Sun in radians.
70
+ sdec: Declination of the Sun in radians.
71
+ """
72
+
73
+ gst: float
74
+ slong: float
75
+ srasn: float
76
+ sdec: float
77
+
78
+
79
+ class MagnetopauseResult(NamedTuple):
80
+ """Result of a magnetopause model evaluation.
81
+
82
+ Attributes:
83
+ boundary: Nearest boundary point in GSW (Earth radii).
84
+ dist: Distance from the input point to the boundary (Earth radii).
85
+ position: Relative position of the point (Inside, Outside, NotDefined).
86
+ """
87
+
88
+ boundary: Vector3
89
+ dist: float
90
+ position: MagnetopausePosition
91
+
92
+
93
+ class Context:
94
+ """A frozen computation context for a single UTC date/time.
95
+
96
+ Created by :func:`recalc`. All field and transform methods take coordinates
97
+ in the documented system and return plain ``(x, y, z)`` tuples. The context
98
+ is immutable on the native side and safe to share between threads.
99
+ """
100
+
101
+ __slots__ = ("_handle", "_closed")
102
+
103
+ def __init__(self, handle: int):
104
+ self._handle = handle
105
+ self._closed = False
106
+
107
+ # -- lifecycle ----------------------------------------------------------
108
+
109
+ def close(self) -> None:
110
+ """Release the native computation context. Idempotent."""
111
+ if not self._closed:
112
+ _native.context_release(self._handle)
113
+ self._closed = True
114
+
115
+ def __del__(self) -> None: # pragma: no cover - interpreter shutdown
116
+ try:
117
+ self.close()
118
+ except Exception:
119
+ pass
120
+
121
+ def __enter__(self) -> "Context":
122
+ return self
123
+
124
+ def __exit__(self, *exc) -> None:
125
+ self.close()
126
+
127
+ def _check_open(self) -> None:
128
+ if self._closed:
129
+ raise RuntimeError("Geopack context has been closed")
130
+
131
+ # -- field models -------------------------------------------------------
132
+
133
+ def igrf_gsw(self, x: float, y: float, z: float) -> Vector3:
134
+ """IGRF magnetic field at ``(x, y, z)`` in GSW, returned in nT."""
135
+ self._check_open()
136
+ return Vector3(*_native.igrf_gsw(self._handle, x, y, z))
137
+
138
+ def dip(self, x: float, y: float, z: float) -> Vector3:
139
+ """Dipole magnetic field at ``(x, y, z)`` in GSW, returned in nT."""
140
+ self._check_open()
141
+ return Vector3(*_native.dip(self._handle, x, y, z))
142
+
143
+ def igrf_geo(self, r: float, theta: float, phi: float) -> Vector3:
144
+ """IGRF field in GEO spherical coordinates at radius ``r`` (Re),
145
+ colatitude ``theta`` and longitude ``phi`` (radians).
146
+
147
+ Returns the spherical components ``(Br, Btheta, Bphi)`` in nT.
148
+ """
149
+ self._check_open()
150
+ return Vector3(*_native.igrf_geo(self._handle, r, theta, phi))
151
+
152
+ # -- external field models ----------------------------------------------
153
+
154
+ @property
155
+ def psi(self) -> float:
156
+ """Dipole tilt angle (radians) for this context's date/time, as computed by Recalc."""
157
+ self._check_open()
158
+ return _native.context_psi(self._handle)
159
+
160
+ def t89(self, iopt: int, x: float, y: float, z: float, psi: Optional[float] = None) -> Vector3:
161
+ """Tsyganenko (1989) external field at ``(x, y, z)`` in GSW.
162
+
163
+ ``iopt`` is the disturbance-level index (1..7, maps to Kp intervals).
164
+ ``psi`` is the dipole tilt in radians and defaults to this context's tilt.
165
+ Returns the field vector in GSM (nT). Calls are serialized natively
166
+ because the model keeps static state (not thread-safe).
167
+ """
168
+ self._check_open()
169
+ if psi is None:
170
+ psi = self.psi
171
+ return Vector3(*_native.t89(iopt, psi, x, y, z))
172
+
173
+ # -- coordinate transforms (in -> out, Earth radii) ---------------------
174
+
175
+ def gsw_to_gse(self, x: float, y: float, z: float) -> Vector3:
176
+ """Transform Cartesian (x, y, z) from GSW to GSE."""
177
+ return self._transform(_native.gsw_to_gse, x, y, z)
178
+
179
+ def gse_to_gsw(self, x: float, y: float, z: float) -> Vector3:
180
+ """Transform Cartesian (x, y, z) from GSE to GSW."""
181
+ return self._transform(_native.gse_to_gsw, x, y, z)
182
+
183
+ def geo_to_mag(self, x: float, y: float, z: float) -> Vector3:
184
+ """Transform Cartesian (x, y, z) from GEO to MAG."""
185
+ return self._transform(_native.geo_to_mag, x, y, z)
186
+
187
+ def mag_to_geo(self, x: float, y: float, z: float) -> Vector3:
188
+ """Transform Cartesian (x, y, z) from MAG to GEO."""
189
+ return self._transform(_native.mag_to_geo, x, y, z)
190
+
191
+ def gei_to_geo(self, x: float, y: float, z: float) -> Vector3:
192
+ """Transform Cartesian (x, y, z) from GEI to GEO."""
193
+ return self._transform(_native.gei_to_geo, x, y, z)
194
+
195
+ def geo_to_gei(self, x: float, y: float, z: float) -> Vector3:
196
+ """Transform Cartesian (x, y, z) from GEO to GEI."""
197
+ return self._transform(_native.geo_to_gei, x, y, z)
198
+
199
+ def mag_to_sm(self, x: float, y: float, z: float) -> Vector3:
200
+ """Transform Cartesian (x, y, z) from MAG to SM."""
201
+ return self._transform(_native.mag_to_sm, x, y, z)
202
+
203
+ def sm_to_mag(self, x: float, y: float, z: float) -> Vector3:
204
+ """Transform Cartesian (x, y, z) from SM to MAG."""
205
+ return self._transform(_native.sm_to_mag, x, y, z)
206
+
207
+ def sm_to_gsw(self, x: float, y: float, z: float) -> Vector3:
208
+ """Transform Cartesian (x, y, z) from SM to GSW."""
209
+ return self._transform(_native.sm_to_gsw, x, y, z)
210
+
211
+ def gsw_to_sm(self, x: float, y: float, z: float) -> Vector3:
212
+ """Transform Cartesian (x, y, z) from GSW to SM."""
213
+ return self._transform(_native.gsw_to_sm, x, y, z)
214
+
215
+ def geo_to_gsw(self, x: float, y: float, z: float) -> Vector3:
216
+ """Transform Cartesian (x, y, z) from GEO to GSW."""
217
+ return self._transform(_native.geo_to_gsw, x, y, z)
218
+
219
+ def gsw_to_geo(self, x: float, y: float, z: float) -> Vector3:
220
+ """Transform Cartesian (x, y, z) from GSW to GEO."""
221
+ return self._transform(_native.gsw_to_geo, x, y, z)
222
+
223
+ def _transform(self, fn, x: float, y: float, z: float) -> Vector3:
224
+ self._check_open()
225
+ return Vector3(*fn(self._handle, x, y, z))
226
+
227
+
228
+ def recalc(
229
+ date_or_year: Union[datetime, int],
230
+ month: Optional[int] = None,
231
+ day: Optional[int] = None,
232
+ hour: int = 0,
233
+ minute: int = 0,
234
+ second: int = 0,
235
+ vx: float = DEFAULT_SOLAR_WIND[0],
236
+ vy: float = DEFAULT_SOLAR_WIND[1],
237
+ vz: float = DEFAULT_SOLAR_WIND[2],
238
+ ) -> Context:
239
+ """Create a :class:`Context` for a UTC date/time and solar wind velocity.
240
+
241
+ ``date_or_year`` is either a ``datetime.datetime`` (then ``month``/``day``
242
+ must be omitted) or a bare year. The solar wind velocity defaults to
243
+ ``(-400, 0, 0)`` km/s (GSE) as in the original GEOPACK-2008.
244
+ """
245
+ if isinstance(date_or_year, datetime):
246
+ if month is not None or day is not None:
247
+ raise ValueError("pass either a datetime or (year, month, day), not both")
248
+ dt = date_or_year
249
+ year, month, day = dt.year, dt.month, dt.day
250
+ hour, minute, second = dt.hour, dt.minute, dt.second
251
+ else:
252
+ if month is None or day is None:
253
+ raise ValueError("month and day are required when a year is given")
254
+ year = date_or_year
255
+
256
+ handle = _native.context_create(
257
+ year, month, day, hour, minute, second, vx, vy, vz
258
+ )
259
+ return Context(handle)
260
+
261
+
262
+ def sun(date_or_year: Union[datetime, int], month: Optional[int] = None, day: Optional[int] = None,
263
+ hour: int = 0, minute: int = 0, second: int = 0) -> Sun:
264
+ """Position of the Sun for a UTC date/time (see :func:`recalc`)."""
265
+ if isinstance(date_or_year, datetime):
266
+ if month is not None or day is not None:
267
+ raise ValueError("pass either a datetime or (year, month, day), not both")
268
+ dt = date_or_year
269
+ year, month, day = dt.year, dt.month, dt.day
270
+ hour, minute, second = dt.hour, dt.minute, dt.second
271
+ else:
272
+ if month is None or day is None:
273
+ raise ValueError("month and day are required when a year is given")
274
+ year = date_or_year
275
+
276
+ return Sun(*_native.sun(year, month, day, hour, minute, second))
277
+
278
+
279
+ def shu_mgnp(
280
+ xn_pd: float, vel: float, bz_imf: float, x: float, y: float, z: float,
281
+ ) -> MagnetopauseResult:
282
+ """Shue et al. (1998) magnetopause model. Input in GSW (Re).
283
+
284
+ ``xn_pd`` is either the solar wind proton number density (per cm3) if
285
+ ``vel > 0``, or the solar wind ram pressure in nPa if ``vel < 0``.
286
+ ``vel`` is the velocity in km/s (a negative value signals that ``xn_pd``
287
+ is a pressure, not a density). ``bz_imf`` is the IMF Bz in nT.
288
+ ``(x, y, z)`` is the probe point in GSW.
289
+ """
290
+ mx, my, mz, dist, position = _native.shu_mgnp(xn_pd, vel, bz_imf, x, y, z)
291
+ return MagnetopauseResult(Vector3(mx, my, mz), dist, MagnetopausePosition(position))
292
+
293
+
294
+ def t96_mgnp(xn_pd: float, vel: float, x: float, y: float, z: float) -> MagnetopauseResult:
295
+ """Tsyganenko 1996 magnetopause model. Input in GSW (Re).
296
+
297
+ ``xn_pd`` is either the solar wind proton number density (per cm3) if
298
+ ``vel > 0``, or the solar wind ram pressure in nPa if ``vel < 0``.
299
+ ``vel`` is the velocity in km/s (a negative value signals that ``xn_pd``
300
+ is a pressure, not a density). ``(x, y, z)`` is the probe point in GSW.
301
+ """
302
+ mx, my, mz, dist, position = _native.t96_mgnp(xn_pd, vel, x, y, z)
303
+ return MagnetopauseResult(Vector3(mx, my, mz), dist, MagnetopausePosition(position))
304
+
305
+
306
+ def t89(
307
+ iopt: int,
308
+ psi: float,
309
+ x: float,
310
+ y: float,
311
+ z: float,
312
+ parmod: Optional[Sequence[float]] = None,
313
+ ) -> Vector3:
314
+ """Tsyganenko (1989) external field model. Input in GSW (Re).
315
+
316
+ ``iopt`` is the disturbance-level index (1..7, maps to Kp intervals).
317
+ ``psi`` is the dipole tilt angle in radians.
318
+ ``parmod`` is a dummy parameter array accepted for parity with the .NET
319
+ ``IT89.Calculate`` contract; it is not used by T89 and defaults to ``None``.
320
+ ``(x, y, z)`` is the probe point in GSW. Returns the field vector in GSM (nT).
321
+ """
322
+ return Vector3(*_native.t89(iopt, psi, x, y, z))
geopack/_enums.py ADDED
@@ -0,0 +1,30 @@
1
+ """Enumerations shared with the native Geopack ABI.
2
+
3
+ The numeric values mirror the C# ``CoordinateSystem`` and
4
+ ``MagnetopausePosition`` enums so the integer codes crossing the C ABI stay
5
+ unambiguous.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import IntEnum
11
+
12
+
13
+ class CoordinateSystem(IntEnum):
14
+ """Geographic coordinate system codes (GEO = 0, GSW = 1, ...)."""
15
+
16
+ GEO = 0
17
+ GSW = 1
18
+ GSE = 2
19
+ MAG = 3
20
+ SM = 4
21
+ GEI = 5
22
+ GSM = 6
23
+
24
+
25
+ class MagnetopausePosition(IntEnum):
26
+ """Position of a point relative to the magnetopause boundary."""
27
+
28
+ NotDefined = 0
29
+ Inside = 1
30
+ Outside = 2
geopack/_native.py ADDED
@@ -0,0 +1,322 @@
1
+ """Thin ctypes layer over the NativeAOT-built geopack shared library.
2
+
3
+ The library exposes a flat C ABI (see src/Geopack.Native/GeopackNative.cs):
4
+
5
+ * every function returns an ``int`` error code (0 = success, non-zero = error);
6
+ * results are written into caller-provided out-pointers;
7
+ * a ``ComputationContext`` lives behind an opaque ``long`` handle created by
8
+ ``gp_context_create`` and released by ``gp_context_release``;
9
+ * the thread-local error message (UTF-8) is fetched with ``gp_last_error``.
10
+
11
+ This module only knows how to cross the FFI boundary. The object-oriented,
12
+ user-facing API lives in :mod:`geopack`.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import ctypes
18
+ import ctypes.util
19
+ import os
20
+ import sys
21
+ from typing import NamedTuple, Optional, Tuple
22
+
23
+ #: Filenames to try, in order, per platform. The native build produces
24
+ #: ``geopack.dylib`` on macOS, ``geopack.so`` on Linux and ``geopack.dll`` on
25
+ #: Windows (no ``lib`` prefix). A GEOPACK_LIBRARY env var overrides all of them.
26
+ _LIBRARY_NAMES = {
27
+ "darwin": ("geopack.dylib", "libgeopack.dylib"),
28
+ "linux": ("geopack.so", "libgeopack.so"),
29
+ "win32": ("geopack.dll",),
30
+ }
31
+ _FALLBACK_NAMES = ("geopack.dylib", "libgeopack.dylib", "geopack.so", "libgeopack.so", "geopack.dll")
32
+
33
+
34
+ class GeopackError(Exception):
35
+ """Raised when the native library reports a non-zero error code."""
36
+
37
+
38
+ def _find_library_path() -> str:
39
+ """Locate the native library, preferring the package directory."""
40
+ package_dir = os.path.dirname(os.path.abspath(__file__))
41
+ override = os.environ.get("GEOPACK_LIBRARY")
42
+ candidates = [override] if override else []
43
+ candidates += list(_LIBRARY_NAMES.get(sys.platform, ())) + list(_FALLBACK_NAMES)
44
+
45
+ for name in candidates:
46
+ if not name:
47
+ continue
48
+ path = os.path.join(package_dir, name)
49
+ if os.path.exists(path):
50
+ return path
51
+ if os.path.isabs(name) and os.path.exists(name):
52
+ return name
53
+
54
+ raise ImportError(
55
+ "Native geopack library not found. Build it with python/build_native.sh "
56
+ f"and place it next to this module. Tried: {', '.join(candidates)}"
57
+ )
58
+
59
+
60
+ _lib = ctypes.CDLL(_find_library_path())
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # Prototypes
64
+ # ---------------------------------------------------------------------------
65
+
66
+ _c_handle = ctypes.c_int64
67
+ _c_double = ctypes.c_double
68
+ _c_int = ctypes.c_int
69
+ _p_handle = ctypes.POINTER(ctypes.c_int64)
70
+ _p_double = ctypes.POINTER(ctypes.c_double)
71
+ _p_byte = ctypes.POINTER(ctypes.c_byte)
72
+
73
+ _lib.gp_context_create.argtypes = [
74
+ _c_int, _c_int, _c_int, _c_int, _c_int, _c_int, # y mo d h mi s
75
+ _c_double, _c_double, _c_double, # vx vy vz
76
+ _p_handle, # out handle
77
+ ]
78
+ _lib.gp_context_create.restype = _c_int
79
+
80
+ _lib.gp_context_release.argtypes = [_c_handle]
81
+ _lib.gp_context_release.restype = None
82
+
83
+ _lib.gp_last_error.argtypes = [_p_byte, _c_int]
84
+ _lib.gp_last_error.restype = _c_int
85
+
86
+ # Field models + transforms: (i64 ctx, double x, y, z, double* o1, o2, o3)
87
+ _TRANSFORM_ARGTYPES = [
88
+ _c_handle,
89
+ _c_double, _c_double, _c_double,
90
+ _p_double, _p_double, _p_double,
91
+ ]
92
+
93
+ _lib.gp_igrf_gsw.argtypes = _TRANSFORM_ARGTYPES
94
+ _lib.gp_igrf_gsw.restype = _c_int
95
+ _lib.gp_dip.argtypes = _TRANSFORM_ARGTYPES
96
+ _lib.gp_dip.restype = _c_int
97
+
98
+ _lib.gp_igrf_geo.argtypes = [
99
+ _c_handle,
100
+ _c_double, _c_double, _c_double, # r theta phi
101
+ _p_double, _p_double, _p_double, # out br bt bp
102
+ ]
103
+ _lib.gp_igrf_geo.restype = _c_int
104
+
105
+ _lib.gp_sun.argtypes = [
106
+ _c_int, _c_int, _c_int, _c_int, _c_int, _c_int, # y mo d h mi s
107
+ _p_double, _p_double, _p_double, _p_double, # out gst slong srasn sdec
108
+ ]
109
+ _lib.gp_sun.restype = _c_int
110
+
111
+ for _name in (
112
+ "gp_gsw_to_gse",
113
+ "gp_gse_to_gsw",
114
+ "gp_geo_to_mag",
115
+ "gp_mag_to_geo",
116
+ "gp_gei_to_geo",
117
+ "gp_geo_to_gei",
118
+ "gp_mag_to_sm",
119
+ "gp_sm_to_mag",
120
+ "gp_sm_to_gsw",
121
+ "gp_gsw_to_sm",
122
+ "gp_geo_to_gsw",
123
+ "gp_gsw_to_geo",
124
+ ):
125
+ getattr(_lib, _name).argtypes = _TRANSFORM_ARGTYPES
126
+ getattr(_lib, _name).restype = _c_int
127
+
128
+ _lib.gp_shu_mgnp.argtypes = [
129
+ _c_double, _c_double, _c_double, # xnPd vel bzImf
130
+ _c_double, _c_double, _c_double, # x y z
131
+ _p_double, _p_double, _p_double, # out mx my mz
132
+ _p_double, # out dist
133
+ _p_int := ctypes.POINTER(ctypes.c_int),
134
+ ]
135
+ _lib.gp_shu_mgnp.restype = _c_int
136
+
137
+ _lib.gp_t96_mgnp.argtypes = [
138
+ _c_double, _c_double, # xnPd vel
139
+ _c_double, _c_double, _c_double, # x y z
140
+ _p_double, _p_double, _p_double, # out mx my mz
141
+ _p_double, # out dist
142
+ ctypes.POINTER(ctypes.c_int),
143
+ ]
144
+ _lib.gp_t96_mgnp.restype = _c_int
145
+
146
+ # External field models (context-free, input GSW, output GSM)
147
+ _lib.gp_t89.argtypes = [
148
+ _c_int, _c_double, _c_double, _c_double, _c_double, # iopt psi x y z
149
+ _p_double, _p_double, _p_double, # out bx by bz
150
+ ]
151
+ _lib.gp_t89.restype = _c_int
152
+
153
+ # Context dipole tilt (radians), computed by Recalc
154
+ _lib.gp_context_psi.argtypes = [_c_handle, _p_double]
155
+ _lib.gp_context_psi.restype = _c_int
156
+
157
+ del _name # loop variable cleanup
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # Error plumbing
161
+ # ---------------------------------------------------------------------------
162
+
163
+
164
+ def _last_error() -> Optional[str]:
165
+ """Read and clear the thread-local error message set by the native side."""
166
+ # Probe: null buffer + zero capacity returns the UTF-8 byte length without
167
+ # clearing the stored message.
168
+ size = _lib.gp_last_error(None, 0)
169
+ if size <= 0:
170
+ return None
171
+ buf = ctypes.create_string_buffer(size + 1)
172
+ _lib.gp_last_error(ctypes.cast(buf, _p_byte), size + 1)
173
+ return buf.value.decode("utf-8", errors="replace")
174
+
175
+
176
+ def _check(rc: int) -> None:
177
+ if rc != 0:
178
+ raise GeopackError(_last_error() or f"native geopack error (code {rc})")
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # Raw wrappers
183
+ # ---------------------------------------------------------------------------
184
+
185
+
186
+ def context_create(
187
+ year: int, month: int, day: int, hour: int, minute: int, second: int,
188
+ vx: float, vy: float, vz: float,
189
+ ) -> int:
190
+ handle = ctypes.c_int64()
191
+ rc = _lib.gp_context_create(
192
+ year, month, day, hour, minute, second, vx, vy, vz, ctypes.byref(handle)
193
+ )
194
+ _check(rc)
195
+ return handle.value
196
+
197
+
198
+ def context_release(handle: int) -> None:
199
+ _lib.gp_context_release(handle)
200
+
201
+
202
+ def igrf_gsw(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
203
+ return _call_vector(_lib.gp_igrf_gsw, handle, x, y, z)
204
+
205
+
206
+ def dip(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
207
+ return _call_vector(_lib.gp_dip, handle, x, y, z)
208
+
209
+
210
+ def igrf_geo(handle: int, r: float, theta: float, phi: float) -> Tuple[float, float, float]:
211
+ bx, bt, bp = ctypes.c_double(), ctypes.c_double(), ctypes.c_double()
212
+ rc = _lib.gp_igrf_geo(
213
+ handle, r, theta, phi, ctypes.byref(bx), ctypes.byref(bt), ctypes.byref(bp)
214
+ )
215
+ _check(rc)
216
+ return bx.value, bt.value, bp.value
217
+
218
+
219
+ def sun(
220
+ year: int, month: int, day: int, hour: int, minute: int, second: int,
221
+ ) -> Tuple[float, float, float, float]:
222
+ gst, slong, srasn, sdec = (ctypes.c_double() for _ in range(4))
223
+ rc = _lib.gp_sun(
224
+ year, month, day, hour, minute, second,
225
+ ctypes.byref(gst), ctypes.byref(slong), ctypes.byref(srasn), ctypes.byref(sdec),
226
+ )
227
+ _check(rc)
228
+ return gst.value, slong.value, srasn.value, sdec.value
229
+
230
+
231
+ def _call_vector(fn, handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
232
+ ox, oy, oz = ctypes.c_double(), ctypes.c_double(), ctypes.c_double()
233
+ rc = fn(handle, x, y, z, ctypes.byref(ox), ctypes.byref(oy), ctypes.byref(oz))
234
+ _check(rc)
235
+ return ox.value, oy.value, oz.value
236
+
237
+
238
+ # Coordinate transforms: (long ctx, double x, y, z) -> (ox, oy, oz)
239
+ def gsw_to_gse(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
240
+ return _call_vector(_lib.gp_gsw_to_gse, handle, x, y, z)
241
+
242
+
243
+ def gse_to_gsw(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
244
+ return _call_vector(_lib.gp_gse_to_gsw, handle, x, y, z)
245
+
246
+
247
+ def geo_to_mag(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
248
+ return _call_vector(_lib.gp_geo_to_mag, handle, x, y, z)
249
+
250
+
251
+ def mag_to_geo(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
252
+ return _call_vector(_lib.gp_mag_to_geo, handle, x, y, z)
253
+
254
+
255
+ def gei_to_geo(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
256
+ return _call_vector(_lib.gp_gei_to_geo, handle, x, y, z)
257
+
258
+
259
+ def geo_to_gei(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
260
+ return _call_vector(_lib.gp_geo_to_gei, handle, x, y, z)
261
+
262
+
263
+ def mag_to_sm(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
264
+ return _call_vector(_lib.gp_mag_to_sm, handle, x, y, z)
265
+
266
+
267
+ def sm_to_mag(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
268
+ return _call_vector(_lib.gp_sm_to_mag, handle, x, y, z)
269
+
270
+
271
+ def sm_to_gsw(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
272
+ return _call_vector(_lib.gp_sm_to_gsw, handle, x, y, z)
273
+
274
+
275
+ def gsw_to_sm(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
276
+ return _call_vector(_lib.gp_gsw_to_sm, handle, x, y, z)
277
+
278
+
279
+ def geo_to_gsw(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
280
+ return _call_vector(_lib.gp_geo_to_gsw, handle, x, y, z)
281
+
282
+
283
+ def gsw_to_geo(handle: int, x: float, y: float, z: float) -> Tuple[float, float, float]:
284
+ return _call_vector(_lib.gp_gsw_to_geo, handle, x, y, z)
285
+
286
+
287
+ def shu_mgnp(
288
+ xn_pd: float, vel: float, bz_imf: float, x: float, y: float, z: float,
289
+ ) -> Tuple[float, float, float, float, int]:
290
+ return _call_mgnp(_lib.gp_shu_mgnp, xn_pd, vel, bz_imf, x, y, z)
291
+
292
+
293
+ def t96_mgnp(xn_pd: float, vel: float, x: float, y: float, z: float) -> Tuple[float, float, float, float, int]:
294
+ return _call_mgnp(_lib.gp_t96_mgnp, xn_pd, vel, x, y, z)
295
+
296
+
297
+ def _call_mgnp(fn, *args: float) -> Tuple[float, float, float, float, int]:
298
+ mx, my, mz, dist = (ctypes.c_double() for _ in range(4))
299
+ position = ctypes.c_int()
300
+ rc = fn(
301
+ *args,
302
+ ctypes.byref(mx), ctypes.byref(my), ctypes.byref(mz),
303
+ ctypes.byref(dist), ctypes.byref(position),
304
+ )
305
+ _check(rc)
306
+ return mx.value, my.value, mz.value, dist.value, position.value
307
+
308
+
309
+ def t89(iopt: int, psi: float, x: float, y: float, z: float) -> Tuple[float, float, float]:
310
+ """Tsyganenko (1989) external field at (x, y, z) in GSW, GSM components in nT."""
311
+ bx, by, bz = ctypes.c_double(), ctypes.c_double(), ctypes.c_double()
312
+ rc = _lib.gp_t89(iopt, psi, x, y, z, ctypes.byref(bx), ctypes.byref(by), ctypes.byref(bz))
313
+ _check(rc)
314
+ return bx.value, by.value, bz.value
315
+
316
+
317
+ def context_psi(handle: int) -> float:
318
+ """Dipole tilt (radians) for the given context, as computed by Recalc."""
319
+ psi = ctypes.c_double()
320
+ rc = _lib.gp_context_psi(handle, ctypes.byref(psi))
321
+ _check(rc)
322
+ return psi.value
geopack/geopack.dll ADDED
Binary file
geopack/py.typed ADDED
File without changes
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: Geopack-2008
3
+ Version: 2.2.1
4
+ Summary: Geopack-2008 magnetospheric field model and solar-terrestrial coordinate transforms (NativeAOT bindings)
5
+ Author: Aurora Science Hub
6
+ License: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/Aurora-Science-Hub/Geopack
8
+ Project-URL: Repository, https://github.com/Aurora-Science-Hub/Geopack
9
+ Project-URL: Issues, https://github.com/Aurora-Science-Hub/Geopack/issues
10
+ Project-URL: Changelog, https://github.com/Aurora-Science-Hub/Geopack/blob/main/CHANGELOG.md
11
+ Keywords: geopack,space physics,geophysics,magnetosphere,magnetic field,igrf,tsyganenko,t89,magnetopause,solar wind,coordinate-transforms
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
25
+ Classifier: Topic :: Scientific/Engineering :: Physics
26
+ Requires-Python: >=3.8
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Geopack-2008 — GEOPACK-2008 geomagnetic field model for Python
30
+
31
+ [![Python](https://img.shields.io/badge/Python-3.8+-3776AB?logo=python&label=Python)](https://www.python.org/)
32
+ [![Python Package](https://github.com/Aurora-Science-Hub/Geopack/actions/workflows/python-package.yml/badge.svg)](https://github.com/Aurora-Science-Hub/Geopack/actions/workflows/python-package.yml)
33
+ [![License: GPL v3+](https://img.shields.io/badge/License-GPLv3+-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
34
+ [![GitHub Stars](https://img.shields.io/github/stars/Aurora-Science-Hub/Geopack?style=social)](https://github.com/Aurora-Science-Hub/Geopack)
35
+
36
+ **Geopack-2008** (imported as `geopack`) is a self-contained, zero-dependency Python package for the
37
+ **GEOPACK-2008** geomagnetic field model: the **International Geomagnetic
38
+ Reference Field (IGRF)**, the **dipole field**, the **Tsyganenko (1989)
39
+ external field model (T89)**, the **Shue et al. (1998) and Tsyganenko (1996)
40
+ magnetopause models**, the **Sun position** computation, and the standard
41
+ **solar-terrestrial coordinate transforms** (GEO, GSW, GSE, SM, MAG, GEI, GSM).
42
+
43
+ It is a Python binding over a [C# port of GEOPACK-2008](https://github.com/Aurora-Science-Hub/Geopack)
44
+ compiled ahead-of-time with NativeAOT into a shared library (`geopack.dylib` /
45
+ `geopack.so` / `geopack.dll`), loaded through `ctypes` from the Python standard
46
+ library. No .NET runtime is required at runtime, and there are no Python
47
+ dependencies (no NumPy/SciPy).
48
+
49
+ - **Double precision**: accuracy matches the original Fortran code to 12–13 decimal digits
50
+ - **Standard library only** — no third-party runtime dependencies
51
+ - **Thread-safe** immutable computation contexts
52
+ - **Platform wheels** for Linux (x64/arm64), Windows (x64/arm64) and macOS (arm64)
53
+
54
+ ## Install
55
+
56
+ Build the wheel for your platform (requires .NET SDK 10 + a C toolchain) and
57
+ `pip install` it. On macOS/Linux:
58
+
59
+ ```bash
60
+ ./python/build_wheel.sh
61
+ pip install python/dist/geopack_2008-*.whl
62
+ ```
63
+
64
+ On Windows (cmd):
65
+
66
+ ```bat
67
+ .\python\build_wheel.bat
68
+ pip install python\dist\geopack_2008-*.whl
69
+ ```
70
+
71
+ The scripts build the native library for the host platform only; for another
72
+ OS/arch, build the wheel on that machine (NativeAOT does not cross-compile).
73
+
74
+ ## Usage
75
+
76
+ ```python
77
+ import geopack
78
+
79
+ ctx = geopack.recalc(1997, 12, 16, 21, 0, 0, vx=-304, vy=13.78, vz=4)
80
+ b = ctx.igrf_gsw(1, 1, 1) # (bx, by, bz) nT in GSW
81
+ r = ctx.gsw_to_geo(1, 1, 1) # (x, y, z) Earth radii in GEO
82
+ ctx.close()
83
+
84
+ # A datetime works too, and contexts are context managers:
85
+ with geopack.recalc(datetime(1997, 12, 16, 21, 0), vx=-304, vy=13.78, vz=4) as ctx:
86
+ b = ctx.igrf_gsw(1, 1, 1)
87
+
88
+ s = geopack.sun(1997, 12, 16, 21, 0, 0) # (gst, slong, srasn, sdec), radians
89
+
90
+ mp = geopack.shu_mgnp(xn_pd=2.0, vel=400.0, bz_imf=0.0, x=10, y=0, z=0)
91
+ print(mp.boundary, mp.dist, mp.position) # Vector3, float, MagnetopausePosition
92
+
93
+ # Tsyganenko (1989) external field — input GSW, output GSM (nT):
94
+ b = geopack.t89(iopt=3, psi=ctx.psi, x=-6.6, y=0, z=0)
95
+ b = ctx.t89(iopt=3, x=-6.6, y=0, z=0) # psi defaults to ctx.psi
96
+ ```
97
+
98
+ A runnable example with all of the above (sun position, IGRF/dipole fields,
99
+ coordinate transforms, magnetopause models) is in
100
+ [`python/example.py`](example.py):
101
+
102
+ ```bash
103
+ python3 python/example.py
104
+ ```
105
+
106
+ ## Implemented models and coordinate systems
107
+
108
+ | Category | Model | Python API |
109
+ | --- | --- | --- |
110
+ | Internal field | IGRF (geomagnetic main field) | `Context.igrf_gsw`, `Context.igrf_geo` |
111
+ | Internal field | Dipole | `Context.dip` |
112
+ | External field | Tsyganenko (1989) — T89 | `Context.t89`, `geopack.t89` |
113
+ | Magnetopause | Shue et al. (1998) | `geopack.shu_mgnp` |
114
+ | Magnetopause | Tsyganenko (1996) | `geopack.t96_mgnp` |
115
+ | Sun position | Greenwich sidereal time, solar ephemeris | `geopack.sun` |
116
+
117
+ Supported coordinate systems (see `CoordinateSystem`): **GEO** (geographic),
118
+ **GSW** (geocentric solar wind), **GSE** (geocentric solar ecliptic),
119
+ **SM** (solar magnetic), **MAG** (geomagnetic), **GEI** (geocentric solar
120
+ equatorial inertial) and **GSM** (geocentric solar magnetospheric).
121
+
122
+ ## API
123
+
124
+ All coordinates are in Earth radii; magnetic fields are in nT; angles are in
125
+ radians; solar wind velocity is in km/s.
126
+
127
+ * `recalc(date_or_year, month=None, day=None, hour=0, minute=0, second=0, vx=-400, vy=0, vz=0) -> Context`
128
+ * `Context` methods:
129
+ * fields: `igrf_gsw(x,y,z)`, `dip(x,y,z)`, `igrf_geo(r,theta,phi)`,
130
+ `t89(iopt,x,y,z, psi=None)`, property `psi` (dipole tilt, radians)
131
+ * transforms: `gsw_to_gse`, `gse_to_gsw`, `geo_to_mag`, `mag_to_geo`,
132
+ `gei_to_geo`, `geo_to_gei`, `mag_to_sm`, `sm_to_mag`, `sm_to_gsw`,
133
+ `gsw_to_sm`, `geo_to_gsw`, `gsw_to_geo` — each `(x,y,z) -> (x,y,z)`
134
+ * lifecycle: `close()`, context manager
135
+ * `sun(...) -> Sun(gst, slong, srasn, sdec)` — no context needed
136
+ * `shu_mgnp(xn_pd, vel, bz_imf, x, y, z)` and `t96_mgnp(xn_pd, vel, x, y, z)`
137
+ return `MagnetopauseResult(boundary, dist, position)` — no context needed
138
+ * `t89(iopt, psi, x, y, z, parmod=None) -> Vector3` — Tsyganenko (1989) external
139
+ field, input GSW, output GSM (nT); `parmod` is a dummy accepted for parity with
140
+ the .NET `IT89.Calculate` contract (unused) — no context needed
141
+
142
+ `GEOPACK_LIBRARY` environment variable points the loader at a specific library
143
+ path if the packaged one is not present.
144
+
145
+ ## How to Cite
146
+
147
+ If you use this software in your research, please cite it — see the
148
+ [`How to Cite`](https://github.com/Aurora-Science-Hub/Geopack#how-to-cite)
149
+ section of the main repository README (APA and BibTeX entries with a Zenodo DOI).
150
+
151
+ ## References
152
+
153
+ The implementation is based on the GEOPACK-2008 library and the following
154
+ scientific works:
155
+
156
+ 1. **Tsyganenko, N. A.** (1989). *A magnetospheric magnetic field model with a warped tail current sheet*. Planetary and Space Science, 37(1), 5–20. https://doi.org/10.1016/0032-0633(89)90066-4
157
+
158
+ 2. **Tsyganenko, N. A.** (1996). *Effects of the solar wind conditions on the global magnetospheric configuration as deduced from data-based field models*. ESA SP-389, 181–185.
159
+
160
+ 3. **Shue, J.-H., et al.** (1998). *Magnetopause location under extreme solar wind conditions*. Journal of Geophysical Research, 103(A8), 17691–17700. https://doi.org/10.1029/98JA01103
161
+
162
+ 4. **Hapgood, M. A.** (1992). *Space physics coordinate transformations: A user guide*. Planetary and Space Science, 40(5), 711–717. https://doi.org/10.1016/0032-0633(92)90012-D
163
+
164
+ 5. **Tsyganenko, N. A.** (2002). *A model of the near magnetosphere with a dawn-dusk asymmetry 1. Mathematical structure*. Journal of Geophysical Research, 107(A8). https://doi.org/10.1029/2001JA000219
165
+
166
+ 6. **Tsyganenko, N. A., & Sitnov, M. I.** (2005). *Modeling the dynamics of the inner magnetosphere during strong geomagnetic storms*. Journal of Geophysical Research: Space Physics, 110(A3), A03208. https://doi.org/10.1029/2004JA010798
@@ -0,0 +1,9 @@
1
+ geopack/__init__.py,sha256=1hLWM4nJnaWuQKcM7045kjHCOpbziYHlfmNyZ5vePiM,11921
2
+ geopack/_enums.py,sha256=o2bZ15IU2J_HMg5hipvFo7HESw8UoHCCBnBdabGE4UE,644
3
+ geopack/_native.py,sha256=IoKTS0-oPK-eaAGIL_mBgUQCM197NKbmRv9ftjtxM2Y,11537
4
+ geopack/geopack.dll,sha256=fBtBC3fma0adAqR4jxedft2opvzugGlbanBT6JZ7O1c,1486336
5
+ geopack/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ geopack_2008-2.2.1.dist-info/METADATA,sha256=oU2_f9zj05h_bYo2sAA1gItTtTsFRjgvg8JoBztCrbA,8472
7
+ geopack_2008-2.2.1.dist-info/WHEEL,sha256=QE-niYB-ZRQVWczDB301OM3v6K9Rt_4BEsv6l0aMysk,98
8
+ geopack_2008-2.2.1.dist-info/top_level.txt,sha256=5wOBhK4wkKyuE1EMPgWf9CTiOAWArRHC3Mk3e0nEbfk,8
9
+ geopack_2008-2.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_arm64
5
+
@@ -0,0 +1 @@
1
+ geopack