From 1b991a54cc834e8ef9ccc8bb15dce7ff70cdf8a3 Mon Sep 17 00:00:00 2001 From: Sadeep Madurange Date: Wed, 24 Dec 2025 16:29:32 +0800 Subject: Matrix post. --- _site/feed.xml | 2 +- _site/log/index.html | 13 ++ _site/log/matrix-digital-rain/index.html | 125 +++++------ _site/log/neo4j-a-star-search/index.html | 370 +++++++++++++++++++++++++++++++ _site/posts.xml | 2 +- _site/robots.txt | 2 +- _site/sitemap.xml | 32 +-- 7 files changed, 467 insertions(+), 79 deletions(-) create mode 100644 _site/log/neo4j-a-star-search/index.html (limited to '_site') diff --git a/_site/feed.xml b/_site/feed.xml index 4d5229e..c7b21b4 100644 --- a/_site/feed.xml +++ b/_site/feed.xml @@ -1 +1 @@ -Jekyll2025-12-22T23:37:38+08:00http://localhost:4000/feed.xmlASCIIMX | LogW. D. Sadeep MadurangeRecreating the Matrix rain with ANSI escape sequences2025-12-21T00:00:00+08:002025-12-21T00:00:00+08:00http://localhost:4000/log/matrix-digital-rainW. D. Sadeep MadurangeHow to manage Suckless software installations2025-11-30T00:00:00+08:002025-11-30T00:00:00+08:00http://localhost:4000/log/suckless-softwareW. D. Sadeep MadurangeFingerprint door lock2025-08-18T00:00:00+08:002025-08-18T00:00:00+08:00http://localhost:4000/log/fpm-door-lockW. D. Sadeep MadurangeOn the use of MOSFETs as electronic switches2025-06-22T00:00:00+08:002025-06-22T00:00:00+08:00http://localhost:4000/log/mosfet-switchesW. D. Sadeep MadurangeHow to configure ATmega328P microcontrollers to run at 3.3V and 5V2025-06-10T00:00:00+08:002025-06-10T00:00:00+08:00http://localhost:4000/log/arduino-unoW. D. Sadeep MadurangeMy first PCB2025-04-26T00:00:00+08:002025-04-26T00:00:00+08:00http://localhost:4000/log/my-first-pcbW. D. Sadeep MadurangeBumblebee: browser automation2025-04-02T00:00:00+08:002025-04-02T00:00:00+08:00http://localhost:4000/log/bumblebeeW. D. Sadeep MadurangeHow to set up ATSAM3X8E microcontrollers for bare-metal programming in C2024-09-16T00:00:00+08:002024-09-16T00:00:00+08:00http://localhost:4000/log/arduino-dueW. D. Sadeep MadurangeEtlas: e-paper dashboard2024-09-05T00:00:00+08:002024-09-05T00:00:00+08:00http://localhost:4000/log/etlasW. D. Sadeep MadurangeExperimental e-reader2023-10-24T00:00:00+08:002023-10-24T00:00:00+08:00http://localhost:4000/log/e-readerW. D. Sadeep Madurange \ No newline at end of file +Jekyll2025-12-24T16:28:25+08:00/feed.xmlASCIIMX | LogW. D. Sadeep MadurangeRecreating the Matrix rain with ANSI escape sequences2025-12-21T00:00:00+08:002025-12-21T00:00:00+08:00/log/matrix-digital-rainW. D. Sadeep MadurangeHow to manage Suckless software installations2025-11-30T00:00:00+08:002025-11-30T00:00:00+08:00/log/suckless-softwareW. D. Sadeep MadurangeFingerprint door lock2025-08-18T00:00:00+08:002025-08-18T00:00:00+08:00/log/fpm-door-lockW. D. Sadeep MadurangeOn the use of MOSFETs as electronic switches2025-06-22T00:00:00+08:002025-06-22T00:00:00+08:00/log/mosfet-switchesW. D. Sadeep MadurangeHow to configure ATmega328P microcontrollers to run at 3.3V and 5V2025-06-10T00:00:00+08:002025-06-10T00:00:00+08:00/log/arduino-unoW. D. Sadeep MadurangeMy first PCB2025-04-26T00:00:00+08:002025-04-26T00:00:00+08:00/log/my-first-pcbW. D. Sadeep MadurangeBumblebee: browser automation2025-04-02T00:00:00+08:002025-04-02T00:00:00+08:00/log/bumblebeeW. D. Sadeep MadurangeHow to set up ATSAM3X8E microcontrollers for bare-metal programming in C2024-09-16T00:00:00+08:002024-09-16T00:00:00+08:00/log/arduino-dueW. D. Sadeep MadurangeEtlas: e-paper dashboard2024-09-05T00:00:00+08:002024-09-05T00:00:00+08:00/log/etlasW. D. Sadeep MadurangeExperimental e-reader2023-10-24T00:00:00+08:002023-10-24T00:00:00+08:00/log/e-readerW. D. Sadeep Madurange \ No newline at end of file diff --git a/_site/log/index.html b/_site/log/index.html index 32f0ff2..4296fb7 100644 --- a/_site/log/index.html +++ b/_site/log/index.html @@ -174,6 +174,19 @@ + + + Neo4J A* search + + + + + + + + + + diff --git a/_site/log/matrix-digital-rain/index.html b/_site/log/matrix-digital-rain/index.html index 6007da5..4a23d29 100644 --- a/_site/log/matrix-digital-rain/index.html +++ b/_site/log/matrix-digital-rain/index.html @@ -44,40 +44,60 @@

