From 61c32714a6afc1d73365e4757cc7789a20908c15 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Mon, 6 Nov 2023 12:36:53 -0800 Subject: [PATCH] Normalize all line endings to LF (#100) --- src/wafer_map/__init__.py | 76 +- src/wafer_map/example.py | 332 +++---- src/wafer_map/gen_fake_data.py | 454 ++++----- src/wafer_map/wm_app.py | 290 +++--- src/wafer_map/wm_constants.py | 100 +- src/wafer_map/wm_core.py | 1714 ++++++++++++++++---------------- src/wafer_map/wm_frame.py | 670 ++++++------- src/wafer_map/wm_info.py | 114 +-- src/wafer_map/wm_legend.py | 1350 ++++++++++++------------- src/wafer_map/wm_utils.py | 1202 +++++++++++----------- tests/__init__.py | 12 +- tests/test_utils.py | 46 +- 12 files changed, 3180 insertions(+), 3180 deletions(-) diff --git a/src/wafer_map/__init__.py b/src/wafer_map/__init__.py index beff3c6..2ccd24d 100644 --- a/src/wafer_map/__init__.py +++ b/src/wafer_map/__init__.py @@ -1,38 +1,38 @@ -# -*- coding: utf-8 -*- -""" -__init__ for wafer_map. - -Determines the python version and monkeypatches wx.Colour. -""" -import os -import sys - - -### Constants ############################################################### -# __all__ = ['wm_app', 'wm_constants', 'wm_core', 'wm_frame', 'wm_info', -# 'wm_legend', 'wm_utils'] - - -if sys.version_info < (3,): - PY2 = True -elif sys.version_info < (2, 6): - raise RuntimeError("Only Python >= 2.7 is supported.") -else: - PY2 = False - - -# if we're building docs, don't try and import or monkeypatch wxPython. -if os.getenv("READTHEDOCS", "False") == "True": - pass -else: - # Fix hashing for wx.Colour - # See https://groups.google.com/forum/#!topic/wxpython-dev/NLd4CZv9rII - import wx - - ok = getattr(wx.Colour, "__hash__") - if ok is None: - - def _Colour___hash(self): - return hash(tuple(self.Get())) - - wx.Colour.__hash__ = _Colour___hash +# -*- coding: utf-8 -*- +""" +__init__ for wafer_map. + +Determines the python version and monkeypatches wx.Colour. +""" +import os +import sys + + +### Constants ############################################################### +# __all__ = ['wm_app', 'wm_constants', 'wm_core', 'wm_frame', 'wm_info', +# 'wm_legend', 'wm_utils'] + + +if sys.version_info < (3,): + PY2 = True +elif sys.version_info < (2, 6): + raise RuntimeError("Only Python >= 2.7 is supported.") +else: + PY2 = False + + +# if we're building docs, don't try and import or monkeypatch wxPython. +if os.getenv("READTHEDOCS", "False") == "True": + pass +else: + # Fix hashing for wx.Colour + # See https://groups.google.com/forum/#!topic/wxpython-dev/NLd4CZv9rII + import wx + + ok = getattr(wx.Colour, "__hash__") + if ok is None: + + def _Colour___hash(self): + return hash(tuple(self.Get())) + + wx.Colour.__hash__ = _Colour___hash diff --git a/src/wafer_map/example.py b/src/wafer_map/example.py index 4264046..80988b2 100644 --- a/src/wafer_map/example.py +++ b/src/wafer_map/example.py @@ -1,166 +1,166 @@ -# -*- coding: utf-8 -*- -""" -Provides examples on how to use the ``wafer_map`` package. - -This module is called when running ``python -m wafer_map``. -""" -from __future__ import absolute_import, division, print_function, unicode_literals - -import wx - -from wafer_map import gen_fake_data -from wafer_map import wm_core -from wafer_map import wm_app -from wafer_map.wm_constants import DataType - - -__author__ = "Douglas Thor" -__version__ = "v0.4.0" - - -def standalone_app(xyd, wafer_info): - """ - Example of running wafer_map as a standalone application. - - All you need to do once you have your data in the correct format is - to call ``wm_app.WaferMapApp`` with your keyword arguments - which define the wafer and die parameters such as die size, the wafer - diameter, and the edge exclusion. - - Parameters - ---------- - xyd : list of 3-tuples - The data to plot. - wafer_info : :class:`wafer_map.wm_info.WaferInfo` - The wafer information such as die size, diameter, etc. - - Notes - ----- - The ``xyd`` values need to have this format:: - - [(grid_x_1, grid_y_1, data_1), (grid_x_2, grid_y_2, data_2), ..., ] - """ - wm_app.WaferMapApp( - xyd, - wafer_info.die_size, - wafer_info.center_xy, - wafer_info.dia, - wafer_info.edge_excl, - wafer_info.flat_excl, - ) - - -def add_to_existing_app(xyd, wafer_info): - """ - Example of adding the wafer map to your existing wxPython application. - - To add a wafer map to an existing application, instance the - ``wm_core.WaferMapPanel()`` class with your data and wafer info. The - wafer info must be a ``wm_info.WaferInfo`` object. - - Parameters - ---------- - xyd : list of 3-tuples - The data to plot. - wafer_info : :class:`wafer_map.wm_info.WaferInfo` - The wafer information such as die size, diameter, etc. - """ - app = wx.App() - - class ExampleFrame(wx.Frame): - """Base Frame.""" - - def __init__(self, title, xyd, wafer_info): - wx.Frame.__init__( - self, - None, # Window Parent - wx.ID_ANY, # id - title=title, # Window Title - size=(600 + 16, 500 + 38), # Size in px - ) - self.xyd = xyd - self.wafer_info = wafer_info - - # Add a status bar if you want to - self.CreateStatusBar() - - # Bind events - self.Bind(wx.EVT_CLOSE, self.OnQuit) - - # Create some other dummy stuff for the example - self.listbox = wx.ListBox( - self, - wx.ID_ANY, - choices=["A", "B", "C", "D"], - ) - self.button = wx.Button(self, wx.ID_ANY, label="Big Button!") - - # Create the wafer map - self.panel = wm_core.WaferMapPanel( - self, - self.xyd, - self.wafer_info, - show_die_gridlines=False, - ) - - # set our layout - self.hbox = wx.BoxSizer(wx.HORIZONTAL) - self.vbox = wx.BoxSizer(wx.VERTICAL) - - self.vbox.Add(self.panel, 3, wx.EXPAND) - self.vbox.Add(self.button, 1, wx.EXPAND) - self.hbox.Add(self.listbox, 1, wx.EXPAND) - self.hbox.Add(self.vbox, 3, wx.EXPAND) - self.SetSizer(self.hbox) - - def OnQuit(self, event): - self.Destroy() - - frame = ExampleFrame("Called as a panel in your own app!", xyd, wafer_info) - frame.Show() - app.MainLoop() - - -def discrete_data_example(xyd, wafer_info): - """ - Example of plotting discrete data using the standalone app version. - - Plotting discrete data is the same as continuous data, but you need to - add the ``data_type`` argument to the class initialization. - - Parameters - ---------- - xyd : list of 3-tuples - The data to plot. - wafer_info : :class:`wafer_map.wm_info.WaferInfo` - The wafer information such as die size, diameter, etc. - """ - import random - - bins = ["Bin1", "Bin1", "Bin1", "Bin2", "Dragons", "Bin1", "Bin2"] - discrete_xyd = [(_x, _y, random.choice(bins)) for _x, _y, _ in xyd] - - wm_app.WaferMapApp( - discrete_xyd, - wafer_info.die_size, - wafer_info.center_xy, - wafer_info.dia, - wafer_info.edge_excl, - wafer_info.flat_excl, - data_type=DataType.DISCRETE, - show_die_gridlines=False, - ) - - -def main(): - """Run when called as a module.""" - # Generate some fake data - wafer_info, xyd = gen_fake_data.generate_fake_data() - - standalone_app(xyd, wafer_info) - add_to_existing_app(xyd, wafer_info) - discrete_data_example(xyd, wafer_info) - - -if __name__ == "__main__": - main() +# -*- coding: utf-8 -*- +""" +Provides examples on how to use the ``wafer_map`` package. + +This module is called when running ``python -m wafer_map``. +""" +from __future__ import absolute_import, division, print_function, unicode_literals + +import wx + +from wafer_map import gen_fake_data +from wafer_map import wm_core +from wafer_map import wm_app +from wafer_map.wm_constants import DataType + + +__author__ = "Douglas Thor" +__version__ = "v0.4.0" + + +def standalone_app(xyd, wafer_info): + """ + Example of running wafer_map as a standalone application. + + All you need to do once you have your data in the correct format is + to call ``wm_app.WaferMapApp`` with your keyword arguments + which define the wafer and die parameters such as die size, the wafer + diameter, and the edge exclusion. + + Parameters + ---------- + xyd : list of 3-tuples + The data to plot. + wafer_info : :class:`wafer_map.wm_info.WaferInfo` + The wafer information such as die size, diameter, etc. + + Notes + ----- + The ``xyd`` values need to have this format:: + + [(grid_x_1, grid_y_1, data_1), (grid_x_2, grid_y_2, data_2), ..., ] + """ + wm_app.WaferMapApp( + xyd, + wafer_info.die_size, + wafer_info.center_xy, + wafer_info.dia, + wafer_info.edge_excl, + wafer_info.flat_excl, + ) + + +def add_to_existing_app(xyd, wafer_info): + """ + Example of adding the wafer map to your existing wxPython application. + + To add a wafer map to an existing application, instance the + ``wm_core.WaferMapPanel()`` class with your data and wafer info. The + wafer info must be a ``wm_info.WaferInfo`` object. + + Parameters + ---------- + xyd : list of 3-tuples + The data to plot. + wafer_info : :class:`wafer_map.wm_info.WaferInfo` + The wafer information such as die size, diameter, etc. + """ + app = wx.App() + + class ExampleFrame(wx.Frame): + """Base Frame.""" + + def __init__(self, title, xyd, wafer_info): + wx.Frame.__init__( + self, + None, # Window Parent + wx.ID_ANY, # id + title=title, # Window Title + size=(600 + 16, 500 + 38), # Size in px + ) + self.xyd = xyd + self.wafer_info = wafer_info + + # Add a status bar if you want to + self.CreateStatusBar() + + # Bind events + self.Bind(wx.EVT_CLOSE, self.OnQuit) + + # Create some other dummy stuff for the example + self.listbox = wx.ListBox( + self, + wx.ID_ANY, + choices=["A", "B", "C", "D"], + ) + self.button = wx.Button(self, wx.ID_ANY, label="Big Button!") + + # Create the wafer map + self.panel = wm_core.WaferMapPanel( + self, + self.xyd, + self.wafer_info, + show_die_gridlines=False, + ) + + # set our layout + self.hbox = wx.BoxSizer(wx.HORIZONTAL) + self.vbox = wx.BoxSizer(wx.VERTICAL) + + self.vbox.Add(self.panel, 3, wx.EXPAND) + self.vbox.Add(self.button, 1, wx.EXPAND) + self.hbox.Add(self.listbox, 1, wx.EXPAND) + self.hbox.Add(self.vbox, 3, wx.EXPAND) + self.SetSizer(self.hbox) + + def OnQuit(self, event): + self.Destroy() + + frame = ExampleFrame("Called as a panel in your own app!", xyd, wafer_info) + frame.Show() + app.MainLoop() + + +def discrete_data_example(xyd, wafer_info): + """ + Example of plotting discrete data using the standalone app version. + + Plotting discrete data is the same as continuous data, but you need to + add the ``data_type`` argument to the class initialization. + + Parameters + ---------- + xyd : list of 3-tuples + The data to plot. + wafer_info : :class:`wafer_map.wm_info.WaferInfo` + The wafer information such as die size, diameter, etc. + """ + import random + + bins = ["Bin1", "Bin1", "Bin1", "Bin2", "Dragons", "Bin1", "Bin2"] + discrete_xyd = [(_x, _y, random.choice(bins)) for _x, _y, _ in xyd] + + wm_app.WaferMapApp( + discrete_xyd, + wafer_info.die_size, + wafer_info.center_xy, + wafer_info.dia, + wafer_info.edge_excl, + wafer_info.flat_excl, + data_type=DataType.DISCRETE, + show_die_gridlines=False, + ) + + +def main(): + """Run when called as a module.""" + # Generate some fake data + wafer_info, xyd = gen_fake_data.generate_fake_data() + + standalone_app(xyd, wafer_info) + add_to_existing_app(xyd, wafer_info) + discrete_data_example(xyd, wafer_info) + + +if __name__ == "__main__": + main() diff --git a/src/wafer_map/gen_fake_data.py b/src/wafer_map/gen_fake_data.py index 0d1c4e1..378f7f5 100644 --- a/src/wafer_map/gen_fake_data.py +++ b/src/wafer_map/gen_fake_data.py @@ -1,227 +1,227 @@ -# -*- coding: utf-8 -*- -""" -Generate fake data for the wafer_map demo. - -Typically not used by anything but the example. It's also a pretty shitty -peice of code so... ye be warned. -""" -from __future__ import absolute_import, division, print_function, unicode_literals -import math -import random - -from wafer_map import PY2 -from wafer_map import wm_info -from wafer_map import wm_utils -from wafer_map import wm_constants as wm_const - - -# Python2 Compatibility -if PY2: - range = xrange - - -def generate_fake_data(**kwargs): - """ - Generate fake data for wafer_map. - - Parameters - ---------- - die_x : float - Die x size in mm - die_y : float - Die y size in mm - dia : float - Wafer diameter in mm - edge_excl : float - Edge exclusion in mm - flat_excl : float - Wafer Flat exclusion in mm - x_offset : float - The center die's x offset. - (0-1), where 0.5 means that the center of the - wafer is located on a vertical street (edge of a die). - y_offset : float - The center die's y offset. - (0-1), where 0.5 means that the center of the - wafer is located on a horizontal street (edge of a die). - grid_center : tuple of floats - The grid coordinates (x_col, y_row) that denote the - center of the wafer. Defaults to None, which determines - the center grid coordinates based on the total number of - rows and columns. - dtype : string - Datatype. Valid options are "discrete" and "continuous". - Defaults to "continuous". - - Examples of Offsets - ------------------- - The ``X`` denotes the center of the wafer:: - - x_offset = 0 x_offset = 0 - y_offset = 0 y_offset = 0.5 - |-----------| |-----------| - | | | | - | X | | | - | | | | - |-----------| |-----X-----| - - x_offset = 0.5 x_offset = 0.5 - y_offset = 0 y_offset = 0.5 - |-----------| |-----------| - | | | | - X | | | - | | | | - |-----------| X-----------| - - Notes - ----- - Another rewrite, this time starting from the top. We will not look at - the wafer map until I'm satisfied with the numerical values. - - - Things to keep in mind: - - grid coords fall on die centers - - die are drawn from the lower-left corner - - center of the wafer is coord (0, 0) - - the center of the wafer is mapped to a - floating-precision (grid_x, grid_y tuple) - - What wm_core needs is: - - - list of (grid_x, grid_y, plot_value) tuples - - die_size (x, y) tuple - - grid_center (grid_x, grid_y) tuple. Without this, we can't plot the - wafer outline - - Here's the game plan: - - 1. Generate a square grid of "die" that is guarenteed to cover the - entire wafer. - 2. Choose an arbitrary center point ``grid_center``. - 3. Calculate the max_dist of each die based on grid_center. - 4. Remove any die that cross the exclusion boundary - 5. Calculate the lower-left coordinate of each of those die - 6. Complete. - """ - dia_list = [100, 150, 200, 210] - excl_list = [0, 2.5, 5, 10] - offset_list = [0, 0.5, -2, 0.24] - DEFAULT_KWARGS = { - "die_x": random.uniform(5, 10), - "die_y": random.uniform(5, 10), - "dia": random.choice(dia_list), - "edge_excl": random.choice(excl_list), - "flat_excl": random.choice(excl_list), - "x_offset": random.choice(offset_list), - "y_offset": random.choice(offset_list), - "grid_center": None, - "dtype": wm_const.DataType.CONTINUOUS, - } - - # parse the keyword arguements, asigning defaults if not found. - for key in DEFAULT_KWARGS: - if key not in kwargs: - kwargs[key] = DEFAULT_KWARGS[key] - - die_x = kwargs["die_x"] - die_y = kwargs["die_y"] - dia = kwargs["dia"] - edge_excl = kwargs["edge_excl"] - flat_excl = kwargs["flat_excl"] - x_offset = kwargs["x_offset"] - y_offset = kwargs["y_offset"] - grid_center = kwargs["grid_center"] - dtype = kwargs["dtype"] - - die_size = (die_x, die_y) - - # Determine where our wafer edge is for the flat area - flat_y = -dia / 2 # assume wafer edge at first - if dia in wm_const.wm_FLAT_LENGTHS: - # A flat is defined by SEMI M1-0302, so we calcualte where it is - flat_y = -math.sqrt((dia / 2) ** 2 - (wm_const.wm_FLAT_LENGTHS[dia] * 0.5) ** 2) - - # calculate the exclusion radius^2 - excl_sqrd = (dia / 2) ** 2 + (edge_excl**2) - (dia * edge_excl) - - # 1. Generate square grid guarenteed to cover entire wafer - # We'll use 2x the wafer dia so that we can move center around a bit - grid_max_x = 2 * int(math.ceil(dia / die_x)) - grid_max_y = 2 * int(math.ceil(dia / die_y)) - - # 2. Determine the centerpoint - if grid_center is None: - grid_center = (grid_max_x / 2 + x_offset, grid_max_y / 2 + y_offset) - print("Offsets: {}".format((x_offset, y_offset))) - - # This could be more efficient - grid_points = [] - for _x in range(1, grid_max_x): - for _y in range(1, grid_max_y): - coord_die_center_x = die_x * (_x - grid_center[0]) - # we have to reverse the y coord, hence why it's - # ``grid_center[1] - _y`` and not ``_y - grid_center[1]`` - coord_die_center_y = die_y * (grid_center[1] - _y) - coord_die_center = (coord_die_center_x, coord_die_center_y) - center_rad_sqrd = coord_die_center_x**2 + coord_die_center_y**2 - die_max_sqrd = wm_utils.max_dist_sqrd(coord_die_center, die_size) - # coord_lower_left_x = coord_die_center_x - die_x/2 - coord_lower_left_y = coord_die_center_y - die_y / 2 - # coord_lower_left = (coord_lower_left_x, coord_lower_left_y) - if die_max_sqrd > excl_sqrd or coord_lower_left_y < (flat_y + flat_excl): - continue - else: - if dtype == "discrete": - grid_points.append( - ( - _x, - _y, - random.choice(["a", "b", "c"]), - # these items are for debug. - # coord_lower_left, - # center_rad_sqrd, - # coord_die_center, - # die_max_sqrd, - ) - ) - - else: - grid_points.append( - ( - _x, - _y, - center_rad_sqrd, - # these items are for debug. - # coord_lower_left, - # center_rad_sqrd, - # coord_die_center, - # die_max_sqrd, - ) - ) - - print("\nPlotting {} die.".format(len(grid_points))) - - # put all the wafer info into the WaferInfo class. - wafer_info = wm_info.WaferInfo( - die_size, # Die Size in (X, Y) - grid_center, # Center Coord (X, Y) - dia, # Wafer Diameter - edge_excl, # Edge Exclusion - flat_excl, # Flat Exclusion - ) - print(wafer_info) - - return (wafer_info, grid_points) - - -def main(): - """Run when called as a module.""" - wafer, data = generate_fake_data(dtype="discrete") - from pprint import pprint - - pprint(data) - # print() - # pprint([_i for _i in data if _i[0] in (17, 18, 19)]) - - -if __name__ == "__main__": - main() +# -*- coding: utf-8 -*- +""" +Generate fake data for the wafer_map demo. + +Typically not used by anything but the example. It's also a pretty shitty +peice of code so... ye be warned. +""" +from __future__ import absolute_import, division, print_function, unicode_literals +import math +import random + +from wafer_map import PY2 +from wafer_map import wm_info +from wafer_map import wm_utils +from wafer_map import wm_constants as wm_const + + +# Python2 Compatibility +if PY2: + range = xrange + + +def generate_fake_data(**kwargs): + """ + Generate fake data for wafer_map. + + Parameters + ---------- + die_x : float + Die x size in mm + die_y : float + Die y size in mm + dia : float + Wafer diameter in mm + edge_excl : float + Edge exclusion in mm + flat_excl : float + Wafer Flat exclusion in mm + x_offset : float + The center die's x offset. + (0-1), where 0.5 means that the center of the + wafer is located on a vertical street (edge of a die). + y_offset : float + The center die's y offset. + (0-1), where 0.5 means that the center of the + wafer is located on a horizontal street (edge of a die). + grid_center : tuple of floats + The grid coordinates (x_col, y_row) that denote the + center of the wafer. Defaults to None, which determines + the center grid coordinates based on the total number of + rows and columns. + dtype : string + Datatype. Valid options are "discrete" and "continuous". + Defaults to "continuous". + + Examples of Offsets + ------------------- + The ``X`` denotes the center of the wafer:: + + x_offset = 0 x_offset = 0 + y_offset = 0 y_offset = 0.5 + |-----------| |-----------| + | | | | + | X | | | + | | | | + |-----------| |-----X-----| + + x_offset = 0.5 x_offset = 0.5 + y_offset = 0 y_offset = 0.5 + |-----------| |-----------| + | | | | + X | | | + | | | | + |-----------| X-----------| + + Notes + ----- + Another rewrite, this time starting from the top. We will not look at + the wafer map until I'm satisfied with the numerical values. + + - Things to keep in mind: + - grid coords fall on die centers + - die are drawn from the lower-left corner + - center of the wafer is coord (0, 0) + - the center of the wafer is mapped to a + floating-precision (grid_x, grid_y tuple) + + What wm_core needs is: + + - list of (grid_x, grid_y, plot_value) tuples + - die_size (x, y) tuple + - grid_center (grid_x, grid_y) tuple. Without this, we can't plot the + wafer outline + + Here's the game plan: + + 1. Generate a square grid of "die" that is guarenteed to cover the + entire wafer. + 2. Choose an arbitrary center point ``grid_center``. + 3. Calculate the max_dist of each die based on grid_center. + 4. Remove any die that cross the exclusion boundary + 5. Calculate the lower-left coordinate of each of those die + 6. Complete. + """ + dia_list = [100, 150, 200, 210] + excl_list = [0, 2.5, 5, 10] + offset_list = [0, 0.5, -2, 0.24] + DEFAULT_KWARGS = { + "die_x": random.uniform(5, 10), + "die_y": random.uniform(5, 10), + "dia": random.choice(dia_list), + "edge_excl": random.choice(excl_list), + "flat_excl": random.choice(excl_list), + "x_offset": random.choice(offset_list), + "y_offset": random.choice(offset_list), + "grid_center": None, + "dtype": wm_const.DataType.CONTINUOUS, + } + + # parse the keyword arguements, asigning defaults if not found. + for key in DEFAULT_KWARGS: + if key not in kwargs: + kwargs[key] = DEFAULT_KWARGS[key] + + die_x = kwargs["die_x"] + die_y = kwargs["die_y"] + dia = kwargs["dia"] + edge_excl = kwargs["edge_excl"] + flat_excl = kwargs["flat_excl"] + x_offset = kwargs["x_offset"] + y_offset = kwargs["y_offset"] + grid_center = kwargs["grid_center"] + dtype = kwargs["dtype"] + + die_size = (die_x, die_y) + + # Determine where our wafer edge is for the flat area + flat_y = -dia / 2 # assume wafer edge at first + if dia in wm_const.wm_FLAT_LENGTHS: + # A flat is defined by SEMI M1-0302, so we calcualte where it is + flat_y = -math.sqrt((dia / 2) ** 2 - (wm_const.wm_FLAT_LENGTHS[dia] * 0.5) ** 2) + + # calculate the exclusion radius^2 + excl_sqrd = (dia / 2) ** 2 + (edge_excl**2) - (dia * edge_excl) + + # 1. Generate square grid guarenteed to cover entire wafer + # We'll use 2x the wafer dia so that we can move center around a bit + grid_max_x = 2 * int(math.ceil(dia / die_x)) + grid_max_y = 2 * int(math.ceil(dia / die_y)) + + # 2. Determine the centerpoint + if grid_center is None: + grid_center = (grid_max_x / 2 + x_offset, grid_max_y / 2 + y_offset) + print("Offsets: {}".format((x_offset, y_offset))) + + # This could be more efficient + grid_points = [] + for _x in range(1, grid_max_x): + for _y in range(1, grid_max_y): + coord_die_center_x = die_x * (_x - grid_center[0]) + # we have to reverse the y coord, hence why it's + # ``grid_center[1] - _y`` and not ``_y - grid_center[1]`` + coord_die_center_y = die_y * (grid_center[1] - _y) + coord_die_center = (coord_die_center_x, coord_die_center_y) + center_rad_sqrd = coord_die_center_x**2 + coord_die_center_y**2 + die_max_sqrd = wm_utils.max_dist_sqrd(coord_die_center, die_size) + # coord_lower_left_x = coord_die_center_x - die_x/2 + coord_lower_left_y = coord_die_center_y - die_y / 2 + # coord_lower_left = (coord_lower_left_x, coord_lower_left_y) + if die_max_sqrd > excl_sqrd or coord_lower_left_y < (flat_y + flat_excl): + continue + else: + if dtype == "discrete": + grid_points.append( + ( + _x, + _y, + random.choice(["a", "b", "c"]), + # these items are for debug. + # coord_lower_left, + # center_rad_sqrd, + # coord_die_center, + # die_max_sqrd, + ) + ) + + else: + grid_points.append( + ( + _x, + _y, + center_rad_sqrd, + # these items are for debug. + # coord_lower_left, + # center_rad_sqrd, + # coord_die_center, + # die_max_sqrd, + ) + ) + + print("\nPlotting {} die.".format(len(grid_points))) + + # put all the wafer info into the WaferInfo class. + wafer_info = wm_info.WaferInfo( + die_size, # Die Size in (X, Y) + grid_center, # Center Coord (X, Y) + dia, # Wafer Diameter + edge_excl, # Edge Exclusion + flat_excl, # Flat Exclusion + ) + print(wafer_info) + + return (wafer_info, grid_points) + + +def main(): + """Run when called as a module.""" + wafer, data = generate_fake_data(dtype="discrete") + from pprint import pprint + + pprint(data) + # print() + # pprint([_i for _i in data if _i[0] in (17, 18, 19)]) + + +if __name__ == "__main__": + main() diff --git a/src/wafer_map/wm_app.py b/src/wafer_map/wm_app.py index 75acfe5..be90c89 100644 --- a/src/wafer_map/wm_app.py +++ b/src/wafer_map/wm_app.py @@ -1,145 +1,145 @@ -# -*- coding: utf-8 -*- -""" -A self-contained Window for a Wafer Map. -""" -from __future__ import absolute_import, division, print_function, unicode_literals - -import wx - -from . import wm_frame -from . import wm_info -from . import gen_fake_data -from . import wm_constants as wm_const - - -class WaferMapApp(object): - """ - A self-contained Window for a Wafer Map. - - Parameters - ---------- - xyd : list of 3-tuples - The data to plot. - die_size : tuple - The die size in mm as a ``(width, height)`` tuple. - center_xy : tuple, optional - The center grid coordinate as a ``(x_grid, y_grid)`` tuple. - dia : float, optional - The wafer diameter in mm. Defaults to `150`. - edge_excl : float, optional - The distance in mm from the edge of the wafer that should be - considered bad die. Defaults to 5mm. - flat_excl : float, optional - The distance in mm from the wafer flat that should be - considered bad die. Defaults to 5mm. - data_type : wm_constants.DataType or str, optional - The type of data to plot. Must be one of `continuous` or `discrete`. - 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``. - show_die_gridlines : bool, optional - If ``True``, displayes gridlines along the die edges. Defaults to - ``True``. - """ - - def __init__( - self, - xyd, - die_size, - center_xy=(0, 0), - dia=150, - edge_excl=5, - flat_excl=5, - data_type=wm_const.DataType.CONTINUOUS, - high_color=wm_const.wm_HIGH_COLOR, - low_color=wm_const.wm_LOW_COLOR, - plot_range=None, - plot_die_centers=False, - show_die_gridlines=True, - ): - self.app = wx.App() - - self.wafer_info = wm_info.WaferInfo( - die_size, - center_xy, - dia, - edge_excl, - flat_excl, - ) - self.xyd = xyd - self.data_type = data_type - self.high_color = high_color - self.low_color = low_color - self.plot_range = plot_range - self.plot_die_centers = plot_die_centers - self.show_die_gridlines = show_die_gridlines - - self.frame = wm_frame.WaferMapWindow( - "Wafer Map Phoenix", - self.xyd, - self.wafer_info, - data_type=self.data_type, - high_color=self.high_color, - low_color=self.low_color, - # high_color=wx.Colour(255, 0, 0), - # low_color=wx.Colour(0, 0, 255), - plot_range=self.plot_range, - size=(600, 500), - plot_die_centers=self.plot_die_centers, - show_die_gridlines=self.show_die_gridlines, - ) - - self.frame.Show() - self.app.MainLoop() - - -def main(): - """Run when called as a module.""" - wafer_info, xyd = gen_fake_data.generate_fake_data( - die_x=5.43, - die_y=6.3, - dia=150, - edge_excl=4.5, - flat_excl=4.5, - x_offset=0, - y_offset=0.5, - grid_center=(29, 21.5), - ) - - import random - - bins = ["Bin1", "Bin1", "Bin1", "Bin2", "Dragons", "Bin1", "Bin2"] - discrete_xyd = [(_x, _y, random.choice(bins)) for _x, _y, _ in xyd] - - discrete = False - dtype = wm_const.DataType.CONTINUOUS - - # discrete = True # uncomment this line to use discrete data - if discrete: - xyd = discrete_xyd - dtype = wm_const.DataType.DISCRETE - - WaferMapApp( - xyd, - wafer_info.die_size, - wafer_info.center_xy, - wafer_info.dia, - wafer_info.edge_excl, - wafer_info.flat_excl, - data_type=dtype, - # plot_range=(0.0, 75.0**2), - plot_die_centers=True, - ) - - -if __name__ == "__main__": - main() +# -*- coding: utf-8 -*- +""" +A self-contained Window for a Wafer Map. +""" +from __future__ import absolute_import, division, print_function, unicode_literals + +import wx + +from . import wm_frame +from . import wm_info +from . import gen_fake_data +from . import wm_constants as wm_const + + +class WaferMapApp(object): + """ + A self-contained Window for a Wafer Map. + + Parameters + ---------- + xyd : list of 3-tuples + The data to plot. + die_size : tuple + The die size in mm as a ``(width, height)`` tuple. + center_xy : tuple, optional + The center grid coordinate as a ``(x_grid, y_grid)`` tuple. + dia : float, optional + The wafer diameter in mm. Defaults to `150`. + edge_excl : float, optional + The distance in mm from the edge of the wafer that should be + considered bad die. Defaults to 5mm. + flat_excl : float, optional + The distance in mm from the wafer flat that should be + considered bad die. Defaults to 5mm. + data_type : wm_constants.DataType or str, optional + The type of data to plot. Must be one of `continuous` or `discrete`. + 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``. + show_die_gridlines : bool, optional + If ``True``, displayes gridlines along the die edges. Defaults to + ``True``. + """ + + def __init__( + self, + xyd, + die_size, + center_xy=(0, 0), + dia=150, + edge_excl=5, + flat_excl=5, + data_type=wm_const.DataType.CONTINUOUS, + high_color=wm_const.wm_HIGH_COLOR, + low_color=wm_const.wm_LOW_COLOR, + plot_range=None, + plot_die_centers=False, + show_die_gridlines=True, + ): + self.app = wx.App() + + self.wafer_info = wm_info.WaferInfo( + die_size, + center_xy, + dia, + edge_excl, + flat_excl, + ) + self.xyd = xyd + self.data_type = data_type + self.high_color = high_color + self.low_color = low_color + self.plot_range = plot_range + self.plot_die_centers = plot_die_centers + self.show_die_gridlines = show_die_gridlines + + self.frame = wm_frame.WaferMapWindow( + "Wafer Map Phoenix", + self.xyd, + self.wafer_info, + data_type=self.data_type, + high_color=self.high_color, + low_color=self.low_color, + # high_color=wx.Colour(255, 0, 0), + # low_color=wx.Colour(0, 0, 255), + plot_range=self.plot_range, + size=(600, 500), + plot_die_centers=self.plot_die_centers, + show_die_gridlines=self.show_die_gridlines, + ) + + self.frame.Show() + self.app.MainLoop() + + +def main(): + """Run when called as a module.""" + wafer_info, xyd = gen_fake_data.generate_fake_data( + die_x=5.43, + die_y=6.3, + dia=150, + edge_excl=4.5, + flat_excl=4.5, + x_offset=0, + y_offset=0.5, + grid_center=(29, 21.5), + ) + + import random + + bins = ["Bin1", "Bin1", "Bin1", "Bin2", "Dragons", "Bin1", "Bin2"] + discrete_xyd = [(_x, _y, random.choice(bins)) for _x, _y, _ in xyd] + + discrete = False + dtype = wm_const.DataType.CONTINUOUS + + # discrete = True # uncomment this line to use discrete data + if discrete: + xyd = discrete_xyd + dtype = wm_const.DataType.DISCRETE + + WaferMapApp( + xyd, + wafer_info.die_size, + wafer_info.center_xy, + wafer_info.dia, + wafer_info.edge_excl, + wafer_info.flat_excl, + data_type=dtype, + # plot_range=(0.0, 75.0**2), + plot_die_centers=True, + ) + + +if __name__ == "__main__": + main() diff --git a/src/wafer_map/wm_constants.py b/src/wafer_map/wm_constants.py index afefaf6..4aefa82 100644 --- a/src/wafer_map/wm_constants.py +++ b/src/wafer_map/wm_constants.py @@ -1,50 +1,50 @@ -# -*- coding: utf-8 -*- -""" -Constants for the wafer_map package. -""" -from __future__ import absolute_import, division, print_function, unicode_literals -from enum import Enum - -import wx - - -# Colors -wm_OOR_HIGH_COLOR = wx.Colour(255, 0, 128, 255) -wm_OOR_LOW_COLOR = wx.Colour(255, 128, 0, 255) -# wm_HIGH_COLOR = wx.Colour(255, 0, 0, 255) -wm_HIGH_COLOR = wx.Colour(0, 255, 128, 255) -# wm_LOW_COLOR = wx.Colour(0, 192, 0, 255) -wm_LOW_COLOR = wx.Colour(128, 0, 255, 255) -wm_INVALID_COLOR = wx.Colour(255, 255, 255, 255) -wm_OUTLINE_COLOR = wx.Colour(255, 255, 0, 255) # yellow -wm_WAFER_EDGE_COLOR = wx.Colour(255, 0, 0, 255) # red -wm_WAFER_CENTER_DOT_COLOR = wx.Colour(255, 0, 0, 255) # red -wm_DIE_CENTER_DOT_COLOR = wx.Colour(255, 0, 0, 255) # red -wm_CROSSHAIR_COLOR = wx.Colour(0, 255, 255, 255) # cyan -wm_TICK_COUNT = 11 - -# Continuous Data Gradient sizez in px. -wm_GRAD_W = 30 -wm_GRAD_H = 500 -wm_TICK_W = 20 -wm_SPACER = 5 - -wm_ZOOM_FACTOR = 1.1 / 120 - -# Wafer Flat lengths, defined by SEMI M1-0302 -wm_FLAT_LENGTHS = {50: 15.88, 75: 22.22, 100: 32.5, 125: 42.5, 150: 57.5} - - -class NoValueEnum(Enum): - def __repr__(self): - return "<%s.%s>" % (self.__class__.__name__, self.name) - - -class DataType(NoValueEnum): - CONTINUOUS = "continuous" - DISCRETE = "discrete" - - -class CoordType(NoValueEnum): - ABSOLUTE = "absolute" - RELATIVE = "relative" +# -*- coding: utf-8 -*- +""" +Constants for the wafer_map package. +""" +from __future__ import absolute_import, division, print_function, unicode_literals +from enum import Enum + +import wx + + +# Colors +wm_OOR_HIGH_COLOR = wx.Colour(255, 0, 128, 255) +wm_OOR_LOW_COLOR = wx.Colour(255, 128, 0, 255) +# wm_HIGH_COLOR = wx.Colour(255, 0, 0, 255) +wm_HIGH_COLOR = wx.Colour(0, 255, 128, 255) +# wm_LOW_COLOR = wx.Colour(0, 192, 0, 255) +wm_LOW_COLOR = wx.Colour(128, 0, 255, 255) +wm_INVALID_COLOR = wx.Colour(255, 255, 255, 255) +wm_OUTLINE_COLOR = wx.Colour(255, 255, 0, 255) # yellow +wm_WAFER_EDGE_COLOR = wx.Colour(255, 0, 0, 255) # red +wm_WAFER_CENTER_DOT_COLOR = wx.Colour(255, 0, 0, 255) # red +wm_DIE_CENTER_DOT_COLOR = wx.Colour(255, 0, 0, 255) # red +wm_CROSSHAIR_COLOR = wx.Colour(0, 255, 255, 255) # cyan +wm_TICK_COUNT = 11 + +# Continuous Data Gradient sizez in px. +wm_GRAD_W = 30 +wm_GRAD_H = 500 +wm_TICK_W = 20 +wm_SPACER = 5 + +wm_ZOOM_FACTOR = 1.1 / 120 + +# Wafer Flat lengths, defined by SEMI M1-0302 +wm_FLAT_LENGTHS = {50: 15.88, 75: 22.22, 100: 32.5, 125: 42.5, 150: 57.5} + + +class NoValueEnum(Enum): + def __repr__(self): + return "<%s.%s>" % (self.__class__.__name__, self.name) + + +class DataType(NoValueEnum): + CONTINUOUS = "continuous" + DISCRETE = "discrete" + + +class CoordType(NoValueEnum): + ABSOLUTE = "absolute" + RELATIVE = "relative" diff --git a/src/wafer_map/wm_core.py b/src/wafer_map/wm_core.py index e0df9c6..e32c387 100644 --- a/src/wafer_map/wm_core.py +++ b/src/wafer_map/wm_core.py @@ -1,857 +1,857 @@ -# -*- coding: utf-8 -*- -""" -The core of ``wafer_map``. -""" -from __future__ import absolute_import, division, print_function, unicode_literals -import math - -import numpy as np -import wx -from wx.lib.floatcanvas import FloatCanvas -import wx.lib.colourselect as csel - -from wafer_map import wm_legend -from wafer_map import wm_utils -from wafer_map 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 = { - # Home - wx.WXK_HOME: self.zoom_fill, - # "O" - 79: self.toggle_outline, - # "C" - 67: self.toggle_crosshairs, - # "L" - 76: self.toggle_legend, - # "D" - 68: self.toggle_die_centers, - } - - # 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!") - - -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() +# -*- coding: utf-8 -*- +""" +The core of ``wafer_map``. +""" +from __future__ import absolute_import, division, print_function, unicode_literals +import math + +import numpy as np +import wx +from wx.lib.floatcanvas import FloatCanvas +import wx.lib.colourselect as csel + +from wafer_map import wm_legend +from wafer_map import wm_utils +from wafer_map 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 = { + # Home + wx.WXK_HOME: self.zoom_fill, + # "O" + 79: self.toggle_outline, + # "C" + 67: self.toggle_crosshairs, + # "L" + 76: self.toggle_legend, + # "D" + 68: self.toggle_die_centers, + } + + # 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!") + + +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() diff --git a/src/wafer_map/wm_frame.py b/src/wafer_map/wm_frame.py index 529cbc1..d6c2264 100644 --- a/src/wafer_map/wm_frame.py +++ b/src/wafer_map/wm_frame.py @@ -1,335 +1,335 @@ -# -*- coding: utf-8 -*- -""" -This is the main window of the Wafer Map application. -""" -from __future__ import absolute_import, division, print_function, unicode_literals - -import wx - -from . import wm_core -from . import wm_constants as wm_const - - -class WaferMapWindow(wx.Frame): - """ - This is the main window of the application. - - It contains the WaferMapPanel and the MenuBar. - - Although technically I don't need to have only 1 panel in the MainWindow, - I can have multiple panels. But I think I'll stick with this for now. - - Parameters - ---------- - title : str - The title to display. - xyd : list of 3-tuples - The data to plot. - wafer_info : :class:`wx_info.WaferInfo` - The wafer information. - size : tuple, optional - The windows size in ``(width, height)``. Values must be ``int``s. - Defaults to ``(800, 600)``. - data_type : wm_constants.DataType or string, optional - The type of data to plot. Must be one of `continuous` or `discrete`. - Defaults to `continuous`. - 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``. - show_die_gridlines : bool, optional - If ``True``, displayes gridlines along the die edges. Defaults to - ``True``. - """ - - def __init__( - self, - title, - xyd, - wafer_info, - size=(800, 600), - data_type=wm_const.DataType.CONTINUOUS, - high_color=wm_const.wm_HIGH_COLOR, - low_color=wm_const.wm_LOW_COLOR, - plot_range=None, - plot_die_centers=False, - show_die_gridlines=True, - ): - wx.Frame.__init__( - self, - None, - wx.ID_ANY, - title=title, - size=size, - ) - 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.high_color = high_color - self.low_color = low_color - self.plot_range = plot_range - self.plot_die_centers = plot_die_centers - self.show_die_gridlines = show_die_gridlines - self._init_ui() - - def _init_ui(self): - """Init the UI components.""" - # Create menu bar - self.menu_bar = wx.MenuBar() - - self._create_menus() - self._create_menu_items() - self._add_menu_items() - self._add_menus() - self._bind_events() - - # Initialize default states - self.mv_outline.Check() - self.mv_crosshairs.Check() - self.mv_diecenters.Check(self.plot_die_centers) - self.mv_legend.Check() - - # Set the MenuBar and create a status bar (easy thanks to wx.Frame) - self.SetMenuBar(self.menu_bar) - self.CreateStatusBar() - - # Allows this module to be run by itself if needed. - if __name__ == "__main__": - self.panel = None - else: - self.panel = wm_core.WaferMapPanel( - self, - self.xyd, - self.wafer_info, - data_type=self.data_type, - high_color=self.high_color, - low_color=self.low_color, - plot_range=self.plot_range, - plot_die_centers=self.plot_die_centers, - show_die_gridlines=self.show_die_gridlines, - ) - - # TODO: There's gotta be a more scalable way to make menu items - # and bind events... I'll run out of names if I have too many items. - # If I use numbers, as displayed in wxPython Demo, then things - # become confusing if I want to reorder things. - def _create_menus(self): - """Create each menu for the menu bar.""" - self.mfile = wx.Menu() - self.medit = wx.Menu() - self.mview = wx.Menu() - self.mopts = wx.Menu() - - def _create_menu_items(self): - """Create each item for each menu.""" - ### Menu: File (mf_) ### - # self.mf_new = wx.MenuItem(self.mfile, - # wx.ID_ANY, - # "&New\tCtrl+N", - # "TestItem") - # self.mf_open = wx.MenuItem(self.mfile, - # wx.ID_ANY, - # "&Open\tCtrl+O", - # "TestItem") - self.mf_close = wx.MenuItem( - self.mfile, - wx.ID_ANY, - "&Close\tCtrl+Q", - "TestItem", - ) - - ### Menu: Edit (me_) ### - self.me_redraw = wx.MenuItem( - self.medit, - wx.ID_ANY, - "&Redraw", - "Force Redraw", - ) - - ### Menu: View (mv_) ### - self.mv_zoomfit = wx.MenuItem( - self.mview, - wx.ID_ANY, - "Zoom &Fit\tHome", - "Zoom to fit", - ) - self.mv_crosshairs = wx.MenuItem( - self.mview, - wx.ID_ANY, - "Crosshairs\tC", - "Show or hide the crosshairs", - wx.ITEM_CHECK, - ) - self.mv_outline = wx.MenuItem( - self.mview, - wx.ID_ANY, - "Wafer Outline\tO", - "Show or hide the wafer outline", - wx.ITEM_CHECK, - ) - self.mv_diecenters = wx.MenuItem( - self.mview, - wx.ID_ANY, - "Die Centers\tD", - "Show or hide the die centers", - wx.ITEM_CHECK, - ) - self.mv_legend = wx.MenuItem( - self.mview, - wx.ID_ANY, - "Legend\tL", - "Show or hide the legend", - wx.ITEM_CHECK, - ) - - # Menu: Options (mo_) ### - self.mo_test = wx.MenuItem( - self.mopts, - wx.ID_ANY, - "&Test", - "Nothing", - ) - self.mo_high_color = wx.MenuItem( - self.mopts, - wx.ID_ANY, - "Set &High Color", - "Choose the color for high values", - ) - self.mo_low_color = wx.MenuItem( - self.mopts, - wx.ID_ANY, - "Set &Low Color", - "Choose the color for low values", - ) - - def _add_menu_items(self): - """Append MenuItems to each menu.""" - # self.mfile.Append(self.mf_new) - # self.mfile.Append(self.mf_open) - self.mfile.Append(self.mf_close) - - self.medit.Append(self.me_redraw) - # self.medit.Append(self.me_test1) - # self.medit.Append(self.me_test2) - - self.mview.Append(self.mv_zoomfit) - self.mview.AppendSeparator() - self.mview.Append(self.mv_crosshairs) - self.mview.Append(self.mv_outline) - self.mview.Append(self.mv_diecenters) - self.mview.Append(self.mv_legend) - - self.mopts.Append(self.mo_test) - self.mopts.Append(self.mo_high_color) - self.mopts.Append(self.mo_low_color) - - def _add_menus(self): - """Append each menu to the menu bar.""" - self.menu_bar.Append(self.mfile, "&File") - self.menu_bar.Append(self.medit, "&Edit") - self.menu_bar.Append(self.mview, "&View") - self.menu_bar.Append(self.mopts, "&Options") - - def _bind_events(self): - """Bind events to varoius MenuItems.""" - self.Bind(wx.EVT_MENU, self.on_quit, self.mf_close) - self.Bind(wx.EVT_MENU, self.on_zoom_fit, self.mv_zoomfit) - self.Bind(wx.EVT_MENU, self.on_toggle_crosshairs, self.mv_crosshairs) - self.Bind(wx.EVT_MENU, self.on_toggle_diecenters, self.mv_diecenters) - self.Bind(wx.EVT_MENU, self.on_toggle_outline, self.mv_outline) - self.Bind(wx.EVT_MENU, self.on_toggle_legend, self.mv_legend) - self.Bind(wx.EVT_MENU, self.on_change_high_color, self.mo_high_color) - self.Bind(wx.EVT_MENU, self.on_change_low_color, self.mo_low_color) - - # If I define an ID to the menu item, then I can use that instead of - # and event source: - # self.mo_test = wx.MenuItem(self.mopts, 402, "&Test", "Nothing") - # self.Bind(wx.EVT_MENU, self.on_zoom_fit, id=402) - - def on_quit(self, event): - """Action for the quit event.""" - self.Close(True) - - # TODO: I don't think I need a separate method for this - def on_zoom_fit(self, event): - """Call :meth:`wafer_map.wm_core.WaferMapPanel.zoom_fill()`.""" - print("Frame Event!") - self.panel.zoom_fill() - - # TODO: I don't think I need a separate method for this - def on_toggle_crosshairs(self, event): - """Call :meth:`wafer_map.wm_core.WaferMapPanel.toggle_crosshairs()`.""" - self.panel.toggle_crosshairs() - - # TODO: I don't think I need a separate method for this - def on_toggle_diecenters(self, event): - """Call :meth:`wafer_map.wm_core.WaferMapPanel.toggle_crosshairs()`.""" - self.panel.toggle_die_centers() - - # TODO: I don't think I need a separate method for this - def on_toggle_outline(self, event): - """Call the WaferMapPanel.toggle_outline() method.""" - self.panel.toggle_outline() - - # TODO: I don't think I need a separate method for this - # However if I don't use these then I have to - # 1) instance self.panel at the start of __init__ - # 2) make it so that self.panel.toggle_legend accepts arg: event - def on_toggle_legend(self, event): - """Call the WaferMapPanel.toggle_legend() method.""" - self.panel.toggle_legend() - - # TODO: See the 'and' in the docstring? Means I need a separate method! - def on_change_high_color(self, event): - """Change the high color and refresh display.""" - print("High color menu item clicked!") - cd = wx.ColourDialog(self) - cd.GetColourData().SetChooseFull(True) - - if cd.ShowModal() == wx.ID_OK: - new_color = cd.GetColourData().Colour - print("The color {} was chosen!".format(new_color)) - self.panel.on_color_change({"high": new_color, "low": None}) - self.panel.Refresh() - else: - print("no color chosen :-(") - cd.Destroy() - - # TODO: See the 'and' in the docstring? Means I need a separate method! - def on_change_low_color(self, event): - """Change the low color and refresh display.""" - print("Low Color menu item clicked!") - cd = wx.ColourDialog(self) - cd.GetColourData().SetChooseFull(True) - - if cd.ShowModal() == wx.ID_OK: - new_color = cd.GetColourData().Colour - print("The color {} was chosen!".format(new_color)) - self.panel.on_color_change({"high": None, "low": new_color}) - self.panel.Refresh() - else: - print("no color chosen :-(") - cd.Destroy() - - -def main(): - """Run when called as a module.""" - app = wx.App() - frame = WaferMapWindow("Testing", [], None) - frame.Show() - app.MainLoop() - - -if __name__ == "__main__": - main() +# -*- coding: utf-8 -*- +""" +This is the main window of the Wafer Map application. +""" +from __future__ import absolute_import, division, print_function, unicode_literals + +import wx + +from . import wm_core +from . import wm_constants as wm_const + + +class WaferMapWindow(wx.Frame): + """ + This is the main window of the application. + + It contains the WaferMapPanel and the MenuBar. + + Although technically I don't need to have only 1 panel in the MainWindow, + I can have multiple panels. But I think I'll stick with this for now. + + Parameters + ---------- + title : str + The title to display. + xyd : list of 3-tuples + The data to plot. + wafer_info : :class:`wx_info.WaferInfo` + The wafer information. + size : tuple, optional + The windows size in ``(width, height)``. Values must be ``int``s. + Defaults to ``(800, 600)``. + data_type : wm_constants.DataType or string, optional + The type of data to plot. Must be one of `continuous` or `discrete`. + Defaults to `continuous`. + 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``. + show_die_gridlines : bool, optional + If ``True``, displayes gridlines along the die edges. Defaults to + ``True``. + """ + + def __init__( + self, + title, + xyd, + wafer_info, + size=(800, 600), + data_type=wm_const.DataType.CONTINUOUS, + high_color=wm_const.wm_HIGH_COLOR, + low_color=wm_const.wm_LOW_COLOR, + plot_range=None, + plot_die_centers=False, + show_die_gridlines=True, + ): + wx.Frame.__init__( + self, + None, + wx.ID_ANY, + title=title, + size=size, + ) + 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.high_color = high_color + self.low_color = low_color + self.plot_range = plot_range + self.plot_die_centers = plot_die_centers + self.show_die_gridlines = show_die_gridlines + self._init_ui() + + def _init_ui(self): + """Init the UI components.""" + # Create menu bar + self.menu_bar = wx.MenuBar() + + self._create_menus() + self._create_menu_items() + self._add_menu_items() + self._add_menus() + self._bind_events() + + # Initialize default states + self.mv_outline.Check() + self.mv_crosshairs.Check() + self.mv_diecenters.Check(self.plot_die_centers) + self.mv_legend.Check() + + # Set the MenuBar and create a status bar (easy thanks to wx.Frame) + self.SetMenuBar(self.menu_bar) + self.CreateStatusBar() + + # Allows this module to be run by itself if needed. + if __name__ == "__main__": + self.panel = None + else: + self.panel = wm_core.WaferMapPanel( + self, + self.xyd, + self.wafer_info, + data_type=self.data_type, + high_color=self.high_color, + low_color=self.low_color, + plot_range=self.plot_range, + plot_die_centers=self.plot_die_centers, + show_die_gridlines=self.show_die_gridlines, + ) + + # TODO: There's gotta be a more scalable way to make menu items + # and bind events... I'll run out of names if I have too many items. + # If I use numbers, as displayed in wxPython Demo, then things + # become confusing if I want to reorder things. + def _create_menus(self): + """Create each menu for the menu bar.""" + self.mfile = wx.Menu() + self.medit = wx.Menu() + self.mview = wx.Menu() + self.mopts = wx.Menu() + + def _create_menu_items(self): + """Create each item for each menu.""" + ### Menu: File (mf_) ### + # self.mf_new = wx.MenuItem(self.mfile, + # wx.ID_ANY, + # "&New\tCtrl+N", + # "TestItem") + # self.mf_open = wx.MenuItem(self.mfile, + # wx.ID_ANY, + # "&Open\tCtrl+O", + # "TestItem") + self.mf_close = wx.MenuItem( + self.mfile, + wx.ID_ANY, + "&Close\tCtrl+Q", + "TestItem", + ) + + ### Menu: Edit (me_) ### + self.me_redraw = wx.MenuItem( + self.medit, + wx.ID_ANY, + "&Redraw", + "Force Redraw", + ) + + ### Menu: View (mv_) ### + self.mv_zoomfit = wx.MenuItem( + self.mview, + wx.ID_ANY, + "Zoom &Fit\tHome", + "Zoom to fit", + ) + self.mv_crosshairs = wx.MenuItem( + self.mview, + wx.ID_ANY, + "Crosshairs\tC", + "Show or hide the crosshairs", + wx.ITEM_CHECK, + ) + self.mv_outline = wx.MenuItem( + self.mview, + wx.ID_ANY, + "Wafer Outline\tO", + "Show or hide the wafer outline", + wx.ITEM_CHECK, + ) + self.mv_diecenters = wx.MenuItem( + self.mview, + wx.ID_ANY, + "Die Centers\tD", + "Show or hide the die centers", + wx.ITEM_CHECK, + ) + self.mv_legend = wx.MenuItem( + self.mview, + wx.ID_ANY, + "Legend\tL", + "Show or hide the legend", + wx.ITEM_CHECK, + ) + + # Menu: Options (mo_) ### + self.mo_test = wx.MenuItem( + self.mopts, + wx.ID_ANY, + "&Test", + "Nothing", + ) + self.mo_high_color = wx.MenuItem( + self.mopts, + wx.ID_ANY, + "Set &High Color", + "Choose the color for high values", + ) + self.mo_low_color = wx.MenuItem( + self.mopts, + wx.ID_ANY, + "Set &Low Color", + "Choose the color for low values", + ) + + def _add_menu_items(self): + """Append MenuItems to each menu.""" + # self.mfile.Append(self.mf_new) + # self.mfile.Append(self.mf_open) + self.mfile.Append(self.mf_close) + + self.medit.Append(self.me_redraw) + # self.medit.Append(self.me_test1) + # self.medit.Append(self.me_test2) + + self.mview.Append(self.mv_zoomfit) + self.mview.AppendSeparator() + self.mview.Append(self.mv_crosshairs) + self.mview.Append(self.mv_outline) + self.mview.Append(self.mv_diecenters) + self.mview.Append(self.mv_legend) + + self.mopts.Append(self.mo_test) + self.mopts.Append(self.mo_high_color) + self.mopts.Append(self.mo_low_color) + + def _add_menus(self): + """Append each menu to the menu bar.""" + self.menu_bar.Append(self.mfile, "&File") + self.menu_bar.Append(self.medit, "&Edit") + self.menu_bar.Append(self.mview, "&View") + self.menu_bar.Append(self.mopts, "&Options") + + def _bind_events(self): + """Bind events to varoius MenuItems.""" + self.Bind(wx.EVT_MENU, self.on_quit, self.mf_close) + self.Bind(wx.EVT_MENU, self.on_zoom_fit, self.mv_zoomfit) + self.Bind(wx.EVT_MENU, self.on_toggle_crosshairs, self.mv_crosshairs) + self.Bind(wx.EVT_MENU, self.on_toggle_diecenters, self.mv_diecenters) + self.Bind(wx.EVT_MENU, self.on_toggle_outline, self.mv_outline) + self.Bind(wx.EVT_MENU, self.on_toggle_legend, self.mv_legend) + self.Bind(wx.EVT_MENU, self.on_change_high_color, self.mo_high_color) + self.Bind(wx.EVT_MENU, self.on_change_low_color, self.mo_low_color) + + # If I define an ID to the menu item, then I can use that instead of + # and event source: + # self.mo_test = wx.MenuItem(self.mopts, 402, "&Test", "Nothing") + # self.Bind(wx.EVT_MENU, self.on_zoom_fit, id=402) + + def on_quit(self, event): + """Action for the quit event.""" + self.Close(True) + + # TODO: I don't think I need a separate method for this + def on_zoom_fit(self, event): + """Call :meth:`wafer_map.wm_core.WaferMapPanel.zoom_fill()`.""" + print("Frame Event!") + self.panel.zoom_fill() + + # TODO: I don't think I need a separate method for this + def on_toggle_crosshairs(self, event): + """Call :meth:`wafer_map.wm_core.WaferMapPanel.toggle_crosshairs()`.""" + self.panel.toggle_crosshairs() + + # TODO: I don't think I need a separate method for this + def on_toggle_diecenters(self, event): + """Call :meth:`wafer_map.wm_core.WaferMapPanel.toggle_crosshairs()`.""" + self.panel.toggle_die_centers() + + # TODO: I don't think I need a separate method for this + def on_toggle_outline(self, event): + """Call the WaferMapPanel.toggle_outline() method.""" + self.panel.toggle_outline() + + # TODO: I don't think I need a separate method for this + # However if I don't use these then I have to + # 1) instance self.panel at the start of __init__ + # 2) make it so that self.panel.toggle_legend accepts arg: event + def on_toggle_legend(self, event): + """Call the WaferMapPanel.toggle_legend() method.""" + self.panel.toggle_legend() + + # TODO: See the 'and' in the docstring? Means I need a separate method! + def on_change_high_color(self, event): + """Change the high color and refresh display.""" + print("High color menu item clicked!") + cd = wx.ColourDialog(self) + cd.GetColourData().SetChooseFull(True) + + if cd.ShowModal() == wx.ID_OK: + new_color = cd.GetColourData().Colour + print("The color {} was chosen!".format(new_color)) + self.panel.on_color_change({"high": new_color, "low": None}) + self.panel.Refresh() + else: + print("no color chosen :-(") + cd.Destroy() + + # TODO: See the 'and' in the docstring? Means I need a separate method! + def on_change_low_color(self, event): + """Change the low color and refresh display.""" + print("Low Color menu item clicked!") + cd = wx.ColourDialog(self) + cd.GetColourData().SetChooseFull(True) + + if cd.ShowModal() == wx.ID_OK: + new_color = cd.GetColourData().Colour + print("The color {} was chosen!".format(new_color)) + self.panel.on_color_change({"high": None, "low": new_color}) + self.panel.Refresh() + else: + print("no color chosen :-(") + cd.Destroy() + + +def main(): + """Run when called as a module.""" + app = wx.App() + frame = WaferMapWindow("Testing", [], None) + frame.Show() + app.MainLoop() + + +if __name__ == "__main__": + main() diff --git a/src/wafer_map/wm_info.py b/src/wafer_map/wm_info.py index 6b5602b..4573087 100644 --- a/src/wafer_map/wm_info.py +++ b/src/wafer_map/wm_info.py @@ -1,57 +1,57 @@ -# -*- coding: utf-8 -*- -""" -The :class:`wafer_map.wm_info.WaferInfo` class. -""" - - -class WaferInfo(object): - """ - Contains the wafer information. - - Parameters - ---------- - die_size : tuple - The die size in mm as a ``(width, height)`` tuple. - center_xy : tuple - The center grid coordinate as a ``(x_grid, y_grid)`` tuple. - dia : float, optional - The wafer diameter in mm. Defaults to `150`. - edge_excl : float, optional - The distance in mm from the edge of the wafer that should be - considered bad die. Defaults to 5mm. - flat_excl : float, optional - The distance in mm from the wafer flat that should be - considered bad die. Defaults to 5mm. - """ - - def __init__(self, die_size, center_xy, dia=150, edge_excl=5, flat_excl=5): - self.die_size = die_size - self.center_xy = center_xy - self.dia = dia - self.edge_excl = edge_excl - self.flat_excl = flat_excl - - def __str__(self): - string = """ -Wafer Dia: {}mm -Die Size: {} -Grid Center XY: {} -Edge Excl: {} -Flat Excl: {} -""" - return string.format( - self.dia, - self.die_size, - self.center_xy, - self.edge_excl, - self.flat_excl, - ) - - -def main(): - """Run when called as a module.""" - raise RuntimeError("This module is not meant to be run by itself.") - - -if __name__ == "__main__": - main() +# -*- coding: utf-8 -*- +""" +The :class:`wafer_map.wm_info.WaferInfo` class. +""" + + +class WaferInfo(object): + """ + Contains the wafer information. + + Parameters + ---------- + die_size : tuple + The die size in mm as a ``(width, height)`` tuple. + center_xy : tuple + The center grid coordinate as a ``(x_grid, y_grid)`` tuple. + dia : float, optional + The wafer diameter in mm. Defaults to `150`. + edge_excl : float, optional + The distance in mm from the edge of the wafer that should be + considered bad die. Defaults to 5mm. + flat_excl : float, optional + The distance in mm from the wafer flat that should be + considered bad die. Defaults to 5mm. + """ + + def __init__(self, die_size, center_xy, dia=150, edge_excl=5, flat_excl=5): + self.die_size = die_size + self.center_xy = center_xy + self.dia = dia + self.edge_excl = edge_excl + self.flat_excl = flat_excl + + def __str__(self): + string = """ +Wafer Dia: {}mm +Die Size: {} +Grid Center XY: {} +Edge Excl: {} +Flat Excl: {} +""" + return string.format( + self.dia, + self.die_size, + self.center_xy, + self.edge_excl, + self.flat_excl, + ) + + +def main(): + """Run when called as a module.""" + raise RuntimeError("This module is not meant to be run by itself.") + + +if __name__ == "__main__": + main() diff --git a/src/wafer_map/wm_legend.py b/src/wafer_map/wm_legend.py index ae4f205..2927692 100644 --- a/src/wafer_map/wm_legend.py +++ b/src/wafer_map/wm_legend.py @@ -1,675 +1,675 @@ -# -*- coding: utf-8 -*- -""" -Draws the wafer map legend. -""" -from __future__ import absolute_import, division, print_function, unicode_literals -import colorsys -from collections import OrderedDict - -import wx -from wx.lib.floatcanvas import FloatCanvas -import wx.lib.colourselect as csel - -from wafer_map import PY2 -from wafer_map import wm_utils -from wafer_map import wm_constants as wm_const - -# TODO: Update to Bezier Curves for colors. See http://bsou.io/p/3 - -# Python2 Compatibility -if PY2: - range = xrange - - -class Legend(object): - """ - Base class for both discrete and continuous legends. - - Not currently used. Not even sure if I will use it. - """ - - pass - - -class ContinuousLegend(wx.Panel): - """ - Legend for continuous values. - - Creates a color scale for plotting. The scale fills all available - vertical space. By default, 11 ticks are printed. - - Parameters - ---------- - parent : wxWindow - The parent window that the legend belongs to. - plot_range : tuple, length 2 - The plot range that the legend should cover: (min, max). Anything - outside this range will be plotted using different colors. - high_color : wxColour (wm_HIGH_COLOR) - The color that values closer to +inf should be. - low_color : wxColour (wm_LOW_COLOR) - The color that values closer to -inf should be. - num_ticks : int (wm_TICK_COUNT) - How many ticks should be plotted. Minimum of 2. - oor_high_color : wxColour (wm_OOR_HIGH_COLOR) - This is the color that should be used for any values that are - greater than plot_range[1]. - oor_low_color : wxColour (wm_OOR_LOW_COLOR) - This is the color that should be used for any values that are - lesser than plot_range[0]. - - Bound Events - ------------ - EVT_PAINT: - Copy MemoryDC buffer to screen. - EVT_SIZE: - Makes the scale fit to the resized window - EVT_LEFT_DOWN: - Used for debugging. Prints the pixel and color of the mouse click. - Uses the GetPixelPoint() method to grab the color. - EVT_RIGHT_DOWN: - Used for debugging. Prints the pixel and color of the mouse - click. Uses the get_color() method - - Logic Overview - -------------- - 1. Create a wx.MemoryDC and initialize it with an empty bitmap that's - the size of the window. - 2. Draw items to the MemoryDC. - 3. On paint, Blit (copy) the MemoryDC to a PaintDC. At the end of the - paint event handler, the PaintDC is destroyed and it's contents - are displayed on the screen. - 4. Any time an outside function needs to get a color for a value, - we access the Gradient class (currently found in wm_utils) - this - class calculates the color directly. - - + Previously, I'd get the color by figuring out which pixel the - value would be on and then get the pixel's color. - + This has issues because it limits things like dynamic color - changing or multi-color scales. - - 5. ??? - 6. Profit. - """ - - def __init__( - self, - parent, - plot_range, - high_color=wm_const.wm_HIGH_COLOR, - low_color=wm_const.wm_LOW_COLOR, - num_ticks=wm_const.wm_TICK_COUNT, - oor_high_color=wm_const.wm_OOR_HIGH_COLOR, - oor_low_color=wm_const.wm_OOR_LOW_COLOR, - ): - wx.Panel.__init__(self, parent) - - ### Inputs ########################################################## - self.parent = parent - self.plot_range = plot_range - self.high_color = high_color - self.low_color = low_color - self.num_ticks = num_ticks - self.oor_high_color = oor_high_color - self.oor_low_color = oor_low_color - self.invalid_color = wm_const.wm_INVALID_COLOR - - ### Initialize Size Attributes ###################################### - # These get set in set_sizes(), but are here to remind me that - # the instance attribute exists and what they are. - # Values are in px. - # fmt: off - self.text_h = None # text height - self.text_w = None # Length of longest tick label - self.grad_w = None # gradient width - self.grad_h = None # gradient height - self.spacer = None # spacer speration between items - self.grad_start_y = None # top of gradient pixel coord - self.grad_end_y = None # bottom of gradient pixel coord - self.grad_start_x = None # gradient left pixel coord - self.grad_end_x = None # gradient right pixel coord - self.tick_w = None # tick mark length - self.tick_start_x = None # tick label left pixel coord - self.dc_w = None # total bitmap width - self.dc_h = None # total bitmap height - # fmt: on - - ### Other Instance Attributes ####################################### - self.ticks = None - self.gradient = wm_utils.LinearGradient(self.low_color, self.high_color) - - ### Remainder of __init__ ########################################### - # Create the MemoryDC now - we'll add the bitmap later. - self.mdc = wx.MemoryDC() - self.mdc.SetFont( - wx.Font( - 9, - wx.FONTFAMILY_SWISS, - wx.FONTSTYLE_NORMAL, - wx.FONTWEIGHT_NORMAL, - ) - ) - - self.set_sizes() - - # Create EmptyBitmap in our MemoryDC where we'll do all our drawing. - # self.mdc.SelectObject(wx.EmptyBitmap(self.dc_w, self.dc_h)) - self.mdc.SelectObject(wx.Bitmap(self.dc_w, self.dc_h)) - - # Draw the entire thing - self.draw_scale() - - # Bind various events - self._bind_events() - - self._init_ui() - - def _init_ui(self): - """Add a Sizer that is the same size as the MemoryDC.""" - self.hbox = wx.BoxSizer(wx.HORIZONTAL) - self.hbox.Add((self.dc_w, self.dc_h)) - self.SetSizer(self.hbox) - - def _bind_events(self): - """Bind events to various event handlers.""" - self.Bind(wx.EVT_PAINT, self._on_paint) - self.Bind(wx.EVT_SIZE, self._on_size) - # self.Bind(wx.EVT_MOTION, self.on_mouse_move) - # self.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_left_down) - # self.Bind(wx.EVT_RIGHT_DOWN, self.on_mouse_right_down) - - def get_color(self, value): - """Get a color from the gradient.""" - # TODO: determine how wxPython's GradientFillLinear works and use that - # instead of grabbing the color from the gradient. - if value > self.plot_range[1]: - color = self.oor_high_color - elif value < self.plot_range[0]: - color = self.oor_low_color - else: - try: - # pxl = int(wm_utils.rescale(value, - # self.plot_range, - # (self.grad_end_y - 1, - # self.grad_start_y))) - # - # x_pt = self.grad_w // 2 + self.grad_start_x - # point = (x_pt, pxl) - # color = self.mdc.GetPixelPoint(point) - - # New Method - pxl = wm_utils.rescale(value, self.plot_range, (0, 1)) - color = self.gradient.get_color(pxl) - color = wx.Colour(*color) - except ValueError: - color = self.invalid_color - return color - - def calc_ticks(self): - """ - Calculate the tick marks' display string, value, and pixel value. - - High values are at the top of the scale, low at the bottom. - """ - # First we need to determine the values for the ticks. - pr = self.plot_range[1] - self.plot_range[0] - spacing = pr / (self.num_ticks - 1) - - tick_values = wm_utils.frange( - self.plot_range[0], self.plot_range[1] + 1, spacing - ) - - ticks = [] - for tick in tick_values: - string = "{:.3f}".format(tick) - value = tick - # `grad_end_y - 1` so that the bottom tick is aligned correctly. - pixel = wm_utils.rescale( - tick, self.plot_range, (self.grad_end_y - 1, self.grad_start_y) - ) - # Putting gradient_end_y as the "low" for rescale makes the - # high value be at the north end and the low value at the south. - ticks.append((string, value, pixel)) - - return ticks - - def draw_ticks(self, ticks): - """ - Print the tickmarks. - - Parameters - ---------- - ticks : list of (string, value, pixel) tuples - """ - pen = wx.Pen(wx.BLACK) - self.mdc.SetPen(pen) - text_w = max([self.mdc.GetTextExtent(_i[0])[0] for _i in ticks]) - for tick in ticks: - # Sorry, everything is measured from right to left... - tick_end = self.grad_start_x - self.spacer - tick_start = tick_end - self.tick_w - self.mdc.DrawLine(tick_start, tick[2], tick_end, tick[2]) - - # Text origin is top left of bounding box. - # Text is currently left-aligned. Maybe Change? - text_x = tick_start - self.spacer - text_w - text_y = tick[2] - self.text_h / 2 - self.mdc.DrawText(tick[0], text_x, text_y) - - def set_sizes(self): - """ - Set various instance attributes for item sizes and locations. - - Uses the current client size and Text Height to set set some items. - """ - # These are in a specific order. Do not change! - # First determine some fixed items - self.text_h = self.mdc.GetTextExtent("A")[1] - self.grad_w = 30 - self.grad_h = self.parent.GetClientSize()[1] - self.text_h - self.tick_w = 20 - self.spacer = 5 - self.grad_start_y = self.text_h / 2 - self.grad_end_y = self.grad_start_y + self.grad_h - - # Now that the widths are defined, I can calculate some other things - self.ticks = self.calc_ticks() - self.text_w = self.get_max_text_w(self.ticks) - - # Note: I'm intentionally listing every spacer manually rather than - # multiplying by how many there are. This makes it easier - # to see where I'm placing them. - self.tick_start_x = self.spacer + self.text_w + self.spacer - self.grad_start_x = self.tick_start_x + self.tick_w + self.spacer - self.grad_end_x = self.grad_start_x + self.grad_w - self.dc_w = self.grad_end_x + self.spacer # total bitmap width - self.dc_h = self.grad_h + self.text_h # total bitmap height - - def draw_background(self): - """ - Draw the background box. - - If I don't do this, then the background is black. - - Could I change wx.EmptyBitmap() so that it defaults to white rather - than black? - """ - # TODO: change the bitmap background to be transparent - c = wx.Colour(200, 230, 230, 0) - c = wx.Colour(255, 255, 255, 0) - pen = wx.Pen(c) - brush = wx.Brush(c) - self.mdc.SetPen(pen) - self.mdc.SetBrush(brush) - self.mdc.DrawRectangle(0, 0, self.dc_w, self.dc_h) - - def draw_scale(self): - """ - Draw the entire scale area. - - The scale area is: background, gradient, OOR colors, ticks, - and labels. - """ - self.draw_background() - - # Draw the Gradient on a portion of the MemoryDC - self.draw_gradient() - - # Draw the out-of-range high and low rectangles - c = self.oor_high_color - pen = wx.Pen(c) - brush = wx.Brush(c) - self.mdc.SetPen(pen) - self.mdc.SetBrush(brush) - self.mdc.DrawRectangle(self.grad_start_x, 2, self.grad_w, self.grad_start_y - 2) - - c = self.oor_low_color - pen = wx.Pen(c) - brush = wx.Brush(c) - self.mdc.SetPen(pen) - self.mdc.SetBrush(brush) - self.mdc.DrawRectangle( - self.grad_start_x, - self.grad_end_y, - self.grad_w, - self.dc_h - self.grad_end_y - 2, - ) - - # Calculate and draw the tickmarks. - self.draw_ticks(self.ticks) - - def draw_gradient(self): - """Draw the Gradient, painted from North (high) to South (low).""" - # self.mdc.GradientFillLinear((self.grad_start_x, self.grad_start_y, - # self.grad_w, self.grad_h), - # self.high_color, - # self.low_color, - # wx.SOUTH, - # ) - - # Remake of the wx.DC.GradientFillLinear wx core function, but uses - # my own algorithm for determining the colors. - # Doing so ensures that both the gradient and the die colors match. - - # Save the old pen colors - old_pen = self.mdc.GetPen() - old_brush = self.mdc.GetBrush() - delta = self.grad_h / 255 # height of one shade box - if delta < 1: - delta = 1 # max of 255 pts - fractional colors not defined. - - y = self.grad_start_y - while y <= self.grad_end_y: - val = wm_utils.rescale(y, (self.grad_start_y, self.grad_end_y), (1, 0)) - color = self.gradient.get_color(val) - self.mdc.SetPen(wx.Pen(color)) - self.mdc.SetBrush(wx.Brush(color)) - self.mdc.DrawRectangle(self.grad_start_x, y, self.grad_w, delta + 1) - y += delta - - # Set the pen and brush back to what they were - self.mdc.SetPen(old_pen) - self.mdc.SetBrush(old_brush) - - def get_max_text_w(self, ticks): - """ - Get the maximum label sizes. - - There's probably a better way... - """ - return max([self.mdc.GetTextExtent(i[0])[0] for i in ticks]) - - ### #-------------------------------------------------------------------- - ### Events - ### #-------------------------------------------------------------------- - - def _on_size(self, event): - """Redraw everything with the new sizes.""" - # TODO: Also reduce number of ticks when text starts to overlap - # or add ticks when there's extra space. - self.set_sizes() - self.hbox.Remove(0) - self.hbox.Add((self.dc_w, self.dc_h)) - # self.mdc.SelectObject(wx.EmptyBitmap(self.dc_w, self.dc_h)) - self.mdc.SelectObject(wx.Bitmap(self.dc_w, self.dc_h)) - self.draw_scale() - self.Refresh() - - def _on_paint(self, event): - """Push the MemoryDC bitmap to the displayed PaintDC.""" - dc = wx.PaintDC(self) - dc.Blit(0, 0, self.dc_w, self.dc_h, self.mdc, 0, 0) - - def on_color_change(self, event): - """ - Change the plot colors. - - This is done by updating self.gradient and calling self.draw_scale() - """ - if event["low"] is not None: - self.low_color = event["low"] - if event["high"] is not None: - self.high_color = event["high"] - self.gradient = wm_utils.LinearGradient(self.low_color, self.high_color) - - # self._clear_scale() - self.hbox.Remove(0) - self.hbox.Add((self.dc_w, self.dc_h)) - # self.mdc.SelectObject(wx.EmptyBitmap(self.dc_w, self.dc_h)) - self.mdc.SelectObject(wx.Bitmap(self.dc_w, self.dc_h)) - - self.draw_scale() - - def on_scale_change(self, event): - """Redraw things on scale change.""" - self.gradient = wm_utils.LinearGradient(self.low_color, self.high_color) - - self.hbox.Remove(0) - self.hbox.Add((self.dc_w, self.dc_h)) - # self.mdc.SelectObject(wx.EmptyBitmap(self.dc_w, self.dc_h)) - self.mdc.SelectObject(wx.Bitmap(self.dc_w, self.dc_h)) - - self.draw_scale() - - def on_mouse_move(self, event): - """Used for debugging.""" - pt = self.mdc.GetPixelPoint(event.GetPosition()) - print(pt) - - def on_mouse_left_down(self, event): - """Used for debugging.""" - print("Left-click - color from self.mdc.GetPixelPoint.") - pos = event.GetPosition() - w, h = self.mdc.GetSize() # change to gradient area - if pos[0] < w and pos[1] < h: - val = wm_utils.rescale( - pos[1], - (self.grad_start_y, self.grad_end_y - 1), - reversed(self.plot_range), - ) - a = self.mdc.GetPixelPoint(event.GetPosition()) - print("{}\t{}\t{}".format(pos, a, val)) - - def on_mouse_right_down(self, event): - """Used for debugging.""" - print("Right-click - color from get_color()") - pos = event.GetPosition() - w, h = self.mdc.GetSize() # change to gradient area - if pos[0] < w and pos[1] < h: - val = wm_utils.rescale( - pos[1], - (self.grad_start_y, self.grad_end_y - 1), - reversed(self.plot_range), - ) - a = self.get_color(val) - print("{}\t{}\t{}".format(pos, a, val)) - - def on_mouse_wheel(self, event): - print("mouse wheel!") - # self.on_mouse_left_down(event) - - -class DiscreteLegend(wx.Panel): - """ - Legend for discrete values. - - Is basically a 2D table of label-color rows. The colors are actually - buttons that can be clicked on - a color picker appears. However, as of - 2014-12-03, changing the color does not do anything. - - Parameters - ---------- - parent : wx.Panel - labels : list - colors : list, optional - """ - - def __init__( - self, - parent, - labels, - colors=None, - ): - wx.Panel.__init__(self, parent) - self.parent = parent - self.labels = labels - self.n_items = len(self.labels) - if colors is None: - self.colors = self.create_colors(self.n_items) - else: - self.colors = colors - self.create_color_dict() - - self._init_ui() - - def _init_ui(self): - """Initialize UI components.""" - # Add layout management - self.hbox = wx.BoxSizer(wx.HORIZONTAL) - self.fgs = wx.FlexGridSizer(rows=self.n_items, cols=2, vgap=0, hgap=2) - - # Create items to add - for _i, (key, value) in enumerate(zip(self.labels, self.colors)): - self.label = wx.StaticText( - self, - label=str(key), - style=wx.ALIGN_LEFT, - ) - - self.colorbox = csel.ColourSelect( - self, _i, "", tuple(value), style=wx.NO_BORDER, size=(20, 20) - ) - - self.Bind(csel.EVT_COLOURSELECT, self.on_color_pick, id=_i) - - self.fgs.Add(self.label, flag=wx.ALIGN_CENTER_VERTICAL) - self.fgs.Add(self.colorbox) - - # Add our items to the layout manager and set the sizer. - self.hbox.Add(self.fgs) - self.SetSizer(self.hbox) - - @staticmethod - def create_colors(n): - """ - Create the colors based on how many legend items there are (n). - - The idea is to start off with one color, assign it to the 1st legend - value, then find that color's complement and assign it to the 2nd - legend value. Then, move around the color wheel by some degree, - probably like so: - - - - We are limited to only using 1/2 of the circle - because we use the other half for the complements. - - 1. Split the circle into n parts. - 2. reorganize into alternations - 1 2 3 4 5 6 7 8 --> - 1 3 5 7 2 4 6 8 - """ - spacing = 360 / n - colors = [] - for val in wm_utils.frange(0, 360, spacing): - hsl = (val / 360, 1, 0.75) - colors.append(colorsys.hsv_to_rgb(*hsl)) - - # convert from 0-1 to 0-255 and return - colors = [tuple(int(i * 255) for i in color) for color in colors] - - # Alternate colors across the circle - colors = colors[::2] + colors[1::2] - return colors - - def create_color_dict(self): - """ - Take the value and color lists and creates a dict from them. - - This may eventually become a public function with two inputs: - lables, colors. - """ - # TODO: Determine if I want this to be a public callable method - self.color_dict = OrderedDict(zip(self.labels, self.colors)) - return self.color_dict - - def on_color_pick(self, event): - """Recreate the {label: color} dict and send to the parent.""" - # print(event.GetId()) - self.colors[event.GetId()] = event.GetValue().Get() - self.create_color_dict() - # Send the event to the parent: - wx.PostEvent(self.parent, event) - - -class LegendOverlay(FloatCanvas.Text): - """Demo of drawing overlay - to be used for legend.""" - - def __init__( - self, - String, - xy, - Size=24, - Color="Black", - BackgroundColor=None, - Family=wx.MODERN, - Style=wx.NORMAL, - Weight=wx.NORMAL, - Underlined=False, - Font=None, - ): - FloatCanvas.Text.__init__( - self, - String, - xy, - Size=Size, - Color=Color, - BackgroundColor=BackgroundColor, - Family=Family, - Style=Style, - Weight=Weight, - Underlined=Underlined, - Font=Font, - ) - - def _Draw(self, dc, Canvas): - """ - _Draw method for Overlay. - - .. note:: - This is a differeent signarture than the DrawObject Draw - """ - dc.SetFont(self.Font) - dc.SetTextForeground(self.Color) - if self.BackgroundColor: - dc.SetBackgroundMode(wx.SOLID) - dc.SetTextBackground(self.BackgroundColor) - else: - dc.SetBackgroundMode(wx.TRANSPARENT) - dc.DrawTextPoint(self.String, self.XY) - - -def main(): - """Display the Legend when module is run directly.""" - legend_labels = ["A", "Banana!", "C", "Donut", "E"] - # legend_labels = [str(_i) for _i in range(10)] - - legend_colors = None - - continuous_range = (10, 50) - - class ExampleFrame(wx.Frame): - """Base Frame.""" - - def __init__(self, title): - wx.Frame.__init__( - self, - None, # Window Parent - wx.ID_ANY, # id - title=title, # Window Title - size=(300 + 16, 550 + 38), # Size in px - ) - - self.Bind(wx.EVT_CLOSE, self.OnQuit) - - # Here's where we call the WaferMapPanel - self.hbox = wx.BoxSizer(wx.HORIZONTAL) - - self.d_legend = DiscreteLegend(self, legend_labels, legend_colors) - self.c_legend = ContinuousLegend(self, continuous_range) - - self.hbox.Add(self.d_legend, 0) - self.hbox.Add(self.c_legend, 0) - self.SetSizer(self.hbox) - - def OnQuit(self, event): - self.Destroy() - - app = wx.App() - frame = ExampleFrame("Legend Example") - frame.Show() - app.MainLoop() - - -if __name__ == "__main__": - main() +# -*- coding: utf-8 -*- +""" +Draws the wafer map legend. +""" +from __future__ import absolute_import, division, print_function, unicode_literals +import colorsys +from collections import OrderedDict + +import wx +from wx.lib.floatcanvas import FloatCanvas +import wx.lib.colourselect as csel + +from wafer_map import PY2 +from wafer_map import wm_utils +from wafer_map import wm_constants as wm_const + +# TODO: Update to Bezier Curves for colors. See http://bsou.io/p/3 + +# Python2 Compatibility +if PY2: + range = xrange + + +class Legend(object): + """ + Base class for both discrete and continuous legends. + + Not currently used. Not even sure if I will use it. + """ + + pass + + +class ContinuousLegend(wx.Panel): + """ + Legend for continuous values. + + Creates a color scale for plotting. The scale fills all available + vertical space. By default, 11 ticks are printed. + + Parameters + ---------- + parent : wxWindow + The parent window that the legend belongs to. + plot_range : tuple, length 2 + The plot range that the legend should cover: (min, max). Anything + outside this range will be plotted using different colors. + high_color : wxColour (wm_HIGH_COLOR) + The color that values closer to +inf should be. + low_color : wxColour (wm_LOW_COLOR) + The color that values closer to -inf should be. + num_ticks : int (wm_TICK_COUNT) + How many ticks should be plotted. Minimum of 2. + oor_high_color : wxColour (wm_OOR_HIGH_COLOR) + This is the color that should be used for any values that are + greater than plot_range[1]. + oor_low_color : wxColour (wm_OOR_LOW_COLOR) + This is the color that should be used for any values that are + lesser than plot_range[0]. + + Bound Events + ------------ + EVT_PAINT: + Copy MemoryDC buffer to screen. + EVT_SIZE: + Makes the scale fit to the resized window + EVT_LEFT_DOWN: + Used for debugging. Prints the pixel and color of the mouse click. + Uses the GetPixelPoint() method to grab the color. + EVT_RIGHT_DOWN: + Used for debugging. Prints the pixel and color of the mouse + click. Uses the get_color() method + + Logic Overview + -------------- + 1. Create a wx.MemoryDC and initialize it with an empty bitmap that's + the size of the window. + 2. Draw items to the MemoryDC. + 3. On paint, Blit (copy) the MemoryDC to a PaintDC. At the end of the + paint event handler, the PaintDC is destroyed and it's contents + are displayed on the screen. + 4. Any time an outside function needs to get a color for a value, + we access the Gradient class (currently found in wm_utils) - this + class calculates the color directly. + + + Previously, I'd get the color by figuring out which pixel the + value would be on and then get the pixel's color. + + This has issues because it limits things like dynamic color + changing or multi-color scales. + + 5. ??? + 6. Profit. + """ + + def __init__( + self, + parent, + plot_range, + high_color=wm_const.wm_HIGH_COLOR, + low_color=wm_const.wm_LOW_COLOR, + num_ticks=wm_const.wm_TICK_COUNT, + oor_high_color=wm_const.wm_OOR_HIGH_COLOR, + oor_low_color=wm_const.wm_OOR_LOW_COLOR, + ): + wx.Panel.__init__(self, parent) + + ### Inputs ########################################################## + self.parent = parent + self.plot_range = plot_range + self.high_color = high_color + self.low_color = low_color + self.num_ticks = num_ticks + self.oor_high_color = oor_high_color + self.oor_low_color = oor_low_color + self.invalid_color = wm_const.wm_INVALID_COLOR + + ### Initialize Size Attributes ###################################### + # These get set in set_sizes(), but are here to remind me that + # the instance attribute exists and what they are. + # Values are in px. + # fmt: off + self.text_h = None # text height + self.text_w = None # Length of longest tick label + self.grad_w = None # gradient width + self.grad_h = None # gradient height + self.spacer = None # spacer speration between items + self.grad_start_y = None # top of gradient pixel coord + self.grad_end_y = None # bottom of gradient pixel coord + self.grad_start_x = None # gradient left pixel coord + self.grad_end_x = None # gradient right pixel coord + self.tick_w = None # tick mark length + self.tick_start_x = None # tick label left pixel coord + self.dc_w = None # total bitmap width + self.dc_h = None # total bitmap height + # fmt: on + + ### Other Instance Attributes ####################################### + self.ticks = None + self.gradient = wm_utils.LinearGradient(self.low_color, self.high_color) + + ### Remainder of __init__ ########################################### + # Create the MemoryDC now - we'll add the bitmap later. + self.mdc = wx.MemoryDC() + self.mdc.SetFont( + wx.Font( + 9, + wx.FONTFAMILY_SWISS, + wx.FONTSTYLE_NORMAL, + wx.FONTWEIGHT_NORMAL, + ) + ) + + self.set_sizes() + + # Create EmptyBitmap in our MemoryDC where we'll do all our drawing. + # self.mdc.SelectObject(wx.EmptyBitmap(self.dc_w, self.dc_h)) + self.mdc.SelectObject(wx.Bitmap(self.dc_w, self.dc_h)) + + # Draw the entire thing + self.draw_scale() + + # Bind various events + self._bind_events() + + self._init_ui() + + def _init_ui(self): + """Add a Sizer that is the same size as the MemoryDC.""" + self.hbox = wx.BoxSizer(wx.HORIZONTAL) + self.hbox.Add((self.dc_w, self.dc_h)) + self.SetSizer(self.hbox) + + def _bind_events(self): + """Bind events to various event handlers.""" + self.Bind(wx.EVT_PAINT, self._on_paint) + self.Bind(wx.EVT_SIZE, self._on_size) + # self.Bind(wx.EVT_MOTION, self.on_mouse_move) + # self.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_left_down) + # self.Bind(wx.EVT_RIGHT_DOWN, self.on_mouse_right_down) + + def get_color(self, value): + """Get a color from the gradient.""" + # TODO: determine how wxPython's GradientFillLinear works and use that + # instead of grabbing the color from the gradient. + if value > self.plot_range[1]: + color = self.oor_high_color + elif value < self.plot_range[0]: + color = self.oor_low_color + else: + try: + # pxl = int(wm_utils.rescale(value, + # self.plot_range, + # (self.grad_end_y - 1, + # self.grad_start_y))) + # + # x_pt = self.grad_w // 2 + self.grad_start_x + # point = (x_pt, pxl) + # color = self.mdc.GetPixelPoint(point) + + # New Method + pxl = wm_utils.rescale(value, self.plot_range, (0, 1)) + color = self.gradient.get_color(pxl) + color = wx.Colour(*color) + except ValueError: + color = self.invalid_color + return color + + def calc_ticks(self): + """ + Calculate the tick marks' display string, value, and pixel value. + + High values are at the top of the scale, low at the bottom. + """ + # First we need to determine the values for the ticks. + pr = self.plot_range[1] - self.plot_range[0] + spacing = pr / (self.num_ticks - 1) + + tick_values = wm_utils.frange( + self.plot_range[0], self.plot_range[1] + 1, spacing + ) + + ticks = [] + for tick in tick_values: + string = "{:.3f}".format(tick) + value = tick + # `grad_end_y - 1` so that the bottom tick is aligned correctly. + pixel = wm_utils.rescale( + tick, self.plot_range, (self.grad_end_y - 1, self.grad_start_y) + ) + # Putting gradient_end_y as the "low" for rescale makes the + # high value be at the north end and the low value at the south. + ticks.append((string, value, pixel)) + + return ticks + + def draw_ticks(self, ticks): + """ + Print the tickmarks. + + Parameters + ---------- + ticks : list of (string, value, pixel) tuples + """ + pen = wx.Pen(wx.BLACK) + self.mdc.SetPen(pen) + text_w = max([self.mdc.GetTextExtent(_i[0])[0] for _i in ticks]) + for tick in ticks: + # Sorry, everything is measured from right to left... + tick_end = self.grad_start_x - self.spacer + tick_start = tick_end - self.tick_w + self.mdc.DrawLine(tick_start, tick[2], tick_end, tick[2]) + + # Text origin is top left of bounding box. + # Text is currently left-aligned. Maybe Change? + text_x = tick_start - self.spacer - text_w + text_y = tick[2] - self.text_h / 2 + self.mdc.DrawText(tick[0], text_x, text_y) + + def set_sizes(self): + """ + Set various instance attributes for item sizes and locations. + + Uses the current client size and Text Height to set set some items. + """ + # These are in a specific order. Do not change! + # First determine some fixed items + self.text_h = self.mdc.GetTextExtent("A")[1] + self.grad_w = 30 + self.grad_h = self.parent.GetClientSize()[1] - self.text_h + self.tick_w = 20 + self.spacer = 5 + self.grad_start_y = self.text_h / 2 + self.grad_end_y = self.grad_start_y + self.grad_h + + # Now that the widths are defined, I can calculate some other things + self.ticks = self.calc_ticks() + self.text_w = self.get_max_text_w(self.ticks) + + # Note: I'm intentionally listing every spacer manually rather than + # multiplying by how many there are. This makes it easier + # to see where I'm placing them. + self.tick_start_x = self.spacer + self.text_w + self.spacer + self.grad_start_x = self.tick_start_x + self.tick_w + self.spacer + self.grad_end_x = self.grad_start_x + self.grad_w + self.dc_w = self.grad_end_x + self.spacer # total bitmap width + self.dc_h = self.grad_h + self.text_h # total bitmap height + + def draw_background(self): + """ + Draw the background box. + + If I don't do this, then the background is black. + + Could I change wx.EmptyBitmap() so that it defaults to white rather + than black? + """ + # TODO: change the bitmap background to be transparent + c = wx.Colour(200, 230, 230, 0) + c = wx.Colour(255, 255, 255, 0) + pen = wx.Pen(c) + brush = wx.Brush(c) + self.mdc.SetPen(pen) + self.mdc.SetBrush(brush) + self.mdc.DrawRectangle(0, 0, self.dc_w, self.dc_h) + + def draw_scale(self): + """ + Draw the entire scale area. + + The scale area is: background, gradient, OOR colors, ticks, + and labels. + """ + self.draw_background() + + # Draw the Gradient on a portion of the MemoryDC + self.draw_gradient() + + # Draw the out-of-range high and low rectangles + c = self.oor_high_color + pen = wx.Pen(c) + brush = wx.Brush(c) + self.mdc.SetPen(pen) + self.mdc.SetBrush(brush) + self.mdc.DrawRectangle(self.grad_start_x, 2, self.grad_w, self.grad_start_y - 2) + + c = self.oor_low_color + pen = wx.Pen(c) + brush = wx.Brush(c) + self.mdc.SetPen(pen) + self.mdc.SetBrush(brush) + self.mdc.DrawRectangle( + self.grad_start_x, + self.grad_end_y, + self.grad_w, + self.dc_h - self.grad_end_y - 2, + ) + + # Calculate and draw the tickmarks. + self.draw_ticks(self.ticks) + + def draw_gradient(self): + """Draw the Gradient, painted from North (high) to South (low).""" + # self.mdc.GradientFillLinear((self.grad_start_x, self.grad_start_y, + # self.grad_w, self.grad_h), + # self.high_color, + # self.low_color, + # wx.SOUTH, + # ) + + # Remake of the wx.DC.GradientFillLinear wx core function, but uses + # my own algorithm for determining the colors. + # Doing so ensures that both the gradient and the die colors match. + + # Save the old pen colors + old_pen = self.mdc.GetPen() + old_brush = self.mdc.GetBrush() + delta = self.grad_h / 255 # height of one shade box + if delta < 1: + delta = 1 # max of 255 pts - fractional colors not defined. + + y = self.grad_start_y + while y <= self.grad_end_y: + val = wm_utils.rescale(y, (self.grad_start_y, self.grad_end_y), (1, 0)) + color = self.gradient.get_color(val) + self.mdc.SetPen(wx.Pen(color)) + self.mdc.SetBrush(wx.Brush(color)) + self.mdc.DrawRectangle(self.grad_start_x, y, self.grad_w, delta + 1) + y += delta + + # Set the pen and brush back to what they were + self.mdc.SetPen(old_pen) + self.mdc.SetBrush(old_brush) + + def get_max_text_w(self, ticks): + """ + Get the maximum label sizes. + + There's probably a better way... + """ + return max([self.mdc.GetTextExtent(i[0])[0] for i in ticks]) + + ### #-------------------------------------------------------------------- + ### Events + ### #-------------------------------------------------------------------- + + def _on_size(self, event): + """Redraw everything with the new sizes.""" + # TODO: Also reduce number of ticks when text starts to overlap + # or add ticks when there's extra space. + self.set_sizes() + self.hbox.Remove(0) + self.hbox.Add((self.dc_w, self.dc_h)) + # self.mdc.SelectObject(wx.EmptyBitmap(self.dc_w, self.dc_h)) + self.mdc.SelectObject(wx.Bitmap(self.dc_w, self.dc_h)) + self.draw_scale() + self.Refresh() + + def _on_paint(self, event): + """Push the MemoryDC bitmap to the displayed PaintDC.""" + dc = wx.PaintDC(self) + dc.Blit(0, 0, self.dc_w, self.dc_h, self.mdc, 0, 0) + + def on_color_change(self, event): + """ + Change the plot colors. + + This is done by updating self.gradient and calling self.draw_scale() + """ + if event["low"] is not None: + self.low_color = event["low"] + if event["high"] is not None: + self.high_color = event["high"] + self.gradient = wm_utils.LinearGradient(self.low_color, self.high_color) + + # self._clear_scale() + self.hbox.Remove(0) + self.hbox.Add((self.dc_w, self.dc_h)) + # self.mdc.SelectObject(wx.EmptyBitmap(self.dc_w, self.dc_h)) + self.mdc.SelectObject(wx.Bitmap(self.dc_w, self.dc_h)) + + self.draw_scale() + + def on_scale_change(self, event): + """Redraw things on scale change.""" + self.gradient = wm_utils.LinearGradient(self.low_color, self.high_color) + + self.hbox.Remove(0) + self.hbox.Add((self.dc_w, self.dc_h)) + # self.mdc.SelectObject(wx.EmptyBitmap(self.dc_w, self.dc_h)) + self.mdc.SelectObject(wx.Bitmap(self.dc_w, self.dc_h)) + + self.draw_scale() + + def on_mouse_move(self, event): + """Used for debugging.""" + pt = self.mdc.GetPixelPoint(event.GetPosition()) + print(pt) + + def on_mouse_left_down(self, event): + """Used for debugging.""" + print("Left-click - color from self.mdc.GetPixelPoint.") + pos = event.GetPosition() + w, h = self.mdc.GetSize() # change to gradient area + if pos[0] < w and pos[1] < h: + val = wm_utils.rescale( + pos[1], + (self.grad_start_y, self.grad_end_y - 1), + reversed(self.plot_range), + ) + a = self.mdc.GetPixelPoint(event.GetPosition()) + print("{}\t{}\t{}".format(pos, a, val)) + + def on_mouse_right_down(self, event): + """Used for debugging.""" + print("Right-click - color from get_color()") + pos = event.GetPosition() + w, h = self.mdc.GetSize() # change to gradient area + if pos[0] < w and pos[1] < h: + val = wm_utils.rescale( + pos[1], + (self.grad_start_y, self.grad_end_y - 1), + reversed(self.plot_range), + ) + a = self.get_color(val) + print("{}\t{}\t{}".format(pos, a, val)) + + def on_mouse_wheel(self, event): + print("mouse wheel!") + # self.on_mouse_left_down(event) + + +class DiscreteLegend(wx.Panel): + """ + Legend for discrete values. + + Is basically a 2D table of label-color rows. The colors are actually + buttons that can be clicked on - a color picker appears. However, as of + 2014-12-03, changing the color does not do anything. + + Parameters + ---------- + parent : wx.Panel + labels : list + colors : list, optional + """ + + def __init__( + self, + parent, + labels, + colors=None, + ): + wx.Panel.__init__(self, parent) + self.parent = parent + self.labels = labels + self.n_items = len(self.labels) + if colors is None: + self.colors = self.create_colors(self.n_items) + else: + self.colors = colors + self.create_color_dict() + + self._init_ui() + + def _init_ui(self): + """Initialize UI components.""" + # Add layout management + self.hbox = wx.BoxSizer(wx.HORIZONTAL) + self.fgs = wx.FlexGridSizer(rows=self.n_items, cols=2, vgap=0, hgap=2) + + # Create items to add + for _i, (key, value) in enumerate(zip(self.labels, self.colors)): + self.label = wx.StaticText( + self, + label=str(key), + style=wx.ALIGN_LEFT, + ) + + self.colorbox = csel.ColourSelect( + self, _i, "", tuple(value), style=wx.NO_BORDER, size=(20, 20) + ) + + self.Bind(csel.EVT_COLOURSELECT, self.on_color_pick, id=_i) + + self.fgs.Add(self.label, flag=wx.ALIGN_CENTER_VERTICAL) + self.fgs.Add(self.colorbox) + + # Add our items to the layout manager and set the sizer. + self.hbox.Add(self.fgs) + self.SetSizer(self.hbox) + + @staticmethod + def create_colors(n): + """ + Create the colors based on how many legend items there are (n). + + The idea is to start off with one color, assign it to the 1st legend + value, then find that color's complement and assign it to the 2nd + legend value. Then, move around the color wheel by some degree, + probably like so: + + + + We are limited to only using 1/2 of the circle + because we use the other half for the complements. + + 1. Split the circle into n parts. + 2. reorganize into alternations + 1 2 3 4 5 6 7 8 --> + 1 3 5 7 2 4 6 8 + """ + spacing = 360 / n + colors = [] + for val in wm_utils.frange(0, 360, spacing): + hsl = (val / 360, 1, 0.75) + colors.append(colorsys.hsv_to_rgb(*hsl)) + + # convert from 0-1 to 0-255 and return + colors = [tuple(int(i * 255) for i in color) for color in colors] + + # Alternate colors across the circle + colors = colors[::2] + colors[1::2] + return colors + + def create_color_dict(self): + """ + Take the value and color lists and creates a dict from them. + + This may eventually become a public function with two inputs: + lables, colors. + """ + # TODO: Determine if I want this to be a public callable method + self.color_dict = OrderedDict(zip(self.labels, self.colors)) + return self.color_dict + + def on_color_pick(self, event): + """Recreate the {label: color} dict and send to the parent.""" + # print(event.GetId()) + self.colors[event.GetId()] = event.GetValue().Get() + self.create_color_dict() + # Send the event to the parent: + wx.PostEvent(self.parent, event) + + +class LegendOverlay(FloatCanvas.Text): + """Demo of drawing overlay - to be used for legend.""" + + def __init__( + self, + String, + xy, + Size=24, + Color="Black", + BackgroundColor=None, + Family=wx.MODERN, + Style=wx.NORMAL, + Weight=wx.NORMAL, + Underlined=False, + Font=None, + ): + FloatCanvas.Text.__init__( + self, + String, + xy, + Size=Size, + Color=Color, + BackgroundColor=BackgroundColor, + Family=Family, + Style=Style, + Weight=Weight, + Underlined=Underlined, + Font=Font, + ) + + def _Draw(self, dc, Canvas): + """ + _Draw method for Overlay. + + .. note:: + This is a differeent signarture than the DrawObject Draw + """ + dc.SetFont(self.Font) + dc.SetTextForeground(self.Color) + if self.BackgroundColor: + dc.SetBackgroundMode(wx.SOLID) + dc.SetTextBackground(self.BackgroundColor) + else: + dc.SetBackgroundMode(wx.TRANSPARENT) + dc.DrawTextPoint(self.String, self.XY) + + +def main(): + """Display the Legend when module is run directly.""" + legend_labels = ["A", "Banana!", "C", "Donut", "E"] + # legend_labels = [str(_i) for _i in range(10)] + + legend_colors = None + + continuous_range = (10, 50) + + class ExampleFrame(wx.Frame): + """Base Frame.""" + + def __init__(self, title): + wx.Frame.__init__( + self, + None, # Window Parent + wx.ID_ANY, # id + title=title, # Window Title + size=(300 + 16, 550 + 38), # Size in px + ) + + self.Bind(wx.EVT_CLOSE, self.OnQuit) + + # Here's where we call the WaferMapPanel + self.hbox = wx.BoxSizer(wx.HORIZONTAL) + + self.d_legend = DiscreteLegend(self, legend_labels, legend_colors) + self.c_legend = ContinuousLegend(self, continuous_range) + + self.hbox.Add(self.d_legend, 0) + self.hbox.Add(self.c_legend, 0) + self.SetSizer(self.hbox) + + def OnQuit(self, event): + self.Destroy() + + app = wx.App() + frame = ExampleFrame("Legend Example") + frame.Show() + app.MainLoop() + + +if __name__ == "__main__": + main() diff --git a/src/wafer_map/wm_utils.py b/src/wafer_map/wm_utils.py index e9f0a1a..d86da10 100644 --- a/src/wafer_map/wm_utils.py +++ b/src/wafer_map/wm_utils.py @@ -1,601 +1,601 @@ -# -*- coding: utf-8 -*- -""" -Holds various utilities used by ``wafer_map``. -""" -from __future__ import absolute_import, division, print_function, unicode_literals - -import numpy as np -from colour import Color - -from wafer_map import PY2 - - -# Python2 Compatibility -if PY2: - range = xrange - - -class Gradient(object): - """ - Base class for all gradients. - - Currently does nothing. - """ - - pass - - -class LinearGradient(Gradient): - """ - Linear gradient between two colors. - - Parameters - ---------- - initial_color : - The starting color for the linear gradient. - - dest_color : - sdfsd - - Attributes - ---------- - self.initial_color : - asad - - self.dest_color : - asdads - - Methods - ------- - get_color(self, value) : - Returns a color that is ``value`` between self.initial_color and - self.final_color - """ - - def __init__(self, initial_color, dest_color): - self.initial_color = initial_color - self.dest_color = dest_color - - def get_color(self, value): - """Get a color along the gradient. Value = 0 to 1 inclusive.""" - return linear_gradient(self.initial_color, self.dest_color, value) - - -class PolylinearGradient(Gradient): - """ - Polylinear Gradient between ``n`` colors. - - Acts as a LinearGradient if ``n == 2``. - - Parameters - ---------- - colors : iterable - A list or tuple of RGB or RGBa tuples (or wx.Colour objects). Each - color in this list is a vertex of the polylinear gradient. - - Attributes - ---------- - self.colors : iterable - The list of colors (or wx.Colour objects) which are the verticies - of the poly gradient. - - self.initial_color : tuple or wx.Colour object - The starting color of the gradient. - - self.dest_color : tuple or wx.Colour object - The final color of the gradient. - - Methods - ------- - get_color(self, value): - Returns a color that is ``value`` along the gradient. - """ - - def __init__(self, *colors): - self.colors = colors - self.initial_color = self.colors[0] - self.dest_color = self.colors[-1] - - def get_color(self, value): - """Get a color value.""" - return polylinear_gradient(self.colors, value) - - -class BeizerGradient(Gradient): - """ - Beizer curve gradient between 3 colors. - - Not implemented. - """ - - def __init__(self, initial_color, arc_color, dest_color): - self.initial_color = initial_color - self.arc_color = arc_color - self.dest_color = dest_color - - def get_color(self, value): - """Get a color.""" - pass - - -def linear_gradient(initial_color, dest_color, value): - """ - Find the color that is ``value`` between initial_color and dest_color. - - Parameters - ---------- - initial_color : tuple - A 3- or 4-tuple of RGB or RGBa values representing the starting - color for the gradient. Each color channel should be - in the range 0-255 inclusive. - - dest_color : tuple - A 3- or 4-tuple of RGB or RGBa values representing the ending - color for the gradient. Each color channel should be - in the range 0-255. - - value : float - A floating point number from 0 to 1 inclusive that determines how - far along the color gradient the returned color should be. A value - of ``0`` returns ``initial_color`` while a value of ``1`` returns - ``dest_color``. - - Returns - ------- - (r, g, b) : tuple - A 3-tuple representing the color that is ``value * 100`` percent - along the gradient. Each color channel is 0-255 inclusive. - - Implementation Details - ---------------------- - All of this package works in the RGB colorspace. However, as is seen in - https://www.youtube.com/watch?v=LKnqECcg6Gw and - https://www.youtube.com/watch?v=sPiWg5jSoZI, the RGB color space does - not blend correctly with standard averaging, which is what I do here. - - I haven't found any source for this, but experimentation has shown that - the HSL colorspace *does* blend correctly with linear averaging. So - I use the ``colour`` module to convert RGB to HSL. After converting, I - take the linear average of my two colors (via ``rescale``) and then - convert back to RGB. - - Examples - -------- - Halfway between Red and Green is Yellow. This really should return - (255, 255, 0), but it's close enough for now. - - >>> linear_gradient((255, 0, 0), (0, 255, 0), 0.5) - (254, 255, 0) - - Red and Blue mix to make green. Standard Rainbow - - >>> linear_gradient((255, 0, 0), (0, 0, 255), 0.5) - (0, 255, 0) - - 75% of the from Red to Green is Orange. - - >>> linear_gradient((255, 0, 0), (0, 255, 0), 0.75) - (127, 255, 0) - """ - if value <= 0: - return initial_color - elif value >= 1: - return dest_color - - # Old way, linear averaging. - # r1, g1, b1 = (_c for _c in initial_color) - # r2, g2, b2 = (_c for _c in dest_color) - # r = int(rescale(value, (0, 1), (r1, r2))) - # g = int(rescale(value, (0, 1), (g1, g2))) - # b = int(rescale(value, (0, 1), (b1, b2))) - - # Using the ``colour`` package - # Convert 0-255 to 0-1, drop the 4th term, and instance the Color class - c1 = Color(rgb=tuple(_c / 255 for _c in initial_color)[:3]) - c2 = Color(rgb=tuple(_c / 255 for _c in dest_color)[:3]) - - # extract the HSL values - h1, s1, l1 = c1.hsl - h2, s2, l2 = c2.hsl - - # Perform the linear interpolation - h = rescale(value, (0, 1), (h1, h2)) - s = rescale(value, (0, 1), (s1, s2)) - l = rescale(value, (0, 1), (l1, l2)) - - # Convert back to 0-255 for wxPython - r, g, b = (int(_c * 255) for _c in Color(hsl=(h, s, l)).rgb) - - return (r, g, b) - - -def polylinear_gradient(colors, value): - """ - Create a gradient. - - colors is a list or tuple of length 2 or more. If length 2, then it's - the same as LinearGradient - Value is the 0-1 value between colors[0] and colors[-1]. - Assumes uniform spacing between all colors. - """ - n = len(colors) - if n == 2: - return linear_gradient(colors[0], colors[1], value) - - if value >= 1: - return colors[-1] - elif value <= 0: - return colors[0] - - # divide up our range into n - 1 segments, where n is the number of colors - l = 1 / (n - 1) # float division - - # figure out which segment we're in - determines start and end colors - m = int(value // l) # Note floor division - - low = m * l - high = (m + 1) * l - - # calculate where our value lies within that particular gradient - v2 = rescale(value, (low, high), (0, 1)) - - return linear_gradient(colors[m], colors[m + 1], v2) - - -def beizer_gradient(initial_color, arc_color, dest_color, value): - """ - Calculate the color value along a Beizer Curve. - - The Beizer Curve's control colors are defined by initial_color, - arc_color, and final_color. - """ - pass - - -def _GradientFillLinear( - rect, - intial_color, - dest_color, - direction, -): - """ - Reimplement the ``wxDCImpl::DoGradientFillLinear`` algorithm. - - This algorithm can be found in - ``wxWidgets-3.0.2/src/common/dcbase.cpp``, line 862. - - wxWidgets uses the native MS Windows (msw) function if it can: - wxMSIMG32DLL.GetSymbol(wxT("GradientFill") - - See function ``wxMSWDCImpl::DoGradientFillLinear`` in - wxWidgets-3.0.2/src/msw/dc.cpp, line 2870 - - Allows user to put in a value from 0 (intial_color) to 1 (dest_color). - - What will this function return? I do not know yet. - - There's not really a struct for a "continuous gradient"... - - I think that perhaps this function will just calculate the color for - a given 0-1 value between initial_color and dest_color on the fly. - - I'm an idiot! This is just linear algebra, I can solve this! - """ - pass - - -r""" - void wxDCImpl::DoGradientFillLinear(const wxRect& rect, - const wxColour& initialColour, - const wxColour& destColour, - wxDirection nDirection) - { - // save old pen - wxPen oldPen = m_pen; - wxBrush oldBrush = m_brush; - - wxUint8 nR1 = initialColour.Red(); - wxUint8 nG1 = initialColour.Green(); - wxUint8 nB1 = initialColour.Blue(); - wxUint8 nR2 = destColour.Red(); - wxUint8 nG2 = destColour.Green(); - wxUint8 nB2 = destColour.Blue(); - wxUint8 nR, nG, nB; - - if ( nDirection == wxEAST || nDirection == wxWEST ) - { - wxInt32 x = rect.GetWidth(); - wxInt32 w = x; // width of area to shade - wxInt32 xDelta = w/256; // height of one shade bend - if (xDelta < 1) - xDelta = 1; # max of 255 points - fractional colors are not defined. - - while (x >= xDelta) - { - x -= xDelta; - if (nR1 > nR2) - nR = nR1 - (nR1-nR2)*(w-x)/w; - else - nR = nR1 + (nR2-nR1)*(w-x)/w; - - if (nG1 > nG2) - nG = nG1 - (nG1-nG2)*(w-x)/w; - else - nG = nG1 + (nG2-nG1)*(w-x)/w; - - if (nB1 > nB2) - nB = nB1 - (nB1-nB2)*(w-x)/w; - else - nB = nB1 + (nB2-nB1)*(w-x)/w; - - wxColour colour(nR,nG,nB); - SetPen(wxPen(colour, 1, wxPENSTYLE_SOLID)); - SetBrush(wxBrush(colour)); - if(nDirection == wxEAST) - DoDrawRectangle(rect.GetRight()-x-xDelta+1, rect.GetTop(), - xDelta, rect.GetHeight()); - else //nDirection == wxWEST - DoDrawRectangle(rect.GetLeft()+x, rect.GetTop(), - xDelta, rect.GetHeight()); - } - } - else // nDirection == wxNORTH || nDirection == wxSOUTH - { - wxInt32 y = rect.GetHeight(); - wxInt32 w = y; // height of area to shade - wxInt32 yDelta = w/255; // height of one shade bend - if (yDelta < 1) - yDelta = 1; # max of 255 points - fractional colors are not defined. - - while (y > 0) - { - y -= yDelta; - if (nR1 > nR2) - nR = nR1 - (nR1-nR2)*(w-y)/w; - else - nR = nR1 + (nR2-nR1)*(w-y)/w; - - if (nG1 > nG2) - nG = nG1 - (nG1-nG2)*(w-y)/w; - else - nG = nG1 + (nG2-nG1)*(w-y)/w; - - if (nB1 > nB2) - nB = nB1 - (nB1-nB2)*(w-y)/w; - else - nB = nB1 + (nB2-nB1)*(w-y)/w; - - wxColour colour(nR,nG,nB); - SetPen(wxPen(colour, 1, wxPENSTYLE_SOLID)); - SetBrush(wxBrush(colour)); - if(nDirection == wxNORTH) - DoDrawRectangle(rect.GetLeft(), rect.GetTop()+y, - rect.GetWidth(), yDelta); - else //nDirection == wxSOUTH - DoDrawRectangle(rect.GetLeft(), rect.GetBottom()-y-yDelta+1, - rect.GetWidth(), yDelta); - } - } - - SetPen(oldPen); - SetBrush(oldBrush); -}""" - - -def frange(start, stop, step): - """Generator that creates an arbitrary-stepsize range.""" - r = start - while r < stop: - yield r - r += step - - -def coord_to_grid(coord, die_size, grid_center): - """ - Convert a panel coordinate to a grid value. - - Parameters - ---------- - coord : tuple - A 2-tuple of (x, y) floating point values for the panel coordinate - - die_size : tuple - A 2-tuple of (x, y) floating point values for the die size - - grid_center : tuple - A 2-tuple of (grid_x, grid_y) values that represents the origin of - the wafer in grid coordinates. - - Returns - ------- - (grid_x, grid_y) : tuple - The grid coordinates. Also known as (column, row). - """ - # grid_x = int(grid_center[0] + 0.5 + (coord[0] / die_size[0])) - # grid_y = int(grid_center[1] + 0.5 - (coord[1] / die_size[1])) - # Fixes #30, courtesy of GitHub user sinkra - grid_x = int(round(grid_center[0] + (coord[0] / die_size[0]))) - grid_y = int(round(grid_center[1] - (coord[1] / die_size[1]))) - return (grid_x, grid_y) - - -def grid_to_rect_coord(grid, die_size, grid_center): - """ - Convert a die's grid value to the origin point of the rectangle to draw. - - Adjusts for the fact that the grid falls on the center of a die by - subtracting die_size/2 from the coordinate. - - Adjusts for the fact that Grid +y is down while panel +y is up by - taking ``grid_center - grid`` rather than ``grid - grid_center`` as is - done in the X case. - """ - _x = die_size[0] * (grid[0] - grid_center[0] - 0.5) - _y = die_size[1] * (grid_center[1] - grid[1] - 0.5) - return (_x, _y) - - -def nanpercentile(a, percentile): - """ - Perform ``numpy.percentile(a, percentile)`` while ignoring NaN values. - - Only works on a 1D array. - """ - if type(a) != np.ndarray: - a = np.array(a) - return np.percentile(a[np.logical_not(np.isnan(a))], percentile) - - -def max_dist_sqrd(center, size): - """ - Calculate the squared distnace to the furthest corner of a rectangle. - - Assumes that the origin is ``(0, 0)``. - - Does not take the square of the distance for the sake of speed. - - If the rectangle's center is in the Q1, then the upper-right corner is - the farthest away from the origin. If in Q2, then the upper-left corner - is farthest away. Etc. - - Returns the magnitude of the largest distance. - - Used primarily for calculating if a die has any part outside of wafer's - edge exclusion. - - Parameters - ---------- - center : tuple of length 2, numerics - (x, y) tuple defining the rectangle's center coordinates - - size : tuple of length 2 - (x, y) tuple that defines the size of the rectangle. - - Returns - ------- - dist : numeric - The distance from the origin (0, 0) to the farthest corner of the - rectangle. - - See Also - -------- - max_dist : - Calculates the distance from the orgin (0, 0) to the - farthest corner of a rectangle. - """ - half_x = size[0] / 2.0 - half_y = size[1] / 2.0 - if center[0] < 0: - half_x = -half_x - if center[1] < 0: - half_y = -half_y - dist = (center[0] + half_x) ** 2 + (center[1] + half_y) ** 2 - return dist - - -def rescale(x, orig_scale, new_scale=(0, 1)): - """ - Rescale ``x`` to run over a new range. - - Rescales x (which was part of scale original_min to original_max) - to run over a range new_min to new_max such - that the value x maintains position on the new scale new_min to new_max. - If x is outside of xRange, then y will be outside of yRange. - - Default new scale range is 0 to 1 inclusive. - - Parameters - ---------- - x : numeric - The value to rescale. - - orig_scale : sequence of numerics, length 2 - The (min, max) value that ``x`` typically ranges over. - - new_scale : sequence of numerics, length 2, optional - The new (min, max) value that the rescaled ``x`` should reference - - Returns - ------- - result : float - The rescaled ``x`` value - - Examples - -------- - >>> rescale(5, (10, 20), (0, 1)) - -0.5 - >>> rescale(27, (0, 200), (0, 5)) - 0.675 - >>> rescale(1.5, (0, 1), (0, 10)) - 15.0 - """ - original_min, original_max = orig_scale - new_min, new_max = new_scale - - part_a = x * (new_max - new_min) - part_b = original_min * new_max - original_max * new_min - denominator = original_max - original_min - try: - result = (part_a - part_b) / denominator - except ZeroDivisionError: - result = 0 - return float(result) - - -def rescale_clip(x, orig_scale, new_scale=(0, 1)): - """ - Rescale and clip ``x`` to run over a new range. - - Same as rescale, but also clips the new data. Any result that is - below new_min or above new_max is listed as new_min or - new_max, respectively - - Examples - -------- - >>> rescale_clip(5, (10, 20), (0, 1)) - 0 - >>> rescale_clip(15, (10, 20), (0, 1)) - 0.5 - >>> rescale_clip(25, (10, 20), (0, 1)) - 1 - """ - original_min, original_max = orig_scale - new_min, new_max = new_scale - - result = rescale(x, (original_min, original_max), (new_min, new_max)) - if result > new_max: - return new_max - elif result < new_min: - return new_min - else: - return result - - -if __name__ == "__main__": - print("0 and 1") - print(polylinear_gradient([(0, 0, 0), (255, 0, 0), (0, 255, 0)], 0)) - print(polylinear_gradient([(0, 0, 0), (255, 0, 0), (0, 255, 0)], 1)) - - print("\n0.5:") - print(polylinear_gradient([(0, 0, 0), (255, 0, 0), (0, 255, 0)], 0.5)) - - print("\n0.25") - print(polylinear_gradient([(0, 0, 0), (255, 0, 0), (0, 255, 0)], 0.25)) - print("\n0.75") - print(polylinear_gradient([(0, 0, 0), (255, 0, 0), (0, 255, 0)], 0.75)) - - print("\n4 colors") - print( - polylinear_gradient( - [ - (0, 0, 0), - (255, 0, 0), - (0, 255, 0), - (0, 0, 255), - ], - 0.5, - ) - ) - - print("\nLinear Gradient, 0.5") - print(linear_gradient((0, 0, 0), (255, 255, 255), 0.5)) +# -*- coding: utf-8 -*- +""" +Holds various utilities used by ``wafer_map``. +""" +from __future__ import absolute_import, division, print_function, unicode_literals + +import numpy as np +from colour import Color + +from wafer_map import PY2 + + +# Python2 Compatibility +if PY2: + range = xrange + + +class Gradient(object): + """ + Base class for all gradients. + + Currently does nothing. + """ + + pass + + +class LinearGradient(Gradient): + """ + Linear gradient between two colors. + + Parameters + ---------- + initial_color : + The starting color for the linear gradient. + + dest_color : + sdfsd + + Attributes + ---------- + self.initial_color : + asad + + self.dest_color : + asdads + + Methods + ------- + get_color(self, value) : + Returns a color that is ``value`` between self.initial_color and + self.final_color + """ + + def __init__(self, initial_color, dest_color): + self.initial_color = initial_color + self.dest_color = dest_color + + def get_color(self, value): + """Get a color along the gradient. Value = 0 to 1 inclusive.""" + return linear_gradient(self.initial_color, self.dest_color, value) + + +class PolylinearGradient(Gradient): + """ + Polylinear Gradient between ``n`` colors. + + Acts as a LinearGradient if ``n == 2``. + + Parameters + ---------- + colors : iterable + A list or tuple of RGB or RGBa tuples (or wx.Colour objects). Each + color in this list is a vertex of the polylinear gradient. + + Attributes + ---------- + self.colors : iterable + The list of colors (or wx.Colour objects) which are the verticies + of the poly gradient. + + self.initial_color : tuple or wx.Colour object + The starting color of the gradient. + + self.dest_color : tuple or wx.Colour object + The final color of the gradient. + + Methods + ------- + get_color(self, value): + Returns a color that is ``value`` along the gradient. + """ + + def __init__(self, *colors): + self.colors = colors + self.initial_color = self.colors[0] + self.dest_color = self.colors[-1] + + def get_color(self, value): + """Get a color value.""" + return polylinear_gradient(self.colors, value) + + +class BeizerGradient(Gradient): + """ + Beizer curve gradient between 3 colors. + + Not implemented. + """ + + def __init__(self, initial_color, arc_color, dest_color): + self.initial_color = initial_color + self.arc_color = arc_color + self.dest_color = dest_color + + def get_color(self, value): + """Get a color.""" + pass + + +def linear_gradient(initial_color, dest_color, value): + """ + Find the color that is ``value`` between initial_color and dest_color. + + Parameters + ---------- + initial_color : tuple + A 3- or 4-tuple of RGB or RGBa values representing the starting + color for the gradient. Each color channel should be + in the range 0-255 inclusive. + + dest_color : tuple + A 3- or 4-tuple of RGB or RGBa values representing the ending + color for the gradient. Each color channel should be + in the range 0-255. + + value : float + A floating point number from 0 to 1 inclusive that determines how + far along the color gradient the returned color should be. A value + of ``0`` returns ``initial_color`` while a value of ``1`` returns + ``dest_color``. + + Returns + ------- + (r, g, b) : tuple + A 3-tuple representing the color that is ``value * 100`` percent + along the gradient. Each color channel is 0-255 inclusive. + + Implementation Details + ---------------------- + All of this package works in the RGB colorspace. However, as is seen in + https://www.youtube.com/watch?v=LKnqECcg6Gw and + https://www.youtube.com/watch?v=sPiWg5jSoZI, the RGB color space does + not blend correctly with standard averaging, which is what I do here. + + I haven't found any source for this, but experimentation has shown that + the HSL colorspace *does* blend correctly with linear averaging. So + I use the ``colour`` module to convert RGB to HSL. After converting, I + take the linear average of my two colors (via ``rescale``) and then + convert back to RGB. + + Examples + -------- + Halfway between Red and Green is Yellow. This really should return + (255, 255, 0), but it's close enough for now. + + >>> linear_gradient((255, 0, 0), (0, 255, 0), 0.5) + (254, 255, 0) + + Red and Blue mix to make green. Standard Rainbow + + >>> linear_gradient((255, 0, 0), (0, 0, 255), 0.5) + (0, 255, 0) + + 75% of the from Red to Green is Orange. + + >>> linear_gradient((255, 0, 0), (0, 255, 0), 0.75) + (127, 255, 0) + """ + if value <= 0: + return initial_color + elif value >= 1: + return dest_color + + # Old way, linear averaging. + # r1, g1, b1 = (_c for _c in initial_color) + # r2, g2, b2 = (_c for _c in dest_color) + # r = int(rescale(value, (0, 1), (r1, r2))) + # g = int(rescale(value, (0, 1), (g1, g2))) + # b = int(rescale(value, (0, 1), (b1, b2))) + + # Using the ``colour`` package + # Convert 0-255 to 0-1, drop the 4th term, and instance the Color class + c1 = Color(rgb=tuple(_c / 255 for _c in initial_color)[:3]) + c2 = Color(rgb=tuple(_c / 255 for _c in dest_color)[:3]) + + # extract the HSL values + h1, s1, l1 = c1.hsl + h2, s2, l2 = c2.hsl + + # Perform the linear interpolation + h = rescale(value, (0, 1), (h1, h2)) + s = rescale(value, (0, 1), (s1, s2)) + l = rescale(value, (0, 1), (l1, l2)) + + # Convert back to 0-255 for wxPython + r, g, b = (int(_c * 255) for _c in Color(hsl=(h, s, l)).rgb) + + return (r, g, b) + + +def polylinear_gradient(colors, value): + """ + Create a gradient. + + colors is a list or tuple of length 2 or more. If length 2, then it's + the same as LinearGradient + Value is the 0-1 value between colors[0] and colors[-1]. + Assumes uniform spacing between all colors. + """ + n = len(colors) + if n == 2: + return linear_gradient(colors[0], colors[1], value) + + if value >= 1: + return colors[-1] + elif value <= 0: + return colors[0] + + # divide up our range into n - 1 segments, where n is the number of colors + l = 1 / (n - 1) # float division + + # figure out which segment we're in - determines start and end colors + m = int(value // l) # Note floor division + + low = m * l + high = (m + 1) * l + + # calculate where our value lies within that particular gradient + v2 = rescale(value, (low, high), (0, 1)) + + return linear_gradient(colors[m], colors[m + 1], v2) + + +def beizer_gradient(initial_color, arc_color, dest_color, value): + """ + Calculate the color value along a Beizer Curve. + + The Beizer Curve's control colors are defined by initial_color, + arc_color, and final_color. + """ + pass + + +def _GradientFillLinear( + rect, + intial_color, + dest_color, + direction, +): + """ + Reimplement the ``wxDCImpl::DoGradientFillLinear`` algorithm. + + This algorithm can be found in + ``wxWidgets-3.0.2/src/common/dcbase.cpp``, line 862. + + wxWidgets uses the native MS Windows (msw) function if it can: + wxMSIMG32DLL.GetSymbol(wxT("GradientFill") + + See function ``wxMSWDCImpl::DoGradientFillLinear`` in + wxWidgets-3.0.2/src/msw/dc.cpp, line 2870 + + Allows user to put in a value from 0 (intial_color) to 1 (dest_color). + + What will this function return? I do not know yet. + + There's not really a struct for a "continuous gradient"... + + I think that perhaps this function will just calculate the color for + a given 0-1 value between initial_color and dest_color on the fly. + + I'm an idiot! This is just linear algebra, I can solve this! + """ + pass + + +r""" + void wxDCImpl::DoGradientFillLinear(const wxRect& rect, + const wxColour& initialColour, + const wxColour& destColour, + wxDirection nDirection) + { + // save old pen + wxPen oldPen = m_pen; + wxBrush oldBrush = m_brush; + + wxUint8 nR1 = initialColour.Red(); + wxUint8 nG1 = initialColour.Green(); + wxUint8 nB1 = initialColour.Blue(); + wxUint8 nR2 = destColour.Red(); + wxUint8 nG2 = destColour.Green(); + wxUint8 nB2 = destColour.Blue(); + wxUint8 nR, nG, nB; + + if ( nDirection == wxEAST || nDirection == wxWEST ) + { + wxInt32 x = rect.GetWidth(); + wxInt32 w = x; // width of area to shade + wxInt32 xDelta = w/256; // height of one shade bend + if (xDelta < 1) + xDelta = 1; # max of 255 points - fractional colors are not defined. + + while (x >= xDelta) + { + x -= xDelta; + if (nR1 > nR2) + nR = nR1 - (nR1-nR2)*(w-x)/w; + else + nR = nR1 + (nR2-nR1)*(w-x)/w; + + if (nG1 > nG2) + nG = nG1 - (nG1-nG2)*(w-x)/w; + else + nG = nG1 + (nG2-nG1)*(w-x)/w; + + if (nB1 > nB2) + nB = nB1 - (nB1-nB2)*(w-x)/w; + else + nB = nB1 + (nB2-nB1)*(w-x)/w; + + wxColour colour(nR,nG,nB); + SetPen(wxPen(colour, 1, wxPENSTYLE_SOLID)); + SetBrush(wxBrush(colour)); + if(nDirection == wxEAST) + DoDrawRectangle(rect.GetRight()-x-xDelta+1, rect.GetTop(), + xDelta, rect.GetHeight()); + else //nDirection == wxWEST + DoDrawRectangle(rect.GetLeft()+x, rect.GetTop(), + xDelta, rect.GetHeight()); + } + } + else // nDirection == wxNORTH || nDirection == wxSOUTH + { + wxInt32 y = rect.GetHeight(); + wxInt32 w = y; // height of area to shade + wxInt32 yDelta = w/255; // height of one shade bend + if (yDelta < 1) + yDelta = 1; # max of 255 points - fractional colors are not defined. + + while (y > 0) + { + y -= yDelta; + if (nR1 > nR2) + nR = nR1 - (nR1-nR2)*(w-y)/w; + else + nR = nR1 + (nR2-nR1)*(w-y)/w; + + if (nG1 > nG2) + nG = nG1 - (nG1-nG2)*(w-y)/w; + else + nG = nG1 + (nG2-nG1)*(w-y)/w; + + if (nB1 > nB2) + nB = nB1 - (nB1-nB2)*(w-y)/w; + else + nB = nB1 + (nB2-nB1)*(w-y)/w; + + wxColour colour(nR,nG,nB); + SetPen(wxPen(colour, 1, wxPENSTYLE_SOLID)); + SetBrush(wxBrush(colour)); + if(nDirection == wxNORTH) + DoDrawRectangle(rect.GetLeft(), rect.GetTop()+y, + rect.GetWidth(), yDelta); + else //nDirection == wxSOUTH + DoDrawRectangle(rect.GetLeft(), rect.GetBottom()-y-yDelta+1, + rect.GetWidth(), yDelta); + } + } + + SetPen(oldPen); + SetBrush(oldBrush); +}""" + + +def frange(start, stop, step): + """Generator that creates an arbitrary-stepsize range.""" + r = start + while r < stop: + yield r + r += step + + +def coord_to_grid(coord, die_size, grid_center): + """ + Convert a panel coordinate to a grid value. + + Parameters + ---------- + coord : tuple + A 2-tuple of (x, y) floating point values for the panel coordinate + + die_size : tuple + A 2-tuple of (x, y) floating point values for the die size + + grid_center : tuple + A 2-tuple of (grid_x, grid_y) values that represents the origin of + the wafer in grid coordinates. + + Returns + ------- + (grid_x, grid_y) : tuple + The grid coordinates. Also known as (column, row). + """ + # grid_x = int(grid_center[0] + 0.5 + (coord[0] / die_size[0])) + # grid_y = int(grid_center[1] + 0.5 - (coord[1] / die_size[1])) + # Fixes #30, courtesy of GitHub user sinkra + grid_x = int(round(grid_center[0] + (coord[0] / die_size[0]))) + grid_y = int(round(grid_center[1] - (coord[1] / die_size[1]))) + return (grid_x, grid_y) + + +def grid_to_rect_coord(grid, die_size, grid_center): + """ + Convert a die's grid value to the origin point of the rectangle to draw. + + Adjusts for the fact that the grid falls on the center of a die by + subtracting die_size/2 from the coordinate. + + Adjusts for the fact that Grid +y is down while panel +y is up by + taking ``grid_center - grid`` rather than ``grid - grid_center`` as is + done in the X case. + """ + _x = die_size[0] * (grid[0] - grid_center[0] - 0.5) + _y = die_size[1] * (grid_center[1] - grid[1] - 0.5) + return (_x, _y) + + +def nanpercentile(a, percentile): + """ + Perform ``numpy.percentile(a, percentile)`` while ignoring NaN values. + + Only works on a 1D array. + """ + if type(a) != np.ndarray: + a = np.array(a) + return np.percentile(a[np.logical_not(np.isnan(a))], percentile) + + +def max_dist_sqrd(center, size): + """ + Calculate the squared distnace to the furthest corner of a rectangle. + + Assumes that the origin is ``(0, 0)``. + + Does not take the square of the distance for the sake of speed. + + If the rectangle's center is in the Q1, then the upper-right corner is + the farthest away from the origin. If in Q2, then the upper-left corner + is farthest away. Etc. + + Returns the magnitude of the largest distance. + + Used primarily for calculating if a die has any part outside of wafer's + edge exclusion. + + Parameters + ---------- + center : tuple of length 2, numerics + (x, y) tuple defining the rectangle's center coordinates + + size : tuple of length 2 + (x, y) tuple that defines the size of the rectangle. + + Returns + ------- + dist : numeric + The distance from the origin (0, 0) to the farthest corner of the + rectangle. + + See Also + -------- + max_dist : + Calculates the distance from the orgin (0, 0) to the + farthest corner of a rectangle. + """ + half_x = size[0] / 2.0 + half_y = size[1] / 2.0 + if center[0] < 0: + half_x = -half_x + if center[1] < 0: + half_y = -half_y + dist = (center[0] + half_x) ** 2 + (center[1] + half_y) ** 2 + return dist + + +def rescale(x, orig_scale, new_scale=(0, 1)): + """ + Rescale ``x`` to run over a new range. + + Rescales x (which was part of scale original_min to original_max) + to run over a range new_min to new_max such + that the value x maintains position on the new scale new_min to new_max. + If x is outside of xRange, then y will be outside of yRange. + + Default new scale range is 0 to 1 inclusive. + + Parameters + ---------- + x : numeric + The value to rescale. + + orig_scale : sequence of numerics, length 2 + The (min, max) value that ``x`` typically ranges over. + + new_scale : sequence of numerics, length 2, optional + The new (min, max) value that the rescaled ``x`` should reference + + Returns + ------- + result : float + The rescaled ``x`` value + + Examples + -------- + >>> rescale(5, (10, 20), (0, 1)) + -0.5 + >>> rescale(27, (0, 200), (0, 5)) + 0.675 + >>> rescale(1.5, (0, 1), (0, 10)) + 15.0 + """ + original_min, original_max = orig_scale + new_min, new_max = new_scale + + part_a = x * (new_max - new_min) + part_b = original_min * new_max - original_max * new_min + denominator = original_max - original_min + try: + result = (part_a - part_b) / denominator + except ZeroDivisionError: + result = 0 + return float(result) + + +def rescale_clip(x, orig_scale, new_scale=(0, 1)): + """ + Rescale and clip ``x`` to run over a new range. + + Same as rescale, but also clips the new data. Any result that is + below new_min or above new_max is listed as new_min or + new_max, respectively + + Examples + -------- + >>> rescale_clip(5, (10, 20), (0, 1)) + 0 + >>> rescale_clip(15, (10, 20), (0, 1)) + 0.5 + >>> rescale_clip(25, (10, 20), (0, 1)) + 1 + """ + original_min, original_max = orig_scale + new_min, new_max = new_scale + + result = rescale(x, (original_min, original_max), (new_min, new_max)) + if result > new_max: + return new_max + elif result < new_min: + return new_min + else: + return result + + +if __name__ == "__main__": + print("0 and 1") + print(polylinear_gradient([(0, 0, 0), (255, 0, 0), (0, 255, 0)], 0)) + print(polylinear_gradient([(0, 0, 0), (255, 0, 0), (0, 255, 0)], 1)) + + print("\n0.5:") + print(polylinear_gradient([(0, 0, 0), (255, 0, 0), (0, 255, 0)], 0.5)) + + print("\n0.25") + print(polylinear_gradient([(0, 0, 0), (255, 0, 0), (0, 255, 0)], 0.25)) + print("\n0.75") + print(polylinear_gradient([(0, 0, 0), (255, 0, 0), (0, 255, 0)], 0.75)) + + print("\n4 colors") + print( + polylinear_gradient( + [ + (0, 0, 0), + (255, 0, 0), + (0, 255, 0), + (0, 0, 255), + ], + 0.5, + ) + ) + + print("\nLinear Gradient, 0.5") + print(linear_gradient((0, 0, 0), (255, 255, 255), 0.5)) diff --git a/tests/__init__.py b/tests/__init__.py index d8a0d24..1dd27ee 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,6 @@ -# -*- coding: utf-8 -*- -""" -Unit tests for wafer_map. - -Please use a test runner for these. -""" +# -*- coding: utf-8 -*- +""" +Unit tests for wafer_map. + +Please use a test runner for these. +""" diff --git a/tests/test_utils.py b/tests/test_utils.py index 5e28644..0e8a466 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,23 +1,23 @@ -# -*- coding: utf-8 -*- -""" -Unittests for the :module:`wafer_map.utils` module. -""" -from __future__ import absolute_import, division, print_function, unicode_literals -import unittest - -from wafer_map import wm_utils as utils - - -class LinearGradient(unittest.TestCase): - - known_values = ( - ((0, 0, 0), (255, 255, 255), 0.5, (127, 127, 127)), - ((0, 0, 0), (255, 0, 0), 0.5, (95, 31, 31)), - ((0, 0, 0), (0, 255, 0), 0.5, (95, 95, 31)), - ((0, 0, 0), (0, 0, 255), 0.5, (31, 95, 31)), - ) - - def test_known_values(self): - for _start, _end, _value, _expected in self.known_values: - result = utils.linear_gradient(_start, _end, _value) - self.assertEqual(result, _expected) +# -*- coding: utf-8 -*- +""" +Unittests for the :module:`wafer_map.utils` module. +""" +from __future__ import absolute_import, division, print_function, unicode_literals +import unittest + +from wafer_map import wm_utils as utils + + +class LinearGradient(unittest.TestCase): + + known_values = ( + ((0, 0, 0), (255, 255, 255), 0.5, (127, 127, 127)), + ((0, 0, 0), (255, 0, 0), 0.5, (95, 31, 31)), + ((0, 0, 0), (0, 255, 0), 0.5, (95, 95, 31)), + ((0, 0, 0), (0, 0, 255), 0.5, (31, 95, 31)), + ) + + def test_known_values(self): + for _start, _end, _value, _expected in self.known_values: + result = utils.linear_gradient(_start, _end, _value) + self.assertEqual(result, _expected)