Ray-Splitting

Ray-splitting is a semi-classical approach to the billiard system, giving a wave attribute to the ray traced by the particle. Upon collision a particle may propagate through an obstacle (transmission & refraction) or be reflected. Following the mindset of this package, implementing a ray-splitting billiard requires only three simple steps. We will introduce them and demonstrate them using a simple example in this documentation page.

1. Ray-Splitting Obstacles

The first step is that an Obstacle that supports ray-splitting is required to be present in your billiard table. The only new feature these obstacles have is an additional Boolean field called pflag (propagation flag). This field notes on which side of the obstacle the particle is currently propagating.

The normal vector as well as the distance from boundary change sign depending on the value of pflag. The obstacles Antidot, SplitterWall and FiniteSplitterWall are the equivalents of disk, wall and finite-wall for ray-splitting.

Let's create a billiard with a bunch of ray-splitting obstacles!

using DynamicalBilliards
x, y = 2.0, 1.0
bdr =  billiard_rectangle(x, y)
sw = SplitterWall([x/2, 0.0], [x/2,y], [-1,0], true)
a1 = Antidot([x/4, y/2], 0.25, "Left Antidot")
a2 = Antidot([3x/4, y/2], 0.15, "Right Antidot")
bd = Billiard(a1, a2, sw, bdr...)
Billiard{Float64} with 7 obstacles:
  Left Antidot
  Right Antidot
  Splitter wall
  Bottom wall
  Right wall
  Top wall
  Left wall
using InteractiveDynamics, CairoMakie
fig, ax = bdplot(bd)
fig

(notice also that raysplitting obstacles are by default plotted with a different color and with dotted linestyle)

2. The RaySplitter structure

In the second step, you have to define 2+1 functions: transmission probability, refraction angle and optionally new angular velocity after transmission. These functions, as well as which obstacles participate in ray-splitting, are bundled into a special structure:

DynamicalBilliards.RaySplitterType
RaySplitter(idxs, transmission, refraction [, newangular]; affect)

Return a RaySplitter instance, used to perform raysplitting. idxs is a Vector{Int} with the indices of the obstacles that this RaySplitter corresponds to.

transmission, refraction and newangular are functions. Let φ be the angle of incidence and ω be the angular velocity and pflag the propagation flag (before transmission). The functions have the following signatures:

  1. transmission(φ, pflag, ω) -> T, transmission probability.
  2. refraction(φ, pflag, ω) -> θ, refraction angle. This angle is relative to the normal vector.
  3. newangular(ω, pflag) -> newω, new angular velocity after transmission.

The above three functions use the same convention: the argument pflag is the one the obstacle has before transmission. For example, if a particle is outside an Antidot (with pflag = true here) and is transmitted inside the Antidot (pflag becomes false here), then all three functions will be given their second argument (the Boolean one) as true!

affect is a function, and denotes which obstacles of the billiard are affected when transmission occurs at obstacle i (for which obstacles should the field pflag be reversed). Defaults to idxs = (i) -> i, i.e. only the colliding obstacle is affected. If you want many obstacles to be affected you could write idxs = (i) -> SVector(2,3,5), etc. Keep in mind that the only values of i that can be passed into this function are the ones that are given in the argument idxs!

source

If you want different type of transmission/refraction functions for different obstacles, then you define multiple RaySplitters.

Continuing from the above billiard, let's also create some RaySplitter instances for it.

First define a refraction function

refraction(φ, pflag, ω) = pflag ? 0.5φ : 2.0φ
refraction (generic function with 1 method)

Then, a transmission probability function. In this example, we want to create a function that given some factor p, it returns a probability weighted with p in one direction of ray-splitting and 1-p in another direction.

transmission_p(p) = (φ, pflag, ω) -> begin
    if pflag
        p*exp(-(φ)^2/2(π/8)^2)
    else
        abs(φ) < π/4 ? (1-p)*exp(-(φ)^2/2(π/4)^2) : 0.0
    end
end
transmission_p (generic function with 1 method)

Notice also how we defined the function in such a way that critical refraction is respected, i.e. if θ(φ) ≥ π/2 then T(φ) = 0. Although this is necessary from a physical perspective, the code does take care of it by clamping the refraction angle (see below).

