Libre OOP Investigation

UPetersen edited this page Oct 3, 2018 · 41 revisions

Introduction

The following paragraphs describe an investigation into temperature dependence of the Freestyle Libre data.

Method

SwiftLibreOOPWeb by @dabear allows to feed the original Abbott algorithm with real data from a sensor and then returns the correct glucose value(s). But SwiftLibreOOPWeb can do more since it can also be fed with artificial data to investigate how the Abbott algorithm works. This is used for the following analyses. SwiftLibreOOPWeb uses @tzachi-dar’s LibreOOPAlgorithm running on an Android phone.

SwiftLibreOOPWeb needs the following data as input:

  • FRAM data of the Freestyle Libre sensor, i.e. header, body and footer bytes, as described in this wiki, and
  • a state vector containing 32 bytes of data.

SwiftLibreOOPWeb then returns the following data as output:

  • The current glucose value with corresponding error flag and minute counter,
  • 32 sets of history data for the last eight hours, each with glucose value, error flag and minute counter, and
  • a new state vector.

The state vector returned by the OOP algorithm is to be used as input state vector for the next set/reading of FRAM data. This provides some smoothing of otherwise sometimes „jumpy“ data, see here.

In order to investigate how specific features of the original Abbott algorithm work, only one parameter is changed with each test run. To achieve this, the FRAM data is manipulated such that the record of six bytes for one set of glucose data is used for each of the 32 history and each of the 16 trend data sets. (That somewhat resembles placing the sensor in an environment with constant glucose and constant temperature for eight hours and then get a full reading which would return all the same constant values for the last eight hours and the current glucose, i.e. a flat constant line on the display of the reader). That means the body section of the FRAM data is fully artificially constructed. In order to have valid FRAM, the CRC for the body section, which resides in its first two bytes, has to be recalculated so that it matches the rest of the data. Otherwise (wrong crc) LibreOOP will return an error and no useful results.

Contrary to the artificial body section data, the header and footer data are taken from an existing sensor.

Example

The record of six bytes shall be

0x e8 03 c8 d4 5b 00

Thereof being 0xe803 the raw glucose value. Its decimal value can be calculated like this:

0x e8 03 c8 d4 5b 00
   \   / 
    \ /  swap            mask with 0x3FFF            decimal    raw glucose
     +--------> 0x03e8 -------------------> 0x03e8 ----------->    1000

The remaining four bytes 0xc8d45b00 contain temperature related data and some possible flags and were taken from a reading of a normal sensor.

Furthermore, data for header and footer is needed, which is also taken from some real sensor reading. The complete test input data then is in hex:

Header bytes
2d b7 c8 15 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
Body bytes (with some comments)
11 d6 08 10          -> crc and trend and history index
e8 03 c8 d4 5b 00    -> record of six bytes of one trend glucose reading
e8 03 c8 d4 5b 00    -> dito
e8 03 c8 d4 5b 00    -> ...
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00    -> last record of trend glucose reading
e8 03 c8 d4 5b 00    -> record of six bytes of one history glucose reading 
e8 03 c8 d4 5b 00    -> dito
e8 03 c8 d4 5b 00    -> ...
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00 
e8 03 c8 d4 5b 00    -> last record of history glucose reading
d9 11 00 00          -> minute counter and last two bytes which seem always zero
Footer bytes
ad 71 00 00 da 03 a3 50 14 07 96 80 5a 00 ed a6 0a 81 1a e9 04 ae 2c 70 
Input state vector:
ff ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
ff ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

This is the default value for an input state vector according to SwiftLibreOOPWeb.

text

Libre OOP response

The response for the above data is

