Simple feedback loops in SynaptiFlux

Simple Feedback Loops

I have been steadily adding features to my SynaptiFlux toy neuron/synapse model. In my most recent work I have been considering feedback loops, since I think there is a lot of scope for interesting structures using feedback. Though in this post I will only consider a handful of simple feedback loops. If anyone has ideas for more interesting structures, please let me know, and I will try to implement them.

In this post I will be using my .map notation, which currently has two types of learn rules:

synapse(s) => neuron(s)
neuron(s) |=> synapse(s)

It is not yet clear if I need more types of learn rules, but these are sufficiently powerful for now. Further, I have a script run_map.py that processes these learn rules, and has a handful of available commands:

  • print: prints the given string
  • poke: pokes the given neurons, specified in SDB ket/superposition/sequence style
  • poke-list: pokes the given neurons, specified in Python list style
  • poke-string: pokes the string, converted to a list of characters
  • update: update the system by the given integer number of time steps
  • print-global-sequences: print our NeuralModule global sequences (this is out of the scope for this post)
  • exit: exit the current map file

where poking means activating the given neuron for 1 time step, independent of any other input at that time step. The idea is you build some neural structure, and then you poke into that system to start it going, or to stop it.

Examples

Now, on to our simple feedback loops, though I also call them latches, since once they latch on they tend to stay latched on. My test cases are in our latching.map file.

simple latches

The first example is the simplest, feed the output of a synapse back into the input of a neuron. The .map is given by:

|simple latch> => |simple latch on neuron>
|simple latch on neuron> |=> |simple latch>
|simple latch off neuron> |=> -1 |simple latch>

where we poke the on neuron to switch on the latch, and the off neuron to switch off the latch. Basically the simplest kind of feedback loop. Something to note here is -1 |simple latch> which inhibits the simple latch neuron, and so breaks the feedback loop. Our code required a very small tweak to enable inhibition, but it seems to be working :slight_smile:. Given this code

update: 5
print:
print: Switch on simple latch:
poke: |simple latch on neuron>
update: 5
print:
print: Switch off simple latch:
poke: |simple latch off neuron>
update: 5

we have the following output:

update: 5

Switch on simple latch:
poke-list: [['simple latch on neuron']]
update: 5
1: 5)    simple latch
1: 6)    simple latch
1: 7)    simple latch
1: 8)    simple latch
1: 9)    simple latch

Switch off simple latch:
poke-list: [['simple latch off neuron']]
update: 5
1: 10)    simple latch
update: 5

where it takes one time step for the latch to switch off, and then it stays off. Also, we should note our lines printed here have this structure

{current-layer}: {current-time-step}) {neuron-name}

where if the input pattern to a neuron has a maximum layer number of n, then the neuron is given the layer number n+1, and time step is the number of time steps our system has evolved.

Alternating or periodic latches

The next latch type is alternating or periodic, it still feedback’s on to itself like the simple latch, but only after a given delay. We do this using:

|alternating latch> => |alternating latch on neuron>
|alternating latch on neuron> |=> |> . |> . |> . |alternating latch>
|alternating latch off neuron> |=> -1 |alternating latch>

where

|> . |> . |> . |alternating latch>

is SDB style sequence notation, with meaning: do nothing for three time steps, and then activate the alternating latch. We invoke and then switch off this latch using this code:

update: 5
print:
print: Switch on an alternating latch with period 4:
poke: |alternating latch on neuron>
update: 20
print:
print: Switch off alternating latch (NB: the phase must be correct for this to switch off the neuron):
update: 3
poke: |alternating latch off neuron>
update: 20

which produces the following output:

Switch on an alternating latch with period 4:
poke-list: [['alternating latch on neuron']]
update: 20
1: 23)    alternating latch
1: 27)    alternating latch
1: 31)    alternating latch
1: 35)    alternating latch
1: 39)    alternating latch

Switch off alternating latch (NB: the phase must be correct for this to switch off the neuron):
update: 3
poke-list: [['alternating latch off neuron']]
update: 20
1: 43)    alternating latch

Noting:

  • the time steps are increasing by 4, instead of 1
  • we have to line up the phase of the switch off poke, otherwise the latch stays on

Poke buffers

We can also repurpose alternating latches as poke buffers. This is a buffer of given length, say 10, in to which we can poke binary values. Then every 10 steps, it outputs the elements that have been poked on, and will continue to do so until you poke off each element, again, while getting the phase exactly right. The buffer is given by:

|poke buffer 10> => |poke buffer 10 on neuron>
|poke buffer 10 on neuron> |=> |> . |> . |> . |> . |> . |> . |> . |> . |> . |poke buffer 10>
|poke buffer 10 off neuron> |=> -1 |poke buffer 10>

which is simply an alternating latch with period 10. Then we invoke it using this code:

print:
print: Switch on an element in the poke buffer of length 10:
poke: |poke buffer 10 on neuron>
update: 3
print: Switch on another element in the poke buffer:
poke: |poke buffer 10 on neuron>
update: 1
print: Switch on another element:
poke: |poke buffer 10 on neuron>
update: 30
print:
print: Switching off the poke buffer currently requires we poke at just the right time-steps in the sequence:
update: 5
poke: |poke buffer 10 off neuron>
update: 3
poke: |poke buffer 10 off neuron>
update: 1
poke: |poke buffer 10 off neuron>
update: 30

where we poked in a value, then waited 3 time steps, poked in a value, waited another time step, and then poked in a third value. On reflection I probably could have poked the buffer in one line:

poke: |poke buffer 10 on neuron> . |> . |> . |> . |poke buffer 10 on neuron> . |poke buffer 10 on neuron>

Regardless, here is the output:

Switch on an element in the poke buffer of length 10:
poke-list: [['poke buffer 10 on neuron']]
update: 3
Switch on another element in the poke buffer:
poke-list: [['poke buffer 10 on neuron']]
update: 1
Switch on another element:
poke-list: [['poke buffer 10 on neuron']]
update: 30
1: 72)    poke buffer 10
1: 75)    poke buffer 10
1: 76)    poke buffer 10
1: 82)    poke buffer 10
1: 85)    poke buffer 10
1: 86)    poke buffer 10
1: 92)    poke buffer 10
1: 95)    poke buffer 10
1: 96)    poke buffer 10

Switching off the poke buffer currently requires we poke at just the right time-steps in the sequence:
update: 5
poke-list: [['poke buffer 10 off neuron']]
update: 3
1: 102)    poke buffer 10
poke-list: [['poke buffer 10 off neuron']]
update: 1
1: 105)    poke buffer 10
poke-list: [['poke buffer 10 off neuron']]
update: 30
1: 106)    poke buffer 10

The key takeaway is that the output repeats itself every 10 time steps, until we unpoke the elements in the buffer.

temporary simple latches

The next latch type is a simple latch, that switches itself off after some number of time steps. This is defined using:

|temporary simple latch> => |temporary simple latch on neuron>
|temporary simple latch on neuron> |=> |temporary simple latch> . |> . |> . |> . |> . |> . -1 |temporary simple latch>
|temporary simple latch off neuron> |=> -1 |temporary simple latch>

And invoked using:

print:
print: Switch on temporary simple latch:
poke: |temporary simple latch on neuron>
update: 20

with corresponding output:

Switch on temporary simple latch:
poke-list: [['temporary simple latch on neuron']]
update: 20
1: 136)    temporary simple latch
1: 137)    temporary simple latch
1: 138)    temporary simple latch
1: 139)    temporary simple latch
1: 140)    temporary simple latch
1: 141)    temporary simple latch
1: 142)    temporary simple latch

So even though we update the system 20 time steps, the latch only stays on for 7 time steps. The desired number of time steps is tweakable by changing the number of empty kets |> in our sequence on the right hand side of the |temporary simple latch on neuron> learn rule.

temporary alternating latches

The same approach can be used for alternating latches:

|temporary alternating latch> => |temporary alternating latch on neuron>
|temporary alternating latch on neuron> |=> |> . |> . |> . |temporary alternating latch> . |> . |> . |> . |> . |> . |> . |> . -1 |temporary alternating latch>
|temporaryalternating latch off neuron> |=> -1 |temporary alternating latch>

Invoked using:

print:
print: Switch on temporary alternating latch:
poke: |temporary alternating latch>
update: 50

with corresponding output:

Switch on temporary alternating latch:
poke-list: [['temporary alternating latch']]
update: 50
0: 156)    temporary alternating latch
1: 160)    temporary alternating latch
1: 164)    temporary alternating latch
1: 168)    temporary alternating latch

length 2 neuron chains, with multiple activation choices

This one is slightly more interesting. We define a length 2 neuron chain, and then define a collection of neurons that can activate that chain, and a single neuron to switch off the chain. The code is given by:

|A> => |H>
|H> => |A>
|H1> |=> |H>
|H2> |=> |H>
|H3> |=> |H>
|H4> |=> |H>
|H5> |=> |H>
|H off> |=> -1 |H>

