Graph::RandomMaze examples

Introduction

This document (notebook) demonstrates the functions of “Graph::RandomMaze”, [AAp1], for generating and displaying random mazes. The methodology and implementations of maze creation based on random rectangular and hexagonal grid graphs are described in detail in the blog post “Day 24 – Maze Making Using Graphs”, [AA1], and in the Wolfram notebook “Maze Making Using Graphs”, [AAn1].

Remark: The corresponding Wolfram Language implementation is Wolfram Function Repository function “RandomLabyrinth”, [AAf1].

Remark: Both synonyms, “labyrinth” and “maze,” are used in this document.

TL;DR

Just look at the “neat examples” in the last section.


Documentation

This section gives basic documentation of the subs.

Usage

FunctionDescription
random-maze(n)generate a random labyrinth based on n × n grid graph
random-maze([n, m])generate a random labyrinth based on a grid graph with n rows and m columns
&random-labyrintha synonym of &random-maze
display-maze(m)displays outputs random-maze using Graphviz graph layout engines

Details & Options

  • The sub random-maze generates mazes based on regular rectangular grid graphs or hexagonal grid graphs.
  • By default, are generated random mazes based on rectangular grid graphs.
  • The named argument (option) “type” can be used the specify the type of the grid graph used for maze’s construction.
  • The labyrinth elements can be obtained by using the second argument (the “properties argument.”)
  • The labyrinth elements are: walls, paths (pathways), solution, start, and end.
  • The sub display-maze can be used to make SVG images of the outputs of random-maze.
  • By default display-maze uses the Graphviz engine “neato”.
  • The sub random-maze uses the grid graphs Graph::GridGraph::HexagonalGrid, and Graph::TriangularGrid. For more details see [AA1, AAn1].
  • For larger sizes the maze generation might be (somewhat) slow.

Setup

Here are the packages used in this document:

use Graph::RandomMaze;
use Data::Generators;
use JavaScript::D3;
use Hash::Merge;

Here are Graph.dot options used in this document:

my $engine = 'neato';
my $vertex-shape = 'square';
my $graph-size = 8;
my %opts = :$engine, :8size, vertex-shape => 'square', :!vertex-labels, edge-thickness => 12;
my %hex-opts = :$engine, :8size, vertex-shape => 'hexagon', :!vertex-labels, vertex-width => 0.8, vertex-height => 0.8, edge-thickness => 32;

my $background = '#1F1F1F';

This code is used to prime the notebook to display (JavaScript) D3.js graphics:

#% javascript
require.config({
     paths: {
     d3: 'https://d3js.org/d3.v7.min'
}});

require(['d3'], function(d3) {
     console.log(d3);
});


Examples

Basic Examples

Make a random rectangular grid labyrinth with 8 rows and columns:

#%html
random-maze(8).dot(|%opts, :3size):svg

Make a random rectangular grid labyrinth with 5 rows and 8 columns:

#%html
random-maze([5, 8]).dot(|%opts, :3size):svg

Scope

Make a random hexagonal grid labyrinth:

#% html
random-maze([8, 16], type => "hexagonal").dot(|%hex-opts):svg

Make a labyrinth using options to specify the rows and columns of the walls graph:

#% html
random-maze(:10rows, :5columns)
andthen display-maze($_, |%opts, :3size)

The sub random-maze take an optional properties argument. Here are the different properties:

random-maze("properties")

# [type dimensions walls paths solution start end]

If the properties argument is Whatever, then an association with all properties is returned (“props” can be used instead of “properties”):

random-maze(5, props => Whatever)

# {dimensions => [5 5], end => 3_3, paths => Graph(vertexes => 16, edges => 15, directed => False), solution => [0_0 1_0 1_1 2_1 2_2 2_3 3_3], start => 0_0, type => rectangular, walls => Graph(vertexes => 23, edges => 21, directed => False)}

The first argument of the sub display-maze can be a graphs or a hashmap. Here is example of using both argument types:

#%html
my %new-opts = merge-hash(%opts, {:2size});
[
    graph   => display-maze(random-maze(5, props => 'walls' ), |%new-opts),
    hashmap => display-maze(random-maze(5, props => Whatever), |%new-opts) 
]
==> to-html-table()


Options

Type

The option :$type specifies the type grid graphs used to make the labyrinth. It takes the values “rectangular” and “hexagonal”:

#% html
<rectangular hexagonal>
andthen .map({ random-maze(7, type => $_, props => Whatever) }).List
andthen 
    [
        $_.head<type> => display-maze($_.head, |merge-hash(%opts, {:3size})), 
        $_.tail<type> => display-maze($_.tail, |merge-hash(%hex-opts, {size => 4.5}))
    ]
andthen .&to-html-table

DOT options

The sub display-graph takes Graphviz DOT options for more tuned maze display. The options are the same as those of Graph.dot.

#%html
random-maze([5, 10], props => 'walls')
==> display-maze(:$engine, vertex-shape => 'ellipse', vertex-width => 0.6, :6size)


Applications

Rectangular maze with solution

Make a rectangular grid labyrinth and show it together with a (shortest path) solution:

#%html
my %res = random-maze([12, 24], props => <walls paths solution>);

display-maze(%res, |%opts)

Hexagonal maze with solution

Make a hexagonal grid labyrinth and show it together with a (shortest path) solution:

#%html
my %res = random-maze([12, 20], type => 'hexagonal', props => <walls paths solution>);

display-maze(%res, |%hex-opts)

Distribution of solution lengths

Generate — in parallel — 500 mazes:

my @labs = (^500).race(:4degree, :125batch).map({ random-maze(12, props => <walls paths solution>) });
deduce-type(@labs)

# Vector(Struct([paths, solution, walls], [Graph, Array, Graph]), 500)

Show the histogram of the shortest path solution lengths:

#% js
js-d3-histogram(
    @labs.map(*<solution>)».elems, 
    title => 'Distribution of solution lengths',
    title-color => 'Silver',
    x-axis-label => 'shortest path solution length',
    y-axis-label => 'count',
    :$background, :grid-lines, 
    :350height, :450width
)

Show the mazes with the shortest and longest shortest paths solutions:

#% html
@labs.sort(*<solution>.elems).List
andthen 
    [
        "shortest : {$_.head<solution>.elems}" => display-maze($_.head, |merge-hash(%opts , {:3size})),
        "longest : {$_.tail<solution>.elems}"  => display-maze($_.tail, |merge-hash(%opts , {size => 3}))
    ]
andthen .&to-html-table


Neat Examples

Larger rectangular grid maze:

#%html
random-maze([30, 60]).dot(|%opts, edge-thickness => 25):svg

A larger hexagonal grid maze with its largest connected components colored:

#%html
my $g = random-maze([20, 30], type => 'hexagonal', props => 'walls');
$g.dot(highlight => $g.connected-components.head(2).map({ my $sg = $g.subgraph($_); [|$sg.vertex-list, |$sg.edge-list] }), |%hex-opts):svg

A grid of tiny labyrinths:

#%html
my $k = 6;
my @mazes = random-maze((6...7).pick) xx $k ** 2;
my %new-opts = size => 0.8, vertex-shape => 'circle', vertex-width => 0.35, vertex-height => 0.35, edge-thickness => 36;
my @maze-plots = @mazes.map({ $_.dot(|%opts, |%new-opts, :svg) });

@maze-plots
==> to-html(:multi-column, :6columns, :html-elements)


References

Articles

[AA1] Anton Antonov, “Day 24 – Maze Making Using Graphs”, (2025), Raku Advent Calendar at WordPress.

Notebooks

[AAn1] Anton Antonov, “Maze making using graphs”, (2026), Wolfram Community.

Functions, packages

[AAf1] Anton Antonov, RandomLabyrinth, (2025), Wolfram Function Repository.

[AAp1] Anton Antonov, Graph::RandomMaze, Raku package, (2025), GitHub/antononcube.

[AAp2] Anton Antonov, Graph, Raku package, (2024-2025), GitHub/antononcube.

Collatz conjecture visualizations

Introduction

This blog post (notebook) presents various visualizations related to the Collatz conjecture, [WMW1, Wk1] using Raku.

The Collatz conjecture, a renowned, unsolved mathematical problem, questions whether iteratively applying two basic arithmetic operations will lead every positive integer to ultimately reach the value of 1.

In this notebook the so-called “shortcut” version of the Collatz function is used:

That function is used repeatedly to form a sequence, beginning with any positive integer, and taking the result of each step as the input for the next.

The Collatz conjecture is: This process will eventually reach the number 1, regardless of which positive integer is chosen initially.

Raku-wise, subs for the Collatz sequences are easy to define. The visualizations are done with the packages “Graph”, [AAp1], “JavaScript::D3”, [AAp2], and “Math::NumberTheory”, [AAp3].

There are many articles, blog posts, and videos dedicated to visualizations of the Collatz conjecture. (For example, [KJR1, PZ1, Vv1]).

Remark: Consider following the warnings in [Vv1] and elsewhere:

Do not work on this [Collatz] problem! (Do real math instead.)

Remark: Notebook’s visualizations based on “JavaScript::D3” look a lot like the visualizations in [PZ1] — D3js is used in both.

Remark: See the Bulgarian version of this post: “Визуализации свързани с хипотезата на Колац”.


Setup

use Data::Reshapers;
use Data::Summarizers;
use Data::TypeSystem;
use Data::Translators;
use Graph;
use JavaScript::D3;
use Math::NumberTheory;

#%javascript

require.config({
     paths: {
     d3: 'https://d3js.org/d3.v7.min'
}});

require(['d3'], function(d3) {
     console.log(d3);
});

my $background = 'none';
my $stroke-color = 'Ivory';
my $fill-color = 'none';
my $title-color = 'DarkGray';

Additional subs are defined for getting color-blending sequences.

sub darker-shades(Str $hex-color, Int $steps) {
    my @rgb = $hex-color.subst(/ ^ '#'/).comb(2).map({ :16($_) });
    my @shades;
    for 1..$steps -> $step {
        my @darker = @rgb.map({ ($_ * (1 - $step / ($steps + 1))).Int });
        @shades.push: '#' ~ @darker.map({ sprintf '%02X', $_ }).join;
    }
    return @shades;
}

#say darker-shades("#34495E", 5);

sub blend-colors(Str $color1, Str $color2, Int $steps) {
    my @rgb1 = $color1.subst(/ ^ '#'/).comb(2).map({ :16($_) });
    my @rgb2 = $color2.subst(/ ^ '#'/).comb(2).map({ :16($_) });
    my @blended;

    for ^$steps -> $step {
        my @blend = (@rgb1 Z @rgb2).map({
            ($_[0] + ($step / $steps) * ($_[1] - $_[0])).Int
        });
        @blended.push: '#' ~ @blend.map({ sprintf '%02X', $_ }).join;
    }
    
    return @blended;
}

#say blend-colors("#34495E", "#FFEBCD", 5);


Collatz function definition

Here is a sub for the shortcut version of the Collatz function:

sub collatz(UInt $n is copy, Int:D $max-steps = 1000) {
    return [] if $n == 0;
    my @sequence = $n;
    while $n != 1 && @sequence.elems < $max-steps {
        $n = ($n %% 2 ?? $n div 2 !! (3 * $n + 1) / 2).Int;
        @sequence.push: $n;
    }
    return @sequence;
}

Here is an example using  as a sequence seed (i.e. starting value):

collatz(26)

# [26 13 20 10 5 8 4 2 1]

The next integer, , produces a much longer sequence:

collatz(27).elems

# 71


Simple visualizations

Collatz sequence numbers

Here is the simplest, informative Collatz sequence — or hailstone numbers — plot:

#% js
js-d3-list-line-plot(collatz(171), :$background, :$title-color, title => 'Hailstone numbers of 171')

Let us make a multi-line plot for a selection of seeds.

my @data = (1..1_000).map({ collatz($_) }).grep({ 30 ≤ $_.elems ≤ 150 && $_.max ≤ 600 }).pick(10).sort(*.head).map({my $i = $_.head; $_.kv.map(-> $x, $y {%(group => $i, :$x, :$y )}).Array }).map(*.Slip).Array;

deduce-type(@data)

# Vector(Assoc(Atom((Str)), Atom((Int)), 3), 320)

#% js
js-d3-list-line-plot(@data.flat, :$background)

Remark: Using simple sampling like the code block below would generally produce very non-uniform length- and max-value sequences.
Hence, we do the filtering above.

my @data = (^100).pick(9).sort.map(-> $i {collatz($i).kv.map(-> $x, $y {%(group => $i, :$x, :$y )}).Array }).map(*.Slip).Array;


Distributions

Here are Collatz sequences and their corresponding lengths and max-values:

my $m = 100_000;
my @cSequences = (1..$m).map({ collatz($_) });
my @cLengths = @cSequences».elems;
my @cMaxes = @cSequences».max;

my @dsCollatz = (1...$m) Z @cLengths Z @cMaxes;
@dsCollatz = @dsCollatz.map({ <seed length max>.Array Z=> $_.Array })».Hash;

sink records-summary(@dsCollatz, field-names => <seed length max>)

# +-------------------+--------------------+------------------------+
# | seed              | length             | max                    |
# +-------------------+--------------------+------------------------+
# | Min    => 1       | Min    => 1        | Min    => 1            |
# | 1st-Qu => 25000.5 | 1st-Qu => 47       | 1st-Qu => 42272        |
# | Mean   => 50000.5 | Mean   => 72.88948 | Mean   => 320578.18243 |
# | Median => 50000.5 | Median => 68       | Median => 85292        |
# | 3rd-Qu => 75000.5 | 3rd-Qu => 97       | 3rd-Qu => 162980       |
# | Max    => 100000  | Max    => 222      | Max    => 785412368    |
# +-------------------+--------------------+------------------------+

Here are histograms of the Collarz sequences lengths and max-value distributions:

#% js
js-d3-histogram(
    @cLengths, 
    100,
    :$background,
    :600width, 
    :400height, 
    title => "Collatz sequences lengths distribution (up to $m)",
    :$title-color
  )
~
js-d3-histogram(
    @cMaxes».log(10), 
    100,
    :$background,
    :600width, 
    :400height, 
    title => "Collatz sequences lg(max-value) distribution (up to $m)",
    :$title-color
  )

Here is a scatter plot of seed vs. sequence length:

#% js
js-d3-list-plot(
    @cLengths, 
    :$background, 
    :2point-size,
    :800width, 
    :400height, 
    title => 'Collatz sequences lengths',
    x-label => 'seed',
    y-label => 'sequence length',
    :$title-color
  )


Benford’s law adherence

It is of interest to see the manifestation of Benford’s law for the first digits of Collatz hailstones.
Here is the corresponding digit tally:

my %digitTally = @cSequences.race(:4degree).map({ $_».comb».head }).flat.&tally

# {1 => 2067347, 2 => 1375360, 3 => 870823, 4 => 857427, 5 => 581237, 6 => 448351, 7 => 334441, 8 => 443043, 9 => 310919}

Here is a comparison with the corresponding Benford’s law values:

#% html
sub benford-law(UInt:D $d, UInt:D $b = 10) { log($d + 1, $b) - log($d, $b) };

my @dsDigitTally = 
    %digitTally.sort(*.key.Int).map({%( 
        digit => $_.key, 
        value => round($_.value / %digitTally.values.sum, 10 ** -7), 
        benford => round(benford-law($_.key.Int), 10 ** -7)) }) 
==> to-html(field-names => <digit value benford>)

digitvaluebenford
10.28362760.30103
20.18869120.1760913
30.11947170.1249387
40.11763380.09691
50.07974220.0791812
60.06151110.0669468
70.04588330.0579919
80.06078280.0511525
90.04265620.0457575

Good adherence is observed for a relatively modest number of sequences.
Here is a corresponding bar chart:

#% js
my @data = 
    |@dsDigitTally.map({ <x y group>.Array Z=> [|$_<digit value>, 'Collatz'] })».Hash,
    |@dsDigitTally.map({ <x y group>.Array Z=> [|$_<digit benford>, 'Benford'] })».Hash;
    
js-d3-bar-chart(
    @data,
    title => "First digits frequencies (up to $m)",
    :$title-color,
    x-label => 'digit',
    y-label => 'frequency', 
    :!grid-lines, 
    :$background, 
    :700width, 
    :400height,
    margins => { :50left }
)


Sunflower embedding

A certain concentric pattern emerges in the spiral embedding plots of the Collatz sequences lengths mod 8. (Using mod 3 makes the pattern clearer.)
Similarly, a clear spiral pattern is seen for the maximum values.

#% js
my @sunflowerLengths = sunflower-embedding(16_000, with => { collatz($_).elems mod 8 mod 3 + 1}):d;
my @sunflowerMaxes = sunflower-embedding(16_000, with => { collatz($_).max mod 8 mod 3 + 1}):d;

js-d3-list-plot(@sunflowerLengths, 
    background => 'none',
    point-size => 4,
    width => 500, height => 440, 
    :!axes, 
    :!legends,
    color-scheme => 'Observable10',
    margins => {:20top, :20bottom, :50left, :50right}
 )

~

js-d3-list-plot(@sunflowerMaxes, 
    background => 'none',
    point-size => 4,
    width => 500, height => 440, 
    :!axes, 
    :!legends,
    color-scheme => 'Observable10',
    margins => {:20top, :20bottom, :50left, :50right}
 )


Small graphs

Define a sub for graph-edge relationship between consecutive integers in Collatz sequences:

proto sub collatz-edges(|) {*}

multi sub collatz-edges(Int:D $n) {
    ($n mod 3 == 2) ?? [$n => 2 * $n, $n => (2 * $n - 1) / 3] !! [$n => 2 * $n,]
}

multi sub collatz-edges(@edges where @edges.all ~~ Pair:D) {
    my @leafs = @edges».value.unique;
    @edges.append(@leafs.map({ collatz-edges($_.Int) }).flat)
}

# &collatz-edges

For didactic purposes let use derive the edges of a graph using a certain small number of iterations:

my @edges = Pair.new(2, 4);

for ^12 { @edges = collatz-edges(@edges) }

deduce-type(@edges)

# Vector((Any), 536)

Make the graph:

my $g = Graph.new(@edges.map({ $_.value.Str => $_.key.Str })):directed

# Graph(vertexes => 155, edges => 154, directed => True)

Plot the graph using suitable embedding:

#% html
$g.dot(
    engine => 'dot',
    :$background,
    vertex-label-color => 'Gray',
    vertex-shape => 'ellipse',
    vertex-width => 0.8,
    vertex-height => 0.6,
    :24vertex-font-size,
    edge-thickness => 6,
    graph-size => 12
):svg

The Collatz sequence paths can be easily followed in the tree graph.


Big graph

Let us make a bigger, visually compelling graph:

my $root = 64;
my @edges = Pair.new($root, 2 * $root);
for ^20 { @edges = collatz-edges(@edges) }
my $gBig = Graph.new(@edges.map({ $_.value.Str => $_.key.Str })):!directed;

# Graph(vertexes => 2581, edges => 2580, directed => False)

Next we find the path lengths from the root to each vertex in order to do some sort concentric coloring:

my %path-lengths = $gBig.vertex-list.race(:4degree).map({ $_ => $gBig.find-path($_, $root.Str).head.elems });
%path-lengths.values.unique.elems

# 22

We make a blend of these colors:

JavaScript::D3::Utilities::get-named-colors()<darkred plum orange>

# (#8B0000 #DDA0DD #FFA500)

Here is the graph plot:

#%html
my %classes = $gBig.vertex-list.classify({ %path-lengths{$_} });
my @colors = |blend-colors("#8B0000", "#DDA0DD", 16), |blend-colors("#DDA0DD", "#FFA500", %classes.elems - 16);
my %highlight = %classes.map({ @colors[$_.key - 1] => $_.value });

$gBig.dot(
    engine => 'neato',
    :%highlight,
    :$background,
    vertex-shape => 'circle',
    vertex-width => 0.55,
    :0vertex-font-size,
    vertex-color => 'Red',
    vertex-stroke-width => 2,
    edge-thickness => 8,
    edge-color => 'Purple',
    graph-size => 10
):svg


References

Articles, blog posts

[KJR1] KJ Runia, “The Collatz Conjecture”, (2020), OpenCurve.info.

[PZ1] Parker Ziegler, “Playing with the Collatz Conjecture”, (2021), ObservableHQ.

[Wk1] Wikipedia entry, “Collatz conjecture”.

[WMW1] Wolfram Math World entry, “Collatz Problem”.

Packages

[AAp1] Anton Antonov, Graph Raku package, (2024-2025), GitHub/antononcube.

[AAp2] Anton Antonov, JavaScript::D3 Raku package, (2022-2025), GitHub/antononcube.

[AAp3] Anton Antonov, Math::NumberTheory Raku package, (2025), GitHub/antononcube.

Videos

[Vv1] Veritasium, “The Simplest Math Problem No One Can Solve – Collatz Conjecture”, (2021), YouTube@Veritasium.

Rock-Paper-Scissors extensions

Introduction

It is easy to make a simple Rock-Paper-Scissors (RPS) game graph using the Raku package “Graph”, [AAp1]. Here is such a graph in which the arrow directions indicate which item (vertex) wins:

#%html
my $g0 = Graph.new(<🪨 ✂️ ✂️ 📄 📄 🪨>.Hash):d;
$g0.dot(:3graph-size, engine => 'neato'):svg

Easy, but now we want to:

In this post (notebook) we show how to do all of the above points.

Remark: Interesting analogies of the presented graphs can be made with warfare graphs, [AAv1]. For example, the graph tanks-infantry-guerillas is analogous to RPS.

TL;DR

  • LLMs “know” the RPS game and its upgrades.
  • LLMs know how to (mostly, reliably) translate to emojis.
  • The package “Graph” (via Graphviz DOT) can produce SVG plots that are readily rendered in different environments.
    • And the graphs of hand-games like RPS look good.
  • The class Graph have handy methods and attributes that make the creation and modification of graphs smooth(er).

Setup

This notebook is a Raku-chatbook, hence, its Jupyter session preloads certain packages and LLM-personas.

# Preloaded in any chatbook
# use LLM::Functions;
# use LLM::Prompts;

# Preloaded in a user init file
# use Graph;

# For this concrete session
use Text::Emoji;

LLM configurations:

my $conf4o = llm-configuration('chat-gpt', model => 'gpt-4o', :4096max-tokens, temperature => 0.4);
my $conf4o-mini = llm-configuration('chat-gpt', model => 'gpt-4o-mini', :4096max-tokens, temperature => 0.4);

($conf4o, $conf4o-mini)».Hash».elems

Default options of Graph.dot:

my $background = '#1F1F1F';
my $engine = 'neato';

my %opts =
    :$background,     
    :6graph-size, 
    :1edge-width,
    :3edge-font-size,
    edge-color => 'LightSlateGray',
    node-width => 0.2, node-height => 0.2, 
    node-shape => 'circle', 
    :node-labels, 
    :8node-font-size,
    node-fill-color => '#1F1F1F',
    node-color => 'LightSlateGray',
    node-stroke-width => 0.6,
    arrow-size => 0.25,
    :$engine;

my %opts-plain = merge-hash(%opts, {:5node-font-size, node-shape => 'ellipse', node-width => 0.27, node-height => 0.15});

(%opts, %opts-plain)».elems

Additional

sub game-table(Graph:D $g, Str:D :$link-value = '+', Str:D :$missing-value = '-') {
    cross-tabulate($g.edges(:dataset), <from>, <to>)
    ==> -> %h { %h.map({ $_.key => ($g.vertex-list Z=> $_.value{$g.vertex-list}).Hash }).Hash }()
    ==> to-dataset(:$missing-value)
    ==> -> %h { for $g.vertex-list { %h{$_}{$_} = ''}; %h }()
    ==> -> %h { $g.vertex-list.map({ [|%h{$_}, "" => $_].Hash }) }()
    ==> to-html(field-names => ["", |$g.vertex-list])
    ==> { .Str.subst('1', $link-value, :g).subst('(Any)', $missing-value, :g) }()
}


LLM request

Raku-chatbooks, [AAp4], can have initialization Raku code and specified preloaded LLM-personas. One such LLM-persona is “raku”. Here we use the “raku” chat object to get Raku code for the edges of the RPS extension Rock-Paper-Scissors-Lizard-Spock, [Wv1].

#% chat raku
Make an array the edges of a graph for the game Rock-Paper-Scissors-Lizard-Spock.
Each edges is represented with a hash with the keys "from", "to", "label".
The label corresponds to the action taken with the edge, like, "Paper covers Rock", "Paper disproves Spock".

my @edges = (
    { from => 'Rock',     to => 'Scissors', label => 'Rock crushes Scissors' },
    { from => 'Rock',     to => 'Lizard',   label => 'Rock crushes Lizard' },
    { from => 'Paper',    to => 'Rock',     label => 'Paper covers Rock' },
    { from => 'Paper',    to => 'Spock',    label => 'Paper disproves Spock' },
    { from => 'Scissors', to => 'Paper',    label => 'Scissors cuts Paper' },
    { from => 'Scissors', to => 'Lizard',   label => 'Scissors decapitates Lizard' },
    { from => 'Lizard',   to => 'Spock',    label => 'Lizard poisons Spock' },
    { from => 'Lizard',   to => 'Paper',    label => 'Lizard eats Paper' },
    { from => 'Spock',    to => 'Scissors', label => 'Spock smashes Scissors' },
    { from => 'Spock',    to => 'Rock',     label => 'Spock vaporizes Rock' },
);

We use the generated code in the next section.


Plain text graph

Here we create the Rock-Paper-Scissors-Lizard-Spock graph generated with the LLM-magic cell above:

my @edges =
    { from => 'Rock',     to => 'Scissors',  label => 'Rock crushes Scissors' },
    { from => 'Scissors', to => 'Paper',     label => 'Scissors cuts Paper' },
    { from => 'Paper',    to => 'Rock',      label => 'Paper covers Rock' },
    { from => 'Rock',     to => 'Lizard',    label => 'Rock crushes Lizard' },
    { from => 'Lizard',   to => 'Spock',     label => 'Lizard poisons Spock' },
    { from => 'Spock',    to => 'Scissors',  label => 'Spock smashes Scissors' },
    { from => 'Scissors', to => 'Lizard',    label => 'Scissors decapitates Lizard' },
    { from => 'Lizard',   to => 'Paper',     label => 'Lizard eats Paper' },
    { from => 'Paper',    to => 'Spock',     label => 'Paper disproves Spock' },
    { from => 'Spock',    to => 'Rock',      label => 'Spock vaporizes Rock' }
;

my $g = Graph.new(@edges, :directed);

# Graph(vertexes => 5, edges => 10, directed => True)

Here we make the edge labels:

my %edge-labels;
@edges.map({ %edge-labels{$_<from>}{$_<to>} = $_<label>.words[1] });

deduce-type(%edge-labels)

# Assoc(Atom((Str)), Assoc(Atom((Str)), Atom((Str)), 2), 5)

Here we plot the graph:

#% html
$g.dot(|%opts-plain, :%edge-labels):svg

Remark: Currently the class Graph does not “deal” with edge labels, but some of its methods (like, dot) do.


Convenient LLM functions

