Skip to contents
library(picoinjector)
library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
library(ggplot2)
library(wesanderson)

Picoinjecting black ink and recording videos at 4000 fps

Black ink was injected into ~ 30 µm droplets and videos were recorded using a high-speed camera. The footage was processed using ImageJ and MATLAB in order to detect droplets and determine their size. The sizes of recognized droplets were stored in text files.

Read the data:

files <- list.files("data/fads/221216/ink/", pattern = "_kymograph_droplets.txt$", full.names = TRUE)
droplets <- tibble::tibble()
for (f in files) {
  droplets <- droplets %>% 
    dplyr::bind_rows(
      readr::read_table(f, show_col_types = FALSE) %>% 
        dplyr::mutate("file" = stringr::str_extract(basename(f), "\\d{8}_\\d{6}")) %>% 
        # normalize intensity by droplet width
        dplyr::mutate("intensity" = intensity/width)
    )
}

# add metadata
droplets <- droplets %>% 
  dplyr::left_join(
    readr::read_table("data/fads/221216/ink/metadata.txt", show_col_types = FALSE),
    by = "file"
  )

Now, plot the measured droplet sizes and check if they make sense:

droplets %>% 
  filter(ac_field == TRUE) %>% 
  mutate("q_emulsion" = as.factor(q_emulsion),
         "p_oil" = as.factor(p_oil),
         "p_water" = as.factor(p_water)) %>% 
  ggplot(aes(x = width)) + 
  geom_histogram(aes(fill = p_water), alpha = 0.3, position = "identity") + 
  facet_grid(p_oil ~ q_emulsion, labeller = label_both, scales = "free") + 
  theme_pretty_thesis()
#> `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Single-file level

Summarize the droplet populations for every file:

dropletSummary <- droplets %>% 
  dplyr::group_by(file, p_oil, q_emulsion, p_water, ac_field) %>% 
  dplyr::summarise("mean_diameter" = mean(width),
                   "median_diameter" = median(width),
                   "sd_diameter" = sd(width),
                   "intensity_mean" = mean(intensity),
                   "intensity_median" = median(intensity),
                   "intensity_sd" = sd(intensity),
                   "n_droplets" = dplyr::n())
#> `summarise()` has grouped output by 'file', 'p_oil', 'q_emulsion', 'p_water'.
#> You can override using the `.groups` argument.
dropletSummary
#> # A tibble: 14 × 12
#> # Groups:   file, p_oil, q_emulsion, p_water [14]
#>    file    p_oil q_emu…¹ p_water ac_fi…² mean_…³ media…⁴ sd_di…⁵ inten…⁶ inten…⁷
#>    <chr>   <dbl>   <dbl>   <dbl> <lgl>     <dbl>   <dbl>   <dbl>   <dbl>   <dbl>
#>  1 202212…   260      35     131 FALSE      31.2    30      5.27    327.    331.
#>  2 202212…    80      15      60 TRUE       33.8    33.5    1.73    128.    129.
#>  3 202212…   115      15      60 TRUE       32.0    32.2    2.12    131.    136.
#>  4 202212…   115      15      70 TRUE       34.8    34.8    1.76    126.    129.
#>  5 202212…   115      15      80 TRUE       37.8    37.8    1.70    114.    115.
#>  6 202212…   130      15      80 TRUE       36.0    36.1    1.80    106.    107.
#>  7 202212…   130      25      80 TRUE       34.7    35.2    2.43    130.    130.
#>  8 202212…   145      25      80 TRUE       33.3    33.5    3.71    160.    154.
#>  9 202212…   145      25      70 TRUE       31.0    31.7    2.72    197.    187.
#> 10 202212…   145      25      70 TRUE       32.1    31.7    5.06    246.    234.
#> 11 202212…   145      35      70 TRUE       33.8    33.9    2.84    135.    134.
#> 12 202212…   160      35      70 TRUE       32.6    33.5    3.00    145.    137.
#> 13 202212…   160      35      70 TRUE       31.5    32.2    3.12    134.    126.
#> 14 202212…   160      35      70 TRUE       30.9    30.9    3.71    139.    135.
#> # … with 2 more variables: intensity_sd <dbl>, n_droplets <int>, and
#> #   abbreviated variable names ¹​q_emulsion, ²​ac_field, ³​mean_diameter,
#> #   ⁴​median_diameter, ⁵​sd_diameter, ⁶​intensity_mean, ⁷​intensity_median

The median diameter of uninjected droplets is 30 µm.

Since pressure and flow rate are related linearly in the Hagen-Poisseuille model, I will summarize the two variables into a single ratio p_oil/q_emulsion I somehow want to express the droplet velocity in terms of p_oil and q_emulsion. For q_emulsion it would be straightforward, but the spacing oil is driven by pressure, which makes the flow rate harder to estimate. I will just use the Hagen-Poisseuille law with a hydraulic resistance value from literature to convert pressure into flow rate, and then add it to the emulsion flow rate.