Lastly, for this example we will use magnetic propagation. We define functions such that the antidots also reverse the direction and magnitude of the magnetic field.

newoantidot(x, bool) =  bool ? -2.0x : -0.5x
newowall(x, bool) = bool ? 0.5x : 2.0x
newowall (generic function with 1 method)

Now we create the RaySplitter instances we want

raywall = RaySplitter([3], transmission_p(0.5), refraction, newowall)
raya = RaySplitter([1, 2], transmission_p(0.8), refraction, newoantidot)
RaySplitter for indices [1, 2]
 transmission: #1
 refraction:   refraction
 new angular:  newoantidot
 affect:       default

Because we want to use same functions for both antidots, we gave both indices in raya, [1, 2] (which are the indices of the antidots in the billiard bd).

3. Evolution with Ray-Splitting

The third step is trivial. After you have created your RaySplitter(s), you simply pass them into evolve or timeseries as a fourth argument! If you have many instances of RaySplitter you pass a tuple of them.

For example,

using Random, InteractiveDynamics, CairoMakie
Random.seed!(42)
p = randominside(bd, 1.0)
raysplitters = (raywall, raya)
xt, yt, vxt, vyt, tt = timeseries(p, bd, 100, raysplitters)
fig, ax = bdplot(bd)
lines!(ax, xt, yt)
scatter!(ax, xt[[1, 100]], yt[[1, 100]]; color = "black")
fig

You can see that at some points the particle crossed the boundaries of the red obstacles, which allow for ray splitting.

Resetting the billiard

Notice that evolving a particle inside a billiard always mutates the billiard if ray-splitting is used. This means that you should always set the fields pflag of some obstacles to the values you desire after each call to evolve. If you use the function randominside you must definitely do this!

The function reset_billiard!(bd) turns all pflags to true.

Angle of refraction is clamped

Internally we clamp the output of the angle of refraction function. Let c = DynamicalBilliards.CLAMPING_ANGLE (currently c = 0.1). We clamp θ to -π/2 + c ≤ θ ≤ π/2 - c. This is so that the relocating algorithm does not fall into an infinite loop. You can change the value of c but very small values can lead to infinite loops in extreme cases.

The Ray-Splitting Algorithm

In this section we describe the algorithm we follow to implement the ray-splitting process. Let $T$ denote the transmission function, $\theta$ the refraction function and $\omega_{\text{new}}$ the new angular velocity function. The following describes the process after a particle has reached an obstacle that supports ray-splitting.

  1. Find the angle of incidence $\phi' = \pi - \arccos(\vec{v} \cdot \vec{n}) = \arccos(\vec{v} \cdot (-\vec{n}))$ with $\vec{n}$ the normal vector at collision point. Notice that we use here $-\vec{n}$ because the velocity is opposing the normal vector before the collision happens. Using $-\vec{n}$ gives the angle between 0 and $\pi/2$ instead of $\pi/2$ to $\pi$.

  2. Find the correct sign of the incidence angle, $\phi = \pm \phi'$. Specifically, use the cross product: if the third entry of $\vec{v} \times \vec{n}$ is negative, then have minus sign. The "correct" sign debates on whether the velocity vector is to the right or to the left of $(-\vec{n})$. This is important for finding the correct transmission angle and/or probability.

  3. Check if $T(\phi, \verb|pflag|, \omega) > \text{random}()$. If not, do standard specular reflection.

  4. If ray-splitting happens, then relocate the particle so that it is on the other side of the colliding obstacle. This contrasts the main evolution algorithm of this billiard package.

  5. Re-compute the correct angle of incidence, as the position of the particle generally changes with relocating.

  6. Find refraction angle $\theta(\phi, \verb|pflag|, \omega)$. Notice that this is a relative angle with respect to the normal vector. Also notice that $\theta$ may have opposite sign from $\phi$. It depends on the user if they want to add anomalous refraction.

  7. Set obstacle.pflag = !obstacle.pflag for all obstacles affected by the current RaySplitter. This reverses $\vec{n}$ to $-\vec{n}$ as well! So from now on $\vec{n}$ is the opposite than what it was at the beginning of the algorithm!

  8. Find the refraction angle in absolute space. First find $a = \text{atan}(n_y, n_x)$ and then set $\Theta = a + \theta$.

  9. Perform refraction, i.e. set the particle velocity to the direction of $\Theta$.

  10. Scale the magnetic field, i.e. set p.omega = $\omega_{\text{new}}(\omega, \verb|!pflag|)$. It is important to note that we use !pflag because we have already changed the pflag field.

