# Tkinter gui window

##############################
# IMPORTS
##############################

from tkinter import *
import os
from PIL import ImageTk, Image
from multiprocessing.managers import BaseManager
import types
import sys
import threading
import webbrowser

##############################
# Global Variables
##############################

ipcQueueItems = []
storagePath = os.path.join(os.getenv('LOCALAPPDATA'), 'hll-companion')
settingsPath = os.path.join(storagePath, 'settings.txt')
scalesPositionsPath = os.path.join(storagePath, 'sliders.txt')
windowOnTop = 0

##############################
# INITIALIZE WINDOW
##############################

def initGui():
    ##############################
    # DATA MANIPULATION
    ##############################

    def getPath(filename):
        return os.path.join(os.path.realpath(os.path.dirname(__file__)), filename)

    # Declare data storage class
    class ParsedObj:
            def __init__(self, option, value):
                self.option = option
                self.value = value

    # Retrieve data from game ini and store in levelsData
    def getData(levelsData):
        levelsData.clear()
        # Read ini file and assign to list
        f = open(os.path.expanduser('~') + '\\AppData\\Local\\HLL\\Saved\\Config\\WindowsNoEditor\\GameUserSettings.ini')
        lines = f.readlines()

        # Function for parsing read data
        def parseData(lines, levelsData):
            for i in range(len(lines)):
                parsed = lines[i].rsplit("=")

                if parsed[0] == "ProximityVoiceVolume" or parsed[0] == "UnitVoiceVolume" or parsed[0] == "LeadershipVoiceVolume":
                    levelsData.append(ParsedObj(parsed[0], parsed[1]))
            return levelsData
        levelsData = parseData(lines, levelsData)
        return levelsData
    levelsData = []
    levelsData = getData(levelsData)

    # Set up Local Storage
    def createSettingsFile():
        print('Creating Settings file')
        settingsFile = open(settingsPath, 'w')
        settingsFile.write('[resolution:{x:1920,y:1080}][stayOnTop:{value:0}]')

    def createSlidersFile():
        print('Creatings sliders.txt...')
        slidersFile = open(scalesPositionsPath, 'w')
        slidersFile.write('{ProximityVoiceVolume:{high:70,low:30}}{UnitVoiceVolume:{high:70,low:30}}{LeadershipVoiceVolume:{high:70,low:30}}')

    def initLocal():
        # If no folder, create folder
        if os.path.exists(storagePath):
            print('Storage present')
        else:
            print('Storage not found, creating...')
            os.mkdir(storagePath)

        # # If no settings file, create settings file
        if os.path.isfile(settingsPath):
            print('Settings file found')
        else:
            print('Settings file not found')
            createSettingsFile()

        # # if no scalesPositions file, create scalesPosition file
        if os.path.isfile(scalesPositionsPath):
            print('sliders.txt found')
        else:
            print('sliders.txt not found')
            createSlidersFile()
    initLocal()

    def parseSettingsString(settingsString):
        resultObj = types.SimpleNamespace()

        # Remove first bracket
        settingsString = settingsString[1:]
        # Remove last bracket
        settingsString = settingsString[:-1]

        # Split objects
        objectsStrings = settingsString.split('][')

        for obj in objectsStrings:
            newObj = obj
            # Check if first character is a bracket and remove
            if newObj[1] == '[':
                newObj = obj[1:]
            if newObj[-1] == ']':
                newObj = obj[:-1]

            # Split category name and assign to result
            catObj = types.SimpleNamespace()
            catName = newObj.split(':{')[0]
            setattr(catObj, catName, types.SimpleNamespace())

            # Remove last curly brace of value pairs
            valuePairs = newObj.split(':{')[1]
            if valuePairs[-1] == '}':
                valuePairs = valuePairs[:-1]

            # Split into individual pairs
            indvPairs = valuePairs.split(',')

            # Split into key value pairs
            for pair in indvPairs:
                keyValueList = pair.split(':')
                # Assign attribute to category
                setattr(catObj, keyValueList[0], keyValueList[1])
                setattr(resultObj, catName, catObj)
        return resultObj

    def parseScaleValuesString(scaleValuesString):

        resultObj = types.SimpleNamespace()

        # Remove first character
        scaleValuesString = scaleValuesString[1:]
        # Remove last character
        scaleValuesString = scaleValuesString[:-1]

        elements = scaleValuesString.split('}{')
        for element in elements:
            splitElement = element.split(':{')
            name = splitElement[0]

            # Set attributes with names
            setattr(resultObj, name, types.SimpleNamespace())

            # Parse values
            # remove last character
            splitElement[1] = splitElement[1][:-1]
            # split
            valuePair = splitElement[1].split(',')
            # Assign values to new object, assign new object to result object
            setattr(vars(resultObj)[name], 'high', valuePair[0].split(':')[1])
            setattr(vars(resultObj)[name], 'low', valuePair[1].split(':')[1])
        return resultObj

    ###############################
    # RENDER PROCESS
    ###############################

    # Render a window and display data

    def render():

        ##############################
        # Event Handlers
        ##############################

        # Reset ipcQueueItems
        def resetQueueItems():
            global ipcQueueItems
            ipcQueueItems = []

        # Add to ipcQueueItems
        def updateQueue(newItem):
            global ipcQueueItems

            # Add item
            ipcQueueItems.append(newItem)

            # Send to multiprocess queue
            manager = BaseManager(address=('localhost', 50000), authkey=b'blahkey')
            manager.connect()

            currentQueue = manager.getIpcQueue()

            if not currentQueue.empty():
                currentQueue.get()

            currentQueue.put(newItem)

            # Reset Queue
            resetQueueItems()

        # Serialize scales values
        def serializeScalesValues(scales):

            scalesValuesString = ''
            for scale in scales:
                # Start Item
                scalesValuesString += '{'
                # name
                scalesValuesString += scale.option
                # Start object
                scalesValuesString += ':{'
                # High value
                scalesValuesString += 'high:' + str(scale.highScale.get()) + ','
                # Low value
                scalesValuesString += 'low:' + str(scale.lowScale.get())
                # End object
                scalesValuesString += '}'
                # Close item
                scalesValuesString += '}'
            return scalesValuesString

        # Update Scales Values
        def updateScalesValues(scales):
            scalesValuesString = serializeScalesValues(scales)
            file = open(scalesPositionsPath, 'w')
            file.write(scalesValuesString)
            updateQueue(scalesValuesString)

        def handleUpdateClick(levelsData, frames):
            #  Refresh levels data
            levelsData = getData(levelsData)

            # Update height of meters
            def updateMeter(levelsData, frames):
                i = 0
                for frame in frames:
                    # Set meter height
                    meterHeight = round(float(levelsData[i].value) * 100)

                    # Set color of meter according to meterHeight
                    if meterHeight > 75:
                        meterColor = 'green'
                    elif meterHeight < 76 and meterHeight > 50:
                        meterColor = 'yellow'
                    elif meterHeight < 51 and meterHeight > 25:
                        meterColor = 'orange'
                    elif meterHeight < 26:
                        meterColor = 'red'

                    if meterHeight == 0:
                        meterHeight = 100
                        meterColor = 'gray20'

                    # Send Options
                    frame.config(bg=meterColor)
                    frame.config(height=meterHeight)

                    i += 1
            updateMeter(levelsData, frames)

        ###############################
        # Set up window
        ###############################
        root = Tk()
        root.geometry('400x330')
        root.title('HLL Companion')
        root.configure(background='gray30')
        root.resizable(False, False)
        root.wm_iconbitmap(getPath("assets\\tank.ico"))

        # Global variable for settings menu
        savedResolution = StringVar();

        # Get Settings from settings txt and parse to object
        def getSettingsObj():
            settingsFile = open(settingsPath, 'r')
            return parseSettingsString(settingsFile.read())

        # Check if window should be on top and update global variable
        def checkIfTop(displayOnTop=False):

            if displayOnTop == False:
                savedSettings = getSettingsObj()
                global windowOnTop
                windowOnTop = int(savedSettings.stayOnTop.value)
            elif displayOnTop == True:
                windowOnTop = 1

        # Set the log message
        def setLogMessage(message, mode='write'):
            manager = BaseManager(address=('localhost', 50000), authkey=b'blahkey')
            manager.connect()
            currentQueue = manager.getLogMessage()
            currentQueue.put(message + '\\n' + mode)

        # Update log from ipc
        def updateLogMessage(log):

            def startLogUpdateThread(log):
                manager = BaseManager(address=('localhost', 50000), authkey=b'blahkey')
                manager.connect()
                currentQueue = manager.getLogMessage()

                if currentQueue.empty() == False:
                    queueString = currentQueue.get()
                    receivedMessage = queueString.split('\\n')[0]
                    receivedMode = queueString.split('\\n')[1]
                    if receivedMode == 'write':
                        message = receivedMessage
                    elif receivedMode == 'append':
                        message = log.cget("text") + receivedMessage
                    else:
                        message = ''
                    log.config(text=message)

            newThread = threading.Thread(target=lambda: startLogUpdateThread(log), daemon=True)
            newThread.start()

        # Set up update function
        def update(levelsData, frames, log):
            handleUpdateClick(levelsData, frames)
            updateLogMessage(log)

            # Stay on top?
            if windowOnTop == 1:
                root.attributes('-topmost', True)
            else:
                root.attributes('-topmost', False)

            root.after(500, lambda: update(levelsData, frames, log))

        # Render meters frame
        metersFrame = Frame(root, bg='gray10', relief='ridge', bd=2)
        metersFrame.pack()
        metersFrame.place(bordermode=INSIDE, anchor='nw', height=120, width=380, x=10, y=10)

        # Render meters
        def renderMeters(levelsData):
            frames = []
            i = 0
            for dataObj in levelsData:
                # Determine height of meter according to levels
                meterHeight = round(float(dataObj.value) * 100)

                # Set color of meter according to meterHeight
                if meterHeight > 75:
                    meterColor = 'green'
                elif meterHeight < 76 and meterHeight > 50:
                    meterColor = 'yellow'
                elif meterHeight < 51 and meterHeight > 25:
                    meterColor = 'orange'
                elif meterHeight < 26:
                    meterColor = 'red'

                # Create meters
                frame = Frame(metersFrame, bg=meterColor, height=meterHeight)
                frame.pack()
                frame.place(bordermode=OUTSIDE, anchor='sw', width=40, x=70 + (i * 100), y=115)
                frames.append(frame)
                i += 1
            return frames
        frames = renderMeters(levelsData)

        # Render Labels
        def renderLabels(levelsData):
            i = 0
            for dateObj in levelsData:
                label = Label(root, relief='ridge', text=dateObj.option.split('Voice')[0], padx=5, pady=3)
                label.pack()
                label.place(bordermode=OUTSIDE, anchor='center', x=100 + (i * 100), y=150)
                i += 1
        renderLabels(levelsData)

        # Render Scales
        def renderScales(levelsData):
            scalesFile = open(scalesPositionsPath, 'r')
            scalesValues = parseScaleValuesString(scalesFile.read())

            scales = []
            i = 0
            for dateObj in levelsData:

                # Get starting values from local storage file
                if dateObj.option == 'LeadershipVoiceVolume':
                    highValue = scalesValues.LeadershipVoiceVolume.high
                    lowValue = scalesValues.LeadershipVoiceVolume.low
                elif dateObj.option == 'UnitVoiceVolume':
                    highValue = scalesValues.UnitVoiceVolume.high
                    lowValue = scalesValues.UnitVoiceVolume.low
                elif dateObj.option == 'ProximityVoiceVolume':
                    highValue = scalesValues.ProximityVoiceVolume.high
                    lowValue = scalesValues.ProximityVoiceVolume.low

                # High Sliders
                dateObj.highScale = Scale(root, from_=0, to=100, orient=HORIZONTAL, length=75, label='      High')
                dateObj.highScale.bind("<ButtonRelease-1>", lambda event : updateScalesValues(scales))
                dateObj.highScale.pack()
                dateObj.highScale.set(highValue)
                dateObj.highScale.place(anchor='center', x=100 + (i * 100), y=195)

                # Low Sliders
                dateObj.lowScale = Scale(root, from_=0, to=100, orient=HORIZONTAL, length=75, label='      Low')
                dateObj.lowScale.bind("<ButtonRelease-1>", lambda event : updateScalesValues(scales))
                dateObj.lowScale.pack()
                dateObj.lowScale.set(lowValue)
                dateObj.lowScale.place(anchor='center', x=100 + (i * 100), y=255)

                scales.append(dateObj)

                i += 1
            return scales
        scales = renderScales(levelsData)
        # On app mount update
        updateScalesValues(scales)

        ###################################################
        # SETTINGS MODAL
        ###################################################

        def startSettingsModal():

            # Local Storage
            savedSettings = getSettingsObj()

            savedHeight = StringVar()
            savedHeight.set(savedSettings.resolution.y)

            savedWidth = StringVar()
            savedWidth.set(savedSettings.resolution.x)

            # Create frame and overlay
            settingsFrame = Frame(root, background='gray50')
            settingsFrame.pack()
            settingsFrame.place(height=330, width=400)

            # Resolution Settings
            resFrame = Frame(settingsFrame, relief='ridge')
            resFrame.pack()
            resFrame.place(anchor='nw', width=150, height=75, x=20, y=20)

            # Resolution label
            resNameLbl = Label(resFrame, text='Resolution')
            resNameLbl.pack(side=TOP)

            # Resolution dropdown
            print('Saved Setting: ', savedSettings.resolution.x)
            if savedSettings.resolution.y == '1440':
                savedResolution.set("1440p");
            else:
                savedResolution.set("1080p");

            resRadio1080p = Radiobutton(resFrame, text="1080p", variable=savedResolution, value="1080p");
            resRadio1080p.pack();
            resRadio1080p.place(anchor='center', x=75, y=30);

            resRadio1440p = Radiobutton(resFrame, text="1440p", variable=savedResolution, value="1440p");
            resRadio1440p.pack();
            resRadio1440p.place(anchor='center', x=75, y=50);

            # Stay on top
            stayTopFrame = Frame(settingsFrame, relief='ridge')
            stayTopFrame.pack()
            stayTopFrame.place(anchor='ne', width=150, height=75, x=380, y=20)

            stayTopLabel = Label(stayTopFrame, text='Stay On Top')
            stayTopLabel.pack()
            stayTopLabel.place(anchor='nw', x=5, y=5)

            stayTop = IntVar()
            stayTop.set(int(savedSettings.stayOnTop.value))
            stayTopCheck = Checkbutton(stayTopFrame, variable=stayTop, onvalue=1, offvalue=0 )
            stayTopCheck.pack()
            stayTopCheck.place(anchor='ne', x=145, y=5)

            def handleOKSettingsModal(settingsFrame):

                # Get resolution settings
                if savedResolution.get() == '1440p':
                    widthSetting = '2560'
                    heightSetting = '1440'
                else:
                    widthSetting = '1920'
                    heightSetting = '1080'

                stringToWrite = '['

                # Serialize resolution settings
                stringToWrite += 'resolution:{'
                # Width
                stringToWrite += 'x:' + widthSetting
                # New Item
                stringToWrite += ','
                # Height
                stringToWrite += 'y:' + heightSetting + '}'

                # Close String
                stringToWrite += ']'

                # # Serialize stay on top setting
                stringToWrite += '[stayOnTop:{value:' + str(stayTop.get()) + '}]'

                # Write to file
                settingsFile = open(settingsPath, 'w')
                settingsFile.write(stringToWrite)
                settingsFile.close()

                checkIfTop(bool(stayTop.get()))

                # Remove Gui
                settingsFrame.destroy()

            # Render Ok button
            okBtn = Button(settingsFrame, text='OK', command=lambda: handleOKSettingsModal(settingsFrame))
            okBtn.pack()
            okBtn.place(anchor='center', width=75, height=25, x=150, y=300)

            # Update log
            setLogMessage('Settings Saved')

            def handleCancelSettingsModal(settingsFrame):
                settingsFrame.destroy()
                setLogMessage('Changes to Settings Discarded')

            # Render cancel button
            cancelBtn = Button(settingsFrame, text='Cancel', command=lambda: handleCancelSettingsModal(settingsFrame))
            cancelBtn.pack()
            cancelBtn.place(anchor='center', width=75, height=25, x=250, y=300)

        # Render settings Button
        def renderSettingsBtn():

            img = Image.open(getPath("assets\\gear.png"))
            img.resize((50, 50), Image.ANTIALIAS)
            finalImg = ImageTk.PhotoImage(img)
            settingsBtn = Button(root, image=finalImg, command=startSettingsModal)
            settingsBtn.pack()
            settingsBtn.image = finalImg
            settingsBtn.place(anchor='center', x=380, y=310)
        renderSettingsBtn()

        ######################################################
        # INFO MODAL
        ######################################################

        def startInfoModal():
            # Create frame and overlay
            infoFrame = Frame(root, background='gray50')
            infoFrame.pack()
            infoFrame.place(height=330, width=400)

            # Event handler for OK Button
            def handleOKInfoModal(infoFrame):
                infoFrame.destroy()

            # Render Ok button
            okBtn = Button(infoFrame, text='OK', command=lambda: handleOKInfoModal(infoFrame))
            okBtn.pack()
            okBtn.place(anchor='center', width=75, height=25, x=200, y=310)

            # Render Hotkeys Frame
            hotFrame = Frame(infoFrame)
            hotFrame.pack()
            hotFrame.place(anchor='nw', width=360, height=160, x=20, y=10)

            # Hotkey Title Label
            hotTitleLbl = Label(hotFrame, text='Hotkeys\nAction triggers after Right Alt is released\nRelease all other keys while triggering hotkey')
            hotTitleLbl.pack(side=TOP)

            # Proximity hotkeys frame
            proxFrame = Frame(hotFrame, bd=2, relief='ridge')
            proxFrame.pack()
            proxFrame.place(width=120, height=90, x=0, y=60, anchor='nw')

            proxTitle = Label(proxFrame, text='Proximity', anchor='center')
            proxTitle.pack(side=TOP)

            proxActionsContent = ' High:\nLow:\n Mute:'
            proxActions = Label(proxFrame, anchor='nw', text=proxActionsContent)
            proxActions.pack()
            proxActions.place(width=55, height=60, anchor='nw', y=20, x=0)

            proxKeysContent = ' RAlt + p\nRAlt + l\nRAlt + ,'
            proxKeys = Label(proxFrame, anchor='nw', text=proxKeysContent)
            proxKeys.pack()
            proxKeys.place(width=55, height=60, anchor='nw', y=20, x=60)

            # Unit hotkeys frame
            unitFrame = Frame(hotFrame, bd=2, relief='ridge')
            unitFrame.pack()
            unitFrame.place(width=119, height=90, x=120, y=60, anchor='nw')

            unitTitle = Label(unitFrame, text='Unit', anchor='center')
            unitTitle.pack(side=TOP)

            unitActionsContent = ' High:\nLow:\n Mute:'
            unitActions = Label(unitFrame, anchor='nw', text=unitActionsContent)
            unitActions.pack()
            unitActions.place(width=55, height=60, anchor='nw', y=20, x=0)

            unitKeysContent = 'RAlt + [\nRAlt + ;\nRAlt + .'
            unitKeys = Label(unitFrame, anchor='nw', text=unitKeysContent)
            unitKeys.pack()
            unitKeys.place(width=55, height=60, anchor='nw', y=20, x=60)

            # Leadership hotkeys frame
            leadershipFrame = Frame(hotFrame, bd=2, relief='ridge')
            leadershipFrame.pack()
            leadershipFrame.place(width=120, height=90, x=239, y=60, anchor='nw')

            leadershipTitle = Label(leadershipFrame, text='Leadership', anchor='center')
            leadershipTitle.pack(side=TOP)

            leadershipActionsContent = ' High:\nLow:\n Mute:'
            leadershipActions = Label(leadershipFrame, anchor='nw', text=leadershipActionsContent)
            leadershipActions.pack()
            leadershipActions.place(width=55, height=60, anchor='nw', y=20, x=0)

            leadershipKeysContent = 'RAlt + ]\nRAlt + \'\nRAlt + /'
            leadershipKeys = Label(leadershipFrame, anchor='nw', text=leadershipKeysContent)
            leadershipKeys.pack()
            leadershipKeys.place(width=55, height=60, anchor='nw', y=20, x=60)

            # Buy me a coffee frame

            coffeeFrame = Frame(infoFrame)
            coffeeFrame.pack()
            coffeeFrame.place(anchor='nw', width=360, height=110, x=20, y=175)

            coffeeMessage = 'Buy Me a Beer?\nHas this app improved your gaming experience? \nAre you pulling off headshots at 400m because you\'re\n no longer distracted by all those PESKY callouts?\n We\'re glad you like the app!\nPlease consider buying us a beer for our hard work.'

            coffeeMessageFrame = Label(coffeeFrame, text=coffeeMessage, pady=0)
            coffeeMessageFrame.pack()
            coffeeMessageFrame.place(width=300, height=90, anchor='n', x=180, y=2)

            def openBeerLink():
                url = 'https://www.buymeacoffee.com/rooseveltcat'
                webbrowser.open_new(url)
            # Render beer Button
            img = Image.open(getPath("assets\\beer.png"))
            img.resize((50, 50), Image.ANTIALIAS)
            finalImg = ImageTk.PhotoImage(img)
            beerBtn = Button(coffeeFrame,  image=finalImg,command=openBeerLink)
            beerBtn.pack()
            beerBtn.image = finalImg
            beerBtn.place(anchor='se', width=35, height=35, x=355, y=105)

        # Render info Button
        def renderInfoBtn():

            img = Image.open(getPath("assets\\info.png"))
            img.resize((50, 50), Image.ANTIALIAS)
            finalImg = ImageTk.PhotoImage(img)
            infoBtn = Button(root, image=finalImg, command=startInfoModal)
            infoBtn.pack()
            infoBtn.image = finalImg
            infoBtn.place(anchor='center', x=380, y=270)
        renderInfoBtn()
        ######################################################

        # Log Label
        def renderLog():
            # Initial Log Message
            settingsObj = getSettingsObj()
            message = 'Resolution at ' + settingsObj.resolution.x + ' x ' + settingsObj.resolution.y + '.  Click button to change.  →'

            # Render
            log = actionsLog = Label(root, text=message, fg='white', bg='black', anchor='center', justify=LEFT, relief='ridge')
            actionsLog.pack()
            actionsLog.place(bordermode=INSIDE, width=300, height=25, x=200, y=310, anchor='center')
            return log
        log = renderLog()

        # call update function
        checkIfTop()
        root.after(500, lambda: update(levelsData, frames, log))
        root.mainloop()

    render()