RECREATING THE MATRIX RAIN WITH ANSI ESCAPE SEQUENCES

21 DECEMBER 2025

-

The Matrix digital rain implemented in raw C using ANSI escape sequences with -zero dependencies—not even ncurses.

- - +

My 2022 implementation of the Matrix rain had too many loose ends. Unicode +support was inflexible: the charset had to be a single contiguous block with no +way to mix ASCII with something like Katakana; Phosphor decay level was stored +in a dedicated array–still don’t understand why I did that when I had already +used bit-packing for the RGB channels; The algorithm was difficult to decipher. +The 2022 version worked, but that’s not the same thing as being correct.

+ +

I began by placing the decay factor in the LSB of the 4-byte RGB value. Let’s +call that RGB-PD. PD plays a somewhat analogous role to an alpha channel; I +avoided labelling it A so as not to cause confusion:

+ +
enum {
+    R,  /* Red   */
+    G,  /* Green */
+    B,  /* Blue  */ 
+    PD  /* Phosphor decay level */
+};
 
-

This is a fork of Domsson’s unique rendition of the Matrix rain: Fakesteak. Three years ago, I forked his project -and added truecolor and Unicode support. I also drastically modified the -algorithm to produce a rain that resembled the original aesthetic with high -visual fidelity.

+typedef union color_tag { + uint32_t value; + unsigned char color[4]; +} color; +
-

Unicode support

+

The decision to use union over more portable bit twiddling was made three years +ago, as I recall, for readability. Seeing as all my systems are little-endian, +this is unlikely to cause me trouble. Besides, if union is never to be used, +why is it in the language anyway?

-

Unicode support in the 2022 version lacked flexibility. The charset used in the -rain had to be a single contiguous block defined by UNICODE_MIN and -UNICODE_MAX settings:

+

The blend() function, which emulates the dim afterglow of Phosphor by eroding +the RGB channels towards the background, remains as elegant as it did three +years ago:

-
#define UNICODE_MIN 0x0021
-#define UNICODE_MAX 0x007E
+
#define DECAY_MPLIER  2
 
-static inline void insert_code(matrix *mat,
-    size_t row, size_t col) 
+static inline void blend(matrix *mat,
+    size_t row, size_t col)
 {
-    mat->code[index(mat, row, col)] = rand()
-        % (UNICODE_MAX - UNICODE_MIN)
-        + UNICODE_MIN;
+    unsigned char *color;
+
+    color = mat->rgb[index(mat, row, col)].color;
+    color[R] = color[R] - (color[R] - RGB_BG_RED) / DECAY_MPLIER;
+    color[G] = color[G] - (color[G] - RGB_BG_GRN) / DECAY_MPLIER;
+    color[B] = color[B] - (color[B] - RGB_BG_BLU) / DECAY_MPLIER;
 }
 
