This notebook is motivated by a question posted on Operations Research Stack Exchange regarding the Holt formula for exponential smoothing of a univariate time series with linear trend, often referred to as “double exponential smoothing”.

Double exponential smoothing

I will use the notation from the Wikipedia page rather than the notation from the OR SE question. We start with a univariate time series \(x_t\), beginning at time \(t=0\) and presumed to contain linear trend. So the underlying generator of the time series is assumed to look like \[x_t = c_0 + c_1 t + \epsilon_t\] for some constants \(c_0\) and \(c_1,\) where \(\epsilon_t\) is independent and identically distributed noise. We do not assume that the noise is necessarily normally distributed, but we do assume zero mean for the noise (which is harmless, since we could otherwise just combine the mean of the noise with \(c_0\)).

The Holt double exponential smoothing formula uses smoothing weights \(\alpha, \beta \in (0,1)\) to produce estimates \(s_t\) and \(b_t\) for the current level (\(c_0 + c_1 t\)) at time \(t\) and the slope (\(c_1\)), respectively. The Wikipedia page starts with \(s_0=x_0\) and \(b_0=x_1 -x_0,\) which is fine. I think you could plug in other unbiased initials estimates of level and slope without violating the spirit of the method. Once you have initial values, the smoothing formulas are \[s_t = \alpha x_t + (1-\alpha)(s_{t-1} + b_{t-1})\] and \[b_t = \beta(s_t - s_{t-1}) + (1-\beta)b_{t-1}.\]

Question

The question on OR SE asked why the update to the slope is not computed as \[b_t = \beta(x_t - x_{t-1}) + (1-\beta)b_{t-1}.\] In other words, rather than blending the change in level estimates with the previous slope estimate, why not blend the change in actual observed value with the previous slope? The answer is that while you could do it that way, it would make the estimates a bit noisier. I did a bit of hand-waving in my answer on OR SE … OK, I flapped my arms hard enough to achieve liftoff … so I will try to provide experimental support here.

Experiment

We will generate observations of a time series containing linear trend and i.i.d. noise, compute level and slope estimates using both the Holt formula and the alternative raised in the OR SE question, and see how the estimates perform. To evaluate performance, we will generate multiple time series with the same parameters (including slope and intercept).

Setup

The following model parameters are utterly arbitrary.

horizon <- 100       # number of observations to generate per series
replications <- 100  # number of independent replications (time series) to generate
intercept <- 10      # $c_0$
slope <- 7           # $c_1$
sigma <- 4           # standard deviation of the noise in $x_t$
seed <- 1312         # random number seed

Generate the sample of \(x\) observations.

# Seed the random number generator.
set.seed(seed)
# Generate the x observations.
x <- intercept + slope * (0 : (horizon - 1)) + rnorm(horizon, mean = 0, sd = sigma)
for (r in 2:replications) {
  x <- rbind(x, intercept + slope * (0 : (horizon - 1)) + rnorm(horizon, mean = 0, sd = sigma))
}

Holt model

First we set the smoothing coefficients (again arbitrarily).

alpha <- 0.2  # weight for smoothing the level estimate
beta <- 0.15  # weight for smoothing the trend estimate

We generate smoothed estimates \(s_t\) of level and \(b_t\) of slope using the Holt formula.

# We first allocate vectors to hold the estimates.
hLevel <- matrix(data = NA, nrow = replications, ncol = horizon)  # level estimates ($s$)
hTrend <- hLevel                                                  # trend estimates ($b$)
# Next, initialize the estimates at time 1.
for (r in 1:replications) {
  hLevel[r, 1] <- x[r, 1]
  hTrend[r, 1] <- x[r, 2] - x[r, 1]
}
# Now use the smoothing formulas to fill in the remaining estimates.
for (t in 2:horizon) {
  for (r in 1:replications) {
    hLevel[r, t] <- alpha * x[r, t] + (1 - alpha) * (hLevel[r, t - 1] + hTrend[r, t - 1])
    hTrend[r, t] <- beta * (hLevel[r, t] - hLevel[r, t - 1]) + (1 - beta) * hTrend[r, t - 1]
  }
}

Variant

Using the same weights, we generate smoothed estimates using the variant in the trend update formula proposed in the OR SE question.

