Tim's Rudimentary
Treadle Reducer

Technical Discussion and Source Code

Introduction

For those wishing to understand how the Treadle Reducer works, or to modify the source code, here's a quick overview. It's written for people with some programming experience who want to get under the hood. If that's not you, then just go back; you shouldn't need to know anything from this page.

The Treadle Reducer is hosted on a standard PC running FreeBSD, and using gcc and Apache. If you're using Linux or most other Unix variants, then you shouldn't have to change anything (much) in the instructions here. For other operating systems and web servers, you're on your own. Making the code work shouldn't be hard, but I can't offer you much advice.

Availability of Source Code

As explained below, the Treadle Reducer consists of one html form, 3 files of C code, and 2 header files. A current snapshot of the Treadle Reducer site is available as a gzipped tar archive here. All these files are copyright (c) 2002 Tim McLarnan.

All these files are free software; anyone may redistribute and/or modify them under the terms of the GNU General Public License (GPL) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. A copy of the GPL is included with the source. Roughly, this license is designed to allow you to use the software however you see fit, subject only to the constraint that in redistributing this software or modified versions of it, you not deprive others of the right of free use. In distributing this or derivative software, you must also inform others of their rights of free use. The full text of the General Public License is available at Gnu's copyleft page, or, locally, here.

Initial Forms

The overall design of the Treadle Reducer is really quite simple. The user starts at the page form1.php, an html form which collects the number of shafts and the number of treadles in the original target tie-up and on the users's loom. This data then gets passed to a cgi script called buildgrid.cgi, which constructs a form containing a big grid of checkboxes used to input the original tie-up. The source code for this script is in buildgrid.c. Compile these with something like

gcc -o buildgrid.cgi -O3 -g -Wall buildgrid.c

and drop the resulting executable into your cgi-bin directory.

There is nothing really interesting in buildgrid.c. It just mindlessly constructs the html form. It should have been written in some more sensible scripting language like Perl or Python, but I've never really learned either language, and it was easier to use C than to learn a new language. Since I had never used cgi before either, holding to a minimum the number of new technologies I was embracing simultaneously seemed prudent.

Once the user has described the original tie-up and made a decision about how long a wait they'll tolerate, the data collected by this form gets passed to a second cgi script called weavereduce.cgi, which is where all the real action takes place.

The Real Action

weavereduce.cgi is also written in C. The cource consists of a library of set theoretic operations consisting of the header BigSet.h and the source file BigSet.c, together with the header weaving.h and the source file weavereduce.c for the application itself. I compiled these with

gcc -o weavereduce.cgi -O3 -g -Wall weavereduce.c BigSet.c

and dropped the executable into cgi-bin.

An Example
To see what happens next, let's work through an example. Suppose you are starting with the following tie-up:

X



X

X X X

X X
X X
X X X X
X


X X X

