Integer Triangle Traits
A Julia Implementation
Part II

Outline

In three parts, we want to consider how the programming language Julia can be applied in the study of integer sequences, in particular of integer triangles.

The article is written in the hope of making it easier for friends of number sequences (SeqFans) to get started with the Julia language. And conversely, that it might animate one or the other member of the Julia community to write contributions for the OEIS. It can also be read as an introductory tutorial to the registered package "IntegerTriangle".

We choose Julia because it is currently (2020) the language best suited for many tasks in scientific computing and, equally important, is an elegant language that makes coding fun. As one co-creator of Julia puts it: "Julia was designed to help researchers write high-level code in an intuitive syntax and produce code with the speed of production programming languages."

Julia is free and open-source. We use version 1.4 which was released in 2020. This article and the code is available under the Creative Commons Attribution 3.0 license.

Integer triangle trait cards

Let us begin the second part by re-focusing on the goal of our efforts: the creation of "Integer Triangle Trait Cards"™. This is a compilation of the essential characteristics of a triangle, whereby we understand the characteristics of a triangle to be sequences that can be obtained from the triangle by elementary transformations.

The definition of most traits were given in the last chapter. The missing traits, PosHalf, NegHalf, N0TS and NATS will be explained below.

A first example is the famous triangle of binomial coefficients.

🔶 Pascal's Δ

[0]  1
[1]  1  1
[2]  1  2   1
[3]  1  3   3   1
[4]  1  4   6   4   1
[5]  1  5  10  10   5   1
[6]  1  6  15  20  15   6   1
[7]  1  7  21  35  35  21   7   1
[8]  1  8  28  56  70  56  28   8   1
[9]  1  9  36  84 126 126  84  36   9   1

Trait        Seq                         Anum     Author   Year
Δ Binomial:  1, 1, 1, 1, 2, 1, 1, 3, ... A007318  Sloane   1994
Δ InvBinom:  1, -1, 1, 1, -2, 1, -1, ... A130595  Deléham  2007
Δ DiagBinom: 1, 1, 1, 1, 1, 2, 1, 3, ... A011973  Sloane   1996
Δ PolyBinom: 1, 1, 1, 1, 2, 1, 1, 4 ...  A009998  Sloane   1996
--
Sum:         1, 2, 4, 8, 16, 32, 64, ... A000079  Sloane   1973
EvenSum:     1, 1, 2, 4, 8, 16, 32, ...  A011782  Killough 1996
OddSum:      0, 1, 2, 4, 8, 16, 32, ...  A131577  Curtz    2007
AltSum:      1, 0, 0, 0, 0, 0, 0, ...    A000007  Sloane   1994
DiagSum:     1, 1, 2, 3, 5, 8, 13, ...   A000045* Sloane   1964
Central:     1, 2, 6, 20, 70, 252, ...   A000984  Sloane   1973
LeftSide:    1, 1, 1, 1, 1, 1, 1, ...    A000012  Sloane   1994
RightSide:   1, 1, 1, 1, 1, 1, 1, ...    A000012  Sloane   1994
PosHalf:     1, 3, 9, 27, 81, 243, ...   A000244  Sloane   1973
NegHalf:     1, -1, 1, -1, 1, -1, ...    A033999  Danilov  1998
N0TS:        0, 1, 4, 12, 32, 80, ...    A001787  Sloane   1973
NATS:        1, 3, 8, 20, 48, 112, ...   A001792  Sloane   1973

--
A000045*(n) = A000045(n+1)

The date gives the year the sequence was entered into the EIS, where EIS stands for Handbook or Encyclopedia or OEIS.
The sequence A000045*(n) = A000045(n+1) does not have an independent entry. Judging from the entries in A000045 and by the fact, that DiagSum(Pascal) = Fibonacci is a perfect definition for this sequence, this arguably would have been justified.

Binomial transform

Given a sequence s and a function T defined for integers 0 ≤ k ≤ n with T(n,n) != 0 ∀ n, and 0 otherwise. Then we call the sequence Ts the T-transform of s if
    Ts(n) = Sum(0 ≤ k ≤ n) T(n,k)*s(k) (∀ n ≥ 0).
iT is the inverse transform of T if the matrix iT(n,k) is the inverse matrix of the matrix T(n,k).

If T(n,k) = binomial(n,k) then T is the binomial transform and (n,k) ↦ iT(n,k) = (-1)^(n,k)*binomial(n,k) the inverse binomial transform. By the binomial theorem, binomial(n,k) and (-1)^(n,k)*binomial(n,k) are the connection coefficients between the bases {x^n} and {(x-1)^n}.