-

There was no way, for instance, to use both ASCII and Katakana at the same -time. The user had to pick one. In the new version, the user can use any number -of Unicode blocks using glyphs array. In fact, the default rain now includes -both ASCII and half-width Katakana characters:

+

While the memory inefficiency of Phosphor decay tracking was a technical +oversight I hadn’t noticed, the limitation around mixing nonadjacent Unicode +blocks was a nagging concern even three years ago. So, a fix was long overdue.

+ +

In the new version, I introduced a glyphs array that enables a user to add as +many Unicode blocks as they want. The insert_code() function picks a block +from the array at random, and then picks a character from that block at random:

#define UNICODE(min, max)  (((uint64_t)max << 32) | min)
 
@@ -104,51 +124,32 @@ static inline void insert_code(matrix *mat,
 }
 
-

Entries in the glyphs array are Unicode blocks bit-packed in an 8-byte -container: the four low bytes forms the first codepoint and the four high bytes -the last.

- -

Phosphor decay

+

The Unicode blocks are stored in 8-byte containers: the low four bytes form the +first codepoint and the high four bytes the last. Here, I chose bitwise +operations over unions because, first and foremost, the operations themselves +are trivial and idiomatic, and the UNICODE() macro simplifies the management of +charsets. The insert_code() function is now ready to take its rightful place +next to blend().

-

The dim afterglow of monochrome CRT displays is achieved by carefully scaling -the RGB channels individually and mixing them:

- -
#define DECAY_MPLIER  2
-
-static inline void blend(matrix *mat,
-    size_t row, size_t col)
-{
-    unsigned char *color;
-
-    color = mat->rgb[index(mat, row, col)].color;
-    color[R] = color[R] - (color[R] - RGB_BG_RED) / DECAY_MPLIER;
-    color[G] = color[G] - (color[G] - RGB_BG_GRN) / DECAY_MPLIER;
-    color[B] = color[B] - (color[B] - RGB_BG_BLU) / DECAY_MPLIER;
-}
-
- -

The blending function emulates the phosphor decay by gradually transitioning -each raindrop’s color towards the background color. The multiplier is the -number of passes over the rain track needed before the afterglow disappears.

- -

The algorithm

- -

Nonetheless, the rain resembles the original with high visual fidelity. It’s -highly customizable and gentle on the CPU. On my 14” ThinkPad T490, which has a -resolution of 1920x1080 and 4GHz CPU, it uses 2-3% of the CPU with occasional -jumps of up to about 8%. Not too bad for a weekend project. The program has -been tested with xterm and urxvt terminal emulators on OpenBSD and Arch Linux -systems. Someone has managed to get it moving on a Raspberry Pi as well.

- -

Lastly, to compile and run:

+

The result is a digital rain that captures the original Matrix aesthetic with +high visual fidelity:

$ cc -O3 main.c -o matrix
 $ ./matrix
 
-

“All I see is blonde, brunette, red head.”

+ + +

There was no cause to measure the program’s performance characteristics +precisely; it’s gentle on the CPU. On my ThinkPad T490 running OpenBSD, which +has a resolution of 1920x1080, it uses about 2-3% of the CPU, with occasional +jumps of up to about 8%; the cores remain silent, the fans don’t whir, the rain +falls in quiet.

Files: source.tar.gz

+
diff --git a/_site/log/neo4j-a-star-search/index.html b/_site/log/neo4j-a-star-search/index.html new file mode 100644 index 0000000..9473dc6 --- /dev/null +++ b/_site/log/neo4j-a-star-search/index.html @@ -0,0 +1,370 @@ + + + + + Neo4J A* search + + + + + Neo4J A* search + + + + + + + + + + + + + +
+
+
+

NEO4J A* SEARCH

+
06 MARCH 2018
+
+

Back in 2018, we used Neo4J graph database to track the +movement of marine vessels. We were interested in the shortest path a ship +could take through a network of about 13,000 route points. Graph theoretic +algorithms provide optimal solutions to such problems, and the set of route +points lends itself well to graph-based modelling.

+ +

