ToC

Power Series

This namespace contains an implementation of two data types:

  • Series, which represents a generic infinite series of arbitrary values, and
  • PowerSeries, a series that represents a power series in a single variable; in other words, a series where the nth entry is interpreted as the coefficient of Loading...:

[a b c d ...] Loading...

We'll proceed by building up implementations of the arithmetic operations +, -, *, / and a few others on bare Clojure lazy sequences, and then introduce deftype wrappers so that we can install these types into the generic dispatch system.

The implementation follows Doug McIlroy's beautiful paper, "Power Series, Power Serious". Doug also has a 10-line version in Haskell on his website.

Okay, let's proceed, in roughly the same order as the paper.

Sequence Operations

A 'series' is an infinite sequence of numbers, represented by Clojure's lazy sequence. First, a function ->series that takes some existing sequence, finite or infinite, and coerces it to an infinite seq by concatenating it with an infinite sequence of zeros. (We use g/zero-like so that everything plays nicely with generic arithmetic.)

(defn ->series
"Form the infinite sequence starting with the supplied values. The
remainder of the series will be filled with the zero-value
corresponding to the first of the given values."
[xs]
(lazy-cat xs (repeat (g/zero-like (first xs)))))
#object[emmy.series.impl$__GT_series 0x2b8802b4 "
emmy.series.impl$__GT_series@2b8802b4"
]

This works as expected:

(->clerk-only
(->series [1 2 3 4]))
(1 2 3 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10+ more elided)

The core observation we'll use in the following definitions (courtesy of McIlroy) is that a power series Loading... in a variable Loading...:

Loading...

Decomposes into a head element Loading... plus a tail series, multiplied by Loading...:

Loading...

We'll use this observation to derive the more complicated sequence operations below.

Negation

To negate a series, negate each element:

(defn negate [xs]
(map g/negate xs))
#object[emmy.series.impl$negate 0x1dc16244 "
emmy.series.impl$negate@1dc16244"
]
(->clerk-only
(let [xs [1 2 3 4]]
(negate (->series xs))))
(-1 -2 -3 -4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10+ more elided)

Addition

We can derive series addition by expanding the series Loading... and Loading... into head and tail and rearranging terms:

Loading...

This is particularly straightforward in Clojure, where map already merges sequences elementwise:

(defn seq:+ [f g]
(map g/+ f g))
#object[emmy.series.impl$seq_COLON__PLUS_ 0x270247f4 "
emmy.series.impl$seq_COLON__PLUS_@270247f4"
]
(->clerk-only
(take 5 (seq:+ (range) (range))))
(0 2 4 6 8)

A constant is a series with its first element populated, all zeros otherwise. To add a constant to another series we only need add it to the first element. Here are two versions, constant-on-left vs constant-on-right:

(defn c+seq [c f]
(lazy-seq
(cons (g/+ c (first f)) (rest f))))
#object[emmy.series.impl$c_PLUS_seq 0x5c2e97c4 "
emmy.series.impl$c_PLUS_seq@5c2e97c4"
]
(defn seq+c [f c]
(lazy-seq
(cons (g/+ (first f) c) (rest f))))
#object[emmy.series.impl$seq_PLUS_c 0x3758a357 "
emmy.series.impl$seq_PLUS_c@3758a357"
]
(->clerk-only
(let [series (->series [1 2 3 4])]
[(take 6 (seq+c series 10))
(take 6 (c+seq 10 series))]))
[(11 2 3 4 0 0) (11 2 3 4 0 0)]

Subtraction

Subtraction comes for free from the two definitions above:

(defn seq:- [f g]
(map g/- f g))
#object[emmy.series.impl$seq_COLON__ 0x219d24ba "
emmy.series.impl$seq_COLON__@219d24ba"
]
(->clerk-only
(take 5 (seq:- (range) (range))))
(0 0 0 0 0)

Subtract a constant from a sequence by subtracting it from the first element:

(defn seq-c [f c]
(lazy-seq
(cons (g/- (first f) c) (rest f))))
#object[emmy.series.impl$seq_c 0x1495ea9a "
emmy.series.impl$seq_c@1495ea9a"
]
(->clerk-only
(take 5 (seq-c (range) 10)))
(-10 1 2 3 4)

