Python program using Lennard-Jones force to approximate cortical stem-cell close packing symmetry

Signal propagation works better than I expected! Code is at: https://sites.google.com/site/intelligencedesignlab/home/BrainBall.zip

Even with way less than perfect vesicle symmetry the waves are soon cancelling out real nice on the opposite side of the sphere.

There is no longer a file reader program. The maker program shows front and back sides, both ways, with signals on top.

If there is a “xyzStartup.txt” file (saved by pressing “s” key) then it uses that, otherwise program starts from scratch on a new one.

When an area starts bouncing around enough to break and reform connections the result is overactivity resembling an epileptic seizure. Since it is of interest to see what happens when connections go to the wrong places I left that in, instead of preventing by silencing cells that are breaking then reforming connections. Program will clear all outputs after becoming too active, but shows enough activity to get an idea what the possible self-oscillation patterns look like. We then get to see complex wave patterns that can on purpose also be generated, for some purpose, like this spiral:

The input pattern each receives normally indicates where on the sphere the wave originated from. On the direct opposite side of the sphere all inputs become 1 then cancel after negating input, to derive output. Input bits 111111 become output bits 000000, no output signal. It’s one of the things (assuming they are intelligent enough) early cells could possibly do for fun “because they can” then later use for polled navigational planning or other purposes.

Full code is:

# Propagate signals around a developing "brain ball" vesicle.  Gary Gaulin, 1/1/2020
# Lennard Jones Potential approximates stem cell migration on a sphere to equally space themselves apart.
# If there is a "xyzStartup.txt" file (saved by pressing "s" key) then program starts with that, otherwise from scratch.
# There is also a "Nice-xyzStartup.txt" from earlier benchmark to use to improve upon, by tweaking physics related code.
# It is not necessary to achieve perfect symmetry, waves will still travel fine, but symmetry can be improved.
# This implementation is for also showing what happens when connections are breaking and signals go to wrong places,
# which can in the timestepsignals function be prevented, when connections change from one timestep to next.
# Keyboard space bar pauses program, then resumes.
# Thin 3D Axis lines are drawn in upper left view showing: x=green, y=blue, z=yellow
# More information:
# https://discourse.numenta.org/t/python-program-using-lennard-jones-force-to-approximate-cortical-stem-cell-close-packing-symmetry/6456

import random
import sys
import numpy as np
import pygame

Pi = 3.14159265358979323846264338327950288412
HalfPi = Pi / 2
Radian = Pi * 2
ThreeQtrRadian = Radian * 3/4
Black = (0, 0, 0)
Brown = (55, 0, 0)
Red = (255, 0, 0)
Orange = (155, 100, 50)
Yellow = (220, 220, 0)
Green = (0, 200, 0)
Blue = (0, 0, 150)
Violet = (255, 0, 255)
Gray = (120, 120, 120)
White = (255, 255, 255)
# From electronics: 0=Black, 1=Brown, 2=Red, 3=Orange, 4=Yellow, 5=Green, 6=Blue, 7=Violet, 8=Gray, 9=White
ElectronicColorCode = Black, Brown, Red, Orange, Yellow, Green, Blue, Violet, Gray, White
# Colors used to draw connections that are either 0, or 1 for action potential.
SignalColor = Black, White

screensquare = 360                          # Size for each of 4 squares.
screensize = Width, Height = (screensquare * 2, screensquare * 2)    # PyGame window size, to show 2*2=4 views.
xc = int(screensize[0]/4)                   # Center x of 1 screen.
yc = int(screensize[1]/4)                   # Center y of 1 screen.
screenradius = screensquare * .45           # Screen radius of vesicle, with some added space around sides.
sas = 4*Pi*(screenradius*screenradius)      # Area of sphere at this given screen radius.
sphereradius = int(screenradius * .9)       # Start with points a little squeezed together.
timestepnumber = 0                          # For displaying the time step number.


