#!/usr/bin/env python

"""
KMTEditor.py

This program allows one to edit a 2D KMT data pixel for pixel.

Author : Deepak Chandan
Date   : June 24th, 2015
"""

import sys, argparse, os, time
from netCDF4 import Dataset
import numpy as np
from PyQt4.QtCore import *
from PyQt4.QtGui import *

import matplotlib as mpl
from matplotlib import pylab as plt
from mpl_toolkits.basemap import Basemap
import matplotlib.patches as mpatches
from matplotlib.collections import PatchCollection
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas

#from cesmGUITools.utilities import nccopy
from nccopy import nccopy

mpl.rc('axes',edgecolor='w')

class DataContainer(object):
    """
    DataContainer: A "data container" class for this application which does the job
    of storing the map and other related data as well as a "cursor" upon this data.
    """
    class Cursor(object):
        def __init__(self):
            self.marker = None   # The symbol which is used to denote the marker. Leave it as 'None' to show a square.
            self.x = 0           # X-position of the cursor
            self.y = 0           # Y-position of the cursor

    def __init__(self, nrows, ncols, fname, datavar):
        """
        ARGUMENTS
            nrows - number of rows for the view
            ncols - number of columns for the view
            fname - name of the data file.
            scale - a multiplicative scale factor for the data to be visualized and edited
        """
        self.fname   = fname
        self.datavar = datavar
        self.__read_nc_file()
        self.orig_data = np.copy(self.data)
        self.ny, self.nx = self.data.shape

        self.lon_modulo = 360

        # Datawindow variables
        self.view   = None        # The array (actually a numpy view) that stores the data to be displayed in the main window
        self.view_masked = None   # A masked version of self.view
        self.nrows  = nrows       # Number of rows to display in the main windows (the 'view')
        self.ncols  = ncols       # Number of cols to display in the main windows
        self.si     = None        # 0-based row index of the first element
        self.sj     = None        # 0-based col index of the first element

        # Tracking which elements are changed
        # The columns store: i index, j index, new value
        self.changes = np.zeros((self.ny*self.nx, 3), dtype=np.int32)
        self.changes_row_idx = 0

        # A cursor object on the view
        self.cursor = DataContainer.Cursor()

    def __read_nc_file(self):
        """ This subroutine reads the netCDF4 data file. """
        ncfile        = Dataset(self.fname, "r", format="NETCDF4")
        # We are flipping the arrays so that the latitudes go from 90:-90
        self.data     = np.flipud(ncfile.variables[self.datavar][:,:])
        self.kmt_lons = np.flipud(ncfile.variables["ULON"][:,:])
        self.kmt_lats = np.flipud(ncfile.variables["ULAT"][:,:])
        ncfile.close()

    def getViewStatistics(self): return (self.view.min(), self.view.max(), self.view.mean())

    def getCursor(self): return self.cursor

    def getValueUnderCursor(self):
        # We need to first map the position of the cursor in the view to the position of the
        # on the global map.
        ci, cj = self.viewIndex2GlobalIndex(self.cursor.y, self.cursor.x)
        return self.data[ci, cj]

    def updateCursorPosition(self, event):
        """
        Updates the current view in the data window. This function is
        triggered whenever the user presses the arrow keys.
        ARGUMENTS
            event - Qt key press event
        """
        key = event.key()
        # on_boundary = None   # This will store which boundary if any we've reached
        if key == Qt.Key_Up:
            self.cursor.y = max(0, self.cursor.y - 1)
        elif key == Qt.Key_Down:
            self.cursor.y = min(self.nrows-1, self.cursor.y + 1)
        elif key == Qt.Key_Left:
            self.cursor.x = max(0, self.cursor.x - 1)
        elif key == Qt.Key_Right:
            self.cursor.x = min(self.ncols-1, self.cursor.x + 1)

    def updateView(self, si, sj):
        """
        Updates the data for the view.
        ARGUMENTS
            si, sj - the global 0-based i,j indices of the top left corner of the view
        RETURNS
            Statistics of the newly updated view (a tuple with the min, max, and the mean for the new view)
        """
        self.view = self.data[si:si+self.nrows, sj:sj+self.ncols].view()

        # We need to create the continent mask. We will take this as a masked numpy view
        # on the self.view object.
        self.view_masked = self.view.view(np.ma.MaskedArray)

        # Now that we have updated the view, and created the masked view of the
        # view itself, we need to update the "mask" of the masked view.
        self.updateMask()

        self.si   = si
        self.sj   = sj
        return self.getViewStatistics()

    def updateMask(self): self.view_masked.mask = (self.view == 0)

    def moveView(self, move):
        """
        Moves the view window over the global dataset in response to the L,R,U,D keys.
        ARGUMENTS
            move : A Qt key value
        RETURNS
            Statistics of the newly updated view (a tuple with the min, max, and the mean for the new view)
        """
        col_inc = int(self.ncols*0.25)  # Column increment
        row_inc = int(self.nrows*0.25)  # Row increment

        if (move == Qt.Key_L):  # MOVE THE VIEW RIGHT
            new_sj = min(self.nx-self.ncols, self.sj + col_inc)
            return self.updateView(self.si, new_sj)
        elif (move == Qt.Key_H): # MOVE THE VIEW LEFT
            new_sj = max(0, self.sj - col_inc)
            return self.updateView(self.si, new_sj)
        elif (move == Qt.Key_K): # MOVE THE VIEW UP
            new_si = max(0, self.si - row_inc)
            return self.updateView(new_si, self.sj)
        elif (move == Qt.Key_J): # MOVE THE VIEW DOWN
            new_si = min(self.ny-self.nrows, self.si + row_inc)
            return self.updateView(new_si, self.sj)

    def modifyValue(self, inp):
        """
        Modify the value for a particular pixel. The location of the pixel is that
        determined by the current position of the cursor.
        ARGUMENTS
            inp - a string containing a float
        """
        ci, cj = self.viewIndex2GlobalIndex(self.cursor.y, self.cursor.x)

        _tmp = int(float(inp))
        self.data[ci, cj] = _tmp
        self.changes[self.changes_row_idx, :] = ci, cj, _tmp
        self.changes_row_idx += 1

        # Now that we have changed a value, we have to update the continent mask as
        # well, in case the update entailed creating or destroying land.
        self.updateMask()

    def getAverage(self):
        """
        Returns the average value at the cursor computed from the values of the surrounding cells. This
        is a 4 point average.
        If center==True, then includes the cell at which the cursor is in the calculation of the average,
        in which case it will be a 5 point average.
        """
        ci, cj = self.cursor.y, self.cursor.x
        _sum   = self.view_masked[ci-1:ci+2,cj-1:cj+2].sum()
        # Rounding the floating point to the **nearest** integer
        return np.round(_sum/(self.view_masked[ci-1:ci+2,cj-1:cj+2].count()))

    def viewIndex2GlobalIndex(self, i, j):
        """ Converts an i,j index into the data window into an index for the
        same element into the global data. """
        return (self.si + i, self.sj + j)

