Skip to content

API reference

This page is autogenerated from the docstrings of the pitchphys package via mkdocstrings. Every public symbol you can from pitchphys import ... (or import from a submodule) is documented here, in source order.

To see the source code for any function, use the "Source" link on the GitHub repo — mkdocstrings is configured to keep this page focused on signatures and prose.

Top-level entry points

simulate

Trajectory integrator built on scipy.integrate.solve_ivp.

The plate-crossing event terminates integration at y == plate_distance_m. Per-step diagnostics (force decomposition, Re, S, Cd, Cl) are filled in via a second pass over the accepted timesteps so the RHS stays cheap and we don't need to dedupe rejected steps.

simulate

simulate(pitch, env=None, ball=None, model='magnus', forces=None, plate_distance_m=DEFAULT_PLATE_DISTANCE_M, method='RK45', rtol=1e-08, atol=1e-10, max_t=2.0, spin_decay_tau_s=1.5, _baselines=True)

Integrate a pitch from release to plate.

Parameters:

Name Type Description Default
pitch PitchRelease

Initial conditions.

required
env Environment | None

Atmospheric environment (defaults to sea level).

None
ball Baseball | None

Baseball physical properties (defaults to standard).

None
model AeroModel | str

Either a string name ("magnus", "lyu", "simple", "nathan", "constant") or an AeroModel instance.

'magnus'
forces Sequence[str] | None

Force names to include (e.g. ["gravity", "drag", "magnus"]). Defaults to all three. An empty list is allowed (free flight).

None
plate_distance_m float

Y-coordinate where the integration terminates.

DEFAULT_PLATE_DISTANCE_M
method Literal['RK45', 'DOP853']

"RK45" (default) or "DOP853".

'RK45'
rtol float

Solver relative tolerance.

1e-08
atol float

Solver absolute tolerance.

1e-10
max_t float

Maximum integration time in seconds.

2.0
spin_decay_tau_s float | None