def csvwrite(sourcelist, filename):         # Write Comma Separated Value list of lists [,,] or tuples (,,) to disk.
    # For control over file behavior csv or other import module is not used for reading or writing.
    with open(filename, 'w') as f:          # Open file to write over, using 'f' as its name:
        for line in sourcelist:             # For each line of values in source list:
            s = str(line)                   # Convert line to a string containing commas.
            f.write(s[1:-1] + "\n")         # Write all but [] or () chars, line feed at end.


# File to read must have no blank lines or comments added to it, comma separated values only.
def csvread(filename):                      # Read disk file back into list of lists.
    destination = []                        # Clear destination list lists are appended to.
    with open(filename, 'r') as f:          # Open file in read mode using 'f' as its name:
        for line in f:                      # For each comma separated line of code in 'f':
            s = line.rstrip('\n')
            d = []                          # Empty list named 'd' to fill with elements.
            if len(s) > 0:
                s = s.split(',')            # Split line at commas into list named 's'
                for e in s:                 # For each string element listed in 's' list:
                    d.append(float(e))      # Append floating point element value to list.
            destination.append(d)           # Append list of elements to destination list.
    return destination                      # Returns list of lists only = [[,,],[,,],[,,]]


#  Update positions, velocities, accelerations.
def update(points, pos, vel, f, acc, mass, dt):
    rmass = 1.0 / mass
    if dialog != 'Holding Own Shape':
        tosphere()
# Update positions, for 3 dimensions.
    for i in range(pointcount):
        for j in range(3):
            pos[i][j] = pos[i][j] + vel[i][j] * dt + 0.5 * acc[i][j] * dt * dt
    if dialog != 'Holding Own Shape':
        tosphere()
#  Update velocities.
    for i in range(pointcount):
        for j in range(3):
            vel[i][j] = vel[i][j] + 0.5 * dt * (f[i][j] * rmass + acc[i][j])
            vel[i][j] = vel[i][j]*.2
#  Update accelerations.
    for i in range(pointcount):
        for j in range(3):
            acc[i][j] = f[i][j] * rmass
    return pos, vel, acc


def forces(points, pos, vel, mass):
    #  Compute Lennard-Jones force and energies.
    ljt = 0  # Initialize variable for totaling Lennard-Jones force.
    ljmax = -100000000                              # Initialize maximum, negative when points attract.
    ljmin = 1000000000                              # Minimum reading goes negative when points attract.
    ljsum = [0 for _ in range(points)]              # Clear sum used for color and brightness drawn to screen.
    force = [[0, 0, 0] for _ in range(points)]
    rij = [0, 0, 0]
    global connect
    connect = [[] for _ in range(points)]           # Articulation list for what connects to where.
    distribution = [0 for _ in range(10)]           # list of how many total have 0,1,2,3,4,5,6,7,8 and 9 connections.
    adjustedconnectdist = connectdist * (sphereradius/screenradius)     # Distance in proportion to squeezed radius.
    lj = 0
    for i in range(len(near)):
        for fj in range(len(near[i])):
            j = near[i][fj]
#  Displacement vector RIJ.
            for k in range(3):
                rij[k] = pos[i][k] - pos[j][k]
#  Compute D and D2, a distance and a truncated distance.
            d = 0.0
            for k in range(3):
                d = d + rij[k] ** 2
            d = np.sqrt(d)
            if d < adjustedconnectdist:
                connect[i].append(j)
#  Lennard Jones force.
            ljr = d / spacing  # Lennard-Jones radius. At ljr=1 force is 0.
            if ljr <= 0:  # Prevent divide by zero error when LJ radius is zero.
                lj = 0
            else:
                lj = 4 * epsilon * ((sigma / ljr) ** 12 - (sigma / ljr) ** 6)
                if lj > ljlimit:
                    lj = ljlimit
                lj = lj / ljdivide
            ljsum[i] += lj                  # Used for brightness to draw on screen.
#  Add particle J's contribution to the force on particle I.
            for k in range(3):
                force[i][k] = force[i][k] + (rij[k] * lj)
# Add force to sum total, for average of all points.
        ljt += ljsum[i]
# Save minimum and maximum readings.
        if lj > ljmax:
            ljmax = lj
        if lj < ljmin:
            ljmin = lj
