Diagonal pieces: domicule tilings of the 2n × 2n board by their count (A401673)

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 sorts the domicule tilings of the 2n \times 2n board by the number of diagonal pieces, the domicules that join two cells sharing only a corner. The triangle of these counts is OEIS A401673 [3]. The cylinders and tori are sorted the same way in notebooks of their own.

The problem

A diagonal piece covers two cells that share only a corner, and on the 2n \times 2n board every tiling has an even number of them, anywhere from 0 to 2n^2. The drawing shows the first tiling of the 8 \times 8 board in the gallery's numbering for k = 0, 8, 16 and 32 diagonal pieces, with the straight pieces greyed out. Only one tiling has k = 32.

In [1]
const { drawTilings } = require('./site/notes.js');
const counts = [0, 8, 16, 32];
const svgs = counts.flatMap(k => drawTilings({ rowCount: 8, colCount: 8, pieces: 'domicule', diagonals: k, ranks: [0] }).svgs);

figure(svgs, 'Tilings of the 8 × 8 board with k = 0, 8, 16 and 32 diagonal pieces.', { highlight: true });
Out [1] 0.04 s
k = 0 k = 8 k = 16 k = 32
Tilings of the 8 × 8 board with k = 0, 8, 16 and 32 diagonal pieces.

Give every diagonal piece the weight x. The weighted count of the h \times w board is a polynomial,

P_{h,w}(x) = \sum_k T(h, w, k)\, x^k,

where T(h, w, k) counts the tilings with k diagonal pieces. At x = 0 only dominoes are left, so P(0) is the domino count (OEIS A004003 on the 2n \times 2n board), and P(1) is the domicule count (OEIS A239273). This is the dimer model of Sandvik and Moessner [1] with diagonal dimers of fugacity x, and the weights x = 3/2, 2, 3 of the companion note on log-convexity [2] are values of the same polynomial.

The Modular solver

domicule-rectangle-polynomial-modular.js runs the transfer of the domicules notebook with a polynomial in every state, modulo primes, one prime per worker thread. domicule-rectangle-polynomial-modular.rs and domicule_rectangle_polynomial_modular.py count the same polynomial. Rust keeps the parity of every state as JavaScript does. Python numbers the entries of a state by the diagonal pieces on white cells, so a cell step needs no table of parities and moves whole views of the table. Its tables take half the memory of the other two, but all three cap the worker threads by the same formula, so the running times below compare the same number of threads.

The BigInt solver

domicule-rectangle-polynomial-bigint.js runs the transfer of the domicules notebook with a polynomial in every state, and keeps its coefficients as BigInt, and so do its Rust and Python implementations, domicule-rectangle-polynomial-bigint.rs and domicule_rectangle_polynomial_bigint.py.

Numbering

The gallery can also number only the tilings with k diagonal pieces. Every entry of the table of the domicule numbering then becomes a list, table[i][state][r], of the completions with exactly r \le k diagonal pieces, and unranking keeps the order of the full numbering. The gallery offers this up to 10 \times 10.

A straight piece covers a black and a white cell of the chessboard, a diagonal piece two cells of one colour. So on the 2n \times 2n board the number of diagonal pieces is even. The count for every k comes from domicule-rectangle-polynomial-bigint.js or spec/cases/, and the build checks that the counts add up to the number of tilings.

The counts

A diagonal piece joins two cells of the same colour of the chessboard and a domino two cells of different colours, so every tiling has as many diagonal pieces on black cells as on white ones and their number is even. The table therefore lists T(n, j), the tilings of the 2n \times 2n board with 2j diagonal pieces, for j = 0, \dots, n^2. When the OEIS was searched by terms and by name on 30 October 2026, before A401673 was drafted, it had no such triangle.

In [2]
const reference = require('./js/solvers/domicule-rectangle-polynomial-bigint.js');
const data = require('./js/core/cases');
const stored = side => data.testCasesOf('domicule-rectangle-polynomial-modular').find(t => t.rowCount === side && t.colCount === side);
const domino = side => data.testCasesOf('domino-rectangle-formula').find(t => t.rowCount === side && t.colCount === side).expectedResult;

