knitr::opts_chunk$set(comment="") options(warn=0) library(dplyr) library(lattice)
We need some reasonable upper bound for the lifetimes of tags, by battery and BI.
Lotek provides a short table here:
http://www.lotek.com/bird+bat-nano.pdf (As of 1 April, 2016)
and there are additional models listed in "ANTC Spec Sheet.pdf" (no source), including the only numbers for battery type "3-1". Since lifetime is listed as depending only on the battery (numeric portion of model string), we model on that.
lt = as.tbl(read.csv("./lotekTagLifespanByBatteryAndBI.csv"))
Add inverses of columns bi and dutyCycle, for modelling
ls = lt %>% mutate(biInv = 1.0 / bi, dcInv = 1.0 / dutyCycle) print(ls)
A simple model assumes that the battery capacity, $K$, depends on the battery model, and that tag power consumption $r$ is a sum of a baseline rate $r_0$ plus a pulse-dependent rate $r_1$. All tags transmit 4 pulses per burst, so the pulse-dependent rate $r_1$ depends only on duty cycle and burst interval like so: $r_1 = r_p * \frac{dutyCyle}{BI}$
The full non-linear model is :
[ lifespan = \frac{K}{r_0 + \frac{r_p * dutyCycle}{BI}} ]
This is over-parameterized (we can divide top and bottom by $r_0$ to get a model with two parameters), so we simplify by rewriting $K/r_0$ as $D$, the number of days of battery life at the baseline rate, and $r_p / r_0$ be $r_t$, the relative rate of power consumption during transmission, versus baseline. The new model is:
[ lifespan = \frac{D}{1 + \frac{r_t * dutyCycle}{BI}} ]
We fit the model to each type of battery:
par = res = pred = NULL bnames = unique(ls$battery) for (m in bnames) { res[[m]] = nls(lifespan~D / (1 + rt * dutyCycle / bi), subset(ls, battery==m), list(D = 500, rt = 5)) par = rbind(par, c(coefficients(res[[m]]), max(residuals(res[[m]])))) pred = rbind(pred, data.frame( battery=m, bi=1:40, lifespan=predict(res[[m]], list(bi=1:40, dutyCycle=1)) )) } rownames(par) = bnames colnames(par) = c("D", "rt", "Max Residual (days)")
Stu Mackenzie pointed out there is a light-weight version of the NTQB-1 which he's dubbed NTQB-1-LW, with "~2/3 the lifetime of the NTQB-1". For now we'll assume that means the $D$ parameter for that model is 2/3 that for the NTQB-1
par = rbind(c(par[["1","D"]] * 2 / 3, par[["1", "rt"]], NA), par) rownames(par)[1] = "1-LW" pred = rbind(data.frame( battery="1-LW", bi=1:40, lifespan=par[["1-LW", "D"]] / (1 + par[["1-LW", "rt"]] / (1:40))) , pred) ## mark which rows are measurements pred = cbind(pred, isMeasured = paste(pred$battery, pred$bi) %in% paste(lt$battery, lt$bi))
Now map model names to batteries. We do this as a simple table because the naming scheme isn't consistent enough to bother doing it programmatically.
## Models and the batteries they correspond to. Note that ## we include a model called 'unknown' which corresponds to ## a battery called 'unknown' that is assigned a life equal ## to the average of those for NTQB-2, NTQB-3-2. ## This forces a warning to be emailed to the motus admin whenever ## the metadata cache is updated, because an unmodelled tag ## is a serious metadata issue. modelBattery = list( "ACT-393" = "393", "ACT-521" = "521", "ACT-626" = "626", "ANTC-M1-1" = "1", "ANTC-M2-1" = "2", "ANTC-M3-1" = "3-1", "ANTC-M3-2" = "3-2", "ANTC-M4-2" = "4-2", "ANTC-M4-2L" = "4-2", "ANTC-M4-2S" = "4-2", "ANTC-M6-1" = "6-1", "ANTC-M6-2" = "6-2", "ANTCW-M1-1" = "1", "ANTCW-M2-1" = "2", "ANTCW-M3-1" = "3-1", "ANTCW-M3-2" = "3-2", "ANTCW-M4-2" = "4-2", "ANTCW-M4-2L" = "4-2", "ANTCW-M4-2S" = "4-2", "ANTCW-M6-1" = "6-1", "ANTCW-M6-2" = "6-2", "NTP-1" = "NanoPin", "NTP-1-M" = "NanoPin", "NTQB-1" = "1", "NTQB-1-LW" = "1-LW", "NTQB-2" = "2", "NTQB2-1" = "2-1", "NTQB2-1-M" = "2-1", "NTQB2-2" = "2-2", "NTQB2-2-M" = "2-2", "NTQB2-3-2" = "2-3-2", "NTQB2-3-2-M" = "2-3-2", "NTQB2-4-2" = "2-4-2", "NTQB2-4-2-M" = "2-4-2", "NTQB2-4-2S" = "2-4-2", "NTQB2-4-2S-M" = "2-4-2", "NTQB2-5-1" = "2-5-1", "NTQB2-5-1-M" = "2-5-1", "NTQB2-6-1" = "2-6-1", "NTQB2-6-1-M" = "2-6-1", "NTQB2-6-2" = "2-6-2", "NTQB2-6-2-M" = "2-6-2", "NTQB2-6-2T-M" = "2-6-2", "NTQB2-9-2" = "2-9-2", "NTQB2-9-2-M" = "2-9-2", "NTQB-3-2" = "3-2", "NTQB-4-2" = "4-2", "NTQB-6-1" = "6-1", "NTQB-6-2" = "6-2", "NTQBW-2" = "2", "NTQBW-3-2" = "3-2", "NTQBW-4-2" = "4-2", "NTQBW-6-2" = "6-2", "NTS-1" = "SOLAR", "NTS-1-M" = "SOLAR" ) modPar = NULL for (b in names(modelBattery)) modPar = rbind(modPar, par[modelBattery[[b]],]) rownames(modPar) = names(modelBattery)
The results show good agreement with the data table from Lotek:
print(round(par, 1))
The parameter $r_t$ can be interpreted as the ratio of energy consumed during 1 second with a burst to that consumed during 1 second without a burst. Estimates of this parameter vary only by 5% across tag types, and in monotonic fashion, perhaps due to variation in battery internal resistance. The table provided by Lotek only covers $2 <= BI <= 20$ (the larger value from BI=10s @ 50% duty cycle); curves are extrapolated down to 1s and from 20 to 40s using the fitted model.
xyplot(log10(lifespan)~bi|as.factor(battery), pred, main="Reported (+) and Predicted Tag Lifespan\nby Battery Type and Burst Interval", xlab="Burst Interval (seconds)", ylab="Lifespan (log10(days); 1->10, 2->100, 3->1000)", type="l", panel = function(x, y, type, subscripts, ...) { panel.xyplot(x, y, type, ...) panel.points(x[pred$isMeasured[subscripts]], y[pred$isMeasured[subscripts]], pch="+", cex=2, col="black") } )
```r tagLifespanPars = modPar[, 1:2] save(tagLifespanPars, file="modelLotekTagLifeSpan.rda")
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.