currentBg: 90 FullAlgoResults: {
"currenTrend":0,"currentBg":90.0,"currentTime":4568,
"historicBg":[
{"bg":90.0,"quality":0,"time":4095},    
{"bg":90.0,"quality":0,"time":4110},
{"bg":90.0,"quality":0,"time":4125},
{"bg":90.0,"quality":0,"time":4140},
{"bg":90.0,"quality":0,"time":4155},
{"bg":90.0,"quality":0,"time":4170},
{"bg":90.0,"quality":0,"time":4185},
{"bg":90.0,"quality":0,"time":4200},
{"bg":90.0,"quality":0,"time":4215},
{"bg":90.0,"quality":0,"time":4230},
{"bg":90.0,"quality":0,"time":4245},
{"bg":90.0,"quality":0,"time":4260},
{"bg":90.0,"quality":0,"time":4275},
{"bg":90.0,"quality":0,"time":4290},
{"bg":90.0,"quality":0,"time":4305},    
{"bg":90.0,"quality":0,"time":4320},
{"bg":90.0,"quality":0,"time":4335},
{"bg":90.0,"quality":0,"time":4350},
{"bg":90.0,"quality":0,"time":4365},
{"bg":90.0,"quality":0,"time":4380},
{"bg":90.0,"quality":0,"time":4395},
{"bg":90.0,"quality":0,"time":4410},
{"bg":90.0,"quality":0,"time":4425},
{"bg":90.0,"quality":0,"time":4440},
{"bg":90.0,"quality":0,"time":4455},
{"bg":90.0,"quality":0,"time":4470},
{"bg":90.0,"quality":0,"time":4485},
{"bg":90.0,"quality":0,"time":4500},
{"bg":90.0,"quality":0,"time":4515},
{"bg":90.0,"quality":0,"time":4530},
{"bg":90.0,"quality":0,"time":4545},
{"bg":90.0,"quality":0,"time":4560}
]}

and the state vector

  d8 11 00 00 00 00 00 00 d8 b0 74 26 bd 82 56 40 
  d8 11 00 00 00 00 00 00 9a 35 f2 9f 19 5d 31 bd 

Obviously, LibreOOP returns the same glucose value of 90 (results are always rounded to integer numbers), the same quality value of zero (this rather seems to be an error flag, which is zero for a "good" response) for the current glucose value and all 32 history glucose values. Time is the value of the minute counter of the respective glucose value.

LibreOOP does not return trend glucose values but obviously uses the trend (input) data to calculate a current glucose value. By providing a "flat" input of the same raw data over time, the response for the current glucose is flat, too, as expected. So this method neglects any possible extrapolation for e.g. rising trend data. Such effects could be studied in a separate investigation. It also neglects any effect of the state input vector, since the default input vector is always used for this study.

Linearity of raw glucose

The first investigation is on whether the glucose values of the LibreOOP algorithm behave linear with respect to changes of the raw glucose values when the remaining four bytes remain the same, i.e. for the same constant temperature and same constant other flag values. The raw values from 700 to 5200 are used as input with the same remaining four bytes 0xC8D45B00 (as in above example) in each run. The table and the graph below show the input and the corresponding glucose values returned by LibreOOP.

raw glucose therefrom constructed record of six bytes glucose value returned by LibreOOP [mg/dl]
300 2C01C8D45B00 39
400 9001C8D45B00 39
500 F401C8D45B00 39
600 5802C8D45B00 46
700 BC02C8D45B00 58
800 2003C8D45B00 69
900 8403C8D45B00 80
1000 E803C8D45B00 92
1100 4C04C8D45B00 103
1200 B004C8D45B00 114
1300 1405C8D45B00 126
1400 7805C8D45B00 137
1500 DC05C8D45B00 148
1600 4006C8D45B00 160
1700 A406C8D45B00 171
1800 0807C8D45B00 182
1900 6C07C8D45B00 194
2000 D007C8D45B00 205
2200 9808C8D45B00 228
2400 6009C8D45B00 250
2600 280AC8D45B00 273
2800 F00AC8D45B00 296
3000 B80BC8D45B00 318
3200 800CC8D45B00 341
3400 480DC8D45B00 364
3600 100EC8D45B00 386
3800 D80EC8D45B00 409
4000 A00FC8D45B00 432
4200 6810C8D45B00 454
4400 3011C8D45B00 477
4600 F811C8D45B00 500
4800 C012C8D45B00 501
5000 8813C8D45B00 501
5200 5014C8D45B00 501

Linear response to raw glucose variation text Figure: LibreOOP glucose value for different raw glucose values but otherwise constant data
(i.e. the same constant four remaining bytes are used for each trend and each history record)

OOP returns values between 39 and 501 mgl/dl. The response is linear from 40 to 500 mg/dl. The values 39 and 501 mg/dl denote the lower and upper threshold, so 39 mg/dl is the infamous „LO“ and 501 mg/dl probably denotes „HI“. (I never experienced such high values myself, so this is a guess). For the linear range the relationship can be calculated using the formula

       glucose = slope * raw_glucose + offset

where glucose denotes the glucose returned by LibreOOP, raw_glucose denotes the raw glucose input and is without dimension. Slope and offset can be calculated from two data sets [(raw_glucose1; glucose1); (raw_glucose2; glucose_2)] as

       slope = (glucose2 - glucose1) / (raw_glucose2 - raw_glucose1)

and

       offset = glucose2 - slope * raw_glucose2

