Here we’ll describe a method for forming Bezier Forms through a series of ‘convolutions’ over n-dimensional arrays. With a clear mathematical notation and construction, we’ll move to implementation in Python and Rust.

Notation

We’ll be working with a collection of entities whose representations are defined here.

  • Multidimensional Array

n-dimensional containers will be labeled $\theta_{abc \ldots n}$ where each subscript correspond to an additional dimension. For example, the symbol $\theta_{abc}$ represents the 3-dimensional container. Supposing that $a, b, c \in [0,1]$, then we’ll have constructed an object with eight components. Accessing an individual component will require resolution of all subscripts to a specific sequence of integers. ie $\theta_{000}$.

An incomplete sequence of integers will corespond to what’s known as a slice. That is to say, if we’re to write $\theta^{a..}_{.bc}$ we will resolve a (3-1)-dimensional container with free variables $b$ and $c$. In our example, there would be two such slices in the item, $\theta^{0..}_{.bc}$ and $\theta^{1..}_{.bc}$.

Scalar multiplication and addition between identically size arrays are needed for this construction. Much like vector and matrix algebra, it will be defined as a broadcast multiplication and componentwise addition.

  • Lambda Functions

These ‘on the fly’ functions will appear throughout the construction as we combine slices of multi-dimensional arrays together into their new form. They will appear as $\lambda \theta_i \theta_{j, k}: \lambda t: \theta_{i} + \theta_{i}t + \theta_{j, k}x_{i}$ with the standard notation for scalars, vectors and matrices.

  • Convolution Operator

A convolution operator denoted $C_{m}$ is a motion from an array of one dimensionality to another $\theta_{\Pi a} \rightarrow \theta_{\Pi b}$. For our purpose, the operation will reduce the dimensionality of the array along the specified index $C_{\phi} \theta_{\phi \Pi a} = \theta_{\Pi a}$. A composition of convolutions $C_{m} \circ C_{n}$ will be denoted by $C_{mn}$

Construction

Firstly, we’ll define a function $\beta$ which accepts a multidimensional array $\theta_{\Pi b_{k}}$, a vector of dimensions $b_{i}$ and a vector from the domain $v_{i}$.

\[\tag{1.1} \beta (\theta_{\Pi b_{k}}, b_{i}, v_{i}) = C_{\Pi b_{i}} \theta_{\Pi b_{k}} (v_{i}) \label{eq:1p1}\] \[\tag{1.2} \beta (\theta_{\Pi b_{k}}, b_{i}, v_{i}) = \theta_{\frac{\Pi b_{k}}{\Pi b_{i}}} (v_{i}) \label{eq:1p2}\]

A motion from $\eqref{eq:1p1}$ to $\eqref{eq:1p2}$ come through repeatedly consuming a convolutional operator and vector component on either side of our multidimensional array. Their combination reduces the dimension of the array as mentioned above. The process continues until all components are consumed.

We now define the implementation of our operator $C_{b_i}$. It itself is a composition of operations over the array. Firstly, we’ll define $\lambda_i$ which combines adjacent slices of $\theta$ into a singular slice.

\[\tag{1.3} \lambda_i t \theta_{i} \theta_{i+1} : (1 - t)\theta_{i} + t\theta_{i+1}\]

A map of this function over the given dimension will procude a new array that’s been reduced by 1 in the given dimension.

\[\tag{1.4} \lambda_i t \triangleright \theta^{.i.}_{...}\]

We wish to repeat this map until one component remains. Which indeed resolves to our desired convolutional operator.

\[\tag{2.0} \lambda_i t \triangleright \lambda_i t \triangleright \lambda_i t \triangleright \dots \lambda_i t \triangleright \theta^{.i.}\_{...} = \lambda\_i t \overset{\|i\|}{\triangleright} \theta^{.i.}\_{...} = C\_{i} \theta^{.i.}\_{...} = \theta^{..}\_{..}\]

Implementation (Python)

from src.typeclass.VFF import VFF

from numpy import array, array_split, squeeze

class Bezier(VFF):
    def __init__(self, control_points, collapse_axes, callparam=lambda t:t):
        self.control_points = control_points
        self.collapse_axes = collapse_axes
        self.callparam = callparam

    def __call__(self, ts):
        ## Extract domain value and axis indicator.
        t, *ts = ts
        m, *ms = self.collapse_axes

        ## Along the given axis, gather sub-arrays from control points
        scp = array_split(self.control_points, self.control_points.shape[m], m)

        ## condense sub-arrays with convolution 
        while len(scp) > 1: 
            scp = [self.convolve(t,p,c) for p, c in zip(scp, scp[1:])]

        ## collect result of above computation and remove the condensed axis
        retv, *_ = scp
        retv = squeeze(retv, axis=m)

        ## recur computation if there're more axes to compress
        if ms:
            ms = list(map(lambda x: x if x < m else x - 1, ms))
            retv = Bezier(retv, ms, self.callparam).__call__(ts)

        return retv

    def convolve(self, t, slice_one, slice_two):
        return (1-t)*slice_one + t*slice_two