Skip to content

Computational Curves

The types of curves presented in this section are enabled by the ability to perform computations efficiently. Otherwise such geometries are impractical and inaccessible with conventional media. The presentation follows the historical evolution of polygonal and smooth curves including Bezier, BSpline and Nurbs curves.

Polylines

Definition

Polylines are piece-wise continuous line segment curves represented by lists of points. Polylines where the first and last points coincide are considered closed or otherwise open. The length of a polyline is the sum of its linear segments.

""" Polyline Class
"""
class Poly:
    """ Construction
    """
    def __init__( self, points ):
        self.Points = points

    """ Source/Start Point
    """
    @property
    def Source( self ):
        return self.Points[0]

    """ Target/End Point
    """
    @property
    def Target( self ):
        return self.Points[-1]

    """ Is Polyline Closed?
    """
    @property
    def IsClosed( self ):
        return self.Source.DistanceTo(
               self.Target ) <= 1e-5

    """ Line Segments Enumerator
    """
    @property
    def Segments( self ):
        for source, target in zip(
                self.Points[:-1], self.Points[1:] )
            yield ( source, target )

    """ Length of Polyline
    """
    @property
    def Length( self ):
        return sum( [source.DistanceTo( target )
            for source, target in self.Segments] )

Smoothing

Polylines are often used for representing time-series, where a value or position is tracked over time. Signals collected from sensors often contain numerous points as well as errors. A simple process for decimating the points and simultaneously deriving a smooth trend curve involves averaging consecutive points.

In each iteration, every line segment is collapsed to its mid-point, and thus the polyline is reduced by one point. Averaging is repeated recursively producing a smooth approximations of the initial curve. The start and end points of the polyline may be retained, or pinned, because otherwise the curve will geometrically shrink, theoretically down to a single point.

class Poly:
    """ Average Consecutive Points
    """
    def Average( self, points, pinned ):
        result = []
        if( pinned ):
            result.append( self.Source )

        for a, b in self.Segments:
            result.append( ( a + b ) / 2 )

        if( pinned ):
            result.append( self.Target )
        return result

    """ Polyline Smoothing
    """
    def Smooth( self, iterations = 32, pinned = True ):
        points = self.Points
        for _ in range( iterations ):
            points = self.Average( points, pinned )
        return Poly( points )

    def ToPolyline( self ):
        return Polyline( self.Points )

""" Smoothing Noisy Signal
"""
import math, random

points = []
count = 50
for index in range( count ):
    x = 2 * math.pi * index / ( count - 1 )
    y = math.sin( x ) + random.uniform( -0.1, 0.1 )
    points.append( Point3d( x, y, 0 ) )

noisy = Poly( points )
smooth = noisy.Smooth( )

Application

Rhino uses two classes for polylines, namely Polyline and PolylineCurve, with the former being a light-weight list-like representation, while the later implementing the Curve interface supporting many more complex methods. The Polyline class is used for manipulating point lists directly, while the PolylineCurve is used for passing curve-like parameters to geometric methods such as computing curve intersections, lofting surfaces etc.

""" Polyline Object
"""
poly = Polyline( [Point3d( x, x * x, 0.0 )
    for x in range( 10 )] )

""" Point Indexing
"""
source = poly.First
target = poly.Last

source = poly[0]
target = poly[poly.Count - 1] #-- Cannot use -1

""" Polyline Length
"""
length = poly.Length

""" Polyline Curve Object
"""
poly = PolylineCurve( [Point3d( x, x * x, 0.0 )
    for x in range( 10 )] )

""" Point Indexing
"""
source = poly.PointAtStart
target = poly.PointAtEnd

source = poly.Point( 0 )
target = poly.Point( poly.PointCount - 1 )

""" Polyline Length
"""
length = poly.GetLength( )

Subdivisions

Definition

Subdivision curves are defined by an initial control polygon which is recursively augmented by introducing new interim points and then adjusting their positions in a weighted average sense. The process typically converges to a limiting smooth curve after only a few iterations. There are two types of subdivision curves, namely approximating and interpolating, with the later retaining the positions of the initial control polygon, unlike the former one, see additional information.

