import time
from concurrent import futures

import grpc

from proto_gen import gamemaster_pb2_grpc
from proto_gen.gamemaster_pb2_grpc import GameMasterServicer
from utils import measures
from utils.measures import ureg

class PongServer:
    """
    Sets up and starts the grpc server
    """

    def __init__(self, servicer: GameMasterServicer, max_workers: int, thread_prefix: str, port: str):
        """
        :param servicer:      An implementation of GameMasterServicer that provides the API behaviour
        :param max_workers:   Each rpc request is handled by a server thread.  This specifies the size of that
                              thread pool.  Once a thread pool is exhausted, calls to the server will be blocked
                              until a thread becomes available
        :param thread_prefix: For logging, the name of the server thread
        :param port:          The listen port for client connections
        """

        # the grpc.so_reuseport option makes it so multiple servers, if instantiated on the same host, can
        # all use the same listen port -- somehow.  Apparently, this is a feature not a bug.
        #  However, this feature isn't compatible with the python grpc package available
        # from the anaconda repository.  You will notice an error when the server is started about SO_RESUSEPORT unavailable.
        # This is because the c-binary provided with the anaconda python grpc was compiled with this option not supported.
        # (You could re-compile from source , but meh)
        # So .. lets formally set the option to zero here so that the server itself knows to throw an error if
        # it detects it is sharing a listen port with another server, just as a safety safeguard
        server_options = [('grpc.so_reuseport', 0), ]
        self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=max_workers,
                                                             thread_name_prefix=thread_prefix),
                                  handlers=None,
                                  interceptors=None,
                                  options=server_options,
                                  maximum_concurrent_rpcs=None)

        # add our handler for the service apis
        gamemaster_pb2_grpc.add_GameMasterServicer_to_server(servicer, self.server)

        # specify the listen port.  insecure means no authentication is done
        self.server.add_insecure_port("{}:{}".format('localhost', port))

    def start_server_blocking(self):
        self.server.start()

        # this is needed because gameserver.start() doesn't block
        seconds_per_day = measures.seconds_per_min.to(ureg.sec / ureg.day)
        try:
            while True:
                time.sleep(seconds_per_day.magnitude)
        except KeyboardInterrupt:
            self.server.stop(0)


import json
import pandas as pd
import shutil
import os

json_path = "data/instances_train2017.json"

def extract_json():
    # Opening JSON file
    f = open(json_path)

    # returns JSON object as
    # a dictionary
    data = json.load(f)
    with open("images_train.json", "w") as outfile:
        json.dump(data["images"], outfile)
    with open("annotations_train.json", "w") as outfile:
        json.dump(data["annotations"], outfile)

    # Closing file
    f.close()

def copy_food_images():

    df_categories = pd.read_json("categories.json")
    df_food = df_categories[df_categories["supercategory"] == "food"]
    print(df_food)

    df_food = df_food[:1]

    for _, row in df_food.iterrows():
        food_id = row["id"]
        food_name = row["name"]

        df_images = pd.read_json("images_train.json")
        df_images.rename(columns={"id": "image_id"}, inplace=True)

        df_annotations = pd.read_json("annotations_train.json")
        df_annotations_food = df_annotations[df_annotations["category_id"] == food_id]

        df = df_annotations_food.join(df_images.set_index("image_id"), on="image_id")
        file_names = df["file_name"].drop_duplicates()

        os.makedirs(f"/home/qiao/TUM_AI/COCO/food_train/{food_name}/", exist_ok=True)

        for file_name in file_names:
            shutil.copyfile(
                f"/home/qiao/TUM_AI/train2017/{file_name}",
                f"/home/qiao/TUM_AI/COCO/food_train/{food_name}/{file_name}",
            )

if __name__ == "__main__":
    extract_json()


import os
import sys
import numpy as np
import torch
from xmodaler.config import kfg
from xmodaler.config import configurable
from .build import EVALUATION_REGISTRY

@EVALUATION_REGISTRY.register()
class RetrievalEvaler(object):
    def __init__(self, cfg, annfile, output_dir):
        super(RetrievalEvaler, self).__init__()

    def eval(self, vfeats, tfeats, labels):
        count = 0
        batch_size = 100
        batch_num = tfeats.size(0) // batch_size
        rank_matrix = np.ones((tfeats.size(0))) * vfeats.size(0)
        for i in range(batch_num):
            if i == batch_num - 1:
                b_tfeats = tfeats[i*batch_size:]
            else:
                b_tfeats = tfeats[i*batch_size:(i+1)*batch_size]

            with torch.no_grad():
                scores = (b_tfeats.unsqueeze(1) * vfeats.unsqueeze(0)).sum(dim=-1).cpu().numpy()
            for score in scores:
                rank = np.where((np.argsort(-score) == np.where(labels[count]==1)[0][0]) == 1)[0][0]
                rank_matrix[count] = rank
                count += 1

        r1 = 100.0 * np.sum(rank_matrix < 1) / len(rank_matrix)
        r5 = 100.0 * np.sum(rank_matrix < 5) / len(rank_matrix)
        r10 = 100.0 * np.sum(rank_matrix < 10) / len(rank_matrix)

        medr = np.floor(np.median(rank_matrix) + 1)
        meanr = np.mean(rank_matrix) + 1
        return {
            "r1": r1,
            "r5": r5,
            "r10": r10,
            "mder": medr,
            "meanr": meanr
        }


from django.urls import path

from Database import views

app_name="Database"

urlpatterns = [
    path('',views.home, name='Home'),
    path('form',views.form, name='Form'),
    path('data',views.data, name='Data'),
    path('delete/<id>',views.delete, name="delete"),
    path('insert',views.insertdata, name="insertadata"),
    path('edit/<id>',views.edit,name="edit"),
    path('sliders',views.sliders,name="sliders"),
    path('search/',views.search, name="search"),
    path('register/',views.SignUpview.as_view(), name="register"),
    path('login/',views.UserLogin.as_view(),name="login")

]


import numpy as np
import scipy.io
import tensorflow as tf
from PIL import Image
# #定义命令行参数
# tf.app.flags.DEFINE_string('star.jpg','','star jpg')
# tf.app.flags.DEFINE_string('xihu.jpg','','xihu jpg')
# tf.app.flags.DEFINE_string('epochs','5000','training epochs')
# tf.app.flags.DEFINE_string('learning_rate','0.5','learning rate')
# FLAGS = tf.app.flags.FLAGS
#声明超参数
STYLE_WEIGHT = 1
CONTENT_WEIGHT = 0
STYLE_LAYERS = ['relu1_1','relu2_1','relu3_1','relu4_1','relu5_1']
CONTENT_LAYERS = ['relu4_2']
_vgg_params = None
def vgg_params():
    #加载VGG19的权值
    global _vgg_params
    if _vgg_params is None:
        _vgg_params = scipy.io.loadmat('imagenet-vgg-verydeep-19.mat')
    return _vgg_params
def vgg19(input_image):
    #声明VGG19网络结构
    layers = ('conv1_1','relu1_1','conv1_2','relu1_2','pool1',
              'conv2_1','relu2_1','conv2_2','relu2_2','pool2',
              'conv3_1','relu3_1','conv3_2','relu3_2','conv3_3','relu3_3','conv3_4','relu3_4','pool3',
              'conv4_1','relu4_1','conv4_2','relu4_2','conv4_3','relu4_3','conv4_4','relu4_4','pool4',
              'conv5_1','relu5_1','conv5_2','relu5_2','conv5_3','relu5_3','conv5_4','relu5_4','pool5')
    weights = vgg_params()['layers'][0]
    net = input_image
    network = {}
    for i,name in enumerate(layers):
        layer_type = name[:4]
        if layer_type == 'conv':
            kernels,bias = weights[i][0][0][0][0]
            # matconvnet weights: [weight,height,in_channels,out_channels]
            # tensorflow weights: [heights,width,in_channels,out_channels]
            kernels = np.transpose(kernels,(1,0,2,3))
            conv = tf.nn.conv2d(net,tf.constant(kernels),strides=(1,1,1,1),padding='SAME',name=name)
            net = tf.nn.bias_add(conv,bias.reshape(-1))
            net = tf.nn.relu(net)
        elif layer_type == 'pool':
            net = tf.nn.max_pool(net,ksize=(1,2,2,1),strides=(1,2,2,1),padding='SAME')
        network[name] = net
    return network
def content_loss(target_features,content_features):
    #使用特征图之差的平方和作为内容差距，越小则合成图与原图的内容越相近
    _,height,width,channel = map(lambda i: i.value,content_features.get_shape())
    content_size = height * width * channel
    return tf.nn.l2_loss(target_features - content_features)/content_size
def style_loss(target_features,style_features):
    #使用Gram matrix 之差的平方和作为风格差距，越小则合成图越具有风格图的纹理特征
    _,height,width,channel = map(lambda i: i.value,target_features.get_shape())
    size = height * width * channel
    #target_gram是特征图矩阵的内积
    target_features = tf.reshape(target_features,(-1,channel))
    target_gram = tf.matmul(tf.transpose(target_features),target_features)/size
    style_features = tf.reshape(style_features,(-1,channel))
    style_gram = tf.matmul(tf.transpose(style_features),style_features)/size
    return tf.nn.l2_loss(target_gram - style_gram)/size
def loss_function(content_image,style_image,target_image):
    #总损失=内容损失*内容权重+风格损失*风格权重
    style_features = vgg19([style_image])
    content_features = vgg19([content_image])
    target_features = vgg19([target_image])
    loss = 0.0
    for layer in CONTENT_LAYERS:
        loss+=CONTENT_WEIGHT*content_loss(target_features[layer],content_features[layer])
    for layer in STYLE_LAYERS:
        loss+=STYLE_WEIGHT*style_loss(target_features[layer],style_features[layer])
    return loss
def stylize(style_image,content_image,learning_rate=0.1,epochs=500):
    #目标合成图，初始化为随机白噪声图
    target = tf.Variable(tf.random_normal(content_image.shape),dtype=tf.float32)
    style_input = tf.constant(style_image,dtype=tf.float32)
    content_input = tf.constant(content_image,dtype=tf.float32)
    cost = loss_function(content_input,style_input,target)
    #使用Adam算法作为优化算法，最小化代价函数
    train_op = tf.train.AdamOptimizer(learning_rate).minimize(cost)
    with tf.Session() as sess:
        tf.global_variables_initializer().run()
        for i in range(epochs):
            _,loss,target_image = sess.run([train_op,cost,target])
            #打印记录迭代中损失函数下降过程
            print("iter:%d,loss:%.9f" %(i,loss))
            if(i+1)%100 == 0:
                #save target image every 100 iterations
                image = np.clip(target_image + 128,0,255).astype(np.uint8)
                Image.fromarray(image).save("neural_%d.jpg" % (i+1))
if __name__=='__main__':
    #图片在读入时，像素值被预处理为0中心，可以加速收敛
    style = Image.open('star.jpg')
    style = np.array(style).astype(np.float32) - 128.0
    content = Image.open('xihu.jpg')
    content = np.array(content).astype(np.float32) - 128.0
    stylize(style,content,0.1,500)


r"""
This module defines tools for geometric analysis of one-dimensional
(time-series) data sets.  The main classes are

    - :class:`Signal`
    - :class:`SpaceCurve`

See `timeseries-data` for a more general outline.

Copyright
---------
- This file is part of https://github.com/geomdata/gda-public/
- 2015, 2016, 2017 by Geometric Data Analytics, Inc. (http://geomdata.com)
- AGPL license. See `LICENSE` or https://github.com/geomdata/gda-public/blob/master/LICENSE

Examples
--------

>>> el = Signal([2.0, 3.0, 0.0, 5.0, 2.5, 2.9])
>>> el.make_pers()
>>> el.pers.diagram
   birth_index  death_index  birth  death  pers
0            0            1    2.0    3.0   1.0
1            4            3    2.5    5.0   2.5
2            2            3    0.0    5.0   5.0
>>> bin_counts, bins = el.pers.bin(1.0)
>>> print(bins)
[-inf   0.   1.   2.   3.   4.   5.  inf]
>>> print(bin_counts)
[[ 0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  1.]
 [ 0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  1.  1.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.]]
>>> sorted(list(el.pers.domains.keys()))
[(0, 1), (2, 3), (3, 4)]
>>> for interval in el.iter_intervals():
...     print("h_A( {} ) == {}".format(interval,el.interval_height(interval)))
h_A( (0, 1) ) == 1.0
h_A( (0, 2) ) == 0.0
h_A( (0, 3) ) == 0.0
h_A( (0, 4) ) == 0.0
h_A( (0, 5) ) == 0.0
h_A( (1, 2) ) == 0.0
h_A( (1, 3) ) == 0.0
h_A( (1, 4) ) == 0.0
h_A( (1, 5) ) == 0.0
h_A( (2, 3) ) == 5.0
h_A( (2, 4) ) == 0.0
h_A( (2, 5) ) == 0.0
h_A( (3, 4) ) == 2.5
h_A( (3, 5) ) == 0.0
h_A( (4, 5) ) == 0.0
>>> list(el.pers.forest.keys())
[None]
>>> sorted(list(el.pers.forest[None]))
[0, 1, 2]
>>> el.jagged(6.0)
0    0.0
1    0.0
2    0.0
3    0.0
4    0.0
5    0.0
dtype: float64
>>> el = Signal([6.5,0.0,2.0])
>>> el.make_pers()
>>> el.pers.diagram
   birth_index  death_index  birth  death  pers
0            1            0    0.0    6.5   6.5
>>> list(el.pers.forest[None])
[0]
>>> el.vertices
   time  height
0   0.0     6.5
1   1.0     0.0
2   2.0     2.0
>>> el.jagged() #el.pers.diagram,el.vertices.index,el.vertices.dtype)
0    6.5
1    0.0
2    0.0
dtype: float64
>>> elN = el.sample_near(sigma=0.1)
>>> elN.make_pers()
>>> elN.pers.domains == el.pers.domains
True
>>> el = Signal([6.5,7.0,2.0,4.5,3.9,9.0,8.3,8.7,5.5,9.9])
>>> el.vertices
   time  height
0   0.0     6.5
1   1.0     7.0
2   2.0     2.0
3   3.0     4.5
4   4.0     3.9
5   5.0     9.0
6   6.0     8.3
7   7.0     8.7
8   8.0     5.5
9   9.0     9.9
>>> el.edges
   src  dst  max  height
2    2    3    3     4.5
3    3    4    3     4.5
0    0    1    1     7.0
1    1    2    1     7.0
6    6    7    7     8.7
7    7    8    7     8.7
4    4    5    5     9.0
5    5    6    5     9.0
8    8    9    9     9.9
>>> el.make_pers()
>>> el.pers.diagram
   birth_index  death_index  birth  death  pers
2            6            7    8.3    8.7   0.4
1            0            1    6.5    7.0   0.5
0            4            3    3.9    4.5   0.6
3            8            5    5.5    9.0   3.5
4            2            9    2.0    9.9   7.9
>>> el.feature()
   time  height
2   2.0     2.0
3   3.0     4.5
4   4.0     3.9
5   5.0     9.0
6   6.0     8.3
7   7.0     8.7
8   8.0     5.5
9   9.0     9.9
>>> el.pers.syzygy((0,0))
2    0.4
1    0.5
0    0.6
3    3.5
4    7.9
dtype: float64
>>> el.pers.syzygy((1,2))
2     251.2908
1     159.2500
0      47.3850
3    1559.2500
4    1548.5580
dtype: float64
>>> el.pers.forest_parents == {0: 4, 1: None, 2: 3, 3: 4, 4: None}
True
>>> el.vertices['height'].sum()/len(el.vertices)
6.5299999999999994
>>> el.normalize()
>>> el2 = Signal(el.vertices)
>>> el2.make_pers()
>>> all(el2.pers.diagram == el.pers.diagram)
True
>>> el = Signal([0.0,3.0,1.5,2.2,0.001])
>>> el.make_pers()
>>> el.vertices
   time  height
0   0.0   0.000
1   1.0   3.000
2   2.0   1.500
3   3.0   2.200
4   4.0   0.001
>>> el.edges
   src  dst  max  height
2    2    3    3     2.2
3    3    4    3     2.2
0    0    1    1     3.0
1    1    2    1     3.0
>>> el.pers.diagram
   birth_index  death_index  birth  death   pers
0            2            3  1.500    2.2  0.700
1            4            1  0.001    3.0  2.999
2            0            1  0.000    3.0  3.000
>>> el = Signal([0.0,0.7,0.45,0.55,0.3, 1.0],
...             times=[0.1, 0.2, 0.3, 0.6, 0.8, 0.85])
>>> el.vertices
   time  height
0  0.10    0.00
1  0.20    0.70
2  0.30    0.45
3  0.60    0.55
4  0.80    0.30
5  0.85    1.00
>>> el.edges
   src  dst  max  height
2    2    3    3    0.55
3    3    4    3    0.55
0    0    1    1    0.70
1    1    2    1    0.70
4    4    5    5    1.00
>>> el.make_pers()
>>> el.pers.diagram
   birth_index  death_index  birth  death  pers
0            2            3   0.45   0.55   0.1
1            4            1   0.30   0.70   0.4
2            0            5   0.00   1.00   1.0
>>> el = Signal([0.0,0.5,0.4,0.9,0.1,1.0])
>>> el.make_pers()
>>> el.pers.diagram
   birth_index  death_index  birth  death  pers
0            2            1    0.4    0.5   0.1
1            4            3    0.1    0.9   0.8
2            0            5    0.0    1.0   1.0
>>> for F in el.iter_features(min_pers=0.5): print(F)
   time  height
0   0.0     0.0
1   1.0     0.5
2   2.0     0.4
3   3.0     0.9
4   4.0     0.1
5   5.0     1.0
   time  height
3   3.0     0.9
4   4.0     0.1
>>> el = Signal(np.sin(np.arange(0,8*np.pi,0.1)))
>>> el.make_pers()
>>> el.pers.domains == {(110, 204): 4, (204, 236): 3, (0, 16): 2, (47, 79): 0, (141, 173): 1}
True

>>> s = Signal([50.0, 120.0, 100, 180, 200, 150, 135])
>>> s.make_pers()
>>> s.pers.diagram
   birth_index  death_index  birth  death   pers
0            2            1  100.0  120.0   20.0
1            6            4  135.0  200.0   65.0
2            0            4   50.0  200.0  150.0
>>> s.pers.mergetree
{1: (0, 2), 4: (1, 6)}
"""

import collections, itertools

import numpy as np
import pandas as pd

import scipy.spatial.distance as ssd
from . import fast_algorithms, curve_geometry
import homology.dim0

def jagged(persdiag, index):
    """ Produce a piecewise-linear function that matches the given persistence
    diagram. This assumes that the index for the vertices is sequential and
    linear, so that the mergetree can be ignored.

    Parameters
    ----------
    persdiag : :class:`homology.PersDiag`
        A 0-dimensional persistence diagram
    index : list-like
        The domain index for the function

    Returns
    -------
    function : `pandas.Series`

    See Also
    --------
    :func:`timeseries.Signal.jagged` :func:`timeseries.Signal.makepers`
    """
    V = pd.Series(index=index, dtype=np.float64)
    if len(persdiag) == 0:
        V[:] = 0
    V[persdiag['birth_index']] = persdiag['birth']
    V[persdiag['death_index']] = persdiag['death']
    # fill with linear interpolation
    V.interpolate(method='linear', inplace=True)
    # make sure we don't lose domain
    V.fillna(method='ffill', inplace=True)  # the end
    V.fillna(method='bfill', inplace=True)  # the beginning
    # MUST ADD CIRCULAR INTERPOLATION!
    return V

def wavy(pasrdiad, index):
    r""" Produce a piecewise-sine function that matches the given persistence
    diagram. This assumes that the index for the vertices is sequential and
    linear, so that the mergetree can be ignored.

    Parameters
    ----------
    persdiag : :class:`homology.PersDiag`
        A 0-dimensional persistence diagram
    index : list-like
        The domain index for the function

    Returns
    -------
    function : `pandas.Series`

    See Also
    --------
    :func:`timeseries.Signal.jagged` :func:`timeseries.Signal.makepers`
    """
    #V = pd.Series(index=index, dtype=np.float64)
    #if len(persdiag) == 0:
    #    V[:] = 0
    #V[persdiag['birth_index']] = persdiag['birth']
    #V[persdiag['death_index']] = persdiag['death']
    ## fill with linear interpolation
    #V.interpolate(method='linear', inplace=True)
    ## make sure we don't lose domain
    #V.fillna(method='ffill', inplace=True)  # the end
    #V.fillna(method='bfill', inplace=True)  # the beginning
    # MUST ADD CIRCULAR INTERPOLATION!
    #return V

class Signal(object):

    def __init__(self, values, times=None):
        """ produce an Signal from function output values.  function input
        domain is implicitly range(len(values)).  The resulting Signal has
        vertices with function values and edges with max of adjacent pairs.
            A Signal is a pair of pd DataFrames that act as indexed
        lists of numerical values.  The vertices are stored as a pd Series,
        Signal.index provides the vertex indices.
            The edges are a DataFrame, giving triples labeled by
        ('src', 'dst', 'max', 'height'), where src and dst are the *indices* (not
        the *values*) of vertices.  Vertices should be considered fixed on
        initialization.  Overloaded functions refer to edges, not vertices.
        """

        if type(values) == pd.core.frame.DataFrame:
            assert values.index.dtype == np.int64
            assert all(values.columns == ['time', 'height'])
            assert values['height'].dtype == np.float64
            assert values['time'].dtype == np.float64
            self.vertices = values.copy()
        else:

            values = np.array(values, dtype=np.float64)
            assert len(values.shape) == 1

            N = values.shape[0]

            if times is None:
                times = np.arange(N, dtype=np.float64)

            index = np.arange(N, dtype=np.int64)

            self.vertices = pd.DataFrame({
                'time': times,
                'height': values},
                columns=['time', 'height'],
                index=index)

        # if times is not None:
        #     self.times
        #
        # if type(values) == pd.core.series.Series and values.index.dtype == 'int64':
        #     self.vertices = values.copy()
        # else:
        #     self.vertices = pd.Series(values, dtype=np.float64,
        #         index=np.arange(len(values), dtype=np.int64))

        # figure out whether the value is from the left (0) or right (1)
        # this is now done in homology.dim0.unionfind.
        leftright = np.array([self.vertices['height'].values[:-1],
                              self.vertices['height'].values[1:]]).argmax(axis=0)
        maxes = pd.Series(self.vertices.index[:-1] + leftright, dtype=np.int64)
        edges = pd.DataFrame({
                'src': self.vertices['height'].index[:-1],
                'dst': self.vertices['height'].index[1:],
                'max': maxes.values,
                'height':  self.vertices['height'].values[maxes]},
                columns=['src', 'dst', 'max', 'height'])

        self.edges = pd.DataFrame(edges)
        self.edges.sort_values(by=['height', 'src'],
                               ascending=[True, True],
                               inplace=True)

        self.components=pd.Series(self.vertices.index,
                                  index=self.vertices.index)

        self.pers = None
        # assert pos.size == len(self.edges.index)
        # self.edges['pos'] = pos

    def make_pers(self):
        tbirth_index, tdeath_index, ybirth_index, ydeath_index, mergetree = homology.dim0.unionfind(self, -1.0)
        self.pers = homology.PersDiag(tbirth_index, tdeath_index, ybirth_index, ydeath_index, mergetree)

    def __len__(self):
        return len(self.vertices)

    def gap(self):
        """ Return the largest homology. """
        bc = self.pers.diagram
        if len(bc) > 0:
            return bc.loc[bc.index[-1]]  # sorted by length!
        else:
            return bc

    def iter_intervals(self):
        """ return the itertools combinations iterator over all sub-intervals.
        """
        return itertools.combinations(self.vertices.index.values, 2)

    def interval_height(self, interval):
        """ the indicator-persistence function for intervals,
        called h_U(A) in the notes.
        """
        if interval in self.pers.domains:
            index = self.pers.domains[interval]
            return self.pers.diagram['pers'].loc[index]
        return 0.0

    def sample_near(self, sigma=1.0):
        """ return a Signal object that is L2-near self in the normal
        distribution.
        """
        diff = np.random.randn(self.vertices['height'].values.shape[0])
        return self.__class__(self.vertices['height'].values + sigma*diff)

    def height_measure(self, sigma=1.0, num_samples=1000, parallel=True, min_pers=0):
        """ Use a simulation to estimate the height-measure of an interval. """
        all_data = [(self, sigma) for _ in range(num_samples)]

        if parallel:
            from concurrent.futures import ProcessPoolExecutor
            pool = ProcessPoolExecutor(max_workers=None)
            all_heights = list(pool.map(fast_algorithms.sample_height, all_data))
        else:
            all_heights = [fast_algorithms.sample_height(x) for x in all_data]
        all_heights = np.stack(all_heights)
        if min_pers > 0:
            all_heights[all_heights < min_pers] = 0
        tot_heights = all_heights.sum(axis=0)/num_samples
        measures = dict()
        for U in np.stack(tot_heights.nonzero()).T:
            i, j = U
            length = j-i+1
            if length not in measures:
                measures[length] = dict()
            measures[length][i] = tot_heights[i, j]
        return measures

    def feature(self, index=None):
        """ return the region of a feature, with vertical displacement threshold tau. """
        if index is None:
            index = self.gap().name
        left = self.pers.lefts[index]
        right = self.pers.rights[index]
        V = self.vertices[left:right+1]
        return V

    def iter_features(self, min_pers=0, depth_first=False):
        """ walk the feature tree. """
        bc = self.pers.diagram
        tree = self.pers.forest
        to_walk = collections.deque(tree[None])  # start at root of tree
        while to_walk:
            idx = to_walk.popleft()
            if bc.loc[idx]['pers'] > min_pers:
                yield self.feature(index=idx)
            if depth_first:
                to_walk.extendleft(tree[idx])
            else:
                to_walk.extend(tree[idx])

    def feature_match(self, other, sigma=1.0, num_samples=1000):
        total_match = 0
        for left, right in self.iter_intervals():
            feature = self.vertices[left:right+1].values
            match_number = other.profile(feature).max()
            interval_height = self.height_measure((left, right),
                                                  sigma=sigma,
                                                  num_samples=num_samples)
            total_match += match_number * interval_height

        return total_match

    def jagged(self, beta=0):
        """ call :func:`timeseries.jagged` on this :class:`Signal`'s
        own persistence diagram. This effectively makes a piecewise-linear
        version of the same function, with the same extrema. """
        # simple cache
        try:
            self._jagged
            if beta in self._jagged:
                return self._jagged[beta]
        except AttributeError as e:
            self._jagged = dict()

        keepbc = self.pers.clip(beta)['keepcode']
        self._jagged[beta] = jagged(keepbc,
                                    self.vertices.index)
        return self._jagged[beta]

    def smoothen(self, beta):
        T = self.vertices['time'].values
        F = self.vertices['height'].values
        N = T.shape[0]
        pd = self.pers.clip(beta)['keepcode']
        cut_indices = np.concatenate([pd[['birth_index', 'death_index']].values.flatten(),
                                      np.array([0, N], dtype='int')])
        cut_indices = np.unique(cut_indices)

        times = [ ]
        segments = [ ]

        for j0,j1 in zip(cut_indices[:-1], cut_indices[1:]):
            times.append(T[j0:j1])
            if F[j0] > F[min(j1,N-1)]:
                segments.append(np.sort(F[j0:j1])[::-1])
            elif F[j0] <= F[min(j1,N-1)]:
                segments.append(np.sort(F[j0:j1]))
            #else:
            #    assert F[j0:j1].min() == F[j0:j1].max()

        times = np.concatenate(times)
        segments = np.concatenate(segments)

        assert np.all(np.sort(segments) == np.sort(F))
        assert np.all(times == T)
        return Signal(segments, times=times)

    def profile(self, arch, normalize=False, norm=np.linalg.norm):
        """ produce profile by dragging an archetype across self,
        looking for matches.  You may want to normalize arch first.
        """
        a = len(arch)
        v = len(self.vertices)
        assert a <= v, "Archetype is too long."
        d = v - a + 1
#        if normalize: arch = (arch-arch.mean())/(arch.max() - arch.min())
        s = []
        for i in range(d):
            snip = self.vertices.values[i:a+i]
            if normalize:
                snip = (snip-snip.mean())/(snip.max() - snip.min())
            s.append(norm(arch - snip))
        p = np.exp2(-np.array(s))
        return p
        # if len(p) > 1:
        #    P = Signal(p)
        # return P

    def iter_windows_by_index(self, window, step=1, start=None, stop=None):
        """ Produce equal-length Signals using a sliding-window on self.  This
        slides by index, not by abstract time.
        window = length of window (number of indices)
        time_step  = step size in index.  (default = 1)
        time_start = start index (default = None, min index)
        time_stop  = stop index  (default = None, max index)
        normalize = renormalize by N(0,1) on window? (default = False)
        norm = norm function to use for comparison (default = np.linalg.norm)

        return: iterator of np arrays, whose columns are timestamp, value.
        To access the tracks as a list,use
        list(Signal.iter_windows_index())

        Examples
        --------
        >>> S = Signal([2.0,3.0,0.0,5.0,2.5,2.9])
        >>> for s in S.iter_windows_by_index(4, step=2):
        ...     print(s[:,1])
        [ 2.  3.  0.  5.]
        [ 0.   5.   2.5  2.9]
        """

        if start is None:
            start = self.vertices.index.values[0]
        if stop is None:
            stop = self.vertices.index.values[-1]

        slices = np.arange(start, stop, step)
        for i, start_i in enumerate(slices):
            stop_i = start_i + window
            yield self.vertices.values[start_i:stop_i, :]

            if stop_i >= stop:
                break

    def self_similarity(self, window, step=1, start=None, stop=None,
                        dist=ssd.euclidean, normalizer=None):
        """ Compare sliding windows of this Signal using a distance function.

        Parameters
        ----------
        window  (length of segment)
        step    (steps to move between windows)
        start   (index to start at)
        stop    (index to stop at)
        dist    (distance function to use. Default:`scipy.spatial.distance.euclidean`
        normalizer (function to use to renormalize each window.  default:None)

        Returns
        -------
        an iterator of the window comparisons.
        (0,0), (0,1), (0,2), ... (0, n-1), (1,1), (1,2), ...  (n-2, n-1)

        The return elements are pairs ((index_lo, index_hi), norm), which
        can be used to populate a dictionary or array.

        Examples
        --------
        >>> S = Signal([0.0, 0.0, 3.0, 4.0, 6.0, 8.0])
        >>> Sss = list(S.self_similarity(window=2, step=2))
        >>> for (ij, d) in Sss:
        ...     print("{} -> {}".format(ij,d))
        (0, 0) -> 0.0
        (0, 2) -> 5.0
        (0, 4) -> 10.0
        (2, 2) -> 0.0
        (2, 4) -> 5.0
        (4, 4) -> 0.0
        >>> D = np.array([d for ij,d in Sss])
        >>> print(D)
        [  0.   5.  10.   0.   5.   0.]
        >>> print(ssd.squareform(D)) # scipy.spatial.distance
        [[  0.   0.   5.  10.]
         [  0.   0.   0.   5.]
         [  5.   0.   0.   0.]
         [ 10.   5.   0.   0.]]

        """
        if start is None:
            start = self.vertices.index.values[0]
        if stop is None:
            stop = self.vertices.index.values[-1]

        slices = np.arange(start, stop, step)
        for i, start_i in enumerate(slices):
            stop_i = start_i + window
            win_i = self.vertices.values[start_i:stop_i, :]
            if normalizer is not None:
                win_i = normalizer(win_i)
            for j, start_j in enumerate(slices[i:]):
                stop_j = start_j + window
                win_j = self.vertices.values[start_j:stop_j, :]
                if normalizer is not None:
                    win_j = normalizer(win_j)
                yield ((start_i, start_j), dist(win_i[:, 1], win_j[:, 1]))

            if stop_i >= stop:
                break

#
    def plot(self, canvas, title="Signal"):
        """ Plot the Signal.

        Parameters
        ----------
        canvas : class:`bokeh.plotting.figure.Figure` or :class:`matplotlib.axes._subplots.AxesSubplot`
                A bokeh or pyplot canvas to draw on.  Create one with
                :code:`canvas = bokeh.plotting.figure()` or
                :code:`fig,canvas = matplotlib.pyplot.subplots()`

        Notes
        -----
        You have to save or show axes after running this call.

        """

        if type(canvas).__module__ == 'bokeh.plotting.figure':
            canvas_type = "bokeh"
            import bokeh.plotting
        elif type(canvas).__module__ == 'matplotlib.axes._subplots':
            canvas_type = "pyplot"
            import matplotlib.pyplot as plt
        else:
            raise NotImplementedError(
                "canvas must be a bokeh.plotting.figure() or a matplotlib.pyplot.subplots()[1].  You gave me {}".format(type(canvas))
                )
        if canvas_type == "bokeh":
            canvas.title=title
        elif canvas_type == "pyplot":
            canvas.set_title(title)

        if canvas_type == "bokeh":
            canvas.circle(self.vertices['time'].values, self.vertices['height'].values)
            canvas.line(self.vertices['time'].values, self.vertices['height'].values)

        elif canvas_type == "pyplot":
            canvas.scatter(self.vertices['height'].index.values, self.vertices['height'].values)
            canvas.plot(self.vertices['height'].index.values, self.vertices['height'].values)
        pass

    @classmethod
    def from_pointcloud(cls, points, direction, norm):
        values = np.dot(points, direction/norm(direction))
        return cls(values)

    def normalize(self):
        """ change this Signal object to have mean = 0 and max-min = 1 """
        bc=self.pers.diagram
        h = self.gap()['pers']
        mean = self.vertices['height'].mean()
        self.vertices['height'] = (self.vertices['height'] - mean)/h
        self.edges['height'] = (self.edges['height'] - mean)/h
        bc['birth'] = (bc['birth'] - mean)/h
        bc['death'] = (bc['death'] - mean)/h
        bc['pers'] = (bc['pers'])/h
        pass

class SpaceCurve(object):
    r""" SpaceCurve is a Python class for studying curves in
    :math:`\mathbb{R}^2` or :math:`\mathbb{R}^3`.
    For example, a SpaceCurve could represent kinematic flight data, or
    trajectories of vehicles given by GPS coordinates.

    All arguments other than :code:`tn` are optional.

    Parameters
    ----------
    tn : list-like
        Integer timestamps, typically in 'numpy.int64` nanoseconds-since-epoch
    px : list-like
    py : list-like
    pz : list-like
        Positions in :class:`numpy.float64` meters
    quality : list-like
        Quality/accuracy of a particular location
    trackid : int
        An integer label for the track
    platform : str
        A descriptive label
    activity : str
        A descriptive label
    mollified: bool
        Whether this track has undegone mollification, for example with :func:`clean_copy`

    Attributes
    ----------
    data : :class:`pandas.DataFrame`
        The original position and velocity data, as originally provided.
        The index of this DataFrame is the :code:`tn` integer time index.

    info : :class:`pandas.DataFrame`
        Data computed using various algorithms.  This is filled by
        :func:`compute`, but more can be added.

    """

    def __init__(self, tn, px=None, py=None, pz=None,
    #             vx=None, vy=None, vz=None,
                 quality=None,
                 trackid=-1, platform=None, activity=None, mollified=False):
        tn = np.array(tn, dtype=np.int64)

        assert len(tn) > 0,\
            "A SpaceCurve cannot have empty nanosecond index. You gave me {}".format(tn)

        if px is None:
            px = np.zeros(tn.shape, dtype=np.float64)
        if py is None:
            py = np.zeros(tn.shape, dtype=np.float64)
        if pz is None:
            pz = np.zeros(tn.shape, dtype=np.float64)
        #if vx is None:
        #    vx = np.zeros(tn.shape, dtype=np.float64)
        #if vy is None:
        #    vy = np.zeros(tn.shape, dtype=np.float64)
        #if vz is None:
        #    vz = np.zeros(tn.shape, dtype=np.float64)
        if quality is None:
            quality = -np.ones(tn.shape, dtype=np.int64)
        px = np.array(px, dtype=np.float64)
        py = np.array(py, dtype=np.float64)
        pz = np.array(pz, dtype=np.float64)
        #vx = np.array(vx, dtype=np.float64)
        #vy = np.array(vy, dtype=np.float64)
        #vz = np.array(vz, dtype=np.float64)
        quality = np.array(quality, dtype=np.int64)

        assert len(tn) == len(px)
        assert len(tn) == len(py)
        assert len(tn) == len(pz)
        #assert len(tn) == len(vx)
        #assert len(tn) == len(vy)
        #assert len(tn) == len(vz)
        assert len(tn) == len(quality)

        sort_by_time = tn.argsort()

        tn = tn[sort_by_time]
        px = px[sort_by_time]
        py = py[sort_by_time]
        pz = pz[sort_by_time]
        #vx = vx[sort_by_time]
        #vy = vy[sort_by_time]
        #vz = vz[sort_by_time]
        quality = quality[sort_by_time]

        ts = (tn - tn[0]).astype(np.float64) / (10 ** 9)

        self.data = pd.DataFrame({'time': ts,
                                  'pos_x': px,
                                  'pos_y': py,
                                  'pos_z': pz,
                                  #'vel_x': vx,
                                  #'vel_y': vy,
                                  #'vel_z': vz,
                                  'quality': quality},
                                 columns=['time', 'pos_x', 'pos_y', 'pos_z',
                                                  #'vel_x', 'vel_y', 'vel_z',
                                                  'quality'],
                                 index=tn)

        self.info = pd.DataFrame({}, index=self.data.index)
        self.trackid = trackid
        self.platform = platform
        self.activity = activity
        self.mollified = mollified

    def __getitem__(self, key):
        """ get raw data via index """
        return self.data.loc[self.data.index[key]]

    def accel(self, rate):
        r"""Change time parametrization, to represent a constant tangential
        acceleration (or deceleration).  Locations, initial time, and
        arc-length are preserved.

        The first timestep is changed to have (1-rate) times the speed of the
        original's first timestep.  The last timestep is changed to have
        (1+rate) times the speed of the original's last timestep.
        That is, if S is a `SpaceCurve` of constant speed, then S.accel(-0.1)
        will start 10% faster and end 10% slower than S.

        If speed[i] changes to speed[i]*q[i], then delta_t[i] changes to
        delta_t[i]*p[i], where p[i] = 1 / q[i]

        Examples
        --------
        >>> tn = np.arange(0, 5*1e9, 1e9)
        >>> s = SpaceCurve(tn=tn, px=10.0*tn/1e9) # drive straight at 10m/s
        >>> s
        SpaceCurve with 5 entries and duration 4.000000000
        >>> s.data
                    time  pos_x  pos_y  pos_z  quality
        0            0.0    0.0    0.0    0.0       -1
        1000000000   1.0   10.0    0.0    0.0       -1
        2000000000   2.0   20.0    0.0    0.0       -1
        3000000000   3.0   30.0    0.0    0.0       -1
        4000000000   4.0   40.0    0.0    0.0       -1
        >>> s.compute()
        >>> s.info['speed']
        0             10.0
        1000000000    10.0
        2000000000    10.0
        3000000000    10.0
        4000000000    10.0
        Name: speed, dtype: float64
        >>> a = s.accel(0.25)
        >>> a
        SpaceCurve with 5 entries and duration 4.147319347
        >>> a.compute()
        >>> a.info['speed']
        0              7.500000
        1333333333     8.250000
        2424242424     9.930556
        3347319347    11.607143
        4147319347    12.500000
        Name: speed, dtype: float64
        >>> a.data
                        time  pos_x  pos_y  pos_z  quality
        0           0.000000    0.0    0.0    0.0       -1
        1333333333  1.333333   10.0    0.0    0.0       -1
        2424242424  2.424242   20.0    0.0    0.0       -1
        3347319347  3.347319   30.0    0.0    0.0       -1
        4147319347  4.147319   40.0    0.0    0.0       -1
        >>> b = s.accel(-0.25)
        >>> b
        SpaceCurve with 5 entries and duration 4.147319347
        >>> b.compute()
        >>> b.info['speed']
        0             12.500000
        800000000     11.607143
        1723076923     9.930556
        2813986013     8.250000
        4147319347     7.500000
        Name: speed, dtype: float64
        >>> b.data
                        time  pos_x  pos_y  pos_z  quality
        0           0.000000    0.0    0.0    0.0       -1
        800000000   0.800000   10.0    0.0    0.0       -1
        1723076923  1.723077   20.0    0.0    0.0       -1
        2813986013  2.813986   30.0    0.0    0.0       -1
        4147319347  4.147319   40.0    0.0    0.0       -1
        """

        n = len(self.data)
        ts = self.data['time'].values

        change_of_speed = np.linspace(1.-rate, 1.+rate, num=n-1)
        new_Dt = np.diff(ts)/change_of_speed
        accum_time = np.cumsum(new_Dt)

        new_ts = np.ndarray(shape=ts.shape, dtype=ts.dtype)
        new_ts[0] = ts[0]
        new_ts[1:] = accum_time + ts[0]

        new_tn = np.int64(new_ts*10**9)
        return SpaceCurve(new_tn,
                          px=self.data['pos_x'].values,
                          py=self.data['pos_y'].values,
                          pz=self.data['pos_z'].values)

    def __matmul__(self, array):
        """ Apply a matrix (NumPy array) to the positions to produce a new
        SpaceCurve.  Used for rotating SpaceCurves.
        Note that this is the LEFT action from a group-theory perspective.

        Examples
        --------

        >>> sc = SpaceCurve(np.arange(4)*10**9, px=np.arange(4))
        >>> sc.data
                    time  pos_x  pos_y  pos_z  quality
        0            0.0    0.0    0.0    0.0       -1
        1000000000   1.0    1.0    0.0    0.0       -1
        2000000000   2.0    2.0    0.0    0.0       -1
        3000000000   3.0    3.0    0.0    0.0       -1
        >>> g = np.array([[np.cos(np.pi/6), -np.sin(np.pi/6), 0],
        ...               [np.sin(np.pi/6),  np.cos(np.pi/6), 0],
        ...               [         0,           0, 1]])
        >>> sc2 = sc.__matmul__(g).__matmul__(g).__matmul__(g) # use @ in Py3
        >>> np.all(sc2.data['pos_y'].values == sc.data['pos_x'].values)
        True
        """

        pos = self.data[['pos_x', 'pos_y', 'pos_z']].values
        new_pos = pos.dot(array.T)
        return SpaceCurve(self.data.index,
                          px=new_pos[:, 0],
                          py=new_pos[:, 1],
                          pz=new_pos[:, 2])

    def __add__(self, other):
        """ Concatenate SpaceCurves, end-to-end in space and time. The other
        SpaceCurve is set to begin at a time and position where self ends.
        This way, the number of points shrinks by one, but the total duration
        adds.

        Examples
        --------

        >>> sc1 = SpaceCurve(np.arange(4)*10**9, px=np.arange(4), py=2*np.arange(4))
        >>> sc1
        SpaceCurve with 4 entries and duration 3.000000000
        >>> sc1.data
                    time  pos_x  pos_y  pos_z  quality
        0            0.0    0.0    0.0    0.0       -1
        1000000000   1.0    1.0    2.0    0.0       -1
        2000000000   2.0    2.0    4.0    0.0       -1
        3000000000   3.0    3.0    6.0    0.0       -1
        >>> sc2 = SpaceCurve(np.arange(8)*10**8, px=np.arange(0,0.4,0.05), py=3*np.arange(8))
        >>> sc2
        SpaceCurve with 8 entries and duration .700000000
        >>> sc2.data
                   time  pos_x  pos_y  pos_z  quality
        0           0.0   0.00    0.0    0.0       -1
        100000000   0.1   0.05    3.0    0.0       -1
        200000000   0.2   0.10    6.0    0.0       -1
        300000000   0.3   0.15    9.0    0.0       -1
        400000000   0.4   0.20   12.0    0.0       -1
        500000000   0.5   0.25   15.0    0.0       -1
        600000000   0.6   0.30   18.0    0.0       -1
        700000000   0.7   0.35   21.0    0.0       -1
        >>> sc3 = sc1 + sc2
        >>> sc3
        SpaceCurve with 11 entries and duration 3.700000000
        >>> sc3.data
                    time  pos_x  pos_y  pos_z  quality
        0            0.0   0.00    0.0    0.0       -1
        1000000000   1.0   1.00    2.0    0.0       -1
        2000000000   2.0   2.00    4.0    0.0       -1
        3000000000   3.0   3.00    6.0    0.0       -1
        3100000000   3.1   3.05    9.0    0.0       -1
        3200000000   3.2   3.10   12.0    0.0       -1
        3300000000   3.3   3.15   15.0    0.0       -1
        3400000000   3.4   3.20   18.0    0.0       -1
        3500000000   3.5   3.25   21.0    0.0       -1
        3600000000   3.6   3.30   24.0    0.0       -1
        3700000000   3.7   3.35   27.0    0.0       -1
        """

        tn_shift = self.data.index.values[-1] - other.data.index.values[0]
        px_shift = self.data['pos_x'].values[-1] - other.data['pos_x'].values[0]
        py_shift = self.data['pos_y'].values[-1] - other.data['pos_y'].values[0]
        pz_shift = self.data['pos_z'].values[-1] - other.data['pos_z'].values[0]

        new_tn = np.concatenate([self.data.index.values,
                                 other.data.index.values[1:] + tn_shift])
        new_px = np.concatenate([self.data['pos_x'].values,
                                 other.data['pos_x'].values[1:] + px_shift])
        new_py = np.concatenate([self.data['pos_y'].values,
                                 other.data['pos_y'].values[1:] + py_shift])
        new_pz = np.concatenate([self.data['pos_z'].values,
                                 other.data['pos_z'].values[1:] + pz_shift])
        return self.__class__(new_tn, px=new_px, py=new_py, pz=new_pz)

    def arclength_param(self):
        """ Change time parametrization to the universal speed=1 arclength
        parametrization.

        Examples
        --------
        >>> tn = np.arange(0, 5e9, 1e9)
        >>> s = SpaceCurve(tn=tn, px=(tn/1e9)**2)
        >>> s.data
                    time  pos_x  pos_y  pos_z  quality
        0            0.0    0.0    0.0    0.0       -1
        1000000000   1.0    1.0    0.0    0.0       -1
        2000000000   2.0    4.0    0.0    0.0       -1
        3000000000   3.0    9.0    0.0    0.0       -1
        4000000000   4.0   16.0    0.0    0.0       -1
        >>> a = s.arclength_param()
        >>> a.data
                     time  pos_x  pos_y  pos_z  quality
        0             0.0    0.0    0.0    0.0       -1
        1000000000    1.0    1.0    0.0    0.0       -1
        4000000000    4.0    4.0    0.0    0.0       -1
        9000000000    9.0    9.0    0.0    0.0       -1
        16000000000  16.0   16.0    0.0    0.0       -1
        """

        pos = self.data[['pos_x', 'pos_y', 'pos_z']].values
        gap = np.diff(pos, axis=0)
        dist = np.linalg.norm(gap, axis=1)
        accum_time = dist.cumsum()

        ts = self.data['time'].values
        new_ts = np.ndarray(shape=ts.shape, dtype=ts.dtype)
        new_ts[0] = ts[0]
        new_ts[1:] = accum_time + ts[0]
        new_tn = np.int64(new_ts*10**9)
        return SpaceCurve(new_tn,
                          px=self.data['pos_x'].values,
                          py=self.data['pos_y'].values,
                          pz=self.data['pos_z'].values)

    def reverse(self):
        """
        Reverse the time parametrization of the SpaceCurve.

        Examples
        --------

        >>> tn = np.arange(0, 5*1e9, 1e9)
        >>> s = SpaceCurve(tn=tn, px=10.0*tn/1e9) # drive straight at 10m/s
        >>> s.data
                    time  pos_x  pos_y  pos_z  quality
        0            0.0    0.0    0.0    0.0       -1
        1000000000   1.0   10.0    0.0    0.0       -1
        2000000000   2.0   20.0    0.0    0.0       -1
        3000000000   3.0   30.0    0.0    0.0       -1
        4000000000   4.0   40.0    0.0    0.0       -1
        >>> a = s.reverse()
        >>> a.data
                    time  pos_x  pos_y  pos_z  quality
        0            0.0   40.0    0.0    0.0       -1
        1000000000   1.0   30.0    0.0    0.0       -1
        2000000000   2.0   20.0    0.0    0.0       -1
        3000000000   3.0   10.0    0.0    0.0       -1
        4000000000   4.0    0.0    0.0    0.0       -1
        """
        return SpaceCurve(self.data.index.values,
                          px=self.data['pos_x'].values[::-1],
                          py=self.data['pos_y'].values[::-1],
                          pz=self.data['pos_z'].values[::-1])

    def reparam(self, rate):
        r"""Change time parametrization, to represent a constant change of speed.
        Locations, initial time, and arc-length are preserved.

        Parameters
        ----------
        rate : float
            A (positive) ratio by which to increase the speed.

        Notes
        -----
        If :code:`speed[i]` changes to :code:`speed[i]*q[i]`, then
        :code:`delta_t[i]` changes to :code:`delta_t[i]*p[i]`, where
        :code:`p[i] = 1 / q[i]`.

        Examples
        --------
        >>> tn = np.arange(0, 5*1e9, 1e9)
        >>> s = SpaceCurve(tn=tn, px=10.0*tn/1e9) # drive straight at 10m/s
        >>> s
        SpaceCurve with 5 entries and duration 4.000000000
        >>> s.data
                    time  pos_x  pos_y  pos_z  quality
        0            0.0    0.0    0.0    0.0       -1
        1000000000   1.0   10.0    0.0    0.0       -1
        2000000000   2.0   20.0    0.0    0.0       -1
        3000000000   3.0   30.0    0.0    0.0       -1
        4000000000   4.0   40.0    0.0    0.0       -1
        >>> a = s.reparam(0.5)
        >>> a.data
                    time  pos_x  pos_y  pos_z  quality
        0            0.0    0.0    0.0    0.0       -1
        2000000000   2.0   10.0    0.0    0.0       -1
        4000000000   4.0   20.0    0.0    0.0       -1
        6000000000   6.0   30.0    0.0    0.0       -1
        8000000000   8.0   40.0    0.0    0.0       -1
        >>> a
        SpaceCurve with 5 entries and duration 8.000000000
        >>> b = s.reparam(2.0)
        >>> b
        SpaceCurve with 5 entries and duration 2.000000000
        >>> b.data
                    time  pos_x  pos_y  pos_z  quality
        0            0.0    0.0    0.0    0.0       -1
        500000000    0.5   10.0    0.0    0.0       -1
        1000000000   1.0   20.0    0.0    0.0       -1
        1500000000   1.5   30.0    0.0    0.0       -1
        2000000000   2.0   40.0    0.0    0.0       -1
        """

        ts = self.data['time'].values
        new_ts = ts/rate
        new_tn = np.int64(new_ts*10**9)
        return SpaceCurve(new_tn,
                          px=self.data['pos_x'].values,
                          py=self.data['pos_y'].values,
                          pz=self.data['pos_z'].values)

    def plot(self, canvas, title="SpaceCurve", color="blue"):
        r""" Plot the SpaceCurve in 3D.

        Parameters
        -----------
        canvas : :class:`matplotlib.axes._subplots.AxesSubplot`
                Be sure that 3D plotting is enabled on this canvas, with
                :code:`mpl_toolkits.mplot3d import Axes3D` and
                :code:`canvas = matplotlib.pyplot.subplot(projection='3d')`
        title : str
                A title for the figure.
        color : str
                The name of a color for the points to draw.  Passed to the
                appropriate drawing library (bokeh or matplotlib).

        Notes
        -----
        Because we are using ECEF coordinates, the horizontal projection should
        be taken as an approximation!
        """

        if type(canvas).__module__ == 'bokeh.plotting.figure':
            canvas_type = "bokeh"
            import bokeh.plotting
        elif type(canvas).__module__ == 'matplotlib.axes._subplots':
            canvas_type = "pyplot"
            import matplotlib.pyplot as plt
            from mpl_toolkits.mplot3d import Axes3D
        else:
            raise NotImplementedError(
                "canvas must be a bokeh.plotting.figure() or a matplotlib.pyplot.subplots()[1].  You gave me {}".format(
                    type(canvas))
            )

        #if canvas_type == "bokeh":
        #    canvas.title = title
        #elif canvas_type == "pyplot":
        #    canvas.set_title(title)

        # transform to local ENU coordinates at every point, and start at 0.
        pos = self.data[['pos_x', 'pos_y', 'pos_z']].values
        if pos.shape[0] == 0:
            return False

        if canvas_type == "pyplot":
            canvas.plot(pos[:, 0], pos[:, 1], pos[:, 2],
                        color=color)
        else:
            raise NotImplementedError

        pass

    def slide(self, window=60 * 10 ** 9,
              time_step=1 * 10 ** 9,
              time_start=None,
              time_stop=None,
              overlap_ends=True):
        r""" Produce equal-time SpaceCurves using a sliding-window on self.

        Parameters
        ----------
        window : int
            length of window in nanoseconds (default = 60*10**9 or 1 minute)
        time_step : int
            step size in nanoseconds between window starting-points (default = 1*10** or 1 second)
        time_start : int
            start time in nanoseconds (default = None, min time)
        time_stop : int
            stop time in nanoseconds (default = None, max time)
        overlap_ends : bool
            Should endpoints overlap (True) or abutt (False) (default = True)

        Returns
        -------
        iterator of SpaceCurves
        To access the tracks as a list, do
        :code:`list( FT.snip() )`

        Notes
        -----
        The trackids are unchanged! You might want to change them manually.
        """

        if time_start is None:
            time_start = self.data.index.values[0]
        if time_stop is None:
            time_stop = self.data.index.values[-1]

        if not overlap_ends:
            window -= 1

        slices = np.arange(time_start, time_stop, time_step)
        for i, start_ns in enumerate(slices):
            stop_ns = start_ns + window
            newFT = self.copy()
            newFT.data = self.data.loc[start_ns:stop_ns].copy()
            newFT.info = self.info.loc[start_ns:stop_ns].copy()
            yield newFT

            if stop_ns >= time_stop:
                break

    def snip(self, time_step=60 * 10 ** 9, time_start=None, time_stop=None,
             overlap_ends=True):
        """ Cut this SpaceCurve into equal-time snippets.
        This is just a simple wrapper around self.slide( )
        with time_step == window.

        time_step=60*10**9 == 1 minute

        Note! The trackids are unchanged!
        You might want to change them manually.

        Yields
        ------
        iterator of SpaceCurves
        To access the tracks as a list, do
        list( FT.snip() )
        """

        for F in self.slide(window=time_step,
                            time_step=time_step,
                            time_start=time_start,
                            time_stop=time_stop,
                            overlap_ends=overlap_ends):
            yield F

    def clean_copy(self, cleanup_func=curve_geometry.mollifier, **kwargs):
        r""" Make copy in which a cleanup function performed
        on the data.

        cleanup_func is the interface to the functions in curve_geometry.
        In particular:
        1. :func:`curve_geometry.cleanup` does slope comparison
        2. :func:`curve_geometry.mollifier` does a convolution

        cleanup_func should at the minimum take time and position as
        positional arguments 1 and 2 and return both new time and new
        position arrays.  All Keyword arguments (:code:`**kwargs`) are passed on to
        cleanup_func.

        One can tune the various cleanup functions by passing
        a user manufactured function, for example
        .. code::

            my_cleanup = lambda time, x:curve_geometry.mollifier(time,x,width=0.5)
            clean_track = track.clean_copy(cleanup_func = my_cleanup)

        would be equivalent to
        .. code::

            clean_track = track.clean_copy(cleanup_func=curve_geometry.mollifier, width=0.5)

        """

        # Compute the cleaned up values
        time, clean_px = cleanup_func(self.data['time'].values,
                                      self.data['pos_x'].values, **kwargs)
        timey, clean_py = cleanup_func(self.data['time'].values,
                                       self.data['pos_y'].values, **kwargs)
        timez, clean_pz = cleanup_func(self.data['time'].values,
                                       self.data['pos_z'].values, **kwargs)
        assert np.all(time == timey)
        assert np.all(time == timez)

        # [time, clean_vx] = cleanup_func(self.data['time'].values,
        #                                self.data['vel_x'].values)
        # [time, clean_vy] = cleanup_func(self.data['time'].values,
        #                                self.data['vel_y'].values)
        # [time, clean_vz] = cleanup_func(self.data['time'].values,
        #                                self.data['vel_z'].values)

        # Build a new index from the time array
        new_index_start = self.data.index.values[0]
        new_index = np.int64(time*10**9)+new_index_start

        assert new_index.shape[0] == time.shape[0],\
            """New index is shape {} but time is shape {}""".format(new_index.shape[0], time.shape[0])

        # Instantiate a new flighttrack object from the cleaned versions
        newFT = self.__class__(new_index,
                               trackid=self.trackid,
                               platform=self.platform,
                               activity=self.activity,
                               mollified=True)
        newFT.data = pd.DataFrame({'time': time,
                                   'pos_x': clean_px,
                                   'pos_y': clean_py,
                                   'pos_z': clean_pz,
                                   'quality': -np.ones(time.shape, dtype=np.int64)},
                                  columns=['time', 'pos_x', 'pos_y', 'pos_z', 'quality'],
                                  index=new_index)
        return newFT

    def compute(self):
        """ Compute some nice invariants and store them to self.info.
        Vector quantities are generally in the fixed frame.
        """
        # raw data in ECEF frame.
        T = self.data['time'].values
        P = self.data[['pos_x', 'pos_y', 'pos_z']].values
        V = curve_geometry.secant_derivative(T, P)
        #V2 = self.data[['vel_x', 'vel_y', 'vel_z']].values
        #assert not np.any(np.isnan(V2)), "{} bad in {}".format(np.where(np.isnan(V2)), V2.shape)

        # all derivatives and integrals
        A = curve_geometry.secant_derivative(T, V)
        J = curve_geometry.secant_derivative(T, A)
        arclengthS = curve_geometry.secant_arclength(P)

        # norms
        #recspeed = np.linalg.norm(V2, axis=1).flatten()
        speed = np.linalg.norm(V, axis=1).flatten()
        acc = np.linalg.norm(A, axis=1).flatten()
        jerk = np.linalg.norm(J, axis=1).flatten()

        if len(self) > 4:
        # Use signature curve to make curv and tors
            kap, kap_s, tau, tau_s = self.signature_curve()

        #KT = np.ndarray(shape=(T.shape[0], 2), dtype='float')
        #KT[:2, :] = sc[0, :]
        #KT[:-2, :] = sc[-1, :]
        #KT[2:-2, :] = sc

        #TNB_enu = curve_geometry.frenet_frame(V, A)
        #tilt = np.arccos(np.abs(TNB_enu[:, 2, 2]))

        #dKT_ds = curve_geometry.secant_derivative(arclengthS, KT)
        #tilt_array = tilt.copy()
        #tilt_array.shape = (tilt.shape[0], 1)
        #dtilt_ds = curve_geometry.secant_derivative(arclengthS,
        #tilt_array).flatten()

#        curv_per_alt = KT[:, 0] / (P[:,2]+1)
#        acc_per_alt = acc / (P[:,2]+1)
#        tors_per_alt = KT[:, 1] / (P[:,2]+1)
        #print(tau.shape)
        #print(kap_s.shape)
        #print(tau.shape)
        #print(tau_s.shape)
#        friction = kap * speed ** 2  ## need to check this for angle.
#        bank = np.arctan(friction / 9.8)  ## replace 9.8 with gravity??

        # dKT_ds = curve_geometry.secant_derivative(arclengthS, KT)

        self.info['vel_x'] = V[:, 0]
        self.info['vel_y'] = V[:, 1]
        self.info['vel_z'] = V[:, 2]
        self.info['acc_x'] = A[:, 0]
        self.info['acc_y'] = A[:, 1]
        self.info['acc_z'] = A[:, 2]
        self.info['len'] = arclengthS  # This is the only thing we store in data
        self.info['speed'] = speed
        #self.info['recspeed'] = recspeed
        self.info['acc'] = acc
        self.info['jerk'] = jerk

        if len(self) > 4:
            self.info['curv'] = kap
            self.info['curv_s'] = kap_s
            self.info['tors'] = tau

            self.info['tors_s'] = 0.0
        #self.info['tors_s'].values[3:-3] = tau_s
        #self.info['tors_s'].values[:3] = tau_s[0]
        #self.info['tors_s'].values[-3:] = tau_s[-1]

        #self.info['tors'] = tau
        #self.info['dKds'] = curv_s
        #self.info['dTds'] = tau_s
        pass

    def featurize(self, sort_and_grab_num=None):
        """ A convenience function to compute everything we think might be
        important.  This returns a dictionary of np arrays.
        Optionally, we can hit all of the persistence diagrams with sort+grab.
        """

        feature_dict = dict()

        self.compute()

        for name in ['curv', 'tors', 'up', 'acc',
                     'Dcurv/Dlen', 'Dtors/Dlen', 'Dup/Dlen',
                     'speed', 'friction', 'curv/alt', 'tors/alt', 'acc/alt',
                     ]:
            S = Signal(self.info[name].values)
            if sort_and_grab_num is not None:
                feature_dict[name] = S.pers.grab(sort_and_grab_num)['keepcode']
            else:
                feature_dict[name] = S.pers.diagram

        return feature_dict

    def copy(self):
        """ make an identical copy of self. """
        d = self.data.copy()
        newFT = self.__class__(d.index.values, trackid=self.trackid,
                               platform=self.platform,
                               activity=self.activity,
                               )
        newFT.data = d
        newFT.info = self.info.copy()
        return newFT

    @classmethod
    def load(cls, filename):
        """ Simple CSV reader.
        columns ('time', 'pos_x', 'pos_y', 'pos_z')
        """
        data = pd.read_csv(filename, sep="\t")
        F = cls(tn=data['time'].values,
                px=data['pos_x'].values,
                py=data['pos_y'].values,
                pz=data['pos_z'].values)
        return F

    def duration(self):
        """ return string of duraction of SpaceCurve,
        converting integer nanoseconds to string seconds.
        """
        times = self.data.index.values
        if len(times) == 0:
            return 0
        ns = repr(times[-1] - times[0])
        assert ns.find("e") == -1
        seconds = ns[:-9] + "." + ns[-9:]
        return seconds

    def __len__(self):
        return len(self.data)

    def __repr__(self):
        return "SpaceCurve with {} entries and duration {}".format(len(self), self.duration())

    def mass_profile(self, num_bins=100, underflow=-50, overflow=50):
        """ histogram of the lift_and_mass by mass. """

        LM = self.lift_and_mass()
        width = np.float64(overflow - underflow) / np.float64(num_bins)

        finite_bins = np.arange(underflow, overflow + width, width)
        bins = np.zeros(shape=(finite_bins.shape[0] + 2,))
        bins[0] = np.float64('-inf')
        bins[-1] = np.float64('inf')
        bins[1:-1] = finite_bins[:]

        return np.histogram(LM[:, 1], bins)

    def lift_profile(self, num_bins=500, underflow=0, overflow=500000):
        """ histogram of the lift_and_mass by lift. """
        LM = self.lift_and_mass()
        width = np.float64(overflow - underflow) / np.float64(num_bins)

        finite_bins = np.arange(underflow, overflow + width, width)
        bins = np.zeros(shape=(finite_bins.shape[0] + 2,))
        bins[0] = np.float64('-inf')
        bins[-1] = np.float64('inf')
        bins[1:-1] = finite_bins[:]

        return np.histogram(LM[:, 0], bins)

    def lift_and_mass(self):
        r""" Produce the terms of the force-balance equations, to help derive
        coefficient-of-lift and mass from pure trajectory information.

        Consider an aircraft flying with velocity :math:`v`, banking angle
        :math:`\theta`, air density :math:`\rho`, mass :math:`m`, gravity
        :math:`g`, vertical acceleration :math:`\ddot{z}`, and lift coefficient
        :math:`C_{\text{lift}}`.

        The force-balance equation is
        ..math::

            LHS = \frac12 \|v\|^2 \, \cos(\theta)\, \rho
            RHS = (g + \ddot{z})
            LHS\, C_{\text{lift}} = RHS\, m

        Returns
        -------
        coefficients : :class:np.array
            the columns are LHS and RHS.

        """

        # build an atmospheric air density profile using the data
        # 1976 international standard atmosphere.  We just use a 4th-order fit.
        # https://en.wikipedia.org/wiki/International_Standard_Atmosphere
        # alts = np.array([-610, 11000, 20000, 32000,47000])
        # dens = np.array([1.2985,0.3639,0.0880,.0105,0.0020])
        # air_denstiy = np.poly1d(np.polyfit(alts,dens,4))
        air_density = np.poly1d([2.70588959e-19, -5.57103078e-14, 3.91598431e-09, -1.15140013e-04, 1.22679477e+00])

        speed = self.info['speed'].values
        altacc = self.info['acc_z'].values
        bank = self.info['bank'].values
        cosb = np.cos(bank)
        h = self.info['alt'].values
        air = air_density(h)
        LHS = 0.5 * speed ** 2 * cosb * air
        RHS = 9.8 + altacc
        return np.array([LHS, RHS]).T

    def auto_bin(self, num_bins=10, underflow=0, overflow=100):
        """ Count bins on the transformed persistence diagrams of
        -Speed/7         (so it is expected to be between 0 and 100)
        -Climb/3         (so it is expected to be between 0 and 100)
        -Curvature*10000 (so it is expected to be between 0 and 100)
        -Torsion*10000   (so it is expected to be between 0 and 100)
        -Bank*100/(pi/4) (==grade, between 0 and 100)

        Bins are added for (-inf, underflow] and (overflow,inf).
        If underflow and overflow are not provided, they are set to min/max.

        """

        speed01 = Signal(self.info['speed'].values / 7)
        climb01 = Signal(self.info['climb'].values / 3)
        curv01 = Signal(self.info['curv'].values * 10000)
        tors01 = Signal(self.info['tors'].values * 10000)
        bank01 = Signal(self.info['bank'].values * 400 / np.pi)

        width = np.float64(overflow - underflow) / np.float64(num_bins)

        speed_hist, speed_bins = speed01.pers.bin(width=width,
                                                  underflow=underflow,
                                                  overflow=overflow)
        climb_hist, climb_bins = climb01.pers.bin(width=width,
                                                  underflow=underflow,
                                                  overflow=overflow)
        curv_hist, curv_bins = curv01.pers.bin(width=width,
                                               underflow=underflow,
                                               overflow=overflow)
        tors_hist, tors_bins = tors01.pers.bin(width=width,
                                               underflow=underflow,
                                               overflow=overflow)
        bank_hist, bank_bins = bank01.pers.bin(width=width,
                                               underflow=underflow,
                                               overflow=overflow)
        assert np.all(curv_bins == tors_bins) and np.all(
            curv_bins == bank_bins)

        return {'bins': curv_bins,
                'speed': speed_hist,
                'climb': climb_hist,
                'curv': curv_hist,
                'tors': tors_hist,
                'bank': bank_hist}

    def signature_curve(self):
        r""" Olver/Boutin signature curve.
        (kappa, kappa_s, tau, tau_s)
        due to difference methods, the lengths are (n-2, n-4, n-4, n-6),
        but we pad them all to length n

        Usage:
        ------
        >>> ts = np.arange(0,12,0.1)
        >>> # Line
        >>> C = SpaceCurve(tn = np.arange(ts.shape[0]),
        ...                px = 5*ts + 3, py=2*ts + 5)
        >>> kappa, kappa_s, tau, tau_s = C.signature_curve()
        >>> np.allclose(kappa, 0.0, atol=1.5e-7)
        True
        >>> np.allclose(kappa_s, 0.0, atol=1.5e-7)
        True
        >>> np.allclose(tau, 0.0, atol=1.5e-7)
        True
        >>> np.allclose(tau_s, 0.0, atol=1.5e-7)
        True
        >>> ts = np.arange(0,12,0.1)
        >>> # Circle with constant speed
        >>> C = SpaceCurve(tn = np.arange(ts.shape[0]),
        ...                px = np.cos(ts),
        ...                py = np.sin(ts))
        >>> kappa, kappa_s, tau, tau_s = C.signature_curve()
        >>> kappa.shape # padded
        (120,)
        >>> kappa_s.shape
        (120,)
        >>> np.allclose(kappa, 1.)
        True
        >>> np.allclose(kappa_s, 0.)
        True
        >>> ts = np.arange(0,12,0.1)
        >>> # Circle with varying speed
        >>> C = SpaceCurve(tn = np.arange(ts.shape[0]),
        ...                px = np.cos(ts**2),
        ...                py = np.sin(ts**2))
        >>> kappa, kappa_s, tau, tau_s  = C.signature_curve()
        >>> kappa_s.shape # padded
        (120,)
        >>> np.allclose(kappa, 1.)
        True
        >>> np.allclose(kappa_s, 0.)
        True
        >>> np.allclose(tau, 0.)
        True
        >>> np.allclose(tau_s, 0.)
        True
        >>> ts = np.arange(1,13,0.01)
        >>> # A Spiral
        >>> C = SpaceCurve(tn = np.arange(ts.shape[0]),
        ...                px = np.exp(0.75*ts)*np.cos(ts),
        ...                py = np.exp(0.75*ts)*np.sin(ts))
        >>> kappa, kappa_s, tau, tau_s = C.signature_curve()
        >>> kappa.shape # padded
        (1200,)
        >>> np.allclose(kappa[1:-1],  np.exp(-0.75*ts[1:-1]), atol=0.1)
        True
        >>> np.allclose(kappa_s[2:-2]*np.exp(1.5*ts[2:-2]), -12./25., atol=0.01)
        True
        >>> # A Helix
        >>> C = SpaceCurve(tn = np.arange(ts.shape[0]),
        ...                px = 3*np.cos(ts),
        ...                py = 3*np.sin(ts),
        ...                pz = 4*ts)
        >>> kappa, kappa_s, tau, tau_s = C.signature_curve()
        >>> np.allclose(kappa, 3/25.)
        True
        >>> np.allclose(kappa_s, 0.0)
        True
        >>> np.allclose(tau, 4/25.)
        True
        >>> np.allclose(tau_s, 0.0)
        True
        >>> # A Helix (reversed)
        >>> C = SpaceCurve(tn = np.arange(ts.shape[0]),
        ...                px = 3*np.cos(-ts),
        ...                py = 3*np.sin(-ts),
        ...                pz = 4*ts)
        >>> kappa, kappa_s, tau, tau_s = C.signature_curve()
        >>> np.allclose(kappa, -3/25.,)
        True
        >>> np.allclose(kappa_s, 0.0)
        True
        >>> np.allclose(tau, -4/25.,)
        True
        >>> np.allclose(tau_s, 0.0)
        True
        """

        #if not np.all(self.data['pos_z'].values == 0.):
        #    raise ValueError("This method currently handles only planar curves.")

        # if np.all(self.data['pos_x'].values == 0.):
        pos = self.data[['pos_x', 'pos_y', 'pos_z']].values
        n = pos.shape[0]

        # follow Calabi's naming convention.
        # We deal with 1-interior points.
        P_i_mns_1 = pos[:-2, :]
        P_i       = pos[1:-1, :]
        P_i_pls_1 = pos[2:, :]

        # Use the determinant to set a right-handed sign.
        triples = np.ndarray(shape=(n-2, 3,3), dtype='float')
        triples[:, 0, :] = P_i_mns_1
        triples[:, 1, :] = P_i
        triples[:, 2, :] = P_i_pls_1
        sign = (-1)**np.signbit(np.linalg.det(triples))

        a = np.sqrt(np.sum((P_i       - P_i_mns_1)**2, axis=1))
        b = np.sqrt(np.sum((P_i_pls_1 - P_i      )**2, axis=1))
        c = np.sqrt(np.sum((P_i_pls_1 - P_i_mns_1)**2, axis=1))
        s = 0.5*(a+b+c)
        # If a,b,c are co-linear, then we might get s-c to be negative
        # due to roundoff error.  (or other permutations)
        s_minus_a = np.clip(s-a, 0., np.infty)
        s_minus_b = np.clip(s-b, 0., np.infty)
        s_minus_c = np.clip(s-c, 0., np.infty)

        abc = a*b*c
        # Calabi,et al eqn (2.2)
        non_trivial = (abc != 0)
        kappa = sign*4*np.sqrt(s*s_minus_a*s_minus_b*s_minus_c)
        kappa[non_trivial] = kappa[non_trivial] / abc[non_trivial]
        kappa[~ non_trivial] = 0.0
        assert kappa.shape[0] == n-2

        # Now, we follow Boutin's naming convention.
        # We deal with 2-interior points.
        P_i = pos[2:-2, :]
        P_i_pls_1 = pos[3:-1, :]
        P_i_pls_2 = pos[4:, :]
        P_i_mns_1 = pos[1:-3, :]
        P_i_mns_2 = pos[0:-4, :]
        a = np.sqrt(np.sum((P_i       - P_i_mns_1)**2, axis=1))
        b = np.sqrt(np.sum((P_i_pls_1 - P_i      )**2, axis=1))
        # c = np.sqrt(np.sum((P_i_pls_1 - P_i_mns_1)**2, axis=1))
        d = np.sqrt(np.sum((P_i_pls_2 - P_i_pls_1)**2, axis=1))
        e = np.sqrt(np.sum((P_i_pls_2 - P_i      )**2, axis=1))
        f = np.sqrt(np.sum((P_i_pls_2 - P_i_mns_1)**2, axis=1))
        g = np.sqrt(np.sum((P_i_mns_2 - P_i_mns_1)**2, axis=1))
        # reverse collections, for reverse tau
        dd = g
        ee = np.sqrt(np.sum((P_i_mns_2 - P_i      )**2, axis=1))
        ff = np.sqrt(np.sum((P_i_mns_2 - P_i_pls_1)**2, axis=1))

        assert a.shape[0] == n-4

        # Note that the index of a goes 0..n-5, and
        # Note that the index of kappa goes 0..n-3.
        # and P[i] corresponds to a[i] and kappa[i+1]
        denom_ks = 2*a + 2*b + d + g
        non_trivial = (denom_ks != 0)
        kappa_s = 3*(kappa[2:] - kappa[:-2])
        kappa_s[non_trivial] = kappa_s[non_trivial]/denom_ks[non_trivial]
        kappa_s[~ non_trivial] = 0.0

        # tau according to Boutin's \tilde{tau}_1, in the forward direction
        tetra_height = np.ndarray(shape = kappa_s.shape, dtype='float')
        for i in range(P_i.shape[0]):
            tetrahedron = np.array([P_i_mns_1[i] - P_i[i],
                                    P_i_pls_1[i] - P_i[i],
                                    P_i_pls_2[i] - P_i[i]]).T

            tetra_height[i] = np.linalg.qr(tetrahedron, mode='r')[-1,-1]
        # we want tau = 6 * tetra_height / denom_t, but
        # don't want to divide by zero, which happens if points repeat.
        tau_fwd = 6 * tetra_height
        denom_t = d * e * f * kappa[1:-1] # sign is inherited!
        non_trivial = (denom_t != 0) & (tetra_height != 0)
        tau_fwd[non_trivial] = tau_fwd[non_trivial] / denom_t[non_trivial]
        tau_fwd[~ non_trivial] = 0.0

        # tau according to Boutin's \tilde{tau}_1, in the backard direction
        tetra_height = np.ndarray(shape = kappa_s.shape, dtype='float')
        for i in range(P_i.shape[0]):
            tetrahedron = np.array([P_i_mns_2[i] - P_i[i],
                                    P_i_mns_1[i] - P_i[i],
                                    P_i_pls_1[i] - P_i[i]]).T

            tetra_height[i] = np.linalg.qr(tetrahedron, mode='r')[-1,-1]
        # we want tau = 6 * tetra_height / denom_t, but
        # don't want to divide by zero, which happens if points repeat.
        tau_bwd = 6 * tetra_height
        denom_t = d * e * f * kappa[1:-1] # sign is inherited!
        non_trivial = (denom_t != 0) & (tetra_height != 0)
        tau_bwd[non_trivial] = tau_bwd[non_trivial] / denom_t[non_trivial]
        tau_bwd[~ non_trivial] = 0.0

        tau = (tau_fwd + tau_bwd)/2

#        # tau_s according to Boutin's (17), in the forward direction
#        P_i_pls_3 = pos[4:, :]
#        h = np.sqrt(np.sum((P_i_pls_3 - P_i_pls_2[:-1])**2, axis=1))
#        dh = d[:-1] + h
#        denom_ts = denom_ks[1:-1] + dh
#        old_settings = np.seterr(divide='ignore')  #seterr to known value
#        tau_s_fwd = 4*(tau[1:] - tau[:-1] + (denom_ks[1:-1] -3*dh) * tau * kappa_s / (6 * kappa[2:-2]))/denom_ts
#        tau_s_fwd[denom_ts == 0] = 0
#        np.seterr(**old_settings)
#
#        # tau_s according to Boutin's (17), in the backward direction
#        P_i_mns_3 = pos[:-4, :]
#        h = np.sqrt(np.sum((P_i_mns_3 - P_i_mns_2[1:])**2, axis=1))
#        dh = d[1:] + h
#        denom_ts = denom_ks[1:-1] + dh
#        old_settings = np.seterr(divide='ignore')  #seterr to known value
#        tau_s_bwd = 4*(tau[1:] - tau[:-1] + (denom_ks[1:-1] -3*dh) * tau * kappa_s / (6 * kappa[2:-2]))/denom_ts
#        tau_s_bwd[denom_ts == 0] = 0
#        np.seterr(**old_settings)
#
#        tau_s = (tau_s_fwd + tau_s_bwd)/2

        assert kappa.shape == (n-2,)
        assert kappa_s.shape == (n-4,)
        assert tau.shape == (n-4,)
        #assert tau_s.shape == (n-6,)

        #print(kappa.shape, tau_s.shape)

        kappa_pad = np.ndarray(shape=(n,), dtype='float')
        kappa_s_pad = np.ndarray(shape=(n,), dtype='float')
        tau_pad = np.ndarray(shape=(n,), dtype='float')
        tau_s_pad = np.ndarray(shape=(n,), dtype='float')

        kappa_pad[1:-1] = kappa
        kappa_pad[:1] = kappa[0]
        kappa_pad[-1:] = kappa[-1]

        kappa_s_pad[2:-2] = kappa_s
        kappa_s_pad[:2] = kappa_s[0]
        kappa_s_pad[-2:] = kappa_s[-1]

        tau_pad[2:-2] = tau
        tau_pad[:2] = tau[0]
        tau_pad[-2:] = tau[-1]

        tau_s_pad[:] = 0.0
        #tau_s_pad[2:-2] = tau_s
        #tau_s_pad[:2] = tau_s[0]
        #tau_s_pad[-2:] = tau_s[-1]

        return kappa_pad, kappa_s_pad, tau_pad, tau_s_pad