\[R_h =\frac{12 \eta L}{1-0.63(h/w)} \frac{1}{h^3w}\]

For the dynamic viscosity I use that of FC-40 fluorinert oil which according to this source is 4.1 mPa s. For the channel dimensions I use L = 0.5 mm, h = 30 µm, w = 20 µm (approximate length of the most confined channel segment)

r_h <-  12 * 4.1e-3 * 0.5e-3 /((1-0.63*(30e-6 / 20e-6))*(30e-6)^3 * 20e-6)
dropletSummary <- dropletSummary %>% 
  dplyr::mutate("q_oil_est" = p_oil * 1e2/r_h * 3.6e12) # in ml/h

Idea: emulsion flow rate negligible compared to spacing oil flow rate, which is usually at least five times larger. Therefore, express p_oil and p_injection as a ratio:

get_volume_change <- function(diameter1, diameter0) {
  return(1/6 * pi * (diameter1^3 - diameter0^3))
}
pal <- wes_palette("Zissou1", 100, type = "continuous")
dropletSummary %>% 
  filter(ac_field == TRUE) %>% 
  mutate("water_oil_ratio" = p_water/p_oil,
         "injected_volume" = sapply(median_diameter * 1e-6, get_volume_change, 30e-6) * 1e15) %>% 
  ggplot(aes(x = water_oil_ratio, y = injected_volume)) + 
  scale_fill_gradientn(colours = pal) + 
  geom_smooth(method = "lm", formula = y~x, linetype = "dashed", color = "black", linewidth = 0.5, se = FALSE) +  
  # geom_hline(yintercept = 30, linetype = "dashed") + 
  geom_point(aes(fill = q_emulsion), shape = 21, size = 4) + 
  theme_pretty_thesis() + 
  labs(x = expression(p["injection"]/p["oil"]),
       y = "Injected volume [pL]",
       fill = "Emulsion flow rate [µl/h]",
       title = "Picoinjected volumes are largest at low throughput and relatively high injection pressure") + 
  ylim(c(0,15)) + 
  guides(fill = guide_legend(title.position = "top", direction = "horizontal"))

Lets investigate the grey intensities.

dropletSummary %>% 
  filter(ac_field == TRUE) %>% 
  mutate("water_oil_ratio" = p_water/p_oil,
         "injected_volume" = sapply(median_diameter * 1e-6, get_volume_change, 30e-6) * 1e15) %>% 
  ggplot(aes(x = injected_volume, y = intensity_median)) + 
  geom_smooth(method = "lm", formula = y~x, linetype = "dashed", color = "black", linewidth = 0.5, se = FALSE) +  
  # geom_hline(yintercept = 30, linetype = "dashed") + 
  geom_point(aes(fill = water_oil_ratio), shape = 21, size = 4) + 
  # scale_fill_viridis_c() +
  scale_fill_gradientn(colours = pal) +
  theme_pretty_thesis() + 
  labs(x = "Injected volume [pL]",
       y = "Median droplet gray value",
       title = "Pixel intensity decreases when picoinjecting ink",
       fill = expression(p["injection"]/p["oil"])) + 
  theme(legend.position = c(1,1),
        legend.justification = c(1,1))

  # guides(fill = 
  #          guide_legend(title.position = "top", direction= "horizontal", ))

Information I can take from this plot:

  • with larger injection volume, more ink is injected, the pixel intensity decreases
  • high variability
# library(ggridges)
# 
# droplets %>% 
#   group_by(file) %>%
#   filter(width > quantile(width, 0.1) & width < quantile(width, 0.9)) %>% # remove outliers
#   ungroup() %>% 
#   ggplot(aes(x = width, y = file)) + 
#   scale_fill_gradientn(colours = pal) +
#   geom_vline(xintercept = 30) + 
#   geom_density_ridges(aes(fill = p_water/p_oil)) + 
#   theme_pretty_thesis() + facet_wrap(~factor(q_emulsion), ncol = 1)

Picoinjecting Atto488 into resorufin droplets (2023-01-26)

In order to characterize the picoinjector operating range, 30 µm resorufin droplets were injected with Att488 dye. Fluorescence activities of the droplets were measured during operation using the FADS-setup. Waveform files corresponding to the signals arriving at the two PMT tubes were recorded and analyzed using MATLAB code, and TSV files containing information of the droplets identified in the signal were generated. These will be analysed in this vignette.

Single-droplet level

Load the TSV data. Each row corresponds to a droplet. Droplet populations resulting from 34 different parameter combinations were collected. I exclude file Signal_29 because picoinjection didn’t work well for those parameters.

greenAll <- fads_read_all("data/fads/230126/", "_barcode.txt$") %>% 
  dplyr::filter(file != "Signal_29") 
redAll <- fads_read_all("data/fads/230126/", "_injected.txt$") %>% 
  dplyr::filter(file != "Signal_29") 