Physics of the Ray-Splitting Functions

If T is the transmission probability function, then the condition for transmission is simply: T(φ, pflag, ω) > rand(). If it returns true, transmission (i.e. ray-splitting) will happen.

The functions given to RaySplitter should have some properties in order to have physical meaning. In order to test if the RaySplitter you have defined has physical meaning, the function isphysical is provided

DynamicalBilliards.isphysicalFunction
isphysical(raysplitter(s))

Return true if the given raysplitters have physically plausible properties.

Specifically, check if (φ is the incidence angle, θ the refraction angle):

  • Critical angle means total reflection: If θ(φ) ≥ π/2 then Tr(φ) = 0
  • Transmission probability is even function: Tr(φ) ≈ Tr(-φ) at ω = 0
  • Refraction angle is odd function: θ(φ) ≈ -θ(-φ) at ω = 0
  • Ray reversal is true: θ(θ(φ, pflag, ω), !pflag, ω) ≈ φ
  • Magnetic conservation is true: (ωnew(ωnew(ω, pflag), !pflag) ≈ ω
source

Snell's Law

In classical geometric optics, the refraction of a ray of light moving from one medium to another is described by Snell's law. For an angle of incidence of $\phi$, the refraction angle $\theta$ is determined by the equation

\[\frac{sin(\phi)}{\sin(\theta)} = \frac{n'}{n}\]

where $n$ and $n'$ are the respective refractive indices of the media.

To easily simulate these relations in DynamicalBilliards, the function law_of_refraction can be used to set up ray-splitting according to this law.

DynamicalBilliards.law_of_refractionFunction
law_of_refraction(n1, n2 = 1.0) -> t, r

Create transmission and refraction functions t, r that follow Snell's law, i.e. the transmission probability is set to 1.0 except for the case of total internal reflection.

n1 is the index of refraction for the pflag = false side of an obstacle, while n2 is the index of refraction for pflag = true.

source

Animation of Inverse Billiards

Here we will show an application of inverse billiards, where particles go in and out of a billiard, while taking advantage of the existence of a strong magnetic field outside of the billiard to return.

As always, we define the ray-splitting functions:

using DynamicalBilliards
trans(args...) = 1.0 # always perfect transmission
refra(φ, pflag, ω) = pflag ? 0.8φ : 1.25φ # refraction angle
neww(ω, pflag) = pflag ? 2.0 : 0.4
neww (generic function with 1 method)

Now, when we define the RaySplitter instance we will choose a different value for affect:

ray = RaySplitter([1,2,3,4], trans, refra, neww, affect = (i) -> SVector(1,2,3,4))
RaySplitter for indices [1, 2, 3, 4]
 transmission: trans
 refraction:   refra
 new angular:  neww
 affect:       #3

We initialize a simple rectangular billiard and a particle

bd = billiard_rectangle(setting = "ray-splitting")
p = MagneticParticle(0.4, 0.6, 0.0, 0.4)
MagneticParticle{Float64}
position: [0.4, 0.6]
velocity: [1.0, 0.0]
ang. velocity: 0.4

bdplot_interactive does not accept ray-splitters yet (but it is easy if you want to do a PR). Nevertheless, doing an animation is still straightforward:

using InteractiveDynamics, CairoMakie

fig, ax = bdplot(bd)
xlims!(ax, (-1, 2)); ylims!(ax, (-1, 2)) # zoom out
xt, yt = timeseries(p, bd, 10.0, (ray,))
D = 300
xta = Observable(xt[1:D])
yta = Observable(yt[1:D])
lines!(ax, xta, yta; color = :green)

record(fig, "inverse_billiard.mp4", D+1:length(xt); framerate = 30) do i
    push!(xta[], xt[i]); popfirst!(xta[])
    push!(yta[], yt[i]); popfirst!(yta[])
    notify.((xta, yta))
end