Source code for plugins.nurbs

#
##
##  SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.4  (Thu Nov 16 18:07:39 CET 2023)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  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 surfaces in pyFormex. The most important classes and
functions for the end user are:

- the :class:`NurbsCurve` class
- the :class:`NurbsSurface` class
- :func:`cubicInterpolate`: construct a cubic NurbsCurve through given points
- :func:`quadraticInterpolate`: construct a quadratic NurbsCurve through given points

"""

import numpy as np

from pyformex import arraytools as at
from pyformex import geomtools as gt
from pyformex import lib
from pyformex.coords import Coords
from pyformex.attributes import Attributes
from pyformex.mesh import Mesh
from pyformex.elements import Quad4
from pyformex import curve
from pyformex import utils

_py2rst_order_ = ['NurbsCurve', 'NurbsSurface']   # for docs only

###########################################################################
##
##   class Coords4
##
#########################
#
[docs]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 4-dimensional coordinates. The :class:`Coords4` class provides some basic functions, including conversion to and from cartesian coordinates. Through the conversion, all pyFormex functions defined on :class:`Coords`, such as transformations, are possible. :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: :term:`array_like`, optional An array of floats, with the length of its last axis not larger than 4. If equal to four, each tuple along the last axis represents a single point in homogeneous coordinates. If smaller than four, the last axis is expanded to four by adding zero values in the second and third position and unity values in the last position (the w-coordinate). If no data are given, a single point (0.,0.,0.,1.) is created. w: :term:`array_like`, optional If specified, the w values are used to denormalize the homogeneous data such that the last component becomes w. dtyp: data-type, optional 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: bool, optional 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 provided. """ 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
[docs] 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. Examples -------- >>> X4 = Coords4([2, 3, 4, 2]) >>> X4.normalize() >>> X4 Coords4([1. , 1.5, 2. , 1. ]) """ self /= self[..., 3:]
[docs] def deNormalize(self, w): """Denormalize 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. Examples -------- >>> X4 = Coords4([1, 2, 3, 1]) >>> X4.deNormalize(2) >>> X4 Coords4([2., 4., 6., 2.]) """ self *= w
[docs] 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` input. Examples -------- >>> Coords4([2, 3, 4, 2]).toCoords() Coords([1. , 1.5, 2. ]) """ return Coords(self[..., :3] / self[..., 3:])
[docs] def npoints(self): """Return the total number of points in the Coords4.""" return np.asarray(self.shape[:-1]).prod()
ncoords = npoints
[docs] def x(self): """Return the x-plane""" return self[..., 0]
[docs] def y(self): """Return the y-plane""" return self[..., 1]
[docs] def z(self): """Return the z-plane""" return self[..., 2]
[docs] def w(self): """Return the w-plane""" return self[..., 3]
[docs] 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()
[docs] def actor(self, **kargs): """Graphical representation""" return self.toCoords().actor(**kargs)
[docs]class Geometry4(): """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.coords4[..., :3] = Coords(self.coords4[..., :3]).scale(*args, **kargs) return self
[docs]class KnotVector(): """A knot vector. A knot vector is sequence of float values sorted in ascending order. Values can occur multiple times. In the typical use case (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: val and mul. Attributes ---------- val: float array (nval) The unique float values, in a strictly ascending sequence. mul: int array (nval) The multiplicity of each of the values in val. See Also -------- genKnotVector: generate a knot vector for a NURBS curve Examples -------- >>> 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(8): 0*3, 0.5*2, 1*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. ] >>> print(K.nknots) 8 >>> 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. ]) """ # This makes the KnotVector string format customizable. # The {v} field formats the value, {m} the multiplicity of the knot. _knot_format = "{v:.6g}*{m}" 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.astype(np.float32) self.mul = mul self.csum = self.mul.cumsum() - 1 @property 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)
[docs] 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.""" return f"KnotVector({self.nknots}): " + ", ".join([ KnotVector._knot_format.format(v=v, m=m) for v, m in zip(self.val, self.mul)])
[docs] 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( f"The value {u} does not appear in the KnotVector") return w[0]
[docs] def vindex(self, j): """Find the index of knot value with index j. j should be in the range(0,self.nknots), else a ValueError is raised. This is equivalent to (but more efficient than):: self.index(self[j]) """ if j not in range(0, self.nknots): raise ValueError("j outside range(0, self.nknots)") return self.csum.searchsorted(j)
[docs] def mult(self, u): """Return the multiplicity of knot value u. Returns ------- int The multiplicity of the knot value u, or 0 if u is not in the KnotVector. """ try: i = self.index(u) return self.mul[i] except Exception: return 0
[docs] 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 __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]
[docs] 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)
[docs] def reverse(self): """Return the reverse knot vector. Examples -------- >>> 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])
[docs]def genKnotVector(nctrl, degree, blended=True, closed=False): """Compute a 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. By default, the returned knot vector will produce a blended, clamped, open curve. A blended curve uses overlapping basis functions spanning multiple intervals of the knot vector. This gives the highest smoothness. An unblended (or decomposed) curve is like a chain of separate curves, each formed on a sequence of ``degree+1`` control points. A clamped (open) curve starts and ends exactly at the first and last control points. Unclamped curves are deprecated and therefore genKnotVector only produces knot vectors for clamped curves. To achieve this, the knot vector should have multiplicity ``degree+1`` for its end values. Thus, for an open blended curve, the 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 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 between the knots, but can have crusts at the knots. There is an extra requirement in this case: ``nctrl`` should be a multiple of ``degree`` plus 1. For a closed curve, we currently only produce blended curves. The knots are equally spread over the interval, all having a multiplicity 1 for maximum continuity of the curve. Parameters ---------- nctrl: int The number of control points. This should be higher than the degree of the curve. degree: int The intended degree of the curve. Should be smaller than nctrl. Returns ------- :class:`KnotVector` A KnotVector to create a NurbsCurve with the specified degree and number of control points. Examples -------- >>> nctrl = 5 >>> for i in range(1, nctrl): print(genKnotVector(nctrl, i)) KnotVector(7): 0*2, 0.25*1, 0.5*1, 0.75*1, 1*2 KnotVector(8): 0*3, 0.333333*1, 0.666667*1, 1*3 KnotVector(9): 0*4, 0.5*1, 1*4 KnotVector(10): 0*5, 1*5 >>> print(genKnotVector(7,3)) KnotVector(11): 0*4, 0.25*1, 0.5*1, 0.75*1, 1*4 >>> print(genKnotVector(7,3,blended=False)) KnotVector(11): 0*4, 1*3, 2*4 >>> print(genKnotVector(3,2,closed=True)) KnotVector(6): 0*1, 0.2*1, 0.4*1, 0.6*1, 0.8*1, 1*1 """ if degree >= nctrl: raise ValueError( f"Degree ({degree}) should be smaller than nctrl ({nctrl})") 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)
[docs]@utils.pzf_register class NurbsCurve(Geometry4): """A NURBS curve. This class can represent a NURBS curve as well as its special case of a non-rational (or simple) B-spline curve. The difference sits only in the weights attributed to the control points: if they are constant, the interpolation functions are simple polynoms and the curve is a B-spline. If variable weights are attributed, the curve becomes a more general Non-Uniform Rational B-Spline or NURBS. A B-spline is defined by `nctrl` control points, a `degree` (>=1) and a sequence of `nknots = nctrl+degree+1` parametric values called `knots`. The knots divide the curve in parametric regions where the mathematical representation of the curve is constant. They form a non-descending sequence , but values may be repeated to model appropriate discontinuities. Given a set of control points and a knot vector, the degree of the curve is obviously `degree = nknots-nctrl-1`. In literature often the `order` of a NURBS is used. This is just `order = degree+1 = nknots-nctrl`. The order is also the minimum number of control points. To model a NURBS curve, we add a weight wi to each control point and use 4-dimensional coordinates (xi*wi, yi*wi, zi*wi, wi) for each point i. Internally, the NurbsCurve class uses 4-dim points, with default weights set to 1. Before returning, the coordinates are divided At the endInput and output are always 3-dimensional points. 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 | list of floats The nknots knot values to be used. If a list, the values should be in ascending order. Identical values have to be repeated to their multiplicity. 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, optional Determines whether the curve is closed. Default is False. The functionality of closed NurbsCurves is currently limited. blended: bool, optional 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, blended=True, closed=False, wrap=True, norm_urange=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: if closed: degree = nknots - nctrl - 1 if wrap: if degree % 2 != 0: raise ValueError("nknots - nctrl should be odd") degree = degree // 2 else: degree = nknots - nctrl - 1 if degree <= 0: raise ValueError( f"Length of knot vector ({nknots}) must be at least" f"number of control points ({nctrl}) plus 2") order = degree + 1 control = Coords4(control) if wts is not None: wts = np.asarray(wts).ravel() control.deNormalize(wts.reshape(wts.shape[-1], 1)) if closed and wrap: # We need to wrap nwrap control points nwrap = degree control = Coords4(np.concatenate([control, control[:nwrap]], axis=0)) nctrl = control.shape[0] if nctrl < order: raise ValueError(f"For a degree {degree} curve you need " f"at least {order} points, got only {nctrl}") if knots is None: knots = genKnotVector(nctrl, degree, blended=blended, closed=closed) nknots = knots.nknots if nknots != nctrl + order: raise ValueError( f"Length of knot vector ({nknots}) must be equal to number " f"of control points ({nctrl}) plus order ({order})") self.ctrl = control self.knotu = knots self.degree = degree self.closed = closed if norm_urange: umin, umax = self.urange() self.knotu.val -= umin self.knotu.val /= (umax-umin) @property def coords4(self): """The 4-dim coordinates""" return self.ctrl @property def coords(self): """The 3-dim coordinates""" return self.ctrl.toCoords() @property def knots(self): """Return the full list of knot values""" return self.knotu.values() @property def nctrl(self): """The number of control points""" return self.ctrl.shape[0] @property def nknots(self): """The number of knots""" return self.knotu.nknots @property def order(self): """The order of the Nurbs curve = nknots - nctrl = degree + 1""" return self.nknots - self.nctrl # @property # def degree(self): # """The degree of the Nurbs curve = order + 1""" # return self.order + 1
[docs] 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 # This is important for closed curves! return [self.knotu[p], self.knotu[-1-p]]
[docs] 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)
[docs] 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()
[docs] def isRational(self): """Return True if the NurbsCurve is rational. The curve is rational if the weights are not constant. The curve is polynomial if the weights are constant. Returns True for a rational curve, False for a polynomial curve. """ w = self.ctrl[:, 3] return not np.isclose(w[1:], w[0]).all()
[docs] 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()
[docs] def bbox(self): """Return the bounding box of the NURBS curve. """ return self.ctrl.toCoords().bbox()
def __str__(self): return (f"NURBS Curve, degree = {self.degree}, " f"nctrl = {len(self.ctrl)}, " f"nknots = {self.nknots}\n " f"closed: {self.closed}; " f"clamped: {self.isClamped()}; " f"uniform: {self.isUniform()}; " f"rational: {self.isRational()}\n " f"Control points:\n{self.ctrl}\n " f"{self.knotu}\n " f"urange: {self.urange()}")
[docs] def copy(self): """Return a (deep) copy of self. Changing the copy will not change the original. """ return NurbsCurve(control=self.ctrl.copy(), degree=self.degree, knots=self.knotu.copy(), closed=self.closed)
[docs] def pointsAt(self, u): """Return the points on the Nurbs curve at given parametric values. Parameters ---------- u: float :term:`array_like` (nu,) The parametric values at which a point is to be placed. Note that valid points are only obtained for parameter values in the :meth:`range`. Returns ------- Coords (nu, 3) The coordinates of nu points on the curve, at the specified parametric values. Examples -------- >>> N = NurbsCurve(control=Coords('0121'), degree=3) >>> N.pointsAt([0.0, 0.5, 1.0]) Coords([[0. , 0. , 0. ], [1. , 0.5, 0. ], [2. , 1. , 0. ]]) >>> N.pointsAt(0.5) Coords([[1. , 0.5, 0. ]]) """ ctrl = self.ctrl.astype(np.double) knots = self.knots.astype(np.double) u = np.atleast_1d(u).astype(np.double) pts = lib.nurbs.curvePoints(ctrl, knots, u) if np.isnan(pts).any(): print("We got a NaN") print(f"{ctrl=}") print(f"{knots=}") print(f"{u=}") 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 __call__(self, u): """Return the points on the Nurbs curve at given parametric values. This is like :meth:`pointsAt` but providing a scalar value returns a (3,) Coords array. Examples -------- >>> N = NurbsCurve(control=Coords('0121'), degree=3) >>> N([0.0, 0.5, 1.0]) Coords([[0. , 0. , 0. ], [1. , 0.5, 0. ], [2. , 1. , 0. ]]) >>> N(0.5) Coords([1. , 0.5, 0. ]) """ P = self.pointsAt(u) if np.array(u).ndim == 0: P = P[0] return P
[docs] def derivs(self, u, d=1): """Returns the points and derivatives up to d at parameter values u Parameters ---------- u: float :term:`array_like` | int If a float array (nu,), these are the parameter values at which to compute points and derivatives. If an int, specifies the number of parameter values (nu) at which to evaluate the points and derivatives of the curve. The points are equally spaced in parameter space. d: int The highest derivative to compute. Returns ------- float array (d+1,nu,3) The coordinates of the points and the derivates up to the order d at those points. Examples -------- >>> N = NurbsCurve(control=Coords('0121'), degree=3) >>> N.derivs([0.0, 0.5, 1.0], 2) Coords([[[ 0. , 0. , 0. ], [ 1. , 0.5, 0. ], [ 2. , 1. , 0. ]], <BLANKLINE> [[ 3. , 0. , 0. ], [ 1.5, 1.5, 0. ], [ 3. , 0. , 0. ]], <BLANKLINE> [[-6. , 6. , 0. ], [ 0. , 0. , 0. ], [ 6. , -6. , 0. ]]]) """ if at.isInt(u): 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.ctrl.astype(np.double) knots = self.knots.astype(np.double) u = np.atleast_1d(u).astype(np.double) d = int(d) try: pts = lib.nurbs.curveDerivs(ctrl, knots, u, d) if np.isnan(pts).any(): print("We got a NaN") print(pts) raise RuntimeError except Exception: 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
[docs] def frenet(self, u): """Compute Frenet vectors, curvature and torsion at parameter values u Parameters ---------- u: float :term:`array_like` | int If a float array (nu,), these are the parameter values at which to compute points and derivatives. If an int, specifies the number of parameter values (nu) at which to evaluate the points and derivatives of the curve. The points are equally spaced in parameter space. Returns ------- T: float array(nu,3) Normalized tangent vectors (nu,3) at nu points. N: float array(nu,3) Normalized normal vectors (nu,3) at nu points. B: float array(nu,3) Normalized binormal vectors (nu,3) at nu points. k: float array(nu,3) Curvature of the curve (nu) at nu points. t: float array(nu,3) Torsion of the curve (nu) at nu points. """ derivs = self.derivs(u, 3) return frenet(derivs[1], derivs[2], derivs[3])
[docs] def curvature(self, u, torsion=False): """Compute curvature and torsion at parameter values u This is like :meth:`frenet` but only returns the curvature and optionally the torsion. """ T, N, B, k, t = self.frenet(u) if torsion: return k, t else: return k
[docs] def knotPoints(self, multiple=False): """Returns the points on the curve at the knot values. If multiple is True, points are returned with their multiplicity. The default is to return the points just once. Examples -------- >>> N = NurbsCurve(Coords('012141'), degree=3) >>> print(N.knotu) KnotVector(10): 0*4, 0.333333*1, 0.666667*1, 1*4 >>> N.knotPoints() Coords([[0. , 0. , 0. ], [1.1667, 0.75 , 0. ], [1.8333, 0.75 , 0. ], [3. , 0. , 0. ]]) """ if self.closed: p = self.degree val = np.unique(self.knots[p:-1-p]) else: if multiple: val = self.knots else: val = self.knotu.val return self.pointsAt(val)
[docs] def insertKnots(self, u): """Insert knots in the Nurbs curve. Parameters ---------- u: float :term:`array_like` (nu,) A list of parameter values to be inserted into the curve's knot vector. The control points are adapted to keep the curve unchanged. Returns ------- NurbsCurve A new 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.ctrl.astype(np.double) knots = self.knots.astype(np.double) u = np.asarray(u).astype(np.double) umin, umax = self.urange() if (u < umin).any() or (u > umax).any(): raise ValueError( f"All u values should be in the range {self.urange()}") newP, newU = lib.nurbs.curveKnotRefine(ctrl, knots, u) return NurbsCurve(newP, degree=self.degree, knots=newU, closed=self.closed)
[docs] def requireKnots(self, val, mul): """Insert knots until the required multiplicity is reached. Inserts knot values only if they are currently not there or their multiplicity is lower than the required one. Parameters ---------- val: float :term:`array_like` (nval,) The list of knot values required in the knot vector. mul: int :term:`array_like` (nval,) The list of multiplicities required for the knot values. Returns ------- NurbsCurve A curve equivalent with the original but where the knot vector is guaranteed to contain the values in `val` with at least the corresponding multiplicity in `mul`. 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
[docs] def removeKnot(self, u, m, tol=1.e-5): """Remove a knot from the knot vector of the Nurbs curve. Parameters ---------- u: float The knot value to remove. m: int How many times to remove the knot `u`. If negative, remove maximally. tol: float Acceptable error (distance between old and new curve). Returns ------- NurbsCurve A Nurbs curve equivalent (to the specified tolerance) with the original but with a knot vector where the value `u` has been removed `m` times, if possible, or else as many times as possible. The control points are adapted accordingly. Returns self if no value was removed. Notes ----- removeKnot currently only works on open curves """ 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.ctrl.astype(np.double) Uv = self.knotu.val.astype(np.double) Um = self.knotu.mul.astype(at.Int) t, newP, newU = lib.nurbs.curveKnotRemove(P, Uv, Um, i, m, tol) if t > 0: print(f"Removed the knot value {u} {t} times") return NurbsCurve(newP, degree=self.degree, knots=newU, closed=self.closed) else: print(f"Can not remove the knot value {u}") return self
[docs] def removeAllKnots(self, tol=1.e-5): """Remove all removable knots. Parameters ---------- tol: float Acceptable error (distance between old and new curve). Returns ------- NurbsCurve An equivalent (if tol is small) NurbsCurve with all extraneous knots removed. Notes ----- :meth:`removeAllKnots` and :meth:`blend` are aliases. """ N = self print(N) while True: print(N) for u in N.knotu.val: print(f"Removing {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
[docs] def decompose(self): """Decompose a curve in subsequent Bezier curves. Returns ------- NurbsCurve An equivalent unblended NurbsCurve. See also -------- toCurve: convert the NurbsCurve to a BezierSpline or PolyLine Notes ----- :meth:`decompose` and :meth:`unblend` are aliases. """ # sanitize arguments for library call ctrl = self.ctrl knots = self.knots.astype(np.double) X = lib.nurbs.curveDecompose(ctrl, knots) return NurbsCurve(X, degree=self.degree, blended=False)
# For compatibility unblend = decompose
[docs] def subCurve(self, u1, u2): """Extract the subcurve between parameter values u1 and u2 Parameters ---------- u1: float Parametric value of the start of the subcurve to extract. The value should be in :meth:`urange`. u2: float Parametric value of the end of the subcurve to extract. The value should be in :meth:`urange` and > `u1`. Returns ------- NurbsCurve A NurbsCurve containing only the part between u1 and u2 of the original. """ 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)
[docs] 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 ------- NurbsCurve An equivalent curve with clamped knot vector, or self if the curve was already clamped. Notes ----- 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)
[docs] def unclamp(self): """Unclamp the knot vector of the curve. Warning ------- The use of unclamped knot vectors is deprecated. Returns ------- NurbsCurve An equivalent curve with unclamped knot vector, or self if the curve was already unclamped. """ if self.isClamped(): from pyformex.lib.nurbs_e import curveUnclamp P, U = curveUnclamp(self.ctrl, self.knots) return NurbsCurve(control=P, degree=self.degree, knots=U, closed=self.closed) else: return self
[docs] 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 ------- BezierSpline | PolyLine A BezierSpline (or PolyLine if degree is 1) that is equivalent with the NurbsCurve. See also -------- unblend: decompose both rational and nonrational NurbsCurves """ if self.isRational(): raise ValueError("Can not convert a rational NURBS to BezierSpline") ctrl = self.ctrl knots = self.knots X = lib.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)
[docs] 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)
[docs] def elevateDegree(self, t=1): """Elevate the degree of the Nurbs curve. Parameters ---------- t: int How much to elevate the degree of the curve Returns ------- NurbsCurve A NurbsCurve equivalent with the original but of a higher degree. """ if self.closed: raise ValueError("elevateDegree currently does not work on " "closed curves") P = self.ctrl.astype(np.double) U = self.knotus.astype(np.double) newP, newU = lib.nurbs.curveDegreeElevate(P, U, t) return NurbsCurve(newP, degree=self.degree+t, knots=newU, closed=self.closed)
[docs] def reduceDegree(self, t=1, tol=np.inf): """Reduce the degree of the Nurbs curve. Parameters ---------- t: int How much to reduce the degree (max. = degree-1) Returns ------- NurbsCurve A NurbsCurve approximating the original but of a lower degree. """ if self.closed: raise ValueError("reduceDegree currently does not work on " "closed curves") if t >= self.degree: raise ValueError( f"This curve can maximally be reduced {self.degree-1} times") N = self while t > 0: from pyformex.lib import nurbs_e # newP, newU, maxerr = lib.nurbs.curveDegreeReduce(N.coords, N.knots) newP, newU, maxerr = nurbs_e.curveDegreeReduce(N.coords, N.knots, tol=tol) print(f"MAXERR = {maxerr}") N = NurbsCurve(newP, degree=self.degree-1, knots=newU, closed=self.closed) t -= 1 return N
# TODO: This should be implemented in C for efficiency
[docs] def projectPoint(self, P, *, u0=None, nseed=20, eps1=1.e-5, eps2=1.e-5, maxit=50): """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: :term:`coords_like` (3,) A set of npts points in space. u0: float, optional Start value for the parameter of the projected point. Providing a value close to the correct foot point will speed up the operation. If not provided, a number of values is tried and the one giving the closest point to P is chosen. See ``nseed``. nseed: int Number of intervals to divide the parameter range into for guessing a suitable initial value ``u0``. Only used if ``u0`` is not provided. Returns ------- u: float Parameter value of the base point X of the projection of P on the NurbsCurve. P: Coords (3,) The coordinates of the base point of the projection of P on the NurbsCurve. d: float The distance of the point P from the NurbsCurve Notes ----- Based on section 6.1 of The NurbsBook. """ P = at.checkArray(P, (3,), 'f', 'i') umin, umax = self.knotu.val[[0, -1]] if u0 is None: # Determine start value from best match of nseed+1 points u = at.uniformParamValues(nseed+1, umin, umax) pts = self.pointsAt(u) d = pts.distanceFromPoint(P) i = d.argmin() u0, P0, d0 = u[i], pts[i], d[i] del pts del d else: P0 = self(u0) d0 = at.length(P-P0) if d0 == 0.: # Trivial case: the point is on the curve return u0, P0, d0 # 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 if C1xC1 == 0. or CPxCP == 0.: chk2 = False else: 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] if (chk1 or chk2) and chk4: # Converged! break else: # Prepare for next it ui = uj if i == maxit: print(f"Convergence not reached after {maxit} iterations") return u0, P0, at.length(P-P0)
[docs] def projectPoints(self, P, **kargs): """Project multiple points""" return Coords.concatenate([self.projectPoint(Pi, **kargs)[1] for Pi in P])
[docs] def distancePoints(self, P, **kargs): """Compute the distance of points P to the NurbsCurve""" return Coords.concatenate([self.projectPoint(Pi, **kargs)[2] for Pi in P])
# return at.length(self.projectPoints(self, P, **kargs) - P)
[docs] 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. """ if ndiv is None: ndiv = self.N_approx umin, umax = self.urange() u = at.uniformParamValues(ndiv, umin, umax) PL = curve.PolyLine(self.pointsAt(u)) if nseg is not None: u = PL.atLength(nseg) PL = curve.PolyLine(PL.pointsAt(u)) return PL
[docs] 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.ctrl[::-1], knots=self.knotu.reverse(), degree=self.degree, closed=self.closed)
[docs] def actor(self, **kargs): """Graphical representation""" from pyformex.opengl.actors import Actor G = self.approx(ndiv=100).toMesh() G.attrib(**self.attrib) return Actor(G, **kargs)
def pzf_dict(self): return { 'control': self.ctrl, 'knots': self.knotu.values(), f'degree:i__{self.degree}': None, f'closed:b__{self.closed}': None, }
####################################################### ## NURBS Surface ##
[docs]@utils.pzf_register 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( f"Length of knot vector ({len(knots)}) must be at" f"least number of control points ({nctrl}) plus 2") # make degree changeable degree = list(degree) degree[d] = deg order = deg+1 if nctrl < order: raise ValueError( f"Number of control points ({nctrl}) must not be " f"smaller than order ({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( f"Length of knot vector ({nknots}) must be equal to " f"number of control points ({nctrl}) plus order ({order})") if d == 0: self.knotu = kn else: self.knotv = kn self.ctrl = control self.degree = degree self.closed = closed def order(self): return (self.knotu.shape[0]-self.ctrl.shape[1], self.knotv.shape[0]-self.ctrl.shape[0])
[docs] 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]]
[docs] 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]]
[docs] def bbox(self): """Return the bounding box of the NURBS surface. """ return self.ctrl.toCoords().bbox()
[docs] 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.ctrl.astype(np.double) U = self.knotv.astype(np.double) V = self.knotu.astype(np.double) u = np.asarray(u).astype(np.double) try: pts = lib.nurbs.surfacePoints(ctrl, U, V, u) if np.isnan(pts).any(): print("We got a NaN") raise RuntimeError except Exception: 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
[docs] 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.ctrl.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 = lib.nurbs.surfaceDerivs(ctrl, U, V, u, mu, mv) if np.isnan(pts).any(): print("We got a NaN") raise RuntimeError except Exception: 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
[docs] def approx(self, ndiv=None, **kargs): """Return a Quad4 Mesh approximation of the Nurbs surface Parameters: - `ndiv`: number of divisions of the parametric space. """ 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')
[docs] 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 pzf_dict(self): return { 'control': self.ctrl, 'knotu': self.knotu.values(), 'knotv': self.knotv.values(), 'dict:r': { 'degree': self.degree, 'closed': self.closed, } }
################################################################ def besselTangents(Q, u): q = Q[1:] - Q[:-1] du = u[1:] - u[:-1] d = q / du[:, np.newaxis] alfa = du[:-1] / (du[:-1] + du[1:]) alfa = alfa[:, np.newaxis] D = np.empty(Q.shape) D[1:-1] = (1-alfa) * d[:-1] + alfa * d[1:] D[0] = 2*d[0] - D[1] D[-1] = 2*d[-1] - D[-2] return D
[docs]def akimTangents(Q, corner=0.5, normalize=True): """Estimate tangents to a curve passing through given points. Parameters ---------- Q: :term:`coords_like` (npts, 3) The points through which the curve should pass. corner: float A value in the range 0.0..1.0. A value 1.0 will set the tangent at a corner to that after the corner. A value sets it to that before. A value 0.5 gives an intermediate value. normalize: bool If True (default), normalized vectors are returned. If False, the vectors are not normalized and are good approximations for the derivative vectors of a curve interpolating/approximating the points. Returns ------- array (npts, 3) The normalized tangent vectors at the points Q to a curve that interpolates those points. Notes ----- Based on The NURBS Book (9.29) and (9.31). """ n = len(Q)-1 q = np.zeros((n+1, 3)) # k = -1..n+2 where Q is 0..n q[1:] = Q[1:] - Q[:-1] q[0] = 2*q[1] - q[2] qm1 = 2*q[0] - q[1] qn1 = 2*q[n] - q[n-1] qn2 = 2*qn1 - q[n] qq = np.zeros((n+4, 3)) qq[1:-2] = q qq[0] = qm1 qq[n+2] = qn1 qq[n+3] = qn2 cq = np.cross(qq[:-1], qq[1:]) lcq = at.length(cq) denom = lcq[:-2] + lcq[2:] w = at.where_1d(denom!=0) alfa = np.full(lcq[:-2].shape, corner) alfa[w] = lcq[:-2][w] / denom[w] alfa = alfa[:, np.newaxis] V = (1-alfa) * qq[1:-2] + alfa * qq[2:-1] return at.normalize(V) if normalize else V
def _compute_alpha(P30, T03): """Nurbsbook (9.50)""" a = 16 - T03 @ T03 b = 12 * P30 @ T03 c = -36 * P30 @ P30 r1, r2, k = at.quadraticEquation(a, b, c) assert (k!=2) return r2 # TODO: we could add a T0/T1 option
[docs]def cubicInterpolate(Q, T=None, return_param=False): """Create a C1 cubic interpolation curve Parameters ---------- Q: :term:`coords_like` (npts, 3) The points through which the curve should pass. T: :term:`coords_like` (npts, 3), optional The normalized tangent vectors at the points Q to a curve that interpolates those points. If not provided, they are computed with :func:`akimTangents`. return_param: bool If True, also returns also parameter values where the given points occur. Returns ------- NurbsCurve: A cubic NurbsCurve interpolating the points Q and having tangents T at those points. The curve is guaranteed to have C1 continuity (including in the speed of the curve) and has a fairly uniform parametrization. u: float array (npts, ) Only returned if return_param=True: the parametric values where the input points are found on the NurbsCurve. Thus, N.pointsAt(u) produces Q. Notes ----- Based on The NURBS Book 9.3.4. """ Q = at.checkArray(Q, (-1, 3), 'f') d = curve.PolyLine(Q).lengths() if (d==0.0).any(): raise ValueError("Double points in the data set are not allowed") if T is None: T = akimTangents(Q) else: T = at.checkArray(T, Q.shape, 'f') T = at.normalize(T) nQ = Q.shape[0] P = np.zeros((2*nQ, 3), dtype=Q.dtype) u = np.empty((nQ,), dtype=Q.dtype) u[0] = 0 P[0] = Q[0] P[-1] = Q[-1] for i in range(1, nQ): alpha = _compute_alpha(Q[i]-Q[i-1], T[i-1]+T[i]) P1 = Q[i-1] + alpha/3 * T[i-1] P2 = Q[i] - alpha/3 * T[i] P[2*i-1:2*i+1] = P1, P2 u[i] = u[i-1] + 3*at.length(P1 - Q[i-1]) u /= u[-1] U = np.empty((2*nQ+4,), dtype=Q.dtype) U[:4] = 0 U[-4:] = 1 U[4:-4:2] = u[1:-1] U[5:-4:2] = u[1:-1] N = NurbsCurve(P, knots=U, degree=3) if return_param: return N, u else: return N
[docs]def quadraticInterpolate(Q, T=None, corners=False, reduce=True, return_param=False): """Create a G1/C1 quadratic interpolation curve Parameters ---------- Q: :term:`coords_like` (npts, 3) The points through which the curve should pass. T: :term:`coords_like` (npts, 3), optional The normalized tangent vectors at the points Q to a curve that interpolates those points. If not provided, they are computed with :func:`akimTangents`, with a corner value of 1.0 if corners is True, and 0.5 if not. corners: bool, optional If True, obvious corners will be retained in the result. The default (False) will round corners. reduce: bool, optional If True (default), the curve is reduced by removing the internal oncurve control points. If set False, the oncurve points are retained. Note that there may be more oncurve points than the originally specified set. return_param: bool If True, also returns also parameter values where the given points occur. Returns ------- NurbsCurve: A quadratic NurbsCurve interpolating the given points and having tangents equal to the provided or computed ones. If not reduced, and corners is False, the curve has G1 (geometric) continuity and a fairly uniform parametrization. If reduced, it has C1 continuity (thus including the curve speed) and a smaller footprint, at the price of a less uniform parametrization. If corners is True, it has C0 continuity. Notes ----- Based on The NURBS Book 9.3.2 """ Q = at.checkArray(Q, (-1, 3), 'f', 'i') d = curve.PolyLine(Q).lengths() if (d==0.0).any(): raise ValueError("Double points in the data set are not allowed") if T is None: akim = 1.0 if corners else 0.5 T = akimTangents(Q, akim) else: T = at.checkArray(T, Q.shape, 'f') Q0, T0 = Q[:-1], T[:-1] Q1, T1 = Q[1:], T[1:] t0, t1 = gt.intersectLineWithLine( Q0, T0, Q1, T1, mode='pair', times=True) R = 0.5 * (Q0 + t0[:, np.newaxis]*T0 + Q1 + t1[:, np.newaxis]*T1) nc = len(Q) newQ = [] newR = [] for i in range(nc-1): newQ.append(Q[i]) if ((t0[i] > 0.0 and t1[i] < 0.0) # Normal segment or corners and (t0[i] == 0.0 or t1[i] == 0.0)): # Corner newR.append(R[i]) else: QQ = at.normalize(Q[i+1] - Q[i]) # chord length if np.allclose(T[i], T[i+1]) and np.allclose(T[i], QQ): R[i] = 0.5 * (Q[i] + Q[i+1]) # colinear 9.40 newR.append(R[i]) else: QQ = Q[i+1] - Q[i] if np.allclose(T[i], T[i+1]) or np.allclose(T[i], -T[i+1]): # parallel or 180 turn gamma = (0.5 * at.length(QQ), ) * 2 # 9.43 else: cos0 = at.projection(T[i], QQ) cos1 = at.projection(T[i+1], QQ) lQQ = at.length(QQ) alfa = 2/3 c0 = (1-alfa) * cos0 + alfa * cos1 c1 = alfa * cos0 + (1-alfa) * cos1 gamma = (0.20 * lQQ / c0, 0.20 * lQQ / c1) # 9.44 if np.isnan(gamma[0]) or np.isnan(gamma[1]): gamma = (0.5 * at.length((Q[i+1] - Q[i])), ) * 2 # 9.43 Rk = Q[i] + gamma[0] * T[i] # 9.41 Rk1 = Q[i+1] - gamma[1] * T[i+1] Qk = (gamma[0] * Rk1 + gamma[1] * Rk) / (gamma[0]+gamma[1]) # 9.42 newR.append(Rk) newQ.append(Qk) newR.append(Rk1) newQ.append(Q[-1]) nc = len(newQ) # parametric values of Q if reduce: # remove the internal oncurve control points Q = Coords.concatenate(newQ) R = Coords.concatenate(newR) u = np.empty(nc) u[:2] = 0, 1 l1 = at.length(R - Q[:-1]) l2 = at.length(Q[1:] - R) for k in range(2, nc): u[k] = u[k-1] + (u[k-1] - u[k-2]) * l1[k-1] / l2[k-2] u /= u[-1] U = np.concatenate([[0, 0], u, [1, 1]]) Q = Coords.concatenate([Q[:1], R, Q[-1:]]) else: # keep oncurve and intermediate points Q = at.interleave(Q, R) u = np.arange(nc) / (nc-1) U = np.empty(2*nc+2) U[0], U[-1] = 0, 1 U[1:-1:2] = u U[2:-1:2] = u N = NurbsCurve(Q, knots=U, degree=2) if return_param: return N, u else: return N
[docs]def globalInterpolationParameters(Q, exp=1.0): """Compute parameters for a global interpolation curve The global interpolation algorithm computes the control points that produce a NurbsCurve with given points occurring at predefined parameter values. The curve shape depends on the choosen values. This function provides a way to set values that work well under mosr conditions/ Parameters ---------- Q: :term:`coords_like` (npts, 3) An ordered set of points through which the curve should pass. Two consecutive points should not coincide. exp: float The exponent to be used in the interpolation algorithm. See Notes. Returns ------- u: float array (npts, ) Only returned if return_param=True: the parametric values where the input points are found on the NurbsCurve. Thus, N.pointsAt(u) produces Q. Notes ----- The algorithm to set these values uses a variable exponent. Different values produce (slighly) different curves. The smaller the value, the more the two spans get curved. Values above 1 will almost straighten the end spans, but intermediate spans become more curved. Typical values are: 0.0: equally spaced (not recommended) 0.5: centripetal (recommended when data set take sharp turns) 0.7: our prefered default 1.0: chord length (widely used) """ # chord length d = curve.PolyLine(Q).lengths() if (d==0.0).any(): utils.warn("warn_nurbs_gic") w = np.where(d!=0)[0] Q = np.concatenate([Q[w], Q[-1:]], axis=0) d = curve.PolyLine(Q).lengths() if (d==0.0).any(): raise ValueError("Double points in the data set are not allowed") # apply exponent if exp != 1.: d = d ** exp d = d.cumsum() d /= d[-1] return np.concatenate([[0.], d])
# def globalInterpolationEndConditions(Q, t0, t1, alfa): # if t0 is not None: # t0 = at.checkArray(t0, (3,), 'f', 'i') # if t1 is not None:-1 # t1 = at.checkArray(t1, (3,), 'f', 'i') # if alfa is None: # alfa = curve.PolyLine(Q).length() # t0 = at.normalize(t0) # t1 = at.normalize(t1) # if t0 is not None: # t0 = alfa*t0 # if t1 is not None: # t1 = alfa*t1 # return t0, t1
[docs]def cubicSpline(Q, D0, D1, *, u=None, exp=0.7, return_param=False): """Cubic spline interpolation. Computes a traditional C2 cubic spline through the points Q with given end tangents. Parameters ---------- Q: :term:`coords_like` (npts, 3) An ordered set of points through which the curve should pass. Two consecutive points should not coincide. D0: :term:`coords_like` (3,) The derivative of the curve at the start point Q[0]. The length of the vector is significant. D1: :term:`coords_like` (3,) The derivative of the curve at the end point Q[-1]. The length of the vector is significant. u: float array (npts,), optional The parameter values where the points Q should be obtained on the curve. If not provided, a default set of parameter values is computed from :func:`globalInterpolationParameters` (Q, exp=exp)``. exp: float, optional The exponent to be used in computing the parameter values if no `u` was provided. The default value 0.7 will work well in most cases. See :func:`globalInterpolationParameters`. return_param: bool If True, also returns also parameter values where the given points occur. Returns ------- N: NurbsCurve A NurbsCurve of the third degree that passes through the given point set Q and has tangents D0 and D1 at its ends. The number of control points of the curve is npts + 2. The parametric values of the input points Q can be got from ``N.knots[3:-3]``. u: float array (npts, ) Only returned if return_param=True: the parametric values where the input points are found on the NurbsCurve. Thus, N.pointsAt(u) produces Q. See Also -------- globalInterpolationCurve: global interpolation curve of any degree """ Q = at.checkArray(Q, (-1, 3), 'f') # set the parameter values if u is None: u = globalInterpolationParameters(Q, exp=exp) else: npts = Q.shape[0] u = at.checkArray(u, (npts,), 'f', 'i') # end conditions D0 = at.checkArray(D0, (3,), 'f', 'i') D1 = at.checkArray(D1, (3,), 'f', 'i') # Knots U = np.concatenate([[0]*3, u, [1]*3]) # Control points P = lib.nurbs.cubicSplineInterpolation(Q, D0, D1, U) N = NurbsCurve(P, knots=U, degree=3) if return_param: return N, u else: return N
[docs]def globalInterpolationCurve(Q, degree=3, *, D=None, D0=None, D1=None, u=None, exp=0.7, return_param=False): """Create a global interpolation NurbsCurve. An interpolation curve is a curve passing through all the given points. Parameters ---------- Q: :term:`coords_like` (npts, 3) An ordered set of points through which the curve should pass. Two consecutive points should not coincide. degree: int The degree of the resulting curve. Usually 2 or 3 is used. For degree 1, the result is a PolyLine through the points Q. For degrees higher than 4, it is better to create a degree 3 curve and then increase the degree. D: :term:`coords_like` (npts, 3) The derivatives of the curve at the points Q. The longer the derivative vectors, the further the curve will follow the tangent. If not provided, the interpolation is done without imposing tangent directions at the intermediate points. D0: :term:`coords_like` (3,) The derivative of the curve at the start point. Can only be used if D is not provided, and derivatives at one or both ends need to be imposed. The length of the vector is significant. D1: :term:`coords_like` (3,) The derivative of the curve at the end point. Can only be used if D is not provided, and derivatives at one or both ends need to be imposed. The length of the vector is significant. u: float :term:`array_like` (npts,), optional The parameter values where the points Q should be obtained on the curve. If not provided, a default set of parameter values is computed from ``globalInterpolationParameters(Q, exp=exp)``. exp: float, optional The exponent to be used in computing the parameter values if no `u` was provided. The default value 0.7 works well in most cases. See :func:`globalInterpolationParameters`. return_param: bool If True, also returns also parameter values where the given points occur. Returns ------- N: NurbsCurve A NurbsCurve of the specified degree that passes through the given point set. The number of control points is equal to the number of input points plus one for every end tangent set. u: float array (npts, ) Only returned if return_param=True: the parametric values where the input points are found on the NurbsCurve. Thus, N.pointsAt(u) produces Q. See Also -------- cubicSpline: returns the classical C2 spline interpolate """ p = degree Q = at.checkArray(Q, (-1, 3), 'f') npts = Q.shape[0] if D is not None: D = at.checkArray(D, Q.shape, 'f') D0 = D1 = None # set the parameter values if u is None: u = globalInterpolationParameters(Q, exp=exp) else: u = at.checkArray(u, (npts,), 'f', 'i') # compute system matrix if D is not None: print(f"Global, Q {Q.shape}, D {D.shape}, u {u.shape}, p {p}") from pyformex.lib import nurbs_e U, A, Q = nurbs_e.curveGlobalInterpolationMatrix2(Q, D, u, p) print(f"U {U.shape}, A {A.shape}, Q {Q.shape}") else: print(f"Global, Q {Q.shape}, D0 {D0}, D1 {D1}") # set the end conditions it0 = D0 is not None it1 = D1 is not None if it0: D0 = at.checkArray(D0, (3,), 'f', 'i') if it1: D1 = at.checkArray(D1, (3,), 'f', 'i') U, A = lib.nurbs.curveGlobalInterpolationMatrix(u, degree, it0, it1) # set right hand side (TODO: move this inside ^^^) if it0: D0 = Coords(D0).reshape(-1, 3) * U[p+1] / p * 4 Q = Coords.concatenate([Q[0:1], D0, Q[1:]]) if it1: D1 = Coords(D1).reshape(-1, 3) * (1-U[-(p+2)]) / p * 4 Q = Coords.concatenate([Q[:-1], D1, Q[-1:]]) P = np.linalg.solve(A, Q) N = NurbsCurve(P, knots=U, degree=degree) if return_param: return N, u else: return N
[docs]def lsqCurve(Q, p, nctrl, Wq=None, D=None, I=None, Wd=None, return_param=False): """Weighed and constrained least squares curve fit Creates a NurbsCurve of a given degree and with given number of control points, approximating a sequence of points. Points can be attributed weights to force the curve closer to specific points. Tangents can be specified at any point, and can be weighed to have better approximation at specific points. Specifying negative weights for specific points or tangents turns them into constraints to be met accurately. Parameters ---------- Q: :term:`coords_like` (npts, 3) The points through which to fit a NurbsCurve p: int The degree of the target curve. A typical value is 3. nctrl: int The number of control points in the target curve. It should be higher than the degree ``p``, but smaller smaller than the number of points ``npts``. The higher the number, the closer the curve can come to all points, at the cost of higher oscillations though. A value of 2 with ``p=1`` fits a straight line throught the data. Wq: :term:`array_like` float (npts,), optional The weigths of the ``npts`` points, where the value is non-negative. Where negative, a point constraint is added to force the curve through the point. If not specified, all weights are set equal to 1. D: :term:`coords_like` (nder, 3), optional The derivatives at ``nder`` points. Note that the length of the vectors is significant. See Notes. I: :term:`array_like` int (nder,), optional The index of the points in Q for which the derivatives D are specified. Required if D is provided. Wd: :term:`array_like` float (nder,), optional The weight of the ``nder`` derivatives, where the value is non-negative. Where negative, a constraint is added to force the curve to have that derivative. If not specified, all weights are set equal to 1. return_param: bool If True, also returns also parameter values where the given points occur. Returns ------- N: NurbsCurve A NurbsCurve of the specified degree and with the specified number of control points, passing as close as possible to the given (weighed) points and obeying any specified constraints. u: float array (npts, ) Only returned if return_param=True: the parametric values where the input points are found on the NurbsCurve. Thus, N.pointsAt(u) produces points in the neighborhood of Q. Notes ----- If derivatives or constraints are provided, the number of control points is further restricted. Let ``nc`` be the number of constraints points and derivatives and ``nu`` be the number of unconstrained points and derivatives, then ``nctrl`` should satisfy: ``nc-2 < nctrl < nu-nc+2``. The length of the derivatives has a significant influence on the resulting curve. The recommended length is the polygonal length of the point set. This can be achieved with:: import pyformex.arraytools as at from pyformex.core import PolyLine D = at.normalize(D) * PolyLine(Q).length() This is algorithm A9.6 p.417 of the NurbsBook. This function requires scipy. """ utils.Module.require('scipy') from pyformex.lib.nurbs_e import find_span, basis_funs, basis_derivs from scipy import linalg if p < 1 or p >= nctrl: raise ValueError(f"Degree p ({p}) should be >0 and <nctrl ({nctrl})") Q = at.checkArray(Q, ndim=2, kind='f') npts = Q.shape[0] if nctrl >= npts: raise ValueError(f"ctrl ({nctrl} should be <npts ({npts})") nd = Q.shape[1] if Wq is None: Wq = np.ones((npts,), dtype=at.Float) else: Wq = at.checkArray(Wq, (npts,), 'f') if D is None: nder = 0 else: D = at.checkArray(D, (-1, nd), 'f') nder = D.shape[0] if nder > 0: I = at.checkArray(I, (nder,), 'i') if Wd is None: Wd = np.ones((nder,), dtype=at.Float) else: Wd = at.checkArray(Wd, (nder,), 'f') ru = -1 + (Wq >= 0.0).sum() rc = -1 + (Wq < 0.0).sum() if Wd is None: su = sc = -1 else: su = -1 + (Wd >= 0.0).sum() sc = -1 + (Wd < 0.0).sum() mu = ru+su+1 mc = rc+sc+1 if nctrl < mc-1 or nctrl > mu-mc+2: print(f"nu={mu+1}, nc={mc+1}, nc-2={mc-1}, nu-nc+2={mu+1-(mc+1)+2}") raise ValueError(f"Invalid number of control points ({nctrl} for " f"constraints: need {mc-1} <= nctrl <= {mu-mc+2}") # Parameters u = globalInterpolationParameters(Q) # Knots nknots = nctrl + p + 1 U = np.zeros((nknots,)) d = npts / (nctrl-p) # NurbsBook 9.68 for j in range(1, nctrl-p): jd = j * d i = int(jd) alfa = jd - i U[p+j] = (1-alfa)*u[i-1] + alfa*u[i] U[-(p+1):] = 1.0 # Set up arrays W = np.zeros((mu+1,)) S = np.zeros((mu+1, nd)) N = np.zeros((mu+1, nctrl)) T = np.zeros((mc+1, nd)) A = np.zeros((mc+1,)) M = np.zeros((mc+1, nctrl)) j = 0 # current index into I mu2 = 0 mc2 = 0 # counters up to mu and mc for i in range(npts): span = find_span(U, u[i], p, nctrl-1) dflag = j < nder and i == I[j] if dflag: funs, derivs = basis_derivs(U, u[i], p, span, 1) else: funs = basis_funs(U, u[i], p, span) if Wq[i] >= 0.0: # Unconstrained point W[mu2] = Wq[i] S[mu2] = Wq[i] * Q[i] N[mu2, span-p:span+1] = funs mu2 += 1 else: # Constrained point T[mc2] = Q[i] M[mc2, span-p:span+1] = funs mc2 += 1 if dflag: # Derivatives at this point if Wd[j] >= 0.0: # Unconstrained derivative W[mu2] = Wd[j] S[mu2] = Wd[j] * D[j] N[mu2, span-p:span+1] = derivs mu2 += 1 else: # Constrained derivative T[mc2] = D[j] M[mc2, span-p:span+1] = derivs mc2 += 1 j += 1 # Compute matrices NTW = N.T * W NTWN = NTW @ N NTWS = N.T @ S # S already contains W !! del NTW lu, piv = linalg.lu_factor(NTWN) if mc < 0: # No constraints: direct solution P = linalg.lu_solve((lu, piv), NTWS) else: # inverse of NTWN NTWNI = linalg.lu_solve((lu, piv), np.identity(NTWN.shape[0])) # Solve for Lagrange multipliers: Nurbsbook eq 9.75 MI = M @ NTWNI MIMT = MI @ M.T MINWST = MI @ NTWS - T A = linalg.solve(MIMT, MINWST) # Solve for control points: Nurbsbook eq 9.74 P = NTWNI @ (NTWS - M.T @ A) N = NurbsCurve(P, knots=U, degree=p) if return_param: return N, u else: return N
[docs]def optLsqCurve(Q, p, dtol, nmin=None, nmax=None): """Compute an optimal lsqCurve Finds the lsqCurve with minimum number of control points that passes not further than dtol from any of the given points. Parameters ---------- Q, p: see lsqCurve dtol: float Maximum distance from the curve allowed for any of the points Q. Note that specifying a very small value may lead to high oscillations in the curve. nmin: int Minimum number of control points in the curve. If not specified, it is set to p+1. nmax: int Minimum number of control points in the curve. If not specified, it is set to npts-1. Returns ------- N: NurbsCurve The NurbsCurve obtained with lsqCurve with the minimum number of control points guaranteeing that no point is further than dtol from the curve. d: float The maximum distance of any point Q from the curve N. """ def _one_step(Q, p, n): N, u = lsqCurve(Q, p, n, return_param=True) return N, at.length(Q-N(u)).max() npts = Q.shape[0] nmax = npts-1 if nmax is None else min(nmax, npts-1) nmin = p+1 if nmin is None else max(nmin, p+1) N, dmin = _one_step(Q, p, nmin) if dmin < dtol: return N, dmin Nmax, dmax = _one_step(Q, p, nmax) if dmax > dtol: raise ValueError(f"No solution with nmax={nmax} and dtol={dtol}; " f"smallest dtol is {dmax}") col = 4 it, maxit = 0, 100 # avoid endless loop while nmin < nmax-1 and it < maxit: nr = (nmin*(dtol-dmax) + nmax*(dmin-dtol)) / (dmin-dmax) n = int(round(nr)) N, d = _one_step(Q, p, n) if d < dtol: if n == nmax: break Nmax, nmax, dmax = N, n, d elif d > dtol: if n == nmin: break nmin, dmin = n, d else: break col += 1 it += 1 return Nmax, dmax
[docs]def NurbsCircle(C=[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 ---------- C: float (3,) The center of the circle r: float The radius of the circle X: float (3,) A unit vector in the plane of the circle Y: float (3,) A unit vector in the plane of the circle and perpendicular to X ths: float Start angle (in degrees) of the arc, measured from the X axis, counterclockwise in X-Y plane the: float End angle (in degrees) of the arc, measured from the X axis, counterclockwise in X-Y plane Returns ------- NurbsCurve A NurbsCurve that is a perfect circle or arc in the plane of the XY-axes. Notes ----- NurbsBook algorithm A7.1 """ 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 C, X, Y = (at.checkArray(x, (3,), 'f', 'i') for x in (C, X, Y)) dths = ths*at.DEG dtheta = theta*at.DEG/narcs w1 = np.cos(dtheta/2.) # base angle # Initialize start values P0 = C + 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 = C + 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 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)
[docs]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
[docs]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([ (lib.nurbs.allBernstein(n, ui).reshape(1, -1, 1) * P).sum(axis=1) for ui in u], axis=0)
[docs]def frenet(d1, d2, d3=None): """Compute Frenet vectors, curvature and torsion. This function computes Frenet vectors, curvature and torsion from the provided first, second, and optional third derivatives of curve. The derivatives can be obtained from :func:`NurbsCurve.deriv`. Curvature is computed as `abs| d1 x d2 | / |d1|**3` Parameters ---------- d1: float :term:`array_like` (npts,3) First derivative at `npts` points of a nurbs curve d2: float :term:`array_like` (npts,3) Second derivative at `npts` points of a nurbs curve d3: float :term:`array_like` (npts,3) , optional Third derivative at `npts` points of a nurbs curve Returns ------- T: float array(npts,3) Normalized tangent vector to the curve at `npts` points. N: float array(npts,3) Normalized normal vector to the curve at `npts` points. B: float array(npts,3) Normalized binormal vector to the curve at `npts` points. k: float array(npts,3) Curvature of the curve at `npts` points. t: float array(npts,3), optional Torsion of the curve at `npts` points. This value is only returned if `d3` was provided. See Also -------- NurbsCurve.frenet : the corresponding NurbsCurve method NurbsCurve.deriv : computation of the derivatives of a NurbsCurve """ ld = at.length(d1) # What to do when ld is 0? same as with k? if ld.min() == 0.0: print(f"ld is zero at {np.where(ld==0.0)[0]}") e1 = d1 / ld.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(f"k is zero at {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) / ld**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