def test_001_curve_knot_vector_too_short(self): knot_vector = (0.0, ) # too short coef = (1.0, ) degree = 0 C = bsp.Curve(knot_vector, coef, degree) result = C.is_valid() self.assertIsInstance(result, AssertionError) self.assertTrue( result.args[0] == "Error: knot vector mininum length is two.")
def test_002_curve_degree_too_small_and_verbose(self): knot_vector = (0.0, 1.0) coef = (1.0, ) degree = -1 # integer >= 0, so -1 is out of range for test verbosity = True # to test the verbose code lines C = bsp.Curve(knot_vector, coef, degree, verbosity) result = C.is_valid() self.assertIsInstance(result, AssertionError) self.assertTrue( result.args[0] == "Error: degree must be non-negative.")
def __init__(self, **kwargs): super().__init__(**kwargs) COEF = kwargs.get( "coefficients", None ) # None is basis, not None is curve, surface, or volume assert COEF is None # build up B-spline basis functions for i in np.arange(self.NCP): coef = np.zeros(self.NCP) coef[i] = 1.0 _B = bsp.Curve(self.knot_vector_t, coef, self.degree_t) if _B.is_valid(): _y = _B.evaluate(self.evaluation_times) self.evaluated_bases.append(_y) # plot B-spline basis functions self.fig = plt.figure(figsize=plt.figaspect(1.0 / (self.nel + 1)), dpi=self.dpi) ax = self.fig.gca() # loop over each basis function and plot it for i in np.arange(self.NCP): _CPTXT = f"{i}" _DEGTXT = f"{self.degree_t}" ax.plot( self.evaluation_times, self.evaluated_bases[i], "-", lw=2, label="$N_{" + _CPTXT + "}^{" + _DEGTXT + "}$", linestyle=self.linestyles[np.remainder(i, len(self.linestyles))], ) ax.set_xlabel(r"$t$") ax.set_ylabel(f"$N^{self.degree_t}_i(t)$") _eps = 0.1 ax.set_xlim( [self.knot_vector_t[0] - 2 * _eps, self.knot_vector_t[-1] + 2 * _eps] ) ax.set_ylim([0.0 - 2 * _eps, 1.0 + 2 * _eps]) ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left", borderaxespad=0.0) ax.xaxis.set_major_locator(MultipleLocator(1.0)) ax.xaxis.set_minor_locator(MultipleLocator(0.25)) ax.yaxis.set_major_locator(MultipleLocator(1.0)) ax.yaxis.set_minor_locator(MultipleLocator(0.25)) # finish figure by calling method with common figure functions ViewBSplineFigure(self)
def test_004_curve_basis_degree_zero_seven_knots(self): knot_vector = (0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0) degree = 0 # constant coef = (0.0, 1.0, 0.0, 0.0, 0.0, 0.0) N1_p0 = bsp.Curve(knot_vector, coef, degree) result = N1_p0.is_valid() self.assertTrue(result) tmin, tmax, npts = knot_vector[0], knot_vector[-1], 13 t = np.linspace(tmin, tmax, npts, endpoint=True) y = N1_p0.evaluate(t) y_known = (0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) self.assertTrue(self.same(y_known, y))
def test_003_curve_basis_recover_bezier_linear(self): knot_vector = (0.0, 0.0, 1.0, 1.0) degree = 1 # linear coef_N0_p1 = (1.0, 0.0) N0_p1 = bsp.Curve(knot_vector, coef_N0_p1, degree) result = N0_p1.is_valid() self.assertTrue(result) tmin, tmax, npts = 0.0, 1.0, 5 t = np.linspace(tmin, tmax, npts, endpoint=True) y = N0_p1.evaluate(t) y_known = (1.0, 0.75, 0.5, 0.25, 0.0) self.assertTrue(self.same(y_known, y)) coef_N1_p1 = (0.0, 1.0) N1_p1 = bsp.Curve(knot_vector, coef_N1_p1, degree) result = N1_p1.is_valid() self.assertTrue(result) # tmin, tmax, npts = 0.0, 1.0, 5 t = np.linspace(tmin, tmax, npts, endpoint=True) y = N1_p1.evaluate(t) y_known = (0.0, 0.25, 0.5, 0.75, 1.0) self.assertTrue(self.same(y_known, y))
def test_000_curve_initialization(self): knot_vector = (0.0, 1.0) coef = (1.0, ) degree = 0 C = bsp.Curve(knot_vector, coef, degree) self.assertIsInstance(C, bsp.Curve)
def test_101_Bingol_2D_curve(self): """See https://nurbs-python.readthedocs.io/en/latest/visualization.html#curves """ # knot vector KV = (0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.0, 1.0, 1.0, 1.0) DEGREE = 4 # quadratic NBI = 1 # number of bisection intervals per knot span # NCP = 9 # number of control points knots_lhs = KV[0:-1] # left-hand-side knot values knots_rhs = KV[1:] # right-hand-side knot values knot_spans = np.array(knots_rhs) - np.array(knots_lhs) dt = knot_spans / (2**NBI) assert all([dti >= 0 for dti in dt]), "Error: knot vector is decreasing." num_knots = len(KV) t = [ knots_lhs[k] + j * dt[k] for k in np.arange(num_knots - 1) for j in np.arange(2**NBI) ] t.append(KV[-1]) t = np.array(t) COEF = ( (5.0, 10.0), (15.0, 25.0), (30.0, 30.0), (45.0, 5.0), (55.0, 5.0), (70.0, 40.0), (60.0, 60.0), (35.0, 60.0), (20.0, 40.0), ) NSD = len(COEF[0]) # number of space dimensions C = bsp.Curve(KV, COEF, DEGREE) result = C.is_valid() self.assertTrue(result) y = C.evaluate(t) # retain only non-repeated points at begnning and end # (which drops redundant points and beginning and end) y = y[2**NBI * DEGREE:-(2**NBI * DEGREE)] P_known = ( (5.0, 10.0), (21.809895833333336, 24.60503472222222), (33.95833333333333, 20.347222222222218), (42.94270833333333, 11.692708333333336), (49.79166666666665, 7.84722222222222), (55.9157986111111, 12.174479166666664), (61.527777777777786, 23.61111111111111), (64.11458333333333, 38.29427083333332), (59.8611111111111, 51.31944444444444), (45.4079861111111, 57.37413194444444), (20.0, 40.0), ) # from geomdl at curve.delta = 1./11., thus 11 evaluation points # The 11 evaluation points bisects the five elements plus the endpoint # will correspond to NBI=1 (number of bisection intervals) used here. for i in np.arange(NSD): P_known_e = [e[i] for e in P_known] # e is evaluation point self.assertTrue(self.same(P_known_e, y[:, i]))
def test_100_curve_basis_quadratic_eight_knots(self): """Known example from NURBS Book, Piegl and Tiller, Ex2.2, Fig 2.6 page 55.""" KV = tuple(map(float, [0, 0, 0, 1, 2, 3, 4, 4, 5, 5, 5])) # knot vector DEGREE = 2 # quadratic NBI = 3 # number of bisection intervals per knot span NCP = 8 # number of control points knots_lhs = KV[0:-1] # left-hand-side knot values knots_rhs = KV[1:] # right-hand-side knot values knot_spans = np.array(knots_rhs) - np.array(knots_lhs) dt = knot_spans / (2**NBI) assert all([dti >= 0 for dti in dt]), "Error: knot vector is decreasing." num_knots = len(KV) t = [ knots_lhs[k] + j * dt[k] for k in np.arange(num_knots - 1) for j in np.arange(2**NBI) ] t.append(KV[-1]) t = np.array(t) N_calc = [] # basis functions calculated by bsp.Curve for i in np.arange(NCP): coef = np.zeros(NCP) coef[i] = 1.0 C = bsp.Curve(KV, coef, DEGREE) if C.is_valid(): y = C.evaluate(t) N_calc.append(y) N_known = np.zeros((NCP, t.size), dtype=t.dtype) # Citation to give credit for Pythonic test implementation on knot intervals: # Roberto Agromayor (RoberAgro) Ph.D. candidate in turbomachinery design and # optimization at the Norwegian University of Science and Technology (NTNU) # https://github.com/RoberAgro/nurbspy/blob/master/tests/test_nurbs_basis_functions.py for j, t in enumerate(t): N0_p2 = (1 - t)**2 * (0 <= t < 1) N1_p2 = (2 * t - 3 / 2 * t**2) * (0 <= t < 1) + (1 / 2 * (2 - t)**2) * (1 <= t < 2) N2_p2 = ((1 / 2 * t**2) * (0 <= t < 1) + (-3 / 2 + 3 * t - t**2) * (1 <= t < 2) + (1 / 2 * (3 - t)**2) * (2 <= t < 3)) N3_p2 = ((1 / 2 * (t - 1)**2) * (1 <= t < 2) + (-11 / 2 + 5 * t - t**2) * (2 <= t < 3) + (1 / 2 * (4 - t)**2) * (3 <= t < 4)) N4_p2 = (1 / 2 * (t - 2)**2) * (2 <= t < 3) + (-16 + 10 * t - 3 / 2 * t**2) * (3 <= t < 4) N5_p2 = (t - 3)**2 * (3 <= t < 4) + (5 - t)**2 * (4 <= t < 5) N6_p2 = (2 * (t - 4) * (5 - t)) * (4 <= t < 5) N7_p2 = (t - 4)**2 * (4 <= t <= 5) N_known[:, j] = np.asarray( [N0_p2, N1_p2, N2_p2, N3_p2, N4_p2, N5_p2, N6_p2, N7_p2])
def __init__( self, sample_points: Union[List[float], Tuple[float], ndarray], degree: int = 0, verbose: bool = False, sample_time_method: str = "chord", knot_method: str = "average", ): """Creates a B-Spline curve based on fits to sample_points on curve. Sample points are either interpolated (default) or approximated (to come). Args: sample_points (ArrayLike[float]): [[a0, a1, a2, ... am], [b0, b1, b2, ... bm], [c0, c1, c2, ... cm]] composed of (m+1) samples points on the curve used to generate a fitted B-sline curve. Coordinate measurements (a, b, c) are made in the (x, y, z) directions. degree (int >= 0): B-spline polynomial degree verbose (bool): prints additional information, default False sample_time_method: "chord": default, standard method to determine sample times "centripetal": (optional), suited for data with sharp turns knot_method: "average": default, recommended method for placing knots "equal": (optional), not recommended, can lead to a singular matrix, implemented herein for unit tests and testing purposes only. """ if not isinstance(sample_points, (list, tuple, ndarray)): raise TypeError( "Error: sample points must be a list, tuple, or ndarray.") if degree < 0: raise ValueError("Error: degree must be non-negative.") if sample_time_method not in ("chord", "centripetal"): raise ValueError( "Error: sample_time_method must be 'chord' or 'centripetal'") if knot_method not in ("average", "equal"): raise ValueError("Error: knot_method must be 'average' or 'equal'") self.samples = np.asarray(sample_points) self.n_samples = len(self.samples) # (m + 1) self._m = self.n_samples - 1 # m index self.n_control_points = ( self.n_samples) # special case n = m, number of control points # self.verbose = verbose self.valid = False self._bspline = None # Piegl 1997 page 364-365, chord length method or centripetal method chord_lengths = np.zeros(self.n_samples) for k in range(1, self.n_samples): chord_length = np.linalg.norm(self.samples[k] - self.samples[k - 1]) if sample_time_method == "chord": chord_lengths[ k] = chord_length # norm from Eq. (9.5) Piegl 1997 else: # "centripetal" chord_lengths[k] = np.sqrt(chord_length) # sqrt norm Eq. (9.6) total_chord_length = sum(chord_lengths) self._sample_times = np.zeros(self.n_samples) for k in range(1, self.n_samples): self._sample_times[k] = (self._sample_times[k - 1] + chord_lengths[k] / total_chord_length ) # Eq. (9.5) or (9.6) Piegl 1997 # averaging knots from sample times, Piegl 1997, page 365, Eq. (9.8) self._n_knots = degree + 1 + self.n_samples # (kappa + 1) self._kappa = self._n_knots - 1 self._knot_vector = np.zeros(self._n_knots) if knot_method == "average": # averaging knots from sample times for j in range(1, self._m - degree + 1): # +1 for Python 0-index # _phi_high = degree - 1 + j _phi_high = degree + j # add +1 back in to account for Python 0-index _knot_subspan_average = (1 / degree) * sum( self._sample_times[j:_phi_high]) self._knot_vector[degree + j] = _knot_subspan_average else: # knot_method is "equal" for j in range(1, self._m - degree + 1): # +1 for Python 0-index self._knot_vector[degree + j] = j / (self._m - degree + 1) # append end of knot vector with 1.0 repeated (degree + 1) times _kappa_minus_p = self._kappa - degree self._knot_vector[_kappa_minus_p:self._kappa + 1] = 1.0 # Python 0-index # matrix A u = f -> (notation) N u = f self._sample_basis_matrix = [] # (m x 1) by (n x 1) coefficient matrix for column in range(self.n_control_points): coef = np.zeros(self.n_control_points) coef[column] = 1.0 # build the B-spline basis functions column-by-column _bspline_basis_function = bsp.Curve(self.knot_vector, coef, degree) if _bspline_basis_function.is_valid(): _y = _bspline_basis_function.evaluate(self.sample_times) self._sample_basis_matrix.append(_y) # now transpose the N matrix, since we filled column-wise self._sample_basis_matrix = np.transpose(self._sample_basis_matrix) # self.control_points = np.linalg.solve(self._N, sample_points) self._control_points = np.linalg.solve(self._sample_basis_matrix, self.samples) self.valid = True # if we come to the end of __init__, all is valid
def __init__(self, **kwargs): super().__init__(**kwargs) COEF = kwargs.get( "coefficients", None ) # None is basis, not None is curve, surface, or volume assert COEF is not None # show/hide control point locations _control_points_shown = kwargs.get("control_points_shown", False) # show/hide knot locations _knots_shown = kwargs.get("knots_shown", False) _evaluated_knots = [] if _knots_shown: _knots_t = np.unique(self.knot_vector_t) # show/hide sample points in case of a B-spline fit _samples_shown = kwargs.get("samples_shown", False) # build up B-spline basis functions, multiplied by coefficients _NSD = len(COEF[0]) # number of space dimensions assert _NSD == 2 # only 2D curves implemented for now, do 3D later for i in np.arange(_NSD): coef = np.array(COEF)[:, i] _B = bsp.Curve(self.knot_vector_t, coef, self.degree_t) if _B.is_valid(): _y = _B.evaluate(self.evaluation_times) self.evaluated_curve.append(_y) if _knots_shown: _y = _B.evaluate(_knots_t) _evaluated_knots.append(_y) # plot B-spline curve, assume 2D for now self.fig = plt.figure(dpi=self.dpi) ax = self.fig.gca() if _control_points_shown: # plot control points _cp_x = np.array(COEF)[:, 0] # control points x-coordinates _cp_y = np.array(COEF)[:, 1] # control points y-coordinates ax.plot( _cp_x, _cp_y, color="red", linewidth=1, alpha=0.5, marker="o", markerfacecolor="white", markersize=9, linestyle="dashed", ) if _samples_shown: _samples_x = np.array(kwargs.get("samples"))[:, 0] _samples_y = np.array(kwargs.get("samples"))[:, 1] ax.plot( _samples_x, _samples_y, color="darkorange", alpha=1.0, linestyle="none", linewidth="6", marker="+", markersize=20, ) # plus mark ax.plot( _samples_x, _samples_y, color="orange", linestyle="none", marker="D", markerfacecolor="none", markersize=14, ) # diamond border around plus mark ax.plot( self.evaluated_curve[0], self.evaluated_curve[1], color="navy", linestyle="solid", linewidth=3, ) if _samples_shown: # yet another layer for samples, small little dot atop curve _samples_x = np.array(kwargs.get("samples"))[:, 0] _samples_y = np.array(kwargs.get("samples"))[:, 1] ax.plot( _samples_x, _samples_y, color="darkorange", linestyle="none", marker=".", markersize=2, ) # dot if _knots_shown: for i, knot_num in enumerate(self.knot_vector_t): # plot only the first knot of any repeated knot if i == 0: # print(f"first index {i}") k = i # first evaluated knot index if self.latex: _str = r"$\mathsf T_{0}$" else: # _str = r"$T_{0}$" continue # GitHub unit test doesn't like LaTex else: if self.knot_vector_t[i] == self.knot_vector_t[i - 1]: continue # don't plot subsequently repeated knots # print(f"subsequent index {i}") k += 1 # next non-repeated knot index in evaluated knots # _Ti = str(int(i + self.degree_t)) _Ti = str(int(i)) if self.latex: _str = r"$\mathsf T_{" + _Ti + "}$" else: # _str = r"$T_{" + _Ti + "}$" continue # GitHub unit test doesn't like LaTex # print(_str) ax.plot( _evaluated_knots[0][k], _evaluated_knots[1][k], color="white", alpha=0.6, marker="o", markersize=14, markeredgecolor="darkcyan", ) # background circle ax.plot( _evaluated_knots[0][k], _evaluated_knots[1][k], color="black", marker=_str, markersize=9, markeredgecolor="none", ) # knot number text # self.fig.patch.set_facecolor("whitesmoke") # ax.set_facecolor("lightgray") ax.set_xlabel(r"$x$") ax.set_ylabel(r"$y$") # finish figure by calling method with common figure functions ViewBSplineFigure(self)
# fig = plt.figure(figsize=plt.figaspect(1.0 / (len(knot_vector) - 1)), dpi=dpi) fig = plt.figure(figsize=plt.figaspect(1.0 / max(knot_vectors[-1])), dpi=dpi) ax = fig.gca() # ax.grid() ax.grid(True, which="major", linestyle="-") ax.grid(True, which="minor", linestyle=":") for i in np.arange(len(degrees)): knot_vector = knot_vectors[i] coef = coefs[i] degree = degrees[i] label = labels[i] N0_p = bsp.Curve(knot_vector, coef, degree) result = N0_p.is_valid() npts = int((knot_vector[-1] - knot_vector[0]) * (2**n_bisections) + 1) # tmin, tmax, npts = knot_vector[0], knot_vector[-1], 13 tmin, tmax, npts = knot_vector[0], knot_vector[-1], npts t = np.linspace(tmin, tmax, npts, endpoint=True) y = N0_p.evaluate(t) ax.plot( t, y, "-", linewidth=2, label=label, linestyle=linestyles[np.remainder(i, len(linestyles))],