greenAll
#> # A tibble: 2,651,225 × 11
#>    t_start   t_max   t_end voltage   width spacing    area injec…¹    id occup…²
#>      <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl> <dbl>   <dbl>
#>  1 0.00288 0.00361 0.00422 0.00600 0.00133 0.00314 5.82e-6       0     1       0
#>  2 0.00736 0.00816 0.00926 0.0108  0.0019  0.00218 1.22e-5       1     2       1
#>  3 0.0114  0.0122  0.0128  0.00634 0.00133 0.00192 6.21e-6       0     3       0
#>  4 0.0147  0.0154  0.0160  0.00652 0.00132 0.00140 6.23e-6       0     4       0
#>  5 0.0174  0.0182  0.0187  0.00701 0.00127 0.00164 6.25e-6       1     5       1
#>  6 0.0203  0.0210  0.0216  0.00674 0.00130 0.0014  6.27e-6       1     6       1
#>  7 0.0230  0.0238  0.0244  0.00680 0.00132 0.00194 6.48e-6       1     7       1
#>  8 0.0263  0.0270  0.0276  0.00662 0.00128 0.00140 6.09e-6       1     8       1
#>  9 0.0290  0.0297  0.0302  0.00704 0.00123 0.00203 6.21e-6       1     9       1
#> 10 0.0322  0.0329  0.0335  0.00631 0.00128 0.00154 5.68e-6       1    10       1
#> # … with 2,651,215 more rows, 1 more variable: file <chr>, and abbreviated
#> #   variable names ¹​injected, ²​occupancy

The information for which times the AC field was on/off, as well as pressures and flow rates are stored in a metadata text file:

metadata <- readr::read_table("data/fads/230126/metadata.txt", show_col_types = FALSE)
metadata
#> # A tibble: 34 × 9
#>    file     q_oil p_water q_emulsion ac_v_pp ac_frequency t_off video    laser…¹
#>    <chr>    <dbl>   <dbl>      <dbl>   <dbl>        <dbl> <dbl> <chr>      <dbl>
#>  1 Signal_0   150     125         25       5           50 103.  2023012…       1
#>  2 Signal_1   150     115         25       5           50  95.3 2023012…       1
#>  3 Signal_2   150     105         25       5           50  85.9 2023012…       1
#>  4 Signal_3   150      95         25       5           50 112.  2023012…       1
#>  5 Signal_4   150      85         25       5           50  99.4 2023012…       1
#>  6 Signal_5   150     130         25       5           50  91.9 2023012…       2
#>  7 Signal_6   150     140         25       5           50  65.2 2023012…       2
#>  8 Signal_7   300     120         50       5           50  42.4 2023012…       2
#>  9 Signal_8   300     130         50       5           50  51.2 2023012…       2
#> 10 Signal_9   300     140         50       5           50  27.3 2023012…       2
#> # … with 24 more rows, and abbreviated variable name ¹​laser_focus

Convert the generator signal voltage to the amplified peak-to-peak voltage applied to the electrodes. For that I use the linear regression fit calculated in vignette("electrical_engineering").

metadata <- metadata %>% 
  dplyr::mutate("ac_v_pp" = ac_v_pp * 52.08425) %>% 
  # also remove the 'video' column
  dplyr::select(-video)

Using the information on flow rates as well as the channel dimensions, I can furthermore estimate the droplet velocity at which the droplets pass the injection junction by simply dividing the total flow rate (i.e. emulsion + spacing oil) by the known microchannel cross section. To account for the different units I use 1 µl = 1e9 µm³.

metadata <- metadata %>% 
  dplyr::mutate("velocity" = ((q_emulsion+q_oil) * 1e9/3600)/(20*30)*1e-6) # m/s

Now I want to join the metadata to the signal data.

grn <- greenAll %>% 
  left_join(metadata, by = "file") %>% 
  dplyr::mutate("ac_field" = case_when(t_max < t_off ~ TRUE,
                                             .default = FALSE))
red <- redAll %>% 
  left_join(metadata, by = "file") %>% 
  dplyr::mutate("ac_field" = case_when(t_max < t_off ~ TRUE,
                                             .default = FALSE))

Furthermore, by knowing the droplet velocity, I can go transform the signal width, which is a period of time, into an actual spatial width in units of length. To that end, I have to multiply the peak width by the droplet velocity to get the spatial length of the droplet.

grn <- grn %>% 
  dplyr::mutate("width_um" = width * velocity * 1e6)
red <- red %>% 
  dplyr::mutate("width_um" = width * velocity * 1e6)
grn %>% 
  dplyr::select(file, q_emulsion, q_oil, p_water, ac_field, width , width_um)
