Skip to content

Commit 3819941

Browse files
committed
routing: regularize bimodal model
If the success and fail amounts indicate that a channel doesn't obey a bimodal distribution, we fall back to a uniform/linear success probability model. This also helps to avoid numerical normalization issues with the bimodal model. This is achieved by adding a very small summand to the balance distribution P(x) ~ exp(-x/s) + exp((x-c)/s), 1/c that helps to regularize the probability distribution. The distribution becomes finite for intermediate balances where the exponentials would be evaluated to an exact zero (float) otherwise. This regularization is effective in edge cases and leads to falling back to a uniform model should the bimodal model fail. This affects the normalization to be s * (-2 * exp(-c/s) + 2 + 1/s) and the primitive function to receive an extra term x/(cs). The previously added fuzz seed is expected to be resolved with this.
1 parent fe32105 commit 3819941

File tree

2 files changed

+40
-16
lines changed

2 files changed

+40
-16
lines changed

routing/probability_bimodal.go

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -416,34 +416,38 @@ func cannotSend(failAmount, capacity lnwire.MilliSatoshi, now,
416416

417417
// primitive computes the indefinite integral of our assumed (normalized)
418418
// liquidity probability distribution. The distribution of liquidity x here is
419-
// the function P(x) ~ exp(-x/s) + exp((x-c)/s), i.e., two exponentials residing
420-
// at the ends of channels. This means that we expect liquidity to be at either
421-
// side of the channel with capacity c. The s parameter (scale) defines how far
422-
// the liquidity leaks into the channel. A very low scale assumes completely
423-
// unbalanced channels, a very high scale assumes a random distribution. More
424-
// details can be found in
419+
// the function P(x) ~ exp(-x/s) + exp((x-c)/s) + 1/c, i.e., two exponentials
420+
// residing at the ends of channels. This means that we expect liquidity to be
421+
// at either side of the channel with capacity c. The s parameter (scale)
422+
// defines how far the liquidity leaks into the channel. A very low scale
423+
// assumes completely unbalanced channels, a very high scale assumes a random
424+
// distribution. More details can be found in
425425
// https://github.com/lightningnetwork/lnd/issues/5988#issuecomment-1131234858.
426+
// Additionally, we add a constant term 1/c to the distribution to avoid
427+
// normalization issues and to fall back to a uniform distribution should the
428+
// previous success and fail amounts contradict a bimodal distribution.
426429
func (p *BimodalEstimator) primitive(c, x float64) float64 {
427430
s := float64(p.BimodalScaleMsat)
428431

429432
// The indefinite integral of P(x) is given by
430-
// Int P(x) dx = H(x) = s * (-e(-x/s) + e((x-c)/s)),
433+
// Int P(x) dx = H(x) = s * (-e(-x/s) + e((x-c)/s) + x/(c*s)),
431434
// and its norm from 0 to c can be computed from it,
432-
// norm = [H(x)]_0^c = s * (-e(-c/s) + 1 -(1 + e(-c/s))).
435+
// norm = [H(x)]_0^c = s * (-e(-c/s) + 1 + 1/s -(-1 + e(-c/s))) =
436+
// = s * (-2*e(-c/s) + 2 + 1/s).
437+
// The prefactors s are left out, as they cancel out in the end.
438+
// norm can only become zero, if c is zero, which we sorted out before
439+
// calling this method.
433440
ecs := math.Exp(-c / s)
434-
exs := math.Exp(-x / s)
441+
norm := -2*ecs + 2 + 1/s
435442

436443
// It would be possible to split the next term and reuse the factors
437444
// from before, but this can lead to numerical issues with large
438445
// numbers.
439446
excs := math.Exp((x - c) / s)
440-
441-
// norm can only become zero, if c is zero, which we sorted out before
442-
// calling this method.
443-
norm := -2*ecs + 2
447+
exs := math.Exp(-x / s)
444448

445449
// We end up with the primitive function of the normalized P(x).
446-
return (-exs + excs) / norm
450+
return (-exs + excs + x/(c*s)) / norm
447451
}
448452

449453
// integral computes the integral of our liquidity distribution from the lower

routing/probability_bimodal_test.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,30 @@ func TestSmallScale(t *testing.T) {
269269
// An amount that's close to the success amount should have a very high
270270
// probability.
271271
amtCloseSuccess := successAmount + 1
272-
_, err := estimator.probabilityFormula(
272+
p, err := estimator.probabilityFormula(
273273
capacity, successAmount, failAmount, amtCloseSuccess,
274274
)
275-
require.ErrorContains(t, err, "normalization factor is zero")
275+
require.NoError(t, err)
276+
require.InDelta(t, 1.0, p, defaultTolerance)
277+
278+
// An amount that's close to the fail amount should have a very low
279+
// probability.
280+
amtCloseFail := failAmount - 1
281+
p, err = estimator.probabilityFormula(
282+
capacity, successAmount, failAmount, amtCloseFail,
283+
)
284+
require.NoError(t, err)
285+
require.InDelta(t, 0.0, p, defaultTolerance)
286+
287+
// In the region where the bimodal model doesn't give good forecasts, we
288+
// fall back to a uniform model, which interpolates probabilities
289+
// linearly.
290+
amtLinear := successAmount + (failAmount-successAmount)*1/4
291+
p, err = estimator.probabilityFormula(
292+
capacity, successAmount, failAmount, amtLinear,
293+
)
294+
require.NoError(t, err)
295+
require.InDelta(t, 0.75, p, defaultTolerance)
276296
}
277297

278298
// TestIntegral tests certain limits of the probability distribution integral.

0 commit comments

Comments
 (0)