Source code for pymead.core.bspline

import typing

import numpy as np
from matplotlib import pyplot as plt
from rust_nurbs import *

from pymead.core.param import Param, ParamSequence
from pymead.core.point import PointSequence, Point
from pymead.core.parametric_curve import ParametricCurve, PCurveData, ParametricCurveEndpoint
from pymead.post.fonts_and_colors import font
from pymead.post.plot_formatters import format_axis_scientific


[docs] class BSpline(ParametricCurve):
[docs] def __init__(self, point_sequence: PointSequence or typing.List[Point], knot_sequence: ParamSequence or typing.List[Param], default_nt: int or None = None, name: str or None = None, t_start: float = None, t_end: float = None, **kwargs): """ Non-uniform rational B-spline (NURBS) curve evaluation class """ super().__init__(sub_container="bsplines", **kwargs) assert len(knot_sequence) >= len(point_sequence) + 1 self.default_nt = default_nt self._point_sequence = None self._knot_sequence = None point_sequence = PointSequence(point_sequence) if isinstance(point_sequence, list) else point_sequence knot_sequence = ParamSequence(knot_sequence) if isinstance(knot_sequence, list) else knot_sequence self.set_point_sequence(point_sequence) self.set_knot_sequence(knot_sequence) self.disable_clamped_knots() self.t_start = t_start self.t_end = t_end name = "BSpline-1" if name is None else name self.set_name(name) self.curve_connections = [] self._add_references()
def _add_references(self): for idx, point in enumerate(self.point_sequence().points()): # If any curves are found at the start point, add their pointers as curve connections if idx == 0: for curve in point.curves: if not curve.reference: # Do not include reference curves self.curve_connections.append(curve) # If any other curves are found at the end point, add their pointers as curve connections elif idx == len(self.point_sequence()) - 1: for curve in point.curves: if not curve.reference: # Do not include reference curves self.curve_connections.append(curve) # Add the object reference to each point in the curve if self not in point.curves: point.curves.append(self) for knot in self.knots(): knot.bspline = self def disable_clamped_knots(self): for knot_idx, knot in enumerate(self.knots()): if knot_idx <= self.degree or knot_idx >= len(self.knot_sequence()) - self.degree - 1: knot.set_enabled(False)
[docs] def set_name(self, name: str): super().set_name(name) for knot_idx, knot in enumerate(self.knot_sequence().params()): knot._name = f"{self.name()}.knot_{knot_idx}"
@property def degree(self) -> int: return len(self.knot_sequence()) - len(self.point_sequence()) - 1 @property def weights(self) -> np.ndarray: return np.ones(len(self.point_sequence())) def point_sequence(self): return self._point_sequence def knot_sequence(self): return self._knot_sequence def points(self): return self.point_sequence().points() def knots(self): return self.knot_sequence().params() def get_control_point_array(self): return self.point_sequence().as_array() def get_knot_vector(self): return self.knot_sequence().as_array() def set_point_sequence(self, point_sequence: PointSequence): self._point_sequence = point_sequence def set_knot_sequence(self, knot_sequence: ParamSequence): self._knot_sequence = knot_sequence def reverse_point_sequence(self): self.point_sequence().reverse() def reverse_knot_sequence(self): self.knot_sequence().reverse() def insert_point(self, idx: int, point: Point): self.point_sequence().insert_point(idx, point) if self not in point.curves: point.curves.append(self) if self.canvas_item is not None: self.canvas_item.point_items.insert(idx, point.canvas_item) self.canvas_item.updateCurveItem(self.evaluate()) def insert_point_after_point(self, point_to_add: Point, preceding_point: Point): idx = self.point_sequence().point_idx_from_ref(preceding_point) + 1 self.insert_point(idx, point_to_add) def point_removal_deletes_curve(self): return len(self.point_sequence()) <= 3 def remove_point(self, idx: int or None = None, point: Point or None = None): if isinstance(point, Point): idx = self.point_sequence().point_idx_from_ref(point) self.point_sequence().remove_point(idx) if len(self.point_sequence()) > 2: delete_curve = False else: delete_curve = True return delete_curve def remove(self): if self.canvas_item is not None: self.canvas_item.sigRemove.emit(self.canvas_item)
[docs] def is_clamped(self, loc: ParametricCurveEndpoint) -> bool: """ Determines whether the knot sequence of the B-spline is clamped. A knot sequence is clamped when the first :math:`p+1` knots are equal and the last :math:`p+1` knots are equal. Parameters ---------- loc: ParametricCurveEndpoint Specifies whether to check if the curve is clamped at the start or end Returns ------- bool Whether the B-spline has a clamped knot sequence """ q = self.degree knots = self.knot_sequence().as_array() if loc == ParametricCurveEndpoint.Start: start_knot = knots[0] if np.all(np.isclose(knots[:(q + 1)], start_knot)): return True return False end_knot = knots[-1] if np.all(np.isclose(knots[-(q + 1):], end_knot)): return True return False
def hodograph(self) -> "BSpline": P = self.get_control_point_array() if self.degree <= 1: point_sequence = PointSequence.generate_from_array(np.array([[0.0, 0.0]])) knot_sequence = ParamSequence.generate_from_array(np.array([0.0])) return BSpline(point_sequence, knot_sequence, default_nt=self.default_nt) point_sequence = PointSequence.generate_from_array(np.array([ [ self.degree / (self.knots()[i + self.degree + 1].value() - self.knots()[i + 1].value()) * ( P[i + 1, 0] - P[i, 0]), self.degree / (self.knots()[i + self.degree + 1].value() - self.knots()[i + 1].value()) * ( P[i + 1, 0] - P[i, 0]) ] for i in range(len(self.points()) - 1) ])) knot_sequence = ParamSequence.generate_from_slice(self.knot_sequence(), slice(1, -1)) return BSpline(point_sequence, knot_sequence, default_nt=self.default_nt)
[docs] def derivative(self, t: np.ndarray, order: int): r""" Calculates an arbitrary-order derivative of the Bézier curve. Parameters ---------- t: np.ndarray The parameter vector order: int The derivative order. For example, ``order=2`` returns the second derivative. Returns ------- np.ndarray An array of ``shape=(N,2)`` where ``N`` is the number of evaluated points specified by the :math:`t` vector. The columns represent :math:`C^{(m)}_x(t)` and :math:`C^{(m)}_y(t)`, where :math:`m` is the derivative order. """ assert order >= 0 curve = self for n in range(order): curve = curve.hodograph() return curve.evaluate_xy(t)
[docs] def evaluate_xy(self, t: np.array or None = None, **kwargs) -> np.ndarray: """ Evaluate the B-spline curve at parameter t """ # Generate the parameter vector if self.default_nt is not None: kwargs["nt"] = self.default_nt t = ParametricCurve.generate_t_vec(**kwargs) if t is None else t # Evaluate the curve return np.array(bspline_curve_eval_tvec(self.point_sequence().as_array(), self.knot_sequence().as_array(), t))
[docs] def evaluate(self, t: np.array or None = None, **kwargs): r""" Evaluates the curve using an optionally specified parameter vector. Parameters ---------- t: np.ndarray or ``None`` Optional direct specification of the parameter vector for the curve. Not specifying this value gives a linearly spaced parameter vector from ``t_start`` or ``t_end`` with the default size. Default: ``None`` Returns ------- PCurveData Data class specifying the following information about the Bézier curve: .. math:: C_x(t), C_y(t), C'_x(t), C'_y(t), C''_x(t), C''_y(t), \kappa(t) where the :math:`x` and :math:`y` subscripts represent the :math:`x` and :math:`y` components of the vector-valued functions :math:`\vec{C}(t)`, :math:`\vec{C}'(t)`, and :math:`\vec{C}''(t)`. """ # Generate the parameter vector if self.default_nt is not None: kwargs["nt"] = self.default_nt t = ParametricCurve.generate_t_vec(**kwargs) if t is None else t # Number of control points, curve degree, control point array P = self.point_sequence().as_array() k = self.knot_sequence().as_array() # Evaluate the curve xy = np.array(bspline_curve_eval_tvec(P, k, t)) # Calculate the first derivative xpyp = np.array(bspline_curve_dcdt_tvec(P, k, t)) xp, yp = xpyp[:, 0], xpyp[:, 1] # Calculate the second derivative xppypp = np.array(bspline_curve_d2cdt2_tvec(P, k, t)) xpp, ypp = xppypp[:, 0], xppypp[:, 1] # Combine the derivative x and y data xpyp = np.column_stack((xp, yp)) xppypp = np.column_stack((xpp, ypp)) # Calculate the curvature with np.errstate(divide='ignore', invalid='ignore'): # Calculate the curvature of the Bézier curve (k = kappa = 1 / R, where R is the radius of curvature) k = np.true_divide((xp * ypp - yp * xpp), (xp ** 2 + yp ** 2) ** (3 / 2)) # Calculate the radius of curvature: R = 1 / kappa with np.errstate(divide='ignore', invalid='ignore'): R = np.true_divide(1, k) return PCurveData(t=t, xy=xy, xpyp=xpyp, xppypp=xppypp, k=k, R=R)
[docs] def plot(self, ax: plt.Axes or None = None, nt: int = 100, show: bool = True, save_file: str or None = None, **plt_kwargs): """ Plots the airfoil to a ``matplotlib`` figure. Parameters ---------- ax: plt.Axes or None Matplotlib Axes object on which the curve will be plotted. If specified, this method will only. If ``None``, a new figure will be created. Default: ``None`` nt: int Number of parametric values to evaluate along the curve. Default: 100 show: bool Whether to immediately show the curve plot. Ignored if ``ax`` is not ``None``. Default: ``True`` save_file: str or None Name of the file to save. If ``None``, the curve image will not be saved to file. Ignored if ``ax`` is not ``None``. Default: ``None`` plt_kwargs Additional keyword arguments to pass to ``matplotlib.pyplot.plot`` """ ax_specified = ax is not None if ax_specified: fig = ax.figure else: fig, ax = plt.subplots(figsize=(10, 2)) # Plot the curves curve_data = self.evaluate_xy(np.linspace(0.0, 1.0, nt)) ax.plot(curve_data[:, 0], curve_data[:, 1], **plt_kwargs) if ax_specified: return # Plot settings ax.set_aspect("equal") ax.set_xlabel("x", fontdict=font) ax.set_ylabel("y", fontdict=font) format_axis_scientific(ax=ax) # Save and/or show if save_file is not None: fig.savefig(save_file, bbox_inches="tight") if show: plt.show()
[docs] def get_dict_rep(self) -> dict: return { "points": [pt.name() for pt in self.point_sequence().points()], "knots": [knot.name() for knot in self.knot_sequence().params()], "default_nt": self.default_nt }
[docs] def main(): point_sequence = PointSequence.generate_from_array(np.array([ [0.0, 0.0], [0.0, 0.2], [0.2, 0.3], [0.5, 0.1], [0.8, 0.2], [1.0, 0.0] ])) knot_sequence = ParamSequence.generate_from_array( np.array([0.0, 0.0, 0.0, 0.0, 1/3, 2/3, 1.0, 1.0, 1.0, 1.0]) ) bspline = BSpline(point_sequence, knot_sequence) data = bspline.evaluate() knot_sequence2 = ParamSequence.generate_from_array( np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]) ) bspline2 = BSpline(point_sequence, knot_sequence2) data2 = bspline2.evaluate() import matplotlib.pyplot as plt fig, ax = plt.subplots() data.plot(ax) ax.plot(bspline.get_control_point_array()[:, 0], bspline.get_control_point_array()[:, 1], ls=":", color="grey", marker="o") data2.plot(ax) plt.show()
if __name__ == "__main__": main()