# PYTHON STANDARD LIBRARY IMPORTS ---------------------------------------------
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from collections import OrderedDict
import math

# DUNDER ----------------------------------------------------------------------
__all__ = [
    "KnitDiNetwork"
]

# LOCAL MODULE IMPORTS --------------------------------------------------------
from cockatoo._knitnetworkbase import KnitNetworkBase
from cockatoo.environment import RHINOINSIDE
from cockatoo.exception import KnitNetworkTopologyError
from cockatoo.utilities import is_ccw_xy
from cockatoo.utilities import pairwise
from cockatoo.utilities import tween_planes

# THIRD PARTY MODULE IMPORTS --------------------------------------------------
import networkx as nx

# RHINO IMPORTS ---------------------------------------------------------------
if RHINOINSIDE:
    import rhinoinside
    rhinoinside.load()
    from Rhino.Geometry import Mesh as RhinoMesh
    from Rhino.Geometry import MeshNgon as RhinoMeshNgon
    from Rhino.Geometry import NurbsSurface as RhinoNurbsSurface
    from Rhino.Geometry import Plane as RhinoPlane
    from Rhino.Geometry import Vector3d as RhinoVector3d
else:
    from Rhino.Geometry import Mesh as RhinoMesh
    from Rhino.Geometry import MeshNgon as RhinoMeshNgon
    from Rhino.Geometry import NurbsSurface as RhinoNurbsSurface
    from Rhino.Geometry import Plane as RhinoPlane
    from Rhino.Geometry import Vector3d as RhinoVector3d