X X X X
(This tie-up is completely imaginary; I just picked it because it showed the mathematical aspects of the calculation I'm trying to highlight below.)

This tie-up is written for a loom with 6 shafts and 6 treadles; but suppose that because of an accident, my loom is broken and only has 5 working treadles. I want to weave the pattern on my 5-treadle loom.

LoadPattern()
The first thing weavereduce.cgi does is to verify that the number of shafts and treadles I gave it make sense. It then calls LoadPattern(InputString), which constructs a collection of sets, one for each treadle in the original tie-up. I call these sets targets, so they end up in an array of sets called TargetArray.

In keeping with C convention, let's number the rows in our example tie-up with numbers 0-5, starting in the top row. The leftmost treadle corresponds to the set {1,3}, since that treadle is tied to shafts 1 and 3. (Remember that we are numbering from the top, and that the top row is row 0.) The next treadle corresponds to the set {0,2,3,5}, and so on. At the end of LoadPattern(), the array TargetArray contains the following 6 target sets:

Targets:
0.{1, 3}
1.{0, 2, 3, 5}
2.{2, 3, 4, 5}
3.{1, 3, 4, 5}
4.{1, 2, 4, 5}
5.{1, 2, 3}

The goal is to find a collection of 5 sets, one for each working treadle of my loom, such that each target set is either one of these 5 sets, or a union of 2 of them.

The trouble with this goal is that even in this simple example, there are 63 non-empty subsets of {1, 2, 3, 4, 5, 6}; and there are 7,028,847 collections of 5 subsets. If we were trying to reduce a 14-treadle tie-up so we could weave it on a 10-treadle loom, then the number of collections we'd have to consider would be 382,805,484,029,414,797,963,859,411,093,325,825, which is too many to examine. We therefore need to do something to reduce our search space. The nice thing turns out to be that if we're a bit clever, we can reduce the search space dramatically without throwing away any possible solutions. How we do this is what makes the subsequent code interesting.

DeChaff()
Next, there are two bits of bookkeeping. A timer is started which will terminate processing after the interval specified by the user. Then the original tie-up is printed, and the function DeChaff() is run. DeChaff() removes from the TargetArray any target sets corresponding to treadles tied to no shafts or to treadles that repeat the same set of shafts as another treadle. These shouldn't ever appear in a tie-up anyway, but it takes little time to remove them if they do. If DeChaff() removes any targets, then it prints a message and prints the tie-up minus the targets removed.
Bricks
As we just said, we're looking in our example for a collection of 5 sets such that each of our 6 targets is either one of the 5 sets or a union of 2 of them. Where should we look for these sets?

My colleague Tekla Lewin was able to prove that the only sets we need to consider as candidates for our 5 sets are intersections of 1 or more targets. That is, if we can find a collection of 5 sets that does the job, then Tekla proved we can also find a collection of 5 sets all of which are intersections of 1 or more targets that also does the job. I call these intersections bricks, since they are the pieces that will be put together to build the target sets.

The next job is therefore to find all the possible bricks - all the sets that are intersections of 1 or more target sets. The functions InitializeBrickList() and MakeBricks() produce all the bricks, and store them in the list BrickList. This is done by first listing all the targets (since every target is a brick), and then starting down the list and intersecting each brick with all the bricks that follow it on the list. Any new set so produced is added to the end of BrickList.

In our example, there are 19 bricks, listed here:

Bricks:
0.{1, 3}
1.{0, 2, 3, 5}
2.{2, 3, 4, 5}
3.{1, 3, 4, 5}
4.{1, 2, 4, 5}
5.{1, 2, 3}
6.{3}
7.{1}
8.{2, 3, 5}
9.{3, 5}
10.{2, 5}
11.{2, 3}
12.{3, 4, 5}
13.{2, 4, 5}
14.{1, 4, 5}
15.{5}
16.{4, 5}
17.{1, 2}
18.{2}
Notice that the first 6 bricks on the list are the targets. The next few bricks are intersections of 2 targets. For instance, brick 8, {2, 3, 5}, is the intersection of target 1, {1, 2, 3, 5}, and target 2, {2, 3, 4, 5}. Some of the later bricks require more than 2 targets be intersected. For instance, brick 15, {5} is the intersection of target 1, {1, 2, 3, 5}. target 2, {2, 3, 4, 5}, target 3, {1, 3, 4, 5}, and target 4, {1, 2, 4, 5}.

We can now refine our goal. We want to find a set of 5 bricks such that every target is either a brick or an intersection of 2 bricks. There are only 11,628 collections of 5 bricks; so directly examining all the collections via some sort of depth-first search seems eminently possible.

Unfortunately, for larger problems, life is not so nice. When reducing a 14-treadle tie-up to 10 treadles, there often seem to be about 30 bricks; which means that there are 30,045,015 collections to consider. This is probably at the border of possibility in reasonable time, but a slightly larger problem with 50 bricks where we were reducing from 16 to 14 treadles would mean examining 937,845,656,300 collections, which is currently out of the question. So we still need to refine our search technique.

MakeTargetOptions()
What we'll do is to conduct a depth-first search through the collections of bricks to find a solution; but we want to make the search as efficient as possible. We'll do this by pre-computing as much information as we can, and by sorting the bricks into an order that makes the top of the search tree as narrow as possible, so that we prune as many nodes as possible each time we are forced to backtrack.

The first step of this is in the function MakeTargetOptions(), which builds an array called TargetOptionsArray. Each entry in this array is a record consisting of a target set together with a list of pairs of bricks, where each pair consists of two bricks, neither of which is the target, but whose union is the target. That is, each record shows a target set along with every way to build that target set as a union of 1 or 2 bricks. Producing these records isn't hard, since we only need to look at all the pairs of bricks, of which there aren't many. (Even with 50 bricks, we'd only have to examine 1225 pairs.)

In our example, the records in TargetOptionsArray are the rows in the following table.

TargetPairs of bricks
{1,3}({3},{1})
{0,2,3,5}none
{2,3,4,5}({3,4,5},{2})   ({3,4,5},{2,4,5})   ({2,3},{4,5})   ({2,3},{2,4,5})   ({2,3},{3,4,5})   ({2,5},{3,4,5})   ({3,5},{2,4,5})   ({2,3,5},{4,5})   ({2,3,5},{2,4,5}) ({2,3,5},{3,4,5})   ({3},{2,4,5})
{1,3,4,5}({3,4,5},{1,4,5})   ({3,5},{1,4,5})   ({1},{3,4,5})   ({3},{1,4,5})   ({1,3},{4,5})   ({1,3},{1,4,5})   ({1,3},{3,4,5})
{1,2,4,5}({4,5},{1,2})   ({1,4,5},{2})   ({1,4,5},{1,2})   ({2,4,5},{1,2})   ({2,4,5},{1,4,5})   ({2,5},{1,4,5})   ({1},{2,4,5})
{1,2,3}({2,3},{1,2})   ({1},{2,3})   ({3},{1,2})   ({1,3},{2})   ({1,3},{1,2})   ({1,3},{2,3})

Preparing for the Depth-First Search
Now the structure of our search is starting to become clear. We know from the table above that one of our 5 bricks has to be {0,2,3,5}, since there is no way to get this target as a union of 2 smaller bricks. We also know that we have to choose either {1,3} or both the bricks {1} and {3}; since these are the only 2 ways to get the target {1,3}. It also seems clear that the next thing we ought to do is to examine each of the 7 possible options to get the target {1,2,3}, rather than looking at the other targets, which can be obtained in 8 or in 12 possible ways. In particular, we ought to put off considering the target {2,3,4,5} as long as possible, in hopes either that we will get down to that target and find that it can already be obtained as a union of two selected bricks, or that the search will be forced to backtrack before we get down to {2,3,4,5}.

The first thing we do to get ready for the search is therefore to call SortTargetOptions, which rearranges the records in TargetOptionsArray so that those with the fewest possible options come first in the array.

So far, we have been dealing with the bricks as sets (i.e., as BigSet structs). We want now to start dealing with collections of bricks, and asking whether or not given bricks live in given collections. If we're not careful we'll end up wasting a lot of time comparing BigSets. It therefore makes more sense to number the BigSets, and to replace each of the sets in the pairs in the table above (i.e., in TargetOptionsArray) with the corresponding number. Then each pair will be a pair of ints, not a pair of BigSets, which will speed up comparisons a lot.

For technical convenience, we start by looking again at our list of 19 bricks. Not all these bricks actually turn up in the table above: the brick {5} was never used in building any target. The function RemoveUnusedBricks() removes from our list of bricks these extraneous bricks, and adjusts BrickCount accordingly.

The function OptionsToSets() carries out the last piece of preprocessing before we start the depth-first search. It builds a global array, BrickArray, containing the remaining bricks. (We can use an array instead of a list since we now know the exact number of bricks we have to deal with, and since we would like to be able to access random bricks quickly.) It also builds a global OptionsArray holding the pairs of ints corresponding to the pairs of bricks in the table above.

When OptionsToSets is finished, we have 3 important global arrays:

TargetArray holds the target sets, which in our example are numbered like this:

Targets:
0.{1, 3}
1.{0, 2, 3, 5}
2.{2, 3, 4, 5}
3.{1, 3, 4, 5}
4.{1, 2, 4, 5}
5.{1, 2, 3}

BrickArray holds the remaining bricks. In our example, only the brick {5} was unused; so there are 18 remaining bricks, numbered like this:

Bricks:
0.{1, 3}
1.{0, 2, 3, 5}
2.{2, 3, 4, 5}
3.{1, 3, 4, 5}
4.{1, 2, 4, 5}
5.{1, 2, 3}
6.{3}
7.{1}
8.{2, 3, 5}
9.{3, 5}
10.{2, 5}
11.{2, 3}
12.{3, 4, 5}
13.{2, 4, 5}
14.{1, 4, 5}
15.{4, 5}
16.{1, 2}
17.{2}

OptionsArray holds lists of pairs of bricks. Each list corresponds to 1 of the target sets. The list corresponding to a target contains every pair of bricks that can be used to form that target. In our case, OptionsArray looks like this:

Representations of targets in terms of bricks:
0.{1,1}
1.{0,0} {6,7}
2.{5,5} {11,16} {7,11} {6,16} {0,17} {0,16} {0,11}
3.{3,3} {12,14} {9,14} {7,12} {6,14} {0,15} {0,14} {0,12}
4.{4,4} {15,16} {14,17} {14,16} {13,16} {13,14} {10,14} {7,13}
5.{2,2} {12,17} {12,13} {11,15} {11,13} {11,12} {10,12} {9,13} {8,15} {8,13} {8,12} {6,13}

The order of the rows in OptionsArray doesn't match that in TargetArray, but one can still look at the entries here and understand what's going on. The first entry in OptionsArray, {1,1}, says that both brick 1 and brick 1 must be included in the set of 5 bricks we are trying to construct. Brick 1 is {0,2,3,5}, which is Target 1, the target we know cannot be gotten except as itself.

The next list, OptionsArray[1], says that our set of 5 bricks needs to include either brick 0, or both bricks 6 and 7. This means we either have the brick {1,3} or both the brick {3} and the brick {1}; so this list is telling us how to get the target {1,3}.

The list in OptionsArray[2] starts with {5,5}, which means it must correspond to brick 5, which is target 5, the set {1,2,3}. This row says we can get the target {1,2,3} either by using brick 5, or by using both bricks 11 and 16, or by using both bricks 7 and 11, and so on. The remaining rows correspond to targets 3, 4, and 2, resp.

The Depth-First Search
DFS() is the function that now carries out the depth-first search. It's a bit messy to read, but it just starts with and empty set and adds the first entry in each list in OptionsArray, looking for a set of 5 bricks that can form all the targets. So it will initially add bricks 1, 0, 5, 3, and 4 in order, then find that target 5 still isn't included. At this point, it backtracks, removing brick 4 and trying to add bricks 15 and 16, the next options in OptionsArray[4]. Neither this nor any of the other choices in OptionsArray[4] work, so it backtracks further, removing brick 3 as well. The set now contains only bricks 1, 0, and 5. We try adding in bricks 12 and 14, the next option in OptionsArray[3], but this fails to give us target 4, so we remove these and try adding 9 and 14, and so on.

In the example, success is finally attained with the bricks 0, 1, 11, 15, and 16, which are gotten by picking

{1,1} from OptionsArray[0],
{0,0} from OptionsArray[1],
{11,16} from OptionsArray[2],
{0,15} from OptionsArray[3]. At this point, we already have
{15,16} from OptionsArray[4] and
{11,15} from OptionsArray[5]; so we're done.

Printing the Results
Now all that's left to do is to print the solution, which is done, logically enough, by PrintSolution(). In our example, we get told


Here's one solution:

Tie up your loom like this:


X


X


X

X X
X
X X X




X

X
X

The treadles of the original tie-up correspond to treadles or pairs of treadles in the reduced tie-up according to the following table:

Original Reduced
1 1
2 2
3 3+4
4 1+4
5 4+5
6 3+5


On the set-theoretic level, our solution consisted of bricks
0.{1, 3}
1.{0, 2, 3, 5}
11.{2, 3}
15.{4, 5}
16.{1, 2}

These 5 bricks correspond to the 5 treadles in our reduced tie-up: treadle 1 (numbering from the left starting at 1) is tied to shafts 1 and 3 (numbering from the top, starting at 0); treadle 2 is tied to shafts 0, 2, 3, and 5; and so on.

The bottom table of the output tells how to obtain each of the original 6 treadles (targets) using intersections of 1 or 2 of the 5 working treadles on our loom (bricks).

That's the whole story. To anyone still reading at this point, congratulations!

Questions of Efficiency
During the 20+ years people have been using the Treadle Reducer, I haven't had complaints about its speed; so it would seem as though the design outlined here works fast enough for the problems most people are throwing at it.

When I profile the weavereduce.cgi under typical inputs, I find that over 99% of the CPU time is used by the single function DFS that does the depth-first search. Since the whole design of the program has been aimed at making the depth-first search as efficient as possible, and since I've worked to make DFS as efficient as I can (at major cost to its readability), there are probably no simple modifications that will yield order of magnitude improvements in efficiency. Of course, it is always possible that a clever new algorithm might be found to replace what is at bottom a rather unrefined brute-force search.

Closing Comments

I hope that this summary helps to clarify the design and function of the Treadle Reducer's source code. Again, it's all available to anyone who wants it under the terms of the GPL. Should you wish to make use of it, though, I would appreciate very much hearing from you. This project was a fun one for me, but it's also lovely to hear from people who find the Treadle Reducer useful.

Finally, comments on the source code from either an algorithmic or a software engineering perspective are more than welcome. Be nice, though; I don't claim to be more than an enthusiastic amateur.


Go back


Tim McLarnan,
Tremewan Professor of Mathematics emeritus
Earlham College,
Richmond, IN 47374 USA

Send me mail


Page last updated: September 23, 2024