#> # A tibble: 2,651,225 × 7
#>    file     q_emulsion q_oil p_water ac_field   width width_um
#>    <chr>         <dbl> <dbl>   <dbl> <lgl>      <dbl>    <dbl>
#>  1 Signal_0         25   150     125 TRUE     0.00133    108. 
#>  2 Signal_0         25   150     125 TRUE     0.0019     154. 
#>  3 Signal_0         25   150     125 TRUE     0.00133    108. 
#>  4 Signal_0         25   150     125 TRUE     0.00132    107. 
#>  5 Signal_0         25   150     125 TRUE     0.00127    103. 
#>  6 Signal_0         25   150     125 TRUE     0.00130    105. 
#>  7 Signal_0         25   150     125 TRUE     0.00132    107. 
#>  8 Signal_0         25   150     125 TRUE     0.00128    103. 
#>  9 Signal_0         25   150     125 TRUE     0.00123     99.7
#> 10 Signal_0         25   150     125 TRUE     0.00128    104. 
#> # … with 2,651,215 more rows

Obviously, these numbers are too large to represent droplet diameters. One must account for the fact that droplets are still squeezed in the microchannel, and appear therefore much longer.

We can convert the measured squeezed squeezed length into an equivalent spherical droplet diameter: In the confined picoinjection junction, 30µm droplets are squeezed into an elongated shape that can be approximated by an elliptical cylinder with half-ellipsoids on each end. The volume of an ellipsoid with semi-axes \(a, b, c\) is \[V_e = \frac{4}{3} \pi a b c.\]

The volume of an elliptical cylinder with semi-axes \(a, b\) and length \(L\) computes as \[V_c = \pi a b L.\]

The droplet volume can than be approximated by \(V_d = V_e + V_c\): \[V_d = V_e + V_c = \frac{4}{3} \pi a b c + \pi a b L.\] Lets say \(c\) is the ellipsoid radius along the axis in direction of the channel, \(a\) the radius in the lateral direction (i.e. half of the channel width), and \(b\) the radius in z-direction (i.e. half of the channel height). The signal measured by the PMT then corresponds to the full length of the droplet \(L+2c\).

Assuming \(c = \frac{1}{2}(a+b)\) and rearranging for \(V_d\) yields: \[V_d = \pi a b \left( \frac{2}{3} (a+b) + L \right).\]

The radius of the droplet in a perfectly spherical shape can then be found as \[r = \left(\frac{3}{4} \frac{V_d}{\pi}\right)^{1/3}.\]

squeezed_length_to_diameter <- function(signalWidth, channelWidth, channelHeight) {
  # if the droplet is not squeezed, l is already the diamteer of a spheere
  a <- channelWidth/2
  b <- channelHeight/2
  if (signalWidth < a+b) {
    return(signalWidth)
  }
  # else...
  # get volume of the corresponding half sphere-cylinder
  vSphere = pi * a * b * (2/3*(a+b) + signalWidth-a-b)
  # return diameter of a same-volume sphere
  return(2*(3/4 * vSphere/pi)^(1/3))
}

# inverse function...
diameter_to_squeezed_length <- function(diameter, channelWidth, channelHeight) {
  # if the droplet is not squeezed, l is already the diameter of a spheere
  a <- channelWidth/2
  b <- channelHeight/2
  if (diameter < a+b) {
    return(diameter)
  }
  # else...
  # get volume of the corresponding half sphere-cylinder
  vsphere = 4/3 * pi *(diameter/2)^3
  signalWidth <- vsphere/(pi*a*b) + 1/3 * (a+b)
  return(signalWidth)
}
tibble::tibble("signal" = seq(10,100)) %>% 
  dplyr::mutate("diameter" = sapply(signal, squeezed_length_to_diameter, 20, 30)) %>% 
  ggplot(aes(x = signal, y = diameter)) + 
  geom_line() + 
  theme_pretty() +
  labs(title = "Droplet diameter as function of PMT signal spatial width. For unsqueezed droplets, the signal is equal to the diameter.",
       x = "PMT signal width converted into [µm]",
       y = "Corresponding sphere diameter [µm]")

tibble::tibble("diameter" = seq(10,45)) %>% 
  dplyr::mutate("length" = sapply(diameter, diameter_to_squeezed_length, 20, 30)) %>% 
  ggplot(aes(x = length, y = diameter)) + 
  geom_line() + 
  theme_pretty() +
  labs(title = "Droplet diameter as function of PMT signal spatial width. For unsqueezed droplets, the signal is equal to the diameter.",
       x = "PMT signal width converted into [µm]",
       y = "Corresponding sphere diameter [µm]")

These calculated widths/diameters represent a flow rate normalization, therefore, the uninjected (i.e. field OFF) droplet populations should now all have the same width_um value, regardless of the flow rate. That is, if no measurement artifacts become important when scaling up the flow rates.

grn %>% 
  filter(ac_field == FALSE) %>%
  group_by(file, ac_field) %>% 
  summarise(median_width = median(width_um)) %>% 
  mutate("corresponding_sphere_diameter" = sapply(median_width, squeezed_length_to_diameter, 20, 30)) %>% 
  arrange(median_width)
