Skip to content

Group the subgraphs found by shape, and draw them - #15

Merged
loloDawit merged 8 commits into
mainfrom
subgraph-shapes
Sep 9, 2026
Merged

loloDawit merged 8 commits into
mainfrom
subgraph-shapes

Conversation

@loloDawit

@loloDawit loloDawit commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Enumerating connected subgraphs is the first step of motif discovery. This is
the second.

The app reported {0, 1, 2} and {2, 3, 4} as two answers without ever saying
both are triangles — which is what a motif actually is. There was also nothing
to compare against a random network, because there were no shapes to count.

Both implementations get the feature: the desktop app on shortcut G, the
browser demo beside the graph.

Grouping

Shape.of finds a canonical form by brute force: every relabelling of a
subgraph's vertices is tried and the smallest adjacency bitstring kept. Two
subgraphs match when their canonical forms do.

Industrial tools use nauty, which earns its keep at the sizes they work at. A
subgraph here has at most a few vertices, so a few hundred relabellings cost
microseconds, need no C library, no JNI and no second port — and the method
fits in a sentence, which matters for a tool whose job is explaining.

Shapes are listed most frequent first, drawn, and named where people have a
name for them: triangle, path, star, cycle, diamond, clique.

Drawing what was found

A shape and a count still do not show you what the subgraphs look like, or that
they look like each other. Choosing a shape now draws every subgraph having it,
each keeping its real vertex numbers, in the same visual language as the input
graph. Identical shape, different vertices, which is the reason to group them.

Clicking one shows it in the input graph, so it is clear where in the network
that occurrence sits.

Every instance is shown whatever step is on screen: the shapes and their counts
describe the finished search, not how far the stepping has got. Selecting a
shape at step 9 of 38 would otherwise show an empty grid under a header saying
six.

Keeping the two implementations honest

java-golden.json now records, per graph and size, the shapes found: name,
edge count, how many, in what order, and the edges each one draws — 30 entries.
The Java classifier gained a fixed order among equal counts, since listing by
insertion order would differ between implementations for no useful reason.

What the tests learned

Four tests here were worthless as first written, and all four failed the same
way: the example chosen had too little variety to tell right from wrong.

  • The invariance check used a complete subgraph, which is symmetric, so every
    relabelling gives the same answer whether the form is canonical or not. It
    passed against an implementation keeping an arbitrary relabelling.
  • The drawing check ran against a graph with a single shape, so "every shape
    draws differently" was true of anything.
  • Fixing that, it still only checked distinctness — and three wrong drawings
    are still three different drawings.
  • The demo's instance count would have passed had the chosen shape occurred
    once, so it now insists on more than one.

Two bugs are covered by different tests, neither catching the other's: every
pair reading the same bit fails the DOM check, which counts lines drawn against
the edges a shape claims (star drew 6 edges, expected 3); the bit order
reversed fails 11 equivalence cases, which an edge count cannot see because
reversing preserves how many bits are set.

65 Java tests, 94 TypeScript.

Enumerating connected subgraphs is the first step of motif discovery.
This is the second. Until now the app reported {0, 1, 2} and {2, 3, 4}
as two answers without ever saying both are triangles, which is the
thing a motif actually is.

Shape.of finds a canonical form by brute force: every relabelling of the
subgraph's vertices is tried and the smallest adjacency bitstring kept.
Two subgraphs match when their canonical forms do. Industrial tools use
nauty, which earns its keep at the sizes they work at; a subgraph here
has at most a few vertices, so a few hundred relabellings cost
microseconds, need no library, and can be explained in a sentence.

Shape.classify groups them, most frequent first, and names the ones
people have names for: triangle, path, star, cycle, diamond, clique.

Checked by hand against the samples: the bowtie has exactly 2 triangles
among 6 connected triples, and cluster.txt contains exactly one K4.

One test was worthless when written. The invariance check -- that
relabelling a subgraph does not change its shape -- used a complete
subgraph, which is symmetric, so every relabelling gives the same answer
whether the form is canonical or not. It passed against an
implementation that kept an arbitrary relabelling instead of the
smallest. It now uses a triangle with a tail, and kills that mutation.
The results were a list of vertex sets. Grouped by shape they become a
statement about the graph: six triangles and four paths, rather than ten
sets of numbers.

