The hardware and bandwidth for this mirror is donated by METANET, the Webhosting and Full Service-Cloud Provider.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]metanet.ch.
LightFitR is an R package for designing complex light regimes with LED lights. Often, these light fixtures are programmed with ‘intensity’ units, which often does not scale linearly with the actual measured light output from the light fixtures. Further, if using multiple wavelength channels, there will often be bleedthrough between the channels, affecting the quality and quantity of light received by your experimental subjects. Our package aims to combat both of these challenges. It takes calibration data and user-defined target irradiances and it tells you what intensities to use in order to achieve those irradiances.
Note that this package does not support broad spectrum white LEDs
This package uses ‘intensity’ to mean the unitless settings that the light fixture uses.
‘irradiance’ means the measured light output from the fixture.This can be in the users’ preferred light units, provided it is used consistently throughout.
‘regime’ refers to a repeating daily schedule that the light fixtures run on.
‘event’ refers to the point in the light regime when the lights change intensity.
You will need several inputs:
Your calibration data must have these 4 columns:
For more information about how to collect calibration data, see https://doi.org/10.1101/2025.06.06.658293
Here is an example of what the calibration data could look like:
calibration <- LightFitR::calibration
head(calibration)
#> filename time
#> 7780 Apollo_Calib_20240827__AbsoluteIrradiance__103__00-52-02-433.txt 00:52:02
#> 7781 Apollo_Calib_20240827__AbsoluteIrradiance__103__00-52-02-433.txt 00:52:02
#> 7782 Apollo_Calib_20240827__AbsoluteIrradiance__103__00-52-02-433.txt 00:52:02
#> 7783 Apollo_Calib_20240827__AbsoluteIrradiance__103__00-52-02-433.txt 00:52:02
#> 7784 Apollo_Calib_20240827__AbsoluteIrradiance__103__00-52-02-433.txt 00:52:02
#> 7785 Apollo_Calib_20240827__AbsoluteIrradiance__103__00-52-02-433.txt 00:52:02
#> led intensity wavelength irradiance
#> 7780 380 500 300.001 0.01
#> 7781 380 500 300.213 0.02
#> 7782 380 500 300.426 -0.01
#> 7783 380 500 300.639 -0.03
#> 7784 380 500 300.852 -0.01
#> 7785 380 500 301.064 0.02
The target irradiances are a set of irradiances that you wish to achieve for your experiment.
This should be a matrix, with rows representing LED channels and columns representing timepoints or events.
For example:
target <- LightFitR::target_irradiance
print(target)
#> [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
#> 380nm 2.8 3.6 2.6 0.4 1.8 3.4 2.0 0.8 3.8 2.6
#> 400nm 4.8 17.8 18.0 13.6 18.0 11.2 18.2 1.6 18.4 19.6
#> 420nm 14.2 5.0 1.2 8.2 1.6 16.4 7.0 15.4 16.0 8.4
#> 450nm 20.4 23.2 15.0 28.4 6.2 21.6 1.2 27.2 33.6 14.6
#> 530nm 4.4 5.2 11.8 10.4 1.2 10.4 5.2 19.0 7.4 17.6
#> 620nm 0.2 0.8 1.4 2.2 2.4 3.4 0.0 4.8 4.8 1.0
#> 660nm 4.0 15.6 8.0 9.2 17.8 11.8 18.8 3.0 23.0 18.6
#> 735nm 1.0 14.2 17.0 17.0 7.6 6.0 16.0 9.8 6.6 0.6
#> 5700k 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
This should be a vector with a length the same as the column number of your target irradiances.
Each event timepoint should be in the POSIXct format. The date can be
arbitrary as the package only cares about the timestamp. We recommend
the lubridate
package for working with times.
For example:
times <- LightFitR::time_vector
print(times)
#> [1] "1970-01-01 00:00:00 GMT" "1970-01-01 00:05:00 GMT"
#> [3] "1970-01-01 00:10:00 GMT" "1970-01-01 00:15:00 GMT"
#> [5] "1970-01-01 00:20:00 GMT" "1970-01-01 00:25:00 GMT"
#> [7] "1970-01-01 00:30:00 GMT" "1970-01-01 00:35:00 GMT"
#> [9] "1970-01-01 00:40:00 GMT" "1970-01-01 00:45:00 GMT"
Now you have all the inputs, let’s make a regime:
regime <- makeRegime(times, target, calibration$led, calibration$wavelength, calibration$intensity, calibration$irradiance)
#> Ranges fall within irradiances acheivable by heliospectra: TRUE
#> Warning in internal.closestWavelength(unique(calibration_df$wavelength), : We
#> couldn't find exact matches with the peak wavelengths specified. Returning the
#> closest wavelengths
#> Warning in internal.closestWavelength(unique(calib$wavelength), peaks): We
#> couldn't find exact matches with the peak wavelengths specified. Returning the
#> closest wavelengths
#> Ranges fall within irradiances acheivable by heliospectra: TRUE
print(regime)
#> 00:00:00 00:05:00 00:10:00 00:15:00 00:20:00 00:25:00
#> time "00:00:00" "00:05:00" "00:10:00" "00:15:00" "00:20:00" "00:25:00"
#> hour "0" "0" "0" "0" "0" "0"
#> minute "0" "5" "10" "15" "20" "25"
#> second "0" "0" "0" "0" "0" "0"
#> 380nm "963" "1000" "1000" "74" "474" "1000"
#> 400nm "517" "1000" "1000" "1000" "1000" "1000"
#> 420nm "385" "0" "0" "0" "0" "160"
#> 450nm "493" "633" "278" "856" "51" "549"
#> 530nm "62" "86" "671" "485" "22" "484"
#> 620nm "6" "49" "43" "777" "821" "600"
#> 660nm "48" "499" "98" "176" "601" "292"
#> 735nm "12" "838" "1000" "1000" "278" "166"
#> 5700k "0" "0" "0" "0" "0" "0"
#> 00:30:00 00:35:00 00:40:00 00:45:00
#> time "00:30:00" "00:35:00" "00:40:00" "00:45:00"
#> hour "0" "0" "0" "0"
#> minute "30" "35" "40" "45"
#> second "0" "0" "0" "0"
#> 380nm "817" "50" "1000" "934"
#> 400nm "1000" "54" "1000" "1000"
#> 420nm "0" "525" "0" "0"
#> 450nm "0" "779" "1000" "261"
#> 530nm "61" "1000" "201" "1000"
#> 620nm "0" "629" "527" "9"
#> 660nm "710" "34" "886" "627"
#> 735nm "1000" "390" "177" "0"
#> 5700k "0" "0" "0" "0"
As you can see, the makeRegime
function automatically
creates a heatmap of your regime to allow the user to sanity check the
intensity output.
It also creates a matrix of intensities for you to set your lights to. The layout is very similar to that of the target matrix. But with extra rows for the timepoints, since some models of lights requires that.
Currently, we can only export regimes compatible with Heliospectra DYNA(TM) lights running the R3.2.2-Release firmware.
If you do not use this model of light, you can still write the regime matrix to a csv. Or you’re very welcome to write your own function to format the regime to be compatible with your model of lights!
makeRegime
is the core user-facing function in this
package. But you probably want to know what is happening behind the
scenes. We will summarise below, and full explanation is available at:
https://doi.org/10.1101/2025.06.06.658293
When you run makeRegime
, it carries out 4 steps in the
background:
We’ll go through each of the steps below.
This step searches through the calibration data to find intensities which produce the closest irradiances to your target irradiance.
Using the example target irradiances from above:
closest <- LightFitR::internal.closestIntensities(target, calibration[, c(3,5,4,6)])
#> Warning in internal.closestWavelength(unique(calibration_df$wavelength), : We
#> couldn't find exact matches with the peak wavelengths specified. Returning the
#> closest wavelengths
rownames(closest) <- LightFitR::helio.dyna.leds$name
print(closest)
#> [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
#> 380nm 1000 1000 800 50 400 1000 400 100 1000 800
#> 400nm 600 1000 1000 1000 1000 1000 1000 100 1000 1000
#> 420nm 600 100 20 200 20 700 200 600 700 200
#> 450nm 500 700 300 900 50 600 10 800 1000 300
#> 530nm 50 100 700 500 20 500 100 1000 200 1000
#> 620nm 10 20 50 1000 1000 700 0 500 500 50
#> 660nm 50 500 100 200 600 300 600 50 900 600
#> 735nm 20 900 1000 1000 300 200 1000 400 200 10
#> 5700k 0 0 0 0 0 0 0 0 0 0
We’ve got a matrix of intensities for each channel and event!
To convince ourselves that these are indeed the closest, we can compare our target irradiances with the calibration data. Let’s do this for the first event:
# Define variables
## Calibration
calib_wavelengths <- unique(calibration$wavelength)
calib_intensities <- unique(calibration$intensity)
## Subset the closest matrix to the first event
closest_first <- closest[,1]
print(closest_first)
#> 380nm 400nm 420nm 450nm 530nm 620nm 660nm 735nm 5700k
#> 1000 600 600 500 50 10 50 20 0
## Subset the targets
target_first <- target[,1]
print(target_first)
#> 380nm 400nm 420nm 450nm 530nm 620nm 660nm 735nm 5700k
#> 2.8 4.8 14.2 20.4 4.4 0.2 4.0 1.0 0.0
# Go through each channel of the first event
sanity_check <- sapply(1:length(closest_first), function(i){
## Set relevant variables
tar <- target_first[i]
clo <- closest_first[i]
led <- helio.dyna.leds[i, 'wavelength']
## Subset calibration data to the LED at the peak wavelengths
criteria <- calibration$led==led & calibration$wavelength==LightFitR:::internal.closestWavelength(calib_wavelengths, led)
calib_subset <- calibration[criteria, 3:6]
# Print outputs for user
print(names(clo))
print('This is the calibration data')
print(calib_subset)
print(paste('The target irradiance is', tar))
print(paste('The closest intensity is', clo))
print('---')
return()
})
#> Warning in LightFitR:::internal.closestWavelength(calib_wavelengths, led): We
#> couldn't find exact matches with the peak wavelengths specified. Returning the
#> closest wavelengths
#> [1] "380nm"
#> [1] "This is the calibration data"
#> led intensity wavelength irradiance
#> 8161 380 500 379.936 2.14
#> 21126 380 600 379.936 2.29
#> 34091 380 700 379.936 2.48
#> 49649 380 800 379.936 2.59
#> 60021 380 900 379.936 2.73
#> 72986 380 1000 379.936 2.79
#> 1120558 380 0 379.936 -0.02
#> 1291696 380 1 379.936 -0.03
#> 1405788 380 5 379.936 0.03
#> 1548403 380 10 379.936 0.08
#> 1691018 380 20 379.936 0.14
#> 1812889 380 50 379.936 0.48
#> 1823261 380 100 379.936 0.93
#> 1836226 380 200 379.936 1.37
#> 1849191 380 300 379.936 1.65
#> 1864749 380 400 379.936 1.90
#> [1] "The target irradiance is 2.8"
#> [1] "The closest intensity is 1000"
#> [1] "---"
#> Warning in LightFitR:::internal.closestWavelength(calib_wavelengths, led): We
#> couldn't find exact matches with the peak wavelengths specified. Returning the
#> closest wavelengths
#> [1] "400nm"
#> [1] "This is the calibration data"
#> led intensity wavelength irradiance
#> 86048 400 0 399.932 -0.03
#> 99013 400 1 399.932 -0.02
#> 111978 400 5 399.932 0.03
#> 124943 400 10 399.932 0.14
#> 137908 400 20 399.932 0.31
#> 150873 400 50 399.932 0.86
#> 163838 400 100 399.932 1.78
#> 176803 400 200 399.932 2.40
#> 189768 400 300 399.932 3.07
#> 202733 400 400 399.932 3.61
#> 215698 400 500 399.932 4.26
#> 228663 400 600 399.932 4.81
#> 241628 400 700 399.932 5.35
#> 254593 400 800 399.932 5.79
#> 267558 400 900 399.932 6.12
#> 280523 400 1000 399.932 6.58
#> [1] "The target irradiance is 4.8"
#> [1] "The closest intensity is 600"
#> [1] "---"
#> Warning in LightFitR:::internal.closestWavelength(calib_wavelengths, led): We
#> couldn't find exact matches with the peak wavelengths specified. Returning the
#> closest wavelengths
#> [1] "420nm"
#> [1] "This is the calibration data"
#> led intensity wavelength irradiance
#> 293586 420 0 419.989 -0.09
#> 306551 420 1 419.989 -0.05
#> 319516 420 5 419.989 0.18
#> 332481 420 10 419.989 0.46
#> 345446 420 20 419.989 1.09
#> 358411 420 50 419.989 3.09
#> 371376 420 100 419.989 5.83
#> 384341 420 200 419.989 7.75
#> 397306 420 300 419.989 9.55
#> 410271 420 400 419.989 11.30
#> 423236 420 500 419.989 13.09
#> 436201 420 600 419.989 14.59
#> 449166 420 700 419.989 16.24
#> 462131 420 800 419.989 17.63
#> 475096 420 900 419.989 18.99
#> 488061 420 1000 419.989 20.31
#> [1] "The target irradiance is 14.2"
#> [1] "The closest intensity is 600"
#> [1] "---"
#> [1] "450nm"
#> [1] "This is the calibration data"
#> led intensity wavelength irradiance
#> 501174 450 0 450 -0.01
#> 516732 450 1 450 0.12
#> 527104 450 5 450 0.47
#> 540069 450 10 450 0.87
#> 553034 450 20 450 1.96
#> 568592 450 50 450 5.01
#> 578964 450 100 450 9.50
#> 591929 450 200 450 12.04
#> 604894 450 300 450 14.74
#> 617859 450 400 450 17.24
#> 630824 450 500 450 19.67
#> 643789 450 600 450 22.06
#> 656754 450 700 450 24.20
#> 669719 450 800 450 26.39
#> 682684 450 900 450 28.55
#> 695649 450 1000 450 30.61
#> [1] "The target irradiance is 20.4"
#> [1] "The closest intensity is 500"
#> [1] "---"
#> Warning in LightFitR:::internal.closestWavelength(calib_wavelengths, led): We
#> couldn't find exact matches with the peak wavelengths specified. Returning the
#> closest wavelengths
#> [1] "530nm"
#> [1] "This is the calibration data"
#> led intensity wavelength irradiance
#> 709017 530 0 530.021 0.02
#> 721982 530 1 530.021 0.02
#> 734947 530 5 530.021 0.26
#> 747912 530 10 530.021 0.59
#> 760877 530 20 530.021 1.27
#> 773842 530 50 530.021 3.44
#> 786807 530 100 530.021 5.94
#> 799772 530 200 530.021 7.10
#> 812737 530 300 530.021 8.42
#> 825702 530 400 530.021 9.45
#> 838667 530 500 530.021 10.53
#> 851632 530 600 530.021 11.38
#> 864597 530 700 530.021 12.19
#> 877562 530 800 530.021 12.89
#> 890527 530 900 530.021 13.69
#> 903492 530 1000 530.021 14.43
#> [1] "The target irradiance is 4.4"
#> [1] "The closest intensity is 50"
#> [1] "---"
#> Warning in LightFitR:::internal.closestWavelength(calib_wavelengths, led): We
#> couldn't find exact matches with the peak wavelengths specified. Returning the
#> closest wavelengths
#> [1] "620nm"
#> [1] "This is the calibration data"
#> led intensity wavelength irradiance
#> 916926 620 0 620.021 -0.06
#> 929891 620 1 620.021 0.02
#> 942856 620 5 620.021 0.07
#> 955821 620 10 620.021 0.21
#> 968786 620 20 620.021 0.47
#> 981751 620 50 620.021 1.38
#> 994716 620 100 620.021 2.61
#> 1007681 620 200 620.021 3.04
#> 1020646 620 300 620.021 3.48
#> 1033611 620 400 620.021 3.60
#> 1046576 620 500 620.021 3.69
#> 1059541 620 600 620.021 3.67
#> 1075099 620 700 620.021 3.46
#> 1088064 620 800 620.021 3.20
#> 1103622 620 900 620.021 2.90
#> 1116587 620 1000 620.021 2.51
#> [1] "The target irradiance is 0.2"
#> [1] "The closest intensity is 10"
#> [1] "---"
#> Warning in LightFitR:::internal.closestWavelength(calib_wavelengths, led): We
#> couldn't find exact matches with the peak wavelengths specified. Returning the
#> closest wavelengths
#> [1] "660nm"
#> [1] "This is the calibration data"
#> led intensity wavelength irradiance
#> 1132359 660 0 659.97 -0.02
#> 1145324 660 1 659.97 0.01
#> 1160882 660 5 659.97 0.27
#> 1173847 660 10 659.97 0.69
#> 1189405 660 20 659.97 1.46
#> 1202370 660 50 659.97 4.07
#> 1217928 660 100 659.97 8.05
#> 1230893 660 200 659.97 10.15
#> 1246451 660 300 659.97 11.97
#> 1259416 660 400 659.97 13.70
#> 1274974 660 500 659.97 15.58
#> 1287939 660 600 659.97 17.59
#> 1303497 660 700 659.97 20.06
#> 1316462 660 800 659.97 21.67
#> 1334613 660 900 659.97 23.29
#> 1344985 660 1000 659.97 24.87
#> [1] "The target irradiance is 4"
#> [1] "The closest intensity is 50"
#> [1] "---"
#> Warning in LightFitR:::internal.closestWavelength(calib_wavelengths, led): We
#> couldn't find exact matches with the peak wavelengths specified. Returning the
#> closest wavelengths
#> [1] "735nm"
#> [1] "This is the calibration data"
#> led intensity wavelength irradiance
#> 1360956 735 0 735.086 -0.02
#> 1373921 735 1 735.086 0.02
#> 1392072 735 5 735.086 0.14
#> 1402444 735 10 735.086 0.50
#> 1418002 735 20 735.086 1.01
#> 1430967 735 50 735.086 2.62
#> 1449118 735 100 735.086 5.26
#> 1459490 735 200 735.086 6.55
#> 1475048 735 300 735.086 7.66
#> 1488013 735 400 735.086 9.39
#> 1506164 735 500 735.086 10.64
#> 1516536 735 600 735.086 11.74
#> 1532094 735 700 735.086 12.62
#> 1545059 735 800 735.086 13.78
#> 1563210 735 900 735.086 14.45
#> 1573582 735 1000 735.086 15.40
#> [1] "The target irradiance is 1"
#> [1] "The closest intensity is 20"
#> [1] "---"
#> Warning in LightFitR:::internal.closestWavelength(calib_wavelengths, led): We
#> couldn't find exact matches with the peak wavelengths specified. Returning the
#> closest wavelengths
#> [1] "5700k"
#> [1] "This is the calibration data"
#> led intensity wavelength irradiance
#> 1589509 5700 0 799.994 -0.08
#> 1602474 5700 1 799.994 0.01
#> 1618032 5700 5 799.994 0.07
#> 1630997 5700 10 799.994 -0.01
#> 1646555 5700 20 799.994 -0.11
#> 1659520 5700 50 799.994 0.03
#> 1675078 5700 100 799.994 -0.06
#> 1688043 5700 200 799.994 0.21
#> 1703601 5700 300 799.994 0.31
#> 1716566 5700 400 799.994 0.48
#> 1732124 5700 500 799.994 0.47
#> 1745089 5700 600 799.994 0.46
#> 1760647 5700 700 799.994 0.32
#> 1773612 5700 800 799.994 0.68
#> 1789170 5700 900 799.994 0.39
#> 1802135 5700 1000 799.994 0.58
#> [1] "The target irradiance is 0"
#> [1] "The closest intensity is 0"
#> [1] "---"
rm(sanity_check)
These closest intensities are used in the next step.
This step predicts the intensities to use, with a system of linear equations or non-negative least squares (a fancy version of SLE).
First, it uses the closest intensities and calibration data to create a matrix of irradiances for each event. Think of this as a heatmap with all of the bleedthrough between all the channels of the LED at the closest intensity. Again, we’ll show this for the first event:
# Define variables
peakWavelengths <- LightFitR:::internal.closestWavelength(unique(calibration$wavelength), helio.dyna.leds[-9, 'wavelength'])
#> Warning in
#> LightFitR:::internal.closestWavelength(unique(calibration$wavelength), : We
#> couldn't find exact matches with the peak wavelengths specified. Returning the
#> closest wavelengths
firstEvent <- data.frame(led=LightFitR::helio.dyna.leds[-9, 'wavelength'], closest=closest_first[-9], intended=target_first[-9])
rm(closest_first, target_first)
# closest matrix
mat <- sapply(1:nrow(firstEvent), function(j){
criteria <- (calibration$led == firstEvent[j, 'led']) & (calibration$intensity == firstEvent[j, 'closest']) & (calibration$wavelength %in% peakWavelengths) # We want the irradiances (from calibration data) of each LED at the intensity where it is closest to the intended irradiance
calibration[criteria, 'irradiance']
})
print(mat)
#> [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
#> [1,] 2.79 0.11 0.01 0.08 -0.03 -0.03 -0.01 0.00
#> [2,] 0.41 4.81 0.35 0.08 0.01 -0.03 -0.03 -0.01
#> [3,] 0.02 4.66 14.59 1.04 -0.08 -0.08 -0.02 -0.08
#> [4,] 0.11 0.29 0.95 19.67 0.05 -0.06 0.00 0.03
#> [5,] 0.02 0.04 0.04 0.07 3.44 0.01 -0.01 0.00
#> [6,] -0.04 -0.01 -0.04 0.01 0.01 0.21 0.14 -0.03
#> [7,] 0.08 0.00 -0.01 0.02 0.00 -0.03 4.07 0.01
#> [8,] 0.11 0.07 -0.02 0.18 -0.02 0.06 0.06 1.01
image(mat)
The leading diagonal is quite intense (which is what we want!). But as you can see, there is some bleedthrough between the channels, indicated by colour outside the leading diagonal.
Next, we make a model either using NNLS or SLE (more on the differences below). It solves the equation Ax=b, where A is the closest irradiance matrix (above), b are the target irradiances and x are unknown coefficients.
For the first event, it looks like this:
mod <- nnls::nnls(mat, firstEvent[,'intended'])
print(mod)
#> Nonnegative least squares model
#> x estimates: 0.9626349 0.8614317 0.6413636 0.985943 1.236892 0.6350962 0.9638369 0.5920488
#> residual sum-of-squares: 0
#> reason terminated: The solution has been computed sucessfully.
Finally, we use the coefficients (x, solved by the model) as well as the closest intensities to calculate the intensities we need to set the lights to:
intensities <- mod$x * firstEvent[, 'closest']
print(intensities)
#> [1] 962.634923 516.859030 384.818139 492.971513 61.844577 6.350962 48.191847
#> [8] 11.840976
Great! We have our intensities! We can put that straight into the lights right?
Well, most lights only accept whole numbers. And, although not apparent in this example, sometimes we predict intensities which are impossible for the lights (e.g. above 1000 in the case of Heliospectra DYNAs). So this brings us onto the next step!
This is exactly as it sounds. We round our predicted intensities to the nearest integer, cap the predicted intensities to the maximum the lights can achieve (inferred from the calibration data), set any negative predictions to 0.
tidied <- LightFitR:::internal.tidyIntensities(intensities, calib_intensities)
print(tidied)
#> [1] 963 517 385 493 62 6 48 12
Much better!
This final step just takes the resultant matrix and makes it more human readable. It adds row and column names, as well as the event timepoints. You can’t really show it with just the first event, and you already saw the tidied matrix in the ‘get started’ example. So there isn’t really any code to show for this step…
From our testing, there isn’t much of a difference between SLE and NNLS when it comes to predicted intensities (https://doi.org/10.1101/2025.06.06.658293), except for occasional outliers. So if you’re not happy with the intensities predicted by one method, try the other. The default for the package is NNLS but this was an arbitrary decision on our part.
This is the ‘simpler’ of the two methods. It solves the equation Ax=b straightforwardly. However, for us, this means it can predict intensities below 0 (i.e. unachievable by the lights since 0 means the light is off) and intensities above the maximum (again, impossible on the lights).
This uses the Lawson-Hanson method to solve Ax=b in such a way that
the predicted intensities are non-negative. This gets around the below 0
predictions, but can still predict intensities above the maximum. See
the nnls
package for more info on how NNLS works.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.