The GAP Mined for Music

The previous post presented some introduction to (Balanced Incomplete) Block Designs with a simple musical application. It did not, however, provide much in the way of obtaining block designs in the first place, other than by using extant published material. It would be nice to find some software capable of producing block designs.

I've been aware of the GAP computer algebra software, and have even occasionally used it, for some years now but did not realise that it included software for block designs. Perhaps that should not come as a surprise as it is growing all the time.

It is free to download and install, is multi-platform and is fun to play with in any case. It's a command line console based system and therefore very useful for generating (and processing) text files. I'm certainly not going to say anything about it here - other than those bits of it I'll be using in this note. Don't ask me for any help on installation or use - I'll probably mislead you and, anyway, there are far more authoritative guides than I.

This post will present not just results of the use of GAP/DESIGN, but will attempt - at the risk of boring the already knowledgeable - to represent the voyage itself. Also note that I've taken a few liberties with the representation of GAP's typical output; which is a stream of unformatted text. There's certainly enough syntax to make the output completely unambiguous, but there's no syntactic sugar - it just flops out. So for clarity's sake I've tidied it up a bit.

The current (v4.9.2 at the time of writing) GAP release includes Soicher's DESIGN package (v1.6 @tow), but does not load it by default. For any particular GAP session you will therefore need to import the software with:


If the package loads successfully, then GAP reports 'true'. The method used by the package is based on what is known as the '*-construction' - far too abstruse to go into here but still available to the adventurous. Basically, one employs a slightly simpler t-(v-1,k-1,(k-t)λ) design to generate a bunch of t-(v,k,) designs, which will work (in our case, where we are interested in block sizes of 2, 3, 4 and 6) if n is the lowest common multiple of (k-t)C(2-t), (k-t)C(3-t), (k-t)C(4-t), (k-t)C(6-t), which is to say of k-2C0, k-2C1, k-2C2 and k-2C4 for each of the k with which we're treating. The DESIGN package will therefore require the use of an eleventh, not twelfth, order group which we will create with

G11:=CyclicGroup(IsPermGroup, 11);

The Two Tone Tour

First of all, we should verify the existence of the (12,2,1) design which essentially lists all 66 of the t=2 design's pitch class pairs. The above conditioning of n for k=2 requires only that it be the lowest common multiple of k-2C0 as it's the only one which can apply (yet). This clearly has n = 1 with a *-construction using 2-(11,1,0) - a trivial design used to generate a 2-(v=12,k=2,=1) design. We know that λ=1 and are required to provide this information by defining a record:

tlamb:=rec(t:=2, lambdas:=[1]);

which information is passed into another record specifying the desired block size and using the earlier defined permutation group:

basis:=rec(v:=12, blockSizes:=[2], tSubsetStructure:=tlamb, requiredAutSubgroup:=G11);

Once this basis is defined it may be used directly:


upon which GAP emits the following list of length 1:

[ rec(autGroup:=Group([(1,2,3,4,5,6,7,8,9,10,11,12),(1,2)]),
blockNumbers:=[66], blockSizes:=[2],
blocks:=[ [1,2], [1,3], [1,4], [1,5], [1,6], [1,7], [1,8], [1,9], [1,10], [1,11], [1,12], [2,3], [2,4], [2,5], [2,6], [2,7], [2,8], [2,9], [2,10], [2,11], [2,12], [3,4], [3,5], [3,6], [3,7], [3,8], [3,9], [3,10], [3,11], [3,12], [4,5], [4,6], [4,7], [4,8], [4,9], [4,10], [4,11], [4,12], [5,6], [5,7], [5,8], [5,9], [5,10], [5,11], [5,12], [6,7], [6,8], [6,9], [6,10], [6,11], [6,12], [7,8], [7,9], [7,10], [7,11], [7,12], [8,9], [8,10], [8,11], [8,12], [9,10], [9,11], [9,12], [10,11], [10,12], [11,12] ],
isBinary:=true, isBlockDesign:=true, isSimple:=true, r:=11,
tSubsetStructure:=rec(lambdas:=[1], t:=2), v:=12) ]

