asteroidpipeline 0.2.1__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 (59) hide show
  1. asteroidpipeline-0.2.1/.gitignore +49 -0
  2. asteroidpipeline-0.2.1/LICENSE +21 -0
  3. asteroidpipeline-0.2.1/PKG-INFO +95 -0
  4. asteroidpipeline-0.2.1/Project.toml +53 -0
  5. asteroidpipeline-0.2.1/README.md +71 -0
  6. asteroidpipeline-0.2.1/pyproject.toml +62 -0
  7. asteroidpipeline-0.2.1/src/AsteroidPipeline.jl +62 -0
  8. asteroidpipeline-0.2.1/src/astrometric_refinement.jl +230 -0
  9. asteroidpipeline-0.2.1/src/astrometry.jl +162 -0
  10. asteroidpipeline-0.2.1/src/calibrated_photometry.jl +135 -0
  11. asteroidpipeline-0.2.1/src/campaign.jl +702 -0
  12. asteroidpipeline-0.2.1/src/crossmatch.jl +434 -0
  13. asteroidpipeline-0.2.1/src/detection.jl +208 -0
  14. asteroidpipeline-0.2.1/src/digest2.jl +119 -0
  15. asteroidpipeline-0.2.1/src/injection.jl +181 -0
  16. asteroidpipeline-0.2.1/src/known_nearby.jl +156 -0
  17. asteroidpipeline-0.2.1/src/linking.jl +241 -0
  18. asteroidpipeline-0.2.1/src/linking_multinight.jl +109 -0
  19. asteroidpipeline-0.2.1/src/mpc_export.jl +276 -0
  20. asteroidpipeline-0.2.1/src/mpcorb.jl +175 -0
  21. asteroidpipeline-0.2.1/src/neocp.jl +149 -0
  22. asteroidpipeline-0.2.1/src/observatories.jl +166 -0
  23. asteroidpipeline-0.2.1/src/pipeline.jl +626 -0
  24. asteroidpipeline-0.2.1/src/platesolve.jl +144 -0
  25. asteroidpipeline-0.2.1/src/psf.jl +202 -0
  26. asteroidpipeline-0.2.1/src/reference.jl +174 -0
  27. asteroidpipeline-0.2.1/src/rotation.jl +89 -0
  28. asteroidpipeline-0.2.1/src/stack_difference.jl +316 -0
  29. asteroidpipeline-0.2.1/src/variables.jl +298 -0
  30. asteroidpipeline-0.2.1/src/zogy.jl +205 -0
  31. asteroidpipeline-0.2.1/ui/asteroid_ui/__init__.py +3 -0
  32. asteroidpipeline-0.2.1/ui/asteroid_ui/__main__.py +3 -0
  33. asteroidpipeline-0.2.1/ui/asteroid_ui/app.py +89 -0
  34. asteroidpipeline-0.2.1/ui/asteroid_ui/backend.py +126 -0
  35. asteroidpipeline-0.2.1/ui/asteroid_ui/clipboard.py +27 -0
  36. asteroidpipeline-0.2.1/ui/asteroid_ui/config.py +58 -0
  37. asteroidpipeline-0.2.1/ui/asteroid_ui/designations.py +75 -0
  38. asteroidpipeline-0.2.1/ui/asteroid_ui/engine/__init__.py +129 -0
  39. asteroidpipeline-0.2.1/ui/asteroid_ui/engine/server.jl +291 -0
  40. asteroidpipeline-0.2.1/ui/asteroid_ui/imaging.py +181 -0
  41. asteroidpipeline-0.2.1/ui/asteroid_ui/labels.py +122 -0
  42. asteroidpipeline-0.2.1/ui/asteroid_ui/profiles.py +99 -0
  43. asteroidpipeline-0.2.1/ui/asteroid_ui/screens/__init__.py +0 -0
  44. asteroidpipeline-0.2.1/ui/asteroid_ui/screens/analysis.py +138 -0
  45. asteroidpipeline-0.2.1/ui/asteroid_ui/screens/check.py +134 -0
  46. asteroidpipeline-0.2.1/ui/asteroid_ui/screens/help.py +28 -0
  47. asteroidpipeline-0.2.1/ui/asteroid_ui/screens/home.py +164 -0
  48. asteroidpipeline-0.2.1/ui/asteroid_ui/screens/open.py +142 -0
  49. asteroidpipeline-0.2.1/ui/asteroid_ui/screens/profile.py +99 -0
  50. asteroidpipeline-0.2.1/ui/asteroid_ui/screens/reason.py +40 -0
  51. asteroidpipeline-0.2.1/ui/asteroid_ui/screens/report.py +118 -0
  52. asteroidpipeline-0.2.1/ui/asteroid_ui/screens/results.py +411 -0
  53. asteroidpipeline-0.2.1/ui/asteroid_ui/screens/settings.py +130 -0
  54. asteroidpipeline-0.2.1/ui/asteroid_ui/screens/summary.py +59 -0
  55. asteroidpipeline-0.2.1/ui/asteroid_ui/sets.py +128 -0
  56. asteroidpipeline-0.2.1/ui/asteroid_ui/styles.tcss +299 -0
  57. asteroidpipeline-0.2.1/ui/asteroid_ui/texts.py +119 -0
  58. asteroidpipeline-0.2.1/ui/asteroid_ui/theme.py +22 -0
  59. asteroidpipeline-0.2.1/ui/asteroid_ui/widgets.py +77 -0