# We again allocate vectors to hold the estimates.
vLevel <- matrix(data = NA, nrow = replications, ncol = horizon)  # level estimates ($s$)
vTrend <- vLevel                                                  # trend estimates ($b$)
# Next, initialize the estimates at time 1, the same way as before.
for (r in 1:replications) {
  vLevel[r, 1] <- x[r, 1]
  vTrend[r, 1] <- x[r, 2] - x[r, 1]
}
# Now use the smoothing formulas to fill in the remaining estimates.
for (t in 2:horizon) {
  for (r in 1:replications) {
    # Level updates proceed as before.
    vLevel[r, t] <- alpha * x[r, t] + (1 - alpha) * (vLevel[r, t - 1] + vTrend[r, t - 1])
    # Trend updates replace the change in level estimate with the change in observed value.
    vTrend[r, t] <- beta * (x[r, t] - x[r, t - 1]) + (1 - beta) * vTrend[r, t - 1]
  }
}

Results.

We will use dplyr to manipulate the data and ggplot2 to plot results.

library(dplyr)
library(ggplot2)

For plotting purposes, we need to turn the level and trend estimates into a dataframe.

xdata <- data.frame(Time = rep.int(1:horizon, replications),
                    Method = "Holt",
                    Level = as.vector(t(hLevel)),
                    Trend = as.vector(t(hTrend)))
xdata2 <- data.frame(Time = rep.int(1:horizon, replications),
                     Method = "Variant",
                    Level = as.vector(t(vLevel)),
                    Trend = as.vector(t(vTrend)))
xdata <- rbind(xdata, xdata2)
rm(xdata2)

Next we convert individual observations into 95% confidence intervals.

xdata <- xdata %>% 
           group_by(Time, Method) %>% 
           summarize(Level_lo = t.test(Level, mu = mean(Level))$conf.int[1],
                     Level_hi = t.test(Level, mu = mean(Level))$conf.int[2],
                     Trend_lo = t.test(Trend, mu = mean(Trend))$conf.int[1],
                     Trend_hi = t.test(Trend, mu = mean(Trend))$conf.int[2])
`summarise()` has grouped output by 'Time'. You can override using the `.groups` argument.

Because the trend swamps the width of the level confidence intervals, making them hard to see, we convert both level and trend values into errors relative to the true level and trend.

xdata <- xdata %>% mutate(Level_lo = Level_lo - (intercept + slope*(Time - 1)),
                          Level_hi = Level_hi - (intercept + slope*(Time - 1)),
                          Trend_lo = Trend_lo - slope,
                          Trend_hi = Trend_hi - slope)

Level

Here we plot the confidence intervals for the error in estimated level over time, by method.

ggplot(data = xdata, mapping = aes(x = Time)) +
       labs(title = "95% C. I. for Error in Level Estimate", x = "Time", y = "Level Error") +
       geom_hline(mapping = aes(yintercept = 0)) +
       geom_errorbar(mapping = aes(color = Method, ymin = Level_lo, ymax = Level_hi)) +
       theme(plot.title = element_text(hjust = 0.5))

Initially the confidence intervals are large (and the proposed variation may actually do a bit better than the Holt method), but over time the width of the confidence intervals stabilizes and the two methods are pretty close in terms of estimate variability.

Slope

Here we plot the confidence intervals for the error in estimated slope over time, by method.

ggplot(data = xdata, mapping = aes(x = Time)) +
       labs(title = "95% C. I. for Error in Trend Estimate", x = "Time", y = "Trend Error") +
       geom_hline(mapping = aes(yintercept = 0)) +
       geom_errorbar(mapping = aes(color = Method, ymin = Trend_lo, ymax = Trend_hi)) +
       theme(plot.title = element_text(hjust = 0.5))

As with the error in level, the confidence intervals for the error in the trend estimates start large and stabilize (somewhat) in width, but here the Holt method has a clearly visible advantage in variability.

