Source code for pycalculix.geometry

"""This module stores geometry classes, which are used to make parts.
"""
from math import atan2, pi, cos, sin, radians, degrees
from matplotlib.patches import Arc as AArc
from matplotlib.patches import PathPatch
from matplotlib.path import Path
from numpy.linalg import det
from numpy import array
from numpy.linalg import solve as linsolve

from . import base_classes

#accuracy for small numbers in math below
ACC = .00001 # original

LDIVS = {}

[docs]def get_text_hv(angle): """Returns (ha, va) text horizontal and vertical alignment for line label. This makes the line label text inside its area, assuming areas closed CW. Args: angle (float): line normal vector in degrees, between 180 and -180 """ horiz, vert = ('', '') if abs(angle) < 67.5: horiz = 'right' elif 67.5 <= abs(angle) < 112.5: horiz = 'center' elif 112.5 <= abs(angle) <= 180: horiz = 'left' if abs(angle) < 22.5 or abs(angle) > 157.5: vert = 'center' else: if angle > 0: vert = 'top' else: vert = 'bottom' return (horiz, vert)
[docs]class Point(base_classes.Idobj): """Makes a point or vector. Args: x (float): vertical coordinate of point y (float): horizontal coordinate of point z (float): in-page coordinate of point, defaults to zero Attributes: x (float): vertical coordinate of point y (float): horizontal coordinate of point z (float): in-page coordinate of point, defaults to zero nodes (Node): a list of nodes in a mesh at this point. List length is 1. nodes (list): list of nodes under point lines (set): set of lines which use this point arc_center (bool): True if this point is an arc center """ def __init__(self, x, y, z=0): self.x = x self.y = y self.z = z self.nodes = [] self.lines = set() self.arc_center = False self.on_part = True self.esize = None base_classes.Idobj.__init__(self)
[docs] def get_name(self): """Returns item name.""" return 'P'+str(self.id)
def __eq__(self, other): """Checks x and y equality to other point. Args: other (Point): passed point """ if isinstance(other, Point): if self.x == other.x and self.y == other.y: return True return False else: return False def __add__(self, other): """Returns new point = self + other. Args: other (Point): passed point Returns: Point = self + other (new point, does not modify self) """ return Point(self.x+other.x, self.y+other.y, self.z+other.z) def __sub__(self, other): """Returns new point = self - other. Args: other (Point): passed point Returns: Point = self - other (new point, does not modify self) """ return Point(self.x-other.x, self.y-other.y, self.z-other.z) def __hash__(self): """Set the hash of the item to the id number. This allows one to make sets of these items. """ return self.id def __mul__(self, factor): """Returns new point = self*factor. Args: factor (float): factor to multiply the Point by Returns: Point = self*factor (new point, does not modify self) """ return Point(self.x*factor, self.y*factor, self.z*factor) def __truediv__(self, other): """Returns new point = self/other. Args: other (float or Point): item we're dividing by Returns: Point or factor. Does not modify self. """ if isinstance(other, float): return Point(self.x/other, self.y/other, self.z/other) elif isinstance(other, Point): res = [] if other.x != 0: res.append(self.x/other.x) if other.y != 0: res.append(self.y/other.y) if other.z != 0: res.append(self.z/other.z) factor = sum(res)/len(res) return factor
[docs] def set_line(self, line): """Saves the line or arc in the point.""" if line not in self.lines: self.lines.add(line) if self.arc_center == True: if len(self.lines) > 1: for line in self.lines: if isinstance(line, Line): self.arc_center = False break
[docs] def unset_line(self, line): """Removes the line or arc in the point.""" if line not in self.lines: return self.lines.remove(line)
[docs] def length(self): """Returns the length of this point or vector.""" res = (self.x**2 + self.y**2)**0.5 return res
[docs] def label(self, axis): """Labels the point on a Matplotlib axis. Args: axis (Matplotlib Axis): Matplotlib Axis """ axis.annotate(self.get_name(), (self.y, self.x))
[docs] def plot(self, axis, label=True): """Plots the point on the passed matplotlib axis. Args: axis (Matplotlib axis): plate to plot the point pnum (bool): True turns on point labeling """ axis.scatter(self.y, self.x) if label: self.label(axis)
[docs] def make_unit(self): """Modifies self vector to make it a unit vector.""" lenval = self.length() self.x = self.x*1/lenval self.y = self.y*1/lenval
[docs] def ang_rad(self): """Returns angle in radians, assume point is vector from 0,0.""" vert = self.x horiz = self.y rad_angle = atan2(vert, horiz) return rad_angle
[docs] def ang_deg(self): """Returns angle in degrees, assume point is vector from 0,0.""" rad_angle = self.ang_rad() deg = rad_angle * 180.0 / pi return deg
[docs] def axrad(self): """Returns a tuple of (ax, rad) for plotting.""" return (self.y, self.x)
[docs] def radax(self): """Returns a tuple of (rad, ax) for checking if inside.""" return (self.x, self.y)
[docs] def rot_ccw_deg(self, ang): """Rotates the current vector by ccw degrees about 0,0. Returns self. Args: ang (float): angle or rotation in degrees, counter-clockwise (ccw) """ ang = radians(ang) axial = self.y*cos(ang) - self.x*sin(ang) rad = self.y*sin(ang) + self.x*cos(ang) axial = round(axial, 5) rad = round(rad, 5) (self.x, self.y) = (rad, axial) return self
[docs] def ang_bet_rad(self, other): """Returns angle between self and other vector in radians. Assumes vectors start at 0,0 Args: other (Point): passed vector Returns: angle (float): radians between self vector and other vector """ avect = Point(self.y*other.x - self.x*other.y, self.y*other.y + self.x*other.x) ang = avect.ang_rad() return ang
[docs] def ang_bet_deg(self, other): """Returns angle between self and other vector in degrees. Assumes vectors start at 0,0 Args: other (Point): passed vector Returns: angle (float): degrees between self vector and other vector """ avect = Point(self.y*other.x - self.x*other.y, self.y*other.y + self.x*other.x) ang = avect.ang_deg() return ang
def __str__(self): """Returns string listing object type, id number, and coordinates""" val = 'Point %s, (x, y)=(%.3f, %.3f)' % (self.get_name(), self.x, self.y) return val
[docs] def set_esize(self, size): self.esize = size
[docs]class Line(base_classes.Idobj): """Stores a line from points start_pt to end_pt. Args: start_pt (Point): first point end_pt (Point): second point Attributes: points (list of Point) : list of the line's points [start_pt, end_pt] ediv (None or float): number of elements on the line midpt (Point): a mid-point between start_pt and end_pt nodes (list of Node): a list of meshed nodes on the line edge (bool): True if line is a non-shared edge in an area signlines (list): list of SignLine that use this Line """ def __init__(self, start_pt, end_pt): self.points = [start_pt, end_pt] self.midpt = self.mid() self.ediv = None self.nodes = [] self.edge = True self.signlines = [] base_classes.Idobj.__init__(self)
[docs] def reverse(self): """Reverses self.""" self.points.reverse()
@property def allpoints(self): """Returns all line defining points.""" return self.points @property def areas(self): """Returns all areas that signlines use.""" areas = set() for sline in self.signlines: areas.add(sline.parent) return list(areas) def __eq__(self, other): """Returns bool telling if self == other. Returns True if the lines are the same type, and contain the same list of points. """ if isinstance(other, self.__class__): mypts = self.points otherpts = other.points same = True for (mypt, otherpt) in zip(mypts, otherpts): if mypt != otherpt: same = False break return same else: return False def __hash__(self): """Set the hash of the item to the id number. This allows one to make sets of these items. """ return self.id
[docs] def add_signline(self, signline): """Adds a signline to self.signlines.""" if signline not in self.signlines: self.signlines.append(signline)
[docs] def get_name(self): """Returns the line name.""" return 'L'+str(self.id)
[docs] def length(self): """Returns the length of the line.""" return (self.pt(0)-self.pt(1)).length()
[docs] def save_to_points(self): """This stores this line in the line's child points.""" for point in self.allpoints: point.set_line(self)
[docs] def set_ediv(self, ediv): """Sets the number of element divisions on the line when meshing. Args: ediv (int): number of required elements on this line """ self.ediv = ediv
[docs] def set_esize(self, esize): """Sets the size of mesh elements on the line when meshing. Args: esize (float): size of mesh elements on this line """ for i in range(2): self.pt(i).set_esize(esize)
[docs] def signed_copy(self, sign): """Returns a SignLine copy of this Line with the passed sign.""" return SignLine(self, sign)
[docs] def pt(self, ind): """Returns the start or end point of the line. Args: ind (int): index of the point we want, 0=start, 1=end Returns: Point: requested Point """ return self.points[ind]
[docs] def set_pt(self, ind, point): """Update the line's point at ind to the new point. Args: ind (int): index of the point we're updating, 0=start, 1=end point (Point): new point we are assigning to Line """ # remove this line from the old point self.points[ind].lines.remove(self) # update the point self.points[ind] = point self.midpt = self.mid() self.save_to_points() other_point = self.pt(int(not ind)) if point == other_point: # we are making a zero length line other_point.lines.remove(self) if self.id != -1: if len(self.signlines) > 0: area = self.signlines[0].lineloop.parent area.part.fea.lines.remove(self) for sline in self.signlines: sline.lineloop.remove(sline)
[docs] def mid(self): """Calculates and returns a midpoint between start and end points.""" point = (self.pt(0) + self.pt(1))*0.5 return point
[docs] def touches(self, other): """Checks if a line is connected to a passed line. Args: other (Line or Arc): passed other line Returns: True: beginning or end point connect to other line False: lines are not connected """ [start_pt, end_pt] = [self.pt(0), self.pt(1)] if start_pt in other.points or end_pt in other.points: return True else: return False
[docs] def offset(self, dist): """Returns a new line offset from the dirrent line by value dist. Right hand rule is followed assuming x is the origina line. Z points out of page. Y is the direction of line offset. Args: dist (float): distance to offset the new line by Returns: tmpline (Line): new Line which is offset from self by dist """ lvect = self.get_perp_vec() lvect.make_unit() # scale it by the new amount lvect = lvect*dist # add the vector onto the defining points start_pt = self.pt(0) + lvect end_pt = self.pt(1) + lvect tmpline = Line(start_pt, end_pt) return tmpline
[docs] def get_abc(self): """ Returns a list of abc terms for a line definition. ax + by + c = 0 Returns: [a, b, c]: list of the above line terms """ delta = self.pt(1)-self.pt(0) dx_ = delta.x dy_ = delta.y (a__, b__, c__) = (0.0, 0.0, 0.0) if dy_ == 0: # vertical radial line # y = 5 --> 0 = -y + 5 --> 0 = 0x -y + 5 (b__, c__) = (-1.0, self.pt(0).y) elif dx_ == 0: # horizontal axial line # x = 5 --> 0 = -x + 5 --> 0 = -x +0y + 5 (a__, c__) = (-1.0, self.pt(0).x) else: # y = ax + c --> 0 = ax - y + c slope = dy_/dx_ offset = self.pt(0).y - slope*self.pt(0).x (a__, b__, c__) = (slope, -1.0, offset) return [a__, b__, c__]
[docs] def get_perp_vec(self, point=None): """ Returns vector perpendicular to current line. Vector is created by rotating the current line ccw 90 degrees around the start point. Args: point (Point): defaults to None, unused for Line, used for Arc Returns: lvect (Point): vector that is perpendiculat to the current line """ lvect = self.pt(1) - self.pt(0) lvect.rot_ccw_deg(90) return lvect
[docs] def get_tan_vec(self, point): """ Returns vector tangent to the current line. Vector points from the passed point to a unit location further away. Args: point (Point): start point Returns: lvect (Point): vector that is tangent to the current line """ end = self.pt(0) if point == end: end = self.pt(1) lvect = end - point lvect.make_unit() return lvect
[docs] def arc_tang_intersection(self, point, mag): """Returns an intersection point on this line of an arc at center point Args: point (Point): arc center point mag (float): passed radius distance from the arc center to the line Returns: newpt (Point or None): Point: the intersection point None: returns None if no intersection point exists """ vect = self.get_perp_vec() vect.make_unit() pt2 = point + vect*(-2*mag) tmpline = Line(point, pt2) newpt = tmpline.intersects(self) if type(newpt) == type(None): print('Intersection failed!') return newpt
[docs] def coincident(self, point): """Checks to see if point is on the line. Args: point (Point): input point to check Returns: bool: True if point on line or False if not """ # check if point is almost equal to end points dist = point - self.pt(0), point - self.pt(1) dist = [item.length() for item in dist] if min(dist) < ACC: return True # check if point is on the lines [a__, b__, c__] = self.get_abc() remainder = abs(a__*point.x + b__*point.y + c__) # print(' coincident: %s %s' % (self.get_name(), point)) # print(' remainder: %.6f' % remainder) if remainder < ACC: # point is on the line equation, but is it between end pts xvals = [apoint.x for apoint in self.points] yvals = [apoint.y for apoint in self.points] xin = min(xvals)-ACC <= point.x <= max(xvals)+ACC yin = min(yvals)-ACC <= point.y <= max(yvals)+ACC if xin and yin: # we're on the line return True else: # we're off of the line # print(' outside of bounds!') return False else: # point is not on line equation return False
[docs] def intersects(self, other): """Checks if other line intersects this line. Args: other (Line, Arc, Signline, SignArc): other line to check Returns: Point or None: Point: intersection point None: None if lines don't intersect """ # other is cutline when checking if isinstance(other, Line) or isinstance(other, SignLine): terms_1 = self.get_abc() terms_2 = other.get_abc() p_1, p_2 = self.pt(0), self.pt(1) p_3, p_4 = other.pt(0), other.pt(1) dist = [p_1 - p_3, p_1 - p_4, p_2 - p_3, p_2 - p_4] vals = [p_1, p_1, p_2, p_2] dist = [item.length() for item in dist] # check each of self's point to see if it == other's points for (distance, val) in zip(dist, vals): if distance < ACC: return Point(val.x, val.y) if terms_1[:2] == terms_2[:2]: # lines are parallel and will not have an intersection point #print('Line-Line intersection = None: same slope') return None denom = (p_1.x-p_2.x)*(p_3.y-p_4.y) - (p_1.y-p_2.y)*(p_3.x-p_4.x) if abs(denom) < ACC: # lines are nearly identical slope #print('Line-Line intersection = None because of denom') return None else: # intersection calculation a_array = array([[terms_1[0], terms_1[1]], [terms_2[0], terms_2[1]]]) b_array = array([-terms_1[2], -terms_2[2]]) result = linsolve(a_array, b_array) point = Point(result[0], result[1]) # check against existing points on self dist = [point - p_1, point - p_2] vals = [p_1, p_2] dist = [item.length() for item in dist] for (distance, loc) in zip(dist, vals): if distance < ACC: point = Point(loc.x, loc.y) break on_self = self.coincident(point) on_other = other.coincident(point) if on_self and on_other: """ s_name = self.get_name() o_name = other.get_name() print('X Checking intersection of %s and %s' % (s_name, o_name)) print(' %s' % point) print(' (on_self, on_other): (%s, %s)' % (on_self, on_other)) """ return point else: """ print('Line-Line intersection = None point not on lines') print(' %s' % point) print(' on_self: %s' % on_self) print(' on_other: %s' % on_other) """ return None elif isinstance(other, Arc) or isinstance(other, SignArc): # arc line intersection return other.intersects(self)
def __str__(self): """Returns string listing object type, name, and points""" start = self.pt(0) end = self.pt(1) val = 'Line %s, start: %s end: %s' % (self.get_name(), start, end) return val
[docs]class SignLine(Line, base_classes.Idobj): """Makes a signed line. Attributes: line (Line): parent line sign (int): 1 = positive, or -1 = negative lineloop (None or LineLoop): parent LineLoop, default is None faces (list): list of element faces n1 (list): list of first order nodes on line nodes (list): nodes on the line """ def __init__(self, parent_line, sign): self.line = parent_line self.sign = sign self.lineloop = None self.faces = [] self.n1 = [] base_classes.Idobj.__init__(self)
[docs] def pt(self, index): """Returns the point at the start or end of the line. Args: index (int): 0 = start point, 1 = end point """ if self.sign == -1: return self.line.points[not index] else: return self.line.points[index]
[docs] def set_pt(self, index, point): """Sets the start or end point of the line to a new point. Args: index (int): 0 for start point, 1 for end point point (Point): the new point we are updating to """ self.line.set_pt(index, point)
[docs] def set_ediv(self, ediv): """Applies the element divisions onto the parent line.""" self.line.set_ediv(ediv)
[docs] def set_esize(self, esize): """Applies the element size onto the parent line.""" self.line.set_esize(esize)
[docs] def set_lineloop(self, lineloop): """Sets the parent LineLoop""" # this is needed to cascade set ediv up to FEA model and down onto # other lines with this ediv self.lineloop = lineloop
[docs] def get_name(self): """Returns the SignLine name.""" if self.sign == -1: return '-L'+str(self.line.id) else: return 'L'+str(self.line.id)
[docs] def label(self, ax): """Labels the line on the matplotlib ax axis. Args: ax (Matplotlib axis): Matplotlib axis """ # find angle of perpendiclar line so we can set text alignment lvect = self.get_perp_vec() ang = lvect.ang_deg() horiz, vert = get_text_hv(ang) axial = self.midpt.y radial = self.midpt.x ax.text(axial, radial, self.get_name(), ha=horiz, va=vert)
[docs] def plot(self, ax, label=True): """Draws and labels the line onto the passed Matplotlib axis. Args: ax (Matlotlib axis): Matplotlib axis label (bool): if True label the line """ lax = [pt.y for pt in self.points] lrad = [pt.x for pt in self.points] # draw line ax.plot(lax, lrad) if label: self.label(ax)
[docs] def signed_copy(self, sign): """Returns a SignLine copy of this line.""" realsign = self.sign*sign return SignLine(self.line, realsign)
@property def nodes(self): """Gets the line nodes.""" return self.line.nodes @nodes.setter def nodes(self, nlist): """Sets the line nodes.""" self.line.nodes = nlist @property def midpt(self): """Returns the mid point.""" return self.line.midpt @property def edge(self): """Returns a bool saying if this is an edge line.""" return self.line.edge @edge.setter def edge(self, isedge): """Sets the line nodes.""" self.line.edge = isedge @property def points(self): """Returns a list of the line's points [pstart, pend]""" pstart = self.pt(0) pend = self.pt(1) return [pstart, pend]
[docs] def reverse(self): """Reverses self.line""" self.line.reverse()
[docs]class Arc(base_classes.Idobj): """Makes an arc from points start_pt to end_pt about center actr. Arcs should be 90 degrees maximum. Arguments: start_pt (Point): first point end_pt (Point): second point actr (Point): arc center Attributes: points (list of Point) : list of the line's points [start_pt, end_pt] allpoints (list of Point): [start_pt, end_pt, actr] actr (Point): arc center radius (float): radius of the arc concavity (str): - 'concave': concave arc - 'convex': convex arc midpt (Point): a mid-point between p1 and p2 ediv (None or int): number of elements on arc nodes (list of Node): a list of meshed nodes on the line edge (bool): True if arc is a non-shared edge in an area signlines (list): list of SignArc that use this Arc """ def __init__(self, start_pt, end_pt, actr): self.points = [start_pt, end_pt] self.actr = actr self.actr.arc_center = True self.radius = (start_pt - actr).length() self.concavity = self.get_concavity() self.midpt = self.mid() self.ediv = None self.nodes = [] self.edge = True self.signlines = [] base_classes.Idobj.__init__(self)
[docs] def reverse(self): """Reverses self.""" self.points.reverse() self.concavity = self.get_concavity()
@property def allpoints(self): """Returns all points including arc center""" return self.points + [self.actr] @property def areas(self): """Returns all areas that signlines use.""" areas = set() for sline in self.signlines: areas.add(sline.parent) return list(areas) def __eq__(self, other): """Returns bool telling if self == other Returns True if the lines are the same type, and contain the same list of points. """ if isinstance(other, self.__class__): # check to see if the end points are the same mypts = self.points otherpts = other.points same = True for (mypt, otherpt) in zip(mypts, otherpts): if mypt != otherpt: same = False break # flip to false if arc centers are different myactr = self.actr otheractr = other.actr if myactr != otheractr: same = False return same else: return False def __hash__(self): """Set the hash of the item to the id number. This allows one to make sets of these items. """ return self.id
[docs] def add_signline(self, signline): """Adds a signline to self.signlines.""" if signline not in self.signlines: self.signlines.append(signline)
[docs] def get_name(self): """Returns the arc name.""" return 'L'+str(self.id)
[docs] def length(self): """Returns the length of the arc.""" a = self.pt(0)-self.actr b = self.pt(1)-self.actr ang = abs(a.ang_bet_rad(b)) res = self.radius*ang return res
[docs] def save_to_points(self): """This stores this line in the line's child points.""" for point in self.allpoints: point.set_line(self)
[docs] def set_ediv(self, ediv): """Sets the number of element divisions on the arc when meshing. Args: ediv (int): number of required elements on this arc """ self.ediv = ediv
[docs] def set_esize(self, esize): """Sets the size of mesh elements on the arc when meshing. Args: esize (float): size of mesh elements on this arc """ for i in range(2): self.pt(i).set_esize(esize)
[docs] def signed_copy(self, sign): """Returns a SignArc instance of this Arc with the passed sign.""" return SignArc(self, sign)
[docs] def pt(self, ind): """Returns the start or end point of the arc. Args: ind (int): index of the point we want, 0=start, 1=end Returns: Point: requested Point """ return self.points[ind]
[docs] def set_pt(self, ind, point): """Update the arc's point at ind to the new point. Args: ind (int): index of the point we're updating, 0=start, 1=end point (Point): new point we are assigning to Line """ # remove this line from the old point self.points[ind].lines.remove(self) # set the new point self.points[ind] = point self.midpt = self.mid() self.save_to_points() other_point = self.pt(int(not ind)) if point == other_point: other_point.lines.remove(self) for sline in self.signlines: sline.lineloop.remove(sline) if self.id != -1: if len(self.signlines) > 0: area = self.signlines[0].lineloop.parent area.part.fea.lines.remove(self)
[docs] def mid(self): """Caclulates and returns the midpoint of the arc.""" pt = self.get_pt_at(0.5) return pt
[docs] def touches(self, other): """Checks if this arc is connected to a passed line. Args: other (Line or Arc): passed other line Returns: True: beginning or end point connect to other line False: lines are not connected """ [p1, p2] = [self.pt(0), self.pt(1)] if p1 in other.points or p2 in other.points: return True else: return False
[docs] @staticmethod def sgn(val): """Returns sign of the passed value. Helper function for self.interects code. Args: val (float): passed value Returns: 1 or -1: sign of the passed value, assumes sign of zero = 1 """ if val < 0: return -1 else: return 1
[docs] def get_ang(self): """ Returns angle from beginning to end of arc from arc ctr in degrees Notes: Answer is between 180 and -180 Positive is concave Negative is convex """ a = self.pt(0)-self.actr b = self.pt(1)-self.actr ang = a.ang_bet_deg(b) return ang
[docs] def get_ang_rad(self): """ Returns angle from beginning to end of arc from arc ctr in rad Notes: Answer is between pi and -pi Positive is concave Negative is convex """ a = self.pt(0)-self.actr b = self.pt(1)-self.actr ang = a.ang_bet_rad(b) return ang
[docs] def get_concavity(self, clockwise=True): """Returns concavity of this arc, 'concave' or 'convex'.""" ang = self.get_ang() res = '' if clockwise: res = 'concave' if ang < 0: res = 'convex' else: res = 'convex' if ang < 0: res = 'concave' return res
[docs] def get_pt_at(self, nondim): """Returns point on arc between start and end point at nondim dist. Args: nondim (float): distance between start and end point, 0=start, 1=end Returns: res (Point): requested point on arc """ a = self.pt(0)-self.actr # neg is convex, pos is concave ang = self.get_ang() ang_delta = nondim*ang a.rot_ccw_deg(ang_delta) res = self.actr + a return res
[docs] def get_perp_vec(self, pt): """ Returns vector perpendicular to current arc. Right hand rule is used: X+ is the direction of the arc from start to end. Z+ is out of the page Y+ is the direction of the perpendicular vector Args: pt (Point): point on arc where we want the perpendicular vector Returns: resv (Point): perpendicular vector """ resv = None if self.concavity == 'convex': resv = pt - self.actr elif self.concavity == 'concave': resv = self.actr - pt return resv
[docs] def get_tan_vec(self, point): """ Returns vector tangent to the current arc. Vector points from the passed point to a unit location further away. Args: point (Point): start point Returns: resv (Point): vector that is tangent to the current line """ if self.concavity == 'concave': resv = self.actr - point elif self.concavity == 'convex': resv = point - self.actr if point == self.pt(0): resv.rot_ccw_deg(-90) else: resv.rot_ccw_deg(90) resv.make_unit() return resv
[docs] def get_verts_codes(self, plot=True): """Returns a list of [verts, codes] vertices for plotting.""" # http://www.spaceroots.org/documents/ellipse/node19.html # http://www.tinaja.com/glib/bezarc1.pdf # http://stackoverflow.com/questions/734076/geometrical-arc-to-bezier-curve # find the angle of rotation to the middle of the arc ang_offset = self.get_pt_at(0.5) - self.actr ang_offset = ang_offset.ang_deg() # store the half angle as theta theta = radians(abs(self.get_ang()))*0.5 ax1 = (4 - cos(theta))/3 rad1 = ((1 - cos(theta))*(cos(theta) - 3))/(3*sin(theta)) pt1 = Point(rad1, ax1)*self.radius pt2 = Point(-rad1, ax1)*self.radius # fix the angles pt1.rot_ccw_deg(ang_offset) pt2.rot_ccw_deg(ang_offset) # fix the location pt1 += self.actr pt2 += self.actr # need to change the order if the arc is concave or convex verts = [pt1, pt2, self.pt(1)] if self.concavity == 'convex': verts = [pt2, pt1, self.pt(1)] if plot: verts = [point.axrad() for point in verts] else: verts = [point.radax() for point in verts] codes = [Path.CURVE4, Path.CURVE4, Path.CURVE4] return [verts, codes]
[docs] def coincident(self, pt): """Checks to see if pt is on the arc. Args: pt (Point): input point to check Returns: bool: True if pt on arc or False if not """ # (x - xc)^2 + (y - yc)^2 - r^2 = 0 dist = pt - self.pt(0), pt - self.pt(1) dist = [item.length() for item in dist] if min(dist) < ACC: # True if point almost equal to end points return True remainder = abs((pt.x - self.actr.x)**2 + (pt.y - self.actr.y)**2 - self.radius**2) if remainder < ACC: # point is on the circle, but is it in the arc? # check if the angle traversed to the point is within the ang # traversed by the arc a1 = self.pt(0)-self.actr a2 = pt-self.actr ang_pt = a1.ang_bet_deg(a2) # this is the angle traversed byt the arc ang = self.get_ang() minval = min(0, ang) maxval = max(0, ang) if minval <= ang_pt <= maxval: # point is on our arc return True else: # point on circle but outside our arc return False else: # point not on arc return False
[docs] def intersects(self, other): """Checks if other line intersects this arc. Args: other (Line or Arc): other line to check Returns: Point or None: Point: intersection point None: None if lines don't intersect """ if isinstance(other, Line) or isinstance(other, SignLine): #print('|C Checking arc-line intersection') #print(' %s' % self) # arc-line intersection # reset the math to be about the circle centroid # http://mathworld.wolfram.com/Circle-LineIntersection.html pt1 = other.pt(0) - self.actr pt2 = other.pt(1) - self.actr # for us x and y are reversed because of y axial (x1, y1, x2, y2) = (pt1.y, pt1.x, pt2.y, pt2.x) dx = x2 - x1 dy = y2 - y1 rad = self.radius dr = (dx**2 + dy**2)**(0.5) D = x1*y2 - x2*y1 dist = [self.pt(0) - other.pt(0), self.pt(0) - other.pt(1), self.pt(1) - other.pt(0), self.pt(1) - other.pt(1)] vals = [self.pt(0), self.pt(0), self.pt(1), self.pt(1)] dist = [item.length() for item in dist] for (distance, val) in zip(dist, vals): if distance < ACC: return Point(val.x, val.y) discr = 1.0*(rad**2)*(dr**2) - D**2 if discr < 0: # no intersection #print('none returned because of discriminant') return None else: # tangent or intersection rsq = rad**2 drsq = dr**2 x1 = (D*dy + self.sgn(dy)*dx*(rsq*drsq-D**2)**(0.5))/drsq y1 = (-D*dx + abs(dy)*(rsq*(dr**2)-D**2)**(0.5))/drsq x2 = (D*dy - self.sgn(dy)*dx*(rsq*drsq-D**2)**(0.5))/drsq y2 = (-D*dx - abs(dy)*(rsq*drsq-D**2)**(0.5))/drsq # convert result points back to global coordinates pt1 = Point(y1, x1) + self.actr pt2 = Point(y2, x2) + self.actr res = [] # check that the resultant points are on line and arcs if self.coincident(pt1) and other.coincident(pt1): for apoint in self.points: dist = pt1 - apoint dist = dist.length() if dist < ACC: pt1 = Point(apoint.x, apoint.y) break res.append(pt1) if pt1 != pt2 and self.coincident(pt2) and other.coincident(pt2): for apoint in self.points: dist = pt2 - apoint dist = dist.length() if dist < ACC: pt2 = Point(apoint.x, apoint.y) break res.append(pt2) if len(res) == 1: return res[0] elif len(res) > 1: return res elif len(res) == 0: #print('none returned because no point from math') return None elif isinstance(other, Arc): # arc-arc intersection # I have not yet written the code here return None
def __str__(self): """Returns string listing object type, name, and points""" name = self.get_name() start, end = self.pt(0), self.pt(1) actr = self.actr val = 'Arc %s, start: %s end: %s center: %s' % (name, start, end, actr) return val
[docs]class SignArc(Arc, base_classes.Idobj): """Makes a signed arc. Attributes: line (Arc): parent arc sign (int): 1 = positive, or -1 = negative lineloop (None or LineLoop): parent LineLoop, default is None nodes (list): nodes on the arc faces (list): list of element faces n1 (list): list of first order nodes on line """ def __init__(self, parent_line, sign): self.line = parent_line self.sign = sign self.lineloop = None self.faces = [] self.n1 = [] base_classes.Idobj.__init__(self)
[docs] def pt(self, ind): """Returns the point at the start or end of the line.""" if self.sign == -1: return self.line.points[not ind] else: return self.line.points[ind]
[docs] def set_pt(self, ind, point): """Sets the passed point to at the index location.""" self.line.set_pt(ind, point)
[docs] def set_ediv(self, ediv): """Apply the element divisions onto the parent line.""" self.line.set_ediv(ediv)
[docs] def set_esize(self, esize): """Applies the element size onto the parent line.""" self.line.set_esize(esize)
[docs] def set_lineloop(self, lineloop): """Sets the parent LineLoop""" # this is needed to cascade set ediv up to FEA model and down onto # other lines with this ediv self.lineloop = lineloop
[docs] def get_name(self): """Returns the line name.""" if self.sign == -1: return '-L'+str(self.line.id) else: return 'L'+str(self.line.id)
[docs] def label(self, ax): """Labels the arc on the matplotlib ax axis. Args: ax (Matplotlib axis): Matplotlib axis """ lvect = self.get_perp_vec(self.midpt) ang = lvect.ang_deg() horiz, vert = get_text_hv(ang) (axial, radial) = (self.midpt.y, self.midpt.x) ax.text(axial, radial, self.get_name(), ha=horiz, va=vert)
[docs] def plot(self, ax, label=True): """Draws and labels the arc onto the passed Matplotlib axis. Args: ax: Matplotlib axis label (bool): if True, label the arc """ ctr = self.actr vect1 = None sign = 1 if self.concavity == 'concave': vect1 = self.pt(0)-ctr else: sign = -1 vect1 = self.pt(1)-ctr rad = self.radius ang1 = (vect1.ang_deg()) ang2 = ang1 + sign*self.get_ang() # matplotlib assumes ccw arc drawing, calculix assumes cw drawing a = AArc(xy=[ctr.y, ctr.x], width=2*rad, height=2*rad, angle=0, theta1=ang1, theta2=ang2) ax.add_artist(a) if label: self.label(ax)
[docs] def signed_copy(self, sign): """Returns a SignArc copy of this arc.""" realsign = self.sign*sign return SignArc(self.line, realsign)
@property def nodes(self): """Gets the line nodes.""" return self.line.nodes @nodes.setter def nodes(self, nlist): """Sets the line nodes.""" self.line.nodes = nlist @property def actr(self): """Return the arc center.""" return self.line.actr @property def radius(self): """Return the arc radius.""" return self.line.radius @property def midpt(self): """Return the mid point.""" return self.line.midpt @property def edge(self): """Returns a bool saying if this is an edge line.""" return self.line.edge @edge.setter def edge(self, isedge): """Sets the line nodes.""" self.line.edge = isedge @property def points(self): """Return a list of the arc's points [pstart, pend]""" pstart = self.pt(0) pend = self.pt(1) return [pstart, pend] @property def concavity(self): """Return the arc's concavity 'concave' or 'convex'""" if self.sign == 1: return self.line.concavity else: if self.line.concavity == 'concave': return 'convex' else: return 'concave'
[docs] def reverse(self): """Reverses self.line""" self.line.reverse()
[docs]class LineLoop(list): """Makes a LineLoop, which stores multiple Line/Arc or SignLine/SignArc. Args: items (list): Line or Arc or SignLine or SignArc hole_bool (bool): True if hole, False otherwise parent (Area or None): parent Area, defaults to None Attributes: closed (bool): True if closed area (float): the loop's area, can be pos or neg. Pos = cw, neg = ccw hole (bool): True if loop is a hole ccw (bool): True if loop is counter-clockwise center (Point): a point at the loop centroid parent (Area or None): the loop's parent Area """ def __init__(self, items=[], hole_bool=False, parent=None): super().__init__(items) self.id = -1 for item in self: if isinstance(item, SignLine) or isinstance(item, SignArc): item.lineloop = self if item not in item.line.signlines: item.line.add_signline(item) self.hole = hole_bool self.parent = parent
[docs] def set_id(self, id): """Sets the id number""" self.id = id
def __hash__(self): """Sets the hash of the item to the id number. This allows one to make sets of these items. """ return self.id @property def closed(self): """Returns True if Closed, False if not.""" if len(self) < 2: return False else: for ind, curr_line in enumerate(self): lower_ind = ind-1 prev_line = self[lower_ind] if curr_line.pt(0) != prev_line.pt(1): return False return True @property def center(self): """Returns None or a Point at the line loop's center.""" if self.closed == False: return None else: cx_sum = 0 cy_sum = 0 a_sum = 0 arcs = [] for item in self: if isinstance(item, Line) or isinstance(item, SignLine): start, end = item.pt(0), item.pt(1) det_val = det([[start.x, end.x], [start.y, end.y]]) cx_sum += (start.x + end.x)*det_val cy_sum += (start.y + end.y)*det_val a_sum += det_val elif isinstance(item, Arc) or isinstance(item, SignArc): # concave: draw to actr, adder is neg # convex: draw to actr, adder is pos tmp_list = [[item.pt(0), item.actr], [item.actr, item.pt(1)]] for [start, end] in tmp_list: det_val = det([[start.x, end.x], [start.y, end.y]]) cx_sum += (start.x + end.x)*det_val cy_sum += (start.y + end.y)*det_val a_sum += det_val arcs.append(item) area_val = a_sum*0.5 val_list = [] if area_val != 0: cx_val = (1/(6*area_val))*cx_sum cy_val = (1/(6*area_val))*cy_sum val_list.append([area_val, cx_val, cy_val]) for arc in arcs: ang = arc.get_ang_rad() area_val = -0.5*ang*(arc.radius**2) half_ang = abs(ang)*0.5 vect = arc.midpt - arc.actr vect.make_unit() mag = 2*arc.radius*sin(half_ang)/(3*half_ang) vect = vect*mag center = arc.actr + vect val_list.append([area_val, center.x, center.y]) a_sum = sum([aval for [aval, cx, cy] in val_list]) cxa_sum = sum([cx*aval for [aval, cx, cy] in val_list]) cya_sum = sum([cy*aval for [aval, cx, cy] in val_list]) cx_val = cxa_sum/a_sum cy_val = cya_sum/a_sum return Point(cx_val, cy_val) @property def ccw(self): """Returns bool telling if the area is closed and clockwise.""" if self.closed == False: return False else: area_val = self.area if area_val < 0: return True else: return False @property def area(self): """Returns the loop's signed area. Returns 0 if not closed. Pos = cw, neg = ccw Formula from: http://mathworld.wolfram.com/PolygonArea.html """ res = 0 adder = 0 arcs = [] if self.closed == False: return res else: for item in self: if isinstance(item, Line) or isinstance(item, SignLine): start, end = item.pt(0), item.pt(1) det_matrix = [[start.x, end.x], [start.y, end.y]] res += det(det_matrix) elif isinstance(item, Arc) or isinstance(item, SignArc): # concave: draw to actr, adder is neg # convex: draw to actr, adder is pos start, end = item.pt(0), item.actr det_matrix = [[start.x, end.x], [start.y, end.y]] res += det(det_matrix) start, end = item.actr, item.pt(1) det_matrix = [[start.x, end.x], [start.y, end.y]] res += det(det_matrix) arcs.append(item) res = 0.5*res # need to loop through arcs here, now that we know if cw or ccw for arc in arcs: ang = arc.get_ang_rad() tmp_adder = -0.5*ang*(arc.radius**2) adder += tmp_adder res = res + adder return res
[docs] def reverse(self): """Reverse the loop direction cw<-->ccw This modifies the order of lines, and flips each line. """ for line in self: line.reverse() super().reverse()
[docs] def set_parent(self, parent): """Sets the line loop's parent part.""" self.parent = parent
[docs] def get_patch(self): """Returns a patch of the LineLoop, or None of not closed.""" if self.closed: codes, verts = [], [] # start poly codes += [Path.MOVETO] verts += [self[0].pt(0).radax()] for line in self: if isinstance(line, SignLine) or isinstance(line, Line): verts.append(line.pt(1).radax()) codes.append(Path.LINETO) elif isinstance(line, SignArc) or isinstance(line, Arc): lverts, lcodes = line.get_verts_codes(plot=False) verts += lverts codes += lcodes if line == self[-1]: # close poly if at last line verts.append((0, 0)) codes.append(Path.CLOSEPOLY) path = Path(verts, codes) patch = PathPatch(path, facecolor='yellow', linewidth=0.0) return patch return None
[docs] def contains_point(self, point): """Returns bool telling if pt is inside the LineLoop. Args: pt (Point): point we are checking Returns: contains (bool): True if in this area False if not """ contains = False if self.closed: patch = self.get_patch() contains = patch.contains_point(point.radax()) return contains
[docs] def inside(self, other): """Checks to see if self is inside other. Only checks self's points. """ if self.closed == False: return False if isinstance(other, LineLoop): points = set() for sline in self: points.update(sline.points) patch = other.get_patch() for point in points: contains = patch.contains_point(point.radax()) if contains == False: return False return True else: return False
[docs] def append(self, item): """Adds an item to this LineLoop Args: item (SingArc or SignLine): item to add to the list """ if isinstance(item, SignLine) or isinstance(item, SignArc): item.set_lineloop(self) super().append(item) return item
[docs] def remove(self, item): """Removes an SignLine or SignArc item from this LineLoop Args: item (SignArc or SignLine): item to remove """ if isinstance(item, SignLine) or isinstance(item, SignArc): item.set_lineloop(None) if item not in item.line.signlines: print('''===THIS SLINE NOT IN SLINE.LINE.SIGNLINES!===''') print('Removing sline %s from loop in area %s' % (item.get_name(), self.parent.get_name())) print('SLINE.LINE %s' % item.line) print('SLINE.LINE.SIGNLINES %r' % item.line.signlines) super().remove(item)
[docs] def insert(self, index, item): """Inserts a SignLine or SignArc item into this LineLoop Args: item (SingArc or SignLine): item to insert """ if isinstance(item, SignLine) or isinstance(item, SignArc): item.set_lineloop(self) super().insert(index, item)
def __str__(self): """Prints the LineLoop definition.""" res = 'Number of items %i\n' % len(self) for line in self: res += '%s\n' % line return res
[docs]class Area(base_classes.Idobj): """ Makes an area. Area is closed in a clockwise direction. Args: part (Part): parent Part line_list (list): list of Lines and Arcs that close the area Attributes: part (Part): parent part closed (boolean): True if closed, False if not exlines (LineLoop): LineLoop of signlines that define the area exterior signlines (list): list of signed lines or arcs that define the area lines (list): a list of all the lines that make the area, includes hole lines points (list): a list of all points making the area, excludes arc centers allpoints (list): a list of all points, includes arc centers holepoints (list): a list of hole points, excludes arc centers matl (Matl): the material fo the area etype (str): element type of area. Options are: 'plstress': plane stress 'plstrain': plane strain 'axisym': axisymmetric area (double): in-plane part area (ballpark number, arcs not calc right) holes (list): list of LineLoop of signlines center (Point or None): the area centroid point. None before closed. nodes (list): child mesh nodes that are in the area elements (list): child elements that are in the area """ def __init__(self, part, line_list=[]): base_classes.Idobj.__init__(self) self.part = part # parent part reference self.closed = False self.matl = None self.etype = None self.area = 0.0 self.holes = [] self.center = None self.nodes = [] self.elements = [] exloop = LineLoop(line_list, False, self) self.part.fea.register(exloop) self.exlines = exloop if self.exlines.closed == True: self.close() def __hash__(self): """Returns the item's id as its hash.""" return self.id @property def lines(self): """Returns a list of all Line and Arc under this area.""" mylist = [] loops = [self.exlines] + self.holes for loop in loops: line_list = [line.line for line in loop] mylist += line_list return mylist @property def signlines(self): """Returns a list of all SignLine and SignArc under this area.""" mylist = [] loops = [self.exlines] + self.holes for loop in loops: mylist += loop return mylist @property def points(self): """Returns a list of all points under this area; excludes arc centers.""" pts = set() for line in self.lines: pts.update(line.points) return list(pts) @property def allpoints(self): """Returns a list of all points under this area; includes arc centers.""" pts = set() for line in self.lines: pts.update(line.allpoints) return list(pts) @property def holepoints(self): """Returns a list of all points in this area's holes.""" pts = set() for hole in self.holes: for sline in hole: pts.update(sline.points) return list(pts) def __str__(self): """Returns a string listing the area's name.""" return self.get_name()
[docs] def get_name(self): """Returns the area name.""" return 'A'+str(self.id)
[docs] def close(self): """Closes the area.""" # sets closed=True # calculates the area center self.closed = True if self.exlines.ccw == True: self.exlines.reverse() self.area, self.center = self.calc_area_center()
[docs] def line_from_startpt(self, pt): """Returns the signline in the area that start at the passed pt. Args: pt (Point): the start point of the line one wants Returns: match (None or SignLine or SignArc): | Returns None if no line is found, otherwise the correct line or | arc which starts with pt will be | returned. """ # returns the line that starts on the given point matches = [] area_slines = self.signlines for sline in area_slines: if sline.pt(0) == pt: matches.append(sline) if len(matches) == 1: return matches[0] elif len(matches) > 1: # we have a saw cut in the area and we're cutting on one of its pts print('>1 slines starting with %s' % pt.get_name()) for match in matches: slines = set(match.line.signlines) if len(slines) == 2 and slines.issubset(area_slines): # this is the saw cut one pass else: # we want the non saw cut one return match return None
[docs] def calc_area_center(self): """Calculates and returns the area and centroid Point. Returns: list: [area, Point] """ loops = [self.exlines] + self.holes val_list = [] for loop in loops: if loop.closed == True: aval, cval = loop.area, loop.center val_list.append([aval, cval]) a_sum = sum([aval[0] for aval in val_list]) cxa_sum = sum([center.x*aval for [aval, center] in val_list]) cya_sum = sum([center.y*aval for [aval, center] in val_list]) cx_val = cxa_sum/a_sum cy_val = cya_sum/a_sum center = Point(cx_val, cy_val) return [a_sum, center]
[docs] def get_maxlength(self): """Returns max distance between points in an area.""" pts = self.points maxlen = 0.0 # loop through points checking dist to next point for ind, p1 in enumerate(pts[:-1]): for p2 in pts[ind:]: vect = p1 - p2 dist = vect.length() if dist > maxlen: maxlen = dist return maxlen
[docs] def rem_sline(self, sline): """Removes a sline from this area.""" loops = [self.exlines] + self.holes for loop in loops: if sline in loop: print('Line %s removed from %s' % (sline.get_name(), self.get_name())) loop.remove(sline) if sline.id != -1: line = sline.line self.part.fea.signlines.remove(sline) if len(line.signlines) == 0: self.part.fea.lines.remove(line)
[docs] def add_sline(self, signline): """Adds signline to the area definition. SignLine is added to the end of the lines list. Area is closed if needed. """ # this adds a line to the area self.exlines.append(signline) if self.exlines.closed: self.close()
[docs] def add_hole_sline(self, signline): """Adds signline to the area hole definition. Line is added to the end of the lines list. Area is closed if needed. Returns: closed (bool): boolean telling if the hole was closed """ make_new = (len(self.holes) == 0) if make_new == False: if self.holes[-1].closed == True: make_new = True if make_new: hole = LineLoop([], hole_bool=True, parent=self) hole = self.part.fea.lineloops.append(hole) self.holes.append(hole) self.holes[-1].append(signline) # reverse the hole if it's closed in the cw direction if self.holes[-1].closed == True: if self.holes[-1].ccw == False: self.holes[-1].reverse() self.area, self.center = self.calc_area_center() return self.holes[-1].closed
[docs] def get_patch(self): """Returns a patch of the area.""" if self.closed: loops = [self.exlines] + self.holes codes, verts = [], [] for loop in loops: # start poly codes += [Path.MOVETO] verts += [loop[0].pt(0).axrad()] for sline in loop: if isinstance(sline, SignLine): verts.append(sline.pt(1).axrad()) codes.append(Path.LINETO) elif isinstance(sline, SignArc): lverts, lcodes = sline.get_verts_codes() verts += lverts codes += lcodes if sline == loop[-1]: # close poly if at last line verts.append((0, 0)) codes.append(Path.CLOSEPOLY) path = Path(verts, codes, _interpolation_steps=2) patch = PathPatch(path, linewidth=0.0) return patch
[docs] def label(self, axis): """Labels the area on a Matplotlib axis Args: axis (Matplotlib Axis): Matplotlib Axis """ axis.text(self.center.y, self.center.x, self.get_name(), ha='center', va='center')
[docs] def plot(self, axis, label=True, color='yellow'): """Plots the area on a Matplotlib axis. Args: axis (Matplotlib axis): Matplotlib axis label (bool): if True, label area """ # http://stackoverflow.com/questions/8919719/how-to-plot-a-complex-polygon if self.closed: # plot area patch patch = self.get_patch() patch.set_color(color) axis.add_patch(patch) # label ares if label: self.label(axis)
[docs] def contains_point(self, point): """Returns bool telling if pt is inside the area. Args: pt (Point): point we are checking Returns: contains (bool): True if in this area False if not """ patch = self.get_patch() contains = patch.contains_point(point.axrad()) return contains
[docs] def line_insert(self, lgiven, lnew, after=True): """Inserts line lnew before or after the given line. Args: lgiven (SignLine or SignArc): the line we are inserting relative to lnew (SignLine or SignArc): the new line we will insert after (bool): if True insert lnew after lgiven, false insert before Returns: bool: True if succeeded, False if failed """ if lgiven in self.signlines: print('Inserting line %s into area %s next to %s' % (lnew.get_name(), self.get_name(), lgiven.get_name())) lineloop = lgiven.lineloop type = 'exterior lines' if lineloop.hole: type = 'hole lines' ind = lineloop.index(lgiven) if after: ind += 1 print(' Inserting line into %s outer lines' % self.get_name()) lineloop.insert(ind, lnew) return True else: print('ERROR: Passed line was not in %s!' % self.get_name()) print('You must pass a line in this area!') return False
[docs] def set_child_ccxtypes(self): """Assigns the child element ccx types.""" if self.etype != None and self.elements != []: # if etype is set and elements have been assigned for element in self.elements: element.set_ccxtype(self.etype)
[docs] def set_etype(self, etype): """Sets the area etype. Sets the ccxtype on elements if they have been assigned to this area. Args: etype (str): element type 'plstress' plane stress 'plstrain': plane strain 'axisym': axisymmetric """ self.etype = etype self.set_child_ccxtypes()
[docs] def set_esize(self, esize): """Sets the size of mesh elements on the area when meshing. Args: esize (float): size of mesh elements on this area """ #size if set if no mesh size on points is peresent for p in self.points: p.set_esize(esize)
[docs] def update(self, line_list): """Updates the area's external lines to the passed list. If the area is cosed, self.close() will be called. Args: line_list (list): list of SignLine and SignArc items """ to_rem = list(self.exlines) for sline in to_rem: self.exlines.remove(sline) for sline in line_list: self.exlines.append(sline) if self.exlines.closed == True: self.close()