Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SkyTonight

SkyTonight is the built-in observability calculator. It computes, for every target in the dataset, which objects are worth imaging tonight - ranked by AstroScore - and exposes the results through a cached JSON API.


Using SkyTonight

The SkyTonight tab shows the results of the last calculation, in sub-tabs:

Sub-tabWhat you do there
PlotA polar map of the sky with the path of every target tonight; filter by category and AstroScore, click a target to inspect it
Deep Sky ObjectsThe ranked table: sort by AstroScore, filter by type, constellation, size, magnitude or equipment fit (Advanced DSO filters); “Tonight for you” picks a few targets for your level (BEGINNER_EXPERIENCE.md)
BeginnerThe curated starter list (can be hidden in My settings)
Solar System Bodies, CometsThe Moon, planets and comets observable tonight
LogsThe log of the last calculation
DSO not found?Search any object to see whether it passes tonight’s constraints, and which one filters it out

From a row you can open the target’s altitude chart, add it to the Astrodex or to Plan My Night, open its visibility calendar and the framing simulator from their icons, and open More: catalogue names, transit time, the best equipment combinations and the Wishlist button.

The results are recomputed twice a night (Scheduler); after changing a constraint, Run SkyTonight Now in App settings -> Configuration recomputes them at once.


Advanced DSO filters

The Deep Sky Objects tab has a collapsible Advanced filters panel above the table. Unlike the text / constellation / type / catalogue / difficulty filters (which run client-side over the rows already on screen), these five run server-side in _build_dso_section_payload, applied before the max_deep_sky_rows truncation cut - so a filtered result set is a genuine catalogue-wide search, not a filter over the top N by AstroScore. They are passed as query params on GET /api/skytonight/data/dso and debounced (~400 ms) with a spinner.

ParamFilterNotes
size_min, size_maxAngular size (arcmin)A view filter inside the configured size_constraint_min/max band, which the panel shows as “within your configured N-M arcmin”.
sb_maxSurface brightness threshold (mag/arcmin^2)Keeps only targets at or brighter than the threshold (lower number = brighter).
alt_window_min_deg + alt_window_start + alt_window_endBest altitude window“above X deg between HHh and HHh”. Evaluated against the row’s hourly_altitude vector; the window may wrap past midnight.
combination_id + fov_fitFOV fitNeeds a chosen equipment combination. Per row: fov_fit: {fits, fill_pct} with fill_pct = size_arcmin / min(fov_h_arcmin, fov_v_arcmin). The guide camera is never part of FOV math; a camera-only combination uses its lens fields.
combination_id + max_integration_hEstimated minimum integration timeNeeds a chosen combination. See EXPOSURE_CALC.md.

Unknown values are never silently dropped. A target with no magnitude or size (so no surface brightness, no FOV fill, no integration estimate) stays in the result and is shown as unknown; a size / SB / integration filter only excludes a target whose value is present and fails. The response echoes advanced_filters.size_band and the resolved combination so the UI can label the result set and state how many targets were considered after filtering.


Target visibility calendar

GET /api/skytonight/visibility-calendar?target=<id_or_name>&year=<yyyy> answers “when is this object best this year?” for the user’s active location. It is computed on demand (the SkyTonight pipeline only ever covers tonight) and cached in a small bounded in-process LRU keyed (target, location, year) - deliberately not a scheduler cache job, because the key space (any target x any location) is unbounded and the underlying ephemeris changes only once a year.

Deep-sky objects only. Planets and comets move measurably over a year, so a fixed-RA calendar is meaningless for them - the response returns supported: false with reason: "moving_target". reason: "not_found" means the identifier could not be resolved to coordinates (SkyTonight dataset first, then a SIMBAD fallback for Astrodex items outside the dataset).

Sampling scheme

  • 24 sample nights: the 1st and 15th of each month across the requested year. Two samples per month keep a single unlucky Moon phase from dominating the monthly figure.
  • Per sample night, the Sun and Moon altitude grid (18:00 -> 06:00 local, 10-minute steps) is built once via the shared astroweather.moon_planner.night_body_altitude_grid helper (one Astropy AltAz transform per body).
  • The target’s own altitude/azimuth is analytic, from the hour angle H = LST - RA -> alt = asin(sin(dec)*sin(lat) + cos(dec)*cos(lat)*cos(H)), with LST from the same time grid. Arcminute accuracy at O(1) trig per sample.

Constraint gates (kept in sync with the live calculator)