And there you are. The information provided - as well as showing all possible pitch class pairs exactly once (λ=1) as requested - reports the number of blocks as (the expected) 66 and the r = 11, being the number of occurrences of each of the 12 pitch classes in all blocks (recall vr = bk, in this case 12×11 = 66×2).

Triadic Transitions

For k=3 block designs, which we used to construct a sequence of triads each carrying a common diad (to provide a binding musical integrity, some kind of voice-leading) between consecutive chords, we'll again need r=11 and v=12. The k=3 triadic requirement means we will expect 132/3 = 44 blocks. In this case n is required to be the lowest common multiple of both 1C0 and 1C1. As this is also just 1 there's no change there and remains unaffected. Setting up the required tSubsetStructure for a λ of 2, this time, and resetting basis to use the new record (otherwise it will - by default - remember the old value:

tlamb:=rec(t:=2, lambdas:=[2]);
basis:=rec(v:=12, blockSizes:=[3], tSubsetStructure:=tlamb, requiredAutSubgroup:=G11);

In fact you may skip the 'tlamb' construction and use its (now nameless) value directly, e.g:

basis:=rec(v:=12, blockSizes:=[3], tSubsetStructure:=rec(t:=2, lambdas:=[2]), requiredAutSubgroup:=G11);

Now you may generate the designs with the new basis:


Alternatively you may skip the named 'basis' construction and use its value directly (although arguably with less clarity):

D:=BlockDesigns(rec(v:=12, blockSizes:=[3], tSubsetStructure:=rec(t:=2, lambdas:=[2]), requiredAutSubgroup:=G11));

The result is a list of five 2-(12,3,2) block designs, of which we'll display only the first and last:

[rec(autGroup:=Group([(1,11,10,9,8,7,6,5,4,3,2)]), blockNumbers:=[44], blockSizes:=[3],
blocks:=[[1,2,3], [1,2,11], [1,3,8], [1,4,7], [1,4,9], [1,5,7], [1,5,12], [1,6,9], [1,6,10], [1,8,12], [1,10,11], [2,3,4], [2,4,9], [2,5,8], [2,5,10], [2,6,8], [2,6,12], [2,7,10], [2,7,11], [2,9,12], [3,4,5], [3,5,10], [3,6,9], [3,6,11], [3,7,9], [3,7,12], [3,8,11], [3,10,12], [4,5,6], [4,6,11], [4,7,10], [4,8,10], [4,8,12], [4,11,12], [5,6,7], [5,8,11], [5,9,11], [5,9,12], [6,7,8], [6,10,12], [7,8,9], [7,11,12], [8,9,10], [9,10,11]], isBinary:=true, isBlockDesign:=true, isSimple:=true, r:=11, tSubsetStructure:=rec(lambdas:=[2], t:=2), v:=12),

… (three further records) …

rec(autGroup:=Group([(1,11,10,9,8,7,6,5,4,3,2)]), blockNumbers:=[44], blockSizes:=[3], blocks:=[[1,2,4], [1,2,6], [1,3,9], [1,3,11], [1,4,6], [1,5,11], [1,5,12], [1,7,8], [1,7,10], [1,8,12], [1,9,10], [2,3,5], [2,3,7], [2,4,10], [2,5,7], [2,6,12], [2,8,9], [2,8,11], [2,9,12], [2,10,11], [3,4,6], [3,4,8], [3,5,11], [3,6,8], [3,7,12], [3,9,10], [3,10,12], [4,5,7], [4,5,9], [4,7,9], [4,8,12], [4,10,11], [4,11,12], [5,6,8], [5,6,10], [5,8,10], [5,9,12], [6,7,9], [6,7,11], [6,9,11], [6,10,12], [7,8,10], [7,11,12], [8,9,11]], isBinary:=true, isBlockDesign:=true, isSimple:=true, r:=11, tSubsetStructure:=rec(lambdas:=[2], t:=2), v:=12)]

You may look for the usual things like Hamiltonian circuits.


Now we can get a little more adventurous and look for designs with tetrads (k=4), where the number of blocks in the design will be b = vr/k = 33. This will require n now being the lowest common multiple of 2C0, 2C1 and 2C2 which is again simply the value of 1 and thus we'll not be forced into more than the λ=r(k-1)/(v-1) = 3 that we wish for, which would require consequently larger b and r values (some simply impossible).

Upon entering into GAP the required calculation:

D:=BlockDesigns(rec(v:=12, blockSizes:=[4], tSubsetStructure:=rec(t:=2, lambdas:=[3]), requiredAutSubgroup:=G11));

We get a list of eight records of 2-(12,4,3) designs. Again, we'll show only the first and last:

[rec(autGroup:=Group([(1,9,6,3,11,8,5,2,10,7,4)]), blockNumbers:=[33], blockSizes:=[4], blocks:=[[1,2,4,5], [1,2,7,12], [1,2,9,10], [1,3,4,11], [1,3,5,8], [1,3,6,10], [1,4,8,10], [1,5,7,9], [1,6,7,12], [1,6,11,12], [1,8,9,11], [2,3,5,6], [2,3,8,12], [2,3,10,11], [2,4,6,9], [2,4,7,11], [2,5,9,11], [2,6,8,10], [2,7,8,12], [3,4,6,7], [3,4,9,12], [3,5,7,10], [3,7,9,11], [3,8,9,12], [4,5,7,8], [4,5,10,12], [4,6,8,11], [4,9,10,12], [5,6,8,9], [5,6,11,12], [5,10,11,12], [6,7,9,10], [7,8,10,11]], isBinary:=true, isBlockDesign:=true, isSimple:=true, r:=11, tSubsetStructure:=rec(lambdas:=[3], t:=2), v:=12),

… (six further records) …

rec(autGroup:=Group([(1,9,6,3,11,8,5,2,10,7,4)]), blockNumbers:=[33], blockSizes:=[4], blocks:=[[1,2,4,5], [1,2,6,8], [1,2,9,10], [1,3,4,11], [1,3,6,12], [1,3,7,8], [1,4,10,12], [1,5,6,10], [1,5,7,11], [1,7,9,12], [1,8,9,11], [2,3,5,6], [2,3,7,9], [2,3,10,11], [2,4,7,12], [2,4,8,9], [2,5,11,12], [2,6,7,11], [2,8,10,12], [3,4,6,7], [3,4,8,10], [3,5,8,12], [3,5,9,10], [3,9,11,12], [4,5,7,8], [4,5,9,11], [4,6,9,12], [4,6,10,11], [5,6,8,9], [5,7,10,12], [6,7,9,10], [6,8,11,12], [7,8,10,11]], isBinary:=true, isBlockDesign:=true, isSimple:=true, r:=11, tSubsetStructure:=rec(lambdas:=[3], t:=2), v:=12)]


The last designs we can attempt to find, without requiring large numbers of blocks, is the 22 block sized 2-(12,6,5) set of hexads. Remembering to check the lowest common multiple constraint, we see that when k=6, 4C0 = 1, 4C1 = 4, 4C2 = 6 and 4C4 = 1 yet again have us needing only an n=1. Thus we can expect the *-construction to work, as indeed it does.

D:=BlockDesigns(rec(v:=12, blockSizes:=[6], tSubsetStructure:=rec(t:=2, lambdas:=[5]), requiredAutSubgroup:=G11));

Yields a list of four 2-(12,6,5) designs.

[rec(autGroup:=Group([(1,11,10,9,8,7,6,5,4,3,2)]), blockNumbers:=[22], blockSizes:=[6],
blocks:=[[1,2,3,4,8,12], [1,2,3,7,11,12], [1,2,4,5,7,9], [1,2,4,6,9,10], [1,2,6,10,11,12], [1,3,4,6,8,11], [1,3,5,8,9,11], [1,3,6,7,9,10], [1,4,5,7,8,10], [1,5,6,7,8,12], [1,5,9,10,11,12], [2,3,4,5,9,12], [2,3,5,6,8,10], [2,3,5,7,10,11], [2,4,7,8,10,11], [2,5,6,8,9,11], [2,6,7,8,9,12], [3,4,5,6,10,12], [3,4,6,7,9,11], [3,7,8,9,10,12], [4,5,6,7,11,12], [4,8,9,10,11,12]],
isBinary:=true, isBlockDesign:=true, isSimple:=true, r:=11,
tSubsetStructure:=rec(lambdas:=[5], t:=2), v:=12),
rec(autGroup:=Group([(1,11,10,9,8,7,6,5,4,3,2)]), blockNumbers:=[22], blockSizes:=[6],
blocks:=[[1,2,3,4,7,12], [1,2,3,6,11,12], [1,2,4,6,8,9], [1,2,5,6,8,10], [1,2,5,10,11,12], [1,3,4,7,8,10], [1,3,5,6,9,10], [1,3,5,7,8,11], [1,4,5,7,9,11], [1,4,9,10,11,12], [1,6,7,8,9,12], [2,3,4,5,8,12], [2,3,5,7,9,10], [2,3,6,7,9,11], [2,4,5,8,9,11], [2,4,6,7,10,11], [2,7,8,9,10,12], [3,4,5,6,9,12], [3,4,6,8,10,11], [3,8,9,10,11,12], [4,5,6,7,10,12], [5,6,7,8,11,12]],
isBinary:=true, isBlockDesign:=true, isSimple:=true, r:=11,
tSubsetStructure:=rec(lambdas:=[5], t:=2), v:=12),
rec(autGroup:=Group([(1,5,8,2,3)(4,10,9,11,7),(1,4,9,10,8)(3,11,6,5,7)]), blockNumbers:=[22], blockSizes:=[6],
blocks:=[[1,2,3,5,6,8], [1,2,3,5,8,12], [1,2,4,5,7,11], [1,2,4,7,11,12], [1,2,4,8,9,10], [1,3,4,6,10,11], [1,3,6,10,11,12], [1,3,7,8,9,11], [1,4,8,9,10,12], [1,5,6,7,9,10], [1,5,6,7,9,12], [2,3,4,6,7,9], [2,3,4,6,9,12], [2,3,5,9,10,11], [2,5,9,10,11,12], [2,6,7,8,10,11], [2,6,7,8,10,12], [3,4,5,7,8,10], [3,4,5,7,10,12], [3,7,8,9,11,12], [4,5,6,8,9,11], [4,5,6,8,11,12]],
isBinary:=true, isBlockDesign:=true, isSimple:=true, r:=11,
tSubsetStructure:=rec(lambdas:=[5], t:=2), v:=12),
rec(autGroup:=Group([(2,3)(4,11)(5,6)(7,10),(2,3)(6,8)(7,12)(9,11),(1,3,2)(5,6,8)(7,10,12),(1,5,3)(4,9,12)(7,11,10),(1,2,3)(5,7,6,10,8,12)(9,11)]), blockNumbers:=[22], blockSizes:=[6],
blocks:=[[1,2,3,5,6,8], [1,2,3,7,10,12], [1,2,4,5,7,11], [1,2,4,8,9,10], [1,2,6,9,11,12], [1,3,4,5,9,12], [1,3,4,6,10,11], [1,3,7,8,9,11], [1,4,6,7,8,12], [1,5,6,7,9,10], [1,5,8,10,11,12], [2,3,4,6,7,9], [2,3,4,8,11,12], [2,3,5,9,10,11], [2,4,5,6,10,12], [2,5,7,8,9,12], [2,6,7,8,10,11], [3,4,5,7,8,10], [3,5,6,7,11,12], [3,6,8,9,10,12], [4,5,6,8,9,11], [4,7,9,10,11,12]],
isBinary:=true, isBlockDesign:=true, isSimple:=true, r:=11,
tSubsetStructure:=rec(lambdas:=[5], t:=2), v:=12)]

More Triads

We can present these block designs as 22 columns of 12 pitch classes, indicating presence or absence thereof with a blue ball. For example the fourth design would look like:

Fourth Block Design

Notice that, in deference to musicians, the blocks have been vertically flipped so that GAP's block design's set element labellings 1 … 12 read upwards from bottom to top and have simultaneously been re-labelled on the right with more familiar note names. Naturally, the corresponding pitch class numbers 0 … 11 may be read as starting from other than the conventional C.

What may be of some interest in this kind of block design is that it gives plenty of scope for ordering the blocks in such a way as to carry several pitch classes at once from one block to the next, were one minded to place the blocks in some 22 bar sequence. For example, there are 6C3 = 20 triads available from each hexad. GAP may be used to list them - for example the final design's first emitted block is [1,2,3,5,6,8] and we can get GAP to list all its 3-sets with:

T:=Combinations([1,2,3,5,6,8], 3);

And GAP obliges with the list of that hexad's 20 triads:

[ [1,2,3], [1,2,5], [1,2,6], [1,2,8], [1,3,5], [1,3,6], [1,3,8], [1,5,6], [1,5,8], [1,6,8], [2,3,5], [2,3,6], [2,3,8], [2,5,6], [2,5,8], [2,6,8], [3,5,6], [3,5,8], [3,6,8], [5,6,8] ]

If we pick another block, at random, out of that same design, say [2,4,5,6,10,12], we may list its triads:

T:=Combinations([2,4,5,6,10,12], 3);

And GAP provides:

[ [2,4,5], [2,4,6], [2,4,10], [2,4,12], [2,5,6], [2,5,10], [2,5,12], [2,6,10], [2,6,12], [2,10,12], [4,5,6], [4,5,10], [4,5,12], [4,6,10], [4,6,12], [4,10,12], [5,6,10], [5,6,12], [5,10,12], [6,10,12] ]

Visual inspection tells us that these two lists have only [2,5,6] in common, but GAP may be used to confirm this with

Intersection(Combinations([1,2,3,5,6,8], 3), Combinations([2,4,5,6,10,12], 3));

by emitting:

[ [2,5,6] ]

The DESIGN package presents what we need ideally suited for passing as arguments to GAP's more basic functions. For instance the aforementioned [1,2,3,5,6,8] and [2,4,5,6,10,12] are available to us directly from D as D[4].blocks[1] and D[4].blocks[15] and it's easy to see that we can find all common triads between each of the 22 hexads with a simple nested for loop:

for i in [1..21] do one:=Combinations(D[4].blocks[i], 3); for j in [i+1..22] do two:=Combinations(D[4].blocks[j], 3); Print(i, " - ", j, " : ", Intersection(one, two), "\n"); od; od;

And 21×22/2 = 231 lines of output are printed, almost all of which show exactly one triad common between each pair of hexads - excepting 11 cases where no triad is held in common. We can also discern that each of the remaining 220 common triads is distinct with no repeats. In fact all possible 12C3 = 12!/(9!3!) = 220 triads are accounted for (as common links between hexad pairs) in this structure. The 22 blocks may thus be represented as a 22 node graph wherein each node is a hexad with 20 triadic edges to other nodes. A complete 22 node graph would connect each node with its 21 neighbours. Our graph has, apparently, each node singling out one particular unloved neighbour. This latter property means that the nodes come in pairs because - for example - any node's one and only disconnected neighbour must be a node whose one and only disconnected neighbour must be that first node since it cannot be not-connected to a third, different, node.

Make GAP work for you

At this point it is worth defining our own GAP function to construct lists of node pairs connected by edges representing any commonly contained k-chords:

BlockLinks := function(des, k)
local blx, blc, lis, i, j, one, two, com;
blx := des.blocks;;
blc := des.blockNumbers[1];;
lis := [];;
for i in [1 .. blc-1] do
one := Combinations(blx[i], k);;
for j in [i+1 .. blc] do
two := Combinations(blx[j], k);;
com := Intersection(one, two);;
if Length(com) > 0 then Add(lis, [ i, j, com]);; fi;
return lis;

By invoking this function in GAP with:

L43 := BlockLinks(D[4], 3);

we can confirm that it generates a list with 220 members (the 11 'empties' have been filtered out), one of which is [1, 15, [[2,5,6]]], representing the fact that the first and fifteenth blocks in the fourth design each share the single common triad [2,5,6] - as noted above. Note that the items are no longer printed (one per line) with punctuating '-' and ':' marks as the output is now a proper GAP object.

Fourth Block Design reordered for Circuit

As this particular design, viewed as a graph of hexadic nodes and common-triadic edges between then is so well-connected, it's not at all difficult to find Hamiltonian Circuits. In fact the design as presented is almost such a circuit (i.e. from block 1 to block 2 via [1,2,3], from block 2 to block 3 via [1,2,5] … etc. One needs only to swap block 12 with 13 and block 21 with 22 to recover this design to the left.

The horizontal bars show the common triads linked across successive columns (vertically flipped as before), including the wraparound at the final column back to the first.

If we look at the third block design however, with a completely different set of 22 hexads -

L33 := BlockLinks(D[3], 3);

We recover this time a list containing only 176 members. This time we can see that the inter-node triadic commonalities form a more complex network. Here is a 'bleeding chunk' of it, showing all non-empty triadic commonalities at node 14:

[14,15,[[2,5,9], [2,5,10], [2,5,11], [2,9,10], [2,9,11], [2,10,11], [5,9,10], [5,9,11], [5,10,11], [9,10,11]]],

We can see that not only is node 14 (corresponding to the hexad [2,3,5,9,10,11]) disconnected from five nodes instead of just one, it is connected to node 15 (hexad [2,5,9,10,11,12]) by ten common triads rather than just one. Furthermore, nodes 1 and 2 are connected to 14 by the same triad (as are nodes 6 and 7, 12 and 13 etc). Design 3 is clearly a very different musical animal to design 4.

Tetradic Bridges Across the Hexachords

We can use our homebrew function to look for common tetrad links between the hexads of the second design:


Second Block Design as (tetradically edged) Graph

The above GAP output looks reasonably promising for just 22 barsworth, and with three edges per node it seems worth presenting here, left, as a graph. We've distributed the 22 hexadic nodes evenly, in the design's order, circularly clockwise from the top. Then we've linked them by their common tetradic edges. There's no point in labelling anything yet.

This graph is connected enough to be worth running through Prof Dharwadker's Hamiltonian Circuit detection software

Second Block Design Reordered as (tetradically edged Circuit) Graph

The software finds eight circuits. Here is the fifth (clockwise through blocks 16 21 22 8 9 15 12 18 7 13 17 11 3 4 5 10 20 19 6 1 2 14 and back to 16 at top)

The light blue boxes carry the six pitch classes of the hexachord as a hexadecimal string where each of the design's block's set element value has been reduced by 1, to bring it into our more conventional modulo 12 territory. Thus block 16, at the top, originally presented as [2,4,6,7,10,11] is here notated as 13569A. The common tetrads between joined nodes are similarly hexadecimally reduced and lie midway along the edge adjoining the two nodes sharing the tetrad.

It has not escaped our attention that the remaining 11 non-circuit edges (i.e. those edges not on the circumference but crossing the circle) of the re-ordered block exhibit an unexpected symmetry along the diametrically dividing line situated from 4 to 15 on the 22 hour clock topped at 0 (or at roughly ten past two across to ten past eight on a 12 hour clock). This rather suggests that a musical composition might start on one of those nodes - it would then, halfway through the run, essentially reverse.

Second Block Design Reordered

The design looks like this, with the design's blocks reordered according to the discovered Hamiltonian circuit, and with the aforementioned symmetry point (originally block [1,4,9,10,11,12], now labelled as 0389AB) moved to the first place.

We can also rotate the graph to make its tetradic-link symmetry more plain as a north-south reflection. The leftmost, westernmost, 9 o'clockiest point corresponds to that new first block, and it thence clockwisely follows the new block order.

Second Block Design Graph Rotated

The Tetrads common to the successive Hexachords in the re-ordered Block Design #2

The Tetrads common to successive Hexachords

Common Pentads - A Bridge too far?

If we now have a look at common pentads between the hexads, by invoking BlockLinks(D[4], 5), we get a completely disconnected graph - there are no common pentads between any of the hexads. But if we try the third design we have a little more luck:


But just not very much. One might expect that 5 is possibly a little too close to 6 for common PC sets in such a small population of 22 blocks. But remember that the initial purpose of block designs was to provide sufficiently large test environments for conducting experiments without introduction of accidental bias. One is quite free to bump up the λ and the r values (subject to those constraints between the parameters) and generate designs with large numbers of blocks, and which still give equal opportunity to all pitch classes and intervals.

So, musically speaking, there's an awful lot to play with here and GAP can help you research the features you may wish to examine. As for converting these designs and any derivatives or transformations thereof into actual music, well, you know what to do. At the risk of pointing out the obvious, all of the integers in these set representations are ranged from 1 to 12, so just subtract 1 from each to recover the musician's traditional PC modulo 12 notations - or just live with it and treat the 12 as a 0. Everything else is just transposition anyway.

Remember that block designs are not restricted to using 12-sets and there's no need to bind yourself inside a twelve-tone universe.

More GAP code

A modification of the BlockLinks function can also provide an adjacency matrix since it may as well be calculated at the same time as the common subsets. First of all, we provide an 'object labelling' function:

OrdList36 := function(lords)
  local s, l, i, u;

  s := [];;
  if IsList(lords) then
    l := Length(lords);;
    if l > 0 then
      if IsInt(lords[1]) then
        for i in [1 .. l] do
          Add(s, u[lords[i]]);;
        for i in [1 .. l] do
          Add(s, OrdList36(lords[i]));;
  return s;

This provides us with PC-Set friendly labels, allowing us to replace the rather bulky (and 1-based) set denotations such as [1,3,5,7,10,11] with a sugar-free (and 0-based, hex-encoded) label 02469A.

The new function, BlockDesignGraph, produces a record carrying node and edge counts and names using the friendlier labels as well as an adjacency matrix which may be used to find those Hamiltonian paths and circuits:

BlockDesignGraph := function(design, subsiz)
  local blx, blc, lis, i, j, k, len, one, two, com, mat;
  blx := design.blocks;;
  blc := design.blockNumbers[1];;
  mat := NullMat(blc, blc);
  lis := [];;
  for i in [1 .. blc-1] do
    one := Combinations(blx[i], subsiz);;
    for j in [i+1 .. blc] do
      two := Combinations(blx[j], subsiz);;
      com := Intersection(one, two);;
      len := Length(com);
      if len > 0 then
        for k in [1 .. len] do
          Add(lis, [ i, j, OrdList36(com)[k]]);;
        mat[i][j] := mat[i][j] + len;;
        mat[j][i] := mat[j][i] + len;;
  return rec(nNames:= OrdList36(blx), nCount := Length(blx), eNames:=lis, eCount:=Length(lis), incidenceMatrix:=mat);