- ShapesPanel lists each shape drawn, named where people have a name for
  it, with how often it occurred. Vertices are dots rather than numbers,
  because which vertex is which is exactly what a shape ignores.
- Choosing a shape picks its subgraphs out in the tree, which is the
  question the panel invites: where are they?
- It shares the side slot with the history, on shortcut G, and choosing
  one puts the other away.
- The status line now ends with how many distinct shapes were found.

EsuSession classifies once and remembers, and can name the tree nodes
having a given shape.

ThemeTest passed on the new style classes without being touched, which
is what it is for: the dark sheet had to gain matching rules or fail.
The shape drawings were wrong: a path and a triangle with a tail came
out identical, and a cycle drew no edges at all.

Shape.joins walked the pairs to find a bit position but never advanced
the counter, so every pair read bit zero. Shapes with different edges
therefore drew the same picture, and the panel was stating something
false about the results.

The method had no test. Its only caller is the drawing, the name test
goes through degrees() instead, and every other test compares whole
canonical forms, so nothing exercised it. It was found by looking at the
running app.

Three tests now cover it: a triangle joins all three of its pairs, every
shape joins exactly as many pairs as it claims edges, and distinct
shapes must produce distinct pictures. All three fail against the old
code.
Ports Shape to TypeScript and puts the shapes beside the graph in the
demo: a chip per shape, drawn with its count, and choosing one picks its
subgraphs out in the tree. Choosing it again clears the choice.

The Java classifier gained a fixed order among equal counts. Listing by
insertion order would differ between the two implementations for no
meaningful reason, and the fixtures compare the listings.

java-golden.json now records, for each graph and size, the shapes found:
name, edge count, how many, in order, and the edges each one draws. 30
entries. A TypeScript classifier that groups differently, names
differently, orders differently or draws differently fails.

Two mutations, caught by different tests, and neither test catches the
other's bug:

  every pair reading the same bit -- the actual Java bug -- fails the
  DOM test, which counts the lines drawn against the edges the shape
  claims: star drew 6 edges, expected 3.

  the bit order reversed fails 11 equivalence cases. The edge count
  cannot see it, because reversing preserves how many bits are set.

Three attempts were needed to get there. The first checked only that
shapes drew differently, which passed against a default graph having a
single shape. The second fixed the graph but still only checked
distinctness, and three wrong drawings are still three different
drawings.
Choosing a shape outlined its subgraphs in the tree, but they were still
four text labels: 0123, 0124, 0234, 1234. Nothing showed what they look
like, or that they look like each other.

The shapes panel now draws them. Each subgraph appears as its own little
graph, in the same style as the input graph -- vertices on a circle,
numbered, joined where the graph joins them -- with its vertices written
underneath. Identical shape, different vertices, which is the point of
grouping them at all.

Clicking one lights it up in the input graph, so it is clear where in
the network that occurrence sits.

Every instance is shown whatever step is on screen, because the shapes
and their counts describe the finished search rather than how far the
stepping has got. Selecting a shape at step 9 of 38 would otherwise show
an empty grid: none of them has been reached yet.
The list had a fixed height chosen when a search found three shapes.
A search finds two shapes at size three and around thirty-three at size
six, so a fixed height is either mostly empty or cramped: at size four
it showed five shapes in a box that fitted four, and scrolled.

It now fits its contents up to eight rows and scrolls beyond that,
leaving the rest of the panel to the drawings.

The heading also says how many there are, since a group can hold thirty
or more and the grid scrolls.
Brings the demo level with the desktop app: choosing a shape now draws
every subgraph having it, each with its real vertex numbers, and
clicking one shows it in the input graph.

The drawings are numbered, unlike the shape thumbnails beside them,
because the point of these is that the shape is the same and the
vertices are not.

Three tests, each killed by a matching mutation: drawing only the first
instance, clicking a tile highlighting nothing, and asking for one
shape's subgraphs but returning all of them. The count assertion also
insists the chosen shape has more than one instance, since a single one
would let the first of those pass.
@loloDawit loloDawit changed the title Group the subgraphs found by shape Group the subgraphs found by shape, and draw them Sep 9, 2026
@loloDawit
loloDawit merged commit 7ad3fa8 into main Sep 9, 2026
2 checks passed
@loloDawit
loloDawit deleted the subgraph-shapes branch September 9, 2026 20:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant