Skip to main content

Greedy observing-calendar scheduler for radial-velocity follow-up campaigns

Project description

rvcadence

Greedy observing-calendar scheduler for radial-velocity follow-up campaigns.

Given a number of observations to schedule, one or more planets' orbital periods (and, optionally, the star's rotation period), and a set of visibility windows, rvcadence picks the calendar dates that best cover all phase cycles while keeping observations temporally spread out. Optionally excludes nights where the Moon is too close to the target for high-resolution spectroscopy.

Algorithm walkthrough

Full-quality version: examples/explainer/CadenceGreedyExplainer.mp4

Install

pip install rvcadence          # core scheduler, zero dependencies
pip install rvcadence[astro]   # + name/site resolution, visibility, lunar pollution avoidance (astropy)

Quickstart

from datetime import date
from rvcadence import plan_calendar

result = plan_calendar(
    n_obs=20,
    periods_d=9.53,
    season_start=date(2026, 5, 1),
    season_end=date(2027, 4, 30),
    rotation_period_d=12.45,
    windows="2026-05-01 to 2026-05-19; 2026-07-29 to 2027-04-30",
)
print(result.dates)
print(f"median gap: {result.median_gap_d} d, mean gap: {result.mean_gap_d} d")

Or let rvcadence compute visibility itself, from a target name and site, instead of hand-typing windows (requires pip install rvcadence[astro]):

result = plan_calendar(
    n_obs=20,
    periods_d=9.53,
    season_start=date(2026, 5, 1),
    season_end=date(2027, 4, 30),
    target_coord="K2-182",
    observer_location="Paranal Observatory",
    min_altitude_deg=30.0,
)

For a multi-planet system, pass a list of periods — coverage is optimized for the worst-covered planet at each step, not an average:

result = plan_calendar(
    n_obs=20,
    periods_d=[9.53, 21.7],   # two known/candidate planets
    season_start=date(2026, 5, 1),
    season_end=date(2027, 4, 30),
)

Composable constraints

windows text, lunar avoidance, and astropy-computed visibility are independent, composable constraints — any subset may be supplied, and supplied constraints intersect:

  • windows: manually-specified date ranges (string or pre-parsed list).
  • min_moon_sep_deg (needs target_coord + observer_location): excludes nights where the Moon is within this separation of the target, evaluated at local solar midnight. Default 30°, a widely-used rule-of-thumb avoidance radius against lunar scattered-light contamination in high-resolution spectroscopy; pass None to disable even when a target is given. Nights are sunset-labeled (the night of date D runs from sunset on D to sunrise on D+1).
  • min_altitude_deg (needs target_coord + observer_location): excludes nights where the target doesn't clear this altitude, AND the Sun isn't below twilight_sun_alt_deg (default -18°, astronomical twilight), both evaluated at the target's transit (culmination) time that night — not local midnight. This deliberately differs from the moon check: for the Moon, midnight-vs-transit is negligible, but for target altitude it would be a real correctness bug (a target transiting at 9pm could be wrongly excluded by a midnight-only check on a night it's perfectly observable). Off by default (None) — there is no universally correct altitude.

target_coord/observer_location accept either already-resolved astropy objects (SkyCoord/EarthLocation) or plain name strings — a string is resolved once per call (CDS Sesame / astropy's site registry) and the resolved value is printed for confirmation.

import astropy.units as u
from astropy.coordinates import EarthLocation, SkyCoord

paranal = EarthLocation(lat=-24.6272 * u.deg, lon=-70.4039 * u.deg, height=2635 * u.m)
target = SkyCoord(ra=123.45 * u.deg, dec=-12.3 * u.deg)

result = plan_calendar(
    n_obs=20,
    periods_d=9.53,
    season_start=date(2026, 5, 1),
    season_end=date(2027, 4, 30),
    target_coord=target,
    observer_location=paranal,
    min_moon_sep_deg=30.0,
    min_altitude_deg=30.0,
)

How it works

Starting from the first and last available dates, the algorithm repeatedly adds the candidate date that best fills gaps in planet-orbital-phase coverage (and stellar-rotation-phase coverage, if known), weighted against how far it is in time from already-selected dates. For multiple planets, phase coverage is the worst-case across all periods — a candidate only scores well if it improves coverage for whichever planet is currently least-covered, so one well-phased planet can't mask a poorly-phased one:

score = 0.55 · d_planet_phase + 0.30 · d_rotation_phase + 0.15 · d_time_spread   (rotation period known)
score = 0.80 · d_planet_phase + 0.20 · d_time_spread                             (rotation period unknown)

See examples/explainer/ for the full animated walkthrough (manim source + rendered mp4) — the example imports its scoring logic directly from this package (see tests/test_explainer_example.py), so it can't drift out of sync with the real algorithm.

Development

git clone <repo-url>
cd rvcadence
pip install -e ".[dev,astro]"
pytest

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rvcadence-0.2.0.tar.gz (3.4 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rvcadence-0.2.0-py3-none-any.whl (12.8 kB view details)

Uploaded Python 3

File details

Details for the file rvcadence-0.2.0.tar.gz.

File metadata

  • Download URL: rvcadence-0.2.0.tar.gz
  • Upload date:
  • Size: 3.4 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for rvcadence-0.2.0.tar.gz
Algorithm Hash digest
SHA256 6d72d20083c5ec34358a4ef8e0111b614b84f5fb9b813f6dfbf4eda6fc50068d
MD5 03ff4afcabc6e27fbef06693fde39c17
BLAKE2b-256 c7797fbe455f9bc501d064c5729fade4d6b83f757f2abb4e6c2598649ac80775

See more details on using hashes here.

Provenance

The following attestation bundles were made for rvcadence-0.2.0.tar.gz:

Publisher: publish.yml on pierrotlamontagne/rvcadence

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rvcadence-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: rvcadence-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 12.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for rvcadence-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f5ca527d5bde4dbf31c297f7081f522a6ab1ae8584b2e1ba2e78924ee3373747
MD5 c91fba27b811c744ac17e14bd5a71fc7
BLAKE2b-256 ab44f5dd5a6276e30bf7ed51804ca8c59061294bbbb4ad691c3d50b34b569aba

See more details on using hashes here.

Provenance

The following attestation bundles were made for rvcadence-0.2.0-py3-none-any.whl:

Publisher: publish.yml on pierrotlamontagne/rvcadence

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page