Dominoes: tilings of the 2n × 2n torus (A335586)

A notebook: every code cell below runs when the site is built, and its output is printed under it. On GitHub only the code is shown; the rendered page has the results. It counts the domino tilings of tori, boards whose opposite edges are glued in both directions. The same boards are counted with domicules, where the torus count of three lattices is compared with their entropy, and on the triangular lattice; the cylinders notebook glues only the left and right edges.

The problem

On a torus both pairs of opposite edges of the board are glued, so a domino may cross the column seam, between the last and the first column, or the row seam, between the last and the first row. A domino that crosses a seam is drawn cut, with one half on each side, and the glued edges are marked, one pair with one tick and the other with two. The drawings show 4 tilings of the 8 \times 8 torus, spread over the numbering.

In [1]
const { drawTilings } = require('./site/notes.js');
const { svgs, total } = drawTilings({ rowCount: 8, colCount: 8, pieces: 'domino', surface: 'torus', spread: 4 });

figure(svgs, `4 of the ${total} domino tilings of the 8 × 8 torus.`);
Out [1] 0.02 s
#1 #77963329 #155926657 #233889985
4 of the 311853312 domino tilings of the 8 × 8 torus.

The Modular solver

all-lattices-cylinder-torus-modular.js goes cell by cell with a state of the next w + 2 cells. A torus is a sum over the sets S of first-row cells covered from the last row. Each run starts from S and must end with S. Rotating the columns maps the runs of S onto those of its rotations, so one run per rotation class is enough. The solver counts modulo primes below 2^{31}. The Chinese remainder theorem rebuilds the count, and one more prime checks it. The same algorithm is in Rust, all-lattices-cylinder-torus-modular.rs, and in Python, all_lattices_cylinder_torus_modular.py.

The BigInt solver

all-lattices-cylinder-torus-bigint.js goes row by row. For every set of cells of a row already covered from the row below, it lists the ways to finish the row. The torus count is then the trace of the h-th power of this row transfer matrix. The same algorithm is in Rust, all-lattices-cylinder-torus-bigint.rs, and in Python, all_lattices_cylinder_torus_bigint.py.

Numbering

The gallery numbers the tilings of the tori up to 10 \times 10 with the cell-by-cell scan of the modular solver. The tilings are ordered by S and then by the scan. The tilings of S and of its rotations are equally many, so one table per rotation class gives every count, and the table of a single S is rebuilt when a page needs it. The Random button draws a uniformly random number, so the sample is exact.

The counts

Domino tilings of the 2n \times 2n torus are OEIS A335586 [1]. For n = 1 the torus has multiple edges, because on a 2 \times 2 torus a cell meets its right and its left neighbour in the same cell, and each edge counts as a separate domino, as in A335586. Boards up to 8 \times 8 go through both solvers, 10 \times 10 and 12 \times 12 through the modular one, and every count is compared with the terms of A335586 stored in spec/cases/.

In [2]
const reference = require('./js/solvers/all-lattices-cylinder-torus-bigint.js');
const modular = require('./js/solvers/all-lattices-cylinder-torus-modular.js');
const { testCasesOf } = require('./js/core/cases');
const stored = side =>
  testCasesOf('all-lattices-cylinder-torus-modular').find(
    t => t.lattice === 'square' && t.torus && !t.twisted && t.oeis === 'A335586' && t.rowCount === side && t.colCount === side,
  );
const rows = [];

for (let n = 1; n <= 6; n++) {
  const side = 2 * n;
  const board = { rowCount: side, colCount: side, lattice: 'square', torus: true };
  const count = await modular.calculateTotalTilingCombinations(board);
  const expected = stored(side);

  if (n <= 4 && reference.calculateTotalTilingCombinations(board) !== count) {
    throw new Error(`the two solvers differ on the ${side}×${side} torus`);
  }

  if (!expected || expected.expectedResult !== String(count)) {
    throw new Error(`the ${side}×${side} torus differs from A335586`);
  }

  rows.push([n, `${side}×${side}`, String(count), String(count).length, n <= 4 ? 'BigInt and modular' : 'modular', 'matches OEIS']);
}

table(['n', 'board', 'a(n)', 'digits', 'solver', 'status'], rows);
Out [2] 3.70 s
nboarda(n)digitssolverstatus
12×281BigInt and modularmatches OEIS
24×42723BigInt and modularmatches OEIS
36×6901765BigInt and modularmatches OEIS
48×83118533129BigInt and modularmatches OEIS
510×101120360449740814modularmatches OEIS
612×12416195756698531020819modularmatches OEIS

The tori of circumference 8 and 10 are in the OEIS as well, C_8 \times C_n in OEIS A253678 and C_{10} \times C_n in OEIS A281679, and the test data holds some of their terms.

Running time

Average time of the count inside each solver, without the start of its process, from the newest complete benchmark run in benchmarks/results.

In [3]
const { formatTime } = nb.context;

table(
  ['board', 'modular, Rust', 'modular, Python', 'modular, JavaScript', 'BigInt, Rust', 'BigInt, Python', 'BigInt, JavaScript'],
  nb.context.dominoPeriodicBenchmark
    .filter(r => r.surface === 'torus')
    .map(r => [
      `${r.rowCount}×${r.colCount}`,
      ...[r.languages.rs, r.languages.py, r.modular, r.bigintLanguages.rs, r.bigintLanguages.py, r.bigint].map(formatTime),
    ]),
);
print(`Run: ${nb.context.benchmarkRun}`);
Out [3] 0.00 s
boardmodular, Rustmodular, Pythonmodular, JavaScriptBigInt, RustBigInt, PythonBigInt, JavaScript
2×20.08 ms3.21 ms10.29 ms0.00 ms0.01 ms0.15 ms
4×40.08 ms3.29 ms10.38 ms0.01 ms0.05 ms0.24 ms
6×60.19 ms6.61 ms13.77 ms0.16 ms0.70 ms1.56 ms
8×81.14 ms22.61 ms25.34 ms2.84 ms16.04 ms11.69 ms
10×1032.30 ms96.99 ms120.40 ms64.42 ms532.97 ms183.09 ms
12×12600.46 ms1.42 s1.60 s1.49 s16.43 s5.40 s
14×1410.63 s30.52 s28.89 s36.09 s474.49 s265.64 s
16×16274.84 s1290.66 s780.03 s---
Run: benchmarks/results/ee071e57836d-node26.11.1-python3.14.5-rust1.99.1

Verification

The two cylinder and torus solvers agree with each other and with OEIS A335586 and OEIS A253678, and the modular solver also with OEIS A281679. FULL_CASES=1 node --test js/test/conformance.test.js runs the tori of spec/cases/all-lattices-cylinder-torus-bigint.json through both solvers and those of spec/cases/all-lattices-cylinder-torus-modular.json through the modular one.

References

  1. OEIS Foundation Inc. The On-Line Encyclopedia of Integer Sequences, entries OEIS A335586, OEIS A253678 and OEIS A281679. oeis.org