# Divide down sum total for average.
    if nearcount > 0:
        ljavr = ljt / nearcount
    else:
        ljavr = 0
# Save distribution for 0 to 9 connections, most should evetually have 6.
    for i in range(points):
        connectioncount = len(connect[i])                   # List length at i same as number of connections.
        if connectioncount < len(distribution):             # Normally only 0 to 9 will ever be nonzero.
            distribution[connectioncount] += 1              # Increment place in the list, for given connections.
    return force, ljavr, ljmax, ljmin, ljsum, distribution


# Cartesian to Spherical coordinates.
def cart2sphere(xyz):
    hxy = np.hypot(xyz[0], xyz[1])
    el = np.arctan2(xyz[2], hxy)
    az = np.arctan2(xyz[1], xyz[0])
    return az, el, np.hypot(hxy, xyz[2])


# Spherical to Cartesian coordinates.
def sphere2cart(az, el, ra):
    theta = ra * np.cos(el)
    x = theta * np.cos(az)
    y = theta * np.sin(az)
    z = ra * np.sin(el)
    return [x, y, z]


def drawaxisline(x, y, z, c):
    az, el, r = cart2sphere([x, y, z])                  # Cartesian to Spherical (az, el, r)
    x1, y1, z1 = sphere2cart(az + rotation, el, r)      # Spherical to Cartesian.
    pygame.draw.line(screen, c, (xc, yc),  (x1+xc, z1+yc), 1)


def update_nearbylist():
    near = [[] for _ in range(pointcount)]  # Initialize list of points within cutoff range.
    nearcount = 0  # And a counter for keeping track of how many.
    for i in range(pointcount):                         # For all the i points
        for j in range(pointcount):                     # and for all the j points
            if i != j:                                  # Except itself
                dx = pos[j][0] - pos[i][0]              # Calculate x,y,z distances.
                dy = pos[j][1] - pos[i][1]
                dz = pos[j][2] - pos[i][2]
        # Use the three distances for calculating an always positive 3D distance.
        # This is the very time consuming command, to less occasionally use.
                dist = np.sqrt((dx * dx) + (dy * dy) + (dz * dz))
        # If distance is within cutoff range then save 'j' point, at this 'i'.
                if dist < cutoffdist:
                    near[i].append(j)
                    nearcount += 1
    print('Computed Nearby List of Points within CutOff Range, ', nearcount, 'distances')
    return near, nearcount