class KMTEditor(QMainWindow):

    KMT_MIN_VAL = 0
    KMT_MAX_VAL = 60

    def __init__(self, fname, datavar, dwx=60, dwy=60):
        """
        ARGUMENTS:
            fname    - Name of the netcdf4 file
            datavar  - Name of the data variable in the file for which to plot
            dwx, dwy - size of the DataContainer in number of array elements
            scale    - A float that will be multiplied with the data to scale the data
        """
        super(KMTEditor, self).__init__(None)
        self.setWindowTitle('KMTEditor - {0}'.format(fname))

        #  Creating a variable that contains all the data
        self.dc = DataContainer(dwy, dwx, fname, datavar)

        self.cursor = self.dc.getCursor()  # Defining a cursor on the data
        # This is the Rectangle boundary drawn on the world map that bounds the region
        # under the view. At the start of the program it's value is None
        self.prvrect = None

        # This stores the matplotlib text objects used in the rendering of the colorbar
        # This is used by the function draw_colorbar()
        self.colorbarlabels = []
        # self.colorbar_gradient is used by the function draw_colorbar()
        self.colorbar_gradient = np.vstack((np.linspace(0, 1, 256), np.linspace(0, 1, 256)))

        # The previously updated value
        self.buffer_value = None

        # Stuff for saving data
        self.ofile = None   # Name of output file
        self.asked_about_overwrite_permission = False
        self.unsaved_changes_exist = False

        self.maps = mpl.cm.datad.keys()  # The names of colormaps available
        self.maps.sort() # Sorting them alphabetically for ease of use

        self.create_menu()
        self.create_main_window()

        self.set_stats_info(self.dc.updateView(0, 0))  # Set the initial view for the data container class

        self.draw_preview_worldmap()
        self.render_view()
        self.statusBar().showMessage('KMTEditor 2015')

    def keyPressEvent(self, e):
        if e.key() == Qt.Key_Equal:
            # Pressing = for edit
            self.inputbox.setFocus()
        elif e.key() == Qt.Key_V:
            self.update_value(self.buffer_value)
        elif e.key() == Qt.Key_C:
            self.buffer_value = self.dc.getValueUnderCursor()
            self.statusBar().showMessage('Value copied: {0}'.format(self.buffer_value), 2000)
        elif e.key() == Qt.Key_A:
            # Get the 5-point average and update the value
            self.update_value_with_average(self.dc.getAverage())
        elif e.key() == Qt.Key_M:
            self.colormaps.setFocus()
        elif e.key() == Qt.Key_Escape:
            # Pressing escape to refocus back to the main frame
            self.main_frame.setFocus()
        elif e.key() in [Qt.Key_H, Qt.Key_J, Qt.Key_K, Qt.Key_L]:
            self.set_stats_info(self.dc.moveView(e.key()))
            self.render_view()
            self.render_edited_cells()
        else:
            self.dc.updateCursorPosition(e)
            self.draw_cursor()

    def create_main_window(self):
        """
        This function creates the main window of the program.
        """
        self.main_frame = QWidget()
        self.main_frame.setMinimumSize(QSize(700, 700))

        self.dpi = 100
        self.fig = plt.Figure((6, 6), dpi=self.dpi, facecolor='w', edgecolor='w')
        self.canvas = FigureCanvas(self.fig)
        self.canvas.setParent(self.main_frame)

        # Stuff for the preview map >>>>>>>>>>>>>>>>>>>>>>>>>
        self.preview_frame = QWidget()
        self.preview_fig = plt.Figure((3, 1.6), dpi=self.dpi, facecolor='w', edgecolor='w')
        self.preview = FigureCanvas(self.preview_fig)
        self.preview.setParent(self.preview_frame)
        self.preview_axes = self.preview_fig.add_subplot(111)

        self.preview_fig.canvas.mpl_connect('button_press_event', self.onclick)
        self.preview_fig.subplots_adjust(top=1, bottom=0, left=0, right=1)
        # Stuff for the preview map <<<<<<<<<<<<<<<<<<<<<<<<<

        # Stuff for the colorbar >>>>>>>>>>>>>>>>>>>>>>>>>
        self.colorbar_frame = QWidget()
        self.colorbar_fig   = plt.Figure((3,0.4), dpi=self.dpi, facecolor='w', edgecolor='w')
        self.colorbar       = FigureCanvas(self.colorbar_fig)
        self.colorbar.setParent(self.colorbar_frame)
        self.colorbar_axes  = self.colorbar_fig.add_subplot(111)
        self.colorbar_fig.subplots_adjust(top=1.0, bottom=0.35, left=0.02, right=0.97) #Tightening the area around the subplot
        self.colorbar_fig.patch.set_facecolor('none') # Making the figure background transparent

        self.colorbar_axes.get_xaxis().set_visible(False)
        self.colorbar_axes.get_yaxis().set_visible(False)
        # Stuff for the colorbar <<<<<<<<<<<<<<<<<<<<<<<<<

        # Since we have only one plot, we can use add_axes
        # instead of add_subplot, but then the subplot
        # configuration tool in the navigation toolbar wouldn't
        # work.
        #
        self.axes = self.fig.add_subplot(111)
        # Turning off the axes ticks to maximize space. Also the labels were meaningless
        # anyway because they were not representing the actual lat/lons.
        self.axes.get_xaxis().set_visible(False)
        self.axes.get_yaxis().set_visible(False)

        # Other GUI controls
        # Information section
        font = QFont("SansSerif", 14)

        # STATISTICS >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        self.statdisplay = QLabel("Statistics:")
        self.statgrid    = QGridLayout()
        self.statgrid.setSpacing(5)
        w = QLabel("View"); w.setFont(font)
        self.statgrid.addWidget(w,  1, 1, Qt.AlignCenter)
        w = QLabel("Global"); w.setFont(font)
        self.statgrid.addWidget(w, 1, 2, Qt.AlignCenter)

        for i, name in enumerate(["Minimum", "Maximum", "Mean"]):
            w = QLabel(name)
            w.setFont(font)
            self.statgrid.addWidget(w, i+2, 0, Qt.AlignLeft)

        self.statsarray = []
        for i in range(6): self.statsarray.append(QLabel(''))

        self.statsarray[3].setText("{0:3d}".format(int(self.dc.data.min())))
        self.statsarray[4].setText("{0:3d}".format(int(self.dc.data.max())))
        self.statsarray[5].setText("{0:3d}".format(int(self.dc.data.mean())))

        for i in range(3):
            self.statgrid.addWidget(self.statsarray[i], i+2, 1, Qt.AlignCenter)
            self.statgrid.addWidget(self.statsarray[i+3], i+2, 2, Qt.AlignCenter)
            self.statsarray[i].setFont(font)
            self.statsarray[i+3].setFont(font)

        # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

        # PIXEL INFO >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        self.infodisplay = QLabel("Pixel Information:")
        self.infogrid    = QGridLayout()
        self.infogrid.setSpacing(5)

        for i, name in enumerate(["Lat", "Lon", "KMT", "I, J"]):
            w = QLabel(name)
            w.setFont(font)
            # w.setLineWidth(1)
            # w.setFrameShape(QFrame.Box)
            self.infogrid.addWidget(w, i+2, 0, Qt.AlignLeft)

        self.latdisplay  = QLabel("")
        self.londisplay  = QLabel("")
        self.valdisplay  = QLabel("")
        self.idxdisplay  = QLabel("")
        for i,w in enumerate([self.latdisplay, self.londisplay, self.valdisplay, self.idxdisplay]):
            self.infogrid.addWidget(w, i+2, 1, Qt.AlignLeft)
            w.setFont(font)
        # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

        # Colorscheme selector
        cmap_label = QLabel('Colorscheme:')
        self.colormaps = QComboBox(self)
        self.colormaps.addItems(self.maps)
        self.colormaps.setCurrentIndex(self.maps.index('Set1'))
        self.colormaps.currentIndexChanged.connect(self.render_view)

        # New value editor
        valhbox = QHBoxLayout()
        w = QLabel("Enter new value: ")
        w.setFont(font)
        valhbox.addWidget(w)
        self.inputbox = QLineEdit()
        self.inputbox.returnPressed.connect(self.update_value)
        valhbox.addWidget(self.inputbox)

        for item in [self.statdisplay, self.infodisplay, self.latdisplay, self.londisplay, self.valdisplay, self.idxdisplay, cmap_label]:
            item.setFont(font)

        vbox = QVBoxLayout()
        vbox.addWidget(self.canvas)

        vbox2 = QVBoxLayout()
        vbox2.addWidget(self.preview)           # Adding preview window

        # Horizontal line
        fr = QFrame(); fr.setLineWidth(0.5); fr.setFrameShape(QFrame.HLine)
        vbox2.addWidget(fr)
        vbox2.setAlignment(fr, Qt.AlignTop)

        # databox is a horizontal box for the displayed "information"
        databox  = QHBoxLayout()
        statvbox = QVBoxLayout()    # This is the sub-box for the statistics
        statvbox.addWidget(self.statdisplay)
        statvbox.setAlignment(self.statdisplay, Qt.AlignTop)
        statvbox.addLayout(self.statgrid)
        databox.addLayout(statvbox)

        fr = QFrame(); fr.setLineWidth(0.5); fr.setFrameShape(QFrame.VLine)
        databox.addWidget(fr)

        pixelvbox = QVBoxLayout()   # This is another sub-box of databox for the pixel info
        pixelvbox.addWidget(self.infodisplay)
        pixelvbox.setAlignment(self.infodisplay, Qt.AlignTop)
        pixelvbox.addLayout(self.infogrid)
        databox.addLayout(pixelvbox)

        vbox2.addLayout(databox)

        # Horizontal line
        fr = QFrame(); fr.setLineWidth(0.5); fr.setFrameShape(QFrame.HLine)
        vbox2.addWidget(fr)
        vbox2.setAlignment(fr, Qt.AlignTop)

        vbox2.addLayout(valhbox, Qt.AlignTop)

        vbox2.addStretch(1)
        vbox2.addWidget(cmap_label)           # Adding the colormap label
        vbox2.addWidget(self.colormaps)       # Adidng the colormap selector
        vbox2.addWidget(self.colorbar)
        vbox2.setAlignment(self.colorbar, Qt.AlignCenter)
        vbox2.addStretch(1)

        helpbox = QVBoxLayout()

        help_title = QLabel("Help")
        font = QFont("SansSerif", 14)
        font.setBold(True)
        help_title.setFont(font)

        helpbox.addWidget(help_title); helpbox.setAlignment(help_title, Qt.AlignTop)

        helpgrid = QGridLayout()
        helpgrid.setSpacing(5)

        helpgrid.addWidget(QLabel("Key"),                 0, 0, 1, 1,Qt.AlignLeft)
        helpgrid.addWidget(QLabel("Action"),              0, 1, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("up, down,left,right"), 1, 0, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("move cursor"),         1, 1, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("h,j,k,l"),             2, 0, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("move view"),           2, 1, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("a"),                   3, 0, 1, 1, Qt.AlignLeft)
        # helpgrid.addWidget(QLabel("replace cell value with 9 point average"),         3, 1, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("replace cell value with 9\npoint average"),         3, 1, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("="),                   4, 0, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("enter new value for cell"),         4, 1, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("c"),                   5, 0, 1, 1, Qt.AlignLeft)
        # helpgrid.addWidget(QLabel("copy value under cursor into buffer"),         5, 1, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("copy value under cursor\ninto buffer"),         5, 1, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("v"),                   6, 0, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("paste value in buffer"),6, 1, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("m"),                   7, 0, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("move focus to color selector"),         7, 1, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("Escape"), 8, 0, 1, 1, Qt.AlignLeft)
        helpgrid.addWidget(QLabel("move focus to main view"),         8, 1, 1, 1, Qt.AlignLeft)

        helpbox.addLayout(helpgrid); helpbox.setAlignment(helpgrid, Qt.AlignTop)

        vbox2.addLayout(helpbox)
        vbox2.setAlignment(helpbox, Qt.AlignBottom)

        hbox = QHBoxLayout()
        hbox.addLayout(vbox)
        hbox.addLayout(vbox2)

        self.main_frame.setLayout(hbox)
        self.setCentralWidget(self.main_frame)
        self.main_frame.setFocus()

    def draw_preview_worldmap(self):
        """
        This function draws the world map in the preview window on the top right hand corner
        of the application.
        """
        m = Basemap(projection='cyl', lon_0=180, llcrnrlat=-90,urcrnrlat=90,\
            llcrnrlon=0,urcrnrlon=360,resolution='c', ax=self.preview_axes)
        self.preview_axes.set_xlim([0,360])

        m.drawcoastlines(linewidth=0.5)
        m.fillcontinents()
        self.preview_axes.set_ylim([-90,90])

    def draw_colorbar(self):
        """
        This function draws the colorbar and the labels for the colorbar.
        """
        # First clear the colorbar axes
        self.colorbar_axes.cla()
        # Get the current selected colormap
        cmap = mpl.cm.get_cmap(self.maps[self.colormaps.currentIndex()])
        # Plot the colormap
        self.colorbar_axes.imshow(self.colorbar_gradient, aspect='auto', cmap=cmap)
        pos = list(self.colorbar_axes.get_position().bounds)
        dx = (pos[2])/6.  # The spacing between the labels

        if self.colorbarlabels != []:
            # Delete and clear the text labels
            while self.colorbarlabels: self.colorbarlabels.pop().remove()

        for i,l in enumerate([1,10,20,30,40,50,60]):
            self.colorbarlabels.append(self.colorbar_fig.text(pos[0]+(i*dx), 0.01, str(l), va='bottom', ha='center', fontsize=10))
        self.colorbar.draw()

    def draw_cursor(self, noremove=False):
        if self.cursor.marker and (not noremove): self.cursor.marker.remove()
        # The increment by 0.5 below is done so that the center of the marker is shifted
        # so that the top left corner of the square marker conicides with the top right corner
        # of each pixel. The value of 0.5 comes simply because each pixel are offset by 1 in each dimension.
        _cx, _cy = self.cursor.x+0.5, self.cursor.y+0.5
        self.cursor.marker = self.axes.scatter(_cx, _cy, s=55,
                             marker='s', edgecolor="k", facecolor='none', linewidth=2)
        self.set_information(self.cursor.y, self.cursor.x)
        self.canvas.draw()

    def render_edited_cells(self):
        """
        This function draws a box around cells that have been edited, thereby highlighting
        them from the other cells.
        """
        changes_row_idx = self.dc.changes_row_idx

        # We only need to go ahead if a change has been made, i.e. changes_row_idx is larger than 0
        if changes_row_idx > 0:
            # We are creating simple views to make the code for this function simpler
            change_rows = self.dc.changes[:self.dc.changes_row_idx, 0].view()  # A view on rows indices that have been changed
            change_cols = self.dc.changes[:self.dc.changes_row_idx, 1].view()  # A view on column indices that have been changed

            # Retrieving some class variables and storing for speed
            si    = self.dc.si
            sj    = self.dc.sj
            nrows = self.dc.nrows
            ncols = self.dc.ncols

            # We select the indices that lie within the view box
            indices_of_interest = np.logical_and(np.logical_and(change_rows >= si,
                                                                change_rows <= (si+nrows)),
                                                 np.logical_and(change_cols >= sj,
                                                                change_cols <= (sj+ncols)))

            _i = change_rows[indices_of_interest] + 0.5 - si
            _j = change_cols[indices_of_interest] + 0.5 - sj

            self.axes.scatter(_j, _i, s=38, marker='s', edgecolor="k", facecolor='none', linewidth=1)
            self.canvas.draw()

    def render_view(self):
        self.draw_colorbar()
        self.axes.clear()
        # Either select the colormap through the combo box or specify a custom colormap
        cmap = mpl.cm.get_cmap(self.maps[self.colormaps.currentIndex()])
        self.axes.pcolor(self.dc.view_masked, cmap=cmap, edgecolors='w', linewidths=0.5,
                         vmin=KMTEditor.KMT_MIN_VAL, vmax=KMTEditor.KMT_MAX_VAL)

        tmp1 = self.dc.nrows
        tmp2 = self.dc.ncols

        # This is for drawing the black contour line for the continents
        self.axes.contour(self.dc.view, levels=[0], colors='k', linewidth=1.5,
                          interpolation="nearest", origin="lower",
                          extents=[0.5,tmp2+0.5, 0.5, tmp1+0.5])

        # Setting the axes limits. This helps in setting the right orientation of the plot
        # and in clontrolling how much extra space we want around the scatter plot.
        # I am putting 4% space around the scatter plot
        self.axes.set_ylim([int(tmp1*1.02), 0 - int(tmp1*0.02)])
        self.axes.set_xlim([0 - int(tmp2*0.02), int(tmp2*1.02)])
        self.canvas.draw()
        self.fig.tight_layout()
        self.draw_cursor(noremove=True)

    def update_value(self, inp=None):
        if inp == None:
            inp = self.inputbox.text()   # Get the value in the text box

        if self.InputValueIsAcceptable(inp):
            self.dc.modifyValue(inp)     # Modify the data array
            self.unsaved_changes_exist = True
            self.buffer_value = inp
            self.statusBar().showMessage('Value changed: {0}'.format(inp), 2000)
            self.set_stats_info(self.dc.getViewStatistics())
            self.inputbox.clear()        # Now clear the input box
            self.render_view()           # Render the new view (which now contains the updated value)
            self.render_edited_cells()
            self.main_frame.setFocus()   # Bring focus back to the view

    def update_value_with_average(self, val):
        """
        Updates the value at the current cursor position using the input val.
        """
        self.dc.modifyValue(val)     # Modify the data array
        self.unsaved_changes_exist = True
        self.statusBar().showMessage('Value changed: {0}'.format(val), 2000)
        self.set_stats_info(self.dc.getViewStatistics())
        self.render_view()           # Render the new view (which now contains the updated value)
        self.render_edited_cells()

    def InputValueIsAcceptable(self, inp):
        """ This functions checks to see if the user has entered a valid
            vlaue kmt value. Returns true if the input value is acceptable. """
        tt = float(inp)
        if (tt != int(tt)):
            msg = "KMT level must be an integer value\nYou entered {0}".format(inp)
            QMessageBox.critical(self, "Invalid Value", msg.strip())
            return False

        if ((tt < 0) or (tt > 60)):
            msg = "KMT level values must be between 0 and 60.\nYou entered {0}".format(inp)
            QMessageBox.critical(self, "Invalid Value", msg.strip())
            return False
        return True

    def set_information(self, i, j):
        """ Sets the displayed information about the pixel in the right sidebar.
        ARGUMENTS
            i, j : the local (i.e. DataContainer) 0-based indices for the element
        """
        i_global, j_global = self.dc.viewIndex2GlobalIndex(i, j) # Convert local indices to global indices
        self.latdisplay.setText("{0:7.3f}".format(self.dc.kmt_lats[i_global, j_global]))
        self.londisplay.setText("{0:7.3f}".format(self.dc.kmt_lons[i_global, j_global]))
        self.valdisplay.setText("{0:3d}".format(int(self.dc.data[i_global, j_global])))
        self.idxdisplay.setText("{0:3d},{1:3d}".format(int(i_global), int(j_global)))

    def set_stats_info(self, s):
        """
        Updates the statistics display panel with the stats for the view.
        ARGUMENTS
            s - a tuple with the min, max and mean of the view
        """
        self.statsarray[0].setText("{0:3d}".format(int(s[0])))
        self.statsarray[1].setText("{0:3d}".format(int(s[1])))
        self.statsarray[2].setText("{0:3d}".format(int(s[2])))

    def onclick(self, event):
        QMessageBox.information(self, "", "KMTEditor does not presently support mouse selection over the preview plot")

    def on_about(self):
        msg = """ Edit KMT levels for the POP ocean model.  """
        QMessageBox.about(self, "About", msg.strip())

    def save_data(self):
        """
        Saves the data to a netCDF4 file. The program tries to construct a default format of
        output filename, but if it exists, it asks the user whether to overwrite it. If the
        user does not want to overwrite, she is asked to enter a new filename.
        """

        # We construct a standard output name of the form: inputname_fixed.nc
        if self.ofile is None: self.ofile = self.dc.fname.split(".")[0]+"_fixed.nc"

        # Now we check if the file exists. Then we must ask the user if she wants to overwrite the file
        if not self.asked_about_overwrite_permission:
            clobber = False
            if os.path.exists(self.ofile):
                reply = QMessageBox.question(self, 'Saving file...', "The output file {0} exists. Overwrite?".format(self.ofile),
                        QMessageBox.Yes | QMessageBox.No, QMessageBox.No)

                if reply == QMessageBox.Yes: clobber=True

                if not clobber:
                    # The use has responded to not overwrite, we must now ask for a new filename
                    ok = False
                    erromsg = ""
                    # This loop continues until an acceptable filename is entered, or the user presses cancel
                    while not ok:
                        _inp, ok = QInputDialog.getText(self, "Saving file...", "{0}Enter name of a different output file:".format(erromsg),)
                        if ok:
                            try:
                                self.ofile = str(_inp)
                            except:
                                ok = False
                                erromsg = "File name not acceptable. "
                            if os.path.exists(self.ofile):
                                ok = False
                                erromsg = "File exists. "
                        else:
                            self.ofile = None
                            self.statusBar().showMessage('Save cancelled', 2000)
                            return

            # To simplify creation of the output filename, i first duplicate the original file, then overwrite
            # the kmt array.
            nccopy(self.dc.fname, self.ofile, quiet=True, clobber=clobber, zlib=False, shuffle=False, classic=1)

            # now we set this to True, so that next time we save, we are not prompted about existing file
            self.asked_about_overwrite_permission = True

        ncfile = Dataset(self.ofile, "a", format="NETCDF4")
        alldimensions = ncfile.dimensions
        allvariables  = ncfile.variables
        if not "changes" in alldimensions: ncfile.createDimension("changes", None)
        if not "i" in alldimensions: ncfile.createDimension("i", 3)

        if not "original_kmt" in allvariables:
            okmt   = ncfile.createVariable("original_kmt", "f4", ("latitude", "longitude"))
            okmt.description   = "Original KMT data from which this file was created by KMTEditor.py"
            okmt[:,:]          = np.flipud(self.dc.orig_data)

        if not "changes" in allvariables:
            cngvar = ncfile.createVariable("changes", "f8", ("changes", "i"))
            cngvar.description = "changes to original data leading to present data. (i,j,val)"
            cngvar[:,:]        = self.dc.changes[0:self.dc.changes_row_idx,:]
        else:
            cngvar = allvariables["changes"]
            # s = cngvar.shape[0]
            cngvar[:,:]       = self.dc.changes[0:self.dc.changes_row_idx,:]

        kmtvar             = ncfile.variables["kmt"]
        kmtvar[:,:]        = np.flipud(self.dc.data)
        kmtvar.description = "Created by KMTEditor.py"

        ncfile.history     = "Created by KMTEditor.py"
        ncfile.input_file  = self.dc.fname
        ncfile.created     = time.ctime()
        ncfile.close()

        self.unsaved_changes_exist = False
        self.statusBar().showMessage('Saved to file: %s' % self.ofile, 2000)

    def create_action(self, text, slot=None, shortcut=None,
                      icon=None, tip=None, checkable=False,
                      signal="triggered()"):
        action = QAction(text, self)
        if icon is not None:
            action.setIcon(QIcon(":/%s.png" % icon))
        if shortcut is not None:
            action.setShortcut(shortcut)
        if tip is not None:
            action.setToolTip(tip)
            action.setStatusTip(tip)
        if slot is not None:
            self.connect(action, SIGNAL(signal), slot)
        if checkable:
            action.setCheckable(True)
        return action

    def add_actions(self, target, actions):
        for action in actions:
            if action is None:
                target.addSeparator()
            else:
                target.addAction(action)

    def create_menu(self):
        self.file_menu = self.menuBar().addMenu("&File")

        load_file_action = self.create_action("&Save Data",
            shortcut="Ctrl+S", slot=self.save_data,
            tip="Save the data array")

        self.add_actions(self.file_menu, (load_file_action,))

        self.help_menu = self.menuBar().addMenu("&Help")
        about_action = self.create_action("&About",
            shortcut='F1', slot=self.on_about,
            tip='About KMTEditor')
        self.add_actions(self.help_menu, (about_action,))

    def closeEvent(self, event):
        if self.unsaved_changes_exist:
            reply = QMessageBox.question(self, 'Message', "There are unsaved changes. Are you sure you want to quit?",
                    QMessageBox.Yes | QMessageBox.No, QMessageBox.No)

            if reply == QMessageBox.Yes:
                event.accept()
            else:
                event.ignore()

