Numerical Carpets

Ralph E. Griswold

Department of Computer Science
The University of Arizona
Tucson, Arizona

IPD285
June 5, 1998
http://www.cs.arizona.edu/icon/docs/ipd285.htm

Note: This article first appeared in the Icon Analyst.

Introduction

It has long been known that modular arithmetic often produces interesting numerical patterns. These patterns can be made easier to comprehend and more interesting by rendering them as images with colors associated with numerical values.

The example most often cited is Pascal's Triangle, which exhibits the binomial coefficients. If you take the coefficients modulo m for various m, you get different but interesting patterns. Figures 1 through 3 show a portion of Pascal's Triangle for m = 2, 3, and 5, using m uniformly spaced shades of gray from black (for 0) to white (for m-1).

Figure 1. Pascal's Triangle Modulo 2

Figure 2. Pascal's Triangle Modulo 3

Figure 3. Pascal's Triangle Modulo 5

See References 1 and 2 for extensive treatments of this subject.

Our interest in the subject came from a paper entitled "Carpets and Rugs: An Exercise in Numbers" [3]. This brief and informal paper describes the results of generating integers on a square array according to a "neighborhood" rule. The results are reduced modulo m and colored according to the resulting values.

Computing the values of the elements of an array according to the values of their neighbors is the basis for cellular automata [4,5]. Cellular automata may be one dimensional, two dimensional, or of higher dimension. The two-dimensional Game of Life is the best known [6].

Cellular automata are characterized by three properties:

Various neighborhoods can be used. For two-dimensional cellular automata, the neighborhood of a cell typically consists of the cell and its eight physically adjacent neighbors, as shown in Figure 4:

Figure 4. The Neighborhood of a Cell

Here we have labeled the cells according to compass points with the center cell labeled c. Which of the cells in the neighborhood contribute to the value of the center cell varies with the automaton.

The "Carpets and Rugs" article we mentioned above differs from cellular automata in that the values of the array elements are not computed in parallel, but rather one after another in a regular way in which previously computed values potentially affect newly computed values. Once a value is computed, it is not changed.

An n x n array is initialized along the top row and down the left column with all other values being zero initially. The rule used to compute new values is very simple:

   ai,j = (aj,i-1 + ai-1,j-1 + ai-1,j) mod m for 2 <= i, j <= n

Note that the initialized cells are not changed.

The neighborhood used is shown in Figure 5, where the value of the center cell is the sum of the values in the gray cells.

Figure 5. The Carpet Neighborhood

Thus, in terms of the labeling, c = n + nw + w. Note that the new value of c does not depend on its prior value.

The paper does not mention the order in which the array is traversed ("woven" seems apt for carpets), but with this neighborhood, the same results can be obtained either by a row-primary traversal, left to right, top to bottom, or by a column-primary traversal.

Having filled in the array, a color is assigned to each integer based on its value and the values of neighboring cells. The result then is displayed as a computer-generated image. The paper does not say how the colors are assigned, but simply assigning a different color to each integer 0 <= i <= m-1 produces images similar to the ones shown in the paper.

Only two initialization schemes are described in the paper: (1) all ones across the top row and down the left column, and (2) alternating zeros and ones across the top and down the left side.

Even with these simple initialization schemes and the simple rule for computing values, the results for different moduli are fascinating. Figures 6 and 7 show "carpets" similar to the ones in the paper.

Figure 6. Carpet from All-Ones Initialization

.

Figure 7. Carpet from One-Zero Initialization

Other moduli produce similar images.

The paper raises more questions than it answers. Some of the more obvious ones are:

The first issue to resolve is whether any such changes yield results that are both significantly different from those using only the method given in the paper and also are interesting. A few simple experiments answered this question, at least for us. See Figures 8 and 9. (Such images cannot be produced using only the methods described in the paper.)

Figure 8. "Pascal" Carpet

Figure 9. "Open Weave" Carpet

With so many independent variables, some of which offer not only endless but very different possibilities, two things are obvious: (1) an experimental approach is appropriate, and (2) a general, flexible, and easily used tool is needed.

This leads us to "programmable numerical carpets" in which a user can use programming techniques to experiment and explore. A visual interface in which various possibilities can be tried and evaluated interactively adds to power and ease of use.

There are, of course, too many independent variables posed by the preceding questions. We decided to stay within the confines of the method described in the paper with only a few extensions that do not affect the underlying ideas:

The width, height, and modulus are just constants that the user can specify. The challenging issue is initialization. Providing the user with a choice among a list of predefined initializations is easy, but it obviously is very limiting. Instead, the user should be able to specify the sequences of numbers for initialization.

We used the word "sequence" in the last sentence for a purpose. We could have used other words, such as "list", to convey the idea of order. But in Icon, the concept of sequence runs so deeply and is such a powerful programming technique that thinking sequences is something that should come naturally.