# CLASS DECLARATION -----------------------------------------------------------

class KnitDiNetwork(nx.DiGraph, KnitNetworkBase):
    """
    Datastructure representing a directed graph of nodes aswell as 'weft'
    and 'warp' edges. Used in the automatic generation of knitting patterns.

    Inherits from :class:`networkx.DiGraph`, :class:`KnitNetworkBase`.
    For more info, see *NetworkX* [13]_.

    Notes
    -----
    The implemented algorithms are strongly based on the paper
    *Automated Generation of Knit Patterns for Non-developable Surfaces* [1]_.
    Also see *KnitCrete - Stay-in-place knitted formworks for complex concrete
    structures* [2]_.

    The implementation was further influenced by concepts and ideas presented
    in the papers *Automatic Machine Knitting of 3D Meshes* [3]_,
    *Visual Knitting Machine Programming* [4]_ and
    *A Compiler for 3D Machine Knitting* [5]_.
    """

    # INITIALIZATION ----------------------------------------------------------

    def __init__(self, data=None, **attr):
        """
        Initialize a KnitNetwork (inherits NetworkX graph with edges, name,
        graph attributes.

        Parameters
        ----------
        data : input graph
            Data to initialize graph.  If data=None (default) an empty
            graph is created.  The data can be an edge list, or any
            NetworkX graph object.  If the corresponding optional Python
            packages are installed the data can also be a NumPy matrix
            or 2d ndarray, a SciPy sparse matrix, or a PyGraphviz graph.

        name : string, optional (default='')
            An optional name for the graph.

        attr : keyword arguments, optional (default= no attributes)
            Attributes to add to graph as key=value pairs.
        """

        # initialize using original init method
        super(KnitDiNetwork, self).__init__(data=data, **attr)

        # also copy the mapping_network attribute if it is already available
        if data and isinstance(data, KnitDiNetwork) and data.mapping_network:
            self.mapping_network = data.mapping_network
        else:
            self.mapping_network = None

        # also copy or initialize the halfedge dict for finding faces
        if data and isinstance(data, KnitDiNetwork) and data.halfedge:
            self.halfedge = data.halfedge
        else:
            self.halfedge = {}

    # TEXTUAL REPRESENTATION OF NETWORK ---------------------------------------

    def __repr__(self):
        """
        Return a textual description of the network.

        Returns
        -------
        description : str
            A textual description of the network.
        """

        if self.name != "":
            name = self.name
        else:
            name = "KnitDiNetwork"

        nn = len(self.nodes())
        ce = len(self.contour_edges)
        wee = len(self.weft_edges)
        wae = len(self.warp_edges)
        data = ("({} Nodes, {} Segment Contours, {} Weft, {} Warp)")
        data = data.format(nn, ce, wee, wae)

        return name + data

    def ToString(self):
        """
        Return a textual description of the network.

        Returns
        -------
        description : str
            A textual description of the network.

        Notes
        -----
        Used for overloading the Grasshopper display in data parameters.
        """

        return repr(self)

    # NODE WEFT EDGE METHODS --------------------------------------------------

    def node_weft_edges_out(self, node, data=False):
        """
        Gets the outgoing 'weft' edges connected to the given node.

        Parameters
        ----------
        node : hashable
            Hashable identifier of the node to check for outgoing 'weft' edges.

        data : bool, optional
            If ``True``, will also return the edges associated data attribute
            dictionary.

            Defaults to ``False``.

        Returns
        -------
        weft_edges : :obj:`list`
            List of outgoing 'weft' edges.
        """

        weft_edges = [e for e in
                      self.edges_iter(node, data=True) if e[2]["weft"]]

        if data:
            return weft_edges
        else:
            return [(e[0], e[1]) for e in weft_edges]

    def node_weft_edges_in(self, node, data=False):
        """
        Gets the incoming 'weft' edges connected to the given node.

        Parameters
        ----------
        node : hashable
            Hashable identifier of the node to check for incoming 'weft' edges.

        data : bool, optional
            If ``True``, will also return the edges associated data attribute
            dictionary.

            Defaults to ``False``.

        Returns
        -------
        weft_edges : :obj:`list`
            List of incoming 'weft' edges.
        """

        weft_edges = [e for e in
                      self.in_edges_iter(node, data=True) if e[2]["weft"]]

        if data:
            return weft_edges
        else:
            return [(e[0], e[1]) for e in weft_edges]

    def node_weft_edges(self, node, data=False):
        """
        Gets incoming and outgoing 'weft' edges connected to the given node.

        Parameters
        ----------
        node : hashable
            Hashable identifier of the node to check for incoming and outgoing
            'weft' edges.

        data : bool, optional
            If ``True``, will also return the edges associated data attribute
            dictionary.

            Defaults to ``False``.

        Returns
        -------
        weft_edges : :obj:`list`
            List of incoming and outgoing 'weft' edges.
        """

        weft_edges = [e for e in
                      self.edges_iter(node, data=True) if e[2]["weft"]]
        weft_edges.extend(e for e in
                          self.in_edges_iter(node, data=True) if e[2]["weft"])

        if data:
            return weft_edges
        else:
            return [(e[0], e[1]) for e in weft_edges]

    # NODE WARP EDGE METHODS --------------------------------------------------

    def node_warp_edges_out(self, node, data=False):
        """
        Gets the outgoing 'warp' edges connected to the given node.

        Parameters
        ----------
        node : hashable
            Hashable identifier of the node to check for outgoing 'warp' edges.

        data : bool, optional
            If ``True``, will also return the edges associated data attribute
            dictionary.

            Defaults to ``False``.

        Returns
        -------
        weft_edges : :obj:`list`
            List of outgoing 'warp' edges.
        """

        warp_edges = [e for e in
                      self.edges_iter(node, data=True) if e[2]["warp"]]

        if data:
            return warp_edges
        else:
            return [(e[0], e[1]) for e in warp_edges]

    def node_warp_edges_in(self, node, data=False):
        """
        Gets the incoming 'warp' edges connected to the given node.

        Parameters
        ----------
        node : hashable
            Hashable identifier of the node to check for incoming 'warp' edges.

        data : bool, optional
            If ``True``, will also return the edges associated data attribute
            dictionary.

            Defaults to ``False``.

        Returns
        -------
        weft_edges : :obj:`list`
            List of incoming 'warp' edges.
        """

        warp_edges = [e for e in
                      self.in_edges_iter(node, data=True) if e[2]["warp"]]

        if data:
            return warp_edges
        else:
            return [(e[0], e[1]) for e in warp_edges]

    def node_warp_edges(self, node, data=False):
        """
        Gets the incoming and outgoing 'warp' edges connected to the given
        node.

        Parameters
        ----------
        node : hashable
            Hashable identifier of the node to check for incoming and outgoing
            'warp' edges.

        data : bool, optional
            If ``True``, will also return the edges associated data attribute
            dictionary.

            Defaults to ``False``.

        Returns
        -------
        weft_edges : :obj:`list`
            List of incoming and outgoing 'warp' edges.
        """

        warp_edges = [e for e in
                      self.edges_iter(node, data=True) if e[2]["warp"]]
        warp_edges.extend(e for e in
                          self.in_edges_iter(node, data=True) if e[2]["warp"])

        if data:
            return warp_edges
        else:
            return [(e[0], e[1]) for e in warp_edges]

    # NODE CONTOUR EDGE METHODS -----------------------------------------------

    def node_contour_edges_out(self, node, data=False):
        """
        Gets the outgoing edges marked neither 'warp' nor 'weft' connected to
        the given node.

        Parameters
        ----------
        node : hashable
            Hashable identifier of the node to check for outgoing edges neither
            'weft' nor 'warp'.

        data : bool, optional
            If ``True``, will also return the edges associated data attribute
            dictionary.

            Defaults to ``False``.

        Returns
        -------
        weft_edges : :obj:`list`
            List of outgoing edges neither 'weft' nor 'warp'.
        """

        contour_edges = [(s, e, d) for s, e, d in
                         self.edges_iter(node, data=True)
                         if not d["warp"] and not d["weft"]]

        if data:
            return contour_edges
        else:
            return [(e[0], e[1]) for e in contour_edges]

    def node_contour_edges_in(self, node, data=False):
        """
        Gets the incoming edges marked neither 'warp' nor 'weft' connected to
        the given node.

        Parameters
        ----------
        node : hashable
            Hashable identifier of the node to check for incoming edges neither
            'weft' nor 'warp'.

        data : bool, optional
            If ``True``, will also return the edges associated data attribute
            dictionary.

            Defaults to ``False``.

        Returns
        -------
        weft_edges : :obj:`list`
            List of incoming edges neither 'weft' nor 'warp'.
        """

        contour_edges = [(s, e, d) for s, e, d in
                         self.in_edges_iter(node, data=True)
                         if not d["warp"] and not d["weft"]]

        if data:
            return contour_edges
        else:
            return [(e[0], e[1]) for e in contour_edges]

    def node_contour_edges(self, node, data=False):
        """
        Gets the incoming and outcoing edges marked neither 'warp' nor 'weft'
        connected to the given node.

        Parameters
        ----------
        node : hashable
            Hashable identifier of the node to check for incoming and outgoing
            edges neither 'weft' nor 'warp'.

        data : bool, optional
            If ``True``, will also return the edges associated data attribute
            dictionary.

            Defaults to ``False``.

        Returns
        -------
        weft_edges : :obj:`list`
            List of incoming and outgoing edges neither 'weft' nor 'warp'.
        """

        contour_edges = [(s, e, d) for s, e, d in
                         self.edges_iter(node, data=True)
                         if not d["warp"] and not d["weft"]]
        contour_edges.extend([(s, e, d) for s, e, d in
                              self.in_edges_iter(node, data=True)
                              if not d["warp"] and not d["weft"]])

        if data:
            return contour_edges
        else:
            return [(e[0], e[1]) for e in contour_edges]

    # FIND FACES (CYCLES) OF NETWORK ------------------------------------------

    def _sort_node_neighbors(self, key, nbrs, xyz, geo,
                             cbp, nrm, mode=-1, ccw=True):
        """
        Sort the neighbors of a network node.

        Notes
        -----
        Based on an implementation inside the COMPAS framework.
        For more info see [7]_.

        References
        ----------
        .. [7] Van Mele, Tom et al. *COMPAS: A framework for computational
               research in architecture and structures*.

               See: `sort_node_neighbors() inside COMPAS <https://github.com/
               compas-dev/compas/blob/e313502995b0dd86d460f86e622cafc0e29d1b75/
               src/compas/datastructures/network/duality.py#L132>`_
        """

        # if there is only one neighbor we don't need to sort anything
        if len(nbrs) == 1:
            return nbrs

        # initialize the ordered list of neighbors with the first node
        ordered_nbrs = nbrs[0:1]

        # retrieve coordinates for current node
        a = xyz[key]

        # compute local orientation if reference geometry data is present
        # CASE 1: Plane is determined by mesh normal of origin node
        if cbp and nrm and mode == 0:
            # construct local reference plane and map coordinates to plane
            # space
            a_geo = geo[key]
            localplane = RhinoPlane(a_geo, nrm[key])
            a_local = localplane.RemapToPlaneSpace(a_geo)[1]
            a = (a_local.X, a_local.Y, a_local.Z)
            # compute local plane coordinates for all neighbors
            xyz_local = {}
            for nbr in nbrs:
                # find closest point on plane and remap to plane space
                nbr_cp = localplane.ClosestPoint(geo[nbr])
                local_nbr = localplane.RemapToPlaneSpace(nbr_cp)[1]
                nbr_coords = (local_nbr.X, local_nbr.Y, local_nbr.Z)
                # set coordinate dict value
                xyz_local[nbr] = nbr_coords
            # reassign coordinate dictionary for neighbor sorting
            xyz = xyz_local
        # CASE 2: Plane is determined by average normal of origin node and nbrs
        elif cbp and nrm and mode == 1:
            # construct local reference plane and map coordinates to plane
            # space
            a_geo = geo[key]
            # get average normal
            avg_nrm = nrm[key]
            nbr_nrms = [nrm[n] for n in nbrs]
            for nv in nbr_nrms:
                avg_nrm += nv
            # construct plane based on average normal
            localplane = RhinoPlane(a_geo, avg_nrm)
            a_local = localplane.RemapToPlaneSpace(a_geo)[1]
            a = (a_local.X, a_local.Y, a_local.Z)
            # compute local plane coordinates for all neighbors
            xyz_local = {}
            for nbr in nbrs:
                # find closest point on plane and remap to plane space
                nbr_cp = localplane.ClosestPoint(geo[nbr])
                local_nbr = localplane.RemapToPlaneSpace(nbr_cp)[1]
                nbr_coords = (local_nbr.X, local_nbr.Y, local_nbr.Z)
                # set coordinate dict value
                xyz_local[nbr] = nbr_coords
            # reassign coordinate dictionary for neighbor sorting
            xyz = xyz_local
        # CASE 3: Plane is determined by avg between fitplane and avg meshplane
        elif cbp and nrm and mode == 2:
            # construct local reference plane and map coordinates to plane
            # space
            a_geo = geo[key]
            # get average normal
            avg_nrm = nrm[key]
            nbr_nrms = [nrm[n] for n in nbrs]
            for nv in nbr_nrms:
                avg_nrm += nv
            # construct plane based on average normal
            localplane = RhinoPlane(a_geo, avg_nrm)
            fitplane = RhinoPlane.FitPlaneToPoints([geo[n] for n in nbrs])[1]
            # align fitplane with localplane
            if fitplane.Normal * localplane.Normal < 0:
                fitplane.Flip()
            # tween the planes and set origin
            tweenplane = tween_planes(localplane, fitplane, 0.5)
            tweenplane.Origin = a_geo
            # remap origin node to plane space
            a_local = tweenplane.RemapToPlaneSpace(a_geo)[1]
            a = (a_local.X, a_local.Y, a_local.Z)
            # compute local plane coordinates for all neighbors
            xyz_local = {}
            for nbr in nbrs:
                # find closest point on plane and remap to plane space
                nbr_cp = tweenplane.ClosestPoint(geo[nbr])
                local_nbr = tweenplane.RemapToPlaneSpace(nbr_cp)[1]
                nbr_coords = (local_nbr.X, local_nbr.Y, local_nbr.Z)
                # set coordinate dict value
                xyz_local[nbr] = nbr_coords
            # reassign coordinate dictionary for neighbor sorting
            xyz = xyz_local

        # loop over all neighbors except the first one
        for i, nbr in enumerate(nbrs[1:]):
            c = xyz[nbr]
            pos = 0
            b = xyz[ordered_nbrs[pos]]
            while not is_ccw_xy(a, b, c):
                pos += 1
                if pos > i:
                    break
                b = xyz[ordered_nbrs[pos]]
            if pos == 0:
                pos -= 1
                b = xyz[ordered_nbrs[pos]]
                while is_ccw_xy(a, b, c):
                    pos -= 1
                    if pos < -len(ordered_nbrs):
                        break
                    b = xyz[ordered_nbrs[pos]]
                pos += 1
            ordered_nbrs.insert(pos, nbr)

        # return the ordered neighbors in cw or ccw order
        if not ccw:
            return ordered_nbrs[::-1]
        return ordered_nbrs

    def _sort_neighbors(self, mode=-1, ccw=True):
        """
        Sort the neighbors of all network nodes.

        Notes
        -----
        Based on an implementation inside the COMPAS framework.
        For more info see [8]_.

        References
        ----------
        .. [8] Van Mele, Tom et al. *COMPAS: A framework for computational
               research in architecture and structures*.

               See: `sort_neighbors() inside COMPAS <https://github.com/
               compas-dev/compas/blob/e313502995b0dd86d460f86e622cafc0e29d1b75/
               src/compas/datastructures/network/duality.py#L121>`_
        """

        # initialize sorted neighbors dict
        sorted_neighbors = {}

        # get dictionary of all coordinates by node index
        xyz = {k: (d["x"], d["y"], d["z"]) for k, d in self.nodes_iter(True)}
        geo = {k: d["geo"] for k, d in self.nodes_iter(True)}

        # compute local orientation data when reference geometry is present
        try:
            reference_geometry = self.graph["reference_geometry"]
        except KeyError:
            reference_geometry = None

        if not reference_geometry:
            cbp = None
            nrm = None
        elif isinstance(reference_geometry, RhinoMesh):
            cbp = {k: reference_geometry.ClosestMeshPoint(geo[k], 0)
                   for k in self.nodes_iter()}
            nrm = {k: reference_geometry.NormalAt(cbp[k])
                   for k in self.nodes_iter()}
        elif isinstance(reference_geometry, RhinoNurbsSurface):
            cbp = {k: reference_geometry.ClosestPoint(geo[k])[1:]
                   for k in self.nodes_iter()}
            nrm = {k: reference_geometry.NormalAt(cbp[k][0], cbp[k][1])
                   for k in self.nodes_iter()}

        # loop over all nodes in network
        for key in self.nodes_iter():
            nbrs = self[key].keys()
            sorted_neighbors[key] = self._sort_node_neighbors(
                                                            key,
                                                            nbrs,
                                                            xyz,
                                                            geo,
                                                            cbp,
                                                            nrm,
                                                            mode=mode,
                                                            ccw=ccw)

        # set the sorted neighbors list as an attribute to the nodes
        for key, nbrs in sorted_neighbors.items():
            self.node[key]["sorted_neighbors"] = nbrs[::-1]

        # return the sorted neighbors dict
        return sorted_neighbors

    def _find_first_node_neighbor(self, key):
        """
        Find the first neighbor for a given node in the network.

        Notes
        -----
        Based on an implementation inside the COMPAS framework.
        For more info see [9]_.

        References
        ----------
        .. [9] Van Mele, Tom et al. *COMPAS: A framework for computational
               research in architecture and structures*.

               See: `find_first_node_neighbor() inside COMPAS <https://
               github.com/compas-dev/compas/blob/
               e313502995b0dd86d460f86e622cafc0e29d1b75/src/compas/
               datastructures/network/duality.py#L103>`_
        """

        # get all node neighbors
        nbrs = self[key].keys()

        # if there is only one neighbor, we have already found our candidate
        if len(nbrs) == 1:
            return nbrs[0]

        ab = [-1.0, -1.0, 0.0]
        rhino_ab = RhinoVector3d(*ab)
        a = self.node_coordinates(key)
        b = [a[0] + ab[0], a[1] + ab[1], 0]

        angles = []
        for nbr in nbrs:
            c = self.node_coordinates(nbr)
            ac = [c[0] - a[0], c[1] - a[1], 0]
            rhino_ac = RhinoVector3d(*ac)
            alpha = RhinoVector3d.VectorAngle(rhino_ab, rhino_ac)
            if is_ccw_xy(a, b, c, True):
                alpha = (2 * math.pi) - alpha
            angles.append(alpha)

        return nbrs[angles.index(min(angles))]

    def _find_edge_cycle(self, u, v):
        """
        Find a cycle based on the given edge.

        Parameters
        ----------
        u : hashable
            Index of the start node of the origin edge for the cycle.

        v : hashable
            Index of the end node of the origin edge for the cycle.

        Notes
        -----
        Based on an implementation inside the COMPAS framework.
        For more info see [6]_.

        References
        ----------
        .. [6] Van Mele, Tom et al. *COMPAS: A framework for computational
               research in architecture and structures*.

               See: `find_edge_cycle() inside COMPAS <https://github.com/
               compas-dev/compas/blob/09153de6718fb3d49a4650b89d2fe91ea4a9fd4a/
               src/compas/datastructures/network/duality.py#L161>`_
        """
        cycle = [u]
        while True:
            cycle.append(v)
            nbrs = self.node[v]["sorted_neighbors"]
            nbr = nbrs[nbrs.index(u) - 1]
            u, v = v, nbr
            if v == cycle[0]:
                break
        return cycle

    def find_cycles(self, mode=-1):
        """
        Finds the cycles (faces) of this network by utilizing a wall-follower
        mechanism.

        Parameters
        ----------
        mode : int, optional
            Determines how the neighbors of each node are sorted when finding
            cycles for the network.

            ``-1`` equals to using the world XY plane.

            ``0`` equals to using a plane normal to the origin nodes closest
            point on the reference geometry.

            ``1`` equals to using a plane normal to the average of the origin
            and neighbor nodes' closest points on the reference geometry.

            ``2`` equals to using an average plane between a plane fit to the
            origin and its neighbor nodes and a plane normal to the origin
            nodes closest point on the reference geometry.

            Defaults to ``-1``.

        Warning
        -------
        Modes other than -1 (default) are only possible if this network has an
        underlying reference geometry in form of a Mesh or NurbsSurface. The
        reference geometry should be assigned when initializing the network by
        assigning the geometry to the "reference_geometry" attribute of the
        network.

        Notes
        -----
        Based on an implementation inside the COMPAS framework.
        For more info see [17]_.

        References
        ----------
        .. [17] Van Mele, Tom et al. *COMPAS: A framework for computational
               research in architecture and structures*.

               See: `find_cycles() inside COMPAS <https://github.com/
               compas-dev/compas/blob/09153de6718fb3d49a4650b89d2fe91ea4a9fd4a/
               src/compas/datastructures/network/duality.py#L20>`_
        """

        # initialize the halfedge dict of the directed network
        for u, v in self.edges_iter():
            try:
                self.halfedge[u][v] = None
            except KeyError:
                self.halfedge[u] = {}
                self.halfedge[u][v] = None
            try:
                self.halfedge[v][u] = None
            except KeyError:
                self.halfedge[v] = {}
                self.halfedge[v][u] = None

        # sort the all the neighbors for each node of the network
        self._sort_neighbors(mode=mode)

        # find start node
        # sort leaf nodes by y and x coordinates
        # leaves = self.leaf_nodes
        # if leaves:
        #     u = sorted(leaves, key=lambda n: (n[1]["y"], n[1]["x"]))[0][0]
        # else:
        #     u = sorted(self.nodes_iter(data=True),
        #                key=lambda n: (n[1]["y"], n[1]["x"]))[0][0]

        # find start node
        # sort leaf nodes by node identifier / index
        leaves = self.leaf_nodes
        if leaves:
            u = sorted(leaves, key=lambda n: n[0])[0][0]
        else:
            u = sorted(self.nodes_iter(data=True), key=lambda n: n[0])[0][0]

        # initialize found and cycles dict
        cycles = {}
        found = {}
        ckey = 0

        # find the very first cycle
        v = self._find_first_node_neighbor(u)
        cycle = self._find_edge_cycle(u, v)
        frozen = frozenset(cycle)
        found[frozen] = ckey
        cycles[ckey] = cycle

        # set halfedge dict
        for a, b in pairwise(cycle + cycle[:1]):
            self.halfedge[a][b] = ckey
        ckey += 1

        # loop over all edges and find cycles
        for u, v in self.edges_iter():
            # find cycles for u -> v edges
            if self.halfedge[u][v] == None:
                cycle = self._find_edge_cycle(u, v)
                frozen = frozenset(cycle)
                if frozen not in found:
                    found[frozen] = ckey
                    cycles[ckey] = cycle
                    ckey += 1
                for a, b in pairwise(cycle + cycle[:1]):
                    self.halfedge[a][b] = found[frozen]
            # find cycles for v -> u edges
            if self.halfedge[v][u] == None:
                cycle = self._find_edge_cycle(v, u)
                frozen = frozenset(cycle)
                if frozen not in found:
                    found[frozen] = ckey
                    cycles[ckey] = cycle
                    ckey += 1
                for a, b in pairwise(cycle + cycle[:1]):
                    self.halfedge[a][b] = found[frozen]

        return cycles

    # MESHING -----------------------------------------------------------------

    def create_mesh(self, mode=-1, max_valence=4):
        """
        Constructs a mesh from this network by finding cycles and using them as
        mesh faces.

        Parameters
        ----------
        mode : int, optional
            Determines how the neighbors of each node are sorted when finding
            cycles for the network.

            ``-1`` equals to using the world XY plane.

            ``0`` equals to using a plane normal to the origin nodes closest
            point on the reference geometry.

            ``1`` equals to using a plane normal to the average of the origin
            and neighbor nodes' closest points on the reference geometry.

            ``2`` equals to using an average plane between a plane fit to the
            origin and its neighbor nodes and a plane normal to the origin
            nodes closest point on the reference geometry.

            Defaults to ``-1``.

        max_valence : int, optional
            Sets the maximum edge valence of the faces. If this is set to > 4,
            n-gon faces (more than 4 edges) are allowed. Otherwise, their
            cycles are treated as invalid and will be ignored.

            Defaults to ``4``.

        Warning
        -------
        Modes other than ``-1`` are only possible if this network has an
        underlying reference geometry in form of a Mesh or NurbsSurface. The
        reference geometry should be assigned when initializing the network by
        assigning the geometry to the "reference_geometry" attribute of the
        network.
        """

        # get cycles dict of this network
        cycles = self.find_cycles(mode=mode)

        # create an empty mesh
        Mesh = RhinoMesh()

        # intialize mapping of network nodes to mesh vertices
        node_to_vertex = {}
        vcount = 0

        # fill mesh and map network nodes to mesh vertices
        for node, data in self.nodes_iter(data=True):
            node_to_vertex[node] = vcount
            Mesh.Vertices.Add(data["x"], data["y"], data["z"])
            vcount += 1

        # loop over cycles and add faces to the mesh
        fcount = 0
        for ckey in cycles.keys():
            cycle = cycles[ckey]
            c_len = len(cycle)
            if c_len > 4:
                if c_len > max_valence:
                    continue
                # find centroid of ngon nodes
                cycle_coords = [[self.node[k]["x"],
                                 self.node[k]["y"],
                                 self.node[k]["z"]] for k in cycle]
                # compute centroid
                c_x, c_y, c_z = zip(*cycle_coords)
                centroid = [sum(c_x) / c_len,
                            sum(c_y) / c_len,
                            sum(c_z) / c_len]
                # add centroid to mesh
                Mesh.Vertices.Add(*centroid)
                # create triangle with centroid for every pair in cycle
                closed_cycle = cycle[:]
                closed_cycle.append(cycle[0])
                ngon_faces = []
                for a, b in pairwise(closed_cycle):
                    Mesh.Faces.AddFace(node_to_vertex[a],
                                       node_to_vertex[b],
                                       vcount)
                    ngon_faces.append(fcount)
                    fcount += 1
                ngon_cycle = [node_to_vertex[n] for n in cycle]
                RhinoMeshNgon.Create(ngon_cycle, ngon_faces)
                # increment mesh vertex counter
                vcount += 1
            elif c_len < 3:
                continue
            else:
                mesh_cycle = [node_to_vertex[n] for n in cycle]
                Mesh.Faces.AddFace(*mesh_cycle)
                fcount += 1

        # unify the normals of the mesh
        Mesh.UnifyNormals()

        return Mesh

    # CONVERSION TO 2D-KNITTINGPATTERN (PIXEL IMAGE) --------------------------

    def verify_dual_form(self):
        """
        Verifies this network to have the correct form of a dual as needed for
        representing this network as a 2d knitting pattern.

        Returns
        -------
        bool
            ``True`` on success, ``False`` otherwise.
        """

        # check every single node
        for node in self.nodes_iter():
            # verify if all nodes have the correct keys for attributes
            try:
                start = self.node[node]["start"]
                end = self.node[node]["end"]
                inc = self.node[node]["increase"]
                dec = self.node[node]["decrease"]
                lfn = self.node[node]["leaf"]
                geo = self.node[node]["geo"]
            except KeyError:
                return False

            # get all neighbors
            prd = self.predecessors(node)
            lpr = len(prd)
            # check for nodes with more than two predecessors
            if lpr > 2:
                return False

            suc = self.successors(node)
            lsu = len(suc)
            # check for nodes with more than two successors
            if lsu > 2:
                return False

            nbr = prd + suc
            lnn = len(nbr)
            # check for disconnected nodes
            if not lnn:
                return False

            # maximum connections per node is four
            elif lnn > 4:
                return False

        return True

    def make_pattern_data(self, consolidate=False):
        """
        Topological sort this network to represent it as 2d knitting pattern
        consisting of rows and columns.

        Parameters
        ----------
        consolidate : bool
            If ``True``, will consolidate the final pattern data.
            Defaulst to ``False``.

        Returns
        -------
        pattern_data : :obj:`list` of :obj:`list`
            List (rows) of lists (column values) where every value represents
            a node.

        Raises
        ------
        KnitNetworkTopologyError
            if the network does not satisfy the topology constraints needed for
            this operation and the outcome would be unfeasible or
            unpredictable.

        Notes
        -----
        Closely resembles the implementation described in *Automated Generation
        of Knit Patterns for Non-developable Surfaces* [1]_. Also see
        *KnitCrete - Stay-in-place knitted formworks for complex concrete
        structures* [2]_.
        """

        # initialize dict for seen nodes and list for storage of rows
        seenrows = {}
        seencols = {}
        rows = []
        cols = []

        # initialize mapping dicts for ordering of rows and columns
        id2row = OrderedDict()
        id2col = OrderedDict()
        node2rowid = OrderedDict()
        node2colid = OrderedDict()

        # BUILD ROWS ----------------------------------------------------------

        # every 'end' node defines the start of a row
        # loop over all 'end' nodes
        for node, data in self.end_nodes:
            # continue if this node has already been visited
            if node in seenrows:
                continue

            # get outgoing 'weft' edges of the current 'end' node
            nodeweft_out = self.node_weft_edges_out(node, data=True)
            nodeweft_in = self.node_weft_edges_in(node, data=True)

            # skip 'end' nodes which have only incoming 'weft' edges
            if nodeweft_in and not nodeweft_out:
                continue

            # if there is more than one outgoing 'weft' edge, we have a problem
            if len(nodeweft_out) > 1:
                errMsg = "More than one outgoing 'weft' edge at " + \
                         "first row node {}!".format(node)
                raise KnitNetworkTopologyError(errMsg)
            # if this is a singular node, it is a separate row.
            elif len(nodeweft_out) == 0 and len(nodeweft_in) == 0:
                # append it as a row to the list of rows
                rows.append([node])
                # set the mapping dictionaries
                row_id = (node, node)
                id2row[row_id] = [node]
                node2rowid.update({node: row_id})
                # set the seen marker and continue to next 'end' node
                seenrows[node] = True
                continue
            # if there is exactly one 'weft' edge, traverse until next node
            elif len(nodeweft_out) == 1:
                # get the connected node to the current node
                connected_node = (nodeweft_out[0][1],
                                  self.node[nodeweft_out[0][1]])
                # define initial row nodes with nodes of the first edge
                row_nodes = [node, connected_node[0]]
                # traverse as long as there is an outgoing next 'weft' edge
                # until an 'end' node is discovered
                while True:
                    # get 'weft' edges of last node in row nodes
                    next_weft = self.node_weft_edges_out(row_nodes[-1])
                    # if there is more than one connected 'weft' edge, we
                    # have a problem
                    if len(next_weft) > 1:
                        errMsg = "More than one outgoing 'weft' edge at " + \
                                 "row node {}!".format(node)
                        raise KnitNetworkTopologyError(errMsg)
                    # if there are no next 'weft' edges, row is complete
                    elif len(next_weft) == 0:
                        if self.node[row_nodes[-1]]["end"]:
                            # this is the finishing 'end' node; set it seen
                            # and complete this row by breaking
                            seenrows[row_nodes[-1]] = True
                            break
                        # if there are no next 'weft' edges but this is not
                        # an 'end' node, we have a problem
                        else:
                            # see if there are incoming 'weft' edges at the
                            # current node which are not the way we came from
                            next_weft = [nw for nw in self.node_weft_edges_in(
                                         row_nodes[-1], data=True)
                                         if nw[0] != row_nodes[-2]]

                            # try to reverse them as a failsafe for imperfect
                            # topological dual graphs
                            if len(next_weft) == 1:
                                # flip geometry first, then the graph edge
                                nwe = next_weft[0]
                                nw_attr = nwe[2].copy()
                                nw_attr["geo"].Flip()
                                self.remove_edge(nwe[0], nwe[1])
                                self.add_edge(nwe[1],
                                              nwe[0],
                                              attr_dict=nw_attr)
                            else:
                                errMsg = ("Unexpected end of row. Missing " +
                                          "'end' attribute at node {}!")
                                errMsg.format(row_nodes[-1])
                                raise KnitNetworkTopologyError(errMsg)

                    # if there is a next node over a 'weft' edge, append to
                    # row and continue
                    if len(next_weft) == 1:
                        row_nodes.append(next_weft[0][1])
                        continue
                # append the completed row to the list of rows
                rows.append(row_nodes)
                # set the mapping dictionaries
                row_id = (row_nodes[0], row_nodes[-1])
                id2row[row_id] = row_nodes
                node2rowid.update({node: row_id for node in row_nodes})
                # finally, set the current node as seen
                seenrows[node] = True

        # BUILD COLUMNS -------------------------------------------------------

        # every 'end' node defines the start of a row
        # loop over all 'end' nodes
        col_sources = [(n, d) for n, d in self.nodes_iter(data=True)
                       if d["increase"] or d["leaf"] or d["end"]]
        for node, data in col_sources:
            # continue if this node has already been visited
            if node in seencols:
                continue

            # get outgoing 'warp' edges of the current node
            nodewarp_out = self.node_warp_edges_out(node, data=True)
            nodewarp_in = self.node_warp_edges_in(node, data=True)

            # skip nodes which have incoming 'warp' edges
            if nodewarp_in:
                continue

            # if there is more than one outgoing 'warp' edge, we have a problem
            if len(nodewarp_out) > 1:
                errMsg = "More than one outgoing 'warp' edge at " + \
                         "first column node {}!".format(node)
                raise KnitNetworkTopologyError(errMsg)
            # if this is a singular node, it is a separate column (?)
            elif len(nodewarp_out) == 0 and len(nodewarp_in) == 0:
                # errMsg = ("Absolutely no 'warp' edges at " +
                #           "node {}!".format(node))
                # raise KnitNetworkTopologyError(errMsg)

                # append it as a column to the list of columns
                cols.append([node])
                # set the mapping dictionaries
                col_id = (node, node)
                id2col[col_id] = [node]
                node2colid.update({node: col_id})
                # set the seen marker and continue to next node
                seencols[node] = True
                continue
            # if there is exactly one 'warp' edge, traverse until next node
            elif len(nodewarp_out) == 1:
                # get the connected node to the current node
                connected_node = (nodewarp_out[0][1],
                                  self.node[nodewarp_out[0][1]])
                # define initial column nodes with nodes of the first edge
                col_nodes = [node, connected_node[0]]
                # traverse as long as there is an outgoing next 'warp' edge
                while True:
                    # get 'warp' edges of last node in row nodes
                    next_warp = self.node_warp_edges_out(col_nodes[-1])
                    # if there is more than one connected 'warp' edge, we
                    # have a problem
                    if len(next_warp) > 1:
                        errMsg = "More than one outgoing 'warp' edge at " + \
                                 "col node {}!".format(node)
                        raise KnitNetworkTopologyError(errMsg)
                    # if there are no next 'warp' edges, column is complete
                    elif len(next_warp) == 0:
                        seencols[col_nodes[-1]] = True
                        break
                    # if there is a next node over a 'warp' edge, append to
                    # column and continue
                    elif len(next_warp) == 1:
                        col_nodes.append(next_warp[0][1])
                        continue
                # append the completed column to the list of columns
                cols.append(col_nodes)
                # set the mapping dictionaries
                col_id = (col_nodes[0], col_nodes[-1])
                id2col[col_id] = col_nodes
                node2colid.update({node: col_id for node in col_nodes})
                # finally, set the current node as seen
                seencols[node] = True

        # BUILD ROW MAPPING FOR TOPOLOGICAL SORT ------------------------------

        # initialize mapping for topological sort of rows
        row_map = nx.DiGraph()
        row_ids = id2row.keys()
        # find all targets of all rows by checking all row nodes
        # for targets and getting the corresponding row
        for row_id in row_ids:
            # get row from mapping dict
            row = id2row[row_id]
            # initialize list for storage of targets
            target_ids = []
            # loop over all nodes in the current row
            for node in row:
                # check the node for outgoing 'warp' edges and get its
                # successor
                try:
                    node_suc = self.node_warp_edges_out(node)[0][1]
                except IndexError:
                    continue
                # find the id of the row which contains the 'warp' edge
                # successor node
                target_id = node2rowid[node_suc]
                # if we already found this id before, continue
                if target_id in target_ids:
                    continue
                # if its a new id, append it to the list of found target ids
                target_ids.append(target_id)

            [row_map.add_edge(row_id, tid) for tid in target_ids]

        # BUILD COLUMN MAPPING FOR TOPOLOGICAL SORT ---------------------------

        # initialize mapping for backtracking of columns
        col_map = nx.DiGraph()
        col_ids = id2col.keys()
        # find all targets of all columns by checking all column nodes
        # for targets and getting the corresponding column
        for col_id in col_ids:
            # get column from mapping dict
            col = id2col[col_id]
            # initialize list for storage of targets
            target_ids = []
            # loop over all nodes in the current column
            for node in col:
                # check the node for outgoing 'weft' edges and get its
                # successor
                try:
                    node_suc = self.node_weft_edges_out(node)[0][1]
                except IndexError:
                    continue
                # find the id of the column which contains the 'weft' edge
                # successor node
                target_id = node2colid[node_suc]
                # if we already found this id before, continue
                if target_id in target_ids:
                    continue
                # if its a new id, append it to the list of found target ids
                target_ids.append(target_id)

            [col_map.add_edge(col_id, tid) for tid in target_ids]

        # TOPOLOGICAL SORT OF ROWS --------------------------------------------

        # own method of topological sort for rows (in utilities)
        # ordered_row_stack = resolve_order_by_backtracking(row_map)

        # use nx topological sort for rows
        try:
            ordered_row_ids = nx.topological_sort(row_map)
        except nx.NetworkXError as e:
            raise KnitNetworkTopologyError(str(e.message))
        except nx.NetworkXUnfeasible as e:
            raise KnitNetworkTopologyError(str(e.message))

        # get the rows with the topological sorted result
        toposort_rows = [id2row[id] for id in ordered_row_ids]
        for i, row in enumerate(toposort_rows):
            for n in row:
                self.node[n]["chain"] = i

        if consolidate:
            # HORIZONTAL CONSOLIDATION ----------------------------------------
            consolidated_rows = []
            for i, row in enumerate(toposort_rows):
                # if we are at the first row, just insert it
                if i == 0:
                    consolidated_rows.append(row)
                    continue

                # else get the previously inserted rows
                prevrows = reversed(consolidated_rows)
                # find first incoming warp edge that connects to a previous row
                for prevrow in prevrows:
                    row_found = False
                    for node in row:
                        warp_in = self.node_warp_edges_in(node)
                        if warp_in:
                            if warp_in[0][0] not in prevrow:
                                continue
                            connection_node = warp_in[0][1]
                            connection_index = row.index(connection_node)
                            prevrow_connection_node = warp_in[0][0]
                            prevrow_connection_index = prevrow.index(
                                                    prevrow_connection_node)
                            row_found = True
                    if row_found:
                        break

                # compute offset
                if connection_index == prevrow_connection_index:
                    offset = 0
                else:
                    offset = prevrow_connection_index - connection_index

                # execute offset and append to consolidated rows
                if offset == 0:
                    consolidated_rows.append(row)
                elif offset < 0:
                    for consrow in consolidated_rows:
                        for o in range(abs(offset)):
                            consrow.insert(0, -1)
                    consolidated_rows.append(row)
                elif offset > 0:
                    offset_list = [-1] * abs(offset)
                    offset_list.extend(row)
                    consolidated_rows.append(offset_list)

            # fill to minlength
            minrl = max([len(row) for row in consolidated_rows])
            for row in consolidated_rows:
                if len(row) != minrl:
                    row.extend([-1] * (minrl - len(row)))

            # VERTICAL CONSOLIDATION ------------------------------------------
            vert_consolidated_rows = []
            for i, row in enumerate(consolidated_rows):
                if i == 0:
                    vert_consolidated_rows.append(row)
                    continue

                # get the first index that is not -1
                rowstart = None
                rowend = None
                for j, nodevalue in enumerate(row):
                    if nodevalue != -1 and rowstart == None:
                        rowstart = j
                        if j == len(row) - 1:
                            rowend = rowstart
                            break
                        continue
                    elif nodevalue == -1 and rowstart != None:
                        rowend = j
                        break
                    elif (nodevalue != -1 and
                          j == len(row) - 1 and
                          rowstart != None):
                        rowend = j
                        break

                # get previous inserted rows and loop over them
                prevrows = reversed(vert_consolidated_rows)

                insertion_index = None
                for j, prevrow in enumerate(prevrows):
                    # get window
                    window = prevrow[rowstart:rowend]
                    # check if the window on the previous row is free
                    if window == [-1] * len(window):
                        insertion_index = len(vert_consolidated_rows) - 1 - j
                        continue
                    else:
                        break

                if insertion_index != None:
                    insertion_row = vert_consolidated_rows[insertion_index]
                    merged_row = insertion_row[:rowstart]
                    merged_row.extend(row[rowstart:rowend + 1])
                    merged_row.extend(insertion_row[rowend + 1:])

                    vert_consolidated_rows[insertion_index] = merged_row
                else:
                    vert_consolidated_rows.append(row)

            return vert_consolidated_rows

        else:
            # TOPOLOGICAL SORT OF COLUMNS -------------------------------------

            # own method of topological sort for columns (in utilities)
            # ordered_column_stack = resolve_order_by_backtracking(col_map)

            # use nx topological sort for columns
            try:
                ordered_column_stack = nx.topological_sort(col_map)
            except nx.NetworkXError as e:
                raise KnitNetworkTopologyError(str(e.message))
            except nx.NetworkXUnfeasible as e:
                raise KnitNetworkTopologyError(str(e.message))

            # SPREAD OUT BY FILLING WITH -1 FILLER ----------------------------

            # fill all the rows to minimum row length with
            # placeholder values (-1)
            minrl = max([len(row) for row in rows])
            # loop over all rows and fill until minimum length
            for key in id2row.keys():
                row = id2row[key]
                for j in range(minrl):
                    try:
                        node = row[j]
                    except IndexError:
                        row.append(-2)
                id2row[key] = row

            # spread out rows according to toposorted columns
            for i, col in enumerate(ordered_column_stack):
                # get column nodes
                colnodes = id2col[col]
                # loop over all rows
                for j, row in enumerate(toposort_rows):
                    # check the entry at the current column index
                    # if this entry is not in colnodes, shift it to the right
                    entry = row[i]
                    if entry in colnodes:
                        toposort_rows[j].append(-2)
                    elif entry not in colnodes:
                        toposort_rows[j].insert(i, -1)

            # trim final topological sorted rows
            trim = toposort_rows[0].index(-2)
            toposort_rows = [btr[:trim] for btr in toposort_rows]

        # return all sorted rows
        return toposort_rows