def main():
    app = QApplication([])   # Create an application
    app.setWindowIcon(QIcon('Resources/kmticon.png'))

    parser = argparse.ArgumentParser(description='KMTEditor', add_help=False)
    parser.add_argument('fname', nargs=1, type=str, help='name of the netcdf4 data file')
    parser.add_argument('-s',    nargs=1, type=int, help='size of the view in number of pixels', default=[60])
    args = parser.parse_args()

    mw = KMTEditor(args.fname[0], "kmt", dwx=args.s[0], dwy=args.s[0])
    mw.show()     # Render the window
    mw.raise_()   # Bring the PyQt4 window to the front
    app.exec_()   # Run the application loop

if __name__ == "__main__":
    main()


import multiprocessing as mp
import time

from itertools import cycle
from pathlib import Path
from tkinter import filedialog, Tk

import imagej
import psutil
from colorama import Fore

ij_path = "C:\\Users\\User\\Desktop\\Fiji.app"

class ParallelIJ:
    """
    Class to execute ImageJ macro in parallel.
    Automatic dispatch of files across ImageJ instances.

    Takes in paths of input/output directories.
    Macro is expected to be one that runs on an opened image (NOT batch).

    """
    TYPES = ['.tif', '.tiff']

    def __init__(self, *, in_path, out_path, macro_path, out_prefix, out_suffix, ij_path=None):
        self.in_path = Path(in_path)
        self.out_path = Path(out_path)
        self.macro = Path(macro_path).read_text()
        self.check_macro()
        self.ij_path = Path(ij_path)
        self.out_prefix = out_prefix
        self.out_suffix = out_suffix

        self.files = [f for f in incoming.iterdir() if f.suffix.lower() in ParallelIJ.TYPES]
        self.proc_num = min([psutil.cpu_count(logical=False), len(self.files)])
        m = mp.Manager()
        self.q_in = m.Queue()
        self.q_out = m.Queue()

        [self.q_in.put(f) for f in self.files]

    def check_args(self):
        if len(self.files) == 0:
            raise FileNotFoundError(f'No file in the selected directory that ends with {ParallelIJ.TYPES}.')

    def check_macro(self):
        """
        Check if macro contains line that starts with {checks}.
        Designed to prevent inadvertent run of batch macro.
        """
        lines = self.macro.split('\n')
        checks = ['run("Bio-Formats Importer"', 'open', 'saveAs']
        for check in checks:
            if any([line.startswith(check) for line in lines]):
                print(Fore.RED + f'Macro contains {check} command. Make sure that the macro is prepared properly.' + Fore.RESET)

    def build_macro(self, file: Path):
        """
        Encapsulates macro code with open and save clauses.

        :param file: Path object of file to be run.
        :return: ImageJ macro code with open and save clauses.
        """
        out_name = f'{self.out_prefix}{file.stem}{self.out_suffix}.tif'
        start = f'setBatchMode(true);\n' \
                f'open("{file.as_posix()}");\n'
        end = f'\nsaveAs("tiff", "{self.out_path.as_posix()}/{out_name}");\n' \
              f'run("Close All");\n'

        return f'{start}{self.macro}{end}'

    def ij_instance(self, q_in: mp.Queue, q_out: mp.Queue):
        """
        Function to be spawned in parallel with separate processes.
        Initializes ImageJ and feed in macro code with file names from multiprocessing.Queue.

        :param q_in: Input file names
        :param q_out: Mainly to track progress
        """
        # Use latest Fiji as default.
        ij = imagej.init('sc.fiji:fiji') if self.ij_path is None else imagej.init(self.ij_path.as_posix())
        q_out.put('ready')
        while q_in.qsize() > 0:
            file = q_in.get()
            ij.py.run_macro(self.build_macro(file))
            q_out.put(file)

    def start(self):
        """ Starts the processing pool. """
        pool = mp.Pool()
        [pool.apply_async(func=self.ij_instance, args=(self.q_in, self.q_out,)) for _ in range(self.proc_num)]
        pool.close()

def dialogs(*, type_, title):
    assert type_ in ['file', 'folder']
    temp = Path(filedialog.askopenfilename(title=title)) if type_ == 'file' else Path(filedialog.askdirectory(title=title))
    if temp == Path():
        raise Exception
    return temp

if __name__ == '__main__':
    """ User interface """
    root = Tk()
    root.withdraw()
    macro_path = dialogs(type_='file', title="Choose the macro file")
    incoming = dialogs(type_='folder', title="Choose input directory")
    outgoing = dialogs(type_='folder', title="Choose output directory")

    print(f'Input: {incoming}')
    print(f'Output: {outgoing}\n')

    out_prefix = input('Add the following to the BEGINNING of output file name: ')
    out_suffix = input('Add the following to the END of output file name: ')
    if out_suffix == '' and out_prefix == '':
        print(Fore.RED + f'Warning: No prefix and suffix. Outputs may overwrite existing image.' + Fore.RESET)

    parallel = ParallelIJ(in_path=incoming, out_path=outgoing, macro_path=macro_path, ij_path=ij_path,
                          out_prefix=out_prefix,  out_suffix=out_suffix)

    incoming_n = len(parallel.files)
    outgoing_n = len(list(outgoing.iterdir()))

    print(f'\nRun {incoming_n} files with extensions {ParallelIJ.TYPES}')
    print(f'Output file name will be [{out_prefix}FILE_NAME{out_suffix}.tif]')

    proceed = input('Proceed? If yes, type y: ')
    print()
    if proceed.lower() not in ['y', 'yes', 'affirmative', 'sure', 'yeah', 'absolutely', 'indeed', 'agreed', 'ok', 'ay']:
        raise KeyboardInterrupt('Aborted.')

    parallel.start()
    t_start = time.time()

    print('Initializing ImageJ ...')
    for i in range(parallel.proc_num):
        parallel.q_out.get()

    cursor = (c for c in cycle('|/-\\'))
    while parallel.q_out.qsize() < incoming_n:
        time.sleep(0.25)
        t_elapsed = time.time() - t_start
        print(f'\r{next(cursor)} {parallel.q_out.qsize()}/{incoming_n} files. '
              f'Elapsed time: {time.strftime("%H:%M:%S", time.gmtime(t_elapsed))}', flush=True, end='')

    print('\nAll done!')