Approximation

Approximation schemes iterate over pairs of consecutive line segments, a->b and b->c, and inject two new mid-points laterally A = ( a + b ) / 2 and C = ( b + c ) / 2. The common point b between the segments is relocated at a position computed from the weighted average of its initial position and the mid-points B = A * wa + b * wb + C * wc, where wa, wb and wb are the weights. The list K = [wa, wb, wc] is known as the subdivision's kernel. Moreover, the sum of weights is unity wa + wb + wc = 1.

The number of points n after each iteration is almost double 2 * n - 1, therefore the curve rapidly reaches its smooth limiting shape. Open and closed control polygons require attention such that the new points wrap around properly.

""" Subdivision Curve
"""
class SubDivisionCurve( Poly ):
    def __init__( self, points, weight = 2 ):
        Poly.__init__( self, points )
        scale = 1.0 / ( weight + 2 )
        self.Kernel = [scale,
                       scale * weight,
                       scale]

    """ Evaluate Kernel
    """
    def Evaluate( self, a, b, c ):
        A = ( a + b ) / 2
        C = ( b + c ) / 2

        wa, wb, wc = self.Kernel
        B = ( A * wa + b * wb + C * wc )

        return A, B, C

    """ Subdivision Iteration
    """
    def Subdivide( self, points, closed ):
        result = []
        if( closed ):
            A, B, C = self.Evaluate(
                points[-2], #-- skip -1
                points[ 0],
                points[ 1] )
            result.append( A )
            result.append( B )
        else:
            result.append( points[0] )

        for a, b, c in zip(
                points[ :-2],
                points[1:-1],
                points[2:  ] ):
            A, B, C = self.Evaluate( a, b, c )
            result.append( A )
            result.append( B )
        result.append( C )

        if( not closed ):
            result.append( points[-1] )
        return result

    """ Curve Approximation
    """
    def Approximate( self, iterations = 8 ):
        points = self.Points
        closed = self.IsClosed
        for _ in range( iterations ):
            points = self.Subdivide( points, closed )
        return Poly( points )

""" Open Curve
"""
curve = SubDivisionCurve( [
    Point3d( 0, 0, 0 ),
    Point3d( 1, 1, 0 ),
    Point3d( 2, 0, 0 ),
    Point3d( 3,-1, 0 ),
    Point3d( 4, 0, 0 )] ).Approximate( )

""" Closed Curve
"""
curve = SubDivisionCurve( [
    Point3d( 0, 3, 0 ),
    Point3d( 1, 4, 0 ),
    Point3d( 2, 3, 0 ),
    Point3d( 1, 2, 0 ),
    Point3d( 0, 3, 0 )] ).Approximate( )

Bézier Curves

Bézier curves B( t ) are parametric curves using t: [0, 1] with a polynomial representation defined by a sequence of control points Pi = [P0, P1, ..., Pn-1]. The number of control points n is associated with the degree d of the polynomial by n = d + 1. Geometrically they generalize the idea of smoothly blending between points. For example below are the first few polynomials.

  1. B( t ) = ( 1 - t )P0 + tP1
  2. B( t ) = ( 1 - t )2P0 + 2( 1 - t )tP1 + t2P2
  3. B( t ) = ( 1 - t )3P0 + 3( 1 - t )2tP1 + 3( 1 - t )t2P2 + t3P3
  4. B( t ) = ( 1 - t )4P0 + 4( 1 - t )3tP1 + 6( 1 - t )2t2P2 + 4( 1 - t )t3P3 + t4P4

The first degree polynomial is just the linear interpolation used in the definition a line segment. Higher degrees are extension of this principle. Bézier curves, especially quadratics and cubics, are pervasive across design software because they are smooth, require limited memory and they are fast to compute.

Definition

