Here's the code for our singularity viewer. It lets you define the type, location, and strength of one or more singularities. To see the results, you specify the starting location for one or more streamlines.  Then the program computes the resulting streamlines and animates them.

## Installing Project Code

To use with CircuitPython, you need to first install a few libraries, into the lib folder on your CIRCUITPY drive. Then you need to update code.py with the example script.

Thankfully, we can do this in one go. In the example below, click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, open the directory Matrix_Portal_Flow_Viewer/flow/ and then click on the directory that matches the version of CircuitPython you're using and copy the contents of that directory to your CIRCUITPY drive.

Your CIRCUITPY drive should now look similar to the following image:

```# SPDX-FileCopyrightText: 2020 Carter Nelson for Adafruit Industries
#

import time
import math
import displayio

#--| User Config |-----------------------------------------
SINGULARITIES = (
# type  location  strength
('freestream', None, (1, 0)),
('source', (26, 16), 3),
('source', (38, 16), -3),
#('doublet', (32, 16), 1),
#('vortex', (32, 16), 1),
)
SEEDS = (
#   (x, y) starting location
(0, 0),
(0, 3),
(0, 6),
(0, 9),
(0, 12),
(0, 15),
(0, 17),
(0, 20),
(0, 23),
(0, 26),
(0, 29),
)
MATRIX_WIDTH = 64
MATRIX_HEIGHT = 32
BACK_COLOR = 0x000000 # background fill
TAIL_COLOR = 0x000A0A # trailing particles
TAIL_LENGTH = 10      # length in pixels
DELAY = 0.01          # smaller = faster
#----------------------------------------------------------

# matrix and displayio setup
matrix = Matrix(width=MATRIX_WIDTH, height=MATRIX_HEIGHT, bit_depth=6)
display = matrix.display
group = displayio.Group()
display.root_group = group

bitmap = displayio.Bitmap(display.width, display.height, 4)

palette = displayio.Palette(4)
palette[0] = BACK_COLOR
palette[1] = SING_COLOR
palette[3] = TAIL_COLOR

group.append(tile_grid)

# global to store streamline data
STREAMLINES = []

def compute_velocity(x, y):
'''Compute resultant velocity induced at (x, y) from all singularities.'''
vx = vy = 0
for s in SINGULARITIES:
if s[0] == 'freestream':
vx += s[2][0]
vy += s[2][1]
else:
dx = x - s[1][0]
dy = y - s[1][1]
r2 = dx*dx + dy*dy
if s[0] == 'source':
vx += s[2] * dx / r2
vy += s[2] * dy / r2
elif s[0] == 'vortex':
vx -=  s[2] * dy / r2
vy +=  s[2] * dx / r2
elif s[0] == 'doublet':
vx += s[2] * (dy*dy - dx*dx) / (r2*r2)
vy -= s[2] * (2*dx*dy) / (r2*r2)
return vx, vy

def compute_streamlines():
'''Compute streamline for each starting point (seed) defined.'''
for seed in SEEDS:
streamline = []
x, y = seed
px = round(x)
py = round(y)
vx, vy = compute_velocity(x, y)
streamline.append( ((px, py), (vx, vy)) )
steps = 0
while x < MATRIX_WIDTH and steps < 2 * MATRIX_WIDTH:
nx = round(x)
ny = round(y)
# if we've moved to a new pixel, store the info
if nx != px or ny != py:
streamline.append( ((nx, ny), (vx, vy)) )
px = nx
py = ny
vx, vy = compute_velocity(x, y)
x += vx
y += vy
steps += 1
# add streamline to global store
STREAMLINES.append(streamline)

def show_singularities():
'''Draw the singularites.'''
for s in SINGULARITIES:
try:
x, y = s[1]
bitmap[round(x), round(y)] = 1
except: # pylint: disable=bare-except
pass # just don't draw it

def show_streamlines():
'''Draw the streamlines.'''
try:
streamline = STREAMLINES[sl]
length = min(index, TAIL_LENGTH)
# draw tail
for data in streamline[index-length:index]:
x, y = data[0]
bitmap[round(x), round(y)] = 3
bitmap[round(x), round(y)] = 2
except: # pylint: disable=bare-except
pass # just don't draw it

def animate_streamlines():
'''Update the current location (head position) along each streamline.'''
# get associated streamline
streamline = STREAMLINES[sl]
# compute index
# get velocity
if index < len(streamline):
vx, vy = streamline[index][1]
else:
vx, vy = streamline[-1][1]
# all streamlines have reached the end, so reset to start

def update_display():
'''Update the matrix display.'''
display.auto_refresh = False
bitmap.fill(0)
show_singularities()
show_streamlines()
display.auto_refresh = True

#==========
# MAIN
#==========
print('Computing streamlines...', end='')
compute_streamlines()
print('DONE')
print('Flowing...')
while True:
animate_streamlines()
update_display()
time.sleep(DELAY)
```

## How to Use

Look at the top of the code for the section commented as `User Config`. These are the lines you can change to configure the resulting flow field displayed. The main ones are SINGULARITIES and SEEDS.

• SINGULARITIES - Add a tuple containing the type, location, and strength for each singularity you want. You generally always want a freestream element, otherwise flow won't "flow" through the display. The available types are:
• freestream
• source (remember, a sink is just a source with a negative strength)
• vortex
• doublet
• SEEDS - These define the streamlines. Add a tuple of (x, y) starting location for each streamline you want.

The code is setup to use a 64x32 matrix size. If you have a different size, change MATRIX_WIDTH and MATRIX_HEIGHT.

The rest affect the general aesthetics of the flow animation:

• BACK_COLOR - Background fill color.
• SING_COLOR - Each singularity is shown as a single pixel of this color.
• HEAD_COLOR - The pixel color for the leading pixel in a streamline trace.
• TAIL_COLOR - The pixel color for the tail of the streamline trace.
• TAIL_LENGTH - How many pixels long the streamline trace will be.
• DELAY - How fast the animation runs. Lower numbers are faster.

## Singularity Strength

So what are good values for the singularity strengths? 1? 50000? We aren't working with any actual units. So you can't set the freestream flow to be 55 mile per hour, for example. You can set any values you want, but you may find the results don't appear or animate well on the actual matrix if you set them too large.

In general, keep things in the single digit range. We'll provide some examples next, so also notice the strength values used in those.

This guide was first published on Nov 17, 2020. It was last updated on Apr 23, 2024.