A sample step counts toward observable_hours when the Sun is below -18 deg (astronomical dark) and the target sits between the effective altitude floor and altitude_constraint_max. The floor is the stricter of altitude_constraint_min, the airmass-derived altitude (asin(1 / airmass_constraint)), and the active location preset’s custom horizon_profile at the target’s azimuth (same _horizon_floor_array helper the calculator uses). moonless_observable_hours additionally requires the Moon below the horizon.

Response shape

{
  target:   { id, name, type },
  location: { id, name },
  year, supported, reason?,
  months:  [ { month, dark_hours, observable_hours, moonless_observable_hours,
               max_altitude, moon_illumination_pct, score, bucket } ],   // 12 aggregates
  samples: [ { date, dark_hours, observable_hours, moonless_observable_hours,
               max_altitude, transit_local_time, moon_illumination_pct } ], // 24 raw nights
  constraints: { altitude_min, altitude_max, has_horizon_profile }
}

score is moonless_observable_hours normalized against the best month of the year (falling back to observable_hours when the target is never moonless-observable); bucket is score mapped to 0-5 for the heatmap cell colour. The 24 raw samples drive the per-month tooltip.


Equipment combination recommendations

For any target, POST /api/skytonight/combination-recommendations rates how well each of the user’s enabled and valid equipment combinations (own + shared, see EQUIPMENT.md) suits that target, on a 1-5 scale.

Unlike AstroScore (which rates the night), this rates the gear - it runs on demand per target, independent of the scheduled calculation (Scheduler).

Scoring factors

FactorWeight (no camera)Weight (with camera)Basis
Focal length vs. ideal range50%30%Target apparent size/type -> an ideal focal-length band
Field-of-view match-30%Real sensor FOV (via calculate_fov) vs. target apparent size - only when the combination’s camera has sensor dimensions
Aperture vs. brightness35%25%Target magnitude -> ideal aperture range
Optical speed (f-ratio)15%15%Faster f-ratios favor nebulae/wide-field targets

A combination without a telescope but with lens_focal_length_mm/lens_focal_ratio set (e.g. a DSLR + lens on a star tracker) is scored the same way, using the lens specs in place of a telescope’s optics. A combination that resolves neither a telescope nor usable lens specs is omitted from the results rather than scored arbitrarily.

Disabled or invalid combinations (see EQUIPMENT.md) never appear in recommendations.


Framing simulator

What does my sensor see on this target? The framing simulator shows a real image of the sky around a deep-sky object with the sensor rectangle of an equipment combination drawn to scale. It opens from the frame icon next to a target in the Deep Sky Objects table, from a Wishlist item, from a Plan My Night entry, and as a Framing section in an Astrodex item detail.

  • Select up to three combinations to compare them on the same image. One is selected on opening: from Plan My Night, the plan’s own combination; otherwise the best fit - a good fit before a tight one, and among equals the one whose target fill is closest to 35% of the short side.
  • Rotate the sensor (position angle, from north through east) and drag the frame - or use the arrow keys - to move it off the target. The coordinates of the frame centre are shown.
  • A dashed circle marks the target’s apparent size, when the catalogue gives one.
  • Each selected combination gets a verdict from the ratio “target size / short side of the field”: below 10% small (with the target’s width in pixels), 10-60% good fit, 60-100% tight, above larger than the field. In that last case an approximate mosaic grid is given, assuming 15% overlap; only the target’s longest dimension is known, so it is an upper bound for an elongated object and is labelled as an estimate.
  • Download image saves the view as a PNG with the target name, the combinations and the image credit.

A combination appears when it is enabled and valid (own or shared), has a camera with sensor dimensions and pixel size, and a focal length - the telescope’s effective one, or the lens of a camera-only combination. Planets and comets are not supported: they move against the stars, so a fixed image of the sky does not apply (supported: false, reason: "moving_target").

Sky image

The background is a 1000 px DSS2 colour cut-out from CDS hips2fits, north up and east left, in gnomonic (TAN) projection centred on the target. A camera sensor is a gnomonic projection too, so the frame is an exact rectangle on the image whatever the field of view.

The server downloads each image once and keeps it in data/cache/framing_images/ (trimmed to 200 MiB, oldest first); the browser never calls CDS. To bound that cache, the field of view of a background is one of a fixed ladder (0.25 to 45 degrees): the page picks the smallest step that is about 2.2 times wider than the widest selected frame. The request carries the target’s catalogue coordinates and nothing about the user.

