Metadata-Version: 2.5
Name: pwdsim
Version: 0.1.0
Summary: A pytorch Pinewood Derby simulator and optimizer
Project-URL: Homepage, https://github.com/reverendbedford/pwdsim
Project-URL: Documentation, https://reverendbedford.github.io/pwdsim/
Project-URL: Source, https://github.com/reverendbedford/pwdsim
Project-URL: Issues, https://github.com/reverendbedford/pwdsim/issues
Author-email: Mark Messner <mark.messner@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: optimization,physics,pinewood derby,pytorch,simulation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.12
Requires-Dist: matplotlib>=3.8
Requires-Dist: numpy>=2.5.3
Requires-Dist: pyzag>=2.0.0
Requires-Dist: torch>=2.4
Description-Content-Type: text/markdown

# pwdsim

A pytorch/python Pinewood Derby simulator and optimizer.

pwdsim simulates, visualizes, and optimizes pinewood derby cars. It has three parts:

1. **Simulation**: given the car and track, compute how the car runs down the track and
   how long it takes to reach the finish line.
2. **Optimization**: tune features of the car to improve performance.
3. **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-constr` method 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

```python
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](https://reverendbedford.github.io/pwdsim/examples/forward_model/) walks through a
complete simulation, and the [which physics matters?](https://reverendbedford.github.io/pwdsim/examples/physics_study/)
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](https://reverendbedford.github.io/pwdsim/examples/optimization/) example tunes a car's center of
gravity and mass, and the [designing a car body](https://reverendbedford.github.io/pwdsim/examples/car_design/)
example optimizes the shape of a car and the size and position of its weight.  The
[docs](https://reverendbedford.github.io/pwdsim/) 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](https://github.com/applied-material-modeling/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:

```bash
pip install pwdsim
```

or, for development, from a clone of the repository (see below).

## Development

The project is managed with [uv](https://docs.astral.sh/uv/). Create the development
environment (a project-local `.venv` with the package, tests, linting, and docs tools)
and install the git hooks:

```bash
uv sync
uv run pre-commit install
```

Common tasks:

```bash
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](https://jupytext.readthedocs.io/) 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](https://github.com/reverendbedford/pwdsim/blob/main/LICENSE).