Exponential time constant for spin decay, omega(t) = omega_0 * exp(-t / tau). Default 1.5 s is an Adair-baseline value that gives ~23 % decay over a 0.4 s flight. Pass None to disable decay (omega stays constant; matches SPEC §8.1 strict reading and Nathan 2008's fit assumption).

1.5
_baselines bool

Internal. If True and "magnus" is in forces, also simulate a gravity+drag baseline for Magnus-break diagnostics.

True

Returns:

Type Description
TrajectoryResult

TrajectoryResult with arrays sampled at the integrator's accepted

TrajectoryResult

timesteps and break-metric properties available.

simulate_many

simulate_many(pitches, **kwargs)

Run :func:simulate over a sequence of pitches (sequential in v0.1).

simulate_many

simulate_many(pitches, **kwargs)

Run :func:simulate over a sequence of pitches (sequential in v0.1).

Data models

PitchRelease dataclass

PitchRelease(speed_m_s, launch_angle_deg, horizontal_angle_deg, release_pos_m, spin_rate_rad_s, spin_axis, active_spin_fraction=1.0, metadata=dict())

Release-time state of a pitched ball.

Internally everything is SI: meters, seconds, kilograms, radians. Use :meth:from_mph_rpm_axis to construct from baseball-friendly units.

from_mph_rpm_axis classmethod

from_mph_rpm_axis(speed_mph, spin_rpm, tilt_clock=12.0, active_spin_fraction=0.95, release_height_ft=6.0, release_side_ft=-1.5, extension_ft=6.0, launch_angle_deg=-1.0, horizontal_angle_deg=0.0, throwing_hand='R')

Construct a PitchRelease from baseball-friendly units.

tilt_clock is the clock-face tilt of the active spin axis viewed from behind home plate (catcher view). See :mod:pitchphys.coordinates for the convention. 12:00 = pure backspin axis, 6:00 = pure topspin, etc.

The release position is at world y = 0; the plate sits ahead at y = DEFAULT_PLATE_DISTANCE_M (55 ft). extension_ft is stored as metadata in v0.1 but does not shift the release point.

Parameters:

Name Type Description Default
speed_mph float

Pitch release speed.

required
spin_rpm float

Total spin rate (active + gyro).

required
tilt_clock float

Active-spin clock tilt in hours, 0..12.

12.0
active_spin_fraction float

Fraction of spin perpendicular to velocity (the part that produces Magnus break). 0 = pure gyro, 1 = pure active.

0.95
release_height_ft float

Height of release above home plate (z, ft).

6.0
release_side_ft float

Lateral release offset (x, ft). Negative = catcher's left, which is the typical RHP release side.

-1.5
extension_ft float

Pitcher extension off the rubber (informational).

6.0
launch_angle_deg float

Angle of velocity above horizontal at release.

-1.0
horizontal_angle_deg float

Yaw of velocity around vertical (positive toward +x).

0.0
throwing_hand Literal['R', 'L']

"R" or "L"; passed to :func:clock_tilt_to_axis.

'R'

initial_velocity

initial_velocity()

Initial velocity 3-vector in world frame (m/s).

omega_vec

omega_vec(v_hat=None)

Angular velocity 3-vector in world frame (rad/s).

If v_hat is given, the omega vector is reconstructed from the active axis + active_spin_fraction so the perp/parallel decomposition matches the requested velocity direction. Otherwise we use the stored spin_axis scaled by spin_rate_rad_s.

Baseball dataclass

Baseball(mass_kg=BASEBALL_MASS_KG, radius_m=BASEBALL_RADIUS_M, seam_height_m=None)

A point-mass baseball.

Defaults are MLB rule midpoints (mass 5.0-5.25 oz, circumference 9.0-9.25 in). Override for different ball lots, training balls, etc.

area_m2 property

area_m2

Cross-sectional area used in drag/lift formulas.

Environment dataclass

Environment(air_density_kg_m3=AIR_DENSITY_SEA_LEVEL, dynamic_viscosity_pa_s=DYNAMIC_VISCOSITY_SEA_LEVEL, gravity_m_s2=G_M_S2, wind_m_s=_zero_wind())

Air properties, gravity, and wind.

Wind is a 3-vector in world frame (m/s). Drag uses v_rel = v - wind.

sea_level classmethod

sea_level()

Standard sea-level air at 15 deg C.

coors_field classmethod

coors_field()

Approximate Denver / Coors Field air density (~5,200 ft elevation).

The 1.00 kg/m^3 value is approximate; real conditions vary with temperature and humidity. Use :meth:from_weather for explicit sensitivity studies.

from_weather classmethod

from_weather(temp_c, pressure_pa, humidity=0.0)

Air properties from temperature, pressure, and relative humidity.

Uses the ideal-gas law with the Tetens equation for water-vapor saturation pressure and a humid-air partial-pressure mixture. Dynamic viscosity is computed via Sutherland's law.

Parameters:

Name Type Description Default
temp_c float

Air temperature in degrees Celsius.

required
pressure_pa float

Total atmospheric pressure in Pa (sea-level standard ≈ 101325).

required
humidity float

Relative humidity in [0, 1]. 0 for dry air.

0.0

Returns:

Type Description
Environment

Environment with computed air_density_kg_m3 and

Environment

dynamic_viscosity_pa_s. gravity_m_s2 and wind_m_s

Environment

keep their defaults.

Note

Humid air is less dense than dry air at the same pressure because water vapor (M ≈ 18 g/mol) is lighter than the nitrogen/oxygen mix (M ≈ 29 g/mol). At 30 °C, 100 % RH, sea-level pressure, this is ~1 % less dense than dry air.

TrajectoryResult dataclass

TrajectoryResult(time, position, velocity, acceleration, forces, reynolds_number, spin_factor, cd, cl, plate_distance_m, ball, env, pitch, forces_used, grav_drag_baseline=None, metadata=dict())

Sampled trajectory + per-step diagnostics.

Arrays are indexed along axis 0 by accepted-integrator timesteps. Forces are stored per name in the forces dict (each value shape (N, 3)).

Break metrics rely on baseline trajectories simulated with subsets of the forces: gravity_only_baseline (forces=["gravity"]) drives induced vertical break; grav_drag_baseline (forces=["gravity","drag"]) drives Magnus break.

total_drop_in property

total_drop_in

Inches the ball ended below the release height (positive = below).

horizontal_break_in property

horizontal_break_in

Lateral deviation from a straight-line projection of the release velocity.

Positive = catcher's right.

induced_vertical_break_in property

induced_vertical_break_in

Vertical movement induced by aerodynamic forces (Statcast §4.1).

Defined as the difference in z between the actual pitch and a no-force projectile with the same release velocity, evaluated at the actual pitch's flight time:

z_no_force(t) = z0 + vz0 * t - 0.5 * g * t**2

Positive ⇒ the ball ended above the gravity-only path (the pitch "rises" relative to a spinless ball). Negative ⇒ extra drop. For a pure-gyro pitch with no Magnus contribution to z, IVB ≈ 0 by construction.

magnus_break_x_in property

magnus_break_x_in

X component of break vs. a gravity+drag baseline.

non_magnus_break_in property

non_magnus_break_in

Reserved for v0.3 seam-shifted-wake models. Always 0.0 in v0.1.

plate_location

plate_location()

(plate_x_ft, plate_z_ft) at the moment of plate crossing.

break_metrics

break_metrics()

Return all break metrics as a dictionary.

Aerodynamic models

Aerodynamic coefficient models.

AeroModel

Bases: Protocol

Pluggable drag/lift coefficient provider.

Implementations may use any subset of Re, S, and ctx keys they care about. non_magnus_force returns a 3-vector (N) for any extra force outside gravity/drag/Magnus; v0.1 ships zeros.

LyuAeroModel dataclass

LyuAeroModel()

Lyu 2022 wind-tunnel C_D(Re, S) and C_L(S) (seam-averaged).

Default for model="magnus". See module docstring for sourcing.

NathanLiftModel dataclass

NathanLiftModel(cd_value=0.35)

Lift model from Nathan 2008 — the Sawicki, Hubbard, Stronge (2003) bilinear.

Implements the Sawicki-Hubbard-Stronge (SHS) bilinear C_L(S):

C_L = 1.5 * S            for S <  0.1
C_L = 0.09 + 0.6 * S     for S >= 0.1

where S = R * |omega_perp| / |v_rel| is the dimensionless spin factor. The two pieces agree at S = 0.1 (both give C_L = 0.15), so the function is continuous.

Nathan's 2008 motion-capture experiment (Am. J. Phys. 76:119-124) independently measured C_L over v = 50-110 mph and S = 0.1-0.6, finding "excellent agreement with the SHS parametrization for S < 0.3." See references/ajpfeb08.pdf Sec. IV.A and Fig. 5; the explicit formula appears as Eq. (2) of Nathan's arXiv preprint physics/0605041.

Drag is held at a constant C_D = 0.35. Nathan 2008 Fig. 4 shows the actual C_D has a weak speed dependence and considerable scatter; for a Re- and spin-dependent drag use LyuAeroModel.

History: v0.1 of this package implemented C_L = 1.5*S/(0.4 + 2.32*S) in this class — that rational fit was misattributed to Nathan 2008 and has been replaced (v0.1.5) with the actual SHS bilinear.

SimpleMagnusModel dataclass

SimpleMagnusModel(cd_value=0.35, a=1.0, cl_max=0.4)

Simple linear-in-spin-factor lift model: Cl = min(a * S, cl_max).

Default for model="magnus". Transparent and pedagogically clear; not a precise empirical fit.

ConstantAeroModel dataclass

ConstantAeroModel(cd_value=0.35, cl_value=0.2)

Constant drag and lift coefficients. Useful for analytic tests.

UserDefinedAeroModel dataclass

UserDefinedAeroModel(cd_fn, cl_fn, force_fn=None)

Wrap user-supplied callables as an AeroModel.

Coordinates

Coordinate system and spin-axis convention.

This module is the single source of truth for how clock-face spin tilts map to 3D spin-axis vectors. Every other module that needs to interpret a spin axis should call into this module rather than re-deriving the rotation.

World axes (right-handed): x = horizontal, positive to catcher's right y = forward, positive from release toward home plate z = vertical, positive up

Spin tilt is described as a clock face viewed from behind home plate looking toward the pitcher (catcher view). For a right-handed pitcher (RHP):

    12 (backspin)
     |

9 ----+---- 3 | 6 (topspin)

12:00 -> axis = +x  (pure backspin -> +z Magnus break, "lift")
 3:00 -> axis = -z  (sidespin     -> +x Magnus break, catcher's right)
 6:00 -> axis = -x  (pure topspin -> -z Magnus break, "drop")
 9:00 -> axis = +z  (sidespin     -> -x Magnus break, catcher's left)

The mapping of tilt T (in clock hours) to the 3D axis is a planar rotation in the catcher-view (x, z) plane:

angle_rad = -pi * T / 6           # measured clockwise from 12:00
axis      = (cos(angle_rad), 0, sin(angle_rad))

For an LHP, the resulting axis is mirrored across the 12-6 axis (x -> -x).

Magnus force is always proportional to omega x v_hat, so under v_hat = +y the break direction equals the clock-tilt direction itself: 12:00 break = up, 6:00 break = down, 3:00 break = catcher's right, 9:00 break = catcher's left. This is intentional (the convention is internally consistent) and is the mental model users should keep.

STRIKE_ZONE_TOP_FT module-attribute

STRIKE_ZONE_TOP_FT = 3.5

STRIKE_ZONE_BOTTOM_FT module-attribute

STRIKE_ZONE_BOTTOM_FT = 1.5

STRIKE_ZONE_HALF_WIDTH_FT module-attribute

STRIKE_ZONE_HALF_WIDTH_FT = 8.5 / 12.0

clock_tilt_to_axis

clock_tilt_to_axis(tilt_clock_hours, throwing_hand='R')

Map a clock-face tilt to a 3D unit vector along the active spin axis.

Parameters:

Name Type Description Default
tilt_clock_hours float

Clock tilt in hours, 0 <= tilt < 12. Fractional values are allowed (e.g. 1.5 for 1:30). Out-of-range inputs are reduced modulo 12.

required
throwing_hand Literal['R', 'L']

"R" (default) or "L". For "L" the resulting axis is mirrored across the 12-6 axis by flipping x.

'R'

Returns:

Type Description
ndarray

Unit 3-vector (x, y, z) lying in the catcher-view (x, z) plane.

build_omega

build_omega(spin_rate_rad_s, active_axis, active_spin_fraction, v_hat)

Compose the full 3D angular velocity vector from an active axis + gyro mix.

active_spin_fraction is the share of the spin that is perpendicular to velocity (and therefore contributes to the Magnus force, SPEC §4.4). The remaining share is gyro spin aligned with velocity:

omega = spin_rate * (active_spin_fraction * active_axis_hat
                     + gyro_fraction * v_hat)

where gyro_fraction = sqrt(1 - active_spin_fraction^2).

Parameters:

Name Type Description Default
spin_rate_rad_s float

Spin magnitude in rad/s.

required
active_axis ndarray

3-vector along the active (perpendicular) spin axis. Need not be a unit vector; it will be normalized.

required
active_spin_fraction float

In [0, 1]. 1.0 means pure active spin.

required
v_hat ndarray

Unit velocity direction at release.

required

Returns:

Type Description
ndarray

3-vector angular velocity in rad/s.

decompose_omega

decompose_omega(omega, v_hat)

Split angular velocity into perpendicular and parallel components.

omega = omega_perp + omega_par where omega_par is along v_hat. Only omega_perp contributes to the Magnus force (SPEC §8.5).

Parameters:

Name Type Description Default
omega ndarray

3-vector angular velocity.

required
v_hat ndarray

Unit velocity direction (need not be exactly unit; will be normalized).

required

Returns:

Type Description
tuple[ndarray, ndarray]

(omega_perp, omega_par), both 3-vectors.

Units

SI unit conversion helpers.

The simulation core stores meters, seconds, kilograms, radians. These helpers convert at the user-facing boundary so mph/rpm/feet/inches never leak into force calculations.

mph_to_mps

mph_to_mps(mph)

mps_to_mph

mps_to_mph(mps)

rpm_to_radps

rpm_to_radps(rpm)

radps_to_rpm

radps_to_rpm(radps)

ft_to_m

ft_to_m(ft)

m_to_ft

m_to_ft(m)

in_to_m

in_to_m(inches)

m_to_in

m_to_in(m)

Visualization — 2D (Matplotlib)

2D Matplotlib visualization helpers.

Matplotlib is imported lazily inside each function so the core package can be used without the visualization extra installed.

All views default to feet on both axes; pass units="m" to switch.

side_view

side_view(traj, ax=None, units='ft', show_release=True, show_plate=True, label=None, **kwargs)

Plot the trajectory from the side (y vs. z).

catcher_view

catcher_view(traj, ax=None, units='ft', show_strike_zone=True, show_trajectory=True, label=None, **kwargs)

Plot the trajectory as seen by the catcher (x vs. z).

top_view

top_view(traj, ax=None, units='ft', label=None, **kwargs)

Plot the trajectory from above (y vs. x).

compare_pitches

compare_pitches(trajs, view='catcher', labels=None, ax=None, units='ft', **kwargs)

Overlay multiple trajectories on a shared view.

draw_force_arrows

draw_force_arrows(ax, traj, times, scale=1.0, view='side', units='ft')

Annotate force vectors at selected times on an existing axis.

Visualization — 3D (Plotly)

3D Plotly visualization helpers.

Plotly is imported lazily inside each function so the core package can be used without the visualization extra installed. Use only plotly.graph_objects (no plotly.express) for explicit, composable trace control.

World-frame axes (right-handed, per :mod:pitchphys.coordinates):

x = catcher's right
y = forward toward home plate
z = vertical, positive up

The default camera puts you behind and slightly above the catcher's left shoulder so vertical break is visible alongside the trajectory. Aspect mode is "data" to keep break-magnitude visually faithful.

trajectory_3d

trajectory_3d(traj, *, fig=None, label=None, units='ft', show_release=True, show_plate=True, show_strike_zone=True, color=None, line_kwargs=None)

Plot a single pitch trajectory in 3D.

Returns the (possibly new) plotly.graph_objects.Figure.

compare_pitches_3d

compare_pitches_3d(trajs, *, labels=None, fig=None, units='ft', show_strike_zone=True, colors=None)

Overlay multiple trajectories in a single 3D scene.

add_spin_axis_arrow

add_spin_axis_arrow(fig, pitch, *, position=None, length_ft=2.0, color='#9467bd', label='spin axis', units='ft')

Draw a 3D arrow along pitch.spin_axis at the given position.

add_force_vectors

add_force_vectors(fig, traj, *, times=(0.1, 0.2, 0.3), forces=('gravity', 'drag', 'magnus'), scale=0.05, units='ft', colors=None)

Annotate the trajectory with force-vector cones at selected times.

scale is in meters per Newton (cone tip is scale * |F| meters from the ball position). Default 0.05 m/N gives ~5 cm per Newton, a reasonable visual size for the ~1 N forces typical at MLB speeds.

Animation

Animated 3D trajectory using Plotly frames + slider/play.

Plotly figure frames together with updatemenus and sliders provide a play/pause/scrub experience entirely in the browser. We don't require a server callback. Frame durations below ~30 ms tend to stutter in browsers; default is 30 fps which gives 33 ms per frame.

animate_pitch

animate_pitch(traj, *, fps=30, fig=None, units='ft', show_trail=True, show_strike_zone=True, target_frames=None)

Build an animated 3D figure for a single pitch trajectory.

The figure has a Play button and a frame slider. target_frames overrides the auto frame count derived from fps * flight_time.

animate_pitches

animate_pitches(trajs, *, labels=None, fps=30, fig=None, units='ft', target_frames=None)

Animate multiple pitches synchronized on a shared time grid.

Each pitch gets a colored ball + trail. All balls advance together over a common normalized time index; pitches with shorter flight reach the plate sooner and freeze at the plate position for remaining frames.

Pitch presets

Pedagogical pitch presets — see :mod:pitchphys.presets.pitches.

four_seam

four_seam(speed_mph=95.0, spin_rpm=2400.0, tilt_clock=12.0, active_spin_fraction=0.95, throwing_hand='R', **kwargs)

High-spin backspin fastball. Default Magnus break: straight up.

curveball

curveball(speed_mph=80.0, spin_rpm=2700.0, tilt_clock=6.0, active_spin_fraction=0.85, throwing_hand='R', **kwargs)

Topspin curveball. Default Magnus break: straight down.

slider

slider(speed_mph=85.0, spin_rpm=2500.0, tilt_clock=3.0, active_spin_fraction=0.3, throwing_hand='R', **kwargs)

Gyro-dominant slider. High raw spin, low total break.

Demonstrates SPEC §4.4: spin rate alone does not predict movement.

sinker

sinker(speed_mph=92.0, spin_rpm=2200.0, tilt_clock=7.5, active_spin_fraction=0.85, throwing_hand='R', **kwargs)

Arm-side sinker for RHP. Default break: arm side + drop.

changeup

changeup(speed_mph=84.0, spin_rpm=1700.0, tilt_clock=7.0, active_spin_fraction=0.8, throwing_hand='R', **kwargs)

Off-speed changeup. Lower spin, slight arm-side run + drop.