plugins.nurbs — pyFormex 1.0.5 documentation

# Source code for plugins.nurbs

    #
    ##
    ##  This file is part of pyFormex 1.0.5  (Sat Feb 16 10:40:32 CET 2019)
    ##  pyFormex is a tool for generating, manipulating and transforming 3D
    ##  geometrical models by sequences of mathematical operations.
    ##  Home page: http://pyformex.org
    ##  Project page:  http://savannah.nongnu.org/projects/pyformex/
    ##  Copyright 2004-2018 (C) Benedict Verhegghe (benedict.verhegghe@ugent.be)
    ##  Distributed under the GNU General Public License version 3 or later.
    ##
    ##  This program is free software: you can redistribute it and/or modify
    ##  it under the terms of the GNU General Public License as published by
    ##  the Free Software Foundation, either version 3 of the License, or
    ##  (at your option) any later version.
    ##
    ##  This program is distributed in the hope that it will be useful,
    ##  but WITHOUT ANY WARRANTY; without even the implied warranty of
    ##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ##  GNU General Public License for more details.
    ##
    ##  You should have received a copy of the GNU General Public License
    ##  along with this program.  If not, see http://www.gnu.org/licenses/.
    ##

    """Using NURBS in pyFormex.

    The :mod:`nurbs` module defines functions and classes to manipulate
    NURBS curves and surface in pyFormex.
    """
    from __future__ import absolute_import, division, print_function

    import numpy as np

    from pyformex import arraytools as at
    from pyformex import geomtools as gt
    from pyformex.coords import Coords
    from pyformex.attributes import Attributes
    from pyformex.lib import nurbs
    from pyformex.plugins import curve
    from pyformex import utils

    ###########################################################################
    ##
    ##   class Coords4
    ##
    #########################
    #
    class Coords4(np.ndarray):
        """A collection of points represented by their homogeneous coordinates.

        While most of the pyFormex implementation is based on the 3D Cartesian
        coordinates class :class:`Coords`, some applications may benefit from using
        homogeneous coordinates. The class :class:`Coords4` provides some basic
        functions and conversion to and from cartesian coordinates.
        Through the conversion, all other pyFormex functions, such as
        transformations, are available.

        :class:`Coords4` is implemented as a float type :class:`numpy.ndarray`
        whose last axis has a length equal to 4.
        Each set of 4 values (x,y,z,w) along the last axis represents a
        single point in 3D space. The cartesian coordinates of the point
        are obtained by dividing the first three values by the fourth:
        (x/w, y/w, z/w). A zero w-value represents a point at infinity.
        Converting such points to :class:`Coords` will result in Inf or NaN
        values in the resulting object.

        The float datatype is only checked at creation time. It is the
        responsibility of the user to keep this consistent throughout the
        lifetime of the object.

        Just like :class:`Coords`, the class :class:`Coords4` is derived from
        :class:`numpy.ndarray`.

        Parameters:

        `data`: array_like
          If specified, data should evaluate to an array of floats, with the
          length of its last axis not larger than 4. When equal to four, each
          tuple along the last axis represents a ingle point in homogeneous
          coordinates.
          If smaller than four, the last axis will be expanded to four by adding
          values zero in the second and third position and values 1 in the last
          position.
          If no data are given, a single point (0.,0.,0.) will be created.

        `w`: array_like
          If specified, the w values are used to denormalize the homogeneous
          data such that the last component becomes w.

        `dtyp`: data-type
          The datatype to be used. It not specified, the datatype of `data`
          is used, or the default :data:`Float` (which is equivalent to
          :data:`numpy.float32`).

        `copy`: boolean
          If ``True``, the data are copied. By default, the original data are
          used if possible, e.g. if a correctly shaped and typed
          :class:`numpy.ndarray` is specified.
        """

        def __new__(cls, data=None, w=None, normalize=True, dtyp=at.Float, copy=False):
            """Create a new instance of :class:`Coords4`."""
            if data is None:
                # create an empty array
                ar = np.ndarray((0, 4), dtype=dtyp)
            else:
                # turn the data into an array, and copy if requested
                ar = np.array(data, dtype=dtyp, copy=copy)

            if ar.shape[-1] in [3, 4]:
                pass
            elif ar.shape[-1] in [1, 2]:
                # make last axis length 3, adding 0 values
                ar = at.growAxis(ar, 3-ar.shape[-1], -1)
            elif ar.shape[-1] == 0:
                # allow empty coords objects
                ar = ar.reshape(0, 3)
            else:
                raise ValueError("Expected a length 1,2,3 or 4 for last array axis")

            # Make sure dtype is a float type
            if ar.dtype.kind != 'f':
                ar = ar.astype(at.Float)

            # We should now have a float array with last axis 3 or 4
            if ar.shape[-1] == 3:
                # Expand last axis to length 4, adding values 1
                ar = at.growAxis(ar, 1, -1, 1.0)

            if w is not None:
                if normalize:
                    ar[...,:3] /= w
                else:
                    # Insert weight
                    ar[...,3] = w

            # Transform 'subarr' from an ndarray to our new subclass.
            ar = ar.view(cls)

            return ar

        def normalize(self):
            """Normalize the homogeneous coordinates.

            Two sets of homogeneous coordinates that differ only by a
            multiplicative constant refer to the same points in cartesian space.
            Normalization of the coordinates is a way to make the representation
            of a single point unique. Normalization is done so that the last
            component (w) is equal to 1.

            The normalization of the coordinates is done in place.

            .. warning:: Normalizing points at infinity will result in Inf or
               NaN values.
            """
            self /= self[..., 3:]

        def deNormalize(self, w):
            """Denormalizes the homogeneous coordinates.

            This multiplies the homogeneous coordinates with the values w.
            w normally is a constant or an array with shape
            self.shape[:-1] + (1,).
            It then multiplies all 4 coordinates of a point with the same
            value, thus resulting in a denormalization while keeping the
            position of the point unchanged.

            The denormalization of the coordinates is done in place.
            If the Coords4 object was normalized, it will have precisely w as
            its 4-th coordinate value after the call.
            """
            self *= w

        def toCoords(self):
            """Convert homogeneous coordinates to cartesian coordinates.

            Returns:

            A :class:`Coords` object with the cartesian coordinates
            of the points. Points at infinity (w=0) will result in
            Inf or NaN value. If there are no points at infinity, the
            resulting :class:`Coords` point set is equivalent to the
            :class:`Coords4` one.
            """
            return Coords(self[..., :3] / self[..., 3:])

        def npoints(self):
            """Return the total number of points."""
            return np.asarray(self.shape[:-1]).prod()

        ncoords = npoints

        def x(self):
            """Return the x-plane"""
            return self[..., 0]
        def y(self):
            """Return the y-plane"""
            return self[..., 1]
        def z(self):
            """Return the z-plane"""
            return self[..., 2]
        def w(self):
            """Return the w-plane"""
            return self[..., 3]

        def bbox(self):
            """Return the bounding box of a set of points.

            Returns the bounding box of the cartesian coordinates of
            the object.
            """
            return self.toCoords().bbox()

        def actor(self,**kargs):
            """Graphical representation"""
            return self.toCoords().actor()

    class Geometry4(object):
        """This is a preliminary class intended to provide some transforms in 4D

        """

        def __init__(self):
            """Initialize a Geometry4"""
            self.attrib = Attributes()

        def scale(self,*args,**kargs):
            self.coords[..., :3] = Coords(self.coords[..., :3]).scale(*args,**kargs)
            return self

    class KnotVector(object):

        """A knot vector

        A knot vector is sequence of float values sorted in ascending order.
        Values can occur multiple times. In they typical use case for this
        class (Nurbs) most values do indeed occur multiple times, and the
        multiplicity of the values is an important quantity.
        Therefore, the knot vector is stored in two arrays of the same length:

        - `v`: the unique float values, a strictly ascending sequence
        - `m`: the multiplicity of each of the values

        Example:

        >>> K = KnotVector([0.,0.,0.,0.5,0.5,1.,1.,1.])
        >>> print(K.val)
        [ 0.   0.5  1. ]
        >>> print(K.mul)
        [3 2 3]
        >>> print(K)
        KnotVector: 0.0(3), 0.5(2), 1.0(3)
        >>> print([(v,m) for v,m in zip(K.val,K.mul)])
        [(0.0, 3), (0.5, 2), (1.0, 3)]
        >>> print(K.values())
        [ 0.   0.   0.   0.5  0.5  1.   1.   1. ]
        >>> K.index(0.5)
        1
        >>> K.span(1.0)
        5
        >>> K.mult(0.5)
        2
        >>> K.mult(0.7)
        0
        >>> K[4],K[-1]
        (0.5, 1.0)
        >>> K[4:6]
        array([ 0.5,  1. ], dtype=float32)

        """

        def __init__(self,data=None,val=None,mul=None):
            """Initialize a KnotVector"""
            if data is not None:
                data = at.checkArray(data,(-1,),'f','i')
                val,inv = np.unique(data,return_inverse=True)
                mul,bins = at.multiplicity(inv)
            else:
                val = at.checkArray(val,(-1,),'f','i')
                mul = at.checkArray(mul,val.shape,'i')
            self.val = val
            self.mul = mul
            self.csum = self.mul.cumsum() - 1

        def nknots(self):
            """Return the total number of knots"""
            return self.mul.sum()

    #    def knots(self):
    #        """Return the knots as tuples (value,multiplicity)"""
    #        return zip(self.val,self.mul)

        def values(self):
            """Return the full list of knot values"""
            return np.concatenate([ [v]*m for v,m in zip(self.val,self.mul) ])

        def __str__(self):
            """Format the knot vector as a string."""
            # TODO
            # the format method makes doctests fail.
            # We should check if we can set precision for all .format operations
            #return "KnotVector: " + ", ".join(["{}({})".format(v,m) for v,m in zip(self.val,self.mul)])
            return "KnotVector: " + ", ".join(["%s(%s)" % (v,m) for v,m in zip(self.val,self.mul)])

        def index(self,u):
            """Find the index of knot value u.

            If the value does not exist, a ValueError is raised.
            """
            w = np.where(np.isclose(self.val,u))[0]
            if len(w) <= 0:
                raise ValueError("The value %s does not appear in the KnotVector" % u)
            return w[0]

        def mult(self,u):
            """Return the multiplicity of knot value u.

            Returns an int with the multiplicity of the knot value u, or 0 if
            the value is not in the KNotVector.
            """
            try:
                i = self.index(u)
                return self.mul[i]
            except:
                return 0

        def span(self,u):
            """Find the (first) index of knot value u in the full knot values vector.

            If the value does not exist, a ValueError is raised.
            """
            i = self.index(u)
            return self.mul[:i].sum()

    #    def value(self,i):
    #        """Return knot i.
    #
    #        Returns the knot with the index i, taking into account
    #        the multiplicity of the knots.
    #        """
    #        ind = self.csum.searchsorted(i)
    #        return self.val[ind]
    #

        def __getitem__(self,i):
            """Return knot i.

            Returns the knot with the index i, taking into account
            the multiplicity of the knots.
            """
            if isinstance(i,slice):
                i = np.arange(i.start,i.stop,i.step)
            i %= self.nknots()
            ind = self.csum.searchsorted(i)
            return self.val[ind]

        def copy(self):
            """Return a copy of the KnotVector.

            Changing the copy will not change the original.
            """
            return KnotVector(val=self.val,mul=self.mul)

        def reverse(self):
            """Return the reverse knot vector.

            Example:

            >>> print(KnotVector([0,0,0,1,3,6,6,8,8,8]).reverse().values())
            [ 0.  0.  0.  2.  2.  5.  7.  8.  8.  8.]

            """
            val = self.val.min() + self.val.max() - self.val
            return KnotVector(val=val[::-1],mul=self.mul[::-1])

    def genKnotVector(nctrl,degree,blended=True,closed=False):
        """Compute sensible knot vector for a Nurbs curve.

        A knot vector is a sequence of non-decreasing parametric values. These
        values define the `knots`, i.e. the points where the analytical expression
        of the Nurbs curve may change. The knot values are only meaningful upon a
        multiplicative constant, and they are usually normalized to the range
        [0.0..1.0].

        A Nurbs curve with ``nctrl`` points and of given ``degree`` needs a knot
        vector with ``nknots = nctrl+degree+1`` values. A ``degree`` curve needs
        at least ``nctrl = degree+1`` control points, and thus at least
        ``nknots = 2*(degree+1)`` knot values.

        To make an open curve start and end in its end points, it needs knots with
        multiplicity ``degree+1`` at its ends. Thus, for an open blended curve, the
        default policy is to set the knot values at the ends to 0.0, resp. 1.0,
        both with multiplicity ``degree+1``, and to spread the remaining
        ``nctrl - degree - 1`` values equally over the interval.

        For a closed (blended) curve, the knots are equally spread over the
        interval, all having a multiplicity 1 for maximum continuity of the curve.

        For an open unblended curve, all internal knots get multiplicity ``degree``.
        This results in a curve that is only one time continuously derivable at
        the knots, thus the curve is smooth, but the curvature may be discontinuous.
        There is an extra requirement in this case: ``nctrl`` should be a multiple
        of ``degree`` plus 1.

        Returns a KnotVector instance.

        Example:

        >>> print(genKnotVector(7,3))
        KnotVector: 0.0(4), 0.25(1), 0.5(1), 0.75(1), 1.0(4)
        >>> print(genKnotVector(7,3,blended=False))
        KnotVector: 0.0(4), 1.0(3), 2.0(4)
        >>> print(genKnotVector(3,2,closed=True))
        KnotVector: 0.0(1), 0.2(1), 0.4(1), 0.6(1), 0.8(1), 1.0(1)
        """
        nknots = nctrl+degree+1
        if closed or blended:
            nval = nknots
            if not closed:
                nval -= 2*degree
            val = at.uniformParamValues(nval-1) # Returns nval values!
            mul = np.ones(nval,dtype=at.Int)
            if not closed:
                mul[0] = mul[-1] = degree+1
        else:
            nparts = (nctrl-1) // degree
            if nparts*degree+1 != nctrl:
                raise ValueError("Discrete knot vectors can only be used if the number of control points is a multiple of the degree, plus one.")
            val = np.arange(nparts+1).astype(at.Float)
            mul = np.ones(nparts+1,dtype=at.Int) * degree
            mul[0] = mul[-1] = degree+1

        return KnotVector(val=val,mul=mul)

    class NurbsCurve(Geometry4):

        """A NURBS curve

        The Nurbs curve is defined by nctrl control points, a degree (>= 1) and
        a knot vector with nknots = nctrl+degree+1 parameter values.

        Parameters:

        - `control`: Coords-like (nctrl,3): the vertices of the control polygon.
        - `degree`: int: the degree of the Nurbs curve. If not specified, it is
          derived from the length of the knot vector (`knots`).
        - `wts`: float array (nctrl): weights to be attributed to the control
          points. Default is to attribute a weight 1.0 to all points. Using
          different weights allows for more versatile modeling (like perfect
          circles and arcs.)
        - `knots`: KnotVector or an ascending list of nknots float values.
          The values are only defined upon a multiplicative constant and will
          be normalized to set the last value to 1.
          If `degree` is specified, default values are constructed automatically
          by calling :func:`genKnotVector`.
          If no knots are given and no degree is specified, the degree is set to
          the nctrl-1 if the curve is blended. If not blended, the degree is not
          set larger than 3.
        - `closed`: bool: determines whether the curve is closed. Default
          False. The use of closed NurbsCurves is currently very limited.
        - `blended`: bool: determines that the curve is blended. Default is True.
          Set blended==False to define a nonblended curve.
          A nonblended curve is a chain of independent curves, Bezier curves if the
          weights are all ones. See also :meth:`decompose`.
          The number of control points should be a multiple of the degree, plus one.      This parameter is only used if no knots are specified.

        """

        N_approx = 100
    #
    #    order (2,3,4,...) = degree+1 = min. number of control points
    #    ncontrol >= order
    #    nknots = order + ncontrol >= 2*order
    #
    #    convenient solutions:
    #    OPEN:
    #      nparts = (ncontrol-1) // degree
    #      nintern =
    #
        def __init__(self,control,degree=None,wts=None,knots=None,closed=False,blended=True):
            Geometry4.__init__(self)
            self.closed = closed
            nctrl = len(control)
            if knots is not None:
                if not isinstance(knots,KnotVector):
                    knots = KnotVector(knots)
                nknots = knots.nknots()

            if degree is None:
                if knots is None:
                    degree = nctrl-1
                    if not blended:
                        degree = min(degree, 3)
                else:
                    degree = nknots - nctrl -1
                    if degree <= 0:
                        raise ValueError("Length of knot vector (%s) must be at least number of control points (%s) plus 2" % (nknots, nctrl))

            order = degree+1
            control = Coords4(control)
            if wts is not None:
                control.deNormalize(wts.reshape(wts.shape[-1], 1))

            if closed:
                # We need to wrap nwrap control points
                if knots is None:
                    nwrap = degree
                else:
                    nwrap = nknots - nctrl - order
    #            # We split them over the start and end
    #            nextra1 = (nwrap+1) // 2
    #            nextra2 = nwrap-nextra1
    #            print("extra %s = %s + %s" % (nwrap, nextra1, nextra2))
    #            control = Coords4(concatenate([control[-nextra1:], control, control[:nextra2]], axis=0))
                # !! Changed: wrap at the end
                control = Coords4(np.concatenate([control, control[:nwrap]], axis=0))

            nctrl = control.shape[0]

            if nctrl < order:
                raise ValueError("Number of control points (%s) must not be smaller than order (%s)" % (nctrl, order))

            if knots is None:
                knots = genKnotVector(nctrl, degree, blended=blended, closed=closed)
                nknots = knots.nknots()

            if nknots != nctrl+order:
                raise ValueError("Length of knot vector (%s) must be equal to number of control points (%s) plus order (%s)" % (nknots, nctrl, order))

            self.coords = control
            self.knotu = knots
            self.degree = degree
            self.closed = closed

        @property
        def knots(self):
            """Return the full list of knot values"""
            return self.knotu.values()

        # This is commented out because there is only 1 place where we set
        # the knots: __init__
    #    @knots.setter
    #    def knots(self,value):
    #        """Set the knot values"""
    #        self.knotu = KnotVector(knots)

        def nctrl(self):
            """Return the number of control points"""
            return self.coords.shape[0]

        def nknots(self):
            """Return the number of knots"""
            return self.knotu.nknots()

        def order(self):
            """Return the order of the Nurbs curve"""
            return self.nknots()-self.nctrl()

        def urange(self):
            """Return the parameter range on which the curve is defined.

            Returns a (2,) float array with the minimum and maximum parameter
            value for which the curve is defined.
            """
            p = self.degree
            return [self.knotu[p],self.knotu[-1-p]]

        def isClamped(self):
            """Return True if the NurbsCurve uses a clamped knot vector.

            A clamped knot vector has a multiplicity p+1 for the first and
            last knot. All our generated knot vectors are clamped.
            """
            return self.knotu.mul[0] == self.knotu.mul[-1] == (self.degree + 1)

        def isUniform(self):
            """Return True if the NurbsCurve has a uniform knot vector.

            A uniform knot vector has a constant spacing between the knot values.
            """
            d = self.knotu.val[1:] - self.knotu.val[:-1]
            return np.isclose(d[1:],d[0]).all()

        def isRational(self):
            """Return True if the NurbsCurve is rational.

            The curve is rational if the weights are not constant.
            The curve is polygonal if the weights are constant.

            Returns True for a rational curve, False for a polygonal curve.
            """
            w = self.coords[:,3]
            return not np.isclose(w[1:],w[0]).all()

        def isBlended(self):
            """Return True if the NurbsCurve is blended.

            An clamped NurbsCurve is unblended (or decomposed) if it consists
            of a chain of independent Bezier curves.
            Such a curve has multiplicity p for all internal knots and p+1
            for the end knots of an open curve.
            Any other NurbsCurve is blended.

            Returns True for a blended curve, False for an unblended one.

            Note: for testing whether an unclamped curve is blended or not,
            first clamp it.
            """
            return self.isClamped() and (self.knotu.mul[1:-1] == self.degree).all()

        def bbox(self):
            """Return the bounding box of the NURBS curve.

            """
            return self.coords.toCoords().bbox()

        def __str__(self):
            return """NURBS Curve, degree = %s, nctrl = %s, nknots = %s
      closed: %s; clamped: %s; uniform: %s; rational: %s
      Control points:
    %s,
      %s
      urange = %s
    """  % ( self.degree, len(self.coords), self.nknots(), self.closed, self.isClamped(), self.isUniform(), self.isRational(), self.coords, self.knotu, self.urange())

        def copy(self):
            """Return a (deep) copy of self.

            Changing the copy will not change the original.
            """
            return NurbsCurve(control=self.coords.copy(),degree=self.degree,knots=self.knotu.copy(),closed=self.closed)

        def pointsAt(self, u):
            """Return the points on the Nurbs curve at given parametric values.

            Parameters:

            - `u`: (nu,) shaped float array, parametric values at which a point
              is to be placed. Note that valid points are only obtained for
              parameter values in the range self.range().

            Returns (nu,3) shaped Coords with nu points at the specified
            parametric values.

            """
            ctrl = self.coords.astype(np.double)
            knots = self.knotu.values().astype(np.double)
            u = np.atleast_1d(u).astype(np.double)

            try:
                pts = nurbs.curvePoints(ctrl, knots, u)
                if np.isnan(pts).any():
                    print("We got a NaN")
                    raise RuntimeError
            except:
                raise RuntimeError("Some error occurred during the evaluation of the Nurbs curve")

            if pts.shape[-1] == 4:
                pts = Coords4(pts).toCoords()
            else:
                pts = Coords(pts)
            return pts

        def derivs(self,u,d=1):
            """Returns the points and derivatives up to d at parameter values u

            Parameters:

            - `u`: either of:

              - int: number of points (npts) at which to evaluate the points
                and derivatives. The points will be equally spaced in parameter
                space.

              - float array (npts): parameter values at which to compute points
                and derivatives.

            - `d`: int: highest derivative to compute.

            Returns a float array of shape (d+1,npts,3).
            """
            if isinstance(u, int):
                u = at.uniformParamValues(u, self.knotu.val[0], self.knotu.val[-1])
            else:
                u = at.checkArray(u, (-1,), 'f', 'i')

            # sanitize arguments for library call
            ctrl = self.coords.astype(np.double)
            knots = self.knotu.values().astype(np.double)
            u = np.atleast_1d(u).astype(np.double)
            d = int(d)

            try:
                pts = nurbs.curveDerivs(ctrl, knots, u, d)
                if np.isnan(pts).any():
                    print("We got a NaN")
                    print(pts)
                    raise RuntimeError
            except:
                raise RuntimeError("Some error occurred during the evaluation of the Nurbs curve")

            if pts.shape[-1] == 4:
                pts = Coords4(pts)
                # When using no weights, ctrl points are Coords4 normalized points,
                # and the derivatives all have w=0: the points represent directions
                # We just strip off the w=0.
                # HOWEVER, if there are weights, not sure what to do.
                # Points themselves could be just normalized and returned.
                pts[0].normalize()
                pts = Coords(pts[..., :3])
            else:
                pts = Coords(pts)
            return pts

        def frenet(self,u):
            """Compute Frenet vectors, curvature and torsion at parameter values u

            Parameters:

            - `u`: either of:

              - int: number of points (npts) at which to evaluate the points
                and derivatives. The points will be equally spaced in parameter
                space.

              - float array (npts): parameter values at which to compute points
                and derivatives.

            Returns a float array of shape (d+1,npts,3).

            Returns a tuple of arrays at nu parameter values u:

            - `T`: normalized tangent vector (nu,3)
            - `N`: normalized normal vector (nu,3)
            - `B`: normalized binormal vector (nu,3)
            - `k`: curvature of the curve (nu)
            - `t`: torsion of the curve (nu)

            """
            derivs = self.derivs(u,3)
            return frenet(derivs[1],derivs[2],derivs[3])

        def curvature(self,u,torsion=False):
            """Compute Frenet vectors, curvature and torsion at parameter values u

            Parameters:

            - `u`: either of:

              - int: number of points (npts) at which to evaluate the points
                and derivatives. The points will be equally spaced in parameter
                space.

              - float array (npts): parameter values at which to compute points
                and derivatives.

            - `torsion`: bool. If True, also returns the torsion in the curve.

            If `torsion` is False (default), returns a float array with the
            curvature at parameter values `u`.
            If `torsion` is True, also returns a float array with the
            torsion at parameter values `u`.

            """
            T,N,B,k,t = self.frenet(u)
            if torsion:
                return k,t
            else:
                return k

        def knotPoints(self,multiple=False):
            """Returns the points at the knot values.

            If multiple is True, points are returned with their multiplicity.
            The default is to return all points just once.
            """
            if multiple:
                val = self.knotu.knots
            else:
                val = self.knotu.val
            return self.pointsAt(val)

        def insertKnots(self, u):
            """Insert a set of knots in the Nurbs curve.

            u is a vector with knot parameter values to be inserted into the
            curve. The control points are adapted to keep the curve unchanged.

            Returns:

            A Nurbs curve equivalent with the original but with the
            specified knot values inserted in the knot vector, and the control
            points adapted.
            """
            if self.closed:
                raise ValueError("insertKnots currently does not work on closed curves")
            # sanitize arguments for library call
            ctrl = self.coords.astype(np.double)
            knots = self.knotu.values().astype(np.double)
            u = np.asarray(u).astype(np.double)
            newP, newU = nurbs.curveKnotRefine(ctrl, knots, u)
            return NurbsCurve(newP, degree=self.degree, knots=newU, closed=self.closed)

        def requireKnots(self, val, mul):
            """Insert knots until the required multiplicity reached.

            Inserts knot values only if they are currently not there or their
            multiplicity is lower than the required one.

            Parameters:

            - `val`: list of float (nval): knot values required in the knot vector.
            - `mul`: list of int (nval): multiplicities required for the knot values `u`.

            Returns:

            A Nurbs curve equivalent with the original but where the knot vector
            is guaranteed to contain the values in `u` with at least the
            corresponding multiplicity in `m`. If all requirements were already
            fulfilled at the beginning, returns self.

            """
            # get actual multiplicities
            m = np.array([ self.knotu.mult(ui) for ui in val ])
            # compute missing multiplicities
            mul = mul - m
            if (mul > 0).any():
                # list of knots to insert
                u = [ [ui]*mi for ui,mi in zip(val,mul) if mi > 0 ]
                return self.insertKnots(np.concatenate(u))
            else:
                return self

        def subCurve(self,u1,u2):
            """Extract the subcurve between parameter values u1 and u2

            Parameters:

            - `u1`, `u2`: two parameter values (u1 < u2), delimiting the part of the
              curve to extract. These values do not have to be knot values.

            Returns a NurbsCurve containing only the part between u1 and u2.
            """
            p = self.degree
            # Make sure we have the knots
            N = self.requireKnots([u1,u2],[p+1,p+1])
            j1 = N.knotu.index(u1)
            j2 = N.knotu.index(u2)
            knots = KnotVector(val=N.knotu.val[j1:j2+1],mul=N.knotu.mul[j1:j2+1])
            k1 = N.knotu.span(u1)
            nctrl = knots.nknots() - p - 1
            ctrl = N.coords[k1:k1+nctrl]
            return NurbsCurve(control=ctrl,degree=p,knots=knots,closed=self.closed)

        def clamp(self):
            """Clamp the knot vector of the curve.

            A clamped knot vector starts and ends with multiplicities p-1.
            See also :meth:`isClamped`.

            Returns self if the curve is already clamped, else returns an
            equivalent curve with clamped knot vector.

            Note: The use of unclamped knot vectors is deprecated.
            This method is provided only as a convenient method to import
            curves from legacy systems using unclamped knot vectors.

            """
            if self.isClamped():
                return self

            else:
                p = self.degree
                u1, u2 = self.knotu.val[[p,-1-p]]
                return self.subCurve(u1,u2)

        def unclamp(self):
            """Unclamp the knot vector of the curve.

            An unclamped knot vector starts and ends with multiplicities p-1.
            See also :meth:`isClamped`.

            Returns self if the curve is already clamped, else returns an
            equivalent curve with clamped knot vector.

            Note: The use of unclamped knot vectors is deprecated.
            This method is provided as a convenient method to export
            curves to legacy systems that only handle unclamped knot vectors.

            """
            if self.isClamped():
                from pyformex.lib.nurbs_e import curveUnclamp
                P,U = curveUnclamp(self.coords,self.knotu.values())
                return NurbsCurve(control=P,degree=self.degree,knots=U,closed=self.closed)

            else:
                return self

        def unblend(self):
            """Decomposes a curve in subsequent Bezier curves.

            Returns an equivalent unblended Nurbs.

            See also :meth:`toBezier`
            """
            # sanitize arguments for library call
            ctrl = self.coords
            knots = self.knotu.values().astype(np.double)
            X = nurbs.curveDecompose(ctrl, knots)
            return NurbsCurve(X, degree=self.degree, blended=False)

        # For compatibility
        decompose = unblend

        def toCurve(self,force_Bezier=False):
            """Convert a (nonrational) NurbsCurve to a BezierSpline or PolyLine.

            This decomposes the curve in a chain of Bezier curves and converts
            the chain to a BezierSpline or PolyLine.

            This only works for nonrational NurbsCurves, as the
            BezierSpline and PolyLine classes do not allow homogeneous
            coordinates required for rational curves.

            Returns a BezierSpline or PolyLine (if degree is 1) that is equivalent
            with the NurbsCurve.

            See also :meth:`unblend` which decomposes both rational and
            nonrational NurbsCurves.

            """
            if self.isRational():
                raise ValueError("Can not convert a rational NURBS ro Bezier")

            ctrl = self.coords
            knots = self.knotu.values()
            X = nurbs.curveDecompose(ctrl, knots)
            X = Coords4(X).toCoords()
            if self.degree > 1 or force_Bezier:
                return curve.BezierSpline(control=X,degree=self.degree,closed=self.closed)
            else:
                return curve.PolyLine(X,closed=self.closed)

        def toBezier(self):
            """Convert a (nonrational) NurbsCurve to a BezierSpline.

            This is equivalent with toCurve(force_Bezier=True) and returns
            a BezierSpline in all cases.
            """
            return self.toCurve(force_Bezier=True)

        def removeKnot(self, u, m, tol=1.e-5):
            """Remove a knot from the knot vector of the Nurbs curve.

            u: knot value to remove
            m: how many times to remove (if negative, remove maximally)

            Returns:

            A Nurbs curve equivalent with the original but with a knot vector
            where the specified value has been removed m times, if possible,
            or else as many times as possible. The control points are adapted
            accordingly.

            """
            if self.closed:
                raise ValueError("removeKnots currently does not work on closed curves")

            i = self.knotu.index(u)

            if m < 0:
                m = self.knotu.mul[i]

            P = self.coords.astype(np.double)
            Uv = self.knotu.val.astype(np.double)
            Um = self.knotu.mul.astype(at.Int)
            t,newP,newU = nurbs.curveKnotRemove(P,Uv,Um,i,m,tol)

            if t > 0:
                print("Removed the knot value %s %s times" % (u,t))
                return NurbsCurve(newP, degree=self.degree, knots=newU, closed=self.closed)
            else:
                print("Can not remove the knot value %s" % u)
                return self

        def removeAllKnots(self,tol=1.e-5):
            """Remove all removable knots

            Parameters:

            - `tol`: float: acceptable error (distance between old and new curve).

            Returns an equivalent (if tol is small) NurbsCurve with all
            extraneous knots removed.

            """
            N = self
            print(N)
            while True:
                print(N)
                for u in N.knotu.val:
                    print("Removing %s" % u)
                    NN = N.removeKnot(u,m=-1,tol=tol)
                    if NN is N:
                        print("Can not remove")
                        continue
                    else:
                        break
                if NN is N:
                    print("Done")
                    break
                else:
                    print("Cycle")
                    N = NN
            return N

        blend = removeAllKnots

        def elevateDegree(self, t=1):
            """Elevate the degree of the Nurbs curve.

            t: how much to elevate the degree

            Returns:

            A Nurbs curve equivalent with the original but of a higher degree.

            """
            if self.closed:
                raise ValueError("elevateDegree currently does not work on closed curves")

            P = self.coords.astype(np.double)
            U = self.knotu.values()

            newP,newU,nh,mh = nurbs.curveDegreeElevate(P, U, t)

            return NurbsCurve(newP, degree=self.degree+t, knots=newU, closed=self.closed)

        def reduceDegree(self, t=1):
            """Reduce the degree of the Nurbs curve.

            t: how much to reduce the degree (max. = degree-1)

            Returns:

            A Nurbs curve approximating the original but of a lower degree.

            """
            from pyformex.lib.nurbs_e import curveDegreeReduce
    #        from nurbs import curveDegreeReduce
            if self.closed:
                raise ValueError("reduceDegree currently does not work on closed curves")

            if t >= self.degree:
                raise ValueError("Can maximally reduce degree %s times" % self.degree-1)

            N = self
            while t > 0:
                newP,newU,nh,mh,maxerr = curveDegreeReduce(N.coords, N.knots)
                #newP,newU,nh,mh = nurbs.curveDegreeReduce(N.coords, N.knots)
                N = NurbsCurve(newP, degree=self.degree-1, knots=newU, closed=self.closed)
                print("Reduced to degree %s with maxerr = %s" % (N.degree,maxerr))
                t -= 1

            return N

        # TODO: This might be implemented in C for efficiency
        def projectPoint(self, P, eps1=1.e-5, eps2=1.e-5, maxit=20, nseed=20):
            """Project a given point on the Nurbs curve.

            This can also be used to determine the parameter value of a point
            lying on the curve.

            Parameters:

            - `P`: Coords-like (npts,3): one or more points is space.

            Returns a tuple (u,X):

            - `u`: float: parameter value of the base point X of the projection
              of P on the NurbsCurve.
            - `X`: Coords (3,): the base point of the projection of P
              on the NurbsCurve.

            The algorithm is based on the from The Nurbs Book.

            """
            P = at.checkArray(P,(3,),'f','i')

            # Determine start value from best match of nseed+1 points
            umin,umax = self.knotu.val[[0,-1]]
            u = at.uniformParamValues(nseed+1,umin,umax)
            pts = self.pointsAt(u)
            i = pts.distanceFromPoint(P).argmin()
            u0,P0 = u[i],pts[i]
            #print("Start at point %s : %s" % (u0,P0))

            # Apply Newton's method to minimize distance
            i = 0
            ui = u0
            while i < maxit:
                i += 1
                C = self.derivs([ui],2)
                C0,C1,C2 = C[:,0]
                CP = (C0-P)
                CPxCP = np.dot(CP,CP)
                C1xCP = np.dot(C1,CP)
                C1xC1 = np.dot(C1,C1)
                eps1sq = eps1*eps1
                eps2sq = eps2*eps2
                # Check convergence
                chk1 = CPxCP <= eps1sq
                chk2 = C1xCP / C1xC1 / CPxCP <= eps2sq

                uj = ui - np.dot(C1,CP) / ( np.dot(C2,CP) + np.dot(C1,C1) )
                # ensure that parameter stays in range
                if self.closed:
                    while uj < umin:
                        uj += umax - umin
                    while uj > umax:
                        uj -= umax - umin
                else:
                    if uj < umin:
                        uj = umin
                    if uj > umax:
                        uj = umax

                # Check convergence
                chk4 = (uj-ui)**2 * C1xC1 <= eps1sq
                P0 = self.pointsAt([uj])[0]
                #print("u[%s] = %s (P=%s) conv=%s" % (i,uj,P0,(chk1,chk2,chk4)))

                if (chk1 or chk2) and chk4:
                    # Converged!
                    break
                else:
                    # Prepare for next it
                    ui = uj

            if i == maxit:
                print("Convergence not reached after %s iterations" % maxit)

            return u0,P0

        def approx(self,ndiv=None,nseg=None,**kargs):
            """Return a PolyLine approximation of the Nurbs curve

            If no `nseg` is given, the curve is approximated by a PolyLine
            through equidistant `ndiv+1` point in parameter space. These points
            may be far from equidistant in Cartesian space.

            If `nseg` is given, a second approximation is computed with `nseg`
            straight segments of nearly equal length. The lengths are computed
            based on the first approximation with `ndiv` segments.
            """
            from pyformex.plugins.curve import PolyLine
            if ndiv is None:
                ndiv = self.N_approx
            umin,umax = self.urange()
            u = at.uniformParamValues(ndiv,umin,umax)
            PL = PolyLine(self.pointsAt(u))
            if nseg is not None:
                u = PL.atLength(nseg)
                PL = PolyLine(PL.pointsAt(u))
            return PL

        def actor(self,**kargs):
            """Graphical representation"""
            from pyformex.opengl.actors import Actor
            G = self.approx(ndiv=100).toFormex()
            G.attrib(**self.attrib)
            return Actor(G,**kargs)

        def reverse(self):
            """Return the reversed Nurbs curve.

            The reversed curve is geometrically identical, but start and en point
            are interchanged and parameter values increase in the opposite direction.
            """
            return NurbsCurve(control=self.coords[::-1], knots=self.knotu.reverse(), degree=self.degree, closed=self.closed)

    #######################################################
    ## NURBS Surface ##

    class NurbsSurface(Geometry4):

        """A NURBS surface

        The Nurbs surface is defined as a tensor product of NURBS curves in two
        parametrical directions u and v. The control points form a grid of
        (nctrlu,nctrlv) points. The other data are like those for a NURBS curve,
        but need to be specified as a tuple for the (u,v) directions.

        The knot values are only defined upon a multiplicative constant, equal to
        the largest value. Sensible default values are constructed automatically
        by a call to the :func:`genKnotVector` function.

        If no knots are given and no degree is specified, the degree is set to
        the number of control points - 1 if the curve is blended. If not blended,
        the degree is not set larger than 3.

        .. warning:: This is a class under development!

        """

        def __init__(self,control,degree=(None, None),wts=None,knots=(None, None),closed=(False, False),blended=(True, True)):
            """Initialize the NurbsSurface.

            """

            Geometry4.__init__(self)
            self.closed = closed

            control = Coords4(control)
            if wts is not None:
                control.deNormalize(wts.reshape(wts.shape[-1], 1))

            for d in range(2):
                nctrl = control.shape[1-d] # BEWARE! the order of the nodes
                deg = degree[d]
                kn = knots[d]
                bl = blended[d]
                cl = closed[d]

                if deg is None:
                    if kn is None:
                        deg = nctrl-1
                        if not bl:
                            deg = min(deg, 3)
                    else:
                        deg = len(kn) - nctrl -1
                        if deg <= 0:
                            raise ValueError("Length of knot vector (%s) must be at least number of control points (%s) plus 2" % (len(knots), nctrl))
                    # make degree changeable
                    degree = list(degree)
                    degree[d] = deg

                order = deg+1

                if nctrl < order:
                    raise ValueError("Number of control points (%s) must not be smaller than order (%s)" % (nctrl, order))

                if kn is None:
                    kn = genKnotVector(nctrl, deg, blended=bl, closed=cl).values()
                else:
                    kn = np.asarray(kn).ravel()

                nknots = kn.shape[0]

                if nknots != nctrl+order:
                    raise ValueError("Length of knot vector (%s) must be equal to number of control points (%s) plus order (%s)" % (nknots, nctrl, order))

                if d == 0:
                    self.knotu = kn
                else:
                    self.knotv = kn

            self.coords = control
            self.degree = degree
            self.closed = closed

        def order(self):
            return (self.knotu.shape[0]-self.coords.shape[1],
                    self.knotv.shape[0]-self.coords.shape[0])

        def urange(self):
            """Return the u-parameter range on which the curve is defined.

            Returns a (2,) float array with the minimum and maximum parameter
            value u for which the curve is defined.
            """
            p = self.degree[0]
            return [self.knotu[p],self.knotu[-1-p]]

        def vrange(self):
            """Return the v-parameter range on which the curve is defined.

            Returns a (2,) float array with the minimum and maximum parameter
            value v for which the curve is defined.
            """
            p = self.degree[1]
            return [self.knotv[p],self.knotv[-1-p]]

        def bbox(self):
            """Return the bounding box of the NURBS surface.

            """
            return self.coords.toCoords().bbox()

        def pointsAt(self, u):
            """Return the points on the Nurbs surface at given parametric values.

            Parameters:

            - `u`: (nu,2) shaped float array: `nu` parametric values (u,v) at which
              a point is to be placed.

            Returns (nu,3) shaped Coords with `nu` points at the specified
            parametric values.

            """
            ctrl = self.coords.astype(np.double)
            U = self.knotv.astype(np.double)
            V = self.knotu.astype(np.double)
            u = np.asarray(u).astype(np.double)

            try:
                pts = nurbs.surfacePoints(ctrl, U, V, u)
                if np.isnan(pts).any():
                    print("We got a NaN")
                    raise RuntimeError
            except:
                raise RuntimeError("Some error occurred during the evaluation of the Nurbs surface.\nPerhaps you are not using the compiled library?")

            if pts.shape[-1] == 4:
                pts = Coords4(pts).toCoords()
            else:
                pts = Coords(pts)
            return pts

        def derivs(self, u, m):
            """Return points and derivatives at given parametric values.

            Parameters:

            - `u`: (nu,2) shaped float array: `nu` parametric values (u,v) at which
              the points and derivatives are evaluated.
            - `m`: tuple of two int values (mu,mv). The points and derivatives up
              to order mu in u direction and mv in v direction are returned.

            Returns:

            (nu+1,nv+1,nu,3) shaped Coords with `nu` points at the
            specified parametric values. The slice (0,0,:,:) contains the
            points.

            """
            # sanitize arguments for library call
            ctrl = self.coords.astype(np.double)
            U = self.knotv.astype(np.double)
            V = self.knotu.astype(np.double)
            u = np.asarray(u).astype(np.double)
            mu, mv = m
            mu = int(mu)
            mv = int(mv)

            try:
                pts = nurbs.surfaceDerivs(ctrl, U, V, u, mu, mv)
                if np.isnan(pts).any():
                    print("We got a NaN")
                    raise RuntimeError
            except:
                raise RuntimeError("Some error occurred during the evaluation of the Nurbs surface")

            if pts.shape[-1] == 4:
                pts = Coords4(pts)
                pts[0][0].normalize()
                pts = Coords(pts[..., :3])
            else:
                pts = Coords(pts)
            return pts

        def approx(self,ndiv=None,**kargs):
            """Return a Quad4 Mesh approximation of the Nurbs surface

            Parameters:

            - `ndiv`: number of divisions of the parametric space.
            """
            from pyformex.mesh import Mesh, quad4_els
            if ndiv is None:
                ndiv = self.N_approx
            if at.isInt(ndiv):
                ndiv = (ndiv,ndiv)
            udiv,vdiv = ndiv
            umin,umax = self.urange()
            vmin,vmax = self.vrange()
            u = at.uniformParamValues(udiv,umin,umax)
            v = at.uniformParamValues(udiv,umin,umax)
            uv = np.ones((udiv+1,vdiv+1,2))
            uv[:,:,0] *= u
            uv[:,:,1] *= v.reshape(-1,1)
            coords = self.pointsAt(uv.reshape(-1,2))
            elems = quad4_els(udiv,vdiv)
            return Mesh(coords,elems,eltype='quad4')

        def actor(self,**kargs):
            """Graphical representation"""
            from pyformex.opengl.actors import Actor
            G = self.approx(ndiv=100)
            G.attrib(**self.attrib)
            return Actor(G,**kargs)

    ################################################################

    def globalInterpolationCurve(Q,degree=3,strategy=0.5):
        """Create a global interpolation NurbsCurve.

        Given an ordered set of points Q, the globalInterpolationCurve
        is a NURBS curve of the given degree, passing through all the
        points.

        Returns:

        A NurbsCurve through the given point set. The number of
        control points is the same as the number of input points.

        .. warning:: Currently there is the limitation that two consecutive
          points should not coincide. If they do, a warning is shown and
          the double points will be removed.

        The procedure works by computing the control points that will
        produce a NurbsCurve with the given points occurring at predefined
        parameter values. The strategy to set this values uses a parameter
        as exponent. Different values produce (slighly) different curves.
        Typical values are:

        0.0: equally spaced (not recommended)
        0.5: centripetal (default, recommended)
        1.0: chord length (often used)
        """
        from pyformex.plugins.curve import PolyLine
        # set the knot values at the points
        #nc = Q.shape[0]
        #n = nc-1

        # chord length
        d = PolyLine(Q).lengths()
        if (d==0.0).any():
            utils.warn("warn_nurbs_gic")
            Q = np.concatenate([Q[d!=0.0], Q[-1:]], axis=0)
            d = PolyLine(Q).lengths()
            if (d==0.0).any():
                raise ValueError("Double points in the data set are not allowed")
        # apply strategy
        d = d ** strategy
        d = d.cumsum()
        d /= d[-1]
        u = np.concatenate([[0.], d])
        #print "u = ",u
        U, A = nurbs.curveGlobalInterpolationMatrix(Q, u, degree)
        #print "U = ",U
        #print "A = ",A
        P = np.linalg.solve(A, Q)
        #print "P = ",P
        return NurbsCurve(P, knots=U, degree=degree)

    def NurbsCircle(O=[0.,0.,0.],r=1.0,X=[1.,0.,0.],Y=[0.,1.,0.],ths=0.,the=360.):
        """Create a NurbsCurve representing a perfect circle or arc.

        Parameters:

        - `O`: float (3,): center of the circle
        - `r`: float: radius
        - `X`: unit vector in the plane of the circle
        - 'Y': unit vector in the plane of the circle and perpendicular to `X`
        - `ths`: start angle, measured from the X axis, coungerclockwise in X-Y plane
        - `the`: end angle, measured from the X axis

        Returns a NurbsCurve that is a perfect circle or arc.
        """
        if the < ths:
            the += 360.
        theta = (the-ths)
        # Get the number of arcs
        narcs = int(np.ceil(theta/90.))
        n = 2*narcs   # n+1 control points
        O,X,Y = ( at.checkArray(x,(3,),'f','i') for x in (O,X,Y) )
        dths = ths*at.DEG
        dtheta = theta*at.DEG/narcs
        w1 = np.cos(dtheta/2.) # base angle
        # Initialize start values
        P0 = O + r*np.cos(dths)*X + r*np.sin(dths)*Y
        T0 = -np.sin(ths)*X + np.cos(ths)*Y
        Pw = np.zeros((n+1,4),dtype=at.Float)
        Pw[0] = Coords4(P0)
        index = 0
        angle = ths*at.DEG
        # create narcs segments
        for i in range(1,narcs+1):
            angle += dtheta
            P2 = O + r*np.cos(angle)*X + r*np.sin(angle)*Y
            Pw[index+2] = Coords4(P2)
            T2 = -np.sin(angle)*X + np.cos(angle)*Y
            P1,P1b = gt.intersectLineWithLine(P0,T0,P2,T2)
            Pw[index+1] = Coords4(P1) * w1
            Pw[index+1,3] = w1
            index += 2
            if i < narcs:
                P0,T0 = P2,T2
        # Load the knot vector
        j= 2*narcs+1
        U = np.zeros((j+3,),dtype=at.Float)
        for i in range(3):
            U[i] = 0.
            U[i+j] = 1.
        if narcs == 2:
            U[3] = U[4] = 0.5
        elif narcs == 3:
            U[3] = U[4] = 1./3.
            U[5] = U[6] = 2./3.
        elif narcs == 4:
            U[3] = U[4] = 0.25
            U[5] = U[6] = 0.5
            U[7] = U[8] = 0.75

        return NurbsCurve(control=Pw,degree=2,knots=U)

    def toCoords4(x):
        """Convert cartesian coordinates to homogeneous

        `x`: :class:`Coords`
          Array with cartesian coordinates.

        Returns a Coords4 object corresponding to the input cartesian coordinates.
        """
        return Coords4(x)

    Coords.toCoords4 = toCoords4

    def pointsOnBezierCurve(P, u):
        """Compute points on a Bezier curve

        Parameters:

        P is an array with n+1 points defining a Bezier curve of degree n.
        u is a vector with nu parameter values between 0 and 1.

        Returns:

        An array with the nu points of the Bezier curve corresponding with the
        specified parametric values.
        ERROR: currently u is a single paramtric value!

        See also:
        examples BezierCurve, Casteljau
        """
        u = np.asarray(u).ravel()
        n = P.shape[0]-1
        return Coords.concatenate([
            (nurbs.allBernstein(n, ui).reshape(1, -1, 1) * P).sum(axis=1)
            for ui in u ], axis=0)

    def deCasteljau(P, u):
        """Compute points on a Bezier curve using deCasteljau algorithm

        Parameters:

        P is an array with n+1 points defining a Bezier curve of degree n.
        u is a single parameter value between 0 and 1.

        Returns:

        A list with point sets obtained in the subsequent deCasteljau
        approximations. The first one is the set of control points, the last one
        is the point on the Bezier curve.

        This function works with Coords as well as Coords4 points.
        """
        n = P.shape[0]-1
        C = [P]
        for k in range(n):
            Q = C[-1]
            Q = (1.-u) * Q[:-1] + u * Q[1:]
            C.append(Q)
        return C

    def splitBezierCurve(P, u):
        """Split a Bezier curve at parametric values

        Parameters:

        P is an array with n+1 points defining a Bezier curve of degree n.
        u is a single parameter value between 0 and 1.

        Returns two arrays of n+1 points, defining the Bezier curves of degree n
        obtained by splitting the input curve at parametric value u. These results
        can be used with the control argument of BezierSpline to create the
        corresponding curve.

        """
        C = deCasteljau(P, u)
        L = np.stack([x[0] for x in C])
        R = np.stack([x[-1] for x in C[::-1]])
        return L,R

    def frenet(d1,d2,d3=None):
        """Compute Frenet vectors, curvature and torsion.

        Parameters:

        - `d1`: first derivative at `npts` points of a nurbs curve
        - `d2`: second derivative at `npts` points of a nurbs curve
        - `d3`: (optional) third derivative at `npts` points of a nurbs curve

        The derivatives of the nurbs curve are normally obtained from
        :func:`NurbsCurve.deriv`.

        Returns:

        - `T`: normalized tangent vector to the curve at `npts` points
        - `N`: normalized normal vector to the curve at `npts` points
        - `B`: normalized binormal vector to the curve at `npts` points
        - `k`: curvature of the curve at `npts` points
        - `t`: (only if `d3` was specified) torsion of the curve at `npts` points

        Curvature is found from  `| d1 x d2 | / |d1|**3`

        """
        l = at.length(d1)
        # What to do when l is 0? same as with k?
        if l.min() == 0.0:
            print("l is zero at %s" % np.where(l==0.0)[0])
        e1 = d1 / l.reshape(-1, 1)
        e2 = d2 - at.dotpr(d2, e1).reshape(-1, 1)*e1
        k = at.length(e2)
        if k.min() == 0.0:
            w = np.where(k==0.0)[0]
            print("k is zero at %s" % w)
        # where k = 0: set e2 to mean of previous and following
        e2 /= k.reshape(-1, 1)
        #e3 = normalize(ddd - dotpr(ddd,e1)*e1 - dotpr(ddd,e2)*e2)
        e3 = np.cross(e1, e2)
        #m = at.dotpr(np.cross(d1, d2), e3)
        #print "m",m
        m = np.cross(d1,d2)
        k = at.length(m) / l**3
        if d3 is None:
            return e1, e2, e3, k
        # compute torsion
        t = at.dotpr(d1, np.cross(d2, d3)) / at.dotpr(d1, d2)
        return e1, e2, e3, k, t

    ### End