For example, the initializations used in the paper can be represented as sequences generated by the expressions |1 and |(0 | 1). Now think of all the other possibilities! Possibilities such as seq(), which generates 1, 2, 3, and fibseq() from the Icon program library, which generates the Fibonacci sequence 1, 1, 2, 3, 5, 8, , and many more.

But this idea takes us into deep water. It implies the ability to evaluate an arbitrary Icon expression during program execution. It is, of course, possible to write a program in which the initialization expressions are edited before the program is compiled and run. But this is too laborious and time-consuming for exploring the vast space of numerical carpets.

How can you evaluate an arbitrary Icon expression within a running program? You can't. But you can accomplish the equivalent.

One method is to write out a file consisting of the expression wrapped in a declaration for a main procedure. For example, to "evaluate" seq(), the file might look like this:

procedure main()
   every write(seq())
end

If the file is to be named expr.icn, a procedure to produce the file is just:

procedure expr_file(expr)
   local output

   output := open("expr.icn", "w") |
     stop("*** cannot open file for expression")

   write(output, "procedure main()")
   write(otuput, "   every write(", expr, ")")
   write(output, "end")

   close(output)

   system("icont -s expr -x")

   return

end

The -s option suppresses informative output from icont, while the option -x causes the program to be executed after compilation.

There is one thing very wrong with expr.icn: an expression like seq() is an infinite generator; output continues until something intervenes. That's easily fixed by limiting the generator. For the initialization of the top row, this might be used:

   every write(seq()) \ width

where width is the width of the array.

Before doing this, however, there is the question of how to get the output of expr back into the program that created it. One way would be to write it to a known file and read from the file when expr completes execution.

An alternative is to open the command line as a pipe instead of using system():

input := open("icont -s expr -x", "pw")

This has the same effect as the use of system() above, except it creates a pipe, input, from which the values produced by expr can be read one at a time as needed. Using this method, it's not necessary to add limitation to expr.icn. Depending on the operating system, expr may produce a few more values than are ever used, but in most situations, this is not a problem. Of course, the operating system must support pipes.

Note that pipes have to be created for every expression that needs to be evaluated to produce a carpet. There are at least three, one for each initializer and one for the neighborhood computation. We also found it helpful for the user to be able to specify the modulus as an expression, such as &phi ^ 2, and it just might be useful to allow the dimensions to be specified by expressions.

We have used this monolithic approach successfully, using exprfile.icn from the Icon program library to manage the details. We prefer a different approach, however; one that is simpler and more flexible. In this approach, a carpet-configuration program writes a file that contains preprocessor definitions for the various carpet parameters and expressions. It then uses system() to compile and execute a carpet-generation program that includes the definition file and constructs the carpet.

The advantage of this approach is that it's easy to write the preprocessor definitions and they are "magically" there when the carpet-generation program is compiled.

Separating the construction into two applications has the additional advantage of separating two quite different functionalities into two programs as opposed to packing them all into one program.

There are downsides to the separation. Since carpet generation is done by a separate application, the user needs to shift attention to this application to view the image it produces. Another problem is that the carpet-configuration program must know the location of the source code for the carpet-generation program.

Less obvious, perhaps, is error checking. In the monolithic approach, a user syntax error in an initialization expression can be detected before carpet construction begins. With the "duolithic" approach, that can't be done without "evaluating" expressions in the carpet-configuration application, which would defeat the purpose of the separation. Instead, the syntax error does not occur until the carpet-generation program is compiled.

But this is, after all, an application for Icon programmers; they don't make mistakes. Or, if they do, they know intuitively what is wrong and how to fix it ... .

In case you are wondering about speed, the "duolithic" approach is faster.

The interface for the carpet-specification program is simple: It consists of menus for file operations and setting specifications, a single button to create a carpet, and a "logo" for decoration. See Figure 10.

Figure 10. Carpet-Specification Interface

Figure 11 shows the dialog for entering and editing initializers.


Figure 11. The Initializer Dialog

The text-entry fields are long to allow complicated expressions to be entered.

Figure 12 shows the dialog for entering and editing the neighborhood expression.

Figure 12. The Neighborhood Dialog

Note that the variables n, nw, and w are used to refer to the cells relative to the current one when the carpet is generated. The values of these variables are supplied in the carpet-generation program. The Default button restores the expression to n + nw + w.

Here is an example of a definition file produced by the carpet-specification program.

$define Comments "October 14, 1997"
$define Name "untitled"
$define Width (128)
$define Height (128)
$define Modulus (5)
$define Hexpr (seq())
$define Vexpr (fibseq())
$define Nexpr (n + nw + 2 * w)
$define Colors "c2"

The definition for Colors is the name of a color palette.

Carpet Specifications

Dimensions

The size of a carpet usually affects its "completeness" as shown in Figure 13.