@@ -0,0 +1,49 @@
1
+ Manifest.toml
2
+ /data/
3
+ *.fits
4
+ *.jl.cov
5
+ *.jl.*.cov
6
+ *.jl.mem
7
+ .vscode/
8
+ result
9
+ result-*
10
+ .direnv/
11
+
12
+ # docs/ — build output and DocumenterVitepress's own Node/Vitepress
13
+ # tooling. docs/src/*.md are real, permanent, committed pages, not
14
+ # generated — deliberately not listed here.
15
+ docs/build/
16
+ docs/node_modules/
17
+ docs/package-lock.json
18
+ docs/.vitepress/cache/
19
+ docs/.vitepress/dist/
20
+ docs/src/.vitepress/cache/
21
+ docs/src/.vitepress/dist/
22
+
23
+ # docs/src/assets/ — generated by docs/make_figures.jl on every build
24
+ # (local and CI), so these would just be stale duplicates if committed;
25
+ # not a blanket ignore of the whole assets/ directory in case a real,
26
+ # hand-authored logo.png/favicon.ico is ever added there.
27
+ docs/src/assets/systematic-error-floor-sweep.png
28
+ docs/src/assets/iasc-match-radius-retuning.png
29
+ docs/src/assets/pipeline-overview.png
30
+ docs/src/assets/completeness.png
31
+ docs/src/assets/set-quality.png
32
+ docs/src/assets/background-excess.png
33
+ docs/src/assets/astrometry-vs-astrometrica.png
34
+ docs/src/assets/tracklets-to-vet.png
35
+ docs/src/assets/nearby-geometry.png
36
+
37
+ # Local-only notes about the GitHub mirror's own CI/Pages plumbing — not
38
+ # part of the project (the wiki documents the pipeline, not its own
39
+ # deploy infrastructure).
40
+ PAGES_NOTES.md
41
+
42
+ # Real API keys for local test runs (e.g. ASTROMETRY_API_KEY) — see
43
+ # .env.example for the expected format.
44
+ .env
45
+
46
+ # Python packaging of the terminal app (pyproject.toml)
47
+ dist/
48
+ __pycache__/
49
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alejandro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.5
2
+ Name: asteroidpipeline
3
+ Version: 0.2.1
4
+ Summary: Find moving objects in astronomical image sets, check them, and write MPC reports — a terminal app on AsteroidPipeline.jl
5
+ Project-URL: Documentation, https://richard7987.github.io/AsteroidPipeline.jl/stable/
6
+ Project-URL: Source, https://github.com/Richard7987/AsteroidPipeline.jl
7
+ Author: Alejandro
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: FITS,MPC,asteroids,astrometry,astronomy,minor planets,terminal
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: juliapkg>=0.1.13
19
+ Requires-Dist: pillow>=10.1
20
+ Requires-Dist: pyfiglet>=1.0
21
+ Requires-Dist: textual-image>=0.13
22
+ Requires-Dist: textual>=8.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # AsteroidPipeline.jl
26
+
27
+ [![CI](https://github.com/Richard7987/AsteroidPipeline.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/Richard7987/AsteroidPipeline.jl/actions/workflows/CI.yml)
28
+ [![docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://richard7987.github.io/AsteroidPipeline.jl/stable/)
29
+ [![docs](https://img.shields.io/badge/docs-dev-blue.svg)](https://richard7987.github.io/AsteroidPipeline.jl/dev/)
30
+
31
+ Find asteroids in astronomical images. Give it a few exposures of one field
32
+ taken minutes apart — a `.zip`, a folder or FITS files, from any telescope —
33
+ and it calibrates them against Gaia, finds what moves, tells known asteroids
34
+ from new candidates, and writes the report for the Minor Planet Center.
35
+
36
+ It comes two ways:
37
+
38
+ - **`asteroidpipeline`**, a terminal app: open an image set, watch the images
39
+ animate with every object marked, confirm or reject each candidate, copy
40
+ the report. Every decision is saved, with the candidate's pixels, as
41
+ training data for a future classifier.
42
+ - **AsteroidPipeline.jl**, the Julia library that does all the analysis —
43
+ for scripts, batches and anything the app doesn't do.
44
+
45
+ **Documentation: <https://richard7987.github.io/AsteroidPipeline.jl/stable/>** (for the latest release; [dev](https://richard7987.github.io/AsteroidPipeline.jl/dev/) for `main`)
46
+
47
+ ## Install the app
48
+
49
+ On any Linux (or macOS) with Python 3.11 or newer:
50
+
51
+ ```sh
52
+ curl -fsSL https://raw.githubusercontent.com/Richard7987/AsteroidPipeline.jl/main/install.sh | sh
53
+ asteroidpipeline
54
+ ```
55
+
56
+ or with [pipx](https://pipx.pypa.io/) (or pip), from PyPI:
57
+
58
+ ```sh
59
+ pipx install asteroidpipeline
60
+ asteroidpipeline --setup # optional: install the analysis engine now
61
+ ```
62
+
63
+ The first run installs the analysis engine: Julia (downloaded if you don't
64
+ have it) and its packages, several minutes once. Images are shown at full
65
+ resolution in terminals with a graphics protocol (Kitty, WezTerm, Ghostty,
66
+ foot…), as coloured blocks elsewhere. With Nix: `nix run github:Richard7987/AsteroidPipeline.jl`.
67
+
68
+ ## Use the library
69
+
70
+ ```julia
71
+ using Pkg; Pkg.add(url="https://github.com/Richard7987/AsteroidPipeline.jl")
72
+
73
+ using AsteroidPipeline
74
+ result = analyze_set("images.zip"; observatory="F51", workdir="work")
75
+ print(result.report)
76
+ ```
77
+
78
+ Many sets at once: `julia --project=. examples/analyze_sets.jl <folder> <observatory code>`.
79
+ Start with [Getting started](https://richard7987.github.io/AsteroidPipeline.jl/dev/getting-started).
80
+
81
+ ## Repository
82
+
83
+ | Path | What |
84
+ |:--|:--|
85
+ | `src/` | the Julia library |
86
+ | `test/` | its tests (`julia --project=. -e 'using Pkg; Pkg.test()'`) |
87
+ | `docs/` | the documentation site (`julia --project=docs docs/make.jl`) |
88
+ | `examples/` | scripts for real data, batches and completeness tests |
89
+ | `ui/` | the terminal app (Python, [Textual](https://textual.textualize.io/)); see [ui/README.md](ui/README.md) |
90
+ | `pyproject.toml`, `install.sh` | packaging of the app, with the library inside |
91
+ | `flake.nix` | Nix: `nix develop` for a development shell, `nix build` for the app |
92
+
93
+ ## License
94
+
95
+ MIT. See `LICENSE`.
@@ -0,0 +1,53 @@
1
+ name = "AsteroidPipeline"
2
+ uuid = "1001636e-a0d1-496e-b23d-cef10bc3cdca"
3
+ version = "0.2.1"
4
+ authors = ["Alejandro <ale_bnes@nezzontli.xyz>"]
5
+
6
+ [deps]
7
+ CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
8
+ CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
9
+ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
10
+ Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
11
+ FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
12
+ FITSIO = "525bcba6-941b-5504-bd06-fd0dc1a4d2eb"
13
+ HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
14
+ Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59"
15
+ JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
16
+ LombScargle = "fc60dff9-86e7-5f2f-a8a0-edeadbb75bd9"
17
+ LsqFit = "2fda8390-95c7-5789-9bda-21331edee243"
18
+ Photometry = "af68cb61-81ac-52ed-8703-edc140936be4"
19
+ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
20
+ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
21
+ Reproject = "d1dcc2e6-806e-11e9-2897-3f99785db2ae"
22
+ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
23
+ TypedTables = "9d95f2ec-7b3d-5a63-8d20-e2491e220bb9"
24
+ WCS = "15f3aee2-9e10-537f-b834-a6fb8bdb944d"
25
+ p7zip_jll = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
26
+
27
+ [compat]
28
+ CSV = "0.10.16"
29
+ CodecZlib = "0.7.8"
30
+ Dates = "1.11.0"
31
+ Distributed = "1.11.0"
32
+ FFTW = "1.10.0"
33
+ FITSIO = "0.17.5"
34
+ HTTP = "2.6.4"
35
+ Interpolations = "0.16.3"
36
+ JSON = "1.7.1"
37
+ LombScargle = "1.0.3"
38
+ LsqFit = "0.16.1"
39
+ Photometry = "0.9.8"
40
+ Printf = "1.11.0"
41
+ Random = "1.11.0"
42
+ Reproject = "0.3.3"
43
+ Statistics = "1.11.1"
44
+ TypedTables = "1.4.6"
45
+ WCS = "0.6.3"
46
+ julia = "1.11"
47
+ p7zip_jll = "17.7.0"
48
+
49
+ [extras]
50
+ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
51
+
52
+ [targets]
53
+ test = ["Test"]
@@ -0,0 +1,71 @@
1
+ # AsteroidPipeline.jl
2
+
3
+ [![CI](https://github.com/Richard7987/AsteroidPipeline.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/Richard7987/AsteroidPipeline.jl/actions/workflows/CI.yml)
4
+ [![docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://richard7987.github.io/AsteroidPipeline.jl/stable/)
5
+ [![docs](https://img.shields.io/badge/docs-dev-blue.svg)](https://richard7987.github.io/AsteroidPipeline.jl/dev/)
6
+
7
+ Find asteroids in astronomical images. Give it a few exposures of one field
8
+ taken minutes apart — a `.zip`, a folder or FITS files, from any telescope —
9
+ and it calibrates them against Gaia, finds what moves, tells known asteroids
10
+ from new candidates, and writes the report for the Minor Planet Center.
11
+
12
+ It comes two ways:
13
+
14
+ - **`asteroidpipeline`**, a terminal app: open an image set, watch the images
15
+ animate with every object marked, confirm or reject each candidate, copy
16
+ the report. Every decision is saved, with the candidate's pixels, as
17
+ training data for a future classifier.
18
+ - **AsteroidPipeline.jl**, the Julia library that does all the analysis —
19
+ for scripts, batches and anything the app doesn't do.
20
+
21
+ **Documentation: <https://richard7987.github.io/AsteroidPipeline.jl/stable/>** (for the latest release; [dev](https://richard7987.github.io/AsteroidPipeline.jl/dev/) for `main`)
22
+
23
+ ## Install the app
24
+
25
+ On any Linux (or macOS) with Python 3.11 or newer:
26
+
27
+ ```sh
28
+ curl -fsSL https://raw.githubusercontent.com/Richard7987/AsteroidPipeline.jl/main/install.sh | sh
29
+ asteroidpipeline
30
+ ```
31
+
32
+ or with [pipx](https://pipx.pypa.io/) (or pip), from PyPI:
33
+
34
+ ```sh
35
+ pipx install asteroidpipeline
36
+ asteroidpipeline --setup # optional: install the analysis engine now
37
+ ```
38
+
39
+ The first run installs the analysis engine: Julia (downloaded if you don't
40
+ have it) and its packages, several minutes once. Images are shown at full
41
+ resolution in terminals with a graphics protocol (Kitty, WezTerm, Ghostty,
42
+ foot…), as coloured blocks elsewhere. With Nix: `nix run github:Richard7987/AsteroidPipeline.jl`.
43
+
44
+ ## Use the library
45
+
46
+ ```julia
47
+ using Pkg; Pkg.add(url="https://github.com/Richard7987/AsteroidPipeline.jl")
48
+
49
+ using AsteroidPipeline
50
+ result = analyze_set("images.zip"; observatory="F51", workdir="work")
51
+ print(result.report)
52
+ ```
53
+
54
+ Many sets at once: `julia --project=. examples/analyze_sets.jl <folder> <observatory code>`.
55
+ Start with [Getting started](https://richard7987.github.io/AsteroidPipeline.jl/dev/getting-started).
56
+
57
+ ## Repository
58
+
59
+ | Path | What |
60
+ |:--|:--|
61
+ | `src/` | the Julia library |
62
+ | `test/` | its tests (`julia --project=. -e 'using Pkg; Pkg.test()'`) |
63
+ | `docs/` | the documentation site (`julia --project=docs docs/make.jl`) |
64
+ | `examples/` | scripts for real data, batches and completeness tests |
65
+ | `ui/` | the terminal app (Python, [Textual](https://textual.textualize.io/)); see [ui/README.md](ui/README.md) |
66
+ | `pyproject.toml`, `install.sh` | packaging of the app, with the library inside |
67
+ | `flake.nix` | Nix: `nix develop` for a development shell, `nix build` for the app |
68
+
69
+ ## License
70
+
71
+ MIT. See `LICENSE`.
@@ -0,0 +1,62 @@
1
+ # The terminal application: one installable program with the Julia library
2
+ # inside it (the wheel bundles src/ and Project.toml as
3
+ # asteroid_ui/julia/AsteroidPipeline). The interface's own code is ui/; the
4
+ # analysis is all AsteroidPipeline.jl, set up on first run (see
5
+ # ui/asteroid_ui/engine/__init__.py).
6
+ #
7
+ # pip install asteroidpipeline (or: pipx install asteroidpipeline)
8
+ # asteroidpipeline
9
+
10
+ [build-system]
11
+ requires = ["hatchling"]
12
+ build-backend = "hatchling.build"
13
+
14
+ [project]
15
+ name = "asteroidpipeline"
16
+ dynamic = ["version"]
17
+ description = "Find moving objects in astronomical image sets, check them, and write MPC reports — a terminal app on AsteroidPipeline.jl"
18
+ readme = "README.md"
19
+ license = "MIT"
20
+ license-files = ["LICENSE"]
21
+ requires-python = ">=3.11"
22
+ authors = [{ name = "Alejandro" }]
23
+ keywords = ["astronomy", "asteroids", "minor planets", "MPC", "FITS", "astrometry", "terminal"]
24
+ classifiers = [
25
+ "Environment :: Console",
26
+ "Intended Audience :: Science/Research",
27
+ "Operating System :: POSIX :: Linux",
28
+ "Operating System :: MacOS",
29
+ "Programming Language :: Python :: 3",
30
+ "Topic :: Scientific/Engineering :: Astronomy",
31
+ ]
32
+ dependencies = [
33
+ "textual>=8.0",
34
+ "textual-image>=0.13",
35
+ "pillow>=10.1",
36
+ "pyfiglet>=1.0",
37
+ # Downloads Julia only when none is installed; see the engine module.
38
+ "juliapkg>=0.1.13",
39
+ ]
40
+
41
+ [project.urls]
42
+ Documentation = "https://richard7987.github.io/AsteroidPipeline.jl/stable/"
43
+ Source = "https://github.com/Richard7987/AsteroidPipeline.jl"
44
+
45
+ [project.scripts]
46
+ asteroidpipeline = "asteroid_ui.app:main"
47
+
48
+ [tool.hatch.version]
49
+ path = "ui/asteroid_ui/__init__.py"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["ui/asteroid_ui"]
53
+ exclude = ["**/__pycache__"]
54
+
55
+ [tool.hatch.build.targets.wheel.force-include]
56
+ "src" = "asteroid_ui/julia/AsteroidPipeline/src"
57
+ "Project.toml" = "asteroid_ui/julia/AsteroidPipeline/Project.toml"
58
+ "LICENSE" = "asteroid_ui/julia/AsteroidPipeline/LICENSE"
59
+
60
+ [tool.hatch.build.targets.sdist]
61
+ include = ["/ui/asteroid_ui", "/src", "/Project.toml", "/LICENSE", "/README.md", "/pyproject.toml"]
62
+ exclude = ["**/__pycache__"]
@@ -0,0 +1,62 @@
1
+ module AsteroidPipeline
2
+
3
+ using FITSIO
4
+ using Photometry
5
+ using LombScargle
6
+ using HTTP
7
+ using CSV
8
+ using JSON
9
+ using TypedTables: Table
10
+ using WCS
11
+ using Printf
12
+ using Reproject
13
+ using Statistics
14
+ using Interpolations
15
+ using FFTW
16
+ using LsqFit
17
+ using Distributed
18
+ using Dates: DateTime, datetime2julian
19
+ import Dates
20
+ using CodecZlib: GzipDecompressorStream
21
+ import p7zip_jll
22
+ using Random: AbstractRNG, default_rng
23
+
24
+ include("detection.jl")
25
+ include("linking.jl")
26
+ include("linking_multinight.jl")
27
+ include("variables.jl")
28
+ include("astrometry.jl")
29
+ include("platesolve.jl")
30
+ include("crossmatch.jl")
31
+ include("astrometric_refinement.jl")
32
+ include("calibrated_photometry.jl")
33
+ include("mpcorb.jl")
34
+ include("campaign.jl")
35
+ include("observatories.jl")
36
+ include("known_nearby.jl")
37
+ include("neocp.jl")
38
+ include("injection.jl")
39
+ include("reference.jl")
40
+ include("psf.jl")
41
+ include("zogy.jl")
42
+ include("stack_difference.jl")
43
+ include("pipeline.jl")
44
+ include("rotation.jl")
45
+ include("mpc_export.jl")
46
+ include("digest2.jl")
47
+
48
+ export detect_sources, fill_value_mask, link_candidates, load_wcs, pix_to_sky,
49
+ astrometric_calibrate, crossmatch_catalog, SkyBoTServiceError, run_pipeline,
50
+ build_reference, load_frame, estimate_psf, fit_moffat_psf, zogy_subtract, light_curve,
51
+ recover_rotation_period, plate_solve, search_field, find_variable_sources,
52
+ variability_chi2, photometric_scale, ades_psv, mpc80_report, julian_date_to_iso8601,
53
+ digest2_score, link_across_nights, gaia_reference_stars, refine_wcs, stack_difference,
54
+ aperture_flux, photometric_zeropoint, candidate_magnitudes, frame_epoch,
55
+ inject_movers, injection_recovery, read_mpcorb, set_quality,
56
+ extract_set, clean_blank_pixels, download_mpcorb, default_mpcorb_path, astrometrica_settings,
57
+ mpc_report_header, blank_mpc_report, mpc_report, pack_designation, check_mpc_report,
58
+ known_objects_in_field, frame_footprint, analyze_set, pipeline_config, IASC_PS1_CONFIG,
59
+ read_obscodes, download_obscodes, frame_site, observatory_candidates, tracklet_motion,
60
+ nearby_known_objects, neocp_objects, neocp_check
61
+
62
+ end # module AsteroidPipeline
@@ -0,0 +1,230 @@
1
+ """
2
+ gaia_reference_stars(ra, dec, radius_deg; epoch_jd::Real, mag_range=(12.0, 21.0)) -> Table
3
+
4
+ Gaia DR3 stars within `radius_deg` of `(ra, dec)` (degrees), with G
5
+ magnitude inside `mag_range` — a live network request, to the ESA Gaia
6
+ archive's TAP service (`gaiadr3.gaia_source`), falling back to VizieR's
7
+ copy (`I/355/gaiadr3`) if the archive fails with a network error, a
8
+ timeout or a 5xx status. Two services because one did go down mid-run:
9
+ VizieR's TAP answered 503 to every request for a sustained stretch while
10
+ this was being validated, which would otherwise have stopped every
11
+ `refine_astrometry` run cold.
12
+
13
+ Positions are propagated by proper motion from Gaia DR3's own epoch
14
+ (J2016.0) to `epoch_jd` (a Julian Date, normally the frame's own); stars
15
+ without a proper motion keep their catalogued position. A decade of proper motion is easily a
16
+ significant fraction of an arcsecond for nearby stars, which is the
17
+ whole error budget [`refine_wcs`](@ref) is trying to beat.
18
+
19
+ Returns a table with columns `ra`, `dec` (degrees, at `epoch_jd`) and
20
+ `gmag`, ready to pass to [`refine_wcs`](@ref).
21
+ """
22
+ function gaia_reference_stars(ra::Real, dec::Real, radius_deg::Real;
23
+ epoch_jd::Real, mag_range=(12.0, 21.0))
24
+ 0 < radius_deg <= 5 || throw(ArgumentError("radius_deg must be in (0, 5]"))
25
+ lo, hi = mag_range
26
+ # Both queries alias their columns to the same names, so one parser
27
+ # serves either service.
28
+ cone(racol, deccol) = "CONTAINS(POINT('ICRS', $racol, $deccol), " *
29
+ "CIRCLE('ICRS', $ra, $dec, $radius_deg)) = 1"
30
+ esa = "SELECT ra AS ra_deg, dec AS dec_deg, pmra AS pm_ra, pmdec AS pm_dec, " *
31
+ "phot_g_mean_mag AS g FROM gaiadr3.gaia_source " *
32
+ "WHERE $(cone("ra", "dec")) AND phot_g_mean_mag BETWEEN $lo AND $hi"
33
+ vizier = "SELECT RA_ICRS AS ra_deg, DE_ICRS AS dec_deg, pmRA AS pm_ra, pmDE AS pm_dec, " *
34
+ "Gmag AS g FROM \"I/355/gaiadr3\" " *
35
+ "WHERE $(cone("RA_ICRS", "DE_ICRS")) AND Gmag BETWEEN $lo AND $hi"
36
+ rows = try
37
+ _tap_query(_GAIA_ARCHIVE_TAP_URL, esa)
38
+ catch e
39
+ _is_service_failure(e) || rethrow()
40
+ @warn "Gaia archive unavailable; falling back to VizieR" exception=e
41
+ _tap_query(_VIZIER_TAP_URL, vizier)
42
+ end
43
+
44
+ years = (epoch_jd - _GAIA_DR3_EPOCH_JD) / 365.25
45
+ out_ra = Float64[]; out_dec = Float64[]; gmag = Float64[]
46
+ for r in rows
47
+ (ismissing(r.ra_deg) || ismissing(r.dec_deg)) && continue
48
+ pmra = ismissing(r.pm_ra) ? 0.0 : Float64(r.pm_ra) # mas/yr, already * cos(dec)
49
+ pmde = ismissing(r.pm_dec) ? 0.0 : Float64(r.pm_dec)
50
+ d = Float64(r.dec_deg) + pmde * years / 3.6e6
51
+ push!(out_ra, Float64(r.ra_deg) + pmra * years / 3.6e6 / cosd(d))
52
+ push!(out_dec, d)
53
+ push!(gmag, ismissing(r.g) ? NaN : Float64(r.g))
54
+ end
55
+ return Table(ra=out_ra, dec=out_dec, gmag=gmag)
56
+ end
57
+
58
+ const _GAIA_DR3_EPOCH_JD = 2457389.0 # J2016.0
59
+ const _GAIA_ARCHIVE_TAP_URL = "https://gea.esac.esa.int/tap-server/tap/sync"
60
+
61
+ """
62
+ refine_wcs(image, wcs, stars; threshold=10.0, mask=nothing, max_offset=60.0,
63
+ match_radius=2.0, min_matches=6, clip_sigma=3.0)
64
+ -> (wcs, refined, n_matches, rms_arcsec, offset_px, n_detections)
65
+
66
+ Re-derive a frame's astrometric solution from reference stars actually
67
+ detected in it, rather than trusting the header's — the same job
68
+ Astrometrica's "Data Reduction" does before any measurement is reported.
69
+
70
+ `image` is in the `(y, x)` order [`detect_sources`](@ref) takes; `wcs`
71
+ is the header's approximate solution; `stars` is a table of reference
72
+ stars with `ra`, `dec` columns (degrees, at the frame's epoch), e.g. from
73
+ [`gaia_reference_stars`](@ref). Stars are detected at `threshold` sigma
74
+ (`mask` is forwarded to `detect_sources`), and every catalog star is
75
+ projected through `wcs`. The header's error is found first as one
76
+ global pixel offset — the peak of a vote over every detection/star pair
77
+ within `max_offset` pixels — then each star is matched to the nearest
78
+ detection within `match_radius` pixels of its offset-corrected position,
79
+ and a linear tangent-plane (TAN) solution — 6 parameters: shift, scale,
80
+ rotation, shear — is fitted by least squares to those matches, rejecting
81
+ matches beyond `clip_sigma` times the residual RMS and refitting until
82
+ none are rejected.
83
+
84
+ Why this is needed rather than optional for real IASC/Pan-STARRS1 data:
85
+ on a real 2025 practice set (field `XY54_p10`), matched against 38 Gaia
86
+ stars, the header WCS was off by a near-constant ~26.6 px (6.8") — far
87
+ beyond the <1" an MPC report needs, though PS1's own `CERROR` keyword
88
+ claims 0.06". Refit this way, the residual RMS fell to ~0.17", and an
89
+ unknown object's positions agreed with Astrometrica's own
90
+ Gaia-calibrated measurement of it. The header's `PCA*` polynomial
91
+ distortion terms were checked as the explanation first and ruled out:
92
+ evaluated at the chip's corners they move a position by <0.5 px.
93
+
94
+ Returns a named tuple: the refined `wcs` (a plain TAN `WCSTransform`,
95
+ tangent point at `wcs`'s own `crval`), `refined` (`false` if fewer than
96
+ `min_matches` stars matched — `wcs` is then the input, unchanged — so a
97
+ caller can warn instead of failing outright), `n_matches`, `rms_arcsec`
98
+ (the fit's residual RMS; `NaN` if not refined), and `offset_px` (the
99
+ global `(dx, dy)` offset found, detected minus predicted), and `n_detections`
100
+ (sources detected at `threshold` — compared with the catalog's star count it
101
+ flags frames full of detector defects; see [`run_pipeline`](@ref)).
102
+ """
103
+ function refine_wcs(image::AbstractMatrix{<:Real}, wcs::WCSTransform, stars;
104
+ threshold::Real=10.0, mask::Union{Nothing,AbstractMatrix{Bool}}=nothing,
105
+ max_offset::Real=60.0, match_radius::Real=2.0,
106
+ min_matches::Integer=6, clip_sigma::Real=3.0)
107
+ unrefined(n, offset) = (wcs=wcs, refined=false, n_matches=n, rms_arcsec=NaN, offset_px=offset,
108
+ n_detections=length(detections))
109
+
110
+ detections = detect_sources(image; threshold, mask)
111
+ det_xy = [(d.xcen, d.ycen) for d in detections]
112
+ n1, n2 = size(image)
113
+
114
+ predicted = Tuple{Float64,Float64,Float64,Float64}[] # (x, y, ra, dec)
115
+ for s in stars
116
+ x, y = world_to_pix(wcs, Float64[s.ra, s.dec])
117
+ (-max_offset < x < n2 + max_offset && -max_offset < y < n1 + max_offset) || continue
118
+ push!(predicted, (x, y, Float64(s.ra), Float64(s.dec)))
119
+ end
120
+ (isempty(det_xy) || isempty(predicted)) && return unrefined(0, (NaN, NaN))
121
+
122
+ offset = _vote_offset(det_xy, predicted, max_offset)
123
+ matches = _match_stars(det_xy, predicted, offset, match_radius)
124
+ length(matches) < min_matches && return unrefined(length(matches), offset)
125
+
126
+ crval = (wcs.crval[1], wcs.crval[2])
127
+ fit = _fit_tan(matches, crval, clip_sigma)
128
+ fit.n < min_matches && return unrefined(fit.n, offset)
129
+ return (wcs=fit.wcs, refined=true, n_matches=fit.n, rms_arcsec=fit.rms_arcsec, offset_px=offset,
130
+ n_detections=length(detections))
131
+ end
132
+
133
+ """
134
+ _vote_offset(det_xy, predicted, max_offset; bin=2.0) -> (dx, dy)
135
+
136
+ The global pixel offset (detected minus predicted) most detection/star
137
+ pairs agree on: every pair within `max_offset` votes for its own offset
138
+ in `bin`-pixel cells, and the winning cell's pairs are averaged. Random
139
+ pairings spread their votes thinly over the whole `max_offset` square;
140
+ true pairings all land in one cell, so this finds the header's error
141
+ without needing it to already be small.
142
+ """
143
+ function _vote_offset(det_xy, predicted, max_offset::Real; bin::Real=2.0)
144
+ votes = Dict{Tuple{Int,Int},Vector{Tuple{Float64,Float64}}}()
145
+ for (px, py, _, _) in predicted, (dx_, dy_) in det_xy
146
+ ox, oy = dx_ - px, dy_ - py
147
+ (abs(ox) <= max_offset && abs(oy) <= max_offset) || continue
148
+ push!(get!(votes, (floor(Int, ox / bin), floor(Int, oy / bin)), Tuple{Float64,Float64}[]), (ox, oy))
149
+ end
150
+ isempty(votes) && return (0.0, 0.0)
151
+ # Count each cell together with its 8 neighbours, so a true offset
152
+ # sitting on a cell boundary isn't split across two cells.
153
+ neighbourhood(c) = (get(votes, (c[1] + a, c[2] + b), ()) for a in -1:1, b in -1:1)
154
+ best = argmax(c -> sum(length, neighbourhood(c)), collect(keys(votes)))
155
+ pairs = collect(Iterators.flatten(neighbourhood(best)))
156
+ return (median(first.(pairs)), median(last.(pairs)))
157
+ end
158
+
159
+ function _match_stars(det_xy, predicted, offset, match_radius::Real)
160
+ matches = Tuple{Float64,Float64,Float64,Float64}[] # (x, y, ra, dec)
161
+ used = Set{Int}()
162
+ for (px, py, ra, dec) in predicted
163
+ tx, ty = px + offset[1], py + offset[2]
164
+ best, best_dist = 0, Float64(match_radius)
165
+ for (i, (x, y)) in enumerate(det_xy)
166
+ d = hypot(x - tx, y - ty)
167
+ d <= best_dist && ((best, best_dist) = (i, d))
168
+ end
169
+ (best == 0 || best in used) && continue
170
+ push!(used, best)
171
+ push!(matches, (det_xy[best]..., ra, dec))
172
+ end
173
+ return matches
174
+ end
175
+
176
+ """
177
+ _gnomonic(ra, dec, crval) -> (xi, eta)
178
+
179
+ Standard (tangent-plane) coordinates of `(ra, dec)` about tangent point
180
+ `crval`, in degrees — FITS's intermediate world coordinates for a TAN
181
+ projection, so a linear fit of pixels to these *is* a TAN WCS solution.
182
+ """
183
+ function _gnomonic(ra::Real, dec::Real, crval)
184
+ a0, d0 = crval
185
+ cosc = sind(d0) * sind(dec) + cosd(d0) * cosd(dec) * cosd(ra - a0)
186
+ xi = cosd(dec) * sind(ra - a0) / cosc
187
+ eta = (cosd(d0) * sind(dec) - sind(d0) * cosd(dec) * cosd(ra - a0)) / cosc
188
+ return rad2deg(xi), rad2deg(eta)
189
+ end
190
+
191
+ function _fit_tan(matches, crval, clip_sigma::Real)
192
+ keep = trues(length(matches))
193
+ local a, b, rms
194
+ while true
195
+ m = matches[keep]
196
+ X = hcat(ones(length(m)), [p[1] for p in m], [p[2] for p in m])
197
+ std_coords = [_gnomonic(p[3], p[4], crval) for p in m]
198
+ a = X \ first.(std_coords)
199
+ b = X \ last.(std_coords)
200
+ all_X = hcat(ones(length(matches)), [p[1] for p in matches], [p[2] for p in matches])
201
+ all_std = [_gnomonic(p[3], p[4], crval) for p in matches]
202
+ resid = hypot.(all_X * a .- first.(all_std), all_X * b .- last.(all_std)) .* 3600
203
+ rms = sqrt(mean(resid[keep] .^ 2))
204
+ # Monotone: a rejected match never comes back, so each pass drops at
205
+ # least one or stops. Re-admitting them could oscillate forever on
206
+ # near-noiseless data, where residuals sit right at the threshold.
207
+ new_keep = keep .& (resid .<= max(clip_sigma * rms, 1e-3))
208
+ (new_keep == keep || count(new_keep) < 3) && break
209
+ keep = new_keep
210
+ end
211
+ cd = [a[2] a[3]; b[2] b[3]]
212
+ crpix = -(cd \ [a[1], b[1]])
213
+ wcs = WCSTransform(2; ctype=["RA---TAN", "DEC--TAN"], crval=[crval[1], crval[2]],
214
+ crpix=crpix, cdelt=[1.0, 1.0], pc=cd)
215
+ return (wcs=wcs, n=count(keep), rms_arcsec=rms)
216
+ end
217
+
218
+ """
219
+ _field_reference_stars(image, wcs, epoch_jd) -> Table
220
+
221
+ [`gaia_reference_stars`](@ref) covering all of `image`'s footprint
222
+ (through `wcs`), with a 10% margin for the header WCS's own error.
223
+ """
224
+ function _field_reference_stars(image::AbstractMatrix, wcs::WCSTransform, epoch_jd::Real)
225
+ n1, n2 = size(image)
226
+ centre = pix_to_world(wcs, Float64[n2 / 2, n1 / 2])
227
+ corners = (pix_to_world(wcs, Float64[x, y]) for (x, y) in ((1, 1), (n2, 1), (1, n1), (n2, n1)))
228
+ radius = 1.1 * maximum(_angular_distance_arcsec(centre..., c...) for c in corners) / 3600
229
+ return gaia_reference_stars(centre[1], centre[2], radius; epoch_jd)
230
+ end