From lyn Fri Sep 29 16:04:17 1989
Return-Path: <lyn>
Date: Fri, 29 Sep 89 16:01:54 edt
From: lyn (Franklyn Turbak)
To: arthur
Subject: Pieces de Resistance


MASSACHUSETTS INSTITUTE OF TECHNOLOGY Department of Electrical
Engineering and Computer Science 6.001 Structure and Interpretation of
Computer Programs Fall Semester, 1986

Pieces de Resistance

Solutions to Problem Set 5


Part 1

Exercise 1

a. The box-and-pointer diagram for object N appears below:

















b.  RESISTANCE is called 3 times in computing the resistance of N:

1. On the whole circuit N
2. On R1 in parallel with R2
3. On R3 in parallel with R4
                                                                     

Exercise 2

L-EXTEND extends base by combining it with parallel-part and series-part.

(define (l-extend base series-part parallel-part)
  (make-series series-part 
               (make-parallel parallel-part base)))

                                                                     

Exercise 3

We generate N using the definitions in the problem set:

(define r1 (make-resistor 10))
(define r2 (make-resistor 20))
(define r3 (make-resistor 20))
(define r4 (make-resistor 40))

(define N
  (make-series (make-parallel r1 r2)
               (make-parallel r3 r4)))

We can then extend N to form a new circuit (let's call it EX3) as follows:

(define ex3 (l-extend n (make-resistor 10) (make-resistor 20)))

If we check the resitance of the new network, we find it is indeed 20 ohms:

1 ==> (resistance ex3)
20.0
                                                                     

Exercise 4

{Note that the definition of repeated given on the handout, 

(define (repeated f n)
  (lambda (x)
    (if (= n 0)
        identity
        (compose f (repeated f (- n 1))))))

is incorrect.  It should read either

(define (repeated f n)
  (if (= n 0)
      identity
      (compose f (repeated f (- n 1)))))

or

(define (repeated f n)
  (lambda (x)
    (if (= n 0)
        (identity x)
        ((compose f (repeated f (- n 1))) x))))  }

The most important point to realize here is that the procedure passed
to REPEATED as an argument can only take a single argument of some
type, and must return a result of the same type.  Since L-EXTEND takes
three arguments, we clearly cannot pass it to REPEATED.  Instead, we
want to use a new procedure that takes a "base circuit" and L-EXTENDS
it by the given series-part and parallel part.  Repeating this
procedure on the initial base gives us the appropriate ladder network.

(define (ladder-extension stages base series-part parallel-part)
  ((repeated (lambda (new-base) 
               (l-extend new-base series-part parallel-part)) 
             stages) 
   base))

Notice how we took a procedure of three arguments (L-EXTEND) and
encapsulated it in a procedure of one argument that calls the
three-argument procedure with two of its arguments "hard-wired" (i.e.
invariant).  This use of LAMBDA to encapsulate a procedure that
doesn't satisfy a desired specification (e.g. number of arguments) is
a standard trick that you should learn to recognize and use.
                                                                     

Exercise 5

We can define a procedure 1-OHM-LADDER that creates a ladder of the specified length out of 1 ohm resistors:

(define (1-ohm-ladder stages)
  (ladder-extension stages 
                    (make-resistor 1)
                    (make-resistor 1)
                    (make-resistor 1)))

We can then manually test the resistance of circuits created by
1-OHM-LADDER for various values of STAGES.  Alternately, we can write
a procedure (like LADDER-TEST below) that does this testing for us
automatically.

(define (ladder-test increment)
  (define (iter stages)
    (newline)
    (princ stages)
    (princ ": ")
    (princ (resistance (1-ohm-ladder stages))) 
    (iter (+ stages increment)))
  (iter 0))

Running LADDER-TEST for 10 iterations gives us the following:

1 ==> (ladder-test 1)
0: 1
1: 1.5
2: 1.6
3: 1.61538
4: 1.61764
5: 1.61797
6: 1.61802
7: 1.61803
8: 1.61803
9: 1.61803
10: 1.61803

The resistance of the ladder network, interestingly enough, seems to
be converging to the golden ratio (the positive root of the equation
x2 - x - 1 = 0):
 
1 ==> (/ (+ 1 (sqrt 5)) 
         2)
1.61803 
                                                                     

Exercise 6

Louis's change causes the resistance of each branch of a parallel
combination to be computed twice.  Careful reading of the original
code shows that the resistance of each branch used to be computed only
once.  Just as with Louis's change in Exercise 1.21 in the text, this
change can slow down the procedure by an exponential factor.  To avoid
the exponential slowdown, Louis could have used LET to guarantee that
the resistance of each branch is only computed once:

(define (resistance-parallel ckt)
  (let ((resistance-of-left-branch (resistance (left-branch ckt)))
        (resistance-of-right-branch (resistance (right-branch ckt))))
    (/ (* resistance-of-left-branch resistance-of-right-branch)
       (+ resistance-of-left-branch resistance-of-right-branch))))

Details of how the change leads to an exponential slowdown are
explored in the parts below:

a. There are 2N+1 resistors in an N-stage ladder, one for the initial
base, and two for each stage of the ladder.

b. For the original definition of RESISTANCE-PARALLEL,
RESISTANCE-RESISTOR get called only once for each resistor.  This
means that it gets called exactly 2N+1 times.

c. There are several approaches for counting the calls to
RESISTANCE-RESISTOR for an N-stage ladder after Louis's change.  We
will explore two approaches below:

Approach #1:

Suppose f(N) is the number of calls to RESISTANCE-RESISTOR in
computing the resistance of an N-stage ladder.  If we can find a way
to express f(N+1) in terms of f(N), then we may have a shot at finding
a nice closed-form solution for f(N).  For the N+1 case we have:


resistor 
branches of the remaining parallel combination.  - One branch is a
single resistor (one call to RESISTANCE-RESISTOR) - The other branch
is an N-stage ladder (f(N) calls to RESISTANCE-RESISTOR)

Thus,

f(N + 1) = 1 + 2(1 + f(N)) 

         = 2*f(N) + 3.  

We also know that 

f(0) = 1.  

Trying out a few values of N we see:

f(1) = 2*f(0) + 3 

     = 2 + 3

f(2) = 2*f(1) + 3 

     = 2*(2 + 3) + 3 

     = 22 + (2 + 1) *3

f(3) = 2*f(2) + 3 

     = 2*(22 + (2 + 1)*3) + 3 

     = 23 + (22 + 2 + 1)*3

>From the patterns in  these examples we can induce the general form:

f(N) = 2N + (2N-1 + 2N-2 + .  .  .  +22 +2 + 1)*3

     = 2N + (2N - 1)*(2 + 1)

     = 2N + 2N+1 + 2N - 3

     = 2N+2 - 3


Approach #2:

Once we find that f(N + 1) = 1 + 2*(1 + f(N)) (as in approach #1) we can see that:

f(N) = 1 + 2 + 2*f(N - 1))

     = 1 + 2 + 2 + 4 + 22*f(N - 2)

     = 1 + 2 + 2 + 4 + 4 + 8 + 23*f(N - 3)

     = 1 + 2 + 2 + 4 + 4 + 8 + 8 + . . + 2N + 2N

     = (1 + 2 + 4 + . . . + 2N) + (2 + 4 + 8 +. . . 2N)

     = (2N+1 -1) + (2N+1 - 2)

     = 2N+2 - 3

d.  Since the number of calls to RESISTANCE-RESISTOR was O(N) (linear)
before and O(2N) (exponential) after Louis's change, the running time
has been slowed down exponentially.

                                                                     
                                                                     


Part 2

Exercise 7

We can define N2 as

(define n2 (make-parallel (make-resistor 2) 
                          (make-parallel (make-inductor 1)
                                         (make-capacitor 1))))

The impedance at s = 2 + 0j can be found by evaluating

1 ==> ((resistance n2) (make-complex 2 0))
(.333333 . 0)

The impedance at s = 0 + 2j is found by evaluating:

1 ==> ((resistance n2) (make-complex 0 2))
(.2 . -.6)

                                                                     

Exercise 8

For this and the following exercises, it will be useful to define
FREQUENCY-RESPONSE-MAGNITUDE which takes a circuit and gives back the
magnitude of the frequency response of the circuit for s = 0 +
imaginary-part*j

(define (frequency-response-magnitude circuit)
  (lambda (imaginary-part)
    (magnitude ((resistance circuit) (make-complex 0 imaginary-part)))))

A procedure PLOT-CIRCUIT that plots the magnitude of the frequency
response of a circuit also makes our lives easier.

(define (plot-circuit circuit)
  (clear-graphics)
  (line-plot
   (frequency-response-magnitude circuit)
   0.001
   2
   0.05))

To plot N2, we can now simply evaluate

(plot-circuit n2)

Evaluating this expression results in the following plot, which looks
like it reaches a maximum at x = 1.





























To check where the maximum actually occurs, we can evaluate a few expressions:

1 ==> ((frequency-response-magnitude n2) .999)
(1.99996 . 8.00387e-3) 

1 ==> ((frequency-response-magnitude n2) 1)
(2 . 0) 

1 ==> ((frequency-response-magnitude n2) 1.001)
(1.99996 . -7.99587e-3) 

These results indicates that a maximum magnitude of 2 occurs at s = 0
= 1j.  (We could also determine the maximum magnitude from the extent
information returned by LINE-PLOT (and thus by PLOT-CIRCUIT).  In this
case, that information indicates a maximum of 1.99998.)

                                                                     

Exercise 9

Let's create a new circuit N3 which uses a 10 ohm resistor rather than
a 2 ohm resistor.

(define n3 (make-parallel (make-resistor 10) 
                          (make-parallel (make-inductor 1)
                                         (make-capacitor 1))))

A plot of the magnitude of the frequency response is obatained by
evaluating

(plot-circuit n3)
     
 






























As with N2, the resonant frequency of N3 appears to be 1 hertz.
However, the height of the peak has increased from 2 to 10, as the
following expressions indicate:

1 ==> ((frequency-response-magnitude n3) 0.999)
(9.996 . .20002) 

1 ==> ((frequency-response-magnitude n3) 1)
(10. . 0) 

1 ==> ((frequency-response-magnitude n3) 1.001)
(9.996 . -.19982) 
  
(The extent information returned by PLOT-CIRCUIT indicates a maximum value of 9.998)

                                                                     
                                                                     

Part 3

Exercise 10

First let's define a procedure FIFTY-FIFTY that returns #!TRUE with
50% probability and #!FALSE otherwise

(define (fifty-fifty)
  (= (random 2) 0))


Next we'll define SPLIT-LIST to randomly separate a set of parts into
two subsets.  We can define SPLIT-LIST either recursively or
iteratively:

Recursive version

By using wishful thinking here, we can assume that SPLIT-LST works on
the CDR of the given list.  We then add the CAR of the list to one of
the resulting subsets.

(define (split-list lst)
  (if (null? lst) 
      (cons '() '())
      (let ((split-rest-pair (split-list (cdr lst))))
        (if (fifty-fifty)
            (cons (cons (car lst)
                        (car split-rest-pair))
                  (cdr split-rest-pair))
            (cons (car split-rest-pair)
                  (cons (car lst)
                        (cdr split-rest-pair)))))))

Iterative version

Here we have an internal iterator ITER with two state variables
(PILE-1 and PILE-2) representing the two susbsets.  On every iteration
we take one of the remaining elements and add it to a randomly chosen
pile.

(define (split-list-1 lst)
  (define (iter current-list pile-1 pile-2)
    (cond ((null? current-list)
           (cons pile-1 pile-2))
          ((fifty-fifty)
           (iter (cdr current-list) 
                 (cons (car current-list) pile-1) 
                 pile-2))
          (else (iter (cdr current-list) 
                      pile-1 
                      (cons (car current-list) pile-2)))))
  (iter lst '() '()))

Finally we define RANDOM-CIRCUIT.  In the general case, RANDOM-CIRCUIT
splits the given list of parts into two subsets, finds random circuits
for each of the subsets, and then combines the resulting two circuits
together either in parallel or series (the means of combination is
determined randomly).  If given a list containing only one part,
RANDOM-CIRCUIT returns that part as its result.  An important special
case to consider occurs when one of the subsets returned by SPLIT-LIST
is empty.  Since RANDOM-CIRCUIT cannot handle an empty list as an
input (we can't create a circuit out of nothing), we must try to
re-split the list.  An easy way to do this is to simply call
RANDOM-CIRCUIT on the original input again.  Although this might look
like it causes an infinite loop, the probabilistic nature of
RANDOM-CIRCUIT guarantees that there is an infinitely small chance of
an infinite loop actually occuring.  A different approach would be to
define SPLIT-LIST in such a way that it always returns a pair of two
non-empty subsets.

(define (random-circuit parts)
  (if (= (length parts) 1)
      (car parts)
      (let ((split-parts-pair (split-list parts)))
        (let ((subset1 (car split-parts-pair))
              (subset2 (cdr split-parts-pair)))
          (cond ((or (null? subset1)
                     (null? subset2))
                 (random-circuit parts))
                ((fifty-fifty)
                 (make-series (random-circuit subset1)
                              (random-circuit subset2)))
                (else (make-parallel (random-circuit subset1)
                                     (random-circuit subset2))))))))
                                                                                                        
                                                                     

Exercise 11

First let's make a list of parts:

(define parts-list (list (make-capacitor 1)
                         (make-capacitor 2)
                         (make-inductor 1)
                         (make-resistor 1)
                         (make-resistor 2)))

It'll be useful to define a procedure COLLECT-CIRCUITS that collects a
specified number of random circuits:

(define (collect-circuits parts number)
  (if (= number 0)
      '()
      (cons (random-circuit parts)
            (collect-circuits parts (- number 1)))))

Now let's create SAMPLE-CIRCUITS, a list of five random circuits (of
course, yours will probably be different from these):

(define sample-circuits (collect-circuits parts-list 5))

In pretty-printed form these look like:

1 ==> (pp sample-circuits)
((PARALLEL-COMBINATION
  (PARALLEL-COMBINATION
   (RESISTOR (CAPACITANCE 2) #[COMPOUND-PROCEDURE 12825804])
   (RESISTOR (INDUCTANCE 1) #[COMPOUND-PROCEDURE 12860504]))
  (PARALLEL-COMBINATION
   (SERIES-COMBINATION
    (RESISTOR (RESISTANCE 1) #[COMPOUND-PROCEDURE 12850388])
    (RESISTOR (CAPACITANCE 1) #[COMPOUND-PROCEDURE 12786784]))
   (RESISTOR (RESISTANCE 2) #[COMPOUND-PROCEDURE 12825788])))
 (SERIES-COMBINATION
  (SERIES-COMBINATION
   (RESISTOR (RESISTANCE 1) #[COMPOUND-PROCEDURE 12850388])
   (RESISTOR (INDUCTANCE 1) #[COMPOUND-PROCEDURE 12860504]))
  (PARALLEL-COMBINATION
   (RESISTOR (CAPACITANCE 1) #[COMPOUND-PROCEDURE 12786784])
   (SERIES-COMBINATION
    (RESISTOR (CAPACITANCE 2) #[COMPOUND-PROCEDURE 12825804])
    (RESISTOR (RESISTANCE 2) #[COMPOUND-PROCEDURE 12825788]))))
 (PARALLEL-COMBINATION
  (SERIES-COMBINATION
   (PARALLEL-COMBINATION
    (RESISTOR (CAPACITANCE 2) #[COMPOUND-PROCEDURE 12825804])
    (RESISTOR (RESISTANCE 2) #[COMPOUND-PROCEDURE 12825788]))
   (RESISTOR (INDUCTANCE 1) #[COMPOUND-PROCEDURE 12860504]))
  (SERIES-COMBINATION
   (RESISTOR (CAPACITANCE 1) #[COMPOUND-PROCEDURE 12786784])
   (RESISTOR (RESISTANCE 1) #[COMPOUND-PROCEDURE 12850388])))
 (PARALLEL-COMBINATION
  (SERIES-COMBINATION
   (RESISTOR (RESISTANCE 1) #[COMPOUND-PROCEDURE 12850388])
   (SERIES-COMBINATION
    (SERIES-COMBINATION
     (RESISTOR (RESISTANCE 2) #[COMPOUND-PROCEDURE 12825788])
     (RESISTOR (CAPACITANCE 2) #[COMPOUND-PROCEDURE 12825804]))
    (RESISTOR (CAPACITANCE 1) #[COMPOUND-PROCEDURE 12786784])))
  (RESISTOR (INDUCTANCE 1) #[COMPOUND-PROCEDURE 12860504]))
 (PARALLEL-COMBINATION
  (PARALLEL-COMBINATION
   (RESISTOR (RESISTANCE 1) #[COMPOUND-PROCEDURE 12850388])
   (RESISTOR (INDUCTANCE 1) #[COMPOUND-PROCEDURE 12860504]))
  (PARALLEL-COMBINATION
   (RESISTOR (CAPACITANCE 2) #[COMPOUND-PROCEDURE 12825804])
   (SERIES-COMBINATION
    (RESISTOR (RESISTANCE 2) #[COMPOUND-PROCEDURE 12825788])
    (RESISTOR (CAPACITANCE 1) #[COMPOUND-PROCEDURE 12786784])))))

                                                                                         

Exercise 12

Everybody's answers will differ here.  Of the circuits in
SAMPLE-CIRCUITS, the first, third, and fifth exhibit resonant
behavior.

                                                                     

Exercise 13

The basic idea here is to look for a sharp peak or dip between the
frequencies of 0 and 2 hertz.  We can find peaks and dips (maxes and
mins) by looking for a change in sign of the derivative of the
frequency response; we can test for sharpness by evaluating the second
derivative of the curve at the possibly resonant frequencies and
comparing the values against some sharpness threshold.

Let's actually implement the above strategy:

DERIV is the derivative procedure from the book

(define (deriv f dx)
  (lambda (x) 
    (/ (- (f (+ x dx)) 
          (f x))
       dx)))


ZERO-CROSSINGS finds all the zero-crossings of a given function over
an interval.  It returns a list of the x-values at which the
zero-crossings occur.  It tests for zero crossings by looking for a
change in sign of the function as it iterates over the interval.  The
argument STEP determines the granularity at which the zero-crossings
are measured.  If a curve crosses the x-axis twice in an interval less
than STEP, ZERO-CROSSING may not detect these crossings.

(define (zero-crossings fn from to step)
  (define (loop current-x previous-y current-y zero-crossing-list)
    (cond ((> current-x to) zero-crossing-list)
          ((or (= current-y 0)
               (< (* previous-y current-y) 
                  0))    ; if these are of opposite sign, must be zero-crossing in between
           (loop (+ current-x step) 
                 current-y 
                 (fn (+ current-x step)) 
                 (cons (if (< (abs current-y) (abs previous-y))
                           current-x
                           (- current-x step))
                       zero-crossing-list)))
          (else (loop (+ current-x step) 
                      current-y 
                      (fn (+ current-x step)) 
                      zero-crossing-list))))
  (loop (+ from step) (fn from) (fn (+ from step)) nil))


MAXES-AND-MINS returns a list of the x-values at which a max or min
occurs.  It does so by looking at the zero-crossings of the derivative
of the function.

(define (maxes-and-mins fn from to step)
  (zero-crossings (deriv fn default-dx) from to step))


To determine sharpness, we define SHARP?, which compares the second
derivative of a function at a point to a threshold.

(define (sharp? fn x threshold)
  (> (abs ((deriv (deriv fn default-dx) default-dx) x))
     threshold))


CHECK-RESONANCE finds the maxes and mins of the magnitude of the
frequency response of a circuit between 0 and 2 hertz.  It then
filters out all the non-sharp values.  If any x-values are left after
the filtering, the circuit exhibits resonance at those frequencies.

(define (check-resonance ckt)
  (let ((magnitude-of-impedance (frequency-response-magnitude ckt)))
    (let ((sharp-peaks-and-dips 
           (filter (lambda (x) (sharp? magnitude-of-impedance 
                                       x
                                       default-sharpness-threshold))
                   (maxes-and-mins magnitude-of-impedance
                                   0
                                   default-max-frequency
                                   default-scan-step))))
      (print-resonance sharp-peaks-and-dips)
      sharp-peaks-and-dips)))


FILTER is the standard general-purpose filtering procedure.

(define (filter pred lst)
  (cond ((null? lst) nil)
        ((pred (car lst))
         (cons (car lst)
               (filter pred (cdr lst))))
        (else (filter pred (cdr lst)))))

               
PRINT-RESONANCE prints our an appropriate message given a list of the
resonant frequencies.

(define (print-resonance peaks-and-dips)
  (cond ((null? peaks-and-dips)
         (newline)
         (princ "The circuit does not exhibit resonance between the frequencies of 0 and ")
         (princ default-max-frequency))
        (else (newline)
              (princ "The circuit exhibits resonance between 0 and ")
              (princ default-max-frequency)
              (newline)
              (princ " at the following frequencies: "))))


Here are a number of arbitrarily chosen default values:

(define default-dx 0.01)
(define default-scan-step 0.1)
(define default-max-frequency 2)
(define default-sharpness-offset 0.1)
(define default-sharpness-threshold 5)


We can test CHECK-RESONANCE out on the circuits generated in Exercise
11:

1 ==> (check-resonance (first sample-circuits))
The circuit exhibits resonance between 0 and 2 at the following frequencies: 
(.6) 

1 ==> (check-resonance (second sample-circuits))
The circuit does not exhibit resonance between the frequencies of 0 and 2
() 

1 ==> (check-resonance (third sample-circuits))
The circuit exhibits resonance between 0 and 2 at the following frequencies: 
(.7) 

1 ==> (check-resonance (fourth sample-circuits))
The circuit does not exhibit resonance between the frequencies of 0 and 2
() 

1 ==> (check-resonance (fifth sample-circuits))
The circuit exhibits resonance between 0 and 2 at the following frequencies: 
(.6) 

                                                                     

Exercise 14
                

This is a fun problem!

The goal of this problem is to generate all possible distinct
series/parallel circuits given a list of parts.  We can approach this
problem in two different ways.

Method #1. Enumerate all possible circuits and enumerate duplicates.
Enumerating the circuits is fairly straightforward if we are careful.
An easy way to determine duplicate circuits is to measure the
impedance of two circuits at several randomly chosen frequencies.  If
the impedances differ at any frequencies, the circuits are obviously
not equivalent; if the impedances are the same at all the randomly
chosen frequencies, there is a very good chance the circuits are
functionally equivalent (this is a probabilistic method quite similar
to the FAST-PRIME? test on Problem Set 2).  Note that two circuits
that are structurally distinct may be functionally equivalent.  For
example, suppose:

R1 is a 1 ohm resistor
R2 is a 1 ohm resistor
C1 is a 1 farad capacitor
I1 is a 1 henry inductor

Then 

R1 

has the same impedance (for all frequencies!) as 

(// (+ R1 C1)  (+ R2 I1)) 

{For brevity of notation we'll use the prefix operator + to indicate a series combination and the prefix operator // to indicate a parallel combination.}
                
Method #2. A second approach is to enumerate only structurally
distinct elements to begin with.  Consider why structural duplications
occur in the first place. There are two main reasons for the
duplications.  First of all, series combinations (and parallel
combinations) are commutative - that is, switching the left and right
branches of either type of combination leaves us with an equivalent
circuit:

(+ R1 R2) is the same as (+ R2 R1)
(// R1 R2) is the same as (// R2 R1)

Second, series combinations (and parallel combinations) are
associative - the way we choose to use two combinations to combine
three ordered pieces is arbitrary:

(+ R1 (+ R2 R3)) is the same as (+ (+ R1 R2)  R3)
(// R1 (// R2 R3)) is the same as (// (// R1 R2) R3)

To enumerate circuits without duplication, we need to determine a
canonical representation in which we guarantee that we will use only
one of the possibilities provided by commutativity and associativity.
This can be accomplished by two rules:

Rule #1: Arbitrarily assign a priority to all primitive elements at
the start.  Let the priority of a circuit be the priority of its
highest priority primitive element.  When combining two circuits,
always put the higher-priority circuit in the left branch.  This means
that commutative duplications will not be produced.

As an example, let's look at all the ways three resistors, R1, R2, and R3 can be in series:

(+ R1 (+ R2 R3))
(+ R1 (+ R3 R2))
(+ R2 (+ R1 R3))
(+ R2 (+ R3 R1))
(+ R3 (+ R1 R2))
(+ R3 (+ R2 R1))
(+ (+ R1 R2) R3)
(+ (+ R1 R3) R2)
(+ (+ R2 R1) R3)
(+ (+ R2 R3) R1)
(+ (+ R3 R1) R2)
(+ (+ R3 R2) R1)

If we apply Rule #1 (assuming R1 has the highest priority, R2 the next
highest, and R3 the lowest) the only circuits we can generate are:

(+ R1 (+ R2 R3))
(+ (+ R1 R2) R3)
(+ (+ R1 R3) R2)

Rule #2: Whenever combining three or more elements all in series (or
all in parallel), combine them in a right-associative fashion, i.e.

choose (+ R1 (+ R2 R3)) rather than (+ (+ R1 R2) R3)
choose (// R1 (// R2 R3)) rather than (// (// R1 R2) R3)

This rule removes duplicates caused by associativity.
 
Applying Rule #2 to the circuits remaining in our example gives the canonical representation of the three resistors in parallel:

(+ R1 (+ R2 R3))

Note that the second approach only guarantees that structurally
distinct circuits will be generated - i.e. it might generate
structurally distinct but functionally equivalent circuits.


O.K., now that we've discussed the high-level approaches, let's look
at some code that implements the approaches.

MAKE-SUBSET-PAIRS (below) is a useful procedure for both approaches.
Given a list of elements, it returns a list of all possible ways the
elements in that list can be split into two non-empty subsets.  This
version of MAKE-SUBSET-PAIRS orders all elements within each subset
(assuming they are ordered from highest to lowest in the original
list). It also guarantees that the subset with the highest priority
primitive element appears first in a pair of subsets.  This eliminates
needless duplications for Method 1 and is used to implement Rule #1
for Method 2.

(define (make-subset-pairs lst)
  (cond ((< (length lst) 2) (error "I must have a list greater than two"))
        ((= (length lst) 2)
         (list (list (list (car lst)) 
                     (list (cadr lst)))))
        (else (let ((smaller-sets (make-subset-pairs (cdr lst)))
                    (new-elt (car lst)))
                (append (list (list (list new-elt) (cdr lst)))
                        (mapcar (lambda (pair)
                                  (list (cons new-elt (car pair))
                                        (cadr pair)))
                                smaller-sets)
                        (mapcar (lambda (pair)
                                  (list (cons new-elt (cadr pair))
                                        (car pair)))
                                smaller-sets))))))

Examples of the input/output behavior of MAKE-SUBSET-PAIRS will
hopefully make its behavior clearer:

1 ==> (pp (make-subset-pairs '(1 2)))
(((1) (2)))

1 ==> (pp (make-subset-pairs '(1 2 3)))
(((1) (2 3)) 
 ((1 2) (3)) 
 ((1 3) (2)))

1 ==> (pp (make-subset-pairs '(1 2 3 4)))
(((1) (2 3 4)) 
 ((1 2) (3 4))
 ((1 2 3) (4))
 ((1 2 4) (3))
 ((1 3 4) (2))
 ((1 4) (2 3))
 ((1 3) (2 4)))

1 ==> (pp (make-subset-pairs '(1 2 3 4 5)))
(((1) (2 3 4 5)) 
 ((1 2) (3 4 5))
 ((1 2 3) (4 5))
 ((1 2 3 4) (5))
 ((1 2 3 5) (4))
 ((1 2 4 5) (3))
 ((1 2 5) (3 4))
 ((1 2 4) (3 5))
 ((1 3 4 5) (2))
 ((1 4 5) (2 3))
 ((1 5) (2 3 4))
 ((1 4) (2 3 5))
 ((1 3) (2 4 5))
 ((1 3 4) (2 5))
 ((1 3 5) (2 4)))


Method 1: Generate all circuits and remove duplicates


GENERATE-ALL generates all combinations of the given parts without
duplicates. It does so by first finding all possible subset pairs of
the given list of parts.  For each subset pair, it recursively
generates all circuits for each subset, and then combines elements in
the Caresian product of the circuits resulting from each subset by
series and parallel combinations.  It collects all the circuits
resulting from focusing on each of the subset pairs, and finally
removes duplicate circuits.

(define (generate-all list-of-parts)
  (if (<= (length list-of-parts) 1)  ; Handle the 0 and 1 cases specially
      list-of-parts
      (let ((subsets (make-subset-pairs list-of-parts)))
        (let ((all-subcircuit-pairs (mapappend all-subset-combinations subsets)))
          (remove-duplicates
           circuit=?
           (append (mapcar (lambda (ckt-pair)
                             (make-series (car ckt-pair)
                                          (cadr ckt-pair)))
                           all-subcircuit-pairs)
                   (mapcar (lambda (ckt-pair)
                             (make-parallel (car ckt-pair)
                                            (cadr ckt-pair)))
                           all-subcircuit-pairs)))))))
              
 
MAPAPPEND is like MAPCAR execpt it APPENDS the results of the function
rather than CONSing them up.  (Note that this implies that the
function must return a list).

(define (mapappend proc lst)
  (if (null? lst)
      nil
      (append (proc (car lst))
              (mapappend proc (cdr lst)))))

    
ALL-SUBSET-COMBINATIONS finds, all circuits in which the elements of
one subset are used in the left branch of a combination and all
elements from the other subset are used in the right branch of a
combination.

(define (all-subset-combinations subset-pair)
  (all-pairs (generate-all (car subset-pair))
             (generate-all (cadr subset-pair))))


ALL-PAIRS returns a list of the elements of the Cartesian product of two lists

(define (all-pairs lst1 lst2)
  (if (null? lst1)
      nil
      (append (mapcar (lambda (elt-of-lst2)
                        (list (car lst1) elt-of-lst2))
                      lst2)
              (all-pairs (cdr lst1) lst2))))
                              

REMOVE-DUPLICATES removes all duplicates from a list, where the
comparision is done by the procedure passed in as the argument
EQUAL-FN.

(define (remove-duplicates equal-fn lst)
  (if (null? lst)
      nil
      (cons (car lst)
        (filter (lambda (elt)
                  (not (equal-fn (car lst) elt)))
                (remove-duplicates equal-fn (cdr lst))))))


CIRCUIT=? checks whether two circuits are functionally equivalent by a
probabilistic method.  It compares the impedances of the circuits for
randomly chosen frequencies between 0 and 2 hertz.  If the impedances
are the same for three random points, we will assume the circuits are
functionally equivalent.

(define (circuit=? ckt1 ckt2)
  (define (compare-circuits number-of-tests)
    (cond ((= number-of-tests 0) #!true)
          ((same-impedance ckt1 ckt2 (random-frequency))
           (compare-circuits (- number-of-tests 1)))
          (else #!false)))
  (compare-circuits 3))


SAME-IMPEDANCE checks whether the magnitude of the frequency response
of two circuits is the "same" for a given frequency.  Given the
vagaries of floating point computations, we cannot check for straight
equality, but must instead see if the two values are reasonably
"close".

(define (same-impedance ckt1 ckt2 freq)
  (close-enough? ((frequency-response-magnitude ckt1) freq)
                 ((frequency-response-magnitude ckt2) freq)))


RANDOM-FREQUENCY picks a random frequency between 0 and 2 hertz.

(define (random-frequency)
    (/ (random 2000) 1000))

CLOSE-ENOUGH? determines whether two values are reasonably "close"

(define (close-enough? num1 num2)
  (< (abs (- num1 num2))
     0.001))
      
      
 
     
Method 2: Generate all circuits without duplications


As with method 1, GENERATE-ALL generates all circuits without
duplicates.  It does so by calling on GENERATE-WITH-COMBINERS, which
is the real workhorse.

(define (generate-all list-of-parts)
  (generate-with-combiners list-of-parts 
                           '()
                           (list make-series make-parallel)))


GENERATE-WITH-COMBINERS generates (without duplicates) all possible
circuits using the LIST-OF-PARTS.  POSSIBLE-COMBINERS is a list of
combining procedures which the generator is allowed to use.  In our
case, this is usually a list of the procedures MAKE-PARALLEL and
MAKE-SERIES, but in general we could have any associative, commutative
combiners. UNUSABLE-COMBINER indicates one of the POSSIBLE-COMBINERS
which we are not allowed to use at this particular level of generation
(if it is not one of the POSSIBLE-COMBINERS - e.g. it is the empty
list - then we can use all of the possible combiners at this level of
generation).  The reason for this obscure argument is that it is our
means of implementing Rule #2 (ways of avoiding duplications resulting
>from associativity).  By guaranteeing that the left-branch of a
combination never uses (at top level) the combiner of the combination
in which it is embedded, we enforce the right-associative approach of
Rule #2.  UNUSABLE-COMBINER is a way of communicating what the
combiner of a combination is from the combination to its left-branch.

Rule #1 (eliminating duplicates resulting from commutativity) is
handled via the smarts in MAKE-SUBSET-PAIRS.  This procedure already
orders elements in its subsets according to priority, and orders
subsets within the pair according to priority.  By recursively
generating circuits based on these prioritized elements, we satisfy
the condition that the left-branch of a circuit will always have a
higher priority than the right branch (see the introductory notes on
Method 2 for a review of what "priority" means when applied to
circuits).

The following code, though short, is extremely dense.  A good way to
get a feel for what it is doing is to simulate it for a small number
of parts, say three.  Keep in mind how the procedure embodies Rule #1
and Rule #2 to avoid generating duiplicates.  (This procedure makes
use of MAPAPPEND, which is defined in the section on Method 1).

(define (generate-with-combiners list-of-parts
                                 unusable-combiner
                                 possible-combiners)
  (if (<= (length list-of-parts) 1) ; Handle the 0 and 1 cases specially
      list-of-parts
      (let ((subset-pairs (make-subset-pairs list-of-parts)))
        (mapappend 
          (lambda (pair)
            (let ((all (generate-all (cadr pair))))
              (mapappend
                (lambda (combiner)
                  (combine-all-pairs combiner 
                                     (generate-with-combiners (car pair) ; Left branch can't 
                                                              combiner   ; use combiner
                                                              possible-combiners)
                                     all)) ; Right branch can use all combiners
                (remove unusable-combiner possible-combiners))))
          subset-pairs))))


REMOVE removes all occurences of a given element from a list. (It uses
FILTER, which is described in Exercise 13).

(define (remove elt lst)
  (filter (lambda (thing)
            (not (eq? thing elt)))
          lst))


COMBINE-ALL-PAIRS is a generalization of the ALL-PAIRS procedure used
for Method 1.  It applies the two-argument COMBINER to all elements in
the Cartesian product of two lists and collects the results in a
single list.

(define (combine-all-pairs combiner lst1 lst2)
  (mapappend 
   (lambda (elt1)
     (mapcar
      (lambda (elt2)
        (combiner elt1 elt2))
      lst2))
   lst1))
 

CIRCUITS generates the possible circuits for a given list of parts and
prints out the circuits in an abbreviated form.  It also returns the
length of the resulting circuit list.

(define (circuits lst)
  (define (print-ckts ckts)
    (define (iter things num)
      (cond ((null? things) npo)
            (else
      (newline)
      (princ num)
      (princ ". ")
      (princ (short-form (car things)))
      (iter (cdr things) (1+ num)))))
    (iter ckts 1))
  (let ((result (generate-all lst)))
    (newline)
    (newline)
    (princ "There are ")
    (princ (length result))
    (princ " circuits for ")
    (princ (length lst))
    (princ " elements:")
    (newline)
    (print-ckts result)
    (length result)))


SHORT-FORM is a hack for printing out circuits in an abbreviated form.
It is so incredibly gross and inelegant that I refuse to include its
details here.


Some examples of CIRCUITS are given below (assume that R1, R2, R3, R4,
and R5 are defined to be different impedance elements).  These were
generated by Method 2, so the regularities implied by Rule #1 and Rule
#2 are quite apparent.

1 ==> (circuits (list r1))

There are 1 circuits for 1 elements:

1. R1
1 

1 ==> (circuits (list r1 r2))

There are 2 circuits for 2 elements:

1. (+ R1 R2)
2. (// R1 R2)
2 

1 ==> (circuits (list r1 r2 r3))

There are 8 circuits for 3 elements:

1. (+ R1 (+ R2 R3))
2. (+ R1 (// R2 R3))
3. (// R1 (+ R2 R3))
4. (// R1 (// R2 R3))
5. (+ (// R1 R2) R3)
6. (// (+ R1 R2) R3)
7. (+ (// R1 R3) R2)
8. (// (+ R1 R3) R2)
8 

1 ==> (circuits (list r1 r2 r3 r4))

There are 52 circuits for 4 elements:

1. (+ R1 (+ R2 (+ R3 R4)))
2. (+ R1 (+ R2 (// R3 R4)))
3. (+ R1 (// R2 (+ R3 R4)))
4. (+ R1 (// R2 (// R3 R4)))
5. (+ R1 (+ (// R2 R3) R4))
6. (+ R1 (// (+ R2 R3) R4))
7. (+ R1 (+ (// R2 R4) R3))
8. (+ R1 (// (+ R2 R4) R3))
9. (// R1 (+ R2 (+ R3 R4)))
10. (// R1 (+ R2 (// R3 R4)))
11. (// R1 (// R2 (+ R3 R4)))
12. (// R1 (// R2 (// R3 R4)))
13. (// R1 (+ (// R2 R3) R4))
14. (// R1 (// (+ R2 R3) R4))
15. (// R1 (+ (// R2 R4) R3))
16. (// R1 (// (+ R2 R4) R3))
17. (+ (// R1 R2) (+ R3 R4))
18. (+ (// R1 R2) (// R3 R4))
19. (// (+ R1 R2) (+ R3 R4))
20. (// (+ R1 R2) (// R3 R4))
21. (+ (// R1 (+ R2 R3)) R4)
22. (+ (// R1 (// R2 R3)) R4)
23. (+ (// (+ R1 R2) R3) R4)
24. (+ (// (+ R1 R3) R2) R4)
25. (// (+ R1 (+ R2 R3)) R4)
26. (// (+ R1 (// R2 R3)) R4)
27. (// (+ (// R1 R2) R3) R4)
28. (// (+ (// R1 R3) R2) R4)
29. (+ (// R1 (+ R2 R4)) R3)
30. (+ (// R1 (// R2 R4)) R3)
31. (+ (// (+ R1 R2) R4) R3)
32. (+ (// (+ R1 R4) R2) R3)
33. (// (+ R1 (+ R2 R4)) R3)
34. (// (+ R1 (// R2 R4)) R3)
35. (// (+ (// R1 R2) R4) R3)
36. (// (+ (// R1 R4) R2) R3)
37. (+ (// R1 (+ R3 R4)) R2)
38. (+ (// R1 (// R3 R4)) R2)
39. (+ (// (+ R1 R3) R4) R2)
40. (+ (// (+ R1 R4) R3) R2)
41. (// (+ R1 (+ R3 R4)) R2)
42. (// (+ R1 (// R3 R4)) R2)
43. (// (+ (// R1 R3) R4) R2)
44. (// (+ (// R1 R4) R3) R2)
45. (+ (// R1 R4) (+ R2 R3))
46. (+ (// R1 R4) (// R2 R3))
47. (// (+ R1 R4) (+ R2 R3))
48. (// (+ R1 R4) (// R2 R3))
49. (+ (// R1 R3) (+ R2 R4))
50. (+ (// R1 R3) (// R2 R4))
51. (// (+ R1 R3) (+ R2 R4))
52. (// (+ R1 R3) (// R2 R4))
52 


How do Method 1 and Method 2 compare in terms of time?  Although
circuit generation seems intrinsically to be an exponential process ,
there are clear differences in the efficiences of the two approaches.
Although Method 1 is more straightforward (to some people, at least),
removing duplicates is a time consuming process.  Method 2 is more
obscure, but has a lot more cleverness built into it.  Here are some
of the sample timings I got when testing the two methods.  Method 2 is
clearly superior up to 5-parts - Method 1 would take an unbearable
amount of time for 5 parts.

	# of Parts	# of Circuits	Method 1 Time	Method 2 Time
			(sec)	(sec)

	3	8	20.26	0.4
	4	52	968	2.6
	5	472	???	22.9

Unfortunately, Method 2 is also incredibly space-consuming, and when
applied to a list of six parts ran out of memory space on my Chipmunk.
For this reason, the number of circuits for 6 parts is still a
mystery! (Challenge: can you develop a more space effiecient version
of Method 2?)


