Tutorials

Swalbe.jl is software that simulates the dynamics of a thin liquid film. At it's core is the lattice Boltzmann method (LBM) a mesoscale method that works based on particle distribtuion functions $f_i(\mathbf{x},t)$. To get a better understandig of both the LBM and the package we supply instructive simulations and discuss the content of the code.

Constant interface

A nice and easy way to check the consistency of a fluid mechanics solver is to prob the mass or density. Per continuity equation $\partial_t\rho + \nabla\cdot\mathbf{j} = 0$, we require that the local density $\rho$ can only change if there is a flux $\mathbf{j}$ (okay a divergence of a flux). In case of vanishing or constant flux $\nabla\cdot\mathbf{j} = 0$ the density has to be time independent and therefore $\partial_t \rho = 0$.

Thin film flows can be described with the same kind of equation

\[\partial_t h(\mathbf{x},t) + \nabla\left(M(h)\nabla p(\mathbf{x},t)\right) = 0. \]

The local height can only change with time if some pressure gradient

Having a vanshing pressure gradient the height $h(\mathbf{x},t)$ have to be constant independent of time. To check this we put together a small sample simulation that probes this.

using Swalbe # import the package

# Define the system size and parameters
sys = Swalbe.SysConst(Lx = 100,     # 100 lattice units in x-direction 
                      Ly = 100,     # 100 lattice units in y-direction
                      Tmax = 1000)  # LBM time loop runs for 1000 iterations


# Allocation of distribution functions, macroscopic variables and forces 
fout, ftemp, feq, height, velx, vely, vsq, pressure, dgrad, Fx, Fy, slipx, slipy, h∇px, h∇py = Swalbe.Sys(sys,      # Size 
                                                                                                          "CPU",    # Where to run 
                                                                                                          false,    # No fluctuations  
                                                                                                          Float64)  # Num type
# Constant film thickness, or flat interface
height .= 1.0
# Compute the first equilibrium distribtuion function based on the initial conditions 
Swalbe.equilibrium!(ftemp, height, velx, vely, vsq)
# Empty list to measure the total mass over time
mass = []
# To add the forcing due to a pressure gradient set true
pressure_gradient = false
# Start of the lattice Boltzmann time loop
for t in 1:sys.Tmax
    # Fill the list 
    push!(mass,sum(height))
    # Talks with us all t % tdump time sets
    if t % sys.tdump == 0
        println("Time step $t mass is $(round(sum(height), digits=3))")
    end
    if pressure_gradient
        # The pressure computation 
        Swalbe.filmpressure!(pressure, height, dgrad, sys.γ, 1/9, sys.n, sys.m, sys.hmin, sys.hcrit)
        # Pressure gradient
        Swalbe.∇f!(h∇px, h∇py, pressure, dgrad, height)
        # Slippage
        Swalbe.slippage!(slipx, slipy, height, velx, vely, sys.δ, sys.μ)
        # Summation of forces
        Fx .= h∇px .+ slipx
        Fy .= h∇py .+ slipy
    end
    # New equilibrium
    Swalbe.equilibrium!(feq, height, velx, vely, vsq)
    # Collision and streaming of distribution function
    Swalbe.BGKandStream!(fout, feq, ftemp, Fx, Fy)
    # Update of macroscopic quantities
    Swalbe.moments!(height, velx, vely, fout)
    # Next iteration
end
# Check if the mass is constant
using Plots
# Since h = 1 everywhere and the size 100 x 100 the mass has to be 100^2
plot(mass, xlabel="time [Δt]", ylabel="Mass [lbu]", label="flat interface", ylim=(100^2-0.1, 100^2+0.1))

In fact this is a particular strength of the lattice Boltzmann method. Under the assumption that no forces are applied the mass is mathematically conserved. Which is shown in the lower plot

flat

Dewetting of patterned a substrate

Next we take a look at substrates with a wettability gradient and show how to use an image or geometrical shapes to induce directed dewetting. Here we actually reuse the display simulation of the README.md. The idea is to use Youngs equation

\[\cos(\theta_{\text{eq}}) = \frac{\gamma_{sg} - \gamma_{sl}}{\gamma}, \]

where the $\gamma$'s are the three interfacial energies and $\theta_{\text{eq}}$ is the equilibrium contact angle to address the wettability of the substrate. Without problems it is possible to discretize $\theta_{\text{eq}}$ similar to the pressure or the film thickness and therefore effectively introduce a wettability gradient $\nabla \theta_{\text{eq}} \neq 0$. In the logo simulation what actually happens is that initial perturbations (ϵ * randn()) grow with time, leading to a film rupture and travelling rims. The ruptures are triggered in regions of high contact angle and the rims meet in regions of low contact angle (letters of the logo). There is a lot of theory involved and if you want to read up on it check out the review from Craster&Matar (paywall), especially section 5. Films driven by intermolecular forces.

But let's start again with some code, first with a triangle with lower wettability in the middle

using Swalbe

# Define the system size and parameters
sys = Swalbe.SysConst(Lx = 100,     # 100 lattice units in x-direction 
                      Ly = 100,     # 100 lattice units in y-direction
                      n = 3,        # first disjoining pressure exponent
                      m = 2,        # second disjoining pressure exponent
                      Tmax = 1000)  # LBM time loop runs for 1000 iterations


# Allocation of distribution functions, macroscopic variables and forces 
fout, ftemp, feq, height, velx, vely, vsq, pressure, dgrad, Fx, Fy, slipx, slipy, h∇px, h∇py = Swalbe.Sys(sys,      # Size 
                                                                                                          "CPU",    # Where to run 
                                                                                                          false,    # No fluctuations
                                                                                                          Float64)  # Num type
# Random (gaussian) perturbations put on top of a flat interface
ϵ = 0.001
height .= 1.0 .+ ϵ .* randn(sys.Lx, sys.Ly)
# The contact angle field
theta = fill(1/6, sys.Lx, sys.Ly)
# Lower contact angle in the middle of the substrate
Swalbe.trianglepattern(theta, 1/6, δₐ=-1/18)
# Compute the first equilibrium distribution function based on the initial conditions 
Swalbe.equilibrium!(ftemp, height, velx, vely, vsq)
# Difference in total height with time
diff_h = []
# Start of the lattice Boltzmann time loop
for t in 1:sys.Tmax
    # Fill the difference list
    push!(diff_h, maximum(height) - minimum(height))
    # Talks with us all t % tdump time sets
    if t % sys.tdump == 0
        println("Time step $t mass is $(round(sum(height), digits=3))")
    end
    # The pressure computation 
    Swalbe.filmpressure!(pressure, height, dgrad, sys.γ, theta, sys.n, sys.m, sys.hmin, sys.hcrit)
    # Pressure gradient
    Swalbe.∇f!(h∇px, h∇py, pressure, dgrad, height)
    # Slippage
    Swalbe.slippage!(slipx, slipy, height, velx, vely, sys.δ, sys.μ)
    # Summation of forces
    Fx .= -h∇px .- slipx
    Fy .= -h∇py .- slipy
    
    # New equilibrium
    Swalbe.equilibrium!(feq, height, velx, vely, vsq)
    # Collision and streaming of distribution function
    Swalbe.BGKandStream!(fout, feq, ftemp, Fx, Fy)
    # Update of macroscopic quantities
    Swalbe.moments!(height, velx, vely, fout)
    # Next iteration
end
# Another library for plotting and to my understanding actually the best you can do
using CairoMakie
# We are interested in the heatmap of the film thickness and the growth rate of the perturbation
let
	x1 = 1:1:sys.Tmax
	y2 = zeros(sys.Tmax)
	y2 .= diff_h
    fig = Figure(resolution = (960,450))
	
    ax1 = Axis(fig, xlabel = "time [Δt]", ylabel = "Δh [lbu]",xscale = log10, yscale = log10,xgridstyle=:dash, ygridstyle=:dash, xminorticksvisible = true,
        xminorticks = IntervalsBetween(9), yminorticksvisible = true,
        yminorticks = IntervalsBetween(9))
	leg = lines!(ax1, x1, y2, color = :navy)
	ax2 = Axis(fig,  aspect = 1, xlabel = "x [Δx]", ylabel = "y [Δx]")
    hmap = heatmap!(ax2, height, colormap = :viridis)
    cbar = Colorbar(fig, hmap, label = "thickness", ticksize=15, tickalign = 1, width = 15)
	fig[1,1] = ax1
    fig[1,2] = ax2
    fig[1,3] = cbar
    fig
end

Fluid is drained into regions of lower contact angle, therefore into in the triangle. The effect is the strongest around the vertices of the triangle. Since in principle this a dewetting instability with a well defined spectrum computable using the surface tension $\gamma$ and the disjoining pressure functional $\Pi(h)$. The result should look like the plot below

patterned

