billiards


Namebilliards JSON
Version 0.5.0 PyPI version JSON
download
home_pagehttps://github.com/markus-ebke/python-billiards
SummaryA 2D physics engine for simulating dynamical billiards
upload_time2023-05-12 21:27:52
maintainer
docs_urlNone
authorMarkus Ebke
requires_python>=3.7
licenseGNU General Public License v3 or later (GPLv3+)
keywords python3 physics-engine physics-2d
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # billiards

> A 2D physics engine for simulating dynamical billiards

**billiards** is a python library that implements a very simple physics engine:
It simulates the movement and elastic collisions of hard, disk-shaped particles in a two-dimensional world.



## Features

- Collisions are found and resolved *exactly*. No reliance on time steps, no tunneling of high-speed bullets!
- Quick state updates thanks to [numpy](https://numpy.org), especially if there are no collisions between the given start and end times.
- Static obstacles to construct a proper billiard table.
- 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).
- Optional features: plotting and animation with [matplotlib](https://matplotlib.org), interaction with [pyglet](http://pyglet.org).
- Free software: GPLv3+ license.



## Installation

**billiards** depends on [numpy](https://numpy.org).
Additionally, billiard systems can be visualized with [matplotlib](https://matplotlib.org) and [pyglet](http://pyglet.org) (and [tqdm](https://tqdm.github.io) to display progress in `visualize.animate`).
But this feature is optional.

Clone the repository from GitHub and install the package:

```shell
git clone https://github.com/markus-ebke/python-billiards.git
pip install .[visualize]
```



## Usage

All important classes (the billiard simulation and obstacles) are accessible from the top-level module.
The visualization module must be imported separately and tries to load *matplotlib*, *tqdm* and *pyglet*.

```pycon
>>> import billiards  # access to Billiard, Disk and InfiniteWall
>>> from billiards import visualize  # for plot, animate and interact
>>> import matplotlib.pyplot as plt  # for plt.show()
```

Let's compute the first few digits of π using a billiard simulation following the setup of Gregory Galperin.
We need a billiard table with a vertical wall and two balls:

```pycon
>>> obstacles = [billiards.obstacles.InfiniteWall((0, -1), (0, 1), inside="right")]
>>> bld = billiards.Billiard(obstacles)
>>> bld.add_ball((3, 0), (0, 0), radius=0.2, mass=1)  # returns index of new ball
0
>>> bld.add_ball((6, 0), (-1, 0), radius=1, mass=100 ** 5)
1
```

Using the _visualize_ module, let's see how this initial state looks:

```pycon
>>> visualize.plot(bld)
<Figure size 800x600 with 1 Axes>
>>> plt.show()
```

![Initial state of Galperin's billiard](docs/_images/quickstart_1.svg)


The _Billiard.evolve_ method simulates our billiard system from _bld.time_ until a given end time.
It returns a list of collisions (ball-ball and ball-obstacle collisions).

```pycon
>>> bld.toi_next  # next ball-ball collision, its a (time, index, index)-triplet
(1.8000000000000005, 0, 1)
>>> total_collisions = 0
>>> for i in [1, 2, 3, 4, 5]:
...     total_collisions += len(bld.evolve(i))
...     print(f"Until t = {bld.time}: {total_collisions} collisions")
Until t = 1: 0 collisions
Until t = 2: 1 collisions
Until t = 3: 1 collisions
Until t = 4: 4 collisions
Until t = 5: 314152 collisions
```

The first collision happened at time t = 1.8.
Until t = 4 there were only 4 collisions, but then between t = 4 and t = 5 there were several thousands.
Let's see how the situation looks now:

```pycon
>>> bld.time  # current time
5
>>> visualize.plot(bld)
<Figure size 800x600 with 1 Axes>
>>> plt.show()
```

![State at time t = 5](docs/_images/quickstart_2.svg)


Let's advance the simulation to t = 16.
As we can check, there won't be any other collisions after this time:

```pycon
>>> total_collisions += len(bld.evolve(16))
>>> bld.balls_velocity  # nx2 numpy array where n is the number of balls
array([[0.73463055, 0.        ],
       [1.        , 0.        ]])
>>> bld.toi_next  # next ball-ball collision
(inf, -1, 0)
>>> bld.obstacles_next  # next ball-obstacle collision
(inf, 0, None)
>>> visualize.plot(bld)
<Figure size 800x600 with 1 Axes>
>>> plt.show()
```

![State at time t = 16](docs/_images/quickstart_3.svg)


Both balls are moving towards infinity, the smaller ball to slow to catch the larger one.
What is the total number of collisions?

```pycon
>>> total_collisions
314159
>>> import math
>>> math.pi
3.141592653589793
```

The first six digits match!
For 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).

Lastly, I want to point out that all collisions were elastic, i.e. they conserved the kinetic energy (within floating point accuracy):

```pycon
>>> 100 ** 5 * (-1) ** 2 / 2  # kinetic energy = m v^2 / 2 at the beginning
5000000000.0
>>> v_squared = (bld.balls_velocity ** 2).sum(axis=1)
>>> (bld.balls_mass * v_squared).sum() / 2  # kinetic energy now
4999999999.990375
```

The video [examples/pi_with_pool.mp4](examples/pi_with_pool.mp4) replays the whole billiard simulation (it was created using `visualize.animate`).



## More Examples

Setup:

```pycon
>>> import matplotlib.pyplot as plt
>>> import billiards
>>> from billiards import visualize
```



### First shot in Pool (no friction)

Construct the billiard table:

```pycon
>>> width, length = 112, 224
bounds = [
    billiards.InfiniteWall((0, 0), (length, 0)),  # bottom side
    billiards.InfiniteWall((length, 0), (length, width)),  # right side
    billiards.InfiniteWall((length, width), (0, width)),  # top side
    billiards.InfiniteWall((0, width), (0, 0))  # left side
]
bld = billiards.Billiard(obstacles=bounds)
```

Arrange the balls in a pyramid shape:

```pycon
>>> from math import sqrt
>>> radius = 2.85
>>> for i in range(5):
>>>     for j in range(i + 1):
>>>         x = 0.75 * length + radius * sqrt(3) * i
>>>         y = width / 2 + radius * (2 * j - i)
>>>         bld.add_ball((x, y), (0, 0), radius)
```

Add the white ball and give it a push, then view the animation:

```pycon
>>> bld.add_ball((0.25 * length, width / 2), (length / 3, 0), radius)
>>> anim = visualize.animate(bld, end_time=10)
>>> anim._fig.set_size_inches((10, 5.5))
>>> plt.show()
```

See [pool.mp4](./examples/pool.mp4)



### Brownian motion

The billiard table is a square box:

```pycon
>>> obs = [
>>>     billiards.InfiniteWall((-1, -1), (1, -1)),  # bottom side
>>>     billiards.InfiniteWall((1, -1), (1, 1)),  # right side
>>>     billiards.InfiniteWall((1, 1), (-1, 1)),  # top side
>>>     billiards.InfiniteWall((-1, 1), (-1, -1)),  # left side
>>>     billiards.Disk((0, 0), radius=0.5)  # disk in the middle
>>> ]
>>> bld = billiards.Billiard(obstacles=obs)
```

Distribute small particles (atoms) uniformly in the square, moving in random directions but with the same speed:

```pycon
>>> from math import cos, pi, sin
>>> from random import uniform
>>> for i in range(250):
>>>     pos = [uniform(-1, 1), uniform(-1, 1)]
>>>     angle = uniform(0, 2 * pi)
>>>     vel = [cos(angle), sin(angle)]
>>>     bld.add_ball(pos, vel, radius=0.01, mass=1)
```

Add a bigger ball (like a dust particle)

```pycon
bld.add_ball((0, 0), (0, 0), radius=0.1, mass=10)
```

and simulate until t = 50, recording the position of the bigger ball at each collision (this will take some time)

```pycon
>>> poslist = []
>>> t_next = 0
>>> while t_next < 50:
>>>     bld.evolve(t_next)
>>>     poslist.append(bld.balls_position[-1].copy())
>>>     t_next = min(bld.toi_min[-1][0], bld.obstacles_toi[-1][0])
>>> bld.evolve(50)
>>> poslist.append(bld.balls_position[-1])
```

Plot the billiard and overlay the path of the particle

```pycon
>>> fig = visualize.plot(bld, velocity_arrow_factor=0)
>>> fig.set_size_inches((7, 7))
>>> ax = fig.gca()
>>> poslist = np.asarray(poslist)
>>> ax.plot(poslist[:, 0], poslist[:, 1], marker=".", color="red")
>>> plt.show()
```

![Brownian motion](docs/_images/brownian_motion.svg)



## Authors

- Markus Ebke - <https://github.com/markus-ebke>

# Changelog

**v0.5.0**
- Use numpy's `argmin`-function for finding next collision, billiards with many ball-ball collisions are now up to 3x faster!
- Visualization improvements: Use progress bar in `animate`, scale/disable velocity indicators in `plot` and `animate`, plot `InfiniteWall` as an infinite line (duh! 🤦)
- Rework documentation and include more examples: ideal gas in a box, compute pi from pool (now the standard example in README.md)
- Change imports: Obstacles can be imported from top-level module, `visualize` module must be imported manually (better if the visualize feature is not wanted)
- Add pre-commit and automatically apply code formatting and linting on every commit

**v0.4.0**
- Add basic obstacles (disk and infinite wall)
- Add interaction with simulations via pyglet
- Add examples

**v0.3.0**
- Add visualizations with matplotlib (plot and animate)

**v0.2.0**
- Implement time of impact calculation and collision handling

**v0.1.0**
- Setup package files, configure tools and add basic functionality

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/markus-ebke/python-billiards",
    "name": "billiards",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.7",
    "maintainer_email": "",
    "keywords": "python3,physics-engine,physics-2d",
    "author": "Markus Ebke",
    "author_email": "markus.ebke92@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/f1/e1/b9b9549879c9ac35917d2a139ea413c984ccd82f0359890e4b4f1054cdaf/billiards-0.5.0.tar.gz",
    "platform": "any",
    "description": "# billiards\n\n> 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](http://pyglet.org).\n- Free software: GPLv3+ license.\n\n\n\n## Installation\n\n**billiards** depends on [numpy](https://numpy.org).\nAdditionally, billiard systems can be visualized with [matplotlib](https://matplotlib.org) and [pyglet](http://pyglet.org) (and [tqdm](https://tqdm.github.io) to display progress in `visualize.animate`).\nBut this feature is optional.\n\nClone the repository from GitHub and install the package:\n\n```shell\ngit clone https://github.com/markus-ebke/python-billiards.git\npip install .[visualize]\n```\n\n\n\n## Usage\n\nAll important classes (the billiard simulation and obstacles) are accessible from the top-level module.\nThe visualization module must be imported separately and tries to load *matplotlib*, *tqdm* and *pyglet*.\n\n```pycon\n>>> import billiards  # access to Billiard, Disk and InfiniteWall\n>>> from billiards import visualize  # for plot, animate and interact\n>>> import matplotlib.pyplot as plt  # for plt.show()\n```\n\nLet's compute the first few digits of \u03c0 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>>> obstacles = [billiards.obstacles.InfiniteWall((0, -1), (0, 1), inside=\"right\")]\n>>> bld = billiards.Billiard(obstacles)\n>>> bld.add_ball((3, 0), (0, 0), radius=0.2, mass=1)  # returns index of new ball\n0\n>>> 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>>> visualize.plot(bld)\n<Figure size 800x600 with 1 Axes>\n>>> 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 from _bld.time_ until a given end time.\nIt returns a list of collisions (ball-ball and ball-obstacle collisions).\n\n```pycon\n>>> bld.toi_next  # next ball-ball collision, its a (time, index, index)-triplet\n(1.8000000000000005, 0, 1)\n>>> total_collisions = 0\n>>> for i in [1, 2, 3, 4, 5]:\n...     total_collisions += len(bld.evolve(i))\n...     print(f\"Until t = {bld.time}: {total_collisions} collisions\")\nUntil t = 1: 0 collisions\nUntil t = 2: 1 collisions\nUntil t = 3: 1 collisions\nUntil t = 4: 4 collisions\nUntil t = 5: 314152 collisions\n```\n\nThe first collision happened at time t = 1.8.\nUntil t = 4 there were only 4 collisions, but then between t = 4 and t = 5 there were several thousands.\nLet's see how the situation looks now:\n\n```pycon\n>>> bld.time  # current time\n5\n>>> visualize.plot(bld)\n<Figure size 800x600 with 1 Axes>\n>>> 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.\nAs we can check, there won't be any other collisions after this time:\n\n```pycon\n>>> total_collisions += len(bld.evolve(16))\n>>> bld.balls_velocity  # nx2 numpy array where n is the number of balls\narray([[0.73463055, 0.        ],\n       [1.        , 0.        ]])\n>>> bld.toi_next  # next ball-ball collision\n(inf, -1, 0)\n>>> bld.obstacles_next  # next ball-obstacle collision\n(inf, 0, None)\n>>> visualize.plot(bld)\n<Figure size 800x600 with 1 Axes>\n>>> 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>>> total_collisions\n314159\n>>> import math\n>>> math.pi\n3.141592653589793\n```\n\nThe first six digits match!\nFor an explanation why this happens, see Galperin's paper [Playing pool with \u03c0 (the number \u03c0 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>>> 100 ** 5 * (-1) ** 2 / 2  # kinetic energy = m v^2 / 2 at the beginning\n5000000000.0\n>>> v_squared = (bld.balls_velocity ** 2).sum(axis=1)\n>>> (bld.balls_mass * v_squared).sum() / 2  # kinetic energy now\n4999999999.990375\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>>> import matplotlib.pyplot as plt\n>>> import billiards\n>>> from billiards import visualize\n```\n\n\n\n### First shot in Pool (no friction)\n\nConstruct the billiard table:\n\n```pycon\n>>> width, length = 112, 224\nbounds = [\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]\nbld = billiards.Billiard(obstacles=bounds)\n```\n\nArrange the balls in a pyramid shape:\n\n```pycon\n>>> from math import sqrt\n>>> radius = 2.85\n>>> 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\nAdd the white ball and give it a push, then view the animation:\n\n```pycon\n>>> bld.add_ball((0.25 * length, width / 2), (length / 3, 0), radius)\n>>> anim = visualize.animate(bld, end_time=10)\n>>> anim._fig.set_size_inches((10, 5.5))\n>>> plt.show()\n```\n\nSee [pool.mp4](./examples/pool.mp4)\n\n\n\n### Brownian motion\n\nThe billiard table is a square box:\n\n```pycon\n>>> 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>>>     billiards.Disk((0, 0), radius=0.5)  # disk in the middle\n>>> ]\n>>> 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>>> from math import cos, pi, sin\n>>> from random import uniform\n>>> 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\nAdd a bigger ball (like a dust particle)\n\n```pycon\nbld.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>>> poslist = []\n>>> t_next = 0\n>>> while t_next < 50:\n>>>     bld.evolve(t_next)\n>>>     poslist.append(bld.balls_position[-1].copy())\n>>>     t_next = min(bld.toi_min[-1][0], bld.obstacles_toi[-1][0])\n>>> bld.evolve(50)\n>>> poslist.append(bld.balls_position[-1])\n```\n\nPlot the billiard and overlay the path of the particle\n\n```pycon\n>>> fig = visualize.plot(bld, velocity_arrow_factor=0)\n>>> fig.set_size_inches((7, 7))\n>>> ax = fig.gca()\n>>> poslist = np.asarray(poslist)\n>>> ax.plot(poslist[:, 0], poslist[:, 1], marker=\".\", color=\"red\")\n>>> plt.show()\n```\n\n![Brownian motion](docs/_images/brownian_motion.svg)\n\n\n\n## Authors\n\n- Markus Ebke - <https://github.com/markus-ebke>\n\n# Changelog\n\n**v0.5.0**\n- Use numpy's `argmin`-function for finding next collision, billiards with many ball-ball collisions are now up to 3x faster!\n- Visualization improvements: Use progress bar in `animate`, scale/disable velocity indicators in `plot` and `animate`, plot `InfiniteWall` as an infinite line (duh! \ud83e\udd26)\n- Rework documentation and include more examples: ideal gas in a box, compute pi from pool (now the standard example in README.md)\n- Change imports: Obstacles can be imported from top-level module, `visualize` module must be imported manually (better if the visualize feature is not wanted)\n- Add pre-commit and automatically apply code formatting and linting on every commit\n\n**v0.4.0**\n- Add basic obstacles (disk and infinite wall)\n- Add interaction with simulations via pyglet\n- Add examples\n\n**v0.3.0**\n- Add visualizations with matplotlib (plot and animate)\n\n**v0.2.0**\n- Implement time of impact calculation and collision handling\n\n**v0.1.0**\n- Setup package files, configure tools and add basic functionality\n",
    "bugtrack_url": null,
    "license": "GNU General Public License v3 or later (GPLv3+)",
    "summary": "A 2D physics engine for simulating dynamical billiards",
    "version": "0.5.0",
    "project_urls": {
        "Bug Reports": "https://github.com/markus-ebke/python-billiards/issues",
        "Homepage": "https://github.com/markus-ebke/python-billiards",
        "Source Code": "https://github.com/markus-ebke/python-billiards"
    },
    "split_keywords": [
        "python3",
        "physics-engine",
        "physics-2d"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "c083a9bc33873322b9d8d5aec4b6718fcb12ac4dc1fa22dc82002ceb1990fc39",
                "md5": "274910b24915be52a5dd79078d0173db",
                "sha256": "065b792dd23f7b9b1417b357812275c0fbec5d70b58bcc68f1c26fcfbefef9c9"
            },
            "downloads": -1,
            "filename": "billiards-0.5.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "274910b24915be52a5dd79078d0173db",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7",
            "size": 31760,
            "upload_time": "2023-05-12T21:27:08",
            "upload_time_iso_8601": "2023-05-12T21:27:08.001440Z",
            "url": "https://files.pythonhosted.org/packages/c0/83/a9bc33873322b9d8d5aec4b6718fcb12ac4dc1fa22dc82002ceb1990fc39/billiards-0.5.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "f1e1b9b9549879c9ac35917d2a139ea413c984ccd82f0359890e4b4f1054cdaf",
                "md5": "85d3b5b611c5cd1b424eec2dd4e49236",
                "sha256": "b549059aaea2369de20368c9619c1dd39c36c03f44960a370d4eacdf63d0e30a"
            },
            "downloads": -1,
            "filename": "billiards-0.5.0.tar.gz",
            "has_sig": false,
            "md5_digest": "85d3b5b611c5cd1b424eec2dd4e49236",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7",
            "size": 10558169,
            "upload_time": "2023-05-12T21:27:52",
            "upload_time_iso_8601": "2023-05-12T21:27:52.486180Z",
            "url": "https://files.pythonhosted.org/packages/f1/e1/b9b9549879c9ac35917d2a139ea413c984ccd82f0359890e4b4f1054cdaf/billiards-0.5.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-05-12 21:27:52",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "markus-ebke",
    "github_project": "python-billiards",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "tox": true,
    "lcname": "billiards"
}
        
Elapsed time: 0.08932s