{"id":34106971,"url":"https://github.com/markus-ebke/python-billiards","last_synced_at":"2026-04-09T04:04:36.967Z","repository":{"id":45186529,"uuid":"205887310","full_name":"markus-ebke/python-billiards","owner":"markus-ebke","description":"A 2D physics engine for simulating dynamical billiards.","archived":false,"fork":false,"pushed_at":"2025-06-20T18:00:52.000Z","size":35891,"stargazers_count":26,"open_issues_count":0,"forks_count":15,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-12-17T01:38:56.462Z","etag":null,"topics":["physics-2d","physics-engine","python","python3"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/markus-ebke.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2019-09-02T15:36:11.000Z","updated_at":"2025-10-11T05:58:33.000Z","dependencies_parsed_at":"2025-06-20T19:19:01.143Z","dependency_job_id":"d176569a-9162-425f-beb5-86d752b40838","html_url":"https://github.com/markus-ebke/python-billiards","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/markus-ebke/python-billiards","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markus-ebke%2Fpython-billiards","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markus-ebke%2Fpython-billiards/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markus-ebke%2Fpython-billiards/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markus-ebke%2Fpython-billiards/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/markus-ebke","download_url":"https://codeload.github.com/markus-ebke/python-billiards/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markus-ebke%2Fpython-billiards/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31584821,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-08T14:31:17.711Z","status":"online","status_checked_at":"2026-04-09T02:00:06.848Z","response_time":112,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["physics-2d","physics-engine","python","python3"],"created_at":"2025-12-14T18:04:28.423Z","updated_at":"2026-04-09T04:04:36.962Z","avatar_url":"https://github.com/markus-ebke.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# billiards\n\n\u003e A 2D physics engine for simulating dynamical billiards\n\n**billiards** is a python library that implements a very simple physics engine:\nIt simulates the movement and elastic collisions of hard, disk-shaped particles in a two-dimensional world.\n\n\n\n## Features\n\n- Collisions are found and resolved *exactly*. No reliance on time steps, no tunneling of high-speed bullets!\n- Quick state updates thanks to [numpy](https://numpy.org), especially if there are no collisions between the given start and end times.\n- Static obstacles to construct a proper billiard table.\n- Balls with zero radii behave like point particles, useful for simulating [dynamical billiards](https://en.wikipedia.org/wiki/Dynamical_billiards) (although this library is not optimized for point particles).\n- Optional features: plotting and animation with [matplotlib](https://matplotlib.org), interaction with [pyglet](https://pyglet.org).\n- Free and open source software under the MIT license.\n\n\n\n## Installation\n\n**billiards** is a library for Python 3.\nIt only depends on [numpy](https://numpy.org).\n\nBilliard systems can be visualized with [matplotlib](https://matplotlib.org) (and [tqdm](https://tqdm.github.io) to display progress in `visualize_matplotlib.animate`).\nInteraction with the simulation is possible via [pyglet](https://pyglet.org).\nThese visualization features are optional.\n\nClone the repository from GitHub and install the package:\n\n```shell\ngit clone https://github.com/markus-ebke/python-billiards.git\ncd python-billiards/\npip install .[visualize]\n```\n\n\n\n## Quickstart\n\nAll important classes (the billiard simulation and obstacles) are accessible from the top-level module.\nThe visualization modules must be imported separately and will load *matplotlib* or *pyglet*.\nFor the following examples we will use matplotlib visualizations.\n\n```pycon\n\u003e\u003e\u003e import billiards  # access to Billiard, Disk and InfiniteWall\n\u003e\u003e\u003e import billiards.visualize_matplotlib as visualize  # for plot and animate\n\u003e\u003e\u003e import matplotlib.pyplot as plt  # for plt.show()\n```\n\n\n## Example: Computing π with pool\n\nLet's compute the first few digits of π using a billiard simulation following the setup of Gregory Galperin.\nWe need a billiard table with a vertical wall and two balls:\n\n```pycon\n\u003e\u003e\u003e obstacles = [billiards.obstacles.InfiniteWall((0, -1), (0, 1), blocked=\"right\")]\n\u003e\u003e\u003e bld = billiards.Billiard(obstacles)\n\u003e\u003e\u003e bld.add_ball((3, 0), (0, 0), radius=0.2, mass=1)  # returns index of new ball\n0\n\u003e\u003e\u003e bld.add_ball((6, 0), (-1, 0), radius=1, mass=100**5)\n1\n```\n\nUsing the _visualize_ module, let's see how this initial state looks:\n\n```pycon\n\u003e\u003e\u003e visualize.plot(bld)\n(\u003cFigure size 800x600 with 1 Axes\u003e, \u003cAxes: \u003e)\n\u003e\u003e\u003e plt.show()\n```\n\n![Initial state of Galperin's billiard](docs/_images/quickstart_1.svg)\n\n\nThe _Billiard.evolve_ method simulates our billiard system for a given time interval.\nIt returns a list of collisions (the number of ball-ball and the number of ball-obstacle collisions).\n\n```pycon\n\u003e\u003e\u003e bld.next_collision  # (time, ball index, ball index or obstacle)-triplet\n(1.8, 0, 1)\n\u003e\u003e\u003e total_collisions = 0\n\u003e\u003e\u003e for i in range(5):\n...     total_collisions += sum(bld.evolve(1.0))\n...     print(f\"Until t = {bld.time}: {total_collisions} collisions\")\n...\nUntil t = 1.0: 0 collisions\nUntil t = 2.0: 1 collisions\nUntil t = 3.0: 1 collisions\nUntil t = 4.0: 4 collisions\nUntil t = 5.0: 314152 collisions\n```\n\nThe first collision happened at time t = 1.8.\nUntil t = 4.0 there were only 4 collisions, but then between t = 4.0 and t = 5.0 there were several thousand.\nLet's see how the situation looks now:\n\n```pycon\n\u003e\u003e\u003e bld.time  # current time\n5.0\n\u003e\u003e\u003e visualize.plot(bld)\n(\u003cFigure size 800x600 with 1 Axes\u003e, \u003cAxes: \u003e)\n\u003e\u003e\u003e plt.show()\n```\n\n![State at time t = 5](docs/_images/quickstart_2.svg)\n\n\nLet's advance the simulation to t = 16.0.\nAs we can check, there won't be any other collisions after this time:\n\n```pycon\n\u003e\u003e\u003e total_collisions += sum(bld.evolve(until=16.0))\n\u003e\u003e\u003e bld.balls_velocity  # nx2 numpy array where n is the number of balls\narray([[0.73463055, 0.        ],\n       [1.        , 0.        ]])\n\u003e\u003e\u003e bld.next_ball_ball_collision  # (time, ball index, ball index) of next collision\n(inf, -1, 0)\n\u003e\u003e\u003e bld.next_ball_obstacle_collision  # (time, ball index, obstacle info)\n(inf, 0, None)\n\u003e\u003e\u003e visualize.plot(bld)\n(\u003cFigure size 800x600 with 1 Axes\u003e, \u003cAxes: \u003e)\n\u003e\u003e\u003e plt.show()\n```\n\n![State at time t = 16](docs/_images/quickstart_3.svg)\n\n\nBoth balls are moving towards infinity, the smaller ball to slow to catch the larger one.\nWhat is the total number of collisions?\n\n```pycon\n\u003e\u003e\u003e total_collisions\n314159\n\u003e\u003e\u003e import math\n\u003e\u003e\u003e math.pi\n3.141592653589793\n```\n\nThe first six digits match!\nFor an explanation why this happens, see Galperin's paper [Playing pool with π (the number π from a billiard point of view)](https://www.maths.tcd.ie/~lebed/Galperin.%20Playing%20pool%20with%20pi.pdf) or the series of youtube videos by [3Blue1Brown](https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw) starting with [The most unexpected answer to a counting puzzle](https://www.youtube.com/watch?v=HEfHFsfGXjs).\n\nLastly, I want to point out that all collisions were elastic, i.e. they conserved the kinetic energy (within floating point accuracy):\n\n```pycon\n\u003e\u003e\u003e 100**5 * (-1) ** 2 / 2  # kinetic energy = m v^2 / 2 at the beginning\n5000000000.0\n\u003e\u003e\u003e v_squared = (bld.balls_velocity**2).sum(axis=1)\n\u003e\u003e\u003e (bld.balls_mass * v_squared).sum() / 2  # kinetic energy now\nnp.float64(5000000000.044419)\n```\n\nThe video [examples/pi_with_pool.mp4](examples/pi_with_pool.mp4) replays the whole billiard simulation (it was created using `visualize.animate`).\n\n\n\n## More Examples\n\nSetup:\n\n```pycon\n\u003e\u003e\u003e import matplotlib.pyplot as plt\n\u003e\u003e\u003e import billiards\n\u003e\u003e\u003e import billiards.visualize_matplotlib as visualize\n```\n\n\n\n### First shot in Pool (no friction)\n\nConstruct the billiard table:\n\n```pycon\n\u003e\u003e\u003e width, length = 112, 224\n\u003e\u003e\u003e bounds = [\n...     billiards.InfiniteWall((0, 0), (length, 0)),  # bottom side\n...     billiards.InfiniteWall((length, 0), (length, width)),  # right side\n...     billiards.InfiniteWall((length, width), (0, width)),  # top side\n...     billiards.InfiniteWall((0, width), (0, 0)),  # left side\n... ]\n\u003e\u003e\u003e bld = billiards.Billiard(obstacles=bounds)\n```\n\nArrange the balls in a pyramid shape:\n\n```pycon\n\u003e\u003e\u003e from math import sqrt\n\u003e\u003e\u003e radius = 2.85\n\u003e\u003e\u003e for i in range(5):\n...     for j in range(i + 1):\n...         x = 0.75 * length + radius * sqrt(3) * i\n...         y = width / 2 + radius * (2 * j - i)\n...         bld.add_ball((x, y), (0, 0), radius)\n...\n```\n\nAdd the white ball and give it a push, then view the animation:\n\n```pycon\n\u003e\u003e\u003e bld.add_ball((0.25 * length, width / 2), (length / 3, 0), radius)\n\u003e\u003e\u003e anim, fig, ax = visualize.animate(bld, 10.0, figsize=(10, 5.5))\n\u003e\u003e\u003e plt.show()\n```\n\nSee [examples/pool.mp4](./examples/pool.mp4)\n\n\n\n### Brownian motion\n\nThe billiard table is a square box:\n\n```pycon\n\u003e\u003e\u003e obs = [\n...     billiards.InfiniteWall((-1, -1), (1, -1)),  # bottom side\n...     billiards.InfiniteWall((1, -1), (1, 1)),  # right side\n...     billiards.InfiniteWall((1, 1), (-1, 1)),  # top side\n...     billiards.InfiniteWall((-1, 1), (-1, -1)),  # left side\n... ]\n\u003e\u003e\u003e bld = billiards.Billiard(obstacles=obs)\n```\n\nDistribute small particles (atoms) uniformly in the square, moving in random directions but with the same speed:\n\n```pycon\n\u003e\u003e\u003e from math import cos, pi, sin\n\u003e\u003e\u003e from random import uniform\n\u003e\u003e\u003e for i in range(250):\n...     pos = [uniform(-1, 1), uniform(-1, 1)]\n...     angle = uniform(0, 2 * pi)\n...     vel = [cos(angle), sin(angle)]\n...     bld.add_ball(pos, vel, radius=0.01, mass=1)\n...\n```\n\nAdd a bigger ball (like a dust particle)\n\n```pycon\n\u003e\u003e\u003e idx = bld.add_ball((0, 0), (0, 0), radius=0.1, mass=10)\n```\n\nand simulate until t = 50, recording the position of the bigger ball at each collision (this will take some time)\n\n```pycon\n\u003e\u003e\u003e poslist = [bld.balls_position[idx].copy()]  # record initial position\n\u003e\u003e\u003e def record(t, dt, p, u, v, i_o):\n...     poslist.append(p)\n...\n\u003e\u003e\u003e bld.evolve(50.0, ball_callbacks={idx: record})\n(25506, 13224)\n\u003e\u003e\u003e poslist.append(bld.balls_position[idx].copy())  # record last position\n```\n\nPlot the billiard and overlay the path of the particle\n\n```pycon\n\u003e\u003e\u003e fig, ax = visualize.plot(bld, arrow_size=0, figsize=(7, 7))\n\u003e\u003e\u003e poslist = np.asarray(poslist)\n\u003e\u003e\u003e ax.plot(poslist[:, 0], poslist[:, 1], color=\"red\")\n[\u003cmatplotlib.lines.Line2D object at 0x...\u003e]\n\u003e\u003e\u003e plt.show()\n```\n\n![Brownian motion](docs/_images/brownian_motion.svg)\n\n\n\n## Authors\n\n- Markus Ebke - \u003chttps://github.com/markus-ebke\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkus-ebke%2Fpython-billiards","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarkus-ebke%2Fpython-billiards","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkus-ebke%2Fpython-billiards/lists"}