nb.rows = {};
const rows = [];

for (let n = 0; n <= 10; n++) {
  const side = 2 * n;
  const board = { rowCount: side, colCount: side };
  let coefficients;
  let source;

  if (n <= 6) {
    coefficients = reference.calculateDiagonalPolynomial(board);
    source = 'BigInt';
  } else if (stored(side)) {
    coefficients = stored(side).expectedResult.split(',').map(BigInt);
    source = 'spec/cases/';
  } else {
    continue;
  }

  const total = coefficients.reduce((sum, c) => sum + c, 0n);
  const row = coefficients.filter((_, k) => k % 2 === 0);
  const problems = [
    total !== (await nb.context.countDomicule(board)) && 'the sum is not A239273',
    String(coefficients[0]) !== domino(side) && 'T(n, 0) is not A004003',
    coefficients.some((c, k) => k % 2 === 1 && c !== 0n) && 'an odd number of diagonal pieces',
    row.at(-1) !== 1n && `T(n, n²) = ${row.at(-1)}`,
    n > 0 && row.at(-2) !== BigInt(n * n * (n + 1)) && `T(n, n² − 1) = ${row.at(-2)}`,
    source === 'BigInt' && stored(side) && stored(side).expectedResult !== coefficients.join(',') && 'the BigInt row differs from spec/cases/',
  ].filter(Boolean);

  if (problems.length) {
    throw new Error(`n = ${n}: ${problems.join(', ')}`);
  }

  const peak = row.indexOf(row.reduce((max, c) => (c > max ? c : max), 0n));

  nb.rows[n] = row;
  rows.push([n, `${side}×${side}`, row.length, peak, String(row[peak]).length, source]);
}

table(['n', 'board', 'terms T(n, 0..n²)', 'j of the largest term', 'digits of the largest term', 'solver'], rows);
print('The first rows:');

for (let n = 0; n <= 3; n++) {
  print(`n = ${n}: ${nb.rows[n].join(', ')}`);
}
Out [2] 0.81 s
nboardterms T(n, 0..n²)j of the largest termdigits of the largest termsolver
00×0101BigInt
12×2201BigInt
24×4513BigInt
36×61047BigInt
48×817712BigInt
510×10261120BigInt
612×12371630BigInt
714×14502342spec/cases/
816×16653056spec/cases/
918×18823972spec/cases/
1020×201014890spec/cases/
The first rows:
n = 0: 1
n = 1: 2, 1
n = 2: 36, 126, 105, 12, 1
n = 3: 6728, 84132, 413932, 934213, 1000638, 491689, 99554, 6638, 36, 1

Every row the cell lists passes the same checks, and the cell stops the build when one fails: the sum is A239273, T(n, 0) is A004003, every odd number of diagonal pieces has no tiling, exactly one tiling uses only diagonal pieces, and T(n, n^2 - 1) = n^2(n + 1) (OEIS A011379). The last two are observations on these rows, not proofs.

How many pieces are diagonal

The cell prints the mean share of diagonal pieces among the 2n^2 pieces of a uniformly random tiling, and the variance of their number per site. A free board has a boundary. In the table its correction to the share shrinks roughly like 1/n, so the last rows are not yet the value of the infinite lattice. That value is 2\,\partial s / \partial \log x at x = 1.

In [3]
const rows = Object.entries(nb.rows)
  .filter(([n]) => Number(n) > 0)
  .map(([n, row]) => {
    const total = row.reduce((sum, c) => sum + c, 0n);
    const scale = 10n ** 15n;
    const mean = Number((row.reduce((sum, c, j) => sum + 2n * BigInt(j) * c, 0n) * scale) / total) / Number(scale);
    const square = Number((row.reduce((sum, c, j) => sum + 4n * BigInt(j * j) * c, 0n) * scale) / total) / Number(scale);
    const pieces = 2 * Number(n) * Number(n);

    return [n, `${2 * n}×${2 * n}`, (mean / pieces).toFixed(6), ((square - mean * mean) / (2 * pieces)).toFixed(6)];
  });

