Skip to content

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:

Setup environment and accessor
"""
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())

Degrees + arcseconds workflow

Your DataFrame provides tid, mjd, ra, dec. Angles are degrees and uncertainties are provided in arcseconds.

Minimal example (degrees + arcsec)
"""
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])

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.

Radians end-to-end
"""
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"]])

Custom column names with Schema

If your DataFrame uses different names, provide a Schema mapping.

Adapt to arbitrary column names
"""
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"]])

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.

Post-processing: statuses and joins
"""
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"]])

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