# MAIN ------------------------------------------------------------------------

if __name__ == '__main__':
    pass


import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, mean_squared_error, mean_squared_log_error, median_absolute_error

# read train data
# =============================================================================
df = pd.read_csv("data/data_features4.csv",
                          encoding = "big5")

train_label = pd.read_csv("data/train_label.csv",
                          encoding = "big5")

# 移除 key 跟 index 欄位
df = df.drop(columns=['Key', 'Index'])

# 移除 其他不想要的
# df = df.drop(
#     columns=[
#             '交易月份',
#             '交易標的',
#             '建物現況格局-隔間',
#             '建物移轉總面積(平方公尺)_log',
#             '移轉層次',
#             'nearest_tarin_station',
#             'low_use_electricity_log',
#             'nearest_tarin_station_distance_log',
#             'lat_norm',
#             'lng_norm',
#             ])

# 保留要的
## 相關係數
df = df[['nearest_tarin_station_distance_log',
          '建物現況格局-房', '建物現況格局-廳',
          '建物現況格局-隔間', 'lat_norm', 'nearest_tarin_station',
          '總樓層數', '主要建材',
          '鄉鎮市區', '土地移轉總面積(平方公尺)_log_2', '建物型態', '土地',
          '都市土地使用分區',
          'income_var_log_2', 'income_avg_log_2']]