table(['n', 'board', 'share of diagonal pieces', 'variance per site'], rows);
Out [3] 0.00 s
nboardshare of diagonal piecesvariance per site
12×20.3333330.222222
24×40.3357140.147398
36×60.3957750.139813
48×80.4265310.140847
510×100.4449170.143127
612×120.4569470.144781
714×140.4653900.145965
816×160.4716360.146842
918×180.4764400.147514
1020×200.4802510.148045

Weighted counts

The polynomial gives the weighted count for any x. The cell prints \log P(x) / (4n^2), the free energy per site of the weighted board. The chart draws it for every x from 0 to 3 on four boards, with dots at the six weights of the table. The columns are x = 0 (dominoes), 1/2, 1 (domicules) and the weights 3/2, 2 and 3 of the companion note. A free boundary keeps every value below the free energy per site of the infinite lattice at the same x, which is G/\pi = 0.2915\ldots at x = 0 and s = 0.5655\ldots at x = 1.

In [4]
const weights = [[0, 1], [1, 2], [1, 1], [3, 2], [2, 1], [3, 1]];
const { logCount } = nb.context;
const evaluate = (row, [p, q]) => {
  const degree = row.length - 1;

  return row.reduce((sum, c, j) => sum + c * BigInt(p) ** BigInt(2 * j) * BigInt(q) ** BigInt(2 * (degree - j)), 0n);
};

table(
  ['n', 'board', ...weights.map(([p, q]) => `x = ${q === 1 ? p : `${p}/${q}`}`)],
  Object.entries(nb.rows)
    .filter(([n]) => Number(n) >= 3)
    .map(([n, row]) => {
      const cells = 4 * Number(n) * Number(n);

      return [
        n,
        `${2 * n}×${2 * n}`,
        ...weights.map(([p, q]) => ((logCount(evaluate(row, [p, q])) - 2 * (row.length - 1) * Math.log(q)) / cells).toFixed(6)),
      ];
    }),
);

const freeEnergy = (row, x) => {
  const terms = row.map((c, j) => (c === 0n ? -Infinity : logCount(c) + (j === 0 ? 0 : 2 * j * Math.log(x))));
  const top = Math.max(...terms);

  return top + Math.log(terms.reduce((sum, t) => sum + Math.exp(t - top), 0));
};
const shown = [4, 6, 8, 10].filter(n => nb.rows[n]);
const xs = Array.from({ length: 61 }, (_, i) => i / 20);