def render():
    # Precalculate all the rotated X,Y screen locations and their colors.
    for i in range(pointcount):
        az, el, r = cart2sphere(pos[i][0:])             # Cartesian to Spherical (az, el, r)
        x, y, z = sphere2cart(az + rotation, el, r)     # Spherical to Cartesian.
    # Vary brightness by amount of force, positive increasingly orange, negative blue.
        if ljsum[i] > 0:
            a1 = (ljsum[i]/ljlimit) * ljdivide * HalfPi
            if a1 > HalfPi:
                a1 = HalfPi
            red255 = np.sin(a1)*255
            grn255 = red255*.6
            blu255 = red255*.2
        else:
            red255 = 0
            grn255 = red255
            blu255 = int((-ljsum[i]/10) * ljdivide * 255)
            if blu255 > 255:
                blu255 = 255
        c1 = (red255, grn255, blu255)
        n = len(connect[i])
        if n <= 9:
            c2 = ElectronicColorCode[n]
        else:
            c2 = White
        xyzc[i] = [x, y, z, c1, c2, i]                  # Save xyz and color.
        unsortedxyzc[i] = xyzc[i]
    # To properly draw on top of each other sort the list according to what is furthest away, [1] = by Y axis.
    xyzc.sort(key=lambda x: x[1])
    cr = int(spacing / 2)
    # Draw two screens on left shoiwing front view.
    for i in range(pointcount):
        x, y, z, c1, c2, n = xyzc[i]                    # Normal i direction, nearest is drawn on top of far away.
        xs = int(x + xc)
        ys = int(z + yc)
        if y > -spacing:                        # For clarity only nearest/visible side of sphere is shown.
            pygame.draw.circle(screen, c1, [xs, ys], cr, 0)
            pygame.draw.circle(screen, (255, 255, 255), [xs, ys], cr, 1)
            pygame.draw.circle(screen, c2, [xs, ys + screensquare], cr, 0)
            pygame.draw.circle(screen, (255, 255, 255), [xs, ys + screensquare], cr, 1)
    # Draw two screens to the right showing back views.
    for i in range(pointcount):
        x, y, z, c1, c2, n = xyzc[pointcount-1-i]       # Reversed i direction, far away is instead drawn on top.
        xs = int(-x + xc)                               # Viewed from back: what was on left is to right, negative x.
        ys = int(z + yc)                                # Z axis remains same up/down orientation.
        if y < spacing:                                 # For clarity only nearest/visible side of sphere is shown.
            pygame.draw.circle(screen, c1, [xs + screensquare, ys], cr, 0)
            pygame.draw.circle(screen, (255, 255, 255), [xs + screensquare, ys], cr, 1)
            pygame.draw.circle(screen, c2, [xs + screensquare, ys + screensquare], cr, 0)
            pygame.draw.circle(screen, (255, 255, 255), [xs + screensquare, ys + screensquare], cr, 1)
    # Draw signal propagation lines on top of 2 front views.
    for i in range(pointcount):
        x, y, z, c1, c2, n = xyzc[i]
        if y > -spacing:                         # For clarity only nearest/visible side of sphere is shown.
            xs1 = int(x + xc)
            ys1 = int(z + yc)
            bits = outs[n]
            for j in range(len(connect[n])):
                x2, y2, z2, c1, c2, n2 = unsortedxyzc[connect[n][j]]
                drawsignalline(xs1, ys1, x2 + xc, z2 + yc, bits[j], 0, screensquare)
                drawsignalline(xs1, ys1, x2 + xc, z2 + yc, bits[j], 0, 0)
    # Draw signal propagation lines on top of 2 back views.
    for i in range(pointcount):
        x, y, z, c1, c2, n = xyzc[pointcount - 1 - i]
        if y < spacing:                          # For clarity only nearest/visible side of sphere is shown.
            xs1 = -x + xc
            ys1 = z + yc
            bits = outs[n]
            for j in range(len(connect[n])):
                x2, y2, z2, c1, c2, n2 = unsortedxyzc[connect[n][j]]
                drawsignalline(xs1, ys1, -x2 + xc, z2 + yc, bits[j], screensquare, screensquare)
                drawsignalline(xs1, ys1, -x2 + xc, z2 + yc, bits[j], screensquare, 0)
    # Draw thin axis lines in upper left view.
    ra = sphereradius * 1.1                     # Distance of axis line, from 0.
    drawaxisline(0, 0, ra, Yellow)
    if rotation > ThreeQtrRadian:               # Green x behind blue y?
        drawaxisline(ra, 0, 0, Green)
    drawaxisline(0, ra, 0, Blue)
    if rotation < ThreeQtrRadian:               # Green x on top of blue y?
        drawaxisline(ra, 0, 0, Green)
    ra = sphereradius * 1.1                     # Distance of axis line, from 0.
    drawaxisline(0, 0, ra, Yellow)
    if rotation > ThreeQtrRadian:               # Green x behind blue y?
        drawaxisline(ra, 0, 0, Green)
    # Draw lines to divide screen into four.
    pygame.draw.line(screen, Gray, (screensquare - 1, 0), (screensquare - 1, Width), 1)
    pygame.draw.line(screen, White, (0, screensquare - 1), (Height, screensquare - 1), 1)
    # Display everything to screen.
    pygame.display.update()                     # Display everything drawn, into screen buffer.
    fps.tick(60)                                # Speed is at most 60 frames per second.


def drawsignalline(x1, y1, x2, y2, b, xleft, yleft):
    thick = int((b * spacing / 4) + 1)
    pygame.draw.line(screen, SignalColor[b], [xleft+x1, yleft+y1], [xleft+x2, yleft+y2], thick)