To subtract a sequence from a constant, subtract the first element as before, but negate the tail of the sequence:

(defn c-seq [c f]
(lazy-seq
(cons (g/- c (first f)) (negate (rest f)))))
#object[emmy.series.impl$c_seq 0x3c2ddf7b "
emmy.series.impl$c_seq@3c2ddf7b"
]
(->clerk-only
(take 5 (c-seq 10 (range))))
(10 -1 -2 -3 -4)

Multiplication

What does it mean to multiply two infinite sequences? As McIlroy notes, multiplication is where the lazy-sequence-based approach really comes into its own.

First, the simple cases of multiplication by a scalar on either side of a sequence:

(defn seq*c [f c] (map #(g/mul % c) f))
#object[emmy.series.impl$seq_STAR_c 0x4e1d91b2 "
emmy.series.impl$seq_STAR_c@4e1d91b2"
]
(defn c*seq [c f] (map #(g/mul c %) f))
#object[emmy.series.impl$c_STAR_seq 0x6edc43f "
emmy.series.impl$c_STAR_seq@6edc43f"
]

To multiply sequences, first recall from above that we can decompose each sequence Loading... and Loading... into a head and tail.

Mutiply the expanded representations out and rearrange terms:

Loading...

Loading... appears on the left and the right, so use an inner function that closes over Loading... to simplify matters, and rewrite the above definition in Clojure:

(defn seq:* [f g]
(letfn [(step [f]
(lazy-seq
(if (g/zero? (first f))
(cons (first f) (step (rest f)))
(let [f*g (g/mul (first f) (first g))
f*G1 (c*seq (first f) (rest g))
F1*G (step (rest f))]
(cons f*g (seq:+ f*G1 F1*G))))))]
(lazy-seq
(if (g/zero? (first g))
(cons (first g) (seq:* f (rest g)))
(step f)))))
#object[emmy.series.impl$seq_COLON__STAR_ 0x3d172f42 "
emmy.series.impl$seq_COLON__STAR_@3d172f42"
]

This works just fine on two infinite sequences:

(->clerk-only
(take 10 (seq:* (range) (->series [4 3 2 1]))))
(0 4 11 20 30 40 50 60 70 80)

NOTE This is also called the "Cauchy Product" of the two sequences: https://en.wikipedia.org/wiki/Cauchy_product The description on the Wikipedia page has complicated index tracking that simply doesn't come in to play with the stream-based approach. Amazing!

Division

The quotient Loading... of Loading... and Loading... should satisfy:

Loading...

From McIlroy, first expand out Loading..., Loading... and one instance of Loading...:

Loading...

Look at just the constant terms and note that Loading....

Consider the terms multiplied by Loading... and solve for Loading...:

Loading...

There are two special cases to consider:

  • If Loading..., Loading... can only succeed if Loading...; in this case, $Q = {F_1 \over G1}$, from the larger formula above.
  • If Loading..., Loading...

Encoded in Clojure:

(defn div [f g]
(lazy-seq
(let [f0 (first f) fs (rest f)
g0 (first g) gs (rest g)]
(cond (and (g/zero? f0) (g/zero? g0))
(div fs gs)
(g/zero? f0)
(cons f0 (div fs g))
(g/zero? g0)
(u/arithmetic-ex "ERROR: denominator has a zero constant term")
:else (let [q (g/div f0 g0)]
(cons q (-> (seq:- fs (c*seq q gs))
(div g))))))))
#object[emmy.series.impl$div 0x6f13e99c "
emmy.series.impl$div@6f13e99c"
]

A simple example shows success:

(->clerk-only
(let [series (->series [0 0 0 4 3 2 1])]
(take 5 (div series series))))
(1 0 0 0 0)

Reciprocal

We could generate the reciprocal of Loading... by dividing Loading... by Loading.... Page 21 of an earlier paper by McIlroy gives us a more direct formula.

We want Loading... such that Loading.... Expand Loading...:

Loading...

Solve for R:

Loading...

A recursive definition is no problem in the stream abstraction:

(defn invert [f]
(lazy-seq
(let [finv (g/invert (first f))
F1*Finv (seq:* (rest f) (invert f))
tail (c*seq finv (negate F1*Finv))]
(cons finv tail))))
#object[emmy.series.impl$invert 0x4da175cd "
emmy.series.impl$invert@4da175cd"
]

This definition of invert matches the more straightforward division implementation:

(->clerk-only
(let [series (iterate inc 3)]
(= (take 5 (invert series))
(take 5 (div (->series [1]) series)))))
true

An example:

(->clerk-only
(let [series (iterate inc 3)]
[(take 5 (seq:* series (invert series)))
(take 5 (div series series))]))
[(1N 0N 0N 0N 0N) (1 0 0 0 0)]

Division of a constant by a series comes easily from our previous multiplication definitions and invert:

(defn c-div-seq [c f]
(c*seq c (invert f)))
#object[emmy.series.impl$c_div_seq 0x25858c7a "
emmy.series.impl$c_div_seq@25858c7a"
]

It's not obvious that this works:

(->clerk-only
(let [nats (iterate inc 1)]
(take 6 (c-div-seq 4 nats))))
(4 -8 4 0 0 0)

But we can recover the initial series:

(->clerk-only
(let [nats (iterate inc 1)
divided (c-div-seq 4 nats)
seq-over-4 (invert divided)
original (seq*c seq-over-4 4)]
[(take 5 nats)
(take 5 original)]))
[(1 2 3 4 5) (1N 2N 3N 4N 5N)]

To divide a series by a constant, divide each element of the series:

(defn seq-div-c [f c]
(map #(g// % c) f))
#object[emmy.series.impl$seq_div_c 0x21eccab7 "
emmy.series.impl$seq_div_c@21eccab7"
]

Division by a constant undoes multiplication by a constant:

(->clerk-only
(let [nats (iterate inc 1)]
(take 5 (seq-div-c (seq*c nats 2) 2))))
(1 2 3 4 5)

Functional Composition

To compose two series Loading... and Loading... means to create a new series Loading.... Derive this by substituting Loading... for Loading... in the expansion of Loading...:

Loading...

For the stream-based calculation to work, we need to be able to calculate the head element and attach it to an infinite tail; unless Loading... above the head element depends on Loading..., an infinite sequence.

If Loading... the calculation simplifies:

Loading...

In Clojure, using an inner function that captures Loading...:

(defn compose [f g]
(letfn [(step [f]
(lazy-seq
;; TODO I don't understand why we get a StackOverflow if I move
;; this assertion out of the `letfn`.
(assert (g/zero? (first g)))
(let [[f0 & fs] f
gs (rest g)
tail (seq:* gs (step fs))]
(cons f0 tail))))]
(step f)))
#object[emmy.series.impl$compose 0x5143881f "
emmy.series.impl$compose@5143881f"
]

Composing Loading... should square all Loading...s, and give us a sequence of only even powers:

(->clerk-only
(take 10 (compose (repeat 1)
(->series [0 0 1]))))
(1 0 1 0 1 0 1 0 1 0)

Reversion

The functional inverse of a power series Loading... is a series Loading... that satisfies Loading....

Following McIlroy, we expand Loading... (substituting Loading... for Loading...) and one occurrence of Loading...:

Loading...

Just like in the composition derivation, in the general case the head term depends on an infinite sequence. Set Loading... to address this:

Loading...

For this to work, the constant Loading... must be 0 as well, hence

Loading...

This works as an implementation because Loading.... Loading... is allowed to reference Loading... thanks to the stream-based approach:

(defn revert [f]
{:pre [(g/zero? (first f))]}
(letfn [(step [f]
(lazy-seq
(let [F1 (rest f)
R (step f)]
(cons 0 (invert
(compose F1 R))))))]
(step f)))
#object[emmy.series.impl$revert 0x170a90e7 "
emmy.series.impl$revert@170a90e7"
]

An example, inverting a series starting with 0:

(->clerk-only
(let [f (cons 0 (iterate inc 1))]
(take 5 (compose f (revert f)))))
(0 1 0 0 0)

Series Calculus

Derivatives of power series are simple and mechanical:

Loading...

