From 2b5bd1e38a746afbfe325dd24ac1e52b98f2cf36 Mon Sep 17 00:00:00 2001 From: jaideeppyne Date: Wed, 16 Sep 2026 10:41:46 +0530 Subject: [PATCH] fix(tdigest): preserve stored min and max when merging Merging a digest whose extreme centroids have weight greater than one re-derived min and max from those centroid means. Copy the stored extrema from the other digest first so rank and quantile tail interpolation stay correct, especially for deserialized images. --- tdigest/double.go | 4 ++++ tdigest/double_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/tdigest/double.go b/tdigest/double.go index d2063b2..10d7a69 100644 --- a/tdigest/double.go +++ b/tdigest/double.go @@ -204,6 +204,10 @@ func (d *Double) Merge(other *Double) error { return ErrEmpty } + // Extreme centroid means are not the true min/max when those centroids have weight > 1. + d.min = min(d.min, other.min) + d.max = max(d.max, other.max) + tmp := make([]doublePrecisionCentroid, 0, len(d.buffer)+len(d.centroids)+len(other.buffer)+len(other.centroids)) for _, v := range d.buffer { tmp = append(tmp, doublePrecisionCentroid{mean: v, weight: 1}) diff --git a/tdigest/double_test.go b/tdigest/double_test.go index b1077ac..d3129ee 100644 --- a/tdigest/double_test.go +++ b/tdigest/double_test.go @@ -246,6 +246,35 @@ func TestDouble_Merge(t *testing.T) { assert.Equal(t, 1.0, rank) }) + t.Run("Merge preserves deserialized min max with weighted tails", func(t *testing.T) { + other, err := newDoubleFromInternalStates(false, DefaultK, -1, 21, + []doublePrecisionCentroid{{mean: 0, weight: 4}, {mean: 10, weight: 4}, {mean: 20, weight: 4}}, + 12, nil) + assert.NoError(t, err) + + empty, err := NewDouble(DefaultK) + assert.NoError(t, err) + assert.NoError(t, empty.Merge(other)) + minVal, err := empty.MinValue() + assert.NoError(t, err) + assert.Equal(t, -1.0, minVal) + maxVal, err := empty.MaxValue() + assert.NoError(t, err) + assert.Equal(t, 21.0, maxVal) + + left, err := newDoubleFromInternalStates(false, DefaultK, 5, 15, + []doublePrecisionCentroid{{mean: 6, weight: 4}, {mean: 10, weight: 4}, {mean: 14, weight: 4}}, + 12, nil) + assert.NoError(t, err) + assert.NoError(t, left.Merge(other)) + minVal, err = left.MinValue() + assert.NoError(t, err) + assert.Equal(t, -1.0, minVal) + maxVal, err = left.MaxValue() + assert.NoError(t, err) + assert.Equal(t, 21.0, maxVal) + }) + t.Run("Merge Large", func(t *testing.T) { sk1, err := NewDouble(DefaultK) assert.NoError(t, err)