where

  • A activates H
  • H activates A
  • A activates H, and so on
  • any of H1, H2, H3, H4, H5 can activate H
  • H off inhibits the H neuron

This is invoked using:

print:
print: Switch on A by poking H3:
poke: |H3>
update: 10
print: Switch off A by poking |H off>:
poke: |H off>
update: 10

with output:

Switch on A by poking H3:
poke-list: [['H3']]
update: 10
0: 206)    H
2: 207)    A
1: 208)    H
2: 209)    A
1: 210)    H
2: 211)    A
1: 212)    H
2: 213)    A
1: 214)    H
2: 215)    A
Switch off A by poking |H off>:
poke-list: [['H off']]
update: 10
1: 216)    H

where it takes 1 time step after poking H off for the sequence to stop.

neuron chain

Finally, we have a neuron chain. Each neuron activates the next one, and the final one invokes the initial neuron. The code is given by:

|B> => |F1>
|F1> => |F2>
|F2> => |F3>
|F3> => |F4>
|F4> => |F5>
|F5> => |F6>
|F6> => |B>

with the property that we can activate the full chain by poking any member of the chain. I haven’t tested it, but presumably switching off the chain would require getting the phase just right. This is invoked using:

print:
print: Switch on B feedback loop by poking F2:
poke: |F2>
update: 20

with output:

Switch on B feedback loop by poking F2:
poke-list: [['F2']]
update: 20
2: 226)    F2
3: 227)    F3
4: 228)    F4
5: 229)    F5
6: 230)    F6
7: 231)    B
1: 232)    F1
2: 233)    F2
3: 234)    F3
4: 235)    F4
5: 236)    F5
6: 237)    F6
7: 238)    B
1: 239)    F1
2: 240)    F2
3: 241)    F3
4: 242)    F4
5: 243)    F5
6: 244)    F6
7: 245)    B

Also note we have different layer numbers (the number in front of the colon) for each of the chain members, which makes sense. Where a layer number is a measure of how deep a neuron is in the neural module. As I said above, if a neuron has synaptic inputs with a max layer number of n, then that neuron is given the layer number n+1.

Future?

I have some thoughts on how these will be useful later (eg, for switching modes on and off), so more later. But again, if anyone has proposals for more interesting feedback loops, please post, and I will see if I can implement them. The above are foundational examples, and many more structures are possible!

Counting latch

OK, this morning I implemented a new feedback latch type, the counting latch, which is a variant of a temporary simple latch. The idea is you activate the 0 neuron and it stays on for 5 steps, then the 1 neuron is triggered, and stays on for 5 steps, and so on through the digits. Eventually the 5 neuron activates the 0 neuron, and the process repeats indefinitely. Note that the exact number of on time steps for each neuron/synapse is tweakable, we just chose a fixed 5 time steps as an example.

Now, recall our rules have two types:

synapse(s) => neuron(s)
neuron(s) |=> synapse(s)

The full counting-latch.map file is here, and our first set of rules, for digit 0, are the following:

|0> => |0 on>
|5> . |5> . |5> . |5> . |> => |0 on>
|0 on> |=> |0> . |> . |> . |> . -1 |0>

where

  • the 0 synapse activates the 0 on neuron
  • a sequence of 4 time steps of the 5 synapse, plus 1 time step of anything, activates the 0 on neuron (it is not currently clear why the 0 digit required the 1 time step of anything, while the rest of our digits did not)
  • the 0 on neuron activates the 0 synapse, takes 3 time steps, and then switches it off again

The rest of the rules for our other digits follow similarly, though not quite identically. For example, lets pick digit 3:

|3> => |3 on>
|2> . |2> . |2> . |2> => |3 on>
|3 on> |=> |3> . |> . |> . |> . -1 |3>

where the only difference from the 0 case is we do not require the 1 time step delay in line 2. Ditto the the rest of our numbers. I’m not totally sure why at this stage.

Here is the full output of our code:

Testing a counting latch ...
----------------------------

poke-list: [['0 on']]
update: 50
1: 0)    0
1: 1)    0
1: 2)    0
1: 3)    0
1: 4)    0
3: 4)    1
3: 5)    1
3: 6)    1
3: 7)    1
3: 8)    1
5: 8)    2
5: 9)    2
5: 10)    2
5: 11)    2
5: 12)    2
7: 12)    3
7: 13)    3
7: 14)    3
7: 15)    3
7: 16)    3
9: 16)    4
9: 17)    4
9: 18)    4
9: 19)    4
9: 20)    4
11: 20)    5
11: 21)    5
11: 22)    5
11: 23)    5
11: 24)    5
1: 25)    0
1: 26)    0
1: 27)    0
1: 28)    0
1: 29)    0
3: 29)    1
3: 30)    1
3: 31)    1
3: 32)    1
3: 33)    1
5: 33)    2
5: 34)    2
5: 35)    2
5: 36)    2
5: 37)    2
7: 37)    3
7: 38)    3
7: 39)    3
7: 40)    3
7: 41)    3
9: 41)    4
9: 42)    4
9: 43)    4
9: 44)    4
9: 45)    4
11: 45)    5
11: 46)    5
11: 47)    5
11: 48)    5
11: 49)    5