# Keep points on sphere by converting to Spherical then (using sphere radius) to Cartesian again.
def tosphere():
    for i in range(pointcount):                     # 1D Radial distance between x points.
        az, el, r = cart2sphere(pos[i])             # Cartesian to Spherical.
        xyz = sphere2cart(az, el, sphereradius)     # Spherical to Cartesian again.
        pos[i] = xyz


def timestepsignals():
    signalactivity = 0
    global outs
    ins = [[] for _ in range(pointcount)]           # Clear Input list output bits from neighbors are gathered into.
    for i in range(pointcount):                     # For each 'i' point in vesicle, each with connections to 'j's
        for j in range(len(connect[i])):            # For each of the (usually 6) 'j' connections to other cells,
            if j < len(outs[i]):                    # If output connection still exists in connection list then
                b = outs[i][j]                      # bit to save is from output list.
            else:                                   # Else
                b = 0                               # to prevent program error append with 0 so any bit fills void.
            ins[connect[i][j]].append(b)            # Append bit from 'j' output to corresponding input line at 'i'
    # Use gathered Input bits to set corresponding Output list bits.
    outs = [[] for _ in range(pointcount)]          # Clear Output list that new Output bits will be placed into.
    for i in range(pointcount):                     # For each 'i' point in vesicle, each with connections to 'j's
        signalactivity += sum(ins[i])               # Signal activity equals total number of bits set.
    # If any of the Input bits are a 1 then keep wave going by Outputting the opposite bit pattern.
        if sum(ins[i]) > 0:                         # If there are any bits set then
            outs[i] = [int(not b) for b in ins[i]]  # all 1's in output list become 0, and all 0 becomes 1, negates.
        else:                                       # Else
            outs[i] = ins[i]                        # Output bits remain all 0.
    # If no signal activity then start wave by setting all output connections of a randomly selected place to 1.
    if signalactivity == 0:                         # If no signal acctivity at all then
        i = random.randint(0, pointcount-1)         # pick one of the cells at random.
        outs[i] = [1] * len(connect[i])             # Set all its outputs to 1, to fire action potentials.
        signalactivity = len(connect[i])            # Signal activity equals number of bits that were just set.
    # If too much uncontrolled signal activity then clear all outputs.
    if signalactivity > pointcount*2:               # If more than average two connections per cell then
        for i in range(pointcount):                 # for all cells
            outs[i] = [0] * len(outs[i])            # fill list with 0's, same length as there are out connections.
        print("All outs set 0 to end Epileptic type overactivity from broken/rewiring connections.")


#######################################################################################################################
    # Start program.
try:
    pos = csvread("xyzStartup.txt")
    pointcount = len(pos)
except IOError:
    # Randomly place points anywhere in array list. Points will later become attached to surface of sphere.
    # Example of 392 is from dimple count of a 1950's Ben Hogan-5 golfball.
    # It has unusually uniform symmetry, as though well planned to close pack dimples neatly around a sphere.
    pointcount = 392
    pos = [[0, 0, 0] for _ in range(pointcount)]    # Positions of points.
    for i in range(pointcount):
        az = random.uniform(-Pi, Pi)                # Guess fractional number between -3.14 to +3.14 for azimuth.
        el = random.uniform(-1.2, 1.2)              # Guess fractional number between -3.14 to +3.14 for elevation.
        xyz = sphere2cart(az, el, sphereradius)     # Spherical to Cartesian.
        pos[i] = xyz                                # Add x,y,z to list of positions.

