Universal Match Format (.parquet)

PadelKit defines a self-contained, single-file binary format based on Apache Parquet (.parquet with Zstandard compression) capable of encoding the complete static context, scoring progression, 9D ball aerodynamics, and 3D skeletal dynamics of an entire padel match.

Why a Two-Layer Parquet Architecture?

Whether a match is synthetically simulated (combining BallPhysicsEngine trajectories and padelkit-animator .glb clips) or reconstructed from real broadcast cameras (via TrackNet, RTMPose + MotionBERT, and Camera), downstream tools should consume the exact same file contract.

PadelKit's .parquet format stores both layers inside a single file with zero sidecar JSONs:

  • Name
    Layer 1: Static Match Metadata Header
    Type
    padelkit_metadata (JSON in Parquet Schema)
    Description

    Stored once in the Parquet schema's custom key-value metadata (b"padelkit_metadata"). Encodes the full Court (dimensions, enclosure variant, orientation, turf color, indoor/outdoor setting, door states, CourtLocation), EnvironmentConditions (temperature, humidity, altitude, wind vector, air density, gravity), BallProperties, SurfaceMaterials, and Match (teams, players, MatchConfiguration, and event-sourced points_history).

  • Name
    Layer 2: 60 FPS Dynamic Frame Table
    Type
    Columnar float32 / int16 / string
    Description

    Each row represents a timestamped snapshot (FrameState) containing structured match score columns (sets_a..points_b, server_team, point_index), the 9D ball state (ball_x..ball_wz), the 4 players' court root poses (p1_x..p4_swing), and optionally the 17-joint H36M 3D skeletal poses (p{1..4}_j{0..16}_{x,y,z}).


Saving & Loading a Complete Match Recording

MatchTimeline.to_parquet() serializes both the domain objects attached to the timeline (court, environment, ball_properties, surface_materials, match) and all frame columns into a compressed .parquet file.

When you pass bake_skeleton=True, PadelKit automatically evaluates the 17-joint 3D H36M skeleton (Pelvis to RightWrist) for every player in every frame before writing to disk, making the resulting file 100% self-contained for 3D playback without needing external .glb assets.

Write & Read .parquet

guide
MatchTimeline
from datetime import datetime
from padelkit import (
    BallProperties,
    Court,
    CourtLocation,
    CourtSetting,
    EnvironmentConditions,
    Match,
    MatchConfiguration,
    MatchTimeline,
    PadelVisualizer,
    Player,
    SurfaceMaterials,
    Team,
    TeamId,
)

# 1. Define static court, atmosphere, and match entities
court = Court(
    turf_color="blue",
    setting=CourtSetting.OUTDOOR,
    location=CourtLocation(
        club="Wizink Center",
        city="Madrid",
        country="Spain",
        latitude=40.4239,
        longitude=-3.6718,
    ),
)
env = EnvironmentConditions.from_atmosphere(
    temperature_c=24.0,
    altitude_m=655.0,
    relative_humidity=0.42,
)
team_a = Team(TeamId.A, Player("A. Coello"), Player("A. Tapia"), name="Team A")
team_b = Team(TeamId.B, Player("A. Galan"), Player("F. Chingotto"), name="Team B")
match = Match(
    teams={TeamId.A: team_a, TeamId.B: team_b},
    config=MatchConfiguration(advantage_method="gold_point"),
    court=court,
    date=datetime(2026, 5, 10, 18, 30),
)
match.history.add_point(TeamId.A)

# 2. Generate or record a 60 FPS timeline and bake 3D skeletons
timeline = MatchTimeline.demo_rally(
    court=court,
    environment=env,
    ball_properties=BallProperties.fip_standard(),
    surface_materials=SurfaceMaterials.fip_standard(),
    match=match,
    bake_skeleton=True,
)

# 3. Save to a single self-contained .parquet file
timeline.to_parquet("madrid_premier_point1.parquet")

# 4. Reload with full domain reconstruction or open in 3D Studio
loaded = MatchTimeline.from_parquet("madrid_premier_point1.parquet")
print(loaded.court.location.city, loaded.environment.temperature_c)

viz = PadelVisualizer.from_parquet("madrid_premier_point1.parquet")
viz.export_html("point1_3d_replay.html")

Layer 1: padelkit_metadata Schema Envelope