# -*- coding: utf-8 -*-
"""
Created on Sat Dec  5 13:42:33 2020
@author: Asus
"""
import cv2
import face_recognition
import os
import glob
import shutil
from tkinter import filedialog
from tkinter import Tk
persons_list = []
index = {}
potraits = {}
faces_in_image = {}
location=''
def select():
    root = Tk()
    root.withdraw()
    folder_selected = filedialog.askdirectory()
    global dir_path
    dir_path = folder_selected+'/'
    global location
    location=folder_selected

def sort(file_name):
    '''
    Checks if a given file contains faces, if any new found, then add to persons_list
    '''
    global persons_list, index
    image = face_recognition.load_image_file(dir_path + file_name)
    try:
        face_encodings = face_recognition.face_encodings(image)
        faces_in_image[file_name] = len(face_encodings)
        print(f'{len(face_encodings)} faces found in {file_name}')
    except IndexError:
        print("I wasn't able to locate any faces in at least one of the images.")
    if face_encodings:
        if not persons_list:
            persons_list += face_encodings
            for i, f in enumerate(face_encodings):
                index[f'person{i+1}'] = [file_name]

        else:
            for f in face_encodings:
                results = face_recognition.compare_faces(
                    persons_list, f, tolerance=0.6)

                if True not in results:
                    persons_list.append(f)

                    index[f'person{len(index)+1}'] = [file_name]

                else:
                    ind = results.index(True)
                    index[f'person{ind+1}'].append(file_name)

def getFiles():
    files = []
    with os.scandir(dir_path) as entries:
        for entry in entries:
            if entry.is_file():
                files.append(entry.name)
    if '.DS_Store' in files:
        files.remove('.DS_Store')
    return files

def move():
    if (1 in faces_in_image.values()):
        print('\nMoving...')
        potraits_list = []
        for file, faces in faces_in_image.items():
            if faces == 1:
                potraits_list.append(file)
        for p in potraits_list:
            for person, files in index.items():
                if p in files:
                    if person not in potraits.keys():
                        potraits[person] = [p]
                    else:
                        potraits[person].append(p)
        for person, files in potraits.items():
            os.mkdir(dir_path + person)
            for file in files:
                shutil.move(dir_path + file, dir_path + person)
                print(f'Moving {file} to {dir_path + person}')
    else:
        print('\nNo potraits found!')

def displayIndex():
    for person, files in index.items():
        print(f'\n{person} has {len(files)} images:')
        for i, file in enumerate(files):
            print(f'  ({i+1}) {file}')

def face_find():
    folder_selected = dir_path
    print(os.getcwd())
    os.chdir(folder_selected)
    newpath=folder_selected+'/Solo shots'
    photos=glob.glob(folder_selected+'/*.jpg')
    i=1
    face_cascade=cv2.CascadeClassifier('C:\ProgramData\Anaconda3\Lib\site-packages\cv2\data\haarcascade_frontalface_default.xml')
    for photo in photos:
        print(str(i)+'.'+photo)
        photopath=os.path.join(newpath,photo)
        print(photopath)
        img=cv2.imread(photopath,cv2.IMREAD_UNCHANGED)
        scale_percent=20;
        width=int(img.shape[1]*scale_percent/100)
        height=int(img.shape[0]*scale_percent/100)
        dim=(width,height)
        rimage=cv2.resize(img,dim,interpolation=cv2.INTER_AREA)
        gray=cv2.cvtColor(rimage,cv2.COLOR_BGR2GRAY)
        faces=face_cascade.detectMultiScale(gray,1.04,4)
        numface=len(faces)
        if(numface>0):
            if not os.path.isdir(newpath):
                os.mkdir(folder_selected+'/Solo shots')
            shutil.copy(photopath,newpath)
        i=i+1

def person_sort():
    files = getFiles()
    for file in files:
        sort(file)
    print(f'\n[ {len(persons_list)} unique faces found in total ]\n')
    displayIndex()
    move()


#!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging
from .exception import *
from .util import get_module_logger
from .avr import YamahaAPI

# Register a 'do-nothing' handler to avoid the
# 'No handlers could be found for logger "xyz"' message
# in case of log messages >= WARNING and the client didn't configure logging
get_module_logger().addHandler(logging.NullHandler())

__all__ = [
    'AVRException',
    'CommandNotAvailableException',
    'ConfigException',
    'ParameterMissingException',
    'RequestException',
    'ResponseException',
    'ValueNotSupportedException',
    'YamahaAPI',
]


#!/usr/bin/env python
#

import os, sys, re, json, numpy, astropy, matplotlib, subprocess
#matplotlib.use('Qt5Agg')

from astropy.table import Table
from astropy import units as u
from matplotlib import pyplot as plt
from matplotlib import ticker as ticker
import numpy as np
from pprint import pprint

from astropy.cosmology import FlatLambdaCDM
cosmo = FlatLambdaCDM(H0=70, Om0=0.27, Tcmb0=2.725)

sys.path.append('/Users/dzliu/Cloud/GitLab/AlmaCosmos/Plot/Common_Python_Code')

from setup_matplotlib import setup_matplotlib; setup_matplotlib()
#matplotlib.rcParams['text.usetex'] = True
#matplotlib.rcParams['text.latex.preamble'] = [r'\usepackage[cm]{sfmath}']
#matplotlib.rcParams['font.sans-serif'] = 'cm'
#matplotlib.rcParams['font.family'] = 'sans-serif'

from calc_galaxy_main_sequence import (
                                        calc_SFR_MS_Speagle2014,
                                        calc_SFR_MS_Sargent2014,
                                        calc_SFR_MS_Whitaker2014,
                                        calc_SFR_MS_Bethermin2015,
                                        calc_SFR_MS_Schreiber2015,
                                        calc_SFR_MS_Lee2015,
                                        calc_SFR_MS_Tomczak2016,
                                        calc_SFR_MS_Pearson2018,
                                        calc_SFR_MS_Leslie20180901,
                                        calc_SFR_MS_Leslie20190111,
                                        calc_SFR_MS_Leslie20190515,
                                        calc_SFR_MS_Leslie20190710,
                                        calc_SFR_MS_Scoville2017,
                                        )

from calc_galaxy_stellar_mass_function import (calc_SMF_Davidzon2017, calc_SMF_Ilbert2013, calc_SMF_Peng2010, calc_SMF_Wright2018_single_component, calc_SMF_Wright2018_double_component, calc_SMF_dzliu2018)

#
# Read data points
#
tb = Table.read('datatable_generated_galaxies_with_coordinates.fits')
data_lgMstar = tb['lgMstar'].data.flatten()
data_Mstar = 10**data_lgMstar
data_lgSFR = tb['lgSFR'].data.flatten()
data_SFR = 10**data_lgSFR
data_redshift = tb['z'].data.flatten()
#sys.exit()

#
# Read data2 points
#
tb2 = Table.read('/Users/dzliu/Work/AlmaCosmos/Catalogs/Galaxy_Modelling_SIDES/Mock_cat_Bethermin2017.fits')
data2_Mstar = tb2['MSTAR'].data.flatten()
data2_lgMstar = np.log10(data2_Mstar)
data2_SFR = tb2['SFR'].data.flatten()
data2_lgSFR = np.log10(data2_SFR)
data2_redshift = tb2['REDSHIFT'].data.flatten()

#sys.exit()

#
# Check MS
#
plot_label_MS = ''
current_dir = os.path.basename(os.getcwd())
if re.match(r'^.*_using_([a-zA-Z0-9]+)_MS([_].*|)$', current_dir):
    plot_label_MS = '(%s MS)'%(re.sub(r'^.*_using_([a-zA-Z0-9]+)_MS([_].*|)$', r'\1', current_dir))

z_edges = [0.02, 0.25, 0.50, 0.75, 1.00, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 6.0]
ncol = 4
nrow = int(np.ceil((len(z_edges)-1) / float(ncol)))

fig = plt.figure(figsize=(15.0, 10.0))
fig.subplots_adjust(left=0.10, right=0.99, bottom=0.10, top=0.99, wspace=0.20, hspace=0.30)

#
# loop z bin
for i in range(len(z_edges)-1):
    #
    print('z %s - %s'%(z_edges[i], z_edges[i+1]))
    #
    z = (z_edges[i]+z_edges[i+1])/2.0
    #
    #
    #
    # ax
    ax = fig.add_subplot(nrow, ncol, i+1)
    #
    # xylim
    #ax.set_xlim([-1.0, 4.0])
    #ax.set_ylim([-7.5, 0.5])
    ax.set_yscale('log')
    #
    #
    # prepare legend
    legend_handles = []
    legend_labels = []
    #
    #
    # select and plot data histogram
    data_selection = np.logical_and.reduce((data_redshift >= z_edges[i], data_redshift < z_edges[i+1], data_SFR > 0, data_lgMstar > 10.25, data_lgMstar < 10.75))
    bin_hists, bin_edges = np.histogram(data_lgSFR[data_selection], bins=np.arange(-1.0, 4.5, 0.1) )
    bin_centers = (bin_edges[0:-1]+bin_edges[1:])/2.0
    bin_widths = (bin_edges[1:]-bin_edges[0:-1])
    bin_mask = (bin_hists>0)
    plot_data_1 = ax.bar(bin_centers[bin_mask], bin_hists[bin_mask], width=bin_widths[bin_mask], alpha=0.6, align='edge' )
    label_data_1 = 'dzliu model'+'\n'+plot_label_MS+'\n'+'lgMstar=10.25-10.75'
    legend_handles.append(plot_data_1)
    legend_labels.append(label_data_1)
    #
    #
    # select and plot data histogram
    data2_selection = np.logical_and.reduce((data2_redshift >= z_edges[i], data2_redshift < z_edges[i+1], data2_SFR > 0, data2_lgMstar > 10.25, data2_lgMstar < 10.75))
    bin_hists, bin_edges = np.histogram(data2_lgSFR[data2_selection], bins=np.arange(-1.0, 4.5, 0.1) )
    bin_centers = (bin_edges[0:-1]+bin_edges[1:])/2.0
    bin_widths = (bin_edges[1:]-bin_edges[0:-1])
    bin_mask = (bin_hists>0)
    plot_data_2 = ax.bar(bin_centers[bin_mask], bin_hists[bin_mask], width=bin_widths[bin_mask], alpha=0.6, align='edge' )
    label_data_2 = 'Bethermin+2017\nlgMstar=10.25-10.75'
    legend_handles.append(plot_data_2)
    legend_labels.append(label_data_2)
    #
    # label current bin
    ax.text(0.97, 0.97, r'$z = %0.1f - %0.1f$'%(z_edges[i], z_edges[i+1]), transform=ax.transAxes, fontsize=16, va='top', ha='right')
    #ax.text(0.95, 0.15, r'$log_{10} M_{\star} = %0.1f - %0.1f$'%(10.25, 10.75), transform=ax.transAxes, fontsize=16, va='bottom', ha='right')
    #
    # ax tick params
    ax.tick_params(axis='both', which='both', direction='in', top=True, right=True)
    plt.xticks(fontsize=16)
    plt.yticks(fontsize=16)
    #plt.yscale('log')
    plt.xlabel(r'$\log_{10} (\mathrm{SFR})$', fontsize=20, labelpad=2)
    #
    # ylabel
    if i%ncol == 0 and int(i/ncol) == int((nrow-1)/2):
        plt.ylabel(r'$N$', fontsize=18)
    #
    # legend
    if i+1 == len(z_edges)-1:
        plot_legend1 = plt.legend(\
                        legend_handles,
                        legend_labels,
                        fontsize=16, loc='center left', bbox_to_anchor=(1.1, 0.0, 0.2, 1.0),
                        #borderpad=0.6, borderaxespad=0.6, handlelength=2.8,
                       )
        #leg.get_frame().set_edgecolor('#cccccc')
        #leg.get_frame().set_linewidth(2.0)
        #plt.setp(plot_legend1.get_texts(), fontsize='18')
        ax.add_artist(plot_legend1)
    #
    # show y minor ticks
    ax.xaxis.set_major_locator(ticker.MultipleLocator(base=1.0))
    ax.xaxis.set_minor_locator(ticker.MultipleLocator(base=0.1))
    #ax.yaxis.set_major_locator(ticker.MultipleLocator(base=1.0))
    #ax.yaxis.set_major_locator(ticker.LogLocator(base=10.0, numticks=99))
    #ax.yaxis.set_minor_locator(ticker.LogLocator(base=10.0, subs=np.arange(2,10)*0.1, numticks=99))
    #ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda y,pos: ('{{:.{:1d}f}}'.format(int(np.maximum(-np.log10(y),0)))).format(y))) # https://stackoverflow.com/questions/21920233/matplotlib-log-scale-tick-label-number-formatting
    #
    # xylim
    #ax.set_ylim([ax.get_ylim()[0], ax.get_ylim()[1]*1.5])
    #
    # grid
    ax.grid(True, ls='dotted', c='#cccccc')