Graph edges

Instead of using chat-cells, we can define an LLM function that provides the graph edges dataset for different RPS variants. Here is such an LLM function using “LLM::Functions”, [AAp1], and “LLM::Prompts”, [AAv2]:

my sub rps-edge-dataset($description, Str:D $game-name = 'Rock-Paper-Scissors', *%args) {
    llm-synthesize([
        "Give the edges the graph for this $game-name variant description",
        'Give the edges as an array of dictionaries. Each dictionary with keys "from", "to", "label",',
        'where "label" has the action of "from" over "to".',
        $description,
        llm-prompt('NothingElse')('JSON')
        ], 
        e => %args<llm-evaluator> // %args<e> // %args<conf> // $conf4o-mini,
        form => sub-parser('JSON'):drop
    )
}

Remark:: Both “LLM::Functions” and “LLM::Prompts” are pre-loaded in Raku chatbooks.

Emoji translations

We can translate to emojis the plain-text vertex labels of RPS graphs in several ways:

  1. Manually
  2. Using to-emoji of “Text::Emoji”, [EMp1]
  3. Via LLMs

Here we take option 2:

my %additional = spock => to-emoji(':vulcan-salute:'), paper => to-emoji(":page-with-curl:");
say (:%additional);
@edges.map(*<from>).map({ $_ => to-emoji(":$_:", %additional) })

# additional => {paper => 📃, spock => 🖖}
# (Rock => 🪨 Scissors => ✂️ Paper => 📃 Rock => 🪨 Lizard => 🦎 Spock => 🖖 Scissors => ✂️ Lizard => 🦎 Paper => 📃 Spock => 🖖)

Again, let us define an LLM function for that does emojification. (I.e. for option 3.)

One way is to do a simple application of the prompt “Emojify” and process its result into a dictionary:

my $res = llm-synthesize( llm-prompt("Emojify")($g.vertex-list), e => $conf4o-mini  );
$res.split(/\s+/, :skip-empty)».trim.Hash

It is better to have a function that provides a more “immediate” result:

my sub emoji-rules($words, *%args) {
    llm-synthesize( [
        llm-prompt("Emojify")($words), 
        'Make a JSON dictionary of the original words as keys and the emojis as values', 
        llm-prompt('NothingElse')('JSON') 
        ], 
        e => %args<llm-evaluator> // %args<e> // %args<conf> // $conf4o-mini,
        form => sub-parser('JSON'):drop
    )
}


Emoji graph

Let us remake the game graph using suitable emojis. Here are the corresponding egdes:

my @edges-emo =
    { from => '🪨', to => '✂️',   label => 'crushes' },
    { from => '✂️',  to => '📄',  label => 'cuts' },
    { from => '📄', to => '🪨',  label => 'covers' },
    { from => '🪨', to => '🦎',  label => 'crushes' },
    { from => '🦎', to => '🖖',  label => 'poisons' },
    { from => '🖖', to => '✂️',   label => 'smashes' },
    { from => '✂️',  to => '🦎',  label => 'decapitates' },
    { from => '🦎', to => '📄',  label => 'eats' },
    { from => '📄', to => '🖖',  label => 'disproves' },
    { from => '🖖', to => '🪨',  label => 'vaporizes' }
;

my $g-emo = Graph.new(@edges-emo, :directed);

# Graph(vertexes => 5, edges => 10, directed => True)

Here is a table of the upgraded game that shows the interaction between the different roles (hand plays):

#% html
game-table($g-emo)

✂️📄🖖🦎🪨
✂️++
📄++
🖖++
🦎++
🪨++

Here we make the edge labels:

my %edge-labels-emo;
@edges-emo.map({ %edge-labels-emo{$_<from>}{$_<to>} = $_<label> });

deduce-type(%edge-labels-emo)

# Assoc(Atom((Str)), Assoc(Atom((Str)), Atom((Str)), 2), 5)

Here we plot the graph (using a variety of setup options):

#% html
$g-emo.dot(|%opts, edge-labels => %edge-labels-emo):svg


Chuck Norris defeats them all!

Consider the image (from www.merchandisingplaza.us):

Let us try to remake it with a graph plot. At this point we simply add a “foot connection” to all five vertices in the graph(s) above:

my $chuck = "🦶🏻";
my $g-chuck = $g.clone.edge-add( ($chuck X=> $g.vertex-list).Array, :directed);

# Graph(vertexes => 6, edges => 15, directed => True)

But we also have to rename the vertices to be hand-gestures:

$g-chuck .= vertex-replace( { Scissors => '✌🏻', Rock => '✊🏻', Lizard => '🤏🏻', Spock => '🖖🏻', 'Paper' => '✋🏻' } )

# Graph(vertexes => 6, edges => 15, directed => True)

Here is the interactions table of the upgraded game:

#% html
game-table($g-chuck)

✊🏻✋🏻✌🏻🖖🏻🤏🏻🦶🏻
✊🏻++
✋🏻++
✌🏻++
🖖🏻++
🤏🏻++
🦶🏻+++++

In order to ensure that we get an “expected” graph plot, we can take the vertex coordinates of a wheel graph or compute them by hand. Here we do the latter:

my @vs = <✊🏻 🖖🏻 🤏🏻 ✌🏻 ✋🏻>;
my %vertex-coordinates = @vs.kv.map( -> $i, $v { $v => [cos(π/2 + $i * 2 * π / 5), sin(π/2 + $i * 2 * π / 5)] });
%vertex-coordinates<🦶🏻> = [0, 0];
$g-chuck.vertex-coordinates = %vertex-coordinates;

deduce-type(%vertex-coordinates)

# Struct([✊🏻, ✋🏻, ✌🏻, 🖖🏻, 🤏🏻, 🦶🏻], [Array, Array, Array, Array, Array, Array])

Here we plot the graph:

#% html
$g-chuck.dot(
    background => '#5f5b4f',
    graph-label => 'Chuck Norris Defeats All'.uc,
    font-color => '#b8aa79',
    :6graph-size, 
    :2edge-width,
    :4edge-font-size,
    edge-color => 'AntiqueWhite',
    node-width => 0.56, node-height => 0.56, 
    node-shape => 'circle', 
    :node-labels, 
    :38node-font-size,
    node-fill-color => '#b8aa79',
    node-color => 'Gray',
    node-stroke-width => 0.6,
    arrow-size => 0.26,
    engine => 'neato',
    :svg
)


Using LLMs

Matching the colors

We can use “LLM vision” to get the colors of the original image:

my $url = 'https://www.merchandisingplaza.us/40488/2/T-shirts-Chuck-Norris-Chuck-Norris-Rock-Paper-Scissors-Lizard-Spock-TShirt-l.jpg';
llm-vision-synthesize('What are the dominant colors in this image? Give them in hex code.', $url)

The dominant colors in the image are:

- Olive Green: #5B5D4A
- Beige: #D0C28A
- White: #FFFFFF
- Black: #000000

Graph generating with LLMs

Instead of specifying the graph edges by hand, we can use LLM-vision and suitable prompting. The results are not that good, but YMMV.

my $res2 =
llm-vision-synthesize([
    'Give the edges the graph for this image of Rock-Paper-Scissors-Lizard-Spock-Chuck -- use relevant emojis.',
    'Give the edges as an array of dictionaries. Each dictionary with keys "from" and "to".',
    llm-prompt('NothingElse')('JSON')
    ], 
    $url,
    e => $conf4o,
    form => sub-parser('JSON'):drop
    )

# [{from => ✋, to => ✌️} {from => ✌️, to => ✊} {from => ✊, to => 🦎} {from => 🦎, to => 🖖} {from => 🖖, to => ✋} {from => ✋, to => ✊} {from => ✊, to => ✋} {from => ✌️, to => 🦎} {from => 🦎, to => ✋} {from => 🖖, to => ✌️} {from => ✌️, to => 🖖} {from => 🖖, to => ✊}]

#% html
Graph.new($res2, :directed).dot(:5graph-size, engine => 'neato', arrow-size => 0.5):svg


Rock-Paper-Scissors-Fire-Water

One notable variant is Rock-Paper-Scissors-Fire-Water. Here is its game table:

#% html
my @edges = |('🔥' X=> $g0.vertex-list), |($g0.vertex-list X=> '💦'), '💦' => '🔥';
my $g-fire-water = $g0.clone.edge-add(@edges, :directed);

game-table($g-fire-water)

✂️💦📄🔥🪨
✂️++
💦+
📄++
🔥+++
🪨++

Here is the graph:

#% html
$g-fire-water.dot(|%opts, engine => 'neato'):svg


Complete RPS upgrade via LLMs

Consider the game RPS-9:

my $txt = data-import('https://www.umop.com/rps9.htm', 'plaintext');
text-stats($txt)

# (chars => 2143 words => 355 lines => 46)

Extract the game description:

my ($start, $end) = 'relationships in RPS-9:', 'Each gesture beats out';
my $txt-rps9 = $txt.substr( $txt.index($start) + $start.chars .. $txt.index($end) - 1 ) 