The expression often used as definition for Bézier curves, namely B( t ) = Σ[0, d] bd,i( t ) Pi, where bd,i( t ) = comb( d, i ) ti ( 1 - t )d-i, is also known as the Bernstein basis function. Comb( n, k ) is a function that returns the binomial coefficient n! / ( k! ( n - k )! ).

""" Binomial Coefficient for python < 3.8
    or just math.comb( ) for python > 3.8
"""
def math_comb( n, k ):
    return math.factorial( n ) // (
           math.factorial( k ) * math.factorial( n - k ) )

""" Evaluate Bezier at Parameter
"""
class BezierCurve:
    def __init__( self, points ):
        self.Points = points

    @property
    def Degree( self ):
        return len( self.Points ) - 1

    def Evaluate( self, t ):
        point = Point3d.Origin
        for index, control in enumerate( self.Points ):
            alpha = t ** index
            omega = ( 1 - t ) ** ( self.Degree - index )
            combo = math_comb( self.Degree, index )
            basis = combo * alpha * omega
            point += control * basis
        return point

The expression is derived is by expanding ( ( 1 - t ) + t ) ** d, hence the binomial coefficients and combinations of ( 1 - t ) and t raised in respective powers. When those terms are combined with the control points, they produce continuous curves. This also explains why the degree + 1 equals the number of control points required.

Evaluation

An alternative approach for evaluating Bézier curves deploys recursive linear interpolation: For every pair of consecutive control points P[i] and P[i+1] we compute a new point Q as the linear combination P[i] * ( 1 - t ) + P[i+1] * t. Processing all n points results into a new list of n-1 points. Thereafter, the process is repeated until we are left with a single point.

class BezierCurve:
    @staticmethod
    def DeCasteljau( points, t ):
        if( len( points ) == 1 ):
            return points[0]
        new_points = []
        for source, target in zip( points[:-1], points[1:] ):
            new_points.append( source * ( 1 - t ) + target * t )
        return BezierCurve.DeCasteljau( new_points, t )

    def Evaluate( self, t ):
        return BezierCurve.DeCasteljau( self.Points, t )

Derivatives

The derivatives of Bezier curves follow exactly the same rules used for deriving polynomials, namely for f( x ) = xd its derivative is f'( x ) = d * xd-1. Therefore, to compute the derivative of a Bezier curve, the differences of consecutive control points are scaled by the number of points, effectively dropping a degree, see derivation details.

class BezierCurve:
    """ Derivative / Hodograph Curve
    """
    def Derive( self ):
        d = self.Degree
        points = [( target - source ) * d
            for source, target in zip(
                self.Points[:-1], self.Points[1:] )]
        return BezierCurve( points )

""" Bezier Derivative
"""
controls = [
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 1.0, 0.0 ),
    Point3d( 2.0, 0.0, 0.0 ),
    Point3d( 3.0,-1.0, 0.0 ),
    Point3d( 4.0, 0.0, 0.0 )]

bezier = BezierCurve( controls )
tangent = bezier.Derive( )

point = bezier.Evaluate( t )
vector = Vector3d( tangent.Evaluate( t ) )
vector.Unitize( )

Higher order derivatives can be computed using the same principle. The Derivatives( ) method computes a number of requested derivatives which can be used for producing the tangent, normal, frame, curvature and torsion as seen below.

class BezierCurve:
    """ Derivatives at Parameter
    """
    def Derivatives( self, t, n = -1 ):
        d = self.Degree
        if( n < 0 or
            n > d ):
            n = d
        curve = self
        delta = [curve.Evaluate( t )] * ( n + 1 )
        for k in range( 1, n + 1 ):
            curve = curve.Derive( )
            delta[k] = curve.Evaluate( t )
        return delta

    """ Tangent Vector
    """
    def Tangent( self, t ):
        _, u = self.Derivatives( t, 1 )
        u.Unitize( )
        return u

    """ Normal Vector
    """
    def Normal( self, t ):
        _, u, v = self.Derivatives( t, 2 )
        n = Vector3d.CrossProduct( u, v )
        n.Unitize( )
        return n

    """ Fresnet Frame
    """
    def Frame( self, t ):
        o, u, v = self.Derivatives( t, 2 )
        return Plane( o, u, v )

    """ Curvature
    """
    def Curvature( self, t ):
        _, u, v = self.Derivatives( t, 2 )
        return Vector3d.CrossProduct( u, v ).Length / u.Length ** 3

    """ Torsion
    """
    def Torsion( self, t ):
        o, u, v, w = self.Derivatives( t, 3 )
        x = Vector3d.CrossProduct( u, v )
        return ( x * w ) / ( x * x )