A graph is a finite set of vertices, and a subset of vertex pairs (edges). +Edges can have weights. In the case of vessel tracking, the route points form +the vertices of a graph; the routes between them the edges; and the distances +between them the weights. For various reasons, people are interested in +minimizing (or maximizing) the weight of a path through a set of vertices, such +as the shortest path between two ports to predict a vessel’s arrival time.

+ +

Given a graph, an algorithm like Dijkstra’s search could compute the shortest +path between two vertices. In fact, this was the algorithm Neo4J shipped with +at the time. One drawback of Dijkstra’s algorithm is that it computes all the +shortest paths from the source to all other vertices before terminating at the +destination vertex. The time complexity of this exhaustive search prevented our +database from scaling beyond 4,000 route points.

+ +

The following enhancement to Dijkstra’s search, also known as the A* search, +employs a heuristic to steer the search in the direction of the destination +more quickly. In the case of our network of vessels, which are on the earth’s +surface, spherical distance is a good candidate for a heuristic:

+ +
package org.neo4j.graphalgo.impl;
+
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import org.neo4j.graphalgo.api.Graph;
+import org.neo4j.graphalgo.core.utils.ProgressLogger;
+import org.neo4j.graphalgo.core.utils.queue.IntPriorityQueue;
+import org.neo4j.graphalgo.core.utils.queue.SharedIntPriorityQueue;
+import org.neo4j.graphalgo.core.utils.traverse.SimpleBitSet;
+import org.neo4j.graphdb.Direction;
+import org.neo4j.graphdb.Node;
+import org.neo4j.kernel.internal.GraphDatabaseAPI;
+
+import com.carrotsearch.hppc.IntArrayDeque;
+import com.carrotsearch.hppc.IntDoubleMap;
+import com.carrotsearch.hppc.IntDoubleScatterMap;
+import com.carrotsearch.hppc.IntIntMap;
+import com.carrotsearch.hppc.IntIntScatterMap;
+
+public class ShortestPathAStar extends Algorithm<ShortestPathAStar> {
+    
+    private final GraphDatabaseAPI dbService;
+    private static final int PATH_END = -1;
+    
+    private Graph graph;
+    private final int nodeCount;
+    private IntDoubleMap gCosts;
+    private IntDoubleMap fCosts;
+    private double totalCost;
+    private IntPriorityQueue openNodes;
+    private IntIntMap path;
+    private IntArrayDeque shortestPath;
+    private SimpleBitSet closedNodes;
+    private final ProgressLogger progressLogger;
+    
+    public static final double NO_PATH_FOUND = -1.0;
+    
+    public ShortestPathAStar(
+        final Graph graph,
+        final GraphDatabaseAPI dbService) {
+
+        this.graph = graph;
+        this.dbService = dbService;
+
+        nodeCount = Math.toIntExact(graph.nodeCount());
+        gCosts = new IntDoubleScatterMap(nodeCount);
+        fCosts = new IntDoubleScatterMap(nodeCount);
+        openNodes = SharedIntPriorityQueue.min(
+            nodeCount,
+            fCosts,
+            Double.MAX_VALUE);
+        path = new IntIntScatterMap(nodeCount);
+        closedNodes = new SimpleBitSet(nodeCount);
+        shortestPath = new IntArrayDeque();
+        progressLogger = getProgressLogger();
+    }
+    
+    public ShortestPathAStar compute(
+        final long startNode,
+        final long goalNode,
+        final String propertyKeyLat,
+        final String propertyKeyLon,
+        final Direction direction) {
+
+        reset();
+
+        final int startNodeInternal = 
+            graph.toMappedNodeId(startNode);
+        final double startNodeLat =
+            getNodeCoordinate(startNodeInternal, propertyKeyLat);
+        final double startNodeLon = 
+            getNodeCoordinate(startNodeInternal, propertyKeyLon);
+
+        final int goalNodeInternal =
+            graph.toMappedNodeId(goalNode);
+        final double goalNodeLat = 
+            getNodeCoordinate(goalNodeInternal, propertyKeyLat);
+        final double goalNodeLon = 
+            getNodeCoordinate(goalNodeInternal, propertyKeyLon);
+
+        final double initialHeuristic = 
+            computeHeuristic(startNodeLat,
+                startNodeLon,
+                goalNodeLat,
+                goalNodeLon);
+
+        gCosts.put(startNodeInternal, 0.0);
+        fCosts.put(startNodeInternal, initialHeuristic);
+        openNodes.add(startNodeInternal, 0.0);
+
+        run(goalNodeInternal,
+            propertyKeyLat,
+            propertyKeyLon,
+            direction);
+
+        if (path.containsKey(goalNodeInternal)) {
+            totalCost = gCosts.get(goalNodeInternal);
+            int node = goalNodeInternal;
+            while (node != PATH_END) {
+                shortestPath.addFirst(node);
+                node = path.getOrDefault(node, PATH_END);
+            }
+        }
+        return this;
+    }
+    
+    private void run(
+        final int goalNodeId,
+        final String propertyKeyLat,
+        final String propertyKeyLon,
+        final Direction direction) {
+
+        final double goalLat = 
+            getNodeCoordinate(goalNodeId, propertyKeyLat);
+        final double goalLon =
+            getNodeCoordinate(goalNodeId, propertyKeyLon);
+
+        while (!openNodes.isEmpty() && running()) {
+            int currentNodeId = openNodes.pop();
+            if (currentNodeId == goalNodeId) {
+                return;
+            }
+
+            closedNodes.put(currentNodeId);
+
+            double currentNodeCost = 
+                this.gCosts.getOrDefault(
+                    currentNodeId, 
+                    Double.MAX_VALUE);
+
+            graph.forEachRelationship(
+                currentNodeId,
+                direction,
+                (source, target, relationshipId, weight) -> {
+                    double neighbourLat = 
+                        getNodeCoordinate(target, propertyKeyLat);
+                    double neighbourLon = 
+                        getNodeCoordinate(target, propertyKeyLon);
+                    double heuristic = 
+                        computeHeuristic(
+                            neighbourLat, 
+                            neighbourLon, 
+                            goalLat,
+                            goalLon);
+
+                    updateCosts(
+                        source,
+                        target,
+                        weight + currentNodeCost,
+                        heuristic);
+
+                    if (!closedNodes.contains(target)) {
+                        openNodes.add(target, 0);
+                    }
+                    return true;
+                });
+
+            progressLogger.logProgress(
+                (double) currentNodeId / (nodeCount - 1));
+        }
+    }
+    
+    private double computeHeuristic(
+        final double lat1,
+        final double lon1,
+        final double lat2,
+        final double lon2) {
+
+        final int earthRadius = 6371;
+        final double kmToNM = 0.539957;
+        final double latDistance = Math.toRadians(lat2 - lat1);
+        final double lonDistance = Math.toRadians(lon2 - lon1);
+        final double a = Math.sin(latDistance / 2)
+            * Math.sin(latDistance / 2)
+            + Math.cos(Math.toRadians(lat1))
+            * Math.cos(Math.toRadians(lat2))
+            * Math.sin(lonDistance / 2)
+            * Math.sin(lonDistance / 2);
+        final double c = 2
+            * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+        final double distance = earthRadius * c * kmToNM;
+        return distance;
+    }
+    
+    private double getNodeCoordinate(
+        final int nodeId,
+        final String coordinateType) {
+
+        final long neo4jId = graph.toOriginalNodeId(nodeId);
+        final Node node = dbService.getNodeById(neo4jId);
+        return (double) node.getProperty(coordinateType);
+    }
+    
+    private void updateCosts(
+        final int source, 
+        final int target, 
+        final double newCost,
+        final double heuristic) {
+
+        final double oldCost = 
+            gCosts.getOrDefault(target, Double.MAX_VALUE);
+
+        if (newCost < oldCost) {
+            gCosts.put(target, newCost);
+            fCosts.put(target, newCost + heuristic);
+            path.put(target, source);
+        }
+    }
+    
+    private void reset() {
+        closedNodes.clear();
+        openNodes.clear();
+        gCosts.clear();
+        fCosts.clear();
+        path.clear();
+        shortestPath.clear();
+        totalCost = NO_PATH_FOUND;
+    }
+    
+    public Stream<Result> resultStream() {
+        return StreamSupport.stream(
+            shortestPath.spliterator(), false)
+                .map(cursor -> new Result(
+                    graph.toOriginalNodeId(cursor.value),
+                    gCosts.get(cursor.value)));
+    }
+
+    public IntArrayDeque getFinalPath() {
+        return shortestPath;
+    }
+    
+    public double getTotalCost() {
+        return totalCost;
+    }
+
+    public int getPathLength() {
+        return shortestPath.size();
+    }
+    
+    @Override
+    public ShortestPathAStar me() {
+        return this;
+    }
+
+    @Override
+    public ShortestPathAStar release() {
+        graph = null;
+        gCosts = null;
+        fCosts = null;
+        openNodes = null;
+        path = null;
+        shortestPath = null;
+        closedNodes = null;
+        return this;
+    }
+    
+    public static class Result {
+
+        /**
+         * the neo4j node id
+         */
+        public final Long nodeId;
+
+        /**
+         * cost to reach the node from startNode
+         */
+        public final Double cost;
+
+        public Result(Long nodeId, Double cost) {
+            this.nodeId = nodeId;
+            this.cost = cost;
+        }
+    }
+}
+
+ +