Figure 13. One Effect of Carpet Size

In some cases, the dimensions may affect the scale of the pattern. The patterns for carpets that are not square usually resemble the patterns for square ones.

In most cases, modest dimensions, such as 64x64 give an indication of the nature of the carpet. Considerable time can be saved by starting with small sizes to find promising candidates for larger carpets.

It is, of course, possible to contrive specifications that produce very different patterns depending on the dimensions of the carpet. Consider, for example,

   (|0 \ 100) | 1

for both initializers. Since the first 100 cells are zero, carpet with dimensions less than 101x101 will be a solid color, while a 200x200 carpet is as shown in Figure 14.

Figure 14. Another Effect of Carpet Size

Moduli

The patterns produced vary considerably in appearance depending on the modulus. Even the simplest initializer, a lone one on the upper-left corner, produces interesting patterns for different moduli. The results for a 128x128 array with moduli from 2 through 17 are shown in Figure 15.


Figure 15. Effects of the Modulus

The patterns that result from different moduli often show significant differences between prime and composite moduli. There is, of course, a strong interaction between the modulus and the initializers.

Initializers

Initializers provide the most fertile ground for designing interesting carpets. There are endless possibilities, which is a problem in itself.

Even the simplest initializers often produce interesting results, as shown in Figure 15. If the initializers for the top row and left column are the same and the default neighborhood computation is used, the resulting carpet is symmetrical around the diagonal from the upper-left corner to the lower-right one. The result often is more attractive than if different initializers are used for the top and left edges, but there are endless exceptions.

Icon's generators offer an easy way to experiment. Even simple generators like |(1 to 5) produce interesting carpets.

Some numerical sequences, when used as initializers, produce interesting patterns. Rather surprisingly, the prime numbers produce interesting carpets for moduli 4 and 8. Figure 16 shows the carpet for modulus 4. The carpet for modulus 16 is similar.

Figure 16. The Primes with Modulus 4

On the other hand, for other moduli at least through 100, the carpets for primes are chaotic and show little structure. The carpet for modulus 3, shown in Figure 17, is typical.

Figure 17. The Primes with Modulus 3

It's worth noting that the Icon program library contains a large number of procedure that generate various numerical sequences. See genrfncs.icn. The module pdco.icn contains programmer-defined control operations [7,8] that allow sequences to be composed in various ways, such as interleaving results from several sequences.

Neighborhoods

Neighborhoods are tricky. Most expressions other than the default one produce degenerate or chaotic carpets. Scaling values sometimes produce interesting results. For example, 3 * n + nw + 2 * w, with modulus 5 and lone-one initializers, produces the carpet shown in Figure 18.

Figure 18. A Non-Standard Neighborhood

If you look closely, you'll see that this carpet is not symmetric around the diagonal.

Colors

Lists of colors are used in displaying carpets. They may come from Icon's built-in palettes, or from color lists provided by the carpet-specification program, or they can be supplied by the user.

Different color lists, of course, may make marked differences in the visual appearances of the same carpet. Contrasting colors may make patterns easier to discern, but they may not produce the most attractive results.

There is a strong correlation between the modulus and the colors used. The number of colors need not be the same as the modulus, but if the number of colors exceeds the modulus, some colors will not be used. A more interesting situation occurs when the number of colors is less than the modulus. In this case the carpet-generation program "wraps around", taking values greater than the number of color modulo the number of colors.

An interesting possibility exists for using color lists in which colors are duplicated, thus mapping different values into the same color. We have not explored this yet.

The Programs

The carpet-specification program, named carport, is a simple VIB application. Most of the code is routine and we'll only show three procedures.

The procedure init() initializes the interface and sets up the default carpet specifications:

procedure init()

   vidgets := ui()     # initialize interface

   # Set up carpet defaults.

   comments := ""
   name := "untitled"     
   width := 128
   height := 128
   modulus := 5
   hexpr := "|1"
   vexpr := "|1"
   nexpr := "n + nw + w"
   colors := "c2"

   return

end

The procedure that is called to edit the initializers shows how simple the process is: There is no error checking; whatever the user enters is passed along to the carpet-generation program:

procedure initers()

   if TextDialog("Initializers:", ["horizontal", "vertical"],
      [hexpr, vexpr], 80) == "Cancel" then fail
   hexpr := dialog_value[1]
   vexpr := dialog_value[2]

   return

end

The procedure create_cb() writes the definition file for the carpet-generation program and then compiles and executes the program, which is named carplay:

procedure create_cb()
   local out

   output := open("carpincl.icn", "w") | fail

   write(out, "$define Comments ", image(comments))
   write(out, "$define Name ", image(name))
   write(out, "$define Width (", width, ")")
   write(out, "$define Height (", height, ")")
   write(out, "$define Modulus (", modulus, ")")
   write(out, "$define Hexpr (", hexpr, ")")
   write(out, "$define Vexpr (", vexpr, ")")
   write(out, "$define Nexpr (", nexpr, ")")
   write(out, "$define Colors ", image(colors))

   close(output)

   # compile and run

   system("icont -s carplay -x")

   return