## RF
# df = df[['土地', '土地移轉總面積(平方公尺)_log_2', '建物移轉總面積(平方公尺)_log', '都市土地使用分區','num_of_bus_stations_in_100m', 'low_use_electricity_log']]

## 自己挑
# df = df[['土地', '土地移轉總面積(平方公尺)_log_2', '建物型態', '建物現況格局-廳', '建物現況格局-房', '總樓層數' ,'都市土地使用分區', '鄉鎮市區', 'num_of_bus_stations_in_100m', 'income_avg_log_2', 'income_var_log_2']]

## 鄉鎮ord_相關 data_features4
# df = df[['nearest_tarin_station_distance_log', '建物現況格局-房', '建物現況格局-廳', '建物現況格局-隔間', 'lat_norm', 'nearest_tarin_station', '總樓層數', '主要建材', '土地移轉總面積(平方公尺)_log_2', '建物型態', '土地', '都市土地使用分區', 'income_var_log_2','income_avg_log_2', '鄉鎮市區']]

## 鄉鎮lab_相關 data_features3
# df = df[['nearest_tarin_station_distance_log', '建物現況格局-房', '建物現況格局-廳', '建物現況格局-隔間', 'lat_norm', 'nearest_tarin_station', '總樓層數', '主要建材', '土地移轉總面積(平方公尺)_log_2', '建物型態', '土地_log_2', '都市土地使用分區', 'income_var_log_2','income_avg_log_2', '鄉鎮市區']]