Application

Rhino implements Bezier curves via the BezierCurve class, see documentation. However, Bezier curves can be represented using Nurbs curves, where their degree equals their number of points plus one, as seen below.

""" Using Rhino's Bezier Curve
"""
from Rhino.Geometry import BezierCurve

curve = BezierCurve( controls )
point = curve.PointAt( t )
tangent = curve.TangentAt( t )

""" Using Rhino's Nurbs Curve
"""
from Rhino.Geometry import NurbsCurve

periodic, degree = False, len( controls ) - 1
curve = NurbsCurve.Create( periodic, degree, controls )
point = curve.PointAt( t )
tangent = curve.TangentAt( t )

BSpline Curves

Definition

Basis splines solve the limitation of Bezier curves, where a curve's degree is bound to the number of its control points. BSplines achieve this by stitching lower degree polynomials in piece-wise continuous manner, see additional information. Stitching is encoded using a list with the domain parameters where the pieces of polynomial of the same degree meet one another, known as the knot vector.

class BasisSplineCurve:
    """ Construction
    """
    def __init__( self, points, degree ):
        self.Points = points
        self.Degree = degree
        self.Knots  = self.ClampedKnots( )

    """ Open Knot Vector
    """
    def OpenKnots( self ):
        n, d = len( self.Points ), self.Degree
        return [i / ( n + d )
            for i in range( n + d + 1 )]

    """ Clamped Knot Vector
    """
    def ClampedKnots( self ):
        n, d = len( self.Points ), self.Degree
        k_min = [0.0] * d
        k_mid = [i / ( n - d ) for i in range( n - d + 1 )]
        k_max = [1.0] * d
        return k_min + k_mid + k_max

    """ Periodic Knot Vector
    """
    def PeriodicKnots( self ):
        n, d = len( self.Points ), self.Degree
        return [( i - d  ) / ( n - d )
            for i in range( n + d + 1 )]

Evaluation

Evaluating points on BSplines is slightly more involved compared to Bezier curves. However, the logic is not unlike De Casteljau's repeated interpolation scheme. First, the polynomial curve piece is identified by locating the knot interval [k_min, k_max] containing the parameter t. Finally, adjacent control points are interpolated in a linear fashion using the basis functions associated with the knot values. Only up to degree + 1 points are used unlike Bezier curves which process the entire list of control points. The Evaluate( ) method below follows the De Boor's algorithm. A more efficient implementation is presented by Piegl and Tiller in The NURBS Book.

class BasisSplineCurve:
    """ Find Knot Span Containing Parameter
    """
    def KnotSpan( self, t ):
        n, d = len( self.Points ), self.Degree
        if( t <= self.Knots[d] ): return d, d
        if( t >= self.Knots[n] ): return d, n - 1
        for knot, ( k_min, k_max ) in enumerate(
                zip( self.Knots[:-1], self.Knots[1:] ) ):
            if( k_min <= t <  k_max or
                k_min <  t <= k_max ):
                return d, knot

    """ De Boor's Algorithm
    """
    def Evaluate( self, t ):
        #-- Relevant Knots and Points
        #--
        d, k = self.KnotSpan( t )
        P = self.Points[k-d:k+1]

        #-- Recursive Interpolations
        #--
        for i in range( d ):
            for j in range( d, i, -1 ):
                to = self.Knots[j + k - d]
                ti = self.Knots[j + k - i]

                alpha = ( t - to ) / ( ti - to )
                omega = 1.0 - alpha

                P[j] = omega * P[j - 1] + alpha * P[j]
        return P[-1]

