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}\]

the quadratic triangle:

(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.
Return type: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.
Returns:

The point on the surface (as a one dimensional NumPy array).

Return type:

numpy.ndarray

Raises:
  • 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.
Returns:

The point on the surface (as a one dimensional NumPy array).

Return type:

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).

Returns:

The point on the surface.

Return type:

numpy.ndarray

Raises:
  • 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:
Returns:

The axis containing the plot. This may be a newly created axis.

Return type:

matplotlib.artist.Artist

Raises:

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).
Return type: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
__delattr__

x.__delattr__(‘name’) <==> del x.name

__format__()

default object formatter

__getattribute__

x.__getattribute__(‘name’) <==> x.name

__hash__
__reduce__()

helper for pickle

__reduce_ex__()

helper for pickle

__setattr__

x.__setattr__(‘name’, value) <==> x.name = value

__sizeof__() → int

size of object in memory, in bytes

__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.