which in this case, using [(700; 58);(3000; 318)], yields

       slope = (318 mg/dl - 58 mg/dl) / (3000 - 700) = 0.113 mg/dl

and

       offset = 318 mg/dl - 0.113 mg/dl * 3000 = -21.1 mg/dl

and finally the equation for calculation fo glucose from raw glucose is

       glucose = 0.113 mg/dl * raw_glucose -21.1 mgl/dl.

Up to this point it can only be stated that this result holds for 0xC8D45B00 as the remaining four bytes being identical for each input record. Does it also hold for variations of the remaining four bytes, i.e. different temperatures?

Temperature

Carrying out some experiments with varying temperatures and looking at the six bytes of the trend data records, one can see, that bytes three and four change significantly with changing temperature. Furthermore it is noticeable, that the changes in byte three are quicker than those in byte four, which is an indication for swapped byes, as already seen with the raw glucose value. A closer look also reveals that the fourth byte should be masked to exclude its first two bits, similar to the masking of raw glucose, in order to get useful results. Thus one can calculate a „raw temperature" from byte three and byte four in the same manner as raw glucose from byte zero and byte one.

An example for calculating raw temperature from a six bytes record of raw data (the one already used above) looks like this:

0x e8 03 c8 d4 5b 00
            \   / 
             \ /  swap           mask with 0x3FFF           decimal   raw temperature
              +--------> 0x5bd4 ------------------> 0x1bd4 -------->       7124 

Now one could continue and try to derive real temperature values in degrees Centigrade or Fahrenheit from this raw temperature. There are also various ways discussed to do this, especially in Pierre Vandevenne's blog, see e.g. here. But for the following investigation only raw temperature will be used.

Variation of temperature

In order to be sure to get valid data for the remaining four bytes, a set of real trend data records is used, where my upper arm with the freestyle libre sensor was hold under warm water from a shower. Four records were chosen. The first record corresponds to room temperature of approximately 25 degrees Celsius (this is a guess since I did not measure room temperature) and the last value to the highest temperature, having applied very warm water under the shower for some minutes (again, no exact temperature measurement). The other two are taken from in between. So omitting the first two bytes of raw glucose the set of data is

0xC8149900, 0xC800D800, 0xC8B89600, 0xC89C5800

Additionally, the last four bytes of the well known example record from above are used, too, so that the complete set for the temperature investigation is

0xC8D45B00, 0xC8149900, 0xC800D800, 0xC8B89600, 0xC89C5800

Example: The table below shows complete six byte records built from the above set of four remaining bytes for a raw glucose value of 700 (0xBC02) and the corresponding raw temperature values:

Record of six bytes of raw data Raw temperature in decimal calculated from byte three and four
0x BC 02 C8 D4 5B 00 7124
0x BC 02 C8 14 99 00 6420
0x BC 02 C8 9C 58 00 6300
0x BC 02 C8 00 D8 00 6144
0x BC 02 C8 B8 96 00 5816

In a first step the LibreOOP glucose values are calculated for raw glucose values of 700, 1000, 1500, 2000, 2500, and 3000 (a subset of what was used above) and this for all of the five different temperatures. The results are given in the table and picture below.

Raw glucose Glucose for raw temperature 7124 [mg/dl] Glucose for raw temperature 6420 [mg/dl] Glucose for raw temperature 6300 [mg/dl] Glucose for raw temperature 6144 [mg/dl] Glucose for raw temperature 5816 [mg/dl]
700 58 50 49 47 44
1000 92 81 79 77 72
1500 148 132 129 126 118
2000 205 183 180 175 165
2500 262 235 230 224 211
3000 318 286 280 273 257

text

As can be seen, the relationship is linear for each temperature set, but the slopes are different. So, unfortunately, there is no general constant slope for a Freestyle Libre sensor that could be just easily used independently of temperature. With this linear relationship it is possible to calculate slope and offset individually for each set, just as it was done in the above section. This yields

Raw temperature 7124 6420 6300 6144 5816
Slope [mg/dl] 0,1130 0,1026 0,1004 0,0983 0,0926
Offset [mg/dl] -21,1304 -21,8261 -21,3043 -21,7826 -20,8261

Contrary to the slopes, the offsets are pretty much in the same range of ca. 21 mg/dl.

The „transposed“ question is, whether the glucose values are linear with respect to raw temperature. The following picture shows the corresponding graph, where the glucose values are printed over raw temperature.

text

The results look linear, but focusing on just the results for raw glucose = 700 one gets the following graph.

text

The points are not exactly on a straight line due to the fact, that the LibreOOP algorithm only returns integer numbers without any fractional digits. But as can be seen the linearity assumption is a good approximation.

