import os
import time

import numpy as np

from . import coarse_psd, coarse_psd_matrix

INTERFEROMETERS = dict(
    GW190521=["H1", "L1", "V1"],
    GW170814=["H1", "L1", "V1"],
    GW150914=["H1", "L1"],
)

def create_parser():
    import argparse

    parser = argparse.ArgumentParser(description="Run analyses for arXiv:2106.13785")
    parser.add_argument("--outdir", help="The output directory")
    parser.add_argument("--event", default="GW170814", help="GW event name")
    parser.add_argument(
        "--interferometer", default="L1", help="Interferometer to analyze"
    )
    parser.add_argument("--duration", type=int, help="The segment duration", default=4)
    parser.add_argument(
        "--medium-duration",
        type=int,
        help="The long segment duration to use for the 'infinite-duration' PSD",
        default=128,
    )
    parser.add_argument(
        "--sampling-frequency",
        type=float,
        help="The sampling frequency to use",
        default=2048,
    )
    parser.add_argument(
        "--low-frequency", type=float, help="The high pass filter frequency", default=16
    )
    parser.add_argument(
        "--tukey-alpha", type=float, help="Tukey window shape parameter", default=0.1
    )
    parser.add_argument(
        "--minimum-frequency",
        type=float,
        help="The minimum analyzed frequency",
        default=20,
    )
    parser.add_argument(
        "--maximum-frequency",
        type=float,
        help="The maximum analyzed frequency",
        default=800,
    )
    return parser

def time_average_psd(data, nfft, window, average="median", sampling_frequency=1):
    """
    Estimate a power spectral density (PSD) by averaging over non-overlapping
    shorter segments.

    This is different from many other implementations as it does not account
    for the window power loss factor (<window ** 2>)

    Parameters
    ----------
    data: np.ndarray
        The input data to use to estimate the PSD
    nfft: int
        The number of input elements per segment
    window: [str, tuple]
        Input arguments for scipy.signal.windows.get_window to specify the
        window.
    average: str
        Time averaging method, should be either "mean" or "median"
    sampling_frequency: float
        The sampling frequency of the input data, used to normalize the PSD
        estimate to have dimensions of 1 / Hz.

    Returns
    -------
    psd: np.ndarray
        The estimate PSD
    """
    from scipy.signal.spectral import _median_bias
    from scipy.signal.windows import get_window

    if not isinstance(window, np.ndarray):
        window = get_window(window, nfft)
    blocked_data = data.reshape(-1, nfft) * window
    blocked_psd = abs(np.fft.rfft(blocked_data, axis=-1) / sampling_frequency) ** 2
    if average == "median":
        normalization = 1 / _median_bias(len(blocked_data))
        func = np.median
    elif average == "mean":
        normalization = 1
        func = np.mean
    else:
        raise ValueError(f"PSD method should be mean or median, not {average}")
    psd = func(blocked_psd, axis=0) / 2 * normalization
    return psd

def band_pass_and_downsample(data, sampling_frequency, low_frequency):
    """
    Band pass and down sample the input data with high pass frequency
    `low_frequency` and new sampling rate `sampling_frequency`.

    Parameters
    ----------
    data: gwpy.timeseries.TimeSeries
        Input time series data
    sampling_frequency: float
        The sampling frequency after down sampling.
    low_frequency: float
        The high pass frequency

    Returns
    -------
    resampled: gwpy.timeseries.TimeSeries
        Result of band passing and down sampling.
    """
    high_frequency = sampling_frequency // 2
    if low_frequency > 0:
        high_passed = data.highpass(low_frequency)
    else:
        high_passed = data.copy()
    if high_frequency < sampling_frequency / 2:
        low_passed = high_passed.lowpass(high_frequency)
    else:
        low_passed = high_passed.copy()
    if sampling_frequency < low_passed.sample_rate.value:
        resampled = low_passed.resample(sampling_frequency)
    else:
        resampled = low_passed.copy()
    return resampled

def regularize_eigenvalues(input, method=("window", 0.1), fill_value=np.inf):
    """
    Regularize the input array of eigenvalues by setting some fraction of them
    to a specified value.

    Parameters
    ----------
    input: np.ndarray
        Array containing the eigenvalues.
    method: (str, float)
        The method to use for the regularization. The possible options are:
        - ("window", alpha), regularize based on the power loss factor from a
          Tukey window.
        - ("psd", value), remove all values less than the provided value.
        - ("fraction", value), keep the first specified fraction of eigenvalues.
    fill_value: float
        The value to set the regularized values to.

    Returns
    -------
    output: np.ndarray
        Array containing the regularized eigenvalues.
    """
    output = input.copy()
    if method[0].lower() == "window":
        n_remove = int(len(input) * (1 - 5 * method[1] / 8))
        output[n_remove:] = fill_value
    elif method[0].lower() == "psd":
        output[input < method[1]] = fill_value
    elif method[0].lower() == "fraction":
        n_remove = int(len(input) * method[1])
        output[n_remove:] = fill_value
    else:
        raise ValueError("Regularization method should be either 'window' or 'psd'")
    return output

def regularized_inversion(svd, method=("window", 0.1)):
    """
    Perform a regularized matrix inverse using the singular value decomposition.

    Parameters
    ----------
    svd: (np.ndarray, np.ndarray, np.ndarray)
        SVD of the matrix to invert.
    method: (str, float)
        Regularization method, see `regularize_eigenvalues`.

    Returns
    -------
    regularized_inverse: np.ndarray
        The regularized inverse of the input matrix.
    """
    regularized = regularize_eigenvalues(svd[1], method)
    regularized_inverse = svd[2].T @ np.nan_to_num(svd[0] / regularized).T
    return regularized_inverse

def fetch_psd_data(
    interferometer_name,
    event="GW170814",
    duration=4,
    sampling_frequency=2048,
    low_frequency=16,
    tukey_alpha=0.1,
    medium_duration=32,
    outdir="outdir",
):
    """
    Fetch open data and calculate a PSD.

    This function will use 512s of data ending at the beginning of the
    analysis segment.

    The PSD estimation uses segments with a duration between the analysis
    segment length and the total data duration with a Hann window.

    A caching mechanism is used to minimize the number of requests to the
    GWOSC servers.

    Parameters
    ----------
    interferometer_name: str
        The name of the interferometer, e.g., "L1".
    event: str
        The event of interest, e.g., "GW170814".
    duration: float
        The duration of the target segments.
    sampling_frequency: float
        The sampling rate to (possibly) downsample the data to.
    low_frequency: float
        High-pass filter frequency.
    tukey_alpha: float
        Tukey window turn on length.
    medium_duration: float
        Duration of segments to use to estimate the PSD.
    outdir: str
        Output directory to save the data to.

    Returns
    -------
    data: dict
        A dictionary containing:
        - strain: the frequency-domain analysis data.
        - medium_psd: the medium duration averaged PSD estimate. This is a
          two-sided PSD estimate. This is obtained by concatenating a reversed
          version of the one-sided estimate after removing the zero and
          Nyquist frequency terms.
        - psd: the "finite-duration" PSD estimated by coarsening the medium
          duration PSD estimate.
        - frequencies: the frequencies corresponding to the strain and psd.
        - td_data: time-domain analysis data.
    """
    import dill
    from gwosc.datasets import event_gps
    from scipy.signal.windows import tukey

    trigger_time = event_gps(event)
    start_time = trigger_time + 2 - duration
    psd_start_time = start_time - 512
    window = tukey(int(sampling_frequency * duration), tukey_alpha)
    stride = int(medium_duration // duration)

    data_file = (
        f"{outdir}/{event}_data_coarse_{medium_duration}_{sampling_frequency}"
        f"_{tukey_alpha}_{interferometer_name}.pkl"
    )

    if os.path.isfile(data_file):
        print(f"Loading {data_file}")
        with open(data_file, "rb") as ff:
            data = dill.load(ff)
    else:
        from gwpy.timeseries import TimeSeries

        try:
            print("Fetching PSD data...")
            psd_data = TimeSeries.fetch_open_data(
                interferometer_name, psd_start_time, psd_start_time + 512
            )
        except ValueError:
            print(f"No data found for {event} for {interferometer_name}.")
            return
        print("Band passing and resampling PSD data...")
        resampled = band_pass_and_downsample(
            psd_data, sampling_frequency, low_frequency
        )
        print("Computing coarse PSD...")
        _window = tukey(int(sampling_frequency * medium_duration), 1)
        _window /= np.mean(_window ** 2) ** 0.5
        medium_psd = time_average_psd(
            resampled.value,
            len(_window),
            _window,
            sampling_frequency=sampling_frequency,
        ) * 4 / medium_duration
        medium_psd = np.concatenate([medium_psd, medium_psd[1:-1][::-1]])
        full_window = np.zeros(len(medium_psd))
        full_window[: len(window)] = window
        _fd_window = np.fft.fft(full_window) / len(full_window)
        psd = coarse_psd(_fd_window, medium_psd, stride)

        print("Fetching analysis data...")
        analysis_data = TimeSeries.fetch_open_data(
            interferometer_name, start_time, start_time + duration
        )
        print("Band passing and resampling analysis data...")
        resampled = band_pass_and_downsample(
            analysis_data, sampling_frequency, low_frequency
        )
        fd_data = np.fft.rfft(resampled.value * window) / sampling_frequency

        freqs = np.fft.rfftfreq(int(sampling_frequency * duration), 1 / sampling_frequency)

        data = dict(
            strain=fd_data,
            psd=psd,
            medium_psd=medium_psd,
            frequencies=freqs,
            td_data=resampled,
        )
        with open(data_file, "wb") as ff:
            print(f"Saving PSD data to {data_file}")
            dill.dump(data, ff)
    return data

def compute_psd_matrix(
    interferometer_name,
    event="GW170814",
    duration=4,
    sampling_frequency=2048,
    low_frequency=16,
    tukey_alpha=0.1,
    minimum_frequency=20,
    maximum_frequency=800,
    medium_duration=32,
    outdir="outdir",
):
    import dill
    svd_file = (
        f"{outdir}/{event}_svd_coarse_{medium_duration}_{sampling_frequency}_"
        f"{tukey_alpha}_{interferometer_name}_{int(minimum_frequency)}_"
        f"{int(maximum_frequency)}.pkl"
    )
    if os.path.isfile(svd_file):
        print(f"Loading SVD file {svd_file}...")
        with open(svd_file, "rb") as ff:
            svd = dill.load(ff)
    else:
        from scipy.signal.windows import tukey

        data = fetch_psd_data(
            interferometer_name=interferometer_name,
            event=event,
            duration=duration,
            sampling_frequency=sampling_frequency,
            low_frequency=low_frequency,
            tukey_alpha=tukey_alpha,
            medium_duration=medium_duration,
            outdir=outdir,
        )
        freqs = data["frequencies"]
        medium_psd = data["medium_psd"]

        window = tukey(sampling_frequency * duration, tukey_alpha)
        stride = medium_duration // duration

        frequency_mask = (freqs >= minimum_frequency) & (freqs <= maximum_frequency)

        from gwpopulation.cupy_utils import xp, to_numpy
        from tqdm.auto import trange

        print("Computing PSD matrix...")
        full_window = np.zeros(len(medium_psd))
        full_window[: len(window)] = window
        _fd_window = np.fft.fft(full_window) / len(full_window)
        start = time.time()
        if xp == np:
            long_frequencies = np.fft.rfftfreq(
                len(full_window), d=1 / sampling_frequency
            )
            first_output = np.where(long_frequencies <= minimum_frequency)[0][-1]
            last_output = np.where(long_frequencies >= maximum_frequency)[0][0]
            kwargs = dict(
                fd_window=_fd_window,
                psd=medium_psd,
                stride=stride,
                first_output=first_output,
                last_output=last_output,
            )
            analytic_psd_matrix = 0 * coarse_psd_matrix(start=0, stop=10, **kwargs)
            for ii in trange(0, len(medium_psd), 16):
                stop = min(ii + 16, len(medium_psd) - 1)
                analytic_psd_matrix += coarse_psd_matrix(start=ii, stop=stop, **kwargs)
        else:
            analytic_psd_matrix = coarse_psd_matrix(_fd_window, medium_psd, stride)
            analytic_psd_matrix = analytic_psd_matrix[frequency_mask][:, frequency_mask]
            xp.cuda.Stream.null.synchronize()
        end = time.time()
        print(f"PSD matrix time: {end - start:.2f}s")

        print("Computing SVD...")
        start = time.time()
        svd = xp.linalg.svd(xp.asarray(analytic_psd_matrix))
        end = time.time()
        svd = (to_numpy(svd[0]), to_numpy(svd[1]), to_numpy(svd[2]))
        print(f"SVD time: {end - start:.2f}s")
        with open(svd_file, "wb") as ff:
            print(f"Saving SVD data to {svd_file}")
            dill.dump(svd, ff)

    return svd

def whiten_diagonal(array, psd):
    return array / psd ** 0.5

def whiten_svd(array, svd):
    return svd[0].T @ (array) / svd[1] ** 0.5

def likelihood(whitened_data, signal, whitener, whiten_method):
    whitened_signal = whiten_method(signal, whitener)
    return -np.sum(abs(whitened_data - whitened_signal) ** 2 / 2)

def reweight_posterior(
    interferometer_name,
    event="GW170814",
    duration=4,
    sampling_frequency=2048,
    low_frequency=16,
    tukey_alpha=0.1,
    minimum_frequency=20,
    maximum_frequency=800,
    reference_frequency=20,
    medium_duration=32,
    target="posterior",
    outdir="outdir",
):
    import pandas as pd
    from bilby.gw.conversion import convert_to_lal_binary_black_hole_parameters
    from bilby.gw.detector import get_empty_interferometer, PowerSpectralDensity
    from bilby.gw.source import lal_binary_black_hole
    from bilby.gw.waveform_generator import WaveformGenerator
    from gwosc.datasets import event_gps
    from scipy.signal.windows import tukey
    from tqdm.auto import trange

    data = fetch_psd_data(
        interferometer_name=interferometer_name,
        event=event,
        duration=duration,
        sampling_frequency=sampling_frequency,
        low_frequency=low_frequency,
        tukey_alpha=tukey_alpha,
        medium_duration=medium_duration,
        outdir=outdir,
    )
    svd = compute_psd_matrix(
        interferometer_name=interferometer_name,
        event=event,
        duration=duration,
        sampling_frequency=sampling_frequency,
        low_frequency=low_frequency,
        tukey_alpha=tukey_alpha,
        minimum_frequency=minimum_frequency,
        maximum_frequency=maximum_frequency,
        medium_duration=medium_duration,
        outdir=outdir,
    )
    trigger_time = event_gps(event)
    start_time = trigger_time + 2 - duration
    freqs = data["frequencies"]
    fd_data = data["strain"]
    psd = data["psd"]
    window = tukey(sampling_frequency * duration, tukey_alpha)

    frequency_mask = (freqs >= minimum_frequency) & (freqs <= maximum_frequency)
    target = f"{target}_{medium_duration}"
    cutoff = int(sum(frequency_mask) * np.mean(window ** 2))

    filename = f"{event}/{target}.hdf5"

    posterior = pd.read_hdf(filename)
    _posterior = posterior[
        [
            "chirp_mass",
            "mass_ratio",
            "a_1",
            "a_2",
            "tilt_1",
            "tilt_2",
            "phi_12",
            "phi_jl",
            "luminosity_distance",
            "theta_jn",
            "geocent_time",
            "ra",
            "dec",
            "phase",
            "psi",
        ]
    ]

    waveform_generator = WaveformGenerator(
        duration=duration,
        sampling_frequency=sampling_frequency,
        frequency_domain_source_model=lal_binary_black_hole,
        parameter_conversion=convert_to_lal_binary_black_hole_parameters,
        waveform_arguments=dict(
            reference_frequency=reference_frequency,
            maximum_frequency=maximum_frequency,
            minimum_frequency=minimum_frequency / 2,
            waveform_approximant="IMRPhenomXPHM",
        ),
    )
    ifo = get_empty_interferometer(interferometer_name)
    ifo.power_spectral_density = PowerSpectralDensity.from_power_spectral_density_array(
        frequency_array=freqs, psd_array=psd
    )
    ifo.set_strain_data_from_frequency_domain_strain(
        frequency_domain_strain=fd_data, frequency_array=freqs, start_time=start_time
    )
    ifo.minimum_frequency = minimum_frequency
    ifo.maximum_frequency = maximum_frequency

    ln_llr_svd = np.zeros(len(_posterior))
    ln_llr_diagonal = np.zeros(len(_posterior))

    fd_data = fd_data[frequency_mask]
    psd = psd[frequency_mask]

    svd = list(svd)
    diagonal_whitened = whiten_diagonal(fd_data, psd)
    svd_whitened = whiten_svd(fd_data, svd)
    noise_ln_l_diagonal = likelihood(
        diagonal_whitened, np.zeros_like(fd_data), psd, whiten_diagonal
    )
    svd[1] = svd[1][:cutoff]
    svd[0] = svd[0][:, :cutoff]
    svd_whitened = whiten_svd(fd_data, svd)
    noise_ln_l_non_diagonal = likelihood(
        svd_whitened, np.zeros_like(fd_data), svd, whiten_svd
    )

    for ii in trange(len(_posterior)):
        parameters = dict(_posterior.iloc[ii])
        signal_polarizations = waveform_generator.frequency_domain_strain(parameters)
        signal = ifo.get_detector_response(signal_polarizations, parameters)
        signal = signal[frequency_mask]

        ln_llr_svd[ii] = float(
            likelihood(svd_whitened, signal, svd, whiten_svd) - noise_ln_l_non_diagonal
        )
        ln_llr_diagonal[ii] = float(
            likelihood(diagonal_whitened, signal, psd, whiten_diagonal)
            - noise_ln_l_diagonal
        )
    ln_weights = ln_llr_svd - ln_llr_diagonal
    posterior[f"ln_weights_{interferometer_name}"] = ln_weights
    posterior[f"ln_likeilhood_ratio_svd_{interferometer_name}"] = ln_llr_svd
    posterior[f"ln_likeilhood_ratio_diagonal_{interferometer_name}"] = ln_llr_diagonal
    posterior.to_hdf(filename, mode="w", key="posterior")


import numpy as np
import torch
import random
from transformers import BertTokenizer
import math
from collections import Counter

class Dataloader:
    def __init__(self, intent_vocab, tag_vocab, req_vocab, req_slot_vocab, slot_intent_vocab, pretrained_weights):
        """
        :param intent_vocab: list of all intents
        :param tag_vocab: list of all tags
        :param req_vocab: list of all slots of intent request
        :param pretrained_weights: which bert, e.g. 'bert-base-uncased'
        """
        self.intent_vocab = intent_vocab
        self.tag_vocab = tag_vocab
        self.req_vocab = req_vocab
        self.req_slot_vocab = req_slot_vocab
        self.slot_intent_vocab = slot_intent_vocab

        self.intent_dim = len(intent_vocab)
        self.slot_intent_dim = len(slot_intent_vocab)
        self.tag_dim = len(tag_vocab)
        self.req_dim =  len(req_vocab)
        self.req_slot_dim = len(req_slot_vocab)

        self.id2intent = dict([(i, x) for i, x in enumerate(intent_vocab)])
        self.intent2id = dict([(x, i) for i, x in enumerate(intent_vocab)])
        self.id2slotintent = dict([(i,x) for i, x in enumerate(slot_intent_vocab)])
        self.slotintent2id = dict([(x,i) for i, x in enumerate(slot_intent_vocab)])
        self.id2req = dict([(i, x) for i, x in enumerate(req_vocab)])
        self.req2id = dict([(x, i) for i, x in enumerate(req_vocab)])
        self.id2reqslot = dict([(i, x) for i, x in enumerate(req_slot_vocab)])
        self.reqslot2id = dict([(x, i) for i, x in enumerate(req_slot_vocab)])
        self.id2tag = dict([(i, x) for i, x in enumerate(tag_vocab)])
        self.tag2id = dict([(x, i) for i, x in enumerate(tag_vocab)])

        self.tokenizer = BertTokenizer.from_pretrained(pretrained_weights)
        self.data = {}
        self.intent_weight = [1] * len(self.intent2id)
        self.req_weight = [1] * len(self.reqslot2id)

    def tag_transfer(self, tags):
        result = [['O']*len(tags) for i in range(self.slot_intent_dim)]
        for i,tag in enumerate(tags):
            if tag != 'O':
                intent,value = tag.split('+')
                prefix = intent[0]
                intent = intent[2:]
                if intent in self.slotintent2id.keys():
                    intent_id = self.slotintent2id[intent]
                    result[intent_id][i] = prefix + '-' + value

        return result

    def req_transfer(self, reqs):
        result = []
        for req in reqs:
            temp = req.split('-')
            req_intent = '-'.join(temp[:-1])
            req_slot = temp[-1]
            result.append([req_intent, req_slot])
        return result

    def load_data(self, data, data_key, cut_sen_len, use_bert_tokenizer=True):
        """
        sample representation: [list of words, list of tags, list of intents, original dialog act]
        :param data_key: train/val/test
        :param data:
        :return:
        """
        self.data[data_key] = data
        max_sen_len, max_context_len = 0, 0
        sen_len = []
        context_len = []
        for d in self.data[data_key]:
            max_sen_len = max(max_sen_len, len(d[0]))
            sen_len.append(len(d[0]))
            # d = (tokens, tags, intents, da2triples(turn["dialog_act"], context(list of str))
            if cut_sen_len > 0:
                d[0] = d[0][:cut_sen_len]#text
                d[1] = d[1][:cut_sen_len]#bio tag

                # d[2] intent
                # d[3] overall label(dialog act)
                d[4] = [' '.join(s.split()[:cut_sen_len]) for s in d[4]] #d[4] context
                # d[5] req_slot

            d[4] = self.tokenizer.encode('[CLS] ' + ' [SEP] '.join(d[4]))
            max_context_len = max(max_context_len, len(d[4]))
            context_len.append(len(d[4]))

            if use_bert_tokenizer:
                word_seq, tag_seq, new2ori = self.bert_tokenize(d[0], d[1])
            else:
                word_seq = d[0]
                tag_seq = self.tag_transfer(d[1])
                new2ori = None

            d.append([[self.req2id[req_intent], self.reqslot2id[req_slot]] for req_intent, req_slot in self.req_transfer(d[5])])
            #d.append(self.seq_req2id(d[5])) # 修改req的处理
            d.append(new2ori)
            d.append(word_seq)

            tag_id_seq = []
            for i in tag_seq:
                tag_id_seq.append(self.seq_tag2id(i))

            d.append(tag_id_seq)
            d.append(self.seq_intent2id(d[2]))
            # ori d = (tokens, tags, intents, da2triples(turn["dialog_act"]), context(token id), new2ori, new_word_seq, tag2id_seq, intent2id_seq)
            # d = (tokens, tags, intents, da2triples(turn["dialog_act"]), context(token id), reqs, req2id_seq, new2ori, new_word_seq, tag2id_seq, intent2id_seq)

            if data_key=='train':
                for intent_id in d[-1]:
                    self.intent_weight[intent_id] += 1
                for req_id in d[-5]:
                    self.req_weight[req_id[1]] += 1

            # keys = ['tokens', 'tags', 'intents', 'das', 'context', 'reqs', 'req2id_seq', 'new2ori', 'new_word_seq', 'tag2id_seq', 'intent2id_seq']
            # assert(len(d) == len(keys))
            # for key, value in zip(keys, d):
            #     print(f"{key} : {value}")

            # print('='*100)

        if data_key == 'train':
            train_size = len(self.data['train'])
            print('intent weight = ', self.intent_weight)
            print('train_size = ', train_size)
            for intent, intent_id in self.intent2id.items():
                neg_pos = (train_size - self.intent_weight[intent_id]) / self.intent_weight[intent_id]
                self.intent_weight[intent_id] = max(np.log10(neg_pos), 0.1)
            print('intent weight = ', self.intent_weight)
            self.intent_weight = torch.tensor(self.intent_weight)

            for req,req_id in self.req2id.items():
                neg_pos = (train_size - self.req_weight[req_id]) / self.req_weight[req_id]
                self.req_weight[req_id] = np.log10(neg_pos)
            self.req_weight = torch.tensor(self.req_weight)

        print('max sen bert len', max_sen_len)
        print(sorted(Counter(sen_len).items()))
        print('max context bert len', max_context_len)
        print(sorted(Counter(context_len).items()))

    def bert_tokenize(self, word_seq, tag_seq):
        split_tokens = []
        new_tag_seq = []
        new2ori = {}
        basic_tokens = self.tokenizer.basic_tokenizer.tokenize(' '.join(word_seq))
        accum = ''
        i, j = 0, 0

        all_tag = [[] for i in range(self.slot_intent_dim)]

        for i, token in enumerate(basic_tokens):
            if (accum + token).lower() == word_seq[j].lower():
                accum = ''
            else:
                accum += token
            for sub_token in self.tokenizer.wordpiece_tokenizer.tokenize(basic_tokens[i]):
                new2ori[len(new_tag_seq)] = j
                split_tokens.append(sub_token)

                tag = tag_seq[j]
                for k in range(self.slot_intent_dim):
                    all_tag[k].append('O')
                if tag != 'O':
                    intent,value = tag.split('+')
                    prefix = intent[0]
                    intent = intent[2:]
                    if intent in self.slotintent2id.keys():
                        intent_id = self.slotintent2id[intent]
                        all_tag[intent_id][-1] = prefix + '-' + value
                new_tag_seq.append(tag_seq[j])

            if accum == '':
                j += 1

        return split_tokens, all_tag, new2ori

    def seq_tag2id(self, tags):
        return [self.tag2id[x] if x in self.tag2id else self.tag2id['O'] for x in tags]

    def seq_id2tag(self, ids):
        return [self.id2tag[x] for x in ids]

    def seq_intent2id(self, intents):
        return [self.intent2id[x] for x in intents if x in self.intent2id]

    def seq_id2intent(self, ids):
        return [self.id2intent[x] for x in ids]

    def seq_slotintent2id(self,intents):
        return [self.slotintent2id[x] for x in intents if x in self.slotintent2id]

    def seq_id2slotintent(self,ids):
        return [self.id2slotintent[x] for x in ids]

    def seq_id2req(self,ids):
        return [self.id2req[x] for x in ids]

    def seq_req2id(self, reqs):
        return [self.req2id[x] for x in reqs if x in self.req2id]

    def pad_batch(self, batch_data): # TODO： add req tensor and req_mask tensor
        #print('enter pad batch')
        batch_size = len(batch_data)
        #print('batch_size = ',batch_size)
        max_seq_len = max([len(x[-3]) for x in batch_data]) + 2
        #print('max_seq_len = ',max_seq_len)
        word_mask_tensor = torch.zeros((batch_size, max_seq_len), dtype=torch.long)
        word_seq_tensor = torch.zeros((batch_size, max_seq_len), dtype=torch.long)

        tag_mask_tensor = torch.zeros((batch_size*self.slot_intent_dim, max_seq_len+2), dtype=torch.long)
        base_tag_mask_tensor = torch.zeros((batch_size*self.slot_intent_dim, max_seq_len+2),dtype=torch.long)
        tag_seq_tensor = torch.zeros((batch_size*self.slot_intent_dim, max_seq_len+2), dtype=torch.long)

        intent_tensor = torch.zeros((batch_size, self.intent_dim), dtype=torch.float)

        req_tensor = torch.zeros((batch_size*self.req_dim, self.req_slot_dim), dtype=torch.float)
        req_mask_tensor = torch.zeros((batch_size*self.req_dim), dtype=torch.float)

        context_max_seq_len = max([len(x[4]) for x in batch_data])#max([len(x[-5]) for x in batch_data])
        context_mask_tensor = torch.zeros((batch_size, context_max_seq_len), dtype=torch.long)
        context_seq_tensor = torch.zeros((batch_size, context_max_seq_len), dtype=torch.long)
        for i in range(batch_size):
            words = batch_data[i][-3]
            tags = batch_data[i][-2]
            intents = batch_data[i][-1]
            reqs = batch_data[i][-5]
            words = ['[CLS]'] + words + ['[SEP]']
            indexed_tokens = self.tokenizer.convert_tokens_to_ids(words)
            sen_len = len(words)
            word_seq_tensor[i, :sen_len] = torch.LongTensor([indexed_tokens])

            flag = False
            for j in range(self.slot_intent_dim):
                tag_seq_tensor[i*self.slot_intent_dim+j, 3:sen_len+1] = torch.LongTensor(tags[j])
                base_tag_mask_tensor[i*self.slot_intent_dim+j, 3:sen_len+1] = torch.LongTensor([1] * (sen_len-2))
                if tags[j] != [self.tag2id['O']]*len(tags[j]):
                    tag_mask_tensor[i*self.slot_intent_dim+j, 3:sen_len+1] = torch.LongTensor([1] * (sen_len-2))

            word_mask_tensor[i, :sen_len] = torch.LongTensor([1] * sen_len)
            for j in intents:
                intent_tensor[i, j] = 1.
            for req_intent_id, req_slot_id in reqs:
                req_tensor[i*self.req_dim+req_intent_id, req_slot_id] = torch.LongTensor([1])
                req_mask_tensor[i*self.req_dim+req_intent_id] = torch.LongTensor([1])

            context_len = len(batch_data[i][4])#len(batch_data[i][-5])
            context_seq_tensor[i, :context_len] = torch.LongTensor([batch_data[i][4]])#[batch_data[i][-5]])
            context_mask_tensor[i, :context_len] = torch.LongTensor([1] * context_len)

        return word_seq_tensor, tag_seq_tensor, intent_tensor, req_tensor, req_mask_tensor, word_mask_tensor, tag_mask_tensor, base_tag_mask_tensor,context_seq_tensor, context_mask_tensor

    def get_train_batch(self, batch_size):
        batch_data = random.choices(self.data['train'], k=batch_size)
        return self.pad_batch(batch_data)

    def yield_batches(self, batch_size, data_key):
        batch_num = math.ceil(len(self.data[data_key]) / batch_size)
        for i in range(batch_num):
            batch_data = self.data[data_key][i * batch_size:(i + 1) * batch_size]
            yield self.pad_batch(batch_data), batch_data, len(batch_data)


#==============================================================================
# Major TODO
#==============================================================================
# Replace move command with _input2coordinate

#==============================================================================
# Minor TODO
#==============================================================================
# Replace write_gds() with GdsLibrary.write_gds()
# geometry: Add packer(), make option to limit die size
# add wire_basic to phidl.routing.  also add endcap parameter
# make “elements to polygons” general function
# fix boolean with empty device
# make gdspy2phidl command (allow add_polygon to take gdspy things like flexpath)

#==============================================================================
# Imports
#==============================================================================

from __future__ import division # Otherwise integer division e.g.  20 / 7 = 2
from __future__ import print_function # Use print('hello') instead of print 'hello'
from __future__ import absolute_import

import gdspy
from copy import deepcopy
import numpy as np
from numpy import sqrt, mod, pi, sin, cos
from numpy.linalg import norm
import webcolors
import warnings
import hashlib

# Remove this once gdspy fully deprecates current_library
import gdspy.library
gdspy.library.use_current_library = False

__version__ = '1.2.2'

#==============================================================================
# Useful transformation functions
#==============================================================================

def _rotate_points(points, angle = 45, center = (0,0)):
    """ Rotates points around a centerpoint defined by ``center``.  ``points`` may be
    input as either single points [1,2] or array-like[N][2], and will return in kind
    """
    if angle == 0:
         return points
    angle = angle*pi/180
    ca = cos(angle)
    sa = sin(angle)
    sa = np.array((-sa, sa))
    c0 = np.array(center)
    if np.asarray(points).ndim == 2:
        return (points - c0) * ca + (points - c0)[:,::-1] * sa + c0
    if np.asarray(points).ndim == 1:
        return (points - c0) * ca + (points - c0)[::-1] * sa + c0

def _reflect_points(points, p1 = (0,0), p2 = (1,0)):
    """ Reflects points across the line formed by p1 and p2.  ``points`` may be
    input as either single points [1,2] or array-like[N][2], and will return in kind
    """
    # From http://math.stackexchange.com/questions/11515/point-reflection-across-a-line
    points = np.array(points); p1 = np.array(p1); p2 = np.array(p2);
    if np.asarray(points).ndim == 1:
        return 2*(p1 + (p2-p1)*np.dot((p2-p1),(points-p1))/norm(p2-p1)**2) - points
    if np.asarray(points).ndim == 2:
        return np.array([2*(p1 + (p2-p1)*np.dot((p2-p1),(p-p1))/norm(p2-p1)**2) - p for p in points])

def _is_iterable(items):
    return isinstance(items, (list, tuple, set, np.ndarray))

def _parse_coordinate(c):
    """ Translates various inputs (lists, tuples, Ports) to an (x,y) coordinate """
    if isinstance(c, Port):
        return c.midpoint
    elif np.array(c).size == 2:
        return c
    else:
        raise ValueError('[PHIDL] Could not parse coordinate, input should be array-like (e.g. [1.5,2.3] or a Port')

def reset():
    Layer.layer_dict = {}
    Device._next_uid = 0

class LayerSet(object):

    def __init__(self):
        self._layers = {}

    def add_layer(self, name = 'unnamed', gds_layer = 0, gds_datatype = 0,
                 description = None, color = None, inverted = False,
                  alpha = 0.6, dither = None):
        new_layer = Layer(gds_layer = gds_layer, gds_datatype = gds_datatype, name = name,
                 description = description, inverted = inverted,
                 color = color, alpha = alpha, dither = dither)
        if name in self._layers:
            raise ValueError('[PHIDL] LayerSet: Tried to add layer named "%s", but a layer'
                ' with that name already exists in this LayerSet' % (name))
        else:
            self._layers[name] = new_layer

    def __getitem__(self, val):
        """ If you have a LayerSet `ls`, allows access to the layer names like ls['gold2'] """
        try:
            return self._layers[val]
        except:
            raise ValueError('[PHIDL] LayerSet: Tried to access layer named "%s"'
                ' which does not exist' % (val))

    def __repr__(self):
        return ('LayerSet (%s layers total)' % (len(self._layers)))

class Layer(object):
    layer_dict = {}

    def __init__(self, gds_layer = 0, gds_datatype = 0, name = 'unnamed',
                 description = None, inverted = False,
                 color = None, alpha = 0.6, dither = None):
        if isinstance(gds_layer, Layer):
            l = gds_layer # We were actually passed Layer(mylayer), make a copy
            gds_datatype = l.gds_datatype
            name = l.name
            description = l.description
            alpha = l.alpha
            dither = l.dither
            inverted = l.inverted
            gds_layer = l.gds_layer

        self.gds_layer = gds_layer
        self.gds_datatype = gds_datatype
        self.name = name
        self.description = description
        self.inverted = inverted
        self.alpha = alpha
        self.dither = dither

        try:
            if color is None: # not specified
                self.color = None
            elif np.size(color) == 3: # in format (0.5, 0.5, 0.5)
                self.color = webcolors.rgb_to_hex(np.array( np.array(color)*255, dtype = int))
            elif color[0] == '#': # in format #1d2e3f
                self.color = webcolors.hex_to_rgb(color)
                self.color = webcolors.rgb_to_hex(self.color)
            else: # in named format 'gold'
                self.color = webcolors.name_to_hex(color)
        except:
            raise ValueError("""[PHIDL] Layer() color must be specified as a
            0-1 RGB triplet, (e.g. [0.5, 0.1, 0.9]), an HTML hex  color
            (e.g. #a31df4), or a CSS3 color name (e.g. 'gold' or
            see http://www.w3schools.com/colors/colors_names.asp )
            """)

        Layer.layer_dict[(gds_layer, gds_datatype)] = self

    def __repr__(self):
        return ('Layer (name %s, GDS layer %s, GDS datatype %s, description %s, color %s)' % \
                (self.name, self.gds_layer, self.gds_datatype, self.description, self.color))

def _parse_layer(layer):
    """ Check if the variable layer is a Layer object, a 2-element list like
    [0,1] representing layer=0 and datatype=1, or just a layer number """
    if isinstance(layer, Layer):
        gds_layer, gds_datatype = layer.gds_layer, layer.gds_datatype
    elif np.shape(layer) == (2,): # In form [3,0]
        gds_layer, gds_datatype = layer[0], layer[1]
    elif np.shape(layer) == (1,): # In form [3]
        gds_layer, gds_datatype = layer[0], 0
    elif layer is None:
        gds_layer, gds_datatype = 0, 0
    elif isinstance(layer, (int, float)):
        gds_layer, gds_datatype = layer, 0
    else:
        raise ValueError("""[PHIDL] _parse_layer() was passed something
            that could not be interpreted as a layer: layer = %s""" % layer)
    return (gds_layer, gds_datatype)

class _GeometryHelper(object):
    """ This is a helper class. It can be added to any other class which has
    the functions move() and the property ``bbox`` (as in self.bbox).  It uses
    that function+property to enable you to do things like check what the center
    of the bounding box is (self.center), and also to do things like move the
    bounding box such that its maximum x value is 5.2 (self.xmax = 5.2) """

    @property
    def center(self):
        return np.sum(self.bbox,0)/2

    @center.setter
    def center(self, destination):
        self.move(destination = destination, origin = self.center)

    @property
    def x(self):
        return np.sum(self.bbox,0)[0]/2

    @x.setter
    def x(self, destination):
        destination = (destination, self.center[1])
        self.move(destination = destination, origin = self.center, axis = 'x')

    @property
    def y(self):
        return np.sum(self.bbox,0)[1]/2

    @y.setter
    def y(self, destination):
        destination = ( self.center[0], destination)
        self.move(destination = destination, origin = self.center, axis = 'y')

    @property
    def xmax(self):
        return self.bbox[1][0]

    @xmax.setter
    def xmax(self, destination):
        self.move(destination = (destination, 0), origin = self.bbox[1], axis = 'x')

    @property
    def ymax(self):
        return self.bbox[1][1]

    @ymax.setter
    def ymax(self, destination):
        self.move(destination = (0, destination), origin = self.bbox[1], axis = 'y')

    @property
    def xmin(self):
        return self.bbox[0][0]

    @xmin.setter
    def xmin(self, destination):
        self.move(destination = (destination, 0), origin = self.bbox[0], axis = 'x')

    @property
    def ymin(self):
        return self.bbox[0][1]

    @ymin.setter
    def ymin(self, destination):
        self.move(destination = (0, destination), origin = self.bbox[0], axis = 'y')

    @property
    def size(self):
        bbox = self.bbox
        return bbox[1] - bbox[0]

    @property
    def xsize(self):
        bbox = self.bbox
        return bbox[1][0] - bbox[0][0]

    @property
    def ysize(self):
        bbox = self.bbox
        return bbox[1][1] - bbox[0][1]

    def movex(self, origin = 0, destination = None):
        if destination is None:
            destination = origin
            origin = 0
        self.move(origin = (origin,0), destination = (destination,0))
        return self

    def movey(self, origin = 0, destination = None):
        if destination is None:
            destination = origin
            origin = 0
        self.move(origin = (0,origin), destination = (0,destination))
        return self

class Port(object):
    _next_uid = 0

    def __init__(self, name = None, midpoint = (0,0), width = 1, orientation = 0, parent = None):
        self.name = name
        self.midpoint = np.array(midpoint, dtype = 'float64')
        self.width = width
        self.orientation = mod(orientation,360)
        self.parent = parent
        self.info = {}
        self.uid = Port._next_uid
        if self.width < 0: raise ValueError('[PHIDL] Port creation error: width must be >=0')
        Port._next_uid += 1

    def __repr__(self):
        return ('Port (name %s, midpoint %s, width %s, orientation %s)' % \
                (self.name, self.midpoint, self.width, self.orientation))

    @property
    def endpoints(self):
        dxdy = np.array([
            self.width/2*np.cos((self.orientation - 90)*pi/180),
            self.width/2*np.sin((self.orientation - 90)*pi/180)
            ])
        left_point = self.midpoint - dxdy
        right_point = self.midpoint + dxdy
        return np.array([left_point, right_point])

    @endpoints.setter
    def endpoints(self, points):
        p1, p2 = np.array(points[0]), np.array(points[1])
        self.midpoint = (p1+p2)/2
        dx, dy = p2-p1
        self.orientation = np.arctan2(dx,dy)*180/pi
        self.width = sqrt(dx**2 + dy**2)

    @property
    def normal(self):
        dx = np.cos((self.orientation)*pi/180)
        dy = np.sin((self.orientation)*pi/180)
        return np.array([self.midpoint, self.midpoint + np.array([dx,dy])])

    @property
    def x(self):
        return self.midpoint[0]

    @property
    def y(self):
        return self.midpoint[1]

    @property
    def center(self):
        return self.midpoint

    # Use this function instead of copy() (which will not create a new numpy array
    # for self.midpoint) or deepcopy() (which will also deepcopy the self.parent
    # DeviceReference recursively, causing performance issues)
    def _copy(self, new_uid = True):
        new_port = Port(name = self.name, midpoint = self.midpoint,
            width = self.width, orientation = self.orientation,
            parent = self.parent)
        new_port.info = deepcopy(self.info)
        if new_uid == False:
            new_port.uid = self.uid
            Port._next_uid -= 1
        return new_port

    def rotate(self, angle = 45, center = None):
        self.orientation = mod(self.orientation + angle, 360)
        if center is None:
            center = self.midpoint
        self.midpoint = _rotate_points(self.midpoint, angle = angle, center = center)
        return self

class Polygon(gdspy.Polygon, _GeometryHelper):

    def __init__(self, points, gds_layer, gds_datatype, parent):
        self.parent = parent
        super(Polygon, self).__init__(points = points, layer=gds_layer,
            datatype=gds_datatype)

    @property
    def bbox(self):
        return self.get_bounding_box()

    def rotate(self, angle = 45, center = (0,0)):
        super(Polygon, self).rotate(angle = angle*pi/180, center = center)
        if self.parent is not None:
            self.parent._bb_valid = False
        return self

    def move(self, origin = (0,0), destination = None, axis = None):
        """ Moves elements of the Device from the origin point to the destination.  Both
         origin and destination can be 1x2 array-like, Port, or a key
         corresponding to one of the Ports in this device """

        # If only one set of coordinates is defined, make sure it's used to move things
        if destination is None:
            destination = origin
            origin = [0,0]

        if isinstance(origin, Port):            o = origin.midpoint
        elif np.array(origin).size == 2:    o = origin
        elif origin in self.ports:    o = self.ports[origin].midpoint
        else: raise ValueError('[PHIDL] [DeviceReference.move()] ``origin`` not array-like, a port, or port name')

        if isinstance(destination, Port):           d = destination.midpoint
        elif np.array(destination).size == 2:        d = destination
        elif destination in self.ports:   d = self.ports[destination].midpoint
        else: raise ValueError('[PHIDL] [DeviceReference.move()] ``destination`` not array-like, a port, or port name')

        if axis == 'x': d = (d[0], o[1])
        if axis == 'y': d = (o[0], d[1])

        dx,dy = np.array(d) - o

        super(Polygon, self).translate(dx, dy)
        if self.parent is not None:
            self.parent._bb_valid = False
        return self

    def reflect(self, p1 = (0,1), p2 = (0,0)):
        for n, points in enumerate(self.polygons):
            self.polygons[n] = _reflect_points(points, p1, p2)
        if self.parent is not None:
            self.parent._bb_valid = False
        return self

def make_device(fun, config = None, **kwargs):
    config_dict = {}
    if type(config) is dict:
        config_dict = dict(config)
    elif config is None:
        pass
    else:
        raise TypeError("""[PHIDL] When creating Device() from a function, the
        second argument should be a ``config`` argument which is a
        dictionary containing arguments for the function.
        e.g. make_device(ellipse, config = my_config_dict) """)
    config_dict.update(**kwargs)
    D = fun(**config_dict)
    if not isinstance(D, Device):
        raise ValueError("""[PHIDL] Device() was passed a function, but that
        function does not produce a Device.""")
    return D

class Device(gdspy.Cell, _GeometryHelper):

    _next_uid = 0

    def __init__(self, *args, **kwargs):
        if len(args) > 0:
            if callable(args[0]):
                raise ValueError('[PHIDL] You can no longer create geometry '
                    'by calling Device(device_making_function), please use '
                    'make_device(device_making_function) instead')

        # Allow name to be set like Device('arc') or Device(name = 'arc')
        if 'name' in kwargs:                          _internal_name = kwargs['name']
        elif (len(args) == 1) and (len(kwargs) == 0): _internal_name = args[0]
        else:                                         _internal_name = 'Unnamed'

        # Make a new blank device
        self.ports = {}
        self.info = {}
        self.aliases = {}
        # self.a = self.aliases
        # self.p = self.ports
        self.uid = Device._next_uid
        self._internal_name = _internal_name
        gds_name = '%s%06d' % (self._internal_name[:20], self.uid) # Write name e.g. 'Unnamed000005'
        super(Device, self).__init__(name = gds_name, exclude_from_current=True)
        Device._next_uid += 1

    def __getitem__(self, key):
        """ If you have a Device D, allows access to aliases you made like D['arc2'] """
        try:
            return self.aliases[key]
        except:
            raise ValueError('[PHIDL] Tried to access alias "%s" in Device "%s",  '
                'which does not exist' % (key, self.name))

    def __repr__(self):
        return ('Device (name "%s" (uid %s),  ports %s, aliases %s, %s polygons, %s references)' % \
                (self._internal_name, self.uid, list(self.ports.keys()), list(self.aliases.keys()),
                len(self.polygons), len(self.references)))

    def __str__(self):
        return self.__repr__()

    def __lshift__(self, element):
        return self.add_ref(element)

    def __setitem__(self, key, element):
        """ Allow adding polygons and cell references like D['arc3'] = pg.arc() """
        if isinstance(element, (DeviceReference,Polygon,CellArray)):
            self.aliases[key] = element
        else:
            raise ValueError('[PHIDL] Tried to assign alias "%s" in Device "%s",  '
                'but failed because the item was not a DeviceReference' % (key, self.name))

    @property
    def layers(self):
        return self.get_layers()

    # @property
    # def references(self):
    #     return [e for e in self.elements if isinstance(e, DeviceReference)]

    # @property
    # def polygons(self):
    #     return [e for e in self.elements if isinstance(e, gdspy.PolygonSet)]

    @property
    def bbox(self):
        bbox = self.get_bounding_box()
        if bbox is None:  bbox = ((0,0),(0,0))
        return np.array(bbox)

    def add_ref(self, device, alias = None):
        """ Takes a Device and adds it as a DeviceReference to the current
        Device.  """
        if _is_iterable(device):
            return [self.add_ref(E) for E in device]
        if not isinstance(device, Device):
            raise TypeError("""[PHIDL] add_ref() was passed something that
            was not a Device object. """)
        d = DeviceReference(device)   # Create a DeviceReference (CellReference)
        d.owner = self
        self.add(d)             # Add DeviceReference (CellReference) to Device (Cell)

        if alias is not None:
            self.aliases[alias] = d
        return d                # Return the DeviceReference (CellReference)

    def add_polygon(self, points, layer = None):
        # Check if input a list of polygons by seeing if it's 3 levels deep
        try:
            points[0][0][0] # Try to access first x point
            return [self.add_polygon(p, layer) for p in points]
        except: pass # Verified points is not a list of polygons, continue on

        if isinstance(points, gdspy.PolygonSet):
            if layer is None:   layers = zip(points.layers, points.datatypes)
            else:   layers = [layer]*len(points.polygons)
            return [self.add_polygon(p, layer) for p, layer in zip(points.polygons, layers)]

        # Check if layer is actually a list of Layer objects
        try:
            if isinstance(layer, LayerSet):
                return [self.add_polygon(points, l) for l in layer._layers.values()]
            elif isinstance(layer, set):
                return [self.add_polygon(points, l) for l in layer]
            elif all([isinstance(l, (Layer)) for l in layer]):
                return [self.add_polygon(points, l) for l in layer]
            elif len(layer) > 2: # Someone wrote e.g. layer = [1,4,5]
                raise ValueError(""" [PHIDL] When using add_polygon() with
                    multiple layers, each element in your `layer` argument
                    list must be of type Layer(), e.g.:
                    `layer = [Layer(1,0), my_layer, Layer(4)]""")
        except: pass

        # If in the form [[1,3,5],[2,4,6]]
        if len(points[0]) > 2:
            # Convert to form [[1,2],[3,4],[5,6]]
            points = np.column_stack((points))

        gds_layer, gds_datatype = _parse_layer(layer)
        polygon = Polygon(points = points, gds_layer = gds_layer,
            gds_datatype = gds_datatype, parent = self)
        self.add(polygon)
        return polygon

    def add_array(self, device, columns = 2, rows = 2, spacing = (100,100), alias = None):
        if not isinstance(device, Device):
            raise TypeError("""[PHIDL] add_array() was passed something that
            was not a Device object. """)
        a = CellArray(device = device, columns = columns, rows = rows, spacing = spacing)
        a.owner = self
        self.add(a)             # Add DeviceReference (CellReference) to Device (Cell)
        if alias is not None:
            self.aliases[alias] = a
        return a                # Return the CellArray

    def add_port(self, name = None, midpoint = (0,0), width = 1, orientation = 45, port = None):
        """ Can be called to copy an existing port like add_port(port = existing_port) or
        to create a new port add_port(myname, mymidpoint, mywidth, myorientation).
        Can also be called to copy an existing port with a new name like add_port(port = existing_port, name = new_name)"""
        if port is not None:
            if not isinstance(port, Port):
                raise ValueError('[PHIDL] add_port() error: Argument `port` must be a Port for copying')
            p = port._copy(new_uid = True)
            p.parent = self
        elif isinstance(name, Port):
            p = name._copy(new_uid = True)
            p.parent = self
            name = p.name
        else:
            p = Port(name = name, midpoint = midpoint, width = width,
                orientation = orientation, parent = self)
        if name is not None: p.name = name
        if p.name in self.ports:
            raise ValueError('[DEVICE] add_port() error: Port name "%s" already exists in this Device (name "%s", uid %s)' % (p.name, self._internal_name, self.uid))
        self.ports[p.name] = p
        return p

    def add_label(self, text = 'hello', position = (0,0), magnification = None, rotation = None, anchor = 'o', layer = 255):
        if len(text) >= 1023:
            raise ValueError('[DEVICE] label() error: Text too long (limit 1024 chars)')
        gds_layer, gds_datatype = _parse_layer(layer)

        if type(text) is not str: text = str(text)
        l = Label(text = text, position = position, anchor = anchor, magnification = magnification, rotation = rotation,
                                 layer = gds_layer, texttype = gds_datatype)
        self.add(l)
        return l

    def label(self, *args, **kwargs):
        warnings.warn('[PHIDL] WARNING: label() will be deprecated, please replace with add_label()')
        return self.add_label(*args, **kwargs)

    def write_gds(self, filename, unit = 1e-6, precision = 1e-9,
                  auto_rename = True, max_cellname_length = 28):
        if filename[-4:] != '.gds':  filename += '.gds'
        tempname = self.name
        referenced_cells = list(self.get_dependencies(recursive=True))
        all_cells = [self] + referenced_cells

        # Autofix names so there are no duplicates
        if auto_rename == True:
            all_cells_sorted = sorted(all_cells, key=lambda x: x.uid)
            all_cells_names = [c._internal_name for c in all_cells_sorted]
            all_cells_original_names = [c.name for c in all_cells_sorted]
            used_names = {'toplevel'}
            n = 1
            for c in all_cells_sorted:
                if max_cellname_length is not None:
                    new_name = c._internal_name[:max_cellname_length]
                else:
                    new_name = c._internal_name
                temp_name = new_name
                while temp_name in used_names:
                    n += 1
                    temp_name = new_name + ('%0.3i' % n)
                new_name = temp_name
                used_names.add(new_name)
                c.name = new_name
            self.name = 'toplevel'
        # Write the gds
        gdspy.write_gds(filename, cells=all_cells, name='library',
                        unit=unit, precision=precision)
        # Return cells to their original names if they were auto-renamed
        if auto_rename == True:
            for n,c in enumerate(all_cells_sorted):
                c.name = all_cells_original_names[n]
        return filename

    def remap_layers_wo_paths(self, layermap = {}, include_labels = True):
        layermap = {_parse_layer(k):_parse_layer(v) for k,v in layermap.items()}

        all_D = list(self.get_dependencies(True))
        all_D += [self]
        for D in all_D:
            for p in D.polygons:
                for n, layer in enumerate(p.layers):
                    original_layer = (p.layers[n], p.datatypes[n])
                    original_layer = _parse_layer(original_layer)
                    if original_layer in layermap.keys():
                        new_layer = layermap[original_layer]
                        p.layers[n] = new_layer[0]
                        p.datatypes[n] = new_layer[1]
            if include_labels == True:
                for l in D.labels:
                    original_layer = (l.layer, l.texttype)
                    original_layer = _parse_layer(original_layer)
                    if original_layer in layermap.keys():
                        new_layer = layermap[original_layer]
                        l.layer = new_layer[0]
                        l.texttype = new_layer[1]
        return self

    def remap_layers(self, layermap = {}, include_labels = True):
        layermap = {_parse_layer(k):_parse_layer(v) for k,v in layermap.items()}

        all_D = list(self.get_dependencies(True))
        all_D += [self]
        for D in all_D:
            for p in D.polygons:
                for n, layer in enumerate(p.layers):
                    original_layer = (p.layers[n], p.datatypes[n])
                    original_layer = _parse_layer(original_layer)
                    if original_layer in layermap.keys():
                        new_layer = layermap[original_layer]
                        p.layers[n] = new_layer[0]
                        p.datatypes[n] = new_layer[1]

            for p in D.paths:
                for n, layer in enumerate(p.layers):
                    original_layer = (p.layers[0], p.datatypes[0])
                    original_layer = _parse_layer(original_layer)
                    if original_layer in layermap.keys():
                            new_layer = layermap[original_layer]
                            p.layers[n] = new_layer[0]
                            p.datatypes[n] = new_layer[1]

            if include_labels == True:
                for l in D.labels:
                    original_layer = (l.layer, l.texttype)
                    original_layer = _parse_layer(original_layer)
                    if original_layer in layermap.keys():
                        new_layer = layermap[original_layer]
                        l.layer = new_layer[0]
                        l.texttype = new_layer[1]
        return self

    def remove_layers(self, layers = (), include_labels = True, invert_selection = False):
        layers = [_parse_layer(l) for l in layers]
        all_D = list(self.get_dependencies(True))
        all_D += [self]
        for D in all_D:
            for polygonset in D.polygons:
                polygon_layers = zip(polygonset.layers, polygonset.datatypes)
                polygons_to_keep = [(pl in layers) for pl in polygon_layers]
                if invert_selection == False: polygons_to_keep = [(not p) for p in polygons_to_keep]
                polygonset.polygons =  [p for p,keep in zip(polygonset.polygons,  polygons_to_keep) if keep]
                polygonset.layers =    [p for p,keep in zip(polygonset.layers,    polygons_to_keep) if keep]
                polygonset.datatypes = [p for p,keep in zip(polygonset.datatypes, polygons_to_keep) if keep]

            new_paths = []
            for p in D.paths:
                original_layer = (p.layers[0], p.datatypes[0])
                original_layer = _parse_layer(original_layer)
                if invert_selection: keep_layer = (original_layer in layers)
                else:                keep_layer = (original_layer not in layers)
                if keep_layer:
                    new_paths += [p]
            D.paths = new_paths

            if include_labels == True:
                new_labels = []
                for l in D.labels:
                    original_layer = (l.layer, l.texttype)
                    original_layer = _parse_layer(original_layer)
                    if invert_selection: keep_layer = (original_layer in layers)
                    else:                keep_layer = (original_layer not in layers)
                    if keep_layer:
                        new_labels += [l]
                D.labels = new_labels
        return self

    def distribute(self, elements = 'all', direction = 'x', spacing = 100, separation = True):
        if direction not in (['+x','-x','x','+y','-y','y']):
            raise ValueError("[PHIDL] distribute(): 'direction' argument must be one of '+x','-x','x','+y','-y','y'")

        if elements == 'all': elements = (self.polygons + self.references)

        if direction == 'x': direction = '+x'
        elif direction == 'y': direction = '+y'

        sizes = [e.size for e in elements]
        xy = elements[0].center
        for n, e in enumerate(elements[:-1]):
            e.center = xy
            if direction == '+x':  xy[0] += spacing + separation*(sizes[n] + sizes[n+1])[0]/2
            if direction == '-x':  xy[0] -= spacing + separation*(sizes[n] + sizes[n+1])[0]/2
            if direction == '+y':  xy[1] += spacing + separation*(sizes[n] + sizes[n+1])[1]/2
            if direction == '-y':  xy[1] -= spacing + separation*(sizes[n] + sizes[n+1])[1]/2
        elements[-1].center = xy
        return self

    def align(self, elements = 'all', alignment = 'ymax'):
        if elements == 'all': elements = (self.polygons + self.references)
        if alignment not in (['x','y','xmin', 'xmax', 'ymin','ymax']):
            raise ValueError("[PHIDL] align(): 'alignment' argument must be one of 'x','y','xmin', 'xmax', 'ymin','ymax'")
        if elements is None:
            elements = (self.polygons + self.references)
        value = self.__getattribute__(alignment)
        for e in elements:
            e.__setattr__(alignment, value)
        return self

    def flatten(self,  single_layer = None):
        if single_layer is None:
            super(Device, self).flatten(single_layer=None, single_datatype=None, single_texttype=None)
        else:
            gds_layer, gds_datatype = _parse_layer(single_layer)
            super(Device, self).flatten(single_layer = gds_layer, single_datatype = gds_datatype, single_texttype=gds_datatype)

        temp_polygons = list(self.polygons)
        self.references = []
        self.polygons = []
        [self.add_polygon(poly) for poly in temp_polygons]
        return self

    def absorb(self, reference):
        """ Flattens and absorbs polygons from an underlying
        DeviceReference into the Device, destroying the reference
        in the process but keeping the polygon geometry """
        if reference not in self.references:
            raise ValueError("""[PHIDL] Device.absorb() failed -
                the reference it was asked to absorb does not
                exist in this Device. """)
        ref_polygons = reference.get_polygons(by_spec = True)
        for (layer, polys) in ref_polygons.items():
            [self.add_polygon(points = p, layer = layer) for p in polys]
        self.remove(reference)
        return self

    def get_ports(self, depth = None):
        """ Returns copies of all the ports of the Device, rotated
        and translated so that they're in their top-level position.
        The Ports returned are copies of the originals, but each copy
        has the same ``uid'' as the original so that they can be
        traced back to the original if needed"""
        port_list = [p._copy(new_uid = False) for p in self.ports.values()]

        if depth is None or depth > 0:
            for r in self.references:
                if depth is None: new_depth = None
                else:             new_depth = depth - 1
                ref_ports = r.parent.get_ports(depth=new_depth)

                # Transform ports that came from a reference
                ref_ports_transformed = []
                for rp in ref_ports:
                    new_port = rp._copy(new_uid = False)
                    new_midpoint, new_orientation = r._transform_port(rp.midpoint, \
                    rp.orientation, r.origin, r.rotation, r.x_reflection)
                    new_port.midpoint = new_midpoint
                    new_port.new_orientation = new_orientation
                    ref_ports_transformed.append(new_port)
                port_list += ref_ports_transformed

        return port_list

    def remove(self, items):
        if not _is_iterable(items):  items = [items]
        for item in items:
            if isinstance(item, Port):
                try:
                    self.ports = { k:v for k, v in self.ports.items() if v != item}
                except:
                    raise ValueError("""[PHIDL] Device.remove() cannot find the Port
                                     it was asked to remove in the Device: "%s".""" % (item))
            else:
                try:
                    if isinstance(item, gdspy.PolygonSet):
                        self.polygons.remove(item)
                    if isinstance(item, gdspy.CellReference):
                        self.references.remove(item)
                    if isinstance(item, gdspy.Label):
                        self.labels.remove(item)
                    self.aliases = { k:v for k, v in self.aliases.items() if v != item}
                except:
                    raise ValueError("""[PHIDL] Device.remove() cannot find the item
                                     it was asked to remove in the Device: "%s".""" % (item))

        self._bb_valid = False
        return self

    def rotate(self, angle = 45, center = (0,0)):
        if angle == 0: return self
        for e in self.polygons:
            e.rotate(angle = angle, center = center)
        for e in self.references:
            e.rotate(angle, center)
        for e in self.labels:
            e.rotate(angle, center)
        for p in self.ports.values():
            p.midpoint = _rotate_points(p.midpoint, angle, center)
            p.orientation = mod(p.orientation + angle, 360)
        self._bb_valid = False
        return self

    def move(self, origin = (0,0), destination = None, axis = None):
        """ Moves elements of the Device from the origin point to the destination.  Both
         origin and destination can be 1x2 array-like, Port, or a key
         corresponding to one of the Ports in this device """

        # If only one set of coordinates is defined, make sure it's used to move things
        if destination is None:
            destination = origin
            origin = [0,0]

        if isinstance(origin, Port):            o = origin.midpoint
        elif np.array(origin).size == 2:    o = origin
        elif origin in self.ports:    o = self.ports[origin].midpoint
        else: raise ValueError('[PHIDL] DeviceReference.move() ``origin`` not array-like, a port, or port name')

        if isinstance(destination, Port):           d = destination.midpoint
        elif np.array(destination).size == 2:        d = destination
        elif destination in self.ports:   d = self.ports[destination].midpoint
        else: raise ValueError('[PHIDL] DeviceReference.move() ``destination`` not array-like, a port, or port name')

        if axis == 'x': d = (d[0], o[1])
        if axis == 'y': d = (o[0], d[1])

        dx,dy = np.array(d) - o

        # Move geometries
        for e in self.polygons:
            e.translate(dx,dy)
        for e in self.references:
            e.move(destination = d, origin = o)
        for e in self.labels:
            e.move(destination = d, origin = o)
        for p in self.ports.values():
            p.midpoint = np.array(p.midpoint) + np.array(d) - np.array(o)

        self._bb_valid = False
        return self

    def reflect(self, p1 = (0,1), p2 = (0,0)):
        for e in (self.polygons+self.references+self.labels):
            e.reflect(p1, p2)
        for p in self.ports.values():
            p.midpoint = _reflect_points(p.midpoint, p1, p2)
            phi = np.arctan2(p2[1]-p1[1], p2[0]-p1[0])*180/pi
            p.orientation = 2*phi - p.orientation
        self._bb_valid = False
        return self

    def hash_geometry(self, precision = 1e-4):
        """
        Algorithm:
        hash(
            hash(First layer information: [layer1, datatype1]),
            hash(Polygon 1 on layer 1 points: [(x1,y1),(x2,y2),(x3,y3)] ),
            hash(Polygon 2 on layer 1 points: [(x1,y1),(x2,y2),(x3,y3),(x4,y4)] ),
            hash(Polygon 3 on layer 1 points: [(x1,y1),(x2,y2),(x3,y3)] ),
            hash(Second layer information: [layer2, datatype2]),
            hash(Polygon 1 on layer 2 points: [(x1,y1),(x2,y2),(x3,y3),(x4,y4)] ),
            hash(Polygon 2 on layer 2 points: [(x1,y1),(x2,y2),(x3,y3)] ),
        )
        ...
        Note: For each layer, each polygon is individually hashed and then
              the polygon hashes are sorted, to ensure the hash stays constant
              regardless of the ordering the polygons.  Similarly, the layers
              are sorted by (layer, datatype)
        """
        polygons_by_spec = self.get_polygons(by_spec = True)
        layers = np.array(list(polygons_by_spec.keys()))
        sorted_layers = layers[np.lexsort((layers[:,0], layers[:,1]))]

        # A random offset which fixes common rounding errors intrinsic
        # to floating point math. Example: with a precision of 0.1, the
        # floating points 7.049999 and 7.050001 round to different values
        # (7.0 and 7.1), but offset values (7.220485 and 7.220487) don't
        magic_offset = .17048614

        final_hash = hashlib.sha1()
        for layer in sorted_layers:
            layer_hash = hashlib.sha1(layer.astype(np.int64)).digest()
            polygons = polygons_by_spec[tuple(layer)]
            polygons = [((p/precision) + magic_offset).astype(np.int64) for p in polygons]
            polygon_hashes = np.sort([hashlib.sha1(p).digest() for p in polygons])
            final_hash.update(layer_hash)
            for ph in polygon_hashes:
                final_hash.update(ph)

        return final_hash.hexdigest()

class DeviceReference(gdspy.CellReference, _GeometryHelper):
    def __init__(self, device, origin=(0, 0), rotation=0, magnification=None, x_reflection=False):
        super(DeviceReference, self).__init__(
                 ref_cell = device,
                 origin=origin,
                 rotation=rotation,
                 magnification=magnification,
                 x_reflection=x_reflection,
                 ignore_missing=False)
        self.parent = device
        self.owner = None
        # The ports of a DeviceReference have their own unique id (uid),
        # since two DeviceReferences of the same parent Device can be
        # in different locations and thus do not represent the same port
        self._local_ports = {name:port._copy(new_uid = True) for name, port in device.ports.items()}

    def __repr__(self):
        return ('DeviceReference (parent Device "%s", ports %s, origin %s, rotation %s, x_reflection %s)' % \
                (self.parent.name, list(self.ports.keys()), self.origin, self.rotation, self.x_reflection))

    def __str__(self):
        return self.__repr__()

    def __getitem__(self, val):
        """ This allows you to access an alias from the reference's parent, and receive
        a copy of the reference which is correctly rotated and translated"""
        try:
            alias_device = self.parent[val]
        except:
            raise ValueError('[PHIDL] Tried to access alias "%s" from parent '
                'Device "%s", which does not exist' % (val, self.parent.name))
        new_reference = DeviceReference(alias_device.parent, origin=alias_device.origin, rotation=alias_device.rotation, magnification=alias_device.magnification, x_reflection=alias_device.x_reflection)

        if self.x_reflection:
            new_reference.reflect((1,0))
        if self.rotation is not None:
            new_reference.rotate(self.rotation)
        if self.origin is not None:
            new_reference.move(self.origin)

        return new_reference

    @property
    def ports(self):
        """ This property allows you to access myref.ports, and receive a copy
        of the ports dict which is correctly rotated and translated"""
        for name, port in self.parent.ports.items():
            port = self.parent.ports[name]
            new_midpoint, new_orientation = self._transform_port(port.midpoint, \
                port.orientation, self.origin, self.rotation, self.x_reflection)
            if name not in self._local_ports:
                self._local_ports[name] = port._copy(new_uid = True)
            self._local_ports[name].midpoint = new_midpoint
            self._local_ports[name].orientation = mod(new_orientation,360)
            self._local_ports[name].parent = self
        # Remove any ports that no longer exist in the reference's parent
        parent_names = self.parent.ports.keys()
        local_names = list(self._local_ports.keys())
        for name in local_names:
            if name not in parent_names: self._local_ports.pop(name)
        return self._local_ports

    @property
    def info(self):
        return self.parent.info

    @property
    def bbox(self):
        bbox = self.get_bounding_box()
        if bbox is None:  bbox = ((0,0),(0,0))
        return np.array(bbox)

    def _transform_port(self, point, orientation, origin=(0, 0), rotation=None, x_reflection=False):
        # Apply GDS-type transformations to a port (x_ref)
        new_point = np.array(point)
        new_orientation = orientation

        if x_reflection:
            new_point[1] = -new_point[1]
            new_orientation = -orientation
        if rotation is not None:
            new_point = _rotate_points(new_point, angle = rotation, center = [0, 0])
            new_orientation += rotation
        if origin is not None:
            new_point = new_point + np.array(origin)
        new_orientation = mod(new_orientation, 360)

        return new_point, new_orientation

    def move(self, origin = (0,0), destination = None, axis = None):
        """ Moves the DeviceReference from the origin point to the destination.  Both
         origin and destination can be 1x2 array-like, Port, or a key
         corresponding to one of the Ports in this device_ref """

        # If only one set of coordinates is defined, make sure it's used to move things
        if destination is None:
            destination = origin
            origin = (0,0)

        if isinstance(origin, Port):            o = origin.midpoint
        elif np.array(origin).size == 2:    o = origin
        elif origin in self.ports:    o = self.ports[origin].midpoint
        else: raise ValueError('[DeviceReference.move()] ``origin`` not array-like, a port, or port name')

        if isinstance(destination, Port):           d = destination.midpoint
        elif np.array(destination).size == 2:   d = destination
        elif destination in self.ports:   d = self.ports[destination].midpoint
        else: raise ValueError('[DeviceReference.move()] ``destination`` not array-like, a port, or port name')

        # Lock one axis if necessary
        if axis == 'x': d = (d[0], o[1])
        if axis == 'y': d = (o[0], d[1])

        # This needs to be done in two steps otherwise floating point errors can accrue
        dxdy = np.array(d) - np.array(o)
        self.origin = np.array(self.origin) + dxdy

        if self.owner is not None:
            self.owner._bb_valid = False
        return self

    def rotate(self, angle = 45, center = (0,0)):
        if angle == 0: return self
        if type(center) is Port:  center = center.midpoint
        self.rotation += angle
        self.origin = _rotate_points(self.origin, angle, center)

        if self.owner is not None:
            self.owner._bb_valid = False
        return self

    def reflect(self, p1 = (0,1), p2 = (0,0)):
        if type(p1) is Port:  p1 = p1.midpoint
        if type(p2) is Port:  p2 = p2.midpoint
        p1 = np.array(p1);  p2 = np.array(p2)
        # Translate so reflection axis passes through origin
        self.origin = self.origin - p1

        # Rotate so reflection axis aligns with x-axis
        angle = np.arctan2((p2[1]-p1[1]),(p2[0]-p1[0]))*180/pi
        self.origin = _rotate_points(self.origin, angle = -angle, center = [0,0])
        self.rotation -= angle

        # Reflect across x-axis
        self.x_reflection = not self.x_reflection
        self.origin[1] = -self.origin[1]
        self.rotation = -self.rotation

        # Un-rotate and un-translate
        self.origin = _rotate_points(self.origin, angle = angle, center = [0,0])
        self.rotation += angle
        self.origin = self.origin + p1

        if self.owner is not None:
            self.owner._bb_valid = False
        return self

    def connect(self, port, destination, overlap = 0):
        # ``port`` can either be a string with the name or an actual Port
        if port in self.ports: # Then ``port`` is a key for the ports dict
            p = self.ports[port]
        elif type(port) is Port:
            p = port
        else:
            raise ValueError('[PHIDL] connect() did not receive a Port or valid port name' + \
                ' - received (%s), ports available are (%s)' % (port, tuple(self.ports.keys())))
        self.rotate(angle =  180 + destination.orientation - p.orientation, center = p.midpoint)
        self.move(origin = p, destination = destination)
        self.move(-overlap*np.array([cos(destination.orientation*pi/180),
                                     sin(destination.orientation*pi/180)]))
        return self

class CellArray(gdspy.CellArray, _GeometryHelper):
    def __init__(self, device, columns, rows, spacing, origin=(0, 0),
                 rotation=0, magnification=None, x_reflection=False):
        super(CellArray, self).__init__(
            columns = columns,
            rows = rows,
            spacing = spacing,
            ref_cell = device,
            origin=origin,
            rotation=rotation,
            magnification=magnification,
            x_reflection=x_reflection,
            ignore_missing=False)
        self.parent = device
        self.owner = None

    @property
    def bbox(self):
        bbox = self.get_bounding_box()
        if bbox is None:  bbox = ((0,0),(0,0))
        return np.array(bbox)

    def move(self, origin = (0,0), destination = None, axis = None):
        """ Moves the CellArray from the origin point to the destination.  Both
         origin and destination can be 1x2 array-like, Port, or a key
         corresponding to one of the Ports in this device_ref """

        # If only one set of coordinates is defined, make sure it's used to move things
        if destination is None:
            destination = origin
            origin = (0,0)

        if isinstance(origin, Port):            o = origin.midpoint
        elif np.array(origin).size == 2:    o = origin
        elif origin in self.ports:    o = self.ports[origin].midpoint
        else: raise ValueError('[CellArray.move()] ``origin`` not array-like, a port, or port name')

        if isinstance(destination, Port):           d = destination.midpoint
        elif np.array(destination).size == 2:   d = destination
        elif destination in self.ports:   d = self.ports[destination].midpoint
        else: raise ValueError('[CellArray.move()] ``destination`` not array-like, a port, or port name')

        # Lock one axis if necessary
        if axis == 'x': d = (d[0], o[1])
        if axis == 'y': d = (o[0], d[1])

        # This needs to be done in two steps otherwise floating point errors can accrue
        dxdy = np.array(d) - np.array(o)
        self.origin = np.array(self.origin) + dxdy

        if self.owner is not None:
            self.owner._bb_valid = False
        return self

    def rotate(self, angle = 45, center = (0,0)):
        if angle == 0: return self
        if type(center) is Port:  center = center.midpoint
        self.rotation += angle
        self.origin = _rotate_points(self.origin, angle, center)
        if self.owner is not None:
            self.owner._bb_valid = False
        return self

    def reflect(self, p1 = (0,1), p2 = (0,0)):
        if type(p1) is Port:  p1 = p1.midpoint
        if type(p2) is Port:  p2 = p2.midpoint
        p1 = np.array(p1);  p2 = np.array(p2)
        # Translate so reflection axis passes through origin
        self.origin = self.origin - p1

        # Rotate so reflection axis aligns with x-axis
        angle = np.arctan2((p2[1]-p1[1]),(p2[0]-p1[0]))*180/pi
        self.origin = _rotate_points(self.origin, angle = -angle, center = [0,0])
        self.rotation -= angle

        # Reflect across x-axis
        self.x_reflection = not self.x_reflection
        self.origin[1] = -self.origin[1]
        self.rotation = -self.rotation

        # Un-rotate and un-translate
        self.origin = _rotate_points(self.origin, angle = angle, center = [0,0])
        self.rotation += angle
        self.origin = self.origin + p1

        if self.owner is not None:
            self.owner._bb_valid = False
        return self

class Label(gdspy.Label, _GeometryHelper):

    def __init__(self, *args, **kwargs):
        super(Label, self).__init__(*args, **kwargs)

    @property
    def bbox(self):
        return np.array([[self.position[0], self.position[1]],[self.position[0], self.position[1]]])

    def rotate(self, angle = 45, center = (0,0)):
        self.position = _rotate_points(self.position, angle = angle, center = center)
        return self

    def move(self, origin = (0,0), destination = None, axis = None):
        if destination is None:
            destination = origin
            origin = [0,0]

        o = _parse_coordinate(origin)
        d = _parse_coordinate(destination)

        if axis == 'x': d = (d[0], o[1])
        if axis == 'y': d = (o[0], d[1])

        self.position += np.array(d) - o
        return self

    def reflect(self, p1 = (0,1), p2 = (0,0)):
        self.position = _reflect_points(self.position, p1, p2)
        return self