The demonstration below constructs a BSpline, prints its knot vector and evaluates a series of points along its domain. Note that the repeated zeros and ones in the knot vector are for keeping the basis functions evaluation within bounds and forcing the curve to pass through the first and last control points.

""" Control Points
"""
controls = [
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 1.0, 0.0 ),
    Point3d( 2.0, 0.0, 0.0 ),
    Point3d( 3.0,-1.0, 0.0 ),
    Point3d( 4.0, 0.0, 0.0 )]

""" Clamped BSpline Curve
"""
curve = BasisSplineCurve( controls, degree = 3 )
print( curve.Knots )
#  [ 0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0 ]
#  | 0.0 * degree | unit range   | 1.0 * degree  |
#  | clamped      | interpolated | clamped       |

""" Open BSpline Curve
"""
curve = BasisSplineCurve( controls, degree = 3 )
curve.Knots = curve.OpenKnots( )
print( curve.Knots )
# [0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]
# |                 interpolated                        |

count  = 128
points = [curve.Evaluate( index / ( count - 1 ) )
    for index in range( count )]

Periodic Curves

Periodic curves are represented by first wrapping the last control points over the first control points, by repeating as many as the degree of the curve, and then using a periodic knot vector.

""" Control Points
"""
controls = [
    Point3d( 0.0, 0.0, 0.0 ), #-- 0
    Point3d( 1.0, 1.5, 0.0 ), #-- 1
    Point3d( 2.0, 0.0, 0.0 ), #-- 2
    Point3d( 0.0, 0.0, 0.0 ), #-- wrap 0
    Point3d( 1.0, 1.5, 0.0 ), #-- wrap 1
    Point3d( 2.0, 0.0, 0.0 )] #-- wrap 2

""" Periodic BSpline Curve
"""
curve = BasisSplineCurve( controls, degree = 3 )
curve.Knots = curve.PeriodicKnots( )
print( curve.Knots )
#  [-1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5]
#  | wrap backward  | unit range   | wrap forward |
#  | periodic       | interpolated | periodic     |

Derivatives

The derivative of a BSpline curve is another one-degree-lower BSpline curve. Its control points are computed from the differences of the original control points, scaled by the degree and knot span differences. Finally, its knot vector is a copy of the original knots without the first and last items, see derivation details. Higher order derivatives and differential properties are computed using the same principles as seen in Bezier curves.

class BasisSplineCurve:
    """ Derivative / Hodograph Curve
    """
    def Derive( self ):
        d = self.Degree
        points = []
        for i in range( 1, len( self.Points ) ):
            po, ko = self.Points[i-1], self.Knots[i  ]
            pi, ki = self.Points[i  ], self.Knots[i+d]
            point = ( pi - po ) * d / ( ki - ko )
            points.append( point )
        curve = BasisSplineCurve( points, d - 1 )
        curve.Knots = self.Knots[1:-1]
        return curve

    def Derivatives( self, t, n = -1 ):
        d = self.Degree
        if( n < 0 or
            n > d ):
            n = d
        curve = self
        delta = [curve.Evaluate( t )] * ( n + 1 )
        for k in range( 1, n + 1 ):
            curve = curve.Derive( )
            delta[k] = curve.Evaluate( t )
        return delta

""" BSpline Curve Derivative
"""
controls = [
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 1.0, 0.0 ),
    Point3d( 2.0, 0.0, 0.0 ),
    Point3d( 3.0,-1.0, 0.0 ),
    Point3d( 4.0, 0.0, 0.0 )]

bspline = BasisSplineCurve( controls, degree = 3 )
point = bspline.Evaluate( t )
plane = bspline.Frame( t )

Application

Rhino does not provide a specialized BSpline class but the functionality is supported via Nurbs curves, see next section, which represent a superset or generalization thereof.