The definitions given here are consistent with their use in the OEIS, especially with the definition in Sloane's 'Transforms'. Unfortunately, there are Internet sources that deviate from this definition. Note that Wikipedia is unreliable in this respect and gives a wrong definition! A good reference is, for example, "Combinatorial Theory" by Martin Aigner.

Implementing the binomial transform

The conventional way to implement the binomial transform looks similar to the function below:

???:  function BinomialTransform(seq)
          sz = size(seq, 1)
          T = ℤSequence(sz)
          T[1] = seq[1]
          for k ∈ 1:sz-1
              next = ℤ(0)
              for j ∈ 0:k
                  next += binomial(k, j)*seq[j+1]
              end
              T[k+1] = next
          end
          T
      end

Our approach is different. To keep it general, we first extract the content of the two loops: they are nothing more than a matrix times vector multiplication, as well known from linear algebra. In fact, the transformations we are looking at are linear.

LinMap(F::Function,V::ℤSeq,n) = sum(F(n-1)[k]*V[k] for k ∈ 1:n)
LinMap(F::Function,V::ℤSeq) = LinMap(F, V, length(V))

LinMap(M::ℤTri,V::ℤSeq,n) = sum(M[n][k] * V[k] for k ∈ 1:n)
LinMap(M::ℤTri,V::ℤSeq) = (n -> LinMap(M, V, n)).(1:length(V))

The call parameters differ depending on whether the transformation kernel is given as a function or as a triangle; the result is the same in both cases.

[Homework] If K(n) = [ℤ(1) for _ ∈ 0:n] and L(n) = [ℤ(k) for k ∈ 0:n] what is [LinMap(K, K(n)) for n ∈ 0:9] respectively [LinMap(L, L(n)) for n ∈ 0:9]?

Now to come back to our example: First we assume that we have two incarnations of the binomial function (called methods in Julia), Binomial(n) which returns the n-th row of Pascal's triangle, and Binomial(n,k) which returns the k-th element of the n-th row. We have already seen this interface in part 1 for the Lah numbers.

Our implementation are three one-liners and has an entirely different look and feel than the above implementation.

DEF: Telescope(V::ℤSequence) = (V[1:k] for k ∈ 1:size(V,1))
     Binomial(V::ℤSequence) = LinMap(Binomial, V)
     BinomialTransform(V::ℤSequence) = Binomial.(Telescope(V))

The Binomial Transform

So what is happening here? Let's start with the second line. Here the binomial function is extended to Vectors and defined as the image of the vector V under the linear transform associated with the binomial matrix.

In the third line, the binomial function is applied to the Telescope of the sequence V. The telescope of V is the expansion of V into a sequence of sequences (i.e., into a ℤTriangle), where the n-th row is the initial segment of V of length n+1.

At this point, one should note a small but important syntactical difference. We have defined Telescope(V) = (V[1:k] ...) and not [V[1:k] ...]. In other words, we used round brackets instead of square brackets. In consequence, Telescope(V) is not an array but an iterator, and the transformation is more efficient.

Much of the joy of coding with Julia derives from the fact that Julia makes it easy to formulate the notions on which the computations are based so easily and succinctly. This makes it very different from 'old style' coding, which is chasing indices through nested loops.

But this is by far not the end of the story. By purely formal substitution of the name in the last two lines (Binomial → LahNumbers), one gets the Lah transform, a transform Sloane added to his Transform library in 2003 at the suggestion of Vladeta Jovovic.

Def: LahNumbers(A::ℤSequence) = LinMap(LahNumbers, A, length(A))
     LahTransform(A) = LahNumbers.(Telescope(A))

And this procedure works for any integer triangle Δ! All it needs are two functions with the same interface as the LahNumbers and the Binomial coefficients above: Δ(n) and Δ(n,k).

Def: Δ(A::ℤSequence) = LinMap(Δ, A, length(A))
     ΔTransform(A::ℤSequence) = Δ.(Telescope(A))

In part 4 of this series we will discuss Julia's macro language and take things a step further (into the realm of metaprogramming) and show how one can generate all these functions with a macro for a given triangle.

Transformed sequences as traits of triangles.

Let's look at an example, where we also use the Motzkin transform derived from the Motzkin triangle defined in the next chapter:

IN:   U = [ℤ(1) for _ ∈ 0:8]
      BinomialTransform(U) |> println
      LahTransform(U)      |> println
      MotzkinTransform(U)  |> println