ROCK POUNDS OUT
FIRE, CRUSHES SCISSORS, HUMAN &
SPONGE.
FIRE MELTS SCISSORS, 
BURNS PAPER, HUMAN & SPONGE.
SCISSORS SWISH THROUGH AIR,
CUT PAPER, HUMAN & SPONGE.
HUMAN CLEANS WITH SPONGE,
WRITES PAPER, BREATHES
AIR, DRINKS WATER.
SPONGE SOAKS PAPER, USES
AIR POCKETS, ABSORBS WATER,
CLEANS GUN.
PAPER FANS AIR,
COVERS ROCK, FLOATS ON WATER,
OUTLAWS GUN.
AIR BLOWS OUT FIRE,
ERODES ROCK, EVAPORATES WATER,
TARNISHES GUN.
WATER ERODES ROCK, PUTS OUT
FIRE, RUSTS SCISSORS & GUN.
GUN TARGETS ROCK,
FIRES, OUTCLASSES SCISSORS, SHOOTS HUMAN.

Here we invoke the defined LLM function to get the edges of the corresponding graph:

my @rps-edges = |rps-edge-dataset($txt-rps9)

# [{from => ROCK, label => POUNDS OUT, to => FIRE} {from => ROCK, label => CRUSHES, to => SCISSORS} {from => ROCK, label => CRUSHES, to => HUMAN}, ..., {from => GUN, label => FIRES, to => FIRE}]

Here we translate the plaintext vertices into emojis:

my %emojied = emoji-rules(@rps-edges.map(*<from to>).flat.unique.sort)

{AIR => 🌬️, FIRE => 🔥, GUN => 🔫, HUMAN => 👤, PAPER => 📄, ROCK => 🪨, SCISSORS => ✂️, SPONGE => 🧽, WATER => 💧}

Here is the graph plot:

#% html
my $g-rps9 = Graph.new(@rps-edges, :directed).vertex-replace(%emojied);
$g-rps9.vertex-coordinates = $g-rps9.vertex-list Z=> Graph::Cycle(9).vertex-coordinates.values;

my %edge-labels = Empty;
$res3.map({ %edge-labels{%emojied{$_<from>}}{%emojied{$_<to>}} = "\"$_<label>\"" });

my %opts2 = %opts , %(:14node-font-size, node-shape => 'circle', node-width => 0.3, edge-width => 0.4);
$g-rps9.dot(|%opts2, :!edge-labels, engine => 'neato', :svg)

Here is the game table:

#% html
game-table($g-rps9)

✂️🌬️👤💧📄🔥🔫🧽🪨
✂️+++
🌬️++++
👤++++
💧++++
📄++++
🔥++++
🔫+++
🧽++++
🪨++++

Future plans

In the (very near) future I plan to use the built-up RPS graph making know-how to make military forces interaction graphs. (Discussed in [AJ1, SM1, NM1, AAv1].)


References

Articles, books, theses

[AJ1] Archer Jones, “The Art of War in Western World”, (2000), University of Illinois Press. 768 pages, ISBN-10: 0252069668, ISBN-13: 978-0252069666.

[SM1] Sergei Makarenko et al., “Обобщенная модель Ланчестера, формализующая конфликт нескольких сторон”, [Eng. “The General Lanchester Model Defining Multilateral Conflicts”], (2021), Automation of Control Processes № 2 (64), doi: 10.35752/1991-2927-2021-2-64-66-76.

[NM1] Николай В. Митюков, “Математические модели и программные средства для реконструкции военно-исторических данных”, (2009), disserCat.

Packages

[AAp1] Anton Antonov, Graph Raku package, (2024-2025), GitHub/antononcube.

[AAp2] Anton Antonov, LLM::Functions Raku package, (2023-2024), GitHub/antononcube.

[AAp3] Anton Antonov, LLM::Prompts Raku package, (2023-2024), GitHub/antononcube.

[AAp4] Anton Antonov, Jupyter::Chatbook Raku package, (2023-2024), GitHub/antononcube.

[EMp1] Elizabeth Mattijsen, Text::Emoji Raku package, (2024-2025), GitHub/lizmat.

Videos

[AAv1] Anton Antonov, “Upgrading Epidemiological Models into War Models”, (2024), YouTube/@WolframResearch.

[Wv1] Wozamil, “Rock Paper Scissors Lizard Spock (Extended Cut) ~ The Big Bang Theory ~”, (2012), YouTube@Wozamil.

Sparse Matrix Neat Examples in Raku

Introduction

Sparse matrices are an essential tool in computational mathematics, allowing us to efficiently represent and manipulate large matrices that are predominantly composed of zero elements. In this blog post, we will delve into a few intriguing examples of sparse matrix utilization, specifically in the Raku programming language.

Examples Covered:

  1. Random Graph:
    • We will explore the adjacency matrix of a random graph generated from a model of social interactions.
    • Additionally, we will overlay adjacency matrices with a shortest path within the graph.
  2. Movie-Actor Bipartite Graph:
    • This example involves ingesting data about relationships between actors and movies.
    • We will demonstrate how sparse matrix algebra can facilitate certain information retrieval tasks.
  3. Sparse Matrices Visualization:
    • We will discuss techniques for visualizing sparse matrices.

Support for sparse matrix linear algebra is a hallmark of a mature computational system. Here’s a brief timeline of when some popular systems introduced sparse matrices:

LanguageInitial IntroductionConfirmed Update
MATLAB1992~
Mathematica / Wolfram Language2003updated 2007
Pythonmaybe since 2004updated 2006
Rmaybe since 2011updated 2014

Setup

(This setup is similar to the one used in the Graph neat examples.)


Random Graph Matrix

Let’s begin by examining a random graph generated using the Watts-Strogatz model. This model is particularly useful for simulating social networks.

#% js
my $gl = Graph::Random.new: Graph::Distribution::WattsStrogatz.new(20,0.06);

my $gp = Graph::Path.new: $gl.find-shortest-path('0','12'), :directed;

my $grPlot = 
js-d3-graph-plot(
    $gl.edges(:dataset),
    highlight => [|$gp.vertex-list, |$gp.edge-list],
    background => '1F1F1F', 
    title-color => 'Silver', 
    edge-thickness => 3,
    vertex-size => 6,
    width => 600,
    force => {charge => {strength => -260, iterations => 2}, y => {strength => 0.2}, collision => {radius => 6, iterations => 10}, link => {distance => 4}}
    )

In the code above, we create a random graph with 20 vertices and a connection probability of 0.06. We also find the shortest path between vertices ‘0’ and ’12’.

Corresponding Matrix

The adjacency matrix of this graph is a sparse matrix, where non-zero elements indicate the presence of an edge between vertices.

#% js
my $m = Math::SparseMatrix.new(edge-dataset => $gl.edges(:dataset), row-names => $gl.vertex-list.sort(*.Int));
say $m;
$m.Array ==> js-d3-matrix-plot(width => 400, margins => 15, :$tick-labels-font-size)

# Math::SparseMatrix(:specified-elements(86), :dimensions((20, 20)), :density(0.215))

Here, we visualize the graph matrix, the shortest path matrix, and the sum of these matrices:

#% js
my $m2 = Math::SparseMatrix.new(edge-dataset => $gp.edges(:dataset), row-names => $m.row-names);

my $m3 = $m.add($m2.multiply(0.75));

# Visualize
 my %opts = width => 350, margins => {top => 30, left => 16, right => 16, bottom => 16}, :$tick-labels-font-size, :$tick-labels-color, :$title-color, :!tooltip, color-palette => 'Inferno';
 [
   js-d3-matrix-plot($m.Array, |%opts, title => 'Graph'),
   js-d3-matrix-plot($m2.Array, |%opts, title => 'Shortest path graph'),
   js-d3-matrix-plot($m3.Array, |%opts, title => 'Sum')
 ].join("\n")

The sum matrix is printed in a “plain” format:

$m3.print

By comparing the graph and the sum matrix side-by-side, we can better understand the structure and relationships within the graph:

#% js
[
    $grPlot,
    js-d3-matrix-plot($m3.Array, margins => 16, :$tick-labels-font-size, :$tick-labels-color, width => 400, color-palette => 'Inferno')
].join("\n")


Ingest Movie-Actor Data

Next, we will ingest a CSV file containing data about movies and actors. This data will be used to create a bipartite graph.

my $file = $*CWD ~ '/Sparse-matrices/dsMovieRecords.csv';
my @dsMovieRecords = data-import($file, 'csv', headers => 'auto');

deduce-type(@dsMovieRecords)

# Vector(Assoc(Atom((Str)), Atom((Str)), 6), 40)

Movie Data Table

Here is a tabular representation of the movie data:

#% html
my @field-names = <Movie Actor Genre1 Genre2 Genre3 BoxOffice>;
@dsMovieRecords ==> to-html(:@field-names)

A summary of the data:

#% html
records-summary(@dsMovieRecords, :8max-tallies, :!say) 
==> to-html(:@field-names)


Bipartite Graph

We construct a bipartite graph based on the movie-actor relationships.

my @rules = @dsMovieRecords.map({ $_<Movie> => $_<Actor> });
my $g = Graph.new(@rules) 

# Graph(vertexes => 27, edges => 40, directed => False)

The graph is confirmed to be bipartite:

$g.is-bipartite

# True

Here is the coloring of the graph:

.say for $g.bipartite-coloring.classify(*.value)