When MatchTimeline.to_parquet() writes a file, it embeds a UTF-8 JSON payload under the padelkit_metadata key in the Apache Arrow schema metadata. Calling MatchTimeline.from_parquet() automatically parses this header and reconstructs all domain objects via their .from_dict() classmethods:

  • Name
    schema_version
    Type
    str
    Description

    Format specification version for forward compatibility.

  • Name
    fps
    Type
    float
    Description

    Nominal sampling rate in frames per second.

  • Name
    has_skeletal_poses
    Type
    bool
    Description

    Indicates whether the 204 per-frame 3D joint columns (p1_j0_x .. p4_j16_z) are populated.

  • Name
    joint_convention
    Type
    str
    Description

    Joint ordering standard (H36M_JOINT_NAMES, 17 joints in root-relative meters).

  • Name
    court
    Type
    dict | None
    Description

    Serialized Court.to_dict() including CourtDimensions, enclosure variant, turf color, indoor/outdoor flag, door states, and CourtLocation.

  • Name
    environment
    Type
    dict | None
    Description

    Serialized EnvironmentConditions.to_dict() (air_density, gravity, wind_velocity, temperature_c, relative_humidity, altitude_m).

  • Name
    ball_properties
    Type
    dict | None
    Description

    Serialized BallProperties.to_dict() (mass, radius, cor_normal, fuzz_factor, spin_decay_tau).

  • Name
    surface_materials
    Type
    dict | None
    Description

    Serialized SurfaceMaterials.to_dict() (floor_mu, floor_cor_scale, glass_mu, glass_cor_scale, mesh_damping, net_damping).

  • Name
    match
    Type
    dict | None
    Description

    Serialized Match.to_dict() (date, team_a, team_b, configuration, initial_serving_state, and points_history).

Inspect Parquet Header

metadata
pyarrow.parquet
import json
import pyarrow.parquet as pq

schema = pq.read_schema("madrid_premier_point1.parquet")
envelope = json.loads(
    schema.metadata[b"padelkit_metadata"].decode("utf-8")
)

print("Schema version:", envelope["schema_version"])
print("Joint convention:", envelope["joint_convention"])
print("Venue:", envelope["court"]["location"]["venue_name"])
print("Temp (C):", envelope["environment"]["temperature_c"])

Layer 2: 60 FPS Columnar Schema

Every row in the Parquet table represents one FrameState at timestamp t. Numeric kinematic columns are stored as float32 during .to_parquet() export so a full 90-minute match at 60 FPS (324,000 rows, 239 columns) compresses down to roughly 35–55 MB with ZSTD.

1. Timing, Events & Structured Match Score (13 columns)

ColumnArrow TypeDescription
timestampfloat32Elapsed time $t$ in seconds from timeline start.
point_indexint320-based rally/point index in match.score_history.
eventstringFrame event tag ("", "serve", "bounce_floor_sliding", "shot_smash", etc.).
scorestringHuman-readable scoreboard label (e.g. "6-4, 2-1 (30-15)").
sets_a, sets_bint16Completed sets won by Team A and Team B.
games_a, games_bint16Games won in the active set by Team A and Team B.
points_a, points_bstringPoint call in the active game ("0", "15", "30", "40", "Ad", or tiebreak integer).
server_teamstringServing team identifier ("A" or "B").
server_playerstringServing player slot ("P1".."P4" or player name).
in_tiebreakboolTrue when the current game is a tiebreak or super-tiebreak.
is_gold_pointboolTrue on a deciding deuce point (40-40 with golden point enabled).

2. 9D Ball State (9 columns)

Column GroupColumnsUnits & Convention
3D Positionball_x, ball_y, ball_zMeters (m) in CENTERED court coordinates ($Z \ge 0$).
3D Velocityball_vx, ball_vy, ball_vzLinear velocity (m/s).
3D Spinball_wx, ball_wy, ball_wzAngular velocity (rad/s, right-hand rule).

3. Player Root Poses (24 columns for i in 1..4)

Column PatternArrow TypeDescription
p{i}_x, p{i}_y, p{i}_zfloat32Player root floor projection (x, y, z) in court meters.
p{i}_yawfloat32Heading angle in degrees (0° = facing +Y, 180° = facing -Y).
p{i}_animstringSemantic action/stroke clip ID ("idle", "run", "forehand_drive", "forehand_serve").
p{i}_swingfloat32Normalized stroke phase in [0.0, 1.0].

4. 17-Joint H36M 3D Skeletal Poses (204 optional columns)

When include_skeleton=True (default whenever PlayerPose.keypoints_3d is present or after calling timeline.bake_skeletal_poses()), the table includes p{1..4}_j{0..16}_{x,y,z} (4 players × 17 joints × 3 axes = 204 float32 columns).

Each joint offset (dx, dy, dz) is stored in root-relative court meters relative to (p{i}_x, p{i}_y, p{i}_z), so adding (p{i}_x + dx, p{i}_y + dy, p{i}_z + dz) yields the exact 3D world coordinate of that joint:

Joint Index (j)Joint Name (H36M_JOINT_NAMES[j])Anatomical Landmark
0PelvisRoot hip center
1, 2, 3RightHip, RightKnee, RightAnkleRight leg chain
4, 5, 6LeftHip, LeftKnee, LeftAnkleLeft leg chain
7, 8Spine, ThoraxMid-spine and upper chest
9, 10Neck, HeadBase of neck and head center
11, 12, 13LeftShoulder, LeftElbow, LeftWristNon-dominant balance arm
14, 15, 16RightShoulder, RightElbow, RightWristDominant racket arm (racket attaches along 15 -> 16)

Was this page helpful?