OUT:  [1, 2, 4, 8, 16, 32, 64, 128, 256]
      [1, 1, 3, 13, 73, 501, 4051, 37633, 394353]
      [1, 2, 5, 13, 35, 96, 267, 750, 2123]

If we look at the transformed sequences, we see that they connect to things that are known to be closely related to the notions behind the respective triangles: Binomial ↦ A000079; Lah ↦ A000262; Motzkin ↦ A005773. In other words, they show characteristic traits of the triangles.

The traits N0TS and NATS.

We are now adding two more to our collection of traits of a triangle, the sequences arising from the transformation of the fundamental sequences:

We call the transform of the positive natural numbers NATS and the transform of the non-negative natural numbers N0TS. Perhaps this is not the very summit of scientific terminology, but quite suitable for distinguishing these two cases in daily use.

Note that the important transform

is already in our list of traits: it generates the row sums of triangles.

DEFS:  N0TS(T::Function) = T.(0:n-1)
       NATS(T::Function) = T.(1:n)

In our context, we simply assume that the function T has been defined as a transformation in the sense of the last section.

The inverse binomial transform

In mathematics, inverse transformations are often more important than direct ones. And often more difficult to calculate. Fortunately, this is not the case here, we can reuse our scheme introduced above with a slight variation.

Again, the construction is based on the concept of the telescope of a sequence. So we assign a triangle to a sequence that consists of the initial segments of the sequence, only that this time the terms are signed.

DEF: SignedTelescope(A::ℤSequence) = 
         ([(-1)^(k-j)*A[j] for j ∈ 1:k] for k ∈ 1:size(A,1))

     SignedTelescope(len::Int, f::Function) = 
         ([(-1)^(k-j)*ZZ(f(j-1)) for j ∈ 1:k] for k ∈ 1:len)

The first of the two incarnations corresponds exactly to the above definition of the telescope except for the factor (-1)^(k-j). The second variant returns the same thing, except that the first len terms of the sequence are calculated first with the function f. This form is sometimes more flexible in application.

The inverse binomial transformation is now defined as the binomial applied to the signed telescope of the sequence.

DEF: 
BinomialInvTransform(A::ℤSequence) = Binomial.(SignedTelescope(A))

BinomialInvTransform(len::Int, f::Function) = 
                           Binomial.(SignedTelescope(len, f))

The Inverse Binomial Transform

It is obvious that in this construction the binomial function has only an exemplary character. Just as described in the case of the direct transformation, we can use this setup as a blueprint for the definition of any inverse transformation which we will consider.

ΔInvTransform(A::ℤSequence) = Δ.(SignedTelescope(A))
ΔInvTransform(len::Int, f::Function) = Δ.(SignedTelescope(len,f))

This suggests to take the final step and remove the type of the transformation from the name and pass it in as parameter instead.

    InvTransform(Δ, A::ℤSequence) = Δ.(SignedTelescope(A))
    InvTransform(Δ, len::Int, f::Function) = 
                                    Δ.(SignedTelescope(len, f))

The Fubini/Worpitzky Triangles

As a first application we consider the calculation of the Fubini triangle A131689. The question is: Consider the power functions (x,n) -> x^n. What is the inverse binomial transform of this sequence? Since the functions depend on a parameter n, let's look at the entire sequence of the transforms and put them row by row into a triangle. One possible implementation is:

function SomeTriangle(dim::Int, f::Function)
    T = ℤTriangle(dim)
    for n ∈ 0:dim-1
        T[n+1] = BinomialInvTransform(n+1, x -> f(x, n))
    end
    T
end

After a brief consideration, it is clear that such a function could also be useful for transformations other than the inverse binomial transform. Also, we do not like for-loops. In fact we can avoid the loop here if we remember the constructors of triangles. This brings us to the following form:

TransformTriangle(Trans::Function, dim::Int, f::Function) = 
                   ℤTriangle(dim, n -> Trans(n+1, x -> f(x, n)))

A hat tip goes to Haskell Curry. Let's look at the result, the Fubini triangle, which is the inverse binomial transform of the power functions:

IN:   FubiniTriangle(dim) = 
      TransformTriangle(BinomialInvTransform, dim, (x,n) -> x^n)

      FubiniTriangle(8)

OUT:  8-element Array{Array{fmpz,1},1}:
      [1]
      [0, 1]
      [0, 1, 2]
      [0, 1, 6, 6]
      [0, 1, 14, 36, 24]
      [0, 1, 30, 150, 240, 120]
      [0, 1, 62, 540, 1560, 1800, 720]
      [0, 1, 126, 1806, 8400, 16800, 15120, 5040]