#> `summarise()` has grouped output by 'file'. You can override using the
#> `.groups` argument.
#> # A tibble: 33 × 4
#> # Groups:   file [33]
#>    file      ac_field median_width corresponding_sphere_diameter
#>    <chr>     <lgl>           <dbl>                         <dbl>
#>  1 Signal_4  FALSE            55.1                          34.8
#>  2 Signal_3  FALSE            60.4                          36.0
#>  3 Signal_2  FALSE            62.8                          36.6
#>  4 Signal_1  FALSE            65.6                          37.2
#>  5 Signal_0  FALSE            67.2                          37.6
#>  6 Signal_24 FALSE            68.3                          37.8
#>  7 Signal_6  FALSE            70.1                          38.2
#>  8 Signal_26 FALSE            70.5                          38.3
#>  9 Signal_21 FALSE            70.6                          38.3
#> 10 Signal_20 FALSE            71.8                          38.5
#> # … with 23 more rows

Interesting: After recording signals 0-4, I had to re-adjust the laser focus due to chip movement. These recordings seem to cluster apart from the other measurements. Also: This measurement approach becomes inaccurate as the peak widths approach the resolution limit (see Signal33).

Reasons for droplets appearing larger then actual when mesured like this:

  • laser spot is not an ideal point but has spatial extent, therefore the PMTs receive a signal also, when the droplet is only partially there
  • peak widths are measured from the beginning of the rising edge until the end of the falling edge

(file, electric field state)-level

Summarize by groups:

grnSummary <- grn %>% 
  dplyr::group_by(file, ac_field) %>% 
  dplyr::summarise("width_mean" = mean(width),
                   "width_sd" = sd(width),
                   "width_median" = median(width),
                   "voltage_mean" = mean(voltage),
                   "voltage_sd" = sd(voltage),
                   "voltage_median" = median(voltage),
                   "area_mean" = mean(area),
                   "area_sd" = sd(area),
                   "area_median" = median(area),
                   "spacing_mean" = mean(spacing),
                   "spacing_sd" = sd(spacing),
                   "spacing_median" = median(spacing),
                   "frequency" = dplyr::n() / (max(t_max) - min(t_max)),
                   "injected" = mean(injected),
                   "n_droplets" = dplyr::n(),
                   .groups = "drop") %>% 
  dplyr::left_join(metadata, by = "file") %>% 
  dplyr::mutate("velocity" = ((q_emulsion+q_oil) * 1e9/3600)/(20*30)*1e-6) # m/s
redSummary <- red %>% 
  dplyr::group_by(file, ac_field) %>% 
  dplyr::summarise("width_mean" = mean(width),
                   "width_sd" = sd(width),
                   "width_median" = median(width),
                   "voltage_mean" = mean(voltage),
                   "voltage_sd" = sd(voltage),
                   "voltage_median" = median(voltage),
                   "area_mean" = mean(area),
                   "area_sd" = sd(area),
                   "area_median" = median(area),
                   "spacing_mean" = mean(spacing),
                   "spacing_sd" = sd(spacing),
                   "spacing_median" = median(spacing),
                   "frequency" = dplyr::n() / (max(t_max) - min(t_max)),
                   "is_false_droplet" = mean(is_false_drop),
                    "n_droplets" = dplyr::n(),
                   .groups = "drop") %>% 
  dplyr::left_join(metadata, by = "file") %>% 
  dplyr::mutate("velocity" = ((q_emulsion+q_oil) * 1e9/3600)/(20*30)*1e-6) # m/s

Plot droplet frequency (as derived from the number of droplets detected in the signal) vs. emulsion flow rate.

grnSummary %>% 
  ggplot(aes(q_emulsion, y = frequency)) + 
  geom_smooth(method = "lm", formula = y~x) + 
  geom_point(shape = 1) + 
  theme_pretty_thesis()

As expected, it can be seen that the droplet frequency depends linearly on the emulsion flow rate. Therefore, I can use it as a second axis when plotting against emulsion flow rate.

# get linear regression
m <- lm(frequency ~ q_emulsion, grnSummary)
m
#> 
#> Call:
#> lm(formula = frequency ~ q_emulsion, data = grnSummary)
#> 
#> Coefficients:
#> (Intercept)   q_emulsion  
#>       21.72        12.05

I find: frequency [Hz] = q_emulsion [µl/h] * 12.05 + 21.72. I neglect the offset.

grnSummary %>% 
  dplyr::mutate("width_um" = width_mean * velocity * 1e6) %>% 
  dplyr::select(file, q_emulsion, q_oil, p_water, ac_field, width_mean, width_um) %>% 
  dplyr::mutate("width_um" = sapply(width_um, squeezed_length_to_diameter, 20, 30))