## 鄉鎮lab_相關 data_features3  , '建物現況格局-隔間'X
# df = df[['nearest_tarin_station_distance_log', '建物現況格局-房', '建物現況格局-廳', 'lat_norm', '總樓層數', '主要建材', '土地移轉總面積(平方公尺)_log_2', '建物型態', '土地_log_2', '都市土地使用分區', 'income_var_log_2','income_avg_log_2',
#           '鄉鎮市區',
#           'nearest_tarin_station',
#          '有無管理組織']]

## info gain
# df = df[['主要建材', '主要用途', '交易標的', '土地_log_2', '建物_log_2', '建物現況格局-廳', '建物現況格局-衛', '建物現況格局-隔間', '都市土地使用分區', 'location_type']]

# df = df[['土地', '土地移轉總面積(平方公尺)_log_2', '建物型態', '建物現況格局-廳', '建物現況格局-房', '建物現況格局-隔間', '總樓層數' ,'都市土地使用分區', '鄉鎮市區', 'num_of_bus_stations_in_100m', 'income_avg_log_2', 'income_var_log_2','有無管理組織']]

# train
# -----------------------------
# X
# 把 train data 抽出
X_train = df[0:46482]

# y
y_train = train_label["price_per_ping"]
# 轉一維
y_train = y_train.values
y_train = np.ravel(y_train)
# -----------------------------

# test
# -----------------------------
X_train, X_test, y_train, y_test = train_test_split(X_train, y_train, test_size = 0.2, random_state = 42)

# -----------------------------
del df, train_label
# =============================================================================

###############################################################################

# RF
# =============================================================================
# 隨機森林（Random Forest）常見的參數包括：
# n_estimators：整数值，表示決策樹的數量。
# criterion：字符串值，表示計算節點的資訊增益的方式。可以是 "gini" 或 "entropy"。
# max_depth：整数值，表示決策樹的最大深度。
# min_samples_split：整数值或浮點数值，表示在建立子節點時所需的最小樣本數。
# min_samples_leaf：整数值或浮點数值，表示樹中葉子節點所需的最小樣本數。
# RandomForestClassifier(
#     n_estimators=100,
#     criterion='gini',
#     max_depth=None,
#     min_samples_split=2,
#     min_samples_leaf=1,
#     min_weight_fraction_leaf=0.0,
#     max_features='auto',
#     max_leaf_nodes=None,
#     min_impurity_decrease=0.0,
#     min_impurity_split=None,
#     bootstrap=True,
#     oob_score=False,
#     n_jobs=None,
#     random_state=None,
#     verbose=0,
#     warm_start=False,
#     class_weight=None,
# )
# =============================================================================
from sklearn.ensemble import RandomForestRegressor
# =============================================================================
model = RandomForestRegressor()
model.fit(X_train, y_train)