But the story doesn't end here. The Fubini triangle has an elder brother, the Worpitzky triangle, from which it differs only in the signs. If one forms the row sums of the Worpitzky triangle in the form Sum(k=0..n) T(n,k)/(k+1), then one gets the Bernoulli numbers, as Worpitzky showed.

But the following is even more descriptive: If the terms of the Worpitzky triangle are interpreted as the coefficients of polynomials Fn(x), then
    Integral{x=0..1} Fn(x) = Bn(1),
where Bn(x) are the Bernoulli polynomials. In other words, the integrals equal the Bernoulli numbers. See also the illustration in A278075.

A prototypical triangle module

Before we go any further, we want to show how what has been said so far can be put together in a Julia module. As an example, we consider the Laguerre triangle.

module LaguerreTriangles

    using IntegerTriangles

    export Laguerre, LaguerreTriangle
    export LaguerreTransform, LaguerreInvTransform

    const CacheLaguerre = Dict{Int, ℤSequence}(0 => [ℤInt(1)])

    function Laguerre(n::Int)
        haskey(CacheLaguerre, n) && return CacheLaguerre[n]
        prevrow = Laguerre(n-1)
        row = ℤSequence(n+1)
        row[n+1] = ℤInt(1)
        for k ∈ 1:n
            row[k] = ( get(prevrow, k-1, 0)
                     + get(prevrow, k,   0)*(2*k-1)
                     + get(prevrow, k+1, 0)*k^2
                     )
        end
        CacheLaguerre[n] = row
    end

    Laguerre(n::Int, k::Int) = Laguerre(n)[k+1]
    Laguerre(A::ℤSequence) = LinMap(Laguerre, A)

    function LaguerreTriangle(size::Int)
        length(CacheLaguerre) < size && Laguerre(size)
        [CacheLaguerre[n] for n ∈ 0:size-1]
    end

    LaguerreTransform(A::ℤSequence) = Laguerre.(Telescope(A))
    LaguerreInvTransform(A::ℤSequence) = 
                                Laguerre.(SignedTelescope(A))
end

The functions LinMap and Telescope (and of course the types ℤInt and ℤSequence) are imported from our package IntegerTriangles. We leave it up to the reader to look up other details in the Julia documentation (especially the use of the get function, which takes care that the values outside the range of prevrow are handled correctly.)

And here is the trait card of the Laguerre triangle:

🔶 Laguerre Δ

[0]      1
[1]      1       1
[2]      2       4       1
[3]      6      18       9       1
[4]     24      96      72      16       1
[5]    120     600     600     200      25      1
[6]    720    4320    5400    2400     450     36     1
[7]   5040   35280   52920   29400    7350    882    49    1
[8]  40320  322560  564480  376320  117600  18816  1568   64  1
[9] 362880 3265920 6531840 5080320 1905120 381024 42336 2592 81 1

Trait      Seq                             Anum     Author     Year
Δ Triangle 1, 1, 1, 2, 4, 1, 6, 18, 9, ... A021009* Sloane     1999
Δ Inverse  -1, 1, 2, -4, 1, -6, 18, -9, .. A021009# Sloane     1999
Δ Diagonal 1, 1, 2, 1, 6, 4, 24, 18, 1, .. A084950  Adamson    2003
Δ Polynom  1, 1, 1, 2, 2, 1, 6, 7, 3, ..   A289192  Heinz      2017
--
Sum        1, 2, 7, 34, 209, 1546, ...     A002720  Sloane     1973
EvenSum    1, 1, 3, 15, 97, 745, 6571, ... A331325  Luschny    2020
OddSum     0, 1, 4, 19, 112, 801, 6756, .. A331326  Luschny    2020
AltSum     1, 0, -1, -4, -15, -56, ...     A009940  Meeussen   1999
DiagSum    1, 1, 3, 10, 43, 225, 1393, ... A001040* Sloane     1973
Central    1, 4, 72, 2400, 117600, ...     A295383* Gutkovskiy 2017
LeftSide   1, 1, 2, 6, 24, 120, 720, ...   A000142  Sloane     1973
RightSide  1, 1, 1, 1, 1, 1, 1, 1, 1, ...  A000012  Sloane     1994
PosHalf    1, 3, 17, 139, 1473, 19091, ... A025167  Meeussen   1999
NegHalf    1, -1, 1, 7, -127, 1711, ...    A025166* Meeussen   1999
N0TS       0, 1, 6, 39, 292, 2505, ...     A103194  Jovovic    2005
NATS       1, 3, 13, 73, 501, 4051, ...    A000262* Sloane     1973

