NYCJUG/2022-03-08

From J Wiki
Jump to navigation Jump to search

Meeting Agenda for NYCJUG 20220308

Beginner's regatta

Counting Things in Order

Let's say we want to count the letters in a piece of text.

   text=. 'Fourscore and seven years ago, our fathers brought forth upon this continent a new nation conceived in liberty and dedicated to the proposition that all men are created equal.'
   #/.~text
1 14 5 11 6 6 18 28 13 14 7 2 2 2 1 2 15 6 2 3 9 1 4 1 1 1

We counted something but this looks like a useless answer unless we can link the counts to what is being counted. Since what we mean to count are the unique items, what are these?

   ~.text
Foursce andvyg,fthbpiwlmq.

The Key Piece

The key to this problem is using key (/.). Key is an adverb that takes two or three arguments. is most concisely defined here.

x u/.y ↔ (=x) u@# y , that is, items of x specify keys for corresponding items of y and u is applied to each collection of y having identical keys.

In this case, our verb u is tally (#); both x and y are the same thing: we are using the letters in the text as the keys to the letters in the text.

Let's also append the unique letters to the counts to clarify the relation between the two.

   (<"0 ~.text),:<"0 #/.~text
+-+--+-+--+-+-+--+--+--+--+-+-+-+-+-+-+--+-+-+-+-+-+-+-+-+-+
|F|o |u|r |s|c|e |  |a |n |d|v|y|g|,|f|t |h|b|p|i|w|l|m|q|.|
+-+--+-+--+-+-+--+--+--+--+-+-+-+-+-+-+--+-+-+-+-+-+-+-+-+-+
|1|14|5|11|6|6|18|28|13|14|7|2|2|2|1|2|15|6|2|3|9|1|4|1|1|1|
+-+--+-+--+-+-+--+--+--+--+-+-+-+-+-+-+--+-+-+-+-+-+-+-+-+-+

Comparing Counts of Different Texts

There, done, but maybe not entirely. What if we want to compare these counts to those for another piece of text which has a different set of unique letters?

   text2=. 'Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure.'

Let's tacitly define our expression above to line up the unique items with their counts.

   (([: <"0 ~.) ,: [: <"0 #/.~) text2     NB. Summarize the above, avoid duplicating the argument
+-+-+-+--+--+--+-+--+-+-+-+-+-+-+-+-+-+-+-+-+-+
|N|o|w|  |e |a |r|n |g|d|i|t|c|v|l|,|s|h|y|u|.|
+-+-+-+--+--+--+-+--+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1|8|4|23|14|12|6|13|5|7|8|9|5|2|2|3|3|3|1|1|1|
+-+-+-+--+--+--+-+--+-+-+-+-+-+-+-+-+-+-+-+-+-+

We must have the alphabet already defined somewhere...

   names_j_ 0
Alpha           AlphaNum        Boxes           Browser         
Browser_nox     CasePaths       Cwh             DirTreeX        
Displayload     EPSReader       Editor          Editor_nox      
...                                   
   Alpha_j_
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

We have both Alpha and AlphaNum in the j namespace.

Let's use this alphabet to order and restrict the domain of what we are counting; also, let's work just with uppercase letters.

   ALPH=: 26{.Alpha_j_
   sumIC=: (([: <"0 ~.) ,: [: <"0 #/.~) NB. Summarize items, counts

We also have a standard verb to convert text to uppercase:

   toupper
3 : 0`((a. (97+i.26)}~'ABCDEFGHIJKLMNOPQRSTUVWXYZ') { ~ a. i. ])@.(2 = 3!:0)
x=. I. 26 > n=. ((97+i.26){a.) i. t=. ,y
($y) $ ((x{n) { (65+i.26){a.) x}t       
)

We define a verb to uppercase our argument and eliminate any characters not in our uppercase alphabet.

   restrictDomain=: (26{.Alpha_j_)&(]-. -.~)@:toupper
   restrictDomain text
FOURSCOREANDSEVENYEARSAGOOURFATHERSBROUGHTFORTHUPONTHISCONTINENTANEWNATIONCONCEIVEDINLIBERTYANDDEDICATEDTOTHEPROPOSITIONTHATALLMENARECREATEDEQUAL

The (]-. -.~) trick is a good one. Explicitly it looks like this: 3 : 'y-.y-.x' . This removes all the items of x from y, giving us all the characters we want to dis-allow, then removes those from y so we end up with only the things in y we want. This way we only have to specify what we need, not what we don't need. So, in our use of it, we remove all the uppercase letters from our text to specify all the characters we don't want, then remove these from our text so we have only uppercase letters remaining.

Finishing Up

Bringing together the definitions above and applying them to both our texts:

   sumIC&>restrictDomain&.>text;text2
+--+--+-+--+--+-+--+--+--+-+-+-+-+--+-+-+-+-+-+-+-+-+
|F |O |U|R |S |C|E |A |N |D|V|Y|G|T |H|B|P|I|W|L|M|Q|
+--+--+-+--+--+-+--+--+--+-+-+-+-+--+-+-+-+-+-+-+-+-+
|3 |14|5|11|6 |6|18|13|14|7|2|2|2|15|6|2|3|9|1|4|1|1|
+--+--+-+--+--+-+--+--+--+-+-+-+-+--+-+-+-+-+-+-+-+-+

+--+--+-+--+--+-+--+--+--+-+-+-+-+--+-+-+-+-+-+-+-+-+
|N |O |W|E |A |R|G |D |I |T|C|V|L|S |H|Y|U| | | | | |
+--+--+-+--+--+-+--+--+--+-+-+-+-+--+-+-+-+-+-+-+-+-+
|14|8 |4|14|12|6|5 |7 |8 |9|5|2|2|3 |3|1|1| | | | | |
+--+--+-+--+--+-+--+--+--+-+-+-+-+--+-+-+-+-+-+-+-+-+

This points up another problem: we are obscuring the common basis on which we want to count. We can enforce this common basis by appending the letters we want to count to each text list, counting their occurrences, then subtracting one from the counts to reduce by the common basis we added, one of each.

   sumIC=: (26{.Alpha_j_)&(13 : '(<"0 x),:<"0 <:#/.~ x,y')   NB. Summarize items, counts for uppercase letters.
   sumIC&>restrictDomain&.>text;text2
+--+-+-+-+--+-+-+-+-+-+-+-+-+--+--+-+-+--+-+--+-+-+-+-+-+-+
|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|
+--+-+-+-+--+-+-+-+-+-+-+-+-+--+--+-+-+--+-+--+-+-+-+-+-+-+
|13|2|6|7|18|3|2|6|9|0|0|4|1|14|14|3|1|11|6|15|5|2|1|0|2|0|
+--+-+-+-+--+-+-+-+-+-+-+-+-+--+--+-+-+--+-+--+-+-+-+-+-+-+

+--+-+-+-+--+-+-+-+-+-+-+-+-+--+--+-+-+--+-+--+-+-+-+-+-+-+
|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|
+--+-+-+-+--+-+-+-+-+-+-+-+-+--+--+-+-+--+-+--+-+-+-+-+-+-+
|12|0|5|7|14|0|5|3|8|0|0|2|0|14|8 |0|0|6 |3|9 |1|2|4|0|1|0|
+--+-+-+-+--+-+-+-+-+-+-+-+-+--+--+-+-+--+-+--+-+-+-+-+-+-+

Now we can count different sets of items but easily compare them on a common basis.

Show-and-tell

Dan showed us some work he's done on HackerRank that showcases the beauty of J.

The Beauty of J

Both solutions are examples of the beauty of J (and other array languages).

Specifically, being able to work with an array or matrix of data without first knowing its size.

HackerRank functions J script Media:NYCJUG_2022-03-08_HackerRank.ijs and test data Media:Test_Data_f_HackerRank.txt

HackerRank Array Manipulation

https://www.hackerrank.com/challenges/crush/problem Media:crush-English.pdf

Starting with a 1-indexed array of zeros and a list of operations, for each operation add a value to each the array element between two given indices, inclusive. Once all operations have been performed, return the maximum value in the array.

This really looks like something better done as a matrix.


From the HackerRank problem: (the problem also included a first line giving the maximum number of columns, and the number of operations--which will become rows. We will see that using a matrix we do not need this information)

1 5 3
4 8 7
6 9 1

The matrix above becomes this:

3 3 3 3 3 0 0 0 0
0 0 0 7 7 7 7 7 0
0 0 0 0 0 1 1 1 1

We'll do it line by line

In the first line (1 5 3) positions 1 through 5 will be 3's

We can look at this as making a vector of 5 3's, 1 is added to the difference of the 2 numbers and is used to reshape the last number.

   (>: -~/ 1 5) $ 3
3 3 3 3 3

We may need to add leading 0's (for lines that don't start with 1)

   _8{. (>: -~/ 4 8) $ 7
0 0 0 7 7 7 7 7

Writing this as a function:

   {{(- 1 { y) {. (>: -~/ 2{. y) $ (2 { y)}} 4 8 7


]abk=. 3 3  $  1 5 3   4 8 7   6 9 1
   1 5 3
   4 8 7
   6 9 1


{{(- 1 { y) {. (>: -~/ 2{. y) $ (2 { y)}} each < " 1 abk
   ┌─────────┬───────────────┬─────────────────┐
   │3 3 3 3 3│0 0 0 7 7 7 7 7│0 0 0 0 0 1 1 1 1│
   └─────────┴───────────────┴─────────────────┘

We don't need to know the max size, we just unbox. The number of rows is dictated by the number of rows from the input.

>{{(- 1 { y) {. (>: -~/ 2{. y) $ (2 { y)}} each < " 1 abk
   3 3 3 3 3 0 0 0 0
   0 0 0 7 7 7 7 7 0
   0 0 0 0 0 1 1 1 1

Then it's plus reduce and scan for the max value

>./ +/ >{{(- 1 { y) {. (>: -~/ 2{. y) $ (2 { y)}} each < " 1 abk
   10

As a function:

solveCrush=: {{>. / +/ > {{(- 1 { y) {. (>: -~/ 2{. y) $ (2 { y)}} each < " 1 y}}

solveCrush 1 2 100,2 5 100,:3 4 100
   200

Code to show the steps: (we'll use infix to allow the data to be passed as a vector instead of a matrix)

showSteps=: {{ {{ ,. (< y), (<>y), (<+/>y), (< >./ +/ >y)}} {{(- 1 { y) {. (>: -~/ 2{. y) $ (2 { y)}} each _3 <\ y}}

First, we'll use our matrix, abk

   showSteps ,abk
┌─────────────────────────────────────────────┐
│┌─────────┬───────────────┬─────────────────┐│
││3 3 3 3 3│0 0 0 7 7 7 7 7│0 0 0 0 0 1 1 1 1││
│└─────────┴───────────────┴─────────────────┘│
├─────────────────────────────────────────────┤
│3 3 3 3 3 0 0 0 0                            │
│0 0 0 7 7 7 7 7 0                            │
│0 0 0 0 0 1 1 1 1                            │
├─────────────────────────────────────────────┤
│3 3 3 10 10 8 8 8 1                          │
├─────────────────────────────────────────────┤
│10                                           │
└─────────────────────────────────────────────┘

We'll do the other 2 sample inputs


   showSteps 1 2 100 2 5 100 3 4 100
┌───────────────────────────────────────┐
│┌───────┬─────────────────┬───────────┐│
││100 100│0 100 100 100 100│0 0 100 100││
│└───────┴─────────────────┴───────────┘│
├───────────────────────────────────────┤
│100 100   0   0   0                    │
│  0 100 100 100 100                    │
│  0   0 100 100   0                    │
├───────────────────────────────────────┤
│100 200 200 200 100                    │
├───────────────────────────────────────┤
│200                                    │
└───────────────────────────────────────┘
   showSteps 2 6 8 3 5 7 1 8 1 5 9 15
┌──────────────────────────────────────────────────────────────┐
│┌───────────┬─────────┬───────────────┬──────────────────────┐│
││0 8 8 8 8 8│0 0 7 7 7│1 1 1 1 1 1 1 1│0 0 0 0 15 15 15 15 15││
│└───────────┴─────────┴───────────────┴──────────────────────┘│
├──────────────────────────────────────────────────────────────┤
│0 8 8 8  8  8  0  0  0                                        │
│0 0 7 7  7  0  0  0  0                                        │
│1 1 1 1  1  1  1  1  0                                        │
│0 0 0 0 15 15 15 15 15                                        │
├──────────────────────────────────────────────────────────────┤
│1 9 16 16 31 24 16 16 15                                      │
├──────────────────────────────────────────────────────────────┤
│31                                                            │
└──────────────────────────────────────────────────────────────┘

And for fun, we'll increase the number of rows in the previous problem from 9 to 12 (second to last item in vector)

   showSteps 2 6 8 3 5 7 1 8 1 5 12 15
┌───────────────────────────────────────────────────────────────────────┐
│┌───────────┬─────────┬───────────────┬───────────────────────────────┐│
││0 8 8 8 8 8│0 0 7 7 7│1 1 1 1 1 1 1 1│0 0 0 0 15 15 15 15 15 15 15 15││
│└───────────┴─────────┴───────────────┴───────────────────────────────┘│
├───────────────────────────────────────────────────────────────────────┤
│0 8 8 8  8  8  0  0  0  0  0  0                                        │
│0 0 7 7  7  0  0  0  0  0  0  0                                        │
│1 1 1 1  1  1  1  1  0  0  0  0                                        │
│0 0 0 0 15 15 15 15 15 15 15 15                                        │
├───────────────────────────────────────────────────────────────────────┤
│1 9 16 16 31 24 16 16 15 15 15 15                                      │
├───────────────────────────────────────────────────────────────────────┤
│31                                                                     │
└───────────────────────────────────────────────────────────────────────┘



HackerRank Encryption

https://www.hackerrank.com/challenges/encryption/problem Media:encryption-English.pdf

An English text needs to be encrypted using the following encryption scheme. First, the spaces are removed from the text. Put the characters into a matrix that is nearly square. Where the number of characters does not form a perfect square, the number of columns can be greater than the number of rows. Each column becomes a 'word'


have a nice day

becomes

haveaniceday

then a matrix

have
anic
eday

(spaced out to make the columns easier to read)

h a v e
a n i c
e d a y

and the columns become the 'words'

hae and via ecy

The problem says to take the number of characters, take the square root of that number and apply floor to get the rows, and ceiling to get the columns. (already this looks like something for J or APL.)

First perform a test to see if the rows x columns is larger than the number of characters. (an example of where this procedure fails is a number slightly smaller than a square: 8, 399, etc.)

With J, it's a bit easier. We will use the ceiling of the square root of the length to get the columns. Infix will do the rest of making the matrix.


Remove spaces

   ' ' -.~ 'if man was meant to stay on the ground god would have given us roots'
ifmanwasmeanttostayonthegroundgodwouldhavegivenusroots

To get the number of columns, round up the square root of the length

   {{>. (#y) ^ 0.5}} ' ' -.~ 'if man was meant to stay on the ground god would have given us roots'
8

Use that number to reformat into a matrix (using infix)

   {{ (- >. (#y) ^ 0.5) [ \ y }} ' ' -.~ 'if man was meant to stay on the ground god would have given us roots'
ifmanwas
meanttos
tayonthe
groundgo
dwouldha
vegivenu
sroots  

Transpose

|: {{ (- >. (#y) ^ 0.5) [ \ y }} ' ' -.~ 'if man was meant to stay on the ground god would have given us roots'
imtgdvs
fearwer
mayoogo
anouuio
ntnnlvt
wttddes
aohghn 
sseoau 

Box on rows and remove spaces again (some rows will have trailing spaces).

(' ' -.~ ]) each < " 1 |: {{ (- >. (#y) ^ 0.5) [ \ y }} ' ' -.~ 'if man was meant to stay on the ground god would have given us roots'
┌───────┬───────┬───────┬───────┬───────┬───────┬──────┬──────┐
│imtgdvs│fearwer│mayoogo│anouuio│ntnnlvt│wttddes│aohghn│sseoau│
└───────┴───────┴───────┴───────┴───────┴───────┴──────┴──────┘

Add spaces between items and raze.

; {{<(>x), ' ', (>y)}}/ (' ' -.~ ]) each < " 1  |: {{ (- >. (#y) ^ 0.5) [ \ y }} ' ' -.~ 'if man was meant to stay on the ground god would have given us roots'
imtgdvs fearwer mayoogo anouuio ntnnlvt wttddes aohghn sseoau

As a function

hackerencode=: {{; {{<(>x), ' ', (>y)}}/ (' ' -.~ ]) each < " 1  |: {{ (- >. (#y) ^ 0.5) [ \ y }} ' ' -.~ y}}


   hackerencode 'if man was meant to stay on the ground god would have given us roots'
imtgdvs fearwer mayoogo anouuio ntnnlvt wttddes aohghn sseoau
   hackerencode 'haveaniceday'
hae and via ecy
   hackerencode 'chillout'
clu hlt io

Now let's make a function to display the steps.


showEncrypt=: {{ {{,. (<y),(<|: y),(<(' ' -.~ ]) each < " 1 |: y),(<;{{<(>x), ' ', (>y)}}/ (' ' -.~ ]) each < " 1  |: y)}} {{{{ (- >. (#y) ^ 0.5) [ \ y }} ' ' -.~ y}} y}}

showEncrypt 'if man was meant to stay on the ground god would have given us roots'
┌───────────────────────────────────────────────────────────────┐
│ifmanwas                                                       │
│meanttos                                                       │
│tayonthe                                                       │
│groundgo                                                       │
│dwouldha                                                       │
│vegivenu                                                       │
│sroots                                                         │
├───────────────────────────────────────────────────────────────┤
│imtgdvs                                                        │
│fearwer                                                        │
│mayoogo                                                        │
│anouuio                                                        │
│ntnnlvt                                                        │
│wttddes                                                        │
│aohghn                                                         │
│sseoau                                                         │
├───────────────────────────────────────────────────────────────┤
│┌───────┬───────┬───────┬───────┬───────┬───────┬──────┬──────┐│
││imtgdvs│fearwer│mayoogo│anouuio│ntnnlvt│wttddes│aohghn│sseoau││
│└───────┴───────┴───────┴───────┴───────┴───────┴──────┴──────┘│
├───────────────────────────────────────────────────────────────┤
│imtgdvs fearwer mayoogo anouuio ntnnlvt wttddes aohghn sseoau  │
└───────────────────────────────────────────────────────────────┘


   showEncrypt 'have a nice day'
┌─────────────────┐
│have             │
│anic             │
│eday             │
├─────────────────┤
│hae              │
│and              │
│via              │
│ecy              │
├─────────────────┤
│┌───┬───┬───┬───┐│
││hae│and│via│ecy││
│└───┴───┴───┴───┘│
├─────────────────┤
│hae and via ecy  │
└─────────────────┘


   showEncrypt 'feed the dog'
┌───────────────┐
│feed           │
│thed           │
│og             │
├───────────────┤
│fto            │
│ehg            │
│ee             │
│dd             │
├───────────────┤
│┌───┬───┬──┬──┐│
││fto│ehg│ee│dd││
│└───┴───┴──┴──┘│
├───────────────┤
│fto ehg ee dd  │
└───────────────┘


   showEncrypt 'chill out'
┌────────────┐
│chi         │
│llo         │
│ut          │
├────────────┤
│clu         │
│hlt         │
│io          │
├────────────┤
│┌───┬───┬──┐│
││clu│hlt│io││
│└───┴───┴──┘│
├────────────┤
│clu hlt io  │
└────────────┘

Genetic Algorithm

We started to look at genetic algorithms (GAs) at an earlier meeting but punted on actually implementing one because the problem given was so easy to solve by looking at all possible solutions. However, this doesn't help someone who wants an example of a GA in J so we addressed this long overdue lack by implementing an algorithm based on the C# code referenced in the earlier meeting.

The Problem

As stated on the website:

...for this example, I have used a simple card splitting exercise, which is as detailed here:

  • You have 10 cards numbered 1 to 10
  • You have to divide them into two piles so that:
    • The sum of the first pile is as close as possible to 36.
    • And the product of all in the second pile is as close as possible to 360.

In our earlier approach to this problem, we were running short on time to implement a proper GA, so, upon noticing how very small the search space is, we instead showed how to implement a brute-force version that runs very quickly. However, implementing the GA in J was not that difficult and it highlighted some subtleties in the C# implementation we translated into J. We completed our initial implementation in J fairly quickly, in about an hour and a half, but this was due in large part to having the existing implementation to guide us through a number of ambiguities in the algorithm.

Necessity of GA as Simple Problem Grows Larger

So we see that we two fundamentally different details in our two versions: simpleGA0 more closely mimics the C# versions by works on only a pair of randomly-chosen genes at a time whereas the more J-like version - simpleGA called by iterateGA - works on the whole set of genes at once.

We see that the original universe, from the article, of only 10 numbers is trivially easy to solve without a genetic algorithm:

      SPsolution (>:i.10);36;360;5
+----------+
|2 7 8 9 10|
+----------+
|1 3 4 5 6 |
+----------+
      6!:2 'SPsolution (>:i.10);36;360;5'
0.0001558

Where SPsolution here is similar to what we came up with the last time we looked at this problem. We see that it also reaches a solution very quickly.

SPsolution=: 3 : 0
   'set sumtarg prodtarg nps'=. y  NB. Set of numbers, targets for sum and product, #/set
   set=. ,set
   cmb=. <"1 set{~nps comb #set         NB. All combinations
   ccmb=. (<set)-.&.>cmb                NB. Complement of cmb
   approxSum=. ((>:sumtarg)&>:*.(<:sumtarg)&<:)&>(+/&>cmb)
   approxProd=. ((10+prodtarg)&>:*.(prodtarg-10)&<:)&>(*/&>ccmb)
   whsoln=. I. approxProd*.approxSum
   whsoln&{&>(<cmb),<ccmb
)

However, this exhaustive methods exhausts itself pretty quickly as our universe increases in size.

   (10) 6!:2 'SPsolution (>:i.15);50;27941760'
0.0117826
   (10) 6!:2 'SPsolution (>:i.20);64;12125707776000'
0.84209
   0.84209%0.0117826
71.4689
   SPsolution (>:i.30);332;11412430848
|out of memory: SPsolution
|   pairs=.}.&.>(0 1,"1 abc)    </."1]0 0,set
|SPsolution[3]
      7!:0''
171692986240
      10^.7!:0''
11.2348

We see here that increasing the solution space from 15 to 20, an increase of 1/3, increases the time required by more than a factor of 70. Increasing the universe to 30 causes this solution to run out memory entirely. We will use the universe of size 30 for our testing to show an advantage of genetic algorithms.

Initial J Implementation

We defined the deck of number cards like this even though it turns out that the C# example never explicitly does this.

   ]CARDS=: >:i.10
1 2 3 4 5 6 7 8 9 10

However, doing it this way gives more flexible code in case we want to try other similar examples whereas changing the C# example would require code changes in multiple places.

In keeping with our idea of closely mimicking the example, we first define a number of global variables:

   TARGETS=: 36 360    NB. C# code does not do this: it hard-codes the two values throughout the code.
   'POP LEN MUT REC END'=: 30,(#CARDS),0.1,0.5,1000  NB. 

Per the article, these are:

  • POP: Size of gene pool
  • LEN: length of genome
  • MUT: mutation likelihood
  • REC: recombination likelihood
  • END: maximum number of trials (called "tournaments" in the article).

We represent the genes as Boolean partition vectors. Using the above globals we can create a set of genes like this:

   $genes=. (POP,LEN)?@$2
30 10
   3{.genes                    NB. Look at first three genes
1 1 1 0 0 1 1 1 1 0
0 0 0 1 0 1 0 1 0 0
1 0 1 1 0 1 1 1 0 1
   ]xx=. (3{.genes)</."1 CARDS
+--------------+------+
|1 2 3 6 7 8 9 |4 5 10|
+--------------+------+
|1 2 3 5 7 9 10|4 6 8 |
+--------------+------+
|1 3 4 6 7 8 10|2 5 9 |
+--------------+------+
   {{(+/>0{y),*/>1{y}}"1 xx    NB. Sum and product of each pair
36 200
37 192
39  90

Initializing Parameters

The parameters mentioned above are initialized this way in C#:

        //population size
        private int POP = 30;
        //geneotype
        private int LEN = 10;
        //mutation rate, change it have a play
        private double MUT = 0.1;
        //recomination rate
        private double REC = 0.5;
        //how many tournaments should be played
        private double END = 1000;
        //the sum pile, end result for the SUM pile
        //card1 + card2 + card3 + card4 + card5, MUST = 36 for a good GA
        private double SUMTARG = 36;
        //the product pile, end result for the PRODUCT pile
        //card1 * card2 * card3 * card4 * card5, MUST = 360 for a good GA
        private double PRODTARG = 360;
        //the genes array, 30 members, 10 cards each
        private int[,] gene = new int[30, 10];
        //used to create randomness (Simulates selection process in nature)
        //randomly selects genes
        Random rnd = new Random();

Notice how the assignment of the genes "gene = new int[30, 10]" uses hard-coded values of "30" and "10". These are in fact the same as the values for POP and LEN above but, due to the limitations of compiled languages, the information in those variables cannot be re-used in the instantiation without more code so we get duplicated values, hence more chance for errors when we update the parameters.

In J, this initialization might be something like this:

if. -.nameExists 'CARDS' do. CARDS=: >:i.10 end.
if. -.nameExists 'POP' do.
   'POP LEN MUT REC END'=: 30,(#CARDS),0.1,0.5,1000 NB. Parms per article:
NB. Population, length of genotype, mutation probability, recombination probability,
NB. end of number of iterations to try.
end.
TARGETS=: 36 360                        NB. sum and product targets

The if statements allow us to set different values for these globals, then reload the code without overwriting them. Notice too how the value of LEN is tied to the number of "cards" to avoid unnecessary repetition of values that should be the same.

Scoring

Here we take a look at the C# scoring code versus the initial J version.

C#
//evaluate the the nth member of the population
//@param n : the nth member of the population
//@return : the score for this member of the population.
//If score is 0.0, then we have a good GA which has solved
//the problem domain
private double evaluate(int n)
{
    //initialise field values
    int sum = 0, prod = 1;
    double scaled_sum_error, scaled_prod_error, combined_error;
    //loop though all genes for this population member
    for (int i = 0; i < LEN; i++)
    {
        //if the gene value is 0, then put it in the sum (pile 0), 
        //and calculate sum
        if (gene[n,i] == 0)
        {
            sum += (1 + i);
        }
        //if the gene value is 1, then put it in the product (pile 1), 
        //and calculate sum
        else
        {
            prod *= (1 + i);
        }
    }
    //work out how good this population member is, based on an overall error
    //for the problem domain
    //NOTE : The fitness function will change for every problem domain.
    scaled_sum_error = (sum - SUMTARG) / SUMTARG;
    scaled_prod_error = (prod - PRODTARG) / PRODTARG;
    combined_error = Math.Abs(scaled_sum_error) + Math.Abs(scaled_prod_error);

    return combined_error;
}            
J
   eval=: 4 : '(+/y#x),*/(-.y)#x'  NB. Evaluate in TARGETS order
   score=: 3 : '+/|TARGETS%~(CARDS eval y)-TARGETS'"1

The J version suffers from infestation by globals (as taken from the C# code) but we can refine that later.

Recombination and Mutation

The two basic transforms we apply to our "genes" are recombination and mutation. Recombination substitutes some bits from a more highly scored variant into one with a lower score. Mutation randomly flips bits in a gene, more if MUT is a higher number, fewer if it is lower.

Here we look briefly at the C# implementation, then the J one.

C#

This is the core of the main run method chosen to most closely correspond to our J version of it below.

            for (int tournamentNo = 0; tournamentNo < END; tournamentNo++)
            {
                //pull 2 population members at random
                a = (int)(POP * rnd.NextDouble());
                b = (int)(POP * rnd.NextDouble());
                //have a fight, see who has best genes
                if (evaluate(a) < evaluate(b))
                {
                    Winner = a;
                    Loser = b;
                }
                else
                {
                    Winner = b;
                    Loser = a;
                }
                //Possibly do some gene jiggling, on all genes of loser
                //again depends on randomness (simulating the 
                //natural selection
                //process of evolutionary selection)
                for (int i = 0; i < LEN; i++)
                {
                    //maybe do some recombination
                    if (rnd.NextDouble() < REC)
                        gene[Loser, i] = gene[Winner, i];
                    //maybe do some muttion
                    if (rnd.NextDouble() < MUT)
                        gene[Loser, i] = 1 - gene[Loser, i];
                    //then test to see if the new population member 
                    //is a winner
                    if (evaluate(Loser) == 0.0)
                        display(tournamentNo, Loser);
                }
            }
J

Again, this code is infested with globals in order to more closely shadow the C# from which it was copied.

Notice how the J code is considerably more concise than the C# code because of the ease with which we can handle arrays.

NB.* recombine: losers recombine winners -> better losers
recombine=: 4 : 0"1
   wh2c=. I. REC>:0?@$~{:$y        NB. Which to combine
   x=. (wh2c{y) wh2c}x             NB. some winner genes->loser
)

NB.* mutate: randomly flip some bits based on global MUT.
mutate=: 3 : 0"1
   wh2m=. MUT>:0?@$~#y    NB. Which to mutate
   y~:wh2m
)

A Note on the Original Algorithm

Notice how the C# code above randomly selects a pair of genes at a time to compare, recombine, and mutate. In our J implementation, it was much easier to break the entire gene pool into winners and losers then do this gene jiggling en-masse. This introduces some important discrepancies between the implementation which we will explore later.

Also, when I was testing the original version of this code, I noticed that duplicate genes would show up randomly. Since the C# code works through the "scalar keyhole", that author probably never noticed the wasted effort from using unnecessary duplicates but I did because I was looking at the entire population of genes at a time. For this reason, I improved the J rendition of that code by nubbing the genes and topping them off with new random genes.

J Version of Original

Here we try to replicate the C# code closely in J.

NB.* simpleGA0: more closely mimic the algo from the article by randomly
NB. selecting pairs of genes to combine and mutate; we still differ from
NB. that algo because we stop once we find a solution.  Also, we make the
NB. maximum number of iterations a parameter instead of relying on global "END".
simpleGA0=: 3 : 0
   'max genes'=. y            NB. Max iterations, set of genes, index of iteration
   for_ix. i. max do.         NB.  at which solution found.
       g2=. genes{~w2=. 2?#genes   NB. 2 random genes
       ord=. \:score g2            NB. ordered 
       g2=. g2{~ord                NB. worse to better
       new=. mutate recombine/g2   NB. gene jiggling
       genes=. new ({.ord{w2)}genes     NB. Replace worse with new
       if. 0=score new do.         NB. Finish if perfect score
           ix;<genes               NB. return # of earliest success & genes
	   return.
       end.                
   end.
   max;<genes
NB.EG 'ct genes'=. simpleGA0 (POP,LEN)?@$2
)   

Notice that the C# version prints a result if it finds a winner but keeps on processing whereas our J version of this algorithm halts once it finds a perfect score. Notice too that the C# code is checking the score after every single gene recombination instead of applying the complete recombination, then checking.

More J-like Version

Compare the above with the more natural J formulation here where we mutate and recombine all the winners with all the losers in one go instead of a random pair at a time.

simpleGA=: 3 : 0
   pop=. #genes=. y
   wix=. (<.pop%2){./:score genes    NB. winner indexes of best half
   lix=. (i.pop)-.wix                NB. loser indexes of worst half
   new=. (lix{genes) recombine wix{genes  NB. Assume (#lix)=#wix
   new=. mutate new
   genes=. (wix{genes),new
NB.EG simpleGA (POP,LEN)?@$2
)   

The above code represents only a single pass. We handle the iteration with the following adverb.

NB.* iterateGA: apply genetic algo repeatedly until we have a winner or
NB. have repeated as much as we have specified.
iterateGA=: 1 : 0
   'max genes'=. y
   pop=. #genes [ ct=. 0
   while. (-.0 e. score genes) *. (ct<max) do.
       genes=. ~.u genes    NB. Unique genes
       if. pop>#genes do.
           genes=. genes,((pop-#genes),{:$genes)?@$2  NB. add random new genes to maintain population
       end.
       if. 0 e. score genes do. break. end.  NB. Break early if we have a solution
       ct=. >:ct
   end.
   ct;<genes
NB.EG 'ct genes'=. (simpleGA iterateGA) 1000;<(POP,LEN)?@$2  NB. No more than 1000 iterations.
)

The "NB.EG" comment at the end gives an example of how we can iterate this code until we either reach a solution or hit the maximum number of runs. Notice how we reduce the gene pool to only the unique instances but top it off with new random genes if the pool falls below the stipulated population level.

We will compare the performance of these two variants and another a little further on.

Tuning Parameters

We have seen that we set a number of arbitrary values for some parameters, set as global variables, which control how quickly we reach a solution on average. For our testing these are

POP             2000            
LEN             30              
MUT             0.05            
REC             0.7             
END             2e6             
TARGETS         332 11412430848 

The important parameters MUT and REC, the likelihoods of mutation and recombination, are quite different from those chosen for the original essay on the C# implementation of a genetic algorithm for this toy problem. However, these are values that my testing has convinced me are better starting points for the algorithm to reach a solution most quickly.

Comparing Versions

One difference we notice right away between the version coded to be (nearly) the same as the C# implementation is that it takes a great many iterations to reach a solution.

C# Algorithm

The way this algorithm is designed, it does very little work per iteration so each iteration runs very quickly. However, this means that it also requires many iterations to stand a chance of finding an exact solution.

   6!:2 '''ct genes0''=. simpleGA01 1e6;<(POP,LEN)?@$2'  NB. No more than a million iterations.'
17.5661
   ct
1000000

We see that we hit the maximum number of iterations. Taking a look at the best set of genes so far, we see that we are close to a solution.

   (]i.<./) score genes0    NB. Where is first best score?
175
   CARDS eval 175{genes0
332 11417656320
   TARGETS
332 11412430848

So we are pretty close, but not exact and we know this problem has an exact solution because we chose the targets from a randomly generated set of genes. Fortunately we return the evolved set of genes so we can continue from where we left off with another set of iterations.

   6!:2 '''ct genes0''=. simpleGA01 1e6;genes0'  NB. Another maximum million iterations starting from previous
3.12089
   ct
177476
   (]i.<./) score genes0    NB. Where is first best score?
1074
   CARDS eval 1074{genes0
332 11412430848
   TARGETS
332 11412430848

Now we have an exact solution but it took over a million iterations, as is typical for this version.

Original J Algorithm

The version of this designed to be more J-like does a lot more work every iteration as it scores all the genes every time, ranks them from winners to losers, then combines each loser with a winner.

   6!:2 '''ct genes1''=. (simpleGA iterateGA) 2e4;<(POP,LEN)?@$2  NB. No more than 20,000 iterations.'
8.42645
   ct
1043

We see that this took about 8 1/2 seconds to run 1,043 iterations. We also see that our solution is exact.

   0 e. score genes1
1
   (score genes1)i.0
1701
   CARDS eval 1701{genes1
332 11412430848
   TARGETS
332 11412430848

So this more expensive algorithm compensates for its expense by finding a solution in fewer iterations.

Advanced topics

Solving the Traveling Salesman Problem with Genetic Algorithms

The earlier GA above solved a made-up problem with little practical use. However, the Traveling Salesman Problem is a well-known, difficult problem with practical applications. Here we will see how to develop a genetic algorithm to solve it.

The Problem

Given a set of "cities" or locations to visit, what is the most efficient path to take, visiting each location once, to visit all of them?

For instance, here we see 20 randomly-generated points arbitrarily sorted in ascending order plotted to show a path through all of them from beginning to end.

   |:pts=. /:~20 2?@$100                   NB. Show points transposed for more efficient use of screen space
 5  7 10 20 22 30 32 32 35 41 65 66 71 73 82 85 85 92 93 99
81 22 78  0 72 68 15 39 92 81 87 39 98 97 91  7 37 79 62 57
   showPts ('Random Path, Distance^2 = ',":totdis2 pts);pts

This gives us this picture:

Distance squared for random points with random path.jpg

Where showPts is this:

showPts=: 3 : 0
   'tit pts'=. y
   pd 'itemcolor green;type point;pensize 3;backcolor white' [ pd 'reset'
   pd j./|:circuit pts                     NB. First show the points themselves
   pd 'itemcolor blue;type line;pensize 2' NB.  then draw a line through all of them
   pd j./|:circuit pts [ pd 'title ',tit
   pd 'show'
)

We ensure that the line goes back to the starting point by the application of the "circuit" verb:

NB.* circuit: complete circuit by adding start to end
circuit=: (],[:{.])   

We calculate the sum of the square of the distances between each set of points, then total these to get the "distance squared" metric. An accurate distance measure would take the square root of the sum of the distances squared but we avoid the extra cost of taking the square root as a minor economy measure. We can compare distances squared as well as distances since all the numbers are positive so if one distance is greater than another, the square of that distance will also be greater than the square of the other distance.

NB.* dis2adjacent: distance^2 between adjacent points in a list.
dis2adjacent=: 13 :  '+/"1 *:2-/\ y'
NB.* totdis2: total distance^2 for a complete circuit of points.
totdis2=: (+/@:dis2adjacent@:circuit)"2

Representing Solutions

What is the range of solutions we might expect? Let's first look at the result of mimicking the parameters given in the C# code we used as a reference. We see here that the best path is substantially better than the worst one. The values of the best path are shown in the first row of complex numbers, where we represent the point pairs as complex numbers for ease of plotting, and the worst path is the second row. However for the purposes of specifying a path, we will work with permutation vectors as our "genes". A permutation vector is a list of integers from zero to one less than the length of the universe in any possible ordering of those indexes into the list of points.

TSP 10 sample points - best and worst paths.jpg

When we looked at this example with only 10 points in an earlier meeting, we solved it with an exhaustive search because that's easy to do with only 10 points, since there are less than !10 (3,628,800) solutions. There are at most !10 because this is the number of permutations of the paths among 10 nodes. Actually, the number of unique paths is quite a bit less than this since there are at least 20 equivalent variants for any given path; i.e. the path "0 1 2 3 4 5 6 7 8 9" has the same length as "1 2 3 4 5 6 7 8 9 0", which is the same as "2 3 4 5 6 7 8 9 0 1" and so on, as well as the same as "9 8 7 6 5 4 3 2 1 0", "8 7 6 5 4 3 2 1 0 9", and so on. Representing the paths as permutation vectors gives us an order in which to visit the points for the problem.

Initial Parameters

As with the other GA above, we set a few global parameters like this:

'POP MUT FLIPMUT REC'=: 1000 0.1 0.1 0.5
NB. population (# of genes to work with at a time), mutation probability,
NB. flip mutation probability, recombination probability.

Once we have set these, we can run our basic "evolve" to generate a better path.

   6!:2 'genes=. evolve 1000;pts'
1.20714
   $genes
1000 20
   totdis2 pts{~{.genes   NB. Genes (path permutations) are ordered from best to worst.
13532
   totdis2 pts{~{:genes   NB. versus the worst.
56744

We see that 1000 steps for 1000 genes at a time takes a little over a second and the improvement, from 50,532 to 13,532, is substantial.

BetterPath1000Evolutions.jpg

One thing I noticed in running this code repeatedly to improve the path length on larger datasets is that line crossings, like the one we see in the upper-right corner above, always seem to indicate that there is improvement to be had in the neighborhood of the crossing.

Further Work

We have only scratched the surface of this promising application of genetic algorithms. There are examples of some of this in the code referenced in the Materials section below. We may extend this work more fully in a future meeting.

Also, the existing code would benefit by getting away from the sloppy practice of relying on numerous global variables which we did to more closely mimic some of the code on which this was based. Even with the little bit of testing I did, varying some of these parameters, I ran into problems impossible to solve because of inadvertent inconsistency between these globals. Some of the recent forum discussions on dictionaries raises an intriguing possibility of at least centrally consolidating these globals.

Learning and Teaching J

Only Writing Programming Languages

We have spoken in the past about how some people in the J community, and elsewhere, seem more interested in writing new programming languages than in using existing ones. We see a similar tendency where people, often J novices, want to change J often in some radical way. This is not limited to J, as we can see in this essay "Fixing APL's trigonometric notation"; this reference is also notable for including an APL character in the URL. The essay also makes mention of the "b." alternatives in J as an example of bad notation. I tend to agree with the author's point on this.

As a recent example of this "radical change" mindset, there was small flurry of J Forum traffic recently about the possibility of making the shape vector of an array be able to take on higher dimensions.

​From: Elijah Stone <elronnd@elronnd.net>
Date: Thu, Mar 3, 2022 at 5:13 AM
Subject: [Jprogramming] Higher-order arrays?
To: <programming@jsoftware.com>

A little thought exercise I embarked upon.
Probably not very clearly expressed, and certainly of no practical use,
but I think there are some interesting ideas:

Why must the result of $y always be a vector?  What would happen if we let
it be a higher-ranked array?  What would that _mean_?
...

John Randall's reply to this chain may have mercifully cut it off. I was tempted to chime in before he did with the observation that the simplicity of representing the shape of arrays with a vector, especially the inclusion of the empty vector for the shape of a scalar, was one of the first startling and useful insights I remember learning from APL.

This essay about why one should not write a programming language seems somewhat pertinent to the discussion. The warning hinges on the observation that "...once you design a programming language you fall down a rabbit hole so deep you can't even see the light when you look up anymore." I think we see a bit of this all-consuming passion exhibited by these "only-write" programming language advocates.

Limitations of Human Memory

This essay on sources of confusion in coding lays out some of the different obstacles we encounter when writing code. One of these, which the author calls "Capacity-based Confusion" touches on themes well-known to those of us who extoll the virtues of terse code. In particular, the author distinguishes between three types of memory which limit our cognitive capacity:

  1. Long-term memory (LTM)
  2. Short-term memory (STM)
  3. Working memory (WM)

He relates these to some of his previous types of confusion. In the community of terse-language advocates, we often emphasize the last one of these since terse notation helps us better grasp larger concepts by giving us a very small handle with which to represent them. However, the other two types of memory are also helped not only by terseness but by regularity of a notation. If we have to keep track of fewer exceptions, we have cleaner tools of thought which improve our use of memory beyond just our working memory.

Materials

* File:SimpleGA.ijs Complete code for our initial simple genetic algorithm
* File:TSPGA.ijs  Complete code for our genetic algorithm to solve the Traveling Salesman Problem

To refine the TSP GA we explain above, we can look at work done on different ways to implement the recombination step, here called crossover:

* Genetic Algorithm for Traveling Salesman Problem with Modified Cycle Crossover Operator

Looking at the types of crossover they mention, it looks like every one of them can be implemented as a very short J expression.

Also, to test our TSP routines for correctness, we can look at a large set of solved datasets:

* TSPLIB datasets with optimal path lengths