When CDS hips2fits cannot be reached, the server builds the same cut-out itself from the tiles of the DSS2 colour HiPS published by mirror sites (ESA ESASky, MAST, CSIRO, China-VO - the list CDS published during its November 2026 incident), tried in that order (object_info.DSS2_HIPS_MIRRORS). observation/hips_cutout.py maps each output pixel to the sky, finds its HEALPix NESTED pixel at the tile resolution hips2fits would use, and averages a 2x2 sub-pixel grid; the result matches hips2fits on the same field. The mirrors serve CDS’s own tiles, so the credit stays “DSS2 Color / CDS HiPS”. The same fallback serves the object thumbnails (/api/object-image/<filename>).

Each server (hips2fits and every mirror) is backed off for a few minutes after a failure, so an unreachable one costs a single timeout. When none answers, the frame is still drawn to scale on a dark background, and image cards show a neutral placeholder.

API

  • GET /api/skytonight/framing?target=<id_or_name> - the resolved target (coordinates, apparent size), the image key and allowed fields of view, and one frame per usable combination: field of view, image scale, half extent in tangent-plane units, verdict, fill ratio, target size in pixels and the mosaic estimate. default_combination_id names the best fit.
  • GET /api/skytonight/framing/image/<ra>_<dec>_<fov>.jpg - the cached background image.

Implementation: backend/observation/sky_framing.py (target resolution, image cache), backend/equipment/framing.py (sensor geometry and verdict), static/js/framing.js.


Architecture overview

Scheduler (backend/skytonight/skytonight_scheduler.py)
  └─ run_calculations()  (backend/skytonight/skytonight_calculator.py)
       ├─ load_targets_dataset()       <- DSOs (OpenNGC / OpenIC / Caldwell / Messier / Herschel400 / Pensack500 / LBN)
       ├─ load_comets_dataset()        <- MPC / JPL comets (auto-updated)
       ├─ Solar-system bodies          <- Skyfield ephemeris (de421.bsp)
       └─ Writes shared JSON to /data/skytonight/
              calculations/dso_results.json
              calculations/bodies_results.json
              calculations/comets_results.json
              calculations/skymap_data.json
              calculations/calculation_results.json   <- metadata-only "all done" signal
              outputs/<target_id>_alttime.json        <- altitude-vs-time graph data

The API (app.py) reads these JSON files; it never re-runs the heavy calculation per request.


Scheduler

ConditionSchedule
System clock is valid (NTP / correct date-time)1 hour after nautical dawn + 1 hour before nautical dusk + on first startup
System clock is invalid / unknownEvery 6 hours

Multiple Gunicorn workers are protected by a file-level lock so only one worker runs the calculator at a time.


Target catalogues

CatalogueSourceNotes
OpenNGCPyOngcNGC objects
OpenICPyOngcIC objects
Messiersubset of OpenNGCflagged via identifiers[0]
Caldwellsubset of OpenNGCextracted from PyOngc other_identifiers
Herschel 400static list in skytonight_catalogue_builder.pyAstronomical League H400 program - cross-ref tag injected on matching NGC records
Pensack 500backend/catalogues/pensack500.json502 NGC/IC objects - cross-ref tag injected on matching records
LBNbackend/catalogues/lbn.jsonLynds’ Bright Nebulae - 94 cross-refs injected on matching NGC/IC records
GaryImmbackend/catalogues/garyimm_crossrefs.json + garyimm_standalone.json206-object astrophotography list by Gary Imm - 133 cross-refs on existing records + 33 standalone objects with no NGC/IC ID (Arp groups, LBN/LDN standalone, etc.)
Arpbackend/catalogues/arp.jsonArp Atlas of Peculiar Galaxies - 120 NGC/IC cross-refs injected on matching records
Sharplessbackend/catalogues/sharpless.json313 HII emission nebulae (Sh2 catalogue, Sharpless 1959) - all standalone records
Barnardbackend/catalogues/barnard.json343 dark nebulae (Barnard 1927 catalogue) - all standalone records
vdBbackend/catalogues/vdb.json158 reflection nebulae (van den Bergh 1966 catalogue) - all standalone records
Abell PNebackend/catalogues/abell_pne.json71 large planetary nebulae (Abell 1966 catalogue) - all standalone records
Abell Clustersbackend/catalogues/abell_clusters.json2,712 rich galaxy clusters (Abell, Corwin & Olowin 1989) - all standalone records, size bypassed (null)
CometsMinor Planet Center / JPL SBDBauto-refreshed
Planets / Moon / SunSkyfield de421.bspalready computed elsewhere

