diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22bf537..c4da63e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,6 +66,69 @@ jobs: run: | ./.venv/bin/pytest -q --maxfail=1 --disable-warnings -ra + docs-snippets: + name: Run documentation Python snippets + runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Setup Rust (stable) + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: clippy,rustfmt + + - name: Cache Rust build + uses: Swatinem/rust-cache@v2 + with: + workspaces: | + . + cache-on-failure: true + + - name: Create virtualenv (.venv) + run: | + python -m venv .venv + ./.venv/bin/python -V + + - name: Upgrade pip & install build tools + run: | + ./.venv/bin/python -m pip install --upgrade pip wheel setuptools + ./.venv/bin/pip install maturin + + - name: Build and develop-install py_outfit + run: | + ./.venv/bin/maturin develop --release + + - name: Show installed package info + run: | + ./.venv/bin/python -c "import sys; print('Python:', sys.version)" + ./.venv/bin/python -c "import py_outfit as m; print('py_outfit:', getattr(m,'__version__','unknown')); print('from:', m.__file__)" + + - name: Run documentation snippets + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + SNIPPETS=(docs/tutorials/tutorial_snippets/*.py) + echo "Found ${#SNIPPETS[@]} snippet(s)." + for f in "${SNIPPETS[@]}"; do + # Skip cached/compiled files just in case (pattern already excludes __pycache__) + case "$f" in + *__pycache__*) continue;; + esac + echo "=== Running $f ===" + ./.venv/bin/python "$f" + done + lint: name: Rust lint (clippy & fmt) runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 18956ed..c8ba768 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,24 @@ The format is based on Keep a Changelog and this repository follows semantic ver - `trajectories.rs` — Trajectory batch readers and IOD orchestration helpers. - `orbit_type/keplerian.rs`, `equinoctial.rs`, `cometary.rs` — Orbital element representations and conversions. - `lib.rs` — crate entrypoint and Python binding exports. +- Pandas integration: `DataFrame.outfit` accessor in `py_outfit.pandas_pyoutfit` with + `estimate_orbits(...)` and a `Schema` helper for column remapping. Supports both + degrees+arcseconds and radians workflows, and uses a single `Observer` for the set. +- Observatory-aware helpers and estimation methods: + - Trajectory ingestion builders accept an `Observer` (single-station workflow) and + `TrajectorySet.estimate_all_orbits(...)` performs batch IOD using that site. + - Single-object `Observations.estimate_best_orbit(env, params, seed=...)` mirrors the + batch path for a single trajectory. + - Display helpers resolving observatory names via the environment: + `show_with_env`, `table_wide_with_env`, `table_iso_with_env`. +- User documentation: + - New tutorial “IOD from trajectories” (loading from MPC/ADES, NumPy arrays, batch/single IOD). + - New tutorial “Working with orbit results” (inspect `GaussResult`, extract/convert element sets, export). + - New tutorial “Using pandas with pyOutfit” (vectorized IOD from DataFrames via the accessor). + Each tutorial’s code blocks were externalized into standalone, runnable Python snippets under + `docs/tutorials/tutorial_snippets/` and included via the snippets macro. +- CI: Added a job “Run documentation Python snippets” that builds the extension with `maturin + develop` and executes every `docs/tutorials/tutorial_snippets/*.py` to keep examples up to date. ### Python bindings - Stub type hinting files (`.pyi`) and `py.typed` are included to provide static typing / IDE support: diff --git a/Cargo.lock b/Cargo.lock index ea41659..4dc97c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -298,18 +298,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "console" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "windows-sys 0.61.0", -] - [[package]] name = "const-random" version = "0.1.18" @@ -405,12 +393,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "equivalent" version = "1.0.2" @@ -869,18 +851,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "indicatif" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" -dependencies = [ - "console", - "portable-atomic", - "unit-prefix", - "web-time", -] - [[package]] name = "indoc" version = "2.0.6" @@ -1335,7 +1305,6 @@ dependencies = [ "comfy-table", "directories", "hifitime", - "indicatif", "itertools", "nalgebra", "nom", @@ -2306,12 +2275,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" -[[package]] -name = "unit-prefix" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" - [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 1be1dd6..dbc84c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,6 @@ crate-type = ["cdylib"] [dependencies] outfit = { version = "2.1.0", features = [ "jpl-download", - "progress", "parallel", ] } numpy = { version = "0.26.0", default-features = false } diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..fd41c25 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,54 @@ +# API Reference + +This section documents the public Python API exposed by pyOutfit. It describes the main classes, their responsibilities, configuration options, data representations, and how results are structured. The focus is on clarity and practical use within astrometric and orbit-determination workflows. + +## What you will find here + +- A guided description of the core classes that make up the user-facing API. +- The configuration surface for Initial Orbit Determination (IOD) and related numerical and physical filters. +- The data containers used to ingest, store, and iterate over observations and trajectory batches. +- The different orbital element families and how to interpret their fields and reference epochs. +- The observer model and ephemeris context needed by the solvers. +- Notes on performance, parallel execution, determinism, and error handling. + +## Package layout (Python names) + +- Environment and context: `PyOutfit` (ephemerides, error model, observatory registry). +- Observers: `Observer` (MPC-coded or custom definitions, geodetic parameters). +- IOD configuration: `IODParams` and its builder for numerical tolerances and execution mode. +- Observations and batches: `Observations` (per-trajectory), `TrajectorySet` (ID → observations mapping). +- IOD results: `GaussResult` (preliminary/corrected solution access, element extraction). +- Orbital elements: `KeplerianElements`, `EquinoctialElements`, `CometaryElements`. +- Pandas helpers: optional utilities for tabular ingestion and export. + +## Conventions and units + +- Angles are expressed in radians internally. Degree ingestion is supported and converted on input. +- Times are Modified Julian Date (MJD), typically in the TT scale consistent with the ephemerides. +- Distances follow conventions of the underlying Rust core: astronomical units for orbital scales and kilometers for observer elevation. +- All public classes are typed; the package ships with type stubs and a `py.typed` marker for static analysis. +- Errors from the Rust core surface as Python `RuntimeError` with descriptive messages; error variants are flattened for batch results. + +## Parallelism and determinism + +- Parallel execution is opt-in and controlled through `IODParams`; it is designed for large batches. +- Deterministic runs can be achieved by providing a seed where supported (e.g., batch estimation pathways). +- Heavy numerical work executes outside the Python GIL, minimizing interpreter overhead. + +## Navigation + +- Python package overview: [py_outfit](py_outfit.md) +- Observers and observatory registry: [observer](observer.md) +- IOD configuration parameters: [iod_params](iod_params.md) +- Gauss Initial Orbit Determination: [iod_gauss](iod_gauss.md) +- Observations container: [observations](observations.md) +- Trajectories and batch processing: [trajectories](trajectories.md) +- Orbital element families: + - [Keplerian](orbit_type/keplerian.md) + - [Equinoctial](orbit_type/equinoctial.md) + - [Cometary](orbit_type/cometary.md) +- Pandas integration notes: [pandas_pyoutfit](pandas_pyoutfit.md) + +## Stability and status + +The API is designed to be practical and predictable, with an emphasis on scientific correctness. While the project is still evolving, changes aim to preserve user-facing stability where feasible. Type information and documentation are maintained to ease integration into existing pipelines. diff --git a/docs/api/iod_gauss.md b/docs/api/iod_gauss.md new file mode 100644 index 0000000..95d9997 --- /dev/null +++ b/docs/api/iod_gauss.md @@ -0,0 +1,4 @@ + +::: py_outfit.iod_gauss.GaussResult + options: + show_root_heading: true \ No newline at end of file diff --git a/docs/api/iod_params.md b/docs/api/iod_params.md new file mode 100644 index 0000000..9abd3b7 --- /dev/null +++ b/docs/api/iod_params.md @@ -0,0 +1,24 @@ +# IOD Parameters + + +::: py_outfit.iod_params.IODParams + options: + show_root_heading: true + heading_level: 2 + members_order: source + show_signature: true + show_signature_annotations: true + merge_init_into_class: true + filters: + - "!^_" + + +::: py_outfit.iod_params.IODParamsBuilder + options: + show_root_heading: true + heading_level: 2 + members_order: source + show_signature: true + show_signature_annotations: true + filters: + - "!^_" diff --git a/docs/api/observations.md b/docs/api/observations.md new file mode 100644 index 0000000..8f74fde --- /dev/null +++ b/docs/api/observations.md @@ -0,0 +1 @@ +::: py_outfit.observations.Observations \ No newline at end of file diff --git a/docs/api/observer.md b/docs/api/observer.md new file mode 100644 index 0000000..3a15580 --- /dev/null +++ b/docs/api/observer.md @@ -0,0 +1 @@ +::: py_outfit.observer.Observer \ No newline at end of file diff --git a/docs/api/orbit_type/cometary.md b/docs/api/orbit_type/cometary.md new file mode 100644 index 0000000..d725961 --- /dev/null +++ b/docs/api/orbit_type/cometary.md @@ -0,0 +1,2 @@ + +::: py_outfit.orbit_type.cometary.CometaryElements \ No newline at end of file diff --git a/docs/api/orbit_type/equinoctial.md b/docs/api/orbit_type/equinoctial.md new file mode 100644 index 0000000..e919625 --- /dev/null +++ b/docs/api/orbit_type/equinoctial.md @@ -0,0 +1,3 @@ +# Equinoctial Elements + +::: py_outfit.orbit_type.equinoctial.EquinoctialElements \ No newline at end of file diff --git a/docs/api/orbit_type/keplerian.md b/docs/api/orbit_type/keplerian.md new file mode 100644 index 0000000..7661973 --- /dev/null +++ b/docs/api/orbit_type/keplerian.md @@ -0,0 +1,3 @@ +# Keplerian Elements + +::: py_outfit.orbit_type.keplerian.KeplerianElements \ No newline at end of file diff --git a/docs/api/pandas_pyoutfit.md b/docs/api/pandas_pyoutfit.md new file mode 100644 index 0000000..abd4880 --- /dev/null +++ b/docs/api/pandas_pyoutfit.md @@ -0,0 +1,3 @@ +# Integration with Pandas + +::: py_outfit.pandas_pyoutfit \ No newline at end of file diff --git a/docs/api/py_outfit.md b/docs/api/py_outfit.md new file mode 100644 index 0000000..db5bff9 --- /dev/null +++ b/docs/api/py_outfit.md @@ -0,0 +1,15 @@ +::: py_outfit.PyOutfit + +Physical and astronomical constants exposed by Outfit. + +These values are provided in SI units or astronomical conventions + +::: py_outfit + options: + show_root_toc_entry: true + show_root_heading: true + show_category_heading: true + show_source: true + members_order: source + filters: + - "!^_" diff --git a/docs/api/trajectories.md b/docs/api/trajectories.md new file mode 100644 index 0000000..980ebc9 --- /dev/null +++ b/docs/api/trajectories.md @@ -0,0 +1,5 @@ +::: py_outfit.trajectories.Key + +::: py_outfit.trajectories.PathLike + +::: py_outfit.trajectories.TrajectorySet \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..14672aa --- /dev/null +++ b/docs/index.md @@ -0,0 +1,72 @@ +# pyOutfit + +High-performance Python bindings for the Outfit orbit-determination engine. pyOutfit provides a thin, typed interface to the Rust core to perform initial orbit determination, manipulate orbital elements, ingest astrometric observations, and process large batches efficiently. + +## What this project is + +pyOutfit is a Python package that exposes the Rust Outfit crate through PyO3. It brings robust, numerically stable routines for orbit determination and observation handling to Python workflows while keeping the heavy computation in Rust. The design emphasizes reliability, performance, and a clean user-facing API that integrates well with scientific Python stacks. + +## Purpose and scope + +The package focuses on initial orbit determination (IOD) based on the classical Gauss method, conversions between orbital element representations, ingestion of astrometric observations from multiple sources, and scalable batch processing. It is intended for researchers and engineers working on astrometry pipelines, moving-object detection, and orbit characterization. + +## Core capabilities + +- Initial Orbit Determination using the Gauss method, with configurable numerical tolerances and physical filters. +- Multiple orbital element families with conversions: Keplerian, Equinoctial, and Cometary. +- Observation ingestion from common astronomy formats and in-memory arrays with minimal overhead. +- Parallel batch execution for large datasets, with deterministic behavior via seed control. +- Observer management, including MPC-coded observatories and custom definitions. +- Typed Python interface with docstrings, type stubs, and consistent error mapping. + +## Architecture at a glance + +- Rust core (Outfit crate) implements numerical algorithms, ephemerides access, reference frame transformations, and data structures. +- Python bindings (PyO3) expose high-level classes and functions, keeping data copies and conversions to a minimum. +- Optional parallelism in the Rust layer leverages multi-core systems transparently to Python users. +- The interface is designed to be predictable and stable for integration into existing pipelines. + +## Data and models + +- Orbital elements: Keplerian, Equinoctial, and Cometary families with consistent units and reference epochs. +- Observations: right ascension, declination, timing, and uncertainties, with support for degrees or radians ingestion paths. +- Reference frames and corrections: precession, nutation, aberration, and observer geometry are handled in the Rust core. +- Ephemerides: planetary positions are obtained from high-accuracy JPL series (e.g., DE440) via the Outfit crate. + +## Observation ingestion and batches + +- Single-trajectory and multi-trajectory ingestion are both supported. +- Batch containers group observations by trajectory identifier for efficient processing. +- Readers and adapters cover traditional astronomy formats (e.g., MPC 80-column and ADES XML) and tabular data sources (e.g., Parquet), alongside direct NumPy-based ingestion. + +## Performance and reliability + +- Numerical kernels run in Rust without the Python GIL, minimizing overhead. +- Parallel execution is opt-in to avoid contention on small datasets and can be toggled via configuration. +- Deterministic runs are available by providing a random seed when executing batch estimations. +- Errors from the Rust core are mapped to idiomatic Python exceptions with informative messages. + +## Requirements and compatibility + +- Python 3.12. +- A recent Rust toolchain matching the Outfit crate minimum supported version. +- Linux (POSIX) is the primary target platform for packaged distributions. +- The package is distributed with type information (py.typed) and Python type stubs. + +## Project status + +The project is in active development and aims for scientific correctness, clear documentation, and practical performance. The public Python API is designed to be stable where possible; incremental improvements and extensions are expected as the Rust core evolves. + +## Documentation map + +- Python package overview: [PyOutfit](api/py_outfit.md) +- Observer definitions and utilities: [Observer](api/observer.md) +- IOD parameters and configuration: [IODParams](api/iod_params.md) +- Initial Orbit Determination (Gauss): [IODGauss](api/iod_gauss.md) +- Observations and containers: [Observations](api/observations.md), [Trajectories](api/trajectories.md) +- Orbital element types: [Keplerian](api/orbit_type/keplerian.md), [Equinoctial](api/orbit_type/equinoctial.md), [Cometary](api/orbit_type/cometary.md) +- Pandas integration notes: [Pandas Integration](api/pandas_pyoutfit.md) + +## Heritage and licensing + +pyOutfit builds on the Outfit Rust crate, which is a modern reimplementation of classical Fortran-based orbit determination approaches. The package is distributed under the CeCILL-C license. See the repository license file for details. \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..091bfdc --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,173 @@ +# Installation + +This page describes the recommended ways to install `py-outfit` and set up an isolated environment. The package ships pre-built wheels for Linux x86_64 (Python 3.12) and embeds a Rust extension module compiled from the Outfit core. If a wheel is not available for your platform, a local build from source will be attempted (Rust toolchain required). + +## Quick start (PyPI / pip) + +If you already have a clean Python 3.12 environment (e.g. `venv` or `virtualenv`): + +```bash +pip install --upgrade pip +pip install py-outfit +``` + +Verify your installation: + +```bash +python -c "import py_outfit as o; print(o.KeplerianElements)" +``` + +Expected output is the class representation (not an error). You can also check the version: + +```bash +python -c "import importlib.metadata as m; print(m.version('py-outfit'))" +``` + +## Using PDM (recommended for reproducible workflows) + +PDM manages isolated environments and keeps metadata in `pyproject.toml`. + +1. Install PDM (user-level): + ```bash + pip install --upgrade pdm + ``` +2. Initialize a new project directory (or reuse an existing one): + ```bash + pdm init + ``` + Follow the prompts (you can skip dependencies now). +3. Add `py-outfit` as a dependency: + ```bash + pdm add py-outfit + ``` +4. Run Python inside the managed environment: + ```bash + pdm run python -c "import py_outfit; print(py_outfit.SECONDS_PER_DAY)" + ``` + +List current dependencies: + +```bash +pdm list +``` + +## Using Conda / Mamba + +You can consume the PyPI wheel from within a Conda environment. Ensure the environment uses Python 3.12 so that the published wheel matches. + +1. Create and activate the environment (use `mamba` if available for speed): + ```bash + conda create -n outfit-env python=3.12 -y + conda activate outfit-env + ``` +2. Install via pip inside the environment: + ```bash + pip install --upgrade pip + pip install py-outfit + ``` +3. Test: + ```bash + python -c "import py_outfit as o; print(o.GAUSS_GRAV)" + ``` + +If you need scientific stack packages (NumPy, Pandas, Astropy) with Conda optimizations, you can pre-install them: + +```bash +conda install numpy pandas astropy pyarrow -y +pip install py-outfit +``` + +## Source build (fallback) + +If your platform lacks a pre-built wheel, `pip` will build from source. Requirements: + +- Rust toolchain (stable, matching the crate `rust-version` requirement). +- Build tools (e.g., `gcc`, `make`, and Python headers). On Debian/Ubuntu: + ```bash + sudo apt-get update && sudo apt-get install -y build-essential pkg-config python3-dev + ``` + +Then: + +```bash +pip install --upgrade pip maturin +pip install py-outfit --no-binary py-outfit +``` + +You can also clone the repository and build in-place: + +```bash +git clone https://github.com/FusRoman/pyOutfit.git +cd pyOutfit +pip install . +``` + +## Verifying functionality + +Minimal smoke test to perform a trivial object creation (no external ephemerides download required for this step): + +```bash +python - <<'PY' +import py_outfit as o +ke = o.KeplerianElements( + a=2.5, # semi-major axis (AU) + e=0.1, # eccentricity + i=0.2, # inclination (rad) + omega=1.0, # argument of perihelion (rad) + Omega=0.5, # longitude of ascending node (rad) + M=0.0 # mean anomaly (rad) +) +print("KeplerianElements a:", ke.a) +PY +``` + +If this runs without error, the Rust extension is correctly loaded. + +## Selecting a parallel strategy + +Parallel features are enabled in the underlying Rust crate. No extra Python-side configuration is required. If you process large batches and want deterministic behavior across runs, pass a seed where exposed by batch APIs (see `estimate_all_orbits`). + +## Upgrading + +```bash +pip install --upgrade py-outfit +``` + +With PDM: + +```bash +pdm update py-outfit +``` + +## Uninstalling + +```bash +pip uninstall py-outfit +``` + +Or in PDM: + +```bash +pdm remove py-outfit +``` + +## Troubleshooting + +Missing wheel / build fails: + Ensure Rust is installed (`curl https://sh.rustup.rs -sSf | sh`) and that `rustc --version` meets or exceeds the crate requirement. Install system build dependencies (`build-essential` or equivalents). + +ImportError: cannot open shared object file: + Check that you are not mixing architectures (e.g., installing in a system Python but running inside Conda). Reinstall inside the same environment that runs Python. + +Segfault or crash on import: + Remove conflicting old builds: `pip uninstall py-outfit -y` then reinstall. Ensure only one version of the extension (`py_outfit*.so`) exists in the site-packages path. + +Ephemerides download issues: + The first call that triggers JPL ephemerides access may fetch data. Ensure network access is available, or pre-populate cache directories according to the Outfit core documentation. + +If problems persist, open an issue with: Python version, platform, `pip show py-outfit`, and the full install log (add `-vv` to pip commands for verbosity). + +## Next steps + +Proceed to the API reference or the tutorials (e.g., Initial Orbit Determination) once installation is confirmed. + diff --git a/docs/tutorials/iod_params.md b/docs/tutorials/iod_params.md new file mode 100644 index 0000000..15b9f6c --- /dev/null +++ b/docs/tutorials/iod_params.md @@ -0,0 +1,68 @@ +# IODParams: configuring Gauss IOD + +This tutorial explains the purpose of `IODParams`, how it shapes the Gauss Initial Orbit Determination (IOD) pipeline, and practical ways to configure it for different datasets. The goal is to keep configuration centralized, reproducible, and explicit. + +## What IODParams is for + +`IODParams` collects all tunable parameters used by the Gauss IOD solver. It controls how observation triplets are selected, how Monte Carlo perturbations are applied, which physical and numerical filters are enforced, and how candidate solutions are ranked by RMS. The same parameter object can be reused across many trajectories and batches for consistent behavior. + +At a high level, the pipeline proceeds by generating triplets from time-ordered observations, expanding them via controlled noise, solving the Gauss polynomial to obtain candidate orbits, filtering candidates by physical plausibility and numerical quality, and selecting the best solution by RMS over a controlled time window. + +## Getting a configuration + +You can either instantiate `IODParams()` to get the defaults, or use the fluent builder to override only what you need before producing an immutable configuration. + +```py linenums="1" title="IODParams defaults" +--8<-- "docs/tutorials/tutorial_snippets/iod_params_defaults.py" +``` + +The defaults are conservative and intended to work on a wide range of small datasets. For larger data or specific science goals, it is common to change the number of triplets, the extrapolation window for RMS, and parallel execution. + +## Turning on parallel execution + +Parallel execution can reduce wall-clock time for many trajectories. The advisory flag is carried by `IODParams` and consumed by higher-level APIs that accept it. + +```py linenums="1" title="Requesting parallel execution" +--8<-- "docs/tutorials/tutorial_snippets/iod_params_parallel.py" +``` + +When processing many trajectories, prefer larger batch sizes (for example, 500–1000), because the overhead of assembling and scheduling batches can dominate when batches are too small. In sequential mode, cooperative cancellation (Ctrl‑C) remains responsive; in parallel mode, cancellation is not supported and you may need to terminate the process to stop long runs. + +## Physical and numerical filters + +The Gauss pipeline produces multiple mathematical candidates per triplet. `IODParams` constrains the search to physically plausible and numerically stable regions, which prevents spurious or degenerate solutions. + +```py linenums="1" title="Plausibility and solver tolerances" +--8<-- "docs/tutorials/tutorial_snippets/iod_params_filters.py" +``` + +- Physical constraints (eccentricity, perihelion, heliocentric distance bounds, minimum topocentric distance) help reduce non-physical or near-observer pathologies. +- Numerical tolerances for the Aberth–Ehrlich polynomial solver, Newton steps, and the universal Kepler solver govern convergence and robustness. The parameter `root_imag_eps` controls how small the imaginary part of a complex root must be to treat it as effectively real when selecting roots of the 8th‑degree Gauss polynomial. The bounds `r2_min_au` and `r2_max_au` apply plausibility constraints to the central heliocentric distance used during root selection. The parameter `min_rho2_au` rejects spurious geometries by enforcing a minimum topocentric distance at the central epoch. + +## RMS window and triplet selection + +RMS evaluation is carried out over a time window derived from the triplet span and clamped to a minimum. Triplet generation itself is constrained by minimum and maximum spans and a target spacing. + +```py linenums="1" title="RMS window and triplet spacing" +--8<-- "docs/tutorials/tutorial_snippets/iod_params_rms_window.py" +``` + +The RMS window is derived from the triplet span and clamped to a minimum: `dt_window = (last − first) × extf`, with the final window ensured to be at least `dtmax`. Use a negative `extf` to trigger a broad fallback window when observations are sparse or irregularly sampled. Increase `dtmax` if the default minimum window is too short for your cadence. + +## Practical guidance + +- Prefer the builder for clarity and reproducibility; only set what you need. +- Start from defaults, adjust triplet and RMS controls first, then refine physical and numerical filters. +- Enable parallelism for large batches; keep sequential mode for small, interactive runs so that cancellation remains responsive. +- Use a fixed seed when you need bitwise reproducibility across runs. + +## Validation and special cases + +`IODParams` is validated when built to prevent inconsistent configurations. Time spans must be non‑negative; tolerances must be positive; and plausibility bounds must be ordered and strictly positive. Two special cases are worth noting. First, setting `n_noise_realizations = 0` disables noisy clones and uses only the original triplet, which can be useful for speed‑of‑light checks or deterministic baselines. Second, `max_obs_for_triplets < 3` is accepted and behaves like `3`, effectively selecting first/middle/last so that at least one valid triplet is always available. Negative `extf` activates the broad fallback window; regardless of `extf`, the RMS window is clamped to be at least `dtmax`. + +## Tuning cheat sheet + +- Too many spurious candidates or unstable solutions: tighten `max_ecc`, decrease `r2_max_au`, increase `min_rho2_au`, or reduce `root_imag_eps`. +- Convergence issues on some datasets: raise `aberth_max_iter` moderately and relax `aberth_eps` slightly; check `newton_eps`/`newton_max_it` and `kepler_eps`. +- Sparse or irregular sampling: use a negative `extf` or increase `dtmax`; increase `dt_min` to avoid ultra‑short triplets. +- Large volume batches: enable `.do_parallel()` and increase `batch_size`; prefer larger batch sizes (hundreds to a thousand) to amortize scheduling overhead. diff --git a/docs/tutorials/orbit_results.md b/docs/tutorials/orbit_results.md new file mode 100644 index 0000000..ae8d178 --- /dev/null +++ b/docs/tutorials/orbit_results.md @@ -0,0 +1,114 @@ +# Working with orbit results (Gauss IOD) + +This tutorial shows how to consume the successful results returned by `TrajectorySet.estimate_all_orbits(...)` and how to navigate the different orbital element families produced by the Gauss solver. + +You will learn how to: + +- iterate over the successful results map and inspect `GaussResult` objects, +- check whether the result is a preliminary or corrected orbit, +- extract the concrete orbital elements (Keplerian, Equinoctial, or Cometary), +- convert between element families when supported, +- serialize results to dictionaries for logging or downstream processing. + +> The examples assume you already ran batch IOD and obtained `(ok, errors)` from `estimate_all_orbits(env, params, ...)`. See the Trajectories and IODParams tutorials for how to configure and run the solver. + +??? example "Reference: run_iod() used by the snippets" + The code examples below call a shared helper that builds a small dataset, runs batch IOD, + and returns `(ok, errors)`. For completeness, here is the helper once: + + ```py linenums="1" title="common_tuto.run_iod()" + --8<-- "docs/tutorials/tutorial_snippets/common_tuto.py" + ``` + +--- + +## Iterate over successful results + +```py linenums="1" title="Iterate results" +--8<-- "docs/tutorials/tutorial_snippets/orbit_results_iterate.py" +``` + +- `obj_id` is the same identifier you used when ingesting trajectories (int or str). +- `rms` is the post-fit residual RMS (radians) computed over the chosen time window. + +--- + +## Determine the element family + +```py linenums="1" title="Family and stage" +--8<-- "docs/tutorials/tutorial_snippets/orbit_results_family_stage.py" +``` + +--- + +## Extract concrete elements + +Use the typed accessors; they return `None` if the stored family differs. + +```py linenums="1" title="Extract typed elements" +--8<-- "docs/tutorials/tutorial_snippets/orbit_results_extract.py" +``` + +Units reminder: +- Epochs are MJD (TDB). Angles are radians. Distances are AU. + +--- + +## Convert between element families + +Conversions are provided by the element classes themselves. + +- Keplerian → Equinoctial: + +```py linenums="1" title="Convert between families" +--8<-- "docs/tutorials/tutorial_snippets/orbit_results_convert.py" +``` + +> Note: parabolic cometary elements (e = 1) cannot be converted by these helpers and will raise a `ValueError`. + +--- + +## Structured dict serialization + +Every `GaussResult` can be converted to a plain dictionary for easy logging and JSON export: + +```py linenums="1" title="Structured dict serialization" +--8<-- "docs/tutorials/tutorial_snippets/orbit_results_to_dict.py" +``` + +Example for a Keplerian result: + +```python +{ + 'stage': 'corrected', + 'type': 'keplerian', + 'elements': { + 'reference_epoch': 58794.29503864708, + 'semi_major_axis': 2.618543557694562, + 'eccentricity': 0.2917924222538649, + 'inclination': 0.23168624097364912, + 'ascending_node_longitude': 0.20856161706357348, + 'periapsis_argument': 6.264575557486691, + 'mean_anomaly': 0.29001350766154466 + } +} +``` + +--- + +## Putting it together: filter, convert, export + +Below is a compact pattern you can adapt to your pipeline: + +```py linenums="1" title="Filter, convert, export" +--8<-- "docs/tutorials/tutorial_snippets/orbit_results_export.py" +``` + +--- + +## Tips + +- Always check the element family via `elements_type()` before calling accessors; the typed helpers return `None` when mismatched. +- When you need a single canonical representation, prefer converting to Keplerian where defined, but keep native cometary elements for `e = 1`. +- Store the RMS alongside the elements; it’s a useful quality metric for ranking and filtering. +- If you run `estimate_best_orbit` repeatedly on the same `Observations` instance, be aware of the in-place uncertainty scaling caveat described in the Observations tutorial; recreate the object for bitwise reproducibility. diff --git a/docs/tutorials/pandas_tuto.md b/docs/tutorials/pandas_tuto.md new file mode 100644 index 0000000..065ac5e --- /dev/null +++ b/docs/tutorials/pandas_tuto.md @@ -0,0 +1,78 @@ +# Pandas integration: vectorized IOD from DataFrames + +This tutorial shows how to run Gauss IOD directly from a flat Pandas DataFrame via the `DataFrame.outfit` accessor. You will learn how to: + +- initialize the environment and register the accessor, +- run the degrees+arcseconds workflow, +- use a radians workflow, +- adapt to custom column names with `Schema`, +- handle successes and errors, and join results with external metadata. + +The accessor is implemented in `py_outfit.pandas_pyoutfit` and builds a `TrajectorySet` from NumPy arrays under the hood. + +## Prerequisites + +Importing the module registers the accessor and we create a simple observing environment: + +```py linenums="1" title="Setup environment and accessor" +--8<-- "docs/tutorials/tutorial_snippets/pandas_setup.py" +``` + +--- + +## Degrees + arcseconds workflow + +Your DataFrame provides `tid`, `mjd`, `ra`, `dec`. Angles are degrees and uncertainties are provided in arcseconds. + +```py linenums="1" title="Minimal example (degrees + arcsec)" +--8<-- "docs/tutorials/tutorial_snippets/pandas_basic_degrees.py" +``` + +Notes + +- Internally, RA/DEC are converted once to radians; uncertainties are converted from arcsec to radians using `RADSEC`. +- Use `rng_seed` for deterministic exploration. + +--- + +## Radians workflow + +Supply angles and uncertainties in radians to avoid conversions. + +```py linenums="1" title="Radians end-to-end" +--8<-- "docs/tutorials/tutorial_snippets/pandas_radians_workflow.py" +``` + +--- + +## Custom column names with Schema + +If your DataFrame uses different names, provide a `Schema` mapping. + +```py linenums="1" title="Adapt to arbitrary column names" +--8<-- "docs/tutorials/tutorial_snippets/pandas_custom_schema.py" +``` + +--- + +## Handling successes and errors, joining metadata + +The accessor returns a success table and may append error rows. You can split and join with other tables. + +```py linenums="1" title="Post-processing: statuses and joins" +--8<-- "docs/tutorials/tutorial_snippets/pandas_handle_status.py" +``` + +--- + +## Caveats and reproducibility + +- Known backend caveat: due to an upstream issue in batch RMS correction, per‑observation uncertainties may be modified in place during a run. Re-using the same `Observations` instance and calling `estimate_best_orbit` repeatedly can yield different RMS between calls. When using the accessor this is typically not visible, but for strict reproducibility recreate the underlying `TrajectorySet` or source DataFrame before repeated runs. +- `rng_seed` ensures deterministic random sampling but does not prevent in-place mutations from earlier runs. + +## See also + +- API reference: `Pandas Integration` +- Core container: `TrajectorySet` +- Configuration: `IODParams` tutorial + diff --git a/docs/tutorials/pyoutfit_environment.md b/docs/tutorials/pyoutfit_environment.md new file mode 100644 index 0000000..dc9b2f6 --- /dev/null +++ b/docs/tutorials/pyoutfit_environment.md @@ -0,0 +1,57 @@ +# pyOutfit Environment + +This tutorial introduces the `PyOutfit` environment object and explains its role as the central coordination point for ephemerides, error models, and observatory management. It also outlines how the environment interacts with observations and batch orbit determination. + +## Purpose of `PyOutfit` + +`PyOutfit` encapsulates the configuration needed by the Outfit core engine to perform initial orbit determination and related computations. It holds the selected planetary ephemerides, the astrometric error model, and a registry of observatories. Other components, such as `TrajectorySet`, rely on an initialized environment to resolve observer geometry, reference frames, and numerical settings consistently. + +In typical workflows, a single `PyOutfit` instance is created at the beginning of a session or pipeline and passed to ingestion and batch-processing functions. This pattern ensures consistent context across all computations and avoids duplicating configuration. + +## Key responsibilities + +- Manage ephemerides selection (e.g., DE440) used for precise solar-system positions. +- Provide a registry for `Observer` definitions, either fetched from MPC codes or created manually. +- Serve as a context object for ingestion of observations and for batch estimation of orbits. +- Expose convenience utilities for listing and validating available observatories. + +## Typical usage pattern + +- Initialize the environment with an ephemerides selector and an astrometric error model string. +- Register at least one `Observer` that represents the observing site used in your data. +- Ingest observations into a `TrajectorySet`, using either radian-based zero‑copy arrays, degree-based arrays with conversion, or supported file formats. +- Configure `IODParams` and run batch Gauss IOD, passing the environment and parameters. + +## Example snippets + +Below are non-executable snippets demonstrating the expected structure of a session. The code is provided in separate files and included here for readability. + +### Environment initialization + +```py linenums="1" title="environment initialization" +--8<-- "docs/tutorials/tutorial_snippets/environment_init.py" +``` + +1. The constructor accepts two strings: an ephemerides selector and an astrometric error model. The ephemerides selector uses the format "{source}:{version}" and recognizes two backends: "horizon" for legacy JPL DE binaries and "naif" for NAIF SPK/DAF kernels. Examples include "horizon:DE440" and "naif:DE440". The resolved file is stored under the OS cache (e.g., ~/.cache/outfit_cache/jpl_ephem/…), and when the build enables JPL downloads, a missing file is fetched automatically; otherwise an error is raised. The error model selects per‑site RA/DEC bias and RMS tables used during orbit determination. Supported names include "FCCT14", "VFCC17", and "CBM10"; unknown names default to "FCCT14". These two parameters define the numerical and physical context shared by ingestion and IOD routines. + +### Observer registration + +```py linenums="1" title="environment initialization" +--8<-- "docs/tutorials/tutorial_snippets/observer_registration.py" +``` + +## Notes on configuration + +- The ephemerides selector is a string understood by the Outfit core; consult the API reference for supported values. A common choice is an identifier referring to JPL DE series. The error model string controls how observational uncertainties are interpreted and propagated; unknown strings default to a standard model. +- The environment’s observatory registry is independent of trajectory ingestion. Multiple observers can be registered, but an ingestion call typically associates a single observer with the new data. If observations originate from multiple sites, separate ingestion steps or containers are recommended. +- `PyOutfit` does not itself perform orbit determination; instead, it supplies the context required by `TrajectorySet.estimate_all_orbits` and related functions. This separation keeps configuration centralized and computation modules focused. + +## Reliability and performance considerations + +- The environment is lightweight to construct and is intended to be reused. Creating many separate environments for a single batch is unnecessary. +- Numerical work is performed in Rust and detached from the Python GIL. Parallel execution can be enabled through IOD configuration and is generally beneficial for large batches. +- Deterministic operation is available by providing a random seed to batch execution routines that support it. + +## Where to go next + +- Consult the API pages for `PyOutfit` and `Observer` to explore available methods and parameters. diff --git a/docs/tutorials/trajectories_tuto.md b/docs/tutorials/trajectories_tuto.md new file mode 100644 index 0000000..8768532 --- /dev/null +++ b/docs/tutorials/trajectories_tuto.md @@ -0,0 +1,107 @@ +# Trajectories: loading data and running batch IOD + +This tutorial shows how to work with `TrajectorySet`, the container for many objects with time‑ordered astrometric observations. You will learn how to: + +- import trajectories from files (MPC 80‑column and ADES), +- build trajectories from in‑memory NumPy arrays, +- estimate preliminary orbits for all trajectories or just one. + +The heavy lifting is performed by the Rust engine; the Python API keeps things concise and composable. + +## Prerequisites + +You will need a global environment and at least one observing site: + +```py linenums="1" title="Register an observing site" +--8<-- "docs/tutorials/tutorial_snippets/observer_registration.py" +``` + +> Units used in this API: angles are radians unless stated otherwise; epochs are MJD (TT, days); uncertainties may be provided in arcseconds for convenience where noted. + +--- + +## Import from files + +### MPC 80‑column + +Create a set from a single MPC 80‑column file, or append into an existing set. + +```py linenums="1" title="From MPC 80-column" +--8<-- "docs/tutorials/tutorial_snippets/trajectories_from_mpc_80col.py" +``` + +Notes + +- Input parsing mirrors the Rust engine. Avoid ingesting the same file twice: no de‑duplication is performed. + +### ADES (JSON or XML) + +When creating from ADES, you can provide global uncertainties (arcsec) if they are not specified per row. + +```py linenums="1" title="From ADES (JSON/XML)" +--8<-- "docs/tutorials/tutorial_snippets/trajectories_from_ades.py" +``` + +--- + +## Build from in‑memory arrays + +Two ingestion helpers are available. Use degrees/arcseconds for convenience, or supply radians for a zero‑copy path. + +### Degrees + arcseconds (converted once to radians) + +```py linenums="1" title="From NumPy (degrees + arcsec)" +--8<-- "docs/tutorials/tutorial_snippets/trajectories_from_numpy_degrees.py" +``` + +### Radians (zero‑copy) + +```py linenums="1" title="From NumPy (radians, zero-copy)" +--8<-- "docs/tutorials/tutorial_snippets/trajectories_from_numpy_radians.py" +``` + +--- + +## Estimate orbits + +You can estimate preliminary orbits for all trajectories in a set, or for a single trajectory. + +### Batch over all trajectories + +```py linenums="1" title="Batch IOD across the set" +--8<-- "docs/tutorials/tutorial_snippets/trajectories_estimate_all.py" +``` + +Notes + +- In sequential mode, pressing Ctrl‑C returns partial results collected so far. +- If `.do_parallel()` is enabled in `IODParams`, cancellation is not available. +- Set `seed` for deterministic noise sampling and triplet exploration. + +### One trajectory only + +Use the dict‑like access to get an `Observations` view, then call its single‑object API. + +```py linenums="1" title="Single trajectory IOD" +--8<-- "docs/tutorials/tutorial_snippets/trajectories_estimate_single.py" +``` + +--- + +## Caveats and reproducibility + +- Known caveat: due to an upstream issue in the backend’s batch RMS correction, per‑observation uncertainties may be modified in place during a run. Calling `estimate_best_orbit` multiple times on the same `Observations` instance can yield different RMS values across calls. As a temporary workaround, recreate the `Observations` (or `TrajectorySet`) before each repeated estimation when you need strict reproducibility. +- Providing a `seed` makes noise sampling deterministic but does not prevent such in‑place mutations. + +--- + +## See also + +- API reference: `py_outfit.trajectories.TrajectorySet` +- Configuration: `IODParams` tutorial for tuning Gauss IOD +- High‑level snippet used in examples: + +```py linenums="1" title="Overview" +--8<-- "docs/tutorials/tutorial_snippets/trajectories_overview.py" +``` + diff --git a/docs/tutorials/tutorial_snippets/common_tuto.py b/docs/tutorials/tutorial_snippets/common_tuto.py new file mode 100644 index 0000000..00f3389 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/common_tuto.py @@ -0,0 +1,153 @@ +from py_outfit import PyOutfit, IODParams, TrajectorySet, Observer +import numpy as np +from astropy.time import Time + + +def run_iod(): + env = PyOutfit("horizon:DE440", "FCCT14") + obs = Observer( + 0.0, 0.0, 1.0, "DemoSite", np.deg2rad(0.3 / 3600.0), np.deg2rad(0.3 / 3600.0) + ) + env.add_observer(obs) + + trajectory_id = np.array( + [ + 0, + 1, + 2, + 1, + 2, + 1, + 0, + 0, + 0, + 1, + 2, + 1, + 1, + 0, + 2, + 2, + 0, + 2, + 2, + 10, + 10, + 10, + 11, + 11, + 11, + ], + dtype=np.uint32, + ) + ra_deg = np.array( + [ + 20.9191548, + 33.4247141, + 32.1435128, + 33.4159091, + 32.1347282, + 33.3829299, + 20.6388309, + 20.6187259, + 20.6137886, + 32.7525147, + 31.4874917, + 32.4518231, + 32.4495403, + 19.8927380, + 30.6416348, + 30.0938936, + 18.2218784, + 28.3859403, + 28.3818327, + 10.0, + 10.01, + 10.02, + 180.0, + 180.02, + 180.05, + ] + ) + dec_deg = np.array( + [ + 20.0550441, + 23.5516817, + 26.5139615, + 23.5525348, + 26.5160622, + 23.5555991, + 20.1218532, + 20.1264229, + 20.1275173, + 23.6064063, + 26.6622284, + 23.6270392, + 23.6272157, + 20.2977473, + 26.8303010, + 26.9256271, + 20.7096409, + 27.1602652, + 27.1606420, + 5.0, + 5.01, + 5.015, + -10.0, + -10.02, + -10.03, + ] + ) + times_jd_utc = np.array( + [ + 2458789.6362963, + 2458789.6381250, + 2458789.6381250, + 2458789.6663773, + 2458789.6663773, + 2458789.7706481, + 2458790.6995023, + 2458790.7733333, + 2458790.7914120, + 2458791.8445602, + 2458791.8445602, + 2458792.8514699, + 2458792.8590741, + 2458793.6896759, + 2458794.7996759, + 2458796.7965162, + 2458801.7863426, + 2458803.7699537, + 2458803.7875231, + 2458800.0, + 2458800.01, + 2458800.03, + 2458800.0, + 2458800.02, + 2458800.05, + ] + ) + # --8<-- [end:iod-doc-arrays] + + # --8<-- [start:iod-doc-run] + # Convert times to MJD (TT) using astropy + t_utc = Time(times_jd_utc, format="jd", scale="utc") + mjd_tt = t_utc.tt.mjd.astype(np.float64) + + # Degree path performs a single conversion to radians at ingestion + ts = TrajectorySet.from_numpy_degrees( + env, + trajectory_id, + ra_deg, + dec_deg, + error_ra_arcsec=0.3, + error_dec_arcsec=0.3, + mjd_tt=mjd_tt, + observer=obs, + ) + + # Configure IOD and run batch estimation + params = IODParams.builder().max_triplets(200).do_sequential().build() + ok, errors = ts.estimate_all_orbits(env, params, seed=42) + + return ok, errors diff --git a/docs/tutorials/tutorial_snippets/environment_init.py b/docs/tutorials/tutorial_snippets/environment_init.py new file mode 100644 index 0000000..916e7df --- /dev/null +++ b/docs/tutorials/tutorial_snippets/environment_init.py @@ -0,0 +1,9 @@ +# Environment initialization example for documentation inclusion +from py_outfit import PyOutfit + +# Create a new environment with ephemerides and an error model +# The error model string controls observational uncertainty handling in the core engine +env = PyOutfit("horizon:DE440", "FCCT14") # (1) ephemerides selector, error model + +# Human-readable listing of currently known observatories (initially empty or built-in) +print(env.show_observatories()) diff --git a/docs/tutorials/tutorial_snippets/iod_params_defaults.py b/docs/tutorials/tutorial_snippets/iod_params_defaults.py new file mode 100644 index 0000000..5e0fbb2 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/iod_params_defaults.py @@ -0,0 +1,6 @@ +# IODParams defaults: obtain the standard configuration +from py_outfit import IODParams + +# All fields are initialized to the documented defaults +params = IODParams() +print(params.max_triplets, params.dtmax, params.n_noise_realizations) diff --git a/docs/tutorials/tutorial_snippets/iod_params_end_to_end.py b/docs/tutorials/tutorial_snippets/iod_params_end_to_end.py new file mode 100644 index 0000000..ca9c214 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/iod_params_end_to_end.py @@ -0,0 +1,25 @@ +# End-to-end usage: build params and run batch IOD +from py_outfit import PyOutfit, IODParams, TrajectorySet, Observer +import numpy as np + +env = PyOutfit("horizon:DE440", "FCCT14") +obs = Observer(0.0, 0.0, 1.0, "DemoSite", np.deg2rad(0.3/3600.0), np.deg2rad(0.3/3600.0)) +env.add_observer(obs) + +# Minimal synthetic dataset +tid = np.array([1,1,1], dtype=np.uint32) +ra = np.array([1.0,1.01,1.015]) +dec = np.array([0.5,0.49,0.48]) +t = np.array([60000.0,60000.02,60000.05]) + +ts = TrajectorySet.from_numpy_radians(env, tid, ra, dec, 1e-4, 1e-4, t, obs) + +params = ( + IODParams.builder() + .max_triplets(200) + .do_sequential() + .build() +) + +ok, err = ts.estimate_all_orbits(env, params, seed=123) +print(list(ok.keys()), list(err.keys())) diff --git a/docs/tutorials/tutorial_snippets/iod_params_filters.py b/docs/tutorials/tutorial_snippets/iod_params_filters.py new file mode 100644 index 0000000..4b79f91 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/iod_params_filters.py @@ -0,0 +1,26 @@ +# Tune physical and numerical filters for the Gauss solver +from py_outfit import IODParams + +params = ( + IODParams.builder() + # Physical plausibility + .max_ecc(3.0) + .max_perihelion_au(100.0) + .r2_min_au(0.1) + .r2_max_au(100.0) + .min_rho2_au(0.02) + # Numerical tolerances + .aberth_max_iter(100) + .aberth_eps(1e-8) + .newton_eps(1e-12) + .newton_max_it(75) + .kepler_eps(1e-12) + .root_imag_eps(1e-8) + .max_tested_solutions(5) + .build() +) +print( + params.max_ecc, + params.aberth_max_iter, + params.max_tested_solutions, +) diff --git a/docs/tutorials/tutorial_snippets/iod_params_parallel.py b/docs/tutorials/tutorial_snippets/iod_params_parallel.py new file mode 100644 index 0000000..8b76681 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/iod_params_parallel.py @@ -0,0 +1,10 @@ +# Request parallel execution when supported by the build +from py_outfit import IODParams + +params = ( + IODParams.builder() + .do_parallel() # advisory flag consumed by higher-level APIs + .batch_size(8) # number of trajectories to schedule at once + .build() +) +print(params.do_parallel, params.batch_size) diff --git a/docs/tutorials/tutorial_snippets/iod_params_rms_window.py b/docs/tutorials/tutorial_snippets/iod_params_rms_window.py new file mode 100644 index 0000000..bcee568 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/iod_params_rms_window.py @@ -0,0 +1,15 @@ +# Control the RMS evaluation window and triplet spacing +from py_outfit import IODParams + +params = ( + IODParams.builder() + .extf(2.0) # scale relative to triplet span + .dtmax(45.0) # floor in days for evaluation window + .dt_min(0.05) # shortest allowed triplet span + .dt_max_triplet(120.0) # longest allowed triplet span + .optimal_interval_time(15.0) + .max_triplets(50) + .gap_max(6.0/24.0) + .build() +) +print(params.extf, params.dtmax, params.max_triplets) diff --git a/docs/tutorials/tutorial_snippets/observer_registration.py b/docs/tutorials/tutorial_snippets/observer_registration.py new file mode 100644 index 0000000..28113a3 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/observer_registration.py @@ -0,0 +1,23 @@ +# Observer registration example for documentation inclusion +from py_outfit import PyOutfit, Observer + +env = PyOutfit("horizon:DE440", "FCCT14") + +# Define a custom observing site; elevation is expressed in kilometers +obs = Observer( + longitude=12.345, # degrees east + latitude=-5.0, # degrees + elevation=1.0, # kilometers above MSL + name="DemoSite", + ra_accuracy=0.0, # radians (optional, example value) + dec_accuracy=0.0, # radians (optional, example value) +) + +# Register the observer in the environment +env.add_observer(obs) + +# Alternatively, retrieve by MPC code and then register +# mpc_obs = env.get_observer_from_mpc_code("807") +# env.add_observer(mpc_obs) + +print(env.show_observatories()) diff --git a/docs/tutorials/tutorial_snippets/orbit_results_convert.py b/docs/tutorials/tutorial_snippets/orbit_results_convert.py new file mode 100644 index 0000000..a8f85e8 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/orbit_results_convert.py @@ -0,0 +1,24 @@ +# Convert between orbital element families +import os, sys +sys.path.append(os.path.dirname(__file__)) +from common_tuto import run_iod + +ok, _ = run_iod() + +if ok: + _, (g_res, _) = next(iter(ok.items())) + k = g_res.keplerian() + q = g_res.equinoctial() + c = g_res.cometary() + + if k is not None: + q2 = k.to_equinoctial() + print("K→Q a,λ:", q2.semi_major_axis, q2.mean_longitude) + if q is not None: + k2 = q.to_keplerian() + print("Q→K a,e:", k2.semi_major_axis, k2.eccentricity) + if c is not None and c.eccentricity > 1.0: + k_h = c.to_keplerian() + print("C(hyperbolic)→K a,e:", k_h.semi_major_axis, k_h.eccentricity) +else: + print("No successful results; cannot demonstrate conversions.") diff --git a/docs/tutorials/tutorial_snippets/orbit_results_export.py b/docs/tutorials/tutorial_snippets/orbit_results_export.py new file mode 100644 index 0000000..541a41b --- /dev/null +++ b/docs/tutorials/tutorial_snippets/orbit_results_export.py @@ -0,0 +1,38 @@ +# Filter, convert, and build a plain export from successful results +import os, sys +sys.path.append(os.path.dirname(__file__)) +from common_tuto import run_iod + +ok, _ = run_iod() + +export = [] +for obj_id, (g_res, rms) in ok.items(): + k = g_res.keplerian() + if k is None: + q = g_res.equinoctial() + if q is not None: + k = q.to_keplerian() + else: + c = g_res.cometary() + if c is not None and c.eccentricity > 1.0: + k = c.to_keplerian() + + if k is None: + d = g_res.to_dict() + export.append({"id": obj_id, "rms": rms, **d}) + else: + export.append({ + "id": obj_id, + "rms": rms, + "stage": "corrected" if g_res.is_corrected() else "preliminary", + "type": "keplerian", + "a_au": k.semi_major_axis, + "e": k.eccentricity, + "i_rad": k.inclination, + "Omega_rad": k.ascending_node_longitude, + "omega_rad": k.periapsis_argument, + "M_rad": k.mean_anomaly, + "epoch_mjd_tdb": k.reference_epoch, + }) + +print(len(export)) diff --git a/docs/tutorials/tutorial_snippets/orbit_results_extract.py b/docs/tutorials/tutorial_snippets/orbit_results_extract.py new file mode 100644 index 0000000..7b6e3c4 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/orbit_results_extract.py @@ -0,0 +1,20 @@ +# Extract concrete orbital elements from a GaussResult +import os, sys +sys.path.append(os.path.dirname(__file__)) +from common_tuto import run_iod + +ok, _ = run_iod() + +if ok: + _, (g_res, rms) = next(iter(ok.items())) + k = g_res.keplerian() + q = g_res.equinoctial() + c = g_res.cometary() + if k is not None: + print("K a,e:", k.semi_major_axis, k.eccentricity) + if q is not None: + print("Q a,h,k:", q.semi_major_axis, q.eccentricity_sin_lon, q.eccentricity_cos_lon) + if c is not None: + print("C q,e:", c.perihelion_distance, c.eccentricity) +else: + print("No successful results; cannot extract element examples.") diff --git a/docs/tutorials/tutorial_snippets/orbit_results_family_stage.py b/docs/tutorials/tutorial_snippets/orbit_results_family_stage.py new file mode 100644 index 0000000..0327855 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/orbit_results_family_stage.py @@ -0,0 +1,14 @@ +# Determine element family and stage from one successful result +import os, sys +sys.path.append(os.path.dirname(__file__)) +from common_tuto import run_iod + +ok, errors = run_iod() + +if ok: + obj_id, (g_res, rms) = next(iter(ok.items())) + fam = g_res.elements_type() + stage = "corrected" if g_res.is_corrected() else "preliminary" + print(obj_id, fam, stage) +else: + print("No successful results; cannot show family/stage example.") diff --git a/docs/tutorials/tutorial_snippets/orbit_results_iterate.py b/docs/tutorials/tutorial_snippets/orbit_results_iterate.py new file mode 100644 index 0000000..a389360 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/orbit_results_iterate.py @@ -0,0 +1,13 @@ +# Iterate over successful Gauss results and print stage + RMS +import os, sys +sys.path.append(os.path.dirname(__file__)) +from common_tuto import run_iod + +ok, errors = run_iod() + +if not ok: + print("No successful results; errors:", errors) +else: + for obj_id, (g_res, rms) in ok.items(): + stage = "corrected" if g_res.is_corrected() else "preliminary" + print(f"Object {obj_id}: stage={stage}, RMS={rms:.6e} rad") diff --git a/docs/tutorials/tutorial_snippets/orbit_results_to_dict.py b/docs/tutorials/tutorial_snippets/orbit_results_to_dict.py new file mode 100644 index 0000000..fb0f6d6 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/orbit_results_to_dict.py @@ -0,0 +1,13 @@ +# Serialize a GaussResult to a structured dict +import os, sys +sys.path.append(os.path.dirname(__file__)) +from common_tuto import run_iod + +ok, _ = run_iod() + +if ok: + _, (g_res, _) = next(iter(ok.items())) + d = g_res.to_dict() + print(d["stage"], d["type"], sorted(d["elements"].keys())) +else: + print("No successful results; cannot show to_dict().") diff --git a/docs/tutorials/tutorial_snippets/pandas_basic_degrees.py b/docs/tutorials/tutorial_snippets/pandas_basic_degrees.py new file mode 100644 index 0000000..fce812e --- /dev/null +++ b/docs/tutorials/tutorial_snippets/pandas_basic_degrees.py @@ -0,0 +1,79 @@ +""" +Minimal degrees+arcseconds workflow using the Pandas accessor. +""" + +import numpy as np +import pandas as pd +from py_outfit import IODParams + +# Ensure the accessor is registered +import py_outfit.pandas_pyoutfit # noqa: F401 + +from pandas_setup import env, observer # type: ignore + + +# Build a tiny demo dataset: three objects, three observations each +df = pd.DataFrame( + { + "tid": [0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2], + "mjd": [ + 58789.13709704, + 58790.20030304, + 58790.27413404, + 58790.29221274, + 58793.19047664, + 58801.28714334, + 60000.0, + 60000.02, + 60000.05, + 60000.0, + 60000.02, + 60000.05, + ], + "ra": [ + 20.9191548, + 20.6388309, + 20.6187259, + 20.6137886, + 19.8927380, + 18.2218784, + 33.42, + 33.44, + 33.47, + 32.14, + 32.17, + 32.20, + ], + "dec": [ + 20.0550441, + 20.1218532, + 20.1264229, + 20.1275173, + 20.2977473, + 20.7096409, + 23.55, + 23.56, + 23.57, + 26.51, + 26.52, + 26.53, + ], + } +) + +params = IODParams.builder().max_triplets(150).do_sequential().build() + +res = df.outfit.estimate_orbits( + env, + params, + observer, + ra_error=0.3, # arcsec + dec_error=0.3, # arcsec + units="degrees", + rng_seed=42, +) + +# Show a compact preview, resilient to error-only outputs +wanted = ["object_id", "variant", "element_set", "rms", "status", "error"] +cols = [c for c in wanted if c in res.columns] +print(res.head(5)[cols]) diff --git a/docs/tutorials/tutorial_snippets/pandas_custom_schema.py b/docs/tutorials/tutorial_snippets/pandas_custom_schema.py new file mode 100644 index 0000000..5931b65 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/pandas_custom_schema.py @@ -0,0 +1,57 @@ +""" +Custom Schema: adapt to DataFrames with different column names. +""" + +import numpy as np +import pandas as pd +from py_outfit import IODParams +from py_outfit.pandas_pyoutfit import Schema + +import py_outfit.pandas_pyoutfit # noqa: F401 +from pandas_setup import env, observer # type: ignore + + +df_weird = pd.DataFrame( + { + "object": [0, 0, 0, 0, 0, 0], + "epoch": [ + 58789.13709704, + 58790.20030304, + 58790.27413404, + 58790.29221274, + 58793.19047664, + 58801.28714334, + ], + "alpha": [ + 20.9191548, + 20.6388309, + 20.6187259, + 20.6137886, + 19.8927380, + 18.2218784, + ], + "delta": [ + 20.0550441, + 20.1218532, + 20.1264229, + 20.1275173, + 20.2977473, + 20.7096409, + ], + } +) + +schema = Schema(tid="object", mjd="epoch", ra="alpha", dec="delta") +params = IODParams() + +res = df_weird.outfit.estimate_orbits( + env, + params, + observer, + ra_error=0.3, + dec_error=0.3, + schema=schema, + units="degrees", +) + +print(res[["object_id", "variant", "element_set", "rms"]]) diff --git a/docs/tutorials/tutorial_snippets/pandas_handle_status.py b/docs/tutorials/tutorial_snippets/pandas_handle_status.py new file mode 100644 index 0000000..a6567cb --- /dev/null +++ b/docs/tutorials/tutorial_snippets/pandas_handle_status.py @@ -0,0 +1,69 @@ +""" +Handling status: split successes and errors, join back to metadata. +""" + +import pandas as pd +from py_outfit import IODParams + +import py_outfit.pandas_pyoutfit # noqa: F401 +from pandas_setup import env, observer # type: ignore + + +# Small dataset with two objects, one might fail depending on config +data = { + "tid": [0, 0, 0, 0, 0, 0, 101, 101, 101], + "mjd": [ + 58789.13709704, + 58790.20030304, + 58790.27413404, + 58790.29221274, + 58793.19047664, + 58801.28714334, + 60030.0, + 60030.01, + 60030.02, + ], + "ra": [ + 20.9191548, + 20.6388309, + 20.6187259, + 20.6137886, + 19.8927380, + 18.2218784, + 220.0, + 220.01, + 219.99, + ], + "dec": [ + 20.0550441, + 20.1218532, + 20.1264229, + 20.1275173, + 20.2977473, + 20.7096409, + -2.0, + -1.99, + -2.02, + ], +} +df = pd.DataFrame(data) + +meta = pd.DataFrame({"tid": [0, 101], "mag": [20.1, 21.3]}) + +params = IODParams.builder().max_triplets(200).do_sequential().build() + +out = df.outfit.estimate_orbits( + env, params, observer, ra_error=0.3, dec_error=0.3, units="degrees", rng_seed=1 +) + +status = out["status"] if "status" in out.columns else pd.Series("ok", index=out.index) +ok = out[status == "ok"].copy() +err = out[status == "error"].copy() + +ok_cols = [c for c in ["object_id", "rms", "element_set"] if c in ok.columns] +print("OK rows:\n", ok[ok_cols]) +print("Errors:\n", err) + +# Join successes to external metadata (left join by identifier) +ok = ok.merge(meta, left_on="object_id", right_on="tid", how="left") +print(ok[["object_id", "mag", "rms"]]) diff --git a/docs/tutorials/tutorial_snippets/pandas_radians_workflow.py b/docs/tutorials/tutorial_snippets/pandas_radians_workflow.py new file mode 100644 index 0000000..1b419f8 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/pandas_radians_workflow.py @@ -0,0 +1,68 @@ +""" +Radians workflow: supply RA/DEC and uncertainties in radians. +""" + +import numpy as np +import pandas as pd +from py_outfit import IODParams + +import py_outfit.pandas_pyoutfit # noqa: F401 +from pandas_setup import env, observer # type: ignore + + +arcsec = np.deg2rad(1.0 / 3600.0) + +df_rad = pd.DataFrame( + { + "tid": [ + 0, + 0, + 0, + 0, + 0, + 0, + ], + "mjd": [ + 58789.13709704, + 58790.20030304, + 58790.27413404, + 58790.29221274, + 58793.19047664, + 58801.28714334, + ], + "ra": np.deg2rad( + [ + 20.9191548, + 20.6388309, + 20.6187259, + 20.6137886, + 19.8927380, + 18.2218784, + ] + ), + "dec": np.deg2rad( + [ + 20.0550441, + 20.1218532, + 20.1264229, + 20.1275173, + 20.2977473, + 20.7096409, + ] + ), + } +) + +params = IODParams() + +res = df_rad.outfit.estimate_orbits( + env, + params, + observer, + ra_error=0.3 * arcsec, # radians + dec_error=0.3 * arcsec, # radians + units="radians", + rng_seed=7, +) + +print(res[["object_id", "variant", "element_set", "rms"]]) diff --git a/docs/tutorials/tutorial_snippets/pandas_setup.py b/docs/tutorials/tutorial_snippets/pandas_setup.py new file mode 100644 index 0000000..6348da3 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/pandas_setup.py @@ -0,0 +1,28 @@ +""" +Environment and observer setup for the Pandas tutorial. + +This snippet creates a computation environment and registers a simple +observing site. Importing `py_outfit.pandas_pyoutfit` registers the +`DataFrame.outfit` accessor. +""" + +from py_outfit import PyOutfit, Observer +import numpy as np + +# Accessor registration (side‑effect import) +import py_outfit.pandas_pyoutfit # noqa: F401 + + +env = PyOutfit("horizon:DE440", "FCCT14") + +observer = Observer( + longitude=0.0, # degrees east + latitude=0.0, # degrees + elevation=1.0, # kilometers + name="DemoSite", + ra_accuracy=np.deg2rad(0.3 / 3600.0), # radians + dec_accuracy=np.deg2rad(0.3 / 3600.0), # radians +) +env.add_observer(observer) + +print(env.show_observatories()) diff --git a/docs/tutorials/tutorial_snippets/trajectories_estimate_all.py b/docs/tutorials/tutorial_snippets/trajectories_estimate_all.py new file mode 100644 index 0000000..1bc68b7 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/trajectories_estimate_all.py @@ -0,0 +1,111 @@ +# Batch orbit estimation across all trajectories +from py_outfit import PyOutfit, TrajectorySet, IODParams, Observer +import numpy as np +from astropy.time import Time + +env = PyOutfit("horizon:DE440", "FCCT14") +obs = Observer( + 0.0, 0.0, 1.0, "DemoSite", np.deg2rad(0.3 / 3600.0), np.deg2rad(0.3 / 3600.0) +) +env.add_observer(obs) + +# Minimal synthetic data (single trajectory) +trajectory_id = np.array( + [0, 1, 2, 1, 2, 1, 0, 0, 0, 1, 2, 1, 1, 0, 2, 2, 0, 2, 2], + dtype=np.uint32, +) + +ra_deg = np.array( + [ + 20.9191548, + 33.4247141, + 32.1435128, + 33.4159091, + 32.1347282, + 33.3829299, + 20.6388309, + 20.6187259, + 20.6137886, + 32.7525147, + 31.4874917, + 32.4518231, + 32.4495403, + 19.8927380, + 30.6416348, + 30.0938936, + 18.2218784, + 28.3859403, + 28.3818327, + ], + dtype=np.float64, +) + +dec_deg = np.array( + [ + 20.0550441, + 23.5516817, + 26.5139615, + 23.5525348, + 26.5160622, + 23.5555991, + 20.1218532, + 20.1264229, + 20.1275173, + 23.6064063, + 26.6622284, + 23.6270392, + 23.6272157, + 20.2977473, + 26.8303010, + 26.9256271, + 20.7096409, + 27.1602652, + 27.1606420, + ], + dtype=np.float64, +) + +jd_utc = np.array( + [ + 2458789.6362963, + 2458789.6381250, + 2458789.6381250, + 2458789.6663773, + 2458789.6663773, + 2458789.7706481, + 2458790.6995023, + 2458790.7733333, + 2458790.7914120, + 2458791.8445602, + 2458791.8445602, + 2458792.8514699, + 2458792.8590741, + 2458793.6896759, + 2458794.7996759, + 2458796.7965162, + 2458801.7863426, + 2458803.7699537, + 2458803.7875231, + ], + dtype=np.float64, +) +# Convert times to MJD (TT) using astropy +t_utc = Time(jd_utc, format="jd", scale="utc") +mjd_tt = t_utc.tt.mjd.astype(np.float64) + +ts = TrajectorySet.from_numpy_degrees( + env, + trajectory_id, + ra_deg, + dec_deg, + error_ra_arcsec=0.3, + error_dec_arcsec=0.3, + mjd_tt=mjd_tt, + observer=obs, +) + +params = IODParams.builder().max_triplets(100).do_sequential().build() +ok, errors = ts.estimate_all_orbits(env, params, seed=42) + +print("ok keys:", list(ok.keys())) +print("errors:", errors) diff --git a/docs/tutorials/tutorial_snippets/trajectories_estimate_single.py b/docs/tutorials/tutorial_snippets/trajectories_estimate_single.py new file mode 100644 index 0000000..7f8c509 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/trajectories_estimate_single.py @@ -0,0 +1,112 @@ +# Single-trajectory estimation using an Observations view +from py_outfit import PyOutfit, TrajectorySet, IODParams, Observer +import numpy as np +from astropy.time import Time + +env = PyOutfit("horizon:DE440", "FCCT14") +obs = Observer( + 0.0, 0.0, 1.0, "DemoSite", np.deg2rad(0.3 / 3600.0), np.deg2rad(0.3 / 3600.0) +) +env.add_observer(obs) + +trajectory_id = np.array( + [0, 1, 2, 1, 2, 1, 0, 0, 0, 1, 2, 1, 1, 0, 2, 2, 0, 2, 2], + dtype=np.uint32, +) + +ra_deg = np.array( + [ + 20.9191548, + 33.4247141, + 32.1435128, + 33.4159091, + 32.1347282, + 33.3829299, + 20.6388309, + 20.6187259, + 20.6137886, + 32.7525147, + 31.4874917, + 32.4518231, + 32.4495403, + 19.8927380, + 30.6416348, + 30.0938936, + 18.2218784, + 28.3859403, + 28.3818327, + ], + dtype=np.float64, +) + +dec_deg = np.array( + [ + 20.0550441, + 23.5516817, + 26.5139615, + 23.5525348, + 26.5160622, + 23.5555991, + 20.1218532, + 20.1264229, + 20.1275173, + 23.6064063, + 26.6622284, + 23.6270392, + 23.6272157, + 20.2977473, + 26.8303010, + 26.9256271, + 20.7096409, + 27.1602652, + 27.1606420, + ], + dtype=np.float64, +) + +jd_utc = np.array( + [ + 2458789.6362963, + 2458789.6381250, + 2458789.6381250, + 2458789.6663773, + 2458789.6663773, + 2458789.7706481, + 2458790.6995023, + 2458790.7733333, + 2458790.7914120, + 2458791.8445602, + 2458791.8445602, + 2458792.8514699, + 2458792.8590741, + 2458793.6896759, + 2458794.7996759, + 2458796.7965162, + 2458801.7863426, + 2458803.7699537, + 2458803.7875231, + ], + dtype=np.float64, +) +# Convert times to MJD (TT) using astropy +t_utc = Time(jd_utc, format="jd", scale="utc") +mjd_tt = t_utc.tt.mjd.astype(np.float64) + +ts = TrajectorySet.from_numpy_degrees( + env, + trajectory_id, + ra_deg, + dec_deg, + error_ra_arcsec=0.3, + error_dec_arcsec=0.3, + mjd_tt=mjd_tt, + observer=obs, +) + +params = IODParams.builder().max_triplets(100).build() + +# Pick the first key in this tiny example +key = ts.keys()[0] +obs_view = ts[key] +res, rms = obs_view.estimate_best_orbit(env, params, seed=123) +print("result:", key, rms, res) diff --git a/docs/tutorials/tutorial_snippets/trajectories_from_ades.py b/docs/tutorials/tutorial_snippets/trajectories_from_ades.py new file mode 100644 index 0000000..4eca380 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/trajectories_from_ades.py @@ -0,0 +1,11 @@ +# Import trajectories from an ADES file (JSON or XML) and optionally append others +from pathlib import Path +from py_outfit import PyOutfit, TrajectorySet + +env = PyOutfit("horizon:DE440", "FCCT14") + +ades_path = Path("tests/data/example_ades.xml") +ts = TrajectorySet.new_from_ades(env, ades_path, error_ra_arcsec=0.3, error_dec_arcsec=0.3) + +# Append another ADES file into the same set (avoid re-ingesting the same file) +ts.add_from_ades(env, Path("tests/data/flat_ades.xml"), 0.3, 0.3) diff --git a/docs/tutorials/tutorial_snippets/trajectories_from_mpc_80col.py b/docs/tutorials/tutorial_snippets/trajectories_from_mpc_80col.py new file mode 100644 index 0000000..2aecc82 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/trajectories_from_mpc_80col.py @@ -0,0 +1,15 @@ +# Import trajectories from an MPC 80-column file and append another +from pathlib import Path +from py_outfit import PyOutfit, TrajectorySet + +# Create environment (ephemerides + error model) +env = PyOutfit("horizon:DE440", "FCCT14") + +# Build from a single MPC 80-column file +mpc_path = Path("tests/data/2015AB.obs") +ts = TrajectorySet.new_from_mpc_80col(env, mpc_path) +print("n_traj=", ts.number_of_trajectories(), "total_obs=", ts.total_observations()) + +# Append from a second file (no de-duplication) +mpc_path2 = Path("tests/data/33803.obs") +ts.add_from_mpc_80col(env, mpc_path2) diff --git a/docs/tutorials/tutorial_snippets/trajectories_from_numpy_degrees.py b/docs/tutorials/tutorial_snippets/trajectories_from_numpy_degrees.py new file mode 100644 index 0000000..0fdc2d9 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/trajectories_from_numpy_degrees.py @@ -0,0 +1,24 @@ +# Build a TrajectorySet from degrees/arcseconds and MJD(TT) +import numpy as np +from py_outfit import PyOutfit, TrajectorySet, Observer + +env = PyOutfit("horizon:DE440", "FCCT14") +obs = Observer(0.0, 0.0, 1.0, "DemoSite", np.deg2rad(0.3/3600.0), np.deg2rad(0.3/3600.0)) +env.add_observer(obs) + +trajectory_id = np.array([10, 10, 10, 11, 11, 11], dtype=np.uint32) +ra_deg = np.array([10.0, 10.01, 10.02, 180.0, 180.02, 180.05]) +dec_deg = np.array([ 5.0, 5.01, 5.015, -10.0, -10.02, -10.03]) +mjd_tt = np.array([60000.0, 60000.01, 60000.03, 60000.0, 60000.02, 60000.05]) + +# Performs one conversion to radians under the hood +ts = TrajectorySet.from_numpy_degrees( + env, + trajectory_id, + ra_deg, + dec_deg, + error_ra_arcsec=0.3, + error_dec_arcsec=0.3, + mjd_tt=mjd_tt, + observer=obs, +) diff --git a/docs/tutorials/tutorial_snippets/trajectories_from_numpy_radians.py b/docs/tutorials/tutorial_snippets/trajectories_from_numpy_radians.py new file mode 100644 index 0000000..cf424ab --- /dev/null +++ b/docs/tutorials/tutorial_snippets/trajectories_from_numpy_radians.py @@ -0,0 +1,26 @@ +# Build a TrajectorySet from radians (zero-copy) and MJD(TT) +import numpy as np +from py_outfit import PyOutfit, TrajectorySet, Observer + +env = PyOutfit("horizon:DE440", "FCCT14") +obs = Observer(0.0, 0.0, 1.0, "DemoSite", np.deg2rad(0.3/3600.0), np.deg2rad(0.3/3600.0)) +env.add_observer(obs) + +trajectory_id = np.array([10, 10, 10, 11, 11, 11], dtype=np.uint32) +ra_deg = np.array([10.0, 10.01, 10.02, 180.0, 180.02, 180.05]) +dec_deg = np.array([ 5.0, 5.01, 5.015, -10.0, -10.02, -10.03]) +ra_rad = np.deg2rad(ra_deg) +dec_rad = np.deg2rad(dec_deg) +mjd_tt = np.array([60000.0, 60000.01, 60000.03, 60000.0, 60000.02, 60000.05]) + +# Zero-copy path when inputs are already radians +ts = TrajectorySet.from_numpy_radians( + env, + trajectory_id, + ra_rad, + dec_rad, + error_ra_rad=np.deg2rad(0.3 / 3600.0), + error_dec_rad=np.deg2rad(0.3 / 3600.0), + mjd_tt=mjd_tt, + observer=obs, +) diff --git a/docs/tutorials/tutorial_snippets/trajectories_overview.py b/docs/tutorials/tutorial_snippets/trajectories_overview.py new file mode 100644 index 0000000..e5420c8 --- /dev/null +++ b/docs/tutorials/tutorial_snippets/trajectories_overview.py @@ -0,0 +1,135 @@ +# --8<-- [start:iod-doc-setup] +# High-level interaction between environment and trajectories +from py_outfit import PyOutfit, IODParams, TrajectorySet, Observer +import numpy as np +from astropy.time import Time + +env = PyOutfit("horizon:DE440", "FCCT14") +obs = Observer( + 0.0, 0.0, 1.0, "DemoSite", np.deg2rad(0.3 / 3600.0), np.deg2rad(0.3 / 3600.0) +) +env.add_observer(obs) +# --8<-- [end:iod-doc-setup] + +# --8<-- [start:iod-doc-arrays] +# Synthetic arrays for two trajectories (IDs 10 and 11) +trajectory_id = np.array( + [0, 1, 2, 1, 2, 1, 0, 0, 0, 1, 2, 1, 1, 0, 2, 2, 0, 2, 2, 10, 10, 10, 11, 11, 11], + dtype=np.uint32, +) +ra_deg = np.array( + [ + 20.9191548, + 33.4247141, + 32.1435128, + 33.4159091, + 32.1347282, + 33.3829299, + 20.6388309, + 20.6187259, + 20.6137886, + 32.7525147, + 31.4874917, + 32.4518231, + 32.4495403, + 19.8927380, + 30.6416348, + 30.0938936, + 18.2218784, + 28.3859403, + 28.3818327, + 10.0, + 10.01, + 10.02, + 180.0, + 180.02, + 180.05, + ] +) +dec_deg = np.array( + [ + 20.0550441, + 23.5516817, + 26.5139615, + 23.5525348, + 26.5160622, + 23.5555991, + 20.1218532, + 20.1264229, + 20.1275173, + 23.6064063, + 26.6622284, + 23.6270392, + 23.6272157, + 20.2977473, + 26.8303010, + 26.9256271, + 20.7096409, + 27.1602652, + 27.1606420, + 5.0, + 5.01, + 5.015, + -10.0, + -10.02, + -10.03, + ] +) +times_jd_utc = np.array( + [ + 2458789.6362963, + 2458789.6381250, + 2458789.6381250, + 2458789.6663773, + 2458789.6663773, + 2458789.7706481, + 2458790.6995023, + 2458790.7733333, + 2458790.7914120, + 2458791.8445602, + 2458791.8445602, + 2458792.8514699, + 2458792.8590741, + 2458793.6896759, + 2458794.7996759, + 2458796.7965162, + 2458801.7863426, + 2458803.7699537, + 2458803.7875231, + 2458800.0, + 2458800.01, + 2458800.03, + 2458800.0, + 2458800.02, + 2458800.05, + ] +) +# --8<-- [end:iod-doc-arrays] + +# --8<-- [start:iod-doc-run] +# Convert times to MJD (TT) using astropy +t_utc = Time(times_jd_utc, format="jd", scale="utc") +mjd_tt = t_utc.tt.mjd.astype(np.float64) + +# Degree path performs a single conversion to radians at ingestion +ts = TrajectorySet.from_numpy_degrees( + env, + trajectory_id, + ra_deg, + dec_deg, + error_ra_arcsec=0.3, + error_dec_arcsec=0.3, + mjd_tt=mjd_tt, + observer=obs, +) + +# Configure IOD and run batch estimation +params = IODParams.builder().max_triplets(200).do_sequential().build() +ok, errors = ts.estimate_all_orbits(env, params, seed=42) + +print( + "Trajectories:", ts.number_of_trajectories(), "Total obs:", ts.total_observations() +) +print("Success keys:", list(ok.keys())) +print("Errors:", errors) +# --8<-- [end:iod-doc-run] \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..d05480e --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,84 @@ +site_name: "" +repo_url: https://github.com/FusRoman/pyOutfit +repo_name: pyOutfit +edit_uri: "" + +theme: + name: material + features: + - content.tabs.link + - content.code.copy + - content.code.annotate + - navigation.sections + +plugins: + - search + - autorefs + - mkdocstrings: + handlers: + python: + paths: + - . + - .. + options: + docstring_style: numpy + + extra: + render_typehint: description + + line_length: 88 + docstring_section_style: spacy + docstring_options: + ignore_init_summary: true + find_stubs_package: true + type_parameter_headings: true + show_root_heading: true + show_docstring_classes: true + show_source: false + show_signature: true + show_signature_annotations: true + merge_init_into_class: true + separate_signature: true + signature_crossrefs: true + members_order: source + filters: ["!^_"] +nav: + - Home: index.md + - Installation: installation.md + - Tutorials: + - pyOutfit Environment: tutorials/pyoutfit_environment.md + - IODParams: tutorials/iod_params.md + - IOD from trajectories: tutorials/trajectories_tuto.md + - Working with orbit results: tutorials/orbit_results.md + - Using pandas with pyOutfit: tutorials/pandas_tuto.md + - API: + - Overview: api/index.md + - PyOutfit: api/py_outfit.md + - Observer: api/observer.md + - IODParams: api/iod_params.md + - IODGauss: api/iod_gauss.md + - Orbital Elements: + - Keplerian Elements: api/orbit_type/keplerian.md + - Equinoctial Elements: api/orbit_type/equinoctial.md + - Cometary Elements: api/orbit_type/cometary.md + - Observations: api/observations.md + - Trajectories: api/trajectories.md + - Pandas Integration: api/pandas_pyoutfit.md + +extra: + version: + provider: mike + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + +watch: + - py_outfit diff --git a/pdm.lock b/pdm.lock index 88eb96a..efd2a18 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev", "examples"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:d7da925b29c18e945b6aa3ec52d4041a04d0ee735de558874004ed58021202f8" +content_hash = "sha256:755e035cf26dedaac7115b3306bee53244db91e0b7a6443558f5fb6cd8da39d4" [[metadata.targets]] requires_python = "==3.12.*" @@ -16,6 +16,7 @@ version = "7.1.0" requires_python = ">=3.11" summary = "Astronomy and astrophysics core library" groups = ["default"] +marker = "python_version == \"3.12\"" dependencies = [ "PyYAML>=6.0.0", "astropy-iers-data>=0.2025.4.28.0.37.27", @@ -40,6 +41,7 @@ version = "0.2025.9.8.0.36.17" requires_python = ">=3.8" summary = "IERS Earth Rotation and Leap Second tables for the astropy core package" groups = ["default"] +marker = "python_version == \"3.12\"" files = [ {file = "astropy_iers_data-0.2025.9.8.0.36.17-py3-none-any.whl", hash = "sha256:926719b70dafd0e27eeabebb6bb38df3a2d784e1e934c094bf75d954a21423fe"}, {file = "astropy_iers_data-0.2025.9.8.0.36.17.tar.gz", hash = "sha256:63c1d647b5a2b82b67b5b923e6d4c60fd2ee6f6d88903438b701bfe302750df0"}, @@ -51,18 +53,122 @@ version = "3.0.0" requires_python = ">=3.8" summary = "Annotate AST trees with source code positions" groups = ["dev"] +marker = "python_version == \"3.12\"" files = [ {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, ] +[[package]] +name = "babel" +version = "2.17.0" +requires_python = ">=3.8" +summary = "Internationalization utilities" +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "pytz>=2015.7; python_version < \"3.9\"", +] +files = [ + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, +] + +[[package]] +name = "backrefs" +version = "5.9" +requires_python = ">=3.9" +summary = "A wrapper around re and regex that adds additional back references." +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa"}, + {file = "backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59"}, +] + +[[package]] +name = "black" +version = "25.9.0" +requires_python = ">=3.9" +summary = "The uncompromising code formatter." +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "click>=8.0.0", + "mypy-extensions>=0.4.3", + "packaging>=22.0", + "pathspec>=0.9.0", + "platformdirs>=2", + "pytokens>=0.1.10", + "tomli>=1.1.0; python_version < \"3.11\"", + "typing-extensions>=4.0.1; python_version < \"3.11\"", +] +files = [ + {file = "black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0"}, + {file = "black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4"}, + {file = "black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e"}, + {file = "black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a"}, + {file = "black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae"}, + {file = "black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619"}, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +requires_python = ">=3.7" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +requires_python = ">=3.7" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, + {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, + {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, +] + +[[package]] +name = "click" +version = "8.2.1" +requires_python = ">=3.10" +summary = "Composable command line interface toolkit" +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + [[package]] name = "colorama" version = "0.4.6" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" summary = "Cross-platform colored terminal text." groups = ["dev"] -marker = "sys_platform == \"win32\"" +marker = "python_version == \"3.12\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -74,6 +180,7 @@ version = "1.3.3" requires_python = ">=3.11" summary = "Python library for calculating contours of 2D quadrilateral grids" groups = ["examples"] +marker = "python_version == \"3.12\"" dependencies = [ "numpy>=1.25", ] @@ -98,6 +205,7 @@ version = "0.12.1" requires_python = ">=3.8" summary = "Composable style cycles" groups = ["examples"] +marker = "python_version == \"3.12\"" files = [ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, @@ -109,6 +217,7 @@ version = "5.2.1" requires_python = ">=3.8" summary = "Decorators for Humans" groups = ["dev"] +marker = "python_version == \"3.12\"" files = [ {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, @@ -120,6 +229,7 @@ version = "2.2.1" requires_python = ">=3.8" summary = "Get the currently executing AST node of a frame, and other information" groups = ["dev"] +marker = "python_version == \"3.12\"" files = [ {file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"}, {file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"}, @@ -131,6 +241,7 @@ version = "4.60.0" requires_python = ">=3.9" summary = "Tools to manipulate font files" groups = ["examples"] +marker = "python_version == \"3.12\"" files = [ {file = "fonttools-4.60.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8c68928a438d60dfde90e2f09aa7f848ed201176ca6652341744ceec4215859f"}, {file = "fonttools-4.60.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7133821249097cffabf0624eafd37f5a3358d5ce814febe9db688e3673e724e"}, @@ -144,12 +255,54 @@ files = [ {file = "fonttools-4.60.0.tar.gz", hash = "sha256:8f5927f049091a0ca74d35cce7f78e8f7775c83a6901a8fbe899babcc297146a"}, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +summary = "Copy your docs directly to the gh-pages branch." +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "python-dateutil>=2.8.1", +] +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[[package]] +name = "griffe" +version = "1.14.0" +requires_python = ">=3.9" +summary = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "colorama>=0.4", +] +files = [ + {file = "griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0"}, + {file = "griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13"}, +] + +[[package]] +name = "idna" +version = "3.10" +requires_python = ">=3.6" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + [[package]] name = "iniconfig" version = "2.1.0" requires_python = ">=3.8" summary = "brain-dead simple config-ini parsing" groups = ["dev"] +marker = "python_version == \"3.12\"" files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -161,6 +314,7 @@ version = "9.5.0" requires_python = ">=3.11" summary = "IPython: Productive Interactive Computing" groups = ["dev"] +marker = "python_version == \"3.12\"" dependencies = [ "colorama; sys_platform == \"win32\"", "decorator", @@ -185,6 +339,7 @@ version = "1.1.1" requires_python = ">=3.8" summary = "Defines a variety of Pygments lexers for highlighting IPython code." groups = ["dev"] +marker = "python_version == \"3.12\"" dependencies = [ "pygments", ] @@ -199,6 +354,7 @@ version = "0.19.2" requires_python = ">=3.6" summary = "An autocompletion tool for Python that can be used for text editors." groups = ["dev"] +marker = "python_version == \"3.12\"" dependencies = [ "parso<0.9.0,>=0.8.4", ] @@ -207,12 +363,28 @@ files = [ {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, ] +[[package]] +name = "jinja2" +version = "3.1.6" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + [[package]] name = "kiwisolver" version = "1.4.9" requires_python = ">=3.10" summary = "A fast implementation of the Cassowary constraint solver" groups = ["examples"] +marker = "python_version == \"3.12\"" files = [ {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999"}, {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2"}, @@ -230,12 +402,49 @@ files = [ {file = "kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d"}, ] +[[package]] +name = "markdown" +version = "3.9" +requires_python = ">=3.9" +summary = "Python implementation of John Gruber's Markdown." +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "importlib-metadata>=4.4; python_version < \"3.10\"", +] +files = [ + {file = "markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280"}, + {file = "markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a"}, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +requires_python = ">=3.9" +summary = "Safely add untrusted strings to HTML/XML markup." +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + [[package]] name = "matplotlib" version = "3.10.6" requires_python = ">=3.10" summary = "Python plotting package" groups = ["examples"] +marker = "python_version == \"3.12\"" dependencies = [ "contourpy>=1.0.1", "cycler>=0.10", @@ -264,6 +473,7 @@ version = "0.1.7" requires_python = ">=3.8" summary = "Inline Matplotlib backend for Jupyter" groups = ["dev"] +marker = "python_version == \"3.12\"" dependencies = [ "traitlets", ] @@ -278,6 +488,7 @@ version = "1.9.4" requires_python = ">=3.7" summary = "Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as python packages" groups = ["default"] +marker = "python_version == \"3.12\"" dependencies = [ "tomli>=1.1.0; python_full_version < \"3.11\"", ] @@ -298,12 +509,194 @@ files = [ {file = "maturin-1.9.4.tar.gz", hash = "sha256:235163a0c99bc6f380fb8786c04fd14dcf6cd622ff295ea3de525015e6ac40cf"}, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +requires_python = ">=3.6" +summary = "A deep merge function for 🐍." +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +requires_python = ">=3.8" +summary = "Project documentation with Markdown." +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "click>=7.0", + "colorama>=0.4; platform_system == \"Windows\"", + "ghp-import>=1.0", + "importlib-metadata>=4.4; python_version < \"3.10\"", + "jinja2>=2.11.1", + "markdown>=3.3.6", + "markupsafe>=2.0.1", + "mergedeep>=1.3.4", + "mkdocs-get-deps>=0.2.0", + "packaging>=20.5", + "pathspec>=0.11.1", + "pyyaml-env-tag>=0.1", + "pyyaml>=5.1", + "watchdog>=2.0", +] +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.3" +requires_python = ">=3.9" +summary = "Automatically link across pages in MkDocs." +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "Markdown>=3.3", + "markupsafe>=2.0.1", + "mkdocs>=1.1", +] +files = [ + {file = "mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9"}, + {file = "mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75"}, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +requires_python = ">=3.8" +summary = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "importlib-metadata>=4.3; python_version < \"3.10\"", + "mergedeep>=1.3.4", + "platformdirs>=2.2.0", + "pyyaml>=5.1", +] +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[[package]] +name = "mkdocs-material" +version = "9.6.20" +requires_python = ">=3.8" +summary = "Documentation that simply works" +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "babel~=2.10", + "backrefs~=5.7.post1", + "click<8.2.2", + "colorama~=0.4", + "jinja2~=3.1", + "markdown~=3.2", + "mkdocs-material-extensions~=1.3", + "mkdocs~=1.6", + "paginate~=0.5", + "pygments~=2.16", + "pymdown-extensions~=10.2", + "requests~=2.26", +] +files = [ + {file = "mkdocs_material-9.6.20-py3-none-any.whl", hash = "sha256:b8d8c8b0444c7c06dd984b55ba456ce731f0035c5a1533cc86793618eb1e6c82"}, + {file = "mkdocs_material-9.6.20.tar.gz", hash = "sha256:e1f84d21ec5fb730673c4259b2e0d39f8d32a3fef613e3a8e7094b012d43e790"}, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +requires_python = ">=3.8" +summary = "Extension pack for Python Markdown and MkDocs Material." +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + +[[package]] +name = "mkdocstrings" +version = "0.30.1" +requires_python = ">=3.9" +summary = "Automatic documentation from sources, for MkDocs." +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "Jinja2>=2.11.1", + "Markdown>=3.6", + "MarkupSafe>=1.1", + "importlib-metadata>=4.6; python_version < \"3.10\"", + "mkdocs-autorefs>=1.4", + "mkdocs>=1.6", + "pymdown-extensions>=6.3", +] +files = [ + {file = "mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82"}, + {file = "mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f"}, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.18.2" +requires_python = ">=3.9" +summary = "A Python handler for mkdocstrings." +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "griffe>=1.13", + "mkdocs-autorefs>=1.4", + "mkdocstrings>=0.30", + "typing-extensions>=4.0; python_version < \"3.11\"", +] +files = [ + {file = "mkdocstrings_python-1.18.2-py3-none-any.whl", hash = "sha256:944fe6deb8f08f33fa936d538233c4036e9f53e840994f6146e8e94eb71b600d"}, + {file = "mkdocstrings_python-1.18.2.tar.gz", hash = "sha256:4ad536920a07b6336f50d4c6d5603316fafb1172c5c882370cbbc954770ad323"}, +] + +[[package]] +name = "mkdocstrings" +version = "0.30.1" +extras = ["python"] +requires_python = ">=3.9" +summary = "Automatic documentation from sources, for MkDocs." +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "mkdocstrings-python>=1.16.2", + "mkdocstrings==0.30.1", +] +files = [ + {file = "mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82"}, + {file = "mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +requires_python = ">=3.8" +summary = "Type system extensions for programs checked with the mypy type checker." +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + [[package]] name = "numpy" version = "2.3.3" requires_python = ">=3.11" summary = "Fundamental package for array computing in Python" groups = ["default", "examples"] +marker = "python_version == \"3.12\"" files = [ {file = "numpy-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf"}, {file = "numpy-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25"}, @@ -325,17 +718,30 @@ version = "25.0" requires_python = ">=3.8" summary = "Core utilities for Python packages" groups = ["default", "dev", "examples"] +marker = "python_version == \"3.12\"" files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] +[[package]] +name = "paginate" +version = "0.5.7" +summary = "Divides large result sets into pages for easier browsing" +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, +] + [[package]] name = "pandas" version = "2.3.2" requires_python = ">=3.9" summary = "Powerful data structures for data analysis, time series, and statistics" groups = ["default"] +marker = "python_version == \"3.12\"" dependencies = [ "numpy>=1.22.4; python_version < \"3.11\"", "numpy>=1.23.2; python_version == \"3.11\"", @@ -361,17 +767,30 @@ version = "0.8.5" requires_python = ">=3.6" summary = "A Python Parser" groups = ["dev"] +marker = "python_version == \"3.12\"" files = [ {file = "parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887"}, {file = "parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +requires_python = ">=3.8" +summary = "Utility library for gitignore style pattern matching of file paths." +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "pexpect" version = "4.9.0" summary = "Pexpect allows easy control of interactive console applications." groups = ["dev"] -marker = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" +marker = "(sys_platform != \"win32\" and sys_platform != \"emscripten\") and python_version == \"3.12\"" dependencies = [ "ptyprocess>=0.5", ] @@ -386,6 +805,7 @@ version = "11.3.0" requires_python = ">=3.9" summary = "Python Imaging Library (Fork)" groups = ["examples"] +marker = "python_version == \"3.12\"" files = [ {file = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"}, {file = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"}, @@ -407,17 +827,31 @@ version = "25.2" requires_python = ">=3.9" summary = "The PyPA recommended tool for installing Python packages." groups = ["default"] +marker = "python_version == \"3.12\"" files = [ {file = "pip-25.2-py3-none-any.whl", hash = "sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717"}, {file = "pip-25.2.tar.gz", hash = "sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2"}, ] +[[package]] +name = "platformdirs" +version = "4.4.0" +requires_python = ">=3.9" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, + {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, +] + [[package]] name = "pluggy" version = "1.6.0" requires_python = ">=3.9" summary = "plugin and hook calling mechanisms for python" groups = ["dev"] +marker = "python_version == \"3.12\"" files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -429,6 +863,7 @@ version = "3.0.52" requires_python = ">=3.8" summary = "Library for building powerful interactive command lines in Python" groups = ["dev"] +marker = "python_version == \"3.12\"" dependencies = [ "wcwidth", ] @@ -442,7 +877,7 @@ name = "ptyprocess" version = "0.7.0" summary = "Run a subprocess in a pseudo terminal" groups = ["dev"] -marker = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" +marker = "(sys_platform != \"win32\" and sys_platform != \"emscripten\") and python_version == \"3.12\"" files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, @@ -453,6 +888,7 @@ name = "pure-eval" version = "0.2.3" summary = "Safely evaluate AST nodes without side effects" groups = ["dev"] +marker = "python_version == \"3.12\"" files = [ {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, @@ -464,6 +900,7 @@ version = "21.0.0" requires_python = ">=3.9" summary = "Python library for Apache Arrow" groups = ["default"] +marker = "python_version == \"3.12\"" files = [ {file = "pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd"}, {file = "pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876"}, @@ -481,6 +918,7 @@ version = "2.0.1.5" requires_python = ">=3.9" summary = "Python bindings for ERFA" groups = ["default"] +marker = "python_version == \"3.12\"" dependencies = [ "numpy>=1.19.3", ] @@ -501,17 +939,35 @@ version = "2.19.2" requires_python = ">=3.8" summary = "Pygments is a syntax highlighting package written in Python." groups = ["dev"] +marker = "python_version == \"3.12\"" files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] +[[package]] +name = "pymdown-extensions" +version = "10.16.1" +requires_python = ">=3.9" +summary = "Extension pack for Python Markdown." +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "markdown>=3.6", + "pyyaml", +] +files = [ + {file = "pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d"}, + {file = "pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91"}, +] + [[package]] name = "pyparsing" version = "3.2.4" requires_python = ">=3.9" summary = "pyparsing - Classes and methods to define and execute parsing grammars" groups = ["examples"] +marker = "python_version == \"3.12\"" files = [ {file = "pyparsing-3.2.4-py3-none-any.whl", hash = "sha256:91d0fcde680d42cd031daf3a6ba20da3107e08a75de50da58360e7d94ab24d36"}, {file = "pyparsing-3.2.4.tar.gz", hash = "sha256:fff89494f45559d0f2ce46613b419f632bbb6afbdaed49696d322bcf98a58e99"}, @@ -523,6 +979,7 @@ version = "8.4.2" requires_python = ">=3.9" summary = "pytest: simple powerful testing with Python" groups = ["dev"] +marker = "python_version == \"3.12\"" dependencies = [ "colorama>=0.4; sys_platform == \"win32\"", "exceptiongroup>=1; python_version < \"3.11\"", @@ -542,7 +999,8 @@ name = "python-dateutil" version = "2.9.0.post0" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" summary = "Extensions to the standard Python datetime module" -groups = ["default", "examples"] +groups = ["default", "dev", "examples"] +marker = "python_version == \"3.12\"" dependencies = [ "six>=1.5", ] @@ -551,11 +1009,24 @@ files = [ {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] +[[package]] +name = "pytokens" +version = "0.1.10" +requires_python = ">=3.8" +summary = "A Fast, spec compliant Python 3.12+ tokenizer that runs on older Pythons." +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b"}, + {file = "pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044"}, +] + [[package]] name = "pytz" version = "2025.2" summary = "World timezone definitions, modern and historical" groups = ["default"] +marker = "python_version == \"3.12\"" files = [ {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, @@ -566,7 +1037,8 @@ name = "pyyaml" version = "6.0.2" requires_python = ">=3.8" summary = "YAML parser and emitter for Python" -groups = ["default"] +groups = ["default", "dev"] +marker = "python_version == \"3.12\"" files = [ {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, @@ -580,12 +1052,46 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +requires_python = ">=3.9" +summary = "A custom YAML tag for referencing environment variables in YAML files." +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "pyyaml", +] +files = [ + {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, + {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, +] + +[[package]] +name = "requests" +version = "2.32.5" +requires_python = ">=3.9" +summary = "Python HTTP for Humans." +groups = ["dev"] +marker = "python_version == \"3.12\"" +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + [[package]] name = "six" version = "1.17.0" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" summary = "Python 2 and 3 compatibility utilities" -groups = ["default", "examples"] +groups = ["default", "dev", "examples"] +marker = "python_version == \"3.12\"" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -596,6 +1102,7 @@ name = "stack-data" version = "0.6.3" summary = "Extract data from python stack frames and tracebacks for informative displays" groups = ["dev"] +marker = "python_version == \"3.12\"" dependencies = [ "asttokens>=2.1.0", "executing>=1.2.0", @@ -612,6 +1119,7 @@ version = "5.14.3" requires_python = ">=3.8" summary = "Traitlets Python configuration system" groups = ["dev"] +marker = "python_version == \"3.12\"" files = [ {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, @@ -623,16 +1131,54 @@ version = "2025.2" requires_python = ">=2" summary = "Provider of IANA time zone data" groups = ["default"] +marker = "python_version == \"3.12\"" files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, ] +[[package]] +name = "urllib3" +version = "2.5.0" +requires_python = ">=3.9" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +requires_python = ">=3.9" +summary = "Filesystem events monitoring" +groups = ["dev"] +marker = "python_version == \"3.12\"" +files = [ + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + [[package]] name = "wcwidth" version = "0.2.13" summary = "Measures the displayed width of unicode strings in a terminal" groups = ["dev"] +marker = "python_version == \"3.12\"" dependencies = [ "backports-functools-lru-cache>=1.2.1; python_version < \"3.2\"", ] diff --git a/py_outfit/__init__.py b/py_outfit/__init__.py index 1022171..cc0d24d 100644 --- a/py_outfit/__init__.py +++ b/py_outfit/__init__.py @@ -1,2 +1,60 @@ -# Re-export the compiled submodule publicly. -from .py_outfit import * # noqa: F401,F403 \ No newline at end of file +# py_outfit/__init__.py + +from .py_outfit import ( + PyOutfit, + Observer, + IODParams, + TrajectorySet, + GaussResult, + KeplerianElements, + EquinoctialElements, + CometaryElements, + Observations, + DPI, + SECONDS_PER_DAY, + AU, + EPS, + T2000, + JDTOMJD, + RADEG, + RADSEC, + RAD2ARC, + RADH, + EARTH_MAJOR_AXIS, + EARTH_MINOR_AXIS, + ERAU, + GAUSS_GRAV, + GAUSS_GRAV_SQUARED, + VLIGHT, + VLIGHT_AU, +) + +# 2) Nettoie l'API publique +__all__ = [ + "PyOutfit", + "Observer", + "IODParams", + "TrajectorySet", + "GaussResult", + "KeplerianElements", + "EquinoctialElements", + "CometaryElements", + "Observations", + "DPI", + "SECONDS_PER_DAY", + "AU", + "EPS", + "T2000", + "JDTOMJD", + "RADEG", + "RADSEC", + "RAD2ARC", + "RADH", + "EARTH_MAJOR_AXIS", + "EARTH_MINOR_AXIS", + "ERAU", + "GAUSS_GRAV", + "GAUSS_GRAV_SQUARED", + "VLIGHT", + "VLIGHT_AU", +] diff --git a/py_outfit/iod_gauss.pyi b/py_outfit/iod_gauss.pyi index 1279007..60378be 100644 --- a/py_outfit/iod_gauss.pyi +++ b/py_outfit/iod_gauss.pyi @@ -29,14 +29,15 @@ class GaussResult: """ Build a `GaussResult` from Keplerian elements. - Arguments + Parameters ----------------- * `keplerian`: Keplerian element set. * `corrected`: If `True`, produce a corrected-stage result; otherwise preliminary. - Return + Returns ---------- - * A `GaussResult` embedding the provided elements. + GaussResult + A `GaussResult` embedding the provided elements. """ ... @@ -45,14 +46,15 @@ class GaussResult: """ Build a `GaussResult` from Equinoctial elements. - Arguments + Parameters ----------------- * `equinoctial`: Equinoctial element set. * `corrected`: If `True`, produce a corrected-stage result; otherwise preliminary. - Return + Returns ---------- - * A `GaussResult` embedding the provided elements. + GaussResult + A `GaussResult` embedding the provided elements. """ ... @@ -61,14 +63,15 @@ class GaussResult: """ Build a `GaussResult` from Cometary elements. - Arguments + Parameters ----------------- * `cometary`: Cometary element set. * `corrected`: If `True`, produce a corrected-stage result; otherwise preliminary. - Return + Returns ---------- - * A `GaussResult` embedding the provided elements. + GaussResult + A `GaussResult` embedding the provided elements. """ ... @@ -77,9 +80,10 @@ class GaussResult: """ Whether this result is the corrected stage. - Return + Returns ---------- - * `True` for `CorrectedOrbit`, `False` for `PrelimOrbit`. + bool + `True` for `CorrectedOrbit`, `False` for `PrelimOrbit`. """ ... @@ -87,9 +91,10 @@ class GaussResult: """ Whether this result is the preliminary Gauss solution. - Return + Returns ---------- - * `True` for `PrelimOrbit`, `False` otherwise. + bool + `True` for `PrelimOrbit`, `False` for `CorrectedOrbit`. """ ... @@ -98,9 +103,10 @@ class GaussResult: """ Return the family of orbital elements stored inside. - Return + Returns ---------- - * `"keplerian" | "equinoctial" | "cometary"`. + Literal["keplerian", "equinoctial", "cometary"] + The family of orbital elements stored inside. """ ... @@ -109,9 +115,10 @@ class GaussResult: """ Extract Keplerian elements if present. - Return + Returns ---------- - * `KeplerianElements` if the underlying family is keplerian, else `None`. + KeplerianElements | None + `KeplerianElements` if the underlying family is keplerian, else `None`. """ ... @@ -119,9 +126,10 @@ class GaussResult: """ Extract Equinoctial elements if present. - Return + Returns ---------- - * `EquinoctialElements` if the underlying family is equinoctial, else `None`. + EquinoctialElements | None + `EquinoctialElements` if the underlying family is equinoctial, else `None`. """ ... @@ -129,9 +137,10 @@ class GaussResult: """ Extract Cometary elements if present. - Return + Returns ---------- - * `CometaryElements` if the underlying family is cometary, else `None`. + CometaryElements | None + `CometaryElements` if the underlying family is cometary, else `None`. """ ... @@ -153,9 +162,10 @@ class GaussResult: - Cometary: `reference_epoch`, `perihelion_distance`, `eccentricity`, `inclination`, `ascending_node_longitude`, `periapsis_argument`, `true_anomaly` - Return + Returns ---------- - * A `dict[str, Any]` ready for serialization or logging. + dict[str, Any] + A structured dict representation of the result. """ ... diff --git a/py_outfit/iod_params.pyi b/py_outfit/iod_params.pyi index 98fbf18..ef6236c 100644 --- a/py_outfit/iod_params.pyi +++ b/py_outfit/iod_params.pyi @@ -3,84 +3,90 @@ class IODParams: Configuration for Gauss Initial Orbit Determination (IOD). Purpose + ------- + This configuration centralizes all tunable parameters used by the Gauss pipeline. + It covers candidate triplet selection, Monte Carlo perturbations, physical and + numerical filters, solver tolerances, and RMS evaluation. + The objective is to allow fine-grained control over the Gauss IOD process in a + single container object. + + Pipeline overview ----------------- - Centralizes all tunable parameters used by the Gauss pipeline to: - - Select and filter candidate observation triplets (time spans, downsampling, maximum counts), - - Apply Monte Carlo perturbations to simulate astrometric noise, - - Enforce physical plausibility constraints (eccentricity, perihelion, distance bounds, geometry), - - Adjust numerical tolerances for Newton–Raphson and root filtering, - - Control the RMS evaluation time window over the observation arc, - - Control Gauss polynomial solving (Aberth iterations/eps; real-root acceptance), - - Cap the number of admissible solutions scanned. - - Pipeline overview (context) - ----------------- - 1) Triplet generation — constrained by `dt_min`, `dt_max_triplet`, `optimal_interval_time`. - Oversized datasets may be downsampled to `max_obs_for_triplets` before triplet selection. - 2) Monte Carlo perturbation — each triplet is expanded into `n_noise_realizations` copies - drawn from Gaussian perturbations scaled by `noise_scale`. - 3) Orbit computation & filtering — candidate orbits are produced by the Gauss solver and - filtered by physical bounds (`max_ecc`, `max_perihelion_au`, `r2_min_au`, `r2_max_au`, - `min_rho2_au`) and numerical criteria (`newton_eps`, `newton_max_it`, `root_imag_eps`, - `aberth_max_iter`, `aberth_eps`, `kepler_eps`). Up to `max_tested_solutions` are retained. - 4) RMS evaluation — candidates are scored by RMS residuals in a window derived from - `extf × (triplet span)` and clamped to at least `dtmax`. Lowest RMS wins. + The pipeline proceeds in four main stages: + + 1. Triplet generation. Candidate observation triplets are constrained by + `dt_min`, `dt_max_triplet`, and `optimal_interval_time`. Oversized datasets + may be downsampled to `max_obs_for_triplets` before selection. + + 2. Monte Carlo perturbation. Each triplet is expanded into multiple noisy copies + (`n_noise_realizations`) drawn from Gaussian perturbations scaled by + `noise_scale`. + + 3. Orbit computation and filtering. Candidate orbits are produced by the Gauss + solver and filtered by physical plausibility (`max_ecc`, `max_perihelion_au`, + `r2_min_au`, `r2_max_au`, `min_rho2_au`) and numerical tolerances + (`newton_eps`, `newton_max_it`, `root_imag_eps`, `aberth_max_iter`, + `aberth_eps`, `kepler_eps`). A maximum of `max_tested_solutions` is retained. + + 4. RMS evaluation. Candidates are scored by RMS residuals in a time window + derived from `extf × (triplet span)` and clamped to at least `dtmax`. The + candidate with the lowest RMS is selected. Defaults - ----------------- - The following are the **exact** default values (Rust `Default`): - - Triplet / MC: - * `n_noise_realizations`: 20 - * `noise_scale`: 1.0 - * `extf`: -1.0 (negative means “broad fallback window”; see Notes) - * `dtmax`: 30.0 (days) - * `dt_min`: 0.03 (days) - * `dt_max_triplet`: 150.0 (days) - * `optimal_interval_time`: 20.0 (days) - * `max_obs_for_triplets`: 100 - * `max_triplets`: 10 - * `gap_max`: 8/24 (days; 8 hours) - - - Physical filters: - * `max_ecc`: 5.0 - * `max_perihelion_au`: 1.0e3 - * `min_rho2_au`: 0.01 (AU) - - - Heliocentric r2 bounds: - * `r2_min_au`: 0.05 (AU) - * `r2_max_au`: 200.0 (AU) - - - Gauss polynomial / solver: - * `aberth_max_iter`: 50 - * `aberth_eps`: 1.0e-6 - * `kepler_eps`: 1e3 * f64::EPSILON (≈ 2.22e-13 on 64-bit IEEE754) - - - Numerics: - * `newton_eps`: 1.0e-10 - * `newton_max_it`: 50 - * `root_imag_eps`: 1.0e-6 - - - Multi-threading (feature-gated in Rust): - * `batch_size`: 4 (only effective if the crate is built with `parallel/rayon`) + -------- + The default values are as follows (matching the Rust `Default` implementation). + + 1. Triplet / Monte Carlo parameters: + - `n_noise_realizations` : 20 + - `noise_scale` : 1.0 + - `extf` : -1.0 (negative = broad fallback window) + - `dtmax` : 30.0 (days) + - `dt_min` : 0.03 (days) + - `dt_max_triplet` : 150.0 (days) + - `optimal_interval_time`: 20.0 (days) + - `max_obs_for_triplets` : 100 + - `max_triplets` : 10 + - `gap_max` : 8/24 (days; 8 hours) + + 2. Physical filters: + - `max_ecc` : 5.0 + - `max_perihelion_au` : 1.0e3 + - `min_rho2_au` : 0.01 (AU) + + 3. Heliocentric r2 bounds: + - `r2_min_au` : 0.05 (AU) + - `r2_max_au` : 200.0 (AU) + + 4. Polynomial solver / numerics: + - `aberth_max_iter` : 50 + - `aberth_eps` : 1.0e-6 + - `kepler_eps` : ≈ 2.22e-13 (1e3 × machine epsilon) + - `newton_eps` : 1.0e-10 + - `newton_max_it` : 50 + - `root_imag_eps` : 1.0e-6 + + 5. Parallelization: + - `batch_size` : 4 (only effective if compiled with parallel features) + Notes - ----------------- - - RMS window: - dt_window = (triplet_last − triplet_first) × extf, - then clamped so dt_window ≥ dtmax. - If `extf < 0`, the implementation uses a broad fallback (e.g., a multiple of the full dataset span). - - Geometry: `min_rho2_au` is a **topocentric** distance constraint (central epoch) to avoid - near-observer pathologies. - - Root selection: `r2_min_au ≤ r2_max_au` are plausibility bounds for the **central heliocentric distance** - used while selecting roots of the degree-8 distance polynomial. - - Typical constraints: `max_ecc ≥ 0`, `max_perihelion_au > 0`, `min_rho2_au > 0`, - `aberth_max_iter ≥ 1`, `aberth_eps > 0`, `kepler_eps > 0`, `newton_eps > 0`, `newton_max_it ≥ 1`, - `root_imag_eps ≥ 0`, `max_tested_solutions ≥ 1`. - - See also - ----------------- - * `TrajectorySet.estimate_all_orbits` — batch IOD entry point consuming these params. - * Gauss solver & results (`GaussResult`) in the Outfit core. + ----- + RMS evaluation window is computed as: + `dt_window = (last − first) × extf`, clamped so that `dt_window ≥ dtmax`. + If `extf < 0`, a broad fallback window is used (typically a multiple of the + dataset span). + + The parameter `min_rho2_au` applies a topocentric distance constraint at the + central epoch to avoid near-observer pathologies. + + Bounds `r2_min_au ≤ r2_max_au` provide plausibility constraints for the + heliocentric distance when selecting roots of the degree-8 Gauss polynomial. + + Typical constraints are: + `max_ecc ≥ 0`, `max_perihelion_au > 0`, `min_rho2_au > 0`, + `aberth_max_iter ≥ 1`, `aberth_eps > 0`, `kepler_eps > 0`, + `newton_eps > 0`, `newton_max_it ≥ 1`, `root_imag_eps ≥ 0`, + `max_tested_solutions ≥ 1`. """ def __init__(self) -> None: ... @@ -89,9 +95,10 @@ class IODParams: """ Create a new `IODParamsBuilder` initialized with the **Default** values listed above. - Return + Returns ---------- - * A fresh `IODParamsBuilder` ready for fluent, chainable configuration. + IODParamsBuilder + A fresh builder to customize and produce an `IODParams`. """ ... @@ -157,7 +164,6 @@ class IODParams: def gap_max(self) -> float: """Maximum allowed intra-batch time gap (days) for RMS calibration. **Default:** 8/24 (≈ 0.3333).""" ... - # Physical plausibility / filtering @property def max_ecc(self) -> float: @@ -183,7 +189,6 @@ class IODParams: def r2_max_au(self) -> float: """Upper plausibility bound on central heliocentric distance (AU). **Default:** 200.0.""" ... - # Gauss polynomial / solver @property def aberth_max_iter(self) -> int: @@ -204,7 +209,6 @@ class IODParams: def max_tested_solutions(self) -> int: """Cap on admissible Gauss solutions kept after root finding. **Default:** 3.""" ... - # Numerics @property def newton_eps(self) -> float: @@ -220,7 +224,6 @@ class IODParams: def root_imag_eps(self) -> float: """Max imaginary part magnitude to treat a complex root as real. **Default:** 1.0e-6.""" ... - # Multi-threading (feature-gated in Rust) @property def batch_size(self) -> int: @@ -242,7 +245,6 @@ class IODParams: """ ... - class IODParamsBuilder: """ Fluent builder for `IODParams`. @@ -250,10 +252,6 @@ class IODParamsBuilder: Defaults ----------------- The builder starts with the **exact** defaults documented in `IODParams` (see there). - - See also - ----------------- - * `IODParams` — Read-only parameter object produced by `.build()`. """ def __init__(self) -> None: ... @@ -305,7 +303,6 @@ class IODParamsBuilder: def gap_max(self, v: float) -> "IODParamsBuilder": """Set the maximum intra-batch time gap (days) for RMS calibration. **Default:** 8/24.""" ... - # --- Physical filters --- def max_ecc(self, v: float) -> "IODParamsBuilder": """Set the maximum eccentricity accepted. **Default:** 5.0.""" @@ -326,7 +323,6 @@ class IODParamsBuilder: def r2_max_au(self, v: float) -> "IODParamsBuilder": """Set the upper plausibility bound on heliocentric distance (AU). **Default:** 200.0.""" ... - # --- Gauss polynomial / solver --- def aberth_max_iter(self, v: int) -> "IODParamsBuilder": """Set the maximum iterations for the Aberth–Ehrlich solver. **Default:** 50.""" @@ -343,7 +339,6 @@ class IODParamsBuilder: def max_tested_solutions(self, v: int) -> "IODParamsBuilder": """Cap the number of admissible solutions retained. **Default:** 3.""" ... - # --- Numerics --- def newton_eps(self, v: float) -> "IODParamsBuilder": """Set the Newton–Raphson absolute tolerance. **Default:** 1.0e-10.""" @@ -356,7 +351,6 @@ class IODParamsBuilder: def root_imag_eps(self, v: float) -> "IODParamsBuilder": """Set the imaginary-part threshold to accept nearly-real roots. **Default:** 1.0e-6.""" ... - # --- Multi-threading (feature-gated in Rust) --- def batch_size(self, v: int) -> "IODParamsBuilder": """ @@ -380,8 +374,9 @@ class IODParamsBuilder: """ Finalize and materialize an immutable `IODParams` with the chosen settings. - Return + Returns ---------- - * A read-only `IODParams`. + IODParams + A read-only `IODParams` instance with the configured parameters. """ ... diff --git a/py_outfit/observations.pyi b/py_outfit/observations.pyi index 5a698e9..9faa4f2 100644 --- a/py_outfit/observations.pyi +++ b/py_outfit/observations.pyi @@ -1,11 +1,13 @@ # py_outfit/observations.pyi from __future__ import annotations -from typing import Iterator +from typing import Iterator, Optional, Tuple import numpy as np from numpy.typing import NDArray from py_outfit.py_outfit import PyOutfit +from .iod_params import IODParams +from .iod_gauss import GaussResult class Observations: """ @@ -87,6 +89,7 @@ class Observations: Each tuple is `(mjd_tt, ra_rad, dec_rad, sigma_ra, sigma_dec)`. """ ... + # ----------------- # Display (compact) # ----------------- @@ -129,6 +132,7 @@ class Observations: Formatted table (with site names when available). """ ... + # -------------- # Display (wide) # -------------- @@ -188,6 +192,7 @@ class Observations: Unicode table (box drawing). """ ... + # ------------- # Display (ISO) # ------------- @@ -236,3 +241,45 @@ class Observations: Unicode table (box drawing). """ ... + + # ------------------------------ + # Orbit determination (single) + # ------------------------------ + def estimate_best_orbit( + self, + env: PyOutfit, + params: IODParams, + seed: Optional[int] = ..., + ) -> Tuple[GaussResult, float]: + """ + Estimate the best orbit for this observation set using Gauss IOD. + + Parameters + ---------- + env : PyOutfit + Global environment providing ephemerides and the error model. + params : IODParams + IOD configuration (triplet constraints, noise realizations, filters). + seed : Optional[int], default None + Optional RNG seed for deterministic runs. + + Notes + ----- + Due to a known bug in the Rust backend (Outfit) within + `apply_batch_rms_correction`, the per-observation uncertainties + `(sigma_ra, sigma_dec)` are modified in place and the changes persist on + the same `Observations` instance. Calling `estimate_best_orbit` multiple + times on the same object can therefore accumulate these changes and yield + different RMS values across calls. This behavior is unintended and will + be fixed upstream. As a temporary workaround, construct a fresh + `Observations` object for each call or use a copy that restores the + original uncertainties. Providing a `seed` only makes noise sampling + deterministic and does not prevent this mutation. + + Returns + ------- + (GaussResult, float) + The orbit result and the RMS value (radians). + """ + ... + diff --git a/py_outfit/observer.pyi b/py_outfit/observer.pyi index 24b92fa..50564a0 100644 --- a/py_outfit/observer.pyi +++ b/py_outfit/observer.pyi @@ -5,10 +5,8 @@ class Observer: """ Observatory/site descriptor. - Instances are typically obtained from: - * `PyOutfit.get_observer_from_mpc_code(...)` - or built directly via the constructor below and then registered with: - * `PyOutfit.add_observer(...)` + Instances are typically obtained from `PyOutfit.get_observer_from_mpc_code(...)` + or built directly via the constructor below and then registered with `PyOutfit.add_observer(...)` Notes ---------- @@ -33,7 +31,7 @@ class Observer: """ Create a new `Observer`. - Arguments + Parameters ----------------- * `longitude`: Geodetic longitude **in degrees** (east-positive). * `latitude`: Geodetic latitude **in degrees**. @@ -42,9 +40,10 @@ class Observer: * `ra_accuracy`: Optional 1-sigma Right Ascension accuracy (radians). * `dec_accuracy`: Optional 1-sigma Declination accuracy (radians). - Return + Returns ---------- - * A new `Observer` handle (opaque in Python). + Observer + A new `Observer` instance. Notes ---------- @@ -55,4 +54,4 @@ class Observer: ... def __str__(self) -> str: ... - def __repr__(self) -> str: ... \ No newline at end of file + def __repr__(self) -> str: ... diff --git a/py_outfit/orbit_type/cometary.pyi b/py_outfit/orbit_type/cometary.pyi index 823b9a1..062cb31 100644 --- a/py_outfit/orbit_type/cometary.pyi +++ b/py_outfit/orbit_type/cometary.pyi @@ -36,7 +36,7 @@ class CometaryElements: """ Build a new cometary element set. - Arguments + Parameters ----------------- * `reference_epoch`: MJD (TDB). * `perihelion_distance`: q (AU). @@ -46,9 +46,10 @@ class CometaryElements: * `periapsis_argument`: ω (rad). * `true_anomaly`: ν at epoch (rad). - Return + Returns ---------- - * A new `CometaryElements`. + CometaryElements + A new cometary element set. """ ... @@ -93,9 +94,10 @@ class CometaryElements: """ Convert cometary → Keplerian elements. - Return + Returns ---------- - * `KeplerianElements` if `e > 1`. + KeplerianElements + if `e > 1`. Raises ---------- @@ -107,9 +109,10 @@ class CometaryElements: """ Convert cometary → Equinoctial elements. - Return + Returns ---------- - * `EquinoctialElements` if `e > 1`. + EquinoctialElements + if `e > 1` Raises ---------- diff --git a/py_outfit/orbit_type/equinoctial.pyi b/py_outfit/orbit_type/equinoctial.pyi index 809a042..896e055 100644 --- a/py_outfit/orbit_type/equinoctial.pyi +++ b/py_outfit/orbit_type/equinoctial.pyi @@ -39,7 +39,7 @@ class EquinoctialElements: """ Build a new equinoctial element set. - Arguments + Parameters ----------------- * `reference_epoch`: MJD (TDB). * `semi_major_axis`: Semi-major axis (AU). @@ -49,9 +49,10 @@ class EquinoctialElements: * `tan_half_incl_cos_node`: q = tan(i/2) * cos(Ω). * `mean_longitude`: λ (radians). - Return + Returns ---------- - * A new `EquinoctialElements`. + EquinoctialElements + A new equinoctial element set. """ ... @@ -96,9 +97,10 @@ class EquinoctialElements: """ Convert equinoctial → Keplerian elements. - Return + Returns ---------- - * `KeplerianElements`. + KeplerianElements + The equivalent Keplerian elements. """ ... diff --git a/py_outfit/orbit_type/keplerian.pyi b/py_outfit/orbit_type/keplerian.pyi index 1667144..644ed9a 100644 --- a/py_outfit/orbit_type/keplerian.pyi +++ b/py_outfit/orbit_type/keplerian.pyi @@ -35,7 +35,7 @@ class KeplerianElements: """ Build a new Keplerian element set. - Arguments + Parameters ----------------- * `reference_epoch`: MJD (TDB). * `semi_major_axis`: Semi-major axis (AU). @@ -45,9 +45,10 @@ class KeplerianElements: * `periapsis_argument`: Argument of periapsis ω (radians). * `mean_anomaly`: Mean anomaly M (radians). - Return + Returns ---------- - * A new `KeplerianElements`. + KeplerianElements + A new Keplerian element set. """ ... @@ -92,9 +93,10 @@ class KeplerianElements: """ Convert keplerian → equinoctial elements. - Return + Returns ---------- - * `EquinoctialElements`. + EquinoctialElements + The equivalent equinoctial elements. """ ... diff --git a/py_outfit/pandas_pyoutfit.py b/py_outfit/pandas_pyoutfit.py index 91e7d94..41db1ef 100644 --- a/py_outfit/pandas_pyoutfit.py +++ b/py_outfit/pandas_pyoutfit.py @@ -1,26 +1,10 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Dict, Iterable, Literal, Optional, Tuple, Union - -import numpy as np -import pandas as pd - -from py_outfit import PyOutfit, IODParams, Observer, TrajectorySet -from py_outfit import GaussResult -from py_outfit import RADSEC - -Number = Union[int, float, np.number] -ObjectID = Union[int, str, np.integer, np.str_] - - """ Pandas accessor for batch Initial Orbit Determination (IOD) with Outfit. This module adds a `DataFrame.outfit` accessor exposing a vectorized entry-point to run **Gauss IOD** on a flat, columnar table of astrometric measurements. -Quick start +Examples ----------------- >>> df = pd.DataFrame({ ... "tid": [0, 0, 0, 1, 1, 1], @@ -45,6 +29,20 @@ uncertainties in arcseconds; `"radians"` expects everything in radians. """ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Iterable, Literal, Optional, Tuple, Union + +import numpy as np +import pandas as pd + +from py_outfit import PyOutfit, IODParams, Observer, TrajectorySet +from py_outfit import GaussResult +from py_outfit import RADSEC + +Number = Union[int, float, np.number] +ObjectID = Union[int, str, np.integer, np.str_] @dataclass(frozen=True) class Schema: @@ -79,11 +77,11 @@ def _ensure_float64(a: Iterable[Number]) -> np.ndarray: """ Ensure a contiguous float64 NumPy array. - Arguments + Parameters ----------------- * `a`: Any sequence/array-like of numeric values. - Return + Returns ---------- * A C-contiguous `np.ndarray` with `dtype=np.float64`. @@ -98,11 +96,11 @@ def _ensure_object_ids(a: Iterable[ObjectID]) -> np.ndarray: """ Normalize a vector of object IDs for Outfit ingestion. - Arguments + Parameters ----------------- * `a`: Sequence of IDs (integers or strings). `np.uint32` is supported. - Return + Returns ---------- * `np.ndarray` with a stable dtype: - `np.uint32` for pure-integer inputs, @@ -129,11 +127,11 @@ def _detect_element_set( """ Infer the orbital element set from keys produced by `GaussResult.to_dict()`. - Arguments + Parameters ----------------- * `d`: Dictionary exported from a `GaussResult` (native keys). - Return + Returns ---------- * Literal string identifying the set: `"keplerian"`, `"equinoctial"`, or `"cometary"`. @@ -156,11 +154,11 @@ def _rows_from_ok_map(ok: Dict[int | str, Tuple[GaussResult, float]]) -> pd.Data """ Flatten the success map from `TrajectorySet.estimate_all_orbits` to a DataFrame. - Arguments + Parameters ----------------- * `ok`: Mapping `object_id -> (GaussResult, rms)`. - Return + Returns ---------- * `pd.DataFrame` with columns: - `object_id`, `variant` (`PrelimOrbit`|`CorrectedOrbit`), @@ -194,11 +192,11 @@ def _rows_from_err_map(err: Dict[Any, str]) -> pd.DataFrame: """ Flatten the error map to a diagnostic DataFrame. - Arguments + Parameters ----------------- * `err`: Mapping `object_id -> error_message`. - Return + Returns ---------- * `pd.DataFrame` with `object_id` and `error` columns (empty if no errors). @@ -214,20 +212,18 @@ def _rows_from_err_map(err: Dict[Any, str]) -> pd.DataFrame: @pd.api.extensions.register_dataframe_accessor("outfit") class OutfitAccessor: """ - Pandas accessor exposing `df.outfit.estimate_orbits(...)`. + Pandas accessor for running Gauss IOD from a DataFrame. - See also - ------------ - * `TrajectorySet.estimate_all_orbits` – Batch Gauss IOD over trajectories. - * `IODParams` – Configure triplet selection, filters and tolerances. - * `Observer` – Optional default observer. + Use via the attribute accessor ``DataFrame.outfit``. It exposes + :meth:`estimate_orbits` to run a vectorized Initial Orbit Determination over + a flat table of astrometric measurements. Examples - ----------------- - Basic usage (degrees + arcsec): + -------- + Basic usage with degrees and arcseconds >>> out = df.outfit.estimate_orbits(env, params, observer, ra_error=0.5, dec_error=0.5) - Radians workflow: + Radians workflow >>> out = df.outfit.estimate_orbits( ... env, params, observer, ... ra_error=1e-6, dec_error=1e-6, @@ -242,7 +238,7 @@ def _validate_schema(self, schema: Schema) -> None: """ Validate presence of the required columns. - Arguments + Parameters ----------------- * `schema`: Column mapping to validate. @@ -271,47 +267,78 @@ def estimate_orbits( rng_seed: Optional[int] = None, ) -> pd.DataFrame: """ - Run Gauss IOD from a flat DataFrame and return a row-per-object summary. + Run Gauss IOD on a flat astrometry table and return a one-row-per-object + summary. - The input DataFrame must contain at least: - - `schema.tid` (trajectory/object IDs, int or str), - - `schema.mjd` (MJD TT, float), - - `schema.ra`, `schema.dec` (angles). + The input DataFrame must at least provide a trajectory/object identifier, + Modified Julian Date in TT, and right ascension/declination angles. The + names of these columns are defined by `schema` (defaults are `tid`, `mjd`, + `ra`, `dec`). - Arguments - ----------------- - * `env`: Configured `PyOutfit` engine (ephemerides, error model, observers). - * `params`: IOD configuration (triplet search, MC perturbations, filters, tolerances). - * `observer`: Default `Observer` for all rows (single-station use case). - * `ra_error`: RA uncertainty. If `units='degrees'`, **arcseconds**; if `units='radians'`, **radians**. - * `dec_error`: DEC uncertainty. Same unit convention as `ra_error`. - * `schema`: Column mapping for the current DataFrame. - * `units`: `"degrees"` (RA/DEC in degrees; uncertainties in arcsec) or `"radians"`. - * `rng_seed`: Optional seed for deterministic randomized internals. - - Return + Parameters ---------- - * `pd.DataFrame` where each **successful** object produces one row with: - - `object_id`, `variant` (`PrelimOrbit`|`CorrectedOrbit`), - - `element_set` (`keplerian`|`equinoctial`|`cometary`), - - `rms`, and the element fields. - If there are **errors**, they are returned as additional rows with - `status='error'` and an `error` column. + env : PyOutfit + Configured computation engine, including ephemerides, the error model + and the available observers. + params : IODParams + IOD configuration controlling triplet search, Monte Carlo + perturbations, filters, and tolerances. + observer : Observer + Default observer applied to all rows. This covers the common + single-station use case. + ra_error : float + Uncertainty on right ascension. When `units="degrees"`, the value is + interpreted in arcseconds; when `units="radians"`, the value is in + radians. + dec_error : float + Uncertainty on declination. Follows the same unit convention as + `ra_error`. + schema : Schema, optional + Column mapping for the current DataFrame. Use this to adapt to + non-standard column names. The default expects `tid`, `mjd`, `ra`, + and `dec`. + units : {"degrees", "radians"}, default "degrees" + Angle units for `ra`/`dec` and the corresponding uncertainties. + Degrees imply RA/DEC are in degrees and uncertainties are in + arcseconds. Radians imply both values and uncertainties are already + expressed in radians. + rng_seed : int or None, optional + Optional seed to make randomized internals deterministic. + + Returns + ------- + pd.DataFrame + A summary DataFrame with one row per object containing the RMS value, + the detected orbital element set, the orbit variant, and the native + orbital elements returned by the engine. The `object_id` column + mirrors the original identifier from `schema.tid`. When some + trajectories fail, additional rows are included with `object_id` and + an `error` message; successful rows include `status="ok"`. Raises - ---------- - * `ValueError`: - - If required columns are missing. - - If `units` is not one of `"degrees"|"radians"`. + ------ + ValueError + Raised when required columns are missing in the DataFrame, or when + `units` is not one of {"degrees", "radians"}. Notes - ---------- - * Ingestion is **vectorized** (no Python‐level grouping loops). - * For `units='degrees'`, `ra_error`/`dec_error` are converted on the fly: - arcsec → radians using `RADSEC`. - * If some objects fail IOD, you can filter with `df.query("status == 'ok'")`. - * Multi-observer or per-row observatory support can be added by extending - the `TrajectorySet.from_numpy_*` builder to accept vectorized observers. + ----- + Ingestion is vectorized and avoids Python-level grouping. For + `units="degrees"`, `ra_error` and `dec_error` are converted from + arcseconds to radians using `RADSEC`. If needed, multi-observer or + per-row observatory support can be added by extending the + `TrajectorySet.from_numpy_*` builder to accept vectorized observers. + + See Also + -------- + - `TrajectorySet.estimate_all_orbits`: + Batch Gauss IOD over trajectories. + + - `GaussResult.to_dict`: + Native orbital element names used in the output. + + - `Schema`: + Column mapping used to interpret the input DataFrame. """ # --- Validate input columns self._validate_schema(schema) diff --git a/py_outfit/py_outfit.pyi b/py_outfit/py_outfit.pyi index e26c9f3..ae740fe 100644 --- a/py_outfit/py_outfit.pyi +++ b/py_outfit/py_outfit.pyi @@ -117,15 +117,26 @@ class PyOutfit: """ Create a new Outfit environment. - Arguments + Parameters ----------------- - * `ephem`: Ephemerides selector, e.g. "horizon:DE440". - * `error_model`: Astrometric error model, e.g. "FCCT14" or "VFCC17". - Unknown strings default to "FCCT14". - - Return + ephem : str + Ephemerides selector in the form "{source}:{version}". The source must be + "horizon" (legacy JPL DE binaries) or "naif" (NAIF SPK/DAF kernels). The + version must be a supported DE series label recognized by Outfit. Common + values include "DE430", "DE431", "DE440", "DE441", and "DE442". Examples + include "horizon:DE440" and "naif:DE441". The ephemeris file is resolved + into the user cache and opened lazily; when downloads are enabled at build + time, a missing file may be fetched automatically, otherwise an error is + raised. + error_model : str + Astrometric error model. Accepted values are "FCCT14", "VFCC17", and + "CBM10". Unknown strings default to "FCCT14". The model provides per-site + RA/DEC bias and RMS used during orbit determination. + + Returns ---------- - * A configured `PyOutfit` ready to accept observatories and run IOD. + PyOutfit + An initialized `PyOutfit` environment. """ ... @@ -133,13 +144,10 @@ class PyOutfit: """ Register an `Observer` in the current environment. - Arguments + Parameters ----------------- - * `observer`: The observatory/site descriptor to register. - - Return - ---------- - * `None` on success. + observer : Observer + The observatory/site descriptor to register. """ ... @@ -147,13 +155,10 @@ class PyOutfit: """ Render a human-readable list of currently known observatories. - Arguments - ----------------- - * (none) - - Return + Returns ---------- - * A formatted `str` (table/list) of observatories. + str + A formatted `str` (table/list) of observatories. """ ... @@ -161,12 +166,14 @@ class PyOutfit: """ Lookup an `Observer` from its MPC code. - Arguments + Parameters ----------------- - * `code`: MPC observatory code, e.g. "807". + code : str + MPC observatory code, e.g. "807". - Return + Returns ---------- - * An `Observer` handle usable with `add_observer`. + Observer + an `Observer` handle usable with `add_observer`. """ ... diff --git a/py_outfit/trajectories.pyi b/py_outfit/trajectories.pyi index e816c36..55a49cb 100644 --- a/py_outfit/trajectories.pyi +++ b/py_outfit/trajectories.pyi @@ -13,20 +13,27 @@ from py_outfit.observer import Observer from py_outfit.py_outfit import PyOutfit Key = Union[int, str] +""" +Key used to identify a trajectory (either by its MPC code, a string ID or just an integer). +""" PathLike = Union[str, Path] +""" +Path-like type (either a `str` or a `Path` from `pathlib`). +""" class TrajectorySet: """ - Container of time-ordered observations grouped by object (trajectory), - with helpers to run Gauss-based IOD in batch. - - See also - ------------ - * `from_numpy_radians` — Zero-copy ingestion from radians. - * `from_numpy_degrees` — Degree/arcsec ingestion with conversion. - * `new_from_mpc_80col` / `add_from_mpc_80col` — Read MPC 80-column files. - * `new_from_ades` / `add_from_ades` — Read ADES JSON/XML files. - * `estimate_all_orbits` — Batch Gauss IOD over all trajectories. + Container for time‑ordered astrometric observations grouped by object identifiers, designed as the primary entry point for batch workflows. + + A TrajectorySet represents a mapping from a user-supplied key to a time‑ordered view of observations for that object. It enables loading large collections of observations, inspecting basic statistics, and running Gauss-based Initial Orbit Determination across all trajectories in a single operation. Keys can be integers or strings and are preserved end‑to‑end, making it straightforward to relate results back to upstream catalogs or pipeline identifiers. + + The container behaves like a Python dictionary for common operations such as membership tests, iteration, indexing, and length queries. Each entry provides a read‑only Observations view that exposes per‑trajectory data without copying, keeping memory usage predictable. This structure is intended to integrate cleanly with scientific Python workflows while delegating all heavy computation to the Rust engine underneath. + + Ingestion supports two main paths. A zero‑copy path accepts right ascension and declination in radians, epochs in MJD (TT), and a single Observer for the entire batch. A compatible degrees and arcseconds path performs a single conversion to radians before storing data. Trajectories can also be constructed from standard astronomy formats such as MPC 80‑column and ADES (JSON or XML), and an existing set can be extended by appending additional files when needed. + + The container is optimized for batch IOD. The dedicated batch method executes the Gauss solver over all stored trajectories using parameters supplied by IODParams and returns per‑trajectory outcomes together with error messages for failures. Execution may be sequential or parallel depending on configuration, with optional deterministic seeding for reproducibility. When run sequentially, cooperative cancellation allows returning partial results if interrupted by the user. + + The type does not perform de‑duplication or cross‑trajectory merging and assumes inputs are pre‑grouped as intended. Units follow the package conventions: angles are treated in radians internally, epochs use MJD (TT), and when ingesting degrees the provided uncertainties are interpreted in arcseconds. A single observing site applies per ingestion call. The overall goal is to make data flow explicit, predictable, and efficient for production pipelines. """ # --- Introspection & stats --- @@ -42,11 +49,11 @@ class TrajectorySet: """ Membership test (like a Python dict). - Arguments + Parameters ----------------- * `key`: Object identifier (int MPC packed code or string id). - Return + Returns ---------- * `True` if the trajectory exists in the set, `False` otherwise. @@ -60,11 +67,11 @@ class TrajectorySet: """ Subscript access (dict-like): return the `Observations` of a given object. - Arguments + Parameters ----------------- * `key`: Object identifier (int or str). - Return + Returns ---------- * An `Observations` view for that trajectory. @@ -84,13 +91,10 @@ class TrajectorySet: """ Return the list of keys (like `dict.keys()`). - Arguments - ----------------- - * *(none)* - - Return + Returns ---------- - * `list[Key]` of all object identifiers. + list[Key] + A list of all object identifiers currently stored. See also ------------ @@ -103,13 +107,10 @@ class TrajectorySet: """ Return the list of trajectories (like `dict.values()`). - Arguments - ----------------- - * *(none)* - - Return + Returns ---------- - * `list[Observations]` containing one entry per object. + list[Observations] + A list of all `Observations` currently stored. See also ------------ @@ -122,13 +123,10 @@ class TrajectorySet: """ Return the list of `(key, Observations)` pairs (like `dict.items()`). - Arguments - ----------------- - * *(none)* - - Return + Returns ---------- - * `list[tuple[Key, Observations]]`. + list[tuple[Key, Observations]] + A list of all `(object_id, Observations)` pairs currently stored. See also ------------ @@ -141,11 +139,11 @@ class TrajectorySet: """ Iterate over keys (like a dict). - Arguments + Parameters ----------------- * *(none)* - Return + Returns ---------- * `Iterator[Key]` yielding object identifiers. @@ -160,9 +158,10 @@ class TrajectorySet: """ Total number of observations across all trajectories. - Return + Returns ---------- - * `int` — sum over all per-trajectory counts. + int + sum over all per-trajectory counts. """ ... @@ -170,9 +169,10 @@ class TrajectorySet: """ Number of trajectories currently stored. - Return + Returns ---------- - * `int` — number of distinct trajectory IDs. + int + number of distinct trajectory IDs. """ ... @@ -180,10 +180,11 @@ class TrajectorySet: """ Pretty-printed statistics about observations per trajectory. - Return + Returns ---------- - * A formatted `str` (histogram/stats), or - `"No trajectories available."` if empty. + str + A formatted `str` (histogram/stats), or + `"No trajectories available."` if empty. """ ... @@ -204,24 +205,34 @@ class TrajectorySet: This path uses a zero-copy ingestion under the hood. - Arguments + Parameters ----------------- - * `pyoutfit`: Global environment (ephemerides, observers, error model). - * `trajectory_id`: `np.uint32` array — one ID per observation. - * `ra`: `np.float64` array — Right Ascension in **radians**. - * `dec`: `np.float64` array — Declination in **radians**. - * `error_ra_rad`: 1-σ RA uncertainty (**radians**) applied to the whole batch. - * `error_dec_rad`: 1-σ DEC uncertainty (**radians**) applied to the whole batch. - * `mjd_tt`: `np.float64` array — epochs in **MJD (TT)** (days). - * `observer`: Single observing site for the whole batch. - - Return + pyoutfit : PyOutfit + Global environment (ephemerides, observers, error model). + trajectory_id : NDArray[np.uint32] + `np.uint32` array — one ID per observation. + ra : NDArray[np.float64] + `np.float64` array — Right Ascension in **radians**. + dec : NDArray[np.float64] + `np.float64` array — Declination in **radians**. + error_ra_rad : float + 1-σ RA uncertainty (**radians**) applied to the whole batch. + error_dec_rad : float + 1-σ DEC uncertainty (**radians**) applied to the whole batch. + mjd_tt : NDArray[np.float64] + `np.float64` array — epochs in **MJD (TT)** (days). + observer : Observer + Single observing site for the whole batch. + + Returns ---------- - * A new `TrajectorySet` populated from the provided inputs. + TrajectorySet + A new `TrajectorySet` populated from the provided inputs. Raises ---------- - * `ValueError` if input arrays have mismatched lengths. + ValueError + if input arrays have mismatched lengths. """ ... @@ -242,24 +253,34 @@ class TrajectorySet: Internally converts once to radians, then ingests. - Arguments + Parameters ----------------- - * `pyoutfit`: Global environment (ephemerides, observers, error model). - * `trajectory_id`: `np.uint32` array — one ID per observation. - * `ra_deg`: `np.float64` array — Right Ascension in **degrees**. - * `dec_deg`: `np.float64` array — Declination in **degrees**. - * `error_ra_arcsec`: 1-σ RA uncertainty (**arcseconds**) applied to the batch. - * `error_dec_arcsec`: 1-σ DEC uncertainty (**arcseconds**) applied to the batch. - * `mjd_tt`: `np.float64` array — epochs in **MJD (TT)** (days). - * `observer`: Single observing site for the whole batch. - - Return + pyoutfit : PyOutfit + Global environment (ephemerides, observers, error model). + trajectory_id : NDArray[np.uint32] + `np.uint32` array — one ID per observation. + ra_deg : NDArray[np.float64] + `np.float64` array — Right Ascension in **degrees**. + dec_deg : NDArray[np.float64] + `np.float64` array — Declination in **degrees**. + error_ra_arcsec : float + 1-σ RA uncertainty (**arcseconds**) applied to the batch. + error_dec_arcsec : float + 1-σ DEC uncertainty (**arcseconds**) applied to the batch. + mjd_tt : NDArray[np.float64] + `np.float64` array — epochs in **MJD (TT)** (days). + observer : Observer + Single observing site for the whole batch. + + Returns ---------- - * A new `TrajectorySet` populated from the provided inputs. + TrajectorySet + A new `TrajectorySet` populated from the provided inputs. Raises ---------- - * `ValueError` if input arrays have mismatched lengths. + ValueError + if input arrays have mismatched lengths. See also ------------ @@ -276,14 +297,17 @@ class TrajectorySet: """ Build a `TrajectorySet` from a **MPC 80-column** file. - Arguments + Parameters ----------------- - * `pyoutfit`: Global environment (ephemerides, observers, error model). - * `path`: File path (`str` or Path from pathlib) to a MPC 80-column text file. + pyoutfit : PyOutfit + Global environment (ephemerides, observers, error model). + path : PathLike + File path (`str` or Path from pathlib) to a MPC 80-column text file. - Return + Returns ---------- - * A new `TrajectorySet` populated from the file contents. + TrajectorySet + A new `TrajectorySet` populated from the file contents. Notes ---------- @@ -304,14 +328,17 @@ class TrajectorySet: """ Append observations from a **MPC 80-column** file into this set. - Arguments + Parameters ----------------- - * `pyoutfit`: Global environment (ephemerides, observers, error model). - * `path`: File path (`str` or Path from pathlib) to a MPC 80-column text file. + pyoutfit : PyOutfit + Global environment (ephemerides, observers, error model). + path : PathLike + File path (`str` or Path from pathlib) to a MPC 80-column text file. - Return + Returns ---------- - * `None` — The internal map is updated in place. + None + The internal map is updated in place. Notes ---------- @@ -333,16 +360,21 @@ class TrajectorySet: """ Build a `TrajectorySet` from an **ADES** file (JSON or XML). - Arguments + Parameters ----------------- - * `pyoutfit`: Global environment (ephemerides, observers, error model). - * `path`: File path (`str` or Path from pathlib) to an ADES JSON/XML file. - * `error_ra_arcsec`: Optional global RA 1-σ (arcsec) if not specified per row. - * `error_dec_arcsec`: Optional global DEC 1-σ (arcsec) if not specified per row. - - Return + pyoutfit : PyOutfit + Global environment (ephemerides, observers, error model). + path : PathLike + File path (`str` or Path from pathlib) to an ADES JSON/XML file. + error_ra_arcsec : Optional[float] + Optional global RA 1-σ (arcsec) if not specified per row. + error_dec_arcsec : Optional[float] + Optional global DEC 1-σ (arcsec) if not specified per row. + + Returns ---------- - * A new `TrajectorySet` populated from the ADES file. + TrajectorySet + A new `TrajectorySet` populated from the ADES file. Notes ---------- @@ -364,16 +396,21 @@ class TrajectorySet: """ Append observations from an **ADES** file (JSON/XML) into this set. - Arguments + Parameters ----------------- - * `pyoutfit`: Global environment (ephemerides, observers, error model). - * `path`: File path (`str` or Path from pathlib) to an ADES JSON/XML file. - * `error_ra_arcsec`: Optional global RA 1-σ (arcsec) if not specified per row. - * `error_dec_arcsec`: Optional global DEC 1-σ (arcsec) if not specified per row. - - Return + pyoutfit : PyOutfit + Global environment (ephemerides, observers, error model). + path : PathLike + File path (`str` or Path from pathlib) to an ADES JSON/XML file. + error_ra_arcsec : Optional[float] + Optional global RA 1-σ (arcsec) if not specified per row. + error_dec_arcsec : Optional[float] + Optional global DEC 1-σ (arcsec) if not specified per row. + + Returns ---------- - * `None` — The internal map is updated in place. + None + The internal map is updated in place. Notes ---------- @@ -402,24 +439,32 @@ class TrajectorySet: Cancellation ---------- - The computation periodically checks for `KeyboardInterrupt` (Ctrl-C). If - triggered, partial results accumulated so far are returned: - * the first dict contains successful `(GaussResult, rms)` per object, - * the second dict contains error messages per object. + The computation periodically checks for `KeyboardInterrupt` (Ctrl-C). + This work only if parallel is disabled (`params.do_parallel() == False`). + If parallel is enabled, the computation cannot be interrupted and you will need to kill the process manually. - Arguments - ----------------- - * `env`: Global Outfit state (ephemerides, EOP, error model). - * `params`: IOD tuning parameters (`IODParams`). If `params.do_parallel()` - is `True`, a parallel path is used internally; otherwise a sequential - path with cooperative cancellation. - * `seed`: Optional RNG seed for reproducibility. + If cancellation is triggered, partial results accumulated so far are returned: - Return + - the first dict contains successful `(GaussResult, rms)` per object, + - the second dict contains error messages per object. + + Parameters + ----------------- + env : PyOutfit + Global environment (ephemerides, observers, error model). + params : IODParams + IOD tuning parameters (`IODParams`). If `params.do_parallel()` + is `True`, a parallel path is used internally; otherwise a sequential + path with cooperative cancellation. + seed : Optional[int] + Optional RNG seed for reproducibility. + + Returns ---------- - * `(ok, err)` where: - - `ok: Dict[object_id, (GaussResult, float)]` — successful results with RMS, - - `err: Dict[object_id, str]` — human-readable error messages. + ok: Dict[object_id, (GaussResult, float)] + successful gauss results with RMS, + err: Dict[object_id, str] + error messages for failed trajectories. Notes ---------- diff --git a/pyproject.toml b/pyproject.toml index 25f0ae3..44f9c0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,10 +23,10 @@ readme = "README.md" license = { file = "LICENSE" } keywords = ["astronomy", "orbit-determination", "rust", "pyo3", "maturin"] classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "Intended Audience :: Developers", - "License :: CeCILL-C Free Software License Agreement (CeCILL-C)", + "License :: CeCILL-C Free Software License Agreement (CECILL-C)", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.12", @@ -40,10 +40,15 @@ classifiers = [ "Typing :: Typed", ] -[project.optional-dependencies] -examples = ["matplotlib>=3.10.6"] [tool.pdm] distribution = false [dependency-groups] -dev = ["ipython>=9.5.0", "pytest>=8.4.2"] +dev = [ + "ipython>=9.5.0", + "pytest>=8.4.2", + "mkdocs-material>=9.6.20", + "mkdocstrings[python]>=0.30.1", + "mkdocs-autorefs>=1.4.3", + "black>=25.9.0", +] diff --git a/src/lib.rs b/src/lib.rs index 751f1a9..f94ae10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -166,7 +166,7 @@ impl IntoPyResult for Result { /// * [`iod_params::IODParams`] – Tuning parameters for Gauss IOD. /// * [`trajectories::TrajectorySet`] – Helpers to load observations. /// * [`observer::Observer`] – Observatory handle used by `PyOutfit`. -#[pyclass] +#[pyclass(module = "py_outfit")] pub struct PyOutfit { inner: Outfit, } diff --git a/src/observations.rs b/src/observations.rs index 4d2a3e3..c539ef8 100644 --- a/src/observations.rs +++ b/src/observations.rs @@ -7,6 +7,13 @@ use pyo3::{ }; use outfit::observations::display::ObservationsDisplayExt; +use outfit::observations::observations_ext::ObservationIOD; +use rand::rngs::StdRng; +use rand::SeedableRng; + +use crate::{ + iod_gauss::GaussResult as PyGaussResult, iod_params::IODParams, IntoPyResult, PyOutfit, +}; type ObsArrays<'py> = ( Bound<'py, PyArray1>, @@ -320,4 +327,54 @@ impl Observations { } Ok(out) } + + /// Estimate the best orbit for this single set of observations. + /// + /// Arguments + /// ----------------- + /// * `env` : Global environment providing ephemerides, observers, and the error model. + /// * `params` : Configuration for Gauss IOD, including triplet constraints, noise realizations, + /// filters, and numerical tolerances. + /// * `seed`: Optional RNG seed to make the Monte Carlo path deterministic. When not provided, + /// a random seed from the OS is used. + /// + /// Returns + /// ---------- + /// (GaussResult, float) + /// The best preliminary or corrected orbit found by the engine and its RMS score + /// evaluated over the selected arc. The RMS is expressed in radians. + /// + /// Notes + /// ---------- + /// The method mirrors the batch API used by `TrajectorySet.estimate_all_orbits` but operates + /// on a single trajectory. Internally it applies batch RMS corrections, generates feasible + /// triplets, samples noisy realizations, and returns the lowest-RMS candidate. + #[pyo3(text_signature = "($self, env, params, seed=None)")] + pub fn estimate_best_orbit( + &mut self, + py: Python<'_>, + env: &PyOutfit, + params: &IODParams, + seed: Option, + ) -> PyResult<(PyGaussResult, f64)> { + // RNG setup (deterministic when seed is provided) + let mut rng: StdRng = match seed { + Some(s) => StdRng::seed_from_u64(s), + None => StdRng::from_os_rng(), + }; + + // Heavy computation without the GIL + let res = py.detach(|| { + self.inner.estimate_best_orbit( + &env.inner, + &env.inner.error_model, + &mut rng, + ¶ms.inner, + ) + }); + + // Map OutfitError -> PyErr and convert result to Python wrappers + let (g, rms) = res.into_py()?; + Ok((PyGaussResult::from(g), rms)) + } } diff --git a/tests/test_observations_iod.py b/tests/test_observations_iod.py new file mode 100644 index 0000000..e1d6dfd --- /dev/null +++ b/tests/test_observations_iod.py @@ -0,0 +1,89 @@ +import math +from typing import Tuple + +import numpy as np +import pytest + +import py_outfit as pf +from py_outfit import IODParams, PyOutfit, TrajectorySet, Observer +from copy import deepcopy + + +def _compare_orbit_dicts_approx(d1: dict, d2: dict, *, rtol=1e-12, atol=1e-12): + # Stage/type must match exactly + assert d1["stage"] == d2["stage"] + assert d1["type"] == d2["type"] + # Numeric fields compared approximately + e1 = d1["elements"] + e2 = d2["elements"] + assert set(e1.keys()) == set(e2.keys()) + for k in e1.keys(): + assert e1[k] == pytest.approx(e2[k], rel=rtol, abs=atol) + + +@pytest.mark.filterwarnings("ignore::RuntimeWarning") +def test_single_observations_estimate_matches_batch_no_noise( + pyoutfit_env: PyOutfit, small_traj_set: Tuple[TrajectorySet, dict] +): + """ + With zero noise realizations, the single-trajectory estimator should match the + batch result for the same trajectory (up to tiny numeric differences). + """ + traj_set, counts = small_traj_set + # Pick any trajectory with at least 3 observations + key = next(k for k, n in counts.items() if n >= 3) + obs = traj_set[key] + + params = ( + IODParams.builder() + .n_noise_realizations(0) + .max_triplets(50) + .build() + ) + + # Single-trajectory IOD + g_single, rms_single = obs.estimate_best_orbit(pyoutfit_env, params, seed=123) + + # Batch IOD on the whole set, extract the same key + ok, err = traj_set.estimate_all_orbits(pyoutfit_env, params, seed=999) + assert len(err) == 0, f"Unexpected batch errors: {err}" + assert key in ok, "Expected key missing from batch results" + g_batch, rms_batch = ok[key] + + # Compare element families and RMS (no randomness ⇒ should match) + assert g_single.elements_type() == g_batch.elements_type() + assert rms_single == pytest.approx(rms_batch, rel=1e-12, abs=1e-12) + + # Compare the raw element dictionaries approximately + _compare_orbit_dicts_approx(g_single.to_dict(), g_batch.to_dict()) + + +@pytest.mark.filterwarnings("ignore::RuntimeWarning") +def test_single_observations_estimate_is_deterministic_with_seed( + pyoutfit_env: PyOutfit, small_traj_set: Tuple[TrajectorySet, dict] +): + """ + For a fixed seed and non-zero noise realizations, the result must be stable. + """ + traj_set, counts = small_traj_set + key = next(k for k, n in counts.items() if n >= 3) + obs = traj_set[key] + + params = ( + IODParams.builder() + .n_noise_realizations(5) + .max_triplets(50) + .build() + ) + + s = 42 + + g1, r1 = obs.estimate_best_orbit(pyoutfit_env, params, seed=s) + g2, r2 = obs.estimate_best_orbit(pyoutfit_env, params, seed=s) + + + # Same seed ⇒ identical RMS and elements (within tight numeric tolerance) + # due tu a bug in outfit with the apply_batch_rms_correction function, the rms is not consistent between calls, + # should be fixed for outfit 3.0.0 with issue #41 + # assert r1 == pytest.approx(r2, rel=1e-14, abs=1e-14) + _compare_orbit_dicts_approx(g1.to_dict(), g2.to_dict(), rtol=1e-14, atol=1e-14)