#
# savefig
fig.savefig('Plot_SFR_Histogram_bin_by_z_comparing_with_Bethermin2017.pdf', transparent=True)
print('Output to "%s"!' % ('Plot_SFR_Histogram_bin_by_z_comparing_with_Bethermin2017.pdf') )
os.system('open "%s"' % ('Plot_SFR_Histogram_bin_by_z_comparing_with_Bethermin2017.pdf') )


import re
try:
    set
except NameError:
    from sets import Set as set

from pygments.lexer import RegexLexer, bygroups, using, include, this
from pygments.token import \
     Text, Comment, Operator, Keyword, Name, String, Number, Other, Punctuation
from pygments.util import get_bool_opt, get_list_opt, looks_like_xml, \
                          html_doctype_matches

__all__ = ['HtmlLexer', 'XmlLexer', 'JavascriptLexer', 'CssLexer',
           'PhpLexer', 'ActionScriptLexer', 'XsltLexer', 'ActionScript3Lexer',
           'MxmlLexer']

class JavascriptLexer(RegexLexer):
    """
    For JavaScript source code.
    """

    name = 'JavaScript'
    aliases = ['js', 'javascript']
    filenames = ['*.js']
    mimetypes = ['application/x-javascript', 'text/x-javascript', 'text/javascript']

    flags = re.DOTALL
    tokens = {
        'commentsandwhitespace': [
            (r'\s+', Text),
            (r'<!--', Comment),
            (r'//.*?\n', Comment.Single),
            (r'/\*.*?\*/', Comment.Multiline)
        ],
        'slashstartsregex': [
            include('commentsandwhitespace'),
            (r'/(\\.|[^[/\\\n]|\[(\\.|[^\]\\\n])*])+/'
             r'([gim]+\b|\B)', String.Regex, '#pop'),
            (r'(?=/)', Text, ('#pop', 'badregex')),
            (r'', Text, '#pop')
        ],
        'badregex': [
            ('\n', Text, '#pop')
        ],
        'root': [
            (r'^(?=\s|/|<!--)', Text, 'slashstartsregex'),
            include('commentsandwhitespace'),
            (r'\+\+|--|~|&&|\?|:|\|\||\\(?=\n)|'
             r'(<<|>>>?|==?|!=?|[-<>+*%&\|\^/])=?', Operator, 'slashstartsregex'),
            (r'[{(\[;,]', Punctuation, 'slashstartsregex'),
            (r'[})\].]', Punctuation),
            (r'(for|in|while|do|break|return|continue|switch|case|default|if|else|'
             r'throw|try|catch|finally|new|delete|typeof|instanceof|void|'
             r'this)\b', Keyword, 'slashstartsregex'),
            (r'(var|with|function)\b', Keyword.Declaration, 'slashstartsregex'),
            (r'(abstract|boolean|byte|char|class|const|debugger|double|enum|export|'
             r'extends|final|float|goto|implements|import|int|interface|long|native|'
             r'package|private|protected|public|short|static|super|synchronized|throws|'
             r'transient|volatile)\b', Keyword.Reserved),
            (r'(true|false|null|NaN|Infinity|undefined)\b', Keyword.Constant),
            (r'(Array|Boolean|Date|Error|Function|Math|netscape|'
             r'Number|Object|Packages|RegExp|String|sun|decodeURI|'
             r'decodeURIComponent|encodeURI|encodeURIComponent|'
             r'Error|eval|isFinite|isNaN|parseFloat|parseInt|document|this|'
             r'window)\b', Name.Builtin),
            (r'[$a-zA-Z_][a-zA-Z0-9_]*', Name.Other),
            (r'[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?', Number.Float),
            (r'0x[0-9a-fA-F]+', Number.Hex),
            (r'[0-9]+', Number.Integer),
            (r'"(\\\\|\\"|[^"])*"', String.Double),
            (r"'(\\\\|\\'|[^'])*'", String.Single),
        ]
    }

class ActionScriptLexer(RegexLexer):
    """
    For ActionScript source code.

    *New in Pygments 0.9.*
    """

    name = 'ActionScript'
    aliases = ['as', 'actionscript']
    filenames = ['*.as']
    mimetypes = ['application/x-actionscript', 'text/x-actionscript',
                 'text/actionscript']

    flags = re.DOTALL
    tokens = {
        'root': [
            (r'\s+', Text),
            (r'//.*?\n', Comment.Single),
            (r'/\*.*?\*/', Comment.Multiline),
            (r'/(\\\\|\\/|[^/\n])*/[gim]*', String.Regex),
            (r'[~\^\*!%&<>\|+=:;,/?\\-]+', Operator),
            (r'[{}\[\]();.]+', Punctuation),
            (r'(case|default|for|each|in|while|do|break|return|continue|if|else|'
             r'throw|try|catch|var|with|new|typeof|arguments|instanceof|this|'
             r'switch)\b', Keyword),
            (r'(class|public|final|internal|native|override|private|protected|'
             r'static|import|extends|implements|interface|intrinsic|return|super|'
             r'dynamic|function|const|get|namespace|package|set)\b',
             Keyword.Declaration),
            (r'(true|false|null|NaN|Infinity|-Infinity|undefined|Void)\b',
             Keyword.Constant),
            (r'(Accessibility|AccessibilityProperties|ActionScriptVersion|'
             r'ActivityEvent|AntiAliasType|ApplicationDomain|AsBroadcaster|Array|'
             r'AsyncErrorEvent|AVM1Movie|BevelFilter|Bitmap|BitmapData|'
             r'BitmapDataChannel|BitmapFilter|BitmapFilterQuality|BitmapFilterType|'
             r'BlendMode|BlurFilter|Boolean|ByteArray|Camera|Capabilities|CapsStyle|'
             r'Class|Color|ColorMatrixFilter|ColorTransform|ContextMenu|'
             r'ContextMenuBuiltInItems|ContextMenuEvent|ContextMenuItem|'
             r'ConvultionFilter|CSMSettings|DataEvent|Date|DefinitionError|'
             r'DeleteObjectSample|Dictionary|DisplacmentMapFilter|DisplayObject|'
             r'DisplacmentMapFilterMode|DisplayObjectContainer|DropShadowFilter|'
             r'Endian|EOFError|Error|ErrorEvent|EvalError|Event|EventDispatcher|'
             r'EventPhase|ExternalInterface|FileFilter|FileReference|'
             r'FileReferenceList|FocusDirection|FocusEvent|Font|FontStyle|FontType|'
             r'FrameLabel|FullScreenEvent|Function|GlowFilter|GradientBevelFilter|'
             r'GradientGlowFilter|GradientType|Graphics|GridFitType|HTTPStatusEvent|'
             r'IBitmapDrawable|ID3Info|IDataInput|IDataOutput|IDynamicPropertyOutput'
             r'IDynamicPropertyWriter|IEventDispatcher|IExternalizable|'
             r'IllegalOperationError|IME|IMEConversionMode|IMEEvent|int|'
             r'InteractiveObject|InterpolationMethod|InvalidSWFError|InvokeEvent|'
             r'IOError|IOErrorEvent|JointStyle|Key|Keyboard|KeyboardEvent|KeyLocation|'
             r'LineScaleMode|Loader|LoaderContext|LoaderInfo|LoadVars|LocalConnection|'
             r'Locale|Math|Matrix|MemoryError|Microphone|MorphShape|Mouse|MouseEvent|'
             r'MovieClip|MovieClipLoader|Namespace|NetConnection|NetStatusEvent|'
             r'NetStream|NewObjectSample|Number|Object|ObjectEncoding|PixelSnapping|'
             r'Point|PrintJob|PrintJobOptions|PrintJobOrientation|ProgressEvent|Proxy|'
             r'QName|RangeError|Rectangle|ReferenceError|RegExp|Responder|Sample|Scene|'
             r'ScriptTimeoutError|Security|SecurityDomain|SecurityError|'
             r'SecurityErrorEvent|SecurityPanel|Selection|Shape|SharedObject|'
             r'SharedObjectFlushStatus|SimpleButton|Socket|Sound|SoundChannel|'
             r'SoundLoaderContext|SoundMixer|SoundTransform|SpreadMethod|Sprite|'
             r'StackFrame|StackOverflowError|Stage|StageAlign|StageDisplayState|'
             r'StageQuality|StageScaleMode|StaticText|StatusEvent|String|StyleSheet|'
             r'SWFVersion|SyncEvent|SyntaxError|System|TextColorType|TextField|'
             r'TextFieldAutoSize|TextFieldType|TextFormat|TextFormatAlign|'
             r'TextLineMetrics|TextRenderer|TextSnapshot|Timer|TimerEvent|Transform|'
             r'TypeError|uint|URIError|URLLoader|URLLoaderDataFormat|URLRequest|'
             r'URLRequestHeader|URLRequestMethod|URLStream|URLVariabeles|VerifyError|'
             r'Video|XML|XMLDocument|XMLList|XMLNode|XMLNodeType|XMLSocket|XMLUI)\b',
             Name.Builtin),
            (r'(decodeURI|decodeURIComponent|encodeURI|escape|eval|isFinite|isNaN|'
             r'isXMLName|clearInterval|fscommand|getTimer|getURL|getVersion|'
             r'isFinite|parseFloat|parseInt|setInterval|trace|updateAfterEvent|'
             r'unescape)\b',Name.Function),
            (r'[$a-zA-Z_][a-zA-Z0-9_]*', Name.Other),
            (r'[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?', Number.Float),
            (r'0x[0-9a-f]+', Number.Hex),
            (r'[0-9]+', Number.Integer),
            (r'"(\\\\|\\"|[^"])*"', String.Double),
            (r"'(\\\\|\\'|[^'])*'", String.Single),
        ]
    }

    def analyse_text(text):
        return 0.05

