setiastrosuitepro 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (174) hide show
  1. setiastro/__init__.py +2 -0
  2. setiastro/saspro/__init__.py +20 -0
  3. setiastro/saspro/__main__.py +784 -0
  4. setiastro/saspro/_generated/__init__.py +7 -0
  5. setiastro/saspro/_generated/build_info.py +2 -0
  6. setiastro/saspro/abe.py +1295 -0
  7. setiastro/saspro/abe_preset.py +196 -0
  8. setiastro/saspro/aberration_ai.py +694 -0
  9. setiastro/saspro/aberration_ai_preset.py +224 -0
  10. setiastro/saspro/accel_installer.py +218 -0
  11. setiastro/saspro/accel_workers.py +30 -0
  12. setiastro/saspro/add_stars.py +621 -0
  13. setiastro/saspro/astrobin_exporter.py +1007 -0
  14. setiastro/saspro/astrospike.py +153 -0
  15. setiastro/saspro/astrospike_python.py +1839 -0
  16. setiastro/saspro/autostretch.py +196 -0
  17. setiastro/saspro/backgroundneutral.py +560 -0
  18. setiastro/saspro/batch_convert.py +325 -0
  19. setiastro/saspro/batch_renamer.py +519 -0
  20. setiastro/saspro/blemish_blaster.py +488 -0
  21. setiastro/saspro/blink_comparator_pro.py +2923 -0
  22. setiastro/saspro/bundles.py +61 -0
  23. setiastro/saspro/bundles_dock.py +114 -0
  24. setiastro/saspro/cheat_sheet.py +168 -0
  25. setiastro/saspro/clahe.py +342 -0
  26. setiastro/saspro/comet_stacking.py +1377 -0
  27. setiastro/saspro/config.py +38 -0
  28. setiastro/saspro/config_bootstrap.py +40 -0
  29. setiastro/saspro/config_manager.py +316 -0
  30. setiastro/saspro/continuum_subtract.py +1617 -0
  31. setiastro/saspro/convo.py +1397 -0
  32. setiastro/saspro/convo_preset.py +414 -0
  33. setiastro/saspro/copyastro.py +187 -0
  34. setiastro/saspro/cosmicclarity.py +1564 -0
  35. setiastro/saspro/cosmicclarity_preset.py +407 -0
  36. setiastro/saspro/crop_dialog_pro.py +948 -0
  37. setiastro/saspro/crop_preset.py +189 -0
  38. setiastro/saspro/curve_editor_pro.py +2544 -0
  39. setiastro/saspro/curves_preset.py +375 -0
  40. setiastro/saspro/debayer.py +670 -0
  41. setiastro/saspro/debug_utils.py +29 -0
  42. setiastro/saspro/dnd_mime.py +35 -0
  43. setiastro/saspro/doc_manager.py +2634 -0
  44. setiastro/saspro/exoplanet_detector.py +2166 -0
  45. setiastro/saspro/file_utils.py +284 -0
  46. setiastro/saspro/fitsmodifier.py +744 -0
  47. setiastro/saspro/free_torch_memory.py +48 -0
  48. setiastro/saspro/frequency_separation.py +1343 -0
  49. setiastro/saspro/function_bundle.py +1594 -0
  50. setiastro/saspro/ghs_dialog_pro.py +660 -0
  51. setiastro/saspro/ghs_preset.py +284 -0
  52. setiastro/saspro/graxpert.py +634 -0
  53. setiastro/saspro/graxpert_preset.py +287 -0
  54. setiastro/saspro/gui/__init__.py +0 -0
  55. setiastro/saspro/gui/main_window.py +8494 -0
  56. setiastro/saspro/gui/mixins/__init__.py +33 -0
  57. setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
  58. setiastro/saspro/gui/mixins/file_mixin.py +445 -0
  59. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  60. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  61. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  62. setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
  63. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  64. setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
  65. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  66. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  67. setiastro/saspro/halobgon.py +462 -0
  68. setiastro/saspro/header_viewer.py +445 -0
  69. setiastro/saspro/headless_utils.py +88 -0
  70. setiastro/saspro/histogram.py +753 -0
  71. setiastro/saspro/history_explorer.py +939 -0
  72. setiastro/saspro/image_combine.py +414 -0
  73. setiastro/saspro/image_peeker_pro.py +1596 -0
  74. setiastro/saspro/imageops/__init__.py +37 -0
  75. setiastro/saspro/imageops/mdi_snap.py +292 -0
  76. setiastro/saspro/imageops/scnr.py +36 -0
  77. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  78. setiastro/saspro/imageops/stretch.py +244 -0
  79. setiastro/saspro/isophote.py +1179 -0
  80. setiastro/saspro/layers.py +208 -0
  81. setiastro/saspro/layers_dock.py +714 -0
  82. setiastro/saspro/lazy_imports.py +193 -0
  83. setiastro/saspro/legacy/__init__.py +2 -0
  84. setiastro/saspro/legacy/image_manager.py +2226 -0
  85. setiastro/saspro/legacy/numba_utils.py +3659 -0
  86. setiastro/saspro/legacy/xisf.py +1071 -0
  87. setiastro/saspro/linear_fit.py +534 -0
  88. setiastro/saspro/live_stacking.py +1830 -0
  89. setiastro/saspro/log_bus.py +5 -0
  90. setiastro/saspro/logging_config.py +460 -0
  91. setiastro/saspro/luminancerecombine.py +309 -0
  92. setiastro/saspro/main_helpers.py +201 -0
  93. setiastro/saspro/mask_creation.py +928 -0
  94. setiastro/saspro/masks_core.py +56 -0
  95. setiastro/saspro/mdi_widgets.py +353 -0
  96. setiastro/saspro/memory_utils.py +666 -0
  97. setiastro/saspro/metadata_patcher.py +75 -0
  98. setiastro/saspro/mfdeconv.py +3826 -0
  99. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  100. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  101. setiastro/saspro/mfdeconvsport.py +2382 -0
  102. setiastro/saspro/minorbodycatalog.py +567 -0
  103. setiastro/saspro/morphology.py +382 -0
  104. setiastro/saspro/multiscale_decomp.py +1290 -0
  105. setiastro/saspro/nbtorgb_stars.py +531 -0
  106. setiastro/saspro/numba_utils.py +3044 -0
  107. setiastro/saspro/numba_warmup.py +141 -0
  108. setiastro/saspro/ops/__init__.py +9 -0
  109. setiastro/saspro/ops/command_help_dialog.py +623 -0
  110. setiastro/saspro/ops/command_runner.py +217 -0
  111. setiastro/saspro/ops/commands.py +1594 -0
  112. setiastro/saspro/ops/script_editor.py +1102 -0
  113. setiastro/saspro/ops/scripts.py +1413 -0
  114. setiastro/saspro/ops/settings.py +560 -0
  115. setiastro/saspro/parallel_utils.py +554 -0
  116. setiastro/saspro/pedestal.py +121 -0
  117. setiastro/saspro/perfect_palette_picker.py +1053 -0
  118. setiastro/saspro/pipeline.py +110 -0
  119. setiastro/saspro/pixelmath.py +1600 -0
  120. setiastro/saspro/plate_solver.py +2435 -0
  121. setiastro/saspro/project_io.py +797 -0
  122. setiastro/saspro/psf_utils.py +136 -0
  123. setiastro/saspro/psf_viewer.py +549 -0
  124. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  125. setiastro/saspro/remove_green.py +314 -0
  126. setiastro/saspro/remove_stars.py +1625 -0
  127. setiastro/saspro/remove_stars_preset.py +404 -0
  128. setiastro/saspro/resources.py +472 -0
  129. setiastro/saspro/rgb_combination.py +207 -0
  130. setiastro/saspro/rgb_extract.py +19 -0
  131. setiastro/saspro/rgbalign.py +723 -0
  132. setiastro/saspro/runtime_imports.py +7 -0
  133. setiastro/saspro/runtime_torch.py +754 -0
  134. setiastro/saspro/save_options.py +72 -0
  135. setiastro/saspro/selective_color.py +1552 -0
  136. setiastro/saspro/sfcc.py +1425 -0
  137. setiastro/saspro/shortcuts.py +2807 -0
  138. setiastro/saspro/signature_insert.py +1099 -0
  139. setiastro/saspro/stacking_suite.py +17712 -0
  140. setiastro/saspro/star_alignment.py +7420 -0
  141. setiastro/saspro/star_alignment_preset.py +329 -0
  142. setiastro/saspro/star_metrics.py +49 -0
  143. setiastro/saspro/star_spikes.py +681 -0
  144. setiastro/saspro/star_stretch.py +470 -0
  145. setiastro/saspro/stat_stretch.py +502 -0
  146. setiastro/saspro/status_log_dock.py +78 -0
  147. setiastro/saspro/subwindow.py +3267 -0
  148. setiastro/saspro/supernovaasteroidhunter.py +1712 -0
  149. setiastro/saspro/swap_manager.py +99 -0
  150. setiastro/saspro/torch_backend.py +89 -0
  151. setiastro/saspro/torch_rejection.py +434 -0
  152. setiastro/saspro/view_bundle.py +1555 -0
  153. setiastro/saspro/wavescale_hdr.py +624 -0
  154. setiastro/saspro/wavescale_hdr_preset.py +100 -0
  155. setiastro/saspro/wavescalede.py +657 -0
  156. setiastro/saspro/wavescalede_preset.py +228 -0
  157. setiastro/saspro/wcs_update.py +374 -0
  158. setiastro/saspro/whitebalance.py +456 -0
  159. setiastro/saspro/widgets/__init__.py +48 -0
  160. setiastro/saspro/widgets/common_utilities.py +305 -0
  161. setiastro/saspro/widgets/graphics_views.py +122 -0
  162. setiastro/saspro/widgets/image_utils.py +518 -0
  163. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  164. setiastro/saspro/widgets/spinboxes.py +275 -0
  165. setiastro/saspro/widgets/themed_buttons.py +13 -0
  166. setiastro/saspro/widgets/wavelet_utils.py +299 -0
  167. setiastro/saspro/window_shelf.py +185 -0
  168. setiastro/saspro/xisf.py +1123 -0
  169. setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
  170. setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
  171. setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
  172. setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
  173. setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
  174. setiastrosuitepro-1.6.0.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,567 @@