Implies that all entries shift left by 1, and each new entry gets multiplied by its former index (i.e., its new index plus 1).

(defn deriv [f]
(map g/* (rest f) (iterate inc 1)))
#object[emmy.series.impl$deriv 0x1e652820 "
emmy.series.impl$deriv@1e652820"
]
(->clerk-only
;; 1 + 2x + 3x^2 + ...
(take 6 (deriv (repeat 1))))
(1 2 3 4 5 6)

The definite integral Loading... is similar. To take the anti-derivative of each term, move it to the right by appending a constant term onto the sequence and divide each element by its new position:

(defn integral
([s] (integral s 0))
([s constant-term]
(cons constant-term
(map g/div s (iterate inc 1)))))
#object[emmy.series.impl$integral 0x591bf029 "
emmy.series.impl$integral@591bf029"
]

With a custom constant term:

(->clerk-only
(take 6 (integral (iterate inc 1) 5)))
(5 1 1 1 1 1)

By default, the constant term is 0:

(->clerk-only
(take 6 (integral (iterate inc 1))))
(0 1 1 1 1 1)

Exponentiation

Exponentiation of a power series by some integer is simply repeated multiplication. The implementation here is more efficient the iterating seq:*, and handles negative exponent terms by inverting the original sequence.

(defn expt [s e]
(letfn [(expt [base pow]
(loop [n pow
y (->series [1])
z base]
(let [t (even? n)
n (quot n 2)]
(cond
t (recur n y (seq:* z z))
(zero? n) (seq:* z y)
:else (recur n (seq:* z y) (seq:* z z))))))]
(cond (pos? e) (expt s e)
(zero? e) (->series [1])
:else (invert (expt s (g/negate e))))))
#object[emmy.series.impl$expt 0x14558865 "
emmy.series.impl$expt@14558865"
]

We can use expt to verify that Loading... expands to Loading...:

(->clerk-only
(take 5 (expt (->series [1 1]) 3)))
(1 3 3 1 0)

Square Root of a Series

The square root of a series Loading... is a series Loading... such that Loading.... We can find this using our calculus methods from above:

Loading...

or

Loading...

When the head term of Loading... is nonzero, i.e., Loading..., the head of Loading... must be Loading... for the multiplication to work out.

Integrate both sides:

Loading...

One optimization appears if the first two terms of Loading... vanish, i.e., Loading.... In this case Loading....

Here it is in Clojure:

(defn sqrt [[f1 & [f2 & fs] :as f]]
(if (and (g/zero? f1)
(g/zero? f2))
(cons f1 (sqrt fs))
(let [const (g/sqrt f1)
step (fn step [g]
(lazy-seq
(-> (div (deriv g)
(c*seq 2 (step g)))
(integral const))))]
(step f))))
#object[emmy.series.impl$sqrt 0x579bafa0 "
emmy.series.impl$sqrt@579bafa0"
]

And a test that we can recover the naturals:

(->clerk-only
(let [xs (iterate inc 1)]
(take 6 (seq:* (sqrt xs)
(sqrt xs)))))
(1 2 3 4 5 6)

We can maintain precision of the first element is the square of a rational number:

(->clerk-only
(let [xs (iterate inc 9)]
(take 6 (seq:* (sqrt xs)
(sqrt xs)))))
(9 10N 11N 12N 13N 14N)

We get a correct result if the sequence starts with 0, 0:

(let [xs (concat [0 0] (iterate inc 9))]
(->clerk-only
(take 6 (seq:* (sqrt xs)
(sqrt xs)))))
(0 0 9 10N 11N 12N)

Examples

Power series computations can encode polynomial computations. Encoding Loading... as a power series returns the correct result:

(->clerk-only
(take 10 (expt (->series [1 0 -2]) 3)))
(1 0 -6 0 12 0 -8 0 0 0)

Encoding Loading... returns the power series Loading... which sums to that value in its region of convergence:

(->clerk-only
(take 10 (div (->series [1])
(->series [1 -1]))))
(1 1 1 1 1 1 1 1 1 1)

Loading... is the derivative of the above series:

(->clerk-only
(take 10 (div (->series [1])
(-> (->series [1 -1])
(expt 2)))))
(1 2 3 4 5 6 7 8 9 10)

Various Power Series

With the above primitives we can define a number of series with somewhat astonishing brevity.

Loading... is its own derivative, so Loading...:

(def expx
(lazy-seq
(integral expx 1)))
(1 1 1/2 1/6 1/24 1/120 1/720 1/5040 1/40320 1/362880 1/3628800 1/39916800 1/479001600 1/6227020800 1/87178291200 1/1307674368000 1/20922789888000 1/355687428096000 1/6402373705728000 1/121645100408832000 10+ more elided)

This bare definition is enough to generate the power series for Loading...:

(->clerk-only
(take 10 expx))
(1 1 1/2 1/6 1/24 1/120 1/720 1/5040 1/40320 1/362880)

Loading... and Loading... afford recursive definitions. Loading... and $D(cos) = -sin$, so (with appropriate constant terms added) on:

(declare cosx)
(1 0 -1/2 0 1/24 0 -1/720 0 1/40320 0 -1/3628800 0 1/479001600 0 -1/87178291200 0 1/20922789888000 0 -1/6402373705728000 0 10+ more elided)
(def sinx (lazy-seq (integral cosx)))
(0 1 0 -1/6 0 1/120 0 -1/5040 0 1/362880 0 -1/39916800 0 1/6227020800 0 -1/1307674368000 0 1/355687428096000 0 -1/121645100408832000 10+ more elided)
(def cosx (lazy-seq (c-seq 1 (integral sinx))))
(1 0 -1/2 0 1/24 0 -1/720 0 1/40320 0 -1/3628800 0 1/479001600 0 -1/87178291200 0 1/20922789888000 0 -1/6402373705728000 0 10+ more elided)

tangent and secant come easily from these:

(def tanx (div sinx cosx))
(0 1 0 1/3 0 2/15 0 17/315 0 62/2835 0 1382/155925 0 21844/6081075 0 929569/638512875 0 6404582/10854718875 0 443861162/1856156927625 10+ more elided)
(def secx (invert cosx))
(1 0 1/2 0 5/24 0 61/720 0 277/8064 0 50521/3628800 0 540553/95800320 0 199360981/87178291200 0 3878302429/4184557977600 0 2404879675441/6402373705728000 0 10+ more elided)

Reversion lets us define arcsine from sine:

(def asinx (revert sinx))
(0 1 0 1/6 0 3/40 0 5/112 0 35/1152 0 63/2816 0 231/13312 0 143/10240 0 6435/557056 0 12155/1245184 10+ more elided)
(def atanx (integral (cycle [1 0 -1 0])))
(0 1 0 -1/3 0 1/5 0 -1/7 0 1/9 0 -1/11 0 1/13 0 -1/15 0 1/17 0 -1/19 10+ more elided)

These two are less elegant, perhaps:

(def acosx (c-seq (/ Math/PI 2) asinx))
(1.5707963267948966 -1 0 -1/6 0 -3/40 0 -5/112 0 -35/1152 0 -63/2816 0 -231/13312 0 -143/10240 0 -6435/557056 0 -12155/1245184 10+ more elided)
(def acotx (c-seq (/ Math/PI 2) atanx))
(1.5707963267948966 -1 0 1/3 0 -1/5 0 1/7 0 -1/9 0 1/11 0 -1/13 0 1/15 0 -1/17 0 1/19 10+ more elided)

The hyperbolic trig functions are defined in a similar way:

(declare sinhx)
(0 1 0 1/6 0 1/120 0 1/5040 0 1/362880 0 1/39916800 0 1/6227020800 0 1/1307674368000 0 1/355687428096000 0 1/121645100408832000 10+ more elided)
(def coshx (lazy-seq (integral sinhx 1)))
(1 0 1/2 0 1/24 0 1/720 0 1/40320 0 1/3628800 0 1/479001600 0 1/87178291200 0 1/20922789888000 0 1/6402373705728000 0 10+ more elided)
(def sinhx (lazy-seq (integral coshx)))
(0 1 0 1/6 0 1/120 0 1/5040 0 1/362880 0 1/39916800 0 1/6227020800 0 1/1307674368000 0 1/355687428096000 0 1/121645100408832000 10+ more elided)
(def tanhx (div sinhx coshx))
(0 1 0 -1/3 0 2/15 0 -17/315 0 62/2835 0 -1382/155925 0 21844/6081075 0 -929569/638512875 0 6404582/10854718875 0 -443861162/1856156927625 10+ more elided)
(def asinhx (revert sinhx))
(0 1 0 -1/6 0 3/40 0 -5/112 0 35/1152 0 -63/2816 0 231/13312 0 -143/10240 0 6435/557056 0 -12155/1245184 10+ more elided)
(def atanhx (revert tanhx))
(0 1 0 1/3 0 1/5 0 1/7 0 1/9 0 1/11 0 1/13 0 1/15 0 1/17 0 1/19 10+ more elided)
(def log1-x
(integral (repeat -1)))
(0 -1 -1/2 -1/3 -1/4 -1/5 -1/6 -1/7 -1/8 -1/9 -1/10 -1/11 -1/12 -1/13 -1/14 -1/15 -1/16 -1/17 -1/18 -1/19 10+ more elided)
(def log1+x
(integral (cycle [1 -1])))
(0 1 -1/2 1/3 -1/4 1/5 -1/6 1/7 -1/8 1/9 -1/10 1/11 -1/12 1/13 -1/14 1/15 -1/16 1/17 -1/18 1/19 10+ more elided)

Generating Functions

Catalan numbers

These are a few more examples from McIlroy's "Power Serious" paper, presented here without context. (If you have the energy to write about these, please feel free and send us a PR!)

(def catalan
(lazy-cat [1] (seq:* catalan catalan)))
(1 1 2 5 14 42 132 429 1430 4862 16796 58786 208012 742900 2674440 9694845 35357670 129644790 477638700 1767263190 10+ more elided)

ordered trees...

(declare tree' forest' list')
(0 1 1 2 5 14 42 132 429 1430 4862 16796 58786 208012 742900 2674440 9694845 35357670 129644790 477638700 10+ more elided)
(def tree' (lazy-cat [0] forest'))
(0 1 1 2 5 14 42 132 429 1430 4862 16796 58786 208012 742900 2674440 9694845 35357670 129644790 477638700 10+ more elided)
(def list' (lazy-cat [1] list'))
(1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 10+ more elided)
(def forest' (compose list' tree'))
(1 1 2 5 14 42 132 429 1430 4862 16796 58786 208012 742900 2674440 9694845 35357670 129644790 477638700 1767263190 10+ more elided)

The catalan numbers again!

(def fib (lazy-cat [0 1] (map + fib (rest fib))))
(0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 10+ more elided)
(defn binomial* [n]
(letfn [(f [acc prev n k]
(if (zero? n)
acc
(let [next (/ (* prev n) k)
acc' (conj! acc next)]
(recur acc' next (dec n) (inc k)))))]
(persistent!
(f (transient [1]) 1 n 1))))
#object[emmy.series.impl$binomial_STAR_ 0x6008a95b "
emmy.series.impl$binomial_STAR_@6008a95b"
]
(defn binomial
"The coefficients of (1+x)^n"
[n]
(->series (binomial* n)))
#object[emmy.series.impl$binomial 0x661497cb "
emmy.series.impl$binomial@661497cb"
]
(def harmonic
"The sequence of [Harmonic
numbers](https://en.wikipedia.org/wiki/Harmonic_number), starting from n=1."
(reductions
g/+ (map g// (iterate inc 1))))
(1 3/2 11/6 25/12 137/60 49/20 363/140 761/280 7129/2520 7381/2520 83711/27720 86021/27720 1145993/360360 1171733/360360 1195757/360360 2436559/720720 42142223/12252240 14274301/4084080 275295799/77597520 55835135/15519504 10+ more elided)
(def bell
"The sequence of [Bell numbers](https://en.wikipedia.org/wiki/Bell_number),
starting from n=1."
(map sf/bell (iterate inc 1)))
(1 2 5 15 52 203 877 4140 21147 115975 678570 4213597 27644437 190899322 1382958545 10480142147 82864869804 682076806159 5832742205057 51724158235372 10+ more elided)