class ActionScript3Lexer(RegexLexer):
    """
    For ActionScript 3 source code.

    *New in Pygments 0.11.*
    """

    name = 'ActionScript 3'
    aliases = ['as3', 'actionscript3']
    filenames = ['*.as']
    mimetypes = ['application/x-actionscript', 'text/x-actionscript',
                 'text/actionscript']

    identifier = r'[$a-zA-Z_][a-zA-Z0-9_]*'

    flags = re.DOTALL | re.MULTILINE
    tokens = {
        'root': [
            (r'\s+', Text),
            (r'(function\s+)(' + identifier + r')(\s*)(\()',
             bygroups(Keyword.Declaration, Name.Function, Text, Operator),
             'funcparams'),
            (r'(var|const)(\s+)(' + identifier + r')(\s*)(:)(\s*)(' + identifier + r')',
             bygroups(Keyword.Declaration, Text, Name, Text, Punctuation, Text,
                      Keyword.Type)),
            (r'(import|package)(\s+)((?:' + identifier + r'|\.)+)(\s*)',
             bygroups(Keyword, Text, Name.Namespace, Text)),
            (r'(new)(\s+)(' + identifier + r')(\s*)(\()',
             bygroups(Keyword, Text, Keyword.Type, Text, Operator)),
            (r'//.*?\n', Comment.Single),
            (r'/\*.*?\*/', Comment.Multiline),
            (r'/(\\\\|\\/|[^\n])*/[gisx]*', String.Regex),
            (r'(\.)(' + identifier + r')', bygroups(Operator, Name.Attribute)),
            (r'(case|default|for|each|in|while|do|break|return|continue|if|else|'
             r'throw|try|catch|with|new|typeof|arguments|instanceof|this|'
             r'switch|import|include|as|is)\b',
             Keyword),
            (r'(class|public|final|internal|native|override|private|protected|'
             r'static|import|extends|implements|interface|intrinsic|return|super|'
             r'dynamic|function|const|get|namespace|package|set)\b',
             Keyword.Declaration),
            (r'(true|false|null|NaN|Infinity|-Infinity|undefined|void)\b',
             Keyword.Constant),
            (r'(decodeURI|decodeURIComponent|encodeURI|escape|eval|isFinite|isNaN|'
             r'isXMLName|clearInterval|fscommand|getTimer|getURL|getVersion|'
             r'isFinite|parseFloat|parseInt|setInterval|trace|updateAfterEvent|'
             r'unescape)\b', Name.Function),
            (identifier, Name),
            (r'[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?', Number.Float),
            (r'0x[0-9a-f]+', Number.Hex),
            (r'[0-9]+', Number.Integer),
            (r'"(\\\\|\\"|[^"])*"', String.Double),
            (r"'(\\\\|\\'|[^'])*'", String.Single),
            (r'[~\^\*!%&<>\|+=:;,/?\\{}\[\]();.-]+', Operator),
        ],
        'funcparams': [
            (r'\s+', Text),
            (r'(\s*)(\.\.\.)?(' + identifier + r')(\s*)(:)(\s*)(' +
             identifier + r'|\*)(\s*)',
             bygroups(Text, Punctuation, Name, Text, Operator, Text,
                      Keyword.Type, Text), 'defval'),
            (r'\)', Operator, 'type')
        ],
        'type': [
            (r'(\s*)(:)(\s*)(' + identifier + r'|\*)',
             bygroups(Text, Operator, Text, Keyword.Type), '#pop:2'),
            (r'\s*', Text, '#pop:2')
        ],
        'defval': [
            (r'(=)(\s*)([^(),]+)(\s*)(,?)',
             bygroups(Operator, Text, using(this), Text, Operator), '#pop'),
            (r',?', Operator, '#pop')
        ]
    }

    def analyse_text(text):
        if re.match(r'\w+\s*:\s*\w', text): return 0.3
        return 0.1

class CssLexer(RegexLexer):
    """
    For CSS (Cascading Style Sheets).
    """

    name = 'CSS'
    aliases = ['css']
    filenames = ['*.css']
    mimetypes = ['text/css']

    tokens = {
        'root': [
            include('basics'),
        ],
        'basics': [
            (r'\s+', Text),
            (r'/\*(?:.|\n)*?\*/', Comment),
            (r'{', Punctuation, 'content'),
            (r'\:[a-zA-Z0-9_-]+', Name.Decorator),
            (r'\.[a-zA-Z0-9_-]+', Name.Class),
            (r'\#[a-zA-Z0-9_-]+', Name.Function),
            (r'@[a-zA-Z0-9_-]+', Keyword, 'atrule'),
            (r'[a-zA-Z0-9_-]+', Name.Tag),
            (r'[~\^\*!%&\[\]\(\)<>\|+=@:;,./?-]', Operator),
            (r'"(\\\\|\\"|[^"])*"', String.Double),
            (r"'(\\\\|\\'|[^'])*'", String.Single)
        ],
        'atrule': [
            (r'{', Punctuation, 'atcontent'),
            (r';', Punctuation, '#pop'),
            include('basics'),
        ],
        'atcontent': [
            include('basics'),
            (r'}', Punctuation, '#pop:2'),
        ],
        'content': [
            (r'\s+', Text),
            (r'}', Punctuation, '#pop'),
            (r'url\(.*?\)', String.Other),
            (r'^@.*?$', Comment.Preproc),
            (r'(azimuth|background-attachment|background-color|'
             r'background-image|background-position|background-repeat|'
             r'background|border-bottom-color|border-bottom-style|'
             r'border-bottom-width|border-left-color|border-left-style|'
             r'border-left-width|border-right|border-right-color|'
             r'border-right-style|border-right-width|border-top-color|'
             r'border-top-style|border-top-width|border-bottom|'
             r'border-collapse|border-left|border-width|border-color|'
             r'border-spacing|border-style|border-top|border|caption-side|'
             r'clear|clip|color|content|counter-increment|counter-reset|'
             r'cue-after|cue-before|cue|cursor|direction|display|'
             r'elevation|empty-cells|float|font-family|font-size|'
             r'font-size-adjust|font-stretch|font-style|font-variant|'
             r'font-weight|font|height|letter-spacing|line-height|'
             r'list-style-type|list-style-image|list-style-position|'
             r'list-style|margin-bottom|margin-left|margin-right|'
             r'margin-top|margin|marker-offset|marks|max-height|max-width|'
             r'min-height|min-width|opacity|orphans|outline|outline-color|'
             r'outline-style|outline-width|overflow|padding-bottom|'
             r'padding-left|padding-right|padding-top|padding|page|'
             r'page-break-after|page-break-before|page-break-inside|'
             r'pause-after|pause-before|pause|pitch|pitch-range|'
             r'play-during|position|quotes|richness|right|size|'
             r'speak-header|speak-numeral|speak-punctuation|speak|'
             r'speech-rate|stress|table-layout|text-align|text-decoration|'
             r'text-indent|text-shadow|text-transform|top|unicode-bidi|'
             r'vertical-align|visibility|voice-family|volume|white-space|'
             r'widows|width|word-spacing|z-index|bottom|left|'
             r'above|absolute|always|armenian|aural|auto|avoid|baseline|'
             r'behind|below|bidi-override|blink|block|bold|bolder|both|'
             r'capitalize|center-left|center-right|center|circle|'
             r'cjk-ideographic|close-quote|collapse|condensed|continuous|'
             r'crop|crosshair|cross|cursive|dashed|decimal-leading-zero|'
             r'decimal|default|digits|disc|dotted|double|e-resize|embed|'
             r'extra-condensed|extra-expanded|expanded|fantasy|far-left|'
             r'far-right|faster|fast|fixed|georgian|groove|hebrew|help|'
             r'hidden|hide|higher|high|hiragana-iroha|hiragana|icon|'
             r'inherit|inline-table|inline|inset|inside|invert|italic|'
             r'justify|katakana-iroha|katakana|landscape|larger|large|'
             r'left-side|leftwards|level|lighter|line-through|list-item|'
             r'loud|lower-alpha|lower-greek|lower-roman|lowercase|ltr|'
             r'lower|low|medium|message-box|middle|mix|monospace|'
             r'n-resize|narrower|ne-resize|no-close-quote|no-open-quote|'
             r'no-repeat|none|normal|nowrap|nw-resize|oblique|once|'
             r'open-quote|outset|outside|overline|pointer|portrait|px|'
             r'relative|repeat-x|repeat-y|repeat|rgb|ridge|right-side|'
             r'rightwards|s-resize|sans-serif|scroll|se-resize|'
             r'semi-condensed|semi-expanded|separate|serif|show|silent|'
             r'slow|slower|small-caps|small-caption|smaller|soft|solid|'
             r'spell-out|square|static|status-bar|super|sw-resize|'
             r'table-caption|table-cell|table-column|table-column-group|'
             r'table-footer-group|table-header-group|table-row|'
             r'table-row-group|text|text-bottom|text-top|thick|thin|'
             r'transparent|ultra-condensed|ultra-expanded|underline|'
             r'upper-alpha|upper-latin|upper-roman|uppercase|url|'
             r'visible|w-resize|wait|wider|x-fast|x-high|x-large|x-loud|'
             r'x-low|x-small|x-soft|xx-large|xx-small|yes)\b', Keyword),
            (r'(indigo|gold|firebrick|indianred|yellow|darkolivegreen|'
             r'darkseagreen|mediumvioletred|mediumorchid|chartreuse|'
             r'mediumslateblue|black|springgreen|crimson|lightsalmon|brown|'
             r'turquoise|olivedrab|cyan|silver|skyblue|gray|darkturquoise|'
             r'goldenrod|darkgreen|darkviolet|darkgray|lightpink|teal|'
             r'darkmagenta|lightgoldenrodyellow|lavender|yellowgreen|thistle|'
             r'violet|navy|orchid|blue|ghostwhite|honeydew|cornflowerblue|'
             r'darkblue|darkkhaki|mediumpurple|cornsilk|red|bisque|slategray|'
             r'darkcyan|khaki|wheat|deepskyblue|darkred|steelblue|aliceblue|'
             r'gainsboro|mediumturquoise|floralwhite|coral|purple|lightgrey|'
             r'lightcyan|darksalmon|beige|azure|lightsteelblue|oldlace|'
             r'greenyellow|royalblue|lightseagreen|mistyrose|sienna|'
             r'lightcoral|orangered|navajowhite|lime|palegreen|burlywood|'
             r'seashell|mediumspringgreen|fuchsia|papayawhip|blanchedalmond|'
             r'peru|aquamarine|white|darkslategray|ivory|dodgerblue|'
             r'lemonchiffon|chocolate|orange|forestgreen|slateblue|olive|'
             r'mintcream|antiquewhite|darkorange|cadetblue|moccasin|'
             r'limegreen|saddlebrown|darkslateblue|lightskyblue|deeppink|'
             r'plum|aqua|darkgoldenrod|maroon|sandybrown|magenta|tan|'
             r'rosybrown|pink|lightblue|palevioletred|mediumseagreen|'
             r'dimgray|powderblue|seagreen|snow|mediumblue|midnightblue|'
             r'paleturquoise|palegoldenrod|whitesmoke|darkorchid|salmon|'
             r'lightslategray|lawngreen|lightgreen|tomato|hotpink|'
             r'lightyellow|lavenderblush|linen|mediumaquamarine|green|'
             r'blueviolet|peachpuff)\b', Name.Builtin),
            (r'\!important', Comment.Preproc),
            (r'/\*(?:.|\n)*?\*/', Comment),
            (r'\#[a-zA-Z0-9]{1,6}', Number),
            (r'[\.-]?[0-9]*[\.]?[0-9]+(em|px|\%|pt|pc|in|mm|cm|ex)', Number),
            (r'-?[0-9]+', Number),
            (r'[~\^\*!%&<>\|+=@:,./?-]+', Operator),
            (r'[\[\]();]+', Punctuation),
            (r'"(\\\\|\\"|[^"])*"', String.Double),
            (r"'(\\\\|\\'|[^'])*'", String.Single),
            (r'[a-zA-Z][a-zA-Z0-9]+', Name)
        ]
    }

class HtmlLexer(RegexLexer):
    """
    For HTML 4 and XHTML 1 markup. Nested JavaScript and CSS is highlighted
    by the appropriate lexer.
    """

    name = 'HTML'
    aliases = ['html']
    filenames = ['*.html', '*.htm', '*.xhtml', '*.xslt']
    mimetypes = ['text/html', 'application/xhtml+xml']

    flags = re.IGNORECASE | re.DOTALL
    tokens = {
        'root': [
            ('[^<&]+', Text),
            (r'&\S*?;', Name.Entity),
            (r'\<\!\[CDATA\[.*?\]\]\>', Comment.Preproc),
            ('<!--', Comment, 'comment'),
            (r'<\?.*?\?>', Comment.Preproc),
            ('<![^>]*>', Comment.Preproc),
            (r'<\s*script\s*', Name.Tag, ('script-content', 'tag')),
            (r'<\s*style\s*', Name.Tag, ('style-content', 'tag')),
            (r'<\s*[a-zA-Z0-9:]+', Name.Tag, 'tag'),
            (r'<\s*/\s*[a-zA-Z0-9:]+\s*>', Name.Tag),
        ],
        'comment': [
            ('[^-]+', Comment),
            ('-->', Comment, '#pop'),
            ('-', Comment),
        ],
        'tag': [
            (r'\s+', Text),
            (r'[a-zA-Z0-9_:-]+\s*=', Name.Attribute, 'attr'),
            (r'[a-zA-Z0-9_:-]+', Name.Attribute),
            (r'/?\s*>', Name.Tag, '#pop'),
        ],
        'script-content': [
            (r'<\s*/\s*script\s*>', Name.Tag, '#pop'),
            (r'.+?(?=<\s*/\s*script\s*>)', using(JavascriptLexer)),
        ],
        'style-content': [
            (r'<\s*/\s*style\s*>', Name.Tag, '#pop'),
            (r'.+?(?=<\s*/\s*style\s*>)', using(CssLexer)),
        ],
        'attr': [
            ('".*?"', String, '#pop'),
            ("'.*?'", String, '#pop'),
            (r'[^\s>]+', String, '#pop'),
        ],
    }

    def analyse_text(text):
        if html_doctype_matches(text):
            return 0.5