chart({
  title: 'log P(x) / (4n²) against the weight x of a diagonal piece, for boards 2n × 2n',
  x: { label: 'weight x of a diagonal piece', min: 0, max: 3, ticks: [0, 0.5, 1, 1.5, 2, 2.5, 3] },
  y: { label: 'log P(x) / (4n²)', min: 0.2, max: 0.9, ticks: [0.2, 0.4, 0.6, 0.8] },
  series: shown.map((n, i) => ({
    name: `${2 * n}×${2 * n}`,
    color: `var(--ordinal-${i + 1})`,
    points: xs.map(x => [x, freeEnergy(nb.rows[n], x) / (4 * n * n)]),
    markers: weights.map(([p, q]) => p / q).map(x => [x, freeEnergy(nb.rows[n], x) / (4 * n * n)]),
  })),
  describe: ([x, value]) => `x = ${x}, log P(x)/(4n²) = ${value.toFixed(6)}`,
});
Out [4] 0.01 s
nboardx = 0x = 1/2x = 1x = 3/2x = 2x = 3
36×60.2448340.3109250.4146270.5061090.5838910.709297
48×80.2559310.3361600.4502150.5479560.6299900.760534
510×100.2627850.3523960.4723410.5739960.6590160.793929
612×120.2674320.3636060.4873620.5916000.6786360.816603
714×140.2707890.3717920.4982170.6042720.6927410.832954
816×160.2733260.3780280.5064260.6138260.7033590.845257
918×180.2753120.3829350.5128510.6212850.7116370.854838
1020×200.2769080.3868970.5180160.6272700.7182710.862509
8×812×1216×1620×20
log P(x) / (4n²) against the weight x of a diagonal piece, for boards 2n × 2n0.20.40.60.800.511.522.53weight x of a diagonal piecelog P(x) / (4n²)8×8: x = 0, log P(x)/(4n²) = 0.2559318×8: x = 0.5, log P(x)/(4n²) = 0.3361608×8: x = 1, log P(x)/(4n²) = 0.4502158×8: x = 1.5, log P(x)/(4n²) = 0.5479568×8: x = 2, log P(x)/(4n²) = 0.6299908×8: x = 3, log P(x)/(4n²) = 0.76053412×12: x = 0, log P(x)/(4n²) = 0.26743212×12: x = 0.5, log P(x)/(4n²) = 0.36360612×12: x = 1, log P(x)/(4n²) = 0.48736212×12: x = 1.5, log P(x)/(4n²) = 0.59160012×12: x = 2, log P(x)/(4n²) = 0.67863612×12: x = 3, log P(x)/(4n²) = 0.81660316×16: x = 0, log P(x)/(4n²) = 0.27332616×16: x = 0.5, log P(x)/(4n²) = 0.37802816×16: x = 1, log P(x)/(4n²) = 0.50642616×16: x = 1.5, log P(x)/(4n²) = 0.61382616×16: x = 2, log P(x)/(4n²) = 0.70335916×16: x = 3, log P(x)/(4n²) = 0.84525720×20: x = 0, log P(x)/(4n²) = 0.27690820×20: x = 0.5, log P(x)/(4n²) = 0.38689720×20: x = 1, log P(x)/(4n²) = 0.51801620×20: x = 1.5, log P(x)/(4n²) = 0.62727020×20: x = 2, log P(x)/(4n²) = 0.71827120×20: x = 3, log P(x)/(4n²) = 0.86250920×2016×1612×128×8log P(x) / (4n²) against the weight x of a diagonal piece, for boards 2n × 2n0.20.40.60.800.511.522.53weight x of a diagonal piecelog P(x) / (4n²)8×8: x = 0, log P(x)/(4n²) = 0.2559318×8: x = 0.5, log P(x)/(4n²) = 0.3361608×8: x = 1, log P(x)/(4n²) = 0.4502158×8: x = 1.5, log P(x)/(4n²) = 0.5479568×8: x = 2, log P(x)/(4n²) = 0.6299908×8: x = 3, log P(x)/(4n²) = 0.76053412×12: x = 0, log P(x)/(4n²) = 0.26743212×12: x = 0.5, log P(x)/(4n²) = 0.36360612×12: x = 1, log P(x)/(4n²) = 0.48736212×12: x = 1.5, log P(x)/(4n²) = 0.59160012×12: x = 2, log P(x)/(4n²) = 0.67863612×12: x = 3, log P(x)/(4n²) = 0.81660316×16: x = 0, log P(x)/(4n²) = 0.27332616×16: x = 0.5, log P(x)/(4n²) = 0.37802816×16: x = 1, log P(x)/(4n²) = 0.50642616×16: x = 1.5, log P(x)/(4n²) = 0.61382616×16: x = 2, log P(x)/(4n²) = 0.70335916×16: x = 3, log P(x)/(4n²) = 0.84525720×20: x = 0, log P(x)/(4n²) = 0.27690820×20: x = 0.5, log P(x)/(4n²) = 0.38689720×20: x = 1, log P(x)/(4n²) = 0.51801620×20: x = 1.5, log P(x)/(4n²) = 0.62727020×20: x = 2, log P(x)/(4n²) = 0.71827120×20: x = 3, log P(x)/(4n²) = 0.862509

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 [5]
const { formatTime } = nb.context;

