Source code for wafer_map.wm_utils

# -*- coding: utf-8 -*-
# pylint: disable=C0103
#         C0103 = invalid variable name
"""
Holds various utilities used by ``wafer_map``.
"""
# ---------------------------------------------------------------------------
### Imports
# ---------------------------------------------------------------------------
# Standard Library
from __future__ import absolute_import, division, print_function, unicode_literals

# Third-Party
import numpy as np
from colour import Color

# Package/Application
from wafer_map import PY2


# Python2 Compatibility
if PY2:
    range = xrange


[docs]class Gradient(object): """ Base class for all gradients. Currently does nothing. """ pass
[docs]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
[docs] 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)
[docs]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]
[docs] def get_color(self, value): """Get a color value.""" return polylinear_gradient(self.colors, value)
[docs]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
[docs] def get_color(self, value): """Get a color.""" pass
[docs]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)
[docs]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)
[docs]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
[docs]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); }"""
[docs]def frange(start, stop, step): """Generator that creates an arbitrary-stepsize range.""" r = start while r < stop: yield r r += step
[docs]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)
[docs]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)
[docs]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)
[docs]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. half_y = size[1]/2. 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
[docs]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)
[docs]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), .5))