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.
- setiastro/__init__.py +2 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +784 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +2 -0
- setiastro/saspro/abe.py +1295 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +694 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +621 -0
- setiastro/saspro/astrobin_exporter.py +1007 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1839 -0
- setiastro/saspro/autostretch.py +196 -0
- setiastro/saspro/backgroundneutral.py +560 -0
- setiastro/saspro/batch_convert.py +325 -0
- setiastro/saspro/batch_renamer.py +519 -0
- setiastro/saspro/blemish_blaster.py +488 -0
- setiastro/saspro/blink_comparator_pro.py +2923 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +168 -0
- setiastro/saspro/clahe.py +342 -0
- setiastro/saspro/comet_stacking.py +1377 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1397 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +187 -0
- setiastro/saspro/cosmicclarity.py +1564 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +948 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2544 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +670 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2634 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +744 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1343 -0
- setiastro/saspro/function_bundle.py +1594 -0
- setiastro/saspro/ghs_dialog_pro.py +660 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +634 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8494 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
- setiastro/saspro/gui/mixins/file_mixin.py +445 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/halobgon.py +462 -0
- setiastro/saspro/header_viewer.py +445 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +753 -0
- setiastro/saspro/history_explorer.py +939 -0
- setiastro/saspro/image_combine.py +414 -0
- setiastro/saspro/image_peeker_pro.py +1596 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +244 -0
- setiastro/saspro/isophote.py +1179 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3659 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +534 -0
- setiastro/saspro/live_stacking.py +1830 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +928 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3826 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +382 -0
- setiastro/saspro/multiscale_decomp.py +1290 -0
- setiastro/saspro/nbtorgb_stars.py +531 -0
- setiastro/saspro/numba_utils.py +3044 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1102 -0
- setiastro/saspro/ops/scripts.py +1413 -0
- setiastro/saspro/ops/settings.py +560 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1053 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1600 -0
- setiastro/saspro/plate_solver.py +2435 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +314 -0
- setiastro/saspro/remove_stars.py +1625 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +472 -0
- setiastro/saspro/rgb_combination.py +207 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +72 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1425 -0
- setiastro/saspro/shortcuts.py +2807 -0
- setiastro/saspro/signature_insert.py +1099 -0
- setiastro/saspro/stacking_suite.py +17712 -0
- setiastro/saspro/star_alignment.py +7420 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +681 -0
- setiastro/saspro/star_stretch.py +470 -0
- setiastro/saspro/stat_stretch.py +502 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3267 -0
- setiastro/saspro/supernovaasteroidhunter.py +1712 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/view_bundle.py +1555 -0
- setiastro/saspro/wavescale_hdr.py +624 -0
- setiastro/saspro/wavescale_hdr_preset.py +100 -0
- setiastro/saspro/wavescalede.py +657 -0
- setiastro/saspro/wavescalede_preset.py +228 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +456 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +305 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +299 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
- setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
- setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
- 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
|