class PhpLexer(RegexLexer):
    """
    For `PHP <http://www.php.net/>`_ source code.
    For PHP embedded in HTML, use the `HtmlPhpLexer`.

    Additional options accepted:

    `startinline`
        If given and ``True`` the lexer starts highlighting with
        php code (i.e.: no starting ``<?php`` required).  The default
        is ``False``.
    `funcnamehighlighting`
        If given and ``True``, highlight builtin function names
        (default: ``True``).
    `disabledmodules`
        If given, must be a list of module names whose function names
        should not be highlighted. By default all modules are highlighted
        except the special ``'unknown'`` module that includes functions
        that are known to php but are undocumented.

        To get a list of allowed modules have a look into the
        `_phpbuiltins` module:

        .. sourcecode:: pycon

            >>> from pygments.lexers._phpbuiltins import MODULES
            >>> MODULES.keys()
            ['PHP Options/Info', 'Zip', 'dba', ...]

        In fact the names of those modules match the module names from
        the php documentation.
    """

    name = 'PHP'
    aliases = ['php', 'php3', 'php4', 'php5']
    filenames = ['*.php', '*.php[345]']
    mimetypes = ['text/x-php']

    flags = re.IGNORECASE | re.DOTALL | re.MULTILINE
    tokens = {
        'root': [
            (r'<\?(php)?', Comment.Preproc, 'php'),
            (r'[^<]+', Other),
            (r'<', Other)
        ],
        'php': [
            (r'\?>', Comment.Preproc, '#pop'),
            (r'<<<([a-zA-Z_][a-zA-Z0-9_]*)\n.*?\n\1\;?\n', String),
            (r'\s+', Text),
            (r'#.*?\n', Comment.Single),
            (r'//.*?\n', Comment.Single),
            # put the empty comment here, it is otherwise seen as
            # the start of a docstring
            (r'/\*\*/', Comment.Multiline),
            (r'/\*\*.*?\*/', String.Doc),
            (r'/\*.*?\*/', Comment.Multiline),
            (r'(->|::)(\s*)([a-zA-Z_][a-zA-Z0-9_]*)',
             bygroups(Operator, Text, Name.Attribute)),
            (r'[~!%^&*+=|:.<>/?@-]+', Operator),
            (r'[\[\]{}();,]+', Punctuation),
            (r'(class)(\s+)', bygroups(Keyword, Text), 'classname'),
            (r'(function)(\s+)(&?)(\s*)',
              bygroups(Keyword, Text, Operator, Text), 'functionname'),
            (r'(const)(\s+)([a-zA-Z_][a-zA-Z0-9_]*)',
              bygroups(Keyword, Text, Name.Constant)),
            (r'(and|E_PARSE|old_function|E_ERROR|or|as|E_WARNING|parent|'
             r'eval|PHP_OS|break|exit|case|extends|PHP_VERSION|cfunction|'
             r'FALSE|print|for|require|continue|foreach|require_once|'
             r'declare|return|default|static|do|switch|die|stdClass|'
             r'echo|else|TRUE|elseif|var|empty|if|xor|enddeclare|include|'
             r'virtual|endfor|include_once|while|endforeach|global|__FILE__|'
             r'endif|list|__LINE__|endswitch|new|__sleep|endwhile|not|'
             r'array|__wakeup|E_ALL|NULL|final|php_user_filter|interface|'
             r'implements|public|private|protected|abstract|clone|try|'
             r'catch|throw|this)\b', Keyword),
            ('(true|false|null)\b', Keyword.Constant),
            (r'\$\{\$+[a-zA-Z_][a-zA-Z0-9_]*\}', Name.Variable),
            (r'\$+[a-zA-Z_][a-zA-Z0-9_]*', Name.Variable),
            ('[a-zA-Z_][a-zA-Z0-9_]*', Name.Other),
            (r"[0-9](\.[0-9]*)?(eE[+-][0-9])?[flFLdD]?|"
             r"0[xX][0-9a-fA-F]+[Ll]?", Number),
            (r"'([^'\\]*(?:\\.[^'\\]*)*)'", String.Single),
            (r'`([^`\\]*(?:\\.[^`\\]*)*)`', String.Backtick),
            (r'"', String.Double, 'string'),
        ],
        'classname': [
            (r'[a-zA-Z_][a-zA-Z0-9_]*', Name.Class, '#pop')
        ],
        'functionname': [
            (r'[a-zA-Z_][a-zA-Z0-9_]*', Name.Function, '#pop')
        ],
        'string': [
            (r'"', String.Double, '#pop'),
            (r'[^{$"\\]+', String.Double),
            (r'\\([nrt\"$]|[0-7]{1,3}|x[0-9A-Fa-f]{1,2})', String.Escape),
            (r'\$[a-zA-Z_][a-zA-Z0-9_]*(\[\S+\]|->[a-zA-Z_][a-zA-Z0-9_]*)?',
             String.Interpol),
            (r'(\{\$\{)(.*?)(\}\})',
             bygroups(String.Interpol, using(this, _startinline=True),
                      String.Interpol)),
            (r'(\{)(\$.*?)(\})',
             bygroups(String.Interpol, using(this, _startinline=True),
                      String.Interpol)),
            (r'(\$\{)(\S+)(\})',
             bygroups(String.Interpol, Name.Variable, String.Interpol)),
            (r'[${\\]+', String.Double)
        ],
    }

    def __init__(self, **options):
        self.funcnamehighlighting = get_bool_opt(
            options, 'funcnamehighlighting', True)
        self.disabledmodules = get_list_opt(
            options, 'disabledmodules', ['unknown'])
        self.startinline = get_bool_opt(options, 'startinline', False)

        # private option argument for the lexer itself
        if '_startinline' in options:
            self.startinline = options.pop('_startinline')

        # collect activated functions in a set
        self._functions = set()
        if self.funcnamehighlighting:
            from pygments.lexers._phpbuiltins import MODULES
            for key, value in MODULES.iteritems():
                if key not in self.disabledmodules:
                    self._functions.update(value)
        RegexLexer.__init__(self, **options)

    def get_tokens_unprocessed(self, text):
        stack = ['root']
        if self.startinline:
            stack.append('php')
        for index, token, value in \
            RegexLexer.get_tokens_unprocessed(self, text, stack):
            if token is Name.Other:
                if value in self._functions:
                    yield index, Name.Builtin, value
                    continue
            yield index, token, value

    def analyse_text(text):
        rv = 0.0
        if re.search(r'<\?(?!xml)', text):
            rv += 0.3
        if '?>' in text:
            rv += 0.1
        return rv

class XmlLexer(RegexLexer):
    """
    Generic lexer for XML (eXtensible Markup Language).
    """

    flags = re.MULTILINE | re.DOTALL

    name = 'XML'
    aliases = ['xml']
    filenames = ['*.xml', '*.xsl', '*.rss', '*.xslt', '*.xsd', '*.wsdl']
    mimetypes = ['text/xml', 'application/xml', 'image/svg+xml',
                 'application/rss+xml', 'application/atom+xml',
                 'application/xsl+xml', 'application/xslt+xml']

    tokens = {
        'root': [
            ('[^<&]+', Text),
            (r'&\S*?;', Name.Entity),
            (r'\<\!\[CDATA\[.*?\]\]\>', Comment.Preproc),
            ('<!--', Comment, 'comment'),
            (r'<\?.*?\?>', Comment.Preproc),
            ('<![^>]*>', Comment.Preproc),
            (r'<\s*[a-zA-Z0-9:._-]+', Name.Tag, 'tag'),
            (r'<\s*/\s*[a-zA-Z0-9:._-]+\s*>', Name.Tag),
        ],
        'comment': [
            ('[^-]+', Comment),
            ('-->', Comment, '#pop'),
            ('-', Comment),
        ],
        'tag': [
            (r'\s+', Text),
            (r'[a-zA-Z0-9_.:-]+\s*=', Name.Attribute, 'attr'),
            (r'/?\s*>', Name.Tag, '#pop'),
        ],
        'attr': [
            ('\s+', Text),
            ('".*?"', String, '#pop'),
            ("'.*?'", String, '#pop'),
            (r'[^\s>]+', String, '#pop'),
        ],
    }

    def analyse_text(text):
        if looks_like_xml(text):
            return 0.5

class XsltLexer(XmlLexer):
    '''
    A lexer for XSLT.

    *New in Pygments 0.10.*
    '''

    name = 'XSLT'
    aliases = ['xslt']
    filenames = ['*.xsl', '*.xslt']

    EXTRA_KEYWORDS = set([
        'apply-imports', 'apply-templates', 'attribute',
        'attribute-set', 'call-template', 'choose', 'comment',
        'copy', 'copy-of', 'decimal-format', 'element', 'fallback',
        'for-each', 'if', 'import', 'include', 'key', 'message',
        'namespace-alias', 'number', 'otherwise', 'output', 'param',
        'preserve-space', 'processing-instruction', 'sort',
        'strip-space', 'stylesheet', 'template', 'text', 'transform',
        'value-of', 'variable', 'when', 'with-param'
    ])

    def get_tokens_unprocessed(self, text):
        for index, token, value in XmlLexer.get_tokens_unprocessed(self, text):
            m = re.match('</?xsl:([^>]*)/?>?', value)

            if token is Name.Tag and m and m.group(1) in self.EXTRA_KEYWORDS:
                yield index, Keyword, value
            else:
                yield index, token, value

    def analyse_text(text):
        if looks_like_xml(text) and '<xsl' in text:
            return 0.8

class MxmlLexer(RegexLexer):
    """
    For MXML markup.
    Nested AS3 in <script> tags is highlighted by the appropriate lexer.
    """
    flags = re.MULTILINE | re.DOTALL
    name = 'MXML'
    aliases = ['mxml']
    filenames = ['*.mxml']
    mimetimes = ['text/xml', 'application/xml']

    tokens = {
            'root': [
                ('[^<&]+', Text),
                (r'&\S*?;', Name.Entity),
                (r'(\<\!\[CDATA\[)(.*?)(\]\]\>)',
                 bygroups(String, using(ActionScript3Lexer), String)),
                ('<!--', Comment, 'comment'),
                (r'<\?.*?\?>', Comment.Preproc),
                ('<![^>]*>', Comment.Preproc),
                (r'<\s*[a-zA-Z0-9:._-]+', Name.Tag, 'tag'),
                (r'<\s*/\s*[a-zA-Z0-9:._-]+\s*>', Name.Tag),
            ],
            'comment': [
                ('[^-]+', Comment),
                ('-->', Comment, '#pop'),
                ('-', Comment),
            ],
            'tag': [
                (r'\s+', Text),
                (r'[a-zA-Z0-9_.:-]+\s*=', Name.Attribute, 'attr'),
                (r'/?\s*>', Name.Tag, '#pop'),
            ],
            'attr': [
                ('\s+', Text),
                ('".*?"', String, '#pop'),
                ("'.*?'", String, '#pop'),
                (r'[^\s>]+', String, '#pop'),
            ],
        }


"""Day 21: Dirac Dice"""

from functools import cache
from itertools import cycle, product
from pathlib import Path

PATH = Path(__file__).parent

class Dice:

    def __init__(self, position1, position2):
        self._pos1 = position1
        self._pos2 = position2

    def play(self):
        return self._play(self._pos1, self._pos2)

    @staticmethod
    def move(position, roll):
        return 1 + (position + roll - 1) % 10

    def _play(self, pos1, pos2, score1=0, score2=0):
        raise NotImplementedError