#> # A tibble: 66 × 7
#>    file      q_emulsion q_oil p_water ac_field width_mean width_um
#>    <chr>          <dbl> <dbl>   <dbl> <lgl>         <dbl>    <dbl>
#>  1 Signal_0          25   150     125 FALSE      0.000839     37.7
#>  2 Signal_0          25   150     125 TRUE       0.00120      43.1
#>  3 Signal_1          25   150     115 FALSE      0.000830     37.6
#>  4 Signal_1          25   150     115 TRUE       0.00112      42.0
#>  5 Signal_10         50   300     150 FALSE      0.000483     39.8
#>  6 Signal_10         50   300     150 TRUE       0.000612     43.4
#>  7 Signal_11         50   300     160 FALSE      0.000484     39.8
#>  8 Signal_11         50   300     160 TRUE       0.000601     43.1
#>  9 Signal_12         50   300     170 FALSE      0.000472     39.4
#> 10 Signal_12         50   300     170 TRUE       0.000607     43.3
#> # … with 56 more rows

Next, I can determine the mean or median droplet diameter in the non-injected populations (i.e. AC field off) and use it to measure the volume increase, i.e. the injected volume. I do this on the droplet population level, and not for individual droplets.

Plot Q_emulsion (~droplet frequency) vs. p_injection

  • relate peak width to droplet dimensions by measuring droplet sizes in high-speed video of uninjected droplets.

Percentage of false droplets

redSummary %>% 
  ggplot(aes(x = is_false_droplet)) + 
  geom_histogram(aes(fill = ac_field), alpha = 0.5, position = "identity", bins = 30) + 
  theme_pretty_thesis() + 
  labs(x = "Percentage of false droplets",
       y = "Count", fill = "Electric field on") + 
  theme(legend.position = c(1,1), legend.justification = c(1,1))

Percentage of injected droplets:

grnSummary %>% 
  ggplot(aes(x = injected)) + 
  geom_histogram(aes(fill = ac_field), alpha = 0.5, position = "identity", bins = 30) + 
  theme_pretty_thesis() + 
  labs(x = "Percentage of injected droplets",
       y = "Count", fill = "Electric field on") + 
  theme(legend.position = c(1,1), legend.justification = c(1,1))

grnSummary %>% 
  # filter(q_emulsion != 300) %>% 
  ggplot(aes(x = velocity, y = p_water, color = injected, shape = ac_field)) + 
  geom_point(size = 2, position = position_dodge2(width = 0.01)) + 
  scale_colour_viridis_c() + 
  # scale_color_gradientn(colours = pal) + 
  theme_pretty_thesis() 

Single-file level

Consolidate electric field and channel into single rows:

  • false_drops = percentage of false drops when field is on
  • false_injections = percentage of injected drops when field is off
  • leakage = percentage of false drops when field is off
  • delta_width = median width field on minus field off
# extract the base information from grnSummary
drops <- grnSummary %>% 
  dplyr::filter(ac_field == TRUE) %>% 
  dplyr::select(c(file, q_oil, q_emulsion, p_water, velocity, frequency, ac_v_pp, ac_frequency)) %>% 
  dplyr::left_join(
    
# get information about INJECTED droplets, i.e. channel == green and injected > 0 (and probably field == off for most cases)
grn %>% 
  dplyr::filter(injected == 1) %>% 
  dplyr::group_by(file) %>% 
  dplyr::summarise("width_green_injected" = median(width),
                   "voltage_green_injected" = median(voltage),
                   "area_green_injected" = median(area),
                   .groups = "drop"),
  by = "file") %>% 
  dplyr::left_join(

# get information about NON-injected droplets
grn %>% 
  dplyr::filter(injected == 0) %>% 
  dplyr::group_by(file) %>% 
  dplyr::summarise("width_green_noninjected" = median(width),
                   "voltage_green_noninjected" = median(voltage),
                   "area_green_noninjected" = median(area),
                   .groups = "drop"),
  by = "file") %>% 
  dplyr::left_join(
    
# get information about green drops when field is on
grn %>% 
  dplyr::filter(ac_field == TRUE) %>% 
  dplyr::group_by(file) %>% 
  dplyr::summarise("n_droplets_on" = dplyr::n(),
                   "missed_injections" = sum(!injected) / dplyr::n()), # missed injection = field on, but not injected
  by = "file") %>% 
  dplyr::left_join(
    
# get information about green drops when field is off
grn %>% 
  dplyr::filter(ac_field == FALSE) %>% 
  dplyr::group_by(file) %>% 
  dplyr::summarise("n_droplets_off" = dplyr::n(), # number of green peaks when field is off
                   "false_injections" = sum(injected) / dplyr::n()), # false injections = field off, still injected
  by = "file") %>% 
  dplyr::left_join(

    
# get information about red droplets
red %>% 
  dplyr::filter(ac_field == TRUE) %>% 
  dplyr::group_by(file) %>% 
  dplyr::summarise("n_false_droplets" = sum(is_false_drop)), # false droplets = number of de-novo red droplets
  by = "file") %>% 
  dplyr::left_join(
    
# droplet leakage and false injections
red %>% 
  dplyr::filter(ac_field == FALSE) %>% 
  dplyr::group_by(file) %>% 
  dplyr::summarise("n_leaked_droplets" = sum(is_false_drop), # leaked droplets = number of de-novo red droplets
                   "n_red_droplets_off" = dplyr::n()
                   ),
  by = "file"
)

