Files
2026-07-13 13:05:14 +08:00

215 lines
5.7 KiB
Python

"""Demonstrate common dataframe operations with a catalog server."""
# region: setup
from __future__ import annotations
from pathlib import Path
import datafusion as dfn
import numpy as np
import pyarrow as pa
from datafusion import col, lit
from datafusion import functions as F
import rerun as rr
sample_5_path = (
Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5"
)
server = rr.server.Server(datasets={"sample_dataset": sample_5_path})
CATALOG_URL = server.url()
client = rr.catalog.CatalogClient(CATALOG_URL)
dataset = client.get_dataset(name="sample_dataset")
observations = dataset.filter_contents(["/observation/**"]).reader(
index="real_time"
)
# endregion: setup
# region: group_by
first_last = observations.aggregate(
col("rerun_segment_id"),
[
F.first_value(col("real_time")).alias("start"),
F.last_value(col("real_time")).alias("end"),
],
)
# Sort for consistency here
first_last = first_last.sort("start")
pa.table(first_last)["start"][0]
# endregion: group_by
# region: join_query
joints = dataset.filter_contents(["/observation/joint_positions"])
# Find the earliest joint position in each episode (cast to unix epoch
# nanoseconds for easier math later)
joint_min_t = (
joints
.reader(index="real_time")
.with_column("joint_epoch_ns", col("real_time").cast(pa.int64()))
.select("rerun_segment_id", "joint_epoch_ns")
.aggregate(
col("rerun_segment_id"),
F.min(col("joint_epoch_ns")).alias("joint_min_t"),
)
)
cameras = dataset.filter_contents(["/camera/**"])
# Find the earliest camera frame in each episode (cast to unix epoch
# nanoseconds for easier math later)
camera_min_t = (
cameras
.reader(index="real_time")
.with_column("camera_epoch_ns", col("real_time").cast(pa.int64()))
.select(
"rerun_segment_id",
col("real_time").cast(pa.int64()).alias("camera_epoch_ns"),
)
.aggregate(
col("rerun_segment_id"),
F.min(col("camera_epoch_ns")).alias("camera_min_t"),
)
)
# Join the two dataframes
min_t = camera_min_t.join(
joint_min_t.with_column_renamed("rerun_segment_id", "segment_id"),
left_on="rerun_segment_id",
right_on="segment_id",
how="left",
)
delta_t = min_t.select(
col("rerun_segment_id"),
(col("camera_min_t") - col("joint_min_t")).alias("start_delta_t"),
)
THRESHOLD_S = 1
NANO_S = 1_000_000_000
outliers = delta_t.filter(
dfn.Expr.between(
col("start_delta_t"),
-THRESHOLD_S * NANO_S,
THRESHOLD_S * NANO_S,
negated=True,
),
)
outliers = outliers.with_column(
"start_delta_t_s", col("start_delta_t") / 1_000_000_000.0
)
print(
f"{outliers.count()=}\n",
f"{joint_min_t.count()=}\n",
f"{camera_min_t.count()=}",
sep="",
)
# endregion: join_query
# region: sub_episodes
# Grab a dataframe
all_data = (
dataset
.filter_contents(["/action/**", "/observation/**"])
.reader(index="real_time", fill_latest_at=True)
.filter(
col(
"/observation/joint_positions:Scalars:scalars"
).is_not_null() # filter out rows where there is no observation
)
)
# Drop heavy columns for performance
light_slice = all_data.select(
"rerun_segment_id",
"real_time",
"/observation/gripper_position:Scalars:scalars",
)
# Define criteria for sub-episode start/end
THRESHOLD = 0.1
light_slice = light_slice.with_column(
"gripper_open",
col("/observation/gripper_position:Scalars:scalars") > [THRESHOLD],
)
# Find start and end
light_slice = light_slice.with_column(
"prev_gripper_open",
F.lag(
col("gripper_open"),
default_value=False,
partition_by=[col("rerun_segment_id")],
order_by=[col("real_time")],
),
)
light_slice = light_slice.with_column(
"gripper_change",
col("gripper_open").cast(pa.int8())
- col("prev_gripper_open").cast(pa.int8()),
)
slice_times = light_slice.with_column(
"start",
F
.case(col("gripper_change"))
.when(lit(1), col("real_time"))
.otherwise(lit(None)),
).with_column(
"end",
F
.case(col("gripper_change"))
.when(lit(-1), col("real_time"))
.otherwise(lit(None)),
)
# Helper because pyarrow timestamps didn't have a nice min/max utility
max_ts = pa.scalar(np.iinfo(np.int64).max, type=pa.timestamp("ns"))
min_ts = pa.scalar(
np.iinfo(np.int64).min + 1_000_000_000, type=pa.timestamp("ns")
)
# This generates the column for the last observed start time
slice_dense_times = (
slice_times
.select("rerun_segment_id", "real_time", "start", "end")
.with_column(
"dense_start",
F.last_value(col("start")).over(
dfn.expr.Window(
window_frame=dfn.expr.WindowFrame("rows", None, 0),
order_by=col("real_time"),
partition_by=col("rerun_segment_id"),
null_treatment=dfn.common.NullTreatment.IGNORE_NULLS,
)
),
)
.fill_null(value=max_ts, subset=["dense_start"])
)
# This generates the column for the next observed end time (by finding the
# last_value in reversed order)
slice_dense_times = slice_dense_times.with_column(
"dense_end",
F.last_value(col("end")).over(
dfn.expr.Window(
window_frame=dfn.expr.WindowFrame("rows", None, 0),
order_by=col("real_time").sort(ascending=False),
partition_by=col("rerun_segment_id"),
null_treatment=dfn.common.NullTreatment.IGNORE_NULLS,
)
),
).fill_null(value=min_ts, subset=["dense_end"])
slice_dense_times = slice_dense_times.select(
"rerun_segment_id", "real_time", "dense_start", "dense_end"
)
sub_episodes = slice_dense_times.filter(
dfn.Expr.between(col("real_time"), col("dense_start"), col("dense_end")),
)
print(f"{sub_episodes.count()=}")
# endregion: sub_episodes