The heuristic function is domain-specific. If chosen wisely, it can +significantly speed up the search. In our case, we achieved a 300x speedup, +enabling us to expand our search from 4,000 to 13,000 route points. The v3.4.0 of the +Neo4J graph algorithms shipped with our A* search algorithm.

+ +
+ +
+
+
+ + + + + + diff --git a/_site/posts.xml b/_site/posts.xml index 1d3402d..37970da 100644 --- a/_site/posts.xml +++ b/_site/posts.xml @@ -1 +1 @@ -Jekyll2025-12-22T23:37:38+08:00http://localhost:4000/posts.xmlASCIIMXW. D. Sadeep Madurange \ No newline at end of file +Jekyll2025-12-24T16:28:25+08:00/posts.xmlASCIIMXW. D. Sadeep Madurange \ No newline at end of file diff --git a/_site/robots.txt b/_site/robots.txt index d297064..e087884 100644 --- a/_site/robots.txt +++ b/_site/robots.txt @@ -1 +1 @@ -Sitemap: http://localhost:4000/sitemap.xml +Sitemap: /sitemap.xml diff --git a/_site/sitemap.xml b/_site/sitemap.xml index 7a775fd..e8e9d7b 100644 --- a/_site/sitemap.xml +++ b/_site/sitemap.xml @@ -1,55 +1,59 @@ -http://localhost:4000/log/e-reader/ +/log/neo4j-a-star-search/ +2018-03-06T00:00:00+08:00 + + +/log/e-reader/ 2023-10-24T00:00:00+08:00 -http://localhost:4000/log/etlas/ +/log/etlas/ 2024-09-05T00:00:00+08:00 -http://localhost:4000/log/arduino-due/ +/log/arduino-due/ 2024-09-16T00:00:00+08:00 -http://localhost:4000/log/bumblebee/ +/log/bumblebee/ 2025-04-02T00:00:00+08:00 -http://localhost:4000/log/my-first-pcb/ +/log/my-first-pcb/ 2025-04-26T00:00:00+08:00 -http://localhost:4000/log/arduino-uno/ +/log/arduino-uno/ 2025-06-10T00:00:00+08:00 -http://localhost:4000/log/mosfet-switches/ +/log/mosfet-switches/ 2025-06-22T00:00:00+08:00 -http://localhost:4000/log/fpm-door-lock/ +/log/fpm-door-lock/ 2025-08-18T00:00:00+08:00 -http://localhost:4000/log/suckless-software/ +/log/suckless-software/ 2025-11-30T00:00:00+08:00 -http://localhost:4000/log/matrix-digital-rain/ +/log/matrix-digital-rain/ 2025-12-21T00:00:00+08:00 -http://localhost:4000/about/ +/about/ -http://localhost:4000/ +/ -http://localhost:4000/log/ +/log/ -http://localhost:4000/projects/ +/projects/ -- cgit v1.2.3