High Level API
DynamicalBilliards
was created with ease-of-use as its main cornerstone. With 3 simple steps, the user can get the output of the propagation of a particle in a billiard.
In general, the workflow of DynamicalBilliards
follows these simple steps:
- Create a billiard.
- Create particles inside that billiard.
- Get the output you want by using one of the high level functions.
Adding more complexity in your billiard does not add complexity in your code. For example, to implement a ray-splitting billiard you only need to define one additional variable, a RaySplitter
and pass it to the high level functions.
After reading through this page, you will be able to use almost all aspects of DynamicalBilliards
with minimal effort.
Visualizing the billiards, particles, and their motion is one of the most important parts of the DynamicalBilliards
. It is not discussed in this page however, but rather in the Visualizing & Animating page.
Billiard
A Billiard
is simply a collection of Obstacle
subtypes. Particles are propagating inside a Billiard
, bouncing from obstacle to obstacle while having constant velocity in-between.
There is a tutorial on how to create your own billiard. In addition, there are many pre-defined billiards that can be found in the Standard Billiards Library section. That is why knowing how to construct a Billiard
is not important at this point.
In this page we will be using the Bunimovich billiard as an example:
using DynamicalBilliards
bd = billiard_bunimovich()
Billiard{Float64} with 4 obstacles:
Bottom wall
Right semicircle
Top wall
Left semicircle
Particles
A "particle" is that thingy that moves around in the billiard. It always moves with velocity of measure 1, by convention.
Currently there are two types of particles:
Particle
, which propagates as a straight line.MagneticParticle
, which propagates as a circle instead of a line (similar to electrons in a perpendicular magnetic field).
To make a particle, provide the constructor with some initial conditions:
x0 = rand(); y0 = rand();
φ0 = 2π*rand()
p = Particle(x0, y0, φ0)
Particle{Float64}
position: [0.3527077028629976, 0.4012699175882124]
velocity: [0.9644462576348167, -0.264279049745145]
To create a MagneticParticle
simply provide the constructor with one more number, the angular velocity:
ω = 0.5
mp = MagneticParticle(x0, y0, φ0, ω)
MagneticParticle{Float64}
position: [0.3527077028629976, 0.4012699175882124]
velocity: [0.9644462576348167, -0.264279049745145]
ang. velocity: 0.5
When creating a billiard or a particle, the object is printed with {Float64}
at the end. This shows what type of numbers are used for all numerical operations. If you are curious you can learn more about it in the Numerical Precision.
You can initialize several particles with the same direction but slightly different position is the following function:
DynamicalBilliards.particlebeam
— Functionparticlebeam(x0, y0, φ, N, dx, ω = nothing, T = eltype(x0)) → ps
Make N
particles, all with direction φ
, starting at x0, y0
. The particles don't all have the same position, but are instead spread by up to dx
in the direction normal to φ
.
The particle element type is T
.
Keep in mind that the particle must be initialized inside a billiard for any functionality to work properly and make sense. If you are not sure what we mean by that, then you should check out the Internals page.
Random initial conditions
If you have a Billiard
which is not a rectangle, creating many random initial conditions inside it can be a pain. Fortunately, we have the following function:
DynamicalBilliards.randominside
— Functionrandominside(bd::Billiard [, ω]) → p
Return a particle p
with random allowed initial conditions inside the given billiard. If supplied with a second argument the type of the returned particle is MagneticParticle
, with angular velocity ω
.
WARNING : randominside
works for any convex billiard but it does not work for non-convex billiards. (this is because it uses distance
)
DynamicalBilliards.randominside_xyφ
— Functionrandominside_xyφ(bd::Billiard) → x, y, φ
Same as randominside
but only returns position and direction.
For example:
p = randominside(bd)
Particle{Float64}
position: [1.1233774423498066, 0.14197694709147857]
velocity: [0.18914044670766636, 0.9819500452768585]
and
mp = randominside(bd, ω)
MagneticParticle{Float64}
position: [-0.2980278160925658, 0.4928813907203643]
velocity: [-0.9575236926021939, -0.28835460479322883]
ang. velocity: 0.5
randominside
always creates particles with same number type as the billiard.
evolve
& timeseries
Now that we have created a billiard and a particle inside, we want to evolve it! There is a simple function for that, called evolve!
(or evolve
if you don't want to mutate the particle), which returns the time, position and velocities at the collision points:
DynamicalBilliards.evolve!
— Functionevolve!([p::AbstractParticle,] bd::Billiard, t)
Evolve the given particle p
inside the billiard bd
. If t
is of type AbstractFloat
, evolve for as much time as t
. If however t
is of type Int
, evolve for as many collisions as t
. Return the states of the particle between collisions.
This function mutates the particle, use evolve
otherwise. If a particle is not given, a random one is picked through randominside
.
Return
ct::Vector{T}
: Collision times.poss::Vector{SVector{2,T}}
: Positions at the collisions.vels::Vector{SVector{2,T}})
: Velocities exactly after the collisions.ω
, eitherT
orVector{T}
: Angular velocity/ies (returned only for magnetic particles).
The time ct[i+1]
is the time necessary to reach state poss[i+1], vels[i+1]
starting from the state poss[i], vels[i]
. That is why ct[1]
is always 0 since poss[1], vels[1]
are the initial conditions. The angular velocity ω[i]
is the one the particle has while propagating from state poss[i], vels[i]
to i+1
.
Notice that at any point, the velocity vector vels[i]
is the one obdained after the specular reflection of the i-1
th collision.
Ray-splitting billiards
evolve!(p, bd, t, raysplitters)
To implement ray-splitting, the evolve!
function is supplemented with a fourth argument, raysplitters
which is a tuple of RaySplitter
instances. Notice that evolve
always mutates the billiard if ray-splitting is used! For more information and instructions on using ray-splitting please visit the official documentation.
Forget the ray-splitting part for now (see Ray-Splitting).
Let's see an example:
ct, poss, vels = evolve(p, bd, 100)
for i in 1:5
println(round(ct[i], digits=3), " ", poss[i], " ", vels[i])
end
0.0 [1.1233774423498066, 0.14197694709147857] [0.18914044670766636, 0.9819500452768585]
0.791 [1.2730219811239667, 0.9188782613399081] [-0.8220391111340614, -0.5694310316148238]
1.609 [-0.04988317577067812, 0.0024945540247507387] [-0.6926218164898774, 0.7213009214760945]
0.649 [-0.49911901689744775, 0.47033171775573623] [0.6022963974042319, 0.7982725409744995]
0.649 [-0.10846841616017253, 0.9880928218030907] [0.8837076900276764, -0.4680392276144687]
Similarly, for magnetic propagation
ct, poss, vels, ω = evolve(mp, bd, 100)
for i in 1:10
println(round(ct[i], digits=3), " ", poss[i], " ", vels[i])
end
0.0 [-0.2980278160925658, 0.4928813907203643] [-0.9575236926021939, -0.28835460479322883]
0.208 [-0.4939666854533699, 0.4225602578632197] [0.9963358245449718, -0.08552733322331635]
1.731 [1.0832572089686718, 0.9930195099128893] [0.4407939197094802, -0.8976083334880269]
0.701 [1.495105056836349, 0.4302075742284694] [-0.8852642326360515, -0.4650884199969977]
0.705 [0.9408493759551109, 0.0] [-0.6701604455218167, 0.7422162604376167]
1.603 [-0.4742208019696549, 0.6584759634116714] [0.8194532868516299, -0.5731459767529217]
2.074 [1.4994496175369474, 0.47654622541686104] [-0.8676441615696581, 0.4971856885451206]
2.093 [-0.4991501699191528, 0.47086054445119174] [0.9173527092908018, -0.39807537823485994]
2.067 [1.465527982116663, 0.6824382028698693] [-0.9924416444242175, -0.12271749024713541]
1.477 [0.19318933060572374, 0.0] [-0.6512225429892826, 0.7588868160026052]
The above return types are helpful in some applications. In other applications however one prefers to have the time series of the individual variables. For this, the timeseries
function is used:
DynamicalBilliards.timeseries!
— Functiontimeseries!([p::AbstractParticle,] bd::Billiard, t; dt, warning)
Evolve the given particle p
inside the billiard bd
for the condition t
and return the x, y, vx, vy timeseries and the time vector. If t
is of type AbstractFloat
, then evolve for as much time as t
. If however t
is of type Int
, evolve for as many collisions as t
. Otherwise, t
can be any function, that takes as an input t(n, τ, i, p)
and returns true
when the evolution should terminate. Here n
is the amount of obstacles collided with so far, τ
the amount time evolved so far, i
the obstacle just collided with and p
the particle (so you can access e.g. p.pos
).
This function mutates the particle, use timeseries
otherwise. If a particle is not given, a random one is picked through randominside
.
The keyword argument dt
is the time step used for interpolating the time series in between collisions. dt
is capped by the collision time, as the interpolation always stops at collisions. For straight propagation dt = Inf
, while for magnetic dt = 0.01
.
For pinned magnetic particles, timeseries!
issues a warning and returns the trajectory of the particle. If t
is integer, the trajectory is evolved for one full circle only.
Internally uses DynamicalBilliards.extrapolate
.
Ray-splitting billiards
timeseries!(p, bd, t, raysplitters; ...)
To implement ray-splitting, the timeseries!
function is supplemented with a fourth argument, raysplitters
which is a tuple of RaySplitter
instances. Notice that timeseries
always mutates the billiard if ray-splitting is used! For more information and instructions on using ray-splitting please visit the official documentation.
For example:
xt, yt, vxt, vyt, t = timeseries(p, bd, 100)
# print as a matrix:
hcat(xt, yt, vxt, vyt, t)[1:5, :]
5×5 Matrix{Float64}:
1.12338 0.141977 0.18914 0.98195 0.0
1.27302 0.918878 -0.822039 -0.569431 0.791182
-0.0498832 0.00249455 -0.692622 0.721301 2.40048
-0.499119 0.470332 0.602296 0.798273 3.04908
-0.108468 0.988093 0.883708 -0.468039 3.69768
Same story for magnetic particles:
# evolve the magnetic particle instead:
xt, yt, vxt, vyt, t = timeseries(mp, bd, 100)
# print as a matrix:
hcat(xt, yt, vxt, vyt, t)[1:5, :]
5×5 Matrix{Float64}:
-0.298028 0.492881 -0.957524 -0.288355 0.0
-0.307596 0.489974 -0.95607 -0.293139 0.01
-0.317149 0.487019 -0.954592 -0.297915 0.02
-0.326688 0.484016 -0.953091 -0.302684 0.03
-0.336211 0.480965 -0.951565 -0.307446 0.04
Sometimes we may need information about which obstacles a particle visited, in which sequence, and when. For this we have the following function:
DynamicalBilliards.visited_obstacles!
— Functionvisited_obstacles!([p::AbstractParticle,] bd::Billiard, t)
Evolve the given particle p
inside the billiard bd
exactly like evolve!
. However return only:
ts::Vector{T}
: Vector of time points of when each collision occured.obst::Vector{Int}
: Vector of obstacle indices inbd
that the particle collided with at the time points ints
.
The first entries are 0.0
and 0
. Similarly with evolve!
the function does not record collisions with periodic walls.
Currently does not support raysplitting. Returns empty arrays for pinned particles.
Remember that the behavior of time evolution depends on the type of the t
argument, which represents "total amount". If it is AbstractFloat
, it represents total amount of time, but if it is Int
it represents total number of collisions.
Poincaré Sections
DynamicalBilliards.psos
— Functionpsos(bd::Billiard, plane::InfiniteWall, t, particles)
Compute the Poincaré section of the particles
with the given plane
, by evolving each one for time t
(either integer or float) inside bd
.
The plane
can be an InfiniteWall
of any orientation, however only crossings of the plane
such that dot(velocity, normal) < 0
are allowed, with normal
the normal unit vector of the plane
.
particles
can be:
- A single particle.
- A
Vector
of particles. - An integer
n
optionally followed by an angular velocityω
.
Return the positions poss
and velocities vels
at the instances of crossing the plane
. If given more than one particle, the result is a vector of vectors of vectors.
Notice - This function can handle pinned particles. If a pinned particle can intersect with the plane
, then an intersection is returned. If however it can't then empty vectors are returned.
For example, the surface of section in the periodic Sinai billiard with magnetic field reveals the mixed nature of the phase-space:
using DynamicalBilliards, CairoMakie
t = 100; r = 0.15
bd = billiard_sinai(r, setting = "periodic")
# the direction of the normal vector is IMPORTANT!!!
# (always keep in mind that ω > 0 means counter-clockwise rotation!)
plane = InfiniteWall([0.5, 0.0], [0.5, 1.0], [-1.0, 0.0])
posvector, velvector = psos(bd, plane, t, 1000, 2.0)
c(a) = length(a) == 1 ? "#6D44D0" : "#DA5210"
fig = Figure(); ax = Axis(fig[1,1]; xlabel = L"y", ylabel=L"v_y")
for i in 1:length(posvector)
poss = posvector[i] # vector of positions
vels = velvector[i] # vector of velocities at the section
L = length(poss)
if L > 0
#plot y vs vy
y = [a[2] for a in poss]
vy = [a[2] for a in vels]
scatter!(y, vy; color = c(y), markersize = 2)
end
end
fig
The psos
function always calculates the crossings within the unit cell of a periodic billiard. This means that no information about the "actual" position of the particle is stored, everything is modulo the unit cell.
This can be seen very well in the above example, where there aren't any entries in the region 0.5 - r ≤ y ≤ 0.5 + r
.
Of course it is very easy to "re-normalize" the result such that it is coherent. The only change we have to do is simply replace the line y = [a[2] for a in poss]
with
y = [a[2] < 0.5 ? a[2] + 1 : a[2] for a in poss]
Escape Times
It is very easy to create your own function that calculates an "escape time": the time until the particle leaves the billiard by meeting a specified condition. There is also a high-level function for this though:
DynamicalBilliards.escapetime
— Functionescapetime([p,] bd, t; warning = false)
Calculate the escape time of a particle p
in the billiard bd
, which is the time until colliding with any "door" in bd
. As a "door" is considered any FiniteWall
with field isdoor = true
.
If the particle evolves for more than t
(integer or float) without colliding with the Door
(i.e. escaping) the returned result is Inf
.
A warning can be thrown if the result is Inf
. Enable this using the keyword warning = true
.
If a particle is not given, a random one is picked through randominside
. See parallelize
for a parallelized version.
To create a "door" simply use FiniteWall
.
For example, the default implementation of the mushroom billiard has a "door" at the bottom of the stem. Thus,
using Statistics
bd = billiard_mushroom()
et = zeros(100)
for i ∈ 1:100
particle = randominside(bd)
et[i] = escapetime(particle, bd, 10000)
end
println("Out of 100 particles, $(count(x-> x != Inf, et)) escaped")
println("Mean escape time was $(mean(et[et .!= Inf]))")
Out of 100 particles, 18 escaped
Mean escape time was 4.021971159341341
Of course, escapetime
works with MagneticParticle
as well
escapetime(randominside(bd, 1.0), bd, 10000)
43.4110151339446
Mean Collision Times
Because the computation of a mean collision time (average time between collisions in a billiard) is often a useful quantity, the following function computes it
DynamicalBilliards.meancollisiontime
— Functionmeancollisiontime([p,] bd, t) → κ
Compute the mean collision time κ
of the particle p
in the billiard bd
by evolving for total amount t
(either float for time or integer for collision number).
Collision times are counted only between obstacles that are not PeriodicWall
.
If a particle is not given, a random one is picked through randominside
. See parallelize
for a parallelized version.
For example,
bd = billiard_sinai()
meancollisiontime(randominside(bd), bd, 10000.0)
0.4486282806014667
Parallelization
DynamicalBilliards.parallelize
— Functionparallelize(f, bd::Billiard, t, particles; partype = :threads)
Parallelize function f
across the available particles. The parallelization type can be :threads
or :pmap
, which use threads or a worker pool initialized with addprocs
before using DynamicalBilliards
.
particles
can be:
- A
Vector
of particles. - An integer
n
optionally followed by an angular velocityω
. This usesrandominside
.
The functions usable here are:
meancollisiontime
escapetime
lyapunovspectrum
(returns only the maximal exponents)boundarymap
(returns vector of vectors of 2-vectors andarcintervals
)
Here are some examples
bd = billiard_stadium()
particles = [randominside(bd) for i in 1:1000]
parallelize(meancollisiontime, bd, 1000, particles)
1000-element Vector{Float64}:
1.1036470567751744
1.0871207561923595
1.1191803042521349
1.0718150255730958
1.0694666609646977
1.10388301042489
1.102827153273332
1.0771852944200162
1.1126130944617518
1.048026560893698
⋮
1.0985589376379064
1.0766005212146483
1.0858777641206665
1.1060758953001573
1.1151813755984576
1.1199524241021304
1.0232601029078092
1.0255908941309613
1.0911691731291364
parallelize(lyapunovspectrum, bd, 1000, particles)
1000-element Vector{Float64}:
0.8472990554715686
0.8863036627255451
0.8546569818827497
0.8482656720190859
0.9249025970613489
0.9142324919614702
0.8425922214333129
0.9195639881762757
0.871825512684627
0.8696111740120869
⋮
0.8835615142308221
0.9057492207127156
0.8941846262333814
0.9011684855925866
0.8770237331799322
0.8991994811096871
0.8523558747071925
0.9261871331006656
0.8834688537969788
It's all about bounce!
The main propagation algorithm used by DynamicalBilliards
is bundled in the following well-behaving function:
DynamicalBilliards.bounce!
— Functionbounce!(p::AbstractParticle, bd::Billiard) → i, t, pos, vel
"Bounce" the particle (advance for one collision) in the billiard. Takes care of finite-precision issues.
Return:
- index of the obstacle that the particle just collided with
- the time from the previous collision until the current collision
t
- position and velocity of the particle at the current collision (after the collision has been resolved!). The position is given in the unit cell of periodic billiards. Do
pos += p.current_cell
for the position in real space.
bounce!(p, bd, raysplit) → i, t, pos, vel
Ray-splitting version of bounce!
.
bounce!
is the function used internally by all high-level functions, like evolve!
, boundarymap
, escapetime
, etc.
This is the function a user should use if they want to calculate other things besides what is already available in the high level API.
Standard Billiards Library
All standard billiards have a function version that accepts keyword arguments instead of positional arguments, for ease of use.
DynamicalBilliards.billiard_rectangle
— Functionbilliard_rectangle(x=1.0, y=1.0; setting = "standard")
Return a vector of obstacles that defines a rectangle billiard of size (x
, y
).
Settings
- "standard" : Specular reflection occurs during collision.
- "periodic" : The walls are
PeriodicWall
type, enforcing periodicity at the boundaries - "random" : The velocity is randomized upon collision.
- "ray-splitting" : All obstacles in the billiard allow for ray-splitting.
DynamicalBilliards.billiard_sinai
— Functionbilliard_sinai(r=0.25, x=1.0, y=1.0; setting = "standard")
Return a vector of obstacles that defines a Sinai billiard of size (x
, y
) with a disk in its center, of radius r
.
In the periodic case, the system is also known as "Lorentz Gas".
Settings
- "standard" : Specular reflection occurs during collision.
- "periodic" : The walls are
PeriodicWall
type, enforcing periodicity at the boundaries - "random" : The velocity is randomized upon collision.
- "ray-splitting" : All obstacles in the billiard allow for ray-splitting.
DynamicalBilliards.billiard_bunimovich
— Functionbilliard_bunimovich(l=1.0, w=1.0)
Return a vector of Obstacle
s that define a Buminovich billiard, also called a stadium. The length is considered without the attached semicircles, meaning that the full length of the billiard is l + w
. The left and right edges of the stadium are Semicircle
s.
billiard_stadium
is an alias of billiard_bunimovich
.
DynamicalBilliards.billiard_mushroom
— Functionbilliard_mushroom(sl = 1.0, sw = 0.2, cr = 1.0, sloc = 0.0; door = true)
Create a mushroom billiard with stem length sl
, stem width sw
and cap radius cr
. The center of the cap (which is Semicircle) is always at [0, sl]
. The center of the stem is located at sloc
.
Optionally, the bottom-most Wall
is a Door
(see escapetime
).
DynamicalBilliards.billiard_polygon
— Functionbilliard_polygon(n::Int, R, center = [0,0]; setting = "standard")
Return a vector of obstacles that defines a regular-polygonal billiard with n
sides, radius r
and given center
.
Note: R
denotes the so-called outer radius, not the inner one.
Settings
- "standard" : Specular reflection occurs during collision.
- "periodic" : The walls are
PeriodicWall
type, enforcing periodicity at the boundaries. Only available forn=4
orn=6
. - "random" : The velocity is randomized upon collision.
DynamicalBilliards.billiard_vertices
— Functionbilliard_vertices(v, type = FiniteWall)
Construct a polygon billiard that connects the given vertices v
(vector of 2-vectors). The vertices should construct a billiard in a counter-clockwise orientation (i.e. the normal vector always points to the left of v[i+1] - v[i]
.).
type
decides what kind of walls to use. Ths function assumes that the first entry of v
should be connected with the last.
DynamicalBilliards.billiard_iris
— Functionbilliard_iris(a=0.2, b=0.4, w=1.0; setting = "standard")
Return a billiard that is a square of side w
enclosing at its center an ellipse with semi axes a
, b
.
DynamicalBilliards.billiard_hexagonal_sinai
— Functionbilliard_hexagonal_sinai(r, R, center = [0,0]; setting = "standard")
Create a sinai-like billiard, which is a hexagon of outer radius R
, containing at its center (given by center
) a disk of radius r
. The setting
keyword is passed to billiard_polygon
.
DynamicalBilliards.billiard_raysplitting_showcase
— Functionbilliard_raysplitting_showcase(x=2.0, y=1.0, r1=0.3, r2=0.2) -> bd, rayspl
Showcase example billiard for ray-splitting processes. A rectangle (x,y)
with a SplitterWall at x/2
and two disks at each side, with respective radii r1
, r2
.
Notice: This function returns a billiard bd
as well as a rayspl
dictionary!
DynamicalBilliards.billiard_logo
— Functionbilliard_logo(;h=1.0, α=0.8, r=0.18, off=0.25, T = Float64) -> bd, ray
Create the billiard used as logo of DynamicalBilliards
and return it along with the tuple of raysplitters.
Particle types
DynamicalBilliards.Particle
— TypeParticle(ic::Vector{T}) #where ic = [x0, y0, φ0]
Particle(x0, y0, φ0)
Particle(pos::SVector, vel::SVector)
Create a particle with initial conditions x0, y0, φ0
. It propagates as a straight line.
The field current_cell
shows at which cell of a periodic billiard is the particle currently located.
DynamicalBilliards.MagneticParticle
— TypeMagneticParticle(ic::AbstractVector{T}, ω::Real) # where ic = [x0, y0, φ0]
MagneticParticle(x0, y0, φ0, ω)
MagneticParticle(pos::SVector, vel::SVector, ω)
MagneticParticle(p::AbstractParticle, ω)
Create a magnetic particle with initial conditions x0, y0, φ0
and angular velocity ω
. It propagates as a circle instead of a line, with radius 1/abs(ω)
.
The field current_cell
shows at which cell of a periodic billiard is the particle currently located.