diff --git a/pymead/core/airfoil.py b/pymead/core/airfoil.py deleted file mode 100644 index 1810447a..00000000 --- a/pymead/core/airfoil.py +++ /dev/null @@ -1,1058 +0,0 @@ -import numpy as np -from pymead.core.anchor_point import AnchorPoint -from pymead.core.free_point import FreePoint -from pymead.core.base_airfoil_params import BaseAirfoilParams -from pymead.core.bezier import Bezier -from pymead.core.trailing_edge_point import TrailingEdgePoint -from pymead.utils.increment_string_index import max_string_index_plus_one -from pymead.utils.transformations import translate_matrix, rotate_matrix, scale_matrix -from pymead.utils.downsampling_schemes import fractal_downsampler2 -from pymead.core.transformation import Transformation2D -import matplotlib.pyplot as plt -from shapely.geometry import Polygon, LineString, Point -from copy import deepcopy -from pymead import DATA_DIR -import os -import subprocess -import pandas as pd - - -class Airfoil: - """A class for Bézier-parametrized airfoil creation.""" - def __init__(self, - number_coordinates: int = 300, - base_airfoil_params: BaseAirfoilParams = None, - tag: str = None - ): - """ - Parameters - __________ - - number_coordinates : int - Represents the number of discrete \\(x\\) - \\(y\\) coordinate pairs in each Bézier curve. Gets \\ - passed to a :class:`pymead.core.bezier.Bezier` object. - - base_airfoil_params: BaseAirfoilParams - Defines the base set of parameters to be used (chord length, angle of attack, leading edge parameters, \\ - and trailing edge parameters) - - tag: str - Specifies the name of the Airfoil - """ - - self.nt = number_coordinates - self.tag = tag - self.mea = None - self.base_airfoil_params = base_airfoil_params - if not self.base_airfoil_params: - self.base_airfoil_params = BaseAirfoilParams(airfoil_tag=self.tag) - - self.param_dicts = {'Base': {}, 'AnchorPoints': {}, 'FreePoints': {'te_1': {}, 'le': {}}} - - self.c = self.base_airfoil_params.c - self.alf = self.base_airfoil_params.alf - self.R_le = self.base_airfoil_params.R_le - self.L_le = self.base_airfoil_params.L_le - self.r_le = self.base_airfoil_params.r_le - self.phi_le = self.base_airfoil_params.phi_le - self.psi1_le = self.base_airfoil_params.psi1_le - self.psi2_le = self.base_airfoil_params.psi2_le - self.L1_te = self.base_airfoil_params.L1_te - self.L2_te = self.base_airfoil_params.L2_te - self.theta1_te = self.base_airfoil_params.theta1_te - self.theta2_te = self.base_airfoil_params.theta2_te - self.t_te = self.base_airfoil_params.t_te - self.r_te = self.base_airfoil_params.r_te - self.phi_te = self.base_airfoil_params.phi_te - self.dx = self.base_airfoil_params.dx - self.dy = self.base_airfoil_params.dy - - for bp in ['c', 'alf', 'R_le', 'L_le', 'r_le', 'phi_le', 'psi1_le', 'psi2_le', 'L1_te', 'L2_te', 'theta1_te', - 'theta2_te', 't_te', 'r_te', 'phi_te', 'dx', 'dy']: - self.param_dicts['Base'][bp] = getattr(self, bp) - - self.control_point_array = None - self.n_control_points = None - self.curve_list = None - self.curve_list_generated = None - - # # Ensure that all the trailing edge parameters are no longer active if the trailing edge thickness is set to 0.0 - # if self.t_te.value == 0.0: - # self.r_te.active = False - # self.phi_te.active = False - - self.coords = None - self.non_transformed_coords = None - self.curvature = None - self.area = None - self.min_radius = None - self.x_thickness = None - self.thickness = None - self.max_thickness = None - - self.curvature_combs_active = False - self.curvature_scale_factor = None - self.normalized_curvature_scale_factor = None - self.plt_normals = None - self.plt_comb_curves = None - - self.Cl = None - self.Cp = None - - self.needs_update = True - - self.transformed_anchor_points = None - self.anchor_point_order = ['te_1', 'le', 'te_2'] - self.free_points = {'te_1': {}, 'le': {}} - self.free_point_order = {'te_1': [], 'le': []} - self.anchor_point_array = np.array([]) - - self.N = { - 'te_1': 4, - 'le': 4 - } - - self.anchor_points = self.base_airfoil_params.generate_main_anchor_points() - self.control_points = [] - - self.airfoil_graph = None - - self.update() - - def __getstate__(self): - # Reimplemented to ensure MEA picklability - # Do not need to reimplement __setstate__ for unpickling because the __setstate__ reimplementation in - # pymead.core.mea.MEA re-adds the airfoil_graph (and thus pg_curve_handle) objects to each airfoil - state = self.__dict__.copy() - for curve in state['curve_list']: - if hasattr(curve, 'pg_curve_handle'): - # curve.pg_curve_handle.clear() - curve.pg_curve_handle = None # Delete unpicklable PlotDataItem object from state - state['airfoil_graph'] = None # Delete GraphItem object from state (contains several unpicklable object) - return state - - def insert_free_point(self, free_point: FreePoint): - """Method used to insert a :class:`pymead.core.free_point.FreePoint` into an already instantiated - :class:`pymead.core.airfoil.Airfoil`. - - Parameters - ========== - free_point: FreePoint - FreePoint to add to a Bézier curve - """ - fp_dict = self.free_points[free_point.anchor_point_tag] - # free_point.x.x = True - free_point.xy.airfoil_tag = self.tag - free_point.xy.mea = self.mea - # free_point.y.y = True - # free_point.y.airfoil_tag = self.tag - free_point.airfoil_transformation = {'dx': self.dx, 'dy': self.dy, 'alf': self.alf, 'c': self.c} - - # Name the FreePoint by incrementing the max of the FreePoint tag indexes by one (or use 0 if no FreePoints) - if not free_point.tag: - free_point.set_tag(max_string_index_plus_one(self.free_point_order[free_point.anchor_point_tag])) - - if free_point.anchor_point_tag in self.free_points.keys(): - fp_dict[free_point.tag] = free_point - self.free_points[free_point.anchor_point_tag] = fp_dict - self.free_point_order[free_point.anchor_point_tag].insert( - self.free_point_order[free_point.anchor_point_tag].index(free_point.previous_free_point) + 1 if free_point. - previous_free_point else 0, free_point.tag) - self.N[free_point.anchor_point_tag] += 1 - self.param_dicts['FreePoints'][free_point.anchor_point_tag][free_point.tag] = { - 'xy': free_point.xy} - - def delete_free_point(self, free_point_tag: str, anchor_point_tag: str): - """Deletes a :class:`pymead.core.free_point.FreePoint` from the Airfoil. - - Parameters - ========== - free_point_tag: str - Label identifying the FreePoint from a dictionary - - anchor_point_tag: str - Label identifying the FreePoint's previous AnchorPoint from a dictionary - """ - self.free_points[anchor_point_tag].pop(free_point_tag) - self.param_dicts['FreePoints'][anchor_point_tag].pop(free_point_tag) - self.free_point_order[anchor_point_tag].remove(free_point_tag) - self.N[anchor_point_tag] -= 1 - - def insert_anchor_point(self, ap: AnchorPoint): - """Method used to insert a :class:`pymead.core.anchor_point.AnchorPoint` into an already instantiated - :class:`pymead.core.airfoil.Airfoil`. - - Parameters - ========== - ap: AnchorPoint - AnchorPoint to add to a Bézier curve - """ - ap.xy.airfoil_tag = self.tag - order_idx = next((idx for idx, anchor_point in enumerate(self.anchor_point_order) - if anchor_point == ap.previous_anchor_point)) - self.anchor_points[order_idx + 1].previous_anchor_point = ap.tag - self.anchor_point_order.insert(order_idx + 1, ap.tag) - self.anchor_points.insert(order_idx + 1, ap) - if self.anchor_point_order[order_idx + 2] == 'te_2': - self.N[ap.tag] = 4 - self.N[self.anchor_point_order[order_idx]] = 5 - else: - self.N[ap.tag] = 5 - if ap.previous_anchor_point == 'le': - self.N['le'] = 5 + len(self.free_point_order['le']) - ap.airfoil_transformation = {'c': self.c, 'alf': self.alf, 'dx': self.dx, 'dy': self.dy} - ap_param_list = ['xy', 'L', 'R', 'r', 'phi', 'psi1', 'psi2'] - self.param_dicts['AnchorPoints'][ap.tag] = {p: getattr(ap, p) for p in ap_param_list} - for p in ap_param_list: - getattr(ap, p).mea = self.mea - self.free_points[ap.tag] = {} - self.free_point_order[ap.tag] = [] - self.param_dicts['FreePoints'][ap.tag] = {} - - def delete_anchor_point(self, anchor_point_tag: str): - """Deletes a :class:`pymead.core.anchor_point.AnchorPoint` from the Airfoil. - - Parameters - ========== - anchor_point_tag: str - Label identifying the AnchorPoint - """ - self.anchor_points = [ap for ap in self.anchor_points if ap.tag != anchor_point_tag] - self.param_dicts['AnchorPoints'].pop(anchor_point_tag) - self.param_dicts['FreePoints'].pop(anchor_point_tag) - current_curve = self.curve_list[self.anchor_point_order.index(anchor_point_tag)] - if current_curve.pg_curve_handle: - current_curve.clear_curve_pg() - self.curve_list.pop(self.anchor_point_order.index(anchor_point_tag)) - current_ap_order_index = self.anchor_point_order.index(anchor_point_tag) - next_ap_tag = self.anchor_point_order[current_ap_order_index + 1] - next_ap = next((ap for ap in self.anchor_points if ap.tag == next_ap_tag), None) - next_ap.previous_anchor_point = self.anchor_point_order[current_ap_order_index - 1] - self.anchor_point_order.remove(anchor_point_tag) - self.free_point_order.pop(anchor_point_tag) - self.N.pop(anchor_point_tag) - - def update(self, skip_fp_ap_regen: bool = False, generate_curves: bool = True): - """Used to update the state of the airfoil, including the Bézier curves, after a change in any parameter - - Parameters - ========== - generate_curves: bool - Determines whether the curves should be re-generated during the update - """ - # Translate back to origin if not already at origin - if self.control_points is not None and self.control_points != []: - self.translate(-self.dx.value, -self.dy.value) - - # Rotate to zero degree angle of attack - if self.control_points is not None and self.control_points != []: - self.rotate(self.alf.value) - - # Scale so that the chord length is equal to 1.0 - if self.control_points is not None and self.control_points != []: - self.scale(1 / self.c.value) - - if not skip_fp_ap_regen: - # Generate anchor point branches - for ap in self.anchor_points: - if isinstance(ap, AnchorPoint): - ap.set_degree_adjacent_bezier_curves(self.N[ap.previous_anchor_point], self.N[ap.tag]) - ap.generate_anchor_point_branch(self.anchor_point_order) - elif isinstance(ap, TrailingEdgePoint): - ap.generate_anchor_point_branch() # trailing edge anchor points do not need to know the ap order - - # Get the control points from all the anchor points - self.control_points = [] - for ap_tag in self.anchor_point_order: - self.control_points.extend(next((ap.ctrlpt_branch_list for ap in self.anchor_points if ap.tag == ap_tag))) - - # Update the FreePoints - for key, fp_dict in self.free_points.items(): - if len(fp_dict) > 0: - if key == 'te_1': - insertion_index = 2 - else: - insertion_index = next((idx for idx, cp in enumerate(self.control_points) - if cp.cp_type == 'g2_plus' and cp.anchor_point_tag == key)) + 1 - self.control_points[insertion_index:insertion_index] = [fp_dict[k].ctrlpt for k in self.free_point_order[key]] - - # Scale airfoil by chord length - self.scale(self.c.value) - - # Rotate by airfoil angle of attack (alf) - self.rotate(-self.alf.value) - - # Translate by airfoil dx, dy - self.translate(self.dx.value, self.dy.value) - - # Get the control point array - self.update_control_point_array() - - # Generate the Bezier curves - if generate_curves: - self.generate_curves() - - def generate_curves(self): - """Generates the Bézier curves from the control point array""" - # Make Bézier curves from the control point array - self.curve_list_generated = True - previous_number_of_curves = 0 - if self.curve_list is None or len(self.curve_list) != len(self.anchor_point_order) - 1: - self.curve_list = [] - self.curve_list_generated = False - else: - previous_number_of_curves = len(self.curve_list) - - P_list = self.get_list_of_control_point_arrays() - for idx, P in enumerate(P_list): - if self.curve_list_generated and previous_number_of_curves == len(self.anchor_point_order) - 1: - self.curve_list[idx].update(P, 150) - else: - self.curve_list.append(Bezier(P, 150)) - self.curve_list_generated = True - - self.n_control_points = len(self.control_points) - - def get_list_of_control_point_arrays(self): - """Converts the control point array for the Airfoil into a separate control point array for each Bézier curve - - Returns - ======= - list - A list of control point arrays for each Bézier curve - """ - P_list = [] - cp_end_idx, cp_start_idx = 0, 1 - for idx, ap_tag in enumerate(self.anchor_point_order[:-1]): - if idx == 0: - cp_end_idx += self.N[ap_tag] + 1 - else: - cp_end_idx += self.N[ap_tag] - P = self.control_point_array[cp_start_idx - 1:cp_end_idx] - P_list.append(P) - cp_start_idx = deepcopy(cp_end_idx) - return P_list - - def update_control_point_array(self): - r"""Updates the control point array from the list of :class:`pymead.core.control_point.ControlPoint` objects - - Returns - ======= - np.ndarray - 2D array of control points (each row is a different control point, and the two columns are :math:`x` and - :math:`y`) - """ - new_control_points = [] - for cp in self.control_points: - if (cp.cp_type == 'anchor_point' and cp.tag not in ['te_1', 'le', 'te_2']) or cp.cp_type == 'free_point': - new_control_points.append([cp.x_val, cp.y_val]) - else: - new_control_points.append([cp.xp, cp.yp]) - # new_control_points = [[cp.xp, cp.yp] for cp in self.control_points] - self.control_point_array = np.array(new_control_points) - return self.control_point_array - - def translate(self, dx: float, dy: float): - r"""Translates all the control points and anchor points by :math:`\Delta x` and :math:`\Delta y`. - - Parameters - ========== - dx: float - :math:`x`-direction translation magnitude - - dy: float - :math:`y`-direction translation magnitude - """ - for cp in self.control_points: - if cp.tag == 'le': - cp.xp = self.dx.value - cp.yp = self.dy.value - else: - # if not ('anchor_point' in cp.tag and all(t not in cp.tag for t in ['te_1', 'le', 'te_2'])): - cp.xp += dx - cp.yp += dy - - def rotate(self, angle: float): - """Rotates all the control points and anchor points by a specified angle. Used to implement the angle of attack. - - Parameters - ========== - angle: float - Angle (in radians) by which to rotate the airfoil. - """ - rot_mat = np.array([[np.cos(angle), -np.sin(angle)], - [np.sin(angle), np.cos(angle)]]) - for cp in self.control_points: - # if not ('anchor_point' in cp.tag and all(t not in cp.tag for t in ['te_1', 'le', 'te_2'])): - rotated_point = (rot_mat @ np.array([[cp.xp], [cp.yp]])).flatten() - cp.xp = rotated_point[0] - cp.yp = rotated_point[1] - - def scale(self, scale_value): - """ - Scales the airfoil about the origin. - - Parameters - ========== - scale_value: float - A value by which to scale the airfoil uniformly in both the :math:`x`- and :math:`y`-directions - """ - for cp in self.control_points: - # if not ('anchor_point' in cp.tag and all(t not in cp.tag for t in ['te_1', 'le', 'te_2'])): - cp.xp *= scale_value - cp.yp *= scale_value - - def compute_area(self): - """Computes the area of the airfoil as the area of a many-sided polygon enclosed by the airfoil coordinates - using the `shapely `_ library. - - Returns - ======= - float - The area of the airfoil - """ - if self.needs_update: - self.update() - points_shapely = list(map(tuple, self.coords)) - polygon = Polygon(points_shapely) - area = polygon.area - self.area = area - return area - - def compute_min_radius(self): - """ - Computes the minimum radius of curvature for the airfoil. - - Returns - ======= - float - The minimum radius of curvature for the airfoil - """ - if self.needs_update: - self.update() - self.min_radius = np.array([c.R_abs_min for c in self.curve_list]).min() - return self.min_radius - - def check_self_intersection(self): - """Determines whether the airfoil intersects itself using the `is_simple()` function of the - `shapely `_ library. - - Returns - ======= - bool - Describes whether the airfoil intersects itself - """ - if self.needs_update: - # print("Calling update in self intersection!") - self.update() - self.get_coords(body_fixed_csys=True) - points_shapely = list(map(tuple, self.coords)) - line_string = LineString(points_shapely) - is_simple = line_string.is_simple - return not is_simple - - def compute_thickness(self, n_lines: int = 201, return_max_thickness_loc: bool = False): - r"""Calculates the thickness distribution and maximum thickness of the airfoil. - - Parameters - ========== - n_lines: int - Describes the number of lines evenly spaced along the chordline produced to determine the thickness - distribution. Default: :code:`201` - - return_max_thickness_loc: bool - Whether to return the :math:`x/c`-location of the maximum thickness. Return type will be a :code:`dict` - rather than a :code:`tuple` if this value is selected to be :code:`True`. Default: :code:`False` - - Returns - ======= - tuple or dict - The list of \(x\)-values used for the thickness distribution calculation, the thickness distribution, the - maximum value of the thickness distribution, and, if :code:`return_max_thickness_location=True`, - the :math:`x/c`-location of the maximum thickness value. - """ - self.get_coords(body_fixed_csys=True) - points_shapely = list(map(tuple, self.coords)) - airfoil_line_string = LineString(points_shapely) - x_thickness = np.linspace(0.0, 1.0, n_lines) - thickness = [] - for idx in range(n_lines): - line_string = LineString([(x_thickness[idx], -1), (x_thickness[idx], 1)]) - x_inters = line_string.intersection(airfoil_line_string) - if x_inters.is_empty: - thickness.append(0.0) - else: - thickness.append(x_inters.convex_hull.length) - self.x_thickness = x_thickness - self.thickness = thickness - self.max_thickness = max(thickness) - if return_max_thickness_loc: - x_c_loc_idx = np.argmax(thickness) - x_c_loc = self.x_thickness[x_c_loc_idx] - return { - 'x/c': self.x_thickness, - 't/c': self.thickness, - 't/c_max': self.max_thickness, - 't/c_max_x/c_loc': x_c_loc - } - else: - return self.x_thickness, self.thickness, self.max_thickness - - def compute_thickness_at_points(self, x_over_c: float or list or np.ndarray, start_y_over_c=-1.0, end_y_over_c=1.0): - """Calculates the thickness (t/c) at a set of x-locations (x/c) - - Parameters - ========== - x_over_c: float or list or np.ndarray - The :math:`x/c` locations at which to evaluate the thickness - - start_y_over_c: float - The :math:`y/c` location to draw the first point in a line whose intersection with the airfoil is checked. May - need to decrease this value for unusually thick airfoils - - end_y_over_c: float - The :math:`y/c` location to draw the last point in a line whose intersection with the airfoil is checked. May - need to increase this value for unusually thick airfoils - - Returns - ======= - np.ndarray - An array of thickness (:math:`t/c`) values corresponding to the input :math:`x/c` values - """ - # If x_over_c is not iterable (i.e., just a float), convert to list - if not hasattr(x_over_c, '__iter__'): - x_over_c = [x_over_c] - - self.get_coords(body_fixed_csys=True) # Get the airfoil coordinates - points_shapely = list(map(tuple, self.coords)) # Convert the coordinates to Shapely input format - airfoil_line_string = LineString(points_shapely) # Create a LineString from the points - thickness = np.array([]) - for pt in x_over_c: - line_string = LineString([(pt, start_y_over_c), (pt, end_y_over_c)]) - x_inters = line_string.intersection(airfoil_line_string) - if pt == 1.0: - thickness = np.append(thickness, self.t_te.value) - elif x_inters.is_empty: # If no intersection between line and airfoil LineString, - thickness = np.append(thickness, 0.0) - else: - thickness = np.append(thickness, x_inters.convex_hull.length) - return thickness # Return an array of t/c values corresponding to the x/c locations - - def compute_camber_at_points(self, x_over_c: float or list or np.ndarray, start_y_over_c=-1.0, end_y_over_c=1.0): - """Calculates the thickness (t/c) at a set of x-locations (x/c) - - Parameters - ========== - x_over_c: float or list or np.ndarray - The :math:`x/c` locations at which to evaluate the camber - - start_y_over_c: float - The :math:`y/c` location to draw the first point in a line whose intersection with the airfoil is checked. May - need to decrease this value for unusually thick airfoils - - end_y_over_c: float - The :math:`y/c` location to draw the last point in a line whose intersection with the airfoil is checked. May - need to increase this value for unusually thick airfoils - - Returns - ======= - np.ndarray - An array of thickness (:math:`t/c`) values corresponding to the input :math:`x/c` values - """ - # If x_over_c is not iterable (i.e., just a float), convert to list - if not hasattr(x_over_c, '__iter__'): - x_over_c = [x_over_c] - - self.get_coords(body_fixed_csys=True) # Get the airfoil coordinates - points_shapely = list(map(tuple, self.coords)) # Convert the coordinates to Shapely input format - airfoil_line_string = LineString(points_shapely) # Create a LineString from the points - camber = np.array([]) - for pt in x_over_c: - line_string = LineString([(pt, start_y_over_c), (pt, end_y_over_c)]) - x_inters = line_string.intersection(airfoil_line_string) - if pt == 0.0 or pt == 1.0 or x_inters.is_empty: - camber = np.append(camber, 0.0) - else: - camber = np.append(camber, x_inters.convex_hull.centroid.xy[1]) - return camber # Return an array of h/c values corresponding to the x/c locations - - def contains_point(self, point: np.ndarray or list): - """Determines whether a point is contained inside the airfoil - - Parameters - ========== - point: np.ndarray or list - The point to test. Should be either a 1-D :code:`ndarray` of the format :code:`array([,])` or a - list of the format :code:`[,]` - - Returns - ======= - bool - Whether the point is contained inside the airfoil - """ - if isinstance(point, list): - point = np.array(point) - self.get_coords(body_fixed_csys=False) - points_shapely = list(map(tuple, self.coords)) - polygon = Polygon(points_shapely) - return polygon.contains(Point(point[0], point[1])) - - def contains_line_string(self, points: np.ndarray or list) -> bool: - """Whether a connected string of points is contained the airfoil - - Parameters - ========== - points: np.ndarray or list - Should be a 2-D array or list of the form :code:`[[, ], [, ], ...]` - - Returns - ======= - bool - Whether the line string is contained inside the airfoil - """ - if isinstance(points, list): - points = np.array(points) - self.get_coords(body_fixed_csys=False) - points_shapely = list(map(tuple, self.coords)) - polygon = Polygon(points_shapely) - line_string = LineString(list(map(tuple, points))) - return polygon.contains(line_string) - - def within_line_string_until_point(self, points: np.ndarray or list, cutoff_point, - **transformation_kwargs) -> bool: - """Whether the airfoil is contained inside a connected string of points until a cutoff point - - Parameters - ========== - points: np.ndarray or list - Should be a 2-D array or list of the form :code:`[[, ], [, ], ...]` - - cutoff_point: float - The :math:`x`-location to set the end of the constraint - - **transformation_kwargs - Keyword arguments to be fed into a transformation function to transform the airfoil prior to the line string - containment check - - Returns - ======= - bool - Whether the airfoil is contained inside the connected string of points before the cutoff point - """ - if isinstance(points, list): - points = np.array(points) - points_shapely = list(map(tuple, points)) - exterior_polygon = Polygon(points_shapely) - - self.get_coords(body_fixed_csys=True) - airfoil_points = self.coords[self.coords[:, 0] < cutoff_point, :] - transform2d = Transformation2D(**transformation_kwargs) - airfoil_points = transform2d.transform(airfoil_points) - line_string = LineString(list(map(tuple, airfoil_points))) - - return exterior_polygon.contains(line_string) - - def plot_airfoil(self, axs: plt.axes, **plot_kwargs): - """Plots each of the airfoil's Bézier curves on a specified matplotlib axis - - Parameters - ========== - axs: plt.axes - A :code:`matplotlib.axes.Axes` object on which to plot each of the airfoil's Bézier curves - - **plot_kwargs - Arguments to feed to the `matplotlib` "plot" function - - Returns - ======= - list - A list of the `matplotlib` plot handles - """ - plt_curves = [] - for curve in self.curve_list: - plt_curve = curve.plot_curve(axs, **plot_kwargs) - plt_curves.append(plt_curve) - return plt_curves - - def plot_control_points(self, axs: plt.axes, **plot_kwargs): - """Plots the airfoil's control point skeleton on a specified `matplotlib` axis - - Parameters - ========== - axs: plt.axes - A :code:`matplotlib.axes.Axes` object on which to plot each of the airfoil's control point skeleton - - **plot_kwargs - Arguments to feed to the `matplotlib` "plot" function - """ - axs.plot(self.control_point_array[:, 0], self.control_point_array[:, 1], **plot_kwargs) - - def init_airfoil_curve_pg(self, v, pen): - """Initializes the `pyqtgraph.PlotDataItem` for each of the airfoil's Bézier curves - - Parameters - ========== - v - The `pyqtgraph` axis on which to draw the airfoil - - pen: QPen - The pen to use to draw the airfoil curves - """ - for curve in self.curve_list: - curve.init_curve_pg(v, pen) - - def set_airfoil_pen(self, pen): - """Sets the QPen for each curve in the airfoil object - """ - for curve in self.curve_list: - if curve.pg_curve_handle: - curve.pg_curve_handle.setPen(pen) - - def update_airfoil_curve(self): - """Updates each airfoil `matplotlib` axis curve handle""" - for curve in self.curve_list: - curve.update_curve() - - def update_airfoil_curve_pg(self): - """Updates each airfoil `pyqtgraph` axis curve handle""" - for curve in self.curve_list: - curve.update_curve_pg() - - def get_coords(self, body_fixed_csys: bool = False, as_tuple: bool = False, downsample: bool = False, - ds_max_points: int or None = None, ds_curve_exp: float = None): - """Gets the set of discrete airfoil coordinates for the airfoil - - Parameters - ========== - body_fixed_csys: bool - Whether to internally transform the airfoil such that :math:`(0,0)` is located at the leading edge and - :math:`(1,0)` is located at the trailing edge prior to the coordinate output - - as_tuple: bool - Whether to return the airfoil coordinates as a tuple (array returned if False) - - Returns - ======= - np.ndarray or tuple - A 2-D array or tuple of the airfoil coordinates - """ - x = np.array([]) - y = np.array([]) - self.coords = [] - original_t = [] - new_t_list = None - - if downsample: - for c_idx, curve in enumerate(self.curve_list): - original_t.append(deepcopy(curve.t)) - new_t_list = self.downsample(max_airfoil_points=ds_max_points, curvature_exp=ds_curve_exp) - for idx, curve in enumerate(self.curve_list): - if downsample: - curve.update(curve.P, t=new_t_list[idx]) - if idx == 0: - x = curve.x - y = curve.y - else: - x = np.append(x, curve.x[1:]) - y = np.append(y, curve.y[1:]) - self.coords = np.column_stack((x, y)) - if body_fixed_csys: - self.coords = translate_matrix(self.coords, -self.dx.value, -self.dy.value) - self.coords = rotate_matrix(self.coords, self.alf.value) - self.coords = scale_matrix(self.coords, 1 / self.c.value) - - if downsample: - for c_idx, curve in enumerate(self.curve_list): - curve.update(curve.P, t=original_t[c_idx]) - - if as_tuple: - return tuple(map(tuple, self.coords)) - else: - return self.coords - - def write_coords_to_file(self, f: str, read_write_mode: str, body_fixed_csys: bool = False, - scale_factor: float = None, downsample: bool = False, ratio_thresh=None, - abs_thresh=None) -> int: - """Writes the coordinates to a file. - - Parameters - ========== - f: str - The file in which to write the coordinates - - read_write_mode: str - Use 'w' to write to a new file, or 'a' to append to an existing file - - body_fixed_csys: bool - Whether to internally transform the airfoil such that :math:`(0,0)` is located at the leading edge and - :math:`(1,0)` is located at the trailing edge prior to the coordinate output. Default: `False` - - scale_factor: float - A value by which to internally scale the airfoil uniformly in the :math:`x`- and :math:`y`-directions - prior to writing the coordinates. Default: `None` - - downsample: bool - Whether to downsample the airfoil coordinates before writing to the file. Default: `False` - - ratio_thresh: float - The threshold ratio used by the downsampler (`1.001` by default, ignored if `downsample=False`) - - abs_thresh: float - The absolute threshold used by the downsampler (`0.1` by default, ignored if `downsample=False`) - - Returns - ======= - int - The number of airfoil coordinates - """ - self.get_coords(body_fixed_csys) - if downsample: - ds = fractal_downsampler2(self.coords, ratio_thresh=ratio_thresh, abs_thresh=abs_thresh) - n_data_pts = len(ds) - with open(f, read_write_mode) as coord_file: - for row in ds: - coord_file.write(f"{row[0]} {row[1]}\n") - else: - n_data_pts = len(self.coords) - if scale_factor is not None: - with open(f, read_write_mode) as coord_file: - for row in self.coords * scale_factor: - coord_file.write(f"{row[0]} {row[1]}\n") - else: - with open(f, read_write_mode) as coord_file: - for row in self.coords: - coord_file.write(f"{row[0]} {row[1]}\n") - return n_data_pts - - def read_Cl_from_file(self, f: str): - with open(f, 'r') as Cl_file: - line = Cl_file.readline() - str_Cl = '' - for ch in line: - if ch.isdigit() or ch in ['.', 'e', 'E', '-']: - str_Cl += ch - self.Cl = float(str_Cl) - return self.Cl - - def read_Cp_from_file(self, f: str): - df = pd.read_csv(f, names=['x/c', 'Cp']) - self.Cp = df.to_numpy() - return self.Cp - - def calculate_Cl_Cp(self, alpha, tool: str = 'panel_fort'): - """ - Calculates the lift coefficient and surface pressure coefficient distribution for the Airfoil. - Note that the angle of attack (alpha) should be entered in degrees. - """ - tool_list = ['panel_fort', 'XFOIL', 'MSES'] - if tool not in tool_list: - raise ValueError(f"\'tool\' must be one of {tool_list}") - coord_file_name = 'airfoil_coords_ClCp_calc.dat' - f = os.path.join(DATA_DIR, coord_file_name) - n_data_pts = self.write_coords_to_file(f, 'w') - if tool == 'panel_fort': - subprocess.run((["panel_fort", DATA_DIR, coord_file_name, str(n_data_pts - 1), str(alpha)]), - stdout=subprocess.DEVNULL) - self.read_Cl_from_file(os.path.join(DATA_DIR, 'LIFT.dat')) - self.read_Cp_from_file(os.path.join(DATA_DIR, 'CPLV.DAT')) - elif tool == 'XFOIL': - subprocess.run((['xfoil', os.path.join(DATA_DIR, coord_file_name)])) - - def plot_control_point_skeleton(self, axs: plt.Axes, **plot_kwargs): - """ - Plots the control points, in counter-clockwise order, without duplicates. - - Parameters - ========== - axs: plt.Axes - Matplotlib Axes on which to plot the control points - - Returns - ======= - list[Line2D] - Matplotlib plot handle list - """ - return axs.plot(self.control_point_array[:, 0], self.control_point_array[:, 1], **plot_kwargs) - - def set_curvature_scale_factor(self, scale_factor: float or None = None): - """ - Sets the curvature scale factor used for curvature comb plotting. - - Parameters - ========== - scale_factor: float or None - If of type float, assign the scale factor and calculate the scale factor normalized by the maximum - curvature point on the airfoil. If ``None`` and there is already an assigned curvature scale factor, - use that value. Otherwise, return an error. - """ - if scale_factor is None and self.curvature_scale_factor is None: - raise ValueError('Curvature scale factor not initialized for airfoil!') - if scale_factor is not None: - self.curvature_scale_factor = scale_factor # otherwise just use the scale factor that is already set - self.normalized_curvature_scale_factor = self.curvature_scale_factor / np.max([np.max(abs(curve.k)) for curve in self.curve_list]) - - def plot_curvature_comb_normals(self, axs: plt.axes, scale_factor, **plot_kwargs): - """ - Plots all the curvature comb normals for each Bézier curve in the airfoil on a specified Matplotlib ``plt.Axes``. - See `this tutorial `__ - for an example of how to call this method for an Airfoil. - - Parameters - ========== - axs: plt.Axes - Matplotlib axis on which to plot the curvature comb - - scale_factor: float - Factor by which to scale the curvature combs. The length of each comb tooth is equal to - ``k_i / max(k) * scale_factor``, where ``k_i`` is the curvature (normalized by the airfoil chord) - at a given airfoil coordinate and ``k`` is the vector of curvature for the curve. Default: 0.1 - - plot_kwargs - Keyword arguments to pass to Matplotlib's plot function (e.g., ``color="blue"``, ``lw=1.5``, etc.) - - Returns - ======= - list - Matplotlib plot handles to the curvature comb - """ - self.set_curvature_scale_factor(scale_factor) - self.plt_normals = [] - for curve in self.curve_list: - plt_normal = curve.plot_curvature_comb_normals(axs, self.normalized_curvature_scale_factor, **plot_kwargs) - self.plt_normals.append(plt_normal) - return self.plt_normals - - def update_curvature_comb_normals(self): - """ - Updates the curvature comb normals for each Bézier curve in the airfoil. - """ - for curve in self.curve_list: - curve.update_curvature_comb_normals() - - def plot_curvature_comb_curve(self, axs: plt.Axes, scale_factor: float = 0.1, **plot_kwargs): - """ - Plots all the curvature combs for each Bézier curve in the airfoil on a specified Matplotlib ``plt.Axes``. - See `this tutorial `__ - for an example of how to call this method for an Airfoil. - - Parameters - ========== - axs: plt.Axes - Matplotlib axis on which to plot the curvature comb - - scale_factor: float - Factor by which to scale the curvature combs. The length of each comb tooth is equal to - ``k_i / max(k) * scale_factor``, where ``k_i`` is the curvature (normalized by the airfoil chord) - at a given airfoil coordinate and ``k`` is the vector of curvature for the curve. Default: 0.1 - - plot_kwargs - Keyword arguments to pass to Matplotlib's plot function (e.g., ``color="blue"``, ``lw=1.5``, etc.) - - Returns - ======= - list - Matplotlib plot handles to the curvature comb - """ - self.set_curvature_scale_factor(scale_factor) - self.plt_comb_curves = [] - for curve in self.curve_list: - plt_comb_curve = curve.plot_curvature_comb_curve(axs, self.normalized_curvature_scale_factor, **plot_kwargs) - self.plt_comb_curves.append(plt_comb_curve) - return self.plt_comb_curves - - def update_curvature_comb_curve(self): - """ - Updates the curvature combs for each Bézier curve in the airfoil. - """ - for curve in self.curve_list: - curve.update_curvature_comb_curve() - - def downsample(self, max_airfoil_points: int, curvature_exp: float = 2.0): - r""" - Downsamples the airfoil coordinates based on a curvature exponent. This method works by evaluating each - Bézier curve using a set number of points (150) and then calculating - :math:`\mathbf{R_e} = \mathbf{R}^{1/e_c}`, where :math:`\mathbf{R}` is the radius of curvature vector and - :math:`e_c` is the curvature exponent (an input to this method). Then, :math:`\mathbf{R_e}` is - normalized by its maximum value and concatenated to a single array for all curves in a given airfoil. - Finally, ``max_airfoil_points`` are chosen from this array to create a new set of parameter vectors - for the airfoil. - - Parameters - ========== - max_airfoil_points: int - Maximum number of points in the airfoil (the actual number in the final airfoil may be slightly less) - - curvature_exp: float - Curvature exponent used to scale the radius of curvature. Values close to 0 place high emphasis on - curvature, while values close to :math:`\infty` place low emphasis on curvature (creating nearly - uniform spacing) - - - Returns - ======= - list[np.ndarray] - List of parameter vectors (one for each Bézier curve) - """ - - if max_airfoil_points > sum([len(curve.t) for curve in self.curve_list]): - for curve in self.curve_list: - curve.update(P=curve.P, nt=np.ceil(max_airfoil_points / len(self.curve_list)).astype(int)) - - new_param_vec_list = [] - new_t_concat = np.array([]) - - for c_idx, curve in enumerate(self.curve_list): - temp_R = deepcopy(curve.R) - for r_idx, r in enumerate(temp_R): - if np.isinf(r) and r > 0: - temp_R[r_idx] = 10000 - elif np.isinf(r) and r < 0: - temp_R[r_idx] = -10000 - - exp_R = np.abs(temp_R) ** (1 / curvature_exp) - new_t = np.zeros(exp_R.shape) - for i in range(1, new_t.shape[0]): - new_t[i] = new_t[i - 1] + (exp_R[i] + exp_R[i - 1]) / 2 - new_t = new_t / np.max(new_t) - new_t_concat = np.concatenate((new_t_concat, new_t)) - - indices_to_select = np.linspace(0, new_t_concat.shape[0] - 1, - max_airfoil_points - 2 * len(self.curve_list)).astype(int) - - t_old = 0.0 - for selection_idx in indices_to_select: - t = new_t_concat[selection_idx] - - if t == 0.0 and selection_idx == 0: - new_param_vec_list.append(np.array([0.0])) - elif t < t_old: - if t_old != 1.0: - new_param_vec_list[-1] = np.append(new_param_vec_list[-1], 1.0) - if t == 0.0: - new_param_vec_list.append(np.array([])) - else: - new_param_vec_list.append(np.array([0.0])) - new_param_vec_list[-1] = np.append(new_param_vec_list[-1], t) - t_old = t - else: - new_param_vec_list[-1] = np.append(new_param_vec_list[-1], t) - t_old = t - - return new_param_vec_list - - def count_airfoil_points(self): - """ - Counts the number of discrete airfoil coordinates based on the current evaluation parameter vector. - - Returns - ======= - int - Number of unique airfoil coordinates - """ - return sum([len(curve.t) for curve in self.curve_list]) - (len(self.curve_list) - 1) - - -if __name__ == '__main__': - airfoil_ = Airfoil() - # airfoil_.calculate_Cl_Cp(5.0) - airfoil_.downsample(70, 2) diff --git a/pymead/core/airfoil2.py b/pymead/core/airfoil2.py index 83c2aa8e..a33b770c 100644 --- a/pymead/core/airfoil2.py +++ b/pymead/core/airfoil2.py @@ -1,3 +1,5 @@ +from copy import deepcopy + import numpy as np from shapely.geometry import Polygon, LineString @@ -322,6 +324,79 @@ def contains_line_string(self, airfoil_polygon: Polygon, points: np.ndarray or l line_string = LineString(list(map(tuple, points))) return airfoil_polygon.contains(line_string) + def downsample(self, max_airfoil_points: int, curvature_exp: float = 2.0): + r""" + Downsamples the airfoil coordinates based on a curvature exponent. This method works by evaluating each + Bézier curve using a set number of points (150) and then calculating + :math:`\mathbf{R_e} = \mathbf{R}^{1/e_c}`, where :math:`\mathbf{R}` is the radius of curvature vector and + :math:`e_c` is the curvature exponent (an input to this method). Then, :math:`\mathbf{R_e}` is + normalized by its maximum value and concatenated to a single array for all curves in a given airfoil. + Finally, ``max_airfoil_points`` are chosen from this array to create a new set of parameter vectors + for the airfoil. + + Parameters + ========== + max_airfoil_points: int + Maximum number of points in the airfoil (the actual number in the final airfoil may be slightly less) + + curvature_exp: float + Curvature exponent used to scale the radius of curvature. Values close to 0 place high emphasis on + curvature, while values close to :math:`\infty` place low emphasis on curvature (creating nearly + uniform spacing) + + + Returns + ======= + list[np.ndarray] + List of parameter vectors (one for each Bézier curve) + """ + + if max_airfoil_points > sum([len(curve.t) for curve in self.curves]): + for curve in self.curves: + curve.update(P=curve.P, nt=np.ceil(max_airfoil_points / len(self.curves)).astype(int)) + + new_param_vec_list = [] + new_t_concat = np.array([]) + + for c_idx, curve in enumerate(self.curves): + temp_R = deepcopy(curve.R) + for r_idx, r in enumerate(temp_R): + if np.isinf(r) and r > 0: + temp_R[r_idx] = 10000 + elif np.isinf(r) and r < 0: + temp_R[r_idx] = -10000 + + exp_R = np.abs(temp_R) ** (1 / curvature_exp) + new_t = np.zeros(exp_R.shape) + for i in range(1, new_t.shape[0]): + new_t[i] = new_t[i - 1] + (exp_R[i] + exp_R[i - 1]) / 2 + new_t = new_t / np.max(new_t) + new_t_concat = np.concatenate((new_t_concat, new_t)) + + indices_to_select = np.linspace(0, new_t_concat.shape[0] - 1, + max_airfoil_points - 2 * len(self.curves)).astype(int) + + t_old = 0.0 + for selection_idx in indices_to_select: + t = new_t_concat[selection_idx] + + if t == 0.0 and selection_idx == 0: + new_param_vec_list.append(np.array([0.0])) + elif t < t_old: + if t_old != 1.0: + new_param_vec_list[-1] = np.append(new_param_vec_list[-1], 1.0) + if t == 0.0: + new_param_vec_list.append(np.array([])) + else: + new_param_vec_list.append(np.array([0.0])) + new_param_vec_list[-1] = np.append(new_param_vec_list[-1], t) + t_old = t + else: + new_param_vec_list[-1] = np.append(new_param_vec_list[-1], t) + t_old = t + + return new_param_vec_list + def get_dict_rep(self): return {"leading_edge": self.leading_edge.name(), "trailing_edge": self.trailing_edge.name(), "upper_surf_end": self.upper_surf_end.name(), "lower_surf_end": self.lower_surf_end.name()} diff --git a/pymead/core/anchor_point.py b/pymead/core/anchor_point.py deleted file mode 100644 index c59057d1..00000000 --- a/pymead/core/anchor_point.py +++ /dev/null @@ -1,651 +0,0 @@ -import numpy as np -import typing - -from pymead.core.param import Param -from pymead.core.pos_param import PosParam -from pymead.core.control_point import ControlPoint -from pymead.utils.transformations import transform_matrix -from pymead.core.transformation import AirfoilTransformation - - -class AnchorPoint(ControlPoint): - """ - - """ - - def __init__(self, - xy: PosParam, - tag: str, - previous_anchor_point: str, - airfoil_tag: str, - L: Param, - R: Param, - r: Param, - phi: Param, - psi1: Param, - psi2: Param): - r""" - The :class:`AnchorPoint` in :class:`pymead` is the way to split a Bézier curve within an - :class:`pymead.core.airfoil.Airfoil` into two Bézier curves and satisfy :math:`G^0`, :math:`G^1`, and - :math:`G^2` continuity at the joint between the curves. Examples of implemented :class:`AnchorPoint`\ s in an - unnecessarily strange airfoil shape are shown in the image below (click on the image to zoom). - - .. image:: ../images/complex_1.png - :width: 400 - :align: center - - Parameters - ========== - - xy: PosParam - :math:`x`- and :math:`y`-location of the :class:`AnchorPoint` - - previous_anchor_point: str - Name of the previous :class:`AnchorPoint` (counter-clockwise ordering) - - L: Param - Distance between the control points before and after the :class:`AnchorPoint` - - R: Param - The radius of curvature at the location of the :class:`AnchorPoint`. A positive value makes the airfoil - convex at the :class:`AnchorPoint` location, and a negative value makes the airfoil concave at the - :class:`AnchorPoint` location. A value of ``0`` creates a flat-plate-type leading edge. The valid range is - :math:`R \in [-\infty, \infty]`. Inclusive brackets are used here because setting :math:`R=\pm \infty` - is valid and creates an anchor point with no curvature (infinite radius of curvature). - - r: Param - The ratio of the distance from the :class:`AnchorPoint` location to the neighboring control point closest to - the trailing edge to the distance between the :class:`AnchorPoint`'s neighboring control points - ( :math:`L_{fore} / L` ). The valid range is :math:`r \in (0,1)`. - - phi: Param - The angle of the line passing through the :class:`AnchorPoint`'s neighboring control points, referenced - counter-clockwise from the chordline if the :class:`AnchorPoint` is on the airfoil's upper surface and - clockwise from the chordline if the :class:`AnchorPoint` is on the airfoil's lower surface. The valid - range is :math:`\psi_1 \in [-180^{\circ},180^{\circ}]`. A value of :math:`0^{\circ}` creates an anchor point - with local slope equal to the slope of the chordline. - - psi1: Param - The angle of the aft curvature control "arm." Regardless of the sign of :math:`R` or which surface the - :class:`AnchorPoint` lies on, an angle of :math:`90^{\circ}` always means that the curvature control arm - points perpendicular to the line passing through the neighboring control points of the :class:`AnchorPoint`. - Angles below :math:`90^{\circ}` "tuck" the arms in, and angles above :math:`90^{\circ}` "spread" the arms - out. The valid range is :math:`\psi_1 \in [0^{\circ},180^{\circ}]`. - - psi2: Param - The angle of the fore curvature control "arm." Regardless of the sign of :math:`R` or which surface - the :class:`AnchorPoint` lies on, an angle of :math:`90^{\circ}` always means that the curvature control - arm points perpendicular to the line passing through the neighboring control points of - the :class:`AnchorPoint`. Angles below :math:`90^{\circ}` "tuck" the arms in, and angles above - :math:`90^{\circ}` "spread" the arms out. The valid range is :math:`\psi_2 \in [0^{\circ},180^{\circ}]`. - - Returns - ======= - - An :class:`AnchorPoint` instance - """ - - super().__init__(xy.value[0], xy.value[1], tag, previous_anchor_point) - - self.ctrlpt = ControlPoint(xy.value[0], xy.value[1], tag, previous_anchor_point, cp_type='anchor_point') - self.ctrlpt_branch_list = None - - self.n1 = None - self.n2 = None - - self.anchor_type = None - self.tag = tag - self.previous_anchor_point = previous_anchor_point - - self.xy = xy - self.airfoil_transformation = None - self.airfoil_tag = airfoil_tag - self.L = L - self.R = R - self.xy.anchor_point = self - - self.Lt_minus = None - self.Lt_plus = None - self.Lc_minus = None - self.Lc_plus = None - self.abs_psi1 = None - self.abs_psi2 = None - self.abs_phi1 = None - self.abs_phi2 = None - - self.g1_minus_ctrlpt = None - self.g1_plus_ctrlpt = None - self.g2_minus_ctrlpt = None - self.g2_plus_ctrlpt = None - - self.ctrlpt_branch_array = None - - self.ctrlpt_branch_generated = False - - if 0 < r.value < 1: - self.r = r - else: - raise ValueError(f'The distance fraction, r, must be between 0 and 1. A value of {r.value} was entered.') - - # if -np.pi <= phi.value <= np.pi: - self.phi = phi - # else: - # raise ValueError(f'The anchor point neighboring control point angle, phi, must be between -180 degrees and' - # f' 180 degrees, inclusive. A value of {phi.value} was entered.') - # - # if 0 <= psi1.value <= np.pi: - self.psi1 = psi1 - # else: - # raise ValueError(f'The aft curvature control arm angle, psi1, must be between 0 degrees and 180 degrees, ' - # f'inclusive. ' - # f'A value of {psi1.value} was entered.') - # - # if 0 <= psi2.value <= np.pi: - self.psi2 = psi2 - # else: - # raise ValueError(f'The fore curvature control arm angle, psi2, must be between 0 degrees and 180 degrees,' - # f'inclusive. ' - # f'A value of {psi2.value} was entered.') - - def __repr__(self): - return f"anchor_point_{self.tag}" - - def set_xp_yp_value(self, xp, yp): - """ - Setter for the AnchorPoint's ``xy`` attribute where the changes are only applied individually for :math:`x` and - :math:`y` if ``linked==False`` and ``active==True``. - - Parameters - ========== - xp - Value to assign to ``self.xy.value[0]`` - - yp - Value to assign to ``self.xy.value[1]`` - """ - x_changed, y_changed = False, False - if self.xy.active[0] and not self.xy.linked[0]: - new_x = xp - x_changed = True - else: - new_x = self.xy.value[0] - if self.xy.active[1] and not self.xy.linked[1]: - new_y = yp - y_changed = True - else: - new_y = self.xy.value[1] - self.xy.value = [new_x, new_y] - - # If x or y was changed, set the location of the control point to reflect this: - if x_changed or y_changed: - self.set_ctrlpt_value() - - def transform_xy(self, dx, dy, angle, sf, transformation_order: typing.List[str]): - """ - Transforms the ``xy``-location of the AnchorPoint. - - Parameters - ========== - dx - Units to translate the ``AnchorPoint`` in the :math:`x`-direction. - - dy - Units to translate the ``AnchorPoint`` in the :math:`y`-direction. - - angle - Angle, in radians, by which to rotate the AnchorPoint's location about the origin. - - sf - Scale factor to apply to the AnchorPoint's ``xy``-location - - transformation_order: typing.List[str] - Order in which to apply the transformations. Use ``"s"`` for scale, ``"t"`` for translate, and ``"r"`` for - rotate - """ - mat = np.array([self.xy.value]) - new_mat = transform_matrix(mat, dx, dy, angle, sf, transformation_order) - self.xy.value = new_mat[0].tolist() - - def set_ctrlpt_value(self): - """ - Sets the :math:`x`- and :math:`y`-values of the AnchorPoints's ``pymead.core.control_point.ControlPoint``. - """ - self.ctrlpt.x_val = self.xy.value[0] - self.ctrlpt.y_val = self.xy.value[1] - self.ctrlpt.xp = self.xy.value[0] - self.ctrlpt.yp = self.xy.value[1] - - def get_anchor_type(self, anchor_point_order: list): - """ - Sets the type of :class:`AnchorPoint` based on the position within the airfoil's anchor point order. - - Parameters - ========== - anchor_point_order: list - Counter-clockwise order of the anchor points in the airfoil - """ - if self.tag == 'le': - self.anchor_type = self.tag - elif anchor_point_order.index(self.tag) < anchor_point_order.index('le'): - self.anchor_type = 'upper_surf' - elif anchor_point_order.index(self.tag) > anchor_point_order.index('le'): - self.anchor_type = 'lower_surf' - - def set_degree_adjacent_bezier_curves(self, n1: int, n2: int): - """ - Sets the degree of the Bézier curves in contact with the :class:`AnchorPoint`. - - Parameters - ========== - - n1: int - Degree of the Bézier curve preceding the :class:`AnchorPoint` in counter-clockwise ordering - - n2: int - Degree of the Bézier curve proceeding the :class:`AnchorPoint` in counter-clockwise ordering - """ - self.n1 = n1 - self.n2 = n2 - - def generate_anchor_point_branch(self, anchor_point_order: typing.List[str]): - """ - Generates the set of 5 :class:`ControlPoint`\ s associated with the :class:`AnchorPoint`. - - Parameters - ========== - anchor_point_order: typing.List[str] - The counter-clockwise order of the :class:`AnchorPoint`\ s by name - """ - r = self.r.value - L = self.L.value - phi = self.phi.value - R = self.R.value - psi1 = self.psi1.value - psi2 = self.psi2.value - tag = self.tag - if all(t not in tag for t in ['te_1', 'le', 'te_2']): # for all inserted AnchorPoints: - xy0_abs = self.xy.value # the absolute x-y position of the Anchor - abs_coords = np.array([xy0_abs]) - transform = AirfoilTransformation(**{k: v.value for k, v in self.airfoil_transformation.items()}) - xy = transform.transform_rel(abs_coords) - xy0 = xy[0].tolist() # get the position of the Anchor relative to the airfoil coordinate system - else: - xy0 = self.xy.value - - if self.n1 is None: - raise ValueError('Degree of Bezier curve before anchor point was not set before generating the anchor' - 'point branch') - else: - n1 = self.n1 - - if self.n2 is None: - raise ValueError('Degree of Bezier curve after anchor point was not set before generating the anchor' - 'point branch') - else: - n2 = self.n2 - - self.get_anchor_type(anchor_point_order) - - def generate_tangent_seg_ctrlpts(minus_plus: str): - # Generates the control points for the two control point segments adjacent to the AnchorPoint - - if R == 0: # degenerate case 1: infinite curvature (sharp corner) - return ControlPoint(xy0[0], xy0[1], f'anchor_point_{tag}_g1_{minus_plus}', tag) - - def evaluate_tangent_segment_length(): - if self.anchor_type == 'upper_surf': - if minus_plus == 'minus': - self.Lt_minus = (1 - r) * L - else: - self.Lt_plus = r * L - elif self.anchor_type in ['lower_surf', 'le']: - if minus_plus == 'minus': - self.Lt_minus = r * L - else: - self.Lt_plus = (1 - r) * L - else: - raise ValueError('Invalid anchor type') - - def map_tilt_angle(): - if self.anchor_type == 'upper_surf': - if minus_plus == 'minus': - self.abs_phi1 = phi - else: - self.abs_phi2 = np.pi + phi - elif self.anchor_type == 'lower_surf': - if minus_plus == 'minus': - self.abs_phi1 = np.pi - phi - else: - self.abs_phi2 = -phi - elif self.anchor_type == 'le': - if minus_plus == 'minus': - self.abs_phi1 = np.pi / 2 + phi - else: - self.abs_phi2 = 3 * np.pi / 2 + phi - else: - raise ValueError('Invalid anchor type') - - evaluate_tangent_segment_length() - map_tilt_angle() - - if minus_plus == 'minus': - xy = np.array(xy0) + self.Lt_minus * np.array([np.cos(self.abs_phi1), np.sin(self.abs_phi1)]) - else: - xy = np.array(xy0) + self.Lt_plus * np.array([np.cos(self.abs_phi2), np.sin(self.abs_phi2)]) - return ControlPoint(xy[0], xy[1], f'{repr(self)}_g1_{minus_plus}', tag, cp_type=f'g1_{minus_plus}') - - def generate_curvature_seg_ctrlpts(psi, tangent_ctrlpt: ControlPoint, n, minus_plus): - if R == 0: # degenerate case 1: infinite curvature (sharp corner) - return ControlPoint(xy0[0], xy0[1], f'{repr(self)}_g2_{minus_plus}', tag) - with np.errstate(divide='ignore'): # accept divide by 0 as valid - if tag == 'le': - if np.sin(psi + np.pi / 2) == 0 or np.true_divide(1, R) == 0: - # degenerate case 2: zero curvature (straight line) - return ControlPoint(tangent_ctrlpt.x_val, tangent_ctrlpt.y_val, f'{repr(self)}_g2_{minus_plus}', - tag, cp_type=f'g2_{minus_plus}') - else: - if np.sin(psi) == 0 or np.true_divide(1, R) == 0: - # degenerate case 2: zero curvature (straight line) - return ControlPoint(tangent_ctrlpt.x_val, tangent_ctrlpt.y_val, f'{repr(self)}_g2_{minus_plus}', - tag, cp_type=f'g2_{minus_plus}') - - if tag == 'le': - if minus_plus == 'minus': - self.Lc_minus = self.Lt_minus ** 2 / (R * (1 - 1 / n) * np.sin(psi + np.pi / 2)) - else: - self.Lc_plus = self.Lt_plus ** 2 / (R * (1 - 1 / n) * np.sin(psi + np.pi / 2)) - else: - if minus_plus == 'minus': - self.Lc_minus = self.Lt_minus ** 2 / (R * (1 - 1 / n) * np.sin(psi)) - else: - self.Lc_plus = self.Lt_plus ** 2 / (R * (1 - 1 / n) * np.sin(psi)) - - def map_psi_to_airfoil_csys(): - if minus_plus == 'minus': - if self.anchor_type == 'upper_surf': - if R > 0: - self.abs_psi1 = np.pi + psi + phi - else: - self.abs_psi1 = np.pi - psi + phi - elif self.anchor_type == 'lower_surf': - if R > 0: - self.abs_psi2 = psi - phi - else: - self.abs_psi2 = -psi - phi - elif self.anchor_type == 'le': - if R > 0: - self.abs_psi1 = psi + phi - else: - self.abs_psi1 = np.pi - psi + phi - else: - raise ValueError("Anchor is of invalid type") - else: - if self.anchor_type == 'upper_surf': - if R > 0: - self.abs_psi2 = -psi + phi - else: - self.abs_psi2 = psi + phi - elif self.anchor_type == 'lower_surf': - if R > 0: - self.abs_psi1 = np.pi - psi - phi - else: - self.abs_psi1 = np.pi + psi - phi - elif self.anchor_type == 'le': - if R > 0: - self.abs_psi2 = -psi + phi - else: - self.abs_psi2 = np.pi + psi + phi - else: - raise ValueError("Anchor is of invalid type") - - map_psi_to_airfoil_csys() - - if (minus_plus == 'minus' and self.anchor_type in ['upper_surf', 'le']) or ( - minus_plus == 'plus' and self.anchor_type == 'lower_surf'): - apsi = self.abs_psi1 - else: - apsi = self.abs_psi2 - - if minus_plus == 'minus': - xy = np.array([tangent_ctrlpt.x_val, tangent_ctrlpt.y_val]) + abs(self.Lc_minus) * \ - np.array([np.cos(apsi), np.sin(apsi)]) - else: - xy = np.array([tangent_ctrlpt.x_val, tangent_ctrlpt.y_val]) + abs(self.Lc_plus) * \ - np.array([np.cos(apsi), np.sin(apsi)]) - - return ControlPoint(xy[0], xy[1], f'{repr(self)}_g2_{minus_plus}', tag, cp_type=f'g2_{minus_plus}') - - self.g1_minus_ctrlpt = generate_tangent_seg_ctrlpts('minus') - self.g1_plus_ctrlpt = generate_tangent_seg_ctrlpts('plus') - if self.anchor_type in ['upper_surf', 'le']: - self.g2_minus_ctrlpt = generate_curvature_seg_ctrlpts(psi1, self.g1_minus_ctrlpt, n1, 'minus') - self.g2_plus_ctrlpt = generate_curvature_seg_ctrlpts(psi2, self.g1_plus_ctrlpt, n2, 'plus') - else: - self.g2_minus_ctrlpt = generate_curvature_seg_ctrlpts(psi2, self.g1_minus_ctrlpt, n1, 'minus') - self.g2_plus_ctrlpt = generate_curvature_seg_ctrlpts(psi1, self.g1_plus_ctrlpt, n2, 'plus') - - self.ctrlpt_branch_list = [self.g2_minus_ctrlpt, self.g1_minus_ctrlpt, self.ctrlpt, self.g1_plus_ctrlpt, - self.g2_plus_ctrlpt] - - self.ctrlpt_branch_array = np.array([[self.g2_minus_ctrlpt.xp, self.g2_minus_ctrlpt.yp], - [self.g1_minus_ctrlpt.xp, self.g1_minus_ctrlpt.yp], - [self.xp, self.yp], - [self.g1_plus_ctrlpt.xp, self.g1_plus_ctrlpt.yp], - [self.g2_plus_ctrlpt.xp, self.g2_plus_ctrlpt.yp]]) - - self.ctrlpt_branch_generated = True - - def recalculate_ap_branch_props_from_g2_pt(self, minus_plus: str, measured_psi, measured_Lc): - """ - Recalculates ``R`` and ``psi1`` or ``psi2`` based on the measured length and absolute angle of the curvature - control arm segment. Used only in the GUI. - - Parameters - ========== - minus_plus: str - Whether the :math:`G^2` point comes before or after the :class:`AnchorPoint` in counter-clockwise ordering. - If before, input ``"minus"``. If after, input ``"plus"``. - - measured_psi - The measured absolute angle of the curvature control arm - - measured_Lc - The measured length of the curvature control arm - """ - apsi = None - if measured_psi is not None: - if (minus_plus == 'minus' and self.anchor_type in ['upper_surf', 'le']) or ( - minus_plus == 'plus' and self.anchor_type == 'lower_surf'): - self.abs_psi1 = measured_psi - apsi = self.abs_psi1 - else: - self.abs_psi2 = measured_psi - apsi = self.abs_psi2 - - if minus_plus == 'minus': - # The following logic block is to ensure that the curvature control arm angle (psi) uses the correct - # coordinate system: - if measured_Lc is not None: - self.Lc_minus = measured_Lc - if 0 < np.arctan2(np.sin(apsi - self.abs_phi1), np.cos(apsi - self.abs_phi1)) < np.pi: - sign_R = -1 - else: - sign_R = 1 - else: - if measured_Lc is not None: - self.Lc_plus = measured_Lc - if 0 < np.arctan2(np.sin(apsi - self.abs_phi2), np.cos(apsi - self.abs_phi2)) < np.pi: - sign_R = 1 - else: - sign_R = -1 - - if self.R.active and not self.R.linked: - if int(np.sign(self.R.value)) * sign_R == -1: - # print('Flipping sign!') - self.R.value *= -1 # Flip the sign of the radius of curvature if different than current value - else: - pass - - # Since we will be overriding the radius of curvature (R.value) with a value that is always positive, we need to - # determine whether the sign of the radius of curvature should be positive or negative: - negate_R = False - if self.R.value < 0: - negate_R = True - - def map_psi_to_airfoil_csys_inverse(): - if minus_plus == 'minus': - if self.psi1.active and not self.psi1.linked: - if self.anchor_type == 'upper_surf': - if self.R.value > 0: - # angle = np.pi + psi + phi - self.psi1.value = self.abs_psi1 + np.pi - self.phi.value - else: - # angle = np.pi - psi + phi - self.psi1.value = -self.abs_psi1 + np.pi + self.phi.value - elif self.anchor_type == 'lower_surf': - if self.R.value > 0: - # angle = psi - phi - self.psi2.value = self.abs_psi2 + self.phi.value - else: - # angle = -psi - phi - self.psi2.value = -self.abs_psi2 - self.phi.value - elif self.anchor_type == 'le': - if self.R.value > 0: - # angle = psi + phi - self.psi1.value = self.abs_psi1 - self.phi.value - else: - # angle = np.pi - psi + phi - self.psi1.value = np.pi - self.abs_psi1 + self.phi.value - else: - raise ValueError("Anchor is of invalid type") - else: - if self.psi2.active and not self.psi2.linked: - if self.anchor_type == 'upper_surf': - if self.R.value > 0: - # angle = -psi + phi - self.psi2.value = -self.abs_psi2 + self.phi.value - else: - # angle = psi + phi - self.psi2.value = self.abs_psi2 - self.phi.value - elif self.anchor_type == 'lower_surf': - if self.R.value > 0: - # angle = np.pi - psi - phi - self.psi1.value = np.pi - self.abs_psi1 - self.phi.value - else: - # angle = np.pi + psi - phi - self.psi1.value = self.abs_psi1 + np.pi + self.phi.value - elif self.anchor_type == 'le': - if self.R.value > 0: - # angle = -psi + phi - self.psi2.value = -self.abs_psi2 + self.phi.value - else: - # angle = np.pi + psi + phi - self.psi2.value = self.abs_psi2 - np.pi - self.phi.value - else: - raise ValueError("Anchor is of invalid type") - - map_psi_to_airfoil_csys_inverse() - - # Take care of the case where R is bounded and the mouse is dragged such that R would flip signs otherwise - if self.R.at_boundary: - for psi in [self.psi1, self.psi2]: - if self.anchor_type == "le": - if psi.value > np.pi / 2: - psi.value -= 2 * (psi.value - np.pi / 2) - elif psi.value < -np.pi / 2: - psi.value += 2 * (psi.value - np.pi / 2) - else: - if psi.value > np.pi: - psi.value -= 2 * (psi.value - np.pi) - elif psi.value < 0.0: - psi.value *= -1 - - if (minus_plus == 'minus' and self.anchor_type in ['upper_surf', 'le']) or ( - minus_plus == 'plus' and self.anchor_type == 'lower_surf'): - apsi = self.psi1.value - else: - apsi = self.psi2.value - - if self.R.active and not self.R.linked: - R_multiplier = -1.0 if negate_R else 1.0 - if self.tag == 'le': - if minus_plus == 'minus': - self.R.value = R_multiplier * self.Lt_minus ** 2 / ( - self.Lc_minus * (1 - 1 / self.n1) * np.sin(apsi + np.pi / 2)) - else: - self.R.value = R_multiplier * self.Lt_plus ** 2 / ( - self.Lc_plus * (1 - 1 / self.n2) * np.sin(apsi + np.pi / 2)) - else: - if minus_plus == 'minus': - self.R.value = R_multiplier * self.Lt_minus ** 2 / (self.Lc_minus * (1 - 1 / self.n1) * np.sin(apsi)) - else: - self.R.value = R_multiplier * self.Lt_plus ** 2 / (self.Lc_plus * (1 - 1 / self.n2) * np.sin(apsi)) - - def recalculate_ap_branch_props_from_g1_pt(self, minus_plus: str, measured_phi, measured_Lt): - """ - Recalculates ``phi``, ``r``, and ``L`` based on the measured length and absolute angle of the tangent - control arm segment. Used only in the GUI. - - Parameters - ========== - minus_plus: str - Whether the :math:`G^1` point comes before or after the :class:`AnchorPoint` in counter-clockwise ordering. - If before, input ``"minus"``. If after, input ``"plus"``. - - measured_phi - The measured absolute angle of the tangent control arm - - measured_Lt - The measured length of the tangent control arm - """ - - if minus_plus == 'minus': - if measured_Lt is not None: - self.Lt_minus = measured_Lt - if measured_phi is not None: - self.abs_phi1 = measured_phi - else: - if measured_Lt is not None: - self.Lt_plus = measured_Lt - if measured_phi is not None: - self.abs_phi2 = measured_phi - - def evaluate_g1_length_and_ratio(): - if self.L.active and not self.L.linked: - self.L.value = self.Lt_minus + self.Lt_plus - if self.r.active and not self.r.linked: - if self.anchor_type == 'upper_surf': - self.r.value = self.Lt_plus / self.L.value - elif self.anchor_type in ['lower_surf', 'le']: - self.r.value = self.Lt_minus / self.L.value - else: - raise ValueError('Invalid anchor type') - - def map_tilt_angle_inverse(): - if self.phi.active and not self.phi.linked: - if self.anchor_type == 'upper_surf': - if minus_plus == 'minus': - # self.abs_phi1 = phi - self.phi.value = self.abs_phi1 - else: - # self.abs_phi2 = np.pi + phi - self.phi.value = self.abs_phi2 - np.pi - elif self.anchor_type == 'lower_surf': - if minus_plus == 'minus': - # self.abs_phi1 = np.pi - phi - self.phi.value = np.pi - self.abs_phi1 - else: - # self.abs_phi2 = -phi - self.phi.value = -self.abs_phi2 - elif self.anchor_type == 'le': - if minus_plus == 'minus': - # self.abs_phi1 = np.pi / 2 + phi - self.phi.value = self.abs_phi1 - np.pi / 2 - else: - # self.abs_phi2 = 3 * np.pi / 2 + phi - self.phi.value = self.abs_phi2 - 3 * np.pi / 2 - else: - raise ValueError('Invalid anchor type') - - evaluate_g1_length_and_ratio() - map_tilt_angle_inverse() diff --git a/pymead/core/base_airfoil_params.py b/pymead/core/base_airfoil_params.py deleted file mode 100644 index 6502000e..00000000 --- a/pymead/core/base_airfoil_params.py +++ /dev/null @@ -1,183 +0,0 @@ -import typing - -from pymead.core.param import Param -from pymead.core.pos_param import PosParam -from pymead.core.anchor_point import AnchorPoint -from pymead.core.trailing_edge_point import TrailingEdgePoint - - -class BaseAirfoilParams: - - def __init__(self, - airfoil_tag: str = None, - c: Param = None, - alf: Param = None, - R_le: Param = None, - L_le: Param = None, - r_le: Param = None, - phi_le: Param = None, - psi1_le: Param = None, - psi2_le: Param = None, - L1_te: Param = None, - L2_te: Param = None, - theta1_te: Param = None, - theta2_te: Param = None, - t_te: Param = None, - r_te: Param = None, - phi_te: Param = None, - dx: Param = None, - dy: Param = None, - ): - r""" - The most fundamental parameters required for the generation of any ``pymead.core.airfoil.Airfoil``. - A geometric description of an example airfoil generated (from ``pymead.examples.simple_airfoil.run()``) is - shown below (it may be helpful to open the image in a new tab to adequately view the details): - - Parameters - ========== - airfoil_tag: str - The Airfoil to which this set of base parameters belongs. - - c: Param or None - :math:`c`: Chord length. Default value if ``None``: ``Param(1.0)``. - - alf: Param or None - :math:`\alpha`: Angle of attack [rad]. Default value if ``None``: ``Param(0.0)``. - - R_le: Param or None - :math:`R_{LE}`: Leading-edge radius. Default value if ``None``: ``Param(0.1)``. - - L_le: Param or None - :math:`L_{LE}`: Distance between the control points immediately before and after the leading-edge - anchor point. Default value if ``None``: ``Param(0.1)``. - - r_le: Param or None - :math:`r_{LE}`: Ratio of the distance from the leading-edge anchor point to the control point before to - the distance between the control points immediately before and after the leading-edge - anchor point (:math:`r_{LE} = L_{LE,\text{upper}} / L_{LE}`). Default value if ``None``: ``Param(0.5)``. - - phi_le: Param or None - :math:`\phi_{LE}`: Leading-edge tilt (rad), referenced counter-clockwise from the perpendicular to - the chordline. Default value if ``None``: ``Param(0.0)``. - - psi1_le: Param or None - :math:`\psi_{LE,1}`: Leading-edge upper curvature control angle (rad), referenced counter-clockwise - from the chordline: Default value if ``None``: ``Param(0.0)``. - - psi2_le: Param or None - :math:`\psi_{LE,2}`: Leading-edge lower curvature control angle (rad), referenced clockwise - from the chordline. Default value if ``None``: ``Param(0.0)``. - - L1_te: Param or None - :math:`L_{TE,1}`: Trailing edge upper length. Default value if ``None``: ``Param(0.1)``. - - L2_te: Param or None - :math:`L_{TE,2}`: Trailing edge lower length. Default value if ``None``: ``Param(0.1)``. - - theta1_te: Param or None - :math:`\theta_{TE,1}`: Trailing edge upper angle (rad), referenced clockwise from the chordline. - Default value if ``None``: ``Param(np.deg2rad(10.0))``. - - theta2_te: Param or None - :math:`\theta_{TE,2}`: Trailing edge lower angle (rad), referenced counter-clockwise from the - chordline. Default value if ``None``: ``Param(np.deg2rad(10.0))``. - - t_te: Param or None - :math:`t_{TE}`: Blunt trailing edge thickness. - Default value if ``None``: ``Param(0.0)`` (sharp trailing edge). - - r_te: Param or None - :math:`r_{TE}`: Ratio of the distance from the chordline's endpoint at the trailing edge to the - first control point of the airfoil to the distance between the first and last control points of the airfoil - (:math:`r_{TE} = L_{TE,upper} / L_{TE}`). This parameter has no effect - on the airfoil geometry unless ``t_te != Param(0.0)``. Default value if ``None``: ``Param(0.5)``. - - phi_te: Param or None - :math:`\phi_{TE}`: Blunt trailing-edge tilt (rad), referenced counter-clockwise from the - perpendicular to the chordline (same as ``phi_le``). This parameter has no effect - on the airfoil geometry unless ``t_te != Param(0.0)``. Default value if ``None``: ``Param(0.0)``. - - dx: Param or None - :math:`\Delta x`: Distance to translate the airfoil in the :math:`x`-direction. The translation operation - follows the rotation operation such that the rotation operation can be performed about the origin. - Default value if ``None``: ``Param(0.0)``. - - dy: Param or None - :math:`\Delta y`: Distance to translate the airfoil in the :math:`y`-direction. The translation operation - follows the rotation operation such that the rotation operation can be performed about the origin. - Default value if ``None``: ``Param(0.0)``. - - Returns - ======= - BaseAirfoilParams - An instance of the ``BaseAirfoilParams`` class. - """ - self.airfoil_tag = airfoil_tag - self.c = c - self.alf = alf - self.R_le = R_le - self.L_le = L_le - self.r_le = r_le - self.phi_le = phi_le - self.psi1_le = psi1_le - self.psi2_le = psi2_le - self.L1_te = L1_te - self.L2_te = L2_te - self.theta1_te = theta1_te - self.theta2_te = theta2_te - self.t_te = t_te - self.r_te = r_te - self.phi_te = phi_te - self.dx = dx - self.dy = dy - - if not self.c: - self.c = Param(1.0) - if not self.alf: - self.alf = Param(0.0, periodic=True) - if not self.R_le: - self.R_le = Param(0.04) - if not self.L_le: - self.L_le = Param(0.1) - if not self.r_le: - self.r_le = Param(0.5) - if not self.phi_le: - self.phi_le = Param(0.0, periodic=True) - if not self.psi1_le: - self.psi1_le = Param(0.3, periodic=True) - if not self.psi2_le: - self.psi2_le = Param(0.1, periodic=True) - if not self.L1_te: - self.L1_te = Param(0.4) - if not self.L2_te: - self.L2_te = Param(0.3) - if not self.theta1_te: - self.theta1_te = Param(0.1, periodic=True) - if not self.theta2_te: - self.theta2_te = Param(0.1, periodic=True) - if not self.t_te: - self.t_te = Param(0.0) - if not self.r_te: - self.r_te = Param(0.5) - if not self.phi_te: - self.phi_te = Param(0.0, periodic=True) - if not self.dx: - self.dx = Param(0.0) - if not self.dy: - self.dy = Param(0.0) - - def generate_main_anchor_points(self) -> typing.List[AnchorPoint]: - """ - Generates the minimal set of ``pymead.core.anchor_point.AnchorPoint``\ s required for an Airfoil in pymead. - - Returns - ======= - typing.List[AnchorPoint] - """ - le_anchor_point = AnchorPoint(PosParam((0.0, 0.0)), 'le', 'te_1', self.airfoil_tag, self.L_le, self.R_le, - self.r_le, self.phi_le, self.psi1_le, self.psi2_le) - te_1_anchor_point = TrailingEdgePoint(self.c, self.r_te, self.t_te, self.phi_te, self.L1_te, self.theta1_te, - True) - te_2_anchor_point = TrailingEdgePoint(self.c, self.r_te, self.t_te, self.phi_te, self.L2_te, self.theta2_te, - False) - return [te_1_anchor_point, le_anchor_point, te_2_anchor_point] diff --git a/pymead/core/bezier.py b/pymead/core/bezier.py deleted file mode 100644 index 05142cf8..00000000 --- a/pymead/core/bezier.py +++ /dev/null @@ -1,255 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np - -from pymead.utils.nchoosek import nchoosek -from pymead.core.parametric_curve import ParametricCurve - - -class Bezier(ParametricCurve): - """Bézier Class""" - - def __init__(self, P, nt: int = 100, t: np.ndarray = None): - r""" - Computes the Bézier curve through the control points ``P`` according to - - .. math:: - - \vec{C}(t)=\sum_{i=0}^n \vec{P}_i B_{i,n}(t) - - where :math:`B_{i,n}(t)` is the Bernstein polynomial, given by - - .. math:: - - B_{i,n}(t)={n \choose i} t^i (1-t)^{n-i} - - Also included are first derivative, second derivative, and curvature information. These are given by - - .. math:: - - \vec{C}'(t)=n \sum_{i=0}^{n-1} (\vec{P}_{i+1} - \vec{P}_i B_{i,n-1}(t) - - .. math:: - - \vec{C}''(t)=n(n-1) \sum_{i=0}^{n-2} (\vec{P}_{i+2}-2\vec{P}_{i+1}+\vec{P}_i) B_{i,n-2}(t) - - .. math:: - - \kappa(t)=\frac{C'_x(t) C''_y(t) - C'_y(t) C''_x(t)}{[(C'_x)^2(t) + (C'_y)^2(t)]^{3/2}} - - Here, the :math:`'` and :math:`''` superscripts are the first and second derivatives with respect to - :math:`x` and :math:`y`, not the parameter :math:`t`. The result of :math:`\vec{C}''(t)`, for example, - is a vector with two components, :math:`C''_x(t)` and :math:`C''_y(t)`. - - .. _cubic-bezier: - .. figure:: ../images/cubic_bezier_light.* - :class: only-light - :width: 600 - :align: center - - Cubic Bézier curve - - .. figure:: ../images/cubic_bezier_dark.* - :class: only-dark - :width: 600 - :align: center - - Cubic Bézier curve - - An example cubic Bézier curve (degree :math:`n=3`) is shown in :numref:`cubic-bezier`. Note that the curve passes - through the first and last control points and has a local slope at :math:`P_0` equal to the slope of the - line passing through :math:`P_0` and :math:`P_1`. Similarly, the local slope at :math:`P_3` is equal to - the slope of the line passing through :math:`P_2` and :math:`P_3`. These properties of Bézier curves allow us to - easily enforce :math:`G^0` and :math:`G^1` continuity at Bézier curve "joints" (common endpoints of - connected Bézier curves). - - Parameters - ========== - P: numpy.ndarray - Array of ``shape=(n+1, 2)``, where ``n`` is the degree of the Bézier curve and ``n+1`` is - the number of control points in the Bézier curve. The two columns represent the :math:`x`- - and :math:`y`-components of the control points. - - nt: int - The number of points in the :math:`t` vector (defines the resolution of the curve). Default: ``100``. - - t: numpy.ndarray - Parameter vector describing where the Bézier curve should be evaluated. This vector should be a 1-D array - beginning and should monotonically increase from 0 to 1. If not specified, ``numpy.linspace(0, 1, nt)`` will - be used. - - Returns - ======= - dict - A dictionary of ``numpy`` arrays of ``shape=nt`` containing information related to the created 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)`. - """ - - self.P = P - self.n = len(self.P) - 1 - - if t is not None: - self.t = t - else: - self.t = np.linspace(0, 1, nt) - - n_ctrl_points = len(P) - - self.x, self.y = np.zeros(self.t.shape), np.zeros(self.t.shape) - - for i in range(n_ctrl_points): - # Calculate the x- and y-coordinates of the Bézier curve given the input vector t - self.x += P[i, 0] * self.bernstein_poly(self.n, i, self.t) - self.y += P[i, 1] * self.bernstein_poly(self.n, i, self.t) - - first_deriv = self.derivative(1) - self.px = first_deriv[:, 0] - self.py = first_deriv[:, 1] - second_deriv = self.derivative(2) - self.ppx = second_deriv[:, 0] - self.ppy = second_deriv[:, 1] - - 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) - self.k = np.true_divide((self.px * self.ppy - self.py * self.ppx), - (self.px ** 2 + self.py ** 2) ** (3 / 2)) - - with np.errstate(divide='ignore', invalid='ignore'): - self.R = np.true_divide(1, self.k) - self.R_abs_min = np.abs(self.R).min() - - super().__init__(self.t, self.x, self.y, self.px, self.py, self.ppx, self.ppy, self.k, self.R) - - @staticmethod - def bernstein_poly(n: int, i: int, t): - """Calculates the Bernstein polynomial for a given Bézier curve order, index, and parameter vector - - Arguments - ========= - n: int - Bézier curve degree (one less than the number of control points in the Bézier curve) - i: int - Bézier curve index - t: int, float, or np.ndarray - Parameter vector for the Bézier curve - - Returns - ======= - np.ndarray - Array of values of the Bernstein polynomial evaluated for each point in the parameter vector - """ - return nchoosek(n, i) * t ** i * (1 - t) ** (n - i) - - @staticmethod - def finite_diff_P(P: np.ndarray, k: int, i: int): - """Calculates the finite difference of the control points as shown in - https://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html - - Arguments - ========= - P: np.ndarray - Array of control points for the Bézier curve - k: int - Finite difference level (e.g., k = 1 is the first derivative finite difference) - i: int - An index referencing a location in the control point array - """ - def finite_diff_recursive(_k, _i): - if _k > 1: - return finite_diff_recursive(_k - 1, _i + 1) - finite_diff_recursive(_k - 1, _i) - else: - return P[_i + 1, :] - P[_i, :] - - return finite_diff_recursive(k, i) - - def derivative(self, order: int): - """ - Calculates an arbitrary-order derivative of the Bézier curve - - Parameters - ========== - 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 the :math:`C^{(m)}_x(t)` and :math:`C^{(m)}_y(t)`, where :math:`m` is the - derivative order. - """ - n_ctrlpts = len(self.P) - return np.sum(np.array([np.prod(np.array([self.n - idx for idx in range(order)])) * - np.array([self.finite_diff_P(self.P, order, i)]).T * - np.array([self.bernstein_poly(self.n - order, i, self.t)]) - for i in range(n_ctrlpts - order)]), axis=0).T - - @staticmethod - def approximate_arc_length(P, nt): - # nchoosek_array = nchoosek_matrix(np.ones(shape=nt) * (len(P) - 1), np.arange(len(P))) - # xy_array = np.sum(P * nchoosek_array) - - x, y = np.zeros(nt), np.zeros(nt) - n = len(P) - t = np.linspace(0, 1, nt) - for i in range(n): - # Calculate the x- and y-coordinates of the Bézier curve given the input vector t - x += P[i, 0] * nchoosek(n - 1, i) * t ** i * (1 - t) ** (n - 1 - i) - y += P[i, 1] * nchoosek(n - 1, i) * t ** i * (1 - t) ** (n - 1 - i) - return np.sum(np.hypot(x[1:] - x[:-1], y[1:] - y[:-1])) - - def update(self, P, nt: int = None, t: np.ndarray = None): - self.P = P - self.n = len(self.P) - 1 - - if t is not None: - if np.min(t) != 0 or np.max(t) != 1: - raise ValueError('\'t\' array must have a minimum at 0 and a maximum at 1') - else: - self.t = t - else: - self.t = np.linspace(0, 1, nt) - - n_ctrl_points = len(P) - - self.x, self.y = np.zeros(self.t.shape), np.zeros(self.t.shape) - - for i in range(n_ctrl_points): - # Calculate the x- and y-coordinates of the Bézier curve given the input vector t - self.x += P[i, 0] * self.bernstein_poly(self.n, i, self.t) - self.y += P[i, 1] * self.bernstein_poly(self.n, i, self.t) - - first_deriv = self.derivative(1) - self.px = first_deriv[:, 0] - self.py = first_deriv[:, 1] - second_deriv = self.derivative(2) - self.ppx = second_deriv[:, 0] - self.ppy = second_deriv[:, 1] - - 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) - self.k = np.true_divide((self.px * self.ppy - self.py * self.ppx), - (self.px ** 2 + self.py ** 2) ** (3 / 2)) - - with np.errstate(divide='ignore', invalid='ignore'): - self.R = np.true_divide(1, self.k) - self.R_abs_min = np.abs(self.R).min() - - def get_curvature_comb(self, max_k_normalized_scale_factor, interval: int = 1): - comb_heads_x = self.x - self.py / np.sqrt(self.px**2 + self.py**2) * self.k * max_k_normalized_scale_factor - comb_heads_y = self.y + self.px / np.sqrt(self.px**2 + self.py**2) * self.k * max_k_normalized_scale_factor - # Stack the x and y columns (except for the last x and y values) horizontally and keep only the rows by the - # specified interval: - self.comb_tails = np.column_stack((self.x, self.y))[:-1:interval, :] - self.comb_heads = np.column_stack((comb_heads_x, comb_heads_y))[:-1:interval, :] - # Add the last x and y values onto the end (to make sure they do not get skipped with input interval) - self.comb_tails = np.row_stack((self.comb_tails, np.array([self.x[-1], self.y[-1]]))) - self.comb_heads = np.row_stack((self.comb_heads, np.array([comb_heads_x[-1], comb_heads_y[-1]]))) - - def plot_control_point_skeleton(self, axs: plt.Axes, **plt_kwargs): - axs.plot(self.P[:, 0], self.P[:, 1], **plt_kwargs) diff --git a/pymead/core/bezier2.py b/pymead/core/bezier2.py index 995a64bc..1ffe7b40 100644 --- a/pymead/core/bezier2.py +++ b/pymead/core/bezier2.py @@ -8,6 +8,86 @@ class Bezier(ParametricCurve): def __init__(self, point_sequence: PointSequence, name: str or None = None, **kwargs): + r""" + Computes the Bézier curve through the control points ``P`` according to + + .. math:: + + \vec{C}(t)=\sum_{i=0}^n \vec{P}_i B_{i,n}(t) + + where :math:`B_{i,n}(t)` is the Bernstein polynomial, given by + + .. math:: + + B_{i,n}(t)={n \choose i} t^i (1-t)^{n-i} + + Also included are first derivative, second derivative, and curvature information. These are given by + + .. math:: + + \vec{C}'(t)=n \sum_{i=0}^{n-1} (\vec{P}_{i+1} - \vec{P}_i B_{i,n-1}(t) + + .. math:: + + \vec{C}''(t)=n(n-1) \sum_{i=0}^{n-2} (\vec{P}_{i+2}-2\vec{P}_{i+1}+\vec{P}_i) B_{i,n-2}(t) + + .. math:: + + \kappa(t)=\frac{C'_x(t) C''_y(t) - C'_y(t) C''_x(t)}{[(C'_x)^2(t) + (C'_y)^2(t)]^{3/2}} + + Here, the :math:`'` and :math:`''` superscripts are the first and second derivatives with respect to + :math:`x` and :math:`y`, not the parameter :math:`t`. The result of :math:`\vec{C}''(t)`, for example, + is a vector with two components, :math:`C''_x(t)` and :math:`C''_y(t)`. + + .. _cubic-bezier: + .. figure:: ../images/cubic_bezier_light.* + :class: only-light + :width: 600 + :align: center + + Cubic Bézier curve + + .. figure:: ../images/cubic_bezier_dark.* + :class: only-dark + :width: 600 + :align: center + + Cubic Bézier curve + + An example cubic Bézier curve (degree :math:`n=3`) is shown in :numref:`cubic-bezier`. Note that the curve passes + through the first and last control points and has a local slope at :math:`P_0` equal to the slope of the + line passing through :math:`P_0` and :math:`P_1`. Similarly, the local slope at :math:`P_3` is equal to + the slope of the line passing through :math:`P_2` and :math:`P_3`. These properties of Bézier curves allow us to + easily enforce :math:`G^0` and :math:`G^1` continuity at Bézier curve "joints" (common endpoints of + connected Bézier curves). + + Parameters + ========== + P: numpy.ndarray + Array of ``shape=(n+1, 2)``, where ``n`` is the degree of the Bézier curve and ``n+1`` is + the number of control points in the Bézier curve. The two columns represent the :math:`x`- + and :math:`y`-components of the control points. + + nt: int + The number of points in the :math:`t` vector (defines the resolution of the curve). Default: ``100``. + + t: numpy.ndarray + Parameter vector describing where the Bézier curve should be evaluated. This vector should be a 1-D array + beginning and should monotonically increase from 0 to 1. If not specified, ``numpy.linspace(0, 1, nt)`` will + be used. + + Returns + ======= + dict + A dictionary of ``numpy`` arrays of ``shape=nt`` containing information related to the created 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)`. + """ super().__init__(sub_container="bezier", **kwargs) self._point_sequence = None self.degree = None diff --git a/pymead/core/free_point.py b/pymead/core/free_point.py deleted file mode 100644 index ec32b02f..00000000 --- a/pymead/core/free_point.py +++ /dev/null @@ -1,136 +0,0 @@ -import numpy as np -from pymead.core.pos_param import PosParam -from pymead.core.control_point import ControlPoint -from pymead.utils.transformations import transform_matrix - - -class FreePoint(ControlPoint): - - def __init__(self, - xy: PosParam, - previous_anchor_point: str, - airfoil_tag: str, - previous_free_point: str or None = None, - tag: str or None = None, - ): - r""" - The FreePoint in pymead is the way to add a control point to a Bézier curve within an Airfoil - without requiring the Bézier curve to pass through that particular point. In other words, a FreePoint allows - an :math:`x`-:math:`y` coordinate pair to be added to the ``P`` matrix (see ``pymead.core.airfoil.bezier`` for - usage). An example showing some possible locations of FreePoints is shown below. - - Parameters - ========== - xy: PosParam - The location of the FreePoint in :math:`x`-:math:`y` space - - previous_anchor_point: str - The previous ``AnchorPoint`` (counter-clockwise ordering) - - airfoil_tag: str - The Airfoil to which this FreePoint belongs - - previous_free_point: str or None - The previous FreePoint associated with the current FreePoint's AnchorPoint (counter-clockwise ordering). If - ``None``, the current FreePoint immediately follows the last ControlPoint associated with its AnchorPoint. - Default: ``None``. - - tag: str or None - A description of this FreePoint. Default: ``None``. - - Returns - ======= - FreePoint - An instance of the ``FreePoint`` class - """ - - super().__init__(xy.value[0], xy.value[1], tag, previous_anchor_point, cp_type='free_point') - - self.ctrlpt = ControlPoint(xy.value[0], xy.value[1], tag, previous_anchor_point, cp_type='free_point') - - self.xy = xy - self.xy.free_point = self - self.airfoil_transformation = None - self.airfoil_tag = airfoil_tag - self.tag = tag - self.previous_free_point = previous_free_point - - def set_tag(self, tag: str): - """ - Sets the tag for this FreePoint and its associated ControlPoint. - - Parameters - ========== - tag: str - A description of the FreePoint - """ - self.tag = tag - self.ctrlpt.tag = tag - - def set_xp_yp_value(self, xp, yp): - """ - Setter for the FreePoint's ``xy`` attribute where the changes are only applied individually for :math:`x` and - :math:`y` if ``linked==False`` and ``active==True``. - - Parameters - ========== - xp - Value to assign to ``self.xy.value[0]`` - - yp - Value to assign to ``self.xy.value[1]`` - """ - x_changed, y_changed = False, False - if self.xy.active[0] and not self.xy.linked[0]: - new_x = xp - x_changed = True - else: - new_x = self.xy.value[0] - if self.xy.active[1] and not self.xy.linked[1]: - new_y = yp - y_changed = True - else: - new_y = self.xy.value[1] - self.xy.value = [new_x, new_y] - - # If x or y was changed, set the location of the control point to reflect this - if x_changed or y_changed: - self.set_ctrlpt_value() - - def transform_xy(self, dx, dy, angle, sf, transformation_order): - """ - Transforms the ``xy``-location of the FreePoint. - - Parameters - ========== - dx - Units to translate the ``FreePoint`` in the :math:`x`-direction. - - dy - Units to translate the ``FreePoint`` in the :math:`y`-direction. - - angle - Angle, in radians, by which to rotate the FreePoint's location about the origin. - - sf - Scale factor to apply to the FreePoint's ``xy``-location - - transformation_order: typing.List[str] - Order in which to apply the transformations. Use ``"s"`` for scale, ``"t"`` for translate, and ``"r"`` for - rotate - """ - mat = np.array([self.xy.value]) - new_mat = transform_matrix(mat, dx, dy, angle, sf, transformation_order) - self.xy.value = new_mat[0].tolist() - - def set_ctrlpt_value(self): - """ - Sets the :math:`x`- and :math:`y`-values of the FreePoints's ``pymead.core.control_point.ControlPoint``. - """ - self.ctrlpt.x_val = self.xy.value[0] - self.ctrlpt.y_val = self.xy.value[1] - self.ctrlpt.xp = self.xy.value[0] - self.ctrlpt.yp = self.xy.value[1] - - def __repr__(self): - return f"free_point_{self.tag}" diff --git a/pymead/core/line.py b/pymead/core/line.py deleted file mode 100644 index 2c067ba9..00000000 --- a/pymead/core/line.py +++ /dev/null @@ -1,96 +0,0 @@ -from pymead.core.parametric_curve import ParametricCurve -import numpy as np -from math import tan - - -class FiniteLine(ParametricCurve): - - def __init__(self, x1, y1, x2, y2, nt: int = 2, t=None): - self.nt = nt - - if t is None: - t = np.linspace(0.0, 1.0, self.nt) - - self.x1 = x1 - self.y1 = y1 - self.x2 = x2 - self.y2 = y2 - - self.m = (y2 - y1) / (x2 - x1) # calculate the slope of the line - self.theta = np.arctan2(self.m, 1) # get the angle of the line in radians - - x = x1 + t * (x2 - x1) * np.cos(self.theta) - y = y1 + t * (y2 - y1) * np.sin(self.theta) - px = np.ones(shape=t.shape) - py = self.m * np.ones(shape=t.shape) - ppx = np.zeros(shape=t.shape) - ppy = np.zeros(shape=t.shape) - k = np.zeros(shape=t.shape) - R = np.inf * np.zeros(shape=t.shape) - - super().__init__(t, x, y, px, py, ppx, ppy, k, R) - - def get_curvature_comb(self, max_k_normalized_scale_factor, interval: int = 1): - comb_heads_x = self.x - comb_heads_y = self.y - # Stack the x and y columns (except for the last x and y values) horizontally and keep only the rows by the - # specified interval: - self.comb_tails = np.column_stack((self.x, self.y))[:-1:interval, :] - self.comb_heads = np.column_stack((comb_heads_x, comb_heads_y))[:-1:interval, :] - # Add the last x and y values onto the end (to make sure they do not get skipped with input interval) - self.comb_tails = np.row_stack((self.comb_tails, np.array([self.x[-1], self.y[-1]]))) - self.comb_heads = np.row_stack((self.comb_heads, np.array([comb_heads_x[-1], comb_heads_y[-1]]))) - - -class InfiniteLine: - """This class is not designed for plotting, but merely as a container for line parameters. It also provides some - convenience functions, especially useful in the 'mirror' function in the GUI.""" - def __init__(self, x1=None, y1=None, x2=None, y2=None, m=None, theta_rad=None, theta_deg=None): - self.x1 = x1 - self.y1 = y1 - self.x2 = x2 - self.y2 = y2 - self.m = m - self.theta_deg = theta_deg - self.theta_rad = theta_rad - self.update() - - def update(self): - # Slope input handling - if self.m is None: - if self.theta_rad is None and self.theta_deg is None: - self.m = (self.y2 - self.y1) / (self.x2 - self.x1) - elif self.theta_rad is not None and self.theta_deg is None: - self.m = np.tan(self.theta_rad) - elif self.theta_rad is None and self.theta_deg is not None: - self.m = np.tan(np.deg2rad(self.theta_deg)) - - # Angle input handling - if self.theta_rad is None and self.theta_deg is None: - self.theta_rad = np.arctan2(self.m, 1) - self.theta_deg = np.rad2deg(self.theta_rad) - elif self.theta_rad is not None and self.theta_deg is None: - self.theta_deg = np.rad2deg(self.theta_rad) - elif self.theta_rad is None and self.theta_deg is not None: - self.theta_rad = np.deg2rad(self.theta_deg) - else: - pass - - if not self.x2 or not self.y2: - self.x2 = self.x1 + np.cos(self.theta_rad) - self.y2 = self.y1 + np.sin(self.theta_rad) - - def get_standard_form_coeffs(self): - return {'A': -self.m, 'B': 1, 'C': self.m * self.x2 - self.y2} - - def evaluate_y(self, x): - return self.m * (x - self.x1) + self.y1 - - -def _test(): - inf_line = InfiniteLine(x1=0.5, y1=0.2, theta_deg=30) - print(f"Standard form coefficients = {inf_line.get_standard_form_coeffs()}") - - -if __name__ == '__main__': - _test() diff --git a/pymead/core/mea.py b/pymead/core/mea.py deleted file mode 100644 index f0b9697a..00000000 --- a/pymead/core/mea.py +++ /dev/null @@ -1,740 +0,0 @@ -from copy import deepcopy -import importlib.util -import itertools -import numpy as np -import os -import typing - -import benedict - -from pymead.core.airfoil import Airfoil -from pymead.core.anchor_point import AnchorPoint -from pymead.core.free_point import FreePoint -from pymead.core.param import Param -from pymead.core.pos_param import PosParam -from pymead.core.base_airfoil_params import BaseAirfoilParams -from pymead.utils.dict_recursion import set_all_dict_values, assign_airfoil_tags_to_param_dict, \ - assign_names_to_params_in_param_dict, unravel_param_dict_deepcopy -from pymead.utils.read_write_files import save_data -from pymead.plugins.IGES.iges_generator import IGESGenerator -from pymead.plugins.IGES.curves import BezierIGES -from pymead import DATA_DIR, PLUGINS_DIR, INCLUDE_FILES - - -class MEA: - """ - Class for multi-element airfoils. Serves as a container for ``pymead.core.airfoil.Airfoil``s and adds a few methods - important for the Graphical User Interface. - """ - def __init__(self, param_tree=None, airfoils: Airfoil or typing.List[Airfoil, ...] or None = None, - airfoil_graphs_active: bool = False): - self.airfoils = {} - self.file_name = None - self.user_mods = None - self.param_tree = param_tree - self.param_dict = {'Custom': {}} - self.airfoil_graphs_active = airfoil_graphs_active - self.te_thickness_edit_mode = False - self.w = None - self.v = None - if not isinstance(airfoils, list): - if airfoils is not None: - self.add_airfoil(airfoils, 0, param_tree) - else: - for idx, airfoil in enumerate(airfoils): - self.add_airfoil(airfoil, idx, param_tree) - - # def __getstate__(self): - # """ - # Reimplemented to ensure MEA picklability - # """ - # state = self.__dict__.copy() - # if state['v'] is not None: - # state['v'].clear() - # state['w'] = None # Set unpicklable GraphicsLayoutWidget object to None - # state['v'] = None # Set unpicklable ViewBox object to None - # return state - - def add_airfoil(self, airfoil: Airfoil, idx: int, param_tree, w=None, v=None, gui_obj=None): - """ - Add an airfoil at index ``idx`` to the multi-element airfoil container. - - Parameters - ========== - airfoil: Airfoil - Airfoil to insert into the MEA container - - idx: int - Insertion index (0 corresponds to insertion at the beginning of the list) - - param_tree - Parameter Tree from the GUI which is added as an airfoil attribute following insertion - - w - A ``pyqtgraph`` ``GraphicsLayoutWidget`` associated with the GUI. Used to link the airfoil to its graph - in the GUI. - - v - A ``pyqtgraph`` ``PlotDataItem`` associated with the GUI. Used to link the airfoil to its graph in the GUI. - """ - if airfoil.tag is None: - airfoil.tag = f'A{idx}' - # print(f"Adding mea to airfoil {airfoil.tag}") - airfoil.mea = self - self.airfoils[airfoil.tag] = airfoil - self.param_dict[airfoil.tag] = airfoil.param_dicts - # print(f"param_dict = {self.param_dict}") - - set_all_dict_values(self.param_dict[airfoil.tag]) - - assign_airfoil_tags_to_param_dict(self.param_dict[airfoil.tag], airfoil_tag=airfoil.tag) - - assign_names_to_params_in_param_dict(self.param_dict) - - if self.airfoil_graphs_active: - self.add_airfoil_graph_to_airfoil(airfoil, idx, param_tree, w=w, v=v, gui_obj=gui_obj) - - dben = benedict.benedict(self.param_dict) - for k in dben.keypaths(): - param = dben[k] - if isinstance(param, Param): - if param.mea is None: - param.mea = self - if param.mea.param_tree is None: - param.mea.param_tree = self.param_tree - - def remove_airfoil(self, airfoil_tag: str): - # Remove all items from the AirfoilGraph corresponding to this airfoil - airfoil_graph = self.airfoils[airfoil_tag].airfoil_graph - if airfoil_graph is not None: - for curve in self.airfoils[airfoil_tag].curve_list[::-1]: - airfoil_graph.v.removeItem(curve.pg_curve_handle) - airfoil_graph.v.removeItem(airfoil_graph.polygon_item) - airfoil_graph.v.removeItem(airfoil_graph) - - # Remove the airfoil from the ParameterTree - if self.param_tree is not None: - self.param_tree.p.child("Airfoil Parameters").child(airfoil_tag).remove() - # self.param_tree.airfoil_headers.pop(airfoil_tag) - - # Remove the airfoil from the MEA - self.param_dict.pop(airfoil_tag) - self.airfoils.pop(airfoil_tag) - - def assign_names_to_params_in_param_dict(self): - """ - Recursively assigns the name of the airfoil along with all its base parameters, ``FreePoint``s, and - ``AnchorPoints`` to the parameter dictionary. - """ - assign_names_to_params_in_param_dict(self.param_dict) - - def add_airfoil_graph_to_airfoil(self, airfoil: Airfoil, idx: int, param_tree, w=None, v=None, gui_obj=None): - """ - Add a ``pyqtgraph``-based ``pymead.gui.airfoil_graph.AirfoilGraph`` to the airfoil at index ``int``. - """ - from pymead.gui.airfoil_graph import AirfoilGraph - if w is None: - if idx == 0: - airfoil_graph = AirfoilGraph(airfoil) - self.w = airfoil_graph.w - self.v = airfoil_graph.v - # print(f"setting te_thickness_edit_mode of airfoil {airfoil.tag} to {self.te_thickness_edit_mode}") - airfoil_graph.te_thickness_edit_mode = self.te_thickness_edit_mode - else: # Assign the first airfoil's Graphics Window and ViewBox to each subsequent airfoil - airfoil_graph = AirfoilGraph(airfoil, - w=self.airfoils['A0'].airfoil_graph.w, - v=self.airfoils['A0'].airfoil_graph.v, gui_obj=gui_obj) - # print(f"setting te_thickness_edit_mode of airfoil {airfoil.tag} to {self.te_thickness_edit_mode}") - airfoil_graph.te_thickness_edit_mode = self.te_thickness_edit_mode - else: - # print("Creating new AirfoilGraph!") - airfoil_graph = AirfoilGraph(airfoil, w=w, v=v, gui_obj=gui_obj) - # print(f"setting te_thickness_edit_mode of airfoil {airfoil.tag} to {self.te_thickness_edit_mode}") - airfoil_graph.te_thickness_edit_mode = self.te_thickness_edit_mode - self.w = w - self.v = v - - airfoil_graph.param_tree = param_tree - airfoil.airfoil_graph = airfoil_graph - - def count_design_variables(self): - dv = [0] - - def increment_dv_count(d: dict, _dv: list): - for k_, v in d.items(): - if isinstance(v, dict): - increment_dv_count(v, _dv) - else: - if isinstance(v, Param) and not isinstance(v, PosParam): - if v.active and not v.linked: - _dv[0] += 1 - elif isinstance(v, PosParam): - if v.active[0] and not v.linked[0]: - _dv[0] += 1 - if v.active[1] and not v.linked[1]: - _dv[0] += 1 - else: - raise ValueError('Found value in dictionary not of type \'Param\' or \'PosParam\'') - - increment_dv_count(self.param_dict, dv) - - return dv[0] - - def extract_parameters(self): - """ - Extracts the 1-D list of parameters from the airfoil system corresponding to all the parameters with - ``active=True`` and ``linked=False``. Any ``PosParam`` corresponds to two consecutive parameters. All - parameters are normalized between their respective lower bounds (``bounds[0]``) and upper bounds (``bounds[1]``) - such that all values are between 0 and 1. - - Returns - ======= - list - 1-D list of normalized parameter values - """ - - parameter_list = [] - norm_value_list = [] - - def check_for_bounds_recursively(d: dict, bounds_error_=False): - for k_, v in d.items(): - if not bounds_error_: - if isinstance(v, dict): - bounds_error_, param_name_ = check_for_bounds_recursively(v, bounds_error_) - else: - if isinstance(v, Param) and not isinstance(v, PosParam): - if v.active and not v.linked: - if v.bounds[0] == -np.inf or v.bounds[0] == np.inf or v.bounds[1] == -np.inf or v.bounds[1] == np.inf: - bounds_error_ = True - return bounds_error_, v.name - elif isinstance(v, PosParam): - if v.active[0] and not v.linked[0]: - if v.bounds[0][0] == -np.inf or v.bounds[0][0] == np.inf or v.bounds[0][1] == -np.inf or v.bounds[0][1] == np.inf: - bounds_error_ = True - return bounds_error_, v.name - if v.bounds[1][0] == -np.inf or v.bounds[1][0] == np.inf or v.bounds[1][1] == -np.inf or v.bounds[1][1] == np.inf: - bounds_error_ = True - return bounds_error_, v.name - else: - raise ValueError('Found value in dictionary not of type \'Param\' or \'PosParam\'') - else: - return bounds_error_, None - return bounds_error_, None - - def extract_parameters_recursively(d: dict): - for k_, v in d.items(): - if isinstance(v, dict): - extract_parameters_recursively(v) - else: - if isinstance(v, Param) and not isinstance(v, PosParam): - if v.active and not v.linked: - norm_value_list.append((v.value - v.bounds[0]) / (v.bounds[1] - v.bounds[0])) - parameter_list.append(v) - elif isinstance(v, PosParam): - if v.active[0] and not v.linked[0]: - norm_value_list.append((v.value[0] - v.bounds[0][0]) / (v.bounds[0][1] - v.bounds[0][0])) - parameter_list.append(v) - if v.active[1] and not v.linked[1]: - norm_value_list.append((v.value[1] - v.bounds[1][0]) / (v.bounds[1][1] - v.bounds[1][0])) - if v not in parameter_list: # only add the Parameter if it hasn't already been added - parameter_list.append(v) - else: - raise ValueError('Found value in dictionary not of type \'Param\' or \'PosParam\'') - - bounds_error, param_name = check_for_bounds_recursively(self.param_dict) - if bounds_error: - error_message = f'Bounds must be set for each active and unlinked parameter for parameter extraction (at ' \ - f'least one infinite bound found for {param_name})' - print(error_message) - return error_message, None - else: - extract_parameters_recursively(self.param_dict) - # parameter_list = np.loadtxt(os.path.join(DATA_DIR, 'parameter_list.dat')) - # self.update_parameters(parameter_list) - # fig_.savefig(os.path.join(DATA_DIR, 'test_airfoil.png')) - return norm_value_list, parameter_list - - def copy_as_param_dict(self, deactivate_airfoil_graphs: bool = False): - """ - Copies the entire airfoil system as a Python dictionary. This dictionary can later be converted back to an - airfoil system using the class method ``generate_from_param_dict``. - - Parameters - ========== - deactivate_airfoil_graphs: bool - This argument should be set to ``True`` if the target case for re-loading the airfoil system is in a script - rather than the GUI. Default: ``False``. - - Returns - ======= - dict - A Python dictionary describing the airfoil system (corresponds to the ``.jmea`` files in the GUI). - """ - output_dict_ = {} - unravel_param_dict_deepcopy(self.param_dict, output_dict=output_dict_) - for k, v in output_dict_.items(): - if k != 'Custom': - output_dict_[k]['anchor_point_order'] = deepcopy(self.airfoils[k].anchor_point_order) - output_dict_[k]['free_point_order'] = deepcopy(self.airfoils[k].free_point_order) - output_dict_['file_name'] = self.file_name - if deactivate_airfoil_graphs: - output_dict_['airfoil_graphs_active'] = False - else: - output_dict_['airfoil_graphs_active'] = self.airfoil_graphs_active - return deepcopy(output_dict_) - - def save_airfoil_system(self, file_name: str): - """ - Saves the encapsulated airfoil system as a ``.jmea`` file. - - Parameters - ========== - file_name: str - The file location where the MEA is to be stored (relative or absolute path). If the file name does not - end with ".jmea", the ``.jmea`` file extension will be added automatically. - """ - if os.path.splitext(file_name)[-1] != '.jmea': - file_name += '.jmea' - self.file_name = file_name - mea_dict = self.copy_as_param_dict(deactivate_airfoil_graphs=True) - save_data(mea_dict, file_name) - - def deepcopy(self, deactivate_airfoil_graphs: bool = False): - """ - Composite function combining, in order, the ``copy_as_param_dict`` and ``generate_from_param_dict`` methods. - Copying the airfoil system this way avoids the complications associated with using the standard ``deepcopy`` - function on some stored functions and ``Qt`` objects. - - Parameters - ========== - deactivate_airfoil_graphs: bool - Deactivates the airfoil graph objects in the GUI. Default: ``False``. - - Returns - ======= - MEA - Airfoil system object - """ - return MEA.generate_from_param_dict(self.copy_as_param_dict(deactivate_airfoil_graphs)) - - def write_to_IGES(self, file_name: str): - """ - Writes the airfoil system to file using the IGES file format. - - Parameters - ========== - file_name: str - Path to IGES file - """ - bez_IGES_entities = [ - [BezierIGES(np.column_stack((c.P[:, 0], np.zeros(len(c.P)), c.P[:, 1]))) for c in a.curve_list] - for a in self.airfoils.values()] - entities_flattened = list(itertools.chain.from_iterable(bez_IGES_entities)) - iges_generator = IGESGenerator(entities_flattened) - iges_generator.generate(file_name) - - def update_parameters(self, norm_value_list: list or np.ndarray): - """ - Updates the airfoil system using the set of normalized parameter values extracted using ``extract_parameters``. - - Parameters - ========== - norm_value_list: list or np.ndarray - List of normalized parameter values from ``extract_parameters`` - """ - - if isinstance(norm_value_list, list): - norm_value_list = np.array(norm_value_list) - - if norm_value_list.ndim == 0: - norm_value_list = np.array([norm_value_list]) - - def check_for_bounds_recursively(d: dict, bounds_error_=False): - for k, v in d.items(): - if not bounds_error_: - if isinstance(v, dict): - bounds_error_, param_name_ = check_for_bounds_recursively(v, bounds_error_) - else: - if isinstance(v, Param) and not isinstance(v, PosParam): - if v.active and not v.linked: - if v.bounds[0] == -np.inf or v.bounds[0] == np.inf or v.bounds[1] == -np.inf or \ - v.bounds[1] == np.inf: - bounds_error_ = True - return bounds_error_, v.name - elif isinstance(v, PosParam): - if v.active[0] and not v.linked[0]: - if v.bounds[0][0] == -np.inf or v.bounds[0][0] == np.inf or v.bounds[0][1] == -np.inf or v.bounds[0][1] == np.inf: - bounds_error_ = True - return bounds_error_, v.name - if v.active[1] and not v.linked[1]: - if v.bounds[1][0] == -np.inf or v.bounds[1][0] == np.inf or v.bounds[1][1] == -np.inf or v.bounds[1][1] == np.inf: - bounds_error_ = True - return bounds_error_, v.name - else: - raise ValueError('Found value in dictionary not of type \'Param\'') - else: - return bounds_error_, None - return bounds_error_, None - - def update_parameters_recursively(d: dict, list_counter: int): - for k, v in d.items(): - if isinstance(v, dict): - list_counter = update_parameters_recursively(v, list_counter) - else: - if isinstance(v, Param) and not isinstance(v, PosParam): - if v.active and not v.linked: - v.value = norm_value_list[list_counter] * (v.bounds[1] - v.bounds[0]) + v.bounds[0] - # v.update() - list_counter += 1 - elif isinstance(v, PosParam): - temp_xy_value = v.value # set up a temp variable because we want to update x and y simultaneously - if v.active[0] and not v.linked[0]: - temp_xy_value[0] = norm_value_list[list_counter] * (v.bounds[0][1] - v.bounds[0][0]) + v.bounds[0][0] - list_counter += 1 - if v.active[1] and not v.linked[1]: - temp_xy_value[1] = norm_value_list[list_counter] * (v.bounds[1][1] - v.bounds[1][0]) + v.bounds[1][0] - list_counter += 1 - v.value = temp_xy_value # replace the PosParam value with the temp value (unchanged if - # neither x nor y are active) - # print(f"Updated PosParam value! {v.value = }") - else: - raise ValueError('Found value in dictionary not of type \'Param\'') - return list_counter - - bounds_error, param_name = check_for_bounds_recursively(self.param_dict) - if bounds_error: - error_message = f'Bounds must be set for each active and unlinked parameter for parameter update ' \ - f'(at least one infinite bound found for {param_name})' - print(error_message) - return error_message - else: - for _ in range(2): # Do this code twice to ensure everything is updated properly: - update_parameters_recursively(self.param_dict, 0) - - for a_tag, airfoil in self.airfoils.items(): - airfoil.update() - if self.airfoil_graphs_active: - airfoil.airfoil_graph.data['pos'] = airfoil.control_point_array - airfoil.airfoil_graph.updateGraph() - airfoil.airfoil_graph.plot_change_recursive( - airfoil.airfoil_graph.airfoil_parameters.child(a_tag).children()) - - # def deactivate_airfoil_matching_params(self, target_airfoil: str): - # def deactivate_recursively(d: dict): - # for k, v in d.items(): - # if isinstance(v, dict): - # # If the dictionary key is equal to the target_airfoil_str, stop the recursion for this branch, - # # otherwise continue with the recursion: - # if k != target_airfoil: - # deactivate_recursively(v) - # elif isinstance(v, Param) and not isinstance(v, PosParam): - # if v.active: - # v.active = False - # v.deactivated_for_airfoil_matching = True - # elif isinstance(v, PosParam): - # if v.active[0]: - # v.active[0] = False - # v.deactivated_for_airfoil_matching = True - # if v.active[1]: - # v.active[1] = False - # v.deactivated_for_airfoil_matching = True - # else: - # raise ValueError('Found value in dictionary not of type \'Param\'') - # - # deactivate_recursively(self.param_dict) - # deactivate_target_params = ['dx', 'dy', 'alf', 'c', 't_te', 'r_te', 'phi_te'] - # for p_str in deactivate_target_params: - # p = self.airfoils[target_airfoil].param_dicts['Base'][p_str] - # if p.active: - # p.active = False - # p.deactivated_for_airfoil_matching = True - # - # def activate_airfoil_matching_params(self, target_airfoil: str): - # def activate_recursively(d: dict): - # for k, v in d.items(): - # if isinstance(v, dict): - # # If the dictionary key is equal to the target_airfoil_str, stop the recursion for this branch, - # # otherwise continue with the recursion: - # if k != target_airfoil: - # activate_recursively(v) - # elif isinstance(v, Param): - # if v.deactivated_for_airfoil_matching: - # v.active = True - # v.deactivated_for_airfoil_matching = False - # else: - # raise ValueError('Found value in dictionary not of type \'Param\'') - # - # activate_recursively(self.param_dict) - # activate_target_params = ['dx', 'dy', 'alf', 'c'] - # for p_str in activate_target_params: - # p = self.airfoils[target_airfoil].param_dicts['Base'][p_str] - # if p.deactivated_for_airfoil_matching: - # p.active = True - # p.deactivated_for_airfoil_matching = False - - def calculate_max_x_extent(self): - """ - Calculates the maximum :math:`x`-value of the airfoil system using the absolute location of the trailing edge - of each airfoil. - - Returns - ======= - float - Left-most trailing edge position of all airfoils in the airfoil system - """ - x = None - for a in self.airfoils.values(): - x_max = a.c.value * np.cos(a.alf.value) + a.dx.value - if x is None: - x = x_max - else: - if x_max > x: - x = x_max - return x - - def add_custom_parameters(self, params: dict): - if 'Custom' not in self.param_dict.keys(): - self.param_dict['Custom'] = {} - for k, v in params.items(): - if hasattr(v['value'], '__iter__'): - self.param_dict['Custom'][k] = PosParam(**v) - else: - self.param_dict['Custom'][k] = Param(**v) - self.param_dict['Custom'][k].param_dict = self.param_dict - self.param_dict['Custom'][k].mea = self - - def get_keys(self): - """ - Used in ``pymead.core.parameter_tree.MEAParamTree`` to update the equation variable AutoCompleter - """ - d_ben = benedict.benedict(self.param_dict) - keypaths = d_ben.keypaths() - elems_to_remove = [] - for idx, elem in enumerate(keypaths): - split = elem.split('.') # separate the keys into lists at the periods - if len(split) < 3 and not split[0] == 'Custom': - elems_to_remove.append(idx) - if len(split) > 1: - if len(split) < 4 and split[1] in ['FreePoints', 'AnchorPoints'] and idx not in elems_to_remove: - elems_to_remove.append(idx) - - for rem in elems_to_remove[::-1]: - keypaths.pop(rem) - - for idx, el in enumerate(keypaths): - keypaths[idx] = '$' + el - - return keypaths - - def get_curve_bounds(self): - """ - Calculates the :math:`x`- and :math:`y`-ranges corresponding to the rectangle which just encapsulates the entire - airfoil system. - - Returns - ======= - typing.Tuple[tuple] - :math:`x`- and :math:`y`-ranges in the format ``(x_min,x_max), (y_min,y_max)`` - """ - x_range, y_range = (None, None), (None, None) - for a in self.airfoils.values(): - if not a.airfoil_graph: - raise ValueError('pyqtgraph curves must be initialized to get curve bounds') - for c in a.curve_list: - curve = c.pg_curve_handle - x_lims = curve.dataBounds(ax=0) - y_lims = curve.dataBounds(ax=1) - if x_range[0] is None: - x_range = x_lims - y_range = y_lims - else: - if x_lims[0] < x_range[0]: - x_range = (x_lims[0], x_range[1]) - if x_lims[1] > x_range[1]: - x_range = (x_range[0], x_lims[1]) - if y_lims[0] < y_range[0]: - y_range = (y_lims[0], y_range[1]) - if y_lims[1] > y_range[1]: - y_range = (y_range[0], y_lims[1]) - - # Check if control points are out of range - x_ctrlpt_range = (np.min(c.P[:, 0]), np.max(c.P[:, 0])) - y_ctrlpt_range = (np.min(c.P[:, 1]), np.max(c.P[:, 1])) - if x_ctrlpt_range[0] < x_range[0]: - x_range = (x_ctrlpt_range[0], x_range[1]) - if x_ctrlpt_range[1] > x_range[1]: - x_range = (x_range[0], x_ctrlpt_range[1]) - if y_ctrlpt_range[0] < y_range[0]: - y_range = (y_ctrlpt_range[0], y_range[1]) - if y_ctrlpt_range[1] > y_range[1]: - y_range = (y_range[0], y_ctrlpt_range[1]) - return x_range, y_range - - def get_ctrlpt_dict(self, zero_col: int = 1): - """ - Gets the set of ControlPoints for each airfoil curve and arranges them in a format appropriate for the - JSON file format. The keys at the top level of the dict represent the airfoil name, and each value contains a - 3-D list. The slices of the list represent the Bézier curve, the rows represent the ControlPoints, and the - columns represent :math:`x`, :math:`y`, and :math:`z`. - - Parameters - ========== - zero_col: int - The column into which the row of zeros should be placed to map the 2-D airfoil control points into 3-D space. - For example, inserting into the first column means the airfoil will be located in the X-Z plane. Valid values: - 0, 1, or 2. Default: 1. - - Returns - ======= - dict - The dictionary containing the ControlPoints. - """ - ctrlpt_dict = {} - for a_name, a in self.airfoils.items(): - a.update() - ctrlpts = [] - for c in a.curve_list: - P = deepcopy(c.P) - P = np.insert(P, zero_col, 0.0, axis=1) - ctrlpts.append(P) - ctrlpt_dict[a_name] = ctrlpts - return ctrlpt_dict - - def write_NX_macro(self, fname: str, opts: dict): - with open(fname, 'w') as f: - for import_ in ['math', 'NXOpen', 'NXOpen.Features', 'NXOpen.GeometricUtilities', 'time']: - f.write(f'import {import_}\n') - - with open(os.path.join(PLUGINS_DIR, 'NX', 'journal_functions.py'), 'r') as g: - f.writelines(g.readlines()) - - ctrlpt_dict = self.get_ctrlpt_dict(zero_col=2) - for k, ctrlpts in ctrlpt_dict.items(): - new_ctrlpts = np.array(ctrlpts) - new_ctrlpts *= 36.98030879 * 1000 - ctrlpt_dict[k] = new_ctrlpts.tolist() - - f.write('ctrlpts = {\n') - for a_name, ctrlpts in ctrlpt_dict.items(): - f.write(f' "{a_name}": [\n') - for ctrlpt_set in ctrlpts: - f.write(f' [\n') - for ctrlpt in ctrlpt_set: - f.write(f' [{ctrlpt[0]}, {ctrlpt[1]}, {ctrlpt[2]}],\n') - f.write(f' ],\n') - f.write(f' ]\n') - f.write('}\n\n') - - f.write('create_bezier_curve_from_ctrlpts(ctrlpts)\n') - - for k, v in ctrlpt_dict.items(): - for idx, v2 in enumerate(v): - v[idx] = v2.tolist() - - save_data(ctrlpt_dict, 'center_profile_2_ctrlpts.json') - - @classmethod - def generate_from_param_dict(cls, param_dict: dict): - """ - Reconstruct an MEA from the MEA's JSON-saved param_dict. This is the normal way of saving an airfoil system - in pymead because it avoids issues associated with serializing portions of the MEA. It also allows for direct - modification of the save file due to the human-readable JSON save format. - - Parameters - ========== - param_dict: dict - A Python dictionary constructed by using ``json.load`` on a ``.jmea`` file. - - Returns - ======= - MEA - An instance of the ``pymead.core.mea.MEA`` class. - """ - base_params_dict = {k: v['Base'] for k, v in param_dict.items() if isinstance(v, dict) and 'Base' in v.keys()} - base_params = {} - for airfoil_name, airfoil_base_dict in base_params_dict.items(): - base_params[airfoil_name] = {} - for pname, pdict in airfoil_base_dict.items(): - base_params[airfoil_name][pname] = Param.from_param_dict(pdict) - airfoil_list = [] - for airfoil_name, airfoil_base_dict in base_params.items(): - base = BaseAirfoilParams(airfoil_tag=airfoil_name, **airfoil_base_dict) - airfoil_list.append(Airfoil(base_airfoil_params=base, tag=airfoil_name)) - mea = cls(airfoils=airfoil_list) # Constructor overload - mea.airfoil_graphs_active = param_dict['airfoil_graphs_active'] # set this after MEA instantiation to avoid - # initializing the graphs - mea.file_name = param_dict['file_name'] - for a_name, airfoil in mea.airfoils.items(): - ap_order = param_dict[a_name]['anchor_point_order'] - aps = param_dict[a_name]['AnchorPoints'] - for idx, ap_name in enumerate(ap_order): - if ap_name not in ['te_1', 'le', 'te_2']: - ap_dict = aps[ap_name] - ap_param_dict = {} - for pname, pdict in ap_dict.items(): - if pname == 'xy': - ap_param_dict[pname] = PosParam.from_param_dict(pdict) - else: - ap_param_dict[pname] = Param.from_param_dict(pdict) - # Create an AnchorPoint from the saved parameter dictionary: - ap = AnchorPoint(airfoil_tag=a_name, tag=ap_name, previous_anchor_point=ap_order[idx - 1], - **ap_param_dict) - - # Now, insert the AnchorPoint into the Airfoil - airfoil.insert_anchor_point(ap) - - for ap_name, fp_list in param_dict[a_name]['free_point_order'].items(): - fps = param_dict[a_name]['FreePoints'][ap_name] - for idx, fp_name in enumerate(fp_list): - fp_dict = fps[fp_name] - fp_param_dict = {} - for pname, pdict in fp_dict.items(): - fp_param_dict[pname] = PosParam.from_param_dict(pdict) - - previous_fp = fp_list[idx - 1] if idx > 0 else None - # Create a FreePoint from the saved parameter dictionary: - fp = FreePoint(airfoil_tag=a_name, tag=fp_name, previous_anchor_point=ap_name, - previous_free_point=previous_fp, **fp_param_dict) - - # Now, insert the FreePoint into the Airfoil - airfoil.insert_free_point(fp) - - for custom_name, custom_param in param_dict['Custom'].items(): - custom_param_dict = {} - temp_dict = {'value': custom_param['_value']} - for attr_name, attr_value in custom_param.items(): - if attr_name in ['bounds', 'active', 'func_str', 'name']: - temp_dict[attr_name] = attr_value - custom_param_dict[custom_name] = deepcopy(temp_dict) # Need deepcopy here? - mea.add_custom_parameters(custom_param_dict) - - mea.assign_names_to_params_in_param_dict() - - mea.user_mods = {} - for f in INCLUDE_FILES: - name = os.path.split(f)[-1] # get the name of the file without the directory - name_no_ext = os.path.splitext(name)[-2] # get the name of the file without the .py extension - spec = importlib.util.spec_from_file_location(name_no_ext, f) - mea.user_mods[name_no_ext] = importlib.util.module_from_spec(spec) # generate the module from the name - spec.loader.exec_module(mea.user_mods[name_no_ext]) # compile and execute the module - - def f(d, key, value): - if isinstance(value, Param) or isinstance(value, PosParam): - value.function_dict['name'] = value.name.split('.')[-1] - value.update() - - dben = benedict.benedict(mea.param_dict) - dben.traverse(f) - - for a in mea.airfoils.values(): - a.update() - - return mea - - def remove_airfoil_graphs(self): - """ - Removes the airfoil graph from each airfoil in the system. - """ - self.airfoil_graphs_active = False - for a in self.airfoils.values(): - a.airfoil_graph = None diff --git a/pymead/core/mea2.py b/pymead/core/mea2.py index 6961cc1e..0e2066eb 100644 --- a/pymead/core/mea2.py +++ b/pymead/core/mea2.py @@ -1,7 +1,11 @@ +import itertools import typing import os import numpy as np +from pymead.plugins.IGES.iges_generator import IGESGenerator + +from pymead.plugins.IGES.curves import BezierIGES from pymead.core.airfoil2 import Airfoil from pymead.core.pymead_obj import PymeadObj @@ -69,5 +73,21 @@ def write_mses_blade_file(self, return blade_file_path + def write_to_IGES(self, file_name: str): + """ + Writes the airfoil system to file using the IGES file format. + + Parameters + ========== + file_name: str + Path to IGES file + """ + bez_IGES_entities = [ + [BezierIGES(np.column_stack((c.P[:, 0], np.zeros(len(c.P)), c.P[:, 1]))) for c in a.curve_list] + for a in self.airfoils.values()] + entities_flattened = list(itertools.chain.from_iterable(bez_IGES_entities)) + iges_generator = IGESGenerator(entities_flattened) + iges_generator.generate(file_name) + def get_dict_rep(self): return {"airfoils": [a.name() for a in self.airfoils]} diff --git a/pymead/core/param.py b/pymead/core/param.py deleted file mode 100644 index 5b947fac..00000000 --- a/pymead/core/param.py +++ /dev/null @@ -1,396 +0,0 @@ -import benedict -import numpy as np -import math -import typing -from pymead.core.transformation import AirfoilTransformation -from pymead.utils.geometry import map_angle_m180_p180 - - -class Param: - - def __init__(self, value: float or typing.Tuple[float], bounds: tuple = (-np.inf, np.inf), - active: bool or typing.Tuple[bool] = True, linked: bool or typing.Tuple[bool] = False, - func_str: str = None, name: str = None, periodic: bool = False): - """ - This is the class used to define parameters used for the airfoil and airfoil parametrization definitions - in pymead. - - ### Args: - - `value`: a `float` representing the value of the parameter - - `bounds`: a `list` or 1D `np.ndarray` with two elements of the form `[, ]`. Used in - `pymead.utils.airfoil_matching` and for normalization during parameter extraction. Default: - `np.array([-np.inf, np.inf])` (no normalization). - - `active`: a `bool` stating whether the parameter is active. If `False`, direct and indirect write access to the - parameter are restricted. Default: `True`. - - `linked`: a `bool` stating whether the parameter is linked to another parameter (i.e., whether the parameter - has an active function). If `True`, direct write access to the parameter is restricted, but indirect write - access is still allowed (via modification of parameters to which the current parameter is linked). - Default: `False`. - - `name`: an optional `str` that gives the name of the parameter. Can be useful in identifying extracted - parameters. - - ### Returns: - - An instance of the `pymead.core.param.Param` class. - """ - self._value = list(value) if isinstance(value, tuple) else value - self.bounds = list(bounds) if isinstance(bounds, tuple) else bounds - self._name = None - self.name = name - - self.active = list(active) if isinstance(active, tuple) or isinstance(active, list) else active - self.linked = list(linked) if isinstance(linked, tuple) or isinstance(active, list) else linked - self.periodic = periodic - self.func_str = func_str - self.func = None - if self.func_str is not None: - if isinstance(self.linked, list): - self.linked = [True, True] - else: - self.linked = True - self.function_dict = {'depends': {}, 'name': self.name.split('.')[-1] if self.name is not None else None} - self.depends_on = {} - self.affects = [] - self.tag_matrix = None - self.user_func_strs = None - self.tag_list = None - self.airfoil_tag = None - self.sets_airfoil_csys = False - self.mea = None - self.deactivated_for_airfoil_matching = False - self.at_boundary = False - self.free_point = None - self.anchor_point = None - - # def __setattr__(self, key, value): - # return super().__setattr__(key, value) - - @property - def name(self): - return self._name - - @name.setter - def name(self, n: str): - self._name = n - if n is not None and n.split('.')[-1] in ['c', 'alf', 'dx', 'dy']: - self.sets_airfoil_csys = True - - @property - def value(self): - return self._value - - @value.setter - def value(self, v): - if self.active: - old_transformation, new_transformation, airfoil = None, None, None - if self.sets_airfoil_csys and self.mea is not None and self.airfoil_tag is not None: - airfoil = self.mea.airfoils[self.airfoil_tag] - old_transformation = AirfoilTransformation(dx=airfoil.dx.value, dy=airfoil.dy.value, - alf=airfoil.alf.value, c=airfoil.c.value) - old_value = self._value - if self.periodic: - # Treatment of the periodic case (only applies to angles) - two_pi = 2 * np.pi - v = map_angle_m180_p180(v) - - if not (np.isinf(self.bounds[0]) or np.isinf(self.bounds[1])): - - if self.bounds[0] % two_pi < self.bounds[1] % two_pi: - - # Calculate the angle half-way between the bounds in the out-of-bounds region - dist_b1_2pi = two_pi - self.bounds[1] - dist_0_b0 = self.bounds[0] - dist_total = dist_b1_2pi + dist_0_b0 - mid_ob_angle = self.bounds[1] + dist_total / 2 - - if self.bounds[0] <= v <= self.bounds[1]: - self._value = v - self.at_boundary = False - elif v % two_pi < self.bounds[0] % two_pi or v % two_pi > mid_ob_angle % two_pi: - self._value = self.bounds[0] - self.at_boundary = True - elif self.bounds[1] % two_pi < v % two_pi <= mid_ob_angle % two_pi: - self._value = self.bounds[1] - self.at_boundary = True - else: - raise ValueError("Somehow found an angle value whose 2*pi modulo was " - "outside the range [0, 2*pi]") - else: # This is usually the case when the lower bound is a negative angle and the upper bound is a - # positive angle - - # Calculate the angle half-way between the bounds in the out-of-bounds region - mid_ob_angle = np.mean([self.bounds[0] % two_pi, self.bounds[1] % two_pi]) - - if v % two_pi >= self.bounds[0] % two_pi or v % two_pi <= self.bounds[1] % two_pi: - self._value = v - self.at_boundary = False - elif mid_ob_angle % two_pi < v % two_pi < self.bounds[0] % two_pi: - self._value = self.bounds[0] - self.at_boundary = True - elif self.bounds[1] % two_pi < v % two_pi <= mid_ob_angle % two_pi: - self._value = self.bounds[1] - self.at_boundary = True - else: - if np.isinf(self.bounds[0]) and np.isfinite(self.bounds[1]): - if v > self.bounds[1]: - self._value = self.bounds[1] - self.at_boundary = True - else: - self._value = v - self.at_boundary = False - elif np.isfinite(self.bounds[0]) and np.isinf(self.bounds[1]): - if v < self.bounds[0]: - self._value = self.bounds[0] - self.at_boundary = True - else: - self._value = v - self.at_boundary = False - elif np.isinf(self.bounds[0]) and np.isinf(self.bounds[1]): - self._value = v - self.at_boundary = False - else: - # Non-periodic treatment - if v < self.bounds[0]: - self._value = self.bounds[0] - self.at_boundary = True - elif v > self.bounds[1]: - self._value = self.bounds[1] - self.at_boundary = True - else: - self._value = v - self.at_boundary = False - - self.update_affected_params(old_value) - if self.sets_airfoil_csys and self.mea is not None and self.airfoil_tag is not None: - new_transformation = AirfoilTransformation(dx=airfoil.dx.value, dy=airfoil.dy.value, - alf=airfoil.alf.value, c=airfoil.c.value) - self.update_ap_fp(self, old_transformation=old_transformation, new_transformation=new_transformation) - - def update_affected_params(self, old_value): - if len(self.affects) > 0: - idx = 0 - any_affected_params_at_boundary = False - old_affected_param_values = [] - for idx, affected_param in enumerate(self.affects): - old_affected_param_values.append(affected_param.value) - affected_param.update() - self.update_ap_fp(affected_param) - if affected_param.at_boundary: - any_affected_params_at_boundary = True - break - if any_affected_params_at_boundary: - self._value = old_value - for idx2, affected_param in enumerate(self.affects[:idx + 1]): - affected_param._value = old_affected_param_values[idx2] - self.update_ap_fp(affected_param) - - @staticmethod - def update_ap_fp(param, old_transformation: AirfoilTransformation = None, - new_transformation: AirfoilTransformation = None): - if old_transformation is not None and new_transformation is not None: - for ap_tag in param.mea.airfoils[param.airfoil_tag].free_points.keys(): - for fp in param.mea.airfoils[param.airfoil_tag].free_points[ap_tag].values(): - old_coords = np.array([fp.xy.value]) - new_coords = new_transformation.transform_abs(old_transformation.transform_rel(old_coords)) - # print(f"{old_coords = }, {new_coords = }") - if fp.xy.linked[0] or not fp.xy.active[0]: - new_coords[0][0] = old_coords[0][0] - if fp.xy.linked[1] or not fp.xy.active[1]: - new_coords[0][1] = old_coords[0][1] - fp.xy.value = new_coords[0].tolist() - fp.set_ctrlpt_value() - for ap in param.mea.airfoils[param.airfoil_tag].anchor_points: - if ap.tag not in ['te_1', 'le', 'te_2']: - old_coords = np.array([ap.xy.value]) - new_coords = new_transformation.transform_abs(old_transformation.transform_rel(old_coords)) - # print(f"{old_coords = }, {new_coords = }") - if ap.xy.linked[0] or not ap.xy.active[0]: - new_coords[0][0] = old_coords[0][0] - if ap.xy.linked[1] or not ap.xy.active[1]: - new_coords[0][1] = old_coords[0][1] - ap.xy.value = new_coords[0].tolist() - ap.set_ctrlpt_value() - else: - if param.free_point is not None: - param.free_point.set_ctrlpt_value() - elif param.anchor_point is not None: - param.anchor_point.set_ctrlpt_value() - - def set_func_str(self, func_str: str): - if len(func_str) == 0: - self.remove_func() - else: - if len(self.function_dict) == 0: - self.function_dict = {'depends': {}, 'name': self.name.split('.')[-1] if self.name is not None else None} - self.func_str = func_str - self.linked = True - - def remove_func(self): - self.func_str = None - self.function_dict = {'depends': {}, 'name': self.name.split('.')[-1] if self.name is not None else None} - self.linked = False - for parameter in self.depends_on.values(): - if self in parameter.affects: - parameter.affects.remove(self) - self.depends_on = {} - - def update_function(self, show_q_error_messages: bool, func_str_changed: bool = False): - if self.func_str is None: - pass - else: - if func_str_changed: - # Convert the function string into a Python function and determine parameters present in string: - math_function_list, user_function_list = self.parse_update_function_str() - - # Add any math functions detected from the func_str: - for s in math_function_list: - if s not in self.function_dict.keys(): - if s in vars(math).keys(): - self.function_dict[s] = vars(math)[s] - - for s in user_function_list: - if s not in self.function_dict.keys(): - s_list = s.split('.') - mod_name = s_list[0] - func_name = s_list[1] - if self.mea.param_tree is not None: - self.function_dict[func_name] = getattr(self.mea.param_tree.user_mods[mod_name], func_name) - else: - self.function_dict[func_name] = getattr(self.mea.user_mods[mod_name], func_name) - - # Add the variables the function depends on to the function_dict: - self.add_dependencies(show_q_error_messages) - - self.update_dependencies() - - if self.func is not None: - self.function_dict['__builtins__'] = {} - exec(self.func, self.function_dict) - - def update_value(self): - if self.func_str is None: - pass - else: - if 'f' not in self.function_dict.keys(): # TODO: test this fix for affected Params not updating - self.update_function(show_q_error_messages=True, func_str_changed=True) - self.value = self.function_dict['f']() # no parameters passed as inputs (inputs all stored and updated - - def update(self, show_q_error_messages: bool = True, func_str_changed: bool = False): - self.update_function(show_q_error_messages, func_str_changed) - self.update_value() - - def parse_update_function_str(self): - self.tag_matrix = [] - self.user_func_strs = [] - self.func = 'def f(): return ' - math_functions_to_include = [] - appending, appending_user_func = False, False - append_new_to_math_function_list = True - for ch_idx, ch in enumerate(self.func_str): - if appending: - if ch.isalnum() or ch == '_': - self.tag_matrix[-1][-1] += ch - elif ch == '.': - self.tag_matrix[-1].append('') - else: - self.func += '"]' - appending = False - if appending_user_func: - if ch == '(' and appending_user_func: - appending_user_func = False - if ch == '$': - self.tag_matrix.append(['']) - appending = True - self.func += 'depends["' - elif ch == '.': - self.func += '.' - if appending_user_func: - self.user_func_strs[-1] += '.' - elif ch == '^': - self.user_func_strs.append('') - appending_user_func = True - else: - self.func += ch - if appending and ch_idx == len(self.func_str) - 1: - self.func += '"]' - if not appending and ch.isalnum(): - if append_new_to_math_function_list: - math_functions_to_include.append('') - math_functions_to_include[-1] += ch - append_new_to_math_function_list = False - if appending_user_func: - self.user_func_strs[-1] += ch - if not appending and not ch.isalnum(): - append_new_to_math_function_list = True - - for user_func_str in self.user_func_strs: - self.func = self.func.replace(user_func_str, user_func_str.split('.')[-1]) - - def concatenate_strings(lst: list): - tag = '' - for idx, s in enumerate(lst): - tag += s - if idx < len(lst) - 1: - tag += '.' - return tag - - self.tag_list = [concatenate_strings(tl) for tl in self.tag_matrix] - for t in self.tag_list: - self.depends_on[t] = None - - return math_functions_to_include, self.user_func_strs - - def add_dependencies(self, show_q_error_messages: bool): - - def get_nested_dict_val(d: dict, tag): - dben = benedict.benedict(d) - return dben[tag] - - for idx, t in enumerate(self.tag_list): - self.depends_on[t] = get_nested_dict_val(self.mea.param_dict, t) - if self.depends_on[t] is not None: - if self not in self.depends_on[t].affects: - self.depends_on[t].affects.append(self) - if self.depends_on[t] is None: - self.depends_on = {} - message = f"Could not compile input function string: {self.func_str}" - self.remove_func() - if not show_q_error_messages: - print(message) - return False - - return True - - def update_dependencies(self): - for key, value in self.depends_on.items(): - self.function_dict['depends'][key] = value.value - - @classmethod - def from_param_dict(cls, param_dict: dict): - """Generates a Param from a JSON-saved param_dict (aids in backward/forward compatibility)""" - temp_dict = {'value': param_dict['_value']} - for attr_name, attr_value in param_dict.items(): - if attr_name in ['bounds', 'active', 'linked', 'func_str', 'name']: - temp_dict[attr_name] = attr_value - return cls(**temp_dict) - - -if __name__ == '__main__': - from pymead.core.mea import MEA - from pymead.core.airfoil import Airfoil - from pymead.core.free_point import FreePoint - mea = MEA(airfoils=Airfoil()) - mea.airfoils['A0'].insert_free_point(FreePoint(x=Param(0.5), y=Param(0.1), previous_anchor_point='te_1')) - mea.param_dict['A0']['FP0']['x'].func_str = '6.0 * $A0.FP0.y + 0.01' - mea.param_dict['A0']['FP0']['y'].value = 0.15 - mea.param_dict['A0']['FP0']['x'].mea = mea - mea.param_dict['A0']['FP0']['x'].update() - mea.add_custom_parameters({'r_htf': dict(value=0.38)}) - mea.param_dict['A0']['FP0']['x'].func_str = '$CUSTOM.r_htf + cos(0.005)' - mea.param_dict['A0']['FP0']['x'].update() - pass diff --git a/pymead/core/parametric_curve.py b/pymead/core/parametric_curve.py deleted file mode 100644 index 733be6fb..00000000 --- a/pymead/core/parametric_curve.py +++ /dev/null @@ -1,75 +0,0 @@ -import matplotlib.pyplot as plt -from abc import abstractmethod - - -class ParametricCurve: - - def __init__(self, t, x, y, px, py, ppx, ppy, k, R): - self.t = t - self.x = x - self.y = y - self.px = px - self.py = py - self.ppx = ppx - self.ppy = ppy - self.k = k - self.R = R - self.comb_heads = None - self.comb_tails = None - self.plt_handle_curve = None - self.plt_handles_normals = None - self.plt_handle_comb_curves = None - self.pg_curve_handle = None - self.scale_factor = None - self.comb_interval = None - - def plot_curve(self, axs: plt.axes, **plt_kwargs): - self.plt_handle_curve, = axs.plot(self.x, self.y, **plt_kwargs) - return self.plt_handle_curve - - def init_curve_pg(self, v, pen): - self.pg_curve_handle = v.plot(pen=pen) - - def update_curve(self): - self.plt_handle_curve.set_xdata(self.x) - self.plt_handle_curve.set_ydata(self.y) - - def update_curve_pg(self): - self.pg_curve_handle.setData(self.x, self.y) - - def clear_curve_pg(self): - self.pg_curve_handle.clear() - - @abstractmethod - def get_curvature_comb(self, max_k_normalized_scale_factor, interval: int = 1): - pass - - def plot_curvature_comb_normals(self, axs: plt.axes, scale_factor, interval: int = 1, **plt_kwargs): - self.scale_factor = scale_factor - self.comb_interval = interval - if self.comb_heads is None or self.comb_tails is None: - self.get_curvature_comb(scale_factor, interval) - self.plt_handles_normals = [] - for idx, head in enumerate(self.comb_heads): - h, = axs.plot([head[0], self.comb_tails[idx, 0]], [head[1], self.comb_tails[idx, 1]], **plt_kwargs) - self.plt_handles_normals.append(h) - - def update_curvature_comb_normals(self): - self.get_curvature_comb(self.scale_factor, self.comb_interval) - for idx, head in enumerate(self.comb_heads): - self.plt_handles_normals[idx].set_xdata([head[0], self.comb_tails[idx, 0]]) - self.plt_handles_normals[idx].set_ydata([head[1], self.comb_tails[idx, 1]]) - - def plot_curvature_comb_curve(self, axs: plt.axes, scale_factor, interval: int = 1, **plt_kwargs): - self.scale_factor = scale_factor - self.comb_interval = interval - if self.comb_heads is None or self.comb_tails is None: - self.get_curvature_comb(scale_factor, interval) - self.plt_handle_comb_curves, = axs.plot(self.comb_heads[:, 0], self.comb_heads[:, 1], **plt_kwargs) - # print(self.plt_handle_comb_curves) - - def update_curvature_comb_curve(self): - if self.comb_heads is None or self.comb_tails is None: - self.get_curvature_comb(self.scale_factor, self.comb_interval) - self.plt_handle_comb_curves.set_xdata(self.comb_heads[:, 0]) - self.plt_handle_comb_curves.set_ydata(self.comb_heads[:, 1]) diff --git a/pymead/core/parametric_curve2.py b/pymead/core/parametric_curve2.py index ff5759ff..2435469c 100644 --- a/pymead/core/parametric_curve2.py +++ b/pymead/core/parametric_curve2.py @@ -20,6 +20,22 @@ def __init__(self, t: np.ndarray, xy: np.ndarray, xpyp: np.ndarray, xppypp: np.n def plot(self, ax: plt.Axes, **kwargs): ax.plot(self.xy[:, 0], self.xy[:, 1], **kwargs) + def get_curvature_comb(self, max_k_normalized_scale_factor, interval: int = 1): + first_deriv_mag = np.hypot(self.xpyp[:, 0], self.xpyp[:, 1]) + comb_heads_x = self.xy[:, 0] - self.xpyp[:, 1] / first_deriv_mag * self.k * max_k_normalized_scale_factor + comb_heads_y = self.xy[:, 1] + self.xpyp[:, 0] / first_deriv_mag * self.k * max_k_normalized_scale_factor + # Stack the x and y columns (except for the last x and y values) horizontally and keep only the rows by the + # specified interval: + comb_tails = np.column_stack((self.xy[:, 0], self.xy[:, 1]))[:-1:interval, :] + comb_heads = np.column_stack((comb_heads_x, comb_heads_y))[:-1:interval, :] + # Add the last x and y values onto the end (to make sure they do not get skipped with input interval) + comb_tails = np.row_stack((comb_tails, np.array([self.xy[-1, 0], self.xy[-1, 1]]))) + comb_heads = np.row_stack((comb_heads, np.array([comb_heads_x[-1], comb_heads_y[-1]]))) + return comb_tails, comb_heads + + def approximate_arc_length(self): + return np.sum(np.hypot(self.xy[1:, 0] - self.xy[:-1, 0], self.xy[1:, 1] - self.xy[:-1, 1])) + class ParametricCurve(PymeadObj, ABC): def __init__(self, sub_container: str, reference: bool = False): diff --git a/pymead/core/pos_param.py b/pymead/core/pos_param.py deleted file mode 100644 index 6e7f37bd..00000000 --- a/pymead/core/pos_param.py +++ /dev/null @@ -1,223 +0,0 @@ -from pymead.core.param import Param -import numpy as np -import typing -import re - - -class PosParam(Param): - def __init__(self, value: tuple, bounds: typing.Tuple[tuple] = ((-np.inf, np.inf), (-np.inf, np.inf)), - active: typing.Tuple[bool] = (True, True), linked: typing.Tuple[bool] = (False, False), - func_str: str = None, name: str = None): - super().__init__(value=value, active=active, bounds=bounds, linked=linked, func_str=func_str, name=name) - if self.func_str is not None: - self.parse_func_str_for_linked() # override the singularly-valued linked bool with a list of bools - - @property - def value(self): - return self._value - - @value.setter - def value(self, v): - update_x = self.active[0] - update_y = self.active[1] - old_value = self._value - if update_x: - if v[0] < self.bounds[0][0]: - self._value[0] = self.bounds[0][0] - self.at_boundary = True - elif v[0] > self.bounds[0][1]: - self._value[0] = self.bounds[0][1] - self.at_boundary = True - else: - self._value[0] = v[0] - self.at_boundary = False - if update_y: - if v[1] < self.bounds[1][0]: - self._value[1] = self.bounds[1][0] - self.at_boundary = True - elif v[1] > self.bounds[1][1]: - self._value[1] = self.bounds[1][1] - self.at_boundary = True - else: - self._value[1] = v[1] - self.at_boundary = False - - if update_x or update_y: - self.update_affected_params(old_value) - - if self.free_point is not None: - self.free_point.ctrlpt.xp = self._value[0] - self.free_point.ctrlpt.yp = self._value[1] - elif self.anchor_point is not None: - self.anchor_point.ctrlpt.xp = self._value[0] - self.anchor_point.ctrlpt.yp = self._value[1] - pass - - def set_func_str(self, func_str: str): - if len(func_str) == 0: - self.remove_func() - else: - if len(self.function_dict) == 0: - self.function_dict = {'depends': {}, 'name': self.name.split('.')[-1] if self.name is not None else None} - self.func_str = func_str - self.parse_func_str_for_linked() - - def remove_func(self): - self.func_str = None - self.function_dict = {'depends': {}, 'name': self.name.split('.')[-1] if self.name is not None else None} - self.linked = [False, False] - for parameter in self.depends_on.values(): - if self in parameter.affects: - parameter.affects.remove(self) - self.depends_on = {} - - def parse_func_str_for_linked(self): - str_before_comma = '' - str_after_comma = '' - appending_before_comma, appending_after_comma = False, False - for ch in self.func_str: - if appending_before_comma: - str_before_comma += ch - elif appending_after_comma: - str_after_comma += ch - if ch == '{': - appending_before_comma = True - elif ch == ',': - appending_before_comma = False - appending_after_comma = True - elif ch == '}': - break - - if len(str_before_comma) > 0 and 'None' not in str_before_comma: - self.linked[0] = True - else: - self.linked[0] = False - if len(str_after_comma) > 0 and 'None' not in str_after_comma: - self.linked[1] = True - else: - self.linked[1] = False - - if "symmetry" in self.func_str: - self.linked = [True, True] - - def update_value(self): - if self.func_str is None: - pass - else: - if 'f' not in self.function_dict.keys(): - self.update_function(show_q_error_messages=True, func_str_changed=True) - temp_value = self.function_dict['f']() - if temp_value[0] is None and temp_value[1] is None: - raise ValueError('Must set at least one of the two cells in the PosParam equation.') - if temp_value[0] is None: - self.value = [self._value[0], temp_value[1]] - elif temp_value[1] is None: - self.value = [temp_value[0], self._value[1]] - else: - self.value = temp_value - # self.value = self.function_dict['f']() # no parameters passed as inputs (inputs all stored and updated - # inside self.function_dict ) - - # def update(self, show_q_error_messages: bool = True, func_str_changed: bool = False): - # self.update_function(show_q_error_messages, func_str_changed) - # if func_str_changed: - # match1 = re.search(r'\{\s+,', self.func) - # match2 = re.search(r',\s+\}', self.func) - # if match1: - # self.func = self.func.replace(match1.group(0), '{None, ') - # if match2: - # self.func = self.func.replace(match2.group(0), ', None}') - # self.update_value() - - def parse_update_function_str(self): - self.tag_matrix = [] - self.user_func_strs = [] - self.func = 'def f(): return ' - math_functions_to_include = [] - appending, appending_user_func = False, False - append_new_to_math_function_list = True - for ch_idx, ch in enumerate(self.func_str): - if appending: - if ch.isalnum() or ch == '_': - self.tag_matrix[-1][-1] += ch - elif ch == '.': - self.tag_matrix[-1].append('') - else: - self.func += '"]' - appending = False - if appending_user_func: - if ch == '(' and appending_user_func: - appending_user_func = False - if ch == '$': - self.tag_matrix.append(['']) - appending = True - self.func += 'depends["' - elif ch == '.': - self.func += '.' - if appending_user_func: - self.user_func_strs[-1] += '.' - elif ch == '^': - self.user_func_strs.append('') - appending_user_func = True - elif ch == '{': - self.func += '[' - elif ch == '}': - self.func += ']' - else: - self.func += ch - if appending and ch_idx == len(self.func_str) - 1: - self.func += '"]' - if not appending and ch.isalnum(): - if append_new_to_math_function_list: - math_functions_to_include.append('') - math_functions_to_include[-1] += ch - append_new_to_math_function_list = False - if appending_user_func: - self.user_func_strs[-1] += ch - if not appending and not ch.isalnum(): - append_new_to_math_function_list = True - - for user_func_str in self.user_func_strs: - self.func = self.func.replace(user_func_str, user_func_str.split('.')[-1]) - - def concatenate_strings(lst: list): - tag = '' - for idx, s in enumerate(lst): - tag += s - if idx < len(lst) - 1: - tag += '.' - return tag - - self.tag_list = [concatenate_strings(tl) for tl in self.tag_matrix] - for t in self.tag_list: - self.depends_on[t] = None - - return math_functions_to_include, self.user_func_strs - - @classmethod - def from_param_dict(cls, param_dict: dict): - """Generates a Param from a JSON-saved param_dict (aids in backward/forward compatibility)""" - temp_dict = {'value': param_dict['_value']} - for attr_name, attr_value in param_dict.items(): - if attr_name in ['bounds', 'active', 'linked', 'func_str', 'name']: - temp_dict[attr_name] = attr_value - return cls(**temp_dict) - - -def main(): - from pymead.core.mea import MEA - from pymead.core.airfoil import Airfoil - mea = MEA(airfoils=[Airfoil()]) - pos_param_1 = {'value': (0.5, 0.3)} - pos_param_2 = {'value': (0.2, 0.1)} - mea.add_custom_parameters({'xy1': pos_param_1, 'xy2': pos_param_2}) - mea.param_dict['Custom']['xy2'].set_func_str('{$Custom.xy1[0] + 0.1, $Custom.xy1[1] + 0.5}') - mea.param_dict['Custom']['xy2'].update(func_str_changed=True) - mea.param_dict['Custom']['xy2'].set_func_str('{$Custom.xy1[0] + 0.3, None}') - mea.param_dict['Custom']['xy2'].update(func_str_changed=True) - # pos_param_2.set_func_str('') - pass - - -if __name__ == '__main__': - main() diff --git a/pymead/core/symmetry.py b/pymead/core/symmetry.py deleted file mode 100644 index 4fcfbc3c..00000000 --- a/pymead/core/symmetry.py +++ /dev/null @@ -1,52 +0,0 @@ -from pymead.core.line import InfiniteLine -from pymead.utils.transformations import transform_matrix -import numpy as np - - -def symmetry(name: str, xy=None, alf_target=None, alf_tool=None, c_target=None, c_tool=None, - dx_target=None, dx_tool=None, dy_target=None, dy_tool=None, upper_target=None, upper_tool=None, - phi=None, psi1=None, psi2=None, r=None, L=None, R=None, x1=None, y1=None, x2=None, y2=None, m=None, - theta_rad=None, theta_deg=None): - new_x, new_y, rel_phi_target = None, None, None - if name in ['xy', 'phi']: - inf_line = InfiniteLine(x1=x1, y1=y1, x2=x2, y2=y2, m=m, theta_rad=theta_rad, theta_deg=theta_deg) - if name == 'xy': - std_coeffs = inf_line.get_standard_form_coeffs() - distance = (std_coeffs['A'] * xy[0] + std_coeffs['B'] * xy[1] + std_coeffs['C'] - ) / np.hypot(std_coeffs['A'], std_coeffs['B']) - over_under_value = y1 - xy[1] - inf_line.m * (x1 - xy[0]) - if over_under_value > 0: - angle = inf_line.theta_rad + np.pi / 2 - elif over_under_value < 0: - angle = inf_line.theta_rad - np.pi / 2 - else: - angle = 0.0 - distance = 0.0 - distance = abs(distance) - new_x = xy[0] + 2 * distance * np.cos(angle) - new_y = xy[1] + 2 * distance * np.sin(angle) - else: - if upper_tool: - abs_phi_tool = phi + (-alf_tool) - else: - abs_phi_tool = -phi + (-alf_tool) - delta_angle = abs_phi_tool - inf_line.theta_rad - abs_phi_target = inf_line.theta_rad - delta_angle - if upper_target: - rel_phi_target = abs_phi_target + alf_target - else: - rel_phi_target = -abs_phi_target + (-alf_target) - if L is not None: - L *= c_tool / c_target - if R is not None: - R *= c_tool / c_target - output_dict = { - 'xy': [new_x, new_y], - 'phi': rel_phi_target, - 'psi1': psi1, - 'psi2': psi2, - 'r': r, - 'L': L, - 'R': R, - } - return output_dict[name] diff --git a/pymead/core/trailing_edge_point.py b/pymead/core/trailing_edge_point.py deleted file mode 100644 index 9e49b362..00000000 --- a/pymead/core/trailing_edge_point.py +++ /dev/null @@ -1,157 +0,0 @@ -import numpy as np - -from pymead.core.param import Param -from pymead.core.control_point import ControlPoint - - -class TrailingEdgePoint(ControlPoint): - - def __init__(self, - c: Param, - r: Param, - t: Param, - phi: Param, - L: Param, - theta: Param, - upper: bool - ): - """ - Similar to the ``pymead.core.anchor_point.AnchorPoint``, except only two ControlPoints are generated instead - of five (either the "g1_plus" point for the upper surface or the "g1_minus" point for the lower surface). - Under normal circumstances, no instance of this class should be directly generated by the user. Instead, the - values for TrailingEdgePoints will be generated when ``BaseAirfoilParams()`` is called. - - Parameters - ========== - c: Param - The chord length of the airfoil associated with this TrailingEdgePoint. - - r: Param - See ``r_te`` in ``pymead.core.base_airfoil_params.BaseAirfoilParams``. - - t: Param - See ``t_te`` in ``pymead.core.base_airfoil_params.BaseAirfoilParams``. - - phi: Param - See ``phi_te`` in ``pymead.core.base_airfoil_params.BaseAirfoilParams``. - - L: Param - Equivalent to ``L1_te`` in ``pymead.core.base_airfoil_params.BaseAirfoilParams`` if ``upper==True`` or - ``L2_te`` if ``upper==False``. - - theta: Param - Equivalent to ``theta1_te`` in ``pymead.core.base_airfoil_params.BaseAirfoilParams`` if ``upper==True`` or - ``theta2_te`` if ``upper==False``. - - upper: bool - If ``upper==True``, this TrailingEdgePoint will apply to the Airfoil upper surface. Otherwise, this - TrailingEdgePoint will apply to the Airfoil lower surface. - - Returns - ======= - TrailingEdgePoint - An instance of the ``TrailingEdgePoint`` class. - """ - - self.c = c - self.r = r - self.t = t - self.phi = phi - self.L = L - self.theta = theta - self.upper = upper - - self.ctrlpt = None - self.tangent_ctrlpt = None - self.ctrlpt_branch_array = None - self.ctrlpt_branch_list = None - self.ctrlpt_branch_generated = False - - if self.upper: - xy = np.array([1, 0]) + self.r.value * self.t.value * np.array([np.cos(np.pi / 2 + self.phi.value), - np.sin(np.pi / 2 + self.phi.value)]) - tag = 'te_1' - else: - xy = np.array([1, 0]) + (1 - self.r.value) * self.t.value * \ - np.array([np.cos(3 * np.pi / 2 + self.phi.value), np.sin(3 * np.pi / 2 + self.phi.value)]) - tag = 'te_2' - - super().__init__(xy[0], xy[1], tag, tag) - - self.ctrlpt = ControlPoint(xy[0], xy[1], tag, tag, cp_type='anchor_point') - - def __repr__(self): - return f"anchor_point_{self.tag}" - - def set_te_points(self): - """ - Generates only the trailing edge points (not the adjacent points). - """ - if self.upper: - xy = np.array([1, 0]) + self.r.value * self.t.value * np.array([np.cos(np.pi / 2 + self.phi.value), - np.sin(np.pi / 2 + self.phi.value)]) - tag = 'te_1' - else: - xy = np.array([1, 0]) + (1 - self.r.value) * self.t.value * \ - np.array([np.cos(3 * np.pi / 2 + self.phi.value), np.sin(3 * np.pi / 2 + self.phi.value)]) - tag = 'te_2' - - super().__init__(xy[0], xy[1], tag, tag) - - self.ctrlpt = ControlPoint(xy[0], xy[1], tag, tag, cp_type='anchor_point') - - def generate_anchor_point_branch(self): - """ - Generate the set of ControlPoints associated with this TrailingEdgePoint. - """ - - self.set_te_points() - - def generate_tangent_seg_ctrlpts(): - - self.ctrlpt_branch_generated = True - - if self.upper: - xy = np.array([self.x_val, self.y_val]) + self.L.value * np.array([np.cos(np.pi - self.theta.value), - np.sin(np.pi - self.theta.value)]) - return ControlPoint(xy[0], xy[1], f'{repr(self)}_g1_plus', self.tag, cp_type='g1_plus') - else: - xy = np.array([self.x_val, self.y_val]) + self.L.value * np.array([np.cos(np.pi + self.theta.value), - np.sin(np.pi + self.theta.value)]) - return ControlPoint(xy[0], xy[1], f'{repr(self)}_g1_minus', self.tag, cp_type='g1_minus') - - self.tangent_ctrlpt = generate_tangent_seg_ctrlpts() - - if self.upper: - self.ctrlpt_branch_array = np.array([[self.xp, self.yp], - [self.tangent_ctrlpt.xp, self.tangent_ctrlpt.yp]]) - self.ctrlpt_branch_list = [self.ctrlpt, self.tangent_ctrlpt] - else: - self.ctrlpt_branch_array = np.array([[self.tangent_ctrlpt.xp, self.tangent_ctrlpt.yp], - [self.xp, self.yp]]) - self.ctrlpt_branch_list = [self.tangent_ctrlpt, self.ctrlpt] - - def recalculate_ap_branch_props_from_g1_pt(self, minus_plus: str, measured_phi, measured_Lt): - """ - [GUI only] Recalculates the TrailingEdgePoint AnchorPoint branch based on the measured angle and distance from - between the trailing edge ControlPoint and its adjacent ControlPoint. - - Parameters - ========== - minus_plus: str - If ``"minus"``, set the value of ``theta`` with a counter-clockwise orientation with respect to the chordline. - If ``"plus"``, set the value of ``theta`` with a clockwise orientation with respect to the chordline. - - measured_phi - Measured absolute trailing edge angle (rad). - - measured_Lt - Measured trailing edge length. - """ - if self.L.active and not self.L.linked: - self.L.value = measured_Lt - if self.theta.active and not self.theta.linked: - if minus_plus == 'minus': - self.theta.value = measured_phi - np.pi - else: - self.theta.value = -measured_phi + np.pi diff --git a/pymead/gui/airfoil_statistics.py b/pymead/gui/airfoil_statistics.py index 69951c53..f475b213 100644 --- a/pymead/gui/airfoil_statistics.py +++ b/pymead/gui/airfoil_statistics.py @@ -1,4 +1,4 @@ -from pymead.core.mea import MEA +from pymead.core.mea2 import MEA import pandas as pd from PyQt5.QtWidgets import QTextEdit, QDialog, QVBoxLayout diff --git a/pymead/gui/gui.py b/pymead/gui/gui.py index 5deb5f22..583f3ef7 100644 --- a/pymead/gui/gui.py +++ b/pymead/gui/gui.py @@ -52,7 +52,7 @@ from pymead.analysis.single_element_inviscid import single_element_inviscid from pymead.gui.text_area import ConsoleTextArea from pymead.gui.dockable_tab_widget import PymeadDockWidget -from pymead.core.mea import MEA +from pymead.core.mea2 import MEA from pymead.analysis.calc_aero_data import calculate_aero_data from pymead.utils.read_write_files import load_data, save_data from pymead.utils.misc import count_func_strs diff --git a/pymead/gui/gui_settings/buttons.json b/pymead/gui/gui_settings/buttons.json index e392a623..2d07b319 100644 --- a/pymead/gui/gui_settings/buttons.json +++ b/pymead/gui/gui_settings/buttons.json @@ -12,32 +12,6 @@ "checked-by-default": false, "function": "change_background_color_button_toggled" }, - "add-airfoil": { - "icon": "Add-Icon3.png", - "status_tip": "Add airfoil (click on the graph to complete action)", - "checkable": false, - "function": "add_airfoil_button_toggled" - }, - "te-thickness": { - "icon": "thickness_icon.png", - "status_tip": "Toggle trailing edge thickness edit mode", - "checkable": true, - "checked-by-default": false, - "function": "te_thickness_mode_toggled" - }, - "symmetry": { - "icon": "symmetry.png", - "status_tip": "Make a pair of FreePoints or AnchorPoints symmetric about a specified line", - "checkable": false, - "function": "on_symmetry_button_pressed" - }, - "pos-constraint": { - "icon": "target_icon.png", - "status_tip": "Constrain the relative position of two FreePoints or AnchorPoints", - "checkable": false, - "checked-by-default": false, - "function": "on_pos_constraint_pressed" - }, "draw-point": { "icon": "point.png", "status_tip": "Draw points", diff --git a/pymead/gui/input_dialog.py b/pymead/gui/input_dialog.py index 0c0bf98c..34f8856c 100644 --- a/pymead/gui/input_dialog.py +++ b/pymead/gui/input_dialog.py @@ -13,7 +13,8 @@ from pymead.core.airfoil2 import Airfoil from pymead.core.geometry_collection import GeometryCollection -from pymead.core.mea import MEA +# from pymead.core.mea import MEA +from pymead.core.mea2 import MEA from pymead.gui.sampling_visualization import SamplingVisualizationWidget from pymead.gui.infty_doublespinbox import InftyDoubleSpinBox from pymead.gui.pyqt_vertical_tab_widget.pyqt_vertical_tab_widget import VerticalTabWidget diff --git a/pymead/gui/main_icon_toolbar.py b/pymead/gui/main_icon_toolbar.py index cde4fce3..57ac937b 100644 --- a/pymead/gui/main_icon_toolbar.py +++ b/pymead/gui/main_icon_toolbar.py @@ -3,10 +3,6 @@ from PyQt5.QtCore import pyqtSlot import os import pyqtgraph as pg -from pymead.core.airfoil import Airfoil -from pymead.core.base_airfoil_params import BaseAirfoilParams -from pymead.core.param import Param -from pymead.core.pos_param import PosParam from pymead.utils.read_write_files import load_data from pymead.gui.input_dialog import SymmetryDialog # from pymead.core.symmetry import symmetry @@ -80,155 +76,6 @@ def change_background_color_button_toggled(self, checked): self.parent.show() - def add_airfoil_button_toggled(self): - - def scene_clicked(ev): - self.new_airfoil_location = self.parent.v.vb.mapSceneToView(ev.scenePos()) - self.parent.v.scene().sigMouseClicked.disconnect() - airfoil = Airfoil(base_airfoil_params=BaseAirfoilParams(dx=Param(self.new_airfoil_location.x()), - dy=Param(self.new_airfoil_location.y()))) - self.parent.add_airfoil(airfoil) - - self.parent.v.scene().sigMouseClicked.connect(scene_clicked) - - def te_thickness_mode_toggled(self, checked): - if checked: - for airfoil in self.parent.mea.airfoils.values(): - airfoil.airfoil_graph.te_thickness_edit_mode = True - self.parent.te_thickness_edit_mode = True - else: - for airfoil in self.parent.mea.airfoils.values(): - airfoil.airfoil_graph.te_thickness_edit_mode = False - self.parent.te_thickness_edit_mode = False - - @pyqtSlot(str) - def symmetry_connection(self, obj: str): - if self.symmetry_dialog: - self.symmetry_dialog.inputs[self.symmetry_dialog.current_form_idx][1].setText(obj) - - def on_symmetry_button_pressed(self): - self.parent.param_tree_instance.t.sigSymmetry.connect(self.symmetry_connection) # connect parameter selection - # to the QLineEdits in the dialog - self.symmetry_dialog = SymmetryDialog(self) # generate the dialog - self.symmetry_dialog.show() # show the dialog - self.symmetry_dialog.accepted.connect(self.make_symmetric) # apply symmetry equations once OK is pressed - - def make_symmetric(self): - def get_grandchild(param_tree, child_list: list, param_name: str): - current_param = param_tree.child(target_list[0]) - for idx in range(1, len(child_list)): - current_param = current_param.child(child_list[idx]) - full_param_name = f"{'.'.join(child_list)}.{param_name}" - return current_param.child(full_param_name) - - def assign_equation(param_name: str): - param = get_grandchild(airfoil_param_tree, target_list, param_name) - self.parent.param_tree_instance.add_equation_box(param) - eq = param.child('Equation Definition') - upper_target, upper_tool = False, False - if fp_or_ap == 'ap': - ap_order_target = self.parent.mea.airfoils[target_list[0]].anchor_point_order - ap_order_tool = self.parent.mea.airfoils[tool_list[0]].anchor_point_order - ap_target = target_list[2] # e.g., 'ap0' - ap_tool = tool_list[2] # e.g., 'ap1' - if ap_order_target.index(ap_target) < ap_order_target.index('le'): - upper_target = True - if ap_order_tool.index(ap_tool) < ap_order_tool.index('le'): - upper_tool = True - extra_args = { - 'xy': f"x1={out['x1']}, y1={out['y1']}, theta_rad={out['angle']}, xy={out['tool']}.xy, " - f"alf_tool={tool_base}.alf, alf_target={target_base}.alf, c_tool={tool_base}.c," - f" c_target={target_base}.c, dx_tool={tool_base}.dx, dx_target={target_base}.dx, " - f"dy_tool={tool_base}.dy, dy_target={target_base}.dy", - 'phi': f"x1={out['x1']}, y1={out['y1']}, theta_rad={out['angle']}, alf_tool={tool_base}.alf, " - f"alf_target={target_base}.alf, phi={out['tool']}.phi, upper_target={upper_target}, " - f"upper_tool={upper_tool}", - 'psi1': f"psi1={out['tool']}.psi1", - 'psi2': f"psi2={out['tool']}.psi2", - 'r': f"r={out['tool']}.r", - 'L': f"L={out['tool']}.L, c_target={target_base}.c, c_tool={tool_base}.c", - 'R': f"R={out['tool']}.R, c_target={target_base}.c, c_tool={tool_base}.c", - } - eq_string = f"^symmetry.symmetry(name, {extra_args[param_name]})" - self.parent.param_tree_instance.block_changes(eq) - eq.setValue(eq_string) - self.parent.param_tree_instance.flush_changes(eq) - self.parent.param_tree_instance.update_equation(eq, eq_string) - if isinstance(param.airfoil_param, PosParam): - param.airfoil_param.linked = (True, True) - else: - param.airfoil_param.linked = True - - airfoil_param_tree = self.parent.param_tree_instance.p.child('Airfoil Parameters') - out = self.symmetry_dialog.valuesFromWidgets() - target = out['target'].replace('$', '') - target_list = target.split('.') - tool = out['tool'].replace('$', '') - tool_list = tool.split('.') - tool_base = f"${tool_list[0]}.Base" - target_base = f"${target_list[0]}.Base" - if 'FreePoints' in target_list: - fp_or_ap = 'fp' - assign_equation('xy') - elif 'AnchorPoints' in target_list: - fp_or_ap = 'ap' - for param_str in ['xy', 'phi', 'psi1', 'psi2', 'L', 'r', 'R']: - assign_equation(param_str) - else: - raise ValueError('Target selection must be either a FreePoint or an AnchorPoint') - - @pyqtSlot(str) - def pos_constraint_connection(self, obj: str): - if self.pos_constraint_dialog: - self.pos_constraint_dialog.inputs[self.pos_constraint_dialog.current_form_idx][1].setText(obj) - - def on_pos_constraint_pressed(self): - self.parent.param_tree_instance.t.sigPosConstraint.connect( - self.pos_constraint_connection) # connect parameter selection to the QLineEdits in the dialog - self.pos_constraint_dialog = PosConstraintDialog(self) # generate the dialog - self.pos_constraint_dialog.show() # show the dialog - self.pos_constraint_dialog.accepted.connect(self.constrain_position) # apply symmetry equations once OK is pressed - - def constrain_position(self): - - def get_grandchild(param_tree, child_list: list, param_name: str = None): - # print(f"{target_list = }") - current_param = param_tree.child(target_list[0]) - # print(f"{current_param = }") - for idx in range(1, len(child_list)): - current_param = current_param.child(child_list[idx]) - # print(f"Now, {current_param = }") - if param_name is not None: - full_param_name = f"{'.'.join(child_list)}.{param_name}" - return current_param.child(full_param_name) - else: - return current_param - - airfoil_param_tree = self.parent.param_tree_instance.p.child('Airfoil Parameters') - out = self.pos_constraint_dialog.valuesFromWidgets() - target = out['target'].replace('$', '') - target_list = target.split('.') - tool = out['tool'].replace('$', '') - tool_list = tool.split('.') - if 'Custom' in target_list: - param = get_grandchild(airfoil_param_tree, target_list) - else: - param = get_grandchild(airfoil_param_tree, target_list, 'xy') - if 'Custom' not in tool_list: - out['tool'] = out['tool'] + '.xy' - self.parent.param_tree_instance.add_equation_box(param) - eq = param.child('Equation Definition') - if 'dx' in out.keys(): - eq_string = "{%s[0] + %s, %s[1] + %s}" % (out['tool'], out['dx'], out['tool'], out['dy']) - else: - eq_string = "{%s[0] + %s * cos(%s), %s[1] + %s * sin(%s)}" % (out['tool'], out['dist'], - out['angle'], out['tool'], - out['dist'], out['angle']) - self.parent.param_tree_instance.block_changes(eq) - eq.setValue(eq_string) - self.parent.param_tree_instance.flush_changes(eq) - self.parent.param_tree_instance.update_equation(eq, eq_string) - def on_draw_points_pressed(self): self.parent.airfoil_canvas.drawPoints() diff --git a/pymead/gui/sampling_visualization.py b/pymead/gui/sampling_visualization.py index 110a436e..e1406bb9 100644 --- a/pymead/gui/sampling_visualization.py +++ b/pymead/gui/sampling_visualization.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QWidget, QDoubleSpinBox, QSpinBox, QLabel from PyQt5.QtWidgets import QGridLayout -from pymead.core.mea import MEA +# from pymead.core.mea import MEA from pymead.optimization.sampling import ConstrictedRandomSampling diff --git a/pymead/optimization/opt_callback.py b/pymead/optimization/opt_callback.py index 83848456..97142a67 100644 --- a/pymead/optimization/opt_callback.py +++ b/pymead/optimization/opt_callback.py @@ -3,7 +3,7 @@ from PyQt5.QtCore import QObject from PyQt5.QtGui import QFontDatabase -from pymead.core.mea import MEA +from pymead.core.mea2 import MEA from pymead.gui.opt_airfoil_graph import OptAirfoilGraph from pymead.gui.parallel_coords_graph import ParallelCoordsGraph from pymead.gui.aero_forces_graphs import DragGraph, CpGraph diff --git a/pymead/optimization/sampling.py b/pymead/optimization/sampling.py index f5ba9c97..b0f263f6 100644 --- a/pymead/optimization/sampling.py +++ b/pymead/optimization/sampling.py @@ -1,7 +1,7 @@ from pymoo.operators.sampling.lhs import LatinHypercubeSampling from pymoo.core.problem import Problem from pymead.utils.read_write_files import load_data, save_data -from pymead.core.mea import MEA +# from pymead.core.mea import MEA from matplotlib import pyplot as plt from random import randint, random import numpy as np diff --git a/pymead/optimization/shape_optimization.py b/pymead/optimization/shape_optimization.py index e2cab2fa..d3ca9314 100644 --- a/pymead/optimization/shape_optimization.py +++ b/pymead/optimization/shape_optimization.py @@ -9,7 +9,7 @@ import os import random -from pymead.core.mea import MEA +from pymead.core.mea2 import MEA from pymead.optimization.opt_setup import CustomDisplay, TPAIOPT from pymead.utils.read_write_files import load_data, save_data from pymead.optimization.pop_chrom import Chromosome, Population diff --git a/pymead/utils/airfoil_matching.py b/pymead/utils/airfoil_matching.py index 8f67f00b..6c25ae1f 100644 --- a/pymead/utils/airfoil_matching.py +++ b/pymead/utils/airfoil_matching.py @@ -10,7 +10,7 @@ from shapely.geometry import Polygon from pymead.utils.get_airfoil import extract_data_from_airfoiltools -from pymead.core.mea import MEA +from pymead.core.mea2 import MEA from copy import deepcopy