Cross-ref catalogues (Herschel400, Pensack500, LBN, GaryImm, Arp) add a catalogue_names entry and a source_catalogues tag to existing NGC/IC records; they never add new coordinates or change the preferred display name. Standalone catalogues (GaryImm, Sharpless, Barnard, vdB, AbellPNe, AbellClusters) create new records for objects with no NGC/IC identifier. Objects selected by Gary Imm in the Sharpless/Barnard/vdB catalogues receive both the catalogue tag and the GaryImm tag via extra_catalogues.

Target names are preferred in this order: CommonName -> Messier -> OpenNGC -> OpenIC -> Caldwell -> LBN -> Herschel400 -> Pensack500 -> GaryImm -> Arp -> Sharpless -> Barnard -> vdB -> AbellPNe -> AbellClusters.


Observability constraints

All constraints are always active (no on/off toggle).

ParameterDefaultDescription
altitude_constraint_min30°Minimum altitude above horizon
altitude_constraint_max80°Maximum altitude (avoids zenith blind spot)
airmass_constraint2.0≈ 30°+ elevation
size_constraint_min0 arcminMinimum angular size (DSOs only); 0 = no lower limit
size_constraint_max300 arcminMaximum angular size (DSOs only)
moon_separation_min45°Minimum angular distance from Moon
moon_separation_use_illuminationtrueWhen enabled, overrides moon_separation_min with Moon illumination % -> degrees (1 % = 1°)
fraction_of_time_observable_threshold0.50DSOs must be within constraints for ≥ 50 % of the night
north_to_east_ccwfalseAzimuth convention; false = clockwise (standard)

Bodies (planets, Moon) use a relaxed minimum fraction of 0.05 (≈ 22 min) instead of the DSO threshold, because a planet visible for 3 h in a 7 h night is still a prime target.


AstroScore

AstroScore is a dimensionless number in [0, 1] that ranks targets by their suitability for astrophotography tonight from the configured location.

Normalisation helper

All sub-scores use a linear clamp:

1 - Visibility score (weight 0.40)

Measures how well-placed the target is in the sky:

InputDescription
alt_maxPeak altitude reached during the night (degrees)
obs_hoursTotal hours within all constraints
alt_meridianAltitude at meridian transit (degrees)

2 - Sky quality score (weight 0.25)

Penalises Moon interference:

InputDescription
moon_phaseIlluminated fraction of Moon disk, 0 (new) - 1 (full)
angular_dist_moonAngular separation between target and Moon (degrees); defaults to 180° when unavailable

3 - Object score (weight 0.25)

Rewards intrinsically bright, high-contrast targets using surface brightness:

Inverting the normalisation means a low SB value (brighter, easier to image) -> high score.

RangeMeaning
SB ≤ 12Very bright extended object -> score = 1.0
SB = 17Mid-range -> score ≈ 0.5
SB ≥ 22Very faint/diffuse -> score = 0.0

When magnitude or size data are unavailable, a neutral value of 0.5 is used.

4 - Comfort score (weight 0.10)

Rewards targets that are observable during convenient evening hours:

obs_hours_in_window is the subset of observable hours that fall within the prime-time window, not the total observable hours.

5 - Final weighted sum

6 - Bonuses

Applied after the weighted sum, before final clamping:

ConditionBonus
Planet at opposition (is_planet=True AND is_opposition=True)+0.20
Object in Messier catalogue+0.05

The final value is clamped to [0.0, 1.0] and rounded to 4 decimal places.

Score interpretation

AstroScoreInterpretation
0.85 - 1.00Exceptional - ideal conditions
0.65 - 0.84Good - recommended target
0.45 - 0.64Average - worth imaging if nothing better
< 0.45Poor - significant limitation (moon, low altitude, faint object)

Difficulty rating

Alongside AstroScore, every target also gets a static difficulty_score (0-100) and difficulty label (beginner / intermediate / advanced), computed once per calculation run from magnitude, angular size, and surface brightness. Unlike AstroScore, difficulty does not depend on location, date, or Bortle class - the same object is always the same difficulty everywhere. It powers the SkyTonight difficulty filter/badges, the “Tonight for you” recommendations panel, and the Beginner Catalog. Full model and thresholds: BEGINNER_EXPERIENCE.md.


Light Pollution Integration

AstroScore includes an optional light pollution penalty based on your site’s Bortle class or a direct SQM (Sky Quality Meter) reading.

Configuration

