geopack-vectorize 2.0.1__tar.gz → 2.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/CHANGELOG.md +13 -0
  2. {geopack_vectorize-2.0.1/geopack_vectorize.egg-info → geopack_vectorize-2.0.2}/PKG-INFO +7 -1
  3. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/README.md +6 -0
  4. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/notebooks/directional_derivatives_maps/t96_field_directional_derivatives.ipynb +6 -6
  5. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/__init__.py +1 -1
  6. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/models/t01.py +1 -1
  7. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/vectorized/coordinates.py +15 -8
  8. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/vectorized/coordinates_complex.py +7 -4
  9. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/vectorized/igrf.py +3 -3
  10. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/vectorized/models/t01.py +8 -8
  11. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/vectorized/models/t96.py +4 -4
  12. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2/geopack_vectorize.egg-info}/PKG-INFO +7 -1
  13. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack_vectorize.egg-info/SOURCES.txt +2 -0
  14. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/pyproject.toml +1 -1
  15. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/setup.py +1 -1
  16. geopack_vectorize-2.0.2/tests/test_coordinate_broadcasting.py +79 -0
  17. geopack_vectorize-2.0.2/tests/test_model_input_regressions.py +67 -0
  18. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/LICENSE +0 -0
  19. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/MANIFEST.in +0 -0
  20. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/notebooks/01_coordinate_transformations_guide.ipynb +0 -0
  21. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/notebooks/02_magnetic_field_models_guide.ipynb +0 -0
  22. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/notebooks/03_performance_comparison.ipynb +0 -0
  23. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/notebooks/04_accuracy_validation.ipynb +0 -0
  24. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/notebooks/05_field_line_tracing_guide.ipynb +0 -0
  25. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/notebooks/06_field_line_tracing_validation.ipynb +0 -0
  26. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/notebooks/07_fieldline_geometry_and_derivatives.ipynb +0 -0
  27. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/notebooks/directional_derivatives_maps/dipole_field_directional_derivatives.ipynb +0 -0
  28. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/python_code_samples/_helpers.py +0 -0
  29. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/python_code_samples/basic_usage.py +0 -0
  30. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/python_code_samples/field_line_9_derivatives_example.py +0 -0
  31. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/python_code_samples/field_line_geometry_example.py +0 -0
  32. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/python_code_samples/magnetic_field_simple_sample.py +0 -0
  33. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/examples/readme_examples.py +0 -0
  34. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/Mageometry/__init__.py +0 -0
  35. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/Mageometry/field_line_directional_derivatives.py +0 -0
  36. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/Mageometry/field_line_geometry.py +0 -0
  37. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/core.py +0 -0
  38. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/geopack.py +0 -0
  39. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/igrf_coeffs/igrf10coeffs.txt +0 -0
  40. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/igrf_coeffs/igrf11coeffs.txt +0 -0
  41. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/igrf_coeffs/igrf12coeffs.txt +0 -0
  42. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/igrf_coeffs/igrf13coeffs.txt +0 -0
  43. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/igrf_coeffs/igrf14coeffs.txt +0 -0
  44. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/igrf_coeffs/igrf9coeffs.txt +0 -0
  45. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/models/__init__.py +0 -0
  46. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/models/t04.py +0 -0
  47. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/models/t89.py +0 -0
  48. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/models/t96.py +0 -0
  49. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/print_structure.py +0 -0
  50. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/t01.py +0 -0
  51. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/t04.py +0 -0
  52. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/t89.py +0 -0
  53. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/t96.py +0 -0
  54. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/vectorized/__init__.py +0 -0
  55. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/vectorized/condip1_exact.py +0 -0
  56. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/vectorized/models/__init__.py +0 -0
  57. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/vectorized/models/t04.py +0 -0
  58. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/vectorized/models/t89.py +0 -0
  59. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack/vectorized/trace.py +0 -0
  60. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack_vectorize.egg-info/dependency_links.txt +0 -0
  61. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack_vectorize.egg-info/requires.txt +0 -0
  62. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/geopack_vectorize.egg-info/top_level.txt +0 -0
  63. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/requirements.txt +0 -0
  64. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/setup.cfg +0 -0
  65. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/tests/installed_smoke.py +0 -0
  66. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/tests/test_distribution.py +0 -0
  67. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/tests/test_trace_vectorized.py +0 -0
  68. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/tests/test_trace_vectorized_with_vectorized_models.py +0 -0
  69. {geopack_vectorize-2.0.1 → geopack_vectorize-2.0.2}/tests/test_vectorized_models.py +0 -0
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [2.0.2] - 2026-11-04
11
+
12
+ ### Fixed
13
+ - Preserve the T01 IMF clock angle for negative By instead of replacing it
14
+ with 2*pi, correcting both scalar and vectorized magnetic fields.
15
+ - Accept integer coordinates without truncation in vectorized T01/T96,
16
+ and preserve NumPy broadcasting and multidimensional inputs in T01.
17
+ - Broadcast all coordinate/vector components before transforms and determine
18
+ scalar outputs from every input, including spherical transforms and the
19
+ IGRF GSM/GSW wrappers.
20
+
8
21
  ## [2.0.1] - 2026-10-28