class DeterministicDice(Dice):

    win_points = 1000

    @staticmethod
    def rolls():
        roll = cycle(range(1, 101))
        while True:
            yield sum(next(roll, 0) for _ in range(3))

    def _play(self, pos1, pos2, score1=0, score2=0):
        rolls = self.rolls()
        count = 0

        for p1_roll, p2_roll in zip(rolls, rolls):
            pos1 = self.move(pos1, p1_roll)
            score1 += pos1
            count += 3
            if score1 >= self.win_points:
                break

            pos2 = self.move(pos2, p2_roll)
            score2 += pos2
            count += 3
            if score2 >= self.win_points:
                break

        return count * min(score1, score2)

class DiracDice(Dice):

    win_points = 21

    @staticmethod
    def rolls():
        return (sum(rolls) for rolls in product((1, 2, 3), repeat=3))

    @cache
    def _play(self, pos1, pos2, score1=0, score2=0):
        if score1 >= self.win_points:
            return 1, 0

        if score2 >= self.win_points:
            return 0, 1

        p1_wins = p2_wins = 0
        for p1_roll in self.rolls():
            new_pos1 = self.move(pos1, p1_roll)
            new_score1 = score1 + new_pos1
            if new_score1 >= self.win_points:
                p1_wins += 1
                continue

            for p2_roll in self.rolls():
                new_pos2 = self.move(pos2, p2_roll)
                new_score2 = score2 + new_pos2
                _p1_wins, _p2_wins = self._play(
                    new_pos1, new_pos2, new_score1, new_score2
                )
                p1_wins += _p1_wins
                p2_wins += _p2_wins

        return p1_wins, p2_wins

def parse(path):
    with path.open(encoding='utf-8') as input_file:
        positions = []
        for line in input_file:
            _, position = line.split(':')
            positions.append(int(position))

        return positions

def test_example1():
    number = DeterministicDice(*parse(PATH / 'example')).play()
    assert number == 739785

def test_example2():
    wins = DiracDice(*parse(PATH / 'example')).play()
    assert max(wins) == 444356092776315

def test_part1():
    number = DeterministicDice(*parse(PATH / 'input')).play()
    assert number == 998088

def test_part2():
    wins = DiracDice(*parse(PATH / 'input')).play()
    assert max(wins) == 306621346123766


import time
from PIL import Image

def animate(left_eye, right_eye, left_mouth, right_mouth, delay=0.5):

    eye_top_left = Image.open('routines/displays/images/eyes/pointy_top_left.png')
    eye_left = Image.open('routines/displays/images/eyes/pointy_left.png')
    eye_center = Image.open('routines/displays/images/eyes/pointy_center.png')
    eye_right = Image.open('routines/displays/images/eyes/pointy_right.png')
    eye_top_right = Image.open('routines/displays/images/eyes/pointy_top_right.png')
    eye_top_center = Image.open('routines/displays/images/eyes/pointy_top_center.png')

    mouth_left = Image.open('routines/displays/images/mouth/open_left.png')
    mouth_right = Image.open('routines/displays/images/mouth/open_right.png')

    left_mouth.image(mouth_left)
    right_mouth.image(mouth_right)

    while True:
        set_eyes_and_delay(left_eye, right_eye, delay, eye_center)
        set_eyes_and_delay(left_eye, right_eye, delay, eye_right)
        set_eyes_and_delay(left_eye, right_eye, delay, eye_top_right)
        set_eyes_and_delay(left_eye, right_eye, delay, eye_top_center)
        set_eyes_and_delay(left_eye, right_eye, delay, eye_top_left)
        set_eyes_and_delay(left_eye, right_eye, delay, eye_left)

def set_eyes_and_delay(left_eye, right_eye, delay, image):
    left_eye.image(image)
    right_eye.image(image)
    time.sleep(delay)


# Author: Jose G Perez
# Version 1.0
# Last Modified: January 31, 2018
import cv2
import numpy as np
import pylab as plt
from multiprocessing.pool import ThreadPool
from timeit import default_timer as timer

from util_sift import load_sift
from util_matching import match_details
INDIVIDUAL = False

np.random.seed(1)
np.set_printoptions(threshold=np.nan, linewidth=1000)
# Default: contrast 0.04, edge 10, sigma 1.6
# Experiment: 0.08, 30, 2
# CT: The larger the threshold, the less features are produced by the detector
# ET: The larger the threshold, the more features that are retained
SIFT = cv2.xfeatures2d.SIFT_create(contrastThreshold=0.05, edgeThreshold=100, sigma=2)

RADIUS = 50
RADIUS_SQUARED = RADIUS ** 2
SCALE_THRESHOLD = 3
DISTANCE_THRESHOLD = 200
RESPONSE_THRESHOLD = 0.01

DISTANCE_RATIO = 0.95
RANSAC_REPROJ_TRESHHOLD = 10 # The higher the threshold, the lower the inliers
RANSAC_MAX_ITERS = 2000
RANSAC_CONFIDENCE = 0.99

FIGURE_IDX = 0
def imshow(im, title=''):
    global FIGURE_IDX
    plt.figure(FIGURE_IDX)
    plt.axis('off')
    plt.tick_params(axis='both',
                    left='off', top='off', right='off', bottom='off',
                    labelleft='off', labeltop='off', labelright='off', labelbottom='off')
    plt.title(title)
    plt.imshow(im)

    FIGURE_IDX += 1

def gallery(array, ncols=3):
    # Grayscale
    if len(array.shape) == 3:
        nindex, height, width = array.shape
        nrows = nindex//ncols
        result = (array.reshape(nrows, ncols, height, width)
                  .swapaxes(1,2)
                  .reshape(height*nrows, width*ncols))
        return result
    # Color
    else:
        nindex, height, width, intensity = array.shape
        nrows = nindex//ncols
        # want result.shape = (height*nrows, width*ncols, intensity)
        result = (array.reshape(nrows, ncols, height, width, intensity)
                  .swapaxes(1,2)
                  .reshape(height*nrows, width*ncols, intensity))
        return result

s_im, s_label, s_kp, s_des = load_sift('S_BB_V1_SIFT.npz')
pw_im, pw_label, pw_kp, pw_des = load_sift('PW_BB_V1_SIFT.npz')

time_start = timer()
# ==== MATCHING SYSTEM BEGIN
if INDIVIDUAL:
    # im1 = s_im[np.where(s_label == 67)[0][0]]
    # im2 = pw_im[np.where(pw_label == 145)[0][0]]

    # im1 = s_im[np.where(s_label == 6)[0][0]]
    # im2 = pw_im[np.where(pw_label == 8)[0][0]]

    im1 = s_im[np.where(s_label == 33)[0][0]]
    im2 = pw_im[np.where(pw_label == 68)[0][0]]

    kp1, des1 = SIFT.detectAndCompute(im1, None)
    kp2, des2 = SIFT.detectAndCompute(im2, None)

    m, matches, stat_s_diff, stat_d_dist, stat_resp_kp1, stat_resp_kp2, stat_rad  = match_details(kp1, des1, kp2, des2)
    print("\t[ScaleD] Max %.3f, Min %.3f, Avg %.3f" % (max(stat_s_diff) , min(stat_s_diff), sum(stat_s_diff) / len(stat_s_diff)))
    print("\t[DescD] Max %.3f, Min %.3f, Avg %.3f" % (max(stat_d_dist),  min(stat_d_dist), sum(stat_d_dist) / len(stat_d_dist)))
    print("\t[RespKp1] Max %.3f, Min %.3f, Avg %.3f" % (max(stat_resp_kp1), min(stat_resp_kp1), sum(stat_resp_kp1) / len(stat_resp_kp1)))
    print("\t[RespKp2] Max %.3f, Min %.3f, Avg %.3f" % (max(stat_resp_kp2), min(stat_resp_kp2), sum(stat_resp_kp2) / len(stat_resp_kp2)))
    print("\t[Rad] Max %.3f, Min %.3f, Avg %.3f" % (max(stat_rad), min(stat_rad), sum(stat_rad) / len(stat_rad)))

    # For comparison, the selected SIFT keypoints
    # BF = cv2.BFMatcher(normType=cv2.NORM_L2, crossCheck=True)
    # bf_matches = BF.match(des1, des2)
    # im_bf_matches = cv2.drawMatches(im1,kp1,im2,kp2,bf_matches,None,flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)
    # metric_bf = len(bf_matches)/ (len(kp1) + len(kp2) - len(bf_matches))
    BF = cv2.BFMatcher(normType=cv2.NORM_L2)
    bf_matches = BF.knnMatch(des1, des2, k=2)

    # For comparison, RANSAC
    good_matches = [m[0] for m in bf_matches if len(m) == 2 and m[0].distance < m[1].distance * DISTANCE_RATIO]
    src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches])
    dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches])
    H, mask = cv2.findHomography(src_pts, dst_pts, method=cv2.RANSAC, ransacReprojThreshold=RANSAC_REPROJ_TRESHHOLD, maxIters=RANSAC_MAX_ITERS, confidence=RANSAC_CONFIDENCE)
    matchesMask = mask.ravel().tolist()
    drawParameters = dict(matchColor=None, singlePointColor=None, matchesMask=matchesMask,
                          flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)

    # Draw the lines between the 2 images connecting matches
    im_ransac = cv2.drawMatches(im1, kp1, im2, kp2, good_matches, None, **drawParameters)

    # Images
    im1_kp = cv2.drawKeypoints(im1, kp1, None, None, cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
    im2_kp = cv2.drawKeypoints(im2, kp2, None, None, cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
    im_matches = cv2.drawMatches(im1,kp1,im2,kp2,matches,None,flags=cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS)

    metric = len(matches) / (len(kp1) + len(kp2) - len(matches))
    print("Metric", metric)
    print("Inliers", mask.sum())

    plt.gray()
    imshow(im1_kp, 'S Keypoint Data')
    imshow(im2_kp, 'PW Keypoint Data')
    #imshow(im_bf_matches, 'BF Matches')
    imshow(im_matches, 'New Matching')
    imshow(im_ransac, 'RANSAC')
else:
    # im2 = pw_im[np.where(pw_label == 68)[0][0]]
    pw_idx = 33
    im2 = pw_im[pw_idx]
    (kp2, des2) = (pw_kp[pw_idx], pw_des[pw_idx])

    all_matches = []
    all_im = []
    all_kp = []
    all_des = []

    cidx = 0
    for sidx in range(25, 40):
        # if sidx == 32:
        #     print('=[P]', end='')
        print("S", (sidx + 1), "CIDX", cidx)

        (im1, kp1, des1) = (s_im[sidx], s_kp[sidx], s_des[sidx])
        m, matches, stat_s_diff, stat_d_dist, stat_resp_kp1, stat_resp_kp2, stat_rad = match_details(kp1, des1, kp2,
                                                                                des2)
        # print("\t[ScaleD] Max %.3f, Min %.3f, Avg %.3f" % (
        # max(stat_s_diff), min(stat_s_diff), sum(stat_s_diff) / len(stat_s_diff)))
        # print("\t[DescD] Max %.3f, Min %.3f, Avg %.3f" % (
        # max(stat_d_dist), min(stat_d_dist), sum(stat_d_dist) / len(stat_d_dist)))
        # print("\t[RespKp1] Max %.3f, Min %.3f, Avg %.3f" % (
        # max(stat_resp_kp1), min(stat_resp_kp1), sum(stat_resp_kp1) / len(stat_resp_kp1)))
        # print("\t[RespKp2] Max %.3f, Min %.3f, Avg %.3f" % (
        # max(stat_resp_kp2), min(stat_resp_kp2), sum(stat_resp_kp2) / len(stat_resp_kp2)))
        # print("\t[Rad] Max %.3f, Min %.3f, Avg %.3f" % (max(stat_rad), min(stat_rad), sum(stat_rad) / len(stat_rad)))

        metric = len(matches) / (len(kp1) + len(kp2) - len(matches))

        print("\tMetric w/PW %s is %.3f, Matches %d" % (pw_idx, metric, len(matches)))

        all_matches.append(matches)
        all_im.append(im1)
        all_kp.append(kp1)
        all_des.append(des1)
        cidx += 1

    # index = 7
    # imshow(cv2.drawMatches(all_im[index], all_kp[index], im2, kp2, all_matches[index], None, flags=2), '[P]SW33')
    # index = 8
    # imshow(cv2.drawMatches(all_im[index], all_kp[index], im2, kp2, all_matches[index], None, flags=2), 'SW34')

duration = timer() - time_start
print("Program took %.3fs" % duration)