# 1 => [X2 => 1 The Lord of the Rings: The Fellowship of the Ring => 1 Pirates of the Caribbean: The Curse of the Black Pearl => 1 The Lord of the Rings: The Return of the King => 1 Pirates of the Caribbean: At World's End => 1 X-Men: The Last Stand => 1 The Lord of the Rings: The Two Towers => 1 Pirates of the Caribbean: Dead Man's Chest => 1]
# 0 => [Sean Astin => 0 Patrick Stewart => 0 Elijah Wood => 0 Rebecca Romijn => 0 Ian McKellen => 0 Keira Knightley => 0 Orlando Bloom => 0 Famke Janssen => 0 Bill Nighy => 0 Johnny Depp => 0 Jack Davenport => 0 Hugh Jackman => 0 Liv Tyler => 0 Halle Berry => 0 Andy Serkis => 0 Geoffrey Rush => 0 Stellan Skarsgård => 0 Anna Paquin => 0 Viggo Mortensen => 0]


#% js

$g.edges(:dataset) 
==> js-d3-graph-plot(
        highlight => @dsMovieRecords.map(*<Actor>).List,
        :$background, 
        title-color => 'silver',  
        width => 1000, 
        :$edge-thickness,
        :$vertex-size,
        vertex-color => 'Red',
        vertex-label-font-size => 12,
        vertex-label-color => 'Grey',
        vertex-label-font-family => 'Helvetica',
        :!directed,
        force => {charge => {strength => -680, iterations => 2}, collision => {radius => 10, iterations => 1}, link => {minDistance => 10}}
    )


Sparse Matrix

We create a sparse matrix representing the movie-actor relationships:

my @allVertexNames = [|@dsMovieRecords.map(*<Movie>).unique.sort, |@dsMovieRecords.map(*<Actor>).unique.sort];
my %h = @allVertexNames Z=> ^@allVertexNames.elems;

# {Andy Serkis => 8, Anna Paquin => 9, Bill Nighy => 10, Elijah Wood => 11, Famke Janssen => 12, Geoffrey Rush => 13, Halle Berry => 14, Hugh Jackman => 15, Ian McKellen => 16, Jack Davenport => 17, Johnny Depp => 18, Keira Knightley => 19, Liv Tyler => 20, Orlando Bloom => 21, Patrick Stewart => 22, Pirates of the Caribbean: At World's End => 0, Pirates of the Caribbean: Dead Man's Chest => 1, Pirates of the Caribbean: The Curse of the Black Pearl => 2, Rebecca Romijn => 23, Sean Astin => 24, Stellan Skarsgård => 25, The Lord of the Rings: The Fellowship of the Ring => 3, The Lord of the Rings: The Return of the King => 4, The Lord of the Rings: The Two Towers => 5, Viggo Mortensen => 26, X-Men: The Last Stand => 6, X2 => 7}

The row and column names are sorted, with movie titles first, followed by actor names:

.say for @allVertexNames

# Pirates of the Caribbean: At World's End
# Pirates of the Caribbean: Dead Man's Chest
# Pirates of the Caribbean: The Curse of the Black Pearl
# The Lord of the Rings: The Fellowship of the Ring
# The Lord of the Rings: The Return of the King
# The Lord of the Rings: The Two Towers
# X-Men: The Last Stand
# X2
# Andy Serkis
# Anna Paquin
# Bill Nighy
# Elijah Wood
# Famke Janssen
# Geoffrey Rush
# Halle Berry
# Hugh Jackman
# Ian McKellen
# Jack Davenport
# Johnny Depp
# Keira Knightley
# Liv Tyler
# Orlando Bloom
# Patrick Stewart
# Rebecca Romijn
# Sean Astin
# Stellan Skarsgård
# Viggo Mortensen

The sparse matrix of the bipartite graph is constructed:

my $m = Math::SparseMatrix.new(edge-dataset => $g.edges(:dataset))

# Math::SparseMatrix(:specified-elements(80), :dimensions((27, 27)), :density(<80/729>))

#%js
$m.Array ==> js-d3-matrix-plot(width=>400)

To clearly show the bipartite nature of the matrix, we restructure it using pre-arranged row and column names:

$m = $m[@allVertexNames; @allVertexNames]

# Math::SparseMatrix(:specified-elements(80), :dimensions((27, 27)), :density(<80/729>))

The matrix plot now clearly indicates a bipartite graph:

#%js
$m.Array ==> js-d3-matrix-plot(width=>400)

For an alternative visualization, we can create an HTML “pretty print” of the sparse matrix:

#% html

$m
.to-html(:v)
.subst('<td>1</td>', '<td><b>●</b></td>', :g)


Fundamental Information Retrieval Operation

Sparse matrices are particularly useful for information retrieval operations. Here, we demonstrate how to retrieve data about an actor, such as Orlando Bloom.

  • Retrieve the row/vector corresponding to the actor and transpose it:
#%html
my $m-actor = $m['Orlando Bloom'].transpose;
$m-actor.to-html.subst('<td>0</td>','<td> </td>'):g

  • Multiply the incidence matrix with the actor-vector to find other actors who starred in the same movies:
#% html
$m.dot($m-actor).to-html


Matrix Plot (Details)

There are two primary methods for plotting sparse matrices.

Via Tuples

This method uses a heatmap plot specification:

#% js
my @ds3D = $m.tuples.map({ <x y z tooltip>.Array Z=> [|$_.Array, "⎡{$m.row-names[$_[0]]}⎦ : ⎡{$m.column-names[$_[1]]}⎦ : {$_.tail}"] })».Hash;
js-d3-matrix-plot(
    @ds3D, 
    :$tooltip-background-color, 
    :$tooltip-color, 
    :$background, 
    width => 400)

Here is the corresponding (“coordinates”) list plot:

#%js
$m.tuples
==> js-d3-list-plot(:$background, width => 400, :!grid-lines)

As Dense Matrix

This method visualizes the matrix as a dense array:

#%js
$m.Array
==> js-d3-matrix-plot(width => 400)

Larger Sparse Matrix

For larger matrices, a list plot might be more useful, especially if the matrix has a relatively high density.

my $gLarge = Graph::Random.new: Graph::Distribution::WattsStrogatz.new(100,0.1);
my $mLarge = Math::SparseMatrix.new(edge-dataset => $gLarge.edges(:dataset));

# Math::SparseMatrix(:specified-elements(444), :dimensions((100, 100)), :density(0.0444))

The corresponding graph:

#% js
$mLarge.tuples
==> js-d3-list-plot( :$background, width => 600, height => 600, :!grid-lines)

Remark: The list plot might be much more useful for large matrices with (relatively) high density.

Tuples dataset:

#%js
$mLarge.tuples(:dataset)
==> {rename-columns($_, (<i j x> Z=> <x y z>).Hash)}()
==> js-d3-matrix-plot(:$background, width => 600)

Random Dense Matrix

Lastly, we explore a dense matrix example:

#%js
my @a = random-real(10, 48) xx 12;
@a = rand > 0.5 ?? @a.map(*.sort) !! @a.&transpose.map(*.sort.Array).&transpose;
say "dimensions : ", dimensions(@a);
js-d3-matrix-plot(@a, width => 1600, margins => 1, :$tick-labels-font-size, color-palette => <Turbo Plasma Warm Inferno Oranges>.pick, :$background)


Reference

Articles

[AA1] Anton Antonov, “RSparseMatrix for sparse matrices with named rows and columns”, (2015), MathematicaForPrediction at WordPress.

Packages

[AAp1] Anton Antonov, Math::SparseMatrix Raku package, (2024), GitHub/antononcube.

[AAp2] Anton Antonov, Math::SparseMatrix::Native Raku package, (2024), GitHub/antononcube.

[AAp3] Anton Antonov, Graph Raku package, (2024), GitHub/antononcube.

[AAp4] Anton Antonov, JavaScript::D3 Raku package, (2022-2024), GitHub/antononcube.

Geographic Data in Raku Demo

Last weekend I made a demo presentation showcasing the capabilities of the Raku packages:

This post encapsulates the essence of that presentation, offering a walk-through of how these packages can be leveraged to create good, informative geographic visualizations.

Here is the video recording of the presentation, [AAv4]:


Main Packages

The primary focus of our exploration is on two Raku packages:

  1. Data::Geographics: This package provides comprehensive country and city data, which is crucial for geographic data visualization and analysis.
  2. JavaScript::Google::Charts: This package interfaces with Google Charts, an established framework for creating various types of charts, including geographic plots.

Geographic Data Visualization

Data::Geographics: The Protagonist

The “Data::Geographics” package is the star of the presentation. It provides extensive data on countries and cities, which is essential for geographic data visualization and analysis. Initially, I attempted to create geographic plots using JavaScript freehand, but it proved challenging. Instead, I found it more practical to use the “JavaScript::Google::Charts” package, which offers a more structured framework for creating pre-defined chart types.

Creating Geographic Plots

Using the “JavaScript::Google::Charts” package, I demonstrated how to generate geographic plots. For instance, we visualized country data with a simple plot highlighting countries known to the “Data::Geographics” package in shades of green, while unknown regions were depicted in gray. (That is presentation’s “opening image.”)