Of course you can and should play with the parameters ($\gamma$, $\delta$, $h_0$, ...) to get a physically correct simulation :wink:.

Droplet spreading in 1D

If a certain amount of liquid is placed on a surface, like a rain drop :droplet: hitting a plant leaf :fourleafclover: we observe a drop sticking to the leaf. The shape of the droplet is actually nature solving Young's law and finds an equilibrium shape which can be described by a simple macroscopic observable, the contact angle $\theta_{\text{eq}}$. There is of course more to it, for further reading check out Snoeijer & Andreotti We can recast this behavior with a simple and fast simulation.

In contrast to the two examples before we put the whole experiment into a function and just call the function. While this may seem overkill here it is in fact very useful. Instead of writing a script for every experiment, we simply write a function and loop through function arguments. Making it very convenient to perform phase space scans and parameter studies.

using Swalbe
# Simulation that let's a droplet relax towards it's equilibrium contact angle
function run_dropletrelax(
    sys::Swalbe.SysConst_1D;    # System Constants
    radius=20,                  # Initial droplet radius
    θ₀=1/6,                     # Initial contact angle
    center=(sys.L÷2),           # Position of the center of mass
    verbose=true,               # Simulation prints output 
    T=Float64                   # Number subtype
)
    println("Simulating an out of equilibrium droplet")
    # Empty list to store the radius evolution
    diameter = []
    # Allocation
    fout, ftemp, feq, height, vel, pressure, dgrad, F, slip, h∇p = Swalbe.Sys(sys, false, T)
    # Initial condition, see initialvalues.jl, or ?Swalbe.singledroplet
    Swalbe.singledroplet(height, radius, θ₀, center)
    # Initial equilibrium, in this case a D1Q3 equilibrium
    Swalbe.equilibrium!(ftemp, height, vel)
    # Lattice Boltzmann loop starts
    for t in 1:sys.Tmax
        if verbose
        # Check if the mass conserved
            if t % sys.tdump == 0
                mass = 0.0
                mass = sum(height)

                println("Time step $t mass is $(round(mass, digits=3))")
            end
        end
        # Push the number of lattice sides inside the droplet to the list
        push!(diameter, length(findall(height .> 0.06)))
        # Compute film pressure with contact angle \theta = 1/9
        Swalbe.filmpressure!(pressure, height, dgrad, sys.γ, 1/9, sys.n, sys.m, sys.hmin, sys.hcrit)
        # Compute the gradient of the pressure and multiply it with the height
        Swalbe.∇f!(h∇p, pressure, dgrad, height)
        # Calculate the substrate friction, velocity boundary condition
        Swalbe.slippage!(slip, height, vel, sys.δ, sys.μ)
        # Sum the forces up
        F .= h∇p .+ slip
        # New equilibrium
        Swalbe.equilibrium!(feq, height, vel)
        # Collide and stream
        Swalbe.BGKandStream!(fout, feq, ftemp, -F)
        # Compute the new moments
        Swalbe.moments!(height, vel, fout)
    end
    return height, diameter
end

If we defined the function as above we can use it to run several experiments and test for example Tanners law. The idea of Tanner was that the evolution of the droplets radius during spreading should be captured by a powerlaw $R(t) \propto t^{\alpha}$, with $\alpha = 1/10$ in this case. There are subtleties to this which you can read up in this very nice paper by Eddi et al.. Swalbe.jl by definition of a numerical solver does not know about real world experiments. That is why we have to find the correct parameters to capture experimental findings (real world physics), in this case we like to observe a powerlaw growth in radius with $\alpha = 1/10$. There are two things we could easily change, the surface tension $\gamma$ and the velocity boundary or slippage $\delta$.

# Dictionary to store the results
results = Dict()
# Loop over different slip lengths
for slip in [2.0, 1.0, 0.5]
    # Simulation parameters
    sys = Swalbe.SysConst_1D(L=2048, γ=0.001, n=3, m=2, δ=slip, Tmax=2000000);
    # Run the simulation
    h, d = run_dropletrelax(sys, radius=400, θ₀=1/4)
    # Store the data in the dict
    results[slip] = d
end

Given the finite slippage we do not observe large deviations from the $\alpha=1/10$ powerlaw in the long time limit.

Further tutorials

More tutorials will follow in the future. I plan to create one for every paper the method was used for. So be sure to check out the docs every now and then.

The next tutorial will be about switchable substrates. In this case the wettability can not only addressed locally but also with a time dependency. Here is what happens if the time frequency is high and this happens if we update with a lower frequency.