To sum up: We have

  1. a linear glucose response by the LibreOOP algorithm with respect to raw glucose variations, when raw temperature is kept constant, and
  2. a linear glucose response by the LibreOOP algorithm with respect to raw temperature variations, when raw glucose is kept constant.

Overall we are interested in an algorithm that allows to calculate glucose from one or more records of six bytes. With the above simplification of only equal and constant values, this is reduced to finding an algorithm that uses raw glucose and raw temperature of only one record of six bytes. Since there are two linear relationships we can try the approach to interpolate the slope and offset over raw temperature and thus get the individual slope and offset for each raw temperature. Therefore, the following graph shows the slope and offset from the above table with respect to raw temperature.

text

The slope is obviously linear with respect to raw temperature. Although the offset does not look really linear, the deviations between the offset values are within +- 1 mg/dl and can be considered minor and a linear approach is for offset is possible, too.

So in order to include raw temperature into an algorithm, slope and offset are linearly interpolated with respect to raw temperature, which then allows for glucose calculation using raw temperature and raw glucose from

       glucose = slope(raw_temperature) * raw_glucose + offset(raw_temperature)

Where slope(raw_temperature) and offset(raw_temperature) themselves are derived from the linear approximation

       slope(raw_temperature) = slopeslope * raw_temperature + offsetslope

       offset(raw_temperature) = slopeoffset * raw_temperature + offsetoffset.

One could argue whether it would be easier to just use a constant offset, but the more general approach is used here.

Slopeslope and offsetslope are calculated from two sets of data [(raw_temperature1; slope1); (raw_temperature2; slope2)] (using libre office), e.g. [(5816; 0.0926); (7124; 0.113)] as

       slopeslope = (slope2 - slope1) / (raw_temperature2 - raw_temperature1)

       slopeslope = (0.113 - 0.0926) / (7124 - 5816) = 0.000015623

and

       offsetslope = slope2 - slopeslope * raw_temperature2

       offsetslope = 0.113 - 0.000015623 * 7214 = 0.0017457

and one finally gets

       slope(raw_temperature) = 0,000015623 * raw_temperature + 0.0017457.

Slopeoffset and offsetoffset are are calculated similarly from two sets of data [(raw_temperature1; offset1); (raw_temperature2; offset2)] (using libre office), e.g. [(5816; -20.8261); (7124; -21.1304)] as

       slopeoffset = -0.0002327        offsetoffset = -19.47

and finally

       offset(raw_temperature) = -0.0002327 * raw_temperature -19.47.

Using this equation one gets slope and offset for the raw temperatures from above as listed in the following table:

Raw temperature 7124 6420 6300 6144 5816
Interpolated slope [mg/dl] 0.1130 0.1020 0.1002 0.0977 0.0926
Interpolated offset [mg/dl] -21.1304 -21.9666 -20.9387 -20.9024 -20.8261

In order to find out how good the approximation is, these slopes and offsets where used to calculate a glucose value from raw glucose and (and thus also from raw temperature) and listed in the following table. The glucose values where rounded and differences shown in parentheses, for cases where the results were not identical to the LibreOOP glucose value.

Raw glucose Calculated glucose for raw temperature 7124 [mg/dl] Calculated glucose for raw temperature 6420 [mg/dl] Calculated glucose for raw temperature 6300 [mg/dl] Calculated glucose for raw temperature 6144 [mg/dl] Calculated glucose for raw temperature 5816 [mg/dl]
700 58 50 49 48 (+1) 44
1000 92 81 79 77 72
1500 148 132 129 126 118
2000 205 183 179 (-1) 175 164 (-1)
2500 261 (-1) 234 (-1) 229 (-1) 223 (-1) 211
3000 318 285 (-1) 280 272 (-1) 257

The largest deviation is 1 mg/dl which accounts for eight of 30 values, all other values are the same, which is a very good approximation for the libreOOP glucose values.

One might be tempted to use the equations derived in this section as an algorithm to calculate glucose from raw glucose and raw temperature. In general this could be done, but the parameters were only calculated for the header and footer data of one single sensor. Thus it is not yet clear, if these parameters are also valid for any other sensor.

Sensor Dependence

To understand the effect of using a different sensor on the parameters for the derived equations, the same tests as above where made with data from six other different sensors. To be more precise: their header and footer data was used and the body data was tweaked as described in the above sections. From these overal seven sensors, the two sensors with the most extreme results (highest glucose value responses from LibreOOP algorithm and lowest glucose value responses from LibreOOP algorithm) where chosen and the corresponding data is displayed below. To reduce the amount of data only a subset of three raw glucose values (1000, 2000 and 3000) and three raw temperatures (7124, 6300, and 5816) was used.

