A model of cells with volume and surface interaction. We create each cell as a collection of atoms, around 20. We colour cells by type, then shade individuals, so you can tell one cell from another.

Here we show the first behaviour of the Cellular Potts model: rounding of a collection of cells. A cell likes cells of the same type. Here, with one cell type, they should form a rough disc as they settle into a dynamic equilibrium


Graner and Glazier created the Cellular Potts model from a magnetism model. Adding cells broke the existing dynamics. Also, the biological context makes the physics terminology misleading.

It is better to define the model as a cellular model. It is a volume model, in contrast to a point model. The core interaction is between the surfaces of the cells. Each cell balances its surface interaction against its desire to stay close to a set volume.

To construct the model, we create a cell as a collection of atoms, or points on a grid. We call the ratio of target volume / current volume the cell density.  As its density drops, the cell spreads less into its surroundings.

The cell has a target volume and its collection of atoms. It can only change at a local boundary interaction. This boundary interaction updates the atom collection, so the cell density stays accurate.

For the dynamics, we define a table of interactions between cell types. This is like a game theory fitness table. We pick an atom and count the contribution its neighbours to its fitness. If the atom is fit and dense it invades a neighbour. If the atom we picked is inside a cell then invasion makes no difference.

To free the model, we choose atoms at random, and we invade at random (informed by the fitness and density). We can initialise cells with single atoms, since their high density forces growth. Boundaries take some thought; here I have placed the cells in a sea of nothing — coded as a single, passive cell.

 
 

Code in Processing, its javascript variant p5

var blockSize = 10;
var grid = [];
var cells = [];
var nothing;

var NONE = 0;
var YIN = 1;

var MINUS = 1;
var ZERO = 2;
var PLUS = 5;

//neighbour: none,  cell
var game = [
  [ZERO, MINUS],
  [MINUS, PLUS], //  cells
];


function setup() {
  var xGrid = round(0.9 * windowWidth / blockSize);
  var yGrid = round(0.9 * windowHeight / blockSize);
  var canvas = createCanvas(xGrid * blockSize, yGrid * blockSize);

  background(255);
  colorMode(HSB);
  noStroke();

  scale(blockSize);

  makeGrid(xGrid, yGrid);
  setNeighbors();
  drawAll();
}

function makeGrid(xGrid, yGrid) {
  nothing = new Compound(NONE);
  cells.push(nothing);

  for (var x = 0; x < xGrid; x++) {
    grid[x] = [];
    for (var y = 0; y < yGrid; y++) {
      var cell = nothing; // most of the space is empty
      if (0.25 * xGrid < x && x < 0.75 * xGrid && 0.25 * yGrid < y && y < 0.75 * yGrid) {
        if (random(30) < 1) {
          cell = new Compound(YIN);
          cells.push(cell);
        }
      }
      grid[x][y] = new Atom(x, y, cell);
    }
  }
}

function setNeighbors() {
  for (var x = 0; x < grid.length; x++) {
    for (var y = 0; y < grid[0].length; y++) {
      grid[x][y].setNeighbors();
    }
  }
}

function drawAll() {
  for (var x = 0; x < grid.length; x++) {
    for (var y = 0; y < grid[0].length; y++) {
      grid[x][y].draw();
    }
  }
}

function draw() {
  scale(blockSize);
  for (var i = 0; i < grid.length * grid[0].length; i++) update();
}

function update() {
  var atom = random(random(grid));

  var fitness = 0;
  for (var i = 0; i < atom.neighbors.length; i++) {
    fitness += game[atom.cellType()][atom.neighbors[i].cellType()] / atom.neighbors.length; // calculate fitness
  }

  if (fitness * atom.cellDensity() > random(MINUS, PLUS)) { // if fit
    random(atom.neighbors).change(atom.cell); // assimilate neighbour
  }
}

function Atom(X, Y, Cell) {
  this.x = X;
  this.y = Y;
  this.cell = Cell;
  this.neighbors = [];

  this.setNeighbors = function() {
    if (this.x > 0) this.neighbors.push(grid[this.x - 1][this.y]); // left
    if (this.y > 0) this.neighbors.push(grid[this.x][this.y - 1]); // down
    if (this.y < grid[0].length - 1) this.neighbors.push(grid[this.x][this.y + 1]); // up
    if (this.x < grid.length - 1) this.neighbors.push(grid[this.x + 1][this.y]); // right
  };

  this.cellType = function() {
    return this.cell.cellType();
  }
  this.cellDensity = function() {
    return this.cell.density();
  }
  this.draw = function() {
    fill(this.cell.colour);
    rect(this.x, this.y, 1, 1);
  }
  this.change = function(Cell) {
    this.cell.shrink();
    this.cell = Cell;
    this.cell.stretch();
    this.draw();
  }
}

function Compound(Type) {
  this.cellSize = 20;
  this.type = Type;
  this.colour = color(255);
  this.atoms = 0;
  var vary = random(-25, 25);
  if (YIN == this.type) this.colour = color(200, 100, 75 + vary);

  this.cellType = function() {
    return this.type;
  }
  this.stretch = function() {
    this.atoms++;
  }
  this.shrink = function() {
    this.atoms--;
  }
  this.density = function() {
    if (NONE == this.type) return 1;
    return this.cellSize / this.atoms;
  }
}