# bezier.surface module¶

Helper for Bézier Surfaces / Triangles.

class bezier.surface.Surface(nodes, _copy=True)

Bases: bezier._base.Base

Represents a Bézier surface.

We define a Bézier triangle as a mapping from the unit simplex in 2D (i.e. the unit triangle) onto a surface in an arbitrary dimension. We use barycentric coordinates

$\lambda_1 = 1 - s - t, \lambda_2 = s, \lambda_3 = t$

for points in

$\left\{(s, t) \mid 0 \leq s, t, s + t \leq 1\right\}.$

As with curves, using these weights we get convex combinations of points $$v_{i, j, k}$$ in some vector space:

$B\left(\lambda_1, \lambda_2, \lambda_3\right) = \sum_{i + j + k = d} \binom{d}{i \, j \, k} \lambda_1^i \lambda_2^j \lambda_3^k \cdot v_{i, j, k}$

Note

We assume the nodes are ordered from left-to-right and from bottom-to-top. So for example, the linear triangle:

(0,0,1)

(1,0,0)  (0,1,0)


is ordered as

$\begin{split}\left[\begin{array}{c c c} v_{1,0,0} & v_{0,1,0} & v_{0,0,1} \end{array}\right]^T\end{split}$

(0,0,2)

(1,0,1)  (0,1,1)

(2,0,0)  (1,1,0)  (0,2,0)


is ordered as

$\begin{split}\left[\begin{array}{c c c c c c} v_{2,0,0} & v_{1,1,0} & v_{0,2,0} & v_{1,0,1} & v_{0,1,1} & v_{0,0,2} \end{array}\right]^T\end{split}$

the cubic triangle:

(0,0,3)

(1,0,2)  (0,1,2)

(2,0,1)  (1,1,1)  (0,2,1)

(3,0,0)  (2,1,0)  (1,2,0)  (0,3,0)


is ordered as

$\begin{split}\left[\begin{array}{c c c c c c c c c c} v_{3,0,0} & v_{2,1,0} & v_{1,2,0} & v_{0,3,0} & v_{2,0,1} & v_{1,1,1} & v_{0,2,1} & v_{1,0,2} & v_{0,1,2} & v_{0,0,3} \end{array}\right]^T\end{split}$

and so on.

>>> import bezier
>>> nodes = np.array([
...     [0.0 , 0.0 ],
...     [1.0 , 0.25],
...     [0.25, 1.0 ],
... ])
>>> surface = bezier.Surface(nodes)
>>> surface
<Surface (degree=1, dimension=2)>

Parameters: nodes (numpy.ndarray) – The nodes in the surface. The rows represent each node while the columns are the dimension of the ambient space.
area

float: The area of the current surface.

Raises: NotImplementedError – If the area isn’t already cached.
edges

tuple: The edges of the surface.

>>> nodes = np.array([
...     [0.0   ,  0.0   ],
...     [0.5   , -0.1875],
...     [1.0   ,  0.0   ],
...     [0.1875,  0.5   ],
...     [0.625 ,  0.625 ],
...     [0.0   ,  1.0   ],
... ])
>>> surface = bezier.Surface(nodes)
>>> edge1, _, _ = surface.edges
>>> edge1
<Curve (degree=2, dimension=2)>
>>> edge1.nodes
array([[ 0.  ,  0.    ],
[ 0.5 , -0.1875],
[ 1.  ,  0.    ]])

Returns: The edges of the surface. Tuple [ Curve, Curve, Curve ]
evaluate_barycentric(lambda1, lambda2, lambda3)

Compute a point on the surface.

Evaluates $$B\left(\lambda_1, \lambda_2, \lambda_3\right)$$.

>>> nodes = np.array([
...     [0.0 , 0.0 ],
...     [1.0 , 0.25],
...     [0.25, 1.0 ],
... ])
>>> surface = bezier.Surface(nodes)
>>> surface.evaluate_barycentric(0.125, 0.125, 0.75)
array([ 0.3125 , 0.78125])


However, this can’t be used for points outside the reference triangle:

>>> surface.evaluate_barycentric(-0.25, 0.75, 0.5)
Traceback (most recent call last):
...
ValueError: ('Parameters must be positive', -0.25, 0.75, 0.5)


or for non-Barycentric coordinates;

>>> surface.evaluate_barycentric(0.25, 0.25, 0.25)
Traceback (most recent call last):
...
ValueError: ('Values do not sum to 1', 0.25, 0.25, 0.25)

Parameters: lambda1 (float) – Parameter along the reference triangle. lambda2 (float) – Parameter along the reference triangle. lambda3 (float) – Parameter along the reference triangle. The point on the surface (as a one dimensional NumPy array). numpy.ndarray ValueError – If the weights are not valid barycentric coordinates, i.e. they don’t sum to 1. ValueError – If some weights are negative.
evaluate_cartesian(s, t)

Compute a point on the surface.

Evaluates $$B\left(1 - s - t, s, t\right)$$ by calling evaluate_barycentric().

Parameters: s (float) – Parameter along the reference triangle. t (float) – Parameter along the reference triangle. The point on the surface (as a one dimensional NumPy array). numpy.ndarray
evaluate_multi(param_vals)