table(
  ['board', 'modular, Rust', 'modular, Python', 'modular, JavaScript', 'BigInt, Rust', 'BigInt, Python', 'BigInt, JavaScript'],
  nb.context.diagonalBenchmark.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 [5] 0.00 s
boardmodular, Rustmodular, Pythonmodular, JavaScriptBigInt, RustBigInt, PythonBigInt, JavaScript
0×00.09 ms2.15 ms9.73 ms0.00 ms0.00 ms0.07 ms
2×20.09 ms3.75 ms9.77 ms0.00 ms0.01 ms0.13 ms
4×40.09 ms4.00 ms9.86 ms0.03 ms0.10 ms0.36 ms
6×60.12 ms4.67 ms11.28 ms0.38 ms1.49 ms1.75 ms
8×80.36 ms14.57 ms20.36 ms4.56 ms19.45 ms10.15 ms
10×101.93 ms51.95 ms56.64 ms44.55 ms198.35 ms63.06 ms
12×1221.75 ms168.29 ms270.12 ms413.57 ms1.75 s574.76 ms
14×14242.28 ms1.30 s1.43 s3.97 s15.10 s4.15 s
16×163.25 s13.51 s13.65 s28.59 s107.74 s30.73 s
18×1825.50 s102.44 s90.97 s200.06 s--
Run: benchmarks/results/ee071e57836d-node26.11.1-python3.14.5-rust1.99.1

PARI/GP

The polynomial P_{h,w}(x) and the rows of the triangle in oeis/A401673/A401673.gp, checked in CI against the test data up to 8 \times 8.

In [6]
print(require('fs').readFileSync(require('path').join(nb.context.root, 'oeis/A401673/A401673.gp'), 'utf-8').trim());
Out [6] 0.00 s
\\ Domicule tilings of an h X w board by the number of diagonal pieces (OEIS A401673): P(h, w) is a polynomial
\\ in x, whose coefficient of x^k counts the tilings with k pieces joining cells that share only a corner.
\\ The dynamic programming of A239264.gp, with a piece to the down-left or down-right neighbour weighted by x.
\\ The number of diagonal pieces is even, so row(n) lists the tilings of the 2n X 2n board with 0, 2, ..., 2n^2 of them.
P(h, w) = {
  my(S = 2^(w+2), v = vector(S), u);
  if (h*w == 0, return(1));
  v[1] = 1;
  for (p = 0, h*w-1,
    my(r = p\w, c = p%w, d = [], e = []);
    if (c+1 < w, d = concat(d, 1); e = concat(e, 1));
    if (r+1 < h,
      if (c > 0, d = concat(d, w-1); e = concat(e, x));
      d = concat(d, w); e = concat(e, 1);
      if (c+1 < w, d = concat(d, w+1); e = concat(e, x)));
    u = vector(S);
    for (s = 0, S-1,
      my(y = v[s+1]);
      if (y,
        if (bitand(s, 1),
          u[(s>>1) + 1] += y,
          for (i = 1, #d, if (!bittest(s, d[i]), my(t = (s + 2^d[i])>>1); u[t+1] += e[i]*y)))));
    v = u);
  v[1]
};
row(n) = my(q = P(2*n, 2*n)); vector(n^2+1, k, polcoef(q, 2*(k-1)));

Verification

The two solvers agree with each other, with A004003 at k = 0 and with A239273 summed over k. The PARI/GP program runs in CI against the same data. The tests are in harness/, the known values in spec/cases/.

References

  1. A. W. Sandvik and R. Moessner. Correlations and confinement in nonplanar two-dimensional dimer models. Physical Review B 73(14):144504, 2006. doi:10.1103/PhysRevB.73.144504
  2. D. Ryłko. Log-convexity of strip dimer counts on the king's graph. Companion note, October 2026. doi:10.5281/zenodo.23686087
  3. OEIS Foundation Inc. The On-Line Encyclopedia of Integer Sequences, entries OEIS A401673, OEIS A004003, OEIS A239273 and OEIS A011379. oeis.org