Notably, Google Charts geo plots get be generated with suitable Large Language Model prompts and directly displayed in Raku chatbooks.

Data Analysis with Data::Geographics

Beyond simple visualization, certain analytical tasks can be done using the country data in “Data::Geographics”. For example, I conducted a rudimentary analysis of gross domestic product (GDP) and electricity production using linear regression.

The package also includes city data, enabling us to perform proximity searches and create neighbor graphs.


Practical Demonstrations

Country Data

Currently, “Data::Geographics” knows about 29 countries (≈195 data elements for each.) Here are the countries:

#% html
use Data::Geographics;
country-data().keys.sort ==> to-html(:multicolumn, columns => 3)
BotswanaHungarySerbia
BrazilIranSlovakia
BulgariaIraqSouthAfrica
CanadaJapanSouthKorea
ChinaMexicoSpain
CzechRepublicNorthKoreaSweden
DenmarkPolandTurkey
FinlandRomaniaUkraine
FranceRussiaUnitedStates
GermanySaudiArabia(Any)

Name Recognition

The package “DSL::Entity::Geographics” was specially made to recognize city and country names, which is particularly useful for conversational agents.

Here is named entity recognition example:

use DSL::Enitity::Geographics;
entity-city-and-state-name('Las Vegas, Nevada', 'Raku::System')

# United_States.Nevada.Las_Vegas

Correlation Plots

We created correlation plots to analyze the relationship between GDP and electricity production. Using Google Charts’ built-in functionality, we plotted regression lines to visualize trends. But Google Charts’ very nice “trend lines” functionality has certain limitations over logarithmic plots. Hence, that gave us the excuse to do linear regression with “Math::Fitting”:

City Data Tabulation and Visualization

City data visualization was another highlight. We filtered city data to display information such as population and location. By integrating Google Maps links, we provided an interactive way to explore city locations.

Tabulation

#% html
@dsCityData.pick(12)
==> { .sort(*<ID>) }()
==> to-html(field-names => <State City Population LocationLink>)
==> { $_.subst(:g, / <?after '<td>'> ('http' .*?) <before '</td>'> /, { "<a href=\"$0\">link</a>" }) }()
StateCityPopulationLocationLink
AlabamaMontgomery200603link
CaliforniaFresno542107link
MassachusettsWorcester206518link
NevadaLas Vegas641903link
TexasEl Paso678815link
VirginiaChesapeake249422link

City locations

Here are city locations plotted with “JavaScript::D3”:

Here are city locations plotted with “JavaScript::Google::Charts”:

Remark: In both plots above Las Vegas, Nevada and cities close to it are given focus.

Proximity Searches

Using the “Math::Nearest” package, we performed proximity searches to find the nearest neighbors of a given city. This feature is particularly useful for geographic analysis and planning.

Graph Visualization

For visualizing neighbor graphs, we used the packages “WWW::MermaidInk” and “JavaScript::D3”. The former interfaces with a web service to generate graph diagrams. The latter has its own built-in graph plotting functionalities. (Based on the force-directed graph plotting component of D3.js.)

Both approaches allow the creation of appealing visual representations of city connections.

Here is a Nearest Neighbor Graph plotted with “JavaScript::D3”:

Here is a Nearest Neighbor Graph plotted with “WWW::MermaidInk”:


Future Plans and Enhancements

While the current capabilities of “Data::Geographics” and “JavaScript::Google::Charts” are impressive, there is always room for improvement. Future plans include:

  • Enhancing the “Math::Fitting” package to support multidimensional regression.
  • Exploring the potential of “JavaScript::D3” for more flexible and advanced visualizations.

Conclusion

In summary, the combination of “Data::Geographics”, “JavaScript::Google::Charts” in Raku provides a powerful toolkit for geographic data visualization and analysis. “JavaScript::D3” is also very applicable exploratory data analysis. The function objects (functors) created by “Math::Nearest” and “Math::Fitting” make them very convenient to use.


References

Articles, blog posts

[AA1] Anton Antonov, “Age at creation for programming languages stats”, (2024), RakuForPrediction.

Packages

[AAp1] Anton Antonov, Data::Geographics Raku package, (2024), GitHub/antononcube.

[AAp2] Anton Antonov, Data::Reshapers Raku package, (2021-2024), GitHub/antononcube.

[AAp3] Anton Antonov, Data::Summarizers Raku package, (2021-2023), GitHub/antononcube.

[AAp4] Anton Antonov, Data::Translators Raku package, (2023-2024), GitHub/antononcube.

[AAp5] Anton Antonov, Data::TypeSystem Raku package, (2023-2024), GitHub/antononcube.

[AAp6] Anton Antonov, DSL::Entity::Geographics Raku package, (2021-2024), GitHub/antononcube.

[AAp7] Anton Antonov, Math::DistanceFunctions Raku package, (2024), GitHub/antononcube.

[AAp8] Anton Antonov, Math::Nearest Raku package, (2024), GitHub/antononcube.

[AAp9] Anton Antonov, JavaScript::D3 Raku package, (2022-2024), GitHub/antononcube.

[AAp10] Anton Antonov, JavaScript::Google::Charts Raku package, (2024), GitHub/antononcube.

Videos

[AAv1] Anton Antonov, “The Raku-ju hijack hack for D3.js”, (2022), YouTube/@AAA4prediction. (7 min.)

[AAv2] Anton Antonov, “Random mandalas generation (with D3.js via Raku)”, (2022), YouTube/@AAA4prediction. (2 min.)

[AAv3] Anton Antonov, “Exploratory Data Analysis with Raku”, (2024), YouTube/@AAA4prediction. (21 min.)

[AAv4] Anton Antonov, “Geographics data in Raku demo”, (2024), YouTube/@AAA4prediction. (37 min.)

Graph representation of grammars

Introduction

This computational Markdown document demonstrates the mapping of Extended Backus-Naur Grammars (EBNF) and Raku grammars into graphs.

The graphs are made with Mermaid-JS and Wolfram Language (WL) (aka Mathematica).

Remark: Mermaid-JS specs are automatically rendered in GitHub Markdown files, and have plug-in support in Integrated Development Environments (IDEs) like IntelliJ IDEA, VS Code, Emacs, etc.

Examples using Large Language Models (LLMs) are provided. (Via “WWW::OpenAI” and “WWW::PaLM”, [AAp4, AAp5].)

The document has the following structure:

  • Reflections and observations
    I.e. “conclusions first.” Those make the anecdotal examples below more scientific, not just conjecture-worthy anecdotes.
  • Packages and interactions
    A list the packages utilized and how they can be used in a coherent way.
  • Generating Mermaid diagrams for EBNFs
    Two simple and similar EBNF grammars with graphs for instructive comparison.
  • Generating graphs with LLMs
    Can LLMs replace Raku programming (of grammar-graphs making)?
  • Generating Mermaid diagrams for Raku grammars
    Same exercise but over Raku grammars, not some EBNFs…
  • LLMs grammars for sentence collections
    An interesting way to derive EBNFs is to “just ask.” Well, some questions might get really long (and their answers really expensive.)
  • Random LLMs grammars
    Why ask for EBNFs of sentence collections, if we can just say “Give me a random EBNF grammar (or five.)”
  • More complicated grammar
    A larger grammar example, in order to illustrate the usefulness — or uselessness — of grammar-graphs.

Reflections and observations

  • I consider grammar graph representation “neat” and “cool” for small enough grammars, but I am not sure how useful it is for large grammars.
    • Especially, large grammars with recursive dependencies between the production rules.
  • I made a fair amount of experiments with relatively small grammars, and fewer experiments with a few large grammars.
  • The ability to make the grammar graphs, of course, has at least didactic utility.
  • Another utility of the examples given below is to show coherent interaction between the packages:
  • This Markdown document is “executed” with the package “Text::CodeProcessing”, which allows generation of Markdown specs that are, say, automatically processed by Web browsers, IDEs, etc.
    • Like Mermaid-JS charts and graphs.
  • Visualizing grammars generated by Large Language Models (LLMs) — like, ChatGPT and PaLM — is both didactic and “neat.”
    • One of my primary motivations for making the packages “FunctionalParsers” and “EBNF::Grammar” was to be able to easily (automatically or semi-automatically) process grammars generated with LLMs.
    • It is not trivial to parse EBNF hallucinations by LLMs. (More details below.)
  • Generating Raku grammars with ChatGPT-3.5 or PaLM often produces “non-working” grammars. That is why I focused on EBNF grammars.
    • My assumption is that EBNF has been around for a longer period of time, hence, LLMs are “better trained for it.”
  • This Markdown document can be converted into a Mathematica notebook using “Markdown::Grammar”, [AAp6]. Mathematica notebooks in RakuMode, [AAp7], make much easier the experiments with diagram generation and LLM utilization. (And more fun!)

Packages and interactions

Here we load the packages used below:

use FunctionalParsers;
use FunctionalParsers::EBNF;
use EBNF::Grammar;
use Grammar::TokenProcessing;
use WWW::OpenAI;
use WWW::PaLM;
# (Any)

Here are flowcharts that summarize use cases, execution paths, and interaction between the packages:


Generating Mermaid diagrams for EBNFs

