planetarypy 0.76.2__tar.gz → 0.76.3__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 (65) hide show
  1. {planetarypy-0.76.2 → planetarypy-0.76.3}/CHANGELOG.md +12 -0
  2. {planetarypy-0.76.2 → planetarypy-0.76.3}/PKG-INFO +1 -1
  3. {planetarypy-0.76.2 → planetarypy-0.76.3}/pyproject.toml +3 -2
  4. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/__init__.py +1 -1
  5. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/instruments/mro/hirise.py +8 -0
  6. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/plotting.py +66 -47
  7. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/search.py +5 -1
  8. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/spice/archived_kernels.py +27 -9
  9. planetarypy-0.76.2/src/planetarypy/instruments/go/__init__.py +0 -0
  10. planetarypy-0.76.2/src/planetarypy/instruments/mro/__init__.py +0 -0
  11. {planetarypy-0.76.2 → planetarypy-0.76.3}/.gitignore +0 -0
  12. {planetarypy-0.76.2 → planetarypy-0.76.3}/AUTHORS.md +0 -0
  13. {planetarypy-0.76.2 → planetarypy-0.76.3}/LICENSE +0 -0
  14. {planetarypy-0.76.2 → planetarypy-0.76.3}/README.md +0 -0
  15. {planetarypy-0.76.2 → planetarypy-0.76.3}/docs/README.md +0 -0
  16. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/catalog/__init__.py +0 -0
  17. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/catalog/_index_resolver.py +0 -0
  18. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/catalog/_mission_map.py +0 -0
  19. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/catalog/_objects.py +0 -0
  20. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/catalog/_parser.py +0 -0
  21. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/catalog/_pattern_resolver.py +0 -0
  22. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/catalog/_repo.py +0 -0
  23. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/catalog/_resolver.py +0 -0
  24. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/catalog/_schema.py +0 -0
  25. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/catalog/_url_rewrite.py +0 -0
  26. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/catalog/_validation.py +0 -0
  27. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/cli.py +0 -0
  28. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/config.py +0 -0
  29. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/constants/__init__.py +0 -0
  30. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/constants/_gm_jpl.py +0 -0
  31. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/constants/base.py +0 -0
  32. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/constants/iau2009.py +0 -0
  33. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/constants/iau2015.py +0 -0
  34. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/constants/nssdc/__init__.py +0 -0
  35. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/constants/nssdc/_loader.py +0 -0
  36. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/crs.py +0 -0
  37. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/data/__init__.py +0 -0
  38. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/datetime_format_converters.py +0 -0
  39. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/geo.py +0 -0
  40. {planetarypy-0.76.2/src/planetarypy/instruments → planetarypy-0.76.3/src/planetarypy/instruments/go}/__init__.py +0 -0
  41. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/instruments/go/ssi.py +0 -0
  42. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/instruments/mro/ctx/__init__.py +0 -0
  43. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/instruments/mro/ctx/ctx_calib.py +0 -0
  44. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/instruments/mro/ctx/ctx_edr.py +0 -0
  45. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/instruments/utils.py +0 -0
  46. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/io.py +0 -0
  47. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/isis/autoseed.py +0 -0
  48. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/pds/__init__.py +0 -0
  49. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/pds/dynamic_index.py +0 -0
  50. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/pds/dynamic_url_handlers.py +0 -0
  51. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/pds/index_fixes.py +0 -0
  52. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/pds/index_labels.py +0 -0
  53. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/pds/index_logging.py +0 -0
  54. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/pds/index_main.py +0 -0
  55. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/pds/meta_display.py +0 -0
  56. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/pds/static_index.py +0 -0
  57. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/pds/utils.py +0 -0
  58. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/psa.py +0 -0
  59. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/spice/__init__.py +0 -0
  60. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/spice/_deps.py +0 -0
  61. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/spice/config.py +0 -0
  62. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/spice/generic_kernels.py +0 -0
  63. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/spice/pckernels.py +0 -0
  64. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/spice/spicer.py +0 -0
  65. {planetarypy-0.76.2 → planetarypy-0.76.3}/src/planetarypy/utils.py +0 -0
