# -*- coding: utf-8 -*- # pylint: disable=E1101 # E1101 = Module X has no Y member """ The core of ``wafer_map``. """ # --------------------------------------------------------------------------- ### Imports # --------------------------------------------------------------------------- # Standard Library from __future__ import absolute_import, division, print_function, unicode_literals import math # Third-Party import numpy as np import wx from wx.lib.floatcanvas import FloatCanvas import wx.lib.colourselect as csel # Package / Application from . import wm_legend from . import wm_utils from . import wm_constants as wm_const # Module-level TODO list. # TODO: make variables "private" (prepend underscore) # TODO: Add function to update wafer map with new die size and the like. class WaferMapPanel(wx.Panel): """ The Canvas that the wafer map resides on. Parameters ---------- parent : :class:`wx.Panel` The panel that this panel belongs to, if any. xyd : list of 3-tuples The data to plot. wafer_info : :class:`wx_info.WaferInfo` The wafer information. data_type : :class:`wm_constants.DataType` or str, optional The type of data to plot. Must be one of `continuous` or `discrete`. Defaults to `CoordType.CONTINUOUS`. coord_type : :class:`wm_constants.CoordType`, optional The coordinate type to use. Defaults to ``CoordType.ABSOLUTE``. Not yet implemented. high_color : :class:`wx.Colour`, optional The color to display if a value is above the plot range. Defaults to `wm_constants.wm_HIGH_COLOR`. low_color : :class:`wx.Colour`, optional The color to display if a value is below the plot range. Defaults to `wm_constants.wm_LOW_COLOR`. plot_range : tuple, optional The plot range to display. If ``None``, then auto-ranges. Defaults to auto-ranging. plot_die_centers : bool, optional If ``True``, display small red circles denoting the die centers. Defaults to ``False``. discrete_legend_values : list, optional A list of strings for die bins. Every data value in ``xyd`` must be in this list. This will define the legend order. Only used when ``data_type`` is ``discrete``. show_die_gridlines : bool, optional If ``True``, displayes gridlines along the die edges. Defaults to ``True``. discrete_legend_values : list, optional A list of strings for die bins. Every data value in ``xyd`` must be in this list. This will define the legend order. Only used when ``data_type`` is ``discrete``. """ def __init__(self, parent, xyd, wafer_info, data_type=wm_const.DataType.CONTINUOUS, coord_type=wm_const.CoordType.ABSOLUTE, high_color=wm_const.wm_HIGH_COLOR, low_color=wm_const.wm_LOW_COLOR, plot_range=None, plot_die_centers=False, discrete_legend_values=None, show_die_gridlines=True, discrete_legend_colors=None, ): wx.Panel.__init__(self, parent) ### Inputs ########################################################## self.parent = parent self.xyd = xyd self.wafer_info = wafer_info # backwards compatability if isinstance(data_type, str): data_type = wm_const.DataType(data_type) self.data_type = data_type self.coord_type = coord_type self.high_color = high_color self.low_color = low_color self.grid_center = self.wafer_info.center_xy self.die_size = self.wafer_info.die_size self.plot_range = plot_range self.plot_die_centers = plot_die_centers self.discrete_legend_values = discrete_legend_values self.discrete_legend_colors = discrete_legend_colors self.die_gridlines_bool = show_die_gridlines ### Other Attributes ################################################ self.xyd_dict = xyd_to_dict(self.xyd) # data duplication! self.drag = False self.wfr_outline_bool = True self.crosshairs_bool = True self.reticle_gridlines_bool = False self.legend_bool = True self.die_centers = None # timer to give a delay when moving so that buffers aren't # re-built too many times. # TODO: Convert PyTimer to Timer and wx.EVT_TIMER. See wxPython demo. self.move_timer = wx.PyTimer(self.on_move_timer) self._init_ui() ### #-------------------------------------------------------------------- ### Methods ### #-------------------------------------------------------------------- def _init_ui(self): """Create the UI Elements and bind various events.""" # Create items to add to our layout self.canvas = FloatCanvas.FloatCanvas(self, BackgroundColor="BLACK", ) # Initialize the FloatCanvas. Needs to come before adding items! self.canvas.InitAll() # Create the legend self._create_legend() # Draw the die and wafer objects (outline, crosshairs, etc) on the canvas self.draw_die() if self.plot_die_centers: self.die_centers = self.draw_die_center() self.canvas.AddObject(self.die_centers) self.draw_wafer_objects() # Bind events to the canvas self._bind_events() # Create layout manager and add items self.hbox = wx.BoxSizer(wx.HORIZONTAL) self.hbox.Add(self.legend, 0, wx.EXPAND) self.hbox.Add(self.canvas, 1, wx.EXPAND) self.SetSizer(self.hbox) def _bind_events(self): """ Bind panel and canvas events. Note that key-down is bound again - this allws hotkeys to work even if the main Frame, which defines hotkeys in menus, is not present. wx sents the EVT_KEY_DOWN up the chain and, if the Frame and hotkeys are present, executes those instead. At least I think that's how that works... See http://wxpython.org/Phoenix/docs/html/events_overview.html for more info. """ # Canvas Events self.canvas.Bind(FloatCanvas.EVT_MOTION, self.on_mouse_move) self.canvas.Bind(FloatCanvas.EVT_MOUSEWHEEL, self.on_mouse_wheel) self.canvas.Bind(FloatCanvas.EVT_MIDDLE_DOWN, self.on_mouse_middle_down) self.canvas.Bind(FloatCanvas.EVT_MIDDLE_UP, self.on_mouse_middle_up) self.canvas.Bind(wx.EVT_PAINT, self._on_first_paint) # XXX: Binding the EVT_LEFT_DOWN seems to cause Issue #24. # What seems to happen is: If I bind EVT_LEFT_DOWN, then the # parent panel or application can't set focus to this # panel, which prevents the EVT_MOUSEWHEEL event from firing # properly. # self.canvas.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_left_down) # self.canvas.Bind(wx.EVT_RIGHT_DOWN, self.on_mouse_right_down) # self.canvas.Bind(wx.EVT_LEFT_UP, self.on_mouse_left_up) # self.canvas.Bind(wx.EVT_KEY_DOWN, self._on_key_down) # This is supposed to fix flicker on mouse move, but it doesn't work. # self.Bind(wx.EVT_ERASE_BACKGROUND, None) # Panel Events self.Bind(csel.EVT_COLOURSELECT, self.on_color_change) def _create_legend(self): """ Create the legend. For Continuous data, uses min(data) and max(data) for plot range. Might change to 5th percentile and 95th percentile. """ if self.data_type == wm_const.DataType.DISCRETE: if self.discrete_legend_values is None: unique_items = list({_die[2] for _die in self.xyd}) else: unique_items = self.discrete_legend_values self.legend = wm_legend.DiscreteLegend(self, labels=unique_items, colors=self.discrete_legend_colors, ) else: if self.plot_range is None: p_98 = float(wm_utils.nanpercentile([_i[2] for _i in self.xyd], 98)) p_02 = float(wm_utils.nanpercentile([_i[2] for _i in self.xyd], 2)) data_min = min([die[2] for die in self.xyd]) data_max = max([die[2] for die in self.xyd]) self.plot_range = (data_min, data_max) self.plot_range = (p_02, p_98) self.legend = wm_legend.ContinuousLegend(self, self.plot_range, self.high_color, self.low_color, ) def _clear_canvas(self): """Clear the canvas.""" self.canvas.ClearAll(ResetBB=False) def draw_die(self): """Draw and add the die on the canvas.""" color_dict = None for die in self.xyd: # define the die color if self.data_type == wm_const.DataType.DISCRETE: color_dict = self.legend.color_dict color = color_dict[die[2]] else: color = self.legend.get_color(die[2]) # Determine the die's lower-left coordinate lower_left_coord = wm_utils.grid_to_rect_coord(die[:2], self.die_size, self.grid_center) # Draw the die on the canvas self.canvas.AddRectangle(lower_left_coord, self.die_size, LineWidth=1, FillColor=color, ) def draw_die_center(self): """Plot the die centers as a small dot.""" centers = [] for die in self.xyd: # Determine the die's lower-left coordinate lower_left_coord = wm_utils.grid_to_rect_coord(die[:2], self.die_size, self.grid_center) # then adjust back to the die center lower_left_coord = (lower_left_coord[0] + self.die_size[0] / 2, lower_left_coord[1] + self.die_size[1] / 2) circ = FloatCanvas.Circle(lower_left_coord, 0.5, FillColor=wm_const.wm_DIE_CENTER_DOT_COLOR, ) centers.append(circ) return FloatCanvas.Group(centers) def draw_wafer_objects(self): """Draw and add the various wafer objects.""" self.wafer_outline = draw_wafer_outline(self.wafer_info.dia, self.wafer_info.edge_excl, self.wafer_info.flat_excl) self.canvas.AddObject(self.wafer_outline) if self.die_gridlines_bool: self.die_gridlines = draw_die_gridlines(self.wafer_info) self.canvas.AddObject(self.die_gridlines) self.crosshairs = draw_crosshairs(self.wafer_info.dia, dot=False) self.canvas.AddObject(self.crosshairs) def zoom_fill(self): """Zoom so that everything is displayed.""" self.canvas.ZoomToBB() def toggle_outline(self): """Toggle the wafer outline and edge exclusion on and off.""" if self.wfr_outline_bool: self.canvas.RemoveObject(self.wafer_outline) self.wfr_outline_bool = False else: self.canvas.AddObject(self.wafer_outline) self.wfr_outline_bool = True self.canvas.Draw() def toggle_crosshairs(self): """Toggle the center crosshairs on and off.""" if self.crosshairs_bool: self.canvas.RemoveObject(self.crosshairs) self.crosshairs_bool = False else: self.canvas.AddObject(self.crosshairs) self.crosshairs_bool = True self.canvas.Draw() def toggle_die_gridlines(self): """Toggle the die gridlines on and off.""" if self.die_gridlines_bool: self.canvas.RemoveObject(self.die_gridlines) self.die_gridlines_bool = False else: self.canvas.AddObject(self.die_gridlines) self.die_gridlines_bool = True self.canvas.Draw() def toggle_die_centers(self): """Toggle the die centers on and off.""" if self.die_centers is None: self.die_centers = self.draw_die_center() if self.plot_die_centers: self.canvas.RemoveObject(self.die_centers) self.plot_die_centers = False else: self.canvas.AddObject(self.die_centers) self.plot_die_centers = True self.canvas.Draw() def toggle_legend(self): """Toggle the legend on and off.""" if self.legend_bool: self.hbox.Remove(0) self.Layout() # forces update of layout self.legend_bool = False else: self.hbox.Insert(0, self.legend, 0) self.Layout() self.legend_bool = True self.canvas.Draw(Force=True) ### #-------------------------------------------------------------------- ### Event Handlers ### #-------------------------------------------------------------------- def _on_key_down(self, event): """ Event Handler for Keyboard Shortcuts. This is used when the panel is integrated into a Frame and the Frame does not define the KB Shortcuts already. If inside a frame, the wx.EVT_KEY_DOWN event is sent to the toplevel Frame which handles the event (if defined). At least I think that's how that works... See http://wxpython.org/Phoenix/docs/html/events_overview.html for more info. Shortcuts: HOME: Zoom to fill window O: Toggle wafer outline C: Toggle wafer crosshairs L: Toggle the legend D: Toggle die centers """ # TODO: Decide if I want to move this to a class attribute keycodes = {wx.WXK_HOME: self.zoom_fill, # "Home 79: self.toggle_outline, # "O" 67: self.toggle_crosshairs, # "C" 76: self.toggle_legend, # "L" 68: self.toggle_die_centers, # "D" } # print("panel event!") key = event.GetKeyCode() if key in keycodes.keys(): keycodes[key]() else: # print("KeyCode: {}".format(key)) pass def _on_first_paint(self, event): """Zoom to fill on the first paint event.""" # disable the handler for future paint events self.canvas.Bind(wx.EVT_PAINT, None) #TODO: Fix a flicker-type event that occurs on this call self.zoom_fill() def on_color_change(self, event): """Update the wafer map canvas with the new color.""" self._clear_canvas() if self.data_type == wm_const.DataType.CONTINUOUS: # call the continuous legend on_color_change() code self.legend.on_color_change(event) self.draw_die() if self.plot_die_centers: self.die_centers = self.draw_die_center() self.canvas.AddObject(self.die_centers) self.draw_wafer_objects() self.canvas.Draw(True) # self.canvas.Unbind(FloatCanvas.EVT_MOUSEWHEEL) # self.canvas.Bind(FloatCanvas.EVT_MOUSEWHEEL, self.on_mouse_wheel) def on_move_timer(self, event=None): """ Redraw the canvas whenever the move_timer is triggered. This is needed to prevent buffers from being rebuilt too often. """ # self.canvas.MoveImage(self.diff_loc, 'Pixel', ReDraw=True) self.canvas.Draw() def on_mouse_wheel(self, event): """Mouse wheel event for Zooming.""" speed = event.GetWheelRotation() pos = event.GetPosition() x, y, w, h = self.canvas.GetClientRect() # If the mouse is outside the FloatCanvas area, do nothing if pos[0] < 0 or pos[1] < 0 or pos[0] > x + w or pos[1] > y + h: return # calculate a zoom factor based on the wheel movement # Allows for zoom acceleration: fast wheel move = large zoom. # factor < 1: zoom out. factor > 1: zoom in sign = abs(speed) / speed factor = (abs(speed) * wm_const.wm_ZOOM_FACTOR)**sign # Changes to FloatCanvas.Zoom mean we need to do the following # rather than calling the zoom() function. # Note that SetToNewScale() changes the pixel center (?). This is why # we can call PixelToWorld(pos) again and get a different value! oldpoint = self.canvas.PixelToWorld(pos) self.canvas.Scale = self.canvas.Scale * factor self.canvas.SetToNewScale(False) # sets new scale but no redraw newpoint = self.canvas.PixelToWorld(pos) delta = newpoint - oldpoint self.canvas.MoveImage(-delta, 'World') # performs the redraw def on_mouse_move(self, event): """Update the status bar with the world coordinates.""" # display the mouse coords on the Frame StatusBar parent = wx.GetTopLevelParent(self) ds_x, ds_y = self.die_size gc_x, gc_y = self.grid_center dc_x, dc_y = wm_utils.coord_to_grid(event.Coords, self.die_size, self.grid_center, ) # lookup the die value grid = "x{}y{}" die_grid = grid.format(dc_x, dc_y) try: die_val = self.xyd_dict[die_grid] except KeyError: die_val = "N/A" # create the status bar string coord_str = "{x:0.3f}, {y:0.3f}" mouse_coord = "(" + coord_str.format(x=event.Coords[0], y=event.Coords[1], ) + ")" die_radius = math.sqrt((ds_x * (gc_x - dc_x))**2 + (ds_y * (gc_y - dc_y))**2) mouse_radius = math.sqrt(event.Coords[0]**2 + event.Coords[1]**2) status_str = "Die {d_grid} :: Radius = {d_rad:0.3f} :: Value = {d_val} " status_str += "Mouse {m_coord} :: Radius = {m_rad:0.3f}" status_str = status_str.format(d_grid=die_grid, # grid d_val=die_val, # value d_rad=die_radius, # radius m_coord=mouse_coord, # coord m_rad=mouse_radius, # radius ) try: parent.SetStatusText(status_str) except: # TODO: put in exception types. pass # If we're dragging, actually move the image. if self.drag: self.end_move_loc = np.array(event.GetPosition()) self.diff_loc = self.mid_move_loc - self.end_move_loc self.canvas.MoveImage(self.diff_loc, 'Pixel', ReDraw=True) self.mid_move_loc = self.end_move_loc # doesn't appear to do anything... self.move_timer.Start(30, oneShot=True) def on_mouse_middle_down(self, event): """Start the drag.""" self.drag = True # Update various positions self.start_move_loc = np.array(event.GetPosition()) self.mid_move_loc = self.start_move_loc self.prev_move_loc = (0, 0) self.end_move_loc = None # Change the cursor to a drag cursor self.SetCursor(wx.Cursor(wx.CURSOR_SIZING)) def on_mouse_middle_up(self, event): """End the drag.""" self.drag = False # update various positions if self.start_move_loc is not None: self.end_move_loc = np.array(event.GetPosition()) self.diff_loc = self.mid_move_loc - self.end_move_loc self.canvas.MoveImage(self.diff_loc, 'Pixel', ReDraw=True) # change the cursor back to normal self.SetCursor(wx.Cursor(wx.CURSOR_ARROW)) def on_mouse_left_down(self, event): """Start making the zoom-to-box box.""" # print("Left mouse down!") # pcoord = event.GetPosition() # wcoord = self.canvas.PixelToWorld(pcoord) # string = "Pixel Coord = {} \tWorld Coord = {}" # print(string.format(pcoord, wcoord)) # TODO: Look into what I was doing here. Why no 'self' on parent? parent = wx.GetTopLevelParent(self) wx.PostEvent(self.parent, event) def on_mouse_left_up(self, event): """End making the zoom-to-box box and execute the zoom.""" print("Left mouse up!") def on_mouse_right_down(self, event): """Start making the zoom-out box.""" print("Right mouse down!") def on_mouse_right_up(self, event): """Stop making the zoom-out box and execute the zoom.""" print("Right mouse up!") # --------------------------------------------------------------------------- ### Module Functions # --------------------------------------------------------------------------- def xyd_to_dict(xyd_list): """Convert the xyd list to a dict of xNNyNN key-value pairs.""" return {"x{}y{}".format(_x, _y): _d for _x, _y, _d in xyd_list} def draw_wafer_outline(dia=150, excl=5, flat=None): """ Draw a wafer outline for a given radius, including any exclusion lines. Parameters ---------- dia : float, optional The wafer diameter in mm. Defaults to `150`. excl : float, optional The exclusion distance from the edge of the wafer in mm. Defaults to `5`. flat : float, optional The exclusion distance from the wafer flat in mm. If ``None``, uses the same value as ``excl``. Defaults to ``None``. Returns ------- :class:`wx.lib.floatcanvas.FloatCanvas.Group` A ``Group`` that can be added to any floatcanvas.FloatCanvas instance. """ rad = float(dia)/2.0 if flat is None: flat = excl # Full wafer outline circle circ = FloatCanvas.Circle((0, 0), dia, LineColor=wm_const.wm_OUTLINE_COLOR, LineWidth=1, ) # Calculate the exclusion Radius exclRad = 0.5 * (dia - 2.0 * excl) if dia in wm_const.wm_FLAT_LENGTHS: # A flat is defined, so we draw it. flat_size = wm_const.wm_FLAT_LENGTHS[dia] x = flat_size/2 y = -math.sqrt(rad**2 - x**2) # Wfr Flat's Y Location arc = FloatCanvas.Arc((x, y), (-x, y), (0, 0), LineColor=wm_const.wm_WAFER_EDGE_COLOR, LineWidth=3, ) # actually a wafer flat, but called notch notch = draw_wafer_flat(rad, wm_const.wm_FLAT_LENGTHS[dia]) # Define the arc angle based on the flat exclusion, not the edge # exclusion. Find the flat exclusion X and Y coords. FSSflatY = y + flat if exclRad < abs(FSSflatY): # Then draw a circle with no flat excl_arc = FloatCanvas.Circle((0, 0), exclRad * 2, LineColor=wm_const.wm_WAFER_EDGE_COLOR, LineWidth=3, ) excl_group = FloatCanvas.Group([excl_arc]) else: FSSflatX = math.sqrt(exclRad**2 - FSSflatY**2) # Define the wafer arc excl_arc = FloatCanvas.Arc((FSSflatX, FSSflatY), (-FSSflatX, FSSflatY), (0, 0), LineColor=wm_const.wm_WAFER_EDGE_COLOR, LineWidth=3, ) excl_notch = draw_wafer_flat(exclRad, FSSflatX * 2) excl_group = FloatCanvas.Group([excl_arc, excl_notch]) else: # Flat not defined, so use a notch to denote wafer orientation. ang = 2.5 start_xy, end_xy = calc_flat_coords(rad, ang) arc = FloatCanvas.Arc(start_xy, end_xy, (0, 0), LineColor=wm_const.wm_WAFER_EDGE_COLOR, LineWidth=3, ) notch = draw_wafer_notch(rad) # Flat not defined, so use a notch to denote wafer orientation. start_xy, end_xy = calc_flat_coords(exclRad, ang) excl_arc = FloatCanvas.Arc(start_xy, end_xy, (0, 0), LineColor=wm_const.wm_WAFER_EDGE_COLOR, LineWidth=3, ) excl_notch = draw_wafer_notch(exclRad) excl_group = FloatCanvas.Group([excl_arc, excl_notch]) # Group the outline arc and the orientation (flat / notch) together group = FloatCanvas.Group([circ, arc, notch, excl_group]) return group def calc_flat_coords(radius, angle): """ Calculate the chord of a circle that spans ``angle``. Assumes the chord is centered on the y-axis. Calculate the starting and ending XY coordinates for a horizontal line below the y axis that interects a circle of radius ``radius`` and makes an angle ``angle`` at the center of the circle. This line is below the y axis. Parameters ---------- radius : float The radius of the circle that the line intersects. angle : float The angle, in degrees, that the line spans. Returns ------- (start_xy, end_xy) : tuple of coord pairs The starting and ending XY coordinates of the line. (start_x, start_y), (end_x, end_y)) Notes ----- What follows is a poor-mans schematic. I hope. :: 1-------------------------------------------------------1 1 1 1 1 1 + 1 1 . . 1 1 . . 1 1 . . Radius 1 1 . . 1 1 . . 1 1 . . 1 1 . . 1 1 . <--angle--> . 1 1 . . 1 1 . . 1 1 . . 1 1 . . 1 1 . . 1 1 . . 1 1 . . 1 1 . . 1 1-------------line--------------1 1 1 1 1 1 1 111111111111 """ ang_rad = angle * math.pi / 180 start_xy = (radius * math.sin(ang_rad), -radius * math.cos(ang_rad)) end_xy = (-radius * math.sin(ang_rad), -radius * math.cos(ang_rad)) return (start_xy, end_xy) def draw_crosshairs(dia=150, dot=False): """Draw the crosshairs or wafer center dot.""" if dot: circ = FloatCanvas.Circle((0, 0), 2.5, FillColor=wm_const.wm_WAFER_CENTER_DOT_COLOR, ) return FloatCanvas.Group([circ]) else: # Default: use crosshairs rad = dia / 2 xline = FloatCanvas.Line([(rad * 1.05, 0), (-rad * 1.05, 0)], LineColor=wx.CYAN, ) yline = FloatCanvas.Line([(0, rad * 1.05), (0, -rad * 1.05)], LineColor=wx.CYAN, ) return FloatCanvas.Group([xline, yline]) def draw_die_gridlines(wf): """ Draw the die gridlines. Parameters ---------- wf : :class:`wm_info.WaferInfo` The wafer info to calculate gridlines for. Returns ------- group : :class:`wx.lib.floatcanvas.FloatCanvas.Group` The collection of all die gridlines. """ x_size = wf.die_size[0] y_size = wf.die_size[1] grey = wx.Colour(64, 64, 64) edge = (wf.dia / 2) * 1.05 # calculate the values for the gridlines x_ref = -math.modf(wf.center_xy[0])[0] * x_size + (x_size / 2) pos_vert = np.arange(x_ref, edge, x_size) neg_vert = np.arange(x_ref, -edge, -x_size) y_ref = math.modf(wf.center_xy[1])[0] * y_size + (y_size/2) pos_horiz = np.arange(y_ref, edge, y_size) neg_horiz = np.arange(y_ref, -edge, -y_size) # reverse `[::-1]`, remove duplicate `[1:]`, and join x_values = np.concatenate((neg_vert[::-1], pos_vert[1:])) y_values = np.concatenate((neg_horiz[::-1], pos_horiz[1:])) line_coords = list([(x, -edge), (x, edge)] for x in x_values) line_coords.extend([(-edge, y), (edge, y)] for y in y_values) lines = [FloatCanvas.Line(l, LineColor=grey) for l in line_coords] return FloatCanvas.Group(list(lines)) def draw_wafer_flat(rad, flat_length): """Draw a wafer flat for a given radius and flat length.""" x = flat_length/2 y = -math.sqrt(rad**2 - x**2) flat = FloatCanvas.Line([(-x, y), (x, y)], LineColor=wm_const.wm_WAFER_EDGE_COLOR, LineWidth=3, ) return flat def draw_excl_flat(rad, flat_y, line_width=1, line_color='black'): """Draw a wafer flat for a given radius and flat length.""" flat_x = math.sqrt(rad**2 - flat_y**2) flat = FloatCanvas.Line([(-flat_x, flat_y), (flat_x, flat_y)], LineColor=wm_const.wm_WAFER_EDGE_COLOR, LineWidth=3, ) return flat def draw_wafer_notch(rad): """Draw a wafer notch for a given wafer radius.""" ang = 2.5 ang_rad = ang * math.pi / 180 # Define the Notch as a series of 3 (x, y) points xy_points = [(-rad * math.sin(ang_rad), -rad * math.cos(ang_rad)), (0, -rad*0.95), (rad * math.sin(ang_rad), -rad * math.cos(ang_rad))] notch = FloatCanvas.Line(xy_points, LineColor=wm_const.wm_WAFER_EDGE_COLOR, LineWidth=2, ) return notch def main(): """Run when called as a module.""" raise RuntimeError("This module is not meant to be run by itself.") if __name__ == "__main__": main()