Source code for hotwing_core.profile

from __future__ import division
from .coordinate import Coordinate
from .surface import Surface
import re
import copy
import os
import sys
import logging
try:
    # For Python 3.0 and later
    import urllib.request
except ImportError:
    # Fall back to Python 2's urllib2
    from urllib2 import urlopen

logging.getLogger(__name__)


[docs]class Profile(): """ A Profile holds a representation of an airfoil, consisting of two Surface objects defining the top and bottom. The job of the Profile is to load a profile depending on the input the user initialized it with and maintain the Surfaces. The Profile __init__ method can take a variety of different inputs using method overloading. Initialization Method Overloading: Profile(filepath): filepath: String-like object containing the file path to dat file to load Profile(url): url: String-like object containing a URL to the dat file to load Profile(coordinates) coordinates: List of Coordinate Objects to create Profile from Profile(top_surface, bottom_surface) top_surface: Surface - top surface to create Profile from bottom_surface: Surface - bottom surface to create Profile from :ivar top: Surface object that makes up the top of the Profile :ivar bottom: Surface object that makes up the bottom of the Profile """ def __init__(self, *args): if len(args) == 1 and isinstance(args[0], list): # list of coordinates self.top, self.bottom = self._split_profile(args[0]) elif len(args) == 1 and isinstance(args[0], str): if "http://" in args[0] or "https://" in args[0]: # received a URL self.top, self.bottom = self._load_dat_from_url(args[0]) else: # received a filename self.top, self.bottom = self._load_dat_file(args[0]) elif len(args) == 2 and isinstance(args[0], Surface) and isinstance(args[1], Surface): # received two surfaces self.top = args[0] self.bottom = args[1] else: raise AttributeError @property def x_bounds(self): """ Get the min and max X values in the Profile. Returns: Tuple of Floats: (min_x,max_x) - containing the min and max X values in the profile. """ min_x = min((self.bottom.left.x, self.top.left.x)) max_x = max((self.bottom.right.x, self.top.right.x)) return (min_x, max_x) @property def y_bounds(self): """ Get the min and max Y values in the Profile. Returns: Tuple of Floats: (min_y,max_y) - containing the min and max y values in the profile. """ top_min, top_max = self.top.bounds bottom_min, bottom_max = self.bottom.bounds min_y = min((top_min.y, bottom_min.y)) max_y = max((top_max.y, bottom_max.y)) return (min_y, max_y) @property def left_midpoint(self): """ Find an XY position at the left-most value of the Profile half way vertically between the two Surfaces 1) Finds the X value for the leftmost point in the Profile 2) Finds/Interpolates the Coordinates on the top and bottom Surfaces for X 3) Finds the point in between the Coordinates, which is a coordinate vertically mid-way between the points Returns: Coordinate: """ min_x, max_x = self.x_bounds top_c = self.top.interpolate(min_x) bottom_c = self.bottom.interpolate(min_x) average_y = (top_c.y + bottom_c.y) / 2 return Coordinate(min_x, average_y) @property def right_midpoint(self): """ Find a XY position at the right-most value of the Profile half way between vertically the two Surfaces 1) Finds the X value for the right most point in the Profile 2) Finds/Interpolates the Coordinates on the top and bottom Surfaces for X 3) Finds the point in between the Coordinates, which is a coordinate vertically mid-way between the points Returns: Coordinate: """ min_x, max_x = self.x_bounds top_c = self.top.interpolate(max_x) bottom_c = self.bottom.interpolate(max_x) average_y = (top_c.y + bottom_c.y) / 2 return Coordinate(max_x, average_y)
[docs] @classmethod def rotate(cls, origin, profile, angle): """ Rotate a Profile around a point. Args: origin (Coordinate): object that defines the point to rotate profile around profile (Profile): object to rotate angle (Float): degrees to rotate profile. Returns: Profile: New rotated Profile """ top_coords = Surface.rotate(origin, profile.top, angle) bot_coords = Surface.rotate(origin, profile.bottom, angle) return cls(top_coords, bot_coords)
[docs] @classmethod def copy(cls, profile): """ Create a copy of a Profile Args: profile (Profile): object to copy Returns: Profile: new copied Profile object """ top = profile.top bottom = profile.bottom return Profile(top, bottom)
[docs] @classmethod def scale(cls, profile, scale): """ Scale a Profile Delagates to the Surface objects' scale method Args: profile (Profile): object to scale scale (Float): - value to scale profile by. Value of 1 will produce no effect; 2 will double the size; 0.5 will reduce to half the size. Returns: Profile: new scaled Profile """ top = Surface.scale(profile.top, scale) bottom = Surface.scale(profile.bottom, scale) return Profile(top, bottom)
[docs] @classmethod def translate(cls, profile, offset): """ Offset a Profile left, right, up or down. Delagates to the Surface objects' translate method Args: profile (Profile): object to offset offset (Coordinate): - Amount to offset the Profile in the x and y directions - this value is simply added to each of the coordinates in the Profile's Surfaces. Returns: Profile: new offset Profile """ top = Surface.translate(profile.top, offset) bottom = Surface.translate(profile.bottom, offset) return Profile(top, bottom)
[docs] @classmethod def offset_around_profile(cls, profile, top_offset, bottom_offset): """ Offset each of a Profile's Surfaces around itself. It can be thought of as scaling the Profile's Surfaces inward or outward around the Surfaces. Delagates to the Surface objects' offset_around_surface method Args: profile (Profile): object to offset top_offset (Float): - Positive value expands profile (offsets upwards/outwards), negative value contracs profile (offsets downwards/inwards). bottom_offset (Float): - Positive value expands profile (offsets downwards/outwards), negative value contracs profile (offsets upwards/inwards). Returns: Profile: new offset Profile """ top = Surface.offset_around_surface(profile.top, top_offset) bottom = Surface.offset_around_surface(profile.bottom, -bottom_offset) return Profile(top, bottom)
[docs] @classmethod def trim(cls, profile, x_min=None, x_max=None): """ Trim a Profile's Surfaces to new starting and ending x values. IMPORTANT - If you specify a value smaller than the min or larger than the max, by default those values will be interpolated and may actually make the width of the surface larger. Delagates trim to the Surface objects Args: profile (Profile): object to trim x_min (Float): left-most value to trim the profile to. x_max (Float): right-most value to trim the profile to. Returns: Profile - new trimmed Profile """ top = Surface.trim(profile.top, x_min, x_max) bottom = Surface.trim(profile.bottom, x_min, x_max) return Profile(top, bottom)
[docs] @classmethod def trim_overlap(cls, profile): """ Trims a Profiles Surfaces where they overlap. After manipulating Surfaces, the profiles may now overlap. This method finds this point using the class' _find_convergence_points method and then trims the profiles based on the results. If no over lap(s) is/are found, not trimming will occur. Args: profile (Profile): object to trim. Returns: Profile: new trimmed Profile """ x1, x2 = profile._find_convergence_points() if x1 is None: x1 = profile.x_bounds[0] if x2 is None: x2 = profile.x_bounds[1] obj = cls.trim(profile, x1, x2) return obj
[docs] @classmethod def interpolate_new_profile( cls, p1, p2, dist_between, dist_interp, points=200): """ Create a new Profile interpolated from two other Profiles. Delagates to the Surface objects. Args: p1 (Profile): first profile to interpolate from p2 (Profile): second profile to interpolate from dist_between (Float): Distance between profiles dist_interp (Float): Distance from s1 where new profile should be interpolated points (Int): Number of points to use for interpolating each surface of the profile Returns: Profile: New Profile interpolated from s1 and s2 """ top = Surface.interpolate_new_surface( p1.top, p2.top, dist_between, dist_interp, points) bot = Surface.interpolate_new_surface( p1.bottom, p2.bottom, dist_between, dist_interp, points) return Profile(top, bot)
def _load_dat_file(self, f): """ Read the contents of a Selig-formatted file and loads the coordinates into the Class' coordinates attribute. Args: f (String):String-like object containing the file path to a Selig-formatted file. Returns: Tuple of (two) Profiles : (top_profile, bottom_profile) """ coordinates = [] with open(f, "r") as f: for line in f.readlines(): c = self._parse_dat_file_line(line) if c: coordinates.append(c) self.all_coordinates = coordinates return self._split_profile(coordinates) def _load_dat_from_url(self, url): """ Download the contents of a Selig-formatted file and loads the coordinates into the Class' coordinates attribute. Args: f (String): String-like object containing a URL to the Selig-formatted. Make sure you include http:// or https:// in the URL Returns: Tuple of (two) Profiles : (top_profile, bottom_profile) """ if "urllib.request" in sys.modules: req = urllib.request.Request(url) res = urllib.request.urlopen(req) contents = res.read().decode('utf-8') else: # Python 2 r = urlopen(url) contents = r.read() coordinates = [] for line in contents.splitlines(): c = self._parse_dat_file_line(line) if c: coordinates.append(c) self.all_coordinates = coordinates return self._split_profile(coordinates) def _parse_dat_file_line(self, line): """ Take a single line for a dat file and parse out any coordinates """ p = re.compile("^\s*(-*\d*\.\d*)\s*(-*\d*\.\d*)") m = p.match(line) if m: try: x = float(m.group(1)) y = float(m.group(2)) except ValueError: # error converting to float - ignore this line return None if x > 1 and y > 1: # the Lednicer format specifies the coord count - ignore this return None c = Coordinate(x,y) return c return None def _split_profile(self, coordinates): """ Split the coordinates into top and bottom surfaces Args: coordinates (Coordinates[]): list of coordinates Returns: Tuple of (two) Profiles : (top_profile, bottom_profile) """ # determine direction inc_count = 0 dec_count = 0 # test the x direction of the first 5 coordinates for i in range(4): c1 = coordinates[i] c2 = coordinates[i+1] if c2.x > c1.x: inc_count += 1 else: dec_count += 1 if inc_count > dec_count: # get rightmost point max_x = max([c.x for c in coordinates]) for i, c in enumerate(coordinates): if c.x == max_x: coordinate_to_split_on = i break top = coordinates[:coordinate_to_split_on + 1] bottom = coordinates[coordinate_to_split_on+1:] else: # get leftmost point min_x = min([c.x for c in coordinates]) for i, c in enumerate(coordinates): if c.x == min_x: coordinate_to_split_on = i break top = coordinates[:coordinate_to_split_on + 1] bottom = coordinates[coordinate_to_split_on:] # coordinate_to_split_on = len(coordinates) # for i,c in enumerate(coordinates): # if c.y < 0: # coordinate_to_split_on = i # break return (Surface(top), Surface(bottom)) def _find_convergence_points(self): """ Find convergence points of a Class' Profiles. Searches for convergence points from the middle of the profile to the left bound and then the middle of the profile to the right bound. Uses the Class' _find_convergence_point method to perform the search. Returns: Tuple of Floats: ex: (0.23,0.92), (0.23,None), (None,0.92), (None,None) """ left_bound, right_bound = self.x_bounds midpoint = (left_bound + right_bound) / 2 left = self._find_convergence_point(midpoint, left_bound) right = self._find_convergence_point(midpoint, right_bound) if left == right: return None, None return left, right def _find_convergence_point(self, x_start, x_stop, accuracy=0.000001): """ Finds convergence points of a Class' Profiles between two x positions. Starts looking for convergence at the x_start value, then moves towards x_stop value until it finds a convergence point or hits the x_stop value. Continues interating each found region and increasing the accuracy until the accuracy level is satisfied. Args: x_start (Float): x_end (Float): accuracy (Float): minimum level of error we can tolerate on the convergence. The algorithm doesn't solve for an exact value but can get very close by iteration. Set this value very high for more accuracy. Returns: Float: None if no convergence found """ direction = "left" if x_start > x_stop else "right" # iterate until we get the accuracy we need for i in range(100): # set resolution to 1/100th of the search distance resolution = abs(x_start - x_stop) / 100 region = self._find_region_of_convergence(x_start=x_start, x_stop=x_stop, resolution=resolution) if region is None: return None if abs(region[0] - region[1]) < accuracy: # accuracy is at a level we are happy with, return average of # ranges return (region[0] + region[1]) / 2 if direction == "left": x_stop = region[0] x_start = region[1] else: x_start = region[0] x_stop = region[1] def _find_region_of_convergence(self, x_start, x_stop, resolution): """ Finds a region of convergence for the Profile's Surfaces. Starting from the x_start and moving towards x_stop in increments defined by the resolution variable, we try to determine a range - between x and x-1 - where the surfaces cross over one other. Args: x_start (Float) x_stop (Float) resolution (Float) Returns: Tuple of Floats: (x1,x2) - two values that contain two x coordinates that bound the region between where the convergence occurs. None if no convergence found. """ x_val = x_start last_x = x_val if x_stop < x_start: while x_val > x_stop: t = self.top.interpolate(x_val) b = self.bottom.interpolate(x_val) if t.y - b.y < 0: return (x_val, last_x) last_x = x_val x_val -= resolution else: while x_val < x_stop: t = self.top.interpolate(x_val) b = self.bottom.interpolate(x_val) if t.y - b.y < 0: return (last_x, x_val) last_x = x_val x_val += resolution # couldn't find return None
[docs] def __eq__(self, other): if isinstance(other, self.__class__): if not self.top == other.top: return False elif not self.bottom == other.bottom: return False return True raise NotImplementedError
[docs] def __ne__(self, other): if isinstance(other, self.__class__): return not self.__eq__(other) raise NotImplementedError
[docs] def __add__(self, other): if isinstance(other, Coordinate): return Profile.translate(self,other) raise NotImplementedError
[docs] def __sub__(self, other): if isinstance(other, Coordinate): new_coord = Coordinate(-other.x,-other.y) return Profile.translate(self,new_coord) raise NotImplementedError
[docs] def __mul__(self, other): return Profile.scale(self, other)