""" Using Rhino's Nurbs Curve
"""
from Rhino.Geometry import NurbsCurve

controls = [
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 1.0, 0.0 ),
    Point3d( 2.0, 0.0, 0.0 ),
    Point3d( 3.0,-1.0, 0.0 ),
    Point3d( 4.0, 0.0, 0.0 )]

periodic, degree = False, 3
curve = NurbsCurve.Create( periodic, degree, controls )
print( list( curve.Knots ) )
# [ 0.0, 0.0, 0.0, 1.0, 2.0, 2.0, 2.0 ]
# | clamped | interpolated | clamped  |

Nurbs Curves

Definition

Nurbs or non-uniform rational basis splines extend BSplines by being able to represent rational curves such as circles and other conic sections, see additional information. This is achieved by introducing a 4th coordinate for each control point known as the weight. The notion of using 4D instead of 3D points originates in projective geometry, where an additional coordinate is used to allow representing points at infinity, see Gerald Farin's NURBS for Curve and Surface Design for additional information.

Evaluation

The De Boor's algorithm can be used for evaluating Nurbs curves. First the rational control points, about the knot interval containing the parameter, are decoupled and pre-scaled by their weights. Then the computed non-rational points and weights are used to define two BSpline curves within the reduced knot vector. Finally, both curves are evaluated and their quotient is returned.

Note that BSpline curves express a general polynomial scheme for smoothly interpolating values, which may be points or just simple numbers as in the case of the weight's curve.

""" Nurbs Curve
"""
class RationalBSplineCurve( BasisSplineCurve ):
    """ Construction
    """
    def __init__( self, points, degree ):
        BasisSplineCurve.__init__( self, points, degree )

    """ Decouple Points and Weights
    """
    def Decouple( self, points ):
        P = [Point3d( p.X * p.W,
                      p.Y * p.W,
                      p.Z * p.W )
                 for p in points]
        W = [p.W for p in points]
        return P, W

    """ Evaluate Point
    """
    def Evaluate( self, t ):
        d, k = self.KnotSpan( t )
        P, W = self.Decouple( self.Points[k-d:k+1] )
        K = self.Knots[k-d:k+d+1]

        A = BasisSplineCurve( P, d ); A.Knots = K
        w = BasisSplineCurve( W, d ); w.Knots = K

        return A.Evaluate( t ) / w.Evaluate( t )

""" Rational Cubic Curve
"""
controls = [
    Point4d( 0.0, 0.0, 0.0, 1.0 ),
    Point4d( 1.0, 1.0, 0.0, 5.0 ), #-- Rational
    Point4d( 2.0, 0.0, 0.0, 1.0 ),
    Point4d( 3.0,-1.0, 0.0, 5.0 ), #-- Rational
    Point4d( 4.0, 0.0, 0.0, 1.0 )]

curve = RationalBSplineCurve( controls, 3 )

""" Rational Circle
"""
r = 1.0                            #-- Radius
o = 0.5 * math.sqrt( 2.0 )         #-- Weight
controls = [Point4d( r, 0, 0, 1 ),
            Point4d( r, r, 0, o ),
            Point4d( 0, r, 0, 1 ),
            Point4d(-r, r, 0, o ),

            Point4d(-r, 0, 0, 1 ),
            Point4d(-r,-r, 0, o ),
            Point4d( 0,-r, 0, 1 ),
            Point4d( r,-r, 0, o )]
controls.append( controls[0] )

circle = RationalBSplineCurve( controls, 2 )
circle.Knots = [0.00, 0.00, 0.00,
                0.25, 0.25, 0.50,
                0.50, 0.75, 0.75,
                1.00, 1.00, 1.00]

Derivatives

The derivatives of Nurbs curves are significantly more complicated when compared to BSplines, see quotient rule. Additionally, there is no hodograph Nurbs curve representation. Therefore, they are computed per parameter. Conceptually, a Nurbs curve is converted into two BSpline curves, one for the pre-weighted control points and one for the weights themselves. Their derivatives are computed and their combinations are used to reconstruct the Nurbs curve's derivatives, see The NURBS Book for additional information.