@@ -5,6 +5,18 @@ 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.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.76.3] - 2026-06-17
9
+
10
+ ### Fixed
11
+
12
+ - **PDS registry search: multi-filter queries no longer fail with HTTP 400.** `planetarypy.search` built its query string by joining clauses with ` and `, but the NASA PDS registry API requires the whole query wrapped in outer parentheses — so any search combining two or more filters (e.g. `target` + `observationals`, or a spatial bounding-box) returned `400 UnparsableQParamException` and only single-clause lookups worked. The query builder now wraps the joined clauses in `(...)`, matching what `pds.peppi` does. This also unblocks spatial searches via the `cart:Bounding_Coordinates.cart:*_bounding_coordinate` fields.
13
+ - **Archived SPICE metakernels with non-`'./data'` path conventions now load.** `archived_kernels.get_metakernel()` rewrote kernel paths by matching the literal `'./data'` in `PATH_VALUES`, which most NAIF archives use — but some (e.g. Hayabusa2's PDS4 archive) ship `'..'` instead. The rewrite silently no-opped on those, leaving an unresolvable relative path that broke `furnsh`. The rewrite is now convention-agnostic: it repoints whatever value the `PATH_VALUES` block holds to the absolute local kernel directory, leaving `KERNELS_TO_LOAD` symbol references intact.
14
+ - **`plotting.add_sun_indicator` no longer rescales the image.** The sun-direction glyph was drawn in data coordinates anchored in a corner; for any azimuth pointing past that corner, plotting the off-bounds marker triggered matplotlib autoscale, adding whitespace and pushing the indicator outside the frame. It is now drawn in axes-fraction coordinates with an inset anchor, so it stays inside the panel for any azimuth and never touches the data limits (this also removes a latent `origin='upper'`-only assumption).
15
+
16
+ ### Changed
17
+
18
+ - **`plotting.add_sun_indicator` / `imshow_with_sun` are marked experimental.** They now carry an experimental note and emit a one-shot `UserWarning`. The azimuth-convention handoff is not yet validated end-to-end: these helpers expect azimuth clockwise-from-image-top (PDS `SUB_SOLAR_AZIMUTH`), whereas `Spicer.solar_azimuth_at` returns clockwise-from-north — they agree only when image-north points up.
19
+
8
20
  ## [0.76.2] - 2026-06-12
9
21
 
10
22
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: planetarypy
3
- Version: 0.76.2
3
+ Version: 0.76.3
4
4
  Summary: Core package for planetary data tools. Includes PDS index utilities, SPICE integrations, and more.
5
5
  Project-URL: Homepage, https://github.com/planetarypy/planetarypy
6
6
  Project-URL: Repository, https://github.com/planetarypy/planetarypy
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "planetarypy"
7
- version = "0.76.2"
7
+ version = "0.76.3"
8
8
  description = "Core package for planetary data tools. Includes PDS index utilities, SPICE integrations, and more."
9
9
  readme = "README.md"
10
10
  requires-python = ">= 3.11, <4"
@@ -185,6 +185,7 @@ DEP001 = [
185
185
  # DEP002 = "declared dependency is not used in the codebase"
186
186
  DEP002 = [
187
187
  "lxml", # used at call time by pandas.read_html in spice/archived_kernels.py
188
+ "pyarrow", # parquet engine loaded internally by pandas to_parquet/read_parquet; never imported directly
188
189
  "pytest", # CLI tool used in CI / dev; not imported from src
189
190
  "pytest-cov",
190
191
  "pytest-xdist",
@@ -201,7 +202,7 @@ DEP003 = [
201
202
  ]
202
203
 
203
204
  [tool.bumpversion]
204
- current_version = "0.76.2"
205
+ current_version = "0.76.3"
205
206
  commit = true
206
207
  tag = true
207
208
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  __author__ = """PlanetaryPy Developers"""
4
4
  __email__ = "kmichael.aye@gmail.com"
5
- __version__ = "0.76.2"
5
+ __version__ = "0.76.3"
6
6
 
7
7
  __all__ = [
8
8
  "enable_logging",
@@ -398,6 +398,14 @@ try:
398
398
  except ImportError:
399
399
  pass # catalog not available
400
400
 
401
+ # Register the HiRISE meta-display handler with the PDS meta dispatcher
402
+ try:
403
+ from planetarypy.pds.meta_display import register_meta_handler
404
+ register_meta_handler("mro.hirise.edr", format_meta)
405
+ register_meta_handler("mro.hirise.rdr", format_meta)
406
+ except ImportError:
407
+ pass # pds.meta_display not available
408
+
401
409
 
402
410
  def _edr_base_url() -> URL:
403
411
  """Resolve EDR base URL from config."""
@@ -4,8 +4,14 @@ Provides common visualization helpers for planetary science:
4
4
  percentile stretching, grayscale image display, sun direction indicators.
5
5
  """
6
6
 
7
+ import warnings
8
+
7
9
  import numpy as np
8
10
 
11
+ # One-shot guard so a multi-panel figure doesn't emit the experimental
12
+ # warning once per subplot.
13
+ _SUN_INDICATOR_WARNED = False
14
+
9
15
 
10
16
  def percentile_stretch(image, lo=1, hi=99):
11
17
  """Compute display limits from percentiles, ignoring zeros and NaN.
@@ -76,27 +82,34 @@ def imshow_gray(image, stretch="1,99", title=None, ax=None, **imshow_kwargs):
76
82
 
77
83
  def add_sun_indicator(ax, sun_azimuth_deg, position="upper right",
78
84
  length=0.12, color="yellow", inner_color="orange"):
79
- """Add a sun direction indicator to an image plot.
85
+ """Add a sun direction indicator to an image plot. **Experimental.**
86
+
87
+ .. warning::
88
+
89
+ Experimental. The azimuth-convention handoff is not fully validated:
90
+ this function expects **clockwise-from-image-top** (the PDS
91
+ ``SUB_SOLAR_AZIMUTH`` convention for *unprojected* images), but
92
+ :meth:`planetarypy.spice.spicer.Spicer.solar_azimuth_at` returns
93
+ **clockwise-from-north** (geographic). They agree only when image-north
94
+ points up; otherwise you must rotate by the image's north azimuth before
95
+ passing the value here. Placement and appearance may change.
80
96
 
81
- The indicator shows a circle (sun) with a line pointing in the
82
- direction of illumination. The azimuth is measured clockwise from
83
- the top of the image.
97
+ The indicator is a small compass glyph (sun ball + arrow) drawn in a corner,
98
+ pointing toward the sun. It is rendered in **axes-fraction coordinates**, so
99
+ it neither rescales the image nor depends on the image's ``origin``.
84
100
 
85
101
  Parameters
86
102
  ----------
87
103
  ax : matplotlib.axes.Axes
88
104
  The axes containing the image.
89
105
  sun_azimuth_deg : float
90
- Solar azimuth in degrees, clockwise from image top.
91
- This matches the PDS `SUB_SOLAR_AZIMUTH` convention for
92
- unprojected images.
106
+ Solar azimuth in degrees, clockwise from image top (see warning).
93
107
  position : str
94
- Where to place the indicator: "upper right", "upper left",
95
- "lower right", "lower left".
108
+ "upper right", "upper left", "lower right", "lower left".
96
109
  length : float
97
- Line length as fraction of image size (default 0.12).
110
+ Arrow length as a fraction of the axes (default 0.12).
98
111
  color : str
99
- Outer circle and line color.
112
+ Outer circle and arrow color.
100
113
  inner_color : str
101
114
  Inner circle color (smaller, overlaid on outer).
102
115
 
@@ -104,56 +117,62 @@ def add_sun_indicator(ax, sun_azimuth_deg, position="upper right",
104
117
  -------
105
118
  ax : matplotlib.axes.Axes
106
119
  """
107
- xlim = ax.get_xlim()
108
- ylim = ax.get_ylim()
109
- # Image extent (works for both origin='upper' and data coords)
110
- width = abs(xlim[1] - xlim[0])
111
- height = abs(ylim[1] - ylim[0])
112
-
113
- # Position the indicator
114
- margin = 0.08
115
- positions = {
116
- "upper right": (xlim[0] + width * (1 - margin), min(ylim) + height * margin),
117
- "upper left": (xlim[0] + width * margin, min(ylim) + height * margin),
118
- "lower right": (xlim[0] + width * (1 - margin), min(ylim) + height * (1 - margin)),
119
- "lower left": (xlim[0] + width * margin, min(ylim) + height * (1 - margin)),
120
+ global _SUN_INDICATOR_WARNED
121
+ if not _SUN_INDICATOR_WARNED:
122
+ warnings.warn(
123
+ "add_sun_indicator is experimental: the azimuth convention "
124
+ "(clockwise-from-top vs Spicer's clockwise-from-north) is not fully "
125
+ "validated, and placement/appearance may change.",
126
+ UserWarning,
127
+ stacklevel=2,
128
+ )
129
+ _SUN_INDICATOR_WARNED = True
130
+
131
+ L = float(length)
132
+ # Inset the anchor by at least the arrow length so the glyph stays inside the
133
+ # axes for *any* azimuth. Axes-fraction coords (y up, independent of image
134
+ # origin); drawing here never touches the data limits, so the image is not
135
+ # rescaled — the bug the old data-coordinate corner placement had.
136
+ inset = L + 0.06
137
+ corners = {
138
+ "upper right": (1 - inset, 1 - inset),
139
+ "upper left": (inset, 1 - inset),
140
+ "lower right": (1 - inset, inset),
141
+ "lower left": (inset, inset),
120
142
  }
121
- cx, cy = positions.get(position, positions["upper right"])
122
-
123
- line_len = length * max(width, height)
124
- az_rad = np.radians(sun_azimuth_deg)
125
- # CW from top: dx = sin(az), dy = -cos(az) for origin='upper'
126
- dx = line_len * np.sin(az_rad)
127
- dy = -line_len * np.cos(az_rad)
143
+ cx, cy = corners.get(position, corners["upper right"])
128
144
 
129
- # Sun circle in the sun direction, arrow points outward toward sun
145
+ az = np.radians(sun_azimuth_deg)
146
+ # Clockwise from top in axes-fraction space (y increases upward):
147
+ # 0deg -> up (+y), 90deg -> right (+x).
148
+ dx = L * np.sin(az)
149
+ dy = L * np.cos(az)
130
150
  sx, sy = cx + dx, cy + dy
131
151
 
132
- # Shorten arrow slightly so the tip doesn't overlap the sun ball
133
- shrink = 0.08 * line_len
134
- norm = np.hypot(dx, dy)
135
- ax_dx = dx / norm * shrink
136
- ax_dy = dy / norm * shrink
152
+ # Stop the arrow just short of the sun ball.
153
+ shrink = 0.12 * L
154
+ norm = np.hypot(dx, dy) or 1.0
155
+ tipx, tipy = sx - dx / norm * shrink, sy - dy / norm * shrink
137
156
 
138
157
  ax.annotate("",
139
- xy=(sx - ax_dx, sy - ax_dy), # arrow tip (gap before sun ball)
140
- xytext=(cx, cy), # arrow tail (interior)
158
+ xy=(tipx, tipy), xytext=(cx, cy),
159
+ xycoords="axes fraction", textcoords="axes fraction",
141
160
  arrowprops=dict(arrowstyle="-|>", color=color, lw=2.5,
142
161
  mutation_scale=20),
143
- zorder=5)
144
-
145
- # Sun symbol just past the arrow tip
146
- ax.plot(sx, sy, "o", color=color, markersize=14, zorder=8)
147
- ax.plot(sx, sy, "o", color=inner_color, markersize=9, zorder=9)
148
-
162
+ zorder=5, annotation_clip=False)
163
+ ax.plot(sx, sy, "o", color=color, markersize=14, zorder=8,
164
+ transform=ax.transAxes, clip_on=False)
165
+ ax.plot(sx, sy, "o", color=inner_color, markersize=9, zorder=9,
166
+ transform=ax.transAxes, clip_on=False)
149
167
  return ax
150
168
 
151
169
 
152
170
  def imshow_with_sun(image, sun_azimuth_deg, title=None, ax=None,
153
171
  stretch="1,99", sun_position="upper right", **imshow_kwargs):
154
- """Display a grayscale image with a sun direction indicator.
172
+ """Display a grayscale image with a sun direction indicator. **Experimental.**
155
173
 
156
- Combines `imshow_gray` with `add_sun_indicator`.
174
+ Combines `imshow_gray` with `add_sun_indicator` (see that function's
175
+ experimental warning about the azimuth convention).
157
176
 
158
177
  Parameters
159
178
  ----------
@@ -119,7 +119,11 @@ def _build_q(
119
119
  clauses.append(query)
120
120
  if not clauses:
121
121
  return None
122
- return " and ".join(clauses)
122
+ # The PDS registry API requires the whole q wrapped in outer parentheses:
123
+ # a bare ``A and B`` returns HTTP 400 (UnparsableQParamException), while
124
+ # ``(A and B)`` parses. (peppi does the same in its ResultSet before sending.)
125
+ # Wrapping a single clause is harmless.
126
+ return "(" + " and ".join(clauses) + ")"
123
127
 
124
128
 
125
129
  def _flatten(value: Any) -> Any:
@@ -20,6 +20,7 @@ __all__ = [
20
20
  "list_cached_kernels",
21
21
  ]
22
22
 
23
+ import re
23
24
  import zipfile
24
25
  from datetime import timedelta
25
26
  from io import BytesIO
@@ -516,18 +517,35 @@ class Subsetter:
516
517
  else self.save_location
517
518
  )
518
519
  savepath = basepath / self.metakernel_file
519
- with (
520
- open(savepath, "w") as outfile,
521
- self.z.open(self.metakernel_file) as infile,
522
- ):
523
- for line in infile:
524
- linestr = line.decode()
525
- if "'./data'" in linestr:
526
- linestr = linestr.replace("'./data'", f"'{savepath.parent}'")
527
- outfile.write(linestr)
520
+ text = self.z.read(self.metakernel_file).decode()
521
+ savepath.write_text(_repoint_path_values(text, str(savepath.parent)))
528
522
  return savepath
529
523
 
530
524
 
525
+ def _repoint_path_values(text: str, abspath: str) -> str:
526
+ """Rewrite a metakernel's ``PATH_VALUES`` to the local kernel directory.
527
+
528
+ NAIF archive metakernels use different relative conventions for
529
+ ``PATH_VALUES`` — most missions ship ``'./data'``, but some (e.g. Hayabusa2's
530
+ PDS4 archive) ship ``'..'``. Replace whatever single-quoted value the
531
+ ``PATH_VALUES = ( … )`` block holds with ``abspath``, leaving every other line
532
+ (notably ``KERNELS_TO_LOAD``'s ``$SYMBOL/…`` references) untouched. The old
533
+ code matched the ``'./data'`` literal only, so it silently no-opped on ``'..'``
534
+ and left an unresolvable path that broke ``furnsh``.
535
+ """
536
+ out = []
537
+ in_path_values = False
538
+ for line in text.splitlines(keepends=True):
539
+ if "PATH_VALUES" in line:
540
+ in_path_values = True
541
+ if in_path_values:
542
+ line = re.sub(r"'[^']*'", f"'{abspath}'", line)
543
+ if ")" in line:
544
+ in_path_values = False
545
+ out.append(line)
546
+ return "".join(out)
547
+
548
+
531
549
  def get_metakernel_and_files(
532
550
  mission: str, start: str, stop: str, save_location: str = None, quiet: bool = False
533
551
  ) -> str:
File without changes
File without changes
File without changes
File without changes