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
|