Compute multiple points on the surface.

If param_vals has two columns, this method treats them as Cartesian:

>>> nodes = np.array([
...     [ 0., 0. ],
...     [ 2., 1. ],
...     [-3., 2. ],
... ])
>>> surface = bezier.Surface(nodes)
>>> surface
<Surface (degree=1, dimension=2)>
>>> param_vals = np.array([
...     [0.0, 0.0],
...     [1.0, 0.0],
...     [0.5, 0.5],
... ])
>>> surface.evaluate_multi(param_vals)
array([[ 0. , 0. ],
[ 2. , 1. ],
[-0.5, 1.5]])


and if param_vals has three columns, treats them as Barycentric:

>>> nodes = np.array([
...     [ 0. , 0.  ],
...     [ 1. , 0.75],
...     [ 2. , 1.  ],
...     [-1.5, 1.  ],
...     [-0.5, 1.5 ],
...     [-3. , 2.  ],
... ])
>>> surface = bezier.Surface(nodes)
>>> surface
<Surface (degree=2, dimension=2)>
>>> param_vals = np.array([
...     [0.   , 0.25, 0.75 ],
...     [1.   , 0.  , 0.   ],
...     [0.25 , 0.5 , 0.25 ],
...     [0.375, 0.25, 0.375],
... ])
>>> surface.evaluate_multi(param_vals)
array([[-1.75  , 1.75    ],
[ 0.    , 0.      ],
[ 0.25  , 1.0625  ],
[-0.625 , 1.046875]])


Note

This currently just uses evaluate_cartesian() and evaluate_barycentric() so is less performant than it could be.

Parameters: param_vals (numpy.ndarray) – Array of parameter values (as a 2D array). The point on the surface. numpy.ndarray ValueError – If param_vals is not a 2D array. ValueError – If param_vals doesn’t have 2 or 3 columns.
plot(pts_per_edge, ax=None, show=False)

Plot the current surface.

Parameters: pts_per_edge (int) – Number of points to plot per edge. ax (Optional [ matplotlib.artist.Artist ]) – matplotlib axis object to add plot to. show (Optional [ bool ]) – Flag indicating if the plot should be shown. The axis containing the plot. This may be a newly created axis. matplotlib.artist.Artist NotImplementedError – If the surface’s dimension is not 2.
subdivide()

Split the surface into four sub-surfaces.

Takes the reference triangle

$T = \left\{(s, t) \mid 0 \leq s, t, s + t \leq 1\right\}$

and splits it into four sub-triangles

\begin{split}\begin{align*} A &= \left\{(s, t) \mid 0 \leq s, t, s + t \leq \frac{1}{2}\right\} \\ B &= -A + \left(\frac{1}{2}, \frac{1}{2}\right) \\ C &= A + \left(\frac{1}{2}, 0\right) \\ D &= A + \left(0, \frac{1}{2}\right). \end{align*}\end{split}

These are the lower left ($$A$$), central ($$B$$), lower right ($$C$$) and upper left ($$D$$) sub-triangles.

>>> nodes = np.array([
...     [ 0. , 0. ],
...     [ 2. , 0. ],
...     [ 0. , 4. ],
... ])
>>> surface = bezier.Surface(nodes)
>>> _, _, _, sub_surface_d = surface.subdivide()
>>> sub_surface_d
<Surface (degree=1, dimension=2)>
>>> sub_surface_d.nodes
array([[ 0., 2.],
[ 1., 2.],
[ 0., 4.]])

Returns: The lower left, central, lower right and upper left sub-surfaces (in that order). Tuple [ Surface, Surface, Surface, Surface ]
is_valid

bool: Flag indicating if the surface no singularites.

This checks if the Jacobian of the map from the reference triangle is nonzero. For example, a linear “surface” with collinear points is invalid:

>>> nodes = np.array([
...     [0.0, 0.0],
...     [1.0, 1.0],
...     [2.0, 2.0],
... ])
>>> surface = bezier.Surface(nodes)
>>> surface.is_valid
False


while a quadratic surface with one straight side:

>>> nodes = np.array([
...     [ 0.0  , 0.0  ],
...     [ 0.5  , 0.125],
...     [ 1.0  , 0.0  ],
...     [-0.125, 0.5  ],
...     [ 0.5  , 0.5  ],
...     [ 0.0  , 1.0  ],
... ])
>>> surface = bezier.Surface(nodes)
>>> surface.is_valid
True

__eq__(other)

Check equality against another shape.

Returns: Boolean indicating if the shapes are the same. bool
__ne__(other)

Check inequality against another shape.

Returns: Boolean indicating if the shapes are not the same. bool
__repr__()

Representation of current object.

Returns: Object representation. str
copy()

Make a copy of the current shape.

Returns: Instance of the current shape.
degree

int: The degree of the current shape.

dimension

int: The dimension that the shape lives in.

For example, if the shape lives in $$\mathbf{R}^3$$, then the dimension is 3.

nodes

numpy.ndarray: The nodes that define the current shape.