predictions = model.predict(X_test)
# =============================================================================
# Program ran for 85.16 seconds

# 評估
# =============================================================================
# 計算平均絕對誤差
mae = mean_absolute_error(y_test, predictions)

# 計算平均平方誤差
mse = mean_squared_error(y_test, predictions)

# 計算平方根平均平方誤差
rmse = np.sqrt(mean_squared_error(y_test, predictions))

evalu_RF = pd.DataFrame({'mae': mae, 'mse': mse, 'rmse': rmse}, index=[0])

# 輸出資料表
print("evalu_RF\n")
print(evalu_RF)
# =============================================================================

# # KNN
# # 交叉驗證
# # =============================================================================
from sklearn.neighbors import KNeighborsRegressor
# from sklearn.model_selection import cross_val_score
# knn = KNeighborsRegressor()
# scores = cross_val_score(knn, X_train, y_train, cv = 10)

# print("Cross Validation scores: ", scores)
# print("Average score: ", scores.mean())

# # =============================================================================

# # =============================================================================
# # 建立 KNN 回歸模型
# knn = KNeighborsRegressor(n_neighbors = 50)

# # 訓練模型
# knn.fit(X_train, y_train)

# # 預測測試資料
# predictions = knn.predict(X_test)
# # =============================================================================

# # 評估
# # =============================================================================
# # 計算平均絕對誤差
# mae = mean_absolute_error(y_test, predictions)

# # 計算平均平方誤差
# mse = mean_squared_error(y_test, predictions)

# # 計算平方根平均平方誤差
# rmse = np.sqrt(mean_squared_error(y_test, predictions))

# evalu_knn = pd.DataFrame({'mae': mae, 'mse': mse, 'rmse': rmse}, index=[0])

# # 輸出資料表
# print("KNN\n")
# print(evalu_knn)
# # =============================================================================

# 多項式回歸
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
# =============================================================================
# 將訓練數據轉換為多項式特徵
poly = PolynomialFeatures(degree = 2)
X_train_poly = poly.fit_transform(X_train)

# 建立
model = LinearRegression()
model.fit(X_train_poly, y_train)

# 將測試數據轉換為多項式特徵
X_test_poly = poly.transform(X_test)

# 預測
predictions = model.predict(X_test_poly)
# =============================================================================
# Program ran for 2.23 seconds

# 評估
# =============================================================================
# 計算平均絕對誤差
mae = mean_absolute_error(y_test, predictions)

# 計算平均平方誤差
mse = mean_squared_error(y_test, predictions)

# 計算平方根平均平方誤差
rmse = np.sqrt(mean_squared_error(y_test, predictions))

evalu_poly = pd.DataFrame({'mae': mae, 'mse': mse, 'rmse': rmse}, index=[0])

# 輸出資料表
print("evalu_poly\n")
print(evalu_poly)
# =============================================================================

# XGboost
# =============================================================================
# XGBoost 常見的參數包括：
# learning_rate：浮點数值，表示模型中使用的學習率。
# n_estimators：整数值，表示弱學習器的數量。
# max_depth：整数值，表示決策樹的最大深度。
# gamma：浮點数值，表示用於控制是否繼續分裂的參數。
# subsample：浮點数值，表示用於訓練模型的子樣本的大小。
# colsample_bytree：浮點数值，表示用於訓練每棵決策樹的特徵的子樣本的大小。
# XGBClassifier(
#     booster='gbtree',
#     base_score=0.5,
#     booster='gbtree',
#     colsample_bylevel=1,
#     colsample_bynode=1,
#     colsample_bytree=1,
#     gamma=0,
#     learning_rate=0.1,
#     max_delta_step=0,
#     max_depth=3,
#     min_child_weight=1,
#     missing=None,
#     n_estimators=100,
#     n_jobs=1,
#     nthread=None,
#     objective='binary:logistic',
#     random_state=0,
#     reg_alpha=0,
#     reg_lambda=1,
#     scale_pos_weight=1,
#     seed=None,
#     silent=None,
#     subsample=1,
#     verbosity=1
# )

# =============================================================================
from xgboost import XGBRegressor
# =============================================================================
model = XGBRegressor(colsample_bytree = 0.7,
                     learning_rate = 0.03,
                     max_depth = 6,
                     min_child_weight = 4,
                     n_estimators = 500,
                     nthread = 4,
                     objective = 'reg:linear',
                     silent = 1,
                     subsample = 0.7)
model.fit(X_train, y_train)

# 預測
predictions = model.predict(X_test)
# =============================================================================
# Program ran for 4.25 seconds

## 參數
# from sklearn.model_selection import GridSearchCV
# # Various hyper-parameters to tune
# parameters = {'nthread':[4], #when use hyperthread, xgboost may become slower
#               'objective':['reg:linear'],
#               'learning_rate': [.03, 0.05, .07], #so called `eta` value
#               'max_depth': [5, 6, 7],
#               'min_child_weight': [4],
#               'silent': [1],
#               'subsample': [0.7],
#               'colsample_bytree': [0.7],
#               'n_estimators': [500]}

# xgb_grid = GridSearchCV(model,
#                         parameters,
#                         cv = 2,
#                         n_jobs = 5,
#                         verbose=True)

# xgb_grid.fit(X_train, y_train)

# print(xgb_grid.best_score_) # -0.41794488813067093
# print(xgb_grid.best_params_)
# {'colsample_bytree': 0.7, 'learning_rate': 0.03, 'max_depth': 6, 'min_child_weight': 4, 'n_estimators': 500, 'nthread': 4, 'objective': 'reg:linear', 'silent': 1, 'subsample': 0.7}

# 評估
# =============================================================================
# 計算平均絕對誤差
mae = mean_absolute_error(y_test, predictions)

# 計算平均平方誤差
mse = mean_squared_error(y_test, predictions)

# 計算平方根平均平方誤差
rmse = np.sqrt(mean_squared_error(y_test, predictions))

evalu_XG = pd.DataFrame({'mae': mae, 'mse': mse, 'rmse': rmse}, index=[0])

# 輸出資料表
print("evalu_XG\n")
print(evalu_XG)
# =============================================================================

# SVM
from sklearn.svm import SVR
# start_time = time.time()
# =============================================================================
svr = SVR(C = 100, gamma = 0.1)
svr.fit(X_train, y_train)

predictions = svr.predict(X_test)
# =============================================================================
# Program ran for 1039.39 seconds

# SVM 參數調整
from sklearn.model_selection import GridSearchCV # 自動對一組參數進行評估，並找到最佳的參數組合
# =============================================================================
# param_grid = {'C': [0.1, 1, 10, 100], 'gamma': [0.1, 1, 10, 100]}

# 使用 GridSearchCV 进行参数调优
# grid_search = GridSearchCV(svr, param_grid, cv = 5)
# grid_search.fit(X_train, y_train)

# 输出最优参数
# print(f'最優參數: {grid_search.best_params_}')
# 最優參數: {'C': 100, 'gamma': 0.1}
# =============================================================================
# end_time = time.time()
# elapsed_time = end_time - start_time
# print(f'參數調整: {elapsed_time:.2f} seconds')

# 評估
# =============================================================================
# 計算平均絕對誤差
mae = mean_absolute_error(y_test, predictions)

# 計算平均平方誤差
mse = mean_squared_error(y_test, predictions)

# 計算平方根平均平方誤差
rmse = np.sqrt(mean_squared_error(y_test, predictions))

evalu_SVM = pd.DataFrame({'mae': mae, 'mse': mse, 'rmse': rmse}, index=[0])

# 輸出資料表
print("evalu_SVM\n")
print(evalu_SVM)
# =============================================================================

05模型建立_自己切資料.py

05模型建立_自己切資料.py