The function fp-ebnf-parse can produce Mermaid-JS diagrams corresponding to grammars with the target “MermaidJS::Graph”. Here is an example:

my $ebnfCode1 = q:to/END/;
<top> = <a> | <b> ;
<a> = 'a' , { 'A' } , [ '1' ];
<b> = 'b' , ( 'B' | '2' );
END

fp-ebnf-parse($ebnfCode1, target=>'MermaidJS::Graph', dir-spec => 'LR').head.tail

Here is a legend:

  • The non-terminals are shown with rectangles
  • The terminals are shown with round rectangles
  • The “conjunctions” are shown in disks
  • The order of parsing in sequences is indicated with integer labels
  • Pick-left and pick-right sequences use the labels “L” and “R” for the corresponding branches

Remark: The Markdown cell above has the parameters output-lang=mermaid, output-prompt=NONE which allow for direct diagram rendering of the obtained Mermaid code in various Markdown viewers (GitHub, IntelliJ, etc.)

Compare the following EBNF grammar and corresponding diagram with the ones above:

my $ebnfCode2 = q:to/END/;
<top> = <a> | <b> ;
<a> = 'a' <& { 'A' } , [ '1' ] ;
<b> = 'b' , 'B' | '2' ;
END

fp-ebnf-parse($ebnfCode2, target=>'MermaidJS::Graph', dir-spec => 'LR').head.tail


Generating graphs with LLMs

It is interesting to see do LLMs do better at producing (Mermaid-JS) graph representations.

More importantly, we want to answer the question:

Can we generate graph-specs (like, Mermaid-JS) without the need of programming the corresponding interpreters?

Here is a LLM request for a Mermaid-JS spec generation for one of the simple grammars above:

my $request = "Make a Mermaid JS diagram for the EBNF grammar:\n$ebnfCode1";
#my $mmdLLM = openai-completion($request, max-tokens => 600, format => 'values', temperature => 1.2);
my $mmdLLM = palm-generate-text($request, max-tokens => 600, format => 'values', temperature => 0.9);

# ```mermaid
# graph LR
# top[top]
# a[a] --> top
# b[b] --> top
# a --> "a"
# a --> "{"
# a --> "A"
# a --> "}"
# a --> "["
# a --> "1"
# a --> "]"
# b --> "b"
# b --> "("
# b --> "B"
# b --> ")"
# b --> "2"
# ```

Here is the corresponding graph:

$mmdLLM

Remark:: After multiple experiments I can say that the obtained Mermaid-JS code is either:

  • Simple, somewhat relevant, and wrong
  • Closer to correct after suitable manual editing

As for the question above — the answer is “No”. But the LLM answers provide (somewhat) good initial versions for manual (human) building of graph specifications.


Generating Mermaid diagrams for Raku grammars

In order to generate graphs for Raku grammars we use the following steps:

  1. Translate Raku-grammar code into EBNF code
  2. Translate EBNF code into graph code (Mermaid-JS or WL)

Consider a grammar for parsing proclaimed feeling toward different programming languages:

grammar LangLove {
    rule TOP  { <workflow-command> }
    rule workflow-command  { <who> 'really'? <love> <lang> }
    token who { 'I' | 'We' }
    token love { 'hate' | 'love' }
    token lang { 'Raku' | 'Perl' | 'Rust' | 'Go' | 'Python' | 'Ruby' }
}

# (LangLove)

Here is an example parsing:

LangLove.parse('I hate Perl')

# 「I hate Perl」
#  workflow-command => 「I hate Perl」
#   who => 「I」
#   love => 「hate」
#   lang => 「Perl」

First we derive the corresponding EBNF grammar:

my $ebnfLangLove = to-ebnf-grammar(LangLove)

# <TOP> = <workflow-command>  ;
# <lang> = 'Raku'  | 'Perl'  | 'Rust'  | 'Go'  | 'Python'  | 'Ruby'  ;
# <love> = 'hate'  | 'love'  ;
# <who> = 'I'  | 'We'  ;
# <workflow-command> = <who> , ['really'] , <love> , <lang>  ;

Here is the corresponding Mermaid-JS graph:

fp-grammar-graph($ebnfLangLove)


LLMs grammars for sentence collections

Here is an EBNF grammar generated with ChatGPT, [AAp4], over a list of chemical formulas:

#my @sentences = <BrI BrClH2Si CCl4 CH3I C2H5Br H2O H2O4S AgBr AgBrO AgBrO2 AgBrO3 AgBrO4 AgCL>;
my @sentences = <AgBr AgBrO AgBrO2 AgBrO3 AgBrO4 AgCL>;
my $request = "Generate EBNF grammar for the sentences:\n{@sentences.map({ $_.comb.join(' ')}).join("\n")}";
#my $ebnfLLM = openai-completion($request, max-tokens => 600, format => 'values');
my $ebnfLLM = palm-generate-text($request, max-tokens => 600, format => 'values');

# ```
# <sentence> ::= <element> <element>
# <element> ::= <letter> | <letter> <element>
# <letter> ::= A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z
# ```

Often LLM requests as the ones above return code as Markdown code cells, hence, we try to remove the code cell markings:

$ebnfLLM = $ebnfLLM.subst(/ ^ '`' ** 3 <-[\v]>* \n | '`' ** 3 \h* $ /,''):g;

# <sentence> ::= <element> <element>
# <element> ::= <letter> | <letter> <element>
# <letter> ::= A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z

fp-grammar-graph($ebnfLLM, style => Whatever)

Another way for “verify” a grammar is to generate random sentences with it:

.say for ebnf-random-sentence($ebnfLLM, 12, style => Whatever)

# O 
# A 
# G H  
# I 
# D N  
# I L 
# J L 
# P Y 
# J F  
# M I 
# K 
# P J

Remark: Random sentences can be also generated with the function fp-random-sentence provided by “FunctionalParsers”.

Remark: The function ebnf-random-sentence uses fp-random-sentence, but ebnf-random-sentence (parses and) standardizes the given EBNF grammar first, (then it gives the standardized grammar to fp-random-sentence.)

Remark: It is not trivial to parse EBNF hallucinations by LLMs. For the same EBNF-making request a given LLM can produce different EBNF grammars, each having “its own” EBNF style. Hence, both “FunctionalParsers” and “EBNF::Grammar” have parsers for different EBNF styles. With the spec style => Whatever parsing of all of the “anticipated” styles are attempted.


Random LLMs grammars

Why ask for EBNF graphs with sentence collections, if we can just say:

Give me a random EBNF grammar. (Or five.)

Remark: Note, the implications of testing the parsers in that way. We can try to produce extensive parser tests by multiple randomly obtained grammars from different LLMs. (Using different LLM parameters, like, temperatures, etc.)

Here is another example using a random (hopefully small) EBNF grammar:

my $request2 = "Give an example of simple EBNF grammar.";
#my $ebnfLLM2 = openai-completion($request2, max-tokens => 600, format => 'values', temperature => 1.2);
my $ebnfLLM2 = palm-generate-text($request2, max-tokens => 600, format => 'values', temperature => 0.9);
$ebnfLLM2 = $ebnfLLM2.subst(/ ^ '`' ** 3 <-[\v]>* \n | '`' ** 3 \h* $ /,''):g;

# <expression> ::= <term> { <addop> <term> }
# <term> ::= <factor> { <mulop> <factor> }
# <factor> ::= <integer> | <float> | <variable> | <parenthesis>
# <addop> ::= + | -
# <mulop> ::= * | /
# <parenthesis> ::= ( <expression> )

fp-grammar-graph($ebnfLLM2, style => Whatever, dir-spec => 'LR')


More complicated grammar

Consider the following grammar for arithmetic expressions:

my $ebnfExpr = q:to/END/;
start   = expr ;
expr    = term '+' expr | term '-' expr | term ;
term    = term '*' factor | term '/' factor | factor ;
factor  = '+' factor | '-' factor | (expr) | integer | integer '.' integer ;
integer = digit integer | digit ;
digit   = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ;
END

# start   = expr ;
# expr    = term '+' expr | term '-' expr | term ;
# term    = term '*' factor | term '/' factor | factor ;
# factor  = '+' factor | '-' factor | (expr) | integer | integer '.' integer ;
# integer = digit integer | digit ;
# digit   = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ;

Here we produce the graph using special parsing style:

fp-grammar-graph($ebnfExpr, style => 'Relaxed')

References

Articles

[Wk1] Wikipedia entry, “Extended Backus–Naur form”.

Packages, repositories

[AAp1] Anton Antonov, FunctionParsers Raku package, (2023), GitHub/antononcube.

[AAp2] Anton Antonov, EBNF:Grammar Raku package, (2023), GitHub/antononcube.

[AAp3] Anton Antonov, Grammar::TokenProcessing Raku package, (2022-2023), GitHub/antononcube.

[AAp4] Anton Antonov, WWW::OpenAI Raku package, (2023), GitHub/antononcube.

[AAp5] Anton Antonov, WWW::PaLM Raku package, (2023), GitHub/antononcube.

[AAp6] Anton Antonov, Markdown::Grammar Raku package, (2022-2023), GitHub/antononcube.

[AAp7] Anton Antonov, RakuMode WL paclet, (2023), Wolfram Resource System / AntonAntonov.