class RationalBSplineCurve( BasisSplineCurve ):
    """ Nurbs Derivatives
    """
    def Derivatives( self, t, n = -1 ):
        d = self.Degree
        if( n < 0 or
            n > d ):
            n = d

        """ Non-Rational Derivatives
        """
        P, W = self.Decouple( self.Points )
        A = BasisSplineCurve( P, d ); A.Knots = self.Knots
        w = BasisSplineCurve( W, d ); w.Knots = self.Knots

        dA = A.Derivatives( t, n )
        dw = w.Derivatives( t, n )

        """ Rational Derivatives
        """
        D = [1.0] * ( n + 1 )
        for k in range( n + 1 ):
            u = Vector3d( dA[k] )
            for i in range( 1, k + 1 ):
                u -= math_comb( k, i ) * dw[i] * D[k - i]
            D[k] = u / dw[0]
        return D

Application

Rhino provides an extensive range of methods for constructing and manipulating Nurbs curves, see documentation. Note that construction of Nurbs curves is performed using the static methods starting with NurbsCurve.Create* instead of the class constructor.

""" Nurbs Curve Construction
"""
controls = [
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 1.0, 0.0 ),
    Point3d( 2.0, 0.0, 0.0 ),
    Point3d( 3.0,-1.0, 0.0 ),
    Point3d( 4.0, 0.0, 0.0 )]

""" Open Cubic BSpline
"""
periodic, degree = False, 3
curve = NurbsCurve.Create( periodic, degree, controls )
print( list( curve.Knots ) )
#-- [0.0, 0.0, 0.0, 1.0, 2.0, 2.0, 2.0]

""" Manual Nurbs Curve Construction
"""
controls = [
    Point4d( 1.5, 0.0, 0.0, 1.0 ),
    Point4d( 2.5, 1.0, 0.0, 5.0 ), #-- Rational
    Point4d( 3.5, 0.0, 0.0, 1.0 ),
    Point4d( 4.5,-1.0, 0.0, 5.0 ), #-- Rational
    Point4d( 5.5, 0.0, 0.0, 1.0 )]

dimensions, degree, rational = 3, 3, True
curve = NurbsCurve( dimensions,
    rational, degree, len( controls ) )

for index in range( len( controls ) ):
    curve.Points.SetPoint( index,
        Point3d( controls[index].X,
                 controls[index].Y,
                 controls[index].Z ),
                 controls[index].W )

curve.Knots.CreateUniformKnots( 1.0 )

""" Open Fitted/Interpolated Cubic BSpline
"""
points = [
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 1.0, 0.0 ),
    Point3d( 2.0, 0.0, 0.0 ),
    Point3d( 3.0,-1.0, 0.0 ),
    Point3d( 4.0, 0.0, 0.0 )]

curve = NurbsCurve.CreateInterpolatedCurve( points, 3 )

""" Periodic Rational BSpline
"""
curve = NurbsCurve.CreateFromCircle(
    Circle( Plane.WorldXY, 1.0 ) )

Some commonly used methods are presented below including projecting points onto curves, splitting and joining curves, performing Boolean operations and offsetting curves.

""" Point Projection on Curve
    returns projected point and parameter
"""
p, t = curve.ClosestPoint( Point3d.Origin )

""" Split curve in parts at parameter(s)
"""
parts = curve.Split( 0.5 )
parts = curve.Split( [0.25, 0.5, 0.75] )

""" Join curves with tolerance
"""
curves = NurbsCurve.JoinCurves( parts, 1e-3 )

""" Boolean operations on closed curves
"""
curves = NurbsCurve.CreateBooleanUnion( parts, 1e-3 )

""" Offset curves with rounded corners
"""
curves = curve.Offset( Plane.WorldXY, 0.1, 1e-5,
    CurveOffsetCornerStyle.Round )

Resources

Grasshopper