Global sequences:
    1:    |0> . |0> . |0> . |0> . |0> . |0> . |0> . |0> . |0> . |0>
    3:    |1> . |1> . |1> . |1> . |1> . |1> . |1> . |1> . |1> . |1>
    5:    |2> . |2> . |2> . |2> . |2> . |2> . |2> . |2> . |2> . |2>
    7:    |3> . |3> . |3> . |3> . |3> . |3> . |3> . |3> . |3> . |3>
    9:    |4> . |4> . |4> . |4> . |4> . |4> . |4> . |4> . |4> . |4>
    11:    |5> . |5> . |5> . |5> . |5> . |5> . |5> . |5> . |5> . |5>

where:

  • the number just before the colon is the layer number of the given synapse
  • the number just before the ) char is the system time step
  • the numbers after the ) char are our synapse labels
  • the print-global-sequences: command then outputs the sequences of synapse labels for the given layers, using SDB style sequence notation.

Perhaps we should mention a little of how layer numbers are calculated in .map format. Say you have these two simple rules:

|S0> => |N1>
|N2> |=> |S3>

then we use:

layer(N1) = max(layer(N1), layer(S0) + 1)
layer(S3) = max(layer(S3), layer(N2) + 1)

More generally, if there is more than one element on the LHS of a rule, then we take the max layer number of those elements.

Edit

Ahh… it finally occurred to me the cause of the difference between the 0 case and the rest. Turns out they all need the extra . |> term in line 2. I hadn’t noticed that the transition from one digit to the next was actually occurring at the same time step. Now with the extra empty ket in there, all synapses are on their own unique time step. So for example, the updated rules for digit 2 are:

|2> => |2 on>
|1> . |1> . |1> . |1> . |> => |2 on>
|2 on> |=> |2> . |> . |> . |> . -1 |2>

The full output is now:

Testing a counting latch ...
----------------------------

poke-list: [['0 on']]
update: 60
1: 0)    0
1: 1)    0
1: 2)    0
1: 3)    0
1: 4)    0
3: 5)    1
3: 6)    1
3: 7)    1
3: 8)    1
3: 9)    1
5: 10)    2
5: 11)    2
5: 12)    2
5: 13)    2
5: 14)    2
7: 15)    3
7: 16)    3
7: 17)    3
7: 18)    3
7: 19)    3
9: 20)    4
9: 21)    4
9: 22)    4
9: 23)    4
9: 24)    4
11: 25)    5
11: 26)    5
11: 27)    5
11: 28)    5
11: 29)    5
1: 30)    0
1: 31)    0
1: 32)    0
1: 33)    0
1: 34)    0
3: 35)    1
3: 36)    1
3: 37)    1
3: 38)    1
3: 39)    1
5: 40)    2
5: 41)    2
5: 42)    2
5: 43)    2
5: 44)    2
7: 45)    3
7: 46)    3
7: 47)    3
7: 48)    3
7: 49)    3
9: 50)    4
9: 51)    4
9: 52)    4
9: 53)    4
9: 54)    4
11: 55)    5
11: 56)    5
11: 57)    5
11: 58)    5
11: 59)    5


Global sequences:
    1:    |0> . |0> . |0> . |0> . |0> . |0> . |0> . |0> . |0> . |0>
    3:    |1> . |1> . |1> . |1> . |1> . |1> . |1> . |1> . |1> . |1>
    5:    |2> . |2> . |2> . |2> . |2> . |2> . |2> . |2> . |2> . |2>
    7:    |3> . |3> . |3> . |3> . |3> . |3> . |3> . |3> . |3> . |3>
    9:    |4> . |4> . |4> . |4> . |4> . |4> . |4> . |4> . |4> . |4>
    11:    |5> . |5> . |5> . |5> . |5> . |5> . |5> . |5> . |5> . |5>

which if you look closely, now takes 60 time steps to cycle through our 6 digits, twice over, 5 time steps each digit. Compared to the previous 50 time steps, for our buggy version.