sar = sas/pointcount                        # Divide for area of one Lennard-Jones radius, between points.
rbp = np.sqrt(sar/Pi)*2                     # Radius between points, for given area.
spacing = rbp-((1.2/pointcount)*rbp)        # Radius between points, spacing to also fit screen.
connectdist = spacing*1.2                   # Distance two points are touching, bumping each other.
cutoffdist = spacing*2.5                    # Distance two points are too far away to influence each other.
cutoffcalctime = 40                         # Time steps between updates of i,j points within cut-off range.
rotationstep = .02                          # Amount of rotation per timestep.
rotation = 0                                # Initialize rotation angle, for drawing all points on screen.
maxsphereradius = int(screenradius * 1.15)  # Maximum radius of sphere points are located on.
max6 = 0                                    # Maximum numbrer of particles with 6 connections, more is better.
mass = 1                                    # Mass, as in Newton's equations of motion.
dt = .3                                     # Timestep size, how (slower but) precise it moves.
epsilon = 1                                 # Lennard Jones variable, remains 1.
sigma = 1                                   # Lennard Jones variable, remains 1.
pressurestep = spacing/250                  # Amount of radius change per time step, in screen pixels.
ljlimit = 700                               # Limit to amount of force to apply to motion.
ljdivide = 300                              # Limit to amount of force to apply to motion.
ljmaxforce = ljlimit/ljdivide / 7
ljavrmax = .023                             # Maximum amount of average Lennard-Jones force to apply.
signalactivity = 0
lastsave = 500                              # Prevent too many files when saving best (5 or more per point) symmetry.
fnd = 1                                     # Calculate again right away after starting.
dialog = "Squeezing"                        # Set to begin squeeze cycle by changing dialog.
moreless = ""                               # Clear "More" or "Less" dialog.

# Initialize global lists
vel = [[0, 0, 0] for _ in range(pointcount)]        # Velocities.
acc = [[0, 0, 0] for _ in range(pointcount)]        # Accelerations.
force = [[0, 0, 0] for _ in range(pointcount)]      # Amount of Lennard-Jones force to apply to velocity.
ljsum = [0 for _ in range(pointcount)]              # Clear sum used for color and brightness.
xyzc = [[] for _ in range(pointcount)]              # X,Y and color to draw on screen.
unsortedxyzc = [[] for _ in range(pointcount)]      # Saved before sorting.
connect = [[0] for _ in range(pointcount)]          # List with list of neighboring point numbers to connect to.
connectto = []                                      # List with list of neighboring point numbers to connect to.
outs = [[] for _ in range(pointcount)]              # Output (action potentials) bits, to each 1 bit connection.
ins = [[] for _ in range(pointcount)]               # Input (action potential) bits, from each 1 bit connection.

# PyGame Initialization
pygame.init()
screen = pygame.display.set_mode(screensize)
pygame.display.set_caption("Vesicle Maker Program. Gary Gaulin, 2020")
fps = pygame.time.Clock()
paused = False

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        if event.type == pygame.KEYUP:
            if event.key == pygame.K_SPACE:
                paused = not paused
            if event.key == pygame.K_s:
                csvwrite(pos, 'xyzStartup.txt')
                screen.fill(White)  # Make screen background flash white to indicate file save.
    if not paused:
        timestepnumber += 1
        # Update positions of points, and rotation to drawn on screen.
        fnd += 1            # Advance count for next update to nearby within cutoff range list.
        if fnd > (cutoffcalctime * (1/(2000/timestepnumber))) or fnd > 100:     # Updates less often over time.
            fnd = 0                                                             # Start new count.
            near, nearcount = update_nearbylist()                               # Update nearby, within cutoff range.
        pos, vel, acc = update(pointcount, pos, vel, force, acc, mass, dt)      # Update xyz location of each location.
        force, ljavr, ljmax, ljmin, ljsum, dn = forces(pointcount, pos, vel, mass)  # Forces and connections
        timestepsignals()
        # Squeeze by reducing radius of sphere, by average L-J force amount.
        if dialog == "Squeezing":
            if ljavr < ljavrmax and ljmax < ljmaxforce:
                sphereradius -= pressurestep  # Decrease sphere radius, increase pressure.
                moreless = "More, "  # Add to dialog as "Squeezing More".
            else:
                sphereradius += pressurestep  # Increase sphere radius, decrease pressure.
                moreless = "Less, "  # Add to dialog as "Squeezing Less".
        if sphereradius > maxsphereradius:
            sphereradius = maxsphereradius
        print(timestepnumber, "  distribution 0-9=", dn, dialog, moreless, "  R=", sphereradius, "  LJavr=", ljavr, "  LJmax =", ljmax, "  LJmin =", ljmin)
    # Print everything to screen.
        render()                            # Draw to screen using currently set background color.
        screen.fill(Black)                  # Normally black background, gets set to render in white by file save.
        rotation += rotationstep            # Add to screen rotation amount.
        if rotation > Radian:               # Keep in range from 0 to 1 Radian
            rotation = 0