Sensor with lowest values

The header bytes are

A4 1F 98 16 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

and the footer bytes are

8C EF 00 01 B6 05 04 51, 14 07 96 80 5A 00 ED A6 12 7D 1A C8 04 C0 69 7A

The glucose values returned by LibreOOP are

Raw glucose Glucose for raw temperature 7124 [mg/dl] Glucose for raw temperature 6300 [mg/dl] Glucose for raw temperature 5816 [mg/dl]
1000 78 67 60
2000 177 155 141
3000 277 243 223

Slope and offset for each raw temperature if using the equation

       glucose = slope * raw_glucose + offset

are

Raw temperature 7124 6300 5816
Slope [mg/dl] 0.0995 0.0882 0.0815
Offset [mg/dl] -21.50 -21.50 -21.50

The equations to calculate glucose from raw glucose and raw temperature are

       glucose = slope(raw_temperature) * raw_glucose + offset(raw_temperature)

where

       slope(raw_temperature) = 0,00001376 * raw_temperature + 0.0014633 and

       offset(raw_temperature) = 0 * raw_temperature -21.50.

Sensor with highest values

The header bytes are

50 7A 88 13 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

and the footer bytes are

6C 0C 00 01 D3 04 7D 51 14 07 96 80 5A 00 ED A6 14 76 1A C8 04 E4 39 6C
Glucose values

The glucose values returned by LibreOOP are

Raw glucose Glucose for raw temperature 7124 [mg/dl] Glucose for raw temperature 6300 [mg/dl] Glucose for raw temperature 5816 [mg/dl]
1000 101 87 79
2000 226 198 181
3000 351 309 284

Slope and offset for each raw temperature if using the equation

       glucose = slope * raw_glucose + offset

are

Raw temperature 7124 6300 5816
Slope [mg/dl] 0.1252 0.1110 0.1026
Offset [mg/dl] -24.65 -24.13 -23.83

The equations to calculate glucose from raw glucose and raw temperature are

       glucose = slope(raw_temperature) * raw_glucose + offset(raw_temperature)

where

       slope(raw_temperature) = 0.00001729 * raw_temperature + 0.002080 and

       offset(raw_temperature) = -0,0006316 * raw_temperature -20.15.

Difference

The difference of the glucose values is given in the table below, in absolute values and percentages.

Raw glucose Glucose for raw temperature 7124 [mg/dl (%)] Glucose for raw temperature 6300 [mg/dl (%)] Glucose for raw temperature 5816 [mg/dl (%)]
1000 23 (29%) 20 (30%) 19 (32%)
2000 49 (28%) 43 (28%) 40 (28%)
3000 74 (27%) 66 (27%) 61 (27%)

The difference is in the range of 30%. Conclusion is that each sensor has its individual parameters to calculate glucose from raw glucose and raw temperature and this must be taken into account for any such approach. Since only seven sensors where considered for this analysis, the difference might be much higher for any other sensor.

Unfortunately, I have no clue, how header and footer data could be used to compensate for this effect. Any ideas on this are welcome.

Note

Note, that although these results look very good, they are made and thus are only valid under/for the following assumptions

  • Constant glucose values and no dynamic changes of any kind.
  • Temperature is in a valid range. The borders of this range have yet to be determined.
  • The equation parameters are valid for an individual sensor (and its individual header and footer data). The parameters are different for any other sensor (with different header and footer data) and the difference is significant.

Further unknowns are:

  • Flags are not yet understood. Investigation needed on the other bits and bytes of the six bytes records.
  • Header and footer not yet understood. Some quick experiments with tweaking many of the header and footer bytes (just one at a time) showed that LibreOOP glucose response mostly changes dramatically.

Algorithm

Despite these assumptions and unknowns, one can still think about an algorithm based on the results of this investigation. One possibility would be to get the LibreOOP glucose response for four artificial data points (two different raw glucose values and two different raw temperatures) and calculate slope and offset functions as described. Then use another e.g. two data points and their LibreOOP glucose response and compare with algorithm calculated results to validate if everything is fine. This would be valid for the current sensor and would need to be repeated with every new sensor. It would also require internet access and access to SwiftLibreOOPWeb or a similar service.

Disclaimer

Everything that is written here are just the results of my personal investigations and they could be totally wrong. There is no collaboration of any kind with Abbott. Use at your own risk.

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.