From da338fbcc089864643dd8f250bd44a1f84745fc1 Mon Sep 17 00:00:00 2001
From: David Robert Verelst <dave@dtu.dk>
Date: Tue, 13 Feb 2018 15:26:47 +0100
Subject: [PATCH] prepost: refactor envelope methods into utils/envelope.py

---
 wetb/prepost/Simulations.py | 229 ++--------------------------
 wetb/prepost/dlctemplate.py |   4 +-
 wetb/utils/envelope.py      | 293 ++++++++++++++++++++++++++++++++++++
 3 files changed, 307 insertions(+), 219 deletions(-)
 create mode 100644 wetb/utils/envelope.py

diff --git a/wetb/prepost/Simulations.py b/wetb/prepost/Simulations.py
index 0c15b8f3..fa0cc7fe 100755
--- a/wetb/prepost/Simulations.py
+++ b/wetb/prepost/Simulations.py
@@ -55,6 +55,7 @@ from wetb.prepost import windIO
 from wetb.prepost import prepost
 from wetb.dlc import high_level as dlc
 from wetb.prepost.GenerateHydro import hydro_input
+from wetb.utils.envelope import compute_envelope
 
 def load_pickled_file(source):
     FILE = open(source, 'rb')
@@ -3287,182 +3288,6 @@ class WeibullParameters(object):
         self.Vstep = 2.
         self.shape_k = 2.
 