LS0tCnRpdGxlOiAiSG9sdCBFeHBvbmVudGlhbCBTbW9vdGhpbmciCmF1dGhvcjogUGF1bCBBLiBSdWJpbiAocnViaW5AbXN1LmVkdSkKZGF0ZTogTm92ZW1iZXIgMSwgMjAyMgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgpUaGlzIG5vdGVib29rIGlzIG1vdGl2YXRlZCBieSBhIFtxdWVzdGlvbl0oaHR0cHM6Ly9vci5zdGFja2V4Y2hhbmdlLmNvbS9xdWVzdGlvbnMvOTIxNy9mb3JlY2FzdGluZy13aXRoLWhvbHRzLWxpbmVhci10cmVuZC1tZXRob2QvKSBwb3N0ZWQgb24gT3BlcmF0aW9ucyBSZXNlYXJjaCBTdGFjayBFeGNoYW5nZSByZWdhcmRpbmcgdGhlIEhvbHQgZm9ybXVsYSBmb3IgW2V4cG9uZW50aWFsIHNtb290aGluZ10oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvRXhwb25lbnRpYWxfc21vb3RoaW5nI0RvdWJsZV9leHBvbmVudGlhbF9zbW9vdGhpbmdfKEhvbHRfbGluZWFyKSkgb2YgYSB1bml2YXJpYXRlIHRpbWUgc2VyaWVzIHdpdGggbGluZWFyIHRyZW5kLCBvZnRlbiByZWZlcnJlZCB0byBhcyAiZG91YmxlIGV4cG9uZW50aWFsIHNtb290aGluZyIuCgojIERvdWJsZSBleHBvbmVudGlhbCBzbW9vdGhpbmcKCkkgd2lsbCB1c2UgdGhlIG5vdGF0aW9uIGZyb20gdGhlIFdpa2lwZWRpYSBwYWdlIHJhdGhlciB0aGFuIHRoZSBub3RhdGlvbiBmcm9tIHRoZSBPUiBTRSBxdWVzdGlvbi4gV2Ugc3RhcnQgd2l0aCBhIHVuaXZhcmlhdGUgdGltZSBzZXJpZXMgJHhfdCQsIGJlZ2lubmluZyBhdCB0aW1lICR0PTAkIGFuZCBwcmVzdW1lZCB0byBjb250YWluIGxpbmVhciB0cmVuZC4gU28gdGhlIHVuZGVybHlpbmcgZ2VuZXJhdG9yIG9mIHRoZSB0aW1lIHNlcmllcyBpcyBhc3N1bWVkIHRvIGxvb2sgbGlrZSAkJHhfdCA9IGNfMCArIGNfMSB0ICsgXGVwc2lsb25fdCQkIGZvciBzb21lIGNvbnN0YW50cyAkY18wJCBhbmQgJGNfMSwkIHdoZXJlICRcZXBzaWxvbl90JCBpcyBpbmRlcGVuZGVudCBhbmQgaWRlbnRpY2FsbHkgZGlzdHJpYnV0ZWQgbm9pc2UuIFdlIGRvIG5vdCBhc3N1bWUgdGhhdCB0aGUgbm9pc2UgaXMgbmVjZXNzYXJpbHkgbm9ybWFsbHkgZGlzdHJpYnV0ZWQsIGJ1dCB3ZSBkbyBhc3N1bWUgemVybyBtZWFuIGZvciB0aGUgbm9pc2UgKHdoaWNoIGlzIGhhcm1sZXNzLCBzaW5jZSB3ZSBjb3VsZCBvdGhlcndpc2UganVzdCBjb21iaW5lIHRoZSBtZWFuIG9mIHRoZSBub2lzZSB3aXRoICRjXzAkKS4KClRoZSBIb2x0IGRvdWJsZSBleHBvbmVudGlhbCBzbW9vdGhpbmcgZm9ybXVsYSB1c2VzIHNtb290aGluZyB3ZWlnaHRzICRcYWxwaGEsIFxiZXRhIFxpbiAoMCwxKSQgdG8gcHJvZHVjZSBlc3RpbWF0ZXMgJHNfdCQgYW5kICRiX3QkIGZvciB0aGUgY3VycmVudCBsZXZlbCAoJGNfMCArIGNfMSB0JCkgYXQgdGltZSAkdCQgYW5kIHRoZSBzbG9wZSAoJGNfMSQpLCByZXNwZWN0aXZlbHkuIFRoZSBXaWtpcGVkaWEgcGFnZSBzdGFydHMgd2l0aCAkc18wPXhfMCQgYW5kICRiXzA9eF8xIC14XzAsJCB3aGljaCBpcyBmaW5lLiBJIHRoaW5rIHlvdSBjb3VsZCBwbHVnIGluIG90aGVyIHVuYmlhc2VkIGluaXRpYWxzIGVzdGltYXRlcyBvZiBsZXZlbCBhbmQgc2xvcGUgd2l0aG91dCB2aW9sYXRpbmcgdGhlIHNwaXJpdCBvZiB0aGUgbWV0aG9kLiBPbmNlIHlvdSBoYXZlIGluaXRpYWwgdmFsdWVzLCB0aGUgc21vb3RoaW5nIGZvcm11bGFzIGFyZSAkJHNfdCA9IFxhbHBoYSB4X3QgKyAoMS1cYWxwaGEpKHNfe3QtMX0gKyBiX3t0LTF9KSQkIGFuZCAkJGJfdCA9IFxiZXRhKHNfdCAtIHNfe3QtMX0pICsgKDEtXGJldGEpYl97dC0xfS4kJAoKIyBRdWVzdGlvbgoKVGhlIHF1ZXN0aW9uIG9uIE9SIFNFIGFza2VkIHdoeSB0aGUgdXBkYXRlIHRvIHRoZSBzbG9wZSBpcyBub3QgY29tcHV0ZWQgYXMgJCRiX3QgPSBcYmV0YSh4X3QgLSB4X3t0LTF9KSArICgxLVxiZXRhKWJfe3QtMX0uJCQgSW4gb3RoZXIgd29yZHMsIHJhdGhlciB0aGFuIGJsZW5kaW5nIHRoZSBjaGFuZ2UgaW4gbGV2ZWwgZXN0aW1hdGVzIHdpdGggdGhlIHByZXZpb3VzIHNsb3BlIGVzdGltYXRlLCB3aHkgbm90IGJsZW5kIHRoZSBjaGFuZ2UgaW4gYWN0dWFsIG9ic2VydmVkIHZhbHVlIHdpdGggdGhlIHByZXZpb3VzIHNsb3BlPyBUaGUgYW5zd2VyIGlzIHRoYXQgd2hpbGUgeW91IGNvdWxkIGRvIGl0IHRoYXQgd2F5LCBpdCB3b3VsZCBtYWtlIHRoZSBlc3RpbWF0ZXMgYSBiaXQgbm9pc2llci4gSSBkaWQgYSBiaXQgb2YgaGFuZC13YXZpbmcgaW4gbXkgYW5zd2VyIG9uIE9SIFNFIC4uLiBPSywgSSBmbGFwcGVkIG15IGFybXMgaGFyZCBlbm91Z2ggdG8gYWNoaWV2ZSBsaWZ0b2ZmIC4uLiBzbyBJIHdpbGwgdHJ5IHRvIHByb3ZpZGUgZXhwZXJpbWVudGFsIHN1cHBvcnQgaGVyZS4KCiMgRXhwZXJpbWVudAoKV2Ugd2lsbCBnZW5lcmF0ZSBvYnNlcnZhdGlvbnMgb2YgYSB0aW1lIHNlcmllcyBjb250YWluaW5nIGxpbmVhciB0cmVuZCBhbmQgaS5pLmQuIG5vaXNlLCBjb21wdXRlIGxldmVsIGFuZCBzbG9wZSBlc3RpbWF0ZXMgdXNpbmcgYm90aCB0aGUgSG9sdCBmb3JtdWxhIGFuZCB0aGUgYWx0ZXJuYXRpdmUgcmFpc2VkIGluIHRoZSBPUiBTRSBxdWVzdGlvbiwgYW5kIHNlZSBob3cgdGhlIGVzdGltYXRlcyBwZXJmb3JtLiBUbyBldmFsdWF0ZSBwZXJmb3JtYW5jZSwgd2Ugd2lsbCBnZW5lcmF0ZSBtdWx0aXBsZSB0aW1lIHNlcmllcyB3aXRoIHRoZSBzYW1lIHBhcmFtZXRlcnMgKGluY2x1ZGluZyBzbG9wZSBhbmQgaW50ZXJjZXB0KS4KCiMjIFNldHVwCgpUaGUgZm9sbG93aW5nIG1vZGVsIHBhcmFtZXRlcnMgYXJlIHV0dGVybHkgYXJiaXRyYXJ5LgoKYGBge3J9Cmhvcml6b24gPC0gMTAwICAgICAgICMgbnVtYmVyIG9mIG9ic2VydmF0aW9ucyB0byBnZW5lcmF0ZSBwZXIgc2VyaWVzCnJlcGxpY2F0aW9ucyA8LSAxMDAgICMgbnVtYmVyIG9mIGluZGVwZW5kZW50IHJlcGxpY2F0aW9ucyAodGltZSBzZXJpZXMpIHRvIGdlbmVyYXRlCmludGVyY2VwdCA8LSAxMCAgICAgICMgJGNfMCQKc2xvcGUgPC0gNyAgICAgICAgICAgIyAkY18xJApzaWdtYSA8LSA0ICAgICAgICAgICAjIHN0YW5kYXJkIGRldmlhdGlvbiBvZiB0aGUgbm9pc2UgaW4gJHhfdCQKc2VlZCA8LSAxMzEyICAgICAgICAgIyByYW5kb20gbnVtYmVyIHNlZWQKYGBgCgpHZW5lcmF0ZSB0aGUgc2FtcGxlIG9mICR4JCBvYnNlcnZhdGlvbnMuCgpgYGB7cn0KIyBTZWVkIHRoZSByYW5kb20gbnVtYmVyIGdlbmVyYXRvci4Kc2V0LnNlZWQoc2VlZCkKIyBHZW5lcmF0ZSB0aGUgeCBvYnNlcnZhdGlvbnMuCnggPC0gaW50ZXJjZXB0ICsgc2xvcGUgKiAoMCA6IChob3Jpem9uIC0gMSkpICsgcm5vcm0oaG9yaXpvbiwgbWVhbiA9IDAsIHNkID0gc2lnbWEpCmZvciAociBpbiAyOnJlcGxpY2F0aW9ucykgewogIHggPC0gcmJpbmQoeCwgaW50ZXJjZXB0ICsgc2xvcGUgKiAoMCA6IChob3Jpem9uIC0gMSkpICsgcm5vcm0oaG9yaXpvbiwgbWVhbiA9IDAsIHNkID0gc2lnbWEpKQp9CmBgYAoKIyMgSG9sdCBtb2RlbAoKRmlyc3Qgd2Ugc2V0IHRoZSBzbW9vdGhpbmcgY29lZmZpY2llbnRzIChhZ2FpbiBhcmJpdHJhcmlseSkuCgpgYGB7cn0KYWxwaGEgPC0gMC4yICAjIHdlaWdodCBmb3Igc21vb3RoaW5nIHRoZSBsZXZlbCBlc3RpbWF0ZQpiZXRhIDwtIDAuMTUgICMgd2VpZ2h0IGZvciBzbW9vdGhpbmcgdGhlIHRyZW5kIGVzdGltYXRlCmBgYAoKV2UgZ2VuZXJhdGUgc21vb3RoZWQgZXN0aW1hdGVzICRzX3QkIG9mIGxldmVsIGFuZCAkYl90JCBvZiBzbG9wZSB1c2luZyB0aGUgSG9sdCBmb3JtdWxhLgoKYGBge3J9CiMgV2UgZmlyc3QgYWxsb2NhdGUgdmVjdG9ycyB0byBob2xkIHRoZSBlc3RpbWF0ZXMuCmhMZXZlbCA8LSBtYXRyaXgoZGF0YSA9IE5BLCBucm93ID0gcmVwbGljYXRpb25zLCBuY29sID0gaG9yaXpvbikgICMgbGV2ZWwgZXN0aW1hdGVzICgkcyQpCmhUcmVuZCA8LSBoTGV2ZWwgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgdHJlbmQgZXN0aW1hdGVzICgkYiQpCiMgTmV4dCwgaW5pdGlhbGl6ZSB0aGUgZXN0aW1hdGVzIGF0IHRpbWUgMS4KZm9yIChyIGluIDE6cmVwbGljYXRpb25zKSB7CiAgaExldmVsW3IsIDFdIDwtIHhbciwgMV0KICBoVHJlbmRbciwgMV0gPC0geFtyLCAyXSAtIHhbciwgMV0KfQojIE5vdyB1c2UgdGhlIHNtb290aGluZyBmb3JtdWxhcyB0byBmaWxsIGluIHRoZSByZW1haW5pbmcgZXN0aW1hdGVzLgpmb3IgKHQgaW4gMjpob3Jpem9uKSB7CiAgZm9yIChyIGluIDE6cmVwbGljYXRpb25zKSB7CiAgICBoTGV2ZWxbciwgdF0gPC0gYWxwaGEgKiB4W3IsIHRdICsgKDEgLSBhbHBoYSkgKiAoaExldmVsW3IsIHQgLSAxXSArIGhUcmVuZFtyLCB0IC0gMV0pCiAgICBoVHJlbmRbciwgdF0gPC0gYmV0YSAqIChoTGV2ZWxbciwgdF0gLSBoTGV2ZWxbciwgdCAtIDFdKSArICgxIC0gYmV0YSkgKiBoVHJlbmRbciwgdCAtIDFdCiAgfQp9CmBgYAoKIyMgVmFyaWFudAoKVXNpbmcgdGhlIHNhbWUgd2VpZ2h0cywgd2UgZ2VuZXJhdGUgc21vb3RoZWQgZXN0aW1hdGVzIHVzaW5nIHRoZSB2YXJpYW50IGluIHRoZSB0cmVuZCB1cGRhdGUgZm9ybXVsYSBwcm9wb3NlZCBpbiB0aGUgT1IgU0UgcXVlc3Rpb24uCgpgYGB7cn0KIyBXZSBhZ2FpbiBhbGxvY2F0ZSB2ZWN0b3JzIHRvIGhvbGQgdGhlIGVzdGltYXRlcy4KdkxldmVsIDwtIG1hdHJpeChkYXRhID0gTkEsIG5yb3cgPSByZXBsaWNhdGlvbnMsIG5jb2wgPSBob3Jpem9uKSAgIyBsZXZlbCBlc3RpbWF0ZXMgKCRzJCkKdlRyZW5kIDwtIHZMZXZlbCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIyB0cmVuZCBlc3RpbWF0ZXMgKCRiJCkKIyBOZXh0LCBpbml0aWFsaXplIHRoZSBlc3RpbWF0ZXMgYXQgdGltZSAxLCB0aGUgc2FtZSB3YXkgYXMgYmVmb3JlLgpmb3IgKHIgaW4gMTpyZXBsaWNhdGlvbnMpIHsKICB2TGV2ZWxbciwgMV0gPC0geFtyLCAxXQogIHZUcmVuZFtyLCAxXSA8LSB4W3IsIDJdIC0geFtyLCAxXQp9CiMgTm93IHVzZSB0aGUgc21vb3RoaW5nIGZvcm11bGFzIHRvIGZpbGwgaW4gdGhlIHJlbWFpbmluZyBlc3RpbWF0ZXMuCmZvciAodCBpbiAyOmhvcml6b24pIHsKICBmb3IgKHIgaW4gMTpyZXBsaWNhdGlvbnMpIHsKICAgICMgTGV2ZWwgdXBkYXRlcyBwcm9jZWVkIGFzIGJlZm9yZS4KICAgIHZMZXZlbFtyLCB0XSA8LSBhbHBoYSAqIHhbciwgdF0gKyAoMSAtIGFscGhhKSAqICh2TGV2ZWxbciwgdCAtIDFdICsgdlRyZW5kW3IsIHQgLSAxXSkKICAgICMgVHJlbmQgdXBkYXRlcyByZXBsYWNlIHRoZSBjaGFuZ2UgaW4gbGV2ZWwgZXN0aW1hdGUgd2l0aCB0aGUgY2hhbmdlIGluIG9ic2VydmVkIHZhbHVlLgogICAgdlRyZW5kW3IsIHRdIDwtIGJldGEgKiAoeFtyLCB0XSAtIHhbciwgdCAtIDFdKSArICgxIC0gYmV0YSkgKiB2VHJlbmRbciwgdCAtIDFdCiAgfQp9CmBgYAoKIyMgUmVzdWx0cy4KCldlIHdpbGwgdXNlIGBkcGx5cmAgdG8gbWFuaXB1bGF0ZSB0aGUgZGF0YSBhbmQgYGdncGxvdDJgIHRvIHBsb3QgcmVzdWx0cy4KCmBgYHtyfQpsaWJyYXJ5KGRwbHlyKQpsaWJyYXJ5KGdncGxvdDIpCmBgYAoKRm9yIHBsb3R0aW5nIHB1cnBvc2VzLCB3ZSBuZWVkIHRvIHR1cm4gdGhlIGxldmVsIGFuZCB0cmVuZCBlc3RpbWF0ZXMgaW50byBhIGRhdGFmcmFtZS4KCmBgYHtyfQp4ZGF0YSA8LSBkYXRhLmZyYW1lKFRpbWUgPSByZXAuaW50KDE6aG9yaXpvbiwgcmVwbGljYXRpb25zKSwKICAgICAgICAgICAgICAgICAgICBNZXRob2QgPSAiSG9sdCIsCiAgICAgICAgICAgICAgICAgICAgTGV2ZWwgPSBhcy52ZWN0b3IodChoTGV2ZWwpKSwKICAgICAgICAgICAgICAgICAgICBUcmVuZCA9IGFzLnZlY3Rvcih0KGhUcmVuZCkpKQp4ZGF0YTIgPC0gZGF0YS5mcmFtZShUaW1lID0gcmVwLmludCgxOmhvcml6b24sIHJlcGxpY2F0aW9ucyksCiAgICAgICAgICAgICAgICAgICAgIE1ldGhvZCA9ICJWYXJpYW50IiwKICAgICAgICAgICAgICAgICAgICBMZXZlbCA9IGFzLnZlY3Rvcih0KHZMZXZlbCkpLAogICAgICAgICAgICAgICAgICAgIFRyZW5kID0gYXMudmVjdG9yKHQodlRyZW5kKSkpCnhkYXRhIDwtIHJiaW5kKHhkYXRhLCB4ZGF0YTIpCnJtKHhkYXRhMikKYGBgCgpOZXh0IHdlIGNvbnZlcnQgaW5kaXZpZHVhbCBvYnNlcnZhdGlvbnMgaW50byA5NSUgY29uZmlkZW5jZSBpbnRlcnZhbHMuCgpgYGB7cn0KeGRhdGEgPC0geGRhdGEgJT4lIAogICAgICAgICAgIGdyb3VwX2J5KFRpbWUsIE1ldGhvZCkgJT4lIAogICAgICAgICAgIHN1bW1hcml6ZShMZXZlbF9sbyA9IHQudGVzdChMZXZlbCwgbXUgPSBtZWFuKExldmVsKSkkY29uZi5pbnRbMV0sCiAgICAgICAgICAgICAgICAgICAgIExldmVsX2hpID0gdC50ZXN0KExldmVsLCBtdSA9IG1lYW4oTGV2ZWwpKSRjb25mLmludFsyXSwKICAgICAgICAgICAgICAgICAgICAgVHJlbmRfbG8gPSB0LnRlc3QoVHJlbmQsIG11ID0gbWVhbihUcmVuZCkpJGNvbmYuaW50WzFdLAogICAgICAgICAgICAgICAgICAgICBUcmVuZF9oaSA9IHQudGVzdChUcmVuZCwgbXUgPSBtZWFuKFRyZW5kKSkkY29uZi5pbnRbMl0pCmBgYApCZWNhdXNlIHRoZSB0cmVuZCBzd2FtcHMgdGhlIHdpZHRoIG9mIHRoZSBsZXZlbCBjb25maWRlbmNlIGludGVydmFscywgbWFraW5nIHRoZW0gaGFyZCB0byBzZWUsIHdlIGNvbnZlcnQgYm90aCBsZXZlbCBhbmQgdHJlbmQgdmFsdWVzIGludG8gZXJyb3JzIHJlbGF0aXZlIHRvIHRoZSB0cnVlIGxldmVsIGFuZCB0cmVuZC4KCmBgYHtyfQp4ZGF0YSA8LSB4ZGF0YSAlPiUgbXV0YXRlKExldmVsX2xvID0gTGV2ZWxfbG8gLSAoaW50ZXJjZXB0ICsgc2xvcGUqKFRpbWUgLSAxKSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgTGV2ZWxfaGkgPSBMZXZlbF9oaSAtIChpbnRlcmNlcHQgKyBzbG9wZSooVGltZSAtIDEpKSwKICAgICAgICAgICAgICAgICAgICAgICAgICBUcmVuZF9sbyA9IFRyZW5kX2xvIC0gc2xvcGUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgVHJlbmRfaGkgPSBUcmVuZF9oaSAtIHNsb3BlKQpgYGAKCiMjIyBMZXZlbAoKSGVyZSB3ZSBwbG90IHRoZSBjb25maWRlbmNlIGludGVydmFscyBmb3IgdGhlIGVycm9yIGluIGVzdGltYXRlZCBsZXZlbCBvdmVyIHRpbWUsIGJ5IG1ldGhvZC4KCmBgYHtyfQpnZ3Bsb3QoZGF0YSA9IHhkYXRhLCBtYXBwaW5nID0gYWVzKHggPSBUaW1lKSkgKwogICAgICAgbGFicyh0aXRsZSA9ICI5NSUgQy4gSS4gZm9yIEVycm9yIGluIExldmVsIEVzdGltYXRlIiwgeCA9ICJUaW1lIiwgeSA9ICJMZXZlbCBFcnJvciIpICsKICAgICAgIGdlb21faGxpbmUobWFwcGluZyA9IGFlcyh5aW50ZXJjZXB0ID0gMCkpICsKICAgICAgIGdlb21fZXJyb3JiYXIobWFwcGluZyA9IGFlcyhjb2xvciA9IE1ldGhvZCwgeW1pbiA9IExldmVsX2xvLCB5bWF4ID0gTGV2ZWxfaGkpKSArCiAgICAgICB0aGVtZShwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41KSkKYGBgCgpJbml0aWFsbHkgdGhlIGNvbmZpZGVuY2UgaW50ZXJ2YWxzIGFyZSBsYXJnZSAoYW5kIHRoZSBwcm9wb3NlZCB2YXJpYXRpb24gbWF5IGFjdHVhbGx5IGRvIGEgYml0IGJldHRlciB0aGFuIHRoZSBIb2x0IG1ldGhvZCksIGJ1dCBvdmVyIHRpbWUgdGhlIHdpZHRoIG9mIHRoZSBjb25maWRlbmNlIGludGVydmFscyBzdGFiaWxpemVzIGFuZCB0aGUgdHdvIG1ldGhvZHMgYXJlIHByZXR0eSBjbG9zZSBpbiB0ZXJtcyBvZiBlc3RpbWF0ZSB2YXJpYWJpbGl0eS4KCiMjIyBTbG9wZQoKSGVyZSB3ZSBwbG90IHRoZSBjb25maWRlbmNlIGludGVydmFscyBmb3IgdGhlIGVycm9yIGluIGVzdGltYXRlZCBzbG9wZSBvdmVyIHRpbWUsIGJ5IG1ldGhvZC4KCmBgYHtyfQpnZ3Bsb3QoZGF0YSA9IHhkYXRhLCBtYXBwaW5nID0gYWVzKHggPSBUaW1lKSkgKwogICAgICAgbGFicyh0aXRsZSA9ICI5NSUgQy4gSS4gZm9yIEVycm9yIGluIFRyZW5kIEVzdGltYXRlIiwgeCA9ICJUaW1lIiwgeSA9ICJUcmVuZCBFcnJvciIpICsKICAgICAgIGdlb21faGxpbmUobWFwcGluZyA9IGFlcyh5aW50ZXJjZXB0ID0gMCkpICsKICAgICAgIGdlb21fZXJyb3JiYXIobWFwcGluZyA9IGFlcyhjb2xvciA9IE1ldGhvZCwgeW1pbiA9IFRyZW5kX2xvLCB5bWF4ID0gVHJlbmRfaGkpKSArCiAgICAgICB0aGVtZShwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41KSkKYGBgCgpBcyB3aXRoIHRoZSBlcnJvciBpbiBsZXZlbCwgdGhlIGNvbmZpZGVuY2UgaW50ZXJ2YWxzIGZvciB0aGUgZXJyb3IgaW4gdGhlIHRyZW5kIGVzdGltYXRlcyBzdGFydCBsYXJnZSBhbmQgc3RhYmlsaXplIChzb21ld2hhdCkgaW4gd2lkdGgsIGJ1dCBoZXJlIHRoZSBIb2x0IG1ldGhvZCBoYXMgYSBjbGVhcmx5IHZpc2libGUgYWR2YW50YWdlIGluIHZhcmlhYmlsaXR5Lgo=