Source code for pymead.core.line

import typing

import numpy as np
import pyqtgraph as pg
from matplotlib import pyplot as plt
from pymead.core.transformation import Transformation2D

from pymead.core.point import PointSequence, Point
from pymead.core.parametric_curve import ParametricCurve, PCurveData
from pymead.core.pymead_obj import PymeadObj
from pymead.post.fonts_and_colors import font
from pymead.post.plot_formatters import format_axis_scientific
from pymead.utils.get_airfoil import extract_data_from_airfoiltools


[docs] class LineSegment(ParametricCurve):
[docs] def __init__(self, point_sequence: PointSequence or typing.List[Point], name: str or None = None, **kwargs): super().__init__(sub_container="lines", **kwargs) self._point_sequence = None point_sequence = PointSequence(point_sequence) if isinstance(point_sequence, list) else point_sequence self.set_point_sequence(point_sequence) name = "Line-1" if name is None else name self.set_name(name) self._add_references()
def _add_references(self): for idx, point in enumerate(self.point_sequence().points()): # 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_control_point_array(self): return self.point_sequence().as_array() def set_point_sequence(self, point_sequence: PointSequence): if len(point_sequence) != 2: raise ValueError("Point sequence must contain exactly two points") self._point_sequence = point_sequence def reverse_point_sequence(self): self.point_sequence().reverse() def point_removal_deletes_curve(self): return True 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()) > 1: 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) def update(self): p_curve_data = self.evaluate() if self.canvas_item is not None: self.canvas_item.updateCanvasItem(curve_data=p_curve_data) def evaluate(self, t: np.ndarray or None = None, **kwargs): if "nt" not in kwargs.keys() and t is None: kwargs["nt"] = 2 # Set the default parameter vector for the line to be [0.0, 1.0] t = ParametricCurve.generate_t_vec(**kwargs) if t is None else t p1 = self.point_sequence().points()[0] p2 = self.point_sequence().points()[1] x1 = p1.x().value() y1 = p1.y().value() x2 = p2.x().value() y2 = p2.y().value() theta = np.arctan2(y2 - y1, x2 - x1) r = np.hypot(x2 - x1, y2 - y1) x = x1 + t * r * np.cos(theta) y = y1 + t * r * np.sin(theta) xy = np.column_stack((x, y)) xpyp = np.repeat(np.array([r * np.cos(theta), r * np.sin(theta)]), t.shape[0]) xppypp = np.repeat(np.array([0.0, 0.0]), t.shape[0]) k = np.zeros(t.shape) R = np.inf * np.ones(t.shape) return PCurveData(t=t, xy=xy, xpyp=xpyp, xppypp=xppypp, k=k, R=R)
[docs] def get_dict_rep(self): return {"points": [pt.name() for pt in self.point_sequence().points()]}
[docs] class PolyLine(ParametricCurve): AirfoilTools = 0 DatFile = 1
[docs] def __init__(self, source: str, coords: np.ndarray = None, start: int or float = None, end: int or float = None, point_sequence: PointSequence = None, name: str or None = None, num_header_rows: int = 0, delimiter: str or None = None, scale: float = 1.0, rotation_rad: float = 0.0, translation_x: float = 0.0, translation_y: float = 0.0, **kwargs): super().__init__(sub_container="polylines", **kwargs) self._point_sequence = None self.source = source self.source_type = self.DatFile if ("/" in source or "\\" in source) else self.AirfoilTools self.start = start self.end = end self.num_header_rows = num_header_rows self.delimiter = delimiter self.original_coords = self._get_original_coords() if coords is None else coords self.coords = self._get_coord_slice() point_sequence = self._extract_point_sequence_from_coords() if point_sequence is None else point_sequence self.set_point_sequence(point_sequence) self.scale = scale self.rotation_rad = rotation_rad self.translation_x = translation_x self.translation_y = translation_y name = self._get_default_name() if name is None else name self.set_name(name) self._add_references()
def split(self, split: int or Point): if split < 3 or split > len(self.coords) - 3: raise ValueError("Split value out of bounds for PolyLine") polyline1, polyline2 = None, None if isinstance(split, int): end_1 = self.start + split + 1 if self.start is not None else split + 1 start_2 = self.start + split if self.start is not None else split polyline1 = PolyLine(source=self.source, start=self.start, end=end_1) polyline2 = PolyLine(source=self.source, start=start_2, end=self.end) polyline2.point_sequence().points()[0] = polyline1.point_sequence().points()[-1] polyline2.point_sequence().points()[0].curves.append(polyline2) for new_polyline in [polyline1, polyline2]: for point_idx, point in enumerate(new_polyline.point_sequence().points()): for original_point in self.point_sequence().points(): if not point.is_coincident(original_point): continue new_polyline.point_sequence().points()[point_idx] = original_point if self in original_point.curves: original_point.curves.remove(self) if new_polyline not in original_point.curves: original_point.curves.append(new_polyline) break return [polyline1, polyline2] def add_polyline_airfoil(self): le = self._add_le() te = self._add_te() blunt_trailing_edge = self._add_trailing_edge_lines(te) return self._add_airfoil(le, te, blunt_trailing_edge=blunt_trailing_edge) def _add_le(self): coords_dist_from_origin = np.hypot(self.coords[:, 0], self.coords[:, 1]) le_row = np.argmin(coords_dist_from_origin) le = self.geo_col.add_point(self.coords[le_row, 0], self.coords[le_row, 1]) le.curves.append(self) return le def _add_te(self): if len(self.point_sequence().points()) == 1: return self.point_sequence().points()[0] if self.coords[0, 1] >= 0.0 >= self.coords[-1, 1]: te = self.geo_col.add_point(1.0, 0.0) else: te = self.geo_col.add_point(0.5 * (self.coords[0, 0] + self.coords[-1, 0]), 0.5 * (self.coords[0, 1] + self.coords[-1, 1])) return te def _add_trailing_edge_lines(self, te: Point): if len(self.point_sequence().points()) == 1: return False self.geo_col.add_line(PointSequence(points=[te, self.point_sequence().points()[0]])) self.geo_col.add_line(PointSequence(points=[te, self.point_sequence().points()[-1]])) return True def _add_airfoil(self, le: Point, te: Point, blunt_trailing_edge: bool): if blunt_trailing_edge: return self.geo_col.add_airfoil( le, te, upper_surf_end=self.point_sequence().points()[0], lower_surf_end=self.point_sequence().points()[-1] ) return self.geo_col.add_airfoil( le, te, upper_surf_end=te, lower_surf_end=te ) def _get_default_name(self): if self.source_type == self.AirfoilTools: return f"{self.source}-1" return "PolyLine-1" def _get_original_coords(self): if self.source_type == self.AirfoilTools: return self._load_coords_from_airfoil_tools() elif self.source_type == self.DatFile: try: return self._load_coords_from_dat_file(self.num_header_rows, self.delimiter) except: self.num_header_rows = 1 return self._load_coords_from_dat_file(self.num_header_rows, self.delimiter) else: raise ValueError("Invalid polyline source type") def _get_coord_slice(self): if self.start is None and isinstance(self.end, int): return self.original_coords[:self.end, :] elif self.end is None and isinstance(self.start, int): return self.original_coords[self.start:, :] elif isinstance(self.start, int) and isinstance(self.end, int): return self.original_coords[self.start:self.end, :] else: return self.original_coords def _load_coords_from_dat_file(self, num_header_rows: int = 0, delimiter: str or None = None): return np.loadtxt(self.source, skiprows=num_header_rows, delimiter=delimiter) def _load_coords_from_airfoil_tools(self): return extract_data_from_airfoiltools(self.source) def _extract_point_sequence_from_coords(self): if Point(self.coords[0, 0], self.coords[0, 1]).is_coincident(Point(self.coords[-1, 0], self.coords[-1, 1])): return PointSequence(points=[Point(self.coords[0, 0], self.coords[0, 1])]) return PointSequence(points=[Point(self.coords[row, 0], self.coords[row, 1]) for row in [0, -1]]) def _add_references(self): for idx, point in enumerate(self.point_sequence().points()): # 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 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 True 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()) > 1: 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) def update(self): p_curve_data = self.evaluate() if self.canvas_item is not None: self.canvas_item.updateCanvasItem(curve_data=p_curve_data) def evaluate(self, t: np.ndarray or None = None, **kwargs): xy = self.coords if self.scale != 1.0 or self.translation_x != 0.0 or self.translation_y != 0.0 or self.rotation_rad != 0.0: transformation = Transformation2D( tx=[self.translation_x], ty=[self.translation_y], r=[self.rotation_rad], sx=[self.scale], sy=[self.scale], order="r,s,t", rotation_units="rad" ) xy = transformation.transform(xy) t = np.linspace(0.0, 1.0, xy.shape[0]) xp = np.gradient(xy[:, 0], t) yp = np.gradient(xy[:, 1], t) xpyp = np.column_stack((xp, yp)) xpp = np.gradient(xp, t) ypp = np.gradient(yp, t) xppypp = np.column_stack((xpp, ypp)) with np.errstate(divide="ignore"): k = np.true_divide(xpyp[:, 0] * xppypp[:, 1] - xpyp[:, 1] * xppypp[:, 0], np.hypot(xpyp[:, 0], xpyp[:, 1])**1.5) 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, 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`` 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, 6)) # Plot the curves curve_data = self.evaluate() ax.plot(curve_data.xy[:, 0], curve_data.xy[:, 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): return {"points": [pt.name() for pt in self.point_sequence().points()], "source": self.source, "start": self.start, "end": self.end, "coords": self.original_coords.tolist()}
[docs] class ReferencePolyline(PymeadObj): AirfoilTools = 0 DatFile = 1
[docs] def __init__(self, points: typing.List[typing.List[float]] or np.ndarray = None, source: str = None, num_header_rows: int = 0, delimiter: str or None = None, color: tuple = (245, 37, 106), lw: float = 1.0, name: str = None): self.source = source self.source_type = None if self.source is not None: self.source_type = self.DatFile if ("/" in source or "\\" in source) else self.AirfoilTools self.num_header_rows = num_header_rows self.delimiter = delimiter if self.source is None: self.points = np.array(points) if isinstance(points, list) else points else: self.points = self._get_original_coords() self.color = color self.lw = lw super().__init__(sub_container="reference") name = "RefPoly-1" if name is None else name self.set_name(name)
def _get_original_coords(self): if self.source_type == self.AirfoilTools: return self._load_coords_from_airfoil_tools() elif self.source_type == self.DatFile: try: return self._load_coords_from_dat_file(self.num_header_rows, self.delimiter) except: self.num_header_rows = 1 return self._load_coords_from_dat_file(self.num_header_rows, self.delimiter) else: raise ValueError("Invalid polyline source type") def _load_coords_from_dat_file(self, num_header_rows: int = 0, delimiter: str or None = None): return np.loadtxt(self.source, skiprows=num_header_rows, delimiter=delimiter) def _load_coords_from_airfoil_tools(self): return extract_data_from_airfoiltools(self.source) def update(self): if self.canvas_item is not None: self.canvas_item.setData(self.points[:, 0], self.points[:, 1]) def set_color(self, color): self.color = color self.canvas_item.setPen(pg.mkPen(color=color, lw=self.lw))
[docs] def get_dict_rep(self) -> dict: return {"points": self.points.tolist(), "color": self.color, "lw": self.lw}