-def compute_env_of_env(envelope, dlc_list, Nx=300, Nsectors=12, Ntheta=181):
-    """
-    The function computes load envelopes for given channels and a groups of
-    load cases starting from the envelopes computed for single simulations.
-    The output is the envelope of the envelopes of the single simulations.
-    This total envelope is projected on defined polar directions.
-
-    Parameters
-    ----------
-
-    envelope : dict, dictionaries of interpolated envelopes of a given
-                    channel (it's important that each entry of the dictonary
-                    contains a matrix of the same dimensions). The dictonary
-                    is organized by load case
-
-    dlc_list : list, list of load cases
-
-    Nx : int, default=300
-        Number of points for the envelope interpolation
-
-    Nsectors: int, default=12
-        Number of sectors in which the total envelope will be divided. The
-        default is every 30deg
-
-    Ntheta; int, default=181
-        Number of angles in which the envelope is interpolated in polar
-        coordinates.
-
-    Returns
-    -------
-
-    envelope : array (Nsectors x 6),
-        Total envelope projected on the number of angles defined in Nsectors.
-        The envelope is projected in Mx and My and the other cross-sectional
-        moments and forces are fetched accordingly (at the same time step where
-        the corresponding Mx and My are occuring)
-
-    """
-
-    # Group all the single DLCs
-    cloud = np.zeros(((Nx+1)*len(envelope),6))
-    for i in range(len(envelope)):
-        cloud[(Nx+1)*i:(Nx+1)*(i+1),:] = envelope[dlc_list[i]]
-    # Compute total Hull of all the envelopes
-    hull = scipy.spatial.ConvexHull(cloud[:,:2])
-    cc = np.append(cloud[hull.vertices,:2],
-                   cloud[hull.vertices[0],:2].reshape(1,2),axis=0)
-    # Interpolate full envelope
-    cc_x,cc_up,cc_low,cc_int= int_envelope(cc[:,0], cc[:,1], Nx=Nx)
-    # Project full envelope on given direction
-    cc_proj = proj_envelope(cc_x, cc_up, cc_low, cc_int, Nx, Nsectors, Ntheta)
-
-    env_proj = np.zeros([len(cc_proj),6])
-    env_proj[:,:2] = cc_proj
-
-    # Based on Mx and My, gather the remaining cross-sectional forces and
-    # moments
-    for ich in range(2, 6):
-        s0 = np.array(cloud[hull.vertices, ich]).reshape(-1, 1)
-        s1 = np.array(cloud[hull.vertices[0], ich]).reshape(-1, 1)
-        s0 = np.append(s0, s1, axis=0)
-        cc = np.append(cc, s0, axis=1)
-
-        _,_,_,extra_sensor = int_envelope(cc[:,0],cc[:,ich],Nx)
-        es = np.atleast_2d(np.array(extra_sensor[:,1])).T
-        cc_int = np.append(cc_int,es,axis=1)
-
-        for isec in range(Nsectors):
-            ids = (np.abs(cc_int[:,0]-cc_proj[isec,0])).argmin()
-            env_proj[isec,ich] = (cc_int[ids-1,ich]+cc_int[ids,ich]+\
-                                                    cc_int[ids+1,ich])/3
-
-    return env_proj
-
-def int_envelope(ch1,ch2,Nx):
-    # Function to interpolate envelopes and output arrays of same length
-
-    # Number of points is defined by Nx + 1, where the + 1 is needed to
-    # close the curve
-
-    upper = []
-    lower = []
-
-    indmax = np.argmax(ch1)
-    indmin = np.argmin(ch1)
-    if indmax > indmin:
-        lower = np.array([ch1[indmin:indmax+1],ch2[indmin:indmax+1]]).T
-        upper = np.concatenate((np.array([ch1[indmax:],ch2[indmax:]]).T,
-                                np.array([ch1[:indmin+1],ch2[:indmin+1]]).T),
-                                axis=0)
-    else:
-        upper = np.array([ch1[indmax:indmin+1],ch2[indmax:indmin+1]]).T
-        lower = np.concatenate((np.array([ch1[indmin:],ch2[indmin:]]).T,
-                                np.array([ch1[:indmax+1],ch2[:indmax+1]]).T),
-                                axis=0)
-
-
-    int_1 = np.linspace(min(upper[:,0].min(),lower[:,0].min()),
-                        max(upper[:,0].max(),lower[:,0].max()),Nx/2+1)
-    upper = np.flipud(upper)
-    int_2_up = np.interp(int_1,np.array(upper[:,0]),np.array(upper[:,1]))
-    int_2_low = np.interp(int_1,np.array(lower[:,0]),np.array(lower[:,1]))
-
-    int_env = np.concatenate((np.array([int_1[:-1],int_2_up[:-1]]).T,
-                              np.array([int_1[::-1],int_2_low[::-1]]).T),
-                              axis=0)
-
-    return int_1, int_2_up, int_2_low, int_env
-
-def proj_envelope(env_x, env_up, env_low, env, Nx, Nsectors, Ntheta):
-    # Function to project envelope on given angles
-
-    # Angles of projection is defined by Nsectors
-    # Projections are obtained in polar coordinates and outputted in
-    # cartesian
-
-    theta_int = np.linspace(-np.pi,np.pi,Ntheta)
-    sectors = np.linspace(-np.pi,np.pi,Nsectors+1)
-    proj = np.zeros([Nsectors,2])
-
-    R_up = np.sqrt(env_x**2+env_up**2)
-    theta_up = np.arctan2(env_up,env_x)
-
-    R_low = np.sqrt(env_x**2+env_low**2)
-    theta_low = np.arctan2(env_low,env_x)
-
-    R = np.concatenate((R_up,R_low))
-    theta = np.concatenate((theta_up,theta_low))
-    R = R[np.argsort(theta)]
-    theta = np.sort(theta)
-
-    R_int = np.interp(theta_int,theta,R,period=2*np.pi)
-
-    for i in range(Nsectors):
-        if sectors[i]>=-np.pi and sectors[i+1]<-np.pi/2:
-            indices = np.where(np.logical_and(theta_int >= sectors[i],
-                                              theta_int <= sectors[i+1]))
-            maxR = R_int[indices].max()
-            proj[i+1,0] = maxR*np.cos(sectors[i+1])
-            proj[i+1,1] = maxR*np.sin(sectors[i+1])
-        elif sectors[i]==-np.pi/2:
-            continue
-        elif sectors[i]>-np.pi/2 and sectors[i+1]<=0:
-            indices = np.where(np.logical_and(theta_int >= sectors[i],
-                                              theta_int <= sectors[i+1]))
-            maxR = R_int[indices].max()
-            proj[i,0] = maxR*np.cos(sectors[i])
-            proj[i,1] = maxR*np.sin(sectors[i])
-        elif sectors[i]>=0 and sectors[i+1]<np.pi/2:
-            indices = np.where(np.logical_and(theta_int >= sectors[i],
-                                              theta_int <= sectors[i+1]))
-            maxR = R_int[indices].max()
-            proj[i+1,0] = maxR*np.cos(sectors[i+1])
-            proj[i+1,1] = maxR*np.sin(sectors[i+1])
-        elif sectors[i]==np.pi/2:
-            continue
-        elif sectors[i]>np.pi/2 and sectors[i+1]<=np.pi:
-            indices = np.where(np.logical_and(theta_int >= sectors[i],
-                                              theta_int <= sectors[i+1]))
-            maxR = R_int[indices].max()
-            proj[i,0] = maxR*np.cos(sectors[i])
-            proj[i,1] = maxR*np.sin(sectors[i])
-
-    ind = np.where(sectors==0)
-    proj[ind,0] = env[:,0].max()
-
-    ind = np.where(sectors==np.pi/2)
-    proj[ind,1] = env[:,1].max()
-
-    ind = np.where(sectors==-np.pi)
-    proj[ind,0] = env[:,0].min()
-
-    ind = np.where(sectors==-np.pi/2)
-    proj[ind,1] = env[:,1].min()
-
-    return proj
 
 # FIXME: Cases has a memory leek somewhere, this whole thing needs to be
 # reconsidered and rely on a DataFrame instead of a dict!
@@ -5175,7 +5000,7 @@ class Cases(object):
 
         return result
 
-    def compute_envelope(self, sig, ch_list, int_env=False, Nx=300):
+    def compute_envelopes(self, ch_list, int_env=False, Nx=300):
         """
         The function computes load envelopes for given signals and a single
         load case. Starting from Mx and My moments, the other cross-sectional
@@ -5184,8 +5009,6 @@ class Cases(object):
         Parameters
         ----------
 
-        sig : list, time-series signal
-
         ch_list : list, list of channels for enevelope computation
 
         int_env : boolean, default=False
@@ -5206,48 +5029,20 @@ class Cases(object):
         """
 
         envelope= {}
-        for ch in ch_list:
-            chi0 = self.res.ch_dict[ch[0]]['chi']
-            chi1 = self.res.ch_dict[ch[1]]['chi']
-            s0 = np.array(sig[:, chi0]).reshape(-1, 1)
-            s1 = np.array(sig[:, chi1]).reshape(-1, 1)
+
+        for ch_names in ch_list:
+            ichans = []
+            for ch_name in ch_names:
+                ichans.append(self.res.ch_dict[ch_name]['chi'])
+            cloud = self.res.sig[:, ichans]
             # Compute a Convex Hull, the vertices number varies according to
             # the shape of the poligon
-            cloud =  np.append(s0, s1, axis=1)
-            hull = scipy.spatial.ConvexHull(cloud)
-            closed_contour = np.append(cloud[hull.vertices,:],
-                                       cloud[hull.vertices[0],:].reshape(1,2),
-                                       axis=0)
-
-            # Interpolate envelope for a given number of points
-            if int_env:
-                _,_,_,closed_contour_int = int_envelope(closed_contour[:,0],
-                                                        closed_contour[:,1],Nx)
-
-
-            # Based on Mx and My envelope, the other cross-sectional moments
-            # and forces components are identified and appended to the initial
-            # envelope
-            for ich in range(2, len(ch)):
-                chix = self.res.ch_dict[ch[ich]]['chi']
-                s0 = np.array(sig[hull.vertices, chix]).reshape(-1, 1)
-                s1 = np.array(sig[hull.vertices[0], chix]).reshape(-1, 1)
-                s0 = np.append(s0, s1, axis=0)
-                closed_contour = np.append(closed_contour, s0, axis=1)
-                if int_env:
-                    _,_,_,extra_sensor = int_envelope(closed_contour[:,0],
-                                                       closed_contour[:,ich],Nx)
-                    es = np.atleast_2d(np.array(extra_sensor[:,1])).T
-                    closed_contour_int = np.append(closed_contour_int,es,axis=1)
-
-            if int_env:
-                envelope[ch[0]] = closed_contour_int
-            else:
-                envelope[ch[0]] = closed_contour
+            vertices = compute_envelope(cloud, int_env=int_env, Nx=Nx)
+            envelope[ch_names[0]] = vertices
 
         return envelope
 
-    def envelope(self, silent=False, ch_list=[], append=''):
+    def envelopes(self, silent=False, ch_list=[], append=''):
         """
         Calculate envelopes and save them in a table.
 
@@ -5294,7 +5089,7 @@ class Cases(object):
 
             self.load_result_file(case)
 
-            envelope = self.compute_envelope(self.sig, ch_list)
+            envelope = self.compute_envelopes(ch_list, int_env=False, Nx=300)
 
             for ch_id in ch_list:
                 h5f.createTable(ctab, str(ch_id[0].replace('-', '_')),
diff --git a/wetb/prepost/dlctemplate.py b/wetb/prepost/dlctemplate.py
index 08e581d2..4d7d939b 100644
--- a/wetb/prepost/dlctemplate.py
+++ b/wetb/prepost/dlctemplate.py
@@ -432,7 +432,7 @@ def post_launch(sim_id, statistics=True, rem_failed=True, check_logs=True,
                                 'blade%i-blade%i-node-%3.3i-forcevec-x' % rpl,
                                 'blade%i-blade%i-node-%3.3i-forcevec-y' % rpl,
                                 'blade%i-blade%i-node-%3.3i-forcevec-z' % rpl])
-        cc.envelope(ch_list=ch_list, append='_blade')
+        cc.envelopes(ch_list=ch_list, append='_blade')
 
     if envelopeturbine:
         ch_list = [['tower-tower-node-001-momentvec-x',
@@ -447,7 +447,7 @@ def post_launch(sim_id, statistics=True, rem_failed=True, check_logs=True,
                    ['hub1-hub1-node-001-momentvec-x',
                    'hub1-hub1-node-001-momentvec-y',
                    'hub1-hub1-node-001-momentvec-z']]
-        cc.envelope(ch_list=ch_list, append='_turbine')
+        cc.envelopes(ch_list=ch_list, append='_turbine')
 
     if fatigue:
         # load the statistics in case they are missing
diff --git a/wetb/utils/envelope.py b/wetb/utils/envelope.py
new file mode 100644
index 00000000..85ec7f18
--- /dev/null
+++ b/wetb/utils/envelope.py
@@ -0,0 +1,293 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Tue Feb 13 12:58:25 2018
+
+@author: dave
+"""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import unicode_literals
+from __future__ import absolute_import
+from builtins import dict
+from io import open
+from builtins import zip
+from builtins import range
+from builtins import str
+from builtins import int
+from future import standard_library
+standard_library.install_aliases()
+from builtins import object
+
+import numpy as np
+import scipy
+
+
+def compute_env_of_env(envelope, dlc_list, Nx=300, Nsectors=12, Ntheta=181):
+    """
+    The function computes load envelopes for given channels and a groups of
+    load cases starting from the envelopes computed for single simulations.
+    The output is the envelope of the envelopes of the single simulations.
+    This total envelope is projected on defined polar directions.
+
+    Parameters
+    ----------
+
+    envelope : dict, dictionaries of interpolated envelopes of a given
+                    channel (it's important that each entry of the dictonary
+                    contains a matrix of the same dimensions). The dictonary
+                    is organized by load case
+
+    dlc_list : list, list of load cases
+
+    Nx : int, default=300
+        Number of points for the envelope interpolation
+
+    Nsectors: int, default=12
+        Number of sectors in which the total envelope will be divided. The
+        default is every 30deg
+
+    Ntheta; int, default=181
+        Number of angles in which the envelope is interpolated in polar
+        coordinates.
+
+    Returns
+    -------
+
+    envelope : array (Nsectors x 6),
+        Total envelope projected on the number of angles defined in Nsectors.
+        The envelope is projected in Mx and My and the other cross-sectional
+        moments and forces are fetched accordingly (at the same time step where
+        the corresponding Mx and My are occuring)
+
+    """
+
+    # Group all the single DLCs
+    cloud = np.zeros(((Nx+1)*len(envelope),6))
+    for i in range(len(envelope)):
+        cloud[(Nx+1)*i:(Nx+1)*(i+1),:] = envelope[dlc_list[i]]
+    # Compute total Hull of all the envelopes
+    hull = scipy.spatial.ConvexHull(cloud[:,:2])
+    cc = np.append(cloud[hull.vertices,:2],
+                   cloud[hull.vertices[0],:2].reshape(1,2),axis=0)
+    # Interpolate full envelope
+    cc_x,cc_up,cc_low,cc_int= int_envelope(cc[:,0], cc[:,1], Nx=Nx)
+    # Project full envelope on given direction
+    cc_proj = proj_envelope(cc_x, cc_up, cc_low, cc_int, Nx, Nsectors, Ntheta)
+
+    env_proj = np.zeros([len(cc_proj),6])
+    env_proj[:,:2] = cc_proj
+
+    # Based on Mx and My, gather the remaining cross-sectional forces and
+    # moments
+    for ich in range(2, 6):
+        s0 = np.array(cloud[hull.vertices, ich]).reshape(-1, 1)
+        s1 = np.array(cloud[hull.vertices[0], ich]).reshape(-1, 1)
+        s0 = np.append(s0, s1, axis=0)
+        cc = np.append(cc, s0, axis=1)
+
+        _,_,_,extra_sensor = int_envelope(cc[:,0],cc[:,ich],Nx)
+        es = np.atleast_2d(np.array(extra_sensor[:,1])).T
+        cc_int = np.append(cc_int,es,axis=1)
+
+        for isec in range(Nsectors):
+            ids = (np.abs(cc_int[:,0]-cc_proj[isec,0])).argmin()
+            env_proj[isec,ich] = (cc_int[ids-1,ich]+cc_int[ids,ich]+\
+                                                    cc_int[ids+1,ich])/3
+
+    return env_proj
+
+
+def int_envelope(ch1, ch2, Nx):
+    """
+    Function to interpolate envelopes and output arrays of same length
+
+    Number of points is defined by Nx + 1, where the + 1 is needed to
+    close the curve
+    """
+
+    upper = []
+    lower = []
+
+    indmax = np.argmax(ch1)
+    indmin = np.argmin(ch1)
+    if indmax > indmin:
+        lower = np.array([ch1[indmin:indmax+1],ch2[indmin:indmax+1]]).T
+        upper = np.concatenate((np.array([ch1[indmax:],ch2[indmax:]]).T,
+                                np.array([ch1[:indmin+1],ch2[:indmin+1]]).T),
+                                axis=0)
+    else:
+        upper = np.array([ch1[indmax:indmin+1],ch2[indmax:indmin+1]]).T
+        lower = np.concatenate((np.array([ch1[indmin:],ch2[indmin:]]).T,
+                                np.array([ch1[:indmax+1],ch2[:indmax+1]]).T),
+                                axis=0)
+
+
+    int_1 = np.linspace(min(upper[:,0].min(),lower[:,0].min()),
+                        max(upper[:,0].max(),lower[:,0].max()),Nx/2+1)
+    upper = np.flipud(upper)
+    int_2_up = np.interp(int_1,np.array(upper[:,0]),np.array(upper[:,1]))
+    int_2_low = np.interp(int_1,np.array(lower[:,0]),np.array(lower[:,1]))
+
+    int_env = np.concatenate((np.array([int_1[:-1],int_2_up[:-1]]).T,
+                              np.array([int_1[::-1],int_2_low[::-1]]).T),
+                              axis=0)
+
+    return int_1, int_2_up, int_2_low, int_env
+
+
+def proj_envelope(env_x, env_up, env_low, env, Nx, Nsectors, Ntheta):
+    """
+    Function to project envelope on given angles
+
+    Angles of projection is defined by Nsectors
+    Projections are obtained in polar coordinates and outputted in
+    cartesian
+    """
+
+    theta_int = np.linspace(-np.pi,np.pi,Ntheta)
+    sectors = np.linspace(-np.pi,np.pi,Nsectors+1)
+    proj = np.zeros([Nsectors,2])
+
+    R_up = np.sqrt(env_x**2+env_up**2)
+    theta_up = np.arctan2(env_up,env_x)
+
+    R_low = np.sqrt(env_x**2+env_low**2)
+    theta_low = np.arctan2(env_low,env_x)
+
+    R = np.concatenate((R_up,R_low))
+    theta = np.concatenate((theta_up,theta_low))
+    R = R[np.argsort(theta)]
+    theta = np.sort(theta)
+
+    R_int = np.interp(theta_int,theta,R,period=2*np.pi)
+
+    for i in range(Nsectors):
+        if sectors[i]>=-np.pi and sectors[i+1]<-np.pi/2:
+            indices = np.where(np.logical_and(theta_int >= sectors[i],
+                                              theta_int <= sectors[i+1]))
+            maxR = R_int[indices].max()
+            proj[i+1,0] = maxR*np.cos(sectors[i+1])
+            proj[i+1,1] = maxR*np.sin(sectors[i+1])
+        elif sectors[i]==-np.pi/2:
+            continue
+        elif sectors[i]>-np.pi/2 and sectors[i+1]<=0:
+            indices = np.where(np.logical_and(theta_int >= sectors[i],
+                                              theta_int <= sectors[i+1]))
+            maxR = R_int[indices].max()
+            proj[i,0] = maxR*np.cos(sectors[i])
+            proj[i,1] = maxR*np.sin(sectors[i])
+        elif sectors[i]>=0 and sectors[i+1]<np.pi/2:
+            indices = np.where(np.logical_and(theta_int >= sectors[i],
+                                              theta_int <= sectors[i+1]))
+            maxR = R_int[indices].max()
+            proj[i+1,0] = maxR*np.cos(sectors[i+1])
+            proj[i+1,1] = maxR*np.sin(sectors[i+1])
+        elif sectors[i]==np.pi/2:
+            continue
+        elif sectors[i]>np.pi/2 and sectors[i+1]<=np.pi:
+            indices = np.where(np.logical_and(theta_int >= sectors[i],
+                                              theta_int <= sectors[i+1]))
+            maxR = R_int[indices].max()
+            proj[i,0] = maxR*np.cos(sectors[i])
+            proj[i,1] = maxR*np.sin(sectors[i])
+
+    ind = np.where(sectors==0)
+    proj[ind,0] = env[:,0].max()
+
+    ind = np.where(sectors==np.pi/2)
+    proj[ind,1] = env[:,1].max()
+
+    ind = np.where(sectors==-np.pi)
+    proj[ind,0] = env[:,0].min()
+
+    ind = np.where(sectors==-np.pi/2)
+    proj[ind,1] = env[:,1].min()
+
+    return proj
+
+
+def closed_contour(cloud):
+    """Return indices of the vertices of the closed convex contour that
+    contain all points of the (x,y) cloud of pairs.
+
+
+    Parameters
+    ----------
+
+    cloud : ndarray of floats, shape (npoints, ndim)
+        Coordinates of points to construct a convex hull from. Ndim should be
+        at least 2 or higher.
+
+    cloud_extra : np.ndarray(npoints, nr_extra_channels)
+
+
+    Returns
+    -------
+
+    ivertices : ndarray(nvertices)
+
+    vertices : ndarray(nvertices, 2+nr_extra_channels)
+
+    """
+    if not cloud.shape[1] >= 2:
+        raise IndexError('Cloud dimension should be 2 or greater')
+
+    hull = scipy.spatial.ConvexHull(cloud[:,0:2])
+
+    # indices to the vertices containing the cloud of the first 2 dimensions
+    ivertices = np.ndarray((len(hull.vertices)+1,), dtype=np.int32)
+    ivertices[0:-1] = hull.vertices
+    ivertices[-1] = hull.vertices[0]
+    # actual vertices of all dimensions in the cloud, based on the first two
+    # dimensions
+    vertices = np.ndarray((len(ivertices), cloud.shape[1]))
+    vertices[0:-1,:] = cloud[ivertices[0:-1],:]
+    vertices[-1,:] = cloud[ivertices[-1],:]
+
+    return vertices, ivertices
+
+
+def compute_envelope(cloud, int_env=False, Nx=300):
+    """
+    The function computes load envelopes for given signals and a single
+    load case. Starting from Mx and My moments, the other cross-sectional
+    forces are identified.
+
+    Parameters
+    ----------
+
+    x, y : np.ndarray(n)
+        2 components of the same time signal, e.g x and y.
+
+    int_env : boolean, default=False
+        If the logic parameter is True, the function will interpolate the
+        envelope on a given number of points
+
+    Nx : int, default=300
+        Number of points for the envelope interpolation
+
+    Returns
+    -------
+
+    envelope : dictionary,
+        The dictionary has entries refered to the channels selected.
+        Inside the dictonary under each entry there is a matrix with 6
+        columns, each for the sectional forces and moments
+
+    """
+
+    vertices, ivertices = closed_contour(cloud)
+
+    # interpolate to a fixed location of equally spaced vertices
+    vert_int = np.ndarray(vertices.shape)
+    if int_env:
+        _,_,_,vert_int[:,0:2] = int_envelope(vertices[:,0], vertices[:,1], Nx)
+        for i in range(2, cloud.shape[1]):
+            _,_,_,extra = int_envelope(vertices[:,0], vertices[:,i], Nx)
+            vert_int[:,i] = extra[:,1]
+        vertices = vert_int
+
+    return vertices
+
-- 
GitLab