HOBO logger calibrations

Two loggers per temperature bath (2 ambient, 2 hot)

protocols
records
Author

Sarah Tanja

Published

July 10, 2024

Modified

September 1, 2025

1 Background

On July 10th I calibrated the HOBO logs to a Kessler NIST K750 RTD Thermometer and an apogee instruments underwater quantum flux PAR meter.

Each was a 2 point calibration matching time-stamp to reference instrument read-out.

NIST temp & time 27.03 , 07/10/2024 10:40:00 27.26, 07/10/2024 11:00:00

Apogee par & time 1600, 07/10/2024 11:10:00 760, 07/10/2024 11:40:00

1.1 Inputs

1.2 Outputs

2 Setup

2.1 Install packages

if ("tidyverse" %in% rownames(installed.packages()) == 'FALSE') install.packages('tidyverse')
if ("broom" %in% rownames(installed.packages()) == 'FALSE') install.packages('broom')

2.2 Load libraries

library(tidyverse)
library(broom) # for tidy()

2.3 Load HOBO logs

amb1_blue <- read.csv("Amb1_blue 2024-07-10 13_10_13 HST (Data HST).csv")
amb1_bluegreen <- read.csv("Amb1_bluegreen 2024-07-10 13_10_51 HST (Data HST).csv")
amb2_orangepink <- read.csv("Amb2_orangepink 2024-07-10 11_47_18 HST (Data HST).csv")
amb2_pink <- read.csv("Amb2_pink 2024-07-10 11_48_03 HST (Data HST).csv")
hot1_black <- read.csv("Hot1_black 2024-07-10 13_09_24 HST (Data HST).csv")
hot1_green <- read.csv("Hot1_green 2024-07-10 13_08_26 HST (Data HST).csv")
hot2_orange <- read.csv("Hot2_orange 2024-07-10 11_44_18 HST (Data HST).csv")
hot2_yellow <- read.csv("Hot2_yellow 2024-07-10 11_46_08 HST (Data HST).csv")

3 References

# Example inputs for temperature calibration
ref_temp <- c(27.03, 27.26)  # NIST temperatures
ref_time_temp <- as.POSIXct(c("07/10/2024 10:40:00", "07/10/2024 11:00:00"),
                            format="%m/%d/%Y %H:%M:%S")

# For PAR calibration
ref_par <- c(1600, 760)
ref_time_par <- as.POSIXct(c("07/10/2024 11:10:00", "07/10/2024 11:40:00"),
                           format="%m/%d/%Y %H:%M:%S")

cal_table <- tibble::tibble(
  datetime = as.POSIXct(c("2024-07-10 10:40:00", "2024-07-10 11:00:00",
                          "2024-07-10 11:10:00", "2024-07-10 11:40:00")),
  ref_temp = c(27.03, 27.26, NA, NA),  # NIST
  ref_par  = c(NA, NA, 1600, 760)      # Apogee PAR
)

4 Format logs

  1. amb1_blue
amb1_blue <- amb1_blue %>% 
  rename(datetime = Date.Time..HST.,
         temp = Temperature.....C.,
         lux = Light....lux.) %>%
  mutate(datetime = as.POSIXct(datetime, format = "%m/%d/%Y %H:%M:%S")) %>% 
  mutate(pendent = "amb1_blue") %>% 
  select(datetime, temp, lux, pendent) %>% 
  filter(datetime %in% c(ref_time_temp, ref_time_par))
  1. amb1_bluegreen
amb1_bluegreen <- amb1_bluegreen %>% 
  rename(datetime = Date.Time..HST.,
         temp = Temperature.....C.,
         lux = Light....lux.) %>%
  mutate(datetime = as.POSIXct(datetime, format = "%m/%d/%Y %H:%M:%S")) %>% 
  mutate(pendent = "amb1_bluegreen") %>% 
  select(datetime, temp, lux, pendent) %>% 
  filter(datetime %in% c(ref_time_temp, ref_time_par))
  1. amb2_orangepink
amb2_orangepink <- amb2_orangepink %>% 
  rename(datetime = Date.Time..HST.,
         temp = Temperature.....C.,
         lux = Light....lux.) %>%
  mutate(datetime = as.POSIXct(datetime, format = "%m/%d/%Y %H:%M:%S")) %>% 
  mutate(pendent = "amb2_orangepink") %>% 
  select(datetime, temp, lux, pendent) %>% 
  filter(datetime %in% c(ref_time_temp, ref_time_par))
  1. amb2_pink
amb2_pink <- amb2_pink %>% 
  rename(datetime = Date.Time..HST.,
         temp = Temperature.....C.,
         lux = Light....lux.) %>%
  mutate(datetime = as.POSIXct(datetime, format = "%m/%d/%Y %H:%M:%S")) %>% 
  mutate(pendent = "amb2_pink") %>% 
  select(datetime, temp, lux, pendent) %>% 
  filter(datetime %in% c(ref_time_temp, ref_time_par))
  1. hot1_black
hot1_black <- hot1_black %>% 
  rename(datetime = Date.Time..HST.,
         temp = Temperature.....C.,
         lux = Light....lux.) %>%
  mutate(datetime = as.POSIXct(datetime, format = "%m/%d/%Y %H:%M:%S")) %>% 
  mutate(pendent = "hot1_black") %>% 
  select(datetime, temp, lux, pendent) %>% 
  filter(datetime %in% c(ref_time_temp, ref_time_par))
  1. hot1_green
