Metadata-Version: 2.4
Name: astro-resolver
Version: 0.4.2
Summary: Astrophotography toolkit: object name resolution, FITS subframe quality analysis.
Author-email: Daniel Grundhöfer <daniel.grundhoefer@mailbox.org>
License-Expression: MIT
Keywords: astronomy,astrophotography,simbad,ngc,ic,messier,caldwell,sharpless,barnard,siril,fits
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Astronomy
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: astropy>=6.1
Requires-Dist: astroquery>=0.4.7
Requires-Dist: numpy>=1.26
Provides-Extra: quality
Requires-Dist: photutils>=1.9; extra == "quality"
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Dynamic: license-file

# astro-resolver

Astrophotography toolkit for FITS workflows and imaging pipelines.
Two independent packages, one library:

| Package | Purpose |
|---------|---------|
| `astro_resolver` | Turns messy `OBJECT` headers into clean folder names |
| `astro_quality` | Analyses FITS subframes for stacking quality |

---

## Installation

```bash
pip install astro-resolver

# With star detection / FWHM support (recommended):
pip install "astro-resolver[quality]"
```

---

## astro_resolver — Object name resolution

Resolves FITS `OBJECT` header values like `"M 81"`, `"sh2-101"` or
`"barnard 33"` into filesystem-safe folder names such as
`"M81_BodesGalaxy"` or `"B33_HorseheadNebula"`.

### Features

- **Local-first** — SIMBAD only as a last resort
- Solar System, Caldwell, custom aliases, OpenNGC (~14k objects),
  Sharpless and Barnard catalogs bundled offline
- Stellar catalog IDs (HD, HIP, SAO, TYC, …) skip the SIMBAD name
  round-trip and go straight to coordinate lookup
- Coordinate fallback via SIMBAD when name lookup fails
- Output is always filesystem-safe (no spaces, special chars)

### Resolution priority

1. Solar System (local)
2. Caldwell (local)
3. Custom aliases (local)
4. OpenNGC offline (~14k NGC/IC objects)
5. Sharpless catalog (Sh 2-xxx HII regions)
6. Barnard catalog (dark nebulae)
7. SIMBAD by name (online)
8. SIMBAD by coordinates (online)
9. Sanitized header string as fallback

### Usage

```python
from astro_resolver import resolve_object_name

resolve_object_name("M 81")           # → "M81_BodesGalaxy"
resolve_object_name("sh2-101")        # → "Sh2-101_TulipNebula"
resolve_object_name("barnard 33")     # → "B33_HorseheadNebula"

# Stellar catalog ID with coordinates → SIMBAD coord lookup
resolve_object_name("HD 1", ra="00 05 09", dec="+67 50 24")
```

The resolver accepts an optional `log` callable so it can be plugged into
Siril scripts (`log=siril.log`) or any other host environment.

---

## astro_quality — Subframe quality analysis

Analyses FITS light frames for stacking quality. Supports any camera
(DSLR, ZWO ASI, QHY, DWARF, …) — Bayer RAW and mono alike.
Calibration frames (Flats, Darks, Bias) are detected and skipped
automatically via filename patterns and the `IMAGETYP` FITS header.

### Metrics per subframe

| Metric | Description | Direction |
|--------|-------------|-----------|
| `background_noise` | Sky background σ (ADU, sigma-clipped) | lower = better |
| `snr_proxy` | Signal/noise ratio proxy | higher = better |
| `gradient` | Spatial background variation (light pollution gradient) | lower = better |
| `saturation_pct` | Fraction of saturated pixels (%) | lower = better |
| `star_count` | Number of detected stars (transparency proxy) | higher = better |
| `fwhm_median` | Median star width in luminance pixels (focus/seeing) | lower = better |
| `eccentricity` | Median star roundness 0=round 1=line (tracking quality) | lower = better |

Star count, FWHM and eccentricity require `photutils` (`pip install "astrolib[quality]"`).

### Scoring

Each subframe receives a relative **quality score 0–100** within its session.
Scores are *relative*: a score of 90 in a poor night may be worse in absolute
terms than 70 in an excellent night. Use raw metric values for cross-session
comparison.

| Grade | Score | Meaning |
|-------|-------|---------|
| A | 85–100 | Excellent |
| B | 70–84  | Good |
| C | 50–69  | Average |
| D | 30–49  | Poor |
| F | 0–29   | Reject |

### Usage

**Analyse a single frame:**

```python
from pathlib import Path
from astro_quality import analyze_fits, score_sub, grade, compute_session_stats

result = analyze_fits(Path("IC443_0042.fits"))
if not result["error"]:
    m = result["metrics"]
    print(f"Noise: {m['background_noise']:.1f} ADU")
    print(f"SNR:   {m['snr_proxy']:.1f}")
    print(f"Stars: {m['star_count']}")   # -1 if photutils not installed
```

**Score a session (relative quality):**

```python
from astro_quality import analyze_fits, compute_session_stats, score_sub, grade

results = [analyze_fits(f) for f in session_fits_files]
stats   = compute_session_stats(results)

for r in results:
    s = score_sub(r["metrics"], stats)
    print(f"{grade(s)} ({s:.1f})  {r['filename']}")
```

**Scan an archive and build a quality report:**

```python
from pathlib import Path
from astro_quality import build_report_data
from astro_quality.archive import iter_sessions, iter_fits
from astro_quality.cache import load_cache, save_cache

ARCHIVE = Path("z:/AstroArchiv/Objects")
CACHE   = Path("quality_cache.json")

cache = load_cache(CACHE)

for obj, sess, sess_path in iter_sessions(ARCHIVE):
    for f in iter_fits(sess_path):          # skips Flats/Darks automatically
        if str(f) not in cache:
            result = analyze_fits(f)
            result["object"]  = obj
            result["session"] = sess
            cache[str(f)] = result

save_cache(cache, CACHE)

report = build_report_data(cache, obj_filter="IC443")
for sess, data in report["IC443_GemA"].items():
    print(f"{sess}: {data['score_mean']:.1f} ({data['grade']})  "
          f"noise={data['stats']['noise_med']:.1f}")
```

**Calibration frame detection:**

```python
from astro_quality.archive import is_calib_frame
from pathlib import Path

is_calib_frame(Path("Flat_30s_ISO800_0001.fit"))   # True
is_calib_frame(Path("Light_M51_120s_0001.fit"))    # False
```

### Archive layout expected by `iter_sessions` / `iter_fits`

```
<root>/
├── IC443_GemA/
│   ├── 2026-01-19_DWARF_3/
│   │   ├── IC 443_60s60_..._0001.fits   ← light frames
│   │   └── Flat_200ms_..._0001.fits     ← skipped automatically
│   └── 2026-04-01_DWARF_3/
│       └── ...
└── M51_WhirlpoolGalaxy/
    ├── 2026-04-25_ZWO_ASI585MC_Pro/
    │   └── Light_M51_120s_..._0001.fit
    └── presets/                          ← skipped (non-session dir)
```

Non-session subdirectories (`presets`, `flats`, `calibration`, `masters`, …)
are skipped automatically.

---

## Data sources

- [OpenNGC](https://github.com/mattiaverga/OpenNGC) — CC BY-SA 4.0
- [SIMBAD](https://simbad.u-strasbg.fr/simbad/) via `astroquery`

## License

MIT