Set your site’s Bortle class in App settings -> Locations -> (edit a preset) -> Sky quality:

  • Bortle Class (dropdown 1-9): choose the class that matches your observing site. You can look up your location on lightpollutionmap.info using the World Atlas layer.
  • SQM (optional, mag/arcsec²): enter if you have a real SQM-meter reading. This overrides the Bortle midpoint estimate and gives more precise weighting.

Leaving both fields empty disables the feature entirely - AstroScore behaves exactly as before.

How it affects AstroScore

The LP factor is applied to the sky score component (weight 0.25) of AstroScore, which already accounts for moon phase and angular distance. The combined factor degrades the sky score in proportion to your light pollution level.

Different object types respond differently to light pollution:

Object typeLP sensitivityNotes
Galaxy1.0 (full)Very sensitive to sky gradient
Nebula0.85Broadband is affected
Cluster0.50Moderately affected
Comet0.70Moderately affected
Planet0.05Effectively immune
Moon0.00Completely immune

At a Bortle 5 (suburban) site, a galaxy loses roughly 20-25 % of its sky score compared to a Bortle 2 site, while planets are unaffected.

API

GET /api/skyquality returns the current sky quality configuration:

{
  "bortle": 5,
  "sqm": 20.55,
  "sqm_source": "bortle_midpoint",
  "light_pollution_factor": 0.5196,
  "description": "Suburban"
}

sqm_source is one of "user_measured", "bortle_midpoint", or "not_configured".


  • Barnard dark nebulae: Barnard (1927), VizieR VII/220A
  • van den Bergh reflection nebulae: van den Bergh (1966), VizieR VII/21
  • Abell planetary nebulae: Abell (1966), ApJS 12, 391; coordinates from SIMBAD (CDS, Strasbourg)
  • Abell galaxy clusters: Abell, Corwin & Olowin (1989), ApJS 70, 1; VizieR VII/110A
  • Minor Planet Center comet elements: minorplanetcenter.net
  • JPL Small-Body Database: NASA JPL
  • Skyfield ephemeris: DE421 / DE430 via Skyfield
  • Astropy / Astroplan: coordinate transforms, moon illumination

Output files

All paths are relative to /data/skytonight/.

FileDescription
calculations/dso_results.jsonDeep-sky objects passing all constraints, sorted by AstroScore desc. Each entry also carries surface_brightness (mean mag/arcmin^2 from integrated magnitude + catalogue size, or null) and hourly_altitude (a compact [{h, alt}] vector, altitude linearly interpolated to the top of each local hour spanned by the night grid) - both feed the v1.4 advanced DSO filters without re-reading per-target alttime files
calculations/bodies_results.jsonSolar-system bodies (planets, Moon, etc.)
calculations/comets_results.jsonComets from MPC
calculations/skymap_data.jsonSky map trajectory data (az/alt arrays per visible target)
calculations/calculation_results.jsonMetadata-only file written last; signals that all calculation files are complete
outputs/<id>_alttime.jsonAltitude-vs-time series for the popup chart (15-min resolution)
runtime/scheduler_status.jsonScheduler run state, progress, last duration
logs/last_calculation.logLast run log lines (JSONL format: one entry per phase)

API endpoints

MethodPathDescription
GET/api/skytonight/scheduler/statusScheduler status, progress, last run time
POST/api/skytonight/scheduler/triggerManually trigger a recalculation (admin)
GET/api/skytonight/dataset/statusDataset build status and catalogue counts
POST/api/skytonight/dataset/rebuildForce a dataset rebuild
GET/api/skytonight/data/dsoDeep-sky results for the UI (reactive, per-section) - optional ?catalogue= filter, plus the advanced filters
GET/api/skytonight/data/bodiesSolar-system body results (reactive, per-section)
GET/api/skytonight/data/cometsComet results (reactive, per-section)
POST/api/skytonight/combination-recommendationsRate the user’s equipment combinations (1-5) for one target
GET/api/skytonight/visibility-calendar12-month visibility calendar for one fixed deep-sky target - see Target visibility calendar
GET/api/skytonight/framingFraming simulator data for one fixed deep-sky target - see Framing simulator
GET/api/skytonight/framing/image/<filename>Cached DSS2 background image of the framing simulator
GET/api/skytonight/alttime/<id>Altitude-time data for one target
GET/api/skytonight/skymapSky map trajectory data
GET/api/skytonight/logLast calculation log content (JSONL)

Full parameter details: API_ENDPOINTS.md


Source references

Last updated 2026-11-13