end

Note that image() is used to place quotation marks around strings and that expressions are surrounded by parentheses to prevent misinterpretation when they are substituted for their names in the carpet-generation program.

The carpet-generation program is a bit more interesting:

link carpcolr    # color handler
link matrix
link genrfncs	   # procedures for initializers
link wopen

$include "carpincl.icn"

procedure main()

   carpet()

   repeat case Event() of {
      "q":  exit()
      "s":  WriteImage(Name || ".gif")
      }

end

procedure carpet()
   local m, n, colors, v, modulus, cmod, array, height, width

   colors := carpcolr(Colors)

   cmod := *colors

   modulus := Modulus
   width := Width
   height := Height

   array := create_matrix(height, width, 0)

   WOpen("size=" || width || "," || height) |
      stop("*** cannot open window")

   m := 0
   every v := (Vexpr \ height) do
      array[m +:= 1, 1] := v % modulus
   n := 0
   every v := (Hexpr \ width) do
      array[1, n +:= 1] := v % modulus

   every m := 1 to height do {		# color left edge
      Fg(colors[(abs(integer(array[m, 1])) % cmod) + 1])
      DrawPoint(m - 1, 0)
      }
   every n := 1 to width do {		#  color top edge
      Fg(colors[(abs(integer(array[1, n])) % cmod) + 1])
      DrawPoint(0, n - 1)
      }

   every m := 2 to height do {		# compute and color
      every n := 2 to width do {
         array[m, n] := neighbor(array[m, n - 1], 
            array[m - 1, n - 1], array[m - 1, n]) % modulus
         Fg(colors[(abs(integer(array[m, n])) % cmod) + 1])
         DrawPoint(n - 1, m - 1)
         }
      }

   return

end

procedure neighbor(n, nw, w)

   return Nexpr

end

The file containing the preprocessor definitions, carpincl.icn, is included before the actual code. The main procedure simply calls carpet() to produce the carpet and then waits for the user to save the image if desired before quitting. The interface is primitive to avoid linking lots of code that would be necessary for a more sophisticated interface, since the user of carport must wait for carplay to compile and link, the time saved is worth the inconvenience.

The procedure carpet() uses the symbols defined in carpincl.icn (distinguished by initial uppercase letters). There are some subtleties here. Modulus, Width, and Height might be expressions, not just numbers. Their assignment to variables, which are used subsequently, assures that expressions are not evaluated repeatedly. Note that the number of colors, assigned to cmod, may be different from the modulus.

First the left and top edges are initialized, using the expressions from carpincl.icn. The edges are colored before going on. Next, the carpet is created by traversing the matrix. Note that negative values and real numbers are allowed in specifications. Real numbers are converted to integers and the absolute value is used in determining the color to assign to a cell.

The procedure neighbor() is called with the three neighbors of interest. It simply returns whatever Nexpr specifies. Notice that the computation in neighbor() does not have access to the matrix or the other local variables in carpet(); this effectively confines the neighborhood computation to the values of the three neighboring cells it can't "reach out" and access other cells.

That's all there is to it. Of course, other features could be added to carplay to, for example, tile the carpet image so the user can see how it looks used in that way.

Comments

There are many more things that can be done to increase the capability of the carpet programs. These include:

Doing this, especially the specification of arbitrary paths on an array, involves solving both conceptual and programming problems.

References

1. "On Computer Graphics and the Aesthetics of Sierpinksi Gaskets Formed from Large Pascal's Triangles", Clifford A. Pickover, in The Visual Mind: Art and Mathematics, Michele Emmer, ed., pp. 125-133.

2. Chaos and Fractals; New Frontiers of Science, Heinz-Otto Peitgen, Hartmut Jürgens, and Dietmar Saupe, Springer-Verlag, 1992.

3. "Carpets and Rugs: An Exercise in Numbers", Dann E. Passoja and Akhlesh Lakhtakia, in The Visual Mind: Art and Mathematics, Michele Emmer, ed., pp. 121-123.

4. Cellular Automata and Complexity; Collected Papers, Stephen Wolfram, Addison-Wesley, 1994.

5. Cellular Automata Machines, Tommaso Toffoli and Norman Margolus, The MIT Press, 1991.

6. The Recursive Universe; Cosmic Complexity and the Limits of Scientific Knowledge, William Poundstone, Contemporary Books, 1985.

7. "Programmer-Defined Control Operations", Icon Analyst 22, pp. 8-12.

8. "Programmer-Defined Control Operations", Icon Analyst 23, pp. 1-4.