Source code for pymead.core.wagner

import typing

import matplotlib.pyplot as plt
import numpy as np

from pymead.core.parametric_curve import ParametricCurve, PCurveData
from pymead.core.param import Param
from pymead.core.point import PointSequence, Point


[docs] class Wagner(ParametricCurve):
[docs] def __init__(self, A: typing.List[Param], point_sequence: PointSequence or typing.List[Point], default_nt: int or None = None, name: str or None = None, t_start: float = None, t_end: float = None, **kwargs): r""" Computes a Wagner curve by the coefficients. Parameters ---------- point_sequence: PointSequence Sequence of points defining the transformation for the curve. The first point defines the origin of the local coordinate system, and the position of the second point relative to the first point defines the scale and rotation angle. name: str or ``None`` Optional name for the curve. Default: ``None`` t_start: float or ``None`` Optional starting parameter vector value for the Wagner curve. Not specifying this value automatically gives a value of ``0.0``. Default: ``None`` t_end: float or ``None`` Optional ending parameter vector value for the Wagner curve. Not specifying this value automatically gives a value of ``1.0``. Default: ``None`` """ super().__init__(sub_container="ferguson", **kwargs) self._point_sequence = None self.default_nt = default_nt self.A = A point_sequence = PointSequence(point_sequence) if isinstance(point_sequence, list) else point_sequence assert len(point_sequence) == 2 assert len(A) > 0 self.set_point_sequence(point_sequence) self.t_start = t_start self.t_end = t_end name = "Wagner-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) def point_sequence(self): return self._point_sequence def points(self): return self.point_sequence().points() def get_A_as_array(self): return np.array([a.value() for a in self.A]) def get_control_point_array(self): return self.point_sequence().as_array() def set_point_sequence(self, point_sequence: PointSequence): self._point_sequence = point_sequence def reverse_point_sequence(self): self.point_sequence().reverse() def point_removal_deletes_curve(self): return len(self.point_sequence()) <= 2 def remove_point(self, idx: int or None = None, point: Point or None = None): point_removal_deletes_curve = self.point_removal_deletes_curve() if isinstance(point, Point): idx = self.point_sequence().point_idx_from_ref(point) self.point_sequence().remove_point(idx) return point_removal_deletes_curve def remove(self): if self.canvas_item is not None: self.canvas_item.sigRemove.emit(self.canvas_item)
[docs] def derivative(self, t: np.ndarray, order: int) -> np.ndarray: r""" Calculates an arbitrary-order derivative of the Ferguson 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. """ A = self.get_A_as_array() theta = np.pi * t def dx_dt(p: int): K1 = int((p - 1) % 2 == 0) * (-1) ** int((p - 1) % 4 != 0) K2 = int(p % 2 == 0) * (-1) ** int(p % 4 == 0) print(f"{K1 = }, {K2 = }") return np.pi ** p / 2 * (K1 * np.sin(np.pi * t) + K2 * np.cos(np.pi * t)) def dy_dt(p: int): out = -A[0] * dx_dt(p) for n in range(len(A)): K1 = (n + 1) ** (p - 1) * int(p % 2 == 0) * (-1) ** int(p % 4 != 0) K2 = (n + 1) ** (p - 1) * int((p - 1) % 2 == 0) * (-1) ** int((p - 1) % 4 != 0) K3 = n ** (p - 1) * int(p % 2 == 0) * (-1) ** int(p % 4 != 0) K4 = n ** (p - 1) * int((p - 1) % 2 == 0) * (-1) ** int((p - 1) % 4 != 0) print(f"{K1 = }, {K2 = }, {K3 = }, {K4 = }") out += np.pi ** (p - 1) * A[n] * (K1 * np.sin((n + 1) * theta) + K2 * np.cos((n + 1) * theta) + K3 * np.sin(n * theta) + K4 * np.cos(n * theta)) return out return np.column_stack((dx_dt(order), dy_dt(order)))
[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 # Evaluate the curve A = self.get_A_as_array() theta = np.pi * t x = 0.5 * (1 - np.cos(theta)) y = -A[0] * np.sin(0.5 * theta) ** 2 + A[0] / np.pi * (theta + np.sin(theta)) for A_idx, A_val in enumerate(A[1:]): n = A_idx + 1 y += A_val / np.pi * (np.sin((n + 1) * theta) / (n + 1) + np.sin(n * theta) / n) xy = np.column_stack((x, y)) # Calculate the first derivative first_deriv = self.derivative(t=t, order=1) xp = first_deriv[:, 0] yp = first_deriv[:, 1] # Calculate the second derivative second_deriv = self.derivative(t=t, order=2) xpp = second_deriv[:, 0] ypp = second_deriv[:, 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)) # Use curvature with x-parametrization instead of t at t=1 because of the zero value of xp and yp # for t_idx, t_val in enumerate(t): # if not np.isclose(t_val, 1.0): # continue # k[t_idx] = xppypp[t_idx, 1] / xppypp[t_idx, 0] / (1 + (-A[0])**2) ** 1.5 # 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 get_dict_rep(self): return { "A": [param.name() for param in self.A], "points": [pt.name() for pt in self.point_sequence().points()], "default_nt": self.default_nt }
[docs] def main(): # # xy = wagner_contour(np.array([0.10148, 0.019233, 0.0044033, 0.008108])) # A = np.array([0.071049, 0.011098, 0.0051307]) # xy = wagner_contour(A) # xy2 = wagner_contour(-A) # n_theta = 150 # dxdydt = wagner_contour_deriv(A, n_theta=n_theta) # plt.plot(xy[:, 0], xy[:, 1], color="indianred") # # for i in range(n_theta): # tail = xy[i, :] # head = tail - 0.03 * dxdydt[i, 1:] / np.linalg.norm(dxdydt[i, 1:]) # print(f"{tail = }, {head = }") # plt.plot([tail[0], head[0]], [tail[1], head[1]], color="steelblue") # # plt.show() wagner = Wagner([Param(0.10148, name="A0"), Param(0.019233, name="A1"), Param(0.0044033, name="A2"), Param(0.008108, name="A3")], PointSequence.generate_from_array(np.array([[0.0, 0.0], [1.0, 0.0]]))) data = wagner.evaluate(nt=200) print(f"Leading edge radius: {data.R[0]}") print(f"Trailing edge radius: {data.R[-1]}") print(f"Leading edge data: {data.xpyp[0] = }, {data.xppypp[0] = }") print(f"Middle data: {data.xpyp[75], data.xppypp[75] = }") print(f"Trailing edge data: {data.xpyp[-1] = }, {data.xppypp[-1] = }") plt.plot(data.xy[:, 0], data.xy[:, 1], color="steelblue") comb_tails, comb_heads = data.get_curvature_comb(0.0005) for comb_tail, comb_head in zip(comb_tails, comb_heads): plt.plot([comb_tail[0], comb_head[0]], [comb_tail[1], comb_head[1]], color="indianred") wagner = Wagner([Param(-0.10148, name="A0"), Param(-0.019233, name="A1"), Param(-0.0044033, name="A2"), Param(-0.008108, name="A3")], PointSequence.generate_from_array(np.array([[0.0, 0.0], [1.0, 0.0]]))) data = wagner.evaluate() plt.plot(data.xy[:, 0], data.xy[:, 1], color="mediumaquamarine") comb_tails, comb_heads = data.get_curvature_comb(0.0005) for comb_tail, comb_head in zip(comb_tails, comb_heads): plt.plot([comb_tail[0], comb_head[0]], [comb_tail[1], comb_head[1]], color="gold", ls=":") plt.gca().set_aspect("equal") plt.show()
if __name__ == "__main__": main()