#
##
## SPDX-FileCopyrightText: © 2007-2022 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.2 (Wed Nov 9 17:44:14 CET 2022)
## 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:`globalInterpolationCurve`: construct a NurbsCurve through given points
The other classes and functions are mostly intended for internal use.
"""
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
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:]
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
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:])
def npoints(self):
"""Return the total number of points in the Coords4."""
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(**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
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."""
return f"KnotVector({self.nknots()}): " + ", ".join([
KnotVector._knot_format.format(v=v, m=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(
f"The value {u} does not appear in the KnotVector")
return w[0]
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
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]
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.
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
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]]
#return self.knotu[0], self.knotu[-1]
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 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()
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.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()}")
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)
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. ]])
"""
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('ctrl',ctrl)
print('knots',knots)
print('u',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 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
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])
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
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.17, 0.75, 0. ],
[1.83, 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)
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)
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
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
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
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
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)
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)
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
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)
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 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)
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
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: :term:`coords_like` (3,)
A set of npts points in space.
Returns
-------
u: float
Parameter value of the base point X of the projection
of P on the NurbsCurve.
X: Coords (3,)
The coordinates of the base point of the projection of P
on the NurbsCurve.
"""
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)
d = pts.distanceFromPoint(P)
i = d.argmin()
u0, P0 = u[i], pts[i]
if d[i] == 0.:
# The point is on the curve
return u0, P0
del pts
del d
# 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
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
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)
def distance(self, N, ndiv=100):
"""Evaluate the distance between two Nurbs Curves"""
u0, u1 = N.urange()
u = u0 + np.arange(ndiv+1) * (u1-u0) / ndiv
pts = N.pointsAt(u)
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 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])
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.ctrl.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.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
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
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')
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,
}
}
################################################################
[docs]def globalInterpolationParameters(Q, exp):
"""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
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:
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 globalInterpolationCurve(Q, degree=3, *, t0=None, t1=None, alfa=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.,
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
--------
cubicSplineInterpolate: returns the classical C2 spline interpolate
"""
p = degree
# set the parameter values
if u is None:
u = globalInterpolationParameters(Q, exp=exp)
else:
u = at.checkArray(u, (npts,), 'f', 'i')
# set the end conditions
it0 = 0 if t0 is None else 1
it1 = 0 if t1 is None else 1
if it0+it1 > 0:
t0, t1 = globalInterpolationEndConditions(Q, t0, t1, alfa)
# compute the system matrix and solve
U, A = lib.nurbs.curveGlobalInterpolationMatrix(u, degree, it0, it1)
# set right hand side (move this inside ...?
if it0 == 1:
t0 = Coords(t0).reshape(-1,3) * U[p+1] / p * 4
Q = Coords.concatenate([Q[0:1], t0, Q[1:]])
if it1 == 1:
t1 = Coords(t1).reshape(-1,3) * (1-U[-(p+2)]) / p * 4
Q = Coords.concatenate([Q[:-1], t1, 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 globalInterpolationCurve2(Q, D, degree=3, *, u=None, exp=0.7, alfa=None,
return_param=False):
"""Create a global interpolation NurbsCurve with given derivatives.
"""
p = degree
# set derivatives
Q = Coords(Q)
D = Coords(D)
if Q.shape != D.shape:
print(Q.shape)
print(D.shape)
raise ValueError("Q and D should have same shape")
if alfa is None:
alfa = curve.PolyLine(Q).length()
D = at.normalize(D)
D = alfa*D
# set the parameter values
nc = Q.shape[0]
if u is None:
u = globalInterpolationParameters(Q, exp=exp)
else:
u = at.checkArray(u, (nc,), 'f', 'i')
# compute the system matrix
from pyformex.lib import nurbs_e
U, A, R = nurbs_e.curveGlobalInterpolationMatrix2(Q, D, u, p)
P = np.linalg.solve(A, R)
N = NurbsCurve(P, knots=U, degree=degree)
if return_param:
return N, u
else:
return N
[docs]def cubicSpline(Q, t0, t1, *, u=None, exp=0.7, alfa=None):
"""Cubic spline interpolation.
Computes a traditional C2 cubic spline through the points Q with
given end tangents.
Parameters
----------
Q: float array (nc, 3)
The nc points where the curve should pass through.
t0: float array (3,)
The tangent to the curve at the start point Q[0]
t1: float array (3,)
The tangent to the curve at the end point Q[nc-1]
u: float array (nc,), 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`.
alfa: float, optional
Multiplication factor for the tangent vectors. If not provided,
the tangent vectors are normalized and the multiplication factor
is set to the mean segment length of the PolyLine through the
given points Q.
Note that the length of these vectors influences the resulting curve.
Returns
-------
N: NurbsCurve
A NurbsCurve of the third degree that passes through the
given point set Q and has tangents t0 and t1 at its ends.
The number of control points of the curve is nc+2.
The parametric values of the input points Q can be got from
``N.knots[3:-3]``.
See Also
--------
globalInterpolationCurve: global interpolation curve of any degree
"""
# set the parameter values
if u is None:
u = globalInterpolationParameters(Q, exp=exp)
else:
u = at.checkArray(u, (npts,), 'f', 'i')
# set the end conditions
t0, t1 = globalInterpolationEndConditions(Q, t0, t1, alfa)
# Knots
U = np.concatenate([[0]*3, u, [1]*3])
# Control points
P = lib.nurbs.cubicSplineInterpolation(Q, t0, t1, U)
return NurbsCurve(P, knots=U, degree=3)
[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,): 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
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
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)
[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