---
A021009* = unsigned A021009.
A021009# = A021009 with different signs.
A001040* = A001040 shifted left once.
A295383* = abs(A295383).
A025166* = A025166 with different signs.
A000262* = A000262 shifted left once.

Julia's Romeo is Nemo

Julia is not a CAS. Julia is a computing language that was designed aiming at numerical computing and current methods of data modeling. So one cannot expect the availability of symbolic mathematics or implementations of algebraic structures like polynomial rings out of the box. But fortunately, some excellent packages exist which provide much of this. Particularly noteworthy is the package Nemo which is used in this article. It provides Julia bindings for various mathematical libraries (including flint, a fast library for number theory). William Hart and others develop it.

IN:  using Nemo

In the following sections, we will make use of polynomial rings and power series imported from Nemo. We introduce some type aliases and constructors.

DEFS:  
  const RationalSequence = Array{fmpq,1}
  const RationalTriangle = Array{Array{fmpq,1},1}

  ℚSequence(len) = Vector{fmpq}(undef, len)
  ℚTriangle(dim) = Vector{RationalSequence}(undef, dim)

  const ℤPolySeq = Array{fmpz_poly,1}
  const ℚPolySeq = Array{fmpq_poly,1}

  ℤPolynomials(x) = PolynomialRing(ZZ, x)
  ℚPolynomials(x) = PolynomialRing(QQ, x)

Triangles as skeletons of polynomials

There is a one-to-one correspondence between regular integer triangles that do not vanish on the right side (main diagonal) and ℤ-polynomial sequences. A polynomial sequence is a sequence of polynomials such that degree(pn(x)) = n (∀ n ≥ 0). The importance of polynomial sequences derives from the fact that they form a basis. Examples are the standard polynomials xn, the falling factorials and the rising factorials.

To illustrate this further, we have put together a vocabulary which translates the concepts introduced in Part 1 into the language of the polynomials.

row   ⇄   polynomial 
rowsum   ⇄  pn(1)
altsum   ⇄   pn(-1)
leftside   ⇄   pn(0)
 rightside   ⇄   lead(pn)

We used this concepts to build a profile of an integer triangle. Now we add two more traits to the profile.

 poshalf   ⇄   2n pn(1/2)
 neghalf   ⇄   (-2)n pn(-1/2) 

We see that it would be difficult to express these two traits in the language of integer triangles. First, we have to learn how to handle polynomials in Julia/Nemo.

Generating polynomials

To warm-up, we show how to convert an integer triangle to a polynomial sequence and vice versa.

DEF:  function Polynomial(s::ℤSequence)
          R, x = ℤPolynomials("x")
          sum(c*x^(k-1) for (k,c) ∈ enumerate(s))
      end

      Polynomial(T::ℤTriangle) = Polynomial.(T)

IN:   T = [[1], [0,1], [0,2,1], [0,6,6,1], [0,24,36,12,1]]
      Polynomial(T)

OUT:  5-element Array{fmpz_poly,1}:
       1
       x
       x^2 +  2*x
       x^3 +  6*x^2 +  6*x
       x^4 + 12*x^3 + 36*x^2 + 24*x

Unfortunately, the polynomials in the output are sorted by descending powers, whereas in the triangle, the exponent is the index in the row, read from left to right. But we can live with that.

What hurts more is x^(k-1) in the definition of the polynomials. The -1 in the exponent is solely due to Julia's decision to have arrays begin with 1. The result is an unnatural representation of polynomials in formulas and, of course, encourages off-by-one errors ...

More pleasing is how elegantly the concept of the coefficient expands. In the first definition, it applies to a polynomial and in the second definition to a sequence of polynomials. Here again, the use of the "broadcasting operator" is the method of choice.

DEF:  coefficients(p) = coeff.(p, 0:degree(p))
      coefficients(P::ℤPolySeq) = coefficients.(P)

IN:   R, x = ℤPolynomials("x")
      P = [R(1),x,x^2+2*x,x^3+6*x^2+6*x,x^4+12*x^3+36*x^2+24*x]
      coefficients(P)

OUT:  5-element Array{Array{fmpz,1},1}:
      [1]
      [0,  1]
      [0,  2,  1]
      [0,  6,  6,  1]
      [0, 24, 36, 12, 1]

Using this method we avoided all for-loop syntax in our definitions. Note the input of the polynomial sequence in the last example: [R(1),x,...]. Here we had to use the constructor for the constant polynomial 1 and had to write R(1); otherwise Julia would had interpreted 1 as an integer which would result in an error.

Evaluating polynomials.

