pwdsim
A pytorch/python Pinewood Derby simulator and optimizer.
pwdsim simulates, visualizes, and optimizes pinewood derby cars. It has three parts:
- Simulation: given the car and track, compute how the car runs down the track and how long it takes to reach the finish line.
- Optimization: tune features of the car to improve performance.
- Visualization: plot performance studies and car geometries.
The simulator is written in pytorch, so automatic differentiation gives parameter sensitivities directly and torch optimizers can tune the car design. Most users will work with pwdsim from Jupyter notebooks.
Documentation: https://reverendbedford.github.io/pwdsim/, with the model in detail, the API reference, and the example notebooks with their results.
Features
- Tracks: smooth spline tracks built from straights, circular arcs, and easements, including a model of the common BestTrack aluminum track.
- Cars: cars described directly by their essential properties, as trainable torch parameters, with batches of designs simulated together.
- Forward simulation: races a car down the track and reports its finish time, speed, energy budget, and the normal forces on its wheels, warning if a wheel lifts off the track. Gradients of the results with respect to every car parameter come from the adjoint method. The physics covers gravity, the translation and pitching of the car, the spin of its wheels, aerodynamic drag, and axle and rolling friction, and each piece can be switched on and off.
- Optimization: tunes chosen car parameters to minimize the finish time,
within bounds and constraints such as the race rules. A constraint keeping the
wheels on the track is on by default. It uses SciPy's
trust-constrmethod with gradients from the adjoint method. - Car geometry: cars described by a B-spline body profile and the weights and pockets in it, with the mass, center of gravity, moment of inertia, and drag computed from the geometry, so the optimizer can shape the body and place the weights.
- Visualization: plots of the track, the car on the track, its geometry, and its run.
Possible future additions include modeling the cars swerving in the track and rail riding, and calibrating the drag model against measurements.
Quick start
import pwdsim
from pwdsim.plotting import plot_run
from pwdsim.units import GRAM, INCH, OUNCE
# A car built from the standard BSA kit, in customary units
wheel_radius = 0.595 * INCH
wheel_inertia = 0.58 * (2.6 * GRAM) * wheel_radius**2
car = pwdsim.SimpleCar(
cg=(1.0 * INCH, 0.4 * INCH),
wheelbase=4.375 * INCH,
front_offset=6.125 * INCH,
mass=5 * OUNCE,
body_inertia=3.9e-4,
rear_wheel_inertia=wheel_inertia,
front_wheel_inertia=wheel_inertia,
rear_wheel_radius=wheel_radius,
front_wheel_radius=wheel_radius,
rear_axle_radius=0.0435 * INCH,
front_axle_radius=0.0435 * INCH,
rear_axle_friction=0.1,
front_axle_friction=0.1,
frontal_area=0.0014,
drag_coefficient=0.4,
rolling_friction=0.002,
)
# Race it down a 42 ft BestTrack
sim = pwdsim.Simulation(pwdsim.besttrack(42), car)
run = sim()
print(f"Finish time: {run.finish_time.item():.4f} s")
# Sensitivity of the finish time to the center of gravity, in ms per inch
run.finish_time.backward()
print(car.cg_.grad * INCH * 1000)
plot_run(run, unit="ft")
The forward model example walks through a complete simulation, and the which physics matters? example compares the effects of the different pieces of physics and ranks the car parameters by their effect on the finish time, and the optimizing a car example tunes a car's center of gravity and mass, and the designing a car body example optimizes the shape of a car and the size and position of its weight. The docs describe the model in detail.
Model overview
pwdsim uses a 2D model of a rigid car rolling along a track.
Track. A curve parameterized by arc length $s$, tracing the path of the wheel contact points. A track provides its height $y(s)$, angle $\theta(s)$, curvature $\kappa(s)$, and curvature derivatives, along with its total length $L$, the start pin location $s_\mathrm{start}$, and the finish line location $s_\mathrm{finish} \le L$.
Car. A rigid body connecting two axles, described by the position $s$ of the rear wheel contact along the track and its speed $\dot{s}$. Car parameters include:
| Symbol | Description |
|---|---|
| $\mathbf{x}_g$ | Center of gravity relative to the rear axle (body frame) |
| $w$ | Wheelbase |
| $d$ | Offset from the rear axle to the front of the car |
| $M$ | Total mass, including wheels |
| $I_b$ | Body moment of inertia about the center of gravity (pitch) |
| $n_r$, $n_f$ | Number of rear / front wheels |
| $I_r$, $I_f$ | Per-wheel moment of inertia, rear / front |
| $r_r$, $r_f$ | Wheel radius, rear / front |
| $a_r$, $a_f$ | Axle radius, rear / front |
| $\mu_r$, $\mu_f$ | Axle friction coefficient, rear / front |
| $A$ | Cross-sectional area |
| $C_d$ | Drag coefficient |
| $c_r$ | Rolling friction coefficient |
Global parameters are gravitational acceleration $g$ and air density $\rho$.
Dynamics. The equation of motion comes from Lagrange's equations with a Rayleigh
dissipation function. It is assembled from modular physics terms, which can be
switched on and off: potential energy (gravity), kinetic energy (the translation and
pitching of the car, and the spin of the wheels rolling without slip), and
dissipation (drag, axle friction, and rolling friction). The normal forces at each
axle come out as the constraint forces holding the axles on the track, so they
automatically include every enabled term. Friction is proportional to the normal
forces, so the acceleration and normal forces are solved for together. The
simulation flags when the model's assumptions break down, such as a wheel lifting
off the track. pwdsim.full_physics() (the default) includes every term, and
pwdsim.simple_physics() just gravity and translation.
Time integration. pyzag integrates the equation of motion with the backward Euler method, and provides gradients of the results with the adjoint method.
Units. Everything is SI internally, with helpers to work in US customary units (inches, ounces, ...).
Installation
pwdsim requires Python 3.12 or later. Install it from PyPI:
pip install pwdsim
or, for development, from a clone of the repository (see below).
Development
The project is managed with uv. Create the development
environment (a project-local .venv with the package, tests, linting, and docs tools)
and install the git hooks:
uv sync
uv run pre-commit install
Common tasks:
uv run pytest # run the tests (skipping slow ones)
uv run pytest --runslow # run all the tests, including the notebooks
uv run pytest --cov --cov-report=html # tests with coverage
uv run ruff check . && uv run ruff format . # lint and format
uv run pre-commit run --all-files # run all hooks
uv run mkdocs serve # preview the docs locally
uv run mkdocs build --strict # build the docs
Continuous integration
GitHub Actions (.github/workflows/ci.yml) runs on every pull request and push to
main, with four jobs: Pre-commit hooks (every hook, on all files),
Tests (pytest --cov), Example notebooks (the slow notebook tests), and
Package, which builds the sdist and wheel, checks them, and runs the tests
against the installed wheel.
The docs workflow (.github/workflows/docs.yml) builds the docs and publishes them
to GitHub Pages, at https://reverendbedford.github.io/pwdsim/, whenever main
changes. Each pull request gets a preview of its docs, linked from a comment on the
pull request and removed when it closes.
Releases
The version comes from the git tags (with hatch-vcs), so there's nothing to bump by
hand. To release, create a GitHub release on main with a new tag vX.Y.Z. The
release workflow (.github/workflows/release.yml) checks the tag, builds and tests
the package, and publishes it to PyPI with trusted publishing. Between releases,
builds get development versions such as 0.1.1.dev3.
Example notebooks
The example notebooks in docs/examples/ are stored as
jupytext percent-format .py scripts, so they
diff cleanly and never carry outputs in version control. To work on one
interactively, run uv run jupyter lab and open the .py file as a notebook
(right-click → Open With → Notebook). Jupytext pairs it with a .ipynb file,
which git ignores; edits are saved back to the .py script.
The docs build executes the notebooks and renders their outputs, and
tests/test_examples.py runs each one as a smoke test. The notebooks take a while,
so these tests only run with uv run pytest --runslow.
License
MIT; see LICENSE.
Metadata
Release files for pwdsim 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pwdsim-0.1.0.tar.gz | 262.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pwdsim-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 317.9 kB
Release files / pwdsim-0.1.0.tar.gz
| Download URL | pwdsim-0.1.0.tar.gz |
|---|---|
| Size | 262.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e746f27d9be4a94be831399f8c908318a77fe8e2d5d4adcdc9c658b1d5931c8d
|
|
BLAKE2b-256 checksum How to use checksums |
f72c1bfec0abe8e47b220aa602311bd8e7e1e0c14f2bdae5f4e48175553098a4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logRelease files / pwdsim-0.1.0-py3-none-any.whl
| Download URL | pwdsim-0.1.0-py3-none-any.whl |
|---|---|
| Size | 55.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5083a929fa51fad4d44a3a59e824b2afa74af67d09be202805660d3180f8d384
|
|
BLAKE2b-256 checksum How to use checksums |
b04b380f466a6ba89dc9a2eacc754274167b2174f41390b2d9a2d114640e79b9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency log