 9bdcbe0447
			
		
	
	9bdcbe0447
	
	
	
		
			
			Major integrations and fixes: - Added BACKBEAT SDK integration for P2P operation timing - Implemented beat-aware status tracking for distributed operations - Added Docker secrets support for secure license management - Resolved KACHING license validation via HTTPS/TLS - Updated docker-compose configuration for clean stack deployment - Disabled rollback policies to prevent deployment failures - Added license credential storage (CHORUS-DEV-MULTI-001) Technical improvements: - BACKBEAT P2P operation tracking with phase management - Enhanced configuration system with file-based secrets - Improved error handling for license validation - Clean separation of KACHING and CHORUS deployment stacks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			509 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			509 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2023 Google Inc. All Rights Reserved.
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS-IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| //
 | |
| 
 | |
| package s2
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"math"
 | |
| )
 | |
| 
 | |
| // This library provides code to compute vertex alignments between Polylines.
 | |
| //
 | |
| // A vertex "alignment" or "warp" between two polylines is a matching between
 | |
| // pairs of their vertices. Users can imagine pairing each vertex from
 | |
| // Polyline `a` with at least one other vertex in Polyline `b`. The "cost"
 | |
| // of an arbitrary alignment is defined as the summed value of the squared
 | |
| // chordal distance between each pair of points in the warp path. An "optimal
 | |
| // alignment" for a pair of polylines is defined as the alignment with least
 | |
| // cost. Note: optimal alignments are not necessarily unique. The standard way
 | |
| // of computing an optimal alignment between two sequences is the use of the
 | |
| // `Dynamic Timewarp` algorithm.
 | |
| //
 | |
| // We provide three methods for computing (via Dynamic Timewarp) the optimal
 | |
| // alignment between two Polylines. These methods are performance-sensitive,
 | |
| // and have been reasonably optimized for space- and time- usage. On modern
 | |
| // hardware, it is possible to compute exact alignments between 4096x4096
 | |
| // element polylines in ~70ms, and approximate alignments much more quickly.
 | |
| //
 | |
| // The results of an alignment operation are captured in a VertexAlignment
 | |
| // object. In particular, a VertexAlignment keeps track of the total cost of
 | |
| // alignment, as well as the warp path (a sequence of pairs of indices into each
 | |
| // polyline whose vertices are linked together in the optimal alignment)
 | |
| //
 | |
| // For a worked example, consider the polylines
 | |
| //
 | |
| // a = [(1, 0), (5, 0), (6, 0), (9, 0)] and
 | |
| // b = [(2, 0), (7, 0), (8, 0)].
 | |
| //
 | |
| // The "cost matrix" between these two polylines (using chordal
 | |
| // distance, .Norm(), as our distance function) looks like this:
 | |
| //
 | |
| //        (2, 0)  (7, 0)  (8, 0)
 | |
| // (1, 0)     1       6       7
 | |
| // (5, 0)     3       2       3
 | |
| // (6, 0)     4       1       2
 | |
| // (9, 0)     7       2       1
 | |
| //
 | |
| // The Dynamic Timewarp DP table for this cost matrix has cells defined by
 | |
| //
 | |
| // table[i][j] = cost(i,j) + min(table[i-1][j-1], table[i][j-1], table[i-1, j])
 | |
| //
 | |
| //        (2, 0)  (7, 0)  (8, 0)
 | |
| // (1, 0)     1       7      14
 | |
| // (5, 0)     4       3       7
 | |
| // (6, 0)     8       4       6
 | |
| // (9, 0)    15       6       5
 | |
| //
 | |
| // Starting at the bottom right corner of the DP table, we can work our way
 | |
| // backwards to the upper left corner  to recover the reverse of the warp path:
 | |
| // (3, 2) -> (2, 1) -> (1, 1) -> (0, 0). The VertexAlignment produced containing
 | |
| // this has alignment_cost = 7 and warp_path = {(0, 0), (1, 1), (2, 1), (3, 2)}.
 | |
| //
 | |
| // We also provide methods for performing alignment of multiple sequences. These
 | |
| // methods return a single, representative polyline from a non-empty collection
 | |
| // of polylines, for various definitions of "representative."
 | |
| //
 | |
| // GetMedoidPolyline() returns a new polyline (point-for-point-equal to some
 | |
| // existing polyline from the collection) that minimizes the summed vertex
 | |
| // alignment cost to all other polylines in the collection.
 | |
| //
 | |
| // GetConsensusPolyline() returns a new polyline (unlikely to be present in the
 | |
| // input collection) that represents a "weighted consensus" polyline. This
 | |
| // polyline is constructed iteratively using the Dynamic Timewarp Barycenter
 | |
| // Averaging algorithm of F. Petitjean, A. Ketterlin, and P. Gancarski, which
 | |
| // can be found here:
 | |
| // https://pdfs.semanticscholar.org/a596/8ca9488199291ffe5473643142862293d69d.pdf
 | |
| 
 | |
| // A columnStride is a [start, end) range of columns in a search window.
 | |
| // It enables us to lazily fill up our costTable structures by providing bounds
 | |
| // checked access for reads. We also use them to keep track of structured,
 | |
| // sparse window matrices by tracking start and end columns for each row.
 | |
| type columnStride struct {
 | |
| 	start int
 | |
| 	end   int
 | |
| }
 | |
| 
 | |
| // InRange reports if the given index is in range of this stride.
 | |
| func (c columnStride) InRange(index int) bool {
 | |
| 	return c.start <= index && index < c.end
 | |
| }
 | |
| 
 | |
| // allColumnStride returns a columnStride where inRange evaluates to `true` for all
 | |
| // non-negative inputs less than math.MaxInt.
 | |
| func allColumnStride() columnStride {
 | |
| 	return columnStride{-1, math.MaxInt}
 | |
| }
 | |
| 
 | |
| // A Window is a sparse binary matrix with specific structural constraints
 | |
| // on allowed element configurations. It is used in this library to represent
 | |
| // "search windows" for windowed dynamic timewarping.
 | |
| //
 | |
| // Valid Windows require the following structural conditions to hold:
 | |
| //  1. All rows must consist of a single contiguous stride of `true` values.
 | |
| //  2. All strides are greater than zero length (i.e. no empty rows).
 | |
| //  3. The index of the first `true` column in a row must be at least as
 | |
| //     large as the index of the first `true` column in the previous row.
 | |
| //  4. The index of the last `true` column in a row must be at least as large
 | |
| //     as the index of the last `true` column in the previous row.
 | |
| //  5. strides[0].start = 0 (the first cell is always filled).
 | |
| //  6. strides[n_rows-1].end = n_cols (the last cell is filled).
 | |
| //
 | |
| // Example valid strided_masks (* = filled, . = unfilled)
 | |
| //
 | |
| //	  0 1 2 3 4 5
 | |
| //	0 * * * . . .
 | |
| //	1 . * * * . .
 | |
| //	2 . * * * . .
 | |
| //	3 . . * * * *
 | |
| //	4 . . * * * *
 | |
| //
 | |
| //	  0 1 2 3 4 5
 | |
| //	0 * * * * . .
 | |
| //	1 . * * * * .
 | |
| //	2 . . * * * .
 | |
| //	3 . . . . * *
 | |
| //	4 . . . . . *
 | |
| //
 | |
| //	  0 1 2 3 4 5
 | |
| //	0 * * . . . .
 | |
| //	1 . * . . . .
 | |
| //	2 . . * * * .
 | |
| //	3 . . . . . *
 | |
| //	4 . . . . . *
 | |
| //
 | |
| // Example invalid strided_masks:
 | |
| //
 | |
| //	0 1 2 3 4 5
 | |
| //
 | |
| // 0 * * * . * * <-- more than one continuous run
 | |
| // 1 . * * * . .
 | |
| // 2 . * * * . .
 | |
| // 3 . . * * * *
 | |
| // 4 . . * * * *
 | |
| //
 | |
| //	0 1 2 3 4 5
 | |
| //
 | |
| // 0 * * * . . .
 | |
| // 1 . * * * . .
 | |
| // 2 . * * * . .
 | |
| // 3 * * * * * * <-- start index not monotonically increasing
 | |
| // 4 . . * * * *
 | |
| //
 | |
| //	0 1 2 3 4 5
 | |
| //
 | |
| // 0 * * * . . .
 | |
| // 1 . * * * * .
 | |
| // 2 . * * * . . <-- end index not monotonically increasing
 | |
| // 3 . . * * * *
 | |
| // 4 . . * * * *
 | |
| //
 | |
| //	0 1 2 3 4 5
 | |
| //
 | |
| // 0 . * . . . . <-- does not fill upper left corner
 | |
| // 1 . * . . . .
 | |
| // 2 . * . . . .
 | |
| // 3 . * * * . .
 | |
| // 4 . . * * * *
 | |
| type window struct {
 | |
| 	rows    int
 | |
| 	cols    int
 | |
| 	strides []columnStride
 | |
| }
 | |
| 
 | |
| // windowFromStrides creates a window from the given columnStrides.
 | |
| func windowFromStrides(strides []columnStride) *window {
 | |
| 	return &window{
 | |
| 		rows:    len(strides),
 | |
| 		cols:    strides[len(strides)-1].end,
 | |
| 		strides: strides,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // TODO(rsned): Add windowFromWarpPath
 | |
| 
 | |
| // isValid reports if this windows data represents a valid window.
 | |
| //
 | |
| // Valid Windows require the following structural conditions to hold:
 | |
| //  1. All rows must consist of a single contiguous stride of `true` values.
 | |
| //  2. All strides are greater than zero length (i.e. no empty rows).
 | |
| //  3. The index of the first `true` column in a row must be at least as
 | |
| //     large as the index of the first `true` column in the previous row.
 | |
| //  4. The index of the last `true` column in a row must be at least as large
 | |
| //     as the index of the last `true` column in the previous row.
 | |
| //  5. strides[0].start = 0 (the first cell is always filled).
 | |
| //  6. strides[n_rows-1].end = n_cols (the last cell is filled).
 | |
| func (w *window) isValid() bool {
 | |
| 	if w.rows <= 0 || w.cols <= 0 || len(w.strides) == 0 ||
 | |
| 		w.strides[0].start != 0 || w.strides[len(w.strides)-1].end != w.cols {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	var prev = columnStride{-1, -1}
 | |
| 	for _, curr := range w.strides {
 | |
| 		if curr.end <= curr.start || curr.start < prev.start ||
 | |
| 			curr.end < prev.end {
 | |
| 			return false
 | |
| 		}
 | |
| 		prev = curr
 | |
| 	}
 | |
| 	return true
 | |
| 
 | |
| }
 | |
| 
 | |
| func (w *window) columnStride(row int) columnStride {
 | |
| 	return w.strides[row]
 | |
| }
 | |
| 
 | |
| func (w *window) checkedColumnStride(row int) columnStride {
 | |
| 	if row < 0 {
 | |
| 		return allColumnStride()
 | |
| 	}
 | |
| 	return w.strides[row]
 | |
| }
 | |
| 
 | |
| // upsample returns a new, larger window that is an upscaled version of this window.
 | |
| //
 | |
| // Used by ApproximateAlignment window expansion step.
 | |
| func (w *window) upsample(newRows, newCols int) *window {
 | |
| 	// TODO(rsned): What to do if the upsample is actually a downsample.
 | |
| 	// C++ has this as a debug CHECK.
 | |
| 	rowScale := float64(newRows) / float64(w.rows)
 | |
| 	colScale := float64(newCols) / float64(w.cols)
 | |
| 	newStrides := make([]columnStride, newRows)
 | |
| 	var fromStride columnStride
 | |
| 	for row := 0; row < newRows; row++ {
 | |
| 		fromStride = w.strides[int((float64(row)+0.5)/rowScale)]
 | |
| 		newStrides[row] = columnStride{
 | |
| 			start: int(colScale*float64(fromStride.start) + 0.5),
 | |
| 			end:   int(colScale*float64(fromStride.end) + 0.5),
 | |
| 		}
 | |
| 	}
 | |
| 	return windowFromStrides(newStrides)
 | |
| }
 | |
| 
 | |
| // dilate returns a new, equal-size window by dilating this window with a square
 | |
| // structuring element with half-length `radius`. Radius = 1 corresponds to
 | |
| // a 3x3 square morphological dilation.
 | |
| //
 | |
| // Used by ApproximateAlignment window expansion step.
 | |
| func (w *window) dilate(radius int) *window {
 | |
| 	// This code takes advantage of the fact that the dilation window is square to
 | |
| 	// ensure that we can compute the stride for each output row in constant time.
 | |
| 	// TODO (mrdmnd): a potential optimization might be to combine this method and
 | |
| 	// the Upsample method into a single "Expand" method. For the sake of
 | |
| 	// testing, I haven't done that here, but I think it would be fairly
 | |
| 	// straightforward to do so. This method generally isn't very expensive so it
 | |
| 	// feels unnecessary to combine them.
 | |
| 
 | |
| 	newStrides := make([]columnStride, w.rows)
 | |
| 	for row := 0; row < w.rows; row++ {
 | |
| 		prevRow := maxInt(0, row-radius)
 | |
| 		nextRow := minInt(row+radius, w.rows-1)
 | |
| 		newStrides[row] = columnStride{
 | |
| 			start: maxInt(0, w.strides[prevRow].start-radius),
 | |
| 			end:   minInt(w.strides[nextRow].end+radius, w.cols),
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return windowFromStrides(newStrides)
 | |
| }
 | |
| 
 | |
| // debugString returns a string representation of this window.
 | |
| func (w *window) debugString() string {
 | |
| 	var buf bytes.Buffer
 | |
| 	for _, row := range w.strides {
 | |
| 		for col := 0; col < w.cols; col++ {
 | |
| 			if row.InRange(col) {
 | |
| 				buf.WriteString(" *")
 | |
| 			} else {
 | |
| 				buf.WriteString(" .")
 | |
| 			}
 | |
| 		}
 | |
| 		buf.WriteString("\n")
 | |
| 	}
 | |
| 	return buf.String()
 | |
| }
 | |
| 
 | |
| // halfResolution reduces the number of vertices of polyline p by selecting every other
 | |
| // vertex for inclusion in a new polyline. Specifically, we take even-index
 | |
| // vertices [0, 2, 4,...]. For an even-length polyline, the last vertex is not
 | |
| // selected. For an odd-length polyline, the last vertex is selected.
 | |
| // Constructs and returns a new Polyline in linear time.
 | |
| func halfResolution(p *Polyline) *Polyline {
 | |
| 	var p2 Polyline
 | |
| 	for i := 0; i < len(*p); i += 2 {
 | |
| 		p2 = append(p2, (*p)[i])
 | |
| 	}
 | |
| 
 | |
| 	return &p2
 | |
| }
 | |
| 
 | |
| // warpPath represents a pairing between vertex
 | |
| // a.vertex(i) and vertex b.vertex(j) in the optimal alignment.
 | |
| // The warpPath is defined in forward order, such that the result of
 | |
| // aligning polylines `a` and `b` is always a warpPath with warpPath[0] = {0,0}
 | |
| // and warp_path[n] = {len(a) - 1, len(b)- 1}
 | |
| //
 | |
| // Note that this DOES NOT define an alignment from a point sequence to an
 | |
| // edge sequence. That functionality may come at a later date.
 | |
| type warpPath []warpPair
 | |
| type warpPair struct{ a, b int }
 | |
| 
 | |
| type vertexAlignment struct {
 | |
| 	// alignmentCost represents the sum of the squared chordal distances
 | |
| 	// between each pair of vertices in the warp path. Specifically,
 | |
| 	// cost = sum_{(i, j) \in path} (a.vertex(i) - b.vertex(j)).Norm();
 | |
| 	// This means that the units of alignment_cost are distance. This is
 | |
| 	// an optimization to avoid the (expensive) atan computation of the true
 | |
| 	// spherical angular distance between the points. All we need to compute
 | |
| 	// vertex alignment is a metric that satisfies the triangle inequality, and
 | |
| 	// chordal distance works as well as spherical s1.Angle distance for
 | |
| 	// this purpose.
 | |
| 	alignmentCost float64
 | |
| 	warpPath      warpPath
 | |
| }
 | |
| 
 | |
| type costTable [][]float64
 | |
| 
 | |
| func newCostTable(rows, cols int) costTable {
 | |
| 	c := make([][]float64, rows)
 | |
| 	for i := 0; i < rows; i++ {
 | |
| 		c[i] = make([]float64, cols)
 | |
| 	}
 | |
| 	return c
 | |
| }
 | |
| 
 | |
| func (c costTable) String() string {
 | |
| 	var buf bytes.Buffer
 | |
| 	for i, row := range c {
 | |
| 		buf.WriteString(fmt.Sprintf("%2d: [", i))
 | |
| 		for _, col := range row {
 | |
| 			buf.WriteString(fmt.Sprintf("%0.3f, ", col))
 | |
| 		}
 | |
| 		buf.WriteString("]\n")
 | |
| 	}
 | |
| 	return buf.String()
 | |
| }
 | |
| 
 | |
| func (c costTable) boundsCheckedTableCost(row, col int, stride columnStride) float64 {
 | |
| 	if row < 0 && col < 0 {
 | |
| 		return 0.0
 | |
| 	} else if row < 0 || col < 0 || !stride.InRange(col) {
 | |
| 		return math.MaxFloat64
 | |
| 	} else {
 | |
| 		return c[row][col]
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (c costTable) cost() float64 {
 | |
| 	r := len(c) - 1
 | |
| 	return c[r][len(c[r])-1]
 | |
| }
 | |
| 
 | |
| // ExactVertexAlignmentCost takes two non-empty polylines as input, and
 | |
| // returns the *cost* of their optimal alignment. A standard, traditional
 | |
| // dynamic timewarp algorithm can output both a warp path and a cost, but
 | |
| // requires quadratic space to reconstruct the path by walking back through the
 | |
| // Dynamic Programming cost table. If all you really need is the warp cost (i.e.
 | |
| // you're inducing a similarity metric between Polylines, or something
 | |
| // equivalent), you can overwrite the DP table and use constant space -
 | |
| // O(max(A,B)). This method provides that space-efficiency optimization.
 | |
| func ExactVertexAlignmentCost(a, b *Polyline) float64 {
 | |
| 	aN := len(*a)
 | |
| 	bN := len(*b)
 | |
| 	cost := make([]float64, bN)
 | |
| 	for i := 0; i < bN; i++ {
 | |
| 		cost[i] = math.MaxFloat64
 | |
| 	}
 | |
| 	leftDiagMinCost := 0.0
 | |
| 	for row := 0; row < aN; row++ {
 | |
| 		for col := 0; col < bN; col++ {
 | |
| 			upCost := cost[col]
 | |
| 			cost[col] = math.Min(leftDiagMinCost, upCost) +
 | |
| 				(*a)[row].Sub((*b)[col].Vector).Norm()
 | |
| 			leftDiagMinCost = math.Min(cost[col], upCost)
 | |
| 		}
 | |
| 		leftDiagMinCost = math.MaxFloat64
 | |
| 	}
 | |
| 	return cost[len(cost)-1]
 | |
| }
 | |
| 
 | |
| // ExactVertexAlignment takes two non-empty polylines as input, and returns
 | |
| // the VertexAlignment corresponding to the optimal alignment between them. This
 | |
| // method is quadratic O(A*B) in both space and time complexity.
 | |
| func ExactVertexAlignment(a, b *Polyline) *vertexAlignment {
 | |
| 	aN := len(*a)
 | |
| 	bN := len(*b)
 | |
| 	strides := make([]columnStride, aN)
 | |
| 	for i := 0; i < aN; i++ {
 | |
| 		strides[i] = columnStride{start: 0, end: bN}
 | |
| 	}
 | |
| 	w := windowFromStrides(strides)
 | |
| 
 | |
| 	return dynamicTimewarp(a, b, w)
 | |
| }
 | |
| 
 | |
| // Perform dynamic timewarping by filling in the DP table on cells that are
 | |
| // inside our search window. For an exact (all-squares) evaluation, this
 | |
| // incurs bounds checking overhead - we don't need to ensure that we're inside
 | |
| // the appropriate cells in the window, because it's guaranteed. Structuring
 | |
| // the program to reuse code for both the EXACT and WINDOWED cases by
 | |
| // abstracting EXACT as a window with full-covering strides is done for
 | |
| // maintainability reasons. One potential optimization here might be to overload
 | |
| // this function to skip bounds checking when the window is full.
 | |
| //
 | |
| // As a note of general interest, the Dynamic Timewarp algorithm as stated here
 | |
| // prefers shorter warp paths, when two warp paths might be equally costly. This
 | |
| // is because it favors progressing in the sequences simultaneously due to the
 | |
| // equal weighting of a diagonal step in the cost table with a horizontal or
 | |
| // vertical step. This may be counterintuitive, but represents the standard
 | |
| // implementation of this algorithm. TODO(user) - future implementations could
 | |
| // allow weights on the lookup costs to mitigate this.
 | |
| //
 | |
| // This is the hottest routine in the whole package, please be careful to
 | |
| // profile any future changes made here.
 | |
| //
 | |
| // This method takes time proportional to the number of cells in the window,
 | |
| // which can range from O(max(a, b)) cells (best) to O(a*b) cells (worst)
 | |
| func dynamicTimewarp(a, b *Polyline, w *window) *vertexAlignment {
 | |
| 	rows := len(*a)
 | |
| 	cols := len(*b)
 | |
| 	costs := newCostTable(rows, cols)
 | |
| 
 | |
| 	var curr columnStride
 | |
| 	prev := allColumnStride()
 | |
| 
 | |
| 	for row := 0; row < rows; row++ {
 | |
| 		curr = w.columnStride(row)
 | |
| 		for col := curr.start; col < curr.end; col++ {
 | |
| 			// The total cost up to (row,col) is the minimum of the cost up, down,
 | |
| 			// left and the distance between the points in row and col. We use
 | |
| 			// the distance between the points, as we are trying to minimize the
 | |
| 			// distance between the two polylines.
 | |
| 			dCost := costs.boundsCheckedTableCost(row-1, col-1, prev)
 | |
| 			uCost := costs.boundsCheckedTableCost(row-1, col-0, prev)
 | |
| 			lCost := costs.boundsCheckedTableCost(row-0, col-1, curr)
 | |
| 
 | |
| 			costs[row][col] = minFloat64(dCost, uCost, lCost) +
 | |
| 				(*a)[row].Sub((*b)[col].Vector).Norm()
 | |
| 		}
 | |
| 		prev = curr
 | |
| 	}
 | |
| 
 | |
| 	// Now we walk back through the cost table and build up the warp path.
 | |
| 	// Somewhat surprisingly, it is faster to recover the path this way than it
 | |
| 	// is to save the comparisons from the computation we *already did* to get the
 | |
| 	// direction we came from. The author speculates that this behavior is
 | |
| 	// assignment-cost-related: to persist direction, we have to do extra
 | |
| 	// stores/loads of "directional" information, and the extra assignment cost
 | |
| 	// this incurs is larger than the cost to simply redo the comparisons.
 | |
| 	// It's probably worth revisiting this assumption in the future.
 | |
| 	// As it turns out, the following code ends up effectively free.
 | |
| 	warpPath := make([]warpPair, 0, maxInt(rows, cols))
 | |
| 	row := rows - 1
 | |
| 	col := cols - 1
 | |
| 	curr = w.checkedColumnStride(row)
 | |
| 	prev = w.checkedColumnStride(row - 1)
 | |
| 	for row >= 0 && col >= 0 {
 | |
| 		warpPath = append(warpPath, warpPair{row, col})
 | |
| 		dCost := costs.boundsCheckedTableCost(row-1, col-1, prev)
 | |
| 		uCost := costs.boundsCheckedTableCost(row-1, col-0, prev)
 | |
| 		lCost := costs.boundsCheckedTableCost(row-0, col-1, curr)
 | |
| 
 | |
| 		if dCost <= uCost && dCost <= lCost {
 | |
| 			row -= 1
 | |
| 			col -= 1
 | |
| 			curr = w.checkedColumnStride(row)
 | |
| 			prev = w.checkedColumnStride(row - 1)
 | |
| 		} else if uCost <= lCost {
 | |
| 			row -= 1
 | |
| 			curr = w.checkedColumnStride(row)
 | |
| 			prev = w.checkedColumnStride(row - 1)
 | |
| 		} else {
 | |
| 			col -= 1
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// TODO(rsned): warpPath.reverse
 | |
| 	return &vertexAlignment{alignmentCost: costs.cost(), warpPath: warpPath}
 | |
| }
 | |
| 
 | |
| // TODO(rsned): Differences from C++
 | |
| // ApproxVertexAlignment/Cost
 | |
| // MedoidPolyline / Options
 | |
| // ConsensusPolyline / Options
 |