Derive some further metrics from these data:

drops <- drops %>% 
  # calculate percentage of false/leaked droplets
  dplyr::mutate("false_droplets" = n_false_droplets/n_droplets_on,
                "leaked_droplets" = n_leaked_droplets/n_droplets_off) %>% 
  # compare injected vs. non-injected droplets
  dplyr::mutate("delta_width" = width_green_injected - width_green_noninjected,
                "delta_voltage" = voltage_green_injected - voltage_green_noninjected,
                "delta_area" = area_green_injected - area_green_noninjected,
                ) %>% 
  # convert signal widths into spatial widths
  dplyr::mutate("width_green_injected_um" = width_green_injected * velocity * 1e6,
                "width_green_noninjected_um" = width_green_noninjected * velocity * 1e6,
                "delta_width_um" = delta_width * velocity * 1e6) %>% 
  # convert spatial width into droplet diameter
  dplyr::mutate("diameter_injected" = sapply(width_green_injected_um, squeezed_length_to_diameter, 20, 30),
                "diameter_noninjected" = sapply(width_green_noninjected_um, squeezed_length_to_diameter, 20, 30)) %>% 
  # express injection pressure as ratio
  dplyr::mutate("p_injection_relative" = p_water/(q_emulsion + q_oil))
drops %>% 
  ggplot(aes(x = p_injection_relative, y = delta_width_um)) + 
  geom_point(aes(fill = q_emulsion + q_oil), shape = 21, size = 4) +
  scale_fill_gradientn(colours = pal, trans = "log",
                       breaks = unique(drops$q_emulsion + drops$q_oil)) +
  theme_pretty_thesis() + 
  guides(fill = guide_legend(direction = "horizontal", title.position = "top", byrow = T)) + 
  labs(x = expression("Relative injection pressure "* p["injection"]/Q["total"] * " [mbar/µL/h]"),
       y = "Delta width [µm]",
       fill = "Total flowrate [µL/h]")

grDevices::pdf("lel.pdf", width=16.5*2, height = 11.7*2)
drops %>% 
  dplyr::select(c(-ac_v_pp, -ac_frequency, -n_droplets_on, -n_droplets_off, -n_false_droplets, -n_leaked_droplets, n_red_droplets_off, -delta_width, -width_green_injected, -width_green_noninjected, -file)) %>% 
  plot()
grDevices::dev.off()

Use additional imaging data to correct the droplet sizes derived from PMT signals and flow rates

Read droplet sizes derived from image analysis and compare:

files <- list.files("data/fads/230126", pattern = "_kymograph_droplets.txt$", full.names = TRUE)
kymographDrops <- tibble::tibble()
for (f in files) {
  kymographDrops <- kymographDrops %>% 
    dplyr::bind_rows(
      readr::read_table(f, show_col_types = FALSE) %>% 
        dplyr::mutate("file" = stringr::str_extract(basename(f), "\\d{8}_\\d{6}")) %>% 
        # normalize intensity by droplet width
        dplyr::mutate("intensity" = intensity/width)
    )
}

# add metadata
kymographDrops <- kymographDrops %>% 
  dplyr::rename(c("video" = "file")) %>% 
  dplyr::left_join(
    readr::read_table("data/fads/230126/metadata.txt", show_col_types = FALSE),
    by = "video"
  )
 kymographDrops
#> # A tibble: 1,769 × 14
#>    slice  left right width intensity video   file  q_oil p_water q_emu…¹ ac_v_pp
#>    <dbl> <dbl> <dbl> <dbl>     <dbl> <chr>   <chr> <dbl>   <dbl>   <dbl>   <dbl>
#>  1     5  16.5  58.3  41.7      325. 202301… Sign…   150     125      25       5
#>  2    13  41.7  82.2  40.4      308. 202301… Sign…   150     125      25       5
#>  3    32  16.1  57.8  41.7      326. 202301… Sign…   150     125      25       5
#>  4    39  41.7  82.2  40.4      321. 202301… Sign…   150     125      25       5
#>  5    46  16.5  57.8  41.3      320. 202301… Sign…   150     125      25       5
#>  6    53  41.7  82.2  40.4      316. 202301… Sign…   150     125      25       5
#>  7    59  15.2  57.0  41.7      329. 202301… Sign…   150     125      25       5
#>  8    67  41.7  82.2  40.4      311. 202301… Sign…   150     125      25       5
#>  9    87  14.3  55.2  40.9      322. 202301… Sign…   150     125      25       5
#> 10    94  41.7  82.2  40.4      312. 202301… Sign…   150     125      25       5
#> # … with 1,759 more rows, 3 more variables: ac_frequency <dbl>, t_off <dbl>,
#> #   laser_focus <dbl>, and abbreviated variable name ¹​q_emulsion