hot1_green <- hot1_green %>% 
  rename(datetime = Date.Time..HST.,
         temp = Temperature.....C.,
         lux = Light....lux.) %>%
  mutate(datetime = as.POSIXct(datetime, format = "%m/%d/%Y %H:%M:%S")) %>% 
  mutate(pendent = "hot1_green") %>% 
  select(datetime, temp, lux, pendent) %>% 
  filter(datetime %in% c(ref_time_temp, ref_time_par))
  1. hot2_orange
hot2_orange <- hot2_orange %>% 
  rename(datetime = Date.Time..HST.,
         temp = Temperature.....C.,
         lux = Light....lux.) %>%
  mutate(datetime = as.POSIXct(datetime, format = "%m/%d/%Y %H:%M:%S")) %>% 
  mutate(pendent = "hot2_orange") %>% 
  select(datetime, temp, lux, pendent)%>% 
  filter(datetime %in% c(ref_time_temp, ref_time_par))
  1. hot2_yellow
hot2_yellow <- hot2_yellow %>% 
  rename(datetime = Date.Time..HST.,
         temp = Temperature.....C.,
         lux = Light....lux.) %>%
  mutate(datetime = as.POSIXct(datetime, format = "%m/%d/%Y %H:%M:%S")) %>% 
  mutate(pendent = "hot2_yellow") %>% 
  select(datetime, temp, lux, pendent) %>% 
  filter(datetime %in% c(ref_time_temp, ref_time_par))

Bind logs

cal_logs <- rbind(amb1_blue,
                  amb1_bluegreen,
                  amb2_orangepink,
                  amb2_pink,
                  hot1_black,
                  hot1_green,
                  hot2_orange,
                  hot2_yellow)

5 Create calibrations

5.1 Temp

For each sensor, calculate slope and intercept using the two point calibration

# Define your calibration temperature reference times
cal_temp_times <- as.POSIXct(c("2024-07-10 10:40:00", "2024-07-10 11:00:00"))

# Sample dataframe 'cal_logs' containing datetime, temp, lux, pendent

# Step 1: Filter calibration temp points only
temp_cal_points <- cal_logs %>%
  filter(datetime %in% cal_temp_times) %>%
  select(pendent, datetime, temp) %>%
  mutate(ref_temp = ifelse(datetime == cal_temp_times[1], 27.03, 27.26))

# Step 2: Group by sensor and fit linear model temp ~ ref_temp
temp_fit_results <- temp_cal_points %>%
  group_by(pendent) %>%
  summarise(
    model = list(lm(ref_temp ~ temp, data = cur_data()))
  ) %>%
  mutate(
    coef = map(model, tidy)
  ) %>%
  unnest(coef) %>%
  select(pendent, term, estimate) %>%
  pivot_wider(names_from = term, values_from = estimate) %>%
  rename(intercept = `(Intercept)`, slope = temp)

# fit_results now contains one row per sensor with slope and intercept ready to use
print(temp_fit_results)
# A tibble: 8 × 3
  pendent         intercept slope
  <chr>               <dbl> <dbl>
1 amb1_blue            19.8 0.256
2 amb1_bluegreen       19.1 0.284
3 amb2_orangepink      18.2 0.315
4 amb2_pink            19.1 0.284
5 hot1_black           19.5 0.271
6 hot1_green           18.3 0.315
7 hot2_orange          19.5 0.267
8 hot2_yellow          17.6 0.338

Average temp calibration

avg_temp <- temp_fit_results %>%
  summarize(
    avg_temp_intercept = mean(intercept),
    avg_temp_slope = mean(slope))

avg_temp
avg_temp_intercept avg_temp_slope
18.89328 0.2912324

5.2 PAR

# Define your calibration PAR reference times
cal_par_times <- as.POSIXct(c("2024-07-10 11:10:00", "2024-07-10 11:40:00"))

# Sample dataframe 'cal_logs' containing datetime, temp, lux, pendent

# Step 1: Filter calibration par points only
par_cal_points <- cal_logs %>%
  filter(datetime %in% cal_par_times) %>%
  select(pendent, datetime, lux) %>%
  mutate(ref_par = ifelse(datetime == cal_par_times[1], 1600, 760))

# Step 2: Group by sensor and fit linear model lux ~ ref_par
par_fit_results <- par_cal_points %>%
  group_by(pendent) %>%
  summarise(
    model = list(lm(ref_par ~ lux, data = cur_data()))
  ) %>%
  mutate(
    coef = map(model, tidy)
  ) %>%
  unnest(coef) %>%
  select(pendent, term, estimate) %>%
  pivot_wider(names_from = term, values_from = estimate) %>%
  rename(intercept = `(Intercept)`, slope = lux)

# fit_results now contains one row per sensor with slope and intercept ready to use
print(par_fit_results)
# A tibble: 8 × 3
  pendent         intercept  slope
  <chr>               <dbl>  <dbl>
1 amb1_blue          -1400. 0.0626
2 amb1_bluegreen     -1428. 0.0727
3 amb2_orangepink     -776. 0.0573
4 amb2_pink          -4905. 0.158 
5 hot1_black         -1427. 0.0697
6 hot1_green         -1797. 0.0868
7 hot2_orange        -1968. 0.0805
8 hot2_yellow        -1998. 0.0957

Average PAR calibration

avg_par <- par_fit_results %>%
  summarize(
    avg_par_intercept = mean(intercept),
    avg_par_slope = mean(slope))

avg_par
avg_par_intercept avg_par_slope
-1962.403 0.0853845

6 Apply calibrations

To apply the intercept and slope from your linear regression calibration to raw data and generate calibrated values, you use the formula:

\[ \text{Calibrated value} = (\text{slope} \times \text{raw value}) + \text{intercept} \]

7 Summary