Quick Start

Fluctuation Theory is key to understanding many solubility and solubilization issues. But I've never really understood what all the angled bracket stuff, e.g. 〈δxδx〉, means. Here we use some simple particles to demonstrate the core ideas.


The app is based on the patient explanations from Dr Seishi Shimizu at U York, a world-class fluctuation theory expert.


Fluctuation x-x
Fluctuation y-y
Fluctuation x-y
% 2
//One universal basic required here to get things going once loaded
window.onload = function () {
    //restoreDefaultValues(); //Un-comment this if you want to start with defaults
//Any global variables go here

//Main is hard wired as THE place to start calculating when input changes
//It does no calculations itself, it merely sets them up, sends off variables, gets results and, if necessary, plots them.
function Main() {

    //Send all the inputs as a structured object
    //If you need to convert to, say, SI units, do it here!
    const inputs = {
        F11: sliders.SlideF11.value,
        F22: sliders.SlideF22.value,
        F12: sliders.SlideF12.value/100,
        Perc2: sliders.SlidePerc2.value/100,

    //Send inputs off to CalcIt where the names are instantly available
    //Get all the resonses as an object, result
    const result = CalcIt(inputs)

    //Set all the text box outputs
    document.getElementById('Results').value = result.Results

    //You might have some other stuff to do here, but for most apps that's it for CalcIt!

//Here's the app calculation
//The inputs are just the names provided - their order in the curly brackets is unimportant!
//By convention the input values are provided with the correct units within Main
function CalcIt({ F11, F22, F12, Perc2 }) {
    let myrng = new Math.seedrandom('hello.');
    const dpr = window.devicePixelRatio || 1;
    const theCanvas = document.querySelector('canvas')
    const ctx = setupCanvas(theCanvas, dpr);
    const w = theCanvas.width / dpr, h = theCanvas.height / dpr
    const gv = 10.01
    const fy=Perc2,fx=1-fy

    let GFx = (100.1 - F11) / 2
     if (F11 < 10) GFx *= Math.pow(10 - F11, 0.75) //Make the low zone flatter
    if (F11 == 0) GFx = 1e99 //And enforce uniformity at 0
    let GFy = (100.1 - F22) / 2
     if (F22 < 10) GFy *= Math.pow(10 - F11, 0.75) //Make the low zone flatter
    if (F22 == 0) GFy = 1e99 //And enforce uniformity at 0
    let i = 0, j = 0, k = 0, xv = 0
    let pax = [], pay = [], ptot = 0,ptotx=0,ptoty=0
    for (i = 0; i <= 10; i++) {
        drawLines(ctx, i / gv * w, 0, i / gv * w, h)
        drawLines(ctx, 0, i / gv * h, w, i / gv * h)
    let isOdd=false,xo,yo,px,py
     for (i = 0; i < 10; i++) {
        for (j = 0; j < 10; j++) {
            isOdd=((i % 2) && !(j % 2)) || (!(i % 2) && (j % 2))
            if (isOdd){
            } else {
            p = Math.exp(-(((i - 4-F12/0.5) * (i - 4-F12/0.5) + (j - 4) * (j - 4))) / GFx)
            px=p * fx*xo*10
            ptotx += px
            p = Math.exp(-(((i - 4+F12/0.5) * (i - 4+F12/0.5) + (j - 4) * (j - 4))) / GFy)
            py=p * fy*yo*10
            ptoty += py
    //Scale to 1000 particles
    if (ptotx > 0) ptotx=fx*1000/(ptotx)
    if (ptoty > 0) ptoty=fy*1000/(ptoty)
    let pc = 0, Nx,Ny, xdN2, ydN2, xydN2, xdNAv = 0, ydNAv = 0, xydNAv = 0
    ctx.font = "20px Arial"
    for (i = 0; i < 10; i++) {
        for (j = 0; j < 10; j++) {
            Nx = Math.floor(ptotx * pax[pc])
            xdN2 = Math.pow(Nx - 10*fx, 2)
            xdNAv += xdN2
             for (k = 0; k < Nx; k++) {
                drawDots(ctx, "orange", 4, i / 10 * w + w / 10 * myrng(), j / 10 * h + h / 10 * myrng())
            Ny = Math.floor(ptoty * pay[pc])
            ydN2 = Math.pow(Ny - 10*fy, 2)
            ydNAv += ydN2
            for (k = 0; k < Ny; k++) {
                drawDots(ctx, "skyblue", 4, i / 10 * w + w / 10 * myrng(), j / 10 * h + h / 10 * myrng())
            ctx.fillStyle = "black"
            // ctx.fillText(Ni.toFixed(0), i / 10 * w + w / 100, j / 10 * h + 2 * h / 50)
            // ctx.fillText(xdN2.toFixed(0), i / 10 * w + w / 100, j / 10 * h + 4 * h / 50)
    xv = (xdNAv / 100)
    yv = (ydNAv / 100)
    xyv = (xydNAv / 100)
    return {
         Results: "<δxδx>=" +xv.toPrecision(4) + ", <δyδy>=" +yv.toPrecision(4)+", <δxδy>=" +xyv.toPrecision(4),

function drawDots(ctx, col, lw, xv, yv) {
    ctx.fillStyle = col;
    ctx.arc(xv, yv, lw, 0, 2 * Math.PI);
function drawLines(ctx, xs, ys, xe, ye) {
    ctx.strokeStyle = "black";
    ctx.moveTo(xs, ys);
    ctx.lineTo(xe, ye);

function setupCanvas(canvas, dpr) {
    // Get the device pixel ratio, falling back to 1.
    var rect = canvas.getBoundingClientRect();
    // Give the canvas pixel dimensions of their CSS
    // size * the device pixel ratio.
    canvas.width = rect.width * dpr;
    canvas.height = rect.height * dpr;
    var ctx = canvas.getContext('2d');
    // Scale all drawing operations by the dpr, so you
    // don't have to worry about the difference.
    ctx.scale(dpr, dpr);
    return ctx;


Fluctuation theory is a powerful method to see what is going on in a solution or at an interface. It can be expressed in some numbers that are simple to describe and calculate but which are hard to grasp intuitively. Here we can play with all the key influences of particles on each other, and see how they translate into the language of N, δ and the <brackets> that simply mean "Take the average".

Start with the default values of all 0. We have a 10x10 grid and in each cell we have 10 particles at random. Looking at the big picture, we have 1000 particles in 100 grid positions so there's an average of 10 per grid. We call that average <Nx>. In each cell we have an actual count of Nx and we calculate δx=Nx-<Nx>. We then, and this at first seems strange, multiply it by itself to get δxδx values in each cell. We sum all these local δxδx values, divide by 100 and get <δxδx>, their average.

With the default values this average = 0. There are no fluctuations from the average. Now slide the Fluctuationx-x slider. At first nothing obvious happens to the particles, but you see <δxδx> slowly increase. The calculation easily picks up that some cells have 1 fewer particle, so <δxδx>=1 and some cells have 1 extra particle, giving <δxδx>=1. As you carry on sliding, some cells lose 2 or 3 particles, so now their contribution to the averge is 4 or 9, just as with those cells that gain 2 or 3. So <δxδx> is capturing small but meaningful fluctuations from the mean. As you slide to the maximum value the fluctuation becomes obvious, and at 100 all the particles are in the central cell.

If you set %2 to 100 and slide Fluctuationy-y, then you have the same scenario as with x, just with blue particles rather than orange ones. Now set %2 to 50, so you have equal x and y particles. Now things get more complicated as they depend on both Fluctuationx-x and Fluctuationy-y. And you also find you have a value that captures the fluctuations around the x & y means,<δxδy>

So far, we've assumed that x and y don't have a special dislike for each other. But if you now slide Fluctuationx-y they become more and more separated. The simulation ceases to be rigorous, but the general idea is sound. The opposite case of x and y being attracted is not covered, but you can work out then general principle.

The symbols we use

Simplifying the nomenclature somewhat, have x fluctuating around a mean, with, as a default, everything symmetrical, the mean of x, 〈x〉 = 0, using the angled brackets. As we know from statistics, we can capture how much variation there is about the mean via the Variance, which can be defined in two ways that are arithmetically equivalent. The first way is intellectually clearer, the second way is computationally simpler. Confusingly, we see different nomenclatures for the variance, each included here

`"〈"δx^2"〉" = "〈"δxδx"〉" = (σ^2)_x = Var(x) = (x-"〈"x"〉")(x-"〈"x"〉")`

The curious δx terms are simply δx=x-〈x〉, i.e. δx is the difference between x at that moment in time (or position in space - the two are equivalent) and the mean 〈x〉, the equation is just restating itself. The other way to calculate it (just multiply to get x²-2x〈x〉+〈x〉² = x²-〈x〉²) is:

`"〈"δx^2"〉" = "〈"δxδx"〉" = (σ^2)_x = Var(x) = "〈"x^2"〉"-"〈"x"〉""〈"x"〉"`

We can do the same things for y. But what's of interest is the Covariance, how X and Y are interrelated.

`"〈"δxδy"〉" = (σ)_(xy) = Covar(x,y) = (x-"〈"x"〉")(y-"〈"y"〉")`

and, of course, the equivalent:

`"〈"δxδy"〉" = (σ)_(xy) = Covar(x,y) = "〈"xy"〉"-"〈"x"〉""〈"y"〉"`

After all this we need one more fact. What everyone calls Variance and Covariance, statistical thermodynamicists call Fluctuation. I suppose they could have called it all Covariance, but although it's respectable to talk about X,X covariance, it's a bit odd. So the single term "fluctuation" ended up as doing the job.

Solubility/solubilization theory

We can now look at the definition of a Kirkwood-Buff integral, a number that describes how much or how little something likes itself compared to the average with the mysterious nomenclature removed. Here we see GXX and GXY, how much X likes itself compared to the average and how much X and Y (dis)like each other. These are expressed in terms of N, the number of moles of each species present For simplicity some pesky volume and Kroneker delta terms are omitted:

`G_(XX) ~ ("〈"δ_Xδ_X"〉")/("〈"N_X"〉""〈"N_X"〉"` and `G_(XY) ~ ("〈"δ_Xδ_Y"〉")/("〈"N_X"〉""〈"N_Y"〉"`

Translated into normal language this means that the size of the effect is the (co)variance normalized by the means. The bigger the absolute (co)variance, the larger the Gij value. Or, to put it more correctly, if we find a large positive Gij that means i and j prefer to be together more than the average, and a large negative value means they prefer to keep apart.