9
22
 
10
23
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geopack-vectorize
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Python implementation of geopack and Tsyganenko models with optimized vectorization
5
5
  Home-page: https://github.com/Butadiene/geopack-vectorize
6
6
  Author: geopack-vectorize contributors
@@ -64,6 +64,12 @@ geopack-vectorize extends the original geopack by adding vectorized implementati
64
64
  - **Full Backward Compatibility**: All original geopack functions remain unchanged and available
65
65
  - **Comprehensive Validation**: Extensive test suite ensuring < 10⁻¹¹ relative error vs original implementations
66
66
 
67
+ ## Publication and Citation
68
+
69
+ geopack-vectorize, including its Mageometry component for local-frame analysis of magnetic field geometry, is described in the following paper published in *Earth, Planets and Space*. If you use this work in your research, please cite:
70
+
71
+ > Isono, K., Yoshikawa, A. & Katoh, Y. (2026). A local-frame approach to three-dimensional analysis of magnetospheric magnetic fields. *Earth, Planets and Space*, **78**, Article 201. [doi:10.1186/s40623-026-02528-2](https://doi.org/10.1186/s40623-026-02528-2).
72
+
67
73
  ## Upgrading to v2.0.0
68
74
 
69
75
  As of v2.0.0, the PyPI package name has changed from `geopack-vectorized` to `geopack-vectorize`. The old `geopack-vectorized` package will remain available for a transitional period but will eventually be removed. Please update your installation:
@@ -16,6 +16,12 @@ geopack-vectorize extends the original geopack by adding vectorized implementati
16
16
  - **Full Backward Compatibility**: All original geopack functions remain unchanged and available
17
17
  - **Comprehensive Validation**: Extensive test suite ensuring < 10⁻¹¹ relative error vs original implementations
18
18
 
19
+ ## Publication and Citation
20
+
21
+ geopack-vectorize, including its Mageometry component for local-frame analysis of magnetic field geometry, is described in the following paper published in *Earth, Planets and Space*. If you use this work in your research, please cite:
22
+
23
+ > Isono, K., Yoshikawa, A. & Katoh, Y. (2026). A local-frame approach to three-dimensional analysis of magnetospheric magnetic fields. *Earth, Planets and Space*, **78**, Article 201. [doi:10.1186/s40623-026-02528-2](https://doi.org/10.1186/s40623-026-02528-2).
24
+
19
25
  ## Upgrading to v2.0.0
20
26
 
21
27
  As of v2.0.0, the PyPI package name has changed from `geopack-vectorized` to `geopack-vectorize`. The old `geopack-vectorized` package will remain available for a transitional period but will eventually be removed. Please update your installation:
@@ -339,7 +339,7 @@
339
339
  " unit:\n",
340
340
  " - None -> uses UNIT_STR (for Gamma, etc.)\n",
341
341
  " - \"\" -> no unit display (for ratio, etc.)\n",
342
- " - \"A\" -> [A] (for FAC, etc.)\n",
342
+ " - \"A/m$^2$\" -> [A/m$^2$] (for FAC current density, etc.)\n",
343
343
  " \"\"\"\n",
344
344
  " if unit is None:\n",
345
345
  " unit = UNIT_STR\n",
@@ -850,7 +850,7 @@
850
850
  "\n",
851
851
  "def compute_fac_proxy_and_ratio(Bmag_nT, dn_db_T, db_dn_T):\n",
852
852
  " mu0 = 1.25663706127e-6\n",
853
- " FAC = Bmag_nT * (dn_db_T - db_dn_T) / mu0 * (1e-9) / (6371 * 1e6) # A\n",
853
+ " FAC = Bmag_nT * (dn_db_T - db_dn_T) / mu0 * (1e-9) / (6371 * 1e3) # A/m^2 (1 Re = 6371 km = 6.371e6 m)\n",
854
854
  " den = np.abs(dn_db_T) + np.abs(db_dn_T)\n",
855
855
  " ratio = np.abs(dn_db_T) / np.where(den == 0, np.nan, den)\n",
856
856
  " return FAC, ratio\n",
@@ -918,7 +918,7 @@
918
918
  " r\"Field-Aligned Current $\\frac{B}{\\mu_0}$((∂n/∂b)·T − (∂b/∂n)·T)\"\n",
919
919
  " ),\n",
920
920
  " cbar_label=\"FAC\",\n",
921
- " unit=\"A\",\n",
921
+ " unit=\"A/m$^2$\",\n",
922
922
  " force_levels=None,\n",
923
923
  " ),\n",
924
924
  " dict(\n",
@@ -1166,7 +1166,7 @@
1166
1166
  " r\"Field-Aligned Current $\\frac{B}{\\mu_0}$((∂n/∂b)·T − (∂b/∂n)·T)\"\n",
1167
1167
  " ),\n",
1168
1168
  " cbar_label=\"FAC\",\n",
1169
- " unit=\"A\",\n",
1169
+ " unit=\"A/m$^2$\",\n",
1170
1170
  " force_levels=None,\n",
1171
1171
  " ),\n",
1172
1172
  " dict(\n",
@@ -1464,7 +1464,7 @@
1464
1464
  " r\"Field-Aligned Current $\\frac{B}{\\mu_0}$((∂n/∂b)·T − (∂b/∂n)·T)\"\n",
1465
1465
  " ),\n",
1466
1466
  " cbar_label=\"FAC\",\n",
1467
- " unit=\"A\",\n",
1467
+ " unit=\"A/m$^2$\",\n",
1468
1468
  " force_levels=None,\n",
1469
1469
  " ),\n",
1470
1470
  " dict(\n",
@@ -1581,7 +1581,7 @@
1581
1581
  " r\"Field-Aligned Current $\\frac{B}{\\mu_0}$((∂n/∂b)·T − (∂b/∂n)·T)\"\n",
1582
1582
  " ),\n",
1583
1583
  " cbar_label=\"FAC\",\n",
1584
- " unit=\"A\",\n",
1584
+ " unit=\"A/m$^2$\",\n",
1585
1585
  " force_levels=None,\n",
1586
1586
  " ),\n",
1587
1587
  " dict(\n",
@@ -6,7 +6,7 @@ This module re-exports all public functions defined in geopack/geopack.py,
6
6
  so that they can be imported directly from `geopack`.
7
7
  """
8
8
 
9
- __version__ = '2.0.1'
9
+ __version__ = '2.0.2'
10
10
 
11
11
  from .geopack import (
12
12
  update_igrf,
@@ -147,7 +147,7 @@ def extall(iopgen,iopt,iopb,iopr,a,ntot,pdyn,dst,byimf,bzimf,vbimf1,vbimf2,ps,x,
147
147
  theta = 0.
148
148
  else:
149
149
  theta=np.arctan2(byimf,bzimf)
150
- if theta <= 0: theta = 2*np.pi
150
+ if theta <= 0: theta += 2*np.pi
151
151
 
152
152
  ct=np.cos(theta)
153
153
  st=np.sin(theta)
@@ -34,12 +34,13 @@ def gsmgse(xgsm, ygsm, zgsm, j):
34
34
  Transformed coordinates (GSE if j>0, GSM if j<0)
35
35
  """
36
36
  # Check if input is scalar
37
- scalar_input = np.isscalar(xgsm)
37
+ scalar_input = all(np.isscalar(c) for c in (xgsm, ygsm, zgsm))
38
38
 
39
39
  # Ensure arrays
40
40
  xgsm = np.atleast_1d(xgsm)
41
41
  ygsm = np.atleast_1d(ygsm)
42
42
  zgsm = np.atleast_1d(zgsm)
43
+ xgsm, ygsm, zgsm = np.broadcast_arrays(xgsm, ygsm, zgsm)
43
44
 
44
45
  # Get transformation angles from geopack globals
45
46
  chi = gp.chi
@@ -83,12 +84,13 @@ def geigeo(xgei, ygei, zgei, j):
83
84
  Transformed coordinates (GEO if j>0, GEI if j<0)
84
85
  """
85
86
  # Check if input is scalar
86
- scalar_input = np.isscalar(xgei)
87
+ scalar_input = all(np.isscalar(c) for c in (xgei, ygei, zgei))
87
88
 
88
89
  # Ensure arrays
89
90
  xgei = np.atleast_1d(xgei)
90
91
  ygei = np.atleast_1d(ygei)
91
92
  zgei = np.atleast_1d(zgei)
93
+ xgei, ygei, zgei = np.broadcast_arrays(xgei, ygei, zgei)
92
94
 
93
95
  # Get transformation angles from geopack globals
94
96
  cgst = gp.cgst
@@ -132,12 +134,13 @@ def magsm(xmag, ymag, zmag, j):
132
134
  Transformed coordinates (SM if j>0, MAG if j<0)
133
135
  """
134
136
  # Check if input is scalar
135
- scalar_input = np.isscalar(xmag)
137
+ scalar_input = all(np.isscalar(c) for c in (xmag, ymag, zmag))
136
138
 
137
139
  # Ensure arrays
138
140
  xmag = np.atleast_1d(xmag)
139
141
  ymag = np.atleast_1d(ymag)
140
142
  zmag = np.atleast_1d(zmag)
143
+ xmag, ymag, zmag = np.broadcast_arrays(xmag, ymag, zmag)
141
144
 
142
145
  # Get transformation angles from geopack globals
143
146
  sfi = gp.sfi
@@ -181,12 +184,13 @@ def smgsm(xsm, ysm, zsm, j):
181
184
  Transformed coordinates (GSM if j>0, SM if j<0)
182
185
  """
183
186
  # Check if input is scalar
184
- scalar_input = np.isscalar(xsm)
187
+ scalar_input = all(np.isscalar(c) for c in (xsm, ysm, zsm))
185
188
 
186
189
  # Ensure arrays
187
190
  xsm = np.atleast_1d(xsm)
188
191
  ysm = np.atleast_1d(ysm)
189
192
  zsm = np.atleast_1d(zsm)
193
+ xsm, ysm, zsm = np.broadcast_arrays(xsm, ysm, zsm)
190
194
 
191
195
  # Get transformation angles from geopack globals
192
196
  sps = gp.sps
@@ -230,12 +234,13 @@ def geomag(xgeo, ygeo, zgeo, j):
230
234
  Transformed coordinates (MAG if j>0, GEO if j<0)
231
235
  """
232
236
  # Check if input is scalar
233
- scalar_input = np.isscalar(xgeo)
237
+ scalar_input = all(np.isscalar(c) for c in (xgeo, ygeo, zgeo))
234
238
 
235
239
  # Ensure arrays
236
240
  xgeo = np.atleast_1d(xgeo)
237
241
  ygeo = np.atleast_1d(ygeo)
238
242
  zgeo = np.atleast_1d(zgeo)
243
+ xgeo, ygeo, zgeo = np.broadcast_arrays(xgeo, ygeo, zgeo)
239
244
 
240
245
  # Get transformation matrix elements from geopack globals
241
246
  # These are set by recalc() based on IGRF coefficients
@@ -286,12 +291,13 @@ def geogsm(xgeo, ygeo, zgeo, j):
286
291
  Transformed coordinates (GSM if j>0, GEO if j<0)
287
292
  """
288
293
  # Check if input is scalar
289
- scalar_input = np.isscalar(xgeo)
294
+ scalar_input = all(np.isscalar(c) for c in (xgeo, ygeo, zgeo))
290
295
 
291
296
  # Ensure arrays
292
297
  xgeo = np.atleast_1d(xgeo)
293
298
  ygeo = np.atleast_1d(ygeo)
294
299
  zgeo = np.atleast_1d(zgeo)
300
+ xgeo, ygeo, zgeo = np.broadcast_arrays(xgeo, ygeo, zgeo)
295
301
 
296
302
  # Get transformation matrix elements from geopack globals
297
303
  a11 = gp.a11
@@ -342,12 +348,13 @@ def gswgsm(xgsw, ygsw, zgsw, j):
342
348
  Transformed coordinates (GSM if j>0, GSW if j<0)
343
349
  """
344
350
  # Check if input is scalar
345
- scalar_input = np.isscalar(xgsw)
351
+ scalar_input = all(np.isscalar(c) for c in (xgsw, ygsw, zgsw))
346
352
 
347
353
  # Ensure arrays
348
354
  xgsw = np.atleast_1d(xgsw)
349
355
  ygsw = np.atleast_1d(ygsw)
350
356
  zgsw = np.atleast_1d(zgsw)
357
+ xgsw, ygsw, zgsw = np.broadcast_arrays(xgsw, ygsw, zgsw)
351
358
 
352
359
  # Get transformation matrix elements from geopack globals
353
360
  e11 = gp.e11
@@ -378,4 +385,4 @@ def gswgsm(xgsw, ygsw, zgsw, j):
378
385
  if scalar_input:
379
386
  return x_out.item(), y_out.item(), z_out.item()
380
387
  else:
381
- return x_out, y_out, z_out
388
+ return x_out, y_out, z_out
@@ -25,12 +25,13 @@ def sphcar(r, theta, phi, j):
25
25
  Transformed coordinates (x,y,z if j>0, r,theta,phi if j<0)
26
26
  """
27
27
  # Check if input is scalar
28
- scalar_input = np.isscalar(r)
28
+ scalar_input = all(np.isscalar(c) for c in (r, theta, phi))
29
29
 
30
30
  # Ensure arrays
31
31
  r = np.atleast_1d(r)
32
32
  theta = np.atleast_1d(theta)
33
33
  phi = np.atleast_1d(phi)
34
+ r, theta, phi = np.broadcast_arrays(r, theta, phi)
34
35
 
35
36
  if j > 0: # Spherical to Cartesian
36
37
  sq = r * np.sin(theta)
@@ -81,7 +82,7 @@ def bspcar(theta, phi, br, btheta, bphi):
81
82
  Magnetic field components in Cartesian coordinates (bx, by, bz)
82
83
  """
83
84
  # Check if input is scalar
84
- scalar_input = np.isscalar(theta)
85
+ scalar_input = all(np.isscalar(c) for c in (theta, phi, br, btheta, bphi))
85
86
 
86
87
  # Ensure arrays
87
88
  theta = np.atleast_1d(theta)
@@ -89,6 +90,7 @@ def bspcar(theta, phi, br, btheta, bphi):
89
90
  br = np.atleast_1d(br)
90
91
  btheta = np.atleast_1d(btheta)
91
92
  bphi = np.atleast_1d(bphi)
93
+ theta, phi, br, btheta, bphi = np.broadcast_arrays(theta, phi, br, btheta, bphi)
92
94
 
93
95
  # Calculate trigonometric values
94
96
  s = np.sin(theta)
@@ -126,7 +128,7 @@ def bcarsp(x, y, z, bx, by, bz):
126
128
  Magnetic field components in spherical coordinates (br, btheta, bphi)
127
129
  """
128
130
  # Check if input is scalar
129
- scalar_input = np.isscalar(x)
131
+ scalar_input = all(np.isscalar(c) for c in (x, y, z, bx, by, bz))
130
132
 
131
133
  # Ensure arrays
132
134
  x = np.atleast_1d(x)
@@ -135,6 +137,7 @@ def bcarsp(x, y, z, bx, by, bz):
135
137
  bx = np.atleast_1d(bx)
136
138
  by = np.atleast_1d(by)
137
139
  bz = np.atleast_1d(bz)
140
+ x, y, z, bx, by, bz = np.broadcast_arrays(x, y, z, bx, by, bz)
138
141
 
139
142
  # Calculate distances
140
143
  rho2 = x*x + y*y
@@ -167,4 +170,4 @@ def bcarsp(x, y, z, bx, by, bz):
167
170
  if scalar_input:
168
171
  return br.item(), btheta.item(), bphi.item()
169
172
  else:
170
- return br, btheta, bphi
173
+ return br, btheta, bphi
@@ -225,7 +225,7 @@ def igrf_gsm(xgsm, ygsm, zgsm):
225
225
  Magnetic field components in GSM coordinates (nT)
226
226
  """
227
227
  # Handle scalar inputs
228
- scalar_input = np.isscalar(xgsm)
228
+ scalar_input = all(np.isscalar(c) for c in (xgsm, ygsm, zgsm))
229
229
  xgsm = np.atleast_1d(xgsm)
230
230
  ygsm = np.atleast_1d(ygsm)
231
231
  zgsm = np.atleast_1d(zgsm)
@@ -270,7 +270,7 @@ def igrf_gsw(xgsw, ygsw, zgsw):
270
270
  Magnetic field components in GSW coordinates (nT)
271
271
  """
272
272
  # Handle scalar inputs
273
- scalar_input = np.isscalar(xgsw)
273
+ scalar_input = all(np.isscalar(c) for c in (xgsw, ygsw, zgsw))
274
274
  xgsw = np.atleast_1d(xgsw)
275
275
  ygsw = np.atleast_1d(ygsw)
276
276
  zgsw = np.atleast_1d(zgsw)
@@ -289,4 +289,4 @@ def igrf_gsw(xgsw, ygsw, zgsw):
289
289
  if scalar_input:
290
290
  return bxgsw.item(), bygsw.item(), bzgsw.item()
291
291
  else:
292
- return bxgsw, bygsw, bzgsw
292
+ return bxgsw, bygsw, bzgsw
@@ -60,10 +60,11 @@ def t01(parmod, ps, x, y, z):
60
60
  # Track if all inputs were scalar
61
61
  scalar_input = np.isscalar(x) and np.isscalar(y) and np.isscalar(z)
62
62
 
63
- # Convert inputs to numpy arrays
64
- x = np.atleast_1d(x)
65
- y = np.atleast_1d(y)
66
- z = np.atleast_1d(z)
63
+ # Normalize dtype and shape before masked coordinate iterations.
64
+ x, y, z = np.broadcast_arrays(
65
+ np.atleast_1d(np.asarray(x, dtype=float)),
66
+ np.atleast_1d(np.asarray(y, dtype=float)),
67
+ np.atleast_1d(np.asarray(z, dtype=float)))
67
68
 
68
69
  a = np.array([
69
70
  1.00000, 2.47341, 0.40791, 0.30429, -0.10637, -0.89108, 3.29350,
@@ -133,7 +134,7 @@ def extall(iopgen,iopt,iopb,iopr,a,ntot,pdyn,dst,byimf,bzimf,vbimf1,vbimf2,ps,x,
133
134
  theta = 0.
134
135
  else:
135
136
  theta=np.arctan2(byimf,bzimf)
136
- if theta <= 0: theta = 2*np.pi
137
+ if theta <= 0: theta += 2*np.pi
137
138
 
138
139
  ct=np.cos(theta)
139
140
  st=np.sin(theta)
@@ -172,8 +173,7 @@ def extall(iopgen,iopt,iopb,iopr,a,ntot,pdyn,dst,byimf,bzimf,vbimf1,vbimf2,ps,x,
172
173
  zss[active] = znew
173
174
 
174
175
  # Skip already-converged points in subsequent iterations
175
- active_idx = np.where(active)[0]
176
- active[active_idx] = dd > 1e-6
176
+ active[active] = dd > 1e-6
177
177
 
178
178
  rho2=y**2+zss**2
179
179
  asq=am**2
@@ -1966,4 +1966,4 @@ def dipole(ps, x, y, z):
1966
1966
  by = -3 * y * q * (x * sps + z * cps)
1967
1967
  bz = q * ((x2 + y2 - 2 * z2) * cps - xz3 * sps)
1968
1968
 
1969
- return bx, by, bz
1969
+ return bx, by, bz
@@ -46,9 +46,9 @@ def t96(parmod, ps, x, y, z):
46
46
  scalar_input = np.isscalar(x) and np.isscalar(y) and np.isscalar(z)
47
47
 
48
48
  # Convert inputs to numpy arrays
49
- x = np.atleast_1d(x)
50
- y = np.atleast_1d(y)
51
- z = np.atleast_1d(z)
49
+ x = np.atleast_1d(np.asarray(x, dtype=float))
50
+ y = np.atleast_1d(np.asarray(y, dtype=float))
51
+ z = np.atleast_1d(np.asarray(z, dtype=float))
52
52
 
53
53
  # Broadcast arrays to same shape
54
54
  x, y, z = np.broadcast_arrays(x, y, z)
@@ -2443,4 +2443,4 @@ if __name__ == '__main__':
2443
2443
  print("\nArray input:")
2444
2444
  for i in range(len(x_arr)):
2445
2445
  print(f" Point ({x_arr[i]}, {y_arr[i]}, {z_arr[i]}): "
2446
- f"B = ({bx_arr[i]:.3f}, {by_arr[i]:.3f}, {bz_arr[i]:.3f}) nT")
2446
+ f"B = ({bx_arr[i]:.3f}, {by_arr[i]:.3f}, {bz_arr[i]:.3f}) nT")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geopack-vectorize
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Python implementation of geopack and Tsyganenko models with optimized vectorization
5
5
  Home-page: https://github.com/Butadiene/geopack-vectorize
6
6
  Author: geopack-vectorize contributors
@@ -64,6 +64,12 @@ geopack-vectorize extends the original geopack by adding vectorized implementati
64
64
  - **Full Backward Compatibility**: All original geopack functions remain unchanged and available
65
65
  - **Comprehensive Validation**: Extensive test suite ensuring < 10⁻¹¹ relative error vs original implementations
66
66
 
67
+ ## Publication and Citation
68
+
69
+ geopack-vectorize, including its Mageometry component for local-frame analysis of magnetic field geometry, is described in the following paper published in *Earth, Planets and Space*. If you use this work in your research, please cite:
70
+
71
+ > Isono, K., Yoshikawa, A. & Katoh, Y. (2026). A local-frame approach to three-dimensional analysis of magnetospheric magnetic fields. *Earth, Planets and Space*, **78**, Article 201. [doi:10.1186/s40623-026-02528-2](https://doi.org/10.1186/s40623-026-02528-2).
72
+
67
73
  ## Upgrading to v2.0.0
68
74
 
69
75
  As of v2.0.0, the PyPI package name has changed from `geopack-vectorized` to `geopack-vectorize`. The old `geopack-vectorized` package will remain available for a transitional period but will eventually be removed. Please update your installation:
@@ -60,7 +60,9 @@ geopack_vectorize.egg-info/dependency_links.txt
60
60
  geopack_vectorize.egg-info/requires.txt
61
61
  geopack_vectorize.egg-info/top_level.txt
62
62
  tests/installed_smoke.py
63
+ tests/test_coordinate_broadcasting.py
63
64
  tests/test_distribution.py
65
+ tests/test_model_input_regressions.py
64
66
  tests/test_trace_vectorized.py
65
67
  tests/test_trace_vectorized_with_vectorized_models.py
66
68
  tests/test_vectorized_models.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "geopack-vectorize"
7
- version = "2.0.1"
7
+ version = "2.0.2"
8
8
  description = "Python implementation of geopack and Tsyganenko models with optimized vectorization"
9
9
  authors = [
10
10
  {name = "Sheng Tian", email = "ts0110@atmos.ucla.edu"}
@@ -6,7 +6,7 @@ with open('README.md', 'r', encoding='utf-8') as fh:
6
6
 
7
7
  setuptools.setup(
8
8
  name='geopack-vectorize',
9
- version='2.0.1',
9
+ version='2.0.2',
10
10
  author='geopack-vectorize contributors',
11
11
  author_email='',
12
12
  description='Vectorized Python implementation of geopack and Tsyganenko models (fork of tsssss/geopack)',
@@ -0,0 +1,79 @@
1
+ """Scalar and NumPy broadcasting contracts for transforms and IGRF wrappers."""
2
+
3
+ import unittest
4
+
5
+ import numpy as np
6
+
7
+ from geopack import geopack as scalar
8
+ from geopack.vectorized import coordinates, coordinates_complex, igrf
9
+
10
+
11
+ class TestCoordinateBroadcasting(unittest.TestCase):
12
+ @classmethod
13
+ def setUpClass(cls):
14
+ scalar.recalc(100., vxgse=-400., vygse=15., vzgse=20.)
15
+
16
+ def assert_pointwise(self, vector_function, scalar_function, arguments, extra=()):
17
+ broadcast = np.broadcast_arrays(*arguments)
18
+ expected = np.array([scalar_function(*point, *extra)
19
+ for point in zip(*(c.ravel() for c in broadcast))])
20
+ expected = expected.T.reshape((3,) + broadcast[0].shape)
21
+ actual = vector_function(*arguments, *extra)
22
+ for component in actual:
23
+ self.assertEqual(component.shape, broadcast[0].shape)
24
+ np.testing.assert_allclose(actual, expected, rtol=1e-11, atol=1e-8)
25
+
26
+ def test_cartesian_transforms_broadcast_each_component_in_both_directions(self):
27
+ names = ('gsmgse', 'geigeo', 'magsm', 'smgsm', 'geomag', 'geogsm', 'gswgsm')
28
+ cases = [(np.array([1., 2.]), 3., 4.),
29
+ (1., np.array([2., 3.]), 4.),
30
+ (1., 2., np.array([3., 4.])),
31
+ (np.array([1., 2.])[:, None], np.array([2., 3., 4.]), 4.)]
32
+ for name in names:
33
+ for direction in (-1, 1):
34
+ for coords in cases:
35
+ with self.subTest(transform=name, direction=direction,
36
+ shapes=tuple(np.shape(c) for c in coords)):
37
+ self.assert_pointwise(getattr(coordinates, name), getattr(scalar, name),
38
+ coords, (direction,))
39
+ with self.subTest(transform=name, direction=direction, scalar=True):
40
+ result = getattr(coordinates, name)(1., 2., 3., direction)
41
+ self.assertTrue(all(isinstance(c, float) for c in result))
42
+ np.testing.assert_allclose(result, getattr(scalar, name)(1., 2., 3., direction))
43
+
44
+ def test_spherical_coordinate_transforms_broadcast(self):
45
+ cases = (
46
+ ((5., np.array([.5, 1.]), .3), 1),
47
+ ((np.array([4., 5.]), .5, .3), 1),
48
+ ((5., .5, np.array([.3, .6])), 1),
49
+ ((1., np.array([2., 3.]), 4.), -1),
50
+ ((np.array([1., 2.])[:, None], 2., np.array([3., 4., 5.])), -1),
51
+ )
52
+ for coords, direction in cases:
53
+ with self.subTest(direction=direction, shapes=tuple(np.shape(c) for c in coords)):
54
+ self.assert_pointwise(coordinates_complex.sphcar, scalar.sphcar, coords, (direction,))
55
+
56
+ def test_magnetic_component_transforms_broadcast_every_input(self):
57
+ for name, base in (('bspcar', [.7, .3, 4., 2., 1.]),
58
+ ('bcarsp', [4., 2., 1., 3., 2., 1.])):
59
+ for index in range(len(base)):
60
+ arguments = list(base)
61
+ arguments[index] = base[index] + np.array([0., .1])
62
+ with self.subTest(transform=name, vector_input=index):
63
+ self.assert_pointwise(getattr(coordinates_complex, name), getattr(scalar, name),
64
+ arguments)
65
+ result = getattr(coordinates_complex, name)(*base)
66
+ self.assertTrue(all(isinstance(c, float) for c in result))
67
+
68
+ def test_igrf_cartesian_wrappers_broadcast(self):
69
+ for name in ('igrf_gsm', 'igrf_gsw'):
70
+ for coords in ((5., np.array([1., 2.]), 1.),
71
+ (np.array([4., 5.])[:, None], np.array([1., 2., 3.]), 1.)):
72
+ with self.subTest(wrapper=name, shapes=tuple(np.shape(c) for c in coords)):
73
+ self.assert_pointwise(getattr(igrf, name), getattr(scalar, name), coords)
74
+ result = getattr(igrf, name)(5., 1., 1.)
75
+ self.assertTrue(all(isinstance(c, float) for c in result))
76
+
77
+
78
+ if __name__ == '__main__':
79
+ unittest.main()
@@ -0,0 +1,67 @@
1
+ """Regressions independent of scalar/vectorized implementation agreement."""
2
+
3
+ import unittest
4
+
5
+ import numpy as np
6
+
7
+ from geopack import models
8
+ from geopack.vectorized import models as vectorized
9
+
10
+
11
+ PARMOD = [2., -20., 2., -5., 1., 1., 1., 1., 1., 1.]
12
+
13
+
14
+ class TestModelInputRegressions(unittest.TestCase):
15
+ def test_t01_imf_interconnection_preserves_clock_angle(self):
16
+ # With G1/G2 held fixed, the IMF dependence is exactly the prescribed
17
+ # T01 interconnection field, including outside the magnetopause.
18
+ for function in (models.t01, vectorized.t01):
19
+ for point in ((-6., 2., 1.), (15., 3., 1.)):
20
+ base = list(PARMOD)
21
+ base[2:4] = [0., 0.]
22
+ baseline = np.array(function(base, .2, *point))
23
+ for by in (-5., -1e-8, -0., 0., 1e-8, 5.):
24
+ for bz in (-5., 5.):
25
+ with self.subTest(api=function.__module__, point=point, by=by, bz=bz):
26
+ parameters = list(PARMOD)
27
+ parameters[2:4] = [by, bz]
28
+ theta = np.arctan2(by, bz)
29
+ factor = .05887 + .57691 * np.sin(theta / 2.)**2
30
+ expected = baseline + factor * np.array([0., by, bz])
31
+ actual = function(parameters, .2, *point)
32
+ np.testing.assert_allclose(actual, expected, rtol=1e-12, atol=1e-9)
33
+
34
+ def test_integer_coordinates_match_scalar_fields_in_all_regions(self):
35
+ points = np.array([[5, 1, 1], [10, 0, -5], [15, 1, 1]])
36
+ for name in ('t01', 't96'):
37
+ scalar, batched = getattr(models, name), getattr(vectorized, name)
38
+ expected = np.array([scalar(PARMOD, .4, *point.astype(float)) for point in points]).T
39
+ with self.subTest(model=name, mode='batch'):
40
+ np.testing.assert_allclose(batched(PARMOD, .4, *points.T), expected,
41
+ rtol=1e-10, atol=1e-6)
42
+ for point, field in zip(points, expected.T):
43
+ with self.subTest(model=name, point=point):
44
+ actual = batched(PARMOD, .4, *(int(c) for c in point))
45
+ np.testing.assert_allclose(actual, field, rtol=1e-10, atol=1e-6)
46
+ self.assertTrue(all(isinstance(c, float) for c in actual))
47
+
48
+ def test_t01_broadcast_and_mesh_inputs_match_pointwise_fields(self):
49
+ points = (
50
+ (5., np.array([1., 2.]), 1.),
51
+ (np.array([5., 10.])[:, None], np.array([1., 2., 3.]), -5.),
52
+ np.meshgrid([5., 10.], [1., 2.], [-5., 1.], indexing='ij'),
53
+ )
54
+ for coords in points:
55
+ broadcast = np.broadcast_arrays(*coords)
56
+ expected = np.array([models.t01(PARMOD, .4, *point)
57
+ for point in zip(*(c.ravel() for c in broadcast))])
58
+ expected = expected.T.reshape((3,) + broadcast[0].shape)
59
+ with self.subTest(shape=broadcast[0].shape):
60
+ actual = vectorized.t01(PARMOD, .4, *coords)
61
+ for component in actual:
62
+ self.assertEqual(component.shape, broadcast[0].shape)
63
+ np.testing.assert_allclose(actual, expected, rtol=1e-10, atol=1e-6)
64
+
65
+
66
+ if __name__ == '__main__':
67
+ unittest.main()