# end of class SpaceCurve

{'ProcessPoolExecutor': 'concurrent.futures.ProcessPoolExecutor', 'bokeh.plotting': 'bokeh.plotting', 'plt': 'matplotlib.pyplot'}


"""
Utility functions.
Most taken from the PTAN (PyTorch Agent Net) library by Shmuma
https://github.com/Shmuma/ptan
and https://github.com/taldatech/pytorch-ls-ddpg
"""

# imports
import numpy as np
import torch
import time
import sys
import collections
import os
from utils.nn_agent_models import float32_preprocessor

def test_net(net, env, agent_model, count=10, device="cpu"):
    rewards = 0.0
    steps = 0
    for _ in range(count):
        obs = env.reset()
        while True:
            obs_v = agent_model.float32_preprocessor([obs]).to(device)
            mu_v = net(obs_v)
            action = mu_v.squeeze(dim=0).data.cpu().numpy()
            action = np.clip(action, -1, 1)
            obs, reward, done, _ = env.step(action)
            rewards += reward
            steps += 1
            if done:
                break
    return rewards / count, steps / count

def save_agent_state(act_net, crt_net, optimizers, frame, games, save_replay=False, replay_buffer=None, name='',
                     path=None):
    """
    This function saves the current state of the NN (the weights) to a local file.
    :param act_net: the current actor NN (nn.Module)
    :param crt_net: the current critic NN (nn.Module)
    :param optimizers: the network's optimizer (torch.optim)
    :param frame: current frame number (int)
    :param games: total number of games seen (int)
    :param save_replay: whether or not to save the replay buffer (bool)
    :param replay_buffer: the replay buffer (list)
    :param name: specific name for the checkpoint (str)
    :param path: path to specific location where to save (str)
    """
    dir_name = './agent_ckpt'
    if path:
        full_path = path
    else:
        if name:
            filename = "agent_randpol_" + name + ".pth"
        else:
            filename = "agent_randpol.pth"
        dir_name = './agent_ckpt'
        full_path = os.path.join(dir_name, filename)
    if not os.path.exists(dir_name):
        os.makedirs(dir_name)
    if save_replay and replay_buffer is not None:
        torch.save({
            'act_state_dict': act_net.state_dict(),
            'crt_state_dict': crt_net.state_dict(),
            'act_optimizer_state_dict': optimizers[0].state_dict(),
            'crt_optimizer_state_dict': optimizers[1].state_dict(),
            'frame_count': frame,
            'games': games,
            'replay_buffer': replay_buffer
        }, full_path)
    else:
        torch.save({
            'act_state_dict': act_net.state_dict(),
            'crt_state_dict': crt_net.state_dict(),
            'act_optimizer_state_dict': optimizers[0].state_dict(),
            'crt_optimizer_state_dict': optimizers[1].state_dict(),
            'frame_count': frame,
            'games': games
        }, full_path)
    print("Saved Agent checkpoint @ ", full_path)

def load_agent_state(act_net, crt_net, optimizers, path=None, copy_to_target_network=False, load_optimizer=True,
                     target_nets=None, buffer=None, load_buffer=False):
    """
    This function loads a state of the NN (the weights) from a local file.
    :param act_net: the current actor NN (nn.Module)
    :param crt_net: the current critic NN (nn.Module)
    :param optimizers: the network's optimizers (torch.optim)
    :param path: full path to checkpoint file (.pth) (str)
    :param copy_to_target_network: whether or not to copy the weights to target network (bool)
    :param load_optimizer: whether or not to load the optimizer state (bool)
    :param load_buffer: whether or not to load the replay buffer (bool)
    :param buffer: the replay buffer
    :param target_nets: the target NNs
    """
    if path is None:
        raise SystemExit("path to model must be specified")
    else:
        full_path = path
    exists = os.path.isfile(full_path)
    if exists:
        if not torch.cuda.is_available():
            checkpoint = torch.load(full_path, map_location='cpu')
        else:
            checkpoint = torch.load(full_path)
        act_net.load_state_dict(checkpoint['act_state_dict'])
        crt_net.load_state_dict(checkpoint['crt_state_dict'])
        if load_optimizer:
            optimizers[0].load_state_dict(checkpoint['act_optimizer_state_dict'])
            optimizers[1].load_state_dict(checkpoint['crt_optimizer_state_dict'])
        # self.steps_count = checkpoint['steps_count']
        # self.episodes_seen = checkpoint['episodes_seen']
        # selector.epsilon = checkpoint['epsilon']
        # self.num_param_update = checkpoint['num_param_updates']
        print("Checkpoint loaded successfully from ", full_path)
        # # for manual loading a checkpoint
        if copy_to_target_network and target_nets is not None:
            target_nets[0].sync()
            target_nets[1].sync()
        if load_buffer and buffer is not None:
            buffer.buffer = checkpoint['replay_buffer']
    else:
        print("No checkpoint found...")

def unpack_batch(batch, device="cpu"):
    states, actions, rewards, dones, last_states = [], [], [], [], []
    for exp in batch:
        states.append(exp.state)
        actions.append(exp.action)
        rewards.append(exp.reward)
        dones.append(exp.last_state is None)
        if exp.last_state is None:
            last_states.append(exp.state)
        else:
            last_states.append(exp.last_state)
    states_v = float32_preprocessor(states).to(device)
    actions_v = float32_preprocessor(actions).to(device)
    rewards_v = float32_preprocessor(rewards).to(device)
    last_states_v = float32_preprocessor(last_states).to(device)
    dones_t = torch.ByteTensor(dones).to(device)
    return states_v, actions_v, rewards_v, dones_t, last_states_v

class TBMeanTracker:
    """
    TensorBoard value tracker: allows to batch fixed amount of historical values and write their mean into TB

    Designed and tested with pytorch-tensorboard in mind
    """
    def __init__(self, writer, batch_size):
        """
        :param writer: writer with close() and add_scalar() methods
        :param batch_size: integer size of batch to track
        """
        assert isinstance(batch_size, int)
        assert writer is not None
        self.writer = writer
        self.batch_size = batch_size

    def __enter__(self):
        self._batches = collections.defaultdict(list)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.writer.close()

    @staticmethod
    def _as_float(value):
        assert isinstance(value, (float, int, np.ndarray, np.generic, torch.autograd.Variable)) or torch.is_tensor(value)
        tensor_val = None
        if isinstance(value, torch.autograd.Variable):
            tensor_val = value.data
        elif torch.is_tensor(value):
            tensor_val = value

        if tensor_val is not None:
            return tensor_val.float().mean()
        elif isinstance(value, np.ndarray):
            return float(np.mean(value))
        else:
            return float(value)

    def track(self, param_name, value, iter_index):
        assert isinstance(param_name, str)
        assert isinstance(iter_index, int)

        data = self._batches[param_name]
        data.append(self._as_float(value))

        if len(data) >= self.batch_size:
            if torch.is_tensor(data[0]):
                self.writer.add_scalar(param_name, torch.mean(torch.stack(data)), iter_index)
            else:
                self.writer.add_scalar(param_name, np.mean(data), iter_index)
            data.clear()

class RewardTracker:
    def __init__(self, writer, min_ts_diff=1.0):
        """
        Constructs RewardTracker
        :param writer: writer to use for writing stats
        :param min_ts_diff: minimal time difference to track speed
        """
        self.writer = writer
        self.min_ts_diff = min_ts_diff

    def __enter__(self):
        self.ts = time.time()
        self.ts_frame = 0
        self.total_rewards = []
        return self

    def __exit__(self, *args):
        self.writer.close()

    def reward(self, reward, frame, epsilon=None):
        self.total_rewards.append(reward)
        mean_reward = np.mean(self.total_rewards[-100:])
        ts_diff = time.time() - self.ts
        if ts_diff > self.min_ts_diff:
            speed = (frame - self.ts_frame) / ts_diff
            self.ts_frame = frame
            self.ts = time.time()
            epsilon_str = "" if epsilon is None else ", eps %.2f" % epsilon
            print("Steps:%d   episodes:%d, mean reward %.3f" % (
                frame, len(self.total_rewards), mean_reward
            ))
            sys.stdout.flush()
            self.writer.add_scalar("speed", speed, frame)
        if epsilon is not None:
            self.writer.add_scalar("epsilon", epsilon, frame)
        self.writer.add_scalar("reward_100", mean_reward, frame)
        self.writer.add_scalar("reward", reward, frame)
        return mean_reward if len(self.total_rewards) > 30 else None


"""The Nettiego component."""
import asyncio
import logging
import time
from datetime import timedelta
from math import ceil

import async_timeout
from aiohttp.client_exceptions import ClientConnectorError
from homeassistant.const import CONF_URL, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import Config, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import (
    DOMAIN,
    MAX_REQUESTS_PER_DAY,
)
from .nettiego import Client, Data, NamfError

PLATFORMS = ["air_quality", "weather"]

_LOGGER = logging.getLogger(__name__)

def set_update_interval(hass, instances):
    # We check how many Nettiego configured instances are and calculate interval to not exceed allowed numbers of requests.
    interval = timedelta(minutes=ceil(24 * 60 / MAX_REQUESTS_PER_DAY) * instances)

    if hass.data.get(DOMAIN):
        for instance in hass.data[DOMAIN].values():
            instance.update_interval = interval

    return interval

async def async_setup(hass: HomeAssistant, config: Config) -> bool:
    return True

async def async_setup_entry(hass, config_entry):
    uniqueId = config_entry.data[CONF_NAME]
    url = config_entry.data[CONF_URL]
    latitude = config_entry.data[CONF_LATITUDE]
    longitude = config_entry.data[CONF_LONGITUDE]

    # For backwards compat, set unique ID
    if config_entry.unique_id is None:
        hass.config_entries.async_update_entry(
            config_entry, unique_id=f"{uniqueId}"
        )

    websession = async_get_clientsession(hass)
    update_interval = set_update_interval(
        hass, len(hass.config_entries.async_entries(DOMAIN))
    )

    coordinator = NettiegoUpdateCoordinator(
        hass, websession, url, latitude, longitude, update_interval
    )

    await coordinator.async_refresh()

    if not coordinator.last_update_success:
        raise ConfigEntryNotReady

    hass.data.setdefault(DOMAIN, {})
    hass.data[DOMAIN][config_entry.entry_id] = coordinator

    for component in PLATFORMS:
        hass.async_create_task(
            hass.config_entries.async_forward_entry_setup(config_entry, component)
        )

    return True

async def async_unload_entry(hass, config_entry):
    """Unload a config entry."""
    unload_ok = all(
        await asyncio.gather(
            *[
                hass.config_entries.async_forward_entry_unload(config_entry, component)
                for component in PLATFORMS
            ]
        )
    )
    if unload_ok:
        hass.data[DOMAIN].pop(config_entry.entry_id)

    set_update_interval(hass, len(hass.data[DOMAIN]))

    return unload_ok

class NettiegoUpdateCoordinator(DataUpdateCoordinator):
    INDEX_DATA = "data"
    INDEX_CONFIG = "config"

    def __init__(self, hass, session, url, latitude, longitude, update_interval):
        """Initialize."""
        super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
        self.latitude = latitude
        self.longitude = longitude
        self.client = Client(session, base_url=url)

    async def _async_update_data(self):
        """Update data via library."""
        data = {}
        with async_timeout.timeout(30):
            try:
                data[NettiegoUpdateCoordinator.INDEX_DATA] = await self._get_data()
                data[NettiegoUpdateCoordinator.INDEX_CONFIG] = await self._get_config()
            except (NamfError, ClientConnectorError) as error:
                raise UpdateFailed(error) from error
        return data

    async def _get_data(self, tries=3, timeout=5):
        with async_timeout.timeout(timeout * tries):
            last_error = None
            for x in range(tries):
                try:
                    return await self.client.get_data()
                except (NamfError, ClientConnectorError) as error:
                    last_error = error
                    _LOGGER.error(error)
                time.sleep(timeout / (tries + 1))
            raise UpdateFailed(last_error) from last_error

    async def _get_config(self, tries=3, timeout=5):
        with async_timeout.timeout(timeout * tries):
            last_error = None
            for x in range(tries):
                try:
                    return await self.client.get_config()
                except (NamfError, ClientConnectorError) as error:
                    last_error = error
                    _LOGGER.error(error)
                time.sleep(timeout/(tries+1))
            raise UpdateFailed(last_error) from last_error


# -*- coding: utf-8 -*-
"""
Created on Tue Sep 14 21:37:28 2021

@author: Chatterpaul
This show the basics of Rectangular Phase Array Antenna Patterns
# Reference: https://www.routledge.com/Radar-Systems-Analysis-and-Design-Using-MATLAB/Mahafza/p/book/9781439884959
# https://www.routledge.com/downloads/K13984/K13984_Downloads.zip :: rect_array.m
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm

def rect_array(Nxr,Nyr,dolxr,dolyr,theta0,phi0,winid,win,nbits):
    #%%%%%%%%%%%%%%%%%%%% ************************ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    # This function computes the 3-D directive gain patterns for a plannar array
    # This function uses the fft2 to compute its output
    #%%%%%%%%%%%%%%%%%%%% ************  INPUTS ************ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    # Nxr ==> number of along x-axis; Nyr ==> number of elements along y-axis
    # dolxr ==> element spacing in x-direction; dolyr ==> element spacing in y-direction Both are in lambda units
    # theta0 ==> elevation steering angle in degrees, phi0 ==> azimuth steering angle in degrees
    # winid ==> window identifier; winid negative ==> no window ; winid positive ==> use window given by win
    # win ==> input window function (2-D window) MUST be of size (Nxr X Nyr)
    # nbits is the number of nbits used in phase quantization; nbits negative ==> NO quantization
    #%%%%%%%%%%%%%%%%%%%% *********** OUTPUTS ************* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    # pattern ==> directive gain pattern
    #%%%%%%%%%%%%%%%%%%%% ************************ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

    eps = 0.0001
    nx = np.arange(0,Nxr)
    ny = np.arange(0,Nyr)
    i = 1j

    # check that window size is the same as the array size
    nw = np.size(win,0);
    mw = np.size(win,1);
    if winid > 0:
        if nw != Nxr:
            print('STOP == Window size must be the same as the array')
            return
        if mw != Nyr:
            print('STOP == Window size must be the same as the array')
            return

    #if dol is > 0.5 then; choose dol = 0.5 and compute new N
    if (dolxr <=0.5):
       ratiox = 1
       dolx = dolxr
       Nx = Nxr
    else:
       ratiox = np.ceil(dolxr/.5)
       Nx = (Nxr -1 ) * ratiox + 1
       dolx = 0.5

    if (dolyr <=0.5):
       ratioy = 1
       doly = dolyr
       Ny = Nyr
    else:
       ratioy = np.ceil(dolyr/.5)
       Ny = (Nyr -1) * ratioy + 1
       doly = 0.5

    # choose proper size fft, for minimum value choose 256X256
    Nrx = 10 * Nx;
    Nry = 10 * Ny;
    nfftx = 2**(np.ceil(np.log(Nrx)/np.log(2)));
    nffty = 2**(np.ceil(np.log(Nry)/np.log(2)));
    if nfftx < 256:
        nfftx = 256
    if nffty < 256:
        nffty = 256;

    # generate array of elements with or without window
    if winid < 0:
        array = np.ones((Nxr,Nyr))
    else:
        array = win

    # convert steering angles (theta0, phi0) to radians
    theta0 = theta0 * np.pi / 180;
    phi0 = phi0 * np.pi / 180;
    # convert steering angles (theta0, phi0) to U-V sine-space
    u0 = np.sin(theta0) * np.cos(phi0);
    v0 = np.sin(theta0) * np.sin(phi0);
    print(f'Steering to UV={u0:0.3f},{v0:0.3f}')

    # Use formula thetal = (2*pi*n*dol) * sin(theta0) divided into 2^m levels
    # and rounded to the nearest qunatization level
    if nbits < 0:
        phasem = np.exp(i*2*np.pi*dolx*u0 * nx * ratiox)
        phasen = np.exp(i*2*np.pi*doly*v0 * ny * ratioy)
    else:
        levels = 2**nbits
        qlevels = 2.0*np.pi / levels # compute quantization levels
        sinthetaq = np.round(dolx * nx * u0 * levels * ratiox) * qlevels # vector of possible angles
        sinphiq = np.round(doly * ny * v0 * levels * ratioy) * qlevels # vector of possible angles
        phasem = np.exp(i*sinthetaq)
        phasen = np.exp(i*sinphiq)

    # add the phase shift terms
    array = array * (np.outer(phasem,phasen));

    # determine if interpolation is needed (i.e N > Nr)
    w = np.zeros((int(Nxr*ratiox),int(Nyr*ratioy)))*i
    if (Nx > Nxr) or (Ny > Nyr):
        for xloop in np.arange(Nxr): #1 : Nxr
            temprow = array[xloop,:]
            w[int(xloop*ratiox),:int(Ny):int(ratioy)] = temprow
        array = w;
    else:
        w = array

    w = array;
    # Compute array pattern
    wpadd = np.pad(w,[(0,int(nfftx-np.size(w,0))),(0,int(nffty-np.size(w,1)))]);
    arrayfft = np.abs(np.fft.fftshift(np.fft.fft2(wpadd)))**2

    #compute [su,sv] matrix
    U = np.arange(-nfftx/2,nfftx/2)/(dolx*nfftx)
    #indexx = np.argwhere(np.abs(U) <= 1)
    #U = U[indexx]
    V = np.arange(-nffty/2,nffty/2)/(doly*nffty)
    #indexy = np.argwhere(np.abs(V) <= 1)
    #V = V[indexy]

    # Normalize to generate gain patern
    #rbar = np.sum(arrayfft[indexx,indexy]) / dolx/doly/4./nfftx/nffty;
    #arrayfft = arrayfft(indexx,indexy) /rbar;
    rbar = np.sum(arrayfft) / dolx/doly/4./nfftx/nffty
    arrayfft = arrayfft/rbar

    SV,SU = np.meshgrid(V,U);
    indx = (SU**2 + SV**2) > 1
    arrayfft[indx] = eps/10;

    pattern = 10*np.log10(arrayfft + eps)

    plt.figure(1)#,figsize = (9,6))
    #plt.contour(SV,SU,pattern)
    plt.imshow(pattern,extent=[-1,1,-1,1],origin = 'lower' )
    plt.plot(v0,u0,'m*')
    plt.colorbar()
    plt.xlabel('v')
    plt.ylabel('u')
    plt.title(f'Steered:u0={u0:0.3f}, v0={v0:0.3f}')

    fig = plt.figure(2,figsize = (9,6))
    ax = fig.gca(projection='3d')
    dist = np.round(pattern)
    dist_max = np.max(dist)
    my_col = cm.jet(dist/dist_max)
    ax.plot_surface(SV, SU, pattern, rstride=1, cstride=1, facecolors=my_col,
        linewidth=0, antialiased=False)
    plt.xlabel('v')
    plt.ylabel('u')
    ax.set_zlabel('gain (dB)')

    return pattern
#%% Main Script
#x_min = 16
dolxr = 0.8
dolyr = 0.8
#dolxr = 0.5
#dolyr = 0.5
#theta0 = 30
#phi0 = 30
theta0 = 0#40
phi0 = 0#-15
winid = 1
nbits = -1

Nxr = 16 #x_min
Nyr = 16 #1*x_min
if winid > 0:
    win = np.ones((Nxr,Nyr))
else:
    win = np.ones((1,1))

antPat = rect_array(Nxr,Nyr,dolxr,dolyr,theta0,phi0,winid,win,nbits)
antPatMax = np.max(antPat)
arrayGain = 10*np.log10(Nxr*Nyr)
bwx = 1.2/(dolxr*Nxr)
bwy = 1.2/(dolyr*Nyr)
bw = np.min([bwx,bwy])
antPatMaxCalc = 10*np.log10(4*np.pi/(bwx*bwy))
# need a factor of 2 before of both tx & rx?
#antPatMaxCalc = 10*np.log10(4*np.pi/(bwx*bwy*2))

print(f'Antenna Pattern Gain = {antPatMax:0.2f}(dB)')
print(f'Array Gain = {arrayGain:0.2f}(dB)')
print(f'Antenna Calculated Gain = {antPatMaxCalc:0.2f}(dB)')
print(f'Antenna Calculated 3dB-BW Az,El = {np.rad2deg(bwx):0.2f}, {np.rad2deg(bwy):0.2f}(deg)')