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 fullCourt(dimensions, enclosure variant, orientation, turf color, indoor/outdoor setting, door states,CourtLocation),EnvironmentConditions(temperature, humidity, altitude, wind vector, air density, gravity),BallProperties,SurfaceMaterials, andMatch(teams, players,MatchConfiguration, and event-sourcedpoints_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
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
204per-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()includingCourtDimensions, enclosure variant, turf color, indoor/outdoor flag, door states, andCourtLocation.
- 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, andpoints_history).
Inspect Parquet Header
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)
| Column | Arrow Type | Description |
|---|---|---|
timestamp | float32 | Elapsed time $t$ in seconds from timeline start. |
point_index | int32 | 0-based rally/point index in match.score_history. |
event | string | Frame event tag ("", "serve", "bounce_floor_sliding", "shot_smash", etc.). |
score | string | Human-readable scoreboard label (e.g. "6-4, 2-1 (30-15)"). |
sets_a, sets_b | int16 | Completed sets won by Team A and Team B. |
games_a, games_b | int16 | Games won in the active set by Team A and Team B. |
points_a, points_b | string | Point call in the active game ("0", "15", "30", "40", "Ad", or tiebreak integer). |
server_team | string | Serving team identifier ("A" or "B"). |
server_player | string | Serving player slot ("P1".."P4" or player name). |
in_tiebreak | bool | True when the current game is a tiebreak or super-tiebreak. |
is_gold_point | bool | True on a deciding deuce point (40-40 with golden point enabled). |
2. 9D Ball State (9 columns)
| Column Group | Columns | Units & Convention |
|---|---|---|
| 3D Position | ball_x, ball_y, ball_z | Meters (m) in CENTERED court coordinates ($Z \ge 0$). |
| 3D Velocity | ball_vx, ball_vy, ball_vz | Linear velocity (m/s). |
| 3D Spin | ball_wx, ball_wy, ball_wz | Angular velocity (rad/s, right-hand rule). |
3. Player Root Poses (24 columns for i in 1..4)
| Column Pattern | Arrow Type | Description |
|---|---|---|
p{i}_x, p{i}_y, p{i}_z | float32 | Player root floor projection (x, y, z) in court meters. |
p{i}_yaw | float32 | Heading angle in degrees (0° = facing +Y, 180° = facing -Y). |
p{i}_anim | string | Semantic action/stroke clip ID ("idle", "run", "forehand_drive", "forehand_serve"). |
p{i}_swing | float32 | Normalized 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 |
|---|---|---|
0 | Pelvis | Root hip center |
1, 2, 3 | RightHip, RightKnee, RightAnkle | Right leg chain |
4, 5, 6 | LeftHip, LeftKnee, LeftAnkle | Left leg chain |
7, 8 | Spine, Thorax | Mid-spine and upper chest |
9, 10 | Neck, Head | Base of neck and head center |
11, 12, 13 | LeftShoulder, LeftElbow, LeftWrist | Non-dominant balance arm |
14, 15, 16 | RightShoulder, RightElbow, RightWrist | Dominant racket arm (racket attaches along 15 -> 16) |