Polynomials are no functions. But we can evaluate them by substitution. Here we are interested in the values at +1/2 and -1/2. This forces us to consider rational polynomials (i.e., polynomials whose coefficients are in ℚ).

DEF:  function Evaluate(p, x)
          Q, q = PolynomialRing(QQ, "q")
          subst(Q(p), x)
      end

DEF:  Evaluate(P::ℤPolySeq, x)  = Evaluate.(P, x)
      Evaluate(T::ℤTriangle, x) = Evaluate.(ℤPolySeq(T), x)

To do this, we have to convert the integer polynomial into a rational polynomial before we evaluate it. In the second definition, we apply the evaluation to a sequence of integer polynomials, but the result is a ℚSequence. If we pay attention to type stability, we have to convert this sequence back to a ℤSequence. The numerator function can be used for this, as we will see below.

Two examples: evaluations at ± 1/2.

The evaluation of an integer polynomial at 1/2:

IN:   R, x = ℤPolynomials("x")
      p = x^4+12*x^3+36*x^2+24*x
      Evaluate(p, 1//2)

OUT:  361//16

The evaluation of an integer triangle at 1/2 (via the associated polynomials):

IN:   T = [[1], [0,1], [0,2,1], [0,6,6,1], [0,24,36,12,1]]
      Evaluate(ℤTriangle(T), 1//2)

OUT:  5-element Array{fmpq,1}:
      1, 1//2, 5//4, 37//8, 361//16

The triangle T as written has the type IntTriangle because integer literals are of type Int. Therefore T has to be converted to a ℤTriangle before applying the evaluation function.

The traits poshalf and neghalf.

Now we can finally introduce the announced traits poshalf and neghalf of an integer triangle.

DEFS:
    poshalf(p) = numerator(2^degree(p)*Evaluate(p,1//2))
    neghalf(p) = numerator((-2)^degree(p)*Evaluate(p,-1//2))

    poshalf(T::ℤTriangle) = poshalf.(Polynomial(T))
    neghalf(T::ℤTriangle) = neghalf.(Polynomial(T))

    poshalf(P::ℤPolySeq) = poshalf.(P)
    neghalf(P::ℤPolySeq) = neghalf.(P)

This simply means: interpret the rows of an integer triangle as the coefficients of a polynomial, evaluate this polynomial at the x = 1/2 respectively x = -1/2 and normalize the result with 2^n respectively (-2)^n, where n is the degree of the polynomial. Finally, and only to resurrect the type ℤInt, apply the numerator function to the resulting value.

As a test case let's look at the Laguerre triangle.

IN:   T = LaguerreTriangle(8)
      poshalf(T) |> println
      neghalf(T) |> println

OUT:  [1,  3, 17, 139, 1473, 19091, 291793, 5129307]
      [1, -1,  1,   7, -127,  1711, -23231,  334391]

These are, apart from the sign-pattern, sequences A025167 and A025166 introduced by Wouter Meeussen in 1999.

Bivariate exp. generating functions

The next function returns the coefficients of a sequence of polynomials generated by a bivariate exponential generating function.

DEF: Laplace(s, k) = factorial(k) * coeff(s, k)

     function EgfExpansion(prec, gf::Function, coeff=true)
         R, x = QQPolynomials("x")
         S, t = PowerSeriesRing(R, prec+1, "t")
         ser = gf(x, t)
         P = Laplace.(ser, 0:prec-1)
         coeff ? coefficients.(P) : P
     end

     EgfExpansionCoeff(prec, gf) = EgfExpansion(prec, gf, true)
     EgfExpansionPoly(prec, gf) = EgfExpansion(prec, gf, false)

Note how much more precise the specifications are compared to the symbolic approach often taken with CASs like Maple or Mathematica. Nemo is more similar in its architecture to programs like Magma or SageMath. We will not go into the details and refer to the documents of the package Nemo.jl, easily to find on GitHub.

Lah and Bernoulli polynomials

We will use the above functions to demonstrate the construction of the Lah triangle from its generating function exp(t*(x/(1-t)).

DEF:  G271703(x,t) = exp(t*divexact(x, 1 - t))
      T271703(dim) = EgfExpansionCoeff(dim, G271703)
      P271703(dim) = EgfExpansionPoly(dim, G271703)

The naming scheme used is obvious: G271703 denotes the generating function of A271703, T271703 is the associated triangle and P271703 the associated sequence of polynomials.

IN:   T271703(5)
OUT:  5-element Array{Array{fmpq,1},1}:
      [1]
      [0, 1]
      [0, 2, 1]
      [0, 6, 6, 1]
      [0, 24, 36, 12, 1]

IN:   P271703(5)
OUT:  5-element Array{fmpq_poly,1}:
      1
      x
      x^2+2*x
      x^3+6*x^2+6*x
      x^4+12*x^3+36*x^2+24*x

What is easy to miss is the small 'q' in the output: the returned array is a rational triangle, not an integer triangle! Therefore we introduced the type alias ℚTriangle for Array{Array{ fmpq,1},1}. But we deliberately did not include the numerator function in EgfExpansion to reduce the risk that the denominators will be cut off accidentally from fractions whose denominator is not equal to 1.

Instead, we lift the numerator function from the rational numbers two levels up to rational triangles.

DEF: import Nemo.numerator
     numerator(T::RationalTriangle) = [numerator.(t) for t ∈ T]

Now we can call numerator(T271703(7)) to get the Lah numbers as an integer triangle.

For a second example consider the computation of the Bernoulli polynomials.

IN:  egfBernoulli(x, t) = divexact(t*exp(x*t), exp(t)-1)
     BernoulliPolynomials(n) = EgfExpansionPoly(n, egfBernoulli)
     BernoulliPolynomials(6)

OUT: 6-element Array{fmpq_poly,1}:
     1
     x   - 1//2
     x^2 -      x   + 1//6
     x^3 - 3//2*x^2 + 1//2*x
     x^4 -    2*x^3 +      x^2 - 1//30
     x^5 - 5//2*x^4 + 5//3*x^3 - 1//6*x

The Bernoulli polynomials.

In Julia rationals are constructed using the // operator. This operator has precedence over multiplication and addition. No need for brackets, no ambiguity here.

As a third example we look at the Worpitzky polynomials, which we have already considered above. Their coefficients can also be calculated from their exponential generating function like this:

IN:  G278075(x, t) = divexact(1, 1 - x*(1 - exp(-t)))
     WorpitzkyTriangle(n) = EgfExpansionCoeff(n, G278075)
     WorpitzkyTriangle(7)
    
OUT: [1]
     [0,  1]
     [0, -1,   2]
     [0,  1,  -6,    6]
     [0, -1,  14,  -36,   24]
     [0,  1, -30,  150, -240,   120]
     [0, -1,  62, -540, 1560, -1800, 720]

The coefficients of the Worpitzky polynomials.

Reinterpretation as a rectangle

Before we go any further, we need some more infrastructure.

T as a triangle
T(0,0)  
T(1,0) T(1,1)  
T(2,0) T(2,1) T(2,2)  
T(3,0) T(3,1) T(3,2) T(3,3)  
T(4,0) T(4,1) T(4,2) T(4,3) T(4,4)
T as a rectangle
T(0,0) T(1,1) T(2,2) T(3,3) T(4,4)
T(1,0) T(2,1) T(3,2) T(4,3) T(5,4)
T(2,0) T(3,1) T(4,2) T(5,3) T(6,4)
T(3,0) T(4,1) T(5,2) T(6,3) T(7,4)
T(4,0) T(5,1) T(6,2) T(7,3) T(8,4)

Informally we can say that pushing the k-th column of a triangle up k cells transforms the triangle into the rectangle. Formally the indices are transformed (n,k) → (n+k,k) when proceeding from the triangle form to the rectangular form.

We call this a reinterpretation and not a transformation because the data does not change in this process. Reading the triangle by rows is the same as reading the rectangle by ascending antidiagonals. However, we get a different view of the data which might lead to further insights into the structure of the generating function.

Note that the first column of the triangle is identical to the first column of the rectangle. That is the supporting pillar in this reinterpretation. Make sure that this pillar remains in place also when the reading direction is reversed (rectangle ↦ triangle). In particular, this means: avoid reading the rectangle by descending antidiagonals.

This is in line with our definition of the left side of a sequence or triangle and it conforms to the western way to read text from left to right.

Flattening a sequence of triangles

How to lay out a sequence of triangles in linear storage? Let's assume a sequence of triangles, A, B, C, D, ... given as flattened triangles in a rectangular array. Thus T(n) here means the n-th row of the triangle T, in the same way as we used it in the implementations above. Now using the same reinterpretation as in the last section, this time reading from right to left, we arrive at a triangle of rows.

Flattened triangles
A(0) A(1) A(2) A(3) A(4) ...
B(0) B(1) B(2) B(3) B(4) ...
C(0) C(1) C(2) C(3) C(4) ...
D(0) D(1) D(2) D(3) D(4) ...
E(0) E(1) E(2) E(3) E(4) ...
......
A triangle of rows
A(0)  
B(0) A(1)  
C(0) B(1) A(2)  
D(0) C(1) B(2) A(3)  
E(0) D(1) C(2) B(3) A(4)
......

Reading the right triangle by rows we finally have the linear layout of the sequence of triangles we looked for. From the mathematical point of view we defined an indexing of ℕ x ℕ where ℕ = {0, 1, 2, ...}. See A332662 for further remarks.

The polynomial array

The trait cards summarize 12 sequences and 4 triangles. Their names in the captions of the cards are: Δ Triangle, Δ Inverse, Δ Diagonal, Δ Polynom, Sum, EvenSum, OddSum, AltSum, DiagSum, Central, LeftSide, RightSide, PosHalf, NegHalf, N0TS, NATS.

We have now discussed all but one triangle: the polynomial triangle, which, however, is better presented as a rectangular array. It is defined as the evaluation of the polynomial associated with the n-th row on the nonnegative natural numbers. This means that the n-th row of the array is generated as indicated in the scheme below.

       T(n,0), T(n,1), ..., T(n,n)
    ↦  p(x) = Sum_{k=0..n} T(n,k)*x^k
    ↦  p(0), p(1), p(2), ....

For instance, the first three lines of the Laguerre polynomial array are computed as follows:

    [1] ↦ 1 ↦ 1, 1, 1, 1, ...
    [1, 1] ↦ 1 + x ↦ 1, 2, 3, 4, ...
    [2, 4, 1] ↦ 2 + 4*x + x^2 ↦ 2, 7, 14, 23, ...
DEFS:  

    function PolyArray(T::ℤTriangle)
        P = Polynomial(T)
        dim = length(T)
        U = ℤTriangle(dim)
        for n = 1:dim
            p = P[n]
            eva = [Evaluate(p, k) for k ∈ 0:dim-1]
            U[n] = numerator.(eva)
        end
        U
    end

    function PolyTriangle(T::ℤTriangle)
        A = PolyArray(T)
        U = ℤTriangle(length(T))
        for n = 1:length(T)
            U[n] = [A[n-k][k+1] for k ∈ 0:n-1]
        end
        U
    end

Applied to the Laguerre triangle these functions return the following triangular, respectively rectangular, arrays:

IN:   T = LaguerreTriangle(5)
      Println.(PolyTriangle(T))
      Println.(PolyArray(T))

OUT:  [1]
      [1,   1]
      [2,   2,  1]
      [6,   7,  3, 1]
      [24, 34, 14, 4, 1]

      [1,    1,   1,    1,    1]
      [1,    2,   3,    4,    5]
      [2,    7,  14,   23,   34]
      [6,   34,  86,  168,  286]
      [24, 209, 648, 1473, 2840]

If the coefficients list of the polynomials is reversed, one gets an entirely different array whose interpretation is not clear. A setup where the lead coefficient is 1 is always preferable.

    1, x+1, x^2+4*x+2,   x^3+9*x^2+18*x+6, ...
    1, x+1, 2*x^2+4*x+1, 6*x^3+18*x^2+9*x+1, ... (reversed)

    # The reversed Laguerre array.
    [1,   1,    1,    1,     1]
    [1,   2,    3,    4,     5]
    [1,   7,   17,   31,    49]
    [1,  34,  139,  352,   709]
    [1, 209, 1473, 5233, 13505]

Finally the question: How efficient is our general setup in the present case?

@time PolyArray(LaguerreTriangle(100))
1.256 seconds (1.88 M allocations: 57.7 MiB, 8.2% gc time)

In our opinion, this result fulfills the promise: "Julia was designed to help researchers write high-level code in an intuitive syntax and produce code with the speed of production programming languages." Let's compare it to the method proposed in A289192 using Maple.

A := (n, k) -> n!*add(binomial(n, i)*k^i/i!, i=0..n):
t := time(): seq(seq(A(n,k), k=0..99),n=0..99): time()-t;
4.141 seconds

However, I am not sure whether this is a fair comparison. After all, "Maple Academic Single-User Edition" currently costs US$ 995, Julia is free and open. In addition, Maple is promoted to introduce the concept of "clickable math". Julia, in contrast, does not have much to offer about clicks.

Get started!

In the third part of this blog post we dive deeper into the mathematical side of the story and look at several methods to generate interesting integer triangles, the Schröder triangles, the Motzkin triangle, the normal and the extended Catalan triangle, the Jacobsthal triangle and the Delannoy triangle.

If a reader can't wait to try out Julia Language now, here are the next steps:

Part 1 of the tutorial       Part 3 of the tutorial