Summarize these into important statistics

diametersKymo <- kymographDrops %>%
  dplyr::filter(!is.na(file)) %>% 
  dplyr::group_by(file, video, laser_focus) %>% 
  dplyr::summarise("diameter_mean" = mean(width),
                   "diameter_median" = median(width),
                   "diameter_sd" = sd(width),
                   "n_droplets" = dplyr::n(),
                   .groups = "drop")
widthNotInjected <- kymographDrops %>%
  dplyr::filter(is.na(file)) %>% 
  dplyr::group_by(video) %>% 
  dplyr::summarise("diameter_mean" = mean(width)) %>% 
  dplyr::pull(diameter_mean) %>% 
  mean()

Correlate the two droplet diameter measurements. I am translating the measured droplet diameter into the corresponding equivalent droplet length, when the droplet is squeezed in the channel (since that’s where the laser measures its length).

diametersBoth <- drops %>% 
  dplyr::select(c(file, width_green_injected_um)) %>% 
  dplyr::left_join(diametersKymo, by = "file") %>% 
  dplyr::filter(!is.na(diameter_median)) %>% 
  # convert measured diameter in equivalent squeezed droplet length
  dplyr::mutate("equivalent_squeezed_length" = sapply(diameter_median, diameter_to_squeezed_length, 20, 30))
diametersBoth %>% 
  ggplot(aes(x = equivalent_squeezed_length, y = width_green_injected_um, color = as.factor(laser_focus))) + 
  geom_abline(slope = 1, intercept = 0) +
  geom_point(aes(size = n_droplets)) + 
  theme_pretty_thesis()

There seems to be a constant offset between droplet sizes measured from images, and those derived from the PMT signals. After the first five measurements, I had to adjust the chip position slightly, and therefore the laser focus was slightly different in subsequent acquisitions. Therefore, I will use two different width-correction values for the two groups.

The average offset is:

diametersBoth %>% 
  dplyr::group_by(laser_focus) %>% 
  dplyr::summarise("y_intercept" = mean(width_green_injected_um - 1* equivalent_squeezed_length))
#> # A tibble: 2 × 2
#>   laser_focus y_intercept
#>         <dbl>       <dbl>
#> 1           1        8.68
#> 2           2       26.2

That means, the signal peaks measured by the PMT tubes and as detected by the MATLAB code are on average 26.18 (or 8.68 in the first laser focus spot configuration) µm too wide. I will therefore subtract this value from the signal length, before computing the corresponding droplet volume.

dropsCorrected <- drops %>% 
  dplyr::select(c(-width_green_injected, -width_green_noninjected, -delta_width)) %>% 
  dplyr::mutate(dplyr::case_when(file %in% c("Signal_0", "Signal_1", "Signal_2", "Signal_3", "Signal_4") ~ dplyr::across(c(width_green_injected_um, width_green_noninjected_um), ~ .x -8.68),
                                 .default = dplyr::across(c(width_green_injected_um, width_green_noninjected_um), ~ .x -26.19))) %>% 
  # convert squeezed width into sphere diameter
  dplyr::mutate("diameter_injected" = sapply(width_green_injected_um, squeezed_length_to_diameter, 20, 30),
                "diameter_noninjected" = sapply(width_green_noninjected_um, squeezed_length_to_diameter, 20, 30)) 

Actually, there is a second factor that need to be corrected for: When a droplet is being injected, the total flow rate in the channel increases, and therefore also the fluid velocity. This was not accounted for when calculating droplet velocities, and deriving spatial droplet sizes from that.

dropsCorrected %>% 
  ggplot(aes(x = p_water, y = diameter_noninjected)) + 
  geom_point()

Since theoretically pressure contributes linearly to flowrate, which, in turn is directly proportional to the fluid velocity, I correct for the additional flow rate component by scaling the width accordingly

Investigate the difference in signal width between injected and non-injected droplets:

For which parameter combinations picoinjection was possible, in general?

Other stuff

Filter outlier widths

Evaluate switching performance

Load RED signals

Summarize them for every file and field state and calculate percentage of false droplets.

Final plots

What do I wanna communicate with my plots:

  • what injected volumes can be expected
  • Injected volume consistency
  • switchability
  • flexibility in the injected volume
  • how to operate the picoinjector, which operating regimes are there? (max throughput, max percentage of injected droplets, min percentage of false droplets, high consistency of injected volume)

Plot the distribution of width values, but only for times were the AC field was switched on, i.e. droplets were being picoinjected and therefore their width is supposed to increase with increasing injection pressure.

Width distribution for every signal:

TODO

If I normalize the width by the droplet velocity, I should be able to superimpose the width distributions of all populations were the field was switched off, and find them at the same location.

Argument (if the signal-width-derived droplet size deviates more )