1
+ # pro/minorbodycatalog.py
2
+ from __future__ import annotations
3
+
4
+ """
5
+ Minor body (asteroid + comet) catalog helper for SASpro.
6
+
7
+ Responsibilities:
8
+ - Fetch JSON manifest from GitHub (saspro-minorbody-data repo).
9
+ - Download / update the SQLite minor body database from the release asset.
10
+ - Provide a small API for querying the DB and (optionally) computing
11
+ RA/Dec for a subset of objects via Skyfield.
12
+
13
+ This module is intentionally independent of Qt; higher-level UI / QSettings
14
+ integration should live in the main app code and call into this helper.
15
+ """
16
+
17
+ import json
18
+ import sqlite3
19
+ import urllib.request
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+ from typing import Optional, Iterable, Dict, Any, Tuple
23
+
24
+ # Skyfield imports (required for position computations)
25
+ from skyfield.api import load as sf_load
26
+ from skyfield.data import mpc as sf_mpc
27
+ from skyfield.constants import GM_SUN_Pitjeva_2005_km3_s2 as GM_SUN
28
+
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Constants
32
+ # ---------------------------------------------------------------------------
33
+
34
+ # Raw GitHub URL to the manifest in your data repo
35
+ MANIFEST_URL = (
36
+ "https://raw.githubusercontent.com/setiastro/"
37
+ "saspro-minorbody-data/main/saspro_minor_bodies_manifest.json"
38
+ )
39
+
40
+ # Default filenames (as defined by the manifest you showed)
41
+ DEFAULT_DB_BASENAME = "saspro_minor_bodies.sqlite"
42
+ DEFAULT_MANIFEST_BASENAME = "saspro_minor_bodies_manifest.json"
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Data structures
47
+ # ---------------------------------------------------------------------------
48
+
49
+ @dataclass
50
+ class MinorBodyManifest:
51
+ schema_version: int
52
+ version: str
53
+ generated_utc: str
54
+ download_url: str
55
+ download_filename: str
56
+ counts_asteroids: int
57
+ counts_comets: int
58
+ raw: Dict[str, Any]
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Network helpers (urllib only, to keep deps minimal)
63
+ # ---------------------------------------------------------------------------
64
+
65
+ def _http_get_json(url: str, timeout: float = 15.0) -> Dict[str, Any]:
66
+ """Fetch JSON from a URL using urllib."""
67
+ req = urllib.request.Request(url, headers={"User-Agent": "SetiAstroSuitePro/1.0"})
68
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
69
+ if resp.status != 200:
70
+ raise RuntimeError(f"HTTP {resp.status} retrieving {url}")
71
+ data = resp.read().decode("utf-8")
72
+ return json.loads(data)
73
+
74
+
75
+ def _http_download_binary(url: str, dest: Path, chunk_size: int = 65536, timeout: float = 30.0) -> None:
76
+ """Download a binary file from URL to dest."""
77
+ dest.parent.mkdir(parents=True, exist_ok=True)
78
+ req = urllib.request.Request(url, headers={"User-Agent": "SetiAstroSuitePro/1.0"})
79
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
80
+ if resp.status != 200:
81
+ raise RuntimeError(f"HTTP {resp.status} retrieving {url}")
82
+ tmp = dest.with_suffix(dest.suffix + ".part")
83
+ with tmp.open("wb") as f_out:
84
+ while True:
85
+ chunk = resp.read(chunk_size)
86
+ if not chunk:
87
+ break
88
+ f_out.write(chunk)
89
+ tmp.replace(dest)
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Manifest + DB management
94
+ # ---------------------------------------------------------------------------
95
+
96
+ def fetch_remote_manifest(url: str = MANIFEST_URL) -> MinorBodyManifest:
97
+ """Fetch the remote manifest from GitHub and parse it."""
98
+ data = _http_get_json(url)
99
+ # Defensive parsing: tolerate missing bits gracefully
100
+ dl = data.get("download", {})
101
+ counts = data.get("counts", {})
102
+
103
+ return MinorBodyManifest(
104
+ schema_version=int(data.get("schema_version", 1)),
105
+ version=str(data.get("version", "unknown")),
106
+ generated_utc=str(data.get("generated_utc", "")),
107
+ download_url=str(dl.get("url", "")),
108
+ download_filename=str(dl.get("filename", DEFAULT_DB_BASENAME)),
109
+ counts_asteroids=int(counts.get("asteroids", 0)),
110
+ counts_comets=int(counts.get("comets", 0)),
111
+ raw=data,
112
+ )
113
+
114
+
115
+ def load_local_manifest(path: Path) -> Optional[MinorBodyManifest]:
116
+ """Load a previously saved local manifest (if it exists)."""
117
+ if not path.is_file():
118
+ return None
119
+ try:
120
+ with path.open("r", encoding="utf-8") as f:
121
+ data = json.load(f)
122
+ dl = data.get("download", {})
123
+ counts = data.get("counts", {})
124
+ return MinorBodyManifest(
125
+ schema_version=int(data.get("schema_version", 1)),
126
+ version=str(data.get("version", "unknown")),
127
+ generated_utc=str(data.get("generated_utc", "")),
128
+ download_url=str(dl.get("url", "")),
129
+ download_filename=str(dl.get("filename", DEFAULT_DB_BASENAME)),
130
+ counts_asteroids=int(counts.get("asteroids", 0)),
131
+ counts_comets=int(counts.get("comets", 0)),
132
+ raw=data,
133
+ )
134
+ except Exception:
135
+ return None
136
+
137
+
138
+ def save_local_manifest(path: Path, manifest: MinorBodyManifest) -> None:
139
+ """Write manifest JSON to disk."""
140
+ path.parent.mkdir(parents=True, exist_ok=True)
141
+ with path.open("w", encoding="utf-8") as f:
142
+ json.dump(manifest.raw, f, indent=2)
143
+
144
+
145
+ def ensure_minor_body_db(
146
+ data_dir: Path,
147
+ manifest_url: str = MANIFEST_URL,
148
+ force_refresh: bool = False,
149
+ ) -> Tuple[Path, MinorBodyManifest]:
150
+ """
151
+ Ensure that the minor body SQLite DB exists (and is up to date).
152
+
153
+ Parameters
154
+ ----------
155
+ data_dir : Path
156
+ Directory where the DB + local manifest should live.
157
+ manifest_url : str
158
+ URL to the JSON manifest in the saspro-minorbody-data repo.
159
+ force_refresh : bool
160
+ If True, always re-download the DB even if version matches.
161
+
162
+ Returns
163
+ -------
164
+ db_path : Path
165
+ Path to the local SQLite DB.
166
+ manifest : MinorBodyManifest
167
+ Parsed manifest object for the local version.
168
+
169
+ Notes
170
+ -----
171
+ This function is network-only; any UI / progress should wrap it and
172
+ catch exceptions.
173
+ """
174
+ data_dir = data_dir.resolve()
175
+ local_manifest_path = data_dir / DEFAULT_MANIFEST_BASENAME
176
+
177
+ remote = fetch_remote_manifest(manifest_url)
178
+ db_path = data_dir / remote.download_filename
179
+
180
+ local = load_local_manifest(local_manifest_path)
181
+
182
+ needs_download = force_refresh
183
+
184
+ if not needs_download:
185
+ if local is None:
186
+ needs_download = True
187
+ elif local.version != remote.version:
188
+ needs_download = True
189
+ elif not db_path.is_file():
190
+ needs_download = True
191
+
192
+ if needs_download:
193
+ if not remote.download_url:
194
+ raise RuntimeError("Manifest does not contain a download URL for the DB.")
195
+ _http_download_binary(remote.download_url, db_path)
196
+ save_local_manifest(local_manifest_path, remote)
197
+ manifest = remote
198
+ else:
199
+ # local manifest + DB are assumed valid
200
+ manifest = local
201
+
202
+ return db_path, manifest
203
+
204
+
205
+ # ---------------------------------------------------------------------------
206
+ # Catalog class
207
+ # ---------------------------------------------------------------------------
208
+
209
+ class MinorBodyCatalog:
210
+ """
211
+ Thin wrapper around the saspro minor body SQLite DB.
212
+
213
+ Usage:
214
+ cat = MinorBodyCatalog(db_path)
215
+ print(cat.counts)
216
+ df = cat.get_bright_asteroids(H_max=20.0, limit=50000)
217
+ """
218
+
219
+ def __init__(self, db_path: Path):
220
+ self.db_path = Path(db_path).resolve()
221
+ if not self.db_path.is_file():
222
+ raise FileNotFoundError(f"Minor body DB not found: {self.db_path}")
223
+ # We keep connection lazy; open on demand
224
+ self._conn: Optional[sqlite3.Connection] = None
225
+
226
+ # ---- Connection management -------------------------------------------------
227
+
228
+ def _get_conn(self) -> sqlite3.Connection:
229
+ if self._conn is None:
230
+ # Use read-only URI mode when possible
231
+ uri = f"file:{self.db_path.as_posix()}?mode=ro"
232
+ self._conn = sqlite3.connect(uri, uri=True)
233
+ return self._conn
234
+
235
+ def close(self) -> None:
236
+ if self._conn is not None:
237
+ self._conn.close()
238
+ self._conn = None
239
+
240
+ # ---- Introspection ---------------------------------------------------------
241
+
242
+ @property
243
+ def counts(self) -> Dict[str, int]:
244
+ """Return row counts for 'asteroids' and 'comets' tables."""
245
+ conn = self._get_conn()
246
+ cur = conn.cursor()
247
+ result = {}
248
+ for table in ("asteroids", "comets"):
249
+ try:
250
+ cur.execute(f"SELECT COUNT(*) FROM {table}")
251
+ result[table] = int(cur.fetchone()[0])
252
+ except sqlite3.Error:
253
+ result[table] = 0
254
+ return result
255
+
256
+ # ---- Simple queries --------------------------------------------------------
257
+
258
+ def get_bright_asteroids(
259
+ self,
260
+ H_max: float = 20.0,
261
+ limit: Optional[int] = 100000,
262
+ ):
263
+ """
264
+ Return a pandas DataFrame of relatively bright asteroids.
265
+
266
+ We treat magnitude_H as numeric even though the column is TEXT in the
267
+ current schema, by casting it to REAL for filtering and ordering.
268
+ """
269
+ import pandas as pd
270
+
271
+ conn = self._get_conn()
272
+
273
+ # Force numeric comparison + ordering
274
+ sql = """
275
+ SELECT *
276
+ FROM asteroids
277
+ WHERE CAST(magnitude_H AS REAL) <= ?
278
+ ORDER BY CAST(magnitude_H AS REAL) ASC
279
+ """
280
+ if limit is not None:
281
+ sql += " LIMIT ?"
282
+ df = pd.read_sql_query(sql, conn, params=(H_max, limit))
283
+ else:
284
+ df = pd.read_sql_query(sql, conn, params=(H_max,))
285
+ return df
286
+
287
+ def get_bright_comets(
288
+ self,
289
+ H_max: float = 15.0,
290
+ limit: Optional[int] = 5000,
291
+ ):
292
+ """
293
+ Return a pandas DataFrame of 'bright' comets.
294
+
295
+ We auto-detect the magnitude column and cast it to REAL so that
296
+ filtering and ordering are done in brightness order.
297
+ """
298
+ import pandas as pd
299
+
300
+ conn = self._get_conn()
301
+ cur = conn.cursor()
302
+ cur.execute("PRAGMA table_info(comets)")
303
+ cols = {row[1] for row in cur.fetchall()}
304
+
305
+ mag_col = None
306
+ for candidate in ("absolute_magnitude", "magnitude_H", "H"):
307
+ if candidate in cols:
308
+ mag_col = candidate
309
+ break
310
+
311
+ if mag_col is None:
312
+ # No magnitude info, just return a limited subset
313
+ sql = "SELECT * FROM comets"
314
+ if limit is not None:
315
+ sql += " LIMIT ?"
316
+ return pd.read_sql_query(sql, conn, params=(limit,))
317
+ return pd.read_sql_query(sql, conn)
318
+
319
+ # Cast to REAL so TEXT columns behave numerically
320
+ mag_expr = f"CAST({mag_col} AS REAL)"
321
+
322
+ sql = f"""
323
+ SELECT *
324
+ FROM comets
325
+ WHERE {mag_expr} <= ?
326
+ ORDER BY {mag_expr} ASC
327
+ """
328
+ if limit is not None:
329
+ sql += " LIMIT ?"
330
+ df = pd.read_sql_query(sql, conn, params=(H_max, limit))
331
+ else:
332
+ df = pd.read_sql_query(sql, conn, params=(H_max,))
333
+ return df
334
+
335
+
336
+ def get_bright_comets(
337
+ self,
338
+ H_max: float = 15.0,
339
+ limit: Optional[int] = 5000,
340
+ ):
341
+ import pandas as pd
342
+
343
+ conn = self._get_conn()
344
+ # Try to detect magnitude column
345
+ cur = conn.cursor()
346
+ cur.execute("PRAGMA table_info(comets)")
347
+ cols = {row[1] for row in cur.fetchall()}
348
+
349
+ mag_col = None
350
+ # Prefer g, then k, then any H-style absolute mag
351
+ for candidate in ("magnitude_g", "magnitude_k", "absolute_magnitude", "magnitude_H", "H"):
352
+ if candidate in cols:
353
+ mag_col = candidate
354
+ break
355
+
356
+ if mag_col is None:
357
+ # No magnitude info we recognize – just return a limited subset
358
+ sql = "SELECT * FROM comets"
359
+ if limit is not None:
360
+ sql += " LIMIT ?"
361
+ return pd.read_sql_query(sql, conn, params=(limit,))
362
+ return pd.read_sql_query(sql, conn)
363
+
364
+ sql = f"""
365
+ SELECT *
366
+ FROM comets
367
+ WHERE {mag_col} <= ?
368
+ ORDER BY {mag_col} ASC
369
+ """
370
+ if limit is not None:
371
+ sql += " LIMIT ?"
372
+ df = pd.read_sql_query(sql, conn, params=(H_max, limit))
373
+ else:
374
+ df = pd.read_sql_query(sql, conn, params=(H_max,))
375
+ return df
376
+
377
+ def get_asteroids_by_designation(
378
+ self,
379
+ designations: Iterable[str],
380
+ ):
381
+ """
382
+ Fetch asteroid rows for one or more MPC designations.
383
+
384
+ Parameters
385
+ ----------
386
+ designations : iterable of str
387
+ MPC designations like "1 Ceres", "433 Eros", etc.
388
+
389
+ Returns
390
+ -------
391
+ pandas.DataFrame
392
+ """
393
+ import pandas as pd
394
+
395
+ designations = list(designations)
396
+ if not designations:
397
+ import pandas as pd # type: ignore
398
+ return pd.DataFrame()
399
+
400
+ conn = self._get_conn()
401
+ placeholders = ",".join("?" for _ in designations)
402
+ sql = f"""
403
+ SELECT *
404
+ FROM asteroids
405
+ WHERE designation IN ({placeholders})
406
+ """
407
+ return pd.read_sql_query(sql, conn, params=designations)
408
+
409
+ # -----------------------------------------------------------------------
410
+ # Ephemeris / position scaffold (Skyfield-based, for small subsets)
411
+ # -----------------------------------------------------------------------
412
+ def compute_positions_skyfield(
413
+ self,
414
+ asteroid_rows,
415
+ jd: float,
416
+ ephemeris_path: Optional[Path] = None,
417
+ topocentric: Optional[Tuple[float, float, float]] = None,
418
+ progress_cb=None,
419
+ debug: bool = False,
420
+ ):
421
+ """
422
+ Compute RA/Dec for a small set of asteroids at a given Julian date.
423
+
424
+ Parameters
425
+ ----------
426
+ asteroid_rows : pandas.DataFrame or list of dict-like rows
427
+ jd : float
428
+ Julian date (TT or TDB is fine, as long as you're consistent).
429
+ ephemeris_path : Path or None
430
+ topocentric : (lat_deg, lon_deg, elevation_m) or None
431
+ progress_cb : callable or None
432
+ Optional callback(done:int, total:int) -> bool.
433
+ Return False to request abort.
434
+ debug : bool
435
+
436
+ Returns
437
+ -------
438
+ list of dicts with keys:
439
+ designation, ra_deg, dec_deg, distance_au
440
+ """
441
+ if sf_load is None or sf_mpc is None or GM_SUN is None:
442
+ raise RuntimeError("Skyfield is not available; install skyfield to use this feature.")
443
+
444
+ import pandas as pd
445
+
446
+ # Normalize to DataFrame
447
+ if isinstance(asteroid_rows, pd.DataFrame):
448
+ df = asteroid_rows.copy()
449
+ else:
450
+ df = pd.DataFrame(list(asteroid_rows))
451
+
452
+ if debug:
453
+ print("[MinorBodies] DataFrame columns:", list(df.columns))
454
+
455
+ # REQUIRED NUMERIC COLUMNS (epoch_packed stays as string)
456
+ required_numeric = [
457
+ "mean_anomaly_degrees",
458
+ "argument_of_perihelion_degrees",
459
+ "longitude_of_ascending_node_degrees",
460
+ "inclination_degrees",
461
+ "eccentricity",
462
+ "mean_daily_motion_degrees",
463
+ "semimajor_axis_au",
464
+ ]
465
+
466
+ # Coerce numeric columns safely
467
+ for col in required_numeric:
468
+ if col in df.columns:
469
+ df[col] = pd.to_numeric(df[col], errors="coerce")
470
+
471
+ before = len(df)
472
+ df = df.dropna(subset=[c for c in required_numeric if c in df.columns])
473
+ if debug:
474
+ print(
475
+ f"[MinorBodies] rows after dropping NaNs in required numeric cols: "
476
+ f"{len(df)} (was {before})"
477
+ )
478
+
479
+ if df.empty:
480
+ if debug:
481
+ print("[MinorBodies] no valid rows after cleaning; aborting.")
482
+ return []
483
+
484
+ ts = sf_load.timescale()
485
+ t = ts.tt_jd(jd)
486
+
487
+ eph = None # track ephemeris so we can close it
488
+ try:
489
+ # Ephemeris
490
+ if ephemeris_path is not None:
491
+ eph = sf_load(str(ephemeris_path))
492
+ else:
493
+ # small ephemeris; OK for bundling/download
494
+ eph = sf_load("de440s.bsp")
495
+
496
+ sun = eph["sun"]
497
+ earth = eph["earth"]
498
+
499
+ # Optional observatory site
500
+ if topocentric is not None:
501
+ from skyfield.api import wgs84
502
+ lat_deg, lon_deg, elev_m = topocentric
503
+ earth = earth + wgs84.latlon(
504
+ latitude_degrees=lat_deg,
505
+ longitude_degrees=lon_deg,
506
+ elevation_m=elev_m,
507
+ )
508
+
509
+ results = []
510
+ total = len(df)
511
+ ok = 0
512
+ failed = 0
513
+
514
+ for i, (_, row) in enumerate(df.iterrows(), start=1):
515
+ # Progress callback
516
+ if progress_cb is not None:
517
+ try:
518
+ cont = progress_cb(i, total)
519
+ except Exception:
520
+ cont = True
521
+ if cont is False:
522
+ if debug:
523
+ print("[MinorBodies] progress_cb requested abort.")
524
+ break
525
+
526
+ try:
527
+ # Let Skyfield interpret the MPCORB-style dict
528
+ orb = sf_mpc.mpcorb_orbit(row, ts, GM_SUN)
529
+
530
+ # Sun-centered small body, observed from Earth
531
+ body = sun + orb
532
+ ast_at_t = earth.at(t).observe(body).apparent()
533
+ ra, dec, distance = ast_at_t.radec()
534
+
535
+ results.append(
536
+ {
537
+ "designation": row.get("designation", ""),
538
+ "ra_deg": float(ra.hours * 15.0),
539
+ "dec_deg": float(dec.degrees),
540
+ "distance_au": float(distance.au),
541
+ }
542
+ )
543
+ ok += 1
544
+ except Exception as e:
545
+ failed += 1
546
+ if debug and failed <= 10:
547
+ print(
548
+ f"[MinorBodies] mpcorb_orbit/observe FAILED for "
549
+ f"'{row.get('designation', '')}': {repr(e)}"
550
+ )
551
+
552
+ if debug:
553
+ print(
554
+ f"[MinorBodies] Skyfield positions: total={total}, ok={ok}, failed={failed}"
555
+ )
556
+
557
+ return results
558
+
559
+ finally:
560
+ # Make sure the ephemeris file handle is closed
561
+ if eph is not None:
562
+ close = getattr(eph, "close", None)
563
+ if callable(close):
564
+ try:
565
+ close()
566
+ except Exception:
567
+ pass