The timestep code for starting waves and signal propagation was reduced down to only this:

def timestepsignals():
    signalactivity = 0
    global outs
    ins = [[] for _ in range(pointcount)]           # Clear Input list output bits from neighbors are gathered into.
    for i in range(pointcount):                     # For each 'i' point in vesicle, each with connections to 'j's
        for j in range(len(connect[i])):            # For each of the (usually 6) 'j' connections to other cells,
            if j < len(outs[i]):                    # If output connection still exists in connection list then
                b = outs[i][j]                      # bit to save is from output list.
            else:                                   # Else
                b = 0                               # to prevent program error append with 0 so any bit fills void.
            ins[connect[i][j]].append(b)            # Append bit from 'j' output to corresponding input line at 'i'
    # Use gathered Input bits to set corresponding Output list bits.
    outs = [[] for _ in range(pointcount)]          # Clear Output list that new Output bits will be placed into.
    for i in range(pointcount):                     # For each 'i' point in vesicle, each with connections to 'j's
        signalactivity += sum(ins[i])               # Signal activity equals total number of bits set.
    # If any of the Input bits are a 1 then keep wave going by Outputting the opposite bit pattern.
        if sum(ins[i]) > 0:                         # If there are any bits set then
            outs[i] = [int(not b) for b in ins[i]]  # all 1's in output list become 0, and all 0 becomes 1, negates.
        else:                                       # Else
            outs[i] = ins[i]                        # Output bits remain all 0.
    # If no signal activity then start wave by setting all output connections of a randomly selected place to 1.
    if signalactivity == 0:                         # If no signal acctivity at all then
        i = random.randint(0, pointcount-1)         # pick one of the cells at random.
        outs[i] = [1] * len(connect[i])             # Set all its outputs to 1, to fire action potentials.
        signalactivity = len(connect[i])            # Signal activity equals number of bits that were just set.
    # If too much uncontrolled signal activity then clear all outputs.
    if signalactivity > pointcount*2:               # If more than average two connections per cell then
        for i in range(pointcount):                 # for all cells
            outs[i] = [0] * len(outs[i])            # fill list with 0's, same length as there are out connections.
        print("All outs set 0 to end Epileptic type overactivity from broken/rewiring connections.")

Now it’s possible to see the signal dynamics early 1 membrane cell colonies would have to work with, when they only had one input and output connection to share with each of their neighbors. Such a thing would not fossilize like bone but from what I read microscopic traces suggest multicellular animals began as mostly undifferentiated blobs. A common example (among many) the volvox includes traits of plants yet has animal-like internal reproduction:

All this in turn brings us back to the old HTM challenge Matt started that sent me looking for a way to not need hexagonal (or even diagonal) math only good for flat surfaces anyway and has so many methods it could take months or never to settle “on the right one”, which had me back to forming vesicles as a possible way to in the long run save time getting something to on its own explore and recall a simple pygame environment.

For this forum the question is now how to make each cell a predictive “thousand brains” element where each has an “eye spot” for through-straw view of the world around them that moves as they do, so they end up together controlling where they are looking, move in a way all get a good look around them in all or preferred directions. From there scale up to roundish human cortices.

Whatever it takes to get a brain-ball to explore an environment can be a significant clue to the underlying cognitive biology of the most complex brains. Where everything is working right the system goes wherever most of the cells want to go, on its own explores attracting locations with a single cilia hair that beats in response to network wave flow, each cell has some control over by propagating as usual or not. In any case this project led back to the old project Matt started, with at least a novel platform for a 392 sensory unit virtual critter that requires no code to randomly move it around or complex visual and other cortical areas to worry about.

I did not plan when this final stage of the long project would be completed but Python made it easy to work with this kind of bit network, so I ahead of schedule had the vesicle lit up with signal activity, to help celebrate in a New Year with. For at least myself it’s a sigh of relief to have all that behind me, and can now focus on the next step from here.

5 Likes